question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Cancel on Job doesn't cancel scope, if Dispatcher context was overwritten

See original GitHub issue

I use coroutines for any asyn action in my applications, but also I wanted to test my classes, so I wrote small abstraction for Dispatchers:

object DispatcherProvider {
    var IO: CoroutineContext = Dispatchers.IO
        private set
    var Main: CoroutineContext = Dispatchers.Main
        private set
    var Default: CoroutineContext = Dispatchers.Default
        private set

    fun enableTesting(coroutineScope: CoroutineScope) {

        IO = coroutineScope.coroutineContext
        Main = coroutineScope.coroutineContext
        Default = coroutineScope.coroutineContext
    }

}

In tests I just use it like this:

 runBlocking {
                DispatcherProvider.enableTesting(this)
           // code that call classes which are using Coroutines
            }
// all async code will be done here

Everything worked well, but recently I had to test class, which implemented CoroutineScope:

class SomeClass():CoroutineScope{

    private val scopeJob by lazy { Job() }
    override val coroutineContext: CoroutineContext by lazy { DispatcherProvider.Main + scopeJob }
    
    fun release(){
        scopeJob.cancel()
    }
}

In my tests when I called cancel on scopeJob, scope was still active, and none of async call were stopped. I’m not sure if it is a bug, or I just don’t understand something, but I hope someone can help me. btw. coroutines rocks!

Issue Analytics

  • State:closed
  • Created 5 years ago
  • Reactions:1
  • Comments:7 (3 by maintainers)

github_iconTop GitHub Comments

1reaction
Coneyscommented, Jan 18, 2019

Thanks for your answer! I was referring to Chris Banes talk about Android Suspenders, where he showed how to use CoroutineScope and he did cancel it with “job.cancel()”

By “scope is not cancelled” I ment, that “isActive” flag is set to true. Whole example (DispatcherProvider is the class from my first comment:

  1. SomeClass
class SomeClass() : CoroutineScope {

    var wasCancelled = false
    private val scopeJob by lazy { Job() }
    override val coroutineContext: CoroutineContext by lazy { DispatcherProvider.Main + scopeJob }

    fun runCoroutine() {
        launch(DispatcherProvider.IO) {
            for (i in 0..10) {
                if (!isActive) {
                    println("Coroutine was cancelled")
                    wasCancelled = true
                    break
                }
                println("In for loop $i")
                delay(100)
            }
            println("Finished with $isActive")
        }
    }

    fun release() {
        scopeJob.cancel()
    }
}

2.SomeTest

class SomeTest {


    @Test
    fun test() {
        val someClass = SomeClass()

        runBlocking {
            DispatcherProvider.enableTesting(this)
            someClass.runCoroutine()
            delay(50)
            println("Calling release")
            someClass.release()
        }
        assert(someClass.wasCancelled)

    }
}

Console output after running test: In for loop 0 Calling release In for loop 1 In for loop 2 In for loop 3 In for loop 4 In for loop 5 In for loop 6 In for loop 7 In for loop 8 In for loop 9 In for loop 10 Finished with true

java.lang.AssertionError: Assertion failed

As you can see, after calling release my scope still “isActive”. Did I understand Chris wrong? Cancelling job doesn’t cancel scope? If so, why do we need this job anyway?

0reactions
dkhalanskyjbcommented, Sep 22, 2021

@EarthyOrange, I couldn’t reproduce the behavior you describe.

        val job = CoroutineScope(Dispatchers.IO).launch {
            while (this.isActive) { // this is CoroutineScope
                println("going to sleep...")
                Thread.sleep(100)
            }
        }
        println("Cancelling")
        job.cancel()
        println("Cancelled")
        Thread.sleep(10000)

For me, this prints going to sleep... only once.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Kotlin Coroutine Job Hierarchy — Succeed, Fail, and Cancel
Even after a cancellation, the parent coroutine still hasn't completed until all of its children are completed.
Read more >
Coroutines: first things first - Medium
The ongoing work (running coroutines) can be canceled by calling scope.cancel() at any point in time. You should create a CoroutineScope ...
Read more >
What is the difference Job.Cancel vs Scope.Cancel in ...
In your first code block, your Job does not cooperate with cancellation. It has an infinite loop and does not call any suspend...
Read more >
Coroutines (Part II) – Job, SupervisorJob, Launch and Async
val scope = CoroutineScope(Dispatchers. ... When a parent's job is canceled, the children's jobs are canceled;; If we cancel the child's job ......
Read more >
Improve app performance with Kotlin coroutines
In Kotlin, all coroutines must run in a dispatcher, even when they're ... Cancel the coroutine started above, this doesn't affect the scope...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found