Skip to content

feat: Drop persistent-store cache after FDv2 in-memory store init#167

Merged
tanderson-ld merged 3 commits into
mainfrom
ta/SDK-2225/drop-persistent-cache
May 28, 2026
Merged

feat: Drop persistent-store cache after FDv2 in-memory store init#167
tanderson-ld merged 3 commits into
mainfrom
ta/SDK-2225/drop-persistent-cache

Conversation

@tanderson-ld
Copy link
Copy Markdown
Contributor

@tanderson-ld tanderson-ld commented May 28, 2026

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 DisableableCache capability interface and a disableCache() method on PersistentDataStoreWrapper. The method sets a volatile boolean cacheDisabled flag 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 are LoadingCache instances and plain invalidateAll() would auto-repopulate via the registered CacheLoaders on the next get().

WriteThroughStore.maybeSwitchStore() probes for the interface via instanceof and invokes disableCache() inside the existing synchronized (activeStoreLock) block, immediately after the active read store is flipped to the in-memory store. The probe is unconditional with respect to DataStoreMode: reads bypass the persistent store in both READ_ONLY and READ_WRITE modes after the flip, so the cache is dead weight in either mode.

The PersistentDataStoreBuilder cache-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 @Deprecated annotation; the SDK source has no @Deprecated precedent 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 uses DisableableCache / 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's disable_cache and Ruby's disable_cache, and matches the .NET sibling PR which made the same naming choice.

Concurrency note: an in-flight reader that passed the cacheDisabled check before disableCache() ran can still complete its cache operation and, on a miss, fire the LoadingCache loader. The fresh value gets stored back in the cache after our invalidateAll(). 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-style AtomicReference swap + 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 new MockDisableableCachePersistentStore spy (extending MockTransactionalPersistentStore) drives the WriteThroughStore probe-and-invoke coverage, including the FDv1 init path, the FDv2 apply path, the delta-does-not-redrop case, the READ_ONLY-mode-still-drops case, and the non-disableable-store no-op case. No Thread.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.

PersistentDataStoreWrapper sets a cacheDisabled flag, invalidates item/all/init caches, and skips cache on get, getAll, init, upsert, isInitialized, getCacheStats, and outage recovery so LoadingCache loaders are not repopulated.

WriteThroughStore calls disableCache() once inside maybeSwitchStore() when the first initializing payload flips the active read store to memory (including READ_ONLY and legacy init).

PersistentDataStoreBuilder javadoc 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.

tanderson-ld and others added 3 commits May 28, 2026 10:07
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).
@tanderson-ld tanderson-ld marked this pull request as ready for review May 28, 2026 14:44
@tanderson-ld tanderson-ld requested a review from a team as a code owner May 28, 2026 14:44
@tanderson-ld tanderson-ld merged commit 90f14f1 into main May 28, 2026
23 of 27 checks passed
@tanderson-ld tanderson-ld deleted the ta/SDK-2225/drop-persistent-cache branch May 28, 2026 15:03
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>
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