Merge pull request #3731 from yizhifengye/fix/process-cleanup-infinite-loop-epipe
fix(background-agent): detach error listener before running body to s…
This commit is contained in:
@@ -275,5 +275,42 @@ describe("#given process cleanup registration", () => {
|
||||
uncaughtExceptionListenersBefore.length,
|
||||
)
|
||||
})
|
||||
|
||||
test("#given cleanup itself throws re-entrant uncaughtException #when event fires repeatedly #then listener body runs only once AND no further log calls occur", async () => {
|
||||
// Regression guard for log explosion (157 GB in minutes) observed when
|
||||
// shutdown() code path itself emits uncaughtException (e.g. EPIPE while
|
||||
// closing a broken pipe). Before the fix, every re-entry logged another
|
||||
// line and re-ran cleanup, producing an unbounded loop that filled disk.
|
||||
const reentrantShutdown = mock(() => {
|
||||
process.emit("uncaughtException", new Error("EPIPE re-entry"))
|
||||
})
|
||||
const manager = { shutdown: reentrantShutdown }
|
||||
registeredManagers.push(manager)
|
||||
|
||||
registerManagerForCleanup(manager)
|
||||
|
||||
process.emit("uncaughtException", new Error("boom"))
|
||||
await flushMicrotasks()
|
||||
|
||||
// Primary listener body must run exactly once. Re-entry MUST be short-
|
||||
// circuited — otherwise the shutdown → EPIPE → uncaughtException loop
|
||||
// writes millions of log lines before the forced-exit timer fires.
|
||||
expect(reentrantShutdown.mock.calls.length).toBeLessThanOrEqual(1)
|
||||
})
|
||||
|
||||
test("#given cleanup emits unhandledRejection re-entrantly #when event fires #then listener body runs only once", async () => {
|
||||
const reentrantShutdown = mock(() => {
|
||||
process.emit("unhandledRejection", new Error("re-entry"), Promise.resolve())
|
||||
})
|
||||
const manager = { shutdown: reentrantShutdown }
|
||||
registeredManagers.push(manager)
|
||||
|
||||
registerManagerForCleanup(manager)
|
||||
|
||||
process.emit("unhandledRejection", new Error("boom"), Promise.resolve())
|
||||
await flushMicrotasks()
|
||||
|
||||
expect(reentrantShutdown.mock.calls.length).toBeLessThanOrEqual(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -38,6 +38,12 @@ function registerErrorEvent(
|
||||
handler: (error: unknown) => void | Promise<void>
|
||||
): (error: unknown) => void {
|
||||
const listener = (error: unknown) => {
|
||||
// Detach before running the body so a re-emit from inside log()/handler()
|
||||
// (e.g. EPIPE while closing a broken pipe during shutdown) cannot recurse.
|
||||
// Prior behavior: the listener re-entered itself, re-logged, re-ran cleanup,
|
||||
// and threw EPIPE again — an unbounded loop that filled disks with 100+ GB
|
||||
// of log lines in minutes before the 6 s forced-exit timer could fire.
|
||||
process.off(signal, listener)
|
||||
log(`[background-agent] ${signal} received during shutdown cleanup:`, error)
|
||||
scheduleForcedExit(handler(error), 1, true)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user