0062. Single declaration sites for shutdown timeout and retry policy¶
- Status: Accepted
- Date: 2026-08-09
Context¶
Two facts in this library were each stored in more than one place, with nothing
that failed when the copies disagreed — the defect shape recorded in
.claude/rules/recurring-defects.md §2.
Shutdown timeout. SubscriptionConfig declared a shutdown_timeout field
with a default and a positivity check, and SubscriptionManager.__init__
declared a shutdown_timeout argument with the same default. Only the
constructor argument reached anything: it is what the manager hands to its
ShutdownCoordinator, and what run_until_shutdown() and stop_all() override
per call. The config field had no reader anywhere in the tree. A user who set it
got a validated value, a truthful attribute read-back, and no behavior change —
the failure mode is silent, and the documentation had already been reduced to
explaining which copy loses.
Retry policy. Two unrelated types were both named RetryPolicy:
eventsource.adapters._bus.retry.RetryPolicy, a frozen dataclass computing
broker redelivery backoff, and eventsource.application.projections.retry.RetryPolicy,
a runtime_checkable Protocol describing projection retry decisions. The
top-level package exports the bus dataclass, so from eventsource import RetryPolicy
resolved to the dataclass — while the annotations users actually encounter, the
keyword-only retry_policy parameter on DatabaseProjection, ReadModelProjection,
DeclarativeProjection, and CheckpointTrackingProjection, all referred to the
Protocol. The two are structurally incompatible: the dataclass has delay_for,
the Protocol wants max_retries, get_backoff, and should_retry. Following the
export produced an argument the type checker rejects and the projection loop
cannot call. Nothing in either type's tests could notice, because each was
imported by its own full path.
Decision¶
A fact has one declaration site.
SubscriptionConfig.shutdown_timeout is deleted, along with its validation rule
and docstring entry. SubscriptionManager.__init__(shutdown_timeout=...) is the
sole declaration; per-call overrides on stop_all() and run_until_shutdown()
remain, because a one-off override at the call site is not a second declaration
of the default. Shutdown is a manager-wide concern and never varied per
subscription, so there is nothing the deleted field could have expressed that
the surviving one cannot.
A name identifies one type.
The projections Protocol is renamed to ProjectionRetryPolicy. The bus dataclass
keeps the bare RetryPolicy name and the top-level export, because that export
is the one users are told about and the one the changelog documents. Renaming the
Protocol rather than removing the export preserves both surfaces and makes the
distinction visible at every annotation, which is where the confusion arose. The
Protocol is not added to the top-level package: eventsource.application.projections
deliberately does not re-export the retry module, and the deep import remains the
documented way to reach it.
Consequences¶
Both changes are breaking, and ship without deprecation shims per the project's standing pre-1.0 policy.
- Constructing
SubscriptionConfig(shutdown_timeout=...)now raisesTypeErrorinstead of silently doing nothing. That is the intended improvement: the failure moves from invisible to immediate. Callers move the value to the manager constructor, where it always belonged. - Code importing
RetryPolicyfromeventsource.application.projections.retrymust importProjectionRetryPolicy. The concrete implementations (ExponentialBackoffRetryPolicy,NoRetryPolicy,FilteredRetryPolicy,DEFAULT_RETRY_POLICY) are unchanged, so structural users of the policy are unaffected — only explicit annotations against the Protocol move. from eventsource import RetryPolicycontinues to resolve to the bus dataclass, and now that is the only thing with the name.- A regression test pins the deletion: it asserts
SubscriptionConfighas noshutdown_timeoutattribute and that passing the keyword raises. Re-adding the second declaration site fails the suite rather than passing quietly.
Related¶
- Amends
0022and the subscription configuration surface described indocs/api/subscriptions.md. - Extends
0048, which established the shared busRetryPolicywhose name this ADR arbitrates.