Skip to content

Commit d0fdde6

Browse files
committed
Fix gh-487 reference cycles in Local
1 parent f34be84 commit d0fdde6

File tree

2 files changed

+58
-0
lines changed

2 files changed

+58
-0
lines changed

asgiref/local.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,15 @@ def __init__(self, thread_critical: bool = False) -> None:
8383
def _lock_storage(self):
8484
# Thread safe access to storage
8585
if self._thread_critical:
86+
is_async = True
8687
try:
8788
# this is a test for are we in a async or sync
8889
# thread - will raise RuntimeError if there is
8990
# no current loop
9091
asyncio.get_running_loop()
9192
except RuntimeError:
93+
is_async = False
94+
if not is_async:
9295
# We are in a sync thread, the storage is
9396
# just the plain thread local (i.e, "global within
9497
# this thread" - it doesn't matter where you are

tests/test_garbage_collection.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import gc
2+
3+
from asgiref.local import Local
4+
5+
6+
def disable_gc_for_garbage_collection_test() -> None:
7+
# Disable automatic garbage collection. To have control over when
8+
# garbage collection is performed. This is necessary to ensure that another
9+
# that thread doesn't accidentally trigger it by simply executing code.
10+
gc.disable()
11+
12+
# Delete the garbage list(`gc.garbage`) to ensure that other tests don't
13+
# interfere with this test.
14+
gc.collect()
15+
16+
# Set the garbage collection debugging flag to store all unreachable
17+
# objects in `gc.garbage`. This is necessary to ensure that the
18+
# garbage list is empty after execute test code. Otherwise, the test
19+
# will always pass. The garbage list isn't automatically populated
20+
# because it costs extra CPU cycles
21+
gc.set_debug(gc.DEBUG_SAVEALL)
22+
23+
24+
def clean_up_after_garbage_collection_test() -> None:
25+
# Clean up the garbage collection settings. Re-enable automatic garbage
26+
# collection. This step is mandatory to avoid running other tests without
27+
# automatic garbage collection.
28+
gc.set_debug(0)
29+
gc.enable()
30+
31+
32+
def test_thread_critical_Local_remove_all_reference_cycles() -> None:
33+
try:
34+
# given
35+
# Disable automatic garbage collection and set debugging flag.
36+
disable_gc_for_garbage_collection_test()
37+
38+
# when
39+
# Create thread critical Local object in sync context.
40+
try:
41+
getattr(Local(thread_critical=True), "missing")
42+
except AttributeError:
43+
pass
44+
# Enforce garbage collection to populate the garbage list for inspection.
45+
gc.collect()
46+
47+
# then
48+
# Ensure that the garbage list is empty. The garbage list is only valid
49+
# until the next collection cycle so we can only make assertions about it
50+
# before re-enabling automatic collection.
51+
assert gc.garbage == []
52+
# Restore garbage collection settings to their original state. This should always be run to avoid interfering
53+
# with other tests to ensure that code should be executed in the `finally' block.
54+
finally:
55+
clean_up_after_garbage_collection_test()

0 commit comments

Comments
 (0)