zcash-android-wallet-sdk/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/ext/WalletService.kt

98 lines
3.6 KiB
Kotlin

package cash.z.ecc.android.sdk.internal.ext
import android.content.Context
import cash.z.ecc.android.sdk.ext.ZcashSdk.MAX_BACKOFF_INTERVAL
import cash.z.ecc.android.sdk.internal.Twig
import kotlinx.coroutines.delay
import java.io.File
import kotlin.random.Random
/**
* Execute the given block and if it fails, retry up to [retries] more times. If none of the
* retries succeed then throw the final error, which can be wrapped in order to add more context.
*
* @param retries the number of times to retry the block after the first attempt fails.
* @param exceptionWrapper a function that can wrap the final failure to add more useful information
* or context. Default behavior is to just return the final exception.
* @param initialDelayMillis the initial amount of time to wait before the first retry.
* @param block the code to execute, which will be wrapped in a try/catch and retried whenever an
* exception is thrown up to [retries] attempts.
*/
suspend inline fun retryUpTo(
retries: Int,
exceptionWrapper: (Throwable) -> Throwable = { it },
initialDelayMillis: Long = 500L,
block: (Int) -> Unit
) {
var failedAttempts = 0
while (failedAttempts <= retries) {
@Suppress("TooGenericExceptionCaught")
try {
block(failedAttempts)
return
} catch (t: Throwable) {
failedAttempts++
if (failedAttempts > retries) {
throw exceptionWrapper(t)
}
val duration = (initialDelayMillis.toDouble() * Math.pow(2.0, failedAttempts.toDouble() - 1)).toLong()
Twig.warn(t) { "Retrying ($failedAttempts/$retries) in ${duration}s..." }
delay(duration)
}
}
}
/**
* Execute the given block and if it fails, retry with an exponential backoff.
*
* @param onErrorListener a callback that gets the first shot at processing any error and can veto
* the retry behavior by returning false.
* @param initialDelayMillis the initial delay before retrying.
* @param maxDelayMillis the maximum delay between retries.
* @param block the logic to run once and then run again if it fails.
*/
@Suppress("MagicNumber")
suspend inline fun retryWithBackoff(
noinline onErrorListener: ((Throwable) -> Boolean)? = null,
initialDelayMillis: Long = 1000L,
maxDelayMillis: Long = MAX_BACKOFF_INTERVAL,
block: () -> Unit
) {
// count up to the max and then reset to half. So that we don't repeat the max but we also don't repeat too much.
var sequence = 0
while (true) {
@Suppress("TooGenericExceptionCaught")
try {
block()
return
} catch (t: Throwable) {
// offer to listener first
if (onErrorListener?.invoke(t) == false) {
throw t
}
sequence++
// initialDelay^(sequence/4) + jitter
var duration = Math.pow(
initialDelayMillis.toDouble(),
(sequence.toDouble() / 4.0)
).toLong() + Random.nextLong(1000L)
if (duration > maxDelayMillis) {
duration = maxDelayMillis - Random.nextLong(1000L) // include jitter but don't exceed max delay
sequence /= 2
}
Twig.warn(t) { "backing off and retrying in ${duration}ms..." }
delay(duration)
}
}
}
/**
* Return true if the given database already exists.
*
* @return true when the given database exists in the given context.
*/
internal fun dbExists(appContext: Context, dbFileName: String): Boolean {
return File(appContext.getDatabasePath(dbFileName).absolutePath).exists()
}