Idempotency: explain the asymmetry between a walk and Call, and pin the principle with a test - #20
Merged
Merged
Conversation
Pages and Scan mark every page WithIdempotent themselves, while Call(ctx, "crm.deal.list", ...) does not, even though it is the same method. Nothing said why, so it read as an oversight — and the obvious way to "fix" an oversight like that is to guess idempotency from the method name, which is the one thing this SDK must not do. The asymmetry is not about the name. A walk is a read by construction: a Pager only ever re-issues the method it was built with, moving a cursor, and no arrangement of options makes it write. Call has only the string it was handed, and "crm.deal.list" is not a fact about the call, it is text. A rule like "*.list is a read" would be a guess about every method family Bitrix24 has shipped and every one it ships next, applied silently, on the side that creates duplicates when it is wrong. So the honest fix is documentation plus a guard. The rule now has a test of its own: the same crm.deal.list is retried inside a walk and returned after one attempt through Call. Guessing from the suffix makes it fail, which is the point — that change would look like an improvement and pass every other test here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
What and why
A live run noticed an asymmetry:
Pages/Scanmark every pageWithIdempotentthemselves, whileCall(ctx, "crm.deal.list", …)does not, even though the method is the very same read. Nowhere was it said why, so it read as an oversight.And an oversight gets "fixed" the obvious way — by guessing from the method name. That is exactly what must not be done: the SDK's key principle is that the call is universal, the method name is a string, and the SDK does not interpret it.
crm.deal.listis a read, but*.listin another family may not be, and the mistake here is a silent one, on the side that ends up with duplicates.The asymmetry is not about the name
A walk knows what it is doing by construction: a
Pagercan only re-issue the method it was built with, moving a cursor, and no arrangement of options will make it write.Callhas only the string it was handed — and"crm.deal.list"is not a fact about the call, it is text.So these are not two different decisions about one method, but one decision applied to two different amounts of knowledge. That is now what it says — in the godoc of
WithIdempotent, inllms.txt,README.mdandCHANGELOG.md, together with the plain instruction: on their own reading calls,WithIdempotentis set by the caller.Nothing better than documentation suggested itself here
Considered and rejected:
Callidempotent by default — flips which side is the safe one:crm.deal.addwould silently create a second deal.The honest option left is to explain it and pin it down.
Checks
go build ./...go vet ./...gofmt -l .— no outputgo test -race ./...Compatibility
Tests
TestTheSameListMethodIsNotGuessedIdempotentFromItsName— the very samecrm.deal.liston an ambiguous failure (a 502 with no code): throughCall— one attempt and an error to the caller; insidePages— a retry and success.The test exists for the rule, not for convenience. It fails if the asymmetry is "closed" the simplest way — checked with the mutation
if strings.HasSuffix(method, ".list") { cc.idempotent = true }:What matters is that such a change looks like an improvement and passes all the other tests in the file: without this test, guessing from the name would have got into the SDK unnoticed, and would have broken for the family whose
*.listis not a read.