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 <TargetConditionals.h>
28 typedef NS_ENUM(int, BucketType) {
34 @interface CKKSRateLimiter()
35 @property (readwrite) NSDictionary<NSString *, NSNumber *> *config;
36 @property NSMutableDictionary<NSString *, NSDate *> *buckets;
37 @property NSDate *overloadUntil;
40 @implementation CKKSRateLimiter
42 - (instancetype)init {
43 return [self initWithCoder:nil];
46 - (instancetype)initWithCoder:(NSCoder *)coder {
47 if ((self = [super init])) {
49 NSDictionary *encoded;
50 encoded = [coder decodeObjectOfClasses:[NSSet setWithObjects:[NSDictionary class],
56 // Strongly enforce types for the dictionary
57 if (![encoded isKindOfClass:[NSDictionary class]]) {
60 for (id key in encoded) {
61 if (![key isKindOfClass:[NSString class]]) {
64 if (![encoded[key] isKindOfClass:[NSDate class]]) {
68 _buckets = [encoded mutableCopy];
70 _buckets = [NSMutableDictionary new];
75 // this should be done from a downloadable plist, rdar://problem/29945628
76 _config = [NSDictionary dictionaryWithObjectsAndKeys:
81 @10 , @"capacityGroup",
85 @1800, @"overloadDuration", nil];
90 - (BOOL)isEqual: (id) object {
91 if(![object isKindOfClass:[CKKSRateLimiter class]]) {
95 CKKSRateLimiter* obj = (CKKSRateLimiter*) object;
97 return ([self.config isEqual: obj.config] &&
98 [self.buckets isEqual: obj.buckets] &&
99 ((self.overloadUntil == nil && obj.overloadUntil == nil) || ([self.overloadUntil isEqual: obj.overloadUntil]))) ? YES : NO;
102 - (int)rate:(enum BucketType)type {
105 return [self.config[@"rateAll"] intValue];
107 return [self.config[@"rateGroup"] intValue];
109 return [self.config[@"rateUUID"] intValue];
113 - (int)capacity:(enum BucketType)type {
116 return [self.config[@"capacityAll"] intValue];
118 return [self.config[@"capacityGroup"] intValue];
120 return [self.config[@"capacityUUID"] intValue];
124 - (NSDate *)consumeTokenFromBucket:(NSString *)name
125 type:(enum BucketType)type
127 NSDate *threshold = [time dateByAddingTimeInterval:-([self capacity:type] * [self rate:type])];
128 NSDate *bucket = self.buckets[name];
130 if (!bucket || [bucket timeIntervalSinceDate:threshold] < 0) {
134 // Implicitly track the number of tokens in the bucket.
135 // "Would the token I need have been generated in the past or in the future?"
136 bucket = [bucket dateByAddingTimeInterval:[self rate:type]];
137 self.buckets[name] = bucket;
138 return ([bucket timeIntervalSinceDate:time] <= 0) ? nil : [bucket copy];
141 - (int)judge:(CKKSOutgoingQueueEntry * _Nonnull const)entry
142 at:(NSDate * _Nonnull)time
143 limitTime:(NSDate * _Nonnull __autoreleasing * _Nonnull) limitTime
145 if (self.overloadUntil) {
146 if ([time timeIntervalSinceDate:self.overloadUntil] >= 0) {
149 if (self.overloadUntil) {
150 *limitTime = [self.overloadUntil copy];
155 NSDate *all = self.buckets[@"All"];
156 if ((all && [time timeIntervalSinceDate:all] > [self.config[@"trimTime"] intValue]) ||
157 self.buckets.count >= [self.config[@"trimSize"] unsignedIntValue]) {
159 if (self.overloadUntil) {
160 *limitTime = self.overloadUntil;
166 NSDate *sendTime = [self consumeTokenFromBucket:@"All" type:All at:time];
170 NSDate *backoff = [self consumeTokenFromBucket:[NSString stringWithFormat:@"G:%@", entry.accessgroup] type:Group at:time];
172 sendTime = sendTime == nil ? backoff : [sendTime laterDate:backoff];
173 badness = ([backoff timeIntervalSinceDate:
174 [time dateByAddingTimeInterval:([self rate:Group] * 2)]] < 0) ? 2 : 3;
176 backoff = [self consumeTokenFromBucket:[NSString stringWithFormat:@"U:%@", entry.uuid] type:UUID at:time];
178 sendTime = sendTime == nil ? backoff : [sendTime laterDate:backoff];
182 *limitTime = sendTime;
186 - (NSUInteger)stateSize {
187 return self.buckets.count;
191 self.buckets = [NSMutableDictionary new];
192 self.overloadUntil = nil;
195 - (void)trim:(NSDate *)time {
196 int threshold = [self.config[@"trimTime"] intValue];
197 NSSet *toRemove = [self.buckets keysOfEntriesPassingTest:^BOOL(NSString *key, NSDate *obj, BOOL *stop) {
198 return [time timeIntervalSinceDate:obj] > threshold;
201 // Nothing to remove means everybody keeps being noisy. Tell them to go away.
202 if ([toRemove count] == 0) {
203 self.overloadUntil = [self.buckets[@"All"] dateByAddingTimeInterval:[self.config[@"overloadDuration"] unsignedIntValue]];
204 seccritical("RateLimiter overloaded until %@", self.overloadUntil);
206 self.overloadUntil = nil;
207 [self.buckets removeObjectsForKeys:[toRemove allObjects]];
211 - (void)encodeWithCoder:(NSCoder *)coder {
212 [coder encodeObject:self.buckets forKey:@"buckets"];
215 - (NSString *)diagnostics {
216 NSMutableString *diag = [NSMutableString stringWithFormat:@"RateLimiter config: %@\n", [self.config description]];
218 if (self.overloadUntil != nil) {
219 [diag appendFormat:@"Overloaded until %@, %lu total buckets\n", self.overloadUntil, (unsigned long)[self.buckets count]];
221 [diag appendFormat:@"Not overloaded, %lu total buckets\n", (unsigned long)[self.buckets count]];
224 NSArray *offenders = [self topOffendingAccessGroups:10];
226 [diag appendFormat:@"%lu congested buckets. Top offenders: \n%@ range %@ to %@\n",
227 (unsigned long)[offenders count], offenders, self.buckets[offenders[0]], self.buckets[offenders[[offenders count] - 1]]];
229 [diag appendString:@"No buckets congested"];
235 - (NSArray *)topOffendingAccessGroups:(NSUInteger)num {
236 NSDate *now = [NSDate date];
237 NSSet *congestedKeys = [self.buckets keysOfEntriesPassingTest:^BOOL(NSString *key, NSDate *obj, BOOL *stop) {
238 if (![key hasPrefix:@"G:"]) {
241 return [now timeIntervalSinceDate:obj] <= 0 ? NO : YES;
244 if ([congestedKeys count] > 0) {
245 // Marker must be type NSDate but can be anything since we know all objects will be in the dictionary
246 NSDictionary *congested = [NSDictionary dictionaryWithObjects:[self.buckets objectsForKeys:[congestedKeys allObjects]
247 notFoundMarker:[NSDate date]]
248 forKeys:[congestedKeys allObjects]];
249 NSArray *sortedKeys = [[[congested keysSortedByValueUsingSelector:@selector(compare:)] reverseObjectEnumerator] allObjects];
250 if ([sortedKeys count] > num) {
251 return [sortedKeys subarrayWithRange:NSMakeRange(0, num)];
260 + (BOOL)supportsSecureCoding {