Skip to content

Commit 4fa33eb

Browse files
committed
Align to Event spec
Signed-off-by: penguindan <[email protected]>
1 parent 48033c5 commit 4fa33eb

File tree

4 files changed

+366
-127
lines changed

4 files changed

+366
-127
lines changed

kotlin-sdk/src/commonMain/kotlin/dev/openfeature/kotlin/sdk/multiprovider/MultiProvider.kt

Lines changed: 94 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package dev.openfeature.kotlin.sdk.multiprovider
33
import dev.openfeature.kotlin.sdk.EvaluationContext
44
import dev.openfeature.kotlin.sdk.FeatureProvider
55
import dev.openfeature.kotlin.sdk.Hook
6+
import dev.openfeature.kotlin.sdk.OpenFeatureStatus
67
import dev.openfeature.kotlin.sdk.ProviderEvaluation
78
import dev.openfeature.kotlin.sdk.ProviderMetadata
89
import dev.openfeature.kotlin.sdk.Value
@@ -13,9 +14,12 @@ import kotlinx.coroutines.awaitAll
1314
import kotlinx.coroutines.coroutineScope
1415
import kotlinx.coroutines.flow.Flow
1516
import kotlinx.coroutines.flow.MutableSharedFlow
17+
import kotlinx.coroutines.flow.MutableStateFlow
1618
import kotlinx.coroutines.flow.asSharedFlow
19+
import kotlinx.coroutines.flow.asStateFlow
1720
import kotlinx.coroutines.flow.launchIn
1821
import kotlinx.coroutines.flow.onEach
22+
import kotlinx.coroutines.flow.update
1923

2024
/**
2125
* MultiProvider is a FeatureProvider implementation that delegates flag evaluations
@@ -39,28 +43,25 @@ class MultiProvider(
3943
cause: Throwable
4044
) : RuntimeException("Provider '$providerName' shutdown failed: ${cause.message}", cause)
4145

46+
/**
47+
* @property name The unique name of the [FeatureProvider] according to this MultiProvider
48+
*/
49+
class ChildFeatureProvider(
50+
implementation: FeatureProvider,
51+
val name: String, // Maybe there's a better variable name for this?
52+
): FeatureProvider by implementation
53+
4254
// TODO: Support hooks
4355
override val hooks: List<Hook<*>> = emptyList()
44-
private val uniqueProviders = getUniqueSetOfProviders(providers)
56+
private val childFeatureProviders: List<ChildFeatureProvider> = providers.toChildFeatureProviders()
4557

4658
// Metadata identifying this as a multiprovider
4759
override val metadata: ProviderMetadata = object : ProviderMetadata {
4860
override val name: String? = MULTIPROVIDER_NAME
4961
override val originalMetadata: Map<String, ProviderMetadata> = constructOriginalMetadata()
5062

5163
private fun constructOriginalMetadata(): Map<String, ProviderMetadata> {
52-
var unprovidedNameCounter = 1
53-
val originalMetadata = mutableMapOf<String, ProviderMetadata>()
54-
uniqueProviders.forEach {
55-
val providerName = it.metadata.name
56-
if (providerName != null) {
57-
originalMetadata[providerName] = it.metadata
58-
} else {
59-
originalMetadata["${it.metadata.getSafeName()}_$unprovidedNameCounter"] = it.metadata
60-
}
61-
}
62-
63-
return originalMetadata
64+
return childFeatureProviders.associate { it.name to it.metadata }
6465
}
6566

6667
override fun toString(): String {
@@ -71,44 +72,47 @@ class MultiProvider(
7172
}
7273
}
7374

75+
private val _statusFlow = MutableStateFlow<OpenFeatureStatus>(OpenFeatureStatus.NotReady)
76+
val statusFlow = _statusFlow.asStateFlow()
77+
7478
// Shared flow because we don't want the distinct operator since it would break consecutive emits of
7579
// ProviderConfigurationChanged
76-
private val eventFlow = MutableSharedFlow<OpenFeatureProviderEvents>(
77-
replay = 1,
78-
extraBufferCapacity = 5
79-
)
80-
81-
// Track individual provider statuses
82-
private val providerStatuses = mutableMapOf<FeatureProvider, OpenFeatureProviderEvents>()
83-
84-
// Event precedence (highest to lowest priority) - based on the specifications
85-
private val eventPrecedence = mapOf(
86-
OpenFeatureProviderEvents.ProviderError::class to 4, // FATAL/ERROR
87-
OpenFeatureProviderEvents.ProviderNotReady::class to 3, // NOT READY, Deprecated but still supporting
88-
OpenFeatureProviderEvents.ProviderStale::class to 2, // STALE
89-
OpenFeatureProviderEvents.ProviderReady::class to 1 // READY
90-
// ProviderConfigurationChanged doesn't affect status, so not included
91-
)
92-
93-
private fun getUniqueSetOfProviders(providers: List<FeatureProvider>): List<FeatureProvider> {
94-
val setOfProviderNames = mutableSetOf<String>()
95-
val uniqueProviders = mutableListOf<FeatureProvider>()
96-
providers.forEach { currProvider ->
97-
val providerName = currProvider.metadata.name
98-
if (setOfProviderNames.add(providerName.orEmpty())) {
99-
uniqueProviders.add(currProvider)
80+
private val eventFlow = MutableSharedFlow<OpenFeatureProviderEvents>(replay = 1, extraBufferCapacity = 5)
81+
82+
// Track individual provider statuses, initial state of all providers is NotReady
83+
private val childProviderStatuses: MutableMap<ChildFeatureProvider, OpenFeatureStatus> =
84+
childFeatureProviders.associateWithTo(mutableMapOf()) { OpenFeatureStatus.NotReady }
85+
86+
private fun List<FeatureProvider>.toChildFeatureProviders(): List<ChildFeatureProvider> {
87+
// Extract a stable base name per provider, falling back for unnamed providers
88+
val providerBaseNames: List<String> = this.map { it.metadata.name ?: UNDEFINED_PROVIDER_NAME }
89+
90+
// How many times each base name occurs in the inputs
91+
val baseNameToTotalCount: Map<String, Int> = providerBaseNames.groupingBy { it }.eachCount()
92+
93+
// Running index per base name used to generate suffixed unique names in order
94+
val baseNameToNextIndex = mutableMapOf<String, Int>()
95+
96+
return this.mapIndexed { providerIndex, provider ->
97+
val baseName = providerBaseNames[providerIndex]
98+
val occurrencesForBase = baseNameToTotalCount[baseName] ?: 0
99+
100+
val uniqueChildName = if (occurrencesForBase > 1) {
101+
val nextIndex = (baseNameToNextIndex[baseName] ?: 0) + 1
102+
baseNameToNextIndex[baseName] = nextIndex
103+
"${baseName}_${nextIndex}"
100104
} else {
101-
println("Duplicate provider with name $providerName found") // Log error, no logging tool
105+
baseName
102106
}
103-
}
104107

105-
return uniqueProviders
108+
ChildFeatureProvider(provider, uniqueChildName)
109+
}
106110
}
107111

108112
/**
109113
* @return Number of unique providers
110114
*/
111-
fun getProviderCount(): Int = uniqueProviders.size
115+
fun getProviderCount(): Int = childFeatureProviders.size
112116

113117
override fun observe(): Flow<OpenFeatureProviderEvents> = eventFlow.asSharedFlow()
114118

@@ -122,7 +126,7 @@ class MultiProvider(
122126
coroutineScope {
123127
// Listen to events emitted by providers to emit our own set of events
124128
// according to https://openfeature.dev/specification/appendix-a/#status-and-event-handling
125-
uniqueProviders.forEach { provider ->
129+
childFeatureProviders.forEach { provider ->
126130
provider.observe()
127131
.onEach { event ->
128132
handleProviderEvent(provider, event)
@@ -131,45 +135,56 @@ class MultiProvider(
131135
}
132136

133137
// State updates captured by observing individual Feature Flag providers
134-
uniqueProviders
138+
childFeatureProviders
135139
.map { async { it.initialize(initialContext) } }
136140
.awaitAll()
137141
}
138142
}
139143

140-
private suspend fun handleProviderEvent(provider: FeatureProvider, event: OpenFeatureProviderEvents) {
141-
val hasStatusUpdated = updateProviderStatus(provider, event)
142-
143-
// This event should be re-emitted any time it occurs from any provider.
144+
private suspend fun handleProviderEvent(provider: ChildFeatureProvider, event: OpenFeatureProviderEvents) {
144145
if (event is OpenFeatureProviderEvents.ProviderConfigurationChanged) {
145146
eventFlow.emit(event)
146147
return
147148
}
148149

149-
// If the status has been updated, calculate what our new event should be
150-
if (hasStatusUpdated) {
151-
// Determine the highest-precedence status among all providers
152-
val highestEvent = providerStatuses.values
153-
.filter { it !is OpenFeatureProviderEvents.ProviderConfigurationChanged }
154-
.maxByOrNull { eventPrecedence[it::class] ?: 0 }
150+
val newChildStatus = when (event) {
151+
is OpenFeatureProviderEvents.ProviderReady -> OpenFeatureStatus.Ready
152+
is OpenFeatureProviderEvents.ProviderNotReady -> OpenFeatureStatus.NotReady
153+
is OpenFeatureProviderEvents.ProviderStale -> OpenFeatureStatus.Stale
154+
is OpenFeatureProviderEvents.ProviderError ->
155+
if (event.error is OpenFeatureError.ProviderFatalError) {
156+
OpenFeatureStatus.Fatal(event.error)
157+
} else {
158+
OpenFeatureStatus.Error(event.error)
159+
}
160+
else -> error("Unexpected event $event")
161+
}
155162

156-
// Only emit if there's a change in overall status
157-
val currentOverall = eventFlow.replayCache.lastOrNull()
163+
val previousStatus = _statusFlow.value
164+
childProviderStatuses[provider] = newChildStatus
165+
val newStatus = calculateAggregateStatus()
158166

159-
if (highestEvent != null && highestEvent != currentOverall) {
160-
eventFlow.emit(highestEvent)
161-
}
167+
if (previousStatus != newStatus) {
168+
_statusFlow.update { newStatus }
169+
// Re-emit the original event that triggered the aggregate status change
170+
eventFlow.emit(event)
162171
}
163172
}
164173

165-
/**
166-
* @return true if the status has been updated to a different value, false otherwise
167-
*/
168-
private fun updateProviderStatus(provider: FeatureProvider, newStatus: OpenFeatureProviderEvents): Boolean {
169-
val oldStatus = providerStatuses[provider]
170-
providerStatuses[provider] = newStatus
174+
private fun calculateAggregateStatus(): OpenFeatureStatus {
175+
val highestPrecedenceStatus = childProviderStatuses.values.maxBy(::precedence)
176+
return highestPrecedenceStatus
177+
}
171178

172-
return oldStatus != newStatus
179+
private fun precedence(status: OpenFeatureStatus): Int {
180+
return when (status) {
181+
is OpenFeatureStatus.Fatal -> 5
182+
is OpenFeatureStatus.NotReady -> 4
183+
is OpenFeatureStatus.Error -> 3
184+
is OpenFeatureStatus.Reconciling -> 2 // Not specified in precedence; treat similar to Stale
185+
is OpenFeatureStatus.Stale -> 2
186+
is OpenFeatureStatus.Ready -> 1
187+
}
173188
}
174189

175190
/**
@@ -178,11 +193,11 @@ class MultiProvider(
178193
*/
179194
override fun shutdown() {
180195
val shutdownErrors = mutableListOf<Pair<String, Throwable>>()
181-
uniqueProviders.forEach { provider ->
196+
childFeatureProviders.forEach { provider ->
182197
try {
183198
provider.shutdown()
184199
} catch (t: Throwable) {
185-
shutdownErrors += provider.metadata.getSafeName() to t
200+
shutdownErrors += provider.name to t
186201
}
187202
}
188203

@@ -208,7 +223,13 @@ class MultiProvider(
208223
oldContext: EvaluationContext?,
209224
newContext: EvaluationContext
210225
) {
211-
uniqueProviders.forEach { it.onContextSet(oldContext, newContext) }
226+
coroutineScope {
227+
// If any of these fail, they should individually bubble up their fail
228+
// event and that is handled by handleProviderEvent()
229+
childFeatureProviders
230+
.map { async { it.onContextSet(oldContext, newContext) } }
231+
.awaitAll()
232+
}
212233
}
213234

214235
override fun getBooleanEvaluation(
@@ -217,7 +238,7 @@ class MultiProvider(
217238
context: EvaluationContext?
218239
): ProviderEvaluation<Boolean> {
219240
return strategy.evaluate(
220-
uniqueProviders,
241+
childFeatureProviders,
221242
key,
222243
defaultValue,
223244
context,
@@ -231,7 +252,7 @@ class MultiProvider(
231252
context: EvaluationContext?
232253
): ProviderEvaluation<String> {
233254
return strategy.evaluate(
234-
uniqueProviders,
255+
childFeatureProviders,
235256
key,
236257
defaultValue,
237258
context,
@@ -245,7 +266,7 @@ class MultiProvider(
245266
context: EvaluationContext?
246267
): ProviderEvaluation<Int> {
247268
return strategy.evaluate(
248-
uniqueProviders,
269+
childFeatureProviders,
249270
key,
250271
defaultValue,
251272
context,
@@ -259,7 +280,7 @@ class MultiProvider(
259280
context: EvaluationContext?
260281
): ProviderEvaluation<Double> {
261282
return strategy.evaluate(
262-
uniqueProviders,
283+
childFeatureProviders,
263284
key,
264285
defaultValue,
265286
context,
@@ -273,21 +294,14 @@ class MultiProvider(
273294
context: EvaluationContext?
274295
): ProviderEvaluation<Value> {
275296
return strategy.evaluate(
276-
uniqueProviders,
297+
childFeatureProviders,
277298
key,
278299
defaultValue,
279300
context,
280301
FeatureProvider::getObjectEvaluation
281302
)
282303
}
283304

284-
/**
285-
* Helps us have a consistent way to handle when providers don't provide a name
286-
*/
287-
private fun ProviderMetadata.getSafeName(): String {
288-
return name ?: UNDEFINED_PROVIDER_NAME
289-
}
290-
291305
companion object {
292306
private const val MULTIPROVIDER_NAME = "multiprovider"
293307
private const val UNDEFINED_PROVIDER_NAME = "<unnamed>"

0 commit comments

Comments
 (0)