Merge lp:~vila/bzr/test-server-races into lp:bzr

Proposed by Vincent Ladeuil
Status: Merged
Approved by: Martin Packman
Approved revision: no longer in the source branch.
Merged at revision: 6337
Proposed branch: lp:~vila/bzr/test-server-races
Merge into: lp:bzr
Diff against target: 131 lines (+27/-26)
2 files modified
bzrlib/tests/test_server.py (+12/-8)
bzrlib/tests/test_test_server.py (+15/-18)
To merge this branch: bzr merge lp:~vila/bzr/test-server-races
Reviewer Review Type Date Requested Status
Martin Packman (community) Approve
Review via email: mp+84096@code.launchpad.net

Commit message

Properly synchronize connection thread start with test server main thread.

Description of the change

This is a followup to the fix for bug #869366 that was sitting on disk for
no good reasons.

It removes one race in the test server when a connection thread is started.

Under rare circumstances (described in bug #869366) an exception in the
connection thread was raised in the server thread too early leading to
semi-hackish tests.

The connection thread will now wait for the server thread to resume its
normal course before processing the conection.

I see no reason why this should address the other issues we currently have
but I may be wrong.

On the other hand, this is clearly better and remove one source of possible
confusion.

To post a comment you must log in.
Revision history for this message
Martin Packman (gz) wrote :

Okay, this all makes sense to me given the earlier merge proposal context.

review: Approve
Revision history for this message
Vincent Ladeuil (vila) wrote :

sent to pqm by email

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'bzrlib/tests/test_server.py'
--- bzrlib/tests/test_server.py 2011-10-10 13:59:22 +0000
+++ bzrlib/tests/test_server.py 2011-12-01 12:49:45 +0000
@@ -432,12 +432,15 @@
432 def get_request(self):432 def get_request(self):
433 """Get the request and client address from the socket."""433 """Get the request and client address from the socket."""
434 sock, addr = TestingTCPServerMixin.get_request(self)434 sock, addr = TestingTCPServerMixin.get_request(self)
435 # The thread is not create yet, it will be updated in process_request435 # The thread is not created yet, it will be updated in process_request
436 self.clients.append((sock, addr, None))436 self.clients.append((sock, addr, None))
437 return sock, addr437 return sock, addr
438438
439 def process_request_thread(self, started, stopped, request, client_address):439 def process_request_thread(self, started, detached, stopped,
440 request, client_address):
440 started.set()441 started.set()
442 # We will be on our own once the server tells us we're detached
443 detached.wait()
441 SocketServer.ThreadingTCPServer.process_request_thread(444 SocketServer.ThreadingTCPServer.process_request_thread(
442 self, request, client_address)445 self, request, client_address)
443 self.close_request(request)446 self.close_request(request)
@@ -446,12 +449,13 @@
446 def process_request(self, request, client_address):449 def process_request(self, request, client_address):
447 """Start a new thread to process the request."""450 """Start a new thread to process the request."""
448 started = threading.Event()451 started = threading.Event()
452 detached = threading.Event()
449 stopped = threading.Event()453 stopped = threading.Event()
450 t = TestThread(454 t = TestThread(
451 sync_event=stopped,455 sync_event=stopped,
452 name='%s -> %s' % (client_address, self.server_address),456 name='%s -> %s' % (client_address, self.server_address),
453 target=self.process_request_thread,457 target = self.process_request_thread,
454 args=(started, stopped, request, client_address))458 args = (started, detached, stopped, request, client_address))
455 # Update the client description459 # Update the client description
456 self.clients.pop()460 self.clients.pop()
457 self.clients.append((request, client_address, t))461 self.clients.append((request, client_address, t))
@@ -460,12 +464,12 @@
460 t.set_ignored_exceptions(self.ignored_exceptions)464 t.set_ignored_exceptions(self.ignored_exceptions)
461 t.start()465 t.start()
462 started.wait()466 started.wait()
463 if debug_threads():
464 sys.stderr.write('Client thread %s started\n' % (t.name,))
465 # If an exception occured during the thread start, it will get raised.467 # If an exception occured during the thread start, it will get raised.
466 # In rare cases, an exception raised during the request processing may
467 # also get caught here (see http://pad.lv/869366)
468 t.pending_exception()468 t.pending_exception()
469 if debug_threads():
470 sys.stderr.write('Client thread %s started\n' % (t.name,))
471 # Tell the thread, it's now on its own for exception handling.
472 detached.set()
469473
470 # The following methods are called by the main thread474 # The following methods are called by the main thread
471475
472476
=== modified file 'bzrlib/tests/test_test_server.py'
--- bzrlib/tests/test_test_server.py 2011-10-10 13:59:22 +0000
+++ bzrlib/tests/test_test_server.py 2011-12-01 12:49:45 +0000
@@ -212,10 +212,12 @@
212212
213 class FailingDuringResponseHandler(TCPConnectionHandler):213 class FailingDuringResponseHandler(TCPConnectionHandler):
214214
215 # We use 'request' instead of 'self' below because the test matters
216 # more and we need a container to properly set connection_thread.
215 def handle_connection(request):217 def handle_connection(request):
216 req = request.readline()218 req = request.readline()
217 # Capture the thread and make it use 'caught' so we can wait on219 # Capture the thread and make it use 'caught' so we can wait on
218 # the even that will be set when the exception is caught. We220 # the event that will be set when the exception is caught. We
219 # also capture the thread to know where to look.221 # also capture the thread to know where to look.
220 self.connection_thread = threading.currentThread()222 self.connection_thread = threading.currentThread()
221 self.connection_thread.set_sync_event(caught)223 self.connection_thread.set_sync_event(caught)
@@ -232,20 +234,12 @@
232 # Check that the connection thread did catch the exception,234 # Check that the connection thread did catch the exception,
233 # http://pad.lv/869366 was wrongly checking the server thread which235 # http://pad.lv/869366 was wrongly checking the server thread which
234 # works for TestingTCPServer where the connection is handled in the236 # works for TestingTCPServer where the connection is handled in the
235 # same thread than the server one but is racy for237 # same thread than the server one but was racy for
236 # TestingThreadingTCPServer where the server thread may be in a238 # TestingThreadingTCPServer. Since the connection thread detaches
237 # blocking accept() call (or not).239 # itself before handling the request, we are guaranteed that the
238 try:240 # exception won't leak into the server thread anymore.
239 self.connection_thread.pending_exception()241 self.assertRaises(FailToRespond,
240 except FailToRespond:242 self.connection_thread.pending_exception)
241 # Great, the test succeeded
242 pass
243 else:
244 # If the exception is not in the connection thread anymore, it's in
245 # the server's one.
246 server.server.stopped.wait()
247 # The exception is available now
248 self.assertRaises(FailToRespond, server.pending_exception)
249243
250 def test_exception_swallowed_while_serving(self):244 def test_exception_swallowed_while_serving(self):
251 # We need to ensure the exception has been caught245 # We need to ensure the exception has been caught
@@ -260,9 +254,11 @@
260254
261 class FailingWhileServingConnectionHandler(TCPConnectionHandler):255 class FailingWhileServingConnectionHandler(TCPConnectionHandler):
262256
257 # We use 'request' instead of 'self' below because the test matters
258 # more and we need a container to properly set connection_thread.
263 def handle(request):259 def handle(request):
264 # Capture the thread and make it use 'caught' so we can wait on260 # Capture the thread and make it use 'caught' so we can wait on
265 # the even that will be set when the exception is caught. We261 # the event that will be set when the exception is caught. We
266 # also capture the thread to know where to look.262 # also capture the thread to know where to look.
267 self.connection_thread = threading.currentThread()263 self.connection_thread = threading.currentThread()
268 self.connection_thread.set_sync_event(caught)264 self.connection_thread.set_sync_event(caught)
@@ -270,6 +266,7 @@
270266
271 server = self.get_server(267 server = self.get_server(
272 connection_handler_class=FailingWhileServingConnectionHandler)268 connection_handler_class=FailingWhileServingConnectionHandler)
269 self.assertEquals(True, server.server.serving)
273 # Install the exception swallower270 # Install the exception swallower
274 server.set_ignored_exceptions(CantServe)271 server.set_ignored_exceptions(CantServe)
275 client = self.get_client()272 client = self.get_client()
@@ -284,8 +281,8 @@
284 # here). More precisely, the exception *has* been caught and captured281 # here). More precisely, the exception *has* been caught and captured
285 # but it is cleared when joining the thread (or trying to acquire the282 # but it is cleared when joining the thread (or trying to acquire the
286 # exception) and as such won't propagate to the server thread.283 # exception) and as such won't propagate to the server thread.
287 self.connection_thread.pending_exception()284 self.assertIs(None, self.connection_thread.pending_exception())
288 server.pending_exception()285 self.assertIs(None, server.pending_exception())
289286
290 def test_handle_request_closes_if_it_doesnt_process(self):287 def test_handle_request_closes_if_it_doesnt_process(self):
291 server = self.get_server()288 server = self.get_server()