Home > Back-end >  NSURLSession .waitsForConnectivity flag ignored when DataTask being run on OperationQueue
NSURLSession .waitsForConnectivity flag ignored when DataTask being run on OperationQueue

Time:10-28

I have a handwritten class MyURLRequest, that implements Operation. Inside it creates URLSession, configures it

public init(shouldWaitForConnectivity: Bool, timeoutForResource: Double?) {
    baseUrl = URL(string: Self.relevantServerUrl   "api/")
    self.shouldWaitForConnectivity = shouldWaitForConnectivity
    self.timeoutForResource = timeoutForResource
    super.init()
    localURLSession = URLSession(configuration: localConfig, delegate: self, delegateQueue: nil)

}

public var localConfig: URLSessionConfiguration {
    let res = URLSessionConfiguration.default
    res.allowsCellularAccess = true
    if let shouldWaitForConnectivity = shouldWaitForConnectivity {
        res.waitsForConnectivity = shouldWaitForConnectivity
        if let timeoutForResource = timeoutForResource {
            res.timeoutIntervalForResource = timeoutForResource
        }
    }
    return res
}

creates URLRequest, dataTask, and then being run on OperationQueue. Operation's methods looks like this

override open func start() {
    if isCancelled {
        isFinished = true
        return
    }
    
    startDate = Date()
    sessionTask?.resume()
    localURLSession.finishTasksAndInvalidate()
}

override open func cancel() {
    super.cancel()
    sessionTask?.cancel()
}

MyURLRequest also implements URLSessionDataDelegate and URLSessionTaskDelegate and the being delegate for it's own URLSession.

There is a problem with waitsForConnectivity NSURLSessionConfiguration's flag. In constructor I set it to true, but this flag is being ignored. In runtime, when network is turned off, request finishes immediately with error -1009. URLSessionTaskDelegate's method urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) is triggered immediately. func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) not being called at all.

The reason definitely not is that flag waitsForConnectivity wasn't correctly set: I've checked config in task received by urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?), and waitsForConnectivity == true.

I also tried to make request without operation queue, and that went fine - behaved such as expected. Maybe have something to do with OperationQueue. Would appreciate your help!

UPDATE: Seems like root of the problem is that Operation being released too early (when request not complete yet). I've tried to synchronise them using DispatchGroup():

override open func start() {
    if isCancelled {
        isFinished = true
        return
    }
    startDate = Date()

    dispatchGroup.enter()
    sessionTask?.resume()
    dispatchGroup.wait()

    localURLSession.finishTasksAndInvalidate()
}

where .leave() is called in URLSessionDelegate's methods. Nothing changed, still not waiting for connectivity.

UPDATE: Here's the error I get in didCompleteWithError:

Error Domain=NSURLErrorDomain Code=-1009 "" UserInfo={_kCFStreamErrorCodeKey=50, NSUnderlyingError=0x7fc319112de0 {Error Domain=kCFErrorDomainCFNetwork Code=-1009 "(null)" UserInfo={_kCFStreamErrorCodeKey=50, _kCFStreamErrorDomainKey=1}}, _NSURLErrorFailingURLSessionTaskErrorKey=LocalDataTask <6388AD46-8497-40DF-8768-44FEBB84A8EC>.<1>, _NSURLErrorRelatedURLSessionTaskErrorKey=(
    "LocalDataTask <6388AD46-8497-40DF-8768-44FEBB84A8EC>.<1>",
    "LocalDataTask <26BCBD73-FC8B-4A48-8EA2-1172ABB8093C>.<1>"
), NSLocalizedDescription=., NSErrorFailingURLStringKey=}

CodePudding user response:

I believe the issue is rooted in your use of finishTasksAndInvalidate. It looks like you are depending on that method synchronously waiting for all pending tasks to complete, but according to the documentation that isn't how it works.

Here's a more in-depth explanation of what I think is happening. By default an Operation is considered complete as soon as the start method returns. This is definitely not what you need, as the task has to be completed asynchronously. Operation is not capable of supporting this behavior out of the box.

Your start returns immediately, long before the session has any time to complete the task you have started. Then, with the operation complete, the queue removes it. This often ends up being the only owner of that instance. If that's true, it kicks off the operation deinit process, which ends up releasing the URLSession. At that point, the session looks like it might do some clean up and terminate any outstanding tasks, forwarding some calls to its delegate. I wasn't sure if URLSession does this, but based on what you are seeing, it sounds like it may.

To achieve what you want, I think you'll need to restructure your NSOperation subclass to be fully asynchronous and to only complete when the started task is done.

Building out a fully thread-safe async NSOperation subclass is a real pain. In case this isn't something you've tackled before, you can check out an implementation here: https://github.com/ChimeHQ/OperationPlus

  • Related