Serialize config.toml writes with a cross-process lock (+ concurrency tests)#217
Merged
Conversation
Add tests/test_concurrency.py for the two genuinely contended runtime paths that lacked direct coverage: - core.config: a 24-writer + reader stress test pins that _dump's temp-file + atomic os.replace never lets a concurrent reader observe a truncated file, and a deterministic two-"process" interleave documents the flip side it does not solve — lost updates, since there is no cross-process write lock. - streaming.StreamSession.on_turn: a deterministic test proves the render + save + meta critical section runs under _callback_lock (a second reader thread cannot acquire the lock mid-save), plus a two-source stress test that every finalized turn lands in the saved transcript exactly once and intact. Test-only change; no source modified. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FueRkrQHWSfpPf1KNaZskX
config.toml's mutating helpers do a read-modify-write (_load -> mutate -> _dump). The atomic os.replace in _dump already keeps a *reader* from seeing a torn file, but two `assembly` processes writing concurrently would still lose an update (both read the same config; the second dump clobbers the first). Wrap every read-modify-write in a cross-process filelock so concurrent writers no longer clobber each other; readers stay lock-free. filelock graduates from a dev-only transitive dependency to a declared runtime dependency. - add core/locking.py: generic cached cross-process FileLock helper (one instance per path, so nested acquisitions stay reentrant). - add core/config_lock.py: config.toml's lock_path/write_lock/locked/update helpers (kept out of config.py to stay under the file-length gate; _load and _dump are injected so the module avoids config's private API). - route set_*/clear_session/get_device_id/persist_login through the lock. Update tests/test_concurrency.py: the lost-update test becomes a no-loss test (16 concurrent writers each adding a distinct profile all survive), plus unit tests pinning the lock path, per-dir rebuild, and that the lock is held across the dump. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FueRkrQHWSfpPf1KNaZskX
…sts-dnlf2d # Conflicts: # aai_cli/AGENTS.md # aai_cli/core/config.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Concurrency tests
Adds
tests/test_concurrency.pyfor the two genuinely contended runtime paths that lacked direct coverage:core.config— config.toml's read-modify-write under thread contention: a 24-writer + reader stress test pins that_dump's temp-file + atomicos.replacenever lets a concurrent reader observe a truncated file.streaming.StreamSession.on_turn— runs on the SDK reader thread, and--system-audiodrives two of them. A deterministic test proves the render + save + meta critical section holds_callback_lock(a second thread can't acquire it mid-save), plus a two-source stress test that every finalized turn lands in the saved transcript exactly once and intact.Config write lock (the fix the tests motivated)
config.toml's mutating helpers do a read-modify-write (
_load→ mutate →_dump). The atomicos.replacealready keeps a reader from seeing a torn file, but twoassemblyprocesses writing concurrently would still lose an update (both read the same config; the second dump clobbers the first). This wraps every read-modify-write in a cross-processfilelockso concurrent writers no longer clobber each other; readers stay lock-free.filelockgraduates from a dev-only transitive dependency to a declared runtime dependency.core/locking.py— generic cached cross-processFileLockhelper (one instance per path, so nested acquisitions stay reentrant).core/config_lock.py— config.toml'slock_path/write_lock/locked/updatehelpers, kept out ofconfig.pyto stay under the file-length gate (_load/_dumpinjected so it avoids config's private API).set_*/clear_session/get_device_id/persist_loginnow route through the lock.The lost-update concurrency test is correspondingly a no-loss test (16 concurrent writers each adding a distinct profile all survive), with unit tests pinning the lock path, per-dir rebuild, and that the lock is held across the dump.
Full
scripts/check.shis green (100% patch coverage, mutation gate, deptry, CodeQL). Rebased/merged onto latestmain(which extractedkeyring_store); the lock and that refactor compose cleanly.🤖 Generated with Claude Code
https://claude.ai/code/session_01FueRkrQHWSfpPf1KNaZskX