Repository navigation
Fix(eventing)/outbox with dedicated db - #1434
HiroshiYoshino wants to merge 1 commit into
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 549931f37f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| /// The default bridge uses <see cref="Begin"/>; multitenancy implementations can resolve | ||
| /// full catalog metadata and define global routing before invoking the action. | ||
| /// </summary> | ||
| async Task ExecuteAsync( |
There was a problem hiding this comment.
Obtain approval before changing BuildingBlocks
When no explicit approval accompanies this change, do not land the new shared interface method: the repository's AGENTS.md marks all of src/BuildingBlocks as protected and prohibits modifications without explicit approval, while this commit changes both the public eventing abstraction and the framework event bus. Record the approval or move the behavior behind a module-owned adapter before merging.
AGENTS.md reference: AGENTS.md:L82-L82
Useful? React with 👍 / 👎.
iammukeshm
left a comment
There was a problem hiding this comment.
Thanks @HiroshiYoshino, and sorry this sat for a while. The core of the fix is right. Running the consumer inside a callback-shaped ExecuteAsync, so resolution and consumer construction happen in the same async flow, is exactly the shape I wanted, and restoring the context in finally is sound. It merges cleanly with main, builds with zero warnings, and the unit suites pass.
The problem is that the PR was written before the constraints I posted on #1427, and it breaks most of them. These are the changes I need before it can go in:
- No database query per event.
EventTenantResolverruns aTenantDbContextquery inside a new DI scope on every dispatch (EventTenantResolver.cs:11,FinbuckleEventTenantScope.cs:86-89). Please resolve throughIMultiTenantStore<AppTenantInfo>, which is cached, and dropIEventTenantResolver,EventTenantResolverand the scope-per-event. - Reuse a context that's already complete.
ExecuteAsyncalways overwrites the context and then does the lookup. If the ambientTenantInfoalready has the same ID and a non-empty connection string (an in-request publish), just run the action. - Global events stay as they are.
FinbuckleEventTenantScope.cs:73-77installs a synthetic root tenant whentenantIdis null. That changes the query filter global-event handlers see. For a null or empty tenant ID, leave the context alone likeBegindoes today. - The default drain pass stays a no-op.
FinbuckleEventingDrainScope.cs:37removes the no-op default pass and installs root with an empty connection string. Please restore theNoopScopepath forTenantId is null && ConnectionString is null. - Unknown or inactive tenants don't throw. Lines 90 and 94 throw, which turns these events into retries and dead-letters. That's a policy decision I want to make separately. For now, log a structured warning and dispatch with the ID-only context as before.
WebhookFanoutHandlernever throws on a mismatch (WebhookFanoutHandler.cs:57-60). Keep the context if it's already complete for this tenant, otherwise install the ID-only context as before and restore it infinally. The bus owns the context.- Trim the tests. The three new files add about 1,400 lines, and many cases test the behaviour above that we're not taking (retry then dead-letter, the synthetic root, the default-drain reset). I only need three things: dedicated-DB dispatch (the consumer row and the Inbox row land in the tenant's DB, default has neither), the webhook subscription lookup for a dedicated tenant, and the context restored after success and after failure. One file is enough.
- Drop the
.agents/rulesedits. They document the behaviour we're not taking and mention an internal issue. One line on the final behaviour is fine if you want it.
Two smaller ones: please use a single constructor on FinbuckleEventTenantScope (the second one without IServiceScopeFactory makes ExecuteAsync throw at runtime), and leave Begin unchanged since it no longer has production callers.
On the docs PR (fullstackhero/docs#268): it currently describes the behaviour above, so it'll need a rewrite once the code changes. Please remove the "Proposed fix" callouts and the link to the fork commit, and keep the changelog to one dated bullet that links this PR, like the other entries. The new "Delivery status and recovery" section is beyond this fix, so let's leave it out.
The real change here is small: about 30 lines in FinbuckleEventTenantScope (a lookup through the cached store, a fast path for a complete context, warn and fall back), plus reverting the drain-scope and webhook changes. Once that's in, I'll merge it quickly. Thanks again for chasing this down; it's a real bug for anyone using dedicated databases.
1a65e24 to
2fdcadc
Compare
…tabase DbContexts capture their tenant and connection at construction, so handlers resolved for a dedicated-database tenant used the shared database. - Add IEventTenantScope.ExecuteAsync (default: wraps Begin) and run the InMemoryEventBus handler loop inside it. - FinbuckleEventTenantScope resolves the full tenant before consumers are built: reuse a matching complete context, else look it up through the registered Finbuckle tenant stores in a short-lived scope (the distributed cache store first, then the EF catalog store, written back to the cache). Missing or inactive tenants fall back to an ID-only context with a warning; global events leave the ambient context unchanged. - WebhookFanoutHandler keeps a complete context the bus already resolved. - Add unit and Testcontainers tests for dedicated-database routing.
2fdcadc to
5593aa5
Compare
|
@iammukeshm
Regarding the Codex note on Docs: fullstackhero/docs#268 is updated too (one dated changelog bullet, no "Proposed fix" callouts, no recovery section). |
Related issue: #1427
Related docs: fullstackhero/docs#268
Problem
An integration event is stored in one database (the Outbox), but its consumers can
need another. For a tenant with a dedicated database,
InMemoryEventBusinstalled anID-only tenant context before resolving consumers, which dropped the dedicated
connection string. Consumers, tenant-aware DbContexts and Inbox rows then went to the
shared default database, and webhook subscriptions in the dedicated database were missed.
Fix
InMemoryEventBusnow runs its handler loop inside the newIEventTenantScope.ExecuteAsync,so tenant resolution and consumer construction share one async flow (DbContexts pick their
database at construction).
IEventTenantScope.ExecuteAsync(tenantId, action, ct)has a default implementation overBegin, so custom scopes keep working.FinbuckleEventTenantScope.ExecuteAsync:IMultiTenantStore<AppTenantInfo>, fallingback to the tenant catalog (EF store) on a miss and writing the result back to the cache;
context, as before;
finally.WebhookFanoutHandlerkeeps a complete context the bus already resolved; otherwise itinstalls the ID-only context as before and restores it in
finally.Begin,FinbuckleEventingDrainScope,.agents/rules.FinbuckleEventTenantScopehas a single constructor.Tests
DedicatedTenantRoutingTests(integration, Testcontainers): dedicated-database dispatch (theconsumer row and the Inbox row land in the tenant's database, the default has neither),
webhook subscription lookup for a dedicated tenant, and context restored after success and
after failure.
FinbuckleEventTenantScopeTestsonly adapt to the constructor.Local run: Multitenancy.Tests 95, Webhooks.Tests 71, Architecture.Tests 64 and
DedicatedTenantRoutingTests4, all passing.Behaviour change
Consumers and Inbox rows now follow the event tenant's database. Outbox claim, completion and
retry updates stay in the source database. No migration or configuration change. Historical
misrouted rows are not repaired.