Skip to content

fix: serialize state and history writes in the agentchat FastAPI sample - #8437

Open
inchang-ing wants to merge 1 commit into
microsoft:mainfrom
inchang-ing:fix/agentchat-fastapi-concurrent-writes
Open

inchang-ing wants to merge 1 commit into
microsoft:mainfrom
inchang-ing:fix/agentchat-fastapi-concurrent-writes

Conversation

@inchang-ing

Copy link
Copy Markdown

What this fixes

python/samples/agentchat_fastapi/app_agent.py persists the agent state and the chat history with a plain "w" open, from a handler FastAPI runs concurrently:

  • state: await agent.save_state() then open(state_path, "w")
  • history: get_history() -> append two messages -> open(history_path, "w")

Two overlapping /chat requests therefore interleave their bytes, and one of them can json.loads() a file that another request is still truncating. The history write is additionally a read-modify-write over a shared file with no mutual exclusion, so a request that started from a stale snapshot overwrites the entries a concurrent request just appended.

What changed

  • _write_json_atomic() writes the payload to a tempfile.mkstemp() file in the same directory and os.replace()s it into place, so a reader sees either the previous document or the new one, never a half-written file.
  • save_state() and append_history() take one module-level asyncio.Lock around the read-modify-write, so concurrent requests no longer drop each other's history entries.

chat() now calls those two helpers instead of opening the files itself; the rest of the sample is untouched.

How I verified it

I ran the chat() handler from this exact file under asyncio.gather() with only the model call stubbed out (on_messages() returns immediately), so all the file writes are the real ones. 12 concurrent requests:

before (file as it is on main), three runs:

  • 5, 6 and 7 of the 12 requests returned HTTP 500, from JSONDecodeError: Expecting value: line 1 column 1 when reading a file mid-write (and Permission denied when two writers opened the same path at once on Windows)
  • agent_history.json ended up with 4, 4 and 6 of the expected 24 entries

after, three runs:

  • 0 of the 12 requests failed
  • both files parsed, 24 of 24 history entries present

ruff check passes with the version pinned in python/pyproject.toml (0.4.8), and pyright in strict mode reports no errors for the file.

The sibling app_team.py sample writes both files the same way; I left it out to keep this change on the sample the report is about, and can extend it here or in a separate PR.


The change and the reproduction above were prepared with AI assistance.

Concurrent /chat requests wrote agent_state.json and agent_history.json with a
plain "w" open, so overlapping requests could interleave their bytes and leave
a file that no longer parses. The history was also read-modify-written without
any mutual exclusion, so every request but the last dropped its entries.

Both writes now go through a single asyncio lock and land via a temp file plus
os.replace, so readers never observe a partial document.

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.

1 participant