bpo-5001: More-informative multiprocessing error messages#3079
Conversation
Replace assertions in error-reporting code with more-informative version that doesn't cause confusion over where and what the error is.
As suggested in PR comment by @pitrou, changing from SystemError; TypeError appears appropriate.
…by additional work)
| pass | ||
|
|
||
| def debug_info(self, c): | ||
| def debug_info(self, c): # c is unused? |
There was a problem hiding this comment.
I don't know how multiprocessing managers work, but c seems to be an API convention (see number_of_objects below which also has an unused c parameter).
There was a problem hiding this comment.
Yes. I was wondering whether it could be used for additional debugging information. Admittedly, anything that called it should have the connection anyway!
| with self.mutex: | ||
| assert self.id_to_refcount[ident] >= 1 | ||
| if self.id_to_refcount[ident] <= 0: | ||
| raise ProcessError( |
There was a problem hiding this comment.
I would keep it an assert (or an AssertionError), since this really seems to be testing an internal consistency thing.
There was a problem hiding this comment.
OK; AssertionError, due to line length limits.
| for key, value in list(method_to_typeid.items()): | ||
| assert type(key) is str, '%r is not a string' % key | ||
| assert type(value) is str, '%r is not a string' % value | ||
| assert isinstance(key, str), '%r is not a string' % key |
There was a problem hiding this comment.
I'm not sure there's much point in making this change.
There was a problem hiding this comment.
Umm... OK. Someone was complaining that this wasn't done with the old 5001 patch...
| def worker(inqueue, outqueue, initializer=None, initargs=(), maxtasks=None, | ||
| wrap_exception=False): | ||
| assert maxtasks is None or (type(maxtasks) == int and maxtasks > 0) | ||
| if maxtasks is not None: |
There was a problem hiding this comment.
worker() is an internal API, I'm not sure it's worth maintaining this more complicated error reporting.
There was a problem hiding this comment.
I've tried to simplify it.
| Equivalent of `func(*args, **kwds)`. | ||
| ''' | ||
| assert self._state == RUN | ||
| if self._state != RUN: |
There was a problem hiding this comment.
apply_sync() seems to already have this check, so we can probably remove it here.
| cls._help_stuff_finish(inqueue, task_handler, len(pool)) | ||
|
|
||
| assert result_handler.is_alive() or len(cache) == 0 | ||
| if not result_handler.is_alive(): |
There was a problem hiding this comment.
I think this is also an internal error, which makes assert fine here.
There was a problem hiding this comment.
Or AssertionError?
|
|
||
| def put(self, obj, block=True, timeout=None): | ||
| assert not self._closed | ||
| assert not self._closed, "Queue {0!r} already closed".format(self) |
There was a problem hiding this comment.
"already" sounds unwanted, since put() isn't asking to close the queue.
There was a problem hiding this comment.
Good point. Substituted "has been".
| @@ -0,0 +1,7 @@ | |||
| This is a fix of some of the uninformative asserts in `multiprocessing`, | |||
There was a problem hiding this comment.
I don't think three separate NEWS entries and such a detailed description are warranted for what is a small improvement :-)
There was a problem hiding this comment.
Well, it's now 1 short NEWS entry and I fixed most of the asserts. (Of course, then I had a typo due to an editor save problem... sigh. Unfortunately and ironically, the multiprocessing tests overload my current machine. Everything else works...) I was going nuts trying to figure out why something I knew I'd changed wasn't there, so shut down the pull request so as to not overload travis with both my fork's and cpython's builds. (I'm not paying them anything so I try to be nice to them.)
|
This now fixes, as far as I can tell, all uninformative asserts in multiprocessing except for those in Windows-specific code (I am not sufficiently familiar with processes on Windows to feel confident writing informative error messages). |
|
|
||
| Returns a pair of fds (status_r, data_w). The calling process can read | ||
| the child process's pid and (eventually) its returncode from status_r. | ||
| the child process\'s pid and (eventually) its returncode from status_r. |
There was a problem hiding this comment.
I don't know if you changed this because it tripped your text editor, but this escaping is unnecessary inside a triple-quoted string.
There was a problem hiding this comment.
Your guess is correct - it made xemacs unable to tell what was quoted after it, which got especially inconvenient when trying to make sure everything was indented correctly.
| for key, value in list(method_to_typeid.items()): | ||
| assert type(key) is str, '%r is not a string' % key | ||
| assert type(value) is str, '%r is not a string' % value | ||
| for key, value in list(method_to_typeid.items()): # isinstance? |
There was a problem hiding this comment.
I think these 3 lines should not be changed.
| def apply(self, func, args=(), kwds={}): | ||
| ''' | ||
| Equivalent of `func(*args, **kwds)`. | ||
| self._state should be RUN. |
There was a problem hiding this comment.
Hmm. Comments about internal state do not have a place in the docstring of a public method.
There was a problem hiding this comment.
"Pool must be running"?
There was a problem hiding this comment.
That sounds ok, though I'm not sure it bears mentioning.
| assert result_handler.is_alive() or len(cache) == 0 | ||
| if (not result_handler.is_alive()) and (len(cache) != 0): | ||
| raise AssertionError( | ||
| "Cannot have cache with result_hander not alive") |
There was a problem hiding this comment.
I'm not sure I get the point of adding a message here. The message will not be understandable for the average user, and whoever wants to debug it has to read the source code anyway.
There was a problem hiding this comment.
How does one tell where the AssertionError is happening, if the pool is being used not by the primary process but by a subprocess (possibly on another machine via a manager)?
There was a problem hiding this comment.
Thanks to the line number in the traceback, I guess?
There was a problem hiding this comment.
I wish that worked in the circumstances I noted above - see here, noting that it's a "nightly" build so includes the earlier patch, for an example.
| while self._woken_count.acquire(False): | ||
| res = self._sleeping_count.acquire(False) | ||
| assert res | ||
| assert res, 'res is {:n}'.format(res) |
There was a problem hiding this comment.
Not really useful, as res will simply be False here.
| def notify(self, n=1): | ||
| assert self._lock._semlock._is_mine(), 'lock is not owned' | ||
| assert not self._wait_semaphore.acquire(False) | ||
| assert not self._wait_semaphore.acquire(False), 'waiting to acquire' |
There was a problem hiding this comment.
The message is rather ambiguous and unhelpful to the user.
There was a problem hiding this comment.
Could you take a look and see if my alternative phrasing is better?
There was a problem hiding this comment.
I don't think it's better. It's ok to call notify() even if there are no waiters. This seems to be testing an internal invariant rather than an user error.
| while self._woken_count.acquire(False): | ||
| res = self._sleeping_count.acquire(False) | ||
| assert res, 'res is {:n}'.format(res) | ||
| assert res, 'Attempting to notify but no sleepers?' |
There was a problem hiding this comment.
Same comment as for the other assert above. I think the original assert is ok.
| def notify(self, n=1): | ||
| assert self._lock._semlock._is_mine(), 'lock is not owned' | ||
| assert not self._wait_semaphore.acquire(False) | ||
| assert not self._wait_semaphore.acquire(False), 'Nobody waiting?' |
There was a problem hiding this comment.
Perhaps this is an overlook, but the message here is still wrong (it's ok to have nobody waiting).
There was a problem hiding this comment.
Hrm. So exactly what is the assertion testing for? Thanks for the clarification!
There was a problem hiding this comment.
It allows waking up the exact desired number of waiters (see below). The logic is a bit difficult to understand, as the various semaphores don't have an obvious meaning, they are just used for careful synchronization between notifier and waiters.
There was a problem hiding this comment.
"notify: Should not have succeeded at acquiring _wait_semaphore"?
There was a problem hiding this comment.
Done (slightly shorter version).
| while self._woken_count.acquire(False): | ||
| res = self._sleeping_count.acquire(False) | ||
| assert res | ||
| assert res, 'Attempting to notify but no sleepers?' |
There was a problem hiding this comment.
And here too (it's more of a sophisticated internal detail that's been tested).
There was a problem hiding this comment.
OK... but what would the assert failing indicate?
There was a problem hiding this comment.
That the implementation has a bug :-) I don't think we can qualify it any further.
There was a problem hiding this comment.
Heh. "notify: Bug in sleeping_count.acquire - res should not be False"?
| familiarity), all asserts in `multiprocessing` are now more informative, and | ||
| some error types have been changed to more specific ones. - Allen W. Smith | ||
| (drallensmith on github) | ||
| Many asserts in `multiprocessing` are now more informative, and some error types have been changed to more specific ones. |
There was a problem hiding this comment.
I thought I was supposed to sign it?
There was a problem hiding this comment.
No, you don't need to. You can add an entry in Misc/ACKS, though (keeping alphabetical order).
There was a problem hiding this comment.
Already done, but thank you.
| @@ -1 +1 @@ | |||
| Many asserts in `multiprocessing` are now more informative, and some error types have been changed to more specific ones. | |||
| Many asserts in `multiprocessing` are now more informative, and some error types have been changed to more specific ones. - Allen W. Smith (drallensmith on github) | |||
There was a problem hiding this comment.
I had thought I was supposed to sign it?
|
Ok, I'm waiting for the CI results and I'll probably merge afterwards. |
|
Excellent; thanks for all of your help! P.S. Sorry for my earlier impatience; I did try to make up for it by doing a (non-core, of course) review of someone else's PR... |
|
Thank your for contributing this! |
* Make error message more informative Replace assertions in error-reporting code with more-informative version that doesn't cause confusion over where and what the error is. * Additional clarification + get travis to check * Change from SystemError to TypeError As suggested in PR comment by @pitrou, changing from SystemError; TypeError appears appropriate. * NEWS file installation; ACKS addition (will do my best to justify it by additional work) * Making current AssertionErrors in multiprocessing more informative * Blurb added re multiprocessing managers.py, queues.py cleanup * Further multiprocessing cleanup - went through pool.py * Fix two asserts in multiprocessing/util.py * Most asserts in multiprocessing more informative * Didn't save right version * Further work on multiprocessing error messages * Correct typo * Correct typo v2 * Blasted colon... serves me right for trying to work on two things at once * Simplify NEWS entry * Update 2017-08-18-17-16-38.bpo-5001.gwnthq.rst * Update 2017-08-18-17-16-38.bpo-5001.gwnthq.rst OK, never mind. * Corrected (thanks to pitrou) error messages for notify * Remove extraneous backslash in docstring.
This is a fix of some of the uninformative asserts in
multiprocessing, including inmultiprocessing.managersandmultiprocessing.queues. Those in the queues.py module remain AssertionErrors, as do some in the managers.py module; others have been changed to more specific errors. In BaseManager's interpretation of method_to_typeid entries, checking for type(key or value) == str has been replaced with isinstance(key or value, str).In pool.py, various uninformative asserts have been changed to other appropriate errors (generally ValueErrors with a few TypeErrors) and information added; one instance of checking of type (== int) was changed to isinstance([thing], int).
In util.py, two uninformative asserts have been changed to other appropriate errors (one TypeError and one ValueError) and information added; with the TypeError, one instance of checking of type (== int) was changed to isinstance([thing],int).
I have also added a couple of comments. (Git, BTW, seems somewhat confused in that merging from python/cpython as upstream doesn't tell it that some of the commits in question have already been done in the master version. I'm not sure what to do about that - I've tried rebase before, but run into problems with github with it.)
Also a question - is there some way that I should mark pull requests that might be suitable for cherry-picking for other versions of Python? My prior multiprocessing PR is such; the code was identical in 2.7.13 and 3.5.3, for instance (I'm not suggesting applying it to 2.7, as opposed to 3.5-dev). I am more uncertain about this one.
https://bugs.python.org/issue5001