From 38f69a2c9515c9afc9df6672bf005c6f5079933a Mon Sep 17 00:00:00 2001 From: Woolim Cho Date: Mon, 6 Apr 2026 08:59:43 +0000 Subject: [PATCH] Fix memory leak from retained exception tracebacks in async tasks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When an async task fails, error._traceback and self._frame hold live references to frame objects and their locals: AsyncTask._error → ._traceback → traceback → frame → f_locals AsyncTask._frame → frame → f_locals, f_globals GC cannot collect these because they are reachable from the live AsyncTask (held by callers or creator chains), not unreachable cycles. Fix: clear self._frame in _computed() after the task finishes, and clear error._traceback after generator.throw() propagates it. Also migrate to single-arg throw(error) (3-arg form is deprecated). --- asynq/async_task.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/asynq/async_task.py b/asynq/async_task.py index ea34846..093c439 100644 --- a/asynq/async_task.py +++ b/asynq/async_task.py @@ -158,6 +158,7 @@ def _computed(self): # super() doesn't work in Cython-ed version here self._dependencies = [] self._last_value = None + self._frame = None # Release frame reference to prevent retention futures.FutureBase._computed(self) error = self.error() @@ -218,9 +219,15 @@ def _continue_on_generator(self, value, error): else: self._frame = debug.get_frame(self._generator) if hasattr(error, "_task"): - return self._generator.throw(error._type_, error, error._traceback) + try: + error.__traceback__ = error._traceback + return self._generator.throw(error) + finally: + # Traceback has been propagated into the generator; + # clear the stored copy to prevent frame retention + error._traceback = None else: - return self._generator.throw(type(error), error) + return self._generator.throw(error) except (StopIteration, GeneratorExit): # Returning leads to a StopIteration exception, which is # handled here. In this case we shouldn't need to extract frame