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

techyourchance commented May 6, 2020

I think in this case it's better to use GlobalScope and make uploadFiles() non-suspending. Two reasons:

  1. Top-level suspending functions are awkward and error-prone when multiple clients wait for notification. Having async notifications after suspending function returns goes against structured concurrency paradigm.
  2. If someone would accidentally use this use case from e.g. ViewModel, it would be cancelled automatically, which would constitute a major bug.

As I said, in this case, coroutines should become internal implementation details rather than be exposed at external API.

@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