Merge lp:~jameinel/bzr/2.5-soft-hangup-795025 into lp:bzr

Proposed by John A Meinel
Status: Merged
Approved by: John A Meinel
Approved revision: no longer in the source branch.
Merged at revision: 6170
Proposed branch: lp:~jameinel/bzr/2.5-soft-hangup-795025
Merge into: lp:bzr
Prerequisite: lp:~jameinel/bzr/drop-idle-connections-824797
Diff against target: 1019 lines (+698/-23)
10 files modified
bzrlib/smart/__init__.py (+1/-1)
bzrlib/smart/medium.py (+46/-4)
bzrlib/smart/server.py (+92/-10)
bzrlib/smart/signals.py (+114/-0)
bzrlib/tests/__init__.py (+1/-0)
bzrlib/tests/blackbox/test_serve.py (+28/-0)
bzrlib/tests/test_smart_signals.py (+189/-0)
bzrlib/tests/test_smart_transport.py (+213/-2)
bzrlib/trace.py (+4/-0)
doc/en/release-notes/bzr-2.5.txt (+10/-6)
To merge this branch: bzr merge lp:~jameinel/bzr/2.5-soft-hangup-795025
Reviewer Review Type Date Requested Status
Jelmer Vernooij (community) Approve
Review via email: mp+76608@code.launchpad.net

Commit message

Allow 'bzr serve' to interpret SIGHUP as a graceful shutdown. (bug #795025)

Description of the change

Silly launchpad losing changes if you accidentally navigate away from the edit box... :( (and silly laptop for putting browser previous right next to the up arrow.)

Anyway, this should be done. It does the bits that I wanted it to, and it seems well tested.

1) sending SIGHUP will start a soft shutdown for both 'bzr serve' and 'bzr serve --inet'. In the former case, it tells all the client threads that we want a soft shutdown (so stop as soon as you are done with the current request), and then waits for them to finish, logging whether it is progressing or not.

2) This changes 'serve()' so that it defaults to closing the client connection when done serving. This makes things cleaner IMO, but it means that we will close sys.stdin during 'bzr serve --inet'. And _flush_stdout_stderr was handling some IOError, but on py2.6 on Windows, I was getting a *ValueError*, so I added that to the trap list.

3) The infrastructure is all hooked up on Windows, except that we don't have SIGHUP. For now, I've been using SIGBREAK and then self._stop_gracefully() to make sure it is working.

4) We close the primary server socket after we get the shutdown request, but before we wait for the clients to finish. That should mean that you can get a no-downtime 'bzr serve' restart by doing "kill -SIGHUP <PID> ; bzr serve". So it will shut down the current one, and bring up another one to serve new requests while the first one finishes out the existing requests. For Launchpad, it will be more complex. We'll need to teach the twisted daemon about SIGHUP, and then have it pass that same signal to all the 'bzr serve --inet' children. With the forking server, I imagine it will similarly need to pass the signal around. But something similar should be possible, making the current one quiet (down to HAProxy), close the primary socket, start a new one responding to HAProxy, etc.

5) We now need to teach the client to reconnect when it gets disconnected, and the rest should flow pretty smoothly.

To post a comment you must log in.
Revision history for this message
Jelmer Vernooij (jelmer) wrote :

I've begun reviewing this, but threads make me sad and I'm not really familiar with this code (yet).
 I've noticed some really minor issues, but I'll try to follow up with some actually useful comments (once I understand the global picture a bit better).

you have an import conflict in bzrlib/smart/medium.py

206 + # have a good way (yet) to poll the spawned clients and
... and what ? :)

246 + if e.args[0] not in (errno.EBADF, errno.EINTR):
The comment above this line should probably be updated.

Revision history for this message
John A Meinel (jameinel) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

On 9/24/2011 11:26 PM, Jelmer Vernooij wrote:
> I've begun reviewing this, but threads make me sad and I'm not
> really familiar with this code (yet). I've noticed some really
> minor issues, but I'll try to follow up with some actually useful
> comments (once I understand the global picture a bit better).
>
> you have an import conflict in bzrlib/smart/medium.py
>
> 206 + # have a good way (yet) to poll the spawned
> clients and ... and what ? :)

I fixed that one, actually, so I just removed the comment.
SmartTCPServer now keeps track of clients and on graceful shutdown
waits for them to finish.

>
> 246 + if e.args[0] not in (errno.EBADF,
> errno.EINTR): The comment above this line should probably be
> updated.

Done. I fixed the import collision, and added gettext() calls to all
the new trace.note() functionality. As an aside, it is really easy to
forget those. It would be nice to have some sort of test/regular
procedure for making sure we get them right.

John
=:->

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAk6AMTYACgkQJdeBCYSNAAPsOACgkYEjHGH0AG7/X5Tid0537l6W
dvkAnj81J8GDMF7ijtwsqdZOSNf8+N1v
=wh/M
-----END PGP SIGNATURE-----

