Reference
Migrating to 1.0
Every user-visible change between RelPrim 0.11 and the 1.0 API, with what changed and what to do. Most applications need no code changes.
Before freezing the public API for 1.0, the 0.11 API was hardened: a number of
inconsistencies from the pre-stable releases were fixed. This page lists every
user-visible change. Most applications need no code changes; the ones most
likely to matter are retry_on validation,
fallback validation and
timeout reporting.
Typing
@resilient(...) preserves signatures
- 0.11: most configurations were inferred as
(*Never, **Never) -> Awaitable[OperationResult[Never]]and rejected by strict type checkers. The package also had nopy.typedmarker, so type checkers treated all of RelPrim asAny. - 1.0: decorated functions keep their parameters and return
Coroutine[Any, Any, OperationResult[R]], orOperationResult[R | RF]with a fallback returningRF. The package shipspy.typed. - What to change: remove casts and
# type: ignorecomments added as workarounds; strict mypy reports the ones that are no longer needed.
Variance
ValidationPolicy and CallableValidator are contravariant, and
OperationResult, FallbackChain, FallbackCandidate and FallbackResult are
covariant. validation_policy(json_schema_validator(...)) is now
ValidationPolicy[object] instead of ValidationPolicy[Any]. Nothing changes at
runtime.
Configuration
retry_on requires retries
- 0.11:
@resilient(retry_on=...)withoutretries, or together withretry=RetryPolicy(...), was silently ignored. - 1.0: both raise
ValueError. - What to change: add
retries=N, or setRetryPolicy(retry_on=...).
Exception filters accept only Exception subclasses
- 0.11:
RetryPolicy.retry_on,FallbackPolicy.fallback_onandCircuitBreaker.record_failure_onacceptedBaseExceptionsubclasses.RetryPolicy.run_asyncwould then retryasyncio.CancelledErrorand turn it intoRetryError. - 1.0: these tuples must contain
Exceptionsubclasses; anything else raisesTypeError. Cancellation is never retried, never falls back and is never recorded by a breaker. - What to change: remove
BaseException,asyncio.CancelledError,KeyboardInterruptorSystemExitfrom these tuples.
Stricter value validation
- Durations (
TimeoutPolicy.seconds,ttl_seconds,max_wait_seconds, backoff delays,recovery_timeout_seconds,busy_timeout_secondsand the matching decorator options) must be finite:nanorinfraiseValueError. In 0.11, ananTTL cached results forever. ExecutionAttempt.duration_secondsandExecutionReport.duration_secondsmust be finite, non-negative numbers; booleans and other types raiseTypeError.- Booleans are rejected for
max_attempts,failure_thresholdandretries. - Event payloads and report metadata must contain JSON scalars.
- Report and event timestamps and the
SQLiteEventStore.delete_before()cutoff must be timezone-aware. Atzinfowhoseutcoffset()returnsNonecounts as naive. 0.11 accepted such a cutoff and interpreted it as local time. - What to change: pass finite numbers and timezone-aware datetimes.
Behavior
Successful validation names a real validator
- 0.11: a successful
ValidationPolicyreportedvalidator_name="validation_policy". - 1.0: it reports the last validator, all of which ran and passed. This
affects
ValidationResult.validator_name, thevalidation_validator_namemetadata and thevalidation.succeededevent. ValidationPolicy.validate_or_raise()now returns the successfulValidationResultinstead ofNone.- What to change: update consumers that match on
"validation_policy".
Fallback candidates are validated individually
- 0.11: validation ran once on the result of the whole chain. An invalid
result from the first candidate failed the operation without trying the next
one, and the failure’s cause was
ValidationFailedError. - 1.0: each candidate’s result is validated. An invalid result is a candidate
failure, so the chain’s
FallbackPolicydecides whether the next candidate runs. When every candidate fails, the cause isFallbackChainError, whosefailuresinclude theValidationFailedErrors. Validation events for candidates includefallback_candidate_name. - What to change: code that checks
isinstance(error.cause, ValidationFailedError)after a fallback failure should inspecterror.cause.failures.
FallbackPolicy also filters the primary failure
- 0.11: inside an operation, the chain ran after any primary failure;
fallback_onwas only applied between candidates. - 1.0: the primary failure must match
fallback_ontoo. With the default(Exception,)nothing changes. - What to change: if you narrowed
fallback_on, include every primary failure that should fall back, such asCircuitBreakerOpenErrororOperationTimeoutError.
retry_count excludes the fallback attempt
- 0.11:
ExecutionReport.retry_countwasattempt_count - 1, so a fallback counted as a retry. - 1.0:
retry_countcounts primary attempts after the first one.attempt_countstill includes the fallback attempt, which is marked with the newExecutionAttempt.fallbackfield (also into_dict()). Theoperation.succeededandoperation.failedevents use the same value. - What to change: nothing, unless dashboards relied on the old count.
TimeoutPolicy only reports its own timeout
- 0.11: a
TimeoutErrorraised by the operation itself, such as a client socket timeout, was converted intoOperationTimeoutError. - 1.0: only expiry of the policy’s deadline raises
OperationTimeoutError; otherTimeoutErrors propagate unchanged and are reported as normal failures. - What to change: to retry client timeouts with a narrowed
retry_on, addTimeoutErrorto it.
Rate-limit metadata
rate_limit_delay_secondsandrate_limit_delay_sourceareNonefor failures that were not rate-limited. 0.11 filled them with the normal backoff delay.- When the selected delay exceeds the maximum wait, they hold the rejected delay.
0.11 reported
None, and therate_limit.wait_exceededevent’sdelay_secondswas alwaysNone. - On the final attempt no delay is selected, so
rate_limit_wait_exceededstaysFalseand norate_limit.wait_exceededevent is emitted.
Idempotency
- The raw key is no longer copied into report metadata (
idempotency_keywas removed) or into theIdempotencyReentryErrormessage.IdempotencyResult.keyandIdempotencyReentryError.keyare unchanged. - If the owning caller is cancelled, joined callers no longer receive
asyncio.CancelledError; one of them executes the operation instead. See cancellation. - What to change: if you need the key in logs, log it yourself.
Circuit breaker cancellation
- 0.11: a cancelled half-open probe kept the probe slot occupied, so the breaker rejected every later call.
- 1.0: a cancelled call releases the probe slot and is neither a success nor a failure.
Identity semantics for stateful objects
CircuitBreaker, InMemoryIdempotencyStore, InMemoryEventSink and
SQLiteEventStore compare by identity and are hashable. In 0.11 they compared by
field values and were unhashable.
CircuitBreakerOpenError.state is a CircuitBreakerState. It is a string enum,
so comparisons with "open" keep working.
Optional integrations
Importing relprim.opentelemetry, relprim.jsonschema or relprim.pydantic
without the dependency raises ModuleNotFoundError with the pip install
command for the matching extra.
Removed
| Removed | Reason | What to change |
|---|---|---|
RetryAttempt |
Never produced by RelPrim | Remove the import. |
ExecutionStatus.CANCELLED, AttemptStatus.CANCELLED |
Never produced: cancellation propagates asyncio.CancelledError and creates no report |
Handle asyncio.CancelledError. |
Report metadata idempotency_key |
May contain sensitive identifiers | Log the key yourself if needed. |
Added
ResilientDecorator, the protocol returned byresilient(...).ExecutionAttempt.fallback.- Python 3.14 support.
