34

I'm using the new NSURLSession API and allowing the user to download files. I'd like to try and tell my NSURLSession how many simultaneous downloads to run, but I don't see a way to do it. I'd like to try to avoid managing the download tasks myself, would be much better if I could tell the system how many to allow instead - that would be better for queuing background downloads as well when my app isn't running. Is there a way to do this?

Cory Imdieke
  • 14,140
  • 8
  • 36
  • 46

2 Answers2

23

You can set it in the NSURLSessionConfiguration object with the HTTPMaximumConnectionsPerHost property.

CouchDeveloper
  • 18,174
  • 3
  • 45
  • 67
  • 11
    So while this does work, the system still won't queue up downloads as I had wanted. If I set the max to 3 and select 5 downloads to start, the last 2 will eventually time out while the first two run. Is there a way to have the system queue the downloads? – Cory Imdieke Jan 03 '14 at 20:40
  • 10
    In the NSURLSessionConfiguration you can set *two* timeouts: one is to specify the maximum duration you accept to wait for the whole resource to finish: `timeoutIntervalForResource` - the other is related to network dropouts: `timeoutIntervalForRequest`, which gets triggered only when the underlying network does not receive any more *new* data within that period. You must set `timeoutIntervalForResource` to an appropriate duration which includes the time required to download the preceding resources. – CouchDeveloper Jan 04 '14 at 08:57
  • 5
    This is such a stupid design implementation. timeoutIntervalForResource shouldn't include the time for preceding resource downloads. What if you don't know how many preceding resources there are!? – simonthumper Apr 29 '16 at 08:47
  • @simonthumper You seem to misunderstand this. `timeoutIntervalForResource` is simply: "The maximum amount of time that a resource request should be allowed to take." This timeout starts when the tasks will be resumed. However, if previous active network tasks are still running and the maximum number of concurrent requests is reached, this task is hold back until one is finished. So, it "virtually" takes longer than expected. You can prevent this by only submitting (resuming) tasks when the current number of task is below the limit. – CouchDeveloper Apr 29 '16 at 12:46
  • 1
    Wouldn't `HTTPMaximumConnectionsPerHost` limit only the maximum number of connections per each host, not overall? – Tommy Aug 26 '16 at 14:45
  • 1
    @Tommy That's right, `HTTPMaximumConnectionsPerHost` limits the maximum number of simultaneous connections to a certain host. If you want to limit the connections per app to _any_ host, there are ways to achieve this as well. One solution is to utilise `NSOperationQueue` - which requires you to encapsulate your URL task into a `NSOperation` (which is elaborate). Other solutions might involve utility libraries, where you can enqueue an _asynchronous task_ (a closure with a completion handler, or a closure returning a future) into a special queue which can execute a set of tasks in parallel. – CouchDeveloper Aug 27 '16 at 07:14
15

I found a workaround for this timeouts.

Tried to download a file with slow connection simulation on device(Settings -> Developer -> Network link conditioner -> Choose a profile -> 3G -> Enable).

Here is my sample code:

- (void) methodForNSURLSession{
  NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
  _tasksArray = [[NSMutableArray alloc] init];
  sessionConfig.HTTPMaximumConnectionsPerHost = 3;
  sessionConfig.timeoutIntervalForResource = 120;
  sessionConfig.timeoutIntervalForRequest = 120;
  NSURLSession* session = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];

  // data tasks
  [self createDownloadTasksWithSession:session];

}

- (void) createDownloadTasksWithSession:(NSURLSession *)session{
  for (int i = 0; i < 100; i++) {
    NSURLSessionDownloadTask *sessionDownloadTask = [session downloadTaskWithURL: [NSURL URLWithString:@"https://discussions.apple.com/servlet/JiveServlet/showImage/2-20930244-204399/iPhone%2B5%2BProblem2.jpg"]];
    [_tasksArray addObject:sessionDownloadTask];
    [sessionDownloadTask addObserver:self forKeyPath:@"countOfBytesReceived" options:NSKeyValueObservingOptionOld context:nil];
    [sessionDownloadTask resume];
  }
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
  if([[change objectForKey:@"old"] integerValue] == 0){
    NSLog(@"task %d: started", [_tasksArray indexOfObject: object]);
  }
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error{
    if (!error) {
      NSLog(@"task %d: finished!", [_tasksArray indexOfObject:task]);
    } else if (error.code == NSURLErrorTimedOut) {
      NSLog(@"task %d: timed out!", [_tasksArray indexOfObject:task]);
    }
}

And my output:

2014-01-10 10:38:48.769 TestApplication[2442:1803] task 1: started
2014-01-10 10:38:49.517 TestApplication[2442:1803] task 2: started
2014-01-10 10:38:50.273 TestApplication[2442:4b03] task 0: started
2014-01-10 10:40:11.794 TestApplication[2442:5003] task 2: finished!
2014-01-10 10:40:13.924 TestApplication[2442:1803] task 3: started
2014-01-10 10:40:26.221 TestApplication[2442:1d0f] task 1: finished!
2014-01-10 10:40:28.487 TestApplication[2442:1d0f] task 4: started
2014-01-10 10:40:43.007 TestApplication[2442:440f] task 5: timed out!
2014-01-10 10:40:43.009 TestApplication[2442:440f] task 6: timed out!
2014-01-10 10:40:43.011 TestApplication[2442:440f] task 7: timed out!
...

As you can see tasks started time out after 2 minutes

I played with timeoutIntervalForResource and timeoutIntervalForRequest parameters and in case we set both to 0 it will downloads without timeouts. But I think it isn't a good idea because of battery drawn. I think that 10 minutes or something like it will be a good value for it. But you have to set both parameters to the same value.

Apple docs:

timeoutIntervalForRequest - The timeout interval to use when waiting for additional data. timeoutIntervalForResource - The maximum amount of time that a resource request should be allowed to take.(timeout for all tasks to one resource)

Noticed strange thing: in case we set timeoutIntervalForResource = 60 and timeoutIntervalForRequest = 30, tasks will time out after 30 seconds! But most of them won't even start!

Looks like timer for timeoutIntervalForRequest started when task resumed. In that case we resumed all tasks in the same time and each task's timeout has to be as a resource timeout.

Also, I can advise wwdc13 705 session with a great demo about background session with download task.

MAhipal Singh
  • 4,745
  • 1
  • 42
  • 57
kaspartus
  • 1,365
  • 15
  • 33
  • I just noticed it in my UI, each cell had a download progress indicator and the server has a connection limit of 3. If I set it to 3 to match and tap download on 5 items, the first three run and the last two eventually time out. I'm more worried about when the app isn't running, I know I can queue up downloads for the system to run in the background and I'm worried it will try to run them all and time out, similar to how it does while the app is running. Not sure the best way to test that part. – Cory Imdieke Jan 09 '14 at 22:17
  • 3
    The docs say that timeoutIntervalForRequest is reset every time data is received -- so as long as continue to trickle in data, that connection is kept alive. However, the timeoutIntervalForResource is the TOTAL timeout for the request. DOCS: "The resource timer starts when the request is initiated and counts until either the request completes or this timeout interval is reached, whichever comes first." Defaults are 60 seconds for request and 7 days for resource. – Phil M Sep 18 '14 at 15:02