Revision history for this message
Jelmer Vernooij (jelmer) :
review: Approve
Revision history for this message
John A Meinel (jameinel) 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/smart/__init__.py'
--- bzrlib/smart/__init__.py 2011-09-22 13:25:47 +0000
+++ bzrlib/smart/__init__.py 2011-09-26 14:29:40 +0000
@@ -1,4 +1,4 @@
1# Copyright (C) 2006 Canonical Ltd1# Copyright (C) 2006,2011 Canonical Ltd
2#2#
3# This program is free software; you can redistribute it and/or modify3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by4# it under the terms of the GNU General Public License as published by
55
=== modified file 'bzrlib/smart/medium.py'
--- bzrlib/smart/medium.py 2011-09-26 14:29:38 +0000
+++ bzrlib/smart/medium.py 2011-09-26 14:29:40 +0000
@@ -46,10 +46,10 @@
46 urlutils,46 urlutils,
47 )47 )
48from bzrlib.i18n import gettext48from bzrlib.i18n import gettext
49from bzrlib.smart import client, protocol, request, vfs49from bzrlib.smart import client, protocol, request, signals, vfs
50from bzrlib.transport import ssh50from bzrlib.transport import ssh
51""")51""")
52from bzrlib import config, osutils52from bzrlib import osutils
5353
54# Throughout this module buffer size parameters are either limited to be at54# Throughout this module buffer size parameters are either limited to be at
55# most _MAX_READ_SIZE, or are ignored and _MAX_READ_SIZE is used instead.55# most _MAX_READ_SIZE, or are ignored and _MAX_READ_SIZE is used instead.
@@ -205,6 +205,8 @@
205 the stream. See also the _push_back method.205 the stream. See also the _push_back method.
206 """206 """
207207
208 _timer = time.time
209
208 def __init__(self, backing_transport, root_client_path='/', timeout=None):210 def __init__(self, backing_transport, root_client_path='/', timeout=None):
209 """Construct new server.211 """Construct new server.
210212
@@ -228,6 +230,11 @@
228 try:230 try:
229 while not self.finished:231 while not self.finished:
230 server_protocol = self._build_protocol()232 server_protocol = self._build_protocol()
233 # TODO: This seems inelegant:
234 if server_protocol is None:
235 # We could 'continue' only to notice that self.finished is
236 # True...
237 break
231 self._serve_one_request(server_protocol)238 self._serve_one_request(server_protocol)
232 except errors.ConnectionTimeout, e:239 except errors.ConnectionTimeout, e:
233 trace.note('%s' % (e,))240 trace.note('%s' % (e,))
@@ -238,6 +245,12 @@
238 except Exception, e:245 except Exception, e:
239 stderr.write("%s terminating on exception %s\n" % (self, e))246 stderr.write("%s terminating on exception %s\n" % (self, e))
240 raise247 raise
248 self._disconnect_client()
249
250 def _stop_gracefully(self):
251 """When we finish this message, stop looking for more."""
252 trace.mutter('Stopping %s' % (self,))
253 self.finished = True
241254
242 def _disconnect_client(self):255 def _disconnect_client(self):
243 """Close the current connection. We stopped due to a timeout/etc."""256 """Close the current connection. We stopped due to a timeout/etc."""
@@ -267,6 +280,9 @@
267 :returns: a SmartServerRequestProtocol.280 :returns: a SmartServerRequestProtocol.
268 """281 """
269 self._wait_for_bytes_with_timeout(self._client_timeout)282 self._wait_for_bytes_with_timeout(self._client_timeout)
283 if self.finished:
284 # We're stopping, so don't try to do any more work
285 return None
270 bytes = self._get_line()286 bytes = self._get_line()
271 protocol_factory, unused_bytes = _get_protocol_factory_for_bytes(bytes)287 protocol_factory, unused_bytes = _get_protocol_factory_for_bytes(bytes)
272 protocol = protocol_factory(288 protocol = protocol_factory(
@@ -281,10 +297,12 @@
281 readable handle before timeout_seconds.297 readable handle before timeout_seconds.
282 :return: None298 :return: None
283 """299 """
284 t_end = time.time() + timeout_seconds300 t_end = self._timer() + timeout_seconds
285 poll_timeout = min(timeout_seconds, self._client_poll_timeout)301 poll_timeout = min(timeout_seconds, self._client_poll_timeout)
286 rs = xs = None302 rs = xs = None
287 while not rs and not xs and time.time() < t_end:303 while not rs and not xs and self._timer() < t_end:
304 if self.finished:
305 return
288 try:306 try:
289 rs, _, xs = select.select([fd], [], [fd], poll_timeout)307 rs, _, xs = select.select([fd], [], [fd], poll_timeout)
290 except (select.error, socket.error) as e:308 except (select.error, socket.error) as e:
@@ -341,6 +359,18 @@
341 timeout=timeout)359 timeout=timeout)
342 sock.setblocking(True)360 sock.setblocking(True)
343 self.socket = sock361 self.socket = sock
362 # Get the getpeername now, as we might be closed later when we care.
363 try:
364 self._client_info = sock.getpeername()
365 except socket.error:
366 self._client_info = '<unknown>'
367
368 def __str__(self):
369 return '%s(client=%s)' % (self.__class__.__name__, self._client_info)
370
371 def __repr__(self):
372 return '%s.%s(client=%s)' % (self.__module__, self.__class__.__name__,
373 self._client_info)
344374
345 def _serve_one_request_unguarded(self, protocol):375 def _serve_one_request_unguarded(self, protocol):
346 while protocol.next_read_size():376 while protocol.next_read_size():
@@ -412,6 +442,17 @@
412 self._in = in_file442 self._in = in_file
413 self._out = out_file443 self._out = out_file
414444
445 def serve(self):
446 """See SmartServerStreamMedium.serve"""
447 # This is the regular serve, except it adds signal trapping for soft
448 # shutdown.
449 stop_gracefully = self._stop_gracefully
450 signals.register_on_hangup(id(self), stop_gracefully)
451 try:
452 return super(SmartServerPipeStreamMedium, self).serve()
453 finally:
454 signals.unregister_on_hangup(id(self))
455
415 def _serve_one_request_unguarded(self, protocol):456 def _serve_one_request_unguarded(self, protocol):
416 while True:457 while True:
417 # We need to be careful not to read past the end of the current458 # We need to be careful not to read past the end of the current
@@ -432,6 +473,7 @@
432473
433 def _disconnect_client(self):474 def _disconnect_client(self):
434 self._in.close()475 self._in.close()
476 self._out.flush()
435 self._out.close()477 self._out.close()
436478
437 def _wait_for_bytes_with_timeout(self, timeout_seconds):479 def _wait_for_bytes_with_timeout(self, timeout_seconds):
438480
=== modified file 'bzrlib/smart/server.py'
--- bzrlib/smart/server.py 2011-09-26 14:29:38 +0000
+++ bzrlib/smart/server.py 2011-09-26 14:29:40 +0000
@@ -20,6 +20,7 @@
20import os.path20import os.path
21import socket21import socket
22import sys22import sys
23import time
23import threading24import threading
2425
25from bzrlib.hooks import Hooks26from bzrlib.hooks import Hooks
@@ -31,7 +32,10 @@
31from bzrlib.i18n import gettext32from bzrlib.i18n import gettext
32from bzrlib.lazy_import import lazy_import33from bzrlib.lazy_import import lazy_import
33lazy_import(globals(), """34lazy_import(globals(), """
34from bzrlib.smart import medium35from bzrlib.smart import (
36 medium,
37 signals,
38 )
35from bzrlib.transport import (39from bzrlib.transport import (
36 chroot,40 chroot,
37 pathfilter,41 pathfilter,
@@ -56,6 +60,10 @@
56 # so the test suite can set it faster. (It thread.interrupt_main() will not60 # so the test suite can set it faster. (It thread.interrupt_main() will not
57 # fire a KeyboardInterrupt during socket.accept)61 # fire a KeyboardInterrupt during socket.accept)
58 _ACCEPT_TIMEOUT = 1.062 _ACCEPT_TIMEOUT = 1.0
63 _SHUTDOWN_POLL_TIMEOUT = 1.0
64 _LOG_WAITING_TIMEOUT = 10.0
65
66 _timer = time.time
5967
60 def __init__(self, backing_transport, root_client_path='/',68 def __init__(self, backing_transport, root_client_path='/',
61 client_timeout=None):69 client_timeout=None):
@@ -73,6 +81,10 @@
73 self.backing_transport = backing_transport81 self.backing_transport = backing_transport
74 self.root_client_path = root_client_path82 self.root_client_path = root_client_path
75 self._client_timeout = client_timeout83 self._client_timeout = client_timeout
84 self._active_connections = []
85 # This is set to indicate we want to wait for clients to finish before
86 # we disconnect.
87 self._gracefully_stopping = False
7688
77 def start_server(self, host, port):89 def start_server(self, host, port):
78 """Create the server listening socket.90 """Create the server listening socket.
@@ -105,8 +117,14 @@
105 self.port = self._sockname[1]117 self.port = self._sockname[1]
106 self._server_socket.listen(1)118 self._server_socket.listen(1)
107 self._server_socket.settimeout(self._ACCEPT_TIMEOUT)119 self._server_socket.settimeout(self._ACCEPT_TIMEOUT)
120 # Once we start accept()ing connections, we set started.
108 self._started = threading.Event()121 self._started = threading.Event()
122 # Once we stop accept()ing connections (and are closing the socket) we
123 # set _stopped
109 self._stopped = threading.Event()124 self._stopped = threading.Event()
125 # Once we have finished waiting for all clients, etc. We set
126 # _fully_stopped
127 self._fully_stopped = threading.Event()
110128
111 def _backing_urls(self):129 def _backing_urls(self):
112 # There are three interesting urls:130 # There are three interesting urls:
@@ -145,7 +163,38 @@
145 for hook in SmartTCPServer.hooks['server_stopped']:163 for hook in SmartTCPServer.hooks['server_stopped']:
146 hook(backing_urls, self.get_url())164 hook(backing_urls, self.get_url())
147165
166 def _stop_gracefully(self):
167 trace.note(gettext('Requested to stop gracefully'))
168 self._should_terminate = True
169 self._gracefully_stopping = True
170 for handler, _ in self._active_connections:
171 handler._stop_gracefully()
172
173 def _wait_for_clients_to_disconnect(self):
174 self._poll_active_connections()
175 if not self._active_connections:
176 return
177 trace.note(gettext('Waiting for %d client(s) to finish')
178 % (len(self._active_connections),))
179 t_next_log = self._timer() + self._LOG_WAITING_TIMEOUT
180 while self._active_connections:
181 now = self._timer()
182 if now >= t_next_log:
183 trace.note(gettext('Still waiting for %d client(s) to finish')
184 % (len(self._active_connections),))
185 t_next_log = now + self._LOG_WAITING_TIMEOUT
186 self._poll_active_connections(self._SHUTDOWN_POLL_TIMEOUT)
187
148 def serve(self, thread_name_suffix=''):188 def serve(self, thread_name_suffix=''):
189 # Note: There is a temptation to do
190 # signals.register_on_hangup(id(self), self._stop_gracefully)
191 # However, that creates a temporary object which is a bound
192 # method. signals._on_sighup is a WeakKeyDictionary so it
193 # immediately gets garbage collected, because nothing else
194 # references it. Instead, we need to keep a real reference to the
195 # bound method for the lifetime of the serve() function.
196 stop_gracefully = self._stop_gracefully
197 signals.register_on_hangup(id(self), stop_gracefully)
149 self._should_terminate = False198 self._should_terminate = False
150 # for hooks we are letting code know that a server has started (and199 # for hooks we are letting code know that a server has started (and
151 # later stopped).200 # later stopped).
@@ -161,14 +210,19 @@
161 pass210 pass
162 except self._socket_error, e:211 except self._socket_error, e:
163 # if the socket is closed by stop_background_thread212 # if the socket is closed by stop_background_thread
164 # we might get a EBADF here, any other socket errors213 # we might get a EBADF here, or if we get a signal we
165 # should get logged.214 # can get EINTR, any other socket errors should get
166 if e.args[0] != errno.EBADF:215 # logged.
167 trace.warning("listening socket error: %s", e)216 if e.args[0] not in (errno.EBADF, errno.EINTR):
217 trace.warning(gettext("listening socket error: %s")
218 % (e,))
168 else:219 else:
169 if self._should_terminate:220 if self._should_terminate:
221 conn.close()
170 break222 break
171 self.serve_conn(conn, thread_name_suffix)223 self.serve_conn(conn, thread_name_suffix)
224 # Cleanout any threads that have finished processing.
225 self._poll_active_connections()
172 except KeyboardInterrupt:226 except KeyboardInterrupt:
173 # dont log when CTRL-C'd.227 # dont log when CTRL-C'd.
174 raise228 raise
@@ -176,14 +230,18 @@
176 trace.report_exception(sys.exc_info(), sys.stderr)230 trace.report_exception(sys.exc_info(), sys.stderr)
177 raise231 raise
178 finally:232 finally:
179 self._stopped.set()
180 try:233 try:
181 # ensure the server socket is closed.234 # ensure the server socket is closed.
182 self._server_socket.close()235 self._server_socket.close()
183 except self._socket_error:236 except self._socket_error:
184 # ignore errors on close237 # ignore errors on close
185 pass238 pass
239 self._stopped.set()
240 signals.unregister_on_hangup(id(self))
186 self.run_server_stopped_hooks()241 self.run_server_stopped_hooks()
242 if self._gracefully_stopping:
243 self._wait_for_clients_to_disconnect()
244 self._fully_stopped.set()
187245
188 def get_url(self):246 def get_url(self):
189 """Return the url of the server"""247 """Return the url of the server"""
@@ -194,6 +252,23 @@
194 conn, self.backing_transport, self.root_client_path,252 conn, self.backing_transport, self.root_client_path,
195 timeout=self._client_timeout)253 timeout=self._client_timeout)
196254
255 def _poll_active_connections(self, timeout=0.0):
256 """Check to see if any active connections have finished.
257
258 This will iterate through self._active_connections, and update any
259 connections that are finished.
260
261 :param timeout: The timeout to pass to thread.join(). By default, we
262 set it to 0, so that we don't hang if threads are not done yet.
263 :return: None
264 """
265 still_active = []
266 for handler, thread in self._active_connections:
267 thread.join(timeout)
268 if thread.isAlive():
269 still_active.append((handler, thread))
270 self._active_connections = still_active
271
197 def serve_conn(self, conn, thread_name_suffix):272 def serve_conn(self, conn, thread_name_suffix):
198 # For WIN32, where the timeout value from the listening socket273 # For WIN32, where the timeout value from the listening socket
199 # propagates to the newly accepted socket.274 # propagates to the newly accepted socket.
@@ -203,9 +278,7 @@
203 handler = self._make_handler(conn)278 handler = self._make_handler(conn)
204 connection_thread = threading.Thread(279 connection_thread = threading.Thread(
205 None, handler.serve, name=thread_name)280 None, handler.serve, name=thread_name)
206 # FIXME: This thread is never joined, it should at least be collected281 self._active_connections.append((handler, connection_thread))
207 # somewhere so that tests that want to check for leaked threads can get
208 # rid of them -- vila 20100531
209 connection_thread.setDaemon(True)282 connection_thread.setDaemon(True)
210 connection_thread.start()283 connection_thread.start()
211 return connection_thread284 return connection_thread
@@ -355,13 +428,17 @@
355 transport = _mod_transport.get_transport_from_url(expand_userdirs.get_url())428 transport = _mod_transport.get_transport_from_url(expand_userdirs.get_url())
356 self.transport = transport429 self.transport = transport
357430
431 def _get_stdin_stdout(self):
432 return sys.stdin, sys.stdout
433
358 def _make_smart_server(self, host, port, inet, timeout):434 def _make_smart_server(self, host, port, inet, timeout):
359 if timeout is None:435 if timeout is None:
360 c = config.GlobalStack()436 c = config.GlobalStack()
361 timeout = c.get('serve.client_timeout')437 timeout = c.get('serve.client_timeout')
362 if inet:438 if inet:
439 stdin, stdout = self._get_stdin_stdout()
363 smart_server = medium.SmartServerPipeStreamMedium(440 smart_server = medium.SmartServerPipeStreamMedium(
364 sys.stdin, sys.stdout, self.transport, timeout=timeout)441 stdin, stdout, self.transport, timeout=timeout)
365 else:442 else:
366 if host is None:443 if host is None:
367 host = medium.BZR_DEFAULT_INTERFACE444 host = medium.BZR_DEFAULT_INTERFACE
@@ -387,6 +464,10 @@
387 self.cleanups.append(restore_default_ui_factory_and_lockdir_timeout)464 self.cleanups.append(restore_default_ui_factory_and_lockdir_timeout)
388 ui.ui_factory = ui.SilentUIFactory()465 ui.ui_factory = ui.SilentUIFactory()
389 lockdir._DEFAULT_TIMEOUT_SECONDS = 0466 lockdir._DEFAULT_TIMEOUT_SECONDS = 0
467 orig = signals.install_sighup_handler()
468 def restore_signals():
469 signals.restore_sighup_handler(orig)
470 self.cleanups.append(restore_signals)
390471
391 def set_up(self, transport, host, port, inet, timeout):472 def set_up(self, transport, host, port, inet, timeout):
392 self._make_backing_transport(transport)473 self._make_backing_transport(transport)
@@ -397,6 +478,7 @@
397 for cleanup in reversed(self.cleanups):478 for cleanup in reversed(self.cleanups):
398 cleanup()479 cleanup()
399480
481
400def serve_bzr(transport, host=None, port=None, inet=False, timeout=None):482def serve_bzr(transport, host=None, port=None, inet=False, timeout=None):
401 """This is the default implementation of 'bzr serve'.483 """This is the default implementation of 'bzr serve'.
402484
403485
=== added file 'bzrlib/smart/signals.py'
--- bzrlib/smart/signals.py 1970-01-01 00:00:00 +0000
+++ bzrlib/smart/signals.py 2011-09-26 14:29:40 +0000
@@ -0,0 +1,114 @@
1# Copyright (C) 2011 Canonical Ltd
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
17"""Signal handling for the smart server code."""
18
19import signal
20import weakref
21
22from bzrlib import trace
23
24
25# I'm pretty sure this has to be global, since signal handling is per-process.
26_on_sighup = None
27# TODO: Using a dict means that the order of calls is unordered. We could use a
28# list and then do something like LIFO ordering. A dict was chosen so
29# that you could have a key to easily remove your entry. However, you
30# could just use the callable itself as the indexed part, and even in
31# large cases, we shouldn't have more than 100 or so callbacks
32# registered.
33def _sighup_handler(signal_number, interrupted_frame):
34 """This is the actual function that is registered for handling SIGHUP.
35
36 It will call out to all the registered functions, letting them know that a
37 graceful termination has been requested.
38 """
39 if _on_sighup is None:
40 return
41 trace.mutter('Caught SIGHUP, sending graceful shutdown requests.')
42 for ref in _on_sighup.valuerefs():
43 try:
44 cb = ref()
45 if cb is not None:
46 cb()
47 except KeyboardInterrupt:
48 raise
49 except Exception:
50 trace.mutter('Error occurred while running SIGHUP handlers:')
51 trace.log_exception_quietly()
52
53
54def install_sighup_handler():
55 """Setup a handler for the SIGHUP signal."""
56 if getattr(signal, "SIGHUP", None) is None:
57 # If we can't install SIGHUP, there is no reason (yet) to do graceful
58 # shutdown.
59 old_signal = None
60 else:
61 old_signal = signal.signal(signal.SIGHUP, _sighup_handler)
62 old_dict = _setup_on_hangup_dict()
63 return old_signal, old_dict
64
65
66def _setup_on_hangup_dict():
67 """Create something for _on_sighup.
68
69 This is done when we install the sighup handler, and for tests that want to
70 test the functionality. If this hasn'nt been called, then
71 register_on_hangup is a no-op. As is unregister_on_hangup.
72 """
73 global _on_sighup
74 old = _on_sighup
75 _on_sighup = weakref.WeakValueDictionary()
76 return old
77
78
79def restore_sighup_handler(orig):
80 """Pass in the returned value from install_sighup_handler to reset."""
81 global _on_sighup
82 old_signal, old_dict = orig
83 if old_signal is not None:
84 signal.signal(signal.SIGHUP, old_signal)
85 _on_sighup = old_dict
86
87
88# TODO: Should these be single-use callables? Meaning that once we've triggered
89# SIGHUP and called them, they should auto-remove themselves? I don't
90# think so. Callers need to clean up during shutdown anyway, so that we
91# don't end up with lots of garbage in the _on_sighup dict. On the other
92# hand, we made _on_sighup a WeakValueDictionary in case cleanups didn't
93# get fired properly. Maybe we just assume we don't have to do it?
94def register_on_hangup(identifier, a_callable):
95 """Register for us to call a_callable as part of a graceful shutdown."""
96 if _on_sighup is None:
97 return
98 _on_sighup[identifier] = a_callable
99
100
101def unregister_on_hangup(identifier):
102 """Remove a callback from being called during sighup."""
103 if _on_sighup is None:
104 return
105 try:
106 del _on_sighup[identifier]
107 except KeyboardInterrupt:
108 raise
109 except Exception:
110 # This usually runs as a tear-down step. So we don't want to propagate
111 # most exceptions.
112 trace.mutter('Error occurred during unregister_on_hangup:')
113 trace.log_exception_quietly()
114
0115
=== modified file 'bzrlib/tests/__init__.py'
--- bzrlib/tests/__init__.py 2011-09-19 18:25:05 +0000
+++ bzrlib/tests/__init__.py 2011-09-26 14:29:40 +0000
@@ -4077,6 +4077,7 @@
4077 'bzrlib.tests.test_smart',4077 'bzrlib.tests.test_smart',
4078 'bzrlib.tests.test_smart_add',4078 'bzrlib.tests.test_smart_add',
4079 'bzrlib.tests.test_smart_request',4079 'bzrlib.tests.test_smart_request',
4080 'bzrlib.tests.test_smart_signals',
4080 'bzrlib.tests.test_smart_transport',4081 'bzrlib.tests.test_smart_transport',
4081 'bzrlib.tests.test_smtp_connection',4082 'bzrlib.tests.test_smtp_connection',
4082 'bzrlib.tests.test_source',4083 'bzrlib.tests.test_source',
40834084
=== modified file 'bzrlib/tests/blackbox/test_serve.py'
--- bzrlib/tests/blackbox/test_serve.py 2011-09-26 14:29:38 +0000
+++ bzrlib/tests/blackbox/test_serve.py 2011-09-26 14:29:40 +0000
@@ -314,6 +314,34 @@
314 err)314 err)
315 self.assertServerFinishesCleanly(process)315 self.assertServerFinishesCleanly(process)
316316
317 def test_bzr_serve_graceful_shutdown(self):
318 big_contents = 'a'*64*1024
319 self.build_tree_contents([('bigfile', big_contents)])
320 process, url = self.start_server_port(['--client-timeout=1.0'])
321 t = transport.get_transport_from_url(url)
322 m = t.get_smart_medium()
323 c = client._SmartClient(m)
324 # Start, but don't finish a response
325 resp, response_handler = c.call_expecting_body('get', 'bigfile')
326 self.assertEqual(('ok',), resp)
327 # Note: process.send_signal is a Python 2.6ism
328 process.send_signal(signal.SIGHUP)
329 # Wait for the server to notice the signal, and then read the actual
330 # body of the response. That way we know that it is waiting for the
331 # request to finish
332 self.assertEqual('Requested to stop gracefully\n',
333 process.stderr.readline())
334 self.assertEqual('Waiting for 1 client(s) to finish\n',
335 process.stderr.readline())
336 body = response_handler.read_body_bytes()
337 if body != big_contents:
338 self.fail('Failed to properly read the contents of "bigfile"')
339 # Now that our request is finished, the medium should notice it has
340 # been disconnected.
341 self.assertEqual('', m.read_bytes(1))
342 # And the server should be stopping
343 self.assertEqual(0, process.wait())
344
317345
318class TestCmdServeChrooting(TestBzrServeBase):346class TestCmdServeChrooting(TestBzrServeBase):
319347
320348
=== added file 'bzrlib/tests/test_smart_signals.py'
--- bzrlib/tests/test_smart_signals.py 1970-01-01 00:00:00 +0000
+++ bzrlib/tests/test_smart_signals.py 2011-09-26 14:29:40 +0000
@@ -0,0 +1,189 @@
1# Copyright (C) 2011 Canonical Ltd
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
17
18import os
19import signal
20import threading
21import weakref
22
23from bzrlib import tests, transport
24from bzrlib.smart import client, medium, server, signals
25
26# Windows doesn't define SIGHUP. And while we could just skip a lot of these
27# tests, we often don't actually care about interaction with 'signal', so we
28# can still run the tests for code coverage.
29SIGHUP = getattr(signal, 'SIGHUP', 1)
30
31
32class TestSignalHandlers(tests.TestCase):
33
34 def setUp(self):
35 super(TestSignalHandlers, self).setUp()
36 # This allows us to mutate the signal handler callbacks, but leave it
37 # 'pristine' after the test case.
38 # TODO: Arguably, this could be put into the base test.TestCase, along
39 # with a tearDown that asserts that all the entries have been
40 # removed properly. Global state is always a bit messy. A shame
41 # that we need it for signal handling.
42 orig = signals._setup_on_hangup_dict()
43 self.assertIs(None, orig)
44 def cleanup():
45 signals._on_sighup = None
46 self.addCleanup(cleanup)
47
48 def test_registered_callback_gets_called(self):
49 calls = []
50 def call_me():
51 calls.append('called')
52 signals.register_on_hangup('myid', call_me)
53 signals._sighup_handler(SIGHUP, None)
54 self.assertEqual(['called'], calls)
55 signals.unregister_on_hangup('myid')
56
57 def test_unregister_not_present(self):
58 # We don't want unregister to fail, since it is generally run at times
59 # that shouldn't interrupt other flow.
60 signals.unregister_on_hangup('no-such-id')
61 log = self.get_log()
62 self.assertContainsRe(log, 'Error occurred during unregister_on_hangup:')
63 self.assertContainsRe(log, '(?s)Traceback.*KeyError')
64
65 def test_failing_callback(self):
66 calls = []
67 def call_me():
68 calls.append('called')
69 def fail_me():
70 raise RuntimeError('something bad happened')
71 signals.register_on_hangup('myid', call_me)
72 signals.register_on_hangup('otherid', fail_me)
73 # _sighup_handler should call both, even though it got an exception
74 signals._sighup_handler(SIGHUP, None)
75 signals.unregister_on_hangup('myid')
76 signals.unregister_on_hangup('otherid')
77 log = self.get_log()
78 self.assertContainsRe(log, '(?s)Traceback.*RuntimeError')
79 self.assertEqual(['called'], calls)
80
81 def test_unregister_during_call(self):
82 # _sighup_handler should handle if some callbacks actually remove
83 # themselves while running.
84 calls = []
85 def call_me_and_unregister():
86 signals.unregister_on_hangup('myid')
87 calls.append('called_and_unregistered')
88 def call_me():
89 calls.append('called')
90 signals.register_on_hangup('myid', call_me_and_unregister)
91 signals.register_on_hangup('other', call_me)
92 signals._sighup_handler(SIGHUP, None)
93
94 def test_keyboard_interrupt_propagated(self):
95 # In case we get 'stuck' while running a hangup function, we should
96 # not suppress KeyboardInterrupt
97 def call_me_and_raise():
98 raise KeyboardInterrupt()
99 signals.register_on_hangup('myid', call_me_and_raise)
100 self.assertRaises(KeyboardInterrupt,
101 signals._sighup_handler, SIGHUP, None)
102 signals.unregister_on_hangup('myid')
103
104 def test_weak_references(self):
105 # TODO: This is probably a very-CPython-specific test
106 # Adding yourself to the callback should not make you immortal
107 # We overrideAttr during the test suite, so that we don't pollute the
108 # original dict. However, we can test that what we override matches
109 # what we are putting there.
110 self.assertIsInstance(signals._on_sighup,
111 weakref.WeakValueDictionary)
112 calls = []
113 def call_me():
114 calls.append('called')
115 signals.register_on_hangup('myid', call_me)
116 del call_me
117 # Non-CPython might want to do a gc.collect() here
118 signals._sighup_handler(SIGHUP, None)
119 self.assertEqual([], calls)
120
121 def test_not_installed(self):
122 # If you haven't called bzrlib.smart.signals.install_sighup_handler,
123 # then _on_sighup should be None, and all the calls become no-ops.
124 signals._on_sighup = None
125 calls = []
126 def call_me():
127 calls.append('called')
128 signals.register_on_hangup('myid', calls)
129 signals._sighup_handler(SIGHUP, None)
130 signals.unregister_on_hangup('myid')
131 log = self.get_log()
132 self.assertEqual('', log)
133
134 def test_install_sighup_handler(self):
135 # install_sighup_handler should set up a signal handler for SIGHUP, as
136 # well as the signals._on_sighup dict.
137 signals._on_sighup = None
138 orig = signals.install_sighup_handler()
139 if getattr(signal, 'SIGHUP', None) is not None:
140 cur = signal.getsignal(SIGHUP)
141 self.assertEqual(signals._sighup_handler, cur)
142 self.assertIsNot(None, signals._on_sighup)
143 signals.restore_sighup_handler(orig)
144 self.assertIs(None, signals._on_sighup)
145
146
147class TestInetServer(tests.TestCase):
148
149 def create_file_pipes(self):
150 r, w = os.pipe()
151 rf = os.fdopen(r, 'rb')
152 wf = os.fdopen(w, 'wb')
153 return rf, wf
154
155 def test_inet_server_responds_to_sighup(self):
156 t = transport.get_transport('memory:///')
157 content = 'a'*1024*1024
158 t.put_bytes('bigfile', content)
159 factory = server.BzrServerFactory()
160 # Override stdin/stdout so that we can inject our own handles
161 client_read, server_write = self.create_file_pipes()
162 server_read, client_write = self.create_file_pipes()
163 factory._get_stdin_stdout = lambda: (server_read, server_write)
164 factory.set_up(t, None, None, inet=True, timeout=4.0)
165 self.addCleanup(factory.tear_down)
166 started = threading.Event()
167 stopped = threading.Event()
168 def serving():
169 started.set()
170 factory.smart_server.serve()
171 stopped.set()
172 server_thread = threading.Thread(target=serving)
173 server_thread.start()
174 started.wait()
175 client_medium = medium.SmartSimplePipesClientMedium(client_read,
176 client_write, 'base')
177 client_client = client._SmartClient(client_medium)
178 resp, response_handler = client_client.call_expecting_body('get',
179 'bigfile')
180 signals._sighup_handler(SIGHUP, None)
181 self.assertTrue(factory.smart_server.finished)
182 # We can still finish reading the file content, but more than that, and
183 # the file is closed.
184 v = response_handler.read_body_bytes()
185 if v != content:
186 self.fail('Got the wrong content back, expected 1M "a"')
187 stopped.wait()
188 server_thread.join()
189
0190
=== modified file 'bzrlib/tests/test_smart_transport.py'
--- bzrlib/tests/test_smart_transport.py 2011-09-26 14:29:38 +0000
+++ bzrlib/tests/test_smart_transport.py 2011-09-26 14:29:40 +0000
@@ -18,10 +18,14 @@
1818
19# all of this deals with byte strings so this is safe19# all of this deals with byte strings so this is safe
20from cStringIO import StringIO20from cStringIO import StringIO
21import doctest
21import os22import os
22import socket23import socket
23import sys24import sys
24import threading25import threading
26import time
27
28from testtools.matchers import DocTestMatches
2529
26import bzrlib30import bzrlib
27from bzrlib import (31from bzrlib import (
@@ -937,6 +941,13 @@
937 server_protocol = self.build_protocol_socket('bzr request 2\n')941 server_protocol = self.build_protocol_socket('bzr request 2\n')
938 self.assertProtocolTwo(server_protocol)942 self.assertProtocolTwo(server_protocol)
939943
944 def test__build_protocol_returns_if_stopping(self):
945 # _build_protocol should notice that we are stopping, and return
946 # without waiting for bytes from the client.
947 server, client_sock = self.create_socket_context(None)
948 server._stop_gracefully()
949 self.assertIs(None, server._build_protocol())
950
940 def test_socket_set_timeout(self):951 def test_socket_set_timeout(self):
941 server, _ = self.create_socket_context(None, timeout=1.23)952 server, _ = self.create_socket_context(None, timeout=1.23)
942 self.assertEqual(1.23, server._client_timeout)953 self.assertEqual(1.23, server._client_timeout)
@@ -975,6 +986,17 @@
975 data = server.read_bytes(1)986 data = server.read_bytes(1)
976 self.assertEqual('', data)987 self.assertEqual('', data)
977988
989 def test_socket_wait_for_bytes_with_shutdown(self):
990 server, client_sock = self.create_socket_context(None)
991 t = time.time()
992 # Override the _timer functionality, so that time never increments,
993 # this way, we can be sure we stopped because of the flag, and not
994 # because of a timeout, etc.
995 server._timer = lambda: t
996 server._client_poll_timeout = 0.1
997 server._stop_gracefully()
998 server._wait_for_bytes_with_timeout(1.0)
999
978 def test_socket_serve_timeout_closes_socket(self):1000 def test_socket_serve_timeout_closes_socket(self):
979 server, client_sock = self.create_socket_context(None, timeout=0.1)1001 server, client_sock = self.create_socket_context(None, timeout=0.1)
980 # This should timeout quickly, and then close the connection so that1002 # This should timeout quickly, and then close the connection so that
@@ -1056,6 +1078,75 @@
10561078
1057class TestSmartTCPServer(tests.TestCase):1079class TestSmartTCPServer(tests.TestCase):
10581080
1081 def make_server(self):
1082 """Create a SmartTCPServer that we can exercise.
1083
1084 Note: we don't use SmartTCPServer_for_testing because the testing
1085 version overrides lots of functionality like 'serve', and we want to
1086 test the raw service.
1087
1088 This will start the server in another thread, and wait for it to
1089 indicate it has finished starting up.
1090
1091 :return: (server, server_thread)
1092 """
1093 t = _mod_transport.get_transport_from_url('memory:///')
1094 server = _mod_server.SmartTCPServer(t, client_timeout=4.0)
1095 server._ACCEPT_TIMEOUT = 0.1
1096 # We don't use 'localhost' because that might be an IPv6 address.
1097 server.start_server('127.0.0.1', 0)
1098 server_thread = threading.Thread(target=server.serve,
1099 args=(self.id(),))
1100 server_thread.start()
1101 # Ensure this gets called at some point
1102 self.addCleanup(server._stop_gracefully)
1103 server._started.wait()
1104 return server, server_thread
1105
1106 def ensure_client_disconnected(self, client_sock):
1107 """Ensure that a socket is closed, discarding all errors."""
1108 try:
1109 client_sock.close()
1110 except Exception:
1111 pass
1112
1113 def connect_to_server(self, server):
1114 """Create a client socket that can talk to the server."""
1115 client_sock = socket.socket()
1116 server_info = server._server_socket.getsockname()
1117 client_sock.connect(server_info)
1118 self.addCleanup(self.ensure_client_disconnected, client_sock)
1119 return client_sock
1120
1121 def connect_to_server_and_hangup(self, server):
1122 """Connect to the server, and then hang up.
1123 That way it doesn't sit waiting for 'accept()' to timeout.
1124 """
1125 # If the server has already signaled that the socket is closed, we
1126 # don't need to try to connect to it. Not being set, though, the server
1127 # might still close the socket while we try to connect to it. So we
1128 # still have to catch the exception.
1129 if server._stopped.isSet():
1130 return
1131 try:
1132 client_sock = self.connect_to_server(server)
1133 client_sock.close()
1134 except socket.error, e:
1135 # If the server has hung up already, that is fine.
1136 pass
1137
1138 def say_hello(self, client_sock):
1139 """Send the 'hello' smart RPC, and expect the response."""
1140 client_sock.send('hello\n')
1141 self.assertEqual('ok\x012\n', client_sock.recv(5))
1142
1143 def shutdown_server_cleanly(self, server, server_thread):
1144 server._stop_gracefully()
1145 self.connect_to_server_and_hangup(server)
1146 server._stopped.wait()
1147 server._fully_stopped.wait()
1148 server_thread.join()
1149
1059 def test_get_error_unexpected(self):1150 def test_get_error_unexpected(self):
1060 """Error reported by server with no specific representation"""1151 """Error reported by server with no specific representation"""
1061 self.overrideEnv('BZR_NO_SMART_VFS', None)1152 self.overrideEnv('BZR_NO_SMART_VFS', None)
@@ -1081,10 +1172,130 @@
10811172
1082 def test_propagates_timeout(self):1173 def test_propagates_timeout(self):
1083 server = _mod_server.SmartTCPServer(None, client_timeout=1.23)1174 server = _mod_server.SmartTCPServer(None, client_timeout=1.23)
1084 server_socket = socket.socket()1175 server_sock, client_sock = portable_socket_pair()
1085 handler = server._make_handler(server_socket)1176 handler = server._make_handler(server_sock)
1086 self.assertEqual(1.23, handler._client_timeout)1177 self.assertEqual(1.23, handler._client_timeout)
10871178
1179 def test_serve_conn_tracks_connections(self):
1180 server = _mod_server.SmartTCPServer(None, client_timeout=4.0)
1181 server_sock, client_sock = portable_socket_pair()
1182 server.serve_conn(server_sock, '-%s' % (self.id(),))
1183 self.assertEqual(1, len(server._active_connections))
1184 # We still want to talk on the connection. Polling should indicate it
1185 # is still active.
1186 server._poll_active_connections()
1187 self.assertEqual(1, len(server._active_connections))
1188 # Closing the socket will end the active thread, and polling will
1189 # notice and remove it from the active set.
1190 client_sock.close()
1191 server._poll_active_connections(0.1)
1192 self.assertEqual(0, len(server._active_connections))
1193
1194 def test_serve_closes_out_finished_connections(self):
1195 server, server_thread = self.make_server()
1196 # The server is started, connect to it.
1197 client_sock = self.connect_to_server(server)
1198 # We send and receive on the connection, so that we know the
1199 # server-side has seen the connect, and started handling the
1200 # results.
1201 self.say_hello(client_sock)
1202 self.assertEqual(1, len(server._active_connections))
1203 # Grab a handle to the thread that is processing our request
1204 _, server_side_thread = server._active_connections[0]
1205 # Close the connection, ask the server to stop, and wait for the
1206 # server to stop, as well as the thread that was servicing the
1207 # client request.
1208 client_sock.close()
1209 # Wait for the server-side request thread to notice we are closed.
1210 server_side_thread.join()
1211 # Stop the server, it should notice the connection has finished.
1212 self.shutdown_server_cleanly(server, server_thread)
1213 # The server should have noticed that all clients are gone before
1214 # exiting.
1215 self.assertEqual(0, len(server._active_connections))
1216
1217 def test_serve_reaps_finished_connections(self):
1218 server, server_thread = self.make_server()
1219 client_sock1 = self.connect_to_server(server)
1220 # We send and receive on the connection, so that we know the
1221 # server-side has seen the connect, and started handling the
1222 # results.
1223 self.say_hello(client_sock1)
1224 server_handler1, server_side_thread1 = server._active_connections[0]
1225 client_sock1.close()
1226 server_side_thread1.join()
1227 # By waiting until the first connection is fully done, the server
1228 # should notice after another connection that the first has finished.
1229 client_sock2 = self.connect_to_server(server)
1230 self.say_hello(client_sock2)
1231 server_handler2, server_side_thread2 = server._active_connections[-1]
1232 # There is a race condition. We know that client_sock2 has been
1233 # registered, but not that _poll_active_connections has been called. We
1234 # know that it will be called before the server will accept a new
1235 # connection, however. So connect one more time, and assert that we
1236 # either have 1 or 2 active connections (never 3), and that the 'first'
1237 # connection is not connection 1
1238 client_sock3 = self.connect_to_server(server)
1239 self.say_hello(client_sock3)
1240 # Copy the list, so we don't have it mutating behind our back
1241 conns = list(server._active_connections)
1242 self.assertEqual(2, len(conns))
1243 self.assertNotEqual((server_handler1, server_side_thread1), conns[0])
1244 self.assertEqual((server_handler2, server_side_thread2), conns[0])
1245 client_sock2.close()
1246 client_sock3.close()
1247 self.shutdown_server_cleanly(server, server_thread)
1248
1249 def test_graceful_shutdown_waits_for_clients_to_stop(self):
1250 server, server_thread = self.make_server()
1251 # We need something big enough that it won't fit in a single recv. So
1252 # the server thread gets blocked writing content to the client until we
1253 # finish reading on the client.
1254 server.backing_transport.put_bytes('bigfile',
1255 'a'*1024*1024)
1256 client_sock = self.connect_to_server(server)
1257 self.say_hello(client_sock)
1258 _, server_side_thread = server._active_connections[0]
1259 # Start the RPC, but don't finish reading the response
1260 client_medium = medium.SmartClientAlreadyConnectedSocketMedium(
1261 'base', client_sock)
1262 client_client = client._SmartClient(client_medium)
1263 resp, response_handler = client_client.call_expecting_body('get',
1264 'bigfile')
1265 self.assertEqual(('ok',), resp)
1266 # Ask the server to stop gracefully, and wait for it.
1267 server._stop_gracefully()
1268 self.connect_to_server_and_hangup(server)
1269 server._stopped.wait()
1270 # It should not be accepting another connection.
1271 self.assertRaises(socket.error, self.connect_to_server, server)
1272 # It should also not be fully stopped
1273 server._fully_stopped.wait(0.01)
1274 self.assertFalse(server._fully_stopped.isSet())
1275 response_handler.read_body_bytes()
1276 client_sock.close()
1277 server_side_thread.join()
1278 server_thread.join()
1279 self.assertTrue(server._fully_stopped.isSet())
1280 log = self.get_log()
1281 self.assertThat(log, DocTestMatches("""\
1282 INFO Requested to stop gracefully
1283... Stopping SmartServerSocketStreamMedium(client=('127.0.0.1', ...
1284 INFO Waiting for 1 client(s) to finish
1285""", flags=doctest.ELLIPSIS|doctest.REPORT_UDIFF))
1286
1287 def test_stop_gracefully_tells_handlers_to_stop(self):
1288 server, server_thread = self.make_server()
1289 client_sock = self.connect_to_server(server)
1290 self.say_hello(client_sock)
1291 server_handler, server_side_thread = server._active_connections[0]
1292 self.assertFalse(server_handler.finished)
1293 server._stop_gracefully()
1294 self.assertTrue(server_handler.finished)
1295 client_sock.close()
1296 self.connect_to_server_and_hangup(server)
1297 server_thread.join()
1298
10881299
1089class SmartTCPTests(tests.TestCase):1300class SmartTCPTests(tests.TestCase):
1090 """Tests for connection/end to end behaviour using the TCP server.1301 """Tests for connection/end to end behaviour using the TCP server.
10911302
=== modified file 'bzrlib/trace.py'
--- bzrlib/trace.py 2011-07-15 14:13:32 +0000
+++ bzrlib/trace.py 2011-09-26 14:29:40 +0000
@@ -559,6 +559,10 @@
559 try:559 try:
560 sys.stdout.flush()560 sys.stdout.flush()
561 sys.stderr.flush()561 sys.stderr.flush()
562 except ValueError, e:
563 # On Windows, I get ValueError calling stdout.flush() on a closed
564 # handle
565 pass
562 except IOError, e:566 except IOError, e:
563 import errno567 import errno
564 if e.errno in [errno.EINVAL, errno.EPIPE]:568 if e.errno in [errno.EINVAL, errno.EPIPE]:
565569
=== modified file 'doc/en/release-notes/bzr-2.5.txt'
--- doc/en/release-notes/bzr-2.5.txt 2011-09-26 14:29:38 +0000
+++ doc/en/release-notes/bzr-2.5.txt 2011-09-26 14:29:40 +0000
@@ -20,6 +20,16 @@
2020
21.. New commands, options, etc that users may wish to try out.21.. New commands, options, etc that users may wish to try out.
2222
23* ``bzr serve`` will now disconnect clients if they have not issued an RPC
24 request after 5minutes. On POSIX platforms, this will also happen for
25 ``bzr serve --inet``. This can be overridden with the configuration
26 variable ``serve.client_timeout`` or in the command line parameter
27 ``bzr serve --client-timeout=X``. Further, it is possible to request
28 ``bzr serve [--inet]`` to shutdown gracefully by sending SIGHUP. It will
29 finish the current request, and then close the connection.
30 (John Arbash Meinel, #824797, #795025)
31
32
23Improvements33Improvements
24************34************
2535
@@ -161,12 +171,6 @@
161 The name change also affects ``bzr missing``.171 The name change also affects ``bzr missing``.
162 (Martin von Gagern)172 (Martin von Gagern)
163173
164* ``bzr serve`` will now disconnect clients if they have not issued an RPC
165 request after 5minutes. On POSIX platforms, this will also happen for
166 ``bzr serve --inet``. This can be overridden with the configuration
167 variable ``serve.client_timeout`` or in the command line parameter
168 ``bzr serve --client-timeout=X``. (John Arbash Meinel, #824797)
169
170* ``config.Option`` can now declare ``default_from_env``, a list of174* ``config.Option`` can now declare ``default_from_env``, a list of
171 environment variables to get a default value from. (Vincent Ladeuil)175 environment variables to get a default value from. (Vincent Ladeuil)
172176