Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NSURLSession concurrent requests with Alamofire

I'm experiencing some strange behaviour with my test app. I've about 50 simultaneous GET requests that I send to the same server. The server is an embedded server on a small piece of hardware with very limited resources. In order to optimize the performance for each single request, I configure one instance of Alamofire.Manager as follows:

let configuration = NSURLSessionConfiguration.defaultSessionConfiguration() configuration.HTTPMaximumConnectionsPerHost = 2 configuration.timeoutIntervalForRequest = 30 let manager = Alamofire.Manager(configuration: configuration) 

When I send the requests with manager.request(...) they get dispatched in pairs of 2 (as expected, checked with Charles HTTP Proxy). The weird thing though is, that all requests which didn't finish within 30 seconds from the first request, get cancelled because of the timeout at the same time (even if they haven't been sent yet). Here is an illustration showcasing the behaviour:

concurrent request illustration

Is this an expected behaviour and how can I make sure that the requests won't get the timeout before they are even sent?

Thanks a lot!

like image 894
Hannes Avatar asked Nov 19 '14 16:11

Hannes


People also ask

Why use Alamofire instead of URLSession?

Alamofire and URLSession both help you to make network requests in Swift. The URLSession API is part of the foundation framework, whereas Alamofire needs to be added as an external dependency. Many developers doubt whether it's needed to include an extra dependency on something basic like networking in Swift.

What is the advantage of Alamofire?

Advantages of Using AlamofireUsing Alamofire will give a cleaner project. API call interactions (POST/GET/PUT/etc.) will be easier and more understable. Alamofire simplifies a number of common networking tasks that makes development faster and easier.

Is Alamofire good?

Alamofire is one of the most popular and widely used Swift networking libraries. Built on Apple's Foundation networking stack, it provides an elegant API to make network requests. With over thirty-thousand stars on GitHub, it's one of the top-rated Swift repositories.

What is Alamofire interceptor?

Adding a RequestInterceptor Alamofire's RequestInterceptor protocol ( RequestAdapter & RequestRetrier ) provides important and powerful request adaptation and retry features. It can be applied at both the Session and Request level.


1 Answers

Yes, this is expected behavior. One solution is to wrap your requests in custom, asynchronous NSOperation subclass, and then use the maxConcurrentOperationCount of the operation queue to control the number of concurrent requests rather than the HTTPMaximumConnectionsPerHost parameter.

The original AFNetworking did a wonderful job wrapping the requests in operations, which made this trivial. But AFNetworking's NSURLSession implementation never did this, nor does Alamofire.


You can easily wrap the Request in an NSOperation subclass. For example:

class NetworkOperation: AsynchronousOperation {      // define properties to hold everything that you'll supply when you instantiate     // this object and will be used when the request finally starts     //     // in this example, I'll keep track of (a) URL; and (b) closure to call when request is done      private let urlString: String     private var networkOperationCompletionHandler: ((_ responseObject: Any?, _ error: Error?) -> Void)?      // we'll also keep track of the resulting request operation in case we need to cancel it later      weak var request: Alamofire.Request?      // define init method that captures all of the properties to be used when issuing the request      init(urlString: String, networkOperationCompletionHandler: ((_ responseObject: Any?, _ error: Error?) -> Void)? = nil) {         self.urlString = urlString         self.networkOperationCompletionHandler = networkOperationCompletionHandler         super.init()     }      // when the operation actually starts, this is the method that will be called      override func main() {         request = Alamofire.request(urlString, method: .get, parameters: ["foo" : "bar"])             .responseJSON { response in                 // do whatever you want here; personally, I'll just all the completion handler that was passed to me in `init`                  self.networkOperationCompletionHandler?(response.result.value, response.result.error)                 self.networkOperationCompletionHandler = nil                  // now that I'm done, complete this operation                  self.completeOperation()         }     }      // we'll also support canceling the request, in case we need it      override func cancel() {         request?.cancel()         super.cancel()     } } 

Then, when I want to initiate my 50 requests, I'd do something like this:

let queue = OperationQueue() queue.maxConcurrentOperationCount = 2  for i in 0 ..< 50 {     let operation = NetworkOperation(urlString: "http://example.com/request.php?value=\(i)") { responseObject, error in         guard let responseObject = responseObject else {             // handle error here              print("failed: \(error?.localizedDescription ?? "Unknown error")")             return         }          // update UI to reflect the `responseObject` finished successfully          print("responseObject=\(responseObject)")     }     queue.addOperation(operation) } 

That way, those requests will be constrained by the maxConcurrentOperationCount, and we don't have to worry about any of the requests timing out..

This is an example AsynchronousOperation base class, which takes care of the KVN associated with asynchronous/concurrent NSOperation subclass:

// //  AsynchronousOperation.swift // //  Created by Robert Ryan on 9/20/14. //  Copyright (c) 2014 Robert Ryan. All rights reserved. //  import Foundation  /// Asynchronous Operation base class /// /// This class performs all of the necessary KVN of `isFinished` and /// `isExecuting` for a concurrent `NSOperation` subclass. So, to developer /// a concurrent NSOperation subclass, you instead subclass this class which: /// /// - must override `main()` with the tasks that initiate the asynchronous task; /// /// - must call `completeOperation()` function when the asynchronous task is done; /// /// - optionally, periodically check `self.cancelled` status, performing any clean-up ///   necessary and then ensuring that `completeOperation()` is called; or ///   override `cancel` method, calling `super.cancel()` and then cleaning-up ///   and ensuring `completeOperation()` is called.  public class AsynchronousOperation : Operation {      private let stateLock = NSLock()      private var _executing: Bool = false     override private(set) public var isExecuting: Bool {         get {             return stateLock.withCriticalScope { _executing }         }         set {             willChangeValue(forKey: "isExecuting")             stateLock.withCriticalScope { _executing = newValue }             didChangeValue(forKey: "isExecuting")         }     }      private var _finished: Bool = false     override private(set) public var isFinished: Bool {         get {             return stateLock.withCriticalScope { _finished }         }         set {             willChangeValue(forKey: "isFinished")             stateLock.withCriticalScope { _finished = newValue }             didChangeValue(forKey: "isFinished")         }     }      /// Complete the operation     ///     /// This will result in the appropriate KVN of isFinished and isExecuting      public func completeOperation() {         if isExecuting {             isExecuting = false         }          if !isFinished {             isFinished = true         }     }      override public func start() {         if isCancelled {             isFinished = true             return         }          isExecuting = true          main()     }      override public func main() {         fatalError("subclasses must override `main`")     } }  /*  Abstract:  An extension to `NSLocking` to simplify executing critical code.   Adapted from Advanced NSOperations sample code in WWDC 2015 https://developer.apple.com/videos/play/wwdc2015/226/  Adapted from https://developer.apple.com/sample-code/wwdc/2015/downloads/Advanced-NSOperations.zip  */  import Foundation  extension NSLocking {      /// Perform closure within lock.     ///     /// An extension to `NSLocking` to simplify executing critical code.     ///     /// - parameter block: The closure to be performed.      func withCriticalScope<T>(block: () throws -> T) rethrows -> T {         lock()         defer { unlock() }         return try block()     } } 

There are other possible variations of this pattern, but just ensure that you (a) return true for asynchronous; and (b) you post the necessary isFinished and isExecuting KVN as outlined the Configuring Operations for Concurrent Execution section of the Concurrency Programming Guide: Operation Queues.

like image 167
Rob Avatar answered Sep 20 '22 07:09

Rob