mirror of
https://github.com/stablyai/orca.git
synced 2026-09-21 16:02:20 +00:00
fix(browser): log CDP outbound overflow before terminating the client
The outbound queue terminated the automation client silently on overflow, so the client saw a socket close indistinguishable from a crash. Surface the cap that tripped and the backlog held when it did. The queue dropped its backlog before invoking onOverflow, so the counters were already zero at the callback. Snapshot them first and pass them through.
This commit is contained in:
@@ -34,7 +34,17 @@ export class CdpClientResponseWriter {
|
||||
byteLengthOf: (frame) => Buffer.byteLength(frame),
|
||||
getBufferedAmount: () => client.bufferedAmount,
|
||||
isWritable: () => client.readyState === WebSocket.OPEN,
|
||||
onOverflow: () => client.terminate(),
|
||||
onOverflow: (evidence) => {
|
||||
// The client only sees an abrupt socket close, so name the cap here.
|
||||
console.warn('[cdp] outbound queue overflow; terminating automation client:', {
|
||||
cap: evidence.cap,
|
||||
queuedBytes: evidence.queuedBytes,
|
||||
queuedFrames: evidence.queuedFrames,
|
||||
maxQueuedBytes: evidence.maxQueuedBytes,
|
||||
maxQueuedFrames: evidence.maxQueuedFrames
|
||||
})
|
||||
client.terminate()
|
||||
},
|
||||
// Preserve large PDF/screenshot replies on a draining connection; queued bursts stay capped.
|
||||
maxFrameBytes: Number.POSITIVE_INFINITY,
|
||||
maxDrainFramesPerTurn: 128
|
||||
|
||||
@@ -10,6 +10,16 @@
|
||||
// Generic over the frame type so it serves both the text reply path (encrypted
|
||||
// base64 strings) and the binary send path (Uint8Array frames).
|
||||
|
||||
/** Which hard bound tripped, plus the backlog held when it did. */
|
||||
export type WsOutboundOverflowEvidence = {
|
||||
cap: 'maxQueuedBytes' | 'maxQueuedFrames' | 'maxFrameBytes' | 'claimQueuedBytes' | 'sendFailed'
|
||||
queuedBytes: number
|
||||
queuedFrames: number
|
||||
maxQueuedBytes: number
|
||||
maxQueuedFrames: number
|
||||
maxFrameBytes: number
|
||||
}
|
||||
|
||||
export type WsOutboundBackpressureQueueOptions<TFrame> = {
|
||||
/** Send a frame on the wire. Called only when under the soft cap. */
|
||||
send: (frame: TFrame) => void
|
||||
@@ -25,8 +35,9 @@ export type WsOutboundBackpressureQueueOptions<TFrame> = {
|
||||
* Called once when queued bytes exceed maxQueuedBytes — the link is wedged.
|
||||
* The caller should tear the connection down so a fresh subscription can
|
||||
* replay an authoritative snapshot. The queue drops its backlog afterward.
|
||||
* Receives the backlog measured before that drop, so callers can report it.
|
||||
*/
|
||||
onOverflow: () => void
|
||||
onOverflow: (evidence: WsOutboundOverflowEvidence) => void
|
||||
/** Soft cap: stop draining onto the wire while bufferedAmount is above this. */
|
||||
softCapBytes?: number
|
||||
/** Hard cap on bytes held in this queue before onOverflow fires. */
|
||||
@@ -135,13 +146,22 @@ export function createWsOutboundBackpressureQueue<TFrame>(
|
||||
stopTimer()
|
||||
}
|
||||
|
||||
const failOverflow = (): void => {
|
||||
const failOverflow = (cap: WsOutboundOverflowEvidence['cap']): void => {
|
||||
if (disposed || overflowed) {
|
||||
return
|
||||
}
|
||||
overflowed = true
|
||||
// Snapshot before dropBacklog() zeroes the counters.
|
||||
const evidence: WsOutboundOverflowEvidence = {
|
||||
cap,
|
||||
queuedBytes: queued,
|
||||
queuedFrames,
|
||||
maxQueuedBytes,
|
||||
maxQueuedFrames,
|
||||
maxFrameBytes
|
||||
}
|
||||
dropBacklog()
|
||||
options.onOverflow()
|
||||
options.onOverflow(evidence)
|
||||
}
|
||||
|
||||
const sendFrame = (frame: TFrame): boolean => {
|
||||
@@ -149,7 +169,7 @@ export function createWsOutboundBackpressureQueue<TFrame>(
|
||||
options.send(frame)
|
||||
return true
|
||||
} catch {
|
||||
failOverflow()
|
||||
failOverflow('sendFailed')
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -243,7 +263,7 @@ export function createWsOutboundBackpressureQueue<TFrame>(
|
||||
}
|
||||
const bytes = options.byteLengthOf(frame)
|
||||
if (!Number.isFinite(bytes) || bytes < 0 || bytes > maxFrameBytes) {
|
||||
failOverflow()
|
||||
failOverflow('maxFrameBytes')
|
||||
return { accepted: false, queued: false, cancel: () => false }
|
||||
}
|
||||
// Fast path: nothing parked and the wire is under the cap — send directly.
|
||||
@@ -261,7 +281,7 @@ export function createWsOutboundBackpressureQueue<TFrame>(
|
||||
}
|
||||
const queuedBytesClaim = options.claimQueuedBytes?.(bytes)
|
||||
if (options.claimQueuedBytes && !queuedBytesClaim) {
|
||||
failOverflow()
|
||||
failOverflow('claimQueuedBytes')
|
||||
return { accepted: false, queued: false, cancel: () => false }
|
||||
}
|
||||
const entry: QueueEntry = {
|
||||
@@ -274,7 +294,7 @@ export function createWsOutboundBackpressureQueue<TFrame>(
|
||||
queued += bytes
|
||||
queuedFrames += 1
|
||||
if (queued > maxQueuedBytes || queuedFrames > maxQueuedFrames) {
|
||||
failOverflow()
|
||||
failOverflow(queued > maxQueuedBytes ? 'maxQueuedBytes' : 'maxQueuedFrames')
|
||||
return { accepted: false, queued: false, cancel: () => false }
|
||||
}
|
||||
if (timer === null) {
|
||||
|
||||
Reference in New Issue
Block a user