Skip to content

Commit 9cd73aa

Browse files
authored
fix: Coroutine resume bug (#118)
Signed-off-by: Nicklas Lundin <[email protected]>
1 parent eae77c7 commit 9cd73aa

File tree

5 files changed

+142
-21
lines changed

5 files changed

+142
-21
lines changed

android/src/main/java/dev/openfeature/sdk/events/FeatureProviderExtensions.kt

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@ import kotlinx.coroutines.CoroutineDispatcher
55
import kotlinx.coroutines.CoroutineScope
66
import kotlinx.coroutines.Dispatchers
77
import kotlinx.coroutines.cancel
8+
import kotlinx.coroutines.flow.merge
89
import kotlinx.coroutines.flow.onStart
910
import kotlinx.coroutines.flow.take
1011
import kotlinx.coroutines.launch
1112
import kotlinx.coroutines.suspendCancellableCoroutine
1213

1314
internal fun FeatureProvider.observeProviderReady() = observe<OpenFeatureEvents.ProviderReady>()
1415
.onStart {
15-
if (getProviderStatus() == OpenFeatureEvents.ProviderReady) {
16+
val status = getProviderStatus()
17+
if (status == OpenFeatureEvents.ProviderReady) {
1618
this.emit(OpenFeatureEvents.ProviderReady)
1719
}
1820
}
@@ -30,15 +32,7 @@ suspend fun FeatureProvider.awaitReadyOrError(
3032
) = suspendCancellableCoroutine { continuation ->
3133
val coroutineScope = CoroutineScope(dispatcher)
3234
coroutineScope.launch {
33-
observeProviderReady()
34-
.take(1)
35-
.collect {
36-
continuation.resumeWith(Result.success(Unit))
37-
}
38-
}
39-
40-
coroutineScope.launch {
41-
observeProviderError()
35+
merge(observeProviderReady(), observeProviderError())
4236
.take(1)
4337
.collect {
4438
continuation.resumeWith(Result.success(Unit))

android/src/test/java/dev/openfeature/sdk/DeveloperExperienceTests.kt

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@ package dev.openfeature.sdk
33
import dev.openfeature.sdk.events.OpenFeatureEvents
44
import dev.openfeature.sdk.exceptions.ErrorCode
55
import dev.openfeature.sdk.helpers.AlwaysBrokenProvider
6+
import dev.openfeature.sdk.helpers.AutoHealingProvider
67
import dev.openfeature.sdk.helpers.DoSomethingProvider
78
import dev.openfeature.sdk.helpers.GenericSpyHookMock
89
import dev.openfeature.sdk.helpers.SlowProvider
910
import kotlinx.coroutines.CoroutineScope
1011
import kotlinx.coroutines.ExperimentalCoroutinesApi
12+
import kotlinx.coroutines.async
13+
import kotlinx.coroutines.flow.toCollection
1114
import kotlinx.coroutines.launch
1215
import kotlinx.coroutines.test.StandardTestDispatcher
1316
import kotlinx.coroutines.test.UnconfinedTestDispatcher
@@ -71,7 +74,11 @@ class DeveloperExperienceTests {
7174
fun testSetProviderAndWaitReady() = runTest {
7275
val dispatcher = StandardTestDispatcher(testScheduler)
7376
CoroutineScope(dispatcher).launch {
74-
OpenFeatureAPI.setProviderAndWait(SlowProvider(dispatcher = dispatcher), dispatcher, ImmutableContext())
77+
OpenFeatureAPI.setProviderAndWait(
78+
SlowProvider(dispatcher = dispatcher),
79+
dispatcher,
80+
ImmutableContext()
81+
)
7582
}
7683
testScheduler.advanceTimeBy(1) // Make sure setProviderAndWait is called
7784
val booleanValue1 = OpenFeatureAPI.getClient().getBooleanValue("test", false)
@@ -108,4 +115,25 @@ class DeveloperExperienceTests {
108115
advanceUntilIdle()
109116
Assert.assertEquals(eventCount, 1)
110117
}
118+
119+
@Test
120+
fun testProviderThatHealsWithErrorThenReady() = runTest {
121+
val dispatcher = StandardTestDispatcher(testScheduler)
122+
val healing = AutoHealingProvider(dispatcher = dispatcher, healDelay = 100)
123+
val resultEvents = mutableListOf<OpenFeatureEvents>()
124+
val r = async {
125+
OpenFeatureAPI.observe<OpenFeatureEvents>().toCollection(resultEvents)
126+
}
127+
OpenFeatureAPI.setProviderAndWait(healing, dispatcher, ImmutableContext())
128+
Assert.assertEquals(2, resultEvents.size)
129+
val errorEvent = resultEvents[0]
130+
Assert.assertTrue(errorEvent is OpenFeatureEvents.ProviderError)
131+
Assert.assertEquals(
132+
"AutoHealingProvider is trying to heal",
133+
(errorEvent as OpenFeatureEvents.ProviderError).error.message
134+
)
135+
Assert.assertEquals(OpenFeatureEvents.ProviderReady, resultEvents[1])
136+
OpenFeatureAPI.shutdown()
137+
r.cancel()
138+
}
111139
}

android/src/test/java/dev/openfeature/sdk/EventsHandlerTest.kt

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class EventsHandlerTest {
2222
fun observing_event_observer_works() = runTest {
2323
val dispatcher = UnconfinedTestDispatcher(testScheduler)
2424
val eventHandler = EventHandler(dispatcher)
25-
val provider = TestFeatureProvider(dispatcher, eventHandler)
25+
val provider = TestFeatureProvider(eventHandler)
2626
var emitted = false
2727

2828
val job = backgroundScope.launch(dispatcher) {
@@ -41,7 +41,7 @@ class EventsHandlerTest {
4141
fun multiple_subscribers_works() = runTest {
4242
val dispatcher = UnconfinedTestDispatcher(testScheduler)
4343
val eventHandler = EventHandler(dispatcher)
44-
val provider = TestFeatureProvider(dispatcher, eventHandler)
44+
val provider = TestFeatureProvider(eventHandler)
4545
val numberOfSubscribers = 10
4646
val parentJob = Job()
4747
var emitted = 0
@@ -65,7 +65,7 @@ class EventsHandlerTest {
6565
fun canceling_one_subscriber_does_not_cancel_others() = runTest {
6666
val dispatcher = UnconfinedTestDispatcher(testScheduler)
6767
val eventHandler = EventHandler(dispatcher)
68-
val provider = TestFeatureProvider(dispatcher, eventHandler)
68+
val provider = TestFeatureProvider(eventHandler)
6969
val numberOfSubscribers = 10
7070
val parentJob = Job()
7171
var emitted = 0
@@ -95,7 +95,7 @@ class EventsHandlerTest {
9595
fun the_provider_status_stream_works() = runTest {
9696
val dispatcher = UnconfinedTestDispatcher(testScheduler)
9797
val eventHandler = EventHandler(dispatcher)
98-
val provider = TestFeatureProvider(dispatcher, eventHandler)
98+
val provider = TestFeatureProvider(eventHandler)
9999
var isProviderReady = false
100100

101101
// observing the provider status after the provider ready event is published
@@ -118,7 +118,7 @@ class EventsHandlerTest {
118118
var isProviderReady = false
119119
val dispatcher = UnconfinedTestDispatcher(testScheduler)
120120
val eventHandler = EventHandler(dispatcher)
121-
val provider = TestFeatureProvider(dispatcher, eventHandler)
121+
val provider = TestFeatureProvider(eventHandler)
122122

123123
// observing the provider status after the provider ready event is published
124124
val job = backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
@@ -137,7 +137,7 @@ class EventsHandlerTest {
137137
fun the_provider_status_stream_is_replays_current_status() = runTest {
138138
val dispatcher = UnconfinedTestDispatcher(testScheduler)
139139
val eventHandler = EventHandler(dispatcher)
140-
val provider = TestFeatureProvider(dispatcher, eventHandler)
140+
val provider = TestFeatureProvider(eventHandler)
141141
provider.emitReady()
142142
var isProviderReady = false
143143

@@ -158,7 +158,7 @@ class EventsHandlerTest {
158158
fun the_provider_becomes_stale() = runTest {
159159
val dispatcher = UnconfinedTestDispatcher(testScheduler)
160160
val eventHandler = EventHandler(dispatcher)
161-
val provider = TestFeatureProvider(dispatcher, eventHandler)
161+
val provider = TestFeatureProvider(eventHandler)
162162
var isProviderStale = false
163163

164164
val job = backgroundScope.launch(dispatcher) {
@@ -179,7 +179,7 @@ class EventsHandlerTest {
179179
fun accessing_status_from_provider_works() = runTest {
180180
val dispatcher = UnconfinedTestDispatcher(testScheduler)
181181
val eventHandler = EventHandler(dispatcher)
182-
val provider = TestFeatureProvider(dispatcher, eventHandler)
182+
val provider = TestFeatureProvider(eventHandler)
183183

184184
Assert.assertEquals(OpenFeatureEvents.ProviderNotReady, provider.getProviderStatus())
185185

android/src/test/java/dev/openfeature/sdk/TestFeatureProvider.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@ package dev.openfeature.sdk
22

33
import dev.openfeature.sdk.events.EventHandler
44
import dev.openfeature.sdk.events.OpenFeatureEvents
5-
import kotlinx.coroutines.CoroutineDispatcher
65

76
class TestFeatureProvider(
8-
dispatcher: CoroutineDispatcher,
97
private val eventHandler: EventHandler
108
) : FeatureProvider {
119
override val hooks: List<Hook<*>>
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package dev.openfeature.sdk.helpers
2+
3+
import dev.openfeature.sdk.EvaluationContext
4+
import dev.openfeature.sdk.FeatureProvider
5+
import dev.openfeature.sdk.Hook
6+
import dev.openfeature.sdk.ProviderEvaluation
7+
import dev.openfeature.sdk.ProviderMetadata
8+
import dev.openfeature.sdk.Value
9+
import dev.openfeature.sdk.events.EventHandler
10+
import dev.openfeature.sdk.events.OpenFeatureEvents
11+
import dev.openfeature.sdk.exceptions.OpenFeatureError
12+
import kotlinx.coroutines.CoroutineScope
13+
import kotlinx.coroutines.delay
14+
import kotlinx.coroutines.flow.Flow
15+
import kotlinx.coroutines.launch
16+
import kotlinx.coroutines.test.TestDispatcher
17+
18+
class AutoHealingProvider(
19+
val dispatcher: TestDispatcher,
20+
val healDelay: Long = 1000L,
21+
override val hooks: List<Hook<*>> = emptyList()
22+
) : FeatureProvider {
23+
override val metadata: ProviderMetadata = object : ProviderMetadata {
24+
override val name: String = "AutoHealingProvider"
25+
}
26+
private var ready = false
27+
private var eventHandler = EventHandler(dispatcher)
28+
override fun initialize(initialContext: EvaluationContext?) {
29+
CoroutineScope(dispatcher).launch {
30+
ready = false
31+
eventHandler.publish(OpenFeatureEvents.ProviderError(OpenFeatureError.ProviderNotReadyError("AutoHealingProvider is trying to heal")))
32+
delay(healDelay)
33+
ready = true
34+
eventHandler.publish(OpenFeatureEvents.ProviderReady)
35+
}
36+
}
37+
38+
override fun shutdown() {
39+
// no-op
40+
}
41+
42+
override fun onContextSet(
43+
oldContext: EvaluationContext?,
44+
newContext: EvaluationContext
45+
) {
46+
// no-op
47+
}
48+
49+
override fun getBooleanEvaluation(
50+
key: String,
51+
defaultValue: Boolean,
52+
context: EvaluationContext?
53+
): ProviderEvaluation<Boolean> {
54+
if (!ready) throw OpenFeatureError.FlagNotFoundError(key)
55+
return ProviderEvaluation(!defaultValue)
56+
}
57+
58+
override fun getStringEvaluation(
59+
key: String,
60+
defaultValue: String,
61+
context: EvaluationContext?
62+
): ProviderEvaluation<String> {
63+
if (!ready) throw OpenFeatureError.FlagNotFoundError(key)
64+
return ProviderEvaluation(defaultValue.reversed())
65+
}
66+
67+
override fun getIntegerEvaluation(
68+
key: String,
69+
defaultValue: Int,
70+
context: EvaluationContext?
71+
): ProviderEvaluation<Int> {
72+
if (!ready) throw OpenFeatureError.FlagNotFoundError(key)
73+
return ProviderEvaluation(defaultValue * 100)
74+
}
75+
76+
override fun getDoubleEvaluation(
77+
key: String,
78+
defaultValue: Double,
79+
context: EvaluationContext?
80+
): ProviderEvaluation<Double> {
81+
if (!ready) throw OpenFeatureError.FlagNotFoundError(key)
82+
return ProviderEvaluation(defaultValue * 100)
83+
}
84+
85+
override fun getObjectEvaluation(
86+
key: String,
87+
defaultValue: Value,
88+
context: EvaluationContext?
89+
): ProviderEvaluation<Value> {
90+
if (!ready) throw OpenFeatureError.FlagNotFoundError(key)
91+
return ProviderEvaluation(Value.Null)
92+
}
93+
94+
override fun observe(): Flow<OpenFeatureEvents> = eventHandler.observe()
95+
96+
override fun getProviderStatus(): OpenFeatureEvents = if (ready) {
97+
OpenFeatureEvents.ProviderReady
98+
} else {
99+
OpenFeatureEvents.ProviderStale
100+
}
101+
}

0 commit comments

Comments
 (0)