0Pricing
NestJS Enterprise Backend APIs · درس

قواطع الدائرة لفشل الخدمات التابعة

حماية الخدمات من الفشل المتسلسل باستخدام تكامل لقواطع دوائر بأسلوب opossum.

قواطع الدائرة لفشل الخدمات التابعة درس مجاني في NestJS Enterprise Backend APIs على CoddyKit. هذا هو الدرس 2 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في NestJS Enterprise Backend APIs، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة NestJS Enterprise Backend APIs 4 دروس في المجموع.

بعض أجزاء هذا الدرس لم تُترجم بعد وتظهر باللغة الإنجليزية.

Why Cascading Failures Happen

In an enterprise NestJS backend, your service rarely lives alone. It calls a payment gateway, an auth provider, a search cluster, other microservices. When one downstream dependency slows down, every request waiting on it holds a connection, a thread, and a chunk of the event loop.

  • A slow dependency exhausts your HTTP connection pool.
  • Pending requests pile up; latency climbs everywhere.
  • Your service becomes unhealthy and its own callers start failing.

This domino effect is a cascading failure. A circuit breaker is the pattern that stops the dominoes from falling.

The Circuit Breaker State Machine

A circuit breaker wraps a risky call and tracks its health through three states:

  • CLOSED — calls flow through normally. Failures are counted.
  • OPEN — too many failures occurred; calls are rejected instantly without touching the dependency. This gives the downstream time to recover.
  • HALF_OPEN — after a cooldown, a few trial calls are allowed. If they succeed, the breaker closes; if they fail, it opens again.

The key insight: when the breaker is OPEN, you fail fast instead of waiting on a timeout for every request.

A Minimal Breaker From Scratch

Before reaching for a library, it helps to understand the mechanics. Here is a tiny, self-contained breaker in TypeScript that flips between CLOSED and OPEN based on consecutive failures and a reset timeout.

This runs standalone so you can watch the state transitions.

type State = 'CLOSED' | 'OPEN' | 'HALF_OPEN';

class MiniBreaker {
  private state: State = 'CLOSED';
  private failures = 0;
  private openedAt = 0;
  constructor(private threshold = 3, private resetMs = 1000) {}

  async call<T>(fn: () => Promise<T>): Promise<T> {
    if (this.state === 'OPEN') {
      if (Date.now() - this.openedAt >= this.resetMs) this.state = 'HALF_OPEN';
      else throw new Error('Circuit OPEN: failing fast');
    }
    try {
      const result = await fn();
      this.failures = 0;
      this.state = 'CLOSED';
      return result;
    } catch (e) {
      this.failures++;
      if (this.failures >= this.threshold) {
        this.state = 'OPEN';
        this.openedAt = Date.now();
      }
      throw e;
    }
  }
  get current() { return this.state; }
}

async function main() {
  const breaker = new MiniBreaker(2, 500);
  const flaky = () => Promise.reject(new Error('downstream down'));
  for (let i = 0; i < 4; i++) {
    try { await breaker.call(flaky); }
    catch (e) { console.log(`call ${i}: ${(e as Error).message} [state=${breaker.current}]`); }
  }
}
main();

Enter opossum

Hand-rolled breakers miss the hard parts: rolling-window statistics, percentage-based thresholds, half-open trial limits, and metrics. opossum is the de-facto Node.js circuit breaker and integrates cleanly into NestJS providers.

  • timeout — how long before a call is considered failed.
  • errorThresholdPercentage — % of failures in the window that trips the breaker.
  • resetTimeout — how long OPEN lasts before a HALF_OPEN trial.
  • rollingCountTimeout — the size of the statistical window.

Install it with npm i opossum and npm i -D @types/opossum.

import CircuitBreaker from 'opossum';

const options: CircuitBreaker.Options = {
  timeout: 3000,                 // a call slower than 3s counts as a failure
  errorThresholdPercentage: 50,  // trip when >=50% of calls fail
  resetTimeout: 10000,           // stay OPEN for 10s, then try HALF_OPEN
  rollingCountTimeout: 10000,    // 10s statistical window
  rollingCountBuckets: 10,       // split the window into 10 buckets
};

// The action is the function we want to protect
async function fetchUser(id: string): Promise<{ id: string }> {
  // ... real HTTP call to a downstream user service ...
  return { id };
}

export const userBreaker = new CircuitBreaker(fetchUser, options);

Wrapping a Downstream Call in a NestJS Provider

In NestJS, the breaker belongs in a provider that owns one logical dependency. Build the CircuitBreaker once in the constructor (or a factory) so the rolling statistics persist across requests — never create a new breaker per request, or it can never learn the dependency's health.

