Home > database >  Coroutines Child Job Cancellation
Coroutines Child Job Cancellation

Time:01-19

According to Job documentation, the invocation of [cancel][Job.cancel] with exception (other than [CancellationException]) on this job also cancels parent. Job.cancel function only accepts CancellationException. I am testing this behavior but cancelling a child job is not cancelling the parent job despite I am not using SupervisorJob.

/**
 * Creates a job object in an active state.
 * A failure of any child of this job immediately causes this job to fail, too, and cancels the rest of its children.
 *
 * To handle children failure independently of each other use [SupervisorJob].
 *
 * If [parent] job is specified, then this job becomes a child job of its parent and
 * is cancelled when its parent fails or is cancelled. All this job's children are cancelled in this case, too.

 * --The invocation of [cancel][Job.cancel] with exception (other than [CancellationException]) on this job also cancels parent.--
 *
 * Conceptually, the resulting job works in the same way as the job created by the `launch { body }` invocation
 * (see [launch]), but without any code in the body. It is active until cancelled or completed. Invocation of
 * [CompletableJob.complete] or [CompletableJob.completeExceptionally] corresponds to the successful or
 * failed completion of the body of the coroutine.
 *
 * @param parent an optional parent job.
 */

@Suppress("FunctionName")
public fun Job(parent: Job? = null): CompletableJob = JobImpl(parent)

So my test code is like;

fun main()  {
    val parentJob = Job()
    val scope = CoroutineScope(parentJob)

    suspend fun printText(text: String) {
        println("Before Delay: $text")
        delay(1000)
        println("After Delay: $text")
    }

    val job1 = scope.launch {
        printText("Job#1")
    }

    job1.invokeOnCompletion {
        println("Job#1 completed. Cause = $it")
    }

    val job2 = scope.launch {
        printText("Job#2")
    }

    job2.invokeOnCompletion {
        println("Job#2 completed. Cause = $it")
    }

    println(parentJob.children.joinToString { it.toString() } )

    job1.cancel(CancellationException())

    Thread.sleep(10000)

}

And the outputs like;

Before Delay: Job#1
Before Delay: Job#2
StandaloneCoroutine{Active}@462d5aee, StandaloneCoroutine{Active}@69b0fd6f
Job#1 completed. Cause = java.util.concurrent.CancellationException
After Delay: Job#2
Job#2 completed. Cause = null

Process finished with exit code 0

My question is why the job#2 is not being cancelled?

EDIT:

After the answers from @broot and @Steyrix. I extended the test case. Now printText(string) function throws exception if the given argument is "Job#1". So I am trying to simulate a viewmodel in android. Lets say that we created CoroutineScope(Job()) and I make two different requests. One of them is throwing exception but it is being caught by try-catch block. So other job continues doing its job and is not being cancelled.

So now the question is then what is the difference between SupervisorJob and Job. Why viewmodelscope (CloseableCoroutineScope(SupervisorJob() Dispatchers.Main.immediate)) uses SupervisorJob ?

Extended example;

fun main() {
    val parentJob = Job()
    val scope = CoroutineScope(parentJob)

    suspend fun printText(text: String) {
        println("Before Delay: $text")
        if (text == "Job#1") {
            throw IllegalArgumentException("Test")
        }
        delay(1000)
        println("After Delay: $text")
    }

    val job1 = scope.launch {
        try {
            printText("Job#1")
        } catch (e: Exception) {
            println(e)
        }


    }

    job1.invokeOnCompletion {
        println("Job#1 completed. Cause = $it")
    }


    val job2 = scope.launch {
        printText("Job#2")
    }

    job2.invokeOnCompletion {
        println("Job#2 completed. Cause = $it")
    }

    println(parentJob.children.joinToString { it.toString() })


    Thread.sleep(10000)

}

Output;

Before Delay: Job#1
java.lang.IllegalArgumentException: Test
Job#1 completed. Cause = null
Before Delay: Job#2
StandaloneCoroutine{Active}@462d5aee
After Delay: Job#2
Job#2 completed. Cause = null

Process finished with exit code 0

CodePudding user response:

According to official documentation:

Normal cancellation of a job is distinguished from its failure by the type of this exception that caused its cancellation. A coroutine that threw CancellationException is considered to be cancelled normally. If a cancellation cause is a different exception type, then the job is considered to have failed. When a job has failed, then its parent gets cancelled with the exception of the same type, thus ensuring transparency in delegating parts of the job to its children.

Note, that cancel function on a job only accepts CancellationException as a cancellation cause, thus calling cancel always results in a normal cancellation of a job, which does not lead to cancellation of its parent. This way, a parent can cancel its own children (cancelling all their children recursively, too) without cancelling itself.

job1.cancel(CancellationException())

Here you cancel the child job with CancellationException, therefore it is being cancelled "normally" and does not lead to parent cancellation and other children cancellation likewise.

CodePudding user response:

Well, it behaves according to the documentation you cited. You used a CancellationException, so it didn't cancel the parent.

The only confusing part in the documentation is why it mentions cancelling using other exception type than CancellationException. This is not possible. cancel() is for normal cancellations, not for failures, so it can never propagate to parents.

It looks like a small mistake in the documentation for a Job() function.

  • Related