Skip to content

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

Description

@jeremydmiller

Found while fixing #3815. Distinct from that issue and larger: #3815 is an off-by-one fan-out, this is that MySQL has no per-tenant queue tables at all.

The shape

In MySQL a schema is a database. MySqlTransport.TransportSchemaName defaults to "wolverine_queues" and the queue tables are qualified with it unconditionally:

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

So every tenant data source, whatever database its connection string names, resolves wolverine_queues.wolverine_queue_<name> to the same physical table on that server. The per-tenant fan-out in MultiTenantedQueueSender / MultiTenantedQueueListener / forEveryDatabase picks a different connection per tenant, but they all land on one table.

PostgreSQL is unaffected: its TransportSchemaName is a schema nested inside each tenant database, so each tenant genuinely has its own copy.

Observed

A host with RegisterStaticTenants for three tenant databases plus the main one, publishing to a MySQL queue.

MySQL — four databases, one queue table:

Database                 |  TABLE_SCHEMA      TABLE_NAME
-------------------------|  ----------------------------------------------
wolverine       (main)   |  wolverine_queues  wolverine_queue_countone
tenant_db1               |  wolverine_queues  wolverine_queue_countone_scheduled
tenant_db2               |
tenant_db3               |  (that is the complete list)

PostgreSQL, same configuration — one queue table per database:

postgres (main)  ->  queue_counts_tenanted.wolverine_queue_countone
db1              ->  queue_counts_tenanted.wolverine_queue_countone
db2              ->  queue_counts_tenanted.wolverine_queue_countone
db3              ->  queue_counts_tenanted.wolverine_queue_countone

With 10 rows physically present in the single MySQL table, CountAsync() returned 40 — the same table counted once per tenant data source.

Why it matters

  • No tenant isolation for MySQL queues. Every tenant's sends land in one table, and a listener on any tenant connection can pick up another tenant's messages. Depending on how the listener filters, this is either cross-tenant delivery or a shared work queue masquerading as a partitioned one.
  • CountAsync()/ScheduledCountAsync() multiply by the tenant count, so GetAttributesAsync() reports a queue depth that is N times the truth. OracleQueue.CountAsync double-counts on a multi-tenanted store: forEveryDatabase visits Main twice #3815 reduces the multiplier from N+1 to N; it does not fix this.
  • PurgeAsync/SetupAsync/TeardownAsync/CheckAsync all repeat their work N times against one table.

Caveat on scope

This only bites when the tenant databases live on the same MySQL server — which is what RegisterStaticTenants with connection strings differing only by Database= produces, and the common multi-tenancy-by-database setup. Tenants on separate servers each get their own wolverine_queues database and behave correctly.

Not verified

Oracle uses the same fixed-schema pattern (TransportSchemaName = "WOLVERINE_QUEUES", and in Oracle a schema is a user), so it may have the same exposure depending on whether tenants land on separate instances/PDBs or separate users on one instance. There is no Oracle multi-tenancy test fixture to settle it with.

Likely directions

  1. Derive the queue table's schema from the tenant's own database rather than a single transport-wide name, so MySQL gets per-tenant tables the way PostgreSQL does.
  2. Or, if a shared queue table is the intended MySQL model, stop fanning forEveryDatabase out over tenants for MySQL entirely (visit the transport's one database once) and document that MySQL queues are not tenant-isolated.

(1) matches every other transport's semantics; (2) is smaller but changes what multi-tenanted MySQL queues promise.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions