Skip to content

Commit 1d4c24f

Browse files
fix: setProviderAndWait does not hang on ProviderError (#88)
Signed-off-by: Fabrizio Demaria <[email protected]>
1 parent e8eea26 commit 1d4c24f

File tree

8 files changed

+147
-15
lines changed

8 files changed

+147
-15
lines changed

android/src/main/java/dev/openfeature/sdk/FeatureProvider.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ interface FeatureProvider : EventObserver, ProviderStatus {
88
val metadata: ProviderMetadata
99

1010
// Called by OpenFeatureAPI whenever the new Provider is registered
11+
// This function should never throw
1112
fun initialize(initialContext: EvaluationContext?)
1213

13-
// called when the lifecycle of the OpenFeatureClient is over
14-
// to release resources/threads.
14+
// Called when the lifecycle of the OpenFeatureClient is over
15+
// to release resources/threads
1516
fun shutdown()
1617

1718
// Called by OpenFeatureAPI whenever a new EvaluationContext is set by the application

android/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ object OpenFeatureAPI {
1111
fun setProvider(provider: FeatureProvider, initialContext: EvaluationContext? = null) {
1212
this@OpenFeatureAPI.provider = provider
1313
if (initialContext != null) context = initialContext
14-
provider.initialize(context)
14+
try {
15+
provider.initialize(context)
16+
} catch (e: Throwable) {
17+
// This is not allowed to happen
18+
}
1519
}
1620

1721
fun getProvider(): FeatureProvider? {

android/src/main/java/dev/openfeature/sdk/async/Extensions.kt

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import dev.openfeature.sdk.FeatureProvider
55
import dev.openfeature.sdk.OpenFeatureAPI
66
import dev.openfeature.sdk.OpenFeatureClient
77
import dev.openfeature.sdk.events.OpenFeatureEvents
8-
import dev.openfeature.sdk.events.isProviderReady
98
import dev.openfeature.sdk.events.observe
109
import kotlinx.coroutines.CoroutineDispatcher
1110
import kotlinx.coroutines.CoroutineScope
@@ -33,21 +32,29 @@ suspend fun OpenFeatureAPI.setProviderAndWait(
3332
initialContext: EvaluationContext? = null
3433
) {
3534
setProvider(provider, initialContext)
36-
provider.awaitReady(dispatcher)
35+
provider.awaitReadyOrError(dispatcher)
3736
}
3837

3938
internal fun FeatureProvider.observeProviderReady() = observe<OpenFeatureEvents.ProviderReady>()
4039
.onStart {
41-
if (isProviderReady()) {
40+
if (getProviderStatus() == OpenFeatureEvents.ProviderReady) {
4241
this.emit(OpenFeatureEvents.ProviderReady)
4342
}
4443
}
4544

45+
internal fun FeatureProvider.observeProviderError() = observe<OpenFeatureEvents.ProviderError>()
46+
.onStart {
47+
val status = getProviderStatus()
48+
if (status is OpenFeatureEvents.ProviderError) {
49+
this.emit(status)
50+
}
51+
}
52+
4653
inline fun <reified T : OpenFeatureEvents> OpenFeatureAPI.observeEvents(): Flow<T>? {
4754
return getProvider()?.observe<T>()
4855
}
4956

50-
suspend fun FeatureProvider.awaitReady(
57+
suspend fun FeatureProvider.awaitReadyOrError(
5158
dispatcher: CoroutineDispatcher = Dispatchers.IO
5259
) = suspendCancellableCoroutine { continuation ->
5360
val coroutineScope = CoroutineScope(dispatcher)
@@ -60,10 +67,10 @@ suspend fun FeatureProvider.awaitReady(
6067
}
6168

6269
coroutineScope.launch {
63-
observe<OpenFeatureEvents.ProviderError>()
70+
observeProviderError()
6471
.take(1)
6572
.collect {
66-
continuation.resumeWith(Result.failure(it.error))
73+
continuation.resumeWith(Result.success(Unit))
6774
}
6875
}
6976

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

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package dev.openfeature.sdk.events
22

3-
import dev.openfeature.sdk.FeatureProvider
43
import kotlinx.coroutines.CoroutineDispatcher
54
import kotlinx.coroutines.CoroutineScope
65
import kotlinx.coroutines.Job
@@ -19,9 +18,6 @@ interface ProviderStatus {
1918
fun getProviderStatus(): OpenFeatureEvents
2019
}
2120

22-
fun FeatureProvider.isProviderReady(): Boolean =
23-
getProviderStatus() == OpenFeatureEvents.ProviderReady
24-
2521
interface EventsPublisher {
2622
fun publish(event: OpenFeatureEvents)
2723
}

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
package dev.openfeature.sdk
22

3+
import dev.openfeature.sdk.async.setProviderAndWait
34
import dev.openfeature.sdk.exceptions.ErrorCode
45
import dev.openfeature.sdk.helpers.AlwaysBrokenProvider
56
import dev.openfeature.sdk.helpers.GenericSpyHookMock
7+
import dev.openfeature.sdk.helpers.SlowProvider
8+
import kotlinx.coroutines.CoroutineScope
69
import kotlinx.coroutines.ExperimentalCoroutinesApi
10+
import kotlinx.coroutines.launch
11+
import kotlinx.coroutines.test.StandardTestDispatcher
12+
import kotlinx.coroutines.test.UnconfinedTestDispatcher
713
import kotlinx.coroutines.test.runTest
814
import org.junit.Assert
915
import org.junit.Test
@@ -58,4 +64,26 @@ class DeveloperExperienceTests {
5864
Assert.assertEquals("Could not find flag named: test", details.errorMessage)
5965
Assert.assertEquals(Reason.ERROR.toString(), details.reason)
6066
}
67+
68+
@Test
69+
fun testSetProviderAndWaitReady() = runTest {
70+
val dispatcher = StandardTestDispatcher(testScheduler)
71+
CoroutineScope(dispatcher).launch {
72+
OpenFeatureAPI.setProviderAndWait(SlowProvider(dispatcher = dispatcher), dispatcher, ImmutableContext())
73+
}
74+
testScheduler.advanceTimeBy(1) // Make sure setProviderAndWait is called
75+
val booleanValue1 = OpenFeatureAPI.getClient().getBooleanValue("test", false)
76+
Assert.assertFalse(booleanValue1)
77+
testScheduler.advanceTimeBy(10000) // SlowProvider is now Ready
78+
val booleanValue2 = OpenFeatureAPI.getClient().getBooleanValue("test", false)
79+
Assert.assertTrue(booleanValue2)
80+
}
81+
82+
@Test
83+
fun testSetProviderAndWaitError() = runTest {
84+
val dispatcher = UnconfinedTestDispatcher()
85+
OpenFeatureAPI.setProviderAndWait(AlwaysBrokenProvider(), dispatcher, ImmutableContext())
86+
val booleanValue = OpenFeatureAPI.getClient().getBooleanValue("test", false)
87+
Assert.assertFalse(booleanValue)
88+
}
6189
}

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import dev.openfeature.sdk.async.observeProviderReady
44
import dev.openfeature.sdk.async.toAsync
55
import dev.openfeature.sdk.events.EventHandler
66
import dev.openfeature.sdk.events.OpenFeatureEvents
7-
import dev.openfeature.sdk.events.isProviderReady
87
import dev.openfeature.sdk.events.observe
98
import kotlinx.coroutines.CoroutineScope
109
import kotlinx.coroutines.ExperimentalCoroutinesApi

android/src/test/java/dev/openfeature/sdk/helpers/AlwaysBrokenProvider.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import dev.openfeature.sdk.ProviderEvaluation
77
import dev.openfeature.sdk.ProviderMetadata
88
import dev.openfeature.sdk.Value
99
import dev.openfeature.sdk.events.OpenFeatureEvents
10+
import dev.openfeature.sdk.exceptions.OpenFeatureError
1011
import dev.openfeature.sdk.exceptions.OpenFeatureError.FlagNotFoundError
1112
import kotlinx.coroutines.flow.Flow
1213
import kotlinx.coroutines.flow.flow
@@ -74,7 +75,7 @@ class AlwaysBrokenProvider(
7475
override fun observe(): Flow<OpenFeatureEvents> = flow { }
7576

7677
override fun getProviderStatus(): OpenFeatureEvents =
77-
OpenFeatureEvents.ProviderError(FlagNotFoundError("test"))
78+
OpenFeatureEvents.ProviderError(OpenFeatureError.GeneralError("Unknown error"))
7879

7980
class AlwaysBrokenProviderMetadata(override val name: String? = "test") : ProviderMetadata
8081
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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.CoroutineDispatcher
13+
import kotlinx.coroutines.CoroutineScope
14+
import kotlinx.coroutines.delay
15+
import kotlinx.coroutines.flow.Flow
16+
import kotlinx.coroutines.flow.flowOf
17+
import kotlinx.coroutines.launch
18+
19+
class SlowProvider(override val hooks: List<Hook<*>> = listOf(), private var dispatcher: CoroutineDispatcher) : FeatureProvider {
20+
override val metadata: ProviderMetadata = SlowProviderMetadata("Slow provider")
21+
private var ready = false
22+
private var eventHandler = EventHandler(dispatcher)
23+
override fun initialize(initialContext: EvaluationContext?) {
24+
CoroutineScope(dispatcher).launch {
25+
delay(10000)
26+
ready = true
27+
eventHandler.publish(OpenFeatureEvents.ProviderReady)
28+
}
29+
}
30+
31+
override fun shutdown() {
32+
// no-op
33+
}
34+
35+
override fun onContextSet(
36+
oldContext: EvaluationContext?,
37+
newContext: EvaluationContext
38+
) {
39+
// no-op
40+
}
41+
42+
override fun getBooleanEvaluation(
43+
key: String,
44+
defaultValue: Boolean,
45+
context: EvaluationContext?
46+
): ProviderEvaluation<Boolean> {
47+
if (!ready) throw OpenFeatureError.FlagNotFoundError(key)
48+
return ProviderEvaluation(!defaultValue)
49+
}
50+
51+
override fun getStringEvaluation(
52+
key: String,
53+
defaultValue: String,
54+
context: EvaluationContext?
55+
): ProviderEvaluation<String> {
56+
if (!ready) throw OpenFeatureError.FlagNotFoundError(key)
57+
return ProviderEvaluation(defaultValue.reversed())
58+
}
59+
60+
override fun getIntegerEvaluation(
61+
key: String,
62+
defaultValue: Int,
63+
context: EvaluationContext?
64+
): ProviderEvaluation<Int> {
65+
if (!ready) throw OpenFeatureError.FlagNotFoundError(key)
66+
return ProviderEvaluation(defaultValue * 100)
67+
}
68+
69+
override fun getDoubleEvaluation(
70+
key: String,
71+
defaultValue: Double,
72+
context: EvaluationContext?
73+
): ProviderEvaluation<Double> {
74+
if (!ready) throw OpenFeatureError.FlagNotFoundError(key)
75+
return ProviderEvaluation(defaultValue * 100)
76+
}
77+
78+
override fun getObjectEvaluation(
79+
key: String,
80+
defaultValue: Value,
81+
context: EvaluationContext?
82+
): ProviderEvaluation<Value> {
83+
if (!ready) throw OpenFeatureError.FlagNotFoundError(key)
84+
return ProviderEvaluation(Value.Null)
85+
}
86+
87+
override fun observe(): Flow<OpenFeatureEvents> = flowOf()
88+
89+
override fun getProviderStatus(): OpenFeatureEvents = if (ready) {
90+
OpenFeatureEvents.ProviderReady
91+
} else {
92+
OpenFeatureEvents.ProviderStale
93+
}
94+
95+
data class SlowProviderMetadata(override val name: String?) : ProviderMetadata
96+
}

0 commit comments

Comments
 (0)