Skip to content

Commit

Permalink
add evaluation of flags inside the confidence object
Browse files Browse the repository at this point in the history
  • Loading branch information
vahidlazio committed Mar 27, 2024
1 parent 880f688 commit b6a9a73
Show file tree
Hide file tree
Showing 17 changed files with 217 additions and 321 deletions.
53 changes: 29 additions & 24 deletions Provider/src/main/java/com/spotify/confidence/Confidence.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.spotify.confidence

import android.content.Context
import com.spotify.confidence.client.ConfidenceRegion
import com.spotify.confidence.client.ResolveReason
import com.spotify.confidence.client.SdkMetadata
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
Expand All @@ -17,10 +18,11 @@ class Confidence private constructor(
private val dispatcher: CoroutineDispatcher,
private val eventSenderEngine: EventSenderEngine,
private val root: ConfidenceContextProvider
) : Contextual, EventSender, FlagEvaluator {
) : ContextApi, EventSender {
private val removedKeys = mutableListOf<String>()
private val coroutineScope = CoroutineScope(dispatcher)
private var contextMap: MutableMap<String, ConfidenceValue> = mutableMapOf()
private lateinit var appliedFlagResolution: FlagResolution
internal val flagResolver by lazy {
RemoteFlagResolver(
clientSecret,
Expand All @@ -31,16 +33,20 @@ class Confidence private constructor(
)
}

internal suspend fun resolveFlags(flags: List<String>): FlagResolution {
internal fun refreshFlagResolution(flagResolution: FlagResolution) {
this.appliedFlagResolution = flagResolution
}

internal suspend fun resolveFlags(flags: List<String>): Result<FlagResolution> {
return flagResolver.resolve(flags, getContext())
}

override fun putContext(key: String, value: ConfidenceValue) {
contextMap[key] = value
}

override fun putContext(context: ConfidenceContext) {
putContext(context.name, context.value)
override fun putContext(context: Map<String, ConfidenceValue>) {
contextMap += context
}

override fun setContext(context: Map<String, ConfidenceValue>) {
Expand All @@ -55,12 +61,26 @@ class Confidence private constructor(
override fun getContext(): Map<String, ConfidenceValue> =
this.root.getContext().filterKeys { removedKeys.contains(it) } + contextMap

override suspend fun <T> getValue(flag: String, defaultValue: T): T {
val response = resolveFlags(listOf(flag))
return response.getValue(flag, defaultValue)
internal fun <T> getValue(flag: String, defaultValue: T): T {
return this.getEvaluation(flag, defaultValue).value
}

internal fun <T> getEvaluation(
key: String,
defaultValue: T
): Evaluation<T> {
if (!this::appliedFlagResolution.isInitialized) {
return Evaluation(
value = defaultValue,
reason = ResolveReason.RESOLVE_REASON_UNSPECIFIED,
errorCode = ErrorCode.CACHE_EMPTY
)
}
// TODO APPLY FlAG
return appliedFlagResolution.getEvaluation(key, defaultValue, getContext())
}

override fun withContext(context: ConfidenceContext) = Confidence(
override fun withContext(context: Map<String, ConfidenceValue>) = Confidence(
clientSecret,
region,
dispatcher,
Expand Down Expand Up @@ -101,7 +121,7 @@ class Confidence private constructor(
val engine = EventSenderEngine.instance(
context,
clientSecret,
flushPolicies = listOf(confidenceFlushPolicy),
flushPolicies = listOf(confidenceSizeFlushPolicy),
dispatcher = dispatcher
)
val confidenceContext = object : ConfidenceContextProvider {
Expand All @@ -112,19 +132,4 @@ class Confidence private constructor(
return Confidence(clientSecret, region, dispatcher, engine, confidenceContext)
}
}
}

private val confidenceFlushPolicy = object : FlushPolicy {
private var size = 0
override fun reset() {
size = 0
}

override fun hit(event: Event) {
size++
}

override fun shouldFlush(): Boolean {
return size > 4
}
}
23 changes: 5 additions & 18 deletions Provider/src/main/java/com/spotify/confidence/ConfidenceContext.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ interface ConfidenceContextProvider {

typealias ConfidenceFieldsType = Map<String, ConfidenceValue>

interface Contextual : ConfidenceContextProvider {
fun withContext(context: ConfidenceContext): Contextual
interface ContextApi : ConfidenceContextProvider {
fun withContext(context: Map<String, ConfidenceValue>): ContextApi

fun putContext(context: ConfidenceContext)
fun putContext(context: Map<String, ConfidenceValue>)
fun setContext(context: Map<String, ConfidenceValue>)
fun putContext(key: String, value: ConfidenceValue)
fun removeContext(key: String)
Expand All @@ -22,23 +22,10 @@ interface ConfidenceContext {
val value: ConfidenceValue
}

class PageContext(private val page: String) : ConfidenceContext {
override val value: ConfidenceValue
get() = ConfidenceValue.String(page)
override val name: String
get() = "page"
}

class CommonContext : ConfidenceContextProvider {
override fun getContext(): Map<String, ConfidenceValue> = mapOf()
}

fun EvaluationContext.toConfidenceContext() = object : ConfidenceContext {
override val name: String = "open_feature"
override val value: ConfidenceValue
get() = ConfidenceValue.Struct(
asMap()
.map { it.key to ConfidenceValue.String(it.value.toString()) }
.toMap() + ("targeting_key" to ConfidenceValue.String(getTargetingKey()))
)
fun EvaluationContext.toConfidenceContext(): ConfidenceValue.Struct {
TODO()
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,17 @@
package com.spotify.confidence

import android.content.Context
import com.spotify.confidence.apply.FlagApplier
import com.spotify.confidence.apply.FlagApplierWithRetries
import com.spotify.confidence.cache.DiskStorage
import com.spotify.confidence.cache.InMemoryCache
import com.spotify.confidence.cache.ProviderCache
import com.spotify.confidence.cache.ProviderCache.CacheResolveResult
import com.spotify.confidence.cache.StorageFileCache
import com.spotify.confidence.client.ConfidenceClient
import com.spotify.confidence.client.ResolveResponse
import com.spotify.confidence.cache.FileDiskStorage
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.Reason
import dev.openfeature.sdk.Value
import dev.openfeature.sdk.events.EventHandler
import dev.openfeature.sdk.events.OpenFeatureEvents
import dev.openfeature.sdk.exceptions.ErrorCode
import dev.openfeature.sdk.exceptions.OpenFeatureError.FlagNotFoundError
import dev.openfeature.sdk.exceptions.OpenFeatureError.InvalidContextError
import dev.openfeature.sdk.exceptions.OpenFeatureError.ParseError
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
Expand All @@ -41,15 +30,14 @@ const val SDK_ID = "SDK_ID_KOTLIN_PROVIDER"
class ConfidenceFeatureProvider private constructor(
override val hooks: List<Hook<*>>,
override val metadata: ProviderMetadata,
private val cache: ProviderCache,
private val storage: DiskStorage,
private val initialisationStrategy: InitialisationStrategy,
private val flagApplier: FlagApplier,
private val eventHandler: EventHandler,
private val confidenceAPI: Confidence,
dispatcher: CoroutineDispatcher
) : FeatureProvider {
private val job = SupervisorJob()

private val coroutineScope = CoroutineScope(job + dispatcher)
private val networkExceptionHandler by lazy {
CoroutineExceptionHandler { _, _ ->
Expand All @@ -72,32 +60,23 @@ class ConfidenceFeatureProvider private constructor(
strategy: InitialisationStrategy
) {
// refresh cache with the last stored data
storage.read()?.let(cache::refresh)
storage.read()?.let(confidenceAPI::refreshFlagResolution)

if (strategy == InitialisationStrategy.ActivateAndFetchAsync) {
eventHandler.publish(OpenFeatureEvents.ProviderReady)
}

coroutineScope.launch(networkExceptionHandler) {
confidenceAPI.putContext(initialContext.toConfidenceContext())
confidenceAPI.putContext("open_feature", initialContext.toConfidenceContext())
val resolveResponse = confidenceAPI.resolveFlags(listOf())
if (resolveResponse is ResolveResponse.Resolved) {
val (flags, resolveToken) = resolveResponse.flags

// update the cache and emit the ready signal when the strategy is expected
// to wait for the network response

if (resolveResponse is Result.Success) {
// we store the flag anyways
val storedData = storage.store(
flags.list,
resolveToken,
initialContext
)
storage.store(resolveResponse.data)

when (strategy) {
InitialisationStrategy.FetchAndActivate -> {
// refresh the cache from the stored data
cache.refresh(cacheData = storedData)
confidenceAPI.refreshFlagResolution(resolveResponse.data)
eventHandler.publish(OpenFeatureEvents.ProviderReady)
}

Expand Down Expand Up @@ -178,71 +157,19 @@ class ConfidenceFeatureProvider private constructor(
defaultValue: T,
context: EvaluationContext?
): ProviderEvaluation<T> {
val parsedKey = FlagKey(key)
val evaluationContext = context ?: throw InvalidContextError()
return when (val resolve: CacheResolveResult = cache.resolve(parsedKey.flagName, evaluationContext)) {
is CacheResolveResult.Found -> {
resolve.entry.toProviderEvaluation(
parsedKey,
defaultValue
)
}
CacheResolveResult.Stale -> ProviderEvaluation(
value = defaultValue,
reason = Reason.ERROR.toString(),
errorCode = ErrorCode.PROVIDER_NOT_READY
)
CacheResolveResult.NotFound -> throw FlagNotFoundError(parsedKey.flagName)
}
}

@Suppress("UNCHECKED_CAST")
private fun <T> getTyped(v: Value): T? {
return when (v) {
is Value.Boolean -> v.boolean as T
is Value.Double -> v.double as T
is Value.Date -> v.date as T
is Value.Integer -> v.integer as T
is Value.List -> v as T
is Value.String -> v.string as T
is Value.Structure -> v as T
Value.Null -> null
}
}

private fun findValueFromValuePath(value: Value.Structure, valuePath: List<String>): Value? {
if (valuePath.isEmpty()) return value
val currValue = value.structure[valuePath[0]]
return when {
currValue is Value.Structure -> {
findValueFromValuePath(currValue, valuePath.subList(1, valuePath.count()))
}
valuePath.count() == 1 -> currValue
else -> null
}
}

private data class FlagKey(
val flagName: String,
val valuePath: List<String>
) {
companion object {
operator fun invoke(evalKey: String): FlagKey {
val splits = evalKey.split(".")
return FlagKey(
splits.getOrNull(0)!!,
splits.subList(1, splits.count())
)
}
}
context ?: throw InvalidContextError()
return confidenceAPI.getEvaluation(
key,
defaultValue
).toProviderEvaluation()
}
companion object {
private class ConfidenceMetadata(override var name: String? = "confidence") : ProviderMetadata

fun isStorageEmpty(
context: Context
): Boolean {
val storage = StorageFileCache.create(context)
val storage = FileDiskStorage.create(context)
val data = storage.read()
return data == null
}
Expand All @@ -253,75 +180,34 @@ class ConfidenceFeatureProvider private constructor(
context: Context,
initialisationStrategy: InitialisationStrategy = InitialisationStrategy.FetchAndActivate,
hooks: List<Hook<*>> = listOf(),
client: ConfidenceClient? = null,
metadata: ProviderMetadata = ConfidenceMetadata(),
cache: ProviderCache? = null,
storage: DiskStorage? = null,
flagApplier: FlagApplier? = null,
eventHandler: EventHandler = EventHandler(Dispatchers.IO),
dispatcher: CoroutineDispatcher = Dispatchers.IO
): ConfidenceFeatureProvider {
val diskStorage = storage ?: StorageFileCache.create(context)
val flagApplierWithRetries = flagApplier ?: FlagApplierWithRetries(
client = TODO(),
dispatcher = dispatcher,
diskStorage = diskStorage
)

val diskStorage = storage ?: FileDiskStorage.create(context)
return ConfidenceFeatureProvider(
hooks = hooks,
metadata = metadata,
cache = cache ?: InMemoryCache(),
storage = diskStorage,
initialisationStrategy = initialisationStrategy,
flagApplier = flagApplierWithRetries,
eventHandler,
confidence,
dispatcher
)
}
}

private fun <T> ProviderCache.CacheResolveEntry.toProviderEvaluation(
parsedKey: FlagKey,
defaultValue: T
): ProviderEvaluation<T> = when (resolveReason) {
com.spotify.confidence.client.ResolveReason.RESOLVE_REASON_MATCH -> {
val resolvedValue: Value = findValueFromValuePath(value, parsedKey.valuePath)
?: throw ParseError(
"Unable to parse flag value: ${parsedKey.valuePath.joinToString(separator = "/")}"
)
val value = getTyped<T>(resolvedValue) ?: defaultValue
flagApplier.apply(parsedKey.flagName, resolveToken)
ProviderEvaluation(
value = value,
variant = variant,
reason = Reason.TARGETING_MATCH.toString()
)
}
com.spotify.confidence.client.ResolveReason.RESOLVE_REASON_TARGETING_KEY_ERROR -> {
ProviderEvaluation(
value = defaultValue,
reason = Reason.ERROR.toString(),
errorCode = ErrorCode.INVALID_CONTEXT,
errorMessage = "Invalid targeting key"
)
}
else -> {
flagApplier.apply(parsedKey.flagName, resolveToken)
ProviderEvaluation(
value = defaultValue,
reason = Reason.DEFAULT.toString()
)
}
}
}

private fun <T> Evaluation<T>.toProviderEvaluation() = ProviderEvaluation(
reason = this.reason.name,
errorCode = null, // TODO convert
errorMessage = this.errorMessage,
value = this.value,
variant = this.variant
)

sealed interface InitialisationStrategy {
object FetchAndActivate : InitialisationStrategy
object ActivateAndFetchAsync : InitialisationStrategy
}

private fun ConfidenceContext.toEvaluationContext(): EvaluationContext {
TODO("Not yet implemented")
}
Loading

0 comments on commit b6a9a73

Please sign in to comment.