Implement mapping of HttpResponseMessage to CachedResponse to fix #152 (#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
This commit is contained in:
Charalampos Chomenidis
2017-11-25 16:47:17 +02:00
committed by Tom Pallister
parent 3b27bb376e
commit 48b5a32676
15 changed files with 401 additions and 46 deletions

View File

@ -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;
}
}
}

View File

@ -58,6 +58,42 @@ namespace Ocelot.AcceptanceTests
.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()
{

View File

@ -68,6 +68,45 @@ namespace Ocelot.AcceptanceTests
.BDDfy();
}
[Fact]
public void should_return_response_200_with_simple_url_when_using_jsonserialized_cache()
{
var configuration = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/",
DownstreamScheme = "http",
DownstreamHost = "localhost",
DownstreamPort = 51779,
UpstreamPathTemplate = "/",
UpstreamHttpMethod = new List<string> { "Get" },
}
},
GlobalConfiguration = new FileGlobalConfiguration()
{
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider()
{
Host = "localhost",
Port = 9502
}
}
};
var fakeConsulServiceDiscoveryUrl = "http://localhost:9502";
this.Given(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl))
.And(x => x.GivenThereIsAServiceRunningOn("http://localhost:51779", "", 200, "Hello from Laura"))
.And(x => _steps.GivenThereIsAConfiguration(configuration))
.And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfigAndJsonSerializedCache())
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
.BDDfy();
}
[Fact]
public void should_load_configuration_out_of_consul()
{
@ -236,7 +275,7 @@ namespace Ocelot.AcceptanceTests
var kvp = new FakeConsulGetResponse(base64);
await context.Response.WriteJsonAsync(new FakeConsulGetResponse[]{kvp});
await context.Response.WriteJsonAsync(new FakeConsulGetResponse[] { kvp });
}
else if (context.Request.Method.ToLower() == "put" && context.Request.Path.Value == "/v1/kv/OcelotConfiguration")
@ -311,4 +350,4 @@ namespace Ocelot.AcceptanceTests
_steps.Dispose();
}
}
}
}

View File

@ -30,6 +30,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="CacheManager.Serialization.Json" Version="1.1.1" />
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0-preview-20171031-01" />

View File

@ -8,6 +8,7 @@ using Microsoft.Extensions.Logging;
using Ocelot.DependencyInjection;
using Ocelot.Middleware;
using ConfigurationBuilder = Microsoft.Extensions.Configuration.ConfigurationBuilder;
using Ocelot.AcceptanceTests.Caching;
namespace Ocelot.AcceptanceTests
{
@ -27,13 +28,8 @@ namespace Ocelot.AcceptanceTests
public IConfigurationRoot Configuration { get; }
public void ConfigureServices(IServiceCollection services)
public virtual void ConfigureServices(IServiceCollection services)
{
Action<ConfigurationBuilderCachePart> settings = (x) =>
{
x.WithDictionaryHandle();
};
services.AddOcelot(Configuration);
}
@ -44,4 +40,43 @@ namespace Ocelot.AcceptanceTests
app.UseOcelot().Wait();
}
}
public class Startup_WithCustomCacheHandle : Startup
{
public Startup_WithCustomCacheHandle(IHostingEnvironment env) : base(env) { }
public override void ConfigureServices(IServiceCollection services)
{
services.AddOcelot(Configuration)
.AddCacheManager((x) =>
{
x.WithMicrosoftLogging(log =>
{
log.AddConsole(LogLevel.Debug);
})
.WithJsonSerializer()
.WithHandle(typeof(InMemoryJsonHandle<>));
});
}
}
public class Startup_WithConsul_And_CustomCacheHandle : Startup
{
public Startup_WithConsul_And_CustomCacheHandle(IHostingEnvironment env) : base(env) { }
public override void ConfigureServices(IServiceCollection services)
{
services.AddOcelot(Configuration)
.AddCacheManager((x) =>
{
x.WithMicrosoftLogging(log =>
{
log.AddConsole(LogLevel.Debug);
})
.WithJsonSerializer()
.WithHandle(typeof(InMemoryJsonHandle<>));
})
.AddStoreOcelotConfigurationInConsul();
}
}
}

