Skip to content

Fix resubscribe race - #571

Open
Inok wants to merge 2 commits into
Eventuous:devfrom
Inok:fix/subscription-resubscribe-race
Open

Fix resubscribe race#571
Inok wants to merge 2 commits into
Eventuous:devfrom
Inok:fix/subscription-resubscribe-race

Conversation

@Inok

@Inok Inok commented Aug 17, 2026

Copy link
Copy Markdown

No description provided.

Inok and others added 2 commits August 16, 2026 04:46
A seconds-long store blip wedged twenty subscriptions for seven hours, and
nothing in the suite could have caught it. Twelve of these fail on every
run and the thirteenth whenever the race lands, so the fix has something to
prove and the next regression has somewhere to land.

The provider suites carry the restart contract itself: a fake transport can
show the framework calls teardown once, but only real infrastructure can
show a given broker survives being restarted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Subscribe returns while the work carries on, so a transport that is naturally a
loop had to spawn one, and that loop's failure could not propagate — it went
sideways through Dropped(), callable from any thread at any time. The state
machine, the gate, the run generation and the drop cycle all existed to collapse
those writers back into one. Moving the loop's lifetime from the drop to the
subscription removes the need for them.

A transport now says only how to connect. Whatever it acquires is registered on
the run as it is acquired and released in reverse when the run stops, so a
failure or an acknowledgement arriving late names the run that produced it
instead of reaching whichever run is current. Pumps belong to transports, which
report their own death. Teardown is one policy: a graceful attempt on
TeardownTimeout, then whatever is left is started and abandoned rather than
skipped; Unsubscribe's token bounds only the caller's wait, never the stopping.

Also fixes: a supervisor dying after connecting reported no drop, leaving health
green; handlers cancelled by teardown were acknowledged, advancing the checkpoint
past everything in flight; RabbitMQ delivered contexts with a default token, so
the stopping guard never matched, and a nack on a closed channel killed a filter
reader for good; $all disposed its subscription twice and released without
honouring the teardown budget; a faulted channel reader skipped the final forced
checkpoint commit; test fixtures abandoned their containers.

Deletes SubscriptionLifecycle, TaskRunner, CheckpointRun, ChannelFullException,
Dropped, Resubscribe, Stopping, Generation, ResetSequence, IsDropped,
MonitorSubscriberTask and DropReason.Stopped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Test Results

 46 files  + 24   46 suites  +24   11m 52s ⏱️ -39s
445 tests + 76  445 ✅ + 76  0 💤 ±0  0 ❌ ±0 
898 runs  +518  898 ✅ +518  0 💤 ±0  0 ❌ ±0 

Results for commit 6aa1cac. ± Comparison against base commit 3cb68c2.

This pull request removes 8 and adds 84 tests. Note that renamed tests count towards both.
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(08/03/2026 14:31:31 +00:00)
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(08/03/2026 14:31:31)
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(d6ea070b-8d6b-46d6-9e04-bb9b4acedc59)
Eventuous.Tests.Subscriptions.ResubscribeOnHandlerFailureTests ‑ Should_not_throw_nre_when_ack_races_with_resubscribe
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2026-08-03T14:31:52.5505416+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-08-03T14:31:52.5505416+00:00 }, CommitPosition { Position: 0, Sequence: 4, Timestamp: 2026-08-03T14:31:52.5505416+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2026-08-03T14:31:52.5505416+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-08-03T14:31:52.5505416+00:00 })
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2026-08-03T14:31:52.5505416+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-08-03T14:31:52.5505416+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2026-08-03T14:31:52.5505416+00:00 }, CommitPosition { Position: 0, Sequence: 8, Timestamp: 2026-08-03T14:31:52.5505416+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-08-03T14:31:52.5505416+00:00 })
Eventuous.Tests.Subscriptions.SubscriptionShutdownTests ‑ Drop_after_shutdown_started_does_not_resubscribe
Eventuous.Tests.Subscriptions.SubscriptionShutdownTests ‑ Drop_racing_unsubscribe_does_not_report_a_disposed_cts
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(08/17/2026 17:38:00 +00:00)
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(08/17/2026 17:38:00)
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(a96896b6-5b69-4d3c-84a1-093d4566fef0)
Eventuous.Tests.GooglePubSub.PubSubTests ‑ StopsAndStartsAgain
Eventuous.Tests.KurrentDB.Subscriptions.SubscriptionRestart ‑ Esdb_ShouldConsumeAfterResubscribe
Eventuous.Tests.KurrentDB.Subscriptions.SubscriptionRestart ‑ Esdb_ShouldTolerateRepeatedUnsubscribe
Eventuous.Tests.Postgres.Subscriptions.SubscriptionRestart ‑ Postgres_ShouldConsumeAfterResubscribe
Eventuous.Tests.Postgres.Subscriptions.SubscriptionRestart ‑ Postgres_ShouldTolerateRepeatedUnsubscribe
Eventuous.Tests.Redis.Subscriptions.PollFailureTests ‑ Poll_failure_is_followed_by_another_poll
Eventuous.Tests.Redis.Subscriptions.PollFailureTests ‑ Poll_failure_is_reported_as_a_drop
…

