Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replaced CachedAt and TimeToLive fields with single Expiry field #13

Merged
merged 7 commits into from
Oct 28, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions CodeCoverage.runsettings
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
<Include>
<ModulePath>.*\.dll$</ModulePath>
</Include>
<Exclude>
<ModulePath>CacheTower.Tests.dll</ModulePath>
<Exclude>
<ModulePath>CacheTower.Tests.dll</ModulePath>
<ModulePath>CacheTower.Benchmarks.dll</ModulePath>
<ModulePath>CacheTower.AlternativesBenchmark.dll</ModulePath>
</Exclude>
</ModulePaths>

Expand Down
99 changes: 50 additions & 49 deletions docs/Comparison.md

Large diffs are not rendered by default.

144 changes: 72 additions & 72 deletions docs/Performance.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/CacheTower.Extensions.Redis/RedisLockExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ private async Task<CacheEntry<T>> WaitForResult<T>(string cacheKey, CacheSetting

//Last minute check to confirm whether waiting is required (in case the notification is missed)
var currentEntry = await RegisteredStack.GetAsync<T>(cacheKey);
if (currentEntry != null && !currentEntry.HasElapsed(settings.StaleAfter))
if (currentEntry != null && currentEntry.GetStaleDate(settings) > DateTime.UtcNow)
{
UnlockWaitingTasks(cacheKey);
return currentEntry;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ public IEnumerable<WriteModel<DbCachedEntry>> GetModel()
var filter = Builders<DbCachedEntry>.Filter.Eq(e => e.CacheKey, Entry.CacheKey);
var updateDefinition = Builders<DbCachedEntry>.Update
.Set(e => e.CacheKey, Entry.CacheKey)
.Set(e => e.CachedAt, Entry.CachedAt)
.Set(e => e.TimeToLive, Entry.TimeToLive)
.Set(e => e.Expiry, Entry.Expiry)
.Set(e => e.Value, Entry.Value);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,9 @@ public class DbCachedEntry

[Index(MongoFramework.IndexSortOrder.Ascending)]
public string CacheKey { get; set; }
public DateTime CachedAt { get; set; }
public TimeSpan TimeToLive { get; set; }

[Index(MongoFramework.IndexSortOrder.Ascending)]
public DateTime Expiry
{
get => CachedAt + TimeToLive;
set { }
}
public DateTime Expiry { get; set; }

public object Value { get; set; }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public async Task<CacheEntry<T>> GetAsync<T>(string cacheKey)

if (dbEntry != default)
{
cacheEntry = new CacheEntry<T>((T)dbEntry.Value, dbEntry.CachedAt, dbEntry.TimeToLive);
cacheEntry = new CacheEntry<T>((T)dbEntry.Value, dbEntry.Expiry);
}

return cacheEntry;
Expand All @@ -72,8 +72,7 @@ public async Task SetAsync<T>(string cacheKey, CacheEntry<T> cacheEntry)
var command = new SetCommand(new DbCachedEntry
{
CacheKey = cacheKey,
CachedAt = cacheEntry.CachedAt,
TimeToLive = cacheEntry.TimeToLive,
Expiry = cacheEntry.Expiry,
Value = cacheEntry.Value
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,6 @@ public class JsonFileCacheLayer : FileCacheLayerBase<ManifestEntry>, ICacheLayer

public JsonFileCacheLayer(string directoryPath) : base(directoryPath, ".json") { }

private class DataWrapper<T>
{
public T Value { get; set; }
}

protected override T Deserialize<T>(Stream stream)
{
using (var streamReader = new StreamReader(stream, Encoding.UTF8, false, 1024))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ public class ProtobufManifestEntry : IManifestEntry
[ProtoMember(1)]
public string FileName { get; set; }
[ProtoMember(2)]
public DateTime CachedAt { get; set; }
[ProtoMember(3)]
public TimeSpan TimeToLive { get; set; }
public DateTime Expiry { get; set; }
}
}
4 changes: 1 addition & 3 deletions src/CacheTower.Providers.Redis/Entities/RedisCacheEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,8 @@ namespace CacheTower.Providers.Redis.Entities
public class RedisCacheEntry<T>
{
[ProtoMember(1)]
public DateTime CachedAt { get; set; }
public DateTime Expiry { get; set; }
[ProtoMember(2)]
public TimeSpan TimeToLive { get; set; }
[ProtoMember(3)]
public T Value { get; set; }
}
}
14 changes: 6 additions & 8 deletions src/CacheTower.Providers.Redis/RedisCacheLayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public class RedisCacheLayer : IAsyncCacheLayer
private IDatabaseAsync Database { get; }
private bool? IsCacheAvailable { get; set; }

public RedisCacheLayer(ConnectionMultiplexer connection, int databaseIndex = -1)
public RedisCacheLayer(IConnectionMultiplexer connection, int databaseIndex = -1)
{
Database = connection.GetDatabase(databaseIndex);
}
Expand All @@ -36,7 +36,7 @@ public async Task<CacheEntry<T>> GetAsync<T>(string cacheKey)
using (var stream = new MemoryStream(redisValue))
{
var redisCacheEntry = Serializer.Deserialize<RedisCacheEntry<T>>(stream);
return new CacheEntry<T>(redisCacheEntry.Value, redisCacheEntry.CachedAt, redisCacheEntry.TimeToLive);
return new CacheEntry<T>(redisCacheEntry.Value, redisCacheEntry.Expiry);
}
}

Expand All @@ -63,17 +63,15 @@ public async Task<bool> IsAvailableAsync(string cacheKey)

public async Task SetAsync<T>(string cacheKey, CacheEntry<T> cacheEntry)
{
//Redis doesn't support setting a TTL in the past, let's confirm the expiry date
var trueTtl = (cacheEntry.CachedAt + cacheEntry.TimeToLive) - DateTime.UtcNow;
if (trueTtl < TimeSpan.Zero)
var expiryOffset = cacheEntry.Expiry - DateTime.UtcNow;
if (expiryOffset < TimeSpan.Zero)
{
return;
}

var redisCacheEntry = new RedisCacheEntry<T>
{
CachedAt = cacheEntry.CachedAt,
TimeToLive = cacheEntry.TimeToLive,
Expiry = cacheEntry.Expiry,
Value = cacheEntry.Value
};

Expand All @@ -83,7 +81,7 @@ public async Task SetAsync<T>(string cacheKey, CacheEntry<T> cacheEntry)
stream.Seek(0, SeekOrigin.Begin);

var redisValue = RedisValue.CreateFrom(stream);
await Database.StringSetAsync(cacheKey, redisValue, cacheEntry.TimeToLive);
await Database.StringSetAsync(cacheKey, redisValue, expiryOffset);
}
}
}
Expand Down
29 changes: 11 additions & 18 deletions src/CacheTower/CacheEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,29 @@ namespace CacheTower
{
public abstract class CacheEntry
{
public DateTime CachedAt { get; }
public DateTime Expiry { get; }

public TimeSpan TimeToLive { get; }

protected CacheEntry(DateTime cachedAt, TimeSpan timeToLive)
protected CacheEntry(DateTime expiry)
{
if (timeToLive < TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(timeToLive), "TimeSpan must be greater than or equal to zero");
}

CachedAt = new DateTime(
cachedAt.Year, cachedAt.Month, cachedAt.Day, cachedAt.Hour, cachedAt.Minute, cachedAt.Second, DateTimeKind.Utc
//Force the resolution of the expiry date to be to the second
Expiry = new DateTime(
expiry.Year, expiry.Month, expiry.Day, expiry.Hour, expiry.Minute, expiry.Second, DateTimeKind.Utc
);
TimeToLive = timeToLive;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool HasElapsed(TimeSpan timeSpan)
public DateTime GetStaleDate(CacheSettings cacheSettings)
{
return CachedAt.Add(timeSpan) < DateTime.UtcNow;
return Expiry - cacheSettings.TimeToLive + cacheSettings.StaleAfter;
}
}

public class CacheEntry<T> : CacheEntry, IEquatable<CacheEntry<T>>
{
public T Value { get; }

public CacheEntry(T value, DateTime cachedAt, TimeSpan timeToLive) : base(cachedAt, timeToLive)
public CacheEntry(T value, TimeSpan timeToLive) : this(value, DateTime.UtcNow + timeToLive) { }
public CacheEntry(T value, DateTime expiry) : base(expiry)
{
Value = value;
}
Expand All @@ -48,8 +42,7 @@ public bool Equals(CacheEntry<T> other)
}

return Equals(Value, other.Value) &&
CachedAt == other.CachedAt &&
TimeToLive == other.TimeToLive;
Expiry == other.Expiry;
}

public override bool Equals(object obj)
Expand All @@ -64,7 +57,7 @@ public override bool Equals(object obj)

public override int GetHashCode()
{
return (Value?.GetHashCode() ?? 1) ^ CachedAt.GetHashCode() ^ TimeToLive.GetHashCode();
return (Value?.GetHashCode() ?? 1) ^ Expiry.GetHashCode();
}
}
}
11 changes: 6 additions & 5 deletions src/CacheTower/CacheStack.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ public async ValueTask<CacheEntry<T>> SetAsync<T>(string cacheKey, T value, Time
{
ThrowIfDisposed();

var cacheEntry = new CacheEntry<T>(value, DateTime.UtcNow, timeToLive);
var expiry = DateTime.UtcNow + timeToLive;
var cacheEntry = new CacheEntry<T>(value, expiry);
await SetAsync(cacheKey, cacheEntry);
return cacheEntry;
}
Expand Down Expand Up @@ -222,10 +223,10 @@ public async ValueTask<T> GetOrSetAsync<T>(string cacheKey, Func<T, ICacheContex
if (cacheEntryPoint != default)
{
var cacheEntry = cacheEntryPoint.CacheEntry;

if (cacheEntry.HasElapsed(settings.StaleAfter))
var currentTime = DateTime.UtcNow;
if (cacheEntry.GetStaleDate(settings) < currentTime)
{
if (cacheEntry.HasElapsed(settings.TimeToLive))
if (cacheEntry.Expiry < currentTime)
{
//Refresh the value in the current thread though short circuit if we're unable to establish a lock
//If the lock isn't established, it will instead use the stale cache entry (even if past the allowed stale period)
Expand Down Expand Up @@ -369,7 +370,7 @@ private async ValueTask<CacheEntry<T>> RefreshValueAsync<T>(string cacheKey, Fun

//Last minute check to confirm whether waiting is required
var currentEntry = await GetAsync<T>(cacheKey);
if (currentEntry != null && !currentEntry.HasElapsed(settings.StaleAfter))
if (currentEntry != null && currentEntry.GetStaleDate(settings) > DateTime.UtcNow)
{
UnlockWaitingTasks(cacheKey);
return currentEntry;
Expand Down
11 changes: 5 additions & 6 deletions src/CacheTower/Providers/FileSystem/FileCacheLayerBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,11 +158,11 @@ public async Task CleanupAsync()
{
await TryLoadManifestAsync();

var currentTime = DateTime.UtcNow;
foreach (var cachePair in CacheManifest)
{
var manifestEntry = cachePair.Value;
var expiryDate = manifestEntry.CachedAt.Add(manifestEntry.TimeToLive);
if (expiryDate < DateTime.UtcNow && CacheManifest.TryRemove(cachePair.Key, out var _))
if (manifestEntry.Expiry < currentTime && CacheManifest.TryRemove(cachePair.Key, out var _))
{
if (FileLock.TryRemove(manifestEntry.FileName, out var lockObj))
{
Expand Down Expand Up @@ -213,7 +213,7 @@ public async Task<CacheEntry<T>> GetAsync<T>(string cacheKey)
{
var path = Path.Combine(DirectoryPath, manifestEntry.FileName);
var value = await DeserializeFileAsync<T>(path);
return new CacheEntry<T>(value, manifestEntry.CachedAt, manifestEntry.TimeToLive);
return new CacheEntry<T>(value, manifestEntry.Expiry);
}
}
}
Expand Down Expand Up @@ -248,9 +248,8 @@ public async Task SetAsync<T>(string cacheKey, CacheEntry<T> cacheEntry)
FileName = GetFileName(cacheKey)
});

//Update the manifest entry with the new cache entry date/times
manifestEntry.CachedAt = cacheEntry.CachedAt;
manifestEntry.TimeToLive = cacheEntry.TimeToLive;
//Update the manifest entry with the new expiry
manifestEntry.Expiry = cacheEntry.Expiry;

var lockObj = FileLock.GetOrAdd(manifestEntry.FileName, (name) => new AsyncReaderWriterLock());

Expand Down
3 changes: 1 addition & 2 deletions src/CacheTower/Providers/FileSystem/IManifestEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ namespace CacheTower.Providers.FileSystem
public interface IManifestEntry
{
string FileName { get; set; }
DateTime CachedAt { get; set; }
TimeSpan TimeToLive { get; set; }
DateTime Expiry { get; set; }
}
}
3 changes: 1 addition & 2 deletions src/CacheTower/Providers/FileSystem/ManifestEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ namespace CacheTower.Providers.FileSystem
public class ManifestEntry : IManifestEntry
{
public string FileName { get; set; }
public DateTime CachedAt { get; set; }
public TimeSpan TimeToLive { get; set; }
public DateTime Expiry { get; set; }
}
}
3 changes: 2 additions & 1 deletion src/CacheTower/Providers/Memory/MemoryCacheLayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ public void Cleanup()
{
var keysToRemove = ArrayPool<string>.Shared.Rent(Cache.Count);
var index = 0;
var currentTime = DateTime.UtcNow;

foreach (var cachePair in Cache)
{
var cacheEntry = cachePair.Value;
if (cacheEntry.HasElapsed(cacheEntry.TimeToLive))
if (cacheEntry.Expiry < currentTime)
{
keysToRemove[index] = cachePair.Key;
index++;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,13 @@ public void CacheTower_MemoryCacheLayer_Direct()
{
LoopAction(Iterations, () =>
{
layer.Set("TestKey", new CacheEntry<int>(123, DateTime.UtcNow, TimeSpan.FromDays(1)));
layer.Set("TestKey", new CacheEntry<int>(123, DateTime.UtcNow + TimeSpan.FromDays(1)));
layer.Get<int>("TestKey");

var getOrSetResult = layer.Get<string>("GetOrSet_TestKey");
if (getOrSetResult == null)
{
layer.Set("GetOrSet_TestKey", new CacheEntry<string>("Hello World", DateTime.UtcNow, TimeSpan.FromDays(1)));
layer.Set("GetOrSet_TestKey", new CacheEntry<string>("Hello World", TimeSpan.FromDays(1)));
}
});
}
Expand Down
6 changes: 3 additions & 3 deletions tests/CacheTower.Benchmarks/CacheStackBenchmark.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ public async Task GetOrSet()
{
await using (var cacheStack = new CacheStack(null, new[] { new MemoryCacheLayer() }, Array.Empty<ICacheExtension>()))
{
await cacheStack.SetAsync("GetOrSet", new CacheEntry<int>(15, DateTime.UtcNow.AddDays(-2), TimeSpan.FromDays(1)));
await cacheStack.SetAsync("GetOrSet", new CacheEntry<int>(15, DateTime.UtcNow.AddDays(-1)));
await cacheStack.GetOrSetAsync<int>("GetOrSet", (old, context) =>
{
return Task.FromResult(12);
Expand All @@ -122,7 +122,7 @@ public async Task GetOrSet_TwoSimultaneous()
{
await using (var cacheStack = new CacheStack(null, new[] { new MemoryCacheLayer() }, Array.Empty<ICacheExtension>()))
{
await cacheStack.SetAsync("GetOrSet", new CacheEntry<int>(15, DateTime.UtcNow.AddDays(-2), TimeSpan.FromDays(1)));
await cacheStack.SetAsync("GetOrSet", new CacheEntry<int>(15, DateTime.UtcNow.AddDays(-1)));
var task1 = cacheStack.GetOrSetAsync<int>("GetOrSet", async (old, context) =>
{
await Task.Delay(50);
Expand All @@ -143,7 +143,7 @@ public async Task GetOrSet_FourSimultaneous()
{
await using (var cacheStack = new CacheStack(null, new[] { new MemoryCacheLayer() }, Array.Empty<ICacheExtension>()))
{
await cacheStack.SetAsync("GetOrSet", new CacheEntry<int>(15, DateTime.UtcNow.AddDays(-2), TimeSpan.FromDays(1)));
await cacheStack.SetAsync("GetOrSet", new CacheEntry<int>(15, DateTime.UtcNow.AddDays(-1)));
var task1 = cacheStack.GetOrSetAsync<int>("GetOrSet", async (old, context) =>
{
await Task.Delay(50);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public async Task RefreshValue()
extension.Register(CacheStack);
await extension.RefreshValueAsync<int>("RefreshValue", () =>
{
return new ValueTask<CacheEntry<int>>(new CacheEntry<int>(5, DateTime.UtcNow, TimeSpan.FromDays(1)));
return new ValueTask<CacheEntry<int>>(new CacheEntry<int>(5, TimeSpan.FromDays(1)));
}, new CacheSettings(TimeSpan.FromDays(1)));
await DisposeOf(extension);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public async Task GetHitSimultaneous()
{
var cacheLayer = CacheLayerProvider.Invoke() as IAsyncCacheLayer;

await cacheLayer.SetAsync("GetHitSimultaneous", new CacheEntry<int>(1, DateTime.UtcNow, TimeSpan.FromDays(1)));
await cacheLayer.SetAsync("GetHitSimultaneous", new CacheEntry<int>(1, TimeSpan.FromDays(1)));

var aTask = cacheLayer.GetAsync<int>("GetHitSimultaneous");
var bTask = cacheLayer.GetAsync<int>("GetHitSimultaneous");
Expand All @@ -29,10 +29,10 @@ public async Task SetExistingSimultaneous()
{
var cacheLayer = CacheLayerProvider.Invoke() as IAsyncCacheLayer;

await cacheLayer.SetAsync("SetExistingSimultaneous", new CacheEntry<int>(1, DateTime.UtcNow, TimeSpan.FromDays(1)));
await cacheLayer.SetAsync("SetExistingSimultaneous", new CacheEntry<int>(1, TimeSpan.FromDays(1)));

var aTask = cacheLayer.SetAsync("SetExistingSimultaneous", new CacheEntry<int>(1, DateTime.UtcNow, TimeSpan.FromDays(1)));
var bTask = cacheLayer.SetAsync("SetExistingSimultaneous", new CacheEntry<int>(1, DateTime.UtcNow, TimeSpan.FromDays(1)));
var aTask = cacheLayer.SetAsync("SetExistingSimultaneous", new CacheEntry<int>(1, TimeSpan.FromDays(1)));
var bTask = cacheLayer.SetAsync("SetExistingSimultaneous", new CacheEntry<int>(1, TimeSpan.FromDays(1)));

await aTask;
await bTask;
Expand Down
Loading