Skip to content

Commit

Permalink
Merge 4f5aaca into 1167f40
Browse files Browse the repository at this point in the history
  • Loading branch information
neon-sunset committed Oct 4, 2022
2 parents 1167f40 + 4f5aaca commit 90488a6
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 10 deletions.
33 changes: 30 additions & 3 deletions src/FastCache.Cached/CacheManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,20 @@ public static class CacheManager
private static long s_AggregatedEvictionsCount;

/// <summary>
/// Submit full eviction for specified Cached<K, V> value
/// Total atomic count of entries present in cache, including expired.
/// </summary>
/// <typeparam name="K">Cache entry key type. string, int or (int, int) for multi-key.</typeparam>
/// <typeparam name="V">Cache entry value type</typeparam>
public static int TotalCount<K, V>() where K : notnull => CacheStaticHolder<K, V>.Store.Count;

/// <summary>
/// Trigger full eviction for expired cache entries of type Cached[K, V]
/// </summary>
public static void QueueFullEviction<K, V>() where K : notnull => QueueFullEviction<K, V>(triggeredByTimer: true);

/// <summary>
/// Remove all cache entries of type Cached[K, V] from the cache
/// </summary>
public static void QueueFullClear<K, V>() where K : notnull
{
ThreadPool.QueueUserWorkItem(async static _ =>
Expand All @@ -37,11 +47,28 @@ public static class CacheManager
});
}

/// <summary>
/// Enumerates all not expired entries currently present in the cache.
/// Cache changes introduced from other threads may not be visible to the enumerator.
/// </summary>
/// <typeparam name="K">Cache entry key type. string, int or (int, int) for multi-key.</typeparam>
/// <typeparam name="V">Cache entry value type</typeparam>
public static IEnumerable<Cached<K, V>> EnumerateEntries<K, V>() where K : notnull
{
foreach (var (key, inner) in CacheStaticHolder<K, V>.Store)
{
if (inner.IsNotExpired())
{
yield return new(key, inner.Value, found: true);
}
}
}

/// <summary>
/// Trims cache store for a given percentage of its size. Will remove at least 1 item.
/// </summary>
/// <typeparam name="K"></typeparam>
/// <typeparam name="V"></typeparam>
/// <typeparam name="K">Cache entry key type. string, int or (int, int) for multi-key.</typeparam>
/// <typeparam name="V">Cache entry value type</typeparam>
/// <param name="percentage"></param>
/// <returns>True: trim is performed inline. False: the count to trim is above threshold and removal is queued to thread pool.</returns>
public static bool Trim<K, V>(double percentage) where K : notnull
Expand Down
21 changes: 14 additions & 7 deletions src/FastCache.Cached/Cached.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,16 @@ namespace FastCache;
[StructLayout(LayoutKind.Auto)]
public readonly record struct Cached<K, V> where K : notnull
{
private readonly K _key;
private readonly bool _found;

/// <summary>
/// Cache entry key. Either a single-argument like string, int, etc. or multi-key value as tuple like (int, int, bool).
/// </summary>
public readonly K Key;

/// <summary>
/// Cache entry value. Guaranteed to be up-to-date with millisecond accuracy as long as 'bool TryGet' conditional is checked.
/// </summary>
public readonly V Value;

/// <summary>
Expand All @@ -26,9 +33,9 @@ namespace FastCache;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal Cached(K key, V value, bool found)
{
_key = key;
_found = found;

Key = key;
Value = value;
}

Expand All @@ -42,12 +49,12 @@ public V Save(V value, TimeSpan expiration)
{
var (timestamp, milliseconds) = TimeUtils.GetTimestamp(expiration);

CacheStaticHolder<K, V>.Store[_key] = new(value, timestamp);
CacheStaticHolder<K, V>.Store[Key] = new(value, timestamp);
CacheStaticHolder<K, V>.EvictionJob.ReportExpiration(milliseconds);

if (!_found)
{
CacheStaticHolder<K, V>.QuickList.Add(_key, timestamp);
CacheStaticHolder<K, V>.QuickList.Add(Key, timestamp);
}

return value;
Expand Down Expand Up @@ -81,9 +88,9 @@ public bool Update(V value)
{
var store = CacheStaticHolder<K, V>.Store;

if (_found && store.TryGetValue(_key, out var inner))
if (_found && store.TryGetValue(Key, out var inner))
{
store[_key] = new(value, inner._timestamp);
store[Key] = new(value, inner._timestamp);
return true;
}

Expand All @@ -95,7 +102,7 @@ public bool Update(V value)
/// </summary>
public void Remove()
{
CacheStaticHolder<K, V>.Store.TryRemove(_key, out _);
CacheStaticHolder<K, V>.Store.TryRemove(Key, out _);
}
}

Expand Down
1 change: 1 addition & 0 deletions tests/FastCache.CachedTests/FastCache.CachedTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="RangeExtensions" Version="1.2.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
Expand Down
35 changes: 35 additions & 0 deletions tests/FastCache.CachedTests/Internals/CacheManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using FastCache.Extensions;
using FastCache.Services;
using FastCache.Helpers;
using System.Linq;

namespace FastCache.CachedTests.Internals;

Expand All @@ -10,10 +11,44 @@ public sealed class CacheManagerTests
private record ExpiredEntry(string Value);
private record OptionallyExpiredEntry(string Value, bool IsExpired);
private record RemovableEntry(string Value);
private record TotalCountEntry(int Value);
private record EnumerableEntry();

private static readonly Random Random = new();
private static readonly TimeSpan DelayTolerance = TimeSpan.FromMilliseconds(100);

[Fact]
public void TotalCount_ReturnsCorrectValue()
{
const int expectedCount = 32768;

var entries = (0..32768)
.AsEnumerable()
.Select(i => (i, new TotalCountEntry(i)));

CachedRange<TotalCountEntry>.Save(entries, TimeSpan.MaxValue);

Assert.Equal(expectedCount, CacheManager.TotalCount<int, TotalCountEntry>());
}

[Fact]
public async Task EnumerateEntries_ReturnsCorrectValues()
{
var expired = (0..1024).AsEnumerable().ToDictionary(i => i, _ => new EnumerableEntry());
var notExpired = (1024..2048).AsEnumerable().ToDictionary(i => i, _ => new EnumerableEntry());

CachedRange<EnumerableEntry>.Save(expired.Select(kvp => (kvp.Key, kvp.Value)), DelayTolerance);
CachedRange<EnumerableEntry>.Save(notExpired.Select(kvp => (kvp.Key, kvp.Value)), DelayTolerance * 2);

await Task.Delay(DelayTolerance);

foreach (var cached in CacheManager.EnumerateEntries<int, EnumerableEntry>())
{
Assert.False(expired.ContainsKey(cached.Key));
Assert.True(notExpired.ContainsKey(cached.Key));
}
}

[Theory]
[InlineData(double.NaN)]
[InlineData(double.MinValue)]
Expand Down

0 comments on commit 90488a6

Please sign in to comment.