diff --git a/MP-RIOC/MP-RIOC.slnx b/MP-RIOC/MP-RIOC.slnx
index 455ea819..c16a708c 100644
--- a/MP-RIOC/MP-RIOC.slnx
+++ b/MP-RIOC/MP-RIOC.slnx
@@ -1,4 +1,5 @@
+
diff --git a/MP-RIOC/MP.RIOC.csproj b/MP-RIOC/MP.RIOC.csproj
index 0c63a52d..e2cd4295 100644
--- a/MP-RIOC/MP.RIOC.csproj
+++ b/MP-RIOC/MP.RIOC.csproj
@@ -4,14 +4,10 @@
net8.0
enable
enable
- MP_RIOC
+ MP.RIOC
8.16.2605.808
-
-
-
-
@@ -25,10 +21,7 @@
-
-
-
-
+
diff --git a/MP-RIOC/Program.cs b/MP-RIOC/Program.cs
index 573ef479..c532a415 100644
--- a/MP-RIOC/Program.cs
+++ b/MP-RIOC/Program.cs
@@ -1,4 +1,5 @@
using MP.RIOC.Services;
+using MP.RIOC.Services;
using NLog;
using NLog.Web;
using StackExchange.Redis;
@@ -54,6 +55,9 @@ builder.Services.AddSingleton(redisMux);
// Registrazione dei servizi custom
builder.Services.AddSingleton();
builder.Services.AddSingleton();
+builder.Services.AddHostedService();
+builder.Services.AddHostedService();
+builder.Services.AddSingleton();
logger.Info("Standard service configured");
diff --git a/MP-RIOC/RedisScript/RedisUpdateScript_v5.lua b/MP-RIOC/RedisScript/RedisUpdateScript_v5.lua
new file mode 100644
index 00000000..1b4a2477
--- /dev/null
+++ b/MP-RIOC/RedisScript/RedisUpdateScript_v5.lua
@@ -0,0 +1,33 @@
+-- RedisUpdateScript_v5
+local key = KEYS[1]
+local countInc = tonumber(ARGV[1]) or 0
+local totalMsInc = tonumber(ARGV[2]) or 0
+local newMax = tonumber(ARGV[3])
+local newMin = tonumber(ARGV[4])
+local sentinel = tonumber(ARGV[5])
+
+-- Incrementi base
+redis.call('HINCRBY', key, 'count', countInc)
+redis.call('HINCRBYFLOAT', key, 'totalMs', totalMsInc)
+
+-- MAX
+local currentMaxStr = redis.call('HGET', key, 'maxMs')
+local currentMax = tonumber(currentMaxStr)
+
+if newMax ~= nil and newMax < sentinel then
+ if currentMax == nil or newMax > currentMax then
+ redis.call('HSET', key, 'maxMs', newMax)
+ end
+end
+
+-- MIN
+local currentMinStr = redis.call('HGET', key, 'minMs')
+local currentMin = tonumber(currentMinStr)
+
+if newMin ~= nil and newMin < sentinel then
+ if currentMin == nil or newMin < currentMin then
+ redis.call('HSET', key, 'minMs', newMin)
+ end
+end
+
+return 1
diff --git a/MP-RIOC/RedisScript/RedisUpdateScript_v6.lua b/MP-RIOC/RedisScript/RedisUpdateScript_v6.lua
new file mode 100644
index 00000000..230ed7f9
--- /dev/null
+++ b/MP-RIOC/RedisScript/RedisUpdateScript_v6.lua
@@ -0,0 +1,33 @@
+-- RedisUpdateScript_v6
+local key = KEYS[1]
+local countInc = tonumber(ARGV[1]) or 0
+local totalMsInc = tonumber(ARGV[2]) or 0
+local newMax = tonumber(ARGV[3])
+local newMin = tonumber(ARGV[4])
+local sentinel = tonumber(ARGV[5])
+
+-- Incrementi
+redis.call('HINCRBY', key, 'count', countInc)
+redis.call('HINCRBYFLOAT', key, 'totalMs', totalMsInc)
+
+-- MAX
+local currentMaxStr = redis.call('HGET', key, 'maxMs')
+local currentMax = tonumber(currentMaxStr)
+
+if newMax ~= nil and newMax < sentinel then
+ if currentMax == nil or newMax > currentMax then
+ redis.call('HSET', key, 'maxMs', tostring(newMax))
+ end
+end
+
+-- MIN
+local currentMinStr = redis.call('HGET', key, 'minMs')
+local currentMin = tonumber(currentMinStr)
+
+if newMin ~= nil and newMin < sentinel then
+ if currentMin == nil or newMin < currentMin then
+ redis.call('HSET', key, 'minMs', tostring(newMin))
+ end
+end
+
+return 1
diff --git a/MP-RIOC/Services/LuaScriptProvider.cs b/MP-RIOC/Services/LuaScriptProvider.cs
new file mode 100644
index 00000000..41951353
--- /dev/null
+++ b/MP-RIOC/Services/LuaScriptProvider.cs
@@ -0,0 +1,42 @@
+using Microsoft.Extensions.Options;
+using MP.Core.Conf;
+
+namespace MP.RIOC.Services
+{
+ public sealed class LuaScriptProvider
+ {
+ private readonly Dictionary _scripts;
+
+ public IReadOnlyDictionary Scripts => _scripts;
+
+ public LuaScriptProvider(
+ IOptions cfg,
+ IWebHostEnvironment env)
+ {
+ _scripts = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ foreach (var kv in cfg.Value.Scripts)
+ {
+ var name = kv.Key;
+ var relativePath = kv.Value;
+
+ var fullPath = Path.Combine(env.ContentRootPath, relativePath);
+
+ if (!File.Exists(fullPath))
+ throw new FileNotFoundException($"Script Lua non trovato: {fullPath}");
+
+ var content = File.ReadAllText(fullPath);
+
+ _scripts[name] = content;
+ }
+ }
+
+ public string Get(string name)
+ {
+ if (!_scripts.TryGetValue(name, out var script))
+ throw new KeyNotFoundException($"Script Lua '{name}' non trovato.");
+
+ return script;
+ }
+ }
+}
diff --git a/MP-RIOC/Services/MetricsCalcService.cs b/MP-RIOC/Services/MetricsCalcService.cs
new file mode 100644
index 00000000..1accf3a5
--- /dev/null
+++ b/MP-RIOC/Services/MetricsCalcService.cs
@@ -0,0 +1,224 @@
+using MP.RIOC.Services;
+using NLog;
+using StackExchange.Redis;
+using System.Globalization;
+
+namespace MP.RIOC.Services
+{
+ public class MetricsCalcService : BackgroundService
+ {
+ #region Public Constructors
+
+ ///
+ /// Metodo x calcolo metriche/statistiche di esecuzone realtime
+ ///
+ ///
+ ///
+ ///
+ public MetricsCalcService(RouteStatsManager stats,
+ LuaScriptProvider luaProvider,
+ IConfiguration config,
+ IConnectionMultiplexer mux)
+ {
+ _stats = stats;
+ _config = config;
+ _db = mux.GetDatabase();
+ _redisBaseKey = _config.GetValue("ServerConf:RedisBaseKey") ?? "MP_IOC";
+ _updateScript = luaProvider.Get("Update");
+ }
+
+ #endregion Public Constructors
+
+ #region Protected Methods
+
+ ///
+ /// Valore SentinelValue x valore minimo in reids/Lua
+ /// Usiamo un valore molto grande (es. 10 milioni di secondi = ~115 giorni).
+ /// E' impossibile che una singola richiesta HTTP duri cos� tanto, quindi usarlo come "SentinelValue" � sicuro.
+ ///
+ private const string SentinelValue = "999999999";
+
+ ///
+ /// Script update Redis in Lua, recuperato dal provider script.
+ /// Lo script gestisce l'incremento e la logica condizionale per Min/Max in un'unica operazione atomica.
+ ///
+ private readonly string _updateScript;
+
+
+ // Classe di supporto per l'aggregazione locale dei valori Daily
+ private class AggregatedStats
+ {
+ public long Count;
+ public double TotalMs;
+ public double MaxMs;
+ public double MinMs = double.MaxValue;
+ }
+
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ var interval = _config.GetValue("RouteMan:MetricCalcIntervalSeconds", 20);
+
+ while (!stoppingToken.IsCancellationRequested)
+ {
+ try
+ {
+ await Task.Delay(TimeSpan.FromSeconds(interval), stoppingToken);
+ var snapshot = _stats.Snapshot();
+ if (snapshot.Count == 0) continue;
+
+ var adesso = DateTime.Now;
+ var hourStart = new DateTime(adesso.Year, adesso.Month, adesso.Day, adesso.Hour, 0, 0);
+ var dayStart = new DateTime(adesso.Year, adesso.Month, adesso.Day, 0, 0, 0);
+
+ if (_db == null) continue;
+
+ var batch = _db.CreateBatch();
+ var tasks = new List();
+
+ // Dizionario per aggregare i valori Daily prima di inviarli a Redis
+ var dailyAggregates = new Dictionary();
+
+ foreach (var kv in snapshot)
+ {
+ string dest = "IO";
+ string method = "NA";
+ string machineId = "ALL";
+ string rawKey = kv.Key;
+ if (rawKey.Contains("|"))
+ {
+ var splitVal = rawKey.Split("|");
+ dest = splitVal[0];
+ if (splitVal.Length > 1)
+ method = splitVal[1];
+ if (splitVal.Length > 2)
+ machineId = splitVal[2];
+ }
+ else
+ {
+ method = rawKey;
+ }
+
+ var stat = kv.Value;
+ var count = Interlocked.Read(ref stat.Count);
+ var totalMs = stat.TotalDuration.TotalMilliseconds;
+ var maxMs = stat.MaxDuration.TotalMilliseconds;
+ // Se la durata è ancora MaxValue, usiamo la SentinelValue per dire a Redis "non aggiornare"
+ var minMs = (stat.MinDuration == TimeSpan.MaxValue)
+ ? double.Parse(SentinelValue)
+ : stat.MinDuration.TotalMilliseconds;
+
+ // --- LOGICA HOURLY (Per ogni route/method e' una chiave distinta) ---
+ var hourKey = HourBucketKey(dest, method, hourStart);
+ var hoursIndex = HoursIndexKey(dest, method);
+ var hourScore = ToEpochSeconds(hourStart);
+
+ // Usiamo lo script Lua per l'aggiornamento atomico dell'ora
+ tasks.Add(batch.ScriptEvaluateAsync(_updateScript,
+ new RedisKey[] { hourKey },
+ new RedisValue[] {
+ count.ToString(CultureInfo.InvariantCulture),
+ totalMs.ToString(CultureInfo.InvariantCulture),
+ maxMs.ToString(CultureInfo.InvariantCulture),
+ minMs.ToString(CultureInfo.InvariantCulture),
+ SentinelValue
+ }));
+
+ tasks.Add(batch.SortedSetAddAsync(hoursIndex, hourKey, hourScore));
+
+ // --- LOGICA DAILY (Aggregazione locale per evitare sovrascritture nel loop) ---
+ var dayKey = DayBucketKey(dest, machineId, dayStart);
+ var daysIndex = DaysIndexKey(dest, machineId);
+ var dayScore = ToEpochSeconds(dayStart);
+
+ if (!dailyAggregates.TryGetValue(dayKey, out var agg))
+ {
+ agg = new AggregatedStats();
+ dailyAggregates[dayKey] = agg;
+ }
+
+ agg.Count += count;
+ agg.TotalMs += totalMs;
+ agg.MaxMs = Math.Max(agg.MaxMs, maxMs);
+ if (minMs != double.MaxValue)
+ agg.MinMs = Math.Min(agg.MinMs, minMs);
+
+ // Aggiungiamo l'indice al batch
+ tasks.Add(batch.SortedSetAddAsync(daysIndex, dayKey, dayScore));
+ }
+
+ // --- INVIO AGGREGATI DAILY A REDIS ---
+ foreach (var dayKV in dailyAggregates)
+ {
+ var key = dayKV.Key;
+ var agg = dayKV.Value;
+ // Se agg.MinMs è inizializzato a double.MaxValue, lo portiamo alla SentinelValue
+ double finalMin = (agg.MinMs == double.MaxValue || agg.MinMs >= double.Parse(SentinelValue))
+ ? double.Parse(SentinelValue)
+ : agg.MinMs;
+
+ tasks.Add(batch.ScriptEvaluateAsync(_updateScript,
+ new RedisKey[] { key },
+ new RedisValue[] {
+ agg.Count.ToString(CultureInfo.InvariantCulture),
+ agg.TotalMs.ToString(CultureInfo.InvariantCulture),
+ agg.MaxMs.ToString(CultureInfo.InvariantCulture),
+ finalMin.ToString(CultureInfo.InvariantCulture),
+ SentinelValue
+ }));
+ }
+
+ // Esegui tutto il batch in un unico round-trip
+ batch.Execute();
+ await Task.WhenAll(tasks);
+
+ _stats.Clear();
+ }
+ catch (TaskCanceledException) { }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Error flushing metrics");
+ }
+ }
+ }
+ #endregion Protected Methods
+
+ #region Private Fields
+
+ private static string _redisBaseKey = "";
+ private static Logger Log = LogManager.GetCurrentClassLogger();
+ private readonly IConfiguration _config;
+ private readonly IDatabase _db;
+ private readonly RouteStatsManager _stats;
+
+ #endregion Private Fields
+
+ #region Private Methods
+
+ private static string DayBucketKey(string dest, string machId, DateTime dtRif)
+ {
+ return $"{_redisBaseKey}:stats:day:{dest}:{machId}:{dtRif.ToString("yyyyMMdd", CultureInfo.InvariantCulture)}";
+ }
+
+ private static string DaysIndexKey(string dest, string machId)
+ {
+ return $"{_redisBaseKey}:stats:days:{dest}:{machId}";
+ }
+
+ private static string HourBucketKey(string dest, string method, DateTime dtRif)
+ {
+ return $"{_redisBaseKey}:stats:hour:{dest}:{method}:{dtRif.ToString("yyyyMMddHH", CultureInfo.InvariantCulture)}";
+ }
+
+ private static string HoursIndexKey(string dest, string method)
+ {
+ return $"{_redisBaseKey}:stats:hours:{dest}:{method}";
+ }
+
+ private static long ToEpochSeconds(DateTime dt)
+ {
+ return new DateTimeOffset(dt).ToUnixTimeSeconds();
+ }
+
+ #endregion Private Methods
+ }
+}
diff --git a/MP-RIOC/Services/MetricsDbFlushService.cs b/MP-RIOC/Services/MetricsDbFlushService.cs
new file mode 100644
index 00000000..dbbc9eb4
--- /dev/null
+++ b/MP-RIOC/Services/MetricsDbFlushService.cs
@@ -0,0 +1,367 @@
+using MP.Data.DbModels.Utils;
+using MP.Data.Services.Utils;
+using NLog;
+using StackExchange.Redis;
+using System.Globalization;
+
+namespace MP.RIOC.Services
+{
+ public class MetricsDbFlushService : BackgroundService
+ {
+ #region Public Constructors
+
+ public MetricsDbFlushService(
+ IServiceScopeFactory scopeFactory,
+ IConfiguration config,
+ IConnectionMultiplexer mux)
+ {
+ _scopeFactory = scopeFactory;
+ _config = config;
+ _mux = mux;
+ _db = mux.GetDatabase();
+ }
+
+ #endregion Public Constructors
+
+ #region Protected Methods
+
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ var interval = _config.GetValue("RouteMan:MetricFlushIntervalSeconds", 300);
+
+ while (!stoppingToken.IsCancellationRequested)
+ {
+ try
+ {
+ await ProcessDayLiveMetricsAsync();
+ await ProcessHourLiveMetricsAsync();
+ await Task.Delay(TimeSpan.FromSeconds(interval), stoppingToken);
+ }
+ catch (TaskCanceledException) { break; }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Error flushing metrics to database");
+ }
+ }
+ }
+
+ #endregion Protected Methods
+
+ #region Private Fields
+
+ private const double SentinelValue = 999999999;
+ private static readonly Logger Log = LogManager.GetCurrentClassLogger();
+ private readonly IConfiguration _config;
+ private readonly IDatabase _db;
+ private readonly IConnectionMultiplexer _mux;
+ private readonly IServiceScopeFactory _scopeFactory;
+
+ #endregion Private Fields
+
+ #region Private Properties
+
+ private string _redisBaseKey => _config.GetValue("ServerConf:RedisBaseKey") ?? "MP_IOC";
+
+ #endregion Private Properties
+
+ #region Private Methods
+
+ ///
+ /// Processing dati giornalieri (da Redis a DB)
+ ///
+ ///
+ private async Task ProcessDayLiveMetricsAsync()
+ {
+ var aggrRecordsToInsert = new List();
+ var keysToDelete = new List();
+
+ bool deleteConfirmed = _config.GetValue("RouteMan:DeleteExpiredMetrics", false);
+ DateTime now = DateTime.Now;
+
+ // Confini temporali per proteggere i dati in corso
+ DateTime currentDayStart = new DateTime(now.Year, now.Month, now.Day, 0, 0, 0);
+
+ var endpoints = _mux.GetEndPoints();
+
+ foreach (var endpoint in endpoints)
+ {
+ var server = _mux.GetServer(endpoint);
+
+ if (server.IsReplica)
+ {
+ continue;
+ }
+
+ string[] patternsToScan = {
+ $"{_redisBaseKey}:stats:days:*"
+ };
+
+ foreach (var pattern in patternsToScan)
+ {
+ // Nota: KeyScanAsync/KeysAsync e' disponibile su IServer
+ await foreach (var indexKey in server.KeysAsync(pattern: pattern))
+ {
+ if (string.IsNullOrEmpty($"{indexKey}")) continue;
+
+ // CORREZIONE: Utilizzo di SortedSetRangeByRankAsync con range 0 a -1 per prendere tutto il set
+ var memberKeys = await _db.SortedSetRangeByRankAsync(indexKey, 0, -1);
+
+ foreach (var statKey in memberKeys)
+ {
+ var sKey = (RedisKey)$"{statKey}";
+ if (!TryParseKeyMetadata(sKey, out string dest, out string method, out string machId, out DateTime timestamp, out bool isHourType))
+ continue;
+
+ // Verifica se la chiave fosse scaduta rispetto all'orario corrente
+ bool isExpired = isHourType
+ //? timestamp < currentHourStart
+ ? false
+ : timestamp < currentDayStart;
+
+ // Se fosse scaduta e abbiamo il permesso, segnamola per la cancellazione
+ if (isExpired && deleteConfirmed)
+ {
+ keysToDelete.Add(sKey);
+ }
+
+ // Recupero dati dalla Hash
+ var hashData = await _db.HashGetAllAsync(sKey);
+ if (hashData.Length == 0) continue;
+
+ var dict = hashData.ToDictionary(x => x.Name.ToString(), x => x.Value.ToString());
+
+ if (dict.TryGetValue("count", out var countStr) &&
+ dict.TryGetValue("totalMs", out var totalMsStr))
+ {
+ long count = long.Parse(countStr);
+ count = long.Parse(countStr);
+ double totalMs = double.Parse(totalMsStr, CultureInfo.InvariantCulture);
+
+ double maxMs = dict.ContainsKey("maxMs") ? double.Parse(dict["maxMs"], CultureInfo.InvariantCulture) : 0;
+ double minMs = dict.ContainsKey("minMs") ? double.Parse(dict["minMs"], CultureInfo.InvariantCulture) : 0;
+
+ // TUA CORREZIONE: Reset sentinella per evitare valori fuori scala nel DB
+ if (minMs >= SentinelValue) minMs = SentinelValue;
+ if (maxMs >= SentinelValue) maxMs = SentinelValue;
+
+ if (count <= 0) continue;
+
+ aggrRecordsToInsert.Add(new StatsAggregatedModel
+ {
+ Destination = dest,
+ MachineId = machId,
+ Hour = timestamp,
+ RequestCount = count,
+ AvgDuration = totalMs / count,
+ MinDuration = minMs,
+ MaxDuration = maxMs,
+ NoReply = 0
+ });
+ }
+ }
+ }
+ }
+ }
+
+ // --- FASE UPSERT DB ---
+ if (aggrRecordsToInsert.Count > 0)
+ {
+ await using var scope = _scopeFactory.CreateAsyncScope();
+ var aggrService = scope.ServiceProvider.GetRequiredService();
+ await aggrService.UpsertManyAsync(aggrRecordsToInsert, false);
+ Log.Info($"[DAY] Upserted {aggrRecordsToInsert.Count} records to DB");
+ }
+
+ // --- FASE PULIZIA REDIS ---
+ if (deleteConfirmed && keysToDelete.Count > 0)
+ {
+ var batch = _db.CreateBatch();
+ int deletedCount = 0;
+ foreach (var key in keysToDelete)
+ {
+ _ = batch.KeyDeleteAsync(key);
+ deletedCount++;
+ }
+ batch.Execute();
+ Log.Info($"[CLEANUP DAY] Deleted {deletedCount} expired metric keys from Redis");
+ }
+ }
+
+ ///
+ /// Processing dati orari (da Redis a DB)
+ ///
+ ///
+ private async Task ProcessHourLiveMetricsAsync()
+ {
+ var detailRecordsToInsert = new List();
+ var keysToDelete = new List();
+
+ bool deleteConfirmed = _config.GetValue("RouteMan:DeleteExpiredMetrics", false);
+ DateTime now = DateTime.Now;
+
+ // Confini temporali per proteggere i dati in corso
+ DateTime currentHourStart = new DateTime(now.Year, now.Month, now.Day, now.Hour, 0, 0);
+ DateTime currentDayStart = new DateTime(now.Year, now.Month, now.Day, 0, 0, 0);
+
+ var endpoints = _mux.GetEndPoints();
+
+ foreach (var endpoint in endpoints)
+ {
+ var server = _mux.GetServer(endpoint);
+
+ if (server.IsReplica)
+ {
+ continue;
+ }
+
+ string[] patternsToScan = {
+ $"{_redisBaseKey}:stats:hours:*"
+ };
+
+ foreach (var pattern in patternsToScan)
+ {
+ // Nota: KeyScanAsync/KeysAsync e' disponibile su IServer
+ await foreach (var indexKey in server.KeysAsync(pattern: pattern))
+ {
+ if (string.IsNullOrEmpty($"{indexKey}")) continue;
+
+ // CORREZIONE: Utilizzo di SortedSetRangeByRankAsync con range 0 a -1 per prendere tutto il set
+ var memberKeys = await _db.SortedSetRangeByRankAsync(indexKey, 0, -1);
+
+ foreach (var statKey in memberKeys)
+ {
+ var sKey = (RedisKey)$"{statKey}";
+ if (!TryParseKeyMetadata(sKey, out string dest, out string method, out string machId, out DateTime timestamp, out bool isHourType))
+ continue;
+
+ // Verifica se la chiave fosse scaduta rispetto all'orario corrente
+ bool isExpired = isHourType
+ ? timestamp < currentHourStart
+ : false;
+ //: timestamp < currentDayStart;
+
+ // Se fosse scaduta e abbiamo il permesso, segnamola per la cancellazione
+ if (isExpired && deleteConfirmed)
+ {
+ keysToDelete.Add(sKey);
+ }
+
+ // Recupero dati dalla Hash
+ var hashData = await _db.HashGetAllAsync(sKey);
+ if (hashData.Length == 0) continue;
+
+ var dict = hashData.ToDictionary(x => x.Name.ToString(), x => x.Value.ToString());
+
+ if (dict.TryGetValue("count", out var countStr) &&
+ dict.TryGetValue("totalMs", out var totalMsStr))
+ {
+ long count = long.Parse(countStr);
+ count = long.Parse(countStr);
+ double totalMs = double.Parse(totalMsStr, CultureInfo.InvariantCulture);
+
+ double maxMs = dict.ContainsKey("maxMs") ? double.Parse(dict["maxMs"], CultureInfo.InvariantCulture) : 0;
+ double minMs = dict.ContainsKey("minMs") ? double.Parse(dict["minMs"], CultureInfo.InvariantCulture) : 0;
+
+ // TUA CORREZIONE: Reset sentinella per evitare valori fuori scala nel DB
+ if (minMs >= SentinelValue) minMs = SentinelValue;
+ if (maxMs >= SentinelValue) maxMs = SentinelValue;
+
+ if (count <= 0) continue;
+
+ detailRecordsToInsert.Add(new StatsDetailModel
+ {
+ Destination = dest,
+ Type = method,
+ Hour = timestamp,
+ RequestCount = count,
+ AvgDuration = totalMs / count,
+ MinDuration = minMs,
+ MaxDuration = maxMs,
+ NoReply = 0
+ });
+ }
+ }
+ }
+ }
+ }
+
+ // --- FASE UPSERT DB ---
+ if (detailRecordsToInsert.Count > 0)
+ {
+ await using var scope = _scopeFactory.CreateAsyncScope();
+ var detailService = scope.ServiceProvider.GetRequiredService();
+ await detailService.UpsertManyAsync(detailRecordsToInsert, false);
+ Log.Info($"[HOUR] Upserted {detailRecordsToInsert.Count} records to DB");
+ }
+
+ // --- FASE PULIZIA REDIS ---
+ if (deleteConfirmed && keysToDelete.Count > 0)
+ {
+ var batch = _db.CreateBatch();
+ int deletedCount = 0;
+ foreach (var key in keysToDelete)
+ {
+ _ = batch.KeyDeleteAsync(key);
+ deletedCount++;
+ }
+ batch.Execute();
+ Log.Info($"[CLEANUP HOUR] Deleted {deletedCount} expired metric keys from Redis");
+ }
+ }
+
+ private bool TryParseKeyMetadata(RedisKey key, out string dest, out string method, out string machId, out DateTime timestamp, out bool isHourType)
+ {
+ dest = "NA";
+ method = "NA";
+ machId = "ALL";
+ timestamp = DateTime.MinValue;
+ isHourType = true;
+ try
+ {
+ string k = key.ToString();
+ string relativeKey = k.Replace($"{_redisBaseKey}:", "");
+ var parts = relativeKey.Split(':');
+
+ if (parts.Length < 4) return false;
+
+ string type = parts[1]; // "hour" o "day"
+ dest = parts[2];
+
+ if (type == "hour")
+ {
+ isHourType = true;
+ method = parts[3];
+ if (parts.Length >= 5 && DateTime.TryParseExact(parts[4], "yyyyMMddHH", CultureInfo.InvariantCulture, DateTimeStyles.None, out var dt))
+ {
+ timestamp = dt;
+ return true;
+ }
+ }
+ else if (type == "day")
+ {
+ isHourType = false;
+ method = "DAILY";
+ string rawDate = "";
+ if (parts.Length >= 5)
+ {
+ machId = parts[3];
+ rawDate = parts[4];
+ }
+ else
+ {
+ rawDate = parts[3];
+ }
+ if (DateTime.TryParseExact(rawDate, "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var dt))
+ {
+ timestamp = dt;
+ return true;
+ }
+ }
+ }
+ catch { }
+ return false;
+ }
+
+ #endregion Private Methods
+ }
+}
diff --git a/MP-RIOC/appsettings.json b/MP-RIOC/appsettings.json
index 4583eef3..0f17cadb 100644
--- a/MP-RIOC/appsettings.json
+++ b/MP-RIOC/appsettings.json
@@ -72,7 +72,6 @@
"useFactory": true
},
"ConnectionStrings": {
- "MP.Utils": "Server=SQL2016DEV;Database=MoonPro_Utils; User ID=sa;Password=keyhammer16; integrated security=False; App=MP.IOC;",
"Redis": "redis.ufficio:26379,serviceName=devel,DefaultDatabase=5,connectTimeout=5000,syncTimeout=5000,asyncTimeout=5000,abortConnect=false,ssl=false",
"RedisAdmin": "redis.ufficio:26379,serviceName=devel,DefaultDatabase=5,connectTimeout=5000,syncTimeout=5000,asyncTimeout=5000,abortConnect=false,ssl=false,allowAdmin=true"
}
diff --git a/MP-RIOC/dotnet-tools.json b/MP-RIOC/dotnet-tools.json
new file mode 100644
index 00000000..ce0aab73
--- /dev/null
+++ b/MP-RIOC/dotnet-tools.json
@@ -0,0 +1,13 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "dotnet-ef": {
+ "version": "10.0.7",
+ "commands": [
+ "dotnet-ef"
+ ],
+ "rollForward": false
+ }
+ }
+}
\ No newline at end of file
diff --git a/MP.IOC/MP.IOC.csproj b/MP.IOC/MP.IOC.csproj
index d1c88af3..cc421694 100644
--- a/MP.IOC/MP.IOC.csproj
+++ b/MP.IOC/MP.IOC.csproj
@@ -4,7 +4,7 @@
net8.0
enable
enable
- 8.16.2605.611
+ 8.16.2605.808
diff --git a/MP.IOC/Resources/ChangeLog.html b/MP.IOC/Resources/ChangeLog.html
index f22519c1..c2c936a0 100644
--- a/MP.IOC/Resources/ChangeLog.html
+++ b/MP.IOC/Resources/ChangeLog.html
@@ -1,6 +1,6 @@
Modulo MP-IOC
- Versione: 8.16.2605.611
+ Versione: 8.16.2605.808
Note di rilascio:
-
diff --git a/MP.IOC/Resources/VersNum.txt b/MP.IOC/Resources/VersNum.txt
index a09c6432..a37f9c6b 100644
--- a/MP.IOC/Resources/VersNum.txt
+++ b/MP.IOC/Resources/VersNum.txt
@@ -1 +1 @@
-8.16.2605.611
+8.16.2605.808
diff --git a/MP.IOC/Resources/manifest.xml b/MP.IOC/Resources/manifest.xml
index 08c9065b..2f8a10b2 100644
--- a/MP.IOC/Resources/manifest.xml
+++ b/MP.IOC/Resources/manifest.xml
@@ -1,6 +1,6 @@
-
- 8.16.2605.611
+ 8.16.2605.808
https://nexus.steamware.net/repository/SWS/MP-IOC/stable/LAST/MP.IOC.zip
https://nexus.steamware.net/repository/SWS/MP-IOC/stable/LAST/ChangeLog.html
false