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@
25 #import "CKKSRateLimiter.h"
26 #import <utilities/debugging.h>
27 #import <TargetConditionals.h>
29 typedef NS_ENUM(int, BucketType) {
35 @interface CKKSRateLimiter()
36 @property (readwrite) NSDictionary<NSString *, NSNumber *> *config;
37 @property NSMutableDictionary<NSString *, NSDate *> *buckets;
38 @property NSDate *overloadUntil;
41 @implementation CKKSRateLimiter
43 - (instancetype)init {
44 return [self initWithCoder:nil];
47 - (instancetype)initWithCoder:(NSCoder *)coder {
51 _buckets = [coder decodeObjectOfClasses:[NSSet setWithObjects:[NSMutableDictionary class],
57 _buckets = [NSMutableDictionary new];
60 // this should be done from a downloadable plist, rdar://problem/29945628
61 _config = [NSDictionary dictionaryWithObjectsAndKeys:
66 @10 , @"capacityGroup",
70 @1800, @"overloadDuration", nil];
75 - (BOOL)isEqual: (id) object {
76 if(![object isKindOfClass:[CKKSRateLimiter class]]) {
80 CKKSRateLimiter* obj = (CKKSRateLimiter*) object;
82 return ([self.config isEqual: obj.config] &&
83 [self.buckets isEqual: obj.buckets] &&
84 ((self.overloadUntil == nil && obj.overloadUntil == nil) || ([self.overloadUntil isEqual: obj.overloadUntil]))) ? YES : NO;
87 - (int)rate:(enum BucketType)type {
90 return [self.config[@"rateAll"] intValue];
92 return [self.config[@"rateGroup"] intValue];
94 return [self.config[@"rateUUID"] intValue];
98 - (int)capacity:(enum BucketType)type {
101 return [self.config[@"capacityAll"] intValue];
103 return [self.config[@"capacityGroup"] intValue];
105 return [self.config[@"capacityUUID"] intValue];
109 - (NSDate *)consumeTokenFromBucket:(NSString *)name
110 type:(enum BucketType)type
112 NSDate *threshold = [time dateByAddingTimeInterval:-([self capacity:type] * [self rate:type])];
113 NSDate *bucket = self.buckets[name];
115 if (!bucket || [bucket timeIntervalSinceDate:threshold] < 0) {
119 // Implicitly track the number of tokens in the bucket.
120 // "Would the token I need have been generated in the past or in the future?"
121 bucket = [bucket dateByAddingTimeInterval:[self rate:type]];
122 self.buckets[name] = bucket;
123 return ([bucket timeIntervalSinceDate:time] <= 0) ? nil : [bucket copy];
126 - (int)judge:(CKKSOutgoingQueueEntry * _Nonnull const)entry
127 at:(NSDate * _Nonnull)time
128 limitTime:(NSDate * _Nonnull __autoreleasing * _Nonnull) limitTime
130 if (self.overloadUntil) {
131 if ([time timeIntervalSinceDate:self.overloadUntil] >= 0) {
134 if (self.overloadUntil) {
135 *limitTime = [self.overloadUntil copy];
140 NSDate *all = self.buckets[@"All"];
141 if ((all && [time timeIntervalSinceDate:all] > [self.config[@"trimTime"] intValue]) ||
142 self.buckets.count >= [self.config[@"trimSize"] unsignedIntValue]) {
144 if (self.overloadUntil) {
145 *limitTime = self.overloadUntil;
151 NSDate *sendTime = [self consumeTokenFromBucket:@"All" type:All at:time];
155 NSDate *backoff = [self consumeTokenFromBucket:[NSString stringWithFormat:@"G:%@", entry.accessgroup] type:Group at:time];
157 sendTime = sendTime == nil ? backoff : [sendTime laterDate:backoff];
158 badness = ([backoff timeIntervalSinceDate:
159 [time dateByAddingTimeInterval:([self rate:Group] * 2)]] < 0) ? 2 : 3;
161 backoff = [self consumeTokenFromBucket:[NSString stringWithFormat:@"U:%@", entry.uuid] type:UUID at:time];
163 sendTime = sendTime == nil ? backoff : [sendTime laterDate:backoff];
167 *limitTime = sendTime;
171 - (NSUInteger)stateSize {
172 return self.buckets.count;
176 self.buckets = [NSMutableDictionary new];
177 self.overloadUntil = nil;
180 - (void)trim:(NSDate *)time {
181 int threshold = [self.config[@"trimTime"] intValue];
182 NSSet *toRemove = [self.buckets keysOfEntriesPassingTest:^BOOL(NSString *key, NSDate *obj, BOOL *stop) {
183 return [time timeIntervalSinceDate:obj] > threshold;
186 // Nothing to remove means everybody keeps being noisy. Tell them to go away.
187 if ([toRemove count] == 0) {
188 self.overloadUntil = [self.buckets[@"All"] dateByAddingTimeInterval:[self.config[@"overloadDuration"] intValue]];
189 seccritical("RateLimiter overloaded until %@", self.overloadUntil);
191 self.overloadUntil = nil;
192 [self.buckets removeObjectsForKeys:[toRemove allObjects]];
196 - (void)encodeWithCoder:(NSCoder *)coder {
197 [coder encodeObject:self.buckets forKey:@"buckets"];
200 - (NSString *)diagnostics {
201 NSMutableString *diag = [NSMutableString stringWithFormat:@"RateLimiter config: %@\n", [self.config description]];
203 if (self.overloadUntil != nil) {
204 [diag appendFormat:@"Overloaded until %@, %lu total buckets\n", self.overloadUntil, (unsigned long)[self.buckets count]];
206 [diag appendFormat:@"Not overloaded, %lu total buckets\n", (unsigned long)[self.buckets count]];
209 NSArray *offenders = [self topOffendingAccessGroups:10];
211 [diag appendFormat:@"%lu congested buckets. Top offenders: \n%@ range %@ to %@\n",
212 (unsigned long)[offenders count], offenders, self.buckets[offenders[0]], self.buckets[offenders[[offenders count] - 1]]];
214 [diag appendString:@"No buckets congested"];
220 - (NSArray *)topOffendingAccessGroups:(NSUInteger)num {
221 NSDate *now = [NSDate date];
222 NSSet *congestedKeys = [self.buckets keysOfEntriesPassingTest:^BOOL(NSString *key, NSDate *obj, BOOL *stop) {
223 if (![key hasPrefix:@"G:"]) {
226 return [now timeIntervalSinceDate:obj] <= 0 ? NO : YES;
229 if ([congestedKeys count] > 0) {
230 // Marker must be type NSDate but can be anything since we know all objects will be in the dictionary
231 NSDictionary *congested = [NSDictionary dictionaryWithObjects:[self.buckets objectsForKeys:[congestedKeys allObjects]
232 notFoundMarker:[NSDate date]]
233 forKeys:[congestedKeys allObjects]];
234 NSArray *sortedKeys = [[[congested keysSortedByValueUsingSelector:@selector(compare:)] reverseObjectEnumerator] allObjects];
235 if ([sortedKeys count] > num) {
236 return [sortedKeys subarrayWithRange:NSMakeRange(0, num)];
245 + (BOOL)supportsSecureCoding {