Skip to content

test(e2e): resiliência + exactly-once ponta a ponta (fase 6) - #8

Merged
thomasmoreira merged 1 commit into
mainfrom
feat/end-to-end-resilience
Jun 8, 2026
Merged

thomasmoreira merged 1 commit into
mainfrom
feat/end-to-end-resilience

Conversation

@thomasmoreira

@thomasmoreira thomasmoreira commented Jun 8, 2026 •

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Messaging broker connections now support automatic recovery and topology restoration following connection disruptions.
  • Tests

    • Added comprehensive end-to-end resilience testing suite verifying exactly-once message delivery guarantees and recovery scenarios across multiple services.
  • Refactor

    • Refactored service configuration to use shared dependency injection extension methods, reducing startup boilerplate.

Sobe os 3 serviços in-process contra Postgres + RabbitMQ reais e prova as duas
garantias centrais do lab — o "killer detail" da spec (§7).

Testes (tests/Integration/EndToEndResilienceTests):
- F1: com o broker congelado (docker pause), OrderPlaced fica retido no outbox
  (não publicado); ao descongelar, o dispatcher publica o que segurava — nada se perde
- Exactly-once: o checkout completo nos 3 serviços conclui uma única vez (Order
  Confirmed, cobrado 1x, estoque reservado 1x)

Para viabilizar/robustecer:
- EfOutboxProcessor: SQL do dispatcher agora é schema-qualified (via metadados do EF),
  removendo a dependência de search_path — corrige bug de migração e funciona em
  qualquer schema
- RabbitMqEventPublisher/RabbitMqConsumerHost: AutomaticRecoveryEnabled +
  TopologyRecoveryEnabled explícitos (reconexão após queda do broker)
- Composição testável: AddOrders/AddInventory/AddPayments extraídos (usados pelo host
  e pelo harness de teste); Programs simplificados
- Testes de integração rodam sequencialmente (containers não saturam o Docker)

Notas de engenharia (descobertas pelo caminho): Testcontainers perde o mapeamento de
porta em Stop/Start; docker stop é graceful-close (não dispara auto-recovery). Por isso
a indisponibilidade é simulada com pause/unpause (preserva porta, dados e conexões).

Build limpo (0/0); unit 4/4; integration 14/14 (2 execuções seguidas estáveis).
@coderabbitai

coderabbitai Bot commented Jun 8, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR hardens a multi-service distributed system with automatic RabbitMQ recovery, refactors outbox SQL generation to use EF metadata, extracts service composition into reusable DI extensions, and adds comprehensive end-to-end resilience tests that verify exactly-once semantics and outbox recovery after broker downtime.

Changes

Distributed Resilience and DI Composition

Layer / File(s) Summary
RabbitMQ resilience and outbox SQL infrastructure
src/BuildingBlocks/Messaging/RabbitMqConsumerHost.cs, src/BuildingBlocks/Messaging/RabbitMqEventPublisher.cs, src/BuildingBlocks/Persistence/EfOutboxProcessor.cs
RabbitMQ consumer and publisher now enable AutomaticRecoveryEnabled and TopologyRecoveryEnabled. Outbox processor replaces hardcoded "outbox" table with EF metadata-driven schema-qualified SQL generation, adds QualifiedTableName helper for dynamic identifier formatting, and parameterizes batch size via FromSqlRaw.
Service DI composition extensions
src/Services/Inventory/InventoryServiceCollectionExtensions.cs, src/Services/Orders/OrdersServiceCollectionExtensions.cs, src/Services/Payments/PaymentsServiceCollectionExtensions.cs
Three new extension classes (AddInventory, AddOrders, AddPayments) encapsulate service-specific registrations: DbContext/Npgsql, outbox/inbox, RabbitMQ publisher/dispatcher, gateway singletons, integration event consumers, and domain handlers.
Program.cs startup consolidation
src/Services/Inventory/Program.cs, src/Services/Orders/Program.cs, src/Services/Payments/Program.cs
Each service's startup now delegates composition to its extension method and explicitly binds RabbitMqOptions from configuration, removing prior boilerplate DI registrations and reducing file size by 40–50%.
End-to-end resilience test suite
tests/Integration/AssemblyInfo.cs, tests/Integration/EndToEndResilienceTests.cs
Assembly-level xUnit attribute disables test parallelization. New EndToEndResilienceTests verifies outbox resilience (message held during broker pause, published on recovery) and exactly-once checkout across Orders, Inventory, and Payments using Testcontainers, Docker orchestration, polling helpers, and diagnostic state dumps.

Sequence Diagram(s)

sequenceDiagram
  participant Test
  participant OrdersHost
  participant RabbitMQ
  participant DB
  Test->>DB: migrate database
  Test->>OrdersHost: start host
  Test->>RabbitMQ: pause container
  Test->>OrdersHost: PlaceOrderHandler (place order)
  OrdersHost->>DB: insert outbox row<br/>ProcessedAt = null
  DB-->>OrdersHost: row persisted
  Test->>DB: assert outbox<br/>ProcessedAt == null
  DB-->>Test: row unprocessed
  Test->>RabbitMQ: unpause container
  Test->>DB: poll until ProcessedAt<br/>is not null
  RabbitMqConsumerHost->>DB: read + lock outbox row
  RabbitMqConsumerHost->>RabbitMQ: publish event
  RabbitMqConsumerHost->>DB: mark ProcessedAt
  DB-->>Test: resilience verified
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • thomasmoreira/distributed-consistency-lab#1: Introduces the baseline RabbitMqEventPublisher and EfOutboxProcessor implementations that are extended in this PR to add resilience settings and metadata-driven SQL generation.

Poem

🐰 With ears held high and whiskers bright,
We've made this system resilient and right!
RabbitMQ recovers, the outbox stands tall,
Three services dance—no storms at all.
Exactly once, with tests to prove,
This distributed maze is smooth as a groove! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.74% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title references end-to-end resilience and exactly-once guarantees (phase 6), which are the main test additions visible in the changeset, but is in Portuguese and could be clearer about the primary changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/end-to-end-resilience

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
src/Services/Inventory/Program.cs (1)

12-12: ⚡ Quick win

Prefer the IConfiguration overload for clearer intent.

The current method-group syntax (.Bind) works but is less idiomatic. Use the Configure<TOptions>(IConfiguration) overload for better clarity.

♻️ Suggested refactor
-builder.Services.Configure<RabbitMqOptions>(builder.Configuration.GetSection("RabbitMq").Bind);
+builder.Services.Configure<RabbitMqOptions>(builder.Configuration.GetSection("RabbitMq"));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Services/Inventory/Program.cs` at line 12, Replace the method-group Bind
approach with the IConfiguration overload for clarity: update the call that
currently reads
builder.Services.Configure<RabbitMqOptions>(builder.Configuration.GetSection("RabbitMq").Bind)
to use the IConfiguration overload instead, e.g. pass
builder.Configuration.GetSection("RabbitMq") directly to
builder.Services.Configure<RabbitMqOptions>(...) so the configuration section is
used as an IConfiguration source for RabbitMqOptions.
src/Services/Orders/Program.cs (1)

13-13: ⚡ Quick win

Prefer the IConfiguration overload for clearer intent.

The current method-group syntax (.Bind) works but is less idiomatic. Use the Configure<TOptions>(IConfiguration) overload for better clarity.

♻️ Suggested refactor
-builder.Services.Configure<RabbitMqOptions>(builder.Configuration.GetSection("RabbitMq").Bind);
+builder.Services.Configure<RabbitMqOptions>(builder.Configuration.GetSection("RabbitMq"));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Services/Orders/Program.cs` at line 13, The Configure call currently
passes a method group binder:
builder.Services.Configure<RabbitMqOptions>(builder.Configuration.GetSection("RabbitMq").Bind);
— replace this with the IConfiguration overload so intent is clearer: call
Configure<RabbitMqOptions>(builder.Configuration.GetSection("RabbitMq")) (i.e.
pass the IConfiguration section directly rather than the .Bind method) to
register RabbitMqOptions from the "RabbitMq" configuration section.
tests/Integration/EndToEndResilienceTests.cs (1)

84-84: ⚡ Quick win

Consider using FirstAsync or adding an explicit filter.

SingleAsync will throw if multiple outbox rows exist. While this should be safe with a fresh database container, using FirstAsync(o => o.ProcessedAt == null, ct) or adding an explicit filter would make the test more robust and produce clearer error messages if test isolation is ever compromised.

🔍 Proposed defensive change
-            var row = await db.Outbox.SingleAsync(ct);
+            var row = await db.Outbox.FirstAsync(o => o.ProcessedAt == null, ct);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Integration/EndToEndResilienceTests.cs` at line 84, The test currently
uses db.Outbox.SingleAsync(ct) which will throw if multiple outbox rows exist;
change it to select a specific unprocessed row instead (e.g., use FirstAsync
with a predicate like o => o.ProcessedAt == null or add an explicit filter
before awaiting) so that the call targets the intended row and yields clearer
failures if test isolation is broken; update the call referencing
db.Outbox.SingleAsync to use db.Outbox.FirstAsync(o => o.ProcessedAt == null,
ct) or an equivalent filtered query.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/Services/Inventory/Program.cs`:
- Line 12: Replace the method-group Bind approach with the IConfiguration
overload for clarity: update the call that currently reads
builder.Services.Configure<RabbitMqOptions>(builder.Configuration.GetSection("RabbitMq").Bind)
to use the IConfiguration overload instead, e.g. pass
builder.Configuration.GetSection("RabbitMq") directly to
builder.Services.Configure<RabbitMqOptions>(...) so the configuration section is
used as an IConfiguration source for RabbitMqOptions.

In `@src/Services/Orders/Program.cs`:
- Line 13: The Configure call currently passes a method group binder:
builder.Services.Configure<RabbitMqOptions>(builder.Configuration.GetSection("RabbitMq").Bind);
— replace this with the IConfiguration overload so intent is clearer: call
Configure<RabbitMqOptions>(builder.Configuration.GetSection("RabbitMq")) (i.e.
pass the IConfiguration section directly rather than the .Bind method) to
register RabbitMqOptions from the "RabbitMq" configuration section.

In `@tests/Integration/EndToEndResilienceTests.cs`:
- Line 84: The test currently uses db.Outbox.SingleAsync(ct) which will throw if
multiple outbox rows exist; change it to select a specific unprocessed row
instead (e.g., use FirstAsync with a predicate like o => o.ProcessedAt == null
or add an explicit filter before awaiting) so that the call targets the intended
row and yields clearer failures if test isolation is broken; update the call
referencing db.Outbox.SingleAsync to use db.Outbox.FirstAsync(o => o.ProcessedAt
== null, ct) or an equivalent filtered query.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7cd05c04-7e89-494a-a717-85de7ba6e802

📥 Commits

Reviewing files that changed from the base of the PR and between 6dca7a0 and e751892.

📒 Files selected for processing (12)
  • src/BuildingBlocks/Messaging/RabbitMqConsumerHost.cs
  • src/BuildingBlocks/Messaging/RabbitMqEventPublisher.cs
  • src/BuildingBlocks/Persistence/EfOutboxProcessor.cs
  • src/Services/Inventory/InventoryServiceCollectionExtensions.cs
  • src/Services/Inventory/Program.cs
  • src/Services/Orders/OrdersServiceCollectionExtensions.cs
  • src/Services/Orders/Program.cs
  • src/Services/Payments/PaymentsServiceCollectionExtensions.cs
  • src/Services/Payments/Program.cs
  • tests/Integration/AssemblyInfo.cs
  • tests/Integration/EndToEndResilienceTests.cs
  • tests/Integration/ExactlyOnceTests.cs
💤 Files with no reviewable changes (1)
  • tests/Integration/ExactlyOnceTests.cs

@thomasmoreira
thomasmoreira merged commit 4d7947f into main Jun 8, 2026
2 checks passed
@thomasmoreira
thomasmoreira deleted the feat/end-to-end-resilience branch June 8, 2026 19:10
thomasmoreira added a commit that referenced this pull request Jun 10, 2026
…#8)

Sobe os 3 serviços in-process contra Postgres + RabbitMQ reais e prova as duas
garantias centrais do lab — o "killer detail" da spec (§7).

Testes (tests/Integration/EndToEndResilienceTests):
- F1: com o broker congelado (docker pause), OrderPlaced fica retido no outbox
  (não publicado); ao descongelar, o dispatcher publica o que segurava — nada se perde
- Exactly-once: o checkout completo nos 3 serviços conclui uma única vez (Order
  Confirmed, cobrado 1x, estoque reservado 1x)

Para viabilizar/robustecer:
- EfOutboxProcessor: SQL do dispatcher agora é schema-qualified (via metadados do EF),
  removendo a dependência de search_path — corrige bug de migração e funciona em
  qualquer schema
- RabbitMqEventPublisher/RabbitMqConsumerHost: AutomaticRecoveryEnabled +
  TopologyRecoveryEnabled explícitos (reconexão após queda do broker)
- Composição testável: AddOrders/AddInventory/AddPayments extraídos (usados pelo host
  e pelo harness de teste); Programs simplificados
- Testes de integração rodam sequencialmente (containers não saturam o Docker)

Notas de engenharia (descobertas pelo caminho): Testcontainers perde o mapeamento de
porta em Stop/Start; docker stop é graceful-close (não dispara auto-recovery). Por isso
a indisponibilidade é simulada com pause/unpause (preserva porta, dados e conexões).

Build limpo (0/0); unit 4/4; integration 14/14 (2 execuções seguidas estáveis).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant