Retry Loop Retry

Some time ago I lamented that I don’t know how to write a retry loop such that:

https://matklad.github.io/2023/12/21/retry-loop.html

To recap, we have

fn action() E!T { ... }
fn is_transient_error(err: E) bool { ... }

and we need to write

fn action_with_retries(retry_count: u32) E!T { ... }

I’ve received many suggestions, and the best one was from https://www.joachimschipper.nl, though it was somewhat specific to Python:

for tries_left in reverse(range(retry_count)):
    try:
        return action()
    except Exception as e:
        if tries_left == 0 or not is_transient_error(e):
            raise
        sleep()
else:
    assert False

A couple of days ago I learned to think better about the problem. You see, the first requirement, that the number of retries is bounded syntactically, was leading me down the wrong path. If we start with that requirement, we get code shape like:

const result: E!T = for (0..retry_count) {
    // ???
    action()
    // ???
}

The salient point here is that, no matter what we do, we need to get E or T out as a result, so we’ll have to call action() at least once. But retry_count could be zero. Looking at the static semantics, any non do while loop’s body can be skipped completely, so we’ll have to have some runtime asserts explaining to the compiler that we really did run action at least once. The part of the loop which is guaranteed to be executed at least once is a condition. So it’s more fruitful to flip this around: it’s not that we are looping until we are out of attempts, but, rather, we are looping while the underlying action returns an error, and then retries are an extra condition to exit the loop early:

var retries_left = retry_count;
const result = try while(true) {
    const err = if (action()) |ok| break ok else |err| err;
    if (!is_transient_error(err)) break err;

    if (retries_left == 0) break err;
    retries_left -= 1;
    sleep();
};

This shape of the loop also works if the condition for retries is not attempts based, but, say, time based. Sadly, this throws “loop is obviously bounded” requirement out of the window. But it can be restored by adding upper bound to the infinite loop:

var retries_left = retry_count;
const result = try for(0..retry_count + 1) {
    const err = if (action()) |ok| break ok else |err| err;
    if (!is_transient_error(err)) break err;

    if (retries_left == 0) break err;
    retries_left -= 1;
    sleep();
} else @panic("runaway loop");

I still don’t like it (if you forget that +1, you’ll get a panic!), but that’s where I am at!