Retry Loop

A post about writing a retry loop. Not a smart post about avoiding thundering heards and resonance. A simpleton kind of post about wrangling ifs and fors together to minimize bugs.

Stage: you are writing a script for some build automation or some such.

Example problem: you want to get a freshly deployed package from Maven Central. As you learn after a CI failure, packages in Maven dont become available immediately after a deploy, there could be a delay. This is a poor API which breaks causality and makes it impossible to code correctly against, but what other alternative do you have? You just need to go and write a retry loop.

You want to retry some action. The action either succeeds or fails. Some, but not all, failures are transient and can be retried after a timeout. If a failure persists after a bounded number of retries, it should be propagated.

The runtime sequence of event we want to see is:

action()
sleep()
action()
sleep()
action()

It has that mightily annoying a-loop-and-a-half shape.

Heres the set of properties I would like to see in a solution:

  1. No useless sleep. A naive loop would sleep one extra time before reporting a retry failure, but we dont want to do that.
  2. In the event of a retry failure, the underlying error is reported. I dont want to see just that all attempts failed, I want to see an actual error from the last attempt.
  3. Obvious upper bound: I dont want to write a while (true) loop with a break in the middle. If I am to do at most 5 attempts, I want to see a for (0..5) loop. Dont ask me why.
  4. No syntactic redundancy there should be a single call to action and a single sleep in the source code.

I dont know how to achieve all four. Thats the best I can do:

fn action() !enum { ok, retry: anyerror } {

}

fn retry_loop() !void {
    for (0..5) {
        if (try action() == .ok) break;
        sleep();
    } else {
        switch (try action()) {
            .ok => {},
            .retry => |err| return err
        }
    }
}

This solution achieves 1-3, fails at 4, and relies on a somewhat esoteric language feature for/else.

Salient points: