Skip to content

Preserve active tenant leases when enforcing the pool limit - #356

Draft
lucianghinda wants to merge 1 commit into
basecamp:mainfrom
lucianghinda:fix/preserve-busy-tenant-pools
Draft

lucianghinda wants to merge 1 commit into
basecamp:mainfrom
lucianghinda:fix/preserve-busy-tenant-pools

Conversation

@lucianghinda

Copy link
Copy Markdown

Busy tenant connection pools could be evicted when new tenants pushed the process over its configured pool limit. This change enforces a hard limit across registered tenant pools and retires only idle pools after identity and checkout checks, so active tenant leases remain usable while admission fails clearly when all slots are occupied.

Pool creation and schema validation now coordinate through bounded locking. Pools remain unavailable to other model lookups while their creator completes validation; failures remove ready registry entries and fail closed. Stale LRU identities are pruned against Rails’ raw handler state. The guide documents the limit, lock ordering, and Rails compatibility seams, and a minimum-version appraisal covers Rails 8.1.2.1.

Validation passed on Ruby 4.0.6 with Rails 8.1.4 bin/ci, plus full unit and integration suites on minimum Rails 8.1.2.1 and pinned Rails 8.2.0.alpha (07cfbd77d4f3c4312b4651ca74f9ee9e28dfde5f). Each suite run reported 1,815 unit runs/3,727 assertions and nine integration configurations of 36 runs/98 assertions, with zero failures, errors, or skips. Seeded lifecycle tests passed at seeds 412041 and 412044; the full tenant test passed at seeds 412042 and 412043; the pending-validation race passed repeatedly across all nine scenarios. Independent specification, correctness/test-quality, security, and performance reviews approved the implementation.

A local comparison with the same runtime, ten pre-migrated synthetic databases, normal schema validation and cap 2 measured 3,000 samples per scenario with zero failures. Hot lookup p50/p95 was 0.042/0.053 ms versus baseline 0.044/0.053 ms; idle tenant cycling was 0.991/1.341 ms versus 1.162/1.575 ms. Both ended with two registered pools. A separate four-worker diagnostic accounted for 200 operations: 126 successes, 74 typed capacity rejections, and no unexpected failures. The baseline accepted all 200 operations. These rejections are the intended all-pools-busy boundary; the shorter lock waits are not a throughput gain or an availability guarantee. These are descriptive local measurements, not a latency guarantee.

Rails removes the handler’s pool configuration before disconnecting it. If disconnect raises, the detached pool is fenced from future model lookups, but physical adapter cleanup after that exception is outside what these tests establish.

Tenant pool admission now measures registered tenant configurations and only
retires idle pools through bounded lock coordination. Model pool lookup stays
behind the creator lock until schema validation succeeds, failed initialization
is fenced, and lifecycle tests cover cross-role capacity, stale registry state,
and concurrent creation and lookup.

Constraint: Rails removes a pool configuration before disconnecting its pool; a disconnect exception can leave an adapter object detached from handler lookup.
Rejected: Reap the least-recently-used pool without checking active leases | this can disconnect a tenant connection in use.
Confidence: high
Scope-risk: moderate
Directive: Recheck the private Rails pool-manager and PoolConfig seams against the minimum, current, and edge Rails appraisals before changing them.
Tested: Rails 8.1.4 bin/ci; full unit and integration suites on Rails 8.1.2.1 and pinned Rails 8.2.0.alpha; seeded tenant pool lifecycle and tenant tests; repeated pending-validation races; actionlint, zizmor, RuboCop.
Not-tested: Physical adapter cleanup after Rails has detached a pool and its disconnect operation raises.
Copilot AI balanced review requested due to automatic review settings October 3, 2026 09:22

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Tenant destruction leaves non-writing role pools registered after dropping the tenant database.

Review effort: Balanced
Findings: 1 High severity

Open (1)
What changed in this PR

Adds concurrency-safe tenant pool capacity enforcement while preserving active leases and rejecting admission when all pools are occupied.

Changes:

  • Introduces tenant-aware pool lifecycle, readiness, retirement, and capacity handling.
  • Validates pool limits and documents locking and compatibility behavior.
  • Adds extensive lifecycle, concurrency, and integration coverage.

[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

File Description
Appraisals Adds minimum Rails appraisal.
gemfiles/​rails_8_1_minimum.gemfile Pins Rails 8.1.2.1 dependencies.
GUIDE.md Documents capacity and locking semantics.
lib/​active_record/​tenanted.rb Defines the capacity error.
lib/​active_record/​tenanted/​database_configurations/​base_config.rb Validates pool limits.
lib/​active_record/​tenanted/​lru.rb Adds snapshot and identity operations.
lib/​active_record/​tenanted/​tenant.rb Coordinates pool admission and retirement.
lib/​active_record/​tenanted/​tenant_connection_pool.rb Implements tenant pool lifecycle safeguards.
lib/​active_record/​tenanted/​tenant_pool_config.rb Installs tenant-aware pools.
test/​integration/​test/​pool_safety_test.rb Covers end-to-end capacity safety.
test/​unit/​database_configurations_test.rb Tests limit validation.
test/​unit/​lru_test.rb Tests new LRU operations.
test/​unit/​tenant_connection_pool_test.rb Tests concurrency and retirement behavior.
test/​unit/​tenant_pool_config_test.rb Tests pool factory behavior.
test/​unit/​tenant_test.rb Expands tenant lifecycle and capacity tests.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +282 to +285
if raw_tenant_connection_pool
pool = raw_tenant_connection_pool
POOL_REGISTRY_LOCK.synchronize do
tenanted_connection_pools.delete_if_same([ current_tenant, current_role ], pool)
@lucianghinda
lucianghinda marked this pull request as draft October 3, 2026 13:03
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.

2 participants