refactor: service cache per-service

A FNV1a hash is generated for the `base_uri` value in each service
config. This hash is used to separate cache files between different
instances.
recyclarr
Robert Dailey 4 years ago
parent bb07aec749
commit 85c7e167a1

@ -31,5 +31,6 @@
<PackageReference Update="Serilog" Version="2.*" /> <PackageReference Update="Serilog" Version="2.*" />
<PackageReference Update="System.IO.Abstractions" Version="13.*" /> <PackageReference Update="System.IO.Abstractions" Version="13.*" />
<PackageReference Update="YamlDotNet" Version="10.*" /> <PackageReference Update="YamlDotNet" Version="10.*" />
<PackageReference Update="System.Data.HashFunction.FNV" Version="2.*" />
</ItemGroup> </ItemGroup>
</Project> </Project>

@ -6,6 +6,7 @@ using Newtonsoft.Json;
using NSubstitute; using NSubstitute;
using NUnit.Framework; using NUnit.Framework;
using Trash.Cache; using Trash.Cache;
using Trash.Config;
namespace Trash.Tests.Cache namespace Trash.Tests.Cache
{ {
@ -13,6 +14,22 @@ namespace Trash.Tests.Cache
[Parallelizable(ParallelScope.All)] [Parallelizable(ParallelScope.All)]
public class ServiceCacheTest public class ServiceCacheTest
{ {
private class Context
{
public Context(IFileSystem? fs = null)
{
Filesystem = fs ?? Substitute.For<IFileSystem>();
StoragePath = Substitute.For<ICacheStoragePath>();
ServiceConfig = Substitute.For<IServiceConfiguration>();
Cache = new ServiceCache(Filesystem, StoragePath, ServiceConfig);
}
public ServiceCache Cache { get; }
public IServiceConfiguration ServiceConfig { get; }
public ICacheStoragePath StoragePath { get; }
public IFileSystem Filesystem { get; }
}
private class ObjectWithoutAttribute private class ObjectWithoutAttribute
{ {
} }
@ -34,43 +51,36 @@ namespace Trash.Tests.Cache
public void Load_NoFileExists_ThrowsException() public void Load_NoFileExists_ThrowsException()
{ {
// use a real filesystem to test no file existing // use a real filesystem to test no file existing
var filesystem = new FileSystem(); var ctx = new Context(new FileSystem());
var storagePath = Substitute.For<ICacheStoragePath>();
var cache = new ServiceCache(filesystem, storagePath);
Action act = () => cache.Load<ObjectWithAttribute>(); Action act = () => ctx.Cache.Load<ObjectWithAttribute>();
act.Should() act.Should().Throw<Exception>();
.Throw<FileNotFoundException>();
} }
[Test] [Test]
public void Load_WithAttribute_ParsesCorrectly() public void Load_WithAttribute_ParsesCorrectly()
{ {
var filesystem = Substitute.For<IFileSystem>(); var ctx = new Context();
var storagePath = Substitute.For<ICacheStoragePath>();
var cache = new ServiceCache(filesystem, storagePath);
storagePath.Path.Returns("testpath"); ctx.StoragePath.Path.Returns("testpath");
dynamic testJson = new {TestValue = "Foo"}; dynamic testJson = new {TestValue = "Foo"};
filesystem.File.ReadAllText(Arg.Any<string>()) ctx.Filesystem.File.ReadAllText(Arg.Any<string>())
.Returns(_ => JsonConvert.SerializeObject(testJson)); .Returns(_ => JsonConvert.SerializeObject(testJson));
var obj = cache.Load<ObjectWithAttribute>(); var obj = ctx.Cache.Load<ObjectWithAttribute>();
obj.TestValue.Should().Be("Foo"); obj.TestValue.Should().Be("Foo");
filesystem.File.Received().ReadAllText($"testpath{Path.DirectorySeparatorChar}{ValidObjectName}.json"); ctx.Filesystem.File.Received().ReadAllText(Path.Join("testpath", "c59d1c81", $"{ValidObjectName}.json"));
} }
[Test] [Test]
public void Load_WithAttributeInvalidName_ThrowsException() public void Load_WithAttributeInvalidName_ThrowsException()
{ {
var filesystem = Substitute.For<IFileSystem>(); var ctx = new Context();
var storagePath = Substitute.For<ICacheStoragePath>();
var cache = new ServiceCache(filesystem, storagePath);
Action act = () => cache.Load<ObjectWithAttributeInvalidChars>(); Action act = () => ctx.Cache.Load<ObjectWithAttributeInvalidChars>();
act.Should() act.Should()
.Throw<ArgumentException>() .Throw<ArgumentException>()
@ -80,11 +90,9 @@ namespace Trash.Tests.Cache
[Test] [Test]
public void Load_WithoutAttribute_Throws() public void Load_WithoutAttribute_Throws()
{ {
var filesystem = Substitute.For<IFileSystem>(); var ctx = new Context();
var storagePath = Substitute.For<ICacheStoragePath>();
var cache = new ServiceCache(filesystem, storagePath);
Action act = () => cache.Load<ObjectWithoutAttribute>(); Action act = () => ctx.Cache.Load<ObjectWithoutAttribute>();
act.Should() act.Should()
.Throw<ArgumentException>() .Throw<ArgumentException>()
@ -94,28 +102,27 @@ namespace Trash.Tests.Cache
[Test] [Test]
public void Save_WithAttribute_ParsesCorrectly() public void Save_WithAttribute_ParsesCorrectly()
{ {
var filesystem = Substitute.For<IFileSystem>(); var ctx = new Context();
var storagePath = Substitute.For<ICacheStoragePath>();
var cache = new ServiceCache(filesystem, storagePath); ctx.StoragePath.Path.Returns("testpath");
storagePath.Path.Returns("testpath"); ctx.Cache.Save(new ObjectWithAttribute {TestValue = "Foo"});
cache.Save(new ObjectWithAttribute {TestValue = "Foo"}); var expectedParentDirectory = Path.Join("testpath", "c59d1c81");
ctx.Filesystem.Directory.Received().CreateDirectory(expectedParentDirectory);
dynamic expectedJson = new {TestValue = "Foo"}; dynamic expectedJson = new {TestValue = "Foo"};
var expectedPath = $"testpath{Path.DirectorySeparatorChar}{ValidObjectName}.json"; var expectedPath = Path.Join(expectedParentDirectory, $"{ValidObjectName}.json");
filesystem.File.Received() ctx.Filesystem.File.Received()
.WriteAllText(expectedPath, JsonConvert.SerializeObject(expectedJson, Formatting.Indented)); .WriteAllText(expectedPath, JsonConvert.SerializeObject(expectedJson, Formatting.Indented));
} }
[Test] [Test]
public void Save_WithAttributeInvalidName_ThrowsException() public void Save_WithAttributeInvalidName_ThrowsException()
{ {
var filesystem = Substitute.For<IFileSystem>(); var ctx = new Context();
var storagePath = Substitute.For<ICacheStoragePath>();
var cache = new ServiceCache(filesystem, storagePath);
Action act = () => cache.Save(new ObjectWithAttributeInvalidChars()); Action act = () => ctx.Cache.Save(new ObjectWithAttributeInvalidChars());
act.Should() act.Should()
.Throw<ArgumentException>() .Throw<ArgumentException>()
@ -125,11 +132,9 @@ namespace Trash.Tests.Cache
[Test] [Test]
public void Save_WithoutAttribute_Throws() public void Save_WithoutAttribute_Throws()
{ {
var filesystem = Substitute.For<IFileSystem>(); var ctx = new Context();
var storagePath = Substitute.For<ICacheStoragePath>();
var cache = new ServiceCache(filesystem, storagePath);
Action act = () => cache.Save(new ObjectWithoutAttribute()); Action act = () => ctx.Cache.Save(new ObjectWithoutAttribute());
act.Should() act.Should()
.Throw<ArgumentException>() .Throw<ArgumentException>()

@ -1,23 +1,30 @@
using System; using System;
using System.Data.HashFunction.FNV;
using System.IO; using System.IO;
using System.IO.Abstractions; using System.IO.Abstractions;
using System.Reflection; using System.Reflection;
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Trash.Config;
namespace Trash.Cache namespace Trash.Cache
{ {
public class ServiceCache : IServiceCache public class ServiceCache : IServiceCache
{ {
private static readonly Regex AllowedObjectNameCharacters = new(@"^\w+$", RegexOptions.Compiled); private static readonly Regex AllowedObjectNameCharacters = new(@"^\w+$", RegexOptions.Compiled);
private readonly IServiceConfiguration _config;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly IFNV1a _hash;
private readonly ICacheStoragePath _storagePath; private readonly ICacheStoragePath _storagePath;
public ServiceCache(IFileSystem fileSystem, ICacheStoragePath storagePath) public ServiceCache(IFileSystem fileSystem, ICacheStoragePath storagePath, IServiceConfiguration config)
{ {
_fileSystem = fileSystem; _fileSystem = fileSystem;
_storagePath = storagePath; _storagePath = storagePath;
_config = config;
_hash = FNV1aFactory.Instance.Create(FNVConfig.GetPredefinedConfig(32));
} }
public T Load<T>() public T Load<T>()
@ -28,8 +35,9 @@ namespace Trash.Cache
public void Save<T>(T obj) public void Save<T>(T obj)
{ {
_fileSystem.File.WriteAllText(PathFromAttribute<T>(), var path = PathFromAttribute<T>();
JsonConvert.SerializeObject(obj, Formatting.Indented)); _fileSystem.Directory.CreateDirectory(Path.GetDirectoryName(path));
_fileSystem.File.WriteAllText(path, JsonConvert.SerializeObject(obj, Formatting.Indented));
} }
private static string GetCacheObjectNameAttribute<T>() private static string GetCacheObjectNameAttribute<T>()
@ -43,6 +51,11 @@ namespace Trash.Cache
return attribute.Name; return attribute.Name;
} }
private string BuildServiceGuid()
{
return _hash.ComputeHash(Encoding.ASCII.GetBytes(_config.BaseUrl)).AsHexString();
}
private string PathFromAttribute<T>() private string PathFromAttribute<T>()
{ {
var objectName = GetCacheObjectNameAttribute<T>(); var objectName = GetCacheObjectNameAttribute<T>();
@ -51,7 +64,7 @@ namespace Trash.Cache
throw new ArgumentException($"Object name '{objectName}' has unacceptable characters"); throw new ArgumentException($"Object name '{objectName}' has unacceptable characters");
} }
return Path.Join(_storagePath.Path, objectName + ".json"); return Path.Join(_storagePath.Path, BuildServiceGuid(), objectName + ".json");
} }
} }
} }

@ -16,6 +16,7 @@
<PackageReference Include="Autofac.Extensions.DependencyInjection" /> <PackageReference Include="Autofac.Extensions.DependencyInjection" />
<PackageReference Include="YamlDotNet" /> <PackageReference Include="YamlDotNet" />
<PackageReference Include="System.IO.Abstractions" /> <PackageReference Include="System.IO.Abstractions" />
<PackageReference Include="System.Data.HashFunction.FNV" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

Loading…
Cancel
Save