How to Exponential Backoff retry on kotlin coroutines - android

I am using kotlin coroutines for network request using extension method to call class in retrofit like this
public suspend fun <T : Any> Call<T>.await(): T {
return suspendCancellableCoroutine { continuation ->
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>?, response: Response<T?>) {
if (response.isSuccessful) {
val body = response.body()
if (body == null) {
continuation.resumeWithException(
NullPointerException("Response body is null")
)
} else {
continuation.resume(body)
}
} else {
continuation.resumeWithException(HttpException(response))
}
}
override fun onFailure(call: Call<T>, t: Throwable) {
// Don't bother with resuming the continuation if it is already cancelled.
if (continuation.isCancelled) return
continuation.resumeWithException(t)
}
})
registerOnCompletion(continuation)
}
}
then from calling side i am using above method like this
private fun getArticles() = launch(UI) {
loading.value = true
try {
val networkResult = api.getArticle().await()
articles.value = networkResult
}catch (e: Throwable){
e.printStackTrace()
message.value = e.message
}finally {
loading.value = false
}
}
i want to exponential retry this api call in some case i.e (IOException) how can i achieve it ??

I would suggest to write a helper higher-order function for your retry logic. You can use the following implementation for a start:
suspend fun <T> retryIO(
times: Int = Int.MAX_VALUE,
initialDelay: Long = 100, // 0.1 second
maxDelay: Long = 1000, // 1 second
factor: Double = 2.0,
block: suspend () -> T): T
{
var currentDelay = initialDelay
repeat(times - 1) {
try {
return block()
} catch (e: IOException) {
// you can log an error here and/or make a more finer-grained
// analysis of the cause to see if retry is needed
}
delay(currentDelay)
currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
}
return block() // last attempt
}
Using this function is very strightforward:
val networkResult = retryIO { api.getArticle().await() }
You can change retry parameters on case-by-case basis, for example:
val networkResult = retryIO(times = 3) { api.doSomething().await() }
You can also completely change the implementation of retryIO to suit the needs of your application. For example, you can hard-code all the retry parameters, get rid of the limit on the number of retries, change defaults, etc.

Here an example with the Flow and the retryWhen function
RetryWhen Extension :
fun <T> Flow<T>.retryWhen(
#FloatRange(from = 0.0) initialDelay: Float = RETRY_INITIAL_DELAY,
#FloatRange(from = 1.0) retryFactor: Float = RETRY_FACTOR_DELAY,
predicate: suspend FlowCollector<T>.(cause: Throwable, attempt: Long, delay: Long) -> Boolean
): Flow<T> = this.retryWhen { cause, attempt ->
val retryDelay = initialDelay * retryFactor.pow(attempt.toFloat())
predicate(cause, attempt, retryDelay.toLong())
}
Usage :
flow {
...
}.retryWhen { cause, attempt, delay ->
delay(delay)
...
}

Here's a more sophisticated and convenient version of my previous answer, hope it helps someone:
class RetryOperation internal constructor(
private val retries: Int,
private val initialIntervalMilli: Long = 1000,
private val retryStrategy: RetryStrategy = RetryStrategy.LINEAR,
private val retry: suspend RetryOperation.() -> Unit
) {
var tryNumber: Int = 0
internal set
suspend fun operationFailed() {
tryNumber++
if (tryNumber < retries) {
delay(calculateDelay(tryNumber, initialIntervalMilli, retryStrategy))
retry.invoke(this)
}
}
}
enum class RetryStrategy {
CONSTANT, LINEAR, EXPONENTIAL
}
suspend fun retryOperation(
retries: Int = 100,
initialDelay: Long = 0,
initialIntervalMilli: Long = 1000,
retryStrategy: RetryStrategy = RetryStrategy.LINEAR,
operation: suspend RetryOperation.() -> Unit
) {
val retryOperation = RetryOperation(
retries,
initialIntervalMilli,
retryStrategy,
operation,
)
delay(initialDelay)
operation.invoke(retryOperation)
}
internal fun calculateDelay(tryNumber: Int, initialIntervalMilli: Long, retryStrategy: RetryStrategy): Long {
return when (retryStrategy) {
RetryStrategy.CONSTANT -> initialIntervalMilli
RetryStrategy.LINEAR -> initialIntervalMilli * tryNumber
RetryStrategy.EXPONENTIAL -> 2.0.pow(tryNumber).toLong()
}
}
Usage:
coroutineScope.launch {
retryOperation(3) {
if (!tryStuff()) {
Log.d(TAG, "Try number $tryNumber")
operationFailed()
}
}
}

Flow Version https://github.com/hoc081098/FlowExt
package com.hoc081098.flowext
import kotlin.time.Duration
import kotlin.time.ExperimentalTime
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.retryWhen
#ExperimentalTime
public fun <T> Flow<T>.retryWithExponentialBackoff(
initialDelay: Duration,
factor: Double,
maxAttempt: Long = Long.MAX_VALUE,
maxDelay: Duration = Duration.INFINITE,
predicate: suspend (cause: Throwable) -> Boolean = { true }
): Flow<T> {
require(maxAttempt > 0) { "Expected positive amount of maxAttempt, but had $maxAttempt" }
return retryWhenWithExponentialBackoff(
initialDelay = initialDelay,
factor = factor,
maxDelay = maxDelay
) { cause, attempt -> attempt < maxAttempt && predicate(cause) }
}
#ExperimentalTime
public fun <T> Flow<T>.retryWhenWithExponentialBackoff(
initialDelay: Duration,
factor: Double,
maxDelay: Duration = Duration.INFINITE,
predicate: suspend FlowCollector<T>.(cause: Throwable, attempt: Long) -> Boolean
): Flow<T> = flow {
var currentDelay = initialDelay
retryWhen { cause, attempt ->
predicate(cause, attempt).also {
if (it) {
delay(currentDelay)
currentDelay = (currentDelay * factor).coerceAtMost(maxDelay)
}
}
}.let { emitAll(it) }
}

You can try this simple but very agile approach with simple usage:
EDIT: added a more sophisticated solution in a separate answer.
class Completion(private val retry: (Completion) -> Unit) {
fun operationFailed() {
retry.invoke(this)
}
}
fun retryOperation(retries: Int,
dispatcher: CoroutineDispatcher = Dispatchers.Default,
operation: Completion.() -> Unit
) {
var tryNumber = 0
val completion = Completion {
tryNumber++
if (tryNumber < retries) {
GlobalScope.launch(dispatcher) {
delay(TimeUnit.SECONDS.toMillis(tryNumber.toLong()))
operation.invoke(it)
}
}
}
operation.invoke(completion)
}
The use it like this:
retryOperation(3) {
if (!tryStuff()) {
// this will trigger a retry after tryNumber seconds
operationFailed()
}
}
You can obviously build more on top of it.

Related

Unit testing network bound resource

I'm aware that there are a couple of topics on this but none of them solve my issue.
I'm trying to test an implementation of NetworkBoundResource.
inline fun <ResultType, RequestType, ErrorType> networkBoundResource(
crossinline query: () -> Flow<ResultType>,
crossinline fetch: suspend () -> Response<RequestType>,
crossinline saveFetchResult: suspend (RequestType) -> Unit,
crossinline onFetchFailed: (Response<*>?, Throwable?) -> ErrorType? = { _, _ -> null },
crossinline shouldFetch: (ResultType) -> Boolean = { true },
coroutineDispatcher: CoroutineDispatcher
) = flow<Resource<ResultType, ErrorType>> {
val data = query().first()
emit(Resource.Success(data))
if (shouldFetch(data)) {
val fetchResponse = safeApiCall { fetch() }
val fetchBody = fetchResponse.body()
if (fetchBody != null) {
saveFetchResult(fetchBody)
}
if (!fetchResponse.isSuccessful) {
emit(Resource.Error(onFetchFailed(fetchResponse, null)))
} else {
query().map { emit(Resource.Success(it)) }
}
}
}.catch { throwable ->
emit(Resource.Error(onFetchFailed(null, throwable)))
}.flowOn(coroutineDispatcher)
This works as expected in my use case in production code.
override suspend fun getCategories() = networkBoundResource(
query = {
categoryDao.getAllAsFlow().map { categoryMapper.categoryListFromDataObjectList(it) }
},
fetch = {
categoryServices.getCategories()
},
onFetchFailed = { errorResponse, _ ->
categoryMapper.toError(errorResponse)
},
saveFetchResult = { response ->
// Clear the old items and add the new ones
categoryDao.clearAll()
categoryDao.insertAll(categoryMapper.toDataObjectList(response.data))
},
coroutineDispatcher = dispatchProvider.IO
)
I have my test setup like this (using turbine for flow testing).
#OptIn(ExperimentalCoroutinesApi::class)
class NetworkBoundResourceTests {
data class ResultType(val data: String)
sealed class RequestType {
object Default : RequestType()
}
sealed class ErrorType {
object Default : RequestType()
}
private val dispatchProvider = TestDispatchProviderImpl()
#Test
fun `Test`() = runTest {
val resource = networkBoundResource(
query = { flowOf(ResultType(data = "")) },
fetch = { Response.success(RequestType.Default) },
saveFetchResult = { },
onFetchFailed = { _, _ -> ErrorType.Default },
coroutineDispatcher = dispatchProvider.IO
)
resource.test {
}
}
}
The coroutine dispatcher is set to unconfined through DI/Test dispatcher.
I want to test that;
Emitting first data from query, then query is updated and new data from saveFetchResult then query().map { emit(Resource.Success(it)) } emits the updated data from that save result.
I think I need to do something around a spyk on my flow with MockK but I can't seem to figure it out. query() will always return the same flow of data as it's mocked to do so if I awaitItem() again it returns the same data (as it should) as that's what the mock is setup for.
I've found a way to test this. Not exactly how I imagined it in my head.
#Test
fun `Given should fetch is true and fetch throws exception, When retrieving data, Then cached items emitted and error item after`() =
runTest {
val saveFetchResultAction = mockk<(() -> Unit)>("Save results action")
val fetchErrorAction = mockk<(() -> ErrorType)>("Fetch error action")
every { fetchErrorAction() } answers { ErrorType }
val fetchRequestAction = mockk<(() -> Response<RequestType>)>("Fetch request action")
coEvery { fetchRequestAction() } throws (Exception(""))
networkBoundResource(
query = { flowOf(ResultType) },
fetch = { fetchRequestAction() },
saveFetchResult = { saveFetchResultAction() },
onFetchFailed = { _, _ -> fetchErrorAction() },
shouldFetch = { true },
coroutineDispatcher = dispatchProvider.IO
).test {
// Assert that we've got the cached item
val cacheItem = awaitItem()
assertThat(cacheItem).isInstanceOf(Resource.Success::class.java)
val errorItem = awaitItem()
assertThat(errorItem).isInstanceOf(Resource.Error::class.java)
awaitComplete()
// Verify order & calls
verifyOrder {
fetchRequestAction()
fetchErrorAction()
}
verify(exactly = 1) { fetchErrorAction() }
verify(exactly = 1) { fetchRequestAction() }
verify(exactly = 0) { saveFetchResultAction() }
}
}

Call parallel multiple the api request in the clean architecture

