Skip to content

Instantly share code, notes, and snippets.

@kevinswiber
Last active September 4, 2024 14:02
Show Gist options
  • Save kevinswiber/f6d49d591dcf425ba586ca86a97a7d20 to your computer and use it in GitHub Desktop.
Save kevinswiber/f6d49d591dcf425ba586ca86a97a7d20 to your computer and use it in GitHub Desktop.
Node.js server with best-effort completions on functions
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