diff --git a/benchmarks/CacheTower.Benchmarks/Providers/CacheLayerComparisonBenchmark.cs b/benchmarks/CacheTower.Benchmarks/Providers/CacheLayerComparisonBenchmark.cs
index acb033e0..7b953878 100644
--- a/benchmarks/CacheTower.Benchmarks/Providers/CacheLayerComparisonBenchmark.cs
+++ b/benchmarks/CacheTower.Benchmarks/Providers/CacheLayerComparisonBenchmark.cs
@@ -125,7 +125,7 @@ public async Task MemoryCacheLayer()
public async Task JsonFileCacheLayer()
{
var directoryPath = "CacheLayerComparison/NewtonsoftJson";
- await using (var cacheLayer = new FileCacheLayer(NewtonsoftJsonCacheSerializer.Instance, directoryPath))
+ await using (var cacheLayer = new FileCacheLayer(new FileCacheLayerOptions(directoryPath, NewtonsoftJsonCacheSerializer.Instance)))
{
await BenchmarkWork(cacheLayer);
}
@@ -136,7 +136,7 @@ public async Task JsonFileCacheLayer()
public async Task ProtobufFileCacheLayer()
{
var directoryPath = "CacheLayerComparison/Protobuf";
- await using (var cacheLayer = new FileCacheLayer(ProtobufCacheSerializer.Instance, directoryPath))
+ await using (var cacheLayer = new FileCacheLayer(new FileCacheLayerOptions(directoryPath, ProtobufCacheSerializer.Instance)))
{
await BenchmarkWork(cacheLayer);
}
diff --git a/benchmarks/CacheTower.Benchmarks/Providers/FileSystem/NewtonsoftJsonFileCacheBenchmark.cs b/benchmarks/CacheTower.Benchmarks/Providers/FileSystem/NewtonsoftJsonFileCacheBenchmark.cs
index bc808a6f..ff57f377 100644
--- a/benchmarks/CacheTower.Benchmarks/Providers/FileSystem/NewtonsoftJsonFileCacheBenchmark.cs
+++ b/benchmarks/CacheTower.Benchmarks/Providers/FileSystem/NewtonsoftJsonFileCacheBenchmark.cs
@@ -10,7 +10,7 @@ public class NewtonsoftJsonFileCacheBenchmark : BaseFileCacheLayerBenchmark
public void Setup()
{
DirectoryPath = "FileCache/NewtonsoftJson";
- CacheLayerProvider = () => new FileCacheLayer(NewtonsoftJsonCacheSerializer.Instance, DirectoryPath);
+ CacheLayerProvider = () => new FileCacheLayer(new(DirectoryPath, NewtonsoftJsonCacheSerializer.Instance));
}
}
}
diff --git a/benchmarks/CacheTower.Benchmarks/Providers/FileSystem/ProtobufFileCacheBenchmark.cs b/benchmarks/CacheTower.Benchmarks/Providers/FileSystem/ProtobufFileCacheBenchmark.cs
index 12acd977..53148c58 100644
--- a/benchmarks/CacheTower.Benchmarks/Providers/FileSystem/ProtobufFileCacheBenchmark.cs
+++ b/benchmarks/CacheTower.Benchmarks/Providers/FileSystem/ProtobufFileCacheBenchmark.cs
@@ -10,7 +10,7 @@ public class ProtobufFileCacheBenchmark : BaseFileCacheLayerBenchmark
public void Setup()
{
DirectoryPath = "FileCache/Protobuf";
- CacheLayerProvider = () => new FileCacheLayer(ProtobufCacheSerializer.Instance, DirectoryPath);
+ CacheLayerProvider = () => new FileCacheLayer(new(DirectoryPath, ProtobufCacheSerializer.Instance));
}
}
}
diff --git a/src/CacheTower.Providers.FileSystem.Json/JsonFileCacheLayer.cs b/src/CacheTower.Providers.FileSystem.Json/JsonFileCacheLayer.cs
index 9733d7ff..c6dbe97c 100644
--- a/src/CacheTower.Providers.FileSystem.Json/JsonFileCacheLayer.cs
+++ b/src/CacheTower.Providers.FileSystem.Json/JsonFileCacheLayer.cs
@@ -1,7 +1,4 @@
-using System.IO;
-using System.Text;
-using Newtonsoft.Json;
-using CacheTower.Serializers.NewtonsoftJson;
+using CacheTower.Serializers.NewtonsoftJson;
using System;
namespace CacheTower.Providers.FileSystem.Json
@@ -17,6 +14,6 @@ public class JsonFileCacheLayer : FileCacheLayer, ICacheLayer
/// Creates a , using the given as the location to store the cache.
///
///
- public JsonFileCacheLayer(string directoryPath) : base(NewtonsoftJsonCacheSerializer.Instance, directoryPath) { }
+ public JsonFileCacheLayer(string directoryPath) : base(new FileCacheLayerOptions(directoryPath, NewtonsoftJsonCacheSerializer.Instance)) { }
}
}
diff --git a/src/CacheTower.Providers.FileSystem.Protobuf/ProtobufFileCacheLayer.cs b/src/CacheTower.Providers.FileSystem.Protobuf/ProtobufFileCacheLayer.cs
index 80fcd8cc..289819af 100644
--- a/src/CacheTower.Providers.FileSystem.Protobuf/ProtobufFileCacheLayer.cs
+++ b/src/CacheTower.Providers.FileSystem.Protobuf/ProtobufFileCacheLayer.cs
@@ -18,6 +18,6 @@ public class ProtobufFileCacheLayer : FileCacheLayer
/// Creates a , using the given as the location to store the cache.
///
///
- public ProtobufFileCacheLayer(string directoryPath) : base(ProtobufCacheSerializer.Instance, directoryPath) { }
+ public ProtobufFileCacheLayer(string directoryPath) : base(new FileCacheLayerOptions(directoryPath, ProtobufCacheSerializer.Instance)) { }
}
}
diff --git a/src/CacheTower/Internal/MD5HashUtility.cs b/src/CacheTower/Internal/MD5HashUtility.cs
new file mode 100644
index 00000000..ff33d6b2
--- /dev/null
+++ b/src/CacheTower/Internal/MD5HashUtility.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace CacheTower.Internal;
+
+internal static class MD5HashUtility
+{
+ [ThreadStatic]
+ private static MD5? ThreadInstance;
+
+ private static MD5 HashAlgorithm => ThreadInstance ??= MD5.Create();
+
+#if NETSTANDARD2_0
+ public static unsafe string ComputeHash(string value)
+ {
+ var bytes = Encoding.UTF8.GetBytes(value);
+ var hashBytes = HashAlgorithm.ComputeHash(bytes);
+
+#elif NETSTANDARD2_1
+ public static unsafe string ComputeHash(ReadOnlySpan value)
+ {
+ var encoding = Encoding.UTF8;
+ var bytesRequired = encoding.GetByteCount(value);
+ Span bytes = stackalloc byte[bytesRequired];
+ encoding.GetBytes(value, bytes);
+
+ Span hashBytes = stackalloc byte[16];
+ HashAlgorithm.TryComputeHash(bytes, hashBytes, out var _);
+#endif
+
+ //Based on byte conversion implementation in BitConverter (but with the dash stripped)
+ //https://github.com/dotnet/coreclr/blob/fbc11ea6afdaa2fe7b9377446d6bb0bd447d5cb5/src/mscorlib/shared/System/BitConverter.cs#L409-L440
+ static char GetHexValue(int i)
+ {
+ if (i < 10)
+ {
+ return (char)(i + '0');
+ }
+
+ return (char)(i - 10 + 'A');
+ }
+
+ const int charArrayLength = 32;
+ var charArrayPtr = stackalloc char[charArrayLength];
+
+ var charPtr = charArrayPtr;
+ for (var i = 0; i < 16; i++)
+ {
+ var hashByte = hashBytes[i];
+ *charPtr++ = GetHexValue(hashByte >> 4);
+ *charPtr++ = GetHexValue(hashByte & 0xF);
+ }
+
+ return new string(charArrayPtr, 0, charArrayLength);
+ }
+}
diff --git a/src/CacheTower/Providers/FileSystem/FileCacheLayer.cs b/src/CacheTower/Providers/FileSystem/FileCacheLayer.cs
index ff27d14f..9d841c5c 100644
--- a/src/CacheTower/Providers/FileSystem/FileCacheLayer.cs
+++ b/src/CacheTower/Providers/FileSystem/FileCacheLayer.cs
@@ -1,10 +1,9 @@
using System;
using System.Collections.Concurrent;
using System.IO;
-using System.Security.Cryptography;
-using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using CacheTower.Internal;
using Nito.AsyncEx;
namespace CacheTower.Providers.FileSystem
@@ -17,30 +16,45 @@ namespace CacheTower.Providers.FileSystem
public class FileCacheLayer : ICacheLayer, IAsyncDisposable
{
private bool Disposed = false;
+ private readonly FileCacheLayerOptions Options;
+ private readonly string ManifestPath;
- private ICacheSerializer Serializer { get; }
- private string DirectoryPath { get; }
- private string ManifestPath { get; }
+ private readonly CancellationTokenSource ManifestSavingCancellationTokenSource;
+ private readonly Task BackgroundManifestSavingTask;
- private SemaphoreSlim ManifestLock { get; } = new SemaphoreSlim(1, 1);
- private bool? IsManifestAvailable { get; set; }
+ private readonly SemaphoreSlim ManifestLock = new(1, 1);
- private HashAlgorithm FileNameHashAlgorithm { get; } = MD5.Create();
+ private bool? IsManifestAvailable { get; set; }
private ConcurrentDictionary? CacheManifest { get; set; }
- private ConcurrentDictionary FileLock { get; }
+ private readonly ConcurrentDictionary FileLock;
///
- /// Initialises the file cache layer with the given .
+ /// Initialises the with the provided .
///
- /// The serializer to use for the data.
- /// The directory to store the cache.
- public FileCacheLayer(ICacheSerializer serializer, string directoryPath)
+ /// Various options that control the behaviour of the .
+ public FileCacheLayer(FileCacheLayerOptions options)
{
- Serializer = serializer;
- DirectoryPath = directoryPath ?? throw new ArgumentNullException(nameof(directoryPath));
- ManifestPath = Path.Combine(directoryPath, "manifest");
+ Options = options;
+ ManifestPath = Path.Combine(options.DirectoryPath, "manifest");
FileLock = new ConcurrentDictionary(StringComparer.Ordinal);
+
+ ManifestSavingCancellationTokenSource = new();
+ BackgroundManifestSavingTask = BackgroundManifestSaving();
+ }
+
+ private async Task BackgroundManifestSaving()
+ {
+ try
+ {
+ var cancellationToken = ManifestSavingCancellationTokenSource.Token;
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ await Task.Delay(Options.ManifestSaveInterval, cancellationToken);
+ await SaveManifestAsync();
+ }
+ }
+ catch (OperationCanceledException) { }
}
private async Task DeserializeFileAsync(string path)
@@ -49,14 +63,14 @@ public FileCacheLayer(ICacheSerializer serializer, string directoryPath)
using var memStream = new MemoryStream((int)stream.Length);
await stream.CopyToAsync(memStream);
memStream.Seek(0, SeekOrigin.Begin);
- return Serializer.Deserialize(memStream);
+ return Options.Serializer.Deserialize(memStream);
}
private async Task SerializeFileAsync(string path, T value)
{
using var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, 1024);
using var memStream = new MemoryStream();
- Serializer.Serialize(memStream, value);
+ Options.Serializer.Serialize(memStream, value);
memStream.Seek(0, SeekOrigin.Begin);
await memStream.CopyToAsync(stream);
}
@@ -78,9 +92,9 @@ private async Task TryLoadManifestAsync()
}
else
{
- if (!Directory.Exists(DirectoryPath))
+ if (!Directory.Exists(Options.DirectoryPath))
{
- Directory.CreateDirectory(DirectoryPath);
+ Directory.CreateDirectory(Options.DirectoryPath);
}
CacheManifest = new ConcurrentDictionary();
@@ -104,9 +118,9 @@ public async Task SaveManifestAsync()
await ManifestLock.WaitAsync();
try
{
- if (!Directory.Exists(DirectoryPath))
+ if (!Directory.Exists(Options.DirectoryPath))
{
- Directory.CreateDirectory(DirectoryPath);
+ Directory.CreateDirectory(Options.DirectoryPath);
}
await SerializeFileAsync(ManifestPath, CacheManifest);
@@ -117,56 +131,12 @@ public async Task SaveManifestAsync()
}
}
-#if NETSTANDARD2_0
- private unsafe string GetFileName(string cacheKey)
- {
- var bytes = Encoding.UTF8.GetBytes(cacheKey);
- var hashBytes = FileNameHashAlgorithm.ComputeHash(bytes);
-
-#elif NETSTANDARD2_1
- private unsafe string GetFileName(ReadOnlySpan cacheKey)
- {
- var encoding = Encoding.UTF8;
- var bytesRequired = encoding.GetByteCount(cacheKey);
- Span bytes = stackalloc byte[bytesRequired];
- encoding.GetBytes(cacheKey, bytes);
-
- Span hashBytes = stackalloc byte[16];
- FileNameHashAlgorithm.TryComputeHash(bytes, hashBytes, out var _);
-#endif
-
- //Based on byte conversion implementation in BitConverter (but with the dash stripped)
- //https://github.com/dotnet/coreclr/blob/fbc11ea6afdaa2fe7b9377446d6bb0bd447d5cb5/src/mscorlib/shared/System/BitConverter.cs#L409-L440
- static char GetHexValue(int i)
- {
- if (i < 10)
- {
- return (char)(i + '0');
- }
-
- return (char)(i - 10 + 'A');
- }
-
- var charArrayLength = 32;
- var charArrayPtr = stackalloc char[charArrayLength];
-
- var charPtr = charArrayPtr;
- for (var i = 0; i < 16; i++)
- {
- var hashByte = hashBytes[i];
- *charPtr++ = GetHexValue(hashByte >> 4);
- *charPtr++ = GetHexValue(hashByte & 0xF);
- }
-
- return new string(charArrayPtr, 0, charArrayLength);
- }
-
///
public async ValueTask CleanupAsync()
{
await TryLoadManifestAsync();
- var currentTime = DateTime.UtcNow;
+ var currentTime = DateTimeProvider.Now;
foreach (var cachePair in CacheManifest!)
{
var manifestEntry = cachePair.Value;
@@ -176,7 +146,7 @@ public async ValueTask CleanupAsync()
{
using (await lockObj.WriterLockAsync())
{
- var path = Path.Combine(DirectoryPath, manifestEntry.FileName);
+ var path = Path.Combine(Options.DirectoryPath, manifestEntry.FileName);
if (File.Exists(path))
{
File.Delete(path);
@@ -198,7 +168,7 @@ public async ValueTask EvictAsync(string cacheKey)
{
using (await lockObj.WriterLockAsync())
{
- var path = Path.Combine(DirectoryPath, manifestEntry.FileName);
+ var path = Path.Combine(Options.DirectoryPath, manifestEntry.FileName);
if (File.Exists(path))
{
File.Delete(path);
@@ -219,7 +189,7 @@ public async ValueTask FlushAsync()
{
using (await lockObj.WriterLockAsync())
{
- var path = Path.Combine(DirectoryPath, manifestEntry.FileName);
+ var path = Path.Combine(Options.DirectoryPath, manifestEntry.FileName);
if (File.Exists(path))
{
File.Delete(path);
@@ -246,7 +216,7 @@ public async ValueTask FlushAsync()
//By the time we have the lock, confirm we still have a cache
if (CacheManifest.ContainsKey(cacheKey))
{
- var path = Path.Combine(DirectoryPath, manifestEntry.FileName);
+ var path = Path.Combine(Options.DirectoryPath, manifestEntry.FileName);
if (File.Exists(path))
{
var value = await DeserializeFileAsync(path);
@@ -304,7 +274,7 @@ public async ValueTask SetAsync(string cacheKey, CacheEntry cacheEntry)
else
{
manifestEntry = new(
- GetFileName(cacheKey!),
+ MD5HashUtility.ComputeHash(cacheKey!),
cacheEntry.Expiry
);
}
@@ -314,7 +284,7 @@ public async ValueTask SetAsync(string cacheKey, CacheEntry cacheEntry)
using (await lockObj.WriterLockAsync())
{
- var path = Path.Combine(DirectoryPath, manifestEntry.FileName);
+ var path = Path.Combine(Options.DirectoryPath, manifestEntry.FileName);
await SerializeFileAsync(path, cacheEntry.Value);
}
}
@@ -330,9 +300,14 @@ public async ValueTask DisposeAsync()
return;
}
+ ManifestSavingCancellationTokenSource.Cancel();
+ if (!BackgroundManifestSavingTask.IsFaulted)
+ {
+ await BackgroundManifestSavingTask;
+ }
await SaveManifestAsync();
+
ManifestLock.Dispose();
- FileNameHashAlgorithm.Dispose();
Disposed = true;
}
diff --git a/src/CacheTower/Providers/FileSystem/FileCacheLayerOptions.cs b/src/CacheTower/Providers/FileSystem/FileCacheLayerOptions.cs
new file mode 100644
index 00000000..fcde2bdc
--- /dev/null
+++ b/src/CacheTower/Providers/FileSystem/FileCacheLayerOptions.cs
@@ -0,0 +1,39 @@
+using System;
+
+namespace CacheTower.Providers.FileSystem;
+
+///
+/// Options for controlling a .
+///
+/// The directory to store the cache in.
+/// The serializer to use for the data.
+public record struct FileCacheLayerOptions(
+ string DirectoryPath,
+ ICacheSerializer Serializer
+)
+{
+ ///
+ /// The default manifest save interval of 5 minutes.
+ ///
+ public static readonly TimeSpan DefaultManifestSaveInterval = TimeSpan.FromMinutes(5);
+
+ ///
+ /// The time interval controlling how often the cache manifest is saved to disk.
+ ///
+ public TimeSpan ManifestSaveInterval { get; init; } = DefaultManifestSaveInterval;
+
+ ///
+ /// Options for controlling a .
+ ///
+ /// The directory to store the cache in.
+ /// The serializer to use for the data.
+ /// The time interval controlling how often the cache manifest is saved to disk.
+ public FileCacheLayerOptions(
+ string directoryPath,
+ ICacheSerializer serializer,
+ TimeSpan manifestSaveInterval
+ ) : this(directoryPath, serializer)
+ {
+ ManifestSaveInterval = manifestSaveInterval;
+ }
+}
diff --git a/tests/CacheTower.Tests/Providers/FileSystem/FileCacheLayerTests.cs b/tests/CacheTower.Tests/Providers/FileSystem/FileCacheLayerTests.cs
index 75ba87cb..8b35562b 100644
--- a/tests/CacheTower.Tests/Providers/FileSystem/FileCacheLayerTests.cs
+++ b/tests/CacheTower.Tests/Providers/FileSystem/FileCacheLayerTests.cs
@@ -41,7 +41,7 @@ public void Setup()
public async Task CanLoadExistingManifest()
{
var testSerializer = new TestCacheSerializer();
- var cacheLayer = new FileCacheLayer(testSerializer, DirectoryPath);
+ var cacheLayer = new FileCacheLayer(new(DirectoryPath, testSerializer));
await using (cacheLayer)
{
//IsAvailableAsync triggers load of manifest which in turn creates it because it doesn't exist
@@ -52,7 +52,7 @@ public async Task CanLoadExistingManifest()
Assert.AreEqual(0, testSerializer.DeserializeCount);
testSerializer = new TestCacheSerializer();
- cacheLayer = new FileCacheLayer(testSerializer, DirectoryPath);
+ cacheLayer = new FileCacheLayer(new(DirectoryPath, testSerializer));
await using (cacheLayer)
{
Assert.IsTrue(await cacheLayer.IsAvailableAsync("AnyKey"));
@@ -64,48 +64,48 @@ public async Task CanLoadExistingManifest()
[TestMethod]
public async Task PersistentGetSetCache()
{
- await AssertPersistentGetSetCacheAsync(() => new FileCacheLayer(new NewtonsoftJsonCacheSerializer(), DirectoryPath));
+ await AssertPersistentGetSetCacheAsync(() => new FileCacheLayer(new(DirectoryPath, NewtonsoftJsonCacheSerializer.Instance)));
}
[TestMethod]
public async Task GetSetCache()
{
- await using var cacheLayer = new FileCacheLayer(new NewtonsoftJsonCacheSerializer(), DirectoryPath);
+ await using var cacheLayer = new FileCacheLayer(new(DirectoryPath, NewtonsoftJsonCacheSerializer.Instance));
await AssertGetSetCacheAsync(cacheLayer);
}
[TestMethod]
public async Task IsCacheAvailable()
{
- await using var cacheLayer = new FileCacheLayer(new NewtonsoftJsonCacheSerializer(), DirectoryPath);
+ await using var cacheLayer = new FileCacheLayer(new(DirectoryPath, NewtonsoftJsonCacheSerializer.Instance));
await AssertCacheAvailabilityAsync(cacheLayer, true);
}
[TestMethod]
public async Task EvictFromCache()
{
- await using var cacheLayer = new FileCacheLayer(new NewtonsoftJsonCacheSerializer(), DirectoryPath);
+ await using var cacheLayer = new FileCacheLayer(new(DirectoryPath, NewtonsoftJsonCacheSerializer.Instance));
await AssertCacheEvictionAsync(cacheLayer);
}
[TestMethod]
public async Task FlushFromCache()
{
- await using var cacheLayer = new FileCacheLayer(new NewtonsoftJsonCacheSerializer(), DirectoryPath);
+ await using var cacheLayer = new FileCacheLayer(new(DirectoryPath, NewtonsoftJsonCacheSerializer.Instance));
await AssertCacheFlushAsync(cacheLayer);
}
[TestMethod]
public async Task CacheCleanup()
{
- await using var cacheLayer = new FileCacheLayer(new NewtonsoftJsonCacheSerializer(), DirectoryPath);
+ await using var cacheLayer = new FileCacheLayer(new(DirectoryPath, NewtonsoftJsonCacheSerializer.Instance));
await AssertCacheCleanupAsync(cacheLayer);
}
[TestMethod]
public async Task CachingComplexTypes()
{
- await using var cacheLayer = new FileCacheLayer(new NewtonsoftJsonCacheSerializer(), DirectoryPath);
+ await using var cacheLayer = new FileCacheLayer(new(DirectoryPath, NewtonsoftJsonCacheSerializer.Instance));
await AssertComplexTypeCachingAsync(cacheLayer);
}
}