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 3 years ago
parent bb07aec749
commit 85c7e167a1

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

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

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

Loading…
Cancel
Save