Skip to content

Commit 9d1d91c

Browse files
authored
Merge pull request #2251 from OneSignal/subscription-not-updated-due-to-400-error
Fix: Anonymous Login request not cleared if app is forced close within 5 seconds on a new install
2 parents c96e91f + 47afee3 commit 9d1d91c

File tree

4 files changed

+119
-18
lines changed

4 files changed

+119
-18
lines changed

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

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -241,18 +241,6 @@ internal class OperationRepo(
241241
queue.forEach { it.operation.translateIds(response.idTranslations) }
242242
}
243243
response.idTranslations.values.forEach { _newRecordState.add(it) }
244-
// Stall processing the queue so the backend's DB has to time
245-
// reflect the change before we do any other operations to it.
246-
// NOTE: Future: We could run this logic in a
247-
// coroutineScope.launch() block so other operations not
248-
// effecting this these id's can still be done in parallel,
249-
// however other parts of the system don't currently account
250-
// for this so this is not safe to do.
251-
val waitTime = _configModelStore.model.opRepoPostCreateDelay
252-
delay(waitTime)
253-
synchronized(queue) {
254-
if (queue.isNotEmpty()) waiter.wake(LoopWaiterMessage(false, waitTime))
255-
}
256244
}
257245

258246
var highestRetries = 0
@@ -316,7 +304,11 @@ internal class OperationRepo(
316304
}
317305
}
318306

307+
// wait for retry and post create waiters to start next operation
319308
delayBeforeNextExecution(highestRetries, response.retryAfterSeconds)
309+
if (response.idTranslations != null) {
310+
delayForPostCreate(_configModelStore.model.opRepoPostCreateDelay)
311+
}
320312
} catch (e: Throwable) {
321313
Logging.log(LogLevel.ERROR, "Error attempting to execute operation: $ops", e)
322314

@@ -345,6 +337,22 @@ internal class OperationRepo(
345337
}
346338
}
347339

340+
/**
341+
* Stall processing the queue so the backend's DB has to time
342+
* reflect the change before we do any other operations to it.
343+
* NOTE: Future: We could run this logic in a
344+
* coroutineScope.launch() block so other operations not
345+
* effecting this these id's can still be done in parallel,
346+
* however other parts of the system don't currently account
347+
* for this so this is not safe to do.
348+
*/
349+
suspend fun delayForPostCreate(postCreateDelay: Long) {
350+
delay(postCreateDelay)
351+
synchronized(queue) {
352+
if (queue.isNotEmpty()) waiter.wake(LoopWaiterMessage(false, postCreateDelay))
353+
}
354+
}
355+
348356
internal fun getNextOps(bucketFilter: Int): List<OperationQueueItem>? {
349357
return synchronized(queue) {
350358
val startingOp =

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/LoginUserOperationExecutor.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,13 @@ internal class LoginUserOperationExecutor(
6666
loginUserOp: LoginUserOperation,
6767
operations: List<Operation>,
6868
): ExecutionResponse {
69+
// Handle a bad state that can happen in User Model 5.1.27 or earlier versions that old Login
70+
// request is not removed after processing if app is force-closed within the PostCreateDelay.
71+
// Anonymous Login being processed alone will surely be rejected, so we need to drop the request
72+
val containsSubscriptionOperation = operations.any { it is CreateSubscriptionOperation || it is TransferSubscriptionOperation }
73+
if (!containsSubscriptionOperation && loginUserOp.externalId == null) {
74+
return ExecutionResponse(ExecutionResult.FAIL_NORETRY)
75+
}
6976
if (loginUserOp.existingOnesignalId == null || loginUserOp.externalId == null) {
7077
// When there is no existing user to attempt to associate with the externalId provided, we go right to
7178
// createUser. If there is no externalId provided this is an insert, if there is this will be an

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,32 @@ class OperationRepoTests : FunSpec({
649649
}
650650
}
651651

652+
// operations not removed from the queue may get stuck in the queue if app is force closed within the delay
653+
test("execution of an operation with translation IDs removes the operation from queue before delay") {
654+
// Given
655+
val mocks = Mocks()
656+
mocks.configModelStore.model.opRepoPostCreateDelay = 100
657+
val operation = mockOperation(groupComparisonType = GroupComparisonType.NONE)
658+
val opId = operation.id
659+
val idTranslation = mapOf("local-id1" to "id1")
660+
coEvery {
661+
mocks.executor.execute(listOf(operation))
662+
} returns ExecutionResponse(ExecutionResult.SUCCESS, idTranslation)
663+
664+
// When
665+
mocks.operationRepo.start()
666+
val response = mocks.operationRepo.enqueueAndWait(operation)
667+
668+
// Then
669+
response shouldBe true
670+
coVerifyOrder {
671+
// ensure the order: IDs are translated, operation removed from the store, then delay for postCreateDelay
672+
operation.translateIds(idTranslation)
673+
mocks.operationModelStore.remove(opId)
674+
mocks.operationRepo.delayBeforeNextExecution(any(), any())
675+
}
676+
}
677+
652678
// We want to prevent a misbehaving app stuck in a loop from continuously
653679
// sending updates every opRepoExecutionInterval (5 seconds currently).
654680
// By waiting for the dust to settle we ensure the app is done making

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

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,16 @@ class LoginUserOperationExecutorTests : FunSpec({
3939
val localSubscriptionId2 = "local-subscriptionId2"
4040
val remoteSubscriptionId1 = "remote-subscriptionId1"
4141
val remoteSubscriptionId2 = "remote-subscriptionId2"
42+
val createSubscriptionOperation =
43+
CreateSubscriptionOperation(
44+
appId,
45+
localOneSignalId,
46+
"subscriptionId1",
47+
SubscriptionType.PUSH,
48+
true,
49+
"pushToken1",
50+
SubscriptionStatus.SUBSCRIBED,
51+
)
4252

4353
test("login anonymous user successfully creates user") {
4454
// Given
@@ -58,7 +68,7 @@ class LoginUserOperationExecutorTests : FunSpec({
5868
val loginUserOperationExecutor =
5969
LoginUserOperationExecutor(
6070
mockIdentityOperationExecutor,
61-
MockHelper.applicationService(),
71+
AndroidMockHelper.applicationService(),
6272
MockHelper.deviceService(),
6373
mockUserBackendService,
6474
mockIdentityModelStore,
@@ -67,7 +77,11 @@ class LoginUserOperationExecutorTests : FunSpec({
6777
MockHelper.configModelStore(),
6878
MockHelper.languageContext(),
6979
)
70-
val operations = listOf<Operation>(LoginUserOperation(appId, localOneSignalId, null, null))
80+
val operations =
81+
listOf<Operation>(
82+
LoginUserOperation(appId, localOneSignalId, null, null),
83+
createSubscriptionOperation,
84+
)
7185

7286
// When
7387
val response = loginUserOperationExecutor.execute(operations)
@@ -98,7 +112,7 @@ class LoginUserOperationExecutorTests : FunSpec({
98112
val loginUserOperationExecutor =
99113
LoginUserOperationExecutor(
100114
mockIdentityOperationExecutor,
101-
MockHelper.applicationService(),
115+
AndroidMockHelper.applicationService(),
102116
MockHelper.deviceService(),
103117
mockUserBackendService,
104118
mockIdentityModelStore,
@@ -107,7 +121,11 @@ class LoginUserOperationExecutorTests : FunSpec({
107121
MockHelper.configModelStore(),
108122
MockHelper.languageContext(),
109123
)
110-
val operations = listOf<Operation>(LoginUserOperation(appId, localOneSignalId, null, null))
124+
val operations =
125+
listOf<Operation>(
126+
LoginUserOperation(appId, localOneSignalId, null, null),
127+
createSubscriptionOperation,
128+
)
111129

112130
// When
113131
val response = loginUserOperationExecutor.execute(operations)
@@ -130,8 +148,12 @@ class LoginUserOperationExecutorTests : FunSpec({
130148
val mockSubscriptionsModelStore = mockk<SubscriptionModelStore>()
131149

132150
val loginUserOperationExecutor =
133-
LoginUserOperationExecutor(mockIdentityOperationExecutor, MockHelper.applicationService(), MockHelper.deviceService(), mockUserBackendService, mockIdentityModelStore, mockPropertiesModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext())
134-
val operations = listOf<Operation>(LoginUserOperation(appId, localOneSignalId, null, null))
151+
LoginUserOperationExecutor(mockIdentityOperationExecutor, AndroidMockHelper.applicationService(), MockHelper.deviceService(), mockUserBackendService, mockIdentityModelStore, mockPropertiesModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext())
152+
val operations =
153+
listOf<Operation>(
154+
LoginUserOperation(appId, localOneSignalId, null, null),
155+
createSubscriptionOperation,
156+
)
135157

136158
// When
137159
val response = loginUserOperationExecutor.execute(operations)
@@ -679,4 +701,42 @@ class LoginUserOperationExecutorTests : FunSpec({
679701
)
680702
}
681703
}
704+
705+
test("ensure anonymous login with no other operations will fail with FAIL_NORETRY") {
706+
// Given
707+
val mockUserBackendService = mockk<IUserBackendService>()
708+
coEvery { mockUserBackendService.createUser(any(), any(), any(), any()) } returns
709+
CreateUserResponse(mapOf(IdentityConstants.ONESIGNAL_ID to remoteOneSignalId), PropertiesObject(), listOf())
710+
711+
val mockIdentityOperationExecutor = mockk<IdentityOperationExecutor>()
712+
713+
val mockIdentityModelStore = MockHelper.identityModelStore()
714+
val mockPropertiesModelStore = MockHelper.propertiesModelStore()
715+
val mockSubscriptionsModelStore = mockk<SubscriptionModelStore>()
716+
717+
val loginUserOperationExecutor =
718+
LoginUserOperationExecutor(
719+
mockIdentityOperationExecutor,
720+
MockHelper.applicationService(),
721+
MockHelper.deviceService(),
722+
mockUserBackendService,
723+
mockIdentityModelStore,
724+
mockPropertiesModelStore,
725+
mockSubscriptionsModelStore,
726+
MockHelper.configModelStore(),
727+
MockHelper.languageContext(),
728+
)
729+
// anonymous Login request
730+
val operations = listOf<Operation>(LoginUserOperation(appId, localOneSignalId, null, null))
731+
732+
// When
733+
val response = loginUserOperationExecutor.execute(operations)
734+
735+
// Then
736+
response.result shouldBe ExecutionResult.FAIL_NORETRY
737+
// ensure user is not created by the bad request
738+
coVerify(
739+
exactly = 0,
740+
) { mockUserBackendService.createUser(appId, any(), any(), any()) }
741+
}
682742
})

0 commit comments

Comments
 (0)