Skip to content

Commit 25f86df

Browse files
Fix a race condition with StreamVideo.removeClient() (#1553)
* Fix race condition with remove client * Spotless
1 parent 0846c4b commit 25f86df

File tree

2 files changed

+60
-3
lines changed

2 files changed

+60
-3
lines changed

stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideo.kt

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -219,9 +219,13 @@ public interface StreamVideo : NotificationHandler {
219219
* Uninstall a previous [StreamVideo] instance.
220220
*/
221221
public fun removeClient() {
222-
isInstalled = false
223-
internalStreamVideo?.cleanup()
224-
internalStreamVideo = null
222+
val client = synchronized(this) {
223+
internalStreamVideo?.also {
224+
isInstalled = false
225+
internalStreamVideo = null
226+
}
227+
} ?: return
228+
client.cleanup()
225229
}
226230

227231
/**

stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/StreamVideoBuilderTest.kt

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,16 @@ import io.getstream.video.android.core.call.CallType
2121
import io.getstream.video.android.core.notifications.internal.service.CallServiceConfigRegistry
2222
import io.getstream.video.android.core.notifications.internal.service.callServiceConfig
2323
import io.getstream.video.android.model.User
24+
import io.mockk.every
25+
import io.mockk.mockk
2426
import junit.framework.TestCase.assertEquals
27+
import junit.framework.TestCase.assertTrue
2528
import org.junit.Test
2629
import org.junit.runner.RunWith
2730
import org.robolectric.RobolectricTestRunner
31+
import java.util.concurrent.CountDownLatch
32+
import java.util.concurrent.TimeUnit
33+
import kotlin.concurrent.thread
2834

2935
@RunWith(RobolectricTestRunner::class)
3036
class StreamVideoBuilderTest : TestBase() {
@@ -104,4 +110,51 @@ class StreamVideoBuilderTest : TestBase() {
104110
assertEquals(client.coordinatorConnectionModule.apiUrl, customApiUrl)
105111
assertEquals(client.coordinatorConnectionModule.wssUrl, customWssUrl)
106112
}
113+
114+
@Test
115+
fun build_afterRemovingClientDuringCleanup_doesNotThrow() {
116+
StreamVideo.removeClient()
117+
118+
val cleanupStarted = CountDownLatch(1)
119+
val cleanupRelease = CountDownLatch(1)
120+
val blockingClient = mockk<StreamVideo>(relaxed = true)
121+
122+
every { blockingClient.cleanup() } answers {
123+
cleanupStarted.countDown()
124+
cleanupRelease.await(5, TimeUnit.SECONDS)
125+
}
126+
127+
StreamVideo.install(blockingClient)
128+
129+
val removeThread = thread(start = true) {
130+
StreamVideo.removeClient()
131+
}
132+
133+
val buildResult = try {
134+
assertTrue(
135+
"Timeout waiting for cleanup to start",
136+
cleanupStarted.await(5, TimeUnit.SECONDS),
137+
)
138+
139+
runCatching {
140+
StreamVideoBuilder(
141+
ensureSingleInstance = true,
142+
context = context,
143+
apiKey = authData!!.apiKey,
144+
user = User.anonymous(),
145+
token = "anonymous-token",
146+
).build()
147+
}
148+
} catch (it: Exception) {
149+
it.printStackTrace()
150+
throw it
151+
} finally {
152+
cleanupRelease.countDown()
153+
removeThread.join(TimeUnit.SECONDS.toMillis(5))
154+
StreamVideo.removeClient()
155+
}
156+
157+
println("buildResult: $buildResult")
158+
assertTrue("Expected builder to succeed after removeClient", buildResult.isSuccess)
159+
}
107160
}

0 commit comments

Comments
 (0)