Skip to content

Commit

Permalink
feat(cache): single flight cache update
Browse files Browse the repository at this point in the history
  • Loading branch information
GZTimeWalker committed Sep 4, 2024
1 parent 7db7805 commit 484aa03
Show file tree
Hide file tree
Showing 2 changed files with 60 additions and 16 deletions.
74 changes: 59 additions & 15 deletions src/GZCTF/Extensions/CacheExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using MemoryPack;
using GZCTF.Services.Cache;
using MemoryPack;
using Microsoft.Extensions.Caching.Distributed;

namespace GZCTF.Extensions;
Expand All @@ -19,26 +20,28 @@ public static async Task<TResult> GetOrCreateAsync<TResult, TLogger>(this IDistr
var value = await cache.GetAsync(key, token);
TResult? result = default;

if (value is not null)
{
try
{
result = MemoryPackSerializer.Deserialize<TResult>(value);
}
catch
{
// ignored
}

if (result is not null)
return result;
}
// most of the time, the cache is already been set
if (TryDeserialize(value, ref result))
return result!;

// wait if the cache is updating
value = await WaitLockAsync(cache, key, token);

if (TryDeserialize(value, ref result))
return result!;

var lockKey = CacheKey.UpdateLock(key);
await SetLockAsync(cache, lockKey, token);

// begin the update
var cacheOptions = new DistributedCacheEntryOptions();
result = await func(cacheOptions);
var bytes = MemoryPackSerializer.Serialize(result);

await cache.SetAsync(key, bytes, cacheOptions, token);
// finish the update

await ReleaseLockAsync(cache, lockKey, token);

logger.SystemLog(Program.StaticLocalizer[
nameof(Resources.Program.Cache_Updated),
Expand All @@ -47,4 +50,45 @@ public static async Task<TResult> GetOrCreateAsync<TResult, TLogger>(this IDistr

return result;
}

static bool TryDeserialize<TResult>(byte[]? value, ref TResult? result)
{
if (value is null)
return false;

try
{
result = MemoryPackSerializer.Deserialize<TResult>(value);
return result is not null;
}
catch
{
return false;
}
}

static async Task<byte[]?> WaitLockAsync(IDistributedCache cache, string key, CancellationToken token = default)
{
var lockKey = CacheKey.UpdateLock(key);
var lockValue = await cache.GetAsync(lockKey, token);

if (lockValue is null)
return null;

while (lockValue is not null)
{
await Task.Delay(100, token);
lockValue = await cache.GetAsync(lockKey, token);
}

// if we wait for the lock, we should try to get the value again
return await cache.GetAsync(key, token);
}

static Task SetLockAsync(IDistributedCache cache, string lockKey, CancellationToken token = default)
=> cache.SetAsync(lockKey, [],
new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1) }, token);

static Task ReleaseLockAsync(IDistributedCache cache, string lockKey, CancellationToken token = default) =>
cache.RemoveAsync(lockKey, token);
}
2 changes: 1 addition & 1 deletion src/GZCTF/Services/Cache/CacheHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public static class CacheKey
/// <summary>
/// The cache update lock
/// </summary>
public static string UpdateLock(string key) => $"_UpdateLock_{key}";
public static string UpdateLock(string key) => $"_UpdateLock{key}";

/// <summary>
/// The last update time
Expand Down

0 comments on commit 484aa03

Please sign in to comment.