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 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()
}
}
launch(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()
})
Kristian 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:3
ExitCase.Completed
coroutineScope: ExitCase.Completed
Kristian 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 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..
val scope = coroutineScope(<http://Dispatchers.IO|Dispatchers.IO>) + currentCoroutineContext()
simon.vergauwen
09/12/2023, 5:41 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 expectedcontext(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)
}
})
}
simon.vergauwen
09/12/2023, 6:09 AMremove the explicit ...job.join() it flows as expected
?Kristian Frøhlich
09/12/2023, 6:10 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 AMsimon.vergauwen
09/12/2023, 6:28 AMKristian Frøhlich
09/12/2023, 6:29 AMSuspendApp(SupervisorJob() + CoroutineExceptionHandler { _, _ -> })...
Would it make sense to have a shortcut to this through the library ? Ie, SuspendApp(emptyExHandler = true) ? Or something similar.simon.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 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#L37the actual exception handlerThe code that runs inside
job
is the lambda you pass to SuspendApp { }
so you can install any exception handler inside that lambdaeither
(or Result
) and log them without rethrowing so the process exits with process code 0Kristian Frøhlich
09/14/2023, 11:17 AMsimon.vergauwen
09/14/2023, 1:14 PMEither
won't catch CancellationException
you need Result
for thatKristian Frøhlich
09/14/2023, 1:17 PM