Skip to content

fix(cdn): serialize refresh and snapshot persisted cache metadata#2264

Draft
cursor[bot] wants to merge 1 commit into
mainfrom
cursor/critical-bug-remediation-2ba3
Draft

fix(cdn): serialize refresh and snapshot persisted cache metadata#2264
cursor[bot] wants to merge 1 commit into
mainfrom
cursor/critical-bug-remediation-2ba3

Conversation

@cursor

@cursor cursor Bot commented Jun 7, 2026

Copy link
Copy Markdown

Bug and impact

Trigger: A CDN-backed app loads from cache and kicks off _refreshIfStale() on cold start; the user backgrounds the app before that refresh finishes, triggering a second _refreshIfStale() from lifecycle paused/inactive.

Impact: Overlapping refreshes mutated shared _etag, _lastUpdatedAt, and _artifactCache, while _saveCachedState() read _etag and _lastUpdatedAt at async write time—not paired with the manifestJson argument. A slower save could persist manifest A with etag B. On the next cold start, If-None-Match could return 304 and skip re-download while the cached manifest body was wrong, leaving the app on stale or mismatched screens/scripts for the session.

Root cause

  • No serialization of concurrent _refreshIfStale() calls.
  • _saveCachedState() used fire-and-forget async closure that sampled instance fields at write time instead of snapshotting metadata with the manifest body being saved.

Fix

  • Chain overlapping _refreshIfStale() calls via _refreshSerial so only one refresh mutates in-memory state at a time.
  • Pass explicit etag and lastUpdatedAt snapshots into _saveCachedState(); extract cdnPersistedCacheEntry() for testability.
  • Add regression tests for tuple pairing and snapshot persistence.

Validation

  • Added tests in modules/ensemble/test/cdn_provider_test.dart for cdnPersistedCacheEntry and snapshot-based _saveCachedState.
  • Run from modules/ensemble: flutter test test/cdn_provider_test.dart

Duplicate check

Open in Web View Automation 

Concurrent _refreshIfStale() calls (cold start + lifecycle background) could
persist a manifest body with a mismatched etag/lastUpdatedAt because
_saveCachedState read instance fields at async write time. On next launch
If-None-Match could skip re-download while the cached body was wrong.

Serialize overlapping refreshes and pass etag/timestamp snapshots into
_saveCachedState so SharedPreferences always stores a consistent tuple.

Co-authored-by: Sharjeel Yunus <sharjeelyunus@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.

1 participant