Networking timeouts and retries — DedSec Project
Networking failures — bounded retries and idempotent writes Failure model A timeout tells the client that it did not receive an answer; it does not prove the remote server performed no…
Networking failures — bounded retries and idempotent writes
Failure model
A timeout tells the client that it did not receive an answer; it does not prove the remote server performed no work. A POST that committed just before its reply was lost must not create a second charge during retry. Distinguish permanent bad requests (400 or 401) from potentially transient 429 and 503 responses. A real client must also respect Retry-After when valid, a total deadline, connection and read timeouts, and a maximum number of attempts.
Executable failure simulation (Python 3)
Save as retry_lab.py and run python3 retry_lab.py. The transport is simulated; no HTTP request, financial transaction or network access takes place. A response is deliberately lost after the first logical write.
def plan(statuses, max_attempts=3):
visits = []
for status in statuses[:max_attempts]:
visits.append(status)
if status == 200:
return ("OK", visits)
if status not in (429, 503):
raise ValueError("permanent HTTP failure")
raise TimeoutError("attempt budget exhausted")
assert plan([503, 429, 200]) == ("OK", [503, 429, 200])
for bad in ([400, 200], [503, 503, 503]):
try:
plan(bad)
except (ValueError, TimeoutError):
pass
else:
raise AssertionError("bad retry accepted")
ledger = {}
def post(key, fail_response=False):
if not key:
raise ValueError("idempotency key required")
if key not in ledger:
ledger[key] = len(ledger) + 1
if fail_response:
raise TimeoutError("reply lost after commit")
return ledger[key]
try:
post("order-7", True)
except TimeoutError:
pass
else:
raise AssertionError("missing simulated timeout")
assert post("order-7") == 1 and len(ledger) == 1
print("PASS: bounded retries and no double commit")
Why retry safety is not just a loop
The plan stops immediately on a permanent response and throws after the attempt budget. The ledger uses a stable idempotency key: two attempts with order-7 refer to the same operation and return the same ID. Real servers must persist idempotency keys and their results atomically with the side effect; a local dictionary cannot do this across restarts. A server that rejects or forgets keys after a TTL needs an explicitly documented retry window. GET is generally intended to be safe/idempotent, but intermediaries and poorly designed endpoints can violate assumptions.
Exercises and diagnostics
- Change statuses to [503, 400, 200]; only two attempts should occur and the permanent error should propagate.
- Make the server fail after committing twice without a stable idempotency key. Show the duplicate, then restore the key and verify one logical write.
- Add a capped exponential delay schedule and deterministic jitter generated from a seeded random source. Test only calculation, never use real sleeps in unit tests.
- Design the clock budget: a 2-second total deadline and a per-attempt timeout must leave time for at least one response. Explain why unlimited backoff violates this guarantee.
Completion proof: PASS, an explicit 429/503/400 policy, a lost-response trace, and a note identifying durable idempotency as a server requirement. The simulation does not establish reliability of a real provider.