Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
fix: [SDK-4388] subscription state permanently stuck at "Never Subscribed" when login() is called before requestPermission() #2627
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
base: main
Are you sure you want to change the base?
fix: [SDK-4388] subscription state permanently stuck at "Never Subscribed" when login() is called before requestPermission() #2627
Changes from all commits
731623f3da01c024a3623File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
There are no files selected for viewing
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.
🔴 Routing every CreateSubscriptionOperation with a non-local subscriptionId through PATCH breaks the existing 404/MISSING recovery flow in updateSubscription(): outside the missing-retry window, updateSubscription returns FAIL_NORETRY with a follow-up CreateSubscriptionOperation that re-uses the same non-local subscriptionId, which is now re-routed back through updateExistingSubscriptionFromCreate -> PATCH -> 404 -> ..., looping indefinitely with no escape. Suggested fix: catch BackendException(404) inside updateExistingSubscriptionFromCreate and fall back to createSubscription (POST) so the rebuild-user / FAIL_NORETRY recovery in createSubscription can terminate the cycle (or null out the id on the recovery op so it forces POST).
Extended reasoning...
Bug
The new
updateExistingSubscriptionFromCreatehelper unconditionally dispatches everyCreateSubscriptionOperationwhosesubscriptionIdis non-local throughupdateSubscription(PATCH/subscriptions/{id}). When that PATCH returns 404 outside the missing-retry window,updateSubscription(lines 235-260) returnsFAIL_NORETRYwith a recovery op:OperationRepo.executeOperations(OperationRepo.kt:314-322) enqueuesresponse.operationsat the front of the queue regardless of theFAIL_NORETRYresult. On the next pass,startingOp is CreateSubscriptionOperationandIDManager.isLocalId(...)is stillfalse, so it re-entersupdateExistingSubscriptionFromCreate-> PATCH -> 404 -> anotherCreateSubscriptionOperationwith the same id... loop.Step-by-step proof
CreateSubscriptionOperation(subscriptionId = "remote-uuid", ...)(e.g. server-side deletion happened after the local model cached the server uuid).execute()(line 56) seesstartingOp is CreateSubscriptionOperationand!IDManager.isLocalId("remote-uuid")is true -> callsupdateExistingSubscriptionFromCreate.patchOpand callsupdateSubscription(patchOp, listOf(patchOp))._newRecordState.isInMissingRetryWindow(...)is false (window expired or never opened).updateSubscriptionenters theMISSINGbranch and returnsFAIL_NORETRY+ aCreateSubscriptionOperationre-usinglastOperation.subscriptionId = "remote-uuid".OperationRepo.executeOperationsremoves the original op (FAIL_NORETRY clearsretries) andif (response.operations != null)enqueues the newCreateSubscriptionOperation("remote-uuid")at the front.CreateSubscriptionOperationUUID but the same non-localsubscriptionId, andretriesnever accumulates because each cycle isFAIL_NORETRY. Backoff is whateverdelayBeforeNextExecution(0, null)gives - effectively nothing.Why pre-PR code did not have this problem
Pre-PR, the recovery
CreateSubscriptionOperationwent throughcreateSubscription(POST). On 404,createSubscription'sMISSINGhandler calls_buildUserService.getRebuildOperationsIfCurrentUser(...), which either bootstraps a rebuild (terminating the create cycle for this id) or returns null -> terminatingFAIL_NORETRY. Either way the cycle ends. The new helper has no such fallback and no 404 handling at all - it just delegates toupdateSubscriptionand lets the recovery op come back to the same branch.Impact
Trigger scenarios are uncommon but plausible: server-side subscription deletion (admin/GDPR cleanup, manual REST delete, server lifecycle), or any case where the cached server uuid no longer exists on the backend. Consequence is unbounded queue churn - the SDK will hammer
PATCH /subscriptions/{id}indefinitely with no client-side termination, draining battery and traffic until the process dies. There is no test exercising this path.Fix
Either:
BackendExceptionwith 404 inupdateExistingSubscriptionFromCreateand fall through tocreateSubscription(POST), restoring the rebuild-user recovery path; orupdateSubscription's 404 handler, setsubscriptionId = nullon the rebuiltCreateSubscriptionOperationso it is forced through the local-id POST branch on re-execution.A unit test exercising PATCH-404-outside-missing-retry-window -> recovery should be added.
🔬 also observed by copilot-pr-reviewer
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.
#2354
Uh oh!
There was an error while loading. Please reload this page.