Overview
Network operations are inherently unreliable in distributed systems. CDN endpoints can experience temporary outages, rate limiting, connection timeouts, DNS resolution failures, or intermittent network partitions. Without reliability mechanisms, these transient failures can cascade through an application. The result is poor user experience, wasted resources, and system instability. The retry module provides a multi-layered defense strategy that addresses these challenges:Core Reliability Mechanisms
- Circuit Breaker Pattern: Temporarily blocks requests to a failing service after repeated failures. The pause gives the service time to recover. This is the first line of defense against cascading failures and resource exhaustion.
- Exponential Backoff: Progressively increases the wait time between retries with an exponential function (2^attempt by default). The increasing waits protect a struggling service during recovery from overload conditions.
- Jitter: Adds controlled randomization to retry timing to prevent thundering herd problems when multiple clients retry simultaneously. Without jitter, all clients would retry at exactly the same intervals, potentially overwhelming the service again.
-
Thread Safety: All operations are thread-safe with atomic state transitions using reentrant locks (
threading.RLock). Multiple threads can safely interact with the circuit breaker concurrently without race conditions or data corruption. -
Monotonic Time: Uses
time.monotonic()for all timeout calculations. This makes the system immune to system clock adjustments, NTP synchronization, and daylight saving time changes. - Configurable Thresholds: Fine-tune behavior for a specific use case, network conditions, and service SLAs. Use global configuration or per-function parameters.
How These Patterns Work Together
These patterns work together to provide defense in depth:- The retry decorator handles individual request failures with exponential backoff and jitter, giving transient errors a chance to resolve
- The circuit breaker provides system-wide protection against cascading failures by detecting persistent problems and failing fast
- Thread safety ensures correct behavior in concurrent applications where multiple threads are making requests simultaneously
- Monotonic time guarantees reliable timeout behavior regardless of system clock changes
When to Use This Module
Use the retry module in these cases:- Fetching data from external APIs or CDN endpoints
- Making network requests that might experience transient failures
- Building applications that need to be resilient to service outages
- Implementing systems that must handle rate limiting gracefully
- Protecting downstream services from being overwhelmed during incidents
- Building concurrent applications where multiple threads make network requests
tif1’s data fetching pipeline. These reliability benefits work out of the box. The patterns also work directly in application code for custom network operations or external API integrations.
Circuit Breaker Pattern
The circuit breaker pattern is a reliability mechanism. It prevents an application from repeatedly attempting operations that are likely to fail. The pattern is named after electrical circuit breakers. Electrical breakers cut power when current exceeds safe levels. This software pattern “opens” (blocks requests) when a service has problems. This gives the service time to recover before the pattern allows requests through again.The Problem: Cascading Failures
Without a circuit breaker, an application faces several critical problems when a downstream service fails: Resource Exhaustion: The application continues to make requests that fail. These requests waste CPU cycles, memory, network bandwidth, and connection pool resources. In a high-traffic system, this can quickly exhaust available resources and bring down the entire application. Slow Failure: Each failed request must wait for the full timeout period before it fails. At hundreds of requests per second, these timeouts accumulate. The result is high latency, and the application appears frozen to users. Overwhelming the Failing Service: Continued requests to an already struggling service prevent it from recovering. The service needs time to clear its queues, restart processes, or scale up capacity. Constant incoming requests make recovery impossible. Cascading Failures: When one service fails and the application keeps sending requests, the failure cascades upstream. The application’s thread pool fills with blocked requests. Its memory fills with queued operations. The application eventually fails too. It can take down other services that depend on it. Poor User Experience: Users experience long waits followed by errors, often repeatedly. The application keeps trying operations that cannot succeed. This creates frustration and erodes trust in the application.The Solution: Circuit Breaker
The circuit breaker solves these problems. It detects failure patterns and fails fast when a service is known to be down: Fast Failure: When the circuit is open, requests fail immediately with a clear error message. The requests do not wait for timeouts. This provides instant feedback and prevents resource waste. Automatic Recovery Testing: The circuit breaker automatically transitions to a half-open state after a timeout period. A single test request then checks if the service has recovered. This eliminates the need for manual intervention. Resource Protection: The circuit breaker blocks requests to a failing service. This prevents wasting CPU, memory, network bandwidth, and connection pool resources on operations that fail. Graceful Degradation: The immediate failure response lets the application use cached data, fallback strategies, or alternative services. The application does not just hang. System Stability: The circuit breaker prevents cascading failures. It keeps one failing service from bringing down the entire application or other dependent services. Observability: The circuit breaker’s state (closed, open, half-open) and failure count provide clear signals about system health. Monitor these signals, set alerts on them, and use them for debugging.Circuit Breaker States
The circuit breaker operates as a finite state machine with three distinct states. Each state serves a specific purpose in the failure detection and recovery cycle. Understanding these states and their transitions is crucial to use and debug circuit breaker behavior.Closed State (Normal Operation)
The Closed state is the default, healthy operating mode where everything is working correctly. Behavior:- All requests are allowed through to the downstream service without any blocking
- The circuit breaker acts as a passive observer, monitoring each request’s outcome
- Successful requests have no impact on the failure counter
- Failed requests increment the failure counter atomically
- Tracks the failure count for each request using thread-safe atomic operations
- The failure counter persists across requests, accumulating failures over time
- The
last_failure_timeproperty records when the most recent failure occurred
- Remains in Closed state as long as the failure count stays below the configured threshold
- Transitions to Open state immediately when the failure count reaches or exceeds the threshold
- The transition is atomic and thread-safe, preventing race conditions in concurrent environments
Open State (Failure Mode)
The Open state is the circuit breaker’s protective mode, activated when the failure threshold is reached. Behavior:- All requests are immediately blocked without attempting to contact the downstream service
- No network calls are made, no timeouts are waited for, and no resources are consumed
- Requests fail instantly with
Exception("Circuit breaker is open") - The circuit breaker starts a timeout timer using monotonic time
- Remains in Open state for the configured timeout period (default: 60 seconds)
- The timeout is calculated using
time.monotonic(), making it immune to system clock changes - The timeout period gives the failing service breathing room to recover
- Remains in Open state until the timeout period elapses
- Automatically transitions to Half-Open state after the timeout expires
- The transition check happens on the next request after timeout expiration
- Cannot transition back to Closed state directly; must go through Half-Open first
- Raises
Exception("Circuit breaker is open")immediately for all requests - This exception is distinct from network errors, so code can handle circuit breaker failures differently
- The immediate failure prevents resource waste and provides fast feedback
Half-Open State (Recovery Testing)
The Half-Open state is the circuit breaker’s recovery mechanism, cautiously testing whether the service has recovered. Behavior:- Allows exactly one test request through to the downstream service
- This single request acts as a “canary” to check service health
- All other concurrent requests are blocked and fail immediately
- The outcome of this test request determines the next state transition
- If the test request succeeds, the circuit breaker transitions to Closed state
- The failure counter is reset to zero atomically
- Normal operation resumes immediately
- All subsequent requests are allowed through
- If the test request fails, the circuit breaker immediately returns to Open state
- The timeout timer is reset, starting a new timeout period
- The failure counter is incremented
- The circuit breaker will try again after another timeout period
- Only one thread’s request is allowed through as the test request
- Other concurrent threads’ requests are blocked and fail immediately
- The state transition (Half-Open → Closed or Half-Open → Open) is atomic
- No race conditions occur even with many concurrent threads
State Transition Diagram
The following diagram illustrates all possible state transitions in the circuit breaker’s finite state machine: Key Transition Rules:- Closed → Closed (Success): When a request succeeds in Closed state, the failure counter resets to 0. The circuit remains closed.
- Closed → Closed (Failure Below Threshold): A request fails, but the count stays below the threshold. The counter increments, and the circuit remains closed.
- Closed → Open (Threshold Reached): A request fails, and the count reaches or exceeds the threshold. The circuit immediately opens and starts the timeout timer.
- Open → Open (Timeout Not Elapsed): In Open state, the timeout has not elapsed. All requests are blocked immediately, and the circuit remains open.
- Open → Half-Open (Timeout Elapsed): When the timeout elapses, the circuit transitions to Half-Open on the next request. The circuit then allows one test request through.
- Half-Open → Closed (Test Success): If the test request succeeds, the circuit closes. It resets the failure counter to 0 and resumes normal operation.
- Half-Open → Open (Test Failure): If the test request fails, the circuit immediately opens again. It resets the timeout timer and increments the failure counter.
-
Any State → Initial (Manual Reset): Calling
reset_circuit_breaker()creates a new circuit breaker instance in Closed state. The new instance has zero failures, regardless of the current state.
- All state transitions are atomic and thread-safe, protected by a reentrant lock
- The timeout is calculated using monotonic time, immune to system clock changes
- The Half-Open → Open transition resets the timeout, so the circuit will try again after another full timeout period
- Multiple concurrent threads can safely interact with the circuit breaker without race conditions
- The failure counter only resets to 0 on successful requests in Closed or Half-Open states, not in Open state
Thread Safety and Atomic Operations
TheCircuitBreaker implementation is fully thread-safe and uses atomic operations for all state transitions and counter updates. This is critical for production applications where multiple threads make requests simultaneously. It ensures correct behavior under high concurrency, without race conditions or data corruption.
Thread Safety Mechanisms
Reentrant Lock (threading.RLock):
- All state transitions and counter operations are protected by a reentrant lock
- Reentrant means the same thread can acquire the lock multiple times without deadlocking
- This allows nested calls within the same thread while still providing mutual exclusion across threads
- The lock is acquired for the minimum time necessary to perform atomic operations
- The failure counter increments are protected by the lock, ensuring no lost updates
- Multiple threads incrementing the counter concurrently will see correct, sequential values
- The counter read and write operations happen atomically within the same lock acquisition
- No race conditions occur between reading the counter and checking the threshold
- Uses
time.monotonic()instead oftime.time()for all timeout calculations - Monotonic time is guaranteed to always move forward, never backward
- Immune to system clock adjustments, NTP synchronization, or daylight saving time changes
- Ensures timeout periods are accurate and predictable regardless of system time changes
- All state transitions happen atomically within the lock
- State checks and updates are performed in the same critical section
- No race conditions between checking the state and updating it
- The state machine maintains consistency even under high concurrency
- The
call()method uses a compare-and-swap pattern for state transitions - Captures the pre-call state before executing the function
- Uses the captured state to determine the correct post-call transition
- Prevents race conditions where the state changes between check and update
Thread Safety Guarantees
The circuit breaker provides these concrete guarantees in concurrent environments:- No Lost Updates: If multiple threads increment the failure counter simultaneously, all increments are recorded. No updates are lost.
- Consistent State Transitions: State transitions are atomic. Partial transitions and inconsistent state combinations never occur.
- Accurate Failure Counting: The failure count always reflects the number of failures. This holds true even with concurrent failures from multiple threads.
- Single Test Request in Half-Open: Only one thread’s request passes as the test request. This applies even if many threads make requests simultaneously.
- No Deadlocks: The reentrant lock prevents deadlocks from nested calls within the same thread.
- Memory Visibility: All state changes are immediately visible to all threads due to the lock’s memory barrier semantics.
Concurrency Example
Here’s an example demonstrating thread-safe behavior with multiple concurrent threads:- 20 threads make concurrent requests through the circuit breaker
- The failure counter is incremented atomically for each failure
- State transitions happen correctly even with concurrent failures
- No race conditions occur, and the final state is consistent
- If the threshold is reached, the circuit opens and subsequent requests fail immediately
Performance Considerations
Lock Contention:- The lock is held for minimal time, only during state checks and updates
- The actual function execution happens outside the lock to avoid holding it during I/O
- This minimizes lock contention and allows high concurrency
- The
stateandfailuresproperties use the lock for reads to ensure memory visibility - This adds minimal overhead compared to lock-free reads but guarantees correctness
- In practice, the overhead is negligible compared to network I/O times
- The circuit breaker scales well to hundreds of concurrent threads
- Lock contention is minimal because critical sections are very short
- The monotonic time check is fast and does not require system calls
- The circuit breaker maintains minimal state: failure count, timestamps, and state
- Memory overhead is constant regardless of the number of threads
- No per-thread state is maintained, keeping memory usage low
CircuitBreaker
Thread-safe circuit breaker implementation with atomic state transitions and monotonic time-based timeout tracking. This class provides the core circuit breaker functionality. Instantiate it directly for custom use cases. Alternatively, access the global instance through get_circuit_breaker().
int
default:5
Number of consecutive failures required before the circuit breaker opens. Must be >= 1.Choosing the right threshold:
- Lower values (1-3): More sensitive, opens quickly. Use for critical services where fast failure detection is important. Also use when good fallback strategies exist.
- Medium values (4-7): Balanced approach. Good default for most use cases. Tolerates brief transient errors while still detecting persistent problems.
- Higher values (8-15): More tolerant, slower to open. Use for services with expected intermittent failures. Also use to avoid false positives from brief network interruptions.
- Higher thresholds make the circuit breaker more tolerant of transient failures but slower to detect persistent problems
- Lower thresholds provide faster failure detection but may open prematurely on brief network interruptions
- Consider the service’s typical failure patterns and SLAs when choosing a threshold
- Threshold=2 for a critical payment API where failures are rare and fast detection is crucial
- Threshold=5 (default) for general CDN requests with occasional transient failures
- Threshold=10 for a less reliable third-party API where intermittent failures are expected
int
default:60
Number of seconds the circuit breaker remains open before transitioning to half-open state to test recovery. Must be >= 1.Choosing the right timeout:
- Short timeouts (10-30s): Quick recovery testing. Use when services typically recover quickly. Also use to minimize downtime.
- Medium timeouts (60-120s): Balanced approach. Good default for most services. Gives services adequate time to recover without excessive waiting.
- Long timeouts (180-300s): Patient recovery. Use for services that need significant time to recover (for example, database failovers, service restarts).
- Shorter timeouts mean faster recovery when the service comes back online
- Longer timeouts give the failing service more time to recover fully
- A timeout that is too short tests recovery before the service is ready. The circuit then opens again.
- A timeout that is too long causes unnecessary waiting if the service recovers quickly
- Timeout=30 for a CDN that typically recovers from transient issues quickly
- Timeout=60 (default) for general API services with moderate recovery times
- Timeout=180 for a database that might need time for failover and connection pool rebuilding
str
Current circuit breaker state - one of
"closed", "open", or "half_open".This is a read-only property that is thread-safe. The state reflects the current operational mode:"closed": Normal operation, all requests allowed through"open": Failure mode, all requests blocked immediately"half_open": Recovery testing, one test request allowed through
int
Current count of consecutive failures. Resets to 0 on successful request or when circuit closes.This property is thread-safe with atomic operations. The failure count accumulates across requests:
- Increments by 1 for each failed request
- Resets to 0 when a request succeeds in Closed or Half-Open state
- Does NOT reset in Open state (only resets when transitioning to Closed via Half-Open)
- When it reaches the threshold, the circuit opens
datetime | None
Timestamp of the most recent failure using
datetime.now(). Useful for logging, monitoring, and debugging.This property is None if no failures have occurred since the circuit breaker was created or reset. The timestamp uses wall-clock time (not monotonic time) for human readability.Usage:float | None
Monotonic time of last failure used for timeout calculations. Immune to system clock adjustments.This internal property uses
time.monotonic() for accurate timeout calculations that are not affected by:- System clock adjustments (manual or automatic)
- NTP synchronization
- Daylight saving time changes
- Leap seconds
threading.RLock
Reentrant lock protecting all state transitions and counter operations.This internal lock ensures thread safety:
- Allows nested calls from the same thread without deadlocking
- Provides mutual exclusion across different threads
- Protects all reads and writes to state and counters
- Held for minimal time to reduce contention
CircuitBreaker class provides these concrete guarantees in concurrent environments:
- All state transitions are atomic: No partial transitions or inconsistent states
- Failure counter increments are protected by lock: No lost updates from concurrent failures
- Multiple threads can safely call any method concurrently: Full thread safety without race conditions
- No race conditions between state checks and updates: Compare-and-swap pattern ensures consistency
- Reentrant lock allows nested calls from same thread: No deadlocks from recursive calls
get_circuit_breaker
Get the global circuit breaker instance used by tif1 for all internal network operations.
CircuitBreaker
The global
CircuitBreaker instance that tif1 uses internally for all CDN requests and network operations.This is a singleton instance that is shared across the entire application. All tif1 data fetching operations (session loading, lap data, telemetry, etc.) use this same circuit breaker instance. The instance provides unified failure detection and protection across all network operations.get_circuit_breaker() for these purposes:
- Monitor the current state of the circuit breaker
- Check the failure count to understand system health
- Implement custom logic based on circuit breaker state
- Use the same circuit breaker for custom network operations
- Debug network issues by inspecting circuit breaker state
reset_circuit_breaker
Reset the global circuit breaker to closed state with zero failures. This creates a new circuit breaker instance, completely clearing all failure history and state.
reset_circuit_breaker() when:
- The underlying issue is manually fixed, and normal operation must resume immediately
- Tests need a clean circuit breaker state between test cases
- A fix is deployed, and the failure history must be cleared
- The application switches to a different CDN or service endpoint
- The circuit breaker must give the service another chance
- This function creates a completely new
CircuitBreakerinstance, not just resetting the state - All failure history is lost, including the failure count and last failure time
- The new circuit breaker uses the current configuration values from
get_config() - This is a global operation that affects all code using the circuit breaker
- Use with caution in production; it is better to let the circuit breaker recover naturally in most cases
Circuit Breaker Methods
call(func, *args, **kwargs)
Execute a function with circuit breaker protection using atomic state transitions. This is the primary method for using the circuit breaker to protect application operations.
Callable[..., T]
required
The function to execute with circuit breaker protection. This can be any callable (function, lambda, method, etc.) that might fail and needs protection.The function runs outside the circuit breaker’s lock. This prevents lock holding during I/O operations, which could cause deadlocks or performance issues.
Any
Positional arguments to pass to the function. These are forwarded directly to
func when it is called.Any
Keyword arguments to pass to the function. These are forwarded directly to
func when it is called.T
The return value from the executed function. The type matches whatever
func returns.Exception
Raises
Exception("Circuit breaker is open") if the circuit breaker is in Open state and the timeout has not elapsed.Also raises any exception that func raises during execution. These exceptions are propagated after recording the failure in the circuit breaker.- State Check (Atomic): Checks the current circuit breaker state within a lock
- Open State Handling: If open and timeout has not elapsed, raises immediately
- Half-Open Transition: If open and timeout has elapsed, transitions to half-open
- Function Execution: Executes the function outside the lock (prevents deadlocks during I/O)
- Success Handling: On success, resets failure counter and closes circuit if in half-open state
- Failure Handling: On failure, increments failure counter and opens circuit if threshold reached
- All state checks and updates are atomic
- Function execution happens outside the lock
- Multiple threads can safely call this method concurrently
- Only one thread’s request succeeds in half-open state (others are blocked)
record_success()
Manually record a successful operation.
record_failure()
Manually record a failed operation.
check_and_update_state()
Check current state and update if timeout elapsed.
- Tuple of (should_proceed, current_state)
Retry Decorator
retry_with_backoff
Decorator for automatic retry with exponential backoff and jitter.
max_retries: Maximum number of retries (default: 3)backoff_factor: Exponential backoff multiplier (default: 2.0)jitter: Add random jitter to backoff (default: True)exceptions: Tuple of exceptions to catch (default: all exceptions)
Custom exception handling
Configuration
Configure circuit breaker and retry behavior via global config:Complete Examples
Monitor circuit breaker
Custom retry logic
Graceful Degradation
Retry with Progress
Best Practices
- Monitor circuit breaker state: Check before critical operations.
- Reset after fixing issues: Do not wait for the timeout when the service is back.
- Use appropriate thresholds: Higher for transient errors, lower for persistent failures.
- Add jitter to retries: Prevents thundering herd problem.
- Log retries: Helps diagnose network issues.
- Catch specific exceptions: Do not retry on client errors (4xx).
- Implement fallbacks: Use cached data when circuit breaker opens.
- Test circuit breaker behavior: Verify it works as expected.
Troubleshooting
Circuit breaker stuck open
Too many retries
Adjust themax_retries parameter when using the @retry_with_backoff decorator: