Сбор нескольких асинхронных результатов в сопрограммах Kotlin, игнорирующих исключения с тайм-аутами

#kotlin #kotlin-coroutines

#kotlin #kotlin-сопрограммы

Вопрос:

У меня есть Generator класс, который, по сути, генерирует некоторые данные, например:

 interface Generator {
    suspend fun generate(): String?
}
 

Существует несколько реализаций. Некоторые из них могут вызывать исключения, а некоторым из них может потребоваться слишком много времени для генерации данных:

 class Faulty : Generator {
    override suspend fun generate(): String? {
        println("Faulty")
        throw IllegalArgumentException();
    }
}

class Lingering : Generator {
    override suspend fun generate(): String? {
        println("Lingering")
        delay(Duration.ofHours(1))
        return null
    }
}
 

Но некоторые реализации достойны

 class Good : Generator {
    override suspend fun generate(): String {
        println("Good")
        return "Goooood"
    }
}
 

Что мне нужно сделать, так это собрать данные, сгенерированные списком предварительно настроенных генераторов, предоставив каждому из них время ожидания для its generate и игнорируя исключения (но регистрируя их):

 fun main() = runBlocking {
    val generators = listOf(Faulty(), Lingering(), Good())
    val results = supervisorScope {
        generators
                .map { generator ->
                    async(CoroutineExceptionHandler { context, exception ->
                        println(exception)
                    }) {
                        withTimeoutOrNull(5000) {
                            generator.generate()
                        }
                    }
                }
                .awaitAll()
                .filterNotNull()
    }

    println(results)
}
 

Проблема в том, что этот код завершается ошибкой с исключением:

 Faulty
Lingering
Good
Exception in thread "main" java.lang.IllegalArgumentException
 at Faulty.generate (File.kt:12) 
 at FileKt$main$1$results$1$1$2$1.invokeSuspend (File.kt:41) 
 at FileKt$main$1$results$1$1$2$1.invoke (File.kt:-1) 
 

Почему это не supervisorScope улавливается? Что я делаю не так?

Ответ №1:

Из документации CoroutineExceptionHandler:

Необязательный элемент в контексте сопрограммы для обработки неперехваченных исключений.

и

Сопрограмма, созданная с использованием async , всегда улавливает все свои исключения и представляет их в результирующем отложенном объекте, поэтому она не может привести к неперехваченным исключениям.

из этого следует, что ваша async работа не выдает неперехваченное исключение. Исключение повторно заполняется awaitAll() вызовом, который выполняется позже. Вы поместили обработчик неперехваченных исключений только в свой async контекст, поэтому он не будет использоваться.

Кроме того, дочерние сопрограммы в любом случае не генерируют неперехваченные исключения. Их исключения делегируются их корневому предку.

Как объяснялось здесь в последнем разделе, озаглавленном Исключения в контролируемых сопрограммах, дочерние элементы области супервизора должны иметь корневую сопрограмму, использующую обработчик.

Что вы можете сделать, так это обернуть всю задачу в launch блок, который использует обработчик. По какой-то причине установка обработчика не работает runBlocking . Может быть, это не считается заданием root?

 fun main() = runBlocking{
    val job = GlobalScope.launch(CoroutineExceptionHandler { context, exception ->
        println(exception)
    }) {
        val generators = listOf(Faulty(), Lingering(), Good())
        val results =
            supervisorScope {
                generators
                    .map { generator ->
                        async {
                            withTimeoutOrNull(5000) {
                                generator.generate()
                            }
                        }
                    }
                    .awaitAll()
                    .filterNotNull()
            }

        println(results)
    }
    job.join()
}
 

Но я думаю, что, возможно, единственной причиной, по которой вы ввели CoroutineExceptionHandler, было игнорирование исключений. Эта стратегия не сработает, потому что обработчик имеет дело только с неперехваченными исключениями, а это означает, что восстанавливать слишком поздно. На этом этапе задание уже завершилось с ошибкой. Вам нужно будет обернуть ваш generate() вызов внутри async блока в try / catch или runCatching .

Комментарии:

1. Спасибо за ответ! На самом деле, я начал с generate() обернутого с try помощью / catch , и это сработало нормально, но потом я подумал об использовании supervisorScope , потому что я надеялся, что это позволит просто опустить try … И, да, проблема, похоже, в том awaitAll , кто выдает, а не в async