From 5390224bf71f28331e166a057bc3adc62df2c751 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Sun, 2 Aug 2026 01:39:03 -0700 Subject: [PATCH] fix(relay): declare reconnection to the director's verified fast lane (#12086) Recovery and broker open now send the optional reconnect hint so the director admits already-assigned hosts through its bounded fast lane (orca-cloud#212) instead of the placement queue that starved session recovery during the 2026-08 incident. A rolled-back director that rejects the hinted field gets one unhinted retry. Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> --- .../runtime/relay/relay-http-client.test.ts | 53 +++++++++++++++++++ src/main/runtime/relay/relay-http-client.ts | 13 ++++- src/main/runtime/relay/relay-origin-pool.ts | 3 ++ .../runtime/relay/relay-session-broker.ts | 4 ++ 4 files changed, 72 insertions(+), 1 deletion(-) diff --git a/src/main/runtime/relay/relay-http-client.test.ts b/src/main/runtime/relay/relay-http-client.test.ts index 29739a7e4..213d27629 100644 --- a/src/main/runtime/relay/relay-http-client.test.ts +++ b/src/main/runtime/relay/relay-http-client.test.ts @@ -56,6 +56,59 @@ describe('relay HTTP client', () => { expect(fetch.mock.calls[0]?.[1]?.signal).toBeInstanceOf(AbortSignal) }) + it('declares reconnection in the assignment body when hinted', async () => { + const fetch = vi.fn(async () => + Response.json({ + v: 1, + cellUrl: 'https://relay-c1.example', + assignmentEpoch: 4, + lease: 'lease-jwt' + }) + ) + await expect( + requestRelayAssignment({ + directorUrl: 'https://relay.example', + relayToken: 'scoped-token', + relayHostId: 'AbCdEf0123_-xyZ9', + reconnect: true, + fetch + }) + ).resolves.toMatchObject({ assignmentEpoch: 4 }) + expect(JSON.parse(String(fetch.mock.calls[0]?.[1]?.body))).toEqual({ + v: 1, + relayHostId: 'AbCdEf0123_-xyZ9', + reconnect: true + }) + }) + + it('retries once unhinted when a rolled-back director rejects the hint', async () => { + const fetch = vi + .fn() + .mockResolvedValueOnce(Response.json({ error: 'invalid_request' }, { status: 400 })) + .mockResolvedValueOnce( + Response.json({ + v: 1, + cellUrl: 'https://relay-c1.example', + assignmentEpoch: 4, + lease: 'lease-jwt' + }) + ) + await expect( + requestRelayAssignment({ + directorUrl: 'https://relay.example', + relayToken: 'scoped-token', + relayHostId: 'AbCdEf0123_-xyZ9', + reconnect: true, + fetch + }) + ).resolves.toMatchObject({ assignmentEpoch: 4 }) + expect(fetch).toHaveBeenCalledTimes(2) + expect(JSON.parse(String(fetch.mock.calls[1]?.[1]?.body))).toEqual({ + v: 1, + relayHostId: 'AbCdEf0123_-xyZ9' + }) + }) + it('aborts a blackholed assignment request so recovery can retry', async () => { const fetch = vi.fn( async (_url, init) => diff --git a/src/main/runtime/relay/relay-http-client.ts b/src/main/runtime/relay/relay-http-client.ts index 394f5eee0..0cb370f5b 100644 --- a/src/main/runtime/relay/relay-http-client.ts +++ b/src/main/runtime/relay/relay-http-client.ts @@ -116,6 +116,7 @@ export async function requestRelayAssignment(input: { directorUrl: string relayToken: string relayHostId: string + reconnect?: boolean fetch?: typeof globalThis.fetch requestDeadlineMs?: number }): Promise { @@ -129,11 +130,21 @@ export async function requestRelayAssignment(input: { 'content-type': 'application/json' }, signal: AbortSignal.timeout(input.requestDeadlineMs ?? RELAY_HTTP_REQUEST_DEADLINE_MS), - body: JSON.stringify({ v: 1, relayHostId: input.relayHostId }) + body: JSON.stringify({ + v: 1, + relayHostId: input.relayHostId, + // Declares likely reconnection so the director can verify and admit + // through its bounded fast lane instead of the placement queue. + ...(input.reconnect ? { reconnect: true } : {}) + }) }) if (!response.ok) { const retryAfterMs = relayRetryAfterMs(response.headers.get('retry-after')) await cancelUnreadResponseBody(response) + if (input.reconnect && response.status === 400) { + // A rolled-back director rejects unknown fields; retry once unhinted. + return await requestRelayAssignment({ ...input, reconnect: false }) + } throw new RelayHttpError('assignment', response.status, retryAfterMs) } const parsed = AssignmentResponseSchema.safeParse(await response.json()) diff --git a/src/main/runtime/relay/relay-origin-pool.ts b/src/main/runtime/relay/relay-origin-pool.ts index 738860177..6f5f5951b 100644 --- a/src/main/runtime/relay/relay-origin-pool.ts +++ b/src/main/runtime/relay/relay-origin-pool.ts @@ -163,6 +163,9 @@ export class RelayOriginPool { directorUrl: this.options.directorUrl, relayToken: this.relayJwt, relayHostId: this.options.relayHostId, + // Recovery always follows an established assignment; the director + // verifies this and admits through its reconnect fast lane. + reconnect: true, fetch: this.options.fetch }) this.assertCurrent() diff --git a/src/main/runtime/relay/relay-session-broker.ts b/src/main/runtime/relay/relay-session-broker.ts index 02a5832a6..e2a8402a0 100644 --- a/src/main/runtime/relay/relay-session-broker.ts +++ b/src/main/runtime/relay/relay-session-broker.ts @@ -208,6 +208,10 @@ export class RelaySessionBroker { directorUrl: this.options.authConfig.relayDirectorUrl, relayToken: authorization.relayToken, relayHostId: this.relayHostId, + // Any previously paired host likely holds a durable assignment; the + // director verifies the claim, and first-ever pairing simply falls + // through to the placement lane. + reconnect: true, fetch: this.options.fetch }) this.assertCurrent()