Skip to content

Commit 44a965a

Browse files
authored
Merge pull request #136 from Vulthil/feature/retry-messaging-and-outbox
Commit for clean slate
2 parents 14c2130 + a16076e commit 44a965a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1495
-355
lines changed

.github/copilot-instructions.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Copilot Instructions
2+
3+
## Project Guidelines
4+
- Use a lazy `Target` pattern from `CreateInstance<T>` when inheriting from `BaseUnitTestCase` (or `BaseUnitTestCase<T>` when accessibility allows) for test classes in this repository.

samples/WebApi/WebApi.Infrastructure/Data/WebApiDbContext.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,10 @@ public WebApiDbContext CreateDbContext(string[] args)
6363
{
6464
var optionsBuilder = new DbContextOptionsBuilder<WebApiDbContext>();
6565

66+
#pragma warning disable S2068 // Credentials should not be hard-coded
6667
optionsBuilder.UseNpgsql("Host=localhost;Port=5432;Database=__dummy__;Username=__dummy__;Password=__dummy__;Pooling=false",
6768
o => o.ExecutionStrategy(d => new NonRetryingExecutionStrategy(d)));
69+
#pragma warning restore S2068 // Credentials should not be hard-coded
6870

6971
return new WebApiDbContext(optionsBuilder.Options);
7072
}

samples/WebApi/WebApi.Infrastructure/DependencyInjection.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,16 @@ public static IHostApplicationBuilder AddRabbitMqMessagingInfrastructure(this IH
4343

4444
x.AddQueue("MainEntityEvents", queue =>
4545
{
46+
queue.UseRetry(r => r.SetIntervals(TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5)));
47+
48+
queue.UseDeadLetterQueue();
49+
4650
queue.AddRequestConsumer<SideEffectRequestConsumer>();
4751

4852
queue.AddConsumer<MainEntityCreatedIntegrationEventConsumer>(c =>
4953
{
5054
c.Bind<MainEntityCreatedIntegrationEvent>("main-entity.created");
55+
c.UseRetry(r => r.Immediate(5));
5156
});
5257
queue.AddConsumer<MainEntityCreatedIntegrationEventConsumer>(c =>
5358
{

samples/WebApi/WebApi.Infrastructure/Migrations/20260225221048_AddOutboxRetryCount.Designer.cs

Lines changed: 100 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using Microsoft.EntityFrameworkCore.Migrations;
2+
3+
#nullable disable
4+
5+
namespace WebApi.Infrastructure.Migrations
6+
{
7+
/// <inheritdoc />
8+
public partial class AddOutboxRetryCount : Migration
9+
{
10+
/// <inheritdoc />
11+
protected override void Up(MigrationBuilder migrationBuilder)
12+
{
13+
migrationBuilder.AddColumn<int>(
14+
name: "RetryCount",
15+
table: "OutboxMessages",
16+
type: "integer",
17+
nullable: false,
18+
defaultValue: 0);
19+
}
20+
21+
/// <inheritdoc />
22+
protected override void Down(MigrationBuilder migrationBuilder)
23+
{
24+
migrationBuilder.DropColumn(
25+
name: "RetryCount",
26+
table: "OutboxMessages");
27+
}
28+
}
29+
}

samples/WebApi/WebApi.Infrastructure/Migrations/WebApiDbContextModelSnapshot.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
1717
{
1818
#pragma warning disable 612, 618
1919
modelBuilder
20-
.HasAnnotation("ProductVersion", "9.0.5")
20+
.HasAnnotation("ProductVersion", "10.0.3")
2121
.HasAnnotation("Relational:MaxIdentifierLength", 63);
2222

2323
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
@@ -44,6 +44,9 @@ protected override void BuildModel(ModelBuilder modelBuilder)
4444
b.Property<DateTimeOffset?>("ProcessedOnUtc")
4545
.HasColumnType("timestamp with time zone");
4646

47+
b.Property<int>("RetryCount")
48+
.HasColumnType("integer");
49+
4750
b.Property<string>("Type")
4851
.IsRequired()
4952
.HasColumnType("text");

samples/WebApi/WebApi.Tests/Fixtures/PostgreSqlTestContainer.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,16 @@ internal sealed class PostgreSqlTestContainer(IMessageSink messageSink) : TestDa
2020
public override DbProviderFactory DbProviderFactory => NpgsqlFactory.Instance;
2121
public override string ConnectionStringKey => ServiceNames.PostgresSqlServerServiceName;
2222
}
23-
23+
2424

2525
public sealed class RabbitMqTestContainer(IMessageSink messageSink) : TestContainerFixtureWithConnectionString<RabbitMqBuilder, RabbitMqContainer>(messageSink)
2626
{
27-
private readonly RabbitMqBuilder _builder = new RabbitMqBuilder("rabbitmq:4.2.2-management")
27+
private readonly RabbitMqBuilder _builder = new RabbitMqBuilder("rabbitmq:4-management")
2828
.WithUsername("guest")
2929
.WithPassword("guest");
3030

3131
protected override RabbitMqBuilder Configure() => _builder;
3232

3333
public override string ConnectionStringKey => ServiceNames.RabbitMqServiceName;
3434
public override string ConnectionString => Container.GetConnectionString();
35-
3635
}

samples/WebApi/WebApi.Tests/MainEntityIntegrationTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public async Task Test_Update()
7272
var outboxMessage = await dbContext.OutboxMessages.Where(o =>
7373
EF.Functions.JsonContains(o.Content, json) &&
7474
o.ProcessedOnUtc.HasValue &&
75-
o.Type == typeof(MainEntityNameUpdatedEvent).AssemblyQualifiedName)
75+
o.Type == typeof(MainEntityNameUpdatedEvent).FullName)
7676
.FirstOrDefaultAsync();
7777

7878
if (outboxMessage is null)
@@ -81,7 +81,7 @@ public async Task Test_Update()
8181
}
8282

8383
return Result.Success(outboxMessage);
84-
});
84+
}, cancellationToken: CancellationToken);
8585

8686
outboxMessageResult.IsSuccess.ShouldBeTrue();
8787
}

samples/WebApi/WebApi.Tests/SideEffectIntegrationTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public async Task TestCreate()
3838
}
3939

4040
return queryResult;
41-
});
41+
}, cancellationToken: CancellationToken);
4242

4343
// Assert
4444
result.IsSuccess.ShouldBeTrue();

src/Vulthil.Extensions.Testing/Polling.cs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ public static class Polling
1010
public static async Task<Result<T>> WaitAsync<T>(
1111
TimeSpan timeout,
1212
Func<Task<Result<T>>> func,
13-
TimeSpan? timerTick = null)
13+
TimeSpan? timerTick = null,
14+
CancellationToken cancellationToken = default)
1415
{
1516
timerTick ??= TimeSpan.FromSeconds(1);
1617
using var timer = new PeriodicTimer(timerTick.Value);
1718

1819
DateTime endTimeUtc = DateTime.UtcNow.Add(timeout);
1920
while (DateTime.UtcNow < endTimeUtc &&
20-
await timer.WaitForNextTickAsync())
21+
await timer.WaitForNextTickAsync(cancellationToken))
2122
{
2223
Result<T> result = await func();
2324

@@ -29,4 +30,28 @@ await timer.WaitForNextTickAsync())
2930

3031
return Result.Failure<T>(Timeout);
3132
}
33+
34+
public static async Task<Result> WaitAsync(
35+
TimeSpan timeout,
36+
Func<Task<Result>> func,
37+
TimeSpan? timerTick = null,
38+
CancellationToken cancellationToken = default)
39+
{
40+
timerTick ??= TimeSpan.FromSeconds(1);
41+
using var timer = new PeriodicTimer(timerTick.Value);
42+
43+
DateTime endTimeUtc = DateTime.UtcNow.Add(timeout);
44+
while (DateTime.UtcNow < endTimeUtc &&
45+
await timer.WaitForNextTickAsync(cancellationToken))
46+
{
47+
Result result = await func();
48+
49+
if (result.IsSuccess)
50+
{
51+
return result;
52+
}
53+
}
54+
55+
return Result.Failure(Timeout);
56+
}
3257
}

0 commit comments

Comments
 (0)