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.
CodePudding user response:
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 theisReady
logic not only return itssuper
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 } ... } } }