Skip to content

Commit

Permalink
Implement BFTask Cancellation
Browse files Browse the repository at this point in the history
Cancellation is provided by a token passed into the continuation methods, which check for the
cancelled state before executing their blocks and return a cancelled BFTask if necessary.
  • Loading branch information
danielrhammond committed Dec 5, 2014
1 parent c1cf44c commit fd6e142
Show file tree
Hide file tree
Showing 9 changed files with 267 additions and 24 deletions.
12 changes: 12 additions & 0 deletions Bolts.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
1EC3017118CDAA8400D06D07 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 1EC3017018CDAA8400D06D07 /* AppDelegate.m */; };
1EC3017318CDAA8400D06D07 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1EC3017218CDAA8400D06D07 /* Images.xcassets */; };
1EC3019118CDABCE00D06D07 /* AppLinkTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1EC3019018CDABCE00D06D07 /* AppLinkTests.m */; };
3D7366631A323398002E16AD /* BFTaskCancellationToken.h in Headers */ = {isa = PBXBuildFile; fileRef = 3D7366611A323398002E16AD /* BFTaskCancellationToken.h */; settings = {ATTRIBUTES = (Public, ); }; };
3D7366641A323398002E16AD /* BFTaskCancellationToken.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D7366621A323398002E16AD /* BFTaskCancellationToken.m */; };
3D7366651A32375F002E16AD /* BFTaskCancellationToken.h in Headers */ = {isa = PBXBuildFile; fileRef = 3D7366611A323398002E16AD /* BFTaskCancellationToken.h */; settings = {ATTRIBUTES = (Public, ); }; };
3D7366661A325686002E16AD /* BFTaskCancellationToken.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D7366621A323398002E16AD /* BFTaskCancellationToken.m */; };
8103FA6819900A84000BAE3F /* BFExecutor.m in Sources */ = {isa = PBXBuildFile; fileRef = 8103FA4F19900A84000BAE3F /* BFExecutor.m */; };
8103FA6919900A84000BAE3F /* BFExecutor.m in Sources */ = {isa = PBXBuildFile; fileRef = 8103FA4F19900A84000BAE3F /* BFExecutor.m */; };
8103FA6A19900A84000BAE3F /* BFTask.m in Sources */ = {isa = PBXBuildFile; fileRef = 8103FA5119900A84000BAE3F /* BFTask.m */; };
Expand Down Expand Up @@ -101,6 +105,8 @@
1EC3017018CDAA8400D06D07 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
1EC3017218CDAA8400D06D07 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
1EC3019018CDABCE00D06D07 /* AppLinkTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppLinkTests.m; sourceTree = "<group>"; };
3D7366611A323398002E16AD /* BFTaskCancellationToken.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BFTaskCancellationToken.h; sourceTree = "<group>"; };
3D7366621A323398002E16AD /* BFTaskCancellationToken.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BFTaskCancellationToken.m; sourceTree = "<group>"; };
8103FA4E19900A84000BAE3F /* BFExecutor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BFExecutor.h; sourceTree = "<group>"; };
8103FA4F19900A84000BAE3F /* BFExecutor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BFExecutor.m; sourceTree = "<group>"; };
8103FA5019900A84000BAE3F /* BFTask.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BFTask.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -230,6 +236,8 @@
8103FA5519900A84000BAE3F /* Bolts.m */,
8103FA5019900A84000BAE3F /* BFTask.h */,
8103FA5119900A84000BAE3F /* BFTask.m */,
3D7366611A323398002E16AD /* BFTaskCancellationToken.h */,
3D7366621A323398002E16AD /* BFTaskCancellationToken.m */,
8103FA5219900A84000BAE3F /* BFTaskCompletionSource.h */,
8103FA5319900A84000BAE3F /* BFTaskCompletionSource.m */,
8103FA4E19900A84000BAE3F /* BFExecutor.h */,
Expand Down Expand Up @@ -366,6 +374,7 @@
81D0EE9219AFAA6F0000AE75 /* BFMeasurementEvent.h in Headers */,
81D0EE9319AFAA6F0000AE75 /* BFURL.h in Headers */,
81D0EE8419AFAA100000AE75 /* Bolts.h in Headers */,
3D7366631A323398002E16AD /* BFTaskCancellationToken.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -375,6 +384,7 @@
files = (
81D0EE8519AFAA190000AE75 /* BFTask.h in Headers */,
81D0EE8819AFAA240000AE75 /* BFExecutor.h in Headers */,
3D7366651A32375F002E16AD /* BFTaskCancellationToken.h in Headers */,
81D0EE8A19AFAA2C0000AE75 /* BFTaskCompletionSource.h in Headers */,
81D0EE8219AFAA060000AE75 /* BoltsVersion.h in Headers */,
81D0EE8319AFAA0E0000AE75 /* Bolts.h in Headers */,
Expand Down Expand Up @@ -571,6 +581,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
3D7366641A323398002E16AD /* BFTaskCancellationToken.m in Sources */,
8103FA7819900A84000BAE3F /* BFAppLinkTarget.m in Sources */,
8103FA6C19900A84000BAE3F /* BFTaskCompletionSource.m in Sources */,
8103FA7619900A84000BAE3F /* BFAppLinkReturnToRefererView.m in Sources */,
Expand All @@ -594,6 +605,7 @@
8103FA6B19900A84000BAE3F /* BFTask.m in Sources */,
8103FA6F19900A84000BAE3F /* Bolts.m in Sources */,
8103FA6919900A84000BAE3F /* BFExecutor.m in Sources */,
3D7366661A325686002E16AD /* BFTaskCancellationToken.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,17 @@
<key>IDESourceControlProjectOriginsDictionary</key>
<dict>
<key>61C4B9E3B61282127C102C87B46B8CDE985974AE</key>
<string>ssh://github.com/BoltsFramework/Bolts-iOS.git</string>
<string>github.com:BoltsFramework/Bolts-iOS.git</string>
</dict>
<key>IDESourceControlProjectPath</key>
<string>Bolts.xcodeproj/project.xcworkspace</string>
<string>Bolts.xcodeproj</string>
<key>IDESourceControlProjectRelativeInstallPathDictionary</key>
<dict>
<key>61C4B9E3B61282127C102C87B46B8CDE985974AE</key>
<string>../..</string>
</dict>
<key>IDESourceControlProjectURL</key>
<string>ssh://github.com/BoltsFramework/Bolts-iOS.git</string>
<string>github.com:BoltsFramework/Bolts-iOS.git</string>
<key>IDESourceControlProjectVersion</key>
<integer>111</integer>
<key>IDESourceControlProjectWCCIdentifier</key>
Expand Down
68 changes: 68 additions & 0 deletions Bolts/Common/BFTask.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

@class BFExecutor;
@class BFTask;
@class BFTaskCancellationToken;

/*!
A block that can act as a continuation for a task.
Expand Down Expand Up @@ -155,6 +156,73 @@ typedef id(^BFContinuationBlock)(BFTask *task);
- (instancetype)continueWithExecutor:(BFExecutor *)executor
withSuccessBlock:(BFContinuationBlock)block;

/*!
Enques the given block to be run once the task is complete
@param executor A BFExecutor responsible for determining how the
continuation block will be run.
@param cancellationToken A BFTaskCancellationToken which allows
the task to be cancelled before the continuation block is executed
@param block The block to be run once this task is complete.
@returns A task that will be completed after block has run.
If block returns a BFTask, then the task returned from
this method will not be completed until that task is completed.
If the cancellationToken is cancelled before the block is executed
then the BFTask returned will have isCancelled true
*/
- (instancetype)continueWithExecutor:(BFExecutor *)executor
withCancellationToken:(BFTaskCancellationToken *)cancellationToken
withBlock:(BFContinuationBlock)block;
/*!
Identical to continueWithExecutor:withCancellationToken:withBlock:,
except that it uses the default execution strategy
@param cancellationToken A BFTaskCancellationToken which allows
the task to be cancelled before the continuation block is executed
@param block The block to be run once this task is complete.
@returns A task that will be completed after block has run.
If block returns a BFTask, then the task returned from
this method will not be completed until that task is completed.
If the cancellationToken is cancelled before the block is executed
then the BFTask returned will have isCancelled true
*/
- (instancetype)continueWithCancellationToken:(BFTaskCancellationToken *)cancellationToken
withBlock:(BFContinuationBlock)block;

/*!
Identical to continueWithExecutor:withCancellationToken:withBlock:,
except that the cancellation token is checked and the block executed
only if this task did not produce a cancellation, error or exception.
@param executor A BFExecutor responsible for determining how the
continuation block will be run.
@param cancellationToken A BFTaskCancellationToken which allows
the task to be cancelled before the continuation block is executed
@param block The block to be run once this task is complete.
@returns A task that will be completed after block has run.
If block returns a BFTask, then the task returned from
this method will not be completed until that task is completed.
If the cancellationToken is cancelled before the block is executed
and this task has not produced a cancellation, error or exception then
the BFTask returned will have isCancelled true.
*/
- (instancetype)continueWithExecutor:(BFExecutor *)executor
withCancellationToken:(BFTaskCancellationToken *)token
withSuccessBlock:(BFContinuationBlock)block;

/*!
Identical to continueWithExecutor:withCancellationToken:withSuccessBlock:,
except that it uses the default execution strategy
@param cancellationToken A BFTaskCancellationToken which allows
the task to be cancelled before the continuation block is executed
@param block The block to be run once this task is complete.
@returns A task that will be completed after block has run.
If block returns a BFTask, then the task returned from
this method will not be completed until that task is completed.
If the cancellationToken is cancelled before the block is executed
and this task has not produced a cancellation, error or exception then
the BFTask returned will have isCancelled true.
*/
- (instancetype)continueWithCancellationToken:(BFTaskCancellationToken *)cancellationToken
withSuccessBlock:(BFContinuationBlock)block;

/*!
Waits until this operation is completed.
This method is inefficient and consumes a thread resource while
Expand Down
38 changes: 38 additions & 0 deletions Bolts/Common/BFTask.m
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,44 @@ - (instancetype)continueWithSuccessBlock:(BFContinuationBlock)block {
return [self continueWithExecutor:[BFExecutor defaultExecutor] withSuccessBlock:block];
}

- (instancetype)continueWithExecutor:(BFExecutor *)executor
withCancellationToken:(BFTaskCancellationToken *)token
withBlock:(BFContinuationBlock)block {
return [self continueWithExecutor:executor withBlock:^id(BFTask *task) {
if (token.isCancelled) {
return [BFTask cancelledTask];
} else {
return block(task);
}
}];
}

- (instancetype)continueWithCancellationToken:(BFTaskCancellationToken *)token
withBlock:(BFContinuationBlock)block {
return [self continueWithExecutor:[BFExecutor defaultExecutor]
withCancellationToken:token
withBlock:block];
}

- (instancetype)continueWithExecutor:(BFExecutor *)executor
withCancellationToken:(BFTaskCancellationToken *)token
withSuccessBlock:(BFContinuationBlock)block {
return [self continueWithExecutor:executor withSuccessBlock:^id(BFTask *task) {
if (token.isCancelled) {
return [BFTask cancelledTask];
} else {
return block(task);
}
}];
}

- (instancetype)continueWithCancellationToken:(BFTaskCancellationToken *)token
withSuccessBlock:(BFContinuationBlock)block {
return [self continueWithExecutor:[BFExecutor defaultExecutor]
withCancellationToken:token
withSuccessBlock:block];
}

#pragma mark - Syncing Task (Avoid it)

- (void)warnOperationOnMainThread {
Expand Down
16 changes: 16 additions & 0 deletions Bolts/Common/BFTaskCancellationToken.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// BFTaskCancellationToken.h
// Bolts
//
// Created by Daniel Hammond on 12/5/14.
// Copyright (c) 2014 Parse Inc. All rights reserved.
//

#import <Foundation/Foundation.h>

@interface BFTaskCancellationToken : NSObject

@property (nonatomic, assign, readonly, getter=isCancelled) BOOL cancelled;
- (void)cancel;

@end
39 changes: 39 additions & 0 deletions Bolts/Common/BFTaskCancellationToken.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// BFTaskCancellationToken.m
// Bolts
//
// Created by Daniel Hammond on 12/5/14.
// Copyright (c) 2014 Parse Inc. All rights reserved.
//

#import "BFTaskCancellationToken.h"

@interface BFTaskCancellationToken ()

@property (atomic, assign, readwrite, getter=isCancelled) BOOL cancelled;
@property (nonatomic, retain) NSObject *lock;

@end

@implementation BFTaskCancellationToken

- (instancetype)init
{
self = [super init];
_lock = [[NSObject alloc] init];
return self;
}

- (BOOL)isCancelled {
@synchronized (self.lock) {
return _cancelled;
}
}

- (void)cancel {
@synchronized (self.lock) {
self.cancelled = YES;
}
}

@end
1 change: 1 addition & 0 deletions Bolts/Common/Bolts.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#import <Bolts/BFExecutor.h>
#import <Bolts/BFTask.h>
#import <Bolts/BFTaskCompletionSource.h>
#import <Bolts/BFTaskCancellationToken.h>

#if TARGET_OS_IPHONE
#import <Bolts/BFAppLinkNavigation.h>
Expand Down
74 changes: 74 additions & 0 deletions BoltsTests/TaskTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,80 @@ - (void)testCancellation {
XCTAssertTrue(task.isCancelled);
}

- (void)testCancellationToken {
BFTaskCancellationToken *token = [[BFTaskCancellationToken alloc] init];
[token cancel];
BFTask *task = [BFTask taskWithDelay:100];
task = [task continueWithExecutor:[BFExecutor defaultExecutor]
withCancellationToken:token
withBlock:^id(BFTask *task) {
XCTFail(@"This callback should be skipped");
return nil;
}];
[task waitUntilFinished];
XCTAssertTrue(task.cancelled);
}

- (void)testCancellationTokenNotCancelled {
BFTaskCancellationToken *token = [[BFTaskCancellationToken alloc] init];
BFTask *task = [BFTask taskWithResult:@"foo"];
__block BOOL blockExecuted = NO;
[[task continueWithExecutor:[BFExecutor defaultExecutor]
withCancellationToken:token
withBlock:^id(BFTask *task) {
XCTAssertEqualObjects(@"foo", task.result);
blockExecuted = YES;
return task;
}] waitUntilFinished];
XCTAssertTrue(blockExecuted);
}

- (void)testCancellationTokenSuccessBlock {
BFTaskCancellationToken *token = [[BFTaskCancellationToken alloc] init];
[token cancel];
BFTask *task = [BFTask taskWithDelay:100];
task = [task continueWithExecutor:[BFExecutor defaultExecutor]
withCancellationToken:token
withSuccessBlock:^id(BFTask *task) {
XCTFail(@"This callback should be skipped");
return nil;
}];
[task waitUntilFinished];
XCTAssertTrue(task.cancelled);
}

- (void)testCancellationTokenSuccessBlockNotCancelled {
BFTaskCancellationToken *token = [[BFTaskCancellationToken alloc] init];
BFTask *task = [BFTask taskWithResult:@"foo"];
__block BOOL blockExecuted = NO;
[[task continueWithExecutor:[BFExecutor defaultExecutor]
withCancellationToken:token
withBlock:^id(BFTask *task) {
XCTAssertEqualObjects(@"foo", task.result);
blockExecuted = YES;
return task;
}] waitUntilFinished];
XCTAssertTrue(blockExecuted);
}

- (void)testCancellationTokenSuccessBlockError {
BFTaskCancellationToken *token = [[BFTaskCancellationToken alloc] init];
[token cancel];
NSError *error = [NSError errorWithDomain:@"BoltsTests" code:35 userInfo:nil];
BFTask *task = [BFTask taskWithError:error];
task = [[task continueWithExecutor:[BFExecutor defaultExecutor]
withCancellationToken:token
withSuccessBlock:^id(BFTask *task) {
XCTFail(@"This callback should be skipped");
return nil;
}] continueWithBlock:^id(BFTask *task) {
XCTAssertFalse(task.isCancelled);
XCTAssertEqualObjects(error, task.error);
return nil;
}];
[task waitUntilFinished];
}

- (void)testTaskForCompletionOfAllTasksSuccess {
NSMutableArray *tasks = [NSMutableArray array];

Expand Down
Loading

0 comments on commit fd6e142

Please sign in to comment.