Skip to content

Commit

Permalink
fix: setProviderAndWait does not hang on ProviderError (#88)
Browse files Browse the repository at this point in the history
Signed-off-by: Fabrizio Demaria <[email protected]>
  • Loading branch information
fabriziodemaria authored Jan 9, 2024
1 parent e8eea26 commit 1d4c24f
Show file tree
Hide file tree
Showing 8 changed files with 147 additions and 15 deletions.
5 changes: 3 additions & 2 deletions android/src/main/java/dev/openfeature/sdk/FeatureProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion android/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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? {
Expand Down
19 changes: 13 additions & 6 deletions android/src/main/java/dev/openfeature/sdk/async/Extensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<OpenFeatureEvents.ProviderReady>()
.onStart {
if (isProviderReady()) {
if (getProviderStatus() == OpenFeatureEvents.ProviderReady) {
this.emit(OpenFeatureEvents.ProviderReady)
}
}

internal fun FeatureProvider.observeProviderError() = observe<OpenFeatureEvents.ProviderError>()
.onStart {
val status = getProviderStatus()
if (status is OpenFeatureEvents.ProviderError) {
this.emit(status)
}
}

inline fun <reified T : OpenFeatureEvents> OpenFeatureAPI.observeEvents(): Flow<T>? {
return getProvider()?.observe<T>()
}

suspend fun FeatureProvider.awaitReady(
suspend fun FeatureProvider.awaitReadyOrError(
dispatcher: CoroutineDispatcher = Dispatchers.IO
) = suspendCancellableCoroutine { continuation ->
val coroutineScope = CoroutineScope(dispatcher)
Expand All @@ -60,10 +67,10 @@ suspend fun FeatureProvider.awaitReady(
}

coroutineScope.launch {
observe<OpenFeatureEvents.ProviderError>()
observeProviderError()
.take(1)
.collect {
continuation.resumeWith(Result.failure(it.error))
continuation.resumeWith(Result.success(Unit))
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -19,9 +18,6 @@ interface ProviderStatus {
fun getProviderStatus(): OpenFeatureEvents
}

fun FeatureProvider.isProviderReady(): Boolean =
getProviderStatus() == OpenFeatureEvents.ProviderReady

interface EventsPublisher {
fun publish(event: OpenFeatureEvents)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -74,7 +75,7 @@ class AlwaysBrokenProvider(
override fun observe(): Flow<OpenFeatureEvents> = flow { }

override fun getProviderStatus(): OpenFeatureEvents =
OpenFeatureEvents.ProviderError(FlagNotFoundError("test"))
OpenFeatureEvents.ProviderError(OpenFeatureError.GeneralError("Unknown error"))

class AlwaysBrokenProviderMetadata(override val name: String? = "test") : ProviderMetadata
}
96 changes: 96 additions & 0 deletions android/src/test/java/dev/openfeature/sdk/helpers/SlowProvider.kt
Original file line number Diff line number Diff line change
@@ -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<Hook<*>> = 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<Boolean> {
if (!ready) throw OpenFeatureError.FlagNotFoundError(key)
return ProviderEvaluation(!defaultValue)
}

override fun getStringEvaluation(
key: String,
defaultValue: String,
context: EvaluationContext?
): ProviderEvaluation<String> {
if (!ready) throw OpenFeatureError.FlagNotFoundError(key)
return ProviderEvaluation(defaultValue.reversed())
}

override fun getIntegerEvaluation(
key: String,
defaultValue: Int,
context: EvaluationContext?
): ProviderEvaluation<Int> {
if (!ready) throw OpenFeatureError.FlagNotFoundError(key)
return ProviderEvaluation(defaultValue * 100)
}

override fun getDoubleEvaluation(
key: String,
defaultValue: Double,
context: EvaluationContext?
): ProviderEvaluation<Double> {
if (!ready) throw OpenFeatureError.FlagNotFoundError(key)
return ProviderEvaluation(defaultValue * 100)
}

override fun getObjectEvaluation(
key: String,
defaultValue: Value,
context: EvaluationContext?
): ProviderEvaluation<Value> {
if (!ready) throw OpenFeatureError.FlagNotFoundError(key)
return ProviderEvaluation(Value.Null)
}

override fun observe(): Flow<OpenFeatureEvents> = flowOf()

override fun getProviderStatus(): OpenFeatureEvents = if (ready) {
OpenFeatureEvents.ProviderReady
} else {
OpenFeatureEvents.ProviderStale
}

data class SlowProviderMetadata(override val name: String?) : ProviderMetadata
}

0 comments on commit 1d4c24f

Please sign in to comment.