fix(spanner): bound the location-aware routing caches (P0-5)#8
Open
fornwall wants to merge 2 commits into
Open
fix(spanner): bound the location-aware routing caches (P0-5)#8fornwall wants to merge 2 commits into
fornwall wants to merge 2 commits into
Conversation
The per-database KeyRangeCache grew without bound: the eviction method shrinkTo(int) had no production callers, so the ranges TreeMap and groups map accumulated an entry for every distinct range the server ever reported. Wire shrinkTo to a 10k-range cap (with 10% hysteresis to amortize the O(size) eviction cost) at the end of addRanges. The per-database ChannelFinder map relied on SoftReference pressure as its eviction policy, so the unbounded caches above filled the heap and caused full-GC thrash before being dropped wholesale; stale references were also only reaped every 1024th lookup. Replace it with a strongly referenced Guava cache bounded by size (100 finders, LRU) and expire-after-access (30 minutes). Evicted finders are marked stale so an in-flight call that still references one cannot re-register endpoints with the lifecycle manager after unregisterFinder has run; the active-address publish now happens under the finder's update lock to make that ordering airtight. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QkZzotf4wMQKXEhEpTLRKL
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QkZzotf4wMQKXEhEpTLRKL
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.
Issue
P0-5 — Unbounded routing-cache memory growth (per-finder cache +
SoftReferencefinder map), from the Spanner Java client review. Two coupled defects across the two layers of the location-aware routing cache produced a single failure mode: routing memory grows without bound until the heap fills.KeyRangeCachewas never trimmed. TherangesTreeMap andgroupsmap grow on every serverCacheUpdate; the eviction methodshrinkTo(int)had zero production callers (confirmed — only the declaration and a unit test existed). Scan-heavy or wide-key workloads accumulate an entry (~200–400 B) per distinct range the server ever reports, cleared only when the database id changes.ChannelFindermap usedSoftReferencepressure as its eviction policy. With (1), each per-database finder grows until the heap is nearly exhausted, triggers full-GC thrash, is dropped wholesale, then rebuilt. Independently, stale references were only reaped every 1024thgetOrCreateChannelFindercall, so finders for no-longer-accessed databases lingered.One correction to the review entry: the finder's recipe caches (
KeyRecipeCache) were already bounded (Guava caches, 1000 entries each) — the unbounded per-finder state was theKeyRangeCachealone.Fix
KeyRangeCache:addRangesnow calls the existing sampled-LRUshrinkTowhen the cache exceeds a 10,000-range cap (DEFAULT_MAX_RANGES), shrinking 10% below the cap so the O(size) eviction cost is amortized over many insertions.KeyAwareChannel: the per-database finder map is now a strongly referenced Guava cache bounded by size (100 finders, LRU) and expire-after-access (30 minutes, driven by the same ticker as the transaction-affinity cache so tests can control it). The removal listener marks the evicted finder stale and callsEndpointLifecycleManager.unregisterFinder.shutdown()/shutdownNow()invalidate all finders.ChannelFinder: newmarkStale(). A stale finder drops async cache updates and stops publishing active addresses. The active-address publish now happens while holding the finder'supdateLock, which makes the eviction ordering airtight: a concurrent publish either completes beforemarkStale()returns (and is then superseded by the finder-generation bump in the subsequentunregisterFinder) or observes the stale flag and skips publishing. Without this, an in-flight call still holding an evicted finder could re-register endpoints under a dead finder key, leaking them permanently.Eviction of a finder that in-flight calls still reference is safe: routing lookups on it are read-only, and its dropped cache just means those calls fall back to the default channel until the cache is re-learned by a fresh finder.
Tests
KeyRangeCacheTest.addRangesEvictsWhenOverMaxRanges— the cache never exceeds the cap during sustainedaddRangestraffic, stays within the hysteresis band, keeps the most recently added range resident, and every resident range still routes to its own server.ChannelFinderTest.markStaleDropsAsyncUpdates/markStaleStopsPublishingActiveAddressesToLifecycleManager.KeyAwareChannelTest.channelFinderExpiresAfterInactivity— end-to-end: a learned route works, then after 31 ticker-minutes of inactivity the finder (and its routing cache) is gone and the query falls back to the default channel.Full
google-cloud-spannermodule suite: 9778 tests, 0 failures (JDK 21).fmt:checkclean.🤖 Generated with Claude Code
https://claude.ai/code/session_01QkZzotf4wMQKXEhEpTLRKL