Source/TNLRequestOperationQueue.m (508 lines of code) (raw):
//
// TNLRequestOperationQueue.m
// TwitterNetworkLayer
//
// Created on 5/23/14.
// Copyright © 2020 Twitter, Inc. All rights reserved.
//
#include <objc/message.h>
#include <stdatomic.h>
#import "NSURLResponse+TNLAdditions.h"
#import "NSURLSessionConfiguration+TNLAdditions.h"
#import "TNL_Project.h"
#import "TNLBackgroundURLSessionTaskOperationManager.h"
#import "TNLBackoff.h"
#import "TNLGlobalConfiguration.h"
#import "TNLNetwork.h"
#import "TNLNetworkObserver.h"
#import "TNLPriority.h"
#import "TNLRequest.h"
#import "TNLRequestAuthorizer.h"
#import "TNLRequestOperation_Project.h"
#import "TNLRequestOperationQueue_Project.h"
#import "TNLResponse.h"
#import "TNLURLSessionTaskOperation.h"
NS_ASSUME_NONNULL_BEGIN
NSString * const TNLBackgroundRequestOperationDidCompleteNotification = @"TNLBackgroundRequestOperationDidCompleteNotification";
NSString * const TNLBackgroundRequestURLRequestKey = @"URLRequest";
NSString * const TNLBackgroundRequestResponseKey = @"response";
NSString * const TNLBackgroundRequestURLSessionConfigurationIdentifierKey = @"identifier";
NSString * const TNLBackgroundRequestURLSessionTaskIdentifierKey = @"taskIdentifier";
NSString * const TNLBackgroundRequestURLSessionSharedContainerIdentifierKey = @"sharedContainerIdentifier";
static void _GlobalRequestOperationQueueAddOperation(TNLRequestOperation *op);
static volatile atomic_int_fast64_t __attribute__((aligned(8))) sGlobalExecutingConnectionCount = ATOMIC_VAR_INIT(0);
static NSMapTable<NSString *, TNLRequestOperationQueue *> *sGlobalRequestOperationQueueMapTable = nil;
static NSMutableSet<id<TNLNetworkObserver>> *sGlobalNetworkObservers = nil;
static NSOperationQueue *sGlobalRequestOperationQueue = nil;
static NSHashTable<TNLRequestOperation *> *sGlobalAutoDependencyOperations = nil;
//use NSMutableArray instead of NSMutableOrderedSet as it avoids an expensive class load when accessed during +(void)load,
//which for a collection with only a few elements and a few lookups is a worthwhile tradeoff
static NSMutableArray<id<TNLHTTPHeaderProvider>> *sGlobalHeaderProviders = nil;
static dispatch_queue_t _GlobalOperationQueueQueue(void);
static dispatch_queue_t _GlobalOperationQueueQueue()
{
static dispatch_queue_t sGlobalOperationQueueQueue = NULL;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sGlobalOperationQueueQueue = dispatch_queue_create("com.TNL.operation.queue.global.queue", DISPATCH_QUEUE_SERIAL);
});
return sGlobalOperationQueueQueue;
}
static void _GlobalEnqueueOperation(NSOperation *op);
static void _GlobalEnqueueOperation(NSOperation *op)
{
@try {
[sGlobalRequestOperationQueue addOperation:op];
} @catch (NSException *exception) {
TNLLogError(@"[%@ addOperation:%@] - %@", sGlobalRequestOperationQueue, op, exception);
@throw exception;
}
}
static NSArray<TNLRequestOperation *> * __nullable _GlobalAutoDependencyOperations(void);
static NSArray<TNLRequestOperation *> * __nullable _GlobalAutoDependencyOperations()
{
__block NSArray<TNLRequestOperation *> *ops = nil;
tnl_dispatch_sync_autoreleasing(_GlobalOperationQueueQueue(), ^{
ops = [sGlobalAutoDependencyOperations allObjects];
});
return ops;
}
static void _GlobalAddAutoDependencyOperation(TNLRequestOperation *op);
static void _GlobalAddAutoDependencyOperation(TNLRequestOperation *op)
{
tnl_dispatch_async_autoreleasing(_GlobalOperationQueueQueue(), ^{
[sGlobalAutoDependencyOperations addObject:op];
});
}
#if 0 // no use case a.t.m.
static void _GlobalRemoveAutoDependencyOperation(TNLRequestOperation *op);
static void _GlobalRemoveAutoDependencyOperation(TNLRequestOperation *op)
{
tnl_dispatch_async_autoreleasing(_GlobalOperationQueueQueue(), ^{
[sGlobalAutoDependencyOperations removeObject:op];
});
}
#endif
static void _GlobalApplyAutoDependenciesToOperation(TNLRequestOperation *op);
static void _GlobalApplyAutoDependenciesToOperation(TNLRequestOperation *op)
{
const NSInteger dependencyThreshold = (NSInteger)[TNLGlobalConfiguration sharedInstance].operationAutomaticDependencyPriorityThreshold;
if (dependencyThreshold < NSIntegerMax) {
if (op.priority > dependencyThreshold) {
// Auto dependency operation encountered!
_GlobalAddAutoDependencyOperation(op);
} else {
// Add outstanding auto dependency operations as dependencies
NSArray<TNLRequestOperation *> *autoDependencies = _GlobalAutoDependencyOperations();
if (autoDependencies.count > 0) {
TNLLogInformation(@"Marking %@ dependent on %tu higher priority operations", op, autoDependencies.count);
for (TNLRequestOperation *depOp in autoDependencies) {
[op addDependency:depOp];
}
}
}
}
}
@interface TNLRequestOperationQueue (NSURLSessionDelegate) <NSURLSessionDataDelegate, NSURLSessionDownloadDelegate>
@end
TNL_OBJC_DIRECT_MEMBERS
@interface TNLRequestOperationQueue ()
@property (nonatomic, readonly) dispatch_queue_t sessionStateQueue;
@end
#pragma mark - TNLRequestOperationQueue
@implementation TNLRequestOperationQueue
{
id<TNLNetworkObserver> _networkObserver;
NSUInteger _suspendCount;
NSMutableArray<TNLRequestOperation *> *_stagedRequestOperations;
}
+ (void)initialize
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sGlobalRequestOperationQueueMapTable = [NSMapTable strongToWeakObjectsMapTable];
sGlobalAutoDependencyOperations = [NSHashTable weakObjectsHashTable];
sGlobalRequestOperationQueue = [[NSOperationQueue alloc] init];
sGlobalRequestOperationQueue.name = @"com.TNL.global.request.operation.queue";
sGlobalRequestOperationQueue.maxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount;
if ([sGlobalRequestOperationQueue respondsToSelector:@selector(setQualityOfService:)]) {
sGlobalRequestOperationQueue.qualityOfService = (NSQualityOfServiceUtility + NSQualityOfServiceUserInitiated / 2);
}
});
}
+ (NSOperationQueue *)globalRequestOperationQueue
{
return sGlobalRequestOperationQueue;
}
+ (void)addGlobalNetworkObserver:(id<TNLNetworkObserver>)observer
{
tnl_dispatch_async_autoreleasing(_GlobalOperationQueueQueue(), ^{
if (!sGlobalNetworkObservers) {
sGlobalNetworkObservers = [[NSMutableSet alloc] init];
}
[sGlobalNetworkObservers addObject:observer];
});
}
+ (void)removeGlobalNetworkObserver:(id<TNLNetworkObserver>)observer
{
tnl_dispatch_async_autoreleasing(_GlobalOperationQueueQueue(), ^{
[sGlobalNetworkObservers removeObject:observer];
});
}
+ (NSArray<id<TNLNetworkObserver>> *)allGlobalNetworkObservers
{
__block NSArray<id<TNLNetworkObserver>> *allGlobalNetworkObservers = nil;
tnl_dispatch_sync_autoreleasing(_GlobalOperationQueueQueue(), ^{
allGlobalNetworkObservers = [sGlobalNetworkObservers allObjects];
});
return allGlobalNetworkObservers;
}
+ (void)addGlobalHeaderProvider:(id<TNLHTTPHeaderProvider>)provider
{
tnl_dispatch_async_autoreleasing(_GlobalOperationQueueQueue(), ^{
if (!sGlobalHeaderProviders) {
sGlobalHeaderProviders = [[NSMutableArray alloc] init];
}
// remove then add to make sure provider is the latest
[sGlobalHeaderProviders removeObject:provider];
[sGlobalHeaderProviders addObject:provider];
});
}
+ (void)removeGlobalHeaderProvider:(id<TNLHTTPHeaderProvider>)provider
{
tnl_dispatch_async_autoreleasing(_GlobalOperationQueueQueue(), ^{
[sGlobalHeaderProviders removeObject:provider];
});
}
+ (nullable NSArray<id<TNLHTTPHeaderProvider>> *)allGlobalHeaderProviders
{
__block NSArray* providers = nil;
tnl_dispatch_sync_autoreleasing(_GlobalOperationQueueQueue(), ^{
providers = [sGlobalHeaderProviders copy];
});
return providers;
}
- (instancetype)init
{
[self doesNotRecognizeSelector:_cmd];
abort();
return nil;
}
- (instancetype)initWithIdentifier:(NSString *)identifier
{
if (self = [super init]) {
TNLIncrementObjectCount([self class]);
_identifier = [identifier copy];
NSMutableCharacterSet *hostCharSet = [NSMutableCharacterSet characterSetWithRange:NSMakeRange('a', 'z' - 'a' + 1)];
[hostCharSet addCharactersInRange:NSMakeRange('A', 'Z' - 'A' + 1)];
[hostCharSet addCharactersInRange:NSMakeRange('0', '9' - '0' + 1)];
[hostCharSet addCharactersInRange:NSMakeRange('.', 1)];
[hostCharSet invert];
if (_identifier.length == 0 || [_identifier rangeOfCharacterFromSet:hostCharSet].location != NSNotFound) {
@throw [NSException exceptionWithName:NSInvalidArgumentException
reason:[NSString stringWithFormat:@"%@ (%@) must be called with a valid URL host/domain string", NSStringFromClass([self class]), NSStringFromSelector(_cmd)]
userInfo:@{ @"identifier" : (_identifier) ?: [NSNull null] }];
}
_sessionStateQueue = dispatch_queue_create([identifier stringByAppendingString:@".operation.queue.state.queue"].UTF8String, DISPATCH_QUEUE_SERIAL);
_stagedRequestOperations = [[NSMutableArray alloc] init];
__block BOOL didRegister = NO;
dispatch_sync(_GlobalOperationQueueQueue(), ^{
didRegister = [sGlobalRequestOperationQueueMapTable objectForKey:self.identifier] == nil;
if (didRegister) {
[sGlobalRequestOperationQueueMapTable setObject:self forKey:self.identifier];
}
});
if (!didRegister) {
@throw [NSException exceptionWithName:NSInvalidArgumentException
reason:[NSString stringWithFormat:@"%@ already exists with identifier = '%@'", NSStringFromClass([self class]), _identifier]
userInfo:@{ @"identifier" : _identifier }];
}
}
return self;
}
- (void)dealloc
{
TNLDecrementObjectCount([self class]);
}
+ (instancetype)defaultOperationQueue
{
static TNLRequestOperationQueue *sDefaultOperationQueue = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sDefaultOperationQueue = [[self alloc] initWithIdentifier:@"com.twitter.http.operation.queue.default"];
});
return sDefaultOperationQueue;
}
#pragma mark Background Events
#if TARGET_OS_IPHONE // == IOS + WATCH + TV
+ (BOOL)handleBackgroundURLSessionEvents:(nullable NSString *)identifier
completionHandler:(dispatch_block_t)completionHandler
{
return [[TNLURLSessionManager sharedInstance] handleBackgroundURLSessionEvents:identifier
completionHandler:completionHandler];
}
#endif
#pragma mark Mutable properties
- (void)setNetworkObserver:(nullable id<TNLNetworkObserver>)networkObserver
{
tnl_dispatch_async_autoreleasing(_sessionStateQueue, ^{
self->_networkObserver = networkObserver;
});
}
- (nullable id<TNLNetworkObserver>)networkObserver
{
__block id<TNLNetworkObserver> observer;
dispatch_sync(_sessionStateQueue, ^{
observer = self->_networkObserver;
});
return observer;
}
#pragma mark Suspension
- (void)suspend
{
if (self == [TNLRequestOperationQueue defaultOperationQueue]) {
TNLAssertNever();
return;
}
METHOD_LOG();
tnl_dispatch_async_autoreleasing(_sessionStateQueue, ^{
self->_suspendCount++;
});
}
- (void)resume
{
if (self == [TNLRequestOperationQueue defaultOperationQueue]) {
TNLAssertNever();
return;
}
METHOD_LOG();
tnl_dispatch_async_autoreleasing(_sessionStateQueue, ^{
if (self->_suspendCount > 0) {
self->_suspendCount--;
}
if (0 == self->_suspendCount) {
[self->_stagedRequestOperations sortUsingComparator:^NSComparisonResult(TNLRequestOperation *obj1, TNLRequestOperation *obj2) {
NSOperationQueuePriority priority1 = [obj1 queuePriority];
NSOperationQueuePriority priority2 = [obj2 queuePriority];
if (priority1 > priority2) {
return NSOrderedAscending; // highest to lowest
} else if (priority1 < priority2) {
return NSOrderedDescending; // highest to lowest
}
return NSOrderedSame;
}];
for (TNLRequestOperation *op in self->_stagedRequestOperations) {
if (!op.isFinished && !op.isExecuting && !op.isCancelled) {
_GlobalRequestOperationQueueAddOperation(op);
}
}
[self->_stagedRequestOperations removeAllObjects];
}
});
}
#pragma mark Enqueue
- (void)enqueueRequestOperation:(TNLRequestOperation *)op
{
if (!op) {
@throw [NSException exceptionWithName:NSInvalidArgumentException
reason:@"TNLRequestOperation argument cannot be nil!"
userInfo:@{ @"operationQueue" : self } ];
} else if (op.requestOperationQueue != nil) {
@throw [NSException exceptionWithName:NSInvalidArgumentException
reason:@"TNLRequestOperation provided was already enqueued!"
userInfo:@{ @"requestOperation" : op, @"operationQueue" : self } ];
} else {
[op enqueueToOperationQueue:self];
}
}
- (TNLRequestOperation *)enqueueRequest:(nullable id<TNLRequest>)request
completion:(nullable TNLRequestDidCompleteBlock)completion
{
TNLRequestOperation *op = [TNLRequestOperation operationWithRequest:request
completion:completion];
[self enqueueRequestOperation:op];
return op;
}
- (void)syncAddRequestOperation:(TNLRequestOperation *)op
{
dispatch_sync(_sessionStateQueue, ^{
if (self->_suspendCount > 0) {
[self->_stagedRequestOperations addObject:op];
} else {
_GlobalRequestOperationQueueAddOperation(op);
}
});
}
- (void)clearQueuedRequestOperation:(TNLRequestOperation *)op
{
tnl_dispatch_async_autoreleasing(_sessionStateQueue, ^{
[self->_stagedRequestOperations removeObject:op];
});
}
#pragma mark Cancel
- (void)_cancelAllStagedRequestOperations:(id<TNLRequestOperationCancelSource>)source
underlyingError:(nullable NSError *)optionalUnderlyingError TNL_OBJC_DIRECT
{
tnl_dispatch_async_autoreleasing(_sessionStateQueue, ^{
NSArray<TNLRequestOperation *> *ops = [self->_stagedRequestOperations copy];
[self->_stagedRequestOperations removeAllObjects];
for (TNLRequestOperation *op in ops) {
[op cancelWithSource:source
underlyingError:optionalUnderlyingError];
}
});
}
- (void)cancelAllWithSource:(id<TNLRequestOperationCancelSource>)source
underlyingError:(nullable NSError *)optionalUnderlyingError
{
[self _cancelAllStagedRequestOperations:source
underlyingError:optionalUnderlyingError];
[[TNLURLSessionManager sharedInstance] cancelAllForQueue:self
source:source
underlyingError:optionalUnderlyingError];
}
- (void)cancelAllWithSource:(id<TNLRequestOperationCancelSource>)source
{
[self cancelAllWithSource:source
underlyingError:nil];
}
#pragma mark Task Operation
- (void)findURLSessionTaskOperationForRequestOperation:(TNLRequestOperation *)op
complete:(TNLRequestOperationQueueFindTaskOperationCompleteBlock)complete
{
[[TNLURLSessionManager sharedInstance] findURLSessionTaskOperationForRequestOperationQueue:self
requestOperation:op
complete:complete];
}
#pragma mark Request Events
- (void)operationDidStart:(TNLRequestOperation *)op
{
[self _executeWithMatchingSelector:@selector(tnl_requestOperationDidStart:)
block:^(id<TNLNetworkObserver> observer) {
[observer tnl_requestOperationDidStart:op];
}];
}
- (void)operation:(TNLRequestOperation *)op
didStartAttemptWithMetrics:(TNLAttemptMetrics *)metrics
{
NSURLRequest *URLRequest = op.currentURLRequest;
[self _executeWithMatchingSelector:@selector(tnl_requestOperation:didStartAttemptRequest:metrics:)
block:^(id<TNLNetworkObserver> observer) {
[observer tnl_requestOperation:op
didStartAttemptRequest:URLRequest
metrics:metrics];
}];
}
- (void)operation:(TNLRequestOperation *)op
didCompleteAttempt:(TNLResponse *)response
disposition:(TNLAttemptCompleteDisposition)disposition
{
[self _executeWithMatchingSelector:@selector(tnl_requestOperation:didCompleteAttemptWithIntermediateResponse:disposition:)
block:^(id<TNLNetworkObserver> observer) {
[observer tnl_requestOperation:op
didCompleteAttemptWithIntermediateResponse:response
disposition:disposition];
}];
}
- (void)operation:(TNLRequestOperation *)op
didCompleteWithResponse:(TNLResponse *)response
{
[self _executeWithMatchingSelector:@selector(tnl_requestOperation:didCompleteWithResponse:)
block:^(id<TNLNetworkObserver> observer) {
[observer tnl_requestOperation:op
didCompleteWithResponse:response];
}];
}
- (void)taskOperation:(TNLURLSessionTaskOperation *)op
didCompleteAttempt:(TNLResponse *)response
{
TNLRequestOperation *requestOp = [op synthesizeRequestOperation];
[self _executeWithMatchingSelector:@selector(tnl_requestOperation:didCompleteWithResponse:)
block:^(id<TNLNetworkObserver> observer) {
[observer tnl_requestOperation:requestOp
didCompleteWithResponse:response];
}];
}
#pragma mark Private
- (void)_executeWithMatchingSelector:(SEL)selector
block:(void(^)(id<TNLNetworkObserver> matchingObserver))matchingBlock TNL_OBJC_DIRECT
{
tnl_dispatch_async_autoreleasing(_GlobalOperationQueueQueue(), ^{
NSSet<id<TNLNetworkObserver>> *observers = [sGlobalNetworkObservers copy];
tnl_dispatch_async_autoreleasing(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (id<TNLNetworkObserver> observer in observers) {
if ([observer respondsToSelector:selector]) {
matchingBlock(observer);
}
}
});
});
tnl_dispatch_async_autoreleasing(_sessionStateQueue, ^{
if ([self->_networkObserver respondsToSelector:selector]) {
matchingBlock(self->_networkObserver);
}
});
}
@end
#pragma mark - Functions
static void _GlobalRequestOperationQueueAddOperation(TNLRequestOperation *op)
{
_GlobalApplyAutoDependenciesToOperation(op);
_GlobalEnqueueOperation(op);
}
@implementation TNLNetwork
+ (BOOL)hasExecutingNetworkConnections
{
return atomic_load(&sGlobalExecutingConnectionCount) > 0;
}
+ (void)incrementExecutingNetworkConnections
{
if (atomic_fetch_add(&sGlobalExecutingConnectionCount, 1) == 0) {
tnl_dispatch_async_autoreleasing(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:TNLNetworkExecutingNetworkConnectionsDidUpdateNotification
object:nil
userInfo:@{ TNLNetworkExecutingNetworkConnectionsExecutingKey : @YES }];
});
}
}
+ (void)decrementExecutingNetworkConnections
{
const int64_t result = atomic_fetch_sub(&sGlobalExecutingConnectionCount, 1);
if (1 == result) {
tnl_dispatch_async_autoreleasing(dispatch_get_main_queue(), ^{
// Post from main thread
[[NSNotificationCenter defaultCenter] postNotificationName:TNLNetworkExecutingNetworkConnectionsDidUpdateNotification
object:nil
userInfo:@{ TNLNetworkExecutingNetworkConnectionsExecutingKey : @NO }];
});
} else if (result <= 0) {
TNLLogWarning(@"%@ was called too many times! Executing connections count is now negative!", NSStringFromSelector(_cmd));
}
}
+ (void)backoffSignalEncounteredForURL:(NSURL *)URL
host:(nullable NSString *)host
responseHTTPHeaders:(nullable NSDictionary<NSString *, NSString *> *)headers
{
[[TNLURLSessionManager sharedInstance] backoffSignalEncounteredForURL:URL
host:host
responseHTTPHeaders:headers];
}
+ (void)HTTPURLResponseEncounteredOutsideOfTNL:(NSHTTPURLResponse *)response
host:(nullable NSString *)host
{
NSURL *URL = response.URL;
if (!URL) {
return;
}
const TNLHTTPStatusCode statusCode = response.statusCode;
NSDictionary *headers = response.allHeaderFields;
const BOOL shouldSignal = [[TNLGlobalConfiguration sharedInstance].backoffSignaler tnl_shouldSignalBackoffForURL:URL
host:host
statusCode:statusCode
responseHeaders:headers];
if (!shouldSignal) {
return;
}
[self backoffSignalEncounteredForURL:URL
host:host
responseHTTPHeaders:headers];
}
+ (void)applyBackoffDependenciesToOperation:(NSOperation *)op
withURL:(NSURL *)URL
host:(nullable NSString *)host
isLongPollRequest:(BOOL)isLongPoll
{
[[TNLURLSessionManager sharedInstance] applyBackoffDependenciesToOperation:op
withURL:URL
host:host
isLongPollRequest:isLongPoll];
}
@end
NS_ASSUME_NONNULL_END