I have 5 api call function in the viewModel that I want to called parallel how can I do this? I put each of the function in the WithContext(Dispachers.IO) but it's not working. I used coroutines flow for calling api.
Note: I used clean architecture pattern and I have single use-case
ViewModel codes:
class MyJobsViewModel constructor(
private val myJobsUseCases: MyJobsUseCases,
private val clientNavigator: ClientNavigator
) : ViewModel(), ClientNavigator by clientNavigator {
private val _state = mutableStateOf(MyJobsState())
val state: State<MyJobsState> get() = _state
private fun getAllJobs(
offset: Int = 0,
limit: Int = 10,
type: JobTypeEnum = JobTypeEnum.ALL
) {
myJobsUseCases.getJobsUseCase.invoke(offset = offset, limit = limit, type = type)
.onEach {
when (it) {
is Resource.Success -> _state.value =
state.value.copy(
isLoading = false,
allJobItems = it.data ?: JobItemsResponse()
)
is Resource.Error -> _state.value =
state.value.copy(
isLoading = false,
error = it.message ?: "An unexpected error occurred"
)
is Resource.Loading -> _state.value = state.value.copy(isLoading = true)
}
}.launchIn(viewModelScope)
}
private fun getActiveJobs(
offset: Int = 0,
limit: Int = 10,
type: JobTypeEnum = JobTypeEnum.ALL
) {
myJobsUseCases.getJobsUseCase.invoke(offset = offset, limit = limit, type = type)
.onEach {
when (it) {
is Resource.Success -> _state.value =
state.value.copy(
isLoading = false,
activeJobItems = it.data ?: JobItemsResponse()
)
is Resource.Error -> _state.value =
state.value.copy(
isLoading = false,
error = it.message ?: "An unexpected error occurred"
)
is Resource.Loading -> _state.value = state.value.copy(isLoading = true)
}
}.launchIn(viewModelScope)
}
}
The best way to parallel multiple calls is to follow structured concurrency principle.
For example, when you need to evaluate the result of 5 independent network requests:
suspend fun fetchA(): Int { /* ... */ }
suspend fun fetchB(): Int { /* ... */ }
suspend fun fetchC(): Int { /* ... */ }
suspend fun fetchD(): Int { /* ... */ }
suspend fun fetchE(): Int { /* ... */ }
suspend fun overallResult(): Int = coroutineScope {
val a = async { fetchA() }
val b = async { fetchB() }
val c = async { fetchC() }
val d = async { fetchD() }
val e = async { fetchE() }
a.await() + b.await() + c.await() + d.await() + e.await()
}
Or when you need to make 5 independent api calls without returning any value:
suspend fun callA() { /* ... */ }
suspend fun callB() { /* ... */ }
suspend fun callC() { /* ... */ }
suspend fun callD() { /* ... */ }
suspend fun callE() { /* ... */ }
suspend fun makeCalls(): Unit = coroutineScope {
launch { callA() }
launch { callB() }
launch { callC() }
launch { callD() }
launch { callE() }
}
Wrapping function in launch or async block produces a new coroutine and executes in parallel.
coroutineScope organizes the area where launch and async can be used. It completes only when every child coroutine is completed.

How to call a coroutine usecase from a rxjava flat map

Hi I have a rxjava flat map in which I want to call a coroutine usecase onStandUseCase which is an api call
Initially the use case was also rxjava based and it used to return Observable<GenericResponse> and it worked fine
now that I changed the use to be coroutines based it only returns GenericResponse
how can modify the flatmap to work fine with coroutines use case please
subscriptions += view.startFuellingObservable
.onBackpressureLatest()
.doOnNext { view.showLoader(false) }
.flatMap {
if (!hasOpenInopIncidents()) {
//THIS IS WHERE THE ERROR IS IT RETURNS GENERICRESPONSE
onStandUseCase(OnStandUseCase.Params("1", "2", TimestampedAction("1", "2", DateTime.now()))) {
}
} else {
val incidentOpenResponse = GenericResponse(false)
incidentOpenResponse.error = OPEN_INCIDENTS
Observable.just(incidentOpenResponse)
}
}
.subscribe(
{ handleStartFuellingClicked(view, it) },
{ onStartFuellingError(view) }
)
OnStandUseCase.kt
class OnStandUseCase #Inject constructor(
private val orderRepository: OrderRepository,
private val serviceOrderTypeProvider: ServiceOrderTypeProvider
) : UseCaseCoroutine<GenericResponse, OnStandUseCase.Params>() {
override suspend fun run(params: Params) = orderRepository.notifyOnStand(
serviceOrderTypeProvider.apiPathFor(params.serviceType),
params.id,
params.action
)
data class Params(val serviceType: String, val id: String, val action: TimestampedAction)
}
UseCaseCoroutine
abstract class UseCaseCoroutine<out Type, in Params> where Type : Any {
abstract suspend fun run(params: Params): Type
operator fun invoke(params: Params, onResult: (type: Type) -> Unit = {}) {
val job = GlobalScope.async(Dispatchers.IO) { run(params) }
GlobalScope.launch(Dispatchers.Main) { onResult(job.await()) }
}
}
startFuellingObservable is
val startFuellingObservable: Observable<Void>
Here is the image of the error
Any suggestion on how to fix this please
thanks in advance
R
There is the integration library linking RxJava and Kotlin coroutines.
rxSingle can be used to turn a suspend function into a Single. OP wants an Observable, so we can call toObservable() for the conversion.
.flatMap {
if (!hasOpenInopIncidents()) {
rxSingle {
callYourSuspendFunction()
}.toObservable()
} else {
val incidentOpenResponse = GenericResponse(false)
incidentOpenResponse.error = OPEN_INCIDENTS
Observable.just(incidentOpenResponse)
}
}
Note that the Observables in both branches contain just one element. We can make this fact more obvious by using Observable#concatMapSingle.
.concatMapSingle {
if (!hasOpenInopIncidents()) {
rxSingle { callYourSuspendFunction() }
} else {
val incidentOpenResponse = GenericResponse(false)
incidentOpenResponse.error = OPEN_INCIDENTS
Single.just(incidentOpenResponse)
}
}

