Skip to content

Commit c842528

Browse files
authored
2.4.1 Patch Build (#187)
* Cherry pick Dan's bug fix for concurrency problem * 2.4.1 Patch release --------- Co-authored-by: Evan Masseau <>
1 parent 9205720 commit c842528

File tree

12 files changed

+119
-17
lines changed

12 files changed

+119
-17
lines changed

README.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ send them timely push notifications via [FCM (Firebase Cloud Messaging)](https:/
5555
```kotlin
5656
// build.gradle.kts
5757
dependencies {
58-
implementation("com.github.klaviyo.klaviyo-android-sdk:analytics:2.4.0")
59-
implementation("com.github.klaviyo.klaviyo-android-sdk:push-fcm:2.4.0")
58+
implementation("com.github.klaviyo.klaviyo-android-sdk:analytics:2.4.1")
59+
implementation("com.github.klaviyo.klaviyo-android-sdk:push-fcm:2.4.1")
6060
}
6161
```
6262
</details>
@@ -67,8 +67,8 @@ send them timely push notifications via [FCM (Firebase Cloud Messaging)](https:/
6767
```groovy
6868
// build.gradle
6969
dependencies {
70-
implementation "com.github.klaviyo.klaviyo-android-sdk:analytics:2.4.0"
71-
implementation "com.github.klaviyo.klaviyo-android-sdk:push-fcm:2.4.0"
70+
implementation "com.github.klaviyo.klaviyo-android-sdk:analytics:2.4.1"
71+
implementation "com.github.klaviyo.klaviyo-android-sdk:push-fcm:2.4.1"
7272
}
7373
```
7474
</details>

docs/index.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
<!-- Redirect to latest version -->
2-
<meta HTTP-EQUIV="REFRESH" content="0; url=./2.4.0/index.html">
2+
<meta HTTP-EQUIV="REFRESH" content="0; url=./2.4.1/index.html">

sdk/analytics/build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ android {
1919
dependencies {
2020
implementation project(":sdk:core")
2121
testImplementation project(":sdk:fixtures")
22+
testImplementation KotlinX.coroutines.test
2223
}
2324

2425
afterEvaluate {

sdk/analytics/src/main/java/com/klaviyo/analytics/networking/KlaviyoApiClient.kt

+7-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import com.klaviyo.analytics.networking.requests.ProfileApiRequest
1414
import com.klaviyo.analytics.networking.requests.PushTokenApiRequest
1515
import com.klaviyo.core.Registry
1616
import com.klaviyo.core.lifecycle.ActivityEvent
17+
import java.util.Collections
1718
import java.util.concurrent.ConcurrentLinkedDeque
1819
import org.json.JSONArray
1920
import org.json.JSONException
@@ -33,7 +34,9 @@ internal object KlaviyoApiClient : ApiClient {
3334
/**
3435
* List of registered API observers
3536
*/
36-
private var apiObservers = mutableListOf<ApiObserver>()
37+
private val apiObservers = Collections.synchronizedList(
38+
mutableListOf<ApiObserver>()
39+
)
3740

3841
/**
3942
* Initialize logic including lifecycle observers and reviving the queue from persistent store
@@ -136,7 +139,9 @@ internal object KlaviyoApiClient : ApiClient {
136139
Registry.log.verbose("${request.responseCode} $response")
137140
}
138141

139-
apiObservers.forEach { it(request) }
142+
synchronized(apiObservers) {
143+
apiObservers.forEach { it(request) }
144+
}
140145
}
141146

142147
/**

sdk/analytics/src/test/java/com/klaviyo/analytics/networking/KlaviyoApiClientTest.kt

+24
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ import io.mockk.unmockkConstructor
3030
import io.mockk.unmockkObject
3131
import io.mockk.verify
3232
import java.net.URL
33+
import kotlinx.coroutines.Dispatchers
34+
import kotlinx.coroutines.launch
35+
import kotlinx.coroutines.test.runTest
36+
import kotlinx.coroutines.withContext
3337
import org.json.JSONObject
3438
import org.junit.After
3539
import org.junit.Assert.assertEquals
@@ -309,6 +313,26 @@ internal class KlaviyoApiClientTest : BaseTest() {
309313
verify { logSpy.verbose(match { it.contains("queue") }) }
310314
}
311315

316+
@Test
317+
fun `Concurrent modification exception does not get thrown on observers`() = runTest {
318+
val apiObserver: ApiObserver = { Thread.sleep(6) }
319+
val request = mockRequest()
320+
321+
KlaviyoApiClient.onApiRequest(true, apiObserver)
322+
323+
val job = launch(Dispatchers.IO) {
324+
KlaviyoApiClient.enqueueRequest(request)
325+
}
326+
val job2 = launch(Dispatchers.Default) {
327+
withContext(Dispatchers.IO) {
328+
Thread.sleep(8)
329+
}
330+
KlaviyoApiClient.offApiRequest(apiObserver)
331+
}
332+
job.start()
333+
job2.start()
334+
}
335+
312336
@Test
313337
fun `Invokes callback and logs when request sent`() {
314338
every { configMock.networkFlushDepth } returns 1

sdk/core/build.gradle

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import static de.fayard.refreshVersions.core.Versions.versionFor
22

33
dependencies {
44
testImplementation project(":sdk:fixtures")
5+
// coroutine testing
6+
testImplementation KotlinX.coroutines.test
57
}
68
description = "Core featureset of the Klaviyo SDK including SDK configuration, session tracking and analytics"
79
evaluationDependsOn(":sdk")

sdk/core/src/main/java/com/klaviyo/core/lifecycle/KlaviyoLifecycleMonitor.kt

+9-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.app.Activity
44
import android.app.Application
55
import android.os.Bundle
66
import com.klaviyo.core.Registry
7+
import java.util.Collections
78

89
/**
910
* Service for monitoring the application lifecycle and network connectivity
@@ -12,7 +13,9 @@ internal object KlaviyoLifecycleMonitor : LifecycleMonitor, Application.Activity
1213

1314
private var activeActivities = 0
1415

15-
private var activityObservers = mutableListOf<ActivityObserver>()
16+
private val activityObservers = Collections.synchronizedList(
17+
mutableListOf<ActivityObserver>()
18+
)
1619

1720
init {
1821
onActivityEvent { Registry.log.verbose(it.type) }
@@ -26,7 +29,11 @@ internal object KlaviyoLifecycleMonitor : LifecycleMonitor, Application.Activity
2629
activityObservers -= observer
2730
}
2831

29-
private fun broadcastEvent(event: ActivityEvent) = activityObservers.forEach { it(event) }
32+
private fun broadcastEvent(event: ActivityEvent) {
33+
synchronized(activityObservers) {
34+
activityObservers.forEach { it(event) }
35+
}
36+
}
3037

3138
//region ActivityLifecycleCallbacks
3239

sdk/core/src/main/java/com/klaviyo/core/model/SharedPreferencesDataStore.kt

+7-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.klaviyo.core.model
33
import android.content.Context
44
import android.content.SharedPreferences
55
import com.klaviyo.core.Registry
6+
import java.util.Collections
67

78
/**
89
* Simple DataStore implementation using SharedPreferences for persistence
@@ -18,7 +19,9 @@ internal object SharedPreferencesDataStore : DataStore {
1819
/**
1920
* List of registered observers
2021
*/
21-
private var storeObservers = mutableListOf<StoreObserver>()
22+
private val storeObservers = Collections.synchronizedList(
23+
mutableListOf<StoreObserver>()
24+
)
2225

2326
init {
2427
onStoreChange { key, value -> Registry.log.verbose("$key=$value") }
@@ -33,7 +36,9 @@ internal object SharedPreferencesDataStore : DataStore {
3336
}
3437

3538
private fun broadcastStoreChange(key: String, value: String?) {
36-
storeObservers.forEach { it(key, value) }
39+
synchronized(storeObservers) {
40+
storeObservers.forEach { it(key, value) }
41+
}
3742
}
3843

3944
/**

sdk/core/src/main/java/com/klaviyo/core/networking/KlaviyoNetworkMonitor.kt

+7-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import android.net.Network
66
import android.net.NetworkCapabilities
77
import android.net.NetworkRequest
88
import com.klaviyo.core.Registry
9+
import java.util.Collections
910

1011
/**
1112
* Service for monitoring the application lifecycle and network connectivity
@@ -23,7 +24,9 @@ internal object KlaviyoNetworkMonitor : NetworkMonitor {
2324
/**
2425
* List of registered network change observers
2526
*/
26-
private var networkChangeObservers = mutableListOf<NetworkObserver>()
27+
private val networkChangeObservers = Collections.synchronizedList(
28+
mutableListOf<NetworkObserver>()
29+
)
2730

2831
/**
2932
* Callback object to register with system
@@ -77,7 +80,9 @@ internal object KlaviyoNetworkMonitor : NetworkMonitor {
7780
*/
7881
private fun broadcastNetworkChange() {
7982
val isConnected = isNetworkConnected()
80-
networkChangeObservers.forEach { it(isConnected) }
83+
synchronized(networkChangeObservers) {
84+
networkChangeObservers.forEach { it(isConnected) }
85+
}
8186
}
8287

8388
/**

sdk/core/src/test/java/com/klaviyo/core/model/SharedPreferencesDataStoreTest.kt

+27
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import io.mockk.every
99
import io.mockk.mockk
1010
import io.mockk.unmockkObject
1111
import io.mockk.verify
12+
import kotlinx.coroutines.Dispatchers
13+
import kotlinx.coroutines.launch
14+
import kotlinx.coroutines.test.runTest
15+
import kotlinx.coroutines.withContext
1216
import org.junit.Assert.assertEquals
1317
import org.junit.Test
1418

@@ -71,6 +75,29 @@ internal class SharedPreferencesDataStoreTest : BaseTest() {
7175
verify { logSpy.verbose("$stubKey=$stubValue") }
7276
}
7377

78+
@Test
79+
fun `Store observers concurrency modification test`() = runTest {
80+
withPreferenceMock()
81+
withWriteStringMock(stubKey, stubValue)
82+
val observer: StoreObserver = { _, _ -> Thread.sleep(6) }
83+
84+
SharedPreferencesDataStore.onStoreChange(observer)
85+
86+
val job = launch(Dispatchers.IO) {
87+
SharedPreferencesDataStore.clear(stubKey)
88+
}
89+
90+
val job2 = launch(Dispatchers.Default) {
91+
withContext(Dispatchers.IO) {
92+
Thread.sleep(8)
93+
}
94+
SharedPreferencesDataStore.offStoreChange(observer)
95+
}
96+
97+
job.start()
98+
job2.start()
99+
}
100+
74101
@Test
75102
fun `Removing key uses Klaviyo preferences`() {
76103
withPreferenceMock()

sdk/core/src/test/java/com/klaviyo/core/networking/KlaviyoNetworkMonitorTest.kt

+28-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ import io.mockk.mockkConstructor
1313
import io.mockk.slot
1414
import io.mockk.unmockkObject
1515
import io.mockk.verify
16+
import kotlinx.coroutines.Dispatchers
17+
import kotlinx.coroutines.launch
18+
import kotlinx.coroutines.test.runTest
19+
import kotlinx.coroutines.withContext
1620
import org.junit.Assert.assertEquals
1721
import org.junit.Before
1822
import org.junit.Test
@@ -103,11 +107,11 @@ internal class KlaviyoNetworkMonitorTest : BaseTest() {
103107
fun `Network change observer is invoked with current network status when network changes`() {
104108
var expectedNetworkConnection = true
105109
var callCount = 0
106-
107-
KlaviyoNetworkMonitor.onNetworkChange {
110+
val observer: NetworkObserver = {
108111
assert(it == expectedNetworkConnection)
109112
callCount++
110113
}
114+
KlaviyoNetworkMonitor.onNetworkChange(observer)
111115

112116
assert(netCallbackSlot.isCaptured) // attaching a listener should have initialized the network callback
113117

@@ -130,6 +134,7 @@ internal class KlaviyoNetworkMonitorTest : BaseTest() {
130134
netCallbackSlot.captured.onLost(mockk())
131135

132136
assertEquals(6, callCount)
137+
KlaviyoNetworkMonitor.offNetworkChange(observer)
133138
}
134139

135140
@Test
@@ -167,4 +172,25 @@ internal class KlaviyoNetworkMonitorTest : BaseTest() {
167172
netCallbackSlot.captured.onAvailable(mockk())
168173
assertEquals(5, callCount)
169174
}
175+
176+
@Test()
177+
fun `Concurrent modification exception doesn't get thrown on concurrent observer access`() = runTest {
178+
val observer: NetworkObserver = { Thread.sleep(6) }
179+
180+
KlaviyoNetworkMonitor.onNetworkChange(observer)
181+
182+
val job = launch(Dispatchers.IO) {
183+
netCallbackSlot.captured.onAvailable(mockk())
184+
}
185+
186+
val job2 = launch(Dispatchers.Default) {
187+
withContext(Dispatchers.IO) {
188+
Thread.sleep(5)
189+
}
190+
KlaviyoNetworkMonitor.offNetworkChange(observer)
191+
}
192+
193+
job.start()
194+
job2.start()
195+
}
170196
}

versions.properties

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111

1212
# Project versioning, run the following gradle command to update version numbers automatically:
1313
# ./gradlew bumpVersion --nextVersion=X.Y.Z
14-
version.klaviyo.versionCode=18
14+
version.klaviyo.versionCode=19
1515

16-
version.klaviyo.versionName=2.4.0
16+
version.klaviyo.versionName=2.4.1
1717

1818
# Android versioning
1919

0 commit comments

Comments
 (0)