Expose a method that delegates to breaker.fire(...).

import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs';
import CircuitBreaker from 'opossum';

interface PricingDto { sku: string; cents: number; }

@Injectable()
export class PricingClient {
  private readonly breaker: CircuitBreaker<[string], PricingDto>;

  constructor(private readonly http: HttpService) {
    this.breaker = new CircuitBreaker(
      (sku: string) => this.requestPrice(sku),
      { timeout: 2000, errorThresholdPercentage: 50, resetTimeout: 15000 },
    );
  }

  private async requestPrice(sku: string): Promise<PricingDto> {
    const res = await firstValueFrom(
      this.http.get<PricingDto>(`https://pricing.internal/skus/${sku}`),
    );
    return res.data;
  }

  getPrice(sku: string): Promise<PricingDto> {
    return this.breaker.fire(sku);
  }
}

Fallbacks: Degrade Gracefully

Failing fast is good, but returning a hard error to the user is often worse than returning something reasonable. opossum's fallback() runs whenever the action rejects or the breaker is OPEN.

  • Serve a cached or last-known-good value.
  • Return a safe default (for example, an empty recommendations list).
  • Queue the work for later instead of dropping it.

The fallback receives the same arguments plus the triggering error, so you can branch on it.

import CircuitBreaker from 'opossum';

type Recommendation = { id: string };

function buildRecommendationBreaker(
  action: (userId: string) => Promise<Recommendation[]>,
) {
  const breaker = new CircuitBreaker(action, {
    timeout: 1500,
    errorThresholdPercentage: 40,
    resetTimeout: 20000,
  });

  // When OPEN or the action fails, return a safe empty list
  breaker.fallback((_userId: string, err?: Error) => {
    if (err) console.warn('recommendations degraded:', err.message);
    return [] as Recommendation[];
  });

  return breaker;
}

Listening to Breaker Events

A breaker that flips silently is an operational blind spot. opossum emits events for every meaningful transition. Wire these to your logger and metrics so on-call engineers see the picture in real time.

  • open / halfOpen / close — state transitions.
  • reject — a call rejected because the breaker was OPEN.
  • timeout — a call exceeded the configured timeout.
  • fallback — the fallback was invoked.
  • success / failure — outcome of each fired call.
import { Logger } from '@nestjs/common';
import CircuitBreaker from 'opossum';

export function attachBreakerTelemetry(
  breaker: CircuitBreaker,
  name: string,
  logger = new Logger('CircuitBreaker'),
) {
  breaker.on('open', () => logger.error(`[${name}] OPEN - failing fast`));
  breaker.on('halfOpen', () => logger.warn(`[${name}] HALF_OPEN - probing`));
  breaker.on('close', () => logger.log(`[${name}] CLOSED - recovered`));
  breaker.on('reject', () => logger.warn(`[${name}] call rejected (OPEN)`));
  breaker.on('timeout', () => logger.warn(`[${name}] call timed out`));
  breaker.on('fallback', () => logger.warn(`[${name}] fallback served`));
}

Timeout Is Part of the Breaker

A common mistake is setting the breaker's timeout longer than the underlying HTTP client timeout. If your axios timeout is 30s but the breaker timeout is 3s, opossum gives up at 3s — good — but the socket may still be held open downstream.

Align them: set the breaker timeout slightly below the transport timeout, and make sure the transport actually aborts. The whole point is to bound how long any single call can pin a resource.

import { Module } from '@nestjs/common';
import { HttpModule } from '@nestjs/axios';

@Module({
  imports: [
    HttpModule.register({
      timeout: 2500,        // axios aborts the socket at 2.5s
      maxRedirects: 0,
    }),
  ],
})
export class DownstreamModule {}

// Breaker timeout (e.g. 2000ms) should sit just BELOW the axios timeout
// so opossum records the failure while the socket is still being released.

One Breaker Per Dependency, Not Per App

Bulkheading means isolating failures so a sick dependency cannot drown a healthy one. Give each downstream its own breaker instance with thresholds tuned to its SLA.

  • A flaky analytics service can trip without affecting payments.
  • A latency-sensitive auth call gets a tight timeout; a batch report call gets a loose one.
  • Per-dependency metrics make dashboards readable.

Do not share a single global breaker across unrelated calls — their statistics would contaminate each other.

import { Injectable } from '@nestjs/common';
import CircuitBreaker from 'opossum';

@Injectable()
export class BreakerRegistry {
  private readonly breakers = new Map<string, CircuitBreaker>();

  get<TArgs extends unknown[], TRet>(
    name: string,
    action: (...args: TArgs) => Promise<TRet>,
    options: CircuitBreaker.Options,
  ): CircuitBreaker<TArgs, TRet> {
    const existing = this.breakers.get(name);
    if (existing) return existing as CircuitBreaker<TArgs, TRet>;
    const breaker = new CircuitBreaker(action, options);
    this.breakers.set(name, breaker);
    return breaker;
  }
}

Exposing Breaker Health to Probes

Operationally you want breaker state visible in a health endpoint and scraped by Prometheus. opossum exposes breaker.stats (counts of success, failure, timeout, reject) and breaker.opened / breaker.halfOpen booleans.

A custom Terminus health indicator can report OPEN breakers as a degraded — not necessarily down — status, so orchestrators do not needlessly kill a pod that is correctly shedding load.

import { Injectable } from '@nestjs/common';
import { HealthIndicator, HealthIndicatorResult } from '@nestjs/terminus';
import CircuitBreaker from 'opossum';

@Injectable()
export class BreakerHealthIndicator extends HealthIndicator {
  check(name: string, breaker: CircuitBreaker): HealthIndicatorResult {
    const isUp = !breaker.opened;
    return this.getStatus(name, isUp, {
      state: breaker.opened ? 'open' : breaker.halfOpen ? 'half_open' : 'closed',
      failures: breaker.stats.failures,
      timeouts: breaker.stats.timeouts,
      rejects: breaker.stats.rejects,
    });
  }
}

Tuning Thresholds Sanely

Bad thresholds are worse than no breaker. Tune against real traffic:

  • errorThresholdPercentage too low (e.g. 10%) trips on normal jitter; too high (e.g. 90%) defeats the purpose. 40-60% is a common starting band.
  • resetTimeout too short hammers a recovering service; too long delays recovery. Start around 10-30s.
  • Account for low traffic: with only 2 calls in the window, one failure is 50%. opossum's volumeThreshold requires a minimum number of calls before the percentage can trip.
import CircuitBreaker from 'opossum';

const options: CircuitBreaker.Options = {
  timeout: 2000,
  errorThresholdPercentage: 50,
  resetTimeout: 15000,
  rollingCountTimeout: 10000,
  rollingCountBuckets: 10,
  volumeThreshold: 10, // need >=10 calls in the window before % can trip
  // errorFilter lets you NOT count expected errors (e.g. 404) as failures:
  errorFilter: (err: { statusCode?: number }) => err?.statusCode === 404,
};

export { options };

Quick Check: Choosing the Right Behavior

Test your understanding of how a circuit breaker behaves under load.

Recap

You now know how to keep a downstream failure from taking down your NestJS service:

  • The pattern: a breaker moves between CLOSED, OPEN, and HALF_OPEN; OPEN means fail fast instead of waiting on timeouts.
  • opossum: configure timeout, errorThresholdPercentage, resetTimeout, and volumeThreshold, then wrap the call with new CircuitBreaker(action, options) and invoke breaker.fire().
  • Build once: create the breaker in a provider/registry so rolling statistics persist — one breaker per dependency for bulkheading.
  • Degrade: register a fallback() for cached or safe-default responses.
  • Observe: hook the open/close/reject/timeout events into logs and metrics, and surface state in a health probe.

Combined with retries (with backoff) and timeouts, circuit breakers are a cornerstone of resilient, SLO-friendly distributed systems.

الأسئلة الشائعة

هل درس «قواطع الدائرة لفشل الخدمات التابعة» مجاني؟

نعم — نص درس «قواطع الدائرة لفشل الخدمات التابعة» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة NestJS Enterprise Backend APIs، انتقل إلى CoddyKit PRO. تتضمن دورة NestJS Enterprise Backend APIs 4 دروس في المجموع.

ماذا ستتعلم في «قواطع الدائرة لفشل الخدمات التابعة»؟

حماية الخدمات من الفشل المتسلسل باستخدام تكامل لقواطع دوائر بأسلوب opossum. تتمرن على NestJS Enterprise Backend APIs مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ NestJS Enterprise Backend APIs؟

لا تُشترط خبرة سابقة. NestJS Enterprise Backend APIs على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 2 من أصل 4.

كم من الوقت يستغرق درس «قواطع الدائرة لفشل الخدمات التابعة»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس NestJS Enterprise Backend APIs هذا؟

نعم. كل درس في NestJS Enterprise Backend APIs يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. المهلات وإعادة المحاولة والعوازل باستخدام Interceptors
  2. قواطع الدائرة لفشل الخدمات التابعة
  3. التتبّع الموزّع باستخدام OpenTelemetry
  4. تحديد SLOs وميزانيات الأخطاء
← العودة إلى NestJS Enterprise Backend APIs