ADR-018. A retry policy belongs to the operation, and an action is never replayed
Status: accepted, 8 September 2026. Still in force.
Context
The runtime had no retry at all: one request, one response. On a cloud that
makes a playbook fragile for nothing, because a 503 lasting thirty seconds
fails an entire deployment.
The obvious fix is the wrong one. A Retry(total=5) on a requests session, or
any policy held by the client, applies the same rule to a GET and to the
POST that reboots a machine. A reboot played twice is not a reboot. The
policy has to belong to the operation, the way a comparison strategy belongs to
the field (ADR-009).
What the policy can be derived from is narrow, and that is deliberate. No Scaleway document declares an operation idempotent, none carries an idempotency key, and none says which failures are safe to replay. Two facts remain, and both are stated rather than guessed: the HTTP method, whose safety RFC 9110 fixes, and the class the classifier established.
The families of failure are not equivalent either:
a
429is the rate limiter answering before the API. The request was not processed, so replaying it changes nothing that has not already been changed;a
500,502,503or504is ambiguous. After one of them, a write may have landed and only a read could tell;a transport failure, a timeout or a refused connection, is the most ambiguous of all: the request may have left.
Decision
Every operation carries a retry policy, and the generator derives it from
the HTTP method and the operation class. Three values, and the class is tested
first because ServerAction is a POST exactly as a creation would be:
policy |
which operations |
replayed on |
|---|---|---|
|
|
|
|
the writes of a managing module |
|
|
the operation an action module triggers |
nothing |
limited does not replay an ambiguous failure. After a 502 the write may
have landed, and this runtime prefers to fail loudly rather than report a state
it did not observe, which is the same rule the postcondition check applies
(ADR-010).
Retry-After is honoured, in both forms RFC 9110 allows, a number of
seconds or an HTTP date. Reading only one of them would ignore the other in
silence and replay too early against an API that just said no. An unreadable
header falls back to the default wait rather than failing the module: the
server already said the essential by answering 429.
A Retry-After beyond the ceiling is not waited for. The server can
announce minutes; obeying blindly would make a module that looks frozen, and the
error that comes out carries the header in its message.
The runtime default is never. A module produced by an older generator,
declaring nothing, must not start replaying writes because this runtime learned
how.
The retry lives around the single HTTP call, and nowhere else. Putting it in
request would make every caller responsible for knowing whether it had already
been replayed; putting it in a requests session would make it global, hence
identical for a read and for a reboot, which is what this record refuses.
Consequences
An operation that deserves a different policy than the derivation gives has no
way to say so yet. depuis_le_nom exists and refuses an invented value, so an
override can be wired the day a case appears; until then, adding the plumbing
would be building for a case nobody has met.
The waits are a plain increasing sequence rather than an exponential backoff with jitter. The exact values are not what matters here, and a formula nobody can read is a formula nobody can judge. If a measurement ever shows the sequence matters, it will be changed with that measurement.
A limited write that fails on a 503 fails the playbook where a replay might
have succeeded. That is the price, and it is the direction this repository errs
in everywhere else: a module that reports what it did not observe is worse than
a module that fails.
Nothing here was measured against a real 429 or a real 503 from Scaleway:
the behaviour on those codes is derived from RFC 9110 and from what the codes
mean, not from an observation. The tests fabricate the responses.
What this record does not decide
Whether a failed write should be reconciled by re-reading and comparing before deciding to replay. That would be a stronger guarantee than failing, and it needs the read to be cheap and the comparison to be exact for every managed field. ADR-010 already reads after a successful write; extending it to an ambiguous failure is a separate decision.