DevOps QA / Advanced: operational validation and governance / Lesson 16
Resilience validation with controlled failures
Test timeout, retry, and recovery behavior in bounded synthetic experiments before relying on resilience claims.
On this page
Lesson goals
- Define a failure hypothesis, invariant, and stop condition.
- Verify recovery and side-effect safety, not only error handling.
Turn a resilience claim into an experiment
A resilience claim describes behavior under a particular failure, such as preserving a confirmed reservation when email delivery times out. Define steady-state expectations, injected condition, affected boundary, duration, allowed impact, and recovery criteria. Start with pure simulations and isolated component environments. Real fault injection or production experiments require separate operational authorization and controls and are not performed here. An experiment should answer one meaningful question, not merely demonstrate that a process can be killed. Protect data integrity and user-visible outcomes even when dependencies are unavailable.
Test bounded retries without real waiting
A retry policy needs a maximum attempt or time budget, retryable failure classification, backoff, and idempotency for side effects. Retrying every exception can amplify an outage and duplicate writes. Use an injected dependency to simulate outcomes deterministically and verify the bound. The example makes no network request and does not sleep. It retries only a teaching TransientError and stops after the configured number of attempts. A real client additionally handles cancellation, jitter, deadlines, and server guidance through its supported implementation.
class TransientError(Exception):
pass
def bounded_call(operation, attempts=3):
if attempts < 1:
raise ValueError("INVALID_ATTEMPT_LIMIT")
for attempt in range(attempts):
try:
return operation()
except TransientError:
if attempt == attempts - 1:
raise
The callback is local and synthetic. This excerpt omits timing deliberately so unit tests do not wait on wall time. Do not wrap non-idempotent real writes in this helper without an explicit remote deduplication and recovery contract.
Assert what must remain true under failure
A timeout response is not enough to validate resilience. Check durable records, queue state, duplicate side effects, capacity accounting, and the userβs ability to retry safely. An email outage should not erase a committed booking, while an ambiguous booking write must not be repeated with a fresh operation identity. Choose invariants that cross the failure boundary. Use a controlled fake to pause after commit or acknowledgment and verify recovery from that exact point. This is more informative than random failures whose timing is never captured.
Verify the failure budget explicitly
The test below proves that a permanently transient fake is invoked exactly three times before the helper raises. It is an interaction assertion because the retry bound itself is the contract. Add separate cases for immediate success, success after one failure, and a permanent error that should not be retried. Keep fixture state local to the case so parallel tests cannot alter the count. The helper and test together are safe local examples; they do not exercise a real service, process supervisor, or infrastructure fault mechanism.
import unittest
class RetryBudgetTests(unittest.TestCase):
def test_transient_failure_stops_at_budget(self):
calls = []
def unavailable():
calls.append("attempt")
raise TransientError("LAB_UNAVAILABLE")
with self.assertRaises(TransientError):
bounded_call(unavailable, attempts=3)
self.assertEqual(len(calls), 3)
Import bounded_call and TransientError from the preceding helper in a real test file. The error message is synthetic and identical in both course languages. A passing unit test does not prove a production client applies the same policy; add component-level evidence for that wiring.
Include recovery, observability, and limits
A system can survive the injected failure yet fail to recover because queues never drain, circuit state stays open, or resource leaks accumulate. Observe recovery time, pending work, integrity, and resource use after the condition ends. Define abort conditions and a cleanup owner before an isolated experiment. Keep concurrency and load bounded so the experiment does not become an uncontrolled stress test. Record the exact hypothesis and result, including limitations, then turn discovered defects into deterministic regression cases. Resilience evidence is strongest when it explains both failure behavior and the return to a healthy user task.
Common errors and fixes
Retries amplify the dependency outage
- Why it happens
- Every failure is retried without a shared budget.
- How to fix it
- Bound attempts and time and classify retryable failures.
- Prevent it next time
- Test overload and retry interactions in isolation.
A timeout duplicates a reservation
- Why it happens
- The retry uses a new operation identity.
- How to fix it
- Preserve identity and verify remote idempotency.
- Prevent it next time
- Test acceptance-before-acknowledgment interruption.
The service returns but pending work never recovers
- Why it happens
- Only the failure period was observed.
- How to fix it
- Verify queue drain and healthy-state restoration.
- Prevent it next time
- Include recovery criteria in every experiment plan.
Check your understanding
1. Email delivery fails after a booking commits. What should the test verify beyond an error message?
Show worked solution
Confirm the booking remains durable, capacity is correct, notification work is tracked for the chosen retry policy, and the user receives an honest status. Recovery should send at most the intended notification without creating another booking.
2. A retry helper stops after three attempts in a unit test. What additional evidence validates real wiring?
Show worked solution
A controlled component test should exercise the configured client with a fake failing dependency, inspect actual attempt count and timing budget, and verify cancellation and permanent-error behavior. Keep network scope local and bounded; do not infer deployment policy from an isolated helper alone.
Key takeaways
- Resilience experiments need explicit hypotheses and invariants.
- Retries require bounds and side-effect safety.
- Recovery is part of the claim, not an optional afterthought.