diff --git a/Directory.Packages.props b/Directory.Packages.props
index f20df00cc..7d0e1a1e4 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -35,8 +35,8 @@
-
-
+
+
diff --git a/src/Persistence/MartenTests/Bugs/Bug_3290_misleading_daemon_disabled_warning.cs b/src/Persistence/MartenTests/Bugs/Bug_3290_misleading_daemon_disabled_warning.cs
new file mode 100644
index 000000000..4c32ec550
--- /dev/null
+++ b/src/Persistence/MartenTests/Bugs/Bug_3290_misleading_daemon_disabled_warning.cs
@@ -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(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().As();
+ 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()
+ .OfType()
+ .ShouldBeEmpty();
+
+ // Marten's coordinator interface resolves to Wolverine's distribution-backed
+ // coordinator (a plain singleton, never hosted), not Marten's ProjectionCoordinator
+ host.Services.GetRequiredService()
+ .ShouldBeOfType();
+ }
+
+ [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);
+ }
+}
diff --git a/src/Persistence/PolecatTests/Bugs/Bug_3290_externally_managed_daemon_mode.cs b/src/Persistence/PolecatTests/Bugs/Bug_3290_externally_managed_daemon_mode.cs
new file mode 100644
index 000000000..7112630a2
--- /dev/null
+++ b/src/Persistence/PolecatTests/Bugs/Bug_3290_externally_managed_daemon_mode.cs
@@ -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(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().As();
+ }
+
+ [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().ToArray();
+ hostedServices.OfType().ShouldBeEmpty();
+ hostedServices.OfType().ShouldBeEmpty();
+ hostedServices.OfType().ShouldBeEmpty();
+
+ // The daemon coordination surface is Wolverine's distribution-backed coordinator,
+ // registered as a plain singleton — never hosted
+ host.Services.GetRequiredService()
+ .ShouldBeOfType();
+ }
+
+ [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);
+ }
+}
diff --git a/src/Persistence/Wolverine.Marten/MartenIntegration.cs b/src/Persistence/Wolverine.Marten/MartenIntegration.cs
index 67c6ee106..4f2f3978b 100644
--- a/src/Persistence/Wolverine.Marten/MartenIntegration.cs
+++ b/src/Persistence/Wolverine.Marten/MartenIntegration.cs
@@ -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;
@@ -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();
+ 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
diff --git a/src/Persistence/Wolverine.Polecat/AncillaryWolverineOptionsPolecatExtensions.cs b/src/Persistence/Wolverine.Polecat/AncillaryWolverineOptionsPolecatExtensions.cs
index f861da515..9cc7b62a2 100644
--- a/src/Persistence/Wolverine.Polecat/AncillaryWolverineOptionsPolecatExtensions.cs
+++ b/src/Persistence/Wolverine.Polecat/AncillaryWolverineOptionsPolecatExtensions.cs
@@ -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 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();
+ if (integration is { UseWolverineManagedEventSubscriptionDistribution: true }
+ && options.DaemonSettings.AsyncMode == JasperFx.Events.Daemon.DaemonMode.Disabled)
+ {
+ options.DaemonSettings.AsyncMode = JasperFx.Events.Daemon.DaemonMode.ExternallyManaged;
+ }
}
}
diff --git a/src/Persistence/Wolverine.Polecat/PolecatIntegration.cs b/src/Persistence/Wolverine.Polecat/PolecatIntegration.cs
index bc5ecaa6a..dfc4f00b5 100644
--- a/src/Persistence/Wolverine.Polecat/PolecatIntegration.cs
+++ b/src/Persistence/Wolverine.Polecat/PolecatIntegration.cs
@@ -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;
@@ -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();
+ if (integration is { UseWolverineManagedEventSubscriptionDistribution: true }
+ && options.DaemonSettings.AsyncMode == DaemonMode.Disabled)
+ {
+ options.DaemonSettings.AsyncMode = DaemonMode.ExternallyManaged;
+ }
}
}