Domain Module Postmortem
Evan Lucas
Domain Module Postmortem
Usability Issues
Implicit Behavior
It's possible for a developer to create a new domain and then simply run
domain.enter()
. Which then acts as a catch-all for any exception in the
future that couldn't be observed by the thrower. Allowing a module author to
intercept the exceptions of unrelated code in a different module. Preventing
the originator of the code from knowing about its own exceptions.
Here's an example of how one indirectly linked modules can affect another:
// module a.js
const import b
b = var require: NodeJS.Require
(id: string) => any
require('./b');
const import c
c = var require: NodeJS.Require
(id: string) => any
require('./c');
// module b.js
const const d: Domain
d = var require: NodeJS.Require
(id: string) => any
require('domain').function create(): Domain
create();
const d: Domain
d.NodeJS.EventEmitter<DefaultEventMap>.on<any>(eventName: string | symbol, listener: (...args: any[]) => void): Domain
on('error', () => {
/* silence everything */
});
const d: Domain
d.Domain.enter(): void
enter();
// module c.js
const import dep
dep = var require: NodeJS.Require
(id: string) => any
require('some-dep');
import dep
dep.method(); // Uh-oh! This method doesn't actually exist.
Since module b
enters the domain but never exits any uncaught exception will
be swallowed. Leaving module c
in the dark as to why it didn't run the entire
script. Leaving a potentially partially populated module.exports
. Doing this
is not the same as listening for 'uncaughtException'
. As the latter is
explicitly meant to globally catch errors. The other issue is that domains are
processed prior to any 'uncaughtException'
handlers, and prevent them from
running.
Another issue is that domains route errors automatically if no 'error'
handler was set on the event emitter. There is no opt-in mechanism for this,
and automatically propagates across the entire asynchronous chain. This may
seem useful at first, but once asynchronous calls are two or more modules deep
and one of them doesn't include an error handler the creator of the domain will
suddenly be catching unexpected exceptions, and the thrower's exception will go
unnoticed by the author.
The following is a simple example of how a missing 'error'
handler allows
the active domain to hijack the error:
const module "domain"
domain = var require: NodeJS.Require
(id: string) => any
require('domain');
const module "net"
net = var require: NodeJS.Require
(id: string) => any
require('net');
const const d: domain.Domain
d = module "domain"
domain.function create(): domain.Domain
create();
const d: domain.Domain
d.NodeJS.EventEmitter<DefaultEventMap>.on<any>(eventName: string | symbol, listener: (...args: any[]) => void): domain.Domain
on('error', err: any
err => var console: Console
console.Console.error(message?: any, ...optionalParams: any[]): void (+1 overload)
error(err: any
err.message));
const d: domain.Domain
d.Domain.run<net.Server>(fn: (...args: any[]) => net.Server, ...args: any[]): net.Server
run(() =>
module "net"
net
.function createServer(connectionListener?: (socket: net.Socket) => void): net.Server (+1 overload)
createServer(c: net.Socket
c => {
c: net.Socket
c.Socket.end(callback?: () => void): net.Socket (+2 overloads)
end();
c: net.Socket
c.Socket.write(buffer: Uint8Array | string, cb?: (err?: Error | null) => void): boolean (+1 overload)
write('bye');
})
.Server.listen(port?: number, hostname?: string, backlog?: number, listeningListener?: () => void): net.Server (+8 overloads)
listen(8000)
);
Even manually removing the connection via d.remove(c)
does not prevent the
connection's error from being automatically intercepted.
Failures that plagues both error routing and exception handling are the inconsistencies in how errors are bubbled. The following is an example of how nested domains will and won't bubble the exception based on when they happen:
const module "domain"
domain = var require: NodeJS.Require
(id: string) => any
require('domain');
const module "net"
net = var require: NodeJS.Require
(id: string) => any
require('net');
const const d: domain.Domain
d = module "domain"
domain.function create(): domain.Domain
create();
const d: domain.Domain
d.NodeJS.EventEmitter<DefaultEventMap>.on<any>(eventName: string | symbol, listener: (...args: any[]) => void): domain.Domain
on('error', () => var console: Console
console.Console.error(message?: any, ...optionalParams: any[]): void (+1 overload)
error('d intercepted an error'));
const d: domain.Domain
d.Domain.run<void>(fn: (...args: any[]) => void, ...args: any[]): void
run(() => {
const const server: net.Server
server = module "net"
net
.function createServer(connectionListener?: (socket: net.Socket) => void): net.Server (+1 overload)
createServer(c: net.Socket
c => {
const const e: domain.Domain
e = module "domain"
domain.function create(): domain.Domain
create(); // No 'error' handler being set.
const e: domain.Domain
e.Domain.run<never>(fn: (...args: any[]) => never, ...args: any[]): never
run(() => {
// This will not be caught by d's error handler.
function setImmediate<[]>(callback: () => void): NodeJS.Immediate (+1 overload)
setImmediate(() => {
throw new var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error('thrown from setImmediate');
});
// Though this one will bubble to d's error handler.
throw new var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error('immediately thrown');
});
})
.Server.listen(port?: number, hostname?: string, backlog?: number, listeningListener?: () => void): net.Server (+8 overloads)
listen(8080);
});
It may be expected that nested domains always remain nested, and will always propagate the exception up the domain stack. Or that exceptions will never automatically bubble. Unfortunately both these situations occur, leading to potentially confusing behavior that may even be prone to difficult to debug timing conflicts.
API Gaps
While APIs based on using EventEmitter
can use bind()
and errback style
callbacks can use intercept()
, alternative APIs that implicitly bind to the
active domain must be executed inside of run()
. Meaning if module authors
wanted to support domains using a mechanism alternative to those mentioned they
must manually implement domain support themselves. Instead of being able to
leverage the implicit mechanisms already in place.
Error Propagation
Propagating errors across nested domains is not straight forward, if even
possible. Existing documentation shows a simple example of how to close()
an
http
server if there is an error in the request handler. What it does not
explain is how to close the server if the request handler creates another
domain instance for another async request. Using the following as a simple
example of the failing of error propagation:
const module d1
const d1: any
d1 = domain.create();
module d1
const d1: any
d1.foo = true; // custom member to make more visible in console
module d1
const d1: any
d1.on('error', er: any
er => {
/* handle error */
});
module d1
const d1: any
d1.run(() =>
function setTimeout<[]>(callback: () => void, delay?: number): NodeJS.Timeout (+2 overloads)
setTimeout(() => {
const const d2: any
d2 = domain.create();
const d2: any
d2.bar = 43;
const d2: any
d2.on('error', er: any
er => var console: Console
console.Console.error(message?: any, ...optionalParams: any[]): void (+1 overload)
error(er: any
er.message, domain._stack));
const d2: any
d2.run(() => {
function setTimeout<[]>(callback: () => void, delay?: number): NodeJS.Timeout (+2 overloads)
setTimeout(() => {
function setTimeout<[]>(callback: () => void, delay?: number): NodeJS.Timeout (+2 overloads)
setTimeout(() => {
throw new var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error('outer');
});
throw new var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error('inner');
});
});
})
);
Even in the case that the domain instances are being used for local storage so
access to resources are made available there is still no way to allow the error
to continue propagating from d2
back to d1
. Quick inspection may tell us
that simply throwing from d2
's domain 'error'
handler would allow d1
to
then catch the exception and execute its own error handler. Though that is not
the case. Upon inspection of domain._stack
you'll see that the stack only
contains d2
.
This may be considered a failing of the API, but even if it did operate in this
way there is still the issue of transmitting the fact that a branch in the
asynchronous execution has failed, and that all further operations in that
branch must cease. In the example of the http request handler, if we fire off
several asynchronous requests and each one then write()
's data back to the
client many more errors will arise from attempting to write()
to a closed
handle. More on this in Resource Cleanup on Exception.
Resource Cleanup on Exception
The following script contains a more complex example of properly cleaning up in a small resource dependency tree in the case that an exception occurs in a given connection or any of its dependencies. Breaking down the script into its basic operations:
'use strict';
const module "domain"
domain = var require: NodeJS.Require
(id: string) => any
require('domain');
const class EE<T extends EventMap<T> = DefaultEventMap>
EE = var require: NodeJS.Require
(id: string) => any
require('events');
const module "fs"
fs = var require: NodeJS.Require
(id: string) => any
require('fs');
const module "net"
net = var require: NodeJS.Require
(id: string) => any
require('net');
const module "util"
util = var require: NodeJS.Require
(id: string) => any
require('util');
const const print: any
print = var process: NodeJS.Process
process._rawDebug;
const const pipeList: any[]
pipeList = [];
const const FILENAME: "/tmp/tmp.tmp"
FILENAME = '/tmp/tmp.tmp';
const const PIPENAME: "/tmp/node-domain-example-"
PIPENAME = '/tmp/node-domain-example-';
const const FILESIZE: 1024
FILESIZE = 1024;
var var uid: number
uid = 0;
// Setting up temporary resources
const const buf: any
buf = var Buffer: BufferConstructor
Buffer(const FILESIZE: 1024
FILESIZE);
for (var var i: number
i = 0; var i: number
i < const buf: any
buf.length; var i: number
i++) const buf: any
buf[var i: number
i] = ((var Math: Math
Math.Math.random(): number
random() * 1e3) % 78) + 48; // Basic ASCII
module "fs"
fs.function writeFileSync(file: fs.PathOrFileDescriptor, data: string | NodeJS.ArrayBufferView, options?: fs.WriteFileOptions): void
writeFileSync(const FILENAME: "/tmp/tmp.tmp"
FILENAME, const buf: any
buf);
function function ConnectionResource(c: any): void
ConnectionResource(c: any
c) {
class EE<T extends EventMap<T> = DefaultEventMap>
EE.NewableFunction.call<EE<EventMap<any>>, []>(this: new () => EE<EventMap<any>>, thisArg: EE<EventMap<any>>): void
call(this);
this.ConnectionResource._connection: any
_connection = c: any
c;
this.ConnectionResource._alive: any
_alive = true;
this.ConnectionResource._domain: any
_domain = module "domain"
domain.function create(): domain.Domain
create();
this.ConnectionResource._id: any
_id = var Math: Math
Math.Math.random(): number
random().Number.toString(radix?: number): string
toString(32).String.substr(from: number, length?: number): string
substr(2).String.substr(from: number, length?: number): string
substr(0, 8) + ++var uid: number
uid;
this.ConnectionResource._domain: domain.Domain
_domain.Domain.add(emitter: EE | NodeJS.Timer): void
add(c: any
c);
this.ConnectionResource._domain: domain.Domain
_domain.NodeJS.EventEmitter<DefaultEventMap>.on<any>(eventName: string | symbol, listener: (...args: any[]) => void): domain.Domain
on('error', () => {
this.ConnectionResource._alive: boolean
_alive = false;
});
}
module "util"
util.function inherits(constructor: unknown, superConstructor: unknown): void
inherits(class ConnectionResource
function ConnectionResource(c: any): void
ConnectionResource, class EE<T extends EventMap<T> = DefaultEventMap>
EE);
class ConnectionResource
function ConnectionResource(c: any): void
ConnectionResource.Function.prototype: any
prototype.ConnectionResource.end(chunk: any): void
end = function function (local function) end(chunk: any): void
end(chunk: any
chunk) {
this.ConnectionResource._alive: boolean
_alive = false;
this.ConnectionResource._connection: any
_connection.end(chunk: any
chunk);
this.emit('end');
};
class ConnectionResource
function ConnectionResource(c: any): void
ConnectionResource.Function.prototype: any
prototype.ConnectionResource.isAlive(): boolean
isAlive = function function (local function) isAlive(): boolean
isAlive() {
return this.ConnectionResource._alive: boolean
_alive;
};
class ConnectionResource
function ConnectionResource(c: any): void
ConnectionResource.Function.prototype: any
prototype.ConnectionResource.id(): string
id = function function (local function) id(): string
id() {
return this.ConnectionResource._id: string
_id;
};
class ConnectionResource
function ConnectionResource(c: any): void
ConnectionResource.Function.prototype: any
prototype.ConnectionResource.write(chunk: any): any
write = function function (local function) write(chunk: any): any
write(chunk: any
chunk) {
this.emit('data', chunk: any
chunk);
return this.ConnectionResource._connection: any
_connection.write(chunk: any
chunk);
};
// Example begin
module "net"
net
.function createServer(connectionListener?: (socket: net.Socket) => void): net.Server (+1 overload)
createServer(c: net.Socket
c => {
const const cr: ConnectionResource
cr = new constructor ConnectionResource(c: any): ConnectionResource
ConnectionResource(c: net.Socket
c);
const const d1: domain.Domain
d1 = module "domain"
domain.function create(): domain.Domain
create();
module "fs"
fs.function open(path: fs.PathLike, flags: fs.OpenMode | undefined, callback: (err: NodeJS.ErrnoException | null, fd: number) => void): void (+2 overloads)
open(
const FILENAME: "/tmp/tmp.tmp"
FILENAME,
'r',
const d1: domain.Domain
d1.Domain.intercept<(fd: NodeJS.ErrnoException | null) => void>(callback: (fd: NodeJS.ErrnoException | null) => void): (fd: NodeJS.ErrnoException | null) => void
intercept(fd: NodeJS.ErrnoException | null
fd => {
function streamInParts(fd: any, cr: any, pos: any): void
streamInParts(fd: NodeJS.ErrnoException | null
fd, const cr: ConnectionResource
cr, 0);
})
);
function pipeData(cr: any): void
pipeData(const cr: ConnectionResource
cr);
c: net.Socket
c.Socket.on(event: "close", listener: (hadError: boolean) => void): net.Socket (+12 overloads)
on('close', () => const cr: ConnectionResource
cr.ConnectionResource.end(chunk: any): void
end());
})
.Server.listen(port?: number, hostname?: string, backlog?: number, listeningListener?: () => void): net.Server (+8 overloads)
listen(8080);
function function streamInParts(fd: any, cr: any, pos: any): void
streamInParts(fd: any
fd, cr: any
cr, pos: any
pos) {
const const d2: domain.Domain
d2 = module "domain"
domain.function create(): domain.Domain
create();
var function (local var) alive: boolean
alive = true;
const d2: domain.Domain
d2.NodeJS.EventEmitter<DefaultEventMap>.on<any>(eventName: string | symbol, listener: (...args: any[]) => void): domain.Domain
on('error', er: any
er => {
const print: any
print('d2 error:', er: any
er.message);
cr: any
cr.end();
});
module "fs"
fs.function read<Buffer<ArrayBuffer>>(fd: number, buffer: Buffer<ArrayBuffer>, offset: number, length: number, position: fs.ReadPosition | null, callback: (err: NodeJS.ErrnoException | null, bytesRead: number, buffer: Buffer<...>) => void): void (+4 overloads)
read(
fd: any
fd,
new var Buffer: BufferConstructor
new (size: number) => Buffer<ArrayBuffer> (+3 overloads)
Buffer(10),
0,
10,
pos: any
pos,
const d2: domain.Domain
d2.Domain.intercept<(bRead: NodeJS.ErrnoException | null, buf: number) => void>(callback: (bRead: NodeJS.ErrnoException | null, buf: number) => void): (bRead: NodeJS.ErrnoException | null, buf: number) => void
intercept((bRead: NodeJS.ErrnoException | null
bRead, buf: number
buf) => {
if (!cr: any
cr.isAlive()) {
return module "fs"
fs.function close(fd: number, callback?: fs.NoParamCallback): void
close(fd: any
fd);
}
if (cr: any
cr._connection.bytesWritten < const FILESIZE: 1024
FILESIZE) {
// Documentation says callback is optional, but doesn't mention that if
// the write fails an exception will be thrown.
const const goodtogo: any
goodtogo = cr: any
cr.write(buf: number
buf);
if (const goodtogo: any
goodtogo) {
function setTimeout<[]>(callback: () => void, delay?: number): NodeJS.Timeout (+2 overloads)
setTimeout(() => function streamInParts(fd: any, cr: any, pos: any): void
streamInParts(fd: any
fd, cr: any
cr, pos: any
pos + bRead: NodeJS.ErrnoException | null
bRead), 1000);
} else {
cr: any
cr._connection.once('drain', () =>
function streamInParts(fd: any, cr: any, pos: any): void
streamInParts(fd: any
fd, cr: any
cr, pos: any
pos + bRead: NodeJS.ErrnoException | null
bRead)
);
}
return;
}
cr: any
cr.end(buf: number
buf);
module "fs"
fs.function close(fd: number, callback?: fs.NoParamCallback): void
close(fd: any
fd);
})
);
}
function function pipeData(cr: any): void
pipeData(cr: any
cr) {
const const pname: string
pname = const PIPENAME: "/tmp/node-domain-example-"
PIPENAME + cr: any
cr.id();
const const ps: net.Server
ps = module "net"
net.function createServer(connectionListener?: (socket: net.Socket) => void): net.Server (+1 overload)
createServer();
const const d3: domain.Domain
d3 = module "domain"
domain.function create(): domain.Domain
create();
const const connectionList: any[]
connectionList = [];
const d3: domain.Domain
d3.NodeJS.EventEmitter<DefaultEventMap>.on<any>(eventName: string | symbol, listener: (...args: any[]) => void): domain.Domain
on('error', er: any
er => {
const print: any
print('d3 error:', er: any
er.message);
cr: any
cr.end();
});
const d3: domain.Domain
d3.Domain.add(emitter: EE | NodeJS.Timer): void
add(const ps: net.Server
ps);
const ps: net.Server
ps.Server.on(event: "connection", listener: (socket: net.Socket) => void): net.Server (+5 overloads)
on('connection', conn: net.Socket
conn => {
const connectionList: any[]
connectionList.Array<any>.push(...items: any[]): number
push(conn: net.Socket
conn);
conn: net.Socket
conn.Socket.on(event: "data", listener: (data: Buffer) => void): net.Socket (+12 overloads)
on('data', () => {}); // don't care about incoming data.
conn: net.Socket
conn.Socket.on(event: "close", listener: (hadError: boolean) => void): net.Socket (+12 overloads)
on('close', () => {
const connectionList: any[]
connectionList.Array<any>.splice(start: number, deleteCount?: number): any[] (+1 overload)
splice(const connectionList: any[]
connectionList.Array<any>.indexOf(searchElement: any, fromIndex?: number): number
indexOf(conn: net.Socket
conn), 1);
});
});
cr: any
cr.on('data', chunk: any
chunk => {
for (var function (local var) i: number
i = 0; function (local var) i: number
i < const connectionList: any[]
connectionList.Array<any>.length: number
length; function (local var) i: number
i++) {
const connectionList: any[]
connectionList[function (local var) i: number
i].write(chunk: any
chunk);
}
});
cr: any
cr.on('end', () => {
for (var function (local var) i: number
i = 0; function (local var) i: number
i < const connectionList: any[]
connectionList.Array<any>.length: number
length; function (local var) i: number
i++) {
const connectionList: any[]
connectionList[function (local var) i: number
i].end();
}
const ps: net.Server
ps.Server.close(callback?: (err?: Error) => void): net.Server
close();
});
const pipeList: any[]
pipeList.Array<any>.push(...items: any[]): number
push(const pname: string
pname);
const ps: net.Server
ps.Server.listen(path: string, backlog?: number, listeningListener?: () => void): net.Server (+8 overloads)
listen(const pname: string
pname);
}
var process: NodeJS.Process
process.NodeJS.Process.on(event: NodeJS.Signals, listener: NodeJS.SignalsListener): NodeJS.Process (+12 overloads)
on('SIGINT', () => var process: NodeJS.Process
process.NodeJS.Process.exit(code?: number | string | null | undefined): never
exit());
var process: NodeJS.Process
process.NodeJS.Process.on(event: "exit", listener: NodeJS.ExitListener): NodeJS.Process (+12 overloads)
on('exit', () => {
try {
for (var function (local var) i: number
i = 0; function (local var) i: number
i < const pipeList: any[]
pipeList.Array<any>.length: number
length; function (local var) i: number
i++) {
module "fs"
fs.function unlinkSync(path: fs.PathLike): void
unlinkSync(const pipeList: any[]
pipeList[function (local var) i: number
i]);
}
module "fs"
fs.function unlinkSync(path: fs.PathLike): void
unlinkSync(const FILENAME: "/tmp/tmp.tmp"
FILENAME);
} catch (function (local var) e: unknown
e) {}
});
- When a new connection happens, concurrently:
- Open a file on the file system
- Open Pipe to unique socket
- Read a chunk of the file asynchronously
- Write chunk to both the TCP connection and any listening sockets
- If any of these resources error, notify all other attached resources that they need to clean up and shutdown
As we can see from this example a lot more must be done to properly clean up resources when something fails than what can be done strictly through the domain API. All that domains offer is an exception aggregation mechanism. Even the potentially useful ability to propagate data with the domain is easily countered, in this example, by passing the needed resources as a function argument.
One problem domains perpetuated was the supposed simplicity of being able to continue execution, contrary to what the documentation stated, of the application despite an unexpected exception. This example demonstrates the fallacy behind that idea.
Attempting proper resource cleanup on unexpected exception becomes more complex as the application itself grows in complexity. This example only has 3 basic resources in play, and all of them with a clear dependency path. If an application uses something like shared resources or resource reuse the ability to cleanup, and properly test that cleanup has been done, grows greatly.
In the end, in terms of handling errors, domains aren't much more than a
glorified 'uncaughtException'
handler. Except with more implicit and
unobservable behavior by third-parties.
Resource Propagation
Another use case for domains was to use it to propagate data along asynchronous data paths. One problematic point is the ambiguity of when to expect the correct domain when there are multiple in the stack (which must be assumed if the async stack works with other modules). Also the conflict between being able to depend on a domain for error handling while also having it available to retrieve the necessary data.
The following is a involved example demonstrating the failing using domains to propagate data along asynchronous stacks:
const module "domain"
domain = var require: NodeJS.Require
(id: string) => any
require('domain');
const module "net"
net = var require: NodeJS.Require
(id: string) => any
require('net');
const const server: net.Server
server = module "net"
net
.function createServer(connectionListener?: (socket: net.Socket) => void): net.Server (+1 overload)
createServer(c: net.Socket
c => {
// Use a domain to propagate data across events within the
// connection so that we don't have to pass arguments
// everywhere.
const const d: domain.Domain
d = module "domain"
domain.function create(): domain.Domain
create();
const d: domain.Domain
d.data = { connection: net.Socket
connection: c: net.Socket
c };
const d: domain.Domain
d.Domain.add(emitter: EventEmitter | NodeJS.Timer): void
add(c: net.Socket
c);
// Mock class that does some useless async data transformation
// for demonstration purposes.
const const ds: DataStream
ds = new constructor DataStream(cb: any): DataStream
DataStream(function dataTransformed(chunk: any): void
dataTransformed);
c: net.Socket
c.Socket.on(event: "data", listener: (data: Buffer) => void): net.Socket (+12 overloads)
on('data', chunk: Buffer<ArrayBufferLike>
chunk => const ds: DataStream
ds.DataStream.data(chunk: any): void
data(chunk: Buffer<ArrayBufferLike>
chunk));
})
.Server.listen(port?: number, listeningListener?: () => void): net.Server (+8 overloads)
listen(8080, () => var console: Console
console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)
log(`listening on 8080`));
function function dataTransformed(chunk: any): void
dataTransformed(chunk: any
chunk) {
// FAIL! Because the DataStream instance also created a
// domain we have now lost the active domain we had
// hoped to use.
module "domain"
domain.active.data.connection.write(chunk: any
chunk);
}
function function DataStream(cb: any): void
DataStream(cb: any
cb) {
this.DataStream.cb: any
cb = cb: any
cb;
// DataStream wants to use domains for data propagation too!
// Unfortunately this will conflict with any domain that
// already exists.
this.DataStream.domain: any
domain = module "domain"
domain.function create(): domain.Domain
create();
this.DataStream.domain: domain.Domain
domain.data = { inst: this
inst: this };
}
class DataStream
function DataStream(cb: any): void
DataStream.Function.prototype: any
prototype.DataStream.data(chunk: any): void
data = function function (local function) data(chunk: any): void
data(chunk: any
chunk) {
// This code is self contained, but pretend it's a complex
// operation that crosses at least one other module. So
// passing along "this", etc., is not easy.
this.DataStream.domain: domain.Domain
domain.Domain.run<void>(fn: (...args: any[]) => void, ...args: any[]): void
run(function () {
// Simulate an async operation that does the data transform.
function setImmediate<[]>(callback: () => void): NodeJS.Immediate (+1 overload)
setImmediate(() => {
for (var function (local var) i: number
i = 0; function (local var) i: number
i < chunk: any
chunk.length; function (local var) i: number
i++)
chunk: any
chunk[function (local var) i: number
i] = ((chunk: any
chunk[function (local var) i: number
i] + var Math: Math
Math.Math.random(): number
random() * 100) % 96) + 33;
// Grab the instance from the active domain and use that
// to call the user's callback.
const const self: any
self = module "domain"
domain.active.data.inst;
const self: any
self.cb.call(const self: any
self, chunk: any
chunk);
});
});
};
The above shows that it is difficult to have more than one asynchronous API
attempt to use domains to propagate data. This example could possibly be fixed
by assigning parent: domain.active
in the DataStream
constructor. Then
restoring it via domain.active = domain.active.data.parent
just before the
user's callback is called. Also the instantiation of DataStream
in the
'connection'
callback must be run inside d.run()
, instead of simply using
d.add(c)
, otherwise there will be no active domain.
In short, for this to have a prayer of a chance usage would need to strictly adhere to a set of guidelines that would be difficult to enforce or test.
Performance Issues
A significant deterrent from using domains is the overhead. Using node's
built-in http benchmark, http_simple.js
, without domains it can handle over
22,000 requests/second. Whereas if it's run with NODE_USE_DOMAINS=1
that
number drops down to under 17,000 requests/second. In this case there is only
a single global domain. If we edit the benchmark so the http request callback
creates a new domain instance performance drops further to 15,000
requests/second.
While this probably wouldn't affect a server only serving a few hundred or even a thousand requests per second, the amount of overhead is directly proportional to the number of asynchronous requests made. So if a single connection needs to connect to several other services all of those will contribute to the overall latency of delivering the final product to the client.
Using AsyncWrap
and tracking the number of times
init
/pre
/post
/destroy
are called in the mentioned benchmark we find
that the sum of all events called is over 170,000 times per second. This means
even adding 1 microsecond overhead per call for any type of setup or tear down
will result in a 17% performance loss. Granted, this is for the optimized
scenario of the benchmark, but I believe this demonstrates the necessity for a
mechanism such as domain to be as cheap to run as possible.
Looking Ahead
The domain module has been soft deprecated since Dec 2014, but has not yet been
removed because node offers no alternative functionality at the moment. As of
this writing there is ongoing work building out the AsyncWrap
API and a
proposal for Zones being prepared for the TC39. At such time there is suitable
functionality to replace domains it will undergo the full deprecation cycle and
eventually be removed from core.