diff --git a/src/CoreTests/setting_solo_mode_in_test_support.cs b/src/CoreTests/setting_solo_mode_in_test_support.cs new file mode 100644 index 0000000000..f49b265eed --- /dev/null +++ b/src/CoreTests/setting_solo_mode_in_test_support.cs @@ -0,0 +1,47 @@ +using System.Threading.Tasks; +using JasperFx.Events.Daemon; +using Marten; +using Marten.Events.Daemon.Coordination; +using Marten.Testing.Harness; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Shouldly; +using Xunit; + +namespace CoreTests; + +public class setting_solo_mode_in_test_support +{ + [Fact] + public async Task override_every_store_to_use_a_solo_async_daemon() + { + using var host = await Host.CreateDefaultBuilder() + .ConfigureServices(services => + { + // Mostly just to prove we can mix and match + services.AddMarten(ConnectionSource.ConnectionString).AddAsyncDaemon(DaemonMode.HotCold); + + services.AddMartenStore(opts => + { + opts.Connection(ConnectionSource.ConnectionString); + opts.DatabaseSchemaName = "first_store"; + }).AddAsyncDaemon(DaemonMode.HotCold); + + services.AddMartenStore(services => + { + var opts = new StoreOptions(); + opts.Connection(ConnectionSource.ConnectionString); + opts.DatabaseSchemaName = "second_store"; + + return opts; + }).AddAsyncDaemon(DaemonMode.HotCold); + + // Forget what the application says, let's make all the daemons run in solo mode! + services.MartenDaemonModeIsSolo(); + }).StartAsync(); + + host.Services.GetRequiredService().Mode.ShouldBe(DaemonMode.Solo); + host.Services.GetRequiredService>().Mode.ShouldBe(DaemonMode.Solo); + host.Services.GetRequiredService>().Mode.ShouldBe(DaemonMode.Solo); + } +} diff --git a/src/Marten.AspNetCore.Testing/AppFixture.cs b/src/Marten.AspNetCore.Testing/AppFixture.cs index 42fb284fbb..e0e8007996 100644 --- a/src/Marten.AspNetCore.Testing/AppFixture.cs +++ b/src/Marten.AspNetCore.Testing/AppFixture.cs @@ -24,6 +24,14 @@ public async Task InitializeAsync() { b.ConfigureServices((context, services) => { + // Important! You can make your test harness work a little faster (important on its own) + // and probably be more reliable by overriding your Marten configuration to run all + // async daemons in "Solo" mode so they spin up faster and there's no issues from + // PostgreSQL having trouble with advisory locks when projections are rapidly started and stopped + + // This was added in V8.8 + services.MartenDaemonModeIsSolo(); + services.Configure(s => { s.SchemaName = SchemaName; diff --git a/src/Marten.AspNetCore.Testing/Examples/SimplifiedIntegrationContext.cs b/src/Marten.AspNetCore.Testing/Examples/SimplifiedIntegrationContext.cs index 7fd33fd801..e300fe9eca 100644 --- a/src/Marten.AspNetCore.Testing/Examples/SimplifiedIntegrationContext.cs +++ b/src/Marten.AspNetCore.Testing/Examples/SimplifiedIntegrationContext.cs @@ -20,6 +20,11 @@ public async Task InitializeAsync() { // Using Marten, wipe out all data and reset the state await Store.Advanced.ResetAllData(); + + // OR if you use the async daemon in your tests, use this + // instead to do the above, but also cleanly stop all projections, + // reset the data, then start all async projections and subscriptions up again + await Host.ResetAllMartenDataAsync(); } // This is required because of the IAsyncLifetime diff --git a/src/Marten.AspNetCore/Daemon/AsyncDaemonHealthCheckExtensions.cs b/src/Marten.AspNetCore/Daemon/AsyncDaemonHealthCheckExtensions.cs index fb254c3520..9677b5e9b2 100644 --- a/src/Marten.AspNetCore/Daemon/AsyncDaemonHealthCheckExtensions.cs +++ b/src/Marten.AspNetCore/Daemon/AsyncDaemonHealthCheckExtensions.cs @@ -67,7 +67,7 @@ public record AsyncDaemonHealthCheckSettings(int MaxEventLag, TimeSpan? MaxSameL /// /// Tracks projection state across multiple health check invocations /// - internal class ProjectionStateTracker + public class ProjectionStateTracker { public ConcurrentDictionary LastProjectionsChecks { get; } = new(); } diff --git a/src/Marten/Events/Daemon/Coordination/IProjectionCoordinator.cs b/src/Marten/Events/Daemon/Coordination/IProjectionCoordinator.cs index 9fcc03798a..b8aa46a461 100644 --- a/src/Marten/Events/Daemon/Coordination/IProjectionCoordinator.cs +++ b/src/Marten/Events/Daemon/Coordination/IProjectionCoordinator.cs @@ -23,6 +23,8 @@ public interface IProjectionCoordinator : IHostedService /// /// Task ResumeAsync(); + + DaemonMode Mode { get; } } public interface IProjectionCoordinator : IProjectionCoordinator where T : IDocumentStore diff --git a/src/Marten/Events/Daemon/Coordination/ProjectionCoordinator.cs b/src/Marten/Events/Daemon/Coordination/ProjectionCoordinator.cs index 62874d7d9c..6381542f28 100644 --- a/src/Marten/Events/Daemon/Coordination/ProjectionCoordinator.cs +++ b/src/Marten/Events/Daemon/Coordination/ProjectionCoordinator.cs @@ -37,6 +37,8 @@ public ProjectionCoordinator(IDocumentStore documentStore, ILogger(this IHost host) where T : IDocumentStore return host.Services.GetRequiredService(); } + /// + /// Override the main Marten DocumentStore and any registered "ancillary" stores that are using the + /// Async Daemon to run in "Solo" mode for faster and probably more reliable automated testing + /// + /// + /// + public static IServiceCollection MartenDaemonModeIsSolo(this IServiceCollection services) + { + services.AddSingleton(); + return services; + } + + internal class OverrideDaemonModeToSolo: IGlobalConfigureMarten + { + public void Configure(IServiceProvider services, StoreOptions options) + { + if (options.Projections.AsyncMode == DaemonMode.HotCold) + { + options.Projections.AsyncMode = DaemonMode.Solo; + } + } + } + + /// /// Clean off all Marten data in the default DocumentStore for this host /// diff --git a/src/Marten/Internal/SecondaryStoreConfig.cs b/src/Marten/Internal/SecondaryStoreConfig.cs index 756e4ada84..1e3454d7b7 100644 --- a/src/Marten/Internal/SecondaryStoreConfig.cs +++ b/src/Marten/Internal/SecondaryStoreConfig.cs @@ -101,6 +101,12 @@ public StoreOptions BuildStoreOptions(IServiceProvider provider) var configures = provider.GetServices>(); foreach (var configure in configures) configure.Configure(provider, options); + var globals = provider.GetServices().OfType(); + foreach (var configureMarten in globals) + { + configureMarten.Configure(provider, options); + } + options.ReadJasperFxOptions(provider.GetService()); options.StoreName = typeof(T).Name; options.ReadJasperFxOptions(provider.GetService()); diff --git a/src/Marten/MartenServiceCollectionExtensions.cs b/src/Marten/MartenServiceCollectionExtensions.cs index f7447c57dd..d0c27c2932 100644 --- a/src/Marten/MartenServiceCollectionExtensions.cs +++ b/src/Marten/MartenServiceCollectionExtensions.cs @@ -874,6 +874,8 @@ public void Configure(IServiceProvider services, StoreOptions options) } } +public interface IGlobalConfigureMarten: IConfigureMarten; + #region sample_IConfigureMarten ///