From f98566b8377dac4ee4e004b6b43b0f4fa39dddc5 Mon Sep 17 00:00:00 2001 From: Jeremy Ellison Date: Mon, 27 Jun 2011 12:39:17 -0400 Subject: [PATCH] Implement Timeout Based Caches --- Code/Network/RKRequest.h | 13 ++++++- Code/Network/RKRequest.m | 66 +++++++++++++++++++++++------------ Code/Network/RKRequestCache.h | 2 ++ Code/Network/RKRequestCache.m | 21 +++++++++++ Specs/Network/RKRequestSpec.m | 66 +++++++++++++++++++++++++++++++++++ Specs/Server/server.rb | 2 ++ 6 files changed, 146 insertions(+), 24 deletions(-) diff --git a/Code/Network/RKRequest.h b/Code/Network/RKRequest.h index d7b06017..b4a866fa 100644 --- a/Code/Network/RKRequest.h +++ b/Code/Network/RKRequest.h @@ -45,8 +45,11 @@ typedef enum { // Load from the cache if we have data stored RKRequestCachePolicyEnabled = 1 << 3, + + // Load from the cache if we are within the timeout window + RKRequestCachePolicyTimeout = 1 << 4, - RKRequestCachePolicyDefault = RKRequestCachePolicyEtag + RKRequestCachePolicyDefault = RKRequestCachePolicyEtag | RKRequestCachePolicyTimeout } RKRequestCachePolicy; /** @@ -86,6 +89,7 @@ typedef enum RKRequestBackgroundPolicy { BOOL _sentSynchronously; BOOL _forceBasicAuthentication; RKRequestCache* _cache; + NSTimeInterval _cacheTimeoutInterval; #if TARGET_OS_IPHONE RKRequestBackgroundPolicy _backgroundPolicy; @@ -181,6 +185,13 @@ typedef enum RKRequestBackgroundPolicy { */ @property (nonatomic, retain) NSString* HTTPBodyString; +/** + * The timeout interval within which the request should not be sent + * and the cached response should be used. Used if the cache policy + * includes RKRequestCachePolicyTimeout + */ +@property (nonatomic, assign) NSTimeInterval cacheTimeoutInterval; + //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// diff --git a/Code/Network/RKRequest.m b/Code/Network/RKRequest.m index 0c16ac50..adb8b423 100644 --- a/Code/Network/RKRequest.m +++ b/Code/Network/RKRequest.m @@ -27,7 +27,8 @@ @synthesize URL = _URL, URLRequest = _URLRequest, delegate = _delegate, additionalHTTPHeaders = _additionalHTTPHeaders, params = _params, userData = _userData, username = _username, password = _password, method = _method, - forceBasicAuthentication = _forceBasicAuthentication, cachePolicy = _cachePolicy, cache = _cache; + forceBasicAuthentication = _forceBasicAuthentication, cachePolicy = _cachePolicy, cache = _cache, + cacheTimeoutInterval = _cacheTimeoutInterval; #if TARGET_OS_IPHONE @synthesize backgroundPolicy = _backgroundPolicy, backgroundTaskIdentifier = _backgroundTaskIdentifier; @@ -44,6 +45,7 @@ [self reset]; _forceBasicAuthentication = NO; _cachePolicy = RKRequestCachePolicyDefault; + _cacheTimeoutInterval = 0; } return self; } @@ -268,23 +270,37 @@ [[NSNotificationCenter defaultCenter] postNotificationName:RKRequestSentNotification object:self userInfo:nil]; } -- (BOOL)shouldDispatchRequest { +- (BOOL)shouldLoadFromCache { + // if RKRequestCachePolicyEnabled or if RKRequestCachePolicyTimeout and we are in the timeout + if ([self.cache hasResponseForRequest:self]) { + if (self.cachePolicy & RKRequestCachePolicyEnabled) { + return YES; + } else if (self.cachePolicy & RKRequestCachePolicyTimeout) { + NSDate* date = [self.cache cacheDateForRequest:self]; + NSTimeInterval interval = [[NSDate date] timeIntervalSinceDate:date]; + return interval <= self.cacheTimeoutInterval; + } + } + return NO; +} + +- (RKResponse*)loadResponseFromCache { + RKLogDebug(@"Found cached content, loading..."); + return [self.cache responseForRequest:self]; +} + +- (BOOL)shouldDispatchRequest { return [RKClient sharedClient] == nil || [[RKClient sharedClient] isNetworkAvailable]; } - (void)sendAsynchronously { NSAssert(NO == _isLoading || NO == _isLoaded, @"Cannot send a request that is loading or loaded without resetting it first."); - _sentSynchronously = NO; - if (self.cachePolicy & RKRequestCachePolicyEnabled) { - if ([self.cache hasResponseForRequest:self]) { - RKLogDebug(@"Found cached content, loading..."); - _isLoading = YES; - [self didFinishLoad:[self.cache responseForRequest:self]]; - return; - } - } - - if ([self shouldDispatchRequest]) { + _sentSynchronously = NO; + if ([self shouldLoadFromCache]) { + RKResponse* response = [self loadResponseFromCache]; + _isLoading = YES; + [self didFinishLoad:response]; + } else if ([self shouldDispatchRequest]) { #if TARGET_OS_IPHONE // Background Request Policy support UIApplication* app = [UIApplication sharedApplication]; @@ -327,7 +343,7 @@ [self.cache hasResponseForRequest:self]) { _isLoading = YES; - [self didFinishLoad:[self.cache responseForRequest:self]]; + [self didFinishLoad:[self loadResponseFromCache]]; } else { RKLogCritical(@"SharedClient = %@ and network availability = %d", [RKClient sharedClient], [[RKClient sharedClient] isNetworkAvailable]); @@ -349,12 +365,16 @@ RKResponse* response = nil; _sentSynchronously = YES; - if ([self shouldDispatchRequest]) { - RKLogDebug(@"Sending synchronous %@ request to URL %@.", [self HTTPMethod], [[self URL] absoluteString]); - if (![self prepareURLRequest]) { - // TODO: Logging - return nil; - } + if ([self shouldLoadFromCache]) { + response = [self loadResponseFromCache]; + _isLoading = YES; + [self didFinishLoad:response]; + } else if ([self shouldDispatchRequest]) { + RKLogDebug(@"Sending synchronous %@ request to URL %@.", [self HTTPMethod], [[self URL] absoluteString]); + if (![self prepareURLRequest]) { + // TODO: Logging + return nil; + } [[NSNotificationCenter defaultCenter] postNotificationName:RKRequestSentNotification object:self userInfo:nil]; @@ -378,7 +398,7 @@ if (_cachePolicy & RKRequestCachePolicyLoadIfOffline && [self.cache hasResponseForRequest:self]) { - response = [self.cache responseForRequest:self]; + response = [self loadResponseFromCache]; } else { NSString* errorMessage = [NSString stringWithFormat:@"The client is unable to contact the resource at %@", [[self URL] absoluteString]]; @@ -404,7 +424,7 @@ if (_cachePolicy & RKRequestCachePolicyLoadOnError && [self.cache hasResponseForRequest:self]) { - [self didFinishLoad:[self.cache responseForRequest:self]]; + [self didFinishLoad:[self loadResponseFromCache]]; } else { _isLoading = NO; @@ -429,7 +449,7 @@ RKResponse* finalResponse = response; if ((_cachePolicy & RKRequestCachePolicyEtag) && [response isNotModified]) { - finalResponse = [self.cache responseForRequest:self]; + finalResponse = [self loadResponseFromCache]; } if (![response wasLoadedFromCache] && [response isSuccessful] && (_cachePolicy != RKRequestCachePolicyNone)) { diff --git a/Code/Network/RKRequestCache.h b/Code/Network/RKRequestCache.h index 3e9e1841..3f9d1ad7 100644 --- a/Code/Network/RKRequestCache.h +++ b/Code/Network/RKRequestCache.h @@ -48,6 +48,8 @@ typedef enum { - (NSString*)etagForRequest:(RKRequest*)request; +- (NSDate*)cacheDateForRequest:(RKRequest*)request; + - (void)invalidateRequest:(RKRequest*)request; - (void)invalidateWithStoragePolicy:(RKRequestCacheStoragePolicy)storagePolicy; diff --git a/Code/Network/RKRequestCache.m b/Code/Network/RKRequestCache.m index e064a29f..7b4aafae 100644 --- a/Code/Network/RKRequestCache.m +++ b/Code/Network/RKRequestCache.m @@ -245,6 +245,27 @@ static NSDateFormatter* __rfc1123DateFormatter; return etag; } +- (NSDate*)cacheDateForRequest:(RKRequest*)request { + NSDate* date; + NSString* dateString; + + NSDictionary* responseHeaders = [self headersForRequest:request]; + + [_cacheLock lock]; + if (responseHeaders) { + for (NSString* responseHeader in responseHeaders) { + if ([[responseHeader uppercaseString] isEqualToString:[cacheDateHeaderKey uppercaseString]]) { + dateString = [responseHeaders objectForKey:responseHeader]; + } + } + } + [_cacheLock unlock]; + date = [[RKRequestCache rfc1123DateFormatter] dateFromString:dateString]; + + RKLogDebug(@"Found cached date '%@' for '%@'", date, request); + return date; +} + - (void)invalidateRequest:(RKRequest*)request { [_cacheLock lock]; RKLogDebug(@"Invalidating cache entry for '%@'", request); diff --git a/Specs/Network/RKRequestSpec.m b/Specs/Network/RKRequestSpec.m index 1d6e06de..f25619d2 100644 --- a/Specs/Network/RKRequestSpec.m +++ b/Specs/Network/RKRequestSpec.m @@ -324,6 +324,72 @@ } } +- (void)itShouldLoadFromTheCacheIfWeAreWithinTheTimeout { + RKLogConfigureByName("RestKit/Network", RKLogLevelTrace); + + NSString* baseURL = RKSpecGetBaseURL(); + NSString* cacheDirForClient = [NSString stringWithFormat:@"RKClientRequestCache-%@", + [[NSURL URLWithString:baseURL] host]]; + NSString* cachePath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] + stringByAppendingPathComponent:cacheDirForClient]; + RKRequestCache* cache = [[RKRequestCache alloc] initWithCachePath:cachePath + storagePolicy:RKRequestCacheStoragePolicyPermanently]; + [cache invalidateWithStoragePolicy:RKRequestCacheStoragePolicyPermanently]; + + NSString* url = [NSString stringWithFormat:@"%@/disk/cached", RKSpecGetBaseURL()]; + NSURL* URL = [NSURL URLWithString:url]; + { + RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; + RKRequest* request = [[RKRequest alloc] initWithURL:URL]; + request.cachePolicy = RKRequestCachePolicyTimeout; + request.cacheTimeoutInterval = 5; + request.cache = cache; + request.delegate = loader; + [request sendAsynchronously]; + [loader waitForResponse]; + [expectThat([loader success]) should:be(YES)]; + [expectThat([loader.response bodyAsString]) should:be(@"This Should Get Cached For 5 Seconds")]; + [expectThat([loader.response wasLoadedFromCache]) should:be(NO)]; + } + { + RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; + RKRequest* request = [[RKRequest alloc] initWithURL:URL]; + request.cachePolicy = RKRequestCachePolicyTimeout; + request.cacheTimeoutInterval = 5; + request.cache = cache; + request.delegate = loader; + [request sendAsynchronously]; + // Don't wait for a response as this actually returns synchronously. + [expectThat([loader.response bodyAsString]) should:be(@"This Should Get Cached For 5 Seconds")]; + [expectThat([loader.response wasLoadedFromCache]) should:be(YES)]; + } + { + RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; + RKRequest* request = [[RKRequest alloc] initWithURL:URL]; + request.cachePolicy = RKRequestCachePolicyTimeout; + request.cacheTimeoutInterval = 5; + request.cache = cache; + request.delegate = loader; + [request sendSynchronously]; + // Don't wait for a response as this actually returns synchronously. + [expectThat([loader.response bodyAsString]) should:be(@"This Should Get Cached For 5 Seconds")]; + [expectThat([loader.response wasLoadedFromCache]) should:be(YES)]; + } + { + RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; + RKRequest* request = [[RKRequest alloc] initWithURL:URL]; + request.cachePolicy = RKRequestCachePolicyTimeout; + request.cacheTimeoutInterval = 0; + request.cache = cache; + request.delegate = loader; + [request sendAsynchronously]; + [loader waitForResponse]; + [expectThat([loader success]) should:be(YES)]; + [expectThat([loader.response bodyAsString]) should:be(@"This Should Get Cached For 5 Seconds")]; + [expectThat([loader.response wasLoadedFromCache]) should:be(NO)]; + } +} + - (void)itShouldLoadFromTheCacheIfWeAreOffline { NSString* baseURL = RKSpecGetBaseURL(); NSString* cacheDirForClient = [NSString stringWithFormat:@"RKClientRequestCache-%@", diff --git a/Specs/Server/server.rb b/Specs/Server/server.rb index 3da1d13c..0d913afb 100644 --- a/Specs/Server/server.rb +++ b/Specs/Server/server.rb @@ -11,11 +11,13 @@ Debugger.start $: << File.join(File.expand_path(File.dirname(__FILE__)), 'lib') require 'restkit/network/authentication' require 'restkit/network/etags' +require 'restkit/network/timeout' class RestKit::SpecServer < Sinatra::Base self.app_file = __FILE__ use RestKit::Network::Authentication use RestKit::Network::ETags + use RestKit::Network::Timeout configure do set :logging, true