NSURLSessionDownloadTask continues download while suspended - ios

I'm using NSURLSessionDownloadTask to download files. At one point in the application it is necessary to pause the download for a short time and then resume it. I tried using the suspend method for pausing the download but the task continuous to download. Apple documentation for NSURLSessionTask's method suspend states:
A task, while suspended, produces no network traffic and is not subject to timeouts. A download task can continue transferring data at a later time. All other tasks must start over when resumed.
This seems to indicate that what I'm doing is right, but the result is not as expected. I was able to reproduce it with the following code in the Playground.
import UIKit
let backgroundSessionConfiguration = URLSessionConfiguration.background(withIdentifier: "some-identifier")
let backgroundSession = Foundation.URLSession(configuration: backgroundSessionConfiguration, delegate: nil, delegateQueue: nil)
let request = URLRequest(url: URL(string: "http://google.com")!)
let task = backgroundSession.downloadTask(with: request)
task.state.rawValue
task.resume()
task.state.rawValue
task.suspend()
task.state.rawValue
sleep(10)
task.state.rawValue
The expected result for task states would be 1, 0, 1, 1 (0 - downloading, 1 - suspended, 2 - cancelled, 3 - done). Actual result is 1, 0, 1, 3. This means that although the task is suspended it still continues to download and marks itself done once the download is finished.
The setup in the app is more complicated and the task states cannot always be replicated (in the Playground the results seem always the same) but the result after suspending the task is always that the file is successfully downloaded (including that the progress callback is called).
The question:
Am I doing something wrong? Have I misunderstood the Apple docs and it's not possible to pause a download task this way?
My first intuition when faced with this issue in the app was that I was calling resume somewhere else that resulted in the download to continue, but this does not seem to be the case after debugging the code + the Playground findings support this as well.

Probably not the answer you're looking for, but an Apple Dev Relations representative has confirmed in their dev forums that background downloads can in some situations not be really suspended by using suspend:
[...] 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.
They go on by suggesting to use cancel(byProducingResumeData:) instead:
if you're implementing a long-term pause (for example, the user wants to pause a download), you'd be better off calling -cancelByProducingResumeData:.

Related

Background URLSession stuck

I'm uploading a lot of files to a server using URLSession, because the uploaded files can be big I'm using URLSessionConfiguration.background so that my uploads can continue in the background.
My url session is declared like this:
urlSessionConfiguration = URLSessionConfiguration.background(withIdentifier: UploadQueue.uploadQueueIdentifier)
urlSessionConfiguration.sessionSendsLaunchEvents = true
urlSessionConfiguration.shouldUseExtendedBackgroundIdleMode = true
urlSessionConfiguration.allowsCellularAccess = true
urlSessionConfiguration.sharedContainerIdentifier = appGroup
backgroundUploadSession = URLSession(configuration: urlSessionConfiguration, delegate: self, delegateQueue: nil)
URLSessionUploadTask are always created in the foreground (we use background session only to ensure that tasks can finish).
var request = try! URLRequest(url: url, method: .put)
backgroundUploadSession.uploadTask(with: request, fromFile: fileUrl).resume()
All the necessary delegates are implemented and called normally when uploading ~ 100 files and staying in the foreground.
However when uploading a lot of files (~1000), the first files upload correctly but after some time the session seems "stuck" and no callback are delivered. (Still with the app in the foreground)
I noticed that if I just wait the upload restarts after ~5 minutes.
I tried replacing URLSessionConfiguration.background(withIdentifier: UploadQueue.uploadQueueIdentifier) with URLSessionConfiguration.default and it's working perfectly in the foreground.
Is it a bug with with URLSessionConfiguration.background or am I doing something wrong ?
After investigating there were multiple problems:
We were expecting that URLSessionConfiguration.httpMaximumConnectionsPerHost would limit the number of open connections. However this limit doesn't seem to be respected when using http/2 or when the server is behind a proxy.
Solution: Do your own connection limiting (eg. with DispatchGroup)
Even when limiting the number of concurrent uploads, the background sessions seem stuck after some time even when the app is in the foreground. There is a rate limiter for background session but the documentation says it is not endorsed while the app is in the background.
Solution: This was discussed on Apple Developer Forum and this is a bug in the CF Network Framework. A bug report has been made. For the time being, the solution is to simply not create too much background session with small files.

iOS URLSession Initiate DownloadTask in Background

I am trying to build a sequential download manager where the user can initiate up to 1000 download tasks at once but only 2 will actually download and the rest will be put on hold, until one of the two finishes so that another one of the 998 can start.
Because there is no way to add a downloadTask then have that task wait for others to complete then automatically start, I have built a download queue myself and will only create new downloadTask when the old one completes.
I have read the documentations and am well aware of the mechanism of iOS's handling of background download event, and what methods I need to implement in order to handle them correctly. However, I am unable to find anything on whether it is safe and reliable to start a new downloadTask when the old one completes IN BACKGROUND.
The Main Question:
When iOS relaunches my app in background to inform me that my download tasks are finished, can I reliably create a new downloadTask and add it to the current session? If so, when that task finishes as well, will the system relaunch my app AGAIN to tell me it's finished? If so, then I can create an infinite loop of adding new tasks when the old backgroundTask finish.
Code Example
Would the following code reliable to get the entire download queue downloaded(say, 1000 items) if I download ONLY one item at a time and meanwhile the app stays entirely in background?
extension MyClass: URLSessionDelegate {
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
// This is called when background tasks are done
// Let's add a new background tasks WHILE app has just been
// relaunched IN BACKGROUND
if let nextURL = myURLQueue.removeFirst() {
session.downloadTask(with: URLRequest(url: nextURL))
}
DispatchQueue.main.async {
if let appDelegate = UIApplication.shared.delegate as? AppDelegate,
let completionHandler = appDelegate.backgroundSessionCompletionHandler {
appDelegate.backgroundSessionCompletionHandler = nil
completionHandler()
}
}
}
}
You are creating the URLSessionDownloadTask, but never starting it with resume().
FWIW, even if you fix the above, you should recognize that requests that are submitted while the app is running in background mode will always be submitted in discretionary mode. As the docs for isDiscretionary say:
For transfers started while your app is in the background, the system always starts transfers at its discretion—in other words, the system assumes this property is true and ignores any value you specified.
Also, recognize that having the system fire up your app every time a request is done in order to submit the next request is both inefficient (because your app will constantly fire up and go back into background) and slow (because concurrent requests reduce latency effects). It’s generally better to just submit all of the requests up front, while the app is active, and let the background session manage how many it will allow to run concurrently.

Does NSURLSessionDataTask get paused on background?

I want to know about the behaviour of NSURLSessionDataTask in background.
Does NSURLSessionDataTask get paused right away (as soon as the app enters background)? or does iOS give some time hopefully 30 seconds or so until the response ?
No, You will not continue with NSURLSessionDataTask in background mode with defaultSessionConfiguration !
If you want to continue execution in background then you need to configure session with backgroundSessionConfiguration.
When you are using backgroundSessionConfiguration you can't send data directly to server, you have to give file url and from that url you have to send bytes or chunks to server, and you have to use uploadTask or downloadTask with backgroundSessionConfiguration!!
If you wants little time after entering in background then you can use UIBackgroundTaskIdentifier. These are very huge concepts to explain every thing here, It's better that you read documentations from apple or some tutorial for that!!! You should refer Apple Documentation for Background Execution and Apple documentation for NSURLSession !

NSURLSessionDownloadTask issues with Storage almost full disk warnings

I'm having issues with handling "out of space" / "full disk" errors on ios with NSURLSessionDownloadTask
If the disk is full due to downloads done in the app I get a call to
URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?)
with the error having domain NSPOSIXErrorDomain and error code ENOSPC
But this only happens once for a task, not for all running
Ex: If I have 3 tasks running at the time, I only get this for one of them and the other 2 remain in the state Running
They don't receive any bytes, but they don't fail either.
Moreovever, calling cancel on any of those tasks changes their state from Running to Cancelling and they remain like this indefinitely.
My solution was, when I receive this error the first time, to call invalidateAndCancel for the session and handle failure for all running tasks.
This seems to work when the "full disk" is caused by the downloads made by the app.
But if "full disk" error is caused by external downloads (Ex: iTunes file sharing, downloading Podcasts, other apps downloading) I receive no error in the app
All my download tasks remain in Running, or Cancelling (if I try to cancel them)
They don't download anything, they don't fail with any of the callbacks for NSURLSessionDelegate or NSURLSessionDownloadDelegate
Before starting a download, I check the available space available on device
I also take into consideration the currently Running tasks
But I have no control over other downloads on the device that might end up triggering the "full disk" warning
How can I handle these cases?
Are the download tasks expected to remain in Running state though they are not downloading anymore?
Shouldn't I get a didCompleteWithError call with (NSPOSIXErrorDomain, ENOSPC) for each task, or for the session at least?
Or at least shouldn't I be able to successfully cancel them? and get a didCompleteWithError call anyway?
Is there a delegate call I'm missing, one that would let me know it's time to close all running tasks?
I'm using a shared background session for more background download tasks
The download tasks are created with NSURLSession's:
func downloadTaskWithRequest(_ request: NSURLRequest) -> NSURLSessionDownloadTask
The sesion configuration is created using NSURLSessionConfiguration's:
class func backgroundSessionConfigurationWithIdentifier(_ identifier: String) -> NSURLSessionConfiguration
The seesion is created using, NSURLSession's
init(configuration configuration: NSURLSessionConfiguration,
delegate delegate: NSURLSessionDelegate?,
delegateQueue queue: NSOperationQueue?)
I'm using a NSOperationQueue with a maxConcurrentOperationCount of 3
I have implemented my NSURLSessionDelegate and NSURLSessionDownloadDelegate
Tasks seem to run fine in foreground and background.
Thanks
Ps: using Xocde7, ios9 sdk, tested on an ios9 device
I had also asked Apple about this issue and the answer came back:
Seems like, because the background session uses the disk to persist its state, behaviour when the disk is full is not exactly reliable to return the ENOSPC
And the handling of full disk case is the developer's responsibility for now by:
checking for disk space before you start a download
monitor disk space when your app is downloading — If your app is in the foreground, or your app gets resumed (or relaunched) in the background, actively monitor the disk space available. If it’s below some threshold, suspend your downloads (by calling -cancelByProducingResumeData:, so you can resume them later on).
For a detailed answer check:
https://forums.developer.apple.com/thread/43263

downloadTaskWithRequest confusing behaviour (swift , IOS )

I do not understand the following and hope some does :-)
I am downloading a file from an url (10 minutes downloadtime), which works fine, when the download completes.
BUT:
When I simulate a crash or internet-interruption after a minute and restart the app again, it behaves strange to me.
In this case when restarting my app to download again with same sessionid, it seems that 2 download tasks are working in parallel. I recognizue this with a jumping progressbar from 10% to 0% and back. One, which starts from scratch and one, which I guess continious the old transfer. (not sure). I can restart again and then there is one more in the queue.
Can someone confirm this behaviour and does someone know how I can:
- continue only the interrupted download task (preferred :-) )
- or how can I start from scratch only.
Here my code for downloading, which works fine without any interruption.
func download_file(sURL: String, sToLocation: String) {
println("start downloading ...");
println("sURL : " + sURL + " sToLocation : " + sToLocation);
bytesDownloaded=0;
var delegate = self;
delegate.storePath=sToLocation;
delegate.progressView=progressView;
struct SessionProperties {
static let identifier : String! = "url_session_background_download"
}
var configuration = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier(SessionProperties.identifier)
var backgroundSession = NSURLSession(configuration: configuration, delegate: delegate, delegateQueue: nil)
//myURLSession = NSURLSession(configuration: configuration, delegate: delegate, delegateQueue: nil)
var url = NSURLRequest(URL: NSURL(string: sURL)!)
var downloadTask = backgroundSession.downloadTaskWithRequest(url)
//downloadTask.cancel()
downloadTask.resume()
}
Update
When I am using a different sessionid, the Download starts from scratch. The previously started download task runs still in the background. So I am wondering why can't I resume the previously download task by using the old sessionid without starting a new download in parallel ?
The whole point of using a download task is so that downloads can continue even if your app isn't running or if it crashes. You don't need to resume the download. It is actually happening in a separate background daemon. You just need to re-create the session using the same ID.
After you re-create the session, any existing downloads are automatically associated with the new session, and your delegate methods are called whenever the task completes.
There's also a little bit of magic that you need to implement for handling background launches if your app isn't running when the download finishes. See URL Session Programming Guide for details.
In addition to what dgatwood said, remember to have to have your app delegate implement application:handleEventsForBackgroundURLSession:completionHandler:, which is called if the download finishes and your app isn't running at the time. When this method is called (again, only if download finishes when your app wasn't running), you should (a) save the completionHandler; (b) instantiate the background session with the same identifier; (c) let your NSURLSessionDownloadDelegate method be called (at which point which didFinishDownloadingToURL: is called) and when (d) URLSessionDidFinishEventsForBackgroundURLSession is called, if you have a completionHandler saved from when handleEventsForBackgroundURLSession was called, then this is the appropriate time to call this saved completionHandler.
The basic idea is as follows: If your app wasn't already running when the download finishes, the OS seamlessly starts your app in the background (unbeknownst to the end user), providing a reference to this completionHandler, you then have the app do all that it needs to do to move this downloaded file to its new location, and when it's all done, you call the saved completionHandler to let the OS know that you're all done handling the downloaded file in the background execution and the app can safely be suspended again (i.e. to avoid keeping the app running in the background, adversely affecting the UX and battery for the user).
Obviously, if the app happens to be running and has the NSURLSession already instantiated, you won't see these background related events taking place. If the app was running when the download finishes, it behaves much like a foreground NSURLSession and the above is not called upon. But you need that logic in case your app wasn't running when the download finishes.
See Downloading Content in the Background section of The App Programming Guide for iOS, which describes this process. Also refer to WWDC 2013 video in What’s New in Foundation Networking (it's covered later in the video).

Resources