Skip to content

Commit 5864da4

Browse files
committed
Merge pull request #1970 from OneSignal/ConcurrentModificationInEventProducer
Concurrent modification in event producer
2 parents 8aea849 + 6a23a4a commit 5864da4

File tree

2 files changed

+35
-9
lines changed

2 files changed

+35
-9
lines changed

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/events/EventProducer.kt

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,9 @@ open class EventProducer<THandler> : IEventNotifier<THandler> {
3939
* @param callback The callback will be invoked for each subscribed handler, allowing you to call the handler.
4040
*/
4141
fun fire(callback: (THandler) -> Unit) {
42-
synchronized(subscribers) {
43-
for (s in subscribers) {
44-
callback(s)
45-
}
42+
val localList = subscribers.toList()
43+
for (s in localList) {
44+
callback(s)
4645
}
4746
}
4847

@@ -55,7 +54,8 @@ open class EventProducer<THandler> : IEventNotifier<THandler> {
5554
*/
5655
fun fireOnMain(callback: (THandler) -> Unit) {
5756
suspendifyOnMain {
58-
for (s in subscribers) {
57+
val localList = subscribers.toList()
58+
for (s in localList) {
5959
callback(s)
6060
}
6161
}
@@ -68,7 +68,8 @@ open class EventProducer<THandler> : IEventNotifier<THandler> {
6868
* @param callback The callback will be invoked for each subscribed handler, allowing you to call the handler.
6969
*/
7070
suspend fun suspendingFire(callback: suspend (THandler) -> Unit) {
71-
for (s in subscribers) {
71+
val localList = subscribers.toList()
72+
for (s in localList) {
7273
callback(s)
7374
}
7475
}
@@ -81,7 +82,8 @@ open class EventProducer<THandler> : IEventNotifier<THandler> {
8182
*/
8283
suspend fun suspendingFireOnMain(callback: suspend (THandler) -> Unit) {
8384
withContext(Dispatchers.Main) {
84-
for (s in subscribers) {
85+
val localList = subscribers.toList()
86+
for (s in localList) {
8587
callback(s)
8688
}
8789
}

OneSignalSDK/onesignal/core/src/test/java/com/onesignal/common/ModelingTests.kt

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import com.onesignal.mocks.MockPreferencesService
99
import com.onesignal.user.internal.subscriptions.SubscriptionModel
1010
import com.onesignal.user.internal.subscriptions.SubscriptionModelStore
1111
import io.kotest.core.spec.style.FunSpec
12+
import io.kotest.matchers.shouldBe
1213
import io.kotest.runner.junit4.KotestTestRunner
1314
import junit.framework.TestCase
1415
import org.junit.runner.RunWith
@@ -70,13 +71,11 @@ class ModelingTests : FunSpec({
7071
val t1 =
7172
Thread {
7273
// acquire "ModelStore.models", then trigger the onChanged event
73-
System.out.println("1")
7474
modelStore.add(newSubscriptionModel)
7575
}
7676

7777
val t2 =
7878
Thread {
79-
System.out.println("2")
8079
// acquire "model.data", then wait for "ModelStore.models"
8180
newSubscriptionModel.toJSON()
8281
}
@@ -116,4 +115,29 @@ class ModelingTests : FunSpec({
116115
// verify if the thread has been successfully terminated
117116
TestCase.assertEquals(Thread.State.TERMINATED, t2.state)
118117
}
118+
119+
test("Unsubscribing handler in change event may cause the concurrent modification exception") {
120+
// Given an arbitrary model
121+
val modelStore = MockHelper.configModelStore()
122+
val model = modelStore.model
123+
124+
// subscribe to a change handler
125+
model.subscribe(
126+
object : IModelChangedHandler {
127+
override fun onChanged(
128+
args: ModelChangedArgs,
129+
tag: String,
130+
) {
131+
// remove from "subscribers" while "subscribers" is being accessed
132+
model.unsubscribe(this)
133+
}
134+
},
135+
)
136+
137+
// this will trigger EventProducer.fire and loop through the list "subscribers"
138+
model.setOptAnyProperty("key1", "value1")
139+
140+
// ensure no concurrent modification exception is thrown and "subcribers" is clear
141+
model.hasSubscribers shouldBe false
142+
}
119143
})

0 commit comments

Comments
 (0)