Reydel Leon
Reydel Leon

Reputation: 1075

Alamofire request not running completion block inside NSOperation

Note: While there are other, similar questions to this one in SO, in none of them the authors seem to be controlling the lifecycle of the Operation by themselves. Please, read through before referring to another question.

I created an [NS]Operation in Swift 3.0 to download, parse and cache some data in Core Data.

At first, I used the main() method in the Operation to execute the task at hand and it worked well. Now I need to run several separate tasks to retrieve information about each/every device I got in this step. For this, I need to ensure that the devices are actually in Core Data before I attempt to get the other information. For that reason, I want to make sure that I decide when the task is complete -- which is when ALL the devices are safe and sound in the cache -- before firing the dependent requests.

The problem is that even though I have checked that Alamofire does execute the request and the server does send the data, the completion block marked with the comment [THIS WONT EXECUTE!] is never executed. This causes the Queue to stall, since the Operation is marked as finished inside the said completion block, which is the desired behavior.

Does anyone has any ideas about what might be going on here?

class FetchDevices: Operation {

    var container: NSPersistentContainer!
    var alamofireManager: Alamofire.SessionManager!
    var host: String!
    var port: Int!

    private var _executing = false
    private var _finished = false

    override internal(set) var isExecuting: Bool {
        get {
            return _executing
        }

        set {
            willChangeValue(forKey: "isExecuting")
            _executing = newValue
            didChangeValue(forKey: "isExecuting")
        }
    }

    override internal(set) var isFinished: Bool {
        get {
            return _finished
        }

        set {
            willChangeValue(forKey: "isFinished")
            _finished = newValue
            didChangeValue(forKey: "isFinished")
        }
    }

    override var isAsynchronous: Bool {
        return true
    }

    init(usingContainer container: NSPersistentContainer, usingHost host: String, usingPort port: Int) {
        super.init()

        self.container = container
        self.host = host
        self.port = port

        let configuration = URLSessionConfiguration.default
        configuration.timeoutIntervalForResource = 10 // in seconds
        self.alamofireManager = Alamofire.SessionManager(configuration: configuration)
    }

    override func start() {
        if self.isCancelled {
            self.isFinished = true
            return
        }

        self.isExecuting = true

        alamofireManager!.request("http://apiurlfor.devices")
            .validate()
            .responseJSON { response in
                // THIS WONT EXECUTE!
                if self.isCancelled {
                    self.isExecuting = false
                    self.isFinished = true
                    return
                }

                switch response.result {
                case .success(let value):
                    let jsonData = JSON(value)

                    self.container.performBackgroundTask { context in
                        for (_, rawDevice):(String, JSON) in jsonData {
                            let _ = Device(fromJSON: rawDevice, usingContext: context)
                        }

                        do {
                            try context.save()
                        } catch {
                            let saveError = error as NSError
                            print("\(saveError), \(saveError.userInfo)")
                        }

                        self.isExecuting = false
                        self.isFinished = true
                    }

                case .failure(let error):
                    print("May Day! May Day! \(error)")
                    self.isExecuting = false
                    self.isFinished = true
                }
        }
    }
}

A piece of information that may be useful is that in the method where I queue all the operations, I use queue.waitUntilAllOperationsAreFinished() to execute a completion handler after all is done.

Upvotes: 3

Views: 1244

Answers (1)

Rob
Rob

Reputation: 437532

The problem is that you have something else that is blocking the main thread, which responseJSON uses for its closure, resulting in a deadlock. You can confirm this quickly replacing responseJSON with .responseJSON(queue: .global()) to have Alamofire use a queue other than the main queue, and you will see this behavior change. But if you do this (for diagnostic purposes only), you should change it back and then turn your attention to identify and eliminate whatever is blocking the main thread (i.e. don't wait on the main thread), as you should never block the main thread.


You mention that you are calling waitUntilAllOperationsAreFinished. While that is an intoxicatingly simple solution for waiting for a series of operations to finish, you should never do that from the main thread. The main thread should never be blocked (or, at least, not for more than a few milliseconds). It can result in a substandard UX (where the app freezes) and your app is susceptible to being summarily terminated by the "watch dog" process. I suspect that one of the reasons that the Alamofire author(s) felt so comfortable dispatching their completion handlers to the main queue by default is that not only is it often useful and convenient, but they know one would never block the main thread.

When using operation queues, the typical pattern to avoid ever waiting is to use a completion operation:

let completionOperation = BlockOperation {
    // something that we'll do when all the operations are done
}

for object in arrayOfObjects {
    let networkOperation = ...
    completionOperation.addDependency(networkOperation)
    queue.addOperation(networkOperation)
}

OperationQueue.main.addOperation(completionOperation)

You can achieve something similar if you use dispatch groups and dispatch group "notify" (though, typically, if you're using operation queues, you'd generally stay within the operation queue paradigm, for consistency's sake).

If you want to call waitUntilAllOperationsAreFinished, technically you can do that, but you should only do that if you dispatch that "wait" to some background queue (e.g. a global queue, but obviously not to your own operation queue upon which you added all of these operations). I think this is a wasteful pattern, though (why tie up some global worker thread waiting for operations to finish when you have a perfectly good mechanism for specifying a completion operation).

Upvotes: 1

Related Questions