Skip to content

fix(remote-config): Remote-config fixes/improvements#3714

Merged
tonidero merged 3 commits into
mainfrom
remote_config_review_hardening
Jul 6, 2026
Merged

fix(remote-config): Remote-config fixes/improvements#3714
tonidero merged 3 commits into
mainfrom
remote_config_review_hardening

Conversation

@tonidero

@tonidero tonidero commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Hardening fixes from a review pass over the remote-config stack.

Changes

  • Malformed topic item no longer crashes the app: ConfigItemSerializer throws SerializationException (instead of the .jsonObject accessor's IllegalStateException, which escaped the manager's catch) for a non-object item, making it a recoverable parse failure.
  • Crash-safe blob writes: RemoteConfigBlobStore writes via AtomicFile (same scheme as the disk cache), so a process kill can never leave a partial blob file under a valid ref.
  • In-memory snapshot in RemoteConfigDiskCache: hot-path reads (topic()/blobData(), blob-source lookups per download) stop re-reading + re-parsing the file and recomputing contentHash on every call.

Tests

  • Non-object topic item: parse fails with SerializationException; manager keeps the cache, releases the guard, no crash.
  • Interrupted blob write leaves no readable blob; the .new orphan is pruned by retainOnly.
  • Disk-cache snapshot is served without file re-reads and dropped on clear(); fresh instances still read from disk.
  • A failed refresh does not stamp the staleness window (next check retries); the rate-limited syncAttributesAndOfferings branch does not force a refresh.
  • Persisted-file forward-compat: unknown keys are tolerated.
  • Concurrency stress: 8 readers × 50 iterations racing clearCache + refreshes all complete, manager stays functional.

Note

Medium Risk
Changes affect on-disk config/blob caching and config parse error handling on a hot SDK path; behavior is defensive and well-covered by tests, but incorrect snapshot or atomic-write semantics could cause stale config or missed updates.

Overview
Hardens the remote-config persistence and parsing stack so bad payloads and crashes are handled safely, and hot-path disk access is cheaper.

ConfigItemSerializer now throws SerializationException when a topic item is not a JSON object (instead of IllegalStateException from .jsonObject), so RemoteConfigManager can treat malformed config as a recoverable parse failure without crashing.

RemoteConfigBlobStore switches blob persistence to AtomicFile (same pattern as the disk cache), so a killed process cannot leave a partial file under a valid ref; retainOnly docs/tests now cover <ref>.new orphans instead of rc_blob_*.tmp.

RemoteConfigDiskCache adds a synchronized in-memory snapshot: read() loads from disk once (including caching a miss), write() updates the snapshot only after a durable write, and clear() resets the snapshot before deleting the file.

Tests cover interrupted blob writes, snapshot behavior, forward-compatible persisted JSON, failed refresh / rate-limited sync not over-refreshing, non-object topic items, and a concurrency stress run over clearCache + refreshes + topic() reads.

Reviewed by Cursor Bugbot for commit a031979. Bugbot is set up for automated code reviews on this repo. Configure here.

@tonidero tonidero changed the title fix(remote-config): post-review hardening fix(remote-config): Remote-config fixes/improvements Jul 3, 2026
@codecov

codecov Bot commented Jul 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 74.19355% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.57%. Comparing base (693d387) to head (a031979).

Files with missing lines Patch % Lines
...hases/common/remoteconfig/RemoteConfigBlobStore.kt 57.14% 3 Missing ⚠️
...hases/common/remoteconfig/RemoteConfigDiskCache.kt 84.21% 2 Missing and 1 partial ⚠️
...rchases/common/remoteconfig/RemoteConfiguration.kt 60.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3714      +/-   ##
==========================================
+ Coverage   80.56%   80.57%   +0.01%     
==========================================
  Files         398      398              
  Lines       16473    16488      +15     
  Branches     2356     2356              
==========================================
+ Hits        13271    13286      +15     
- Misses       2265     2266       +1     
+ Partials      937      936       -1     

☔ View full report in Codecov by Harness.
📢 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 force-pushed the remote_config_review_hardening branch from d0e238a to 653b269 Compare July 3, 2026 15:35
@tonidero tonidero marked this pull request as ready for review July 3, 2026 15:37
@tonidero tonidero requested a review from a team as a code owner July 3, 2026 15:37
Base automatically changed from remote_config_logs_and_disable_on_cec to main July 3, 2026 16:08
tonidero and others added 3 commits July 3, 2026 18:08
Fixes from a review pass over the remote-config stack:

- ConfigItemSerializer throws SerializationException (not ISE) for a
  non-object topic item, so a malformed server payload is a recoverable
  parse failure instead of an uncaught crash.
- RemoteConfigBlobStore writes via AtomicFile (matching the disk cache),
  so a killed process can never leave a partial blob under a valid ref.
- RemoteConfigDiskCache keeps an in-memory snapshot of the persisted
  state, so hot-path reads stop re-reading + re-parsing the file (and
  recomputing contentHash) on every call.
- RemoteConfigManager runs its disk IO on the manager scope: pre-request
  reads hop off the caller thread, and persist() is split into
  commitConfig (under cacheLock) + syncBlobs (outside it) so clearCache
  on an identity change is never blocked behind blob IO. The clearCache
  file wipe stays synchronous by design: deferring it to a parallel IO
  job could run after a subsequent new-user commit and delete it.

Plus tests: failed refresh does not stamp the staleness window,
rate-limited syncAttributesAndOfferings branch does not refresh,
persisted-file forward-compat with unknown keys, and a concurrency
stress (readers x clearCache x refreshes).

Test plan:
- ./gradlew detektAll
- ./gradlew :purchases:testDefaultsBc8DebugUnitTest
- Backend integration tests against production

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pulls the "disk IO off the caller thread" change out of this PR to keep
it small: RemoteConfigManager is restored to the stack parent's version
(persist() back under cacheLock, pre-request disk reads synchronous),
and the two tests specific to that behavior are removed. The fix and
its ordering-hazard rationale are recoverable from this PR's history.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@tonidero tonidero force-pushed the remote_config_review_hardening branch from 653b269 to a031979 Compare July 3, 2026 16:08

@ajpallares ajpallares left a comment

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.

Nice improvements indeed!

@tonidero tonidero added this pull request to the merge queue Jul 6, 2026
Merged via the queue into main with commit ae737e8 Jul 6, 2026
37 checks passed
@tonidero tonidero deleted the remote_config_review_hardening branch July 6, 2026 07:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants