caching: cache_disabled() context manager is not safe in asyncio contexts
Author: harsh4vardhanCreated Aug 6, 2026Updated Aug 6, 2026
cache_disabled() (lines 186–195) mutates a module-level global:
@contextlib.contextmanager
def cache_disabled():
global _caching_enabled
original_state = _caching_enabled
_caching_enabled = False
try:
yield
finally:
_caching_enabled = original_stateIn an asyncio event loop, when coroutine A enters this context manager and then awaits, the event loop runs other coroutines. Those coroutines see _caching_enabled = False even though they never opted in — they will skip the cache and recompute unnecessarily (or see inconsistent behaviour if the cache is expected to be active).
Minimum example (deterministic with asyncio.Event synchronization):
import asyncio, contextlib
_caching_enabled = True # replica of caching.py global
@contextlib.contextmanager
def cache_disabled(): # exact code from caching.py:186-195
global _caching_enabled
original_state = _caching_enabled
_caching_enabled = False
try:
yield
finally:
_caching_enabled = original_state
async def coroutine_a(entered, done):
with cache_disabled():
entered.set() # signal: flag is now False
await done.wait() # yield — B runs here
async def coroutine_b(entered, done):
await entered.wait() # wait until A holds the context
print(_caching_enabled) # False — B never called cache_disabled()
done.set()
async def main():
e, d = asyncio.Event(), asyncio.Event()
await asyncio.gather(coroutine_a(e, d), coroutine_b(e, d))
asyncio.run(main())
# prints: FalseFix: replace _caching_enabled with a contextvars.ContextVar, which isolates the flag per-coroutine execution context:
import contextvars
_caching_enabled: contextvars.ContextVar[bool] = contextvars.ContextVar(
"_caching_enabled", default=True
)
@contextlib.contextmanager
def cache_disabled():
token = _caching_enabled.set(False)
try:
yield
finally:
_caching_enabled.reset(token)Source: dottxt-ai/outlines