Skip to content

Replace workflows forceStale flag with a forceRefresh param#3541

Merged
vegaro merged 6 commits into
mainfrom
cesar/wfl-280-replace-forceworkflowslistcachestale-with-a
Jun 8, 2026
Merged

Replace workflows forceStale flag with a forceRefresh param#3541
vegaro merged 6 commits into
mainfrom
cesar/wfl-280-replace-forceworkflowslistcachestale-with-a

Conversation

@vegaro

@vegaro vegaro commented Jun 5, 2026

Copy link
Copy Markdown
Member

Stacked on #3540.

forceWorkflowsListCacheStale() was a side effect I didn't like that cleared the list-cache timestamp, and the next getWorkflowsList checked it. That made it a bit coupled, hard to follow and not synchronized, so I replaced it with a forceRefresh parameter on getWorkflowsList. The behavior is preserved and a fresh offerings network response passes forceRefresh = true and a read from disk passes false.


Note

Medium Risk
Touches offerings delivery gating and workflows caching/prefetch timing; behavior is intended to match the old force-stale path but adds new error-path invalidation and detail-cache clearing on successful force refresh.

Overview
Replaces the forceWorkflowsListCacheStale() side effect with a forceRefresh argument on getWorkflowsList, so offerings can request a workflows refresh in one call instead of invalidating the list timestamp first.

OfferingsManager passes forceRefresh = !loadedFromDiskCache: network offerings trigger a workflows list refetch and realignment of the offeringId → workflowId map; disk fallback keeps the workflows list on its normal TTL.

On a successful forced list fetch, WorkflowManager clears in-memory workflow detail caches so prefetches always miss TTL and hit the backend. Detail caches are not cleared before the network call, so a failed forced fetch leaves cached details intact. List fetch errors without disk restore now call invalidateWorkflowsListTimestamp() (renamed from forceWorkflowsListCacheStale) so the next call retries, matching offerings error behavior.

Concurrent callers still dedupe; forceRefresh is ignored when joining an in-flight batch (documented). Tests updated and expanded for forceRefresh behavior.

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

vegaro commented Jun 5, 2026

Copy link
Copy Markdown
Member Author

@vegaro vegaro added the pr:other label Jun 5, 2026
@vegaro vegaro changed the title refactor: replace workflows forceStale flag with a getWorkflowsList forceRefresh param Replace workflows forceStale flag with a forceRefresh param Jun 5, 2026
@vegaro vegaro marked this pull request as ready for review June 5, 2026 07:58
@vegaro vegaro requested a review from a team as a code owner June 5, 2026 07:58
@vegaro vegaro force-pushed the cesar/wfl-280-replace-forceworkflowslistcachestale-with-a branch from cbe7276 to 44c2a5c Compare June 5, 2026 07:59
@codecov

codecov Bot commented Jun 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 80.19%. Comparing base (689b9e7) to head (d43f8a2).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3541      +/-   ##
==========================================
+ Coverage   80.18%   80.19%   +0.01%     
==========================================
  Files         371      371              
  Lines       15233    15239       +6     
  Branches     2111     2112       +1     
==========================================
+ Hits        12214    12221       +7     
+ Misses       2169     2168       -1     
  Partials      850      850              

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

@vegaro vegaro force-pushed the cesar/workflow-detail-swr branch from 8e422d6 to 8509e19 Compare June 8, 2026 10:54
@vegaro vegaro force-pushed the cesar/wfl-280-replace-forceworkflowslistcachestale-with-a branch from 44c2a5c to 6f9ba69 Compare June 8, 2026 10:54
@facumenzella

facumenzella commented Jun 8, 2026

Copy link
Copy Markdown
Member

I opened #3555 against this branch with a focused failing test for the forceRefresh retry case.

It demonstrates that when a forced workflows-list refresh fails while an old list cache is still fresh, the next non-forced getWorkflowsList call skips the backend retry. The repro expects 3 getWorkflows calls, but the current implementation only makes 2.

Leaving it as a draft/repro PR so you can decide how to handle the behavior.

vegaro pushed a commit that referenced this pull request Jun 8, 2026
## Summary

Adds a regression test that demonstrates the failed `forceRefresh` retry
path in PR #3541.

The test covers this sequence:

1. A workflows list fetch succeeds and caches `default -> wf_old`.
2. A later `forceRefresh = true` fetch runs while that old list is still
fresh, but the backend request fails.
3. The next normal `getWorkflowsList(..., forceRefresh = false)` should
retry and update the map to `wf_new`.

