Node.js Волокна и код, запланированные с помощью setTimeout, приводят к сбою

Я использую Fibers для решения проблемы, касающейся того, как передать управление циклу событий в node.js, приостановив выполнение некоторого синхронного кода. В основном это работает хорошо, но я столкнулся со странным сбоем, но я не могу найти причину этого.

Настроить

Есть три процесса:

  • Основной серверный процесс, он получает код для инструментов и выполнения. Когда он получает новый код для его выполнения, используйте child_process.fork() для появления
  • Процесс исполнения. Это позволяет полученному коду время от времени вызывать определенный обратный вызов, чтобы сообщить, что произошло в исполняемом коде. Затем он выполняет код в песочнице, созданной с помощью Contextify. Иногда эти отчеты содержат неверную информацию о местоположении строки и столбца в коде, что-то происходит. В этом случае исходная карта необходима для сопоставления местоположений в инструментальном коде с местоположениями в исходном коде. Но вычисление этой исходной карты занимает значительное время. Следовательно, перед началом выполнения запускается процесс выполнения
  • Процесс расчета карты источника. Это просто берет оригинальный код и инструментированный код и вычисляет исходную карту. Когда это сделано, он отправляет готовую исходную карту в процесс выполнения и завершает работу.

Если процессу выполнения требуется исходная карта в обратном вызове до завершения выполнения, он будет использовать 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);
    }
});

Итак, подведем итог: когда новый код получен, создается новый процесс для его выполнения. Этот процесс сначала инструменты, а затем выполняет его. Перед этим он запускает третий процесс для вычисления исходной карты для кода. Инструментальный код вызывает функцию с именем callback в приведенном выше коде передача сообщений среде выполнения, в которых сообщается о ходе выполнения кода. Иногда их нужно корректировать, например, для которых необходима корректировка - это сообщения "журнала консоли". Для этой корректировки необходима исходная карта, рассчитанная по третьему процессу. Когда обратному вызову требуется карта источника, он вызывает функцию getSourceMap(), которая ожидает, пока процесс sourceMap завершит свои вычисления, и в течение этого времени ожидания передает управление циклу событий, чтобы позволить себе получать сообщения от процесса sourceMap (в противном случае цикл событий будет заблокирован и сообщение не может быть получено).

Сообщения, переданные обратному вызову, сначала сохраняются в массиве, а затем отправляются в виде пакета в основной процесс по соображениям производительности. Однако мы не хотим, чтобы основной процесс слишком долго ждал сообщений, поэтому помимо отправки пакета сообщений при достижении порога мы запланировали функцию sendMessagesWithEventLoopTurnAround() запустить в цикле событий и проверить, есть ли сообщения для отправки. Это имеет два преимущества:

  1. Когда процесс выполнения ожидает процесс карты источника, он может использовать время для отправки уже полученных сообщений. Поэтому, если процессу sourceMap требуется несколько секунд, основной процесс не должен ждать одно и то же время для сообщений, которые уже были созданы и содержат правильные данные.
  2. Когда исполняемый код генерирует только очень мало сообщений в цикле событий (например, функцией, запланированной с setTimeInterval(f, 2000) который создает только одно сообщение за выполнение), ему не нужно долго ждать, пока буфер сообщений не заполнится (в этом примере 200 с), но он получает обновления о ходе выполнения каждые 10 мс (если что-то изменилось).

Эта проблема

Что работает

Эта установка отлично работает в следующих случаях

  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

Здесь происходит сбой процесса выполнения. Однако я не могу выяснить, почему это происходит или как отследить проблему. Как вы можете видеть выше, я уже добавил несколько операторов журнала, чтобы узнать, что происходит. Я также слушаю событие "uncaught исключения" в процессе выполнения, но это, похоже, не срабатывает.

Кроме того, сообщение журнала, которое мы видим в конце, не принадлежит мне, так как я добавляю к сообщениям журнала некоторую строку описания, поэтому оно создается самим node.js. Я не понимаю, почему это происходит, ни какой код выхода 8 или даже что я мог сделать, чтобы сузить причину. Любая помощь будет принята с благодарностью.

1 ответ

Решение

Как обычно, как только кто-то заканчивает описывать проблему полностью, решение представляется.

Проблема, я думаю, в том, что код, выполняемый setTimeout, не заключен в Fiber. Так что вызов Fiber.yield() внутри этого кода завершается ошибкой, понятно.

Поэтому решение состоит в том, чтобы перезаписать setTimeout в исполняемом коде. Поскольку я уже предоставляю изолированную программную среду с некоторыми специальными функциями (например, моим собственным консольным объектом), я также могу обменять реализацию 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);
};

Эта реализация не поддерживает передачу дополнительных параметров в setTimeout, но ее можно легко расширить для этого. Он также не поддерживает версию setTimeout, в которой вместо функции передается строка кода, но кто бы это использовал?

Чтобы заставить его работать полностью, я должен был бы обменяться реализациями setTimeout, setInterval, setImmediate and process.nextTick, Что-нибудь еще, что обычно используется, чтобы выполнить такую ​​роль?

Это только оставляет вопрос, есть ли более простой способ сделать это, чем переопределение каждой из этих функций?

Другие вопросы по тегам