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

Hi there,

Following up on this, my interpretation of this response was that I could use setTaskCompleted(success: true) to avoid the UI notification of a task failure.

But when simulating a situation where the system expires the task, I still see the UI failure even if I call bgTask.setCompleted(success: true)

How can I avoid the UI notification in cases where the system kills the task?

I've filed a bug/feature request on configurability of the failure notification, FB21601088

but to clarify, in response to

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".

My understanding is that if I do setTaskCompleted(success: true), then there would not be a failure notification even in cases where the system expires the task, but that is not what I'm seeing (by debugging, it seems the failure notification is already set before the expirationHandler is called, and it seems its immutable after that).

Please let me know if I'm missing something and there is an easy way to surpress the failure notification in cases where the system expires the task.

I've filed a bug/feature request on configurability of the failure notification, FB21601088

Looking at your bug, you laid out this sequence:

  1. Start Uploading with BGContinuedProcessingTask
  2. Turn on airplane mode
  3. The task will ALWAYS fail, even if I handle it in expirationHandler and do task.setCompleted(success: true)

On it's own, that sequence shouldn't have failed. BGContinuedProcessingTask doesn't know/define what "work" is actually being done, so changing to airplane mode shouldn't have had any effect on the task itself (at least not on it's own).

Are you sure your network code isn't actually what's triggering this by failing the task on it's own when the network goes away? More specifically, it's possible that directly using the progress object of one of our network classes might end up triggering expiration when the network connection dies.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

I believe the expiration is happening from the progress not updating after ~30 seconds because there is no network (which my understanding is expected behavior, as the system can terminate the task when progress isn't updated)

The part that is not expected is, after the task expires, based on the comment above, I should be able to avoid the failure UI notification by calling task.setCompleted(success: true). But no matter what I do, I always get the "Task Failed" failure notification (which seems to happen before the expirationHandler is even called).

I'm ok that the task is expiring in this case, I would just like to avoid the failure notification and handle it myself

I'm okay that the task is expiring in this case. I would just like to avoid the failure notification and handle it myself.

This is one of those situations where you're better off altering your architecture to avoid the entire issue instead of trying to find a specific "tweak" and/or relying on us changing/fixing something. The issue here is that there are multiple routes that could all end up with the same result. For example:

(1) I initially suggested that this could be happening:

More specifically, it's possible that directly using the progress object of one of our network classes might end up triggering expiration when the network connection dies.

...since the NSProgress is a relatively "intelligent" class and, in theory, could be propagating the error itself.

(2) And then you suggested this today:

I believe the expiration is happening from the progress not updating after ~30 seconds because there is no network (which my understanding is expected behavior, as the system can terminate the task when progress isn't updated).

It's certainly possible that is happening and that the system is automatically posting the failure dialog when it does, since it considers your app "non-responsive", not "expired". I haven't specifically looked at the code, but I suspect this is what's going on, primarily because it makes the code significantly simpler and unblocks the interface more quickly.

Sidebar: Expanding on that last point for the curious, the implementation of things like this can basically either be:

Case 1:

  • At ~30s, the daemon detects the component has not updated.
  • The daemon fires off the expiration message to the app.
  • The daemon immediately posts the error, tears down the UI, and is then "done".

Case 2:

  • At ~30s, the daemon detects the component has not updated.
  • The daemon fires off the expiration message to the app.
  • The daemon sets up a timer for some interval in the future (typically ~10s).
  • If the daemon receives a reply, it cancels the timer, does whatever the reply told it to do, and is then "done”.
  • If/when the timer fires, it posts the error, tears down the UI, and is then "done".

The problem here is that case #2’s basic implementation is not just more complicated, it's also open to more weird edge cases. For example, what happens if the app is able to exit, relaunch, and then start a NEW processing task within that time window?

Those kinds of issues are why we generally prefer to "fail immediately"- it's much easier to implement and avoids having to worry about lots of weird edge cases.

Returning to the recommendation above:

"This is one of those situations where you're better off altering your architecture to avoid the entire issue instead of trying to find a specific "tweak" and/or relying on us changing/fixing something. The issue here is that there are multiple routes that could all end up with the same result."

The core problem here is that as long as you're "allowing" the failure to go to "us" first, there's always the possibility that we'll either reintroduce the same bug (due to direct changes in our code) or recreate the same behavior through a different route (like the progress case).

The other issue here is that you’re needlessly inhibiting your own implementation's robustness and flexibility. For example, you've limited your ability to retry or save data, as your app will typically be forced to suspend shortly after your task expires. While it's possible that isn't an issue in your app, you could have removed all risk by simply finishing your teardown process and then ending the task.

In any case, my recommendation for all of this is that you either end the task earlier or artificially report progress (to give yourself more time), instead of allowing your task to expire as non-responsive. Failing to report progress should be considered a failure state, not a tool for managing your app’s internal flow.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Behavior of BGContinuedProcessingTask on Failure
 
 
Q