Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IPC deferred with larger payloads (macOS only) #30009

Closed
Phrogz opened this issue Oct 17, 2019 · 4 comments
Closed

IPC deferred with larger payloads (macOS only) #30009

Phrogz opened this issue Oct 17, 2019 · 4 comments

Comments

@Phrogz
Copy link

Phrogz commented Oct 17, 2019

  • Version: v12.12.0
  • Platform: macOS v10.14
  • Subsystem: child_process

When creating a child process with fork() and IPC messages sent inside a loop are received ~immediately by the parent when the data is 'small' (<8k?), but when the data is 'large' (>10k?) on macOS the parent process does not receive the messages until the loop has finished. On Windows and Linux no such problem occurs.

Repro steps:

  1. Run node server.js using code below on macOS.
  2. curl http://localhost:8080/15
    • You will see alternating send/receive messages (correct)
  3. curl http://localhost:8080/123456

EXPECTED: Alternating send/receive messages in the console with interleaved timestamps.
ACTUAL: All send messages occur first, followed by all receive messages.

server.js

const opts = {stdio:['inherit', 'inherit', 'inherit', 'ipc']};
const child = require('child_process').fork('test-child.js', [], opts);

let lastUpdate = {};
child.on('message', msg => console.log(`parent: receive() ${msg.data.length} bytes`, Date.now()));

require('http').createServer((req, res) => {
   console.log(req.url);
   const match = /\d+/.exec(req.url);
   if (match) {
      child.send(match[0]*1);
      res.writeHead(200, {'Content-Type':'text/plain'});
      res.end(`Sending packets of size ${match[0]}`);
   } else {
      res.writeHead(404, {'Content-Type':'text/plain'});
      res.end('what?');
   }
}).listen(8080);

worker.js

if (process.send) process.on('message', msg => run(msg));

function run(messageSize) {
   const msg = new Array(messageSize+1).join('x');
   let lastUpdate = Date.now();
   for (let i=0; i<1e7; ++i) {
      const now = Date.now();
      if ((now-lastUpdate)>200) {
         console.log(`worker: send()  > ${messageSize} bytes`, now);
         process.send({action:'update', data:msg});
         lastUpdate = Date.now();
      }
      Math.sqrt(Math.random());
   }
   console.log('worker done');
}

Output of a run:

/15
worker: send()  > 15 bytes 1571324249029
parent: receive() 15 bytes 1571324249034
worker: send()  > 15 bytes 1571324249235
parent: receive() 15 bytes 1571324249235
worker: send()  > 15 bytes 1571324249436
parent: receive() 15 bytes 1571324249436
worker done
/123456
worker: send()  > 123456 bytes 1571324276973
worker: send()  > 123456 bytes 1571324277174
worker: send()  > 123456 bytes 1571324277375
worker done
parent: receive() 123456 bytes 1571324277391
parent: receive() 123456 bytes 1571324277391
parent: receive() 123456 bytes 1571324277393
@bnoordhuis
Copy link
Member

Alternating send/receive messages in the console with interleaved timestamps.

Can you explain why that's your expectation? Keep in mind that the behavior you observed on other platforms is not at all guaranteed behavior.

@Phrogz
Copy link
Author

Phrogz commented Oct 18, 2019

Can you explain why that's your expectation?

Given 6 scenarios (Win/Linux/macOS × small/large payloads), when five out of the six behave the same I think it is reasonable to expect that the sole remaining case would behave in a consistent manner.

The purpose of send() with IPC is to communicate a message between processes. If you told me that send() might queue up every message until you chose to quit the programs involved, I would say that send() was useless. Thankfully, it does not behave this way. It behaves as one would hope: it ~immediately sends the message to be received by the other side. Hooray!

Except that in this one case on one platform it behaves differently.

Keep in mind that the behavior you observed on other platforms is not at all guaranteed behavior.

It is hard to keep that in mind when I did not previously know it. I would appreciate links to sources that elaborate on this claim. Do you simply mean that the documentation on child_process.send() provides no guarantees or discussion on the timing of delivery?

@bnoordhuis
Copy link
Member

Do you simply mean that the documentation on child_process.send() provides no guarantees or discussion on the timing of delivery?

That's right and that's intentional.

Apropos the documentation, this paragraph is relevant to your test case:

subprocess.send() will return false if the channel has closed or when the backlog of unsent messages exceeds a threshold that makes it unwise to send more. Otherwise, the method returns true. The callback function can be used to implement flow control.

@targos
Copy link
Member

targos commented Dec 26, 2020

This seems to have been answered.

@targos targos closed this as completed Dec 26, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants