Robust Service Wrapper in Python
This challenge focuses on creating a reusable and robust wrapper around a hypothetical external service. Service wrappers are crucial for abstracting away the complexities of interacting with external APIs, handling errors gracefully, and providing a consistent interface for your application. Your task is to design and implement a Python class that encapsulates the interaction with this service, including retry logic and error handling.
Problem Description
You are tasked with building a ServiceWrapper class in Python. This class will interact with a simulated external service (represented by a function external_service – provided below). The external_service function may occasionally fail (simulate a 50% chance of failure). The ServiceWrapper should handle these failures with a retry mechanism and provide a clean interface for calling the underlying service.
The ServiceWrapper class should have the following methods:
__init__(self, max_retries=3, initial_delay=1): Initializes the wrapper with a maximum number of retries and an initial delay (in seconds) for exponential backoff.call_service(self, *args, **kwargs): This is the primary method. It calls theexternal_servicefunction with the provided arguments. It should implement retry logic with exponential backoff. If the service call fails after the maximum number of retries, it should raise an exception.
The retry logic should work as follows:
- On initial failure, wait for
initial_delayseconds. - On subsequent failures, double the delay before retrying.
- The maximum number of retries is controlled by the
max_retriesparameter.
Examples
Example 1:
Input: wrapper = ServiceWrapper(max_retries=2, initial_delay=0.5); wrapper.call_service("request1")
Output: "Service call successful: request1"
Explanation: The external service might fail initially, but the wrapper retries and eventually succeeds.
Example 2:
Input: wrapper = ServiceWrapper(max_retries=1, initial_delay=1); wrapper.call_service("request2")
Output: Raises ServiceCallFailed exception after 2 seconds (1 + 2).
Explanation: The external service fails, the wrapper retries once after a 1-second delay, and then fails again, raising the exception.
Example 3: (Edge Case - No Retries)
Input: wrapper = ServiceWrapper(max_retries=0, initial_delay=1); wrapper.call_service("request3")
Output: Raises ServiceCallFailed exception immediately.
Explanation: No retries are configured, so the first failure immediately raises the exception.
Constraints
max_retriesmust be a non-negative integer.initial_delaymust be a positive float.- The
external_servicefunction is provided and should not be modified. - The
ServiceWrapperclass should handle exceptions raised byexternal_servicegracefully. - The exponential backoff delay should be calculated correctly.
- The
ServiceCallFailedexception should be raised when the maximum number of retries is reached.
Notes
- Consider using
time.sleep()for implementing the delays. - You can use a custom exception class
ServiceCallFailedto indicate failure after retries. - Think about how to handle different types of exceptions raised by the
external_service. For simplicity, assume all exceptions are considered failures. - The
external_servicefunction is provided below for testing purposes. It simulates a service that fails 50% of the time.
import random
import time
def external_service(*args, **kwargs):
"""
Simulates an external service that fails 50% of the time.
"""
if random.random() < 0.5:
raise Exception("Service unavailable")
return f"Service call successful: {args[0] if args else kwargs.get('data', 'no data')}"
class ServiceCallFailed(Exception):
pass