geeViz.eeAuth.threadctx¶
Carry attribution context across thread boundaries.
Why this exists¶
EE attribution rides on ContextVar``s. ``TenantAwareHttp reads
CURRENT_USER_EMAIL / CURRENT_SESSION_ID / CURRENT_TENANT on
every outbound EE request and stamps them as X-Agent-* headers; the
ee-proxy resolves identity from those headers and mints the workload tag
that Cloud Monitoring bills against.
ContextVar``s do **not** cross ``threading.Thread boundaries. A
thread starts with an empty context regardless of what the spawning
thread had set. That is normally harmless, but geeViz runs EE work in
threads constantly — run_code execs user code in one, printEE
spawns one per call, map/chart helpers spawn more — so the identity set
for a tool call evaporated the moment the work moved off the calling
thread. Every such call reached the proxy with no identity headers and
was logged via=ANONYMOUS user=anonymous sub=- sess=-, then billed to
nobody.
Wrapping the thread target with run_in_context() makes the child
thread run inside a snapshot of the spawning thread’s context, so the
ContextVars are visible and the headers get stamped.
Usage¶
Take the snapshot on the spawning thread, at spawn time:
from geeViz.eeAuth.threadctx import run_in_context
t = threading.Thread(target=run_in_context(work), args=(x,))
t.start()
One wrapper per thread¶
contextvars.Context.run() refuses re-entry — calling it on a
Context that is already executing raises RuntimeError. Each call to
run_in_context() takes its own snapshot, so a wrapper is good for
exactly one thread. For a pool, wrap per task rather than reusing one
wrapper across workers:
pool.map(lambda a: run_in_context(work)(a), items) # per task
Deliberately stdlib-only, with no geeViz imports, so any module can import it without risking a cycle.
Functions
|
|
|
Wrap |
- geeViz.eeAuth.threadctx.run_in_context(fn: Callable[[...], _T]) Callable[[...], _T][source]¶
Wrap
fnso it runs inside a snapshot of the CALLER’s context.The snapshot is taken here, on the calling thread — not inside the wrapper — because by the time the wrapper runs it is already on the new thread, whose context is empty. Capturing late is the whole bug this function exists to avoid.
The returned callable is single-use per thread; see the module docstring.
- geeViz.eeAuth.threadctx.context_thread(target: Callable[[...], Any], *, args: tuple = (), kwargs: dict | None = None, daemon: bool | None = None, name: str | None = None) Thread[source]¶
threading.Threadthat inherits the caller’s context.Convenience over
Thread(target=run_in_context(fn), ...). Returns an unstarted thread so the caller keeps control ofstart().