1.fhxd.de

The retry loop that sleeps until Thursday

2026-07-06

The bug report says the daemon "sometimes hangs after a network blip, but only on machines that have been up a while, and only after a reboot of the switch." That is not three conditions. It is one condition described three ways: the clock got stepped.

The shape of it

A retry loop written the obvious way:

deadline = time.time() + timeout
while time.time() < deadline:
    if attempt():
        return
    time.sleep(backoff)

time.time() is the wall clock. It is not required to move forward at one second per second, and it is not required to move forward at all. When the time daemon decides the local clock is badly wrong — which is exactly what happens after a long network partition, or on a VM whose host suspended — it steps the clock rather than slewing it. Step it backwards by an hour and that loop now runs for an hour and a timeout. Step it forward and the loop exits immediately, having made one attempt.

Use the clock that only goes forward

Every platform has a monotonic clock for precisely this. It has no relationship to civil time, it is unaffected by NTP and by timezone changes, and it is the only correct source for a duration:

deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
    ...

The rule is short enough to memorise: wall clock for timestamps you will show a human or store in a database, monotonic for every interval, timeout, deadline, and rate limit. If you are subtracting two times to get a duration, you want monotonic.

The same bug in shell

SECONDS in bash is derived from the wall clock and has the same defect. So does anything built on date +%s. On Linux the monotonic source from shell is:

awk '{print int($1)}' /proc/uptime

Not elegant, but it does not go backwards, which is the only property being asked of it.

Why it survives testing

Nobody steps the clock in CI. The failure needs a real machine, a real disruption, and a time daemon deciding to correct a real drift — and by the time it happens the logs have rotated. It is worth injecting deliberately: timedatectl set-time on a scratch VM while the loop runs will reproduce it in one attempt.