Apply occurrent.subscription.mode=manual to reactive subscriptions too - #503
Conversation
The reactive starter stopped the synchronous model under manual and did nothing about the asynchronous one, so synchronous projections stopped running while asynchronous subscriptions kept going. The durable model is outermost on this stack and already declines to subscribe while stopped, so manual needs no wrapper: the bean method calls stop() on it. No reactor model implements Spring's Lifecycle, so nothing starts it again. The eight waitUntilStarted().block() calls in the reactor registrars had to be gated in the same change. A paused reactive subscription's waitUntilStarted() never completes, and the wait happens while Spring builds the context, so a missed gate hangs the build rather than failing a test. A @Projection(source = PUSH) is fed by a bean the application supplies, so a reactor ManualStartProjections withholds those instead. Its start and startAll return a Mono, and the id is claimed on subscribe so an unsubscribed Mono leaves the projection withheld rather than dropping it. For a domain feed the registration is withheld along with the catch-up, since registering alone starts buffering, which needs the per-id DomainEventFeed.catchUp the blocking feed already had. ADR 88 records why this needs no wrapper, and corrects ADR 86's claim that the reactive stack already had the property. Resolves #481.
There was a problem hiding this comment.
🟡 Not ready to approve
A concurrency/contract mismatch in the new reactor ManualStartProjections.startAll() return semantics should be addressed to avoid misleading callers in concurrent use.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Pull request overview
Extends occurrent.subscription.mode=manual behavior to the reactive MongoDB Spring Boot starter so reactive subscriptions/projections are registered but do not start (or block context boot) until explicitly resumed/started, matching the property’s documented semantics across stacks.
Changes:
- Stop the reactive durable subscription model (and synchronous model) up-front when
occurrent.subscription.mode != auto, and gate registrarwaitUntilStarted().block()behavior so context startup doesn’t hang inmanual. - Add reactive support for withholding
@Projection(source = PUSH)startup work via a new reactorManualStartProjectionsregistry, including deferring DomainEventFeed registration + catch-up together. - Add reactive test coverage for manual mode (basic pause/resume, push projections, domain-push projections, and restart-with-checkpoint catch-up path), plus ADR/changelog updates.
File summaries
| File | Description |
|---|---|
| framework/spring-boot-starter-mongodb/src/test/java/org/occurrent/springboot/mongo/blocking/SubscriptionModeManualMongoTest.java | Corrects test documentation around context-build hangs vs JUnit @Timeout. |
| framework/spring-boot-starter-mongodb-reactive/src/test/java/org/occurrent/springboot/mongo/reactor/ReactiveSubscriptionModeManualRestartMongoTest.java | New test ensuring manual mode withholds catch-up replay across restart until resumed. |
| framework/spring-boot-starter-mongodb-reactive/src/test/java/org/occurrent/springboot/mongo/reactor/ReactiveSubscriptionModeManualPushProjectionMongoTest.java | New test verifying manual withholding/starting for @Projection(source=PUSH) using PushSubscriptionModel. |
| framework/spring-boot-starter-mongodb-reactive/src/test/java/org/occurrent/springboot/mongo/reactor/ReactiveSubscriptionModeManualMongoTest.java | New test proving reactive subscriptions are registered-but-paused in manual, and resume delivers withheld events. |
| framework/spring-boot-starter-mongodb-reactive/src/test/java/org/occurrent/springboot/mongo/reactor/ReactiveSubscriptionModeManualDomainPushProjectionMongoTest.java | New test verifying domain-push projections are fully withheld (registration + catch-up) until started. |
| framework/spring-boot-starter-mongodb-reactive/src/main/java/org/occurrent/springboot/mongo/reactor/OccurrentReactiveMongoAutoConfiguration.java | Stops reactive durable/synchronous subscription models up-front when not auto to enforce manual semantics. |
| framework/spring-boot-autoconfigure/reactor/src/main/java/org/occurrent/springboot/reactor/SubscriptionAnnotationRegistrar.java | Gates waitUntilStarted behavior on “subscriptions start automatically” to avoid hangs in manual. |
| framework/spring-boot-autoconfigure/reactor/src/main/java/org/occurrent/springboot/reactor/SnapshotAnnotationRegistrar.java | Same gating for snapshot registration waits under manual. |
| framework/spring-boot-autoconfigure/reactor/src/main/java/org/occurrent/springboot/reactor/ProjectionAnnotationRegistrar.java | Defers push/domain-push projection startup work into ManualStartProjections under manual (and adds per-id domain feed catch-up). |
| framework/spring-boot-autoconfigure/reactor/src/main/java/org/occurrent/springboot/reactor/OccurrentReactiveAnnotationConfiguration.java | Auto-configures reactor ManualStartProjections bean for applications to start withheld push projections. |
| framework/spring-boot-autoconfigure/reactor/src/main/java/org/occurrent/springboot/reactor/ManualStartProjections.java | New reactor registry for deferred push-projection startup (start by id / start all). |
| dsl/projection-dsl/reactor/src/main/java/org/occurrent/dsl/projection/reactor/DomainEventFeed.java | Adds per-id catchUp(String) for domain feeds to avoid re-running unrelated catch-ups. |
| dsl/projection-dsl/reactor/src/main/java/org/occurrent/dsl/projection/reactor/CatchupProjectionFeed.java | Exposes package-private id() for per-id catch-up lookup. |
| dsl/projection-dsl/blocking/src/main/java/org/occurrent/dsl/projection/blocking/CatchupProjectionFeed.java | Moves id() earlier (keeps package-private lookup parity with reactor). |
| doc/architecture/decisions/0088-manual-subscription-mode-on-the-reactive-stack.md | New ADR documenting reactive manual-mode design and rationale. |
| changelog.md | Updates release notes to reflect reactive manual-mode parity and new APIs. |
| .context/ORCHESTRATOR.md | Updates orchestrator memory to reflect reactive parity details and ADR 88 context. |
Review details
- Files reviewed: 17/17 changed files
- Comments generated: 1
- Review effort level: Lite
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
startAll snapshotted pendingIds and returned that list whole, so an id another caller claimed first was still reported as started even though start(id) had become a no-op for it. The javadoc promised the ids started. Both stacks had it, so both are fixed: each id is claimed and reported by the same call, and one that was already claimed is left out.
The reactive starter stopped the synchronous model under
occurrent.subscription.mode=manualand did nothing about the asynchronous one, so synchronous projections stopped running while asynchronous subscriptions kept going. Half-applying is worse than not applying, because the property's own documentation told you none of your subscriptions would run.This finishes it. The durable model is outermost on this stack and already declines to subscribe while stopped, so
manualneeds no wrapper, unlike the blocking stack: the bean method callsstop()on it, and no reactor model implements Spring'sLifecycle, so nothing starts it again.The eight
waitUntilStarted().block()calls in the reactor registrars had to be gated in the same change rather than a later one. A paused reactive subscription'swaitUntilStarted()never completes, so stopping the model without gating them means context startup never finishes.A
@Projection(source = PUSH)is fed by a bean you supply rather than by Occurrent's subscription model, so a reactorManualStartProjectionswithholds those instead.start(id)andstartAll()return aMono, and the id is claimed when it is subscribed rather than when it is called, so aMonoyou build and never subscribe leaves the projection withheld instead of dropping its startup work. For a domain feed the registration is withheld along with the catch-up, because registering alone starts buffering, which is why the reactiveDomainEventFeedgains the per-idcatchUpthe blocking one got in #497.ADR 88 records why no wrapper is needed here, and corrects ADR 86's claim that the reactive stack already had this property. It did withhold delivery, but it did not pin the start position, which was the bug fixed in #501.
One thing worth knowing, found by removing a gate and watching what happened. The wait runs while Spring builds the context, before the test method, so a JUnit
@Timeoutdoes not cover it: a dropped gate hangs the build for as long as you let it rather than reporting a failure. The blocking test has claimed the opposite since #497 and that claim is corrected here too.Resolves #481, apart from the OpenRewrite recipe for the renamed property.