Skip to content

Idempotency: explain the asymmetry between a walk and Call, and pin the principle with a test - #20

Merged
ExaltedTrou6 merged 1 commit into
mainfrom
docs/idempotency-asymmetry
Aug 7, 2026
Merged

Idempotency: explain the asymmetry between a walk and Call, and pin the principle with a test#20
ExaltedTrou6 merged 1 commit into
mainfrom
docs/idempotency-asymmetry

Conversation

@ExaltedTrou6

@ExaltedTrou6 ExaltedTrou6 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

What and why

A live run noticed an asymmetry: Pages/Scan mark every page WithIdempotent themselves, while Call(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.list is a read, but *.list in 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 Pager can only re-issue the method it was built with, moving a cursor, and no arrangement of options will make it write. Call has 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, in llms.txt, README.md and CHANGELOG.md, together with the plain instruction: on their own reading calls, WithIdempotent is set by the caller.

Nothing better than documentation suggested itself here

Considered and rejected:

  • guessing from the suffix — breaks the principle, covered above;
  • a list of known reading methods inside the SDK — the same principle, only with a maintenance duty attached: the SDK would start falling behind the API exactly the way the removed generated wrappers did;
  • making Call idempotent by default — flips which side is the safe one: crm.deal.add would 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 output
  • go test -race ./...

Compatibility

  • The change is additive: behaviour and signatures are untouched, comments and a test were added

Tests

TestTheSameListMethodIsNotGuessedIdempotentFromItsName — the very same crm.deal.list on an ambiguous failure (a 502 with no code): through Call — one attempt and an error to the caller; inside Pages — 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 }:

--- FAIL: TestTheSameListMethodIsNotGuessedIdempotentFromItsName
    idempotent_test.go:217: want the ambiguous failure back

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 *.list is not a read.

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>
@ExaltedTrou6
ExaltedTrou6 merged commit ff0e62e into main Aug 7, 2026
2 of 4 checks passed
@ExaltedTrou6 ExaltedTrou6 changed the title Идемпотентность: объяснить асимметрию обхода и Call, и закрепить принцип тестом Idempotency: explain the asymmetry between a walk and Call, and pin the principle with a test Aug 7, 2026
@ExaltedTrou6
ExaltedTrou6 deleted the docs/idempotency-asymmetry branch August 7, 2026 15:32
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