using EgwCoreLib.Lux.Core.Stats; using StackExchange.Redis; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace EgwCoreLib.Lux.Data.Services { /// /// Gestione servizio indice richieste /// public class CalcRuidService { #region Public Constructors public CalcRuidService(ConnectionMultiplexer redis, TimeSpan retention, string redisBaseKey) { _db = redis.GetDatabase(); _retention = retention; _base = redisBaseKey.TrimEnd(':'); //_base = redisBaseKey.EndsWith(":") ? redisBaseKey : redisBaseKey + ":"; } #endregion Public Constructors #region Public Methods /// /// Metodo Creazione nuova richiesta /// /// Environment calcolo /// Tipologia richiesta /// UID di riferimento /// restituisce il valore del RUID (ID univoco richiesta) public async Task AddRequestAsync(string envir, string tipo, string uid) { var ruid = GenerateRuid(); var processStart = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); var hashKey = GetRequestKey(ruid); var setKey = GetSortedSetKey(envir, tipo); var uidKey = GetUidSetKey(uid); var combKey = GetCombinationsKey(); var batch = _db.CreateBatch(); // NON await qui! var t1 = batch.HashSetAsync(hashKey, new HashEntry[] { new HashEntry("processStart", processStart), new HashEntry("UID", uid), new HashEntry("tipo", tipo), new HashEntry("envir", envir) }); var t2 = batch.SortedSetAddAsync(setKey, ruid, processStart); var t3 = batch.SetAddAsync(uidKey, ruid); string comb = $"{envir}|{tipo}"; var t4 = batch.SetAddAsync(combKey, comb); RedisKey minuteKey = Key($"stats:requests:count:{DateTime.UtcNow:yyyyMMddHHmm}"); RedisKey hourKey = Key($"stats:requests:count:{DateTime.UtcNow:yyyyMMddHH}"); var t5 = batch.StringIncrementAsync(minuteKey); var t6 = batch.StringIncrementAsync(hourKey); // Esegue il batch batch.Execute(); // Ora puoi attendere le task await Task.WhenAll(t1, t2, t3, t4, t5, t6); return ruid; } /// /// Metodo di Cleanup periodico /// /// Environment calcolo /// Tipologia richiesta /// public async Task CleanupOldRequestsAsync(string environment, string tipo) { var cutoff = DateTimeOffset.UtcNow.Add(-_retention).ToUnixTimeMilliseconds(); var setKey = GetSortedSetKey(environment, tipo); var oldIds = await _db.SortedSetRangeByScoreAsync(setKey, stop: cutoff); if (oldIds.Length == 0) return; var batch = _db.CreateBatch(); var tasks = new List(); foreach (var id in oldIds) { var ruid = id.ToString(); var hashKey = GetRequestKey(ruid); var uid = await _db.HashGetAsync(hashKey, "UID"); if (!uid.IsNull) { var uidKey = GetUidSetKey(uid); tasks.Add(batch.SetRemoveAsync(uidKey, ruid)); tasks.Add(batch.SetLengthAsync(uidKey).ContinueWith(t => { if (t.Result == 0) _db.KeyDelete(uidKey); })); } tasks.Add(batch.KeyDeleteAsync(hashKey)); } tasks.Add(batch.SortedSetRemoveRangeByScoreAsync(setKey, double.NegativeInfinity, cutoff)); tasks.Add(batch.SortedSetLengthAsync(setKey).ContinueWith(t => { if (t.Result == 0) _db.KeyDelete(setKey); })); batch.Execute(); await Task.WhenAll(tasks); } /// /// Metodo di Aggiornamento richiesta esistente /// /// RUID richiesta /// public async Task CompleteRequestAsync(string ruid) { var hashKey = GetRequestKey(ruid); var processEnd = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); // Recupero processStart var processStartValue = await _db.HashGetAsync(hashKey, "processStart"); if (processStartValue.IsNull) return; var processStart = (long)processStartValue; var elapsed = (processEnd - processStart) / 1000.0; // secondi con decimali // Recupero max corrente PRIMA del batch RedisKey hourKeySum = Key($"stats:processing:sum:{DateTime.UtcNow:yyyyMMddHH}"); RedisKey hourKeyMax = Key($"stats:processing:max:{DateTime.UtcNow:yyyyMMddHH}"); var currentMaxValue = await _db.StringGetAsync(hourKeyMax); double currentMax = currentMaxValue.IsNull ? 0 : (double)currentMaxValue; var batch = _db.CreateBatch(); // Aggiorno hash var t1 = batch.HashSetAsync(hashKey, new HashEntry[] { new HashEntry("processEnd", processEnd), new HashEntry("processElapsed", elapsed) }); // Incremento somma var t2 = batch.StringIncrementAsync(hourKeySum, elapsed); // Aggiorno max se necessario Task t3 = Task.CompletedTask; if (elapsed > currentMax) t3 = batch.StringSetAsync(hourKeyMax, elapsed); // Eseguo batch batch.Execute(); // Attendo completamento await Task.WhenAll(t1, t2, t3); } /// /// Metodo Recupero combinazioni envir/tipo /// /// public async Task> GetCombinationsAsync() { var members = await _db.SetMembersAsync(GetCombinationsKey()); return members .Select(x => x.ToString().Split('|')) .Select(a => (a[0], a[1])); } /// /// Metodo Recupero richieste per UID /// /// /// public async Task> GetRequestsByUidAsync(string uid) { var uidKey = GetUidSetKey(uid); var members = await _db.SetMembersAsync(uidKey); return members.Select(x => x.ToString()); } /// /// Metodo Statistiche aggregate /// /// public async Task GetStatsAsync() { RedisKey minuteKey = Key($"stats:requests:count:{DateTime.UtcNow:yyyyMMddHHmm}"); RedisKey hourKey = Key($"stats:requests:count:{DateTime.UtcNow:yyyyMMddHH}"); RedisKey hourSumKey = Key($"stats:processing:sum:{DateTime.UtcNow:yyyyMMddHH}"); RedisKey hourMaxKey = Key($"stats:processing:max:{DateTime.UtcNow:yyyyMMddHH}"); var minuteCount = await _db.StringGetAsync(minuteKey); var hourCount = await _db.StringGetAsync(hourKey); var sum = await _db.StringGetAsync(hourSumKey); var max = await _db.StringGetAsync(hourMaxKey); long minCnt = minuteCount.IsNull ? 0 : (long)minuteCount; long hourCnt = hourCount.IsNull ? 0 : (long)hourCount; double sumVal = sum.IsNull ? 0 : (double)sum; double maxVal = max.IsNull ? 0 : (double)max; double avg = hourCnt > 0 ? sumVal / hourCnt : 0; return new { RequestsLastMinute = minCnt, RequestsLastHour = hourCnt, ProcessingAvgLastHour = avg, ProcessingMaxLastHour = maxVal }; } /// /// Restituisce le statistiche per range date /// /// /// /// public async Task GetStatsRangeAsync(DateTime from, DateTime to) { var dto = new StatsRangeDto(); var hourKeys = new List(); var sumKeys = new List(); var maxKeys = new List(); var cursor = from; while (cursor <= to) { hourKeys.Add(Key($"stats:requests:count:{cursor:yyyyMMddHH}")); sumKeys.Add(Key($"stats:processing:sum:{cursor:yyyyMMddHH}")); maxKeys.Add(Key($"stats:processing:max:{cursor:yyyyMMddHH}")); dto.HourLabels.Add(cursor.ToString("dd/MM HH:mm")); cursor = cursor.AddHours(1); } var batch = _db.CreateBatch(); var hourTasks = hourKeys.Select(k => batch.StringGetAsync(k)).ToArray(); var sumTasks = sumKeys.Select(k => batch.StringGetAsync(k)).ToArray(); var maxTasks = maxKeys.Select(k => batch.StringGetAsync(k)).ToArray(); batch.Execute(); await Task.WhenAll(hourTasks); await Task.WhenAll(sumTasks); await Task.WhenAll(maxTasks); for (int i = 0; i < hourTasks.Length; i++) { long req = hourTasks[i].Result.IsNull ? 0 : (long)hourTasks[i].Result; double sum = sumTasks[i].Result.IsNull ? 0 : (double)sumTasks[i].Result; double max = maxTasks[i].Result.IsNull ? 0 : (double)maxTasks[i].Result; dto.Requests.Add(req); dto.MaxProcessing.Add(max); double avg = req > 0 ? sum / req : 0; dto.AvgProcessing.Add(avg); } return dto; } #endregion Public Methods #region Private Fields private readonly string _base; private readonly IDatabase _db; private readonly TimeSpan _retention; private readonly Random _rnd = new Random(); #endregion Private Fields #region Private Methods /// /// Generatore RUID: /// ID incrementale = timestamp ms + random 4 chars HEX /// /// private string GenerateRuid() { long ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); string rand = Convert.ToString(_rnd.Next(0x1000, 0xFFFF), 16).ToUpper(); return $"{ts}-{rand}"; } private RedisKey GetCombinationsKey() => Key("req:combinations"); private RedisKey GetRequestKey(string ruid) => Key($"request:{ruid}"); private RedisKey GetSortedSetKey(string envir, string tipo) => Key($"requests:{envir}:{tipo}"); private RedisKey GetUidSetKey(string uid) => Key($"uid:{uid}:requests"); private RedisKey Key(string suffix) => (RedisKey)($"{_base}:RUID:{suffix}"); #endregion Private Methods } }