Skip to content

Commit

Permalink
Add ExecuteFullEviction which supports either inducing a new full evi…
Browse files Browse the repository at this point in the history
…ction or awaing an existing one (#46)

* Add ExecuteFullEviction which supports either inducing a new full eviction or awaing an existing one
- Adjust implementation to make it possible to await in-flight eviction or clear
- Drop EOL'ed .NET Core 3.1 target
- Add .NET 8 preview target for tests
- Make main cache store delay in staggered full eviction less aggressive
  • Loading branch information
neon-sunset authored May 27, 2023
1 parent 0a7df07 commit 0ab994d
Show file tree
Hide file tree
Showing 10 changed files with 75 additions and 52 deletions.
6 changes: 4 additions & 2 deletions .github/workflows/dotnet-releaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ jobs:
uses: actions/setup-dotnet@v2
with:
dotnet-version: |
3.1.x
6.0.x
7.0.x
8.0.x
include-prerelease: true
- name: Build, Test, Pack, Publish
shell: bash
env:
Expand All @@ -46,9 +47,10 @@ jobs:
uses: actions/setup-dotnet@v2
with:
dotnet-version: |
3.1.x
6.0.x
7.0.x
8.0.x
include-prerelease: true
- name: Build, Test, Pack, Publish
shell: pwsh
env:
Expand Down
5 changes: 4 additions & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable</WarningsAsErrors>
<IsTrimmable>true</IsTrimmable>
<!-- This is a hack because the life is too short to learn MSBuild -->
<IsTrimmable Condition="$(TargetFrameworkVersion) == '6.0'">true</IsTrimmable>
<IsTrimmable Condition="$(TargetFrameworkVersion) == '7.0'">true</IsTrimmable>
<IsTrimmable Condition="$(TargetFrameworkVersion) == '8.0'">true</IsTrimmable>
<NoWarn>CS1591</NoWarn>
</PropertyGroup>

Expand Down
2 changes: 1 addition & 1 deletion src/FastCache.Benchmarks/FastCache.Benchmarks.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net7.0</TargetFrameworks>
<TargetFrameworks>net7.0;net8.0</TargetFrameworks>
<IsPackable>false</IsPackable>
<DebugType>pdbonly</DebugType>
<DebugSymbols>true</DebugSymbols>
Expand Down
96 changes: 56 additions & 40 deletions src/FastCache.Cached/CacheManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,39 +17,60 @@ public static class CacheManager
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]
/// Trigger full eviction for expired cache entries of type Cached[K, V].
/// This operation is a no-op when eviction is suspended.
/// </summary>
public static void QueueFullEviction<K, V>() where K : notnull => QueueFullEviction<K, V>(triggeredByTimer: true);
public static void QueueFullEviction<K, V>() where K : notnull
{
_ = ExecuteFullEviction<K, V>(triggeredByGC: false);
}

/// <summary>
/// Remove all cache entries of type Cached[K, V] from the cache
/// </summary>
public static void QueueFullClear<K, V>() where K : notnull
{
Task.Run(static () => ExecuteFullClear<K, V>());
_ = ExecuteFullClear<K, V>();
}

/// <summary>
/// Remove all cache entries of type Cached[K, V] from the cache
/// Trigger full eviction for expired cache entries of type Cached[K, V].
/// This operation is a no-op when eviction is suspended.
/// Disclaimer: if there is an ongoing staggered full eviction (triggered by Gen2 GC), this method will await its completion, which can take significant time.
/// </summary>
/// <returns>One of: A task for a new full eviction that completes upon its execution; A task for an already ongoing eviction or clear.</returns>
public static Task ExecuteFullEviction<K, V>() where K : notnull => ExecuteFullEviction<K, V>(triggeredByGC: false);

/// <summary>
/// Remove all cache entries of type Cached[K, V] from the cache.
/// Disclaimer: if there is an ongoing staggered full eviction (triggered by Gen2 GC),
/// this method will await its completion before proceeding with full clear, which can take significant time.
/// For benchmarking purposes, consider suspending eviction first before calling this method.
/// </summary>
/// <returns>A task that completes upon full clear execution.</returns>
public static async Task ExecuteFullClear<K, V>() where K : notnull
{
#if FASTCACHE_DEBUG
var countBefore = CacheStaticHolder<K, V>.Store.Count;
#endif

var evictionJob = CacheStaticHolder<K, V>.EvictionJob;
await evictionJob.FullEvictionLock.WaitAsync();

#if FASTCACHE_DEBUG
var countBefore = CacheStaticHolder<K, V>.Store.Count;
#endif
static void Inner()
{
CacheStaticHolder<K, V>.Store.Clear();
CacheStaticHolder<K, V>.QuickList.Reset();
}

CacheStaticHolder<K, V>.Store.Clear();
CacheStaticHolder<K, V>.QuickList.Reset();
await (evictionJob.ActiveFullEviction = Task.Run(Inner));

evictionJob.FullEvictionLock.Release();
evictionJob.ActiveFullEviction = null;

#if FASTCACHE_DEBUG
Console.WriteLine(
$"FastCache: Cache has been fully cleared for {typeof(K).Name}:{typeof(V).Name}. Was {countBefore}, now {CacheStaticHolder<K, V>.QuickList.AtomicCount}/{CacheStaticHolder<K, V>.Store.Count}");
Console.WriteLine(
$"FastCache: Cache has been fully cleared for {typeof(K).Name}:{typeof(V).Name}. Was {countBefore}, now {CacheStaticHolder<K, V>.QuickList.AtomicCount}/{CacheStaticHolder<K, V>.Store.Count}");
#endif
}

Expand Down Expand Up @@ -157,37 +178,43 @@ internal static void ReportEvictions(uint count)
Interlocked.Add(ref s_AggregatedEvictionsCount, count);
}

internal static void QueueFullEviction<K, V>(bool triggeredByTimer) where K : notnull
internal static async Task ExecuteFullEviction<K, V>(bool triggeredByGC) where K : notnull
{
if (!CacheStaticHolder<K, V>.EvictionJob.IsActive)
var evictionJob = CacheStaticHolder<K, V>.EvictionJob;
if (!evictionJob.IsActive)
{
return;
}

if (triggeredByTimer)
{
Task.Run(ImmediateFullEviction<K, V>);
}
else
Retry:
if (!evictionJob.FullEvictionLock.Wait(millisecondsTimeout: 0))
{
Task.Run(async () => await StaggeredFullEviction<K, V>());
var activeEviction = evictionJob.ActiveFullEviction;
if (activeEviction is null)
{
goto Retry;
}

await activeEviction;
return;
}

evictionJob.ActiveFullEviction = !triggeredByGC
? Task.Run(ImmediateFullEviction<K, V>)
: StaggeredFullEviction<K, V>();

await evictionJob.ActiveFullEviction;

evictionJob.FullEvictionLock.Release();
evictionJob.ActiveFullEviction = null;
}

private static void ImmediateFullEviction<K, V>() where K : notnull
{
var evictionJob = CacheStaticHolder<K, V>.EvictionJob;

if (!evictionJob.FullEvictionLock.Wait(millisecondsTimeout: 0))
{
return;
}

evictionJob.RescheduleConsideringExpiration();
CacheStaticHolder<K, V>.EvictionJob.RescheduleConsideringExpiration();

if (CacheStaticHolder<K, V>.QuickList.Evict(resize: true))
{
evictionJob.FullEvictionLock.Release();
return;
}

Expand All @@ -206,19 +233,12 @@ private static void ImmediateFullEviction<K, V>() where K : notnull
#endif

Task.Run(async static () => await ConsiderFullGC<V>());

evictionJob.FullEvictionLock.Release();
}

private static async ValueTask StaggeredFullEviction<K, V>() where K : notnull
private static async Task StaggeredFullEviction<K, V>() where K : notnull
{
var evictionJob = CacheStaticHolder<K, V>.EvictionJob;

if (!evictionJob.FullEvictionLock.Wait(millisecondsTimeout: 0))
{
return;
}

if (CacheStaticHolder<K, V>.QuickList.Evict())
{
// When a lot of items are being added to cache, it triggers GC and its callbacks
Expand All @@ -227,15 +247,13 @@ private static async ValueTask StaggeredFullEviction<K, V>() where K : notnull
// over newly added items which is not profitable to do.
// Delaying lock release for extra (quick list interval / 5) avoids the issue.
await Task.Delay(Constants.EvictionCooldownDelayOnGC);
evictionJob.FullEvictionLock.Release();
return;
}

evictionJob.EvictionGCNotificationsCount++;
if (evictionJob.EvictionGCNotificationsCount < 2)
{
await Task.Delay(Constants.EvictionCooldownDelayOnGC);
evictionJob.FullEvictionLock.Release();
return;
}

Expand All @@ -256,9 +274,7 @@ private static async ValueTask StaggeredFullEviction<K, V>() where K : notnull
#endif

await Task.Delay(Constants.EvictionCooldownDelayOnGC);

evictionJob.EvictionGCNotificationsCount = 0;
evictionJob.FullEvictionLock.Release();
}

private static uint EvictFromCacheStore<K, V>() where K : notnull
Expand Down
2 changes: 1 addition & 1 deletion src/FastCache.Cached/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public static TimeSpan CacheStoreEvictionDelay
get
{
var delay = (int)QuickListEvictionInterval.TotalMilliseconds;
var jitter = GetRandomInt(0, delay * 2);
var jitter = GetRandomInt(0, delay / 2);
return TimeSpan.FromMilliseconds(delay + jitter);
}
}
Expand Down
6 changes: 4 additions & 2 deletions src/FastCache.Cached/EvictionJob.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ internal sealed class EvictionJob<K, V> where K : notnull

public readonly SemaphoreSlim FullEvictionLock = new(1, 1);

public Task? ActiveFullEviction;

public int EvictionGCNotificationsCount;

public EvictionJob()
Expand Down Expand Up @@ -48,13 +50,13 @@ public EvictionJob()
// Full eviction interval is always computed with jitter. Store to local so that start and repeat intervals are equal.
var fullEvictionInterval = Constants.FullEvictionInterval;
_fullEvictionTimer = new(
static _ => CacheManager.QueueFullEviction<K, V>(triggeredByTimer: true),
static _ => CacheManager.QueueFullEviction<K, V>(),
null,
fullEvictionInterval,
fullEvictionInterval);

#if NETCOREAPP3_0_OR_GREATER
Gen2GcCallback.Register(() => CacheManager.QueueFullEviction<K, V>(triggeredByTimer: false));
Gen2GcCallback.Register(() => _ = CacheManager.ExecuteFullEviction<K, V>(triggeredByGC: true));
#endif
}

Expand Down
2 changes: 1 addition & 1 deletion src/FastCache.Cached/FastCache.Cached.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Credit to Vladimir Sadov for his implementation of NonBlocking.ConcurrentDiction
</PropertyGroup>

<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1;netcoreapp3.1;net6.0;net7.0</TargetFrameworks>
<TargetFrameworks>netstandard2.0;netstandard2.1;net6.0;net7.0</TargetFrameworks>
<DefineConstants>$(DefineConstants)</DefineConstants>
</PropertyGroup>

Expand Down
4 changes: 2 additions & 2 deletions src/FastCache.Sandbox/EvictionStress.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ public static void Run()
// ThreadPool.QueueUserWorkItem(_ => SeedRandomlyExpirable<Uri2>(10));
// Thread.Sleep(250);
// CacheManager.QueueFullClear<(uint, string, int, string, char, bool, float), Uri2>();
// ThreadPool.QueueUserWorkItem(_ => SeedRandomlyExpirable<Struct>(5));
ThreadPool.QueueUserWorkItem(_ => SeedRandomlyExpirable<Struct>(5));
ThreadPool.QueueUserWorkItem(_ => SeedSequentiallyExpirable<long>());
// ThreadPool.QueueUserWorkItem(_ => SeedRandomlyExpirable<User>(1));
// ThreadPool.QueueUserWorkItem(_ => SeedRandomlyExpirable<bool>(25));
ThreadPool.QueueUserWorkItem(_ => SeedRandomlyExpirable<bool>(25));
// ThreadPool.QueueUserWorkItem(_ => SeedSequentiallyExpirable<Uri2>());
// ThreadPool.QueueUserWorkItem(_ => SeedIndefinite<Uri2>(10));

Expand Down
2 changes: 1 addition & 1 deletion src/FastCache.Sandbox/FastCache.Sandbox.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
<ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>
Expand Down
2 changes: 1 addition & 1 deletion tests/FastCache.CachedTests/FastCache.CachedTests.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netcoreapp3.1;net6.0;net7.0</TargetFrameworks>
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net48</TargetFrameworks>
<IsPackable>false</IsPackable>
</PropertyGroup>
Expand Down

0 comments on commit 0ab994d

Please sign in to comment.