Skip to content

Commit 694bf51

Browse files
jeremydmillerclaude
andcommitted
GH-3883: the Marten integration no longer clobbers an explicit transport schema
IntegrateWithWolverine() registers MartenIntegration as an IWolverineExtension, so its Configure() runs at host build — after an inline UsePostgresqlPersistenceAndTransport(..., transportSchema: ...) in the same options lambda. It then assigned its OWN schema names onto the shared PostgreSQL transport unconditionally, so the integration's defaults silently overwrote whatever the caller had asked for. The failure is silent and lands on the data plane rather than at startup. A host without Marten honours the configured schema and publishes to {configured}.wolverine_queue_x; a Marten-backed consumer listens on wolverine_queues.wolverine_queue_x. Auto-provision creates both tables happily, no error is logged on either side, and nothing is ever delivered — the publisher's rows just accumulate in a table nobody polls. Found from a user's minimal reproduction where the only visible symptom was the same queue table existing in two schemas. TransportSchemaName now records whether it was explicitly assigned and is stamped onto the transport only then; MessageStorageSchemaName is stamped only when non-empty rather than falling back to public. Both cases that work today are unchanged: an explicitly-set Marten knob still wins (a deliberate statement about where the queues go), and a host that configures neither still lands on wolverine_queues. Polecat has the mirror-image gap — PolecatIntegration declares the same two properties but nothing reads them — noted on the issue, not addressed here. MartenTests 552/552, PostgresqlTests 475/475 (net9.0), including four new regression tests covering both registration orders, the explicit-Marten-wins case, and the untouched default. Closes GH-3883. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ri7jHRKhaFNBesQM1B5i1k
1 parent bf6d537 commit 694bf51

2 files changed

Lines changed: 146 additions & 4 deletions

File tree

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
using IntegrationTests;
2+
using Marten;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using Microsoft.Extensions.Hosting;
5+
using Shouldly;
6+
using Wolverine;
7+
using Wolverine.Marten;
8+
using Wolverine.Persistence.Durability;
9+
using Wolverine.Postgresql;
10+
using Wolverine.Postgresql.Transport;
11+
using Wolverine.Runtime;
12+
13+
namespace MartenTests;
14+
15+
/// <summary>
16+
/// GH-3883. <c>IntegrateWithWolverine()</c> is an <see cref="IWolverineExtension"/>, so its
17+
/// Configure() runs at host build — AFTER the inline <c>UsePostgresqlPersistenceAndTransport(...)</c>
18+
/// call in the same options lambda. It used to unconditionally assign its own
19+
/// <c>TransportSchemaName</c> (default "wolverine_queues") onto the shared PostgreSQL transport, so
20+
/// any host combining Marten with an explicitly-schema'd database transport silently lost the
21+
/// schema it asked for: publishers wrote to one schema's queue table and listeners polled another's,
22+
/// with no error on either side.
23+
/// </summary>
24+
public class marten_does_not_clobber_transport_schema : PostgresqlContext
25+
{
26+
private static IHost buildHost(Action<WolverineOptions> configure)
27+
{
28+
return Host.CreateDefaultBuilder()
29+
.UseWolverine(opts =>
30+
{
31+
opts.Discovery.DisableConventionalDiscovery();
32+
opts.Durability.Mode = DurabilityMode.Solo;
33+
configure(opts);
34+
})
35+
.Build(); // Build, not Start — extensions apply at build and no database is touched.
36+
}
37+
38+
private static PostgresqlTransport transportOf(IHost host)
39+
{
40+
var runtime = host.Services.GetRequiredService<IWolverineRuntime>();
41+
return runtime.Options.Transports.GetOrCreate<PostgresqlTransport>();
42+
}
43+
44+
[Fact]
45+
public void an_explicit_transport_schema_survives_marten_integration()
46+
{
47+
using var host = buildHost(opts =>
48+
{
49+
opts.UsePostgresqlPersistenceAndTransport(
50+
Servers.PostgresConnectionString,
51+
"myapp",
52+
"myapp_queues",
53+
MessageStoreRole.Ancillary)
54+
.AutoProvision();
55+
56+
opts.Services.AddMarten(m => m.Connection(Servers.PostgresConnectionString))
57+
.IntegrateWithWolverine();
58+
});
59+
60+
transportOf(host).TransportSchemaName.ShouldBe("myapp_queues");
61+
}
62+
63+
[Fact]
64+
public void the_explicit_schema_survives_regardless_of_registration_order()
65+
{
66+
using var host = buildHost(opts =>
67+
{
68+
opts.Services.AddMarten(m => m.Connection(Servers.PostgresConnectionString))
69+
.IntegrateWithWolverine();
70+
71+
opts.UsePostgresqlPersistenceAndTransport(
72+
Servers.PostgresConnectionString,
73+
"myapp",
74+
"myapp_queues",
75+
MessageStoreRole.Ancillary)
76+
.AutoProvision();
77+
});
78+
79+
transportOf(host).TransportSchemaName.ShouldBe("myapp_queues");
80+
}
81+
82+
[Fact]
83+
public void an_explicit_schema_on_the_marten_integration_still_wins()
84+
{
85+
// The integration's own knob remains authoritative when the caller sets it — that is a
86+
// deliberate statement about where the queues go, not the default leaking through.
87+
using var host = buildHost(opts =>
88+
{
89+
opts.UsePostgresqlPersistenceAndTransport(
90+
Servers.PostgresConnectionString,
91+
"myapp",
92+
"myapp_queues",
93+
MessageStoreRole.Ancillary)
94+
.AutoProvision();
95+
96+
opts.Services.AddMarten(m => m.Connection(Servers.PostgresConnectionString))
97+
.IntegrateWithWolverine(x => x.TransportSchemaName = "marten_chosen");
98+
});
99+
100+
transportOf(host).TransportSchemaName.ShouldBe("marten_chosen");
101+
}
102+
103+
[Fact]
104+
public void the_default_is_unchanged_when_nobody_configures_a_schema()
105+
{
106+
using var host = buildHost(opts =>
107+
{
108+
opts.UsePostgresqlPersistenceAndTransport(Servers.PostgresConnectionString);
109+
110+
opts.Services.AddMarten(m => m.Connection(Servers.PostgresConnectionString))
111+
.IntegrateWithWolverine();
112+
});
113+
114+
transportOf(host).TransportSchemaName.ShouldBe("wolverine_queues");
115+
}
116+
}

