Bug Class 5: Failed Recovery
The dependency came back; the system didn’t. Recovery is the least-tested transition in production systems - almost every fault test checks behavior during the fault, almost none checks the system after the fault clears.
The incident
Kafka was down for five minutes at 03:00. It came back on its own; the on-call never woke up. But the consumer is stuck - dead connection pool, poisoned offset, a circuit breaker that never half-opens. Nobody notices until Monday, when the backlog graph has been climbing for sixty hours and the “real-time” dashboard is three days stale.
Why your tests miss it
Recovery bugs live in the transition, not in either steady state. Testing “works normally” passes; testing “fails when broker is down” passes; the stuck-forever path between them is exercised by neither. And in production the transition happens at 03:00, unobserved.
The spec that closes it
Scope the fault, let it clear, then make recovery itself the assertion - with a temporal property, not a sleep:
def attempt_during_outage():
# during the fault: deferred, not lost. The fault clears the moment
# run= returns, so recovery is what the eventually() below watches for.
fault(kafka, connect = deny("ECONNREFUSED"), run = lambda:
api.public.post(path = "/orders", body = '{"cart": 42}'))
test("broker_recovery",
body = attempt_during_outage,
expect = eventually(lambda trace:
trace.event(type = "kafka_publish", topic = "orders")),
timeout = "30s", # the bound: recovery must arrive within it
)
The verdict semantics matter here: eventually() renders PASS only if
the evidence arrives, FAIL if the run completes without it, and
INCONCLUSIVE if the test ended before the question was decided - a
timeout is never a silent green.
What to assert
- The deferred work actually flows after the fault clears - events published, queue drained, cache repopulated.
- Recovery happens within a bound (the test’s
timeout=), because “eventually, eventually” is an outage with better marketing. - Steady-state resumes: post-recovery requests behave identically to pre-fault ones (compare traces).
Which level
Either level for the fault - what defines this class is the scoped fault (it must end) plus the temporal assertion after it. See Choosing Fault Levels.
Learn the pieces
- Chapter 15: Monitors & Temporal Properties -
eventually(),await_stable() - Chapter 16: Network Partitions - recovery after a split
- Reading Reports & Verdicts - PASS / FAIL / INCONCLUSIVE