From 0eb79e6d16967c1509346eeb50768e346e1605a7 Mon Sep 17 00:00:00 2001 From: Daniel Hammond Date: Fri, 5 Dec 2014 13:00:47 -0800 Subject: [PATCH 1/2] Implement BFTask Cancellation 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. --- Bolts.xcodeproj/project.pbxproj | 12 +++++ Bolts/Common/BFTask.h | 68 +++++++++++++++++++++++ Bolts/Common/BFTask.m | 38 +++++++++++++ Bolts/Common/BFTaskCancellationToken.h | 16 ++++++ Bolts/Common/BFTaskCancellationToken.m | 39 ++++++++++++++ Bolts/Common/Bolts.h | 1 + BoltsTests/TaskTests.m | 74 ++++++++++++++++++++++++++ Readme.md | 37 ++++++------- 8 files changed, 264 insertions(+), 21 deletions(-) create mode 100644 Bolts/Common/BFTaskCancellationToken.h create mode 100644 Bolts/Common/BFTaskCancellationToken.m diff --git a/Bolts.xcodeproj/project.pbxproj b/Bolts.xcodeproj/project.pbxproj index 21bd5d3d7..c962a1acb 100644 --- a/Bolts.xcodeproj/project.pbxproj +++ b/Bolts.xcodeproj/project.pbxproj @@ -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 */; }; @@ -101,6 +105,8 @@ 1EC3017018CDAA8400D06D07 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 1EC3017218CDAA8400D06D07 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 1EC3019018CDABCE00D06D07 /* AppLinkTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppLinkTests.m; sourceTree = ""; }; + 3D7366611A323398002E16AD /* BFTaskCancellationToken.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BFTaskCancellationToken.h; sourceTree = ""; }; + 3D7366621A323398002E16AD /* BFTaskCancellationToken.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BFTaskCancellationToken.m; sourceTree = ""; }; 8103FA4E19900A84000BAE3F /* BFExecutor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BFExecutor.h; sourceTree = ""; }; 8103FA4F19900A84000BAE3F /* BFExecutor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BFExecutor.m; sourceTree = ""; }; 8103FA5019900A84000BAE3F /* BFTask.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BFTask.h; sourceTree = ""; }; @@ -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 */, @@ -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; }; @@ -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 */, @@ -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 */, @@ -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; }; diff --git a/Bolts/Common/BFTask.h b/Bolts/Common/BFTask.h index ce8db4f88..7ae58b49f 100644 --- a/Bolts/Common/BFTask.h +++ b/Bolts/Common/BFTask.h @@ -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. @@ -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 diff --git a/Bolts/Common/BFTask.m b/Bolts/Common/BFTask.m index 681cba77d..e7d137255 100644 --- a/Bolts/Common/BFTask.m +++ b/Bolts/Common/BFTask.m @@ -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 { diff --git a/Bolts/Common/BFTaskCancellationToken.h b/Bolts/Common/BFTaskCancellationToken.h new file mode 100644 index 000000000..d2e1d2a4d --- /dev/null +++ b/Bolts/Common/BFTaskCancellationToken.h @@ -0,0 +1,16 @@ +// +// BFTaskCancellationToken.h +// Bolts +// +// Created by Daniel Hammond on 12/5/14. +// Copyright (c) 2014 Parse Inc. All rights reserved. +// + +#import + +@interface BFTaskCancellationToken : NSObject + +@property (nonatomic, assign, readonly, getter=isCancelled) BOOL cancelled; +- (void)cancel; + +@end diff --git a/Bolts/Common/BFTaskCancellationToken.m b/Bolts/Common/BFTaskCancellationToken.m new file mode 100644 index 000000000..208f0084b --- /dev/null +++ b/Bolts/Common/BFTaskCancellationToken.m @@ -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 diff --git a/Bolts/Common/Bolts.h b/Bolts/Common/Bolts.h index e22bdd541..effd8ac23 100644 --- a/Bolts/Common/Bolts.h +++ b/Bolts/Common/Bolts.h @@ -12,6 +12,7 @@ #import #import #import +#import #if TARGET_OS_IPHONE #import diff --git a/BoltsTests/TaskTests.m b/BoltsTests/TaskTests.m index bf0ad5726..6abb6160c 100644 --- a/BoltsTests/TaskTests.m +++ b/BoltsTests/TaskTests.m @@ -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]; diff --git a/Readme.md b/Readme.md index 4b825f727..abae55530 100644 --- a/Readme.md +++ b/Readme.md @@ -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 From ae811af9141c822d4a1bc40a2ca065224fcbc8f0 Mon Sep 17 00:00:00 2001 From: Daniel Hammond Date: Mon, 9 Mar 2015 16:30:14 -0700 Subject: [PATCH 2/2] Fix documentation warning --- Bolts/Common/BFTask.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Bolts/Common/BFTask.h b/Bolts/Common/BFTask.h index 7ae58b49f..78dd61595 100644 --- a/Bolts/Common/BFTask.h +++ b/Bolts/Common/BFTask.h @@ -227,7 +227,7 @@ typedef id(^BFContinuationBlock)(BFTask *task); the BFTask returned will have isCancelled true. */ - (instancetype)continueWithExecutor:(BFExecutor *)executor - withCancellationToken:(BFTaskCancellationToken *)token + withCancellationToken:(BFTaskCancellationToken *)cancellationToken withSuccessBlock:(BFContinuationBlock)block; /*!