Merge lp:~gz/bzr/smarter_smart_error_reporting_722416_B into lp:bzr

Proposed by Martin Packman
Status: Merged
Merged at revision: 5694
Proposed branch: lp:~gz/bzr/smarter_smart_error_reporting_722416_B
Merge into: lp:bzr
Diff against target: 245 lines (+95/-35)
5 files modified
bzrlib/remote.py (+21/-14)
bzrlib/smart/message.py (+10/-20)
bzrlib/smart/request.py (+3/-0)
bzrlib/tests/test_remote.py (+58/-1)
bzrlib/tests/test_smart_request.py (+3/-0)
To merge this branch: bzr merge lp:~gz/bzr/smarter_smart_error_reporting_722416_B
Reviewer Review Type Date Requested Status
bzr-core preliminary Pending
Review via email: mp+51666@code.launchpad.net

Description of the change

This is step 3 (really) in fixing bug 722416. The aim of the branch is to turn an remote response in the form `('MemoryError',)` into a useful message for the user. I'm pretty sure the code is incorrect or incomplete however.

I've got two main problems here. Firstly, there seem to be two different _translate_error functions for the local-side error interpretation. Secondly, I can only reproduce one of the codepaths from the bug reports in the tests.

The stack I can test is going through autopack as-like bug 602614, which uses the bzrlib.remote._translate_error function. However, I think the one I really need to patch is the bzrlib.smart.message._translate_error but I'm failing to write a test with a stack like the local-side one in bug 721598 as the important bits in test_remote seem to be Fake'd out. Help?

Note re-raising MemoryError locally isn't what we want to do, that's not the starving python. A new error class could be created though. Is there any additional 'advice' it would be useful to show to the user? Perhaps a suggestion to try the operation again locally or with 'nosmart+' instead?

To post a comment you must log in.
Revision history for this message
Andrew Bennetts (spiv) wrote :
Download full text (5.3 KiB)

Martin [gz] wrote:
[...]
> This is step 3 (really) in fixing bug 722416. The aim of the branch is to turn
> an remote response in the form `('MemoryError',)` into a useful message for
> the user. I'm pretty sure the code is incorrect or incomplete however.
>
> I've got two main problems here. Firstly, there seem to be two different
> _translate_error functions for the local-side error interpretation. Secondly,
> I can only reproduce one of the codepaths from the bug reports in the tests.
>
> The stack I can test is going through autopack as-like bug 602614, which uses
> the bzrlib.remote._translate_error function. However, I think the one I really
> need to patch is the bzrlib.smart.message._translate_error but I'm failing to
> write a test with a stack like the local-side one in bug 721598 as the
> important bits in test_remote seem to be Fake'd out. Help?

Hmm. I don't think we have good unit tests for
bzrlib.smart.message._translate_error. Well, we do have
TestClientDecodingProtocolThree in test_smart_transport, which currently tests
the UnknownMethod and ErrorFromSmartServer paths there, but via the byte-level
details of the v3 protocol which you don't really want to care about for this.
I suspect the other paths are only exercised incidentally via the broader test
suite.

The intent was that message._translate_error would be for all the context-free
error translations, i.e. error translations that are independent of the request
that triggered them, such as UnknownMethod. Then the translations that want
access to the branch object or whatever belong in remote._translate_error. So
you're right that message._translate_error is probably the better place.

I'm not sure the distinction is particularly important, and looking at it with
relatively fresh eyes it has the drawback that the message._translate_error
function is never used by v2 (or v1, but it has a fixed set of error names
anyway), and so the cases other than UnknownMethod and ErrorFromSmartServer are
thus duplicated anyway. I'm also not sure that drawback matters much though,
although worryingly NoSuchFile has richer behaviour in remote._translate_error
that will never be triggered in v3 because message._translate_error will see it
first.

We have good unit tests I think that the protocol layer signals the presence of
an error to its caller, and from the other end test_remote does pretty good job
of unit testing remote._translate_error (at a glance I see
TestErrorTranslationSuccess is missing one or two newer errors like NotStacked).
But as you've noticed there's a gap for message._translate_error. I think the
ideal solution is to write the unit tests for that gap, but I don't want to
force you to do that just for this fix. Hmm :/

I'm currently inclined to say:

 * leave the UnknownMethod and ErrorFromSmartServer paths in
   message._translate_error: they probably do belong there, and are already well
   tested.
 * move the context-free translations into a new function, probably in remote.py
 * invoke that new function as part of remote._translate_error

This I think satisfies all the concerns adequately:

 * the context-free translations don't clutter up the more complex...

Read more...

Revision history for this message
Martin Packman (gz) wrote :

> The intent was that message._translate_error would be for all the context-free
> error translations, i.e. error translations that are independent of the
> request
> that triggered them, such as UnknownMethod. Then the translations that want
> access to the branch object or whatever belong in remote._translate_error. So
> you're right that message._translate_error is probably the better place.

This helps, thanks. As a MemoryError could be raised from any command, it makes sense for it to be re-raised from the context-free translation function. However, see the ☃ question below.

> I'm currently inclined to say:
>
> * leave the UnknownMethod and ErrorFromSmartServer paths in
> message._translate_error: they probably do belong there, and are already
> well
> tested.
> * move the context-free translations into a new function, probably in
> remote.py
> * invoke that new function as part of remote._translate_error

I'll do this for now, and resist the urge to pointlessly rewrite in a more functional style.

> > Note re-raising MemoryError locally isn't what we want to do, that's not the
> > starving python. A new error class could be created though. Is there any
> > additional 'advice' it would be useful to show to the user? Perhaps a
> > suggestion to try the operation again locally or with 'nosmart+' instead?

Your thoughts in response to this much the same as mine, so will just add a little more text and leave at that.

> It's probably a good idea for the error to say which remote server, though.
> Various scripts, plugins, stacking configurations, etc, may have multiple
> connections active at one time, and the user probably wants to know which one
> had the problem :)

That's a good point. Though... ☃ asks doesn't that mean we do want context then? It also goes for the other general errors that are being re-raised, so perhaps a different approach might be helpful in the future.

Revision history for this message
Martin Packman (gz) wrote :

Sorry, bad diff in email again, launchpad hadn't finished updating the branch when it got sent out apparently, see the mp for the updates.

Revision history for this message
Andrew Bennetts (spiv) wrote :

Martin [gz] wrote:
[...]
> > It's probably a good idea for the error to say which remote server, though.
> > Various scripts, plugins, stacking configurations, etc, may have multiple
> > connections active at one time, and the user probably wants to know which one
> > had the problem :)
>
> That's a good point. Though... ☃ asks doesn't that mean we do want
> context then? It also goes for the other general errors that are being
> re-raised, so perhaps a different approach might be helpful in the
> future.

I suppose you're right! Although there's sort of different levels of
context...

I think part of the reason I had the distinct message._translate_error
vs. remote._translate_error functions was to keep knowledge of random
bzr model stuff out of the basic protocol. i.e. there's no reason why
the message layer, which is about taking simple structures off the wire
and turning them into events like “here's a request body” should know
anything about what NoSuchRevision means.

I don't think I thought through that particular refactoring carefully at
the time, and so conflated “doesn't need context stuff from remote.py”
with “message-level logic.” But it was a step towards that, and now
with your patches I think it's actually pretty right: the message layer
knows that an error is either UnknownMethodError, or else it is a
ErrorFromSmartServer for a higher layer to interpret, and that's it.

It's not really a goal, but if you like you can imagine a design
principle here as being “the smart protocol stuff could be used to write
useful servers and clients that have nothing to do with bzr.” So
bzrlib.smart.protocol, bzrlib.smart.message, bzrlib.smart.client and
most of bzrlib.smart.request for instance are pretty generic and on
nothing-to-do-with-the-rest-of-bzr side of the fence, while
bzrlib.remote and bzrlib.smart.bzrdir, etc, clearly belong to other
side.

That's a long winded way of saying: yes, I think you're right that the
medium or its URL or something probably belongs in the context, and that
the MemoryError translation (and possibly others) should use that.

