feat: Drop persistent-store cache after FDv2 in-memory store init#167
Merged
Conversation
Once the FDv2 in-memory store takes over as the active read source, the persistent-store wrapper's three Guava caches are no longer consulted on reads and roughly double the in-memory footprint of flag data (or worse, indefinitely, in cacheForever() mode). Add an internal DisableableCache capability interface implemented by PersistentDataStoreWrapper. The wrapper's disableCache() sets a volatile cacheDisabled flag and invalidates all three Guava caches. Every cache touch site (isInitialized, init, get, getAll, upsert, getCacheStats, pollAvailabilityAfterOutage) checks the flag and short-circuits to the core when set; this is required because the caches are LoadingCache instances and plain invalidation would auto-repopulate via the registered CacheLoaders on the next get(). WriteThroughStore.maybeSwitchStore() probes for the interface and invokes disableCache() inside the existing synchronized block, after the active read store has been flipped to the in-memory store. The PersistentDataStoreBuilder cache-config setters (cacheTime, cacheSeconds, cacheMillis, cacheForever, noCaching, staleValuesPolicy, recordCacheStats) remain functional during the bootstrap window for backward compatibility; class-level javadoc explains that under FDv2 they only govern that window. Naming note: the capability is named disableCache rather than the ticket's clearCache so the verb conveys that the cache is off going forward, not just emptied. This aligns with Python and Ruby's disable_cache and matches the dotnet sibling PR. Mirrors dotnet-core#274 (.NET), launchdarkly/python-server-sdk#426 (Python), launchdarkly/ruby-server-sdk#384 (Ruby), and launchdarkly/go-server-sdk#373 (Go).
keelerm84
approved these changes
May 28, 2026
tanderson-ld
pushed a commit
that referenced
this pull request
May 28, 2026
🤖 I have created a release *beep* *boop* --- ## [7.14.0](launchdarkly-java-server-sdk-7.13.4...launchdarkly-java-server-sdk-7.14.0) (2026-05-28) ### Features * add X-LaunchDarkly-Instance-Id header (SDK-2356) ([#162](#162)) ([a363777](a363777)) * Drop persistent-store cache after FDv2 in-memory store init ([#167](#167)) ([90f14f1](90f14f1)) ### Bug Fixes * honor FDv1 fallback directive during initializer phase ([#158](#158)) ([b0a3957](b0a3957)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > No runtime code in the diff—only version strings and changelog—so review risk is limited to verifying the tagged commits match the release notes. > > **Overview** > **Release Please** bumps **launchdarkly-java-server-sdk** from **7.13.4** to **7.14.0** in `.release-please-manifest.json`, `lib/sdk/server/gradle.properties`, `Version.SDK_VERSION`, and adds the **7.14.0** section to `CHANGELOG.md`. > > The diff is **version and changelog metadata only**; the listed product changes ship in the already-merged commits this release tags: **`X-LaunchDarkly-Instance-Id`** on outbound HTTP, dropping the persistent-store cache after FDv2 in-memory init, and honoring **FDv1 fallback** during the initializer phase. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 594728e. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.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.
Summary
With FDv2, the in-memory store retains every flag and segment once it has received a full payload. The persistent-store wrapper's three internal Guava caches (item, all-items, init) become dead weight at that point, roughly doubling the in-memory footprint of flag data (or worse, indefinitely, in
cacheForever()mode).This change introduces an internal
DisableableCachecapability interface and adisableCache()method onPersistentDataStoreWrapper. The method sets avolatile boolean cacheDisabledflag and then invalidates all three Guava caches. Every cache touch site (isInitialized,init,get,getAll,upsert,getCacheStats,pollAvailabilityAfterOutage) checks the flag and short-circuits to the core path when set. The flag-check is required because the caches areLoadingCacheinstances and plaininvalidateAll()would auto-repopulate via the registeredCacheLoaders on the nextget().WriteThroughStore.maybeSwitchStore()probes for the interface viainstanceofand invokesdisableCache()inside the existingsynchronized (activeStoreLock)block, immediately after the active read store is flipped to the in-memory store. The probe is unconditional with respect toDataStoreMode: reads bypass the persistent store in bothREAD_ONLYandREAD_WRITEmodes after the flip, so the cache is dead weight in either mode.The
PersistentDataStoreBuildercache-config setters (noCaching,cacheTime,cacheMillis,cacheSeconds,cacheForever,staleValuesPolicy,recordCacheStats) remain functional during the bootstrap window for backward compatibility. Class-level javadoc on the builder explains that under FDv2 these only govern the window before the in-memory store has received its first payload. No@Deprecatedannotation; the SDK source has no@Deprecatedprecedent and Matthew Keeler's pattern across Python, Ruby, and Go uses doc-only deprecation.Mirrors the same change shipped for Python (launchdarkly/python-server-sdk#426), Ruby (launchdarkly/ruby-server-sdk#384), Go (launchdarkly/go-server-sdk#373), and .NET (launchdarkly/dotnet-core#274).
Naming note: the ticket text says
CacheClearable/clearCache(). This change usesDisableableCache/disableCache()instead so the verb conveys that the cache is off going forward (a state change), not just emptied (a one-time imperative). This aligns with Python'sdisable_cacheand Ruby'sdisable_cache, and matches the.NETsibling PR which made the same naming choice.Concurrency note: an in-flight reader that passed the
cacheDisabledcheck beforedisableCache()ran can still complete its cache operation and, on a miss, fire theLoadingCacheloader. The fresh value gets stored back in the cache after ourinvalidateAll(). Those leftover entries are unreachable from any subsequent read (every future caller bypasses) and hold fresh, correct values, so they don't cause stale reads or torn observations. They simply persist in the cache instance until GC reclaims it. See dotnet-core#274 for the full analysis of this trade-off vs. a Go-styleAtomicReferenceswap + read-once-per-call refactor.Tests follow the indirect-observation pattern: prime the cache, call
disableCache(), mutate the core directly, assert the next read sees the new value. A newMockDisableableCachePersistentStorespy (extendingMockTransactionalPersistentStore) drives theWriteThroughStoreprobe-and-invoke coverage, including the FDv1initpath, the FDv2applypath, the delta-does-not-redrop case, theREAD_ONLY-mode-still-drops case, and the non-disableable-store no-op case. NoThread.sleep../gradlew test-- 1906 tests, 0 failures.Note
Medium Risk
Changes the server SDK persistent-store read/write path at the FDv2 handoff; behavior is guarded by tests and matches other language SDKs, but incorrect timing could affect bootstrap reads.
Overview
Adds an internal
DisableableCache/disableCache()path so the persistent data store wrapper stops using its Guava caches after FDv2 hands reads to the in-memory store.PersistentDataStoreWrappersets acacheDisabledflag, invalidates item/all/init caches, and skips cache onget,getAll,init,upsert,isInitialized,getCacheStats, and outage recovery soLoadingCacheloaders are not repopulated.WriteThroughStorecallsdisableCache()once insidemaybeSwitchStore()when the first initializing payload flips the active read store to memory (includingREAD_ONLYand legacyinit).PersistentDataStoreBuilderjavadoc notes cache options only matter during the bootstrap window before that switch. New unit tests cover bypass behavior and the one-time probe.Reviewed by Cursor Bugbot for commit 6ca393f. Bugbot is set up for automated code reviews on this repo. Configure here.