Last active
September 4, 2024 14:02
-
-
Save kevinswiber/f6d49d591dcf425ba586ca86a97a7d20 to your computer and use it in GitHub Desktop.
Node.js server with best-effort completions on functions
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { AsyncResource } from "node:async_hooks"; | |
import { createServer, request } from "node:http"; | |
// A thin wrapper around AsyncResource to manage best-effort completions. | |
// Best-effort resources are not guaranteed to complete, but they are | |
// allowed time to complete before the process exits on SIGINT. | |
// In a real-world scenario, you would likely include more details | |
// in the resource context to help trace completed and | |
// abandoned best-effort executions. | |
class BestEffort extends AsyncResource { | |
constructor(globalState) { | |
super("BestEffort"); | |
this.globalState = globalState; | |
} | |
bind(fn, thisArg) { | |
this.globalState.set(this.asyncId(), this); | |
return super.bind(fn, thisArg); | |
} | |
runInAsyncScope(fn, thisArg, ...args) { | |
this.globalState.set(this.asyncId(), this); | |
return super.runInAsyncScope(fn, thisArg, ...args); | |
} | |
complete() { | |
this.globalState.delete(this.asyncId()); | |
this.emitDestroy(); | |
} | |
} | |
const bestEffortResources = new Map(); | |
const timeoutMilliseconds = [1_000, 10_000, 30_000, 50_000, 100_000]; | |
let timeoutIndex = 0; | |
const server = createServer((_req, res) => { | |
// Create a new BestEffort resource for each execution scope. | |
const bestEffort = new BestEffort(bestEffortResources); | |
const ms = timeoutMilliseconds[timeoutIndex++ % timeoutMilliseconds.length]; | |
console.log( | |
"Setting best-effort function:", | |
bestEffort.asyncId(), | |
"with timeout:", | |
ms, | |
); | |
res.on( | |
"finish", | |
// Bind the event listener to execute in the best-effort's scope. | |
bestEffort.bind(() => { | |
setTimeout(() => { | |
console.log("Executing best-effort function:", bestEffort.asyncId()); | |
// Signal completion of the best-effort resource. | |
// If using try/catch, be sure to call complete() in the finally block. | |
bestEffort.complete(); | |
}, ms); | |
}), | |
); | |
res.end(bestEffort.asyncId().toString()); | |
}); | |
const port = process.env.PORT || 3000; | |
server.listen(port, () => { | |
console.log("Listening: ", server.address()); | |
}); | |
function forceExit(code) { | |
server.closeAllConnections(); | |
// Force complete all best-effort resources. Optional. | |
if (bestEffortResources.size > 0) { | |
for (const [id, bestEffort] of bestEffortResources.entries()) { | |
console.log("Dropping best-effort function without completion:", id); | |
bestEffort.complete(); | |
} | |
} | |
console.log("Exiting..."); | |
process.exit(code); | |
} | |
const successExitCode = 0; | |
const sigIntExitCode = 1; | |
let sigIntCount = 0; | |
process.on("SIGINT", () => { | |
sigIntCount++; | |
if (sigIntCount > 1) { | |
console.log("Received SIGINT (Ctrl-C) twice, exiting..."); | |
process.exit(sigIntExitCode); | |
return; | |
} | |
server.close(); | |
// No best-effort resources, so exit immediately. | |
if (bestEffortResources.size === 0) { | |
forceExit(successExitCode); | |
return; | |
} | |
// Set a failsafe to force exit after 60 seconds. | |
const failsafe = setTimeout(() => forceExit(sigIntExitCode), 60_000); | |
// Monitor best-effort resources waiting to complete. | |
const monitor = setInterval(() => { | |
const size = bestEffortResources.size; | |
console.log( | |
"Waiting to complete best-effort functions:", | |
Array.from(bestEffortResources.keys()), | |
); | |
if (size === 0) { | |
clearTimeout(failsafe); | |
clearInterval(monitor); | |
forceExit(successExitCode); | |
} | |
}, 1_000); | |
}); | |
// Send a few requests to the server. | |
for (let i = 0, max = 5; i < max; i++) { | |
request({ port }, () => { | |
if (i === max - 1) { | |
console.log("Press Ctrl+C to exit."); | |
} | |
}).end(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment