Creado apartir del commit b888de6c92056de294456f581a56e25e734d4ab7 de develop
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
using adas_core.Application.Services.Interfaces;
|
||||
using adas_core.Domain.Enums;
|
||||
using adas_core.Domain.Models.AppSettings;
|
||||
using adas_core.Domain.Models.GroupedObservations;
|
||||
using adas_core.Domain.Utils;
|
||||
using MongoDB.Bson;
|
||||
|
||||
namespace adas_core.Application.Services.Caching
|
||||
{
|
||||
/// <summary>
|
||||
/// Orquestador de caché. Selecciona el backend (Redis, InMemory, None)
|
||||
/// según CacheSettings y la entidad del key.
|
||||
/// Implementa ICacheService y delega en el backend elegido.
|
||||
/// </summary>
|
||||
public class CacheDispatcher(
|
||||
RedisService redis,
|
||||
CacheService memory,
|
||||
NoCacheService noop,
|
||||
CacheSettings cacheSettings)
|
||||
: ICacheService
|
||||
{
|
||||
|
||||
// Selección de backend
|
||||
private ICacheService SelectBackend(string key)
|
||||
{
|
||||
var entity = CacheKeyClassifier.Classify(key);
|
||||
var mode = entity switch
|
||||
{
|
||||
CacheEnum.EntityType.Patients => cacheSettings.Patients,
|
||||
CacheEnum.EntityType.Displays => cacheSettings.Displays,
|
||||
CacheEnum.EntityType.PumpObservations => cacheSettings.PumpObservations,
|
||||
CacheEnum.EntityType.Appointments => cacheSettings.Appointments,
|
||||
CacheEnum.EntityType.GroupedObservations => cacheSettings.GroupedObservations,
|
||||
_ => CacheEnum.Mode.Cache
|
||||
};
|
||||
|
||||
return mode switch
|
||||
{
|
||||
CacheEnum.Mode.Redis => redis,
|
||||
CacheEnum.Mode.Cache => memory,
|
||||
_ => noop
|
||||
};
|
||||
}
|
||||
|
||||
// Para GroupedObservations generamos la misma clave compuesta que el resto de servicios,
|
||||
// de modo que el clasificador y la política de TTL funcionen igual.
|
||||
private static string BuildGroupedKey(GroupedField gf, ObjectId patientId)
|
||||
=> $"GroupedObs:{patientId}:{gf.Name}";
|
||||
|
||||
private ICacheService SelectBackend(GroupedField groupedField, ObjectId patientId)
|
||||
=> SelectBackend(BuildGroupedKey(groupedField, patientId));
|
||||
|
||||
|
||||
// GetOrSet (KEY string)
|
||||
public Task<T> GetOrSetObjectAsync<T>(
|
||||
string key,
|
||||
Func<Task<T>> factory,
|
||||
TimeSpan? ttl = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> SelectBackend(key).GetOrSetObjectAsync(key, factory, ttl, cancellationToken);
|
||||
|
||||
public Task<string?> GetOrSetValueAsync(
|
||||
string key,
|
||||
Func<Task<string>> loader,
|
||||
TimeSpan? ttl = null)
|
||||
=> SelectBackend(key).GetOrSetValueAsync(key, loader, ttl);
|
||||
|
||||
|
||||
// GetOrSet (GroupedField + PatientId)
|
||||
public Task<T> GetOrSetObjectAsync<T>(
|
||||
GroupedField groupedField,
|
||||
ObjectId patientId,
|
||||
Func<Task<T>> factory,
|
||||
TimeSpan? ttl = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> SelectBackend(groupedField, patientId)
|
||||
.GetOrSetObjectAsync(groupedField, patientId, factory, ttl, cancellationToken);
|
||||
|
||||
|
||||
// Set/Get básicos
|
||||
public void SetValue(string key, string value)
|
||||
=> SelectBackend(key).SetValue(key, value);
|
||||
|
||||
public string? GetValue(string key)
|
||||
=> SelectBackend(key).GetValue(key);
|
||||
|
||||
public Task<T?> GetObjectAsync<T>(string key, bool upd = true)
|
||||
=> SelectBackend(key).GetObjectAsync<T>(key, upd);
|
||||
|
||||
public Task SetObjectAsync<T>(string key, T obj, bool upd = true)
|
||||
=> SelectBackend(key).SetObjectAsync(key, obj, upd);
|
||||
|
||||
public Task<T?> GetObjectAsync<T>(string key, TimeSpan? ttl, bool upd)
|
||||
=> SelectBackend(key).GetObjectAsync<T>(key, ttl, upd);
|
||||
|
||||
public Task SetObjectAsync<T>(string key, T obj, TimeSpan? ttl, bool upd)
|
||||
=> SelectBackend(key).SetObjectAsync(key, obj, ttl, upd);
|
||||
|
||||
public Task DeleteObjectAsync(string key)
|
||||
=> SelectBackend(key).DeleteObjectAsync(key);
|
||||
|
||||
public async Task<long> DeleteByPatternAsync(string pattern)
|
||||
=> await redis.DeleteByPatternAsync(pattern) + await memory.DeleteByPatternAsync(pattern);
|
||||
|
||||
public void CleanCache()
|
||||
{
|
||||
memory.CleanCache();
|
||||
redis.CleanCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
using adas_core.Application.Services.Interfaces;
|
||||
using adas_core.Domain.Models.GroupedObservations;
|
||||
using MongoDB.Bson;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace adas_core.Application.Services.Caching
|
||||
{
|
||||
/// <summary>
|
||||
/// Implementación de caché en memoria con soporte de locking seguro
|
||||
/// mediante LockManagerService + InMemoryLockProvider.
|
||||
/// Compatible con la interfaz ICacheService incluyendo GetOrSet.
|
||||
/// </summary>
|
||||
public class CacheService(LockManagerService lockManager) : ICacheService
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, object> _mem = new();
|
||||
|
||||
// HELPERS
|
||||
private static string BuildGroupedKey(GroupedField gf, ObjectId patientId)
|
||||
=> $"GroupedObs:{patientId}:{gf.Name}";
|
||||
|
||||
|
||||
// GET OR SET (string key)
|
||||
public async Task<T> GetOrSetObjectAsync<T>(
|
||||
string key,
|
||||
Func<Task<T>> factory,
|
||||
TimeSpan? ttl = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// FAST PATH
|
||||
if (_mem.TryGetValue(key, out var existing))
|
||||
return (T)existing;
|
||||
|
||||
// LOCKED PATH
|
||||
return await lockManager.WithLockAsync(
|
||||
$"getorset:{key}",
|
||||
TimeSpan.FromSeconds(5),
|
||||
async () =>
|
||||
{
|
||||
if (_mem.TryGetValue(key, out var again))
|
||||
return (T)again;
|
||||
|
||||
var created = await factory();
|
||||
|
||||
if (created != null!)
|
||||
_mem[key] = created;
|
||||
|
||||
return created!;
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<string?> GetOrSetValueAsync(
|
||||
string key,
|
||||
Func<Task<string>> loader,
|
||||
TimeSpan? ttlOverride = null)
|
||||
{
|
||||
|
||||
var result = await GetOrSetObjectAsync(key, loader, ttlOverride);
|
||||
return (string?)result;
|
||||
|
||||
}
|
||||
|
||||
|
||||
// GET OR SET (GroupedField + patientId)
|
||||
public async Task<T> GetOrSetObjectAsync<T>(
|
||||
GroupedField groupedField,
|
||||
ObjectId patientId,
|
||||
Func<Task<T>> factory,
|
||||
TimeSpan? ttl = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var key = BuildGroupedKey(groupedField, patientId);
|
||||
|
||||
if (_mem.TryGetValue(key, out var existing))
|
||||
return (T)existing;
|
||||
|
||||
return await lockManager.WithLockAsync(
|
||||
$"getorset:{key}",
|
||||
TimeSpan.FromSeconds(5),
|
||||
async () =>
|
||||
{
|
||||
if (_mem.TryGetValue(key, out var again))
|
||||
return (T)again;
|
||||
|
||||
var created = await factory();
|
||||
|
||||
if (created != null!)
|
||||
_mem[key] = created;
|
||||
|
||||
return created!;
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
// GET / SET
|
||||
public void SetValue(string key, string value)
|
||||
=> _mem[key] = value;
|
||||
|
||||
public string? GetValue(string key)
|
||||
=> _mem.TryGetValue(key, out var v) ? v.ToString() : null;
|
||||
|
||||
public Task<T?> GetObjectAsync<T>(string key, bool updateExpiration = true)
|
||||
{
|
||||
return Task.FromResult(
|
||||
_mem.TryGetValue(key, out var v) ? (T?)v : default
|
||||
);
|
||||
}
|
||||
|
||||
public Task SetObjectAsync<T>(string key, T obj, bool updateExpiration = true)
|
||||
{
|
||||
_mem[key] = obj!;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<T?> GetObjectAsync<T>(string key, TimeSpan? ttlOverride, bool upd)
|
||||
=> GetObjectAsync<T>(key, upd);
|
||||
|
||||
public Task SetObjectAsync<T>(string key, T obj, TimeSpan? ttlOverride, bool upd)
|
||||
=> SetObjectAsync(key, obj, upd);
|
||||
|
||||
|
||||
// DELETE / CLEAN
|
||||
public Task DeleteObjectAsync(string key)
|
||||
{
|
||||
_mem.TryRemove(key, out _);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<long> DeleteByPatternAsync(string pattern)
|
||||
{
|
||||
var p = pattern.Replace("*", "");
|
||||
var keys = _mem.Keys.Where(k => k.Contains(p)).ToList();
|
||||
|
||||
long removed = 0;
|
||||
foreach (var k in keys)
|
||||
if (_mem.TryRemove(k, out _))
|
||||
removed++;
|
||||
|
||||
return Task.FromResult(removed);
|
||||
}
|
||||
|
||||
public void CleanCache() => _mem.Clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using System.Collections.Concurrent;
|
||||
using adas_core.Application.Services.Interfaces;
|
||||
|
||||
namespace adas_core.Application.Services.Caching
|
||||
{
|
||||
/// <summary>
|
||||
/// Implementación local del sistema de locking por clave.
|
||||
/// Se basa en SemaphoreSlim y solo controla concurrencia DENTRO del proceso.
|
||||
/// Para CacheService (in-memory).
|
||||
/// </summary>
|
||||
public class InMemoryLockProvider : ILockProvider
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, SemaphoreSlim> _locks = new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// Crea u obtiene un semáforo asociado a la clave.
|
||||
/// </summary>
|
||||
private SemaphoreSlim GetOrCreate(string key)
|
||||
{
|
||||
return _locks.GetOrAdd(key, _ => new SemaphoreSlim(1, 1));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Intenta adquirir el lock por clave.
|
||||
/// </summary>
|
||||
public async Task<bool> AcquireAsync(string key, TimeSpan timeout)
|
||||
{
|
||||
var sem = GetOrCreate(key);
|
||||
|
||||
try
|
||||
{
|
||||
return await sem.WaitAsync(timeout).ConfigureAwait(false);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// Si el semáforo se eliminó entre medio, creamos uno nuevo.
|
||||
_locks.TryRemove(key, out _);
|
||||
return await GetOrCreate(key).WaitAsync(timeout).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Libera el lock (si existe y no está ya liberado).
|
||||
/// </summary>
|
||||
public Task ReleaseAsync(string key)
|
||||
{
|
||||
if (!_locks.TryGetValue(key, out var sem))
|
||||
return Task.CompletedTask;
|
||||
|
||||
try
|
||||
{
|
||||
sem.Release();
|
||||
}
|
||||
catch (SemaphoreFullException)
|
||||
{
|
||||
// Idempotencia: ignoramos exceso de releases
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using adas_core.Application.Services.Interfaces;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace adas_core.Application.Services.Caching
|
||||
{
|
||||
/// <summary>
|
||||
/// Servicio que ejecuta acciones dentro de una sección crítica controlada por lock.
|
||||
/// CacheService lo usará para evitar condiciones de carrera en GetOrSet.
|
||||
/// Funciona igual para locks locales o distribuidos.
|
||||
/// </summary>
|
||||
public class LockManagerService(
|
||||
ILogger<LockManagerService> logger,
|
||||
ILockProvider provider)
|
||||
{
|
||||
/// <summary>
|
||||
/// Ejecuta una función que devuelve un valor bajo un lock por clave.
|
||||
/// </summary>
|
||||
public async Task<T> WithLockAsync<T>(
|
||||
string key,
|
||||
TimeSpan timeout,
|
||||
Func<Task<T>> action,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
logger.LogDebug(
|
||||
"LockManager — intentando adquirir lock para key={Key} timeout={TimeoutMs}ms",
|
||||
key, timeout.TotalMilliseconds);
|
||||
|
||||
var acquired = false;
|
||||
|
||||
try
|
||||
{
|
||||
acquired = await provider.AcquireAsync(key, timeout).ConfigureAwait(false);
|
||||
|
||||
if (!acquired)
|
||||
{
|
||||
logger.LogWarning(
|
||||
"LockManager — timeout al adquirir lock para key={Key} tras {TimeoutMs}ms",
|
||||
key, timeout.TotalMilliseconds);
|
||||
throw new TimeoutException($"No se pudo adquirir el lock para '{key}'");
|
||||
}
|
||||
|
||||
logger.LogDebug(
|
||||
"LockManager — lock adquirido correctamente para key={Key}",
|
||||
key);
|
||||
|
||||
return await action().ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (acquired)
|
||||
{
|
||||
await SafeReleaseAsync(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Version Task (sin valor).
|
||||
/// </summary>
|
||||
public Task WithLockAsync(
|
||||
string key,
|
||||
TimeSpan timeout,
|
||||
Func<Task> action,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return WithLockAsync<object?>(
|
||||
key,
|
||||
timeout,
|
||||
async () =>
|
||||
{
|
||||
await action();
|
||||
return null;
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Libera el lock y registra posibles errores sin interrumpir el flujo.
|
||||
/// </summary>
|
||||
private async Task SafeReleaseAsync(string key)
|
||||
{
|
||||
try
|
||||
{
|
||||
await provider.ReleaseAsync(key).ConfigureAwait(false);
|
||||
logger.LogDebug("LockManager — key={Key} lock deleted ", key);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "LockManager — key={Key} error deleting lock", key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
using adas_core.Application.Services.Interfaces;
|
||||
using adas_core.Domain.Models.GroupedObservations;
|
||||
using MongoDB.Bson;
|
||||
|
||||
namespace adas_core.Application.Services.Caching
|
||||
{
|
||||
/// <summary>
|
||||
/// Implementación nula de ICacheService.
|
||||
/// No almacena nada, no devuelve nada y no interfiere con el flujo.
|
||||
/// Se usa cuando el CacheMode es "None".
|
||||
/// </summary>
|
||||
public class NoCacheService : ICacheService
|
||||
{
|
||||
public void SetValue(string key, string value)
|
||||
{
|
||||
// No hacer nada
|
||||
}
|
||||
|
||||
public string? GetValue(string key)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public Task<T?> GetObjectAsync<T>(string key, bool updateExpiration = true)
|
||||
{
|
||||
return Task.FromResult<T?>(default);
|
||||
}
|
||||
|
||||
public Task SetObjectAsync<T>(string key, T obj, bool updateExpiration = true)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<T?> GetObjectAsync<T>(string key, TimeSpan? ttlOverride, bool updateExpiration)
|
||||
{
|
||||
return Task.FromResult<T?>(default);
|
||||
}
|
||||
|
||||
public Task SetObjectAsync<T>(string key, T obj, TimeSpan? ttlOverride, bool updateExpiration)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<long> DeleteByPatternAsync(string pattern)
|
||||
{
|
||||
return Task.FromResult(0L);
|
||||
}
|
||||
|
||||
public Task DeleteObjectAsync(string key)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void CleanCache()
|
||||
{
|
||||
// Nada que limpiar
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// GET OR SET - STRING KEY
|
||||
// ============================================================
|
||||
|
||||
public async Task<T> GetOrSetObjectAsync<T>(
|
||||
string key,
|
||||
Func<Task<T>> factory,
|
||||
TimeSpan? ttl = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// En modo NONE no hay caché → siempre ejecutar factory
|
||||
return await factory();
|
||||
}
|
||||
|
||||
public async Task<string?> GetOrSetValueAsync(
|
||||
string key,
|
||||
Func<Task<string>> loader,
|
||||
TimeSpan? ttl = null)
|
||||
{
|
||||
var result = await loader();
|
||||
return (string?)result;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// GET OR SET - GroupedField + patientId
|
||||
// ============================================================
|
||||
|
||||
public async Task<T> GetOrSetObjectAsync<T>(
|
||||
GroupedField groupedField,
|
||||
ObjectId patientId,
|
||||
Func<Task<T>> factory,
|
||||
TimeSpan? ttl = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Igual que arriba: en modo NONE no hay caché
|
||||
return await factory();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using StackExchange.Redis;
|
||||
using System.Collections.Concurrent;
|
||||
using adas_core.Application.Services.Interfaces;
|
||||
|
||||
namespace adas_core.Application.Services.Caching
|
||||
{
|
||||
/// <summary>
|
||||
/// Lock distribuido en Redis. Usa token por adquisición (owner)
|
||||
/// y liberación segura con script Lua: borra la key solo si el valor coincide.
|
||||
/// </summary>
|
||||
public class RedisLockProvider(Func<IDatabase?> getDatabase) : ILockProvider
|
||||
{
|
||||
private readonly string _prefix = "lock:";
|
||||
private readonly TimeSpan _ttl = TimeSpan.FromSeconds(5);
|
||||
private readonly TimeSpan _retryDelay = TimeSpan.FromMilliseconds(50);
|
||||
|
||||
private static readonly string LuaReleaseScript = @"
|
||||
if redis.call('get', KEYS[1]) == ARGV[1] then
|
||||
return redis.call('del', KEYS[1])
|
||||
else
|
||||
return 0
|
||||
end";
|
||||
|
||||
private readonly ConcurrentDictionary<string, string> _tokens =
|
||||
new(StringComparer.Ordinal);
|
||||
|
||||
public async Task<bool> AcquireAsync(string key, TimeSpan timeout)
|
||||
{
|
||||
var redis = getDatabase();
|
||||
if (redis is null) return false;
|
||||
|
||||
var redisKey = (RedisKey)(_prefix + key);
|
||||
var token = Guid.NewGuid().ToString("N");
|
||||
var end = DateTime.UtcNow.Add(timeout);
|
||||
|
||||
while (DateTime.UtcNow < end)
|
||||
{
|
||||
if (await redis.StringSetAsync(redisKey, token, _ttl, When.NotExists))
|
||||
{
|
||||
_tokens[key] = token;
|
||||
return true;
|
||||
}
|
||||
|
||||
await Task.Delay(_retryDelay);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task ReleaseAsync(string key)
|
||||
{
|
||||
if (!_tokens.TryRemove(key, out var token))
|
||||
return;
|
||||
|
||||
var redis = getDatabase();
|
||||
if (redis is null) return;
|
||||
|
||||
var redisKey = (RedisKey)(_prefix + key);
|
||||
|
||||
await redis.ScriptEvaluateAsync(
|
||||
LuaReleaseScript,
|
||||
[redisKey],
|
||||
[token]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
using adas_core.Application.Services.Interfaces;
|
||||
using adas_core.Domain.Models.AppSettings;
|
||||
using adas_core.Domain.Utils;
|
||||
using adas_core.Domain.Enums;
|
||||
using adas_core.Domain.Models.GroupedObservations;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
using MongoDB.Bson;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace adas_core.Application.Services.Caching
|
||||
{
|
||||
public class RedisService : ICacheService
|
||||
{
|
||||
private readonly ILogger<RedisService> _logger;
|
||||
private readonly CacheSettings _cacheSettings;
|
||||
private readonly LockManagerService _lockManager;
|
||||
private ConnectionMultiplexer? _connection;
|
||||
private IDatabase? _database;
|
||||
private IServer? _server;
|
||||
|
||||
public IDatabase? Database => _database;
|
||||
private bool _isRedisAvailable;
|
||||
|
||||
public RedisService(
|
||||
IOptions<CacheSettings> options,
|
||||
ILogger<RedisService> logger,
|
||||
LockManagerService lockManager)
|
||||
{
|
||||
_cacheSettings = options.Value;
|
||||
_logger = logger;
|
||||
_lockManager = lockManager;
|
||||
|
||||
if (!string.IsNullOrEmpty(_cacheSettings.Redis.ConnectionString))
|
||||
_ = InitializeRedisConnectionAsync();
|
||||
}
|
||||
|
||||
|
||||
// GET OR SET (string key)
|
||||
public async Task<T> GetOrSetObjectAsync<T>(
|
||||
string key,
|
||||
Func<Task<T>> factory,
|
||||
TimeSpan? ttl = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!_isRedisAvailable)
|
||||
return await factory();
|
||||
|
||||
var direct = await GetObjectAsync<T>(key);
|
||||
|
||||
if (direct is not null)
|
||||
return direct;
|
||||
|
||||
return await _lockManager.WithLockAsync(
|
||||
$"getorset:{key}",
|
||||
TimeSpan.FromSeconds(5),
|
||||
async () =>
|
||||
{
|
||||
var again = await GetObjectAsync<T>(key);
|
||||
if (again is not null)
|
||||
return again;
|
||||
|
||||
var created = await factory();
|
||||
|
||||
if (created != null)
|
||||
await SetObjectAsync(key, created, ttl, true);
|
||||
|
||||
return created!;
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<string?> GetOrSetValueAsync(
|
||||
string key,
|
||||
Func<Task<string>> loader,
|
||||
TimeSpan? ttl = null)
|
||||
{
|
||||
if (!_isRedisAvailable)
|
||||
return await loader();
|
||||
|
||||
var direct = GetValue(key);
|
||||
if (direct is not null)
|
||||
return direct;
|
||||
|
||||
return await _lockManager.WithLockAsync(
|
||||
$"getorset:{key}",
|
||||
TimeSpan.FromSeconds(5),
|
||||
async () =>
|
||||
{
|
||||
var again = GetValue(key);
|
||||
if (again is not null)
|
||||
return again;
|
||||
|
||||
var created = await loader();
|
||||
SetValue(key, created);
|
||||
|
||||
return created;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// GET OR SET (GroupedField + patientId)
|
||||
private static string BuildGroupedKey(GroupedField gf, ObjectId patientId)
|
||||
=> $"GroupedObs:{patientId}:{gf.Name}";
|
||||
|
||||
public async Task<T> GetOrSetObjectAsync<T>(
|
||||
GroupedField groupedField,
|
||||
ObjectId patientId,
|
||||
Func<Task<T>> factory,
|
||||
TimeSpan? ttl = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var key = BuildGroupedKey(groupedField, patientId);
|
||||
|
||||
if (!_isRedisAvailable)
|
||||
return await factory();
|
||||
|
||||
var direct = await GetObjectAsync<T>(key);
|
||||
if (direct is not null)
|
||||
return direct;
|
||||
|
||||
return await _lockManager.WithLockAsync(
|
||||
$"getorset:{key}",
|
||||
TimeSpan.FromSeconds(5),
|
||||
async () =>
|
||||
{
|
||||
var again = await GetObjectAsync<T>(key);
|
||||
if (again is not null)
|
||||
return again;
|
||||
|
||||
var created = await factory();
|
||||
|
||||
if (created != null)
|
||||
await SetObjectAsync(key, created, ttl, true);
|
||||
|
||||
return created!;
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
// BASIC OPERATIONS
|
||||
public void SetValue(string key, string value)
|
||||
=> _database?.StringSet(key, value, GetEntityTtl(key), true);
|
||||
|
||||
public string? GetValue(string key)
|
||||
{
|
||||
var val = _database?.StringGet(key);
|
||||
if (val.HasValue && ShouldRenewTtl(key))
|
||||
_database?.KeyExpire(key, GetEntityTtl(key));
|
||||
return val;
|
||||
}
|
||||
|
||||
public async Task<T?> GetObjectAsync<T>(string key, bool updateExpiration = true)
|
||||
{
|
||||
if (!_isRedisAvailable)
|
||||
return default;
|
||||
|
||||
var json = await _database!.StringGetAsync(key);
|
||||
if (json.IsNullOrEmpty)
|
||||
return default;
|
||||
|
||||
if (updateExpiration)
|
||||
_database!.KeyExpire(key, GetEntityTtl(key));
|
||||
|
||||
var settings = new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
||||
Converters = new List<JsonConverter> { new StringEnumConverter(), new ObjectIdConverter() }
|
||||
};
|
||||
try
|
||||
{
|
||||
return JsonConvert.DeserializeObject<T>(json!, settings);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError("Error deserializing object in RedisService {e}",e.ToString());
|
||||
throw new Exception($"Error deserializing object in RedisService {e}", e);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SetObjectAsync<T>(
|
||||
string key,
|
||||
T obj,
|
||||
bool updateExpiration = true)
|
||||
=> await SetObjectAsync(key, obj, null, updateExpiration);
|
||||
|
||||
public async Task SetObjectAsync<T>(
|
||||
string key,
|
||||
T obj,
|
||||
TimeSpan? ttlOverride,
|
||||
bool updateExpiration)
|
||||
{
|
||||
if (!_isRedisAvailable)
|
||||
return;
|
||||
var settings = new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
||||
Converters = new List<JsonConverter> { new StringEnumConverter(), new ObjectIdConverter() }
|
||||
};
|
||||
var json = JsonConvert.SerializeObject(obj, settings);
|
||||
await _database!.StringSetAsync(key, json, ttlOverride ?? GetEntityTtl(key), true);
|
||||
}
|
||||
|
||||
public async Task DeleteObjectAsync(string key)
|
||||
{
|
||||
if (_isRedisAvailable)
|
||||
await _database!.KeyDeleteAsync(key);
|
||||
}
|
||||
|
||||
public async Task<long> DeleteByPatternAsync(string pattern)
|
||||
{
|
||||
if (!_isRedisAvailable || _server == null)
|
||||
return 0;
|
||||
|
||||
var keys = _server.Keys(pattern: pattern).ToArray();
|
||||
|
||||
foreach (var key in keys)
|
||||
await _database!.KeyDeleteAsync(key);
|
||||
|
||||
return keys.Length;
|
||||
}
|
||||
|
||||
public void CleanCache()
|
||||
=> _server?.FlushDatabase();
|
||||
|
||||
|
||||
// TTL
|
||||
private TimeSpan? GetEntityTtl(string key)
|
||||
{
|
||||
var entity = CacheKeyClassifier.Classify(key);
|
||||
var ttl = _cacheSettings.Redis.Ttl;
|
||||
|
||||
int? seconds = entity switch
|
||||
{
|
||||
CacheEnum.EntityType.Patients => ttl.PatientsSeconds,
|
||||
CacheEnum.EntityType.Displays => ttl.DisplaysSeconds,
|
||||
_ => ttl.GlobalSeconds
|
||||
};
|
||||
|
||||
return seconds > 0 ? TimeSpan.FromSeconds(seconds.Value) : null;
|
||||
}
|
||||
|
||||
private bool ShouldRenewTtl(string key)
|
||||
=> GetEntityTtl(key) != null;
|
||||
|
||||
|
||||
// INITIALIZATION
|
||||
private async Task InitializeRedisConnectionAsync()
|
||||
{
|
||||
_isRedisAvailable = false;
|
||||
try
|
||||
{
|
||||
if (_cacheSettings.Redis.ConnectionString == null)
|
||||
return;
|
||||
|
||||
_connection = await ConnectionMultiplexer.ConnectAsync(_cacheSettings.Redis.ConnectionString);
|
||||
_database = _connection.GetDatabase();
|
||||
_server = _connection.GetServer(_cacheSettings.Redis.ConnectionString);
|
||||
|
||||
_isRedisAvailable = true;
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
_logger.LogError( "Error Initializing Redis Connection. Exception:{ex}", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<T?> GetObjectAsync<T>(string key, TimeSpan? ttlOverride, bool updateExpiration)
|
||||
=> GetObjectAsync<T>(key, updateExpiration);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user