Merge lp:~jameinel/bzr/2.4-client-reconnect-819604 into lp:bzr/2.4

Proposed by John A Meinel
Status: Merged
Approved by: John A Meinel
Approved revision: no longer in the source branch.
Merged at revision: 6076
Proposed branch: lp:~jameinel/bzr/2.4-client-reconnect-819604
Merge into: lp:bzr/2.4
Diff against target: 1576 lines (+891/-230)
14 files modified
bzrlib/help_topics/en/debug-flags.txt (+2/-0)
bzrlib/osutils.py (+12/-4)
bzrlib/smart/client.py (+208/-86)
bzrlib/smart/medium.py (+46/-6)
bzrlib/smart/protocol.py (+5/-3)
bzrlib/smart/request.py (+145/-100)
bzrlib/tests/__init__.py (+3/-1)
bzrlib/tests/test_bundle.py (+6/-3)
bzrlib/tests/test_osutils.py (+40/-1)
bzrlib/tests/test_remote.py (+4/-2)
bzrlib/tests/test_smart.py (+5/-2)
bzrlib/tests/test_smart_request.py (+10/-0)
bzrlib/tests/test_smart_transport.py (+399/-21)
doc/en/release-notes/bzr-2.4.txt (+6/-1)
To merge this branch: bzr merge lp:~jameinel/bzr/2.4-client-reconnect-819604
Reviewer Review Type Date Requested Status
Richard Wilbur Approve
Review via email: mp+78844@code.launchpad.net

Commit message

Bug #819604, merge the bzr-2.3 client-reconnect 819604 changes into bzr-2.4.

Description of the change

