MySQL: resolve queue tables per tenant database (GH-3859) - #3861
Merged
Conversation
…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
A MySQL schema IS a database, so qualifying the transport's queue tables with
the single MySqlTransport.TransportSchemaName resolved every tenant's data
source to the same physical table. Four registered databases had exactly one
wolverine_queue_<name> table between them, in the "wolverine_queues" database:
- no tenant isolation at all -- every tenant's sends landed in one table
- CountAsync()/ScheduledCountAsync(), and so the queue depth reported by
GetAttributesAsync(), multiplied the true row count by the tenant count
- Purge/Setup/Teardown/Check repeated their work against that one table
PostgreSQL is unaffected: its TransportSchemaName is a schema nested inside
each tenant database, so every tenant already had its own copy.
On a multi-tenanted host the queue tables are now resolved against the
connecting data source's own database, so each tenant owns its tables the way
it does on every other provider. Single database hosts are untouched and keep
using TransportSchemaName.
Note that this does not address GH-3860, the same root cause in the tenant
message stores, which is a separate design decision.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JKfy5EzLX1i149gjUb3Tfg
This was referenced Aug 7, 2026
This file contains hidden or 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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #3859.
The bug
A MySQL schema is a database. The transport qualifies its queue tables with one transport-wide name:
So on a multi-tenanted host every tenant data source resolves
wolverine_queues.wolverine_queue_<name>to the same physical table. Four registered databases, one queue table between them:Consequences:
CountAsync()multiplied rather than summed — with 10 rows physically present it reported 40. That feedsGetAttributesAsync(), so it is user visible queue depth.Purge/Setup/Teardown/Checkrepeated their work against that one table.PostgreSQL is unaffected: its
TransportSchemaNameis a schema nested inside each tenant database, so each tenant already had its own copy. Verified both ways.The fix
MySqlQueue.SchemaFor(MySqlDataSource)resolves the schema from the connecting data source's own database when the host is multi-tenanted, and the sender, listener, and everyforEveryDatabaseoperation build their identifiers through it. Single database hosts are untouched and keep usingTransportSchemaName.Verification
every_tenant_database_owns_its_own_queue_tablesa_tenants_message_lands_only_in_that_tenants_databasecounts_sum_across_databases_instead_of_multiplyingdotnet build wolverine.slnx -c Release -f net9.0The full MySQL suite passing matters most here: it covers the single-database transport paths that must not change.
Known wart
Bootstrap still leaves an empty, unused pair of tables in the
wolverine_queuesdatabase on a multi-tenanted host. A provisioning pass runs before the transport has resolved its tenancy, so it provisions againstTransportSchemaName. I confirmed by elimination that this is not theStore.AddTableregistration — disabling it entirely does not stop it. Nothing reads or writes those tables: every sender, listener and count goes through the per-tenant resolution. I left it rather than restructure transport bootstrap ordering inside a bug fix; happy to chase it separately if you'd rather not ship the clutter.Not addressed here
#3860 — the same root cause in the tenant message stores (
wolverine_incoming_envelopesand friends also share one schema across tenants). That one is a design decision about how MySQL should map Wolverine's two-levelschema.tablemodel, not a bug fix, so it is deliberately out of scope.🤖 Generated with Claude Code
https://claude.ai/code/session_01JKfy5EzLX1i149gjUb3Tfg