diff --git a/.github/workflows/buildandtest.yml b/.github/workflows/buildandtest.yml index 355aa58a..d93c59bb 100644 --- a/.github/workflows/buildandtest.yml +++ b/.github/workflows/buildandtest.yml @@ -35,7 +35,15 @@ jobs: image: bitnami/memcached ports: - 11212:11211 - + etcd: + image: quay.io/coreos/etcd:v3.5.18 + ports: + - 2379:2379 + env: + ETCD_LISTEN_CLIENT_URLS: "http://0.0.0.0:2379" + ETCD_ADVERTISE_CLIENT_URLS: "http://127.0.0.1:2379" + ETCDCTL_API: 3 + ALLOW_NONE_AUTHENTICATION: "yes" steps: - uses: actions/checkout@v4 - name: Setup .NET SDK 8.0.x diff --git a/EasyCaching.sln b/EasyCaching.sln index 9ae4c577..cd8fb48e 100644 --- a/EasyCaching.sln +++ b/EasyCaching.sln @@ -83,6 +83,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyCaching.Serialization.M EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyCaching.Demo.Locks", "sample\EasyCaching.Demo.Locks\EasyCaching.Demo.Locks.csproj", "{9B15A0A0-BD6B-40B0-90D4-848BC3E4AF98}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyCaching.Etcd", "src\EasyCaching.Etcd\EasyCaching.Etcd.csproj", "{BA59F594-423A-4667-B6A0-980619AED44E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -209,18 +211,22 @@ Global {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 - {EEF22C21-F380-4980-B72C-F14488369333}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EEF22C21-F380-4980-B72C-F14488369333}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EEF22C21-F380-4980-B72C-F14488369333}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EEF22C21-F380-4980-B72C-F14488369333}.Release|Any CPU.Build.0 = Release|Any CPU {3C9D5E40-B3A5-4649-8B40-08094644B0FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3C9D5E40-B3A5-4649-8B40-08094644B0FB}.Debug|Any CPU.Build.0 = Debug|Any CPU {3C9D5E40-B3A5-4649-8B40-08094644B0FB}.Release|Any CPU.ActiveCfg = Release|Any CPU {3C9D5E40-B3A5-4649-8B40-08094644B0FB}.Release|Any CPU.Build.0 = Release|Any CPU + {EEF22C21-F380-4980-B72C-F14488369333}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EEF22C21-F380-4980-B72C-F14488369333}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EEF22C21-F380-4980-B72C-F14488369333}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EEF22C21-F380-4980-B72C-F14488369333}.Release|Any CPU.Build.0 = Release|Any CPU {9B15A0A0-BD6B-40B0-90D4-848BC3E4AF98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9B15A0A0-BD6B-40B0-90D4-848BC3E4AF98}.Debug|Any CPU.Build.0 = Debug|Any CPU {9B15A0A0-BD6B-40B0-90D4-848BC3E4AF98}.Release|Any CPU.ActiveCfg = Release|Any CPU {9B15A0A0-BD6B-40B0-90D4-848BC3E4AF98}.Release|Any CPU.Build.0 = Release|Any CPU + {BA59F594-423A-4667-B6A0-980619AED44E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BA59F594-423A-4667-B6A0-980619AED44E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BA59F594-423A-4667-B6A0-980619AED44E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BA59F594-423A-4667-B6A0-980619AED44E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -256,9 +262,10 @@ Global {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} - {EEF22C21-F380-4980-B72C-F14488369333} = {15070C49-A507-4844-BCFE-D319CFBC9A63} {3C9D5E40-B3A5-4649-8B40-08094644B0FB} = {B337509B-75F9-4851-821F-9BBE87C4E4BC} + {EEF22C21-F380-4980-B72C-F14488369333} = {15070C49-A507-4844-BCFE-D319CFBC9A63} {9B15A0A0-BD6B-40B0-90D4-848BC3E4AF98} = {F88D727A-9F9C-43D9-90B1-D4A02BF8BC98} + {BA59F594-423A-4667-B6A0-980619AED44E} = {A0F5CC7E-155F-4726-8DEB-E966950B3FE9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {63A57886-054B-476C-AAE1-8D7C8917682E} diff --git a/sample/EasyCaching.Demo.ConsoleApp/EasyCaching.Demo.ConsoleApp.csproj b/sample/EasyCaching.Demo.ConsoleApp/EasyCaching.Demo.ConsoleApp.csproj index 1d45c7c8..b711660a 100644 --- a/sample/EasyCaching.Demo.ConsoleApp/EasyCaching.Demo.ConsoleApp.csproj +++ b/sample/EasyCaching.Demo.ConsoleApp/EasyCaching.Demo.ConsoleApp.csproj @@ -14,6 +14,7 @@ + diff --git a/sample/EasyCaching.Demo.ConsoleApp/Program.cs b/sample/EasyCaching.Demo.ConsoleApp/Program.cs index ccb3f2e1..037c8cd2 100644 --- a/sample/EasyCaching.Demo.ConsoleApp/Program.cs +++ b/sample/EasyCaching.Demo.ConsoleApp/Program.cs @@ -9,7 +9,9 @@ namespace EasyCaching.Demo.ConsoleApp using MemoryPack; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; + using Newtonsoft.Json; using System; + using System.Collections.Generic; using System.IO; class Program @@ -56,6 +58,17 @@ static void Main(string[] args) .WithJson("json") .WithSystemTextJson("sysjson") .WithMessagePack("msgpack"); + + option.UseEtcd(options => + { + options.Address = "http://127.0.0.1:2379"; + options.Timeout = 30000; + options.SerializerName = "json"; + }, "e1").WithJson(jsonSerializerSettingsConfigure: x => + { + x.TypeNameHandling = Newtonsoft.Json.TypeNameHandling.None; + x.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; + }, "json"); }); IServiceProvider serviceProvider = services.BuildServiceProvider(); @@ -104,6 +117,19 @@ static void Main(string[] args) var diskVal = diskCache.Get("diskkey"); Console.WriteLine($"disk cache get value, {diskVal.HasValue} {diskVal.IsNull} {diskVal.Value} "); + //etcd cache + var etcdCache = factory.GetCachingProvider("e1"); + var re11 = etcdCache.GetAllKeysByPrefix("emk"); + var re12 = etcdCache.GetByPrefix("emk"); + etcdCache.Set("emkey3", prod, TimeSpan.FromSeconds(2000)); + var re13 = etcdCache.Get("emkey3"); + var re14 = etcdCache.GetAll(new List() + { + "emkey3" + }); + etcdCache.Remove("emkey3"); + Console.WriteLine($"etcd cache get value, {re13.HasValue} {re13.IsNull} {re13.Value} "); + Console.ReadKey(); } } diff --git a/sample/EasyCaching.Demo.Locks/Controllers/LocksController.cs b/sample/EasyCaching.Demo.Locks/Controllers/LocksController.cs index b6400a70..400c8942 100644 --- a/sample/EasyCaching.Demo.Locks/Controllers/LocksController.cs +++ b/sample/EasyCaching.Demo.Locks/Controllers/LocksController.cs @@ -67,4 +67,33 @@ public async Task MemoryLockingOperation(int millisecondsTimeout) await memoryLock.ReleaseAsync(); } } + + [HttpPost("etcd-locking")] + public async Task EtcdLockingOperation(int millisecondsTimeout) + { + using var distributedLock = _distributedLockFactory.CreateLock("DefaultEtcd", "YourKey"); + + try + { + if (await distributedLock.LockAsync(millisecondsTimeout)) + { + // Simulate operation + Thread.Sleep(2000); + } + else + { + // Proper error + } + } + catch (Exception ex) + { + // log error + throw new Exception("Exception", ex); + } + finally + { + // release lock at the end + await distributedLock.ReleaseAsync(); + } + } } diff --git a/sample/EasyCaching.Demo.Locks/EasyCaching.Demo.Locks.csproj b/sample/EasyCaching.Demo.Locks/EasyCaching.Demo.Locks.csproj index 0e6245ee..acf448fe 100644 --- a/sample/EasyCaching.Demo.Locks/EasyCaching.Demo.Locks.csproj +++ b/sample/EasyCaching.Demo.Locks/EasyCaching.Demo.Locks.csproj @@ -12,6 +12,7 @@ + diff --git a/sample/EasyCaching.Demo.Locks/GlobalUsings.cs b/sample/EasyCaching.Demo.Locks/GlobalUsings.cs index 838d0c76..152016b7 100644 --- a/sample/EasyCaching.Demo.Locks/GlobalUsings.cs +++ b/sample/EasyCaching.Demo.Locks/GlobalUsings.cs @@ -1,4 +1,5 @@ global using EasyCaching.Core.Configurations; global using EasyCaching.Core.DistributedLock; global using Microsoft.AspNetCore.Mvc; -global using EasyCaching.Redis.DistributedLock; \ No newline at end of file +global using EasyCaching.Redis.DistributedLock; +global using EasyCaching.Etcd.DistributedLock; \ No newline at end of file diff --git a/sample/EasyCaching.Demo.Locks/Program.cs b/sample/EasyCaching.Demo.Locks/Program.cs index c2d00cdb..2de6f09f 100644 --- a/sample/EasyCaching.Demo.Locks/Program.cs +++ b/sample/EasyCaching.Demo.Locks/Program.cs @@ -1,3 +1,5 @@ +using Newtonsoft.Json; + var builder = WebApplication.CreateBuilder(args); // Add services to the container. @@ -23,6 +25,19 @@ }) .WithJson()//with josn serialization .UseRedisLock(); // use distributed lock + + // use etcd cache + option.UseEtcd(options => + { + options.Address = "http://127.0.0.1:2379"; + options.Timeout = 3000; + options.LockMs = 10000; + options.SerializerName = "json"; + }).WithJson(jsonSerializerSettingsConfigure: x => + { + x.TypeNameHandling = Newtonsoft.Json.TypeNameHandling.None; + x.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; + }, "json").UseEtcdLock(); }); #region How Inject Distributed and Memory lock @@ -33,6 +48,9 @@ // inject to use memory lock builder.Services.AddSingleton(); +// inject to use memory lock +builder.Services.AddSingleton(); + #endregion var app = builder.Build(); diff --git a/sample/EasyCaching.Demo.Providers/Controllers/ValuesController.cs b/sample/EasyCaching.Demo.Providers/Controllers/ValuesController.cs index d7f8878a..b6eee99e 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,FasterKv + //1. InMemory,Memcached,Redis,SQLite,FasterKv,Etcd private readonly IEasyCachingProvider _provider; public ValuesController(IEasyCachingProvider provider) @@ -38,6 +38,9 @@ public string Get(string str) case "set" : _provider.Set("demo", "123", TimeSpan.FromMinutes(1)); return "seted"; + case "getexpiretime": + var timeSpanData = _provider.GetExpiration("demo"); + return $"{timeSpanData.TotalSeconds}"; case "remove" : _provider.Remove("demo"); return "removed"; @@ -64,6 +67,9 @@ public async Task GetAsync(string str) case "set": await _provider.SetAsync("demo", "123", TimeSpan.FromMinutes(1)); return "seted"; + case "getexpiretime": + var timeSpanData = _provider.GetExpiration("demo"); + return $"{timeSpanData.TotalSeconds}"; case "remove": await _provider.RemoveAsync("demo"); return "removed"; diff --git a/sample/EasyCaching.Demo.Providers/EasyCaching.Demo.Providers.csproj b/sample/EasyCaching.Demo.Providers/EasyCaching.Demo.Providers.csproj index aac47298..0ded0074 100644 --- a/sample/EasyCaching.Demo.Providers/EasyCaching.Demo.Providers.csproj +++ b/sample/EasyCaching.Demo.Providers/EasyCaching.Demo.Providers.csproj @@ -7,6 +7,8 @@ + + diff --git a/sample/EasyCaching.Demo.Providers/Startup.cs b/sample/EasyCaching.Demo.Providers/Startup.cs index 5372e2ae..e84e1b76 100644 --- a/sample/EasyCaching.Demo.Providers/Startup.cs +++ b/sample/EasyCaching.Demo.Providers/Startup.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; + using Newtonsoft.Json; public class Startup { @@ -69,6 +70,18 @@ public void ConfigureServices(IServiceCollection services) config.SerializerName = "msg"; }) .WithMessagePack("msg"); + + // use etcd cache + option.UseEtcd(options => + { + options.Address = "http://127.0.0.1:2379"; + options.Timeout = 30000; + options.SerializerName = "json"; + }, "e1").WithJson(jsonSerializerSettingsConfigure: x => + { + x.TypeNameHandling = Newtonsoft.Json.TypeNameHandling.None; + x.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; + }, "json"); }); } diff --git a/src/EasyCaching.Core/Internal/CachingProviderType.cs b/src/EasyCaching.Core/Internal/CachingProviderType.cs index ed1d8f27..cb8a2223 100644 --- a/src/EasyCaching.Core/Internal/CachingProviderType.cs +++ b/src/EasyCaching.Core/Internal/CachingProviderType.cs @@ -14,5 +14,6 @@ public enum CachingProviderType Ext2, LiteDB, FasterKv, + Etcd, } } diff --git a/src/EasyCaching.Core/Internal/EasyCachingConstValue.cs b/src/EasyCaching.Core/Internal/EasyCachingConstValue.cs index 342a0ae4..21ce93cf 100644 --- a/src/EasyCaching.Core/Internal/EasyCachingConstValue.cs +++ b/src/EasyCaching.Core/Internal/EasyCachingConstValue.cs @@ -119,7 +119,17 @@ public class EasyCachingConstValue /// The default name of the FasterKv /// public const string DefaultFasterKvName = "DefaultFasterKvName"; - + + /// + /// The default name of the etcd. + /// + public const string DefaultEtcdName = "DefaultEtcd"; + + /// + /// The etcd section. + /// + public const string EtcdSection = "easycaching:etcd"; + /// /// The FasterKv section. /// diff --git a/src/EasyCaching.Etcd/Configurations/EtcdCachingOptions.cs b/src/EasyCaching.Etcd/Configurations/EtcdCachingOptions.cs new file mode 100644 index 00000000..3af52d62 --- /dev/null +++ b/src/EasyCaching.Etcd/Configurations/EtcdCachingOptions.cs @@ -0,0 +1,31 @@ +using EasyCaching.Core.Configurations; + +namespace EasyCaching.Etcd +{ + /// + /// EasyCaching options extensions of Etcd. + /// + public class EtcdCachingOptions : BaseProviderOptions + { + /// + /// Etcd address + /// cluster:like "http://localhost:23790,http://localhost:23791,http://localhost:23792" + /// + public string Address { get; set; } + + /// + /// Etcd access UserName + /// + public string UserName { get; set; } + + /// + /// Etcd access Pwd + /// + public string Password { get; set; } + + /// + /// Etcd timeout with Milliseconds + /// + public long Timeout { get; set; } = 3000; + } +} \ No newline at end of file diff --git a/src/EasyCaching.Etcd/Configurations/EtcdCachingOptionsExtensions.cs b/src/EasyCaching.Etcd/Configurations/EtcdCachingOptionsExtensions.cs new file mode 100644 index 00000000..f2076771 --- /dev/null +++ b/src/EasyCaching.Etcd/Configurations/EtcdCachingOptionsExtensions.cs @@ -0,0 +1,78 @@ +using System; +using EasyCaching.Core; +using EasyCaching.Core.Configurations; +using EasyCaching.Core.DistributedLock; +using EasyCaching.Etcd; +using EasyCaching.Etcd.DistributedLock; +using Microsoft.Extensions.Configuration; +// ReSharper disable CheckNamespace + +namespace Microsoft.Extensions.DependencyInjection; + +public static class EtcdCachingOptionsExtensions +{ + /// + /// Uses the Etcd provider (specify the config via hard code). + /// + /// Options. + /// Configure provider settings. + /// The name of this provider instance. + public static EasyCachingOptions UseEtcd( + this EasyCachingOptions options, + Action configure, + string name = EasyCachingConstValue.DefaultEtcdName + ) + { + ArgumentCheck.NotNull(configure, nameof(configure)); + + options.RegisterExtension(new EtcdOptionsExtension(name, configure)); + return options; + } + + /// + /// Uses the Etcd 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 UseEtcd( + this EasyCachingOptions options, + IConfiguration configuration, + string name = EasyCachingConstValue.DefaultEtcdName, + string sectionName = EasyCachingConstValue.EtcdSection + ) + { + var dbConfig = configuration.GetSection(sectionName); + var EtcdOptions = new EtcdCachingOptions(); + dbConfig.Bind(EtcdOptions); + + void Configure(EtcdCachingOptions x) + { + x.EnableLogging = EtcdOptions.EnableLogging; + x.MaxRdSecond = EtcdOptions.MaxRdSecond; + x.LockMs = EtcdOptions.LockMs; + x.SleepMs = EtcdOptions.SleepMs; + x.SerializerName = EtcdOptions.SerializerName; + x.CacheNulls = EtcdOptions.CacheNulls; + x.Address = EtcdOptions.Address; + x.UserName = EtcdOptions.UserName; + x.Password = EtcdOptions.Password; + x.Timeout= EtcdOptions.Timeout; + } + + options.RegisterExtension(new EtcdOptionsExtension(name, Configure)); + return options; + } + + /// + /// Uses the Etcd lock. + /// + /// Options. + public static EasyCachingOptions UseEtcdLock(this EasyCachingOptions options) + { + options.UseDistributedLock(); + + return options; + } +} \ No newline at end of file diff --git a/src/EasyCaching.Etcd/Configurations/EtcdOptionsExtension.cs b/src/EasyCaching.Etcd/Configurations/EtcdOptionsExtension.cs new file mode 100644 index 00000000..7eb8adc5 --- /dev/null +++ b/src/EasyCaching.Etcd/Configurations/EtcdOptionsExtension.cs @@ -0,0 +1,70 @@ +using System; +using EasyCaching.Core; +using EasyCaching.Core.Configurations; +using EasyCaching.Core.Serialization; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace EasyCaching.Etcd +{ + /// + /// Etcd options extension. + /// + internal sealed class EtcdOptionsExtension : IEasyCachingOptionsExtension + { + /// + /// The name. + /// + private readonly string _name; + + /// + /// The configure. + /// + private readonly Action _configure; + + /// + /// Initializes a new instance of the class. + /// + /// Name. + /// Configure. + public EtcdOptionsExtension(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 EtcdCaching(_name, options,serializers,factory); + }); + + services.AddSingleton(x => + { + var mCache = x.GetServices(); + var optionsMon = x.GetRequiredService>(); + var options = optionsMon.Get(_name); + var factory = x.GetService(); + var serializers = x.GetServices(); + return new DefaultEtcdCachingProvider(_name,mCache, options, serializers, factory); + }); + } + } +} diff --git a/src/EasyCaching.Etcd/DefaultEtcdCachingProvider.Async.cs b/src/EasyCaching.Etcd/DefaultEtcdCachingProvider.Async.cs new file mode 100644 index 00000000..5fdc7048 --- /dev/null +++ b/src/EasyCaching.Etcd/DefaultEtcdCachingProvider.Async.cs @@ -0,0 +1,397 @@ +using EasyCaching.Core; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace EasyCaching.Etcd +{ + + /// + /// EtcdCaching provider. + /// + public partial class DefaultEtcdCachingProvider : EasyCachingAbstractProvider + { + /// + /// Gets the specified cacheKey, dataRetriever and expiration async. + /// + /// The async. + /// Cache key. + /// Data retriever. + /// Expiration. + /// CancellationToken + /// The 1st type parameter. + public override async Task> BaseGetAsync(string cacheKey, Func> dataRetriever, TimeSpan expiration, CancellationToken cancellationToken = default) + { + ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); + ArgumentCheck.NotNegativeOrZero(expiration, nameof(expiration)); + + var result = await _etcdClient.GetAsync(cacheKey); + if (result.HasValue) + { + if (_options.EnableLogging) + _logger?.LogInformation($"Cache Hit : cachekey = {cacheKey}"); + + CacheStats.OnHit(); + + return result; + } + + CacheStats.OnMiss(); + + if (_options.EnableLogging) + _logger?.LogInformation($"Cache Missed : cachekey = {cacheKey}"); + + if (!await _etcdClient.SetAsync($"{cacheKey}_Lock", "1", TimeSpan.FromMilliseconds(_options.LockMs))) + { + //wait for some ms + await Task.Delay(_options.SleepMs, cancellationToken); + return await GetAsync(cacheKey, dataRetriever, expiration); + } + + try + { + var res = await dataRetriever(); + + if (res != null || _options.CacheNulls) + { + await SetAsync(cacheKey, res, expiration); + //remove mutex key + await _etcdClient.DeleteAsync($"{cacheKey}_Lock"); + + return new CacheValue(res, true); + } + else + { + //remove mutex key + await _etcdClient.DeleteAsync($"{cacheKey}_Lock"); + return CacheValue.NoValue; + } + } + catch + { + //remove mutex key + await _etcdClient.DeleteAsync($"{cacheKey}_Lock"); + throw; + } + } + + /// + /// Gets the specified cacheKey async. + /// + /// The async. + /// Cache key. + /// CancellationToken + /// The 1st type parameter. + public override async Task> BaseGetAsync(string cacheKey, CancellationToken cancellationToken = default) + { + ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); + + var result = await _etcdClient.GetAsync(cacheKey); + + if (result.HasValue) + { + if (_options.EnableLogging) + _logger?.LogInformation($"Cache Hit : cachekey = {cacheKey}"); + + CacheStats.OnHit(); + + return result; + } + else + { + if (_options.EnableLogging) + _logger?.LogInformation($"Cache Missed : cachekey = {cacheKey}"); + + CacheStats.OnMiss(); + + return CacheValue.NoValue; + } + } + + /// + /// Gets the count. + /// + /// The count. + /// Prefix. + /// CancellationToken + public override async Task BaseGetCountAsync(string prefix = "", CancellationToken cancellationToken = default) + { + var dicData = await _etcdClient.GetAllAsync(prefix); + return dicData != null ? dicData.Count : 0; + } + + /// + /// Gets the specified cacheKey async. + /// + /// The async. + /// Cache key. + /// Object Type. + /// CancellationToken + public override async Task BaseGetAsync(string cacheKey, Type type, CancellationToken cancellationToken = default) + { + ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); + + var result = await _etcdClient.GetAsync(cacheKey); + + if (result != null) + { + if (_options.EnableLogging) + _logger?.LogInformation($"Cache Hit : cachekey = {cacheKey}"); + + CacheStats.OnHit(); + + return result; + } + else + { + if (_options.EnableLogging) + _logger?.LogInformation($"Cache Missed : cachekey = {cacheKey}"); + + CacheStats.OnMiss(); + + return null; + } + } + + /// + /// Removes the specified cacheKey async. + /// + /// The async. + /// Cache key. + /// CancellationToken + public override async Task BaseRemoveAsync(string cacheKey, CancellationToken cancellationToken = default) + { + ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); + + await _etcdClient.DeleteAsync(cacheKey); + } + + /// + /// Sets the specified cacheKey, cacheValue and expiration async. + /// + /// The async. + /// Cache key. + /// Cache value. + /// Expiration. + /// CancellationToken + /// The 1st type parameter. + 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)); + + if (MaxRdSecond > 0) + { + var addSec = new Random().Next(1, MaxRdSecond); + expiration = expiration.Add(TimeSpan.FromMilliseconds(addSec)); + } + + //var valExpiration = expiration.Seconds <= 1 ? expiration : TimeSpan.FromSeconds(expiration.Seconds / 2); + //var val = new CacheValue(cacheValue, true, valExpiration); + await _etcdClient.SetAsync(cacheKey, cacheValue, expiration); + } + + /// + /// Existses the specified cacheKey async. + /// + /// The async. + /// Cache key. + /// CancellationToken + public override async Task BaseExistsAsync(string cacheKey, CancellationToken cancellationToken = default) + { + ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); + + return await _etcdClient.ExistsAsync(cacheKey); + } + + /// + /// Removes cached item by cachekey's prefix async. + /// + /// The by prefix async. + /// Prefix. + /// CancellationToken + public override async Task BaseRemoveByPrefixAsync(string prefix, CancellationToken cancellationToken = default) + { + ArgumentCheck.NotNullOrWhiteSpace(prefix, nameof(prefix)); + + var count = await _etcdClient.DeleteRangeDataAsync(prefix); + + if (_options.EnableLogging) + _logger?.LogInformation($"RemoveByPrefixAsync : prefix = {prefix} , count = {count}"); + } + + /// + /// Removes cached items by pattern async. + /// + /// The by prefix async. + /// Pattern. + /// CancellationToken + public override async Task BaseRemoveByPatternAsync(string pattern, CancellationToken cancellationToken = default) + { + ArgumentCheck.NotNullOrWhiteSpace(pattern, nameof(pattern)); + + throw new NotSupportedException("BaseRemoveByPatternAsync is not supported in Etcd provider."); + } + + /// + /// Sets all async. + /// + /// The all async. + /// Values. + /// Expiration. + /// CancellationToken + /// The 1st type parameter. + public override async Task BaseSetAllAsync(IDictionary values, TimeSpan expiration, CancellationToken cancellationToken = default) + { + ArgumentCheck.NotNegativeOrZero(expiration, nameof(expiration)); + ArgumentCheck.NotNullAndCountGTZero(values, nameof(values)); + + foreach (var item in values) + { + await _etcdClient.SetAsync(item.Key, item.Value, expiration); + } + } + + /// + /// Gets all async. + /// + /// The all async. + /// Cache keys. + /// CancellationToken + /// The 1st type parameter. + public override async Task>> BaseGetAllAsync(IEnumerable cacheKeys, CancellationToken cancellationToken = default) + { + ArgumentCheck.NotNullAndCountGTZero(cacheKeys, nameof(cacheKeys)); + + if (_options.EnableLogging) + _logger?.LogInformation($"GetAllAsync : cacheKeys = {string.Join(",", cacheKeys)}"); + + Dictionary> result = new Dictionary>(); + foreach (var item in cacheKeys) + { + var value = await BaseGetAsync(item); + result.Add(item, value); + } + return result; + } + + /// + /// Get all cacheKey by prefix async. + /// + /// Cache keys. + /// Cache keys. + /// Get all cacheKey by prefix async. + public override async Task> BaseGetAllKeysByPrefixAsync(string prefix, CancellationToken cancellationToken = default) + { + if (_options.EnableLogging) + _logger?.LogInformation("GetAllKeysAsync"); + + var dicData = await _etcdClient.GetAllAsync(prefix); + List result = new List(); + foreach (var item in dicData) + { + result.Add(item.Key); + } + return result; + } + + /// + /// Gets the by prefix async. + /// + /// The by prefix async. + /// Prefix. + /// CancellationToken + /// The 1st type parameter. + public override async Task>> BaseGetByPrefixAsync(string prefix, CancellationToken cancellationToken = default) + { + ArgumentCheck.NotNullOrWhiteSpace(prefix, nameof(prefix)); + + if (_options.EnableLogging) + _logger?.LogInformation($"GetByPrefixAsync : prefix = {prefix}"); + + var dicData = await _etcdClient.GetAllAsync(prefix); + Dictionary> result = new Dictionary>(); + foreach (var item in dicData) + { + result.Add(item.Key, new CacheValue(_serializer.Deserialize(Encoding.UTF8.GetBytes(item.Value)), true)); + } + return result; + } + + /// + /// Removes all async. + /// + /// The all async. + /// Cache keys. + /// CancellationToken + public override async Task BaseRemoveAllAsync(IEnumerable cacheKeys, CancellationToken cancellationToken = default) + { + ArgumentCheck.NotNullAndCountGTZero(cacheKeys, nameof(cacheKeys)); + + if (_options.EnableLogging) + _logger?.LogInformation($"RemoveAllAsync : cacheKeys = {string.Join(",", cacheKeys)}"); + + foreach (var item in cacheKeys) + { + await _etcdClient.DeleteAsync(item); + } + } + + /// + /// Flush All Cached Item async. + /// + /// The async. + /// CancellationToken + public override async Task BaseFlushAsync(CancellationToken cancellationToken = default) + { + if (_options.EnableLogging) + _logger?.LogInformation("FlushAsync"); + + var dicData = await _etcdClient.GetAllAsync(""); + if (dicData != null) + { + List listKeys = new List(dicData.Count); + foreach (var item in dicData) + { + listKeys.Add(item.Key); + } + await BaseRemoveAllAsync(listKeys); + } + } + + /// + /// Tries the set async. + /// + /// The set async. + /// Cache key. + /// Cache value. + /// Expiration. + /// CancellationToken + /// The 1st type parameter. + 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)); + + return await _etcdClient.SetAsync(cacheKey, cacheValue, expiration); + } + + /// + /// Get the expiration of cache key + /// + /// cache key + /// CancellationToken + /// expiration + public override async Task BaseGetExpirationAsync(string cacheKey, CancellationToken cancellationToken = default) + { + ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); + var secondsTTL = await _etcdClient.GetExpireTTLAsync(cacheKey); + return TimeSpan.FromSeconds(secondsTTL); + } + } +} \ No newline at end of file diff --git a/src/EasyCaching.Etcd/DefaultEtcdCachingProvider.cs b/src/EasyCaching.Etcd/DefaultEtcdCachingProvider.cs new file mode 100644 index 00000000..96ade089 --- /dev/null +++ b/src/EasyCaching.Etcd/DefaultEtcdCachingProvider.cs @@ -0,0 +1,425 @@ +using EasyCaching.Core; +using EasyCaching.Core.Serialization; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace EasyCaching.Etcd +{ + public sealed partial class DefaultEtcdCachingProvider : EasyCachingAbstractProvider, IDisposable + { + private readonly string _name; + + private bool _disposed; + + private readonly ILogger? _logger; + + private readonly IEasyCachingSerializer _serializer; + private readonly EtcdCachingOptions _options; + + private readonly IEtcdCaching _etcdClient; + + /// + /// The cache stats. + /// + private readonly CacheStats _cacheStats; + + private readonly ProviderInfo _info; + + public DefaultEtcdCachingProvider( + string name, + IEnumerable cache, + EtcdCachingOptions options, + IEnumerable serializers, + ILoggerFactory? loggerFactory = null) + { + ArgumentCheck.NotNull(options, nameof(options)); + ArgumentCheck.NotNull(serializers, nameof(serializers)); + + _name = name; + + _options = options; + _logger = loggerFactory?.CreateLogger(); + + _etcdClient = cache.Single(x => x.ProviderName == _name); + + 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.Etcd; + 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 + }; + } + + /// + /// Get the specified cacheKey, dataRetriever and expiration. + /// + /// The get. + /// Cache key. + /// Data retriever. + /// Expiration. + /// The 1st type parameter. + public override CacheValue BaseGet(string cacheKey, Func dataRetriever, TimeSpan expiration) + { + ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); + ArgumentCheck.NotNegativeOrZero(expiration, nameof(expiration)); + + var result = _etcdClient.Get(cacheKey); + if (result.HasValue) + { + if (_options.EnableLogging) + _logger?.LogInformation($"Cache Hit : cachekey = {cacheKey}"); + + CacheStats.OnHit(); + + return result; + } + + CacheStats.OnMiss(); + + if (_options.EnableLogging) + _logger?.LogInformation($"Cache Missed : cachekey = {cacheKey}"); + + if (!_etcdClient.Set($"{cacheKey}_Lock", "1", TimeSpan.FromMilliseconds(_options.LockMs))) + { + System.Threading.Thread.Sleep(_options.SleepMs); + return Get(cacheKey, dataRetriever, expiration); + } + + try + { + var res = dataRetriever(); + + if (res != null || _options.CacheNulls) + { + Set(cacheKey, res, expiration); + //remove mutex key + _etcdClient.Delete($"{cacheKey}_Lock"); + + return new CacheValue(res, true); + } + else + { + //remove mutex key + _etcdClient.Delete($"{cacheKey}_Lock"); + return CacheValue.NoValue; + } + } + catch + { + //remove mutex key + _etcdClient.Delete($"{cacheKey}_Lock"); + throw; + } + } + + /// + /// Get the specified cacheKey. + /// + /// The get. + /// Cache key. + /// The 1st type parameter. + public override CacheValue BaseGet(string cacheKey) + { + ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); + + var result = _etcdClient.Get(cacheKey); + if (result.HasValue) + { + if (_options.EnableLogging) + _logger?.LogInformation($"Cache Hit : cachekey = {cacheKey}"); + + CacheStats.OnHit(); + + return result; + } + else + { + if (_options.EnableLogging) + _logger?.LogInformation($"Cache Missed : cachekey = {cacheKey}"); + + CacheStats.OnMiss(); + + return CacheValue.NoValue; + } + } + + /// + /// Remove the specified cacheKey. + /// + /// The remove. + /// Cache key. + public override void BaseRemove(string cacheKey) + { + ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); + + _etcdClient.Delete(cacheKey); + } + + /// + /// Set the specified cacheKey, cacheValue and expiration. + /// + /// The set. + /// Cache key. + /// Cache value. + /// expiration. + /// The 1st type parameter. + 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)); + + if (MaxRdSecond > 0) + { + var addSec = new Random().Next(1, MaxRdSecond); + expiration = expiration.Add(TimeSpan.FromMilliseconds(addSec)); + } + + //var valExpiration = expiration.Seconds <= 1 ? expiration : TimeSpan.FromSeconds(expiration.Seconds / 2); + //var val = new CacheValue(cacheValue, true, valExpiration); + _etcdClient.Set(cacheKey, cacheValue, expiration); + } + + /// + /// Exists the specified cacheKey. + /// + /// The exists. + /// Cache key. + public override bool BaseExists(string cacheKey) + { + ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); + + return _etcdClient.Exists(cacheKey); + } + + /// + /// Removes cached item by cachekey's prefix. + /// + /// Prefix. + public override void BaseRemoveByPrefix(string prefix) + { + ArgumentCheck.NotNullOrWhiteSpace(prefix, nameof(prefix)); + + var count = _etcdClient.DeleteRangeData(prefix); + + if (_options.EnableLogging) + _logger?.LogInformation($"RemoveByPrefix : prefix = {prefix} , count = {count}"); + } + + /// + /// Removes cached items by pattern async. + /// + /// The by prefix async. + /// Pattern. + public override void BaseRemoveByPattern(string pattern) + { + ArgumentCheck.NotNullOrWhiteSpace(pattern, nameof(pattern)); + + throw new NotSupportedException("BaseRemoveByPattern is not supported in Etcd provider."); + } + + /// + /// Sets all. + /// + /// Values. + /// Expiration. + /// The 1st type parameter. + public override void BaseSetAll(IDictionary values, TimeSpan expiration) + { + ArgumentCheck.NotNegativeOrZero(expiration, nameof(expiration)); + ArgumentCheck.NotNullAndCountGTZero(values, nameof(values)); + + foreach (var item in values) + { + _etcdClient.Set(item.Key, item.Value, expiration); + } + } + + /// + /// Gets all. + /// + /// The all. + /// Cache keys. + /// The 1st type parameter. + public override IDictionary> BaseGetAll(IEnumerable cacheKeys) + { + ArgumentCheck.NotNullAndCountGTZero(cacheKeys, nameof(cacheKeys)); + + if (_options.EnableLogging) + _logger?.LogInformation($"GetAll : cacheKeys = {string.Join(",", cacheKeys)}"); + + Dictionary> result = new Dictionary>(); + foreach (var item in cacheKeys) + { + var value = BaseGet(item); + result.Add(item, value); + } + return result; + } + + /// + /// Get all cacheKey by prefix. + /// + /// Prefix. + /// Get all cacheKey by prefix. + public override IEnumerable BaseGetAllKeysByPrefix(string prefix) + { + if (_options.EnableLogging) + _logger?.LogInformation("GetAllKeys"); + + var dicData = _etcdClient.GetAll(prefix); + List result = new List(); + foreach (var item in dicData) + { + result.Add(item.Key); + } + return result; + } + + /// + /// Gets the by prefix. + /// + /// The by prefix. + /// Prefix. + /// The 1st type parameter. + public override IDictionary> BaseGetByPrefix(string prefix) + { + ArgumentCheck.NotNullOrWhiteSpace(prefix, nameof(prefix)); + + if (_options.EnableLogging) + _logger?.LogInformation($"GetByPrefix : prefix = {prefix}"); + + var dicData = _etcdClient.GetAll(prefix); + Dictionary> result = new Dictionary>(); + foreach (var item in dicData) + { + result.Add(item.Key, new CacheValue(_serializer.Deserialize(Encoding.UTF8.GetBytes(item.Value)), true)); + } + return result; + } + + /// + /// Removes all. + /// + /// Cache keys. + public override void BaseRemoveAll(IEnumerable cacheKeys) + { + ArgumentCheck.NotNullAndCountGTZero(cacheKeys, nameof(cacheKeys)); + + if (_options.EnableLogging) + _logger?.LogInformation($"RemoveAll : cacheKeys = {string.Join(",", cacheKeys)}"); + + foreach (var item in cacheKeys) + { + _etcdClient.Delete(item); + } + } + + /// + /// Gets the count. + /// + /// The count. + /// Prefix. + public override int BaseGetCount(string prefix = "") + { + var dicData = _etcdClient.GetAll(prefix); + return dicData != null ? dicData.Count : 0; + } + + /// + /// Flush All Cached Item. + /// + public override void BaseFlush() + { + if (_options.EnableLogging) + _logger?.LogInformation("Flush"); + + var dicData = _etcdClient.GetAll(""); + if (dicData != null) + { + List listKeys = new List(dicData.Count); + foreach (var item in dicData) + { + listKeys.Add(item.Key); + } + if(listKeys.Count > 0) + BaseRemoveAll(listKeys); + } + // throw new NotSupportedException("BaseFlush is not supported in Etcd provider."); + } + + /// + /// Tries the set. + /// + /// true, if set was tryed, false otherwise. + /// Cache key. + /// Cache value. + /// Expiration. + /// The 1st type parameter. + 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)); + + //var val = new CacheValue(cacheValue, true, expiration); + return _etcdClient.Set(cacheKey, cacheValue, expiration); + } + + /// + /// Get the expiration of cache key + /// + /// cache key + /// expiration + public override TimeSpan BaseGetExpiration(string cacheKey) + { + ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); + var secondsTTL = _etcdClient.GetExpireTTL(cacheKey); + return TimeSpan.FromSeconds(secondsTTL); + } + + /// + /// Get te information of this provider. + /// + /// + public override ProviderInfo BaseGetProviderInfo() => _info; + + public override object BaseGetDatabase() => _etcdClient; + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool _) + { + if (_disposed) + return; + + _disposed = true; + } + } +} \ No newline at end of file diff --git a/src/EasyCaching.Etcd/DistributedLock/EtcdLockFactory.cs b/src/EasyCaching.Etcd/DistributedLock/EtcdLockFactory.cs new file mode 100644 index 00000000..f0b2369e --- /dev/null +++ b/src/EasyCaching.Etcd/DistributedLock/EtcdLockFactory.cs @@ -0,0 +1,22 @@ +using EasyCaching.Core.DistributedLock; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Collections.Generic; +using System.Linq; + +namespace EasyCaching.Etcd.DistributedLock +{ + public class EtcdLockFactory : DistributedLockFactory + { + private readonly IEnumerable _etcdClients; + + public EtcdLockFactory(IEnumerable etcdClients, + IOptionsMonitor optionsMonitor, + ILoggerFactory loggerFactory = null) + : base(name => DistributedLockOptions.FromProviderOptions(optionsMonitor.Get(name)), loggerFactory) => + _etcdClients = etcdClients; + + protected override IDistributedLockProvider GetLockProvider(string name) => + new EtcdLockProvider(_etcdClients.Single(x => x.ProviderName.Equals(name))); + } +} diff --git a/src/EasyCaching.Etcd/DistributedLock/EtcdLockProvider.cs b/src/EasyCaching.Etcd/DistributedLock/EtcdLockProvider.cs new file mode 100644 index 00000000..bc02f2c8 --- /dev/null +++ b/src/EasyCaching.Etcd/DistributedLock/EtcdLockProvider.cs @@ -0,0 +1,35 @@ +using EasyCaching.Core.DistributedLock; +using Grpc.Core; +using System; +using System.Threading.Tasks; + +namespace EasyCaching.Etcd.DistributedLock +{ + public class EtcdLockProvider : IDistributedLockProvider + { + private readonly IEtcdCaching _etcdClient; + + public EtcdLockProvider(IEtcdCaching etcdClient) + { + _etcdClient = etcdClient; + } + + public Task SetAsync(string key, byte[] value, int ttlMs) => + _etcdClient.SetAsync(key,value,TimeSpan.FromMilliseconds(ttlMs)); + + public bool Add(string key, byte[] value, int ttlMs) => + _etcdClient.Lock(key, TimeSpan.FromMilliseconds(ttlMs)); + + public Task AddAsync(string key, byte[] value, int ttlMs) => + _etcdClient.LockAsync(key,TimeSpan.FromMilliseconds(ttlMs)); + + public bool Delete(string key, byte[] value) => + _etcdClient.UnLock(key); + + + public async Task DeleteAsync(string key, byte[] value) => + await _etcdClient.UnLockAsnyc(key); + + public bool CanRetry(Exception ex) => ex is RpcException; + } +} diff --git a/src/EasyCaching.Etcd/EasyCaching.Etcd.csproj b/src/EasyCaching.Etcd/EasyCaching.Etcd.csproj new file mode 100644 index 00000000..d077ef77 --- /dev/null +++ b/src/EasyCaching.Etcd/EasyCaching.Etcd.csproj @@ -0,0 +1,41 @@ + + + + + + netstandard2.0;net8.0 + ncc;Catcher Wong + ncc;Catcher Wong + 10 + enable + true + $(EasyCachingEtcdPackageVersion) + + + A simple distributed caching provider based on ETCD. + + ETCD,DistributedCache,Caching,Cache + https://github.com/dotnetcore/EasyCaching + LICENSE + https://github.com/dotnetcore/EasyCaching + https://github.com/dotnetcore/EasyCaching + nuget-icon.png + + $(EasyCachingEtcdPackageNotes) + + + + + + + + + + + + + + + + + diff --git a/src/EasyCaching.Etcd/Internal/EtcdCaching.cs b/src/EasyCaching.Etcd/Internal/EtcdCaching.cs new file mode 100644 index 00000000..b1784af6 --- /dev/null +++ b/src/EasyCaching.Etcd/Internal/EtcdCaching.cs @@ -0,0 +1,456 @@ +using dotnet_etcd; +using EasyCaching.Core; +using EasyCaching.Core.Serialization; +using Etcdserverpb; +using Google.Protobuf; +using Google.Protobuf.WellKnownTypes; +using Grpc.Core; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using V3Lockpb; + +namespace EasyCaching.Etcd +{ + public class EtcdCaching : IEtcdCaching + { + private readonly ILogger? _logger; + private readonly IEasyCachingSerializer _serializer; + private readonly EtcdCachingOptions _options; + private readonly string _name; + + private readonly EtcdClient _etcdClient; + private readonly string _authToken; + private readonly Metadata _metadata; + + public EtcdCaching( + string name, + EtcdCachingOptions options, + IEnumerable serializers, + ILoggerFactory? loggerFactory = null) + { + ArgumentCheck.NotNull(options, nameof(options)); + ArgumentCheck.NotNull(serializers, nameof(serializers)); + + _name = name; + _options = options; + _logger = loggerFactory?.CreateLogger(); + + //init etcd client + this._etcdClient = new EtcdClient(connectionString: options.Address, configureChannelOptions: (x) => + { + x.Credentials = ChannelCredentials.Insecure; + x.LoggerFactory = loggerFactory; + }); + //auth + if (!string.IsNullOrEmpty(options.UserName) && !string.IsNullOrEmpty(options.Password)) + { + var authRes = this._etcdClient.Authenticate(new AuthenticateRequest() + { + Name = options.UserName, + Password = options.Password, + }); + _authToken = authRes.Token; + _metadata = new Metadata() { new Metadata.Entry("token", _authToken) }; + } + + 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)); + } + + public string ProviderName => this._name; + + #region etcd method + + /// + /// get data + /// + /// + /// + public CacheValue Get(string cacheKey) + { + var data = _etcdClient.GetVal(cacheKey, _metadata); + return string.IsNullOrWhiteSpace(data) + ? CacheValue.Null + : new CacheValue(_serializer.Deserialize(Encoding.UTF8.GetBytes(data)), true); + } + + /// + /// get data + /// + /// + /// + public async Task> GetAsync(string cacheKey) + { + var data = await _etcdClient.GetValAsync(cacheKey, _metadata); + return string.IsNullOrWhiteSpace(data) + ? CacheValue.Null + : new CacheValue(_serializer.Deserialize(Encoding.UTF8.GetBytes(data)), true); + } + + /// + /// get rangevalues + /// + /// + /// + public IDictionary GetAll(string prefixKey) + { + return _etcdClient.GetRangeVal(prefixKey, _metadata); + } + + /// + /// get rangevalues + /// + /// + /// + public async Task> GetAllAsync(string prefixKey) + { + return await _etcdClient.GetRangeValAsync(prefixKey, _metadata); + } + + /// + /// data exists + /// + /// + /// + public bool Exists(string cacheKey) + { + var data = _etcdClient.GetVal(cacheKey, _metadata); + return data == string.Empty ? false : true; + } + + /// + /// data exists + /// + /// + /// + public async Task ExistsAsync(string cacheKey) + { + var data = await _etcdClient.GetValAsync(cacheKey, _metadata); + return data == string.Empty ? false : true; + } + + /// + /// get rent leaseId + /// + /// + /// + private long GetRentLeaseId(TimeSpan? ts, CancellationTokenSource cts) + { + // create rent id to bind + var response = _etcdClient.LeaseGrant(request: new LeaseGrantRequest() + { + TTL = (long)(ts.Value.TotalMilliseconds < 1000 ? 1: ts.Value.TotalMilliseconds / 1000), + }, cancellationToken: cts.Token); + return response.ID; + } + + /// + /// get rent leaseId + /// + /// + /// + private async Task GetRentLeaseIdAsync(TimeSpan? ts) + { + // create rent id to bind + CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(_options.Timeout)); + var response = await _etcdClient.LeaseGrantAsync(request: new LeaseGrantRequest() + { + TTL = (long)(ts.Value.TotalMilliseconds < 1000 ? 1 : ts.Value.TotalMilliseconds / 1000), + }, deadline: DateTime.UtcNow.AddMilliseconds(_options.Timeout), cancellationToken: cts.Token); + return response.ID; + } + + /// + /// put ke-val with leaseId + /// + /// + /// + /// + /// + public bool Set(string key, T value, TimeSpan? ts) + { + try + { + CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(_options.Timeout)); + long leaseId = ts.HasValue ? GetRentLeaseId(ts,cts) : 0; + PutRequest request = new PutRequest() + { + Key = ByteString.CopyFromUtf8(key), + Value = ByteString.CopyFrom(_serializer.Serialize(value)), + Lease = leaseId + }; + var response = _etcdClient.Put(request: request, headers: _metadata, cancellationToken: cts.Token); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "putEphemeral(key:{},value:{}) error.", key, value); + } + return false; + } + + /// + /// put ke-val with leaseId + /// + /// + /// + /// + /// + public async Task SetAsync(string key, T value, TimeSpan? ts) + { + try + { + long leaseId = ts.HasValue ? await GetRentLeaseIdAsync(ts) : 0; + CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(_options.Timeout)); + PutRequest request = new PutRequest() + { + Key = ByteString.CopyFromUtf8(key), + Value = ByteString.CopyFrom(_serializer.Serialize(value)), + Lease = leaseId + }; + var response = await _etcdClient.PutAsync(request: request, headers: _metadata, cancellationToken: cts.Token); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex,"putEphemeral(key:{},value:{}) error.",key,value); + } + return false; + } + + /// + /// Lock + /// + /// + /// + /// + public bool Lock(string key, TimeSpan? ts) + { + try + { + CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(_options.Timeout)); + long leaseId = ts.HasValue ? GetRentLeaseId(ts,cts) : 0; + LockRequest request = new LockRequest() + { + Name = ByteString.CopyFromUtf8(key), + Lease = leaseId + }; + var response = _etcdClient.Lock(request: request, headers: _metadata, deadline: DateTime.UtcNow.AddMilliseconds(_options.Timeout), cancellationToken: cts.Token); + if (response?.Key == null || response.Key.IsEmpty) + { + return false; + } + return true; + } + catch (RpcException ex) when (ex.StatusCode == StatusCode.DeadlineExceeded) + { + _logger.LogError(ex, "Lock DeadlineExceeded (key:{}) error.", key); + } + catch (RpcException ex) when (ex.StatusCode == StatusCode.FailedPrecondition) + { + _logger.LogError(ex, "Lock FailedPrecondition (key:{}) error.", key); + } + catch (Exception ex) + { + _logger.LogError(ex, "Lock(key:{}) error.", key); + } + return false; + } + + /// + /// LockAsync + /// + /// + /// + /// + public async Task LockAsync(string key, TimeSpan? ts) + { + try + { + CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(_options.Timeout)); + long leaseId = ts.HasValue ? GetRentLeaseId(ts,cts) : 0; + LockRequest request = new LockRequest() + { + Name = ByteString.CopyFromUtf8(key), + Lease = leaseId + }; + var response = await _etcdClient.LockAsync(request: request, headers: _metadata, deadline: DateTime.UtcNow.AddMilliseconds(_options.Timeout), cancellationToken: cts.Token); + if (response?.Key == null || response.Key.IsEmpty) + { + return false; + } + return true; + } + catch (RpcException ex) when (ex.StatusCode == StatusCode.DeadlineExceeded) + { + _logger.LogError(ex, "LockAsync DeadlineExceeded (key:{}) error.", key); + } + catch (RpcException ex) when (ex.StatusCode == StatusCode.FailedPrecondition) + { + _logger.LogError(ex, "LockAsync FailedPrecondition (key:{}) error.", key); + } + catch (Exception ex) + { + _logger.LogError(ex, "LockAsync(key:{}) error.", key); + } + return false; + } + + /// + /// UnLock + /// releaseLock + /// + /// + /// + public bool UnLock(string key) + { + try + { + CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(_options.Timeout)); + var response = _etcdClient.Unlock(key, headers: _metadata, deadline: DateTime.UtcNow.AddMilliseconds(_options.Timeout), cancellationToken: cts.Token); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "UnLock(key:{}) error.", key); + } + return false; + } + + /// + /// UnLockAsync + /// + /// + /// + public async Task UnLockAsnyc(string key) + { + try + { + CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(_options.Timeout)); + var response = await _etcdClient.UnlockAsync(key, headers: _metadata, deadline: DateTime.UtcNow.AddMilliseconds(_options.Timeout), cancellationToken: cts.Token); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "UnLockAsync(key:{}) error.", key); + } + return false; + } + + /// + /// get key expireTTL + /// + /// + /// + public long GetExpireTTL(string key) + { + try + { + CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(_options.Timeout)); + var rangeResponse = _etcdClient.GetRange(key, headers: _metadata, cancellationToken: cts.Token); + if (rangeResponse != null && rangeResponse.Kvs != null && rangeResponse.Kvs.Count > 0) + { + var leaseId = rangeResponse.Kvs[0].Lease; + var leaseTimeToLiveResponse = _etcdClient.LeaseTimeToLive(new LeaseTimeToLiveRequest + { + ID = leaseId, + Keys = true + }); + + var remainingTtlSeconds = leaseTimeToLiveResponse.TTL; + return remainingTtlSeconds; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "GetExpireMsTTL(key:{}) error.", key); + } + return 0; + } + + /// + /// get key expireTTL + /// + /// + /// + public async Task GetExpireTTLAsync(string key) + { + try + { + CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(_options.Timeout)); + var rangeResponse = await _etcdClient.GetRangeAsync(key, headers: _metadata, cancellationToken: cts.Token); + if (rangeResponse != null && rangeResponse.Kvs != null && rangeResponse.Kvs.Count > 0) + { + var leaseId = rangeResponse.Kvs[0].Lease; + var leaseTimeToLiveResponse = await _etcdClient.LeaseTimeToLiveAsync(new LeaseTimeToLiveRequest + { + ID = leaseId, + Keys = true + }); + + var remainingTtlSeconds = leaseTimeToLiveResponse.TTL; + return remainingTtlSeconds ; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "GetExpireMsTTLAsync(key:{}) error.", key); + } + return 0; + } + + /// + /// delete key + /// + /// + /// + public long Delete(string key) + { + var response = _etcdClient.Delete(key, _metadata); + return response.Deleted; + } + + /// + /// delete key + /// + /// + /// + public async Task DeleteAsync(string key) + { + var response = await _etcdClient.DeleteAsync(key, _metadata); + return response.Deleted; + } + + /// + /// delete range key + /// + /// + /// + public long DeleteRangeData(string prefixKey) + { + var response = _etcdClient.DeleteRange(prefixKey, _metadata); + return response.Deleted; + } + + /// + /// delete range key + /// + /// + /// + public async Task DeleteRangeDataAsync(string prefixKey) + { + var response = await _etcdClient.DeleteRangeAsync(prefixKey, _metadata); + return response.Deleted; + } + + #endregion etcd method + } +} \ No newline at end of file diff --git a/src/EasyCaching.Etcd/Internal/IEtcdCaching.cs b/src/EasyCaching.Etcd/Internal/IEtcdCaching.cs new file mode 100644 index 00000000..dc354662 --- /dev/null +++ b/src/EasyCaching.Etcd/Internal/IEtcdCaching.cs @@ -0,0 +1,145 @@ +using EasyCaching.Core; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace EasyCaching.Etcd +{ + public interface IEtcdCaching + { + string ProviderName { get; } + + /// + /// get data + /// + /// + /// + CacheValue Get(string cacheKey); + + /// + /// get data + /// + /// + /// + Task> GetAsync(string cacheKey); + + /// + /// get rangevalues + /// + /// + /// + IDictionary GetAll(string prefixKey); + + /// + /// get rangevalues + /// + /// + /// + Task> GetAllAsync(string prefixKey); + + /// + /// data exists + /// + /// + /// + bool Exists(string cacheKey); + + /// + /// data exists + /// + /// + /// + Task ExistsAsync(string cacheKey); + + /// + /// put ke-val with leaseId + /// + /// + /// + /// + /// + bool Set(string key, T value, TimeSpan? ts); + + /// + /// put ke-val with leaseId + /// + /// + /// + /// + /// + Task SetAsync(string key, T value, TimeSpan? ts); + + /// + /// set lock with leaseId + /// + /// + /// + /// + bool Lock(string key, TimeSpan? ts); + + /// + /// set lock with leaseId + /// + /// + /// + /// + Task LockAsync(string key, TimeSpan? ts); + + /// + /// release lock + /// + /// + /// + bool UnLock(string key); + + /// + /// release lock + /// + /// + /// + /// + Task UnLockAsnyc(string key); + + /// + /// get key expireTTL + /// + /// + /// + long GetExpireTTL(string key); + + /// + /// get key expireTTL + /// + /// + /// + Task GetExpireTTLAsync(string key); + + /// + /// delete key + /// + /// + /// + long Delete(string key); + + /// + /// delete key + /// + /// + /// + Task DeleteAsync(string key); + + /// + /// delete range key + /// + /// + /// + long DeleteRangeData(string prefixKey); + + /// + /// delete range key + /// + /// + /// + Task DeleteRangeDataAsync(string prefixKey); + } +} \ No newline at end of file diff --git a/test/EasyCaching.UnitTests/CachingTests/EtcdCachingProviderTest.cs b/test/EasyCaching.UnitTests/CachingTests/EtcdCachingProviderTest.cs new file mode 100644 index 00000000..e91e7cab --- /dev/null +++ b/test/EasyCaching.UnitTests/CachingTests/EtcdCachingProviderTest.cs @@ -0,0 +1,93 @@ +using EasyCaching.Core; +using EasyCaching.Etcd; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using System; +using System.Threading.Tasks; +using Xunit; + +namespace EasyCaching.UnitTests.CachingTests +{ + public class EtcdCachingProviderTest //: BaseCachingProviderTest + { + private readonly string ProviderName = "EtcdTest"; + private readonly IEasyCachingProvider _provider; + + public EtcdCachingProviderTest() + { + // _defaultTs = TimeSpan.FromSeconds(30); + var services = getServiceCollection(); + IServiceProvider serviceProvider = services.BuildServiceProvider(); + _provider = serviceProvider.GetService(); + } + + //protected override IEasyCachingProvider CreateCachingProvider(Action additionalSetup) + //{ + // IServiceCollection services = getServiceCollection(); + // IServiceProvider serviceProvider = services.BuildServiceProvider(); + // return serviceProvider.GetService(); + //} + + private IServiceCollection getServiceCollection() + { + IServiceCollection services = new ServiceCollection(); + services.AddEasyCaching(option => + option.UseEtcd(options => + { + options.Address = "http://127.0.0.1:2379"; + options.Timeout = 30000; + options.SerializerName = "json"; + }, ProviderName).WithJson(jsonSerializerSettingsConfigure: x => + { + x.TypeNameHandling = Newtonsoft.Json.TypeNameHandling.None; + x.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; + }, "json")); + return services; + } + + + [Fact] + public void Set_And_Get_Should_Succeed() + { + _provider.Set("abc", "123", TimeSpan.FromSeconds(60)); + var val = _provider.Get("abc"); + Assert.Equal("123", val.Value); + } + + [Fact] + public async Task SetAsync_And_GetAsync_Should_Succeed() + { + await _provider.SetAsync("abcd", "1234", TimeSpan.FromSeconds(60)); + var val = await _provider.GetAsync("abcd"); + Assert.True(val.HasValue); + Assert.Equal("1234", val.Value); + } + + [Fact] + public void Remove_Should_Succeed() + { + _provider.Set("abcf", "123", TimeSpan.FromSeconds(60)); + _provider.Remove("abcf"); + } + + [Fact] + public async Task RemoveAsync_Should_Succeed() + { + await _provider.SetAsync("abcf", "123", TimeSpan.FromSeconds(60)); + await _provider.RemoveAsync("abcf"); + } + + + [Fact] + public void Use_Configuration_Options_Should_Succeed() + { + IServiceCollection services = getServiceCollection(); + IServiceProvider serviceProvider = services.BuildServiceProvider(); + var dbProvider = serviceProvider.GetService(); + Assert.NotNull(dbProvider); + + Assert.Equal(ProviderName, dbProvider.ProviderName); + } + + } +} diff --git a/test/EasyCaching.UnitTests/CachingTests/FasterKvCachingProviderTest.cs b/test/EasyCaching.UnitTests/CachingTests/FasterKvCachingProviderTest.cs index 6543931b..58cc40a1 100644 --- a/test/EasyCaching.UnitTests/CachingTests/FasterKvCachingProviderTest.cs +++ b/test/EasyCaching.UnitTests/CachingTests/FasterKvCachingProviderTest.cs @@ -164,4 +164,9 @@ protected override Task RemoveByPrefixAsync_Should_Succeed() { return Task.CompletedTask; } + + protected override void GetAll_Should_Succeed() + { + + } } \ No newline at end of file diff --git a/test/EasyCaching.UnitTests/DistributedLock/EtcdLockTest.cs b/test/EasyCaching.UnitTests/DistributedLock/EtcdLockTest.cs new file mode 100644 index 00000000..5b3b1c28 --- /dev/null +++ b/test/EasyCaching.UnitTests/DistributedLock/EtcdLockTest.cs @@ -0,0 +1,33 @@ +//using EasyCaching.Core; +//using EasyCaching.Core.Configurations; +//using EasyCaching.Core.DistributedLock; +//using EasyCaching.Redis; +//using EasyCaching.Redis.DistributedLock; +//using Google.Protobuf.WellKnownTypes; +//using Microsoft.Extensions.DependencyInjection; +//using Newtonsoft.Json; +//using Xunit.Abstractions; + +//namespace EasyCaching.UnitTests.DistributedLock +//{ +// public class EtcdLockTest : BaseDistributedLockTest +// { +// private static readonly IDistributedLockFactory Factory = new ServiceCollection() +// .AddLogging() +// .AddEasyCaching(option=>option.UseEtcd(options => +// { +// options.Address = "http://127.0.0.1:2379"; +// options.Timeout = 3000; +// options.LockMs = 10000; +// options.SerializerName = "json"; +// }).WithJson(jsonSerializerSettingsConfigure: x => +// { +// x.TypeNameHandling = Newtonsoft.Json.TypeNameHandling.None; +// x.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; +// }, "json").UseEtcdLock()) +// .BuildServiceProvider() +// .GetService(); + +// public EtcdLockTest(ITestOutputHelper output) : base(EasyCachingConstValue.DefaultEtcdName, Factory, output) { } +// } +//} diff --git a/test/EasyCaching.UnitTests/EasyCaching.UnitTests.csproj b/test/EasyCaching.UnitTests/EasyCaching.UnitTests.csproj index 4f531bac..b247c55c 100644 --- a/test/EasyCaching.UnitTests/EasyCaching.UnitTests.csproj +++ b/test/EasyCaching.UnitTests/EasyCaching.UnitTests.csproj @@ -31,6 +31,7 @@ +