Skip to content

Instantly share code, notes, and snippets.

@bmeck
Last active December 21, 2021 15:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bmeck/fbcf80b2dbd1a0639ba53d6e75923e02 to your computer and use it in GitHub Desktop.
Save bmeck/fbcf80b2dbd1a0639ba53d6e75923e02 to your computer and use it in GitHub Desktop.
// node [--trace-gc] local-storage-exmaple.cjs
// creates a server
// will drop /favicon.ico connections poorly without properly closing them
// `cleanupContext` AsyncLocalStorage + `cleanup` FinalizationRegistry will clean up
// will track the http request URL for errors in httpContext
// will show up in error generated for bad permissions
// will grant permissions in permissionsContext based upon query params / search params
// need ?fs=true to make / respond with a 200
//
// goto /?fs=true in browser to see it work
// goto /?fs=false in browser to see it error
// browsers will automatically request /favicon.ico
'use strict';
const { AsyncLocalStorage } = require('async_hooks');
const http = require('http');
const events = require('events');
const { readFile } = require('fs/promises');
// tracking what permissions we have
const permissionsContext = new AsyncLocalStorage();
// tracking which HTTP context we are in
const httpContext = new AsyncLocalStorage();
const getSearchParams = (reqUrl) => {
const url = new URL(reqUrl, 'http://invalid.invalid/');
return url.searchParams;
}
/**
* FOR DEMO PURPOSES ONLY, NEVER DO THIS
* Use real auth methods and not search params
* @param req
*/
async function auth(req, fn) {
// NO NO NO NO NO
let permissions = {};
for (const [key, value] of getSearchParams(req.url).entries()) {
permissions[key] = value;
}
permissionsContext.run(permissions, fn);
}
function hasPermission(key) {
return permissionsContext.getStore()?.[key] === 'true';
}
/**
* needs ?fs=true
* @param filepath
* @returns
*/
function gatedReadFile(filepath) {
// has no reference to how auth is obtained/propagated
if (!hasPermission('fs')) {
// has no reference to the request / no need to propagate it
throw new Error('403');
}
return readFile(filepath, 'utf8');
}
process.on('uncaughtExceptionMonitor', function (err) {
let reqUrl = httpContext.getStore();
if (reqUrl) {
console.error('Error from URL %s', reqUrl);
}
});
const cleanup = new FinalizationRegistry(([req, res, id, cleanup]) => {
console.log('closing dropped connection to ', req.url, 'with cleanup handler id', id);
cleanup();
try {
res.writeHead(500);
res.end();
} catch {
// this is normal
}
});
// Garbage generator to keep GC running periodically
setInterval(() => [].concat(1), 0);
setInterval(() => [].concat(1), 0);
setInterval(() => [].concat(1), 0);
setInterval(() => [].concat(1), 0);
let gcId = 1;
const cleanupContext = new AsyncLocalStorage();
/**
* @param {import('http').IncomingMessage} req
* @param {import('http').ServerResponse} res
* @returns
*/
function setCleanup(req, res, fn) {
let id = gcId++;
// {id} is our "token"
// don't directly hold onto {id}, it won't be able to GC then
let GC = new WeakRef({id});
// data to associate with the token
let held = [req, res, id, () => {
res.off('finish', unregister);
}];
console.log(req.url, 'assigned cleanup handler', id);
function unregister() {
console.log(req.url, 'unregistered cleanup handler', id);
cleanup.unregister(GC.deref());
}
// setup when the token deallocs, fire handler
cleanup.register(GC.deref(), held, GC.deref());
res.on('finish', unregister);
// Force the token to stay alive for all async work spawned from the current task
cleanupContext.run(GC.deref(), fn);
}
/**
* @param {import('http').IncomingMessage} req
* @param {import('http').ServerResponse} res
* @returns
*/
function handleHTTP(req, res) {
setCleanup(req, res, () => {
if (req.url === '/favicon.ico') {
// uh oh, forgot to close (don't worry the Finalization Registry handles it)
setTimeout(() => {
// keep the req, res alive WAAAAY too long
// should see a GC entry before this
console.log('can cleanup /favicon.ico')
}, 0);
return;
}
httpContext.run(req.url, () => {
auth(req, () => {
console.log('%s %s', req.method, req.url);
// "simulate" a work queue
setTimeout(doWork, 1, res);
});
});
});
}
async function doWork(res) {
res.end(await gatedReadFile(__filename));
}
async function main() {
const server = http.createServer(handleHTTP).listen(process.env.PORT || 0)
await events.once(server, 'listening');
console.log('Listening on', server.address());
console.log(`http://127.0.0.1:${server.address().port}`)
}
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment