-
-
Notifications
You must be signed in to change notification settings - Fork 349
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Avoid creating reference cycles #1805
Conversation
Codecov Report
@@ Coverage Diff @@
## master #1805 +/- ##
==========================================
+ Coverage 99.59% 99.62% +0.03%
==========================================
Files 114 114
Lines 14512 14550 +38
Branches 1108 1110 +2
==========================================
+ Hits 14453 14496 +43
+ Misses 42 38 -4
+ Partials 17 16 -1
|
trio/_core/_multierror.py
Outdated
@@ -114,6 +114,9 @@ def push_tb_down(tb, exc, preserved): | |||
preserved = set() | |||
new_root_exc = filter_tree(root_exc, preserved) | |||
push_tb_down(None, root_exc, preserved) | |||
# Delete the local functions avoid a reference cycle (see |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"to avoid a..."
@pytest.mark.skipif( | ||
sys.implementation.name != "cpython", reason="Only makes sense with refcounting GC" | ||
) | ||
async def test_simple_cancel_scope_usage_doesnt_create_cyclic_garbage(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
Would it be worth adding a test to catch garbage regressions at a higher level, say sleep()
? Even if that case isn't at 0 now, we could assert on the count being below what's observed currently.
Note: in Trio there are several places that have the same pattern as |
In #1770 (comment) there is a self-contained example that counts garbage across sleep(). I'd expect it to work within a test 🤔 I'll try to test staged trio + outcome changes against that test and our app in a day or so. |
Yeah, the test included in this PR was derived from that example, and the test does indeed fail without the fixes in these two PRs. The weird thing is that I tried locally adding some extra versions of the test to catch cases that I thought should also fail, like a nursery where one task is cancelled and another raises an exception, and in those cases I couldn't get it to fail or figure out why it wasn't failing. I guess if anyone is desperately curious you could check out the video at https://www.twitch.tv/videos/804632095 since I did this all on stream (NB that link will probably stop working in a few weeks, following twitch's garbage collection algorithm). But I'll probably get back to it later, just ran out of steam for today. |
@njsmith Should we request |
I'd opt for just noting in the newsfragment that Trio is doing its part, and together with the recent |
From the standalone test at #1770 (comment), here are the garbage items per sleep count for various cases:
However, even with both fixes I can create significant garbage (about 40 items) by way of a cancelled background task: async with trio.open_nursery() as nursery:
nursery.start_soon(trio.sleep_forever)
await trio.sleep(T/N) # a little less garbage if this is removed
nursery.cancel_scope.cancel() Cancelled nursery without the background task is 25 items: async with trio.open_nursery() as nursery:
nursery.cancel_scope.cancel() |
@belm0 does this test code reproduce what you are talking about here? import gc
import trio
import pprint
import objgraph
async def amain():
for _ in range(3):
async with trio.open_nursery() as n:
gc.collect()
gc.set_debug(gc.DEBUG_LEAK)
n.cancel_scope.cancel()
gc.collect()
gc.set_debug(0)
print(len(gc.garbage))
if _ < 2:
gc.garbage.clear()
if __name__ == "__main__":
trio.run(amain)
gc.collect()
pprint.pprint(gc.garbage)
stuff = [x for x in gc.garbage if "Cancelled" in repr(x)]
objgraph.show_backrefs(stuff,extra_ignore=(id(gc.garbage),),max_depth=10,filename="nursery_cycle.png") This is the reference cycle I observe with that code: Lines 932 to 940 in e113e56
I've been able to break the cycle by doing: if self._pending_excs:
try:
return MultiError(self._pending_excs)
finally:
del self._pending_excs or if self._pending_excs:
try:
return MultiError(self._pending_excs)
finally:
del popped, self |
@richardsheridan thank you for your contributions on the GC issue I think you'll find some other cycle cases by adding these inside the cancelled nursery: nursery.start_soon(trio.sleep_forever)
await trio.sleep(.01) |
Adding those doesn't seem to make any more garbage for me with the |
confirmed, thank you
For what it's worth, the try/finally may not be needed. I think |
Shall we merge this PR as is, or add the additional fix by @richardsheridan with corresponding test? I'm happy to commit the latter to this PR. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
When put together, this change + python-trio/outcome#29 should fix python-trio#1770
from MultiErrorCatcher.__exit__, _multierror.copy_tb, MultiError.filter, CancelScope.__exit__, and NurseryManager.__aexit__ methods. This was nearly impossible to catch until #1864 landed so it wasn't cleaned up in #1805 (or during the live stream: https://www.youtube.com/watch?v=E_jvJVYXUAk).
When put together, this change +
python-trio/outcome#29 should fix #1770
Note that the tests on this PR are expected to fail until that fix to
outcome
lands and has been released, since this PR includes a fullend-to-end test.