Skip to content

Commit debb698

Browse files
committed
Implement exponential backoff with retries for file download
1 parent d75dffb commit debb698

2 files changed

Lines changed: 96 additions & 17 deletions

File tree

data/src/main/kotlin/com/theapache64/stackzy/data/repo/PlayStoreRepo.kt

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import com.github.theapache64.gpa.api.Play
77
import com.github.theapache64.gpa.model.Account
88
import com.malinskiy.adam.request.pkg.Package
99
import com.theapache64.stackzy.data.local.AndroidApp
10+
import com.theapache64.stackzy.data.util.Result
1011
import com.theapache64.stackzy.data.util.bytesToMb
12+
import com.theapache64.stackzy.data.util.withExponentialBackOff
1113
import kotlinx.coroutines.Dispatchers
1214
import kotlinx.coroutines.flow.flow
1315
import kotlinx.coroutines.flow.flowOn
@@ -100,25 +102,32 @@ class PlayStoreRepo @Inject constructor() {
100102
val totalSize = downloadData.appSize
101103

102104
// Starting download
103-
downloadData.openApp().use { input ->
104-
FileOutputStream(apkFile).use { output ->
105-
val buffer = ByteArray(1024)
106-
var read: Int
107-
var counter = 0f
108-
while (input.read(buffer).also { read = it } != -1) {
109-
// Write
110-
output.write(buffer, 0, read)
105+
withExponentialBackOff(
106+
retryIf = { result, retryCount ->
107+
result is Result.Error && retryCount < 3
108+
},
109+
block = { retryCount, previousException ->
110+
downloadData.openApp().use { input ->
111+
FileOutputStream(apkFile).use { output ->
112+
val buffer = ByteArray(1024)
113+
var read: Int
114+
var counter = 0f
115+
while (input.read(buffer).also { read = it } != -1) {
116+
// Write
117+
output.write(buffer, 0, read)
111118

112-
// Update progress
113-
counter += read
114-
val percentage = (counter / totalSize) * 100
115-
emit(percentage.toInt())
116-
}
119+
// Update progress
120+
counter += read
121+
val percentage = (counter / totalSize) * 100
122+
emit(percentage.toInt())
123+
}
117124

118-
// Finish progress
119-
emit(100)
125+
// Finish progress
126+
emit(100)
127+
}
128+
}
120129
}
121-
}
130+
)
122131
}.flowOn(Dispatchers.IO)
123132

124-
}
133+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package com.theapache64.stackzy.data.util
2+
3+
import kotlinx.coroutines.delay
4+
import kotlin.math.pow
5+
import kotlin.math.roundToInt
6+
import kotlin.time.Duration.Companion.seconds
7+
import kotlin.time.ExperimentalTime
8+
9+
10+
/*
11+
Generic class which hold Success and Error Response
12+
*/
13+
sealed class Result<T> {
14+
data class Success<T>(val data: T) : Result<T>()
15+
data class Error<T>(val exception: Exception) : Result<T>()
16+
}
17+
18+
private const val DEFAULT_RETRY_LIMIT = 2
19+
private const val DEFAULT_BACKOFF_MULTIPLIER = 2
20+
private const val DEFAULT_BACKOFF_BASE = 2
21+
private const val DEFAULT_IS_LOG_ENABLED = false
22+
23+
24+
/**
25+
* Generic method to fire given [block] with exponential backOff based on given condition ([retryIf]).
26+
*
27+
* Exception from [block]:
28+
* if during retry, it'll be passed through [retryIf].
29+
* if in the last retry, propagated to call site.
30+
*/
31+
@OptIn(ExperimentalTime::class)
32+
suspend fun <T> withExponentialBackOff(
33+
retryLimit: Int = DEFAULT_RETRY_LIMIT,
34+
backoffMultiplier: Int = DEFAULT_BACKOFF_MULTIPLIER,
35+
backOffBase: Int = DEFAULT_BACKOFF_BASE,
36+
isLogEnabled: Boolean = DEFAULT_IS_LOG_ENABLED,
37+
retryIf: (retryResult: Result<T>, retryCount: Int) -> Boolean,
38+
block: suspend (retryCount: Int, previousException: Exception?) -> T
39+
): T {
40+
var retryCount = 0
41+
var result: Result<T>
42+
var previousException: Exception? = null
43+
do {
44+
if (retryCount != 0) {
45+
// API failed. backoff needed
46+
val temp = (backOffBase * backoffMultiplier.toDouble().pow(retryCount)) / 2
47+
val jitter = (0..temp.roundToInt()).random() // randomness to avoid parallel retry
48+
val sleep = (temp + jitter).toLong().seconds
49+
if (isLogEnabled) {
50+
println("Sleeping for $sleep (temp: $temp + jitter: $jitter) - Retry: $retryCount/$retryLimit")
51+
}
52+
delay(sleep)
53+
}
54+
// If there's any exception happened while executing the block, we'll pass it to retryIf block
55+
// to decide if we should retry
56+
result = try {
57+
Result.Success(block(retryCount, previousException))
58+
} catch (e: Exception) {
59+
previousException = e
60+
Result.Error(e)
61+
}
62+
retryCount++
63+
} while (retryIf(result, retryCount))
64+
65+
// final result
66+
return when (result) {
67+
is Result.Error -> throw result.exception
68+
is Result.Success -> result.data
69+
}
70+
}

0 commit comments

Comments
 (0)