Skip to content

[apps][long-polling] make retries configurable & add jitter, exponential backoff and a per-attempt abort - #491

Draft
Ayc0 wants to merge 1 commit into
masterfrom
Ayc0/retries
Draft

[apps][long-polling] make retries configurable & add jitter, exponential backoff and a per-attempt abort#491
Ayc0 wants to merge 1 commit into
masterfrom
Ayc0/retries

Conversation

@Ayc0

@Ayc0Ayc0 commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Motivation

The dev server's long-poll loop was hardcoded to 10 attempts that fired back-to-back with no delay, and nothing bounded a single attempt — if a connection stalled it just hung. Made the retry behaviour configurable and added the two standard API auto-retry strategies (jitter + exponential backoff) on top.

Changes

  • New apps.longPolling config: maxRetries (set to 1 to disable long-poll retrying entirely), jitter, exponentialBackoff, timeoutMs.
  • Each attempt now carries an AbortSignal.timeout(timeoutMs). A stalled attempt is abandoned and re-polled against the same receipt rather than failing the action — the receipt stays valid across attempts.
    • timeoutMs defaults to 40s, not 30s: the deadline needs headroom over the server's ~30s window, otherwise a healthy poll racing its own response gets aborted.
  • Retry delay is capped at 2s and uses equal jitter (d/2 + random*d/2). done: false is the expected outcome of a healthy poll, not a failure, so the delay is there to de-synchronize concurrent pollers — every ms of it is time with no poll in flight.
  • RequestOpts gains a signal that doRequest forwards to fetch (also flows through the OAuth path).

Two things worth flagging for review, both caught after the first pass:

  • doRequest calls bail(error) on any fetch-level rejection, so the naive version of this (just passing a signal) made things worse than no signal at all — a stall returned a 500 on attempt 1 instead of retrying. The request is now wrapped so aborts continue and non-abort errors rethrow.
  • The abort check can't use instanceof Error. Node rejects with a DOMException built in undici's realm, which fails instanceof across realm boundaries (vm contexts, and the Jest env) — so the retry silently wouldn't happen. It matches on name structurally instead.

QA Instructions

No visual change — dev-server behaviour only.

Covered by unit tests, including the paths that were broken: stall-then-retry against a real abort, maxRetries: 1 fail-fast, and non-abort errors surfacing instead of being retried away.

Blast Radius

apps dev server only (/__dd/executeAction), which is alpha. Defaults keep the previous 10-attempt behaviour, so the only change for existing users is that retries are now spaced out and a hung poll recovers instead of hanging.

The signal addition to RequestOpts is opt-in and unset everywhere else, so no other product plugin changes behaviour.

…ial backoff and a per-attempt abort
Prompts:
> add the ability to backend functions to disable the long-polling feature:
> https://github.com/DataDog/build-plugins/blob/a84854feb2e9af61b365f9af751683f3c0973a4e/packages/plugins/apps/src/vite/dev-server.ts#L200-L222
>
> Like to say that max = 1, etc.?
> Also, could we introduce 2 abilities to this?
>
> some jittering (so that if 3 requests are done in //, the 3 retries aren't done exactly at the same time)
> some exponential backoff?
> Those 2 strategies are quite standards for API auto retries
>
> And as the long poll is only valid for 30s, add a
> const signal = new AbortSignal();
> const timeout = timeout(30);
> timeout.then(() => signal.abort());
>
> for (…) {
> doAuthenticatedRequest(…, signal);
> /code-review
> create a new branch: Ayc0/retries, and /pr-ayc0-fe open it in draft
Sign up for freeto 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

@Ayc0