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

fix: Improve remote evaluation fetch retry logic #17

Merged
merged 5 commits into from
Jan 29, 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
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ java {
dependencies {
implementation(kotlin("stdlib"))
testImplementation(kotlin("test"))
testImplementation("org.mockito:mockito-core:${Versions.mockito}")
testImplementation("io.mockk:mockk:${Versions.mockk}")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:${Versions.serializationRuntime}")
implementation("com.squareup.okhttp3:okhttp:${Versions.okhttp}")
implementation("com.amplitude:evaluation-core:${Versions.evaluationCore}")
Expand Down
2 changes: 1 addition & 1 deletion buildSrc/src/main/kotlin/Versions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ object Versions {
const val evaluationCore = "1.1.1"
const val evaluationSerialization = "1.1.1"
const val amplitudeAnalytics = "1.12.0"
const val mockito = "4.8.1"
const val mockk = "1.13.9"
}
12 changes: 10 additions & 2 deletions src/main/kotlin/RemoteEvaluationClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.amplitude.experiment

import com.amplitude.experiment.evaluation.serialization.SerialVariant
import com.amplitude.experiment.util.BackoffConfig
import com.amplitude.experiment.util.FetchException
import com.amplitude.experiment.util.Logger
import com.amplitude.experiment.util.backoff
import com.amplitude.experiment.util.toSerialExperimentUser
Expand Down Expand Up @@ -44,7 +45,7 @@ class RemoteEvaluationClient internal constructor(
fun fetch(user: ExperimentUser): CompletableFuture<Map<String, Variant>> {
return doFetch(user, config.fetchTimeoutMillis).handle { variants, t ->
if (t != null || variants == null) {
if (retry) {
if (retry && shouldRetryFetch(t)) {
backoff(backoffConfig) {
doFetch(user, config.fetchTimeoutMillis)
}
Expand Down Expand Up @@ -90,7 +91,7 @@ class RemoteEvaluationClient internal constructor(
Logger.d("Received fetch response: $response")
val variants = response.use {
if (!response.isSuccessful) {
throw IOException("fetch error response: $response")
throw FetchException(response.code, "fetch error response: $response")
}
parseRemoteResponse(response.body?.string() ?: "")
}
Expand All @@ -112,3 +113,10 @@ internal fun parseRemoteResponse(jsonString: String): Map<String, Variant> =
json.decodeFromString<HashMap<String, SerialVariant>>(
jsonString
).mapValues { it.value.toVariant() }

private fun shouldRetryFetch(t: Throwable): Boolean {
if (t is FetchException) {
return t.statusCode < 400 || t.statusCode >= 500 || t.statusCode == 429
}
return true
}
8 changes: 8 additions & 0 deletions src/main/kotlin/util/FetchException.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.amplitude.experiment.util

import okio.IOException

internal class FetchException (
val statusCode: Int,
message: String
) : IOException(message)
40 changes: 40 additions & 0 deletions src/test/kotlin/RemoteEvaluationClientTest.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package com.amplitude.experiment

import com.amplitude.experiment.util.FetchException
import com.amplitude.experiment.util.Logger
import com.amplitude.experiment.util.SystemLogger
import io.mockk.every
import io.mockk.spyk
import io.mockk.verify
import org.junit.Assert
import java.util.Date
import java.util.concurrent.CancellationException
Expand Down Expand Up @@ -84,6 +88,7 @@ class RemoteEvaluationClientTest {
}
}


// @Test
// fun test() {
// val client = RemoteEvaluationClient(API_KEY, RemoteEvaluationConfig())
Expand All @@ -100,6 +105,41 @@ class RemoteEvaluationClientTest {
// }
// }
// }

@Test
fun `fetch retry with different response codes`() {
// Response code, error message, and whether retry should be called
val testData = listOf(
Triple(300, "Fetch Exception 300", 2),
Triple(400, "Fetch Exception 400", 1),
Triple(429, "Fetch Exception 429", 2),
Triple(500, "Fetch Exception 500", 2),
Triple(0, "Other Exception", 2)
)

testData.forEach { (responseCode, errorMessage, fetchCalls) ->
val config = RemoteEvaluationConfig(fetchRetries = 1, debug = true)
val client = spyk(RemoteEvaluationClient("apiKey", config), recordPrivateCalls = true)
// Mock the private method to throw FetchException or other exceptions
every { client["doFetch"](any<ExperimentUser>(), any<Long>()) } answers {
val future = CompletableFuture<Map<String, Variant>>()
if (responseCode == 0) {
future.completeExceptionally(Exception(errorMessage))
} else {
future.completeExceptionally(FetchException(responseCode, errorMessage))
}
future
}

try {
client.fetch(ExperimentUser("test_user")).get()
} catch (t: Throwable) {
// catch exception
}

verify(exactly = fetchCalls) { client["doFetch"](any<ExperimentUser>(), any<Long>()) }
}
}
}

@Suppress("SameParameterValue")
Expand Down
Loading