How to guarantee new unique work will run regardless of already running work

Viewed 36

I would like to schedule work to sync user data from my application to a server. The work should run when the user saves new information within the application.

fun scheduleWork(context: Context) {
    val constraints = Constraints.Builder()
        .setRequiredNetworkType(NetworkType.CONNECTED)
        .build()

    val request = OneTimeWorkRequest.Builder(ObservationSyncWorker::class.java)
        .setConstraints(constraints)
        .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
        .build()

    WorkManager
        .getInstance(context)
        .beginUniqueWork("syncData", ExistingWorkPolicy.APPEND_OR_REPLACE, request)
        .enqueue()
}

Example of work:

override suspend fun doWork(): Result {
  // get unsync'ed data from database
  val data = repository.getLocalData()
  // Start network request to sync data
  val response = repository.sync(data)
  if (response.isSuccessful) {
    // Network request complete, save server response
    repository.save(response.body())
  }

  // Done
  return Result.success()
}

The is a chance that the user will save new data before the previous data has finished. The documentation work policy is a bit limited and want to make sure I am using the correct policy.

WorkPolicy:

public enum ExistingWorkPolicy {

    /**
     * If there is existing pending (uncompleted) work with the same unique name, cancel and delete
     * it.  Then, insert the newly-specified work.
     */
    REPLACE,

    /**
     * If there is existing pending (uncompleted) work with the same unique name, do nothing.
     * Otherwise, insert the newly-specified work.
     */
    KEEP,

    /**
     * If there is existing pending (uncompleted) work with the same unique name, append the
     * newly-specified work as a child of all the leaves of that work sequence.  Otherwise, insert
     * the newly-specified work as the start of a new sequence.
     * <br/>
     * <b>Note:</b> When using APPEND with failed or cancelled prerequisites, newly enqueued work
     * will also be marked as failed or cancelled respectively. Use
     * {@link ExistingWorkPolicy#APPEND_OR_REPLACE} to create a new chain of work.
     */
    APPEND,

    /**
     * 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.
     * <br/>
     * <b>Note:</b> If there are failed or cancelled prerequisites, these prerequisites are
     * <i>dropped</i> and the newly-specified work is the start of a new sequence.
     */
    APPEND_OR_REPLACE,
}

Using REPLACE: Since my work to synchronize data will catch all data I could use REPLACE, which will cancel the first work request, and schedule the second work request. This second work request should catch unsync'ed data from the first work that was cancelled. The benefit here might be that scheduling multiple work requests when offline (see online constraint), won't stack up children to sync.

Using KEEP: This will do nothing if existing work to sync is scheduled. This work great if offline and the user saves data multiple times. Each save of data will result in no new work being scheduled. However in the case that existing work is still in the process or sync'ing, if the user saves and new work is scheduled, I think that new work request will be ignore. This seems like a bad option for my use case.

Using: APPEND: If previous sync work failed, using APPEND will result in new work not being scheduled. Since I want all sync work to be scheduled regardless of previous work, I think this is a bad option.

Using APPEND_OR_REPLACE: This is a viable option since scheduling new work if previous work failed is ok. This option will also allow previously scheduled work to complete before more work is scheduled. This works great if the users data is in the process of sync'ing and they save new data. However saving multiple times while offline will cause multiple work to be appended. This will be wasteful as when a connection is established the first work request will sync all data, leaving the remaining work tasks with nothing to do.

For my use case I am considering REPLACE or APPEND_OR_REPLACE.

With REPLACE since work can be cancelled there is a chance that a running sync could cancel in between a valid server response and saving that response to my database.

With APPEND_OR_REPLACE no work will be cancelled. However extra work could run in some cases with nothing to sync. My worker does check if it has work to do, so this might be harmless. It seems this is the safer option.

1 Answers

It is not a clean approach, but you can try like this with APPEND_OR_REPLACE:

fun shouldEnqueueNewWork(): Boolean {
        return getNumberOfUnfinishedWorks() < 2
    }

fun getNumberOfUnfinishedWorks(): Int {
    val workInfos = workManager.getWorkInfosForUniqueWork("syncData")
    var result = 0
    val workInfoList: List<WorkInfo> = workInfos.get()
    try {
        for (workInfo in workInfoList) {
            if (!workInfo.state.isFinished) {
                result++
            }
        }
    } catch (e: ExecutionException) {
        return result
    } catch (e: InterruptedException) {
        return result
    }
    return result
}

https://developer.android.com/topic/libraries/architecture/workmanager/how-to/states-and-observation#one-time_work_states

https://developer.android.com/reference/androidx/work/WorkInfo.State#isFinished()

Related