Networking/QRunLoopOperation.m

/*
    File:       QRunLoopOperation.m
 
    Contains:   An abstract subclass of NSOperation for async run loop based operations.
 
    Written by: DTS
 
    Copyright:  Copyright (c) 2010 Apple Inc. All Rights Reserved.
 
    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.
 
*/
 
#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. -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.
    
    Some state transitions:
 
     1. init -> dealloc
     2. init -> cancel -> dealloc
XXX  3. init -> cancel -> start -> finish -> dealloc
     4. init -> cancel -> start -> startOnRunLoopThreadThread -> finish dealloc
!!!  5. init -> start -> cancel -> startOnRunLoopThreadThread -> finish -> cancelOnRunLoopThreadThread -> dealloc
XXX  6. init -> start -> cancel -> cancelOnRunLoopThreadThread -> startOnRunLoopThreadThread -> finish -> dealloc
XXX  7. init -> start -> cancel -> startOnRunLoopThreadThread -> cancelOnRunLoopThreadThread -> finish -> dealloc
     8. init -> start -> startOnRunLoopThreadThread -> finish -> dealloc
     9. init -> start -> startOnRunLoopThreadThread -> cancel -> cancelOnRunLoopThreadThread -> finish -> dealloc
!!! 10. init -> start -> startOnRunLoopThreadThread -> cancel -> finish -> cancelOnRunLoopThreadThread -> dealloc
    11. init -> start -> startOnRunLoopThreadThread -> finish -> cancel -> dealloc
 
    Markup:
        XXX means that the case doesn't happen.
        !!! means that the case is interesting.
        
    Described:
    
     1. It's valid to allocate an operation and never run it.
     2. It's also valid to allocate an operation, cancel it, and yet never run it.
     3. While it's valid to cancel an operation before it starting it, this case doesn't 
        happen because -start always bounces to the run loop thread to maintain the invariant 
        that the executing to finished transition always happens on the run loop thread.
     4. In this -startOnRunLoopThread detects the cancellation and finishes immediately.
     5. Because the -cancel can happen on any thread, it's possible for the -cancel 
        to come in between the -start and the -startOnRunLoop thread.  In this case 
        -startOnRunLoopThread notices isCancelled and finishes straightaway.  And 
        -cancelOnRunLoopThread detects that the operation is finished and does nothing.
     6. This case can never happen because -performSelecton:onThread:xxx 
        callbacks happen in order, -start is synchronised with -cancel, and -cancel 
        only schedules if -start has run.
     7. This case can never happen because -startOnRunLoopThread will finish immediately 
        if it detects isCancelled (see case 5).
     8. This is the standard run-to-completion case. 
     9. This is the standard cancellation case.  -cancelOnRunLoopThread wins the race 
        with finish, and it detects that the operation is executing and actually cancels. 
    10. In this case the -cancelOnRunLoopThread loses the race with finish, but that's OK 
        because -cancelOnRunLoopThread already does nothing if the operation is already 
        finished.
    11. Cancellating after finishing still sets isCancelled but has no impact 
        on the RunLoop thread code.
*/
 
@interface QRunLoopOperation ()
 
// read/write versions of public properties
 
@property (assign, readwrite) QRunLoopOperationState    state;
@property (copy,   readwrite) NSError *                 error;          
 
@end
 
@implementation QRunLoopOperation
 
- (id)init
{
    self = [super init];
    if (self != nil) {
        assert(self->_state == kQRunLoopOperationStateInited);
    }
    return self;
}
 
- (void)dealloc
{
    assert(self->_state != kQRunLoopOperationStateExecuting);
    [self->_runLoopModes release];
    [self->_runLoopThread release];
    [self->_error release];
    [super dealloc];
}
 
#pragma mark * Properties
 
@synthesize runLoopThread = _runLoopThread;
@synthesize runLoopModes  = _runLoopModes;
 
- (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;
}
 
@synthesize error         = _error;
 
#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.
{
    // any thread
 
    @synchronized (self) {
        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);
 
        // Transitions from executing to finished must be done on the run loop thread.
        
        assert( (newState != kQRunLoopOperationStateFinished) || self.isActualRunLoopThread );
 
        // 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"];
        }
    }
}
 
- (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.
{
    assert(self.isActualRunLoopThread);
    assert(self.state == kQRunLoopOperationStateExecuting);
 
    if ([self isCancelled]) {
        
        // We were cancelled before we even got running.  Flip the the finished 
        // state immediately.
        
        [self finishWithError:[NSError errorWithDomain:NSCocoaErrorDomain code:NSUserCancelledError userInfo:nil]];
    } else {
        [self operationDidStart];
    }
}
 
- (void)cancelOnRunLoopThread
    // Cancels the operation.
{
    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) {
        [self finishWithError:[NSError errorWithDomain:NSCocoaErrorDomain code:NSUserCancelledError userInfo:nil]];
    }
}
 
- (void)finishWithError:(NSError *)error
{
    assert(self.isActualRunLoopThread);
    // error may be nil
 
    if (self.error == nil) {
        self.error = error;
    }
    [self operationWillFinish];
    self.state = kQRunLoopOperationStateFinished;
}
 
#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
{
    // 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;
    [self performSelector:@selector(startOnRunLoopThread) onThread:self.actualRunLoopThread withObject:nil waitUntilDone:NO modes:[self.actualRunLoopModes allObjects]];
}
 
- (void)cancel
{
    BOOL    runCancelOnRunLoopThread;
    BOOL    oldValue;
 
    // any thread
 
    // We need to synchronise here to avoid state changes to isCancelled and state
    // while we're running.
    
    @synchronized (self) {
        oldValue = [self isCancelled];
        
        // 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.
 
        runCancelOnRunLoopThread = ! oldValue && self.state == kQRunLoopOperationStateExecuting;
    }
    if (runCancelOnRunLoopThread) {
        [self performSelector:@selector(cancelOnRunLoopThread) onThread:self.actualRunLoopThread withObject:nil waitUntilDone:YES modes:[self.actualRunLoopModes allObjects]];
    }
}
 
@end