diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ad85ec95..2b4ab1a7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,9 +16,9 @@ jobs: os: [ windows-latest ] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Setup .NET SDK 6.0.x - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v3 with: dotnet-version: 6.0.x diff --git a/.github/workflows/buildandtest.yml b/.github/workflows/buildandtest.yml index fb79d718..2466ba79 100644 --- a/.github/workflows/buildandtest.yml +++ b/.github/workflows/buildandtest.yml @@ -38,9 +38,9 @@ jobs: - 11212:11211 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Setup .NET SDK 6.0.x - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v3 with: dotnet-version: 6.0.x diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ef7049cc..61396945 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,9 +13,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Setup .NET Core - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v3 with: dotnet-version: 6.0.x - name: Build with dotnet @@ -23,7 +23,7 @@ jobs: - name: Pack with dotnet run: dotnet pack /home/runner/work/EasyCaching/EasyCaching/EasyCaching.sln --version-suffix alpha`date +%Y%m%d%H%M%S` -o /home/runner/work/nugetpkgs -c Release --no-build - name: Upload artifact - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v3 with: name: nugetpkgs path: /home/runner/work/nugetpkgs @@ -35,9 +35,10 @@ jobs: steps: - name: Download build artifacts - uses: actions/download-artifact@v1 + uses: actions/download-artifact@v3 with: name: nugetpkgs + path: nugetpkgs - name: list nugetpkgs run: ls nugetpkgs - name: Release diff --git a/.github/workflows/release_stable.yml b/.github/workflows/release_stable.yml index cb7a7eca..b4448612 100644 --- a/.github/workflows/release_stable.yml +++ b/.github/workflows/release_stable.yml @@ -11,9 +11,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Setup .NET Core - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v3 with: dotnet-version: 6.0.x - name: Build with dotnet @@ -21,7 +21,7 @@ jobs: - name: Pack with dotnet run: dotnet pack /home/runner/work/EasyCaching/EasyCaching/EasyCaching.sln -o /home/runner/work/nugetpkgs -c Release --no-build - name: Upload artifact - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v3 with: name: nugetpkgs path: /home/runner/work/nugetpkgs @@ -33,9 +33,10 @@ jobs: steps: - name: Download build artifacts - uses: actions/download-artifact@v1 + uses: actions/download-artifact@v3 with: name: nugetpkgs + path: nugetpkgs - name: list nugetpkgs run: ls nugetpkgs - name: Release diff --git a/EasyCaching.sln b/EasyCaching.sln index 7d07ad0a..9ae4c577 100644 --- a/EasyCaching.sln +++ b/EasyCaching.sln @@ -1,4 +1,5 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 + +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.2.32616.157 MinimumVisualStudioVersion = 10.0.40219.1 @@ -72,12 +73,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyCaching.Serialization.S EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyCaching.Bus.ConfluentKafka", "bus\EasyCaching.Bus.ConfluentKafka\EasyCaching.Bus.ConfluentKafka.csproj", "{F7FBADEB-D766-4595-949A-07104B52692C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyCaching.Bus.Zookeeper", "bus\EasyCaching.Bus.Zookeeper\EasyCaching.Bus.Zookeeper.csproj", "{5E488583-391E-4E15-83C1-7301B4FE79AE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "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}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyCaching.FasterKv", "src\EasyCaching.FasterKv\EasyCaching.FasterKv.csproj", "{7191E567-38DF-4879-82E1-73EC618AFCAC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyCaching.Bus.RabbitMQStream", "bus\EasyCaching.Bus.RabbitMQStream\EasyCaching.Bus.RabbitMQStream.csproj", "{3C9D5E40-B3A5-4649-8B40-08094644B0FB}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyCaching.Serialization.MemoryPack", "serialization\EasyCaching.Serialization.MemoryPack\EasyCaching.Serialization.MemoryPack.csproj", "{EEF22C21-F380-4980-B72C-F14488369333}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyCaching.Demo.Locks", "sample\EasyCaching.Demo.Locks\EasyCaching.Demo.Locks.csproj", "{9B15A0A0-BD6B-40B0-90D4-848BC3E4AF98}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -208,6 +213,14 @@ Global {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 + {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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -244,6 +257,8 @@ Global {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} + {9B15A0A0-BD6B-40B0-90D4-848BC3E4AF98} = {F88D727A-9F9C-43D9-90B1-D4A02BF8BC98} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {63A57886-054B-476C-AAE1-8D7C8917682E} diff --git a/build/releasenotes.props b/build/releasenotes.props index 1fe50f01..68666432 100644 --- a/build/releasenotes.props +++ b/build/releasenotes.props @@ -14,6 +14,7 @@ 1. Upgrading dependencies. + 2. Fix no check expires when get cache count 1. Upgrading dependencies. @@ -59,6 +60,7 @@ 1. Upgrading dependencies. + 2. Always use utc to compare the expiry 1. Upgrading dependencies. @@ -69,5 +71,8 @@ 1. Upgrading dependencies. + + 1. Initial release. + diff --git a/build/version.props b/build/version.props index 5dc47ec6..18841930 100644 --- a/build/version.props +++ b/build/version.props @@ -1,28 +1,29 @@ - 1.9.1 - 1.9.1 - 1.9.1 - 1.9.1 - 1.9.1 - 1.9.1 - 1.9.1 - 1.9.1 - 1.9.1 - 1.9.1 - 1.9.1 - 1.9.1 - 1.9.1 - 1.9.1 - 1.9.1 - 1.9.1 - 1.9.1 - 1.9.1 - 1.9.1 - 1.9.1 - 1.9.1 - 1.9.1 - 1.9.1 - 1.9.1 + 1.9.2 + 1.9.2 + 1.9.2 + 1.9.2 + 1.9.2 + 1.9.2 + 1.9.2 + 1.9.2 + 1.9.2 + 1.9.2 + 1.9.2 + 1.9.2 + 1.9.2 + 1.9.2 + 1.9.2 + 1.9.2 + 1.9.2 + 1.9.2 + 1.9.2 + 1.9.2 + 1.9.2 + 1.9.2 + 1.9.2 + 1.9.2 + 1.9.2 diff --git a/bus/EasyCaching.Bus.RabbitMQ/Configurations/RabbitMQBusOptions.cs b/bus/EasyCaching.Bus.RabbitMQ/Configurations/RabbitMQBusOptions.cs index f5a09bf6..359568fe 100644 --- a/bus/EasyCaching.Bus.RabbitMQ/Configurations/RabbitMQBusOptions.cs +++ b/bus/EasyCaching.Bus.RabbitMQ/Configurations/RabbitMQBusOptions.cs @@ -6,7 +6,7 @@ /// RabbitMQ Bus options. /// public class RabbitMQBusOptions : BaseRabbitMQOptions - { + { /// /// Gets or sets the name of the queue. /// diff --git a/bus/EasyCaching.Bus.RabbitMQStream/Configurations/EasyCachingOptionsExtensions.cs b/bus/EasyCaching.Bus.RabbitMQStream/Configurations/EasyCachingOptionsExtensions.cs new file mode 100644 index 00000000..9edbb383 --- /dev/null +++ b/bus/EasyCaching.Bus.RabbitMQStream/Configurations/EasyCachingOptionsExtensions.cs @@ -0,0 +1,68 @@ +namespace Microsoft.Extensions.DependencyInjection +{ + using System; + using EasyCaching.Bus.RabbitMQ; + using EasyCaching.Bus.RabbitMQStream; + using EasyCaching.Core; + using EasyCaching.Core.Configurations; + using Microsoft.Extensions.Configuration; + + /// + /// EasyCaching options extensions. + /// + public static class EasyCachingOptionsExtensions + { + /// + /// Withs the RabbitMQStream bus (specify the config via hard code). + /// + /// Options. + /// Configure bus settings. + + public static EasyCachingOptions WithRabbitMQStreamBus( + this EasyCachingOptions options + , Action configure + ) + { + ArgumentCheck.NotNull(configure, nameof(configure)); + + options.RegisterExtension(new RabbitMQStreamBusOptionsExtension(configure)); + return options; + } + + /// + /// Withs the RabbitMQStream bus (read config from configuration file). + /// + /// Options. + /// The configuration. + /// The section name in the configuration file. + public static EasyCachingOptions WithRabbitMQStreamBus( + this EasyCachingOptions options + , IConfiguration configuration + , string sectionName = EasyCachingConstValue.RabbitMQBusSection + ) + { + var dbConfig = configuration.GetSection(sectionName); + var busOptions = new RabbitMQBusOptions(); + dbConfig.Bind(busOptions); + + void configure(RabbitMQBusOptions x) + { + x.HostName = busOptions.HostName; + x.Password = busOptions.Password; + x.Port = busOptions.Port; + x.QueueMessageExpires = busOptions.QueueMessageExpires; + x.RequestedConnectionTimeout = busOptions.RequestedConnectionTimeout; + //x.RouteKey = busOptions.RouteKey; + x.SocketReadTimeout = busOptions.SocketReadTimeout; + x.SocketWriteTimeout = busOptions.SocketWriteTimeout; + x.TopicExchangeName = busOptions.TopicExchangeName; + x.UserName = busOptions.UserName; + x.VirtualHost = busOptions.VirtualHost; + x.QueueName = busOptions.QueueName; + } + + options.RegisterExtension(new RabbitMQStreamBusOptionsExtension(configure)); + return options; + } + } +} \ No newline at end of file diff --git a/bus/EasyCaching.Bus.RabbitMQStream/Configurations/RabbitMQStreamBusOptionsExtension.cs b/bus/EasyCaching.Bus.RabbitMQStream/Configurations/RabbitMQStreamBusOptionsExtension.cs new file mode 100644 index 00000000..275cf5ae --- /dev/null +++ b/bus/EasyCaching.Bus.RabbitMQStream/Configurations/RabbitMQStreamBusOptionsExtension.cs @@ -0,0 +1,43 @@ +namespace EasyCaching.Bus.RabbitMQStream +{ + using System; + using EasyCaching.Bus.RabbitMQ; + using EasyCaching.Core.Bus; + using EasyCaching.Core.Configurations; + using global::RabbitMQ.Client; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.ObjectPool; + + /// + /// RabbitMQ Bus options extension. + /// + public class RabbitMQStreamBusOptionsExtension : IEasyCachingOptionsExtension + { + /// + /// The configure. + /// + private readonly Action configure; + + /// + /// Initializes a new instance of the class. + /// + /// Configure. + public RabbitMQStreamBusOptionsExtension(Action configure) + { + this.configure = configure; + } + + /// + /// Adds the services. + /// + /// Services. + public void AddServices(IServiceCollection services) + { + services.AddOptions(); + services.Configure(configure); + + services.AddSingleton, ModelPooledObjectPolicy>(); + services.AddSingleton(); + } + } +} diff --git a/bus/EasyCaching.Bus.RabbitMQStream/DefaultRabbitMQStreamBus.cs b/bus/EasyCaching.Bus.RabbitMQStream/DefaultRabbitMQStreamBus.cs new file mode 100644 index 00000000..ac602cdb --- /dev/null +++ b/bus/EasyCaching.Bus.RabbitMQStream/DefaultRabbitMQStreamBus.cs @@ -0,0 +1,251 @@ +using Microsoft.Extensions.Logging; + +namespace EasyCaching.Bus.RabbitMQ +{ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using EasyCaching.Core; + using EasyCaching.Core.Bus; + using EasyCaching.Core.Serialization; + using global::RabbitMQ.Client; + using global::RabbitMQ.Client.Events; + using Microsoft.Extensions.ObjectPool; + using Microsoft.Extensions.Options; + + /// + /// Default RabbitMQ Bus. + /// + public class DefaultRabbitMQStreamBus : EasyCachingAbstractBus + { + /// + /// The subscriber connection. + /// + private readonly IConnection _subConnection; + + /// + /// The publish channel pool. + /// + private readonly ObjectPool _pubChannelPool; + + /// + /// The rabbitMQ Bus options. + /// + private readonly RabbitMQBusOptions _options; + + /// + /// The serializer. + /// + private readonly IEasyCachingSerializer _serializer; + + /// + /// The logger. + /// + private readonly ILogger _logger; + + /// + /// The identifier. + /// + private readonly string _busId; + + private static readonly Dictionary _streamArgs = new Dictionary + { + { "x-queue-type", "stream" }, + { "x-max-age", "5m" }, + { "x-stream-max-segment-size-bytes", 4_000_000 } + }; + + /// + /// Initializes a new instance of the class. + /// + /// Object policy. + /// RabbitMQ Options. + /// Serializer. + /// Logger. + public DefaultRabbitMQStreamBus( + IPooledObjectPolicy objectPolicy + , IOptions rabbitMQOptions + , IEasyCachingSerializer serializer + , Lazy> logger) + { + this._options = rabbitMQOptions.Value; + this._serializer = serializer; + this._logger = logger.Value; + + _logger?.LogInformation("DefaultRabbitMQStreamBus: Initializing"); + try + { + var factory = new ConnectionFactory + { + HostName = _options.HostName, + UserName = _options.UserName, + Port = _options.Port, + Password = _options.Password, + VirtualHost = _options.VirtualHost, + RequestedConnectionTimeout = System.TimeSpan.FromMilliseconds(_options.RequestedConnectionTimeout), + SocketReadTimeout = System.TimeSpan.FromMilliseconds(_options.SocketReadTimeout), + SocketWriteTimeout = System.TimeSpan.FromMilliseconds(_options.SocketWriteTimeout), + ClientProvidedName = _options.ClientProvidedName + }; + + _subConnection = factory.CreateConnection(); + + _subConnection.ConnectionShutdown += (_, e) => + { + _logger?.LogError($"ConnectionShutdown: {e.ReplyText}"); + }; + + var provider = new DefaultObjectPoolProvider(); + + _pubChannelPool = provider.Create(objectPolicy); + + _busId = Guid.NewGuid().ToString("N"); + + BusName = "easycachingbus"; + } + catch (Exception ex) + { + _logger?.LogError(ex, $"DefaultRabbitMQStreamBus: Initializing failed ({ex.Message})"); + throw; + } + } + + /// + /// Publish the specified topic and message. + /// + /// Topic. + /// Message. + public override void BasePublish(string topic, EasyCachingMessage message) + { + var channel = _pubChannelPool.Get(); + + try + { + var body = _serializer.Serialize(message); + + channel.ExchangeDeclare(_options.TopicExchangeName, ExchangeType.Topic, true, false, null); + channel.BasicPublish(_options.TopicExchangeName, topic, false, null, body); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + finally + { + _pubChannelPool.Return(channel); + } + } + + /// + /// Publish the specified topic and message async. + /// + /// The async. + /// Topic. + /// Message. + /// Cancellation token. + public override Task BasePublishAsync(string topic, EasyCachingMessage message, CancellationToken cancellationToken = default(CancellationToken)) + { + var channel = _pubChannelPool.Get(); + try + { + var body = _serializer.Serialize(message); + + channel.ExchangeDeclare(_options.TopicExchangeName, ExchangeType.Topic, true, false, null); + channel.BasicPublish(_options.TopicExchangeName, topic, false, null, body); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + finally + { + _pubChannelPool.Return(channel); + } + return Task.CompletedTask; + } + + /// + /// Subscribe the specified topic and action. + /// + /// Topic. + /// Action. + public override void BaseSubscribe(string topic, Action action) + { + var queueName = $"rmq.stream.easycaching.{topic}"; + + Task.Factory.StartNew( + () => StartConsumer(queueName, topic), + TaskCreationOptions.LongRunning); + } + + /// + /// Subscribe the specified topic and action async. + /// + /// Topic. + /// Action. + /// Cancellation token. + public override Task BaseSubscribeAsync(string topic, Action action, CancellationToken cancellationToken = default(CancellationToken)) + { + _logger?.LogWarning($"BaseSubscribeAsync for {topic}"); + + var queueName = $"rmq.stream.easycaching.{topic}"; + + StartConsumer(queueName, topic); + return Task.CompletedTask; + } + + private void StartConsumer(string queueName, string topic) + { + _logger?.LogWarning($"Starting Consumer for {topic} ({queueName})"); + var channel = _subConnection.CreateModel(); + + channel.ModelShutdown += (_, e) => + { + _logger?.LogError($"ModelShutdown: {e.ReplyText}"); + }; + + // Streams require Qos setup + channel.BasicQos(0, 100, false); + channel.ExchangeDeclare(_options.TopicExchangeName, ExchangeType.Topic, true, false, null); + channel.QueueDeclare(queueName, true, false, false, _streamArgs); + // bind the queue with the exchange. + channel.QueueBind(queueName, _options.TopicExchangeName, topic); + var consumer = new EventingBasicConsumer(channel); + consumer.Received += OnMessage; + + consumer.ConsumerCancelled += (sender, e) => + { + _logger?.LogError($"EasyCaching.DefaultRabbitMQStreamBus.OnConsumerCancelled: (Q: {queueName})"); + }; + + consumer.Shutdown += (sender, e) => + { + _logger?.LogError($"EasyCaching.DefaultRabbitMQStreamBus.OnConsumerShutdown: (Q: {queueName}) {e.ReplyText}"); + StartConsumer(queueName, topic); + BaseOnReconnect(); + }; + + channel.BasicConsume(queueName, false, consumer); + } + + /// + /// Ons the message. + /// + /// Sender. + /// E. + private void OnMessage(object sender, BasicDeliverEventArgs e) + { + try + { + var message = _serializer.Deserialize(e.Body.ToArray()); + _logger?.LogDebug(string.Join(",", message.CacheKeys)); + BaseOnMessage(message); + } + finally + { + (sender as EventingBasicConsumer)?.Model.BasicAck(e.DeliveryTag, false); + } + } + } +} diff --git a/bus/EasyCaching.Bus.RabbitMQStream/EasyCaching.Bus.RabbitMQStream.csproj b/bus/EasyCaching.Bus.RabbitMQStream/EasyCaching.Bus.RabbitMQStream.csproj new file mode 100644 index 00000000..4a1d9b02 --- /dev/null +++ b/bus/EasyCaching.Bus.RabbitMQStream/EasyCaching.Bus.RabbitMQStream.csproj @@ -0,0 +1,50 @@ + + + + + netstandard2.0;net6.0 + ncc;Thomas Sarmis + ncc;Thomas Sarmis + $(EasyCachingRabbitStreamBusPackageVersion) + + + A simple caching bus(message bus) based on RabbitMQ using amqp Streams. + + Bus,Hybrid,RabbitMQ,Caching,Cache + https://github.com/dotnetcore/EasyCaching + LICENSE + https://github.com/dotnetcore/EasyCaching + https://github.com/dotnetcore/EasyCaching + nuget-icon.png + + $(EasyCachingRabbitStreamBusPackageNotes) + + + + + true + $(NoWarn);1591 + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sample/EasyCaching.Demo.Locks/Controllers/LocksController.cs b/sample/EasyCaching.Demo.Locks/Controllers/LocksController.cs new file mode 100644 index 00000000..b6400a70 --- /dev/null +++ b/sample/EasyCaching.Demo.Locks/Controllers/LocksController.cs @@ -0,0 +1,70 @@ +namespace EasyCaching.Demo.Locks.Controllers; + +[Route("api/[controller]")] +public class LocksController : Controller +{ + private readonly IDistributedLockFactory _distributedLockFactory; + + public LocksController(IDistributedLockFactory distributedLockFactory) + { + _distributedLockFactory = distributedLockFactory; + } + + [HttpPost("distributed-locking")] + public async Task DistributedLockingOperation(int millisecondsTimeout) + { + using var distributedLock = _distributedLockFactory.CreateLock("DefaultRedis", "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(); + } + } + + [HttpPost("memory-locking")] + public async Task MemoryLockingOperation(int millisecondsTimeout) + { + using var memoryLock = _distributedLockFactory.CreateLock("DefaultInMemory", "YourKey"); + + try + { + if (await memoryLock.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 memoryLock.ReleaseAsync(); + } + } +} diff --git a/sample/EasyCaching.Demo.Locks/EasyCaching.Demo.Locks.csproj b/sample/EasyCaching.Demo.Locks/EasyCaching.Demo.Locks.csproj new file mode 100644 index 00000000..dc06abc5 --- /dev/null +++ b/sample/EasyCaching.Demo.Locks/EasyCaching.Demo.Locks.csproj @@ -0,0 +1,20 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + + diff --git a/sample/EasyCaching.Demo.Locks/GlobalUsings.cs b/sample/EasyCaching.Demo.Locks/GlobalUsings.cs new file mode 100644 index 00000000..838d0c76 --- /dev/null +++ b/sample/EasyCaching.Demo.Locks/GlobalUsings.cs @@ -0,0 +1,4 @@ +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 diff --git a/sample/EasyCaching.Demo.Locks/Program.cs b/sample/EasyCaching.Demo.Locks/Program.cs new file mode 100644 index 00000000..c2d00cdb --- /dev/null +++ b/sample/EasyCaching.Demo.Locks/Program.cs @@ -0,0 +1,53 @@ +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +builder.Services.AddEasyCaching(option => +{ + //use memory cache + option.UseInMemory() + .UseMemoryLock(); // use memory lock + + //use redis cache + option.UseRedis(config => + { + config.DBConfig.Endpoints.Add(new ServerEndPoint("127.0.0.1", 6379)); + config.DBConfig.SyncTimeout = 10000; + config.DBConfig.AsyncTimeout = 10000; + config.SerializerName = "NewtonsoftJson"; + }) + .WithJson()//with josn serialization + .UseRedisLock(); // use distributed lock +}); + +#region How Inject Distributed and Memory lock + +// inject to use distributed lock +builder.Services.AddSingleton(); + +// inject to use memory lock +builder.Services.AddSingleton(); + +#endregion + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/sample/EasyCaching.Demo.Locks/appsettings.Development.json b/sample/EasyCaching.Demo.Locks/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/sample/EasyCaching.Demo.Locks/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/sample/EasyCaching.Demo.Locks/appsettings.json b/sample/EasyCaching.Demo.Locks/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/sample/EasyCaching.Demo.Locks/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/EasyCaching.CSRedis/EasyCaching.CSRedis.csproj b/src/EasyCaching.CSRedis/EasyCaching.CSRedis.csproj index 9ddaee11..c651637a 100644 --- a/src/EasyCaching.CSRedis/EasyCaching.CSRedis.csproj +++ b/src/EasyCaching.CSRedis/EasyCaching.CSRedis.csproj @@ -32,7 +32,7 @@ - + diff --git a/src/EasyCaching.InMemory/Internal/InMemoryCaching.cs b/src/EasyCaching.InMemory/Internal/InMemoryCaching.cs index 408f7fa8..913ebb10 100644 --- a/src/EasyCaching.InMemory/Internal/InMemoryCaching.cs +++ b/src/EasyCaching.InMemory/Internal/InMemoryCaching.cs @@ -49,8 +49,8 @@ public void Clear(string prefix = "") public int GetCount(string prefix = "") { return string.IsNullOrWhiteSpace(prefix) - ? _memory.Count - : _memory.Count(x => x.Key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)); + ? _memory.Values.Where(x => x.ExpiresAt > SystemClock.UtcNow).Count() + : _memory.Values.Where(x => x.Key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) && x.ExpiresAt > SystemClock.UtcNow).Count(); } internal void RemoveExpiredKey(string key) @@ -283,7 +283,7 @@ public int RemoveByPattern(string searchKey, SearchKeyPattern searchPattern) public IEnumerable GetAllKeys(string prefix) { return _memory.Values.Where(x => x.Key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) && x.ExpiresAt > SystemClock.UtcNow) - .Select(x=> x.Key).ToList(); + .Select(x => x.Key).ToList(); } private static bool FilterByPattern(string key, string searchKey, SearchKeyPattern searchKeyPattern) @@ -314,10 +314,10 @@ public IDictionary> GetAll(IEnumerable keys) public IDictionary> GetAll(string prefix = "") { - var values = string.IsNullOrEmpty(prefix) - ? _memory.Values.Where(x => x.ExpiresAt > SystemClock.UtcNow) + var values = string.IsNullOrEmpty(prefix) + ? _memory.Values.Where(x => x.ExpiresAt > SystemClock.UtcNow) : _memory.Values.Where(x => x.Key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) && x.ExpiresAt > SystemClock.UtcNow); - + return values.ToDictionary(k => k.Key, v => new CacheValue(v.GetValue(_options.EnableReadDeepClone), true)); } diff --git a/src/EasyCaching.LiteDB/DefaultLiteDBCachingProvider.cs b/src/EasyCaching.LiteDB/DefaultLiteDBCachingProvider.cs index 908919a6..bf2e5809 100644 --- a/src/EasyCaching.LiteDB/DefaultLiteDBCachingProvider.cs +++ b/src/EasyCaching.LiteDB/DefaultLiteDBCachingProvider.cs @@ -113,7 +113,7 @@ public override bool BaseExists(string cacheKey) { ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); - var dbResult = _cache.Count(fc => fc.cachekey == cacheKey && fc.expiration > DateTimeOffset.Now.ToUnixTimeSeconds()); + var dbResult = _cache.Count(fc => fc.cachekey == cacheKey && fc.expiration > DateTimeOffset.UtcNow.ToUnixTimeSeconds()); return dbResult > 0; } @@ -131,7 +131,7 @@ public override CacheValue BaseGet(string cacheKey, Func dataRetriever, ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); ArgumentCheck.NotNegativeOrZero(expiration, nameof(expiration)); - var cacheItem = _cache.FindOne(c => c.cachekey == cacheKey && c.expiration > DateTimeOffset.Now.ToUnixTimeSeconds()); + var cacheItem = _cache.FindOne(c => c.cachekey == cacheKey && c.expiration > DateTimeOffset.UtcNow.ToUnixTimeSeconds()); if (cacheItem != null) { @@ -164,7 +164,7 @@ public override CacheValue BaseGet(string cacheKey) { ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); - var cacheItem = _cache.FindOne(c => c.cachekey == cacheKey && c.expiration > DateTimeOffset.Now.ToUnixTimeSeconds()); + var cacheItem = _cache.FindOne(c => c.cachekey == cacheKey && c.expiration > DateTimeOffset.UtcNow.ToUnixTimeSeconds()); if (cacheItem != null || _options.CacheNulls) { @@ -317,7 +317,7 @@ public override IDictionary> BaseGetAll(IEnumerable lst.Contains(c.cachekey) && c.expiration > DateTimeOffset.Now.ToUnixTimeSeconds()).ToList(); + var list = _cache.Find(c => lst.Contains(c.cachekey) && c.expiration > DateTimeOffset.UtcNow.ToUnixTimeSeconds()).ToList(); return GetDict(list); } @@ -354,7 +354,7 @@ private IDictionary> GetDict(List list) public override IDictionary> BaseGetByPrefix(string prefix) { ArgumentCheck.NotNullOrWhiteSpace(prefix, nameof(prefix)); - var list = _cache.Find(c => c.cachekey.StartsWith(prefix) && c.expiration > DateTimeOffset.Now.ToUnixTimeSeconds()).ToList(); + var list = _cache.Find(c => c.cachekey.StartsWith(prefix) && c.expiration > DateTimeOffset.UtcNow.ToUnixTimeSeconds()).ToList(); return GetDict(list); } @@ -380,11 +380,11 @@ public override int BaseGetCount(string prefix = "") { if (string.IsNullOrWhiteSpace(prefix)) { - return _cache.Count(c => c.expiration > DateTimeOffset.Now.ToUnixTimeSeconds()); + return _cache.Count(c => c.expiration > DateTimeOffset.UtcNow.ToUnixTimeSeconds()); } else { - return _cache.Count(c => c.cachekey.StartsWith(prefix) && c.expiration > DateTimeOffset.Now.ToUnixTimeSeconds()); + return _cache.Count(c => c.cachekey.StartsWith(prefix) && c.expiration > DateTimeOffset.UtcNow.ToUnixTimeSeconds()); } } @@ -413,7 +413,7 @@ public override bool BaseTrySet(string cacheKey, T cacheValue, TimeSpan expir expiration.Add(new TimeSpan(0, 0, addSec)); } - var r = _cache.FindOne(c => c.cachekey == cacheKey && c.expiration > DateTimeOffset.Now.ToUnixTimeSeconds()); + var r = _cache.FindOne(c => c.cachekey == cacheKey && c.expiration > DateTimeOffset.UtcNow.ToUnixTimeSeconds()); bool result = false; if (r == null) { diff --git a/src/EasyCaching.LiteDB/EasyCaching.LiteDB.csproj b/src/EasyCaching.LiteDB/EasyCaching.LiteDB.csproj index 2f6f6724..4c3ccb2b 100644 --- a/src/EasyCaching.LiteDB/EasyCaching.LiteDB.csproj +++ b/src/EasyCaching.LiteDB/EasyCaching.LiteDB.csproj @@ -35,7 +35,7 @@ - + diff --git a/test/EasyCaching.UnitTests/CachingTests/CSRedisCachingProviderTest.cs b/test/EasyCaching.UnitTests/CachingTests/CSRedisCachingProviderTest.cs index c90a2343..8ded8e1a 100644 --- a/test/EasyCaching.UnitTests/CachingTests/CSRedisCachingProviderTest.cs +++ b/test/EasyCaching.UnitTests/CachingTests/CSRedisCachingProviderTest.cs @@ -41,6 +41,32 @@ protected override IEasyCachingProvider CreateCachingProvider(Action(); } + /*[Fact] + public async void Use_Redis6_ACL_Should_Succeed() + { + IServiceCollection services = new ServiceCollection(); + services.AddEasyCaching(x => + x.UseCSRedis(options => + { + options.DBConfig = new CSRedisDBOptions + { + ConnectionStrings = new System.Collections.Generic.List + { + "127.0.0.1:6388,user=user,password=userpwd,defaultDatabase=13,poolsize=10" + } + }; + }).UseCSRedisLock().WithJson(EasyCachingConstValue.DefaultCSRedisName)); + IServiceProvider serviceProvider = services.BuildServiceProvider(); + var provider = serviceProvider.GetService(); + var key = Guid.NewGuid().ToString(); + var value = "value"; + await provider.SetAsync(key, value, TimeSpan.FromSeconds(20)); + + var getValue = await provider.GetAsync(key); + + Assert.Equal(value, getValue?.Value); + }*/ + [Fact] public void GetDatabase_Should_Succeed() { @@ -148,7 +174,7 @@ public CSRedisCachingProviderWithKeyPrefixTest() config.SerializerName = "json"; }, "WithKeyPrefix"); - + x.WithJson("json"); }); @@ -194,7 +220,7 @@ public void RemoveByPrefixTest() Assert.False(val3.HasValue); Assert.False(val4.HasValue); } - + [Theory] [InlineData("WithKeyPrefix")] [InlineData("NotKeyPrefix")] @@ -219,7 +245,7 @@ public void RemoveByKeyPatternTest(string provider) var val6 = WithKeyPrefix.Get("sky:birds:bar"); var val7 = WithKeyPrefix.Get("sky:birds:test:bar"); var val8 = WithKeyPrefix.Get("akey"); - + Assert.True(val1.HasValue); Assert.True(val2.HasValue); Assert.True(val3.HasValue); @@ -231,15 +257,15 @@ public void RemoveByKeyPatternTest(string provider) // contains WithKeyPrefix.RemoveByPattern("*:pots:*"); - + // postfix WithKeyPrefix.RemoveByPattern("*foo"); - + // prefix - WithKeyPrefix.RemoveByPattern("sky*"); - + WithKeyPrefix.RemoveByPattern("sky*"); + // exact - WithKeyPrefix.RemoveByPattern("akey"); + WithKeyPrefix.RemoveByPattern("akey"); var val9 = WithKeyPrefix.Get("garden:pots:flowers"); var val10 = WithKeyPrefix.Get("garden:pots:flowers:test"); @@ -249,7 +275,7 @@ public void RemoveByKeyPatternTest(string provider) var val14 = WithKeyPrefix.Get("sky:birds:bar"); var val15 = WithKeyPrefix.Get("sky:birds:test:bar"); var val16 = WithKeyPrefix.Get("akey"); - + Assert.False(val9.HasValue); Assert.False(val10.HasValue); Assert.True(val11.HasValue); @@ -259,8 +285,8 @@ public void RemoveByKeyPatternTest(string provider) Assert.False(val15.HasValue); Assert.False(val16.HasValue); } - - [Theory] + + [Theory] [InlineData("WithKeyPrefix")] [InlineData("NotKeyPrefix")] public async Task RemoveByKeyPatternAsyncTest(string provider) @@ -284,7 +310,7 @@ public async Task RemoveByKeyPatternAsyncTest(string provider) var val6 = WithKeyPrefix.Get("sky:birds:bar"); var val7 = WithKeyPrefix.Get("sky:birds:test:bar"); var val8 = WithKeyPrefix.Get("akey"); - + Assert.True(val1.HasValue); Assert.True(val2.HasValue); Assert.True(val3.HasValue); @@ -296,15 +322,15 @@ public async Task RemoveByKeyPatternAsyncTest(string provider) // contains await WithKeyPrefix.RemoveByPatternAsync("*:pots:*"); - + // postfix await WithKeyPrefix.RemoveByPatternAsync("*foo"); - + // prefix - await WithKeyPrefix.RemoveByPatternAsync("sky*"); - + await WithKeyPrefix.RemoveByPatternAsync("sky*"); + // exact - await WithKeyPrefix.RemoveByPatternAsync("akey"); + await WithKeyPrefix.RemoveByPatternAsync("akey"); var val9 = WithKeyPrefix.Get("garden:pots:flowers"); var val10 = WithKeyPrefix.Get("garden:pots:flowers:test"); @@ -314,7 +340,7 @@ public async Task RemoveByKeyPatternAsyncTest(string provider) var val14 = WithKeyPrefix.Get("sky:birds:bar"); var val15 = WithKeyPrefix.Get("sky:birds:test:bar"); var val16 = WithKeyPrefix.Get("akey"); - + Assert.False(val9.HasValue); Assert.False(val10.HasValue); Assert.True(val11.HasValue); diff --git a/test/EasyCaching.UnitTests/CachingTests/FasterKvCachingProviderTest.cs b/test/EasyCaching.UnitTests/CachingTests/FasterKvCachingProviderTest.cs index d16ecf8c..6543931b 100644 --- a/test/EasyCaching.UnitTests/CachingTests/FasterKvCachingProviderTest.cs +++ b/test/EasyCaching.UnitTests/CachingTests/FasterKvCachingProviderTest.cs @@ -77,7 +77,7 @@ protected override void Get_Parallel_Should_Succeed() } public override Task GetAllByPrefix_Async_Should_Throw_ArgumentNullException_When_Prefix_IsNullOrWhiteSpace( - string preifx) + string preifx) { return Task.CompletedTask; } @@ -92,68 +92,71 @@ protected override Task Get_Count_Async_With_Prefix_Should_Succeed() return Task.CompletedTask; } - protected override Task Get_Count_Async_Without_Prefix_Should_Succeed() + public override void RemoveByPattern_Should_Succeed() { - return Task.CompletedTask; } - protected override void Get_Count_With_Prefix_Should_Succeed() + public override Task RemoveByPatternAsync_Should_Succeed() { + return Task.CompletedTask; } - protected override void Get_Count_Without_Prefix_Should_Succeed() - { - } + public override Task RemoveByPrefix_Async_Should_Throw_ArgumentNullException_When_Prefix_IsNullOrWhiteSpace( + string preifx) - protected override void GetByPrefix_Should_Succeed() { + return Task.CompletedTask; } - protected override void GetByPrefix_With_Not_Existed_Prefix_Should_Return_Empty_Dict() + public override void RemoveByPrefix_Should_Throw_ArgumentNullException_When_CacheKey_IsNullOrWhiteSpace( + string prefix) { } - protected override Task GetByPrefixAsync_Should_Succeed() + protected override Task Get_Count_Async_Without_Prefix_Should_Succeed() { return Task.CompletedTask; } - protected override Task GetByPrefixAsync_With_Not_Existed_Prefix_Should_Return_Empty_Dict() + + protected override void Get_Count_With_Prefix_Should_Succeed() { - return Task.CompletedTask; } - protected override Task GetExpiration_Async_Should_Succeed() + protected override void Get_Count_Without_Prefix_Should_Succeed() { - return Task.CompletedTask; } - protected override void GetExpiration_Should_Succeed() + protected override void GetByPrefix_Should_Succeed() { } - public override void RemoveByPattern_Should_Succeed() + protected override void GetByPrefix_With_Not_Existed_Prefix_Should_Return_Empty_Dict() { } - public override Task RemoveByPatternAsync_Should_Succeed() + protected override Task GetByPrefixAsync_Should_Succeed() { return Task.CompletedTask; } - public override Task RemoveByPrefix_Async_Should_Throw_ArgumentNullException_When_Prefix_IsNullOrWhiteSpace( - string preifx) + + protected override Task GetByPrefixAsync_With_Not_Existed_Prefix_Should_Return_Empty_Dict() { return Task.CompletedTask; } - protected override void RemoveByPrefix_Should_Succeed() + protected override Task GetExpiration_Async_Should_Succeed() { + return Task.CompletedTask; } - public override void RemoveByPrefix_Should_Throw_ArgumentNullException_When_CacheKey_IsNullOrWhiteSpace( - string prefix) + protected override void GetExpiration_Should_Succeed() + { + } + + protected override void RemoveByPrefix_Should_Succeed() { } diff --git a/test/EasyCaching.UnitTests/CachingTests/MemoryCachingProviderTest.cs b/test/EasyCaching.UnitTests/CachingTests/MemoryCachingProviderTest.cs index 899b2589..0795a6ea 100644 --- a/test/EasyCaching.UnitTests/CachingTests/MemoryCachingProviderTest.cs +++ b/test/EasyCaching.UnitTests/CachingTests/MemoryCachingProviderTest.cs @@ -155,6 +155,21 @@ public void Evicted_Event_Should_Trigger_When_GetExpiredItems() _provider.Get(key1); System.Threading.Thread.Sleep(500); } + + [Fact] + public async void Issues497_GetCountAsync_Check_Expires_Test() + { + for (int i = 0; i < 9; i++) + { + await _provider.SetAsync(Guid.NewGuid().ToString(), $"value-{i}", TimeSpan.FromSeconds(5)); + } + + Assert.Equal(9, await _provider.GetCountAsync()); + + await Task.Delay(5000); + + Assert.Equal(0, await _provider.GetCountAsync()); + } } public class MemoryCachingProviderWithFactoryTest : BaseCachingProviderWithFactoryTest diff --git a/test/EasyCaching.UnitTests/SerializerTests/MemoryPackSerializerTest.cs b/test/EasyCaching.UnitTests/SerializerTests/MemoryPackSerializerTest.cs index 32cbbfb9..6f65205e 100644 --- a/test/EasyCaching.UnitTests/SerializerTests/MemoryPackSerializerTest.cs +++ b/test/EasyCaching.UnitTests/SerializerTests/MemoryPackSerializerTest.cs @@ -14,6 +14,7 @@ public MemoryPackSerializerTest() } //This should be overrided becuse it is not supported by memory-pack + [Fact] public override void DeserializeObject_should_Succeed() { Person input = new("test", "test1");