The bzr-2.4 version of client-reconnect (bug #819604).

The only difference for this vs the bzr-2.3 branch is that we added "Branch.heads_to_fetch" as an RPC that needed to be quantified.

To post a comment you must log in.
Revision history for this message
John A Meinel (jameinel) wrote :

Resurrecting this, since I'm thinking to try and land it in 2.4, but not worry about 2.1 (which has very different internals from 2.4).

Revision history for this message
Richard Wilbur (richard-wilbur) wrote :

This looks like a measured response to failures caused by network misbehaviour. It seems like a good idea to get these into a version that can be tested and then tweak them further, if needed, based on experience.
+1

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

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

On 2013-05-24 8:36, Richard Wilbur wrote:
> Review: Approve
>
> This looks like a measured response to failures caused by network
> misbehaviour. It seems like a good idea to get these into a
> version that can be tested and then tweak them further, if needed,
> based on experience. +1
>

These changes have actually been in trunk for a long time. This is
just backporting it to earlier versions so that they also are in old
stable releases. (Launchpad itself will start disconnecting clients if
they are idle for too long, so we wanted people to not get too many
disconnects. Obviously releasing took longer than originally planned.)

So the changes should be pretty safe to land, since we've been
actively using them for >1year now.

John
=:->
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.13 (Cygwin)
Comment: Using GnuPG with Thunderbird - http://www.enigmail.net/

iEYEARECAAYFAlGgoN4ACgkQJdeBCYSNAAOH1QCgrc8wfn2yS+PbJxsdU4Ulslho
O08AoM0xE0NNaNinOSUsNX+5++mCfNkQ
=VCVm
-----END PGP SIGNATURE-----

Revision history for this message
Richard Wilbur (richard-wilbur) wrote :

In light of the fact that these changes have already been in trunk under active use for over a year, I'd like to change my vote to: I unreservedly approve.
+2

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
1=== modified file 'bzrlib/help_topics/en/debug-flags.txt'
2--- bzrlib/help_topics/en/debug-flags.txt 2011-05-27 17:10:05 +0000
3+++ bzrlib/help_topics/en/debug-flags.txt 2013-05-23 13:00:35 +0000
4@@ -24,6 +24,8 @@
5 -Dindex Trace major index operations.
6 -Dknit Trace knit operations.
7 -Dlock Trace when lockdir locks are taken or released.
8+-Dnoretry If a connection is reset, fail immediately rather than
9+ retrying the request.
10 -Dprogress Trace progress bar operations.
11 -Dmem_dump Dump memory to a file upon an out of memory error.
12 -Dmerge Emit information for debugging merges.
13
14=== modified file 'bzrlib/osutils.py'
15--- bzrlib/osutils.py 2013-05-23 08:25:07 +0000
16+++ bzrlib/osutils.py 2013-05-23 13:00:35 +0000
17@@ -1,4 +1,4 @@
18-# Copyright (C) 2005-2011 Canonical Ltd
19+# Copyright (C) 2005-2012 Canonical Ltd
20 #
21 # This program is free software; you can redistribute it and/or modify
22 # it under the terms of the GNU General Public License as published by
23@@ -32,6 +32,7 @@
24 # and need the former on windows
25 import shutil
26 from shutil import rmtree
27+import signal
28 import socket
29 import subprocess
30 # We need to import both tempfile and mkdtemp as we export the later on posix
31@@ -2044,7 +2045,7 @@
32 # data at once.
33 MAX_SOCKET_CHUNK = 64 * 1024
34
35-_end_of_stream_errors = [errno.ECONNRESET]
36+_end_of_stream_errors = [errno.ECONNRESET, errno.EPIPE, errno.EINVAL]
37 for _eno in ['WSAECONNRESET', 'WSAECONNABORTED']:
38 _eno = getattr(errno, _eno, None)
39 if _eno is not None:
40@@ -2116,12 +2117,19 @@
41 while sent_total < byte_count:
42 try:
43 sent = sock.send(buffer(bytes, sent_total, MAX_SOCKET_CHUNK))
44- except socket.error, e:
45+ except (socket.error, IOError), e:
46+ if e.args[0] in _end_of_stream_errors:
47+ raise errors.ConnectionReset(
48+ "Error trying to write to socket", e)
49 if e.args[0] != errno.EINTR:
50 raise
51 else:
52+ if sent == 0:
53+ raise errors.ConnectionReset('Sending to %s returned 0 bytes'
54+ % (sock,))
55 sent_total += sent
56- report_activity(sent, 'write')
57+ if report_activity is not None:
58+ report_activity(sent, 'write')
59
60
61 def connect_socket(address):
62
63=== modified file 'bzrlib/smart/client.py'
64--- bzrlib/smart/client.py 2011-03-30 11:45:54 +0000
65+++ bzrlib/smart/client.py 2013-05-23 13:00:35 +0000
66@@ -14,12 +14,18 @@
67 # along with this program; if not, write to the Free Software
68 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
69
70+from bzrlib import lazy_import
71+lazy_import.lazy_import(globals(), """
72+from bzrlib.smart import request as _mod_request
73+""")
74+
75 import bzrlib
76 from bzrlib.smart import message, protocol
77-from bzrlib.trace import warning
78 from bzrlib import (
79+ debug,
80 errors,
81 hooks,
82+ trace,
83 )
84
85
86@@ -39,93 +45,12 @@
87 def __repr__(self):
88 return '%s(%r)' % (self.__class__.__name__, self._medium)
89
90- def _send_request(self, protocol_version, method, args, body=None,
91- readv_body=None, body_stream=None):
92- encoder, response_handler = self._construct_protocol(
93- protocol_version)
94- encoder.set_headers(self._headers)
95- if body is not None:
96- if readv_body is not None:
97- raise AssertionError(
98- "body and readv_body are mutually exclusive.")
99- if body_stream is not None:
100- raise AssertionError(
101- "body and body_stream are mutually exclusive.")
102- encoder.call_with_body_bytes((method, ) + args, body)
103- elif readv_body is not None:
104- if body_stream is not None:
105- raise AssertionError(
106- "readv_body and body_stream are mutually exclusive.")
107- encoder.call_with_body_readv_array((method, ) + args, readv_body)
108- elif body_stream is not None:
109- encoder.call_with_body_stream((method, ) + args, body_stream)
110- else:
111- encoder.call(method, *args)
112- return response_handler
113-
114- def _run_call_hooks(self, method, args, body, readv_body):
115- if not _SmartClient.hooks['call']:
116- return
117- params = CallHookParams(method, args, body, readv_body, self._medium)
118- for hook in _SmartClient.hooks['call']:
119- hook(params)
120-
121 def _call_and_read_response(self, method, args, body=None, readv_body=None,
122 body_stream=None, expect_response_body=True):
123- self._run_call_hooks(method, args, body, readv_body)
124- if self._medium._protocol_version is not None:
125- response_handler = self._send_request(
126- self._medium._protocol_version, method, args, body=body,
127- readv_body=readv_body, body_stream=body_stream)
128- return (response_handler.read_response_tuple(
129- expect_body=expect_response_body),
130- response_handler)
131- else:
132- for protocol_version in [3, 2]:
133- if protocol_version == 2:
134- # If v3 doesn't work, the remote side is older than 1.6.
135- self._medium._remember_remote_is_before((1, 6))
136- response_handler = self._send_request(
137- protocol_version, method, args, body=body,
138- readv_body=readv_body, body_stream=body_stream)
139- try:
140- response_tuple = response_handler.read_response_tuple(
141- expect_body=expect_response_body)
142- except errors.UnexpectedProtocolVersionMarker, err:
143- # TODO: We could recover from this without disconnecting if
144- # we recognise the protocol version.
145- warning(
146- 'Server does not understand Bazaar network protocol %d,'
147- ' reconnecting. (Upgrade the server to avoid this.)'
148- % (protocol_version,))
149- self._medium.disconnect()
150- continue
151- except errors.ErrorFromSmartServer:
152- # If we received an error reply from the server, then it
153- # must be ok with this protocol version.
154- self._medium._protocol_version = protocol_version
155- raise
156- else:
157- self._medium._protocol_version = protocol_version
158- return response_tuple, response_handler
159- raise errors.SmartProtocolError(
160- 'Server is not a Bazaar server: ' + str(err))
161-
162- def _construct_protocol(self, version):
163- request = self._medium.get_request()
164- if version == 3:
165- request_encoder = protocol.ProtocolThreeRequester(request)
166- response_handler = message.ConventionalResponseHandler()
167- response_proto = protocol.ProtocolThreeDecoder(
168- response_handler, expect_version_marker=True)
169- response_handler.setProtoAndMediumRequest(response_proto, request)
170- elif version == 2:
171- request_encoder = protocol.SmartClientRequestProtocolTwo(request)
172- response_handler = request_encoder
173- else:
174- request_encoder = protocol.SmartClientRequestProtocolOne(request)
175- response_handler = request_encoder
176- return request_encoder, response_handler
177+ request = _SmartClientRequest(self, method, args, body=body,
178+ readv_body=readv_body, body_stream=body_stream,
179+ expect_response_body=expect_response_body)
180+ return request.call_and_read_response()
181
182 def call(self, method, *args):
183 """Call a method on the remote server."""
184@@ -191,6 +116,203 @@
185 return self._medium.remote_path_from_transport(transport)
186
187
188+class _SmartClientRequest(object):
189+ """Encapsulate the logic for a single request.
190+
191+ This class handles things like reconnecting and sending the request a
192+ second time when the connection is reset in the middle. It also handles the
193+ multiple requests that get made if we don't know what protocol the server
194+ supports yet.
195+
196+ Generally, you build up one of these objects, passing in the arguments that
197+ you want to send to the server, and then use 'call_and_read_response' to
198+ get the response from the server.
199+ """
200+
201+ def __init__(self, client, method, args, body=None, readv_body=None,
202+ body_stream=None, expect_response_body=True):
203+ self.client = client
204+ self.method = method
205+ self.args = args
206+ self.body = body
207+ self.readv_body = readv_body
208+ self.body_stream = body_stream
209+ self.expect_response_body = expect_response_body
210+
211+ def call_and_read_response(self):
212+ """Send the request to the server, and read the initial response.
213+
214+ This doesn't read all of the body content of the response, instead it
215+ returns (response_tuple, response_handler). response_tuple is the 'ok',
216+ or 'error' information, and 'response_handler' can be used to get the
217+ content stream out.
218+ """
219+ self._run_call_hooks()
220+ protocol_version = self.client._medium._protocol_version
221+ if protocol_version is None:
222+ return self._call_determining_protocol_version()
223+ else:
224+ return self._call(protocol_version)
225+
226+ def _is_safe_to_send_twice(self):
227+ """Check if the current method is re-entrant safe."""
228+ if self.body_stream is not None or 'noretry' in debug.debug_flags:
229+ # We can't restart a body stream that has already been consumed.
230+ return False
231+ request_type = _mod_request.request_handlers.get_info(self.method)
232+ if request_type in ('read', 'idem', 'semi'):
233+ return True
234+ # If we have gotten this far, 'stream' cannot be retried, because we
235+ # already consumed the local stream.
236+ if request_type in ('semivfs', 'mutate', 'stream'):
237+ return False
238+ trace.mutter('Unknown request type: %s for method %s'
239+ % (request_type, self.method))
240+ return False
241+
242+ def _run_call_hooks(self):
243+ if not _SmartClient.hooks['call']:
244+ return
245+ params = CallHookParams(self.method, self.args, self.body,
246+ self.readv_body, self.client._medium)
247+ for hook in _SmartClient.hooks['call']:
248+ hook(params)
249+
250+ def _call(self, protocol_version):
251+ """We know the protocol version.
252+
253+ So this just sends the request, and then reads the response. This is
254+ where the code will be to retry requests if the connection is closed.
255+ """
256+ response_handler = self._send(protocol_version)
257+ try:
258+ response_tuple = response_handler.read_response_tuple(
259+ expect_body=self.expect_response_body)
260+ except errors.ConnectionReset, e:
261+ self.client._medium.reset()
262+ if not self._is_safe_to_send_twice():
263+ raise
264+ trace.warning('ConnectionReset reading response for %r, retrying'
265+ % (self.method,))
266+ trace.log_exception_quietly()
267+ encoder, response_handler = self._construct_protocol(
268+ protocol_version)
269+ self._send_no_retry(encoder)
270+ response_tuple = response_handler.read_response_tuple(
271+ expect_body=self.expect_response_body)
272+ return (response_tuple, response_handler)
273+
274+ def _call_determining_protocol_version(self):
275+ """Determine what protocol the remote server supports.
276+
277+ We do this by placing a request in the most recent protocol, and
278+ handling the UnexpectedProtocolVersionMarker from the server.
279+ """
280+ for protocol_version in [3, 2]:
281+ if protocol_version == 2:
282+ # If v3 doesn't work, the remote side is older than 1.6.
283+ self.client._medium._remember_remote_is_before((1, 6))
284+ try:
285+ response_tuple, response_handler = self._call(protocol_version)
286+ except errors.UnexpectedProtocolVersionMarker, err:
287+ # TODO: We could recover from this without disconnecting if
288+ # we recognise the protocol version.
289+ trace.warning(
290+ 'Server does not understand Bazaar network protocol %d,'
291+ ' reconnecting. (Upgrade the server to avoid this.)'
292+ % (protocol_version,))
293+ self.client._medium.disconnect()
294+ continue
295+ except errors.ErrorFromSmartServer:
296+ # If we received an error reply from the server, then it
297+ # must be ok with this protocol version.
298+ self.client._medium._protocol_version = protocol_version
299+ raise
300+ else:
301+ self.client._medium._protocol_version = protocol_version
302+ return response_tuple, response_handler
303+ raise errors.SmartProtocolError(
304+ 'Server is not a Bazaar server: ' + str(err))
305+
306+ def _construct_protocol(self, version):
307+ """Build the encoding stack for a given protocol version."""
308+ request = self.client._medium.get_request()
309+ if version == 3:
310+ request_encoder = protocol.ProtocolThreeRequester(request)
311+ response_handler = message.ConventionalResponseHandler()
312+ response_proto = protocol.ProtocolThreeDecoder(
313+ response_handler, expect_version_marker=True)
314+ response_handler.setProtoAndMediumRequest(response_proto, request)
315+ elif version == 2:
316+ request_encoder = protocol.SmartClientRequestProtocolTwo(request)
317+ response_handler = request_encoder
318+ else:
319+ request_encoder = protocol.SmartClientRequestProtocolOne(request)
320+ response_handler = request_encoder
321+ return request_encoder, response_handler
322+
323+ def _send(self, protocol_version):
324+ """Encode the request, and send it to the server.
325+
326+ This will retry a request if we get a ConnectionReset while sending the
327+ request to the server. (Unless we have a body_stream that we have
328+ already started consuming, since we can't restart body_streams)
329+
330+ :return: response_handler as defined by _construct_protocol
331+ """
332+ encoder, response_handler = self._construct_protocol(protocol_version)
333+ try:
334+ self._send_no_retry(encoder)
335+ except errors.ConnectionReset, e:
336+ # If we fail during the _send_no_retry phase, then we can
337+ # be confident that the server did not get our request, because we
338+ # haven't started waiting for the reply yet. So try the request
339+ # again. We only issue a single retry, because if the connection
340+ # really is down, there is no reason to loop endlessly.
341+
342+ # Connection is dead, so close our end of it.
343+ self.client._medium.reset()
344+ if (('noretry' in debug.debug_flags)
345+ or (self.body_stream is not None
346+ and encoder.body_stream_started)):
347+ # We can't restart a body_stream that has been partially
348+ # consumed, so we don't retry.
349+ # Note: We don't have to worry about
350+ # SmartClientRequestProtocolOne or Two, because they don't
351+ # support client-side body streams.
352+ raise
353+ trace.warning('ConnectionReset calling %r, retrying'
354+ % (self.method,))
355+ trace.log_exception_quietly()
356+ encoder, response_handler = self._construct_protocol(
357+ protocol_version)
358+ self._send_no_retry(encoder)
359+ return response_handler
360+
361+ def _send_no_retry(self, encoder):
362+ """Just encode the request and try to send it."""
363+ encoder.set_headers(self.client._headers)
364+ if self.body is not None:
365+ if self.readv_body is not None:
366+ raise AssertionError(
367+ "body and readv_body are mutually exclusive.")
368+ if self.body_stream is not None:
369+ raise AssertionError(
370+ "body and body_stream are mutually exclusive.")
371+ encoder.call_with_body_bytes((self.method, ) + self.args, self.body)
372+ elif self.readv_body is not None:
373+ if self.body_stream is not None:
374+ raise AssertionError(
375+ "readv_body and body_stream are mutually exclusive.")
376+ encoder.call_with_body_readv_array((self.method, ) + self.args,
377+ self.readv_body)
378+ elif self.body_stream is not None:
379+ encoder.call_with_body_stream((self.method, ) + self.args,
380+ self.body_stream)
381+ else:
382+ encoder.call(self.method, *self.args)
383+
384+
385 class SmartClientHooks(hooks.Hooks):
386
387 def __init__(self):
388
389=== modified file 'bzrlib/smart/medium.py'
390--- bzrlib/smart/medium.py 2011-04-07 10:36:24 +0000
391+++ bzrlib/smart/medium.py 2013-05-23 13:00:35 +0000
392@@ -1,4 +1,4 @@
393-# Copyright (C) 2006-2011 Canonical Ltd
394+# Copyright (C) 2006-2012 Canonical Ltd
395 #
396 # This program is free software; you can redistribute it and/or modify
397 # it under the terms of the GNU General Public License as published by
398@@ -24,6 +24,7 @@
399 bzrlib/transport/smart/__init__.py.
400 """
401
402+import errno
403 import os
404 import sys
405 import urllib
406@@ -175,6 +176,14 @@
407 ui.ui_factory.report_transport_activity(self, bytes, direction)
408
409
410+_bad_file_descriptor = (errno.EBADF,)
411+if sys.platform == 'win32':
412+ # Given on Windows if you pass a closed socket to select.select. Probably
413+ # also given if you pass a file handle to select.
414+ WSAENOTSOCK = 10038
415+ _bad_file_descriptor += (WSAENOTSOCK,)
416+
417+
418 class SmartServerStreamMedium(SmartMedium):
419 """Handles smart commands coming over a stream.
420
421@@ -238,6 +247,8 @@
422
423 :param protocol: a SmartServerRequestProtocol.
424 """
425+ if protocol is None:
426+ return
427 try:
428 self._serve_one_request_unguarded(protocol)
429 except KeyboardInterrupt:
430@@ -709,6 +720,14 @@
431 """
432 return SmartClientStreamMediumRequest(self)
433
434+ def reset(self):
435+ """We have been disconnected, reset current state.
436+
437+ This resets things like _current_request and connected state.
438+ """
439+ self.disconnect()
440+ self._current_request = None
441+
442
443 class SmartSimplePipesClientMedium(SmartClientStreamMedium):
444 """A client medium using simple pipes.
445@@ -723,11 +742,20 @@
446
447 def _accept_bytes(self, bytes):
448 """See SmartClientStreamMedium.accept_bytes."""
449- self._writeable_pipe.write(bytes)
450+ try:
451+ self._writeable_pipe.write(bytes)
452+ except IOError, e:
453+ if e.errno in (errno.EINVAL, errno.EPIPE):
454+ raise errors.ConnectionReset(
455+ "Error trying to write to subprocess", e)
456+ raise
457 self._report_activity(len(bytes), 'write')
458
459 def _flush(self):
460 """See SmartClientStreamMedium._flush()."""
461+ # Note: If flush were to fail, we'd like to raise ConnectionReset, etc.
462+ # However, testing shows that even when the child process is
463+ # gone, this doesn't error.
464 self._writeable_pipe.flush()
465
466 def _read_bytes(self, count):
467@@ -752,8 +780,8 @@
468
469 class SmartSSHClientMedium(SmartClientStreamMedium):
470 """A client medium using SSH.
471-
472- It delegates IO to a SmartClientSocketMedium or
473+
474+ It delegates IO to a SmartSimplePipesClientMedium or
475 SmartClientAlreadyConnectedSocketMedium (depending on platform).
476 """
477
478@@ -896,6 +924,20 @@
479 SmartClientSocketMedium.__init__(self, base)
480 self._host = host
481 self._port = port
482+ self._socket = None
483+
484+ def _accept_bytes(self, bytes):
485+ """See SmartClientMedium.accept_bytes."""
486+ self._ensure_connection()
487+ osutils.send_all(self._socket, bytes, self._report_activity)
488+
489+ def disconnect(self):
490+ """See SmartClientMedium.disconnect()."""
491+ if not self._connected:
492+ return
493+ self._socket.close()
494+ self._socket = None
495+ self._connected = False
496
497 def _ensure_connection(self):
498 """Connect this medium if not already connected."""
499@@ -992,5 +1034,3 @@
500 This invokes self._medium._flush to ensure all bytes are transmitted.
501 """
502 self._medium._flush()
503-
504-
505
506=== modified file 'bzrlib/smart/protocol.py'
507--- bzrlib/smart/protocol.py 2011-05-19 09:32:38 +0000
508+++ bzrlib/smart/protocol.py 2013-05-23 13:00:35 +0000
509@@ -1081,9 +1081,6 @@
510 self._real_write_func = write_func
511
512 def _write_func(self, bytes):
513- # TODO: It is probably more appropriate to use sum(map(len, _buf))
514- # for total number of bytes to write, rather than buffer based on
515- # the number of write() calls
516 # TODO: Another possibility would be to turn this into an async model.
517 # Where we let another thread know that we have some bytes if
518 # they want it, but we don't actually block for it
519@@ -1292,6 +1289,7 @@
520 _ProtocolThreeEncoder.__init__(self, medium_request.accept_bytes)
521 self._medium_request = medium_request
522 self._headers = {}
523+ self.body_stream_started = None
524
525 def set_headers(self, headers):
526 self._headers = headers.copy()
527@@ -1357,6 +1355,7 @@
528 if path is not None:
529 mutter(' (to %s)', path)
530 self._request_start_time = osutils.timer_func()
531+ self.body_stream_started = False
532 self._write_protocol_version()
533 self._write_headers(self._headers)
534 self._write_structure(args)
535@@ -1364,6 +1363,9 @@
536 # have finished sending the stream. We would notice at the end
537 # anyway, but if the medium can deliver it early then it's good
538 # to short-circuit the whole request...
539+ # Provoke any ConnectionReset failures before we start the body stream.
540+ self.flush()
541+ self.body_stream_started = True
542 for exc_info, part in _iter_with_errors(stream):
543 if exc_info is not None:
544 # Iterating the stream failed. Cleanly abort the request.
545
546=== modified file 'bzrlib/smart/request.py'
547--- bzrlib/smart/request.py 2011-05-19 09:32:38 +0000
548+++ bzrlib/smart/request.py 2013-05-23 13:00:35 +0000
549@@ -1,4 +1,4 @@
550-# Copyright (C) 2006-2010 Canonical Ltd
551+# Copyright (C) 2006-2012 Canonical Ltd
552 #
553 # This program is free software; you can redistribute it and/or modify
554 # it under the terms of the GNU General Public License as published by
555@@ -491,157 +491,202 @@
556 return SuccessfulSmartServerResponse((answer,))
557
558
559+# In the 'info' attribute, we store whether this request is 'safe' to retry if
560+# we get a disconnect while reading the response. It can have the values:
561+# read This is purely a read request, so retrying it is perfectly ok.
562+# idem An idempotent write request. Something like 'put' where if you put
563+# the same bytes twice you end up with the same final bytes.
564+# semi This is a request that isn't strictly idempotent, but doesn't
565+# result in corruption if it is retried. This is for things like
566+# 'lock' and 'unlock'. If you call lock, it updates the disk
567+# structure. If you fail to read the response, you won't be able to
568+# use the lock, because you don't have the lock token. Calling lock
569+# again will fail, because the lock is already taken. However, we
570+# can't tell if the server received our request or not. If it didn't,
571+# then retrying the request is fine, as it will actually do what we
572+# want. If it did, we will interrupt the current operation, but we
573+# are no worse off than interrupting the current operation because of
574+# a ConnectionReset.
575+# semivfs Similar to semi, but specific to a Virtual FileSystem request.
576+# stream This is a request that takes a stream that cannot be restarted if
577+# consumed. This request is 'safe' in that if we determine the
578+# connection is closed before we consume the stream, we can try
579+# again.
580+# mutate State is updated in a way that replaying that request results in a
581+# different state. For example 'append' writes more bytes to a given
582+# file. If append succeeds, it moves the file pointer.
583 request_handlers = registry.Registry()
584 request_handlers.register_lazy(
585- 'append', 'bzrlib.smart.vfs', 'AppendRequest')
586+ 'append', 'bzrlib.smart.vfs', 'AppendRequest', info='mutate')
587 request_handlers.register_lazy(
588 'Branch.get_config_file', 'bzrlib.smart.branch',
589- 'SmartServerBranchGetConfigFile')
590+ 'SmartServerBranchGetConfigFile', info='read')
591 request_handlers.register_lazy(
592- 'Branch.get_parent', 'bzrlib.smart.branch', 'SmartServerBranchGetParent')
593+ 'Branch.get_parent', 'bzrlib.smart.branch', 'SmartServerBranchGetParent',
594+ info='read')
595 request_handlers.register_lazy(
596 'Branch.get_tags_bytes', 'bzrlib.smart.branch',
597- 'SmartServerBranchGetTagsBytes')
598+ 'SmartServerBranchGetTagsBytes', info='read')
599 request_handlers.register_lazy(
600 'Branch.set_tags_bytes', 'bzrlib.smart.branch',
601- 'SmartServerBranchSetTagsBytes')
602+ 'SmartServerBranchSetTagsBytes', info='idem')
603 request_handlers.register_lazy(
604 'Branch.heads_to_fetch', 'bzrlib.smart.branch',
605- 'SmartServerBranchHeadsToFetch')
606-request_handlers.register_lazy(
607- 'Branch.get_stacked_on_url', 'bzrlib.smart.branch', 'SmartServerBranchRequestGetStackedOnURL')
608-request_handlers.register_lazy(
609- 'Branch.last_revision_info', 'bzrlib.smart.branch', 'SmartServerBranchRequestLastRevisionInfo')
610-request_handlers.register_lazy(
611- 'Branch.lock_write', 'bzrlib.smart.branch', 'SmartServerBranchRequestLockWrite')
612-request_handlers.register_lazy( 'Branch.revision_history',
613- 'bzrlib.smart.branch', 'SmartServerRequestRevisionHistory')
614-request_handlers.register_lazy( 'Branch.set_config_option',
615- 'bzrlib.smart.branch', 'SmartServerBranchRequestSetConfigOption')
616-request_handlers.register_lazy( 'Branch.set_config_option_dict',
617- 'bzrlib.smart.branch', 'SmartServerBranchRequestSetConfigOptionDict')
618-request_handlers.register_lazy( 'Branch.set_last_revision',
619- 'bzrlib.smart.branch', 'SmartServerBranchRequestSetLastRevision')
620+ 'SmartServerBranchHeadsToFetch', info='read')
621+request_handlers.register_lazy(
622+ 'Branch.get_stacked_on_url', 'bzrlib.smart.branch',
623+ 'SmartServerBranchRequestGetStackedOnURL', info='read')
624+request_handlers.register_lazy(
625+ 'Branch.last_revision_info', 'bzrlib.smart.branch',
626+ 'SmartServerBranchRequestLastRevisionInfo', info='read')
627+request_handlers.register_lazy(
628+ 'Branch.lock_write', 'bzrlib.smart.branch',
629+ 'SmartServerBranchRequestLockWrite', info='semi')
630+request_handlers.register_lazy(
631+ 'Branch.revision_history', 'bzrlib.smart.branch',
632+ 'SmartServerRequestRevisionHistory', info='read')
633+request_handlers.register_lazy(
634+ 'Branch.set_config_option', 'bzrlib.smart.branch',
635+ 'SmartServerBranchRequestSetConfigOption', info='idem')
636+request_handlers.register_lazy(
637+ 'Branch.set_config_option_dict', 'bzrlib.smart.branch',
638+ 'SmartServerBranchRequestSetConfigOptionDict', info='idem')
639+request_handlers.register_lazy(
640+ 'Branch.set_last_revision', 'bzrlib.smart.branch',
641+ 'SmartServerBranchRequestSetLastRevision', info='idem')
642 request_handlers.register_lazy(
643 'Branch.set_last_revision_info', 'bzrlib.smart.branch',
644- 'SmartServerBranchRequestSetLastRevisionInfo')
645+ 'SmartServerBranchRequestSetLastRevisionInfo', info='idem')
646 request_handlers.register_lazy(
647 'Branch.set_last_revision_ex', 'bzrlib.smart.branch',
648- 'SmartServerBranchRequestSetLastRevisionEx')
649+ 'SmartServerBranchRequestSetLastRevisionEx', info='idem')
650 request_handlers.register_lazy(
651 'Branch.set_parent_location', 'bzrlib.smart.branch',
652- 'SmartServerBranchRequestSetParentLocation')
653+ 'SmartServerBranchRequestSetParentLocation', info='idem')
654 request_handlers.register_lazy(
655- 'Branch.unlock', 'bzrlib.smart.branch', 'SmartServerBranchRequestUnlock')
656+ 'Branch.unlock', 'bzrlib.smart.branch',
657+ 'SmartServerBranchRequestUnlock', info='semi')
658 request_handlers.register_lazy(
659 'BzrDir.cloning_metadir', 'bzrlib.smart.bzrdir',
660- 'SmartServerBzrDirRequestCloningMetaDir')
661+ 'SmartServerBzrDirRequestCloningMetaDir', info='read')
662 request_handlers.register_lazy(
663 'BzrDir.create_branch', 'bzrlib.smart.bzrdir',
664- 'SmartServerRequestCreateBranch')
665+ 'SmartServerRequestCreateBranch', info='semi')
666 request_handlers.register_lazy(
667 'BzrDir.create_repository', 'bzrlib.smart.bzrdir',
668- 'SmartServerRequestCreateRepository')
669+ 'SmartServerRequestCreateRepository', info='semi')
670 request_handlers.register_lazy(
671 'BzrDir.find_repository', 'bzrlib.smart.bzrdir',
672- 'SmartServerRequestFindRepositoryV1')
673+ 'SmartServerRequestFindRepositoryV1', info='read')
674 request_handlers.register_lazy(
675 'BzrDir.find_repositoryV2', 'bzrlib.smart.bzrdir',
676- 'SmartServerRequestFindRepositoryV2')
677+ 'SmartServerRequestFindRepositoryV2', info='read')
678 request_handlers.register_lazy(
679 'BzrDir.find_repositoryV3', 'bzrlib.smart.bzrdir',
680- 'SmartServerRequestFindRepositoryV3')
681+ 'SmartServerRequestFindRepositoryV3', info='read')
682 request_handlers.register_lazy(
683 'BzrDir.get_config_file', 'bzrlib.smart.bzrdir',
684- 'SmartServerBzrDirRequestConfigFile')
685+ 'SmartServerBzrDirRequestConfigFile', info='read')
686 request_handlers.register_lazy(
687 'BzrDirFormat.initialize', 'bzrlib.smart.bzrdir',
688- 'SmartServerRequestInitializeBzrDir')
689+ 'SmartServerRequestInitializeBzrDir', info='semi')
690 request_handlers.register_lazy(
691 'BzrDirFormat.initialize_ex_1.16', 'bzrlib.smart.bzrdir',
692- 'SmartServerRequestBzrDirInitializeEx')
693-request_handlers.register_lazy(
694- 'BzrDir.open', 'bzrlib.smart.bzrdir', 'SmartServerRequestOpenBzrDir')
695-request_handlers.register_lazy(
696- 'BzrDir.open_2.1', 'bzrlib.smart.bzrdir', 'SmartServerRequestOpenBzrDir_2_1')
697+ 'SmartServerRequestBzrDirInitializeEx', info='semi')
698+request_handlers.register_lazy(
699+ 'BzrDir.open', 'bzrlib.smart.bzrdir', 'SmartServerRequestOpenBzrDir',
700+ info='read')
701+request_handlers.register_lazy(
702+ 'BzrDir.open_2.1', 'bzrlib.smart.bzrdir',
703+ 'SmartServerRequestOpenBzrDir_2_1', info='read')
704 request_handlers.register_lazy(
705 'BzrDir.open_branch', 'bzrlib.smart.bzrdir',
706- 'SmartServerRequestOpenBranch')
707+ 'SmartServerRequestOpenBranch', info='read')
708 request_handlers.register_lazy(
709 'BzrDir.open_branchV2', 'bzrlib.smart.bzrdir',
710- 'SmartServerRequestOpenBranchV2')
711+ 'SmartServerRequestOpenBranchV2', info='read')
712 request_handlers.register_lazy(
713 'BzrDir.open_branchV3', 'bzrlib.smart.bzrdir',
714- 'SmartServerRequestOpenBranchV3')
715-request_handlers.register_lazy(
716- 'delete', 'bzrlib.smart.vfs', 'DeleteRequest')
717-request_handlers.register_lazy(
718- 'get', 'bzrlib.smart.vfs', 'GetRequest')
719-request_handlers.register_lazy(
720- 'get_bundle', 'bzrlib.smart.request', 'GetBundleRequest')
721-request_handlers.register_lazy(
722- 'has', 'bzrlib.smart.vfs', 'HasRequest')
723-request_handlers.register_lazy(
724- 'hello', 'bzrlib.smart.request', 'HelloRequest')
725-request_handlers.register_lazy(
726- 'iter_files_recursive', 'bzrlib.smart.vfs', 'IterFilesRecursiveRequest')
727-request_handlers.register_lazy(
728- 'list_dir', 'bzrlib.smart.vfs', 'ListDirRequest')
729-request_handlers.register_lazy(
730- 'mkdir', 'bzrlib.smart.vfs', 'MkdirRequest')
731-request_handlers.register_lazy(
732- 'move', 'bzrlib.smart.vfs', 'MoveRequest')
733-request_handlers.register_lazy(
734- 'put', 'bzrlib.smart.vfs', 'PutRequest')
735-request_handlers.register_lazy(
736- 'put_non_atomic', 'bzrlib.smart.vfs', 'PutNonAtomicRequest')
737-request_handlers.register_lazy(
738- 'readv', 'bzrlib.smart.vfs', 'ReadvRequest')
739-request_handlers.register_lazy(
740- 'rename', 'bzrlib.smart.vfs', 'RenameRequest')
741+ 'SmartServerRequestOpenBranchV3', info='read')
742+request_handlers.register_lazy(
743+ 'delete', 'bzrlib.smart.vfs', 'DeleteRequest', info='semivfs')
744+request_handlers.register_lazy(
745+ 'get', 'bzrlib.smart.vfs', 'GetRequest', info='read')
746+request_handlers.register_lazy(
747+ 'get_bundle', 'bzrlib.smart.request', 'GetBundleRequest', info='read')
748+request_handlers.register_lazy(
749+ 'has', 'bzrlib.smart.vfs', 'HasRequest', info='read')
750+request_handlers.register_lazy(
751+ 'hello', 'bzrlib.smart.request', 'HelloRequest', info='read')
752+request_handlers.register_lazy(
753+ 'iter_files_recursive', 'bzrlib.smart.vfs', 'IterFilesRecursiveRequest',
754+ info='read')
755+request_handlers.register_lazy(
756+ 'list_dir', 'bzrlib.smart.vfs', 'ListDirRequest', info='read')
757+request_handlers.register_lazy(
758+ 'mkdir', 'bzrlib.smart.vfs', 'MkdirRequest', info='semivfs')
759+request_handlers.register_lazy(
760+ 'move', 'bzrlib.smart.vfs', 'MoveRequest', info='semivfs')
761+request_handlers.register_lazy(
762+ 'put', 'bzrlib.smart.vfs', 'PutRequest', info='idem')
763+request_handlers.register_lazy(
764+ 'put_non_atomic', 'bzrlib.smart.vfs', 'PutNonAtomicRequest', info='idem')
765+request_handlers.register_lazy(
766+ 'readv', 'bzrlib.smart.vfs', 'ReadvRequest', info='read')
767+request_handlers.register_lazy(
768+ 'rename', 'bzrlib.smart.vfs', 'RenameRequest', info='semivfs')
769 request_handlers.register_lazy(
770 'PackRepository.autopack', 'bzrlib.smart.packrepository',
771- 'SmartServerPackRepositoryAutopack')
772-request_handlers.register_lazy('Repository.gather_stats',
773- 'bzrlib.smart.repository',
774- 'SmartServerRepositoryGatherStats')
775-request_handlers.register_lazy('Repository.get_parent_map',
776- 'bzrlib.smart.repository',
777- 'SmartServerRepositoryGetParentMap')
778-request_handlers.register_lazy(
779- 'Repository.get_revision_graph', 'bzrlib.smart.repository', 'SmartServerRepositoryGetRevisionGraph')
780-request_handlers.register_lazy(
781- 'Repository.has_revision', 'bzrlib.smart.repository', 'SmartServerRequestHasRevision')
782-request_handlers.register_lazy(
783- 'Repository.insert_stream', 'bzrlib.smart.repository', 'SmartServerRepositoryInsertStream')
784-request_handlers.register_lazy(
785- 'Repository.insert_stream_1.19', 'bzrlib.smart.repository', 'SmartServerRepositoryInsertStream_1_19')
786-request_handlers.register_lazy(
787- 'Repository.insert_stream_locked', 'bzrlib.smart.repository', 'SmartServerRepositoryInsertStreamLocked')
788-request_handlers.register_lazy(
789- 'Repository.is_shared', 'bzrlib.smart.repository', 'SmartServerRepositoryIsShared')
790-request_handlers.register_lazy(
791- 'Repository.lock_write', 'bzrlib.smart.repository', 'SmartServerRepositoryLockWrite')
792+ 'SmartServerPackRepositoryAutopack', info='idem')
793+request_handlers.register_lazy(
794+ 'Repository.gather_stats', 'bzrlib.smart.repository',
795+ 'SmartServerRepositoryGatherStats', info='read')
796+request_handlers.register_lazy(
797+ 'Repository.get_parent_map', 'bzrlib.smart.repository',
798+ 'SmartServerRepositoryGetParentMap', info='read')
799+request_handlers.register_lazy(
800+ 'Repository.get_revision_graph', 'bzrlib.smart.repository',
801+ 'SmartServerRepositoryGetRevisionGraph', info='read')
802+request_handlers.register_lazy(
803+ 'Repository.has_revision', 'bzrlib.smart.repository',
804+ 'SmartServerRequestHasRevision', info='read')
805+request_handlers.register_lazy(
806+ 'Repository.insert_stream', 'bzrlib.smart.repository',
807+ 'SmartServerRepositoryInsertStream', info='stream')
808+request_handlers.register_lazy(
809+ 'Repository.insert_stream_1.19', 'bzrlib.smart.repository',
810+ 'SmartServerRepositoryInsertStream_1_19', info='stream')
811+request_handlers.register_lazy(
812+ 'Repository.insert_stream_locked', 'bzrlib.smart.repository',
813+ 'SmartServerRepositoryInsertStreamLocked', info='stream')
814+request_handlers.register_lazy(
815+ 'Repository.is_shared', 'bzrlib.smart.repository',
816+ 'SmartServerRepositoryIsShared', info='read')
817+request_handlers.register_lazy(
818+ 'Repository.lock_write', 'bzrlib.smart.repository',
819+ 'SmartServerRepositoryLockWrite', info='semi')
820 request_handlers.register_lazy(
821 'Repository.set_make_working_trees', 'bzrlib.smart.repository',
822- 'SmartServerRepositorySetMakeWorkingTrees')
823+ 'SmartServerRepositorySetMakeWorkingTrees', info='idem')
824 request_handlers.register_lazy(
825- 'Repository.unlock', 'bzrlib.smart.repository', 'SmartServerRepositoryUnlock')
826+ 'Repository.unlock', 'bzrlib.smart.repository',
827+ 'SmartServerRepositoryUnlock', info='semi')
828 request_handlers.register_lazy(
829 'Repository.get_rev_id_for_revno', 'bzrlib.smart.repository',
830- 'SmartServerRepositoryGetRevIdForRevno')
831+ 'SmartServerRepositoryGetRevIdForRevno', info='read')
832 request_handlers.register_lazy(
833 'Repository.get_stream', 'bzrlib.smart.repository',
834- 'SmartServerRepositoryGetStream')
835+ 'SmartServerRepositoryGetStream', info='read')
836 request_handlers.register_lazy(
837 'Repository.get_stream_1.19', 'bzrlib.smart.repository',
838- 'SmartServerRepositoryGetStream_1_19')
839+ 'SmartServerRepositoryGetStream_1_19', info='read')
840 request_handlers.register_lazy(
841 'Repository.tarball', 'bzrlib.smart.repository',
842- 'SmartServerRepositoryTarball')
843-request_handlers.register_lazy(
844- 'rmdir', 'bzrlib.smart.vfs', 'RmdirRequest')
845-request_handlers.register_lazy(
846- 'stat', 'bzrlib.smart.vfs', 'StatRequest')
847-request_handlers.register_lazy(
848- 'Transport.is_readonly', 'bzrlib.smart.request', 'SmartServerIsReadonly')
849+ 'SmartServerRepositoryTarball', info='read')
850+request_handlers.register_lazy(
851+ 'rmdir', 'bzrlib.smart.vfs', 'RmdirRequest', info='semivfs')
852+request_handlers.register_lazy(
853+ 'stat', 'bzrlib.smart.vfs', 'StatRequest', info='read')
854+request_handlers.register_lazy(
855+ 'Transport.is_readonly', 'bzrlib.smart.request',
856+ 'SmartServerIsReadonly', info='read')
857
858=== modified file 'bzrlib/tests/__init__.py'
859--- bzrlib/tests/__init__.py 2012-11-05 11:20:40 +0000
860+++ bzrlib/tests/__init__.py 2013-05-23 13:00:35 +0000
861@@ -2336,8 +2336,10 @@
862 from bzrlib.smart import request
863 request_handlers = request.request_handlers
864 orig_method = request_handlers.get(verb)
865+ orig_info = request_handlers.get_info(verb)
866 request_handlers.remove(verb)
867- self.addCleanup(request_handlers.register, verb, orig_method)
868+ self.addCleanup(request_handlers.register, verb, orig_method,
869+ info=orig_info)
870
871
872 class CapturedCall(object):
873
874=== modified file 'bzrlib/tests/test_bundle.py'
875--- bzrlib/tests/test_bundle.py 2011-05-13 12:51:05 +0000
876+++ bzrlib/tests/test_bundle.py 2013-05-23 13:00:35 +0000
877@@ -1852,20 +1852,23 @@
878 self.sock.bind(('127.0.0.1', 0))
879 self.sock.listen(1)
880 self.port = self.sock.getsockname()[1]
881+ self.stopping = threading.Event()
882 self.thread = threading.Thread(
883 name='%s (port %d)' % (self.__class__.__name__, self.port),
884 target=self.accept_and_close)
885 self.thread.start()
886
887 def accept_and_close(self):
888- conn, addr = self.sock.accept()
889- conn.shutdown(socket.SHUT_RDWR)
890- conn.close()
891+ while not self.stopping.isSet():
892+ conn, addr = self.sock.accept()
893+ conn.shutdown(socket.SHUT_RDWR)
894+ conn.close()
895
896 def get_url(self):
897 return 'bzr://127.0.0.1:%d/' % (self.port,)
898
899 def stop_server(self):
900+ self.stopping.set()
901 try:
902 # make sure the thread dies by connecting to the listening socket,
903 # just in case the test failed to do so.
904
905=== modified file 'bzrlib/tests/test_osutils.py'
906--- bzrlib/tests/test_osutils.py 2013-05-23 08:25:07 +0000
907+++ bzrlib/tests/test_osutils.py 2013-05-23 13:00:35 +0000
908@@ -1,4 +1,4 @@
909-# Copyright (C) 2005-2011 Canonical Ltd
910+# Copyright (C) 2005-2012 Canonical Ltd
911 #
912 # This program is free software; you can redistribute it and/or modify
913 # it under the terms of the GNU General Public License as published by
914@@ -872,6 +872,45 @@
915 self.assertEqual('/etc/shadow', osutils._posix_normpath('///etc/shadow'))
916
917
918+class TestSendAll(tests.TestCase):
919+
920+ def test_send_with_disconnected_socket(self):
921+ class DisconnectedSocket(object):
922+ def __init__(self, err):
923+ self.err = err
924+ def send(self, content):
925+ raise self.err
926+ def close(self):
927+ pass
928+ # All of these should be treated as ConnectionReset
929+ errs = []
930+ for err_cls in (IOError, socket.error):
931+ for errnum in osutils._end_of_stream_errors:
932+ errs.append(err_cls(errnum))
933+ for err in errs:
934+ sock = DisconnectedSocket(err)
935+ self.assertRaises(errors.ConnectionReset,
936+ osutils.send_all, sock, 'some more content')
937+
938+ def test_send_with_no_progress(self):
939+ # See https://bugs.launchpad.net/bzr/+bug/1047309
940+ # It seems that paramiko can get into a state where it doesn't error,
941+ # but it returns 0 bytes sent for requests over and over again.
942+ class NoSendingSocket(object):
943+ def __init__(self):
944+ self.call_count = 0
945+ def send(self, bytes):
946+ self.call_count += 1
947+ if self.call_count > 100:
948+ # Prevent the test suite from hanging
949+ raise RuntimeError('too many calls')
950+ return 0
951+ sock = NoSendingSocket()
952+ self.assertRaises(errors.ConnectionReset,
953+ osutils.send_all, sock, 'content')
954+ self.assertEqual(1, sock.call_count)
955+
956+
957 class TestWin32Funcs(tests.TestCase):
958 """Test that _win32 versions of os utilities return appropriate paths."""
959
960
961=== modified file 'bzrlib/tests/test_remote.py'
962--- bzrlib/tests/test_remote.py 2011-08-09 14:18:05 +0000
963+++ bzrlib/tests/test_remote.py 2013-05-23 13:00:35 +0000
964@@ -3411,9 +3411,11 @@
965 def override_verb(self, verb_name, verb):
966 request_handlers = request.request_handlers
967 orig_verb = request_handlers.get(verb_name)
968- request_handlers.register(verb_name, verb, override_existing=True)
969+ orig_info = request_handlers.get_info(verb_name)
970+ request_handlers.register(verb_name, verb, override_existing=True,
971+ info=orig_info)
972 self.addCleanup(request_handlers.register, verb_name, orig_verb,
973- override_existing=True)
974+ override_existing=True, info=orig_info)
975
976 def test_fetch_everything_backwards_compat(self):
977 """Can fetch with EverythingResult even with pre 2.4 servers.
978
979=== modified file 'bzrlib/tests/test_smart.py'
980--- bzrlib/tests/test_smart.py 2011-05-26 08:05:45 +0000
981+++ bzrlib/tests/test_smart.py 2013-05-23 13:00:35 +0000
982@@ -1860,8 +1860,11 @@
983 """All registered request_handlers can be found."""
984 # If there's a typo in a register_lazy call, this loop will fail with
985 # an AttributeError.
986- for key, item in smart_req.request_handlers.iteritems():
987- pass
988+ for key in smart_req.request_handlers.keys():
989+ try:
990+ item = smart_req.request_handlers.get(key)
991+ except AttributeError, e:
992+ raise AttributeError('failed to get %s: %s' % (key, e))
993
994 def assertHandlerEqual(self, verb, handler):
995 self.assertEqual(smart_req.request_handlers.get(verb), handler)
996
997=== modified file 'bzrlib/tests/test_smart_request.py'
998--- bzrlib/tests/test_smart_request.py 2011-03-02 21:04:00 +0000
999+++ bzrlib/tests/test_smart_request.py 2013-05-23 13:00:35 +0000
1000@@ -118,6 +118,16 @@
1001 self.assertEqual(
1002 [[transport]] * 3, handler._command.jail_transports_log)
1003
1004+ def test_all_registered_requests_are_safety_qualified(self):
1005+ unclassified_requests = []
1006+ allowed_info = ('read', 'idem', 'mutate', 'semivfs', 'semi', 'stream')
1007+ for key in request.request_handlers.keys():
1008+ info = request.request_handlers.get_info(key)
1009+ if info is None or info not in allowed_info:
1010+ unclassified_requests.append(key)
1011+ if unclassified_requests:
1012+ self.fail('These requests were not categorized as safe/unsafe'
1013+ ' to retry: %s' % (unclassified_requests,))
1014
1015
1016 class TestSmartRequestHandlerErrorTranslation(TestCase):
1017
1018=== modified file 'bzrlib/tests/test_smart_transport.py'
1019--- bzrlib/tests/test_smart_transport.py 2011-05-26 08:05:45 +0000
1020+++ bzrlib/tests/test_smart_transport.py 2013-05-23 13:00:35 +0000
1021@@ -18,13 +18,17 @@
1022
1023 # all of this deals with byte strings so this is safe
1024 from cStringIO import StringIO
1025+import errno
1026 import os
1027 import socket
1028+import subprocess
1029+import sys
1030 import threading
1031
1032 import bzrlib
1033 from bzrlib import (
1034 bzrdir,
1035+ debug,
1036 errors,
1037 osutils,
1038 tests,
1039@@ -53,6 +57,29 @@
1040 )
1041
1042
1043+def create_file_pipes():
1044+ r, w = os.pipe()
1045+ # These must be opened without buffering, or we get undefined results
1046+ rf = os.fdopen(r, 'rb', 0)
1047+ wf = os.fdopen(w, 'wb', 0)
1048+ return rf, wf
1049+
1050+
1051+def portable_socket_pair():
1052+ """Return a pair of TCP sockets connected to each other.
1053+
1054+ Unlike socket.socketpair, this should work on Windows.
1055+ """
1056+ listen_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1057+ listen_sock.bind(('127.0.0.1', 0))
1058+ listen_sock.listen(1)
1059+ client_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1060+ client_sock.connect(listen_sock.getsockname())
1061+ server_sock, addr = listen_sock.accept()
1062+ listen_sock.close()
1063+ return server_sock, client_sock
1064+
1065+
1066 class StringIOSSHVendor(object):
1067 """A SSH vendor that uses StringIO to buffer writes and answer reads."""
1068
1069@@ -67,6 +94,27 @@
1070 return StringIOSSHConnection(self)
1071
1072
1073+class FirstRejectedStringIOSSHVendor(StringIOSSHVendor):
1074+ """The first connection will be considered closed.
1075+
1076+ The second connection will succeed normally.
1077+ """
1078+
1079+ def __init__(self, read_from, write_to, fail_at_write=True):
1080+ super(FirstRejectedStringIOSSHVendor, self).__init__(read_from,
1081+ write_to)
1082+ self.fail_at_write = fail_at_write
1083+ self._first = True
1084+
1085+ def connect_ssh(self, username, password, host, port, command):
1086+ self.calls.append(('connect_ssh', username, password, host, port,
1087+ command))
1088+ if self._first:
1089+ self._first = False
1090+ return ClosedSSHConnection(self)
1091+ return StringIOSSHConnection(self)
1092+
1093+
1094 class StringIOSSHConnection(ssh.SSHConnection):
1095 """A SSH connection that uses StringIO to buffer writes and answer reads."""
1096
1097@@ -82,6 +130,29 @@
1098 return 'pipes', (self.vendor.read_from, self.vendor.write_to)
1099
1100
1101+class ClosedSSHConnection(ssh.SSHConnection):
1102+ """An SSH connection that just has closed channels."""
1103+
1104+ def __init__(self, vendor):
1105+ self.vendor = vendor
1106+
1107+ def close(self):
1108+ self.vendor.calls.append(('close', ))
1109+
1110+ def get_sock_or_pipes(self):
1111+ # We create matching pipes, and then close the ssh side
1112+ bzr_read, ssh_write = create_file_pipes()
1113+ # We always fail when bzr goes to read
1114+ ssh_write.close()
1115+ if self.vendor.fail_at_write:
1116+ # If set, we'll also fail when bzr goes to write
1117+ ssh_read, bzr_write = create_file_pipes()
1118+ ssh_read.close()
1119+ else:
1120+ bzr_write = self.vendor.write_to
1121+ return 'pipes', (bzr_read, bzr_write)
1122+
1123+
1124 class _InvalidHostnameFeature(tests.Feature):
1125 """Does 'non_existent.invalid' fail to resolve?
1126
1127@@ -177,6 +248,91 @@
1128 client_medium._accept_bytes('abc')
1129 self.assertEqual('abc', output.getvalue())
1130
1131+ def test_simple_pipes__accept_bytes_subprocess_closed(self):
1132+ # It is unfortunate that we have to use Popen for this. However,
1133+ # os.pipe() does not behave the same as subprocess.Popen().
1134+ # On Windows, if you use os.pipe() and close the write side,
1135+ # read.read() hangs. On Linux, read.read() returns the empty string.
1136+ p = subprocess.Popen([sys.executable, '-c',
1137+ 'import sys\n'
1138+ 'sys.stdout.write(sys.stdin.read(4))\n'
1139+ 'sys.stdout.close()\n'],
1140+ stdout=subprocess.PIPE, stdin=subprocess.PIPE)
1141+ client_medium = medium.SmartSimplePipesClientMedium(
1142+ p.stdout, p.stdin, 'base')
1143+ client_medium._accept_bytes('abc\n')
1144+ self.assertEqual('abc', client_medium._read_bytes(3))
1145+ p.wait()
1146+ # While writing to the underlying pipe,
1147+ # Windows py2.6.6 we get IOError(EINVAL)
1148+ # Lucid py2.6.5, we get IOError(EPIPE)
1149+ # In both cases, it should be wrapped to ConnectionReset
1150+ self.assertRaises(errors.ConnectionReset,
1151+ client_medium._accept_bytes, 'more')
1152+
1153+ def test_simple_pipes__accept_bytes_pipe_closed(self):
1154+ child_read, client_write = create_file_pipes()
1155+ client_medium = medium.SmartSimplePipesClientMedium(
1156+ None, client_write, 'base')
1157+ client_medium._accept_bytes('abc\n')
1158+ self.assertEqual('abc\n', child_read.read(4))
1159+ # While writing to the underlying pipe,
1160+ # Windows py2.6.6 we get IOError(EINVAL)
1161+ # Lucid py2.6.5, we get IOError(EPIPE)
1162+ # In both cases, it should be wrapped to ConnectionReset
1163+ child_read.close()
1164+ self.assertRaises(errors.ConnectionReset,
1165+ client_medium._accept_bytes, 'more')
1166+
1167+ def test_simple_pipes__flush_pipe_closed(self):
1168+ child_read, client_write = create_file_pipes()
1169+ client_medium = medium.SmartSimplePipesClientMedium(
1170+ None, client_write, 'base')
1171+ client_medium._accept_bytes('abc\n')
1172+ child_read.close()
1173+ # Even though the pipe is closed, flush on the write side seems to be a
1174+ # no-op, rather than a failure.
1175+ client_medium._flush()
1176+
1177+ def test_simple_pipes__flush_subprocess_closed(self):
1178+ p = subprocess.Popen([sys.executable, '-c',
1179+ 'import sys\n'
1180+ 'sys.stdout.write(sys.stdin.read(4))\n'
1181+ 'sys.stdout.close()\n'],
1182+ stdout=subprocess.PIPE, stdin=subprocess.PIPE)
1183+ client_medium = medium.SmartSimplePipesClientMedium(
1184+ p.stdout, p.stdin, 'base')
1185+ client_medium._accept_bytes('abc\n')
1186+ p.wait()
1187+ # Even though the child process is dead, flush seems to be a no-op.
1188+ client_medium._flush()
1189+
1190+ def test_simple_pipes__read_bytes_pipe_closed(self):
1191+ child_read, client_write = create_file_pipes()
1192+ client_medium = medium.SmartSimplePipesClientMedium(
1193+ child_read, client_write, 'base')
1194+ client_medium._accept_bytes('abc\n')
1195+ client_write.close()
1196+ self.assertEqual('abc\n', client_medium._read_bytes(4))
1197+ self.assertEqual('', client_medium._read_bytes(4))
1198+
1199+ def test_simple_pipes__read_bytes_subprocess_closed(self):
1200+ p = subprocess.Popen([sys.executable, '-c',
1201+ 'import sys\n'
1202+ 'if sys.platform == "win32":\n'
1203+ ' import msvcrt, os\n'
1204+ ' msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)\n'
1205+ ' msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)\n'
1206+ 'sys.stdout.write(sys.stdin.read(4))\n'
1207+ 'sys.stdout.close()\n'],
1208+ stdout=subprocess.PIPE, stdin=subprocess.PIPE)
1209+ client_medium = medium.SmartSimplePipesClientMedium(
1210+ p.stdout, p.stdin, 'base')
1211+ client_medium._accept_bytes('abc\n')
1212+ p.wait()
1213+ self.assertEqual('abc\n', client_medium._read_bytes(4))
1214+ self.assertEqual('', client_medium._read_bytes(4))
1215+
1216 def test_simple_pipes_client_disconnect_does_nothing(self):
1217 # calling disconnect does nothing.
1218 input = StringIO()
1219@@ -564,6 +720,28 @@
1220 request.finished_reading()
1221 self.assertRaises(errors.ReadingCompleted, request.read_bytes, None)
1222
1223+ def test_reset(self):
1224+ server_sock, client_sock = portable_socket_pair()
1225+ # TODO: Use SmartClientAlreadyConnectedSocketMedium for the versions of
1226+ # bzr where it exists.
1227+ client_medium = medium.SmartTCPClientMedium(None, None, None)
1228+ client_medium._socket = client_sock
1229+ client_medium._connected = True
1230+ req = client_medium.get_request()
1231+ self.assertRaises(errors.TooManyConcurrentRequests,
1232+ client_medium.get_request)
1233+ client_medium.reset()
1234+ # The stream should be reset, marked as disconnected, though ready for
1235+ # us to make a new request
1236+ self.assertFalse(client_medium._connected)
1237+ self.assertIs(None, client_medium._socket)
1238+ try:
1239+ self.assertEqual('', client_sock.recv(1))
1240+ except socket.error, e:
1241+ if e.errno not in (errno.EBADF,):
1242+ raise
1243+ req = client_medium.get_request()
1244+
1245
1246 class RemoteTransportTests(test_smart.TestCaseWithSmartMedium):
1247
1248@@ -617,20 +795,6 @@
1249 super(TestSmartServerStreamMedium, self).setUp()
1250 self.overrideEnv('BZR_NO_SMART_VFS', None)
1251
1252- def portable_socket_pair(self):
1253- """Return a pair of TCP sockets connected to each other.
1254-
1255- Unlike socket.socketpair, this should work on Windows.
1256- """
1257- listen_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1258- listen_sock.bind(('127.0.0.1', 0))
1259- listen_sock.listen(1)
1260- client_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1261- client_sock.connect(listen_sock.getsockname())
1262- server_sock, addr = listen_sock.accept()
1263- listen_sock.close()
1264- return server_sock, client_sock
1265-
1266 def test_smart_query_version(self):
1267 """Feed a canned query version to a server"""
1268 # wire-to-wire, using the whole stack
1269@@ -695,7 +859,7 @@
1270
1271 def test_socket_stream_with_bulk_data(self):
1272 sample_request_bytes = 'command\n9\nbulk datadone\n'
1273- server_sock, client_sock = self.portable_socket_pair()
1274+ server_sock, client_sock = portable_socket_pair()
1275 server = medium.SmartServerSocketStreamMedium(
1276 server_sock, None)
1277 sample_protocol = SampleRequest(expected_bytes=sample_request_bytes)
1278@@ -714,7 +878,7 @@
1279 self.assertTrue(server.finished)
1280
1281 def test_socket_stream_shutdown_detection(self):
1282- server_sock, client_sock = self.portable_socket_pair()
1283+ server_sock, client_sock = portable_socket_pair()
1284 client_sock.close()
1285 server = medium.SmartServerSocketStreamMedium(
1286 server_sock, None)
1287@@ -734,7 +898,7 @@
1288 rest_of_request_bytes = 'lo\n'
1289 expected_response = (
1290 protocol.RESPONSE_VERSION_TWO + 'success\nok\x012\n')
1291- server_sock, client_sock = self.portable_socket_pair()
1292+ server_sock, client_sock = portable_socket_pair()
1293 server = medium.SmartServerSocketStreamMedium(
1294 server_sock, None)
1295 client_sock.sendall(incomplete_request_bytes)
1296@@ -810,7 +974,7 @@
1297 # _serve_one_request should still process both of them as if they had
1298 # been received separately.
1299 sample_request_bytes = 'command\n'
1300- server_sock, client_sock = self.portable_socket_pair()
1301+ server_sock, client_sock = portable_socket_pair()
1302 server = medium.SmartServerSocketStreamMedium(
1303 server_sock, None)
1304 first_protocol = SampleRequest(expected_bytes=sample_request_bytes)
1305@@ -847,7 +1011,7 @@
1306 self.assertTrue(server.finished)
1307
1308 def test_socket_stream_error_handling(self):
1309- server_sock, client_sock = self.portable_socket_pair()
1310+ server_sock, client_sock = portable_socket_pair()
1311 server = medium.SmartServerSocketStreamMedium(
1312 server_sock, None)
1313 fake_protocol = ErrorRaisingProtocol(Exception('boom'))
1314@@ -868,7 +1032,7 @@
1315 self.assertEqual('', from_server.getvalue())
1316
1317 def test_socket_stream_keyboard_interrupt_handling(self):
1318- server_sock, client_sock = self.portable_socket_pair()
1319+ server_sock, client_sock = portable_socket_pair()
1320 server = medium.SmartServerSocketStreamMedium(
1321 server_sock, None)
1322 fake_protocol = ErrorRaisingProtocol(KeyboardInterrupt('boom'))
1323@@ -885,7 +1049,7 @@
1324 return server._build_protocol()
1325
1326 def build_protocol_socket(self, bytes):
1327- server_sock, client_sock = self.portable_socket_pair()
1328+ server_sock, client_sock = portable_socket_pair()
1329 server = medium.SmartServerSocketStreamMedium(
1330 server_sock, None)
1331 client_sock.sendall(bytes)
1332@@ -2793,6 +2957,33 @@
1333 'e', # end
1334 output.getvalue())
1335
1336+ def test_records_start_of_body_stream(self):
1337+ requester, output = self.make_client_encoder_and_output()
1338+ requester.set_headers({})
1339+ in_stream = [False]
1340+ def stream_checker():
1341+ self.assertTrue(requester.body_stream_started)
1342+ in_stream[0] = True
1343+ yield 'content'
1344+ flush_called = []
1345+ orig_flush = requester.flush
1346+ def tracked_flush():
1347+ flush_called.append(in_stream[0])
1348+ if in_stream[0]:
1349+ self.assertTrue(requester.body_stream_started)
1350+ else:
1351+ self.assertFalse(requester.body_stream_started)
1352+ return orig_flush()
1353+ requester.flush = tracked_flush
1354+ requester.call_with_body_stream(('one arg',), stream_checker())
1355+ self.assertEqual(
1356+ 'bzr message 3 (bzr 1.6)\n' # protocol version
1357+ '\x00\x00\x00\x02de' # headers
1358+ 's\x00\x00\x00\x0bl7:one arge' # args
1359+ 'b\x00\x00\x00\x07content' # body
1360+ 'e', output.getvalue())
1361+ self.assertEqual([False, True, True], flush_called)
1362+
1363
1364 class StubMediumRequest(object):
1365 """A stub medium request that tracks the number of times accept_bytes is
1366@@ -3218,6 +3409,193 @@
1367 # encoder.
1368
1369
1370+class Test_SmartClientRequest(tests.TestCase):
1371+
1372+ def make_client_with_failing_medium(self, fail_at_write=True, response=''):
1373+ response_io = StringIO(response)
1374+ output = StringIO()
1375+ vendor = FirstRejectedStringIOSSHVendor(response_io, output,
1376+ fail_at_write=fail_at_write)
1377+ ssh_params = medium.SSHParams('a host', 'a port', 'a user', 'a pass')
1378+ client_medium = medium.SmartSSHClientMedium('base', ssh_params, vendor)
1379+ smart_client = client._SmartClient(client_medium, headers={})
1380+ return output, vendor, smart_client
1381+
1382+ def make_response(self, args, body=None, body_stream=None):
1383+ response_io = StringIO()
1384+ response = _mod_request.SuccessfulSmartServerResponse(args, body=body,
1385+ body_stream=body_stream)
1386+ responder = protocol.ProtocolThreeResponder(response_io.write)
1387+ responder.send_response(response)
1388+ return response_io.getvalue()
1389+
1390+ def test__call_doesnt_retry_append(self):
1391+ response = self.make_response(('appended', '8'))
1392+ output, vendor, smart_client = self.make_client_with_failing_medium(
1393+ fail_at_write=False, response=response)
1394+ smart_request = client._SmartClientRequest(smart_client, 'append',
1395+ ('foo', ''), body='content\n')
1396+ self.assertRaises(errors.ConnectionReset, smart_request._call, 3)
1397+
1398+ def test__call_retries_get_bytes(self):
1399+ response = self.make_response(('ok',), 'content\n')
1400+ output, vendor, smart_client = self.make_client_with_failing_medium(
1401+ fail_at_write=False, response=response)
1402+ smart_request = client._SmartClientRequest(smart_client, 'get',
1403+ ('foo',))
1404+ response, response_handler = smart_request._call(3)
1405+ self.assertEqual(('ok',), response)
1406+ self.assertEqual('content\n', response_handler.read_body_bytes())
1407+
1408+ def test__call_noretry_get_bytes(self):
1409+ debug.debug_flags.add('noretry')
1410+ response = self.make_response(('ok',), 'content\n')
1411+ output, vendor, smart_client = self.make_client_with_failing_medium(
1412+ fail_at_write=False, response=response)
1413+ smart_request = client._SmartClientRequest(smart_client, 'get',
1414+ ('foo',))
1415+ self.assertRaises(errors.ConnectionReset, smart_request._call, 3)
1416+
1417+ def test__send_no_retry_pipes(self):
1418+ client_read, server_write = create_file_pipes()
1419+ server_read, client_write = create_file_pipes()
1420+ client_medium = medium.SmartSimplePipesClientMedium(client_read,
1421+ client_write, base='/')
1422+ smart_client = client._SmartClient(client_medium)
1423+ smart_request = client._SmartClientRequest(smart_client,
1424+ 'hello', ())
1425+ # Close the server side
1426+ server_read.close()
1427+ encoder, response_handler = smart_request._construct_protocol(3)
1428+ self.assertRaises(errors.ConnectionReset,
1429+ smart_request._send_no_retry, encoder)
1430+
1431+ def test__send_read_response_sockets(self):
1432+ listen_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1433+ listen_sock.bind(('127.0.0.1', 0))
1434+ listen_sock.listen(1)
1435+ host, port = listen_sock.getsockname()
1436+ client_medium = medium.SmartTCPClientMedium(host, port, '/')
1437+ client_medium._ensure_connection()
1438+ smart_client = client._SmartClient(client_medium)
1439+ smart_request = client._SmartClientRequest(smart_client, 'hello', ())
1440+ # Accept the connection, but don't actually talk to the client.
1441+ server_sock, _ = listen_sock.accept()
1442+ server_sock.close()
1443+ # Sockets buffer and don't really notice that the server has closed the
1444+ # connection until we try to read again.
1445+ handler = smart_request._send(3)
1446+ self.assertRaises(errors.ConnectionReset,
1447+ handler.read_response_tuple, expect_body=False)
1448+
1449+ def test__send_retries_on_write(self):
1450+ output, vendor, smart_client = self.make_client_with_failing_medium()
1451+ smart_request = client._SmartClientRequest(smart_client, 'hello', ())
1452+ handler = smart_request._send(3)
1453+ self.assertEqual('bzr message 3 (bzr 1.6)\n' # protocol
1454+ '\x00\x00\x00\x02de' # empty headers
1455+ 's\x00\x00\x00\tl5:helloee',
1456+ output.getvalue())
1457+ self.assertEqual(
1458+ [('connect_ssh', 'a user', 'a pass', 'a host', 'a port',
1459+ ['bzr', 'serve', '--inet', '--directory=/', '--allow-writes']),
1460+ ('close',),
1461+ ('connect_ssh', 'a user', 'a pass', 'a host', 'a port',
1462+ ['bzr', 'serve', '--inet', '--directory=/', '--allow-writes']),
1463+ ],
1464+ vendor.calls)
1465+
1466+ def test__send_doesnt_retry_read_failure(self):
1467+ output, vendor, smart_client = self.make_client_with_failing_medium(
1468+ fail_at_write=False)
1469+ smart_request = client._SmartClientRequest(smart_client, 'hello', ())
1470+ handler = smart_request._send(3)
1471+ self.assertEqual('bzr message 3 (bzr 1.6)\n' # protocol
1472+ '\x00\x00\x00\x02de' # empty headers
1473+ 's\x00\x00\x00\tl5:helloee',
1474+ output.getvalue())
1475+ self.assertEqual(
1476+ [('connect_ssh', 'a user', 'a pass', 'a host', 'a port',
1477+ ['bzr', 'serve', '--inet', '--directory=/', '--allow-writes']),
1478+ ],
1479+ vendor.calls)
1480+ self.assertRaises(errors.ConnectionReset, handler.read_response_tuple)
1481+
1482+ def test__send_request_retries_body_stream_if_not_started(self):
1483+ output, vendor, smart_client = self.make_client_with_failing_medium()
1484+ smart_request = client._SmartClientRequest(smart_client, 'hello', (),
1485+ body_stream=['a', 'b'])
1486+ response_handler = smart_request._send(3)
1487+ # We connect, get disconnected, and notice before consuming the stream,
1488+ # so we try again one time and succeed.
1489+ self.assertEqual(
1490+ [('connect_ssh', 'a user', 'a pass', 'a host', 'a port',
1491+ ['bzr', 'serve', '--inet', '--directory=/', '--allow-writes']),
1492+ ('close',),
1493+ ('connect_ssh', 'a user', 'a pass', 'a host', 'a port',
1494+ ['bzr', 'serve', '--inet', '--directory=/', '--allow-writes']),
1495+ ],
1496+ vendor.calls)
1497+ self.assertEqual('bzr message 3 (bzr 1.6)\n' # protocol
1498+ '\x00\x00\x00\x02de' # empty headers
1499+ 's\x00\x00\x00\tl5:helloe'
1500+ 'b\x00\x00\x00\x01a'
1501+ 'b\x00\x00\x00\x01b'
1502+ 'e',
1503+ output.getvalue())
1504+
1505+ def test__send_request_stops_if_body_started(self):
1506+ # We intentionally use the python StringIO so that we can subclass it.
1507+ from StringIO import StringIO
1508+ response = StringIO()
1509+
1510+ class FailAfterFirstWrite(StringIO):
1511+ """Allow one 'write' call to pass, fail the rest"""
1512+ def __init__(self):
1513+ StringIO.__init__(self)
1514+ self._first = True
1515+
1516+ def write(self, s):
1517+ if self._first:
1518+ self._first = False
1519+ return StringIO.write(self, s)
1520+ raise IOError(errno.EINVAL, 'invalid file handle')
1521+ output = FailAfterFirstWrite()
1522+
1523+ vendor = FirstRejectedStringIOSSHVendor(response, output,
1524+ fail_at_write=False)
1525+ ssh_params = medium.SSHParams('a host', 'a port', 'a user', 'a pass')
1526+ client_medium = medium.SmartSSHClientMedium('base', ssh_params, vendor)
1527+ smart_client = client._SmartClient(client_medium, headers={})
1528+ smart_request = client._SmartClientRequest(smart_client, 'hello', (),
1529+ body_stream=['a', 'b'])
1530+ self.assertRaises(errors.ConnectionReset, smart_request._send, 3)
1531+ # We connect, and manage to get to the point that we start consuming
1532+ # the body stream. The next write fails, so we just stop.
1533+ self.assertEqual(
1534+ [('connect_ssh', 'a user', 'a pass', 'a host', 'a port',
1535+ ['bzr', 'serve', '--inet', '--directory=/', '--allow-writes']),
1536+ ('close',),
1537+ ],
1538+ vendor.calls)
1539+ self.assertEqual('bzr message 3 (bzr 1.6)\n' # protocol
1540+ '\x00\x00\x00\x02de' # empty headers
1541+ 's\x00\x00\x00\tl5:helloe',
1542+ output.getvalue())
1543+
1544+ def test__send_disabled_retry(self):
1545+ debug.debug_flags.add('noretry')
1546+ output, vendor, smart_client = self.make_client_with_failing_medium()
1547+ smart_request = client._SmartClientRequest(smart_client, 'hello', ())
1548+ self.assertRaises(errors.ConnectionReset, smart_request._send, 3)
1549+ self.assertEqual(
1550+ [('connect_ssh', 'a user', 'a pass', 'a host', 'a port',
1551+ ['bzr', 'serve', '--inet', '--directory=/', '--allow-writes']),
1552+ ('close',),
1553+ ],
1554+ vendor.calls)
1555+
1556+
1557 class LengthPrefixedBodyDecoder(tests.TestCase):
1558
1559 # XXX: TODO: make accept_reading_trailer invoke translate_response or
1560
1561=== modified file 'doc/en/release-notes/bzr-2.4.txt'
1562--- doc/en/release-notes/bzr-2.4.txt 2013-05-23 08:30:09 +0000
1563+++ doc/en/release-notes/bzr-2.4.txt 2013-05-23 13:00:35 +0000
1564@@ -145,7 +145,12 @@
1565 avoids raising a spurious MemoryError on certain platforms such as AIX.
1566 (John Arbash Meinel, #856731)
1567
1568-
1569+* Teach the bzr client how to reconnect if we get ``ConnectionReset``
1570+ while making an RPC request. This doesn't handle all possible network
1571+ disconnects, but it should at least handle when the server is asked to
1572+ shutdown gracefully. (John Arbash Meinel, #819604)
1573+
1574+
1575 Documentation
1576 *************
1577

Subscribers

People subscribed via source and target branches