Skip to content

GH-3683: make conjoined tenant partition bucketing actually work - #3688

Merged
jeremydmiller merged 1 commit into
mainfrom
gh-3683/conjoined-partition-bucketing
Jul 28, 2026
Merged

GH-3683: make conjoined tenant partition bucketing actually work#3688
jeremydmiller merged 1 commit into
mainfrom
gh-3683/conjoined-partition-bucketing

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Closes #3683. Also closes #3686 (the co-tenant data loss found alongside it).

Important

Draft — blocked on Weasel 9.20.0 reaching nuget.org. The root cause is in Weasel, fixed by
JasperFx/weasel#392. CI will fail restore until that version is published. Everything here was
verified locally against a packed 9.20.0 build.

The problem

Tenant bucketing — registering several small tenants against one partition suffix so they share a physical
partition — is documented on the EF Core multi-tenancy page and exposed through
PartitionPerTenant(p => p.AllowPartitionSharing = true). It did not work on either engine. It had no test
coverage
, which is why it went unnoticed; the doc sample demonstrating it is compile-only and never executed.

Both defects were reproduced against real PostgreSQL and SQL Server:

  • PostgreSQL emitted one single-value ListPartition per tenant, so the second member of a bucket was
    swallowed by CREATE TABLE IF NOT EXISTS and its first write failed with 23514 no partition of relation found for row.
  • SQL Server worked inside a single batch but silently not across calls — the natural tenant-onboarding
    shape, and the one the docs showed. The registry stored only tenant_id -> ordinal, so a brand-new tenant
    could not discover which ordinal its bucket already owned and quietly got its own partition. The
    15,000-partition ceiling that bucketing exists to dodge was not mitigated at all.

Neither was fixable inside Wolverine, hence JasperFx/weasel#392.

Plus a data-loss defect found along the way (#3686)

Dropping one member of a bucket destroyed every co-tenant's rows: the by-value drop resolved the tenant
to its suffix and then dropped by suffix, deleting the whole bucket's registry rows and DROPping the shared
partition table. Also fixed in weasel#392, and covered here.

Changes

PostgresqlTenantPartitioning needs no change — it hands Weasel the tenant → suffix map, and the fixed
Weasel does the right thing.

SqlServerTenantPartitioning was resolving the bucket's ordinal itself out of the tenant → ordinal map, which
is blind to buckets: for a brand-new tenant the lookup matched nothing and it allocated a fresh ordinal. It now
delegates to Weasel's bucket-aware overload, and its AllowPartitionSharing guard spans calls via the
persisted bucket map — matching what PostgreSQL already did.

Compliance coverage on both engines for #3683's acceptance criteria: members registered together and
separately land in one physical partition, and dropping one member leaves the others reading, writing, and
holding their data.

Docs. The bucketing sample showed the sequential pattern without enabling AllowPartitionSharing, so as
written it would have thrown. The config sample now enables it, and a new Tenant Bucketing section covers
when to reach for it, the drop semantics, the isolation trade-off, and the 9.20.0 requirement.

Verification

Real databases throughout.

Suite Result
EfCoreTests.MultiTenancy 193/193
SqlServerTests 388/390 (2 skipped, pre-existing)
PostgresqlTests 466/467 (1 skipped, pre-existing)
wolverine.slnx Release build clean, 0 warnings

Upstream weasel#392 carries 5 new PostgreSQL tests (all 5 red on 9.19.1) and 5 new SQL Server tests, with
both full Weasel suites green.

Merge order

  1. Make managed tenant partition bucketing actually share a partition (#391) weasel#392
  2. Publish Weasel 9.20.0
  3. Mark this ready — the Directory.Packages.props bump to 9.20.0 is already in this branch

…H-3683)

Tenant "bucketing" -- registering several small tenants against one partition suffix so
they share a physical partition -- is documented on the EF Core multi-tenancy page and
exposed via ConjoinedTenancyOptions.PartitionPerTenant(p => p.AllowPartitionSharing = true),
but it did not work on either engine. It had no test coverage, which is why this went
unnoticed. Both defects were reproduced against real PostgreSQL and SQL Server.

The root cause was in Weasel and is fixed by JasperFx/weasel#392 (9.20.0):

  - PostgreSQL emitted one single-value ListPartition per tenant, so the second member of
    a bucket was swallowed by CREATE TABLE IF NOT EXISTS and its first write failed with
    23514. Widening an existing partition needs DETACH + re-ATTACH, which IF NOT EXISTS
    cannot express.
  - SQL Server's ordinal registry had no column for the bucket, so a tenant joining an
    existing bucket in a later call could not discover its ordinal.

Wolverine changes here:

PostgresqlTenantPartitioning needs no change at all -- it hands Weasel the tenant -> suffix
map and the fixed Weasel does the right thing.

SqlServerTenantPartitioning was resolving the bucket's ordinal itself out of the tenant ->
ordinal map, which is blind to buckets: for a brand new tenant the lookup matched nothing
and it allocated a fresh ordinal, so sequentially registered members silently never shared.
It now delegates to Weasel's bucket-aware overload, and its AllowPartitionSharing guard
spans calls via the persisted bucket map, matching what PostgreSQL already did.

Compliance coverage on BOTH engines for the acceptance criteria on GH-3683: members
registered together and separately land in one physical partition, and dropping one member
leaves the others reading, writing, and holding their data. That last one covers GH-3686,
the co-tenant data loss found alongside this.

Docs: the bucketing sample showed the sequential pattern without enabling
AllowPartitionSharing, so as written it would have thrown. The config sample now enables it,
and a new Tenant Bucketing section covers when to reach for it, the drop semantics, and the
9.20.0 requirement.

Verified: EfCoreTests.MultiTenancy 193/193, SqlServerTests 388/390 (2 skipped),
full wolverine.slnx Release build clean at 0 warnings.

NOTE: requires Weasel 9.20.0 on nuget.org; verified locally against a packed build.
@jeremydmiller

Copy link
Copy Markdown
Member Author

Unblocked — Weasel 9.20.0 is published (JasperFx/weasel#392 merged; publish workflow completed 16:36 UTC).

Verified against the published package, with no local feed overlay in play:

  • Directory.Packages.props resolves Weasel.Core/9.20.0 and Weasel.SqlServer/9.20.0 straight from nuget.org
  • Full wolverine.slnx Release build clean — 0 warnings, 0 errors

Marking ready for review.

@jeremydmiller
jeremydmiller merged commit cda3f39 into main Jul 28, 2026
58 of 88 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant