Monday, 1 July 2019

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?



from Combining WorkContinuation doesn't work as expected

No comments:

Post a Comment