Search code examples
kotlinkotlin-coroutinesandroid-workmanager

How to proper cancel CoroutineWorker


I've a CoroutineWorker that need to start and stop when user interact with my app, so I use the awaitCancelation() method, like this:

override suspend fun doWork(): Result {
        try {
            startListen()
            awaitCancellation()
        } finally {
            stopListen()
            return Result.success()
        }
    }

All works fine, but when I call WorkManager.cancel and cancel my job, an internal exception is thrown by the WorkerWrapper:

I/WM-WorkerWrapper: Work [ id=721c463b-15aa-4daa-988d-a4c080915443, tags={ br.com.example.app.workers.MyWorker, WORKER_TAG } ] was cancelled
    java.util.concurrent.CancellationException: Task was cancelled.
        at androidx.work.impl.utils.futures.AbstractFuture.cancellationExceptionWithCause(AbstractFuture.java:1184)
        at androidx.work.impl.utils.futures.AbstractFuture.getDoneValue(AbstractFuture.java:514)
        at androidx.work.impl.utils.futures.AbstractFuture.get(AbstractFuture.java:475)
        at androidx.work.impl.WorkerWrapper$2.run(WorkerWrapper.java:300)
        at androidx.work.impl.utils.SerialExecutor$Task.run(SerialExecutor.java:91)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:923)

I didn'd find any other way that looks more properly to stop my CoroutineWorker at the same I dont feel that is the right way to stop it.

If anyone knows another way to avoid this or to supress this exception, I'd appreciate.


Solution

  • Normally, there is nothing really wrong that our cancellable background task is cancelled by throwing CancellationException. It is usually the easiest to throw an exception, because it can automatically propagate through the whole call stack. Java utilizes the same technique when interrupting a thread - it throws InterruptedException.

    But if you really need a cleaner way of signalling your background service that it should stop working, there are plenty of utils in coroutine library that let you achieve this. For example, you can do this with Mutex:

    private val mutex = Mutex(true)
    
    override suspend fun doWork(): Result {
            try {
                startListen()
                mutex.lock()
            } finally {
                stopListen()
                return Result.success()
            }
        }
    
    fun stop() {
        mutex.unlock()
    }
    

    As the mutex is initially in a locked state, it will suspend at lock(). Then the mutex is unlocked in stop(), allowing the first coroutine to continue. You can do the same with semaphores, channels, by creating a CompletableJob and then join() on it, etc.