Repository navigation
Retry 409 write.contention in the v1 and v2 clients (aito-core #1619) - #87
Merged
Merged
Conversation
…e code A large write starved by concurrent same-table writes gives up without committing and, since core #1619, answers 409 with the code write.contention and Retry-After: 1. Nothing was written, so the request is safe to repeat. AitoClientV2.request (under every v2 method) now repeats it: - only on the code write.contention: the server's other 409s (an env being migrated, an old binary format that needs a repair, a release or branch conflict) and any status without the code are raised at once, as before; - after the Retry-After floor (seconds; 1 when absent or unreadable) plus full jitter up to 0.5 * 2**n s, capped at 8 s, so clients starved by the same churn don't retry in step; - at most write_contention_retries times (default 3; 0 turns it off; negative is refused). The final error keeps its code and status, and says how many times it was retried. Fail-first: tests/sdk/test_write_contention_retry.py (10 tests against a fake server; sleeps recorded, jitter pinned). FakeResponse grows a headers field. td-20261008104251322745 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AGxAujqUXtXMNcLfpqm96r
…and v2 Most traffic is still on v1, so a large v1 delete under same-table churn is the likelier real case (CPO). aito/_write_contention.py now holds the one policy both clients use: the code (v1 body `error`, v2 `data.code`), the Retry-After floor plus full jitter up to 0.5 * 2**n s (cap 8 s), and the validated retry count. v2's request() uses it (no behaviour change). v1 AitoClient(write_contention_retries=3): - request(): a 409 whose code is write.contention is sent again, up to the cap; the body is parsed for that check only on a 409, so a normal response is still parsed once; - async_request(): the same, with asyncio.sleep; aiohttp's raise_for_status moves after the check, so the body can be read first; - RequestError(..., retries) says "(retried N times)" when it gave up after retrying. Other 409s, 500/503, and raise_for_status=False behave as before. Fail-first: tests/sdk/test_v1_write_contention_retry.py (6 sync on real requests.Response objects, 3 async on an aiohttp stand-in). The v2 tests pin jitter through the shared module. td-20261008104251322745 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AGxAujqUXtXMNcLfpqm96r
Merged
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.
td-20261008104251322745. Since aito-core #1619, a large write starved by concurrent same-table writes answers 409
write.contentionwithRetry-After: 1, where it used to answer 500. Nothing was committed, so repeating it is safe. Both clients now do.The policy
It lives in one module,
aito/_write_contention.py, used by both clients:error, v2data.code), never on 409 alone. The server's other 409s are raised at once, as before: an env being migrated, an old binary format that needs a repair, a release or branch conflict, or a 409 with no code. 429/500/503 aren't retried as contention either, even with a Retry-After.Retry-Afterfloor (seconds; 1 when absent or unreadable), plus full jitter up to 0.5·2ⁿ s, capped at 8 s. Clients starved by the same churn don't come back in step.write_contention_retries=3by default, on either client. 0 turns it off; a negative value is refused. The final error keeps its code and status and adds "(retried N times)".Where
AitoClientV2.request: under every v2 method.AitoClient.requestandasync_request:raise_for_statusmoves after the check, so the body can be read first;Tests (fail-first)
tests/sdk/test_write_contention_retry.py(v2, 10 tests) andtests/sdk/test_v1_write_contention_retry.py(v1, 6 sync + 3 async). They run against a fake server: realrequests.Responseobjects for v1, an aiohttp stand-in for async. Sleeps are recorded and the jitter pinned.import aito.v2weight tests still pass; the new module is standard library only.Changelog: under Unreleased. It needs an engine newer than v2.11.4; older engines still answer this condition with a 500, which isn't retried.
🤖 Generated with Claude Code
https://claude.ai/code/session_01AGxAujqUXtXMNcLfpqm96r