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>
This commit is contained in:
parent
3f8654c26e
commit
5390224bf7
|
|
@ -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<typeof globalThis.fetch>(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<typeof globalThis.fetch>()
|
||||
.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<typeof globalThis.fetch>(
|
||||
async (_url, init) =>
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ export async function requestRelayAssignment(input: {
|
|||
directorUrl: string
|
||||
relayToken: string
|
||||
relayHostId: string
|
||||
reconnect?: boolean
|
||||
fetch?: typeof globalThis.fetch
|
||||
requestDeadlineMs?: number
|
||||
}): Promise<RelayAssignment> {
|
||||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in New Issue