Skip to content

fix(marten): GH-2669 alternative — Uri-based ancillary store routing (no IMessageStore.Id dependency)#2674

Merged
jeremydmiller merged 1 commit intomainfrom
alt-2669-uri-based-ancillary-store-routing
May 4, 2026
Merged

fix(marten): GH-2669 alternative — Uri-based ancillary store routing (no IMessageStore.Id dependency)#2674
jeremydmiller merged 1 commit intomainfrom
alt-2669-uri-based-ancillary-store-routing

Conversation

@jeremydmiller
Copy link
Copy Markdown
Member

Alternative to #2670. Same root cause + same primary fix on the DurableLocalQueue / DurableReceiver side (the receiving handler's ancillary store wins over the publisher-stamped envelope.Store), but the gate on FlushOutgoingMessagesOnCommit's in-transaction inbox UPDATE compares stores by Uri rather than IMessageStore.Id.

Why Uri instead of Id

  1. Internal consistency: the same-database fallback in the envelope.Store == null branch (Durable receiver should store envelopes in the ancillary store's database/schema when handler targets an ancillary MartenStore #2382) at FlushOutgoingMessagesOnCommit.cs:51-52 already uses mainStore.Uri == _messageStore.Uri. Reusing that heuristic keeps the file's local notion of "same store" consistent and avoids introducing IMessageStore.Id as a contract surface.

  2. Conservative cross-schema semantics: PostgresqlMessageStore.Id is DatabaseId(server, database) — same DB different schema = same Id. The PR's Id-equality therefore enables a same-database cross-schema in-transaction UPDATE, which only works when the connection user has cross-schema permissions. Uri-equality (server + database + schema) is more conservative — cross-schema cases skip the in-transaction shortcut and let the envelope's owning store mark-handled through its own connection. Same end-state for the ancillary handler scenario in this issue, slightly more predictable behavior in the cross-schema case.

Three changes

  • DurableLocalQueue.assignAncillaryStoreIfNeeded — drop the if (envelope.Store != null) return; guard so the receiving handler's ancillary store overrides any store the publisher stamped. This is the primary fix; without it the publisher's main-store reference reached FlushOutgoingMessagesOnCommit and the envelopeStore.IncomingFullName branch pointed at a table that doesn't exist in the ancillary database (42P01: relation \"public.wolverine_incoming_envelopes\" does not exist).

  • DurableReceiver.assignAncillaryStoreIfNeeded — symmetric guard removal for the broker-receiver path.

  • Wolverine.Marten/FlushOutgoingMessagesOnCommit.BeforeSaveChangesAsync — in the Ancillary listener, when envelope.Store is set, only fold the handled-update into the current Marten transaction if envelopeStore.Uri matches _messageStore.Uri (same connection / schema). Cross-store envelopes return early; the envelope's owning store handles the mark-handled separately.

Tests

Adopted #2670's regression test verbatim (Bug_2669_ancillary_marten_store_local_message_from_main_store.cs).

  • Bug-present (this fix reverted): test fails with the exact 42P01: relation \"public.wolverine_incoming_envelopes\" does not exist error from the issue.
  • Fix-present: test passes.
  • 34/34 of the wider Marten ancillary / Bug_2382 / Bug_2576 / Bug_2669 subset pass with the fix.

Test plan

  • CI: marten workflow green
  • CI: .NET workflow green
  • Local: bug reproduces without fix; passes with fix; no regressions in the ancillary-related subset

Closes #2669. Alternative to #2670 — pick whichever resolution path you prefer; either resolves the underlying bug.

🤖 Generated with Claude Code

…s ancillary store

GH-2669 alternative to #2670. Same root cause + same primary fix on the
DurableLocalQueue / DurableReceiver side (the receiving handler's ancillary
store wins over the publisher-stamped envelope.Store), but the gate on
FlushOutgoingMessagesOnCommit's in-transaction inbox UPDATE compares stores
by Uri rather than IMessageStore.Id.

Two reasons to prefer the Uri compare here:

1. The same-database fallback in the envelope.Store==null branch (GH-2382)
   already uses `mainStore.Uri == _messageStore.Uri`. Reusing that heuristic
   keeps the file's local notion of "same store" consistent and avoids
   introducing IMessageStore.Id as a contract surface.

2. PostgresqlMessageStore.Id is `DatabaseId(server, database)` — same DB
   different schema = same Id. The PR's Id-equality therefore enables a
   same-database cross-schema in-transaction UPDATE, which only works when
   the connection user has cross-schema permissions. Uri-equality
   (server + database + schema) is more conservative: cross-schema cases
   skip the in-transaction shortcut and let the envelope's owning store
   mark-handled through its own connection. Same end-state for the
   ancillary handler scenario in this issue, slightly more predictable
   behavior in the cross-schema case.

Three changes:

- DurableLocalQueue.assignAncillaryStoreIfNeeded — drop the
  `if (envelope.Store != null) return;` guard so the receiving handler's
  ancillary store overrides any store the publisher stamped. This is the
  primary fix; without it the publisher's main-store reference reached
  FlushOutgoingMessagesOnCommit and the envelopeStore.IncomingFullName
  branch pointed at a table that doesn't exist in the ancillary database
  (`42P01: relation "public.wolverine_incoming_envelopes" does not exist`).

- DurableReceiver.assignAncillaryStoreIfNeeded — symmetric guard removal
  for the broker-receiver path.

- Wolverine.Marten/FlushOutgoingMessagesOnCommit.BeforeSaveChangesAsync —
  in the Ancillary listener, when envelope.Store is set, only fold the
  handled-update into the current Marten transaction if envelopeStore.Uri
  matches _messageStore.Uri (same connection / schema). Cross-store
  envelopes return early; the envelope's owning store handles the
  mark-handled separately.

Test: PolicyTests adopted from #2670 verbatim
(Bug_2669_ancillary_marten_store_local_message_from_main_store.cs).
Verified bug-present (this fix reverted) reproduces the exact
`42P01: relation "public.wolverine_incoming_envelopes" does not exist`
error from the issue; with the fix the test passes. 34/34 of the wider
Marten ancillary / Bug_2382 / Bug_2576 / Bug_2669 subset pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jeremydmiller jeremydmiller merged commit 389ef55 into main May 4, 2026
21 of 22 checks passed
jeremydmiller added a commit that referenced this pull request May 4, 2026
Minor release. Highlights:

- WolverineFx.Marten: durable local messages routed by the receiving handler's
  ancillary store (Uri-based gate, no IMessageStore.Id dependency). Closes #2669.
  PR #2674.
- WolverineFx.RabbitMQ: public AddClusterNode(...) API for multi-node failover.
  Closes #2659. PR #2664.
- WolverineFx.Polecat: fixed NRE in OutboxedSessionFactory when constructing the
  FlushOutgoingMessagesOnCommit listener. Closes #2668. PR #2672.
- WolverineFx core: new DocumentStores collection on ServiceCapabilities for
  CritterWatch document-side rendering.
- Dependency bumps: JasperFx 1.28.2 → 1.29.0, JasperFx.Events 1.31.1 → 1.33.1,
  Marten + Marten.AspNetCore 8.32.0 → 8.35.0.

Also backfilled a 5.36.2 entry in CHANGELOG covering the EF Core + outbox flush
rework from PR #2665 that landed without a CHANGELOG note at the time.

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.

Durable local messages from the main Marten store can use the wrong inbox for ancillary store handlers

1 participant