mirror of https://github.com/abpframework/abp.git
3 changed files with 106 additions and 576 deletions
@ -1,496 +0,0 @@ |
|||||
// This software is part of the DOTNET extensions
|
|
||||
// Copyright (c) .NET Foundation and Contributors
|
|
||||
// https://dotnet.microsoft.com/
|
|
||||
//
|
|
||||
// All rights reserved.
|
|
||||
//
|
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||
// in the Software without restriction, including without limitation the rights
|
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||
// furnished to do so, subject to the following conditions:
|
|
||||
//
|
|
||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||
// copies or substantial portions of the Software.
|
|
||||
//
|
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||
// SOFTWARE.
|
|
||||
|
|
||||
using System; |
|
||||
using System.Threading; |
|
||||
using System.Threading.Tasks; |
|
||||
using Microsoft.Extensions.Caching.Distributed; |
|
||||
using Microsoft.Extensions.Options; |
|
||||
using StackExchange.Redis; |
|
||||
|
|
||||
namespace Microsoft.Extensions.Caching.StackExchangeRedis |
|
||||
{ |
|
||||
public class RedisCache : IDistributedCache, IDisposable |
|
||||
{ |
|
||||
// KEYS[1] = = key
|
|
||||
// ARGV[1] = absolute-expiration - ticks as long (-1 for none)
|
|
||||
// ARGV[2] = sliding-expiration - ticks as long (-1 for none)
|
|
||||
// ARGV[3] = relative-expiration (long, in seconds, -1 for none) - Min(absolute-expiration - Now, sliding-expiration)
|
|
||||
// ARGV[4] = data - byte[]
|
|
||||
// this order should not change LUA script depends on it
|
|
||||
protected const string SetScript = (@"
|
|
||||
redis.call('HMSET', KEYS[1], 'absexp', ARGV[1], 'sldexp', ARGV[2], 'data', ARGV[4]) |
|
||||
if ARGV[3] ~= '-1' then |
|
||||
redis.call('EXPIRE', KEYS[1], ARGV[3]) |
|
||||
end |
|
||||
return 1");
|
|
||||
|
|
||||
protected const string AbsoluteExpirationKey = "absexp"; |
|
||||
protected const string SlidingExpirationKey = "sldexp"; |
|
||||
protected const string DataKey = "data"; |
|
||||
protected const long NotPresent = -1; |
|
||||
|
|
||||
protected volatile ConnectionMultiplexer Connection; |
|
||||
protected IDatabase Cache; |
|
||||
|
|
||||
protected readonly RedisCacheOptions Options; |
|
||||
protected readonly string Instance; |
|
||||
|
|
||||
protected readonly SemaphoreSlim ConnectionLock = new SemaphoreSlim(initialCount: 1, maxCount: 1); |
|
||||
|
|
||||
public RedisCache(IOptions<RedisCacheOptions> optionsAccessor) |
|
||||
{ |
|
||||
if (optionsAccessor == null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(optionsAccessor)); |
|
||||
} |
|
||||
|
|
||||
Options = optionsAccessor.Value; |
|
||||
|
|
||||
// This allows partitioning a single backend cache for use with multiple apps/services.
|
|
||||
Instance = Options.InstanceName ?? string.Empty; |
|
||||
} |
|
||||
|
|
||||
public virtual byte[] Get(string key) |
|
||||
{ |
|
||||
if (key == null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(key)); |
|
||||
} |
|
||||
|
|
||||
return GetAndRefresh(key, getData: true); |
|
||||
} |
|
||||
|
|
||||
public virtual async Task<byte[]> GetAsync( |
|
||||
string key, |
|
||||
CancellationToken token = default) |
|
||||
{ |
|
||||
if (key == null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(key)); |
|
||||
} |
|
||||
|
|
||||
token.ThrowIfCancellationRequested(); |
|
||||
|
|
||||
return await GetAndRefreshAsync(key, getData: true, token: token); |
|
||||
} |
|
||||
|
|
||||
public virtual void Set( |
|
||||
string key, |
|
||||
byte[] value, |
|
||||
DistributedCacheEntryOptions options) |
|
||||
{ |
|
||||
if (key == null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(key)); |
|
||||
} |
|
||||
|
|
||||
if (value == null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(value)); |
|
||||
} |
|
||||
|
|
||||
if (options == null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(options)); |
|
||||
} |
|
||||
|
|
||||
Connect(); |
|
||||
|
|
||||
var creationTime = DateTimeOffset.UtcNow; |
|
||||
|
|
||||
var absoluteExpiration = GetAbsoluteExpiration(creationTime, options); |
|
||||
|
|
||||
Cache.ScriptEvaluate(SetScript, new RedisKey[] {Instance + key}, |
|
||||
new RedisValue[] |
|
||||
{ |
|
||||
absoluteExpiration?.Ticks ?? NotPresent, |
|
||||
options.SlidingExpiration?.Ticks ?? NotPresent, |
|
||||
GetExpirationInSeconds(creationTime, absoluteExpiration, options) ?? NotPresent, |
|
||||
value |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
public virtual async Task SetAsync( |
|
||||
string key, |
|
||||
byte[] value, |
|
||||
DistributedCacheEntryOptions options, |
|
||||
CancellationToken token = default) |
|
||||
{ |
|
||||
if (key == null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(key)); |
|
||||
} |
|
||||
|
|
||||
if (value == null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(value)); |
|
||||
} |
|
||||
|
|
||||
if (options == null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(options)); |
|
||||
} |
|
||||
|
|
||||
token.ThrowIfCancellationRequested(); |
|
||||
|
|
||||
await ConnectAsync(token); |
|
||||
|
|
||||
var creationTime = DateTimeOffset.UtcNow; |
|
||||
|
|
||||
var absoluteExpiration = GetAbsoluteExpiration(creationTime, options); |
|
||||
|
|
||||
await Cache.ScriptEvaluateAsync(SetScript, new RedisKey[] {Instance + key}, |
|
||||
new RedisValue[] |
|
||||
{ |
|
||||
absoluteExpiration?.Ticks ?? NotPresent, |
|
||||
options.SlidingExpiration?.Ticks ?? NotPresent, |
|
||||
GetExpirationInSeconds(creationTime, absoluteExpiration, options) ?? NotPresent, |
|
||||
value |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
public virtual void Refresh(string key) |
|
||||
{ |
|
||||
if (key == null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(key)); |
|
||||
} |
|
||||
|
|
||||
GetAndRefresh(key, getData: false); |
|
||||
} |
|
||||
|
|
||||
public virtual async Task RefreshAsync( |
|
||||
string key, |
|
||||
CancellationToken token = default) |
|
||||
{ |
|
||||
if (key == null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(key)); |
|
||||
} |
|
||||
|
|
||||
token.ThrowIfCancellationRequested(); |
|
||||
|
|
||||
await GetAndRefreshAsync(key, getData: false, token: token); |
|
||||
} |
|
||||
|
|
||||
protected virtual void Connect() |
|
||||
{ |
|
||||
if (Cache != null) |
|
||||
{ |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
ConnectionLock.Wait(); |
|
||||
try |
|
||||
{ |
|
||||
if (Cache == null) |
|
||||
{ |
|
||||
if (Options.ConfigurationOptions != null) |
|
||||
{ |
|
||||
Connection = ConnectionMultiplexer.Connect(Options.ConfigurationOptions); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
Connection = ConnectionMultiplexer.Connect(Options.Configuration); |
|
||||
} |
|
||||
|
|
||||
Cache = Connection.GetDatabase(); |
|
||||
} |
|
||||
} |
|
||||
finally |
|
||||
{ |
|
||||
ConnectionLock.Release(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
protected virtual async Task ConnectAsync(CancellationToken token = default) |
|
||||
{ |
|
||||
token.ThrowIfCancellationRequested(); |
|
||||
|
|
||||
if (Cache != null) |
|
||||
{ |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
await ConnectionLock.WaitAsync(token); |
|
||||
try |
|
||||
{ |
|
||||
if (Cache == null) |
|
||||
{ |
|
||||
if (Options.ConfigurationOptions != null) |
|
||||
{ |
|
||||
Connection = await ConnectionMultiplexer.ConnectAsync(Options.ConfigurationOptions); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
Connection = await ConnectionMultiplexer.ConnectAsync(Options.Configuration); |
|
||||
} |
|
||||
|
|
||||
Cache = Connection.GetDatabase(); |
|
||||
} |
|
||||
} |
|
||||
finally |
|
||||
{ |
|
||||
ConnectionLock.Release(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
protected virtual byte[] GetAndRefresh(string key, bool getData) |
|
||||
{ |
|
||||
if (key == null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(key)); |
|
||||
} |
|
||||
|
|
||||
Connect(); |
|
||||
|
|
||||
// This also resets the LRU status as desired.
|
|
||||
// TODO: Can this be done in one operation on the server side? Probably, the trick would just be the DateTimeOffset math.
|
|
||||
RedisValue[] results; |
|
||||
if (getData) |
|
||||
{ |
|
||||
results = Cache.HashMemberGet(Instance + key, AbsoluteExpirationKey, SlidingExpirationKey, DataKey); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
results = Cache.HashMemberGet(Instance + key, AbsoluteExpirationKey, SlidingExpirationKey); |
|
||||
} |
|
||||
|
|
||||
// TODO: Error handling
|
|
||||
if (results.Length >= 2) |
|
||||
{ |
|
||||
MapMetadata(results, out DateTimeOffset? absExpr, out TimeSpan? sldExpr); |
|
||||
Refresh(key, absExpr, sldExpr); |
|
||||
} |
|
||||
|
|
||||
if (results.Length >= 3 && results[2].HasValue) |
|
||||
{ |
|
||||
return results[2]; |
|
||||
} |
|
||||
|
|
||||
return null; |
|
||||
} |
|
||||
|
|
||||
protected virtual async Task<byte[]> GetAndRefreshAsync( |
|
||||
string key, |
|
||||
bool getData, |
|
||||
CancellationToken token = default) |
|
||||
{ |
|
||||
if (key == null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(key)); |
|
||||
} |
|
||||
|
|
||||
token.ThrowIfCancellationRequested(); |
|
||||
|
|
||||
await ConnectAsync(token); |
|
||||
|
|
||||
// This also resets the LRU status as desired.
|
|
||||
// TODO: Can this be done in one operation on the server side? Probably, the trick would just be the DateTimeOffset math.
|
|
||||
RedisValue[] results; |
|
||||
if (getData) |
|
||||
{ |
|
||||
results = await Cache.HashMemberGetAsync(Instance + key, AbsoluteExpirationKey, SlidingExpirationKey, |
|
||||
DataKey); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
results = await Cache.HashMemberGetAsync(Instance + key, AbsoluteExpirationKey, SlidingExpirationKey); |
|
||||
} |
|
||||
|
|
||||
// TODO: Error handling
|
|
||||
if (results.Length >= 2) |
|
||||
{ |
|
||||
MapMetadata(results, out DateTimeOffset? absExpr, out TimeSpan? sldExpr); |
|
||||
await RefreshAsync(key, absExpr, sldExpr, token); |
|
||||
} |
|
||||
|
|
||||
if (results.Length >= 3 && results[2].HasValue) |
|
||||
{ |
|
||||
return results[2]; |
|
||||
} |
|
||||
|
|
||||
return null; |
|
||||
} |
|
||||
|
|
||||
public virtual void Remove(string key) |
|
||||
{ |
|
||||
if (key == null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(key)); |
|
||||
} |
|
||||
|
|
||||
Connect(); |
|
||||
|
|
||||
Cache.KeyDelete(Instance + key); |
|
||||
// TODO: Error handling
|
|
||||
} |
|
||||
|
|
||||
public virtual async Task RemoveAsync( |
|
||||
string key, |
|
||||
CancellationToken token = default) |
|
||||
{ |
|
||||
if (key == null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(key)); |
|
||||
} |
|
||||
|
|
||||
await ConnectAsync(token); |
|
||||
|
|
||||
await Cache.KeyDeleteAsync(Instance + key); |
|
||||
// TODO: Error handling
|
|
||||
} |
|
||||
|
|
||||
protected virtual void MapMetadata( |
|
||||
RedisValue[] results, |
|
||||
out DateTimeOffset? absoluteExpiration, |
|
||||
out TimeSpan? slidingExpiration) |
|
||||
{ |
|
||||
absoluteExpiration = null; |
|
||||
slidingExpiration = null; |
|
||||
var absoluteExpirationTicks = (long?) results[0]; |
|
||||
if (absoluteExpirationTicks.HasValue && absoluteExpirationTicks.Value != NotPresent) |
|
||||
{ |
|
||||
absoluteExpiration = new DateTimeOffset(absoluteExpirationTicks.Value, TimeSpan.Zero); |
|
||||
} |
|
||||
|
|
||||
var slidingExpirationTicks = (long?) results[1]; |
|
||||
if (slidingExpirationTicks.HasValue && slidingExpirationTicks.Value != NotPresent) |
|
||||
{ |
|
||||
slidingExpiration = new TimeSpan(slidingExpirationTicks.Value); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
protected virtual void Refresh( |
|
||||
string key, |
|
||||
DateTimeOffset? absExpr, |
|
||||
TimeSpan? sldExpr) |
|
||||
{ |
|
||||
if (key == null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(key)); |
|
||||
} |
|
||||
|
|
||||
// Note Refresh has no effect if there is just an absolute expiration (or neither).
|
|
||||
TimeSpan? expr = null; |
|
||||
if (sldExpr.HasValue) |
|
||||
{ |
|
||||
if (absExpr.HasValue) |
|
||||
{ |
|
||||
var relExpr = absExpr.Value - DateTimeOffset.Now; |
|
||||
expr = relExpr <= sldExpr.Value ? relExpr : sldExpr; |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
expr = sldExpr; |
|
||||
} |
|
||||
|
|
||||
Cache.KeyExpire(Instance + key, expr); |
|
||||
// TODO: Error handling
|
|
||||
} |
|
||||
} |
|
||||
|
|
||||
protected virtual async Task RefreshAsync( |
|
||||
string key, |
|
||||
DateTimeOffset? absExpr, |
|
||||
TimeSpan? sldExpr, |
|
||||
CancellationToken token = default) |
|
||||
{ |
|
||||
if (key == null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(key)); |
|
||||
} |
|
||||
|
|
||||
token.ThrowIfCancellationRequested(); |
|
||||
|
|
||||
// Note Refresh has no effect if there is just an absolute expiration (or neither).
|
|
||||
TimeSpan? expr = null; |
|
||||
if (sldExpr.HasValue) |
|
||||
{ |
|
||||
if (absExpr.HasValue) |
|
||||
{ |
|
||||
var relExpr = absExpr.Value - DateTimeOffset.Now; |
|
||||
expr = relExpr <= sldExpr.Value ? relExpr : sldExpr; |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
expr = sldExpr; |
|
||||
} |
|
||||
|
|
||||
await Cache.KeyExpireAsync(Instance + key, expr); |
|
||||
// TODO: Error handling
|
|
||||
} |
|
||||
} |
|
||||
|
|
||||
protected static long? GetExpirationInSeconds( |
|
||||
DateTimeOffset creationTime, |
|
||||
DateTimeOffset? absoluteExpiration, |
|
||||
DistributedCacheEntryOptions options) |
|
||||
{ |
|
||||
if (absoluteExpiration.HasValue && options.SlidingExpiration.HasValue) |
|
||||
{ |
|
||||
return (long) Math.Min( |
|
||||
(absoluteExpiration.Value - creationTime).TotalSeconds, |
|
||||
options.SlidingExpiration.Value.TotalSeconds); |
|
||||
} |
|
||||
else if (absoluteExpiration.HasValue) |
|
||||
{ |
|
||||
return (long) (absoluteExpiration.Value - creationTime).TotalSeconds; |
|
||||
} |
|
||||
else if (options.SlidingExpiration.HasValue) |
|
||||
{ |
|
||||
return (long) options.SlidingExpiration.Value.TotalSeconds; |
|
||||
} |
|
||||
|
|
||||
return null; |
|
||||
} |
|
||||
|
|
||||
protected static DateTimeOffset? GetAbsoluteExpiration( |
|
||||
DateTimeOffset creationTime, |
|
||||
DistributedCacheEntryOptions options) |
|
||||
{ |
|
||||
if (options.AbsoluteExpiration.HasValue && options.AbsoluteExpiration <= creationTime) |
|
||||
{ |
|
||||
throw new ArgumentOutOfRangeException( |
|
||||
nameof(DistributedCacheEntryOptions.AbsoluteExpiration), |
|
||||
options.AbsoluteExpiration.Value, |
|
||||
"The absolute expiration value must be in the future."); |
|
||||
} |
|
||||
|
|
||||
var absoluteExpiration = options.AbsoluteExpiration; |
|
||||
if (options.AbsoluteExpirationRelativeToNow.HasValue) |
|
||||
{ |
|
||||
absoluteExpiration = creationTime + options.AbsoluteExpirationRelativeToNow; |
|
||||
} |
|
||||
|
|
||||
return absoluteExpiration; |
|
||||
} |
|
||||
|
|
||||
public virtual void Dispose() |
|
||||
{ |
|
||||
Connection?.Close(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,70 +0,0 @@ |
|||||
// This software is part of the DOTNET extensions
|
|
||||
// Copyright (c) .NET Foundation and Contributors
|
|
||||
// https://dotnet.microsoft.com/
|
|
||||
//
|
|
||||
// All rights reserved.
|
|
||||
//
|
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||
// in the Software without restriction, including without limitation the rights
|
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||
// furnished to do so, subject to the following conditions:
|
|
||||
//
|
|
||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||
// copies or substantial portions of the Software.
|
|
||||
//
|
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||
// SOFTWARE.
|
|
||||
|
|
||||
using System.Threading.Tasks; |
|
||||
using StackExchange.Redis; |
|
||||
|
|
||||
namespace Microsoft.Extensions.Caching.StackExchangeRedis |
|
||||
{ |
|
||||
internal static class RedisExtensions |
|
||||
{ |
|
||||
private const string HmGetScript = (@"return redis.call('HMGET', KEYS[1], unpack(ARGV))"); |
|
||||
|
|
||||
internal static RedisValue[] HashMemberGet(this IDatabase cache, string key, params string[] members) |
|
||||
{ |
|
||||
var result = cache.ScriptEvaluate( |
|
||||
HmGetScript, |
|
||||
new RedisKey[] { key }, |
|
||||
GetRedisMembers(members)); |
|
||||
|
|
||||
// TODO: Error checking?
|
|
||||
return (RedisValue[])result; |
|
||||
} |
|
||||
|
|
||||
internal static async Task<RedisValue[]> HashMemberGetAsync( |
|
||||
this IDatabase cache, |
|
||||
string key, |
|
||||
params string[] members) |
|
||||
{ |
|
||||
var result = await cache.ScriptEvaluateAsync( |
|
||||
HmGetScript, |
|
||||
new RedisKey[] { key }, |
|
||||
GetRedisMembers(members)).ConfigureAwait(false); |
|
||||
|
|
||||
// TODO: Error checking?
|
|
||||
return (RedisValue[])result; |
|
||||
} |
|
||||
|
|
||||
private static RedisValue[] GetRedisMembers(params string[] members) |
|
||||
{ |
|
||||
var redisMembers = new RedisValue[members.Length]; |
|
||||
for (int i = 0; i < members.Length; i++) |
|
||||
{ |
|
||||
redisMembers[i] = (RedisValue)members[i]; |
|
||||
} |
|
||||
|
|
||||
return redisMembers; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
Loading…
Reference in new issue