Cache remote config response in memory with TTL and persist to disk#3457
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## toniricodiez/add-remote-config-manager-and-topic-fetcher #3457 +/- ##
============================================================================================
- Coverage 79.54% 79.54% -0.01%
============================================================================================
Files 365 366 +1
Lines 14721 14751 +30
Branches 2007 2013 +6
============================================================================================
+ Hits 11710 11733 +23
- Misses 2198 2202 +4
- Partials 813 816 +3 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
e2f648d to
88436cf
Compare
29cea95 to
ee20e11
Compare
88436cf to
92529c2
Compare
ee20e11 to
ba8adae
Compare
92529c2 to
6052810
Compare
📸 Snapshot Test591 unchanged
🛸 Powered by Emerge Tools |
ajpallares
left a comment
There was a problem hiding this comment.
I didn't take a deep look yet, but an idea came to mind as I was starting to review this. Would it make sense to try to reuse, somehow, the response that the ETagManager already caches? (maybe based on ETagData.lastRefreshTime for the TTL gate). In fact, with the current changes in this PR, wouldn't we be caching the response of the Config endpoint twice (new custom cache + ETagManager's cache)?
Perhaps it's a crazy idea, but may be worth considering
|
Not a crazy idea at all! We are indeed caching the offerings response twice right now for example 😅 (etag + offerings cache). I think you have a point and we could totally do that... But it might be a bigger refactor/behavior change than this approach... As in, the etag cache will become used by things other than networking so we would need to rewire our dependencies... feels cleaner to me to do it separately. Especially considering this response should never actually be too big. Wdyt? |
6052810 to
6083c3f
Compare
ajpallares
left a comment
There was a problem hiding this comment.
I think this looks great! I have one small concern about concurrency calls in updateRemoteConfigIfNeeded. And some other minor comments
6083c3f to
657bcec
Compare
Adds an in-memory cache to RemoteConfigManager keyed off the existing isCacheStale helper (5 min foreground / 25 hr background), so callers within the TTL window short-circuit without hitting the backend. Also adds RemoteConfigDiskCache, which atomically persists the latest successful response to noBackupFilesDir/RevenueCat/remote_config.json. This is currently write-only; a subsequent PR will read it at startup to seed the in-memory cache before the first network refresh. Both caches are populated only when the backend response succeeded AND every per-topic fetch returned null.
657bcec to
79b773f
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 79b773f. Configure here.
79b773f to
02d493a
Compare
8e27a11 to
9a1c9ca
Compare
ajpallares
left a comment
There was a problem hiding this comment.
I have some more comments.
Also, regarding the PR description:
This caches the remote config response both on disk and in memory. Right now the in memory cache is only used to avoid fetching the config multiple times without need.
I believe the memory cache is not used yet in this PR either (since the RemoteConfigManager is not yet used anywhere)?
| import kotlinx.coroutines.suspendCancellableCoroutine | ||
| import java.util.Date | ||
|
|
||
| @OptIn(InternalRevenueCatAPI::class) |
There was a problem hiding this comment.
It is since we use some internal APIs, for example DateProvider.
| if (firstError == null && source != null && tasks.isNotEmpty()) { | ||
| val previousResponse = cache?.response | ||
| cache = CacheEntry(response, dateProvider.now) | ||
| if (previousResponse != response) { | ||
| diskCache.write(response) | ||
| } | ||
| } |
There was a problem hiding this comment.
If we get a null source of empty tasks, we would be keeping the existing cache. Is that correct? (I'm guessing it's better to have a stale cached rather than no cache, but want to be sure)
There was a problem hiding this comment.
I believe so yeah... I would say, a config without tasks feels like an error, so we might as well ignore it and keep what we have if any.
| val deferred = synchronized(refreshLock) { | ||
| inFlightRefresh ?: scope.async(start = CoroutineStart.LAZY) { | ||
| try { | ||
| refresh(appInBackground) | ||
| } finally { | ||
| synchronized(refreshLock) { inFlightRefresh = null } | ||
| } | ||
| }.also { inFlightRefresh = it } | ||
| } | ||
| // LAZY + explicit start: prevents the body's `finally` from clearing `inFlightRefresh` | ||
| // before the `.also` assignment has stored it (possible with a synchronous dispatcher). | ||
| deferred.start() |
There was a problem hiding this comment.
If I'm not mistaken, when appInBackground == true, there's some jitter to the call. Could this deduplication logic mean that a subsequent foreground call to the config may get stalled by the same call triggered while appInBackground? 🤔
Perhaps not very likely though
There was a problem hiding this comment.
That's indeed true, good catch!
There was a problem hiding this comment.
Ended up removing the dedupe logic here and relying on the backend dedupe itself
9a1c9ca to
8f91f3d
Compare
This is indeed correct in this PR, but it's used in the final PRs in this stack which actually use the RemoteConfigManager |
ajpallares
left a comment
There was a problem hiding this comment.
Great job! I think this looks good!
**This is an automatic release.** ## RevenueCat SDK ### 📦 Dependency Updates * [RENOVATE] Update dependency gradle to v8.14.5 (#3459) via RevenueCat Git Bot (@RCGitBot) ## RevenueCatUI SDK ### ✨ New Features * Pre-warm image cache for workflow step states (#3447) via Cesar de la Vega (@vegaro) ### Paywallv2 #### ✨ New Features * Add `close_workflow` button action (#3453) via Cesar de la Vega (@vegaro) #### 🐞 Bugfixes * Fix preload VideoComponent fallback override images (#3449) via Cesar de la Vega (@vegaro) ### 🔄 Other Changes * Select blob source by priority and weighted random (#3458) via Toni Rico (@tonidero) * [AUTOMATIC] Update golden test files for backend integration tests (#3473) via RevenueCat Git Bot (@RCGitBot) * Clean up unreferenced topic files after successful remote-config refresh (#3439) via Toni Rico (@tonidero) * Cache remote config response in memory with TTL and persist to disk (#3457) via Toni Rico (@tonidero) * build(deps): bump fastlane from 2.233.1 to 2.234.0 (#3463) via dependabot[bot] (@dependabot[bot]) * Update codelabs links (#3460) via Jaewoong Eum (@skydoves) * Add RemoteConfigManager and TopicFetcher (#3437) via Toni Rico (@tonidero) * Add exit offers support to workflows (#3452) via Cesar de la Vega (@vegaro) * Update baseline profiles (#3461) via RevenueCat Git Bot (@RCGitBot) * Add network scaffolding for remote config endpoint (#3435) via Toni Rico (@tonidero) * test: cover singleStepFallbackId == initialStepId edge case (#3445) via Facundo Menzella (@facumenzella) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk: this is a release/versioning update (SNAPSHOT -> final) plus docs deployment path changes, with no functional code changes beyond version constants. > > **Overview** > Finalizes the `10.6.0` release by switching all version references from `10.6.0-SNAPSHOT` to `10.6.0` (root `.version`, `gradle.properties`, `Config.frameworkVersion`, and sample/test app `libs.versions.toml` files). > > Updates documentation publishing to point at the `10.6.0` docs path (CircleCI S3 sync target and `docs/index.html` redirect), and prepends the `10.6.0` section to `CHANGELOG.md`/`CHANGELOG.latest.md`. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 4da1697. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->


Description
This caches the remote config response both on disk and in memory. Right now the in memory cache is only used to avoid fetching the config multiple times without need. The disk cache is currently unused but will be used in future PRs.
The idea is to use this cache for the api config to be used to perform other SDK requests.
Note
Medium Risk
Introduces new caching and disk I/O paths in remote config fetching, which can affect refresh behavior and persistence semantics if TTL logic or write conditions are incorrect.
Overview
Adds an in-memory TTL cache to
RemoteConfigManagerso repeatedupdateRemoteConfigIfNeededcalls can short-circuit without hitting the backend when the cached response is still fresh (foreground vs background TTL viaisCacheStale).Introduces
RemoteConfigDiskCacheto atomically persist the latest successfulRemoteConfigResponsetonoBackupFilesDir/RevenueCat/remote_config.json, writing only when at least one topic fetch occurred and the response changed.Tightens
TopicFetcherblob ref validation from “alphanumeric” to a strict 64-char hex SHA-256 format, with expanded tests covering acceptance/rejection cases.Reviewed by Cursor Bugbot for commit 8f91f3d. Bugbot is set up for automated code reviews on this repo. Configure here.