using Microsoft.Extensions.Configuration; using MP.Core.DTO; using StackExchange.Redis; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using ZiggyCreatures.Caching.Fusion; namespace MP.Data.Services.RouteWeight { public class RedisWeightProvider : IWeightProvider { #region Public Constructors public RedisWeightProvider( IConnectionMultiplexer mux, IFusionCache cache, IConfiguration config) { _cache = cache; _config = config; _db = mux.GetDatabase(); _mux = mux; _defaultOld = config.GetValue("RouteMan:DefaultWeightOld", 100); _defaultNew = config.GetValue("RouteMan:DefaultWeightNew", 0); _redisBaseKey = config.GetValue("ServerConf:RedisBaseKey") ?? "MP_IOC"; _keyPrefix = $"{_redisBaseKey}:route_weight:"; } #endregion Public Constructors #region Public Methods /// /// Eliminazione esplicita cache data chiave /// /// public void EvictLocalCacheFor(string method) { if (string.IsNullOrEmpty(method)) return; var cacheKey = $"weights:{method}"; // Rimuove la chiave dalla RAM (L1) dell'istanza corrente del Gateway _cache.Remove(cacheKey); } public async Task> GetAllWeightsAsync() { var result = new List(); var server = _mux.GetServer(_mux.GetEndPoints().First()); if (server.IsReplica) { return result; } await foreach (var key in server.KeysAsync(pattern: $"{_keyPrefix}*")) { var methodName = KeyToString(key.ToString()); if (string.IsNullOrEmpty(methodName)) continue; var oldVal = _db.HashGet(key, "old"); var newVal = _db.HashGet(key, "new"); int oldW = 100; int newW = 0; if (!oldVal.IsNull && int.TryParse(oldVal.ToString(), out var parsedOld)) oldW = Math.Clamp(parsedOld, 0, 100); if (!newVal.IsNull && int.TryParse(newVal.ToString(), out var parsedNew)) newW = Math.Clamp(parsedNew, 0, 100); result.Add(new WeightDTO { Method = methodName, OldWeight = oldW, NewWeight = newW }); } // riordino desc x NEW poi alfabetico... result = result .OrderByDescending(x => x.NewWeight) .ThenBy(x => x.Method) .ToList(); return result; } /// /// Ritorna (oldWeight, newWeight) per il metodo con FusionCache e poi con dati Redis. /// Se non esiste, crea la chiave con i default value. /// public (int oldWeight, int newWeight) GetWeightsFor(string method) { if (string.IsNullOrEmpty(method)) method = "unknown"; var cacheKey = $"weights:{method}"; // FusionCache gestisce L1, L2 e se non trova nulla esegue la factory sotto var weights = _cache.GetOrSet( cacheKey, _ => { // FACTORY DI INIZIALIZZAZIONE (Viene eseguita solo se la cache è vuota ovunque) var redisKey = _keyPrefix + method; var oldVal = _db.HashGet(redisKey, "old"); var newVal = _db.HashGet(redisKey, "new"); if (oldVal.IsNull && newVal.IsNull) { _db.HashSet(redisKey, "old", _defaultOld, When.NotExists); _db.HashSet(redisKey, "new", _defaultNew, When.NotExists); oldVal = _defaultOld; newVal = _defaultNew; } if (oldVal.IsNull) { _db.HashSet(redisKey, "old", _defaultOld, When.NotExists); oldVal = _defaultOld; } if (newVal.IsNull) { _db.HashSet(redisKey, "new", _defaultNew, When.NotExists); newVal = _defaultNew; } if (!int.TryParse(oldVal.ToString(), out var oldW)) oldW = _defaultOld; if (!int.TryParse(newVal.ToString(), out var newW)) newW = _defaultNew; return new RouteWeights(Math.Clamp(oldW, 0, 100), Math.Clamp(newW, 0, 100)); }, // Durata specifica per questa tipologia di dato options => options.SetDuration(TimeSpan.FromMinutes(10)) ); return (weights.OldWeight, weights.NewWeight); #if false var key = _keyPrefix + method; // Leggi entrambi i campi var oldVal = _db.HashGet(key, "old"); var newVal = _db.HashGet(key, "new"); // Se entrambi mancanti, inizializza con default (usando HSet con When.NotExists per evitare overwrite) if (oldVal.IsNull && newVal.IsNull) { // Imposta i campi singolarmente con When.NotExists per evitare overwrite _db.HashSet(key, "old", _defaultOld, When.NotExists); _db.HashSet(key, "new", _defaultNew, When.NotExists); // Rileggi per essere sicuri oldVal = _db.HashGet(key, "old"); newVal = _db.HashGet(key, "new"); } // Se uno dei due manca, impostalo al default (non sovrascrive l'altro) if (oldVal.IsNull) { _db.HashSet(key, "old", _defaultOld, When.NotExists); oldVal = _defaultOld; } if (newVal.IsNull) { _db.HashSet(key, "new", _defaultNew, When.NotExists); newVal = _defaultNew; } if (!int.TryParse(oldVal.ToString(), out var oldW)) oldW = _defaultOld; if (!int.TryParse(newVal.ToString(), out var newW)) newW = _defaultNew; // clamp 0..100 oldW = Math.Clamp(oldW, 0, 100); newW = Math.Clamp(newW, 0, 100); return (oldW, newW); #endif } // API per aggiornare i pesi a runtime (opzionale) public void SetWeights(string method, int oldWeight, int newWeight) { // 1. Scrittura su Redis var key = _keyPrefix + (string.IsNullOrEmpty(method) ? "unknown" : method); _db.HashSet(key, new HashEntry[] { new HashEntry("old", Math.Clamp(oldWeight,0,100)), new HashEntry("new", Math.Clamp(newWeight,0,100)) }); // 2. Rimozione della chiave da FusionCache. // Grazie al Backplane Pub/Sub, la RAM verrà azzerata istantaneamente su TUTTI i server. var cacheKey = $"weights:{method}"; _cache.Remove(cacheKey); } public bool UpsertWeight(WeightDTO updRecord) { if (updRecord == null || string.IsNullOrEmpty(updRecord.Method)) return false; // 1. Scrivi su Redis (Sorgente dati reale) var redisKey = _keyPrefix + updRecord.Method; _db.HashSet(redisKey, new HashEntry[] { new HashEntry("old", Math.Clamp(updRecord.OldWeight, 0, 100)), new HashEntry("new", Math.Clamp(updRecord.NewWeight, 0, 100)) }); // 2. Rimozione della chiave da FusionCache. // Grazie al Backplane Pub/Sub, la RAM verrà azzerata istantaneamente su TUTTI i server. var cacheKey = $"weights:{updRecord.Method}"; _cache.Remove(cacheKey); return true; #if false var key = _keyPrefix + updRecord.Method; _db.HashSet(key, new HashEntry[] { new HashEntry("old", Math.Clamp(updRecord.OldWeight, 0, 100)), new HashEntry("new", Math.Clamp(updRecord.NewWeight, 0, 100)) }); return true; #endif } #endregion Public Methods #region Private Fields private static string _keyPrefix = "route_weight:"; private static string _redisBaseKey = ""; private readonly IFusionCache _cache; /// /// Definizione Record leggero per la cache /// /// /// public record RouteWeights(int OldWeight, int NewWeight); private readonly IConfiguration _config; private readonly IDatabase _db; private readonly int _defaultNew; private readonly int _defaultOld; private readonly IConnectionMultiplexer _mux; #endregion Private Fields #region Private Methods private string KeyToString(string key) { if (string.IsNullOrEmpty(key)) return ""; var prefix = _keyPrefix ?? ""; if (key.StartsWith(prefix)) return key.Substring(prefix.Length); return key; } #endregion Private Methods } }