Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

gh-131466: concurrent.futures.Executor.map: avoid temporarily exceeding buffersize while collecting the next result #131467

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

Conversation

ebonnal
Copy link
Contributor

@ebonnal ebonnal commented Mar 19, 2025

Context recap:

If we have:

results: Iterator = executor.map(fn, iterable, buffersize=buffersize)

What happens when calling next(results):

  1. fetch the next arg from interable and put a task for fn(arg) in the buffer
  2. wait for the next result to be available
  3. yield the collected result

-> During step 2. there is buffersize + 1 buffered tasks.

This PR swaps steps 1. and 2. so that buffersize is never exceeded, even during next.

@ebonnal ebonnal changed the title gh-131466: concurrent.futures.Executor.map: avoid temporarily exceeding buffersize while collecting the next result gh-131466: concurrent.futures.Executor.map: avoid temporarily exceeding buffersize while collecting the next result Mar 20, 2025
@picnixz picnixz self-requested a review March 22, 2025 15:59
yield _result_or_cancel(fs.pop(), end_time - time.monotonic())

# Yield the awaited result
yield fs.pop().result()
Copy link
Contributor Author

@ebonnal ebonnal Mar 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to be discussed: this could be replaced by a lighter yield fs.pop()._result because the prior call to _result_or_cancel guarantees that at this point the result is available.

@ebonnal ebonnal requested a review from picnixz March 23, 2025 01:02
Copy link
Member

@picnixz picnixz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I understand that we could possibly exceed buffersize while collecting the next result, is there a real-word use case where it would really cause an issue? the reason is that we access to fs[-1] and then do fs.pop().

I see that have a del fut in _result_or_cancel() but can you confirm that it's sufficient to not hold any reference to the yet-to-be-popped future?

@picnixz
Copy link
Member

picnixz commented Mar 23, 2025

Asking Gregory as well since he's the mp expert c:

@ebonnal ebonnal requested a review from picnixz March 23, 2025 13:30
@ebonnal
Copy link
Contributor Author

ebonnal commented Mar 23, 2025

@picnixz sorry I re-asked your review because you made me realize that we actually don't need _result_or_cancel anymore:

test_executor_map_current_future_cancel introduced in #95169 does not break anymore because now if the fs[-1].result() access fails, the future is still in fs (not popped out like before) and it will be properly cancelled as part of the result_iterator's finally block.

I'm digging deeper into #95169 's context to check if I miss any non-tested scenario, especially regarding this:

    finally:
        # Break a reference cycle with the exception in self._exception
        del fut

@picnixz
Copy link
Member

picnixz commented Mar 23, 2025

especially regarding this:

yes, that's what I wanted to ask, but I'm not an expert here so i'll let you investigate first c:

fut.cancel()
finally:
# Break a reference cycle with the exception in self._exception
del fut
Copy link
Contributor Author

@ebonnal ebonnal Mar 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @graingert!
Context:
As a side effect, this PR may remove the need for _result_or_cancel (introduced in #95169). If fetching the next result raises a TimeoutError, its future will still be in fs and will be properly cancelled by the result_iterator's finally block.

Question:
Do you remember in which scenario the del fut was required? Removing it in the current main does not break any tests 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is if fut.result() raises an exception there's a reference cycle where fut.exception().__traceback__ -> fut.exception()

Probably worth adding a test, a git grep for no_other_refs will find a similar one

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! Will add the test 🫡

Copy link
Contributor Author

@ebonnal ebonnal Mar 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fyi #131701 adds the test @graingert @picnixz 🙏🏻

@picnixz picnixz requested a review from graingert March 29, 2025 23:50
# for free to join this conversation on GitHub. Already have an account? # to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants