Robust Network Request with Retry Logic
Implementing retry logic is crucial for building resilient applications, especially when dealing with external services like network requests. This challenge asks you to create a reusable function in Rust that attempts a given operation (represented by a closure) a specified number of times, with a configurable delay between retries, handling potential errors gracefully.
Problem Description
You need to implement a retry function that takes a closure representing an operation that might fail, the maximum number of retry attempts, and a delay (in seconds) between retries. The closure should return a Result<T, E>, where T is the type of the successful result and E is the type of the error. The retry function should execute the closure, and if it returns an Err, it should retry the operation up to the maximum number of attempts. A delay should be introduced between each retry. If the operation succeeds, the retry function should return the successful result. If the maximum number of attempts is reached without success, the function should return the last error encountered.
Key Requirements:
- Closure-based Operation: The operation to be retried should be passed as a closure.
- Retry Count: The function should accept a maximum retry count.
- Delay: A delay (in seconds) should be introduced between retries.
- Error Handling: The function should handle errors returned by the closure and return the last error encountered if all retries fail.
- Success Handling: The function should return the successful result if the operation succeeds within the retry limit.
- Clear Return Type: The function should return a
Result<T, E>to indicate success or failure.
Expected Behavior:
- The function should execute the provided closure.
- If the closure returns
Ok(value), the function should immediately returnOk(value). - If the closure returns
Err(error), the function should wait for the specified delay. - The function should then retry the closure up to the maximum number of attempts.
- If the closure succeeds on any retry, the function should return
Ok(value). - If the closure fails after all retries, the function should return the last
Err(error)encountered.
Edge Cases to Consider:
- Zero Retry Attempts: If the retry count is zero, the function should execute the closure once and return the result immediately.
- Immediate Success: If the closure succeeds on the first attempt, no delay should be introduced.
- Negative Retry Attempts: Handle negative retry attempts gracefully (e.g., treat as zero).
- Zero Delay: Handle a zero delay gracefully (no delay).
Examples
Example 1:
Input: retry(|| { Err(std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "Connection refused")) }, 3, 1)
Output: Err(std::io::Error { kind: ConnectionRefused, display: "Connection refused" })
Explanation: The closure always returns a connection refused error. After 3 retries with a 1-second delay between each, the last error is returned.
Example 2:
Input: retry(|| { Ok(5) }, 3, 1)
Output: Ok(5)
Explanation: The closure always returns Ok(5). The function returns Ok(5) immediately.
Example 3:
Input: retry(|| { if rand::random::<f64>() > 0.5 { Ok(10) } else { Err(std::io::Error::new(std::io::ErrorKind::TimedOut, "Timeout")) } }, 5, 0.5)
Output: (Could be Ok(10) or Err(std::io::Error { kind: TimedOut, display: "Timeout" }))
Explanation: The closure randomly returns either Ok(10) or Err(Timeout). The output depends on whether the closure succeeds within the 5 retry attempts and 0.5-second delay.
Constraints
- The delay between retries must be a non-negative floating-point number.
- The retry count must be a non-negative integer.
- The function should be generic over the result type
Tand the error typeE. - The function should not block the current thread indefinitely. Use
std::thread::sleepfor the delay. - The function should be reasonably efficient; avoid unnecessary allocations.
Notes
- Consider using a loop to implement the retry logic.
- The
std::thread::sleepfunction can be used to introduce the delay. - Think about how to handle the error type effectively. Returning the last error encountered is a common approach.
- The
randcrate is used in Example 3 for demonstration purposes. You may need to add it to yourCargo.tomlif you want to reproduce that example exactly.rand = "0.8"