Skip to content

Commit 3954858

Browse files
committed
Refactoring, documentation
Still working on the layered cache tests next...
1 parent 8dce6d0 commit 3954858

18 files changed

+544
-183
lines changed

DigitalRuby.SimpleCache.Sandbox/appsettings.json

+17-11
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,32 @@
99
},
1010
"DigitalRuby.SimpleCache":
1111
{
12-
/* redis connection string */
12+
/*
13+
optional, cache key prefix, by default the entry assembly name is used
14+
you can set this to an empty string to share keys between services that are using the same redis cluster
15+
*/
16+
"KeyPrefix": "sandbox",
17+
18+
/* optional, override max memory size (in megabytes). Default is 1024. */
19+
"MaxMemorySize": 2048,
20+
21+
/* redis connection string, required */
1322
"RedisConnectionString": "localhost:6379",
1423

1524
/*
16-
file cache directory, set to empty to not use file cache (recommended if not on SSD)
25+
opptional, override file cache directory, set to empty to not use file cache (recommended if not on SSD)
1726
the default is %temp% which means to use the temp directory
1827
this example assumes running on Windows, for production, use an environment variable or just leave off for default of %temp%.
1928
*/
2029
"FileCacheDirectory": "c:/temp",
2130

22-
/* override the file cache cleanup threshold (0-100 percent). default is 15 */
31+
/* optional, override the file cache cleanup threshold (0-100 percent). default is 15 */
2332
"FileCacheFreeSpaceThreshold": 10,
2433

25-
/* optional override the default json-lz4 serializer with your own class that implements DigitalRuby.SimpleCache.ISerializer */
26-
"SerializerType": "DigitalRuby.SimpleCache.Sandbox.JsonSerializer, DigitalRuby.SimpleCache.Sandbox",
27-
28-
/* optional key prefix, by default the entry assembly name is used */
29-
"KeyPrefix": "sandbox",
30-
31-
/* optional, set max memory size (in megabytes). Default is 1024. */
32-
"MaxMemorySize": 2048
34+
/*
35+
optional, override the default json-lz4 serializer with your own class that implements DigitalRuby.SimpleCache.ISerializer
36+
the serializer is used to convert objects to bytes for the file and redis caches
37+
*/
38+
"SerializerType": "DigitalRuby.SimpleCache.JsonSerializer, DigitalRuby.SimpleCache"
3339
}
3440
}

DigitalRuby.SimpleCache.Tests/FileCacheTests.cs

+9-5
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ namespace DigitalRuby.SimpleCache.Tests;
33
/// <summary>
44
/// File cache tests
55
/// </summary>
6-
public class FileCacheTests : IDateTimeProvider, IDiskSpace
6+
public sealed class FileCacheTests : IClockHandler, IDiskSpace
77
{
8+
private DateTimeOffset utcNow;
9+
810
/// <inheritdoc />
9-
public DateTimeOffset UtcNow { get; set; }
11+
DateTimeOffset ISystemClock.UtcNow => utcNow;
1012

1113
/// <inheritdoc />
12-
public Task DelayAsync(TimeSpan interval, CancellationToken cancelToken = default)
14+
Task IClockHandler.DelayAsync(TimeSpan interval, CancellationToken cancelToken)
1315
{
1416
return Task.CompletedTask;
1517
}
@@ -39,6 +41,8 @@ public async Task TestFileCache()
3941
{
4042
const int testCount = 10;
4143
const string data = "74956375-DD97-4857-816E-188BC8D4090F74956375-DD97-4857-816E-188BC8D4090F74956375-DD97-4857-816E-188BC8D4090F74956375-DD97-4857-816E-188BC8D4090F74956375-DD97-4857-816E-188BC8D4090F74956375-DD97-4857-816E-188BC8D4090F74956375-DD97-4857-816E-188BC8D4090F74956375-DD97-4857-816E-188BC8D4090F74956375-DD97-4857-816E-188BC8D4090F74956375-DD97-4857-816E-188BC8D4090F";
44+
45+
utcNow = new DateTimeOffset(2022, 1, 1, 1, 1, 1, TimeSpan.Zero);
4246
using FileCache fileCache = new(new() { FreeSpaceThreshold = 20 }, new JsonLZ4Serializer(), this, this, new NullLogger<FileCache>());
4347

4448
var item = await fileCache.GetAsync<string>("key1");
@@ -50,7 +54,7 @@ public async Task TestFileCache()
5054
Assert.That(item.Item, Is.EqualTo(data));
5155

5256
// step time, item should expire out
53-
UtcNow += TimeSpan.FromSeconds(6.0);
57+
utcNow += TimeSpan.FromSeconds(6.0);
5458
await fileCache.CleanupFreeSpaceAsync();
5559
item = await fileCache.GetAsync<string>("key1");
5660
Assert.That(item, Is.Null);
@@ -98,7 +102,7 @@ public async Task TestFileCache()
98102
sw.Restart();
99103

100104
// step time, item should expire out
101-
UtcNow += TimeSpan.FromSeconds(6.0);
105+
utcNow += TimeSpan.FromSeconds(6.0);
102106

103107
for (int i = 0; i < testCount; i++)
104108
{
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
global using System.Diagnostics;
22

3+
global using Microsoft.Extensions.Caching.Memory;
4+
global using Microsoft.Extensions.Internal;
35
global using Microsoft.Extensions.Logging.Abstractions;
6+
global using Microsoft.Extensions.Options;
47

58
global using DigitalRuby.SimpleCache;
69

10+
global using NUnit.Framework;
11+
712
namespace DigitalRuby.SimpleCache;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
namespace DigitalRuby.SimpleCache.Tests;
2+
3+
/// <summary>
4+
/// Tests for layered cache
5+
/// </summary>
6+
[TestFixture]
7+
public sealed class LayeredCacheTests : IDiskSpace, IClockHandler, IOptions<MemoryCacheOptions>, ISystemClock
8+
{
9+
private ISerializer serializer = new JsonSerializer();
10+
private Dictionary<string, long> fileSpaces = new();
11+
private MemoryCache memoryCache;
12+
private FileCache fileCache;
13+
private DistributedMemoryCache distributedCache;
14+
private LayeredCache layeredCache;
15+
private long freeSpace;
16+
private long totalSpace;
17+
private DateTimeOffset utcNow;
18+
19+
[SetUp]
20+
public void Setup()
21+
{
22+
fileSpaces.Clear();
23+
memoryCache = new(this);
24+
freeSpace = 10000;
25+
totalSpace = 100000;
26+
utcNow = new DateTimeOffset(2022, 1, 1, 1, 1, 1, TimeSpan.Zero);
27+
fileCache = new FileCache(new FileCacheOptions(), serializer, this, this, new NullLogger<FileCache>());
28+
distributedCache = new DistributedMemoryCache(this);
29+
layeredCache = new LayeredCache(new LayeredCacheOptions { KeyPrefix = "test" }, serializer,
30+
memoryCache, fileCache, distributedCache, new NullLogger<LayeredCache>());
31+
}
32+
33+
MemoryCacheOptions IOptions<MemoryCacheOptions>.Value => new() { Clock = this };
34+
35+
DateTimeOffset ISystemClock.UtcNow => utcNow;
36+
37+
Task IClockHandler.DelayAsync(System.TimeSpan interval, System.Threading.CancellationToken cancelToken) => Task.Delay(0, cancelToken);
38+
39+
long IDiskSpace.GetFileSize(string fileName)
40+
{
41+
fileSpaces.TryGetValue(fileName, out long space);
42+
return space;
43+
}
44+
45+
double IDiskSpace.GetPercentFreeSpace(string path, out long availableFreeSpace, out long totalSpace)
46+
{
47+
availableFreeSpace = this.freeSpace;
48+
totalSpace = this.totalSpace;
49+
return (double)availableFreeSpace / (double)totalSpace;
50+
}
51+
}

DigitalRuby.SimpleCache.Tests/Usings.cs

-1
This file was deleted.

DigitalRuby.SimpleCache.sln

+10-2
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,17 @@ VisualStudioVersion = 17.2.32519.379
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigitalRuby.SimpleCache", "DigitalRuby.SimpleCache\DigitalRuby.SimpleCache.csproj", "{20A96010-CAEF-4ADF-A076-9434261916BE}"
77
EndProject
8-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigitalRuby.SimpleCache.Tests", "DigitalRuby.SimpleCache.Tests\DigitalRuby.SimpleCache.Tests.csproj", "{A82855AE-B0FD-4B77-8839-A29A094F706A}"
8+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigitalRuby.SimpleCache.Tests", "DigitalRuby.SimpleCache.Tests\DigitalRuby.SimpleCache.Tests.csproj", "{A82855AE-B0FD-4B77-8839-A29A094F706A}"
99
EndProject
10-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigitalRuby.SimpleCache.Sandbox", "DigitalRuby.SimpleCache.Sandbox\DigitalRuby.SimpleCache.Sandbox.csproj", "{CB0257DD-3D4A-4C42-B7C0-EC626C662CFA}"
10+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigitalRuby.SimpleCache.Sandbox", "DigitalRuby.SimpleCache.Sandbox\DigitalRuby.SimpleCache.Sandbox.csproj", "{CB0257DD-3D4A-4C42-B7C0-EC626C662CFA}"
11+
EndProject
12+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionItems", "{96B77B60-6CDC-47A9-8861-B55FC991CA5C}"
13+
ProjectSection(SolutionItems) = preProject
14+
.gitignore = .gitignore
15+
icon.png = icon.png
16+
LICENSE = LICENSE
17+
README.md = README.md
18+
EndProjectSection
1119
EndProject
1220
Global
1321
GlobalSection(SolutionConfigurationPlatforms) = preSolution

DigitalRuby.SimpleCache/CacheParameters.cs

+22-13
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,31 @@ public struct CacheParameters
1919
/// Constructor
2020
/// </summary>
2121
/// <param name="duration">Duration</param>
22-
public CacheParameters(TimeSpan duration)
22+
/// <param name="jitterDuration">Whether to jitter the duration</param>
23+
public CacheParameters(TimeSpan duration, bool jitterDuration = true)
2324
{
2425
Duration = duration;
2526
Size = DefaultSize;
27+
if (jitterDuration)
28+
{
29+
JitterDuration();
30+
}
2631
}
2732

2833
/// <summary>
2934
/// Constructor
3035
/// </summary>
3136
/// <param name="duration">Duration</param>
3237
/// <param name="size">Size</param>
33-
public CacheParameters(TimeSpan duration, int size)
38+
/// <param name="jitterDuration">Whether to jitter the duration</param>
39+
public CacheParameters(TimeSpan duration, int size, bool jitterDuration = true)
3440
{
3541
Duration = duration;
36-
Size = size;
42+
Size = size <= 0 ? DefaultSize : size;
43+
if (jitterDuration)
44+
{
45+
JitterDuration();
46+
}
3747
}
3848

3949
/// <summary>
@@ -53,33 +63,32 @@ internal static void AssignDefaultIfNeeded(ref CacheParameters cacheParameters)
5363
private static readonly ThreadLocal<Random> jitter = new(() => new Random());
5464

5565
/// <summary>
56-
/// Jitter the cache duration, ensure that we don't have a bunch of stuff expiring right at the same time
66+
/// Wiggle the cache duration slightly to avoid having multiple cache items expire all at once
5767
/// </summary>
58-
/// <param name="cacheParameters">Cache parameters</param>
59-
internal static void JitterDuration(ref CacheParameters cacheParameters)
68+
private void JitterDuration()
6069
{
6170
double upperJitter;
62-
if (cacheParameters.Duration.TotalMinutes <= 1.0)
71+
if (Duration.TotalMinutes <= 1.0)
6372
{
6473
// don't jitter cache times 1 minute or less
6574
return;
6675
}
67-
else if (cacheParameters.Duration.TotalMinutes <= 15.0)
76+
else if (Duration.TotalMinutes <= 15.0)
6877
{
6978
// up to 15 min
7079
upperJitter = 1.2;
7180
}
72-
else if (cacheParameters.Duration.TotalMinutes <= 60.0)
81+
else if (Duration.TotalMinutes <= 60.0)
7382
{
7483
// up to 1 hour
7584
upperJitter = 1.15;
7685
}
77-
else if (cacheParameters.Duration.TotalMinutes <= 360.0)
86+
else if (Duration.TotalMinutes <= 360.0)
7887
{
7988
// up to 6 hours
8089
upperJitter = 1.1;
8190
}
82-
else if (cacheParameters.Duration.TotalMinutes <= 1440.0)
91+
else if (Duration.TotalMinutes <= 1440.0)
8392
{
8493
// up to 24 hours
8594
upperJitter = 1.05;
@@ -92,8 +101,8 @@ internal static void JitterDuration(ref CacheParameters cacheParameters)
92101

93102
double randomDouble = jitter.Value!.NextDouble();
94103
double multiplier = 1.0 + (randomDouble * upperJitter);
95-
long jitteredTicks = (long)(cacheParameters.Duration.Ticks * multiplier);
96-
cacheParameters.Duration = TimeSpan.FromTicks(jitteredTicks);
104+
long jitteredTicks = (long)(Duration.Ticks * multiplier);
105+
Duration = TimeSpan.FromTicks(jitteredTicks);
97106
}
98107

99108
/// <summary>

DigitalRuby.SimpleCache/DateTimeProvider.cs renamed to DigitalRuby.SimpleCache/ClockHandler.cs

+3-8
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,8 @@
33
/// <summary>
44
/// Allows mocking current date/time and delays
55
/// </summary>
6-
public interface IDateTimeProvider
6+
public interface IClockHandler : ISystemClock
77
{
8-
/// <summary>
9-
/// Current date/time
10-
/// </summary>
11-
DateTimeOffset UtcNow { get; }
12-
138
/// <summary>
149
/// Delay for a set amount of time
1510
/// </summary>
@@ -20,9 +15,9 @@ public interface IDateTimeProvider
2015
}
2116

2217
/// <summary>
23-
/// Piggy back on IDateTimeProvider interface
18+
/// Implement IClockHandler
2419
/// </summary>
25-
public sealed class DateTimeProvider : IDateTimeProvider
20+
public sealed class ClockHandler : IClockHandler
2621
{
2722
/// <inheritdoc />
2823
public DateTimeOffset UtcNow => DateTimeOffset.UtcNow;

DigitalRuby.SimpleCache/DigitalRuby.SimpleCache.csproj

-6
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,4 @@
1717
<PackageReference Include="SauceControl.Blake2Fast" Version="2.0.0" />
1818
</ItemGroup>
1919

20-
<ItemGroup>
21-
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
22-
<_Parameter1>$(AssemblyName).Tests</_Parameter1>
23-
</AssemblyAttribute>
24-
</ItemGroup>
25-
2620
</Project>

DigitalRuby.SimpleCache/DistributedCache.cs

+6-2
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,11 @@ public interface IDistributedCache
6969
/// </summary>
7070
public sealed class DistributedMemoryCache : IDistributedCache, IDistributedLockFactory
7171
{
72-
private sealed class FakeDistributedLock : IAsyncDisposable
72+
private readonly ISystemClock clock;
73+
74+
public DistributedMemoryCache(ISystemClock clock) => this.clock = clock;
75+
76+
private sealed class FakeDistributedLock : IAsyncDisposable
7377
{
7478
public ValueTask DisposeAsync()
7579
{
@@ -328,7 +332,7 @@ public interface IDistributedLockFactory
328332
/// Attempt to acquire a distributed lock
329333
/// </summary>
330334
/// <param name="key">Lock key</param>
331-
/// <param name="lockTime">Duration to acquire the lock before it auto-expires</param>
335+
/// <param name="lockTime">Duration to hold the lock before it auto-expires. Set this to the maximum possible duration you think your code might hold the lock.</param>
332336
/// <param name="timeout">Time out to acquire the lock or default to only make one attempt to acquire the lock</param>
333337
/// <returns>The lock or null if the lock could not be acquired</returns>
334338
Task<IAsyncDisposable?> TryAcquireLockAsync(string key, TimeSpan lockTime, TimeSpan timeout = default);

0 commit comments

Comments
 (0)