Robust Operation: Implementing Retry Logic in Rust
Network requests, external API calls, and file operations are inherently fallible. They can fail due to transient issues like network glitches, temporary service unavailability, or resource contention. Implementing robust retry logic is crucial for building resilient applications that can automatically recover from such temporary failures. This challenge will guide you through creating a generic retry mechanism in Rust.
Problem Description
You are tasked with creating a Rust function that can execute a given operation and automatically retry it a specified number of times if it fails. The operation itself will be a closure that returns a Result. If the operation succeeds (returns Ok), the function should return the successful value. If it fails (returns Err), the function should wait for a specified duration before retrying, up to a maximum number of attempts.
Key Requirements:
- The retry function should be generic over the success type
Tand the error typeEof the operation. - It should accept the operation (as a closure
F: FnMut() -> Result<T, E>), the maximum number of retry attempts, and the delay between retries as parameters. - If the operation succeeds within the allowed attempts, return
Ok(T). - If the operation fails after all retry attempts have been exhausted, return the last error encountered wrapped in an
Err(E). - The delay between retries should be a
std::time::Duration.
Expected Behavior:
The function should execute the provided operation. If it returns Ok, the result is immediately returned. If it returns Err, the function should pause for the specified duration and then attempt the operation again. This process repeats until either the operation succeeds or the maximum number of retries is reached.
Edge Cases to Consider:
- An operation that succeeds on the first try.
- An operation that fails multiple times but eventually succeeds.
- An operation that consistently fails.
- Zero retry attempts.
Examples
Example 1:
Input:
operation: `|| { static mut count: i32 = 0; unsafe { count += 1; if count < 3 { Err("Temporary error") } else { Ok("Success!") } } }`
max_attempts: 5
delay: 100ms
Output:
Ok("Success!")
Explanation: The operation fails the first two times, triggering retries. On the third attempt, it succeeds and returns "Success!".
Example 2:
Input:
operation: `|| Err("Persistent error")`
max_attempts: 3
delay: 50ms
Output:
Err("Persistent error")
Explanation: The operation fails on the first attempt. It is retried twice more, failing each time. After the third failed attempt, the last error ("Persistent error") is returned.
Example 3:
Input:
operation: `|| Ok(100)`
max_attempts: 10
delay: 1s
Output:
Ok(100)
Explanation: The operation succeeds on the very first attempt. No retries are performed.
Constraints
max_attemptswill be ausizeand will be greater than or equal to 0.delaywill be astd::time::Durationand will be a positive duration.- The closure
Fwill return aResult<T, E>. - The error type
Emust implementDebugto be printable if needed for debugging purposes.
Notes
- You will need to use
std::thread::sleepfor introducing the delay. - Consider how to handle the
max_attemptsparameter. Ifmax_attemptsis 0, the operation should only be attempted once. - The function signature should look something like:
fn retry<T, E, F>(operation: &mut F, max_attempts: usize, delay: std::time::Duration) -> Result<T, E> where F: FnMut() -> Result<T, E>, E: std::fmt::Debug. - The use of
FnMutfor the closure allows it to mutate its environment if necessary (as seen in Example 1).