Commit 90f14f1
authored
feat: Drop persistent-store cache after FDv2 in-memory store init (#167)
## 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 `CacheLoader`s 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.
<!-- CURSOR_SUMMARY -->
---
> [!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.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
6ca393f. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->1 parent a363777 commit 90f14f1
6 files changed
Lines changed: 270 additions & 12 deletions
File tree
- lib/sdk/server/src
- main/java/com/launchdarkly/sdk/server
- integrations
- test/java/com/launchdarkly/sdk/server
Lines changed: 18 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
Lines changed: 30 additions & 11 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
43 | 43 | | |
44 | 44 | | |
45 | 45 | | |
46 | | - | |
| 46 | + | |
47 | 47 | | |
48 | 48 | | |
49 | 49 | | |
| |||
54 | 54 | | |
55 | 55 | | |
56 | 56 | | |
57 | | - | |
| 57 | + | |
58 | 58 | | |
59 | 59 | | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
60 | 66 | | |
61 | 67 | | |
62 | 68 | | |
| |||
151 | 157 | | |
152 | 158 | | |
153 | 159 | | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
154 | 172 | | |
155 | 173 | | |
156 | 174 | | |
157 | 175 | | |
158 | 176 | | |
159 | 177 | | |
160 | 178 | | |
161 | | - | |
| 179 | + | |
162 | 180 | | |
163 | 181 | | |
164 | 182 | | |
| |||
187 | 205 | | |
188 | 206 | | |
189 | 207 | | |
190 | | - | |
| 208 | + | |
191 | 209 | | |
192 | 210 | | |
193 | 211 | | |
| |||
228 | 246 | | |
229 | 247 | | |
230 | 248 | | |
231 | | - | |
| 249 | + | |
232 | 250 | | |
233 | 251 | | |
234 | 252 | | |
| |||
242 | 260 | | |
243 | 261 | | |
244 | 262 | | |
245 | | - | |
| 263 | + | |
246 | 264 | | |
247 | 265 | | |
248 | 266 | | |
| |||
281 | 299 | | |
282 | 300 | | |
283 | 301 | | |
284 | | - | |
| 302 | + | |
285 | 303 | | |
286 | 304 | | |
287 | 305 | | |
| |||
297 | 315 | | |
298 | 316 | | |
299 | 317 | | |
300 | | - | |
| 318 | + | |
301 | 319 | | |
302 | 320 | | |
303 | 321 | | |
| |||
340 | 358 | | |
341 | 359 | | |
342 | 360 | | |
343 | | - | |
| 361 | + | |
344 | 362 | | |
345 | 363 | | |
346 | 364 | | |
| |||
443 | 461 | | |
444 | 462 | | |
445 | 463 | | |
446 | | - | |
447 | | - | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
448 | 467 | | |
449 | 468 | | |
450 | 469 | | |
| |||
Lines changed: 3 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
149 | 149 | | |
150 | 150 | | |
151 | 151 | | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
152 | 155 | | |
153 | 156 | | |
154 | 157 | | |
| |||
Lines changed: 10 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
35 | | - | |
| 35 | + | |
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
39 | 48 | | |
40 | 49 | | |
41 | 50 | | |
| |||
Lines changed: 110 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
35 | 35 | | |
36 | 36 | | |
37 | 37 | | |
| 38 | + | |
38 | 39 | | |
39 | 40 | | |
40 | 41 | | |
| |||
713 | 714 | | |
714 | 715 | | |
715 | 716 | | |
| 717 | + | |
| 718 | + | |
| 719 | + | |
| 720 | + | |
| 721 | + | |
| 722 | + | |
| 723 | + | |
| 724 | + | |
| 725 | + | |
| 726 | + | |
| 727 | + | |
| 728 | + | |
| 729 | + | |
| 730 | + | |
| 731 | + | |
| 732 | + | |
| 733 | + | |
| 734 | + | |
| 735 | + | |
| 736 | + | |
| 737 | + | |
| 738 | + | |
| 739 | + | |
| 740 | + | |
| 741 | + | |
| 742 | + | |
| 743 | + | |
| 744 | + | |
| 745 | + | |
| 746 | + | |
| 747 | + | |
| 748 | + | |
| 749 | + | |
| 750 | + | |
| 751 | + | |
| 752 | + | |
| 753 | + | |
| 754 | + | |
| 755 | + | |
| 756 | + | |
| 757 | + | |
| 758 | + | |
| 759 | + | |
| 760 | + | |
| 761 | + | |
| 762 | + | |
| 763 | + | |
| 764 | + | |
| 765 | + | |
| 766 | + | |
| 767 | + | |
| 768 | + | |
| 769 | + | |
| 770 | + | |
| 771 | + | |
| 772 | + | |
| 773 | + | |
| 774 | + | |
| 775 | + | |
| 776 | + | |
| 777 | + | |
| 778 | + | |
| 779 | + | |
| 780 | + | |
| 781 | + | |
| 782 | + | |
| 783 | + | |
| 784 | + | |
| 785 | + | |
| 786 | + | |
| 787 | + | |
| 788 | + | |
| 789 | + | |
| 790 | + | |
| 791 | + | |
| 792 | + | |
| 793 | + | |
| 794 | + | |
| 795 | + | |
| 796 | + | |
| 797 | + | |
| 798 | + | |
| 799 | + | |
| 800 | + | |
| 801 | + | |
| 802 | + | |
| 803 | + | |
| 804 | + | |
| 805 | + | |
| 806 | + | |
| 807 | + | |
| 808 | + | |
| 809 | + | |
| 810 | + | |
| 811 | + | |
| 812 | + | |
| 813 | + | |
| 814 | + | |
| 815 | + | |
| 816 | + | |
| 817 | + | |
| 818 | + | |
| 819 | + | |
| 820 | + | |
| 821 | + | |
| 822 | + | |
| 823 | + | |
| 824 | + | |
| 825 | + | |
716 | 826 | | |
717 | 827 | | |
718 | 828 | | |
| |||
0 commit comments