Skip to content

Commit

Permalink
revert Instance+Fetch.kt with main branch
Browse files Browse the repository at this point in the history
  • Loading branch information
Tan108 committed Sep 30, 2024
1 parent ca52f6b commit 5c5afe9
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 150 deletions.
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
173 changes: 48 additions & 125 deletions src/main/kotlin/com/featurevisor/sdk/Instance+Fetch.kt
Original file line number Diff line number Diff line change
@@ -1,99 +1,31 @@
package com.featurevisor.sdk

import com.featurevisor.types.DatafileContent
import com.featurevisor.types.DatafileFetchResult
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import java.io.IOException
import okhttp3.*
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import java.io.IOException
import java.net.ConnectException
import java.net.UnknownHostException
import java.lang.IllegalArgumentException

// MARK: - Fetch datafile content
@Throws(IOException::class)
internal fun fetchDatafileContentJob(
internal fun FeaturevisorInstance.fetchDatafileContent(
url: String,
logger: Logger?,
coroutineScope: CoroutineScope,
retryCount: Int = 3, // Retry count
retryInterval: Long = 300L, // Retry interval in milliseconds
handleDatafileFetch: DatafileFetchHandler? = null,
completion: (Result<DatafileFetchResult>) -> Unit,
): Job {
val job = Job()
coroutineScope.launch(job) {
fetchDatafileContent(
url = url,
handleDatafileFetch = handleDatafileFetch,
completion = completion,
retryCount = retryCount,
retryInterval = retryInterval,
job = job,
logger = logger,
)
}
return job
}

internal suspend fun fetchDatafileContent(
url: String,
logger: Logger? = null,
retryCount: Int = 1,
retryInterval: Long = 0L,
job: Job? = null,
handleDatafileFetch: DatafileFetchHandler? = null,
completion: (Result<DatafileFetchResult>) -> Unit,
completion: (Result<DatafileContent>) -> Unit,
) {
handleDatafileFetch?.let { handleFetch ->
for (attempt in 0 until retryCount) {
if (job != null && (job.isCancelled || job.isActive.not())) {
completion(Result.failure(FeaturevisorError.FetchingDataFileCancelled))
break
}

val result = handleFetch(url)
result.fold(
onSuccess = {
completion(Result.success(DatafileFetchResult(it, "")))
return
},
onFailure = { exception ->
if (attempt < retryCount - 1) {
logger?.error(exception.localizedMessage)
delay(retryInterval)
} else {
completion(Result.failure(exception))
}
}
)
}
val result = handleFetch(url)
completion(result)
} ?: run {
fetchDatafileContentFromUrl(
url = url,
completion = completion,
retryCount = retryCount,
retryInterval = retryInterval,
job = job,
logger = logger,
)
fetchDatafileContentFromUrl(url, completion)
}
}

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

private suspend fun fetchDatafileContentFromUrl(
private fun fetchDatafileContentFromUrl(
url: String,
logger: Logger?,
retryCount: Int,
retryInterval: Long,
job: Job?,
completion: (Result<DatafileFetchResult>) -> Unit,
completion: (Result<DatafileContent>) -> Unit,
) {
try {
val httpUrl = url.toHttpUrl()
Expand All @@ -102,66 +34,57 @@ private suspend fun fetchDatafileContentFromUrl(
.addHeader("Content-Type", "application/json")
.build()

fetchWithRetry(
request = request,
completion = completion,
retryCount = retryCount,
retryInterval = retryInterval,
job = job,
logger = logger,
)
fetch(request, completion)
} catch (throwable: IllegalArgumentException) {
completion(Result.failure(FeaturevisorError.InvalidUrl(url)))
} catch (e: Exception) {
logger?.error("Exception occurred during datafile fetch: ${e.message}")
completion(Result.failure(e))
}
}

private suspend fun fetchWithRetry(
const val BODY_BYTE_COUNT = 1000000L
val client = OkHttpClient()


private inline fun fetch(
request: Request,
logger: Logger?,
completion: (Result<DatafileFetchResult>) -> Unit,
retryCount: Int,
retryInterval: Long,
job: Job?
crossinline completion: (Result<DatafileContent>) -> Unit,
) {
for (attempt in 0 until retryCount) {
if (job != null && (job.isCancelled || job.isActive.not())) {
completion(Result.failure(FeaturevisorError.FetchingDataFileCancelled))
return
}

val call = client.newCall(request)
try {
val response = call.execute()
val call = client.newCall(request)
call.enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
val responseBody = response.peekBody(BODY_BYTE_COUNT)
val responseBodyString = responseBody.string()
if (response.isSuccessful) {
val json = Json { ignoreUnknownKeys = true }
val json = Json {
ignoreUnknownKeys = true
}
val responseBodyString = responseBody.string()
FeaturevisorInstance.companionLogger?.debug(responseBodyString)
val content = json.decodeFromString<DatafileContent>(responseBodyString)
completion(Result.success(DatafileFetchResult(content, responseBodyString)))
return
} else {
if (attempt < retryCount - 1) {
logger?.error("Request failed with message: ${response.message}")
delay(retryInterval)
} else {
completion(Result.failure(FeaturevisorError.UnparsableJson(responseBodyString, response.message)))
try {
val content = json.decodeFromString<DatafileContent>(responseBodyString)
completion(Result.success(content))
} catch (throwable: Throwable) {
completion(
Result.failure(
FeaturevisorError.UnparsableJson(
responseBody.string(),
response.message
)
)
)
}
}
} catch (e: IOException) {
val isInternetException = e is ConnectException || e is UnknownHostException
if (attempt >= retryCount - 1 || isInternetException) {
completion(Result.failure(e))
} else {
logger?.error("IOException occurred during request: ${e.message}")
delay(retryInterval)
completion(
Result.failure(
FeaturevisorError.UnparsableJson(
responseBody.string(),
response.message
)
)
)
}
} catch (e: Exception) {
logger?.error("Exception occurred during request: ${e.message}")
}

override fun onFailure(call: Call, e: IOException) {
completion(Result.failure(e))
}
}
})
}
7 changes: 3 additions & 4 deletions src/main/kotlin/com/featurevisor/sdk/Instance+Refresh.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ fun FeaturevisorInstance.startRefreshing() = when {
refresh()
delay(refreshInterval)
}
stopRefreshing()
}
}
}
Expand All @@ -32,7 +33,7 @@ fun FeaturevisorInstance.stopRefreshing() {
logger?.warn("refreshing has stopped")
}

private suspend fun FeaturevisorInstance.refresh() {
private fun FeaturevisorInstance.refresh() {
logger?.debug("refreshing datafile")
when {
statuses.refreshInProgress -> logger?.warn("refresh in progress, skipping")
Expand All @@ -43,9 +44,7 @@ private suspend fun FeaturevisorInstance.refresh() {
url = datafileUrl,
handleDatafileFetch = handleDatafileFetch,
) { result ->

result.onSuccess { fetchResult ->
val datafileContent = fetchResult.datafileContent
result.onSuccess { datafileContent ->
val currentRevision = getRevision()
val newRevision = datafileContent.revision
val isNotSameRevision = currentRevision != newRevision
Expand Down
38 changes: 18 additions & 20 deletions src/main/kotlin/com/featurevisor/sdk/Instance.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.featurevisor.types.EventName.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json

Expand Down Expand Up @@ -103,28 +104,25 @@ class FeaturevisorInstance private constructor(options: InstanceOptions) {
}

datafileUrl != null -> {
if(::datafileReader.isInitialized.not()) {
if (::datafileReader.isInitialized.not()) {
datafileReader = DatafileReader(options.datafile ?: emptyDatafile)
}
fetchJob = fetchDatafileContentJob(
url = datafileUrl,
logger = logger,
handleDatafileFetch = handleDatafileFetch,
retryCount = retryCount.coerceAtLeast(1),
retryInterval = retryInterval.coerceAtLeast(0),
coroutineScope = coroutineScope,
) { result ->
result.onSuccess { fetchResult ->
val datafileContent = fetchResult.datafileContent
datafileReader = DatafileReader(datafileContent)
statuses.ready = true
emitter.emit(READY, datafileContent, fetchResult.responseBodyString)
if (refreshInterval != null) startRefreshing()
}.onFailure { error ->
logger?.error("Failed to fetch datafile: $error")
emitter.emit(ERROR)
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()
}
cancelFetchRetry()
}
}

Expand All @@ -134,7 +132,7 @@ class FeaturevisorInstance private constructor(options: InstanceOptions) {
}

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

0 comments on commit 5c5afe9

Please sign in to comment.