Skip to content

Commit b85e36a

Browse files
authored
Merge pull request #2081 from OneSignal/fix/loadSavedOperations-index-out-of-bounds
[Fix] loadSavedOperations IndexOutOfBoundsException (5.1.11 only issue)
2 parents 05a00a2 + 8aa83c5 commit b85e36a

File tree

3 files changed

+53
-17
lines changed

3 files changed

+53
-17
lines changed

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/modeling/ModelStore.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,11 @@ abstract class ModelStore<TModel>(
6565
}
6666
}
6767

68+
/**
69+
* @return list of read-only models, cloned for thread safety
70+
*/
6871
override fun list(): Collection<TModel> {
69-
return models
72+
return synchronized(models) { models.toList() }
7073
}
7174

7275
override fun get(id: String): TModel? {

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

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -136,20 +136,25 @@ internal class OperationRepo(
136136
return waiter.waitForWake()
137137
}
138138

139-
// WARNING: Never set to true, until budget rules are added, even for internal use!
139+
/**
140+
* Only used inside this class, adds OperationQueueItem to queue
141+
* WARNING: Never set flush=true until budget rules are added, even for internal use!
142+
*
143+
* @returns true if the OperationQueueItem was added, false if not
144+
*/
140145
private fun internalEnqueue(
141146
queueItem: OperationQueueItem,
142147
flush: Boolean,
143148
addToStore: Boolean,
144149
index: Int? = null,
145-
) {
146-
val hasExisting = queue.any { it.operation.id == queueItem.operation.id }
147-
if (hasExisting) {
148-
Logging.debug("OperationRepo: internalEnqueue - operation.id: ${queueItem.operation.id} already exists in the queue.")
149-
return
150-
}
151-
150+
): Boolean {
152151
synchronized(queue) {
152+
val hasExisting = queue.any { it.operation.id == queueItem.operation.id }
153+
if (hasExisting) {
154+
Logging.debug("OperationRepo: internalEnqueue - operation.id: ${queueItem.operation.id} already exists in the queue.")
155+
return false
156+
}
157+
153158
if (index != null) {
154159
queue.add(index, queueItem)
155160
} else {
@@ -161,6 +166,7 @@ internal class OperationRepo(
161166
}
162167

163168
waiter.wake(LoopWaiterMessage(flush, 0))
169+
return true
164170
}
165171

166172
/**
@@ -405,18 +411,25 @@ internal class OperationRepo(
405411

406412
/**
407413
* Load saved operations from preference service and add them into the queue
414+
* WARNING: Make sure queue.remove is NEVER called while this method is
415+
* running, as internalEnqueue will throw IndexOutOfBounds or put things
416+
* out of order if what was removed was something added by this method.
417+
* - This never happens now, but is a landmine to be aware of!
408418
* NOTE: Sometimes the loading might take longer than expected due to I/O reads from disk
409419
* Any I/O implies executing time will vary greatly.
410420
*/
411421
internal fun loadSavedOperations() {
412422
_operationModelStore.loadOperations()
413-
for (operation in _operationModelStore.list().withIndex()) {
414-
internalEnqueue(
415-
OperationQueueItem(operation.value, bucket = enqueueIntoBucket),
416-
flush = false,
417-
addToStore = false,
418-
operation.index,
419-
)
423+
var successfulIndex = 0
424+
for (operation in _operationModelStore.list()) {
425+
val successful =
426+
internalEnqueue(
427+
OperationQueueItem(operation, bucket = enqueueIntoBucket),
428+
flush = false,
429+
addToStore = false,
430+
index = successfulIndex,
431+
)
432+
if (successful) successfulIndex++
420433
}
421434
loadedSubscription.fire { it.onOperationRepoLoaded() }
422435
}

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

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ private class Mocks {
3838
val operationStoreList = mutableListOf<Operation>()
3939
val mockOperationModelStore = mockk<OperationModelStore>()
4040
every { mockOperationModelStore.loadOperations() } just runs
41-
every { mockOperationModelStore.list() } returns operationStoreList
41+
every { mockOperationModelStore.list() } answers { operationStoreList.toList() }
4242
every { mockOperationModelStore.add(any()) } answers { operationStoreList.add(firstArg<Operation>()) }
4343
every { mockOperationModelStore.remove(any()) } answers {
4444
val id = firstArg<String>()
@@ -630,6 +630,26 @@ class OperationRepoTests : FunSpec({
630630
mocks.operationRepo.queue.size shouldBe 1
631631
mocks.operationRepo.queue.first().operation shouldBe op
632632
}
633+
634+
// Real world scenario is this can happen if a few operations are added when the device is
635+
// offline then the app is restarted.
636+
test("ensure loadSavedOperations doesn't index out of bounds on queue when duplicates exist") {
637+
// Given
638+
val mocks = Mocks()
639+
val op1 = mockOperation()
640+
val op2 = mockOperation()
641+
642+
repeat(2) { mocks.operationModelStore.add(op1) }
643+
mocks.operationModelStore.add(op2)
644+
645+
// When
646+
mocks.operationRepo.loadSavedOperations()
647+
648+
// Then
649+
mocks.operationRepo.queue.size shouldBe 2
650+
mocks.operationRepo.queue[0].operation shouldBe op1
651+
mocks.operationRepo.queue[1].operation shouldBe op2
652+
}
633653
}) {
634654
companion object {
635655
private fun mockOperation(

0 commit comments

Comments
 (0)