Skip to content

Commit 3f4651f

Browse files
authored
Merge pull request #1970 from OneSignal/ConcurrentModificationInEventProducer
Concurrent modification in event producer
2 parents 709b441 + a74f95d commit 3f4651f

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
@@ -45,10 +45,9 @@ open class EventProducer<THandler> : IEventNotifier<THandler> {
4545
* @param callback The callback will be invoked for each subscribed handler, allowing you to call the handler.
4646
*/
4747
fun fire(callback: (THandler) -> Unit) {
48-
synchronized(subscribers) {
49-
for (s in subscribers) {
50-
callback(s)
51-
}
48+
val localList = subscribers.toList()
49+
for (s in localList) {
50+
callback(s)
5251
}
5352
}
5453

@@ -61,7 +60,8 @@ open class EventProducer<THandler> : IEventNotifier<THandler> {
6160
*/
6261
fun fireOnMain(callback: (THandler) -> Unit) {
6362
suspendifyOnMain {
64-
for (s in subscribers) {
63+
val localList = subscribers.toList()
64+
for (s in localList) {
6565
callback(s)
6666
}
6767
}
@@ -74,7 +74,8 @@ open class EventProducer<THandler> : IEventNotifier<THandler> {
7474
* @param callback The callback will be invoked for each subscribed handler, allowing you to call the handler.
7575
*/
7676
suspend fun suspendingFire(callback: suspend (THandler) -> Unit) {
77-
for (s in subscribers) {
77+
val localList = subscribers.toList()
78+
for (s in localList) {
7879
callback(s)
7980
}
8081
}
@@ -87,7 +88,8 @@ open class EventProducer<THandler> : IEventNotifier<THandler> {
8788
*/
8889
suspend fun suspendingFireOnMain(callback: suspend (THandler) -> Unit) {
8990
withContext(Dispatchers.Main) {
90-
for (s in subscribers) {
91+
val localList = subscribers.toList()
92+
for (s in localList) {
9193
callback(s)
9294
}
9395
}

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)