Browse Source

child_process: add callback parameter to .send()

Add an optional callback parameter to `ChildProcess.prototype.send()`
that is invoked when the message has been sent.

Juggle the control channel's reference count so that in-flight messages
keep the event loop (and therefore the process) alive until they have
been sent.

`ChildProcess.prototype.send()` and `process.send()` used to operate
synchronously but became asynchronous in commit libuv/libuv@393c1c5
("unix: set non-block mode in uv_{pipe,tcp,udp}_open"), which landed
in io.js in commit 07bd05b ("deps: update libuv to 1.2.1").

Fixes: https://github.com/nodejs/node/issues/760
PR-URL: https://github.com/nodejs/node/pull/2620
Reviewed-By: trevnorris - Trevor Norris <trev.norris@gmail.com>
Reviewed-By: jasnell - James M Snell <jasnell@gmail.com>
v5.x
Ben Noordhuis 9 years ago
committed by Rod Vagg
parent
commit
607aa3ac82
  1. 23
      doc/api/child_process.markdown
  2. 4
      doc/api/cluster.markdown
  3. 10
      lib/child_process.js
  4. 91
      lib/internal/child_process.js
  5. 19
      test/parallel/test-child-process-send-cb.js

23
doc/api/child_process.markdown

@ -214,13 +214,15 @@ to a process.
See `kill(2)` See `kill(2)`
### child.send(message[, sendHandle]) ### child.send(message[, sendHandle][, callback])
* `message` {Object} * `message` {Object}
* `sendHandle` {Handle object} * `sendHandle` {Handle object}
* `callback` {Function}
* Return: Boolean
When using `child_process.fork()` you can write to the child using When using `child_process.fork()` you can write to the child using
`child.send(message, [sendHandle])` and messages are received by `child.send(message[, sendHandle][, callback])` and messages are received by
a `'message'` event on the child. a `'message'` event on the child.
For example: For example:
@ -246,11 +248,6 @@ And then the child script, `'sub.js'` might look like this:
In the child the `process` object will have a `send()` method, and `process` In the child the `process` object will have a `send()` method, and `process`
will emit objects each time it receives a message on its channel. will emit objects each time it receives a message on its channel.
Please note that the `send()` method on both the parent and child are
synchronous - sending large chunks of data is not advised (pipes can be used
instead, see
[`child_process.spawn`](#child_process_child_process_spawn_command_args_options)).
There is a special case when sending a `{cmd: 'NODE_foo'}` message. All messages There is a special case when sending a `{cmd: 'NODE_foo'}` message. All messages
containing a `NODE_` prefix in its `cmd` property will not be emitted in containing a `NODE_` prefix in its `cmd` property will not be emitted in
the `message` event, since they are internal messages used by Node.js core. the `message` event, since they are internal messages used by Node.js core.
@ -261,8 +258,16 @@ The `sendHandle` option to `child.send()` is for sending a TCP server or
socket object to another process. The child will receive the object as its socket object to another process. The child will receive the object as its
second argument to the `message` event. second argument to the `message` event.
Emits an `'error'` event if the message cannot be sent, for example because The `callback` option is a function that is invoked after the message is
the child process has already exited. sent but before the target may have received it. It is called with a single
argument: `null` on success, or an `Error` object on failure.
`child.send()` emits an `'error'` event if no callback was given and the message
cannot be sent, for example because the child process has already exited.
Returns `true` under normal circumstances or `false` when the backlog of
unsent messages exceeds a threshold that makes it unwise to send more.
Use the callback mechanism to implement flow control.
#### Example: sending server object #### Example: sending server object

4
doc/api/cluster.markdown

@ -426,10 +426,12 @@ exit, the master may choose not to respawn a worker based on this value.
// kill worker // kill worker
worker.kill(); worker.kill();
### worker.send(message[, sendHandle]) ### worker.send(message[, sendHandle][, callback])
* `message` {Object} * `message` {Object}
* `sendHandle` {Handle object} * `sendHandle` {Handle object}
* `callback` {Function}
* Return: Boolean
Send a message to a worker or master, optionally with a handle. Send a message to a worker or master, optionally with a handle.

10
lib/child_process.js

@ -48,16 +48,12 @@ exports._forkChild = function(fd) {
var p = new Pipe(true); var p = new Pipe(true);
p.open(fd); p.open(fd);
p.unref(); p.unref();
setupChannel(process, p); const control = setupChannel(process, p);
var refs = 0;
process.on('newListener', function(name) { process.on('newListener', function(name) {
if (name !== 'message' && name !== 'disconnect') return; if (name === 'message' || name === 'disconnect') control.ref();
if (++refs === 1) p.ref();
}); });
process.on('removeListener', function(name) { process.on('removeListener', function(name) {
if (name !== 'message' && name !== 'disconnect') return; if (name === 'message' || name === 'disconnect') control.unref();
if (--refs === 0) p.unref();
}); });
}; };

91
lib/internal/child_process.js

@ -397,6 +397,25 @@ function setupChannel(target, channel) {
target._channel = channel; target._channel = channel;
target._handleQueue = null; target._handleQueue = null;
const control = new class extends EventEmitter {
constructor() {
super();
this.channel = channel;
this.refs = 0;
}
ref() {
if (++this.refs === 1) {
this.channel.ref();
}
}
unref() {
if (--this.refs === 0) {
this.channel.unref();
this.emit('unref');
}
}
};
var decoder = new StringDecoder('utf8'); var decoder = new StringDecoder('utf8');
var jsonBuffer = ''; var jsonBuffer = '';
channel.buffering = false; channel.buffering = false;
@ -446,7 +465,7 @@ function setupChannel(target, channel) {
target._handleQueue = null; target._handleQueue = null;
queue.forEach(function(args) { queue.forEach(function(args) {
target._send(args.message, args.handle, false); target._send(args.message, args.handle, false, args.callback);
}); });
// Process a pending disconnect (if any). // Process a pending disconnect (if any).
@ -478,14 +497,24 @@ function setupChannel(target, channel) {
}); });
}); });
target.send = function(message, handle) { target.send = function(message, handle, callback) {
if (!this.connected) if (typeof handle === 'function') {
this.emit('error', new Error('channel closed')); callback = handle;
else handle = undefined;
this._send(message, handle, false); }
if (this.connected) {
this._send(message, handle, false, callback);
return;
}
const ex = new Error('channel closed');
if (typeof callback === 'function') {
process.nextTick(callback, ex);
} else {
this.emit('error', ex); // FIXME(bnoordhuis) Defer to next tick.
}
}; };
target._send = function(message, handle, swallowErrors) { target._send = function(message, handle, swallowErrors, callback) {
assert(this.connected || this._channel); assert(this.connected || this._channel);
if (message === undefined) if (message === undefined)
@ -516,7 +545,11 @@ function setupChannel(target, channel) {
// Queue-up message and handle if we haven't received ACK yet. // Queue-up message and handle if we haven't received ACK yet.
if (this._handleQueue) { if (this._handleQueue) {
this._handleQueue.push({ message: message.msg, handle: handle }); this._handleQueue.push({
callback: callback,
handle: handle,
message: message.msg,
});
return; return;
} }
@ -538,24 +571,43 @@ function setupChannel(target, channel) {
} else if (this._handleQueue && } else if (this._handleQueue &&
!(message && message.cmd === 'NODE_HANDLE_ACK')) { !(message && message.cmd === 'NODE_HANDLE_ACK')) {
// Queue request anyway to avoid out-of-order messages. // Queue request anyway to avoid out-of-order messages.
this._handleQueue.push({ message: message, handle: null }); this._handleQueue.push({
callback: callback,
handle: null,
message: message,
});
return; return;
} }
var req = new WriteWrap(); var req = new WriteWrap();
req.oncomplete = nop; req.async = false;
var string = JSON.stringify(message) + '\n'; var string = JSON.stringify(message) + '\n';
var err = channel.writeUtf8String(req, string, handle); var err = channel.writeUtf8String(req, string, handle);
if (err) { if (err === 0) {
if (!swallowErrors) if (handle && !this._handleQueue)
this.emit('error', errnoException(err, 'write')); this._handleQueue = [];
} else if (handle && !this._handleQueue) { req.oncomplete = function() {
this._handleQueue = []; if (this.async === true)
} control.unref();
if (obj && obj.postSend)
if (obj && obj.postSend) { obj.postSend(handle);
req.oncomplete = obj.postSend.bind(null, handle); if (typeof callback === 'function')
callback(null);
};
if (req.async === true) {
control.ref();
} else {
process.nextTick(function() { req.oncomplete(); });
}
} else if (!swallowErrors) {
const ex = errnoException(err, 'write');
if (typeof callback === 'function') {
process.nextTick(callback, ex);
} else {
this.emit('error', ex); // FIXME(bnoordhuis) Defer to next tick.
}
} }
/* If the master is > 2 read() calls behind, please stop sending. */ /* If the master is > 2 read() calls behind, please stop sending. */
@ -616,6 +668,7 @@ function setupChannel(target, channel) {
}; };
channel.readStart(); channel.readStart();
return control;
} }

19
test/parallel/test-child-process-send-cb.js

@ -0,0 +1,19 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const fork = require('child_process').fork;
if (process.argv[2] === 'child') {
process.send('ok', common.mustCall(function(err) {
assert.strictEqual(err, null);
}));
} else {
const child = fork(process.argv[1], ['child']);
child.on('message', common.mustCall(function(message) {
assert.strictEqual(message, 'ok');
}));
child.on('exit', common.mustCall(function(exitCode, signalCode) {
assert.strictEqual(exitCode, 0);
assert.strictEqual(signalCode, null);
}));
}
Loading…
Cancel
Save