QRunLoopOperation.m

/*
     File: QRunLoopOperation.m
 Abstract: An abstract subclass of NSOperation for async run loop based operations.
  Version: 2.2
 
 Disclaimer: IMPORTANT:  This Apple software is supplied to you by Apple
 Inc. ("Apple") in consideration of your agreement to the following
 terms, and your use, installation, modification or redistribution of
 this Apple software constitutes acceptance of these terms.  If you do
 not agree with these terms, please do not use, install, modify or
 redistribute this Apple software.
 
 In consideration of your agreement to abide by the following terms, and
 subject to these terms, Apple grants you a personal, non-exclusive
 license, under Apple's copyrights in this original Apple software (the
 "Apple Software"), to use, reproduce, modify and redistribute the Apple
 Software, with or without modifications, in source and/or binary forms;
 provided that if you redistribute the Apple Software in its entirety and
 without modifications, you must retain this notice and the following
 text and disclaimers in all such redistributions of the Apple Software.
 Neither the name, trademarks, service marks or logos of Apple Inc. may
 be used to endorse or promote products derived from the Apple Software
 without specific prior written permission from Apple.  Except as
 expressly stated in this notice, no other rights or licenses, express or
 implied, are granted by Apple herein, including but not limited to any
 patent rights that may be infringed by your derivative works or by other
 works in which the Apple Software may be incorporated.
 
 The Apple Software is provided by Apple on an "AS IS" basis.  APPLE
 MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
 THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS
 FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
 OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
 
 IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
 OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
 MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
 AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
 STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
 POSSIBILITY OF SUCH DAMAGE.
 
 Copyright (C) 2013 Apple Inc. All Rights Reserved.
 
 */
 
#import "QRunLoopOperation.h"
 
