• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 *
3 * Copyright 2019 gRPC authors.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 *
17 */
18
19#import "CacheInterceptor.h"
20
21@implementation RequestCacheEntry {
22 @protected
23  NSString *_path;
24  id<NSObject> _message;
25}
26
27@synthesize path = _path;
28@synthesize message = _message;
29
30- (instancetype)initWithPath:(NSString *)path message:(id)message {
31  if ((self = [super init])) {
32    _path = [path copy];
33    _message = [message copy];
34  }
35  return self;
36}
37
38- (id)copyWithZone:(NSZone *)zone {
39  return [[RequestCacheEntry allocWithZone:zone] initWithPath:_path message:_message];
40}
41
42- (BOOL)isEqual:(id)object {
43  if ([self class] != [object class]) return NO;
44  RequestCacheEntry *rhs = (RequestCacheEntry *)object;
45  return ([_path isEqualToString:rhs.path] && [_message isEqual:rhs.message]);
46}
47
48- (NSUInteger)hash {
49  return _path.hash ^ _message.hash;
50}
51
52@end
53
54@implementation MutableRequestCacheEntry
55
56@dynamic path;
57@dynamic message;
58
59- (void)setPath:(NSString *)path {
60  _path = [path copy];
61}
62
63- (void)setMessage:(id)message {
64  _message = [message copy];
65}
66
67@end
68
69@implementation ResponseCacheEntry {
70 @protected
71  NSDate *_deadline;
72  NSDictionary *_headers;
73  id _message;
74  NSDictionary *_trailers;
75}
76
77@synthesize deadline = _deadline;
78@synthesize headers = _headers;
79@synthesize message = _message;
80@synthesize trailers = _trailers;
81
82- (instancetype)initWithDeadline:(NSDate *)deadline
83                         headers:(NSDictionary *)headers
84                         message:(id)message
85                        trailers:(NSDictionary *)trailers {
86  if (([super init])) {
87    _deadline = [deadline copy];
88    _headers = [[NSDictionary alloc] initWithDictionary:headers copyItems:YES];
89    _message = [message copy];
90    _trailers = [[NSDictionary alloc] initWithDictionary:trailers copyItems:YES];
91  }
92  return self;
93}
94
95- (id)copyWithZone:(NSZone *)zone {
96  return [[ResponseCacheEntry allocWithZone:zone] initWithDeadline:_deadline
97                                                           headers:_headers
98                                                           message:_message
99                                                          trailers:_trailers];
100}
101
102@end
103
104@implementation MutableResponseCacheEntry
105
106@dynamic deadline;
107@dynamic headers;
108@dynamic message;
109@dynamic trailers;
110
111- (void)setDeadline:(NSDate *)deadline {
112  _deadline = [deadline copy];
113}
114
115- (void)setHeaders:(NSDictionary *)headers {
116  _headers = [[NSDictionary alloc] initWithDictionary:headers copyItems:YES];
117}
118
119- (void)setMessage:(id)message {
120  _message = [message copy];
121}
122
123- (void)setTrailers:(NSDictionary *)trailers {
124  _trailers = [[NSDictionary alloc] initWithDictionary:trailers copyItems:YES];
125}
126
127@end
128
129@implementation CacheContext {
130  NSCache<RequestCacheEntry *, ResponseCacheEntry *> *_cache;
131}
132
133- (instancetype)init {
134  if ((self = [super init])) {
135    _cache = [[NSCache alloc] init];
136  }
137  return self;
138}
139
140- (GRPCInterceptor *)createInterceptorWithManager:(GRPCInterceptorManager *)interceptorManager {
141  return [[CacheInterceptor alloc] initWithInterceptorManager:interceptorManager cacheContext:self];
142}
143
144- (ResponseCacheEntry *)getCachedResponseForRequest:(RequestCacheEntry *)request {
145  ResponseCacheEntry *response = nil;
146  @synchronized(self) {
147    response = [_cache objectForKey:request];
148    if ([response.deadline timeIntervalSinceNow] < 0) {
149      [_cache removeObjectForKey:request];
150      response = nil;
151    }
152  }
153  return response;
154}
155
156- (void)setCachedResponse:(ResponseCacheEntry *)response forRequest:(RequestCacheEntry *)request {
157  @synchronized(self) {
158    [_cache setObject:response forKey:request];
159  }
160}
161
162@end
163
164@implementation CacheInterceptor {
165  GRPCInterceptorManager *_manager;
166  CacheContext *_context;
167  dispatch_queue_t _dispatchQueue;
168
169  BOOL _cacheable;
170  BOOL _writeMessageSeen;
171  BOOL _readMessageSeen;
172  GRPCCallOptions *_callOptions;
173  GRPCRequestOptions *_requestOptions;
174  id _requestMessage;
175  MutableRequestCacheEntry *_request;
176  MutableResponseCacheEntry *_response;
177}
178
179- (dispatch_queue_t)requestDispatchQueue {
180  return _dispatchQueue;
181}
182
183- (dispatch_queue_t)dispatchQueue {
184  return _dispatchQueue;
185}
186
187- (instancetype)initWithInterceptorManager:(GRPCInterceptorManager *_Nonnull)intercepterManager
188                              cacheContext:(CacheContext *_Nonnull)cacheContext {
189  if ((self = [super initWithInterceptorManager:intercepterManager
190                           requestDispatchQueue:dispatch_get_main_queue()
191                          responseDispatchQueue:dispatch_get_main_queue()])) {
192    _manager = intercepterManager;
193    _context = cacheContext;
194    _dispatchQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
195
196    _cacheable = YES;
197    _writeMessageSeen = NO;
198    _readMessageSeen = NO;
199    _request = nil;
200    _response = nil;
201  }
202  return self;
203}
204
205- (void)startWithRequestOptions:(GRPCRequestOptions *)requestOptions
206                    callOptions:(GRPCCallOptions *)callOptions {
207  if (requestOptions.safety != GRPCCallSafetyCacheableRequest) {
208    _cacheable = NO;
209    [_manager startNextInterceptorWithRequest:requestOptions callOptions:callOptions];
210  } else {
211    _requestOptions = [requestOptions copy];
212    _callOptions = [callOptions copy];
213  }
214}
215
216- (void)writeData:(id)data {
217  if (!_cacheable) {
218    [_manager writeNextInterceptorWithData:data];
219  } else {
220    NSAssert(!_writeMessageSeen, @"CacheInterceptor does not support streaming call");
221    if (_writeMessageSeen) {
222      NSLog(@"CacheInterceptor does not support streaming call");
223    }
224    _writeMessageSeen = YES;
225    _requestMessage = [data copy];
226  }
227}
228
229- (void)finish {
230  if (!_cacheable) {
231    [_manager finishNextInterceptor];
232  } else {
233    _request = [[MutableRequestCacheEntry alloc] init];
234    _request.path = _requestOptions.path;
235    _request.message = [_requestMessage copy];
236    _response = [[_context getCachedResponseForRequest:_request] copy];
237    if (!_response) {
238      [_manager startNextInterceptorWithRequest:_requestOptions callOptions:_callOptions];
239      [_manager writeNextInterceptorWithData:_requestMessage];
240      [_manager finishNextInterceptor];
241    } else {
242      [_manager forwardPreviousInterceptorWithInitialMetadata:_response.headers];
243      [_manager forwardPreviousInterceptorWithData:_response.message];
244      [_manager forwardPreviousInterceptorCloseWithTrailingMetadata:_response.trailers error:nil];
245      [_manager shutDown];
246    }
247  }
248}
249
250- (void)didReceiveInitialMetadata:(NSDictionary *)initialMetadata {
251  if (_cacheable) {
252    NSDate *deadline = nil;
253    for (NSString *key in initialMetadata) {
254      if ([key.lowercaseString isEqualToString:@"cache-control"]) {
255        NSArray *cacheControls = [initialMetadata[key] componentsSeparatedByString:@","];
256        for (NSString *option in cacheControls) {
257          NSString *trimmedOption =
258              [option stringByTrimmingCharactersInSet:[NSCharacterSet
259                                                          characterSetWithCharactersInString:@" "]];
260          if ([trimmedOption.lowercaseString isEqualToString:@"no-cache"] ||
261              [trimmedOption.lowercaseString isEqualToString:@"no-store"] ||
262              [trimmedOption.lowercaseString isEqualToString:@"no-transform"]) {
263            _cacheable = NO;
264            break;
265          } else if ([trimmedOption.lowercaseString hasPrefix:@"max-age="]) {
266            NSArray<NSString *> *components = [trimmedOption componentsSeparatedByString:@"="];
267            if (components.count == 2) {
268              NSUInteger maxAge = components[1].intValue;
269              deadline = [NSDate dateWithTimeIntervalSinceNow:maxAge];
270            }
271          }
272        }
273      }
274    }
275    if (_cacheable) {
276      _response = [[MutableResponseCacheEntry alloc] init];
277      _response.headers = [initialMetadata copy];
278      _response.deadline = deadline;
279    }
280  }
281  [_manager forwardPreviousInterceptorWithInitialMetadata:initialMetadata];
282}
283
284- (void)didReceiveData:(id)data {
285  if (_cacheable) {
286    NSAssert(!_readMessageSeen, @"CacheInterceptor does not support streaming call");
287    if (_readMessageSeen) {
288      NSLog(@"CacheInterceptor does not support streaming call");
289    }
290    _readMessageSeen = YES;
291    _response.message = [data copy];
292  }
293  [_manager forwardPreviousInterceptorWithData:data];
294}
295
296- (void)didCloseWithTrailingMetadata:(NSDictionary *)trailingMetadata error:(NSError *)error {
297  if (error == nil && _cacheable) {
298    _response.trailers = [trailingMetadata copy];
299    [_context setCachedResponse:_response forRequest:_request];
300    NSLog(@"Write cache for %@", _request);
301  }
302  [_manager forwardPreviousInterceptorCloseWithTrailingMetadata:trailingMetadata error:error];
303  [_manager shutDown];
304}
305
306@end
307