Node.js使用setTimeout调度的光纤和代码会导致崩溃

我正在使用Fibers来解决有关如何在node.js中控制事件循环的问题,暂停某些同步代码的执行 。 这很好,主要是,但我遇到了一个奇怪的崩溃,但我无法find它的原因。

build立

有三个过程:

  • 一个主要的服务器进程,它接收代码进行testing和执行。 当它接收到新的代码来执行它时,使用child_process.fork()来产生
  • 执行过程。 这将使接收到的代码不时地调用特定的callback来报告执行的代码中发生了什么。 然后执行使用Contextify创build的沙箱中的代码。 有时这些报告包括错误的位置信息关于代码中的行和列的事情发生。 在这种情况下,需要使用源地图将仪表代码中的位置映射到原始代码中的位置。 但是计算这个源图需要花费大量的时间。 因此,在开始执行之前,执行过程会产生
  • 源图计算过程。 这只需要原始代码和检测代码并计算源地图。 完成后,它将完成的源映射发送到执行过程并退出。

如果执行过程在执行完成之前需要callback中的源映射,它将使用Fiber.yield()来控制事件循环,从而暂停执行。 当执行进程接收到数据时,它使用pausedFiber.run()继续执行。

这是这样实现的:

// server.js / main process function executeCode(codeToExecute) { var runtime = fork("./runtime"); runtime.on("uncaught exception", function (exception) { console.log("An uncaught exception occured in process with id " + id + ": ", exception); console.log(exception.stack); }); runtime.on("exit", function (code, signal) { console.log("Child process exited with code: " + code + " after receiving signal: " + signal); }); runtime.send({ type: "code", code: code}); } 

 // runtime.js / execution process var pausedExecution, sourceMap, messagesToSend = []; function getSourceMap() { if (sourceMap === undefined) { console.log("Waiting for source map."); pausedExecution = Fiber.current; Fiber.yield(); pausedExecution = undefined; console.log("Wait is over.") } if (sourceMap === null) { throw new Error("Source map could not be generated."); } else { // we should have a proper source map now return sourceMap; } } function callback(message) { console.log("Message:", message.type;) if (message.type === "console log") { // the location of the console log message will be the location in the instrumented code /// we have to adjust it to get the position in the original code message.loc = getSourceMap().originalPositionFor(message.loc); } messagesToSend.push(message); // gather messages in a buffer // do not forward messages every time, instead gather a bunch and send them all at once if (messagesToSend.length > 100) { console.log("Sending messages."); process.send({type: "message batch", messages: messagesToSend}); messagesToSend.splice(0); // empty the array } } // function to send messages when we get a chance to prevent the client from waiting too long function sendMessagesWithEventLoopTurnaround() { if (messagesToSend.length > 0) { process.send({type: "message batch", messages: messagesToSend}); messagesToSend.splice(0); // empty the array } setTimeout(sendMessagesWithEventLoopTurnAround, 10); } function executeCode(code) { // setup child process to calculate the source map importantDataCalculator = fork("./runtime"); importantDataCalculator.on("message", function (msg) { if (msg.type === "result") { importantData = msg.data; console.log("Finished source map generation!") } else if (msg.type === "error") { importantData = null; } else { throw new Error("Unknown message from dataGenerator!"); } if (pausedExecution) { // execution is waiting for the data pausedExecution.run(); } }); // setup automatic messages sending in the event loop sendMessagesWithEventLoopTurnaround(); // instrument the code to call a function called "callback", which will be defined in the sandbox instrumentCode(code); // prepare the sandbox var sandbox = Contextify(new utils.Sandbox(callback)); // the callback to be called from the instrumented code is defined in the sandbox // wrap the execution of the code in a Fiber, so it can be paused Fiber(function () { sandbox.run(code); // send messages because the execution finished console.log("Sending messages."); process.send({type: "message batch", messages: messagesToSend}); messagesToSend.splice(0); // empty the array }).run(); } process.on("message", function (msg) { if (msg.type === "code") { executeCode(msg.code, msg.options); } }); 

所以总结一下:当收到新的代码时,会创build一个新的进程来执行它。 这个过程首先工具然后执行它。 在这之前,它开始第三个过程来计算代码的源地图。 检测代码在上面的代码中调用名为callback的函数,将消息传递给报告执行代码进度的运行时。 有时需要对这些进行调整,需要调整的一个例子是“控制台日志”消息。 为了做这个调整,第三个过程计算的源图是必要的。 当callback需要源地图时,它调用getSourceMap(),它等待sourceMap进程完成其计算,并在等待时间期间控制事件循环,以使其自身能够从源地图进程接收消息(否则事件循环将是被阻止,并且不能收到消息)。

