Skip to content
About
Main content

Replay from log and the catch-up condition

When does replaying a log rebuild the same state, and how much headroom does catching up need?

What it is. State can be written as a fold over the log: state = fold(f, initial, log), meaning: start from an initial state and apply the function f to every log record in order. If the log is complete and ordered, any state you can reach by processing the log you can reach again by processing it again from a known . does not need to know what the failure was, which makes it the cheapest mechanism to have. Its steady-state cost is a log you were already keeping.

Determinism is the precondition, and nobody checks it

Replay gives the same state only if f is a real function (same input, same output) and the log is its only input. The ways to break this are ordinary:

  • reading the clock: now(),
  • unseeded random numbers,
  • enrichment lookups against a table that has changed since,
  • summing floating-point numbers in a different partition order (floating-point addition is not associative: (a + b) + c can differ slightly from a + (b + c)),
  • hash iteration order,
  • and the subtle one: any dependence on the arrival order of events when that order is not itself recorded in the log.

The worst feature of this failure is that replay produces a different but plausible answer, silently, and only on the recovery path. The difference may surface weeks later as a reconciliation break. This is the clearest example here of a mechanism that assumes purity, cannot check purity, and lets the data pay without anyone filing a ticket. It is the third symptom: Wanjiru's code read now() and a lookup table that changed.

The catch-up condition

During an outage, new work keeps arriving. Recovery time is:

the system's maximum throughput
normal arrival rate
how long the outage lasted
Data table
Catch-up time as a multiple of the outage, by utilization
Utilization a/MCatch-up time ÷ outage
0.050.053×
0.0610.065×
0.0730.078×
0.0840.091×
0.0950.105×
0.1060.119×
0.1170.133×
0.1290.148×
0.140.163×
0.1510.178×
0.1630.194×
0.1740.21×
0.1850.227×
0.1960.244×
0.2070.262×
0.2190.28×
0.230.299×
0.2410.318×
0.2530.338×
0.2640.358×
0.2750.379×
0.2860.401×
0.2970.423×
0.3090.447×
0.320.471×
0.3310.495×
0.3420.521×
0.3540.547×
0.3650.575×
0.3760.603×
0.3870.633×
0.3990.663×
0.410.695×
0.4210.728×
0.4320.762×
0.4440.798×
0.4550.835×
0.4660.874×
0.4770.914×
0.4890.956×
0.51×
0.5111.05×
0.5221.09×
0.5341.14×
0.5451.2×
0.5561.25×
0.5681.31×
0.5791.37×
0.591.44×
0.6011.51×
0.6121.58×
0.6241.66×
0.6351.74×
0.6461.83×
0.6571.92×
0.6692.02×
0.682.12×
0.6912.24×
0.7032.36×
0.7142.49×
0.7252.64×
0.7362.79×
0.7482.96×
0.7593.15×
0.773.35×
0.7813.57×
0.7923.82×
0.8044.1×
0.8154.41×
0.8264.76×
0.8375.15×
0.8495.61×
0.866.14×
0.8716.77×
0.8827.51×
0.8948.41×
0.9059.53×
0.91610.9×
0.92712.8×
0.93915.3×
0.9519×

If the replay throughput does not exceed the arrival rate, recovery never ends.

The backlog drains at the headroommax 1,000/s · 3 h outage

To recover in T/k you need the drain rate to be k times the arrival rate: M − a = k·a, so M = (k + 1)·a. To recover in a third of the outage (k = 3) you need maximum throughput four times the arrival rate, which means running at 25% utilization.

The consequence is important: This is a different and usually stricter limit than the queueing argument (which says that latency grows quickly as utilization approaches 100%). It is also why Wanjiru's backlog barely shrank: results-intake ran at 90% of its maximum, so its drain rate was only 10% of maximum throughput (about 1/9 of the arrival rate). (The case where catch-up load breaks the system again is a replay storm.)

What bounds replay

The retention window: you can replay exactly as far back as someone decided to keep the log. So the retention policy is silently a recovery-capability policy, usually owned by a different team on cost grounds.