Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@
<PackageVersion Include="Grpc.StatusProto" Version="2.76.0" />
<PackageVersion Include="Grpc.Tools" Version="2.76.0" />
<PackageVersion Include="HtmlTags" Version="9.0.0" />
<PackageVersion Include="JasperFx" Version="2.20.0" />
<PackageVersion Include="JasperFx.Events" Version="2.20.0" />
<PackageVersion Include="JasperFx" Version="2.21.0" />
<PackageVersion Include="JasperFx.Events" Version="2.21.0" />
<PackageVersion Include="JasperFx.Events.SourceGenerator" Version="2.19.1" />
<!-- RuntimeCompiler is on its own 5.x line (the Roslyn compiler package) — not the 2.1.x
family; it stays at 5.0.0. -->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
using IntegrationTests;
using JasperFx.Core.Reflection;
using JasperFx.Events.Daemon;
using JasperFx.Events.Projections;
using Marten;
using MartenTests.Distribution.TripDomain;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Shouldly;
using Wolverine;
using Wolverine.Marten;
using Wolverine.Marten.Distribution;

namespace MartenTests.Bugs;

// GH-3290: with UseWolverineManagedEventSubscriptionDistribution = true, Marten's
// DocumentStore wrote a misleading "The async daemon is disabled ... will not be
// executed" console warning at startup even though Wolverine's managed distribution
// does run the async projections. The Marten integration now records the real daemon
// state as DaemonMode.ExternallyManaged (jasperfx#490) — same runtime posture as
// Disabled (no Marten-hosted coordinator, no Marten-side agents), but the warning
// gate stays quiet because the external host runs the projections.
public class Bug_3290_misleading_daemon_disabled_warning : PostgresqlContext
{
private static IHostBuilder configureHost(bool useWolverineManagedDistribution,
DaemonMode? explicitDaemonMode = null,
bool explicitDaemonBeforeIntegration = false)
{
return Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
var marten = opts.Services.AddMarten(m =>
{
m.DisableNpgsqlLogging = true;
m.Connection(Servers.PostgresConnectionString);
m.DatabaseSchemaName = "bug3290";

m.Projections.Add<TripProjection>(ProjectionLifecycle.Async);
});

if (explicitDaemonMode.HasValue && explicitDaemonBeforeIntegration)
{
marten.AddAsyncDaemon(explicitDaemonMode.Value);
}

marten.IntegrateWithWolverine(m =>
{
m.UseWolverineManagedEventSubscriptionDistribution = useWolverineManagedDistribution;
});

if (explicitDaemonMode.HasValue && !explicitDaemonBeforeIntegration)
{
marten.AddAsyncDaemon(explicitDaemonMode.Value);
}
});
}

// Marten writes the async-daemon warning with Console.WriteLine inside the
// DocumentStore constructor, so capture stdout around the first resolution of
// the store. The MartenTests assembly runs a single collection (no parallel
// test classes), so swapping Console.Out here is safe.
private static (DocumentStore Store, string ConsoleOutput) buildStoreCapturingConsole(IHost host)
{
var original = Console.Out;
var writer = new StringWriter();
Console.SetOut(writer);
try
{
var store = host.Services.GetRequiredService<IDocumentStore>().As<DocumentStore>();
return (store, writer.ToString());
}
finally
{
Console.SetOut(original);
}
}

[Fact]
public void managed_distribution_records_externally_managed_and_does_not_warn()
{
using var host = configureHost(useWolverineManagedDistribution: true).Build();

var (store, console) = buildStoreCapturingConsole(host);

// Wolverine's managed distribution replaces Marten's own daemon coordination
// outright, so Marten should see the daemon as externally managed...
store.Options.Projections.AsyncMode.ShouldBe(DaemonMode.ExternallyManaged);

// ...and must not claim the async projections will not be executed
console.ShouldNotContain("The async daemon is disabled");
}

[Fact]
public void managed_distribution_starts_no_marten_hosted_coordinator()
{
using var host = configureHost(useWolverineManagedDistribution: true).Build();

buildStoreCapturingConsole(host);

// The whole point of ExternallyManaged over #3329's HotCold: nothing
// Marten-hosted starts. Marten's own ProjectionCoordinator only ever enters
// the container as an IHostedService via AddAsyncDaemon(), so no registered
// hosted service may be a projection coordinator of any flavor.
host.Services.GetServices<IHostedService>()
.OfType<JasperFx.Events.Daemon.IProjectionCoordinator>()
.ShouldBeEmpty();

// Marten's coordinator interface resolves to Wolverine's distribution-backed
// coordinator (a plain singleton, never hosted), not Marten's ProjectionCoordinator
host.Services.GetRequiredService<Marten.Events.Daemon.Coordination.IProjectionCoordinator>()
.ShouldBeOfType<WolverineProjectionCoordinator>();
}

[Fact]
public void without_managed_distribution_the_warning_still_fires()
{
using var host = configureHost(useWolverineManagedDistribution: false).Build();

var (store, console) = buildStoreCapturingConsole(host);

store.Options.Projections.AsyncMode.ShouldBe(DaemonMode.Disabled);
console.ShouldContain("The async daemon is disabled");
}

[Fact]
public void explicit_add_async_daemon_choice_after_integration_is_not_overwritten()
{
using var host = configureHost(useWolverineManagedDistribution: true, DaemonMode.Solo).Build();

var (store, _) = buildStoreCapturingConsole(host);

store.Options.Projections.AsyncMode.ShouldBe(DaemonMode.Solo);
}

[Fact]
public void explicit_add_async_daemon_choice_before_integration_is_not_overwritten()
{
using var host = configureHost(useWolverineManagedDistribution: true, DaemonMode.Solo,
explicitDaemonBeforeIntegration: true).Build();

var (store, _) = buildStoreCapturingConsole(host);

store.Options.Projections.AsyncMode.ShouldBe(DaemonMode.Solo);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
using IntegrationTests;
using JasperFx.Core.Reflection;
using JasperFx.Events.Daemon;
using JasperFx.Events.Projections;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Polecat;
using Polecat.Internal;
using PolecatTests.Distribution.TripDomain;
using Shouldly;
using Wolverine;
using Wolverine.Polecat;
using Wolverine.Polecat.Distribution;

namespace PolecatTests.Bugs;

// GH-3290 (Polecat parity with the Marten side): with
// UseWolverineManagedEventSubscriptionDistribution = true, Wolverine replaces Polecat's
// own daemon/coordinator hosting outright, but the store's only knowledge of the daemon
// state was DaemonSettings.AsyncMode, which the integration never set. The integration
// now records DaemonMode.ExternallyManaged (jasperfx#490) — the same runtime posture as
// Disabled (nothing Polecat-hosted starts), but any AsyncMode reader sees that the async
// projections DO run under Wolverine's distribution.
public class Bug_3290_externally_managed_daemon_mode
{
private static IHostBuilder configureHost(bool useWolverineManagedDistribution,
DaemonMode? explicitDaemonMode = null,
bool explicitDaemonBeforeIntegration = false)
{
return Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
var polecat = opts.Services.AddPolecat(m =>
{
m.ConnectionString = Servers.SqlServerConnectionString;
m.DatabaseSchemaName = "bug3290";

m.Projections.Add<TripProjection>(ProjectionLifecycle.Async);
});

if (explicitDaemonMode.HasValue && explicitDaemonBeforeIntegration)
{
polecat.AddAsyncDaemon(explicitDaemonMode.Value);
}

polecat.IntegrateWithWolverine(o =>
{
o.UseWolverineManagedEventSubscriptionDistribution = useWolverineManagedDistribution;
});

if (explicitDaemonMode.HasValue && !explicitDaemonBeforeIntegration)
{
polecat.AddAsyncDaemon(explicitDaemonMode.Value);
}
});
}

private static DocumentStore buildStore(IHost host)
{
return host.Services.GetRequiredService<IDocumentStore>().As<DocumentStore>();
}

[Fact]
public void managed_distribution_records_externally_managed()
{
using var host = configureHost(useWolverineManagedDistribution: true).Build();

var store = buildStore(host);

store.Options.DaemonSettings.AsyncMode.ShouldBe(DaemonMode.ExternallyManaged);
}

[Fact]
public void managed_distribution_starts_nothing_polecat_hosted()
{
using var host = configureHost(useWolverineManagedDistribution: true).Build();

buildStore(host);

// The same runtime posture as Disabled: Polecat's daemon hosted service and its
// coordinator only ever enter the container as hosted services via the user's
// explicit AddAsyncDaemon()/AddProjectionCoordinator() calls, so no registered
// hosted service may be a daemon runner or a projection coordinator of any flavor.
var hostedServices = host.Services.GetServices<IHostedService>().ToArray();
hostedServices.OfType<PolecatDaemonHostedService>().ShouldBeEmpty();
hostedServices.OfType<JasperFx.Events.Daemon.IProjectionCoordinator>().ShouldBeEmpty();
hostedServices.OfType<Wolverine.Polecat.Distribution.IProjectionCoordinator>().ShouldBeEmpty();

// The daemon coordination surface is Wolverine's distribution-backed coordinator,
// registered as a plain singleton — never hosted
host.Services.GetRequiredService<Wolverine.Polecat.Distribution.IProjectionCoordinator>()
.ShouldBeOfType<WolverineProjectionCoordinator>();
}

[Fact]
public void without_managed_distribution_the_mode_stays_disabled()
{
using var host = configureHost(useWolverineManagedDistribution: false).Build();

var store = buildStore(host);

store.Options.DaemonSettings.AsyncMode.ShouldBe(DaemonMode.Disabled);
}

[Fact]
public void explicit_add_async_daemon_choice_after_integration_is_not_overwritten()
{
using var host = configureHost(useWolverineManagedDistribution: true, DaemonMode.Solo).Build();

var store = buildStore(host);

store.Options.DaemonSettings.AsyncMode.ShouldBe(DaemonMode.Solo);
}

