diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..d73637c --- /dev/null +++ b/src/.gitignore @@ -0,0 +1 @@ +/binaries/**/* \ No newline at end of file diff --git a/src/Billing/.editorconfig b/src/Billing/.editorconfig new file mode 100644 index 0000000..7333907 --- /dev/null +++ b/src/Billing/.editorconfig @@ -0,0 +1,7 @@ +[*.cs] + +# Justification: Test project +dotnet_diagnostic.CA2007.severity = none + +# Justification: Tests don't support cancellation and don't need to forward IMessageHandlerContext.CancellationToken +dotnet_diagnostic.NSB0002.severity = suggestion \ No newline at end of file diff --git a/src/Billing/Billing.csproj b/src/Billing/Billing.csproj new file mode 100644 index 0000000..d890dc6 --- /dev/null +++ b/src/Billing/Billing.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + Exe + enable + enable + ..\binaries\Billing\ + failures.ico + + + + + + + + + + + \ No newline at end of file diff --git a/src/Billing/OrderPlacedHandler.cs b/src/Billing/OrderPlacedHandler.cs new file mode 100644 index 0000000..0ec8f80 --- /dev/null +++ b/src/Billing/OrderPlacedHandler.cs @@ -0,0 +1,20 @@ +namespace Billing; + +using System.Threading.Tasks; +using MassTransit; +using Messages; + +public class OrderPlacedHandler(SimulationEffects simulationEffects) : IConsumer +{ + public async Task Consume(ConsumeContext context) + { + await simulationEffects.SimulatedMessageProcessing(context.CancellationToken); + + var orderBilled = new OrderBilled + { + OrderId = context.Message.OrderId + }; + + await context.Publish(orderBilled); + } +} \ No newline at end of file diff --git a/src/Billing/Program.cs b/src/Billing/Program.cs new file mode 100644 index 0000000..1ed13d8 --- /dev/null +++ b/src/Billing/Program.cs @@ -0,0 +1,86 @@ +#pragma warning disable IDE0010 +namespace Billing; + +using Microsoft.Extensions.Hosting; +using MassTransit; +using Microsoft.Extensions.DependencyInjection; +using System.Reflection; + +class Program +{ + public static IHostBuilder CreateHostBuilder(string[] args) + { + var host = Host.CreateDefaultBuilder(args) + .ConfigureServices((hostContext, services) => + { + services.AddMassTransit(x => + { + x.UsingRabbitMq((context, cfg) => + { + cfg.Host("localhost", "/", h => + { + h.Username("guest"); + h.Password("guest"); + }); + + cfg.ConfigureEndpoints(context); + }); + + x.AddConfigureEndpointsCallback((name, cfg) => + { + if (cfg is IRabbitMqReceiveEndpointConfigurator rmq) + { + rmq.SetQuorumQueue(); + } + }); + + x.AddConsumers(Assembly.GetExecutingAssembly()); + }); + + services.AddSingleton(); + }); + + return host; + } + + static async Task Main(string[] args) + { + Console.Title = "Failure rate (Billing)"; + Console.SetWindowSize(65, 15); + + var host = CreateHostBuilder(args).Build(); + await host.StartAsync(); + + var state = host.Services.GetRequiredService(); + await RunUserInterfaceLoop(state); + } + + static Task RunUserInterfaceLoop(SimulationEffects state) + { + while (true) + { + Console.Clear(); + Console.WriteLine("Billing Endpoint"); + Console.WriteLine("Press F to increase the simulated failure rate"); + Console.WriteLine("Press S to decrease the simulated failure rate"); + Console.WriteLine("Press ESC to quit"); + Console.WriteLine(); + + state.WriteState(Console.Out); + + var input = Console.ReadKey(true); + + switch (input.Key) + { + case ConsoleKey.F: + state.IncreaseFailureRate(); + break; + case ConsoleKey.S: + state.DecreaseFailureRate(); + break; + case ConsoleKey.Escape: + return Task.CompletedTask; + } + } + } +} \ No newline at end of file diff --git a/src/Billing/SimulationEffects.cs b/src/Billing/SimulationEffects.cs new file mode 100644 index 0000000..6324d32 --- /dev/null +++ b/src/Billing/SimulationEffects.cs @@ -0,0 +1,23 @@ +namespace Billing; + +public class SimulationEffects +{ + public void IncreaseFailureRate() => failureRate = Math.Min(1, failureRate + FailureRateIncrement); + + public void DecreaseFailureRate() => failureRate = Math.Max(0, failureRate - FailureRateIncrement); + + public void WriteState(TextWriter output) => output.WriteLine("Failure rate: {0:P0}", failureRate); + + public async Task SimulatedMessageProcessing(CancellationToken cancellationToken = default) + { + await Task.Delay(200, cancellationToken); + + if (Random.Shared.NextDouble() < failureRate) + { + throw new Exception("BOOM! A failure occurred"); + } + } + + double failureRate; + const double FailureRateIncrement = 0.1; +} \ No newline at end of file diff --git a/src/Billing/failures.ico b/src/Billing/failures.ico new file mode 100644 index 0000000..d302303 Binary files /dev/null and b/src/Billing/failures.ico differ diff --git a/src/ClientUI/.editorconfig b/src/ClientUI/.editorconfig new file mode 100644 index 0000000..7333907 --- /dev/null +++ b/src/ClientUI/.editorconfig @@ -0,0 +1,7 @@ +[*.cs] + +# Justification: Test project +dotnet_diagnostic.CA2007.severity = none + +# Justification: Tests don't support cancellation and don't need to forward IMessageHandlerContext.CancellationToken +dotnet_diagnostic.NSB0002.severity = suggestion \ No newline at end of file diff --git a/src/ClientUI/ClientUI.csproj b/src/ClientUI/ClientUI.csproj new file mode 100644 index 0000000..923e7dc --- /dev/null +++ b/src/ClientUI/ClientUI.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + Exe + enable + enable + ..\binaries\ClientUI\ + traffic.ico + + + + + + + + + + + \ No newline at end of file diff --git a/src/ClientUI/Program.cs b/src/ClientUI/Program.cs new file mode 100644 index 0000000..e2e6602 --- /dev/null +++ b/src/ClientUI/Program.cs @@ -0,0 +1,84 @@ +#pragma warning disable IDE0010 +namespace ClientUI; + +using Microsoft.Extensions.Hosting; +using MassTransit; +using Microsoft.Extensions.DependencyInjection; +using System.Reflection; + +class Program +{ + public static IHostBuilder CreateHostBuilder(string[] args) + { + var host = Host.CreateDefaultBuilder(args) + .ConfigureServices((_, services) => + { + services.AddMassTransit(x => + { + x.UsingRabbitMq((context, cfg) => + { + cfg.Host("localhost", "/", h => + { + h.Username("guest"); + h.Password("guest"); + }); + + cfg.ConfigureEndpoints(context); + }); + + x.AddConfigureEndpointsCallback((name, cfg) => + { + if (cfg is IRabbitMqReceiveEndpointConfigurator rmq) + { + rmq.SetQuorumQueue(); + } + }); + + x.AddConsumers(Assembly.GetExecutingAssembly()); + }); + + services.AddSingleton(); + services.AddHostedService(p => p.GetRequiredService()); + }); + + return host; + } + + static async Task Main(string[] args) + { + Console.Title = "Load (ClientUI)"; + Console.SetWindowSize(65, 15); + + var host = CreateHostBuilder(args).Build(); + await host.StartAsync(); + + var customers = host.Services.GetRequiredService(); + + await RunUserInterfaceLoop(customers); + } + + static Task RunUserInterfaceLoop(SimulatedCustomers simulatedCustomers) + { + while (true) + { + Console.Clear(); + Console.WriteLine("Simulating customers placing orders on a website"); + Console.WriteLine("Press T to toggle High/Low traffic mode"); + Console.WriteLine("Press ESC to quit"); + Console.WriteLine(); + + simulatedCustomers.WriteState(Console.Out); + + var input = Console.ReadKey(true); + + switch (input.Key) + { + case ConsoleKey.T: + simulatedCustomers.ToggleTrafficMode(); + break; + case ConsoleKey.Escape: + return Task.CompletedTask; + } + } + } +} \ No newline at end of file diff --git a/src/ClientUI/SimulatedCustomers.cs b/src/ClientUI/SimulatedCustomers.cs new file mode 100644 index 0000000..d64ec7b --- /dev/null +++ b/src/ClientUI/SimulatedCustomers.cs @@ -0,0 +1,74 @@ +namespace ClientUI; + +using MassTransit; +using Messages; +using Microsoft.Extensions.Hosting; + +class SimulatedCustomers(IBus _bus) : BackgroundService +{ + public void WriteState(TextWriter output) + { + var trafficMode = highTrafficMode ? "High" : "Low"; + output.WriteLine($"{trafficMode} traffic mode - sending {rate} orders / second"); + } + + public void ToggleTrafficMode() + { + highTrafficMode = !highTrafficMode; + rate = highTrafficMode ? HightTrafficRate : LowTrafficRate; + } + + Task PlaceSingleOrder(CancellationToken cancellationToken) + { + var placeOrderCommand = new PlaceOrder + { + OrderId = Guid.NewGuid().ToString() + }; + + return _bus.Publish(placeOrderCommand, cancellationToken); + } + + protected override async Task ExecuteAsync(CancellationToken cancellationToken) + { + nextReset = DateTime.UtcNow.AddSeconds(1); + currentIntervalCount = 0; + + while (!cancellationToken.IsCancellationRequested) + { + var now = DateTime.UtcNow; + if (now > nextReset) + { + currentIntervalCount = 0; + nextReset = now.AddSeconds(1); + } + + await PlaceSingleOrder(cancellationToken); + currentIntervalCount++; + + try + { + if (currentIntervalCount >= rate) + { + var delay = nextReset - DateTime.UtcNow; + if (delay > TimeSpan.Zero) + { + await Task.Delay(delay, cancellationToken); + } + } + } + catch (TaskCanceledException) + { + break; + } + } + } + + bool highTrafficMode; + + DateTime nextReset; + int currentIntervalCount; + int rate = LowTrafficRate; + + const int HightTrafficRate = 8; + const int LowTrafficRate = 1; +} \ No newline at end of file diff --git a/src/ClientUI/traffic.ico b/src/ClientUI/traffic.ico new file mode 100644 index 0000000..d5dc024 Binary files /dev/null and b/src/ClientUI/traffic.ico differ diff --git a/src/MassTransitShowcaseDemo.sln b/src/MassTransitShowcaseDemo.sln new file mode 100644 index 0000000..9dd0615 --- /dev/null +++ b/src/MassTransitShowcaseDemo.sln @@ -0,0 +1,53 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34902.65 +MinimumVisualStudioVersion = 15.0.26730.12 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7D29C905-CE3A-4D93-8271-7BA09CEE1631}" + ProjectSection(SolutionItems) = preProject + Directory.Build.props = Directory.Build.props + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ClientUI", "ClientUI\ClientUI.csproj", "{918001C1-B9F6-4E81-894B-128271E8D910}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Messages", "Messages\Messages.csproj", "{CFF586B0-0FA1-4F3C-B860-44BE86B0F341}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sales", "Sales\Sales.csproj", "{6AD27F13-8B6B-4851-BBB8-A93D7FE463D9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Billing", "Billing\Billing.csproj", "{709E5DF7-B76F-4FA6-BCB3-EF0C43C51FC6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shipping", "Shipping\Shipping.csproj", "{457FCA71-C1D9-43FF-838A-825A47E9E112}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {918001C1-B9F6-4E81-894B-128271E8D910}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {918001C1-B9F6-4E81-894B-128271E8D910}.Debug|Any CPU.Build.0 = Debug|Any CPU + {918001C1-B9F6-4E81-894B-128271E8D910}.Release|Any CPU.ActiveCfg = Release|Any CPU + {918001C1-B9F6-4E81-894B-128271E8D910}.Release|Any CPU.Build.0 = Release|Any CPU + {CFF586B0-0FA1-4F3C-B860-44BE86B0F341}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CFF586B0-0FA1-4F3C-B860-44BE86B0F341}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CFF586B0-0FA1-4F3C-B860-44BE86B0F341}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CFF586B0-0FA1-4F3C-B860-44BE86B0F341}.Release|Any CPU.Build.0 = Release|Any CPU + {6AD27F13-8B6B-4851-BBB8-A93D7FE463D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6AD27F13-8B6B-4851-BBB8-A93D7FE463D9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6AD27F13-8B6B-4851-BBB8-A93D7FE463D9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6AD27F13-8B6B-4851-BBB8-A93D7FE463D9}.Release|Any CPU.Build.0 = Release|Any CPU + {709E5DF7-B76F-4FA6-BCB3-EF0C43C51FC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {709E5DF7-B76F-4FA6-BCB3-EF0C43C51FC6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {709E5DF7-B76F-4FA6-BCB3-EF0C43C51FC6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {709E5DF7-B76F-4FA6-BCB3-EF0C43C51FC6}.Release|Any CPU.Build.0 = Release|Any CPU + {457FCA71-C1D9-43FF-838A-825A47E9E112}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {457FCA71-C1D9-43FF-838A-825A47E9E112}.Debug|Any CPU.Build.0 = Debug|Any CPU + {457FCA71-C1D9-43FF-838A-825A47E9E112}.Release|Any CPU.ActiveCfg = Release|Any CPU + {457FCA71-C1D9-43FF-838A-825A47E9E112}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {081FC59E-04F4-4FB2-88A6-64A7C18BAA10} + EndGlobalSection +EndGlobal diff --git a/src/Messages/.editorconfig b/src/Messages/.editorconfig new file mode 100644 index 0000000..7333907 --- /dev/null +++ b/src/Messages/.editorconfig @@ -0,0 +1,7 @@ +[*.cs] + +# Justification: Test project +dotnet_diagnostic.CA2007.severity = none + +# Justification: Tests don't support cancellation and don't need to forward IMessageHandlerContext.CancellationToken +dotnet_diagnostic.NSB0002.severity = suggestion \ No newline at end of file diff --git a/src/Messages/Messages.csproj b/src/Messages/Messages.csproj new file mode 100644 index 0000000..fa71b7a --- /dev/null +++ b/src/Messages/Messages.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/src/Messages/OrderBilled.cs b/src/Messages/OrderBilled.cs new file mode 100644 index 0000000..87c9759 --- /dev/null +++ b/src/Messages/OrderBilled.cs @@ -0,0 +1,6 @@ +namespace Messages; + +public class OrderBilled +{ + public string? OrderId { get; set; } +} \ No newline at end of file diff --git a/src/Messages/OrderPlaced.cs b/src/Messages/OrderPlaced.cs new file mode 100644 index 0000000..413151c --- /dev/null +++ b/src/Messages/OrderPlaced.cs @@ -0,0 +1,6 @@ +namespace Messages; + +public class OrderPlaced +{ + public string? OrderId { get; set; } +} \ No newline at end of file diff --git a/src/Messages/PlaceOrder.cs b/src/Messages/PlaceOrder.cs new file mode 100644 index 0000000..b5988cd --- /dev/null +++ b/src/Messages/PlaceOrder.cs @@ -0,0 +1,6 @@ +namespace Messages; + +public class PlaceOrder +{ + public string? OrderId { get; set; } +} \ No newline at end of file diff --git a/src/Sales/.editorconfig b/src/Sales/.editorconfig new file mode 100644 index 0000000..7333907 --- /dev/null +++ b/src/Sales/.editorconfig @@ -0,0 +1,7 @@ +[*.cs] + +# Justification: Test project +dotnet_diagnostic.CA2007.severity = none + +# Justification: Tests don't support cancellation and don't need to forward IMessageHandlerContext.CancellationToken +dotnet_diagnostic.NSB0002.severity = suggestion \ No newline at end of file diff --git a/src/Sales/PlaceOrderHandler.cs b/src/Sales/PlaceOrderHandler.cs new file mode 100644 index 0000000..b262b41 --- /dev/null +++ b/src/Sales/PlaceOrderHandler.cs @@ -0,0 +1,21 @@ +namespace Sales; + +using Messages; +using MassTransit; +using System.Threading.Tasks; + +public class PlaceOrderHandler(SimulationEffects simulationEffects) : IConsumer +{ + public async Task Consume(ConsumeContext context) + { + // Simulate the time taken to process a message + await simulationEffects.SimulateMessageProcessing(context.CancellationToken); + + var orderPlaced = new OrderPlaced + { + OrderId = context.Message.OrderId + }; + + await context.Publish(orderPlaced); + } +} \ No newline at end of file diff --git a/src/Sales/Program.cs b/src/Sales/Program.cs new file mode 100644 index 0000000..5d70d82 --- /dev/null +++ b/src/Sales/Program.cs @@ -0,0 +1,89 @@ +#pragma warning disable IDE0010 +namespace Sales; + +using Microsoft.Extensions.Hosting; +using MassTransit; +using Microsoft.Extensions.DependencyInjection; +using System.Security.Cryptography; +using System.Text; +using System.Reflection; + +class Program +{ + public static IHostBuilder CreateHostBuilder(string[] args) + { + var host = Host.CreateDefaultBuilder(args) + .ConfigureServices((hostContext, services) => + { + services.AddMassTransit(x => + { + x.UsingRabbitMq((context, cfg) => + { + cfg.Host("localhost", "/", h => + { + h.Username("guest"); + h.Password("guest"); + }); + + cfg.ConfigureEndpoints(context); + }); + + x.AddConsumers(Assembly.GetExecutingAssembly()); + + x.AddConfigureEndpointsCallback((name, cfg) => + { + if (cfg is IRabbitMqReceiveEndpointConfigurator rmq) + { + rmq.SetQuorumQueue(); + } + }); + }); + + services.AddSingleton(); + }); + + return host; + } + + static async Task Main(string[] args) + { + Console.SetWindowSize(65, 15); + Console.Title = "Processing (Sales)"; + + var host = CreateHostBuilder(args).Build(); + await host.StartAsync(); + + var state = host.Services.GetRequiredService(); + await RunUserInterfaceLoop(state); + } + + static Task RunUserInterfaceLoop(SimulationEffects state) + { + while (true) + { + Console.Clear(); + Console.WriteLine($"Sales Endpoint"); + Console.WriteLine("Press F to process messages faster"); + Console.WriteLine("Press S to process messages slower"); + + Console.WriteLine("Press ESC to quit"); + Console.WriteLine(); + + state.WriteState(Console.Out); + + var input = Console.ReadKey(true); + + switch (input.Key) + { + case ConsoleKey.F: + state.ProcessMessagesFaster(); + break; + case ConsoleKey.S: + state.ProcessMessagesSlower(); + break; + case ConsoleKey.Escape: + return Task.CompletedTask; + } + } + } +} \ No newline at end of file diff --git a/src/Sales/Sales.csproj b/src/Sales/Sales.csproj new file mode 100644 index 0000000..e9da9a2 --- /dev/null +++ b/src/Sales/Sales.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + Exe + enable + enable + ..\binaries\Sales\ + processing-time-alternate.ico + + + + + + + + + + + \ No newline at end of file diff --git a/src/Sales/SimulationEffects.cs b/src/Sales/SimulationEffects.cs new file mode 100644 index 0000000..4dfea0c --- /dev/null +++ b/src/Sales/SimulationEffects.cs @@ -0,0 +1,30 @@ +namespace Sales; + +public class SimulationEffects +{ + public void WriteState(TextWriter output) + { + output.WriteLine("Base time to handle each order: {0} seconds", baseProcessingTime.TotalSeconds); + } + + public Task SimulateMessageProcessing(CancellationToken cancellationToken = default) + { + return Task.Delay(baseProcessingTime, cancellationToken); + } + + public void ProcessMessagesFaster() + { + if (baseProcessingTime > TimeSpan.Zero) + { + baseProcessingTime -= increment; + } + } + + public void ProcessMessagesSlower() + { + baseProcessingTime += increment; + } + + TimeSpan baseProcessingTime = TimeSpan.FromMilliseconds(1300); + TimeSpan increment = TimeSpan.FromMilliseconds(100); +} \ No newline at end of file diff --git a/src/Sales/processing-time-alternate.ico b/src/Sales/processing-time-alternate.ico new file mode 100644 index 0000000..776266f Binary files /dev/null and b/src/Sales/processing-time-alternate.ico differ diff --git a/src/Shipping/.editorconfig b/src/Shipping/.editorconfig new file mode 100644 index 0000000..7333907 --- /dev/null +++ b/src/Shipping/.editorconfig @@ -0,0 +1,7 @@ +[*.cs] + +# Justification: Test project +dotnet_diagnostic.CA2007.severity = none + +# Justification: Tests don't support cancellation and don't need to forward IMessageHandlerContext.CancellationToken +dotnet_diagnostic.NSB0002.severity = suggestion \ No newline at end of file diff --git a/src/Shipping/OrderBilledHandler.cs b/src/Shipping/OrderBilledHandler.cs new file mode 100644 index 0000000..09ba7d9 --- /dev/null +++ b/src/Shipping/OrderBilledHandler.cs @@ -0,0 +1,13 @@ +namespace Shipping; + +using System.Threading.Tasks; +using MassTransit; +using Messages; + +public class OrderBilledHandler(SimulationEffects simulationEffects) : IConsumer +{ + public Task Consume(ConsumeContext context) + { + return simulationEffects.SimulateOrderBilledMessageProcessing(context.CancellationToken); + } +} \ No newline at end of file diff --git a/src/Shipping/OrderPlacedHandler.cs b/src/Shipping/OrderPlacedHandler.cs new file mode 100644 index 0000000..53e06b7 --- /dev/null +++ b/src/Shipping/OrderPlacedHandler.cs @@ -0,0 +1,13 @@ +namespace Shipping; + +using System.Threading.Tasks; +using MassTransit; +using Messages; + +public class OrderPlacedHandler(SimulationEffects simulationEffects) : IConsumer +{ + public Task Consume(ConsumeContext context) + { + return simulationEffects.SimulateOrderPlacedMessageProcessing(context.CancellationToken); + } +} \ No newline at end of file diff --git a/src/Shipping/Program.cs b/src/Shipping/Program.cs new file mode 100644 index 0000000..14ccc4b --- /dev/null +++ b/src/Shipping/Program.cs @@ -0,0 +1,93 @@ +#pragma warning disable IDE0010 +namespace Shipping; + +using Microsoft.Extensions.Hosting; +using MassTransit; +using Microsoft.Extensions.DependencyInjection; +using System.Security.Cryptography; +using System.Text; +using System.Reflection; + +class Program +{ + public static IHostBuilder CreateHostBuilder(string[] args) + { + var host = Host.CreateDefaultBuilder(args) + .ConfigureServices((_, services) => + { + services.AddMassTransit(x => + { + x.UsingRabbitMq((context, cfg) => + { + cfg.Host("localhost", "/", h => + { + h.Username("guest"); + h.Password("guest"); + }); + + cfg.ConfigureEndpoints(context); + }); + + x.AddConsumers(Assembly.GetExecutingAssembly()); + + x.AddConfigureEndpointsCallback((name, cfg) => + { + if (cfg is IRabbitMqReceiveEndpointConfigurator rmq) + { + rmq.SetQuorumQueue(); + } + }); + }); + + services.AddSingleton(); + }); + + return host; + } + + static async Task Main(string[] args) + { + Console.SetWindowSize(65, 15); + + Console.Title = "Processing (Shipping)"; + + var host = CreateHostBuilder(args).Build(); + await host.StartAsync(); + + var state = host.Services.GetRequiredService(); + await RunUserInterfaceLoop(state); + } + + static Task RunUserInterfaceLoop(SimulationEffects state) + { + while (true) + { + Console.Clear(); + Console.WriteLine("Shipping Endpoint"); + Console.WriteLine("Press D to toggle resource degradation simulation"); + Console.WriteLine("Press F to process OrderBilled events faster"); + Console.WriteLine("Press S to process OrderBilled events slower"); + Console.WriteLine("Press ESC to quit"); + Console.WriteLine(); + + state.WriteState(Console.Out); + + var input = Console.ReadKey(true); + + switch (input.Key) + { + case ConsoleKey.D: + state.ToggleDegradationSimulation(); + break; + case ConsoleKey.F: + state.ProcessMessagesFaster(); + break; + case ConsoleKey.S: + state.ProcessMessagesSlower(); + break; + case ConsoleKey.Escape: + return Task.CompletedTask; + } + } + } +} \ No newline at end of file diff --git a/src/Shipping/Shipping.csproj b/src/Shipping/Shipping.csproj new file mode 100644 index 0000000..31e0112 --- /dev/null +++ b/src/Shipping/Shipping.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + Exe + enable + enable + ..\binaries\Shipping\ + processing-time.ico + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Shipping/SimulationEffects.cs b/src/Shipping/SimulationEffects.cs new file mode 100644 index 0000000..35d7dcf --- /dev/null +++ b/src/Shipping/SimulationEffects.cs @@ -0,0 +1,58 @@ +namespace Shipping; + +public class SimulationEffects +{ + public void WriteState(TextWriter output) + { + output.WriteLine("Base time to handle each OrderBilled event: {0} seconds", baseProcessingTime.TotalSeconds); + + output.Write("Simulated degrading resource: "); + output.WriteLine(degradingResourceSimulationStarted.HasValue ? "ON" : "OFF"); + } + + public Task SimulateOrderBilledMessageProcessing(CancellationToken cancellationToken = default) + { + return Task.Delay(baseProcessingTime, cancellationToken); + } + + public void ProcessMessagesFaster() + { + if (baseProcessingTime > TimeSpan.Zero) + { + baseProcessingTime -= increment; + } + } + + public void ProcessMessagesSlower() + { + baseProcessingTime += increment; + } + + public Task SimulateOrderPlacedMessageProcessing(CancellationToken cancellationToken = default) + { + var delay = TimeSpan.FromMilliseconds(200) + Degradation(); + return Task.Delay(delay, cancellationToken); + } + + public void ToggleDegradationSimulation() + { + degradingResourceSimulationStarted = degradingResourceSimulationStarted.HasValue ? default(DateTime?) : DateTime.UtcNow; + } + + TimeSpan Degradation() + { + var timeSinceDegradationStarted = DateTime.UtcNow - (degradingResourceSimulationStarted ?? DateTime.MaxValue); + if (timeSinceDegradationStarted < TimeSpan.Zero) + { + return TimeSpan.Zero; + } + + return new TimeSpan(timeSinceDegradationStarted.Ticks / degradationRate); + } + + TimeSpan baseProcessingTime = TimeSpan.FromMilliseconds(700); + TimeSpan increment = TimeSpan.FromMilliseconds(100); + + DateTime? degradingResourceSimulationStarted; + const int degradationRate = 5; +} \ No newline at end of file diff --git a/src/Shipping/processing-time.ico b/src/Shipping/processing-time.ico new file mode 100644 index 0000000..afd6aba Binary files /dev/null and b/src/Shipping/processing-time.ico differ