Async Hooks در Node.js: راهنمای کامل

ماژول node:async_hooks در Node.js ابزاری برای ردیابی منابع ناهمگام است. با این حال، این API Experimental محسوب می‌شود و استفاده از آن توصیه نمی‌گردد، زیرا مشکلاتی در کارایی، ایمنی و قابلیت استفاده دارد. برای نیازهای ردیابی کانتکست، بهتر است از AsyncLocalStorage استفاده شود. با این وجود، آشنایی با Async Hooks برای درک چرخهٔ عمر منابع ناهمگام مفید است.

async_hooksexecutionAsyncIdtriggerAsyncIdcreateHookAsyncHook Classinit / before / after / destroy / promiseResolve

~4 دقیقه مطالعه • بروزرسانی ۵ دی ۱۴۰۴

1. مقدمه


Async Hooks امکان ردیابی منابع ناهمگام مانند Promiseها، Timeoutها، TCP connectionها و غیره را فراهم می‌کند. هر منبع ناهمگام دارای یک asyncId یکتا است که چرخهٔ عمر آن را مشخص می‌کند.

2. API اصلی


const async_hooks = require('node:async_hooks');

// شناسهٔ کانتکست جاری
const eid = async_hooks.executionAsyncId();

// شناسهٔ منبعی که باعث ایجاد منبع جدید شده
const tid = async_hooks.triggerAsyncId();

// ساخت یک hook جدید
const hook = async_hooks.createHook({ init, before, after, destroy, promiseResolve });
hook.enable();
hook.disable();

3. Callbackهای Hook


  • init(asyncId, type, triggerAsyncId, resource): هنگام ساخت منبع ناهمگام فراخوانی می‌شود.
  • before(asyncId): درست قبل از اجرای callback منبع.
  • after(asyncId): درست بعد از اجرای callback منبع.
  • destroy(asyncId): هنگام تخریب منبع.
  • promiseResolve(asyncId): هنگام resolve شدن Promise.

4. مدیریت خطا


اگر یکی از callbackها خطا پرتاب کند، فرآیند Node.js خاتمه می‌یابد. این رفتار برای جلوگیری از وضعیت‌های ناایمن در چرخهٔ عمر منابع طراحی شده است.

5. نکات Debugging


چون console.log() خود یک عملیات ناهمگام است، استفاده از آن داخل callbackهای AsyncHook باعث recursion بی‌نهایت می‌شود. برای Debug بهتر است از fs.writeFileSync() استفاده شود.

const fs = require('node:fs');
const util = require('node:util');

function debug(...args) {
  fs.writeFileSync('log.out', `${util.format(...args)}\n`, { flag: 'a' });
}

6. کلاس AsyncHook


کلاس AsyncHook رابطی برای مدیریت چرخهٔ عمر منابع ناهمگام فراهم می‌کند. متدهای enable() و disable() برای فعال/غیرفعال کردن hookها استفاده می‌شوند.

7. مثال عملی


نمونه‌ای از استفاده برای ردیابی رویدادهای TCP Server:

const async_hooks = require('node:async_hooks');
const fs = require('node:fs');
const net = require('node:net');
const { fd } = process.stdout;

let indent = 0;
async_hooks.createHook({
  init(asyncId, type, triggerAsyncId) {
    const eid = async_hooks.executionAsyncId();
    const indentStr = ' '.repeat(indent);
    fs.writeSync(fd, `${indentStr}${type}(${asyncId}): trigger: ${triggerAsyncId} execution: ${eid}\n`);
  },
  before(asyncId) {
    fs.writeSync(fd, `${' '.repeat(indent)}before: ${asyncId}\n`);
    indent += 2;
  },
  after(asyncId) {
    indent -= 2;
    fs.writeSync(fd, `${' '.repeat(indent)}after: ${asyncId}\n`);
  },
  destroy(asyncId) {
    fs.writeSync(fd, `${' '.repeat(indent)}destroy: ${asyncId}\n`);
  },
}).enable();

net.createServer(() => {}).listen(8080);

8. تفاوت executionAsyncId و triggerAsyncId


  • executionAsyncId(): نشان می‌دهد منبع در چه کانتکستی ایجاد شده.
  • triggerAsyncId: نشان می‌دهد کدام منبع باعث ایجاد منبع جدید شده.