View File

@ -23,6 +23,7 @@ using Ocelot.Middleware;
using Ocelot.ServiceDiscovery;
using Shouldly;
using ConfigurationBuilder = Microsoft.Extensions.Configuration.ConfigurationBuilder;
using Ocelot.AcceptanceTests.Caching;
namespace Ocelot.AcceptanceTests
{
@ -74,9 +75,9 @@ namespace Ocelot.AcceptanceTests
/// </summary>
public void GivenOcelotIsRunning()
{
_webHostBuilder = new WebHostBuilder();
_webHostBuilder.ConfigureServices(s =>
_webHostBuilder = new WebHostBuilder();
_webHostBuilder.ConfigureServices(s =>
{
s.AddSingleton(_webHostBuilder);
});
@ -107,6 +108,21 @@ namespace Ocelot.AcceptanceTests
_ocelotClient = _ocelotServer.CreateClient();
}
public void GivenOcelotIsRunningUsingJsonSerializedCache()
{
_webHostBuilder = new WebHostBuilder();
_webHostBuilder.ConfigureServices(s =>
{
s.AddSingleton(_webHostBuilder);
});
_ocelotServer = new TestServer(_webHostBuilder
.UseStartup<Startup_WithCustomCacheHandle>());
_ocelotClient = _ocelotServer.CreateClient();
}
public void GivenOcelotIsRunningUsingConsulToStoreConfig()
{
_webHostBuilder = new WebHostBuilder();
@ -122,16 +138,31 @@ namespace Ocelot.AcceptanceTests
_ocelotClient = _ocelotServer.CreateClient();
}
public void GivenOcelotIsRunningUsingConsulToStoreConfigAndJsonSerializedCache()
{
_webHostBuilder = new WebHostBuilder();
_webHostBuilder.ConfigureServices(s =>
{
s.AddSingleton(_webHostBuilder);
});
_ocelotServer = new TestServer(_webHostBuilder
.UseStartup<Startup_WithConsul_And_CustomCacheHandle>());
_ocelotClient = _ocelotServer.CreateClient();
}
internal void ThenTheResponseShouldBe(FileConfiguration expected)
{
var response = JsonConvert.DeserializeObject<FileConfiguration>(_response.Content.ReadAsStringAsync().Result);
response.GlobalConfiguration.AdministrationPath.ShouldBe(expected.GlobalConfiguration.AdministrationPath);
response.GlobalConfiguration.RequestIdKey.ShouldBe(expected.GlobalConfiguration.RequestIdKey);
response.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expected.GlobalConfiguration.ServiceDiscoveryProvider.Host);
response.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(expected.GlobalConfiguration.ServiceDiscoveryProvider.Port);
for(var i = 0; i < response.ReRoutes.Count; i++)
for (var i = 0; i < response.ReRoutes.Count; i++)
{
response.ReRoutes[i].DownstreamHost.ShouldBe(expected.ReRoutes[i].DownstreamHost);
response.ReRoutes[i].DownstreamPathTemplate.ShouldBe(expected.ReRoutes[i].DownstreamPathTemplate);
@ -155,7 +186,7 @@ namespace Ocelot.AcceptanceTests
var configuration = builder.Build();
_webHostBuilder = new WebHostBuilder();
_webHostBuilder.ConfigureServices(s =>
_webHostBuilder.ConfigureServices(s =>
{
s.AddSingleton(_webHostBuilder);
});
@ -172,7 +203,7 @@ namespace Ocelot.AcceptanceTests
})
.WithDictionaryHandle();
};
s.AddOcelot(configuration);
})
.ConfigureLogging(l =>
@ -299,12 +330,12 @@ namespace Ocelot.AcceptanceTests
public void WhenIGetUrlOnTheApiGatewayMultipleTimes(string url, int times)
{
var tasks = new Task[times];
for (int i = 0; i < times; i++)
{
var urlCopy = url;
tasks[i] = GetForServiceDiscoveryTest(urlCopy);
Thread.Sleep(_random.Next(40,60));
Thread.Sleep(_random.Next(40, 60));
}
Task.WaitAll(tasks);
@ -327,7 +358,7 @@ namespace Ocelot.AcceptanceTests
request.Headers.Add("ClientId", clientId);
_response = _ocelotClient.SendAsync(request).Result;
}
}
}
public void WhenIGetUrlOnTheApiGateway(string url, string requestId)
{

View File

@ -19,12 +19,12 @@
public class OutputCacheMiddlewareTests : ServerHostedMiddlewareTest
{
private readonly Mock<IOcelotCache<HttpResponseMessage>> _cacheManager;
private HttpResponseMessage _response;
private readonly Mock<IOcelotCache<CachedResponse>> _cacheManager;
private CachedResponse _response;
public OutputCacheMiddlewareTests()
{
_cacheManager = new Mock<IOcelotCache<HttpResponseMessage>>();
_cacheManager = new Mock<IOcelotCache<CachedResponse>>();
ScopedRepository
.Setup(sr => sr.Get<HttpRequestMessage>("DownstreamRequest"))
@ -36,7 +36,8 @@
[Fact]
public void should_returned_cached_item_when_it_is_in_cache()
{
this.Given(x => x.GivenThereIsACachedResponse(new HttpResponseMessage()))
var cachedResponse = new CachedResponse();
this.Given(x => x.GivenThereIsACachedResponse(cachedResponse))
.And(x => x.GivenTheDownstreamRouteIs())
.And(x => x.GivenThereIsADownstreamUrl())
.When(x => x.WhenICallTheMiddleware())
@ -70,7 +71,7 @@
app.UseOutputCacheMiddleware();
}
private void GivenThereIsACachedResponse(HttpResponseMessage response)
private void GivenThereIsACachedResponse(CachedResponse response)
{
_response = response;
_cacheManager
@ -123,7 +124,7 @@
private void ThenTheCacheAddIsCalledCorrectly()
{
_cacheManager
.Verify(x => x.Add(It.IsAny<string>(), It.IsAny<HttpResponseMessage>(), It.IsAny<TimeSpan>(), It.IsAny<string>()), Times.Once);
.Verify(x => x.Add(It.IsAny<string>(), It.IsAny<CachedResponse>(), It.IsAny<TimeSpan>(), It.IsAny<string>()), Times.Once);
}
}
}

View File

@ -14,12 +14,12 @@ namespace Ocelot.UnitTests.Controllers
public class OutputCacheControllerTests
{
private OutputCacheController _controller;
private Mock<IOcelotCache<HttpResponseMessage>> _cache;
private Mock<IOcelotCache<CachedResponse>> _cache;
private IActionResult _result;
public OutputCacheControllerTests()
{
_cache = new Mock<IOcelotCache<HttpResponseMessage>>();
_cache = new Mock<IOcelotCache<CachedResponse>>();
_controller = new OutputCacheController(_cache.Object);
}

View File

@ -72,9 +72,9 @@ namespace Ocelot.UnitTests.DependencyInjection
private void OnlyOneVersionOfEachCacheIsRegistered()
{
var outputCache = _services.Single(x => x.ServiceType == typeof(IOcelotCache<HttpResponseMessage>));
var outputCacheManager = _services.Single(x => x.ServiceType == typeof(ICacheManager<HttpResponseMessage>));
var thing = (CacheManager.Core.ICacheManager<System.Net.Http.HttpResponseMessage>)outputCacheManager.ImplementationInstance;
var outputCache = _services.Single(x => x.ServiceType == typeof(IOcelotCache<CachedResponse>));
var outputCacheManager = _services.Single(x => x.ServiceType == typeof(ICacheManager<CachedResponse>));
var thing = (CacheManager.Core.ICacheManager<CachedResponse>)outputCacheManager.ImplementationInstance;
thing.Configuration.MaxRetries.ShouldBe(_maxRetries);
var ocelotConfigCache = _services.Single(x => x.ServiceType == typeof(IOcelotCache<IOcelotConfiguration>));