And you're making me think that the no-context distinction isn't
important at all (now that I've figured out that it was the “belongs in
bzrlib.smart.message or not” distinction that I really cared about, even
though I had it a bit wrong).

-Andrew.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bzrlib/remote.py'
2--- bzrlib/remote.py 2011-02-19 17:37:45 +0000
3+++ bzrlib/remote.py 2011-03-02 20:20:52 +0000
4@@ -2973,10 +2973,7 @@
5 'Missing key %r in context %r', key_err.args[0], context)
6 raise err
7
8- if err.error_verb == 'IncompatibleRepositories':
9- raise errors.IncompatibleRepositories(err.error_args[0],
10- err.error_args[1], err.error_args[2])
11- elif err.error_verb == 'NoSuchRevision':
12+ if err.error_verb == 'NoSuchRevision':
13 raise NoSuchRevision(find('branch'), err.error_args[0])
14 elif err.error_verb == 'nosuchrevision':
15 raise NoSuchRevision(find('repository'), err.error_args[0])
16@@ -2989,22 +2986,12 @@
17 detail=extra)
18 elif err.error_verb == 'norepository':
19 raise errors.NoRepositoryPresent(find('bzrdir'))
20- elif err.error_verb == 'LockContention':
21- raise errors.LockContention('(remote lock)')
22 elif err.error_verb == 'UnlockableTransport':
23 raise errors.UnlockableTransport(find('bzrdir').root_transport)
24- elif err.error_verb == 'LockFailed':
25- raise errors.LockFailed(err.error_args[0], err.error_args[1])
26 elif err.error_verb == 'TokenMismatch':
27 raise errors.TokenMismatch(find('token'), '(remote token)')
28 elif err.error_verb == 'Diverged':
29 raise errors.DivergedBranches(find('branch'), find('other_branch'))
30- elif err.error_verb == 'TipChangeRejected':
31- raise errors.TipChangeRejected(err.error_args[0].decode('utf8'))
32- elif err.error_verb == 'UnstackableBranchFormat':
33- raise errors.UnstackableBranchFormat(*err.error_args)
34- elif err.error_verb == 'UnstackableRepositoryFormat':
35- raise errors.UnstackableRepositoryFormat(*err.error_args)
36 elif err.error_verb == 'NotStacked':
37 raise errors.NotStacked(branch=find('branch'))
38 elif err.error_verb == 'PermissionDenied':
39@@ -3020,6 +3007,24 @@
40 elif err.error_verb == 'NoSuchFile':
41 path = get_path()
42 raise errors.NoSuchFile(path)
43+ _translate_error_without_context(err)
44+
45+
46+def _translate_error_without_context(err):
47+ """Translate any ErrorFromSmartServer values that don't require context"""
48+ if err.error_verb == 'IncompatibleRepositories':
49+ raise errors.IncompatibleRepositories(err.error_args[0],
50+ err.error_args[1], err.error_args[2])
51+ elif err.error_verb == 'LockContention':
52+ raise errors.LockContention('(remote lock)')
53+ elif err.error_verb == 'LockFailed':
54+ raise errors.LockFailed(err.error_args[0], err.error_args[1])
55+ elif err.error_verb == 'TipChangeRejected':
56+ raise errors.TipChangeRejected(err.error_args[0].decode('utf8'))
57+ elif err.error_verb == 'UnstackableBranchFormat':
58+ raise errors.UnstackableBranchFormat(*err.error_args)
59+ elif err.error_verb == 'UnstackableRepositoryFormat':
60+ raise errors.UnstackableRepositoryFormat(*err.error_args)
61 elif err.error_verb == 'FileExists':
62 raise errors.FileExists(err.error_args[0])
63 elif err.error_verb == 'DirectoryNotEmpty':
64@@ -3044,4 +3049,6 @@
65 raise UnicodeEncodeError(encoding, val, start, end, reason)
66 elif err.error_verb == 'ReadOnlyError':
67 raise errors.TransportNotPossible('readonly transport')
68+ elif err.error_verb == 'MemoryError':
69+ raise errors.BzrError("remote server out of memory")
70 raise errors.UnknownErrorFromSmartServer(err)
71
72=== modified file 'bzrlib/smart/message.py'
73--- bzrlib/smart/message.py 2009-09-11 06:07:05 +0000
74+++ bzrlib/smart/message.py 2011-03-02 20:20:52 +0000
75@@ -303,7 +303,7 @@
76 mutter(' result: %r', self.args)
77 if self.status == 'E':
78 self._wait_for_response_end()
79- _translate_error(self.args)
80+ _raise_smart_server_error(self.args)
81 return tuple(self.args)
82
83 def read_body_bytes(self, count=-1):
84@@ -335,27 +335,17 @@
85 yield bytes_part
86 self._read_more()
87 if self._body_stream_status == 'E':
88- _translate_error(self._body_error_args)
89+ _raise_smart_server_error(self._body_error_args)
90
91 def cancel_read_body(self):
92 self._wait_for_response_end()
93
94
95-def _translate_error(error_tuple):
96- # Many exceptions need some state from the requestor to be properly
97- # translated (e.g. they need a branch object). So this only translates a
98- # few errors, and the rest are turned into a generic ErrorFromSmartServer.
99- error_name = error_tuple[0]
100- error_args = error_tuple[1:]
101- if error_name == 'UnknownMethod':
102- raise errors.UnknownSmartMethod(error_args[0])
103- if error_name == 'LockContention':
104- raise errors.LockContention('(remote lock)')
105- elif error_name == 'LockFailed':
106- raise errors.LockFailed(*error_args[:2])
107- elif error_name == 'FileExists':
108- raise errors.FileExists(error_args[0])
109- elif error_name == 'NoSuchFile':
110- raise errors.NoSuchFile(error_args[0])
111- else:
112- raise errors.ErrorFromSmartServer(error_tuple)
113+def _raise_smart_server_error(error_tuple):
114+ """Raise exception based on tuple received from smart server
115+
116+ Specific error translation is handled by bzrlib.remote._translate_error
117+ """
118+ if error_tuple[0] == 'UnknownMethod':
119+ raise errors.UnknownSmartMethod(error_tuple[1])
120+ raise errors.ErrorFromSmartServer(error_tuple)
121
122=== modified file 'bzrlib/smart/request.py'
123--- bzrlib/smart/request.py 2010-11-26 06:31:54 +0000
124+++ bzrlib/smart/request.py 2011-03-02 20:20:52 +0000
125@@ -446,6 +446,9 @@
126 return ('TokenMismatch', err.given_token, err.lock_token)
127 elif isinstance(err, errors.LockContention):
128 return ('LockContention',)
129+ elif isinstance(err, MemoryError):
130+ # GZ 2011-02-24: Copy bzrlib.trace -Dmem_dump functionality here?
131+ return ('MemoryError',)
132 # Unserialisable error. Log it, and return a generic error
133 trace.log_exception_quietly()
134 return ('error', str(err))
135
136=== modified file 'bzrlib/tests/test_remote.py'
137--- bzrlib/tests/test_remote.py 2011-02-24 16:13:39 +0000
138+++ bzrlib/tests/test_remote.py 2011-03-02 20:20:52 +0000
139@@ -2775,6 +2775,16 @@
140 ('pack collection autopack',)],
141 client._calls)
142
143+ def test_oom_error_reporting(self):
144+ """An out-of-memory condition on the server is reported clearly"""
145+ transport_path = 'quack'
146+ repo, client = self.setup_fake_client_and_repository(transport_path)
147+ client.add_expected_call(
148+ 'PackRepository.autopack', ('quack/',),
149+ 'error', ('MemoryError',))
150+ err = self.assertRaises(errors.BzrError, repo.autopack)
151+ self.assertContainsRe(str(err), "^remote server out of mem")
152+
153
154 class TestErrorTranslationBase(tests.TestCaseWithMemoryTransport):
155 """Base class for unit tests for bzrlib.remote._translate_error."""
156@@ -2853,6 +2863,13 @@
157 detail='extra detail')
158 self.assertEqual(expected_error, translated_error)
159
160+ def test_norepository(self):
161+ bzrdir = self.make_bzrdir('')
162+ translated_error = self.translateTuple(('norepository',),
163+ bzrdir=bzrdir)
164+ expected_error = errors.NoRepositoryPresent(bzrdir)
165+ self.assertEqual(expected_error, translated_error)
166+
167 def test_LockContention(self):
168 translated_error = self.translateTuple(('LockContention',))
169 expected_error = errors.LockContention('(remote lock)')
170@@ -2886,6 +2903,12 @@
171 expected_error = errors.DivergedBranches(branch, other_branch)
172 self.assertEqual(expected_error, translated_error)
173
174+ def test_NotStacked(self):
175+ branch = self.make_branch('')
176+ translated_error = self.translateTuple(('NotStacked',), branch=branch)
177+ expected_error = errors.NotStacked(branch)
178+ self.assertEqual(expected_error, translated_error)
179+
180 def test_ReadError_no_args(self):
181 path = 'a path'
182 translated_error = self.translateTuple(('ReadError',), path=path)
183@@ -2907,7 +2930,8 @@
184
185 def test_PermissionDenied_no_args(self):
186 path = 'a path'
187- translated_error = self.translateTuple(('PermissionDenied',), path=path)
188+ translated_error = self.translateTuple(('PermissionDenied',),
189+ path=path)
190 expected_error = errors.PermissionDenied(path)
191 self.assertEqual(expected_error, translated_error)
192
193@@ -2936,6 +2960,39 @@
194 expected_error = errors.PermissionDenied(path, extra)
195 self.assertEqual(expected_error, translated_error)
196
197+ # GZ 2011-03-02: TODO test for PermissionDenied with non-ascii 'extra'
198+
199+ def test_NoSuchFile_context_path(self):
200+ local_path = "local path"
201+ translated_error = self.translateTuple(('ReadError', "remote path"),
202+ path=local_path)
203+ expected_error = errors.ReadError(local_path)
204+ self.assertEqual(expected_error, translated_error)
205+
206+ def test_NoSuchFile_without_context(self):
207+ remote_path = "remote path"
208+ translated_error = self.translateTuple(('ReadError', remote_path))
209+ expected_error = errors.ReadError(remote_path)
210+ self.assertEqual(expected_error, translated_error)
211+
212+ def test_ReadOnlyError(self):
213+ translated_error = self.translateTuple(('ReadOnlyError',))
214+ expected_error = errors.TransportNotPossible("readonly transport")
215+ self.assertEqual(expected_error, translated_error)
216+
217+ def test_MemoryError(self):
218+ translated_error = self.translateTuple(('MemoryError',))
219+ expected_error = errors.BzrError("remote server out of memory")
220+ self.assertEqual(expected_error, translated_error)
221+
222+ def test_generic_IndexError(self):
223+ err = errors.ErrorFromSmartServer(('error', "list index out of range"))
224+ translated_error = self.translateErrorFromSmartServer(err)
225+ expected_error = errors.UnknownErrorFromSmartServer(err)
226+ self.assertEqual(expected_error, translated_error)
227+
228+ # GZ 2011-03-02: TODO test generic non-ascii error string
229+
230
231 class TestErrorTranslationRobustness(TestErrorTranslationBase):
232 """Unit tests for bzrlib.remote._translate_error's robustness.
233
234=== modified file 'bzrlib/tests/test_smart_request.py'
235--- bzrlib/tests/test_smart_request.py 2010-07-13 19:02:12 +0000
236+++ bzrlib/tests/test_smart_request.py 2011-03-02 20:20:52 +0000
237@@ -172,6 +172,9 @@
238 ('TokenMismatch', 'some-token', 'actual-token'),
239 errors.TokenMismatch('some-token', 'actual-token'))
240
241+ def test_MemoryError(self):
242+ self.assertTranslationEqual(("MemoryError",), MemoryError())
243+
244
245 class TestRequestJail(TestCaseWithMemoryTransport):
246