Parallel Jobs, Tolerating Failure
Example: we’ve got several requests we need to make to another service. Each is for a task that is independent of the others, so even if one request fails we’d like the others to be tried. However, if one or more fail we’d like to log the failures and throw at least one of them so it’s reflected to the caller of our service and our logging system.
private val logger = KotlinLogging.logger {}
class CustomContext(val usefulId: String) : CoroutineContext.Element {
override val key: CoroutineContext.Key<*> = Key
companion object Key : CoroutineContext.Key<CustomContext>
}
suspend fun main() {
val ids = listOf("id1", "id2", "id3")
// CopyOnWriteArrayList is a thread-safe list implementation
val errors = CopyOnWriteArrayList<Pair<CustomContext?, Throwable>>()
val exceptionHandler = CoroutineExceptionHandler { context, throwable ->
errors.add(context[CustomContext] to throwable)
}
supervisorScope { // does not cancel children if one fails
ids.forEach { id ->
launch(coroutineContext + CustomContext(id) + exceptionHandler) {
callExternalService(id)
}
}
}
// log all errors and throw the first one
if (errors.isNotEmpty()) {
errors.forEach { (context, error) ->
logger.error(error) {
"Error while calling external service. Id = ${context?.usefulId}"
}
}
throw errors.first().let { (_, error) -> error }
}
}
suspend fun callExternalService(usefulId: String) {
// make some HTTP call, throwing an exception if it fails
}