Merge lp:~adeuring/launchpad/bug-750274 into lp:launchpad

Proposed by Abel Deuring
Status: Merged
Approved by: Aaron Bentley
Approved revision: no longer in the source branch.
Merged at revision: 13151
Proposed branch: lp:~adeuring/launchpad/bug-750274
Merge into: lp:launchpad
Diff against target: 107 lines (+44/-6)
2 files modified
lib/canonical/librarian/client.py (+10/-6)
lib/canonical/librarian/ftests/test_client.py (+34/-0)
To merge this branch: bzr merge lp:~adeuring/launchpad/bug-750274
Reviewer Review Type Date Requested Status
Aaron Bentley (community) Approve
Review via email: mp+63129@code.launchpad.net

Commit message

[r=abentley][no-qa] FileUploadClient does no longer treat a very fast 200 response from the server while uploading an empty file as an error.

Description of the change

I hope that this branch fixes bug 750274: librarian.txt fails sometimes. I find it hard to be sure that a patch really fixes all possible causes for such a low-level communication problem, but I think my suspicion about the cause is reasonable:

Both tracebacks mentioned in the bug report show that the error occurs when an empty file is uploaded. In other words: The client sends only a few header lines and nothing more. After sending these header lines, the server is supposed to return a success response -- and it does so.

The problem is the error check in FileUploadClient._sendLine(): When _sendLine() has sent the empty line signalling the end of the header, it calls _checkErrror(), which treats _any_ response from the server as an error.

If the server responds quick enough, the select() call in _checkError() succeeds, and the method raises UploadFailed(). I suspect that this happens when the kernel's process manager switches from the client process to the server process after "self.state.f.write(line + '\r\n')" in _sendLine() but before the select() call in _checkError().

This is a very small time window, so the error occurs only quite seldom.

The fix is obvious: the sendLine() call for the empty line signalling "all headers sent" should not check if the server responded, if an empty file is uploaded. addFile() does the final check of the server's response a few lines below the the last _sendLine() call.

test: ./bin/test canonical -vvt test_client

To post a comment you must log in.
Revision history for this message
Aaron Bentley (abentley) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/librarian/client.py'
2--- lib/canonical/librarian/client.py 2011-01-27 09:19:44 +0000
3+++ lib/canonical/librarian/client.py 2011-06-01 15:23:36 +0000
4@@ -95,9 +95,10 @@
5 response = self.state.f.readline().strip()
6 raise UploadFailed('Server said: ' + response)
7
8- def _sendLine(self, line):
9+ def _sendLine(self, line, check_for_error_responses=True):
10 self.state.f.write(line + '\r\n')
11- self._checkError()
12+ if check_for_error_responses:
13+ self._checkError()
14
15 def _sendHeader(self, name, value):
16 self._sendLine('%s: %s' % (name, value))
17@@ -162,8 +163,11 @@
18 if debugID is not None:
19 self._sendHeader('Debug-ID', debugID)
20
21- # Send blank line
22- self._sendLine('')
23+ # Send blank line. Do not check for a response from the
24+ # server when no data will be sent. Otherwise
25+ # _checkError() might consume the "200" response which
26+ # is supposed to be read below in this method.
27+ self._sendLine('', check_for_error_responses=(size > 0))
28
29 # Prepare to the upload the file
30 shaDigester = hashlib.sha1()
31@@ -173,7 +177,7 @@
32 # Read in and upload the file 64kb at a time, by using the two-arg
33 # form of iter (see
34 # /usr/share/doc/python2.4/html/lib/built-in-funcs.html#l2h-42).
35- for chunk in iter(lambda: file.read(1024*64), ''):
36+ for chunk in iter(lambda: file.read(1024 * 64), ''):
37 self.state.f.write(chunk)
38 bytesWritten += len(chunk)
39 shaDigester.update(chunk)
40@@ -241,7 +245,7 @@
41 # Read in and upload the file 64kb at a time, by using the two-arg
42 # form of iter (see
43 # /usr/share/doc/python2.4/html/lib/built-in-funcs.html#l2h-42).
44- for chunk in iter(lambda: file.read(1024*64), ''):
45+ for chunk in iter(lambda: file.read(1024 * 64), ''):
46 self.state.f.write(chunk)
47 bytesWritten += len(chunk)
48
49
50=== modified file 'lib/canonical/librarian/ftests/test_client.py'
51--- lib/canonical/librarian/ftests/test_client.py 2011-01-26 23:54:06 +0000
52+++ lib/canonical/librarian/ftests/test_client.py 2011-06-01 15:23:36 +0000
53@@ -21,6 +21,11 @@
54
55
56 class InstrumentedLibrarianClient(LibrarianClient):
57+
58+ def __init__(self, *args, **kwargs):
59+ super(InstrumentedLibrarianClient, self).__init__(*args, **kwargs)
60+ self.check_error_calls = 0
61+
62 sentDatabaseName = False
63 def _sendHeader(self, name, value):
64 if name == 'Database-Name':
65@@ -32,6 +37,10 @@
66 self.called_getURLForDownload = True
67 return LibrarianClient._getURLForDownload(self, aliasID)
68
69+ def _checkError(self):
70+ self.check_error_calls += 1
71+ super(InstrumentedLibrarianClient, self)._checkError()
72+
73
74 def make_mock_file(error, max_raise):
75 """Return a surrogate for client._File.
76@@ -108,6 +117,31 @@
77 f = client.getFileByAlias(alias_id)
78 self.assertEqual(f.read(), 'sample')
79
80+ def test_addFile_no_response_check_at_end_headers_for_empty_file(self):
81+ # When addFile() sends the request header, it checks if the
82+ # server responded with an error response after sending each
83+ # header line. It does _not_ do this check when it sends the
84+ # empty line following the headers.
85+ client = InstrumentedLibrarianClient()
86+ client.addFile(
87+ 'sample.txt', 0, StringIO(''), 'text/plain',
88+ allow_zero_length=True)
89+ # addFile() calls _sendHeader() three times and _sendLine()
90+ # twice, but it does not check if the server responded
91+ # in the second call.
92+ self.assertEqual(4, client.check_error_calls)
93+
94+ def test_addFile_response_check_at_end_headers_for_non_empty_file(self):
95+ # When addFile() sends the request header, it checks if the
96+ # server responded with an error response after sending each
97+ # header line. It does _not_ do this check when it sends the
98+ # empty line following the headers.
99+ client = InstrumentedLibrarianClient()
100+ client.addFile('sample.txt', 4, StringIO('1234'), 'text/plain')
101+ # addFile() calls _sendHeader() three times and _sendLine()
102+ # twice.
103+ self.assertEqual(5, client.check_error_calls)
104+
105 def test__getURLForDownload(self):
106 # This protected method is used by getFileByAlias. It is supposed to
107 # use the internal host and port rather than the external, proxied