Skip to content

Commit

Permalink
Merge pull request #58 from spotify/initialisation-strategy
Browse files Browse the repository at this point in the history
feat: Initialisation strategy
  • Loading branch information
vahidlazio authored Sep 6, 2023
2 parents c8f9fc5 + 197bb5e commit 5b3e011
Show file tree
Hide file tree
Showing 10 changed files with 229 additions and 67 deletions.
1 change: 1 addition & 0 deletions ConfidenceDemoApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
signingConfig = signingConfigs.getByName("debug")
}
}
compileOptions {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import dev.openfeature.contrib.providers.ConfidenceFeatureProvider
import dev.openfeature.contrib.providers.InitialisationStrategy
import dev.openfeature.sdk.Client
import dev.openfeature.sdk.EvaluationContext
import dev.openfeature.sdk.FlagEvaluationDetails
Expand Down Expand Up @@ -36,10 +37,18 @@ class MainVm(app: Application) : AndroidViewModel(app) {
init {
val start = System.currentTimeMillis()
val clientSecret = ClientSecretProvider.clientSecret()

val strategy = if(ConfidenceFeatureProvider.isStorageEmpty(app.applicationContext)) {
InitialisationStrategy.FetchAndActivate
} else {
InitialisationStrategy.ActivateAndFetchAsync
}

OpenFeatureAPI.setProvider(
ConfidenceFeatureProvider.create(
app.applicationContext,
clientSecret
clientSecret,
initialisationStrategy = strategy
),
initialContext = ctx
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package dev.openfeature.contrib.providers
import android.content.Context
import dev.openfeature.contrib.providers.apply.FlagApplier
import dev.openfeature.contrib.providers.apply.FlagApplierWithRetries
import dev.openfeature.contrib.providers.cache.DiskStorage
import dev.openfeature.contrib.providers.cache.InMemoryCache
import dev.openfeature.contrib.providers.cache.ProviderCache
import dev.openfeature.contrib.providers.cache.ProviderCache.CacheResolveResult
import dev.openfeature.contrib.providers.cache.StorageFileCache
Expand Down Expand Up @@ -41,10 +43,12 @@ 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 client: ConfidenceClient,
private val flagApplier: FlagApplier,
private val eventsPublisher: EventsPublisher,
private val dispatcher: CoroutineDispatcher
dispatcher: CoroutineDispatcher
) : FeatureProvider {
private val job = SupervisorJob()
private val coroutineScope = CoroutineScope(job + dispatcher)
Expand All @@ -54,14 +58,43 @@ class ConfidenceFeatureProvider private constructor(
eventsPublisher.publish(OpenFeatureEvents.ProviderReady)
}
}

override fun initialize(initialContext: EvaluationContext?) {
if (initialContext == null) return

// refresh cache with the last stored data
storage.read()?.let(cache::refresh)

if (initialisationStrategy == InitialisationStrategy.ActivateAndFetchAsync) {
eventsPublisher.publish(OpenFeatureEvents.ProviderReady)
}

coroutineScope.launch(networkExceptionHandler) {
val resolveResponse = client.resolve(listOf(), initialContext)
if (resolveResponse is ResolveResponse.Resolved) {
val (flags, resolveToken) = resolveResponse.flags
cache.refresh(flags.list, resolveToken, initialContext)
eventsPublisher.publish(OpenFeatureEvents.ProviderReady)

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

// we store the flag anyways
val storedData = storage.store(
flags.list,
resolveToken,
initialContext
)

when (initialisationStrategy) {
InitialisationStrategy.FetchAndActivate -> {
// refresh the cache from the stored data
cache.refresh(data = storedData)
eventsPublisher.publish(OpenFeatureEvents.ProviderReady)
}

InitialisationStrategy.ActivateAndFetchAsync -> {
// do nothing
}
}
}
}
}
Expand Down Expand Up @@ -184,15 +217,25 @@ class ConfidenceFeatureProvider private constructor(
companion object {
private class ConfidenceMetadata(override var name: String? = "confidence") : ProviderMetadata

fun isStorageEmpty(
context: Context
): Boolean {
val storage = StorageFileCache.create(context)
val data = storage.read()
return data == null
}

@Suppress("LongParameterList")
fun create(
context: Context,
clientSecret: String,
region: ConfidenceRegion = ConfidenceRegion.EUROPE,
initialisationStrategy: InitialisationStrategy = InitialisationStrategy.FetchAndActivate,
hooks: List<Hook<*>> = listOf(),
client: ConfidenceClient? = null,
metadata: ProviderMetadata = ConfidenceMetadata(),
cache: ProviderCache? = null,
storage: DiskStorage? = null,
flagApplier: FlagApplier? = null,
eventsPublisher: EventsPublisher = EventHandler.eventsPublisher(Dispatchers.IO),
dispatcher: CoroutineDispatcher = Dispatchers.IO
Expand All @@ -211,7 +254,9 @@ class ConfidenceFeatureProvider private constructor(
return ConfidenceFeatureProvider(
hooks = hooks,
metadata = metadata,
cache = cache ?: StorageFileCache.create(context),
cache = cache ?: InMemoryCache(),
storage = storage ?: StorageFileCache.create(context),
initialisationStrategy = initialisationStrategy,
client = configuredClient,
flagApplier = flagApplierWithRetries,
eventsPublisher,
Expand Down Expand Up @@ -253,4 +298,9 @@ class ConfidenceFeatureProvider private constructor(
)
}
}
}

sealed interface InitialisationStrategy {
object FetchAndActivate : InitialisationStrategy
object ActivateAndFetchAsync : InitialisationStrategy
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package dev.openfeature.contrib.providers.cache

import dev.openfeature.contrib.providers.client.ResolvedFlag
import dev.openfeature.sdk.EvaluationContext

interface DiskStorage {
fun store(
resolvedFlags: List<ResolvedFlag>,
resolveToken: String,
evaluationContext: EvaluationContext
): CacheData
fun read(): CacheData?

fun clear()
}
Original file line number Diff line number Diff line change
@@ -1,32 +1,14 @@
package dev.openfeature.contrib.providers.cache

import dev.openfeature.contrib.providers.cache.ProviderCache.CacheEntry
import dev.openfeature.contrib.providers.cache.ProviderCache.CacheResolveEntry
import dev.openfeature.contrib.providers.cache.ProviderCache.CacheResolveResult
import dev.openfeature.contrib.providers.client.ResolvedFlag
import dev.openfeature.sdk.EvaluationContext
import dev.openfeature.sdk.Value
import kotlinx.serialization.Serializable

open class InMemoryCache : ProviderCache {
var data: CacheData? = null
private var data: CacheData? = null

override fun refresh(
resolvedFlags: List<ResolvedFlag>,
resolveToken: String,
evaluationContext: EvaluationContext
) {
data = CacheData(
values = resolvedFlags.associate {
it.flag to CacheEntry(
it.variant,
Value.Structure(it.value.asMap()),
it.reason
)
},
evaluationContextHash = evaluationContext.hashCode(),
resolveToken = resolveToken
)
override fun refresh(cacheData: CacheData) {
data = cacheData
}

override fun resolve(flagName: String, ctx: EvaluationContext): CacheResolveResult {
Expand All @@ -43,11 +25,4 @@ open class InMemoryCache : ProviderCache {
override fun clear() {
data = null
}

@Serializable
data class CacheData(
val resolveToken: String,
val evaluationContextHash: Int,
val values: Map<String, CacheEntry>
)
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package dev.openfeature.contrib.providers.cache

import dev.openfeature.contrib.providers.client.ResolveReason
import dev.openfeature.contrib.providers.client.ResolvedFlag
import dev.openfeature.sdk.EvaluationContext
import dev.openfeature.sdk.Value
import kotlinx.serialization.Serializable

interface ProviderCache {
fun refresh(resolvedFlags: List<ResolvedFlag>, resolveToken: String, evaluationContext: EvaluationContext)
fun refresh(data: CacheData)
fun resolve(flagName: String, ctx: EvaluationContext): CacheResolveResult
fun clear()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,48 +3,73 @@ package dev.openfeature.contrib.providers.cache
import android.content.Context
import dev.openfeature.contrib.providers.client.ResolvedFlag
import dev.openfeature.sdk.EvaluationContext
import dev.openfeature.sdk.Value
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.io.File

const val FLAGS_FILE_NAME = "confidence_flags_cache.json"

class StorageFileCache private constructor(context: Context) : InMemoryCache() {
class StorageFileCache private constructor(context: Context) : DiskStorage {
private val file: File = File(context.filesDir, FLAGS_FILE_NAME)

override fun refresh(
override fun store(
resolvedFlags: List<ResolvedFlag>,
resolveToken: String,
evaluationContext: EvaluationContext
) {
super.refresh(resolvedFlags, resolveToken, evaluationContext)
// TODO Should this happen before in-memory cache is changed?
writeToFile()
): CacheData {
val data = toCacheData(
resolvedFlags,
resolveToken,
evaluationContext
)

write(Json.encodeToString(data))
return data
}

override fun clear() {
super.clear()
// TODO Should this happen before in-memory cache is changed?
file.delete()
}

private fun writeToFile() {
val fileData = Json.encodeToString(data)
file.writeText(fileData)
private fun write(data: String) {
file.writeText(data)
}

private fun readFile() {
if (!file.exists()) return
override fun read(): CacheData? {
if (!file.exists()) return null
val fileText: String = file.bufferedReader().use { it.readText() }
if (fileText.isEmpty()) return
data = Json.decodeFromString(CacheData.serializer(), fileText)
if (fileText.isEmpty()) return null
return Json.decodeFromString(fileText)
}

companion object {
fun create(context: Context): StorageFileCache {
val storage = StorageFileCache(context)
storage.readFile()
return storage
fun create(context: Context): DiskStorage {
return StorageFileCache(context)
}
}
}
}

internal fun toCacheData(
resolvedFlags: List<ResolvedFlag>,
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<String, ProviderCache.CacheEntry>
)
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import dev.openfeature.contrib.providers.apply.EventStatus
import dev.openfeature.contrib.providers.apply.FlagsAppliedMap
import dev.openfeature.contrib.providers.apply.json
import dev.openfeature.contrib.providers.cache.InMemoryCache
import dev.openfeature.contrib.providers.cache.toCacheData
import dev.openfeature.contrib.providers.client.AppliedFlag
import dev.openfeature.contrib.providers.client.ConfidenceClient
import dev.openfeature.contrib.providers.client.Flags
Expand Down Expand Up @@ -650,7 +651,8 @@ internal class ConfidenceFeatureProviderTests {
)

// Simulate a case where the context in the cache is not synced with the evaluation's context
cache.refresh(resolvedFlags.list, "token2", ImmutableContext("user1"))
val cacheData = toCacheData(resolvedFlags.list, "token2", ImmutableContext("user1"))
cache.refresh(cacheData)
val evalString = confidenceFeatureProvider.getStringEvaluation("test-kotlin-flag-1.mystring", "default", ImmutableContext("user2"))
val evalBool = confidenceFeatureProvider.getBooleanEvaluation("test-kotlin-flag-1.myboolean", true, ImmutableContext("user2"))
val evalInteger = confidenceFeatureProvider.getIntegerEvaluation("test-kotlin-flag-1.myinteger", 1, ImmutableContext("user2"))
Expand Down Expand Up @@ -736,7 +738,12 @@ internal class ConfidenceFeatureProviderTests {
)
)

cache.refresh(resolvedFlagInvalidKey.list, "token", ImmutableContext("user1"))
val cacheData = toCacheData(
resolvedFlagInvalidKey.list,
"token",
ImmutableContext("user1")
)
cache.refresh(cacheData)
val evalString = confidenceFeatureProvider.getStringEvaluation("test-kotlin-flag-1.mystring", "default", ImmutableContext("user1"))
assertEquals("default", evalString.value)
assertEquals(Reason.ERROR.toString(), evalString.reason)
Expand Down Expand Up @@ -810,7 +817,12 @@ internal class ConfidenceFeatureProviderTests {
)
// Simulate a case where the context in the cache is not synced with the evaluation's context
// This shouldn't have an effect in this test, given that not found values are priority over stale values
cache.refresh(resolvedFlags.list, "token2", ImmutableContext("user1"))
val cacheData = toCacheData(
resolvedFlags.list,
"token2",
ImmutableContext("user1")
)
cache.refresh(cacheData)
val ex = assertThrows(FlagNotFoundError::class.java) {
confidenceFeatureProvider.getStringEvaluation("test-kotlin-flag-2.mystring", "default", ImmutableContext("user2"))
}
Expand Down Expand Up @@ -853,7 +865,7 @@ internal class ConfidenceFeatureProviderTests {
whenever(mockClient.resolve(eq(listOf()), any())).thenReturn(ResolveResponse.NotModified)
confidenceFeatureProvider.initialize(ImmutableContext("user1"))
advanceUntilIdle()
verify(cache, never()).refresh(any(), any(), any())
verify(cache, never()).refresh(any())
}

@Test
Expand Down
Loading

0 comments on commit 5b3e011

Please sign in to comment.