Skip to content

Instantly share code, notes, and snippets.

@kean
Last active April 22, 2016 08:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kean/c9b7b72b089328b38d62383b9b3fde5a to your computer and use it in GitHub Desktop.
Save kean/c9b7b72b089328b38d62383b9b3fde5a to your computer and use it in GitHub Desktop.
#import "GMAutoRetryController.h"
#import "NSTimer+GMBlocks.h"
#import "NSError+Fitmeup.h"
#import "GMNetworkReachability.h"
#import "GMRepository.h"
typedef NS_ENUM(NSInteger, _GMARCState) {
_GMARCStateSuspended, // initial state
_GMARCStateExecuting,
_GMARCStateWaitingRetry,
_GMARCStateCancelled
};
@interface GMAutoRetryController ()
@property (nonatomic) _GMARCState state;
@end
@implementation GMAutoRetryController {
NSTimeInterval _retryDelay;
NSTimer *__weak _retryTimer;
}
- (void)dealloc {
[_retryTimer invalidate];
}
- (nonnull instancetype)initWithBlock:(void (^)(GMAutoRetryController *))block {
GM_CREATE_NONNULL_SELF([super init]);
_state = _GMARCStateSuspended;
_retryDelayIncreaseRate = 2;
_maximumRetryDelay = 64;
_initialRetryDelay = 8;
_currentAttemptCount = 0;
_maximumAttemptCount = GMAutoRetryControllerInfiniteAttemptCount;
_block = block;
return self;
}
- (void)resume {
self.state = _GMARCStateExecuting;
}
- (void)cancel {
self.state = _GMARCStateCancelled;
}
- (BOOL)scheduleAutoRetryWithError:(nullable NSError *)error {
if ([self shouldScheduleAutoRetryWithError:error]) {
_currentError = error;
self.state = _GMARCStateWaitingRetry;
return YES;
} else {
// execution can still be restarted manually
self.state = _GMARCStateSuspended;
return NO;
}
}
// TODO: Should this method return NO when error is nil?
- (BOOL)shouldScheduleAutoRetryWithError:(nullable NSError *)error {
return self.currentAttemptCount < self.maximumAttemptCount;
}
#pragma mark - FSM
- (BOOL)_isValidNextState:(_GMARCState)nextState {
switch (self.state) {
case _GMARCStateSuspended:
return (nextState == _GMARCStateExecuting ||
nextState == _GMARCStateCancelled);
case _GMARCStateExecuting:
return (nextState == _GMARCStateWaitingRetry ||
nextState == _GMARCStateCancelled);
case _GMARCStateWaitingRetry:
return (nextState == _GMARCStateExecuting ||
nextState == _GMARCStateCancelled);
default:
return NO;
}
}
- (void)setState:(_GMARCState)state {
@synchronized(self) {
if ([self _isValidNextState:state]) {
[self _executeExitActionForState:_state];
_state = state;
[self _executeEnterActionForState:state];
}
}
}
- (void)_executeExitActionForState:(_GMARCState)state {
if (state == _GMARCStateWaitingRetry) {
[_retryTimer invalidate];
}
}
- (void)_executeEnterActionForState:(_GMARCState)state {
if (state == _GMARCStateExecuting) {
_currentAttemptCount++;
self.block(self);
}
if (state == _GMARCStateWaitingRetry) {
[self _scheduleRetryTimer];
}
}
#pragma mark - Timer
- (void)_scheduleRetryTimer {
typeof(self) __weak weakSelf = self;
NSTimeInterval timeInterval = [self _retryDelay];
NSTimer *timer = [NSTimer timerWithTimeInterval:timeInterval block:^(NSTimer *timer) {
[weakSelf _retryTimerDidFire:timer];
} userInfo:nil repeats:NO];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
_retryTimer = timer;
}
- (void)_retryTimerDidFire:(NSTimer *)timer {
self.state = _GMARCStateExecuting;
}
/*! Returns current retry delay and increments it afterwards.
*/
- (NSTimeInterval)_retryDelay {
NSTimeInterval delay = _retryDelay > 0 ? _retryDelay : self.initialRetryDelay;
_retryDelay = MIN(delay * self.retryDelayIncreaseRate, self.maximumRetryDelay);
return delay;
}
@end
@implementation GMURLAutoRetryController
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (nonnull instancetype)initWithBlock:(void (^ __nullable)(GMAutoRetryController * __nonnull))block {
GM_CREATE_NONNULL_SELF([super initWithBlock:block]);
_allowsFastRetries = YES;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_networkReachabilityDidChange:) name:GMNetworkReachabilyDidChangeNotification object:GMRepository.networkReachability];
return self;
}
- (BOOL)shouldScheduleAutoRetryWithError:(nullable NSError *)error {
if ([error.domain isEqualToString:NSURLErrorDomain] &&
error.code == NSURLErrorCancelled) {
return NO;
}
return [super shouldScheduleAutoRetryWithError:error];
}
- (void)_networkReachabilityDidChange:(NSNotification *)notification {
if (!self.allowsFastRetries) {
return;
}
GMNetworkReachability *reachability = notification.object;
if (!reachability.reachable) {
return;
}
if (![self.currentError gm_isInternetConnectionError]) {
return;
}
[self resume];
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment