Files
lux/EgwCoreLib.Lux.Data/Services/CalcRuidService.cs
T
2025-12-16 17:06:50 +01:00

723 lines
27 KiB
C#

using EgwCoreLib.Lux.Core.Stats;
using EgwCoreLib.Lux.Data.Controllers;
using EgwCoreLib.Lux.Data.DbModel.Stats;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using NLog;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace EgwCoreLib.Lux.Data.Services
{
/// <summary>
/// Gestione servizio indice richieste
/// </summary>
public class CalcRuidService : BaseServ
{
#region Public Constructors
public CalcRuidService(IConfiguration configuration, IConnectionMultiplexer redisConn, string redisBaseKey, TimeSpan retention, TimeSpan archivePeriod) : base(configuration, redisConn)
{
_db = redisConn.GetDatabase();
_base = redisBaseKey.TrimEnd(':');
_retention = retention;
_archivePeriod = archivePeriod;
// conf DB
string connStr = BaseServ._config.GetConnectionString("Lux.All") ?? "";
if (string.IsNullOrEmpty(connStr))
{
Log.Error("ConnString empty!");
}
else
{
//dbController = new Controllers.LuxController(_config);
dbController = new LuxController();
StringBuilder sb = new StringBuilder();
sb.AppendLine($"CalcRuidService | LuxController OK");
Log.Info(sb.ToString());
}
}
#endregion Public Constructors
#region Public Methods
/// <summary>
/// Metodo Creazione nuova richiesta
/// </summary>
/// <param name="envir">Environment calcolo</param>
/// <param name="tipo">Tipologia richiesta</param>
/// <param name="uid">UID di riferimento</param>
/// <returns>restituisce il valore del RUID (ID univoco richiesta)</returns>
public async Task<string> 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;
}
/// <summary>
/// Metodo di Cleanup periodico
/// </summary>
/// <param name="environment">Environment calcolo</param>
/// <param name="tipo">Tipologia richiesta</param>
/// <returns></returns>
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<Task>();
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);
}
/// <summary>
/// Metodo di Cleanup dato periodo limite prima del quale va eliminato l'elenco dei dati
/// </summary>
/// <param name="environment">Environment calcolo</param>
/// <param name="tipo">Tipologia richiesta</param>
/// <param name="timeLimit">Limite temporale dati da eliminare</param>
/// <returns></returns>
public async Task CleanupOldRequestsAsync(string environment, string tipo, TimeSpan timeLimit)
{
var cutoff = DateTimeOffset.UtcNow.Add(-timeLimit).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<Task>();
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);
}
/// <summary>
/// Metodo di Aggiornamento richiesta esistente
/// </summary>
/// <param name="ruid">RUID richiesta</param>
/// <returns></returns>
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
// Aggiorno hash della richiesta
await _db.HashSetAsync(hashKey, new HashEntry[]
{
new HashEntry("processEnd", processEnd),
new HashEntry("processElapsed", elapsed)
});
// Chiave aggregata per slot orario
string dayKey = $"stats:agg:{DateTime.UtcNow:yyyyMMddHH}");
var aggHourKey = Key($"{dayKey}");
await _db.SetAddAsync("stats:agg:index", dayKey);
// --- Aggiornamento globale (ALL|ALL) ---
var globalKey = "ALL|ALL";
// Somma e count
var tasks = new List<Task>
{
_db.HashIncrementAsync(aggHourKey, $"{globalKey}:sum", elapsed, CommandFlags.HighPriority),
_db.HashIncrementAsync(aggHourKey, $"{globalKey}:count", 1, CommandFlags.HighPriority)
};
// Max globale
var currentGlobalMax = await _db.HashGetAsync(aggHourKey, $"{globalKey}:max");
double maxGlobal = currentGlobalMax.IsNull ? 0 : (double)currentGlobalMax;
if (elapsed > maxGlobal)
{
tasks.Add(_db.HashSetAsync(aggHourKey, new HashEntry[]
{
new HashEntry($"{globalKey}:max", elapsed)
}));
}
// --- Aggiornamento per env/tipo ---
var envir = await _db.HashGetAsync(hashKey, "envir");
var tipo = await _db.HashGetAsync(hashKey, "tipo");
if (!envir.IsNull && !tipo.IsNull)
{
var envTipo = $"{envir}|{tipo}";
tasks.Add(_db.HashIncrementAsync(aggHourKey, $"{envTipo}:sum", elapsed, CommandFlags.HighPriority));
tasks.Add(_db.HashIncrementAsync(aggHourKey, $"{envTipo}:count", 1, CommandFlags.HighPriority));
var currentMaxForGroup = await _db.HashGetAsync(aggHourKey, $"{envTipo}:max");
double maxGroup = currentMaxForGroup.IsNull ? 0 : (double)currentMaxForGroup;
if (elapsed > maxGroup)
{
tasks.Add(_db.HashSetAsync(aggHourKey, new HashEntry[]
{
new HashEntry($"{envTipo}:max", elapsed)
}));
}
}
// Attendo completamento di tutti i task
await Task.WhenAll(tasks);
}
/// <summary>
/// Espostazione statistiche da REDIS al DB
/// </summary>
/// <param name="clearRedisAfterExport"></param>
/// <returns></returns>
public async Task ExportStatsToDbAsync(bool clearRedisAfterExport = false)
{
var aggregatedData = new List<StatsAggregated>();
var detailData = new List<StatsDetail>();
// Recupero tutte le chiavi dall'indice (sono RedisValue)
RedisValue[] indexValues = await _db.SetMembersAsync("stats:agg:index");
// Ora corrente (slot da saltare)
string currentHourKey = $"stats:agg:{DateTime.UtcNow:yyyyMMddHH}";
// Lista chiavi da cancellare dopo export (uso RedisValue perché l'indice è un set di valori)
var keysToDelete = new List<RedisValue>();
foreach (RedisValue indexValue in indexValues)
{
string keyStr = indexValue.ToString();
if (keyStr == currentHourKey) continue; // salto slot corrente
// Estraggo l'ora dalla chiave
string hourStr = keyStr.Replace("stats:agg:", "");
if (!DateTime.TryParseExact(hourStr, "yyyyMMddHH", null,
System.Globalization.DateTimeStyles.None, out DateTime hour))
continue;
// Recupero tutti i valori dell'hash (qui serve RedisKey)
HashEntry[] entries = await _db.HashGetAllAsync((RedisKey)keyStr);
// --- Globali ALL|ALL ---
RedisValue sumGlobal = entries.FirstOrDefault(e => e.Name == "ALL|ALL:sum").Value;
RedisValue countGlobal = entries.FirstOrDefault(e => e.Name == "ALL|ALL:count").Value;
RedisValue maxGlobal = entries.FirstOrDefault(e => e.Name == "ALL|ALL:max").Value;
if (!sumGlobal.IsNull && !countGlobal.IsNull)
{
long count = (long)countGlobal;
double sum = (double)sumGlobal;
double max = maxGlobal.IsNull ? 0 : (double)maxGlobal;
aggregatedData.Add(new StatsAggregated
{
Hour = hour,
RequestCount = count,
AvgDuration = count > 0 ? sum / count : 0,
MaxDuration = max
});
}
// --- Dettagli per env|tipo ---
IEnumerable<string> grouped = entries
.Select(e => e.Name.ToString())
.Where(n => !n.StartsWith("ALL|ALL"))
.Select(n => n.Split(':')[0])
.Distinct();
foreach (string envTipo in grouped)
{
string[] envTipoSplit = envTipo.Split('|');
if (envTipoSplit.Length != 2) continue;
string env = envTipoSplit[0];
string tipo = envTipoSplit[1];
RedisValue sumVal = entries.FirstOrDefault(e => e.Name == $"{envTipo}:sum").Value;
RedisValue countVal = entries.FirstOrDefault(e => e.Name == $"{envTipo}:count").Value;
RedisValue maxVal = entries.FirstOrDefault(e => e.Name == $"{envTipo}:max").Value;
if (!sumVal.IsNull && !countVal.IsNull)
{
long count = (long)countVal;
double sum = (double)sumVal;
double max = maxVal.IsNull ? 0 : (double)maxVal;
detailData.Add(new StatsDetail
{
Environment = env,
Type = tipo,
Hour = hour,
RequestCount = count,
AvgDuration = count > 0 ? sum / count : 0,
MaxDuration = max
});
}
}
// Se richiesto, preparo cancellazione
if (clearRedisAfterExport)
{
keysToDelete.Add(indexValue); // RedisValue coerente
}
}
// Scrittura su DB
long aggrResult = await dbController.StatsAggrUpsertAsync(aggregatedData, true);
long detailResult = await dbController.StatsDetailUpsertAsync(detailData, true);
// Se entrambe hanno successo, eseguo batch di cancellazione
if (clearRedisAfterExport && aggrResult > 0 && detailResult > 0 && keysToDelete.Any())
{
var batch = _db.CreateBatch();
foreach (RedisValue indexValue in keysToDelete)
{
string keyStr = indexValue.ToString();
_ = batch.KeyDeleteAsync((RedisKey)keyStr); // cancellazione hash
_ = batch.SetRemoveAsync("stats:agg:index", indexValue); // rimozione dall'indice
}
batch.Execute();
}
}
public async Task<List<StatsDetail>> GetOldStatsFromRedisAsync()
{
var results = new List<StatsDetail>();
var now = DateTime.UtcNow;
var cutoff = now.AddHours(-24);
for (var dt = cutoff.Date; dt < now; dt = dt.AddHours(1))
{
var hourKey = Key($"stats:agg:{dt:yyyyMMddHH}");
var entries = await _db.HashGetAllAsync(hourKey);
if (entries.Length == 0) continue;
foreach (var entry in entries)
{
var key = entry.Name.ToString();
var value = (double)entry.Value;
// Dividi chiave per ottenere envir e tipo
var parts = key.Split(':');
if (parts.Length < 2) continue;
string groupKey = parts[0]; // es: "prod|api"
string metric = parts[1]; // es: "count", "sum", "max"
// Se è una chiave di tipo "count", "sum", o "max", puoi costruire l'oggetto StatsDetail
if (metric == "count")
{
var envTipo = groupKey.Split('|');
var envir = envTipo[0];
var tipo = envTipo[1];
results.Add(new StatsDetail
{
Environment = envir,
Type = tipo,
Hour = dt,
RequestCount = (long)value,
AvgDuration = 0, // Calcolato dopo aggregazione
MaxDuration = 0 // Calcolato dopo aggregazione
});
}
}
}
return results;
}
#if false
public async Task AggregateAndSaveStatsAsync(TimeSpan? timeRange = null, bool deleteFromRedis = false)
{
var now = DateTime.UtcNow;
// Se non è specificato un range, usa il default di 24 ore
var rangeHours = timeRange?.TotalHours ?? 24;
// Calcola l'orario di riferimento (ora intera precedente)
var referenceTime = now.AddMinutes(-now.Minute).AddSeconds(-now.Second);
var cutoffTime = referenceTime.AddHours(-rangeHours);
// Se non si specifica un range, prendiamo le ultime 24 ore
if (!timeRange.HasValue)
{
cutoffTime = referenceTime.AddHours(-24);
}
var aggregatedData = new List<StatsAggregated>();
var detailData = new List<StatsDetail>();
// Per ogni ora nel range da processare
var currentHour = cutoffTime;
var processedKeys = new HashSet<string>(); // Per evitare duplicati
while (currentHour < referenceTime)
{
var hourKey = Key($"stats:agg:{currentHour:yyyyMMddHH}");
// Verifica se la chiave esiste
var exists = await _db.KeyExistsAsync(hourKey);
if (!exists)
{
currentHour = currentHour.AddHours(1);
continue;
}
var entries = await _db.HashGetAllAsync(hourKey);
if (entries.Length == 0)
{
currentHour = currentHour.AddHours(1);
continue;
}
// Processa le voci per questa ora
foreach (var entry in entries)
{
var key = entry.Name.ToString();
var value = (double)entry.Value;
// Dividi chiave per ottenere envir e tipo
var parts = key.Split(':');
if (parts.Length < 2) continue;
string groupKey = parts[0];
string metric = parts[1];
// Se è una chiave di tipo "count", "sum", o "max"
if (metric == "count")
{
var count = (long)value;
// Recupera i valori sum e max per questo gruppo
var sumKey = $"{groupKey}:sum";
var maxKey = $"{groupKey}:max";
var sumValue = await _db.HashGetAsync(hourKey, sumKey);
var maxValue = await _db.HashGetAsync(hourKey, maxKey);
double avg = count > 0 ? (double)sumValue / count : 0;
double max = maxValue.IsNull ? 0 : (double)maxValue;
// Estrai envir e tipo
var envTipo = groupKey.Split('|');
if (envTipo.Length >= 2)
{
var envir = envTipo[0];
var tipo = envTipo[1];
// Aggiungi ai dati aggregati
aggregatedData.Add(new StatsAggregated
{
GroupType = "envir|tipo",
GroupValue = $"{envir}|{tipo}",
Hour = currentHour,
RequestCount = count,
AvgDuration = avg,
MaxDuration = max
});
// Aggiungi ai dati dettagliati
detailData.Add(new StatsDetail
{
Environment = envir,
Type = tipo,
Hour = currentHour,
RequestCount = count,
AvgDuration = avg,
MaxDuration = max
});
}
}
}
// Se richiesto, elimina la chiave da Redis
if (deleteFromRedis)
{
await _db.KeyDeleteAsync(hourKey);
}
currentHour = currentHour.AddHours(1);
}
// Salva i dati sul DB solo se ci sono dati da salvare
if (aggregatedData.Count > 0)
{
await dbController.StatsAggrUpsertAsync(aggregatedData, true);
await dbController.StatsDetailUpsertAsync(detailData, true);
Log.Info($"Salvate {aggregatedData.Count} righe su DB per il periodo da {cutoffTime:yyyy-MM-dd HH:mm} a {referenceTime:yyyy-MM-dd HH:mm}");
}
else
{
Log.Info("Nessun dato da salvare sul DB");
}
}
#endif
/// <summary>
/// Metodo Recupero combinazioni envir/tipo
/// </summary>
/// <returns></returns>
public async Task<IEnumerable<(string env, string tipo)>> GetCombinationsAsync()
{
var members = await _db.SetMembersAsync(GetCombinationsKey());
return members
.Select(x => x.ToString().Split('|'))
.Select(a => (a[0], a[1]));
}
/// <summary>
/// Metodo Recupero richieste per UID
/// </summary>
/// <param name="uid"></param>
/// <returns></returns>
public async Task<IEnumerable<string>> GetRequestsByUidAsync(string uid)
{
var uidKey = GetUidSetKey(uid);
var members = await _db.SetMembersAsync(uidKey);
return members.Select(x => x.ToString());
}
/// <summary>
/// Metodo Statistiche aggregate
/// </summary>
/// <returns></returns>
public async Task<StatsRealtimeDto> 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 StatsRealtimeDto
{
RequestsLastMinute = minCnt,
RequestsLastHour = hourCnt,
ProcessingAvgLastHour = avg,
ProcessingMaxLastHour = maxVal
};
}
/// <summary>
/// Restituisce le statistiche per range date
/// </summary>
/// <param name="from"></param>
/// <param name="to"></param>
/// <returns></returns>
public async Task<StatsRangeDto> GetStatsRangeAsync(DateTime from, DateTime to)
{
var dto = new StatsRangeDto();
var hourKeys = new List<RedisKey>();
var sumKeys = new List<RedisKey>();
var maxKeys = new List<RedisKey>();
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 static Logger Log = LogManager.GetCurrentClassLogger();
private readonly TimeSpan _archivePeriod;
private readonly string _base;
private readonly IDatabase _db;
private readonly TimeSpan _retention;
private readonly Random _rnd = new Random();
#endregion Private Fields
#region Private Properties
private static LuxController dbController { get; set; } = null!;
#endregion Private Properties
#region Private Methods
/// <summary>
/// Generatore RUID:
/// ID incrementale = timestamp ms + random 4 chars HEX
/// </summary>
/// <returns></returns>
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
}
}