WorkManager: Why does failed unique work with the "APPEND" ExistingWork strategy not allow more work under the same name?

Viewed 3126

Let's say we are developing a messaging app where we want to send messages into given conversations where the order of those messages is important in that conversation only, and if the app is put to the background, we want a guarantee that the message will be sent.

The WorkManager#beginUniqueWork method seems ideal for this, where the uniqueWorkName will be some conversation id, and ExistingWorkPolicy.APPEND will be used as the work policy to keep the work in the order scheduled.

So far in my application, as long as each piece of Work returns Result.SUCCESS, then any future scheduled work will be executed as expected. However, if one particular message fails to send in a fatal way and I return Result.FAILURE, then all future work with the same conversation id never seems to reach my implementation of Worker#doWork().

After digging through the source code of the EnqueueRunnable class, this seems like a very deliberate choice. What I can't really understand is why that is? It seems odd that if a uniqueWorkName fails, that that name becomes unusable for the rest of the life of the application (this persists across killing the app).

Furthermore, I'd like to know if anybody has a good solution to this, or knows if this will change in future versions of WorkManager. So far, the best thing I can think of is to return Result.SUCCESS but encode my own failure state in the output Data so that any observers of the work know it has failed. This however is a bit awkward and isn't very obvious for future maintainers of the code (and can be a bit confusing when watching the logs for a given piece of Work).

Perhaps my intended use of unique work is completely wrong and there's a better solution out there. Any ideas would be greatly appreciated, thanks!

3 Answers

So I found the answer to my own question in this google issue tracker report.

Basically, unique work using the APPEND strategy creates a WorkContinuation where every new item is chained on as if we were to use the WorkContinuation#then method. Failing or cancelling the chain then cancels all downstream work, and so this is intended behaviour.

The ticket suggests 2 approaches:

If you really want APPEND's behavior, then another thing you could do is to check for WorkStatuses of the WorkRequests, and if (all of them happened to be cancelled) use REPLACE instead of APPEND. Bear in mind, this is inherently racy, because your WorkRequests might not have cancelled yet. So make sure you have some synchronization primitives around your use of WorkManager's APIs.

and

The simplest way to do this is to not actually return Result.FAILED; if you always return SUCCEEDED and return the actual pass/fail bit (if needed) in the output data, you can make sure the chain always keeps running.

Which is what I'm already doing. Hope this helps someone else.

Important update regarding that matter: https://developer.android.com/reference/kotlin/androidx/work/ExistingWorkPolicy#append_or_replace

APPEND_OR_REPLACE

enum val APPEND_OR_REPLACE : ExistingWorkPolicy

If there is existing pending (uncompleted) work with the same unique name, append the newly-specified work as the child of all the leaves of that work sequence. Otherwise, insert the newly-specified work as the start of a new sequence.

Note: If there are failed or cancelled prerequisites, these prerequisites are dropped and the newly-specified work is the start of a new sequence.

That's probably the team's response to exactly this issue. This new ExistingWorkPolicy is available in version 2.4.0-alpha01.

EDIT AFTER FURTHER TESTING...

So it turns out the only thing this fixes is not being able to reuse the UniqueWorkContinuation if for some reason it failed once. However, the main feature, e.g. being able to cancel a single WorkChain when many Workchains are queues with the same unique name still doesn't work. Typically, consider: WorkChainA with CompressWorkerA and UploadWorkerA and then queuing WorkChainB with CompressWorkerB and UploadWorkerB, all under the same UniqueWorkName. Canceling or failing any Worker in WorkChainA will cause WorkChainB never to run... Thus we still have to make sure to return Result.success() every time. And don't use any cancelWorkByTag or cancelWorkById withing the UniqueWork!

Stacked with the same, thanks for your investigation on this problem.

After playing a while to find best solution, currently this one works for me

startUpload() {

   workManager.pruneWork()
   //Prunes all eligible finished work from the internal database.

   photoAdapter.data.forEach {
      val workRequest = OneTimeWorkRequestBuilder<FileUploadWorker>()
            .setInputData(workDataOf(IMAGE_PATH_PROP to it.path))
            .addTag(UPLOADER_TAG)
            .build()

      workManager.enqueueUniqueWork(UNIQUE_WORK_TAG, ExistingWorkPolicy.APPEND, workRequest)
   }
}

calling "pruneWork" makes it to forget about cancelled or failed works. But, I guess, this solution might have caveats so be carefull.

Another idea can look like this:

workManager.getWorkInfosForUniqueWork(UNIQUE_WORK_TAG).get().forEach {

            if ( it.state == WorkInfo.State.FAILED || it.state == WorkInfo.State.CANCELLED) {
                //Redefine your UNIQUE_WORK_TAG
                UNIQUE_WORK_TAG = UUID().toString()
                return
            }
        }

try to find failed WorkInfo's and "switch" to new unique work tag. Better aproaches appreciated.

p.s. "androidx.work:work-runtime-ktx:2.3.4" does't have new ExistingWorkPolicy apart from (KEEP,REPLACE,APPEND)

Related