From 5b093dc309989dbafb92d050be65ccddd3876c6a Mon Sep 17 00:00:00 2001 From: InCerryGit <36690780+InCerryGit@users.noreply.github.com> Date: Thu, 17 Nov 2022 23:11:02 +0800 Subject: [PATCH] feat(FasterKv): Add FasterKv support (#418) * feat(FasterKv): add FasterKv caching provider support. * fix wrong summary --- EasyCaching.sln | 7 + .../Controllers/ValuesController.cs | 2 +- .../EasyCaching.Demo.Providers.csproj | 1 + sample/EasyCaching.Demo.Providers/Startup.cs | 10 +- .../appsettings.json | 4 + .../Internal/CachingProviderType.cs | 1 + .../Internal/EasyCachingConstValue.cs | 36 +- .../ClientSessionPooledObjectPolicy.cs | 27 ++ .../Configurations/FasterKvCachingOptions.cs | 74 ++++ .../FasterKvCachingOptionsExtensions.cs | 68 ++++ .../FasterKvOptionsExtension.cs | 61 +++ .../DefaultFasterKvCachingProvider.Async.cs | 223 +++++++++++ .../DefaultFasterKvCachingProvider.cs | 377 ++++++++++++++++++ .../EasyCaching.FasterKv.csproj | 18 + src/EasyCaching.FasterKv/StoreFunctions.cs | 34 ++ .../EasyCaching.PerformanceTests.csproj | 6 +- .../FasterKvBenchmark.cs | 141 +++++++ test/EasyCaching.PerformanceTests/Program.cs | 3 +- .../CachingTests/BaseCachingProviderTest.cs | 8 +- .../FasterKvCachingProviderTest.cs | 164 ++++++++ .../EasyCaching.UnitTests.csproj | 3 +- 21 files changed, 1245 insertions(+), 23 deletions(-) create mode 100644 src/EasyCaching.FasterKv/ClientSessionPooledObjectPolicy.cs create mode 100644 src/EasyCaching.FasterKv/Configurations/FasterKvCachingOptions.cs create mode 100644 src/EasyCaching.FasterKv/Configurations/FasterKvCachingOptionsExtensions.cs create mode 100644 src/EasyCaching.FasterKv/Configurations/FasterKvOptionsExtension.cs create mode 100644 src/EasyCaching.FasterKv/DefaultFasterKvCachingProvider.Async.cs create mode 100644 src/EasyCaching.FasterKv/DefaultFasterKvCachingProvider.cs create mode 100644 src/EasyCaching.FasterKv/EasyCaching.FasterKv.csproj create mode 100644 src/EasyCaching.FasterKv/StoreFunctions.cs create mode 100644 test/EasyCaching.PerformanceTests/FasterKvBenchmark.cs create mode 100644 test/EasyCaching.UnitTests/CachingTests/FasterKvCachingProviderTest.cs diff --git a/EasyCaching.sln b/EasyCaching.sln index ff571815..3f2504a3 100644 --- a/EasyCaching.sln +++ b/EasyCaching.sln @@ -74,6 +74,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyCaching.Bus.ConfluentKa EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyCaching.Bus.Zookeeper", "bus\EasyCaching.Bus.Zookeeper\EasyCaching.Bus.Zookeeper.csproj", "{5E488583-391E-4E15-83C1-7301B4FE79AE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyCaching.FasterKv", "src\EasyCaching.FasterKv\EasyCaching.FasterKv.csproj", "{7191E567-38DF-4879-82E1-73EC618AFCAC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -196,6 +198,10 @@ Global {5E488583-391E-4E15-83C1-7301B4FE79AE}.Debug|Any CPU.Build.0 = Debug|Any CPU {5E488583-391E-4E15-83C1-7301B4FE79AE}.Release|Any CPU.ActiveCfg = Release|Any CPU {5E488583-391E-4E15-83C1-7301B4FE79AE}.Release|Any CPU.Build.0 = Release|Any CPU + {7191E567-38DF-4879-82E1-73EC618AFCAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7191E567-38DF-4879-82E1-73EC618AFCAC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7191E567-38DF-4879-82E1-73EC618AFCAC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7191E567-38DF-4879-82E1-73EC618AFCAC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -230,6 +236,7 @@ Global {4FCF16BF-5E21-4B74-AB45-3C121ADF1485} = {15070C49-A507-4844-BCFE-D319CFBC9A63} {F7FBADEB-D766-4595-949A-07104B52692C} = {B337509B-75F9-4851-821F-9BBE87C4E4BC} {5E488583-391E-4E15-83C1-7301B4FE79AE} = {B337509B-75F9-4851-821F-9BBE87C4E4BC} + {7191E567-38DF-4879-82E1-73EC618AFCAC} = {A0F5CC7E-155F-4726-8DEB-E966950B3FE9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {63A57886-054B-476C-AAE1-8D7C8917682E} diff --git a/sample/EasyCaching.Demo.Providers/Controllers/ValuesController.cs b/sample/EasyCaching.Demo.Providers/Controllers/ValuesController.cs index d08da6a7..d7f8878a 100644 --- a/sample/EasyCaching.Demo.Providers/Controllers/ValuesController.cs +++ b/sample/EasyCaching.Demo.Providers/Controllers/ValuesController.cs @@ -8,7 +8,7 @@ [Route("api/[controller]")] public class ValuesController : Controller { - //1. InMemory,Memcached,Redis,SQLite + //1. InMemory,Memcached,Redis,SQLite,FasterKv private readonly IEasyCachingProvider _provider; public ValuesController(IEasyCachingProvider provider) diff --git a/sample/EasyCaching.Demo.Providers/EasyCaching.Demo.Providers.csproj b/sample/EasyCaching.Demo.Providers/EasyCaching.Demo.Providers.csproj index 0fb6da6f..116c4206 100644 --- a/sample/EasyCaching.Demo.Providers/EasyCaching.Demo.Providers.csproj +++ b/sample/EasyCaching.Demo.Providers/EasyCaching.Demo.Providers.csproj @@ -7,6 +7,7 @@ + diff --git a/sample/EasyCaching.Demo.Providers/Startup.cs b/sample/EasyCaching.Demo.Providers/Startup.cs index 9f9e30f5..5372e2ae 100644 --- a/sample/EasyCaching.Demo.Providers/Startup.cs +++ b/sample/EasyCaching.Demo.Providers/Startup.cs @@ -61,6 +61,14 @@ public void ConfigureServices(IServiceCollection services) }); option.UseMemcached(Configuration); + + //use fasterKv + option.UseFasterKv(config => + { + // fasterKv must be set SerializerName + config.SerializerName = "msg"; + }) + .WithMessagePack("msg"); }); } @@ -84,4 +92,4 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerF }); } } -} +} diff --git a/sample/EasyCaching.Demo.Providers/appsettings.json b/sample/EasyCaching.Demo.Providers/appsettings.json index 85b08a94..9184d1a9 100644 --- a/sample/EasyCaching.Demo.Providers/appsettings.json +++ b/sample/EasyCaching.Demo.Providers/appsettings.json @@ -76,6 +76,10 @@ "GroupId": "MyGroupId" }, "ConsumerCount":2 + }, + "fasterKv": { + "MemorySizeBit": 16, + "PageSizeBit": 15 } } } diff --git a/src/EasyCaching.Core/Internal/CachingProviderType.cs b/src/EasyCaching.Core/Internal/CachingProviderType.cs index c7ff8d1b..ed1d8f27 100644 --- a/src/EasyCaching.Core/Internal/CachingProviderType.cs +++ b/src/EasyCaching.Core/Internal/CachingProviderType.cs @@ -13,5 +13,6 @@ public enum CachingProviderType Ext1, Ext2, LiteDB, + FasterKv, } } diff --git a/src/EasyCaching.Core/Internal/EasyCachingConstValue.cs b/src/EasyCaching.Core/Internal/EasyCachingConstValue.cs index 4e471456..342a0ae4 100644 --- a/src/EasyCaching.Core/Internal/EasyCachingConstValue.cs +++ b/src/EasyCaching.Core/Internal/EasyCachingConstValue.cs @@ -53,13 +53,13 @@ public class EasyCachingConstValue /// /// The rabbitMQ Bus section. /// - public const string RabbitMQBusSection = "easycaching:rabbitmqbus"; - + public const string RabbitMQBusSection = "easycaching:rabbitmqbus"; + /// /// The kafka bus section. /// - public const string KafkaBusSection = "easycaching:kafkabus"; - + public const string KafkaBusSection = "easycaching:kafkabus"; + /// /// The zookeeper bus section. /// @@ -104,19 +104,29 @@ public class EasyCachingConstValue /// /// The default name of the serializer. /// - public const string DefaultSerializerName = "binary"; - + public const string DefaultSerializerName = "binary"; + /// /// The default name of the LiteDB. /// - public const string DefaultLiteDBName = "DefaultLiteDB"; + public const string DefaultLiteDBName = "DefaultLiteDB"; /// /// The LiteDB Bus section. - /// - public const string LiteDBSection= "easycaching:litedb"; - - public const string NotFoundCliExceptionMessage = "Can not find the matched client instance, client name is {0}"; - - public const string NotFoundSerExceptionMessage = "Can not find the matched serializer instance, serializer name is {0}"; + /// + public const string LiteDBSection= "easycaching:litedb"; + + /// + /// The default name of the FasterKv + /// + public const string DefaultFasterKvName = "DefaultFasterKvName"; + + /// + /// The FasterKv section. + /// + public const string FasterKvSection= "easycaching:fasterKv"; + + public const string NotFoundCliExceptionMessage = "Can not find the matched client instance, client name is {0}"; + + public const string NotFoundSerExceptionMessage = "Can not find the matched serializer instance, serializer name is {0}"; } } diff --git a/src/EasyCaching.FasterKv/ClientSessionPooledObjectPolicy.cs b/src/EasyCaching.FasterKv/ClientSessionPooledObjectPolicy.cs new file mode 100644 index 00000000..75cd7e7b --- /dev/null +++ b/src/EasyCaching.FasterKv/ClientSessionPooledObjectPolicy.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Concurrent; +using FASTER.core; + +namespace EasyCaching.FasterKv +{ + public class ClientSessionWrap : IDisposable + { + public ClientSession Session { get; } + + private readonly ConcurrentQueue> _innerPool; + + public ClientSessionWrap( + ClientSession clientSession, + ConcurrentQueue> innerPool) + { + Session = clientSession; + _innerPool = innerPool; + } + + public void Dispose() + { + Session.CompletePending(true); + _innerPool.Enqueue(Session); + } + } +} \ No newline at end of file diff --git a/src/EasyCaching.FasterKv/Configurations/FasterKvCachingOptions.cs b/src/EasyCaching.FasterKv/Configurations/FasterKvCachingOptions.cs new file mode 100644 index 00000000..325ff15f --- /dev/null +++ b/src/EasyCaching.FasterKv/Configurations/FasterKvCachingOptions.cs @@ -0,0 +1,74 @@ +using System; +using System.IO; +using EasyCaching.Core.Configurations; +using FASTER.core; + +namespace EasyCaching.FasterKv.Configurations +{ + /// + /// FasterKvCachingOptions + /// for details, see https://microsoft.github.io/FASTER/docs/fasterkv-basics/#fasterkvsettings + /// + public class FasterKvCachingOptions : BaseProviderOptions + { + /// + /// FasterKv index count, Must be power of 2 + /// + /// For example: 1024(2^10) 2048(2^11) 65536(2^16) 131072(2^17) + /// Each index is 64 bits. So this define 131072 keys. Used 1024Kb memory + public long IndexCount { get; set; } = 131072; + + /// + /// FasterKv used memory size (default: 16MB) + /// + public int MemorySizeBit { get; set; } = 24; + + /// + /// FasterKv page size (default: 1MB) + /// + public int PageSizeBit { get; set; } = 20; + + /// + /// FasterKv read cache used memory size (default: 16MB) + /// + public int ReadCacheMemorySizeBit { get; set; } = 24; + + /// + /// FasterKv read cache page size (default: 16MB) + /// + public int ReadCachePageSizeBit { get; set; } = 20; + + /// + /// FasterKv commit logs path + /// + public string LogPath { get; set; } = +#if (NET6_0 || NET7_0) + Path.Combine(Environment.CurrentDirectory, $"EasyCaching-FasterKv-{Environment.ProcessId}"); +#else + Path.Combine(Environment.CurrentDirectory, $"EasyCaching-FasterKv-{System.Diagnostics.Process.GetCurrentProcess().Id}"); +#endif + + + /// + /// Set Custom Store + /// + public FasterKV? CustomStore { get; set; } + + internal LogSettings GetLogSettings(string name) + { + return new LogSettings + { + LogDevice = Devices.CreateLogDevice(Path.Combine(LogPath, name), + preallocateFile: true, + deleteOnClose: true), + PageSizeBits = PageSizeBit, + MemorySizeBits = MemorySizeBit, + ReadCacheSettings = new ReadCacheSettings + { + MemorySizeBits = ReadCacheMemorySizeBit, + PageSizeBits = ReadCachePageSizeBit, + } + }; + } + } +} \ No newline at end of file diff --git a/src/EasyCaching.FasterKv/Configurations/FasterKvCachingOptionsExtensions.cs b/src/EasyCaching.FasterKv/Configurations/FasterKvCachingOptionsExtensions.cs new file mode 100644 index 00000000..d1d770da --- /dev/null +++ b/src/EasyCaching.FasterKv/Configurations/FasterKvCachingOptionsExtensions.cs @@ -0,0 +1,68 @@ +using System; +using EasyCaching.Core; +using EasyCaching.Core.Configurations; +using EasyCaching.FasterKv; +using EasyCaching.FasterKv.Configurations; +using Microsoft.Extensions.Configuration; +// ReSharper disable CheckNamespace + +namespace Microsoft.Extensions.DependencyInjection; + +public static class FasterKvCachingOptionsExtensions +{ + /// + /// Uses the FasterKv provider (specify the config via hard code). + /// + /// Options. + /// Configure provider settings. + /// The name of this provider instance. + public static EasyCachingOptions UseFasterKv( + this EasyCachingOptions options, + Action configure, + string name = EasyCachingConstValue.DefaultFasterKvName + ) + { + ArgumentCheck.NotNull(configure, nameof(configure)); + + options.RegisterExtension(new FasterKvOptionsExtension(name, configure)); + return options; + } + + /// + /// Uses the FasterKv provider (read config from configuration file). + /// + /// Options. + /// The configuration. + /// The name of this provider instance. + /// The section name in the configuration file. + public static EasyCachingOptions UseFasterKv( + this EasyCachingOptions options, + IConfiguration configuration, + string name = EasyCachingConstValue.DefaultFasterKvName, + string sectionName = EasyCachingConstValue.FasterKvSection + ) + { + var dbConfig = configuration.GetSection(sectionName); + var fasterKvOptions = new FasterKvCachingOptions(); + dbConfig.Bind(fasterKvOptions); + + void Configure(FasterKvCachingOptions x) + { + x.EnableLogging = fasterKvOptions.EnableLogging; + x.MaxRdSecond = fasterKvOptions.MaxRdSecond; + x.LockMs = fasterKvOptions.LockMs; + x.SleepMs = fasterKvOptions.SleepMs; + x.SerializerName = fasterKvOptions.SerializerName; + x.CacheNulls = fasterKvOptions.CacheNulls; + x.IndexCount = fasterKvOptions.IndexCount; + x.MemorySizeBit = fasterKvOptions.MemorySizeBit; + x.PageSizeBit = fasterKvOptions.PageSizeBit; + x.ReadCacheMemorySizeBit = fasterKvOptions.ReadCacheMemorySizeBit; + x.ReadCachePageSizeBit = fasterKvOptions.ReadCachePageSizeBit; + x.LogPath = fasterKvOptions.LogPath; + } + + options.RegisterExtension(new FasterKvOptionsExtension(name, Configure)); + return options; + } +} \ No newline at end of file diff --git a/src/EasyCaching.FasterKv/Configurations/FasterKvOptionsExtension.cs b/src/EasyCaching.FasterKv/Configurations/FasterKvOptionsExtension.cs new file mode 100644 index 00000000..5f956666 --- /dev/null +++ b/src/EasyCaching.FasterKv/Configurations/FasterKvOptionsExtension.cs @@ -0,0 +1,61 @@ +using System; +using EasyCaching.Core; +using EasyCaching.Core.Configurations; +using EasyCaching.Core.Serialization; +using EasyCaching.FasterKv.Configurations; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace EasyCaching.FasterKv +{ + /// + /// FasterKv options extension. + /// + internal sealed class FasterKvOptionsExtension : IEasyCachingOptionsExtension + { + /// + /// The name. + /// + private readonly string _name; + + /// + /// The configure. + /// + private readonly Action _configure; + + /// + /// Initializes a new instance of the class. + /// + /// Name. + /// Configure. + public FasterKvOptionsExtension(string name, Action configure) + { + _name = name; + _configure = configure; + } + + /// + /// Adds the services. + /// + /// Services. + public void AddServices(IServiceCollection services) + { + services.AddOptions(); + + services.Configure(_name, _configure); + + services.TryAddSingleton(); + + services.AddSingleton(x => + { + var optionsMon = x.GetRequiredService>(); + var options = optionsMon.Get(_name); + var factory = x.GetService(); + var serializers = x.GetServices(); + return new DefaultFasterKvCachingProvider(_name, options, serializers, factory); + }); + } + } +} diff --git a/src/EasyCaching.FasterKv/DefaultFasterKvCachingProvider.Async.cs b/src/EasyCaching.FasterKv/DefaultFasterKvCachingProvider.Async.cs new file mode 100644 index 00000000..80393de0 --- /dev/null +++ b/src/EasyCaching.FasterKv/DefaultFasterKvCachingProvider.Async.cs @@ -0,0 +1,223 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using EasyCaching.Core; +using Microsoft.Extensions.Logging; + +namespace EasyCaching.FasterKv +{ + public partial class DefaultFasterKvCachingProvider + { + public override async Task BaseExistsAsync(string cacheKey, CancellationToken cancellationToken = default) + { + ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); + using var session = GetSession(); + var result = (await session.Session.ReadAsync(GetSpanByte(cacheKey), token: cancellationToken)).Complete(); + return result.status.Found; + } + + + public override async Task BaseFlushAsync(CancellationToken cancellationToken = default) + { + EnsureNotDispose(); + using var session = GetSession(); + using var iter = session.Session.Iterate(); + while (iter.GetNext(out var recordInfo)) + { + await session.Session.DeleteAsync(ref iter.GetKey(), token: cancellationToken).ConfigureAwait(false); + } + } + + + public override async Task>> BaseGetAllAsync(IEnumerable cacheKeys, + CancellationToken cancellationToken = default) + { + ArgumentCheck.NotNullAndCountGTZero(cacheKeys, nameof(cacheKeys)); + EnsureNotDispose(); + + using var session = GetSession(); + var dic = new Dictionary>(); + foreach (var cacheKey in cacheKeys) + { + dic[cacheKey] = await BaseGetInternalAsync(session, cacheKey, cancellationToken); + } + + return dic; + } + + public override async Task> BaseGetAsync(string cacheKey, Func> dataRetriever, + TimeSpan expiration, + CancellationToken cancellationToken = default) + { + ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); + ArgumentCheck.NotNegativeOrZero(expiration, nameof(expiration)); + EnsureNotDispose(); + + using var session = GetSession(); + var result = await BaseGetInternalAsync(session, cacheKey, cancellationToken); + if (result.HasValue) + { + return result; + } + + var item = await dataRetriever(); + if (item is not null || _options.CacheNulls) + { + await SetAsync(cacheKey, item, expiration, cancellationToken); + return new CacheValue(item, true); + } + + return CacheValue.NoValue; + } + + public override async Task BaseGetAsync(string cacheKey, Type type, + CancellationToken cancellationToken = default) + { + ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); + EnsureNotDispose(); + + using var session = GetSession(); + var result = await BaseGetAsync(cacheKey, cancellationToken); + return Convert.ChangeType(result.Value, type); + } + + public override async Task> BaseGetAsync(string cacheKey, + CancellationToken cancellationToken = default) + { + ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); + EnsureNotDispose(); + + using var session = GetSession(); + return await BaseGetInternalAsync(session, cacheKey, cancellationToken); + } + + public override async Task BaseRemoveAllAsync(IEnumerable cacheKeys, CancellationToken cancellation = default) + { + ArgumentCheck.NotNullAndCountGTZero(cacheKeys, nameof(cacheKeys)); + EnsureNotDispose(); + + using var session = GetSession(); + foreach (var cacheKey in cacheKeys) + { + await session.Session.DeleteAsync(GetSpanByte(cacheKey), token: cancellation).ConfigureAwait(false); + } + } + + public override async Task BaseRemoveAsync(string cacheKey, CancellationToken cancellationToken = default) + { + ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); + EnsureNotDispose(); + + using var session = GetSession(); + await session.Session.DeleteAsync(GetSpanByte(cacheKey), token: cancellationToken).ConfigureAwait(false); + } + + + public override async Task BaseSetAllAsync(IDictionary values, TimeSpan expiration, + CancellationToken cancellationToken = default) + { + ArgumentCheck.NotNegativeOrZero(expiration, nameof(expiration)); + ArgumentCheck.NotNullAndCountGTZero(values, nameof(values)); + EnsureNotDispose(); + + using var session = GetSession(); + foreach (var kp in values) + { + await BaseSetInternalAsync(session, kp.Key, kp.Value, cancellationToken); + } + } + + public override async Task BaseSetAsync(string cacheKey, T cacheValue, TimeSpan expiration, + CancellationToken cancellationToken = default) + { + ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); + ArgumentCheck.NotNull(cacheValue, nameof(cacheValue), _options.CacheNulls); + ArgumentCheck.NotNegativeOrZero(expiration, nameof(expiration)); + EnsureNotDispose(); + + using var session = GetSession(); + await BaseSetInternalAsync(session, cacheKey, cacheValue, cancellationToken); + } + + public override async Task BaseTrySetAsync(string cacheKey, T cacheValue, TimeSpan expiration, + CancellationToken cancellationToken = default) + { + ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); + ArgumentCheck.NotNull(cacheValue, nameof(cacheValue), _options.CacheNulls); + ArgumentCheck.NotNegativeOrZero(expiration, nameof(expiration)); + EnsureNotDispose(); + + using var session = GetSession(); + var result = await BaseGetAsync(cacheKey, cancellationToken); + if (result.HasValue == false) + { + await BaseSetInternalAsync(session, cacheKey, cacheValue, cancellationToken); + return true; + } + + return false; + } + + private async Task BaseSetInternalAsync(ClientSessionWrap sessionWarp, string cacheKey, T cacheValue, + CancellationToken cancellationToken) + { + _ = await (await sessionWarp.Session.UpsertAsync(GetSpanByte(cacheKey), + GetSpanByte(cacheValue), token: cancellationToken) + .ConfigureAwait(false)).CompleteAsync(cancellationToken); + } + + + private async Task> BaseGetInternalAsync(ClientSessionWrap session, string cacheKey, CancellationToken cancellationToken) + { + var result = (await session.Session.ReadAsync(GetSpanByte(cacheKey), + token: cancellationToken) + .ConfigureAwait(false)).Complete(); + if (result.status.Found) + { + if (_options.EnableLogging) + _logger?.LogInformation("Cache Hit : cacheKey = {CacheKey}", cacheKey); + + CacheStats.OnHit(); + + return new CacheValue(GetTValue(ref result.output), true); + } + + CacheStats.OnMiss(); + if (_options.EnableLogging) + _logger?.LogInformation("Cache Missed : cacheKey = {CacheKey}", cacheKey); + + return CacheValue.NoValue; + } + + // Operations not currently supported by FasterKv + #region NotSupprotOperate + public override Task>> BaseGetByPrefixAsync(string prefix, + CancellationToken cancellationToken = default) + { + throw new NotSupportedException("BaseGetByPrefixAsync is not supported in FasterKv provider."); + } + + public override Task BaseGetCountAsync(string prefix = "", CancellationToken cancellationToken = default) + { + throw new NotSupportedException("BaseGetCountAsync is not supported in FasterKv provider."); + } + + public override Task BaseRemoveByPrefixAsync(string prefix, CancellationToken cancellationToken = default) + { + throw new NotSupportedException("BaseRemoveByPrefixAsync is not supported in FasterKv provider."); + } + + public override Task BaseRemoveByPatternAsync(string pattern, CancellationToken cancellationToken = default) + { + throw new NotSupportedException("BaseRemoveByPatternAsync is not supported in FasterKv provider."); + } + + public override Task BaseGetExpirationAsync(string cacheKey, + CancellationToken cancellationToken = default) + { + throw new NotSupportedException("BaseGetExpirationAsync is not supported in FasterKv provider."); + } + #endregion + } +} \ No newline at end of file diff --git a/src/EasyCaching.FasterKv/DefaultFasterKvCachingProvider.cs b/src/EasyCaching.FasterKv/DefaultFasterKvCachingProvider.cs new file mode 100644 index 00000000..f2e2e840 --- /dev/null +++ b/src/EasyCaching.FasterKv/DefaultFasterKvCachingProvider.cs @@ -0,0 +1,377 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using EasyCaching.Core; +using EasyCaching.Core.Serialization; +using EasyCaching.FasterKv.Configurations; +using FASTER.core; +using Microsoft.Extensions.Logging; + +namespace EasyCaching.FasterKv +{ + public sealed partial class DefaultFasterKvCachingProvider : EasyCachingAbstractProvider, IDisposable + { + // name + private readonly string _name; + private bool _disposed; + + // com + private readonly ILogger? _logger; + private readonly IEasyCachingSerializer _serializer; + private readonly FasterKvCachingOptions _options; + + // faster + private readonly FasterKV _fasterKv; + private readonly IDevice? _logDevice; + private readonly + ConcurrentQueue> _sessionPool; + + /// + /// The cache stats. + /// + private readonly CacheStats _cacheStats; + + private readonly ProviderInfo _info; + + ~DefaultFasterKvCachingProvider() + { + Dispose(false); + } + + public DefaultFasterKvCachingProvider( + string name, + FasterKvCachingOptions options, + IEnumerable serializers, + ILoggerFactory? loggerFactory = null) + { + ArgumentCheck.NotNull(options, nameof(options)); + ArgumentCheck.NotNull(serializers, nameof(serializers)); + + _name = name; + + _options = options; + _logger = loggerFactory?.CreateLogger(); + + if (_options.CustomStore is null) + { + var logSetting = options.GetLogSettings(name); + _logDevice = logSetting.LogDevice; + _fasterKv = new FasterKV(_options.IndexCount, logSetting, + loggerFactory: loggerFactory); + } + else + { + _fasterKv = _options.CustomStore; + } + + _sessionPool = + new ConcurrentQueue>(); + + var serName = !string.IsNullOrWhiteSpace(options.SerializerName) ? options.SerializerName : name; + _serializer = serializers.FirstOrDefault(x => x.Name.Equals(serName)) ?? + throw new EasyCachingNotFoundException(string.Format( + EasyCachingConstValue.NotFoundSerExceptionMessage, + serName)); + + _cacheStats = new CacheStats(); + ProviderName = _name; + ProviderType = CachingProviderType.FasterKv; + ProviderStats = _cacheStats; + ProviderMaxRdSecond = _options.MaxRdSecond; + IsDistributedProvider = false; + _info = new ProviderInfo + { + CacheStats = _cacheStats, + EnableLogging = options.EnableLogging, + IsDistributedProvider = IsDistributedProvider, + LockMs = options.LockMs, + MaxRdSecond = options.MaxRdSecond, + ProviderName = ProviderName, + ProviderType = ProviderType, + SerializerName = options.SerializerName, + SleepMs = options.SleepMs, + CacheNulls = options.CacheNulls + }; + } + + public override object BaseGetDatabase() + { + EnsureNotDispose(); + return _fasterKv; + } + + public override bool BaseExists(string cacheKey) + { + ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); + EnsureNotDispose(); + + using var sessionWarp = GetSession(); + var key = GetSpanByte(cacheKey); + var result = sessionWarp.Session.Read(key); + if (result.status.IsPending) + sessionWarp.Session.CompletePending(true); + return result.status.Found; + } + + + public override void BaseFlush() + { + EnsureNotDispose(); + using var session = GetSession(); + using var iter = session.Session.Iterate(); + while (iter.GetNext(out var recordInfo)) + { + session.Session.Delete(ref iter.GetKey()); + } + } + + + public override CacheValue BaseGet(string cacheKey, Func dataRetriever, TimeSpan expiration) + { + ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); + ArgumentCheck.NotNegativeOrZero(expiration, nameof(expiration)); + EnsureNotDispose(); + + using var session = GetSession(); + var result = BaseGetInternal(cacheKey, session); + if (result.HasValue) + { + return result; + } + + var item = dataRetriever(); + if (item is not null || _options.CacheNulls) + { + Set(cacheKey, item, expiration); + return new CacheValue(item, true); + } + + return CacheValue.NoValue; + } + + public override CacheValue BaseGet(string cacheKey) + { + ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); + EnsureNotDispose(); + + using var session = GetSession(); + return BaseGetInternal(cacheKey, session); + } + + public override IDictionary> BaseGetAll(IEnumerable cacheKeys) + { + ArgumentCheck.NotNullAndCountGTZero(cacheKeys, nameof(cacheKeys)); + EnsureNotDispose(); + + using var session = GetSession(); + var dic = new Dictionary>(); + foreach (var cacheKey in cacheKeys) + { + dic[cacheKey] = BaseGetInternal(cacheKey, session); + } + + return dic; + } + + + public override void BaseRemove(string cacheKey) + { + ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); + EnsureNotDispose(); + + using var session = GetSession(); + // ignore result + _ = session.Session.Delete(GetSpanByte(cacheKey)); + } + + public override void BaseRemoveAll(IEnumerable cacheKeys) + { + ArgumentCheck.NotNullAndCountGTZero(cacheKeys, nameof(cacheKeys)); + EnsureNotDispose(); + + using var session = GetSession(); + foreach (var cacheKey in cacheKeys) + { + _ = session.Session.Delete(GetSpanByte(cacheKey)); + } + } + + + public override void BaseSet(string cacheKey, T cacheValue, TimeSpan expiration) + { + ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); + ArgumentCheck.NotNull(cacheValue, nameof(cacheValue), _options.CacheNulls); + ArgumentCheck.NotNegativeOrZero(expiration, nameof(expiration)); + + EnsureNotDispose(); + + using var sessionWarp = GetSession(); + BaseSetInternal(sessionWarp, cacheKey, cacheValue); + } + + public override void BaseSetAll(IDictionary values, TimeSpan expiration) + { + ArgumentCheck.NotNegativeOrZero(expiration, nameof(expiration)); + ArgumentCheck.NotNullAndCountGTZero(values, nameof(values)); + + using var session = GetSession(); + foreach (var kp in values) + { + BaseSetInternal(session, kp.Key, kp.Value); + } + } + + public override bool BaseTrySet(string cacheKey, T cacheValue, TimeSpan expiration) + { + ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); + ArgumentCheck.NotNull(cacheValue, nameof(cacheValue), _options.CacheNulls); + ArgumentCheck.NotNegativeOrZero(expiration, nameof(expiration)); + EnsureNotDispose(); + + using var session = GetSession(); + var result = BaseGet(cacheKey); + if (result.HasValue == false) + { + BaseSetInternal(session, cacheKey, cacheValue); + return true; + } + + return false; + } + + + public override ProviderInfo BaseGetProviderInfo() + { + return _info; + } + + + + + private CacheValue BaseGetInternal(string cacheKey, ClientSessionWrap session) + { + var context = new StoreContext(); + var result = session.Session.Read(GetSpanByte(cacheKey), context); + if (result.status.IsPending) + { + session.Session.CompletePending(true); + context.FinalizeRead(out result.status, out result.output); + } + + if (result.status.Found) + { + CacheStats.OnHit(); + if (_options.EnableLogging) + _logger?.LogInformation("Cache Hit : cacheKey = {CacheKey}", cacheKey); + var value = GetTValue(ref result.output); + return new CacheValue(value, true); + } + + CacheStats.OnMiss(); + if (_options.EnableLogging) + _logger?.LogInformation("Cache Missed : cacheKey = {CacheKey}", cacheKey); + + return CacheValue.NoValue; + } + + private void BaseSetInternal(ClientSessionWrap sessionWarp, string cacheKey, T cacheValue) + { + var key = GetSpanByte(cacheKey); + var value = GetSpanByte(cacheValue); + _ = sessionWarp.Session.Upsert(key, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private SpanByte GetSpanByte(T value) + { + var bytes = _serializer.Serialize(value); + bytes.AsSpan().GetPinnableReference(); + return SpanByte.FromFixedSpan(bytes); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private T GetTValue(ref SpanByteAndMemory span) + { + return _serializer.Deserialize(span.Memory.Memory.ToArray()); + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnsureNotDispose() + { + if (_disposed) throw new ObjectDisposedException(nameof(DefaultFasterKvCachingProvider)); + } + + /// + /// Get ClientSession from pool + /// + /// + private ClientSessionWrap GetSession() + { + if (_sessionPool.TryDequeue(out var session) == false) + { + session = _fasterKv.For(new StoreFunctions()).NewSession(); + } + + return new ClientSessionWrap(session, _sessionPool); + } + + // Operations not currently supported by FasterKv + #region NotSupprotOperate + + public override TimeSpan BaseGetExpiration(string cacheKey) + { + throw new NotSupportedException("BaseGetExpiration is not supported in FasterKv provider."); + } + + public override IDictionary> BaseGetByPrefix(string prefix) + { + throw new NotSupportedException("BaseGetByPrefix is not supported in FasterKv provider."); + } + + public override int BaseGetCount(string prefix = "") + { + throw new NotSupportedException("BaseGetCount is not supported in FasterKv provider."); + } + + public override void BaseRemoveByPrefix(string prefix) + { + throw new NotSupportedException("BaseRemoveByPrefix is not supported in FasterKv provider."); + } + + public override void BaseRemoveByPattern(string pattern) + { + throw new NotSupportedException("BaseRemoveByPattern is not supported in FasterKv provider."); + } + + #endregion + + private void Dispose(bool _) + { + if (_disposed) + return; + + foreach (var session in _sessionPool) + { + session.Dispose(); + } + + _logDevice?.Dispose(); + if (_options.CustomStore != _fasterKv) + { + _fasterKv.Dispose(); + } + _disposed = true; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/src/EasyCaching.FasterKv/EasyCaching.FasterKv.csproj b/src/EasyCaching.FasterKv/EasyCaching.FasterKv.csproj new file mode 100644 index 00000000..99c28038 --- /dev/null +++ b/src/EasyCaching.FasterKv/EasyCaching.FasterKv.csproj @@ -0,0 +1,18 @@ + + + + netstandard2.0;net6.0 + 10 + enable + true + + + + + + + + + + + diff --git a/src/EasyCaching.FasterKv/StoreFunctions.cs b/src/EasyCaching.FasterKv/StoreFunctions.cs new file mode 100644 index 00000000..f7b8e9be --- /dev/null +++ b/src/EasyCaching.FasterKv/StoreFunctions.cs @@ -0,0 +1,34 @@ +using FASTER.core; + +namespace EasyCaching.FasterKv; + +public class StoreContext +{ + private Status _status; + private SpanByteAndMemory _output; + + internal void Populate(ref Status status, ref SpanByteAndMemory output) + { + _status = status; + _output = output; + } + + internal void FinalizeRead(out Status status, out SpanByteAndMemory output) + { + status = _status; + output = _output; + } +} + +public class StoreFunctions : SpanByteFunctions +{ + public override void ReadCompletionCallback(ref SpanByte key, + ref SpanByte input, + ref SpanByteAndMemory output, + StoreContext ctx, + Status status, + RecordMetadata recordMetadata) + { + ctx?.Populate(ref status, ref output); + } +} \ No newline at end of file diff --git a/test/EasyCaching.PerformanceTests/EasyCaching.PerformanceTests.csproj b/test/EasyCaching.PerformanceTests/EasyCaching.PerformanceTests.csproj index 3197bf99..cf989173 100644 --- a/test/EasyCaching.PerformanceTests/EasyCaching.PerformanceTests.csproj +++ b/test/EasyCaching.PerformanceTests/EasyCaching.PerformanceTests.csproj @@ -7,8 +7,8 @@ - - + + @@ -19,6 +19,8 @@ + + diff --git a/test/EasyCaching.PerformanceTests/FasterKvBenchmark.cs b/test/EasyCaching.PerformanceTests/FasterKvBenchmark.cs new file mode 100644 index 00000000..05e00ece --- /dev/null +++ b/test/EasyCaching.PerformanceTests/FasterKvBenchmark.cs @@ -0,0 +1,141 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Order; +using EasyCaching.Core; +using EasyCaching.FasterKv; +using EasyCaching.SQLite; +using Microsoft.Extensions.DependencyInjection; + +namespace EasyCaching.PerformanceTests; + +public enum TestType +{ + Read, + Write, + Random +} + +[GcForce] +[Orderer(SummaryOrderPolicy.FastestToSlowest)] +[SimpleJob(RuntimeMoniker.Net60, launchCount: 1, warmupCount: 5, targetCount: 10)] +[MemoryDiagnoser] +public class FasterKvBenchmark +{ + private const long Count = 1000; + private static readonly Random _random = new Random(1024); + private IEasyCachingProvider _provider; + private static readonly TimeSpan _default = TimeSpan.FromSeconds(30); + + [Params("sqlite","fasterKv")] + public string Provider { get; set; } + + [Params(TestType.Read, TestType.Write, TestType.Random)] + public TestType Type { get; set; } + + [Params(1,4,8)] + public int ThreadCount { get; set; } + + private static readonly string[] HotKeys = Enumerable.Range(0, (int)(Count * 0.5)) + .Select(i => $"cache_{_random.Next(0, (int) Count)}") + .ToArray(); + + [GlobalSetup] + public void Setup() + { + var services = new ServiceCollection(); + services.AddEasyCaching(x => + { + if (Provider == "fasterKv") + { + x.UseFasterKv(options => { options.SerializerName = "msg"; }) + .WithMessagePack("msg"); + } + else + { + x.UseSQLite(config => + { + config.DBConfig = new SQLiteDBOptions { FileName = "my.db" }; + }); + } + } + ); + IServiceProvider serviceProvider = services.BuildServiceProvider(); + _provider = serviceProvider.GetService(); + + switch (Type) + { + case TestType.Write: + break; + case TestType.Read: + case TestType.Random: + for (int i = 0; i < Count; i++) + { + _provider!.Set($"cache_{i}", "cache", _default); + } + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + [Benchmark] + public async Task Full() + { + var tasks = new Task[ThreadCount]; + var threadOpCount = (int)(HotKeys.Length / ThreadCount); + for (int i = 0; i < ThreadCount; i++) + { + int i1 = i; + tasks[i] = Task.Run(() => + { + var j = i1 * threadOpCount; + switch (Type) + { + case TestType.Read: + for (; j < threadOpCount; j++) + { + _provider.Get(HotKeys[j]); + } + + break; + case TestType.Write: + for (; j < threadOpCount; j++) + { + _provider.Set(HotKeys[j], "cache", _default); + } + + break; + case TestType.Random: + for (; j < threadOpCount; j++) + { + if (j % 2 == 0) + { + _provider.Get(HotKeys[j]); + } + else + { + _provider.Set(HotKeys[j], "cache", _default); + } + } + + break; + } + }); + } + + await Task.WhenAll(tasks); + } + + + [GlobalCleanup] + public void Cleanup() + { + if (_provider is DefaultFasterKvCachingProvider fasterKv) + { + fasterKv.Dispose(); + } + } +} \ No newline at end of file diff --git a/test/EasyCaching.PerformanceTests/Program.cs b/test/EasyCaching.PerformanceTests/Program.cs index 308ffafe..21ece500 100644 --- a/test/EasyCaching.PerformanceTests/Program.cs +++ b/test/EasyCaching.PerformanceTests/Program.cs @@ -10,8 +10,9 @@ static void Main(string[] args) //BenchmarkRunner.Run(); //BenchmarkRunner.Run(); //BenchmarkRunner.Run(); - BenchmarkRunner.Run(); + //BenchmarkRunner.Run(); //BenchmarkRunner.Run(); + BenchmarkRunner.Run(); } } } diff --git a/test/EasyCaching.UnitTests/CachingTests/BaseCachingProviderTest.cs b/test/EasyCaching.UnitTests/CachingTests/BaseCachingProviderTest.cs index ce00bdf9..9b359f10 100644 --- a/test/EasyCaching.UnitTests/CachingTests/BaseCachingProviderTest.cs +++ b/test/EasyCaching.UnitTests/CachingTests/BaseCachingProviderTest.cs @@ -246,7 +246,7 @@ public async Task Refresh_Async_Should_Throw_ArgumentOutOfRangeException_When_Ex [InlineData("")] [InlineData(" ")] [InlineData(null)] - public void RemoveByPrefix_Should_Throw_ArgumentNullException_When_CacheKey_IsNullOrWhiteSpace(string prefix) + public virtual void RemoveByPrefix_Should_Throw_ArgumentNullException_When_CacheKey_IsNullOrWhiteSpace(string prefix) { Assert.Throws(() => _provider.RemoveByPrefix(prefix)); } @@ -255,7 +255,7 @@ public void RemoveByPrefix_Should_Throw_ArgumentNullException_When_CacheKey_IsNu [InlineData("")] [InlineData(" ")] [InlineData(null)] - public async Task RemoveByPrefix_Async_Should_Throw_ArgumentNullException_When_Prefix_IsNullOrWhiteSpace(string preifx) + public async virtual Task RemoveByPrefix_Async_Should_Throw_ArgumentNullException_When_Prefix_IsNullOrWhiteSpace(string preifx) { await Assert.ThrowsAsync(async () => await _provider.RemoveByPrefixAsync(preifx)); } @@ -264,7 +264,7 @@ public async Task RemoveByPrefix_Async_Should_Throw_ArgumentNullException_When_P [InlineData("")] [InlineData(" ")] [InlineData(null)] - public void GetAllByPrefix_Should_Throw_ArgumentNullException_When_CacheKey_IsNullOrWhiteSpace(string prefix) + public virtual void GetAllByPrefix_Should_Throw_ArgumentNullException_When_CacheKey_IsNullOrWhiteSpace(string prefix) { Assert.Throws(() => _provider.RemoveByPrefix(prefix)); } @@ -273,7 +273,7 @@ public void GetAllByPrefix_Should_Throw_ArgumentNullException_When_CacheKey_IsNu [InlineData("")] [InlineData(" ")] [InlineData(null)] - public async Task GetAllByPrefix_Async_Should_Throw_ArgumentNullException_When_Prefix_IsNullOrWhiteSpace(string preifx) + public virtual async Task GetAllByPrefix_Async_Should_Throw_ArgumentNullException_When_Prefix_IsNullOrWhiteSpace(string preifx) { await Assert.ThrowsAsync(async () => await _provider.RemoveByPrefixAsync(preifx)); } diff --git a/test/EasyCaching.UnitTests/CachingTests/FasterKvCachingProviderTest.cs b/test/EasyCaching.UnitTests/CachingTests/FasterKvCachingProviderTest.cs new file mode 100644 index 00000000..d16ecf8c --- /dev/null +++ b/test/EasyCaching.UnitTests/CachingTests/FasterKvCachingProviderTest.cs @@ -0,0 +1,164 @@ +using System; +using System.Threading.Tasks; +using EasyCaching.Core; +using EasyCaching.Core.Configurations; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using Xunit.Abstractions; + +namespace EasyCaching.UnitTests.CachingTests; + +public class FasterKvCachingProviderTest : BaseCachingProviderTest +{ + private readonly ITestOutputHelper _testOutputHelper; + + public FasterKvCachingProviderTest(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + _defaultTs = TimeSpan.FromSeconds(30); + } + + protected override IEasyCachingProvider CreateCachingProvider(Action additionalSetup) + { + var services = new ServiceCollection(); + services.AddEasyCaching(x => + x.UseFasterKv(options => + { + options.SerializerName = "msg"; + additionalSetup(options); + }) + .WithMessagePack("msg") + ); + IServiceProvider serviceProvider = services.BuildServiceProvider(); + return serviceProvider.GetService(); + } + + [Fact] + protected void Set_And_Get_Big_DataSet_Should_Succeed() + { + // set 10w key + for (int i = 0; i < 10_0000; i++) + { + _provider.Set($"Key_{i}", $"Cache_{i}", _defaultTs); + } + + for (int i = 0; i < 10_0000; i++) + { + var value = _provider.Get($"Key_{i}"); + Assert.True(value.HasValue); + Assert.Equal(value.Value, $"Cache_{i}"); + } + } + + [Fact] + protected async Task SetAsync_And_GetAsync_Big_DataSet_Should_Succeed() + { + // set 10w key + for (int i = 0; i < 10_0000; i++) + { + await _provider.SetAsync($"Key_{i}", $"Cache_{i}", _defaultTs); + } + + for (int i = 0; i < 10_0000; i++) + { + var value = await _provider.GetAsync($"Key_{i}"); + Assert.True(value.HasValue); + Assert.Equal(value.Value, $"Cache_{i}"); + } + } + + protected override Task GetAsync_Parallel_Should_Succeed() + { + return Task.FromResult(1); + } + + protected override void Get_Parallel_Should_Succeed() + { + } + + public override Task GetAllByPrefix_Async_Should_Throw_ArgumentNullException_When_Prefix_IsNullOrWhiteSpace( + string preifx) + { + return Task.CompletedTask; + } + + public override void GetAllByPrefix_Should_Throw_ArgumentNullException_When_CacheKey_IsNullOrWhiteSpace( + string prefix) + { + } + + protected override Task Get_Count_Async_With_Prefix_Should_Succeed() + { + return Task.CompletedTask; + } + + protected override Task Get_Count_Async_Without_Prefix_Should_Succeed() + { + return Task.CompletedTask; + } + + protected override void Get_Count_With_Prefix_Should_Succeed() + { + } + + protected override void Get_Count_Without_Prefix_Should_Succeed() + { + } + + protected override void GetByPrefix_Should_Succeed() + { + } + + protected override void GetByPrefix_With_Not_Existed_Prefix_Should_Return_Empty_Dict() + { + } + + protected override Task GetByPrefixAsync_Should_Succeed() + { + return Task.CompletedTask; + } + + protected override Task GetByPrefixAsync_With_Not_Existed_Prefix_Should_Return_Empty_Dict() + { + return Task.CompletedTask; + } + + protected override Task GetExpiration_Async_Should_Succeed() + { + return Task.CompletedTask; + } + + protected override void GetExpiration_Should_Succeed() + { + } + + public override void RemoveByPattern_Should_Succeed() + { + } + + public override Task RemoveByPatternAsync_Should_Succeed() + { + return Task.CompletedTask; + } + + public override Task RemoveByPrefix_Async_Should_Throw_ArgumentNullException_When_Prefix_IsNullOrWhiteSpace( + string preifx) + + { + return Task.CompletedTask; + } + + protected override void RemoveByPrefix_Should_Succeed() + { + } + + public override void RemoveByPrefix_Should_Throw_ArgumentNullException_When_CacheKey_IsNullOrWhiteSpace( + string prefix) + { + } + + protected override Task RemoveByPrefixAsync_Should_Succeed() + { + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/test/EasyCaching.UnitTests/EasyCaching.UnitTests.csproj b/test/EasyCaching.UnitTests/EasyCaching.UnitTests.csproj index e86532c4..26b51b1d 100644 --- a/test/EasyCaching.UnitTests/EasyCaching.UnitTests.csproj +++ b/test/EasyCaching.UnitTests/EasyCaching.UnitTests.csproj @@ -17,7 +17,7 @@ - + @@ -40,6 +40,7 @@ +