-
Notifications
You must be signed in to change notification settings - Fork 106
Remove workflows fallback on 4xx errors #3562
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ import com.revenuecat.purchases.InternalRevenueCatAPI | |
| import com.revenuecat.purchases.PurchasesError | ||
| import com.revenuecat.purchases.common.Backend | ||
| import com.revenuecat.purchases.common.Dispatcher | ||
| import com.revenuecat.purchases.common.GetWorkflowsErrorHandlingBehavior | ||
| import com.revenuecat.purchases.common.errorLog | ||
| import com.revenuecat.purchases.common.safeResume | ||
| import com.revenuecat.purchases.common.toPurchasesError | ||
|
|
@@ -246,43 +247,60 @@ internal class WorkflowManager( | |
| completePendingCallbacks(appUserID) | ||
| } | ||
| }, | ||
| onError = { error -> | ||
| onError = { error, behavior -> | ||
| errorLog { "Failed to fetch workflows list: ${error.underlyingErrorMessage}" } | ||
| // Restore the in-memory cache from disk without rewriting it — disk already holds | ||
| // this payload. | ||
| val restoredFromDisk = workflowsCache.cachedWorkflowsListResponseFromDisk()?.let { response -> | ||
| val filtered = response.onlyWorkflowsWithOfferingId() | ||
| workflowsCache.cacheWorkflowsListInMemory(filtered, buildOfferingIdMap(filtered.workflows)) | ||
| } != null | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| // Mirror OfferingsManager.handleErrorFetchingOfferings: when there is no disk | ||
| // cache to fall back on, force the list stale so the next call retries. When a | ||
| // disk restore did succeed, cacheWorkflowsListInMemory already stamped a fresh | ||
| // timestamp, so we leave it alone — same as offerings leaving the cache fresh | ||
| // after createAndCacheOfferings runs on the disk-fallback path. | ||
| if (!restoredFromDisk) { | ||
| if (behavior == GetWorkflowsErrorHandlingBehavior.SHOULD_NOT_FALLBACK) { | ||
| // A 4xx means the server intentionally changed/removed these workflows. Don't | ||
| // resurrect a stale list from disk; just settle the callbacks so offerings | ||
| // delivery isn't stranded. Invalidate the timestamp so the next non-forced call | ||
| // retries rather than serving a still-fresh in-memory list — mirrors | ||
| // OfferingsManager.handleErrorFetchingOfferings calling forceCacheStale(). | ||
| workflowsCache.invalidateWorkflowsListTimestamp() | ||
| } | ||
| val envelopes = workflowsCache.cachedWorkflowDetailEnvelopesFromDisk().orEmpty() | ||
| if (envelopes.isEmpty()) { | ||
| completePendingCallbacks(appUserID) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: 4xx leaves the timestamp stale, so we re-hit the backend every call while still serving the stale in-memory map. Before this PR the restore path re-stamped it, so we used to back off. Want to keep retrying every call, or stamp it so a sticky 4xx backs off? Offerings does the same thing so either way is fine, just flagging.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let's mimic what offerings does
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there was actually a divergence with offerings here. In offerings we call forceCacheStale on 4xx errors |
||
| } else { | ||
| scope.launch { | ||
| // Re-resolve persisted envelopes into the in-memory cache, mirroring the | ||
| // success-path prefetch loop. A failed re-resolve is logged and does not | ||
| // fail its siblings. completePendingCallbacks still fires exactly once. | ||
| coroutineScope { | ||
| envelopes.forEach { (workflowId, envelope) -> | ||
| launch { restoreWorkflowFromEnvelope(workflowId, envelope) } | ||
| } | ||
| } | ||
| completePendingCallbacks(appUserID) | ||
| } | ||
| restoreWorkflowsListFromDisk(appUserID) | ||
| } | ||
| }, | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Restores the workflows list and persisted detail envelopes from disk after a fallback-eligible | ||
| * fetch failure (transport error, 5xx, or malformed body), then settles pending callbacks. The | ||
| * in-memory cache is rewritten from disk without re-persisting it — disk already holds this payload. | ||
| * [completePendingCallbacks] always fires exactly once. | ||
| */ | ||
| private fun restoreWorkflowsListFromDisk(appUserID: String) { | ||
| val restoredFromDisk = workflowsCache.cachedWorkflowsListResponseFromDisk()?.let { response -> | ||
| val filtered = response.onlyWorkflowsWithOfferingId() | ||
| workflowsCache.cacheWorkflowsListInMemory(filtered, buildOfferingIdMap(filtered.workflows)) | ||
| } != null | ||
| // Mirror OfferingsManager.handleErrorFetchingOfferings: when there is no disk cache to fall | ||
| // back on, force the list stale so the next call retries. When a disk restore did succeed, | ||
| // cacheWorkflowsListInMemory already stamped a fresh timestamp, so we leave it alone — same as | ||
| // offerings leaving the cache fresh after createAndCacheOfferings runs on the disk-fallback path. | ||
| if (!restoredFromDisk) { | ||
| workflowsCache.invalidateWorkflowsListTimestamp() | ||
| } | ||
| val envelopes = workflowsCache.cachedWorkflowDetailEnvelopesFromDisk().orEmpty() | ||
| if (envelopes.isEmpty()) { | ||
| completePendingCallbacks(appUserID) | ||
| } else { | ||
| scope.launch { | ||
| // Re-resolve persisted envelopes into the in-memory cache, mirroring the success-path | ||
| // prefetch loop. A failed re-resolve is logged and does not fail its siblings. | ||
| // completePendingCallbacks still fires exactly once. | ||
| coroutineScope { | ||
| envelopes.forEach { (workflowId, envelope) -> | ||
| launch { restoreWorkflowFromEnvelope(workflowId, envelope) } | ||
| } | ||
| } | ||
| completePendingCallbacks(appUserID) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Suspends until [getWorkflow] for [workflowId] resolves. Prefetch is best-effort, so a failure | ||
| * is logged and the coroutine resumes normally instead of throwing, keeping one failed prefetch | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On iOS I've generalized instead of duplicating the whole logic. But it's fine. We can always follow-up after releasing.