Schedule in future reading aggregate#2941
Closed
JurJean wants to merge 1 commit into
Closed
Conversation
Member
|
@JurJean Definite problem this time. Apparently a permutation that hadn't been addressed before. There'll be a 6.1.1 release within a couple hours to address that. |
jeremydmiller
added a commit
that referenced
this pull request
May 28, 2026
…ed-cascade Fix scheduled-cascade loss from [ReadAggregate]/[DocumentExists] handlers (closes #2941)
jeremydmiller
added a commit
that referenced
this pull request
May 28, 2026
…de tests Polecat 4.2.1 ships the upstream GH-2941 companion fix (polecat#161): its DocumentSessionBase.SaveChangesAsync no longer early-returns when only ITransactionParticipants are queued, so the StoreIncomingEnvelopeParticipant added via Session.StoreIncoming(...) inside PolecatEnvelopeTransaction. PersistIncomingAsync now actually runs for scheduled cascades from [ReadAggregate]/[DocumentExists] handlers that don't write any documents. This lets the three previously [Skip]'d Polecat tests run for real: - Bug_2941_read_aggregate_scheduled_cascade.read_aggregate_handler_schedules_its_cascading_message - Bug_2941_document_exists_scheduled_cascade.document_exists_handler_schedules_its_cascading_message - Bug_2941_document_exists_scheduled_cascade.document_does_not_exist_handler_schedules_its_cascading_message Local: full wolverine.slnx -c Release builds clean (0 warnings, 0 errors). Marten Bugs sweep 45/45 pass. Polecat tests will be validated by CI (the local SQL Server 2025 image on Apple Silicon is too slow to run them; existing Polecat durable tests time out the same way against it - unrelated to this change). Also tightened the PolecatPersistenceFrameProvider.CanApply comment so it documents the pairing with polecat#161 / Polecat 4.2.1 rather than the 'necessary but not sufficient' caveat from the original commit.
outofrange-consulting
pushed a commit
to outofrange-consulting/wolverine
that referenced
this pull request
May 28, 2026
…lers (JasperFxGH-2941) Closes JasperFx#2941. Co-authored from @JurJean's repro in PR JasperFx#2941. ## Root cause A handler whose only Marten/Polecat dependency comes from a parameter or chain attribute - [ReadAggregate], [DocumentExists<T>], [DocumentDoesNotExist<T>] - silently lost any cascading DeliveryMessage<T>.DelayedFor(...) it emitted. The non-scheduled cascade case is unaffected because local non-scheduled cascades take Wolverine's in-memory delivery path that does not require flushing the session. Mechanism: 1. AutoApplyTransactions decides which persistence provider to apply by calling IPersistenceFrameProvider.CanApply, which queries chain.ServiceDependencies() for IDocumentSession. 2. Chain.serviceDependencies only walks Middleware.OfType<MethodCall>() - other frame types (AsyncFrame, etc.) are not inspected. 3. [ReadAggregate] injects FetchLatestAggregateFrame (AsyncFrame). [DocumentExists<T>]/ [DocumentDoesNotExist<T>] inject DocumentExistenceCheckFrame (AsyncFrame). Both depend on IDocumentSession but neither is a MethodCall, so the dependency is invisible to ServiceDependencies. 4. Worse: WolverineParameterAttribute.Modify and ModifyChainAttribute.Modify both run lazily inside HandlerChain.applyCustomizations - long AFTER AutoApplyTransactions has evaluated CanApply. Even adding chain.AddDependencyType inside Modify is too late. Result: CanApply returns false, no DocumentSessionSaveChanges postprocessor is attached, and MartenEnvelopeTransaction.PersistIncomingAsync / its Polecat equivalent queue StoreIncoming(...) on the session that is never flushed. The scheduled envelope never lands in wolverine_incoming_envelopes, so the scheduler never picks it up. [WriteAggregate] and [BoundaryModel] do not need this fix - their Modify() paths explicitly call new XxxPersistenceFrameProvider().ApplyTransactionSupport(...) themselves (AggregateHandling.Apply, BoundaryModelAttribute.Modify). Saga chains hit the SagaChain short-circuit in CanApply. ## Fix Add direct attribute detection to MartenPersistenceFrameProvider.CanApply and PolecatPersistenceFrameProvider.CanApply - walk handler-method parameters for [ReadAggregate], and handler-method/handler-type/message-type attributes for DocumentExistsAttribute<>/DocumentDoesNotExistAttribute<>. Detection happens by reflection on the existing handler metadata, so it doesn't depend on attribute Modify having run yet. ## Tests - src/Persistence/MartenTests/Bugs/Bug_aggregate_should_still_publish.cs: carries @JurJean's PR JasperFx#2941 scheduled-cascade scenarios. Also fixed the test setup by adding PublishReader to IncludeType - the PR refactored the single ScheduleReader class into PublishReader (non-scheduled) and ScheduleReader (scheduled) but only kept ScheduleReader registered, which made the publishes test fail for "no handler" reasons rather than the real bug. - Bug_2941_document_exists_scheduled_cascade.cs (Marten + Polecat parallels): pin the same contract for [DocumentExists<T>]/[DocumentDoesNotExist<T>]. Negative-control verified locally: temporarily disabling the DocumentExists branch of the CanApply fix makes both tests fail (2/2). - Bug_2941_read_aggregate_scheduled_cascade.cs (Polecat parallel of the Marten Bug_aggregate test): pins the [ReadAggregate] contract on the Polecat side. Local verification: Marten Bugs sweep 45/45 (full Postgres-backed integration tests including the new scheduled-cascade pins) + full `wolverine.slnx -c Release` clean (0 warnings, 0 errors). The Polecat tests build cleanly; local SQL Server 2025 emulation on Apple Silicon is too slow to validate them locally (existing Polecat durable tests time out the same way against this image), so they will be validated by CI's native Linux SQL Server. ## Out of scope (follow-up) The IMartenDataRequirement / IPolecatDataRequirement return-value continuation strategies create MartenDataRequirementFrame / PolecatDataRequirementFrame (AsyncFrames using IDocumentSession) via a different code path (handler return value, not attribute injection). They could in theory have a parallel issue when combined with a scheduled cascade, but require a separate test surface. Co-Authored-By: Jur Balledux <JurJean@users.noreply.github.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
outofrange-consulting
pushed a commit
to outofrange-consulting/wolverine
that referenced
this pull request
May 28, 2026
…scade tests Polecat 4.2.1 ships polecat#161, the upstream companion fix to JasperFxGH-2941. Its DocumentSessionBase.SaveChangesAsync no longer early-returns when only ITransactionParticipants are queued, so the StoreIncomingEnvelopeParticipant added via Session.StoreIncoming(...) inside PolecatEnvelopeTransaction. PersistIncomingAsync actually runs for scheduled cascades from [ReadAggregate] / [DocumentExists] handlers that don't write any documents. That lets the three previously [Skip]'d Polecat tests (added in JasperFx#2943 with a Skip reason pointing at polecat#161) exercise the end-to-end path on CI: - Bug_2941_read_aggregate_scheduled_cascade.read_aggregate_handler_schedules_its_cascading_message - Bug_2941_document_exists_scheduled_cascade.document_exists_handler_schedules_its_cascading_message - Bug_2941_document_exists_scheduled_cascade.document_does_not_exist_handler_schedules_its_cascading_message Also tightens the PolecatPersistenceFrameProvider.CanApply comment so it documents the pairing with polecat#161 / Polecat 4.2.1 rather than the 'necessary but not sufficient' caveat from the original commit. Local: full wolverine.slnx -c Release builds clean (0 warnings, 0 errors). Marten Bugs sweep 45/45 pass. Polecat tests will be validated by CI - the local SQL Server 2025 image on Apple Silicon is too slow to run them (existing Polecat durable tests time out the same way, unrelated to this change). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
outofrange-consulting
pushed a commit
to outofrange-consulting/wolverine
that referenced
this pull request
May 28, 2026
…-routing map sees [MartenStore] (closes JasperFx#2944) Closes JasperFx#2944. Reported by @fadrian23. ## The bug A message arriving from an external system in INTEROP mode (no Wolverine envelope headers) whose handler targets an ancillary Marten store via [MartenStore] had its durable inbox envelope persisted in the MAIN store's inbox instead of the ancillary store's inbox. The handler ran correctly, but the envelope landed in the wrong inbox. ## Root cause [MartenStore] derives from ModifyChainAttribute, so MartenStoreAttribute.Modify() runs in PHASE B (lazy, inside HandlerChain.applyCustomizations at first-codegen time). The message-type-to-ancillary-store map that WolverineRuntime.HostService builds during startMessagingTransportsAsync runs in PHASE A (eager, at handler-graph compile). At that point chain. AncillaryStoreType is still null on every chain, so the map is built empty. When a message arrives, DurableLocalQueue / DurableReceiver consults that map, finds nothing, and falls back to the main store. Same Phase A vs Phase B trap as JasperFxGH-2941. See [[reference_handler_chain_customization_phases]] for the broader pattern. The prior fix at WolverineRuntime.HostService.cs:447 (AllChains() over Chains, refs JasperFxGH-2576) addressed per-endpoint sticky chains under MultipleHandlerBehavior.Separated, but didn't address the ordering trap - by the time it runs, the chains still haven't had their AncillaryStoreType set. ## Fix MartenStoreEagerPolicy (IHandlerPolicy, lives in Wolverine.Marten, registered in MartenIntegration alongside MartenAggregateHandlerStrategy). Runs in Phase A and pre-populates chain.AncillaryStoreType by walking each HandlerChain's handler-type and handler-method for [MartenStore] - matching the discovery rules in Chain.applyAttributesAndConfigureMethods. Also walks the per-endpoint sticky child chains (ByEndpoint) so Separated-mode keeps working alongside JasperFx#2576's AllChains() fix. The Phase B MartenStoreAttribute.Modify() still runs later: the AncillaryStoreType reassignment is idempotent, and the AncillaryOutboxFactoryFrame middleware insertion stays where it has to be (it participates in codegen). ## Tests New Bug_2944_interop_ancillary_inbox in Wolverine.RabbitMQ.Tests/Bugs/ mirrors the reporter's repro: publish a raw JSON message (no Wolverine headers) to a durable RabbitMQ queue whose default incoming message type has a [MartenStore]-decorated handler, then assert the inbox envelope landed in the ancillary store, not the main store. ## Verification Local (with fix): - Bug_2944_interop_ancillary_inbox: 1/1 pass. - Marten ancillary + bug-family sweep (AncillaryStores + Bug_2318 + Bug_2382 + Bug_2576 + Bug_2669 + Bug_2887 + Bug_ancillary + Distribution.with_ancillary): 30/30. - EfCoreTests.Bug_DurableLocalQueue_ancillary: 3/3. - Wolverine.RabbitMQ.Tests.Bug_2155_ancillary: 1/1. - Wolverine.Http.Tests.using_ancillary_stores: 1/1. - Full Marten Bugs: 45/45. - Full AggregateHandlerWorkflow: 66/66. Negative-control: commenting out the policy registration makes Bug_2944_interop_ancillary_inbox fail with exactly the assertion the reporter described - 'The interop message should have been persisted in the ancillary store's inbox' - confirms the test exercises the bug. PersistenceTests.ModularMonoliths.end_to_end_modular_monolith and .registration_of_message_stores cannot be validated locally (their SQL-Server-touching fixtures time out under emulated SQL Server 2025 on Apple Silicon - the existing Postgres-only subset (13/37) passes; the remaining 24 are local-environment limitation, not a regression). CI on native Linux SQL Server will validate them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
outofrange-consulting
pushed a commit
to outofrange-consulting/wolverine
that referenced
this pull request
May 28, 2026
Implements the in-process mediator portion of the design plan @jeremydmiller posted on JasperFx#2221: - Registry + registration (Configuration/ResultTypeRegistration.cs + ResultTypeRegistry.cs + IResultTypeRegistration.cs). Open-generic and closed-type shapes both supported; open-generic resolves against any closed form via GetGenericTypeDefinition match. - Public API on WolverineOptions (WolverineOptions.Results.cs): UseResultType<TResult>(stopWhen, unwrapWith, errorsFrom) // closed UseResultType<TResult>(stopWhen, errorsFrom) // non-generic UseResultType(typeof(Result<>), ..., unwrappedArgumentIndex = 0) // open-generic - Seam 1 — Middleware/ResultTypeContinuationPolicy.cs. Mirrors RequirementResultContinuationPolicy. New opt-in IRulesAwareContinuationStrategy lets the dispatcher hand GenerationRules to strategies that need to consult per-host state (non-breaking; existing strategies keep the rules-free overload). - Seam 3 — Runtime/Handlers/ResultUnwrappingActionSource.cs + ResultTypeReturnActionPolicy.cs. IHandlerPolicy walks chains at compile and substitutes the chain's IReturnVariableActionSource for the unwrap- and-cascade variant when the handler returns a registered Result type. Phase A pattern, matches the MartenStoreEagerPolicy precedent. - Component R — Runtime/RemoteInvocation/ReplyListener<T>.Complete. Caller-side unwrap: when the response envelope carries a registered Result type, the awaited T decides what we hand back. T == wrapper: pass through. T == inner: unwrap on success, throw ResultFailureException on failure. Threaded via ReplyTracker so the same path serves in-process and remote callers. - ResultFailureException (top-level). Tests at Testing/CoreTests/Acceptance/result_types_end_to_end.cs cover the B-series happy path against FluentResults 3.16.0: Passing: - B-1 invokeasync_T_against_result_returning_handler_unwraps_success - B-4 invokeasync_void_against_result_success_cascades_inner_T - B-5 invokeasync_void_against_result_failure_does_not_cascade_anything - B-7 async_handler_returning_task_of_result_unwraps_normally - E-1 plain_non_result_handlers_are_unaffected_when_result_types_registered [Skip]'d with follow-up notes: - B-2 InvokeAsync<T> failure throws ResultFailureException - B-3 InvokeAsync<Result<T>> returns the wrapper Why those two are deferred: with only seam 3 substituting the chain's ReturnVariableActionSource, the request/reply path on a failure either suppresses the cascade entirely (no reply gets sent) or unwraps before the wire (caller awaiting Result<T> never sees the wrapper). Both need seam 2 (HandlerChain.UseForResponse) to be taught to KEEP the literal wrapper on the reply path while seam 3 unwraps for fire-and-forget. That refinement is the next slice (Phase 3 polish), naturally bundled with Phase 4 (HTTP) and the external-transport request/reply C-series tests since they share the same wire-format-is-literal-Result<T> contract Jeremy specified in Q5. The Phase A vs Phase B ordering trap from JasperFx#2941 / JasperFx#2944 doesn't bite this feature: the seam-1 continuation strategy uses GenerationRules.Properties to read the registry without depending on attribute Modify() having run, and the seam-3 handler policy runs eagerly during HandlerGraph.Compile. Local: full wolverine.slnx -c Release builds clean (0 warnings, 0 errors). CoreTests result_types_end_to_end suite: 5/5 active tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jeremydmiller
added a commit
that referenced
this pull request
May 28, 2026
Bug-fix + feature release on top of 6.1.0 — 13 PRs. Notable additions: - Custom Result<T> handler-return-value support (Phases 0+1+2+3, #2952, refs #2221) - DbContext abstractions for EF Core transaction middleware (#2919 + docs/tests #2954) - Outgoing Envelope pooling at MessageRouter.RouteForPublish (#2956, closes #2955) — ~-504 B/op on transport-bound sends per the CritterStackScalability WolverineTransportBenchmarks harness Bug fixes: scheduled-cascade loss from [ReadAggregate]/[DocumentExists] handlers (#2941), ancillary-store inbox routing (#2944), Postgres queue-name length (#2942), MySQL node-record quoting (#2940), Pulsar batched-partition ack KeyNotFoundException (#2883/#2950), remote-node agent reply timeout (#2949), and additional resource-disposal cleanup (#2894 from dmytro-pryvedeniuk). Polecat bumped 4.1.1 -> 4.2.1 (#2947); Marten + JasperFx families unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
I expanded on the improved version of my initial proposed test, to try to prove my point again that when using
[ReadAggregate], published messages do not end up in thewolverine_incoming_envelopestable.Since non-scheduled messages are published right away the issue is not appearing in the current test. The table is still omitted though. By scheduling the messages in the future I believe I expose the issue.
When stepping through the code, I see the StoreIncomingEnvelope in the unit of work:

But it never gets saved.