-
Notifications
You must be signed in to change notification settings - Fork 0
fix(core): retry busy write admission #235
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
22d47b1
3c09d3d
6c2985e
b4cad79
495b329
554f2f3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -85,6 +85,33 @@ export class RelayfileWritebackPendingError extends RelayfileWritebackError { | |||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| export interface RelayfileWritebackAdmissionTimeoutErrorOptions { | ||||||||||||||||||||||||||||||||||||||
| provider: string; | ||||||||||||||||||||||||||||||||||||||
| operation: string; | ||||||||||||||||||||||||||||||||||||||
| path: string; | ||||||||||||||||||||||||||||||||||||||
| timeoutMs: number; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| /** Direct HTTP admission never minted an op before the caller's admission deadline. */ | ||||||||||||||||||||||||||||||||||||||
| export class RelayfileWritebackAdmissionTimeoutError extends RelayfileWritebackError { | ||||||||||||||||||||||||||||||||||||||
| readonly path: string; | ||||||||||||||||||||||||||||||||||||||
| readonly timeoutMs: number; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| constructor(options: RelayfileWritebackAdmissionTimeoutErrorOptions) { | ||||||||||||||||||||||||||||||||||||||
| super({ | ||||||||||||||||||||||||||||||||||||||
| provider: options.provider, | ||||||||||||||||||||||||||||||||||||||
| operation: options.operation, | ||||||||||||||||||||||||||||||||||||||
| cause: new Error( | ||||||||||||||||||||||||||||||||||||||
| `writeback_admission_timeout: no operation admitted for ${options.path} after ${options.timeoutMs}ms` | ||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||
| retryable: true | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| this.name = "RelayfileWritebackAdmissionTimeoutError"; | ||||||||||||||||||||||||||||||||||||||
| this.path = options.path; | ||||||||||||||||||||||||||||||||||||||
| this.timeoutMs = options.timeoutMs; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| export interface RelayfileWritebackTerminalErrorOptions { | ||||||||||||||||||||||||||||||||||||||
| provider: string; | ||||||||||||||||||||||||||||||||||||||
| operation: string; | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -148,7 +175,12 @@ export interface IntegrationClientOptions { | |||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||
| * Max wait, in ms, for the Relayfile writeback worker to emit a receipt onto | ||||||||||||||||||||||||||||||||||||||
| * the just-written draft. Defaults to 3000ms. `0` means fire-and-forget — the | ||||||||||||||||||||||||||||||||||||||
| * client returns immediately without a receipt. | ||||||||||||||||||||||||||||||||||||||
| * client returns immediately without a receipt. In direct HTTP mode, an | ||||||||||||||||||||||||||||||||||||||
| * explicit value also bounds write admission as an independent phase. When | ||||||||||||||||||||||||||||||||||||||
| * omitted, receipt waiting defaults to 3s while admission defaults to 90s. | ||||||||||||||||||||||||||||||||||||||
| * Advertised delays are honored while they fit inside that admission budget; | ||||||||||||||||||||||||||||||||||||||
| * after three consecutive 30s delays, the deadline wins at t+90s before a | ||||||||||||||||||||||||||||||||||||||
| * fourth request. | ||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||
| writebackTimeoutMs?: number; | ||||||||||||||||||||||||||||||||||||||
| /** Poll interval while waiting for a receipt. Default 250ms. */ | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -197,6 +229,9 @@ export interface WritebackResult { | |||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const DEFAULT_WRITEBACK_TIMEOUT_MS = 3_000; | ||||||||||||||||||||||||||||||||||||||
| const SDK_LEGACY_RETRY_MAX_DELAY_MS = 2_000; | ||||||||||||||||||||||||||||||||||||||
| const WORKSPACE_BUSY_RETRY_MAX_DELAY_MS = 30_000; | ||||||||||||||||||||||||||||||||||||||
| const DEFAULT_WRITEBACK_ADMISSION_TIMEOUT_MS = WORKSPACE_BUSY_RETRY_MAX_DELAY_MS * 3; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| function isRecord(value: unknown): value is Record<string, unknown> { | ||||||||||||||||||||||||||||||||||||||
| return typeof value === "object" && value !== null && !Array.isArray(value); | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -207,6 +242,85 @@ function nonEmpty(value: string | undefined): string | undefined { | |||||||||||||||||||||||||||||||||||||
| return trimmed ? trimmed : undefined; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| function isWorkspaceBusyAdmission(value: unknown): boolean { | ||||||||||||||||||||||||||||||||||||||
| if (!isRecord(value) || value.code !== "workspace_busy") return false; | ||||||||||||||||||||||||||||||||||||||
| const reason = value.reason ?? (isRecord(value.details) ? value.details.reason : undefined); | ||||||||||||||||||||||||||||||||||||||
| return reason === "write_admission_limit"; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| function isWorkspaceBusyAdmissionError(value: unknown): value is RelayFileApiError { | ||||||||||||||||||||||||||||||||||||||
| return value instanceof RelayFileApiError && value.status === 429 && isWorkspaceBusyAdmission(value); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| function retryAfterDelayMs(value: string): number | undefined { | ||||||||||||||||||||||||||||||||||||||
| const trimmed = value.trim(); | ||||||||||||||||||||||||||||||||||||||
| if (/^\d+$/.test(trimmed)) { | ||||||||||||||||||||||||||||||||||||||
| const seconds = Number(trimmed); | ||||||||||||||||||||||||||||||||||||||
| if (Number.isFinite(seconds)) return seconds * 1_000; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| const timestamp = Date.parse(trimmed); | ||||||||||||||||||||||||||||||||||||||
| return Number.isNaN(timestamp) ? undefined : Math.max(0, timestamp - Date.now()); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+255
to
+263
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current implementation of If the To prevent this, we should use a regular expression to ensure the entire string consists only of digits before parsing it as relative seconds. function retryAfterDelayMs(value: string): number | undefined {
const trimmed = value.trim();
if (/^\d+$/.test(trimmed)) {
return Number.parseInt(trimmed, 10) * 1_000;
}
const timestamp = Date.parse(trimmed);
return Number.isNaN(timestamp) ? undefined : Math.max(0, timestamp - Date.now());
}
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 3c09d3d. Relative-seconds parsing now requires the entire trimmed header to be digits. Exact workspace-admission responses are normalized to a numeric delay before the SDK sees them, which also avoids the SDK misreading digit-leading date strings. Added a red/green regression using a digit-leading 1970 date (old behavior slept 1s per retry; fixed behavior retries immediately). |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| async function responseIsWorkspaceBusyAdmission(response: Response): Promise<boolean> { | ||||||||||||||||||||||||||||||||||||||
| if (response.status !== 429) return false; | ||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||
| return isWorkspaceBusyAdmission(await response.clone().json()); | ||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| function responseWithRetryAfter(response: Response, retryAfter: string): Response { | ||||||||||||||||||||||||||||||||||||||
| const headers = new Headers(response.headers); | ||||||||||||||||||||||||||||||||||||||
| headers.set("Retry-After", retryAfter); | ||||||||||||||||||||||||||||||||||||||
| return new Response(response.body, { | ||||||||||||||||||||||||||||||||||||||
| status: response.status, | ||||||||||||||||||||||||||||||||||||||
| statusText: response.statusText, | ||||||||||||||||||||||||||||||||||||||
| headers | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| function isDirectWriteAdmissionRequest(input: RequestInfo | URL, init?: RequestInit): boolean { | ||||||||||||||||||||||||||||||||||||||
| const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase(); | ||||||||||||||||||||||||||||||||||||||
| const url = input instanceof Request ? input.url : String(input); | ||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||
| return method === "PUT" && new URL(url, "http://relayfile.invalid").pathname.endsWith("/fs/file"); | ||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+284
to
+292
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If To make this robust against relative URLs, we should provide a dummy base URL to the
Suggested change
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 3c09d3d. Relative request URLs now resolve against a non-routable parsing-only base; absolute URLs remain unchanged. |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||
| * The SDK owns the only retry loop. Raising its max delay lets workspace write | ||||||||||||||||||||||||||||||||||||||
| * admission honor Retry-After; this adapter keeps the SDK's previous 2s cap for | ||||||||||||||||||||||||||||||||||||||
| * every other retryable response so unrelated 429/5xx behavior does not move. | ||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||
| function directRetryFetch(fetchImpl: typeof fetch): typeof fetch { | ||||||||||||||||||||||||||||||||||||||
| return async (input, init) => { | ||||||||||||||||||||||||||||||||||||||
| const response = await fetchImpl(input, init); | ||||||||||||||||||||||||||||||||||||||
| if (response.status !== 429 && (response.status < 500 || response.status > 599)) { | ||||||||||||||||||||||||||||||||||||||
| return response; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| const retryAfter = response.headers.get("Retry-After"); | ||||||||||||||||||||||||||||||||||||||
| if (!retryAfter) return response; | ||||||||||||||||||||||||||||||||||||||
| const delayMs = retryAfterDelayMs(retryAfter); | ||||||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||||||
| isDirectWriteAdmissionRequest(input, init) && | ||||||||||||||||||||||||||||||||||||||
| (await responseIsWorkspaceBusyAdmission(response)) | ||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||
| return delayMs === undefined | ||||||||||||||||||||||||||||||||||||||
| ? response | ||||||||||||||||||||||||||||||||||||||
| : responseWithRetryAfter(response, String(Math.ceil(delayMs / 1_000))); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (delayMs === undefined || delayMs <= SDK_LEGACY_RETRY_MAX_DELAY_MS) { | ||||||||||||||||||||||||||||||||||||||
| return response; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| return responseWithRetryAfter(response, String(SDK_LEGACY_RETRY_MAX_DELAY_MS / 1_000)); | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||
| function mountRootCandidate(value: string | undefined): string | undefined { | ||||||||||||||||||||||||||||||||||||||
| const trimmed = value?.trim(); | ||||||||||||||||||||||||||||||||||||||
| return trimmed ? trimmed : undefined; | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -304,12 +418,16 @@ function directClientConfig( | |||||||||||||||||||||||||||||||||||||
| nonEmpty(process.env.RELAYFILE_WORKSPACE) ?? | ||||||||||||||||||||||||||||||||||||||
| nonEmpty(process.env.RELAY_WORKSPACE_ID); | ||||||||||||||||||||||||||||||||||||||
| if (!baseUrl || !token || !workspaceId) return undefined; | ||||||||||||||||||||||||||||||||||||||
| const fetchImpl = directRetryFetch(client.fetchImpl ?? globalThis.fetch); | ||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||
| workspaceId, | ||||||||||||||||||||||||||||||||||||||
| relayfile: new RelayFileClient({ | ||||||||||||||||||||||||||||||||||||||
| baseUrl, | ||||||||||||||||||||||||||||||||||||||
| token, | ||||||||||||||||||||||||||||||||||||||
| fetchImpl: client.fetchImpl | ||||||||||||||||||||||||||||||||||||||
| fetchImpl, | ||||||||||||||||||||||||||||||||||||||
| retry: { | ||||||||||||||||||||||||||||||||||||||
| maxDelayMs: WORKSPACE_BUSY_RETRY_MAX_DELAY_MS | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -423,30 +541,53 @@ async function writeJsonFileViaRelayfileApi( | |||||||||||||||||||||||||||||||||||||
| body: unknown, | ||||||||||||||||||||||||||||||||||||||
| direct: { workspaceId: string; relayfile: RelayFileClient } | ||||||||||||||||||||||||||||||||||||||
| ): Promise<WritebackResult> { | ||||||||||||||||||||||||||||||||||||||
| const queued = await direct.relayfile.writeFile({ | ||||||||||||||||||||||||||||||||||||||
| workspaceId: direct.workspaceId, | ||||||||||||||||||||||||||||||||||||||
| path: relayPath, | ||||||||||||||||||||||||||||||||||||||
| baseRevision: "*", | ||||||||||||||||||||||||||||||||||||||
| contentType: "application/json", | ||||||||||||||||||||||||||||||||||||||
| content: `${JSON.stringify(body, null, 2)}\n` | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| if (queued.writeback && !nonEmpty(queued.opId)) { | ||||||||||||||||||||||||||||||||||||||
| throw new RelayfileWritebackReceiptError({ | ||||||||||||||||||||||||||||||||||||||
| provider, | ||||||||||||||||||||||||||||||||||||||
| operation, | ||||||||||||||||||||||||||||||||||||||
| opId: "(missing)", | ||||||||||||||||||||||||||||||||||||||
| reason: "queued writeback response did not include opId" | ||||||||||||||||||||||||||||||||||||||
| const timeoutMs = client.writebackTimeoutMs ?? DEFAULT_WRITEBACK_ADMISSION_TIMEOUT_MS; | ||||||||||||||||||||||||||||||||||||||
| const controller = timeoutMs > 0 ? new AbortController() : undefined; | ||||||||||||||||||||||||||||||||||||||
| const deadlineTimer = controller | ||||||||||||||||||||||||||||||||||||||
| ? setTimeout(() => controller.abort(), timeoutMs) | ||||||||||||||||||||||||||||||||||||||
| : undefined; | ||||||||||||||||||||||||||||||||||||||
| let admitted = false; | ||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||
| const queued = await direct.relayfile.writeFile({ | ||||||||||||||||||||||||||||||||||||||
| workspaceId: direct.workspaceId, | ||||||||||||||||||||||||||||||||||||||
| path: relayPath, | ||||||||||||||||||||||||||||||||||||||
| baseRevision: "*", | ||||||||||||||||||||||||||||||||||||||
| contentType: "application/json", | ||||||||||||||||||||||||||||||||||||||
| content: `${JSON.stringify(body, null, 2)}\n`, | ||||||||||||||||||||||||||||||||||||||
| signal: controller?.signal | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| admitted = true; | ||||||||||||||||||||||||||||||||||||||
| if (deadlineTimer !== undefined) clearTimeout(deadlineTimer); | ||||||||||||||||||||||||||||||||||||||
| if (queued.writeback && !nonEmpty(queued.opId)) { | ||||||||||||||||||||||||||||||||||||||
| throw new RelayfileWritebackReceiptError({ | ||||||||||||||||||||||||||||||||||||||
| provider, | ||||||||||||||||||||||||||||||||||||||
| operation, | ||||||||||||||||||||||||||||||||||||||
| opId: "(missing)", | ||||||||||||||||||||||||||||||||||||||
| reason: "queued writeback response did not include opId" | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| const receipt = queued.opId | ||||||||||||||||||||||||||||||||||||||
| ? await waitForOperationReceipt(client, provider, operation, direct, queued.opId, relayPath) | ||||||||||||||||||||||||||||||||||||||
| : undefined; | ||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||
| path: relayPath, | ||||||||||||||||||||||||||||||||||||||
| absolutePath: relayPath, | ||||||||||||||||||||||||||||||||||||||
| opId: queued.opId, | ||||||||||||||||||||||||||||||||||||||
| ...(receipt ? { receipt } : {}) | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||
| if (controller?.signal.aborted && !admitted && !(error instanceof RelayfileWritebackError)) { | ||||||||||||||||||||||||||||||||||||||
| throw new RelayfileWritebackAdmissionTimeoutError({ | ||||||||||||||||||||||||||||||||||||||
| provider, | ||||||||||||||||||||||||||||||||||||||
| operation, | ||||||||||||||||||||||||||||||||||||||
| path: relayPath, | ||||||||||||||||||||||||||||||||||||||
| timeoutMs | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| throw error; | ||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||
| if (deadlineTimer !== undefined) clearTimeout(deadlineTimer); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| const receipt = queued.opId | ||||||||||||||||||||||||||||||||||||||
| ? await waitForOperationReceipt(client, provider, operation, direct, queued.opId, relayPath) | ||||||||||||||||||||||||||||||||||||||
| : undefined; | ||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||
| path: relayPath, | ||||||||||||||||||||||||||||||||||||||
| absolutePath: relayPath, | ||||||||||||||||||||||||||||||||||||||
| opId: queued.opId, | ||||||||||||||||||||||||||||||||||||||
| ...(receipt ? { receipt } : {}) | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| function toAbsolutePath(client: IntegrationClientOptions, relayPath: string): string { | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -612,7 +753,12 @@ export async function writeJsonFile( | |||||||||||||||||||||||||||||||||||||
| if (cause instanceof RelayfileWritebackError) { | ||||||||||||||||||||||||||||||||||||||
| throw cause; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| throw new RelayfileWritebackError({ provider, operation, cause, retryable: false }); | ||||||||||||||||||||||||||||||||||||||
| throw new RelayfileWritebackError({ | ||||||||||||||||||||||||||||||||||||||
| provider, | ||||||||||||||||||||||||||||||||||||||
| operation, | ||||||||||||||||||||||||||||||||||||||
| cause, | ||||||||||||||||||||||||||||||||||||||
| retryable: isWorkspaceBusyAdmissionError(cause) | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: Three 30s
Retry-Afterdelays cannot reach the promised fourth request: the 90s deadline is armed before attempt one and fires when the third retry becomes due. Allow time for the final attempt (or explicitly clamp/rework the final retry) so a valid max-delay sequence is not aborted after only three attempts.Prompt for AI agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed against 495b329. This boundary is intentional: the 90s admission deadline is installed before attempt 1 and must win at t+90 when all three advertised delays equal the 30s safety maximum. The red regression pins attempts=3 plus cancellation of the pending fourth attempt, so no retry survives the caller deadline. Four total attempts remain proven for advertised delays that fit inside the deadline (t+0/5/10/15). Allowing the t+90 request would exceed the client bound and conflict with the daily-ship 90s deadline. The PR timeline documents this pathological boundary explicitly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The parent comment was wrong here: the 90s admission deadline is intentional and documented, and it’s meant to win at t+90. The PR also proves the four-attempt path for delays that stay within the deadline, so the missing t+90 attempt is expected behavior.