Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement BFTask Cancellation #71

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 *)cancellationToken
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;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing I was curious about and would love some help understanding for my own education is why BFTask creates a separate NSObject to use in its @synchronized blocks (the pattern I cargo-culted here). Is there a particular reason for doing this over just using self (as in @synchronized(self)) which seems like it would be less risky since its less likely to errantly be modified or released?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Synchronizing around a private lock object is generally preferable to @synchronized(self) blocks unless you specifically want to share the lock with other objects which is what happens with self, since other objects can call @synchronized(you).


@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