Skip to content

MySQL: resolve queue tables per tenant database (GH-3859) - #3861

Merged
jeremydmiller merged 2 commits into
mainfrom
gh-3859/mysql-per-tenant-queue-tables
Aug 6, 2026
Merged

MySQL: resolve queue tables per tenant database (GH-3859)#3861
jeremydmiller merged 2 commits into
mainfrom
gh-3859/mysql-per-tenant-queue-tables

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Fixes #3859.

Contains #3858. The count assertion here depends on the forEveryDatabase fix from #3858, so that commit is carried on this branch rather than stacking — a PR based on a feature branch gets no CI in this repo. Merge #3858 first and this rebases to a single commit.

The bug

A MySQL schema is a database. The transport qualifies its queue tables with one transport-wide name:

internal class QueueTable : Table
{
    public QueueTable(MySqlTransport parent, string tableName) : base(
        new DbObjectName(parent.TransportSchemaName, tableName))   // "wolverine_queues"

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:

MySQL, before                          MySQL, after
--------------------------------       --------------------------------
wolverine_queues  wolverine_queue_x     wolverine   wolverine_queue_x
                                        tenant_db1  wolverine_queue_x
(tenant_db1/2/3 have none)              tenant_db2  wolverine_queue_x
                                        tenant_db3  wolverine_queue_x

Consequences:

  • No tenant isolation. Every tenant's sends landed in one table.
  • CountAsync() multiplied rather than summed — with 10 rows physically present it reported 40. That feeds GetAttributesAsync(), so it is user visible queue depth.
  • 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 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 every forEveryDatabase operation build their identifiers through it. Single database hosts are untouched and keep using TransportSchemaName.

Verification

Evidence Without fix With fix
every_tenant_database_owns_its_own_queue_tables fails passes
a_tenants_message_lands_only_in_that_tenants_database fails passes
counts_sum_across_databases_instead_of_multiplying fails passes
Full MySqlTests suite 116 / 116
PostgresqlTests.MultiTenancy 21 / 21
dotnet build wolverine.slnx -c Release -f net9.0 clean, 0 warnings

The 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_queues database on a multi-tenanted host. A provisioning pass runs before the transport has resolved its tenancy, so it provisions against TransportSchemaName. I confirmed by elimination that this is not the Store.AddTable registration — 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_envelopes and friends also share one schema across tenants). That one is a design decision about how MySQL should map Wolverine's two-level schema.table model, not a bug fix, so it is deliberately out of scope.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JKfy5EzLX1i149gjUb3Tfg

jeremydmiller and others added 2 commits August 6, 2026 04:49
…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
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.

MySQL multi-tenanted queues share one physical queue table: TransportSchemaName is a database, so there are no per-tenant queue tables

1 participant