src/Persistence/Wolverine.Marten/MartenIntegration.cs

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,24 @@ public void Configure(WolverineOptions options)
9797

9898
options.Policies.ForwardHandledTypes(new EventWrapperForwarder());
9999

100+
// GH-3883: only stamp the transport with schema names the caller actually asked for. This
101+
// Configure() runs at host build — AFTER an inline UsePostgresqlPersistenceAndTransport(...)
102+
// in the same options lambda — so assigning unconditionally meant this integration's own
103+
// DEFAULTS silently overwrote the caller's explicit transport schema. That is invisible and
104+
// it breaks the data plane rather than startup: a Marten-backed consumer ends up listening
105+
// on wolverine_queues.wolverine_queue_x while non-Marten publishers write to
106+
// {configured}.wolverine_queue_x, auto-provision creates both, and nothing is ever delivered.
100107
var transport = options.Transports.GetOrCreate<PostgresqlTransport>();
101-
transport.TransportSchemaName = TransportSchemaName;
102-
transport.MessageStorageSchemaName = MessageStorageSchemaName ?? "public";
108+
109+
if (_transportSchemaNameIsExplicit)
110+
{
111+
transport.TransportSchemaName = TransportSchemaName;
112+
}
113+
114+
if (MessageStorageSchemaName.IsNotEmpty())
115+
{
116+
transport.MessageStorageSchemaName = MessageStorageSchemaName;
117+
}
103118

104119
options.Policies.Add<MartenOpPolicy>();
105120

@@ -125,14 +140,25 @@ public void Configure(WolverineOptions options)
125140
internal MartenEventRouter EventRouter { get; } = new();
126141

127142
private string _transportSchemaName = "wolverine_queues";
143+
private bool _transportSchemaNameIsExplicit;
128144

129145
/// <summary>
130-
/// The database schema to place postgres-backed queues. The default is "wolverine_queues"
146+
/// The database schema to place postgres-backed queues. The default is "wolverine_queues".
131147
/// </summary>
148+
/// <remarks>
149+
/// GH-3883: setting this is authoritative — it overwrites whatever the PostgreSQL transport was
150+
/// configured with, because this integration applies at host build. Leaving it alone leaves the
151+
/// transport's own configuration (its default, or an explicit
152+
/// <c>UsePostgresqlPersistenceAndTransport(..., transportSchema: ...)</c>) untouched.
153+
/// </remarks>
132154
public string TransportSchemaName
133155
{
134156
get => _transportSchemaName;
135-
set => _transportSchemaName = value.ToLowerInvariant();
157+
set
158+
{
159+
_transportSchemaName = value.ToLowerInvariant();
160+
_transportSchemaNameIsExplicit = true;
161+
}
136162
}
137163

138164
private string? _messageStorageSchemaName;

0 commit comments

Comments
 (0)