fix(orchestration): treat ask as a long-poll so it survives the 30s socket idle wall (#9351)

orchestration.ask blocks server-side until a reply lands or its timeout
(default 600s) elapses, holding the RPC open — but isLongPollRequest never
classified it as a long-poll. So the keepalive that resets the 30s
RUNTIME_RPC_SOCKET_IDLE_TIMEOUT_MS was never armed, and any ask left
unanswered for 30s died with a misleading runtime_unavailable ("The Orca
runtime closed the connection"), regardless of --timeout-ms. The same
omission left the handler's abort signal unwired (it is only passed for
long-polls), so the client-disconnect release path guarded by
signal?.aborted was dead code.

Add orchestration.ask to isLongPollRequest so it gets the keepalive, the
abort signal, and long-poll admission. The client already extends its
per-call socket timeout for ask (handlers/orchestration.ts passes
timeoutMs + 5s at the call site), so only the server-side classification
was missing.
This commit is contained in:
Avery Bloom 2026-07-24 15:25:53 +08:00 committed by GitHub
parent 143d2232bb
commit cd28da13f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 53 additions and 0 deletions

View File

@ -3909,6 +3909,51 @@ describe('OrcaRuntimeRpcServer', () => {
}
})
it('emits keepalive frames while orchestration.ask blocks for a reply', async () => {
const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-'))
const runtime = new OrcaRuntimeService()
const db = new OrchestrationDb(':memory:')
runtime.setOrchestrationDb(db)
const server = new OrcaRuntimeRpcServer({
runtime,
userDataPath,
keepaliveIntervalMs: 50
})
await server.start()
try {
const metadata = readRuntimeMetadata(userDataPath)
// Why: no reply is ever sent, so ask blocks the full window on the same
// hold-the-socket path check --wait uses. Without ask in the long-poll
// set the 30s idle timer would tear this down before it keepalives.
const session = openFramedSession(metadata!.transports[0]!.endpoint, {
id: 'req_ask',
authToken: metadata!.authToken,
method: 'orchestration.ask',
params: {
to: 'term_nobody',
from: 'term_asker',
question: 'ping?',
timeoutMs: 300
}
})
await session.done
const keepalives = session.frames.filter((f) => f._keepalive === true)
const terminals = session.frames.filter((f) => f.ok !== undefined)
expect(terminals).toHaveLength(1)
expect(terminals[0]).toMatchObject({
id: 'req_ask',
ok: true,
result: { timedOut: true }
})
expect(keepalives.length).toBeGreaterThanOrEqual(3)
} finally {
db.close()
await server.stop()
}
})
it('emits keepalive frames while terminal.wait blocks and returns its structured timeout', async () => {
const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-'))
const runtime = new OrcaRuntimeService()

View File

@ -397,6 +397,14 @@ function isLongPollRequest(request: RpcRequest): boolean {
if (request.method === 'terminal.wait') {
return true
}
// Why: orchestration.ask blocks unconditionally (default 600 s) holding the
// RPC open until a reply lands or the deadline passes, so it needs the same
// keepalive as check --wait or the 30 s socket idle timer tears it down. It
// also relies on the abort signal (only wired for long-polls) to release the
// waiter when the asking client disconnects.
if (request.method === 'orchestration.ask') {
return true
}
if (request.method === 'orchestration.check') {
const params = request.params as { wait?: unknown } | undefined
return params?.wait === true