From aca28f9318c1dab73566f6402a0a2b9703e0bab5 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Tue, 2 Dec 2025 22:38:47 +0800 Subject: [PATCH] :recycle: Refactored the cache service --- .../Startup/ServiceCollectionExtensions.cs | 2 - .../Startup/ServiceCollectionExtensions.cs | 2 - .../Startup/ServiceCollectionExtensions.cs | 2 - .../Startup/ServiceCollectionExtensions.cs | 2 - .../Startup/ServiceCollectionExtensions.cs | 2 - DysonNetwork.Shared/Cache/CacheService.cs | 443 +++++------------- DysonNetwork.Shared/Cache/ICacheSerializer.cs | 7 + DysonNetwork.Shared/Cache/ICacheService.cs | 86 ++++ DysonNetwork.Shared/Cache/IDistributedLock.cs | 22 + .../Cache/JsonCacheSerializer.cs | 20 + .../Cache/MessagePackCacheSerializer.cs | 21 + .../DysonNetwork.Shared.csproj | 4 + DysonNetwork.Shared/Extensions.cs | 24 + .../Startup/ServiceCollectionExtensions.cs | 2 - .../Startup/ServiceCollectionExtensions.cs | 2 - 15 files changed, 308 insertions(+), 333 deletions(-) create mode 100644 DysonNetwork.Shared/Cache/ICacheSerializer.cs create mode 100644 DysonNetwork.Shared/Cache/ICacheService.cs create mode 100644 DysonNetwork.Shared/Cache/IDistributedLock.cs create mode 100644 DysonNetwork.Shared/Cache/JsonCacheSerializer.cs create mode 100644 DysonNetwork.Shared/Cache/MessagePackCacheSerializer.cs diff --git a/DysonNetwork.Develop/Startup/ServiceCollectionExtensions.cs b/DysonNetwork.Develop/Startup/ServiceCollectionExtensions.cs index 1e9dd8f..a1669b6 100644 --- a/DysonNetwork.Develop/Startup/ServiceCollectionExtensions.cs +++ b/DysonNetwork.Develop/Startup/ServiceCollectionExtensions.cs @@ -16,9 +16,7 @@ public static class ServiceCollectionExtensions services.AddLocalization(); services.AddDbContext(); - services.AddSingleton(SystemClock.Instance); services.AddHttpContextAccessor(); - services.AddSingleton(); services.AddHttpClient(); diff --git a/DysonNetwork.Drive/Startup/ServiceCollectionExtensions.cs b/DysonNetwork.Drive/Startup/ServiceCollectionExtensions.cs index 3fbd280..25b531d 100644 --- a/DysonNetwork.Drive/Startup/ServiceCollectionExtensions.cs +++ b/DysonNetwork.Drive/Startup/ServiceCollectionExtensions.cs @@ -12,9 +12,7 @@ public static class ServiceCollectionExtensions public static IServiceCollection AddAppServices(this IServiceCollection services, IConfiguration configuration) { services.AddDbContext(); // Assuming you'll have an AppDatabase - services.AddSingleton(SystemClock.Instance); services.AddHttpContextAccessor(); - services.AddSingleton(); // Uncomment if you have CacheServiceRedis services.AddHttpClient(); diff --git a/DysonNetwork.Insight/Startup/ServiceCollectionExtensions.cs b/DysonNetwork.Insight/Startup/ServiceCollectionExtensions.cs index 40fca32..f9d7ec9 100644 --- a/DysonNetwork.Insight/Startup/ServiceCollectionExtensions.cs +++ b/DysonNetwork.Insight/Startup/ServiceCollectionExtensions.cs @@ -14,9 +14,7 @@ public static class ServiceCollectionExtensions public static IServiceCollection AddAppServices(this IServiceCollection services) { services.AddDbContext(); - services.AddSingleton(SystemClock.Instance); services.AddHttpContextAccessor(); - services.AddSingleton(); services.AddHttpClient(); diff --git a/DysonNetwork.Pass/Startup/ServiceCollectionExtensions.cs b/DysonNetwork.Pass/Startup/ServiceCollectionExtensions.cs index ffa9944..cdeb908 100644 --- a/DysonNetwork.Pass/Startup/ServiceCollectionExtensions.cs +++ b/DysonNetwork.Pass/Startup/ServiceCollectionExtensions.cs @@ -35,9 +35,7 @@ public static class ServiceCollectionExtensions services.AddLocalization(options => options.ResourcesPath = "Resources"); services.AddDbContext(); - services.AddSingleton(SystemClock.Instance); services.AddHttpContextAccessor(); - services.AddSingleton(); services.AddHttpClient(); diff --git a/DysonNetwork.Ring/Startup/ServiceCollectionExtensions.cs b/DysonNetwork.Ring/Startup/ServiceCollectionExtensions.cs index 6274017..093359f 100644 --- a/DysonNetwork.Ring/Startup/ServiceCollectionExtensions.cs +++ b/DysonNetwork.Ring/Startup/ServiceCollectionExtensions.cs @@ -17,9 +17,7 @@ public static class ServiceCollectionExtensions public static IServiceCollection AddAppServices(this IServiceCollection services, IConfiguration configuration) { services.AddDbContext(); - services.AddSingleton(SystemClock.Instance); services.AddHttpContextAccessor(); - services.AddSingleton(); services.AddHttpClient(); diff --git a/DysonNetwork.Shared/Cache/CacheService.cs b/DysonNetwork.Shared/Cache/CacheService.cs index fbcac9a..598556f 100644 --- a/DysonNetwork.Shared/Cache/CacheService.cs +++ b/DysonNetwork.Shared/Cache/CacheService.cs @@ -1,396 +1,201 @@ -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Metadata; -using DysonNetwork.Shared.Data; -using NodaTime; -using NodaTime.Serialization.SystemTextJson; +using Microsoft.Extensions.Caching.Distributed; +using RedLockNet; using StackExchange.Redis; namespace DysonNetwork.Shared.Cache; -/// -/// Represents a distributed lock that can be used to synchronize access across multiple processes -/// -public interface IDistributedLock : IAsyncDisposable +public class CacheServiceRedis( + IDistributedCache cache, + IConnectionMultiplexer redis, + ICacheSerializer serializer, + IDistributedLockFactory lockFactory +) + : ICacheService { - /// - /// The resource identifier this lock is protecting - /// - string Resource { get; } + private const string GlobalKeyPrefix = "dyson:"; + private const string GroupKeyPrefix = GlobalKeyPrefix + "cg:"; + private const string LockKeyPrefix = GlobalKeyPrefix + "lock:"; - /// - /// Unique identifier for this lock instance - /// - string LockId { get; } + private static string Normalize(string key) => $"{GlobalKeyPrefix}{key}"; - /// - /// Extends the lock's expiration time - /// - Task ExtendAsync(TimeSpan timeSpan); - - /// - /// Releases the lock immediately - /// - Task ReleaseAsync(); -} - -public interface ICacheService -{ - /// - /// Sets a value in the cache with an optional expiration time - /// - Task SetAsync(string key, T value, TimeSpan? expiry = null); - - /// - /// Gets a value from the cache - /// - Task GetAsync(string key); - - /// - /// Get a value from the cache with the found status - /// - Task<(bool found, T? value)> GetAsyncWithStatus(string key); - - /// - /// Removes a specific key from the cache - /// - Task RemoveAsync(string key); - - /// - /// Adds a key to a group for group-based operations - /// - Task AddToGroupAsync(string key, string group); - - /// - /// Removes all keys associated with a specific group - /// - Task RemoveGroupAsync(string group); - - /// - /// Gets all keys belonging to a specific group - /// - Task> GetGroupKeysAsync(string group); - - /// - /// Helper method to set a value in cache and associate it with multiple groups in one operation - /// - /// The type of value being cached - /// Cache key - /// The value to cache - /// Optional collection of group names to associate the key with - /// Optional expiration time for the cached item - /// True if the set operation was successful - Task SetWithGroupsAsync(string key, T value, IEnumerable? groups = null, TimeSpan? expiry = null); - - /// - /// Acquires a distributed lock on the specified resource - /// - /// The resource identifier to lock - /// How long the lock should be held before automatically expiring - /// How long to wait for the lock before giving up - /// How often to retry acquiring the lock during the wait time - /// A distributed lock instance if acquired, null otherwise - Task AcquireLockAsync(string resource, TimeSpan expiry, TimeSpan? waitTime = null, - TimeSpan? retryInterval = null); - - /// - /// Executes an action with a distributed lock, ensuring the lock is properly released afterwards - /// - /// The resource identifier to lock - /// The action to execute while holding the lock - /// How long the lock should be held before automatically expiring - /// How long to wait for the lock before giving up - /// How often to retry acquiring the lock during the wait time - /// True if the lock was acquired and the action was executed, false otherwise - Task ExecuteWithLockAsync(string resource, Func action, TimeSpan expiry, TimeSpan? waitTime = null, - TimeSpan? retryInterval = null); - - /// - /// Executes a function with a distributed lock, ensuring the lock is properly released afterwards - /// - /// The return type of the function - /// The resource identifier to lock - /// The function to execute while holding the lock - /// How long the lock should be held before automatically expiring - /// How long to wait for the lock before giving up - /// How often to retry acquiring the lock during the wait time - /// The result of the function if the lock was acquired, default(T) otherwise - Task<(bool Acquired, T? Result)> ExecuteWithLockAsync(string resource, Func> func, TimeSpan expiry, - TimeSpan? waitTime = null, TimeSpan? retryInterval = null); -} - -public class RedisDistributedLock : IDistributedLock -{ - private readonly IDatabase _database; - private bool _disposed; - - public string Resource { get; } - public string LockId { get; } - - internal RedisDistributedLock(IDatabase database, string resource, string lockId) - { - _database = database; - Resource = resource; - LockId = lockId; - } - - public async Task ExtendAsync(TimeSpan timeSpan) - { - if (_disposed) - throw new ObjectDisposedException(nameof(RedisDistributedLock)); - - var script = @" - if redis.call('get', KEYS[1]) == ARGV[1] then - return redis.call('pexpire', KEYS[1], ARGV[2]) - else - return 0 - end - "; - - var result = await _database.ScriptEvaluateAsync( - script, - [$"{CacheServiceRedis.LockKeyPrefix}{Resource}"], - [LockId, (long)timeSpan.TotalMilliseconds] - ); - - return (long)result! == 1; - } - - public async Task ReleaseAsync() - { - if (_disposed) - return; - - var script = @" - if redis.call('get', KEYS[1]) == ARGV[1] then - return redis.call('del', KEYS[1]) - else - return 0 - end - "; - - await _database.ScriptEvaluateAsync( - script, - [$"{CacheServiceRedis.LockKeyPrefix}{Resource}"], - [LockId] - ); - - _disposed = true; - } - - public async ValueTask DisposeAsync() - { - await ReleaseAsync(); - GC.SuppressFinalize(this); - } -} - -public class CacheServiceRedis : ICacheService -{ - private readonly IDatabase _database; - private readonly JsonSerializerOptions _jsonOptions; - - // Global prefix for all cache keys - public const string GlobalKeyPrefix = "dyson:"; - - // Using prefixes for different types of keys - public const string GroupKeyPrefix = GlobalKeyPrefix + "cg:"; - public const string LockKeyPrefix = GlobalKeyPrefix + "lock:"; - - public CacheServiceRedis(IConnectionMultiplexer redis) - { - var rds = redis ?? throw new ArgumentNullException(nameof(redis)); - _database = rds.GetDatabase(); - - // Configure System.Text.Json with proper NodaTime serialization - _jsonOptions = new JsonSerializerOptions - { - TypeInfoResolver = new DefaultJsonTypeInfoResolver - { - Modifiers = { JsonExtensions.UnignoreAllProperties() }, - }, - ReferenceHandler = ReferenceHandler.Preserve, - NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals, - Converters = { new ByteStringConverter() } - }; - _jsonOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); - _jsonOptions.PropertyNameCaseInsensitive = true; - } + // ----------------------------------------------------- + // BASIC OPERATIONS + // ----------------------------------------------------- public async Task SetAsync(string key, T value, TimeSpan? expiry = null) { - key = $"{GlobalKeyPrefix}{key}"; - if (string.IsNullOrEmpty(key)) - throw new ArgumentException("Key cannot be null or empty", nameof(key)); + key = Normalize(key); - var serializedValue = JsonSerializer.Serialize(value, _jsonOptions); - return await _database.StringSetAsync(key, serializedValue, expiry); + var json = serializer.Serialize(value); + + var options = new DistributedCacheEntryOptions(); + if (expiry.HasValue) + options.SetAbsoluteExpiration(expiry.Value); + + await cache.SetStringAsync(key, json, options); + return true; } public async Task GetAsync(string key) { - key = $"{GlobalKeyPrefix}{key}"; - if (string.IsNullOrEmpty(key)) - throw new ArgumentException("Key cannot be null or empty", nameof(key)); + key = Normalize(key); - var value = await _database.StringGetAsync(key); + var json = await cache.GetStringAsync(key); + if (json is null) + return default; - return value.IsNullOrEmpty ? default : - // For NodaTime serialization, use the configured JSON options - JsonSerializer.Deserialize(value.ToString(), _jsonOptions); + return serializer.Deserialize(json); } public async Task<(bool found, T? value)> GetAsyncWithStatus(string key) { - key = $"{GlobalKeyPrefix}{key}"; - if (string.IsNullOrEmpty(key)) - throw new ArgumentException("Key cannot be null or empty", nameof(key)); + key = Normalize(key); - var value = await _database.StringGetAsync(key); + var json = await cache.GetStringAsync(key); + if (json is null) + return (false, default); - return value.IsNullOrEmpty ? (false, default) : - // For NodaTime serialization, use the configured JSON options - (true, JsonSerializer.Deserialize(value!.ToString(), _jsonOptions)); + return (true, serializer.Deserialize(json)); } public async Task RemoveAsync(string key) { - key = $"{GlobalKeyPrefix}{key}"; - if (string.IsNullOrEmpty(key)) - throw new ArgumentException("Key cannot be null or empty", nameof(key)); + key = Normalize(key); - // Before removing the key, find all groups it belongs to and remove it from them - var script = @" - local groups = redis.call('KEYS', ARGV[1]) - for _, group in ipairs(groups) do - redis.call('SREM', group, ARGV[2]) - end - return redis.call('DEL', ARGV[2]) - "; + // Remove key from all groups + var db = redis.GetDatabase(); - var result = await _database.ScriptEvaluateAsync( - script, - values: [$"{GroupKeyPrefix}*", key] - ); + var groupPattern = $"{GroupKeyPrefix}*"; + var server = redis.GetServers().First(); - return (long)result! > 0; + var groups = server.Keys(pattern: groupPattern); + foreach (var group in groups) + { + await db.SetRemoveAsync(group, key); + } + + await cache.RemoveAsync(key); + return true; } + // ----------------------------------------------------- + // GROUP OPERATIONS + // ----------------------------------------------------- + public async Task AddToGroupAsync(string key, string group) { - if (string.IsNullOrEmpty(key)) - throw new ArgumentException(@"Key cannot be null or empty.", nameof(key)); - - if (string.IsNullOrEmpty(group)) - throw new ArgumentException(@"Group cannot be null or empty.", nameof(group)); - + key = Normalize(key); + var db = redis.GetDatabase(); var groupKey = $"{GroupKeyPrefix}{group}"; - key = $"{GlobalKeyPrefix}{key}"; - await _database.SetAddAsync(groupKey, key); + await db.SetAddAsync(groupKey, key); } public async Task RemoveGroupAsync(string group) { - if (string.IsNullOrEmpty(group)) - throw new ArgumentException(@"Group cannot be null or empty.", nameof(group)); - var groupKey = $"{GroupKeyPrefix}{group}"; + var db = redis.GetDatabase(); - // Get all keys in the group - var keys = await _database.SetMembersAsync(groupKey); + var keys = await db.SetMembersAsync(groupKey); if (keys.Length > 0) { - // Delete all the keys - var keysTasks = keys.Select(key => _database.KeyDeleteAsync(key.ToString())); - await Task.WhenAll(keysTasks); + foreach (var key in keys) + await cache.RemoveAsync(key.ToString()); } - // Delete the group itself - await _database.KeyDeleteAsync(groupKey); + await db.KeyDeleteAsync(groupKey); } public async Task> GetGroupKeysAsync(string group) { - if (string.IsNullOrEmpty(group)) - throw new ArgumentException("Group cannot be null or empty.", nameof(group)); - - var groupKey = string.Concat(GroupKeyPrefix, group); - var members = await _database.SetMembersAsync(groupKey); - - return members.Select(m => m.ToString()); + var groupKey = $"{GroupKeyPrefix}{group}"; + var db = redis.GetDatabase(); + var members = await db.SetMembersAsync(groupKey); + return members.Select(x => x.ToString()); } - public async Task SetWithGroupsAsync(string key, T value, IEnumerable? groups = null, + public async Task SetWithGroupsAsync( + string key, + T value, + IEnumerable? groups = null, TimeSpan? expiry = null) { - // First, set the value in the cache - var setResult = await SetAsync(key, value, expiry); + var result = await SetAsync(key, value, expiry); + if (!result || groups == null) + return result; - // If successful and there are groups to associate, add the key to each group - if (!setResult || groups == null) return setResult; - var groupsArray = groups.Where(g => !string.IsNullOrEmpty(g)).ToArray(); - if (groupsArray.Length <= 0) return setResult; - var tasks = groupsArray.Select(group => AddToGroupAsync(key, group)); + var tasks = groups.Select(g => AddToGroupAsync(key, g)); await Task.WhenAll(tasks); - - return setResult; + return true; } - public async Task AcquireLockAsync(string resource, TimeSpan expiry, TimeSpan? waitTime = null, + // ----------------------------------------------------- + // DISTRIBUTED LOCK (RedLock wrapper) + // ----------------------------------------------------- + + private readonly TimeSpan _defaultRetry = TimeSpan.FromMilliseconds(100); + + public async Task 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)); + if (string.IsNullOrWhiteSpace(resource)) + throw new ArgumentException("Resource cannot be null", nameof(resource)); var lockKey = $"{LockKeyPrefix}{resource}"; - var lockId = Guid.NewGuid().ToString("N"); - var waitTimeSpan = waitTime ?? TimeSpan.Zero; - var retryIntervalSpan = retryInterval ?? TimeSpan.FromMilliseconds(100); + var redlock = await lockFactory.CreateLockAsync( + lockKey, + expiry, + waitTime ?? TimeSpan.Zero, + retryInterval ?? _defaultRetry + ); - var startTime = DateTime.UtcNow; - var acquired = false; - - // Try to acquire the lock, retry until waitTime is exceeded - while (!acquired && (DateTime.UtcNow - startTime) < waitTimeSpan) - { - acquired = await _database.StringSetAsync(lockKey, lockId, expiry, When.NotExists); - - if (!acquired) - { - await Task.Delay(retryIntervalSpan); - } - } - - if (!acquired) - { - return null; // Could not acquire the lock within the wait time - } - - return new RedisDistributedLock(_database, resource, lockId); + return !redlock.IsAcquired ? null : new RedLockAdapter(redlock, resource); } - public async Task ExecuteWithLockAsync(string resource, Func action, TimeSpan expiry, - TimeSpan? waitTime = null, TimeSpan? retryInterval = null) + public async Task ExecuteWithLockAsync( + string resource, + Func action, + TimeSpan expiry, + TimeSpan? waitTime = null, + TimeSpan? retryInterval = null) { - await using var lockObj = await AcquireLockAsync(resource, expiry, waitTime, retryInterval); - - if (lockObj == null) - return false; // Could not acquire the lock + await using var l = await AcquireLockAsync(resource, expiry, waitTime, retryInterval); + if (l is null) + return false; await action(); return true; } - public async Task<(bool Acquired, T? Result)> ExecuteWithLockAsync(string resource, Func> func, - TimeSpan expiry, TimeSpan? waitTime = null, TimeSpan? retryInterval = null) + public async Task<(bool Acquired, T? Result)> ExecuteWithLockAsync( + string resource, + Func> func, + TimeSpan expiry, + TimeSpan? waitTime = null, + TimeSpan? retryInterval = null) { - await using var lockObj = await AcquireLockAsync(resource, expiry, waitTime, retryInterval); - - if (lockObj == null) - return (false, default); // Could not acquire the lock + await using var l = await AcquireLockAsync(resource, expiry, waitTime, retryInterval); + if (l is null) + return (false, default); var result = await func(); return (true, result); } } + +public class RedLockAdapter(IRedLock inner, string resource) : IDistributedLock +{ + public string Resource { get; } = resource; + public string LockId => inner.LockId; + + public ValueTask ReleaseAsync() => inner.DisposeAsync(); + + public async ValueTask DisposeAsync() + { + await inner.DisposeAsync(); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/DysonNetwork.Shared/Cache/ICacheSerializer.cs b/DysonNetwork.Shared/Cache/ICacheSerializer.cs new file mode 100644 index 0000000..801a806 --- /dev/null +++ b/DysonNetwork.Shared/Cache/ICacheSerializer.cs @@ -0,0 +1,7 @@ +namespace DysonNetwork.Shared.Cache; + +public interface ICacheSerializer +{ + string Serialize(T value); + T? Deserialize(string data); +} \ No newline at end of file diff --git a/DysonNetwork.Shared/Cache/ICacheService.cs b/DysonNetwork.Shared/Cache/ICacheService.cs new file mode 100644 index 0000000..a7c79f7 --- /dev/null +++ b/DysonNetwork.Shared/Cache/ICacheService.cs @@ -0,0 +1,86 @@ +namespace DysonNetwork.Shared.Cache; + +public interface ICacheService +{ + /// + /// Sets a value in the cache with an optional expiration time + /// + Task SetAsync(string key, T value, TimeSpan? expiry = null); + + /// + /// Gets a value from the cache + /// + Task GetAsync(string key); + + /// + /// Get a value from the cache with the found status + /// + Task<(bool found, T? value)> GetAsyncWithStatus(string key); + + /// + /// Removes a specific key from the cache + /// + Task RemoveAsync(string key); + + /// + /// Adds a key to a group for group-based operations + /// + Task AddToGroupAsync(string key, string group); + + /// + /// Removes all keys associated with a specific group + /// + Task RemoveGroupAsync(string group); + + /// + /// Gets all keys belonging to a specific group + /// + Task> GetGroupKeysAsync(string group); + + /// + /// Helper method to set a value in cache and associate it with multiple groups in one operation + /// + /// The type of value being cached + /// Cache key + /// The value to cache + /// Optional collection of group names to associate the key with + /// Optional expiration time for the cached item + /// True if the set operation was successful + Task SetWithGroupsAsync(string key, T value, IEnumerable? groups = null, TimeSpan? expiry = null); + + /// + /// Acquires a distributed lock on the specified resource + /// + /// The resource identifier to lock + /// How long the lock should be held before automatically expiring + /// How long to wait for the lock before giving up + /// How often to retry acquiring the lock during the wait time + /// A distributed lock instance if acquired, null otherwise + Task AcquireLockAsync(string resource, TimeSpan expiry, TimeSpan? waitTime = null, + TimeSpan? retryInterval = null); + + /// + /// Executes an action with a distributed lock, ensuring the lock is properly released afterwards + /// + /// The resource identifier to lock + /// The action to execute while holding the lock + /// How long the lock should be held before automatically expiring + /// How long to wait for the lock before giving up + /// How often to retry acquiring the lock during the wait time + /// True if the lock was acquired and the action was executed, false otherwise + Task ExecuteWithLockAsync(string resource, Func action, TimeSpan expiry, TimeSpan? waitTime = null, + TimeSpan? retryInterval = null); + + /// + /// Executes a function with a distributed lock, ensuring the lock is properly released afterwards + /// + /// The return type of the function + /// The resource identifier to lock + /// The function to execute while holding the lock + /// How long the lock should be held before automatically expiring + /// How long to wait for the lock before giving up + /// How often to retry acquiring the lock during the wait time + /// The result of the function if the lock was acquired, default(T) otherwise + Task<(bool Acquired, T? Result)> ExecuteWithLockAsync(string resource, Func> func, TimeSpan expiry, + TimeSpan? waitTime = null, TimeSpan? retryInterval = null); +} diff --git a/DysonNetwork.Shared/Cache/IDistributedLock.cs b/DysonNetwork.Shared/Cache/IDistributedLock.cs new file mode 100644 index 0000000..815b0cc --- /dev/null +++ b/DysonNetwork.Shared/Cache/IDistributedLock.cs @@ -0,0 +1,22 @@ +namespace DysonNetwork.Shared.Cache; + +/// +/// Represents a distributed lock that can be used to synchronize access across multiple processes +/// +public interface IDistributedLock : IAsyncDisposable +{ + /// + /// The resource identifier this lock is protecting + /// + string Resource { get; } + + /// + /// Unique identifier for this lock instance + /// + string LockId { get; } + + /// + /// Releases the lock immediately + /// + ValueTask ReleaseAsync(); +} diff --git a/DysonNetwork.Shared/Cache/JsonCacheSerializer.cs b/DysonNetwork.Shared/Cache/JsonCacheSerializer.cs new file mode 100644 index 0000000..9f289d5 --- /dev/null +++ b/DysonNetwork.Shared/Cache/JsonCacheSerializer.cs @@ -0,0 +1,20 @@ +using System.Text.Json; + +namespace DysonNetwork.Shared.Cache; + +public class JsonCacheSerializer(JsonSerializerOptions? options = null) : ICacheSerializer +{ + private readonly JsonSerializerOptions _options = options ?? new JsonSerializerOptions(JsonSerializerDefaults.Web) + { + // Customize as needed (NodaTime, camelCase, converters, etc.) + WriteIndented = false + }; + + // Customize as needed (NodaTime, camelCase, converters, etc.) + + public string Serialize(T value) + => JsonSerializer.Serialize(value, _options); + + public T? Deserialize(string data) + => JsonSerializer.Deserialize(data, _options); +} \ No newline at end of file diff --git a/DysonNetwork.Shared/Cache/MessagePackCacheSerializer.cs b/DysonNetwork.Shared/Cache/MessagePackCacheSerializer.cs new file mode 100644 index 0000000..c2cc664 --- /dev/null +++ b/DysonNetwork.Shared/Cache/MessagePackCacheSerializer.cs @@ -0,0 +1,21 @@ +using MessagePack; + +namespace DysonNetwork.Shared.Cache; + +public class MessagePackCacheSerializer(MessagePackSerializerOptions? options = null) : ICacheSerializer +{ + private readonly MessagePackSerializerOptions _options = options ?? MessagePackSerializerOptions.Standard + .WithCompression(MessagePackCompression.Lz4BlockArray); + + public string Serialize(T value) + { + var bytes = MessagePackSerializer.Serialize(value!, _options); + return Convert.ToBase64String(bytes); + } + + public T? Deserialize(string data) + { + var bytes = Convert.FromBase64String(data); + return MessagePackSerializer.Deserialize(bytes, _options); + } +} \ No newline at end of file diff --git a/DysonNetwork.Shared/DysonNetwork.Shared.csproj b/DysonNetwork.Shared/DysonNetwork.Shared.csproj index f58cf2d..ebf2020 100644 --- a/DysonNetwork.Shared/DysonNetwork.Shared.csproj +++ b/DysonNetwork.Shared/DysonNetwork.Shared.csproj @@ -22,8 +22,10 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + @@ -31,6 +33,8 @@ + + diff --git a/DysonNetwork.Shared/Extensions.cs b/DysonNetwork.Shared/Extensions.cs index 74f9008..f717f52 100644 --- a/DysonNetwork.Shared/Extensions.cs +++ b/DysonNetwork.Shared/Extensions.cs @@ -1,11 +1,18 @@ +using DysonNetwork.Shared.Cache; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging; +using NodaTime; using OpenTelemetry; using OpenTelemetry.Metrics; using OpenTelemetry.Trace; +using RedLockNet; +using RedLockNet.SERedis; +using RedLockNet.SERedis.Configuration; +using StackExchange.Redis; namespace Microsoft.Extensions.Hosting; @@ -43,11 +50,28 @@ public static class Extensions // options.AllowedSchemes = ["https"]; // }); + builder.Services.AddSingleton(SystemClock.Instance); + builder.AddNatsClient("queue"); builder.AddRedisClient("cache", configureOptions: opts => { opts.AbortOnConnectFail = false; }); + + // Setup cache service + builder.Services.AddStackExchangeRedisCache(options => + { + options.Configuration = builder.Configuration.GetConnectionString("cache"); + options.InstanceName = "dyson:"; + }); + builder.Services.AddSingleton(sp => + { + var mux = sp.GetRequiredService(); + return RedLockFactory.Create(new List { new(mux) }); + }); + builder.Services.AddSingleton(); + // Using message pack for now + builder.Services.AddSingleton(); return builder; } diff --git a/DysonNetwork.Sphere/Startup/ServiceCollectionExtensions.cs b/DysonNetwork.Sphere/Startup/ServiceCollectionExtensions.cs index dfd76fb..7e00870 100644 --- a/DysonNetwork.Sphere/Startup/ServiceCollectionExtensions.cs +++ b/DysonNetwork.Sphere/Startup/ServiceCollectionExtensions.cs @@ -27,9 +27,7 @@ public static class ServiceCollectionExtensions services.AddLocalization(options => options.ResourcesPath = "Resources"); services.AddDbContext(); - services.AddSingleton(SystemClock.Instance); services.AddHttpContextAccessor(); - services.AddSingleton(); services.AddHttpClient(); diff --git a/DysonNetwork.Zone/Startup/ServiceCollectionExtensions.cs b/DysonNetwork.Zone/Startup/ServiceCollectionExtensions.cs index c60d126..437730f 100644 --- a/DysonNetwork.Zone/Startup/ServiceCollectionExtensions.cs +++ b/DysonNetwork.Zone/Startup/ServiceCollectionExtensions.cs @@ -18,9 +18,7 @@ public static class ServiceCollectionExtensions services.AddLocalization(options => options.ResourcesPath = "Resources"); services.AddDbContext(); - services.AddSingleton(SystemClock.Instance); services.AddHttpContextAccessor(); - services.AddSingleton(); services.AddSingleton(); services.AddHttpClient();