supervisorScope cancels its parent - kotlin

I was playing around with the supervisorScope in android. The documentation says the following.
A failure of a child does not cause this scope to fail and does not
affect its other children, so a custom policy for handling failures of
its children can be implemented. See SupervisorJob for details. A
failure of the scope itself (exception thrown in the block or
cancellation) fails the scope with all its children, but does not
cancel parent job.
But consider the following code
viewModelScope.launch {
launch { someLongJob() }
supervisorScope {
launch { jobTwo() }
launch { jobThree() }
delay(500)
throw ArithmeticException()
}
}
private suspend fun someLongJob() {
try {
delay(Long.MAX_VALUE)
} catch (e: Exception) {
Log.d(TAG, "Long job cancelled")
}
}
private suspend fun jobOne() {
delay(1000)
throw ArithmeticException()
}
private suspend fun jobTwo() {
try {
delay(2000)
} catch (e: Exception) {
Log.d(TAG, "Job 2 cancelled")
throw e
}
Log.d(TAG, "Job 2 completed successfully")
}
private suspend fun jobThree() {
try {
delay(3000)
} catch (e: Exception) {
Log.d(TAG, "Job 3 cancelled")
throw e
}
Log.d(TAG, "Job 3 completed successfully")
}
It produces the following output
D/TEST: Job 2 cancelled
D/TEST: Job 3 cancelled
D/TEST: Long job cancelled
---beginning of crash
My doubt is, as per documentation mentioned in bold, the supervisorScope should not cancel its parent even when the scope itself fails meaning that long job(which is running in the parent job) should not be cancelled. But the output clearly shows that the Long job which runs in the parent also gets cancelled. Why?

supervisorScope is a function call just like any other. In your case, it completed abruptly, with an exception. Since you didn't catch it, the same exception caused the launch block to complete abruptly, and that caused it to be cancelled. Then, following the fundamental principles of structured concurrency, cancellation spread inward and cancelled all the child jobs.

Related

Reactor - doOnError stops exception propagation

I have this simple code
fun method(): Mono<String> {
return Mono.error(RuntimeException("RUNTIME EXCEPTION"))
}
fun main(args: Array<String>) {
try {
method()
.doOnError {
when(it){
is RuntimeException -> println("DO_ON_NEXT RUNTIME EXCEPTION")
else -> println("DO_ON_NEXT OTHER EXCEPTION")
}
}
.subscribe {
println("FLOW FINISHED")
}
} catch (e: Exception){
println("CAUGHT EXCEPTION IN TRY CATCH CLAUSE")
}
}
When I run it it finishes with exit code 0 but the phrase FLOW FINISHED from the subscribe clause is NOT printed.
However when I remove the doOnError clause, the RuntimeException is being caught in the try-catch and CAUGHT EXCEPTION IN TRY CATCH CLAUSE is printed.
Can anyone help me how to make it so that the exception will be caught in the try-catch and the doOnError clause will be kept ?

After a coroutine scope is canceled, can it still be used again?

