diff --git a/build/version.props b/build/version.props index 5dc47ec6..05918071 100644 --- a/build/version.props +++ b/build/version.props @@ -1,11 +1,11 @@ - 1.9.1 + 1.9.2 1.9.1 - 1.9.1 + 1.9.2 1.9.1 - 1.9.1 - 1.9.1 + 1.9.2 + 1.9.2 1.9.1 1.9.1 1.9.1 diff --git a/bus/EasyCaching.Bus.Redis/EasyCaching.Bus.Redis.csproj b/bus/EasyCaching.Bus.Redis/EasyCaching.Bus.Redis.csproj index f6f506e2..9e0211e5 100644 --- a/bus/EasyCaching.Bus.Redis/EasyCaching.Bus.Redis.csproj +++ b/bus/EasyCaching.Bus.Redis/EasyCaching.Bus.Redis.csproj @@ -24,6 +24,7 @@ true $(NoWarn);1591 + Fix.Stampede.$(MSBuildProjectName) diff --git a/serialization/EasyCaching.Serialization.MessagePack/EasyCaching.Serialization.MessagePack.csproj b/serialization/EasyCaching.Serialization.MessagePack/EasyCaching.Serialization.MessagePack.csproj index 4584dc3e..9ebf2f9d 100644 --- a/serialization/EasyCaching.Serialization.MessagePack/EasyCaching.Serialization.MessagePack.csproj +++ b/serialization/EasyCaching.Serialization.MessagePack/EasyCaching.Serialization.MessagePack.csproj @@ -24,6 +24,7 @@ true $(NoWarn);1591 + Fix.Stampede.$(MSBuildProjectName) diff --git a/src/EasyCaching.CSRedis/DefaultCSRedisCachingProvider.Async.cs b/src/EasyCaching.CSRedis/DefaultCSRedisCachingProvider.Async.cs index b3c2ad34..0ed0ebdb 100644 --- a/src/EasyCaching.CSRedis/DefaultCSRedisCachingProvider.Async.cs +++ b/src/EasyCaching.CSRedis/DefaultCSRedisCachingProvider.Async.cs @@ -10,6 +10,10 @@ public partial class DefaultCSRedisCachingProvider : EasyCachingAbstractProvider { + public override async Task> BaseGetAsync(string cacheKey, Func> dataRetriever, Func> expirationRetriever, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } /// /// Existses the async. /// diff --git a/src/EasyCaching.Core/EasyCaching.Core.csproj b/src/EasyCaching.Core/EasyCaching.Core.csproj index 4e348f6b..2f0a7dad 100644 --- a/src/EasyCaching.Core/EasyCaching.Core.csproj +++ b/src/EasyCaching.Core/EasyCaching.Core.csproj @@ -24,6 +24,7 @@ true $(NoWarn);1591 + Fix.Stampede.$(MSBuildProjectName) diff --git a/src/EasyCaching.Core/EasyCachingAbstractProvider.cs b/src/EasyCaching.Core/EasyCachingAbstractProvider.cs index b4985128..ae678b31 100644 --- a/src/EasyCaching.Core/EasyCachingAbstractProvider.cs +++ b/src/EasyCaching.Core/EasyCachingAbstractProvider.cs @@ -54,6 +54,7 @@ protected EasyCachingAbstractProvider(IDistributedLockFactory lockFactory, BaseP public abstract IDictionary> BaseGetAll(IEnumerable cacheKeys); public abstract Task>> BaseGetAllAsync(IEnumerable cacheKeys, CancellationToken cancellationToken = default); public abstract Task> BaseGetAsync(string cacheKey, Func> dataRetriever, TimeSpan expiration, CancellationToken cancellationToken = default); + public abstract Task> BaseGetAsync(string cacheKey, Func> dataRetriever, Func> expirationRetriever, CancellationToken cancellationToken = default); public abstract Task BaseGetAsync(string cacheKey, Type type, CancellationToken cancellationToken = default); public abstract Task> BaseGetAsync(string cacheKey, CancellationToken cancellationToken = default); public abstract IDictionary> BaseGetByPrefix(string prefix); @@ -363,6 +364,66 @@ public async Task>> GetAllAsync(IEnumerable } } + public async Task> GetAsync(string cacheKey, Func> dataRetriever, Func> expirationRetriever, CancellationToken cancellationToken = default) + { + var operationId = s_diagnosticListener.WriteGetCacheBefore(new BeforeGetRequestEventData(CachingProviderType.ToString(), Name, nameof(GetAsync), new[] { cacheKey })); + Exception e = null; + try + { + if (_lockFactory == null) return await BaseGetAsync(cacheKey, dataRetriever, expirationRetriever, cancellationToken); + + var value = await BaseGetAsync(cacheKey, cancellationToken); + if (value.HasValue) return value; + + var @lock = _lockFactory.CreateLock(Name, $"{cacheKey}_Lock"); + try + { + if (!await @lock.LockAsync(_options.SleepMs, cancellationToken)) throw new TimeoutException(); + + value = await BaseGetAsync(cacheKey, cancellationToken); + if (value.HasValue) return value; + + var task = dataRetriever(); + if (!task.IsCompleted && + await Task.WhenAny(task, Task.Delay(_options.LockMs, cancellationToken)) != task) + throw new TimeoutException(); + + var item = await task; + if (item != null || _options.CacheNulls) + { + TimeSpan expiration = await expirationRetriever(); + await BaseSetAsync(cacheKey, item, expiration, cancellationToken); + + return new CacheValue(item, true); + } + else + { + return CacheValue.NoValue; + } + } + finally + { + await @lock.DisposeAsync(); + } + } + catch (Exception ex) + { + e = ex; + throw; + } + finally + { + if (e != null) + { + s_diagnosticListener.WriteGetCacheError(operationId, e); + } + else + { + s_diagnosticListener.WriteGetCacheAfter(operationId); + } + } + } + public async Task> GetAsync(string cacheKey, Func> dataRetriever, TimeSpan expiration, CancellationToken cancellationToken = default) { var operationId = s_diagnosticListener.WriteGetCacheBefore(new BeforeGetRequestEventData(CachingProviderType.ToString(), Name, nameof(GetAsync), new[] { cacheKey }, expiration)); diff --git a/src/EasyCaching.Core/IEasyCachingProviderBase.cs b/src/EasyCaching.Core/IEasyCachingProviderBase.cs index 3c431f21..a311dfa4 100644 --- a/src/EasyCaching.Core/IEasyCachingProviderBase.cs +++ b/src/EasyCaching.Core/IEasyCachingProviderBase.cs @@ -153,6 +153,17 @@ public interface IEasyCachingProviderBase /// The 1st type parameter. Task> GetAsync(string cacheKey, Func> dataRetriever, TimeSpan expiration, CancellationToken cancellationToken = default); + /// + /// Gets the specified cacheKey, dataRetriever and expirationRetriever async. + /// + /// The async. + /// Cache key. + /// Data retriever. + /// Expiration retriever. + /// + /// The 1st type parameter. + Task> GetAsync(string cacheKey, Func> dataRetriever, Func> expirationRetriever, CancellationToken cancellationToken = default); + /// /// Removes cached item by cachekey's prefix. /// diff --git a/src/EasyCaching.Disk/DefaultDiskCachingProvider.Async.cs b/src/EasyCaching.Disk/DefaultDiskCachingProvider.Async.cs index b7908e83..7c1ca908 100644 --- a/src/EasyCaching.Disk/DefaultDiskCachingProvider.Async.cs +++ b/src/EasyCaching.Disk/DefaultDiskCachingProvider.Async.cs @@ -10,7 +10,11 @@ using Microsoft.Extensions.Logging; public partial class DefaultDiskCachingProvider : EasyCachingAbstractProvider - { + { + public override async Task> BaseGetAsync(string cacheKey, Func> dataRetriever, Func> expirationRetriever, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } public override async Task BaseExistsAsync(string cacheKey, CancellationToken cancellationToken = default) { ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); diff --git a/src/EasyCaching.FasterKv/DefaultFasterKvCachingProvider.Async.cs b/src/EasyCaching.FasterKv/DefaultFasterKvCachingProvider.Async.cs index c3b04e37..309c188d 100644 --- a/src/EasyCaching.FasterKv/DefaultFasterKvCachingProvider.Async.cs +++ b/src/EasyCaching.FasterKv/DefaultFasterKvCachingProvider.Async.cs @@ -9,6 +9,10 @@ namespace EasyCaching.FasterKv { public partial class DefaultFasterKvCachingProvider { + public override async Task> BaseGetAsync(string cacheKey, Func> dataRetriever, Func> expirationRetriever, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } public override async Task BaseExistsAsync(string cacheKey, CancellationToken cancellationToken = default) { ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); diff --git a/src/EasyCaching.HybridCache/EasyCaching.HybridCache.csproj b/src/EasyCaching.HybridCache/EasyCaching.HybridCache.csproj index 6b6d4436..c304f19f 100644 --- a/src/EasyCaching.HybridCache/EasyCaching.HybridCache.csproj +++ b/src/EasyCaching.HybridCache/EasyCaching.HybridCache.csproj @@ -24,6 +24,7 @@ true $(NoWarn);1591 + Fix.Stampede.$(MSBuildProjectName) diff --git a/src/EasyCaching.HybridCache/HybridCachingProvider.cs b/src/EasyCaching.HybridCache/HybridCachingProvider.cs index 0d63644a..a0614f3d 100644 --- a/src/EasyCaching.HybridCache/HybridCachingProvider.cs +++ b/src/EasyCaching.HybridCache/HybridCachingProvider.cs @@ -304,42 +304,26 @@ public CacheValue Get(string cacheKey) public async Task> GetAsync(string cacheKey, CancellationToken cancellationToken = default) { ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); - - var cacheValue = await _localCache.GetAsync(cacheKey, cancellationToken); - - if (cacheValue.HasValue) - { - return cacheValue; - } - - LogMessage($"local cache can not get the value of {cacheKey}"); - - try - { - cacheValue = await _distributedCache.GetAsync(cacheKey, cancellationToken); - } - catch (Exception ex) - { - LogMessage($"distributed cache get error, [{cacheKey}]", ex); - - if (_options.ThrowIfDistributedCacheError) + var result = await _localCache.GetAsync( + cacheKey, + async () => { - throw; - } - } - - if (cacheValue.HasValue) - { - TimeSpan ts = await GetExpirationAsync(cacheKey, cancellationToken); - - await _localCache.SetAsync(cacheKey, cacheValue.Value, ts, cancellationToken); - - return cacheValue; - } - - LogMessage($"distributed cache can not get the value of {cacheKey}"); - - return CacheValue.NoValue; + var value = default(T); + try + { + value = (await _distributedCache.GetAsync(cacheKey, cancellationToken)).Value; + } + catch (Exception ex) + { + LogMessage($"distributed cache get error, [{cacheKey}]", ex); + if (_options.ThrowIfDistributedCacheError) + throw; + } + return value; + }, + () => GetExpirationAsync(cacheKey, cancellationToken), + cancellationToken); + return result; } /// @@ -780,6 +764,21 @@ public async Task> GetAsync(string cacheKey, Func> data return CacheValue.NoValue; } + /// + /// Gets the specified cacheKey, dataRetriever and expirationRetriever async. + /// + /// The async. + /// Cache key. + /// Data retriever. + /// Expiration retriever. + /// + /// The 1st type parameter. + public async Task> GetAsync(string cacheKey, Func> dataRetriever, Func> expirationRetriever, CancellationToken cancellationToken = default) + { + // TiSvc 沒用到,不實作 + throw new NotImplementedException(); + } + /// /// Removes the by prefix. /// diff --git a/src/EasyCaching.InMemory/DefaultInMemoryCachingProvider.Async.cs b/src/EasyCaching.InMemory/DefaultInMemoryCachingProvider.Async.cs index 96b7050e..cb8e9367 100644 --- a/src/EasyCaching.InMemory/DefaultInMemoryCachingProvider.Async.cs +++ b/src/EasyCaching.InMemory/DefaultInMemoryCachingProvider.Async.cs @@ -76,6 +76,48 @@ public override async Task> BaseGetAsync(string cacheKey, Func< } } + public override async Task> BaseGetAsync(string cacheKey, Func> dataRetriever, Func> expirationRetriever, CancellationToken cancellationToken = default) + { + ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); + bool getLock = false; + do + { + var result = _cache.Get(cacheKey); + if (result.HasValue) + return result; + getLock = _cache.Add($"{cacheKey}_Lock", 1, TimeSpan.FromMilliseconds(_options.LockMs)); + if (!getLock) + await Task.Delay(_options.SleepMs, cancellationToken); + } + while (!getLock); + try + { + var res = await dataRetriever(); + + if (res != null || _options.CacheNulls) + { + TimeSpan expiration = await expirationRetriever(); + await SetAsync(cacheKey, res, expiration, cancellationToken); + //remove mutex key + _cache.Remove($"{cacheKey}_Lock"); + + return new CacheValue(res, true); + } + else + { + //remove mutex key + _cache.Remove($"{cacheKey}_Lock"); + return CacheValue.NoValue; + } + } + catch + { + //remove mutex key + _cache.Remove($"{cacheKey}_Lock"); + throw; + } + } + /// /// Gets the specified cacheKey async. /// diff --git a/src/EasyCaching.InMemory/EasyCaching.InMemory.csproj b/src/EasyCaching.InMemory/EasyCaching.InMemory.csproj index 2d1dd05d..cad52f15 100644 --- a/src/EasyCaching.InMemory/EasyCaching.InMemory.csproj +++ b/src/EasyCaching.InMemory/EasyCaching.InMemory.csproj @@ -24,6 +24,7 @@ true $(NoWarn);1591 + Fix.Stampede.$(MSBuildProjectName) diff --git a/src/EasyCaching.LiteDB/DefaultLiteDBCachingProvider.Async.cs b/src/EasyCaching.LiteDB/DefaultLiteDBCachingProvider.Async.cs index 466c95fb..97e35905 100644 --- a/src/EasyCaching.LiteDB/DefaultLiteDBCachingProvider.Async.cs +++ b/src/EasyCaching.LiteDB/DefaultLiteDBCachingProvider.Async.cs @@ -12,6 +12,10 @@ /// public partial class DefaultLiteDBCachingProvider : EasyCachingAbstractProvider { + public override async Task> BaseGetAsync(string cacheKey, Func> dataRetriever, Func> expirationRetriever, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } /// /// Existses the specified cacheKey async. /// diff --git a/src/EasyCaching.Memcached/DefaultMemcachedCachingProvider.Async.cs b/src/EasyCaching.Memcached/DefaultMemcachedCachingProvider.Async.cs index 1d96f1d5..145028b4 100644 --- a/src/EasyCaching.Memcached/DefaultMemcachedCachingProvider.Async.cs +++ b/src/EasyCaching.Memcached/DefaultMemcachedCachingProvider.Async.cs @@ -13,6 +13,10 @@ /// public partial class DefaultMemcachedCachingProvider : EasyCachingAbstractProvider { + public override async Task> BaseGetAsync(string cacheKey, Func> dataRetriever, Func> expirationRetriever, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } /// /// Gets the specified cacheKey, dataRetriever and expiration async. /// diff --git a/src/EasyCaching.Redis/DefaultRedisCachingProvider.Async.cs b/src/EasyCaching.Redis/DefaultRedisCachingProvider.Async.cs index 3604b5df..edd4d50f 100644 --- a/src/EasyCaching.Redis/DefaultRedisCachingProvider.Async.cs +++ b/src/EasyCaching.Redis/DefaultRedisCachingProvider.Async.cs @@ -103,6 +103,53 @@ public override async Task> BaseGetAsync(string cacheKey, Func< } } + public override async Task> BaseGetAsync(string cacheKey, Func> dataRetriever, Func> expirationRetriever, CancellationToken cancellationToken = default) + { + ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); + + var result = await _cache.StringGetAsync(cacheKey); + if (!result.IsNull) + { + CacheStats.OnHit(); + + if (_options.EnableLogging) + _logger?.LogInformation($"Cache Hit : cachekey = {cacheKey}"); + + var value = _serializer.Deserialize(result); + return new CacheValue(value, true); + } + + CacheStats.OnMiss(); + + if (_options.EnableLogging) + _logger?.LogInformation($"Cache Missed : cachekey = {cacheKey}"); + + var flag = await _cache.StringSetAsync($"{cacheKey}_Lock", 1, TimeSpan.FromMilliseconds(_options.LockMs), When.NotExists); + + if (!flag) + { + await Task.Delay(_options.SleepMs, cancellationToken); + return await GetAsync(cacheKey, dataRetriever, expirationRetriever, cancellationToken); + } + + var item = await dataRetriever(); + if (item != null || _options.CacheNulls) + { + TimeSpan expiration = await expirationRetriever(); + await SetAsync(cacheKey, item, expiration, cancellationToken); + + //remove mutex key + await _cache.KeyDeleteAsync($"{cacheKey}_Lock"); + return new CacheValue(item, true); + } + else + { + //remove mutex key + await _cache.KeyDeleteAsync($"{cacheKey}_Lock"); + return CacheValue.NoValue; + } + } + /// /// Gets the specified cacheKey async. /// diff --git a/src/EasyCaching.Redis/EasyCaching.Redis.csproj b/src/EasyCaching.Redis/EasyCaching.Redis.csproj index 3c526a83..21258293 100644 --- a/src/EasyCaching.Redis/EasyCaching.Redis.csproj +++ b/src/EasyCaching.Redis/EasyCaching.Redis.csproj @@ -25,6 +25,7 @@ true $(NoWarn);1591 + Fix.Stampede.$(MSBuildProjectName) diff --git a/src/EasyCaching.SQLite/DefaultSQLiteCachingProvider.Async.cs b/src/EasyCaching.SQLite/DefaultSQLiteCachingProvider.Async.cs index 5463aec2..a4d09f6b 100644 --- a/src/EasyCaching.SQLite/DefaultSQLiteCachingProvider.Async.cs +++ b/src/EasyCaching.SQLite/DefaultSQLiteCachingProvider.Async.cs @@ -14,6 +14,10 @@ /// public partial class DefaultSQLiteCachingProvider : EasyCachingAbstractProvider { + public override async Task> BaseGetAsync(string cacheKey, Func> dataRetriever, Func> expirationRetriever, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } /// /// Existses the specified cacheKey async. /// diff --git a/test/EasyCaching.UnitTests/Diagnostics/MyCachingProvider.cs b/test/EasyCaching.UnitTests/Diagnostics/MyCachingProvider.cs index 81257398..3efc185c 100644 --- a/test/EasyCaching.UnitTests/Diagnostics/MyCachingProvider.cs +++ b/test/EasyCaching.UnitTests/Diagnostics/MyCachingProvider.cs @@ -18,6 +18,11 @@ public MyCachingProvider() this.IsDistributedProvider = false; } + public override async Task> BaseGetAsync(string cacheKey, Func> dataRetriever, Func> expirationRetriever, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + public override bool BaseExists(string cacheKey) { return true; diff --git a/test/EasyCaching.UnitTests/Fake/FakeDistributedCachingProvider.cs b/test/EasyCaching.UnitTests/Fake/FakeDistributedCachingProvider.cs index 67b750dc..820b2412 100644 --- a/test/EasyCaching.UnitTests/Fake/FakeDistributedCachingProvider.cs +++ b/test/EasyCaching.UnitTests/Fake/FakeDistributedCachingProvider.cs @@ -205,5 +205,10 @@ public virtual Task TrySetAsync(string cacheKey, T cacheValue, TimeSpan { return Task.FromResult(true); } + + public Task> GetAsync(string cacheKey, Func> dataRetriever, Func> expirationRetriever, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } } } diff --git a/test/EasyCaching.UnitTests/Fake/FakeLocalCachingProvider.cs b/test/EasyCaching.UnitTests/Fake/FakeLocalCachingProvider.cs index 9638084f..c7cc7606 100644 --- a/test/EasyCaching.UnitTests/Fake/FakeLocalCachingProvider.cs +++ b/test/EasyCaching.UnitTests/Fake/FakeLocalCachingProvider.cs @@ -205,5 +205,10 @@ public Task TrySetAsync(string cacheKey, T cacheValue, TimeSpan expirat { return Task.FromResult(true); } + + public Task> GetAsync(string cacheKey, Func> dataRetriever, Func> expirationRetriever, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } } }