Repository navigation
fix(querier): evict per-tenant metadata fetchers for inactive tenants - #7573
Merged
SungJin1212 merged 1 commit intoJun 8, 2026
Merged
SungJin1212 merged 1 commit into
SungJin1212 merged 1 commit into
Conversation
friedrichg
approved these changes
May 30, 2026
friedrichg
left a comment
Member
There was a problem hiding this comment.
Thanks!.
Bucket index disabled is not recommended for production. it's very bad in terms of performance.But I am ok fixing bugs in it.
7 of 8 tasks
BucketScanBlocksFinder lazily creates one block.MetadataFetcher per tenant (each with its own Prometheus registry and on-disk meta cache) and caches it in d.fetchers. These entries were never removed, so for a process that has seen N tenants over its lifetime the fetcher map, the aggregated per-tenant Prometheus registries, and the on-disk meta caches all grew unbounded even after tenants were deleted from storage. After every scan, reconcile d.fetchers against the active-user set returned by ScanUsers: evict the fetcher, unregister its per-tenant registry via RemoveUserRegistry, and delete the fetcher's on-disk meta-syncer cache for tenants that are no longer active. The active set comes from a successful ScanUsers call, so reconciliation runs even when individual per-tenant scans fail, keeping the leak bounded under tenant churn; it is skipped only when the context is cancelled. Only the fetcher's own "meta-syncer" cache sub-directory is removed (not the whole CacheDir/<userID> tree) because in single-binary mode CacheDir is the store-gateway's SyncDir, whose block data lives under the same per-tenant directory and must not be deleted here. Fixes cortexproject#7572 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Sandy Chen <Yuxuan.Chen@morganstanley.com>
sandy2008
force-pushed
the
fix-querier-bucket-scan-fetcher-leak
branch
from
June 8, 2026 04:46
84930cb to
c0c5381
Compare
This was referenced Jul 27, 2026
sandy2008
added a commit
to sandy2008/cortex-1
that referenced
this pull request
Oct 29, 2026
…e-gateway With the bucket index disabled and the querier and store-gateway running in the same process sharing -blocks-storage.bucket-store.sync-dir, both components cached block metas under the identical per-tenant path <sync-dir>/<tenant>/meta-syncer/ and deleted that shared state based on their own, different views of which tenants are live: - With store-gateway sharding enabled, deleteLocalFilesForExcludedTenants removes <sync-dir>/<tenant> for every tenant outside the local shard on every sync, permanently defeating the co-located unsharded querier finder's disk cache (every querier restart becomes a cold start). - Since cortexproject#7573 the finder evicts against the active tenant set only and removes <sync-dir>/<tenant>/meta-syncer, while the co-located store-gateway still serves active plus deleting tenants from that very directory. The comment claiming the finder never reaches into a co-located store-gateway's cache was wrong in single-binary mode. The finder now keeps its metas under its own reserved root, <sync-dir>/__querier__/<tenant>/meta-syncer/, so neither component can reach into the other's directories, fixing both directions: - users.QuerierMetaCacheDirName ("__querier__") is defined next to GlobalMarkersDir and reserved the same way __markers__ is: rejected by tenant ID validation and skipped by the users scanner, so nothing can treat it as a tenant. The explicit reservation matters because bucket prefixes are never run through tenant ID validation, and tenant IDs created before the character allowlist could contain characters the allowlist now forbids. - The finder creates fetchers and evicts caches under the reserved root and sweeps its own root against the active set on every scan (the same shape the compactor uses), which also reclaims directories left behind by a previous process for the cost of one ReadDir. - The store-gateway's exclusion cleanup skips the reserved directory. On upgrade the querier lazily re-populates its cache under the new root (a one-time cold meta fetch). Old <sync-dir>/<tenant>/meta-syncer directories are deliberately not migrated or cleaned up: in single-binary mode they are the store-gateway's live cache, and a querier cannot distinguish its own legacy cache from a co-located store-gateway's. Fixes cortexproject#7727 Signed-off-by: Sandy Chen <Yuxuan.Chen@morganstanley.com>
sandy2008
added a commit
to sandy2008/cortex-1
that referenced
this pull request
Oct 29, 2026
…nants on partial scan errors The bucket-scan blocks finder pruned its per-tenant maps (userMetas, userMetasLookup, userDeletionMarks) only when a scan completed with no errors, by replacing the maps wholesale. On the partial-error path the maps were merged via maps.Copy and entries were never deleted, so a single permanently-failing tenant meant no scan ever fully succeeded and entries for every tenant the process had ever seen were retained forever: unbounded memory growth, and GetBlocks kept resolving stale blocks for long-deleted tenants (failing the store-gateway consistency check instead of returning empty results). Since cortexproject#7573 the same partial path already reconciled the metadata fetchers against the active set, so fetcher eviction and metadata pruning diverged. The fetcher eviction is now generalized into reconcileActiveUsers, which reconciles all per-tenant state (the three metadata maps, the fetchers, their Prometheus registries and on-disk caches) against the authoritative active set from ScanUsers in one place, on every scan regardless of per-tenant errors. Reconciling against the active set makes the partial-error path converge on what the fully-successful path already does: scan jobs are only ever scheduled for active tenants, so a wholesale replacement never contains entries for tenants in the deleting state either. Nothing is pruned when ScanUsers itself fails (no authoritative set) or when the context is cancelled during shutdown. Fixes cortexproject#7728 Signed-off-by: Sandy Chen <Yuxuan.Chen@morganstanley.com>
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.
What this PR does
Fixes #7572.
BucketScanBlocksFinder(the querier's block discovery used when the bucket index is disabled) lazily creates oneblock.MetadataFetcherper tenant and caches it ind.fetchers. Each fetcher owns a per-tenantprometheus.Registry(registered into the aggregatingMetadataFetcherMetricsviaAddUserRegistry) and an on-disk meta cache under<sync-dir>/<userID>/meta-syncer/. These entries were never removed, so over a process's lifetime the fetcher map, the per-tenant Prometheus registries, and the on-disk meta caches grew unbounded as tenants were deleted from storage — even thoughscanBucketalready prunes the block-metadata maps for deleted tenants.Fix
After each scan,
scanBucketreconcilesd.fetchersagainst the active-user set returned byScanUsers:delete()it from the map;MetadataFetcherMetrics.RemoveUserRegistry(this already existed — the querier just never called it);meta-syncercache.Design notes (surfaced and hardened during review):
ScanUsers(the scan returns early if that fails), so a tenant being evicted is by construction absent fromresErrs. Eviction therefore runs on every scan where the context isn't cancelled — not only on fully-successful scans — keeping the leak bounded even under churn with transient per-tenant errors.CacheDiris the store-gateway'sSyncDir, whose block data lives at<sync-dir>/<userID>/<blockID>/. The cleanup therefore removes only the fetcher's own<userID>/meta-syncersub-directory (plus a best-effortos.Removeof the now-empty parent), never the per-tenant tree, provably preserving co-located block data.bucket_stores.go:RemoveUserRegistry+deleteLocalFilesForExcludedTenants). The bucket-index blocks finder is unaffected — it already idle-prunes its per-tenant loaders.Tests
Added unit tests covering fetcher-map eviction, per-tenant Prometheus registry removal (the
cortex_blocks_meta_syncedgauge series drop to zero), on-diskmeta-syncerremoval, preservation of co-located store-gateway block data under the same per-tenant directory, multi-tenant selectivity + re-creation of a returning tenant, and eviction continuing to run when another tenant's scan errors. All pass under-race.Out of scope (potential follow-ups)
userMetas/userMetasLookup/userDeletionMarksare merged (maps.Copy) rather than pruned, so block metadata for tenants that disappear while some other tenant keeps failing to scan is retained until the next fully-successful scan. This is pre-existing and is not a registry/heap leak.Checklist
CHANGELOG.mdupdated ([BUGFIX] Querier)-racegofmt/go vetcleanmake doc— not needed (no flag/config changes)AI assistance disclosure
Per the Generative AI Contribution Policy: this change was developed with AI assistance (Claude Code), including a multi-round review pass. I have reviewed and validated every line, the root-cause analysis, and the tests, and take full responsibility for the contribution.
🤖 Generated with Claude Code