Skip to content

Stop visiting the main database twice in forEveryDatabase (GH-3815) - #3858

Merged
jeremydmiller merged 1 commit into
mainfrom
gh-3815/foreverydatabase-double-visit
Aug 6, 2026
Merged

Stop visiting the main database twice in forEveryDatabase (GH-3815)#3858
jeremydmiller merged 1 commit into
mainfrom
gh-3815/foreverydatabase-double-visit

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Fixes #3815.

The unverified fact resolves to "yes"

#3815 called out one thing that had to be checked before any change: does MultiTenantedMessageStore.ActiveDatabases() actually include Main? It does — databases() yields it first:

private IEnumerable<IMessageStore> databases()
{
    yield return Main;                                        // <-- here
    foreach (var database in Source.AllActive()) yield return database;
}

And every affected transport assigns Databases only alongside Store = mt.Main (checked at both assignment sites — Initialize and ConnectAsync — in each). So walking Parent.Store and then all of ActiveDatabases() hits the main database twice.

Measured on a real multi-tenanted Postgres host: GetAttributesAsync()["Count"] returns "2" for a single row, and 5 for 4 rows.

Scope is wider than the title, but not universal

Transport Shape Affected
Oracle, PostgreSQL, MySQL ifif yes
SqlServer, Sqlite ifelse no — already correct

The SqlServer and Sqlite queues already branch the right way, so this PR brings the other three in line with them rather than introducing a new dedupe concept.

What was doubled:

  • CountAsync() / ScheduledCountAsync() — these feed GetAttributesAsync(), so this is user visible queue depth, not just a test concern
  • CheckAsync() — ran its schema diff against the main database twice
  • SetupAsync() / PurgeAsync() / TeardownAsync() — did their work twice

Why no existing test caught it

The multi-tenant coverage in clear_all_wolverine_storage_across_tenant_databases asserts CountAsync() is 0 after a purge — and zero doubled is still zero. The new tests assert a non-zero count, with a row deliberately placed in the main database (an untenanted send lands there).

Verification

Evidence Without fix With fix
does_not_double_count_rows_in_the_main_database fails — 2 vs 1 passes
still_sums_across_main_and_every_tenant_database fails — 5 vs 4 passes
reported_attributes_match_the_physical_row_count fails — "2" vs "1" passes
dotnet build wolverine.slnx -c Release -f net9.0 clean, 0 warnings
PostgresqlTests.MultiTenancy (incl. the pre-existing suite) 21 / 21
OracleTests.Transport 22 / 22

The second test guards the obvious way to get this wrong — trading the double count for a missed tenant database.

A related finding, filed separately

While trying to add the same test for MySQL I found a different and larger problem, tracked in its own issue rather than fixed here: MySQL has no per-tenant queue tables at all. Because a schema is a database in MySQL, TransportSchemaName resolves to one fixed database on the server, so the multi-tenant fan-out queries a single physical table once per tenant:

TABLE_SCHEMA      TABLE_NAME                        rows
wolverine_queues  wolverine_queue_countone          10      <-- one table, total

…while CountAsync() reported 40. This PR takes the MySQL multiplier from N+1 down to N, which is strictly closer to correct, but does not resolve that. PostgreSQL is unaffected because its transport schema nests inside each tenant database.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JKfy5EzLX1i149gjUb3Tfg

…abase (GH-3815)

MultiTenantedMessageStore.ActiveDatabases() yields Main first, and each
transport only assigns Databases alongside Store = mt.Main. The Oracle,
PostgreSQL and MySQL queues walked both sources, so on any multi-tenanted
configuration the main database was visited twice:

  - CountAsync()/ScheduledCountAsync() double counted every row living in it,
    and those feed GetAttributesAsync() -- user visible queue depth, measured
    at "2" for a single row and 5 for 4 rows before this change
  - CheckAsync() ran its schema diff against it twice
  - SetupAsync()/PurgeAsync()/TeardownAsync() did their work twice

The SqlServer and Sqlite queues already branch if/else here and were never
affected; the three are brought in line with them.

The existing multi-tenant coverage missed this because it only ever asserts a
count of 0, and zero doubled is still zero.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JKfy5EzLX1i149gjUb3Tfg
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.

OracleQueue.CountAsync double-counts on a multi-tenanted store: forEveryDatabase visits Main twice

1 participant