bpo-39190: Fix deadlock when callback raises

This commit is contained in:
Sindri Guðmundsson 2020-01-02 12:31:22 +00:00
parent c3a651ad25
commit b27f966554
2 changed files with 62 additions and 18 deletions

View File

@ -592,6 +592,9 @@ class Pool(object):
cache[job]._set(i, obj)
except KeyError:
pass
except Exception:
# Even if we raised we still want to handle callbacks
traceback.print_exc()
task = job = obj = None
while cache and thread._state != TERMINATE:
@ -609,6 +612,9 @@ class Pool(object):
cache[job]._set(i, obj)
except KeyError:
pass
except Exception:
# Even if we raised we still want to handle callbacks
traceback.print_exc()
task = job = obj = None
if hasattr(outqueue, '_reader'):
@ -772,13 +778,15 @@ class ApplyResult(object):
def _set(self, i, obj):
self._success, self._value = obj
if self._callback and self._success:
self._callback(self._value)
if self._error_callback and not self._success:
self._error_callback(self._value)
self._event.set()
del self._cache[self._job]
self._pool = None
try:
if self._callback and self._success:
self._callback(self._value)
if self._error_callback and not self._success:
self._error_callback(self._value)
finally:
self._event.set()
del self._cache[self._job]
self._pool = None
__class_getitem__ = classmethod(types.GenericAlias)
@ -809,11 +817,13 @@ class MapResult(ApplyResult):
if success and self._success:
self._value[i*self._chunksize:(i+1)*self._chunksize] = result
if self._number_left == 0:
if self._callback:
self._callback(self._value)
del self._cache[self._job]
self._event.set()
self._pool = None
try:
if self._callback:
self._callback(self._value)
finally:
del self._cache[self._job]
self._event.set()
self._pool = None
else:
if not success and self._success:
# only store first exception
@ -821,11 +831,13 @@ class MapResult(ApplyResult):
self._value = result
if self._number_left == 0:
# only consider the result ready once all jobs are done
if self._error_callback:
self._error_callback(self._value)
del self._cache[self._job]
self._event.set()
self._pool = None
try:
if self._error_callback:
self._error_callback(self._value)
finally:
del self._cache[self._job]
self._event.set()
self._pool = None
#
# Class whose instances are returned by `Pool.imap()`

View File

@ -2741,6 +2741,39 @@ class _TestPoolWorkerErrors(BaseTestCase):
p.close()
p.join()
class _TestPoolResultHandlerErrors(BaseTestCase):
ALLOWED_TYPES = ('processes', )
def test_apply_async_callback_raises_exception(self):
p = multiprocessing.Pool(1)
def job():
return 1
def callback(value):
raise Exception()
p.apply_async(job, callback=callback)
self.assertTrue(p._result_handler.is_alive())
p.close()
p.join()
def test_map_async_callback_raises_exception(self):
p = multiprocessing.Pool(1)
def job(value):
return value
def callback(value):
raise Exception()
p.map_async(job, [1], callback=callback)
self.assertTrue(p._result_handler.is_alive())
p.close()
p.join()
class _TestPoolWorkerLifetime(BaseTestCase):
ALLOWED_TYPES = ('processes', )
@ -5740,7 +5773,6 @@ def install_tests_in_module_dict(remote_globs, start_method):
__module__ = remote_globs['__name__']
local_globs = globals()
ALL_TYPES = {'processes', 'threads', 'manager'}
for name, base in local_globs.items():
if not isinstance(base, type):
continue