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!

Corrections as of March 19th, 2020

A few weeks after this was posted, it wound up /r/rust, where /u/najamelan and /u/viaxxdev pointed out some issues with the original code in this post. This has been updated to take their points into account, and major thanks to them for catching it!

Later in the day, type_N_is_N_to_Never also noted that this could be reduced down - so the code block below now uses that approach, as it's cleaner and arguably easier to digest. Nice!

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 panic events. Notice we mark the passed in closure as requiring RefUnwindSafe.

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

/// Given a closure, will attempt to run it and unwind from panics... infinitely
/// retrying until it works.
///
/// Maybe.
fn wobble<
    F: RefUnwindSafe + Fn() -> Result<(), Box<dyn Error>>
>(process: F) {
    while let Err(err) = catch_unwind(|| process()) {
        eprintln!("{:?}", err);
    }
}

Usage is relatively straightforward. For example:

fn do_something() -> Result<(), Box<dyn Error>> {
    panic!("Induce a panic here, for testing");
}

fn main() {
    wobble(|| {
        do_something()
    });
}

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