Skip to content

Commit 8aa83c5

Browse files
committed
fix loadSavedOperations index out of bounds issue
Since things can be added to the queue before loadSavedOperations is called it has to account for duplicate entries. It did do this, however it was missing the logic to account for not advancing the index on duplicates so an out of bounds was possible. We solved the problem by only incrementing the index if it wasn't a duplicate, however this implementation has a future landmine. If something ever removes something from the queue that loadSavedOperations added and it is still executing index problems can still happen. This scenario never happens now, but a new feature to OperationRepo or a refactor could introduce the problem. A fast follow is recommend so this landmine isn't left here.
1 parent 354115e commit 8aa83c5

File tree

3 files changed

+33
-17
lines changed

3 files changed

+33
-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: 1 addition & 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>()

0 commit comments

Comments
 (0)