• Stefy Lanza (nextime / spora )'s avatar
    two requests to one pod must not read each other's answer (v0.4.80) · eb9b7ba0
    Stefy Lanza (nextime / spora ) authored
    The orchestrator already talks to a pod in parallel, and nothing in the path
    serializes: the front admits 256, acquiring a model instance is a ref-count, the
    pool hands the same pod to every caller and only GROWS at the in-flight
    threshold, the HTTP client keeps 64-256 connections per pod without blocking,
    and the blocking call runs in a 256-worker executor (measured on the production
    install; Python's own default there would have been 20).
    
    What was not parallel-safe is everything a backend cannot return in its text.
    `generate_chat` returns a string, so usage, logprobs, the tool calls a remote
    parsed and the thought it separated travel beside it and are read AFTER the call
    through `get_last_*`. Those were attributes of the backend -- and one backend
    object serves every concurrent request for its model. Each call began by
    resetting them, so a second request arriving between the first one's answer and
    its read blanked the first one's tool call, or handed it the second one's. That
    is the same shape as the empty answers seen in production on 2026-10-21, and
    under real parallelism it is not a rare interleaving but the common case.
    
    The store is per CALL now: a context variable, because a request is an asyncio
    task with its own context and `asyncio.to_thread` copies it, so the worker
    thread writes into the very dict the awaiting request will read. A stream
    captures the store when it opens, since a thread inherits no context variables,
    and accumulates its own thought instead of appending to a shared attribute. The
    backends keep their attributes as the fallback, so every caller that never opens
    a store -- the OCR pipeline, a script, a test -- behaves exactly as before.
    
    Tests drive two deliberately interleaved calls, and two interleaved streams,
    through ONE backend and assert neither sees the other's tool call, thought or
    usage. The suite's autouse fixtures gain one more: a store is request-scoped
    state, and tests share a context, so it is cleared between them like the
    template cache.
    Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01S8LAP3ueNzQJtwzhgVbq66
    eb9b7ba0
test_parallel_requests_dont_cross.py 8.63 KB