ADR-006: Circuit Breaker on Safety Kernel Client
- Status: Accepted
- Date: 2026-01-15
Context
The Safety Kernel is a synchronous dependency on the scheduler hot path (see ADR-001). If the Safety Kernel becomes unavailable or slow, the scheduler cannot dispatch any jobs. Without a fallback mechanism, a Safety Kernel outage causes total platform unavailability.
Decision
The scheduler's Safety Kernel client uses a circuit breaker pattern with configurable thresholds.
States
CLOSED (normal) → OPEN (tripped) → HALF-OPEN (probe)
↑ │
└────────────────────────────────────┘
- Closed: All requests forwarded to Safety Kernel normally.
- Open: After N consecutive failures, the circuit trips. Requests are short-circuited with a fallback decision (configurable: deny-all or allow-all).
- Half-Open: After a cooldown period, one probe request is sent. If it succeeds, the circuit closes. If it fails, the circuit reopens.
Configuration
| Parameter | Default | Description |
|---|---|---|
| Failure threshold | 3 | Consecutive failures before tripping (safetyCircuitFailBudget) |
| Cooldown period | 30s | Time in open state before probing (safetyCircuitOpenFor) |
| Half-open probes | 3 | Probe budget while half-open (safetyCircuitHalfOpenMax) |
| Successes to close | 2 | Consecutive probe successes that re-close the circuit (safetyCircuitCloseAfter) |
| Timeout per call | 2s | gRPC deadline for safety-kernel calls (safetyTimeout) |
Behavior When the Circuit Is Open
The circuit breaker itself does not decide allow/deny. When the circuit is
open, SafetyClient.Check returns a SafetyUnavailable record (reason
"safety kernel circuit open"); the scheduler then resolves what to do with the
job using its fail mode:
- fail-closed (default): the job is not allowed through — the scheduler
requeues it via
RetryAfteruntil the kernel recovers. No unchecked action executes. Set withWithInputFailMode("closed"). - fail-open (opt-in): the job is dispatched with a
SafetyAllowdecision, taggedsafety_bypassed=true, and a dedicated safety-bypass audit event is emitted. Set withWithInputFailMode("open").
Fail mode is resolved per request: a per-tenant FailModeResolver overrides the
global flag when configured (isInputFailOpenForTenant), otherwise the global
default (fail-closed) applies. The same model governs the async output-safety
check (isAsyncFailOpenForTenant).
Observability
IncSafetyUnavailable(topic)— incremented whenever the kernel is unavailable (circuit open or call error).IncInputFailOpen(topic)— incremented when a job is allowed through under fail-open.- Redis counter
cordum:scheduler:input_fail_open_total— durable fail-open tally. - A safety-bypass audit event is emitted on every fail-open dispatch so SIEM can detect bypass-on-unavailable without parsing logs.
Key source files:
core/controlplane/scheduler/circuit_breaker.go—RedisCircuitBreaker(distributed breaker + local fallback)core/controlplane/scheduler/safety_client.go— breaker thresholds,safetyTimeout, open-circuit handlingcore/controlplane/scheduler/engine.go—SafetyUnavailablehandling, fail-open/fail-closed resolution
Consequences
Positive:
- Platform degrades gracefully instead of hanging on Safety Kernel outage
- Configurable fail mode (per-tenant or global) lets operators choose safety vs availability tradeoff
- Automatic recovery when Safety Kernel comes back (no manual intervention)
- Observability signals (fail-open metrics + audit events) enable alerting on bypass-on-unavailable
Tradeoffs:
- fail-closed (default) means jobs are requeued and processing stalls during an outage
- fail-open means unchecked actions can execute (risk) — gated behind explicit opt-in + audit trail
- Circuit breaker adds complexity to the evaluation path
- Probe requests during half-open may see stale results