2 * Copyright (c) 2017 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
21 * @APPLE_LICENSE_HEADER_END@
24 #import <Foundation/Foundation.h>
28 #import <dispatch/dispatch.h>
30 #import "keychain/ckks/CKKSZoneChangeFetcher.h"
31 #import "keychain/ckks/CKKSFetchAllRecordZoneChangesOperation.h"
32 #import "keychain/ckks/CKKSKeychainView.h"
33 #import "keychain/ckks/CKKSNearFutureScheduler.h"
34 #import "keychain/ckks/CloudKitCategories.h"
35 #import "keychain/ckks/CKKSReachabilityTracker.h"
36 #import "keychain/categories/NSError+UsefulConstructors.h"
38 CKKSFetchBecause* const CKKSFetchBecauseAPNS = (CKKSFetchBecause*) @"apns";
39 CKKSFetchBecause* const CKKSFetchBecauseAPIFetchRequest = (CKKSFetchBecause*) @"api";
40 CKKSFetchBecause* const CKKSFetchBecauseCurrentItemFetchRequest = (CKKSFetchBecause*) @"currentitemcheck";
41 CKKSFetchBecause* const CKKSFetchBecauseInitialStart = (CKKSFetchBecause*) @"initialfetch";
42 CKKSFetchBecause* const CKKSFetchBecauseSecuritydRestart = (CKKSFetchBecause*) @"restart";
43 CKKSFetchBecause* const CKKSFetchBecausePreviousFetchFailed = (CKKSFetchBecause*) @"fetchfailed";
44 CKKSFetchBecause* const CKKSFetchBecauseNetwork = (CKKSFetchBecause*) @"network";
45 CKKSFetchBecause* const CKKSFetchBecauseKeyHierarchy = (CKKSFetchBecause*) @"keyhierarchy";
46 CKKSFetchBecause* const CKKSFetchBecauseTesting = (CKKSFetchBecause*) @"testing";
47 CKKSFetchBecause* const CKKSFetchBecauseResync = (CKKSFetchBecause*) @"resync";
49 #pragma mark - CKKSZoneChangeFetchDependencyOperation
50 @interface CKKSZoneChangeFetchDependencyOperation : CKKSResultOperation
51 @property CKKSZoneChangeFetcher* owner;
52 @property NSMutableArray<CKKSZoneChangeFetchDependencyOperation*>* chainDependents;
53 - (void)chainDependency:(CKKSZoneChangeFetchDependencyOperation*)newDependency;
56 @implementation CKKSZoneChangeFetchDependencyOperation
57 - (instancetype)init {
58 if((self = [super init])) {
59 _chainDependents = [NSMutableArray array];
64 - (NSError* _Nullable)descriptionError {
65 return [NSError errorWithDomain:CKKSResultDescriptionErrorDomain
66 code:CKKSResultDescriptionPendingSuccessfulFetch
67 description:@"Fetch failed"
68 underlying:self.owner.lastCKFetchError];
71 - (void)chainDependency:(CKKSZoneChangeFetchDependencyOperation*)newDependency {
72 [self addSuccessDependency:newDependency];
74 // There's no need to build a chain more than two links long. Move all our children up to depend on the new dependency.
75 for(CKKSZoneChangeFetchDependencyOperation* op in self.chainDependents) {
76 [newDependency.chainDependents addObject:op];
77 [op addSuccessDependency:newDependency];
78 [op removeDependency:self];
80 [self.chainDependents removeAllObjects];
84 #pragma mark - CKKSZoneChangeFetcher
86 @interface CKKSZoneChangeFetcher ()
87 @property NSString* name;
88 @property NSOperationQueue* operationQueue;
89 @property dispatch_queue_t queue;
91 @property NSError* lastCKFetchError;
93 @property NSMapTable<CKRecordZoneID*, id<CKKSChangeFetcherClient>>* clientMap;
95 @property CKKSFetchAllRecordZoneChangesOperation* currentFetch;
96 @property CKKSResultOperation* currentProcessResult;
98 @property NSMutableSet<CKKSFetchBecause*>* currentFetchReasons;
99 @property NSMutableSet<CKRecordZoneNotification*>* apnsPushes;
100 @property bool newRequests; // true if there's someone pending on successfulFetchDependency
101 @property CKKSZoneChangeFetchDependencyOperation* successfulFetchDependency;
103 @property CKKSResultOperation* holdOperation;
106 @implementation CKKSZoneChangeFetcher
108 - (instancetype)initWithContainer:(CKContainer*)container
109 fetchClass:(Class<CKKSFetchRecordZoneChangesOperation>)fetchRecordZoneChangesOperationClass
110 reachabilityTracker:(CKKSReachabilityTracker *)reachabilityTracker
112 if((self = [super init])) {
113 _container = container;
114 _fetchRecordZoneChangesOperationClass = fetchRecordZoneChangesOperationClass;
115 _reachabilityTracker = reachabilityTracker;
117 _currentFetchReasons = [[NSMutableSet alloc] init];
118 _apnsPushes = [[NSMutableSet alloc] init];
120 _clientMap = [NSMapTable strongToWeakObjectsMapTable];
122 _name = @"zone-change-fetcher";
123 _queue = dispatch_queue_create([_name UTF8String], DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
124 _operationQueue = [[NSOperationQueue alloc] init];
125 _successfulFetchDependency = [self createSuccesfulFetchDependency];
127 _newRequests = false;
129 // If we're testing, for the initial delay, use 0.2 second. Otherwise, 2s.
130 dispatch_time_t initialDelay = (SecCKKSReduceRateLimiting() ? 200 * NSEC_PER_MSEC : 2 * NSEC_PER_SEC);
132 // If we're testing, for the initial delay, use 2 second. Otherwise, 30s.
133 dispatch_time_t continuingDelay = (SecCKKSReduceRateLimiting() ? 2 * NSEC_PER_SEC : 30 * NSEC_PER_SEC);
135 __weak __typeof(self) weakSelf = self;
136 _fetchScheduler = [[CKKSNearFutureScheduler alloc] initWithName:@"zone-change-fetch-scheduler"
137 initialDelay:initialDelay
138 continuingDelay:continuingDelay
139 keepProcessAlive:false
140 dependencyDescriptionCode:CKKSResultDescriptionPendingZoneChangeFetchScheduling
142 [weakSelf maybeCreateNewFetch];
148 - (NSString*)description {
149 NSDate* nextFetchAt = self.fetchScheduler.nextFireTime;
151 NSDateFormatter* dateFormatter = [[NSDateFormatter alloc] init];
152 [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
153 return [NSString stringWithFormat: @"<CKKSZoneChangeFetcher(%@): next fetch at %@", self.name, [dateFormatter stringFromDate: nextFetchAt]];
155 return [NSString stringWithFormat: @"<CKKSZoneChangeFetcher(%@): no pending fetches", self.name];
159 - (void)registerClient:(id<CKKSChangeFetcherClient>)client
161 @synchronized(self.clientMap) {
162 [self.clientMap setObject:client forKey:client.zoneID];
167 - (CKKSResultOperation*)requestSuccessfulFetch:(CKKSFetchBecause*)why {
168 return [self requestSuccessfulFetchForManyReasons:[NSSet setWithObject:why]];
171 - (CKKSResultOperation*)requestSuccessfulFetchForManyReasons:(NSSet<CKKSFetchBecause*>*)why
173 return [self requestSuccessfulFetchForManyReasons:why apns:nil];
176 - (CKKSResultOperation*)requestSuccessfulFetchDueToAPNS:(CKRecordZoneNotification*)notification
178 return [self requestSuccessfulFetchForManyReasons:[NSSet setWithObject:CKKSFetchBecauseAPNS] apns:notification];
181 - (CKKSResultOperation*)requestSuccessfulFetchForManyReasons:(NSSet<CKKSFetchBecause*>*)why apns:(CKRecordZoneNotification*)notification
183 __block CKKSResultOperation* dependency = nil;
184 dispatch_sync(self.queue, ^{
185 dependency = self.successfulFetchDependency;
186 self.newRequests = true;
187 [self.currentFetchReasons unionSet:why];
189 [self.apnsPushes addObject:notification];
191 if(notification.ckksPushTracingEnabled) {
192 // Report that we saw this notification before doing anything else
193 secnotice("ckksfetch", "Submitting initial CKEventMetric due to notification %@", notification);
195 CKEventMetric *metric = [[CKEventMetric alloc] initWithEventName:@"APNSPushMetrics"];
196 metric.isPushTriggerFired = true;
197 metric[@"push_token_uuid"] = notification.ckksPushTracingUUID;
198 metric[@"push_received_date"] = notification.ckksPushReceivedDate;
199 metric[@"push_event_name"] = @"CKKS APNS Push Received";
201 [self.container submitEventMetric:metric];
206 [self.fetchScheduler trigger];
212 -(void)maybeCreateNewFetch {
213 dispatch_sync(self.queue, ^{
214 if(self.newRequests &&
215 (self.currentFetch == nil || [self.currentFetch isFinished]) &&
216 (self.currentProcessResult == nil || [self.currentProcessResult isFinished])) {
217 [self _onqueueCreateNewFetch];
222 -(void)_onqueueCreateNewFetch {
223 dispatch_assert_queue(self.queue);
225 __weak __typeof(self) weakSelf = self;
227 CKKSZoneChangeFetchDependencyOperation* dependency = self.successfulFetchDependency;
229 secnotice("ckksfetcher", "Starting a new fetch");
231 NSMutableSet<CKKSFetchBecause*>* lastFetchReasons = self.currentFetchReasons;
232 self.currentFetchReasons = [[NSMutableSet alloc] init];
234 NSMutableSet<CKRecordZoneNotification*>* lastAPNSPushes = self.apnsPushes;
235 self.apnsPushes = [[NSMutableSet alloc] init];
237 CKOperationGroup* operationGroup = [CKOperationGroup CKKSGroupWithName: [[lastFetchReasons sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"description" ascending:YES]]] componentsJoinedByString:@","]];
239 NSMutableArray<id<CKKSChangeFetcherClient>>* clients = [NSMutableArray array];
240 @synchronized(self.clientMap) {
241 for(id<CKKSChangeFetcherClient> client in [self.clientMap objectEnumerator]) {
243 [clients addObject:client];
248 if(clients.count == 0u) {
249 // Nothing to do, really.
252 CKKSFetchAllRecordZoneChangesOperation* fetchAllChanges = [[CKKSFetchAllRecordZoneChangesOperation alloc] initWithContainer:self.container
253 fetchClass:self.fetchRecordZoneChangesOperationClass
255 fetchReasons:lastFetchReasons
256 apnsPushes:lastAPNSPushes
258 ckoperationGroup:operationGroup];
260 if ([lastFetchReasons containsObject:CKKSFetchBecauseNetwork]) {
261 [fetchAllChanges addNullableDependency: self.reachabilityTracker.reachabilityDependency]; // wait on network, if its unavailable
263 [fetchAllChanges addNullableDependency: self.holdOperation];
265 self.currentProcessResult = [CKKSResultOperation operationWithBlock: ^{
266 __strong __typeof(self) strongSelf = weakSelf;
268 secerror("ckksfetcher: Received a null self pointer; strange.");
272 dispatch_sync(strongSelf.queue, ^{
273 self.lastCKFetchError = fetchAllChanges.error;
275 if(!fetchAllChanges.error) {
276 // success! notify the listeners.
277 [self.operationQueue addOperation: dependency];
279 // Did new people show up and want another fetch?
280 if(strongSelf.newRequests) {
281 [strongSelf.fetchScheduler trigger];
284 // The operation errored. Chain the dependency on the current one...
285 [dependency chainDependency:strongSelf.successfulFetchDependency];
286 [strongSelf.operationQueue addOperation: dependency];
288 // Check in with clients: should we keep fetching for them?
289 bool attemptAnotherFetch = false;
290 @synchronized(self.clientMap) {
291 for(CKRecordZoneID* zoneID in fetchAllChanges.fetchedZoneIDs) {
292 id<CKKSChangeFetcherClient> client = [self.clientMap objectForKey:zoneID];
294 attemptAnotherFetch |= [client notifyFetchError:fetchAllChanges.error];
299 if(!attemptAnotherFetch) {
300 secerror("ckksfetcher: All clients thought %@ is a fatal error. Not restarting fetch.", fetchAllChanges.error);
304 // And in a bit, try the fetch again.
305 NSNumber* delaySeconds = fetchAllChanges.error.userInfo[CKErrorRetryAfterKey];
306 if([fetchAllChanges.error.domain isEqual: CKErrorDomain] && delaySeconds) {
307 secnotice("ckksfetcher", "Fetch failed with rate-limiting error, restarting in %@ seconds: %@", delaySeconds, fetchAllChanges.error);
308 [strongSelf.fetchScheduler waitUntil: NSEC_PER_SEC * [delaySeconds unsignedLongValue]];
310 secnotice("ckksfetcher", "Fetch failed with error, restarting soon: %@", fetchAllChanges.error);
313 // Add the failed fetch reasons to the new fetch reasons
314 [strongSelf.currentFetchReasons unionSet:lastFetchReasons];
315 [strongSelf.apnsPushes unionSet:lastAPNSPushes];
317 // If its a network error, make next try depend on network availability
318 if ([self.reachabilityTracker isNetworkError:fetchAllChanges.error]) {
319 [strongSelf.currentFetchReasons addObject:CKKSFetchBecauseNetwork];
321 [strongSelf.currentFetchReasons addObject:CKKSFetchBecausePreviousFetchFailed];
323 strongSelf.newRequests = true;
324 [strongSelf.fetchScheduler trigger];
328 self.currentProcessResult.name = @"zone-change-fetcher-worker";
329 [self.currentProcessResult addDependency: fetchAllChanges];
331 [self.operationQueue addOperation:self.currentProcessResult];
333 self.currentFetch = fetchAllChanges;
334 [self.operationQueue addOperation:self.currentFetch];
336 // creata a new fetch dependency, for all those who come in while this operation is executing
337 self.newRequests = false;
338 self.successfulFetchDependency = [self createSuccesfulFetchDependency];
341 -(CKKSZoneChangeFetchDependencyOperation*)createSuccesfulFetchDependency {
342 CKKSZoneChangeFetchDependencyOperation* dep = [[CKKSZoneChangeFetchDependencyOperation alloc] init];
344 dep.name = @"successful-fetch-dependency";
345 dep.descriptionErrorCode = CKKSResultDescriptionPendingSuccessfulFetch;
351 - (void)holdFetchesUntil:(CKKSResultOperation*)holdOperation {
352 self.holdOperation = holdOperation;
356 [self.fetchScheduler cancel];