From d2ae2b62960170ce5da7d722417f28b8d661f3ec Mon Sep 17 00:00:00 2001 From: "Jeremy D. Miller" Date: Fri, 11 Oct 2024 11:53:09 -0500 Subject: [PATCH] Quick little RunWolverineInSoloMode() helper. Closes GH-1065 --- docs/guide/diagnostics.md | 2 +- docs/guide/durability/marten/distribution.md | 2 +- .../durability/marten/event-forwarding.md | 4 +- docs/guide/durability/sqlserver.md | 4 +- docs/guide/handlers/error-handling.md | 6 +- docs/guide/handlers/sticky.md | 8 +-- docs/guide/messaging/listeners.md | 2 +- docs/guide/messaging/transports/mqtt.md | 4 +- docs/guide/testing.md | 68 +++++++++++++++++++ .../IntegrationContext.cs | 20 +++++- .../override_durability_mode_to_solo.cs | 19 ++++++ src/Wolverine/TestingExtensions.cs | 26 +++++++ 12 files changed, 148 insertions(+), 17 deletions(-) create mode 100644 src/Http/Wolverine.Http.Tests/override_durability_mode_to_solo.cs diff --git a/docs/guide/diagnostics.md b/docs/guide/diagnostics.md index 451caf82c..e3c030ca0 100644 --- a/docs/guide/diagnostics.md +++ b/docs/guide/diagnostics.md @@ -180,6 +180,6 @@ public static void using_preview_subscriptions(IMessageBus bus) } } ``` -snippet source | anchor +snippet source | anchor diff --git a/docs/guide/durability/marten/distribution.md b/docs/guide/durability/marten/distribution.md index 6f96de431..a370d4ab7 100644 --- a/docs/guide/durability/marten/distribution.md +++ b/docs/guide/durability/marten/distribution.md @@ -26,7 +26,7 @@ opts.Services.AddMarten(m => m.UseWolverineManagedEventSubscriptionDistribution = true; }); ``` -snippet source | anchor +snippet source | anchor ::: tip diff --git a/docs/guide/durability/marten/event-forwarding.md b/docs/guide/durability/marten/event-forwarding.md index d83191688..d1774bcbe 100644 --- a/docs/guide/durability/marten/event-forwarding.md +++ b/docs/guide/durability/marten/event-forwarding.md @@ -163,7 +163,7 @@ public async Task execution_of_forwarded_events_can_be_awaited_from_tests() events[1].Data.ShouldBeOfType(); } ``` -snippet source | anchor +snippet source | anchor Where the result contains `FourthEvent` because `SecondEvent` was forwarded as `SecondMessage` and that persisted `FourthEvent` in a handler such as: @@ -178,5 +178,5 @@ public static Task HandleAsync(SecondMessage message, IDocumentSession session) return session.SaveChangesAsync(); } ``` -snippet source | anchor +snippet source | anchor diff --git a/docs/guide/durability/sqlserver.md b/docs/guide/durability/sqlserver.md index 69d2cdf9d..5f0809852 100644 --- a/docs/guide/durability/sqlserver.md +++ b/docs/guide/durability/sqlserver.md @@ -91,7 +91,7 @@ that they are utilizing the transactional inbox and outbox. The Sql Server queue ```cs opts.ListenToSqlServerQueue("sender").BufferedInMemory(); ``` -snippet source | anchor +snippet source | anchor Using this option just means that the Sql Server queues can be used for both sending or receiving with no integration @@ -142,7 +142,7 @@ _listener = await Host.CreateDefaultBuilder() .IncludeType(); }).StartAsync(); ``` -snippet source | anchor +snippet source | anchor ## Lightweight Saga Usage diff --git a/docs/guide/handlers/error-handling.md b/docs/guide/handlers/error-handling.md index 69473bdb7..8f336630c 100644 --- a/docs/guide/handlers/error-handling.md +++ b/docs/guide/handlers/error-handling.md @@ -318,7 +318,7 @@ theReceiver = await Host.CreateDefaultBuilder() }); }).StartAsync(); ``` -snippet source | anchor +snippet source | anchor Optionally, you can implement a new type to handle this same custom logic by @@ -345,7 +345,7 @@ public class ShippingOrderFailurePolicy : UserDefinedContinuation } } ``` -snippet source | anchor +snippet source | anchor and register that secondary action like this: @@ -363,7 +363,7 @@ theReceiver = await Host.CreateDefaultBuilder() .Discard().And(); }).StartAsync(); ``` -snippet source | anchor +snippet source | anchor diff --git a/docs/guide/handlers/sticky.md b/docs/guide/handlers/sticky.md index 9faba34ed..5eac186d2 100644 --- a/docs/guide/handlers/sticky.md +++ b/docs/guide/handlers/sticky.md @@ -25,7 +25,7 @@ message as an input. ```cs public class StickyMessage; ``` -snippet source | anchor +snippet source | anchor And we're going to handle that `StickyMessage` message separately with two different handler types: @@ -51,7 +51,7 @@ public static class GreenStickyHandler } } ``` -snippet source | anchor +snippet source | anchor ::: tip @@ -79,7 +79,7 @@ using var host = await Host.CreateDefaultBuilder() opts.ListenAtPort(4000).Named("blue"); }).StartAsync(); ``` -snippet source | anchor +snippet source | anchor With all of that being said, the end result of the two `StickyMessage` handlers that are marked with `[StickyHandler]` @@ -119,7 +119,7 @@ using var host = await Host.CreateDefaultBuilder() }).StartAsync(); ``` -snippet source | anchor +snippet source | anchor diff --git a/docs/guide/messaging/listeners.md b/docs/guide/messaging/listeners.md index cb74c5ecc..92d90a8de 100644 --- a/docs/guide/messaging/listeners.md +++ b/docs/guide/messaging/listeners.md @@ -153,7 +153,7 @@ var host = await Host.CreateDefaultBuilder().UseWolverine(opts => .ListenWithStrictOrdering(); }).StartAsync(); ``` -snippet source | anchor +snippet source | anchor This option does a couple things: diff --git a/docs/guide/messaging/transports/mqtt.md b/docs/guide/messaging/transports/mqtt.md index 1e356ca8d..0a4ef25d9 100644 --- a/docs/guide/messaging/transports/mqtt.md +++ b/docs/guide/messaging/transports/mqtt.md @@ -211,7 +211,7 @@ _receiver = await Host.CreateDefaultBuilder() opts.ListenToMqttTopic("incoming/#").RetainMessages(); }).StartAsync(); ``` -snippet source | anchor +snippet source | anchor In the case of receiving any message that matches the topic filter *according to the [MQTT topic filter rules](https://cedalo.com/blog/mqtt-topics-and-mqtt-wildcards-explained/)*, that message @@ -358,7 +358,7 @@ public static ClearMqttTopic Handle(TriggerZero message) return new ClearMqttTopic("red"); } ``` -snippet source | anchor +snippet source | anchor diff --git a/docs/guide/testing.md b/docs/guide/testing.md index 9fc2729a6..4ad26db3c 100644 --- a/docs/guide/testing.md +++ b/docs/guide/testing.md @@ -586,3 +586,71 @@ public class When_message_is_sent : IAsyncLifetime As you can see, we just have to start our application, attach a tracked session to it, and then wait for the message to be published. This way, we can test the whole process of the application, from the file change to the message publication, in a single test. + +## Running Wolverine in "Solo" Mode + +Wolverine's [leadership election](/guide/durability/leadership-and-troubleshooting.html#troubleshooting-and-leadership-election) process is necessary for distributing several background tasks in real life production, +but that subsystem can lead to some inconvenient sluggishness in [cold start times](https://dontpaniclabs.com/blog/post/2022/09/20/net-cold-starts/#:~:text=In%20software%20development%2C%20cold%20starts,have%20an%20increased%20start%20time.) in automation testing. + +To sidestep that problem, you can direct Wolverine to run in "Solo" mode where the current process assumes that it's the +only running node and automatically starts up all known background tasks immediately. + +To do so, you could do something like this in your main `Program` file: + + + +```cs +var builder = Host.CreateApplicationBuilder(); + +builder.UseWolverine(opts => +{ + opts.Services.AddMarten("some connection string") + + // This adds quite a bit of middleware for + // Marten + .IntegrateWithWolverine(); + + // You want this maybe! + opts.Policies.AutoApplyTransactions(); + + if (builder.Environment.IsDevelopment()) + { + // But wait! Optimize Wolverine for usage as + // if there would never be more than one node running + opts.Durability.Mode = DurabilityMode.Solo; + } +}); + +using var host = builder.Build(); +await host.StartAsync(); +``` +snippet source | anchor + + +Or if you're using something like [WebHostFactory](https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-8.0) to +bootstrap your Wolverine application in an integration testing harness, you can use this helper to override Wolverine into being +"Solo": + + + +```cs +// This is bootstrapping the actual application using +// its implied Program.Main() set up +// For non-Alba users, this is using IWebHostBuilder +Host = await AlbaHost.For(x => +{ + x.ConfigureServices(services => + { + // Override the Wolverine configuration in the application + // to run the application in "solo" mode for faster + // testing cold starts + services.RunWolverineInSoloMode(); + + // And just for completion, disable all Wolverine external + // messaging transports + services.DisableAllExternalWolverineTransports(); + }); +}); +``` +snippet source | anchor + diff --git a/src/Http/Wolverine.Http.Tests/IntegrationContext.cs b/src/Http/Wolverine.Http.Tests/IntegrationContext.cs index a218ace89..77272355f 100644 --- a/src/Http/Wolverine.Http.Tests/IntegrationContext.cs +++ b/src/Http/Wolverine.Http.Tests/IntegrationContext.cs @@ -24,9 +24,27 @@ public async Task InitializeAsync() // use WebApplicationFactory and/or Alba for integration testing OaktonEnvironment.AutoStartHost = true; + #region sample_using_run_wolverine_in_solo_mode_with_extension + // This is bootstrapping the actual application using // its implied Program.Main() set up - Host = await AlbaHost.For(x => { }); + // For non-Alba users, this is using IWebHostBuilder + Host = await AlbaHost.For(x => + { + x.ConfigureServices(services => + { + // Override the Wolverine configuration in the application + // to run the application in "solo" mode for faster + // testing cold starts + services.RunWolverineInSoloMode(); + + // And just for completion, disable all Wolverine external + // messaging transports + services.DisableAllExternalWolverineTransports(); + }); + }); + + #endregion } public Task DisposeAsync() diff --git a/src/Http/Wolverine.Http.Tests/override_durability_mode_to_solo.cs b/src/Http/Wolverine.Http.Tests/override_durability_mode_to_solo.cs new file mode 100644 index 000000000..69c239e9e --- /dev/null +++ b/src/Http/Wolverine.Http.Tests/override_durability_mode_to_solo.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Wolverine.Runtime; + +namespace Wolverine.Http.Tests; + +public class override_durability_mode_to_solo : IntegrationContext +{ + public override_durability_mode_to_solo(AppFixture fixture) : base(fixture) + { + } + + [Fact] + public void verify_that_testing_helper_works() + { + Host.Services.GetRequiredService() + .Options.Durability.Mode.ShouldBe(DurabilityMode.Solo); + } +} \ No newline at end of file diff --git a/src/Wolverine/TestingExtensions.cs b/src/Wolverine/TestingExtensions.cs index 80e9b763b..990c4dc05 100644 --- a/src/Wolverine/TestingExtensions.cs +++ b/src/Wolverine/TestingExtensions.cs @@ -1,6 +1,7 @@ using System.Text; using JasperFx.Core; using JasperFx.Core.Reflection; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Wolverine.Runtime; using Wolverine.Tracking; @@ -366,4 +367,29 @@ public bool HasReached() return true; } } + + /// + /// Just overrides the Wolverine configuration to run in "solo" mode + /// that is advantageous in testing because the Wolverine application can + /// start up faster + /// + /// + /// + public static IServiceCollection RunWolverineInSoloMode(this IServiceCollection services) + { + return services.AddSingleton(); + } +} + +/// +/// Just overrides the Wolverine configuration to run in "solo" mode +/// that is advantageous in testing because the Wolverine application can +/// start up faster +/// +internal class RunWolverineInSoloMode : IWolverineExtension +{ + public void Configure(WolverineOptions options) + { + options.Durability.Mode = DurabilityMode.Solo; + } } \ No newline at end of file