mirror of https://github.com/oxen-io/session-ios
mirror of https://github.com/oxen-io/session-ios
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
251 lines
6.4 KiB
251 lines
6.4 KiB
// |
|
// Copyright (c) 2019 Open Whisper Systems. All rights reserved. |
|
// |
|
|
|
#import "OWSOperation.h" |
|
#import "OWSBackgroundTask.h" |
|
#import "OWSError.h" |
|
#import <SessionProtocolKit/SessionProtocolKit.h> |
|
|
|
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
|
|
|