diff --git a/EDQueue.podspec b/EDQueue.podspec index 2618403..908f312 100644 --- a/EDQueue.podspec +++ b/EDQueue.podspec @@ -1,14 +1,14 @@ Pod::Spec.new do |s| s.name = 'EDQueue' - s.version = '0.7.1' + s.version = '1.1' s.license = 'MIT' s.summary = 'A persistent background job queue for iOS.' - s.homepage = 'https://github.com/thisandagain/queue' - s.authors = {'Andrew Sliwinski' => 'andrewsliwinski@acm.org', 'Francois Lambert' => 'flambert@mirego.com'} - s.source = { :git => 'https://github.com/thisandagain/queue.git', :tag => 'v0.7.1' } - s.platform = :ios, '5.0' + s.homepage = 'https://github.com/gelosi/queue' + s.authors = {'Andrew Sliwinski' => 'andrewsliwinski@acm.org', 'Francois Lambert' => 'flambert@mirego.com', 'Oleg Shanyuk' => 'oleg.shanyuk@gmail.com'} + s.source = { :git => 'https://github.com/gelosi/queue.git', :tag => 'v1.1' } + s.platform = :ios, '7.0' s.source_files = 'EDQueue' s.library = 'sqlite3.0' s.requires_arc = true - s.dependency 'FMDB', '~> 2.0' + s.dependency 'FMDB', '~> 2.1' end diff --git a/EDQueue/EDQueue.h b/EDQueue/EDQueue.h index 9d49848..0b0a636 100755 --- a/EDQueue/EDQueue.h +++ b/EDQueue/EDQueue.h @@ -6,7 +6,12 @@ // Copyright (c) 2012 Andrew Sliwinski. All rights reserved. // -#import +@import Foundation; + +#import "EDQueueJob.h" +#import "EDQueuePersistentStorageProtocol.h" + +NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSInteger, EDQueueResult) { EDQueueResultSuccess = 0, @@ -22,30 +27,42 @@ extern NSString *const EDQueueJobDidSucceed; extern NSString *const EDQueueJobDidFail; extern NSString *const EDQueueDidDrain; -@protocol EDQueueDelegate; -@interface EDQueue : NSObject +@class EDQueue; -+ (EDQueue *)sharedInstance; +@protocol EDQueueDelegate +- (void)queue:(EDQueue *)queue processJob:(EDQueueJob *)job completion:(EDQueueCompletionBlock)block; +@end + +@interface EDQueue : NSObject @property (nonatomic, weak) id delegate; +@property (nonatomic, strong, readonly) id storage; +/** + * Returns true if Queue is running (e.g. not stopped). + */ @property (nonatomic, readonly) BOOL isRunning; +/** + * Returns true if Queue is performing Job right now + */ @property (nonatomic, readonly) BOOL isActive; -@property (nonatomic) NSUInteger retryLimit; -- (void)enqueueWithData:(id)data forTask:(NSString *)task; ++ (instancetype)defaultQueue; + +- (instancetype)initWithPersistentStore:(id)persistentStore; + +- (void)enqueueJob:(EDQueueJob *)job; - (void)start; - (void)stop; - (void)empty; -- (BOOL)jobExistsForTask:(NSString *)task; -- (BOOL)jobIsActiveForTask:(NSString *)task; -- (NSDictionary *)nextJobForTask:(NSString *)task; +- (NSInteger)jobCount; -@end +- (BOOL)jobExistsForTag:(NSString *)tag; +- (BOOL)jobIsActiveForTag:(NSString *)tag; +- (nullable EDQueueJob *)nextJobForTag:(NSString *)tag; -@protocol EDQueueDelegate -@optional -- (EDQueueResult)queue:(EDQueue *)queue processJob:(NSDictionary *)job; -- (void)queue:(EDQueue *)queue processJob:(NSDictionary *)job completion:(EDQueueCompletionBlock)block; @end + + +NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/EDQueue/EDQueue.m b/EDQueue/EDQueue.m index a26f870..89e7818 100755 --- a/EDQueue/EDQueue.m +++ b/EDQueue/EDQueue.m @@ -15,110 +15,109 @@ NSString *const EDQueueJobDidFail = @"EDQueueJobDidFail"; NSString *const EDQueueDidDrain = @"EDQueueDidDrain"; +static NSString *const EDQueueNameKey = @"name"; +static NSString *const EDQueueDataKey = @"data"; + + +NS_ASSUME_NONNULL_BEGIN + @interface EDQueue () -{ - BOOL _isRunning; - BOOL _isActive; - NSUInteger _retryLimit; -} -@property (nonatomic) EDQueueStorageEngine *engine; -@property (nonatomic, readwrite) NSString *activeTask; +@property (nonatomic, nullable) NSString *activeJobTag; +@property (nonatomic) dispatch_queue_t dispatchQueue; @end -// @implementation EDQueue -@synthesize isRunning = _isRunning; -@synthesize isActive = _isActive; -@synthesize retryLimit = _retryLimit; ++ (instancetype)defaultQueue +{ + static EDQueue *defaultQueue; + static dispatch_once_t onceToken; -#pragma mark - Singleton + dispatch_once(&onceToken, ^{ + EDQueueStorageEngine *fmdbBasedStorage = [[EDQueueStorageEngine alloc] initWithName:@"edqueue.default.v1.0.sqlite"]; -+ (EDQueue *)sharedInstance -{ - static EDQueue *singleton = nil; - static dispatch_once_t once = 0; - dispatch_once(&once, ^{ - singleton = [[self alloc] init]; + defaultQueue = [[EDQueue alloc] initWithPersistentStore:fmdbBasedStorage]; }); - return singleton; -} -#pragma mark - Init + return defaultQueue; +} -- (id)init +- (instancetype)initWithPersistentStore:(id)persistentStore { self = [super init]; if (self) { - _engine = [[EDQueueStorageEngine alloc] init]; - _retryLimit = 4; + _storage = persistentStore; + self.dispatchQueue = dispatch_queue_create("edqueue.serial", DISPATCH_QUEUE_SERIAL); } return self; } -- (void)dealloc -{ - self.delegate = nil; - _engine = nil; -} #pragma mark - Public methods +/** + * Total number of enqueued & valid jobs. + * + * @return {NSInteger} + */ +- (NSInteger)jobCount +{ + return [self.storage jobCount]; +} + /** * Adds a new job to the queue. * - * @param {id} Data - * @param {NSString} Task label + * @param {EDQueueJob} job * * @return {void} */ -- (void)enqueueWithData:(id)data forTask:(NSString *)task +- (void)enqueueJob:(EDQueueJob *)job { - if (data == nil) data = @{}; - [self.engine createJob:data forTask:task]; + [self.storage createJob:job]; [self tick]; } /** - * Returns true if a job exists for this task. + * Returns true if a job exists for this tag. * - * @param {NSString} Task label + * @param {NSString} job tag * * @return {Boolean} */ -- (BOOL)jobExistsForTask:(NSString *)task +- (BOOL)jobExistsForTag:(NSString *)tag { - BOOL jobExists = [self.engine jobExistsForTask:task]; + BOOL jobExists = [self.storage jobExistsForTag:tag]; return jobExists; } /** - * Returns true if the active job if for this task. + * Returns true if the active job if for this tag. * - * @param {NSString} Task label + * @param {NSString} job tag * * @return {Boolean} */ -- (BOOL)jobIsActiveForTask:(NSString *)task +- (BOOL)jobIsActiveForTag:(NSString *)tag { - BOOL jobIsActive = [self.activeTask length] > 0 && [self.activeTask isEqualToString:task]; + BOOL jobIsActive = [self.activeJobTag length] > 0 && [self.activeJobTag isEqualToString:tag]; return jobIsActive; } /** - * Returns the list of jobs for this + * Returns the next job for tag * - * @param {NSString} Task label + * @param {NSString} job tag * * @return {NSArray} */ -- (NSDictionary *)nextJobForTask:(NSString *)task +- (nullable EDQueueJob *)nextJobForTag:(NSString *)tag { - NSDictionary *nextJobForTask = [self.engine fetchJobForTask:task]; - return nextJobForTask; + id item = [self.storage fetchNextJobForTag:tag validForDate:[NSDate date]]; + return item.job; } /** @@ -131,7 +130,10 @@ - (void)start if (!self.isRunning) { _isRunning = YES; [self tick]; - [self performSelectorOnMainThread:@selector(postNotification:) withObject:[NSDictionary dictionaryWithObjectsAndKeys:EDQueueDidStart, @"name", nil, @"data", nil] waitUntilDone:false]; + + NSDictionary *object = @{ EDQueueNameKey : EDQueueDidStart }; + + [self postNotificationOnMainThread:object]; } } @@ -145,7 +147,9 @@ - (void)stop { if (self.isRunning) { _isRunning = NO; - [self performSelectorOnMainThread:@selector(postNotification:) withObject:[NSDictionary dictionaryWithObjectsAndKeys:EDQueueDidStop, @"name", nil, @"data", nil] waitUntilDone:false]; + + NSDictionary *object = @{ EDQueueNameKey : EDQueueDidStop }; + [self postNotificationOnMainThread:object]; } } @@ -159,7 +163,9 @@ - (void)stop */ - (void)empty { - [self.engine removeAllJobs]; + [self.storage removeAllJobs]; + + [self postNotificationOnMainThread:@{ EDQueueNameKey : EDQueueDidDrain }]; } @@ -172,59 +178,101 @@ - (void)empty */ - (void)tick { - dispatch_queue_t gcd = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0); - dispatch_async(gcd, ^{ - if (self.isRunning && !self.isActive && [self.engine fetchJobCount] > 0) { - // Start job - _isActive = YES; - id job = [self.engine fetchJob]; - self.activeTask = [(NSDictionary *)job objectForKey:@"task"]; - - // Pass job to delegate - if ([self.delegate respondsToSelector:@selector(queue:processJob:completion:)]) { - [self.delegate queue:self processJob:job completion:^(EDQueueResult result) { - [self processJob:job withResult:result]; - self.activeTask = nil; - }]; - } else { - EDQueueResult result = [self.delegate queue:self processJob:job]; - [self processJob:job withResult:result]; - self.activeTask = nil; + dispatch_barrier_async(self.dispatchQueue, ^{ + + if (!self.isRunning) { + return; + } + + if (self.isActive) { + return; + } + + if ([self.storage jobCount] > 0) { + + id storedJob = [self.storage fetchNextJobValidForDate:[NSDate date]]; + + if (!storedJob) { + __weak typeof(self) weakSelf = self; + NSTimeInterval nextTime = [self.storage fetchNextJobTimeInterval]; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(nextTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [weakSelf performSelectorOnMainThread:@selector(tick) withObject:nil waitUntilDone:false]; + }); + + return; } + + // Start job & Pass job to delegate + self.activeJobTag = storedJob.job.tag; + _isActive = YES; + + [self.delegate queue:self processJob:storedJob.job completion:^(EDQueueResult result) { + [self processJob:storedJob withResult:result]; + self.activeJobTag = nil; + }]; } }); } -- (void)processJob:(NSDictionary*)job withResult:(EDQueueResult)result +- (void)processJob:(id)storedJob withResult:(EDQueueResult)result { // Check result switch (result) { case EDQueueResultSuccess: - [self performSelectorOnMainThread:@selector(postNotification:) withObject:[NSDictionary dictionaryWithObjectsAndKeys:EDQueueJobDidSucceed, @"name", job, @"data", nil] waitUntilDone:false]; - [self.engine removeJob:[job objectForKey:@"id"]]; + + [self postNotificationOnMainThread:@{ + EDQueueNameKey : EDQueueJobDidSucceed, + EDQueueDataKey : storedJob.job + }]; + + [self.storage removeJob:storedJob]; + break; + case EDQueueResultFail: - [self performSelectorOnMainThread:@selector(postNotification:) withObject:[NSDictionary dictionaryWithObjectsAndKeys:EDQueueJobDidFail, @"name", job, @"data", nil] waitUntilDone:true]; - NSUInteger currentAttempt = [[job objectForKey:@"attempts"] intValue] + 1; - if (currentAttempt < self.retryLimit) { - [self.engine incrementAttemptForJob:[job objectForKey:@"id"]]; + + [self postNotificationOnMainThread:@{ + EDQueueNameKey : EDQueueJobDidFail, + EDQueueDataKey : storedJob.job + }]; + + BOOL shouldRetry = NO; + + if (storedJob.job.maxRetryCount == EDQueueJobInfiniteRetryCount) { + shouldRetry = YES; + } else if(storedJob.job.maxRetryCount > storedJob.attempts.integerValue) { + shouldRetry = YES; + } + + if (shouldRetry) { + [self.storage scheduleNextAttemptForJob:storedJob]; } else { - [self.engine removeJob:[job objectForKey:@"id"]]; + [self.storage removeJob:storedJob]; } + break; case EDQueueResultCritical: - [self performSelectorOnMainThread:@selector(postNotification:) withObject:[NSDictionary dictionaryWithObjectsAndKeys:EDQueueJobDidFail, @"name", job, @"data", nil] waitUntilDone:false]; + + [self postNotificationOnMainThread:@{ + EDQueueNameKey : EDQueueJobDidFail, + EDQueueDataKey : storedJob.job + }]; + [self errorWithMessage:@"Critical error. Job canceled."]; - [self.engine removeJob:[job objectForKey:@"id"]]; + [self.storage removeJob:storedJob]; + break; } // Clean-up _isActive = NO; - + // Drain - if ([self.engine fetchJobCount] == 0) { - [self performSelectorOnMainThread:@selector(postNotification:) withObject:[NSDictionary dictionaryWithObjectsAndKeys:EDQueueDidDrain, @"name", nil, @"data", nil] waitUntilDone:false]; + if ([self.storage jobCount] == 0) { + + [self postNotificationOnMainThread:@{ + EDQueueNameKey : EDQueueDidDrain, + }]; } else { [self performSelectorOnMainThread:@selector(tick) withObject:nil waitUntilDone:false]; } @@ -239,9 +287,13 @@ - (void)processJob:(NSDictionary*)job withResult:(EDQueueResult)result * * @return {void} */ -- (void)postNotification:(NSDictionary *)object +- (void)postNotificationOnMainThread:(NSDictionary *)object { - [[NSNotificationCenter defaultCenter] postNotificationName:[object objectForKey:@"name"] object:[object objectForKey:@"data"]]; + dispatch_async(dispatch_get_main_queue(), ^{ + [[NSNotificationCenter defaultCenter] postNotificationName:[object objectForKey:EDQueueNameKey] + object:[object objectForKey:EDQueueDataKey]]; + }); + } /** @@ -257,3 +309,5 @@ - (void)errorWithMessage:(NSString *)message } @end + +NS_ASSUME_NONNULL_END diff --git a/EDQueue/EDQueueJob.h b/EDQueue/EDQueueJob.h new file mode 100644 index 0000000..35db4de --- /dev/null +++ b/EDQueue/EDQueueJob.h @@ -0,0 +1,34 @@ +// +// EDQueueJob.h +// queue +// +// Created by Oleg Shanyuk on 18/02/16. +// Copyright © 2016 DIY, Co. All rights reserved. +// + +@import Foundation; + +NS_ASSUME_NONNULL_BEGIN + +static NSUInteger const EDQueueJobInfiniteRetryCount = 0; +static NSTimeInterval const EDQueueJobDefaultRetryTimeInterval = 15.0; + + +@interface EDQueueJob : NSObject + +@property(nonatomic, readonly) NSString *tag; +@property(nonatomic, readonly) NSDictionary, id> *userInfo; +@property(nonatomic, readwrite) NSUInteger maxRetryCount; +@property(nonatomic, readwrite) NSTimeInterval retryTimeInterval; +@property(nonatomic) NSDate *expirationDate; + +- (instancetype)initWithTag:(NSString *)tag + userInfo:(nullable NSDictionary, id> *)userInfo; + +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)new NS_UNAVAILABLE; + +@end + + +NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/EDQueue/EDQueueJob.m b/EDQueue/EDQueueJob.m new file mode 100644 index 0000000..4aa06d1 --- /dev/null +++ b/EDQueue/EDQueueJob.m @@ -0,0 +1,33 @@ +// +// EDQueueJob.m +// queue +// +// Created by Oleg Shanyuk on 18/02/16. +// Copyright © 2016 DIY, Co. All rights reserved. +// + +#import "EDQueueJob.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation EDQueueJob + +- (instancetype)initWithTag:(NSString *)tag + userInfo:(nullable NSDictionary, id> *)userInfo +{ + self = [super init]; + + if (self) { + _tag = [tag copy]; + _userInfo = userInfo ? [userInfo copy] : @{}; + _maxRetryCount = EDQueueJobInfiniteRetryCount; + _retryTimeInterval = EDQueueJobDefaultRetryTimeInterval; + _expirationDate = [NSDate distantFuture]; + } + + return self; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/EDQueue/EDQueuePersistentStorageProtocol.h b/EDQueue/EDQueuePersistentStorageProtocol.h new file mode 100644 index 0000000..5cfb2a5 --- /dev/null +++ b/EDQueue/EDQueuePersistentStorageProtocol.h @@ -0,0 +1,42 @@ +// +// EDQueuePersistentStorageProtocol.h +// queue +// +// Created by Oleg Shanyuk on 19/02/16. +// Copyright © 2016 DIY, Co. All rights reserved. +// + +@import Foundation; + +NS_ASSUME_NONNULL_BEGIN + +@class EDQueueJob; + + +@protocol EDQueueStorageItem + +@property(nonatomic, readonly) EDQueueJob *job; + +@property(nonatomic, readonly, nullable) NSNumber *jobID; +@property(nonatomic, readonly) NSNumber *attempts; + +@end + + +@protocol EDQueuePersistentStorage + +- (void)createJob:(EDQueueJob *)job; +- (BOOL)jobExistsForTag:(NSString *)tag; +- (void)scheduleNextAttemptForJob:(id)jid; + +- (void)removeJob:(id)jid; +- (void)removeAllJobs; + +- (NSUInteger)jobCount; +- (nullable id)fetchNextJobValidForDate:(NSDate *)date; +- (nullable id)fetchNextJobForTag:(NSString *)tag validForDate:(NSDate *)date; +- (NSTimeInterval)fetchNextJobTimeInterval; + +@end + +NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/EDQueue/EDQueueStorageEngine.h b/EDQueue/EDQueueStorageEngine.h index aec98f4..9afef0d 100755 --- a/EDQueue/EDQueueStorageEngine.h +++ b/EDQueue/EDQueueStorageEngine.h @@ -6,20 +6,21 @@ // Copyright (c) 2012 DIY, Co. All rights reserved. // -#import +@import Foundation; -@class FMDatabaseQueue; -@interface EDQueueStorageEngine : NSObject +#import "EDQueuePersistentStorageProtocol.h" -@property (retain) FMDatabaseQueue *queue; +NS_ASSUME_NONNULL_BEGIN -- (void)createJob:(id)data forTask:(id)task; -- (BOOL)jobExistsForTask:(id)task; -- (void)incrementAttemptForJob:(NSNumber *)jid; -- (void)removeJob:(NSNumber *)jid; -- (void)removeAllJobs; -- (NSUInteger)fetchJobCount; -- (NSDictionary *)fetchJob; -- (NSDictionary *)fetchJobForTask:(id)task; +@class EDQueueJob; -@end \ No newline at end of file +@interface EDQueueStorageEngine : NSObject + +- (nullable instancetype)initWithName:(NSString *)name; +- (instancetype)init NS_UNAVAILABLE; + ++ (void)deleteDatabaseName:(NSString *)name; + +@end + +NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/EDQueue/EDQueueStorageEngine.m b/EDQueue/EDQueueStorageEngine.m index 9d2e907..58e5fe5 100755 --- a/EDQueue/EDQueueStorageEngine.m +++ b/EDQueue/EDQueueStorageEngine.m @@ -13,23 +13,91 @@ #import "FMDatabasePool.h" #import "FMDatabaseQueue.h" +#import "EDQueueJob.h" + +static NSTimeInterval const DefaultJobSleepInteval = 5.0; +static NSTimeInterval const MaxJobSleepInterval = 60.0; + + +NS_ASSUME_NONNULL_BEGIN + +static NSString *pathForStorageName(NSString *storage) +{ + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask,YES); + NSString *documentsDirectory = [paths objectAtIndex:0]; + NSString *path = [documentsDirectory stringByAppendingPathComponent:storage]; + + return path; +} + +@interface EDQueueStorageEngineJob : NSObject + +- (instancetype)initWithTag:(NSString *)tag + userInfo:(nullable NSDictionary, id> *)userInfo + jobID:(nullable NSNumber *)jobID + atempts:(NSNumber *)attemps; + +@end + +@implementation EDQueueStorageEngineJob + +@synthesize job = _job; +@synthesize jobID = _jobID; +@synthesize attempts = _attempts; + +- (instancetype)initWithTag:(NSString *)tag + userInfo:(nullable NSDictionary, id> *)userInfo + jobID:(nullable NSNumber *)jobID + atempts:(NSNumber *)attemps +{ + self = [super init]; + + if (self) { + + _job = [[EDQueueJob alloc] initWithTag:tag userInfo:userInfo]; + _jobID = [jobID copy]; + _attempts = [attemps copy]; + } + + return self; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"%@ : id:%@, tag: %@",NSStringFromClass([self class]), _jobID, _job.tag]; +} + +@end + +@interface EDQueueStorageEngine() + +@property (retain) FMDatabaseQueue *queue; + +@end + @implementation EDQueueStorageEngine +#pragma mark - Class + ++ (void)deleteDatabaseName:(NSString *)name +{ + [[NSFileManager defaultManager] removeItemAtPath:pathForStorageName(name) error:nil]; +} + #pragma mark - Init -- (id)init +- (nullable instancetype)initWithName:(NSString *)name { self = [super init]; if (self) { - // Database path - NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask,YES); - NSString *documentsDirectory = [paths objectAtIndex:0]; - NSString *path = [documentsDirectory stringByAppendingPathComponent:@"edqueue_0.5.0d.db"]; - - // Allocate the queue - _queue = [[FMDatabaseQueue alloc] initWithPath:path]; - [self.queue inDatabase:^(FMDatabase *db) { - [db executeUpdate:@"CREATE TABLE IF NOT EXISTS queue (id INTEGER PRIMARY KEY, task TEXT NOT NULL, data TEXT NOT NULL, attempts INTEGER DEFAULT 0, stamp STRING DEFAULT (strftime('%s','now')) NOT NULL, udef_1 TEXT, udef_2 TEXT)"]; + _queue = [[FMDatabaseQueue alloc] initWithPath:pathForStorageName(name)]; + + if (!_queue) { + return nil; + } + + [_queue inDatabase:^(FMDatabase *db) { + [db executeUpdate:@"CREATE TABLE IF NOT EXISTS queue (id INTEGER PRIMARY KEY, tag TEXT NOT NULL, data TEXT NOT NULL, attempts INTEGER DEFAULT 0, maxAttempts INTEGER DEFAULT 0, expiration DOUBLE DEFAULT 0, retryTimeInterval DOUBLE DEFAULT 30, lastAttempt DOUBLE DEFAULT 0 )"]; [self _databaseHadError:[db hadError] fromDatabase:db]; }]; } @@ -47,34 +115,47 @@ - (void)dealloc /** * Creates a new job within the datastore. * - * @param {NSString} Data (JSON string) - * @param {NSString} Task name + * @param {EDQueueJob} a Job * * @return {void} */ -- (void)createJob:(id)data forTask:(id)task +- (void)createJob:(EDQueueJob *)job { - NSString *dataString = [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:data options:NSJSONWritingPrettyPrinted error:nil] encoding:NSUTF8StringEncoding]; - + if (!job.userInfo) { + @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"EDQueueJob.userInfo can not be nil" userInfo:nil]; + } + + NSData *data = [NSKeyedArchiver archivedDataWithRootObject:job.userInfo]; + + NSString *dataString = [data base64EncodedStringWithOptions:0]; + + [self.queue inDatabase:^(FMDatabase *db) { - [db executeUpdate:@"INSERT INTO queue (task, data) VALUES (?, ?)", task, dataString]; + + NSTimeInterval expiration = job.expirationDate.timeIntervalSince1970; + + if (expiration == 0) { + expiration = [NSDate distantFuture].timeIntervalSince1970; + } + + [db executeUpdate:@"INSERT INTO queue (tag, data, maxAttempts, expiration, retryTimeInterval) VALUES (?, ?, ?, ?, ?)", job.tag, dataString, @(job.maxRetryCount), @(expiration), @(job.retryTimeInterval)]; [self _databaseHadError:[db hadError] fromDatabase:db]; }]; } /** - * Tells if a job exists for the specified task name. + * Tells if a job exists for the specified tag * - * @param {NSString} Task name + * @param {NSString} tag * * @return {BOOL} */ -- (BOOL)jobExistsForTask:(id)task +- (BOOL)jobExistsForTag:(NSString *)tag { __block BOOL jobExists = NO; [self.queue inDatabase:^(FMDatabase *db) { - FMResultSet *rs = [db executeQuery:@"SELECT count(id) AS count FROM queue WHERE task = ?", task]; + FMResultSet *rs = [db executeQuery:@"SELECT count(id) AS count FROM queue WHERE tag = ?", tag]; [self _databaseHadError:[db hadError] fromDatabase:db]; while ([rs next]) { @@ -94,25 +175,35 @@ - (BOOL)jobExistsForTask:(id)task * * @return {void} */ -- (void)incrementAttemptForJob:(NSNumber *)jid +- (void)scheduleNextAttemptForJob:(id)job { + if (!job.jobID) { + return; + } + [self.queue inDatabase:^(FMDatabase *db) { - [db executeUpdate:@"UPDATE queue SET attempts = attempts + 1 WHERE id = ?", jid]; + [db executeUpdate:@"UPDATE queue SET attempts = attempts + 1, lastAttempt = ? WHERE id = ?", @([NSDate date].timeIntervalSince1970 + job.job.retryTimeInterval), job.jobID]; [self _databaseHadError:[db hadError] fromDatabase:db]; }]; } /** - * Removes a job from the datastore using a specified id. + * Removes a job from the datastore using a specified id. And also removes all Expired jobs. + * (aka clean-up) * * @param {NSNumber} Job id * * @return {void} */ -- (void)removeJob:(NSNumber *)jid +- (void)removeJob:(id)job { + if (!job.jobID) { + return; + } + [self.queue inDatabase:^(FMDatabase *db) { - [db executeUpdate:@"DELETE FROM queue WHERE id = ?", jid]; + NSNumber *expiration = @([NSDate date].timeIntervalSince1970); + [db executeUpdate:@"DELETE FROM queue WHERE id = ? OR expiration < ?", job.jobID, expiration]; [self _databaseHadError:[db hadError] fromDatabase:db]; }]; } @@ -123,7 +214,8 @@ - (void)removeJob:(NSNumber *)jid * @return {void} * */ -- (void)removeAllJobs { +- (void)removeAllJobs +{ [self.queue inDatabase:^(FMDatabase *db) { [db executeUpdate:@"DELETE FROM queue"]; [self _databaseHadError:[db hadError] fromDatabase:db]; @@ -135,12 +227,12 @@ - (void)removeAllJobs { * * @return {uint} */ -- (NSUInteger)fetchJobCount +- (NSUInteger)jobCount { __block NSUInteger count = 0; [self.queue inDatabase:^(FMDatabase *db) { - FMResultSet *rs = [db executeQuery:@"SELECT count(id) AS count FROM queue"]; + FMResultSet *rs = [db executeQuery:@"SELECT count(id) AS count FROM queue WHERE expiration >= ? ",@([NSDate date].timeIntervalSince1970)]; [self _databaseHadError:[db hadError] fromDatabase:db]; while ([rs next]) { @@ -154,16 +246,17 @@ - (NSUInteger)fetchJobCount } /** - * Returns the oldest job from the datastore. + * Returns the oldest valid job from the datastore. * - * @return {NSDictionary} + * @return {id} */ -- (NSDictionary *)fetchJob +- (nullable id)fetchNextJobValidForDate:(NSDate *)date { - __block id job; + __block id job; [self.queue inDatabase:^(FMDatabase *db) { - FMResultSet *rs = [db executeQuery:@"SELECT * FROM queue ORDER BY id ASC LIMIT 1"]; + NSTimeInterval timestamp = date.timeIntervalSince1970; + FMResultSet *rs = [db executeQuery:@"SELECT * FROM queue WHERE lastAttempt <= ? AND expiration >= ? ORDER BY id ASC LIMIT 1", @(timestamp), @(timestamp)]; [self _databaseHadError:[db hadError] fromDatabase:db]; while ([rs next]) { @@ -177,42 +270,93 @@ - (NSDictionary *)fetchJob } /** - * Returns the oldest job for the task from the datastore. + * Returns the oldest valid job from the datastore with specific tag + * + * @return {id} + */ +- (nullable id)fetchNextJobForTag:(NSString *)tag validForDate:(NSDate *)date +{ + __block id job; + + [self.queue inDatabase:^(FMDatabase *db) { + NSTimeInterval timestamp = date.timeIntervalSince1970; + FMResultSet *rs = [db executeQuery:@"SELECT * FROM queue WHERE tag = ? AND lastAttempt <= ? AND expiration >= ? ORDER BY id ASC LIMIT 1", tag, @(timestamp), @(timestamp)]; + [self _databaseHadError:[db hadError] fromDatabase:db]; + + while ([rs next]) { + job = [self _jobFromResultSet:rs]; + } + + [rs close]; + }]; + + return job; +} + +/** + * Returns the minumum timeout for starting the next job. * - * @param {id} Task label + * @param {id} tag * - * @return {NSDictionary} + * @return {id} */ -- (NSDictionary *)fetchJobForTask:(id)task +- (NSTimeInterval)fetchNextJobTimeInterval { - __block id job; - + + __block NSTimeInterval timeInterval = DefaultJobSleepInteval; + [self.queue inDatabase:^(FMDatabase *db) { - FMResultSet *rs = [db executeQuery:@"SELECT * FROM queue WHERE task = ? ORDER BY id ASC LIMIT 1", task]; + + FMResultSet *rs = [db executeQuery:@"SELECT * FROM queue WHERE lastAttempt < expiration ORDER BY lastAttempt ASC LIMIT 1"]; [self _databaseHadError:[db hadError] fromDatabase:db]; while ([rs next]) { - job = [self _jobFromResultSet:rs]; + timeInterval = [rs doubleForColumn:@"lastAttempt"]; + + timeInterval = fabs(timeInterval - [NSDate date].timeIntervalSince1970); + + if (timeInterval > MaxJobSleepInterval) { + timeInterval = MaxJobSleepInterval; + } } [rs close]; }]; - return job; + return timeInterval; } #pragma mark - Private methods -- (NSDictionary *)_jobFromResultSet:(FMResultSet *)rs +- (id)_jobFromResultSet:(FMResultSet *)rs { - NSDictionary *job = @{ - @"id": [NSNumber numberWithInt:[rs intForColumn:@"id"]], - @"task": [rs stringForColumn:@"task"], - @"data": [NSJSONSerialization JSONObjectWithData:[[rs stringForColumn:@"data"] dataUsingEncoding:NSUTF8StringEncoding] options:NSJSONReadingMutableContainers error:nil], - @"attempts": [NSNumber numberWithInt:[rs intForColumn:@"attempts"]], - @"stamp": [rs stringForColumn:@"stamp"] - }; - return job; + NSString *encodedString = [rs stringForColumn:@"data"]; + NSDictionary *userInfo; + @try { + NSData *data = [[NSData alloc] initWithBase64EncodedString:encodedString options:0]; + if (encodedString && !data) { + @throw [NSException exceptionWithName:NSInvalidArchiveOperationException reason:nil userInfo:nil]; + } + userInfo = [NSKeyedUnarchiver unarchiveObjectWithData:data]; + } @catch (NSException *exception) { + /* respect previous JSON serialization [though, good idea to move serializer out of storage] */ + userInfo = [NSJSONSerialization JSONObjectWithData:[encodedString dataUsingEncoding:NSUTF8StringEncoding] options:NSJSONReadingMutableContainers error:nil]; + } + + EDQueueStorageEngineJob *storedItem = [[EDQueueStorageEngineJob alloc] initWithTag:[rs stringForColumn:@"tag"] + userInfo:userInfo + jobID:@([rs intForColumn:@"id"]) + atempts:@([rs intForColumn:@"attempts"])]; + + storedItem.job.maxRetryCount = [rs intForColumn:@"maxAttempts"]; + storedItem.job.retryTimeInterval = [rs doubleForColumn:@"retryTimeInterval"]; + + NSTimeInterval expiration = [rs doubleForColumn:@"expiration"]; + + storedItem.job.expirationDate = [NSDate dateWithTimeIntervalSince1970:expiration]; + + + return storedItem; } - (BOOL)_databaseHadError:(BOOL)flag fromDatabase:(FMDatabase *)db @@ -222,3 +366,5 @@ - (BOOL)_databaseHadError:(BOOL)flag fromDatabase:(FMDatabase *)db } @end + +NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md index 1226c79..9b005d4 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,4 @@ -Copyright (c) 2012 Andrew Sliwinski +Copyright (c) 2012 Andrew Sliwinski, 2016 Oleg Shanyuk Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/Project/Default-568h@2x.png b/Project/Default-568h@2x.png new file mode 100644 index 0000000..0891b7a Binary files /dev/null and b/Project/Default-568h@2x.png differ diff --git a/Project/queue.xcodeproj/project.pbxproj b/Project/queue.xcodeproj/project.pbxproj index 90332f1..70960c4 100644 --- a/Project/queue.xcodeproj/project.pbxproj +++ b/Project/queue.xcodeproj/project.pbxproj @@ -7,6 +7,10 @@ objects = { /* Begin PBXBuildFile section */ + 7C57D1AD1C7DF458009C794E /* EDQueueStorageEngineTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7C57D1AC1C7DF458009C794E /* EDQueueStorageEngineTests.m */; }; + 7CA53AF41C75F88E00420814 /* EDQueueJob.m in Sources */ = {isa = PBXBuildFile; fileRef = 7CA53AF31C75F88E00420814 /* EDQueueJob.m */; }; + 7CA53AF61C7733E400420814 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 7CA53AF51C7733E400420814 /* Default-568h@2x.png */; }; + 7CA53B061C77496800420814 /* EDQueueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7CA53B051C77496800420814 /* EDQueueTests.m */; }; C32F6E07160795C3004BA8A1 /* EDQueue.podspec in Resources */ = {isa = PBXBuildFile; fileRef = C32F6E06160795C3004BA8A1 /* EDQueue.podspec */; }; C32F6E0C16079680004BA8A1 /* libsqlite3.0.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = C32F6E0B16079680004BA8A1 /* libsqlite3.0.dylib */; }; C32F6E191607AC35004BA8A1 /* FMDatabase.m in Sources */ = {isa = PBXBuildFile; fileRef = C32F6E101607AC35004BA8A1 /* FMDatabase.m */; }; @@ -28,7 +32,25 @@ C395D534159E57880041510C /* EDQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = C395D533159E57880041510C /* EDQueue.m */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 7CA53B001C77495200420814 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C395D4FF159E56760041510C /* Project object */; + proxyType = 1; + remoteGlobalIDString = C395D507159E56760041510C; + remoteInfo = queue; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXFileReference section */ + 7C57D1AC1C7DF458009C794E /* EDQueueStorageEngineTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = EDQueueStorageEngineTests.m; sourceTree = ""; }; + 7CA53AF21C75F88E00420814 /* EDQueueJob.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = EDQueueJob.h; path = ../EDQueue/EDQueueJob.h; sourceTree = ""; }; + 7CA53AF31C75F88E00420814 /* EDQueueJob.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = EDQueueJob.m; path = ../EDQueue/EDQueueJob.m; sourceTree = ""; }; + 7CA53AF51C7733E400420814 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-568h@2x.png"; sourceTree = ""; }; + 7CA53AFB1C77495200420814 /* queueTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = queueTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 7CA53AFF1C77495200420814 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 7CA53B051C77496800420814 /* EDQueueTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = EDQueueTests.m; sourceTree = ""; }; + 7CA53B071C774EE900420814 /* EDQueuePersistentStorageProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = EDQueuePersistentStorageProtocol.h; path = ../EDQueue/EDQueuePersistentStorageProtocol.h; sourceTree = ""; }; C32F6E06160795C3004BA8A1 /* EDQueue.podspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = EDQueue.podspec; path = ../EDQueue.podspec; sourceTree = ""; }; C32F6E0B16079680004BA8A1 /* libsqlite3.0.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libsqlite3.0.dylib; path = usr/lib/libsqlite3.0.dylib; sourceTree = SDKROOT; }; C32F6E0F1607AC35004BA8A1 /* FMDatabase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FMDatabase.h; sourceTree = ""; }; @@ -63,6 +85,13 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 7CA53AF81C77495200420814 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; C395D505159E56760041510C /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -77,6 +106,16 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 7CA53AFC1C77495200420814 /* queueTests */ = { + isa = PBXGroup; + children = ( + 7CA53B051C77496800420814 /* EDQueueTests.m */, + 7C57D1AC1C7DF458009C794E /* EDQueueStorageEngineTests.m */, + 7CA53AFF1C77495200420814 /* Info.plist */, + ); + path = queueTests; + sourceTree = ""; + }; C32F6E05160793FB004BA8A1 /* EDQueue */ = { isa = PBXGroup; children = ( @@ -84,6 +123,9 @@ C395D533159E57880041510C /* EDQueue.m */, C32F6E221607E1FB004BA8A1 /* EDQueueStorageEngine.h */, C32F6E231607E1FB004BA8A1 /* EDQueueStorageEngine.m */, + 7CA53AF21C75F88E00420814 /* EDQueueJob.h */, + 7CA53AF31C75F88E00420814 /* EDQueueJob.m */, + 7CA53B071C774EE900420814 /* EDQueuePersistentStorageProtocol.h */, ); name = EDQueue; sourceTree = ""; @@ -116,12 +158,14 @@ C395D4FD159E56760041510C = { isa = PBXGroup; children = ( + 7CA53AF51C7733E400420814 /* Default-568h@2x.png */, C395D52D159E57520041510C /* LICENSE.md */, C395D52E159E57520041510C /* README.md */, C32F6E06160795C3004BA8A1 /* EDQueue.podspec */, C32F6E05160793FB004BA8A1 /* EDQueue */, C32F6E0D1607AC13004BA8A1 /* Lib */, C395D512159E56760041510C /* Example */, + 7CA53AFC1C77495200420814 /* queueTests */, C395D50B159E56760041510C /* Frameworks */, C395D509159E56760041510C /* Products */, ); @@ -131,6 +175,7 @@ isa = PBXGroup; children = ( C395D508159E56760041510C /* queue.app */, + 7CA53AFB1C77495200420814 /* queueTests.xctest */, ); name = Products; sourceTree = ""; @@ -174,6 +219,24 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 7CA53AFA1C77495200420814 /* queueTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7CA53B021C77495200420814 /* Build configuration list for PBXNativeTarget "queueTests" */; + buildPhases = ( + 7CA53AF71C77495200420814 /* Sources */, + 7CA53AF81C77495200420814 /* Frameworks */, + 7CA53AF91C77495200420814 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 7CA53B011C77495200420814 /* PBXTargetDependency */, + ); + name = queueTests; + productName = queueTests; + productReference = 7CA53AFB1C77495200420814 /* queueTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; C395D507159E56760041510C /* queue */ = { isa = PBXNativeTarget; buildConfigurationList = C395D526159E56760041510C /* Build configuration list for PBXNativeTarget "queue" */; @@ -198,8 +261,14 @@ isa = PBXProject; attributes = { CLASSPREFIX = ED; - LastUpgradeCheck = 0460; + LastUpgradeCheck = 0720; ORGANIZATIONNAME = "DIY, Co."; + TargetAttributes = { + 7CA53AFA1C77495200420814 = { + CreatedOnToolsVersion = 7.2.1; + TestTargetID = C395D507159E56760041510C; + }; + }; }; buildConfigurationList = C395D502159E56760041510C /* Build configuration list for PBXProject "queue" */; compatibilityVersion = "Xcode 3.2"; @@ -214,11 +283,19 @@ projectRoot = ""; targets = ( C395D507159E56760041510C /* queue */, + 7CA53AFA1C77495200420814 /* queueTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 7CA53AF91C77495200420814 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; C395D506159E56760041510C /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -228,16 +305,27 @@ C395D52F159E57520041510C /* LICENSE.md in Resources */, C395D530159E57520041510C /* README.md in Resources */, C32F6E07160795C3004BA8A1 /* EDQueue.podspec in Resources */, + 7CA53AF61C7733E400420814 /* Default-568h@2x.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 7CA53AF71C77495200420814 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7C57D1AD1C7DF458009C794E /* EDQueueStorageEngineTests.m in Sources */, + 7CA53B061C77496800420814 /* EDQueueTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; C395D504159E56760041510C /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7CA53AF41C75F88E00420814 /* EDQueueJob.m in Sources */, C395D519159E56760041510C /* main.m in Sources */, C395D51D159E56760041510C /* EDAppDelegate.m in Sources */, C395D520159E56760041510C /* EDViewController.m in Sources */, @@ -253,6 +341,14 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 7CA53B011C77495200420814 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = C395D507159E56760041510C /* queue */; + targetProxy = 7CA53B001C77495200420814 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ C395D515159E56760041510C /* InfoPlist.strings */ = { isa = PBXVariantGroup; @@ -273,19 +369,71 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 7CA53B031C77495200420814 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + DEBUG_INFORMATION_FORMAT = dwarf; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + INFOPLIST_FILE = queueTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.2; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = YES; + PRODUCT_BUNDLE_IDENTIFIER = oleg.shanyuk.queueTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/queue.app/queue"; + }; + name = Debug; + }; + 7CA53B041C77495200420814 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + INFOPLIST_FILE = queueTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.2; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = oleg.shanyuk.queueTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/queue.app/queue"; + }; + name = Release; + }; C395D524159E56760041510C /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - ARCHS = "$(ARCHS_STANDARD_32_BIT)"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", @@ -293,10 +441,14 @@ ); GCC_SYMBOLS_PRIVATE_EXTERN = NO; GCC_VERSION = com.apple.compilers.llvm.clang.1_0; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 5.1; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; }; name = Debug; @@ -305,19 +457,27 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - ARCHS = "$(ARCHS_STANDARD_32_BIT)"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = YES; + ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; GCC_VERSION = com.apple.compilers.llvm.clang.1_0; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 5.1; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; OTHER_CFLAGS = "-DNS_BLOCK_ASSERTIONS=1"; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; @@ -327,10 +487,13 @@ C395D527159E56760041510C /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "queue/queue-Prefix.pch"; INFOPLIST_FILE = "queue/queue-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.diy.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; WRAPPER_EXTENSION = app; }; @@ -339,10 +502,13 @@ C395D528159E56760041510C /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "queue/queue-Prefix.pch"; INFOPLIST_FILE = "queue/queue-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.diy.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; WRAPPER_EXTENSION = app; }; @@ -351,6 +517,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 7CA53B021C77495200420814 /* Build configuration list for PBXNativeTarget "queueTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7CA53B031C77495200420814 /* Debug */, + 7CA53B041C77495200420814 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; C395D502159E56760041510C /* Build configuration list for PBXProject "queue" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Project/queue/EDAppDelegate.h b/Project/queue/EDAppDelegate.h index 3fdc149..1f77188 100644 --- a/Project/queue/EDAppDelegate.h +++ b/Project/queue/EDAppDelegate.h @@ -6,14 +6,15 @@ // Copyright (c) 2012 Andrew Sliwinski. All rights reserved. // -#import +@import UIKit; + #import "EDQueue.h" @class EDViewController; @interface EDAppDelegate : UIResponder -@property (strong, nonatomic) UIWindow *window; -@property (strong, nonatomic) EDViewController *viewController; +@property (nonatomic) UIWindow *window; +@property (nonatomic) EDViewController *viewController; @end \ No newline at end of file diff --git a/Project/queue/EDAppDelegate.m b/Project/queue/EDAppDelegate.m index 385978f..a7fe2fe 100644 --- a/Project/queue/EDAppDelegate.m +++ b/Project/queue/EDAppDelegate.m @@ -8,12 +8,10 @@ #import "EDAppDelegate.h" #import "EDViewController.h" +#import "EDQueueStorageEngine.h" @implementation EDAppDelegate -@synthesize window = _window; -@synthesize viewController = _viewController; - - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; @@ -28,23 +26,23 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( - (void)applicationDidBecomeActive:(UIApplication *)application { - [[EDQueue sharedInstance] setDelegate:self]; - [[EDQueue sharedInstance] start]; + [[EDQueue defaultQueue] setDelegate:self]; + [[EDQueue defaultQueue] start]; } - (void)applicationWillResignActive:(UIApplication *)application { - [[EDQueue sharedInstance] stop]; + [[EDQueue defaultQueue] stop]; } -- (void)queue:(EDQueue *)queue processJob:(NSDictionary *)job completion:(void (^)(EDQueueResult))block +- (void)queue:(EDQueue *)queue processJob:(EDQueueJob *)job completion:(void (^)(EDQueueResult))block { sleep(1); @try { - if ([[job objectForKey:@"task"] isEqualToString:@"success"]) { + if ([job.tag isEqualToString:@"success"]) { block(EDQueueResultSuccess); - } else if ([[job objectForKey:@"task"] isEqualToString:@"fail"]) { + } else if ([job.tag isEqualToString:@"fail"]) { block(EDQueueResultFail); } else { block(EDQueueResultCritical); @@ -54,27 +52,6 @@ - (void)queue:(EDQueue *)queue processJob:(NSDictionary *)job completion:(void ( block(EDQueueResultCritical); } } - -//- (EDQueueResult)queue:(EDQueue *)queue processJob:(NSDictionary *)job -//{ -// sleep(1); -// -// @try { -// if ([[job objectForKey:@"task"] isEqualToString:@"success"]) { -// return EDQueueResultSuccess; -// } else if ([[job objectForKey:@"task"] isEqualToString:@"fail"]) { -// return EDQueueResultFail; -// } -// } -// @catch (NSException *exception) { -// return EDQueueResultCritical; -// } -// -// return EDQueueResultCritical; -//} - -// - - (void)applicationDidEnterBackground:(UIApplication *)application { // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. diff --git a/Project/queue/EDViewController.h b/Project/queue/EDViewController.h index a946793..32690c3 100644 --- a/Project/queue/EDViewController.h +++ b/Project/queue/EDViewController.h @@ -11,10 +11,12 @@ @interface EDViewController : UIViewController -@property (nonatomic, retain) IBOutlet UITextView *activity; +@property (weak, nonatomic) IBOutlet UILabel *activityTitle; +@property (nonatomic) IBOutlet UITextView *activity; - (IBAction)addSuccess:(id)sender; - (IBAction)addFail:(id)sender; - (IBAction)addCritical:(id)sender; +- (IBAction)clearQueue:(id)sender; @end \ No newline at end of file diff --git a/Project/queue/EDViewController.m b/Project/queue/EDViewController.m index aa1a8b9..d8bf3c4 100644 --- a/Project/queue/EDViewController.m +++ b/Project/queue/EDViewController.m @@ -36,17 +36,26 @@ - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interface - (IBAction)addSuccess:(id)sender { - [[EDQueue sharedInstance] enqueueWithData:@{ @"nyan" : @"cat" } forTask:@"success"]; + EDQueueJob *success = [[EDQueueJob alloc] initWithTag:@"success" userInfo:@{ @"nyan" : @"cat" }]; + [[EDQueue defaultQueue] enqueueJob:success]; } - (IBAction)addFail:(id)sender { - [[EDQueue sharedInstance] enqueueWithData:nil forTask:@"fail"]; + EDQueueJob *fail = [[EDQueueJob alloc] initWithTag:@"fail" userInfo:nil]; + fail.maxRetryCount = 10; + [[EDQueue defaultQueue] enqueueJob:fail]; } - (IBAction)addCritical:(id)sender { - [[EDQueue sharedInstance] enqueueWithData:nil forTask:@"critical"]; + EDQueueJob *critical = [[EDQueueJob alloc] initWithTag:@"critical" userInfo:nil]; + [[EDQueue defaultQueue] enqueueJob:critical]; +} + +- (IBAction)clearQueue:(id)sender +{ + [[EDQueue defaultQueue] empty]; } #pragma mark - Notifications @@ -55,6 +64,8 @@ - (void)receivedNotification:(NSNotification *)notification { self.activity.text = [NSString stringWithFormat:@"%@%@\n", self.activity.text, notification]; [self.activity scrollRangeToVisible:NSMakeRange([self.activity.text length], 0)]; + + self.activityTitle.text = [NSString stringWithFormat:@"Activity: %ld",(long)[[EDQueue defaultQueue] jobCount]]; } #pragma mark - Dealloc diff --git a/Project/queue/en.lproj/EDViewController.xib b/Project/queue/en.lproj/EDViewController.xib index a822a86..4f0ba13 100644 --- a/Project/queue/en.lproj/EDViewController.xib +++ b/Project/queue/en.lproj/EDViewController.xib @@ -1,376 +1,80 @@ - - - - 1296 - 11E53 - 2182 - 1138.47 - 569.00 - - com.apple.InterfaceBuilder.IBCocoaTouchPlugin - 1181 - - - IBUITextView - IBUIButton - IBUIView - IBUILabel - IBProxyObject - - - com.apple.InterfaceBuilder.IBCocoaTouchPlugin - - - PluginDependencyRecalculationVersion - - - - - IBFilesOwner - IBCocoaTouchFramework - - - IBFirstResponder - IBCocoaTouchFramework - - - - 274 - - - - 292 - {{20, 403}, {280, 37}} - - - - _NS:9 - NO - IBCocoaTouchFramework - 0 - 0 - 1 - Add A Critically Bad Job - - 3 - MQA - - - 1 - MC4xOTYwNzg0MzQ2IDAuMzA5ODAzOTMyOSAwLjUyMTU2ODY1NgA - - - 3 - MC41AA - - - 2 - 15 - - - Helvetica-Bold - 15 - 16 - - - - - 292 - {{20, 358}, {280, 37}} - - - - _NS:9 - NO - IBCocoaTouchFramework - 0 - 0 - 1 - Add A Bad Job - - - 1 - MC4xOTYwNzg0MzQ2IDAuMzA5ODAzOTMyOSAwLjUyMTU2ODY1NgA - - - - - - - - 292 - {{20, 313}, {280, 37}} - - - - _NS:9 - NO - IBCocoaTouchFramework - 0 - 0 - 1 - Add A Good Job - - - 1 - MC4xOTYwNzg0MzQ2IDAuMzA5ODAzOTMyOSAwLjUyMTU2ODY1NgA - - - - - - - - 292 - {{20, 20}, {61, 21}} - - - - _NS:9 - NO - YES - 7 - NO - IBCocoaTouchFramework - Activity: - - 1 - MCAwIDAAA - - - 0 - 10 - - 1 - 17 - - - Helvetica - 17 - 16 - - - - - 292 - {{20, 49}, {280, 240}} - - - - _NS:9 - - 1 - MSAxIDEAA - - YES - YES - IBCocoaTouchFramework - - - 2 - IBCocoaTouchFramework - - - Courier - Courier - 0 - 10 - - - Courier - 10 - 16 - - - - {{0, 20}, {320, 460}} - - - - - 3 - MC43NQA - - 2 - - - NO - - IBCocoaTouchFramework - - - - - - - view - - - - 7 - - - - activity - - - - 11 - - - - addCritical: - - - 7 - - 20 - - - - addFail: - - - 7 - - 19 - - - - addSuccess: - - - 7 - - 18 - - - - - - 0 - - - - - - -1 - - - File's Owner - - - -2 - - - - - 6 - - - - - - - - - - - - 8 - - - - - 9 - - - - - 10 - - - - - 14 - - - - - 16 - - - - - - - EDViewController - com.apple.InterfaceBuilder.IBCocoaTouchPlugin - UIResponder - com.apple.InterfaceBuilder.IBCocoaTouchPlugin - com.apple.InterfaceBuilder.IBCocoaTouchPlugin - com.apple.InterfaceBuilder.IBCocoaTouchPlugin - com.apple.InterfaceBuilder.IBCocoaTouchPlugin - com.apple.InterfaceBuilder.IBCocoaTouchPlugin - com.apple.InterfaceBuilder.IBCocoaTouchPlugin - com.apple.InterfaceBuilder.IBCocoaTouchPlugin - - - - - - 20 - - - - - EDViewController - UIViewController - - id - id - id - - - - addCritical: - id - - - addFail: - id - - - addSuccess: - id - - - - activity - UITextView - - - activity - - activity - UITextView - - - - IBProjectSource - ./Classes/EDViewController.h - - - - - 0 - IBCocoaTouchFramework - - com.apple.InterfaceBuilder.CocoaTouchPlugin.iPhoneOS - - - YES - 3 - 1181 - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Project/queue/queue-Info.plist b/Project/queue/queue-Info.plist index 36f966e..3eb7025 100644 --- a/Project/queue/queue-Info.plist +++ b/Project/queue/queue-Info.plist @@ -9,7 +9,7 @@ CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier - com.diy.${PRODUCT_NAME:rfc1034identifier} + $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName diff --git a/Project/queueTests/EDQueueStorageEngineTests.m b/Project/queueTests/EDQueueStorageEngineTests.m new file mode 100644 index 0000000..dbc6919 --- /dev/null +++ b/Project/queueTests/EDQueueStorageEngineTests.m @@ -0,0 +1,146 @@ +// +// EDQueueStorageEngineTests.m +// queue +// +// Created by Oleg Shanyuk on 24/02/16. +// Copyright © 2016 DIY, Co. All rights reserved. +// + +#import +#import "EDQueueStorageEngine.h" +#import "EDQueueJob.h" + +@interface EDQueueStorageEngineTests : XCTestCase + +@end + +@implementation EDQueueStorageEngineTests + +- (void)tearDown { + + [EDQueueStorageEngine deleteDatabaseName:@"test.db"]; + + [super tearDown]; +} + +- (void)testDefaultJobAdded +{ + EDQueueStorageEngine *testEngine = [[EDQueueStorageEngine alloc] initWithName:@"test.db"]; + + EDQueueJob *job = [[EDQueueJob alloc] initWithTag:@"tag" userInfo:@{@"user":@"info"}]; + + [testEngine createJob:job]; + + id item = [testEngine fetchNextJobValidForDate:[NSDate date]]; + + XCTAssertNotNil(item); + + XCTAssertEqual(item.job.expirationDate, [NSDate distantFuture]); + XCTAssertEqual(item.job.retryTimeInterval, job.retryTimeInterval); + XCTAssertEqual(item.job.maxRetryCount, job.maxRetryCount); + + XCTAssertEqualObjects(item.job.userInfo, job.userInfo); + XCTAssertEqualObjects(item.job.tag, job.tag); +} + +- (void)testAddExpiredJob +{ + EDQueueStorageEngine *testEngine = [[EDQueueStorageEngine alloc] initWithName:@"test.db"]; + + EDQueueJob *job = [[EDQueueJob alloc] initWithTag:@"tag" userInfo:@{@"user":@"info"}]; + + job.expirationDate = [[NSDate date] dateByAddingTimeInterval:-1]; + + id item = [testEngine fetchNextJobValidForDate:[NSDate date]]; + + XCTAssertNil(item); +} + +- (void)testJobCountOnEmptyDatabase +{ + EDQueueStorageEngine *testEngine = [[EDQueueStorageEngine alloc] initWithName:@"test.db"]; + + XCTAssertEqual([testEngine jobCount], 0); +} + +- (void)testJobCountWithExpiredJobs +{ + EDQueueStorageEngine *testEngine = [[EDQueueStorageEngine alloc] initWithName:@"test.db"]; + + EDQueueJob *job = [[EDQueueJob alloc] initWithTag:@"tag" userInfo:@{@"user":@"info"}]; + + job.expirationDate = [[NSDate date] dateByAddingTimeInterval:-1]; + + XCTAssertEqual([testEngine jobCount], 0); +} + +- (void)testJobCountWithOneJob +{ + EDQueueStorageEngine *testEngine = [[EDQueueStorageEngine alloc] initWithName:@"test.db"]; + + EDQueueJob *job = [[EDQueueJob alloc] initWithTag:@"tag" userInfo:@{@"user":@"info"}]; + + [testEngine createJob:job]; + + XCTAssertEqual([testEngine jobCount], 1); +} + + +- (void)testAddJobThatExpires +{ + EDQueueStorageEngine *testEngine = [[EDQueueStorageEngine alloc] initWithName:@"test.db"]; + + EDQueueJob *job = [[EDQueueJob alloc] initWithTag:@"tag" userInfo:@{@"user":@"info"}]; + + NSDate *expirationDate = [NSDate date]; + + job.expirationDate = expirationDate; + + [testEngine createJob:job]; + + id itemInvalid = [testEngine fetchNextJobValidForDate:[expirationDate dateByAddingTimeInterval:1]]; + + XCTAssertNil(itemInvalid); + + id itemValid = [testEngine fetchNextJobValidForDate:[expirationDate dateByAddingTimeInterval:-1]]; + + XCTAssertNotNil(itemValid); +} + +- (void)testScheduleNextAttemptForJob +{ + EDQueueStorageEngine *testEngine = [[EDQueueStorageEngine alloc] initWithName:@"test.db"]; + + EDQueueJob *job = [[EDQueueJob alloc] initWithTag:@"tag" userInfo:@{@"user":@"info"}]; + + [testEngine createJob:job]; + + id item = [testEngine fetchNextJobValidForDate:[NSDate date]]; + + XCTAssertEqual(item.attempts.integerValue, 0); + + [testEngine scheduleNextAttemptForJob:item]; + + id itemIncreased = [testEngine fetchNextJobValidForDate:[[NSDate date] dateByAddingTimeInterval:job.retryTimeInterval]]; + + XCTAssertEqual(itemIncreased.attempts.integerValue, 1); +} + +- (void)testFetchNextJobTimeInterval +{ + EDQueueStorageEngine *testEngine = [[EDQueueStorageEngine alloc] initWithName:@"test.db"]; + + EDQueueJob *job = [[EDQueueJob alloc] initWithTag:@"tag" userInfo:@{@"user":@"info"}]; + + [testEngine createJob:job]; + + id item = [testEngine fetchNextJobValidForDate:[NSDate date]]; + + [testEngine scheduleNextAttemptForJob:item]; + + NSTimeInterval nextTimeInterval = [testEngine fetchNextJobTimeInterval]; + + XCTAssertEqualWithAccuracy(nextTimeInterval, job.retryTimeInterval, 2); +} + +@end diff --git a/Project/queueTests/EDQueueTests.m b/Project/queueTests/EDQueueTests.m new file mode 100644 index 0000000..e11feea --- /dev/null +++ b/Project/queueTests/EDQueueTests.m @@ -0,0 +1,179 @@ +// +// EDQueueTests.m +// queue +// +// Created by Oleg Shanyuk on 19/02/16. +// Copyright © 2016 DIY, Co. All rights reserved. +// + +#import +#import "EDQueue.h" +#import "EDQueueStorageEngine.h" + +static NSString *const EQTestDatabaseName = @"database.test.sqlite"; +static NSString *const EQExpectationKey = @"EK"; + +@interface EDQueueTests : XCTestCase +@property (nonatomic) EDQueue *queue; +@property (nonatomic) NSMutableDictionary *expectationHashes; +@end + +@implementation EDQueueTests + +-(void)queue:(EDQueue *)queue processJob:(EDQueueJob *)job completion:(EDQueueCompletionBlock)block +{ + NSString *expectationHash = (NSString *)job.userInfo[EQExpectationKey]; + + XCTestExpectation *expectation = self.expectationHashes[expectationHash]; + + NSLog(@"Testing(%p): %@ -> %@", self, expectationHash, expectation); + + [expectation fulfill]; + + block(EDQueueResultSuccess); +} + +- (void)setUp { + EDQueueStorageEngine *fmdbBasedStorage = [[EDQueueStorageEngine alloc] initWithName:EQTestDatabaseName]; + + self.queue = [[EDQueue alloc] initWithPersistentStore:fmdbBasedStorage]; + + self.queue.delegate = self; + + self.expectationHashes = [NSMutableDictionary dictionary]; +} + +- (void)tearDown +{ + self.queue = nil; + + [EDQueueStorageEngine deleteDatabaseName:EQTestDatabaseName]; +} + +- (void)testQueueStart +{ + [self.queue start]; + + XCTAssertTrue(self.queue.isRunning); +} + +- (void)testQueueStop +{ + [self.queue start]; + [self.queue stop]; + + XCTAssertFalse(self.queue.isRunning); +} + +- (void)testQueueStartThenAddJob +{ + XCTestExpectation *expectation = [self expectationWithDescription:@"testQueueStartThenAddJob"]; + + NSString *expectationHash = [NSString stringWithFormat:@"%p", expectation]; + + self.expectationHashes[expectationHash] = expectation; + + EDQueueJob *job = [[EDQueueJob alloc] initWithTag:@"testTask" userInfo:@{EQExpectationKey : expectationHash}]; + + NSLog(@"Added: %@", expectationHash); + + [self.queue start]; + + [self.queue enqueueJob:job]; + + [self waitForExpectationsWithTimeout:1000.5 handler:nil]; +} + +- (void)testQueueAddJobThenStart +{ + XCTestExpectation *expectation = [self expectationWithDescription:@"testQueueAddJobThenStart"]; + + NSString *expectationHash = [NSString stringWithFormat:@"%p", expectation]; + + self.expectationHashes[expectationHash] = expectation; + + EDQueueJob *job = [[EDQueueJob alloc] initWithTag:@"testTask" userInfo:@{EQExpectationKey : expectationHash}]; + + [self.queue enqueueJob:job]; + + [self.queue start]; + + [self waitForExpectationsWithTimeout:0.5 handler:nil]; +} + +- (void)testJobExistsForTagAndEmpty +{ + EDQueueJob *job = [[EDQueueJob alloc] initWithTag:@"testTask" userInfo:@{}]; + + self.queue.delegate = nil; // queue won't be able to complete task w/o delegate. so, we have time to check it + + [self.queue enqueueJob:job]; + + XCTAssertTrue([self.queue jobExistsForTag:@"testTask"]); + + [self.queue empty]; + + XCTAssertFalse([self.queue jobExistsForTag:@"testTask"]); +} + +- (void)testJobDoesNotExistForTag +{ + EDQueueJob *job = [[EDQueueJob alloc] initWithTag:@"testTask" userInfo:@{}]; + + self.queue.delegate = nil; // queue won't be able to complete task w/o delegate. so, we have time to check it + + [self.queue enqueueJob:job]; + + XCTAssertFalse([self.queue jobExistsForTag:@"testTaskFalse"]); +} + + +- (void)testJobIsActiveForTag +{ + EDQueueJob *job = [[EDQueueJob alloc] initWithTag:@"testTask" userInfo:@{}]; + + self.queue.delegate = nil; // queue won't be able to complete task w/o delegate. so, we have time to check it + + [self.queue enqueueJob:job]; + + XCTAssertFalse([self.queue jobIsActiveForTag:@"testTask"]); + + [self.queue start]; + + sleep(1); + + XCTAssertTrue([self.queue jobIsActiveForTag:@"testTask"]); +} + +-(void)testNextJobForTag +{ + EDQueueJob *job = [[EDQueueJob alloc] initWithTag:@"testTask" userInfo:@{@"testId":@"uniqueForThisTest"}]; + + self.queue.delegate = nil; // queue won't be able to complete task w/o delegate. so, we have time to check it + + [self.queue enqueueJob:job]; + + EDQueueJob *nextJob = [self.queue nextJobForTag:@"testTask"]; + + XCTAssertNotNil(nextJob); + + XCTAssertEqualObjects(nextJob.userInfo[@"testId"], @"uniqueForThisTest"); +} + +- (void)testIfQueuePersists +{ + EDQueueJob *job = [[EDQueueJob alloc] initWithTag:@"testTaskUniqueName" userInfo:@{@"test":@"test"}]; + + self.queue.delegate = nil; // queue won't be able to complete task w/o delegate. so, we have time to check it + + [self.queue enqueueJob:job]; + + self.queue = nil; + + [self setUp]; + + XCTAssertTrue([self.queue jobExistsForTag:@"testTaskUniqueName"]); +} + + +@end diff --git a/Project/queueTests/Info.plist b/Project/queueTests/Info.plist new file mode 100644 index 0000000..ba72822 --- /dev/null +++ b/Project/queueTests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/README.md b/README.md index 6c2d0f1..383b0ee 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,22 @@ While `NSOperation` and `NSOperationQueue` work well for some repetitive problem The easiest way to get going with EDQueue is to take a look at the included example application. The XCode project file can be found in `Project > queue.xcodeproj`. ### Setup -EDQueue needs both `libsqlite3.0.dylib` and [FMDB](https://github.com/ccgus/fmdb) for the storage engine. As always, the quickest way to take care of all those details is to use [CocoaPods](http://cocoapods.org/). EDQueue is implemented as a singleton as to allow jobs to be created from anywhere throughout an application. However, tasks are all processed through a single delegate method and thus it often makes the most sense to setup EDQueue within the application delegate: +EDQueue needs both `libsqlite3.0.dylib` and [FMDB](https://github.com/ccgus/fmdb) for the storage engine. As always, the quickest way to take care of all those details is to use [CocoaPods](http://cocoapods.org/). EDQueue is implemented as a singleton as to allow jobs to be created from anywhere throughout an application. However, tasks are all processed through a single delegate method and thus it often makes the most sense to setup EDQueue within the application delegate. See examples below. + +#### No-sigleton approach + +EDQueue is easy to use with your DI or other way around singletons. You also able to use non-FMDB storage for persistence. To do so have a look at `EDQueuePersistentStorageProtocol`. It's pretty straighforward. + +Here's exaple of configuring EDQueue with your storage: + +``` +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + EDQueueStorageEngine *fmdbBasedStorage = [[EDQueueStorageEngine alloc] initWithName:@"mydatabase.sqlite"]; + + self.persistentTaskQueue = [[EDQueue alloc] initWithPersistentStore:fmdbBasedStorage]; +} +``` YourAppDelegate.h ```objective-c @@ -21,50 +36,51 @@ YourAppDelegate.m ```objective-c - (void)applicationDidBecomeActive:(UIApplication *)application { - [[EDQueue sharedInstance] setDelegate:self]; - [[EDQueue sharedInstance] start]; + [[EDQueue defaultQueue] setDelegate:self]; + [[EDQueue defaultQueue] start]; } - (void)applicationWillResignActive:(UIApplication *)application { - [[EDQueue sharedInstance] stop]; + [[EDQueue defaultQueue] stop]; } -- (EDQueueResult)queue:(EDQueue *)queue processJob:(NSDictionary *)job +- (void)queue:(EDQueue *)queue processJob:(EDQueueJob *)job completion:(void (^)(EDQueueResult))block { - sleep(1); // This won't block the main thread. Yay! + sleep(1); - // Wrap your job processing in a try-catch. Always use protection! @try { - if ([[job objectForKey:@"task"] isEqualToString:@"success"]) { - return EDQueueResultSuccess; - } else if ([[job objectForKey:@"task"] isEqualToString:@"fail"]) { - return EDQueueResultFail; + if ([job.tag isEqualToString:@"success"]) { + block(EDQueueResultSuccess); + } else if ([job.tag isEqualToString:@"fail"]) { + block(EDQueueResultFail); + } else { + block(EDQueueResultCritical); } } @catch (NSException *exception) { - return EDQueueResultCritical; + block(EDQueueResultCritical); } - - return EDQueueResultCritical; } ``` SomewhereElse.m ```objective-c -[[EDQueue sharedInstance] enqueueWithData:@{ @"foo" : @"bar" } forTask:@"nyancat"]; +EDQueueJob *success = [[EDQueueJob alloc] initWithTag:@"success" userInfo:@{ @"nyan" : @"cat" }]; +[[EDQueue defaultQueue] enqueueJob:success]; ``` -In order to keep things simple, the delegate method expects a return type of `EDQueueResult` which permits three distinct states: +In order to keep things simple, the delegate method expects a call of a callback with one parameter type of `EDQueueResult` which permits three distinct states: - `EDQueueResultSuccess`: Used to indicate that a job has completed successfully - `EDQueueResultFail`: Used to indicate that a job has failed and should be retried (up to the specified `retryLimit`) - `EDQueueResultCritical`: Used to indicate that a job has failed critically and should not be attempted again ### Handling Async Jobs -As of v0.6.0 queue includes a delegate method suited for handling asyncronous jobs such as HTTP requests or [Disk I/O](https://github.com/thisandagain/storage): +As of v1.0 queue switched to a delegate method suited for handling asyncronous jobs such as HTTP requests or [Disk I/O](https://github.com/thisandagain/storage): + ```objective-c -- (void)queue:(EDQueue *)queue processJob:(NSDictionary *)job completion:(void (^)(EDQueueResult))block +- (void)queue:(EDQueue *)queue processJob:(EDQueueJob *)job completion:(void (^)(EDQueueResult))block { sleep(1); @@ -84,32 +100,31 @@ As of v0.6.0 queue includes a delegate method suited for handling asyncronous jo ``` ### Introspection -As of v0.7.0 queue includes a collection of methods to aid in queue introspection specific to each task: +As of v0.7.0 queue includes a collection of methods to aid in queue introspection specific to each task, using tags: ```objective-c -- (Boolean)jobExistsForTask:(NSString *)task; -- (Boolean)jobIsActiveForTask:(NSString *)task; -- (NSDictionary *)nextJobForTask:(NSString *)task; +- (Boolean)jobExistsForTag:(NSString *)tag; +- (Boolean)jobIsActiveForTag:(NSString *)tag; +- (EDQueueJob *)nextJobForTag:(NSString *)tag; ``` --- ### Methods ```objective-c -- (void)enqueueWithData:(id)data forTask:(NSString *)task; +- (void)enqueueJob:(EDQueueJob *)job; - (void)start; - (void)stop; - (void)empty; -- (Boolean)jobExistsForTask:(NSString *)task; -- (Boolean)jobIsActiveForTask:(NSString *)task; -- (NSDictionary *)nextJobForTask:(NSString *)task; +- (Boolean)jobExistsForTag:(NSString *)tag; +- (Boolean)jobIsActiveForTag:(NSString *)tag; +- (EDQueueJob *)nextJobForTag:(NSString *)tag; ``` ### Delegate Methods ```objective-c -- (EDQueueResult)queue:(EDQueue *)queue processJob:(NSDictionary *)job; -- (void)queue:(EDQueue *)queue processJob:(NSDictionary *)job completion:(void (^)(EDQueueResult result))block; +- (void)queue:(EDQueue *)queue processJob:(EDQueueJob *)job completion:(void (^)(EDQueueResult result))block; ``` ### Result Types @@ -139,7 +154,7 @@ EDQueueJobDidFail --- ### iOS Support -EDQueue is designed for iOS 5 and up. +EDQueue is designed for iOS 7 and up. ### ARC EDQueue is built using ARC. If you are including EDQueue in a project that **does not** use [Automatic Reference Counting (ARC)](http://developer.apple.com/library/ios/#releasenotes/ObjectiveC/RN-TransitioningToARC/Introduction/Introduction.html), you will need to set the `-fobjc-arc` compiler flag on all of the EDQueue source files. To do this in Xcode, go to your active target and select the "Build Phases" tab. Now select all EDQueue source files, press Enter, insert `-fobjc-arc` and then "Done" to enable ARC for EDQueue.