Skip to content

Commit 304b5e1

Browse files
authored
fix: Initialize call in MultiProvider never completes when listening to non-completing flow (#186)
Signed-off-by: penguindan <[email protected]>
1 parent 09dd988 commit 304b5e1

File tree

3 files changed

+42
-17
lines changed

3 files changed

+42
-17
lines changed

kotlin-sdk/src/commonMain/kotlin/dev/openfeature/kotlin/sdk/OpenFeatureAPI.kt

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ import kotlinx.coroutines.Dispatchers
1010
import kotlinx.coroutines.ExperimentalCoroutinesApi
1111
import kotlinx.coroutines.Job
1212
import kotlinx.coroutines.SupervisorJob
13-
import kotlinx.coroutines.cancel
14-
import kotlinx.coroutines.cancelChildren
1513
import kotlinx.coroutines.flow.Flow
1614
import kotlinx.coroutines.flow.FlowCollector
1715
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -26,7 +24,6 @@ object OpenFeatureAPI {
2624
private var setProviderJob: Job? = null
2725
private var setEvaluationContextJob: Job? = null
2826
private var observeProviderEventsJob: Job? = null
29-
private var providerEventObservationScope: CoroutineScope? = null
3027

3128
private val NOOP_PROVIDER = NoOpProvider()
3229
private var provider: FeatureProvider = NOOP_PROVIDER
@@ -242,8 +239,6 @@ object OpenFeatureAPI {
242239
observeProviderEventsJob?.cancel(
243240
CancellationException("Provider event observe job was cancelled due to shutdown")
244241
)
245-
providerEventObservationScope?.coroutineContext?.cancelChildren()
246-
providerEventObservationScope?.coroutineContext?.cancel()
247242
clearProvider()
248243
}
249244

kotlin-sdk/src/commonMain/kotlin/dev/openfeature/kotlin/sdk/multiprovider/MultiProvider.kt

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ import dev.openfeature.kotlin.sdk.Value
1010
import dev.openfeature.kotlin.sdk.events.OpenFeatureProviderEvents
1111
import dev.openfeature.kotlin.sdk.events.toOpenFeatureStatusError
1212
import dev.openfeature.kotlin.sdk.exceptions.OpenFeatureError
13+
import kotlinx.coroutines.CancellationException
14+
import kotlinx.coroutines.CoroutineScope
15+
import kotlinx.coroutines.Job
16+
import kotlinx.coroutines.SupervisorJob
1317
import kotlinx.coroutines.async
1418
import kotlinx.coroutines.awaitAll
1519
import kotlinx.coroutines.coroutineScope
@@ -21,6 +25,7 @@ import kotlinx.coroutines.flow.asStateFlow
2125
import kotlinx.coroutines.flow.launchIn
2226
import kotlinx.coroutines.flow.onEach
2327
import kotlinx.coroutines.flow.update
28+
import kotlinx.coroutines.launch
2429

2530
/**
2631
* Type alias for a function that evaluates a feature flag using a FeatureProvider.
@@ -154,6 +159,8 @@ class MultiProvider(
154159
}
155160
}
156161

162+
private var observeProviderEventsJob: Job? = null
163+
157164
/**
158165
* @return Number of unique providers
159166
*/
@@ -169,20 +176,27 @@ class MultiProvider(
169176
*/
170177
override suspend fun initialize(initialContext: EvaluationContext?) {
171178
coroutineScope {
172-
// Listen to events emitted by providers to emit our own set of events
173-
// according to https://openfeature.dev/specification/appendix-a/#status-and-event-handling
174-
childFeatureProviders.forEach { provider ->
175-
provider.observe()
176-
.onEach { event ->
177-
handleProviderEvent(provider, event)
178-
}
179-
.launchIn(this)
179+
observeProviderEventsJob?.cancel(
180+
cause = CancellationException("Observe provider events job cancelled due to new initialize call")
181+
)
182+
observeProviderEventsJob = CoroutineScope(this.coroutineContext + SupervisorJob()).launch {
183+
// Listen to events emitted by providers to emit our own set of events
184+
// according to https://openfeature.dev/specification/appendix-a/#status-and-event-handling
185+
childFeatureProviders.forEach { provider ->
186+
provider.observe()
187+
.onEach { event ->
188+
handleProviderEvent(provider, event)
189+
}
190+
.launchIn(this)
191+
}
180192
}
181193

182-
// State updates captured by observing individual Feature Flag providers
183-
childFeatureProviders
184-
.map { async { it.initialize(initialContext) } }
185-
.awaitAll()
194+
launch {
195+
// State updates captured by observing individual Feature Flag providers
196+
childFeatureProviders
197+
.map { async { it.initialize(initialContext) } }
198+
.awaitAll()
199+
}
186200
}
187201
}
188202

@@ -221,6 +235,10 @@ class MultiProvider(
221235
* This allows providers to clean up resources and complete any pending operations.
222236
*/
223237
override fun shutdown() {
238+
observeProviderEventsJob?.cancel(
239+
cause = CancellationException("Observe provider events job cancelled due to shutdown")
240+
)
241+
224242
val shutdownErrors = mutableListOf<Pair<String, Throwable>>()
225243
childFeatureProviders.forEach { provider ->
226244
try {

kotlin-sdk/src/commonTest/kotlin/dev/openfeature/kotlin/sdk/multiprovider/MultiProviderTests.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import kotlinx.coroutines.flow.first
1818
import kotlinx.coroutines.launch
1919
import kotlinx.coroutines.test.advanceUntilIdle
2020
import kotlinx.coroutines.test.runTest
21+
import kotlinx.coroutines.withTimeout
2122
import kotlin.test.Test
2223
import kotlin.test.assertEquals
2324
import kotlin.test.assertFailsWith
@@ -368,6 +369,17 @@ class MultiProviderTests {
368369
assertTrue(suppressedMessages.any { it.contains("Provider 'bad1' shutdown failed: oops1") })
369370
assertTrue(suppressedMessages.any { it.contains("Provider '<unnamed>' shutdown failed: oops2") })
370371
}
372+
373+
@Test
374+
fun initializeFunctionCompletesWhenObservingNeverCompletingFlows() = runTest {
375+
val fakeEventProvider = FakeEventProvider(name = "ok")
376+
377+
// Should complete immediately
378+
withTimeout(1000) {
379+
val multi = MultiProvider(listOf(fakeEventProvider))
380+
multi.initialize(null)
381+
}
382+
}
371383
}
372384

373385
// Helpers

0 commit comments

Comments
 (0)