Skip to content

fix(contextvars): make bind_contextvars thread-safe for new keys - #852

Open
nosmile99 wants to merge 2 commits into
hynek:mainfrom
nosmile99:fix/bind-contextvars-thread-safe-851
Open

nosmile99 wants to merge 2 commits into
hynek:mainfrom
nosmile99:fix/bind-contextvars-thread-safe-851

Conversation

@nosmile99

Copy link
Copy Markdown

Summary

Fixes #851.

When two threads call bind_contextvars() with a new key at the same time, each thread built its own contextvars.ContextVar and 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 therefore bound_contextvars() -- raises ValueError: 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

  • New regression test test_concurrent_first_bind_creates_single_var in tests/test_contextvars.py: deterministically forces 8 threads into the creation window (patched factory + start barrier) and asserts exactly one ContextVar is constructed and all reset_contextvars() calls succeed.
    Fails on unfixed code (8 vars constructed), passes with the fix.
  • Full tests/test_contextvars.py: 19/19 green.
  • ruff check and ruff format --check: clean.

Pull Request Checklist

  • I acknowledge this project's AI policy.
  • This pull request is not from my `main" branch.
  • There's tests for all new and changed code.
  • New APIs are added to our typing tests in api.py -- n/a (no new API).
  • Updated documentation for changed code.
    • New functions/classes have to be added to docs/api.rst by hand -- n/a.
    • Changed/added classes/methods/functions have appropriate versionadded, versionchanged, or deprecated directives -- versionchanged:: 26.2.0 on bind_contextvars.
  • Documentation in .rst" and .md" files is written using semantic newlines.
  • Changes (and possible deprecations) are documented in the changelog.

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.
@codspeed

codspeed Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

1 warning

⚠️ 1 benchmark is dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

✅ 11 untouched benchmarks


Comparing nosmile99:fix/bind-contextvars-thread-safe-851 (4726c97) with main (19c4f21)

Open in CodSpeed

…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

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bind_contextvars is not thread-safe?

1 participant