المستوى المطلوب: متوسط — لازم تعرف Node.js و async/await، ويفضّل تكون شفت Microservices أو API Calls بين خدمات قبل كده.
لو الـ Payment Service بتاعك بطئت من 80ms لـ 4 ثواني، خدمة الـ Checkout اللي بتنادي عليها هتبدأ تخزّن طلبات معلّقة، وفي 30 ثانية الـ Memory هيتفجّر، وكل الموقع هيسقط — مش بس الـ Payment. ده اللي اسمه cascading failure. Circuit Breaker بيمنع ده بكام سطر كود، والمقال ده هيعدّيك من المثال البسيط للكود الإنتاجي الكامل.
اعمل Circuit Breaker لـ API Calls بـ Node.js يحمي خدمتك من السقوط المتسلسل
المشكلة باختصار
أي تطبيق فيه أكتر من خدمة بتنادي بعض، أو بيعتمد على API خارجي زي Stripe و SendGrid و OpenAI، عرضة لمشكلة واحدة محددة: الخدمة الخارجية بتبطّأ، مش بتسقط. الطلبات عندك بتفضل مفتوحة بتستنى رد. كل request مفتوح بياخد socket، وذاكرة، وحصته من event loop. في خلال دقيقتين، خدمتك انت — اللي المفروض شغّالة — بتسقط لأنها استنفدت كل الـ connections. ده مش سقوط الخدمة الخارجية. ده سقوطك انت بسبب فشل في حد تاني.
مثال للمبتدئين: قاطع الكهربا في البيت
تخيّل البيت بتاعك. لو حصل short circuit في الفرن، قاطع الكهربا بيفصل التيار عن الفرن خلال جزء من الثانية. لو القاطع مكانش موجود، الحرارة كانت هتسري في الأسلاك وتولّع الجدار كله. القاطع بيعمل تلات حاجات بالظبط: بيكتشف المشكلة، بيقطع الدايرة، وبعد ما المالك يصلّح المشكلة بيرجّع التيار يدوي. Circuit Breaker في الكود بيعمل نفس التلات حاجات، بس مع API calls بدل الكهربا، وبيرجّع نفسه تلقائيًا بدون تدخّل بشري.
التعريف العلمي: الحالات الثلاث
Circuit Breaker بيشتغل كآلة حالات (state machine) بتلات حالات. الفهم الدقيق ليهم هو اللي بيحدّد إعداداته صح:
- Closed: الحالة الافتراضية. الطلبات بتعدي للخدمة الخارجية عادي. القاطع بيعدّ نسبة الفشل في نافذة زمنية (sliding window). لو نسبة الفشل عدّت threshold معيّن (مثلاً 50% من آخر 20 طلب) خلال دقيقة، القاطع بينقل لـ Open.
- Open: القاطع مفتوح، يعني الدايرة مفصولة. أي طلب جديد بيرجع فورًا بـ error مخصّص (CircuitBreakerOpenError) من غير ما يلمس الشبكة أصلاً. ده بيوفّر وقت كل request وبيحرّر الـ connections. القاطع بيفضل Open لمدة ثابتة (reset timeout)، شائع 30 ثانية.
- Half-Open: بعد reset timeout، القاطع بيسمح بعدد محدود من الطلبات (مثلاً 3) يعدّوا فعلاً للخدمة. لو نجحوا كلهم، القاطع بيرجع Closed. لو فشل أي واحد، بيرجع Open ويفضل تاني فترة الـ timeout كاملة.
الحل: تنفيذ بـ Node.js في 60 سطر
هنستخدم مكتبة opossum اللي هي إصدار Node.js المُلهم من Hystrix بتاع Netflix. هي battle-tested ومستخدمة في IBM وعدد كبير من شركات الإنتاج. التركيب مباشر:
npm install opossumالكود الكامل لحماية API call على Stripe:
import CircuitBreaker from 'opossum';
async function chargeCustomer(customerId, amount) {
const response = await fetch('https://api.stripe.com/v1/charges', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.STRIPE_KEY}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `amount=${amount}&customer=${customerId}¤cy=usd`,
signal: AbortSignal.timeout(2000),
});
if (!response.ok) throw new Error(`Stripe ${response.status}`);
return response.json();
}
const options = {
timeout: 2500,
errorThresholdPercentage: 50,
resetTimeout: 30000,
rollingCountTimeout: 60000,
rollingCountBuckets: 10,
volumeThreshold: 10,
};
const breaker = new CircuitBreaker(chargeCustomer, options);
breaker.fallback(() => ({
status: 'queued',
message: 'الدفع تم تأجيله، هيتنفّذ لما خدمة Stripe ترجع',
}));
breaker.on('open', () => console.warn('[Stripe] Circuit OPEN'));
breaker.on('halfOpen', () => console.info('[Stripe] Circuit HALF-OPEN'));
breaker.on('close', () => console.info('[Stripe] Circuit CLOSED'));
export async function safeCharge(customerId, amount) {
return breaker.fire(customerId, amount);
}
السطور المهمة في الإعداد:
volumeThreshold: 10: ما يفتحش القاطع إلا بعد 10 طلبات على الأقل في النافذة. ده بيمنع false positives لو traffic قليل.rollingCountTimeout: 60000: نافذة قياس الفشل دقيقة كاملة، مقسومة على 10 buckets. ده الـ sliding window الفعلي.fallback: لما القاطع Open، بدل ما المستخدم يشوف 500، بنرجّع له status معقول. هنا بنحط الطلب في queue ونتعامل معاه لاحقًا.timeout: 2500: لو الطلب نفسه أخد أكتر من 2.5 ثانية، يتعدّ كأنه فشل بتأثير الإحصاء. ده بيعالج البطء، مش بس الفشل المباشر.
أرقام من الإنتاج: قبل وبعد
على خدمة E-commerce بتعمل 250 طلب/ثانية، لما خدمة الـ Email Provider الخارجي بطئت من 200ms لـ 8 ثواني:
- بدون Circuit Breaker: استهلاك الذاكرة قفز من 380MB لـ 1.4GB في 90 ثانية. عدد active connections عدّى 5000. الـ Order API بدأ يرجع 503 لأن الـ event loop اتقفل. الموقع كله سقط رغم إن Email خدمة ثانوية أصلاً.
- مع Circuit Breaker (threshold 50%, reset 30s): القاطع فتح بعد 18 ثانية من بداية المشكلة. الإيميلات اتعلّقت في queue. الـ Order API فضل شغّال 100%. الذاكرة فضلت ثابتة عند 410MB. لما الـ Email رجعت بعد 6 دقايق، القاطع رجع Closed تلقائيًا والـ queue اتنفّذت.
الفرق العملي: 6 دقايق downtime كاملة لميزة ثانوية، مقابل 0 دقايق downtime للموقع الأساسي. الـ recovery كمان كان تلقائي بدون تدخّل من فريق الـ on-call.
الـ trade-offs اللي لازم تفهمها
Circuit Breaker مش free lunch. اللي بتكسبه:
- عزل الفشل: مشكلة في خدمة واحدة ما بتسقطش الباقي.
- توفير resources وقت الـ outages بدل ما الـ connections تتكدّس.
- مهلة طبيعية للخدمة الخارجية تتعافى من غير ضغط إضافي.
- recovery تلقائي بدون تدخّل بشري لما المشكلة تتحلّ.
اللي بتخسره:
- تعقيد إضافي: حالة جديدة في الكود لازم تختبرها وتراقبها وتسجّلها في الـ logs.
- false positives لو الـ thresholds مش مضبوطة على الـ baseline بتاع خدمتك. القاطع ممكن يفتح في spike مؤقت طبيعي.
- محتاج fallback strategy حقيقي. لو مفيش، تجربة الـ user هتبقى أسوأ من غير القاطع، لأنه بياخد رد سريع بفشل بدل ما يستنّى.
- لازم monitoring فوقه. القاطع اللي بيفتح ومحدش عارف عنه أسوأ من غيابه.
متى لا تستخدم Circuit Breaker
القاطع مش للحالات دي:
- عملية حرجة بدون fallback معقول: لو دي خطوة تحقّق هوية مستخدم لا يمكن تأجيلها أو queueها، الـ retry strategy مع backoff أحسن من القاطع.
- traffic منخفض جدًا: أقل من 10 طلب/دقيقة على الـ endpoint. الإحصاء مش بيكون موثوق وممكن القاطع يفتح بسبب طلبين فشلوا بالصدفة.
- internal calls على نفس البروسس: القاطع لـ network calls. لـ in-process function call، استخدم try/catch عادي، مش محتاج آلة حالات.
- عمليات batch مش real-time: لو السكريبت بيمشي ليلًا ومحدش مستنّى رد فوري، الـ retry مع exponential backoff أفضل بكتير.
الخطوة التالية
افتح أكتر API call خارجي عندك حساس (الـ Payment أو Email أو AI provider)، ولفّه بـ Circuit Breaker زي الكود فوق. ابدأ بـ errorThresholdPercentage: 50 وresetTimeout: 30000 كقيم افتراضية، وراقب الـ events لمدة أسبوع. لو القاطع مفتحش ولا مرة، ده ممكن يكون علامة إن الـ thresholds واسعة جدًا — قلّل المدى. لو فتح كتير في وقت ما فيش outage فعلي، الـ thresholds ضيقة. الإعداد الصح بيتعدّل حسب الـ baseline بتاعك، مش من الكتاب.
المصادر
- Martin Fowler — CircuitBreaker pattern: martinfowler.com/bliki/CircuitBreaker.html
- opossum library docs: nodeshift.dev/opossum
- Netflix Hystrix wiki (الإلهام الأصلي للنمط): github.com/Netflix/Hystrix/wiki
- AWS Builders' Library — Avoiding insurmountable queue backlogs: aws.amazon.com/builders-library
- Microsoft Azure Architecture — Circuit Breaker pattern: learn.microsoft.com/azure/architecture/patterns/circuit-breaker