From 85c7e167a1f859aa5daee1a780c66d8456d13378 Mon Sep 17 00:00:00 2001 From: Robert Dailey Date: Sun, 2 May 2021 18:43:17 -0500 Subject: [PATCH] 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. --- src/Directory.Build.targets | 1 + src/Trash.Tests/Cache/ServiceCacheTest.cs | 77 ++++++++++++----------- src/Trash/Cache/ServiceCache.cs | 21 +++++-- src/Trash/Trash.csproj | 1 + 4 files changed, 60 insertions(+), 40 deletions(-) diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index ac4c564d..60a466e4 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -31,5 +31,6 @@ + diff --git a/src/Trash.Tests/Cache/ServiceCacheTest.cs b/src/Trash.Tests/Cache/ServiceCacheTest.cs index 2e0ce080..62b46c7a 100644 --- a/src/Trash.Tests/Cache/ServiceCacheTest.cs +++ b/src/Trash.Tests/Cache/ServiceCacheTest.cs @@ -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(); + StoragePath = Substitute.For(); + ServiceConfig = Substitute.For(); + 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(); - var cache = new ServiceCache(filesystem, storagePath); + var ctx = new Context(new FileSystem()); - Action act = () => cache.Load(); + Action act = () => ctx.Cache.Load(); - act.Should() - .Throw(); + act.Should().Throw(); } [Test] public void Load_WithAttribute_ParsesCorrectly() { - var filesystem = Substitute.For(); - var storagePath = Substitute.For(); - 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()) + ctx.Filesystem.File.ReadAllText(Arg.Any()) .Returns(_ => JsonConvert.SerializeObject(testJson)); - var obj = cache.Load(); + var obj = ctx.Cache.Load(); 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(); - var storagePath = Substitute.For(); - var cache = new ServiceCache(filesystem, storagePath); + var ctx = new Context(); - Action act = () => cache.Load(); + Action act = () => ctx.Cache.Load(); act.Should() .Throw() @@ -80,11 +90,9 @@ namespace Trash.Tests.Cache [Test] public void Load_WithoutAttribute_Throws() { - var filesystem = Substitute.For(); - var storagePath = Substitute.For(); - var cache = new ServiceCache(filesystem, storagePath); + var ctx = new Context(); - Action act = () => cache.Load(); + Action act = () => ctx.Cache.Load(); act.Should() .Throw() @@ -94,28 +102,27 @@ namespace Trash.Tests.Cache [Test] public void Save_WithAttribute_ParsesCorrectly() { - var filesystem = Substitute.For(); - var storagePath = Substitute.For(); - 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(); - var storagePath = Substitute.For(); - 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() @@ -125,11 +132,9 @@ namespace Trash.Tests.Cache [Test] public void Save_WithoutAttribute_Throws() { - var filesystem = Substitute.For(); - var storagePath = Substitute.For(); - 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() diff --git a/src/Trash/Cache/ServiceCache.cs b/src/Trash/Cache/ServiceCache.cs index 6c9402f8..37eae7a4 100644 --- a/src/Trash/Cache/ServiceCache.cs +++ b/src/Trash/Cache/ServiceCache.cs @@ -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() @@ -28,8 +35,9 @@ namespace Trash.Cache public void Save(T obj) { - _fileSystem.File.WriteAllText(PathFromAttribute(), - JsonConvert.SerializeObject(obj, Formatting.Indented)); + var path = PathFromAttribute(); + _fileSystem.Directory.CreateDirectory(Path.GetDirectoryName(path)); + _fileSystem.File.WriteAllText(path, JsonConvert.SerializeObject(obj, Formatting.Indented)); } private static string GetCacheObjectNameAttribute() @@ -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() { var objectName = GetCacheObjectNameAttribute(); @@ -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"); } } } diff --git a/src/Trash/Trash.csproj b/src/Trash/Trash.csproj index 962065ba..5c34f742 100644 --- a/src/Trash/Trash.csproj +++ b/src/Trash/Trash.csproj @@ -16,6 +16,7 @@ +