I followed this tutorial Downloading files in background with URLSessionDownloadTask
And this Apple doc Downloading Files in the Background
I tried to setup because we have a long running API
let config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).background")
config.timeoutIntervalForRequest = 120
config.timeoutIntervalForResource = 180
But I bumped into this weird issue:
If the server does not response with any piece of data, meaning that
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64)
does not get called at all.
The app will kill off previous request and retry a new request every 66 seconds. I don't know where this number come from but from my experiment, it's roughly 66 seconds
If I set the timeoutIntervalForRequest = 10 the sure enough, the app will retry the request every 10 seconds but any attempt to set it above 66 seconds doesn't work
Not sure if anyone encounter the same issue and found a solution.
Just 1 note: the whole thing will timeout when it reach 180 seconds and the app stop retrying new requests
First, that's a bug. Please file it at bugreport.apple.com. The timeout should not be ignored like that. Of course, there's a decent chance that this is a power management issue and won't be fixed, so I wouldn't hold your breath.
Second, you're approaching the problem in a way that is pretty much guaranteed to cause problems even if the timeout bug were fixed. The fact that your server isn't sending back any bytes to keep the connection alive is, of course, the reason the iOS device is disconnecting, but even if you change that and make it send out a bogus header one byte at a time every five seconds until the data is ready, you'll still have problems.
Basically, on a mobile device, you really shouldn't keep a long-running connection open to a remote server for any reason. It massively wastes battery to keep the Wi-Fi radio on continuously, much less the cellular radio, and worse, that connection could fail at any time when the user walks out of range, switches cell sites, or otherwise momentarily loses connectivity. Networks are crap — cellular networks doubly so.
A much better approach for long-running server processing is to do it asynchronously:
Make the request to the server.
Have the server send you back a unique identifier associated with the request, and, optionally, an estimated completion time.
Wait until the estimated completion time, then ask the server how things are going (providing that unique identifier).
Continue to periodically poll the server until the server says that the task is completed (or has failed).
When the server says that the task is completed, issue a request to retrieve the results, then issue a request to free the completed results.
Periodically clean up old, uncollected results on the server with a cron job or similar.
This approach avoids keeping the radio hot except for a couple of seconds on either side of your polling requests, and it makes the timeout issue entirely moot.
Related
We need to process a lot of data in the background for iOS and came across BGProcessingTaskRequest which is intended for tasks which may take minutes. However in practice, the task is always killed after exactly 295 seconds on my iOS 14.8 device.
Is this the most amount of background processing time we can expect on iOS or are there any other ways to increase the background execution time? If not, is it possible to chain the requests by scheduling the task again in it's own handler?
As you know, they only promise that it “can run for minutes”. One would have hoped that setting the task’s requiresExternalPower, might increase the allotted time as the battery concerns are eliminated, but in my tests, the allotted time is still limited to roughly 5 minutes (tested in iOS 15).
As you suggest, you can schedule another processing request when time expires. In my experiments, though, this subsequent request took even longer before it started and expired even more quickly.
In short, you can chain requests as you suggest, but you are at the mercy of the OS.
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.
From the doc it looks like uploading a file is a good use case for using beginBackgroundTaskWithExpirationHandler. I've found that using
let uploadTask = session.uploadTask(with: request as URLRequest, fromFile: file)
uploadTask.resume()
will already run while the app is backgrounded (I'm getting upload progress pings for a while). Additionally I can set the URLSession to be backgrounded:
let config = URLSessionConfiguration.background(withIdentifier: "uploads")
session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
So what is the advantage of using beginBackgroundTaskWithExpirationHandler ? Will it extend the time I've to finish my upload? If so can I know by how much (didn't see anything about this in the doc)? Or is it just that I'll get pinged before the app stops? Should I use it in combination with a background URLSession?
Additionally the docs says that the handler will be called shortly before the app’s remaining background time reaches 0 Does it mean that the app will be terminated after that? ie can I assume that the next call will be application:didFinishLaunchingWithOptions or can it be applicationDidBecomeActive ?
So what is the advantage of using beginBackgroundTaskWithExpirationHandler ?
If you are going to use URLSessionConfiguration.background, there is no such advantage and you should not use beginBackgroundTask(expirationHandler:) at all. Your entire premise (your very first sentence) was wrong. Uploading a file is not a good use case for beginBackgroundTask(expirationHandler:). It's a good use case for URLSessionConfiguration.background. The two things have nothing to do with each other.
This background task will let your app continue to run in background after the user leaves your app for an extra 3 minutes or so (check backgroundTimeRemaining for actual value) to let your request finish. And, yes, near the end of that 3 minutes, the timeout handler will be called if you haven't yet ended the background task.
So, if you end the background task during the normal flow of your app, this timeout closure won't need to be called. This closure is solely for any quick, last minute cleanup, that you might need to do before your app stops running in the background because it timed out before you had a chance to indicate that the background task ended. It's not for starting anything new, but just any last second clean-up. And make sure to end the background task in this timeout handler ... if you don't end the background task, the OS will summarily kill your app rather than just suspending it. Often, the only thing you need to do in this timeout closure is end the background task, but if you need to do any other cleanup, this is where you can do it.
Needless to say, you must end your background task (either when the network request finishes, or in the timeout handler if your app didn't yet get a chance to end the background task in its normal flow). If you don't, your app won't just be suspended, but rather it will be killed.
Regarding making assumptions about what happens when your app is restarted by the user later, you can't make any assumes about which app delegate method will be called. Even if you gracefully ended the background task, you have no assurances that it won't get jettisoned for other reasons (e.g. memory pressure). So don't assume anything.
We're converting our CURL HTTP Get requests to native IOS code. With CURL we can set two different timeouts - CURLOPT_CONNECTTIMEOUT - how long before a call fails if it cant connect, and CURLOPT_TIMEOUT - how long before a call fails if all the data hasnt been retrieved. If the connect fails we want it to return pretty quickly (10 seconds), but we download large chunks of data possibly on slow connections so we need the completion timeout to be quite large (5 minutes).
How do we set different timeouts using NSMutableURLRequest
Currently we are setting the single timeout like this
[urlRequest setTimeoutInterval:30.0f]
Is there a way to set two separate timeouts, like CURL does ? And which timeout are we currently setting ? The connection timeout or the completion one.
Thanks
shaun
That's a really good question. The documentation on it was unclear to me:
If during a connection attempt the request remains idle for longer than the timeout interval, the request is considered to have timed out. The default timeout interval is 60 seconds.
I did find this really helpful post in the Apple Developer Forums, which an Apple employee explains:
The timeoutInterval property is equivalent to
timeoutIntervalForRequest property.
He's referencing the property on NSURLSessionConfiguration, which can be attached to an NSURLSession. If you set the timeoutInterval of an NSURLRequest, it is used as the value for timeoutIntervalForRequest on the configuration. This property's documentation does provide some insight:
The request timeout interval controls how long (in seconds) a task
should wait for additional data to arrive before giving up. The timer
associated with this value is reset whenever new data arrives. When
the request timer reaches the specified interval without receiving any
new data, it triggers a timeout.
The default value is 60.
Based on that, it seems this value is actually neither!
This is an issue that's making me question my own sanity, but I'm posting the question in case it's something real rather than a problem of my own making.
I have an iOS app that is making use of the NSURLConnection class to send a request to a webserver. The object is instantiated and instructed to call back the delegate, which receives the corresponding notifications didReceiveResponse / didReceiveData / didFinishLoading / didFailWithError. Effectively the same code that is posted on Apple's dev page for using the class. The requests are all short POST transmissions with JSON data; the responses are also JSON-formatted, and come back from an Apache Tomcat Java Servlet.
For the most part it all works as advertised. The app sends a series of requests to the server in order to start a job and poll for partial results. Most of the exhanges are short, but sometimes the results can be up to about 100-200Kb maximum when there are partial results available.
The individual pieces of data get handed back by the operating system in chunks of about 10Kb each time, give or take. The transport is essentially instantaneous, as it is talking to a test server on the LAN.
However: after a few dozen polling operations, the rate of transport grinds to a near standstill. The sequence of response/data.../finished works normally: the webserver has delivered its payload, but the iOS app is receiving exactly 2896 bytes, with a periodicity of 20-30 seconds in between chunks. It is the correct data, and waiting about 5 minutes for 130Kb of data does confirm that it's operating correctly.
Nothing I do seems to conveniently work around it. I tried switching to the "async" invocation method with a response block; same result. Talking to a remote website rather than my LAN test deployment gets the same result. Running in simulator or iPhone gets the same result. The server returns content-length and doesn't try to do anything weird like keeping the connection alive.
Changing the frequency of the polling achieves little, unless I crank up the delay in between polling to 50 seconds, then everything works fine, presumably because it only ends up polling once or twice.
A hypothesis that fits this observation is that the NSURLConnection object hangs around long after it has been released, and chews up resources. Once a certain limit is hit, the progress rate grinds to a near halt. If the slowed down connection actually completes, subsequent connections work normally again, presumably because they've been cleaned up.
So does this sound familiar to anyone?