Aggiunta gestione dati callStat persistenti su Redis

This commit is contained in:
Samuele Locatelli
2026-04-29 10:49:00 +02:00
parent 19dd15c78f
commit 9f22e152e7
9 changed files with 163 additions and 68 deletions
+81 -2
View File
@@ -1,7 +1,11 @@
using System;
using Newtonsoft.Json;
using StackExchange.Redis;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
namespace IOB_UT_NEXT
{
@@ -19,11 +23,46 @@ namespace IOB_UT_NEXT
(_, existing) => new CallStats(existing.Count + 1, existing.TotalElapsed.Add(elapsed)));
}
/// <summary>
/// Carica lo stato persistente da Redis. Thread-safe.
/// </summary>
public static async Task LoadFromRedisAsync(IDatabase redisDb, string redisKey = "app:callmetrics")
{
if (redisDb == null) throw new ArgumentNullException(nameof(redisDb));
;
var json = await redisDb.StringGetAsync(redisKey);
if (!json.HasValue) return;
Dictionary<string, CallStats> parsed;
try
{
parsed = JsonConvert.DeserializeObject<Dictionary<string, CallStats>>(json.ToString());
}
catch (Exception ex)
{
// Log: dati Redis corrotti o schema incompatibile. Si ignora per sicurezza.
return;
}
if (parsed == null || parsed.Count == 0) return;
// 5. Applicazione dati sotto lock
lock (_serializationLock)
{
_metrics.Clear();
foreach (var kvp in parsed)
{
_metrics[kvp.Key] = kvp.Value;
}
}
}
/// <summary>
/// Metodo per estrarre il report Pareto e reset del contatore.
/// </summary>
/// <param name="keys">Se null o vuoto, elabora tutte le chiavi. Altrimenti filtra solo quelle indicate.</param>
public static List<ParetoEntry> LogHourlyPareto(IEnumerable<string> keys = null)
public static List<ParetoEntry> LogPareto(IEnumerable<string> keys = null)
{
List<ParetoEntry> result;
@@ -74,6 +113,43 @@ namespace IOB_UT_NEXT
return result;
}
/// <summary>
/// Salva lo stato corrente in Redis. Thread-safe.
/// </summary>
/// <param name="redisDb">Istanza di IDatabase</param>
/// <param name="redisKey">Chiave Redis dove salvare i dati</param>
/// <param name="clearAfterSave">Se true, resetta i contatori dopo il salvataggio</param>
public static async Task SaveToRedisAsync(IDatabase redisDb, string redisKey = "app:callmetrics", bool clearAfterSave = true)
{
if (redisDb == null) throw new ArgumentNullException(nameof(redisDb));
// 1. Snapshot atomico sotto lock
Dictionary<string, CallStats> snapshot;
lock (_serializationLock)
{
snapshot = _metrics.ToDictionary(
kvp => kvp.Key,
kvp => new CallStats(kvp.Value.Count, kvp.Value.TotalElapsed));
}
if (snapshot.Count == 0) return;
// 2. Serializzazione JSON (fuori dal lock)
var json = JsonConvert.SerializeObject(snapshot);
// 3. Scrittura su Redis (fuori dal lock)
await redisDb.StringSetAsync(redisKey, json);
// 4. Reset opzionale (sotto lock per sicurezza)
if (clearAfterSave)
{
lock (_serializationLock)
{
_metrics.Clear();
}
}
}
#endregion Public Methods
#region Public Classes
@@ -117,6 +193,9 @@ namespace IOB_UT_NEXT
// Accumulatore thread-safe
private static readonly ConcurrentDictionary<string, CallStats> _metrics = new ConcurrentDictionary<string, CallStats>();
// lock x serializzazione su redisDb
private static readonly object _serializationLock = new object();
#endregion Private Fields
#region Private Classes
+3 -3
View File
@@ -878,7 +878,7 @@ namespace IOB_UT_NEXT
#if false
long nCount = currDb.ListLength(queueName);
List<RedisValue> listData = currDb.ListRange(queueName, 0, nCount).ToList();
return listData;
return listData;
#endif
// lettura + reset in blocco
var listData = currDb.ListRange(queueName, 0, -1).ToList();
@@ -912,7 +912,7 @@ namespace IOB_UT_NEXT
long nCount = currDb.ListLength(queueName);
nCount = nCount < maxElem ? nCount : maxElem;
List<RedisValue> listData = currDb.ListRange(queueName, 0, nCount).ToList();
return listData;
return listData;
#endif
// nuovo metodo con rimozione
@@ -1451,7 +1451,7 @@ namespace IOB_UT_NEXT
return eventId;
}
#endregion Public Methods
#endregion Public Methods
#region Protected Fields
+1 -1
View File
@@ -1113,7 +1113,7 @@ namespace IOB_UT_NEXT
if (!LastHourCurr.Equals(HourCurr))
{
// recupero pareto chiamate...
var callList = CallMetricsCollector.LogHourlyPareto();
var callList = CallMetricsCollector.LogPareto();
if (callList != null)
{
StringBuilder sb = new StringBuilder();
+5
View File
@@ -1434,6 +1434,11 @@ namespace IOB_WIN_FORM
displayTaskAndLog("Waiting for config file selection");
}
// ricarica stats da redis
string callKey = iobObj.redisMan.redHash($"IOB:Status:{IOBConfFull.General.FilenameIOB}:CallStats");
await CallMetricsCollector.LoadFromRedisAsync(iobObj.redisMan.currDb, callKey);
// Start timer periodico comunicazione
gather.Interval = IOBConfFull.General.Timers.MsUI;
gather.Enabled = true;
+1
View File
@@ -61,6 +61,7 @@
<HintPath>..\packages\NLog.5.3.4\lib\net46\NLog.dll</HintPath>
</Reference>
<Reference Include="PresentationFramework" />
<Reference Include="StackExchange.Redis, Version=2.0.0.0, Culture=neutral, PublicKeyToken=c219ff1ca8c2ce46" />
<Reference Include="System" />
<Reference Include="System.Buffers, Version=4.0.5.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\System.Buffers.4.6.1\lib\net462\System.Buffers.dll</HintPath>
+5 -7
View File
@@ -201,11 +201,6 @@ namespace IOB_WIN_FORM.Iob
/// </summary>
protected bool hasRecipe = false;
/// <summary>
/// Elenco delle eventuali condizioni di veto conditions attive
/// </summary>
protected List<string> ListVetoCond = new List<string>();
/// <summary>
/// Array dei contatori x segnali blinking
/// </summary>
@@ -266,6 +261,11 @@ namespace IOB_WIN_FORM.Iob
/// </summary>
protected List<objItem> list2Write = new List<objItem>();
/// <summary>
/// Elenco delle eventuali condizioni di veto conditions attive
/// </summary>
protected List<string> ListVetoCond = new List<string>();
/// <summary>
/// Num massimo di errori in funzionalità check alive
/// </summary>
@@ -3434,7 +3434,6 @@ namespace IOB_WIN_FORM.Iob
}
}
/// <summary>
/// Effettua invio al server IO dei dati di IOB + macchina:
/// - dati dell'IOB (IdxMacchina, IobName, IobIp, Tipo, counter...) "classici"
@@ -4708,7 +4707,6 @@ namespace IOB_WIN_FORM.Iob
/// </summary>
private string LastDayCurr = "";
/// <summary>
/// Ultimo invio valore a server
/// </summary>
+56 -51
View File
@@ -100,57 +100,6 @@ namespace IOB_WIN_FORM.Iob
lgError("Error: IobCOnf is null!");
}
}
/// <summary>
/// Verifica veto invio per una chiave specifica in Redis
/// </summary>
/// <param name="keyReq"></param>
/// <returns></returns>
public bool CheckSendVeto(string keyReq, TimeSpan vetoReq)
{
DateTime adesso = DateTime.Now;
DateTime lastSend = LastSendGet(keyReq);
bool sendEnab = lastSend.Add(vetoReq) <= adesso;
return sendEnab;
}
/// <summary>
/// Imposta valore datetime x ultimo invio di una chiamata in REDIS HashSet
/// </summary>
/// <param name="keyReq"></param>
/// <param name="dtRif"></param>
public bool LastSendSet(string keyReq, DateTime dtRif)
{
string lastSendKey = redisMan.redHash($"IOB:Status:{IOBConfFull.General.FilenameIOB}:LastSend");
string rawVal = JsonConvert.SerializeObject(dtRif);
KeyValuePair<string, string>[] hashFields = new KeyValuePair<string, string>[1];
hashFields[0] = new KeyValuePair<string, string>(keyReq, rawVal);
bool fatto = redisMan.redSaveHash(lastSendKey, hashFields);
return fatto;
}
/// <summary>
/// Restitusice valore ultimo invio di una chiave, se non esistesse fornisce valore di 1 gg prima e lo salva...
/// </summary>
/// <param name="keyReq"></param>
/// <returns></returns>
public DateTime LastSendGet(string keyReq)
{
DateTime lastSend = DateTime.Now.AddDays(-1);
string lastSendKey = redisMan.redHash($"IOB:Status:{IOBConfFull.General.FilenameIOB}:LastSend");
string rawVal = redisMan.redGetHashField(lastSendKey, keyReq);
if (!string.IsNullOrEmpty(rawVal))
{
lastSend = JsonConvert.DeserializeObject<DateTime>(rawVal);
}
else
{
rawVal = JsonConvert.SerializeObject(lastSend);
KeyValuePair<string, string>[] hashFields = new KeyValuePair<string, string>[1];
hashFields[0] = new KeyValuePair<string, string>(keyReq, rawVal);
redisMan.redSaveHash(lastSendKey, hashFields);
}
return lastSend;
}
#endregion Public Constructors
@@ -708,6 +657,19 @@ namespace IOB_WIN_FORM.Iob
return currentAnsw;
}
/// <summary>
/// Verifica veto invio per una chiave specifica in Redis
/// </summary>
/// <param name="keyReq"></param>
/// <returns></returns>
public bool CheckSendVeto(string keyReq, TimeSpan vetoReq)
{
DateTime adesso = DateTime.Now;
DateTime lastSend = LastSendGet(keyReq);
bool sendEnab = lastSend.Add(vetoReq) <= adesso;
return sendEnab;
}
/// <summary>
/// Verifica se il server sia ALIVE (tramite PING)
/// </summary>
@@ -2088,6 +2050,45 @@ namespace IOB_WIN_FORM.Iob
return answ;
}
/// <summary>
/// Restitusice valore ultimo invio di una chiave, se non esistesse fornisce valore di 1 gg prima e lo salva...
/// </summary>
/// <param name="keyReq"></param>
/// <returns></returns>
public DateTime LastSendGet(string keyReq)
{
DateTime lastSend = DateTime.Now.AddDays(-1);
string lastSendKey = redisMan.redHash($"IOB:Status:{IOBConfFull.General.FilenameIOB}:LastSend");
string rawVal = redisMan.redGetHashField(lastSendKey, keyReq);
if (!string.IsNullOrEmpty(rawVal))
{
lastSend = JsonConvert.DeserializeObject<DateTime>(rawVal);
}
else
{
rawVal = JsonConvert.SerializeObject(lastSend);
KeyValuePair<string, string>[] hashFields = new KeyValuePair<string, string>[1];
hashFields[0] = new KeyValuePair<string, string>(keyReq, rawVal);
redisMan.redSaveHash(lastSendKey, hashFields);
}
return lastSend;
}
/// <summary>
/// Imposta valore datetime x ultimo invio di una chiamata in REDIS HashSet
/// </summary>
/// <param name="keyReq"></param>
/// <param name="dtRif"></param>
public bool LastSendSet(string keyReq, DateTime dtRif)
{
string lastSendKey = redisMan.redHash($"IOB:Status:{IOBConfFull.General.FilenameIOB}:LastSend");
string rawVal = JsonConvert.SerializeObject(dtRif);
KeyValuePair<string, string>[] hashFields = new KeyValuePair<string, string>[1];
hashFields[0] = new KeyValuePair<string, string>(keyReq, rawVal);
bool fatto = redisMan.redSaveHash(lastSendKey, hashFields);
return fatto;
}
/// <summary>
/// Effettua un trim della stringa al numero max di linee da mostrare a video
/// </summary>
@@ -3770,6 +3771,10 @@ namespace IOB_WIN_FORM.Iob
parentForm.displayTaskAndLog("[STOP] Stopping adapter - last periodic data read...", true);
// salvo statistiche
string callKey = redisMan.redHash($"IOB:Status:{IOBConfFull.General.FilenameIOB}:CallStats");
await CallMetricsCollector.SaveToRedisAsync(redisMan.currDb, callKey, false);
// chiudo la connessione all'adapter...
tryDisconnect();
dtStopAdp = DateTime.Now;
+2 -1
View File
@@ -36,8 +36,9 @@ CLI_INST=SteamWareSim
;STARTLIST=PIZ08
;STARTLIST=iBTB1P
;STARTLIST=3010
STARTLIST=3020
;STARTLIST=PIZ03
;STARTLIST=3020
STARTLIST=SIMUL_01
MAXCNC=10
+9 -3
View File
@@ -242,9 +242,15 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="DATA\CONF\SIEMENS.ini" />
<None Include="DATA\CONF\SIMUL_01.ini" />
<None Include="DATA\CONF\SIMUL_01.json" />
<None Include="DATA\CONF\SIMUL_01_alarm.json" />
<None Include="DATA\CONF\SIMUL_01.ini">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="DATA\CONF\SIMUL_01.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="DATA\CONF\SIMUL_01_alarm.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="DATA\CONF\SIMUL_01_WD.ini" />
<None Include="DATA\CONF\VALV_SAET.ini" />
<None Include="DATA\CONF\VL20.ini" />