Skip to content

STOR-5615: Add configurable Durable Object retries - #7383

Open
apeacock1991 wants to merge 4 commits into
mainfrom
apeacock/STOR-5615-do-retry-policy-config
Open

apeacock1991 wants to merge 4 commits into
mainfrom
apeacock/STOR-5615-do-retry-policy-config

Conversation

@apeacock1991

@apeacock1991 apeacock1991 commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add an optional retryPolicy to Durable Object namespace bindings and carry it into minted stubs and jurisdictional subnamespaces.
  • Support configurable retry counts and call duration limits for eligible fetch() calls.
  • Add the off-by-default DURABLE_OBJECT_RETRIES_USERLAND autogate.
  • Cap full-jitter retry backoff at two seconds.
  • Keep existing redirect semantics. Each redirect starts a new actor call, while an explicit duration spans the full redirect chain.

Behavior

A retry policy has two fields:

  • maxAttempts sets the number of retries after the initial attempt. It defaults to four, matching the runtime default of five total attempts, and cannot exceed ten. Setting to 0 disables retries.
  • maxDurationMs limits the call to at most 60 seconds.

The binding policy applies only when DURABLE_OBJECT_RETRIES_USERLAND and both existing fetch-retry gates are enabled. If any gate is disabled, or the binding has no policy, the call uses the runtime defaults.

A configured duration starts after the output-gate wait. It covers the initial attempt, backoff, retries, and redirects, and interrupts an attempt still in flight when the deadline passes. It has no effect when the call cannot retry, either because maxAttempts is zero or because the request body is not replayable.

Without a configured duration, each actor call has the existing 10-second window in which a retry may start. The window does not interrupt an attempt in flight.

A redirect starts a new actor call with a fresh retry allowance, retry token, isRetry = false, subrequest accounting, and default retry window. A configured duration keeps its original start time across redirects.

This PR does not enable the new gate.

@apeacock1991
apeacock1991 requested review from a team as code owners September 16, 2026 08:59
@apeacock1991
apeacock1991 force-pushed the apeacock/STOR-5615-do-retry-policy-config branch 11 times, most recently from 7b33183 to c2a7334 Compare September 17, 2026 13:57
@apeacock1991
apeacock1991 requested a review from maxmcd September 17, 2026 13:59
@apeacock1991
apeacock1991 force-pushed the apeacock/STOR-5615-do-retry-policy-config branch from c2a7334 to 652c1fd Compare September 17, 2026 14:28
Cap the randomized backoff between actor call retries at two seconds.
Without a cap the doubling window reaches 800ms by the fifth attempt
and grows past the retry budget once attempt counts become
configurable, which would waste most of a caller's budget waiting.

Add fetch tests for redirects through Durable Object stubs. Each
redirect hop is already treated as a new actor call with a fresh
retry token and first-attempt subrequest accounting, and a redirected
streaming body becomes replayable and so retryable. Nothing today
asserts any of that. Configurable retry counts and durations change
how limits are measured across hops, so the current per-hop behavior
needs to be pinned down first.
Add an optional `retryPolicy` group to Durable Object namespace
bindings with a `maxAttempts` field, and carry it into minted stubs
and jurisdictional subnamespaces as a UserDefinedRetryPolicy. It
counts retries after the initial attempt, so zero disables retries.
Reject values above ten at config load.

Resolve the policy once per fetch into an ActorRetryPolicy. A binding
without a retry policy always runs under the runtime's five-attempt
default. A binding with one uses it when the new off-by-default
DURABLE_OBJECT_RETRIES_USERLAND autogate and both fetch retry gates
are on, and falls back to the default otherwise. The gates are checked
before the stub's I/O objects are touched so a disabled rollout never
reaches them. `maxAttempts` defaults to four so a policy that sets
only other fields still yields five attempts in total, and enabling
the gate changes nothing for bindings that have not opted in.

ActorCallRetryState reads the resolved policy and never asks where it
came from. Consolidate the two retries-exhausted exits into one helper
that records the outcome and returns the original disconnect.
Add `maxDurationMs` to the Durable Object namespace binding's
`retryPolicy`, capped at 60 seconds. When set, and the call can retry
at all, it bounds the whole logical call, including backoff and
redirects. Whatever is in flight when it passes is interrupted. The
caller sees a timeout error, or the original disconnect if an attempt
has already failed. When omitted, the runtime's 10-second window still
applies. That window only decides whether a retry may start and never
interrupts an attempt in flight.

ActorCallTimeLimit carries which of the two limits is in force, so
ActorCallRetryState reads the kind instead of inferring it from where
the policy came from. A call that cannot retry, because the count is
zero or the body is not replayable, runs under the default window
regardless of any configured duration.

Both limits start when the retry state is created, after the
output-gate wait. Covering the wait would mean resolving the target
stub's policy before the Request exists. Not worth it. The wait is
bounded by storage, not by this feature. A redirect carries the
original call's start so one deadline spans the chain, while the
default window resets per hop.

Add TimerChannel::atLimitTimeout() so a deadline can be expressed as
an absolute point on the limit-timeout clock.
Forward namespace retry policy through ctx.exports loopback bindings so stubs minted from them retain the binding's retry limits.
@apeacock1991
apeacock1991 force-pushed the apeacock/STOR-5615-do-retry-policy-config branch from 652c1fd to ff80881 Compare September 18, 2026 11:41
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