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
=== modified file 'lib/canonical/librarian/client.py'
--- lib/canonical/librarian/client.py 2011-01-27 09:19:44 +0000
+++ lib/canonical/librarian/client.py 2011-06-01 15:23:36 +0000
@@ -95,9 +95,10 @@
95 response = self.state.f.readline().strip()95 response = self.state.f.readline().strip()
96 raise UploadFailed('Server said: ' + response)96 raise UploadFailed('Server said: ' + response)
9797
98 def _sendLine(self, line):98 def _sendLine(self, line, check_for_error_responses=True):
99 self.state.f.write(line + '\r\n')99 self.state.f.write(line + '\r\n')
100 self._checkError()100 if check_for_error_responses:
101 self._checkError()
101102
102 def _sendHeader(self, name, value):103 def _sendHeader(self, name, value):
103 self._sendLine('%s: %s' % (name, value))104 self._sendLine('%s: %s' % (name, value))
@@ -162,8 +163,11 @@
162 if debugID is not None:163 if debugID is not None:
163 self._sendHeader('Debug-ID', debugID)164 self._sendHeader('Debug-ID', debugID)
164165
165 # Send blank line166 # Send blank line. Do not check for a response from the
166 self._sendLine('')167 # server when no data will be sent. Otherwise
168 # _checkError() might consume the "200" response which
169 # is supposed to be read below in this method.
170 self._sendLine('', check_for_error_responses=(size > 0))
167171
168 # Prepare to the upload the file172 # Prepare to the upload the file
169 shaDigester = hashlib.sha1()173 shaDigester = hashlib.sha1()
@@ -173,7 +177,7 @@
173 # Read in and upload the file 64kb at a time, by using the two-arg177 # Read in and upload the file 64kb at a time, by using the two-arg
174 # form of iter (see178 # form of iter (see
175 # /usr/share/doc/python2.4/html/lib/built-in-funcs.html#l2h-42).179 # /usr/share/doc/python2.4/html/lib/built-in-funcs.html#l2h-42).
176 for chunk in iter(lambda: file.read(1024*64), ''):180 for chunk in iter(lambda: file.read(1024 * 64), ''):
177 self.state.f.write(chunk)181 self.state.f.write(chunk)
178 bytesWritten += len(chunk)182 bytesWritten += len(chunk)
179 shaDigester.update(chunk)183 shaDigester.update(chunk)
@@ -241,7 +245,7 @@
241 # Read in and upload the file 64kb at a time, by using the two-arg245 # Read in and upload the file 64kb at a time, by using the two-arg
242 # form of iter (see246 # form of iter (see
243 # /usr/share/doc/python2.4/html/lib/built-in-funcs.html#l2h-42).247 # /usr/share/doc/python2.4/html/lib/built-in-funcs.html#l2h-42).
244 for chunk in iter(lambda: file.read(1024*64), ''):248 for chunk in iter(lambda: file.read(1024 * 64), ''):
245 self.state.f.write(chunk)249 self.state.f.write(chunk)
246 bytesWritten += len(chunk)250 bytesWritten += len(chunk)
247251
248252
=== modified file 'lib/canonical/librarian/ftests/test_client.py'
--- lib/canonical/librarian/ftests/test_client.py 2011-01-26 23:54:06 +0000
+++ lib/canonical/librarian/ftests/test_client.py 2011-06-01 15:23:36 +0000
@@ -21,6 +21,11 @@
2121
2222
23class InstrumentedLibrarianClient(LibrarianClient):23class InstrumentedLibrarianClient(LibrarianClient):
24
25 def __init__(self, *args, **kwargs):
26 super(InstrumentedLibrarianClient, self).__init__(*args, **kwargs)
27 self.check_error_calls = 0
28
24 sentDatabaseName = False29 sentDatabaseName = False
25 def _sendHeader(self, name, value):30 def _sendHeader(self, name, value):
26 if name == 'Database-Name':31 if name == 'Database-Name':
@@ -32,6 +37,10 @@
32 self.called_getURLForDownload = True37 self.called_getURLForDownload = True
33 return LibrarianClient._getURLForDownload(self, aliasID)38 return LibrarianClient._getURLForDownload(self, aliasID)
3439
40 def _checkError(self):
41 self.check_error_calls += 1
42 super(InstrumentedLibrarianClient, self)._checkError()
43
3544
36def make_mock_file(error, max_raise):45def make_mock_file(error, max_raise):
37 """Return a surrogate for client._File.46 """Return a surrogate for client._File.
@@ -108,6 +117,31 @@
108 f = client.getFileByAlias(alias_id)117 f = client.getFileByAlias(alias_id)
109 self.assertEqual(f.read(), 'sample')118 self.assertEqual(f.read(), 'sample')
110119
120 def test_addFile_no_response_check_at_end_headers_for_empty_file(self):
121 # When addFile() sends the request header, it checks if the
122 # server responded with an error response after sending each
123 # header line. It does _not_ do this check when it sends the
124 # empty line following the headers.
125 client = InstrumentedLibrarianClient()
126 client.addFile(
127 'sample.txt', 0, StringIO(''), 'text/plain',
128 allow_zero_length=True)
129 # addFile() calls _sendHeader() three times and _sendLine()
130 # twice, but it does not check if the server responded
131 # in the second call.
132 self.assertEqual(4, client.check_error_calls)
133
134 def test_addFile_response_check_at_end_headers_for_non_empty_file(self):
135 # When addFile() sends the request header, it checks if the
136 # server responded with an error response after sending each
137 # header line. It does _not_ do this check when it sends the
138 # empty line following the headers.
139 client = InstrumentedLibrarianClient()
140 client.addFile('sample.txt', 4, StringIO('1234'), 'text/plain')
141 # addFile() calls _sendHeader() three times and _sendLine()
142 # twice.
143 self.assertEqual(5, client.check_error_calls)
144
111 def test__getURLForDownload(self):145 def test__getURLForDownload(self):
112 # This protected method is used by getFileByAlias. It is supposed to146 # This protected method is used by getFileByAlias. It is supposed to
113 # use the internal host and port rather than the external, proxied147 # use the internal host and port rather than the external, proxied