Merge lp:~gary/lazr.smtptest/threadsafety into lp:lazr.smtptest

Proposed by Gary Poster
Status: Merged
Merged at revision: 63
Proposed branch: lp:~gary/lazr.smtptest/threadsafety
Merge into: lp:lazr.smtptest
Diff against target: 103 lines (+37/-6)
3 files modified
src/lazr/smtptest/NEWS.txt (+11/-0)
src/lazr/smtptest/server.py (+25/-5)
src/lazr/smtptest/version.txt (+1/-1)
To merge this branch: bzr merge lp:~gary/lazr.smtptest/threadsafety
Reviewer Review Type Date Requested Status
Barry Warsaw Approve
Review via email: mp+67090@code.launchpad.net

Description of the change

Hi Barry. As we discussed, these changes make lazr.smtptest more thread-safe for other users of asyncore (such as Launchpad). I am hopeful that this will reduce spurious test failures in tests run in Launchpad's appserver layer. It seems to do the trick for the particular problem that I was trying to diagnose and solve (though the intermittent and opaque nature of these sorts of things means I only have statistics, not absolute confidence).

The addition of add_channel for the server and the channel are to work around the fact that the smtpd base classes (in Python 2.6 and 2.7) do not expose the asyncore map argument to the __init__. The options I saw were to copy over the __init__ code of the two classes, which seemed unattractive; or to try and come up with a hack that involved less code copying while trying to be as un-ugly as possible. add_channel is one of the few methods in the asyncore dispatcher that actually use the mapping, and the first one in the dispatcher's lifecycle to do so, so it seems a relatively reasonable an safe place to do the work, given the unfortunate circumstances. Other ideas welcome.

The other change I made was to actually close the sockets when the server is told to stop: before, we simply cleared out the map, without actually closing the sockets.

I still need to run the LP tests with these changes; I'm more suspicious of the second, smaller change causing a problem to LP than the first.

Thank you!

Gary

To post a comment you must log in.
Revision history for this message
Gary Poster (gary) wrote :

All LP tests pass with this branch.

Revision history for this message
Barry Warsaw (barry) wrote :

All the Mailman 3 tests passed with this branch.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/lazr/smtptest/NEWS.txt'
2--- src/lazr/smtptest/NEWS.txt 2009-07-07 22:47:59 +0000
3+++ src/lazr/smtptest/NEWS.txt 2011-07-06 21:31:16 +0000
4@@ -2,6 +2,17 @@
5 NEWS for lazr.smtptest
6 ======================
7
8+1.3 (2011-06-07)
9+================
10+
11+- Make the test server thread-safe with other code that starts an asyncore
12+ loop. Requires Python 2.6 or 2.7.
13+
14+- Be cleaner about stopping the server: before, it left sockets running
15+ ans simply cleared out the socket map. In an associated change, the EXIT
16+ smtp command sends the reply first, and then shuts down the server, rather
17+ than the other way around.
18+
19 1.2 (2009-07-07)
20 ================
21
22
23=== modified file 'src/lazr/smtptest/server.py'
24--- src/lazr/smtptest/server.py 2009-06-30 00:40:38 +0000
25+++ src/lazr/smtptest/server.py 2011-07-06 21:31:16 +0000
26@@ -41,14 +41,14 @@
27 """A channel that can reset the mailbox."""
28
29 def __init__(self, server, connection, address):
30+ self._server = server
31 smtpd.SMTPChannel.__init__(self, server, connection, address)
32- self._server = server
33
34 # pylint: disable-msg=W0613
35 def smtp_EXIT(self, argument):
36 """EXIT - a new SMTP command to cleanly stop the server."""
37+ self.push('250 Ok')
38 self._server.stop()
39- self.push('250 Ok')
40
41 def smtp_RSET(self, argument):
42 """RSET - hijack this to reset the server instance."""
43@@ -66,6 +66,16 @@
44 # Nothing here can affect the outcome.
45 pass
46
47+ def add_channel(self, map=None):
48+ # This has an old style base class. We want to make _map equal to
49+ # our server's socket_map, to make this thread safe with other
50+ # asyncores. We do it here, overriding the behavior of the asyncore
51+ # __init__ as soon as we can, without having to copy over code from
52+ # the rest of smtpd.SMTPChannel.__init__ and modify it.
53+ if self._map is not self._server.socket_map:
54+ self._map = self._server.socket_map
55+ smtpd.SMTPChannel.add_channel(self, map)
56+
57
58 class Server(smtpd.SMTPServer):
59 """An SMTP server."""
60@@ -80,10 +90,21 @@
61 """
62 self.host = host
63 self.port = port
64+ self.socket_map = {}
65 smtpd.SMTPServer.__init__(self, (host, port), None)
66 self.set_reuse_addr()
67 log.info('[SMTPServer] listening: %s:%s', host, port)
68
69+ def add_channel(self, map=None):
70+ # This has an old style base class. We want to make _map equal to
71+ # socket_map, to make this thread safe with other asyncores. We do
72+ # it here, overriding the behavior of the SMTPServer __init__ as
73+ # soon as we can, without having to copy over code from the rest of
74+ # smtpd.SMTPServer.__init__ and modify it.
75+ if self._map is not self.socket_map:
76+ self._map = self.socket_map
77+ smtpd.SMTPServer.add_channel(self, map)
78+
79 def handle_accept(self):
80 """Handle connections by creating our own Channel object."""
81 connection, address = self.accept()
82@@ -106,12 +127,11 @@
83 def start(self):
84 """Start the asyncore loop."""
85 log.info('[SMTPServer] starting asyncore loop')
86- asyncore.loop()
87+ asyncore.loop(map=self.socket_map)
88
89 def stop(self):
90 """Stop the asyncore loop."""
91- asyncore.socket_map.clear()
92- asyncore.close_all()
93+ asyncore.close_all(map=self.socket_map)
94 self.close()
95
96 # pylint: disable-msg=R0201
97
98=== modified file 'src/lazr/smtptest/version.txt'
99--- src/lazr/smtptest/version.txt 2009-09-04 19:06:38 +0000
100+++ src/lazr/smtptest/version.txt 2011-07-06 21:31:16 +0000
101@@ -1,1 +1,1 @@
102-1.2
103+1.3

Subscribers

People subscribed via source and target branches

to all changes: