baldur.core.exceptions — Exception Hierarchy
Top-level re-export selection rule: a name is exported by
baldur/__init__.py iff it is (a) a domain base class, or (b) a
leaf class raised by code reachable from a top-level public surface.
Everything else stays in baldur.core.exceptions and is reachable via
the nested import path.
When-to-catch (13 top-level exceptions)
| Exception | When raised | Recovery hint |
|---|---|---|
BaldurError |
Catch-all base | Last-resort logging + alert |
AdapterError |
Adapter base (Redis/SQL misuse) | Reconnect; fall back to in-memory |
AdapterNotFoundError |
a ProviderRegistry slot's get(...) finds no provider by that name |
Wire the missing adapter |
CircuitBreakerError |
CB-base — domain-specific subclasses | Do NOT retry — non-retryable |
DLQError |
DLQ-base | Inspect DLQ entry; manual replay if needed |
DLQReplayError |
retrying a single DLQ entry (retry_entry) could not complete |
Mark for manual review |
ResilienceError |
Resilience-pattern base | Domain-specific fallback |
RetryExhaustedError |
All retry attempts failed | Send to DLQ; alert on SLA breach |
TimeoutPolicyError |
protect(timeout=...) exceeded |
Cancel downstream; degrade |
RateLimitExceeded |
@rate_limit rejected the call |
Surface 429; honor reset_at |
IdempotencyDuplicateError |
@idempotent or idempotency_key= detected a duplicate |
Treat as "already happened" — no result is returned (decision: SKIP completed, ABORT still running); do NOT retry |
DomainValidationError |
Domain-tag validation rejected the call | Caller bug — fix the domain identifier |
ConfigurationError |
Settings or wiring misconfiguration | Crash fast — operator must fix env |
Nested-only classes
exceptions
Baldur library exception hierarchy.
All library exceptions inherit from BaldurError.
Callers can use except BaldurError to catch any library error.
Top-level re-export selection rule
A name in core/exceptions.__all__ is re-exported by baldur/__init__.py
iff it is either (a) a domain base class, or (b) a leaf class raised by code
reachable from a top-level public surface (protect, decorators, ...).
Re-exported at baldur top-level (15 names):
Bases — BaldurError, AdapterError, CircuitBreakerError,
DLQError, ResilienceError, ConfigurationError
Leaves — AdapterNotFoundError, RetryExhaustedError,
TimeoutPolicyError, RateLimitExceeded,
IdempotencyDuplicateError, IdempotencyUnavailableError,
DomainValidationError, DLQReplayError,
LLMUnavailableError
Internal / nested-only (baldur.core.exceptions):
AdapterInitializationError, AdapterConnectionError,
RecoveryAdapterError, StoreError, UnconfiguredStoreError,
CircuitBreakerTransitionError, InvalidStateTransitionError,
DLQEntryNotFoundError, AuditError,
DistributedHashChainUnavailableError,
HashChainSequenceRefusedError, RunbookError,
SettingsValidationError, StepExecutionError, StepTimeoutError,
CompensationError, ConcurrencyConflictError,
RateLimitDeferredError (re-exported by
baldur.services.rate_limit_coordinator).
BaldurError
BaldurError(message: str = '', *, code: str = '')
Bases: Exception
Base exception for all baldur library errors.
extra_context
extra_context() -> dict[str, Any]
Return structlog-bindable context. Override in subclasses.
AdapterError
AdapterError(message: str = '', *, code: str = '')
AdapterNotFoundError
AdapterNotFoundError(
message: str = "",
*,
adapter_type: str = "",
adapter_name: str = "",
code: str = ""
)
AdapterInitializationError
AdapterInitializationError(
message: str = "", *, code: str = ""
)
AdapterConnectionError
AdapterConnectionError(
message: str = "", *, code: str = ""
)
RecoveryAdapterError
RecoveryAdapterError(
message: str = "",
*,
service_name: str = "",
replicas: int | None = None,
namespace: str = "",
code: str = ""
)
Bases: AdapterError
Raised when the recovery adapter encounters an error.
Used for input validation and workload detection failures.
StoreError
StoreError(message: str = '', *, code: str = '')
Bases: AdapterError
Base exception for domain state store errors.
Used by domain-specific stores (ConfigHistoryStore, CanaryRolloutStore, ChaosExperimentStore, CrossClusterStore) for data corruption or store-level logic errors.
Infrastructure failures (Redis down) are handled transparently by ResilientStorageBackend's silent fallback — this exception covers cases where the store itself encounters an unrecoverable problem (e.g. unparseable data, schema mismatch).
UnconfiguredStoreError
UnconfiguredStoreError(
message: str = "",
*,
service: str = "",
operation: str = ""
)
Bases: AdapterError
Raised when a store declines to reach for a Redis nobody configured.
Not a failure report — no connection was attempted. A store path whose only fallback lives in its caller raises this instead of returning a value that would read as a verdict from the store, so the caller takes its local path knowing the store never answered.
Distinct from AdapterConnectionError, which reports a dial that
failed. This one says the dial was declined: the address came from the
shipped default through every channel an operator could have used, and
this process has never reached a Redis there. A store someone named
keeps its ordinary loud failure.
CircuitBreakerError
CircuitBreakerError(message: str = '', *, code: str = '')
CircuitBreakerTransitionError
CircuitBreakerTransitionError(
message: str = "", *, code: str = ""
)
InvalidStateTransitionError
InvalidStateTransitionError(
message: str = "",
*,
current: str = "",
target: str = "",
entity_id: str = "",
code: str = ""
)
Bases: BaldurError
Raised when an invalid state transition is attempted.
Follows the same pattern as CircuitBreakerTransitionError but is domain-agnostic (Recovery Session, etc.).
DLQError
DLQError(message: str = '', *, code: str = '')
DLQEntryNotFoundError
DLQEntryNotFoundError(message: str = '', *, code: str = '')
DLQStateConflictError
DLQStateConflictError(message: str = '', *, code: str = '')
Bases: DLQError
Raised when a DLQ operation violates an entry's state precondition.
Covers resolved/archived/at-cap/not-in-replayable-state conflicts (e.g. a double-click force-redrive, or a retry of an already-resolved entry). Maps to HTTP 409 Conflict at the handler layer, distinct from a not-found (404) or an unexpected replay-execution failure (500).
DLQReplayError
DLQReplayError(message: str = '', *, code: str = '')
ResilienceError
ResilienceError(message: str = '', *, code: str = '')
RetryExhaustedError
RetryExhaustedError(message: str = '', *, code: str = '')
TimeoutPolicyError
TimeoutPolicyError(
timeout_seconds: float, message: str = ""
)
RateLimitExceeded
RateLimitExceeded(
message: str = "",
*,
key: str = "",
limit: int = 0,
window_seconds: int = 0,
reset_at: int = 0
)
Bases: ResilienceError
Raised by @rate_limit when a call is rejected by the limiter.
Function-level rejection signal — distinct from
RateLimitStorageError (storage-backend failure).
RateLimitDeferredError
RateLimitDeferredError(
message: str = "",
*,
key: str = "",
not_before: float | None = None
)
Bases: ResilienceError
Raised when an outbound 429 cooldown outlasts the caller's wait budget.
Outbound cooldown deferral signal — distinct from RateLimitExceeded
(inbound limiter rejection) and RateLimitStorageError (storage-backend
failure).
The call was never attempted: retrying it at or after not_before is
safe, which makes this signal suitable for a DLQ/scheduler requeue. It is
in the default non-retryable set for the same reason — a retry loop that
re-called before not_before would only be refused again.
LLMUnavailableError
LLMUnavailableError(
message: str = "",
*,
attempts: tuple[tuple[str, str], ...] = ()
)
Bases: ResilienceError
Raised by a baldur.llm.wrap client when no endpoint answered the call.
Every endpoint was tried in order — each with its own waits, retries and
breaker — and each was rate-limited, overloaded, out of quota, refused the
key, failed, or stood behind an open breaker. The last endpoint's error is
chained as __cause__.
It is the one error a job sees for "the provider could not answer right
now": under @protected(..., dlq=True) it is parked under the job's name,
and with replay=True the job is re-run when its breaker closes. It is
not retried by an enclosing retry stage: every endpoint already ran its own
retries, so running them all again would only multiply calls to providers
that are refusing them.
Attributes:
| Name | Type | Description |
|---|---|---|
attempts |
|
IdempotencyDuplicateError
IdempotencyDuplicateError(
message: str = "",
*,
key: str = "",
domain: str = "",
decision: str = ""
)
Bases: BaldurError
Raised by @idempotent on a detected duplicate or in-flight collision.
Inherits BaldurError directly (correctness contract, not a
resilience stage). Non-retryable by default — outer @dlq_protect
layers should treat this as a terminal signal.
IdempotencyUnavailableError
IdempotencyUnavailableError(
message: str = "", *, key: str = "", error: str = ""
)
Bases: BaldurError
Raised when an idempotency check cannot complete due to a cache I/O failure (e.g. Redis unreachable) on an enabled, explicitly-requested gate.
Distinct from IdempotencyDuplicateError (a successful dedup verdict):
this signals the verdict is unknown, so the caller can assume neither
"safe to skip" nor "safe to run". Fail-closed by default — opt into
fail-open via BALDUR_IDEMPOTENCY_FAIL_OPEN_ON_CACHE_ERROR or the per-call
idempotency_fail_open=True. Wraps the original cache exception (raised
from it) so a backend-specific error never leaks across the boundary.
DomainValidationError
DomainValidationError(
message: str = "",
*,
original_domain: str = "",
reason: Any = None
)
Bases: BaldurError
Raised when a domain input string fails validation.
Carries the original (pre-normalization) input and a typed reject reason for downstream logging / metric labelling.
Modeled on RecoveryAdapterError: raised at validation sites that have
a loud failure mode (decoration-time, where a CI/dev surface can recover
via test or rename). Runtime APIs catch this and fall back to
FALLBACK_DOMAIN instead of propagating.
AuditError
AuditError(message: str = '', *, code: str = '')
DistributedHashChainUnavailableError
DistributedHashChainUnavailableError(
message: str = "", *, redis_url: str = ""
)
Bases: AuditError
Raised when a stated distributed audit hash chain cannot be built.
The operator asked for the cross-host chain and no Redis client could be constructed at all. Falling back to the local file-locked chain here would write entries indistinguishable from a chain nobody asked to be distributed — a cross-host guarantee silently served by a mechanism that cannot span hosts.
Only the unlabelled substitution refuses. A client that builds but whose
server is unreachable keeps writing, because every entry its fallback
produces is stamped degraded and an auditor can tell those apart.
HashChainSequenceRefusedError
HashChainSequenceRefusedError(
message: str = "",
*,
manager: str = "",
log_dir: str = "",
filename_pattern: str = "",
error: str = ""
)
Bases: AuditError
Raised when the chain cannot read the ledger it is about to append to.
The sequence a chain mints is only safe against re-use if the entries
already on disk can be seen. When the ledger cannot be read at all — the
directory is unreadable, a file read fails, a single row is larger than
the tail window, or the walk budget ran out — minting anyway would place a
number the ledger may already hold, and returning "fresh ledger" would
place 1 inside a live one.
Raised before any mutation of the sequence source, so a refusal consumes no number and leaves nothing to abort. The caller's write fails the same way a failed file open already does: logged, re-raised, and held by the recovery-replay sync worker rather than silently dropped.
RunbookError
RunbookError(message: str = '', *, code: str = '')
ConfigurationError
ConfigurationError(message: str = '', *, code: str = '')
SettingsValidationError
SettingsValidationError(
message: str = "", *, code: str = ""
)
StepExecutionError
StepExecutionError(message: str = '', *, code: str = '')
Bases: BaldurError
Base exception for step execution engine errors.
Shared by the step-based execution engines. Domain-specific subclasses live in each service module.
StepTimeoutError
StepTimeoutError(
step_type: str | float = "",
timeout_seconds: float | int = 0,
message: str = "",
)
Bases: StepExecutionError
Step execution timed out.
Raised by TimeoutExecutor when a handler does not complete within timeout_seconds.
Supports two call conventions
StepTimeoutError(step_type="X", timeout_seconds=30) # keyword StepTimeoutError("X", 30) # positional (legacy compat) StepTimeoutError(timeout_seconds=30) # timeout only (TimeoutExecutor)
CompensationError
CompensationError(message: str = '', *, code: str = '')
Bases: StepExecutionError
Raised when step compensation fails.
Domain-specific subclasses add service-specific extra_context.
ConcurrencyConflictError
ConcurrencyConflictError(
message: str = "",
*,
entity_id: str = "",
expected_version: int = 0,
actual_version: int = 0
)
Bases: BaldurError
Raised on optimistic concurrency control (OCC) conflicts.
Covers version CAS failures across the step-execution engines.
ConfigVersionConflictError
ConfigVersionConflictError(
config_type: str = "",
*,
expected_version: int = 0,
actual_version: int = 0,
message: str = ""
)
Bases: ConcurrencyConflictError
Raised when a runtime-config write conflicts with a concurrent version.
An optimistic-lock consumer for runtime-config writes, it carries the
config section (config_type) plus the inherited
expected_version / actual_version so the REST layer can map it to
HTTP 409 and hand the client a usable retry token. Lives in OSS core so the
OSS config REST handler can except it across the PRO-manager →
OSS-handler boundary.
SystemControlStoreError
SystemControlStoreError(
message: str = "",
*,
change: str = "",
persisted: bool | None = False,
applies: str = "none",
withdrew_held_change: bool = False,
may_still_land: bool = False
)
Bases: BaldurError
Raised when an operator control change did not reach the shared store.
Covers the kill switch, dry-run and emergency-level changes. persisted
is False when the store does not hold the change and never will, and
None when the write may or may not have landed (a later read of the
store decides it). applies says where the change is in force:
"none" (nowhere) or "this_process" (held locally). Lives in OSS
core so the OSS REST handlers can map it to HTTP 503 across the
PRO-manager boundary.
non_retryable_exceptions
non_retryable_exceptions() -> tuple[type[Exception], ...]
Exceptions that must never be retried.
CircuitBreakerError: CB OPEN means 'stop sending traffic'. Retrying defeats circuit breaker semantics. Industry standard (Hystrix, Resilience4j, Polly).
RateLimitDeferredError: the call was never attempted because a shared
429 cooldown outlasts the caller's wait budget. A loop that retries it
can only sleep through attempts that cannot succeed before
not_before — the refusal is a scheduling signal, not a transient
failure.
LLMUnavailableError: a wrapped LLM client already tried every endpoint, each with its own retries. Retrying the whole call would multiply those attempts against providers that are refusing them.