/*
    Theory of Operation
    -------------------
    Some critical points:
    
     1. By the time we're running on the run loop thread, we know that all further state 
        transitions happen on the run loop thread.  That's because there are only three 
        states (inited, executing, and finished) and run loop thread code can only run 
        in the last two states and the transition from executing to finished is 
        always done on the run loop thread.
 
     2. -start can only be called once.  So run loop thread code doesn't have to worry 
        about racing with -start because, by the time the run loop thread code runs, 
        -start has already been called.
        
     3. Likewise, because -start can only be called once, it doesn't have to worry about 
        racing with invocations of -start.
        
     3. -cancel can be called multiple times from any thread.  Run loop thread code 
        must take a lot of care with do the right thing with cancellation.  Also, -cancel 
        and -start can race.
    
    Some state sequences:
    
     1. no execute (testNoExecute)
    
        [-init]
        [-dealloc]
 
        This is the case where you create the operation and never run it.  That's just fine by us.
        
        There are no formal sequence points here because neither -init nor -dealloc have logging (-init 
        because you can't tell whether logging is enabled at that time, and -dealloc because, once the 
        object is called, there's no way to collect the log).
 
     2. no execute, cancel (testNoExecute)
     
        [-init]
        >cancel
        -cancel.winner
        <cancel
        [-dealloc]
 
        This is the case where you create the operation, cancel it, but never run it.  We specifically 
        want to allow this to make it easier to write clean up code.
 
     3. cancel before start (testCancelBeforeStart)
     
        [-init]
        >cancel
        -cancel.winner
        <cancel
        >start
        -setState.executing
        <start
        >startOnRunLoopThread
        -startOnRunLoopThread.cancelled
        >finishWithError
        -finishWithError.error
        -setState.finished
        <finishWithError
        <startOnRunLoopThread
        [-dealloc]
        
        I originally thought that this could never happen, but it seems that NSOperationQueue will 
        quite happily start a cancelled operation.
        
        Note that the following sequence /can't/ happen:
        
        [-init]
        >cancel
        -cancel.winner
        <cancel
        >start
        -setState.executing
        <start
        [-dealloc]
 
        because a) the operation queue holds a reference to the operation until it's finished, and 
        b) the -start uses -performSelector:onThread: to queue a call to -startOnRunLoopThread, 
        and that retains the object.
 
     4. cancel during start, before schedule (testCancelDuringStart)
 
        [-init]
        >start
        -setState.executing
        -start.cancelBefore
        >cancel
        -cancel.winner
        -cancel.schedule
        <cancel
        <start
        >cancelOnRunLoopThread
        -cancelOnRunLoopThread.cancel
        >finishWithError
        -finishWithError.error
        -setState.finished
        <finishWithError
        <cancelOnRunLoopThread
        >startOnRunLoopThread
        -startOnRunLoopThread.bounce
        <startOnRunLoopThread
        [-dealloc]
     
        This is what happens if -cancel gets called while -start is running but before -start 
        has scheduled -startOnRunLoopThread to execute.  -cancelOnRunLoopThread is queued 
        before -startOnRunLoopThread, so -startOnRunLoopThread runs second, notices the cancellation, 
        and then bounces.
 
     5. cancel during start, after schedule (testCancelDuringStart)
 
        [-init]
        >start
        -setState.executing
        -start.cancelAfter
        >cancel
        -cancel.winner
        -cancel.schedule
        <cancel
        <start
        >startOnRunLoopThread
        -startOnRunLoopThread.cancelled
        >finishWithError
        -finishWithError.error
        -setState.finished
        <finishWithError
        <startOnRunLoopThread
        >cancelOnRunLoopThread
        -cancelOnRunLoopThread.bounce
        <cancelOnRunLoopThread
        [-dealloc]
     
        This is what happens if -cancel gets called while -start is running but before -start 
        has scheduled -startOnRunLoopThread to execute.  -cancelOnRunLoopThread is queued 
        after -startOnRunLoopThread, so -startOnRunLoopThread runs first and does the real work 
        and -cancelOnRunLoopThread runs second and bounces.
        
     6. Basics (testBasics)
 
        [-init]
        >start
        -setState.executing
        <start
        >startOnRunLoopThread
        -startOnRunLoopThread.start
        <startOnRunLoopThread
        >finishWithError
        -finishWithError.noError
        -setState.finished
        <finishWithError
        [-dealloc]
        
        This is the standard run-to-completion case.
 
     7. Basics with cancel (testBasicsCancel)
 
        [-init]
        >start
        -setState.executing
        <start
        >startOnRunLoopThread
        -startOnRunLoopThread.start
        <startOnRunLoopThread
        >cancel
        -cancel.winner
        -cancel.schedule
        <cancel
        >cancelOnRunLoopThread
        -cancelOnRunLoopThread.cancel
        >finishWithError
        -finishWithError.error
        -setState.finished
        <finishWithError
        <cancelOnRunLoopThread
        [-dealloc]
 
        This is the standard cancel-while-executing case.  -cancelOnRunLoopThread wins the race 
        with finish, and it detects that the operation is executing and actually cancels. 
 
     8. Basics with late cancel (testBasicsCancelLate)
     
        [-init]
        >start,
        -setState.executing,
        <start,
        >startOnRunLoopThread,
        -startOnRunLoopThread.start,
        <startOnRunLoopThread,
        >finishWithError,
        -finishWithError.noError,
        -setState.finished,
        <finishWithError,
        >cancel,
        -cancel.winner,
        <cancel
        [-dealloc]
        
        The cancellation comes in after the operation has finished.  -cancelOnRunLoopThread is 
        not scheduled because the operation is already in the finished state.  Also note that 
        we call [super cancel] in this case, but that has no effect: -[NSOperation cancel] looks 
        at -isFinished and bounces in that case.
 
     9. Much delayed cancel (testDelayedCancel)
 
        [-init]
        >start
        -setState.executing
        <start
        >startOnRunLoopThread
        -startOnRunLoopThread.start
        <startOnRunLoopThread
        >cancel
        -cancel.winner
        -cancel.delay
        >finishWithError
        -finishWithError.noError
        -setState.finished
        <finishWithError
        -cancel.schedule
        <cancel
        >cancelOnRunLoopThread
        -cancelOnRunLoopThread.bounce
        <cancelOnRunLoopThread
        [-dealloc]
        
        This is very similar to case 5 but the thread doing the cancel has been artifically 
        delayed to ensure that the finish happens between the start of -cancel and its end.
 
    Markup:
        [x]  denotes an emplied sequence point.
        >x   denotes the entry to -[QRunLoopOperation x].
        <x   denotes the return of -[QRunLoopOperation x].
        -x.y denotes a significant point within -[QRunLoopOperation x].
        -x   denotes an otherwise unannotated invocation of -[QRunLoopOperation x].
        (x)  means that the case is tested by the unit test method -[UnitTests x].
*/
 
