A breaker that opens on failure and closes on a timer either flaps or stays open, and the half-open state is what makes the recovery decision evidence-based.
// closed → requests pass, failures counted
// open → requests fail immediately, no call made
// half-open → ONE request passes. success closes,
// failure reopens the timer.
match ($this->state) {
State::Closed => $this->callAndCount($fn),
State::Open => $this->expired()
? $this->attemptReset($fn)
: throw new CircuitOpen(),
State::HalfOpen => throw new CircuitOpen(),
};
The half-open state must admit exactly one request, which requires a lock in any multi-process deployment — several workers each sending a probe is the thundering herd the breaker was supposed to prevent. Thresholds are better expressed as a failure rate over a window than as a consecutive count, because a service failing one request in three never trips a consecutive counter and is unambiguously unhealthy.