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
=== modified file 'bzrlib/remote.py'
--- bzrlib/remote.py 2011-02-19 17:37:45 +0000
+++ bzrlib/remote.py 2011-03-02 20:20:52 +0000
@@ -2973,10 +2973,7 @@
2973 'Missing key %r in context %r', key_err.args[0], context)2973 'Missing key %r in context %r', key_err.args[0], context)
2974 raise err2974 raise err
29752975
2976 if err.error_verb == 'IncompatibleRepositories':2976 if err.error_verb == 'NoSuchRevision':
2977 raise errors.IncompatibleRepositories(err.error_args[0],
2978 err.error_args[1], err.error_args[2])
2979 elif err.error_verb == 'NoSuchRevision':
2980 raise NoSuchRevision(find('branch'), err.error_args[0])2977 raise NoSuchRevision(find('branch'), err.error_args[0])
2981 elif err.error_verb == 'nosuchrevision':2978 elif err.error_verb == 'nosuchrevision':
2982 raise NoSuchRevision(find('repository'), err.error_args[0])2979 raise NoSuchRevision(find('repository'), err.error_args[0])
@@ -2989,22 +2986,12 @@
2989 detail=extra)2986 detail=extra)
2990 elif err.error_verb == 'norepository':2987 elif err.error_verb == 'norepository':
2991 raise errors.NoRepositoryPresent(find('bzrdir'))2988 raise errors.NoRepositoryPresent(find('bzrdir'))
2992 elif err.error_verb == 'LockContention':
2993 raise errors.LockContention('(remote lock)')
2994 elif err.error_verb == 'UnlockableTransport':2989 elif err.error_verb == 'UnlockableTransport':
2995 raise errors.UnlockableTransport(find('bzrdir').root_transport)2990 raise errors.UnlockableTransport(find('bzrdir').root_transport)
2996 elif err.error_verb == 'LockFailed':
2997 raise errors.LockFailed(err.error_args[0], err.error_args[1])
2998 elif err.error_verb == 'TokenMismatch':2991 elif err.error_verb == 'TokenMismatch':
2999 raise errors.TokenMismatch(find('token'), '(remote token)')2992 raise errors.TokenMismatch(find('token'), '(remote token)')
3000 elif err.error_verb == 'Diverged':2993 elif err.error_verb == 'Diverged':
3001 raise errors.DivergedBranches(find('branch'), find('other_branch'))2994 raise errors.DivergedBranches(find('branch'), find('other_branch'))
3002 elif err.error_verb == 'TipChangeRejected':
3003 raise errors.TipChangeRejected(err.error_args[0].decode('utf8'))
3004 elif err.error_verb == 'UnstackableBranchFormat':
3005 raise errors.UnstackableBranchFormat(*err.error_args)
3006 elif err.error_verb == 'UnstackableRepositoryFormat':
3007 raise errors.UnstackableRepositoryFormat(*err.error_args)
3008 elif err.error_verb == 'NotStacked':2995 elif err.error_verb == 'NotStacked':
3009 raise errors.NotStacked(branch=find('branch'))2996 raise errors.NotStacked(branch=find('branch'))
3010 elif err.error_verb == 'PermissionDenied':2997 elif err.error_verb == 'PermissionDenied':
@@ -3020,6 +3007,24 @@
3020 elif err.error_verb == 'NoSuchFile':3007 elif err.error_verb == 'NoSuchFile':
3021 path = get_path()3008 path = get_path()
3022 raise errors.NoSuchFile(path)3009 raise errors.NoSuchFile(path)
3010 _translate_error_without_context(err)
3011
3012
3013def _translate_error_without_context(err):
3014 """Translate any ErrorFromSmartServer values that don't require context"""
3015 if err.error_verb == 'IncompatibleRepositories':
3016 raise errors.IncompatibleRepositories(err.error_args[0],
3017 err.error_args[1], err.error_args[2])
3018 elif err.error_verb == 'LockContention':
3019 raise errors.LockContention('(remote lock)')
3020 elif err.error_verb == 'LockFailed':
3021 raise errors.LockFailed(err.error_args[0], err.error_args[1])
3022 elif err.error_verb == 'TipChangeRejected':
3023 raise errors.TipChangeRejected(err.error_args[0].decode('utf8'))
3024 elif err.error_verb == 'UnstackableBranchFormat':
3025 raise errors.UnstackableBranchFormat(*err.error_args)
3026 elif err.error_verb == 'UnstackableRepositoryFormat':
3027 raise errors.UnstackableRepositoryFormat(*err.error_args)
3023 elif err.error_verb == 'FileExists':3028 elif err.error_verb == 'FileExists':
3024 raise errors.FileExists(err.error_args[0])3029 raise errors.FileExists(err.error_args[0])
3025 elif err.error_verb == 'DirectoryNotEmpty':3030 elif err.error_verb == 'DirectoryNotEmpty':
@@ -3044,4 +3049,6 @@
3044 raise UnicodeEncodeError(encoding, val, start, end, reason)3049 raise UnicodeEncodeError(encoding, val, start, end, reason)
3045 elif err.error_verb == 'ReadOnlyError':3050 elif err.error_verb == 'ReadOnlyError':
3046 raise errors.TransportNotPossible('readonly transport')3051 raise errors.TransportNotPossible('readonly transport')
3052 elif err.error_verb == 'MemoryError':
3053 raise errors.BzrError("remote server out of memory")
3047 raise errors.UnknownErrorFromSmartServer(err)3054 raise errors.UnknownErrorFromSmartServer(err)
30483055
=== modified file 'bzrlib/smart/message.py'
--- bzrlib/smart/message.py 2009-09-11 06:07:05 +0000
+++ bzrlib/smart/message.py 2011-03-02 20:20:52 +0000
@@ -303,7 +303,7 @@
303 mutter(' result: %r', self.args)303 mutter(' result: %r', self.args)
304 if self.status == 'E':304 if self.status == 'E':
305 self._wait_for_response_end()305 self._wait_for_response_end()
306 _translate_error(self.args)306 _raise_smart_server_error(self.args)
307 return tuple(self.args)307 return tuple(self.args)
308308
309 def read_body_bytes(self, count=-1):309 def read_body_bytes(self, count=-1):
@@ -335,27 +335,17 @@
335 yield bytes_part335 yield bytes_part
336 self._read_more()336 self._read_more()
337 if self._body_stream_status == 'E':337 if self._body_stream_status == 'E':
338 _translate_error(self._body_error_args)338 _raise_smart_server_error(self._body_error_args)
339339
340 def cancel_read_body(self):340 def cancel_read_body(self):
341 self._wait_for_response_end()341 self._wait_for_response_end()
342342
343343
344def _translate_error(error_tuple):344def _raise_smart_server_error(error_tuple):
345 # Many exceptions need some state from the requestor to be properly345 """Raise exception based on tuple received from smart server
346 # translated (e.g. they need a branch object). So this only translates a346
347 # few errors, and the rest are turned into a generic ErrorFromSmartServer.347 Specific error translation is handled by bzrlib.remote._translate_error
348 error_name = error_tuple[0]348 """
349 error_args = error_tuple[1:]349 if error_tuple[0] == 'UnknownMethod':
350 if error_name == 'UnknownMethod':350 raise errors.UnknownSmartMethod(error_tuple[1])
351 raise errors.UnknownSmartMethod(error_args[0])351 raise errors.ErrorFromSmartServer(error_tuple)
352 if error_name == 'LockContention':
353 raise errors.LockContention('(remote lock)')
354 elif error_name == 'LockFailed':
355 raise errors.LockFailed(*error_args[:2])
356 elif error_name == 'FileExists':
357 raise errors.FileExists(error_args[0])
358 elif error_name == 'NoSuchFile':
359 raise errors.NoSuchFile(error_args[0])
360 else:
361 raise errors.ErrorFromSmartServer(error_tuple)
362352
=== modified file 'bzrlib/smart/request.py'
--- bzrlib/smart/request.py 2010-11-26 06:31:54 +0000
+++ bzrlib/smart/request.py 2011-03-02 20:20:52 +0000
@@ -446,6 +446,9 @@
446 return ('TokenMismatch', err.given_token, err.lock_token)446 return ('TokenMismatch', err.given_token, err.lock_token)
447 elif isinstance(err, errors.LockContention):447 elif isinstance(err, errors.LockContention):
448 return ('LockContention',)448 return ('LockContention',)
449 elif isinstance(err, MemoryError):
450 # GZ 2011-02-24: Copy bzrlib.trace -Dmem_dump functionality here?
451 return ('MemoryError',)
449 # Unserialisable error. Log it, and return a generic error452 # Unserialisable error. Log it, and return a generic error
450 trace.log_exception_quietly()453 trace.log_exception_quietly()
451 return ('error', str(err))454 return ('error', str(err))
452455
=== modified file 'bzrlib/tests/test_remote.py'
--- bzrlib/tests/test_remote.py 2011-02-24 16:13:39 +0000
+++ bzrlib/tests/test_remote.py 2011-03-02 20:20:52 +0000
@@ -2775,6 +2775,16 @@
2775 ('pack collection autopack',)],2775 ('pack collection autopack',)],
2776 client._calls)2776 client._calls)
27772777
2778 def test_oom_error_reporting(self):
2779 """An out-of-memory condition on the server is reported clearly"""
2780 transport_path = 'quack'
2781 repo, client = self.setup_fake_client_and_repository(transport_path)
2782 client.add_expected_call(
2783 'PackRepository.autopack', ('quack/',),
2784 'error', ('MemoryError',))
2785 err = self.assertRaises(errors.BzrError, repo.autopack)
2786 self.assertContainsRe(str(err), "^remote server out of mem")
2787
27782788
2779class TestErrorTranslationBase(tests.TestCaseWithMemoryTransport):2789class TestErrorTranslationBase(tests.TestCaseWithMemoryTransport):
2780 """Base class for unit tests for bzrlib.remote._translate_error."""2790 """Base class for unit tests for bzrlib.remote._translate_error."""
@@ -2853,6 +2863,13 @@
2853 detail='extra detail')2863 detail='extra detail')
2854 self.assertEqual(expected_error, translated_error)2864 self.assertEqual(expected_error, translated_error)
28552865
2866 def test_norepository(self):
2867 bzrdir = self.make_bzrdir('')
2868 translated_error = self.translateTuple(('norepository',),
2869 bzrdir=bzrdir)
2870 expected_error = errors.NoRepositoryPresent(bzrdir)
2871 self.assertEqual(expected_error, translated_error)
2872
2856 def test_LockContention(self):2873 def test_LockContention(self):
2857 translated_error = self.translateTuple(('LockContention',))2874 translated_error = self.translateTuple(('LockContention',))
2858 expected_error = errors.LockContention('(remote lock)')2875 expected_error = errors.LockContention('(remote lock)')
@@ -2886,6 +2903,12 @@
2886 expected_error = errors.DivergedBranches(branch, other_branch)2903 expected_error = errors.DivergedBranches(branch, other_branch)
2887 self.assertEqual(expected_error, translated_error)2904 self.assertEqual(expected_error, translated_error)
28882905
2906 def test_NotStacked(self):
2907 branch = self.make_branch('')
2908 translated_error = self.translateTuple(('NotStacked',), branch=branch)
2909 expected_error = errors.NotStacked(branch)
2910 self.assertEqual(expected_error, translated_error)
2911
2889 def test_ReadError_no_args(self):2912 def test_ReadError_no_args(self):
2890 path = 'a path'2913 path = 'a path'
2891 translated_error = self.translateTuple(('ReadError',), path=path)2914 translated_error = self.translateTuple(('ReadError',), path=path)
@@ -2907,7 +2930,8 @@
29072930
2908 def test_PermissionDenied_no_args(self):2931 def test_PermissionDenied_no_args(self):
2909 path = 'a path'2932 path = 'a path'
2910 translated_error = self.translateTuple(('PermissionDenied',), path=path)2933 translated_error = self.translateTuple(('PermissionDenied',),
2934 path=path)
2911 expected_error = errors.PermissionDenied(path)2935 expected_error = errors.PermissionDenied(path)
2912 self.assertEqual(expected_error, translated_error)2936 self.assertEqual(expected_error, translated_error)
29132937
@@ -2936,6 +2960,39 @@
2936 expected_error = errors.PermissionDenied(path, extra)2960 expected_error = errors.PermissionDenied(path, extra)
2937 self.assertEqual(expected_error, translated_error)2961 self.assertEqual(expected_error, translated_error)
29382962
2963 # GZ 2011-03-02: TODO test for PermissionDenied with non-ascii 'extra'
2964
2965 def test_NoSuchFile_context_path(self):
2966 local_path = "local path"
2967 translated_error = self.translateTuple(('ReadError', "remote path"),
2968 path=local_path)
2969 expected_error = errors.ReadError(local_path)
2970 self.assertEqual(expected_error, translated_error)
2971
2972 def test_NoSuchFile_without_context(self):
2973 remote_path = "remote path"
2974 translated_error = self.translateTuple(('ReadError', remote_path))
2975 expected_error = errors.ReadError(remote_path)
2976 self.assertEqual(expected_error, translated_error)
2977
2978 def test_ReadOnlyError(self):
2979 translated_error = self.translateTuple(('ReadOnlyError',))
2980 expected_error = errors.TransportNotPossible("readonly transport")
2981 self.assertEqual(expected_error, translated_error)
2982
2983 def test_MemoryError(self):
2984 translated_error = self.translateTuple(('MemoryError',))
2985 expected_error = errors.BzrError("remote server out of memory")
2986 self.assertEqual(expected_error, translated_error)
2987
2988 def test_generic_IndexError(self):
2989 err = errors.ErrorFromSmartServer(('error', "list index out of range"))
2990 translated_error = self.translateErrorFromSmartServer(err)
2991 expected_error = errors.UnknownErrorFromSmartServer(err)
2992 self.assertEqual(expected_error, translated_error)
2993
2994 # GZ 2011-03-02: TODO test generic non-ascii error string
2995
29392996
2940class TestErrorTranslationRobustness(TestErrorTranslationBase):2997class TestErrorTranslationRobustness(TestErrorTranslationBase):
2941 """Unit tests for bzrlib.remote._translate_error's robustness.2998 """Unit tests for bzrlib.remote._translate_error's robustness.
29422999
=== modified file 'bzrlib/tests/test_smart_request.py'
--- bzrlib/tests/test_smart_request.py 2010-07-13 19:02:12 +0000
+++ bzrlib/tests/test_smart_request.py 2011-03-02 20:20:52 +0000
@@ -172,6 +172,9 @@
172 ('TokenMismatch', 'some-token', 'actual-token'),172 ('TokenMismatch', 'some-token', 'actual-token'),
173 errors.TokenMismatch('some-token', 'actual-token'))173 errors.TokenMismatch('some-token', 'actual-token'))
174174
175 def test_MemoryError(self):
176 self.assertTranslationEqual(("MemoryError",), MemoryError())
177
175178
176class TestRequestJail(TestCaseWithMemoryTransport):179class TestRequestJail(TestCaseWithMemoryTransport):
177180