New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
NSOperation's completionBlock is called twice. #123
Comments
Looking into this more closely. Have re-produces your exception. Hmm, I think you're right about a race condition with regard to fulfill - which explains why some of the tests are inconsistent (I need to refactor them). But, also, there is this test: https://github.com/danthorpe/Operations/blob/development/Tests/Core/OperationTests.swift#L142 which admittedly doesn't test against multiple invocation of the completion block, but according to this bug, should be failing. Also, I'll just add, that most of the tests fulfill their expectation using a BlockObserver (https://github.com/danthorpe/Operations/blob/development/Tests/Core/OperationTests.swift#L100) instead of a completion handler. This is because it's a little ill-defined exactly when that completion handler will be run by NSOperation. It happens after the operation's Anyway, I will continue to investigate... |
Okay, so more info. This bug doesn't have anything to do with chaining completion blocks (as @difujia said). Changing the test case a little bit to see what's happening into this: func test__block_operation_with_default_block_runs_completion_block_once() {
let _queue = OperationQueue()
let expectation = expectationWithDescription("Test: \(__FUNCTION__)")
let operation = BlockOperation()
operation.log.severity = .Verbose
operation.completionBlock = {
print("*** I'm in a completion block on \(String.fromCString(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL)))")
}
operation.addObserver(BlockObserver { op, errors in
expectation.fulfill()
})
_queue.addOperation(operation)
waitForExpectationsWithTimeout(3, handler: nil)
} Will output the following in the test runner logs:
Notice that as the operation transitions |
Okay, think I'm getting to the bottom of it. I've added a KV observer to the override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if context != &completionBlockObservationContext {
super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
}
guard let
oldIsFinished = change?[NSKeyValueChangeOldKey] as? Bool,
newIsFinished = change?[NSKeyValueChangeNewKey] as? Bool
else { return }
switch (oldIsFinished, newIsFinished) {
case (false, true):
print("Asserting isFinished for the first time.")
case (_, true):
print("Asserting isFinished again.")
default:
break
}
} And if you put a break point on the 2nd assertion that isFinished is true, it's from the
Which is from: https://developer.apple.com/library/mac/documentation/Cocoa/Reference/NSOperation_class/ However, during the development of this framework, there was quite a bit of back and forth with @davedelong and (via him) a Foundation engineer who wrote/maintains Anyway, regardless, I suspect that there is some KVC happening on the state property, which is triggering UPDATE: Okay, whoops my bad - the observer is called twice because I had |
Okay, so, the override of |
Hi @difujia - pretty sure I've fixed this now. The bug was not as wide reaching as I initially thought it might be, as I could only re-produce it using If you're able to install Operations from the development branch and re-check, that would be great. I'll leave this issue here until it's verified fixed. Again, thanks for the feedback! |
I am able to re-produce this problem on both the development and release branches with an Operation class (as opposed to just BlockOperation). It's strange though - sometimes the code will run correctly for a few goes but if you Clean and Re-build it will have problems. I did a bunch of testing on this tonight and I figured some stuff out. You were definitely right about the Based on my testing, it looks like you have to override the public override final func start() {
if cancelled {
finish()
} else {
main()
}
} This seems to be the approach recommended by apple in the past for subclassing NSOperation (see https://gist.github.com/LoganWright/eb844fdec0873182abd8). It's very odd that the "Advanced NSOperations" Sample Code makes a point of calling If you are curious, here is a really simple way to re-produce the double-completion block calling: class DoubleCompletionOp : NSOperation {
var _finished = false
override var finished: Bool {
return _finished
}
override final func main() {
willChangeValueForKey("isFinished")
_finished = true
didChangeValueForKey("isFinished")
}
} And as soon as you override class SingleCompletionOp : NSOperation {
var _finished = false
override var finished: Bool {
return _finished
}
override final func start() {
main()
}
override final func main() {
willChangeValueForKey("isFinished")
_finished = true
didChangeValueForKey("isFinished")
}
} The tests seem to all run correctly with this new |
Thank you guys for investigating this issue. func testCompletionBlock() {
measureBlock {
let queue = OperationQueue()
let op = DummyOperation()
let expectation = self.expectationWithDescription("Test: \(__FUNCTION__)")
op.addCompletionBlock {
expectation.fulfill()
}
queue.addOperation(op)
self.waitForExpectationsWithTimeout(10, handler: nil)
}
}
class DummyOperation: Operation {
} Here The sample code from Apple has actually been changed a couple of times. I have a very early version of the sample (June 10, 2015) in my computer. The override of override final func start() {
assert(state == .Ready, "This operation must be performed on an operation queue.")
state = .Executing
for observer in observers {
observer.operationDidStart(self)
}
execute() // No override of main in this version.
} I don't have time to go deeper into this issue right now. Hope this snippet could help. |
@kevinbrewster @difujia okay, great stuff. I've left the Also - great idea on using |
@difujia @kevinbrewster proposed fix: 689c29e has been merged into |
Thanks for testing @OperationKit - will leave the issue open for @difujia and @kevinbrewster as they found/diagnosed the bug. |
(sorry i was logged in under the wrong account earlier. changing to avoid confusion). @danthorpe - I can confirm that the completion block is no longer called twice with the latest development code. |
@kevinbrewster are you the mysterious @OperationKit? ;) |
Okay, closing this issue now as fix has been verified. Thanks @kevinbrewster & @difujia! |
haha - my secret identity has been revealed!! I had actually been working on my own framework for Operations before I discovered this one only a few days ago. |
Ha! Cool. I know there are a couple of other projects. If you have any more feature requests or suggestions - feel free to create an issue - I'm eager to develop this more to increase its utility and fix annoyances people are having. I've not got time to look at your other issue now, but I will do tomorrow. |
Thanks @danthorpe. I too believe this issue has been addressed. For other improvements, I think a generic way of piping operation results could be great. I will create an issue for discussion. |
… and it is known to cause issues (see ProcedureKit/ProcedureKit#123).
Try the following test.
Then the test runner will log:
The same error appear even if I set the
completionBlock
directly, which I think make no difference when only one completionBlock exists.Discussion Xcode would not always stop the test as the error pops up, I think this is due to a race condition that the second call of
fulfill()
may happen either before or afterwaitForExpectationsWithTimeout
has seen the effect of the first call (in case of after, the test has finished running and succeeded). Anyway, this is probably irrelevant of the issue. Isn't thecompletionBlock
guaranteed to execute only once by Foundation framework?The text was updated successfully, but these errors were encountered: