// // Copyright (c) 2019 Open Whisper Systems. All rights reserved. // #import "OWSOperation.h" #import "OWSBackgroundTask.h" #import "OWSError.h" #import NS_ASSUME_NONNULL_BEGIN NSString *const OWSOperationKeyIsExecuting = @"isExecuting"; NSString *const OWSOperationKeyIsFinished = @"isFinished"; @interface OWSOperation () @property (nullable) NSError *failingError; @property (atomic) OWSOperationState operationState; @property (nonatomic) OWSBackgroundTask *backgroundTask; // This property should only be accessed on the main queue. @property (nonatomic) NSTimer *_Nullable retryTimer; @end @implementation OWSOperation - (instancetype)init { self = [super init]; if (!self) { return self; } _operationState = OWSOperationStateNew; _backgroundTask = [OWSBackgroundTask backgroundTaskWithLabel:self.logTag]; // Operations are not retryable by default. _remainingRetries = 0; return self; } - (void)dealloc { OWSLogDebug(@"in dealloc"); } #pragma mark - Subclass Overrides // Called one time only - (nullable NSError *)checkForPreconditionError { // OWSOperation have a notion of failure, which is inferred by the presence of a `failingError`. // // By default, any failing dependency cascades that failure to it's dependent. // If you'd like different behavior, override this method (`checkForPreconditionError`) without calling `super`. for (NSOperation *dependency in self.dependencies) { if (![dependency isKindOfClass:[OWSOperation class]]) { // Native operations, like NSOperation and NSBlockOperation have no notion of "failure". // So there's no `failingError` to cascade. continue; } OWSOperation *dependentOperation = (OWSOperation *)dependency; // Don't proceed if dependency failed - surface the dependency's error. NSError *_Nullable dependencyError = dependentOperation.failingError; if (dependencyError != nil) { return dependencyError; } } return nil; } // Called every retry, this is where the bulk of the operation's work should go. - (void)run { OWSAbstractMethod(); } // Called at most one time. - (void)didSucceed { // no-op // Override in subclass if necessary } // Called at most one time. - (void)didCancel { // no-op // Override in subclass if necessary } // Called zero or more times, retry may be possible - (void)didReportError:(NSError *)error { // no-op // Override in subclass if necessary } // Called at most one time, once retry is no longer possible. - (void)didFailWithError:(NSError *)error { // no-op // Override in subclass if necessary } #pragma mark - NSOperation overrides // Do not override this method in a subclass instead, override `run` - (void)main { OWSLogDebug(@"started."); NSError *_Nullable preconditionError = [self checkForPreconditionError]; if (preconditionError) { [self failOperationWithError:preconditionError]; return; } if (self.isCancelled) { [self reportCancelled]; return; } [self run]; } - (void)runAnyQueuedRetry { dispatch_async(dispatch_get_main_queue(), ^{ NSTimer *_Nullable retryTimer = self.retryTimer; self.retryTimer = nil; [retryTimer invalidate]; if (retryTimer != nil) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [self run]; }); } else { OWSLogVerbose(@"not re-running since operation is already running."); } }); } #pragma mark - Public Methods // These methods are not intended to be subclassed - (void)reportSuccess { OWSLogDebug(@"succeeded."); [self didSucceed]; [self markAsComplete]; } // These methods are not intended to be subclassed - (void)reportCancelled { OWSLogDebug(@"cancelled."); [self didCancel]; [self markAsComplete]; } - (void)reportError:(NSError *)error { [self didReportError:error]; if (self.remainingRetries == 0) { [self failOperationWithError:error]; return; } self.remainingRetries--; dispatch_async(dispatch_get_main_queue(), ^{ OWSAssertDebug(self.retryTimer == nil); [self.retryTimer invalidate]; // The `scheduledTimerWith*` methods add the timer to the current thread's RunLoop. // Since Operations typically run on a background thread, that would mean the background // thread's RunLoop. However, the OS can spin down background threads if there's no work // being done, so we run the risk of the timer's RunLoop being deallocated before it's // fired. // // To ensure the timer's thread sticks around, we schedule it while on the main RunLoop. self.retryTimer = [NSTimer weakScheduledTimerWithTimeInterval:self.retryInterval target:self selector:@selector(runAnyQueuedRetry) userInfo:nil repeats:NO]; }); } // Override in subclass if you want something more sophisticated, e.g. exponential backoff - (NSTimeInterval)retryInterval { return 0.1; } #pragma mark - Life Cycle - (void)failOperationWithError:(NSError *)error { OWSLogDebug(@"failed terminally."); self.failingError = error; [self didFailWithError:error]; [self markAsComplete]; } - (BOOL)isExecuting { return self.operationState == OWSOperationStateExecuting; } - (BOOL)isFinished { return self.operationState == OWSOperationStateFinished; } - (void)start { [self willChangeValueForKey:OWSOperationKeyIsExecuting]; self.operationState = OWSOperationStateExecuting; [self didChangeValueForKey:OWSOperationKeyIsExecuting]; [self main]; } - (void)markAsComplete { [self willChangeValueForKey:OWSOperationKeyIsExecuting]; [self willChangeValueForKey:OWSOperationKeyIsFinished]; // Ensure we call the success or failure handler exactly once. @synchronized(self) { OWSAssertDebug(self.operationState != OWSOperationStateFinished); self.operationState = OWSOperationStateFinished; } [self didChangeValueForKey:OWSOperationKeyIsExecuting]; [self didChangeValueForKey:OWSOperationKeyIsFinished]; } @end NS_ASSUME_NONNULL_END