Skip to content
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

gh-74028: concurrent.futures.Executor.map: avoid reference cycles when an exception is raised #131701

Open
wants to merge 48 commits into
base: main
Choose a base branch
from

Conversation

ebonnal
Copy link
Contributor

@ebonnal ebonnal commented Mar 24, 2025

A failed future stores the raise exception in its ._exception attribute. We get a reference cycle if this exception's __traceback__ refers back to the future.

A particular care has been taken to avoid that (e.g. by @graingert in #95169)

This PR tests this behavior.

(follows up discussion in #131467)

Edit:
We extend this PR to ensure that no reference to an Exception is captured in the error's traceback frames.

…le from failed future captured in its exception’s traceback
@ebonnal ebonnal changed the title test_concurrent_futures.test_map_exception: assert no reference cycle from failed future captured in its exception’s traceback test_concurrent_futures: Executor.map: assert no reference cycle from failed future captured in its exception’s traceback Mar 25, 2025
@ebonnal ebonnal changed the title test_concurrent_futures: Executor.map: assert no reference cycle from failed future captured in its exception’s traceback concurrent.futures.Executor.map test: assert no reference cycle from failed future captured in its exception’s traceback Mar 25, 2025
@ebonnal ebonnal changed the title concurrent.futures.Executor.map test: assert no reference cycle from failed future captured in its exception’s traceback concurrent.futures.Executor.map: test no reference cycle from failed future captured in its exception’s traceback Mar 25, 2025
@ebonnal
Copy link
Contributor Author

ebonnal commented Mar 25, 2025

only failing for Ubuntu (free-threading) / build and test (ubuntu-24.04-arm) and I cannot reproduce on my ubuntu arm VM

Edit: actually I did reproduce it after a couple attempts

@picnixz picnixz changed the title concurrent.futures.Executor.map: test no reference cycle from failed future captured in its exception’s traceback gh-74028: concurrent.futures.Executor.map: test no reference cycle from failed future captured in its exception’s traceback Mar 30, 2025
@picnixz picnixz requested a review from graingert March 30, 2025 11:36
@graingert
Copy link
Contributor

graingert commented Mar 30, 2025

@ebonnal thanks for the test! Your first test was good, and it found a bug https://github.com/python/cpython/actions/runs/14048391667/job/39333996238?pr=131701#step:22:632

I've pushed a commit to revert to the original style of test - which includes the references in the error message, and fixed the bug.

@ebonnal ebonnal changed the title gh-74028: concurrent.futures.Executor.map: test no reference cycle from failed future captured in its exception’s traceback gh-74028: concurrent.futures.Executor.map: test there are no reference cycles when an exception is raised Mar 30, 2025
@ebonnal
Copy link
Contributor Author

ebonnal commented Mar 30, 2025

I've pushed a commit to revert to the original style of test - which includes the references in the error message, and fixed the bug.

Thanks @graingert !

probably we also need to finally: del exc_wrapper here too

Nice, I have cleaned up this exc_wrapper and added an additional test that goes through the error's traceback frames and search for any reference to an Exception.

(I have renamed the PR to show that we have extended its scope)

@graingert graingert marked this pull request as draft April 3, 2025 07:11
@graingert
Copy link
Contributor

ok I can't repeat it locally, even disabling the GC - sorry for hijacking your PR but I think I need to test it in CI to repeat

@ebonnal
Copy link
Contributor Author

ebonnal commented Apr 5, 2025

Hey @graingert, update from my investigations with a free-threading build on an Ubuntu ARM machine:
It fails like 50% of the time, looking like some GC race condition, but the test is always successful if we pause for a bit with a time.sleep(1) before checking the referrers.
Maybe for the scope of this PR we should go back to the state of 03f8ab4 and just add this little pause?

@graingert
Copy link
Contributor

I don't think it's a gc race condition as the problem still happens with the GC disabled. I think the problem is that the main thread is resumed before the background thread can delete the future.

I'm happy for you to revert back to 03f8ab4

@ebonnal ebonnal marked this pull request as ready for review April 7, 2025 12:45
@ebonnal
Copy link
Contributor Author

ebonnal commented Apr 7, 2025

Clear, thanks for the help @graingert, I appreciate it! Merging this test will definitely help prevent future regressions (e.g. in #131467).

Should we open a follow up issue summarizing the behavior we have found with free-threading builds on some platforms? Happy to do it!

@ebonnal
Copy link
Contributor Author

ebonnal commented Apr 11, 2025

@graingert ready for your review 🙏🏻

@ebonnal ebonnal changed the title gh-74028: concurrent.futures.Executor: avoid reference cycles when an exception is raised gh-74028: concurrent.futures.Executor.map: avoid reference cycles when an exception is raised Apr 12, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants