46 lines
1.3 KiB
TypeScript
46 lines
1.3 KiB
TypeScript
import { setTimeout as sleep } from 'node:timers/promises'
|
|
|
|
/** After a signal, give cleanup this long before exiting anyway. */
|
|
const FORCE_EXIT_MS = 25_000
|
|
|
|
/**
|
|
* Graceful shutdown for the long-running commands. SIGTERM (docker stop) and
|
|
* SIGINT wake up `wait()` and `sleep()`, so workers can finish their active
|
|
* jobs and close their connections before the process exits.
|
|
*/
|
|
class Shutdown {
|
|
private readonly controller = new AbortController()
|
|
|
|
constructor() {
|
|
for (const signal of ['SIGTERM', 'SIGINT'] as const) {
|
|
process.once(signal, () => {
|
|
this.controller.abort()
|
|
setTimeout(() => process.exit(0), FORCE_EXIT_MS).unref()
|
|
})
|
|
}
|
|
}
|
|
|
|
get requested(): boolean {
|
|
return this.controller.signal.aborted
|
|
}
|
|
|
|
/** Resolves when a shutdown signal arrives. */
|
|
wait(): Promise<void> {
|
|
if (this.requested) return Promise.resolve()
|
|
return new Promise((resolve) =>
|
|
this.controller.signal.addEventListener('abort', () => resolve(), { once: true })
|
|
)
|
|
}
|
|
|
|
/** Sleeps, but returns early on shutdown. */
|
|
async sleep(ms: number): Promise<void> {
|
|
try {
|
|
await sleep(ms, undefined, { signal: this.controller.signal })
|
|
} catch {
|
|
// aborted by a shutdown signal
|
|
}
|
|
}
|
|
}
|
|
|
|
export default new Shutdown()
|