Skip to content

Commit

Permalink
Twelve: Add retries to Api.kt
Browse files Browse the repository at this point in the history
May help with transient remote errors.

Also ensure the http calls happens in the IO
dispatcher.

Change-Id: I5bd86f6596edf2ecdce9ac9b1826d5d32d2fda02
  • Loading branch information
luca020400 committed Jan 17, 2025
1 parent f2d0e8b commit 8385ea7
Showing 1 changed file with 53 additions and 17 deletions.
70 changes: 53 additions & 17 deletions app/src/main/java/org/lineageos/twelve/utils/Api.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ package org.lineageos.twelve.utils

import android.net.Uri
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException
import kotlinx.serialization.encodeToString
Expand Down Expand Up @@ -90,6 +94,7 @@ class DeleteRequestInterface<T>(
class Api(
private val okHttpClient: OkHttpClient,
private val serverUri: Uri,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
val json: Json = Json {
ignoreUnknownKeys = true
},
Expand All @@ -110,27 +115,58 @@ class Api(
onEmptyResponse: () -> T = {
throw IllegalStateException("No onEmptyResponse() provided, but response is empty")
}
) = runCatching {
okHttpClient.newCall(request).executeAsync().let { response ->
if (response.isSuccessful) {
response.body?.use { body ->
val string = body.string()
if (string.isEmpty()) {
MethodResult.Success(onEmptyResponse())
) = withContext(dispatcher) {
withRetry(maxAttempts = 3) {
runCatching {
okHttpClient.newCall(request).executeAsync().let { response ->
if (response.isSuccessful) {
response.body?.use { body ->
val string = body.string()
if (string.isEmpty()) {
MethodResult.Success(onEmptyResponse())
} else {
@Suppress("Unchecked_Cast")
val serializer =
json.serializersModule.serializer(type) as KSerializer<T>
MethodResult.Success(json.decodeFromString(serializer, string))
}
} ?: MethodResult.Success(onEmptyResponse())
} else {
@Suppress("Unchecked_Cast")
val serializer = json.serializersModule.serializer(type) as KSerializer<T>
MethodResult.Success(json.decodeFromString(serializer, string))
MethodResult.HttpError(response.code, Throwable(response.message))
}
} ?: MethodResult.Success(onEmptyResponse())
} else {
MethodResult.HttpError(response.code, Throwable(response.message))
}
}.fold(
onSuccess = { it },
onFailure = { e -> handleError(e) }
)
}
}

private suspend fun <T> withRetry(
maxAttempts: Int,
initialDelay: Long = 100,
maxDelay: Long = 1000,
factor: Double = 2.0,
block: suspend () -> MethodResult<T>
): MethodResult<T> {
var currentDelay = initialDelay
repeat(maxAttempts - 1) { _ ->
when (val result = block()) {
is MethodResult.Success -> return result
is MethodResult.HttpError -> when (result.code) {
in 500..599 -> {
delay(currentDelay)
currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
}

else -> return result
}

else -> return result
}
}
}.fold(
onSuccess = { it },
onFailure = { e -> handleError(e) }
)
return block()
}

private fun <T> handleError(e: Throwable): MethodResult<T> = when (e) {
is SocketTimeoutException -> MethodResult.HttpError(408, e)
Expand Down

0 comments on commit 8385ea7

Please sign in to comment.