How to download an .apk with ktor library and kotlin?

I'm using the Ktor library (io.ktor:ktor-client-android:1.2.5) for downloading files.
When I download an image everything is fine, but when I want to download an .apk, it doesn't work.
I'm using ktor 1.2.5.
This is the function that I use:
suspend fun HttpClient.downloadFile(file: OutputStream, url: String): Flow<DownloadResult> {
return flow {
try {
val response = call {
url(url)
method = HttpMethod.Get
}.response
val data = ByteArray(response.contentLength()!!.toInt())
var offset = 0
do {
val currentRead = response.content.readAvailable(data, offset, data.size)
offset += currentRead
val progress = (offset * 100f / data.size).roundToInt()
emit(DownloadResult.Progress(progress))
} while (currentRead > 0)
response.close()
if (response.status.isSuccess()) {
withContext(Dispatchers.IO) {
file.write(data)
//file.write()
}
emit(DownloadResult.Success)
} else {
emit(DownloadResult.Error("File not downloaded"))
}
} catch (e: TimeoutCancellationException) {
emit(DownloadResult.Error("Connection timed out", e))
} catch (t: Throwable) {
emit(DownloadResult.Error("Failed to connect. ${t.message}"))
}
}
}
And I use it like tthis:
private fun downloadFile(view: View, context: Context, url: String, file: Uri) {
val ktor = HttpClient(Android)
//viewModel.setDownloading(true)
context.contentResolver.openOutputStream(file)?.let { outputStream ->
CoroutineScope(Dispatchers.IO).launch {
ktor.downloadFile(outputStream, url).collect {
withContext(Dispatchers.Main) {
when (it) {
is DownloadResult.Success -> {
//viewModel.setDownloading(false)
view.custom_des_pb_progreso_descarga.progress = 0
Toast.makeText(context, "Descarga Completa", Toast.LENGTH_LONG).show()
dismiss()
viewFile(file, context)
}
is DownloadResult.Error -> {
Toast.makeText(context, "Error al descargar actualizaciĆ³n. ${it.message} ${it.cause} ", Toast.LENGTH_LONG).show()
dismiss()
}
is DownloadResult.Progress -> {
view.custom_des_pb_progreso_descarga.progress = it.progress
view.custom_des_pb_conteo_descarga.text = "${it.progress}%"
}
}
}
}
}
}
}
Anyone know what might be happening?
I recommend starting with the simplest possible code for downloading files and then add more complexity (flow and DownloadResult) while checking every step of the way. Here is the working code that just downloads a file:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val client = HttpClient(Android)
runBlocking {
client.downloadFile(
"https://search.maven.org/remotecontent?filepath=de/sweetcode/e/1.7.2-alpha/e-1.7.2-alpha.jar",
applicationContext.openFileOutput("result.jar", MODE_PRIVATE)
)
}
}
}
suspend fun HttpClient.downloadFile(url: String, output: OutputStream) {
get<HttpStatement>(url).execute { response ->
response.content.copyTo(output)
}
}

Android: How to detect how long workmanager is already in enqueue mode?

I want to detect, how long a specific work is already in enqueue mode. I need this information, in order to inform the user about his state (e.g when workmanager is longer than 10 seconds in enqueue mode -> cancel work -> inform user that he needs to do X in order to achieve Y). Something like this:
Pseudo Code
workInfo.observe(viewLifecylceOwner) {
when(it.state) {
WorkInfo.State.ENQUEUED -> if(state.enqueue.time > 10) cancelWork()
}
}
I didn't find anything about this anywhere. Is this possible?
I appreciate every help.
I have managed to create a somewhat robust "Workmanager watcher". My intention was the following: When the Workmanager is not finished within 7 seconds, tell the user that an error occurred. The Workmanager itself will never be cancelled, furthermore my function is not even interacting with the Workmanager itself. This works in 99% of all cases:
Workerhelper
object WorkerHelper {
private var timeStamp by Delegates.notNull<Long>()
private var running = false
private var manuallyStopped = false
private var finished = false
open val maxTime: Long = 7000000000L
// Push the current timestamp, set running to true
override fun start() {
timeStamp = System.nanoTime()
running = true
manuallyStopped = false
finished = false
Timber.d("Mediator started")
}
// Manually stop the WorkerHelper (e.g when Status is Status.Success)
override fun stop() {
if (!running) return else {
running = false
manuallyStopped = true
finished = true
Timber.d("Mediator stopped")
}
}
override fun observeMaxTimeReachedAndCancel(): Flow<Boolean> = flow {
try {
coroutineScope {
// Check if maxTime is not passed with => (System.nanoTime() - timeStamp) <= maxTime
while (running && !finished && !manuallyStopped && (System.nanoTime() - timeStamp) <= maxTime) {
emit(false)
}
// This will be executed only when the Worker is running longer than maxTime
if (!manuallyStopped || !finished) {
emit(true)
running = false
finished = true
this#coroutineScope.cancel()
} else if (finished) {
this#coroutineScope.cancel()
}
}
} catch (e: CancellationException) {
}
}.flowOn(Dispatchers.IO)
Then in my Workmanager.enqueueWork function:
fun startDownloadDocumentWork() {
WorkManager.getInstance(context)
.enqueueUniqueWork("Download Document List", ExistingWorkPolicy.REPLACE, downloadDocumentListWork)
pushNotification()
}
private fun pushNotification() {
WorkerHelper.start()
}
And finally in my ViewModel
private fun observeDocumentList() = viewModelScope.launch {
observerWorkerState(documentListWorkInfo).collect {
when(it) {
is Status.Loading -> {
_documentDataState.postValue(Status.loading())
// Launch another Coroutine, otherwise current viewmodelscrope will be blocked
CoroutineScope(Dispatchers.IO).launch {
WorkerHelper.observeMaxTimeReached().collect { lostConnection ->
if (lostConnection) {
_documentDataState.postValue(Status.failed("Internet verbindung nicht da"))
}
}
}
}
is Status.Success -> {
WorkerHelper.finishWorkManually()
_documentDataState.postValue(Status.success(getDocumentList()))
}
is Status.Failure -> {
WorkerHelper.finishWorkManually()
_documentDataState.postValue(Status.failed(it.message.toString()))
}
}
}
}
I've also created a function that converts the Status of my workmanager to my custom status class:
Status
sealed class Status<out T> {
data class Success<out T>(val data: T) : Status<T>()
class Loading<T> : Status<T>()
data class Failure<out T>(val message: String?) : Status<T>()
companion object {
fun <T> success(data: T) = Success<T>(data)
fun <T> loading() = Loading<T>()
fun <T> failed(message: String?) = Failure<T>(message)
}
}
Function
suspend inline fun observerWorkerState(workInfoFlow: Flow<WorkInfo>): Flow<Status<Unit>> = flow {
workInfoFlow.collect {
when (it.state) {
WorkInfo.State.ENQUEUED -> emit(Status.loading<Unit>())
WorkInfo.State.RUNNING -> emit(Status.loading<Unit>())
WorkInfo.State.SUCCEEDED -> emit(Status.success(Unit))
WorkInfo.State.BLOCKED -> emit(Status.failed<Unit>("Workmanager blocked"))
WorkInfo.State.FAILED -> emit(Status.failed<Unit>("Workmanager failed"))
WorkInfo.State.CANCELLED -> emit(Status.failed<Unit>("Workmanager cancelled"))
}
}
}

Categories

Resources