Files
2026-05-27 23:29:53 +01:00

5.7 KiB

OpenSpec Change: network-plugin-circuit-breaker

  • Change ID: network-plugin-circuit-breaker
  • Package: @suaveplan/network-plugin-circuit-breaker (new)
  • Status: PROPOSED
  • Date: 2026-05-01

Summary

Implement circuit breakers at mesh boundaries. @suaveplan/retry provides per-request retry with exponential backoff, but there is no circuit breaker on cross-mesh relay paths. If Mesh A's Tier 0 host cannot reach Mesh B's Tier 0 host, repeated retries waste bandwidth, delay failure detection, and can cascade load onto alternative paths. A circuit breaker allows fast-fail and graceful degradation.


Why

Current state: @suaveplan/retry exists but no circuit breaker is integrated at the mesh level. The enterprise-network-standby-transport proposal has transport-level failover but no mesh-level circuit breaking.

Why this matters: In a multi-mesh federation:

  • A dead host on a relay path causes every cross-mesh message to retry N times before failing.
  • Without a circuit breaker, the mesh cannot distinguish between "slow" and "dead" quickly.
  • Cascading retries can saturate the remaining healthy paths.

Core components

1. MeshCircuitBreaker

Per-path circuit breaker state machine: CLOSED → OPEN → HALF_OPEN.

class MeshCircuitBreaker {
  constructor(options: {
    pathId: string; // e.g. "meshA:host1→meshB:host2"
    failureThreshold: number; // default 5
    successThreshold: number; // default 3
    openDurationMs: number; // default 30000
    halfOpenMaxCalls: number; // default 3
  });
  getState(): "CLOSED" | "OPEN" | "HALF_OPEN";
  recordSuccess(): void;
  recordFailure(): void;
  canExecute(): boolean;
  on(event: "state:changed", handler: (state: string) => void): void;
}

States:

  • CLOSED: Normal operation. Failures are counted. When failureThreshold is reached → OPEN.
  • OPEN: All executions are rejected immediately (fast-fail). After openDurationMs → HALF_OPEN.
  • HALF_OPEN: A limited number (halfOpenMaxCalls) of trial executions are allowed. If successThreshold successes occur → CLOSED. If any failure → OPEN.

2. CircuitBreakerRegistry

Manages circuit breakers for all relay paths in the mesh.

class CircuitBreakerRegistry {
  getOrCreate(pathId: string, options?: CircuitBreakerOptions): MeshCircuitBreaker;
  getState(pathId: string): "CLOSED" | "OPEN" | "HALF_OPEN" | undefined;
  remove(pathId: string): void;
  listOpenPaths(): string[];
}

3. HalfOpenProbe

Sends lightweight probe messages during HALF_OPEN to test path health without risking real traffic.

class HalfOpenProbe {
  constructor(options: {
    registry: CircuitBreakerRegistry;
    probeIntervalMs: number; // default 5000
    probeMessageFactory: () => MessageEnvelope;
  });
  start(): void;
  stop(): void;
}

Probes are ping-like messages with type: "relay:probe" and expect an ack. They do not carry application payload.

4. CircuitBreakerMiddleware

Integrates with network-plugin-relay by wrapping HostBackboneTransport.send():

function createCircuitBreakerMiddleware(
  registry: CircuitBreakerRegistry
): RelayMiddleware {
  return {
    beforeSend(envelope, nextHopHostId) {
      const pathId = buildPathId(localHostId, nextHopHostId);
      const cb = registry.getOrCreate(pathId);
      if (!cb.canExecute()) {
        throw new CircuitBreakerError(CB.PATH_OPEN, { pathId });
      }
      try {
        const result = next();
        cb.recordSuccess();
        return result;
      } catch (err) {
        cb.recordFailure();
        throw err;
      }
    },
  };
}

Error codes

const CB_CODES = defineCodes("CB", [
  "PATH_OPEN",
  "PATH_HALF_OPEN_REJECTED",
  "PROBE_FAILED",
  "REGISTRY_PATH_NOT_FOUND",
] as const);

CircuitBreakerError extends SuaveplanError<CircuitBreakerErrorCode>.


Dependencies

  • Consumes: @suaveplan/network (NetworkBus, MessageEnvelope), @suaveplan/network-plugin-relay (HostBackboneTransport, RelayManager), @suaveplan/retry (for underlying retry policy configuration), @suaveplan/error, @suaveplan/types, @opentelemetry/api.
  • Blocked on: network-core (shipped), network-plugin-relay.

Verification approach

  • src/breaker/breaker.spec-conformance.test.ts — pins CLOSED→OPEN→HALF_OPEN→CLOSED transition, threshold behaviour.
  • src/registry/registry.spec-conformance.test.ts — pins getOrCreate, listOpenPaths, removal.
  • src/probe/probe.spec-conformance.test.ts — pins probe emission, ack handling, failure counting.
  • src/middleware/middleware.spec-conformance.test.ts — pins relay middleware integration, fast-fail on OPEN.
  • 100/100/100/100 Istanbul coverage.

Conformance requirements (proposed)

  • NET-84 — MeshCircuitBreaker transitions from CLOSED to OPEN after failureThreshold consecutive failures.
  • NET-85 — MeshCircuitBreaker transitions from OPEN to HALF_OPEN after openDurationMs.
  • NET-86 — MeshCircuitBreaker transitions from HALF_OPEN to CLOSED after successThreshold consecutive successes.
  • NET-87 — MeshCircuitBreaker in OPEN state rejects all executions immediately.
  • NET-88 — HalfOpenProbe sends lightweight probe messages during HALF_OPEN.
  • NET-89 — CircuitBreakerRegistry manages per-path circuit breakers.
  • NET-90 — Circuit breaker middleware wraps HostBackboneTransport.send() and records success/failure.

Out of scope

  • Adaptive thresholds → v2; v1 uses fixed configuration.
  • ML-based anomaly detection → v2.
  • Per-peer circuit breakers → handled by @suaveplan/retry at the transport layer; this plugin focuses on mesh-boundary paths.