Kristian Frøhlich
09/10/2023, 8:54 AMcontext(ResourceScope)
suspend fun coroutineScope(context: CoroutineContext) =
install({ CoroutineScope(context) }, { scope, exitCase ->
when (exitCase) {
ExitCase.Completed -> scope.cancel()
is ExitCase.Cancelled -> scope.cancel(exitCase.exception)
is ExitCase.Failure -> scope.cancel("Resource failed, so cancelling associated scope", exitCase.failure)
}
scope.coroutineContext.job.join()
})
When using this in a processor like the following:
context (ResourceScope)
fun schemaProcessor(): SchemaProcessor = SchemaProcessor {
val scope = coroutineScope(<http://Dispatchers.IO|Dispatchers.IO>)
return@SchemaProcessor scope.launch(Job(coroutineContext.job)) {
delay(10000)
throw IllegalArgumentException("BOOM")
}
}
….to force an exception i would expect this would trigger the ExitCase.Failure but ExitCase.Cancelled is triggered. Looks like the failure is never reached whatever exception is thrown. This means that if i want to do ie logging i have to check on the exception type in the cancelled block instead of just writing out the failure.
Is this working like this due to how the nature of coroutines is ? In what cases would the ExitCase.Failure be triggered ?simon.vergauwen
09/11/2023, 6:46 AMExitCase.Failure inside coroutineScope? The ExitCase comes from the ResourceScope, so in this case not from the `launch`/`Job` running inside but that might depend a bit on how Job(coroutineContext.job) relates to each other so I I have a couple of questions to try and figure out what is going on but t
I am not entire sure how SchemaProcessor works, is it something like:
fun interface SchemaProcessor {
suspend fun name(): Job
}
Since you're calling launch(Job(coroutineContext.job)) I assume that the coroutineContext where ResourceScope is being created already has a Job? Is that coming from runBlocking?Kristian Frøhlich
09/11/2023, 6:52 AMKristian Frøhlich
09/11/2023, 6:53 AMfun main(): Unit = SuspendApp {
val config = config()
resourceScope {
val hikari = hikari(config.database.toProperties())
val schemaDatabase = schemaDatabase(hikari)
with(config, schemaDatabase, logger()) {
with(schemaRepository()) {
with(schemaService(), schemaConsumer()) {
schemaProcessor().process()
server(Netty, host = "0.0.0.0", port = 8080, preWait = 5.seconds) { setup() }
}
}
}
awaitCancellation()
}
}Kristian Frøhlich
09/11/2023, 7:12 AMlaunch(Job(coroutineContext.job)) is to link it with the parent job, but i think you know that already 🙂 Or else it won’t propagate the exception. I’m open to solving it in a different way if there are more idiomatic ways.simon.vergauwen
09/11/2023, 7:17 AMcontext(ResourceScope)
suspend fun coroutineScope(context: CoroutineContext) =
install({ CoroutineScope(currentCoroutineContext() + context) }, { scope, exitCase ->
when (exitCase) {
ExitCase.Completed -> scope.cancel()
is ExitCase.Cancelled -> scope.cancel(exitCase.exception)
is ExitCase.Failure -> scope.cancel("Resource failed, so cancelling associated scope", exitCase.failure)
}
scope.coroutineContext.job.join()
})simon.vergauwen
09/11/2023, 7:17 AMKristian Frøhlich
09/11/2023, 7:19 AMsimon.vergauwen
09/11/2023, 7:20 AMserver(Netty, ... because in my minimal example I am seeing the following:simon.vergauwen
09/11/2023, 7:21 AM3
ExitCase.Completed
coroutineScope: ExitCase.CompletedKristian Frøhlich
09/11/2023, 7:23 AMsimon.vergauwen
09/11/2023, 7:23 AMrunBlocking and SuspendApp work a bit differentKristian Frøhlich
09/11/2023, 7:24 AMsimon.vergauwen
09/11/2023, 7:36 AMsimon.vergauwen
09/11/2023, 7:41 AMExitCase.Cancelled is awaitCancellation. Since the IllegalArgumentException inside launch cancels the parent, awaitCancellation throws it's CancellationException.
If inside ExitCase.Cancelled you check for the cause you'll get the original IllegalArgumentExeption.
So to finally answer your original questions:
• Yes, this is how coroutines work. A child cancelling it's parents result in the original exception being wrapped in CancellationException. So the ResourceScope only ever sees CancellationException and not IllegalArgumentException. It's present in the cause if you want to have it logged though.
• So a child cannot trigger ExitCase.Failure, this would be triggered only if an exception is being thrown inside of the resourceScope directly. So if you replace awaitCancellation with IllegalArgumentException.
Sorry for some of the confusion above, I always need to think a little of what's going on 😅Kristian Frøhlich
09/11/2023, 8:22 AMsimon.vergauwen
09/11/2023, 8:44 AMlaunch itself. Since that's "a unit of work", and logging it on the end of the cancellation process can be considered bad practice cause it mixing concernsKristian Frøhlich
09/11/2023, 9:29 AMSimon Vergauwen [MOD] [9:17 AM]
Yes, but I thought that would’ve happened automatically 🤔 but now I see that’s not the case. I think it would’ve also worked with this updated code.I guess that means removing the explicit linking in the launch…..does not seem to work for me. I agree it would be nice if one could link the child-coroutine to the parent without doing it explicit for every launch..
Kristian Frøhlich
09/11/2023, 10:06 AMval scope = coroutineScope(<http://Dispatchers.IO|Dispatchers.IO>) + currentCoroutineContext()simon.vergauwen
09/12/2023, 5:41 AMsimon.vergauwen
09/12/2023, 5:42 AMdoes not seem to work for me.Seems to be something strange going on, I have several version that work and several that don't 🤕 but it should all be the same
Kristian Frøhlich
09/12/2023, 6:05 AMcurrentCoroutineContext() call outside the CoroutineContext(..) call and remove the explicit ...job.join() it flows as expectedKristian Frøhlich
09/12/2023, 6:05 AMcontext(ResourceScope)
suspend fun coroutineScope(context: CoroutineContext): CoroutineScope {
val currentContext = currentCoroutineContext()
return install({ CoroutineScope(currentContext + context) }, { scope, exitCase ->
when (exitCase) {
ExitCase.Completed -> scope.cancel()
is ExitCase.Cancelled -> scope.cancel(exitCase.exception)
is ExitCase.Failure -> scope.cancel("Resource failed, so cancelling associated scope", exitCase.failure)
}
})
}Kristian Frøhlich
09/12/2023, 6:08 AMsimon.vergauwen
09/12/2023, 6:09 AMremove the explicit ...job.join() it flows as expected?Kristian Frøhlich
09/12/2023, 6:10 AMKristian Frøhlich
09/12/2023, 6:11 AMsimon.vergauwen
09/12/2023, 6:20 AMcoroutineScope { } digging again into the internals of coroutineScope. WDYT?
I think it was missing a new job that inherits from the parent.
context(ResourceScope)
suspend fun coroutineScope(context: CoroutineContext): CoroutineScope {
val newContext = currentCoroutineContext()
val job = currentCoroutineContext()[Job]?.let { Job(it) } ?: Job()
return install({ CoroutineScope(context + newContext + job) }) { scope, exitCase ->
println(exitCase)
when (exitCase) {
ExitCase.Completed -> job.cancel()
is ExitCase.Cancelled -> job.cancel(exitCase.exception)
is ExitCase.Failure -> job.cancel("Resource failed, so cancelling associated scope", exitCase.failure)
}
job.join()
}
}Kristian Frøhlich
09/12/2023, 6:25 AMKristian Frøhlich
09/12/2023, 6:28 AMsimon.vergauwen
09/12/2023, 6:28 AMKristian Frøhlich
09/12/2023, 6:29 AMKristian Frøhlich
09/12/2023, 6:49 AMSuspendApp(SupervisorJob() + CoroutineExceptionHandler { _, _ -> })...
Would it make sense to have a shortcut to this through the library ? Ie, SuspendApp(emptyExHandler = true) ? Or something similar.Kristian Frøhlich
09/14/2023, 10:30 AMsimon.vergauwen
09/14/2023, 10:42 AMrunBlocking.
handle and log all exceptions i find it quite redundant to let the exceptions bubble all the way out to the edge of my appsI do the same thing, but then it's no longer necessary to install
SupervisorJob() + CoroutineExceptionHandler { _, _ -> } , right? 🤔 What is the reason for the SupervisorJob() btw?
I'm not sure if it makes sense for the library but it's very annoying if exceptions get logged twice, since it's sometimes gives the impression that something is wrong when it's notKristian Frøhlich
09/14/2023, 10:44 AMKristian Frøhlich
09/14/2023, 10:48 AMval job: Job = launch(context = context, start = CoroutineStart.LAZY) { block() }simon.vergauwen
09/14/2023, 11:10 AMrunBlocking. https://github.com/arrow-kt/suspendapp/blob/bb7889c6ed5cad877b3c042dab4575b55e149a73/src/jvmMain/kotlin/arrow/continuations/unsafe/unsafe.kt#L37simon.vergauwen
09/14/2023, 11:15 AMthe actual exception handlerThe code that runs inside
job is the lambda you pass to SuspendApp { } so you can install any exception handler inside that lambdasimon.vergauwen
09/14/2023, 11:16 AMeither (or Result) and log them without rethrowing so the process exits with process code 0Kristian Frøhlich
09/14/2023, 11:17 AMKristian Frøhlich
09/14/2023, 12:55 PMsimon.vergauwen
09/14/2023, 1:14 PMEither won't catch CancellationException you need Result for thatKristian Frøhlich
09/14/2023, 1:17 PMKristian Frøhlich
09/14/2023, 1:18 PMKristian Frøhlich
09/14/2023, 1:19 PM