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

Fetch improvement #42

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
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 build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ dependencies {
// Uncomment when needed
testImplementation("io.mockk:mockk:1.13.8")
testImplementation("io.kotest:kotest-assertions-core:5.7.2")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.0")

testRuntimeOnly("org.junit.platform:junit-platform-launcher")

Expand Down
9 changes: 5 additions & 4 deletions src/main/kotlin/com/featurevisor/sdk/Instance+Fetch.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import java.lang.IllegalArgumentException

const val BODY_BYTE_COUNT = 1000000L
val client = OkHttpClient()

// MARK: - Fetch datafile content
@Throws(IOException::class)
internal fun FeaturevisorInstance.fetchDatafileContent(
suspend fun FeaturevisorInstance.fetchDatafileContent(
url: String,
handleDatafileFetch: DatafileFetchHandler? = null,
completion: (Result<DatafileContent>) -> Unit,
Expand Down Expand Up @@ -40,12 +43,10 @@ private fun fetchDatafileContentFromUrl(
}
}

const val BODY_BYTE_COUNT = 1000000L
private inline fun fetch(
request: Request,
crossinline completion: (Result<DatafileContent>) -> Unit,
) {
val client = OkHttpClient()
val call = client.newCall(request)
call.enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
Expand All @@ -59,7 +60,7 @@ private inline fun fetch(
try {
val content = json.decodeFromString<DatafileContent>(responseBodyString)
completion(Result.success(content))
} catch(throwable: Throwable) {
} catch (throwable: Throwable) {
completion(
Result.failure(
FeaturevisorError.UnparsableJson(
Expand Down
14 changes: 6 additions & 8 deletions src/main/kotlin/com/featurevisor/sdk/Instance+Refresh.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ fun FeaturevisorInstance.startRefreshing() = when {
refreshJob != null -> logger?.warn("refreshing has already started")
refreshInterval == null -> logger?.warn("no `refreshInterval` option provided")
else -> {
refreshJob = CoroutineScope(Dispatchers.Unconfined).launch {
refreshJob = coroutineScope.launch {
while (isActive) {
refresh()
delay(refreshInterval)
Expand All @@ -32,20 +32,18 @@ fun FeaturevisorInstance.stopRefreshing() {
logger?.warn("refreshing has stopped")
}

private fun FeaturevisorInstance.refresh() {
private suspend fun FeaturevisorInstance.refresh() {
logger?.debug("refreshing datafile")
when {
statuses.refreshInProgress -> logger?.warn("refresh in progress, skipping")
datafileUrl.isNullOrBlank() -> logger?.error("cannot refresh since `datafileUrl` is not provided")
else -> {
statuses.refreshInProgress = true
fetchDatafileContent(
datafileUrl,
handleDatafileFetch,
url = datafileUrl,
handleDatafileFetch = handleDatafileFetch,
) { result ->

if (result.isSuccess) {
val datafileContent = result.getOrThrow()
result.onSuccess { datafileContent ->
val currentRevision = getRevision()
val newRevision = datafileContent.revision
val isNotSameRevision = currentRevision != newRevision
Expand All @@ -59,7 +57,7 @@ private fun FeaturevisorInstance.refresh() {
}

statuses.refreshInProgress = false
} else {
}.onFailure {
logger?.error(
"failed to refresh datafile",
mapOf("error" to result)
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/com/featurevisor/sdk/Instance+Status.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ package com.featurevisor.sdk

data class Statuses(var ready: Boolean, var refreshInProgress: Boolean)

internal fun FeaturevisorInstance.isReady(): Boolean {
fun FeaturevisorInstance.isReady(): Boolean {
return statuses.ready
}
17 changes: 9 additions & 8 deletions src/main/kotlin/com/featurevisor/sdk/Instance+Variable.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
package com.featurevisor.sdk

import com.featurevisor.testRunner.getVariableValues
import com.featurevisor.types.Context
import com.featurevisor.types.FeatureKey
import com.featurevisor.types.VariableKey
import com.featurevisor.types.VariableValue
import com.featurevisor.types.VariableValue.ArrayValue
import com.featurevisor.types.VariableValue.BooleanValue
import com.featurevisor.types.VariableValue.DoubleValue
import com.featurevisor.types.VariableValue.IntValue
import com.featurevisor.types.VariableValue.JsonValue
import com.featurevisor.types.VariableValue.ObjectValue
import com.featurevisor.types.VariableValue.StringValue
import com.featurevisor.types.VariableValue.*
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromJsonElement
Expand Down Expand Up @@ -76,8 +71,14 @@ inline fun <reified T : Any> FeaturevisorInstance.getVariableObject(
context: Context,
): T? {
val objectValue = getVariable(featureKey, variableKey, context) as? ObjectValue
val actualValue = objectValue?.value?.keys?.map {
mapOf(
it to getVariableValues(objectValue.value[it]).toString()
)
}?.firstOrNull()

return try {
val encoded = Json.encodeToJsonElement(objectValue?.value)
val encoded = Json.encodeToJsonElement(actualValue)
return Json.decodeFromJsonElement<T>(encoded)
} catch (e: Exception) {
null
Expand Down
56 changes: 44 additions & 12 deletions src/main/kotlin/com/featurevisor/sdk/Instance.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,18 @@ package com.featurevisor.sdk
import com.featurevisor.sdk.FeaturevisorError.MissingDatafileOptions
import com.featurevisor.types.*
import com.featurevisor.types.EventName.*
import kotlinx.coroutines.Job
import kotlinx.coroutines.*
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlin.coroutines.resume

typealias ConfigureBucketKey = (Feature, Context, BucketKey) -> BucketKey
typealias ConfigureBucketValue = (Feature, Context, BucketValue) -> BucketValue
typealias InterceptContext = (Context) -> Context
typealias DatafileFetchHandler = (datafileUrl: String) -> Result<DatafileContent>

var emptyDatafile = DatafileContent(
schemaVersion = "1",
schemaVersion = "1",
revision = "unknown",
attributes = emptyList(),
segments = emptyList(),
Expand Down Expand Up @@ -56,6 +57,8 @@ class FeaturevisorInstance private constructor(options: InstanceOptions) {
internal var configureBucketKey = options.configureBucketKey
internal var configureBucketValue = options.configureBucketValue
internal var refreshJob: Job? = null
private var fetchJob: Job? = null
internal val coroutineScope = CoroutineScope(Dispatchers.IO)

init {
with(options) {
Expand Down Expand Up @@ -99,16 +102,24 @@ class FeaturevisorInstance private constructor(options: InstanceOptions) {
}

datafileUrl != null -> {
datafileReader = DatafileReader(options.datafile?: emptyDatafile)
fetchDatafileContent(datafileUrl, handleDatafileFetch) { result ->
if (result.isSuccess) {
datafileReader = DatafileReader(result.getOrThrow())
statuses.ready = true
emitter.emit(READY, result.getOrThrow())
if (refreshInterval != null) startRefreshing()
} else {
logger?.error("Failed to fetch datafile: $result")
emitter.emit(ERROR)
if (::datafileReader.isInitialized.not()) {
datafileReader = DatafileReader(options.datafile ?: emptyDatafile)
}
fetchJob = coroutineScope.launch {
fetchDatafileContent(
url = datafileUrl,
handleDatafileFetch = handleDatafileFetch,
) { result ->
result.onSuccess { datafileContent ->
datafileReader = DatafileReader(datafileContent)
statuses.ready = true
emitter.emit(READY, datafileContent)
if (refreshInterval != null) startRefreshing()
}.onFailure { error ->
logger?.error("Failed to fetch datafile: $error")
emitter.emit(ERROR)
}
cancelFetchJob()
}
}
}
Expand All @@ -118,10 +129,31 @@ class FeaturevisorInstance private constructor(options: InstanceOptions) {
}
}

// Provide a mechanism to cancel the fetch job if retry count is more than one
private fun cancelFetchJob() {
fetchJob?.cancel()
fetchJob = null
}

fun setLogLevels(levels: List<Logger.LogLevel>) {
this.logger?.setLevels(levels)
}

suspend fun onReady(): FeaturevisorInstance {
return suspendCancellableCoroutine { continuation ->
if (this.statuses.ready) {
continuation.resume(this)
}

val cb :(result:Array<out Any>) -> Unit = {
this.emitter.removeListener(READY)
continuation.resume(this)
}

this.emitter.addListener(READY,cb)
}
}

fun setDatafile(datafileJSON: String) {
val data = datafileJSON.toByteArray(Charsets.UTF_8)
try {
Expand Down
13 changes: 13 additions & 0 deletions src/main/kotlin/com/featurevisor/testRunner/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.featurevisor.sdk.FeaturevisorInstance
import com.featurevisor.sdk.InstanceOptions
import com.featurevisor.sdk.emptyDatafile
import com.featurevisor.types.*
import com.featurevisor.types.VariableValue.*
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
Expand Down Expand Up @@ -177,6 +178,18 @@ fun getContextValues(contextValue: AttributeValue?) =
null -> null
}

fun getVariableValues(variableValue: VariableValue?) =
when (variableValue) {
is IntValue -> variableValue.value
is DoubleValue -> variableValue.value
is StringValue -> variableValue.value
is BooleanValue -> variableValue.value
is ArrayValue -> variableValue.values
is JsonValue -> variableValue.value
is ObjectValue -> variableValue.value
null -> null
}

fun checkIfArraysAreEqual(a: Array<Any>, b: Array<Any>): Boolean {
if (a.size != b.size) return false

Expand Down
Loading
Loading