الخطافات غير المتزامنة في Node.js

من موسوعة حسوب
اذهب إلى التنقل اذهب إلى البحث

الاستقرار: 1-تجريبي

توفِّر الوحدة async_hooks واجهة برمجية (API) لتسجيل دوال ردود النداء التي تتعقَّب دورة حياة (lifetime) الموارد غير المتزامنة (asynchronous resources) المُنشأَة داخل تطبيقات Node.js. يمكن الوصول إلى هذه الوحدة باستعمال الأمر التالي:

const async_hooks = require('async_hooks');

اصطلاحات

يمثِّل المورد غير المتزامن كائنًا مرفقًا به رد نداء. قد يُستدعَى رد النداء هذا عدَّة مرات، مثل الحدث 'conection' في net.createServer()‎، أو مرةً واحدة فقط، مثل fs.open()‎. يمكن أن يُغلق المورد أيضًا قبل أن يُستدعَى رد النداء. لا تفرِّق الوحدة AsyncHook بشكل واضح بين هاتين الحالتين المختلفتين، ولكن ستمثلهما بمفهوم مجرَّد بوصفهما موردًا.

إن استُعمِل الصنف worker، فسيكون لكل خيط (thread) واجهة async_hooks مستقلة وستُستعمَل مجموعةٌ جديدةٌ من المعرِّفات غير المتزامنة (async IDs).

الواجهات البرمجية العامة (Public API)

نظرة سريعة

تشرح الشيفرة التالية بشكل سريع وشامل الواجهات البرمجيَّة العامَّة:

const async_hooks = require('async_hooks');

// تعيد مُعرِّف محتوى التنفيذ الحالي
const eid = async_hooks.executionAsyncId();

// تعيد معرِّف المعالج المراد استدعاؤه والمسؤول عن 
// استدعاء رد النداء لمجال التنفيذ الحالي
const tid = async_hooks.triggerAsyncId();

// .جميع ردود النداء هذه هي اختيارية .AsyncHook إنشاء نسخة جديدة من
const asyncHook =
    async_hooks.createHook({ init, before, after, destroy, promiseResolve });

// هذه. هذا ليس فعلًا ضمنيًا AsyncHook السماح باستدعاء ردود نداء النسخة
// .ينفَّذ بعد تشغيل الدالة البانية، ويجب التصريح به بشكل واضح لبدء تنفيذ ردود النداء
asyncHook.enable();

// تعطيل استعمال الأحداث غير المتزامنة الجديدة
asyncHook.disable();

//
// createHook() ما يلي هو ردود النداء التي يمكن تمريرها إلى
//


//  خلال إنشاء الكائن. ربما لم يكتمل إنشاء المورد بعد عندما ينفَّذ رد النداء init يستدعى رد النداء
// .قد مُلئت بعد "asyncId" هذا، لذا لا تكون جميع حقول المورد المشار إليه من قبل
function init(asyncId, type, triggerAsyncId, resource) { }

// N فقط قبل استدعاء رد نداء المورد. يمكن أن يستدعى من 0 إلى before يستدعى رد النداء 
// بينما يستدعى مرة واحدة فقط مع ،TCPWrap مرة مع المعالجات مثل
// FSReqWrap الطلبات مثل
function before(asyncId) { }

// فقط بعد أن يكون رد نداء المورد قد اكتمل after يستدعى رد النداء
function after(asyncId) { }

// AsyncWrap عندما تدمر نسخةٌ من destroy  يستدعى رد النداء
function destroy(asyncId) { }

// عندما تمرر Promise فقط من أجل الموارد promiseResolve يستدعى 
// الباني المستدعى `Promise` إلى الكائن `resolve` الدالة
// .(Promise إما بشكل مباشرةً أو عبر وسائل أخرى لاستبيان الكائن)
function promiseResolve(asyncId) { }

async_hooks.createHook(callbacks)‎

أضيفت في الإصدار: v8.1.0

  • callbacks: ‏<Object> ردود نداء الخطاف (Hook Callbacks) المراد تسجيلها، ويمكن أن يحوي إحدى الدوال التالية:

تعيد هذه الدالة نسخةً من خطاف غير متزامن AsyncHook يستعمل لتعطيل وتشغيل الخطافات.

تسجِّل الدالة async_hooks.createHook(callbacks)‎ الدوال المراد استدعاؤها في مختلف أوقات تنفيذ أحداث كل عمليَّة غير متزامنة.

تُستدعَى ردود النداء init()‎، و before()‎، و after()‎، و destroy()‎ للحدث غير المتزامن الخاص بها خلال فترة تواجد المورد.

جميع ردود النداء اختياريَّة. إن احتجت مثلًا إلى تعقُّب عمليَّة مسح مورد فقط، فمرِّر رد النداء destroy. مواصفات جميع الدوال التي يمكن تمريرها إلى callbacks موجودةٌ في القسم "ردود نداء الخطاف" الذي سيأتي ذكره لاحقًا.

const async_hooks = require('async_hooks');

const asyncHook = async_hooks.createHook({
  init(asyncId, type, triggerAsyncId, resource) { },
  destroy(asyncId) { }
});

انتبه إلى أنَّ ردود النداء ستُورَّث بوساطة سلسلة prototype:

class MyAsyncCallbacks {
  init(asyncId, type, triggerAsyncId, resource) { }
  destroy(asyncId) {}
}

class MyAddedCallbacks extends MyAsyncCallbacks {
  before(asyncId) { }
  after(asyncId) { }
}

const asyncHook = async_hooks.createHook(new MyAddedCallbacks());
معالجة الأخطاء

إن رُميت أيَّة ردود النداء AsyncHook، فسيطبع التطبيق تقرير متعقِّب المكدس (stack trace) ويُغلق بعدها. يتبع مسار الخروج نفس مسار الاستثناء uncaught، ولكن يحذف جميع المستمعين 'uncaughtException' وتجبر العمليَّة بذلك على الخروج. لا تزال تُستدعَى ردود النداء 'exit' إلا إذ شُغِّل البرنامج مع الراية ‎--abort-on-uncaught-exception وفي هذه الحالة سيُطبَع تقرير متعقِّب المكدس وسيُغلَق التطبيق مغادرًا ملف النواة.

السبب الذي يقبع خلف سلوك معالج الخطأ بهذا الشكل هو عمل ردود النداء تلك في نقاط متقلِّبة ومؤقَّتة غالبًا أثناء وجود الكائن مثل أثناء إنشاء صنف وإزالته. نتيجةً لذلك، يُعدُّ من الضروري إيقاف هذه العمليَّة بسرعة لمنع حصول أيَّة مقاطعة غير مقصودة في المستقبل. سيخضع هذا الأمر للتغيير إن أُجرِي تحليل شامل وتم التأكد من أنَّ الاستثناءات يمكنها أن تتعقب مسار التحكم الطبيعي دون حصول أي تأثيرات جانبيَّة غير متوقَّعة.

الطباعة في ردود النداء AsyncHooks

لمَّا كانت الطباعة في الطرفية عمليةً غير متزامنة، فستتسبَّب console.log()‎ في أن تستدعى ردود النداء AsyncHooks؛ لذا، سيُسبِّب استعمال console.log()‎ أو أيَّة عمليَّة غير متزامنة مشابهة داخل دالة رد النداء AsyncHooks عودية لا نهاية. هنالك حل بسيط لهذا الأمر عند تصحيح الأخطاء هو استعمال عمليَّة تسجيل (logging) غير متزامنة مثل fs.writeSync(1, msg)‎. سيطبع هذا على مجرى الخرج القياسي (stdout)، إذ يُمثَّل 1 في واصف الملف (file descriptor) بمجرى الخرج القياسي ولن يستدعي ردود النداء AsyncHooks بشكل تعاودي لأنَّها متزامنة.

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

function debug(...args) {
  // AsyncHooks استعمل دالةً مثل هذه عند تصحيح الأخطاء داخل رد النداء 
  fs.writeSync(1, `${util.format(...args)}\n`);
}

إن دعت الحاجة إلى استعمال عمليَّة غير متزامنة للتسجيل (logging)، فمن الممكن تعقُّب الذي سبب العمليَّة غير المتزامنة باستمرار عبر استعمال المعلومات التي توفِّرها AsyncHooks نفسها. يجب تخطي عمليَّة التسجيل نفسها عندما تكون هي السبب وراء استدعاء رد النداء AsyncHooks. عند تنفيذ ذلك، ستتحطم التعاودي اللانهائيَّة.

asyncHook.enable()‎

تعيد هذه الدالة مرجعًا إلى asyncHook، إذ تُفعَّل ردود النداء للنسخة AsyncHook المعطاة. إن لم يعطَ أي رد نداء، فلن يكون لاستدعاء هذه الدالة أي تأثير.

تكون النسخة AsyncHook المنشأة معطَّلة افتراضيًّا. لتفعيلها مباشرةً بعد إنشائها، يمكنك اتباع النموذج التالي:

const async_hooks = require('async_hooks');
const hook = async_hooks.createHook(callbacks).enable();

asyncHook.disable()‎

تعيد هذه الدالة مرجعًا إلى asyncHook، إذ تعطِّل ردود النداء للنسخة asyncHook المعطاة من مجموعة (pool) ردود النداء asyncHook العامَّة المراد تنفيذها. لن يستدعَ الخطاف مرةً أخرى متى ما عُطِّل حتى يفعَّل من جديد.

من أجل تناسق الواجهة البرمجية (API)، تعيد disable()‎ نسخةً من AsyncHook أيضًا.

ردود نداء الخطاف (Hook Callbacks)

صُنِّفَت الأحداث key خلال فترة وجود الأحداث غير المتزامنة إلى أربعة أقسام هي: أثناء الإنشاء، وقبل استدعاء رد النداء، وبعد استدعاء رد النداء، وعند إزالة النسخة.

init(asyncId, type, triggerAsyncId, resource)‎
  • asyncId: ‏<number> عددٌ يمثِّل معرِّفًا (ID) فريدًا للمورد غير المتزامن.
  • type: ‏<string> سلسلةٌ نصيةٌ تمثِّل نوع المورد غير المتزامن.
  • triggerAsyncId: ‏<number> عددٌ يمثِّل معرفًا فريدًا للمورد غير المتزامن الذي يخص سياق التنفيذ حيث أنشئ هذا المورد ضمنه.
  • resource:‏ <Object> كائنٌ يحوي مرجعًا إلى المورد الذي يمثِّل العمليَّة غير المتزامنة، وهو الذي يُطلَق أثناء عمليَّة الإزالة.

تُستدعَى هذه الدالة عند إنشاء صنفٍ قادرٍ على إطلاق حدث غير متزامن. هذا لا يعني أنَّه ينبغي على النسخة استدعاء رد النداء before ورد النداء after قبل أن يُستدعَى رد النداء destroy وإنَّما يمتلك المقدرة على ذلك فقط.

يمكن أن يُراقَب هذا السلوك بفعل شيء مثل فتح موردٍ ثمَّ إغلاقه قبل أن يُستعمَل. توضِّح قطعة الشيفرة التالية ذلك:

require('net').createServer().listen(function() { this.close(); });
// أو
clearTimeout(setTimeout(() => {}, 10));

يسند لكل مورد جديد مُعرِّف (ID) فريد ضمن مجال نسخة Node.js الحاليَّة.

المعامل type

المعامل type هو سلسلة نصيَّة تُعرِّف نوع المورد الذي أدى إلى استدعاء رد النداء init. عمومًا، سيتطابق مع اسم باني المورد.

FSEVENTWRAP, FSREQWRAP, GETADDRINFOREQWRAP, GETNAMEINFOREQWRAP, HTTPPARSER,
JSSTREAM, PIPECONNECTWRAP, PIPEWRAP, PROCESSWRAP, QUERYWRAP, SHUTDOWNWRAP,
SIGNALWRAP, STATWATCHER, TCPCONNECTWRAP, TCPSERVER, TCPWRAP, TIMERWRAP, TTYWRAP,
UDPSENDWRAP, UDPWRAP, WRITEWRAP, ZLIB, SSLCONNECTION, PBKDF2REQUEST,
RANDOMBYTESREQUEST, TLSWRAP, Timeout, Immediate, TickObject

هنالك أيضًا نوع المورد PROMISE الذي يُستعمَل لتعقُّب نُسَخ الكائن Promise والعمل غير المتزامن المُجَدْوَل من طرفهم.

يستطيع المستخدمون أن يعرِّفوا نوعهم الخاص بهم عند استعمال الواجهة البرمجيَّة Embedder (API)‎ العامَّة.

من الممكن حصول تعارض في أسماء الأنواع، لذا ينصح باستعمال بادئة فريدة، مثل اسم الحزمة npm، لمنع أي تعارض عند الاستماع إلى الخطافات.

المعامل triggerAsyncId

المعامل triggerAsyncId هو المُعرِّف asyncId للمورد الذي تسبَّب في تهيئة المورد الجديد والذي أدى بعدها إلى استدعاء رد النداء init. هذا مختلفٌ عن async_hooks.executionAsyncId()‎ الذي يُظهِر متى أنشئ المورد بينما يُظهِر triggerAsyncId لماذا أنشئ المورد.

الشيفرة التالية هي شرح عملي بسيط حول المعامل triggerAsyncId:

async_hooks.createHook({
  init(asyncId, type, triggerAsyncId) {
    const eid = async_hooks.executionAsyncId();
    fs.writeSync(
      1, `${type}(${asyncId}): trigger: ${triggerAsyncId} execution: ${eid}\n`);
  }
}).enable();

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

سيعطي هذا المثال النتيجة التالية عند إرسال nc localhost 8080 إلى الخادم:

TCPSERVERWRAP(2): trigger: 1 execution: 1
TCPWRAP(4): trigger: 2 execution: 0

نجد أنَّ TCPSERVERWRAP هو الخادم الذي يستقبل الاتصالات، وأنَّ TCPWRAP هو اتصال جديد من العميل. عندما يُؤسَّس اتصال جديد، ستُنشَأ نسخةٌ من TCPWrap مباشرةً وهذا يحدث خارج مكدس JavaScript (القيمة 0 التي أعادها executionAsyncId()‎ تشير إلى أنه يُنفَّذ الآن من C++‎ دون وجود مكدس JavaScript فوقه). مع تلك المعلومات فقط، يستحيل ربط الموارد مع بعضهم بعضًا من طرف من تَسبَّب في إنشائهم، لذا يعطى triggerAsyncId مهمة نشر (propagate) أيَّ مورد مسؤول عن وجود المورد الجديد.

المعامل resource

المعامل resource هو كائنٌ يمثِّل المورد غير المتزامن الفعلي الذي قد تمت تهيئته، ويمكن أن يحتوي على معلوماتٍ مفيدة تختلف باختلاف قيمة المعامل type. على سبيل المثال، يوفِّر المعامل resource من أجل نوع المورد GETADDRINFOREQWRAP اسم المضيف (hostname) المُستعمَل عند البحث عن العنوان IP الخاص به في net.Server.listen()‎. لا تُعدُّ الواجهة البرمجيَّة (API) التي توفِّر الوصول إلى هذه المعلومات عامَّةً حاليًا. مع ذلك، يستطيع المستخدمون باستعمال الواجهة البرمجيَّة المُضمِّنة (Embedder API) توفير وتوثيق الكائنات resource الخاصَّة بهم. فيمكن مثلًا توفير كائن resource يستطيع أن يحوي تعليمة SQL لكي تُنفَّذ حينذاك.

في حالة الوعود (Promises)، سيكون للكائن resource الخاصِّيَّة promise التي تشير إلى الكائن Promise الذي يُهيَّأ آنذاك، والخاصِّيَّة isChainedPromise التي تُضبَط إلى القيمة true إن كان للكائن Promise كائن أب من النوع Promise أيضًا أو إلى القيمة false خلاف ذلك. فإذا نظرنا مثلًا إلى b = a.then(handler)‎، نجد أنَّ a يُعدُّ كائن Promise أب للكائن b؛ ويُعدُّ b هنا وعدًا متسلسلًا (chained promise).

في بعض الأحيان، يُعاد استعمال الكائن resource لأسباب متعلقة بالأداء إلا أنَّه من غير الآمن استعماله كمفتاح في الكائن WeakMap أو إضافة خاصِّيَّات إليه.

مثال عن سياق غير متزامن

يوضح المثال التالي كيفيَّة استدعاء رد النداء init بين استدعاء ردي النداء before و after، وكيف سيبدو رد النداء للدالة listen()‎ تحديدًا. تنسيق المخرجات مفصَّلة بشكل واسع لتسهيل قراءة سياق الاستدعاء.

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

require('net').createServer(() => {}).listen(8080, () => {
  // Let's wait 10ms before logging the server started.
  setTimeout(() => {
    console.log('>>>', async_hooks.executionAsyncId());
  }, 10);
});

ستظهر المخرجات التالية بدءًا من عمل الخادم:

TCPSERVERWRAP(2): trigger: 1 execution: 1
TickObject(3): trigger: 2 execution: 1
before:  3
  Timeout(4): trigger: 3 execution: 3
  TIMERWRAP(5): trigger: 3 execution: 3
after:   3
destroy: 3
before:  5
  before:  4
    TTYWRAP(6): trigger: 4 execution: 4
    SIGNALWRAP(7): trigger: 4 execution: 4
    TTYWRAP(8): trigger: 4 execution: 4
>>> 4
    TickObject(9): trigger: 4 execution: 4
  after:   4
after:   5
before:  9
after:   9
destroy: 4
destroy: 9
destroy: 5

كما هو موضَّح في المثال، يُحدِّد كلًا من executionAsyncId()‎ و execution قيمة سياق التنفيذ الحالي التي توصف بدقة عبر استدعاء before و after. استعمل execution لوضع نتائج تخصيص موردٍ في مخطط بالشكل التالي:

TTYWRAP(6) -> Timeout(4) -> TIMERWRAP(5) -> TickObject(3) -> root(1)

لا يُعدُّ TCPSERVERWRAP جزءًا من هذا المخطط رغم أنَّه كان سببًا في استدعاء console.log()‎. هذا بسبب أنَّ عمليَّة ربط منفذٍ دون وجود اسم مضيف هي عمليَّة متزامنة. على أي حال، لإبقاء الواجهة البرمجية غير متزامنة بشكل كامل، يجب وضع رد نداء المستخدم في process.nextTick()‎.

