Mike Levenick Mike Levenick - 17 days ago 9
Swift Question

How to handle many API calls with Swift 3 GCD

I am building an swift app to interact with an MDM API to do large numbers of updates via PUT commands, and I am running in to issues with how to handle the massive numbers of API calls without overloading the servers.

I am parsing through a CSV, and each line is an update. If I run the commands asynchronously, it generates and sends ALL of the API calls immediately, which the server doesn't like.

But if I run the commands synchronously, it freezes my GUI which is less than ideal, as the end user doesn't know what's going on, how long is left, if things are failing, etc.

I have also tried creating my own NSOperation queue and setting the max number of items to like 5, and then putting the synchronous function in there, but that doesn't seem to work very well either. It still freezes the GUI with some really random UI updates that seem buggy at best.

The servers can handle 5-10 requests at a time, but these CSV files can be upwards of 5,000 lines sometimes.

So how can I limit the number of simultaneous PUT requests going out in my loop, while not having the GUI freeze on me? To be honest, I don't even really care if the end user can interact with the GUI while it's running, I just want to be able to provide feedback on the lines that have run so far.




I have a wrapper which a colleague wrote most of, and the async function looks like this:

func sendRequest(endpoint: String, method: HTTPMethod, base64credentials: String, dataType: DataType, body: Data?, queue: DispatchQueue, handler: @escaping (Response)->Swift.Void) {
let url = self.resourceURL.appendingPathComponent(endpoint)
var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 30.0)
request.httpMethod = "\(method)"

var headers = ["Authorization": "Basic \(base64credentials)"]
switch dataType {
case .json:
headers["Content-Type"] = "application/json"
headers["Accept"] = "application/json"
if let obj = body {
do {
request.httpBody = try JSONSerialization.data(withJSONObject: obj, options: JSONSerialization.WritingOptions(rawValue: 0))
} catch {
queue.async {
handler(.badRequest)
}
return
}
}
case .xml:
headers["Content-Type"] = "application/xml"
headers["Accept"] = "application/xml"
request.httpBody = body
/*if let obj = body {
request.httpBody = (obj as! XMLDocument).xmlData
}*/
}
request.allHTTPHeaderFields = headers

session.dataTask(with: request) {
var response: Response
if let error = $2 {
response = .error(error)
} else {
let httpResponse = $1 as! HTTPURLResponse
switch httpResponse.statusCode {
case 200..<299:
if let object = try? JSONSerialization.jsonObject(with: $0!, options: JSONSerialization.ReadingOptions(rawValue: 0)) {
response = .json(object)
} else if let object = try? XMLDocument(data: $0!, options: 0) {
response = .xml(object)
} else {
response = .success
}
default:
response = .httpCode(httpResponse.statusCode)
}
}

queue.async {
handler(response)
}
}.resume()


Then, there is a synchronous option which uses semaphore, which looks like this:

func sendRequestAndWait(endpoint: String, method: HTTPMethod, base64credentials: String, dataType: DataType, body: Data?) -> Response {
var response: Response!
let semephore = DispatchSemaphore(value: 0)
sendRequest(endpoint: endpoint, method: method, base64credentials: base64credentials, dataType: dataType, body: body, queue: DispatchQueue.global(qos: .default)) {
response = $0
semephore.signal()
}
semephore.wait()
return response
}


Usage information is as follows:

class ViewController: NSViewController {

let client = JSSClient(urlString: "https://my.mdm.server:8443/", allowUntrusted: true)
let credentials = JSSClient.Credentials(username: "admin", password: "ObviouslyNotReal")


func asynchronousRequestExample() {
print("Sending asynchronous request")

client.sendRequest(endpoint: "computers", method: .get, credentials: credentials, dataType: .xml, body: nil, queue: DispatchQueue.main) { (response) in

print("Response recieved")

switch response {
case .badRequest:
print("Bad request")
case .error(let error):
print("Receieved error:\n\(error)")
case .httpCode(let code):
print("Request failed with http status code \(code)")
case .json(let json):
print("Received JSON response:\n\(json)")
case .success:
print("Success with empty response")
case .xml(let xml):
print("Received XML response:\n\(xml.xmlString(withOptions: Int(XMLNode.Options.nodePrettyPrint.rawValue)))")
}

print("Completed")
}

print("Request sent")

}

func synchronousRequestExample() {
print("Sending synchronous request")

let response = client.sendRequestAndWait(endpoint: "computers", method: .get,credentials: credentials, dataType: .json, body: nil)

print("Response recieved")

switch response {
case .badRequest:
print("Bad request")
case .error(let error):
print("Receieved error:\n\(error)")
case .httpCode(let code):
print("Request failed with http status code \(code)")
case .json(let json):
print("Received JSON response:\n\(json)")
case .success:
print("Success with empty response")
case .xml(let xml):
print("Received XML response:\n\(xml.xmlString(withOptions: Int(XMLNode.Options.nodePrettyPrint.rawValue)))")
}

print("Completed")
}


override func viewDidAppear() {
super.viewDidAppear()
synchronousRequestExample()
asynchronousRequestExample()

}


I have modified the send functions slightly, so that they take base64 encoded credentials off the bat, and maybe one or two other things.

Answer

Well, I think I got this covered! I decided to climb out of the rabbit hole a ways and simplify things. I wrote my own session instead of relying on the wrapper, and set up semaphores in it, threw it in an OperationQueue and it seems to be working perfectly.

This was the video I followed to set up my simplified semaphores request. https://www.youtube.com/watch?v=j4k8sN8WdaM

I'll have to tweak the below code to be a PUT instead of the GET I've been using for testing, but that part is easy.

//print (row[0])
    let myOpQueue = OperationQueue()
    myOpQueue.maxConcurrentOperationCount = 3
    let semaphore = DispatchSemaphore(value: 0)
    var i = 0
    while i < 10 {
        let myURL = NSURL(string: "https://my.server.com/APIResources/computers/id/\(i)")
        myOpQueue.addOperation {

            let request = NSMutableURLRequest(url: myURL! as URL)
            request.httpMethod = "GET"
            let configuration = URLSessionConfiguration.default
            configuration.httpAdditionalHeaders = ["Authorization" : "Basic 123456789ABCDEFG=", "Content-Type" : "text/xml", "Accept" : "text/xml"]
            let session = Foundation.URLSession(configuration: configuration)
            let task = session.dataTask(with: request as URLRequest, completionHandler: {
                (data, response, error) -> Void in
                if let httpResponse = response as? HTTPURLResponse {
                    print(httpResponse.statusCode)
                    semaphore.signal()
                    self.lblLine.stringValue = "\(i)"
                    self.appendLogString(stringToAppend: "\(httpResponse.statusCode)")
                    print(myURL!)

                }
                if error == nil {
                    print("No Errors")
                    print("")
                } else {
                    print(error!)
                }
            })

            task.resume()
            semaphore.wait()

        }
        i += 1
    }
Comments