Event Handlers
Use event handlers to monitor, log, and react to retry events.
Overview
Backoff decorators accept three types of event handlers:
- on_success - Called when function succeeds
- on_backoff - Called before each retry wait
- on_giveup - Called when all retries are exhausted
Handler Signature
All handlers must accept a single dict argument containing event details:
def my_handler(details):
print(f"Event details: {details}")
Available Details
The details dict contains:
| Key | Type | Description | Available In |
|---|---|---|---|
target |
function | Function being called | All handlers |
args |
tuple | Positional arguments | All handlers |
kwargs |
dict | Keyword arguments | All handlers |
tries |
int | Number of attempts so far | All handlers |
elapsed |
float | Total elapsed time (seconds) | All handlers |
wait |
float | Seconds to wait before retry | on_backoff |
value |
any | Return value that triggered retry | on_predicate + on_backoff/giveup |
exception |
Exception | Exception that was raised | on_exception + on_backoff/giveup |
on_success Handler
Called when the function completes successfully.
def log_success(details):
print(f"{details['target'].__name__} succeeded after {details['tries']} tries")
@backoff.on_exception(
backoff.expo,
Exception,
on_success=log_success
)
def my_function():
pass
on_backoff Handler
Called before each retry wait period.
def log_backoff(details):
print(
f"Backing off {details['wait']:.1f}s after {details['tries']} tries "
f"(elapsed: {details['elapsed']:.1f}s)"
)
@backoff.on_exception(
backoff.expo,
Exception,
on_backoff=log_backoff
)
def my_function():
pass
Accessing Exception Info
For on_exception, the exception is available:
def log_exception_backoff(details):
exc = details.get('exception')
print(f"Retrying due to: {type(exc).__name__}: {exc}")
@backoff.on_exception(
backoff.expo,
requests.exceptions.RequestException,
on_backoff=log_exception_backoff
)
def api_call():
pass
Accessing Return Value
For on_predicate, the return value is available:
def log_value_backoff(details):
value = details.get('value')
print(f"Retrying because value was: {value}")
@backoff.on_predicate(
backoff.constant,
lambda x: x is None,
on_backoff=log_value_backoff,
interval=2
)
def poll_resource():
pass
on_giveup Handler
Called when retries are exhausted.
def log_giveup(details):
print(
f"Giving up on {details['target'].__name__} "
f"after {details['tries']} tries and {details['elapsed']:.1f}s"
)
@backoff.on_exception(
backoff.expo,
Exception,
on_giveup=log_giveup,
max_tries=5
)
def my_function():
pass
Multiple Handlers
You can provide multiple handlers as a list:
def log_to_console(details):
print(f"Retry #{details['tries']}")
def log_to_file(details):
with open('retries.log', 'a') as f:
f.write(f"Retry #{details['tries']}\\n")
def send_metric(details):
metrics.increment('retry_count')
@backoff.on_exception(
backoff.expo,
Exception,
on_backoff=[log_to_console, log_to_file, send_metric]
)
def my_function():
pass
Common Patterns
Structured Logging
import logging
import json
logger = logging.getLogger(__name__)
def structured_log_backoff(details):
logger.warning(json.dumps({
'event': 'retry',
'function': details['target'].__name__,
'tries': details['tries'],
'wait': details['wait'],
'elapsed': details['elapsed']
}))
@backoff.on_exception(
backoff.expo,
Exception,
on_backoff=structured_log_backoff
)
def my_function():
pass
Metrics Collection
from prometheus_client import Counter, Histogram
retry_counter = Counter('backoff_retries_total', 'Total retries', ['function'])
retry_duration = Histogram('backoff_retry_duration_seconds', 'Retry duration')
def record_metrics(details):
retry_counter.labels(function=details['target'].__name__).inc()
retry_duration.observe(details['elapsed'])
@backoff.on_exception(
backoff.expo,
Exception,
on_backoff=record_metrics
)
def monitored_function():
pass
Error Tracking
import sentry_sdk
def report_to_sentry(details):
if details['tries'] > 3: # Only report after 3 failures
sentry_sdk.capture_message(
f"Multiple retries for {details['target'].__name__}",
level='warning',
extra=details
)
@backoff.on_exception(
backoff.expo,
Exception,
on_backoff=report_to_sentry
)
def my_function():
pass
Alerting
def alert_on_giveup(details):
if details['tries'] >= 5:
send_alert(
f"Function {details['target'].__name__} failed "
f"after {details['tries']} attempts"
)
@backoff.on_exception(
backoff.expo,
Exception,
on_giveup=alert_on_giveup,
max_tries=5
)
def critical_function():
pass
Async Event Handlers
Event handlers can be async when used with async functions:
import aiohttp
async def async_log_backoff(details):
async with aiohttp.ClientSession() as session:
await session.post(
'http://log-service/events',
json=details
)
@backoff.on_exception(
backoff.expo,
Exception,
on_backoff=async_log_backoff
)
async def async_function():
pass
Exception Access
In on_exception handlers, you can access exception info:
import sys
import traceback
def detailed_exception_log(details):
exc_type, exc_value, exc_tb = sys.exc_info()
tb_str = ''.join(traceback.format_tb(exc_tb))
logger.error(
f"Retry {details['tries']} due to {exc_type.__name__}: {exc_value}\\n"
f"Traceback:\\n{tb_str}"
)
@backoff.on_exception(
backoff.expo,
Exception,
on_backoff=detailed_exception_log
)
def my_function():
pass
Conditional Handlers
Execute handler logic conditionally:
def conditional_alert(details):
# Only alert after many retries
if details['tries'] >= 5:
send_alert(f"High retry count: {details['tries']}")
# Only log errors, not warnings
if details.get('exception'):
if isinstance(details['exception'], CriticalError):
logger.error("Critical error during retry")
@backoff.on_exception(
backoff.expo,
Exception,
on_backoff=conditional_alert
)
def my_function():
pass
Complete Example
import logging
from datetime import datetime
logger = logging.getLogger(__name__)
def log_attempt(details):
logger.info(
f"[{datetime.now()}] Attempt {details['tries']} "
f"for {details['target'].__name__}"
)
def log_backoff(details):
logger.warning(
f"Backing off {details['wait']:.1f}s after {details['tries']} tries. "
f"Total elapsed: {details['elapsed']:.1f}s. "
f"Error: {details.get('exception', 'N/A')}"
)
def log_giveup(details):
logger.error(
f"Gave up on {details['target'].__name__} after "
f"{details['tries']} tries and {details['elapsed']:.1f}s. "
f"Final error: {details.get('exception', 'N/A')}"
)
def log_success(details):
logger.info(
f"Success for {details['target'].__name__} after "
f"{details['tries']} tries in {details['elapsed']:.1f}s"
)
@backoff.on_exception(
backoff.expo,
requests.exceptions.RequestException,
max_tries=5,
max_time=60,
on_backoff=[log_attempt, log_backoff],
on_giveup=log_giveup,
on_success=log_success
)
def comprehensive_retry():
return requests.get("https://api.example.com/data")