Search code examples
swiftconcurrencynsurlsessionnsurlrequest

Can you have synchronous but non-blocking URLSesssions?


I am utilizing URLSessions which are asynchronous in nature. This works well when there is only one call for a session. When I need to execute multiple (serial) calls, where the results need to be combined in the order of execution, it makes program logic painful and error prone. I am also blocking the main thread, which isn't good.

Constraints

Moving to next task may not occur before 4 second elapses, though it can be more.

Utilizing Monterey (OS upgrade required), so using the let (data, response) = try await session.data(from: url) is not optional.

If tasks are executed faster than every 4 seconds, the result is a server side error, forcing a retry.

Task execution

  • Execute task -- if the task concludes in less than 4 seconds, wait for the difference so the next task does not execute before the 4 seconds elapses.
  • Repeat process until all task have been completed.
  • Combine the results

My current process utilizes semaphores or dispatchGroups but both block the main thread.

Is there a way to get synchronous behavior without blocking the main thread?

    func getDataFromInput_Sync(authToken: String, transformedText: String ) -> (Data?, URLResponse?, Error?)
    {
        /*
         By default, transactions are asynchronous, so they return data while the rest of the program continues.
         Changing the behavior to synchronous requires blocking to wait for the outcome.  It affords us the
         ability to manage program flow inline.  This methods forces synchronous behavior and the output, which
         are returned via a tuple, types: (Data?, URLResponse?, Error?)
         */
        
        
        var outData       : Data?
        var outError      : Error?
        var urlResponse   : URLResponse?
        let targetURL     = "https://..." 
        let urlconfig     = URLSessionConfiguration.ephemeral
       
        // set the timeout to a high number, if we are prepared to wait that long.  Otherwise the session will timeout.
        urlconfig.timeoutIntervalForRequest  = 120
        urlconfig.timeoutIntervalForResource = 120

        let urlSession    = URLSession(configuration: urlconfig)
//        let dispatchGroup = DispatchGroup()
        let semaphore     = DispatchSemaphore(value: 0)

        // ephermeral doesnt write cookies, cache or credentials to disk
                
        guard let url = URL(string: targetURL),
              let httpBodyData = transformedText.data(using: .utf8) else { return (nil,nil,nil) }
        
        var request = URLRequest(url: url)
        
        request.httpMethod = "POST"
        request.httpBody   = httpBodyData
        request.addValue("Token " + authToken, forHTTPHeaderField: "Authorization")
        
        // Perform HTTP Request
        let task = (urlSession).dataTask(with: request) { (data, response, error) in
            guard error == nil
            else {
                print("we have an error: \(error!.localizedDescription)")
                return
            }
            
            guard let data = data else { print("Empty data"); return }
            
            outData     = data
            urlResponse = response
            outError    = error
//            dispatchGroup.leave()
            semaphore.signal()

        }
        
        task.resume()
        semaphore.wait()

//        dispatchGroup.enter()
//        task.resume()
//        dispatchGroup.wait()
        
        return (outData, urlResponse, outError)
    }

    func testServerRequest()
    {
        let sentences   = ["Example Sentence1","Example Sentence2","Example Sentence3"] //...array to process
        for (_,thisString) in sentences.enumerated()
        {
            let timeTestPoint   = Date()
            let futureSecs      = 4.0
            let (data, urlResponse, error) = getDataFromInput_Sync(authToken: authToken, transformedText: thisString )
            let elapsed         = timeTestPoint.timeIntervalSinceNow * -1 // time elapsed between request and result

            // act on the data received
            
            // executing the next request before futureSecs will cause an error, so pause
            let delayAmt = futureSecs - elapsed
            Thread.sleep(forTimeInterval: delayAmt)
        }
    }

Solution

  • Make it in background queue, like

    DispatchQueue.global(qos: .background).async {
        testServerRequest()
    }