Skip to content

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 backoff_strategy, or an

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.