Posts from this year

Questions or comments? Email me.

Wed 01 January 2020

Wobbling in Rust

In 2019, one of my goals was to get back to writing here more regularly. I'm proud to say I was mostly successful in that endeavor: it was certainly an increase from the amount written in 2018, and absolutely a win over 2017 (which saw... no posts). I think personal websites still have value in the modern era, and so I'm going to continue this goal in 2020 and try to write at least a post a month.

To start things off, I figured this'd be an amusing topic - equal parts stupid, dangerous, and potentially useful.

How to never fail in Rust

Let's say that you've got a task you need to run. For whatever reason, a multitude of runtime errors can occur where you need to just keep retrying - for example, an upstream network endpoint is finnicky and drops the connection sometimes, and resuming is not an option. It can panic due to a third party library, too - Rust is still a newer language, even with the insane growth it's seen the past few years, so this isn't too out there concept-wise.

Assuming we have a method like the following:

use std::error::Error;

fn do_something() -> Result<(), Box<dyn Error>> {
    // Code that could potentially Err() or panic
    Ok(())
}

Our goal is simple: how do we just keep retrying this thing, results be damned?

Unwinding a panic

In the standard library, we can find some methods that are useful for this problem. The one we use below is std::panic::catch_unwind, which will catch unwinding panic events. Note that this doesn't cover every type of panic event - as the docs point out, one that aborts the process will bypass this entirely. It returns a Result, where the Ok variant is just the result of a passed in closure, and the Err variant being the cause of the panic.

Hey, listen! This is a Footgun.

The docs do point out that you should not use this as a general try/catch mechanism. They're right.

My use case for this was a panic happening somewhere in ssh2, where I'd occassionally run into a panic due (seemingly) to it linking against C code. In my particular case, it was easier to just keep trying since it was randomly failing, but would work 100% fine the rest of the time.

It's entirely possible this was a bug in an older version of ssh2 and would not happen today. Consider this trick a footgun and only contemplate using it if you're truly in a situation that requires it.

You've been warned!

Disclaimers out of the way, we now know how to catch and loop a panic. The code below illustrates a naive way to loop and handle both panic and Err events. You may notice we mark the passed in closure as AssertUnwindSafe - this is a wrapper type around our closure to tell the compiler that we know what we're doing. In my case, the closure wasn't actually capturing anything as it created its own local state each time. If your scenario differs, you should make sure you've read the docs on this one.

use std::error::Error;
use std::panic::{catch_unwind, AssertUnwindSafe};

/// Given a closure, will attempt to run it and unwind from panics... infinitely
/// retrying until it works.
///
/// Maybe.
fn wobble<
    F: Fn() -> Result<(), Box<dyn Error>>
>(process: F) -> Result<(), Box<dyn Error>> {
    // Loop to keep catching panic events and try again if they fail...
    // Notice the `AssertUnwindSafe`!
    loop {
        let result = catch_unwind(AssertUnwindSafe(|| {
            loop {
                match process() {
                    // No errors? Break out of the loop.
                    Ok(_) => { break; },

                    // Errors? I'd put some logging/backoff/etc here.
                    // Otherwise it'll just keep looping.
                    Err(e) => {}
                }
            }
        }));

        // Break the outer loop if we go through everything fine.
        if result.is_ok() {
            break;
        }

        // Well a panic definitely happened. Unwrapping the `result` will
        // give us information about it. This is naive logging - you'd want
        // to substitute your own stuff here.
        eprintln!("{:?}", result.unwrap());

        // Perhaps you'd want to implement some backoff logic here, or something.
        // Sky's the limits.
    }

    Ok(())
}

Usage is relatively straightforward. For example:

fn main() {
    wobble(|| {
        // Or just do_something()
        do_something()?;
        Ok(())
    });
}

Why's it called Wobbling?

It's an "infinite" move (or glitch, some might say) in Super Smash Bros Melee for the Nintendo Gamecube. I chose the name because the move is divisive, not something most people enjoy seeing in competitive tournaments, and while valid (depending on the ruleset), might not be the best way to play the game. It kind of symbolically fits here: you probably don't want to use this, and it's arguably not a smart way of doing things... but hey, you might make top 8 once in awhile.

Great! Should I use this?

Probably not.

I mean, sure, you can. For a small subset of problems it can be a useful trick. It's probably not what you want to do, though. It almost feels like a lurking unsafe {} without explicitly being so. The code could also be slightly less verbose, for sure, albeit code golf wasn't the point here.

With that said, it definitely worked for me. Lately I've come to value my time more highly than I did in the past, and this let me focus on other things while still logging errors to determine what the actual issue was. I'll call it a win.

Ryan around the Web