From 792a24a6f57349f5db6084c36f2e2345fb6b1b9c Mon Sep 17 00:00:00 2001 From: nlIsotools Date: Wed, 1 Apr 2015 18:07:46 +0200 Subject: [PATCH 1/3] Bug where cacheStore implementaiton is really async : need wait cacheStore task finished before use task result --- src/Marvin.HttpCache/HttpCacheHandler.cs | 677 +++++++++++------------ 1 file changed, 327 insertions(+), 350 deletions(-) diff --git a/src/Marvin.HttpCache/HttpCacheHandler.cs b/src/Marvin.HttpCache/HttpCacheHandler.cs index 58fd113..a58ab44 100644 --- a/src/Marvin.HttpCache/HttpCacheHandler.cs +++ b/src/Marvin.HttpCache/HttpCacheHandler.cs @@ -1,351 +1,328 @@ -using Marvin.HttpCache.Store; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; - -namespace Marvin.HttpCache -{ - public class HttpCacheHandler: DelegatingHandler - { - - private readonly ICacheStore _cacheStore; - - private bool _enableConditionalPut = true; - - private bool _enableConditionalPatch = true; - - private bool _enableClearRelatedResourceRepresentationsAfterPut = true; - - private bool _enableClearRelatedResourceRepresentationsAfterPatch = true; - - private bool _forceRevalidationOfStaleResourceRepresentations = false; - - /// - /// Instantiates the HttpCacheHandler - /// - public HttpCacheHandler() - : this(new ImmutableInMemoryCacheStore(), new HttpCacheHandlerSettings()) - { - } - - /// - /// Instantiates the HttpCacheHandler - /// - /// An instance of an implementation of ICacheStore - public HttpCacheHandler(ICacheStore cacheStore) - : this(cacheStore, new HttpCacheHandlerSettings()) - { - } - - /// - /// Instantiates the HttpCacheHandler - /// - /// An instance of an implementation of IHttpCacheHandlerSettings - public HttpCacheHandler(IHttpCacheHandlerSettings cacheHandlerSettings) - : this(new ImmutableInMemoryCacheStore(), cacheHandlerSettings) - { - } - - - /// - /// Instantiates the HttpCacheHandler - /// - /// An instance of an implementation of ICacheStore - /// An instance of an implementation of IHttpCacheHandlerSettings - public HttpCacheHandler(ICacheStore cacheStore, IHttpCacheHandlerSettings cacheHandlerSettings) - { - if (cacheStore == null) - { - throw new ArgumentNullException("cacheStore", "Provided ICacheStore implementation cannot be null."); - } - - if (cacheHandlerSettings == null) - { - throw new ArgumentNullException("cacheHandlerSettings", "Provided IHttpCacheHandlerSettings implementation cannot be null."); - } - - _cacheStore = cacheStore; - - _forceRevalidationOfStaleResourceRepresentations = cacheHandlerSettings.ForceRevalidationOfStaleResourceRepresentations; - _enableConditionalPatch = cacheHandlerSettings.EnableConditionalPatch; - _enableConditionalPut = cacheHandlerSettings.EnableConditionalPut; - _enableClearRelatedResourceRepresentationsAfterPatch = cacheHandlerSettings.EnableClearRelatedResourceRepresentationsAfterPatch; - _enableClearRelatedResourceRepresentationsAfterPut = cacheHandlerSettings.EnableClearRelatedResourceRepresentationsAfterPut; - - } - - - protected override Task SendAsync(HttpRequestMessage request, - System.Threading.CancellationToken cancellationToken) - { - - if (request.Method == HttpMethod.Put || request.Method.Method.ToLower() == "patch") - { - // PUT - PATCH - return HandleHttpPutOrPatch(request, cancellationToken); - } - else if (request.Method == HttpMethod.Get) - { - // GET - return HandleHttpGet(request, cancellationToken); - } - else - { - // DELETE - POST - OTHERS - return base.SendAsync(request, cancellationToken); - } - - } - - private Task HandleHttpPutOrPatch(HttpRequestMessage request, - System.Threading.CancellationToken cancellationToken) - { - - string primaryCacheKey = CacheKeyHelpers.CreatePrimaryCacheKey(request); - var cacheKey = CacheKeyHelpers.CreateCacheKey(primaryCacheKey); - - // cached + conditional PUT or cached + conditional PATCH - if ((_enableConditionalPut && request.Method == HttpMethod.Put) - || - (_enableConditionalPatch && request.Method.Method.ToLower() == "patch")) - { - - bool addCachingHeaders = false; - HttpResponseMessage responseFromCache = null; - - // available in cache? - var responseFromCacheAsTask = _cacheStore.GetAsync(cacheKey); - if (responseFromCacheAsTask.Result != null) - { - addCachingHeaders = true; - responseFromCache = responseFromCacheAsTask.Result.HttpResponse; - } - - if (addCachingHeaders) - { - // set etag / lastmodified. Both are set for better compatibility - // with different backend caching systems. - if (responseFromCache.Headers.ETag != null) - { - request.Headers.Add(HttpHeaderConstants.IfMatch, - responseFromCache.Headers.ETag.ToString()); - } - - if (responseFromCache.Content.Headers.LastModified != null) - { - request.Headers.Add(HttpHeaderConstants.IfUnmodifiedSince, - responseFromCache.Content.Headers.LastModified.Value.ToString("r")); - } - } - } - - return HandleSendAndContinuationForPutPatch(cacheKey, request, cancellationToken); - } - - - private Task HandleHttpGet(HttpRequestMessage request, - System.Threading.CancellationToken cancellationToken) - { - // get VaryByHeaders - order in the request shouldn't matter, so order them so the - // rest of the logic doesn't result in different keys. - - - string primaryCacheKey = CacheKeyHelpers.CreatePrimaryCacheKey(request);// request.RequestUri.ToString(); - bool responseIsCached = false; - HttpResponseMessage responseFromCache = null; - IEnumerable cacheEntriesFromCache = null; - - // first, before even looking at the cache: - // The Cache-Control: no-cache HTTP/1.1 header field is also intended for use in requests made by the client. - // It is a means for the browser to tell the server and any intermediate caches that it wants a - // fresh version of the resource. - - if (request.Headers.CacheControl != null && request.Headers.CacheControl.NoCache) - { - // Don't get from cache. Get from server. - return HandleSendAndContinuation( - CacheKeyHelpers.CreateCacheKey(primaryCacheKey), request, cancellationToken, false); - } - - - - - // available in cache? - var cacheEntriesFromCacheAsTask = _cacheStore.GetAsync(primaryCacheKey); - if (cacheEntriesFromCacheAsTask.Result != default(IEnumerable)) - { - cacheEntriesFromCache = cacheEntriesFromCacheAsTask.Result; - - // TODO: for all of these, check the varyby headers (secondary key). - // An item is a match if secondary & primary keys both match! - responseFromCache = cacheEntriesFromCache.First().HttpResponse; - responseIsCached = true; - } - - if (responseIsCached) - { - - // set the accompanying request message - responseFromCache.RequestMessage = request; - - // Check conditions that might require us to revalidate/check - - // we must assume "the worst": get from server. - - bool mustRevalidate = HttpResponseHelpers.MustRevalidate(responseFromCache); - - if (mustRevalidate) - { - // we must revalidate - add headers to the request for validation. - // - // we add both ETag & IfModifiedSince for better interop with various - // server-side caching handlers. - // - if (responseFromCache.Headers.ETag != null) - { - request.Headers.Add(HttpHeaderConstants.IfNoneMatch, - responseFromCache.Headers.ETag.ToString()); - } - - if (responseFromCache.Content.Headers.LastModified != null) - { - request.Headers.Add(HttpHeaderConstants.IfModifiedSince, - responseFromCache.Content.Headers.LastModified.Value.ToString("r")); - } - - return HandleSendAndContinuation( - CacheKeyHelpers.CreateCacheKey(primaryCacheKey), request, cancellationToken, true); - } - else - { - // response is allowed to be cached and there's - // no need to revalidate: return the cached response - return Task.FromResult(responseFromCache); - } - } - else - { - // response isn't cached. Get it, and (possibly) add it to cache. - return HandleSendAndContinuation( - CacheKeyHelpers.CreateCacheKey(primaryCacheKey), request, cancellationToken, false); - } - - - } - - - private Task HandleSendAndContinuation(CacheKey cacheKey, HttpRequestMessage request, - System.Threading.CancellationToken cancellationToken, bool mustRevalidate) - { - - return base.SendAsync(request, cancellationToken) - .ContinueWith( - task => - { - - var serverResponse = task.Result; - - // if we had to revalidate & got a 304 returned, that means - // we can get the response message from cache. - if (mustRevalidate && serverResponse.StatusCode == HttpStatusCode.NotModified) - { - var cacheEntry = _cacheStore.GetAsync(cacheKey).Result; - var responseFromCacheEntry = cacheEntry.HttpResponse; - responseFromCacheEntry.RequestMessage = request; - - return responseFromCacheEntry; - } - - if (serverResponse.IsSuccessStatusCode) - { - - // ensure no NULL dates - if (serverResponse.Headers.Date == null) - { - serverResponse.Headers.Date = DateTimeOffset.UtcNow; - } - - // check the response: is this response allowed to be cached? - bool isCacheable = HttpResponseHelpers.CanBeCached(serverResponse); - - if (isCacheable) - { - // add the response to cache - _cacheStore.SetAsync(cacheKey, new CacheEntry(serverResponse)); - } - - - // what about vary by headers (=> key should take this into account)? - - } - - return serverResponse; - }); - } - - - - - private Task HandleSendAndContinuationForPutPatch(CacheKey cacheKey, HttpRequestMessage request, - System.Threading.CancellationToken cancellationToken) - { - - return base.SendAsync(request, cancellationToken) - .ContinueWith( - task => - { - - var serverResponse = task.Result; - - if (serverResponse.IsSuccessStatusCode) - { - - // ensure no NULL dates - if (serverResponse.Headers.Date == null) - { - serverResponse.Headers.Date = DateTimeOffset.UtcNow; - } - - // should we clear? - - if ((_enableClearRelatedResourceRepresentationsAfterPut && request.Method == HttpMethod.Put) - || - (_enableClearRelatedResourceRepresentationsAfterPatch && request.Method.Method.ToLower() == "patch")) - { - // clear related resources - // - // - remove resource with cachekey. This must be done, as there's no - // guarantee the new response is cacheable. - // - // - look for resources in cache that start with - // the cachekey + "?" for querystring. - - _cacheStore.RemoveAsync(cacheKey); - _cacheStore.RemoveRangeAsync(cacheKey.PrimaryKey + "?"); - } - - - // check the response: is this response allowed to be cached? - bool isCacheable = HttpResponseHelpers.CanBeCached(serverResponse); - - if (isCacheable) - { - // add the response to cache - _cacheStore.SetAsync(cacheKey, new CacheEntry(serverResponse)); - } - - // what about vary by headers (=> key should take this into account)? - - } - - return serverResponse; - - }); - } - - } +using Marvin.HttpCache.Store; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; + +namespace Marvin.HttpCache +{ + public class HttpCacheHandler: DelegatingHandler + { + + private readonly ICacheStore _cacheStore; + + private bool _enableConditionalPut = true; + + private bool _enableConditionalPatch = true; + + private bool _enableClearRelatedResourceRepresentationsAfterPut = true; + + private bool _enableClearRelatedResourceRepresentationsAfterPatch = true; + + private bool _forceRevalidationOfStaleResourceRepresentations = false; + + /// + /// Instantiates the HttpCacheHandler + /// + public HttpCacheHandler() + : this(new ImmutableInMemoryCacheStore(), new HttpCacheHandlerSettings()) + { + } + + /// + /// Instantiates the HttpCacheHandler + /// + /// An instance of an implementation of ICacheStore + public HttpCacheHandler(ICacheStore cacheStore) + : this(cacheStore, new HttpCacheHandlerSettings()) + { + } + + /// + /// Instantiates the HttpCacheHandler + /// + /// An instance of an implementation of IHttpCacheHandlerSettings + public HttpCacheHandler(IHttpCacheHandlerSettings cacheHandlerSettings) + : this(new ImmutableInMemoryCacheStore(), cacheHandlerSettings) + { + } + + + /// + /// Instantiates the HttpCacheHandler + /// + /// An instance of an implementation of ICacheStore + /// An instance of an implementation of IHttpCacheHandlerSettings + public HttpCacheHandler(ICacheStore cacheStore, IHttpCacheHandlerSettings cacheHandlerSettings) + { + if (cacheStore == null) + { + throw new ArgumentNullException("cacheStore", "Provided ICacheStore implementation cannot be null."); + } + + if (cacheHandlerSettings == null) + { + throw new ArgumentNullException("cacheHandlerSettings", "Provided IHttpCacheHandlerSettings implementation cannot be null."); + } + + _cacheStore = cacheStore; + + _forceRevalidationOfStaleResourceRepresentations = cacheHandlerSettings.ForceRevalidationOfStaleResourceRepresentations; + _enableConditionalPatch = cacheHandlerSettings.EnableConditionalPatch; + _enableConditionalPut = cacheHandlerSettings.EnableConditionalPut; + _enableClearRelatedResourceRepresentationsAfterPatch = cacheHandlerSettings.EnableClearRelatedResourceRepresentationsAfterPatch; + _enableClearRelatedResourceRepresentationsAfterPut = cacheHandlerSettings.EnableClearRelatedResourceRepresentationsAfterPut; + + } + + + protected override Task SendAsync(HttpRequestMessage request, + System.Threading.CancellationToken cancellationToken) + { + + if (request.Method == HttpMethod.Put || request.Method.Method.ToLower() == "patch") + { + // PUT - PATCH + return HandleHttpPutOrPatch(request, cancellationToken); + } + else if (request.Method == HttpMethod.Get) + { + // GET + return HandleHttpGet(request, cancellationToken); + } + else + { + // DELETE - POST - OTHERS + return base.SendAsync(request, cancellationToken); + } + + } + + private async Task HandleHttpPutOrPatch(HttpRequestMessage request, + System.Threading.CancellationToken cancellationToken) + { + + string primaryCacheKey = CacheKeyHelpers.CreatePrimaryCacheKey(request); + var cacheKey = CacheKeyHelpers.CreateCacheKey(primaryCacheKey); + + // cached + conditional PUT or cached + conditional PATCH + if ((_enableConditionalPut && request.Method == HttpMethod.Put) + || + (_enableConditionalPatch && request.Method.Method.ToLower() == "patch")) + { + + bool addCachingHeaders = false; + HttpResponseMessage responseFromCache = null; + + // available in cache? + var cacheEntryFromCache = await _cacheStore.GetAsync(cacheKey); + if (cacheEntryFromCache != null) + { + addCachingHeaders = true; + responseFromCache = cacheEntryFromCache.HttpResponse; + } + + if (addCachingHeaders) + { + // set etag / lastmodified. Both are set for better compatibility + // with different backend caching systems. + if (responseFromCache.Headers.ETag != null) + { + request.Headers.Add(HttpHeaderConstants.IfMatch, + responseFromCache.Headers.ETag.ToString()); + } + + if (responseFromCache.Content.Headers.LastModified != null) + { + request.Headers.Add(HttpHeaderConstants.IfUnmodifiedSince, + responseFromCache.Content.Headers.LastModified.Value.ToString("r")); + } + } + } + + return await HandleSendAndContinuationForPutPatch(cacheKey, request, cancellationToken); + } + + + private async Task HandleHttpGet(HttpRequestMessage request, + System.Threading.CancellationToken cancellationToken) + { + // get VaryByHeaders - order in the request shouldn't matter, so order them so the + // rest of the logic doesn't result in different keys. + + + string primaryCacheKey = CacheKeyHelpers.CreatePrimaryCacheKey(request);// request.RequestUri.ToString(); + bool responseIsCached = false; + HttpResponseMessage responseFromCache = null; + + // first, before even looking at the cache: + // The Cache-Control: no-cache HTTP/1.1 header field is also intended for use in requests made by the client. + // It is a means for the browser to tell the server and any intermediate caches that it wants a + // fresh version of the resource. + + if (request.Headers.CacheControl != null && request.Headers.CacheControl.NoCache) + { + // Don't get from cache. Get from server. + return await HandleSendAndContinuation( + CacheKeyHelpers.CreateCacheKey(primaryCacheKey), request, cancellationToken, false); + } + + + + + // available in cache? + var cacheEntriesFromCache = await _cacheStore.GetAsync(primaryCacheKey); + if (cacheEntriesFromCache != default(IEnumerable)) + { + // TODO: for all of these, check the varyby headers (secondary key). + // An item is a match if secondary & primary keys both match! + responseFromCache = cacheEntriesFromCache.First().HttpResponse; + responseIsCached = true; + } + + if (responseIsCached) + { + + // set the accompanying request message + responseFromCache.RequestMessage = request; + + // Check conditions that might require us to revalidate/check + + // we must assume "the worst": get from server. + + bool mustRevalidate = HttpResponseHelpers.MustRevalidate(responseFromCache); + + if (mustRevalidate) + { + // we must revalidate - add headers to the request for validation. + // + // we add both ETag & IfModifiedSince for better interop with various + // server-side caching handlers. + // + if (responseFromCache.Headers.ETag != null) + { + request.Headers.Add(HttpHeaderConstants.IfNoneMatch, + responseFromCache.Headers.ETag.ToString()); + } + + if (responseFromCache.Content.Headers.LastModified != null) + { + request.Headers.Add(HttpHeaderConstants.IfModifiedSince, + responseFromCache.Content.Headers.LastModified.Value.ToString("r")); + } + + return await HandleSendAndContinuation( + CacheKeyHelpers.CreateCacheKey(primaryCacheKey), request, cancellationToken, true); + } + else + { + // response is allowed to be cached and there's + // no need to revalidate: return the cached response + return responseFromCache; + } + } + else + { + // response isn't cached. Get it, and (possibly) add it to cache. + return await HandleSendAndContinuation( + CacheKeyHelpers.CreateCacheKey(primaryCacheKey), request, cancellationToken, false); + } + + + } + + + private async Task HandleSendAndContinuation(CacheKey cacheKey, HttpRequestMessage request, + System.Threading.CancellationToken cancellationToken, bool mustRevalidate) + { + var serverResponse = await base.SendAsync(request, cancellationToken); + // if we had to revalidate & got a 304 returned, that means + // we can get the response message from cache. + if (mustRevalidate && serverResponse.StatusCode == HttpStatusCode.NotModified) + { + var cacheEntry = await _cacheStore.GetAsync(cacheKey); + var responseFromCacheEntry = cacheEntry.HttpResponse; + responseFromCacheEntry.RequestMessage = request; + + return responseFromCacheEntry; + } + + if (serverResponse.IsSuccessStatusCode) + { + + // ensure no NULL dates + if (serverResponse.Headers.Date == null) + { + serverResponse.Headers.Date = DateTimeOffset.UtcNow; + } + + // check the response: is this response allowed to be cached? + bool isCacheable = HttpResponseHelpers.CanBeCached(serverResponse); + + if (isCacheable) + { + // add the response to cache + await _cacheStore.SetAsync(cacheKey, new CacheEntry(serverResponse)); + } + + + // what about vary by headers (=> key should take this into account)? + + } + + return serverResponse; + } + + + + + private async Task HandleSendAndContinuationForPutPatch(CacheKey cacheKey, HttpRequestMessage request, + System.Threading.CancellationToken cancellationToken) + { + var serverResponse = await base.SendAsync(request, cancellationToken); + if (serverResponse.IsSuccessStatusCode) + { + // ensure no NULL dates + if (serverResponse.Headers.Date == null) + { + serverResponse.Headers.Date = DateTimeOffset.UtcNow; + } + + // should we clear? + + if ((_enableClearRelatedResourceRepresentationsAfterPut && request.Method == HttpMethod.Put) + || + (_enableClearRelatedResourceRepresentationsAfterPatch && request.Method.Method.ToLower() == "patch")) + { + // clear related resources + // + // - remove resource with cachekey. This must be done, as there's no + // guarantee the new response is cacheable. + // + // - look for resources in cache that start with + // the cachekey + "?" for querystring. + + await _cacheStore.RemoveAsync(cacheKey); + await _cacheStore.RemoveRangeAsync(cacheKey.PrimaryKey + "?"); + } + + + // check the response: is this response allowed to be cached? + bool isCacheable = HttpResponseHelpers.CanBeCached(serverResponse); + + if (isCacheable) + { + // add the response to cache + await _cacheStore.SetAsync(cacheKey, new CacheEntry(serverResponse)); + } + + // what about vary by headers (=> key should take this into account)? + + } + return serverResponse; + } + } } \ No newline at end of file From e87d8fe079656f72a62fcf6d45e9add3e1e62ff8 Mon Sep 17 00:00:00 2001 From: nlIsotools Date: Wed, 1 Apr 2015 18:16:03 +0200 Subject: [PATCH 2/3] Revert "Bug where cacheStore implementaiton is really async : need wait cacheStore task finished before use task result" This reverts commit 792a24a6f57349f5db6084c36f2e2345fb6b1b9c. --- src/Marvin.HttpCache/HttpCacheHandler.cs | 677 ++++++++++++----------- 1 file changed, 350 insertions(+), 327 deletions(-) diff --git a/src/Marvin.HttpCache/HttpCacheHandler.cs b/src/Marvin.HttpCache/HttpCacheHandler.cs index a58ab44..58fd113 100644 --- a/src/Marvin.HttpCache/HttpCacheHandler.cs +++ b/src/Marvin.HttpCache/HttpCacheHandler.cs @@ -1,328 +1,351 @@ -using Marvin.HttpCache.Store; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; - -namespace Marvin.HttpCache -{ - public class HttpCacheHandler: DelegatingHandler - { - - private readonly ICacheStore _cacheStore; - - private bool _enableConditionalPut = true; - - private bool _enableConditionalPatch = true; - - private bool _enableClearRelatedResourceRepresentationsAfterPut = true; - - private bool _enableClearRelatedResourceRepresentationsAfterPatch = true; - - private bool _forceRevalidationOfStaleResourceRepresentations = false; - - /// - /// Instantiates the HttpCacheHandler - /// - public HttpCacheHandler() - : this(new ImmutableInMemoryCacheStore(), new HttpCacheHandlerSettings()) - { - } - - /// - /// Instantiates the HttpCacheHandler - /// - /// An instance of an implementation of ICacheStore - public HttpCacheHandler(ICacheStore cacheStore) - : this(cacheStore, new HttpCacheHandlerSettings()) - { - } - - /// - /// Instantiates the HttpCacheHandler - /// - /// An instance of an implementation of IHttpCacheHandlerSettings - public HttpCacheHandler(IHttpCacheHandlerSettings cacheHandlerSettings) - : this(new ImmutableInMemoryCacheStore(), cacheHandlerSettings) - { - } - - - /// - /// Instantiates the HttpCacheHandler - /// - /// An instance of an implementation of ICacheStore - /// An instance of an implementation of IHttpCacheHandlerSettings - public HttpCacheHandler(ICacheStore cacheStore, IHttpCacheHandlerSettings cacheHandlerSettings) - { - if (cacheStore == null) - { - throw new ArgumentNullException("cacheStore", "Provided ICacheStore implementation cannot be null."); - } - - if (cacheHandlerSettings == null) - { - throw new ArgumentNullException("cacheHandlerSettings", "Provided IHttpCacheHandlerSettings implementation cannot be null."); - } - - _cacheStore = cacheStore; - - _forceRevalidationOfStaleResourceRepresentations = cacheHandlerSettings.ForceRevalidationOfStaleResourceRepresentations; - _enableConditionalPatch = cacheHandlerSettings.EnableConditionalPatch; - _enableConditionalPut = cacheHandlerSettings.EnableConditionalPut; - _enableClearRelatedResourceRepresentationsAfterPatch = cacheHandlerSettings.EnableClearRelatedResourceRepresentationsAfterPatch; - _enableClearRelatedResourceRepresentationsAfterPut = cacheHandlerSettings.EnableClearRelatedResourceRepresentationsAfterPut; - - } - - - protected override Task SendAsync(HttpRequestMessage request, - System.Threading.CancellationToken cancellationToken) - { - - if (request.Method == HttpMethod.Put || request.Method.Method.ToLower() == "patch") - { - // PUT - PATCH - return HandleHttpPutOrPatch(request, cancellationToken); - } - else if (request.Method == HttpMethod.Get) - { - // GET - return HandleHttpGet(request, cancellationToken); - } - else - { - // DELETE - POST - OTHERS - return base.SendAsync(request, cancellationToken); - } - - } - - private async Task HandleHttpPutOrPatch(HttpRequestMessage request, - System.Threading.CancellationToken cancellationToken) - { - - string primaryCacheKey = CacheKeyHelpers.CreatePrimaryCacheKey(request); - var cacheKey = CacheKeyHelpers.CreateCacheKey(primaryCacheKey); - - // cached + conditional PUT or cached + conditional PATCH - if ((_enableConditionalPut && request.Method == HttpMethod.Put) - || - (_enableConditionalPatch && request.Method.Method.ToLower() == "patch")) - { - - bool addCachingHeaders = false; - HttpResponseMessage responseFromCache = null; - - // available in cache? - var cacheEntryFromCache = await _cacheStore.GetAsync(cacheKey); - if (cacheEntryFromCache != null) - { - addCachingHeaders = true; - responseFromCache = cacheEntryFromCache.HttpResponse; - } - - if (addCachingHeaders) - { - // set etag / lastmodified. Both are set for better compatibility - // with different backend caching systems. - if (responseFromCache.Headers.ETag != null) - { - request.Headers.Add(HttpHeaderConstants.IfMatch, - responseFromCache.Headers.ETag.ToString()); - } - - if (responseFromCache.Content.Headers.LastModified != null) - { - request.Headers.Add(HttpHeaderConstants.IfUnmodifiedSince, - responseFromCache.Content.Headers.LastModified.Value.ToString("r")); - } - } - } - - return await HandleSendAndContinuationForPutPatch(cacheKey, request, cancellationToken); - } - - - private async Task HandleHttpGet(HttpRequestMessage request, - System.Threading.CancellationToken cancellationToken) - { - // get VaryByHeaders - order in the request shouldn't matter, so order them so the - // rest of the logic doesn't result in different keys. - - - string primaryCacheKey = CacheKeyHelpers.CreatePrimaryCacheKey(request);// request.RequestUri.ToString(); - bool responseIsCached = false; - HttpResponseMessage responseFromCache = null; - - // first, before even looking at the cache: - // The Cache-Control: no-cache HTTP/1.1 header field is also intended for use in requests made by the client. - // It is a means for the browser to tell the server and any intermediate caches that it wants a - // fresh version of the resource. - - if (request.Headers.CacheControl != null && request.Headers.CacheControl.NoCache) - { - // Don't get from cache. Get from server. - return await HandleSendAndContinuation( - CacheKeyHelpers.CreateCacheKey(primaryCacheKey), request, cancellationToken, false); - } - - - - - // available in cache? - var cacheEntriesFromCache = await _cacheStore.GetAsync(primaryCacheKey); - if (cacheEntriesFromCache != default(IEnumerable)) - { - // TODO: for all of these, check the varyby headers (secondary key). - // An item is a match if secondary & primary keys both match! - responseFromCache = cacheEntriesFromCache.First().HttpResponse; - responseIsCached = true; - } - - if (responseIsCached) - { - - // set the accompanying request message - responseFromCache.RequestMessage = request; - - // Check conditions that might require us to revalidate/check - - // we must assume "the worst": get from server. - - bool mustRevalidate = HttpResponseHelpers.MustRevalidate(responseFromCache); - - if (mustRevalidate) - { - // we must revalidate - add headers to the request for validation. - // - // we add both ETag & IfModifiedSince for better interop with various - // server-side caching handlers. - // - if (responseFromCache.Headers.ETag != null) - { - request.Headers.Add(HttpHeaderConstants.IfNoneMatch, - responseFromCache.Headers.ETag.ToString()); - } - - if (responseFromCache.Content.Headers.LastModified != null) - { - request.Headers.Add(HttpHeaderConstants.IfModifiedSince, - responseFromCache.Content.Headers.LastModified.Value.ToString("r")); - } - - return await HandleSendAndContinuation( - CacheKeyHelpers.CreateCacheKey(primaryCacheKey), request, cancellationToken, true); - } - else - { - // response is allowed to be cached and there's - // no need to revalidate: return the cached response - return responseFromCache; - } - } - else - { - // response isn't cached. Get it, and (possibly) add it to cache. - return await HandleSendAndContinuation( - CacheKeyHelpers.CreateCacheKey(primaryCacheKey), request, cancellationToken, false); - } - - - } - - - private async Task HandleSendAndContinuation(CacheKey cacheKey, HttpRequestMessage request, - System.Threading.CancellationToken cancellationToken, bool mustRevalidate) - { - var serverResponse = await base.SendAsync(request, cancellationToken); - // if we had to revalidate & got a 304 returned, that means - // we can get the response message from cache. - if (mustRevalidate && serverResponse.StatusCode == HttpStatusCode.NotModified) - { - var cacheEntry = await _cacheStore.GetAsync(cacheKey); - var responseFromCacheEntry = cacheEntry.HttpResponse; - responseFromCacheEntry.RequestMessage = request; - - return responseFromCacheEntry; - } - - if (serverResponse.IsSuccessStatusCode) - { - - // ensure no NULL dates - if (serverResponse.Headers.Date == null) - { - serverResponse.Headers.Date = DateTimeOffset.UtcNow; - } - - // check the response: is this response allowed to be cached? - bool isCacheable = HttpResponseHelpers.CanBeCached(serverResponse); - - if (isCacheable) - { - // add the response to cache - await _cacheStore.SetAsync(cacheKey, new CacheEntry(serverResponse)); - } - - - // what about vary by headers (=> key should take this into account)? - - } - - return serverResponse; - } - - - - - private async Task HandleSendAndContinuationForPutPatch(CacheKey cacheKey, HttpRequestMessage request, - System.Threading.CancellationToken cancellationToken) - { - var serverResponse = await base.SendAsync(request, cancellationToken); - if (serverResponse.IsSuccessStatusCode) - { - // ensure no NULL dates - if (serverResponse.Headers.Date == null) - { - serverResponse.Headers.Date = DateTimeOffset.UtcNow; - } - - // should we clear? - - if ((_enableClearRelatedResourceRepresentationsAfterPut && request.Method == HttpMethod.Put) - || - (_enableClearRelatedResourceRepresentationsAfterPatch && request.Method.Method.ToLower() == "patch")) - { - // clear related resources - // - // - remove resource with cachekey. This must be done, as there's no - // guarantee the new response is cacheable. - // - // - look for resources in cache that start with - // the cachekey + "?" for querystring. - - await _cacheStore.RemoveAsync(cacheKey); - await _cacheStore.RemoveRangeAsync(cacheKey.PrimaryKey + "?"); - } - - - // check the response: is this response allowed to be cached? - bool isCacheable = HttpResponseHelpers.CanBeCached(serverResponse); - - if (isCacheable) - { - // add the response to cache - await _cacheStore.SetAsync(cacheKey, new CacheEntry(serverResponse)); - } - - // what about vary by headers (=> key should take this into account)? - - } - return serverResponse; - } - } +using Marvin.HttpCache.Store; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; + +namespace Marvin.HttpCache +{ + public class HttpCacheHandler: DelegatingHandler + { + + private readonly ICacheStore _cacheStore; + + private bool _enableConditionalPut = true; + + private bool _enableConditionalPatch = true; + + private bool _enableClearRelatedResourceRepresentationsAfterPut = true; + + private bool _enableClearRelatedResourceRepresentationsAfterPatch = true; + + private bool _forceRevalidationOfStaleResourceRepresentations = false; + + /// + /// Instantiates the HttpCacheHandler + /// + public HttpCacheHandler() + : this(new ImmutableInMemoryCacheStore(), new HttpCacheHandlerSettings()) + { + } + + /// + /// Instantiates the HttpCacheHandler + /// + /// An instance of an implementation of ICacheStore + public HttpCacheHandler(ICacheStore cacheStore) + : this(cacheStore, new HttpCacheHandlerSettings()) + { + } + + /// + /// Instantiates the HttpCacheHandler + /// + /// An instance of an implementation of IHttpCacheHandlerSettings + public HttpCacheHandler(IHttpCacheHandlerSettings cacheHandlerSettings) + : this(new ImmutableInMemoryCacheStore(), cacheHandlerSettings) + { + } + + + /// + /// Instantiates the HttpCacheHandler + /// + /// An instance of an implementation of ICacheStore + /// An instance of an implementation of IHttpCacheHandlerSettings + public HttpCacheHandler(ICacheStore cacheStore, IHttpCacheHandlerSettings cacheHandlerSettings) + { + if (cacheStore == null) + { + throw new ArgumentNullException("cacheStore", "Provided ICacheStore implementation cannot be null."); + } + + if (cacheHandlerSettings == null) + { + throw new ArgumentNullException("cacheHandlerSettings", "Provided IHttpCacheHandlerSettings implementation cannot be null."); + } + + _cacheStore = cacheStore; + + _forceRevalidationOfStaleResourceRepresentations = cacheHandlerSettings.ForceRevalidationOfStaleResourceRepresentations; + _enableConditionalPatch = cacheHandlerSettings.EnableConditionalPatch; + _enableConditionalPut = cacheHandlerSettings.EnableConditionalPut; + _enableClearRelatedResourceRepresentationsAfterPatch = cacheHandlerSettings.EnableClearRelatedResourceRepresentationsAfterPatch; + _enableClearRelatedResourceRepresentationsAfterPut = cacheHandlerSettings.EnableClearRelatedResourceRepresentationsAfterPut; + + } + + + protected override Task SendAsync(HttpRequestMessage request, + System.Threading.CancellationToken cancellationToken) + { + + if (request.Method == HttpMethod.Put || request.Method.Method.ToLower() == "patch") + { + // PUT - PATCH + return HandleHttpPutOrPatch(request, cancellationToken); + } + else if (request.Method == HttpMethod.Get) + { + // GET + return HandleHttpGet(request, cancellationToken); + } + else + { + // DELETE - POST - OTHERS + return base.SendAsync(request, cancellationToken); + } + + } + + private Task HandleHttpPutOrPatch(HttpRequestMessage request, + System.Threading.CancellationToken cancellationToken) + { + + string primaryCacheKey = CacheKeyHelpers.CreatePrimaryCacheKey(request); + var cacheKey = CacheKeyHelpers.CreateCacheKey(primaryCacheKey); + + // cached + conditional PUT or cached + conditional PATCH + if ((_enableConditionalPut && request.Method == HttpMethod.Put) + || + (_enableConditionalPatch && request.Method.Method.ToLower() == "patch")) + { + + bool addCachingHeaders = false; + HttpResponseMessage responseFromCache = null; + + // available in cache? + var responseFromCacheAsTask = _cacheStore.GetAsync(cacheKey); + if (responseFromCacheAsTask.Result != null) + { + addCachingHeaders = true; + responseFromCache = responseFromCacheAsTask.Result.HttpResponse; + } + + if (addCachingHeaders) + { + // set etag / lastmodified. Both are set for better compatibility + // with different backend caching systems. + if (responseFromCache.Headers.ETag != null) + { + request.Headers.Add(HttpHeaderConstants.IfMatch, + responseFromCache.Headers.ETag.ToString()); + } + + if (responseFromCache.Content.Headers.LastModified != null) + { + request.Headers.Add(HttpHeaderConstants.IfUnmodifiedSince, + responseFromCache.Content.Headers.LastModified.Value.ToString("r")); + } + } + } + + return HandleSendAndContinuationForPutPatch(cacheKey, request, cancellationToken); + } + + + private Task HandleHttpGet(HttpRequestMessage request, + System.Threading.CancellationToken cancellationToken) + { + // get VaryByHeaders - order in the request shouldn't matter, so order them so the + // rest of the logic doesn't result in different keys. + + + string primaryCacheKey = CacheKeyHelpers.CreatePrimaryCacheKey(request);// request.RequestUri.ToString(); + bool responseIsCached = false; + HttpResponseMessage responseFromCache = null; + IEnumerable cacheEntriesFromCache = null; + + // first, before even looking at the cache: + // The Cache-Control: no-cache HTTP/1.1 header field is also intended for use in requests made by the client. + // It is a means for the browser to tell the server and any intermediate caches that it wants a + // fresh version of the resource. + + if (request.Headers.CacheControl != null && request.Headers.CacheControl.NoCache) + { + // Don't get from cache. Get from server. + return HandleSendAndContinuation( + CacheKeyHelpers.CreateCacheKey(primaryCacheKey), request, cancellationToken, false); + } + + + + + // available in cache? + var cacheEntriesFromCacheAsTask = _cacheStore.GetAsync(primaryCacheKey); + if (cacheEntriesFromCacheAsTask.Result != default(IEnumerable)) + { + cacheEntriesFromCache = cacheEntriesFromCacheAsTask.Result; + + // TODO: for all of these, check the varyby headers (secondary key). + // An item is a match if secondary & primary keys both match! + responseFromCache = cacheEntriesFromCache.First().HttpResponse; + responseIsCached = true; + } + + if (responseIsCached) + { + + // set the accompanying request message + responseFromCache.RequestMessage = request; + + // Check conditions that might require us to revalidate/check + + // we must assume "the worst": get from server. + + bool mustRevalidate = HttpResponseHelpers.MustRevalidate(responseFromCache); + + if (mustRevalidate) + { + // we must revalidate - add headers to the request for validation. + // + // we add both ETag & IfModifiedSince for better interop with various + // server-side caching handlers. + // + if (responseFromCache.Headers.ETag != null) + { + request.Headers.Add(HttpHeaderConstants.IfNoneMatch, + responseFromCache.Headers.ETag.ToString()); + } + + if (responseFromCache.Content.Headers.LastModified != null) + { + request.Headers.Add(HttpHeaderConstants.IfModifiedSince, + responseFromCache.Content.Headers.LastModified.Value.ToString("r")); + } + + return HandleSendAndContinuation( + CacheKeyHelpers.CreateCacheKey(primaryCacheKey), request, cancellationToken, true); + } + else + { + // response is allowed to be cached and there's + // no need to revalidate: return the cached response + return Task.FromResult(responseFromCache); + } + } + else + { + // response isn't cached. Get it, and (possibly) add it to cache. + return HandleSendAndContinuation( + CacheKeyHelpers.CreateCacheKey(primaryCacheKey), request, cancellationToken, false); + } + + + } + + + private Task HandleSendAndContinuation(CacheKey cacheKey, HttpRequestMessage request, + System.Threading.CancellationToken cancellationToken, bool mustRevalidate) + { + + return base.SendAsync(request, cancellationToken) + .ContinueWith( + task => + { + + var serverResponse = task.Result; + + // if we had to revalidate & got a 304 returned, that means + // we can get the response message from cache. + if (mustRevalidate && serverResponse.StatusCode == HttpStatusCode.NotModified) + { + var cacheEntry = _cacheStore.GetAsync(cacheKey).Result; + var responseFromCacheEntry = cacheEntry.HttpResponse; + responseFromCacheEntry.RequestMessage = request; + + return responseFromCacheEntry; + } + + if (serverResponse.IsSuccessStatusCode) + { + + // ensure no NULL dates + if (serverResponse.Headers.Date == null) + { + serverResponse.Headers.Date = DateTimeOffset.UtcNow; + } + + // check the response: is this response allowed to be cached? + bool isCacheable = HttpResponseHelpers.CanBeCached(serverResponse); + + if (isCacheable) + { + // add the response to cache + _cacheStore.SetAsync(cacheKey, new CacheEntry(serverResponse)); + } + + + // what about vary by headers (=> key should take this into account)? + + } + + return serverResponse; + }); + } + + + + + private Task HandleSendAndContinuationForPutPatch(CacheKey cacheKey, HttpRequestMessage request, + System.Threading.CancellationToken cancellationToken) + { + + return base.SendAsync(request, cancellationToken) + .ContinueWith( + task => + { + + var serverResponse = task.Result; + + if (serverResponse.IsSuccessStatusCode) + { + + // ensure no NULL dates + if (serverResponse.Headers.Date == null) + { + serverResponse.Headers.Date = DateTimeOffset.UtcNow; + } + + // should we clear? + + if ((_enableClearRelatedResourceRepresentationsAfterPut && request.Method == HttpMethod.Put) + || + (_enableClearRelatedResourceRepresentationsAfterPatch && request.Method.Method.ToLower() == "patch")) + { + // clear related resources + // + // - remove resource with cachekey. This must be done, as there's no + // guarantee the new response is cacheable. + // + // - look for resources in cache that start with + // the cachekey + "?" for querystring. + + _cacheStore.RemoveAsync(cacheKey); + _cacheStore.RemoveRangeAsync(cacheKey.PrimaryKey + "?"); + } + + + // check the response: is this response allowed to be cached? + bool isCacheable = HttpResponseHelpers.CanBeCached(serverResponse); + + if (isCacheable) + { + // add the response to cache + _cacheStore.SetAsync(cacheKey, new CacheEntry(serverResponse)); + } + + // what about vary by headers (=> key should take this into account)? + + } + + return serverResponse; + + }); + } + + } } \ No newline at end of file From 9e1dec2221dd20a5d0552d33ab2099247bd380dc Mon Sep 17 00:00:00 2001 From: nlIsotools Date: Wed, 1 Apr 2015 18:28:44 +0200 Subject: [PATCH 3/3] Bug if cacheStore implementation is really async (need wait task finished before use task result) --- src/Marvin.HttpCache/HttpCacheHandler.cs | 76 ++++++++++-------------- 1 file changed, 31 insertions(+), 45 deletions(-) diff --git a/src/Marvin.HttpCache/HttpCacheHandler.cs b/src/Marvin.HttpCache/HttpCacheHandler.cs index 58fd113..c96b562 100644 --- a/src/Marvin.HttpCache/HttpCacheHandler.cs +++ b/src/Marvin.HttpCache/HttpCacheHandler.cs @@ -101,7 +101,7 @@ protected override Task SendAsync(HttpRequestMessage reques } - private Task HandleHttpPutOrPatch(HttpRequestMessage request, + private async Task HandleHttpPutOrPatch(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { @@ -118,11 +118,11 @@ private Task HandleHttpPutOrPatch(HttpRequestMessage reques HttpResponseMessage responseFromCache = null; // available in cache? - var responseFromCacheAsTask = _cacheStore.GetAsync(cacheKey); - if (responseFromCacheAsTask.Result != null) + var cacheEntry = await _cacheStore.GetAsync(cacheKey); + if (cacheEntry != null) { - addCachingHeaders = true; - responseFromCache = responseFromCacheAsTask.Result.HttpResponse; + addCachingHeaders = true; + responseFromCache = cacheEntry.HttpResponse; } if (addCachingHeaders) @@ -143,11 +143,11 @@ private Task HandleHttpPutOrPatch(HttpRequestMessage reques } } - return HandleSendAndContinuationForPutPatch(cacheKey, request, cancellationToken); + return await HandleSendAndContinuationForPutPatch(cacheKey, request, cancellationToken); } - private Task HandleHttpGet(HttpRequestMessage request, + private async Task HandleHttpGet(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { // get VaryByHeaders - order in the request shouldn't matter, so order them so the @@ -167,19 +167,17 @@ private Task HandleHttpGet(HttpRequestMessage request, if (request.Headers.CacheControl != null && request.Headers.CacheControl.NoCache) { // Don't get from cache. Get from server. - return HandleSendAndContinuation( + return await HandleSendAndContinuation( CacheKeyHelpers.CreateCacheKey(primaryCacheKey), request, cancellationToken, false); } - // available in cache? - var cacheEntriesFromCacheAsTask = _cacheStore.GetAsync(primaryCacheKey); - if (cacheEntriesFromCacheAsTask.Result != default(IEnumerable)) + // available in cache? + cacheEntriesFromCache = await _cacheStore.GetAsync(primaryCacheKey); + if (cacheEntriesFromCache != default(IEnumerable)) { - cacheEntriesFromCache = cacheEntriesFromCacheAsTask.Result; - // TODO: for all of these, check the varyby headers (secondary key). // An item is a match if secondary & primary keys both match! responseFromCache = cacheEntriesFromCache.First().HttpResponse; @@ -217,20 +215,20 @@ private Task HandleHttpGet(HttpRequestMessage request, responseFromCache.Content.Headers.LastModified.Value.ToString("r")); } - return HandleSendAndContinuation( + return await HandleSendAndContinuation( CacheKeyHelpers.CreateCacheKey(primaryCacheKey), request, cancellationToken, true); } else { // response is allowed to be cached and there's // no need to revalidate: return the cached response - return Task.FromResult(responseFromCache); + return responseFromCache; } } else { // response isn't cached. Get it, and (possibly) add it to cache. - return HandleSendAndContinuation( + return await HandleSendAndContinuation( CacheKeyHelpers.CreateCacheKey(primaryCacheKey), request, cancellationToken, false); } @@ -238,22 +236,17 @@ private Task HandleHttpGet(HttpRequestMessage request, } - private Task HandleSendAndContinuation(CacheKey cacheKey, HttpRequestMessage request, + private async Task HandleSendAndContinuation(CacheKey cacheKey, HttpRequestMessage request, System.Threading.CancellationToken cancellationToken, bool mustRevalidate) { - return base.SendAsync(request, cancellationToken) - .ContinueWith( - task => - { - - var serverResponse = task.Result; + var serverResponse = await base.SendAsync(request, cancellationToken); // if we had to revalidate & got a 304 returned, that means // we can get the response message from cache. if (mustRevalidate && serverResponse.StatusCode == HttpStatusCode.NotModified) - { - var cacheEntry = _cacheStore.GetAsync(cacheKey).Result; + { + var cacheEntry = await _cacheStore.GetAsync(cacheKey); var responseFromCacheEntry = cacheEntry.HttpResponse; responseFromCacheEntry.RequestMessage = request; @@ -274,8 +267,8 @@ private Task HandleSendAndContinuation(CacheKey cacheKey, H if (isCacheable) { - // add the response to cache - _cacheStore.SetAsync(cacheKey, new CacheEntry(serverResponse)); + // add the response to cache + await _cacheStore.SetAsync(cacheKey, new CacheEntry(serverResponse)); } @@ -284,22 +277,17 @@ private Task HandleSendAndContinuation(CacheKey cacheKey, H } return serverResponse; - }); + } - private Task HandleSendAndContinuationForPutPatch(CacheKey cacheKey, HttpRequestMessage request, + private async Task HandleSendAndContinuationForPutPatch(CacheKey cacheKey, HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) - { - - return base.SendAsync(request, cancellationToken) - .ContinueWith( - task => - { - - var serverResponse = task.Result; + { + + var serverResponse = await base.SendAsync(request, cancellationToken); if (serverResponse.IsSuccessStatusCode) { @@ -322,10 +310,10 @@ private Task HandleSendAndContinuationForPutPatch(CacheKey // guarantee the new response is cacheable. // // - look for resources in cache that start with - // the cachekey + "?" for querystring. - - _cacheStore.RemoveAsync(cacheKey); - _cacheStore.RemoveRangeAsync(cacheKey.PrimaryKey + "?"); + // the cachekey + "?" for querystring. + + await _cacheStore.RemoveAsync(cacheKey); + await _cacheStore.RemoveRangeAsync(cacheKey.PrimaryKey + "?"); } @@ -334,8 +322,8 @@ private Task HandleSendAndContinuationForPutPatch(CacheKey if (isCacheable) { - // add the response to cache - _cacheStore.SetAsync(cacheKey, new CacheEntry(serverResponse)); + // add the response to cache + await _cacheStore.SetAsync(cacheKey, new CacheEntry(serverResponse)); } // what about vary by headers (=> key should take this into account)? @@ -343,8 +331,6 @@ private Task HandleSendAndContinuationForPutPatch(CacheKey } return serverResponse; - - }); } }