Skip to content

Cache remote config response in memory with TTL and persist to disk#3457

Merged
tonidero merged 2 commits into
mainfrom
toniricodiez/cache-remote-config-response
May 12, 2026
Merged

Cache remote config response in memory with TTL and persist to disk#3457
tonidero merged 2 commits into
mainfrom
toniricodiez/cache-remote-config-response

Conversation

@tonidero
Copy link
Copy Markdown
Contributor

@tonidero tonidero commented May 8, 2026

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 RemoteConfigManager so repeated updateRemoteConfigIfNeeded calls can short-circuit without hitting the backend when the cached response is still fresh (foreground vs background TTL via isCacheStale).

Introduces RemoteConfigDiskCache to atomically persist the latest successful RemoteConfigResponse to noBackupFilesDir/RevenueCat/remote_config.json, writing only when at least one topic fetch occurred and the response changed.

Tightens TopicFetcher blob 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.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 8, 2026

Codecov Report

❌ Patch coverage is 78.12500% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.54%. Comparing base (8e9c782) to head (e2f648d).

Files with missing lines Patch % Lines
...hases/common/remoteconfig/RemoteConfigDiskCache.kt 71.42% 4 Missing and 2 partials ⚠️
...rchases/common/remoteconfig/RemoteConfigManager.kt 90.90% 0 Missing and 1 partial ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tonidero tonidero marked this pull request as ready for review May 8, 2026 10:44
@tonidero tonidero requested a review from a team as a code owner May 8, 2026 10:44
@tonidero tonidero force-pushed the toniricodiez/cache-remote-config-response branch from e2f648d to 88436cf Compare May 8, 2026 11:27
@tonidero tonidero force-pushed the toniricodiez/add-remote-config-manager-and-topic-fetcher branch 2 times, most recently from 29cea95 to ee20e11 Compare May 8, 2026 12:09
@tonidero tonidero force-pushed the toniricodiez/cache-remote-config-response branch from 88436cf to 92529c2 Compare May 8, 2026 12:09
@tonidero tonidero force-pushed the toniricodiez/add-remote-config-manager-and-topic-fetcher branch from ee20e11 to ba8adae Compare May 8, 2026 13:48
@tonidero tonidero force-pushed the toniricodiez/cache-remote-config-response branch from 92529c2 to 6052810 Compare May 8, 2026 13:48
@emerge-tools
Copy link
Copy Markdown

emerge-tools Bot commented May 8, 2026

📸 Snapshot Test

591 unchanged

Name Added Removed Modified Renamed Unchanged Errored Approval
TestPurchasesUIAndroidCompatibility Paparazzi
com.revenuecat.testpurchasesuiandroidcompatibility.paparazzi
0 0 0 0 257 0 N/A
TestPurchasesUIAndroidCompatibility
com.revenuecat.testpurchasesuiandroidcompatibility
0 0 0 0 334 0 N/A

🛸 Powered by Emerge Tools

Copy link
Copy Markdown
Member

@ajpallares ajpallares left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@tonidero
Copy link
Copy Markdown
Contributor Author

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?

@tonidero tonidero force-pushed the toniricodiez/cache-remote-config-response branch from 6052810 to 6083c3f Compare May 11, 2026 08:40
Copy link
Copy Markdown
Member

@ajpallares ajpallares left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this looks great! I have one small concern about concurrency calls in updateRemoteConfigIfNeeded. And some other minor comments

@tonidero tonidero force-pushed the toniricodiez/cache-remote-config-response branch from 6083c3f to 657bcec Compare May 11, 2026 11:37
Base automatically changed from toniricodiez/add-remote-config-manager-and-topic-fetcher to main May 11, 2026 12:27
@circleci-app circleci-app Bot requested a review from a team as a code owner May 11, 2026 12:27
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.
@tonidero tonidero force-pushed the toniricodiez/cache-remote-config-response branch from 657bcec to 79b773f Compare May 11, 2026 13:16
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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.

@tonidero tonidero force-pushed the toniricodiez/cache-remote-config-response branch from 79b773f to 02d493a Compare May 11, 2026 13:30
@tonidero tonidero force-pushed the toniricodiez/cache-remote-config-response branch 2 times, most recently from 8e27a11 to 9a1c9ca Compare May 11, 2026 14:05
Copy link
Copy Markdown
Member

@ajpallares ajpallares left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is since we use some internal APIs, for example DateProvider.

Comment on lines +102 to +108
if (firstError == null && source != null && tasks.isNotEmpty()) {
val previousResponse = cache?.response
cache = CacheEntry(response, dateProvider.now)
if (previousResponse != response) {
diskCache.write(response)
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +52 to +63
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()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's indeed true, good catch!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ended up removing the dedupe logic here and relying on the backend dedupe itself

@tonidero tonidero force-pushed the toniricodiez/cache-remote-config-response branch from 9a1c9ca to 8f91f3d Compare May 12, 2026 09:47
@tonidero
Copy link
Copy Markdown
Contributor Author

I believe the memory cache is not used yet in this PR either (since the RemoteConfigManager is not yet used anywhere)?

This is indeed correct in this PR, but it's used in the final PRs in this stack which actually use the RemoteConfigManager

@tonidero tonidero requested a review from ajpallares May 12, 2026 09:49
Copy link
Copy Markdown
Member

@ajpallares ajpallares left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great job! I think this looks good!

@tonidero tonidero added this pull request to the merge queue May 12, 2026
Merged via the queue into main with commit b83a198 May 12, 2026
38 checks passed
@tonidero tonidero deleted the toniricodiez/cache-remote-config-response branch May 12, 2026 11:28
github-merge-queue Bot pushed a commit that referenced this pull request May 13, 2026
**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 -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants