Skip to content

Commit

Permalink
Add PromiseContext.isExecutingNow
Browse files Browse the repository at this point in the history
Fixes #53.
  • Loading branch information
lilyball committed May 23, 2020
1 parent e857163 commit 49ccbdb
Show file tree
Hide file tree
Showing 9 changed files with 267 additions and 15 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,15 @@ Unless you explicitly state otherwise, any contribution intentionally submitted

## Version History

### Development

- Add `PromiseContext.isExecutingNow` (`TWLPromiseContext.isExecutingNow` in Obj-C) that returns `true` if accessed from within a callback registered
with `.nowOr(_:)` and executing synchronously, or `false` otherwise. If accessed from within a callback (or `Promise.init(on:_:)`) registered with
`.immediate` and running synchronously, it inherits the surrounding scope's `PromiseContext.isExecutingNow` flag. This is intended to allow
`Promise(on: .immediate, { … })` to query the surrounding scope's flag ([#53][]).

[#53]: https://github.com/lilyball/Tomorrowland/issues/53 "Can we add something like PromiseContext.isExecutingNow?"

### v1.2.0

- Add `PromiseContext.nowOr(context)` (`+[TWLContext nowOrContext:]` in Obj-C) that runs the callback synchronously when registered if the promise
Expand Down
18 changes: 18 additions & 0 deletions Sources/ObjC/TWLContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,24 @@ NS_ASSUME_NONNULL_BEGIN
/// Returns \c .main when accessed from the main thread, otherwise <tt>.defaultQoS</tt>.
@property (class, readonly) TWLContext *automatic;

/// Returns whether a \c +nowOrContext: context is executing synchronously.
///
/// When accessed from within a callback registered with \c +nowOrContext: this returns \c YES if
/// the callback is executing synchronously or \c NO if it's executing on the wrapped context. When
/// accessed from within a callback (including <tt>[TWLPromise newOnContext:withBlock:]</tt>
/// registered with \c .immediate this returns \c YES if and only if the callback is executing
/// synchronously and is nested within a \c +nowOrContext: context that is executing synchronously.
/// When accessed from any other scenario this always returns \c NO.
///
/// \note The behavior of \c .immediate is intended to allow <tt>[TWLPromise
/// newOnContext:withBlock:]</tt> registered with \c .immediate to query the synchronous state of
/// its surrounding scope.
///
/// \note This flag will return \c NO when executed from within a dispatch sync to the main queue
/// nested inside a \c +nowOrContext: callback, or any similar construct that blocks the current
/// thread and runs code on another thread.
@property (class, readonly) BOOL isExecutingNow;

/// Returns the \c TWLContext that corresponds to a given Dispatch QoS class.
///
/// If the given QoS is \c QOS_CLASS_UNSPECIFIED then \c QOS_CLASS_DEFAULT is assumed.
Expand Down
13 changes: 11 additions & 2 deletions Sources/ObjC/TWLContext.m
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ + (TWLContext *)automatic {
}
}

+ (BOOL)isExecutingNow {
return TWLGetSynchronousContextThreadLocalFlag();
}

+ (TWLContext *)queue:(dispatch_queue_t)queue {
return [[self alloc] initWithQueue:queue];
}
Expand Down Expand Up @@ -169,7 +173,7 @@ - (BOOL)isImmediate {

- (void)executeIsSynchronous:(BOOL)isSynchronous block:(dispatch_block_t)block {
if (isSynchronous && _canRunNow) {
block();
TWLExecuteBlockWithSynchronousContextThreadLocalFlag(YES, block);
} else if (_queue) {
if (_isMain) {
if (TWLGetMainContextThreadLocalFlag()) {
Expand Down Expand Up @@ -204,7 +208,12 @@ - (void)executeIsSynchronous:(BOOL)isSynchronous block:(dispatch_block_t)block {
[_operationQueue addOperationWithBlock:block];
} else {
// immediate
block();
if (isSynchronous) {
// Inherit the synchronous context flag from our current scope
block();
} else {
TWLExecuteBlockWithSynchronousContextThreadLocalFlag(NO, block);
}
}
}

Expand Down
10 changes: 10 additions & 0 deletions Sources/Private/TWLThreadLocal.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,13 @@ void TWLEnqueueThreadLocalBlock(dispatch_block_t _Nonnull block);
/// Blocks are dequeued in FIFO order.
dispatch_block_t _Nullable TWLDequeueThreadLocalBlock(void);

#pragma mark -

/// Gets the synchronous context thread local flag.
BOOL TWLGetSynchronousContextThreadLocalFlag(void);

/// Executes a block with hte synchronous context thread local flag set to the given value, and
/// restores the previous value afterwards.
///
/// This guarantees the previous value will be restored even if an exception occurs.
BOOL TWLExecuteBlockWithSynchronousContextThreadLocalFlag(BOOL value, NS_NOESCAPE dispatch_block_t _Nonnull block);
51 changes: 43 additions & 8 deletions Sources/Private/TWLThreadLocal.m
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,35 @@
#import "TWLThreadLocal.h"

#if __has_feature(c_thread_local)
_Thread_local BOOL flag = NO;
_Thread_local BOOL mainContextFlag = NO;
_Thread_local BOOL synchronousContextFlag = NO;
#else
#include <pthread.h>
static pthread_key_t flagKey;
__attribute__((constructor)) static void constructFlagKey() {
int err = pthread_key_create(&flagKey, NULL);
static pthread_key_t mainContextFlagKey;
static pthread_key_t synchronousContextFlagKey;
__attribute__((constructor)) static void constructFlagKeys() {
int err = pthread_key_create(&mainContextFlagKey, NULL);
assert(err == 0);
err = pthread_key_create(&synchronousContextFlagKey, NULL);
assert(err == 0);
}
#endif

#pragma mark -

BOOL TWLGetMainContextThreadLocalFlag(void) {
#if __has_feature(c_thread_local)
return flag;
return mainContextFlag;
#else
return pthread_getspecific(flagKey) != NULL;
return pthread_getspecific(mainContextFlagKey) != NULL;
#endif
}

void TWLSetMainContextThreadLocalFlag(BOOL value) {
#if __has_feature(c_thread_local)
flag = value;
mainContextFlag = value;
#else
int err = pthread_setspecific(flagKey, value ? kCFBooleanTrue : NULL);
int err = pthread_setspecific(mainContextFlagKey, value ? kCFBooleanTrue : NULL);
assert(err == 0);
#endif
}
Expand Down Expand Up @@ -111,3 +117,32 @@ dispatch_block_t _Nullable TWLDequeueThreadLocalBlock(void) {
return nil;
}
}

#pragma mark -

BOOL TWLGetSynchronousContextThreadLocalFlag(void) {
#if __has_feature(c_thread_local)
return synchronousContextFlag;
#else
return pthread_getspecific(synchronousContextFlagKey) != NULL;
#endif
}

static void TWLSetSynchronousContextThreadLocalFlag(BOOL value) {
#if __has_feature(c_thread_local)
synchronousContextFlag = value;
#else
int err = pthread_setspecific(synchronousContextFlagKey, value ? kCFBooleanTrue : NULL);
assert(err == 0);
#endif
}

BOOL TWLExecuteBlockWithSynchronousContextThreadLocalFlag(BOOL value, NS_NOESCAPE dispatch_block_t _Nonnull block) {
BOOL previousValue = TWLGetSynchronousContextThreadLocalFlag();
TWLSetSynchronousContextThreadLocalFlag(value);
@try {
block();
} @finally {
TWLSetSynchronousContextThreadLocalFlag(previousValue);
}
}
28 changes: 26 additions & 2 deletions Sources/Promise.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,25 @@ public enum PromiseContext: Equatable, Hashable {
}
}

/// Returns whether a `.nowOr(_:)` context is executing synchronously.
///
/// When accessed from within a callback registered with `.nowOr(_:)` this returns `true` if the
/// callback is executing synchronously or `false` if it's executing on the wrapped context.
/// When accessed from within a callback (including `Promise.init(on:_:)` registered with
/// `.immediate` this returns `true` if and only if the callback is executing synchronously and
/// is nested within a `.nowOr(_:)` context that is executing synchronously. When accessed from
/// any other scenario this always returns `false`.
///
/// - Remark: The behavior of `.immediate` is intended to allow `Promise(on: .immediate, { … })`
/// to query the synchronous state of its surrounding scope.
///
/// - Note: This flag will return `false` when executed from within `DispatchQueue.main.sync`
/// nested inside a `.nowOr` callback, or any similar construct that blocks the current thread
/// and runs code on another thread.
public static var isExecutingNow: Bool {
return TWLGetSynchronousContextThreadLocalFlag()
}

/// Returns the `PromiseContext` that corresponds to a given Dispatch QoS class.
///
/// If the given QoS is `.unspecified` then `.default` is assumed.
Expand Down Expand Up @@ -152,10 +171,15 @@ public enum PromiseContext: Equatable, Hashable {
case .operationQueue(let queue):
queue.addOperation(f)
case .immediate:
f()
case .nowOr(let context):
if isSynchronous {
// Inherit the synchronous context flag from our current scope
f()
} else {
TWLExecuteBlockWithSynchronousContextThreadLocalFlag(false, f)
}
case .nowOr(let context):
if isSynchronous {
TWLExecuteBlockWithSynchronousContextThreadLocalFlag(true, f)
} else {
context.execute(isSynchronous: false, f)
}
Expand Down
57 changes: 57 additions & 0 deletions Tests/ObjC/TWLPromiseTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -1476,10 +1476,15 @@ @implementation TWLPromiseNowOrContextTests
- (void)testPromiseInitUsingNowOrContext {
// +[TWLPromise new…] will treat it as now
__auto_type expectation = [XCTestExpectation new];
XCTAssertFalse(TWLContext.isExecutingNow);
dispatch_async(TestQueue.one, ^{
XCTAssertFalse(TWLContext.isExecutingNow);
[[TWLPromise<NSNumber*,NSString*> newOnContext:[TWLContext nowOrContext:[TWLContext queue:TestQueue.two]] withBlock:^(TWLResolver<NSNumber *,NSString *> * _Nonnull resolver) {
AssertOnTestQueue(1);
XCTAssertTrue(TWLContext.isExecutingNow);
[resolver fulfillWithValue:@42];
}] inspectOnContext:TWLContext.immediate handler:^(NSNumber * _Nullable value, NSString * _Nullable error) {
XCTAssertFalse(TWLContext.isExecutingNow);
[expectation fulfill];
}];
});
Expand All @@ -1491,7 +1496,9 @@ - (void)testWhenCancelRequestedUsingNowOrContext {
{
__auto_type sema = dispatch_semaphore_create(0);
__auto_type promise = [TWLPromise<NSNumber*,NSString*> newOnContext:[TWLContext queue:TestQueue.one] withBlock:^(TWLResolver<NSNumber *,NSString *> * _Nonnull resolver) {
XCTAssertFalse(TWLContext.isExecutingNow);
[resolver whenCancelRequestedOnContext:[TWLContext nowOrContext:[TWLContext queue:TestQueue.two]] handler:^(TWLResolver<NSNumber *,NSString *> * _Nonnull innerResolver) {
XCTAssertFalse(TWLContext.isExecutingNow);
AssertOnTestQueue(2);
[resolver cancel]; // capture outer resolver here
}];
Expand All @@ -1508,7 +1515,9 @@ - (void)testWhenCancelRequestedUsingNowOrContext {
__auto_type expectation = [XCTestExpectation new];
__auto_type promise = [TWLPromise<NSNumber*,NSString*> newOnContext:[TWLContext queue:TestQueue.one] withBlock:^(TWLResolver<NSNumber *,NSString *> * _Nonnull resolver) {
[resolver cancel];
XCTAssertFalse(TWLContext.isExecutingNow);
[resolver whenCancelRequestedOnContext:[TWLContext nowOrContext:[TWLContext queue:TestQueue.two]] handler:^(TWLResolver<NSNumber *,NSString *> * _Nonnull innerResolver) {
XCTAssertTrue(TWLContext.isExecutingNow);
AssertOnTestQueue(1);
[resolver cancel]; // capture outer resolver here
[expectation fulfill];
Expand All @@ -1524,9 +1533,11 @@ - (void)testThenNowOrContext {
{
__auto_type sema = dispatch_semaphore_create(0);
__auto_type promise = [[TWLPromise<NSNumber*,NSString*> newOnContext:[TWLContext queue:TestQueue.one] withBlock:^(TWLResolver<NSNumber *,NSString *> * _Nonnull resolver) {
XCTAssertFalse(TWLContext.isExecutingNow);
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
[resolver fulfillWithValue:@42];
}] thenOnContext:[TWLContext nowOrContext:[TWLContext queue:TestQueue.two]] handler:^(NSNumber * _Nonnull value) {
XCTAssertFalse(TWLContext.isExecutingNow);
AssertOnTestQueue(2);
}];
__auto_type expectation = TWLExpectationSuccess(promise);
Expand All @@ -1538,7 +1549,9 @@ - (void)testThenNowOrContext {
{
__block TWLPromise<NSNumber*,NSString*> *promise;
dispatch_sync(TestQueue.one, ^{
XCTAssertFalse(TWLContext.isExecutingNow);
promise = [[TWLPromise<NSNumber*,NSString*> newFulfilledWithValue:@42] thenOnContext:[TWLContext nowOrContext:[TWLContext queue:TestQueue.two]] handler:^(NSNumber * _Nonnull value) {
XCTAssertTrue(TWLContext.isExecutingNow);
AssertOnTestQueue(1);
}];
});
Expand All @@ -1555,6 +1568,7 @@ - (void)testMapNowOrContext {
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
[resolver fulfillWithValue:@42];
}] mapOnContext:[TWLContext nowOrContext:[TWLContext queue:TestQueue.two]] handler:^id _Nonnull(NSNumber * _Nonnull value) {
XCTAssertFalse(TWLContext.isExecutingNow);
AssertOnTestQueue(2);
return @1;
}];
Expand All @@ -1568,6 +1582,7 @@ - (void)testMapNowOrContext {
__block TWLPromise<NSNumber*,NSString*> *promise;
dispatch_sync(TestQueue.one, ^{
promise = [[TWLPromise<NSNumber*,NSString*> newFulfilledWithValue:@42] mapOnContext:[TWLContext nowOrContext:[TWLContext queue:TestQueue.two]] handler:^id _Nonnull(NSNumber * _Nonnull value) {
XCTAssertTrue(TWLContext.isExecutingNow);
AssertOnTestQueue(1);
return @1;
}];
Expand All @@ -1584,11 +1599,13 @@ - (void)testWhenCancelRequestedUsingNowOrContextWithPromiseChaining {
__auto_type expectation = [XCTestExpectation new];
dispatch_async(TestQueue.one, ^{
[resolver whenCancelRequestedOnContext:[TWLContext nowOrContext:[TWLContext queue:TestQueue.two]] handler:^(TWLResolver<NSNumber *,NSString *> * _Nonnull innerResolver) {
XCTAssertFalse(TWLContext.isExecutingNow);
AssertOnTestQueue(2);
[resolver cancel]; // capture resolver
[expectation fulfill];
}];
[parentPromise inspectOnContext:[TWLContext nowOrContext:[TWLContext queue:TestQueue.two]] handler:^(NSNumber * _Nullable value, NSString * _Nullable error) {
XCTAssertTrue(TWLContext.isExecutingNow);
AssertOnTestQueue(1); // This runs now
[promise requestCancel];
}];
Expand All @@ -1606,6 +1623,7 @@ - (void)testThenNowOrContextWithPromiseChaining {
__auto_type expectation = [XCTestExpectation new];
dispatch_async(TestQueue.one, ^{
[promise thenOnContext:[TWLContext nowOrContext:[TWLContext queue:TestQueue.two]] handler:^(NSNumber * _Nonnull value) {
XCTAssertFalse(TWLContext.isExecutingNow);
AssertOnTestQueue(2);
[expectation fulfill];
}];
Expand All @@ -1622,10 +1640,12 @@ - (void)testThenNowOrContextWithPromiseChaining {
__auto_type expectation = [XCTestExpectation new];
dispatch_async(TestQueue.one, ^{
[promise thenOnContext:[TWLContext nowOrContext:[TWLContext queue:TestQueue.two]] handler:^(NSNumber * _Nonnull value) {
XCTAssertFalse(TWLContext.isExecutingNow);
AssertOnTestQueue(2);
[expectation fulfill];
}];
[parentPromise inspectOnContext:[TWLContext nowOrContext:[TWLContext queue:TestQueue.two]] handler:^(NSNumber * _Nullable value, NSString * _Nullable error) {
XCTAssertTrue(TWLContext.isExecutingNow);
AssertOnTestQueue(1); // This runs now
[resolver resolveWithValue:value error:error];
}];
Expand All @@ -1634,6 +1654,43 @@ - (void)testThenNowOrContextWithPromiseChaining {
}
}

- (void)testImmediateNestedInNowOrContext {
(void)[TWLPromise<NSNumber*,NSString*> newOnContext:TWLContext.immediate withBlock:^(TWLResolver<NSNumber *,NSString *> * _Nonnull resolver) {
XCTAssertFalse(TWLContext.isExecutingNow);
}];
__auto_type expectation = [XCTestExpectation new];
(void)[TWLPromise<NSNumber*,NSString*> newOnContext:[TWLContext nowOrContext:TWLContext.defaultQoS] withBlock:^(TWLResolver<NSNumber *,NSString *> * _Nonnull resolver) {
XCTAssertTrue(TWLContext.isExecutingNow);
(void)[TWLPromise<NSNumber*,NSString*> newOnContext:TWLContext.immediate withBlock:^(TWLResolver<NSNumber *,NSString *> * _Nonnull resolver) {
// We're nested in a +nowOrContext: so we inherit its flag
XCTAssertTrue(TWLContext.isExecutingNow);
}];
// Also test a chained callback
(void)[[TWLPromise<NSNumber*,NSString*> newFulfilledWithValue:@42] thenOnContext:TWLContext.immediate handler:^(NSNumber * _Nonnull value) {
XCTAssertTrue(TWLContext.isExecutingNow);
}];
[expectation fulfill]; // just in case
}];
[self waitForExpectations:@[expectation] timeout:0];
}

- (void)testImmediateChainedFromNowOrContext {
TWLResolver<NSNumber*,NSString*> *resolver;
__auto_type promise = [[TWLPromise<NSNumber*,NSString*> alloc] initWithResolver:&resolver];
__auto_type expectation = TWLExpectationSuccessWithHandlerOnContext(TWLContext.immediate, promise, ^(NSNumber * _Nonnull value) {
XCTAssertFalse(TWLContext.isExecutingNow);
});
__auto_type expectation2 = [XCTestExpectation new];
[[TWLPromise<NSNumber*,NSString*> newFulfilledWithValue:@42] inspectOnContext:[TWLContext nowOrContext:TWLContext.defaultQoS] handler:^(NSNumber * _Nullable value, NSString * _Nullable error) {
XCTAssertTrue(TWLContext.isExecutingNow);
[resolver resolveWithValue:value error:error];
[self waitForExpectations:@[expectation] timeout:0];
XCTAssertTrue(TWLContext.isExecutingNow);
[expectation2 fulfill];
}];
[self waitForExpectations:@[expectation2] timeout:0]; // Just in case
}

@end

@implementation TWLPromiseTestsRunLoopObserver {
Expand Down
Loading

0 comments on commit 49ccbdb

Please sign in to comment.