Hone logo
Hone
Problems

Implement the Circuit Breaker Pattern in Python

The circuit breaker pattern is a design pattern used to prevent a service from repeatedly attempting to execute an operation that is likely to fail. This protects the service from cascading failures and allows the failing service time to recover. Your challenge is to implement a robust circuit breaker in Python.

Problem Description

You need to create a Python class, CircuitBreaker, that acts as a wrapper for a potentially unreliable function or method. The circuit breaker should monitor the success and failure rate of calls to the wrapped function and decide whether to allow further calls or to "trip" the circuit, preventing further calls for a period.

Key Requirements:

  • States: The circuit breaker should have three states:
    • CLOSED: Operations are allowed. The circuit breaker monitors failures.
    • OPEN: Operations are blocked. The circuit breaker waits for a timeout period.
    • HALF-OPEN: After the timeout in the OPEN state, a limited number of operations are allowed to test if the underlying service has recovered.
  • Failure Threshold: A configurable number of consecutive failures that will cause the circuit to trip from CLOSED to OPEN.
  • Timeout: A configurable duration (in seconds) that the circuit breaker remains in the OPEN state before transitioning to HALF-OPEN.
  • Success Threshold (for HALF-OPEN): A configurable number of consecutive successful calls in the HALF-OPEN state that will cause the circuit to reset to CLOSED.
  • Error Handling: The circuit breaker should gracefully handle exceptions raised by the wrapped function.
  • Reset Mechanism: The circuit breaker should automatically transition states based on the configured thresholds and timeouts.
  • Allowing Calls: The circuit breaker should provide a mechanism to execute the wrapped function, managing the state transitions and exceptions.

Expected Behavior:

  1. When in CLOSED state:
    • Calls to the wrapped function are executed.
    • If the wrapped function raises an exception, increment the failure count.
    • If the wrapped function succeeds, reset the failure count.
    • If the failure count reaches the failure_threshold, transition to the OPEN state and start the timeout timer.
  2. When in OPEN state:
    • Calls to the wrapped function are immediately blocked and raise a CircuitBreakerOpenError.
    • After the timeout duration has passed, transition to the HALF-OPEN state.
  3. When in HALF-OPEN state:
    • Allow a limited number of calls (implicitly one at a time, but can be configured).
    • If a call succeeds, reset the failure count and transition to CLOSED.
    • If a call fails, transition back to OPEN and reset the timeout timer.

Edge Cases:

  • Initial state should be CLOSED.
  • Handling concurrent calls to the circuit breaker (though a full thread-safe implementation might be an advanced extension, focus on the core logic first).
  • What happens if the wrapped function returns None or a falsy value? For this challenge, consider any return value as a success unless an exception is explicitly raised.

Examples

Let's define a hypothetical unreliable service function.

import time
import random

class UnreliableService:
    def __init__(self, failure_rate=0.5):
        self.failure_rate = failure_rate
        self.call_count = 0

    def perform_operation(self, data):
        self.call_count += 1
        print(f"Attempting operation with data: {data} (Call #{self.call_count})")
        if random.random() < self.failure_rate:
            print("Operation FAILED!")
            raise ConnectionError("Simulated network error")
        else:
            print("Operation SUCCESSFUL!")
            return f"Processed: {data}"

Example 1: Normal Operation and Trip

# Setup: Circuit breaker with failure threshold of 3, timeout of 5 seconds
# Unreliable service that fails 70% of the time
service = UnreliableService(failure_rate=0.7)
cb = CircuitBreaker(failure_threshold=3, timeout=5)

# Simulate calls until circuit trips
print("--- Simulating calls to trip the circuit ---")
for i in range(5):
    try:
        result = cb.execute(service.perform_operation, f"data_{i}")
        print(f"Success: {result}")
    except CircuitBreakerOpenError:
        print("Circuit is OPEN, call blocked.")
    except ConnectionError as e:
        print(f"Caught expected service error: {e}")
    time.sleep(0.5) # Small delay between calls

print("\n--- Current state after tripping ---")
print(f"Circuit breaker state: {cb.state}") # Should be OPEN

Expected Output Snippet (will vary due to randomness):

--- Simulating calls to trip the circuit ---
Attempting operation with data: data_0 (Call #1)
Operation FAILED!
Caught expected service error: Simulated network error
Attempting operation with data: data_1 (Call #2)
Operation FAILED!
Caught expected service error: Simulated network error
Attempting operation with data: data_2 (Call #3)
Operation FAILED!
Caught expected service error: Simulated network error
Circuit is OPEN, call blocked.
Circuit is OPEN, call blocked.

--- Current state after tripping ---
Circuit breaker state: OPEN

Explanation: The first few calls to service.perform_operation fail due to the high failure_rate. The CircuitBreaker counts these failures. Once the failure_threshold (3) is reached, the circuit trips to the OPEN state. Subsequent calls are immediately blocked and raise CircuitBreakerOpenError.

Example 2: Recovery in HALF-OPEN State

# Assume previous example has run and circuit is OPEN for at least 5 seconds
print("\n--- Simulating recovery in HALF-OPEN ---")
time.sleep(5.1) # Wait for timeout to pass

# Now the circuit should be in HALF-OPEN state.
# Let's make a few calls, assuming the service has now recovered (lower failure rate)
service_recovered = UnreliableService(failure_rate=0.1) # Lower failure rate for recovery test

print(f"Circuit breaker state before recovery test: {cb.state}") # Should be HALF-OPEN

# First call in HALF-OPEN
try:
    result = cb.execute(service_recovered.perform_operation, "recovery_data_1")
    print(f"Success in HALF-OPEN: {result}")
except CircuitBreakerOpenError:
    print("Circuit is OPEN, call blocked (unexpected).")
except ConnectionError as e:
    print(f"Caught expected service error in HALF-OPEN: {e}")

print(f"Circuit breaker state after first HALF-OPEN call: {cb.state}") # Should be CLOSED if successful

# Subsequent calls
for i in range(2):
    try:
        result = cb.execute(service_recovered.perform_operation, f"data_{i}")
        print(f"Success: {result}")
    except CircuitBreakerOpenError:
        print("Circuit is OPEN, call blocked.")
    except ConnectionError as e:
        print(f"Caught expected service error: {e}")
    time.sleep(0.2)

print("\n--- Final state ---")
print(f"Circuit breaker state: {cb.state}") # Should be CLOSED

Expected Output Snippet (will vary):

--- Simulating recovery in HALF-OPEN ---
Circuit breaker state before recovery test: HALF-OPEN
Attempting operation with data: recovery_data_1 (Call #4)
Operation SUCCESSFUL!
Success in HALF-OPEN: Processed: recovery_data_1
Circuit breaker state after first HALF-OPEN call: CLOSED
Attempting operation with data: data_0 (Call #5)
Operation SUCCESSFUL!
Success: Processed: data_0
Attempting operation with data: data_1 (Call #6)
Operation SUCCESSFUL!
Success: Processed: data_1

--- Final state ---
Circuit breaker state: CLOSED

Explanation: After waiting for the timeout, the circuit enters the HALF-OPEN state. The first call is allowed. If it succeeds (as simulated with service_recovered), the circuit breaker transitions back to CLOSED, indicating the service has likely recovered. Subsequent calls are then allowed to proceed normally.

Constraints

  • failure_threshold: An integer greater than or equal to 1.
  • timeout: A float or integer representing seconds, greater than 0.
  • The execute method should accept a callable and its arguments.
  • The CircuitBreaker class must be instantiable.
  • The CircuitBreakerOpenError must be a custom exception class.
  • The state transitions should be logically sound.

Notes

  • You will need to implement a custom exception, CircuitBreakerOpenError.
  • Consider how you will track time for the timeout. The time module in Python will be useful.
  • For the HALF-OPEN state, the requirement is to allow a limited number of operations. The simplest approach is to allow one successful operation to reset the circuit to CLOSED. You can extend this to a configurable number of successful calls if you wish, but start with the basic reset on the first success.
  • While this challenge focuses on the core pattern, real-world implementations often need to consider thread safety. For this challenge, assume a single-threaded environment or that concurrency is handled externally.
  • The state of the circuit breaker should be accessible for monitoring (e.g., a .state attribute).
Loading editor...
python