iosswiftnsoperationqueuensoperation

How to schedule a NSOperation to the head of queue and let all other operations waiting for it?


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.


Solution

  • There are a few approaches:

    1. 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).

    2. 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:

      enter image description here

    3. 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
                  }
      
                  ...
              }
          }
      }
      

      enter image description here