From 22d10eb3d5025a270ba2531c08b4e16b6df1e89f Mon Sep 17 00:00:00 2001 From: dudu Date: Fri, 23 Dec 2022 22:06:50 +0800 Subject: [PATCH] Reimplement with latest ServiceStack.Redis --- .github/workflows/ci.yml | 1 + .../CacheEntry.cs | 12 ++ ...xtensions.Caching.ServiceStackRedis.csproj | 1 + .../ServiceStackRedisCache.cs | 189 ++++++++++++------ ...ckRedisCacheServiceCollectionExtensions.cs | 11 + .../DistributedCacheFixture.cs | 12 +- .../DistributedCacheTests.cs | 108 +++++++--- 7 files changed, 246 insertions(+), 88 deletions(-) create mode 100644 src/Microsoft.Extensions.Caching.ServiceStackRedis/CacheEntry.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5842c78..e91fcef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,7 @@ name: CI on: push: + branches: [ "main" ] pull_request: branches: [ "main" ] diff --git a/src/Microsoft.Extensions.Caching.ServiceStackRedis/CacheEntry.cs b/src/Microsoft.Extensions.Caching.ServiceStackRedis/CacheEntry.cs new file mode 100644 index 0000000..4357a68 --- /dev/null +++ b/src/Microsoft.Extensions.Caching.ServiceStackRedis/CacheEntry.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Extensions.Caching.ServiceStackRedis +{ + internal struct CacheEntry + { + public string Value { get; set; } + public TimeSpan SlidingExpiration { get; set; } + } +} diff --git a/src/Microsoft.Extensions.Caching.ServiceStackRedis/Microsoft.Extensions.Caching.ServiceStackRedis.csproj b/src/Microsoft.Extensions.Caching.ServiceStackRedis/Microsoft.Extensions.Caching.ServiceStackRedis.csproj index 11554e3..d9ce830 100644 --- a/src/Microsoft.Extensions.Caching.ServiceStackRedis/Microsoft.Extensions.Caching.ServiceStackRedis.csproj +++ b/src/Microsoft.Extensions.Caching.ServiceStackRedis/Microsoft.Extensions.Caching.ServiceStackRedis.csproj @@ -9,6 +9,7 @@ https://github.com/cnblogs/ServiceStackRedisCache git https://github.com/cnblogs/ServiceStackRedisCache + Latest diff --git a/src/Microsoft.Extensions.Caching.ServiceStackRedis/ServiceStackRedisCache.cs b/src/Microsoft.Extensions.Caching.ServiceStackRedis/ServiceStackRedisCache.cs index e82d618..b44741c 100644 --- a/src/Microsoft.Extensions.Caching.ServiceStackRedis/ServiceStackRedisCache.cs +++ b/src/Microsoft.Extensions.Caching.ServiceStackRedis/ServiceStackRedisCache.cs @@ -1,55 +1,92 @@ -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Options; -using ServiceStack.Redis; using System; using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Distributed; +using ServiceStack.Redis; namespace Microsoft.Extensions.Caching.ServiceStackRedis { public class ServiceStackRedisCache : IDistributedCache { - private readonly IRedisClientsManager _redisManager; + private readonly IRedisClientsManager _redisClientsManager; private readonly ServiceStackRedisCacheOptions _options; - public ServiceStackRedisCache(IOptions optionsAccessor) + public ServiceStackRedisCache(IRedisClientsManager redisClientsManager) + { + RedisConfig.VerifyMasterConnections = false; + _redisClientsManager = redisClientsManager; + } + + public byte[] Get(string key) { - if (optionsAccessor == null) + if (key == null) { - throw new ArgumentNullException(nameof(optionsAccessor)); + throw new ArgumentNullException(nameof(key)); } - _options = optionsAccessor.Value; + using var client = _redisClientsManager.GetClient(); + if (!client.ContainsKey(key)) + { + return null; + } - var host = $"{_options.Password}@{_options.Host}:{_options.Port}"; - RedisConfig.VerifyMasterConnections = false; - _redisManager = new RedisManagerPool(host); + var values = client.GetValuesFromHash(key, nameof(CacheEntry.Value), nameof(CacheEntry.SlidingExpiration)); + + if (TimeSpan.TryParse(values[1], out var sldExp)) + { + Refresh(key, sldExp); + } + + return Encoding.UTF8.GetBytes(values[0]); } - public byte[] Get(string key) + public async Task GetAsync(string key, CancellationToken token = default) { if (key == null) { throw new ArgumentNullException(nameof(key)); } - using (var client = _redisManager.GetClient() as IRedisNativeClient) + await using var client = await _redisClientsManager.GetClientAsync(); + if (!await client.ContainsKeyAsync(key)) { - if (client.Exists(key) == 1) - { - return client.Get(key); - } + return null; + } + + var values = await client.GetValuesFromHashAsync(key, nameof(CacheEntry.Value), nameof(CacheEntry.SlidingExpiration)); + + if (TimeSpan.TryParse(values[1], out var slbExp)) + { + await RefreshAsync(key, slbExp); } - return null; + + return Encoding.UTF8.GetBytes(values[0]); } - public Task GetAsync(string key, CancellationToken token = default(CancellationToken)) + public void Set(string key, byte[] value, DistributedCacheEntryOptions options) { - return Task.FromResult(Get(key)); + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + using var client = _redisClientsManager.GetClient(); + client.SetEntryInHash(key, nameof(CacheEntry.Value), Encoding.UTF8.GetString(value)); + SetExpiration(client, key, options); } - public void Set(string key, byte[] value, DistributedCacheEntryOptions options) + public async Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default(CancellationToken)) { if (key == null) { @@ -66,97 +103,131 @@ public void Set(string key, byte[] value, DistributedCacheEntryOptions options) throw new ArgumentNullException(nameof(options)); } - using (var client = _redisManager.GetClient() as IRedisNativeClient) + await using var client = await _redisClientsManager.GetClientAsync(); + await client.SetEntryInHashAsync(key, nameof(CacheEntry.Value), Encoding.UTF8.GetString(value)); + await SetExpirationAsync(client, key, options); + } + + public void Refresh(string key) + { + Refresh(key, null); + } + + public void Refresh(string key, TimeSpan? sldExp) + { + if (key == null) { - var expireInSeconds = GetExpireInSeconds(options); - if (expireInSeconds > 0) + throw new ArgumentNullException(nameof(key)); + } + + using var client = _redisClientsManager.GetClient(); + var ttl = client.GetTimeToLive(key); + if (ttl.HasValue) + { + if (!sldExp.HasValue) { - client.SetEx(key, expireInSeconds, value); - client.SetEx(GetExpirationKey(key), expireInSeconds, Encoding.UTF8.GetBytes(expireInSeconds.ToString())); + var sldExpStr = client.GetValueFromHash(key, nameof(CacheEntry.SlidingExpiration)); + if (TimeSpan.TryParse(sldExpStr, out var cachedSldExp)) + { + sldExp = cachedSldExp; + } } - else + + if (sldExp.HasValue && ttl < sldExp) { - client.Set(key, value); + client.ExpireEntryIn(key, sldExp.Value); } } } - public Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default(CancellationToken)) + public async Task RefreshAsync(string key, CancellationToken token) { - return Task.Run(() => Set(key, value, options)); + await RefreshAsync(key, null); } - public void Refresh(string key) + public async Task RefreshAsync(string key, TimeSpan? sldExp) { if (key == null) { throw new ArgumentNullException(nameof(key)); } - using (var client = _redisManager.GetClient() as IRedisNativeClient) + await using var client = await _redisClientsManager.GetClientAsync(); + var ttl = await client.GetTimeToLiveAsync(key); + if (ttl.HasValue) { - if (client.Exists(key) == 1) + if (!sldExp.HasValue) { - var value = client.Get(key); - if (value != null) + var sldExpStr = await client.GetValueFromHashAsync(key, nameof(CacheEntry.SlidingExpiration)); + if (TimeSpan.TryParse(sldExpStr, out var cachedSldExp)) { - var expirationValue = client.Get(GetExpirationKey(key)); - if (expirationValue != null) - { - client.Expire(key, int.Parse(Encoding.UTF8.GetString(expirationValue))); - } + sldExp = cachedSldExp; } } + + if (sldExp.HasValue && ttl < sldExp) + { + await client.ExpireEntryInAsync(key, sldExp.Value); + } } } - public Task RefreshAsync(string key, CancellationToken token = default(CancellationToken)) + public void Remove(string key) { if (key == null) { throw new ArgumentNullException(nameof(key)); } - return Task.Run(() => Refresh(key)); + using var client = _redisClientsManager.GetClient(); + client.Remove(key); } - public void Remove(string key) + public async Task RemoveAsync(string key, CancellationToken token = default) { if (key == null) { throw new ArgumentNullException(nameof(key)); } - using (var client = _redisManager.GetClient() as IRedisNativeClient) - { - client.Del(key); - } + await using var client = await _redisClientsManager.GetClientAsync(); + await client.RemoveAsync(key); } - public Task RemoveAsync(string key, CancellationToken token = default(CancellationToken)) - { - return Task.Run(() => Remove(key)); - } - - private int GetExpireInSeconds(DistributedCacheEntryOptions options) + private void SetExpiration(IRedisClient client, string key, DistributedCacheEntryOptions options) { if (options.SlidingExpiration.HasValue) { - return (int)options.SlidingExpiration.Value.TotalSeconds; + var sldExp = options.SlidingExpiration.Value; + client.SetEntryInHash(key, nameof(CacheEntry.SlidingExpiration), sldExp.ToString()); + client.ExpireEntryIn(key, sldExp); } else if (options.AbsoluteExpirationRelativeToNow.HasValue) { - return (int)options.AbsoluteExpirationRelativeToNow.Value.TotalSeconds; + client.ExpireEntryAt(key, DateTime.Now + options.AbsoluteExpirationRelativeToNow.Value); } - else + else if (options.AbsoluteExpiration.HasValue) { - return 0; + client.ExpireEntryAt(key, options.AbsoluteExpiration.Value.DateTime); } } - private string GetExpirationKey(string key) + private async Task SetExpirationAsync(IRedisClientAsync client, string key, DistributedCacheEntryOptions options) { - return key + $"-{nameof(DistributedCacheEntryOptions)}"; + if (options.SlidingExpiration.HasValue) + { + var sldExp = options.SlidingExpiration.Value; + await client.SetEntryInHashAsync(key, nameof(CacheEntry.SlidingExpiration), sldExp.ToString()); + await client.ExpireEntryInAsync(key, sldExp); + } + else if (options.AbsoluteExpirationRelativeToNow.HasValue) + { + await client.ExpireEntryAtAsync(key, DateTime.Now + options.AbsoluteExpirationRelativeToNow.Value); + } + else if (options.AbsoluteExpiration.HasValue) + { + await client.ExpireEntryAtAsync(key, options.AbsoluteExpiration.Value.DateTime); + } } } } \ No newline at end of file diff --git a/src/Microsoft.Extensions.Caching.ServiceStackRedis/ServiceStackRedisCacheServiceCollectionExtensions.cs b/src/Microsoft.Extensions.Caching.ServiceStackRedis/ServiceStackRedisCacheServiceCollectionExtensions.cs index c3346c4..ce87b32 100644 --- a/src/Microsoft.Extensions.Caching.ServiceStackRedis/ServiceStackRedisCacheServiceCollectionExtensions.cs +++ b/src/Microsoft.Extensions.Caching.ServiceStackRedis/ServiceStackRedisCacheServiceCollectionExtensions.cs @@ -1,8 +1,11 @@ using System; +using System.ComponentModel; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.ServiceStackRedis; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using ServiceStack.Redis; namespace Microsoft.Extensions.DependencyInjection { @@ -49,6 +52,14 @@ public static IServiceCollection AddDistributedServiceStackRedisCache(this IServ } services.Configure(section); + + services.TryAddSingleton(provider => + { + var options = provider.GetRequiredService>().Value; + var host = $"{options.Password}@{options.Host}:{options.Port}"; + return new RedisManagerPool(host); + }); + services.TryAddSingleton(); return services; diff --git a/test/ServiceStackRedisCacheTests/DistributedCacheFixture.cs b/test/ServiceStackRedisCacheTests/DistributedCacheFixture.cs index 5a9b815..8f680ee 100644 --- a/test/ServiceStackRedisCacheTests/DistributedCacheFixture.cs +++ b/test/ServiceStackRedisCacheTests/DistributedCacheFixture.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using ServiceStack.Redis; using Xunit; using Xunit.Abstractions; @@ -16,15 +17,16 @@ namespace ServiceStackRedisCacheTests; public class DistributedCacheFixture { public IDistributedCache DistributedCache { get; private set; } - public string KeyPostfix { get; private set; } + public IRedisClientsManager RedisClientManager { get; private set; } public DistributedCacheFixture() { - DistributedCache = GetInstance(); - KeyPostfix = "-" + Guid.NewGuid(); + using IServiceScope scope = GetServiceProvider().CreateScope(); + DistributedCache = scope.ServiceProvider.GetRequiredService(); + RedisClientManager = scope.ServiceProvider.GetRequiredService(); } - private IDistributedCache GetInstance() + private IServiceProvider GetServiceProvider() { IServiceCollection services = new ServiceCollection(); IConfiguration conf = new ConfigurationBuilder(). @@ -32,6 +34,6 @@ private IDistributedCache GetInstance() .Build(); services.AddSingleton(conf); services.AddDistributedServiceStackRedisCache("redis"); - return services.BuildServiceProvider().GetRequiredService(); + return services.BuildServiceProvider(); } } diff --git a/test/ServiceStackRedisCacheTests/DistributedCacheTests.cs b/test/ServiceStackRedisCacheTests/DistributedCacheTests.cs index b9a096e..3ccc018 100644 --- a/test/ServiceStackRedisCacheTests/DistributedCacheTests.cs +++ b/test/ServiceStackRedisCacheTests/DistributedCacheTests.cs @@ -1,4 +1,6 @@ +using System.Formats.Tar; using Microsoft.Extensions.Caching.Distributed; +using ServiceStack.Redis; using Xunit.Priority; using PriorityAttribute = Xunit.Priority.PriorityAttribute; @@ -8,45 +10,103 @@ namespace ServiceStackRedisCacheTests; [Collection(nameof(DistributedCacheCollection))] public class DistributedCacheTests { - private readonly string _key = "distributed_cache"; - private readonly string _keyAsync = "distributed_cache_async"; private const string _value = "Coding changes the world"; - private readonly IDistributedCache _distributedCache; + private readonly IDistributedCache _cache; + private readonly IRedisClientsManager _redisClientManager; public DistributedCacheTests(DistributedCacheFixture fixture) { - _distributedCache = fixture.DistributedCache; - _key += fixture.KeyPostfix; - _keyAsync += fixture.KeyPostfix; + _cache = fixture.DistributedCache; + _redisClientManager = fixture.RedisClientManager; } - [Fact, Priority(1)] - public async Task Sets_a_value_with_the_given_key() + [Fact] + public async Task Cache_with_absolute_expiration() { - _distributedCache.SetString(_key, _value); - await _distributedCache.SetStringAsync(_keyAsync, _value); + var key = nameof(Cache_with_absolute_expiration) + "_" + Guid.NewGuid(); + var keyAsync = nameof(Cache_with_absolute_expiration) + "_async_" + Guid.NewGuid(); + + var options = new DistributedCacheEntryOptions + { + AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(1) + }; + + _cache.SetString(key, _value, options); + await _cache.SetStringAsync(keyAsync, _value, options); + Assert.Equal(_value, _cache.GetString(key)); + Assert.Equal(_value, await _cache.GetStringAsync(keyAsync)); + + await Task.Delay(1010); + + Assert.Null(_cache.GetString(key)); + Assert.Null(await _cache.GetStringAsync(keyAsync)); } - [Fact, Priority(2)] - public async Task Gets_a_value_with_the_given_key() + [Fact] + public async Task Cache_with_absolute_expiration_relative_to_now() { - Assert.Equal(_value, _distributedCache.GetString(_key)); - Assert.Equal(_value, await _distributedCache.GetStringAsync(_keyAsync)); + var key = nameof(Cache_with_absolute_expiration_relative_to_now) + "_" + Guid.NewGuid(); + var keyAsync = nameof(Cache_with_absolute_expiration_relative_to_now) + "_async_" + Guid.NewGuid(); + + var options = new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(1) + }; + + _cache.SetString(key, _value, options); + await _cache.SetStringAsync(keyAsync, _value, options); + Assert.Equal(_value, _cache.GetString(key)); + Assert.Equal(_value, await _cache.GetStringAsync(keyAsync)); + + await Task.Delay(1010); + + Assert.Null(_cache.GetString(key)); + Assert.Null(await _cache.GetStringAsync(keyAsync)); + } + + [Fact] + public async Task Cache_with_sliding_expiration() + { + var key = nameof(Cache_with_sliding_expiration) + "_" + Guid.NewGuid(); + var keyAsync = nameof(Cache_with_sliding_expiration) + "_async_" + Guid.NewGuid(); + + var options = new DistributedCacheEntryOptions + { + SlidingExpiration = TimeSpan.FromSeconds(2) + }; + + _cache.SetString(key, _value, options); + await _cache.SetStringAsync(keyAsync, _value, options); + + await Task.Delay(500); + Assert.Equal(_value, _cache.GetString(key)); + Assert.Equal(_value, await _cache.GetStringAsync(keyAsync)); + Assert.True(GetTtl(key) > TimeSpan.FromMilliseconds(1900)); + Assert.True(await GetTtlAsync(keyAsync) > TimeSpan.FromMilliseconds(1900)); + + await Task.Delay(1000); + Assert.True(GetTtl(key) <= TimeSpan.FromMilliseconds(1000)); + Assert.True(await GetTtlAsync(keyAsync) <= TimeSpan.FromMilliseconds(1000)); + + _cache.Refresh(key); + await _cache.RefreshAsync(keyAsync); + Assert.True(GetTtl(key) > TimeSpan.FromMilliseconds(1900)); + Assert.True(await GetTtlAsync(keyAsync) > TimeSpan.FromMilliseconds(1900)); + + await Task.Delay(2010); + Assert.Null(_cache.GetString(key)); + Assert.Null(await _cache.GetStringAsync(keyAsync)); } - [Fact, Priority(3)] - public async Task Refreshes_a_value_in_the_cache_based_on_its_key() + private TimeSpan? GetTtl(string key) { - _distributedCache.Refresh(_key); - await _distributedCache.RefreshAsync(_keyAsync); + using var client = _redisClientManager.GetClient(); + return client.GetTimeToLive(key); } - [Fact, Priority(4)] - public async Task Removes_the_value_with_the_given_key() + private async Task GetTtlAsync(string key) { - _distributedCache.Remove(_key); - await _distributedCache.RemoveAsync(_keyAsync); - Assert.Null(_distributedCache.Get(_key)); - Assert.Null(await _distributedCache.GetAsync(_keyAsync)); + await using var client = await _redisClientManager.GetClientAsync(); + return await client.GetTimeToLiveAsync(key); } } \ No newline at end of file