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 Mar 9, 2015
1 parent b72d5f2 commit 0eb79e6
Show file tree
Hide file tree
Showing 8 changed files with 264 additions and 21 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 @@ -231,6 +237,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 @@ -368,6 +376,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 @@ -377,6 +386,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 @@ -573,6 +583,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 @@ -596,6 +607,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
68 changes: 68 additions & 0 deletions Bolts/Common/BFTask.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ extern NSString *const BFTaskMultipleExceptionsException;

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

/*!
A block that can act as a continuation for a task.
Expand Down Expand Up @@ -178,6 +179,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 @@ -359,6 +359,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
37 changes: 16 additions & 21 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -476,29 +476,24 @@ For common cases, such as dispatching on the main thread, we have provided defau

## Task Cancellation

It's generally bad design to keep track of the `BFTaskCompletionSource` for cancellation. A better model is to create a "cancellation token" at the top level, and pass that to each async function that you want to be part of the same "cancelable operation". Then, in your continuation blocks, you can check whether the cancellation token has been cancelled and bail out early by returning a `[BFTask cancelledTask]`. For example:
If you need to be able to cancel a `BFTask` the `continueWithCancellationToken:withBlock` and `continueWithCancellationToken:withSuccessBlock` method allow you to pass in a `BFTaskCancellationToken` object which is checked before executing the continuation block.

```objective-c
- (void)doSomethingComplicatedAsync:(MYCancellationToken *)cancellationToken {
[[self doSomethingAsync:cancellationToken] continueWithBlock:^{
if (cancellationToken.isCancelled) {
return [BFTask cancelledTask];
}
// Do something that takes a while.
return result;
}];
}

// Somewhere else.
MYCancellationToken *cancellationToken = [[MYCancellationToken alloc] init];
[obj doSomethingComplicatedAsync:cancellationToken];

// When you get bored...
[cancellationToken cancel];
```
````objective-c
PFQuery *query = [PFQuery queryWithClassName:@"Images"];
BFTaskCancellationToken *token = [[BFTaskCancellationToken alloc] init];

**Note:** The cancellation token implementation should be thread-safe.
We are likely to add some concept like this to Bolts at some point in the future.
[[[self findAsync:query] continueWithCancellationToken:token withBlock:^id(BFTask *task) {
// This won't be executed if -cancel is called on token before the query has returned
return [self processImages:task.results];
}] continueWithBlock:^id(BFTask *task) {
if (task.isCancelled) {
// Handle cancellation if needed
} else {
// Update UI etc...
}
return nil;
}];
````
# App Links
Expand Down

0 comments on commit 0eb79e6

Please sign in to comment.