When we have a coroutine scope, when it is canceled, can it be used again?
e.g. for the below, when I have scope.cancel, the scope.launch no longer work
#Test
fun testingLaunch() {
val scope = MainScope()
runBlocking {
scope.cancel()
scope.launch {
try {
println("Start Launch 2")
delay(200)
println("End Launch 2")
} catch (e: CancellationException) {
println("Cancellation Exception")
}
}.join()
println("Finished")
}
}
Similarly, when we have scope.cancel before await called,
#Test
fun testingAsync() {
val scope = MainScope()
runBlocking {
scope.cancel()
val defer = scope.async {
try {
println("Start Launch 2")
delay(200)
println("End Launch 2")
} catch (e: CancellationException) {
println("Cancellation Exception")
}
}
defer.await()
println("Finished")
}
}
It will not execute. Instead, it will crash with
kotlinx.coroutines.JobCancellationException: Job was cancelled
; job=SupervisorJobImpl{Cancelled}#39529185
at kotlinx.coroutines.JobSupport.cancel(JobSupport.kt:1579)
at kotlinx.coroutines.CoroutineScopeKt.cancel(CoroutineScope.kt:217)
at kotlinx.coroutines.CoroutineScopeKt.cancel$default(CoroutineScope.kt:215)
at com.example.coroutinerevise.CoroutineExperiment$testingAsync$1.invokeSuspend(CoroutineExperiment.kt:241)
at |b|b|b(Coroutine boundary.|b(|b)
at kotlinx.coroutines.DeferredCoroutine.await$suspendImpl(Builders.common.kt:101)
at com.example.coroutinerevise.CoroutineExperiment$testingAsync$1.invokeSuspend(CoroutineExperiment.kt:254)
Caused by: kotlinx.coroutines.JobCancellationException: Job was cancelled; job=SupervisorJobImpl{Cancelled}#39529185
at kotlinx.coroutines.JobSupport.cancel(JobSupport.kt:1579)
at kotlinx.coroutines.CoroutineScopeKt.cancel(CoroutineScope.kt:217)
at kotlinx.coroutines.CoroutineScopeKt.cancel$default(CoroutineScope.kt:215)
at com.example.coroutinerevise.CoroutineExperiment$testingAsync$1.invokeSuspend(CoroutineExperiment.kt:241)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
Is it true, a canceled coroutine scope cannot be used for launch or async anymore?
Instead of using CoroutineScope for cancelling all of the launched jobs in it, you may want to use the underlying CoroutineContext with its cancelChildren() method which doesn't affect the Job state (which is not true for plain cancel() method) and allows to continue launching new coroutines after being invoked.
Following up on #Alex Bonel response.
For example you have a method like
fun doApiCall() {
viewModelScope.launch {
// do api call here
}
}
You can call doApiCall() again and again
viewModelScope.coroutineContext.cancelChildren()
doApiCall()
Calling doApiCall() will not have any effect.
viewModelScope.coroutineContext.cancel()
doApiCall() // would not call
I am using that in compose and it is slightly different.
Define coroutine scope in compose
val coroutineScope = rememberCoroutineScope()
Launch coroutine
coroutineScope.launch(Dispatchers.Main) { //Perform your operation }
Cancel all coroutines that belong to coroutineScope
coroutineScope.coroutineContext.cancelChildren()
Related to lifecycleScope comment above, I'm not sure how common cancellation like this would be in practice? fwiw something like following will work:
val scope = MainScope()
runBlocking {
val job1 = scope.launch {
try {
println("Start Launch 1")
delay(200)
println("End Launch 1")
} catch (e: CancellationException) {
println("Cancellation Exception")
}
}
job1.cancel()
val job2 = scope.launch {
try {
println("Start Launch 2")
delay(200)
println("End Launch 2")
} catch (e: CancellationException) {
println("Cancellation Exception")
}
}
job2.join()
println("Finished")
}
This particular example will print
Start Launch 1
Cancellation Exception
Start Launch 2
End Launch 2
Finished
I needed to collect the event just once and then stop listening. I came up with this solution:
/**
* Collect the value once and stop listening (one-time events)
*/
suspend fun <T> Flow<T>.collectOnce(action: suspend (value: T) -> Unit) {
try {
coroutineScope {
collectLatest {
action(it)
this#coroutineScope.cancel()
}
}
} catch (e: CancellationException) { }
}
You can use it like this:
viewModelScope.launch {
myStateFlow.collectOnce {
// Code to run
}
}

Why i can't use try/catch to catch exception in Kotlin coroutine?

I have code in Android as follow:
//use MainScope
private val scope = MainScope()
private fun test() {
scope.launch {
Log.d(TAG, "test: launch")
try {
val response: Deferred<String> = async {
Log.d(TAG, "test: in async block")
throw IllegalStateException("an IllegalStateException")
}
response.await()
} catch (e: Exception) {
Log.d(TAG, "test: error ${e.message}")
}
}
}
I use MainScope to launch a coroutine and use async to launch a child coroutine in it. When i use try/catch to catch the exception of child coroutine , i failed and the application crashed. Could some one tell me why ?
Crash messages as follow:
D/AsyncExceptionTestActiv: test: launch
D/AsyncExceptionTestActiv: test: in async block
D/AsyncExceptionTestActiv: test: error an IllegalStateException
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.hm.dumingwei.kotlinandroid, PID: 18461
java.lang.IllegalStateException: an IllegalStateException
at com.hm.dumingwei.kotlinandroid.AsyncExceptionTestActivity$test$1$response$1.invokeSuspend(AsyncExceptionTestActivity.kt:61)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:241)
at android.os.Handler.handleCallback(Handler.java:761)
at android.os.Handler.dispatchMessage(Handler.java:98)
at android.os.Looper.loop(Looper.java:156)
at android.app.ActivityThread.main(ActivityThread.java:6517)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:942)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:832)
The exception thrown inside nested coroutine is propagated to the parent coroutine unless you use a SupervisorJob:
scope.launch {
Log.d(TAG, "test: launch")
try {
val response: Deferred<String> = async(SupervisorJob()) {
Log.d(TAG, "test: in async block")
throw IllegalStateException("an IllegalStateException")
}
response.await()
} catch (e: Exception) {
Log.d(TAG, "test: error ${e.message}")
}
}
Kotlin Documentation, info, more info

Does the scope handle exceptions/failures silently under the hood?

I have the following snippet for testing purposes;
fun main() {
val myScope = CoroutineScope(Dispatchers.Default) + Job()
myScope.launch {
val job = async {
delay(1000)
throw RuntimeException("shiiiet")
}
try {
job.await()
} catch (ret: RuntimeException){
throw RuntimeException("yooo!")
}
}
try {
Thread.sleep(5000)
} catch(e: Exception){
}
println("wohoooo!")
}
I thought the flow would never reach the last "wohooo!" line but I was wrong. I see it's printed on the screen. The reason I had in my mind that launch would propagate the exception to the parent scope and since the parent scope does not handle it, it would crash the JVM by the time it reaches the print statement.
Is this because the parent scope got cancelled once it's child failed, received a CancellationException and it was ignored?
You tried multiple throwand catch approaches in your example.
async works as expected - when you await for it, you can catch the exception. But if you launch a co-routine, the default Thread.uncaughtExceptionHandler just prints the result to console.
Even if you do
myScope.launch {
....
}.invokeOnCompletion { e -> println("Exception: $e") }
you still get the result additionally on console.
The propagation rules and the different types of calls to handle the exceptions are explained here.
An example on how to catch an exception in the "main" co-routine:
fun main() = runBlocking {
try {
GlobalScope.launch {
delay(10)
throw Exception("You don't catch me in main.")
}
launch {
delay(10)
throw Exception("You catch me in main.")
}
delay(100)
println("Never reached.")
} catch(e: Exception) {
println("Caught in main: ${e.cause}")
}
println("The end")
}

RxJava2 Flowable onErrorReturn not called

Say I need to wrap BroadcastReceiver with Flowable:
Flowable
.create<Boolean>({ emitter ->
val broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
throw RuntimeException("Test exception")
}
}
application.registerReceiver(broadcastReceiver, IntentFilter(LocationManager.PROVIDERS_CHANGED_ACTION))
}, BackpressureStrategy.MISSING)
.onErrorReturn { false }
Then I need to catch any exceptions thrown inside Flowable in one single place.
I supposed onErrorReturn should be able to catch that throw RuntimeException("Test exception") inside broadcastReceiver but it doesn't catch that exception and app crashes.
Certainly, I can wrap anything inside BroadcastReceiver with try/catch. But actually, I have a lot of source code there so that adding try/catch makes source code quite messy.
Is there any way to catch all the exceptions thrown in any line inside Flowable in one single place?
In case of Flowable#create() to follow contract of Flowable if you have error and want to pass it through stream, you need to catch it and call emitter.onError(). If you do that, Flowable.onErrorReturn() starts work as expected.
To properly register/unregister BroadcastReceiver and handle exceptions you can use that approach
Flowable
.create<Boolean>({ emitter ->
val broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
try {
throw RuntimeException("Test exception")
} catch(e: Throwable) {
emitter.tryOnError(e)
}
}
}
try {
application.registerReceiver(broadcastReceiver, IntentFilter(LocationManager.PROVIDERS_CHANGED_ACTION))
emitter.setCancellable {
application.unregisterReceiver(broadcastReceiver)
}
} catch (e: Throwable) {
emitter.tryOnError(e)
}
}, BackpressureStrategy.MISSING)
.onErrorReturn { false }