]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/CKKSNearFutureScheduler.m
Security-59306.61.1.tar.gz
[apple/security.git] / keychain / ckks / CKKSNearFutureScheduler.m
1 /*
2 * Copyright (c) 2017 Apple Inc. All Rights Reserved.
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
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
11 * file.
12 *
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.
20 *
21 * @APPLE_LICENSE_HEADER_END@
22 */
23
24 #if OCTAGON
25
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>
32
33 @interface CKKSNearFutureScheduler ()
34 @property NSString* name;
35 @property dispatch_time_t initialDelay;
36
37 @property dispatch_time_t currentDelay;
38 @property dispatch_time_t maximumDelay;
39
40 @property double backoff;
41
42 @property NSInteger operationDependencyDescriptionCode;
43 @property CKKSResultOperation* operationDependency;
44 @property (nonnull) NSOperationQueue* operationQueue;
45
46 @property NSDate* predictedNextFireTime;
47 @property bool liveRequest;
48
49 @property dispatch_source_t timer;
50 @property dispatch_queue_t queue;
51
52 @property bool keepProcessAlive;
53 @property os_transaction_t transaction;
54 @end
55
56 @implementation CKKSNearFutureScheduler
57
58 -(instancetype)initWithName:(NSString*)name
59 delay:(dispatch_time_t)ns
60 keepProcessAlive:(bool)keepProcessAlive
61 dependencyDescriptionCode:(NSInteger)code
62 block:(void (^)(void))futureBlock
63 {
64 return [self initWithName:name
65 initialDelay:ns
66 continuingDelay:ns
67 keepProcessAlive:keepProcessAlive
68 dependencyDescriptionCode:code
69 block:futureBlock];
70 }
71
72 -(instancetype)initWithName:(NSString*)name
73 initialDelay:(dispatch_time_t)initialDelay
74 continuingDelay:(dispatch_time_t)continuingDelay
75 keepProcessAlive:(bool)keepProcessAlive
76 dependencyDescriptionCode:(NSInteger)code
77 block:(void (^)(void))futureBlock
78 {
79 // If the continuing delay is below the initial delay, use an exponential backoff of 1
80 // We'll clamp the timer delay to continuing delay at use time.
81 return [self initWithName:name
82 initialDelay:initialDelay
83 expontialBackoff:MAX(initialDelay > 0 ? (continuingDelay / initialDelay) : 1, 1)
84 maximumDelay:continuingDelay
85 keepProcessAlive:keepProcessAlive
86 dependencyDescriptionCode:code
87 block:futureBlock];
88 }
89
90 - (instancetype)initWithName:(NSString*)name
91 initialDelay:(dispatch_time_t)initialDelay
92 expontialBackoff:(double)backoff
93 maximumDelay:(dispatch_time_t)maximumDelay
94 keepProcessAlive:(bool)keepProcessAlive
95 dependencyDescriptionCode:(NSInteger)code
96 block:(void (^_Nonnull)(void))futureBlock
97 {
98 if((self = [super init])) {
99 _name = name;
100
101 _queue = dispatch_queue_create([[NSString stringWithFormat:@"near-future-scheduler-%@",name] UTF8String], DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
102 _initialDelay = initialDelay;
103
104 _currentDelay = initialDelay;
105 _maximumDelay = maximumDelay;
106 _backoff = backoff;
107
108 _futureBlock = futureBlock;
109
110 _liveRequest = false;
111 _liveRequestReceived = [[CKKSCondition alloc] init];
112 _predictedNextFireTime = nil;
113
114 _keepProcessAlive = keepProcessAlive;
115
116 _operationQueue = [[NSOperationQueue alloc] init];
117 _operationDependencyDescriptionCode = code;
118 _operationDependency = [self makeOperationDependency];
119 }
120 return self;
121 }
122
123 - (void)changeDelays:(dispatch_time_t)initialDelay continuingDelay:(dispatch_time_t)continuingDelay
124 {
125 dispatch_sync(self.queue, ^{
126 self.initialDelay = initialDelay;
127 self.currentDelay = self.initialDelay;
128 self.backoff = initialDelay > 0 ? ((double)continuingDelay) / initialDelay : 1;
129 self.maximumDelay = continuingDelay;
130 });
131 }
132
133 - (CKKSResultOperation*)makeOperationDependency {
134 CKKSResultOperation* op = [CKKSResultOperation named:[NSString stringWithFormat:@"nfs-%@", self.name] withBlock:^{}];
135 op.descriptionErrorCode = self.operationDependencyDescriptionCode;
136 return op;
137 }
138
139 -(NSString*)description {
140 NSDate* nextAt = self.nextFireTime;
141 if(nextAt) {
142 NSDateFormatter* dateFormatter = [[NSDateFormatter alloc] init];
143 [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
144 return [NSString stringWithFormat: @"<CKKSNearFutureScheduler(%@): next at %@", self.name, [dateFormatter stringFromDate: nextAt]];
145 } else {
146 return [NSString stringWithFormat: @"<CKKSNearFutureScheduler(%@): no pending attempts", self.name];
147 }
148 }
149
150 - (NSDate*)nextFireTime {
151 // 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.
152 if(self.liveRequest) {
153 return self.predictedNextFireTime;
154 } else if([self.liveRequestReceived wait:50*NSEC_PER_USEC] == 0) {
155 return self.predictedNextFireTime;
156 }
157
158 return nil;
159 }
160
161 -(void)waitUntil:(uint64_t)delay {
162 dispatch_sync(self.queue, ^{
163 [self _onqueueTrigger:delay maximumDelay:DISPATCH_TIME_FOREVER];
164 });
165 }
166
167 - (void)triggerAt:(uint64_t)delay {
168 WEAKIFY(self);
169 dispatch_async(self.queue, ^{
170 STRONGIFY(self);
171 self.liveRequest = true;
172 [self.liveRequestReceived fulfill];
173 [self _onqueueTrigger:(delay == DISPATCH_TIME_FOREVER ? DISPATCH_TIME_NOW : delay) maximumDelay:delay];
174 });
175 }
176
177 -(void)_onqueueTimerTick {
178 dispatch_assert_queue(self.queue);
179
180 if(self.liveRequest) {
181 // Put a new dependency in place, and save the old one for execution
182 NSOperation* dependency = self.operationDependency;
183 self.operationDependency = [self makeOperationDependency];
184
185 self.futureBlock();
186 self.liveRequest = false;
187 self.liveRequestReceived = [[CKKSCondition alloc] init];
188 self.transaction = nil;
189
190 // No current delay means that exponential backoff means nothing. Head straight for slowtown.
191 if(self.currentDelay == 0) {
192 self.currentDelay = self.maximumDelay;
193 } else {
194 // Modify the delay by the exponential backoff, unless that exceeds the maximum delay
195 self.currentDelay = MIN(self.currentDelay * self.backoff, self.maximumDelay);
196 }
197 dispatch_source_set_timer(self.timer,
198 dispatch_walltime(NULL, self.currentDelay),
199 self.currentDelay,
200 50 * NSEC_PER_MSEC);
201
202 [self.operationQueue addOperation: dependency];
203
204 self.predictedNextFireTime = [NSDate dateWithTimeIntervalSinceNow: (NSTimeInterval) ((double) self.currentDelay) / (double) NSEC_PER_SEC];
205 } else {
206 // The timer has fired with no requests to call the block. Cancel it.
207 dispatch_source_cancel(self.timer);
208 self.predictedNextFireTime = nil;
209 self.currentDelay = self.initialDelay;
210 }
211 }
212
213 -(void)trigger {
214 WEAKIFY(self);
215 dispatch_async(self.queue, ^{
216 STRONGIFY(self);
217 // The timer tick should call the block!
218 self.liveRequest = true;
219 [self.liveRequestReceived fulfill];
220
221 [self _onqueueTrigger:DISPATCH_TIME_NOW maximumDelay:DISPATCH_TIME_FOREVER];
222 });
223 }
224
225 -(void)_onqueueTrigger:(dispatch_time_t)requestedDelay maximumDelay:(dispatch_time_t)maximumDelay {
226 dispatch_assert_queue(self.queue);
227 WEAKIFY(self);
228
229 // If we don't have one already, set up an os_transaction
230 if(self.keepProcessAlive && self.transaction == nil) {
231 self.transaction = os_transaction_create([[NSString stringWithFormat:@"com.apple.securityd.%@",self.name] UTF8String]);
232 }
233
234 if(requestedDelay != DISPATCH_TIME_NOW && self.predictedNextFireTime != nil) {
235 NSDate* delayTime = [NSDate dateWithTimeIntervalSinceNow: (NSTimeInterval) ((double) requestedDelay) / (double) NSEC_PER_SEC];
236 if([delayTime compare:self.predictedNextFireTime] != NSOrderedDescending) {
237 // The next fire time is after this delay. Do nothing with the request.
238 } else {
239 // Need to cancel the timer and reset it below.
240 dispatch_source_cancel(self.timer);
241 self.predictedNextFireTime = nil;
242 }
243 }
244
245 if(maximumDelay != DISPATCH_TIME_FOREVER && self.predictedNextFireTime != nil) {
246 NSDate* delayTime = [NSDate dateWithTimeIntervalSinceNow: (NSTimeInterval) ((double) requestedDelay) / (double) NSEC_PER_SEC];
247 if([delayTime compare:self.predictedNextFireTime] != NSOrderedDescending) {
248 // Need to cancel the timer and reset it below.
249 dispatch_source_cancel(self.timer);
250 self.predictedNextFireTime = nil;
251 } else {
252 // The next fire time is before the maximum delay. Do nothing with the request.
253 }
254 }
255
256 // Check if the timer is alive
257 if(self.timer != nil && 0 == dispatch_source_testcancel(self.timer)) {
258 // timer is alive, do nothing
259 } else {
260 // start the timer
261 self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
262 0,
263 (dispatch_source_timer_flags_t)0,
264 self.queue);
265 dispatch_source_set_event_handler(self.timer, ^{
266 STRONGIFY(self);
267 [self _onqueueTimerTick];
268 });
269
270 dispatch_time_t actualDelay = self.currentDelay;
271 if(requestedDelay != DISPATCH_TIME_NOW) {
272 actualDelay = MAX(actualDelay, requestedDelay);
273 }
274 if(maximumDelay != DISPATCH_TIME_FOREVER) {
275 actualDelay = MIN(actualDelay, maximumDelay);
276 }
277
278 // Note: we pass initialDelay in as the timerInterval here. [-_onqueueTimerTick] is responsible for
279 // modifying the delay to be correct for the next time period.
280 dispatch_source_set_timer(self.timer,
281 dispatch_walltime(NULL, actualDelay),
282 self.currentDelay,
283 50 * NSEC_PER_MSEC);
284 dispatch_resume(self.timer);
285
286 self.predictedNextFireTime = [NSDate dateWithTimeIntervalSinceNow: (NSTimeInterval) ((double) actualDelay) / (double) NSEC_PER_SEC];
287 };
288 }
289
290 -(void)cancel {
291 dispatch_sync(self.queue, ^{
292 if(self.timer != nil && 0 == dispatch_source_testcancel(self.timer)) {
293 dispatch_source_cancel(self.timer);
294 }
295 });
296 }
297
298 @end
299
300 #endif // OCTAGON