I have a NSOperationQueue
that is concurrent. For a specific NSOperation
, if it fails, I want to immediately retry this operation at the highest priority, and suspend all other operations until it succeeded.
I can think of scheduling a operation with higher priority, but how can I make all other operations waiting for this one in an efficient way? Changing all remaining operations dependencies seem too time consuming.
There are a few approaches:
One simple approach, which cuts the Gordian knot, is to just make the task that may require multiple attempts not finish until the retries are done (i.e., incorporate the retry login within the operation, itself). Then schedule the first task with a barrier, schedule the subsequent tasks, and that way none of the subsequent tasks will be able to run until the first one finishes (including all of its retries).
Alternatively, if you want to make the retry tasks separate operations, but do not want to use dependencies, you could add the subsequent tasks to a separate, suspended, queue:
let taskQueue = OperationQueue()
taskQueue.maxConcurrentOperationCount = 4
taskQueue.isSuspended = true
for i in 0 ..< 20 {
taskQueue.addOperation {
...
}
}
Then, add the task that may require retries to another queue (i.e., obviously, one that is not suspended):
func attempt(_ count: Int = 0) {
retryQueue.addOperation {
...
if isSuccessful {
taskQueue.isSuspended = false
} else {
attempt(count + 1)
}
...
}
}
When you do this, the first operation will un-suspend the task queue when the necessary criteria have been met:
For the sake of completeness, the other alternative is to subclass Operation
and make the isReady
logic not only return its super
implementation, but also observe some property. E.g.
class WaitingOperation: Operation {
@objc dynamic var canStart = false
var object: NSObject
var observer: NSKeyValueObservation?
let taskId: Int
override var isReady: Bool { super.isReady && canStart }
init<T>(object: T, canStartTasksKeyPath keyPath: KeyPath<T, Bool>, taskId: Int) where T: NSObject {
self.object = object
self.taskId = taskId
super.init()
observer = object.observe(keyPath, options: [.initial, .new]) { [weak self] _, changes in
if let newValue = changes.newValue {
self?.canStart = newValue
}
}
}
override class func keyPathsForValuesAffectingValue(forKey key: String) -> Set<String> {
var set = super.keyPathsForValuesAffectingValue(forKey: key)
if key == #keyPath(isReady) {
set.insert(#keyPath(canStart))
}
return set
}
override func main() {
...
}
}
and then
@objc dynamic var canStartTasks = false
func begin() {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 4
for i in 0 ..< 20 {
queue.addOperation(WaitingOperation(object: self, canStartTasksKeyPath: \.canStartTasks, taskId: i))
}
let start = CACurrentMediaTime()
attempt()
func attempt(_ count: Int = 0) {
queue.addOperation { [self] in
...
if notSuccessful {
attempt(count + 1)
} else {
canStartTasks = true
}
...
}
}
}