Рубрики
Без рубрики

Получение контекста за запрос в Nodejs с async_hooks

Получение одной потрясающей функции из PHP в Nodejs: контекст каждого запроса

Автор оригинала: Guilaume Besson.

Я недавно получил проблему, когда я строл HTTP-сервер в Nodejs. Я лежу в вещах во многих местах внутри моей кодовой базы, и у меня есть уникальный идентификатор для каждого запроса. Я хочу добавить этот идентификатор для каждого из моих сообщений журнала, чтобы проследить то, что происходит в каждом запросе. Как эффективно сделать это?

Самый простой способ – пройти этот идентификатор запроса в качестве аргумента в каждой из моих функций. Проблема с этим является то, что она не поддерживалась: если мне 5 функций глубоко в моем стеке, и я хочу воплотить что-то, мне нужно редактировать 5 функций, чтобы добавить аргумент и редактировать каждый вызов функции.

Вы можете всегда передавать объект «контекст» в каждой из ваших функций, но все еще есть проблема. Я использую SQL Lib, который можно настроить для запуска функции, когда обнаруживается длинный запрос, но эта функция называется только с строкой запроса. Я не могу пройти свой контекст запроса.

Если вы готовы использовать экспериментальные Nodejs, у меня действительно элегантное решение для вас благодаря async_hooks Отказ

Некоторые теория о Async_Hooks

async_hooks Модуль разоблачает функции для отслеживания асинхронных ресурсов. Эти крючки называются всякий раз, когда Сетримс Слушатель сервера или любая другая асинхронная задача создана, начата, закончена и уничтожена.

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

Вот как мы можем использовать его для простого сообщения сообщения, когда крюки называются:

const fs = require('fs');
const async_hooks = require('async_hooks');

const log = (str) => fs.writeSync(1, `${str}\n`);

async_hooks.createHook({
  init(asyncId, type, triggerAsyncId) {
    log(`INIT: asyncId: ${asyncId} / type: ${type} / trigger: ${triggerAsyncId}`);
  },
  destroy(asyncId) {
    log(`DESTROY: asyncId: ${asyncId}`);
  },
}).enable();

Вы можете заметить, что мы не используем console.log здесь. Это потому, что console.log это асинхронная операция и будет вызвать крючок, который позвонит console.log Что является асинхронной операцией и будет вызвать крючок … и это случай бесконечной петли. Решение для использования Fs.writeync который синхронно и не запускает один из наших крючков.

Давайте попробуем эти крючки простым Сетримс Пример, в котором мы регистрируем текущий Asyncid снаружи и внутри Сетримс :

log(`>> Calling setTimeout: asyncId: ${async_hooks.executionAsyncId()}`);
setTimeout(() => {
  log(`>> Inside setTimeout callback: asyncId: ${async_hooks.executionAsyncId()}`);
}, 0);
log(`>> Called setTimeout: asyncId: ${async_hooks.executionAsyncId()}`);

При выполнении этого кода мы получим этот выход:

>> Calling setTimeout: asyncId: 1
INIT: asyncId: 2 / type: Timeout / trigger: 1
>> Called setTimeout: asyncId: 1
>> Inside setTimeout callback: asyncId: 2
DESTROY: asyncId: 2

Посмотрим, что здесь произошло.

  • Мы начали с Asyncid равный 1 и называется Сетримс Отказ
  • Это создало Тайм-аут асинхронный ресурс и вызвал наши init крючок с недавно созданным Asyncid из 2 и его родитель Asyncid из 1.
  • Мы регистрировали конец нашей программы с помощью Asyncid все еще равняется 1
  • Тайм-аут Вызов ресурса был вызван, и мы регистрировали текущий Asyncid что равно 2
  • Тайм-аут Ресурс был уничтожен и наш уничтожить Крюк был вызван

Есть также два других крючка: до и после Отказ Их можно использовать для контроля времени некоторых асинхронных ресурсов, таких как внешние HTTP-запросы или SQL-запросы.

Хорошо, но какой смысл?

С ExecutionAsyncid () и init Мы можем воссоздать «стек» наших функций звонков, даже если они были асинхронными.

Вот настоящий пример. Мы создаем HTTP-сервер, чтение и отправка содержимого A test.txt Файл по каждому запросу.

const fs = require('fs');
const async_hooks = require('async_hooks');
const http = require('http');

const log = (str) => fs.writeSync(1, `${str}\n`);

async_hooks.createHook({
  init(asyncId, type, triggerAsyncId) {
    log(`asyncId: ${asyncId} / trigger: ${triggerAsyncId}`);
  },
}).enable();

const readAndSendFile = (res) => {
  fs.readFile('./test.txt', (err, file) => {
    log(`>> Inside readAndSendFile: execution: ${async_hooks.executionAsyncId()}`);
    res.end(file);
  });
}

const requestHandler = (req, res) => {
  log(`>> Inside request: execution: ${async_hooks.executionAsyncId()}`);
  readAndSendFile(res);
}

const server = http.createServer(requestHandler);

server.listen(8080);

Давайте выполним этот код и отправлю два запроса. Я удалил некоторые бесполезные линии от вывода.

>> Inside request: execution: 6
asyncId: 9 / trigger: 6
asyncId: 11 / trigger: 9
asyncId: 12 / trigger: 11
asyncId: 13 / trigger: 12
>> Inside readAndSendFile: execution: 13
[...]
>> Inside request: execution: 31
asyncId: 34 / trigger: 31
asyncId: 36 / trigger: 34
asyncId: 37 / trigger: 36
asyncId: 38 / trigger: 37
>> Inside readAndSendFile: execution: 38

Мы видим, что наши два запроса были назначены два Asyncid : 6 и 31. Чтение нашего файла создано новые async-ресурсы, прозрачные для нашего кода, а затем наши readandsendfile Зарегистрировано два Asyncid : 13 и 38.

От readandsendfile Функция, мы можем получить наш оригинальный запрос Asyncid извлекая нашу «асинхронный путь». Например, для нашего первого запроса мы начинаем с Asyncid равняется 13, а затем мы получаем 13 → 12 → 11 → 9 → 6.

Получить что-то полезное

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

const async_hooks = require('async_hooks');

const contexts = {};

async_hooks.createHook({
  init: (asyncId, type, triggerAsyncId) => {
    // A new async resource was created
    // If our parent asyncId already had a context object
    // we assign it to our resource asyncId
    if (contexts[triggerAsyncId]) {
      contexts[asyncId] = contexts[triggerAsyncId];
    }
  },
  destroy: (asyncId) => {
    // some cleaning to prevent memory leaks
    delete contexts[asyncId];
  },
}).enable();


function initContext(fn) {
  // We force the initialization of a new Async Resource
  const asyncResource = new async_hooks.AsyncResource('REQUEST_CONTEXT');
  return asyncResource.runInAsyncScope(() => {
    // We now have a new asyncId
    const asyncId = async_hooks.executionAsyncId();
    // We assign a new empty object as the context of our asyncId
    contexts[asyncId] = {}
    return fn(contexts[asyncId]);
  });
}

function getContext() {
  const asyncId = async_hooks.executionAsyncId();
  // We try to get the context object linked to our current asyncId
  // if there is none, we return an empty object
  return contexts[asyncId] || {};
};

module.exports = {
  initContext,
  getContext,
};

Давайте напишем небольшой тест, чтобы проверить, все ли он работает правильно.

const {initContext, getContext} = require('./context.js');

const logId = () => {
  const context = getContext();
  console.log(`My context id is: ${context.id}`);
}

initContext((context) => {
  context.id = 1;
  setTimeout(logId, 100);
  setTimeout(logId, 300);
});

initContext((context) => {
  context.id = 2;
  setTimeout(logId, 200);
  setTimeout(logId, 400);
});

Выполняя это, мы получаем:

My context id is: 1
My context id is: 2
My context id is: 1
My context id is: 2

Что дальше?

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

Реальное использование будет создано контекст для каждого HTTP-запроса, генерируя идентификатор запроса и выбирая этот идентификатор внутри нашей функции ведения журнала для печати его на каждой строке. Я также использовал его для запуска каждого вызова базы данных одного HTTP-запроса в той же транзакции SQL.

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

Имейте в виду, что async_hooks все еще экспериментал, но если вы любите жить на краю, идите и попробуйте!

Обложка изображения по EFE Kurnaz На бессплашне