Skip to content

Commit 06b6f6b

Browse files
authored
fix: bring back provider eventing (#124)
Signed-off-by: Nicklas Lundin <[email protected]>
1 parent 0921091 commit 06b6f6b

19 files changed

+560
-60
lines changed

README.md

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ coroutineScope.launch(Dispatchers.IO) {
7575
|| [Tracking](#tracking) | Associate user actions with feature flag evaluations. |
7676
|| [Logging](#logging) | Integrate with popular logging packages. |
7777
|| [Named clients](#named-clients) | Utilize multiple providers in a single application. |
78-
| | [Eventing](#eventing) | React to state changes in the provider or flag management system. |
78+
| | [Eventing](#eventing) | React to state changes in the provider or flag management system. |
7979
|| [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. |
8080
| ⚠️ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. |
8181

@@ -171,13 +171,37 @@ Tracking is optionally implemented by Providers.
171171

172172
Logging customization is not yet available in the Kotlin SDK.
173173

174+
It is possible to write and inject logging `Hook`s to log events at different stages of the flag evaluation life-cycle.
175+
174176
### Named clients
175177

176178
Support for named clients is not yet available in the Kotlin SDK.
177179

178180
### Eventing
179181

180-
Support for eventing is not yet available in the Kotlin SDK.
182+
Events from the Provider allow the SDK to react to state changes in the provider or underlying flag management system, such as flag definition changes, provider readiness, or error conditions.
183+
Events are optional which mean that not all Providers will emit them and it is not a must have. Some providers support additional events, such as `PROVIDER_CONFIGURATION_CHANGED`.
184+
185+
Please refer to the documentation of the provider you're using to see what events are supported.
186+
187+
Example usage:
188+
```kotlin
189+
viewModelScope.launch {
190+
OpenFeatureAPI.observe().collect {
191+
println(">> Provider event received")
192+
}
193+
}
194+
195+
viewModelScope.launch {
196+
OpenFeatureAPI.setProviderAndWait(
197+
MyFeatureProvider(),
198+
Dispatchers.IO,
199+
myEvaluationContext
200+
)
201+
}
202+
```
203+
204+
<!-- (It's only possible to observe events from the global `OpenFeatureAPI`, until multiple providers are supported) -->
181205

182206
### Shutdown
183207

@@ -244,6 +268,18 @@ class NewProvider(override val hooks: List<Hook<*>>, override val metadata: Meta
244268
override suspend fun onContextSet(oldContext: EvaluationContext?, newContext: EvaluationContext) {
245269
// add necessary changes on context change
246270
}
271+
272+
override fun track(
273+
trackingEventName: String,
274+
context: EvaluationContext?,
275+
details: TrackingEventDetails?
276+
) {
277+
// Optionally track an event
278+
}
279+
280+
override fun observe(): Flow<OpenFeatureProviderEvents> {
281+
// Optionally return a `Flow` of OpenFeatureProviderEvents
282+
}
247283
}
248284
```
249285

android/api/android.api

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,14 @@ public abstract interface class dev/openfeature/sdk/FeatureProvider {
6363
public abstract fun getObjectEvaluation (Ljava/lang/String;Ldev/openfeature/sdk/Value;Ldev/openfeature/sdk/EvaluationContext;)Ldev/openfeature/sdk/ProviderEvaluation;
6464
public abstract fun getStringEvaluation (Ljava/lang/String;Ljava/lang/String;Ldev/openfeature/sdk/EvaluationContext;)Ldev/openfeature/sdk/ProviderEvaluation;
6565
public abstract fun initialize (Ldev/openfeature/sdk/EvaluationContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
66+
public abstract fun observe ()Lkotlinx/coroutines/flow/Flow;
6667
public abstract fun onContextSet (Ldev/openfeature/sdk/EvaluationContext;Ldev/openfeature/sdk/EvaluationContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
6768
public abstract fun shutdown ()V
6869
public abstract fun track (Ljava/lang/String;Ldev/openfeature/sdk/EvaluationContext;Ldev/openfeature/sdk/TrackingEventDetails;)V
6970
}
7071

7172
public final class dev/openfeature/sdk/FeatureProvider$DefaultImpls {
73+
public static fun observe (Ldev/openfeature/sdk/FeatureProvider;)Lkotlinx/coroutines/flow/Flow;
7274
public static fun track (Ldev/openfeature/sdk/FeatureProvider;Ljava/lang/String;Ldev/openfeature/sdk/EvaluationContext;Ldev/openfeature/sdk/TrackingEventDetails;)V
7375
}
7476

@@ -237,6 +239,7 @@ public class dev/openfeature/sdk/NoOpProvider : dev/openfeature/sdk/FeatureProvi
237239
public fun getObjectEvaluation (Ljava/lang/String;Ldev/openfeature/sdk/Value;Ldev/openfeature/sdk/EvaluationContext;)Ldev/openfeature/sdk/ProviderEvaluation;
238240
public fun getStringEvaluation (Ljava/lang/String;Ljava/lang/String;Ldev/openfeature/sdk/EvaluationContext;)Ldev/openfeature/sdk/ProviderEvaluation;
239241
public fun initialize (Ldev/openfeature/sdk/EvaluationContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
242+
public fun observe ()Lkotlinx/coroutines/flow/Flow;
240243
public fun onContextSet (Ldev/openfeature/sdk/EvaluationContext;Ldev/openfeature/sdk/EvaluationContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
241244
public fun shutdown ()V
242245
public fun track (Ljava/lang/String;Ldev/openfeature/sdk/EvaluationContext;Ldev/openfeature/sdk/TrackingEventDetails;)V
@@ -257,23 +260,24 @@ public final class dev/openfeature/sdk/OpenFeatureAPI {
257260
public static final field INSTANCE Ldev/openfeature/sdk/OpenFeatureAPI;
258261
public final fun addHooks (Ljava/util/List;)V
259262
public final fun clearHooks ()V
260-
public final fun clearProvider ()V
263+
public final fun clearProvider (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
261264
public final fun getClient (Ljava/lang/String;Ljava/lang/String;)Ldev/openfeature/sdk/Client;
262265
public static synthetic fun getClient$default (Ldev/openfeature/sdk/OpenFeatureAPI;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Ldev/openfeature/sdk/Client;
263266
public final fun getEvaluationContext ()Ldev/openfeature/sdk/EvaluationContext;
264267
public final fun getHooks ()Ljava/util/List;
265268
public final fun getProvider ()Ldev/openfeature/sdk/FeatureProvider;
266269
public final fun getProviderMetadata ()Ldev/openfeature/sdk/ProviderMetadata;
270+
public final fun getProvidersFlow ()Lkotlinx/coroutines/flow/MutableStateFlow;
267271
public final fun getStatus ()Ldev/openfeature/sdk/OpenFeatureStatus;
268272
public final fun getStatusFlow ()Lkotlinx/coroutines/flow/Flow;
269273
public final fun setEvaluationContext (Ldev/openfeature/sdk/EvaluationContext;Lkotlinx/coroutines/CoroutineDispatcher;)V
270274
public static synthetic fun setEvaluationContext$default (Ldev/openfeature/sdk/OpenFeatureAPI;Ldev/openfeature/sdk/EvaluationContext;Lkotlinx/coroutines/CoroutineDispatcher;ILjava/lang/Object;)V
271275
public final fun setEvaluationContextAndWait (Ldev/openfeature/sdk/EvaluationContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
272276
public final fun setProvider (Ldev/openfeature/sdk/FeatureProvider;Lkotlinx/coroutines/CoroutineDispatcher;Ldev/openfeature/sdk/EvaluationContext;)V
273277
public static synthetic fun setProvider$default (Ldev/openfeature/sdk/OpenFeatureAPI;Ldev/openfeature/sdk/FeatureProvider;Lkotlinx/coroutines/CoroutineDispatcher;Ldev/openfeature/sdk/EvaluationContext;ILjava/lang/Object;)V
274-
public final fun setProviderAndWait (Ldev/openfeature/sdk/FeatureProvider;Ldev/openfeature/sdk/EvaluationContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
275-
public static synthetic fun setProviderAndWait$default (Ldev/openfeature/sdk/OpenFeatureAPI;Ldev/openfeature/sdk/FeatureProvider;Ldev/openfeature/sdk/EvaluationContext;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
276-
public final fun shutdown ()V
278+
public final fun setProviderAndWait (Ldev/openfeature/sdk/FeatureProvider;Ldev/openfeature/sdk/EvaluationContext;Lkotlinx/coroutines/CoroutineDispatcher;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
279+
public static synthetic fun setProviderAndWait$default (Ldev/openfeature/sdk/OpenFeatureAPI;Ldev/openfeature/sdk/FeatureProvider;Ldev/openfeature/sdk/EvaluationContext;Lkotlinx/coroutines/CoroutineDispatcher;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
280+
public final fun shutdown (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
277281
}
278282

279283
public final class dev/openfeature/sdk/OpenFeatureClient : dev/openfeature/sdk/Client {
@@ -325,7 +329,8 @@ public final class dev/openfeature/sdk/OpenFeatureStatus$Error : dev/openfeature
325329
}
326330

327331
public final class dev/openfeature/sdk/OpenFeatureStatus$Fatal : dev/openfeature/sdk/OpenFeatureStatus {
328-
public static final field INSTANCE Ldev/openfeature/sdk/OpenFeatureStatus$Fatal;
332+
public fun <init> (Ldev/openfeature/sdk/exceptions/OpenFeatureError;)V
333+
public final fun getError ()Ldev/openfeature/sdk/exceptions/OpenFeatureError;
329334
}
330335

331336
public final class dev/openfeature/sdk/OpenFeatureStatus$NotReady : dev/openfeature/sdk/OpenFeatureStatus {
@@ -589,6 +594,36 @@ public final class dev/openfeature/sdk/Value$Structure : dev/openfeature/sdk/Val
589594
public fun toString ()Ljava/lang/String;
590595
}
591596

597+
public abstract interface class dev/openfeature/sdk/events/OpenFeatureProviderEvents {
598+
}
599+
600+
public final class dev/openfeature/sdk/events/OpenFeatureProviderEvents$ProviderConfigurationChanged : dev/openfeature/sdk/events/OpenFeatureProviderEvents {
601+
public static final field INSTANCE Ldev/openfeature/sdk/events/OpenFeatureProviderEvents$ProviderConfigurationChanged;
602+
}
603+
604+
public final class dev/openfeature/sdk/events/OpenFeatureProviderEvents$ProviderError : dev/openfeature/sdk/events/OpenFeatureProviderEvents {
605+
public fun <init> (Ldev/openfeature/sdk/exceptions/OpenFeatureError;)V
606+
public final fun component1 ()Ldev/openfeature/sdk/exceptions/OpenFeatureError;
607+
public final fun copy (Ldev/openfeature/sdk/exceptions/OpenFeatureError;)Ldev/openfeature/sdk/events/OpenFeatureProviderEvents$ProviderError;
608+
public static synthetic fun copy$default (Ldev/openfeature/sdk/events/OpenFeatureProviderEvents$ProviderError;Ldev/openfeature/sdk/exceptions/OpenFeatureError;ILjava/lang/Object;)Ldev/openfeature/sdk/events/OpenFeatureProviderEvents$ProviderError;
609+
public fun equals (Ljava/lang/Object;)Z
610+
public final fun getError ()Ldev/openfeature/sdk/exceptions/OpenFeatureError;
611+
public fun hashCode ()I
612+
public fun toString ()Ljava/lang/String;
613+
}
614+
615+
public final class dev/openfeature/sdk/events/OpenFeatureProviderEvents$ProviderNotReady : dev/openfeature/sdk/events/OpenFeatureProviderEvents {
616+
public static final field INSTANCE Ldev/openfeature/sdk/events/OpenFeatureProviderEvents$ProviderNotReady;
617+
}
618+
619+
public final class dev/openfeature/sdk/events/OpenFeatureProviderEvents$ProviderReady : dev/openfeature/sdk/events/OpenFeatureProviderEvents {
620+
public static final field INSTANCE Ldev/openfeature/sdk/events/OpenFeatureProviderEvents$ProviderReady;
621+
}
622+
623+
public final class dev/openfeature/sdk/events/OpenFeatureProviderEvents$ProviderStale : dev/openfeature/sdk/events/OpenFeatureProviderEvents {
624+
public static final field INSTANCE Ldev/openfeature/sdk/events/OpenFeatureProviderEvents$ProviderStale;
625+
}
626+
592627
public final class dev/openfeature/sdk/exceptions/ErrorCode : java/lang/Enum {
593628
public static final field FLAG_NOT_FOUND Ldev/openfeature/sdk/exceptions/ErrorCode;
594629
public static final field GENERAL Ldev/openfeature/sdk/exceptions/ErrorCode;

android/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ dependencies {
101101
testImplementation("junit:junit:4.13.2")
102102
testImplementation("org.mockito.kotlin:mockito-kotlin:5.4.0")
103103
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
104+
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-debug:1.7.3")
104105
}
105106

106107
signing {

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

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

3+
import dev.openfeature.sdk.events.OpenFeatureProviderEvents
34
import dev.openfeature.sdk.exceptions.OpenFeatureError
5+
import kotlinx.coroutines.flow.Flow
6+
import kotlinx.coroutines.flow.emptyFlow
47
import kotlin.jvm.Throws
58

69
interface FeatureProvider {
@@ -53,4 +56,12 @@ interface FeatureProvider {
5356
fun track(trackingEventName: String, context: EvaluationContext?, details: TrackingEventDetails?) {
5457
// an empty default implementation to make implementing this functionality optional
5558
}
59+
60+
/**
61+
* Used by providers to expose internal events to the SDK or the application.
62+
* This can be optionally implemented by the provider to expose a flow of internal events.
63+
*/
64+
fun observe(): Flow<OpenFeatureProviderEvents> {
65+
return emptyFlow()
66+
}
5667
}

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

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

3+
import dev.openfeature.sdk.events.OpenFeatureProviderEvents
34
import dev.openfeature.sdk.exceptions.OpenFeatureError
45
import kotlinx.coroutines.CoroutineDispatcher
56
import kotlinx.coroutines.CoroutineScope
6-
import kotlinx.coroutines.Deferred
77
import kotlinx.coroutines.Dispatchers
8-
import kotlinx.coroutines.async
8+
import kotlinx.coroutines.ExperimentalCoroutinesApi
9+
import kotlinx.coroutines.Job
10+
import kotlinx.coroutines.SupervisorJob
11+
import kotlinx.coroutines.cancel
12+
import kotlinx.coroutines.cancelChildren
913
import kotlinx.coroutines.flow.Flow
14+
import kotlinx.coroutines.flow.FlowCollector
1015
import kotlinx.coroutines.flow.MutableSharedFlow
16+
import kotlinx.coroutines.flow.MutableStateFlow
1117
import kotlinx.coroutines.flow.distinctUntilChanged
18+
import kotlinx.coroutines.flow.filterIsInstance
19+
import kotlinx.coroutines.flow.flatMapLatest
20+
import kotlinx.coroutines.launch
1221
import java.util.concurrent.CancellationException
1322

1423
@Suppress("TooManyFunctions")
1524
object OpenFeatureAPI {
16-
private var setProviderJob: Deferred<Unit>? = null
17-
private var setEvaluationContextJob: Deferred<Unit>? = null
25+
private var setProviderJob: Job? = null
26+
private var setEvaluationContextJob: Job? = null
27+
private var observeProviderEventsJob: Job? = null
28+
private var providerEventObservationScope: CoroutineScope? = null
29+
1830
private val NOOP_PROVIDER = NoOpProvider()
1931
private var provider: FeatureProvider = NOOP_PROVIDER
2032
private var context: EvaluationContext? = null
33+
val providersFlow: MutableStateFlow<FeatureProvider> = MutableStateFlow(NOOP_PROVIDER)
2134

2235
private val _statusFlow: MutableSharedFlow<OpenFeatureStatus> =
2336
MutableSharedFlow<OpenFeatureStatus>(replay = 1, extraBufferCapacity = 5)
@@ -29,6 +42,7 @@ object OpenFeatureAPI {
2942
* A flow of [OpenFeatureStatus] that emits the current status of the SDK.
3043
*/
3144
val statusFlow: Flow<OpenFeatureStatus> get() = _statusFlow.distinctUntilChanged()
45+
private var providerJob: Job? = null
3246

3347
var hooks: List<Hook<*>> = listOf()
3448
private set
@@ -51,8 +65,8 @@ object OpenFeatureAPI {
5165
initialContext: EvaluationContext? = null
5266
) {
5367
setProviderJob?.cancel()
54-
this.setProviderJob = CoroutineScope(dispatcher).async {
55-
setProviderInternal(provider, initialContext)
68+
this.setProviderJob = CoroutineScope(SupervisorJob() + dispatcher).launch {
69+
setProviderInternal(provider, dispatcher, initialContext)
5670
}
5771
}
5872

@@ -64,19 +78,35 @@ object OpenFeatureAPI {
6478
*/
6579
suspend fun setProviderAndWait(
6680
provider: FeatureProvider,
67-
initialContext: EvaluationContext? = null
81+
initialContext: EvaluationContext? = null,
82+
dispatcher: CoroutineDispatcher = Dispatchers.IO
6883
) {
69-
setProviderInternal(provider, initialContext)
84+
setProviderInternal(provider, dispatcher, initialContext)
85+
}
86+
87+
private fun listenToProviderEvents(provider: FeatureProvider, dispatcher: CoroutineDispatcher) {
88+
providerJob?.cancel()
89+
this.providerJob = CoroutineScope(SupervisorJob() + dispatcher).launch {
90+
provider.observe().collect(handleProviderEvents)
91+
}
7092
}
7193

94+
@OptIn(ExperimentalCoroutinesApi::class)
7295
private suspend fun setProviderInternal(
7396
provider: FeatureProvider,
97+
dispatcher: CoroutineDispatcher,
7498
initialContext: EvaluationContext? = null
7599
) {
76-
this@OpenFeatureAPI.provider = provider
77-
_statusFlow.emit(OpenFeatureStatus.NotReady)
100+
// TODO should we send shutdown to previous provider:
101+
// getProvider().shutdown()
102+
103+
this@OpenFeatureAPI.provider = provider.also {
104+
_statusFlow.emit(OpenFeatureStatus.NotReady)
105+
}
106+
providersFlow.value = provider
78107
if (initialContext != null) context = initialContext
79108
try {
109+
listenToProviderEvents(provider, dispatcher)
80110
getProvider().initialize(context)
81111
_statusFlow.emit(OpenFeatureStatus.Ready)
82112
} catch (e: OpenFeatureError) {
@@ -102,9 +132,11 @@ object OpenFeatureAPI {
102132
/**
103133
* Clear the current [FeatureProvider] for the SDK and set it to a no-op provider.
104134
*/
105-
fun clearProvider() {
135+
suspend fun clearProvider() {
136+
getProvider().shutdown()
106137
provider = NOOP_PROVIDER
107-
_statusFlow.tryEmit(OpenFeatureStatus.NotReady)
138+
providersFlow.value = NOOP_PROVIDER
139+
_statusFlow.emit(OpenFeatureStatus.NotReady)
108140
}
109141

110142
/**
@@ -140,7 +172,7 @@ object OpenFeatureAPI {
140172
dispatcher: CoroutineDispatcher = Dispatchers.IO
141173
) {
142174
setEvaluationContextJob?.cancel()
143-
this.setEvaluationContextJob = CoroutineScope(dispatcher).async {
175+
this.setEvaluationContextJob = CoroutineScope(SupervisorJob() + dispatcher).launch {
144176
setEvaluationContextInternal(evaluationContext)
145177
}
146178
}
@@ -208,17 +240,54 @@ object OpenFeatureAPI {
208240
* This will cancel the provider set job and call the provider's shutdown method.
209241
* The SDK status will be set to [OpenFeatureStatus.NotReady].
210242
*/
211-
fun shutdown() {
243+
suspend fun shutdown() {
244+
clearHooks()
212245
setEvaluationContextJob?.cancel(CancellationException("Set context job was cancelled"))
213246
setProviderJob?.cancel(CancellationException("Provider set job was cancelled"))
214-
provider = NOOP_PROVIDER
215-
_statusFlow.tryEmit(OpenFeatureStatus.NotReady)
216-
getProvider().shutdown()
217-
clearHooks()
247+
observeProviderEventsJob?.cancel(CancellationException("Provider event observe job was cancelled"))
248+
providerEventObservationScope?.coroutineContext?.cancelChildren()
249+
providerEventObservationScope?.coroutineContext?.cancel()
250+
clearProvider()
218251
}
219252

220253
/**
221254
* Get the current [OpenFeatureStatus] of the SDK.
222255
*/
223256
fun getStatus(): OpenFeatureStatus = _statusFlow.replayCache.first()
257+
258+
/**
259+
* Observe events from currently configured Provider.
260+
*/
261+
@OptIn(ExperimentalCoroutinesApi::class)
262+
inline fun <reified T : OpenFeatureProviderEvents> observe(): Flow<T> = providersFlow
263+
.flatMapLatest { it.observe() }.filterIsInstance()
264+
265+
private val handleProviderEvents: FlowCollector<OpenFeatureProviderEvents> = FlowCollector {
266+
when (it) {
267+
OpenFeatureProviderEvents.ProviderReady -> {
268+
_statusFlow.emit(OpenFeatureStatus.Ready)
269+
}
270+
271+
is OpenFeatureProviderEvents.ProviderError -> {
272+
val status = if (it.error is OpenFeatureError.ProviderFatalError) {
273+
OpenFeatureStatus.Fatal(it.error)
274+
} else {
275+
OpenFeatureStatus.Error(it.error)
276+
}
277+
_statusFlow.emit(status)
278+
}
279+
280+
OpenFeatureProviderEvents.ProviderNotReady -> {
281+
_statusFlow.emit(OpenFeatureStatus.NotReady)
282+
}
283+
284+
OpenFeatureProviderEvents.ProviderStale -> {
285+
_statusFlow.emit(OpenFeatureStatus.Stale)
286+
}
287+
288+
OpenFeatureProviderEvents.ProviderConfigurationChanged -> {
289+
_statusFlow.emit(OpenFeatureStatus.Ready)
290+
}
291+
}
292+
}
224293
}

0 commit comments

Comments
 (0)