传递给callback函数的消息首先被存储在一个数组中,然后出于性能原因作为批处理发送到主进程。 但是,我们不希望主进程等待消息的时间过长,因此除了在达到阈值时发送一批消息之外,我们还安排了一个函数sendMessagesWithEventLoopTurnAround()在事件循环中运行,并检查是否有消息要发送。 这有两个好处:

  1. 当执行进程正在等待源映射进程时,它可以使用该时间来发送已经获得的消息。 因此,如果sourceMap进程需要几秒钟才能完成,那么主进程不必等待已经创build的消息并包含正确的数据。
  2. 当执行代码在事件循环中只生成非常less的消息时(例如,通过使用setTimeInterval(f, 2000)调度的函数,该函数只在每次执行时创build一个消息),它不必等待很长时间,直到消息缓冲区已满(在这个例子中为200s),但每10ms接收一次关于进度的更新(如果有任何改变的话)。

问题

什么工作

在以下情况下,此设置可以正常工作

  1. 我不使用纤维和一个单独的过程来计算源图。 而是在代码执行之前计算源映射。 在这种情况下,所有要执行的代码都按预期工作。
  2. 我使用光纤和一个单独的过程,并执行代码,我不需要源地图。 例如var a = 2;
    要么
    setTimeout(function () { var a = 2;}, 10)

在第一种情况下,输出看起来像这样。

 Starting source map generation. Message: 'variables init' Message: 'program finished' Sending messages. Finished source map generation. Source map generator process exited with code: 0 after receiving signal: null 
  1. 我使用光纤和一个单独的过程和代码,我需要源映射但不使用事件循环,例如console.log("foo");

在这种情况下,输出如下所示:

 Starting source map generation. Message: 'console log' Waiting for source map generation. Finished source map generation. Wait is over. Message: 'program finished' Sending messages. Source map generator process exited with code: 0 after receiving signal: null 
  1. 我使用光纤和一个单独的过程和代码,我需要源地图,并使用事件循环,但源地图仅在源地图计算已完成(因此不需要等待)时才需要。

例如

 setTimeout(function () { console.log("foo!"); }, 100); // the source map generation takes around 100ms 

在这种情况下,输出如下所示:

 Starting source map generation. Message: 'function declaration' Message: 'program finished' Sending messages. Finished source map generation. Source map generator process exited with code: 0 after receiving signal: null Message: 'function enter' Message: 'console log' Message: 'function exit' Sending messages in event loop. 

什么都行不通

只有当我使用光纤和单独的进程和使用事件循环的代码,但在完成之前需要源映射的情况下,它才会中断

 setTimeout(function () { console.log("foo!"); }, 10); // the source map generation takes around 100ms 

输出结果如下所示:

 Starting source map generation. Message: 'function declaration' Message: 'program finished' Sending messages. Message: 'function enter' Message: 'console log' Waiting for source map generation. /path/to/code/runtime.js:113 Fiber.yield(); ^ getSourceMap (/path/to/code/runtime.js:113:28),callback (/path/to/code/runtime.js:183:9),/path/to/code/utils.js:102:9,Object.console.log (/path/to/code/utils.js:190:13),null._onTimeout (<anonymous>:56:21),Timer.listOnTimeout [as ontimeout] (timers.js:110:15) Child process exited with code: 8 after receiving signal: null 

这里崩溃的过程是执行过程。 但是,我不明白为什么会发生这种情况,或者如何找出这个问题。 正如你在上面看到的,我已经添加了几个日志语句来了解发生了什么。 我也正在监听执行过程中的“未捕获的exception”事件,但似乎并没有被解雇。

另外,我们最终看到的日志消息不是我的,因为我在日志消息前添加了一些描述string,所以它是由node.js本身创build的。 我不明白为什么会出现这种情况,也不知道退出代码8,甚至还可以采取什么措施来缩小原因。 任何帮助将不胜感激。

像往常一样,一旦完成完整描述问题,就会出现一个解决scheme。

我想这个问题是由setTimeout执行的代码不包含在光纤中。 所以在代码中调用Fiber.yield()会崩溃,可以理解。

因此,解决方法是在执行的代码中覆盖setTimeout。 由于我已经提供了一些特殊function的沙箱(例如我自己的控制台对象),我也可以通过将执行的函数包裹在光纤中的setTimeout来实现,如下所示:

 // this being the sandbox object, which si the global object for the executing code this.setTimeout = function (functionToExecute, delay) { return setTimeout(function () { fibers(functionToExecute).run(); }, delay); }; 

此实现不支持将其他parameter passing给setTimeout,但可以将其细化为可扩展。 它也不支持传递一串代码而不是一个函数的setTimeout版本,但是谁又会使用它?

为了使它完全工作,我将不得不交换setTimeout, setInterval, setImmediate and process.nextTick 。 还有什么是通常用来履行这样的angular色?

这只留下了是否有更简单的方法来完成这个function而不是重新实现这些function的问题呢?

Interesting Posts