يُظهِر المخطط متى أُنشِأ المورد فقط ولا يُظهِر كيفيَّة ذلك، لذا استعمل triggerAsyncId لتعقُّب كيفيَّة الإنشاء.

before(asyncId)‎
  • asyncId: ‏<number> عددٌ يمثل معرِّفًا (ID) فريدًا للمورد غير المتزامن.

عندما تبدأ عمليةٌ غير متزامنة (مثل استقبال خادم TCP اتصالًا جديدًا) أو تصبح مكتملة (مثل كتابة بيانات إلى القرص الصلب)، يُستدعَى رد نداءٍ لتنبيه المستخدم ولكن يستدعى رد النداء before فقط قبل تنفيذه، إذ يُمرَّر إليه المُعرِّف الفريد الذي أُسنِد إلى المورد الذي على وشك تنفيذ رد النداء.

سيُستدعَى رد النداء before من 0 إلى N مرة، إذ لن يستدعى مطلقًا (أي 0 مرة) إن ألغيت العمليَّة غير المتزامنة أو لم يستلم خادم TCP مثلًا أي اتصال.

ستستدعي الموارد المستمرة -مثل خادم TCP- رد النداء before عدَّة مرات عادةً بينما ستستدعيه الموارد الأخرى -مثل fs.open()‎- مرةً واحدةً.

after(asyncId)‎
  • asyncId: ‏<number> عددٌ يمثل معرِّفًا (ID) فريدًا للمورد غير المتزامن.

يُستدعَى رد النداء after بعد اكتمال رد النداء المحدَّد في before مباشرةً.

إن وقع الاستثناء uncaught خلال تنفيذ رد النداء، فسيُنفَّذ after حينئذ ٍبعد أن يُطلَق الحدث 'uncaughtException' أو ينفَّذ معالج النطاق domain.

destroy(asyncId)‎
  • asyncId: ‏<number> عددٌ يمثِّل معرِّفًا (ID) فريدًا للمورد غير المتزامن.

يستدعى رد النداء destroy بعد تدمير المورد المقابل للمعامل asyncId المعطى. يستدعى أيضًا بشكل غير متزامن من الواجهة البرمجية المُضمِّنَة (embedder) عبر emitDestroy()‎.

تعتمد بعض الموارد على مجموعة المخلفات (garbage collection) المراد مسحها، لذا إن أُنشِئ موردٌ للكائن resource المُمرَّر إلى رد النداء init، فمن المحتمل أن لا يُستدعَى رد النداء destroy مطلقًا محدثًا بذلك تسربًا في الذاكرة ضمن التطبيق. لا يؤخذ هذا الأمر بالحسبان إن لم يكن المورد يعتمد على تلك المخلفات.

promiseResolve(asyncId)‎
  • asyncId: ‏<number> عددٌ يمثل معرِّفًا (ID) فريدًا للمورد غير المتزامن.

يستدعى رد النداء هذا عندما تُستدعَى الدالة resolve()‎ المُمرَّرة إلى الكائن Promise الباني (سواءً بشكل مباشر أو عبر طرائق أخرى لاستبيان الكائن Promise).

انتبه إلى أنَّ الدالة resolve()‎ لا تجري أي عمل متزامن ملحوظ.

ليس بالضرورة أن يكتمل الكائن Promise أو يُرفَض عند هذه النقطة إن كان قد قُبِلَ بناءً على افتراض حالة معيَّنة لكائن Promise آخر.

new Promise((resolve) => resolve(true)).then((a) => {});

استدعِ ردود النداء التالية:

init for PROMISE with id 5, trigger id: 1
  promise resolve 5      # resolve(true) هذا يقابل
init for PROMISE with id 6, trigger id: 5  # Promise الكائن then() تعيد الدالة
  before 6                     # then() أعطي رد النداء
  promise resolve 6      # promise بدوره الكائن then() يقبل رد النداء 
  after 6

async_hooks.executionAsyncId()‎

أضيفت في الإصدار: v8.1.0

تعيد هذه الدالة العدد asyncId الذي يمثِّل معرفًا فريدًا لسياق التنفيذ الحالي، وهو مفيد في عمليَّة التعقُّب عند استدعاء أي شيء.

const async_hooks = require('async_hooks');

console.log(async_hooks.executionAsyncId());  // 1 - bootstrap
fs.open(path, 'r', (err, fd) => {
  console.log(async_hooks.executionAsyncId());  // 6 - open()
});

يتعلق المُعرِّف الذي تعيده الدالة executionAsyncId()‎ بوقت التنفيذ وليس بمسبِّباته (استعمل triggerAsyncId()‎ من أجل معرفة المسببات):

const server = net.createServer(function onConnection(conn) {
  // يعاد معرف الخادم وليس الاتصال الجديد لأن 
  // يعمل في مجال onConnection رد النداء
  // MakeCallback() تنفيذ الخادم
  async_hooks.executionAsyncId();

}).listen(port, function onListening() {
  // لأن (process.nextTick() مثل) TickObject يعاد معرف الكائن 
  // nextTick() مغلفةٌ في .listen() جميع ردود النداء المُمرَّرة إلى
  async_hooks.executionAsyncId();
});

انتبه إلى أنَّ السياق Promise قد لا يجلب قيمة صحيحة للمُعرِّف executionAsyncIds بشكل افتراضي. (انظر قسم "تعقُّب تنفيذ الوعد" اللاحق.)

async_hooks.triggerAsyncId()‎

تعيد هذه الدالة مُعرِّف المورد المسؤول عن استدعاء رد النداء الذي يُنفَّذ حاليًا.

const server = net.createServer((conn) => {
  // كان المورد المتسبب في استدعاء رد النداء هذا هو ذلك الاتصال 
  // triggerAsyncId() الجديد. بناءً على ذلك، القيمة التي تعيدها
  // "conn" هي معرف الاتصال
  async_hooks.triggerAsyncId();

}).listen(port, () => {
  // إلا ،nextTick() مغلفةٌ في .listen() رغم أن جميع ردود النداء المُمرَّرة إلى
  // .listen() أنَّ وجود رد النداء نفسه عائد إلى استدعاء الخادم
  // نتيجة لذلك، سيعاد مُعرِّف هذا الخادم
  async_hooks.triggerAsyncId();
});

انتبه إلى أنَّ السياق Promise قد لا يجلب قيمة صحيحة للمُعرِّف executionAsyncIds بشكل افتراضي. (انظر قسم "تعقب تنفيذ الوعد" اللاحق.)

تعقب تنفيذ الوعد (Promise execution tracking)

لا يُسنَد لتنفيذ الوعد أيًّا من المُعرِّفات asyncId نتيجةً للطبيعة المكلفة لواجهة عمليات التحقُّق الداخلية البرمجيَّة للكائن Promise التي أُوجدَت في الإصدار v8. هذا يعني أنَّ البرامج التي تستعمل الوعود، أو المعامل await، أو async لن تجلب قيمة صحيحة لمُعرِّفات التنفيذ والاستدعاء لسياق ردود النداء Promise بشكل افتراضي.

إليك هذا المثال:

const ah = require('async_hooks');
Promise.resolve(1729).then(() => {
  console.log(`eid ${ah.executionAsyncId()} tid ${ah.triggerAsyncId()}`);
});
// :سينتج
// eid 1 tid 0

لاحظ أنَّ رد النداء then()‎ يتطلَّب أن يُنفَّذ في سياق المجال الخارجي (outer scope) رغم وجود عقدة غير متزامنة (asynchronous hop) مشتركة. لاحظ أيضًا أنَّ قيمة المُعرِّف triggerAsyncId هي 0 وهذا يعني أنَّنا نفقد سياقًا عن المورد الذي تسبب في استدعاء رد النداء then()‎ لتنفيذه. إنَّ تثبيت الخطافات غير المتزامنة عبر async_hooks.createHook يُفعِّل تعقُّب تنفيذ الوعد. تفحَّص مثلًا الشيفرة التالية:

const ah = require('async_hooks');
ah.createHook({ init() {} }).enable(); // رغمًا عنه PromiseHooks يُفعَّل
Promise.resolve(1729).then(() => {
  console.log(`eid ${ah.executionAsyncId()} tid ${ah.triggerAsyncId()}`);
});
// :سينتج
// eid 7 tid 6

في هذا المثال، إضافة أيَّة دالة خطاف فعليَّة يُفعِّل تعقُّب الوعود. هنالك وعدين في هذا المثال؛ أولهما أنشئ عبر Promise.resolve()‎، وثانيهما أعيد عبر استدعاء then()‎. كانت قيمة المُعرِّف asyncId للوعد الأول هي 6 وللوعد الثاني هي 7. أثناء تنفيذ رد النداء then()‎، كنا نُنفِّذ حينذاك في سياق الوعد الذي يملك القيمة 7 للمُعرِّف asyncId. استُدعِي هذا الوعد عبر المورد 6 غير المتزامن.

أمر آخر متعلق بدقة الوعود هو أنَّ ردي النداء before و after لا ينفذان إلا مع الوعود المتسلسلة (chained promises). هذا يعني أن الوعود التي لا تنشئها الدالة then()‎ أو الدالة catch()‎ لن يُطلَق معها ردي النداء before و after. للمزيد من المعلومات، راجع تفاصيل الإصدار V8 للواجهة البرمجية PromiseHooks.

الواجهة البرمجية المُضمِّنة (Embedder API) في JavaScript

هي مكتبةٌ للمطورين تسمح لهم بالتعامل مع أداء المهام للموارد غير المتزامنة الخاصَّة بهم مثل المدخلات والمخرجات (I/O)، أو تجمُّع الاتصالات (connection pooling)، أو إدارة طوابير ردود النداء التي قد تتطلب الحاجة إلى استعمال الواجهة البرمجية AsyncWrap من JavaScript لاستدعاء جميع ردود النداء المناسبة.

الصنف AsyncResource

صُمِّم الصنف AsyncResource ليكون قابلًا للتوسع بوساطة الموارد المُضمِّنَة (embedder) غير المتزامنة. عند استعماله، يستطيع المستخدمون بسهولة استدعاء الأحداث في أي وقت مع الموارد الخاصَّة بهم.

سيُستدعَى رد النداء init عندما ينشأ نسخةٌ من الصنف AsyncResource. تعطي الشيفرة التالية نظرة عامة عن الواجهة البرمجيَّة AsyncResource:

const { AsyncResource, executionAsyncId } = require('async_hooks');

// سيُوسَّع. يؤدي إنشاء نسخة جديدة AsyncResource() هذا يعني أنَّ
// إن لم يعطَ المعامل .init إلى استدعاء رد النداء AsyncResource() من
// async_hook.executionAsyncId() فسيستعمل ،triggerAsyncId
const asyncResource = new AsyncResource(
  type, { triggerAsyncId: executionAsyncId(), requireManualDestroy: false }
);

// تشغيل دالةٍ في سياق تنفيذ المورد يؤدي إلى
// * إنشاء سياق المورد
// * قبل ردود النداء AsyncHooks استدعاء
// * المعطاة مع الوسائط المتوافرة `fn` استدعاء الدالة
// * بعد ردود النداء AsyncHooks استدعاء
// * استرجاع سياق التنفيذ الأصلي
asyncResource.runInAsyncScope(fn, thisArg, ...args);

// يدمر ردود النداء AsyncHooks استدعاء
asyncResource.emitDestroy();

// AsyncResource إعادة المعرف الفريد المسند للنسخة
asyncResource.asyncId();

// AsyncResource للنسخة (triggerAsyncId أو المعرف ،trigger ID) إعادة معرف الاستدعاء
asyncResource.triggerAsyncId();

AsyncResource(type[, options])‎

  • type: ‏<string> سلسلةٌ نصيٌة تمثِّل نوع الحدث غير المتزامن.
  • options: ‏<Object> كائنٌ من النوع Object يحتوي على إحدى الخيارات التالية:
    • triggerAsyncId: ‏<number> عددٌ يمثِّل مُعرِّف سياق التنفيذ الذي أنشأ هذا الحدث غير المتزامن. القيمة الافتراضيَّة هي: executionAsyncId()‎.
    • requireManualDestroy: ‏<boolean> قيمةٌ منطقيَّةٌ تعطِّل أو تفعِّل emitDestroy تلقائيًّا عندما يُجلَب الكائن من المهملات. لا يُضبَط هذا الخيار عادةً (حتى لو استدعيت emitDestroy يدويًّا) إلا إذا أعيد المُعرِّف asyncId للمورد ثمَّ استُعمِل مع الواجهة البرمجيَّة emitDestroy. القيمة الافتراضيَّة هي: false.

تفحَّص المثال العملي التالي الذي يشرح استعمال هذه الدالة:

class DBQuery extends AsyncResource {
  constructor(db) {
    super('DBQuery');
    this.db = db;
  }

  getInfo(query, callback) {
    this.db.get(query, (err, data) => {
      this.runInAsyncScope(callback, null, err, data);
    });
  }

  close() {
    this.db = null;
    this.emitDestroy();
  }
}

asyncResource.runInAsyncScope(fn[, thisArg, ...args])‎

أضيفت في الإصدار: v9.6.0

  • fn: ‏<Function> دالةٌ يراد استدعاؤها في سياق التنفيذ لهذا المورد المتزامن.
  • thisArg: ‏<any> أيُّ نوعٍ من أنواع بيانات JavaScript، ويمثِّل المُستقبِل (recever) المراد استخدامه مع استدعاء الدالة.
  • ...args: ‏<any> أي نوع من أنواع بيانات JavaScript، ويمثِّل الوسائط الإضافيَّة المراد تمريرها إلى الدالة.

تفحَّص المثال العملي التالي الذي يشرح استعمال هذه الدالة:

class DBQuery extends AsyncResource {
  constructor(db) {
    super('DBQuery');
    this.db = db;
  }

  getInfo(query, callback) {
    this.db.get(query, (err, data) => {
      this.runInAsyncScope(callback, null, err, data);
    });
  }

  close() {
    this.db = null;
    this.emitDestroy();
  }
}

asyncResource.runInAsyncScope(fn[, thisArg, ...args])‎

أضيفت في الإصدار: v9.6.0

  • fn: ‏<Function> دالةٌ يراد استدعاؤها في سياق تنفيذ هذا المورد غير المتزامن.
  • thisArg: ‏<any> أيُّ نوعٍ من أنواع بيانات JavaScript، ويمثِّل المُستقبِل (recever) المراد استخدامه مع استدعاء الدالة.
  • ...args: ‏<any> أي نوع من أنواع بيانات JavaScript، ويمثِّل الوسائط الإضافيَّة المراد تمريرها إلى الدالة.

تستدعي هذه الدالةُ الدالةَ المُمرَّرة إليها مع الوسائط المعطاة في سياق التنفيذ للمورد غير المتزامن. هذا سيؤدي إلى إنشاء السياق، ثمَّ إطلاق AsyncHooks قبل ردود النداء، ثمَّ استدعاء الدالة، ثمَّ إطلاق AsyncHooks بعد ردود النداء، ثمَّ استعادة سياق التنفيذ الأصلي.

asyncResource.emitBefore()‎

أهملت منذ الإصدار: v9.6.0

الاستقرار: 0-مهمل: استعمل الدالة asyncResource.runInAsyncScope()‎ عوضًا عنها.

تستدعي هذه الدالة جميع ردود النداء before للتنبيه ببدء إنشاء سياق تنفيذ جديد. إن استُدعيَت emitBefore()‎ عدة مرات، فسيُتعقَّب المُعرِّفات asyncId في المكدس وسيُتخلَّص منها بالترتيب الذي استُدعيَت به.

يجب إزالة ردي النداء before و after بنفس الترتيب الذي استدعيا به وإلا سيًطلَق استثناء لا يمكنك إصلاحه، وستُوقف العمليَّة حينذاك. لهذا السبب، أهملت الواجهتان البرمجيتان emitBefore و emitAfter. استعمل رجاءً الدالة runInAsyncScope عوضًا عن هذه الدالة فهي أكثر أمانًا.

asyncResource.emitAfter()‎

أهملت منذ الإصدار: v9.6.0

الاستقرار: 0-مهمل: استعمل الدالة asyncResource.runInAsyncScope()‎ عوضًا عنها.

تستدعي هذه الدالة جميع ردود النداء after. إن استُدعيَت emitAfter()‎ عدَّة مرات بطريقة متشعِّبة، فسيُتعقَّب المُعرِّفات asyncId في المكدس وسيُتخلَّص منها بالترتيب الذي استدعيت به.

إن رَمَت ردود نداء المستخدم استثناءً، فستُستدعى emitAfter()‎ تلقائيًّا مع جميع المُعرِّفات asyncIds الموجودة في المكدس إن تمت معالجة الخطأ عبر معالج النطاق (domain handler) أو المعالج 'uncaughtException'.

يجب إزالة ردي النداء before و after بنفس الترتيب الذي استدعيا به وإلا سيُطلَق استثناءٌ لا يمكنك إصلاحه، وستُوقف العمليَّة. لهذا السبب، أهملت الواجهتان البرمجيتان emitBefore و emitAfter. استعمل رجاءً الدالة runInAsyncScope عوضًا عن هذه الدالة فهي أكثر أمانًا.

asyncResource.emitDestroy()‎

تستدعي هذه الدالة جميع الخطافات destroy. يجب استدعاؤها مرةً واحدةً فقط يدويًّا، وسيُرمى خطأٌ إن استدعيت أكثر من ذلك. إن تُرِكَ المورد ليُجمَّع عبر مُجمِّع المهملات (garbage collection، ويدعى اختصارًا GC)، فلن تُستدعَى الخطافات destroy حينها.

asyncResource.asyncId()‎

تعيد هذه الدالة عددًا يمثِّل المُعرِّف asyncId الفريد المُسنَد إلى المورد.

asyncResource.triggerAsyncId()‎

تعيد هذه الدالة عددًا يمثِّل المُعرِّف triggerAsyncId نفسه الذي مُرِّر إلى الصنف AsyncResource الباني.

مصادر