الأعطال بوصفها عقدًا
لمعرفة كيف يتصرف النظام حين تتدهور الشبكة أو تتعطل إحدى تبعياته، يوضع العطل في التجربة. يُفتح المرحّل أو المحاكي مع التشغيل، وتفعّله خطوة عند الطلب، ويحصي تقرير التشغيل ما جرى في كل مرحلة. ونهاية التشغيل — ناجحًا أو فاشلًا أو موقوفًا — تغلقهما، فلا يبقى شيء مُضعَفًا بعده.
| العقدة | ما تفعله |
|---|---|
| إضعاف | مرحّل بين النظام قيد الاختبار وهدفه، يُضعف ما يمر عبره، طوال التشغيل |
| تغيير الإضعاف | ينقل أحد مرحّلات التشغيل إلى ملف تعريف آخر، ابتداءً من هذه الخطوة |
| محاكٍ | واجهة API أو جهاز أو وسيط يؤدي Signal Lab دوره، طوال التشغيل |
| تعطيل/تفعيل المحاكي | يعطّل أحد محاكيات التشغيل، أو يعيده إلى العمل |
العقد الأربع كلها في قائمة الإضافة تحت الأعطال والمحاكاة. وحقولها في مرجع العقد؛ أما المرحّل نفسه فمشروح في الإضعاف، والمحاكيات في المحاكيات.
الإضعاف
يرسل النظام قيد الاختبار إلى المرحّل بدلًا من هدفه الحقيقي؛ ويمرّر المرحّل إلى الهدف، ويعيد الردود، ويُضعف الاتجاهين كليهما.
| الحقل | ما هو |
|---|---|
| الاستماع على | IP:port الذي يرسل إليه النظام قيد الاختبار أو يتصل به، والمنفذ غير 0 |
| التمرير إلى | IP:port الوجهة الحقيقية |
| البروتوكول | UDP — يلقى كل مخطط بيانات مصيره الخاص — أو TCP — يُربط كل اتصال باتصال خاص به إلى الهدف |
| ملف التعريف | إعداد مسبق أو قيم خاصة |
ما يقرؤه المرحّل من ملف تعريفه يتوقف على البروتوكول؛ وتُترك القيم الأخرى:
| البروتوكول | أنواع الإضعاف |
|---|---|
| UDP | زمن الانتقال، والتذبذب، وفقد الحزم، والفقد على دفعات، والازدواج، والتلف، وإعادة الترتيب، وحد لعرض النطاق، وعدم الاتصال |
| TCP | زمن الانتقال والتذبذب (يبقى التدفق مرتبًا)، وحد لعرض النطاق (يُبطَّأ المرسِل ولا يُسقَط شيء)، وإعادة تعيين الاتصالات، وترك الاتصالات نصف مفتوحة، وعدم الاتصال |
يُفتح قبل الخطوة الأولى. يُفتح كل مرحّل في التجربة عند بدء التشغيل، كمقابس عقد الانتظار، لذا يقبل الاستماع على والتمرير إلى نصًا ومعاملات فقط (node.params_only) — {{relay}} مع معامل relay، لا متغيرًا أبدًا. والمنفذ الذي يتعذّر فتحه يوقف التشغيل قبل أي حركة مرور، عند العقدة.
المرور في التدفق. حين يبلغ التشغيل العقدة، يمر منها فورًا، ويذكر الخط الزمني ما تُضعفه وبماذا. ويعمل المرحّل من بداية التشغيل إلى نهايته، أيًّا كان موضع العقدة في المخطط.
يُغلق مع التشغيل. كيفما انتهى التشغيل، يُغلق المرحّل؛ وتُغلق معه اتصالات مرحّل TCP. أما المرحّل الذي توقف عن التمرير من تلقاء نفسه فيحتفظ بالسبب: وتفشل الخطوات التي تستخدمه به، ويذكره التقرير.
التوجيه عبر الإضعاف
في عقدة رسالة OSC أو مخطط بيانات UDP، يضع الخيار التوجيه عبر الإضعاف إضعافًا أمامها: يستمع المرحّل على منفذ حر من 127.0.0.1، ويمرّر إلى هدف العقدة بالإعداد المسبق شبكة محلية، وتصبح العقدة ترسل إلى المرحّل.
تغيير الإضعاف
تسمّي تغيير الإضعاف أحد مرحّلات التجربة في الحقل الإضعاف وتعطيه ملف التعريف الذي يُضعف به ابتداءً من تلك الخطوة. ويحتفظ المرحّل بمنفذه واتصالاته؛ وتسري القيم الجديدة على الحزمة أو الجزء التالي. ويعرض الخط الزمني ملف التعريف الجديد.
كل تغيير يُنهي مرحلة. ويحتفظ تقرير التشغيل، لكل مرحّل، بما يلي:
- عنواني الاستماع والهدف، وبروتوكوله إن كان TCP؛
- أعداده في المجموع: المستلمة، والمُمرَّرة، والمُسقَطة، والمقيّدة، والمزدوجة، والتالفة، والمعاد ترتيبها، والبايتات — وفي TCP الاتصالات، وما أُعيد تعيينه منها وما تُرك نصف مفتوح؛
- كل مرحلة: اسم ملف التعريف، ومتى بدأت وانتهت بالميلي ثانية منذ لحظة فتح المرحّل، والأعداد نفسها لتلك المرحلة وحدها.
وتُحسب الحزمة في المرحلة التي قررت مصيرها، حتى لو خرجت نسختها المتأخرة بعد التبديل. ويحتفظ المرحّل بآخر 1000 مرحلة؛ أما الأقدم فتُعدّ ولا تُحفظ.
وعقدة تغيير الإضعاف التي لا تسمّي أي مرحّل في التجربة تُرفض (impair.relay_unknown).
المحاكي
تؤدي محاكٍ دور تبعية طوال التشغيل: واجهة HTTP API، أو جهاز OSC أو UDP أو TCP، أو وسيط MQTT. وهو المحاكي نفسه الذي تشغّله شاشة المحاكيات على حدة: يفتح الزر تعديل… قواعده، ويحتفظ إلى المكتبة بنسخة في المكتبة، ويأخذ من المكتبة محاكيًا منها.
- يُفتح قبل الخطوة الأولى ويجيب حتى ينتهي التشغيل؛ والمنفذ الذي يتعذّر فتحه يوقف التشغيل قبل أي حركة مرور. ويمر منه التدفق فورًا.
- عنوانه
IP:portحرفي. وأنماط المطابقة فيه تقبل المعاملات فقط؛ وردوده قوالب تُقرأ بما وصل ({{request.…}}) ومعاملات التشغيل. ولا يمكنه قراءة الأسرار. - خياراته العشوائية — مزيج موزون من الاستجابات، وتذبذب التأخير، والمولّدات في الردود — تُسحب من بذرة التشغيل.
- ومحاكي HTTP هو أيضًا ما تستمع إليه انتظار طلب HTTP على العنوان نفسه: فتفحص ما أرسله النظام قيد الاختبار. ومن دون محاكٍ هناك، يجيب مستمع التشغيل نفسه كل طلب بـ
204. - ومحاكي OSC أو UDP يتشارك منفذه مع عقد الانتظار في التشغيل عليه: فكلاهما يرى كل مخطط بيانات.
- ومحاكي MQTT وسيط يمكن لعقدتي نشر MQTT وانتظار MQTT في التشغيل استخدامه كأي وسيط آخر.
يحتفظ تقرير التشغيل، لكل عقدة محاكٍ، باسمه وبروتوكوله وعنوانه، وأعداده: الطلبات في المجموع، وما لم تأخذه قاعدة، وما فشل، وما صادفه متعطلًا، والرسائل التي تعذّر على الوسيط تسليمها إلى عميل بطيء، ومطابقات كل قاعدة.
تعطيل المحاكي وتفعيله
تسمّي تعطيل/تفعيل المحاكي أحد محاكيات التشغيل في الحقل المحاكي؛ والحقل الحالة هو معطّل أو يعمل. وأثناء التعطل:
| المحاكي | ما يُواجَه |
|---|---|
| HTTP | ما يحدده الحقل أثناء التعطل: 503 غير متاح (503، والمتن {"error":"unavailable"})، أو إغلاق الاتصال، أو بلا رد — يُحتجز الطلب حتى يستسلم العميل، بحد أقصى 120 ث |
| جهاز TCP، ووسيط MQTT | تُسقط الاتصالات وتُرفض الجديدة |
| جهاز OSC أو UDP | لا يُجاب عن شيء |
ما يصل أثناء التعطل يُعدّ down، لا طلبًا لم تأخذه قاعدة أبدًا. وتظل انتظار طلب HTTP ترى الطلبات. ويبقى المحاكي معطّلًا حتى تعيده خطوة إلى العمل، أيًّا كان ما يقوله جدول انقطاعه، وتغلقه نهاية التشغيل في الحالتين.
وعقدة تعطيل/تفعيل المحاكي التي لا تسمّي أي محاكٍ في التجربة تُرفض (emulator.node_unknown).
الانقطاعات وفق جدول
يمكن للمحاكي أن يتعطل من تلقاء نفسه أيضًا: ففي قواعده، يعيّن الخيار يتعطل من حين لآخر الحقلين مدة العمل، ms ومدة التعطل، ms، كلٌّ منهما بين 10 و3,600,000 ms، ويعيّن أثناء التعطل لـ HTTP. يجيب المحاكي طوال الأولى، ويتعطل طوال الثانية، وهكذا، عدًّا من لحظة فتحه — وفي التشغيل، قبل الخطوة الأولى. وحين يتعطل محاكي HTTP وفق جدوله، يحمل رده 503 الترويسة Retry-After بعدد الثواني الصحيحة حتى عودته، ثانية واحدة على الأقل؛ أما 503 الصادر والمحاكي معطّل بخطوة تعطيل/تفعيل المحاكي فلا ترويسة فيه، إذ لا أحد يعلم متى ينتهي ذلك.
الجدول لا يحتاج إلى خطوة؛ والخطوة لا تحتاج إلى جدول. يُستخدم الجدول لتبعية تتقلب بين العمل والتعطل، والخطوة لانقطاع عند نقطة مختارة من التدفق.
مثال: انقطاع خلف رابط بطيء
يطلب عميل طلبًا من API عبر مرحّل. وأثناء الطلب، يُبطئ فرع ثانٍ الرابط إلى 4G، ويعطّل الـ API ثانيتين، ثم يعيده، ويجعل الرابط نظيفًا من جديد. ويجب أن يواصل العميل الطلب حتى يتلقى رده.
start → orders → link → split
split ─ branch1 → settle → until_ok ─ done → answered → joined
until_ok ─ body → get → status → pause → until_ok
split ─ branch2 → slow → down → outage → up → clean → joined
joined → endالأسماء هي معرّفات العقد في الملف أدناه.
- إضافة معامل
api=http://127.0.0.1:18091— المرحّل، لا الـ API. - إضافة محاكٍ: HTTP، و
127.0.0.1:18090، ومسارGET /orders/:idيجيب بـ200مع{"order":"{{request.params.id}}"}. - بعده، إضعاف: الاستماع على
127.0.0.1:18091، والتمرير إلى127.0.0.1:18090، والبروتوكول TCP، والإعداد المسبق شبكة محلية. - بعده، فرع متوازٍ.
- على الفرع 1، العميل: تأخير بمقدار 300 ms، ثم حلقة — أقصى عدد للدورات 40، والتوقف مبكرًا عندما
{{status}}يساوي200. متنها: طلب HTTPGET {{api}}/orders/42، ثم استخراج قيمة لـ رمز الحالة فيstatus، وتأخير 250 ms، موصولًا عائدًا إلى الحلقة. وعلى تم، علامة في السجلOrders API answers again: HTTP {{status}}. - على الفرع 2، الأعطال: تغيير الإضعاف ينقل المرحّل إلى 4G؛ ثم تعطيل/تفعيل المحاكي يجعل Orders API معطّل مع 503 غير متاح؛ ثم تأخير 2000 ms؛ ثم تعطيل/تفعيل المحاكي آخر يجعله يعمل؛ ثم تغيير الإضعاف آخر يعيده إلى شبكة محلية.
- وصل الفرعين كليهما بعقدة دمج الفروع، ووصل الدمج بعقدة النهاية.
- تشغيلها.
يعرض الخط الزمني طلبات العميل مجابًا عنها بـ 503 عبر الرابط البطيء، ثم عودة الـ API، ثم 200 وخروج الحلقة عبر تم. ويحصي التقرير نحو خمسة طلبات صادفت الـ API معطّلًا، وطلبًا واحدًا أجاب عنه مساره، وثلاث مراحل للمرحّل — شبكة محلية للحظة، ثم 4G طوال الانقطاع، ثم شبكة محلية مجددًا — لكل منها حركة مرورها.
التجربة ملفًا
تُحفظ في ملف .json وتُفتح بالزر فتح JSON… في التجارب.
{
"version": 9,
"name": "Outage behind a slow link",
"params": [{ "name": "api", "value": "http://127.0.0.1:18091" }],
"profiles": [],
"profile": null,
"seed": null,
"nodes": [
{ "id": "start", "type": "start", "x": 40, "y": 270 },
{ "id": "orders", "type": "emulator", "x": 260, "y": 270,
"emulator": { "name": "Orders API", "bind": "127.0.0.1:18090", "protocol": "http",
"routes": [{ "method": "GET", "path": "/orders/:id", "when": [], "order": "sequence",
"responses": [{ "status": 200, "headers": [], "body": "{\"order\":\"{{request.params.id}}\"}", "delay_ms": 0, "jitter_ms": 0, "fault": "none", "weight": 1 }] }],
"fallback": null } },
{ "id": "link", "type": "impairment", "x": 490, "y": 270, "listen": "127.0.0.1:18091", "target": "127.0.0.1:18090", "protocol": "tcp",
"profile": { "name": "lan", "latency_ms": 1, "jitter_ms": 1 } },
{ "id": "split", "type": "fork", "x": 720, "y": 270 },
{ "id": "settle", "type": "delay", "x": 950, "y": 140, "ms": 300 },
{ "id": "until_ok", "type": "loop", "x": 1180, "y": 140, "max": 40,
"until": { "value": "{{status}}", "op": "eq", "expected": "200" } },
{ "id": "get", "type": "http", "x": 1410, "y": 20,
"request": { "method": "GET", "url": "{{api}}/orders/42", "headers": [], "body": null, "timeout_ms": 3000 } },
{ "id": "status", "type": "extract", "x": 1640, "y": 20, "variable": "status", "from": "status", "expr": "" },
{ "id": "pause", "type": "delay", "x": 1870, "y": 20, "ms": 250 },
{ "id": "answered", "type": "log", "x": 1410, "y": 140, "message": "Orders API answers again: HTTP {{status}}" },
{ "id": "slow", "type": "impairment_change", "x": 950, "y": 400, "relay": "link",
"profile": { "name": "4g", "latency_ms": 60, "jitter_ms": 25, "rate_kbps": 20000 } },
{ "id": "down", "type": "emulator_state", "x": 1180, "y": 400, "emulator": "orders", "down": true, "fault": "unavailable" },
{ "id": "outage", "type": "delay", "x": 1410, "y": 400, "ms": 2000 },
{ "id": "up", "type": "emulator_state", "x": 1640, "y": 400, "emulator": "orders", "down": false, "fault": "unavailable" },
{ "id": "clean", "type": "impairment_change", "x": 1870, "y": 400, "relay": "link",
"profile": { "name": "lan", "latency_ms": 1, "jitter_ms": 1 } },
{ "id": "joined", "type": "join", "x": 2100, "y": 270 },
{ "id": "end", "type": "end", "x": 2330, "y": 270 }
],
"edges": [
{ "from": "start", "to": "orders" },
{ "from": "orders", "to": "link" },
{ "from": "link", "to": "split" },
{ "from": "split", "to": "settle", "port": "branch1" },
{ "from": "split", "to": "slow", "port": "branch2" },
{ "from": "settle", "to": "until_ok" },
{ "from": "until_ok", "to": "get", "port": "body" },
{ "from": "get", "to": "status" },
{ "from": "status", "to": "pause" },
{ "from": "pause", "to": "until_ok" },
{ "from": "until_ok", "to": "answered", "port": "done" },
{ "from": "answered", "to": "joined" },
{ "from": "slow", "to": "down" },
{ "from": "down", "to": "outage" },
{ "from": "outage", "to": "up" },
{ "from": "up", "to": "clean" },
{ "from": "clean", "to": "joined" },
{ "from": "joined", "to": "end" }
]
}قالبان في التجارب يفعلان الشيء نفسه بطريقتين أخريين: يرسل مراحل الأعطال مخططات بيانات إلى جهاز محاكى عبر مرحّل UDP يُبدَّل بين نظيف وفاقد وغير متصل ونظيف من جديد؛ ويعطّل انقطاع تبعية واجهة API محاكاة ثانيتين بينما يواصل عميل الطلب.
المنافذ
لا يمكن لمقابس التشغيل الواحد — الانتظار والردود والمحاكيات والمرحّلات — أن تتشارك منفذًا في بروتوكول واحد؛ أما مقبسا UDP وTCP فيمكن أن يستخدما الرقم نفسه. وبالنسبة إلى الاستماع على في المرحّل، يتعارض عنوان على 0.0.0.0 مع كل عنوان على المنفذ نفسه.
| المقبس | لا يمكنه مشاركة منفذه مع |
|---|---|
| محاكي HTTP أو TCP أو MQTT | محاكٍ آخر منها (emulator.bind_taken) |
| محاكي OSC أو UDP | محاكٍ آخر منها (emulator.bind_taken) |
| محاكي TCP أو MQTT | انتظار طلب HTTP (emulator.bind_taken) |
| الاستماع على في مرحّل UDP | مرحّل UDP آخر، أو محاكي OSC أو UDP، أو انتظار، أو مقبس رد (impair.bind_taken) |
| الاستماع على في مرحّل TCP | مرحّل TCP آخر، أو محاكي HTTP أو TCP أو MQTT، أو انتظار طلب HTTP (impair.bind_taken) |
المشاركة مقصودة في: محاكي HTTP وخطوات انتظار طلب HTTP على عنوانه؛ ومحاكي OSC أو UDP وعقد الانتظار على منفذه؛ وعقد الانتظار على عنوان واحد فيما بينها.
ولا يجوز لمرحّل أن يمرّر إلى نفسه، مباشرة أو عبر مرحّلات أخرى: إذ ستدور حركة مروره على عنوان الاسترجاع (impair.loop). أما مرحّلان متتاليان أمام جهاز فلا بأس بهما.
تكرار تشغيل معطوب
كل قرار يتخذه المرحّل — هل تُفقد الحزمة أو تُزدوج أو تتلف أو تُحتجز، وكم تذبذبًا تنال — يُسحب من بذرة التشغيل، منفصلًا لكل اتجاه وحزمةً حزمةً. وتُسحب منها الخيارات العشوائية للمحاكي أيضًا. وعند إعادة التشغيل بالبذرة نفسها وحركة المرور نفسها، تلقى الحزم نفسها المصير نفسه: فالفشل الذي شوهد مرة يمكن أن يُشاهد ثانية.
ولحفظ البذرة، يُضغط تثبيت بجانبها في الخط الزمني، أو يُشغَّل بها من تشغيل بقيم…؛ انظر البذور. أما ما لا تستطيع البذرة تثبيته فهو التوقيت: متى يرسل النظام قيد الاختبار، وبالتالي في أي مرحلة تقع الحزمة.