Robust Function Execution with Retries
In software development, it's common for operations to occasionally fail due to transient issues like network glitches, temporary service unavailability, or race conditions. Implementing retry logic allows your program to automatically reattempt these operations, increasing the overall resilience and reliability of your application. This challenge focuses on building a flexible retry mechanism in Python.
Problem Description
Your task is to implement a Python function decorator that adds retry logic to any decorated function. This decorator should allow for a specified number of retries and a delay between each retry. The decorated function might raise exceptions, and the decorator should catch these exceptions and trigger a retry until either the function succeeds or the maximum number of retries is exhausted.
Key Requirements:
- Decorator Implementation: Create a Python decorator that can be applied to any function.
- Max Retries: The decorator should accept an argument specifying the maximum number of times to retry the function.
- Retry Delay: The decorator should accept an argument specifying the delay (in seconds) between retries.
- Exception Handling: The decorator should catch specific exceptions (or all exceptions if not specified) raised by the decorated function.
- Success: If the decorated function executes successfully without raising an exception within the allowed retries, its return value should be returned.
- Failure: If the decorated function consistently fails after all retries, the last exception raised should be re-raised by the decorator.
- Flexibility: The retry logic should be easily configurable.
Expected Behavior:
When a decorated function is called, it will be executed. If it raises an exception, the decorator will wait for the specified delay and then re-execute the function. This process repeats up to the maximum number of retries. If the function succeeds at any point, its result is returned. If all retries fail, the exception from the final attempt is propagated.
Important Edge Cases:
- What happens if
max_retriesis 0? - What happens if
delayis 0? - Consider a scenario where the decorated function returns
Noneor other falsy values – these should still be considered successful executions.
Examples
Example 1:
import time
@retry(max_retries=3, delay=1)
def unreliable_operation(attempt_count):
print(f"Attempting operation (Attempt {attempt_count})...")
if attempt_count < 2:
raise ConnectionError("Simulated network issue")
print("Operation successful!")
return "Success"
# Calling the function
result = unreliable_operation(1)
print(f"Final Result: {result}")
Output:
Attempting operation (Attempt 1)...
Attempting operation (Attempt 2)...
Attempting operation (Attempt 3)...
Operation successful!
Final Result: Success
Explanation:
The unreliable_operation is called with attempt_count=1. It fails the first two times, raising ConnectionError. The retry decorator catches the exception, waits for 1 second, and retries. On the third attempt, the condition attempt_count < 2 is false, so the operation succeeds and returns "Success".
Example 2:
import time
@retry(max_retries=2, delay=0.5)
def always_fails():
print("This will always fail.")
raise ValueError("Something went wrong")
try:
always_fails()
except ValueError as e:
print(f"Caught expected exception after retries: {e}")
Output:
This will always fail.
This will always fail.
This will always fail.
Caught expected exception after retries: Something went wrong
Explanation:
The always_fails function is called. It fails on the first attempt. The decorator retries after 0.5 seconds. It fails again on the second attempt. The decorator retries one last time. Since max_retries is 2, this is the final attempt. The function fails again, and the decorator re-raises the ValueError.
Constraints
max_retrieswill be a non-negative integer (0 or greater).delaywill be a non-negative float representing seconds.- The decorated function can accept any number of positional and keyword arguments.
- The decorator should preserve the original function's metadata (like
__name__and__doc__).
Notes
- You can use the
time.sleep()function for implementing the delay. - Consider using
functools.wrapsto preserve function metadata. - For simplicity, you can initially focus on retrying on any exception. If you want to add more advanced filtering of which exceptions to retry on, that could be a follow-up enhancement.