Skip to content

Instantly share code, notes, and snippets.

@zemccartney
Last active September 19, 2019 12:52
Show Gist options
  • Save zemccartney/4dd02ce584b444b043b82bbc3b5964bc to your computer and use it in GitHub Desktop.
Save zemccartney/4dd02ce584b444b043b82bbc3b5964bc to your computer and use it in GitHub Desktop.
hapi crash-only
'use strict';
/*
Inspired by ideas in:
- https://www.joyent.com/node-js/production/design/errors
- https://www.usenix.org/legacy/events/hotos03/tech/full_papers/candea/candea.pdf
*/
const Boom = require('@hapi/boom');
// https://github.com/kanongil/exiting
// Not strictly necessary, but used
// 1.) to try out what looks like an interesting tool
// 2.) abstracts away the variety of exit triggers, hopefully simplifies graceful shutdown
const Exiting = require('exiting');
const Glue = require('@hapi/glue');
const Manifest = require('./manifest');
exports.deployment = async (start) => {
const manifest = Manifest.get('/');
const server = await Glue.compose(manifest, { relativeTo: __dirname });
await server.initialize();
if (!start) {
return server;
}
server.events.on('stop', () => {
console.log('Server stopped.');
});
const manager = Exiting.createManager(server);
server.ext('onPreResponse', async (request, h) => {
// We're trying to express: if the current response represents
// an encountered bug, then (gracefully) shutdown the server
// In production, we'd rely on pm2 (or some other tooling)
// to reboot the server
// We crash to:
// - guarantee that any hanging/leaked resources stemming from the bug
// are closed
// - hopefully simplify server's fault model to simplify debugging:
// -- crashing = bug (programmer's responsibility to fix)
// -- responding = success or operational error
// no ambiguity in channels; unrecoverable errors never sent back to the requesting client
// crashing hopefully provides a simpler hook to capture and somehow report
// bugs to maintainers
if (request.response.isBoom && request.response.isDeveloperError) {
console.error('Programmer Error detected; crashing server');
console.error(request.response);
await manager.stop();
// TODO What does this mean for the web client?
// https://www.tjvantoll.com/2015/09/13/fetch-and-errors/
// At least for fetch, would get to catch block
// Not sure about axios or others
}
// TODO If some sort of 5xx error, configure Retry-After headers?
return h.continue;
});
server.route({
method: 'get',
path: '/test/{trigger?}',
options: {
handler: (request, h) => {
const { trigger } = request.params;
switch (trigger) {
case 'throw':
console.log('throwing will not crash; desired behavior');
// hapi treats these as plain errors (just calls boomify)
// I assume so hapi doesn't assume anything about user's error handling
// conventions; user might intend to throw Errors i.e. not use Boom
// see: https://github.com/hapijs/hapi/blob/7a5d2820ccf7f2b82785982cb35a5d0de9a5831e/lib/toolkit.js#L47-L57
throw new Error('regular throw');
case 'boom':
console.log('regular boom error will not crash; desired behavior');
// returning or throwing yield same result
throw Boom.notFound('Not found');
case 'internal':
console.log('standard internal error will not crash; ambiguous?');
// ambiguous b/c Boom.internal does NOT set isDeveloperError,
// even though Boom docs refer to internal as an alias of
// badImplementation, which does set err.isDeveloperError
// Why?
throw Boom.internal('BAD!');
case 'impl':
console.log('standard bad implementation error will crash; desired behavior');
throw Boom.badImplementation('');
case 'why':
console.log('throwing nonerror will crash the server');
// https://github.com/hapijs/hapi/blob/7a5d2820ccf7f2b82785982cb35a5d0de9a5831e/lib/toolkit.js#L51-L52
// Illustrates that programmer errors can be system errors (as defined by Boom: https://github.com/hapijs/bounce#issystemerrs)
// or misuse of hapi; either case represents an unrecoverable error (bug) caused by the programmer
throw 'thx cinco';
case 'bug':
console.log('programmer error will crash; desired behavior');
return request.duggs.nuggs;
default:
return 'success';
}
}
}
});
//await server.start();
await manager.start();
console.log(`Server started at ${server.info.uri}`);
//return server;
return manager;
};
if (!module.parent) {
exports.deployment(true);
/*
Commented out b/c Exiting sets its own unhandledRejection handler
process.on('unhandledRejection', (err) => {
throw err;
});
*/
}
@zemccartney
Copy link
Author

zemccartney commented Sep 18, 2019

Issues not addressed here:

  • Recovery: is relying on a tool like PM2 enough to reboot the server in an acceptable state? Any issues with that?
  • Session / volatile state: if one client crashes the server, then other clients' sessions could be disrupted if they depended on some sort of session state e.g. authentication stored in cookies, memory, or some other other temporary store.
  • How do web clients handle server downtime?
  • Performance: what does the possibility of rebooting mean for the application's availability and performance?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment