I have a chain of many NSBlockOperations with dependencies. If one operation early in the chain fails - I want the other operations to not run. According to docs, this should be easy to do from the outside - if I cancel an operation, all dependent operations should automatically be cancelled.
However - if only the execution-block of my operation "knows" that it failed, while executing - can it cancel its own work?
I tried the following:
NSBlockOperation *op = [[NSBlockOperation alloc] init];
__weak NSBlockOperation *weakOpRef = op;
[takeScreenShot addExecutionBlock:^{
LOGInfo(@"Say Cheese...");
if (some_condition == NO) { // for some reason we can't take a photo
[weakOpRef cancel];
LOGError(@"Photo failed");
}
else {
// take photo, process it, etc.
LOGInfo(@"Photo taken");
}
}];
However, when I run this, other operations dependent on op are executed even though op was cancelled. Since they are dependent - surely they're not starting before op finished, and I verified (in debugger and using logs) that isCancelled state of op is YES before the block returns. Still the queue executes them as if op finished successfully.
I then further challenged the docs, like thus:
NSOperationQueue *myQueue = [[NSOperationQueue alloc] init];
NSBlockOperation *op = [[NSBlockOperation alloc] init];
__weak NSBlockOperation *weakOpRef = takeScreenShot;
[takeScreenShot addExecutionBlock:^{
NSLog(@"Say Cheese...");
if (weakOpRef.isCancelled) { // Fail every once in a while...
NSLog(@"Photo failed");
}
else {
[NSThread sleepForTimeInterval:0.3f];
NSLog(@"Photo taken");
}
}];
NSOperation *processPhoto = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"Processing Photo...");
[NSThread sleepForTimeInterval:0.1f]; // Process
NSLog(@"Processing Finished.");
}];
// setup dependencies for the operations.
[processPhoto addDependency: op];
[op cancel]; // cancelled even before dispatching!!!
[myQueue addOperation: op];
[myQueue addOperation: processPhoto];
NSLog(@">>> Operations Dispatched, Wait for processing");
[eventQueue waitUntilAllOperationsAreFinished];
NSLog(@">>> Work Finished");
But was horrified to see the following output in the log:
2020-11-05 16:18:03.803341 >>> Operations Dispatched, Wait for processing
2020-11-05 16:18:03.803427 Processing Photo...
2020-11-05 16:18:03.813557 Processing Finished.
2020-11-05 16:18:03.813638+0200 TesterApp[6887:111445] >>> Work Finished
Pay attention: the cancelled op was never run - but the dependent processPhoto was executed, despite its dependency on op.
Ideas anyone?
OK. I think I solved the mystery. I just misunderstood the
[NSOperation cancel]documentation.it says:
I thought if operation B depends on operation A - it implies that if A is canceled (hence - A didn't finish its work) then B should be cancelled as well, because semantically it can't start until A completes its work.
Apparently, that was just wishful thinking...
What documentation says is different. When you cancel operation B (which depends on operation A), then despite being dependent on A - it won't wait for A to finish before it's removed from the queue. If operation A started, but hasn't finished yet - canceling B will remove it (B) immediately from the queue - because it will now ignore dependencies (the completion of A).
Soooo... to accomplish my scheme, I will need to introduce my own "dependencies" mechanism. The straightforward way is by introducing a set of boolean properties like
isPhotoTaken,isPhotoProcessed,isPhotoColorAnalyzedetc. Then, an operation dependent on these pre-processing actions, will need to check in its preamble (of execution block) whether all required previous operations actually finished successfully, else cancel itself.However, it may be worth subclassing NSBlockOperation, overriding the logic that calls 'start' to skip to finished if any of the 'dependencies' has been cancelled!
Initially I thought this is a long shot and may be hard to implement, but fortunately, I wrote this quick subclass, and it seems to work fine. Of course deeper inspection and stress tests are due:
When I substitute NSBlockOperation with MYBlockOperation in the original question (and my other tests, the behaviour is the one I described and expected.