Skip to content

← Concurrency with Threads and asyncio step 46 of 56

Medium Primitives

Build a Request ID That Follows Tasks and Threads

Build the helpers a web service uses to tag its logs with a request ID. Code anywhere in the request can ask for the current ID, without anyone passing it as an argument.

with request_context("req-42"):
    current_request_id()  # "req-42"
    with request_context("req-43"):
        current_request_id()  # "req-43"
    current_request_id()  # "req-42"
current_request_id()  # None

What to write

@contextmanager
def request_context(request_id: str) -> Iterator[None]: ...

def current_request_id() -> str | None: ...

async def with_request[T](request_id: str, coro: Coroutine[Any, Any, T]) -> T: ...

Store the ID in one module-level ContextVar[str | None] with default=None. Then follow these rules:

  • request_context(request_id) sets the variable for the with block. On exit, it puts back exactly the value from before. Keep the token that set returns, and pass it to reset in a finally block. Then the old value comes back after an exception or a cancellation too.
  • Leaving a nested block puts back the outer ID. A plain set(None) on exit would lose it.
  • current_request_id() returns the current ID, or None when none is set.
  • with_request(request_id, coro) enters request_context(request_id), awaits coro directly inside it, in the current task, and returns the result. Exceptions and cancellation pass through to the caller.
  • IDs are strings, and the empty string "" is a real ID, distinct from None.

What the driver checks

Keep solve as it is. It runs one scenario:

Scenario What it checks
nested_task Two child tasks started inside scope A both see A
isolation Requests A and B run at the same time, and each sees only its own ID
no_leak A child task sets CHILD inside A, then sees A again, and the parent still sees A
exception A block that raises still puts back the previous value
cancel A cancelled with_request puts back the previous value before its caller sees the CancelledError
thread A function run with asyncio.to_thread inside A sees A in the worker thread
nested_ctx Nested scopes read A, B, A, then None

The report has these keys:

  • before and after: the ID at the start and end of the scenario. Both are None.
  • observed: the IDs the scenario read, in order.
  • thread_value: what the worker thread read in the thread scenario, and None otherwise.
  • outcome: "ok", "ValueError" or "CancelledError".