Repository navigation
Conversation
Concurrent first binds of the same key raced creating the backing
ContextVar: every racing thread built its own and all but one were
orphaned (leaked). Worse, a thread that bound on an orphaned var then
failed in reset_contextvars() with ValueError ('Token was created by a
different ContextVar'), which also breaks bound_contextvars().
Guard the lazy creation with a module-level lock (double-checked), so
exactly one canonical var exists per key. Fixes hynek#851.
Merging this PR will not alter performance1 warning
Comparing |
…ind_contextvars var mypy inferred var's type from the fast-path assignment (ContextVar[Any]), so the slow-path re-assignment from _CONTEXT_VARS.get() (ContextVar | None) errored with [assignment]. An explicit ContextVar[Any] | None annotation preserves the narrowed ContextVar[Any] after the double-checked creation while keeping the hot path a single dict lookup, no lock.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #851.
When two threads call
bind_contextvars()with a new key at the same time, each thread built its owncontextvars.ContextVarand all but one were orphaned (leaked until the context closed).Worse than the leak: the losing thread's value and token live on the orphaned var, so a later
reset_contextvars()-- and thereforebound_contextvars()-- raisesValueError: Token was created by a different ContextVar.I verified this empirically: 8 threads racing a fresh key constructed 8 vars and 7 of 8 resets raised
ValueError(repro script in the issue thread if wanted).The fix guards the lazy creation with a module-level lock (double-checked), so exactly one canonical var exists per key.
The hot path (key already present) is unchanged: a single dict lookup, no lock taken.
Verification
test_concurrent_first_bind_creates_single_varintests/test_contextvars.py: deterministically forces 8 threads into the creation window (patched factory + start barrier) and asserts exactly oneContextVaris constructed and allreset_contextvars()calls succeed.Fails on unfixed code (8 vars constructed), passes with the fix.
tests/test_contextvars.py: 19/19 green.ruff checkandruff format --check: clean.Pull Request Checklist
api.py-- n/a (no new API).docs/api.rstby hand -- n/a.versionadded,versionchanged, ordeprecateddirectives --versionchanged:: 26.2.0onbind_contextvars..rst" and.md" files is written using semantic newlines.