@interface QRunLoopOperation ()
 
// read/write versions of public properties
 
@property (assign, readwrite) QRunLoopOperationState    state;
@property (copy,   readwrite) NSError *                 error;          
 
@end
 
// debugging infrastructure
 
#if defined(NDEBUG)
 
#define DebugLogEvent(str) do { } while (0)
 
#else
 
@interface QRunLoopOperation (UnitTestSupportPrivate)
 
- (void)debugLogEvent:(NSString *)event;
 
@end
 
#define DebugLogEvent(str) do { [self debugLogEvent:str]; } while (0)
 
#endif
 
@implementation QRunLoopOperation
 
@synthesize debugName     = _debugName;
 
@synthesize runLoopThread = _runLoopThread;
@synthesize runLoopModes  = _runLoopModes;
 
@synthesize error         = _error;
 
- (id)init
{
    self = [super init];
    if (self != nil) {
        assert(self->_state == kQRunLoopOperationStateInited);
    }
    return self;
}
 
- (void)dealloc
{
    assert(self->_state != kQRunLoopOperationStateExecuting);
    [self->_debugName release];
    [self->_runLoopModes release];
    [self->_runLoopThread release];
    [self->_error release];
    #if ! defined(NDEBUG)
        [self->_debugEventLog release];
    #endif
    [super dealloc];
}
 
#pragma mark * Non-synthesized Properties
 
- (NSThread *)actualRunLoopThread
    // Returns the effective run loop thread, that is, the one set by the user 
    // or, if that's not set, the main thread.
{
    NSThread *  result;
    
    result = self.runLoopThread;
    if (result == nil) {
        result = [NSThread mainThread];
    }
    return result;
}
 
- (BOOL)isActualRunLoopThread
    // Returns YES if the current thread is the actual run loop thread.
{
    return [[NSThread currentThread] isEqual:self.actualRunLoopThread];
}
 
- (NSSet *)actualRunLoopModes
{
    NSSet * result;
    
    result = self.runLoopModes;
    if ( (result == nil) || ([result count] == 0) ) {
        result = [NSSet setWithObject:NSDefaultRunLoopMode];
    }
    return result;
}
 
#pragma mark * Core state transitions
 
- (QRunLoopOperationState)state
{
    return self->_state;
}
 
- (void)setState:(QRunLoopOperationState)newState
    // Change the state of the operation, sending the appropriate KVO notifications.
{
    QRunLoopOperationState  oldState;
 
    // The following check is really important.  The state can only go forward, and there 
    // should be no redundant changes to the state (that is, newState must never be 
    // equal to self->_state).
    
    assert(newState > self->_state);
 
    // As a corollary to the above, you can't change the state to inited because it starts 
    // out there.
    
    assert(newState != kQRunLoopOperationStateInited);
 
    // The -start method is the one that transitions from inited to executing, 
    // and it can run on any thread.  However, there's no race possible because 
    // only one thread is allowed to call -start.  The transition from executing 
    // to finished must be done by the run loop thread.
    //
    // There's a subtle requirement here, namely that -start must change the state 
    // before scheduling -startOnRunLoopThread.  Without that, the inited to executing 
    // and executing to finished changes race.
 
    assert((newState == kQRunLoopOperationStateExecuting) || self.isActualRunLoopThread);
    
    // Change the state and send the right KVO notifications.
    
    // inited    + executing -> isExecuting
    // inited    + finished  -> isFinished
    // executing + finished  -> isExecuting + isFinished
 
    oldState = self->_state;
    if ( (newState == kQRunLoopOperationStateExecuting) || (oldState == kQRunLoopOperationStateExecuting) ) {
        [self willChangeValueForKey:@"isExecuting"];
    }
    if (newState == kQRunLoopOperationStateFinished) {
        [self willChangeValueForKey:@"isFinished"];
    }
    self->_state = newState;
    if (newState == kQRunLoopOperationStateFinished) {
        [self didChangeValueForKey:@"isFinished"];
    }
    if ( (newState == kQRunLoopOperationStateExecuting) || (oldState == kQRunLoopOperationStateExecuting) ) {
        [self didChangeValueForKey:@"isExecuting"];
    }
    
    // Log the change.
    
    #if ! defined(NDEBUG)
        switch (newState) {
            default:
                assert(NO);
                // fall through
            case kQRunLoopOperationStateInited: {
                DebugLogEvent(@"-setState.inited");
            } break;
            case kQRunLoopOperationStateExecuting: {
                DebugLogEvent(@"-setState.executing");
            } break;
            case kQRunLoopOperationStateFinished: {
                DebugLogEvent(@"-setState.finished");
            } break;
        }
    #endif
}
 
- (void)startOnRunLoopThread
    // Starts the operation.  The actual -start method is very simple, 
    // deferring all of the work to be done on the run loop thread by this 
    // method.
{
    DebugLogEvent(@">startOnRunLoopThread");
 
    assert(self.isActualRunLoopThread);
    assert(self.state != kQRunLoopOperationStateInited);
 
    // State might be kQRunLoopOperationStateFinished at this point if someone managed 
    // to cancel us from the actual run loop thread between -start and -startOnRunLoopThread.  
    // In that case we've already finished, so we just do nothing.
    
    if (self.state == kQRunLoopOperationStateExecuting) {
        if ([self isCancelled]) {
            DebugLogEvent(@"-startOnRunLoopThread.cancelled");
            
            // We were cancelled before we even got running.  Flip the the finished 
            // state immediately.
            
            [self finishWithError:[NSError errorWithDomain:NSCocoaErrorDomain code:NSUserCancelledError userInfo:nil]];
        } else {
            DebugLogEvent(@"-startOnRunLoopThread.start");
            [self operationDidStart];
        }
    } else {
        DebugLogEvent(@"-startOnRunLoopThread.bounce");
    }
 
    DebugLogEvent(@"<startOnRunLoopThread");
}
 
- (void)cancelOnRunLoopThread
    // Cancels the operation.
{
    DebugLogEvent(@">cancelOnRunLoopThread");
 
    assert(self.isActualRunLoopThread);
 
    // We know that a) state was kQRunLoopOperationStateExecuting when we were 
    // scheduled (that's enforced by -cancel), and b) the state can't go 
    // backwards (that's enforced by -setState), so we know the state must 
    // either be kQRunLoopOperationStateExecuting or kQRunLoopOperationStateFinished. 
    // We also know that the transition from executing to finished always 
    // happens on the run loop thread.  Thus, we don't need to lock here.  
    // We can look at state and, if we're executing, trigger a cancellation.
    
    if (self.state == kQRunLoopOperationStateExecuting) {
        DebugLogEvent(@"-cancelOnRunLoopThread.cancel");
        [self finishWithError:[NSError errorWithDomain:NSCocoaErrorDomain code:NSUserCancelledError userInfo:nil]];
    } else {
        DebugLogEvent(@"-cancelOnRunLoopThread.bounce");
    }
    DebugLogEvent(@"<cancelOnRunLoopThread");
}
 
- (void)finishWithError:(NSError *)error
    // See comment in header.
{
    DebugLogEvent(@">finishWithError");
 
    assert(self.isActualRunLoopThread);
    // error may be nil
 
    // Latch the error.  This code is very simple once you remove all the debug logging (-:
    if (self.error == nil) {
        if (error != nil) {
            DebugLogEvent(@"-finishWithError.error");
        } else {
            DebugLogEvent(@"-finishWithError.noError");
        }
        self.error = error;
    } else {
        if (error != nil) {
            DebugLogEvent(@"-finishWithError.bounceError");
        } else {
            DebugLogEvent(@"-finishWithError.bounceNoError");
        }
    }
    
    // Call -operationWillFinish to let subclasses know about the change.
    
    [self operationWillFinish];
 
    // Make the change.
    
    self.state = kQRunLoopOperationStateFinished;
 
    DebugLogEvent(@"<finishWithError");
}
 
#pragma mark * Subclass override points
 
- (void)operationDidStart
{
    assert(self.isActualRunLoopThread);
}
 
- (void)operationWillFinish
{
    assert(self.isActualRunLoopThread);
}
 
#pragma mark * Overrides
 
- (BOOL)isConcurrent
{
    // any thread
    return YES;
}
 
- (BOOL)isExecuting
{
    // any thread
    return self->_state == kQRunLoopOperationStateExecuting;
}
 
- (BOOL)isFinished
{
    // any thread
    return self->_state == kQRunLoopOperationStateFinished;
}
 
- (void)start
{
    DebugLogEvent(@">start");
 
    // any thread
 
    assert(self.state == kQRunLoopOperationStateInited);
    
    // We have to change the state here, otherwise isExecuting won't necessarily return 
    // true by the time we return from -start.  Also, we don't test for cancellation 
    // here because that would a) result in us sending isFinished notifications on a 
    // thread that isn't our run loop thread, and b) confuse the core cancellation code, 
    // which expects to run on our run loop thread.  Finally, we don't have to worry 
    // about races with other threads calling -start.  Only one thread is allowed to 
    // start us at a time.
    
    self.state = kQRunLoopOperationStateExecuting;
    #if ! defined(NDEBUG)
        if (self.debugCancelSelfBeforeSchedulingStart) {
            DebugLogEvent(@"-start.cancelBefore");
            [self cancel];
        }
    #endif
    [self performSelector:@selector(startOnRunLoopThread) onThread:self.actualRunLoopThread withObject:nil waitUntilDone:NO modes:[self.actualRunLoopModes allObjects]];
    #if ! defined(NDEBUG)
        if (self.debugCancelSelfAfterSchedulingStart) {
            DebugLogEvent(@"-start.cancelAfter");
            [self cancel];
        }
    #endif
 
    DebugLogEvent(@"<start");
}
 
