Nominalista
Nominalista

Reputation: 4840

Combining WorkContinuation doesn't work as expected

I've encountered behavior of WorkManager (version 2.0.1) that I cannot understand. Unfortunately this behavior leads to issues in my application. To illustrate my problem, I will use a simpler example.

Let's assume with have three Worker implementations - UniqueWorker1, UniqueWorker2 and FinishingWorker.

class UniqueWorker1(context: Context, workerParams: WorkerParameters) :
    Worker(context, workerParams) {

    override fun doWork(): Result {
        if (runAttemptCount == 0) {
            Log.d("UniqueWorker1", "First try.")
            return Result.retry()
        }
        Log.d("UniqueWorker1", "Second try")
        return Result.success()
    }
}

class UniqueWorker2(context: Context, workerParams: WorkerParameters) :
    Worker(context, workerParams) {

    override fun doWork(): Result {
        Log.d("UniqueWorker2", "doWork")
        return Result.success()
    }
}

class FinishingWorker(context: Context, workerParams: WorkerParameters) :
    Worker(context, workerParams) {
    override fun doWork(): Result {
        Log.d("FinishingWorker", "doWork")
        return Result.success()
    }
}

As you can see, the first worker succeeds after second run attempt. Others just log the message and return successful result.

Now I'm enqueueing these workers in two ways. Firstly I start UniqueWorker1 as the unique work and tell WorkManager to run FinishingWorker when UniqueWorker1 succeeds.

val uniqueWorker1 = OneTimeWorkRequest.Builder(UniqueWorker1::class.java).build()
val finishingWorker = OneTimeWorkRequest.Builder(FinishingWorker::class.java).build()

val uniqueWorkContinuation = WorkManager.getInstance()
    .beginUniqueWork("UniqueWorker", ExistingWorkPolicy.KEEP, uniqueWorker1)

val continuations = listOf(uniqueWorkContinuation)

WorkContinuation.combine(continuations)
    .then(finishingWorker)
    .enqueue()

The second way looks like that: I combine unique works of UniqueWork1 and UniqueWork2. Then I tell WorkManager to run FinishingWorker when both works complete.

val uniqueWorker1 = OneTimeWorkRequest.Builder(UniqueWorker1::class.java).build()
val uniqueWorker2 = OneTimeWorkRequest.Builder(UniqueWorker2::class.java).build()
val finishingWorker = OneTimeWorkRequest.Builder(FinishingWorker::class.java).build()

val uniqueWorkContinuation1 = WorkManager.getInstance()
    .beginUniqueWork("UniqueWorker1", ExistingWorkPolicy.KEEP, uniqueWorker1)
val uniqueWorkContinuation2 = WorkManager.getInstance()
    .beginUniqueWork("UniqueWorker2", ExistingWorkPolicy.KEEP, uniqueWorker2)

val continuations = listOf(uniqueWorkContinuation1, uniqueWorkContinuation2)

WorkContinuation.combine(continuations)
    .then(finishingWorker)
    .enqueue()

Now imagine such case. I start workers in a first way. The UniqueWorker1 retries because it's his first run attempt. We have 30 seconds to wait (with the default BackoffPolicy values). Before it retries, I start workers in a second way. The UniqueWorker1 is not enqueued (because it has been already started) but UniqueWorker2 starts its work. Now after 30 seconds, UniqueWorker1 succeeds, the WorkManager starts FinishingWorker, because of the first way work combination. The problem is that WorkManager doesn't start FinishingWorker for the second time. Why it should start FinishingWorker for the second time? Because work combination in a second way tells to start FinishingWorker when UniqueWorker1 succeeds and UniqueWorker2 succeeds. UniqueWorker2 succeeded immediately and UniqueWorker1 succeeded after 30s.

At the beginning I thought that when WorkerManager sees that when one of the works in work combination is already enqueued, it won't finish and won't run request from then method. But I checked this in a simpler example and it worked.

So the output of the situation I described looks like that:

// Run workers in a first way
D/UniqueWorker1: First try.
I/WM-WorkerWrapper: Worker result RETRY for Work [ id=7e2fe6b4-4c8e-42af-8a13-244c0cc30059, tags={ UniqueWorker1 } ]
// Run workers in a second way before 30s will pass
E/WM-EnqueueRunnable: Prerequisite b98a6246-28d4-4b25-ae50-ec3dda6cd3ac doesn't exist; not enqueuing
E/WM-EnqueueRunnable: Prerequisite 02d017e7-30b0-4038-9b44-a6217da3979c doesn't exist; not enqueuing
D/UniqueWorker2: doWork
I/WM-WorkerWrapper: Worker result SUCCESS for Work [ id=ce9810cd-9565-4cad-b7d1-9556a01eae67, tags={ UniqueWorker2 } ]
// 30s passed
D/UniqueWorker1: Second try
I/WM-WorkerWrapper: Worker result SUCCESS for Work [ id=7e2fe6b4-4c8e-42af-8a13-244c0cc30059, tags={ UniqueWorker1 } ]
I/WM-WorkerWrapper: Setting status to enqueued for c2ac89de-3a67-496f-93e6-037d85d11646
I/WM-WorkerWrapper: Worker result SUCCESS for Work [ id=c2ac89de-3a67-496f-93e6-037d85d11646, tags={ androidx.work.impl.workers.CombineContinuationsWorker } ]
I/WM-WorkerWrapper: Setting status to enqueued for 3287bbec-b1c4-488a-b64b-35e0e6b58137
D/FinishingWorker: doWork
I/WM-WorkerWrapper: Worker result SUCCESS for Work [ id=3287bbec-b1c4-488a-b64b-35e0e6b58137, tags={ FinishingWorker } ]

As you can see FinishingWorker was enqueued only once. Sorry for the long explanation but this example shows exactly my problem. It's a serious issue for me because some of the important workers are not enqueued.

Question

Can someone explain the reason of such behavior? Is it intended behavior of WorkManager or is it a bug?

Upvotes: 4

Views: 1077

Answers (1)

SumirKodes
SumirKodes

Reputation: 563

I think there is some confusion around how you are approaching unique work so it's a little hard to reason about what you're doing.

You've got the following sequences:

  1. "UniqueWorker": UniqueWorker1 -> FinishingWorker
  2. "UniqueWorker1": UniqueWorker1
  3. "UniqueWorker2": UniqueWorker2
  4. (unnamed): combine(UniqueWorker1, UniqueWorker2) -> FinishingWorker

First of all, sequence 1 has nothing to do with the rest of your code. Its got its own name; nothing about it has anything to do with the rest of it. It will try to execute independently and will succeed on its second try when it returns a success and then executes FinishingWorker.

So let's move on to the rest of it. This code will display the error message "Prerequisite [something] does not exist; not enqueuing" if you run your second method twice in a row (without even enqueuing the first sequence). That's because you are try to enqueue the UniqueWorkers twice with an ExistingWorkPolicy.KEEP. The second time, the policy will kick in and not enqueue anything new. At that point, when you decide to enqueue the FinishingWorker, it will have no parents. SO this behavior is working as intended.

It looks like you have some confusion about how unique works are supposed to work. I think the short answer for you is that each logical grouping of work should all have the same unique name. Otherwise you will run into strange issues like this. You can, for example, rewrite the second method to be something like this:

WorkManager.getInstance(context)
  .beginUniqueWork("second_method_name", KEEP, listOf(uniqueWork1, uniqueWork2))
  .then(finishingWork)
  .enqueue()

I'd suggest further reading here: https://developer.android.com/topic/libraries/architecture/workmanager/how-to/unique-work and also the API documentation for:

public abstract WorkContinuation beginUniqueWork (String uniqueWorkName, 
                ExistingWorkPolicy existingWorkPolicy, 
                List<OneTimeWorkRequest> work)

Upvotes: 5

Related Questions