Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Non-async API #7

Open
quietlychris opened this issue Oct 27, 2024 · 3 comments
Open

Non-async API #7

quietlychris opened this issue Oct 27, 2024 · 3 comments

Comments

@quietlychris
Copy link

quietlychris commented Oct 27, 2024

I've been really curious about building operations on potentially-fallible synchronous code as well, without necessarily needing to pulling in an async runtime and block on a given function. The context of this is also for code that non-deterministically fails; right now, I'm just spawning a thread and crossing my fingers, but it would be cool to add some level of fault-tolerancy to that. However, most of that code is not explicitly asynchronous, and it would be a bit of a pain to convert the entire codebase over to async to support what ends up being (for me) like 12 lines of code (which spawn functions that are much larger).

use std::time::Duration;

fn fallible_operation(msg: &str) -> std::io::Result<()> {
    // Your potentially failing operation here
    Err(std::io::Error::other(msg))
}

fn main() {
    // Spawns a background thread with some fault tolerancy
    let result = mulligan::until_ok()
        .stop_after(5)                     // Try up to 5 times
        .max_delay(Duration::from_secs(3)) // Cap maximum delay at 3 seconds
        .exponential(Duration::from_secs(1)) // Use exponential backoff
        .full_jitter()                     // Add randomized jitter
        .retry(move || {
            fallible_operation("connection failed").await
        }); 
    
    // Do other stuff
    loop {}

}

I've also been thinking about composable ways to joining a bunch of these functions together with some level of fault tolerance; in my head, it's looked a bit like Bevy's way of adding systems to an App, although for mulligan, it might not need schedule markers like Startup alongside the function definitions. That way, it could end up sort of similar to creating fallible actors but without needing to manage overhead of things like the messaging structures that typically come along with those frameworks.

use std::time::Duration;

fn fallible_operation(msg: &str) -> std::io::Result<()> {
    // Your potentially failing operation here
    Err(std::io::Error::other(msg))
}

fn fallible_operation_1(other_msg: &str) -> std::io::Result<()> {
    // A different potentially-failing operation
    Err(std::io::Error::other(other_msg))
}

fn main() {

    // Have a manager for lots of functions, where we want the same re-start strategy for all of them
    // If you wanted different strategies, you 
    let result = mulligan::Manager::new()
        .stop_after(5)                     // Try up to 5 times
        .max_delay(Duration::from_secs(3)) // Cap maximum delay at 3 seconds
        .exponential(Duration::from_secs(1)) // Use exponential backoff
        .full_jitter()                     // Add randomized jitter
        .manage(move ||  {
            fallible_operation("connection failed")
        })
        .manage(move || {
            fallible_operation_1("rng condition eventually led to a panic")
        })
        .run();
; 
    
    // Do other stuff
    loop {}

}

Apologies for hijacking your thread a bit (and of course feel free to ignore any/all of this), but having a library that does stuff like mulligan does even now has been something I've been wishing existed for a bit, and I'm really excited to see someone building stuff in the space :)

@quietlychris
Copy link
Author

quietlychris commented Oct 27, 2024

(wait, I was still composing this!! really sorry)

@theelderbeever
Copy link
Owner

Not a problem at all and not a hijack! Looking for open feedback and I was actually already thinking a blocking retryable was something I wanted to add. I haven't gotten around to using Bevy yet so I will need to digest the manager idea. Right now it was just going to be a .retry_blocking call instead of the async .retry one and use std::thread::sleep or park or something. Need to research.

Right now (need to add Clone to Mulligan) you should be able to reuse the struct as much as you want since .retry(...) doesn't have any side effects. So you build the strategy then just use .retry wherever you want. That said I am trying to create .jitter(j: Jitter) and .backoff(b: Backoff) trait implementations for better extensibility and that might upset the reuse without Cloning first. ie: .retry(...) would consume the struct. So the Manager could maybe leverage some of that?

Feel free to open a PR with Manager code though if you want otherwise I can look at it a little later. Things might be changing quite a bit though in the next day or so just as I grok the initial feedback from people.

@quietlychris
Copy link
Author

quietlychris commented Oct 27, 2024

That sounds great! I'll keep an eye on things to see how they change :) I'm fairly sure I won't have immediately to dig into a Manager struct, but might during mid-November. Regardless, I'lll be keeping it in the back of my mind and will also likely have some time towards the end of the year where I sort of expect to pick that problem up one way or the other--if there's a way I can integrate that with Mulligan, I'll be more than happy to open a PR! Thanks so much for being willing to engage in this sort of conversation, and looking forward to seeing how this project evolves!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants