From c229526af70273b3a0b267be17f723da4c7aca58 Mon Sep 17 00:00:00 2001 From: Alexander Hartl Date: Tue, 2 Jul 2024 10:00:37 +0200 Subject: [PATCH 1/4] Store strong references to pending tasks --- .../pycore_global_objects_fini_generated.h | 4 + Include/internal/pycore_global_strings.h | 4 + .../internal/pycore_runtime_init_generated.h | 4 + .../internal/pycore_unicodeobject_generated.h | 16 ++++ Lib/asyncio/base_events.py | 14 ++++ Lib/asyncio/futures.py | 10 +-- Lib/asyncio/tasks.py | 8 ++ Lib/test/test_asyncio/test_tasks.py | 3 + Modules/_asynciomodule.c | 81 +++++++++++++++++-- Modules/clinic/_asynciomodule.c.h | 40 ++++++++- 10 files changed, 172 insertions(+), 12 deletions(-) diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index 62e10e2325b8fb..1571e8a1a31f16 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -739,6 +739,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_abc_impl)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_abstract_)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_active)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_add_pending_task)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_align_)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_anonymous_)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_argtypes_)); @@ -748,11 +749,13 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_bootstrap)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_check_retval_)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_dealloc_warn)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_del_pending_task)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_feature_version)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_field_types)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_fields_)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_finalizing)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_find_and_load)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_finish_execution)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_fix_up_module)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_flags_)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_get_sourcefile)); @@ -768,6 +771,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_needs_com_addref_)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_only_immortal)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_pack_)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_pending_tasks)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_restype_)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_showwarnmsg)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_shutdown)); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index 0e3d1a5a9a9c76..2e3bb77bbd41a3 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -228,6 +228,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(_abc_impl) STRUCT_FOR_ID(_abstract_) STRUCT_FOR_ID(_active) + STRUCT_FOR_ID(_add_pending_task) STRUCT_FOR_ID(_align_) STRUCT_FOR_ID(_anonymous_) STRUCT_FOR_ID(_argtypes_) @@ -237,11 +238,13 @@ struct _Py_global_strings { STRUCT_FOR_ID(_bootstrap) STRUCT_FOR_ID(_check_retval_) STRUCT_FOR_ID(_dealloc_warn) + STRUCT_FOR_ID(_del_pending_task) STRUCT_FOR_ID(_feature_version) STRUCT_FOR_ID(_field_types) STRUCT_FOR_ID(_fields_) STRUCT_FOR_ID(_finalizing) STRUCT_FOR_ID(_find_and_load) + STRUCT_FOR_ID(_finish_execution) STRUCT_FOR_ID(_fix_up_module) STRUCT_FOR_ID(_flags_) STRUCT_FOR_ID(_get_sourcefile) @@ -257,6 +260,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(_needs_com_addref_) STRUCT_FOR_ID(_only_immortal) STRUCT_FOR_ID(_pack_) + STRUCT_FOR_ID(_pending_tasks) STRUCT_FOR_ID(_restype_) STRUCT_FOR_ID(_showwarnmsg) STRUCT_FOR_ID(_shutdown) diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index 5b8f29146287a3..94cf6dc49f88f8 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -737,6 +737,7 @@ extern "C" { INIT_ID(_abc_impl), \ INIT_ID(_abstract_), \ INIT_ID(_active), \ + INIT_ID(_add_pending_task), \ INIT_ID(_align_), \ INIT_ID(_anonymous_), \ INIT_ID(_argtypes_), \ @@ -746,11 +747,13 @@ extern "C" { INIT_ID(_bootstrap), \ INIT_ID(_check_retval_), \ INIT_ID(_dealloc_warn), \ + INIT_ID(_del_pending_task), \ INIT_ID(_feature_version), \ INIT_ID(_field_types), \ INIT_ID(_fields_), \ INIT_ID(_finalizing), \ INIT_ID(_find_and_load), \ + INIT_ID(_finish_execution), \ INIT_ID(_fix_up_module), \ INIT_ID(_flags_), \ INIT_ID(_get_sourcefile), \ @@ -766,6 +769,7 @@ extern "C" { INIT_ID(_needs_com_addref_), \ INIT_ID(_only_immortal), \ INIT_ID(_pack_), \ + INIT_ID(_pending_tasks), \ INIT_ID(_restype_), \ INIT_ID(_showwarnmsg), \ INIT_ID(_shutdown), \ diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index 7fa7bb79e21dc6..77a90437e096ba 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -712,6 +712,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(_add_pending_task); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(_align_); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -748,6 +752,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(_del_pending_task); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(_feature_version); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -768,6 +776,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(_finish_execution); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(_fix_up_module); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -828,6 +840,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(_pending_tasks); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(_restype_); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index f0e690b61a73dd..ebd35539da9a0c 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -449,6 +449,10 @@ def __init__(self): # Set to True when `loop.shutdown_default_executor` is called. self._executor_shutdown_called = False + # Holds references to all pending tasks to avoid garbage + # collection (see #91887) + self._pending_tasks = set() + def __repr__(self): return ( f'<{self.__class__.__name__} running={self.is_running()} ' @@ -2043,6 +2047,16 @@ def _set_coroutine_origin_tracking(self, enabled): self._coroutine_origin_tracking_enabled = enabled + def _add_pending_task(self, task): + """Add task to the _pending_tasks set. + + This avoids garbage collection as long as the loop is alive. + """ + self._pending_tasks.add(task) + + def _del_pending_task(self, task): + self._pending_tasks.discard(task) + def get_debug(self): return self._debug diff --git a/Lib/asyncio/futures.py b/Lib/asyncio/futures.py index 9c1b5e49e1a70b..7ed593b6427fe1 100644 --- a/Lib/asyncio/futures.py +++ b/Lib/asyncio/futures.py @@ -152,11 +152,11 @@ def cancel(self, msg=None): return False self._state = _CANCELLED self._cancel_message = msg - self.__schedule_callbacks() + self._finish_execution() return True - def __schedule_callbacks(self): - """Internal: Ask the event loop to call all callbacks. + def _finish_execution(self): + """Ask the event loop to call all callbacks. The callbacks are scheduled to be called as soon as possible. Also clears the callback list. @@ -257,7 +257,7 @@ def set_result(self, result): raise exceptions.InvalidStateError(f'{self._state}: {self!r}') self._result = result self._state = _FINISHED - self.__schedule_callbacks() + self._finish_execution() def set_exception(self, exception): """Mark the future done and set an exception. @@ -279,7 +279,7 @@ def set_exception(self, exception): self._exception = exception self._exception_tb = exception.__traceback__ self._state = _FINISHED - self.__schedule_callbacks() + self._finish_execution() self.__log_traceback = True def __await__(self): diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py index dadcb5b5f36bd7..c2cf068e0df8b0 100644 --- a/Lib/asyncio/tasks.py +++ b/Lib/asyncio/tasks.py @@ -127,6 +127,10 @@ def __init__(self, coro, *, loop=None, name=None, context=None, self._loop.call_soon(self.__step, context=self._context) _register_task(self) + def _finish_execution(self): + super()._finish_execution() + _unregister_task(self) + def __del__(self): if self._state == futures._PENDING and self._log_destroy_pending: context = { @@ -1047,6 +1051,8 @@ def factory(loop, coro, *, name=None, context=None): def _register_task(task): """Register an asyncio Task scheduled to run on an event loop.""" _scheduled_tasks.add(task) + loop = futures._get_loop(task) + loop._add_pending_task(task) def _register_eager_task(task): @@ -1082,6 +1088,8 @@ def _swap_current_task(loop, task): def _unregister_task(task): """Unregister a completed, scheduled Task.""" _scheduled_tasks.discard(task) + loop = futures._get_loop(task) + loop._del_pending_task(task) def _unregister_eager_task(task): diff --git a/Lib/test/test_asyncio/test_tasks.py b/Lib/test/test_asyncio/test_tasks.py index cc0d7f52a1bf4c..c2affbcb338fe8 100644 --- a/Lib/test/test_asyncio/test_tasks.py +++ b/Lib/test/test_asyncio/test_tasks.py @@ -2278,6 +2278,9 @@ async def kill_me(loop): coro = None source_traceback = task._source_traceback task = None + + # remove strong reference held by the event loop + self.loop._pending_tasks.clear() # no more reference to kill_me() task: the task is destroyed by the GC support.gc_collect() diff --git a/Modules/_asynciomodule.c b/Modules/_asynciomodule.c index a26714f9755df5..929e9e2fcb6227 100644 --- a/Modules/_asynciomodule.c +++ b/Modules/_asynciomodule.c @@ -486,6 +486,17 @@ future_schedule_callbacks(asyncio_state *state, FutureObj *fut) return 0; } +static inline int +future_call_finish_execution(FutureObj *fut) +{ + PyObject *res = PyObject_CallMethodNoArgs((PyObject *)fut, + &_Py_ID(_finish_execution)); + if (res == NULL) { + return -1; + } + Py_DECREF(res); + return 0; +} static int future_init(FutureObj *fut, PyObject *loop) @@ -562,7 +573,7 @@ future_set_result(asyncio_state *state, FutureObj *fut, PyObject *res) fut->fut_result = Py_NewRef(res); fut->fut_state = STATE_FINISHED; - if (future_schedule_callbacks(state, fut) == -1) { + if (future_call_finish_execution(fut) < 0) { return NULL; } Py_RETURN_NONE; @@ -626,7 +637,7 @@ future_set_exception(asyncio_state *state, FutureObj *fut, PyObject *exc) fut->fut_exception_tb = PyException_GetTraceback(exc_val); fut->fut_state = STATE_FINISHED; - if (future_schedule_callbacks(state, fut) == -1) { + if (future_call_finish_execution(fut) < 0) { return NULL; } @@ -784,10 +795,9 @@ future_cancel(asyncio_state *state, FutureObj *fut, PyObject *msg) Py_XINCREF(msg); Py_XSETREF(fut->fut_cancel_msg, msg); - if (future_schedule_callbacks(state, fut) == -1) { + if (future_call_finish_execution(fut) < 0) { return NULL; } - Py_RETURN_TRUE; } @@ -1444,6 +1454,26 @@ _asyncio_Future__make_cancelled_error_impl(FutureObj *self) return create_cancelled_error(state, self); } +/*[clinic input] +_asyncio.Future._finish_execution + +Ask the event loop to call all callbacks. + +The callbacks are scheduled to be called as soon as possible. Also +clears the callback list. +[clinic start generated code]*/ + +static PyObject * +_asyncio_Future__finish_execution_impl(FutureObj *self) +/*[clinic end generated code: output=77354407293553bb input=c18b4c42a7810aa9]*/ +{ + asyncio_state *state = get_asyncio_state_by_def((PyObject *)self); + if (future_schedule_callbacks(state, self) == -1) { + return NULL; + } + Py_RETURN_NONE; +} + static void FutureObj_finalize(FutureObj *fut) { @@ -1515,6 +1545,7 @@ static PyMethodDef FutureType_methods[] = { _ASYNCIO_FUTURE_DONE_METHODDEF _ASYNCIO_FUTURE_GET_LOOP_METHODDEF _ASYNCIO_FUTURE__MAKE_CANCELLED_ERROR_METHODDEF + _ASYNCIO_FUTURE__FINISH_EXECUTION_METHODDEF {"__class_getitem__", Py_GenericAlias, METH_O|METH_CLASS, PyDoc_STR("See PEP 585")}, {NULL, NULL} /* Sentinel */ }; @@ -1976,6 +2007,17 @@ register_task(asyncio_state *state, PyObject *task) return -1; } Py_DECREF(res); + + PyObject *loop = get_future_loop(state, task); + if (loop == NULL) { + return -1; + } + res = PyObject_CallMethodOneArg(loop, &_Py_ID(_add_pending_task), task); + Py_DECREF(loop); + if (res == NULL) { + return -1; + } + Py_DECREF(res); return 0; } @@ -1988,8 +2030,13 @@ register_eager_task(asyncio_state *state, PyObject *task) static int unregister_task(asyncio_state *state, PyObject *task) { - PyObject *res = PyObject_CallMethodOneArg(state->scheduled_tasks, - &_Py_ID(discard), task); + PyObject *loop = get_future_loop(state, task); + if (loop == NULL) { + return -1; + } + PyObject *res = PyObject_CallMethodOneArg(loop, &_Py_ID(_del_pending_task), + task); + Py_DECREF(loop); if (res == NULL) { return -1; } @@ -2583,6 +2630,27 @@ _asyncio_Task_set_name(TaskObj *self, PyObject *value) Py_RETURN_NONE; } + + +/*[clinic input] +_asyncio.Task._finish_execution +[clinic start generated code]*/ + +static PyObject * +_asyncio_Task__finish_execution_impl(TaskObj *self) +/*[clinic end generated code: output=9d218be34fb814b7 input=783199407feedc8a]*/ +{ + asyncio_state *state = get_asyncio_state_by_def((PyObject *)self); + if (future_schedule_callbacks(state, (FutureObj *)self) < 0) { + return NULL; + } + if (unregister_task(state, (PyObject*) self) < 0) { + return NULL; + } + Py_RETURN_NONE; +} + + static void TaskObj_finalize(TaskObj *task) { @@ -2665,6 +2733,7 @@ static PyMethodDef TaskType_methods[] = { _ASYNCIO_TASK_SET_NAME_METHODDEF _ASYNCIO_TASK_GET_CORO_METHODDEF _ASYNCIO_TASK_GET_CONTEXT_METHODDEF + _ASYNCIO_TASK__FINISH_EXECUTION_METHODDEF {"__class_getitem__", Py_GenericAlias, METH_O|METH_CLASS, PyDoc_STR("See PEP 585")}, {NULL, NULL} /* Sentinel */ }; diff --git a/Modules/clinic/_asynciomodule.c.h b/Modules/clinic/_asynciomodule.c.h index 6a9c8ff6d8fdd9..c6f24401a05966 100644 --- a/Modules/clinic/_asynciomodule.c.h +++ b/Modules/clinic/_asynciomodule.c.h @@ -481,6 +481,27 @@ _asyncio_Future__make_cancelled_error(FutureObj *self, PyObject *Py_UNUSED(ignor return _asyncio_Future__make_cancelled_error_impl(self); } +PyDoc_STRVAR(_asyncio_Future__finish_execution__doc__, +"_finish_execution($self, /)\n" +"--\n" +"\n" +"Ask the event loop to call all callbacks.\n" +"\n" +"The callbacks are scheduled to be called as soon as possible. Also\n" +"clears the callback list."); + +#define _ASYNCIO_FUTURE__FINISH_EXECUTION_METHODDEF \ + {"_finish_execution", (PyCFunction)_asyncio_Future__finish_execution, METH_NOARGS, _asyncio_Future__finish_execution__doc__}, + +static PyObject * +_asyncio_Future__finish_execution_impl(FutureObj *self); + +static PyObject * +_asyncio_Future__finish_execution(FutureObj *self, PyObject *Py_UNUSED(ignored)) +{ + return _asyncio_Future__finish_execution_impl(self); +} + PyDoc_STRVAR(_asyncio_Task___init____doc__, "Task(coro, *, loop=None, name=None, context=None, eager_start=False)\n" "--\n" @@ -940,6 +961,23 @@ PyDoc_STRVAR(_asyncio_Task_set_name__doc__, #define _ASYNCIO_TASK_SET_NAME_METHODDEF \ {"set_name", (PyCFunction)_asyncio_Task_set_name, METH_O, _asyncio_Task_set_name__doc__}, +PyDoc_STRVAR(_asyncio_Task__finish_execution__doc__, +"_finish_execution($self, /)\n" +"--\n" +"\n"); + +#define _ASYNCIO_TASK__FINISH_EXECUTION_METHODDEF \ + {"_finish_execution", (PyCFunction)_asyncio_Task__finish_execution, METH_NOARGS, _asyncio_Task__finish_execution__doc__}, + +static PyObject * +_asyncio_Task__finish_execution_impl(TaskObj *self); + +static PyObject * +_asyncio_Task__finish_execution(TaskObj *self, PyObject *Py_UNUSED(ignored)) +{ + return _asyncio_Task__finish_execution_impl(self); +} + PyDoc_STRVAR(_asyncio__get_running_loop__doc__, "_get_running_loop($module, /)\n" "--\n" @@ -1487,4 +1525,4 @@ _asyncio_current_task(PyObject *module, PyObject *const *args, Py_ssize_t nargs, exit: return return_value; } -/*[clinic end generated code: output=b26155080c82c472 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=35e3e0394ea06ade input=a9049054013a1b77]*/ From bbdbce1354e446cc8c4939a507fb80f9458c9845 Mon Sep 17 00:00:00 2001 From: Alexander Hartl Date: Tue, 2 Jul 2024 14:12:54 +0200 Subject: [PATCH 2/4] NEWS, ACKs --- Misc/ACKS | 1 + .../next/Library/2024-07-02-14-07-32.gh-issue-91887.eWzc5E.rst | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2024-07-02-14-07-32.gh-issue-91887.eWzc5E.rst diff --git a/Misc/ACKS b/Misc/ACKS index a406fca8744a5f..797593cfe7106c 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -723,6 +723,7 @@ Derek Harland Jason Harper David Harrigan Brian Harring +Alexander Hartl Jonathan Hartley Travis B. Hartwell Henrik Harutyunyan diff --git a/Misc/NEWS.d/next/Library/2024-07-02-14-07-32.gh-issue-91887.eWzc5E.rst b/Misc/NEWS.d/next/Library/2024-07-02-14-07-32.gh-issue-91887.eWzc5E.rst new file mode 100644 index 00000000000000..2b0185c7f0634f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-07-02-14-07-32.gh-issue-91887.eWzc5E.rst @@ -0,0 +1,2 @@ +Retain strong references for asyncio pending tasks to avoid rare crashes due +to brackground tasks. Patch by Alexander Hartl. From 9a5ada01b8857066cfb08c4fb58dd4782beffd2d Mon Sep 17 00:00:00 2001 From: Alexander Hartl Date: Thu, 11 Jul 2024 08:30:05 +0200 Subject: [PATCH 3/4] Improved news item Co-authored-by: Peter Bierma --- .../Library/2024-07-02-14-07-32.gh-issue-91887.eWzc5E.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2024-07-02-14-07-32.gh-issue-91887.eWzc5E.rst b/Misc/NEWS.d/next/Library/2024-07-02-14-07-32.gh-issue-91887.eWzc5E.rst index 2b0185c7f0634f..4564f4d5b577a0 100644 --- a/Misc/NEWS.d/next/Library/2024-07-02-14-07-32.gh-issue-91887.eWzc5E.rst +++ b/Misc/NEWS.d/next/Library/2024-07-02-14-07-32.gh-issue-91887.eWzc5E.rst @@ -1,2 +1,2 @@ -Retain strong references for asyncio pending tasks to avoid rare crashes due -to brackground tasks. Patch by Alexander Hartl. +Retain strong references for pending :class:`asyncio.Task` objects to avoid rare crashes due +to background tasks. Patch by Alexander Hartl. From 5d2b57e2db1133f4e6306f8a32f795ce3c3c17ad Mon Sep 17 00:00:00 2001 From: Alexander Hartl Date: Fri, 12 Jul 2024 08:32:26 +0200 Subject: [PATCH 4/4] Add test code to ensure #91887 is fixed --- Lib/test/test_asyncio/test_tasks.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Lib/test/test_asyncio/test_tasks.py b/Lib/test/test_asyncio/test_tasks.py index c2affbcb338fe8..950872abaa35a1 100644 --- a/Lib/test/test_asyncio/test_tasks.py +++ b/Lib/test/test_asyncio/test_tasks.py @@ -2279,6 +2279,14 @@ async def kill_me(loop): source_traceback = task._source_traceback task = None + # no more reference to kill_me() task in user code. the task + # should be kept alive as long as it is pending and we hold a + # reference to the event loop (#91887) + support.gc_collect() + + self.assertEqual(len(asyncio.all_tasks(loop=self.loop)), 1) + mock_handler.assert_not_called() + # remove strong reference held by the event loop self.loop._pending_tasks.clear()