Home > OS >  Properly cancelling kotlin coroutine job
Properly cancelling kotlin coroutine job

Time:10-14

I'm scratching my head around properly cancelling coroutine job. Test case is simple I have a class with two methods:

class CancellationTest {
   private var job: Job? = null
   private var scope = MainScope()

   fun run() {
      job?.cancel()
      job = scope.launch { doWork() }
   }

   fun doWork() {
      // gets data from some source and send it to BE
   }
}

Method doWork has an api call that is suspending and respects cancellation.

In the above example after counting objects that were successfully sent to backend I can see many duplicates, meaning that cancel did not really cancel previous invocation.

However if I use snippet found on the internet

internal class WorkingCancellation<T> {
    private val activeTask = AtomicReference<Deferred<T>?>(null)
    suspend fun cancelPreviousThenRun(block: suspend () -> T): T {
        activeTask.get()?.cancelAndJoin()

        return coroutineScope {
            val newTask = async(start = CoroutineStart.LAZY) {
                block()
            }

            newTask.invokeOnCompletion {
                activeTask.compareAndSet(newTask, null)
            }

            val result: T

            while (true) {
                if (!activeTask.compareAndSet(null, newTask)) {
                    activeTask.get()?.cancelAndJoin()
                    yield()
                } else {
                    result = newTask.await()
                    break
                }
            }

            result
        }
    }
}

It works properly, objects are not duplicated and sent properly to BE. One last thing is that I'm calling run method in a for loop - but anyways I'm not quire sure I understand why job?.cancel does not do its job properly and WorkingCancellation is actually working

CodePudding user response:

Short answer: cancellation only works out-of-the box if you call suspending library functions. Non-suspending code needs manual checks to make it cancellable.

Cancellation in Kotlin coroutines is cooperative, and requires the job being cancelled to check for cancellation and terminate whatever work it's doing. If the job doesn't check for cancellation, it can quite happily carry on running forever and never find out it has been cancelled.

Coroutines automatically check for cancellation when you call built-in suspending functions. If you look at the docs for commonly-used suspending functions like await() and yield(), you'll see that they always say "This suspending function is cancellable".

Your doWork isn't a suspend function, so it can't call any other suspending functions and consequently will never hit one of those automatic checks for cancellation. If you do want to cancel it, you will need to have it periodically check whether the job is still active, or change its implementation to use suspending functions. You can manually check for cancellation by calling ensureActive on the Job.

CodePudding user response:

In addition to Sam's answer, consider this example that mocks a continuous transaction, lets say location updates to a server.

var pingInterval = System.currentTimeMillis()
job = launch {
     while (true) {
         if (System.currentTimeMillis() > pingInterval) {
             Log.e("LocationJob", "Executing location updates... ")
             pingInterval  = 1000L
         }
     }
}

Continuously it will "ping" the server with location udpates, or like any other common use-cases, say this will continuously fetch something from it.

Then I have a function here that's being called by a button that cancels this job operation.

fun cancel() {
    job.cancel()
    Log.e("LocationJob", "Location updates done.")
}

When this function is called, the job is cancelled, however the operation keeps on going because nothing ensures the coroutine scope to stop working, all actions above will print

 Ping server my location...
 Ping server my location...
 Ping server my location...
 Ping server my location...
 Location updates done.
 Ping server my location...
 Ping server my location...

Now if we insert ensureActive() inside the infinite loop

while (true) {
     ensureActive()
     if (System.currentTimeMillis() > pingInterval) {
          Log.e("LocationJob", "Ping server my location... ")
           pingInterval  = 1000L
     }
}

Cancelling the job will guarantee that the operation will stop. I tested using delay though and it guaranteed total cancellation when the job it is being called in is cancelled. Emplacing ensureActive(), and cancelling after 2 seconds, prints

 Ping server my location...
 Ping server my location...
 Location updates done.
  • Related