On the current implementation from #3541, step 3 is skipped because the
old workflows-list timestamp remains fresh after the failed forced
refresh.

## Validation

This is a repro PR, so the added test is expected to fail until the
implementation changes.

```bash
rtk ./gradlew :purchases:testDefaultsBc8DebugUnitTest --tests "com.revenuecat.purchases.common.workflows.WorkflowManagerTest.getWorkflowsList retries after failed forceRefresh even when old list cache was fresh"
```

Failure observed on PR #3541 head:

```text
Verification failed ... 2 matching calls found, but needs at least 3 and at most 3 calls
```

Before adding this repro, `WorkflowManagerTest` and `WorkflowsCacheTest`
passed on PR #3541 head in the focused run.

<details>
<summary>AI session context</summary>

# AI Context

## Metadata

- PR: Not captured
- Branch: `facundo/pr3541-force-refresh-retry-test`
- Author / human owner: `facumenzella`
- Agent(s): Codex
- Session source: current conversation
- Generated: 2026-06-08
- Context document version: 1

## Goal

Review PR #3541, verify whether a suspected workflows-list
`forceRefresh` retry regression is real, then open a repro PR against PR
#3541 so the original author can decide how to handle it.

## Initial Prompt

Review `https://github.com/RevenueCat/purchases-android/pull/3541`.

## Important Follow-up Prompts

- User asked to write a test and prove the suspected issue is broken.
- User then asked to open a PR against PR #3541 so `vegaro` can decide.

## Agent Contribution

- Reviewed PR #3541 diff against its stacked base branch.
- Identified a behavioral difference: replacing
`forceWorkflowsListCacheStale()` with call-scoped `forceRefresh` no
longer clears the old workflows-list timestamp before the forced fetch.
- Added a focused failing unit test that proves a failed forced refresh
is not retried by the next non-forced list call while the old cache
timestamp is still fresh.
- Ran the focused Gradle test command and captured the failure.
- Created and pushed branch `facundo/pr3541-force-refresh-retry-test`.

## Human Decisions

- Human accepted the review finding as worth proving with a test.
- Human decided to open a PR against PR #3541 rather than directly
changing the implementation.

## Key Implementation Decisions

- Decision: Add only a regression test, not a production fix.
- Rationale: The purpose is to provide reproducible evidence and let PR
#3541's author decide the implementation.
  - Rejected: Directly patching `WorkflowManager` in this PR.

## Files / Symbols Touched

-
`purchases/src/test/java/com/revenuecat/purchases/common/workflows/WorkflowManagerTest.kt`
  - Why: Add the smallest unit-level repro for the retry behavior.
- Symbols: `WorkflowManagerTest`, `getWorkflowsList retries after failed
forceRefresh even when old list cache was fresh`
- Review relevance: Confirms whether a failed forced refresh should
leave the list retryable even when the previous list timestamp was
fresh.

## Dependencies / Config / Migrations

- None captured.

## Validation

- Commands run:
- `rtk ./gradlew :purchases:testDefaultsBc8DebugUnitTest --tests
"com.revenuecat.purchases.common.workflows.WorkflowManagerTest.getWorkflowsList
retries after failed forceRefresh even when old list cache was fresh"`:
failed as expected with only two `Backend.getWorkflows` calls observed
where the test expected three.
- `rtk ./gradlew :purchases:testDefaultsBc8DebugUnitTest --tests
"com.revenuecat.purchases.common.offerings.OfferingsManagerTest" --tests
"com.revenuecat.purchases.common.workflows.WorkflowManagerTest" --tests
"com.revenuecat.purchases.common.workflows.WorkflowsCacheTest"`:
`WorkflowManagerTest` and `WorkflowsCacheTest` passed before adding the
repro; `OfferingsManagerTest` failed at class initialization because
Robolectric selected Android SDK 36 with Java 17.
- Manual verification:
- Inspected the failing test output and confirmed the skipped third
backend call.
- CI:
  - Not captured.

## Validation Gaps

- The repro PR intentionally contains a failing test against PR #3541's
current implementation.
- `OfferingsManagerTest` was not validated locally due to
Java/Robolectric SDK mismatch.

## Review Focus

- Should a failed forced workflows-list refresh keep the list
stale/retryable for the next call?
- If yes, decide whether to restore timestamp-clearing behavior, track
failed forced refresh state, or use another retry signal.

## Risks / Reviewer Notes

- Risk: The test encodes a desired retry behavior rather than current
behavior.
- Evidence: It fails on PR #3541 head with only two backend list calls.
- Mitigation: Treat this PR as a repro and decision aid, not a final
implementation.

## Non-goals / Out of Scope

- Production fix for the retry behavior.
- Broader refactor of workflows list caching.

## Omitted Context

- Raw transcript, unrelated exploration, sensitive details, repetitive
attempts, and chain-of-thought-style content were omitted.

</details>
@vegaro

vegaro commented Jun 8, 2026

Copy link
Copy Markdown
Member Author

@facumenzella thanks for that. Fixed in 5c62525

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

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 4b68fd3. Configure here.

// Clear detail caches after a successful fetch so the prefetch loop below is a
// guaranteed cache miss and always populates fresh data. Cleared here rather than
// before the network call so a failed fetch leaves in-memory details intact.
if (forceRefresh) workflowsCache.clearWorkflowDetailCaches()

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is a fix for a regression I noticed in main. Force refresh wasn't working

Base automatically changed from cesar/workflow-detail-swr to main June 8, 2026 14:59
vegaro and others added 5 commits June 8, 2026 17:02
…orceRefresh param

forceWorkflowsListCacheStale() was a side-channel flag: it cleared the list
cache timestamp, and the next getWorkflowsList consulted it. That coupled two
calls (force then fetch), and any completing in-flight fetch re-stamped the
cache fresh, quietly swallowing the force.

Replace it with a forceRefresh parameter on getWorkflowsList, OR'd into the
staleness check, so the "fetch fresh" intent travels with the call. Behavior is
preserved: a fresh offerings network response passes forceRefresh = true; a
disk-cache replay passes false. The in-flight dedup still joins an existing
batch (a joined batch returns a live backend response, so "force fresh" is
satisfied) — now documented in the getWorkflowsList KDoc instead of emergent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
## Summary

Adds a regression test that demonstrates the failed `forceRefresh` retry
path in PR #3541.

The test covers this sequence:

1. A workflows list fetch succeeds and caches `default -> wf_old`.
2. A later `forceRefresh = true` fetch runs while that old list is still
fresh, but the backend request fails.
3. The next normal `getWorkflowsList(..., forceRefresh = false)` should
retry and update the map to `wf_new`.

On the current implementation from #3541, step 3 is skipped because the
old workflows-list timestamp remains fresh after the failed forced
refresh.

## Validation

This is a repro PR, so the added test is expected to fail until the
implementation changes.

```bash
rtk ./gradlew :purchases:testDefaultsBc8DebugUnitTest --tests "com.revenuecat.purchases.common.workflows.WorkflowManagerTest.getWorkflowsList retries after failed forceRefresh even when old list cache was fresh"
```

Failure observed on PR #3541 head:

```text
Verification failed ... 2 matching calls found, but needs at least 3 and at most 3 calls
```

Before adding this repro, `WorkflowManagerTest` and `WorkflowsCacheTest`
passed on PR #3541 head in the focused run.

<details>
<summary>AI session context</summary>

# AI Context

## Metadata

- PR: Not captured
- Branch: `facundo/pr3541-force-refresh-retry-test`
- Author / human owner: `facumenzella`
- Agent(s): Codex
- Session source: current conversation
- Generated: 2026-06-08
- Context document version: 1

## Goal

Review PR #3541, verify whether a suspected workflows-list
`forceRefresh` retry regression is real, then open a repro PR against PR
#3541 so the original author can decide how to handle it.

## Initial Prompt

Review `https://github.com/RevenueCat/purchases-android/pull/3541`.

## Important Follow-up Prompts

- User asked to write a test and prove the suspected issue is broken.
- User then asked to open a PR against PR #3541 so `vegaro` can decide.

## Agent Contribution

- Reviewed PR #3541 diff against its stacked base branch.
- Identified a behavioral difference: replacing
`forceWorkflowsListCacheStale()` with call-scoped `forceRefresh` no
longer clears the old workflows-list timestamp before the forced fetch.
- Added a focused failing unit test that proves a failed forced refresh
is not retried by the next non-forced list call while the old cache
timestamp is still fresh.
- Ran the focused Gradle test command and captured the failure.
- Created and pushed branch `facundo/pr3541-force-refresh-retry-test`.

## Human Decisions

- Human accepted the review finding as worth proving with a test.
- Human decided to open a PR against PR #3541 rather than directly
changing the implementation.

## Key Implementation Decisions

- Decision: Add only a regression test, not a production fix.
- Rationale: The purpose is to provide reproducible evidence and let PR
#3541's author decide the implementation.
  - Rejected: Directly patching `WorkflowManager` in this PR.

## Files / Symbols Touched

-
`purchases/src/test/java/com/revenuecat/purchases/common/workflows/WorkflowManagerTest.kt`
  - Why: Add the smallest unit-level repro for the retry behavior.
- Symbols: `WorkflowManagerTest`, `getWorkflowsList retries after failed
forceRefresh even when old list cache was fresh`
- Review relevance: Confirms whether a failed forced refresh should
leave the list retryable even when the previous list timestamp was
fresh.

## Dependencies / Config / Migrations

- None captured.

## Validation

- Commands run:
- `rtk ./gradlew :purchases:testDefaultsBc8DebugUnitTest --tests
"com.revenuecat.purchases.common.workflows.WorkflowManagerTest.getWorkflowsList
retries after failed forceRefresh even when old list cache was fresh"`:
failed as expected with only two `Backend.getWorkflows` calls observed
where the test expected three.
- `rtk ./gradlew :purchases:testDefaultsBc8DebugUnitTest --tests
"com.revenuecat.purchases.common.offerings.OfferingsManagerTest" --tests
"com.revenuecat.purchases.common.workflows.WorkflowManagerTest" --tests
"com.revenuecat.purchases.common.workflows.WorkflowsCacheTest"`:
`WorkflowManagerTest` and `WorkflowsCacheTest` passed before adding the
repro; `OfferingsManagerTest` failed at class initialization because
Robolectric selected Android SDK 36 with Java 17.
- Manual verification:
- Inspected the failing test output and confirmed the skipped third
backend call.
- CI:
  - Not captured.

## Validation Gaps

- The repro PR intentionally contains a failing test against PR #3541's
current implementation.
- `OfferingsManagerTest` was not validated locally due to
Java/Robolectric SDK mismatch.

## Review Focus

- Should a failed forced workflows-list refresh keep the list
stale/retryable for the next call?
- If yes, decide whether to restore timestamp-clearing behavior, track
failed forced refresh state, or use another retry signal.

## Risks / Reviewer Notes

- Risk: The test encodes a desired retry behavior rather than current
behavior.
- Evidence: It fails on PR #3541 head with only two backend list calls.
- Mitigation: Treat this PR as a repro and decision aid, not a final
implementation.

## Non-goals / Out of Scope

- Production fix for the retry behavior.
- Broader refactor of workflows list caching.

## Omitted Context

- Raw transcript, unrelated exploration, sensitive details, repetitive
attempts, and chain-of-thought-style content were omitted.

</details>
… cache

Mirrors OfferingsManager.handleErrorFetchingOfferings: when a backend
fetch fails and there is no disk cache to fall back on, force the list
stale so the next call retries. Previously the old timestamp survived a
failed forceRefresh, preventing the retry while the cache appeared fresh.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…serves fresh data

forceRefresh=true forced a list re-fetch but left per-workflow detail caches
untouched. The subsequent prefetch path called getWorkflow() with
staleWhileRevalidate=false, which blocks on a re-fetch when the cache is
stale — but returns the cached value immediately when it is still within
its 5-minute TTL. A pull-to-refresh triggered within that window would
re-fetch the list and silently serve stale detail content from cache.

Clear cachedWorkflows before the backend call when forceRefresh=true so
every subsequent prefetch is a guaranteed cache miss and fetches fresh
data from the backend regardless of the TTL.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move clearWorkflowDetailCaches() into the onSuccess handler so detail
caches are only evicted after a successful list fetch. Previously the
clear happened before the network call, which meant a failed forceRefresh
left the detail cache empty with no disk envelopes to restore from.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vegaro vegaro force-pushed the cesar/wfl-280-replace-forceworkflowslistcachestale-with-a branch from 10f949b to a3e28bb Compare June 8, 2026 15:02

@facumenzella facumenzella 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.

Let's go!

The test covered a scenario where the in-memory timestamp was fresh but
the disk cache returned null — a state that cannot occur in production
because cacheWorkflowsList always writes both. The `!restoredFromDisk`
branch it exercised is a no-op on the real path: when a forceRefresh
fails, the disk copy restores the old list and stamps a fresh timestamp,
and self-healing happens via the next forceRefresh from offerings (which
bypasses TTL anyway).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vegaro vegaro enabled auto-merge June 8, 2026 16:05
@vegaro vegaro added this pull request to the merge queue Jun 8, 2026
Merged via the queue into main with commit e1c4a90 Jun 8, 2026
38 checks passed
@vegaro vegaro deleted the cesar/wfl-280-replace-forceworkflowslistcachestale-with-a branch June 8, 2026 16:38
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