Skip to content

Instantly share code, notes, and snippets.

@roschlau
Last active May 9, 2020 07:33
Show Gist options
  • Save roschlau/246e9ae7f2f8e4abb0576e4d8415c858 to your computer and use it in GitHub Desktop.
Save roschlau/246e9ae7f2f8e4abb0576e4d8415c858 to your computer and use it in GitHub Desktop.
Kotlin Coroutines implementation of "More Granular Retry" from https://www.techyourchance.com/concurrency-frameworks-overrated-android/
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import java.io.File
class UploadFilesUseCase : BaseBusyObservable<UploadFilesUseCase.Listener>() {
interface Listener {
fun onFilesUploaded()
fun onFilesUploadFailed()
}
companion object {
private const val MAX_RETRIES = 3
}
suspend fun uploadFiles() {
if (!isFreeAndBecomeBusy()) {
// log concurrent invocation attempt
return
}
uploadFilesSync()
}
private suspend fun uploadFilesSync() = withContext(Dispatchers.IO) {
val aJob = async { processAndMergeFilesOfTypeAWithRetry() }
val bJob = async { processAndMergeFilesOfTypeBWithRetry() }
val mergedA = aJob.await()
val mergedB = bJob.await()
if (mergedA == null || mergedB == null) {
flowFailed()
return@withContext
}
val archive: File = try {
compressMergedFiles(mergedA, mergedB)
} catch (e: OperationFailedException) {
flowFailed()
throw e
}
if (uploadFileToServerWithRetry(archive)) {
deleteTempDir()
notifySuccess()
} else {
flowFailed()
}
}
private suspend fun uploadFileToServerWithRetry(archive: File): Boolean {
for (i in 0 until MAX_RETRIES) {
val responseCode = uploadFileToServer(archive)
if (responseCode / 100 == 2) {
return true
}
}
return false
}
private suspend fun uploadFileToServer(archive: File): Int {
return suspendCancellableCoroutine { continuation ->
HttpManager.getInstance.uploadFiles(
archive,
object : HttpRequestListener() {
fun onDone(code: Int, body: ByteArray?) {
continuation.resumeWith(Result.success(code))
}
fun onFailure() {
continuation.resumeWith(Result.success(0))
}
}
)
}
}
private suspend fun flowFailed() {
deleteTempDir()
notifyFailure()
}
private fun processAndMergeFilesOfTypeAWithRetry(): File? {
for (i in 0 until MAX_RETRIES) {
try {
// ...
} catch (e: OperationFailedException) {
// log the exception
}
}
return null
}
private fun processAndMergeFilesOfTypeBWithRetry(): File? {
for (i in 0 until MAX_RETRIES) {
try {
// ...
} catch (e: OperationFailedException) {
// log the exception
}
}
return null
}
@Throws(OperationFailedException::class)
private fun compressMergedFiles(fileA: File?, fileB: File?): File { TODO() }
private fun deleteTempDir() { TODO() }
private suspend fun notifySuccess() = withContext(Dispatchers.Main) {
for (listener in listeners) {
listener.onFilesUploaded()
}
becomeNotBusy()
}
private suspend fun notifyFailure() = withContext(Dispatchers.Main) {
for (listener in listeners) {
listener.onFilesUploadFailed()
}
becomeNotBusy()
}
}
@techyourchance
Copy link

Also why not awaitAll instead of two individual awaits?

@multlurk
Copy link

multlurk commented May 8, 2020

How to get rid of BaseBusyObservable's uploadFilesSync and becomeNotBusy?

@roschlau
Copy link
Author

roschlau commented May 8, 2020

@techyourchance It's been quite some time since I actually worked on Android and have thus never worked with ViewModel, so I have no idea how that would integrate specifically 😅 So yeah, points taken, Making the coroutines an implementation detail certainly makes sense in this kind of scenario. I'm used to working in a backend service that's coroutines through and through, so I didn't really think about this.

About awaitAll vs individual awaits: Mostly because I wanted to leave the function signatures mostly intact, and having the async jobs in a list would have required to change the compressMergedFiles call, or work around it awkwardly. So, I just left it that way. But yeah, in a real scenario I would probably use that as well.

@roschlau
Copy link
Author

roschlau commented May 8, 2020

@multlurk

How to get rid of BaseBusyObservable's uploadFilesSync and becomeNotBusy?

I don't know the implementation details and requirements of the base class, so I just left these as they are. Would probably need to use a Mutex under the hood to play nice with coroutines.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment