From b6a9a731900d89d3667536dcf34fcc6f1e847df6 Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Wed, 27 Mar 2024 15:41:30 +0100 Subject: [PATCH] add evaluation of flags inside the confidence object --- .../java/com/spotify/confidence/Confidence.kt | 53 +++--- .../spotify/confidence/ConfidenceContext.kt | 23 +-- .../confidence/ConfidenceFeatureProvider.kt | 158 +++--------------- .../confidence/ConfidenceFlagEvaluation.kt | 105 ++++++++++++ .../confidence/ConfidenceSizeFlushPolicy.kt | 16 ++ .../com/spotify/confidence/EventSender.kt | 2 +- .../com/spotify/confidence/FlagEvaluator.kt | 23 ++- .../spotify/confidence/RemoteFlagResolver.kt | 24 +-- .../spotify/confidence/cache/DiskStorage.kt | 11 +- ...StorageFileCache.kt => FileDiskStorage.kt} | 51 +----- .../spotify/confidence/cache/InMemoryCache.kt | 28 ---- .../spotify/confidence/cache/ProviderCache.kt | 32 ---- .../spotify/confidence/client/CommonTypes.kt | 2 + .../com/spotify/confidence/client/Types.kt | 4 +- .../ConfidenceFeatureProviderTests.kt | 1 - .../confidence/ConfidenceIntegrationTests.kt | 4 +- .../confidence/StorageFileCacheTests.kt | 1 - 17 files changed, 217 insertions(+), 321 deletions(-) create mode 100644 Provider/src/main/java/com/spotify/confidence/ConfidenceFlagEvaluation.kt create mode 100644 Provider/src/main/java/com/spotify/confidence/ConfidenceSizeFlushPolicy.kt rename Provider/src/main/java/com/spotify/confidence/cache/{StorageFileCache.kt => FileDiskStorage.kt} (62%) delete mode 100644 Provider/src/main/java/com/spotify/confidence/cache/InMemoryCache.kt delete mode 100644 Provider/src/main/java/com/spotify/confidence/cache/ProviderCache.kt diff --git a/Provider/src/main/java/com/spotify/confidence/Confidence.kt b/Provider/src/main/java/com/spotify/confidence/Confidence.kt index ed9d7daf..30b32c60 100644 --- a/Provider/src/main/java/com/spotify/confidence/Confidence.kt +++ b/Provider/src/main/java/com/spotify/confidence/Confidence.kt @@ -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 @@ -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() private val coroutineScope = CoroutineScope(dispatcher) private var contextMap: MutableMap = mutableMapOf() + private lateinit var appliedFlagResolution: FlagResolution internal val flagResolver by lazy { RemoteFlagResolver( clientSecret, @@ -31,7 +33,11 @@ class Confidence private constructor( ) } - internal suspend fun resolveFlags(flags: List): FlagResolution { + internal fun refreshFlagResolution(flagResolution: FlagResolution) { + this.appliedFlagResolution = flagResolution + } + + internal suspend fun resolveFlags(flags: List): Result { return flagResolver.resolve(flags, getContext()) } @@ -39,8 +45,8 @@ class Confidence private constructor( contextMap[key] = value } - override fun putContext(context: ConfidenceContext) { - putContext(context.name, context.value) + override fun putContext(context: Map) { + contextMap += context } override fun setContext(context: Map) { @@ -55,12 +61,26 @@ class Confidence private constructor( override fun getContext(): Map = this.root.getContext().filterKeys { removedKeys.contains(it) } + contextMap - override suspend fun getValue(flag: String, defaultValue: T): T { - val response = resolveFlags(listOf(flag)) - return response.getValue(flag, defaultValue) + internal fun getValue(flag: String, defaultValue: T): T { + return this.getEvaluation(flag, defaultValue).value + } + + internal fun getEvaluation( + key: String, + defaultValue: T + ): Evaluation { + 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) = Confidence( clientSecret, region, dispatcher, @@ -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 { @@ -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 - } } \ No newline at end of file diff --git a/Provider/src/main/java/com/spotify/confidence/ConfidenceContext.kt b/Provider/src/main/java/com/spotify/confidence/ConfidenceContext.kt index bfacccc7..5fd5125a 100644 --- a/Provider/src/main/java/com/spotify/confidence/ConfidenceContext.kt +++ b/Provider/src/main/java/com/spotify/confidence/ConfidenceContext.kt @@ -8,10 +8,10 @@ interface ConfidenceContextProvider { typealias ConfidenceFieldsType = Map -interface Contextual : ConfidenceContextProvider { - fun withContext(context: ConfidenceContext): Contextual +interface ContextApi : ConfidenceContextProvider { + fun withContext(context: Map): ContextApi - fun putContext(context: ConfidenceContext) + fun putContext(context: Map) fun setContext(context: Map) fun putContext(key: String, value: ConfidenceValue) fun removeContext(key: String) @@ -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 = 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() } \ No newline at end of file diff --git a/Provider/src/main/java/com/spotify/confidence/ConfidenceFeatureProvider.kt b/Provider/src/main/java/com/spotify/confidence/ConfidenceFeatureProvider.kt index cf6d30c5..8ceb2627 100644 --- a/Provider/src/main/java/com/spotify/confidence/ConfidenceFeatureProvider.kt +++ b/Provider/src/main/java/com/spotify/confidence/ConfidenceFeatureProvider.kt @@ -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 @@ -41,15 +30,14 @@ const val SDK_ID = "SDK_ID_KOTLIN_PROVIDER" class ConfidenceFeatureProvider private constructor( override val hooks: List>, 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 { _, _ -> @@ -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) } @@ -178,63 +157,11 @@ class ConfidenceFeatureProvider private constructor( defaultValue: T, context: EvaluationContext? ): ProviderEvaluation { - 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 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): 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 - ) { - 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 @@ -242,7 +169,7 @@ class ConfidenceFeatureProvider private constructor( fun isStorageEmpty( context: Context ): Boolean { - val storage = StorageFileCache.create(context) + val storage = FileDiskStorage.create(context) val data = storage.read() return data == null } @@ -253,75 +180,34 @@ class ConfidenceFeatureProvider private constructor( context: Context, initialisationStrategy: InitialisationStrategy = InitialisationStrategy.FetchAndActivate, hooks: List> = 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 ProviderCache.CacheResolveEntry.toProviderEvaluation( - parsedKey: FlagKey, - defaultValue: T - ): ProviderEvaluation = 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(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 Evaluation.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") } \ No newline at end of file diff --git a/Provider/src/main/java/com/spotify/confidence/ConfidenceFlagEvaluation.kt b/Provider/src/main/java/com/spotify/confidence/ConfidenceFlagEvaluation.kt new file mode 100644 index 00000000..09b34f12 --- /dev/null +++ b/Provider/src/main/java/com/spotify/confidence/ConfidenceFlagEvaluation.kt @@ -0,0 +1,105 @@ +package com.spotify.confidence + +import com.spotify.confidence.client.ResolveReason + +internal fun FlagResolution.getEvaluation( + flag: String, + defaultValue: T, + context: Map +): Evaluation { + val parsedKey = FlagKey(flag) + val resolvedFlag = this.flags.firstOrNull { it.flag == parsedKey.flagName } + ?: return Evaluation( + value = defaultValue, + reason = ResolveReason.RESOLVE_REASON_UNSPECIFIED, + errorCode = ErrorCode.FLAG_NOT_FOUND + ) + + // handle stale case + if (this.context != context) { + return Evaluation( + value = defaultValue, + reason = resolvedFlag.reason, + errorCode = ErrorCode.RESOLVE_STALE + ) + } + + // handle flag found + val flagValue = resolvedFlag.value + val resolvedValue: ConfidenceValue = findValueFromValuePath(flagValue, parsedKey.valuePath) + ?: throw ParseError( + "Unable to parse flag value: ${parsedKey.valuePath.joinToString(separator = "/")}" + ) + + return when (resolvedFlag.reason) { + ResolveReason.RESOLVE_REASON_MATCH -> { + val value = getTyped(resolvedValue) ?: defaultValue + return Evaluation( + value = value, + reason = resolvedFlag.reason, + variant = resolvedFlag.variant + ) + } + ResolveReason.RESOLVE_REASON_TARGETING_KEY_ERROR -> { + Evaluation( + value = defaultValue, + reason = resolvedFlag.reason, + errorCode = ErrorCode.INVALID_CONTEXT, + errorMessage = "Invalid targeting key" + ) + } + + else -> { + Evaluation(defaultValue, reason = resolvedFlag.reason) + } + } +} + +@Suppress("UNCHECKED_CAST") +private fun getTyped(v: ConfidenceValue): T? { + return when (v) { + is ConfidenceValue.Boolean -> v.value as T + is ConfidenceValue.Double -> v.value as T + is ConfidenceValue.Int -> v.value as T + is ConfidenceValue.String -> v.value as T + is ConfidenceValue.Struct -> v as T + } +} + +private fun findValueFromValuePath(value: ConfidenceValue.Struct, valuePath: List): ConfidenceValue? { + if (valuePath.isEmpty()) return value + val currValue = value.value[valuePath[0]] + return when { + currValue is ConfidenceValue.Struct -> { + findValueFromValuePath(currValue, valuePath.subList(1, valuePath.count())) + } + valuePath.count() == 1 -> currValue + else -> null + } +} + +private data class FlagKey( + val flagName: String, + val valuePath: List +) { + companion object { + operator fun invoke(evalKey: String): FlagKey { + val splits = evalKey.split(".") + return FlagKey( + splits.getOrNull(0)!!, + splits.subList(1, splits.count()) + ) + } + } +} + +enum class ErrorCode { + CACHE_EMPTY, + RESOLVE_STALE, + + // The flag could not be found. + FLAG_NOT_FOUND, + INVALID_CONTEXT +} + +class ParseError(message: String) : Error(message) \ No newline at end of file diff --git a/Provider/src/main/java/com/spotify/confidence/ConfidenceSizeFlushPolicy.kt b/Provider/src/main/java/com/spotify/confidence/ConfidenceSizeFlushPolicy.kt new file mode 100644 index 00000000..5afea6e1 --- /dev/null +++ b/Provider/src/main/java/com/spotify/confidence/ConfidenceSizeFlushPolicy.kt @@ -0,0 +1,16 @@ +package com.spotify.confidence + +internal val confidenceSizeFlushPolicy = object : FlushPolicy { + private var size = 0 + override fun reset() { + size = 0 + } + + override fun hit(event: Event) { + size++ + } + + override fun shouldFlush(): Boolean { + return size > 4 + } +} \ No newline at end of file diff --git a/Provider/src/main/java/com/spotify/confidence/EventSender.kt b/Provider/src/main/java/com/spotify/confidence/EventSender.kt index f7836773..0a20401a 100644 --- a/Provider/src/main/java/com/spotify/confidence/EventSender.kt +++ b/Provider/src/main/java/com/spotify/confidence/EventSender.kt @@ -2,7 +2,7 @@ package com.spotify.confidence import java.io.File -interface EventSender : Contextual { +interface EventSender : ContextApi { fun send( definition: String, payload: ConfidenceFieldsType = mapOf() diff --git a/Provider/src/main/java/com/spotify/confidence/FlagEvaluator.kt b/Provider/src/main/java/com/spotify/confidence/FlagEvaluator.kt index 8b03c6db..c8b0bee5 100644 --- a/Provider/src/main/java/com/spotify/confidence/FlagEvaluator.kt +++ b/Provider/src/main/java/com/spotify/confidence/FlagEvaluator.kt @@ -1,27 +1,26 @@ package com.spotify.confidence +import com.spotify.confidence.client.ResolveReason import com.spotify.confidence.client.ResolvedFlag +import kotlinx.serialization.Contextual import kotlinx.serialization.Serializable -interface FlagEvaluator: Contextual { - suspend fun getValue(flag: String, defaultValue: T): T -} - data class Evaluation( - val reason: String, val value: T, + val variant: String? = null, + val reason: ResolveReason, + val errorCode: ErrorCode? = null, + val errorMessage: String? = null ) @Serializable data class FlagResolution( - val context: Map, - val flags: List<@kotlinx.serialization.Contextual ResolvedFlag>, + val context: Map, + val flags: List<@Contextual ResolvedFlag>, val resolveToken: String ) -fun FlagResolution.getEvaluation(flag: String, defaultValue: T): Evaluation { - TODO() -} -fun FlagResolution.getValue(flag: String, defaultValue: T): T { - return getEvaluation(flag, defaultValue).value +sealed class Result { + data class Success(val data: T) : Result() + data class Failure(val error: Throwable) : Result() } \ No newline at end of file diff --git a/Provider/src/main/java/com/spotify/confidence/RemoteFlagResolver.kt b/Provider/src/main/java/com/spotify/confidence/RemoteFlagResolver.kt index 024b050c..6ff42798 100644 --- a/Provider/src/main/java/com/spotify/confidence/RemoteFlagResolver.kt +++ b/Provider/src/main/java/com/spotify/confidence/RemoteFlagResolver.kt @@ -25,7 +25,7 @@ import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response internal interface FlagResolver { - suspend fun resolve(flags: List, context: Map): FlagResolution + suspend fun resolve(flags: List, context: Map): Result } internal class RemoteFlagResolver( @@ -33,15 +33,15 @@ internal class RemoteFlagResolver( private val region: ConfidenceRegion, private val httpClient: OkHttpClient, private val dispatcher: CoroutineDispatcher = Dispatchers.IO, - private val sdkMetadata: SdkMetadata, -): FlagResolver { + private val sdkMetadata: SdkMetadata +) : FlagResolver { private val headers = Headers.headersOf( - "Content-Type", - "application/json", - "Accept", - "application/json" + "Content-Type", + "application/json", + "Accept", + "application/json" ) - override suspend fun resolve(flags: List, context: Map): FlagResolution { + override suspend fun resolve(flags: List, context: Map): Result { val sdk = Sdk(sdkMetadata.sdkId, sdkMetadata.sdkVersion) val request = ResolveFlagsRequest(flags, context, clientSecret, false, sdk) @@ -56,13 +56,14 @@ internal class RemoteFlagResolver( httpClient.newCall(httpRequest).await().toResolveFlags() } - when(response) { + return when (response) { is ResolveResponse.Resolved -> { val (flagList, resolveToken) = response.flags - return FlagResolution(context, flagList.list, resolveToken) + Result.Success(FlagResolution(context, flagList.list, resolveToken)) } + else -> { - TODO() + Result.Failure(Error("could not return flag resolution")) } } } @@ -72,7 +73,6 @@ internal class RemoteFlagResolver( ConfidenceRegion.EUROPE -> "https://resolver.eu.confidence.dev" ConfidenceRegion.USA -> "https://resolver.us.confidence.dev" } - } private val json = Json { diff --git a/Provider/src/main/java/com/spotify/confidence/cache/DiskStorage.kt b/Provider/src/main/java/com/spotify/confidence/cache/DiskStorage.kt index 4e463bb7..f36266f0 100644 --- a/Provider/src/main/java/com/spotify/confidence/cache/DiskStorage.kt +++ b/Provider/src/main/java/com/spotify/confidence/cache/DiskStorage.kt @@ -1,16 +1,11 @@ package com.spotify.confidence.cache +import com.spotify.confidence.FlagResolution import com.spotify.confidence.apply.ApplyInstance -import com.spotify.confidence.client.ResolvedFlag -import dev.openfeature.sdk.EvaluationContext interface DiskStorage { - fun store( - resolvedFlags: List, - resolveToken: String, - evaluationContext: EvaluationContext - ): CacheData - fun read(): CacheData? + fun store(flagResolution: FlagResolution) + fun read(): FlagResolution? fun clear() fun writeApplyData(applyData: Map>) diff --git a/Provider/src/main/java/com/spotify/confidence/cache/StorageFileCache.kt b/Provider/src/main/java/com/spotify/confidence/cache/FileDiskStorage.kt similarity index 62% rename from Provider/src/main/java/com/spotify/confidence/cache/StorageFileCache.kt rename to Provider/src/main/java/com/spotify/confidence/cache/FileDiskStorage.kt index f5dfa4c9..c5983bbc 100644 --- a/Provider/src/main/java/com/spotify/confidence/cache/StorageFileCache.kt +++ b/Provider/src/main/java/com/spotify/confidence/cache/FileDiskStorage.kt @@ -1,13 +1,10 @@ package com.spotify.confidence.cache import android.content.Context +import com.spotify.confidence.FlagResolution import com.spotify.confidence.apply.ApplyInstance -import com.spotify.confidence.client.ResolvedFlag import com.spotify.confidence.client.serializers.UUIDSerializer import dev.openfeature.sdk.DateSerializer -import dev.openfeature.sdk.EvaluationContext -import dev.openfeature.sdk.Value -import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.modules.SerializersModule @@ -17,24 +14,13 @@ import java.io.File internal const val FLAGS_FILE_NAME = "confidence_flags_cache.json" internal const val APPLY_FILE_NAME = "confidence_apply_cache.json" -internal class StorageFileCache private constructor( +internal class FileDiskStorage private constructor( private val flagsFile: File, private val applyFile: File ) : DiskStorage { - override fun store( - resolvedFlags: List, - resolveToken: String, - evaluationContext: EvaluationContext - ): CacheData { - val data = toCacheData( - resolvedFlags, - resolveToken, - evaluationContext - ) - - write(Json.encodeToString(data)) - return data + override fun store(flagResolution: FlagResolution) { + write(Json.encodeToString(flagResolution)) } override fun clear() { @@ -59,7 +45,7 @@ internal class StorageFileCache private constructor( flagsFile.writeText(data) } - override fun read(): CacheData? { + override fun read(): FlagResolution? { if (!flagsFile.exists()) return null val fileText: String = flagsFile.bufferedReader().use { it.readText() } return if (fileText.isEmpty()) { @@ -71,7 +57,7 @@ internal class StorageFileCache private constructor( companion object { fun create(context: Context): DiskStorage { - return StorageFileCache( + return FileDiskStorage( flagsFile = File(context.filesDir, FLAGS_FILE_NAME), applyFile = File(context.filesDir, APPLY_FILE_NAME) ) @@ -81,34 +67,11 @@ internal class StorageFileCache private constructor( * Testing purposes only! */ fun forFiles(flagsFile: File, applyFile: File): DiskStorage { - return StorageFileCache(flagsFile, applyFile) + return FileDiskStorage(flagsFile, applyFile) } } } -internal fun toCacheData( - resolvedFlags: List, - resolveToken: String, - evaluationContext: EvaluationContext -) = CacheData( - values = resolvedFlags.associate { - it.flag to ProviderCache.CacheEntry( - it.variant, - Value.Structure(it.value.asMap()), - it.reason - ) - }, - evaluationContextHash = evaluationContext.hashCode(), - resolveToken = resolveToken -) - -@Serializable -data class CacheData( - val resolveToken: String, - val evaluationContextHash: Int, - val values: Map -) - internal val json = Json { serializersModule = SerializersModule { contextual(UUIDSerializer) diff --git a/Provider/src/main/java/com/spotify/confidence/cache/InMemoryCache.kt b/Provider/src/main/java/com/spotify/confidence/cache/InMemoryCache.kt deleted file mode 100644 index 5b531eed..00000000 --- a/Provider/src/main/java/com/spotify/confidence/cache/InMemoryCache.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.spotify.confidence.cache - -import com.spotify.confidence.cache.ProviderCache.CacheResolveEntry -import com.spotify.confidence.cache.ProviderCache.CacheResolveResult -import dev.openfeature.sdk.EvaluationContext - -open class InMemoryCache : ProviderCache { - private var data: CacheData? = null - - override fun refresh(cacheData: CacheData) { - data = cacheData - } - - override fun resolve(flagName: String, ctx: EvaluationContext): CacheResolveResult { - val dataSnapshot = data ?: return CacheResolveResult.NotFound - val cacheEntry = dataSnapshot.values[flagName] ?: return CacheResolveResult.NotFound - if (ctx.hashCode() != dataSnapshot.evaluationContextHash) { - return CacheResolveResult.Stale - } - return CacheResolveResult.Found( - CacheResolveEntry(cacheEntry.variant, cacheEntry.value, dataSnapshot.resolveToken, cacheEntry.reason) - ) - } - - override fun clear() { - data = null - } -} \ No newline at end of file diff --git a/Provider/src/main/java/com/spotify/confidence/cache/ProviderCache.kt b/Provider/src/main/java/com/spotify/confidence/cache/ProviderCache.kt deleted file mode 100644 index f6476b20..00000000 --- a/Provider/src/main/java/com/spotify/confidence/cache/ProviderCache.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.spotify.confidence.cache - -import com.spotify.confidence.client.ResolveReason -import dev.openfeature.sdk.EvaluationContext -import dev.openfeature.sdk.Value -import kotlinx.serialization.Serializable - -interface ProviderCache { - fun refresh(cacheData: CacheData) - fun resolve(flagName: String, ctx: EvaluationContext): CacheResolveResult - fun clear() - - data class CacheResolveEntry( - val variant: String, - val value: Value.Structure, - val resolveToken: String, - val resolveReason: ResolveReason - ) - - @Serializable - data class CacheEntry( - val variant: String, - val value: Value.Structure, - val reason: ResolveReason - ) - - sealed interface CacheResolveResult { - data class Found(val entry: CacheResolveEntry) : CacheResolveResult - object NotFound : CacheResolveResult - object Stale : CacheResolveResult - } -} \ No newline at end of file diff --git a/Provider/src/main/java/com/spotify/confidence/client/CommonTypes.kt b/Provider/src/main/java/com/spotify/confidence/client/CommonTypes.kt index 2c83217e..c6da6473 100644 --- a/Provider/src/main/java/com/spotify/confidence/client/CommonTypes.kt +++ b/Provider/src/main/java/com/spotify/confidence/client/CommonTypes.kt @@ -13,6 +13,8 @@ enum class ResolveReason { // The flag was successfully resolved because one rule matched. RESOLVE_REASON_MATCH, + RESOLVE_REASON_STALE, + // The flag could not be resolved because no rule matched. RESOLVE_REASON_NO_SEGMENT_MATCH, diff --git a/Provider/src/main/java/com/spotify/confidence/client/Types.kt b/Provider/src/main/java/com/spotify/confidence/client/Types.kt index 9d70c8b6..3129645f 100644 --- a/Provider/src/main/java/com/spotify/confidence/client/Types.kt +++ b/Provider/src/main/java/com/spotify/confidence/client/Types.kt @@ -1,6 +1,6 @@ package com.spotify.confidence.client -import dev.openfeature.sdk.ImmutableStructure +import com.spotify.confidence.ConfidenceValue import dev.openfeature.sdk.Structure import kotlinx.serialization.Contextual import kotlinx.serialization.Serializable @@ -43,7 +43,7 @@ data class Flags( data class ResolvedFlag( val flag: String, val variant: String, - val value: Structure = ImmutableStructure(mutableMapOf()), + val value: ConfidenceValue.Struct = ConfidenceValue.Struct(mapOf()), val reason: ResolveReason ) diff --git a/Provider/src/test/java/com/spotify/confidence/ConfidenceFeatureProviderTests.kt b/Provider/src/test/java/com/spotify/confidence/ConfidenceFeatureProviderTests.kt index 2e1b5848..ed89135a 100644 --- a/Provider/src/test/java/com/spotify/confidence/ConfidenceFeatureProviderTests.kt +++ b/Provider/src/test/java/com/spotify/confidence/ConfidenceFeatureProviderTests.kt @@ -13,7 +13,6 @@ import android.content.Context import com.spotify.confidence.apply.EventStatus import com.spotify.confidence.apply.FlagsAppliedMap import com.spotify.confidence.cache.APPLY_FILE_NAME -import com.spotify.confidence.cache.InMemoryCache import com.spotify.confidence.cache.json import com.spotify.confidence.cache.toCacheData import com.spotify.confidence.client.AppliedFlag diff --git a/Provider/src/test/java/com/spotify/confidence/ConfidenceIntegrationTests.kt b/Provider/src/test/java/com/spotify/confidence/ConfidenceIntegrationTests.kt index f12f1946..14f153f9 100644 --- a/Provider/src/test/java/com/spotify/confidence/ConfidenceIntegrationTests.kt +++ b/Provider/src/test/java/com/spotify/confidence/ConfidenceIntegrationTests.kt @@ -2,7 +2,7 @@ package com.spotify.confidence import android.content.Context import com.spotify.confidence.cache.FLAGS_FILE_NAME -import com.spotify.confidence.cache.StorageFileCache +import com.spotify.confidence.cache.FileDiskStorage import com.spotify.confidence.client.ResolveReason import com.spotify.confidence.client.ResolvedFlag import dev.openfeature.sdk.ImmutableContext @@ -59,7 +59,7 @@ class ConfidenceIntegrationTests { ) ) - val storage = StorageFileCache.create(mockContext).apply { + val storage = FileDiskStorage.create(mockContext).apply { val flags = listOf( ResolvedFlag( "kotlin-test-flag", diff --git a/Provider/src/test/java/com/spotify/confidence/StorageFileCacheTests.kt b/Provider/src/test/java/com/spotify/confidence/StorageFileCacheTests.kt index ee62037f..8d2b0248 100644 --- a/Provider/src/test/java/com/spotify/confidence/StorageFileCacheTests.kt +++ b/Provider/src/test/java/com/spotify/confidence/StorageFileCacheTests.kt @@ -3,7 +3,6 @@ package com.spotify.confidence import android.content.Context -import com.spotify.confidence.cache.InMemoryCache import com.spotify.confidence.client.ConfidenceClient import com.spotify.confidence.client.Flags import com.spotify.confidence.client.ResolveFlags