@@ -3,6 +3,7 @@ package dev.openfeature.kotlin.sdk.multiprovider
33import dev.openfeature.kotlin.sdk.EvaluationContext
44import dev.openfeature.kotlin.sdk.FeatureProvider
55import dev.openfeature.kotlin.sdk.Hook
6+ import dev.openfeature.kotlin.sdk.OpenFeatureStatus
67import dev.openfeature.kotlin.sdk.ProviderEvaluation
78import dev.openfeature.kotlin.sdk.ProviderMetadata
89import dev.openfeature.kotlin.sdk.Value
@@ -13,9 +14,12 @@ import kotlinx.coroutines.awaitAll
1314import kotlinx.coroutines.coroutineScope
1415import kotlinx.coroutines.flow.Flow
1516import kotlinx.coroutines.flow.MutableSharedFlow
17+ import kotlinx.coroutines.flow.MutableStateFlow
1618import kotlinx.coroutines.flow.asSharedFlow
19+ import kotlinx.coroutines.flow.asStateFlow
1720import kotlinx.coroutines.flow.launchIn
1821import 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