diff --git a/android/src/main/java/dev/openfeature/sdk/FeatureProvider.kt b/android/src/main/java/dev/openfeature/sdk/FeatureProvider.kt index 9de1a041..09523b63 100644 --- a/android/src/main/java/dev/openfeature/sdk/FeatureProvider.kt +++ b/android/src/main/java/dev/openfeature/sdk/FeatureProvider.kt @@ -8,10 +8,11 @@ interface FeatureProvider : EventObserver, ProviderStatus { val metadata: ProviderMetadata // Called by OpenFeatureAPI whenever the new Provider is registered + // This function should never throw fun initialize(initialContext: EvaluationContext?) - // called when the lifecycle of the OpenFeatureClient is over - // to release resources/threads. + // Called when the lifecycle of the OpenFeatureClient is over + // to release resources/threads fun shutdown() // Called by OpenFeatureAPI whenever a new EvaluationContext is set by the application diff --git a/android/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt b/android/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt index fc247dfe..f487202d 100644 --- a/android/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt +++ b/android/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt @@ -11,7 +11,11 @@ object OpenFeatureAPI { fun setProvider(provider: FeatureProvider, initialContext: EvaluationContext? = null) { this@OpenFeatureAPI.provider = provider if (initialContext != null) context = initialContext - provider.initialize(context) + try { + provider.initialize(context) + } catch (e: Throwable) { + // This is not allowed to happen + } } fun getProvider(): FeatureProvider? { diff --git a/android/src/main/java/dev/openfeature/sdk/async/Extensions.kt b/android/src/main/java/dev/openfeature/sdk/async/Extensions.kt index af049766..a583cd5f 100644 --- a/android/src/main/java/dev/openfeature/sdk/async/Extensions.kt +++ b/android/src/main/java/dev/openfeature/sdk/async/Extensions.kt @@ -5,7 +5,6 @@ import dev.openfeature.sdk.FeatureProvider import dev.openfeature.sdk.OpenFeatureAPI import dev.openfeature.sdk.OpenFeatureClient import dev.openfeature.sdk.events.OpenFeatureEvents -import dev.openfeature.sdk.events.isProviderReady import dev.openfeature.sdk.events.observe import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope @@ -33,21 +32,29 @@ suspend fun OpenFeatureAPI.setProviderAndWait( initialContext: EvaluationContext? = null ) { setProvider(provider, initialContext) - provider.awaitReady(dispatcher) + provider.awaitReadyOrError(dispatcher) } internal fun FeatureProvider.observeProviderReady() = observe() .onStart { - if (isProviderReady()) { + if (getProviderStatus() == OpenFeatureEvents.ProviderReady) { this.emit(OpenFeatureEvents.ProviderReady) } } +internal fun FeatureProvider.observeProviderError() = observe() + .onStart { + val status = getProviderStatus() + if (status is OpenFeatureEvents.ProviderError) { + this.emit(status) + } + } + inline fun OpenFeatureAPI.observeEvents(): Flow? { return getProvider()?.observe() } -suspend fun FeatureProvider.awaitReady( +suspend fun FeatureProvider.awaitReadyOrError( dispatcher: CoroutineDispatcher = Dispatchers.IO ) = suspendCancellableCoroutine { continuation -> val coroutineScope = CoroutineScope(dispatcher) @@ -60,10 +67,10 @@ suspend fun FeatureProvider.awaitReady( } coroutineScope.launch { - observe() + observeProviderError() .take(1) .collect { - continuation.resumeWith(Result.failure(it.error)) + continuation.resumeWith(Result.success(Unit)) } } diff --git a/android/src/main/java/dev/openfeature/sdk/events/EventHandler.kt b/android/src/main/java/dev/openfeature/sdk/events/EventHandler.kt index 24141460..1e6ab8c4 100644 --- a/android/src/main/java/dev/openfeature/sdk/events/EventHandler.kt +++ b/android/src/main/java/dev/openfeature/sdk/events/EventHandler.kt @@ -1,6 +1,5 @@ package dev.openfeature.sdk.events -import dev.openfeature.sdk.FeatureProvider import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -19,9 +18,6 @@ interface ProviderStatus { fun getProviderStatus(): OpenFeatureEvents } -fun FeatureProvider.isProviderReady(): Boolean = - getProviderStatus() == OpenFeatureEvents.ProviderReady - interface EventsPublisher { fun publish(event: OpenFeatureEvents) } diff --git a/android/src/test/java/dev/openfeature/sdk/DeveloperExperienceTests.kt b/android/src/test/java/dev/openfeature/sdk/DeveloperExperienceTests.kt index 3e6c7ba6..398fc630 100644 --- a/android/src/test/java/dev/openfeature/sdk/DeveloperExperienceTests.kt +++ b/android/src/test/java/dev/openfeature/sdk/DeveloperExperienceTests.kt @@ -1,9 +1,15 @@ package dev.openfeature.sdk +import dev.openfeature.sdk.async.setProviderAndWait import dev.openfeature.sdk.exceptions.ErrorCode import dev.openfeature.sdk.helpers.AlwaysBrokenProvider import dev.openfeature.sdk.helpers.GenericSpyHookMock +import dev.openfeature.sdk.helpers.SlowProvider +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.Assert import org.junit.Test @@ -58,4 +64,26 @@ class DeveloperExperienceTests { Assert.assertEquals("Could not find flag named: test", details.errorMessage) Assert.assertEquals(Reason.ERROR.toString(), details.reason) } + + @Test + fun testSetProviderAndWaitReady() = runTest { + val dispatcher = StandardTestDispatcher(testScheduler) + CoroutineScope(dispatcher).launch { + OpenFeatureAPI.setProviderAndWait(SlowProvider(dispatcher = dispatcher), dispatcher, ImmutableContext()) + } + testScheduler.advanceTimeBy(1) // Make sure setProviderAndWait is called + val booleanValue1 = OpenFeatureAPI.getClient().getBooleanValue("test", false) + Assert.assertFalse(booleanValue1) + testScheduler.advanceTimeBy(10000) // SlowProvider is now Ready + val booleanValue2 = OpenFeatureAPI.getClient().getBooleanValue("test", false) + Assert.assertTrue(booleanValue2) + } + + @Test + fun testSetProviderAndWaitError() = runTest { + val dispatcher = UnconfinedTestDispatcher() + OpenFeatureAPI.setProviderAndWait(AlwaysBrokenProvider(), dispatcher, ImmutableContext()) + val booleanValue = OpenFeatureAPI.getClient().getBooleanValue("test", false) + Assert.assertFalse(booleanValue) + } } \ No newline at end of file diff --git a/android/src/test/java/dev/openfeature/sdk/EventsHandlerTest.kt b/android/src/test/java/dev/openfeature/sdk/EventsHandlerTest.kt index 7116cd28..97b52910 100644 --- a/android/src/test/java/dev/openfeature/sdk/EventsHandlerTest.kt +++ b/android/src/test/java/dev/openfeature/sdk/EventsHandlerTest.kt @@ -4,7 +4,6 @@ import dev.openfeature.sdk.async.observeProviderReady import dev.openfeature.sdk.async.toAsync import dev.openfeature.sdk.events.EventHandler import dev.openfeature.sdk.events.OpenFeatureEvents -import dev.openfeature.sdk.events.isProviderReady import dev.openfeature.sdk.events.observe import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi diff --git a/android/src/test/java/dev/openfeature/sdk/helpers/AlwaysBrokenProvider.kt b/android/src/test/java/dev/openfeature/sdk/helpers/AlwaysBrokenProvider.kt index 27f484e4..a99e6135 100644 --- a/android/src/test/java/dev/openfeature/sdk/helpers/AlwaysBrokenProvider.kt +++ b/android/src/test/java/dev/openfeature/sdk/helpers/AlwaysBrokenProvider.kt @@ -7,6 +7,7 @@ import dev.openfeature.sdk.ProviderEvaluation import dev.openfeature.sdk.ProviderMetadata import dev.openfeature.sdk.Value import dev.openfeature.sdk.events.OpenFeatureEvents +import dev.openfeature.sdk.exceptions.OpenFeatureError import dev.openfeature.sdk.exceptions.OpenFeatureError.FlagNotFoundError import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow @@ -74,7 +75,7 @@ class AlwaysBrokenProvider( override fun observe(): Flow = flow { } override fun getProviderStatus(): OpenFeatureEvents = - OpenFeatureEvents.ProviderError(FlagNotFoundError("test")) + OpenFeatureEvents.ProviderError(OpenFeatureError.GeneralError("Unknown error")) class AlwaysBrokenProviderMetadata(override val name: String? = "test") : ProviderMetadata } \ No newline at end of file diff --git a/android/src/test/java/dev/openfeature/sdk/helpers/SlowProvider.kt b/android/src/test/java/dev/openfeature/sdk/helpers/SlowProvider.kt new file mode 100644 index 00000000..d391ad83 --- /dev/null +++ b/android/src/test/java/dev/openfeature/sdk/helpers/SlowProvider.kt @@ -0,0 +1,96 @@ +package dev.openfeature.sdk.helpers + +import dev.openfeature.sdk.EvaluationContext +import dev.openfeature.sdk.FeatureProvider +import dev.openfeature.sdk.Hook +import dev.openfeature.sdk.ProviderEvaluation +import dev.openfeature.sdk.ProviderMetadata +import dev.openfeature.sdk.Value +import dev.openfeature.sdk.events.EventHandler +import dev.openfeature.sdk.events.OpenFeatureEvents +import dev.openfeature.sdk.exceptions.OpenFeatureError +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.launch + +class SlowProvider(override val hooks: List> = listOf(), private var dispatcher: CoroutineDispatcher) : FeatureProvider { + override val metadata: ProviderMetadata = SlowProviderMetadata("Slow provider") + private var ready = false + private var eventHandler = EventHandler(dispatcher) + override fun initialize(initialContext: EvaluationContext?) { + CoroutineScope(dispatcher).launch { + delay(10000) + ready = true + eventHandler.publish(OpenFeatureEvents.ProviderReady) + } + } + + override fun shutdown() { + // no-op + } + + override fun onContextSet( + oldContext: EvaluationContext?, + newContext: EvaluationContext + ) { + // no-op + } + + override fun getBooleanEvaluation( + key: String, + defaultValue: Boolean, + context: EvaluationContext? + ): ProviderEvaluation { + if (!ready) throw OpenFeatureError.FlagNotFoundError(key) + return ProviderEvaluation(!defaultValue) + } + + override fun getStringEvaluation( + key: String, + defaultValue: String, + context: EvaluationContext? + ): ProviderEvaluation { + if (!ready) throw OpenFeatureError.FlagNotFoundError(key) + return ProviderEvaluation(defaultValue.reversed()) + } + + override fun getIntegerEvaluation( + key: String, + defaultValue: Int, + context: EvaluationContext? + ): ProviderEvaluation { + if (!ready) throw OpenFeatureError.FlagNotFoundError(key) + return ProviderEvaluation(defaultValue * 100) + } + + override fun getDoubleEvaluation( + key: String, + defaultValue: Double, + context: EvaluationContext? + ): ProviderEvaluation { + if (!ready) throw OpenFeatureError.FlagNotFoundError(key) + return ProviderEvaluation(defaultValue * 100) + } + + override fun getObjectEvaluation( + key: String, + defaultValue: Value, + context: EvaluationContext? + ): ProviderEvaluation { + if (!ready) throw OpenFeatureError.FlagNotFoundError(key) + return ProviderEvaluation(Value.Null) + } + + override fun observe(): Flow = flowOf() + + override fun getProviderStatus(): OpenFeatureEvents = if (ready) { + OpenFeatureEvents.ProviderReady + } else { + OpenFeatureEvents.ProviderStale + } + + data class SlowProviderMetadata(override val name: String?) : ProviderMetadata +} \ No newline at end of file