issue with URLSession.shared.datatask and dispatchgroup

842 Views Asked by At

I am still very new to swift, so bear with me. I am having an issue, where this app works fine on my dev machine after being archived, and gatekeeper signed. but on other users machines, it fails to return the variables. I wanted to get it working so the catch stuff probably needs some work, like I said im very new/green here on swift


    enum apiCalls: Error {
        case badRequest
    }
    
    func CheckPortSpeeds() {
        if NetMon.shared.isConnected == true {
            guard let url = URL(string: MdnUrl) else {
                return
            }
            var request = URLRequest(url: url )
            request.httpMethod = "POST"
            request.setValue("application/json", forHTTPHeaderField: "Content-Type")
            request.setValue("application/json", forHTTPHeaderField: "Accept")
            let data = ["jsonrpc": "2.0", "method": "runCmds", "params": ["version": 1, "cmds": ["enable", "show interfaces ethernet1/1-30/1 status"], "format": "json"], "id": 1] as [String : Any]
            request.httpBody = try! JSONSerialization.data(withJSONObject: data, options: JSONSerialization.WritingOptions.prettyPrinted)
            var response : JSON
            var status : Int
            do {
                (response, status) = try APICaller().getResults(request: request)
            }
            catch apiCalls.badRequest {
                status = 400
                response = ["nil": "nil"]
                Alerts.shared.AlertUser(Message: "Host Not Reachable", Info: "Api Module")
            }
            catch SwiftyJSONError.notExist {
                status = 400
                response = ["nil": "nil"]
                Alerts.shared.AlertUser(Message: "Bad JSON response from host", Info: "Api Module")
            }
            catch {
                status = 400
                response = ["nil": "nil"]
                Alerts.shared.AlertUser(Message: "Unknown Error", Info: "Api Module")
            }
            if status == 200 {
                for n in 1...30 {
                    InterfaceDict["eth\(n)"] = Dictionary<String, String>()
                    InterfaceDict["eth\(n)"]?["portSpeed"] = response["result"][1]["interfaceStatuses"]["Ethernet\(n)/1"]["bandwidth"].int64
                    InterfaceDict["eth\(n)"]?["optic"] = response["result"][1]["interfaceStatuses"]["Ethernet\(n)/1"]["interfaceType"].string
                    guard let optic = InterfaceDict["eth\(n)"]?["optic"] as? String else {
                        return
                    }
                    InstalledOptic[n] = optic
                    guard let prtspd = InterfaceDict["eth\(n)"]?["portSpeed"] as? Int64 else {
                        return
                    }
                    PortSpeed[n] = prtspd
                }
                PortHandler().PortFlopper()
                ContentView().replies.responses += 1
            
                for r in 1...30 {
                    if PortMatch[r] == false {
                        FlipPorts(port: r, optic: InstalledOptic[r]!)
                    }
                }
            }
        }
        else{
            Alerts.shared.AlertUser(Message: "Network connection is down, please check netwok conection and try again", Info: "Network Monitor")
        }
    }

this above is basically setting up the api call etc..

this below is where im making the api call to an Arista switch's eapi


import Cocoa
import SwiftyJSON

class APICaller: NSObject {
    

    enum apiCalls: Error {
        case badRequest
    }
    
    func getResults(request: URLRequest) throws -> (JSON, Int) {
        var result: Result<(JSON, Int), Error> = .failure(SwiftyJSONError.notExist)
        let group = DispatchGroup()
        group.enter()
        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
            result = Result(catching: {
                if let e = error { throw e }
                guard let status = (response as? HTTPURLResponse)?.statusCode else {
                    throw apiCalls.badRequest
                }
                let json = try JSON(data: data ?? Data())
                return (json, status)
            })
            group.leave()
        }
        task.resume()
        group.wait()
        return try result.get()
    }
}

I have tried creating a dispatch queue for this and running the entire block inside the dispatch queue in async, ive tried in sync, i've tried some crazy things, and it doesn't seems to work on user machines as far as returning the variable, it just doesn't seem to wait. waits on mine and works perfectly... Im kinda just confused here, the apicall portion has been rewritten several times with help, as it was originally locking the threads on the user machine and triggering thread safety, now it just returns a nil variable and pops up my custom error message...

please... help....

1

There are 1 best solutions below

1
On BEST ANSWER

You should abandon this approach of trying to use dispatch group to make an inherently asynchronous API behave synchronously. In general, you would be well advised to simply avoid the use of wait at all (whether dispatch groups or semaphores or whatever). The wait method is inherently inefficient (it blocks one of the very limited worker threads) and should only be used by background queues, if you really need it, which is not the case here.

But if you block the main thread on mobile platforms, methods that do not respond immediately and are synchronous can result in a very poor UX (the app will freeze during this synchronous method) and, worse, the OS watchdog process might even terminate your app if you do this at the wrong time.

You are calling asynchronous methods, so your methods should employ asynchronous patterns, too. E.g. rather than trying to return the value, give your method an @escaping closure parameter, and call that closure when the network request is done.

Consider getResults: You already employ a Result<(JSON, Int), Error> there. So let us use that as our closure’s parameter type:

enum ApiError: Error {      // type names should start with upper case letter
    case badRequest
}

func getResults(request: URLRequest, completion: @escaping (Result<(JSON, Int), Error>) -> Void) {
    let task = URLSession.shared.dataTask(with: request) { data, response, error in
        if let error = error {
            completion(.failure(error))
            return
        }

        guard
            let status = (response as? HTTPURLResponse)?.statusCode,
            let data = data
        else {
            completion(.failure(ApiError.badRequest))
            return
        }

        do {
            let json = try JSON(data: data)
            completion(.success((json, status)))
        } catch let parseError {
            completion(.failure(parseError))
        }
    }
    task.resume()
}

Having done that, you would use it like so:

getResults(request: request) { result in
    switch result {
    case let .failure(error):
        // do something with error
    case let .success((json, statusCode)):
        // do something with json and statusCode
    }
}

// but do not use error, json, or statusCode here, because the above runs asynchronous (i.e. later)

Note, if you were targeting iOS 15 or macOS 12, Swift now has a more intuitive asynchronous pattern (called async/await), but I assume you are trying to target a broader range of OS versions, so the above is the traditional technique if you need to support older OS versions.


By the way, you mentioned it throwing errors about “thread safety”. That is a very different problem, likely because you are trying to do UI updates from a background thread or have some thread-safety violations where you are updating some shared property from multiple threads.

A very common pattern is to make sure we dispatch our completion handlers back to the main thread. It is exceedingly hard to introduce thread-safety issues or update the UI from the wrong thread if we dispatch everything back to the main queue.

E.g., in the following rendition of the above method, I dispatch both success and failure conditions back to the main queue:

func getResults(request: URLRequest, completion: @escaping (Result<(JSON, Int), Error>) -> Void) {
    let task = URLSession.shared.dataTask(with: request) { data, response, error in
        do {
            if let error = error { throw error }

            guard
                let status = (response as? HTTPURLResponse)?.statusCode,
                let data = data
            else {
                throw ApiError.badRequest
            }

            let json = try JSON(data: data)
            DispatchQueue.main.async {
                completion(.success((json, status)))
            }
        } catch let error {
            DispatchQueue.main.async {
                completion(.failure(error))
            }
        }
    }
    task.resume()
}

If you still have “thread safety” issues after all of this, I would suggest that you research that separately, and if you still have questions, post a small reproducible example of that in a separate question. But thread safety and asynchronous patterns are very different questions.