I need to pause download tasks and resume it even after app restarted. But I am unsure which method should I use, suspend or cancelByProducingResumeData.
With cancelByProducingResumeData I can get the partially downloaded data and recreate download task with it. However I have to manually manage the data, save it to file, read it back, and recreate the task and ensure the new task doesn't fail.
With suspend, I can pause and resume the download task. But can I resume this task after the app is restarted? I am using background session so tasks are preserved across restart.
cancelByProducingResumeData have requirements for it to work, does those requirements also applies to suspend/resume? Or suspend/resume is only mean for "temporarily suspends a task" as the document said?
You're overthinking the problem. The "resume data" for a download task is not the data that the task has received up to that point. It is a tiny blob of bookkeeping data—the sort of thing that you'd typically throw into an array in NSUserDefaults.
With that said, to answer the original question, a task is only valid within the context of a session. So for a foreground session, once your app quits, the session ceases to exist, so it is no longer possible to gain access to tasks in that session. Therefore, it is not possible to resume a suspended task after you relaunch the app because the task no longer exists (because its session no longer exists).
For a background session, you'd pretty much have to ask somebody on the Foundation Networking team to get an answer to that one, because it depends on the extent to which you can recreate a session after the fact. However, my guess is that it probably won't work there, either, and if it does, you should consider it unsupported.
After some research on apple developer forms, I found this
Tasks suspension is rarely used and, when it is, it's mostly used to temporarily disable callbacks as part of some sort of concurrency control system. That's because a suspended task can still be active on the wire; all that the suspend does is prevent it making progress internally, issuing callbacks, and so on.
OTOH, if you're implementing a long-term pause (for example, the user wants to pause a download), you'd be better off calling -cancelByProducingResumeData:.
So suspend may not actually stop downloading and I should use cancelByProducingResumeData: for long-term pause.
Related
What happens to the current tasks present in the session when we do reset(completionHandler:) on UrlSession.
Also if it cancels the current tasks then how do we wait for the completion of the current tasks before calling reset(completionHandler:)
When you call URLSession reset, all running tasks continue to run.
A common use case to use reset is, where you want to clear all data associated to the signed-in user for example - a "sign-out" feature. Requests resumed before calling reset may use the old URL Credential store, but yet they may use the new URL cache when the response completes. This is certainly NOT what you want.
So, a more robust way to accomplish this is as follows:
prevent new tasks to be started (avoiding any data race issues)
clear all data associated to the old "session environment" - which may include access tokens and user data etc.
cancel all running tasks (asynchronously - fire & forget), then
call reset, then
enable resuming tasks.
New tasks will use the new "session environment", previously resumed tasks will complete with a cancellation error.
The first bullet point is probably the most complex one, since you need to ensure that you even do not create requests using data associated to the old session environment. That may be solved using a network layer that has a feature where incoming high level "API requests" will be queued, and that queue can be suspended and resumed.
Currently what I want to achieve is download files from an array that download only one file at a time and it still performs download even the app goes to the background state.
I'm using Rob code as stated in here but he's using URLSessionConfiguration.default which I want to use URLSessionConfiguration.background(withIdentifier: "uniqueID") instead.
It did work in the first try but after It goes to background everything became chaos. operation starts to download more than one file at a time and not in order anymore.
Is there any solution to this or what should I use instead to achieve what I want. If in android we have service to handle that easily.
The whole idea of wrapping requests in operation is only applicable if the app is active/running. It’s great for things like constraining the degree of concurrency for foreground requests, managing dependencies, etc.
For background session that continues to proceed after the app has been suspended, though, none of that is relevant. You create your request, hand it to the background session to manage, and monitor the delegate methods called for your background session. No operations needed/desired. Remember, these requests will be handled by the background session daemon even if your app is suspended (or if it terminated in the course of its normal lifecycle, though not if you force quit it). So the whole idea of operations, operation queues, etc., just doesn’t make sense if the background URLSession daemon is handling the requests and your app isn’t active.
See https://stackoverflow.com/a/44140059/1271826 for example of background session.
By the way, true background sessions are really useful when download very large resources that might take a very long time. But it introduces all sorts of complexities (e.g., you often want to debug and diagnose when not connected to the Xcode debugger which changes your app lifecycle, so you have to resort to mechanisms like unified messaging; you need to figure out how to restore UI if the app was terminated between the time the requests were initiated and when they finished; etc.).
Because of this complexity, you might want to consider whether this is absolutely needed. Sometimes, if you only need less than 30 seconds to complete some requests, it’s easier to just ask the OS to keep your app running in the background for a little bit after the user leaves the app and just use standard URLSession. For more information, see Extending Your App's Background Execution Time. It’s a much easier solution, bypassing many background URLSession hassles. But it only works if you only need 30 seconds or less. For larger requests that might exceed this small window, a true background URLSession is needed.
Below, you asked:
There are some downside with [downloading multiple files in parallel] as I understanding.
No, it’s always better to allow downloads to progress asynchronously and in parallel. It’s much faster and is more efficient. The only time you want to do requests consecutively, one after another, is where you need the parse the response of one request in order to prepare the next request. But that is not the case here.
The exception here is with the default, foreground URLSession. In that case you have to worry about latter requests timing out waiting for earlier requests. In that scenario you might bump up the timeout interval. Or we might wrap our requests in Operation subclass, allowing us to constrain not only how many concurrent requests we will allow, but not start subsequent requests until earlier ones finish. But even in that case, we don’t usually do it serially, but rather use a maxConcurrentOperationCount of 4 or something like that.
But for background sessions, requests don’t time out just because the background daemon hasn’t gotten around to them yet. Just add your requests to the background URLSession and let the OS handle this for you. You definitely don’t want to download images one at a time, with the background daemon relaunching your app in the background when one download is done so you can initiate the next one. That would be very inefficient (both in terms of the user’s battery as well as speed).
You need to loop inside an array of files and then add to the session to make it download but It will be download asynchronously so it's hard to keeping track also since the files are a lot.
Sure, you can’t do a naive “add to the end of array” if the requests are running in parallel, because you’re not guaranteed the order that they will complete. But it’s not hard to capture these responses as they come in. Just use a dictionary for example, perhaps keyed by the URL of the original request. Then you can easily look up in that dictionary to find the response associated with a particular request URL.
It’s incredibly simple. And we now can perform requests in parallel, which is much faster and more efficient.
You go on to say:
[Downloading in parallel] could lead the battery to be high consumption with a lot of requests at the same time. that's why I tried to make it download each file one at a time.
No, you never need to perform downloads one at a time for the sake of power. If anything, downloading one at a time is slower, and will take more power.
Unrelated, if you’re downloading 800+ files, you might want to allow the user to not perform these requests when the user is in “low data mode”. In iOS 13, for example, you might set allowsExpensiveNetworkAccess and allowsConstrainedNetworkAccess.
Regardless (and especially if you are supporting older iOS versions), you might also want to consider the appropriate settings isDiscretionary and allowsCellularAccess.
Bottom line, you want to make sure that you are respectful of a user’s limited cellular data plan or if they’re on some expensive service (e.g. connecting on an airplane’s expensive data plan or tethered via some local hotspot).
For more information on these considerations, see WWDC 2019 Advances in Networking, Part 1.
I have a CPU task that needs to occur when the app is running in the background (either by way of fetch or silent notification). This task takes about 1s when running in the foreground but about 9s when running in the background. It's basically saving out ~100K textual entries to a database. Whether I use FileHandle operations or a Core Data sqlite solution, the performance profile is about the same (Core Data is a little slower surprisingly).
I don't really want to get into the specifics of the code. I've already profiled the hell out of it and in the foreground it's quite performant. But clearly when the app is running in the background it's being throttled by iOS to the tune of a 9x slowdown. This wouldn't be such a big issue except in response to a silent notification iOS only gives the app 30-40s to complete and this 9s task can put it over the limit. (The rest of it is waiting on subsystems that I have no control over.)
So the question:
Is there any way to tell iOS Hi, yes, I'm in the background but I really need this chunk of code to run quickly and avoid your throttling ? FWIW I'm already running in a .userInitiated qos dispatch queue:
DispatchQueue.global(qos: .userInitiated).async {
// code to run faster goes here
}
Thanks!
First, no. The throttling is on purpose, and you can't stop it. I'm curious if using a .userInitiated queue is actually improving performance much over a default queue when you're in the background. Even if that's true today, I wouldn't bet on that, and as a rule you shouldn't mark something user initiated that is clearly not user initiated. I wouldn't put it past Apple to run that queue slower when in the background.
Rather than asking to run more quickly, you should start by asking the OS for more time. You do that by calling beginBackgroundTask(expirationHandler:) when you start processing data, and then call endBackgroundTask(_:) when you're done. This tells the OS that you're doing something that would be very helpful if you could complete, and the OS may give you several minutes. When you run out of whatever time it gives you, then it'll call your expirationHandler, and you can save off where you were at that point to resume work later.
When you run out of time, you're only going to get a few seconds to complete your expiration handler, so you may not be able to write a lot of data to disk at that point. If the data is coming from the network, then you address this by downloading the data first (using a URLSessionDownloadTask). These are very energy efficient, and your app won't even be launched until the data is finished downloading. Then you start reading and processing, and if you run out of time, you squirrel away where you were in user defaults so you can pick it up again when you launch next. When you're done, you delete the file.
AppDelegate.applicationWillTerminate is called when the application is about to terminate. In this function, I am issuing a network request via Alamofire, to notify the server that the app is terminating. Alamofire's response handler is never invoked. It looks to me like the termination completes before the completion handler is invoked.
Alamofire's completion handlers appear to run on the main thread. I found documentation saying that the app is responsible for draining the main queue: "Although you do not need to create the main dispatch queue, you do need to make sure your application drains it appropriately. For more information on how this queue is managed, see Performing Tasks on the Main Thread." (From https://developer.apple.com/library/content/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html) And this is where I am stuck.
How do I drain the main thread? I need to ensure that this last Alamofire request runs before the main thread exits.
Don't worry about “draining” the main thread. The problem is more simple than that. It's just a question of how to do something when your app is leaves the “foreground”/“active” state.
When a user leaves your app to go do something else, it is generally not terminated. It enters a “suspended” state where it remains in memory but does not execute any code. So when the app is suspended, it cannot process your request (but the app isn't yet terminated, either).
There are two approaches to solve this problem.
You could just request a little time to finish your request (see Extending Your App's Background Execution Time). By doing this, your app is not suspended, but temporarily enters a "background" state, where execution can continue for a short period of time.
The advantage of this approach is that it is fairly simple process. Just get background task id before starting the request and you tell it that the background task is done in the Alamofire completion handler.
The disadvantage of this approach is that you only have 30 seconds (previously 3 minutes) for the request to be processed. If you have a good connection, this is generally adequate. But if you don't have a good network connection in that period, the request might never get sent.
The second approach is a little more complicated: You could make your request using a background URLSession. In this scenario, you are effectively telling iOS to take over the handling of this request, and the OS will continue to do so, even if your app is suspends (or later terminated during its natural lifecycle).
But this is much more complicated than the first approach I outlined, and you lose much of the ease and elegance of Alamofire in the process. You can contort yourself to do it (see https://stackoverflow.com/a/26542755/1271826 for an example), but it is far from the obvious and intuitive interface that you're used to with Alamofire. For example, you cannot use the simple response/responseJSON completion handlers. You can only download/upload tasks (no data tasks). You have to write code to handle the OS restarting your app to tell you that the network request was sent (even if you're not doing anything meaningful with this response). Etc.
But the advantage of this more complicated approach is that it is more robust. There's no 3 minute limit to this process. The OS will still take care of sending the request on your behalf whenever connectivity is reestablished. Your app may may even be terminated by that point in time, and the OS will still send the request on your behalf.
Note, neither of these approaches can handle a "force-quit" (e.g. the user double taps on the home button and swipes up to terminate the app). It just handles the normal graceful leaving of the app to go do something else.
I have problem suspending the current task being executed, I have tried to set NSOperationQueue setSuspended=YES for pausing and setSuspended=NO for resuming the process.
According to apple docs I can not suspend already executing task.
If you want to issue a temporary halt to the execution of operations, you can suspend the corresponding operation queue using the setSuspended: method. Suspending a queue does not cause already executing operations to pause in the middle of their tasks. It simply prevents new operations from being scheduled for execution. You might suspend a queue in response to a user request to pause any ongoing work, because the expectation is that the user might eventually want to resume that work.
My app needs to suspend the time taking upload operation in case of internet unavailability and finally resume the same operation once internet is available. Is there any work around for this? or I just need to start the currently executing task from zero?
I think you need to start from zero. otherwise two problems will come there. If you resume the current uploading you cant assure that you are not missed any packets or not. At the same time if the connection available after a long period of time, server may delete the data that you uploaded previously because of the incomplete operation.
Whether or not you can resume or pause a operation queue is not your issue here...
If it worked like you imagined it could (and it doesn't) when you get back to servicing the TCP connection it may very well be in a bad state, it could have timed out, closed remotely...
you will want to find out what your server supports and use the parts of a REST (or similar) service to resume a stalled upload on a brand new fresh connection.
If you haven't yet, print out this and put it on the walls of your cube, make t-shirts for your family members to wear... maybe add it as a screensaver?