@Inok
Inok marked this pull request as ready for review August 18, 2026 07:07
@qodo-free-for-open-source-projects

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Fix subscription resubscribe race via per-run supervisor loop

🐞 Bug fix ✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Replace drop-based resubscribe with a single supervisor loop per subscription run.
• Fence late acks/drops by scoping tokens, sequence, and teardown to SubscriptionRun.
• Add extensive concurrency/restart/teardown tests across transports to prevent regressions.
Diagram

graph TD
  Host["Host / caller"] --> Sub["EventSubscription<T>"] --> Loop["Supervisor loop"] --> Run["SubscriptionRun"] --> Tx["Transport Connect"]
  Tx --> Run
  Run --> Cp["Checkpoint handler"] --> Store[("Checkpoint store")]
  Run --> Worker["Channel workers"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Minimal locking fix on old Dropped()/generation model
  • ➕ Smaller diff; less transport churn
  • ➕ Lower short-term regression surface in provider packages
  • ➖ Keeps multiple-writer lifecycle model (Dropped/stop/dispose) that caused the race
  • ➖ Hard to reason about and to prove correct; more future regressions likely
2. External retry policy (e.g., Polly) around transport reconnects
  • ➕ Reuses battle-tested retry primitives
  • ➕ Potentially configurable backoff/jitter features
  • ➖ Doesn't solve late-ack/late-drop fencing; still needs per-run identity
  • ➖ Teardown ordering and resource release registration still required in-house
3. Per-transport supervisor loops (instead of unified core loop)
  • ➕ Transport authors can tune behavior per broker SDK quirks
  • ➕ Avoids enforcing a single contract across all implementations
  • ➖ Duplicates complex lifecycle logic across providers
  • ➖ Inconsistent semantics between transports; test matrix grows significantly

Recommendation: Keep the PR's unified supervisor-loop + per-run identity approach. It directly eliminates the core race class (late signals targeting the wrong run) and standardizes teardown ordering via run.OnDisconnect, making correctness reviewable in one place (EventSubscription + SubscriptionRun) and provable via the added concurrency/restart suites.

Files changed (58) +4237 / -816

Enhancement (3) +220 / -1
SubscriptionLogging.csAdd supervision, teardown, and run-fencing log events +55/-1

Add supervision, teardown, and run-fencing log events

• Adds logs for invalid supervisor settings, stop timeouts, supervisor failures, disconnect failures, callback failures, and run-fenced message handling outcomes.

src/Core/src/Eventuous.Subscriptions/Logging/SubscriptionLogging.cs

SubscriptionOptions.csAdd retry delay and teardown timeout options with defaults +30/-0

Add retry delay and teardown timeout options with defaults

• Introduces RetryDelay and TeardownTimeout with documented defaults and behavior, enabling central supervision tuning without per-transport knobs.

src/Core/src/Eventuous.Subscriptions/SubscriptionOptions.cs

SubscriptionRun.csIntroduce SubscriptionRun for per-attempt identity, failure, and teardown +135/-0

Introduce SubscriptionRun for per-attempt identity, failure, and teardown

• Adds a run object that owns the per-attempt cancellation token, first-failure-wins drop reason, disconnect release registration, and per-run sequence generation.

src/Core/src/Eventuous.Subscriptions/SubscriptionRun.cs

Bug fix (15) +733 / -554
ServiceBusSubscription.csAdopt per-run Connect and fail-on-dead-receive semantics +67/-47

Adopt per-run Connect and fail-on-dead-receive semantics

• Refactors Service Bus subscription startup to Connect(run), scopes sequence numbers to the run, registers processor teardown via run.OnDisconnect, and ensures receive-loop terminal errors trigger run.Fail so the supervisor can resubscribe.

src/Azure/src/Eventuous.Azure.ServiceBus/Subscriptions/ServiceBusSubscription.cs

ChannelExtensions.csMake channel finalize robust even when reader drain fails +15/-14

Make channel finalize robust even when reader drain fails

• Ensures finalize callbacks run in a finally block after drain attempts, improving checkpoint flush reliability during timeouts or reader failures.

src/Core/src/Eventuous.Subscriptions/Channels/ChannelExtensions.cs

ChannelWorkerBase.csRefuse writes during shutdown and return acceptance result +30/-18

Refuse writes during shutdown and return acceptance result

• Changes Write to return a boolean indicating whether the element was queued, and makes worker disposal explicitly idempotent and safe under concurrent shutdown paths.

src/Core/src/Eventuous.Subscriptions/Channels/ChannelWorkerBase.cs

CheckpointCommitHandler.csReturn whether commit was accepted (vs handler already stopped) +2/-2

Return whether commit was accepted (vs handler already stopped)

• Changes Commit to return a bool so callers can distinguish a successful enqueue from a stopped handler, preventing false 'acked' progress on ended runs.

src/Core/src/Eventuous.Subscriptions/Checkpoints/CheckpointCommitHandler.cs

EventSubscription.csReplace drop-driven resubscribe with single supervisor loop + sessions +214/-98

Replace drop-driven resubscribe with single supervisor loop + sessions

• Introduces a single background supervisor loop per subscription instance, enforces single active Subscribe call, scopes lifecycle state into a Session record, and uses SubscriptionRun to coordinate failure, teardown, and resubscribe timing.

src/Core/src/Eventuous.Subscriptions/EventSubscription.cs

EventSubscriptionWithCheckpoint.csScope checkpoint commit handler to a SubscriptionRun +71/-69

Scope checkpoint commit handler to a SubscriptionRun

• Moves checkpoint commit handler ownership into a derived run type, passes run explicitly through ack/nack paths to fence late acknowledgements, and ensures teardown ordering keeps checkpoint flush last.

src/Core/src/Eventuous.Subscriptions/EventSubscriptionWithCheckpoint.cs

AsyncHandlingFilter.csAvoid acknowledging cancelled/stopping messages and log refused queueing +21/-10

Avoid acknowledging cancelled/stopping messages and log refused queueing

• Refines cancellation handling so stopping workers do not ack undecided messages, and logs when a message cannot be queued because the worker is stopping.

src/Core/src/Eventuous.Subscriptions/Filters/AsyncHandlingFilter.cs

CloudRunPubSubSubscription.csAlign Cloud Run Pub/Sub subscription with run-based Connect +12/-3

Align Cloud Run Pub/Sub subscription with run-based Connect

• Adjusts the Cloud Run Pub/Sub subscription implementation to use the new Connect(run) pattern and run-scoped sequencing/teardown where applicable.

src/GooglePubSub/src/Eventuous.GooglePubSub.CloudRun/CloudRunPubSubSubscription.cs

GooglePubSubSubscription.csRefactor Pub/Sub start/stop to run-scoped Connect + failure reporting +40/-34

Refactor Pub/Sub start/stop to run-scoped Connect + failure reporting

• Builds and starts SubscriberClient per run, reports unexpected task termination via run.Fail, and registers orderly StopAsync/join logic with run.OnDisconnect to prevent deadlocks and missed drops.

src/GooglePubSub/src/Eventuous.GooglePubSub/Subscriptions/GooglePubSubSubscription.cs

AllStreamSubscription.csMove $all subscription pump into run-scoped Connect with fenced teardown +86/-112

Move $all subscription pump into run-scoped Connect with fenced teardown

• Refactors $all subscription to Connect(run), runs message consumption on a dedicated task that reports run.Fail on errors, and registers teardown to join the pump and dispose enumerator/subscription safely and in order.

src/KurrentDB/src/Eventuous.KurrentDB/Subscriptions/AllStreamSubscription.cs

PersistentSubscriptionBase.csAdopt Connect(run) and run-scoped drop reporting for persistent subs +15/-27

Adopt Connect(run) and run-scoped drop reporting for persistent subs

• Moves persistent subscription connect into Connect(run), wires drop callbacks to run.Fail, scopes context sequence to the run, and registers handle disposal via run.OnDisconnect.

src/KurrentDB/src/Eventuous.KurrentDB/Subscriptions/PersistentSubscriptionBase.cs

StreamSubscription.csAdopt Connect(run) and run-scoped sequencing/teardown for stream subs +11/-9

Adopt Connect(run) and run-scoped sequencing/teardown for stream subs

• Refactors stream subscription to use Connect(run), report drops via run.Fail, generate per-run sequences, and register subscription disposal via run.OnDisconnect.

src/KurrentDB/src/Eventuous.KurrentDB/Subscriptions/StreamSubscription.cs

RabbitMqSubscription.csMake RabbitMQ subscription run-scoped and resilient to late ack/nack +67/-43

Make RabbitMQ subscription run-scoped and resilient to late ack/nack

• Refactors to Connect(run), scopes channel/connection to the run with run.OnDisconnect teardown, and safely swallows ack/nack failures caused by already-closed channels so late handlers don't wedge resubscribe.

src/RabbitMq/src/Eventuous.RabbitMq/Subscriptions/RabbitMqSubscription.cs

RedisSubscriptionBase.csRun Redis polling loop per run and report pump failures via run.Fail +37/-29

Run Redis polling loop per run and report pump failures via run.Fail

• Moves polling startup into Connect(run), computes start position once per run, runs the poll loop on a background task that classifies failures, and fences sequence generation to the run.

src/Redis/src/Eventuous.Redis/Subscriptions/RedisSubscriptionBase.cs

SqlSubscriptionBase.csRefactor SQL polling subscriptions to run-scoped Connect and pump failure reporting +45/-39

Refactor SQL polling subscriptions to run-scoped Connect and pump failure reporting

• Moves the polling loop to Poll(run, ...) invoked from Connect(run), improves cancellation classification, lets pump faults propagate to the supervisor via run.Fail, and scopes sequence numbers to the run.

src/Relational/src/Eventuous.Sql.Base/Subscriptions/SqlSubscriptionBase.cs

Refactor (5) +8 / -31
ChannelWorkers.csAlign channel worker construction with new worker base API +2/-2

Align channel worker construction with new worker base API

• Updates worker wiring to match the updated ChannelWorkerBase constructor/signature changes.

src/Core/src/Eventuous.Subscriptions/Channels/ChannelWorkers.cs

DropReason.csSimplify and stabilize drop reasons for supervision +4/-5

Simplify and stabilize drop reasons for supervision

• Removes Stopped as a drop reason and assigns stable numeric values to remaining reasons, matching the new supervisor-driven shutdown classification.

src/Core/src/Eventuous.Subscriptions/DropReason.cs

KafkaBasicSubscription.csRename subscription hook to Connect(run) (still unimplemented) +1/-4

Rename subscription hook to Connect(run) (still unimplemented)

• Updates the stub Kafka subscription to the new abstract method signature so it compiles against the refactored base class contract.

src/Kafka/src/Eventuous.Kafka/Subscriptions/KafkaBasicSubscription.cs

KurrentDBCatchUpSubscriptionBase.csRemove obsolete catch-up base logic now handled by supervisor/run +0/-19

Remove obsolete catch-up base logic now handled by supervisor/run

• Deletes legacy catch-up lifecycle code paths that are superseded by the unified supervisor loop and per-run teardown model.

src/KurrentDB/src/Eventuous.KurrentDB/Subscriptions/KurrentDBCatchUpSubscriptionBase.cs

KurrentDBMappings.csAdjust drop-reason mapping for updated DropReason enum +1/-1

Adjust drop-reason mapping for updated DropReason enum

• Updates mapping to align with the revised DropReason values and semantics under the new supervisor model.

src/KurrentDB/src/Eventuous.KurrentDB/Subscriptions/KurrentDBMappings.cs

Tests (32) +3058 / -230
SendAndReceive.csAdjust Service Bus test behavior for new supervision model +4/-0

Adjust Service Bus test behavior for new supervision model

• Extends/adjusts the send/receive test to remain stable under the updated subscription lifecycle and teardown behavior.

src/Azure/test/Eventuous.Tests.Azure.ServiceBus/SendAndReceive.cs

StoreFixtureBase.csHarden store fixture lifecycle for restart/blip scenarios +32/-8

Harden store fixture lifecycle for restart/blip scenarios

• Adjusts persistence fixture behavior to support new restart-focused tests, improving setup/teardown stability under transient store interruptions.

src/Core/test/Eventuous.Tests.Persistence.Base/Fixtures/StoreFixtureBase.cs

SubscriptionFixtureBase.csAlign base subscription test fixtures with new supervision contract +16/-4

Align base subscription test fixtures with new supervision contract

• Updates shared subscription test fixture setup to reflect Connect/run-based lifecycle and new teardown semantics.

src/Core/test/Eventuous.Tests.Subscriptions.Base/Fixtures/SubscriptionFixtureBase.cs

TestEventHandler.csEnhance test handler observability for concurrency assertions +13/-1

Enhance test handler observability for concurrency assertions

• Adds small helper/metrics changes so tests can assert ordering, cancellation, and replay behavior under resubscribe races.

src/Core/test/Eventuous.Tests.Subscriptions.Base/Fixtures/TestEventHandler.cs

SubscriptionRestartBase.csAdd cross-provider restart contract test base +99/-0

Add cross-provider restart contract test base

• Introduces a reusable base suite defining expected restart behavior (teardown once, recovery after broker restarts) for transport-specific test projects.

src/Core/test/Eventuous.Tests.Subscriptions.Base/SubscriptionRestartBase.cs

AssemblyInfo.csConfigure subscription test assembly behavior +7/-0

Configure subscription test assembly behavior

• Adds assembly-level configuration to support the new concurrency-heavy subscription tests (e.g., controlling parallelization/visibility).

src/Core/test/Eventuous.Tests.Subscriptions/AssemblyInfo.cs

CancelledMessageTests.csAdd coverage for cancellation paths and non-ack behavior +218/-0

Add coverage for cancellation paths and non-ack behavior

• Introduces tests ensuring cancelled/stopping message handling does not incorrectly ack and that messages are redelivered appropriately on subsequent runs.

src/Core/test/Eventuous.Tests.Subscriptions/CancelledMessageTests.cs

CapturingLoggerFactory.csAdd logger capture utility for asserting lifecycle logs +47/-0

Add logger capture utility for asserting lifecycle logs

• Adds a test logger factory to capture and assert supervision/teardown logs produced under concurrent stop/resubscribe scenarios.

src/Core/test/Eventuous.Tests.Subscriptions/CapturingLoggerFactory.cs

CheckpointCommitHandlerBackpressureTests.csUpdate backpressure expectations to match new commit return value +5/-11

Update backpressure expectations to match new commit return value

• Adjusts existing backpressure tests to reflect Commit returning acceptance state and updated worker/refusal semantics.

src/Core/test/Eventuous.Tests.Subscriptions/CheckpointCommitHandlerBackpressureTests.cs

CheckpointCommitHandlerLifecycleTests.csAdd lifecycle tests for commit handler stop/flush ordering +149/-0

Add lifecycle tests for commit handler stop/flush ordering

• Adds tests validating commit handler shutdown ordering, flush guarantees, and behavior when commits race teardown.

src/Core/test/Eventuous.Tests.Subscriptions/CheckpointCommitHandlerLifecycleTests.cs

CompositionHandlerTests.csUpdate composition tests for revised subscription APIs +1/-3

Update composition tests for revised subscription APIs

• Adjusts tests to match the new subscription lifecycle contract and any changed exception/registration behavior.

src/Core/test/Eventuous.Tests.Subscriptions/CompositionHandlerTests.cs

RegistrationTests.csUpdate registration tests for single-run Subscribe enforcement +1/-3

Update registration tests for single-run Subscribe enforcement

• Updates tests to reflect Subscribe throwing when called concurrently and to validate lifecycle callbacks under the supervisor model.

src/Core/test/Eventuous.Tests.Subscriptions/RegistrationTests.cs

ResubscribeConcurrencyTests.csAdd high-contention resubscribe/teardown race reproducer suite +889/-0

Add high-contention resubscribe/teardown race reproducer suite

• Adds an extensive concurrency test suite reproducing resubscribe/teardown defects and validating that late drops/acks are fenced to the correct run.

src/Core/test/Eventuous.Tests.Subscriptions/ResubscribeConcurrencyTests.cs

ResubscribeOnHandlerFailureTests.csRework handler-failure resubscribe tests for run-scoped failures +27/-119

Rework handler-failure resubscribe tests for run-scoped failures

• Updates tests to validate that handler failures are reported as run failures and retried via the supervisor loop without wedging the subscription.

src/Core/test/Eventuous.Tests.Subscriptions/ResubscribeOnHandlerFailureTests.cs

SubscriptionRunTests.csAdd unit tests for SubscriptionRun failure and disconnect semantics +179/-0

Add unit tests for SubscriptionRun failure and disconnect semantics

• Adds coverage for first-failure-wins behavior, Ended completion, teardown ordering, and disconnect registration execution.

src/Core/test/Eventuous.Tests.Subscriptions/SubscriptionRunTests.cs

SubscriptionShutdownTests.csValidate shutdown waits/budgets and non-blocking StopAsync behavior +82/-75

Validate shutdown waits/budgets and non-blocking StopAsync behavior

• Updates and expands shutdown tests to ensure StopAsync does not throw on timeouts, teardown continues on its own budget, and sessions are cleared for future runs.

src/Core/test/Eventuous.Tests.Subscriptions/SubscriptionShutdownTests.cs

SupervisorSettingsTests.csAdd tests for retry/teardown option validation and fallbacks +82/-0

Add tests for retry/teardown option validation and fallbacks

• Adds tests verifying invalid RetryDelay/TeardownTimeout values are logged and replaced with safe defaults.

src/Core/test/Eventuous.Tests.Subscriptions/SupervisorSettingsTests.cs

SupervisorTests.csAdd comprehensive supervisor loop behavior tests +598/-0

Add comprehensive supervisor loop behavior tests

• Introduces a large suite validating connect failures, resubscribe timing, drop reporting, and teardown ordering through the single supervisor loop.

src/Core/test/Eventuous.Tests.Subscriptions/SupervisorTests.cs

Transitions.csAdd transition helpers for supervisor state assertions +27/-0

Add transition helpers for supervisor state assertions

• Adds shared helpers/models used by supervisor tests to describe and assert lifecycle transitions deterministically.

src/Core/test/Eventuous.Tests.Subscriptions/Transitions.cs

TransportPump.csAdd fake transport pump for deterministic loop/failure injection +31/-0

Add fake transport pump for deterministic loop/failure injection

• Introduces a controllable pump used by tests to simulate transport loops, drops, and late completions without real broker dependencies.

src/Core/test/Eventuous.Tests.Subscriptions/TransportPump.cs

TransportTeardownTests.csAdd tests ensuring teardown runs once and in correct order +60/-0

Add tests ensuring teardown runs once and in correct order

• Adds tests validating run.OnDisconnect ordering, idempotent disposal, and that teardown does not overlap with subsequent Connect attempts.

src/Core/test/Eventuous.Tests.Subscriptions/TransportTeardownTests.cs

Wait.csAdd wait utilities for concurrency-heavy tests +25/-0

Add wait utilities for concurrency-heavy tests

• Adds test timing utilities to reduce flakiness and to coordinate race windows in concurrency tests.

src/Core/test/Eventuous.Tests.Subscriptions/Wait.cs

RegistrationTests.csUpdate gateway registration tests for updated subscription contract +1/-3

Update gateway registration tests for updated subscription contract

• Adjusts gateway-level registration tests to align with single active Subscribe and updated lifecycle expectations.

src/Gateway/test/Eventuous.Tests.Gateway/RegistrationTests.cs

PubSubTests.csUpdate Pub/Sub tests for restart/supervisor semantics +21/-1

Update Pub/Sub tests for restart/supervisor semantics

• Adjusts Pub/Sub integration tests to match the new lifecycle behavior and improved teardown/retry logic.

src/GooglePubSub/test/Eventuous.Tests.GooglePubSub/PubSubTests.cs

PersistentSubscriptionFixture.csUpdate KurrentDB persistent subscription fixture for restart contract +14/-2

Update KurrentDB persistent subscription fixture for restart contract

• Adjusts fixture setup/teardown to support the new restart-focused base tests and updated subscription lifecycle semantics.

src/KurrentDB/test/Eventuous.Tests.KurrentDB/Subscriptions/Fixtures/PersistentSubscriptionFixture.cs

SubscriptionRestartTests.csAdd KurrentDB restart tests using shared restart contract +25/-0

Add KurrentDB restart tests using shared restart contract

• Adds provider-level tests validating the subscription survives broker restarts and teardown is correctly observed and bounded.

src/KurrentDB/test/Eventuous.Tests.KurrentDB/Subscriptions/SubscriptionRestartTests.cs

SubscriptionRestartTests.csAdd Postgres restart tests using shared restart contract +25/-0

Add Postgres restart tests using shared restart contract

• Introduces restart tests to validate polling subscriptions reconnect correctly after store interruptions.

src/Postgres/test/Eventuous.Tests.Postgres/Subscriptions/SubscriptionRestartTests.cs

PollFailureTests.csAdd Redis poll failure tests for drop/retry classification +92/-0

Add Redis poll failure tests for drop/retry classification

• Adds tests ensuring Redis polling failures trigger resubscribe via run.Fail and that shutdown vs drop is correctly distinguished.

src/Redis/test/Eventuous.Tests.Redis/Subscriptions/PollFailureTests.cs

ExcludeOnMacOs.csAdd platform exclusion attribute for SqlServer tests +7/-0

Add platform exclusion attribute for SqlServer tests

• Adds a helper to skip SqlServer subscription tests on macOS where the environment/runtime support is constrained.

src/SqlServer/test/Eventuous.Tests.SqlServer/ExcludeOnMacOs.cs

SubscriptionRestartTests.csAdd SqlServer restart tests using shared restart contract +24/-0

Add SqlServer restart tests using shared restart contract

• Introduces restart tests validating SQL Server polling subscriptions recover cleanly after transient failures.

src/SqlServer/test/Eventuous.Tests.SqlServer/Subscriptions/SubscriptionRestartTests.cs

PollFailureTests.csAdd SQLite poll failure tests for supervisor-driven recovery +153/-0

Add SQLite poll failure tests for supervisor-driven recovery

• Adds tests validating how polling failures are surfaced and retried under the new run/supervisor model.

src/Sqlite/test/Eventuous.Tests.Sqlite/Subscriptions/PollFailureTests.cs

SubscriptionRestartTests.csAdd SQLite restart/teardown tests with shared restart contract +104/-0

Add SQLite restart/teardown tests with shared restart contract

• Introduces restart tests validating lifecycle correctness and teardown behavior across simulated store interruptions.

src/Sqlite/test/Eventuous.Tests.Sqlite/Subscriptions/SubscriptionRestartTests.cs

Documentation (2) +213 / -0
2026-08-14-subscription-supervisor-design.mdDocument the new subscription supervisor/run contract +205/-0

Document the new subscription supervisor/run contract

• Adds a detailed design note describing the Connect(SubscriptionRun) contract, per-run identity, teardown registration, and supervision semantics.

docs/plans/2026-08-14-subscription-supervisor-design.md

IMessageSubscription.csClarify Subscribe contract (single active run, retry semantics) +8/-0

Clarify Subscribe contract (single active run, retry semantics)

• Documents the updated Subscribe/Unsubscribe semantics, including single active run enforcement and callback behavior across resubscribes.

src/Core/src/Eventuous.Subscriptions/IMessageSubscription.cs

Other (1) +5 / -0
Eventuous.slnxWire new subscription test projects/files into solution +5/-0

Wire new subscription test projects/files into solution

• Updates the solution configuration to include the new/updated subscription test artifacts added for restart/race coverage.

Eventuous.slnx

@qodo-free-for-open-source-projects

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Delay upper bound unchecked 🐞 Bug ☼ Reliability
Description
SupervisorSettings.From only guards negative RetryDelay/TeardownTimeout; overly-large finite values
can throw in Task.Delay(settings.RetryDelay) or new
CancellationTokenSource(settings.TeardownTimeout), which terminates RunSubscriptionLoop and leaves
the subscription down. Because the supervisor’s outer catch only logs/report-drops and then exits
(no retry), this becomes a permanent outage until restart/recreate.
Code

src/Core/src/Eventuous.Subscriptions/EventSubscription.cs[R361-364]

+        // InfiniteTimeSpan is exempt: both Task.Delay and CancellationTokenSource accept it as "never".
+        if (retryDelay < TimeSpan.Zero && retryDelay != Timeout.InfiniteTimeSpan) {
+            log.SubscriptionRetryDelayInvalid(retryDelay, SubscriptionOptions.DefaultRetryDelay);
+            retryDelay = SubscriptionOptions.DefaultRetryDelay;
Evidence
The PR adds RetryDelay/TeardownTimeout as user-settable TimeSpans, but SupervisorSettings.From only
checks for negative values. Those settings are then used directly in Task.Delay and
CancellationTokenSource creation; an out-of-range finite TimeSpan will throw and is handled by the
supervisor’s top-level catch which logs and then exits, leaving the subscription stopped.

src/Core/src/Eventuous.Subscriptions/SubscriptionOptions.cs[18-47]
src/Core/src/Eventuous.Subscriptions/EventSubscription.cs[357-372]
src/Core/src/Eventuous.Subscriptions/EventSubscription.cs[154-170]
src/Core/src/Eventuous.Subscriptions/EventSubscription.cs[207-210]
src/Core/src/Eventuous.Subscriptions/EventSubscription.cs[177-185]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`SupervisorSettings.From` validates only negative `RetryDelay`/`TeardownTimeout` (excluding `Timeout.InfiniteTimeSpan`), but does not validate **overly-large finite** values. Those values are later passed to `Task.Delay(settings.RetryDelay, ...)` and `new CancellationTokenSource(settings.TeardownTimeout)`, which can throw `ArgumentOutOfRangeException` for values above the runtime-supported finite maximum (milliseconds > `int.MaxValue`). The exception is caught by the supervisor’s outer catch, which logs and exits the loop, leaving the subscription down permanently.

## Issue Context
- `SubscriptionOptions` exposes both properties as user-configurable `TimeSpan`s.
- `RunSubscriptionLoop` uses them directly.

## Fix Focus Areas
- src/Core/src/Eventuous.Subscriptions/EventSubscription.cs[357-374]
- src/Core/src/Eventuous.Subscriptions/EventSubscription.cs[154-170]
- src/Core/src/Eventuous.Subscriptions/EventSubscription.cs[207-210]

## Suggested change
- Extend validation in `SupervisorSettings.From` to also reject/clamp finite values greater than the maximum supported by `Task.Delay`/`CancellationTokenSource`.
 - Keep allowing `Timeout.InfiniteTimeSpan`.
 - Use a max like `TimeSpan.FromMilliseconds(int.MaxValue)`.
 - If configured value is invalid (too large), log (reuse existing `SubscriptionRetryDelayInvalid` / `SubscriptionTeardownTimeoutInvalid` or add dedicated log methods) and fall back to `SubscriptionOptions.DefaultRetryDelay` / `DefaultTeardownTimeout`.

## Acceptance criteria
- No `ArgumentOutOfRangeException` can be thrown by `Task.Delay(settings.RetryDelay, ...)` or `new CancellationTokenSource(settings.TeardownTimeout)` due to user configuration.
- Invalid values are surfaced once per Subscribe via log warning and then normalized.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can keep summaries lean with Finding overflow, which tucks the rest behind 'View more'

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +361 to +364
// InfiniteTimeSpan is exempt: both Task.Delay and CancellationTokenSource accept it as "never".
if (retryDelay < TimeSpan.Zero && retryDelay != Timeout.InfiniteTimeSpan) {
log.SubscriptionRetryDelayInvalid(retryDelay, SubscriptionOptions.DefaultRetryDelay);
retryDelay = SubscriptionOptions.DefaultRetryDelay;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Delay upper bound unchecked 🐞 Bug ☼ Reliability

SupervisorSettings.From only guards negative RetryDelay/TeardownTimeout; overly-large finite values
can throw in Task.Delay(settings.RetryDelay) or new
CancellationTokenSource(settings.TeardownTimeout), which terminates RunSubscriptionLoop and leaves
the subscription down. Because the supervisor’s outer catch only logs/report-drops and then exits
(no retry), this becomes a permanent outage until restart/recreate.
Agent Prompt
## Issue description
`SupervisorSettings.From` validates only negative `RetryDelay`/`TeardownTimeout` (excluding `Timeout.InfiniteTimeSpan`), but does not validate **overly-large finite** values. Those values are later passed to `Task.Delay(settings.RetryDelay, ...)` and `new CancellationTokenSource(settings.TeardownTimeout)`, which can throw `ArgumentOutOfRangeException` for values above the runtime-supported finite maximum (milliseconds > `int.MaxValue`). The exception is caught by the supervisor’s outer catch, which logs and exits the loop, leaving the subscription down permanently.

## Issue Context
- `SubscriptionOptions` exposes both properties as user-configurable `TimeSpan`s.
- `RunSubscriptionLoop` uses them directly.

## Fix Focus Areas
- src/Core/src/Eventuous.Subscriptions/EventSubscription.cs[357-374]
- src/Core/src/Eventuous.Subscriptions/EventSubscription.cs[154-170]
- src/Core/src/Eventuous.Subscriptions/EventSubscription.cs[207-210]

## Suggested change
- Extend validation in `SupervisorSettings.From` to also reject/clamp finite values greater than the maximum supported by `Task.Delay`/`CancellationTokenSource`.
  - Keep allowing `Timeout.InfiniteTimeSpan`.
  - Use a max like `TimeSpan.FromMilliseconds(int.MaxValue)`.
  - If configured value is invalid (too large), log (reuse existing `SubscriptionRetryDelayInvalid` / `SubscriptionTeardownTimeoutInvalid` or add dedicated log methods) and fall back to `SubscriptionOptions.DefaultRetryDelay` / `DefaultTeardownTimeout`.

## Acceptance criteria
- No `ArgumentOutOfRangeException` can be thrown by `Task.Delay(settings.RetryDelay, ...)` or `new CancellationTokenSource(settings.TeardownTimeout)` due to user configuration.
- Invalid values are surfaced once per Subscribe via log warning and then normalized.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant