Skip to content

Schedule in future reading aggregate#2941

Closed
JurJean wants to merge 1 commit into
JasperFx:mainfrom
JurJean:schedule-from-aggregate
Closed

Schedule in future reading aggregate#2941
JurJean wants to merge 1 commit into
JasperFx:mainfrom
JurJean:schedule-from-aggregate

Conversation

@JurJean
Copy link
Copy Markdown
Contributor

@JurJean JurJean commented May 28, 2026

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 the wolverine_incoming_envelopes table.

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:
afbeelding

But it never gets saved.

@jeremydmiller
Copy link
Copy Markdown
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>
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.

2 participants