Skip to content

Commit 4eb640b

Browse files
committed
refactor: api changes and alignment with spec
Signed-off-by: Nicklas Lundin <[email protected]> Signed-off-by: Nicklas Lundin <[email protected]> fix: mark as throws Signed-off-by: Nicklas Lundin <[email protected]> Signed-off-by: Nicklas Lundin <[email protected]> Signed-off-by: Nicklas Lundin <[email protected]> Signed-off-by: Nicklas Lundin <[email protected]> fix: add extra buffer capacity for the status flow Signed-off-by: Nicklas Lundin <[email protected]> fix: short circuit on fatal/non ready status refactor!: align setProvider API naming with Swift SDK Signed-off-by: Nicklas Lundin <[email protected]>
1 parent 732bf79 commit 4eb640b

21 files changed

+455
-596
lines changed

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

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,35 @@
11
package dev.openfeature.sdk
22

3-
import dev.openfeature.sdk.events.EventObserver
4-
import dev.openfeature.sdk.events.ProviderStatus
3+
import dev.openfeature.sdk.exceptions.OpenFeatureError
4+
import kotlin.jvm.Throws
55

6-
interface FeatureProvider : EventObserver, ProviderStatus {
6+
interface FeatureProvider {
77
val hooks: List<Hook<*>>
88
val metadata: ProviderMetadata
99

10-
// Called by OpenFeatureAPI whenever the new Provider is registered
11-
// This function should never throw
12-
fun initialize(initialContext: EvaluationContext?)
10+
/**
11+
* Called by OpenFeatureAPI whenever the new Provider is registered
12+
* This function should block until ready and throw exceptions if it fails to initialize
13+
* @param initialContext any initial context to be set before the provider is ready
14+
*/
15+
@Throws(OpenFeatureError::class)
16+
suspend fun initialize(initialContext: EvaluationContext?)
1317

14-
// Called when the lifecycle of the OpenFeatureClient is over
15-
// to release resources/threads
18+
/**
19+
* Called when the lifecycle of the OpenFeatureClient is over to release resources/threads
20+
*/
1621
fun shutdown()
1722

18-
// Called by OpenFeatureAPI whenever a new EvaluationContext is set by the application
19-
fun onContextSet(oldContext: EvaluationContext?, newContext: EvaluationContext)
23+
/**
24+
* Called by OpenFeatureAPI whenever a new EvaluationContext is set by the application
25+
* Perform blocking work here until the provider is ready again or throws an exception
26+
* @param oldContext The old EvaluationContext
27+
* @param newContext The new EvaluationContext
28+
* @throws OpenFeatureError if the provider cannot perform the task
29+
*/
30+
@Throws(OpenFeatureError::class)
31+
suspend fun onContextSet(oldContext: EvaluationContext?, newContext: EvaluationContext)
32+
2033
fun getBooleanEvaluation(
2134
key: String,
2235
defaultValue: Boolean,

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

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,16 @@
11
package dev.openfeature.sdk
22

3-
import dev.openfeature.sdk.events.OpenFeatureEvents
4-
import kotlinx.coroutines.flow.Flow
5-
import kotlinx.coroutines.flow.flowOf
6-
73
open class NoOpProvider(override val hooks: List<Hook<*>> = listOf()) : FeatureProvider {
84
override val metadata: ProviderMetadata = NoOpProviderMetadata("No-op provider")
9-
override fun initialize(initialContext: EvaluationContext?) {
5+
override suspend fun initialize(initialContext: EvaluationContext?) {
106
// no-op
117
}
128

139
override fun shutdown() {
1410
// no-op
1511
}
1612

17-
override fun onContextSet(
13+
override suspend fun onContextSet(
1814
oldContext: EvaluationContext?,
1915
newContext: EvaluationContext
2016
) {
@@ -61,9 +57,5 @@ open class NoOpProvider(override val hooks: List<Hook<*>> = listOf()) : FeatureP
6157
return ProviderEvaluation(defaultValue, "Passed in default", Reason.DEFAULT.toString())
6258
}
6359

64-
override fun observe(): Flow<OpenFeatureEvents> = flowOf()
65-
66-
override fun getProviderStatus(): OpenFeatureEvents = OpenFeatureEvents.ProviderReady
67-
6860
data class NoOpProviderMetadata(override val name: String?) : ProviderMetadata
6961
}
Lines changed: 133 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,192 @@
11
package dev.openfeature.sdk
22

3-
import dev.openfeature.sdk.events.OpenFeatureEvents
4-
import dev.openfeature.sdk.events.awaitReadyOrError
5-
import dev.openfeature.sdk.events.observe
3+
import dev.openfeature.sdk.exceptions.OpenFeatureError
64
import kotlinx.coroutines.CoroutineDispatcher
7-
import kotlinx.coroutines.ExperimentalCoroutinesApi
8-
import kotlinx.coroutines.flow.Flow
5+
import kotlinx.coroutines.CoroutineScope
6+
import kotlinx.coroutines.Deferred
7+
import kotlinx.coroutines.Dispatchers
8+
import kotlinx.coroutines.async
99
import kotlinx.coroutines.flow.MutableSharedFlow
1010
import kotlinx.coroutines.flow.SharedFlow
11-
import kotlinx.coroutines.flow.flatMapLatest
11+
import java.util.concurrent.CancellationException
1212

1313
@Suppress("TooManyFunctions")
1414
object OpenFeatureAPI {
15+
private var setProviderJob: Deferred<Unit>? = null
1516
private val NOOP_PROVIDER = NoOpProvider()
1617
private var provider: FeatureProvider = NOOP_PROVIDER
1718
private var context: EvaluationContext? = null
18-
private val providersFlow: MutableSharedFlow<FeatureProvider> = MutableSharedFlow(replay = 1)
19-
internal val sharedProvidersFlow: SharedFlow<FeatureProvider> get() = providersFlow
19+
20+
private val _statusFlow: MutableSharedFlow<OpenFeatureStatus> =
21+
MutableSharedFlow<OpenFeatureStatus>(replay = 1, extraBufferCapacity = 5)
22+
.apply {
23+
tryEmit(OpenFeatureStatus.NotReady)
24+
}
25+
26+
/**
27+
* A flow of [OpenFeatureStatus] that emits the current status of the SDK.
28+
*/
29+
val statusFlow: SharedFlow<OpenFeatureStatus> get() = _statusFlow
2030

2131
var hooks: List<Hook<*>> = listOf()
2232
private set
2333

24-
fun setProvider(provider: FeatureProvider, initialContext: EvaluationContext? = null) {
25-
this@OpenFeatureAPI.provider = provider
26-
providersFlow.tryEmit(provider)
27-
if (initialContext != null) context = initialContext
28-
try {
29-
provider.initialize(context)
30-
} catch (e: Throwable) {
31-
// This is not allowed to happen
34+
/**
35+
* Set the [FeatureProvider] for the SDK. This method will return immediately and initialize the provider in a coroutine scope
36+
* When the provider is successfully initialized it will set the status to Ready.
37+
* If the provider fails to initialize it will set the status to Error.
38+
*
39+
* This method requires you to manually wait for the status to be Ready before using the SDK for flag evaluations.
40+
* This can be done by using the [statusFlow] and waiting for the first Ready status or by accessing [getStatus]
41+
*
42+
* @param provider the provider to set
43+
* @param dispatcher the dispatcher to use for the provider initialization coroutine. Defaults to [Dispatchers.IO] if not set.
44+
* @param initialContext the initial [EvaluationContext] to use for the provider initialization. Defaults to an null context if not set.
45+
*/
46+
fun setProvider(
47+
provider: FeatureProvider,
48+
dispatcher: CoroutineDispatcher = Dispatchers.IO,
49+
initialContext: EvaluationContext? = null
50+
) {
51+
setProviderJob?.cancel()
52+
this.setProviderJob = CoroutineScope(dispatcher).async {
53+
setProviderInternal(provider, initialContext)
3254
}
3355
}
3456

57+
/**
58+
* Set the [FeatureProvider] for the SDK. This method will block until the provider is initialized.
59+
*
60+
* @param provider the [FeatureProvider] to set
61+
* @param initialContext the initial [EvaluationContext] to use for the provider initialization. Defaults to an null context if not set.
62+
*/
3563
suspend fun setProviderAndWait(
3664
provider: FeatureProvider,
37-
dispatcher: CoroutineDispatcher,
3865
initialContext: EvaluationContext? = null
3966
) {
40-
setProvider(provider, initialContext)
41-
provider.awaitReadyOrError(dispatcher)
67+
setProviderInternal(provider, initialContext)
68+
}
69+
70+
private suspend fun setProviderInternal(
71+
provider: FeatureProvider,
72+
initialContext: EvaluationContext? = null
73+
) {
74+
this@OpenFeatureAPI.provider = provider
75+
// TODO consider if stale status should emit? _statusFlow.tryEmit(OpenFeatureStatus.Stale)
76+
if (initialContext != null) context = initialContext
77+
try {
78+
getProvider().initialize(context)
79+
_statusFlow.tryEmit(OpenFeatureStatus.Ready)
80+
} catch (e: OpenFeatureError) {
81+
_statusFlow.tryEmit(OpenFeatureStatus.Error(e))
82+
} catch (e: Throwable) {
83+
_statusFlow.tryEmit(
84+
OpenFeatureStatus.Error(
85+
OpenFeatureError.GeneralError(
86+
e.message ?: e.javaClass.name
87+
)
88+
)
89+
)
90+
// TODO deal with things by setting status to Error or Fatal
91+
}
4292
}
4393

94+
/**
95+
* Get the current [FeatureProvider] for the SDK.
96+
*/
4497
fun getProvider(): FeatureProvider {
4598
return provider
4699
}
47100

101+
/**
102+
* Clear the current [FeatureProvider] for the SDK and set it to a no-op provider.
103+
*/
48104
fun clearProvider() {
49105
provider = NOOP_PROVIDER
50106
}
51107

52-
fun setEvaluationContext(evaluationContext: EvaluationContext) {
108+
/**
109+
* Set the [EvaluationContext] for the SDK.
110+
* If the new context is different compare to the old context, this will cause the provider to reconcile with the new context.
111+
* When the provider "Reconciles" it will set the status to [OpenFeatureStatus.Reconciling].
112+
* When the provider successfully reconciles it will set the status to [OpenFeatureStatus.Ready].
113+
* If the provider fails to reconcile it will set the status to [OpenFeatureStatus.Error].
114+
*
115+
* @param evaluationContext the [EvaluationContext] to set
116+
*/
117+
suspend fun setEvaluationContext(evaluationContext: EvaluationContext) {
53118
val oldContext = context
54119
context = evaluationContext
55-
getProvider().onContextSet(oldContext, evaluationContext)
120+
if (oldContext != evaluationContext) {
121+
_statusFlow.tryEmit(OpenFeatureStatus.Reconciling)
122+
try {
123+
getProvider().onContextSet(oldContext, evaluationContext)
124+
_statusFlow.tryEmit(OpenFeatureStatus.Ready)
125+
} catch (e: OpenFeatureError) {
126+
_statusFlow.tryEmit(OpenFeatureStatus.Error(e))
127+
// TODO how do we handle fatal errors?
128+
} catch (e: Throwable) {
129+
_statusFlow.tryEmit(
130+
OpenFeatureStatus.Error(
131+
OpenFeatureError.GeneralError(
132+
e.message ?: e.javaClass.name
133+
)
134+
)
135+
)
136+
}
137+
}
56138
}
57139

140+
/**
141+
* Get the current [EvaluationContext] for the SDK.
142+
*/
58143
fun getEvaluationContext(): EvaluationContext? {
59144
return context
60145
}
61146

147+
/**
148+
* Get the [ProviderMetadata] for the current [FeatureProvider].
149+
*/
62150
fun getProviderMetadata(): ProviderMetadata? {
63-
return provider.metadata
151+
return getProvider().metadata
64152
}
65153

154+
/**
155+
* Get a [Client] for the SDK.
156+
* This client can be used to evaluate flags.
157+
*/
66158
fun getClient(name: String? = null, version: String? = null): Client {
67159
return OpenFeatureClient(this, name, version)
68160
}
69161

162+
/**
163+
* Add [Hook]s to the SDK.
164+
*/
70165
fun addHooks(hooks: List<Hook<*>>) {
71166
this.hooks += hooks
72167
}
73168

169+
/**
170+
* Clear all [Hook]s from the SDK.
171+
*/
74172
fun clearHooks() {
75173
this.hooks = listOf()
76174
}
77175

176+
/**
177+
* Shutdown the SDK.
178+
* This will cancel the provider set job and call the provider's shutdown method.
179+
* The SDK status will be set to [OpenFeatureStatus.NotReady].
180+
*/
78181
fun shutdown() {
79-
provider.shutdown()
182+
setProviderJob?.cancel(CancellationException("Provider set job was cancelled"))
183+
_statusFlow.tryEmit(OpenFeatureStatus.NotReady)
184+
getProvider().shutdown()
185+
clearHooks()
80186
}
81187

82-
/*
83-
Observe events from currently configured Provider.
84-
*/
85-
@OptIn(ExperimentalCoroutinesApi::class)
86-
internal inline fun <reified T : OpenFeatureEvents> observe(): Flow<T> {
87-
return sharedProvidersFlow.flatMapLatest { provider ->
88-
provider.observe<T>()
89-
}
90-
}
188+
/**
189+
* Get the current [OpenFeatureStatus] of the SDK.
190+
*/
191+
fun getStatus(): OpenFeatureStatus = statusFlow.replayCache.first()
91192
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ class OpenFeatureClient(
188188
)
189189
try {
190190
hookSupport.beforeHooks(flagValueType, hookCtx, mergedHooks, hints)
191+
shortCircuitIfNotReady()
191192
val providerEval = createProviderEvaluation(
192193
flagValueType,
193194
key,
@@ -216,6 +217,15 @@ class OpenFeatureClient(
216217
return details
217218
}
218219

220+
private fun shortCircuitIfNotReady() {
221+
val providerStatus = openFeatureAPI.getStatus()
222+
if (providerStatus == OpenFeatureStatus.NotReady) {
223+
throw OpenFeatureError.ProviderNotReadyError()
224+
} else if (providerStatus == OpenFeatureStatus.Fatal) {
225+
throw OpenFeatureError.ProviderFatalError()
226+
}
227+
}
228+
219229
@Suppress("UNCHECKED_CAST")
220230
private fun <V> createProviderEvaluation(
221231
flagValueType: FlagValueType,
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package dev.openfeature.sdk
2+
3+
import dev.openfeature.sdk.exceptions.OpenFeatureError
4+
5+
sealed interface OpenFeatureStatus {
6+
/**
7+
* The provider has not been initialized and cannot yet evaluate flags.
8+
*/
9+
object NotReady : OpenFeatureStatus
10+
11+
/**
12+
* The provider is ready to resolve flags.
13+
*/
14+
object Ready : OpenFeatureStatus
15+
16+
/**
17+
* The provider is in an error state and unable to evaluate flags.
18+
*/
19+
class Error(val error: OpenFeatureError) : OpenFeatureStatus
20+
21+
/**
22+
* The provider's cached state is no longer valid and may not be up-to-date with the source of truth.
23+
*/
24+
object Stale : OpenFeatureStatus
25+
26+
/**
27+
* The provider has entered an irrecoverable error state.
28+
*/
29+
object Fatal : OpenFeatureStatus
30+
31+
/**
32+
* The provider is reconciling its state with a context change.
33+
*/
34+
object Reconciling : OpenFeatureStatus
35+
}

0 commit comments

Comments
 (0)