fix: reclaim idle shared tenant cache groups - #809
Conversation
📝 WalkthroughWalkthroughTenant backend loading now uses releasable acquisitions. Shared database handles use generation-aware leases, activity refresh, and coordinated idle reaping. Server proxy handling and lifecycle tests were updated for the new pool API and cleanup behavior. ChangesTenant pool lease lifecycle
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant S3Proxy
participant Pool
participant SharedDB
Client->>S3Proxy: Request tenant S3 path
S3Proxy->>Pool: LoadS3Backend
Pool->>SharedDB: Acquire shared handle lease
SharedDB-->>Pool: Backend and release callback
Pool-->>S3Proxy: Backend and release callback
S3Proxy-->>Client: Serve S3 request
S3Proxy->>Pool: Release backend
Possibly related issues
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 00915be970
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
This PR refines the tenant backend pool’s caching semantics for shared-schema tenants by separating foreground (request-path) access from background work, and by reclaiming cached shared-DB tenant groups when their physical shared DB has been idle from a foreground perspective for a fixed TTL.
Changes:
- Introduces
AcquireForegroundandAcquireForWork(withAcquireas a foreground compatibility alias) to prevent background work from promoting capacity-LRU entries or extending shared physical-DB foreground activity. - Adds shared physical-DB “foreground-idle” reaping that cascades retirement of cached tenant entries for an expired shared DB, then closes the shared handle after refs drain.
- Updates request authentication and background workers to use the appropriate acquisition mode, and extends pool tests to cover the new behaviors.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| pkg/tenant/pool.go | Adds access-kind-aware acquisition paths and shared-DB foreground-idle cascade reaping/handle cleanup. |
| pkg/tenant/pool_test.go | Updates and adds tests for work-vs-foreground LRU behavior and shared-DB foreground-idle reaping cascades. |
| pkg/server/tenant_worker.go | Switches kick-driven background tenant work to AcquireForWork. |
| pkg/server/object_gc_worker.go | Switches object-GC candidate processing to AcquireForWork. |
| pkg/server/auth.go | Switches request-path auth acquisition to AcquireForeground. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
mornyx
left a comment
There was a problem hiding this comment.
Careful review pass — the lifecycle split is correct and complete. Verified against the PR head:
- Locking is sound. The documented order
p.mu -> p.sharedMuholds at every site (acquire hit/insert paths,releaseEntry, the cascade revalidation).removeLockednever touchessharedMu, so the cascade's double-lock section cannot deadlock;closeEntry -> releaseSharedDBruns after both locks are released. The two-phase candidate-then-revalidate design closes the race where a foreground acquire refreshes the physical timestamp mid-cascade — everytouchSharedDBForegroundLockedcall site holdsp.mu, which the revalidation phase blocks on. - Caller classification is complete. Grepped all non-test
Acquirecall sites: auth middleware (API-key + capability) usesAcquireForeground;tenantWorkerManager.targetForRefand the object-GC worker useAcquireForWork; the safety-net scan keepsAcquireCached(correctly refresh-free).Get/S3Backend/LoadS3Backendonly serve the local mock-S3 endpoint (cfg.S3Dir), so no production foreground path bypasses the foreground timestamp. - Retired-but-active entries stay usable until refs drain (covered by
TestSharedDBIdleReapDefersActiveEntryClose), and the physical handle closes only after both the foreground TTL expiry and reference drain — the reap order inreapOnce(cascade, thenreapIdleSharedDBs) lets both happen in one tick. - Standalone tenants are unaffected:
sharedDBID == 0skips the touch, the cascade only iterates real handle IDs, and work access still refreshes the standalone idle TTL.
Tests: all pool lifecycle tests pass locally, including a -race run over the cascade/foreground-activity/work-LRU tests; CI green.
Two non-blocking operational notes inline. LGTM.
|
Reviewed the full change plus the downstream call graph ( 1. An expired handle can be closed while a cold open is in flight
Probe result: the handle is Before this PR the handle lookup refreshed the timestamp, so the reaper could not see an expired window while a cold open was resolving. That protection is what the change removes. 2. The same race leaks
|
00915be to
d805f44
Compare
|
Addressed the full review in d805f44 after rebasing onto merged #808. The two reproduced correctness races are fixed with a generation-specific physical-handle lease acquired under sharedMu and transferred into cached-entry ownership; cold-open failures and duplicate losers release it, and stale releases cannot affect a reopened handle. The long cascade no longer holds sharedMu during the LRU scan, and removal metrics are emitted outside the request-path lock. I also removed the foreground/work split plus the obsolete unpinned Get and S3Backend paths: all demand-driven request, worker, object-GC, and local-S3 use now goes through Acquire, while only the periodic safety-net uses warm-only AcquireCached. Local S3 is pinned for the full handler lifetime. The physical idle TTL is a fixed 10 minutes, yielding roughly 10-12 minute cleanup with the existing reaper. Full pkg/tenant, affected pkg/server tests, focused race tests, and diff checks pass locally; new-head CI is running. |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
pkg/tenant/pool_test.go (1)
1604-1840: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valuePrefer
t.Errorffor the assertion failures in these new tests.Lines 1632, 1704, 1748, 1801, and 1837 use
t.Fatal/t.Fatalffor assertions rather than setup failures, which aborts before the remaining store/handle checks in the same test can report. As per coding guidelines: "Uset.Fatalort.Fatalffor setup failures andt.Errorffor assertion failures."♻️ Example for Line 1703-1705
if firstCached || secondCached || !otherCached { - t.Fatalf("cache after shared cascade: first=%t second=%t other=%t, want false false true", firstCached, secondCached, otherCached) + t.Errorf("cache after shared cascade: first=%t second=%t other=%t, want false false true", firstCached, secondCached, otherCached) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pkg/tenant/pool_test.go` around lines 1604 - 1840, Update the assertion failures in TestAcquireCountsBackgroundWorkAsCapacityLRUActivity, TestSharedDBIdleReapCascadesTenantEntries, TestSharedDBIdleReapDefersActiveEntryClose, TestAcquireRefreshesSharedDBActivity, and TestAcquireRefreshesStandaloneIdleTTL to use t.Errorf or t.Errorf-style reporting instead of t.Fatal/t.Fatalf. Keep t.Fatal/t.Fatalf for setup failures such as Acquire, sql.Open, and Ping errors.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@pkg/tenant/pool.go`:
- Around line 878-896: The LoadS3Backend path currently acquires tenant backends
before the local S3 handler authenticates requests. Update LoadS3Backend and its
/s3/ integration so backend loading is warm-only or otherwise gated by
successful tenant/S3 authentication, ensuring unauthenticated cold requests
cannot open a tenant backend while preserving authenticated S3 operations.
---
Nitpick comments:
In `@pkg/tenant/pool_test.go`:
- Around line 1604-1840: Update the assertion failures in
TestAcquireCountsBackgroundWorkAsCapacityLRUActivity,
TestSharedDBIdleReapCascadesTenantEntries,
TestSharedDBIdleReapDefersActiveEntryClose,
TestAcquireRefreshesSharedDBActivity, and TestAcquireRefreshesStandaloneIdleTTL
to use t.Errorf or t.Errorf-style reporting instead of t.Fatal/t.Fatalf. Keep
t.Fatal/t.Fatalf for setup failures such as Acquire, sql.Open, and Ping errors.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 5208f5f5-385a-4d5a-9933-c26581a27256
📒 Files selected for processing (4)
pkg/server/leader_lifecycle_test.gopkg/server/server.gopkg/tenant/pool.gopkg/tenant/pool_test.go
Re-review of the revised version (
|
mornyx
left a comment
There was a problem hiding this comment.
Re-reviewed the new head after the redesign (5609f37f9..3ff728a22). The lease-based ownership model is a strict improvement over both the original and my previously approved revision:
- Unified
Acquirewith all activity counting removes the foreground/work split — my earlier note about background single-use churn on a saturated cache is now moot, and the entry-levellastUsedsemantics are much easier to reason about. - The generation-safe
sharedDBHandleLeasefixes three real latent bugs: a delayed release can no longer decrement refs of a reopened handle generation (the oldreleaseSharedDB(dbID)had no generation check);PurgeSharedTenantnow pins the handle across its batched deletes instead of using an unpinned raw handle; and an in-flight cold open holds a lease, so the 10-minute reaper can't close a handle mid-open. LoadS3Backendgoing warm-only with a pinned release closes the two holes in the old path: unauthenticated presigned-URL requests can no longer trigger a cold open, and the returned backend is pinned untilrelease()instead of being closeable underfoot by eviction/cascade.- Cascade reaper changes check out: releasing
sharedMubefore the LRU walk keeps unrelated handle opens unblocked, while the expiry revalidation still happens underp.muso no activity touch can interleave between revalidation and retirement (everytouchSharedDBActivityLockedcaller holdsp.mu). A new acquire landing between the cascade andreapIdleSharedDBsre-leases and refresheslastUsed, so the handle correctly survives the same reap tick. - Lock order
p.mu -> p.sharedMuholds at every site;closeEntryreleases backend -> store -> lease in the right order; the double-create loser path releases all three.
Verified locally: full pkg/tenant suite, pkg/server leader-lifecycle + shared-DB tests, -race over the new lease/cascade/activity tests and TestLeaderGatedWorkersCloseRacesOnLead; CI green.
Two non-blocking notes inline. LGTM.
| } | ||
|
|
||
| func (p *Pool) LoadS3Backend(ctx context.Context, metaStore *meta.Store, tenantID string) (out *backend.Dat9Backend) { | ||
| // LoadS3Backend resolves tenant metadata and pins an already-warm backend for |
There was a problem hiding this comment.
Worth calling out in the deploy note: LoadS3Backend is now warm-only, so a local mock-S3 presigned-URL fetch for a tenant whose entry has expired returns 404 instead of lazily cold-opening. That is strictly better (an unauthenticated URL should not trigger a cold open, and the pin-until-release semantics are now correct), but any local tooling that fetches URLs long after the upload request must expect to re-warm the tenant through the API first.
| defaultTenantPoolMaxTenants = 1024 | ||
| defaultTenantPoolIdleReapInterval = 2 * time.Minute | ||
| defaultSharedDBHandleIdleTTL = 30 * time.Minute | ||
| defaultSharedDBHandleIdleTTL = 10 * time.Minute |
There was a problem hiding this comment.
Non-blocking: with the TTL shortened 30m -> 10m and sharedDBHandle leasing itself counting as activity, a physical group with only sporadic traffic (less than one acquire/lease per ~10 min) will now be fully reclaimed each window and pay a cold reopen plus per-tenant cold opens on the first touch of the next window. Expected given the reclamation goal — just note that drive9_shared_db_pool_cache_handles will show this sawtooth for low-traffic orgs, which is distinguishable from a regression only if you know to look for it.
Summary
Acquirefor all real activity andAcquireCachedfor the warm-only safety-net scanAcquireCachednever cold-opens, promotes the capacity LRU, or refreshes tenant/physical idle activity. There is no separate foreground/work state machine: background work is demand-driven real use, while the periodic safety-net observation remains explicitly warm-only.Local mock-S3 requests also cannot cold-open because those development presigned URLs do not carry independent tenant authentication. An already-warm hit is promoted and refreshed as real activity, then pinned until the HTTP handler completes.
No schema, environment variable, durable state, per-tenant TTL, or new goroutine is added. The shared cache capacity default remains 20,480. Obsolete unpinned
Get/S3Backendaccessors were removed so new callers cannot bypass the two-path ownership model.Safety
sharedDBRefscounter used by cached tenant ownership before the handle leavessharedMudb_pool.idand the exact*sql.DBgeneration, so a delayed release after invalidation cannot decrement a reopened handlep.mu -> p.sharedMu, then the potentially 20,480-entry LRU scan runs with onlyp.muVerification
GOCACHE=/tmp/drive9-go-cache go test ./pkg/tenant -count=1GOCACHE=/tmp/drive9-go-cache go test ./pkg/server -run 'Test.*(TenantWorker|ObjectGC|Auth|S3|LeaderGatedWorkers)' -count=1GOCACHE=/tmp/drive9-go-cache go test -race ./pkg/tenant -run 'TestSharedDBHandleLease|TestSharedDBIdleReap|TestAcquireCached|TestAcquireCounts|TestAcquireRefreshes|TestLoadS3Backend' -count=1git diff --check