-
Notifications
You must be signed in to change notification settings - Fork 75
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Host.Outbox] Ensure SQL tables are provisioned without consumer star…
…t and with first publish. Also: - Add baseline performance test for Outbox. - Send Created bus lifecycle event to be able to hook into Master bus creation Signed-off-by: Tomasz Maruszak <maruszaktomasz@gmail.com>
- Loading branch information
Showing
10 changed files
with
248 additions
and
53 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
docker compose -f src/Infrastructure/docker-compose.yml up --force-recreate -V |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
18 changes: 18 additions & 0 deletions
18
src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/DatabaseFacadeExtenstions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
namespace SlimMessageBus.Host.Outbox.DbContext.Test; | ||
|
||
using Microsoft.EntityFrameworkCore; | ||
using Microsoft.EntityFrameworkCore.Infrastructure; | ||
|
||
public static class DatabaseFacadeExtenstions | ||
{ | ||
public static Task EraseTableIfExists(this DatabaseFacade db, string tableName) | ||
{ | ||
return db.ExecuteSqlRawAsync( | ||
$""" | ||
IF EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'dbo' AND TABLE_NAME = '{tableName}') | ||
BEGIN | ||
DELETE FROM dbo.{tableName}; | ||
END | ||
"""); | ||
} | ||
} |
132 changes: 132 additions & 0 deletions
132
src/Tests/SlimMessageBus.Host.Outbox.DbContext.Test/OutboxBenchmarkTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
namespace SlimMessageBus.Host.Outbox.DbContext.Test; | ||
|
||
using System.Diagnostics; | ||
using System.Reflection; | ||
|
||
using Microsoft.EntityFrameworkCore; | ||
using Microsoft.Extensions.Configuration; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using Microsoft.Extensions.Logging; | ||
|
||
using SecretStore; | ||
|
||
using SlimMessageBus.Host; | ||
using SlimMessageBus.Host.AzureServiceBus; | ||
using SlimMessageBus.Host.Outbox.DbContext.Test.DataAccess; | ||
using SlimMessageBus.Host.Serialization.SystemTextJson; | ||
using SlimMessageBus.Host.Test.Common.IntegrationTest; | ||
|
||
[Trait("Category", "Integration")] // for benchmarks | ||
public class OutboxBenchmarkTests(ITestOutputHelper testOutputHelper) : BaseIntegrationTest<OutboxBenchmarkTests>(testOutputHelper) | ||
{ | ||
private static readonly string OutboxTableName = "IntTest_Benchmark_Outbox"; | ||
private static readonly string MigrationsTableName = "IntTest_Benchmark_Migrations"; | ||
|
||
protected override void SetupServices(ServiceCollection services, IConfigurationRoot configuration) | ||
{ | ||
services.AddSlimMessageBus(mbb => | ||
{ | ||
mbb.AddChildBus("ExternalBus", mbb => | ||
{ | ||
var topic = "tests.outbox-benchmark/customer-events"; | ||
mbb | ||
.WithProviderServiceBus(cfg => cfg.ConnectionString = Secrets.Service.PopulateSecrets(configuration["Azure:ServiceBus"])) | ||
.Produce<CustomerCreatedEvent>(x => x.DefaultTopic(topic)) | ||
.Consume<CustomerCreatedEvent>(x => x | ||
.Topic(topic) | ||
.WithConsumer<CustomerCreatedEventConsumer>() | ||
.SubscriptionName(nameof(OutboxBenchmarkTests))) // for AzureSB | ||
.UseOutbox(); // All outgoing messages from this bus will go out via an outbox | ||
}); | ||
mbb.AddServicesFromAssembly(Assembly.GetExecutingAssembly()); | ||
mbb.AddJsonSerializer(); | ||
mbb.AddOutboxUsingDbContext<CustomerContext>(opts => | ||
{ | ||
opts.PollBatchSize = 100; | ||
opts.PollIdleSleep = TimeSpan.FromSeconds(0.5); | ||
opts.MessageCleanup.Interval = TimeSpan.FromSeconds(10); | ||
opts.MessageCleanup.Age = TimeSpan.FromMinutes(1); | ||
opts.SqlSettings.DatabaseTableName = OutboxTableName; | ||
opts.SqlSettings.DatabaseMigrationsTableName = MigrationsTableName; | ||
}); | ||
mbb.AutoStartConsumersEnabled(false); | ||
}); | ||
|
||
services.AddSingleton<TestEventCollector<CustomerCreatedEvent>>(); | ||
|
||
// Entity Framework setup - application specific EF DbContext | ||
services.AddDbContext<CustomerContext>(options => options.UseSqlServer(Secrets.Service.PopulateSecrets(Configuration.GetConnectionString("DefaultConnection")))); | ||
} | ||
|
||
private async Task PerformDbOperation(Func<CustomerContext, Task> action) | ||
{ | ||
using var scope = ServiceProvider!.CreateScope(); | ||
var context = scope.ServiceProvider.GetRequiredService<CustomerContext>(); | ||
await action(context); | ||
} | ||
|
||
[Theory] | ||
[InlineData([100])] | ||
public async Task Given_EventPublisherAndConsumerUsingOutbox_When_BurstOfEventsIsSent_Then_EventsAreConsumedProperly(int messageCount) | ||
{ | ||
// arrange | ||
await PerformDbOperation(async context => | ||
{ | ||
// migrate db | ||
await context.Database.MigrateAsync(); | ||
// clean outbox from previous test run | ||
await context.Database.EraseTableIfExists(OutboxTableName); | ||
await context.Database.EraseTableIfExists(MigrationsTableName); | ||
}); | ||
|
||
var surnames = new[] { "Doe", "Smith", "Kowalsky" }; | ||
var events = Enumerable.Range(0, messageCount).Select(x => new CustomerCreatedEvent(Guid.NewGuid(), $"John {x:000}", surnames[x % surnames.Length])).ToList(); | ||
var store = ServiceProvider!.GetRequiredService<TestEventCollector<CustomerCreatedEvent>>(); | ||
|
||
// act | ||
|
||
// publish the events in one shot (consumers are not started yet) | ||
var publishTimer = Stopwatch.StartNew(); | ||
|
||
var publishTasks = events | ||
.Select(async ev => | ||
{ | ||
var unitOfWorkScope = ServiceProvider!.CreateScope(); | ||
await using (unitOfWorkScope as IAsyncDisposable) | ||
{ | ||
var bus = unitOfWorkScope.ServiceProvider.GetRequiredService<IMessageBus>(); | ||
try | ||
{ | ||
await bus.Publish(ev, headers: new Dictionary<string, object> { ["CustomerId"] = ev.Id }); | ||
} | ||
catch (Exception ex) | ||
{ | ||
Logger.LogInformation("Exception occurred while publishing event {Event}: {Message}", ev, ex.Message); | ||
} | ||
} | ||
}) | ||
.ToArray(); | ||
|
||
await Task.WhenAll(publishTasks); | ||
|
||
var publishTimerElapsed = publishTimer.Elapsed; | ||
|
||
// start consumers | ||
await EnsureConsumersStarted(); | ||
|
||
// consume the events from outbox | ||
var consumptionTimer = Stopwatch.StartNew(); | ||
await store.WaitUntilArriving(newMessagesTimeout: 5, expectedCount: events.Count); | ||
|
||
// assert | ||
|
||
var consumeTimerElapsed = consumptionTimer.Elapsed; | ||
|
||
Logger.LogInformation("Message Publish took: {Elapsed}", publishTimerElapsed); | ||
Logger.LogInformation("Message Consume took: {Elapsed}", consumeTimerElapsed); | ||
|
||
// Ensure the expected number of events was actually published to ASB and delivered via that channel. | ||
store.Count.Should().Be(events.Count); | ||
} | ||
} |
Oops, something went wrong.