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@
26 #import "CKKSNearFutureScheduler.h"
27 #import "CKKSCondition.h"
28 #import "keychain/ckks/NSOperationCategories.h"
29 #import "keychain/ckks/CKKSResultOperation.h"
30 #import "keychain/ot/ObjCImprovements.h"
31 #include <os/transaction_private.h>
33 @interface CKKSNearFutureScheduler ()
34 @property NSString* name;
35 @property dispatch_time_t initialDelay;
37 @property dispatch_time_t currentDelay;
38 @property dispatch_time_t maximumDelay;
40 @property double backoff;
42 @property NSInteger operationDependencyDescriptionCode;
43 @property CKKSResultOperation* operationDependency;
44 @property (nonnull) NSOperationQueue* operationQueue;
46 @property NSDate* predictedNextFireTime;
47 @property bool liveRequest;
48 @property CKKSCondition* liveRequestReceived; // Triggered when liveRequest goes to true.
50 @property dispatch_source_t timer;
51 @property dispatch_queue_t queue;
53 @property bool keepProcessAlive;
54 @property os_transaction_t transaction;
57 @implementation CKKSNearFutureScheduler
59 -(instancetype)initWithName:(NSString*)name
60 delay:(dispatch_time_t)ns
61 keepProcessAlive:(bool)keepProcessAlive
62 dependencyDescriptionCode:(NSInteger)code
63 block:(void (^)(void))futureBlock
65 return [self initWithName:name
68 keepProcessAlive:keepProcessAlive
69 dependencyDescriptionCode:code
73 -(instancetype)initWithName:(NSString*)name
74 initialDelay:(dispatch_time_t)initialDelay
75 continuingDelay:(dispatch_time_t)continuingDelay
76 keepProcessAlive:(bool)keepProcessAlive
77 dependencyDescriptionCode:(NSInteger)code
78 block:(void (^)(void))futureBlock
80 // If the continuing delay is below the initial delay, use an exponential backoff of 1
81 // We'll clamp the timer delay to continuing delay at use time.
82 return [self initWithName:name
83 initialDelay:initialDelay
84 expontialBackoff:MAX(initialDelay > 0 ? (continuingDelay / initialDelay) : 1, 1)
85 maximumDelay:continuingDelay
86 keepProcessAlive:keepProcessAlive
87 dependencyDescriptionCode:code
91 - (instancetype)initWithName:(NSString*)name
92 initialDelay:(dispatch_time_t)initialDelay
93 expontialBackoff:(double)backoff
94 maximumDelay:(dispatch_time_t)maximumDelay
95 keepProcessAlive:(bool)keepProcessAlive
96 dependencyDescriptionCode:(NSInteger)code
97 block:(void (^_Nonnull)(void))futureBlock
99 if((self = [super init])) {
102 _queue = dispatch_queue_create([[NSString stringWithFormat:@"near-future-scheduler-%@",name] UTF8String], DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
103 _initialDelay = initialDelay;
105 _currentDelay = initialDelay;
106 _maximumDelay = maximumDelay;
109 _futureBlock = futureBlock;
111 _liveRequest = false;
112 _liveRequestReceived = [[CKKSCondition alloc] init];
113 _predictedNextFireTime = nil;
115 _keepProcessAlive = keepProcessAlive;
117 _operationQueue = [[NSOperationQueue alloc] init];
118 _operationDependencyDescriptionCode = code;
119 _operationDependency = [self makeOperationDependency];
124 - (void)changeDelays:(dispatch_time_t)initialDelay continuingDelay:(dispatch_time_t)continuingDelay
126 dispatch_sync(self.queue, ^{
127 self.initialDelay = initialDelay;
128 self.currentDelay = self.initialDelay;
129 self.backoff = initialDelay > 0 ? ((double)continuingDelay) / initialDelay : 1;
130 self.maximumDelay = continuingDelay;
134 - (CKKSResultOperation*)makeOperationDependency {
135 CKKSResultOperation* op = [CKKSResultOperation named:[NSString stringWithFormat:@"nfs-%@", self.name] withBlock:^{}];
136 op.descriptionErrorCode = self.operationDependencyDescriptionCode;
140 -(NSString*)description {
141 NSDate* nextAt = self.nextFireTime;
143 NSDateFormatter* dateFormatter = [[NSDateFormatter alloc] init];
144 [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
145 return [NSString stringWithFormat: @"<CKKSNearFutureScheduler(%@): next at %@", self.name, [dateFormatter stringFromDate: nextAt]];
147 return [NSString stringWithFormat: @"<CKKSNearFutureScheduler(%@): no pending attempts", self.name];
151 - (NSDate*)nextFireTime {
152 // If we have a live request, send the next fire time back. Otherwise, wait a tiny tiny bit to see if we receive a request.
153 if(self.liveRequest) {
154 return self.predictedNextFireTime;
155 } else if([self.liveRequestReceived wait:50*NSEC_PER_USEC] == 0) {
156 return self.predictedNextFireTime;
162 -(void)waitUntil:(uint64_t)delay {
163 dispatch_sync(self.queue, ^{
164 [self _onqueueTrigger:delay maximumDelay:DISPATCH_TIME_FOREVER];
168 - (void)triggerAt:(uint64_t)delay {
170 dispatch_async(self.queue, ^{
172 self.liveRequest = true;
173 [self.liveRequestReceived fulfill];
174 [self _onqueueTrigger:(delay == DISPATCH_TIME_FOREVER ? DISPATCH_TIME_NOW : delay) maximumDelay:delay];
178 -(void)_onqueueTimerTick {
179 dispatch_assert_queue(self.queue);
181 if(self.liveRequest) {
182 // Put a new dependency in place, and save the old one for execution
183 NSOperation* dependency = self.operationDependency;
184 self.operationDependency = [self makeOperationDependency];
187 self.liveRequest = false;
188 self.liveRequestReceived = [[CKKSCondition alloc] init];
189 self.transaction = nil;
191 // No current delay means that exponential backoff means nothing. Head straight for slowtown.
192 if(self.currentDelay == 0) {
193 self.currentDelay = self.maximumDelay;
195 // Modify the delay by the exponential backoff, unless that exceeds the maximum delay
196 self.currentDelay = MIN(self.currentDelay * self.backoff, self.maximumDelay);
198 dispatch_source_set_timer(self.timer,
199 dispatch_walltime(NULL, self.currentDelay),
203 [self.operationQueue addOperation: dependency];
205 self.predictedNextFireTime = [NSDate dateWithTimeIntervalSinceNow: (NSTimeInterval) ((double) self.currentDelay) / (double) NSEC_PER_SEC];
207 // The timer has fired with no requests to call the block. Cancel it.
208 dispatch_source_cancel(self.timer);
209 self.predictedNextFireTime = nil;
210 self.currentDelay = self.initialDelay;
216 dispatch_async(self.queue, ^{
218 // The timer tick should call the block!
219 self.liveRequest = true;
220 [self.liveRequestReceived fulfill];
222 [self _onqueueTrigger:DISPATCH_TIME_NOW maximumDelay:DISPATCH_TIME_FOREVER];
226 -(void)_onqueueTrigger:(dispatch_time_t)requestedDelay maximumDelay:(dispatch_time_t)maximumDelay {
227 dispatch_assert_queue(self.queue);
230 // If we don't have one already, set up an os_transaction
231 if(self.keepProcessAlive && self.transaction == nil) {
232 self.transaction = os_transaction_create([[NSString stringWithFormat:@"com.apple.securityd.%@",self.name] UTF8String]);
235 if(requestedDelay != DISPATCH_TIME_NOW && self.predictedNextFireTime != nil) {
236 NSDate* delayTime = [NSDate dateWithTimeIntervalSinceNow: (NSTimeInterval) ((double) requestedDelay) / (double) NSEC_PER_SEC];
237 if([delayTime compare:self.predictedNextFireTime] != NSOrderedDescending) {
238 // The next fire time is after this delay. Do nothing with the request.
240 // Need to cancel the timer and reset it below.
241 dispatch_source_cancel(self.timer);
242 self.predictedNextFireTime = nil;
246 if(maximumDelay != DISPATCH_TIME_FOREVER && self.predictedNextFireTime != nil) {
247 NSDate* delayTime = [NSDate dateWithTimeIntervalSinceNow: (NSTimeInterval) ((double) requestedDelay) / (double) NSEC_PER_SEC];
248 if([delayTime compare:self.predictedNextFireTime] != NSOrderedDescending) {
249 // Need to cancel the timer and reset it below.
250 dispatch_source_cancel(self.timer);
251 self.predictedNextFireTime = nil;
253 // The next fire time is before the maximum delay. Do nothing with the request.
257 // Check if the timer is alive
258 if(self.timer != nil && 0 == dispatch_source_testcancel(self.timer)) {
259 // timer is alive, do nothing
262 self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
264 (dispatch_source_timer_flags_t)0,
266 dispatch_source_set_event_handler(self.timer, ^{
268 [self _onqueueTimerTick];
271 dispatch_time_t actualDelay = self.currentDelay;
272 if(requestedDelay != DISPATCH_TIME_NOW) {
273 actualDelay = MAX(actualDelay, requestedDelay);
275 if(maximumDelay != DISPATCH_TIME_FOREVER) {
276 actualDelay = MIN(actualDelay, maximumDelay);
279 // Note: we pass initialDelay in as the timerInterval here. [-_onqueueTimerTick] is responsible for
280 // modifying the delay to be correct for the next time period.
281 dispatch_source_set_timer(self.timer,
282 dispatch_walltime(NULL, actualDelay),
285 dispatch_resume(self.timer);
287 self.predictedNextFireTime = [NSDate dateWithTimeIntervalSinceNow: (NSTimeInterval) ((double) actualDelay) / (double) NSEC_PER_SEC];
292 dispatch_sync(self.queue, ^{
293 if(self.timer != nil && 0 == dispatch_source_testcancel(self.timer)) {
294 dispatch_source_cancel(self.timer);