Skip to content

Commit

Permalink
Implement mapping of HttpResponseMessage to CachedResponse to fix #152 (
Browse files Browse the repository at this point in the history
#153)

* changed name to cache options to fix issue #146

* Add acceptance test that exposes JSON deserialization bug from issue #146

- Create InMemoryJsonHandle for CacheManager that mimics DictionaryHandle but uses ICacheSerializer to serialize/deserialize values instead of saving references
- Add CacheManager.Serialization.Json package
- Add StartupWithCustomCacheHandle class that extends Startup and overrides ConfigureServices to register InMemoryJsonHandle
- Add GivenOcelotIsRunningUsingConsulToStoreConfigAndJsonSerializedCache method to initiate Ocelot with StartupWithCustomCacheHandle
- Add test should_return_response_200_with_simple_url_when_using_jsonserialized_cache

* Create Acceptance test that exposes issue #152

- Add GivenOcelotIsRunningUsingJsonSerializedCache() that initializes Ocelot with InMemoryJsonHandle
- Add should_return_cached_response_when_using_jsonserialized_cache test

* Change Consul port to 9502 on should_return_response_200_with_simple_url_when_using_jsonserialized_cache() test

* Implement mapping of HttpResponseMessage to CachedResponse to enable distributed caching

- Add CachedResponse class that holds HttpResponse data
- Add mapping methods in OutputCacheMiddleware to create HttpResponseMessage from CachedResponse and vice versa
- Replace HttpResponseMessage with CachedResponse in services registrations
- Replace HttpResponseMessage with CachedResponse in OutputCacheController's IOcelotCache

* Fix unit tests for OutputCacheMiddleware and OutputCacheController by replacing HttpResponseMessage with CachedResponse

* Add .editorconfig with default identation settings (spaces with size 4)

* Re-format broken files with new identation settings

* Add Startup_WithConsul_And_CustomCacheHandle class

- Use Startup_WithConsul_And_CustomCacheHandle in GivenOcelotIsRunningUsingConsulToStoreConfigAndJsonSerializedCache step

* Do minor cleanups

- Rename StartupWithCustomCacheHandle to Startup_WithCustomCacheHandle for better readability
- Remove cachemanager settings Action in Startup since it is not used anymore

* Drop Task in CreateHttpResponseMessage - unnecessary overhead

* Make setters private in CachedResponse

- Rework CreateCachedResponse to use new CachedResponse constructor
  • Loading branch information
hampos authored and TomPallister committed Nov 25, 2017
1 parent 3b27bb3 commit 48b5a32
Show file tree
Hide file tree
Showing 15 changed files with 401 additions and 46 deletions.
9 changes: 9 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
root = true

[*]
end_of_line = crlf
insert_final_newline = true

[*.cs]
indent_style = space
indent_size = 4
25 changes: 25 additions & 0 deletions src/Ocelot/Cache/CachedResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.Collections.Generic;
using System.Net;

namespace Ocelot.Cache
{
public class CachedResponse
{
public CachedResponse(
HttpStatusCode statusCode = HttpStatusCode.OK,
Dictionary<string, IEnumerable<string>> headers = null,
string body = null
)
{
StatusCode = statusCode;
Headers = headers ?? new Dictionary<string, IEnumerable<string>>();
Body = body ?? "";
}

public HttpStatusCode StatusCode { get; private set; }

public Dictionary<string, IEnumerable<string>> Headers { get; private set; }

public string Body { get; private set; }
}
}
55 changes: 48 additions & 7 deletions src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,23 @@
using Ocelot.Infrastructure.RequestData;
using Ocelot.Logging;
using Ocelot.Middleware;
using System.IO;

namespace Ocelot.Cache.Middleware
{
public class OutputCacheMiddleware : OcelotMiddleware
{
private readonly RequestDelegate _next;
private readonly IOcelotLogger _logger;
private readonly IOcelotCache<HttpResponseMessage> _outputCache;
private readonly IOcelotCache<CachedResponse> _outputCache;
private readonly IRegionCreator _regionCreator;

public OutputCacheMiddleware(RequestDelegate next,
IOcelotLoggerFactory loggerFactory,
IRequestScopedDataRepository scopedDataRepository,
IOcelotCache<HttpResponseMessage> outputCache,
IOcelotCache<CachedResponse> outputCache,
IRegionCreator regionCreator)
:base(scopedDataRepository)
: base(scopedDataRepository)
{
_next = next;
_outputCache = outputCache;
Expand All @@ -40,14 +41,15 @@ public async Task Invoke(HttpContext context)
var downstreamUrlKey = $"{DownstreamRequest.Method.Method}-{DownstreamRequest.RequestUri.OriginalString}";

_logger.LogDebug("started checking cache for {downstreamUrlKey}", downstreamUrlKey);

var cached = _outputCache.Get(downstreamUrlKey, DownstreamRoute.ReRoute.CacheOptions.Region);

if (cached != null)
{
_logger.LogDebug("cache entry exists for {downstreamUrlKey}", downstreamUrlKey);

SetHttpResponseMessageThisRequest(cached);
var response = CreateHttpResponseMessage(cached);
SetHttpResponseMessageThisRequest(response);

_logger.LogDebug("finished returned cached response for {downstreamUrlKey}", downstreamUrlKey);

Expand All @@ -65,11 +67,50 @@ public async Task Invoke(HttpContext context)
return;
}

var response = HttpResponseMessage;
cached = await CreateCachedResponse(HttpResponseMessage);

_outputCache.Add(downstreamUrlKey, response, TimeSpan.FromSeconds(DownstreamRoute.ReRoute.CacheOptions.TtlSeconds), DownstreamRoute.ReRoute.CacheOptions.Region);
_outputCache.Add(downstreamUrlKey, cached, TimeSpan.FromSeconds(DownstreamRoute.ReRoute.CacheOptions.TtlSeconds), DownstreamRoute.ReRoute.CacheOptions.Region);

_logger.LogDebug("finished response added to cache for {downstreamUrlKey}", downstreamUrlKey);
}

internal HttpResponseMessage CreateHttpResponseMessage(CachedResponse cached)
{
if (cached == null)
{
return null;
}

var response = new HttpResponseMessage(cached.StatusCode);
foreach (var header in cached.Headers)
{
response.Headers.Add(header.Key, header.Value);
}
var content = new MemoryStream(Convert.FromBase64String(cached.Body));
response.Content = new StreamContent(content);

return response;
}

internal async Task<CachedResponse> CreateCachedResponse(HttpResponseMessage response)
{
if (response == null)
{
return null;
}

var statusCode = response.StatusCode;
var headers = response.Headers.ToDictionary(v => v.Key, v => v.Value);
string body = null;

if (response.Content != null)
{
var content = await response.Content.ReadAsByteArrayAsync();
body = Convert.ToBase64String(content);
}

var cached = new CachedResponse(statusCode, headers, body);
return cached;
}
}
}
4 changes: 2 additions & 2 deletions src/Ocelot/Configuration/ReRoute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public ReRoute(PathTemplate downstreamPathTemplate,
List<ClaimToThing> claimsToQueries,
string requestIdKey,
bool isCached,
CacheOptions fileCacheOptions,
CacheOptions cacheOptions,
string downstreamScheme,
string loadBalancer,
string downstreamHost,
Expand Down Expand Up @@ -49,7 +49,7 @@ public ReRoute(PathTemplate downstreamPathTemplate,
IsAuthorised = isAuthorised;
RequestIdKey = requestIdKey;
IsCached = isCached;
CacheOptions = fileCacheOptions;
CacheOptions = cacheOptions;
ClaimsToQueries = claimsToQueries
?? new List<ClaimToThing>();
ClaimsToClaims = claimsToClaims
Expand Down
4 changes: 2 additions & 2 deletions src/Ocelot/Controllers/OutputCacheController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ namespace Ocelot.Controllers
[Route("outputcache")]
public class OutputCacheController : Controller
{
private IOcelotCache<HttpResponseMessage> _cache;
private IOcelotCache<CachedResponse> _cache;

public OutputCacheController(IOcelotCache<HttpResponseMessage> cache)
public OutputCacheController(IOcelotCache<CachedResponse> cache)
{
_cache = cache;
}
Expand Down
12 changes: 6 additions & 6 deletions src/Ocelot/DependencyInjection/OcelotBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -161,13 +161,13 @@ public IOcelotBuilder AddStoreOcelotConfigurationInConsul()

public IOcelotBuilder AddCacheManager(Action<ConfigurationBuilderCachePart> settings)
{
var cacheManagerOutputCache = CacheFactory.Build<HttpResponseMessage>("OcelotOutputCache", settings);
var ocelotOutputCacheManager = new OcelotCacheManagerCache<HttpResponseMessage>(cacheManagerOutputCache);
var cacheManagerOutputCache = CacheFactory.Build<CachedResponse>("OcelotOutputCache", settings);
var ocelotOutputCacheManager = new OcelotCacheManagerCache<CachedResponse>(cacheManagerOutputCache);

_services.RemoveAll(typeof(ICacheManager<HttpResponseMessage>));
_services.RemoveAll(typeof(IOcelotCache<HttpResponseMessage>));
_services.AddSingleton<ICacheManager<HttpResponseMessage>>(cacheManagerOutputCache);
_services.AddSingleton<IOcelotCache<HttpResponseMessage>>(ocelotOutputCacheManager);
_services.RemoveAll(typeof(ICacheManager<CachedResponse>));
_services.RemoveAll(typeof(IOcelotCache<CachedResponse>));
_services.AddSingleton<ICacheManager<CachedResponse>>(cacheManagerOutputCache);
_services.AddSingleton<IOcelotCache<CachedResponse>>(ocelotOutputCacheManager);

var ocelotConfigCacheManagerOutputCache = CacheFactory.Build<IOcelotConfiguration>("OcelotConfigurationCache", settings);
var ocelotConfigCacheManager = new OcelotCacheManagerCache<IOcelotConfiguration>(ocelotConfigCacheManagerOutputCache);
Expand Down
137 changes: 137 additions & 0 deletions test/Ocelot.AcceptanceTests/Caching/InMemoryJsonHandle.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
using CacheManager.Core;
using CacheManager.Core.Internal;
using CacheManager.Core.Logging;
using System;
using System.Collections.Concurrent;
using System.Linq;
using static CacheManager.Core.Utility.Guard;

namespace Ocelot.AcceptanceTests.Caching
{
public class InMemoryJsonHandle<TCacheValue> : BaseCacheHandle<TCacheValue>
{
private readonly ICacheSerializer _serializer;
private readonly ConcurrentDictionary<string, Tuple<Type, byte[]>> _cache;

public InMemoryJsonHandle(
ICacheManagerConfiguration managerConfiguration,
CacheHandleConfiguration configuration,
ICacheSerializer serializer,
ILoggerFactory loggerFactory) : base(managerConfiguration, configuration)
{
_cache = new ConcurrentDictionary<string, Tuple<Type, byte[]>>();
_serializer = serializer;
Logger = loggerFactory.CreateLogger(this);
}

public override int Count => _cache.Count;

protected override ILogger Logger { get; }

public override void Clear() => _cache.Clear();

public override void ClearRegion(string region)
{
NotNullOrWhiteSpace(region, nameof(region));

var key = string.Concat(region, ":");
foreach (var item in _cache.Where(p => p.Key.StartsWith(key, StringComparison.OrdinalIgnoreCase)))
{
_cache.TryRemove(item.Key, out Tuple<Type, byte[]> val);
}
}

public override bool Exists(string key)
{
NotNullOrWhiteSpace(key, nameof(key));

return _cache.ContainsKey(key);
}

public override bool Exists(string key, string region)
{
NotNullOrWhiteSpace(region, nameof(region));
var fullKey = GetKey(key, region);
return _cache.ContainsKey(fullKey);
}

protected override bool AddInternalPrepared(CacheItem<TCacheValue> item)
{
NotNull(item, nameof(item));

var key = GetKey(item.Key, item.Region);

var serializedItem = _serializer.SerializeCacheItem(item);

return _cache.TryAdd(key, new Tuple<Type, byte[]>(item.Value.GetType(), serializedItem));
}

protected override CacheItem<TCacheValue> GetCacheItemInternal(string key) => GetCacheItemInternal(key, null);

protected override CacheItem<TCacheValue> GetCacheItemInternal(string key, string region)
{
var fullKey = GetKey(key, region);

CacheItem<TCacheValue> deserializedResult = null;

if (_cache.TryGetValue(fullKey, out Tuple<Type, byte[]> result))
{
deserializedResult = _serializer.DeserializeCacheItem<TCacheValue>(result.Item2, result.Item1);

if (deserializedResult.ExpirationMode != ExpirationMode.None && IsExpired(deserializedResult, DateTime.UtcNow))
{
_cache.TryRemove(fullKey, out Tuple<Type, byte[]> removeResult);
TriggerCacheSpecificRemove(key, region, CacheItemRemovedReason.Expired, deserializedResult.Value);
return null;
}
}

return deserializedResult;
}

protected override void PutInternalPrepared(CacheItem<TCacheValue> item)
{
NotNull(item, nameof(item));

var serializedItem = _serializer.SerializeCacheItem<TCacheValue>(item);

_cache[GetKey(item.Key, item.Region)] = new Tuple<Type, byte[]>(item.Value.GetType(), serializedItem);
}

protected override bool RemoveInternal(string key) => RemoveInternal(key, null);

protected override bool RemoveInternal(string key, string region)
{
var fullKey = GetKey(key, region);
return _cache.TryRemove(fullKey, out Tuple<Type, byte[]> val);
}

private static string GetKey(string key, string region)
{
NotNullOrWhiteSpace(key, nameof(key));

if (string.IsNullOrWhiteSpace(region))
{
return key;
}

return string.Concat(region, ":", key);
}

private static bool IsExpired(CacheItem<TCacheValue> item, DateTime now)
{
if (item.ExpirationMode == ExpirationMode.Absolute
&& item.CreatedUtc.Add(item.ExpirationTimeout) < now)
{
return true;
}
else if (item.ExpirationMode == ExpirationMode.Sliding
&& item.LastAccessedUtc.Add(item.ExpirationTimeout) < now)
{
return true;
}

return false;
}
}
}
36 changes: 36 additions & 0 deletions test/Ocelot.AcceptanceTests/CachingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,42 @@ public void should_return_cached_response()
.BDDfy();
}

[Fact]
public void should_return_cached_response_when_using_jsonserialized_cache()
{
var configuration = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/",
DownstreamPort = 51879,
DownstreamScheme = "http",
DownstreamHost = "localhost",
UpstreamPathTemplate = "/",
UpstreamHttpMethod = new List<string> { "Get" },
FileCacheOptions = new FileCacheOptions
{
TtlSeconds = 100
}
}
}
};

this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", 200, "Hello from Laura"))
.And(x => _steps.GivenThereIsAConfiguration(configuration))
.And(x => _steps.GivenOcelotIsRunningUsingJsonSerializedCache())
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
.Given(x => x.GivenTheServiceNowReturns("http://localhost:51879", 200, "Hello from Tom"))
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
.BDDfy();
}

[Fact]
public void should_not_return_cached_response_as_ttl_expires()
{
Expand Down
Loading

0 comments on commit 48b5a32

Please sign in to comment.