Добрый день! Я сейчас разрабатываю кодогенерацию ...
# russian
a
Добрый день! Я сейчас разрабатываю кодогенерацию Swagger для kotlin-multiplatform. Под капотом использую ktor, kotlinx-serialization и kotlinx-coroutines. Одна из фич кодогенерации - обработка схемы кастомных ошибок. Сейчас это реализовано через Exceptions. Это накладывает кучу ограничений. Например, мне всегда нужно пользоваться SupervisorScope и при обработке всегда прокидывать CancellationsException. Насколко я понимаю, exceptions имеют другое значение при использовании корутин (механизм управления ресурасами?). Я попробовал использовать Result из Kotlin 1.3, но его нельзя вернуть и нельзя параметризовать типом ошибки. У каждого роута у меня свой набор ошибок. Мой вопрос в том, как правильно организовать обработку ошибок при использовании корутин (и ktor)?
g
Не кидать ошибку, вернуть какой нибудь Sealed класс результата, в общем работа с ошибками в этом случае ничем у корутин не отличается с любым другим кодом
так же не оченьо понятно чем Result в данном случае бы помог. ведь это фактически тоже самое, это не кастомная ошибка, а просто обертка над эксепшеном
a
Исключения имеют ровно ту же природу в корутинах. Исключением является CancelationException, которое может быть частью нормального поведения.
1
g
Может есть какой то пример кода, сейчас звучит как будто можно улучшить то как эта корутина написана, SupervisorScope имеет смысл, но в относительно специфичных кейсах и в большинстве suspend функий смысла в нем нет
r

https://www.youtube.com/watch?v=GYBhHUGR1ZY

Это для GraphQL но хорошо и для REST
g
я думаю тут больше проблема в том как конкретно корутины используются чем общий вопрос
a
@gildor Спасибо за совет! В итоге, сделал именно sealed классы для каждого варианта. Но, всё же, интересно, почему выбрано именно такое поведение корутин по-умолчанию
Copy code
val scope = CoroutineScope(<http://Dispatchers.IO|Dispatchers.IO>)

scope.async {
	
	// Я хочу обработать exception здесь 
	// и не пробрасывать его на уровень выше,
	// но он пробрасывается
	val result = try { 
		otherCallAsync(this).await()
	} catch (e: Exception) {
		fallback
	}

	result
}

fun otherCallAsync(scope: CoroutineScope) = scope.async {
	throw RuntimeException("no internet")
}
g
Попозже скину почему такое поведение выбрано, что бы не отменять скоуп и обработать ошибку руками просто создайте CoroutineScope передав SupervisorJob
a
@gildor Добрый день! Можете, пожалуйста, скинуть материалы по поводу этого поведения?
g
Ох. забыл
прежде всего это описано в официальном гайде по корутинам https://kotlinlang.org/docs/reference/coroutines/exception-handling.html
там же написано как использовать SupervisorJob что бы обрабатывать отмену родительского скопа вручную
Вот еще немного контекста, там описано почему такое поведение по умолчанию, но issue о том что необходимо еще и SupervisorJob https://github.com/Kotlin/kotlinx.coroutines/issues/576
@adev_one Еще раз возвращаясь к вашему примеру выше, хотел бы добавить, что как демонстрация этого поведения он работает, но сам подход не должен быть использоваться для продакшн кода, например функции которые возвращают Deferred должны быть очень редки и по хорошему должно быть переписано:
Copy code
val scope = CoroutineScope(<http://Dispatchers.IO|Dispatchers.IO>)

scope.async {
    val result = try { 
        otherCallAsync(this)
    } catch (e: Exception) {
        fallback
    }

    result
}

fun otherCallAsync() {
   throw RuntimeException("no internet")
}
Deferred по хорошему нужен только что бы паралелить операции, создавать async и сразу вызывать await всегда значит не верное использование (в данном случае скрытое правда за вызовом функции)
a
@gildor Спасибо за материалы! Раньше не видел issue на github. Стало понятнее, почему было принято такое решение. Оно действительно оптимально для бэка, где у нас много клиентов на одной ноде. Нам async нужен именно для parallel исполнения, а не concurrent. За счёт того, что клиентов много и они исполняются concurrently и так, мы эффективно утилизируем ресурсы. Если бы мы использовали async внутри handler’ов - нам бы это ничего не дало так как просто бы выросла очередь. Но на мобилках другой кейс. Мы хотим доставить данные пользователю как можно скорее и если что-то отвалится - всё равно что-то показать. Чаще всего, одновременно исполняется только один Interactor, но он ходит в несколько источников. И все они нестабильны. И интернет плохой. Поэтому хочется использовать что-то для решения этой проблемы, но async не подходит, а альтернатив в корутинах я не знаю. Именно поэтому возник такой вопрос.
g
@adev_one Не уверен что это как то отличается при использовании на бэкенде или на клиенте, и там и там есть разные юз кейсы, подходящие под разные способы использования лайфсайкла корутин Не понимаю почему вы пишите, что async не подходит, просто используйте SupervisorJob:
Copy code
val scope = CoroutineScope(SupervisorJob () + <http://Dispatchers.IO|Dispatchers.IO>)
Мои комменты выше были по отношению к стилю того как вы ваш код групируете, что если есть возможность, лучше использовать только suspend функции и паралелить их врапая в async, но только на месте вызова, где можно будет легко обработать ошибку, это просто то как рекомендуется в официальном гайде это делать, опять же там где возможно и это зависит от юз кейса и если у вас есть пример кода, то как вы используете корутины, то можно было бы более предметно поговорить