KEMBAR78
Domains! | PPTX
Domains!
BY @DOMENIC
Domenic Denicola
• http://domenic.me
• https://github.com/domenic
• https://npmjs.org/~domenic
• http://slideshare.net/domenicdenicola
Things I’m doing:
• @esdiscuss onTwitter
• The Promises/A+ spec
• Client-Side Packages talk
@DOMENIC
Let’s talk about errors in
@DOMENIC
• Errors passed to a callback: beAsync(function (err, result) { … })
• Errors emitted from an event emitter: emitter.emit('error', err)
• Errors thrown “intentionally”: throw err
• Errors thrown “accidentally”: JSON.parse(badUserInput)
@DOMENIC
IF AN ERROR IS THROWN,
BUT NOBODY CATCHES IT,
DOES IT MAKE A SOUND?
Answer: yes!!
@DOMENIC
• Errors passed to a callback: beAsync(function (err, result) { … })
• Errors emitted from an event emitter: emitter.emit('error', err)
• Errors thrown “intentionally”: throw err
• Errors thrown “accidentally”: JSON.parse(badUserInput)
SERVER GO CRASH
DEMOTIME
@DOMENIC
https://github.com/domenic/domains-romance/blob/master/server.js
Before domains: process.on('uncaughtException', …)
• Instead of crashing, you could listen for “about to crash,” and do
something about it.
• But: how do you keep track of what caused the crash?
• Concretely: how can you return a 500 error to the right request?
• Domains are supposed to help with this… somehow.
@DOMENIC
Build your own domains: uncaught throws
• Before initiating some asynchronous work:
• process.pseudoDomain = new EventEmitter();
• After initiating the async work:
• process.pseudoDomain = null;
• Inside your 'uncaughtException' handler:
• if (process.pseudoDomain) process.pseudoDomain.emit('error', err);
• Now, in the pseudo-domain’s 'error' handler, you know the cause!
@DOMENIC
Build your own domains: unheard 'error's
• When creating an event emitter:
• ee.pseudoDomain = new EventEmitter();
• ee.on('error', function (err) {
if (EventEmitter.listenerCount(this, 'error') === 1) {
this.pseudoDomain.emit('error', err);
}
});
• Now, in the pseudo-domain’s 'error' handler, you know the cause!
@DOMENIC
Build your own domains: callback err params
• function bindToDomain(domain, cb) {
return function (err, result) {
if (err) return domain.emit('error', err);
cb(null, result);
};
}
• Every time you use a callback:
• var pseudoDomain = new EventEmitter();
• doAsyncThing(bindToDomain(pseudoDomain, function (result) { … });
• Now, in the pseudo-domain’s 'error' handler, you know the cause!
@DOMENIC
The key feature: automatic aggregation
• This is pretty useless if you have to create a domain for every async
operation.You might as well keep track of things yourself.
• But what if we assigned a single domain to a whole bunch of async
operations?
• Say, every async operation involved in a single HTTP
request/response cycle?
• What if we had hooks into every asynchronous function and every
event emitter in Node.js, to automatically associate with an
“active” domain?
@DOMENIC
DEMOTIME
@DOMENIC
https://github.com/domenic/domains-romance/blob/master/serverWithDomains.js
But … how!?
To understand how domains hook in to all that, we’re going
to need to take a journey into the heart of Node.js.
@DOMENIC
MakeCallback
node.cc line 1001:
Handle<Value> MakeCallback(const Handle<Object> object,
const Handle<Function> callback,
int argc, Handle<value> argv[]) {
⋮
TryCatch try_catch;
Local<Value> ret = callback->Call(object, argc, argv);
if (try_catch.HasCaught()) {
FatalException(try_catch);
⋮
@DOMENIC
Domain basics
• var d = require('domain').create();
• There’s a globally active current domain:
• process.domain === require('domain').active
• (There’s actually a stack, so you can recursively enter domains, but whatevs.)
• d.enter() makes d the current domain.
• d.exit() makes d inactive.
• d.run(func) is shorthand for d.enter(); func(); d.exit().
@DOMENIC
Effects of having an active domain
• MakeCallback is most directly influenced
• process.domain.enter() and process.domain.exit() wrap the previous code
• setImmediate, setTimeout, setInterval, process.nextTick
• record the active domain and attach it to the callback (1, 2, 3, 4)
• when the callback is triggered, wrap it with enter() and exit() (1, 2/3, 4)
• new EventEmitter()
• records the active domain and attach it to the emitter
• when any events are emitted, wraps with enter() and exit()
• when an 'error' event is emitted and not handled, gives it to the domain
@DOMENIC
(mmm, yummy global state…)
Uncaught exceptions
• Remember FatalException(try_catch)?
• That calls from C++ to process._fatalException (node.js line 222)
• Much like in our pseudo-domains, it hands them off to the active
domain’s 'error' handler.
• Thus all the enter()/exit() wrapping was just establishing context for this
moment: deciding which domain gets the errors.
• If there is an active domain, this behavior replaces
'uncaughtException'.
@DOMENIC
That’s cool. Now what?
We know how domains work.
But do we truly know how to use them?
@DOMENIC
Stability 2: Unstable
“TheAPI is in the process of settling, but has not yet had sufficient
real-world testing to be considered stable. Backwards-compatibility
will be maintained if reasonable.” (source)
• Don’t use d.dispose().
@DOMENIC
APIs to know
• domain.create(), obviously
• d.run(), to enter/exit the domain
• d.add(), for adding event emitters created before the domain
existed into the domain.
• d.bind(), for manually wiring up a callback to a domain.
• d.intercept(), that callback helper from before.
@DOMENIC
EventEmitter pitfalls
• Event emitters are bound to a domain on creation. But sometimes
event emitters stick around for a long time.
• Any callbacks given to the event emitter “escape” the active
domain, which is replaced by whatever the active domain was
when the event emitter was created.
• Generally, anything involving a persistent connection or
connection pool will be in trouble, and not able to associate errors
with the currently-active domain.
• https://github.com/domenic/domains-tragedy
@DOMENIC
Error recovery
• Domains don’t give you try/catch back.
• By the time you see an error on the domain, it escaped any close-
to-the-source error handling; nobody handled it explicitly.
• You’re probably in an inconsistent state, halfway through a
transaction or with file handles open or who knows what.
• The recommendation is to gracefully return a 500, then shut down
your worker and restart while others in the cluster take over.
@DOMENIC
Versus promises
• Domains have grown … organically.
• Promises attempt to solve error handling from first principles, by
giving you back return and throw semantics.
• In practice, this means wrapping every callback in try/catch, much
like MakeCallback, but less pervasively and more intelligently.
• But, they won’t save you completely in Node.js:
• 'error' events are still fatal
• If you step outside of promise-land, e.g. setTimeout, uncaught exceptions
are again dangerous.
@DOMENIC
IN CONCLUSION
• Domains work pretty well to tame
async errors in Node.js.
• They do so by hooking into Node.js at
the deepest level.
• They don’t work perfectly, and they
don’t absolve you of cleanup.
• But you should be able to get some
solid wins with minimal extra code.
@DOMENIC

Domains!

  • 1.
  • 2.
    Domenic Denicola • http://domenic.me •https://github.com/domenic • https://npmjs.org/~domenic • http://slideshare.net/domenicdenicola Things I’m doing: • @esdiscuss onTwitter • The Promises/A+ spec • Client-Side Packages talk @DOMENIC
  • 3.
    Let’s talk abouterrors in @DOMENIC • Errors passed to a callback: beAsync(function (err, result) { … }) • Errors emitted from an event emitter: emitter.emit('error', err) • Errors thrown “intentionally”: throw err • Errors thrown “accidentally”: JSON.parse(badUserInput)
  • 4.
    @DOMENIC IF AN ERRORIS THROWN, BUT NOBODY CATCHES IT, DOES IT MAKE A SOUND?
  • 5.
    Answer: yes!! @DOMENIC • Errorspassed to a callback: beAsync(function (err, result) { … }) • Errors emitted from an event emitter: emitter.emit('error', err) • Errors thrown “intentionally”: throw err • Errors thrown “accidentally”: JSON.parse(badUserInput) SERVER GO CRASH
  • 6.
  • 7.
    Before domains: process.on('uncaughtException',…) • Instead of crashing, you could listen for “about to crash,” and do something about it. • But: how do you keep track of what caused the crash? • Concretely: how can you return a 500 error to the right request? • Domains are supposed to help with this… somehow. @DOMENIC
  • 8.
    Build your owndomains: uncaught throws • Before initiating some asynchronous work: • process.pseudoDomain = new EventEmitter(); • After initiating the async work: • process.pseudoDomain = null; • Inside your 'uncaughtException' handler: • if (process.pseudoDomain) process.pseudoDomain.emit('error', err); • Now, in the pseudo-domain’s 'error' handler, you know the cause! @DOMENIC
  • 9.
    Build your owndomains: unheard 'error's • When creating an event emitter: • ee.pseudoDomain = new EventEmitter(); • ee.on('error', function (err) { if (EventEmitter.listenerCount(this, 'error') === 1) { this.pseudoDomain.emit('error', err); } }); • Now, in the pseudo-domain’s 'error' handler, you know the cause! @DOMENIC
  • 10.
    Build your owndomains: callback err params • function bindToDomain(domain, cb) { return function (err, result) { if (err) return domain.emit('error', err); cb(null, result); }; } • Every time you use a callback: • var pseudoDomain = new EventEmitter(); • doAsyncThing(bindToDomain(pseudoDomain, function (result) { … }); • Now, in the pseudo-domain’s 'error' handler, you know the cause! @DOMENIC
  • 11.
    The key feature:automatic aggregation • This is pretty useless if you have to create a domain for every async operation.You might as well keep track of things yourself. • But what if we assigned a single domain to a whole bunch of async operations? • Say, every async operation involved in a single HTTP request/response cycle? • What if we had hooks into every asynchronous function and every event emitter in Node.js, to automatically associate with an “active” domain? @DOMENIC
  • 12.
  • 13.
    But … how!? Tounderstand how domains hook in to all that, we’re going to need to take a journey into the heart of Node.js. @DOMENIC
  • 14.
    MakeCallback node.cc line 1001: Handle<Value>MakeCallback(const Handle<Object> object, const Handle<Function> callback, int argc, Handle<value> argv[]) { ⋮ TryCatch try_catch; Local<Value> ret = callback->Call(object, argc, argv); if (try_catch.HasCaught()) { FatalException(try_catch); ⋮ @DOMENIC
  • 15.
    Domain basics • vard = require('domain').create(); • There’s a globally active current domain: • process.domain === require('domain').active • (There’s actually a stack, so you can recursively enter domains, but whatevs.) • d.enter() makes d the current domain. • d.exit() makes d inactive. • d.run(func) is shorthand for d.enter(); func(); d.exit(). @DOMENIC
  • 16.
    Effects of havingan active domain • MakeCallback is most directly influenced • process.domain.enter() and process.domain.exit() wrap the previous code • setImmediate, setTimeout, setInterval, process.nextTick • record the active domain and attach it to the callback (1, 2, 3, 4) • when the callback is triggered, wrap it with enter() and exit() (1, 2/3, 4) • new EventEmitter() • records the active domain and attach it to the emitter • when any events are emitted, wraps with enter() and exit() • when an 'error' event is emitted and not handled, gives it to the domain @DOMENIC (mmm, yummy global state…)
  • 17.
    Uncaught exceptions • RememberFatalException(try_catch)? • That calls from C++ to process._fatalException (node.js line 222) • Much like in our pseudo-domains, it hands them off to the active domain’s 'error' handler. • Thus all the enter()/exit() wrapping was just establishing context for this moment: deciding which domain gets the errors. • If there is an active domain, this behavior replaces 'uncaughtException'. @DOMENIC
  • 18.
    That’s cool. Nowwhat? We know how domains work. But do we truly know how to use them? @DOMENIC
  • 19.
    Stability 2: Unstable “TheAPIis in the process of settling, but has not yet had sufficient real-world testing to be considered stable. Backwards-compatibility will be maintained if reasonable.” (source) • Don’t use d.dispose(). @DOMENIC
  • 20.
    APIs to know •domain.create(), obviously • d.run(), to enter/exit the domain • d.add(), for adding event emitters created before the domain existed into the domain. • d.bind(), for manually wiring up a callback to a domain. • d.intercept(), that callback helper from before. @DOMENIC
  • 21.
    EventEmitter pitfalls • Eventemitters are bound to a domain on creation. But sometimes event emitters stick around for a long time. • Any callbacks given to the event emitter “escape” the active domain, which is replaced by whatever the active domain was when the event emitter was created. • Generally, anything involving a persistent connection or connection pool will be in trouble, and not able to associate errors with the currently-active domain. • https://github.com/domenic/domains-tragedy @DOMENIC
  • 22.
    Error recovery • Domainsdon’t give you try/catch back. • By the time you see an error on the domain, it escaped any close- to-the-source error handling; nobody handled it explicitly. • You’re probably in an inconsistent state, halfway through a transaction or with file handles open or who knows what. • The recommendation is to gracefully return a 500, then shut down your worker and restart while others in the cluster take over. @DOMENIC
  • 23.
    Versus promises • Domainshave grown … organically. • Promises attempt to solve error handling from first principles, by giving you back return and throw semantics. • In practice, this means wrapping every callback in try/catch, much like MakeCallback, but less pervasively and more intelligently. • But, they won’t save you completely in Node.js: • 'error' events are still fatal • If you step outside of promise-land, e.g. setTimeout, uncaught exceptions are again dangerous. @DOMENIC
  • 24.
    IN CONCLUSION • Domainswork pretty well to tame async errors in Node.js. • They do so by hooking into Node.js at the deepest level. • They don’t work perfectly, and they don’t absolve you of cleanup. • But you should be able to get some solid wins with minimal extra code. @DOMENIC

Editor's Notes

  • #2 Opening story: GothamJS, diving into AngularJS code.
  • #3 I work here at Lab49!
  • #6 The first of these, you handle yourself.The second, by design, re-throws the error if there are no listeners for it.The third, since we’re almost always inside a callback, can’t bubble up the call stack; callbacks reset the call stack, so they hit the top of that call stack pretty quickly.—If we were in a browser, those would hit window.onerror.
  • #7 (On switch back) How can we fix this??
  • #8 Let’s build our own “domains” to track error sources!
  • #9 I dug into the source code, and figured out how domains work. This is basically it.
  • #10 I dug into the source code, and figured out how domains work. This is basically it.
  • #11 Since we’ve got this working for everything else, might as well try to make callbacks a bit easier to use.
  • #15 All callbacks you give to Node.js’s code pass through here.Either directly, from C++ code which directly calls MakeCallback to execute your callbackOr indirectly, via a convoluted chain inside src/node.js that ends up in process._makeCallback.
  • #16 To understand how this gives us the power we need, let’s take a step back and see how the domain object themselves work.
  • #17 The moment you start using domains all this gets switched out from under you. process._setupDomainUse()BUT: wrapping in enter()/exit() is just the setup. enter() and exit() do literally nothing besides setting the active domain. The magic takes place in our next chapter: