Skip to content

Commit 2f615fc

Browse files
committed
fix: skip dedup merge of local existingOnesignalId
Guard the LoginUserOperation dedup+merge path with !IDManager.isLocalId. When upgrading from 5.0.0-5.1.7 with a dropped anon-user create, switchUser returns a local- existingOneSignalId. If RecoverFromDroppedLoginBug wins the race and its null-existingOnesignalId op is queued first, merging the local id flips canStartExecute to false and strands the op — the waiter (now transferred) never wakes, and the stuck op blocks future recovery.
1 parent 8fea1e9 commit 2f615fc

2 files changed

Lines changed: 39 additions & 3 deletions

File tree

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/impl/OperationRepo.kt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.onesignal.core.internal.operations.impl
22

3+
import com.onesignal.common.IDManager
34
import com.onesignal.common.threading.WaiterWithValue
45
import com.onesignal.core.internal.config.ConfigModelStore
56
import com.onesignal.core.internal.operations.ExecutionResult
@@ -172,9 +173,15 @@ internal class OperationRepo(
172173
if (existing != null) {
173174
val existingOp = existing.operation as LoginUserOperation
174175
// Preserve the anon-user conversion link if the queued op lacks it (e.g. RecoverFromDroppedLoginBug enqueued with null).
175-
if (op.existingOnesignalId != null && existingOp.existingOnesignalId == null) {
176-
Logging.debug("OperationRepo: internalEnqueue - merging existingOnesignalId=${op.existingOnesignalId} into queued LoginUserOperation for onesignalId: ${op.onesignalId}.")
177-
existingOp.existingOnesignalId = op.existingOnesignalId
176+
// Skip local ids: merging one would flip canStartExecute to false and strand the op,
177+
// since a local id that never hit the backend will never receive an idTranslation.
178+
val incomingExistingId = op.existingOnesignalId
179+
if (incomingExistingId != null &&
180+
!IDManager.isLocalId(incomingExistingId) &&
181+
existingOp.existingOnesignalId == null
182+
) {
183+
Logging.debug("OperationRepo: internalEnqueue - merging existingOnesignalId=$incomingExistingId into queued LoginUserOperation for onesignalId: ${op.onesignalId}.")
184+
existingOp.existingOnesignalId = incomingExistingId
178185
} else {
179186
Logging.debug("OperationRepo: internalEnqueue - LoginUserOperation for onesignalId: ${op.onesignalId} already exists in the queue.")
180187
}

OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/operations/OperationRepoTests.kt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,35 @@ class OperationRepoTests : FunSpec({
170170
merged.existingOnesignalId shouldBe "anon-uuid"
171171
}
172172

173+
test("enqueue dedupe does not merge a local existingOnesignalId onto the queued op") {
174+
// Regression: when upgrading from 5.0.0-5.1.7, an anon user's backend-create
175+
// may have been dropped, so its onesignalId is still "local-". If
176+
// RecoverFromDroppedLoginBug wins the race (queued op has existingOnesignalId=null,
177+
// canStartExecute=true), we must NOT overwrite it with the incoming op's local
178+
// existingOnesignalId — doing so flips canStartExecute to false and strands the op
179+
// forever (the local id will never receive an idTranslation).
180+
val mocks = Mocks()
181+
val operationRepo = mocks.operationRepo
182+
183+
val queuedOp = LoginUserOperation("appId", "local-new", "alice", null)
184+
queuedOp.id = UUID.randomUUID().toString()
185+
synchronized(operationRepo.queue) {
186+
operationRepo.queue.add(OperationQueueItem(queuedOp, bucket = 0))
187+
}
188+
189+
val incomingOp = LoginUserOperation("appId", "local-new", "alice", "local-anon")
190+
191+
// When
192+
operationRepo.enqueue(incomingOp)
193+
mocks.waitForInternalEnqueue()
194+
195+
// Then — queue still has one op, existingOnesignalId stays null (canStartExecute stays true)
196+
operationRepo.queue.size shouldBe 1
197+
val survivor = operationRepo.queue.first().operation as LoginUserOperation
198+
survivor.existingOnesignalId shouldBe null
199+
survivor.canStartExecute shouldBe true
200+
}
201+
173202
test("containsInstanceOf") {
174203
// Given
175204
val mocks = Mocks()

0 commit comments

Comments
 (0)