نتیجه‌گیری


هرچند استفاده از async_hooks توصیه نمی‌شود، اما درک آن برای شناخت چرخهٔ عمر منابع ناهمگام در Node.js اهمیت دارد. برای ردیابی کانتکست بهتر است از AsyncLocalStorage استفاده شود که پایدارتر و ایمن‌تر است.

1. before(asyncId)


قبل از اجرای callback منبع ناهمگام فراخوانی می‌شود. asyncId شناسهٔ یکتای منبع است. این hook ممکن است 0 تا N بار فراخوانی شود.

  • برای منابع پایدار مانند TCP Server چندین بار اجرا می‌شود.
  • برای عملیات‌هایی مثل fs.open() فقط یک بار اجرا می‌شود.

2. after(asyncId)


بلافاصله پس از پایان اجرای callback فراخوانی می‌شود. حتی اگر خطای uncaught exception رخ دهد، بعد از آن اجرا خواهد شد.

3. destroy(asyncId)


هنگام تخریب منبع فراخوانی می‌شود. اگر منبع به GC وابسته باشد، ممکن است هرگز اجرا نشود و باعث memory leak شود. استفاده از destroy هزینهٔ اضافی دارد زیرا Promiseها را از طریق GC ردیابی می‌کند.

4. promiseResolve(asyncId)


هنگام فراخوانی resolve() در Promise اجرا می‌شود. این hook نشان‌دهندهٔ resolve شدن Promise است، حتی اگر هنوز fulfilled یا rejected نشده باشد.

new Promise((resolve) => resolve(true)).then(() => {});
// Calls: init, promiseResolve, before, after

5. executionAsyncResource()


منبع جاری را برمی‌گرداند. معمولاً یک handle داخلی Node.js است. استفاده از ویژگی‌های آن خطرناک است و ممکن است باعث crash شود.

مثال:

const { open } = require('node:fs');
const { executionAsyncId, executionAsyncResource } = require('node:async_hooks');

console.log(executionAsyncId(), executionAsyncResource()); // 1 {}
open(__filename, 'r', (err, fd) => {
  console.log(executionAsyncId(), executionAsyncResource()); // 7 FSReqWrap
});

6. executionAsyncId()


شناسهٔ کانتکست جاری را برمی‌گرداند. این شناسه مربوط به زمان اجراست، نه علت ایجاد منبع.

const async_hooks = require('node:async_hooks');
console.log(async_hooks.executionAsyncId()); // 1 bootstrap

7. triggerAsyncId()


شناسهٔ منبعی را برمی‌گرداند که باعث اجرای callback جاری شده است.

const server = net.createServer((conn) => {
  async_hooks.triggerAsyncId(); // ID of connection
}).listen(port);

8. asyncWrapProviders


از نسخهٔ v17.2.0 اضافه شده است. یک map از انواع providerها و شناسهٔ عددی آن‌ها برمی‌گرداند. جایگزین process.binding('async_wrap').Providers است.

9. Promise Execution Tracking


به‌طور پیش‌فرض Promiseها شناسهٔ asyncId دریافت نمی‌کنند. با فعال‌سازی hookها می‌توان آن‌ها را ردیابی کرد.

const { createHook, executionAsyncId, triggerAsyncId } = require('node:async_hooks');

createHook({ init() {} }).enable();
Promise.resolve(1729).then(() => {
  console.log(`eid ${executionAsyncId()} tid ${triggerAsyncId()}`);
});

در این مثال، Promise اول asyncId=6 و Promise دوم asyncId=7 دارد.

نتیجه‌گیری


Hookهای before، after، destroy و promiseResolve امکان ردیابی دقیق چرخهٔ عمر منابع ناهمگام را فراهم می‌کنند. متدهای executionAsyncResource، executionAsyncId و triggerAsyncId برای شناسایی کانتکست‌های جاری و منابع محرک حیاتی هستند. هرچند استفاده از async_hooks در تولید توصیه نمی‌شود، اما برای درک عمیق رفتار ناهمگام Node.js بسیار ارزشمند است.

نوشته و پژوهش شده توسط دکتر شاهین صیامی