Hone logo
Hone
Problems

Implementing a Circuit Breaker Pattern in Python

The Circuit Breaker pattern is a crucial design pattern for building resilient and fault-tolerant systems. It prevents an application from repeatedly trying to execute an operation that is likely to fail, allowing it to recover and potentially retry later. This challenge asks you to implement a basic Circuit Breaker pattern in Python to protect a simulated external service.

Problem Description

You are tasked with creating a CircuitBreaker class in Python that wraps a function representing a call to an external service. The CircuitBreaker should monitor the success and failure rates of the wrapped function and automatically transition between three states: Closed, Open, and Half-Open.

  • Closed: The circuit is closed, and calls to the wrapped function are allowed. The circuit monitors the success/failure rate. If the failure rate exceeds a defined threshold within a specified time window, the circuit transitions to the Open state.
  • Open: The circuit is open, and calls to the wrapped function are immediately failed without actually calling the function. After a configured timeout period, the circuit transitions to the Half-Open state.
  • Half-Open: The circuit is half-open. A limited number of calls are allowed to the wrapped function. If these calls succeed, the circuit transitions back to the Closed state. If they fail, the circuit transitions back to the Open state.

The CircuitBreaker class should provide a call method that wraps the original function and handles the circuit breaker logic.

Key Requirements:

  • Implement the three states: Closed, Open, and Half-Open.
  • Track the number of successful and failed calls within a sliding time window.
  • Define configurable parameters:
    • failure_threshold: The percentage of failures that triggers the circuit to open.
    • recovery_timeout: The time (in seconds) the circuit remains in the Open state before transitioning to Half-Open.
    • half_open_attempts: The number of attempts allowed in the Half-Open state.
  • Use a sliding window approach for failure rate calculation.
  • Handle potential exceptions raised by the wrapped function.

Expected Behavior:

  • The call method should return the result of the wrapped function when the circuit is Closed and the call is successful.
  • The call method should return a predefined error message (e.g., "Circuit is Open") when the circuit is Open.
  • The call method should attempt a limited number of calls when the circuit is Half-Open.
  • The circuit should automatically transition between states based on the configured parameters and observed failure rates.

Edge Cases to Consider:

  • What happens if the wrapped function raises an exception? This should be counted as a failure.
  • How to handle concurrent calls to the call method? (Consider thread safety if necessary, though a basic implementation is acceptable for this challenge).
  • What happens if failure_threshold, recovery_timeout, or half_open_attempts are set to zero or negative values? (Handle gracefully, perhaps by defaulting to reasonable values).

Examples

Example 1:

# Assume a function 'external_service' that sometimes fails
def external_service():
    import random
    if random.random() < 0.5:
        return "Service Success"
    else:
        raise Exception("Service Failure")

circuit = CircuitBreaker(external_service, failure_threshold=0.5, recovery_timeout=2, half_open_attempts=3)

# Initially, the circuit is Closed
for _ in range(5):
    try:
        result = circuit.call()
        print(f"Call successful: {result}")
    except Exception as e:
        print(f"Call failed: {e}")

Output (will vary due to randomness):

Call failed: Service Failure
Call failed: Service Failure
Call successful: Service Success
Call failed: Service Failure
Call failed: Service Failure

Explanation: The circuit will likely open after a few failures. Subsequent calls will fail until the recovery timeout expires.

Example 2:

circuit = CircuitBreaker(lambda: "Always Success", failure_threshold=0.9, recovery_timeout=1, half_open_attempts=2)
for _ in range(10):
    result = circuit.call()
    print(result)

Output:

Always Success
Always Success
Always Success
Always Success
Always Success
Always Success
Always Success
Always Success
Always Success
Always Success

Explanation: The circuit remains closed because the service never fails.

Example 3: (Edge Case)

def failing_service():
    raise Exception("Always Fails")

circuit = CircuitBreaker(failing_service, failure_threshold=0.1, recovery_timeout=1, half_open_attempts=1)
for _ in range(3):
    try:
        result = circuit.call()
        print(result)
    except Exception as e:
        print(f"Call failed: {e}")

Output:

Call failed: Always Fails
Call failed: Always Fails
Call failed: Always Fails

Explanation: The circuit quickly opens and remains open, as the service always fails.

Constraints

  • failure_threshold must be between 0.0 and 1.0 (inclusive).
  • recovery_timeout must be a positive integer (seconds).
  • half_open_attempts must be a positive integer.
  • The sliding window should have a reasonable size (e.g., last 10 calls).
  • The implementation should be reasonably efficient (avoid unnecessary computations).

Notes

  • Consider using a deque for the sliding window to efficiently add and remove elements.
  • You can use the time module to implement the timeout functionality.
  • Focus on the core circuit breaker logic. Thread safety is not a primary requirement for this challenge, but consider it if you have time.
  • Think about how to make the CircuitBreaker class configurable and reusable.
  • Error handling is important. Ensure your code gracefully handles exceptions and invalid input.
Loading editor...
python