You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
We use Django, which uses asgiref.local.Local for its connection management. While debugging memory buildup, I've noticed that thread-critical Locals create reference cycles when used in a sync context.
Steps to reproduce
Disable garbage collection
Set garbage collectors debug mode to DEBUG_LEAK
Create a Local variable in synchronous context
Try to read an inexistent attribute from said Local
Force garbage collection and look at its output
importgcfromasgiref.localimportLocall=Local(thread_critical=True)
gc.collect() # make sure there is no lingering garbagegc.disable()
gc.set_debug(gc.DEBUG_LEAK)
try:
getattr(l, "missing")
exceptAttributeError:
passgc.collect()
gc.set_debug(0)
Explanation
When Django tries to reference a database connection that is not yet established, it executes something like this (paraphrasing):
It attempts to find the asyncio.get_running_loop(), which raises a RuntimeError
The exception handler yields self._storage (at this point, we're still in the exception handler inside the context manager)
Local executes getattr on storage, which raises an AttributeError
The AttributeError is propagated back to the context manager and since it's in the exception handler, it's linked to the previous RuntimeError (Python assumes the AttributeError was raised while attempting to handle the RuntimeError)
At this point, both exceptions hold each other referenced (one through exception chaining, the other through the traceback)
They also hold everything up to the point in my code where I attempted to use the database connection referenced, preventing those objects from being freed as well
Potential solution
Changing the _lock_storage implementation to the following fixes the issue:
We use Django, which uses
asgiref.local.Local
for its connection management. While debugging memory buildup, I've noticed that thread-criticalLocal
s create reference cycles when used in a sync context.Steps to reproduce
DEBUG_LEAK
Local
variable in synchronous contextLocal
Explanation
When Django tries to reference a database connection that is not yet established, it executes something like this (paraphrasing):
Now, internally,
asgiref
's Local does this:Now, putting everything together:
getattr
on theLocal
object_lock_storage
context manager is enteredasyncio.get_running_loop()
, which raises aRuntimeError
self._storage
(at this point, we're still in the exception handler inside the context manager)Local
executesgetattr
onstorage
, which raises anAttributeError
AttributeError
is propagated back to the context manager and since it's in the exception handler, it's linked to the previousRuntimeError
(Python assumes theAttributeError
was raised while attempting to handle theRuntimeError
)Potential solution
Changing the
_lock_storage
implementation to the following fixes the issue:The text was updated successfully, but these errors were encountered: