Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor!: split the confidence module to a new module #136

Merged
merged 17 commits into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Confidence/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
121 changes: 121 additions & 0 deletions Confidence/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("maven-publish")
id("org.jlleitschuh.gradle.ktlint")
id("signing")
kotlin("plugin.serialization").version("1.8.10").apply(true)
}

val providerVersion = project.extra["version"].toString()

object Versions {
const val openFeatureSDK = "0.2.3"
const val okHttp = "4.10.0"
const val kotlinxSerialization = "1.6.0"
const val coroutines = "1.7.3"
const val junit = "4.13.2"
const val kotlinMockito = "4.1.0"
const val mockWebServer = "4.9.1"
}

android {
namespace = "com.spotify.confidence"
compileSdk = 33

defaultConfig {
minSdk = 21

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
buildConfigField("String", "SDK_VERSION", "\"" + providerVersion + "\"")
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}

publishing {
singleVariant("release") {
withJavadocJar()
withSourcesJar()
}
}
}

dependencies {
implementation("com.squareup.okhttp3:okhttp:${Versions.okHttp}")
implementation(
"org.jetbrains.kotlinx:kotlinx-serialization-json:${Versions.kotlinxSerialization}"
)
implementation("androidx.lifecycle:lifecycle-process:2.6.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.coroutines}")
testImplementation("junit:junit:${Versions.junit}")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:${Versions.coroutines}")
testImplementation("org.mockito.kotlin:mockito-kotlin:${Versions.kotlinMockito}")
testImplementation("com.squareup.okhttp3:mockwebserver:${Versions.mockWebServer}")
}

publishing {
publications {
register<MavenPublication>("release") {
groupId = project.extra["groupId"].toString()
artifactId = "confidence-sdk-android"
version = providerVersion

pom {
name.set("Confidence SDK Android")
description.set("Android SDK for Confidence")
url.set("https://github.com/spotify/confidence-openfeature-provider-kotlin")
licenses {
license {
name.set("The Apache License, Version 2.0")
url.set("http://www.apache.org/licenses/LICENSE-2.0.txt")
}
}
developers {
developer {
id.set("vahidlazio")
name.set("Vahid Torkaman")
email.set("vahidt@spotify.com")
}
developer {
id.set("fabriziodemaria")
name.set("Fabrizio Demaria")
email.set("fdema@spotify.com")
}
developer {
id.set("nicklasl")
name.set("Nicklas Lundin")
email.set("nicklasl@spotify.com")
}
developer {
id.set("nickybondarenko")
name.set("Nicky Bondarenko")
email.set("nickyb@spotify.com")
}
}
scm {
connection.set(
"scm:git:git://spotify/confidence-openfeature-provider-kotlin.git"
)
developerConnection.set(
"scm:git:ssh://spotify/confidence-openfeature-provider-kotlin.git"
)
url.set("https://github.com/spotify/confidence-openfeature-provider-kotlin")
}
}
afterEvaluate {
from(components["release"])
}
}
}
}

signing {
sign(publishing.publications["release"])
}
Empty file added Confidence/consumer-rules.pro
Empty file.
21 changes: 21 additions & 0 deletions Confidence/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
4 changes: 4 additions & 0 deletions Confidence/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

</manifest>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.spotify.confidence.client
package com.spotify.confidence

enum class ConfidenceRegion {
GLOBAL,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import android.content.Context
import com.spotify.confidence.apply.FlagApplierWithRetries
import com.spotify.confidence.cache.DiskStorage
import com.spotify.confidence.cache.FileDiskStorage
import com.spotify.confidence.client.ConfidenceRegion
import com.spotify.confidence.client.FlagApplierClient
import com.spotify.confidence.client.FlagApplierClientImpl
import com.spotify.confidence.client.SdkMetadata
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
Expand All @@ -19,41 +20,76 @@ import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient

internal const val SDK_ID = "SDK_ID_KOTLIN_CONFIDENCE"

class Confidence internal constructor(
private val clientSecret: String,
private val dispatcher: CoroutineDispatcher,
private val eventSenderEngine: EventSenderEngine,
private val diskStorage: DiskStorage,
private val flagResolver: FlagResolver,
private val cache: ProviderCache = InMemoryCache(),
initialContext: Map<String, ConfidenceValue> = mapOf(),
private val flagApplierClient: FlagApplierClient,
private val parent: ConfidenceContextProvider? = null,
private val region: ConfidenceRegion = ConfidenceRegion.GLOBAL
) : Contextual, EventSender {
private val removedKeys = mutableListOf<String>()
private val contextMap = MutableStateFlow(mapOf<String, ConfidenceValue>())
private val contextMap = MutableStateFlow(initialContext)
private var currentFetchJob: Job? = null

// only return changes not the initial value
// only return distinct value
internal val contextChanges: Flow<Map<String, ConfidenceValue>> = contextMap
private val contextChanges: Flow<Map<String, ConfidenceValue>> = contextMap
.drop(1)
.distinctUntilChanged()
private val coroutineScope = CoroutineScope(dispatcher)
private val eventProducers: MutableList<EventProducer> = mutableListOf()

init {
coroutineScope.launch {
contextChanges
.collect {
fetchAndActivate()
}
}
}

private val flagApplier = FlagApplierWithRetries(
client = flagApplierClient,
dispatcher = dispatcher,
diskStorage = diskStorage
)

internal suspend fun resolve(flags: List<String>): Result<FlagResolution> {
private suspend fun resolve(flags: List<String>): Result<FlagResolution> {
return flagResolver.resolve(flags, getContext())
}
suspend fun awaitReconciliation() {
if (currentFetchJob != null) {
currentFetchJob?.join()
activate()
}
}

internal fun apply(flagName: String, resolveToken: String) {
fun apply(flagName: String, resolveToken: String) {
flagApplier.apply(flagName, resolveToken)
}

fun <T> getValue(key: String, default: T) = getFlag(key, default).value

fun <T> getFlag(
key: String,
defaultValue: T
): Evaluation<T> = cache.get().getEvaluation(
key,
defaultValue,
getContext()
) { flagName, resolveToken ->
// this lambda will be invoked inside the evaluation process
// and only if the resolve reason is not targeting key error.
apply(flagName, resolveToken)
}

@Synchronized
override fun putContext(key: String, value: ConfidenceValue) {
val map = contextMap.value.toMutableMap()
Expand All @@ -68,8 +104,10 @@ class Confidence internal constructor(
contextMap.value = map
}

fun isStorageEmpty(): Boolean = diskStorage.read() == FlagResolution.EMPTY

@Synchronized
internal fun putContext(context: Map<String, ConfidenceValue>, removedKeys: List<String>) {
fun putContext(context: Map<String, ConfidenceValue>, removedKeys: List<String>) {
val map = contextMap.value.toMutableMap()
map += context
for (key in removedKeys) {
Expand Down Expand Up @@ -98,6 +136,8 @@ class Confidence internal constructor(
eventSenderEngine,
diskStorage,
flagResolver,
cache,
mapOf(),
flagApplierClient,
this,
region
Expand All @@ -112,6 +152,42 @@ class Confidence internal constructor(
eventSenderEngine.emit(eventName, message, getContext())
}

private val networkExceptionHandler by lazy {
CoroutineExceptionHandler { _, _ ->
// network failed, provider is ready but with default/cache values
}
}

private fun fetch(): Job = coroutineScope.launch(networkExceptionHandler) {
try {
val resolveResponse = resolve(listOf())
if (resolveResponse is Result.Success) {
// we store the flag anyways except when the response was not modified
if (resolveResponse.data != FlagResolution.EMPTY) {
diskStorage.store(resolveResponse.data)
}
}
} catch (e: ParseError) {
throw ParseError(e.message)
}
}

fun activate() {
val resolveResponse = diskStorage.read()
cache.refresh(resolveResponse)
}

fun asyncFetch() {
currentFetchJob?.cancel()
currentFetchJob = fetch()
}

suspend fun fetchAndActivate() {
currentFetchJob?.cancel()
currentFetchJob = fetch()
currentFetchJob?.join()
activate()
}
override fun track(eventProducer: EventProducer) {
coroutineScope.launch {
eventProducer
Expand Down Expand Up @@ -149,6 +225,7 @@ object ConfidenceFactory {
fun create(
context: Context,
clientSecret: String,
initialContext: Map<String, ConfidenceValue> = mapOf(),
region: ConfidenceRegion = ConfidenceRegion.GLOBAL,
dispatcher: CoroutineDispatcher = Dispatchers.IO
): Confidence {
Expand All @@ -173,16 +250,24 @@ object ConfidenceFactory {
dispatcher = dispatcher,
sdkMetadata = SdkMetadata(SDK_ID, BuildConfig.SDK_VERSION)
)
val visitorId = ConfidenceValue.String(VisitorUtil.getId(context))
val initContext = initialContext.toMutableMap()
initContext[VISITOR_ID_CONTEXT_KEY] = visitorId

return Confidence(
clientSecret,
dispatcher,
engine,
initialContext = initContext,
region = region,
flagResolver = flagResolver,
diskStorage = FileDiskStorage.create(context),
flagApplierClient = flagApplierClient
).apply {
putContext(VISITOR_ID_CONTEXT_KEY, ConfidenceValue.String(VisitorUtil.getId(context)))
}
)
}
}

suspend fun Confidence.awaitPutContext(context: Map<String, ConfidenceValue>) {
putContext(context)
awaitReconciliation()
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package com.spotify.confidence

import com.spotify.confidence.client.ResolveReason
import kotlin.jvm.Throws

@Throws(FlagNotFoundError::class, ParseError::class)
internal fun <T> FlagResolution?.getEvaluation(
fun <T> FlagResolution?.getEvaluation(
flag: String,
defaultValue: T,
context: Map<String, ConfidenceValue>,
Expand Down
Loading
Loading