How to properly implement delay gap between operations for NSOperationQueue?

I have the following method in NSOperationQueue category:

- (void)addOperation:(NSOperation *)op withDelayInNanoseconds:(int64_t)nanoseconds {
    int64_t delay = (self.operationCount * nanoseconds);
    int64_t timeInterval = (int64_t)(nanoseconds + delay);
    int64_t boundTimeInterval = timeInterval >= 0 ? timeInterval : 0;
    __weak typeof(self) weakSelf = self;
    NSLog(@"%@ addOperation: %@ intoQueue: %@ withOperationDelayInNanoseconds: %@ boundTimeInterval: %@. operationCount: %@", weakSelf.debugDescription, op, weakSelf.underlyingQueue, @(nanoseconds), @(boundTimeInterval), @(weakSelf.operationCount));
    //uderlyingQueue could be nil.
    //maybe just add operation in queue?
    //https://github.com/Tricertops/GrandSwiftDispatch/issues/1
    //maybe the best why is to remove such a queue :/
    if (weakSelf.underlyingQueue == nil) {
        NSLog(@"%@ underlyingQueue is %@", weakSelf.debugDescription, weakSelf.underlyingQueue);
    }
    dispatch_queue_t queue = weakSelf.underlyingQueue ?: dispatch_queue_create("com.myproject.concurrency", NULL);

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, boundTimeInterval), queue, ^{
        [weakSelf addOperation:op];
    });
}


It may not work ( not sure how it works. )
But in this code I encountered self.unerlyingQueue value nil due to documentation.
So, I put adding operation in dispatch_after block and it is fires after delay.


This example adds delay only before adding operation. It don't add delay gap between two operations with desired interval.


Now I try to figure out how to do it.

Subclass from NSOperation.

@interface MyProjectDelayedOperation : NSOperation
@property (assign, nonatomic, readwrite) NSInteger beforeDelayInSeconds;
@property (assign, nonatomic, readwrite) NSInteger afterDelayInSeconds;
@end
@interface MyProjectDelayedOperation ()
@property (strong, readwrite) NSOperation *operation;
@property (readwrite, getter=isCancelled) BOOL cancelled;
@property (readwrite, getter=isExecuting) BOOL executing;
@property (readwrite, getter=isFinished) BOOL finished;
//@property (readonly, getter=isConcurrent) BOOL concurrent; // To be deprecated; use and override 'asynchronous' below
//@property (readonly, getter=isAsynchronous) BOOL asynchronous NS_AVAILABLE(10_8, 7_0);
@property (readonly, getter=isReady) BOOL ready;
@end
static const void *observerContextIsCancelled;
static const void *observerContextIsFinished;
@implementation MyProjectDelayedOperation
- (instancetype)initWithOperation:(NSOperation *)operation {
    if (self = [super init]) {
        self.operation = operation;
     
        [self.operation addObserver:self forKeyPath:@"isCancelled" options:0 context:observerContextIsCancelled];
        [self.operation addObserver:self forKeyPath:@"isFinished" options:0 context:observerContextIsFinished];
    }
    return self;
}
- (void)start {
    // wait before start for seconds.
    double delayInSeconds = self.beforeDelayInSeconds;
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
    __weak __typeof(self)weakSelf = self;
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
        [weakSelf willChangeValueForKey:@"isExecuting"];
        weakSelf.executing = YES && !weakSelf.cancelled;
        [weakSelf didChangeValueForKey:@"isExecuting"];
        if (self.executing && !self.cancelled) {
            [weakSelf.operation start];
        }
    });
}


// subscipt on operation values.


- (void)cancel {
    [self willChangeValueForKey:@"isCancelled"];
    self.cancelled = YES;
    [self didChangeValueForKey:@"isCancelled"];
    if (!self.operation.cancelled) {
        [self.operation cancel];
    }
}


- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ((object == self.operation) && (context == observerContextIsCancelled)) {
        [self cancel];
    }
    else if ((object == self.operation) && (context == observerContextIsFinished)){
        [self finished];
    }
}


- (void)finished {
    // should wait for state "done"
    double delayInSeconds = self.afterDelayInSeconds;
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
    __weak __typeof(self)weakSelf = self;
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
        [weakSelf willChangeValueForKey:@"isExecuting"];
        [weakSelf willChangeValueForKey:@"isFinished"];
        weakSelf.executing = NO;
        weakSelf.finished  = YES;
        [weakSelf didChangeValueForKey:@"isFinished"];
        [weakSelf didChangeValueForKey:@"isExecuting"];
    });
}
@end


It wraps operation and adds intervals before start and after finish.

It listens underlying operation events isCancelled and isFinished to react on them and to fire delayed operation finish.

Not sure how to add before and after delays properly.


Of course, this may work with queues maximum concurrent operations value equal one. ( Serial queue )

Replies

I have the following method in NSOperationQueue category …

Yeah, I wouldn’t do this.

Subclass from NSOperation.

Right. I haven’t reviewed your NSOperation code in detail but this is the right basic idea IMO.

You can then use dependencies to insert a delay. If you have an operation D that delays for a specific time, you can insert a delay gap between operations A and B by making B depend on D and D depend on A.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Hi, I read the code - not thoroughly, and I think this can be done much simpler.

First, I agree with Eskimo that messing with NSOperationQueue isn't the right thing. Your task is better achieved by constructing a proper NSOperation instead.

Second, and this is "high level" remark" you almost re-implemented NSOperation in your subclass, instead of modifying its behavior, and I think there's a much much simpler way to do this.

As you're already aware, the isCancelled and isFinished properties of an NSOperation are observable - and are also observed by the NSOperationQueue. But you forgot the one property that BY DESIGN gives you what you want --

@property (readonly, getter=isReady) BOOL ready;

Now when you want to delay the execution of an NSOperation, all you need to do is to keep its ready property at NO until such time you want it to execute, and then - change it to YES. NSOperationQueue observes the ready property, and will schedule pulling it from the queue when it becomes ready.

You'll need to wisely override the ready property, so that you don't break other behavior of it, and you will need to have a timer somewhere that will trigger its change to YES.

I don't have time to implement a sample now... maybe I'll do it and add to this answer.

Hope this helps.