Closes #4607: Disable/Enable/AllDisabled lifecycle on ShardedTenancy#4608
Merged
Conversation
When ShardedTenancy started implementing IDynamicTenantSource<string> in #4605, three of the interface's lifecycle methods stayed throwing NotSupportedException because the assignment table had no `disabled` column to record the soft-delete state. CritterWatch#269 needs them implemented so its tenant-management UI can treat sharded and master-table tenancies uniformly via the store-agnostic `DynamicTenancyAdminExtensions`. ## What lands - **MartenTenantAssignmentTable**: Marten-side derivative of Weasel's `TenantAssignmentTable` that adds a `disabled boolean not null default false` column. Subclassed on the Marten side so existing pools pick up the column via Weasel's additive table-delta migration (legacy rows backfill to enabled) without requiring a coordinated Weasel release. - **ShardedTenancy** swaps in the Marten subclass via its `PoolFeatureSchema` yield. The IDynamicTenantSource<string> lifecycle methods are now real: - `DisableTenantAsync(tenantId)` flips `disabled = true` and evicts the in-memory cache entry. Idempotent for already-disabled / unknown. - `EnableTenantAsync(tenantId)` flips `disabled = false`. Idempotent. - `AllDisabledAsync()` enumerates rows where `disabled = true`. - **Resolution gates**: `FindDatabaseForTenantAsync`, `BuildDatabases`, and the under-lock check in `findOrAssignTenantDatabaseAsync` all filter `disabled = false`. The under-lock path further distinguishes "no assignment" (auto-assign) from "disabled assignment" (throw `UnknownTenantIdException`) so auto-assign cannot silently resurrect a soft-deleted tenant onto a different shard. Mirrors MasterTableTenancy's soft-delete semantics. - **Explicit re-assignment**: `AssignTenantAsync`'s UPSERT now also clears the `disabled` flag so caller-supplied re-assignment of a soft-deleted tenant reactivates it (explicit intent overrides soft-delete). Same applies to the caller-supplied `IDynamicTenantSource<string>.AddTenantAsync(tenantId, dbId)` overload that delegates to it. ## Tests `src/MultiTenancyTests/sharded_tenancy_soft_delete_tests.cs` — 10 tests: - Disabled tenant resolution throws `UnknownTenantIdException`. - `FindDatabaseForTenantAsync` returns null for disabled. - Auto-assign on a disabled tenant throws (not silent resurrection); the row keeps its original database_id and `disabled = true`. - `DisableTenantAsync` / `EnableTenantAsync` are idempotent (already-disabled / already-enabled / unknown). - `EnableTenantAsync` restores resolution and the tenant resolves to its original shard (re-enable doesn't relocate). - `AllDisabledAsync` returns exactly the disabled set (empty when none). - Explicit re-assignment via the caller-supplied AddTenantAsync overload reactivates a disabled tenant. - The same lifecycle works when accessed only through the `IDynamicTenantSource<string>` interface — the CritterWatch surface. Regression sweep on net9.0: full sharded test suite (existing sharded_tenancy_tests + the #4598 sharded_tenancy_per_tenant_events_tests + the new #4607 sharded_tenancy_soft_delete_tests) — **30/30 PASS**. Closes #4607. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced Jun 3, 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.
Closes #4607. Follow-up explicitly invited by #4605.
When
ShardedTenancystarted implementingIDynamicTenantSource<string>in #4605, three of the interface's lifecycle methods stayed throwingNotSupportedExceptionbecause the assignment table had nodisabledcolumn to record the soft-delete state. CritterWatch#269 needs them implemented so its tenant-management UI can treat sharded and master-table tenancies uniformly via the store-agnosticDynamicTenancyAdminExtensions.What lands
MartenTenantAssignmentTable— Marten-side derivative of Weasel'sTenantAssignmentTablethat adds adisabled boolean not null default falsecolumn. Subclassed on the Marten side so existing pools pick up the column via Weasel's additive table-delta migration (legacy rows backfill to enabled) without requiring a coordinated Weasel release.ShardedTenancyswaps in the Marten subclass via itsPoolFeatureSchemayield and implements the three lifecycle methods for real:DisableTenantAsync(tenantId)— flipsdisabled = true, evicts in-memory cache entry. Idempotent for already-disabled / unknown.EnableTenantAsync(tenantId)— flipsdisabled = false. Idempotent.AllDisabledAsync()— enumerates rows wheredisabled = true.Resolution gates —
FindDatabaseForTenantAsync,BuildDatabases, and the under-lock check infindOrAssignTenantDatabaseAsyncall filterdisabled = false. The under-lock path further distinguishes "no assignment" (auto-assign) from "disabled assignment" (throwUnknownTenantIdException) so auto-assign cannot silently resurrect a soft-deleted tenant onto a different shard. MirrorsMasterTableTenancy's soft-delete semantics.Explicit re-assignment —
AssignTenantAsync's UPSERT now also clears thedisabledflag so caller-supplied re-assignment of a soft-deleted tenant reactivates it (explicit intent overrides soft-delete). Same applies to the caller-suppliedIDynamicTenantSource<string>.AddTenantAsync(tenantId, dbId)overload that delegates to it.Tests
src/MultiTenancyTests/sharded_tenancy_soft_delete_tests.cs— 10 tests:UnknownTenantIdExceptionFindDatabaseForTenantAsyncreturns null for disableddatabase_idanddisabled = trueDisableTenantAsyncis idempotent (already-disabled / unknown)EnableTenantAsyncrestores resolution to the same shard the tenant had before — re-enable doesn't relocateEnableTenantAsyncis idempotentAllDisabledAsyncreturns exactly the disabled setAllDisabledAsyncis empty when none disabledAddTenantAsyncoverload reactivates a disabled tenantIDynamicTenantSource<string>interface — the CritterWatch surfaceRegression sweep (net9.0)
Full sharded test suite (existing
sharded_tenancy_tests+ the #4598sharded_tenancy_per_tenant_events_tests+ the new #4607sharded_tenancy_soft_delete_tests) — 30/30 PASS.Related: #413 (
IDynamicTenantSource<T>abstraction), #4605 (the "open an issue to request the soft-delete surface" note that this closes), CritterWatch#269 (handler refactor that consumes this).🤖 Generated with Claude Code