- (void)cancel
{
    BOOL    runCancelOnRunLoopThread;
    BOOL    oldValue;
 
    DebugLogEvent(@">cancel");
 
    // any thread
 
    // We synchronise here to ensure that only one thread calls [super cancel]. 
    
    @synchronized (self) {
        oldValue = [self isCancelled];
 
        if ( ! oldValue ) {
            DebugLogEvent(@"-cancel.winner");
        }
        
        // Call our super class so that isCancelled starts returning true immediately.
        
        [super cancel];
        
        // If we were the one to set isCancelled (that is, we won the race with regards 
        // other threads calling -cancel) and we're actually running (that is, we lost 
        // the race with other threads calling -start and the run loop thread finishing), 
        // we schedule to run on the run loop thread.
        //
        // The concurrency guarantee here is kinda hazy.  Specifically, state can change 
        // immediately after we read it (because of another thread calling -start or 
        // the run loop thread finishing).  There are two important cases to consider here: 
        //
        // o -start taking us from inited to executing -- We might want to schedule 
        //   -cancelOnRunLoopThread in this case, but we miss our chance.  That's OK though: 
        //   after changing the state -start will schedule -startOnRunLoopThread which will 
        //   check for cancellation.
        //
        // o run loop thread taking us from executing to finished -- In this case we might 
        //   schedule -cancelOnRunLoopThread redundantly.  That's OK though because 
        //   -cancelOnRunLoopThread will just bounce in that case.
 
        runCancelOnRunLoopThread = ! oldValue && self.state == kQRunLoopOperationStateExecuting;
    }
    if (runCancelOnRunLoopThread) {
        #if ! defined(NDEBUG)
            if (self.debugSecondaryThreadCancelDelay > 0.0) {
                if ( ! self.isActualRunLoopThread ) {
                    DebugLogEvent(@"-cancel.delay");
                    [NSThread sleepForTimeInterval:self.debugSecondaryThreadCancelDelay];
                }
            }
        #endif
        DebugLogEvent(@"-cancel.schedule");
        [self performSelector:@selector(cancelOnRunLoopThread) onThread:self.actualRunLoopThread withObject:nil waitUntilDone:NO modes:[self.actualRunLoopModes allObjects]];
    }
    DebugLogEvent(@"<cancel");
}
 
@end
 
#if ! defined(NDEBUG)
 
@implementation QRunLoopOperation (UnitTestSupport)
 
// The compiler won't let me @synthesize these accessors, so we write them out 
// by hand.  Fortunately they are single item "assign" properties, so atomicity is 
// not a problem.
 
// If debugCancelSelfBeforeSchedulingStart is set, -start calls -cancel 
// before scheduling -startOnRunLoopThread.
 
- (BOOL)debugCancelSelfBeforeSchedulingStart
{
    return self->_debugCancelSelfBeforeSchedulingStart;
}
 
- (void)setDebugCancelSelfBeforeSchedulingStart:(BOOL)newValue
{
    self->_debugCancelSelfBeforeSchedulingStart = newValue;
}
 
// If debugCancelSelfAfterSchedulingStart is set, -start calls -cancel 
// after scheduling -startOnRunLoopThread.
 
- (BOOL)debugCancelSelfAfterSchedulingStart
{
    return self->_debugCancelSelfAfterSchedulingStart;
}
 
- (void)setDebugCancelSelfAfterSchedulingStart:(BOOL)newValue
{
    self->_debugCancelSelfAfterSchedulingStart = newValue;
}
 
// debugSecondaryThreadCancelDelay controls a delay in -cancel, just 
// before is schedules -cancelOnRunLoopThread.
 
- (NSTimeInterval)debugSecondaryThreadCancelDelay
{
    return self->_debugSecondaryThreadCancelDelay;
}
 
- (void)setDebugSecondaryThreadCancelDelay:(NSTimeInterval)newValue
{
    self->_debugSecondaryThreadCancelDelay = newValue;
}
 
- (NSArray *)debugEventLog
    // Returns the current event log.
{
    NSArray *   result;
    
    // Synchronisation is necessary to avoid accessing the array while 
    // it's being mutated by another thread.
    
    @synchronized (self) {
        // _debugEventLog may be nil, and that's OK.
        result = [[self->_debugEventLog copy] autorelease];
    }
    return result;
}
 
- (void)debugEnableEventLog
    // Enables the event log on this object.
{
    // Synchronisation is necessary to because it's reasonable for multiple 
    // threads to call this routine at once.
 
    @synchronized (self) {
        if (self->_debugEventLog == nil) {
            self->_debugEventLog = [[NSMutableArray alloc] init];
        }
    }
}
 
- (void)debugLogEvent:(NSString *)event
    // Called by the implementation to log events.
{
    assert(event != nil);
    
    // Synchronisation is necessary because multiple threads might be adding 
    // events concurrently.
    
    @synchronized (self) {
        if (self->_debugEventLog != nil) {
            [self->_debugEventLog addObject:event];
        }
    }
}
 
@end
 
#endif