Skip to content

Commit 91c9f50

Browse files
authored
Merge pull request #2087 from OneSignal/fix/canaccess_breaking_order_with_grouping
[Fix] grouping skipping opRepoPostCreateDelay, causing operations being applied out of order when multiple login operations are pending. (fixes issue since 5.1.10)
2 parents a6e9f80 + 9f8afc0 commit 91c9f50

File tree

3 files changed

+72
-64
lines changed

3 files changed

+72
-64
lines changed

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

Lines changed: 40 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -134,12 +134,12 @@ internal class OperationRepo(
134134
flush: Boolean,
135135
addToStore: Boolean,
136136
index: Int? = null,
137-
): Boolean {
137+
) {
138138
synchronized(queue) {
139139
val hasExisting = queue.any { it.operation.id == queueItem.operation.id }
140140
if (hasExisting) {
141141
Logging.debug("OperationRepo: internalEnqueue - operation.id: ${queueItem.operation.id} already exists in the queue.")
142-
return false
142+
return
143143
}
144144

145145
if (index != null) {
@@ -153,7 +153,6 @@ internal class OperationRepo(
153153
}
154154

155155
waiter.wake(LoopWaiterMessage(flush, 0))
156-
return true
157156
}
158157

159158
/**
@@ -236,12 +235,17 @@ internal class OperationRepo(
236235
queue.forEach { it.operation.translateIds(response.idTranslations) }
237236
}
238237
response.idTranslations.values.forEach { _newRecordState.add(it) }
239-
coroutineScope.launch {
240-
val waitTime = _configModelStore.model.opRepoPostCreateDelay
241-
delay(waitTime)
242-
synchronized(queue) {
243-
if (queue.isNotEmpty()) waiter.wake(LoopWaiterMessage(false, waitTime))
244-
}
238+
// Stall processing the queue so the backend's DB has to time
239+
// reflect the change before we do any other operations to it.
240+
// NOTE: Future: We could run this logic in a
241+
// coroutineScope.launch() block so other operations not
242+
// effecting this these id's can still be done in parallel,
243+
// however other parts of the system don't currently account
244+
// for this so this is not safe to do.
245+
val waitTime = _configModelStore.model.opRepoPostCreateDelay
246+
delay(waitTime)
247+
synchronized(queue) {
248+
if (queue.isNotEmpty()) waiter.wake(LoopWaiterMessage(false, waitTime))
245249
}
246250
}
247251

@@ -359,8 +363,7 @@ internal class OperationRepo(
359363
* THIS SHOULD BE CALLED WHILE THE QUEUE IS SYNCHRONIZED!!
360364
*/
361365
private fun getGroupableOperations(startingOp: OperationQueueItem): List<OperationQueueItem> {
362-
val ops = mutableListOf<OperationQueueItem>()
363-
ops.add(startingOp)
366+
val ops = mutableListOf(startingOp)
364367

365368
if (startingOp.operation.groupComparisonType == GroupComparisonType.NONE) {
366369
return ops
@@ -373,23 +376,25 @@ internal class OperationRepo(
373376
startingOp.operation.modifyComparisonKey
374377
}
375378

376-
if (queue.isNotEmpty()) {
377-
for (item in queue.toList()) {
378-
val itemKey =
379-
if (startingOp.operation.groupComparisonType == GroupComparisonType.CREATE) {
380-
item.operation.createComparisonKey
381-
} else {
382-
item.operation.modifyComparisonKey
383-
}
384-
385-
if (itemKey == "" && startingKey == "") {
386-
throw Exception("Both comparison keys can not be blank!")
379+
for (item in queue.toList()) {
380+
val itemKey =
381+
if (startingOp.operation.groupComparisonType == GroupComparisonType.CREATE) {
382+
item.operation.createComparisonKey
383+
} else {
384+
item.operation.modifyComparisonKey
387385
}
388386

389-
if (itemKey == startingKey) {
390-
queue.remove(item)
391-
ops.add(item)
392-
}
387+
if (itemKey == "" && startingKey == "") {
388+
throw Exception("Both comparison keys can not be blank!")
389+
}
390+
391+
if (!_newRecordState.canAccess(item.operation.applyToRecordId)) {
392+
continue
393+
}
394+
395+
if (itemKey == startingKey) {
396+
queue.remove(item)
397+
ops.add(item)
393398
}
394399
}
395400

@@ -398,25 +403,18 @@ internal class OperationRepo(
398403

399404
/**
400405
* Load saved operations from preference service and add them into the queue
401-
* WARNING: Make sure queue.remove is NEVER called while this method is
402-
* running, as internalEnqueue will throw IndexOutOfBounds or put things
403-
* out of order if what was removed was something added by this method.
404-
* - This never happens now, but is a landmine to be aware of!
405-
* NOTE: Sometimes the loading might take longer than expected due to I/O reads from disk
406-
* Any I/O implies executing time will vary greatly.
406+
* NOTE: Sometimes the loading might take longer than expected due to I/O reads from disk,
407+
* so always ensure this is call off the main thread.
407408
*/
408409
internal fun loadSavedOperations() {
409410
_operationModelStore.loadOperations()
410-
var successfulIndex = 0
411-
for (operation in _operationModelStore.list()) {
412-
val successful =
413-
internalEnqueue(
414-
OperationQueueItem(operation, bucket = enqueueIntoBucket),
415-
flush = false,
416-
addToStore = false,
417-
index = successfulIndex,
418-
)
419-
if (successful) successfulIndex++
411+
for (operation in _operationModelStore.list().reversed()) {
412+
internalEnqueue(
413+
OperationQueueItem(operation, bucket = enqueueIntoBucket),
414+
flush = false,
415+
addToStore = false,
416+
index = 0,
417+
)
420418
}
421419
initialized.complete(Unit)
422420
}

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/states/NewRecordsState.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class NewRecordsState(
2323

2424
fun canAccess(key: String): Boolean {
2525
val timeLastMovedOrCreated = records[key] ?: return true
26-
return _time.currentTimeMillis - timeLastMovedOrCreated > _configModelStore.model.opRepoPostCreateDelay
26+
return _time.currentTimeMillis - timeLastMovedOrCreated >= _configModelStore.model.opRepoPostCreateDelay
2727
}
2828

2929
fun isInMissingRetryWindow(key: String): Boolean {

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

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import io.mockk.spyk
2525
import io.mockk.verify
2626
import kotlinx.coroutines.delay
2727
import kotlinx.coroutines.launch
28-
import kotlinx.coroutines.time.withTimeout
2928
import kotlinx.coroutines.withTimeout
3029
import kotlinx.coroutines.withTimeoutOrNull
3130
import kotlinx.coroutines.yield
@@ -547,31 +546,42 @@ class OperationRepoTests : FunSpec({
547546
mocks.operationRepo.start()
548547
mocks.operationRepo.enqueue(operation1)
549548
val job = launch { mocks.operationRepo.enqueueAndWait(operation2) }.also { yield() }
550-
mocks.operationRepo.enqueue(operation3)
549+
mocks.operationRepo.enqueueAndWait(operation3)
551550
job.join()
552551

553552
// Then
554553
coVerifyOrder {
555-
mocks.executor.execute(
556-
withArg {
557-
it.count() shouldBe 1
558-
it[0] shouldBe operation1
559-
},
560-
)
554+
mocks.executor.execute(listOf(operation1))
561555
operation2.translateIds(mapOf("local-id1" to "id2"))
562-
mocks.executor.execute(
563-
withArg {
564-
it.count() shouldBe 1
565-
it[0] shouldBe operation3
566-
},
567-
)
568-
// Ensure operation2 runs after operation3 as it has to wait for the create delay
569-
mocks.executor.execute(
570-
withArg {
571-
it.count() shouldBe 1
572-
it[0] shouldBe operation2
573-
},
574-
)
556+
mocks.executor.execute(listOf(operation2))
557+
mocks.executor.execute(listOf(operation3))
558+
}
559+
}
560+
561+
// This tests the same logic as above, but makes sure the delay also
562+
// applies to grouping operations.
563+
test("execution of an operation with translation IDs delays follow up operations, including grouping") {
564+
// Given
565+
val mocks = Mocks()
566+
mocks.configModelStore.model.opRepoPostCreateDelay = 100
567+
val operation1 = mockOperation(groupComparisonType = GroupComparisonType.NONE)
568+
val operation2 = mockOperation(groupComparisonType = GroupComparisonType.CREATE)
569+
val operation3 = mockOperation(groupComparisonType = GroupComparisonType.CREATE, applyToRecordId = "id2")
570+
coEvery {
571+
mocks.executor.execute(listOf(operation1))
572+
} returns ExecutionResponse(ExecutionResult.SUCCESS, mapOf("local-id1" to "id2"))
573+
574+
// When
575+
mocks.operationRepo.start()
576+
mocks.operationRepo.enqueue(operation1)
577+
mocks.operationRepo.enqueue(operation2)
578+
mocks.operationRepo.enqueueAndWait(operation3)
579+
580+
// Then
581+
coVerifyOrder {
582+
mocks.executor.execute(listOf(operation1))
583+
operation2.translateIds(mapOf("local-id1" to "id2"))
584+
mocks.executor.execute(listOf(operation2, operation3))
575585
}
576586
}
577587

0 commit comments

Comments
 (0)