baldur.services — Retry
Retry policy and configuration types, the per-attempt action/result records, and the exhaustion exception.
RetryPolicy
RetryPolicy(
config: RetryPolicyConfig,
backoff: BackoffStrategy | None = None,
rate_limit_coordinator: (
RateLimitCoordinator | None
) = None,
retry_budget: AdaptiveRetryBudget | None = None,
sleeper: Callable[[float], None] | None = None,
)
Bases: ResiliencePolicy[T]
Pure retry Policy.
External concerns such as Kill Switch, ErrorBudgetGate, Audit, and DLQ are handled by PolicyComposer's Guard/Hook/Sink.
Idempotency contract
Functions passed to execute() MUST be idempotent. Use IdempotencyGuard + IdempotencyHook via PolicyComposer for framework-level enforcement, or implement idempotency in your handler.
Collaborator:
- retry_budget: state mutates on every in-loop attempt (Guard-unsuitable)
- rate_limit_coordinator: bundles wait / success-signal / cooldown. Unlike
the others this one is optional at construction: when it is not passed,
the loop resolves the shared coordinator at use time so a settings-derived
policy coordinates outbound 429s by default. Passing one always wins, and
two levers turn the default off — rate_limit_aware=False on the config
and BALDUR_RATE_LIMIT_BACKOFF_COORDINATION_ENABLED=false. The default
also requires an identified domain (see the config field docs).
- backoff: reuses core/backoff.py BackoffStrategy ABC
- sleeper: between-attempt wait function. None (default) -> time.sleep;
pass lambda _: None to defer waiting to an external scheduler.
execute
execute(
func: Callable[..., T],
*args: Any,
context: PolicyContext | None = None,
**kwargs: Any
) -> PolicyResult[T]
Pure retry execution.
Kill Switch, ErrorBudgetGate, Audit, and DLQ are handled by PolicyComposer via Guard/Hook/Sink.
RetryPolicyConfig
dataclass
RetryPolicyConfig(
max_attempts: int = STANDARD_RETRY_COUNT,
backoff_base: float = STANDARD_BASE_DELAY,
backoff_max: float = STANDARD_MAX_DELAY,
jitter_percent: float = STANDARD_JITTER_PERCENT,
backoff_multiplier: float = STANDARD_BACKOFF_MULTIPLIER,
backoff_increment: float = STANDARD_LINEAR_INCREMENT,
backoff_strategy: str = FALLBACK_BACKOFF_STRATEGY,
retryable_exceptions: tuple[type[Exception], ...] = (
lambda: (Exception,)
)(),
non_retryable_exceptions: tuple[
type[Exception], ...
] = non_retryable_exceptions(),
domain: str = "default",
enable_dlq: bool = True,
retry_on_result: Callable[[Any], bool] | None = None,
max_elapsed: float | None = None,
rate_limit_aware: bool = True,
rate_limit_key: str | None = None,
config_source: str = "direct",
)
Configuration dedicated to the pure retry Policy. Does not include externally dependent settings.
from_settings
classmethod
from_settings(domain: str = 'default') -> RetryPolicyConfig
Load only the pure retry settings from Settings.
Both resolution branches bottom out in the same operator-facing fields
(BALDUR_RETRY_* for the ladder, BALDUR_BACKOFF_* for its shape),
so a domain resolves identically with and without the PRO runtime store
when that store holds no override.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
domain
|
str
|
Domain name for per-domain overrides |
'default'
|
Returns:
| Type | Description |
|---|---|
RetryPolicyConfig
|
RetryPolicyConfig instance |
build_backoff
build_backoff(*, jitter: bool = True) -> BackoffStrategy
Build the backoff strategy these resolved values describe.
Reads only this dataclass's own fields — never the settings tree. Every
strategy parameter is resolved at from_settings time so the config
alone reproduces the ladder: that is what lets a caller reason about the
effective backoff from a startup report, and what keeps the two
construction sites (sync and async) from drifting apart.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
jitter
|
bool
|
Build the jitterless skeleton when False. Ignored by the decorrelated strategy, whose randomization is its definition. |
True
|
Returns:
| Name | Type | Description |
|---|---|---|
BackoffStrategy |
BackoffStrategy
|
the strategy named by |
BackoffStrategy
|
exponential one when that name cannot be honored (fail-open — a |
|
BackoffStrategy
|
config-shaped side input must never fail a business call). |
RetryResult
dataclass
RetryResult(
success: bool,
action: RetryAction,
attempt: int,
value: Any = None,
error: Exception | None = None,
dlq_id: str | None = None,
next_delay: int | None = None,
)
Result of a retry operation.
should_retry
property
should_retry: bool
Whether another retry should be attempted.
was_retried
property
was_retried: bool
Whether this result came from a retry (not first attempt).
to_policy_result
to_policy_result() -> PolicyResult
Convert to the unified PolicyResult result type.
RetryAction
Bases: str, Enum
Actions that can be taken after a failure.
MaxRetriesExceededError
MaxRetriesExceededError(
message: str,
retry_count: int,
max_retries: int,
last_error: Exception | None = None,
last_result: Any = None,
result_rejected: bool = False,
)
Bases: RetryExhaustedError
Raised when maximum retry attempts have been exhausted.
Carries the terminal cause via two mutually-exclusive slots:
last_error (the final exception, for exception-driven exhaustion) or
last_result + result_rejected (the final rejected value, for
result-predicate exhaustion). is_result_exhaustion is the first-class
discriminator — do not infer it from last_result is not None (the
predicate may legitimately reject None) or last_error is None.
is_result_exhaustion
property
is_result_exhaustion: bool
True when exhaustion was caused by a rejected result, not an exception.