|
|
|
@@ -1,4 +1,7 @@
|
|
|
|
|
using System.Text.Json;
|
|
|
|
|
using Newtonsoft.Json;
|
|
|
|
|
using Newtonsoft.Json.Serialization;
|
|
|
|
|
using NodaTime;
|
|
|
|
|
using NodaTime.Serialization.JsonNet;
|
|
|
|
|
using StackExchange.Redis;
|
|
|
|
|
|
|
|
|
|
namespace DysonNetwork.Sphere.Storage;
|
|
|
|
@@ -80,7 +83,8 @@ public interface ICacheService
|
|
|
|
|
/// <param name="waitTime">How long to wait for the lock before giving up</param>
|
|
|
|
|
/// <param name="retryInterval">How often to retry acquiring the lock during the wait time</param>
|
|
|
|
|
/// <returns>A distributed lock instance if acquired, null otherwise</returns>
|
|
|
|
|
Task<IDistributedLock?> AcquireLockAsync(string resource, TimeSpan expiry, TimeSpan? waitTime = null, TimeSpan? retryInterval = null);
|
|
|
|
|
Task<IDistributedLock?> AcquireLockAsync(string resource, TimeSpan expiry, TimeSpan? waitTime = null,
|
|
|
|
|
TimeSpan? retryInterval = null);
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Executes an action with a distributed lock, ensuring the lock is properly released afterwards
|
|
|
|
@@ -91,7 +95,8 @@ public interface ICacheService
|
|
|
|
|
/// <param name="waitTime">How long to wait for the lock before giving up</param>
|
|
|
|
|
/// <param name="retryInterval">How often to retry acquiring the lock during the wait time</param>
|
|
|
|
|
/// <returns>True if the lock was acquired and the action was executed, false otherwise</returns>
|
|
|
|
|
Task<bool> ExecuteWithLockAsync(string resource, Func<Task> action, TimeSpan expiry, TimeSpan? waitTime = null, TimeSpan? retryInterval = null);
|
|
|
|
|
Task<bool> ExecuteWithLockAsync(string resource, Func<Task> action, TimeSpan expiry, TimeSpan? waitTime = null,
|
|
|
|
|
TimeSpan? retryInterval = null);
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Executes a function with a distributed lock, ensuring the lock is properly released afterwards
|
|
|
|
@@ -103,7 +108,8 @@ public interface ICacheService
|
|
|
|
|
/// <param name="waitTime">How long to wait for the lock before giving up</param>
|
|
|
|
|
/// <param name="retryInterval">How often to retry acquiring the lock during the wait time</param>
|
|
|
|
|
/// <returns>The result of the function if the lock was acquired, default(T) otherwise</returns>
|
|
|
|
|
Task<(bool Acquired, T? Result)> ExecuteWithLockAsync<T>(string resource, Func<Task<T>> func, TimeSpan expiry, TimeSpan? waitTime = null, TimeSpan? retryInterval = null);
|
|
|
|
|
Task<(bool Acquired, T? Result)> ExecuteWithLockAsync<T>(string resource, Func<Task<T>> func, TimeSpan expiry,
|
|
|
|
|
TimeSpan? waitTime = null, TimeSpan? retryInterval = null);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class RedisDistributedLock : IDistributedLock
|
|
|
|
@@ -138,8 +144,8 @@ public class RedisDistributedLock : IDistributedLock
|
|
|
|
|
|
|
|
|
|
var result = await _database.ScriptEvaluateAsync(
|
|
|
|
|
script,
|
|
|
|
|
new RedisKey[] { $"{LockKeyPrefix}{Resource}" },
|
|
|
|
|
new RedisValue[] { LockId, (long)timeSpan.TotalMilliseconds }
|
|
|
|
|
[$"{LockKeyPrefix}{Resource}"],
|
|
|
|
|
[LockId, (long)timeSpan.TotalMilliseconds]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return (long)result! == 1;
|
|
|
|
@@ -160,8 +166,8 @@ public class RedisDistributedLock : IDistributedLock
|
|
|
|
|
|
|
|
|
|
await _database.ScriptEvaluateAsync(
|
|
|
|
|
script,
|
|
|
|
|
new RedisKey[] { $"{LockKeyPrefix}{Resource}" },
|
|
|
|
|
new RedisValue[] { LockId }
|
|
|
|
|
[$"{LockKeyPrefix}{Resource}"],
|
|
|
|
|
[LockId]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
_disposed = true;
|
|
|
|
@@ -177,20 +183,25 @@ public class RedisDistributedLock : IDistributedLock
|
|
|
|
|
public class CacheServiceRedis : ICacheService
|
|
|
|
|
{
|
|
|
|
|
private readonly IDatabase _database;
|
|
|
|
|
private readonly JsonSerializerOptions _serializerOptions;
|
|
|
|
|
private readonly JsonSerializerSettings _serializerSettings;
|
|
|
|
|
|
|
|
|
|
// Global prefix for all cache keys
|
|
|
|
|
private const string GlobalKeyPrefix = "dyson:";
|
|
|
|
|
|
|
|
|
|
// Using prefixes for different types of keys
|
|
|
|
|
private const string GroupKeyPrefix = "CacheGroup_";
|
|
|
|
|
private const string LockKeyPrefix = "Lock_";
|
|
|
|
|
private const string GroupKeyPrefix = GlobalKeyPrefix + "cg:";
|
|
|
|
|
private const string LockKeyPrefix = GlobalKeyPrefix + "lock:";
|
|
|
|
|
|
|
|
|
|
public CacheServiceRedis(IConnectionMultiplexer redis)
|
|
|
|
|
{
|
|
|
|
|
var rds = redis ?? throw new ArgumentNullException(nameof(redis));
|
|
|
|
|
_database = rds.GetDatabase();
|
|
|
|
|
_serializerOptions = new JsonSerializerOptions
|
|
|
|
|
_serializerSettings = new JsonSerializerSettings
|
|
|
|
|
{
|
|
|
|
|
PropertyNameCaseInsensitive = true
|
|
|
|
|
};
|
|
|
|
|
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
|
|
|
|
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
|
|
|
|
|
NullValueHandling = NullValueHandling.Include,
|
|
|
|
|
}.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<bool> SetAsync<T>(string key, T value, TimeSpan? expiry = null)
|
|
|
|
@@ -198,7 +209,7 @@ public class CacheServiceRedis : ICacheService
|
|
|
|
|
if (string.IsNullOrEmpty(key))
|
|
|
|
|
throw new ArgumentException("Key cannot be null or empty", nameof(key));
|
|
|
|
|
|
|
|
|
|
var serializedValue = JsonSerializer.Serialize(value, _serializerOptions);
|
|
|
|
|
var serializedValue = JsonConvert.SerializeObject(value, _serializerSettings);
|
|
|
|
|
return await _database.StringSetAsync(key, serializedValue, expiry);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -212,7 +223,7 @@ public class CacheServiceRedis : ICacheService
|
|
|
|
|
if (value.IsNullOrEmpty)
|
|
|
|
|
return default;
|
|
|
|
|
|
|
|
|
|
return JsonSerializer.Deserialize<T>(value!, _serializerOptions);
|
|
|
|
|
return JsonConvert.DeserializeObject<T>(value!, _serializerSettings);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<bool> RemoveAsync(string key)
|
|
|
|
@@ -231,7 +242,7 @@ public class CacheServiceRedis : ICacheService
|
|
|
|
|
|
|
|
|
|
var result = await _database.ScriptEvaluateAsync(
|
|
|
|
|
script,
|
|
|
|
|
values: new RedisValue[] { $"{GroupKeyPrefix}*", key }
|
|
|
|
|
values: [$"{GroupKeyPrefix}*", key]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return (long)result! > 0;
|
|
|
|
@@ -281,26 +292,24 @@ public class CacheServiceRedis : ICacheService
|
|
|
|
|
return members.Select(m => m.ToString());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<bool> SetWithGroupsAsync<T>(string key, T value, IEnumerable<string>? groups = null, TimeSpan? expiry = null)
|
|
|
|
|
public async Task<bool> SetWithGroupsAsync<T>(string key, T value, IEnumerable<string>? groups = null,
|
|
|
|
|
TimeSpan? expiry = null)
|
|
|
|
|
{
|
|
|
|
|
// First set the value in the cache
|
|
|
|
|
// First, set the value in the cache
|
|
|
|
|
var setResult = await SetAsync(key, value, expiry);
|
|
|
|
|
|
|
|
|
|
// If successful and there are groups to associate, add the key to each group
|
|
|
|
|
if (setResult && groups != null)
|
|
|
|
|
{
|
|
|
|
|
if (!setResult || groups == null) return setResult;
|
|
|
|
|
var groupsArray = groups.Where(g => !string.IsNullOrEmpty(g)).ToArray();
|
|
|
|
|
if (groupsArray.Length > 0)
|
|
|
|
|
{
|
|
|
|
|
if (groupsArray.Length <= 0) return setResult;
|
|
|
|
|
var tasks = groupsArray.Select(group => AddToGroupAsync(key, group));
|
|
|
|
|
await Task.WhenAll(tasks);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return setResult;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<IDistributedLock?> AcquireLockAsync(string resource, TimeSpan expiry, TimeSpan? waitTime = null, TimeSpan? retryInterval = null)
|
|
|
|
|
public async Task<IDistributedLock?> AcquireLockAsync(string resource, TimeSpan expiry, TimeSpan? waitTime = null,
|
|
|
|
|
TimeSpan? retryInterval = null)
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrEmpty(resource))
|
|
|
|
|
throw new ArgumentException("Resource cannot be null or empty", nameof(resource));
|
|
|
|
@@ -332,7 +341,8 @@ public class CacheServiceRedis : ICacheService
|
|
|
|
|
return new RedisDistributedLock(_database, resource, lockId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<bool> ExecuteWithLockAsync(string resource, Func<Task> action, TimeSpan expiry, TimeSpan? waitTime = null, TimeSpan? retryInterval = null)
|
|
|
|
|
public async Task<bool> ExecuteWithLockAsync(string resource, Func<Task> action, TimeSpan expiry,
|
|
|
|
|
TimeSpan? waitTime = null, TimeSpan? retryInterval = null)
|
|
|
|
|
{
|
|
|
|
|
await using var lockObj = await AcquireLockAsync(resource, expiry, waitTime, retryInterval);
|
|
|
|
|
|
|
|
|
@@ -343,7 +353,8 @@ public class CacheServiceRedis : ICacheService
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<(bool Acquired, T? Result)> ExecuteWithLockAsync<T>(string resource, Func<Task<T>> func, TimeSpan expiry, TimeSpan? waitTime = null, TimeSpan? retryInterval = null)
|
|
|
|
|
public async Task<(bool Acquired, T? Result)> ExecuteWithLockAsync<T>(string resource, Func<Task<T>> func,
|
|
|
|
|
TimeSpan expiry, TimeSpan? waitTime = null, TimeSpan? retryInterval = null)
|
|
|
|
|
{
|
|
|
|
|
await using var lockObj = await AcquireLockAsync(resource, expiry, waitTime, retryInterval);
|
|
|
|
|
|
|
|
|
|