Behavior of BGContinuedProcessingTask on Failure

Hi there,

First thanks for all the work on BGContinuedProcessingTask! It looks really promising.

I have a question / issue around the behavior when a BGContinuedProcessingTask expires. Here is my setup.

  • I have an app who's responsible for uploading large files in the field (AKA wifi is not expected)
  • For a given file, it can likely fail due to network conditions
  • I'm using Multipart upload though so I can retry a file to pick up where it left off.
  • I use one taskIdentifier per file, and when the file fails, I can retry the task and have it continue where it left off (I am reusing the taskIdentifier here for retries, let me know if I shouldn't be doing that)

Here is the behavior I am seeing

  1. I start an upload, it seems to be uploading normally
  2. I turn on airplane mode to simulate expiration of the task
  3. the task fails as expected after ~30 seconds, and I see the failure in my home screen.
  4. I have callbacks in the task to put my app in the proper state on expiration / failure
  5. I turn back on airplane mode and I retry the task, the way I do this is I do NOT re-register, I simply re-submit the task with the same TaskIdentifier.

What I would have expected is that the failure task is REPLACED with the new task and new progress. Instead what I see is TWO ContinuedBackgroundProcessingTasks, one in the failure state and one in progress.

My question is

  • How can I make retries reuse the same task notification item?
  • OR if that's not possible, how do I programmatically clear the task failure? I've tried cancelTask but that doesn't seem to clear it.
Answered by DTS Engineer in 868043022

I use one taskIdentifier per file, and when the file fails, I can retry the task and have it continue where it left off.

This is not what I would do. The problem here is that you're not going to be able to start new tasks once you enter the background, so you're not going to be able to retry once you enter the background. I would use the background task to handle the "whole" operation, including retries.

(I am reusing the taskIdentifier here for retries, let me know if I shouldn't be doing that.)

Yeah, don't do that. The problem is that you don't really "know" when we're "done" with any particular task ID, which means you don't know when it's "safe" to reuse that ID. These task IDs will never be user visible, so I'd just append a value to the string to force uniqueness.

Case in point on the "done" front:

What I would have expected is that the failure task is REPLACED with the new task and new progress. Instead what I see is TWO ContinuedBackgroundProcessingTasks, one in the failure state and one in progress.

The system isn't "done" tracking any particular task until it clears its UI, but that interface is managed independently of your app.

OR if that's not possible, how do I programmatically clear the task failure?

I think you're thinking about this the wrong way. If you don't want a task to be marked as "failed"... the solution is to "not" fail the task. Putting that another way, the "success" argument to "setTaskCompleted(success:)" is actually you telling the system what to do with the task you're completing, NOT communicating some higher level concept of what the task actually "did". As a more concrete example, if the user cancels a task in your app UI, you should pass in "success=true". The task may have "failed" but there's no reason to bother the user with "extra" UI for that.

I've tried cancelTask but that doesn't seem to clear it.

So, there are actually two issues here:

  1. Task cancellation applies to tasks that are "scheduled", not tasks that are actively running. Tasks that are actively running can stop using setTaskCompleted(success:). In practice, that means it's really only useful with BGContinuedProcessingTask if you're using BGContinuedProcessingTaskRequest.SubmissionStrategy.queue and submitting enough tasks that they're actively queueing.

  2. Task cancellation doesn't really apply to failed tasks, as they've already completed as far as the scheduling system is concerned. I can see why you might want to clear them, but I'm not sure how that would work in the current API.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Accepted Answer

I use one taskIdentifier per file, and when the file fails, I can retry the task and have it continue where it left off.

This is not what I would do. The problem here is that you're not going to be able to start new tasks once you enter the background, so you're not going to be able to retry once you enter the background. I would use the background task to handle the "whole" operation, including retries.

(I am reusing the taskIdentifier here for retries, let me know if I shouldn't be doing that.)

Yeah, don't do that. The problem is that you don't really "know" when we're "done" with any particular task ID, which means you don't know when it's "safe" to reuse that ID. These task IDs will never be user visible, so I'd just append a value to the string to force uniqueness.

Case in point on the "done" front:

What I would have expected is that the failure task is REPLACED with the new task and new progress. Instead what I see is TWO ContinuedBackgroundProcessingTasks, one in the failure state and one in progress.

The system isn't "done" tracking any particular task until it clears its UI, but that interface is managed independently of your app.

OR if that's not possible, how do I programmatically clear the task failure?

I think you're thinking about this the wrong way. If you don't want a task to be marked as "failed"... the solution is to "not" fail the task. Putting that another way, the "success" argument to "setTaskCompleted(success:)" is actually you telling the system what to do with the task you're completing, NOT communicating some higher level concept of what the task actually "did". As a more concrete example, if the user cancels a task in your app UI, you should pass in "success=true". The task may have "failed" but there's no reason to bother the user with "extra" UI for that.

I've tried cancelTask but that doesn't seem to clear it.

So, there are actually two issues here:

  1. Task cancellation applies to tasks that are "scheduled", not tasks that are actively running. Tasks that are actively running can stop using setTaskCompleted(success:). In practice, that means it's really only useful with BGContinuedProcessingTask if you're using BGContinuedProcessingTaskRequest.SubmissionStrategy.queue and submitting enough tasks that they're actively queueing.

  2. Task cancellation doesn't really apply to failed tasks, as they've already completed as far as the scheduling system is concerned. I can see why you might want to clear them, but I'm not sure how that would work in the current API.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Ah interesting,

Based off that, it feels like I should NOT rely on the BGContinuedProcessingTask notifications (or whatever they are called) to communicate state. It seems like instead what I should do is do something like local notifications to communicate state and handle it more in my app, is that correct?

Based off that, it feels like I should NOT rely on the BGContinuedProcessingTask notifications (or whatever they are called) to communicate state. It seems like instead what I should do is do something like local notifications to communicate state and handle it more in my app, is that correct?

This is one of those questions where the right answer really depends ENTIRELY on the exact details of what you're trying to do. The simple end of the spectrum here are things like short-lived, "single" jobs, where putting up "extra” UI to manage a single task which isn't going to take very long anyway might be unnecessary. At the other end of things, I think there are lots of situations where the work the app is doing doesn't "nicely" map directly to the task model and you'll absolutely want to use other tools/APIs to tell the user what's going on.

As one example, if an app is doing lots of small network transfers, using an individual processing task for each transfer is probably a mistake. At a technical level, there are cases where having 20+ simultaneous network transfers is perfectly reasonable, but that isn't going to look great or work great in our interface, even if we let you start that many tasks (I'm not sure what our "cap" is).

More generally, I think it's important to understand that BGContinuedProcessingTask is an interface/lifecycle API, NOT a "work scheduling" API. That is, its role in your app is much closer to UIApplication.beginBackgroundTask(..) than it is to dispatch_async(). I have forum posts here and here which talk about these issues in more detail, but the short summary is that it isn't necessary and could be a mistake to use "BGContinuedProcessingTask" as the "core" of your app’s work management architecture. It will probably work fine for simple use cases, but past a certain point I think you'll end up needlessly complicating and restricting your app’s architecture without providing any real benefit.

As a side note, it is true that our documentation and sample code do tend to look like your app should use BGContinuedProcessingTask this way. That's ENTIRELY because of the limitations inherent to sample code and documentation. Our examples tend to favor the "simplest" solution, both because it's easier to understand and because we've seen cases in the past where the more complicated solution ended up in developers’ code because they copied that implementation without considering whether it was actually appropriate.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Behavior of BGContinuedProcessingTask on Failure
 
 
Q