CKKSZoneChangeFetcher.m [plain text]
/*
* Copyright (c) 2017 Apple Inc. All Rights Reserved.
*
* @APPLE_LICENSE_HEADER_START@
*
* This file contains Original Code and/or Modifications of Original Code
* as defined in and that are subject to the Apple Public Source License
* Version 2.0 (the 'License'). You may not use this file except in
* compliance with the License. Please obtain a copy of the License at
* http://www.opensource.apple.com/apsl/ and read it before using this
* file.
*
* The Original Code and all software distributed under the License are
* distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
* EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
* INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
* Please see the License for the specific language governing rights and
* limitations under the License.
*
* @APPLE_LICENSE_HEADER_END@
*/
#import <Foundation/Foundation.h>
#if OCTAGON
#import <dispatch/dispatch.h>
#import "keychain/ckks/CKKSZoneChangeFetcher.h"
#import "keychain/ckks/CKKSFetchAllRecordZoneChangesOperation.h"
#import "keychain/ckks/CKKSKeychainView.h"
#import "keychain/ckks/CKKSNearFutureScheduler.h"
#import "keychain/ckks/CloudKitCategories.h"
CKKSFetchBecause* const CKKSFetchBecauseAPNS = (CKKSFetchBecause*) @"apns";
CKKSFetchBecause* const CKKSFetchBecauseAPIFetchRequest = (CKKSFetchBecause*) @"api";
CKKSFetchBecause* const CKKSFetchBecauseCurrentItemFetchRequest = (CKKSFetchBecause*) @"currentitemcheck";
CKKSFetchBecause* const CKKSFetchBecauseInitialStart = (CKKSFetchBecause*) @"initialfetch";
CKKSFetchBecause* const CKKSFetchBecauseSecuritydRestart = (CKKSFetchBecause*) @"restart";
CKKSFetchBecause* const CKKSFetchBecausePreviousFetchFailed = (CKKSFetchBecause*) @"fetchfailed";
CKKSFetchBecause* const CKKSFetchBecauseNetwork = (CKKSFetchBecause*) @"network";
CKKSFetchBecause* const CKKSFetchBecauseKeyHierarchy = (CKKSFetchBecause*) @"keyhierarchy";
CKKSFetchBecause* const CKKSFetchBecauseTesting = (CKKSFetchBecause*) @"testing";
CKKSFetchBecause* const CKKSFetchBecauseResync = (CKKSFetchBecause*) @"resync";
#pragma mark - CKKSZoneChangeFetchDependencyOperation
@interface CKKSZoneChangeFetchDependencyOperation : CKKSResultOperation
@property CKKSZoneChangeFetcher* owner;
@end
@implementation CKKSZoneChangeFetchDependencyOperation
- (NSError* _Nullable)descriptionError {
return [NSError errorWithDomain:CKKSResultDescriptionErrorDomain
code:CKKSResultDescriptionPendingSuccessfulFetch
description:@"Fetch failed"
underlying:self.owner.lastCKFetchError];
}
@end
#pragma mark - CKKSZoneChangeFetcher
@interface CKKSZoneChangeFetcher ()
@property NSString* name;
@property dispatch_queue_t queue;
@property NSError* lastCKFetchError;
@property CKKSFetchAllRecordZoneChangesOperation* currentFetch;
@property CKKSResultOperation* currentProcessResult;
@property NSMutableSet<CKKSFetchBecause*>* currentFetchReasons;
@property bool newRequests; // true if there's someone pending on successfulFetchDependency
@property bool newResyncRequests; // true if someone asked for a refetch operation
@property CKKSResultOperation* successfulFetchDependency;
@property CKKSNearFutureScheduler* fetchScheduler;
@property CKKSResultOperation* holdOperation;
@end
@implementation CKKSZoneChangeFetcher
- (instancetype)initWithCKKSKeychainView:(CKKSKeychainView*)ckks {
if((self = [super init])) {
_ckks = ckks;
_zoneID = ckks.zoneID;
_currentFetchReasons = [[NSMutableSet alloc] init];
_name = [NSString stringWithFormat:@"zone-change-fetcher- _queue = dispatch_queue_create([_name UTF8String], DISPATCH_QUEUE_SERIAL);
_successfulFetchDependency = [self createSuccesfulFetchDependency];
_newRequests = false;
// If we're testing, for the initial delay, use 0.2 second. Otherwise, 2s.
dispatch_time_t initialDelay = (SecCKKSReduceRateLimiting() ? 200 * NSEC_PER_MSEC : 2 * NSEC_PER_SEC);
// If we're testing, for the initial delay, use 2 second. Otherwise, 30s.
dispatch_time_t continuingDelay = (SecCKKSReduceRateLimiting() ? 2 * NSEC_PER_SEC : 30 * NSEC_PER_SEC);
__weak __typeof(self) weakSelf = self;
_fetchScheduler = [[CKKSNearFutureScheduler alloc] initWithName:[NSString stringWithFormat:@"zone-change-fetch-scheduler- initialDelay:initialDelay
continuingDelay:continuingDelay
keepProcessAlive:false
dependencyDescriptionCode:CKKSResultDescriptionPendingZoneChangeFetchScheduling
block:^{
[weakSelf maybeCreateNewFetch];
}];
}
return self;
}
- (NSString*)description {
NSDate* nextFetchAt = self.fetchScheduler.nextFireTime;
if(nextFetchAt) {
NSDateFormatter* dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
return [NSString stringWithFormat: @"<CKKSZoneChangeFetcher( } else {
return [NSString stringWithFormat: @"<CKKSZoneChangeFetcher( }
}
- (CKKSResultOperation*)requestSuccessfulFetch:(CKKSFetchBecause*)why {
return [self requestSuccessfulFetch:why resync:false];
}
- (CKKSResultOperation*)requestSuccessfulResyncFetch:(CKKSFetchBecause*)why {
return [self requestSuccessfulFetch:why resync:true];
}
- (CKKSResultOperation*)requestSuccessfulFetch:(CKKSFetchBecause*)why resync:(bool)resync {
__block CKKSResultOperation* dependency = nil;
dispatch_sync(self.queue, ^{
dependency = self.successfulFetchDependency;
self.newRequests = true;
self.newResyncRequests |= resync;
[self.currentFetchReasons addObject: why];
[self.fetchScheduler trigger];
});
return dependency;
}
-(void)maybeCreateNewFetch {
dispatch_sync(self.queue, ^{
if(self.newRequests &&
(self.currentFetch == nil || [self.currentFetch isFinished]) &&
(self.currentProcessResult == nil || [self.currentProcessResult isFinished])) {
[self _onqueueCreateNewFetch];
}
});
}
-(void)_onqueueCreateNewFetch {
dispatch_assert_queue(self.queue);
__weak __typeof(self) weakSelf = self;
CKKSResultOperation* dependency = self.successfulFetchDependency;
CKKSKeychainView* ckks = self.ckks; // take a strong reference
if(!ckks) {
secerror("ckksfetcher: received a null CKKSKeychainView pointer; strange.");
return;
}
ckksnotice("ckksfetcher", self.zoneID, "Starting a new fetch for
NSMutableSet<CKKSFetchBecause*>* lastFetchReasons = self.currentFetchReasons;
self.currentFetchReasons = [[NSMutableSet alloc] init];
if(self.newResyncRequests) {
[lastFetchReasons addObject:CKKSFetchBecauseResync];
}
CKOperationGroup* operationGroup = [CKOperationGroup CKKSGroupWithName: [[lastFetchReasons sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"description" ascending:YES]]] componentsJoinedByString:@","]];
CKKSFetchAllRecordZoneChangesOperation* fetchAllChanges = [[CKKSFetchAllRecordZoneChangesOperation alloc] initWithCKKSKeychainView:ckks
fetchReasons:lastFetchReasons
ckoperationGroup:operationGroup];
if ([lastFetchReasons containsObject:CKKSFetchBecauseNetwork]) {
[fetchAllChanges addNullableDependency: ckks.reachabilityTracker.reachablityDependency]; // wait on network, if its unavailable
}
[fetchAllChanges addNullableDependency: self.holdOperation];
fetchAllChanges.resync = self.newResyncRequests;
self.newResyncRequests = false;
// Can't fetch until the zone is setup.
[fetchAllChanges addNullableDependency:ckks.zoneSetupOperation];
self.currentProcessResult = [CKKSResultOperation operationWithBlock: ^{
__strong __typeof(self) strongSelf = weakSelf;
if(!strongSelf) {
secerror("ckksfetcher: Received a null self pointer; strange.");
return;
}
CKKSKeychainView* blockckks = strongSelf.ckks; // take a strong reference
if(!blockckks) {
secerror("ckksfetcher: Received a null CKKSKeychainView pointer; strange.");
return;
}
dispatch_sync(strongSelf.queue, ^{
self.lastCKFetchError = fetchAllChanges.error;
if(!fetchAllChanges.error) {
// success! notify the listeners.
[blockckks scheduleOperation: dependency];
// Did new people show up and want another fetch?
if(strongSelf.newRequests) {
[strongSelf.fetchScheduler trigger];
}
} else {
// The operation errored. Chain the dependency on the current one...
[dependency addSuccessDependency: strongSelf.successfulFetchDependency];
[blockckks scheduleOperation: dependency];
if([blockckks isFatalCKFetchError: fetchAllChanges.error]) {
ckkserror("ckksfetcher", strongSelf.zoneID, "Notified that return;
}
// And in a bit, try the fetch again.
NSNumber* delaySeconds = fetchAllChanges.error.userInfo[CKErrorRetryAfterKey];
if([fetchAllChanges.error.domain isEqual: CKErrorDomain] && delaySeconds) {
ckksnotice("ckksfetcher", strongSelf.zoneID, "Fetch failed with rate-limiting error, restarting in [strongSelf.fetchScheduler waitUntil: NSEC_PER_SEC * [delaySeconds unsignedLongValue]];
} else {
ckksnotice("ckksfetcher", strongSelf.zoneID, "Fetch failed with error, restarting soon: }
// Add the failed fetch reasons to the new fetch reasons
[strongSelf.currentFetchReasons unionSet:lastFetchReasons];
// If its a network error, make next try depend on network availability
if ([blockckks.reachabilityTracker isNetworkError:fetchAllChanges.error]) {
[strongSelf.currentFetchReasons addObject:CKKSFetchBecauseNetwork];
} else {
[strongSelf.currentFetchReasons addObject:CKKSFetchBecausePreviousFetchFailed];
}
strongSelf.newRequests = true;
strongSelf.newResyncRequests |= fetchAllChanges.resync;
[strongSelf.fetchScheduler trigger];
}
});
}];
self.currentProcessResult.name = @"zone-change-fetcher-worker";
[self.currentProcessResult addDependency: fetchAllChanges];
[ckks scheduleOperation: self.currentProcessResult];
self.currentFetch = fetchAllChanges;
[ckks scheduleOperation: self.currentFetch];
// creata a new fetch dependency, for all those who come in while this operation is executing
self.newRequests = false;
self.successfulFetchDependency = [self createSuccesfulFetchDependency];
}
-(CKKSZoneChangeFetchDependencyOperation*)createSuccesfulFetchDependency {
CKKSZoneChangeFetchDependencyOperation* dep = [[CKKSZoneChangeFetchDependencyOperation alloc] init];
__weak __typeof(dep) weakDep = dep;
// Since these dependencies might chain, when one runs, break the chain.
[dep addExecutionBlock:^{
__strong __typeof(dep) strongDep = weakDep;
// Remove all dependencies
NSArray* deps = [strongDep.dependencies copy];
for(NSOperation* op in deps) {
[strongDep removeDependency: op];
}
}];
dep.name = @"successful-fetch-dependency";
dep.descriptionErrorCode = CKKSResultDescriptionPendingSuccessfulFetch;
dep.owner = self;
return dep;
}
- (void)holdFetchesUntil:(CKKSResultOperation*)holdOperation {
self.holdOperation = holdOperation;
}
-(void)cancel {
[self.fetchScheduler cancel];
}
@end
#endif