🐛 Replace the serializer in cache service with newtonsoft json to solve JsonIgnore issue
This commit is contained in:
parent
460ce62452
commit
445e5d3705
@ -2,7 +2,6 @@ using System.Net.WebSockets;
|
|||||||
using DysonNetwork.Sphere.Chat;
|
using DysonNetwork.Sphere.Chat;
|
||||||
using DysonNetwork.Sphere.Storage;
|
using DysonNetwork.Sphere.Storage;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Connection.Handlers;
|
namespace DysonNetwork.Sphere.Connection.Handlers;
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ public class WebSocketPacket
|
|||||||
{
|
{
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||||
DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower,
|
DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||||
}.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);;
|
}.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
|
||||||
var json = JsonSerializer.Serialize(this, jsonOpts);
|
var json = JsonSerializer.Serialize(this, jsonOpts);
|
||||||
return System.Text.Encoding.UTF8.GetBytes(json);
|
return System.Text.Encoding.UTF8.GetBytes(json);
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,9 @@
|
|||||||
<PackageReference Include="NetVips" Version="3.0.1"/>
|
<PackageReference Include="NetVips" Version="3.0.1"/>
|
||||||
<PackageReference Include="NetVips.Native.linux-x64" Version="8.16.1"/>
|
<PackageReference Include="NetVips.Native.linux-x64" Version="8.16.1"/>
|
||||||
<PackageReference Include="NetVips.Native.osx-arm64" Version="8.16.1"/>
|
<PackageReference Include="NetVips.Native.osx-arm64" Version="8.16.1"/>
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="NodaTime" Version="3.2.2"/>
|
<PackageReference Include="NodaTime" Version="3.2.2"/>
|
||||||
|
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0" />
|
||||||
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0"/>
|
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0"/>
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4"/>
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4"/>
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0"/>
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0"/>
|
||||||
|
@ -11,17 +11,17 @@ public class PermissionService(
|
|||||||
ILogger<PermissionService> logger)
|
ILogger<PermissionService> logger)
|
||||||
{
|
{
|
||||||
private static readonly TimeSpan CacheExpiration = TimeSpan.FromMinutes(1);
|
private static readonly TimeSpan CacheExpiration = TimeSpan.FromMinutes(1);
|
||||||
|
|
||||||
private const string PermCacheKeyPrefix = "Perm_";
|
private const string PermCacheKeyPrefix = "Perm_";
|
||||||
private const string PermGroupCacheKeyPrefix = "PermCacheGroup_";
|
private const string PermGroupCacheKeyPrefix = "PermCacheGroup_";
|
||||||
private const string PermissionGroupPrefix = "PermGroup_";
|
private const string PermissionGroupPrefix = "PermGroup_";
|
||||||
|
|
||||||
private static string _GetPermissionCacheKey(string actor, string area, string key) =>
|
private static string _GetPermissionCacheKey(string actor, string area, string key) =>
|
||||||
PermCacheKeyPrefix + actor + ":" + area + ":" + key;
|
PermCacheKeyPrefix + actor + ":" + area + ":" + key;
|
||||||
|
|
||||||
private static string _GetGroupsCacheKey(string actor) =>
|
private static string _GetGroupsCacheKey(string actor) =>
|
||||||
PermGroupCacheKeyPrefix + actor;
|
PermGroupCacheKeyPrefix + actor;
|
||||||
|
|
||||||
private static string _GetPermissionGroupKey(string actor) =>
|
private static string _GetPermissionGroupKey(string actor) =>
|
||||||
PermissionGroupPrefix + actor;
|
PermissionGroupPrefix + actor;
|
||||||
|
|
||||||
@ -68,8 +68,8 @@ public class PermissionService(
|
|||||||
|
|
||||||
var result = permission is not null ? _DeserializePermissionValue<T>(permission.Value) : default;
|
var result = permission is not null ? _DeserializePermissionValue<T>(permission.Value) : default;
|
||||||
|
|
||||||
await cache.SetWithGroupsAsync(cacheKey, result,
|
await cache.SetWithGroupsAsync(cacheKey, result,
|
||||||
new[] { _GetPermissionGroupKey(actor) },
|
[_GetPermissionGroupKey(actor)],
|
||||||
CacheExpiration);
|
CacheExpiration);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
@ -45,13 +45,12 @@ builder.WebHost.ConfigureKestrel(options => options.Limits.MaxRequestBodySize =
|
|||||||
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
|
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
|
||||||
|
|
||||||
builder.Services.AddDbContext<AppDatabase>();
|
builder.Services.AddDbContext<AppDatabase>();
|
||||||
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
|
builder.Services.AddSingleton<IConnectionMultiplexer>(_ =>
|
||||||
{
|
{
|
||||||
var connection = builder.Configuration.GetConnectionString("FastRetrieve")!;
|
var connection = builder.Configuration.GetConnectionString("FastRetrieve")!;
|
||||||
return ConnectionMultiplexer.Connect(connection);
|
return ConnectionMultiplexer.Connect(connection);
|
||||||
});
|
});
|
||||||
|
builder.Services.AddSingleton<ICacheService, CacheServiceRedis>();
|
||||||
builder.Services.AddScoped<ICacheService, CacheServiceRedis>();
|
|
||||||
|
|
||||||
builder.Services.AddHttpClient();
|
builder.Services.AddHttpClient();
|
||||||
builder.Services.AddControllers().AddJsonOptions(options =>
|
builder.Services.AddControllers().AddJsonOptions(options =>
|
||||||
|
@ -67,9 +67,7 @@ public class StickerService(AppDatabase db, FileService fs, ICacheService cache)
|
|||||||
|
|
||||||
// Invalidate cache for all stickers in this pack
|
// Invalidate cache for all stickers in this pack
|
||||||
foreach (var sticker in stickers)
|
foreach (var sticker in stickers)
|
||||||
{
|
await PurgeStickerCache(sticker);
|
||||||
PurgeStickerCache(sticker);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Sticker?> LookupStickerByIdentifierAsync(string identifier)
|
public async Task<Sticker?> LookupStickerByIdentifierAsync(string identifier)
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
using System.Text.Json;
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Serialization;
|
||||||
|
using NodaTime;
|
||||||
|
using NodaTime.Serialization.JsonNet;
|
||||||
using StackExchange.Redis;
|
using StackExchange.Redis;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Storage;
|
namespace DysonNetwork.Sphere.Storage;
|
||||||
@ -12,17 +15,17 @@ public interface IDistributedLock : IAsyncDisposable
|
|||||||
/// The resource identifier this lock is protecting
|
/// The resource identifier this lock is protecting
|
||||||
/// </summary>
|
/// </summary>
|
||||||
string Resource { get; }
|
string Resource { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Unique identifier for this lock instance
|
/// Unique identifier for this lock instance
|
||||||
/// </summary>
|
/// </summary>
|
||||||
string LockId { get; }
|
string LockId { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extends the lock's expiration time
|
/// Extends the lock's expiration time
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<bool> ExtendAsync(TimeSpan timeSpan);
|
Task<bool> ExtendAsync(TimeSpan timeSpan);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Releases the lock immediately
|
/// Releases the lock immediately
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -35,32 +38,32 @@ public interface ICacheService
|
|||||||
/// Sets a value in the cache with an optional expiration time
|
/// Sets a value in the cache with an optional expiration time
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<bool> SetAsync<T>(string key, T value, TimeSpan? expiry = null);
|
Task<bool> SetAsync<T>(string key, T value, TimeSpan? expiry = null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a value from the cache
|
/// Gets a value from the cache
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<T?> GetAsync<T>(string key);
|
Task<T?> GetAsync<T>(string key);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes a specific key from the cache
|
/// Removes a specific key from the cache
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<bool> RemoveAsync(string key);
|
Task<bool> RemoveAsync(string key);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds a key to a group for group-based operations
|
/// Adds a key to a group for group-based operations
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task AddToGroupAsync(string key, string group);
|
Task AddToGroupAsync(string key, string group);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes all keys associated with a specific group
|
/// Removes all keys associated with a specific group
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task RemoveGroupAsync(string group);
|
Task RemoveGroupAsync(string group);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets all keys belonging to a specific group
|
/// Gets all keys belonging to a specific group
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<IEnumerable<string>> GetGroupKeysAsync(string group);
|
Task<IEnumerable<string>> GetGroupKeysAsync(string group);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Helper method to set a value in cache and associate it with multiple groups in one operation
|
/// Helper method to set a value in cache and associate it with multiple groups in one operation
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -71,7 +74,7 @@ public interface ICacheService
|
|||||||
/// <param name="expiry">Optional expiration time for the cached item</param>
|
/// <param name="expiry">Optional expiration time for the cached item</param>
|
||||||
/// <returns>True if the set operation was successful</returns>
|
/// <returns>True if the set operation was successful</returns>
|
||||||
Task<bool> SetWithGroupsAsync<T>(string key, T value, IEnumerable<string>? groups = null, TimeSpan? expiry = null);
|
Task<bool> SetWithGroupsAsync<T>(string key, T value, IEnumerable<string>? groups = null, TimeSpan? expiry = null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Acquires a distributed lock on the specified resource
|
/// Acquires a distributed lock on the specified resource
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -80,8 +83,9 @@ public interface ICacheService
|
|||||||
/// <param name="waitTime">How long to wait for the lock before giving up</param>
|
/// <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>
|
/// <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>
|
/// <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>
|
/// <summary>
|
||||||
/// Executes an action with a distributed lock, ensuring the lock is properly released afterwards
|
/// Executes an action with a distributed lock, ensuring the lock is properly released afterwards
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -91,8 +95,9 @@ public interface ICacheService
|
|||||||
/// <param name="waitTime">How long to wait for the lock before giving up</param>
|
/// <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>
|
/// <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>
|
/// <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>
|
/// <summary>
|
||||||
/// Executes a function with a distributed lock, ensuring the lock is properly released afterwards
|
/// Executes a function with a distributed lock, ensuring the lock is properly released afterwards
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -103,31 +108,32 @@ public interface ICacheService
|
|||||||
/// <param name="waitTime">How long to wait for the lock before giving up</param>
|
/// <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>
|
/// <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>
|
/// <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
|
public class RedisDistributedLock : IDistributedLock
|
||||||
{
|
{
|
||||||
private readonly IDatabase _database;
|
private readonly IDatabase _database;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
private const string LockKeyPrefix = "Lock_";
|
private const string LockKeyPrefix = "Lock_";
|
||||||
|
|
||||||
public string Resource { get; }
|
public string Resource { get; }
|
||||||
public string LockId { get; }
|
public string LockId { get; }
|
||||||
|
|
||||||
internal RedisDistributedLock(IDatabase database, string resource, string lockId)
|
internal RedisDistributedLock(IDatabase database, string resource, string lockId)
|
||||||
{
|
{
|
||||||
_database = database;
|
_database = database;
|
||||||
Resource = resource;
|
Resource = resource;
|
||||||
LockId = lockId;
|
LockId = lockId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> ExtendAsync(TimeSpan timeSpan)
|
public async Task<bool> ExtendAsync(TimeSpan timeSpan)
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
throw new ObjectDisposedException(nameof(RedisDistributedLock));
|
throw new ObjectDisposedException(nameof(RedisDistributedLock));
|
||||||
|
|
||||||
var script = @"
|
var script = @"
|
||||||
if redis.call('get', KEYS[1]) == ARGV[1] then
|
if redis.call('get', KEYS[1]) == ARGV[1] then
|
||||||
return redis.call('pexpire', KEYS[1], ARGV[2])
|
return redis.call('pexpire', KEYS[1], ARGV[2])
|
||||||
@ -135,21 +141,21 @@ public class RedisDistributedLock : IDistributedLock
|
|||||||
return 0
|
return 0
|
||||||
end
|
end
|
||||||
";
|
";
|
||||||
|
|
||||||
var result = await _database.ScriptEvaluateAsync(
|
var result = await _database.ScriptEvaluateAsync(
|
||||||
script,
|
script,
|
||||||
new RedisKey[] { $"{LockKeyPrefix}{Resource}" },
|
[$"{LockKeyPrefix}{Resource}"],
|
||||||
new RedisValue[] { LockId, (long)timeSpan.TotalMilliseconds }
|
[LockId, (long)timeSpan.TotalMilliseconds]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (long)result! == 1;
|
return (long)result! == 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ReleaseAsync()
|
public async Task ReleaseAsync()
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var script = @"
|
var script = @"
|
||||||
if redis.call('get', KEYS[1]) == ARGV[1] then
|
if redis.call('get', KEYS[1]) == ARGV[1] then
|
||||||
return redis.call('del', KEYS[1])
|
return redis.call('del', KEYS[1])
|
||||||
@ -157,16 +163,16 @@ public class RedisDistributedLock : IDistributedLock
|
|||||||
return 0
|
return 0
|
||||||
end
|
end
|
||||||
";
|
";
|
||||||
|
|
||||||
await _database.ScriptEvaluateAsync(
|
await _database.ScriptEvaluateAsync(
|
||||||
script,
|
script,
|
||||||
new RedisKey[] { $"{LockKeyPrefix}{Resource}" },
|
[$"{LockKeyPrefix}{Resource}"],
|
||||||
new RedisValue[] { LockId }
|
[LockId]
|
||||||
);
|
);
|
||||||
|
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
await ReleaseAsync();
|
await ReleaseAsync();
|
||||||
@ -177,49 +183,54 @@ public class RedisDistributedLock : IDistributedLock
|
|||||||
public class CacheServiceRedis : ICacheService
|
public class CacheServiceRedis : ICacheService
|
||||||
{
|
{
|
||||||
private readonly IDatabase _database;
|
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
|
// Using prefixes for different types of keys
|
||||||
private const string GroupKeyPrefix = "CacheGroup_";
|
private const string GroupKeyPrefix = GlobalKeyPrefix + "cg:";
|
||||||
private const string LockKeyPrefix = "Lock_";
|
private const string LockKeyPrefix = GlobalKeyPrefix + "lock:";
|
||||||
|
|
||||||
public CacheServiceRedis(IConnectionMultiplexer redis)
|
public CacheServiceRedis(IConnectionMultiplexer redis)
|
||||||
{
|
{
|
||||||
var rds = redis ?? throw new ArgumentNullException(nameof(redis));
|
var rds = redis ?? throw new ArgumentNullException(nameof(redis));
|
||||||
_database = rds.GetDatabase();
|
_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)
|
public async Task<bool> SetAsync<T>(string key, T value, TimeSpan? expiry = null)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(key))
|
if (string.IsNullOrEmpty(key))
|
||||||
throw new ArgumentException("Key cannot be null or empty", nameof(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);
|
return await _database.StringSetAsync(key, serializedValue, expiry);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<T?> GetAsync<T>(string key)
|
public async Task<T?> GetAsync<T>(string key)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(key))
|
if (string.IsNullOrEmpty(key))
|
||||||
throw new ArgumentException("Key cannot be null or empty", nameof(key));
|
throw new ArgumentException("Key cannot be null or empty", nameof(key));
|
||||||
|
|
||||||
var value = await _database.StringGetAsync(key);
|
var value = await _database.StringGetAsync(key);
|
||||||
|
|
||||||
if (value.IsNullOrEmpty)
|
if (value.IsNullOrEmpty)
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
return JsonSerializer.Deserialize<T>(value!, _serializerOptions);
|
return JsonConvert.DeserializeObject<T>(value!, _serializerSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> RemoveAsync(string key)
|
public async Task<bool> RemoveAsync(string key)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(key))
|
if (string.IsNullOrEmpty(key))
|
||||||
throw new ArgumentException("Key cannot be null or empty", nameof(key));
|
throw new ArgumentException("Key cannot be null or empty", nameof(key));
|
||||||
|
|
||||||
// Before removing the key, find all groups it belongs to and remove it from them
|
// Before removing the key, find all groups it belongs to and remove it from them
|
||||||
var script = @"
|
var script = @"
|
||||||
local groups = redis.call('KEYS', ARGV[1])
|
local groups = redis.call('KEYS', ARGV[1])
|
||||||
@ -228,128 +239,128 @@ public class CacheServiceRedis : ICacheService
|
|||||||
end
|
end
|
||||||
return redis.call('DEL', ARGV[2])
|
return redis.call('DEL', ARGV[2])
|
||||||
";
|
";
|
||||||
|
|
||||||
var result = await _database.ScriptEvaluateAsync(
|
var result = await _database.ScriptEvaluateAsync(
|
||||||
script,
|
script,
|
||||||
values: new RedisValue[] { $"{GroupKeyPrefix}*", key }
|
values: [$"{GroupKeyPrefix}*", key]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (long)result! > 0;
|
return (long)result! > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AddToGroupAsync(string key, string group)
|
public async Task AddToGroupAsync(string key, string group)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(key))
|
if (string.IsNullOrEmpty(key))
|
||||||
throw new ArgumentException(@"Key cannot be null or empty.", nameof(key));
|
throw new ArgumentException(@"Key cannot be null or empty.", nameof(key));
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(group))
|
if (string.IsNullOrEmpty(group))
|
||||||
throw new ArgumentException(@"Group cannot be null or empty.", nameof(group));
|
throw new ArgumentException(@"Group cannot be null or empty.", nameof(group));
|
||||||
|
|
||||||
var groupKey = $"{GroupKeyPrefix}{group}";
|
var groupKey = $"{GroupKeyPrefix}{group}";
|
||||||
await _database.SetAddAsync(groupKey, key);
|
await _database.SetAddAsync(groupKey, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RemoveGroupAsync(string group)
|
public async Task RemoveGroupAsync(string group)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(group))
|
if (string.IsNullOrEmpty(group))
|
||||||
throw new ArgumentException(@"Group cannot be null or empty.", nameof(group));
|
throw new ArgumentException(@"Group cannot be null or empty.", nameof(group));
|
||||||
|
|
||||||
var groupKey = $"{GroupKeyPrefix}{group}";
|
var groupKey = $"{GroupKeyPrefix}{group}";
|
||||||
|
|
||||||
// Get all keys in the group
|
// Get all keys in the group
|
||||||
var keys = await _database.SetMembersAsync(groupKey);
|
var keys = await _database.SetMembersAsync(groupKey);
|
||||||
|
|
||||||
if (keys.Length > 0)
|
if (keys.Length > 0)
|
||||||
{
|
{
|
||||||
// Delete all the keys
|
// Delete all the keys
|
||||||
var keysTasks = keys.Select(key => _database.KeyDeleteAsync(key.ToString()));
|
var keysTasks = keys.Select(key => _database.KeyDeleteAsync(key.ToString()));
|
||||||
await Task.WhenAll(keysTasks);
|
await Task.WhenAll(keysTasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the group itself
|
// Delete the group itself
|
||||||
await _database.KeyDeleteAsync(groupKey);
|
await _database.KeyDeleteAsync(groupKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<string>> GetGroupKeysAsync(string group)
|
public async Task<IEnumerable<string>> GetGroupKeysAsync(string group)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(group))
|
if (string.IsNullOrEmpty(group))
|
||||||
throw new ArgumentException(@"Group cannot be null or empty.", nameof(group));
|
throw new ArgumentException(@"Group cannot be null or empty.", nameof(group));
|
||||||
|
|
||||||
var groupKey = $"{GroupKeyPrefix}{group}";
|
var groupKey = $"{GroupKeyPrefix}{group}";
|
||||||
var members = await _database.SetMembersAsync(groupKey);
|
var members = await _database.SetMembersAsync(groupKey);
|
||||||
|
|
||||||
return members.Select(m => m.ToString());
|
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);
|
var setResult = await SetAsync(key, value, expiry);
|
||||||
|
|
||||||
// If successful and there are groups to associate, add the key to each group
|
// 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();
|
||||||
var groupsArray = groups.Where(g => !string.IsNullOrEmpty(g)).ToArray();
|
if (groupsArray.Length <= 0) return setResult;
|
||||||
if (groupsArray.Length > 0)
|
var tasks = groupsArray.Select(group => AddToGroupAsync(key, group));
|
||||||
{
|
await Task.WhenAll(tasks);
|
||||||
var tasks = groupsArray.Select(group => AddToGroupAsync(key, group));
|
|
||||||
await Task.WhenAll(tasks);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return setResult;
|
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))
|
if (string.IsNullOrEmpty(resource))
|
||||||
throw new ArgumentException("Resource cannot be null or empty", nameof(resource));
|
throw new ArgumentException("Resource cannot be null or empty", nameof(resource));
|
||||||
|
|
||||||
var lockKey = $"{LockKeyPrefix}{resource}";
|
var lockKey = $"{LockKeyPrefix}{resource}";
|
||||||
var lockId = Guid.NewGuid().ToString("N");
|
var lockId = Guid.NewGuid().ToString("N");
|
||||||
var waitTimeSpan = waitTime ?? TimeSpan.Zero;
|
var waitTimeSpan = waitTime ?? TimeSpan.Zero;
|
||||||
var retryIntervalSpan = retryInterval ?? TimeSpan.FromMilliseconds(100);
|
var retryIntervalSpan = retryInterval ?? TimeSpan.FromMilliseconds(100);
|
||||||
|
|
||||||
var startTime = DateTime.UtcNow;
|
var startTime = DateTime.UtcNow;
|
||||||
var acquired = false;
|
var acquired = false;
|
||||||
|
|
||||||
// Try to acquire the lock, retry until waitTime is exceeded
|
// Try to acquire the lock, retry until waitTime is exceeded
|
||||||
while (!acquired && (DateTime.UtcNow - startTime) < waitTimeSpan)
|
while (!acquired && (DateTime.UtcNow - startTime) < waitTimeSpan)
|
||||||
{
|
{
|
||||||
acquired = await _database.StringSetAsync(lockKey, lockId, expiry, When.NotExists);
|
acquired = await _database.StringSetAsync(lockKey, lockId, expiry, When.NotExists);
|
||||||
|
|
||||||
if (!acquired)
|
if (!acquired)
|
||||||
{
|
{
|
||||||
await Task.Delay(retryIntervalSpan);
|
await Task.Delay(retryIntervalSpan);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!acquired)
|
if (!acquired)
|
||||||
{
|
{
|
||||||
return null; // Could not acquire the lock within the wait time
|
return null; // Could not acquire the lock within the wait time
|
||||||
}
|
}
|
||||||
|
|
||||||
return new RedisDistributedLock(_database, resource, lockId);
|
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);
|
await using var lockObj = await AcquireLockAsync(resource, expiry, waitTime, retryInterval);
|
||||||
|
|
||||||
if (lockObj == null)
|
if (lockObj == null)
|
||||||
return false; // Could not acquire the lock
|
return false; // Could not acquire the lock
|
||||||
|
|
||||||
await action();
|
await action();
|
||||||
return true;
|
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);
|
await using var lockObj = await AcquireLockAsync(resource, expiry, waitTime, retryInterval);
|
||||||
|
|
||||||
if (lockObj == null)
|
if (lockObj == null)
|
||||||
return (false, default); // Could not acquire the lock
|
return (false, default); // Could not acquire the lock
|
||||||
|
|
||||||
var result = await func();
|
var result = await func();
|
||||||
return (true, result);
|
return (true, result);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user