[Fact]
public void explicit_add_async_daemon_choice_before_integration_is_not_overwritten()
{
using var host = configureHost(useWolverineManagedDistribution: true, DaemonMode.Solo,
explicitDaemonBeforeIntegration: true).Build();

var store = buildStore(host);

store.Options.DaemonSettings.AsyncMode.ShouldBe(DaemonMode.Solo);
}
}
18 changes: 18 additions & 0 deletions src/Persistence/Wolverine.Marten/MartenIntegration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using JasperFx.Core;
using JasperFx.Core.Reflection;
using JasperFx.Events;
using JasperFx.Events.Daemon;
using Marten;
using Marten.Events;
using Marten.Exceptions;
Expand Down Expand Up @@ -168,6 +169,23 @@ public void Configure(IServiceProvider services, StoreOptions options)
{
options.Events.MessageOutbox = new MartenToWolverineOutbox(services, StoreType);

// GH-3290: when Wolverine manages the event subscription distribution, it replaces
// Marten's AddAsyncDaemon() registration outright — Marten hosts no coordinator and
// starts no agents, but the async projections and subscriptions DO run under
// Wolverine's distribution. Marten's only knowledge of the daemon state is
// Projections.AsyncMode; left at Disabled, DocumentStore writes a misleading
// "The async daemon is disabled ... projections will not be executed" warning at
// startup. Record the real state: ExternallyManaged keeps Marten's runtime posture
// identical to Disabled (no Marten-side coordination starts) while suppressing the
// warning. Only upgrades from Disabled — an explicit user AddAsyncDaemon() choice
// is never overwritten, regardless of call order relative to IntegrateWithWolverine.
var integration = services.GetService<MartenIntegration>();
if (integration is { UseWolverineManagedEventSubscriptionDistribution: true }
&& options.Projections.AsyncMode == DaemonMode.Disabled)
{
options.Projections.AsyncMode = DaemonMode.ExternallyManaged;
}

// Envelope is Wolverine's operational outbox document. Keep it
// single-tenant and unpartitioned regardless of blanket document
// policies the user has applied (AllDocumentsAreMultiTenanted or
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -332,5 +332,18 @@ public void Configure(IServiceProvider services, StoreOptions options)
// Marten ancillary side. Without this the ancillary store silently drops projection-published
// messages.
options.Events.MessageOutbox = new PolecatToWolverineOutbox(services);

// GH-3290: mirror the primary store (PolecatOverrides) — the ancillary managed
// distribution flag lives on the main PolecatIntegration (see the
// IProjectionCoordinator<T> factory registration above), so defer to it here too.
// ExternallyManaged keeps the runtime posture of Disabled (nothing Polecat-hosted
// starts) while recording that the async projections DO run under Wolverine's
// distribution. Only upgrades from Disabled — never overrides an explicit user choice.
var integration = services.GetService<PolecatIntegration>();
if (integration is { UseWolverineManagedEventSubscriptionDistribution: true }
&& options.DaemonSettings.AsyncMode == JasperFx.Events.Daemon.DaemonMode.Disabled)
{
options.DaemonSettings.AsyncMode = JasperFx.Events.Daemon.DaemonMode.ExternallyManaged;
}
}
}
16 changes: 16 additions & 0 deletions src/Persistence/Wolverine.Polecat/PolecatIntegration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using JasperFx.Core;
using JasperFx.Core.Reflection;
using JasperFx.Events;
using JasperFx.Events.Daemon;
using Polecat;
using Microsoft.Extensions.DependencyInjection;
using Wolverine.ErrorHandling;
Expand Down Expand Up @@ -132,6 +133,21 @@ public void Configure(IServiceProvider services, StoreOptions options)
// Wolverine after the projection batch's SQL transaction commits. Mirrors
// the Marten side at MartenIntegration.cs:153. See wolverine#2774.
options.Events.MessageOutbox = new PolecatToWolverineOutbox(services);

// GH-3290 (Polecat parity with the Marten side): when Wolverine manages the event
// subscription distribution, it replaces Polecat's own daemon/coordinator hosting
// outright, but the store's only knowledge of the daemon state is
// DaemonSettings.AsyncMode. Record the real state: ExternallyManaged keeps the
// store's runtime posture identical to Disabled (nothing Polecat-hosted starts)
// while telling any AsyncMode reader that the async projections DO run. Only
// upgrades from Disabled — an explicit user AddAsyncDaemon()/AddProjectionCoordinator()
// choice is never overwritten, regardless of call order relative to IntegrateWithWolverine.
var integration = services.GetService<PolecatIntegration>();
if (integration is { UseWolverineManagedEventSubscriptionDistribution: true }
&& options.DaemonSettings.AsyncMode == DaemonMode.Disabled)
{
options.DaemonSettings.AsyncMode = DaemonMode.ExternallyManaged;
}
}
}

Expand Down
Loading