Reputation: 1075
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
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