Files
mapo-core/MP.SPEC/Data/MpDataService.cs
T
2026-05-29 11:43:52 +02:00

2671 lines
108 KiB
C#

using EgwCoreLib.Utils;
using Microsoft.EntityFrameworkCore;
using MP.Core.Conf;
using MP.Core.DTO;
using MP.Core.Objects;
using MP.Data;
using MP.Data.Controllers;
using MP.Data.DbModels;
using MP.Data.MgModels;
using MP.Data.Services;
using Newtonsoft.Json;
using NLog;
using StackExchange.Redis;
using System.Data;
using System.Diagnostics;
using ZiggyCreatures.Caching.Fusion;
namespace MP.SPEC.Data
{
public class MpDataService : IDisposable
{
#region Public Constructors
public MpDataService(IConfiguration configuration, IFusionCache cache)
{
// salvataggio oggetti
_configuration = configuration;
_cache = cache;
// Verifica conf trace...
traceEnabled = _configuration.GetValue<bool>("Otel:EnableTracing", false);
slowLogThresh = _configuration.GetValue<double>("ServerConf:slowLogThresh", 1);
Log.Info($"MpDataService | INIT | Trace enabled: {traceEnabled}");
// setup compoenti REDIS
redisConn = ConnectionMultiplexer.Connect(_configuration.GetConnectionString("Redis") ?? "localhost:6379");
redisConnAdmin = ConnectionMultiplexer.Connect(_configuration.GetConnectionString("RedisAdmin") ?? "localhost:6379");
redisDb = redisConn.GetDatabase();
// leggo cache lungo/cordo periodo
int.TryParse(_configuration.GetValue<string>("ServerConf:redisShortTimeCache"), out redisShortTimeCache);
int.TryParse(_configuration.GetValue<string>("ServerConf:redisLongTimeCache"), out redisLongTimeCache);
// setup MsgPipe
BroadastMsgPipe = new MessagePipe(redisConn, Constants.BROADCAST_M_PIPE);
Log.Info("MpDataService | Redis OK");
// conf DB
string connStr = _configuration.GetConnectionString("MP.Data") ?? "";
if (string.IsNullOrEmpty(connStr))
{
Log.Error("DbController: ConnString empty!");
}
else
{
dbController = new MpSpecController(configuration);
Log.Info("DbController OK");
}
// conf x lettura dati da area REDIS di MP-IO
MpIoNS = _configuration.GetValue<string>("ServerConf:MpIoNS") ?? "";
// conf mongo...
connStr = _configuration.GetConnectionString("mdbConnString") ?? "";
if (string.IsNullOrEmpty(connStr))
{
Log.Error("MongoController: ConnString empty!");
}
else
{
mongoController = new MpMongoController(configuration);
Log.Info("MongoController OK");
}
Log.Info("MpDataService | INIT completed");
}
#endregion Public Constructors
#region Public Events
/// <summary>
/// Evento richiesta rilettura dati pagina (x refresh pagine aperte)
/// </summary>
public event EventHandler ReloadRequest = delegate { };
#endregion Public Events
#region Public Properties
public static MpSpecController dbController { get; set; } = null!;
public static MpMongoController mongoController { get; set; } = null!;
public MessagePipe BroadastMsgPipe { get; set; } = null!;
public Dictionary<string, List<TagData>> currTagConf { get; set; } = new Dictionary<string, List<TagData>>();
/// <summary>
/// Expiry DateTime x refresh pagina parametri
/// </summary>
public DateTime DtParamExpiry
{
get => _dtParamExpiry;
set => _dtParamExpiry = value;
}
#endregion Public Properties
#region Public Methods
/// <summary>
/// Recupera eventuali azioni richieste
/// </summary>
/// <returns></returns>
public async Task<DisplayAction> ActionGetReq()
{
using var activity = ActivitySource.StartActivity("ActionGetReq");
string source = "REDIS";
DisplayAction? result = null;
// cerco in redis...
RedisValue rawData = await redisDb.StringGetAsync(Utils.redisActionReq);
if (!string.IsNullOrEmpty($"{rawData}"))
{
result = JsonConvert.DeserializeObject<DisplayAction>($"{rawData}");
}
if (result == null)
{
result = new DisplayAction();
}
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"ActionGetReq Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return result;
}
/// <summary>
/// Salva richiesta azione
/// </summary>
/// <param name="act2save"></param>
/// <returns></returns>
public bool ActionSetReq(DisplayAction? act2save)
{
using var activity = ActivitySource.StartActivity("ActionSetReq");
string source = "REDIS";
bool fatto = false;
// cerco in redis...
string rawData = JsonConvert.SerializeObject(act2save);
// invio broadcast + salvo in redis
BroadastMsgPipe.saveAndSendMessage(Utils.redisActionReq, rawData);
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"ActionSetReq {source} send to broadcast + Write cache: {activity?.Duration.TotalMilliseconds}ms");
return fatto;
}
/// <summary>
/// Stacca un nuovo counter x il tipo richiesto
/// </summary>
/// <returns></returns>
public AnagCountersModel AnagCountersGetNext(string cntType)
{
using var activity = ActivitySource.StartActivity("AnagCountersGetNext");
AnagCountersModel result = new AnagCountersModel();
string source = "DB";
result = dbController.AnagCountersGetNext(cntType);
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"AnagCountersGetNext | {source} | {activity?.Duration.TotalMilliseconds}ms");
return result;
}
/// <summary>
/// Elenco EVENTI validi x ogni macchina secondo conf standard macchina
/// </summary>
/// <returns></returns>
public async Task<List<vSelEventiBCodeModel>> AnagEventiGeneralAsync()
{
return await GetOrFetchAsync(
operationName: "AnagEventiGeneralAsync",
cacheKey: $"{Utils.redisEventList}:VSEB:GENERAL",
expiration: getRandTOut(redisLongTimeCache),
fetchFunc: async () =>
{
return await dbController.AnagEventiGeneralAsync() ?? new List<vSelEventiBCodeModel>();
},
tagList: [Utils.redisEventList]
);
}
/// <summary>
/// Delete record AnagraficaGruppi
/// </summary>
/// <returns></returns>
public bool AnagGruppiDelete(AnagGruppiModel updRec)
{
using var activity = ActivitySource.StartActivity("AnagGruppiDelete");
bool result = false;
result = dbController.AnagGruppiDelete(updRec);
// elimino cache redis...
string pattern = $"{Utils.redisAnagGruppi}:*";
bool answ = ExecFlushRedisPattern(pattern);
activity?.SetTag("data.source", "DB+REDIS");
activity?.Stop();
LogTrace($"AnagGruppiDelete | CodGruppo {updRec.CodGruppo} | {activity?.Duration.TotalMilliseconds}ms");
return result;
}
/// <summary>
/// Upsert record AnagraficaGruppi
/// </summary>
/// <param name="UpdRec"></param>
/// <returns></returns>
public bool AnagGruppiUpsert(AnagGruppiModel UpdRec)
{
using var activity = ActivitySource.StartActivity("AnagGruppiUpsert");
bool result = false;
result = dbController.AnagGruppiUpsert(UpdRec);
// elimino cache redis...
string pattern = $"{Utils.redisAnagGruppi}:*";
bool answ = ExecFlushRedisPattern(pattern);
activity?.SetTag("data.source", "DB+REDIS");
activity?.Stop();
LogTrace($"AnagGruppiUpsert | CodGruppo {UpdRec.CodGruppo} | {activity?.Duration.TotalMilliseconds}ms");
return result;
}
public async Task<List<ListValuesModel>> AnagStatiCommAsync()
{
return await GetOrFetchAsync(
operationName: "AnagStatiCommAsync",
cacheKey: Utils.redisStatoCom,
expiration: getRandTOut(redisLongTimeCache),
fetchFunc: async () =>
await dbController.AnagStatiCommAsync() ?? new List<ListValuesModel>(),
tagList: [Utils.redisStatoCom]
);
}
/// <summary>
/// Restituisce elenco tipi articolo livello anagrafica
/// </summary>
/// <returns></returns>
public async Task<List<ListValuesModel>> AnagTipoArtLvAsync()
{
return await GetOrFetchAsync(
operationName: "AnagTipoArtLvAsync",
cacheKey: Utils.redisTipoArt,
expiration: getRandTOut(redisLongTimeCache),
fetchFunc: async () => await dbController.AnagTipoArtLvAsync() ?? new List<ListValuesModel>(),
tagList: [Utils.redisTipoArt]
);
}
/// <summary>
/// Elenco Codice articolo con dati dossier gestiti
/// </summary>
/// <returns></returns>
public async Task<List<string>> ArticleWithDossierAsync()
{
return await GetOrFetchAsync(
operationName: "ArticleWithDossierAsync",
cacheKey: Utils.redisArtByDossier,
expiration: getRandTOut(redisLongTimeCache),
fetchFunc: async () => await Task.FromResult(dbController.ArticleWithDossier()) ?? new List<string>(),
tagList: [Utils.redisArtByDossier]
);
}
public async Task<int> ArticoliCountAsync()
{
string redisKey = $"{Utils.redisArtList}:Count";
return await GetOrFetchAsync(
operationName: "ArticoliCountAsync",
cacheKey: redisKey,
expiration: getRandTOut(redisLongTimeCache),
fetchFunc: async () =>
await dbController.ArticoliCountAsync(),
tagList: [Utils.redisArtList, $"{Utils.redisArtList}:CountAll"]
);
}
public async Task<int> ArticoliCountSearchAsync(string tipo = "*", string azienda = "*", string searchVal = "")
{
string sKey = string.IsNullOrWhiteSpace(tipo) ? "ALL" : tipo.Trim();
string redisKey = $"{Utils.redisArtList}:{azienda}:{sKey}:{searchVal}:Count";
return await GetOrFetchAsync(
operationName: "ArticoliCountSearchAsync",
cacheKey: redisKey,
expiration: getRandTOut(redisLongTimeCache),
fetchFunc: async () =>
await dbController.ArticoliCountSearchAsync(tipo, azienda, searchVal),
tagList: [Utils.redisArtList, $"{Utils.redisArtList}:CountSearch"]
);
}
/// <summary>
/// Eliminazione record selezionato
/// </summary>
/// <param name="currRec"></param>
/// <returns></returns>
public async Task<bool> ArticoliDeleteRecord(AnagArticoliModel currRec)
{
using var activity = ActivitySource.StartActivity("ArticoliDeleteRecord");
string source = "DB+REDIS";
bool fatto = await dbController.ArticoliDeleteRecord(currRec);
await resetCacheArticoli();
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"ArticoliDeleteRecord | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return fatto;
}
/// <summary>
/// Restitusice elenco articoli dato tipo (es KIT)
/// </summary>
/// <param name="tipo"></param>
/// <param name="azienda"></param>
/// <returns></returns>
public async Task<List<AnagArticoliModel>> ArticoliGetByTipoAsync(string tipo, string azienda = "*")
{
string sKey = string.IsNullOrWhiteSpace(tipo) ? "ALL" : tipo.Trim();
string redisKey = $"{Utils.redisArtList}:{azienda}:{sKey}";
return await GetOrFetchAsync(
operationName: "ArticoliGetByTipoAsync",
cacheKey: redisKey,
expiration: getRandTOut(redisLongTimeCache),
fetchFunc: async () =>
await dbController.ArticoliGetByTipoAsync(tipo, azienda) ?? new List<AnagArticoliModel>(),
tagList: [Utils.redisArtList, $"{Utils.redisArtList}:Tipo"]
);
}
/// <summary>
/// Restitusice elenco articoli cercati
/// </summary>
/// <param name="numRecord"></param>
/// <param name="searchVal"></param>
/// <returns></returns>
public async Task<List<AnagArticoliModel>> ArticoliGetSearchAsync(int numRecord, string tipoArt, string azienda, string searchVal)
{
string sKey = string.IsNullOrWhiteSpace(searchVal) ? "***" : searchVal.Trim();
string redisKey = $"{Utils.redisArtList}:{tipoArt}:{azienda}:{sKey}:{numRecord}";
return await GetOrFetchAsync(
operationName: "ArticoliGetSearchAsync",
cacheKey: redisKey,
expiration: getRandTOut(redisLongTimeCache),
fetchFunc: async () =>
await dbController.ArticoliGetSearchAsync(numRecord, tipoArt, azienda, searchVal) ?? new List<AnagArticoliModel>(),
tagList: [Utils.redisArtList, $"{Utils.redisArtList}:Search"]
);
}
/// <summary>
/// Elenco articoli contenuti in Kit (come child), non eliminabli
/// </summary>
/// <returns></returns>
public async Task<List<AnagArticoliModel>> ArticoliInKitAsync()
{
string redisKey = $"{Utils.redisArtList}:InKit";
return await GetOrFetchAsync(
operationName: "ArticoliInKitAsync",
cacheKey: redisKey,
expiration: getRandTOut(redisLongTimeCache),
fetchFunc: async () =>
await dbController.ArticoliInKitAsync() ?? new List<AnagArticoliModel>(),
tagList: [Utils.redisArtList, $"{Utils.redisArtList}:InKit"]
);
}
/// <summary>
/// Aggiornamento record selezionato
/// </summary>
/// <param name="currRec"></param>
/// <returns></returns>
public async Task<bool> ArticoliUpdateRecord(AnagArticoliModel currRec)
{
using var activity = ActivitySource.StartActivity("ArticoliUpdateRecord");
string source = "DB+REDIS";
bool fatto = await dbController.ArticoliUpdateRecord(currRec);
await resetCacheArticoli();
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"ArticoliUpdateRecord | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return fatto;
}
/// <summary>
/// Verifica se sia possiubile cancellare articolo dato suo CodArt cercando su redis o su
/// tab veto da DB
/// </summary>
/// <param name="CodArt"></param>
/// <returns></returns>
public bool ArticoloDelEnabled(object CodArt)
{
using var activity = ActivitySource.StartActivity("ArticoloDelEnabled");
string codArticolo = $"{CodArt}";
int numUsed = _listCodArtUsed.Count;
int numUnused = _listCodArtNotUsed.Count;
bool usato = true;
string source = "MEMORY";
// 1. Controllo immediato sulla cache locale (HashSet) x eventuale refresh
if (DateTime.Now >= _artCacheExpiry || (numUsed + numUnused) <= 0)
{
source = "DB/REDIS";
// Fallback sincrono minimo per non rompere il componente Blazor
// Nota: Questo è un workaround per la firma sincrona.
var task = EnsureArtCacheLoadedAsync(false);
task.Wait();
// rileggo
numUsed = _listCodArtUsed.Count;
numUnused = _listCodArtNotUsed.Count;
}
// verifico quale sia l'elenco
if (numUsed > 0)
{
usato = _listCodArtUsed.Contains(codArticolo);
}
else
{
usato = !_listCodArtNotUsed.Contains(codArticolo);
}
// verifico infine anche che NON sia nell'elenco degli articoli in KIT
if (!usato)
{
usato = _listCodArtInKit.Contains(CodArt);
}
activity?.SetTag("data.source", source);
activity?.Stop();
if (activity?.Duration.TotalMilliseconds > slowLogThresh)
{
LogTrace($"ArticoloDelEnabled | Cod: {codArticolo} | {source} | {activity?.Duration.TotalMilliseconds}ms");
}
return !usato;
}
public string CalcRecipe(RecipeModel currRecipe)
{
using var activity = ActivitySource.StartActivity("CalcRecipe");
var result = mongoController.CalcRecipe(currRecipe);
activity?.SetTag("data.source", "MONGO");
return result;
}
/// <summary>
/// Recupero tab config in modalità Asincrona
/// </summary>
/// <returns></returns>
public async Task<List<ConfigModel>> ConfigGetAllAsync()
{
return await GetOrFetchAsync(
operationName: "ConfigGetAllAsync",
cacheKey: Utils.redisConfAll,
expiration: getRandTOut(redisLongTimeCache * 2),
fetchFunc: async () => await dbController.ConfigGetAllAsync() ?? new List<ConfigModel>(),
tagList: [Utils.redisConfAll]
);
}
/// <summary>
/// Reset dati cache config
/// </summary>
/// <returns></returns>
public async Task ConfigResetCacheAsync()
{
using var activity = ActivitySource.StartActivity("ConfigResetCacheAsync");
string source = "REDIS";
await ResetConfigCache();
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"ConfigResetCacheAsync Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
}
/// <summary>
/// Restituisce valore della stringa (SE disponibile) - modalità async
/// </summary>
/// <param name="keyName"></param>
/// <returns></returns>
public async Task<string> ConfigTryGetAsync(string keyName)
{
using var activity = ActivitySource.StartActivity("ConfigTryGetAsync");
string source = "MEMORY";
await EnsureConfigLoadedAsync();
_configData.TryGetValue(keyName, out var value);
activity?.SetTag("data.source", source);
activity?.Stop();
if (activity?.Duration.TotalMilliseconds > slowLogThresh)
{
LogTrace($"ConfigTryGetAsync | {keyName} | {source} | {activity?.Duration.TotalMilliseconds}ms");
}
return value ?? "";
}
/// <summary>
/// Update chiave config
/// </summary>
/// <returns></returns>
public async Task<bool> ConfigUpdateAsync(ConfigModel updRec)
{
using var activity = ActivitySource.StartActivity("ConfigUpdateAsync");
string source = "DB";
var updRes = await dbController.ConfigUpdateAsync(updRec);
await ResetConfigCache();
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"ConfigUpdateAsync Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return updRes;
}
/// <summary>
/// Restituisce le statistiche di DB maintenance eseguite
/// </summary>
/// <returns></returns>
public Dictionary<DateTime, double> DbDedupStats()
{
using var activity = ActivitySource.StartActivity("DbDedupStats");
string source = "REDIS";
Dictionary<DateTime, double> actStats = new Dictionary<DateTime, double>();
string currKey = $"{Utils.redisStatsDbMaint}";
// recupero i record statistiche correnti
RedisValue rawData = redisDb.StringGet(currKey);
if (rawData.HasValue)
{
var rawStats = JsonConvert.DeserializeObject<Dictionary<DateTime, double>>($"{rawData}");
if (rawStats != null)
{
actStats = rawStats;
}
}
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"DbDedupStats Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return actStats;
}
/// <summary>
/// Dispose del connettore ai dati
/// </summary>
public void Dispose()
{
// Clear database controller
mongoController.Dispose();
redisConn.Dispose();
}
/// <summary>
/// Eliminazione di un dossier
/// </summary>
/// <param name="selRecord">record dossier da eliminare</param>
/// <returns></returns>
public async Task<bool> DossiersDeleteRecord(DossierModel selRecord)
{
using var activity = ActivitySource.StartActivity("DossiersDeleteRecord");
bool result = false;
result = await dbController.DossiersDeleteRecord(selRecord);
// elimino cache redis...
RedisValue pattern = new RedisValue($"{Utils.redisDossByMac}:*");
bool answ = await ExecFlushRedisPatternAsync(pattern);
activity?.SetTag("data.source", "DB+REDIS");
activity?.Stop();
LogTrace($"DossiersDeleteRecord | IdxMacchina {selRecord.IdxMacchina} | DtRif {selRecord.DtRif} | IdxODL {selRecord.IdxODL} | {activity?.Duration.TotalMilliseconds}ms");
return result;
}
/// <summary>
/// Elenco ultimi n record DOssiers (che contengono ad esempio "salvataggi" di FLuxLog) dato
/// idxMaccSel (ordinato x data registrazione)
/// </summary>
/// <param name="IdxMacchina">* = tutte, altrimenti solo x una data idxMaccSel</param>
/// <param name="DtStart">Data minima per estrazione records</param>
/// <param name="DtEnd">Data Massima per estrazione records</param>
/// <param name="MaxRec">Num Max records da recuperare</param>
/// <returns></returns>
public async Task<List<DossierModel>> DossiersGetLastFiltAsync(string IdxMacchina, string CodArticolo, DateTime DtStart, DateTime DtEnd, int MaxRec)
{
string currKey = $"{Utils.redisDossByMac}:{IdxMacchina}:{CodArticolo}:{DtStart:yyyyMMddHHmm}:{DtEnd:yyyyMMddHHmm}:{MaxRec}";
return await GetOrFetchAsync(
operationName: "DossiersGetLastFiltAsync",
cacheKey: currKey,
expiration: getRandTOut(redisLongTimeCache * 5),
fetchFunc: async () => await dbController.DossiersGetLastFiltAsync(IdxMacchina, CodArticolo, DtStart, DtEnd, MaxRec) ?? new List<DossierModel>(),
tagList: [Utils.redisDossByMac]
);
}
/// <summary>
/// Inserimento nuovo record dossier
/// </summary>
/// <param name="currDoss"></param>
/// <returns></returns>
public async Task<bool> DossiersInsert(DossierModel currDoss)
{
using var activity = ActivitySource.StartActivity("DossiersInsert");
string source = "DB";
// aggiorno record sul DB
bool answ = await dbController.DossiersInsert(currDoss);
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"DossiersInsert | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return answ;
}
/// <summary>
/// Effettua salvataggio snapshot parametri (con stored) + svuota eventuale cache redis
/// </summary>
/// <param name="IdxMacchina">idxMaccSel</param>
/// <param name="MaxSec">NUm massimo secondi per recuperare dati correnti</param>
/// <param name="DtRif">DataOra riferimento x cui prendere valori antecedenti</param>
/// <returns></returns>
public async Task<bool> DossiersTakeParamsSnapshotLast(string IdxMacchina, DateTime dtMin, DateTime dtMax)
{
using var activity = ActivitySource.StartActivity("DossiersUpdateValore");
string source = "DB+REDIS";
bool answ = false;
Log.Info($"Richiesta snapshot per idxMaccSel {IdxMacchina} | periodo {dtMin} --> {dtMax}");
// chiamo stored x salvare parametri
dbController.DossiersTakeParamsSnapshotLast(IdxMacchina, dtMin, dtMax);
// elimino cache redis...
RedisValue pattern = new RedisValue($"{Utils.redisDossByMac}:*");
answ = await ExecFlushRedisPatternAsync(pattern);
activity?.SetTag("data.source", "DB+REDIS");
activity?.Stop();
LogTrace($"DossiersTakeParamsSnapshotLast | Svuotata cache dossier | {pattern} | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return answ;
}
/// <summary>
/// Update valore dossier
/// </summary>
/// <param name="currDoss"></param>
/// <returns></returns>
public async Task<bool> DossiersUpdateValore(DossierModel currDoss)
{
using var activity = ActivitySource.StartActivity("DossiersUpdateValore");
string source = "DB";
// aggiorno record sul DB
bool answ = await dbController.DossiersUpdateValore(currDoss);
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"DossiersUpdateValore | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return answ;
}
/// <summary>
/// Restituisce elenco aziende
/// </summary>
/// <returns></returns>
public async Task<List<AnagGruppiModel>> ElencoAziendeAsync()
{
return await GetOrFetchAsync(
operationName: "ElencoAziendeAsync",
cacheKey: $"{Utils.redisAnagGruppi}:Aziende",
expiration: getRandTOut(redisLongTimeCache * 2),
fetchFunc: async () =>
await dbController.AnagGruppiAziendeAsync() ?? new List<AnagGruppiModel>(),
tagList: [Utils.redisAnagGruppi, $"{Utils.redisAnagGruppi}:Aziende"]
);
}
/// <summary>
/// Restituisce elenco Fasi
/// </summary>
/// <returns></returns>
public async Task<List<AnagGruppiModel>> ElencoGruppiFaseAsync()
{
return await GetOrFetchAsync(
operationName: "ElencoGruppiFaseAsync",
cacheKey: $"{Utils.redisAnagGruppi}:FASE",
expiration: getRandTOut(redisLongTimeCache),
fetchFunc: async () => await dbController.AnagGruppiFaseAsync() ?? new List<AnagGruppiModel>(),
tagList: [Utils.redisAnagGruppi]
);
}
/// <summary>
/// Elenco link validi per il menu
/// </summary>
/// <returns></returns>
public async Task<List<LinkMenuModel>> ElencoLinkAsync()
{
return await GetOrFetchAsync(
operationName: "ElencoLinkAsync",
cacheKey: Utils.redisLinkMenu,
expiration: getRandTOut(redisLongTimeCache),
fetchFunc: async () => await dbController.ElencoLinkAsync() ?? new List<LinkMenuModel>(),
tagList: [Utils.redisLinkMenu]
);
}
/// <summary>
/// Restitusice elenco Reparti
/// </summary>
/// <returns></returns>
public List<RepartiDTO> ElencoRepartiDTO()
{
using var activity = ActivitySource.StartActivity("ElencoRepartiDTO");
List<RepartiDTO> result = new List<RepartiDTO>();
string source = "DB";
string currKey = $"{Utils.redisAnagGruppi}:REPARTO";
// cerco in redis dato valore sel idxMaccSel...
RedisValue rawData = redisDb.StringGet(currKey);
if (rawData.HasValue)
{
var rawResult = JsonConvert.DeserializeObject<List<RepartiDTO>>($"{rawData}");
if (rawResult != null)
{
result = rawResult;
}
source = "REDIS";
}
else
{
result = dbController.AnagGruppiRepartoDTO();
// serializzo e salvo...
rawData = JsonConvert.SerializeObject(result);
redisDb.StringSet(currKey, rawData, getRandTOut(redisLongTimeCache));
}
if (result == null)
{
result = new List<RepartiDTO>();
}
activity?.SetTag("data.source", source);
activity?.SetTag("result.count", result.Count);
activity?.Stop();
LogTrace($"ElencoRepartiDTO | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return result;
}
/// <summary>
/// Caricamento asincrono della cache degli articoli (Used/Unused)
/// </summary>
public async Task EnsureArtCacheLoadedAsync(bool forceReload)
{
if (!forceReload && (DateTime.Now < _artCacheExpiry && (_listCodArtUsed.Count > 0 || _listCodArtNotUsed.Count > 0)))
return;
try
{
// verifico quale sia il set + piccolo
int totalCount = await dbController.ArticoliCountAsync();
int usedCount = await dbController.ArticoliCountUsedAsync();
if (usedCount <= (totalCount - usedCount))
{
var usedList = await dbController.ArticoliGetUsedAsync();
_listCodArtUsed = new HashSet<string>(usedList.Select(x => x.CodArticolo));
_listCodArtNotUsed.Clear();
}
else
{
var unusedList = await dbController.ArticoliGetUnusedAsync();
_listCodArtNotUsed = new HashSet<string>(unusedList.Select(x => x.CodArticolo));
_listCodArtUsed.Clear();
}
// calcolo anche elenco articoli impiegati in istanzanKIT
var listInKit = await dbController.ArticoliInKitAsync();
_listCodArtInKit = new HashSet<string>(listInKit.Select(x => x.CodArticolo));
_artCacheExpiry = DateTime.Now.AddMinutes(15); // TTL ragionevole per la cache locale
}
catch (Exception ex)
{
Log.Error($"Errore nel caricamento cache articoli: {ex.Message}");
_artCacheExpiry = DateTime.Now.AddSeconds(1); // Retry breve in caso di errore
}
}
/// <summary>
/// Aggiunta record EventList
/// </summary>
/// <param name="newRec"></param>
/// <returns></returns>
public async Task<bool> EvListInsert(EventListModel newRec)
{
using var activity = ActivitySource.StartActivity("EvListInsert");
string source = "DB";
var result = await dbController.EvListInsert(newRec);
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"EvListInsert | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return result;
}
/// <summary>
/// Esegue flush memoria redis dato keyVal
/// </summary>
/// <param name="pat2Flush"></param>
/// <returns></returns>
public bool ExecFlushRedisPattern(string pat2Flush)
{
using var activity = ActivitySource.StartActivity("ExecFlushRedisPattern");
string source = "REDIS";
bool answ = false;
var masterEndpoint = redisConn.GetEndPoints()
.Where(ep => redisConn.GetServer(ep).IsConnected && !redisConn.GetServer(ep).IsReplica)
.FirstOrDefault();
// sepattern è "*" elimino intero DB...
if (masterEndpoint != null && (pat2Flush.Equals(new RedisValue("*")) || pat2Flush == RedisValue.Null))
{
redisConn.GetServer(masterEndpoint).FlushDatabase(database: redisDb.Database);
}
else
{
var server = redisConn.GetServer(masterEndpoint);
var keys = server.Keys(database: redisDb.Database, pattern: pat2Flush, pageSize: 1000);
var batch = new List<RedisKey>();
foreach (var key in keys)
{
batch.Add(key);
// Flush in batches of 1000
if (batch.Count >= 1000)
{
foreach (var item in batch)
redisDb.KeyDelete(item);
batch.Clear();
}
}
// Flush remaining keys
foreach (var item in batch)
redisDb.KeyDelete(item);
}
answ = true;
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"ExecFlushRedisPattern | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return answ;
}
/// <summary>
/// Esegue flush memoria redis dato keyVal, async
/// </summary>
/// <param name="pat2Flush"></param>
/// <returns></returns>
public async Task<bool> ExecFlushRedisPatternAsync(RedisValue pat2Flush)
{
bool answ = false;
using var activity = ActivitySource.StartActivity("ExecFlushRedisPatternAsync");
string source = "REDIS";
var masterEndpoint = redisConn.GetEndPoints()
.Where(ep => redisConn.GetServer(ep).IsConnected && !redisConn.GetServer(ep).IsReplica)
.FirstOrDefault();
// sepattern è "*" elimino intero DB...
if (masterEndpoint != null && (pat2Flush.Equals(new RedisValue("*")) || pat2Flush == RedisValue.Null))
{
redisConn.GetServer(masterEndpoint).FlushDatabase(database: redisDb.Database);
}
else
{
var server = redisConn.GetServer(masterEndpoint);
var keys = server.Keys(database: redisDb.Database, pattern: pat2Flush, pageSize: 1000);
var deleteTasks = new List<Task>();
foreach (var key in keys)
{
deleteTasks.Add(redisDb.KeyDeleteAsync(key));
if (deleteTasks.Count >= 1000)
{
await Task.WhenAll(deleteTasks);
deleteTasks.Clear();
}
}
if (deleteTasks.Count > 0)
{
await Task.WhenAll(deleteTasks);
}
}
answ = true;
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"ExecFlushRedisPatternAsync | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return answ;
}
/// <summary>
/// Cancellazione FusionCache (totale)
/// </summary>
/// <returns></returns>
public async Task<bool> FlushCacheAsync()
{
await _cache.ClearAsync(allowFailSafe: false);
_configData.Clear();
_artCacheExpiry = DateTime.Now.AddHours(-1);
return true;
}
/// <summary>
/// Cancellazione FusionCache dato singolo tag
/// </summary>
/// <returns></returns>
public async Task<bool> FlushCacheByTagAsync(string tag)
{
if (string.IsNullOrWhiteSpace(tag)) return false;
await _cache.RemoveByTagAsync(tag);
_configData.Clear();
return true;
}
/// <summary>
/// Cancellazione FusionCache dato elenco tags
/// </summary>
/// <returns></returns>
public async Task<bool> FlushCacheByTagsAsync(List<string> listTags)
{
if (listTags == null || listTags.Count == 0) return false;
// Generiamo i Task di rimozione ed eseguiamoli in parallelo su Redis/L1
var tasks = listTags
.Where(tag => !string.IsNullOrWhiteSpace(tag))
.Select(tag => _cache.RemoveByTagAsync(tag).AsTask());
await Task.WhenAll(tasks);
_configData.Clear();
return true;
}
public async Task<bool> FlushCacheFluxLog()
{
using var activity = ActivitySource.StartActivity("FlushCacheFluxLog");
string source = "REDIS";
bool answ = false;
RedisValue pattern = new RedisValue($"{Utils.redisParetoFLKey}:*");
answ = await ExecFlushRedisPatternAsync(pattern);
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"FlushCacheFluxLog | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return answ;
}
/// <summary>
/// Flush cache relativa a MP-IO x dati ODL
/// </summary>
/// <returns></returns>
public async Task<bool> FlushMpIoOdlCache()
{
using var activity = ActivitySource.StartActivity("FlushMpIoOdlCache");
string source = "REDIS";
// svuoto dalla cache REDIS del server IO...
bool ok01 = await ResetIoCache("CurrODL");
bool ok02 = await ResetIoCache("CurrOdlRow");
bool ok03 = await ResetIoCache("CurrStatoMacc");
bool ok04 = await ResetIoCache("DtMac");
activity?.SetTag("data.source", "REDIS");
activity?.Stop();
LogTrace($"FlushMpIoOdlCache | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return ok01 && ok02 && ok03 && ok04;
}
public async Task<bool> FlushRedisCache()
{
using var activity = ActivitySource.StartActivity("FlushRedisCache");
string source = "REDIS";
RedisValue pattern = Utils.RedValue("*");
bool answ = await ExecFlushRedisPatternAsync(pattern);
activity?.Stop();
LogTrace($"FlushRedisCache | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return answ;
}
public async Task<bool> FlushRedisKey(string redKey)
{
using var activity = ActivitySource.StartActivity("FlushRedisKey");
string source = "REDIS";
RedisValue pattern = Utils.RedValue(redKey);
bool answ = await ExecFlushRedisPatternAsync(pattern);
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"FlushRedisKey | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return answ;
}
/// <summary>
/// Funzione di Data Reduction x FluxLog
/// </summary>
/// <param name="idxMaccSel">Macchina</param>
/// <param name="fluxList">Elenco FL da processare</param>
/// <param name="currPeriodo">Periodo</param>
/// <param name="valMode">modalità sel valore</param>
/// <param name="intReq">intervallo di analisi</param>
/// <param name="maxItem">max num per intervallo</param>
/// <returns></returns>
public async Task FluxLogDataRedux(string idxMaccSel, List<string> fluxList, DtUtils.Periodo currPeriodo, Enums.ValSelection valMode, Enums.DataInterval intReq, int maxItem)
{
using var activity = ActivitySource.StartActivity("FluxLogDataRedux");
string source = "DB+REDIS";
List<StatDedupDTO> procStats = await dbController.FluxLogDataRedux(idxMaccSel, fluxList, currPeriodo, valMode, intReq, maxItem);
// effettuo merge statistiche...
ProcDedupStatMerge(procStats);
// svuoto cache
await FlushCacheFluxLog();
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"FluxLogDataRedux | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
}
public List<FluxLogDTO> FluxLogDtoGetByFlux(string Valore)
{
List<FluxLogDTO> answ = new List<FluxLogDTO>();
DossierFluxLogDTO? result = JsonConvert.DeserializeObject<DossierFluxLogDTO>(Valore);
if (result != null)
{
if (result.ODL != null)
{
answ = result
.ODL
.OrderBy(x => x.CodFlux)
.ToList();
// inizializzo SE necessario
foreach (var item in answ)
{
item.ValoreEdit = String.IsNullOrEmpty(item.ValoreEdit) ? item.Valore : item.ValoreEdit;
}
}
}
return answ;
}
/// <summary>
/// Elenco FluxLog in modalità filtro
/// </summary>
/// <param name="DtMax">Data massima x eventi</param>
/// <param name="DtMin">Data minima x eventi</param>
/// <param name="IdxMacchina">* = tutte, altrimenti solo x una data idxMaccSel</param>
/// <param name="CodFlux">*=tutti, altrimenti solo selezionato</param>
/// <param name="MaxRec">numero massimo record da restituire</param>
/// <param name="redisCacheSec">durata cache in secondi</param>
/// <returns></returns>
public async Task<List<FluxLogModel>> FluxLogGetLastFiltAsync(DateTime DtMax, DateTime DtMin, string IdxMacchina, string CodFlux, int MaxRec, double redisCacheSec)
{
string currKey = $"{Utils.redisFluxLogFilt}:{IdxMacchina}:{CodFlux}:{MaxRec}:{DtMax:yyyyMMddHHmm}:{DtMin:yyyyMMddHHmm}";
return await GetOrFetchAsync(
operationName: "FluxLogGetLastFiltAsync",
cacheKey: currKey,
expiration: TimeSpan.FromSeconds(redisCacheSec),
fetchFunc: async () => await dbController.FluxLogGetLastFiltAsync(DtMax, DtMin, IdxMacchina, CodFlux, MaxRec) ?? new List<FluxLogModel>(),
tagList: [Utils.redisFluxLogFilt]
);
}
/// <summary>
/// Elenco FluxLog in modalità Pareto
/// </summary>
/// <returns></returns>
public async Task<List<ParetoFluxLogDTO>> FluxLogParetoAsync(string idxMacchina, DateTime dtFrom, DateTime dtTo)
{
string redKey = $"{Utils.redisParetoFLKey}:{idxMacchina}:{dtFrom:yyyyMMdd}:{dtTo:yyyyMMdd}";
return await GetOrFetchAsync(
operationName: "FluxLogParetoAsync",
cacheKey: redKey,
expiration: getRandTOut(redisLongTimeCache),
fetchFunc: async () => await dbController.FluxLogParetoAsync(idxMacchina, dtFrom, dtTo) ?? new List<ParetoFluxLogDTO>(),
tagList: [Utils.redisParetoFLKey]
);
}
/// <summary>
/// Stored manutenzione del DB
/// </summary>
/// <param name="doExec">Esegue realmente il task</param>
/// <param name="doUpdStat">Aggiornamento statistiche</param>
/// <param name="doSave">Salvataggio</param>
/// <param name="minPgCnt">def: 1000</param>
/// <param name="minAvgFrag">def: 10</param>
/// <param name="maxAvgFragReb">def: 50</param>
/// <returns></returns>
public async Task ForceDbMaint(bool doExec = true, bool doUpdStat = true, bool doSave = true, int minPgCnt = 1000, int minAvgFrag = 10, int maxAvgFragReb = 50)
{
using var activity = ActivitySource.StartActivity("ForceDbMaint");
string source = "DB+REDIS";
await dbController.ForceDbMaint(doExec, doUpdStat, doSave, minPgCnt, minAvgFrag, maxAvgFragReb);
// svuoto cache
await FlushCacheFluxLog();
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"ForceDbMaint | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
// registro statistiche esecuzione
RecDbMaintStat(activity?.Duration ?? TimeSpan.FromSeconds(1));
}
/// <summary>
/// Eliminazione di un record macchina dal gruppo
/// </summary>
/// <param name="rec2del"></param>
/// <returns></returns>
public bool Grp2MaccDelete(Gruppi2MaccModel rec2del)
{
using var activity = ActivitySource.StartActivity("Grp2MaccDelete");
bool result = false;
result = dbController.Grp2MaccDelete(rec2del);
// elimino cache redis...
ResetMacGrpCache();
activity?.SetTag("data.source", "DB+REDIS");
activity?.Stop();
LogTrace($"Grp2MaccDelete | CodGruppo {rec2del.CodGruppo} | IdxMacc {rec2del.IdxMacchina} | {activity?.Duration.TotalMilliseconds}ms");
return result;
}
/// <summary>
/// Insert di un record macchina
/// </summary>
/// <param name="upsRec"></param>
/// <returns></returns>
public bool Grp2MaccInsert(Gruppi2MaccModel upsRec)
{
using var activity = ActivitySource.StartActivity("Grp2MaccInsert");
bool result = false;
result = dbController.Grp2MaccInsert(upsRec);
// elimino cache redis...
ResetMacGrpCache();
activity?.SetTag("data.source", "DB+REDIS");
activity?.Stop();
LogTrace($"Grp2MaccInsert | CodGruppo {upsRec.CodGruppo} | IdxMacc {upsRec.IdxMacchina} | {activity?.Duration.TotalMilliseconds}ms");
return result;
}
/// <summary>
/// Eliminazione di un record operatore dal gruppo
/// </summary>
/// <param name="rec2del"></param>
/// <returns></returns>
public bool Grp2OperDelete(Gruppi2OperModel rec2del)
{
using var activity = ActivitySource.StartActivity("Grp2OperDelete");
bool result = false;
result = dbController.Grp2OperDelete(rec2del);
// elimino cache redis...
ResetOprGrpCache();
activity?.SetTag("data.source", "DB+REDIS");
activity?.Stop();
LogTrace($"Grp2OperDelete | CodGruppo {rec2del.CodGruppo} | MatrOpr {rec2del.MatrOpr} | {activity?.Duration.TotalMilliseconds}ms");
return result;
}
/// <summary>
/// Insert di un record operatore
/// </summary>
/// <param name="upsRec"></param>
/// <returns></returns>
public bool Grp2OperInsert(Gruppi2OperModel upsRec)
{
using var activity = ActivitySource.StartActivity("Grp2OperInsert");
bool result = false;
result = dbController.Grp2OperInsert(upsRec);
// elimino cache redis...
ResetOprGrpCache();
activity?.SetTag("data.source", "DB+REDIS");
activity?.Stop();
LogTrace($"Grp2OperInsert | CodGruppo {upsRec.CodGruppo} | MatrOpr {upsRec.MatrOpr} | {activity?.Duration.TotalMilliseconds}ms");
return result;
}
/// <summary>
/// Init ricetta
/// </summary>
/// <param name="confPath"></param>
/// <param name="idxPODL"></param>
/// <param name="CalcArgs"></param>
/// <returns></returns>
public RecipeModel InitRecipe(string confPath, int idxPODL, Dictionary<string, string> CalcArgs)
{
return mongoController.InitRecipe(confPath, idxPODL, CalcArgs);
}
/// <summary>
/// Elimina record + svuotamento cache
/// </summary>
/// <param name="currRecord"></param>
public async Task<bool> IstKitDelete(IstanzeKitModel currRecord)
{
using var activity = ActivitySource.StartActivity("IstKitDelete");
string source = "DB+REDIS";
bool fatto = false;
// salvo
fatto = dbController.IstKitDelete(currRecord);
// svuoto cache
RedisValue pattern = $"{Utils.redisKitInst}:*";
await ExecFlushRedisPatternAsync(pattern);
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"IstKitDelete | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return fatto;
}
/// <summary>
/// Elenco Istanze KIT da ricerca
/// </summary>
/// <param name="keyKit"></param>
/// <param name="keyExtOrd"></param>
/// <returns></returns>
public async Task<List<IstanzeKitModel>> IstKitFiltAsync(string keyKit, string keyExtOrd)
{
string currKey = $"{Utils.redisKitInst}:{keyKit}:{keyExtOrd}";
return await GetOrFetchAsync(
operationName: "IstKitFiltAsync",
cacheKey: currKey,
expiration: getRandTOut(redisLongTimeCache),
fetchFunc: async () => await dbController.IstKitFiltAsync(keyKit, keyExtOrd) ?? new List<IstanzeKitModel>(),
tagList: [Utils.redisKitInst]
);
}
/// <summary>
/// Effettua creazione istanza KIT
/// </summary>
/// <param name="CodArtParent">Articolo KIT (fittizio)</param>
/// <param name="KeyFilt">Chiave x filtro conf su tab WKS</param>
public bool IstKitInsertByWKS(string CodArtParent, string KeyFilt)
{
bool fatto = false;
using var activity = ActivitySource.StartActivity("IstKitInsertByWKS");
string source = "DB+REDIS";
// salvo
fatto = dbController.IstKitInsertByWKS(CodArtParent, KeyFilt);
// svuoto cache
ExecFlushRedisPattern($"{Utils.redisKit}:*");
//ExecFlushRedisPattern((RedisValue)$"{Utils.redisKitInst}:*");
//ExecFlushRedisPattern((RedisValue)$"{Utils.redisKitScore}:*");
//ExecFlushRedisPattern((RedisValue)$"{Utils.redisKitTempl}:*");
//ExecFlushRedisPattern((RedisValue)$"{Utils.redisKitWip}:*");
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"IstKitInsertByWKS | {source} | {activity?.Duration.TotalMilliseconds}ms");
return fatto;
}
/// <summary>
/// Esegue salvataggio record + svuotamento cache
/// </summary>
/// <param name="currRecord"></param>
public async Task<bool> IstKitUpsert(IstanzeKitModel currRecord)
{
using var activity = ActivitySource.StartActivity("IstKitUpsert");
string source = "DB+REDIS";
bool fatto = false;
// salvo
fatto = dbController.IstKitUpsert(currRecord);
// svuoto cache
RedisValue pattern = $"{Utils.redisKitInst}:*";
await ExecFlushRedisPatternAsync(pattern);
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"IstKitUpsert | {source} | {activity?.Duration.TotalMilliseconds}ms");
return fatto;
}
/// <summary>
/// Elenco giacenze filtrate x IdxOdl
/// </summary>
/// <param name="IdxOdl">id odl da cercare</param>
/// <returns></returns>
public async Task<List<AnagGiacenzeModel>> ListGiacenzeAsync(int IdxOdl)
{
string currKey = $"{Utils.redisGiacenzaList}:{IdxOdl}";
return await GetOrFetchAsync(
operationName: "ListGiacenzeAsync",
cacheKey: currKey,
expiration: getRandTOut(redisShortTimeCache),
fetchFunc: async () => await dbController.ListGiacenzeAsync(IdxOdl) ?? new List<AnagGiacenzeModel>(),
tagList: [Utils.redisGiacenzaList]
);
}
/// <summary>
/// Recupero elenco PODL filtrati
/// </summary>
/// <param name="CodArticolo"></param>
/// <param name="OnlyAvail">True = aperti (=senza ODL)</param>
/// <returns></returns>
public List<PODLExpModel> ListPODL_ByCodArt(string CodArticolo, bool OnlyAvail)
{
List<PODLExpModel> result = new List<PODLExpModel>();
if (!string.IsNullOrEmpty(CodArticolo))
{
using var activity = ActivitySource.StartActivity("ListPODL_ByCodArt");
string source = "DB";
string avType = OnlyAvail ? "Avail" : "ALL";
string currKey = $"{Utils.redisPOdlByCodArt}:{CodArticolo}:{avType}";
// cerco in redis dato valore sel idxMaccSel...
RedisValue rawData = redisDb.StringGet(currKey);
if (rawData.HasValue && rawData.Length() > 2)
{
var rawResult = JsonConvert.DeserializeObject<List<PODLExpModel>>($"{rawData}");
if (rawResult != null)
{
result = rawResult;
source = "REDIS";
}
}
else
{
result = dbController.ListPODL_ByCodArt(CodArticolo, OnlyAvail);
// serializzo e salvo...
rawData = JsonConvert.SerializeObject(result);
redisDb.StringSet(currKey, rawData, getRandTOut(redisLongTimeCache));
}
if (result == null)
{
result = new List<PODLExpModel>();
}
activity?.SetTag("data.source", source);
activity?.SetTag("result.count", result.Count);
activity?.Stop();
Log.Trace($"ListPODL_ByCodArt | {source} | {activity?.Duration.TotalMilliseconds}ms");
}
else
{
Log.Debug("Errore CodArt vuoto");
}
return result;
}
/// <summary>
/// Elenco di tutte le macchine filtrate x gruppo
/// </summary>
/// <param name="codGruppo"></param>
/// <returns></returns>
public async Task<List<MacchineModel>> MacchineGetFiltAsync(string codGruppo)
{
string keyGrp = codGruppo != "*" ? codGruppo : "ALL";
string redisKey = $"{Utils.redisMacList}:{keyGrp}";
return await GetOrFetchAsync(
operationName: "MacchineGetFiltAsync",
cacheKey: redisKey,
expiration: getRandTOut(redisLongTimeCache),
fetchFunc: async () =>
await dbController.MacchineGetFiltAsync(codGruppo)
?? new List<MacchineModel>(),
tagList: [Utils.redisMacList]
);
}
/// <summary>
/// Verifica se la idxMaccSel abbia un codice PATH ricette associato
/// </summary>
/// <param name="idxMacchina"></param>
/// <returns></returns>
public async Task<string> MacchineRecipeArchiveAsync(string idxMacchina)
{
string currKey = $"{Utils.redisMacRecipePath}:{idxMacchina}";
return await GetOrFetchAsync(
operationName: "MacchineRecipeArchiveAsync",
cacheKey: currKey,
expiration: getRandTOut(redisLongTimeCache),
fetchFunc: async () =>
{
var machineList = await MacchineGetFiltAsync("*");
var currMach = machineList.FirstOrDefault(x => x.IdxMacchina == idxMacchina);
return currMach?.RecipeArchivePath ?? "";
},
tagList: [Utils.redisMacRecipePath]
);
}
/// <summary>
/// Verifica se la idxMaccSel abbia un codice CONF ricetta associato
/// </summary>
/// <param name="idxMacchina"></param>
/// <returns></returns>
public async Task<string> MacchineRecipeConfAsync(string idxMacchina)
{
string currKey = $"{Utils.redisMacRecipeConf}:{idxMacchina}";
return await GetOrFetchAsync(
operationName: "MacchineRecipeConfAsync",
cacheKey: currKey,
expiration: getRandTOut(redisLongTimeCache),
fetchFunc: async () =>
{
var machineList = await MacchineGetFiltAsync("*");
var currMach = machineList.FirstOrDefault(x => x.IdxMacchina == idxMacchina);
return currMach?.RecipePath ?? "";
},
tagList: [Utils.redisMacRecipeConf]
);
}
/// <summary>
/// Elenco id Macchine che abbiano dati FLuxLog, nel periodo indicato
/// </summary>
/// <param name="dtStart"></param>
/// <param name="dtEnd"></param>
/// <returns></returns>
public async Task<List<string>> MacchineWithFluxAsync(DateTime dtStart, DateTime dtEnd)
{
string currKey = $"{Utils.redisMacByFlux}:{dtStart:yyyyMMddHHmm}:{dtEnd:yyyyMMddHHmm}";
return await GetOrFetchAsync(
operationName: "MacchineWithFluxAsync",
cacheKey: currKey,
expiration: getRandTOut(redisLongTimeCache),
fetchFunc: async () => await dbController.MacchineWithFluxAsync(dtStart, dtEnd) ?? new List<string>(),
tagList: [Utils.redisMacByFlux]
);
}
public async Task<List<string>> MachineWithOdlAsync()
{
string redisKey = Utils.redisOdlCurrByMac;
return await GetOrFetchAsync(
operationName: "MachineWithOdlAsync",
cacheKey: redisKey,
expiration: getRandTOut(redisShortTimeCache),
fetchFunc: async () =>
{
var rawData = await dbController.OdlGetCurrentAsync();
var dbResult = rawData
.Select(x => x.IdxMacchina)
.Distinct()
.ToList();
return dbResult ?? new List<string>();
},
tagList: [Utils.redisOdlCurrByMac]
);
}
/// <summary>
/// Recupero info Machine-IOB x TAB (da info registrate IOB-WIN --&gt; MP-IO)
/// </summary>
/// <param name="IdxMacchina"></param>
/// <returns></returns>
public async Task<Dictionary<string, string>> MachIobConfAsync(string IdxMacchina)
{
string redisKey = Utils.redisIobConf;
return await GetOrFetchAsync(
operationName: "MachIobConfAsync",
cacheKey: redisKey,
expiration: getRandTOut(redisShortTimeCache),
fetchFunc: async () =>
{
Dictionary<string, string> result = new Dictionary<string, string>();
// cerco in redis...
string currKey = redHashMpIO($"IOB:{IdxMacchina}:MachIobConfAsync");
if (await redisDb.KeyExistsAsync(currKey))
{
result = (await redisDb.HashGetAllAsync(currKey))
.ToDictionary(x => $"{x.Name}", x => $"{x.Value}");
}
return result;
},
tagList: [Utils.redisIobConf]
);
}
/// <summary>
/// Elenco MSE stato amcchine
/// </summary>
/// <param name="forceDb">Force refresh from DB</param>
/// <returns></returns>
public async Task<List<MappaStatoExplModel>> MseGetAllAsync(bool forceDb = false)
{
using var activity = ActivitySource.StartActivity("MseGetAllAsync");
if (forceDb)
{
await _cache.RemoveAsync(Constants.redisMseKey);
}
return await GetOrFetchAsync(
operationName: "MseGetAllAsync",
cacheKey: Constants.redisMseKey,
expiration: TimeSpan.FromSeconds(1),
fetchFunc: async () => await dbController.MseGetAllAsync(2000) ?? new(),
tagList: [Constants.redisMseKey]
);
}
/// <summary>
/// Invio notifica rilettura (con parametro)
/// </summary>
/// <param name="message"></param>
public void NotifyReloadRequest(string message)
{
if (ReloadRequest != null)
{
// messaggio
ReloadEventArgs rea = new ReloadEventArgs(message);
ReloadRequest.Invoke(this, rea);
}
}
/// <summary>
/// Elenco ODL dato batch selezionato
/// </summary>
/// <param name="BatchSel">Batch richiesto</param>
/// <returns></returns>
public async Task<List<int>> OdlByBatchAsync(string BatchSel)
{
return await GetOrFetchAsync(
operationName: "OdlByBatchAsync",
cacheKey: Utils.redisOdlByBatch,
expiration: getRandTOut(redisLongTimeCache),
fetchFunc: async () => await dbController.OdlByBatchAsync(BatchSel),
tagList: [Utils.redisOdlByBatch]
);
}
/// <summary>
/// ODL da chiave
/// </summary>
/// <param name="IdxOdl"></param>
/// <returns></returns>
public async Task<ODLExpModel> OdlByKeyAsync(int IdxOdl)
{
string currKey = $"{Utils.redisOdlByKey}:{IdxOdl}";
return await GetOrFetchAsync(
operationName: "OdlByKeyAsync",
cacheKey: currKey,
expiration: getRandTOut(redisLongTimeCache),
fetchFunc: async () => await dbController.OdlByKeyAsync(IdxOdl),
tagList: [Utils.redisOdlByKey]
);
}
/// <summary>
/// Effettua chiusura dell'ODL indicato, andand
/// </summary>
/// <param name="idxOdl">idx odl da chiudere</param>
/// <param name="idxMacchina">idx idxMaccSel</param>
/// <param name="matrOpr">matricola operatore</param>
/// <param name="confPezzi">indica se confermare i pezzi priam di chiudere ODL</param>
public async Task<bool> ODLClose(int idxOdl, string idxMacchina, int matrOpr, bool confPezzi)
{
using var activity = ActivitySource.StartActivity("ODLClose");
string source = "DB";
bool fatto = false;
await EnsureConfigLoadedAsync();
bool confRett = false;
_configData.TryGetValue("confRett", out var value);
if (!string.IsNullOrEmpty(value))
{
bool.TryParse(value, out confRett);
}
int modoConfProd = 0;
_configData.TryGetValue("modoConfProd", out var vModo);
if (!string.IsNullOrEmpty(value))
{
int.TryParse(vModo, out modoConfProd);
}
// chiamo metodo conferma!
fatto = await dbController.ODLClose(idxOdl, idxMacchina, matrOpr, confPezzi, confRett, modoConfProd);
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"ODLClose | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return fatto;
}
#if false
/// <summary>
/// Record ODL da chaive
/// </summary>
/// <returns></returns>
public async Task<ODLModel> OdlGetByKey(int IdxOdl)
{
using var activity = ActivitySource.StartActivity("OdlGetByKey");
string source = "DB";
var dbResult = await dbController.OdlGetByKey(IdxOdl);
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"OdlGetByKey | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return dbResult;
}
#endif
/// <summary>
/// Elenco ODL filtrati x stato, articolo, KeyRich (che contiene stato)
/// </summary>
/// <param name="inCorso">Stato ODL: true=in corso/completato</param>
/// <param name="codArt">Cod articolo</param>
/// <param name="keyRichPart">KeyRich (parziale) da cercare (es cod stato x yacht)</param>
/// <param name="Reparto">Reparto selezionato</param>
/// <param name="IdxMacchina">Macchina selezionata</param>
/// <param name="startDate">Data inizio</param>
/// <param name="endDate">Data fine</param>
/// <returns></returns>
public async Task<List<ODLExpModel>> OdlListGetFiltAsync(bool inCorso, string codArt, string keyRichPart, string Reparto, string IdxMacchina, DateTime startDate, DateTime endDate)
{
string currKey = $"{Utils.redisOdlList}:{inCorso}:{codArt}:{keyRichPart}:{Reparto}:{IdxMacchina}:{startDate:yyyyMMdd_HHmmss}:{endDate:yyyyMMdd_HHmmss}";
return await GetOrFetchAsync(
operationName: "OdlListGetFiltAsync",
cacheKey: currKey,
expiration: getRandTOut(redisShortTimeCache),
fetchFunc: async () => await dbController.ListODLFiltAsync(inCorso, codArt, keyRichPart, Reparto, IdxMacchina, startDate, endDate) ?? new(),
tagList: [Utils.redisOdlList]
);
}
/// <summary>
/// Elenco operatori filtrati x gruppo
/// </summary>
/// <param name="codGruppo"></param>
/// <returns></returns>
public async Task<List<AnagOperatoriModel>> OperatoriGetFiltAsync(string codGruppo)
{
string keyGrp = codGruppo != "*" ? codGruppo : "ALL";
string currKey = $"{Utils.redisOprList}:{keyGrp}";
return await GetOrFetchAsync(
operationName: "OperatoriGetFiltAsync",
cacheKey: currKey,
expiration: getRandTOut(redisLongTimeCache),
fetchFunc: async () => await dbController.OperatoriGetFiltAsync(codGruppo) ?? new List<AnagOperatoriModel>(),
tagList: [Utils.redisOprList]
);
}
/// <summary>
/// Elenco di tutti i parametri filtrati x idxMaccSel
/// </summary>
/// <param name="IdxMacchina">* = tutte, altrimenti solo x una data idxMaccSel</param>
/// <returns></returns>
public async Task<List<string>> ParametriGetFiltAsync(string IdxMacchina)
{
string currKey = $"{Utils.redisFluxByMac}:{IdxMacchina}";
return await GetOrFetchAsync(
operationName: "ParametriGetFiltAsync",
cacheKey: currKey,
expiration: getRandTOut(redisShortTimeCache),
fetchFunc: async () => await dbController.ParametriGetFiltAsync(IdxMacchina) ?? new(),
tagList: [Utils.redisFluxByMac]
);
}
/// <summary>
/// Restituisce dizionario ODL/PODL data lista IdxOdl
/// </summary>
/// <param name="idxOdlList"></param>
/// <returns></returns>
public async Task<Dictionary<int, int>> PODL_getDictOdlPodlAsync(List<int> idxOdlList)
{
if (idxOdlList == null || !idxOdlList.Any())
return new Dictionary<int, int>();
var distinctIds = idxOdlList.Distinct().ToList();
var resultDictionary = new Dictionary<int, int>();
var missingIds = new List<int>();
// STEP 1: Controllo rapido in FusionCache (L1/Memory cache)
foreach (var id in distinctIds)
{
var cacheKey = $"val:{id}";
var cachedValue = await _cache.TryGetAsync<int>(cacheKey);
if (cachedValue.HasValue)
{
resultDictionary[id] = cachedValue.Value;
}
else
{
// ID non presente in cache, andrà cercato tramite il servizio EF
missingIds.Add(id);
}
}
// STEP 2: Se ci sono cache miss, interroghiamo il servizio EF Core
if (missingIds.Any())
{
// Riceviamo direttamente un Dictionary<int, int> ottimizzato da EF Core
Dictionary<int, int> dbResults = await dbController.PODL_getDictOdlPodlAsync(missingIds);
// STEP 3: Scriviamo i risultati in cache e li uniamo al dizionario finale
foreach (var kvp in dbResults)
{
var id = kvp.Key;
var targetValue = kvp.Value;
resultDictionary[id] = targetValue;
// Salvataggio atomico e globale su FusionCache
var cacheKey = $"val:{id}";
await _cache.SetAsync(cacheKey, targetValue, TimeSpan.FromMinutes(30));
}
// STEP 4 [Altamente Consigliato]: Cache Penetration Protection
// Se un ID era tra i "missing" ma NON è presente nei risultati del DB, significa che non esiste.
// Salviamo un valore sentinella (es. 0 o -1) per evitare di ricontrollare il DB al prossimo giro.
foreach (var id in missingIds)
{
if (!dbResults.ContainsKey(id))
{
resultDictionary[id] = 0; // Imposta un default per l'output corrente
var cacheKey = $"val:{id}";
await _cache.SetAsync(cacheKey, 0, TimeSpan.FromMinutes(2)); // Scadenza breve per i record inesistenti
}
}
}
return resultDictionary;
}
/// <summary>
/// Eliminazione record selezionato
/// </summary>
/// <param name="currRec"></param>
/// <returns></returns>
public async Task<bool> POdlDeleteRecord(PODLExpModel currRec)
{
using var activity = ActivitySource.StartActivity("POdlDeleteRecord");
string source = "DB+REDIS";
var dbResult = await dbController.PODLDeleteRecord(currRec);
// elimino cache redis...
await POdlFlushCache();
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"POdlDeleteRecord | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return dbResult;
}
/// <summary>
/// Avvio fase setup per il record selezionato
/// </summary>
/// <param name="currRec"></param>
/// <returns></returns>
public async Task<bool> POdlDoSetup(PODLExpModel currRec)
{
using var activity = ActivitySource.StartActivity("POdlDoSetup");
string source = "DB+REDIS";
var dbResult = await dbController.PODL_startSetup(currRec, 0, 1, 1, "", DateTime.Now);
// elimino cache redis...
await POdlFlushCache();
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"POdlDoSetup | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return dbResult;
}
/// <summary>
/// Recupero PODL da chiave
/// </summary>
/// <param name="idxPODL"></param>
/// <returns></returns>
public async Task<PODLModel> POdlGetByKey(int idxPODL)
{
PODLModel result = new PODLModel();
if (idxPODL != 0)
{
using var activity = ActivitySource.StartActivity("POdlGetByKey");
string source = "DB";
string currKey = $"{Utils.redisPOdlByPOdl}:{idxPODL}";
// cerco in redis dato valore sel idxMaccSel...
RedisValue rawData = redisDb.StringGet(currKey);
if (rawData.HasValue)
{
var rawResult = JsonConvert.DeserializeObject<PODLModel>($"{rawData}");
if (rawResult != null)
{
result = rawResult;
source = "REDIS";
}
}
else
{
result = await dbController.PODL_getByKey(idxPODL);
// serializzo e salvo...
rawData = JsonConvert.SerializeObject(result);
redisDb.StringSet(currKey, rawData, getRandTOut(redisLongTimeCache));
}
if (result == null)
{
result = new PODLModel();
}
activity?.SetTag("data.source", source);
activity?.SetTag("result.count", 1);
activity?.Stop();
Log.Trace($"POdlGetByKey | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
}
else
{
Log.Debug("Errore IdxPODL = 0");
}
return result;
}
/// <summary>
/// Recupero PODL da IdxODL
/// </summary>
/// <param name="idxODL"></param>
/// <returns></returns>
public async Task<PODLModel> POdlGetByOdlAsync(int idxODL)
{
string currKey = $"{Utils.redisPOdlByOdl}:{idxODL}";
return await GetOrFetchAsync(
operationName: "POdlGetByOdlAsync",
cacheKey: currKey,
expiration: TimeSpan.FromMinutes(redisLongTimeCache),
fetchFunc: async () => await dbController.PODL_getByOdlAsync(idxODL) ?? new(),
tagList: [Utils.redisPOdlByOdl]
);
}
/// <summary>
/// Effettua il task di eliminazione PODL KIT + istanze + riattivazione PODL originali disattivate tramite stored
/// </summary>
/// <param name="IdxPODL">IdxPODL parent</param>
public bool PodlIstKitDelete(int IdxPODL)
{
using var activity = ActivitySource.StartActivity("PodlIstKitDelete");
bool fatto = false;
// salvo
fatto = dbController.PodlIstKitDelete(IdxPODL);
// svuoto cache
string pattern = $"{Utils.redisKit}:*";
if (!string.IsNullOrEmpty(pattern))
{
ExecFlushRedisPattern(pattern);
}
activity?.SetTag("data.source", "DB+REDIS");
return fatto;
}
/// <summary>
/// Elenco PODL in un istanza KIT dall'ID del parent
/// </summary>
/// <param name="IdxPodlParent">IDX PODL parent</param>
/// <returns></returns>
public List<PODLExpModel> POdlListByKitParent(int IdxPodlParent)
{
using var activity = ActivitySource.StartActivity("POdlListByKitParent");
List<PODLExpModel>? result = new List<PODLExpModel>();
string source = "DB";
string currKey = $"{Utils.redisPOdlList}_kit:ByParent:{IdxPodlParent}";
// cerco in redis dato valore sel idxMaccSel...
RedisValue rawData = redisDb.StringGet(currKey);
if (rawData.HasValue)
{
result = JsonConvert.DeserializeObject<List<PODLExpModel>>($"{rawData}");
source = "REDIS";
}
else
{
result = dbController.ListPODL_ByKitParent(IdxPodlParent);
// serializzo e salvo...
rawData = JsonConvert.SerializeObject(result);
redisDb.StringSet(currKey, rawData, TimeSpan.FromSeconds(redisShortTimeCache));
}
if (result == null)
{
result = new List<PODLExpModel>();
}
activity?.SetTag("data.source", source);
activity?.SetTag("result.count", result.Count);
activity?.Stop();
LogTrace($"POdlListByKitParent | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return result;
}
/// <summary>
/// Elenco PODL non avviati filtrati x articolo, KeyRich (che contiene stato)
/// </summary>
/// <param name="lanciato">Solo lanciati (1) o ancora disponibili (0)</param>
/// <param name="keyRichPart">KeyRich (parziale) da cercare (es cod stato x yacht)</param>
/// <param name="idxMacchina">Macchina</param>
/// <param name="codGruppo">Gruppo</param>
/// <param name="startDate">Data inizio</param>
/// <param name="endDate">Data fine</param>
/// <returns></returns>
public async Task<List<PODLExpModel>> POdlListGetFiltAsync(bool lanciato, string keyRichPart, string idxMacchina, string codGruppo, DateTime startDate, DateTime endDate)
{
string currKey = $"{Utils.redisPOdlList}:{codGruppo}:{idxMacchina}:{keyRichPart}:{lanciato}:{startDate:yyyyMMdd_HHmmss}:{endDate:yyyyMMdd_HHmmss}";
return await GetOrFetchAsync(
operationName: "POdlListGetFiltAsync",
cacheKey: currKey,
expiration: getRandTOut(redisShortTimeCache),
fetchFunc: async () => await dbController.ListPODLFiltAsync(lanciato, keyRichPart, idxMacchina, codGruppo, startDate, endDate) ?? new List<PODLExpModel>(),
tagList: [Utils.redisPOdlList]
);
}
/// <summary>
/// Elenco PODL per composizione KIT (Async) non avviati filtrati x articolo, KeyRich (che contiene stato)
/// </summary>
/// <param name="lanciato">Solo lanciati (1) o ancora disponibili (0)</param>
/// <param name="keyRichPart">KeyRich (parziale) da cercare (es cod stato x yacht)</param>
/// <param name="idxMacchina">Macchina</param>
/// <param name="codGruppo">Gruppo</param>
/// <param name="startDate">Data inizio</param>
/// <param name="endDate">Data fine</param>
/// <returns></returns>
public async Task<List<PODLExpModel>> POdlToKitListGetFiltAsync(bool lanciato, string keyRichPart, string idxMacchina, string codGruppo, DateTime startDate, DateTime endDate)
{
string redisKey = $"{Utils.redisPOdlList}_kit:{codGruppo}:{idxMacchina}:{keyRichPart}:{lanciato}:{startDate:yyyyMMdd_HHmmss}:{endDate:yyyyMMdd_HHmmss}";
return await GetOrFetchAsync(
operationName: "POdlToKitListGetFiltAsync",
cacheKey: redisKey,
expiration: getRandTOut(redisShortTimeCache * 5),
fetchFunc: async () =>
await dbController.ListPODL_KitFiltAsync(
lanciato,
keyRichPart,
idxMacchina,
codGruppo,
startDate,
endDate
) ?? new List<PODLExpModel>(),
tagList: [Utils.redisPOdlList, $"{Utils.redisPOdlList}_kit"]
);
}
/// <summary>
/// Chiamata salvataggio ricetta + refresh REDIS
/// </summary>
/// <param name="idxPODL"></param>
/// <param name="recipeName"></param>
/// <returns></returns>
public async Task<bool> POdlUpdateRecipe(int idxPODL, string recipeName)
{
using var activity = ActivitySource.StartActivity("POdlUpdateRecipe");
string source = "DB+REDIS";
bool answ = false;
answ = await dbController.PODL_updateRecipe(idxPODL, recipeName);
// reset redis...
if (answ)
{
await POdlFlushCache();
}
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"POdlUpdateRecipe | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return answ;
}
/// <summary>
/// Aggiornamento record selezionato
/// </summary>
/// <param name="currRec"></param>
/// <returns></returns>
public async Task<bool> POdlUpdateRecord(PODLModel currRec)
{
using var activity = ActivitySource.StartActivity("POdlUpdateRecord");
string source = "DB+REDIS";
var dbResult = await dbController.PODLUpdateRecord(currRec);
// elimino cache redis...
await POdlFlushCache();
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"POdlUpdateRecord | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return dbResult;
}
/// <summary>
/// Restituisce le statistiche di processo correnti x depluplica FluxLog
/// </summary>
/// <returns></returns>
public List<StatDedupDTO> ProcFLStats()
{
using var activity = ActivitySource.StartActivity("ProcFLStats");
string source = "REDIS";
List<StatDedupDTO> actStats = new List<StatDedupDTO>();
string currKey = $"{Utils.redisStatsProcFL}";
// recupero i record statistiche correnti
RedisValue rawData = redisDb.StringGet(currKey);
if (rawData.HasValue)
{
var rawStats = JsonConvert.DeserializeObject<List<StatDedupDTO>>($"{rawData}");
if (rawStats != null)
{
actStats = rawStats;
}
}
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"ProcFLStats | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return actStats;
}
/// <summary>
/// Ricerca ricetta su MongoDB dato PODL
/// </summary>
/// <param name="idxPODL"></param>
/// <returns></returns>
public async Task<RecipeModel?> RecipeGetByPODL(int idxPODL)
{
RecipeModel? result = null;
using var activity = ActivitySource.StartActivity("RecipeGetByPODL");
string source = "MongoDB";
result = await mongoController.RecipeGetByPODL(idxPODL);
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"RecipeGetByPODL | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return result;
}
/// <summary>
/// Salva ricetta su MongoDB
/// </summary>
/// <param name="currRecord"></param>
/// <returns></returns>
public async Task<bool> RecipeSetByPODL(RecipeModel currRecord)
{
using var activity = ActivitySource.StartActivity("RecipeSetByPODL");
string source = "DB+REDIS";
bool answ = false;
answ = await mongoController.RecipeSetByPODL(currRecord);
if (answ)
{
await POdlFlushCache();
}
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"RecipeSetByPODL | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return answ;
}
/// <summary>
/// Effettua conteggio chaivi REDIS dato pat2Flush ricerca
/// </summary>
/// <param name="keyPattern"></param>
/// <returns></returns>
public int RedisCountKey(string keyPattern)
{
using var activity = ActivitySource.StartActivity("RedisCountKey");
string source = "REDIS";
int num = 0;
keyPattern = (string.IsNullOrEmpty(keyPattern) ? "**" : keyPattern);
try
{
var listEndpoints = redisConnAdmin.GetEndPoints();
foreach (var endPoint in listEndpoints)
{
var server = redisConnAdmin.GetServer(endPoint);
foreach (RedisKey item in server.Keys(pattern: keyPattern, database: redisDb.Database, pageSize: 250, cursor: 0L))
{
num++;
}
}
}
catch (Exception arg)
{
Log.Error($"Eccezione in RedisCountKey{Environment.NewLine}{arg}");
}
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"RedisCountKey | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return num;
}
/// <summary>
/// Esegue eliminazione memoria redis keyVal
/// </summary>
/// <param name="keyVal"></param>
/// <returns></returns>
public bool RedisDelKey(string keyVal)
{
using var activity = ActivitySource.StartActivity("RedisDelKey");
string source = "REDIS";
bool answ = false;
var listEndpoints = redisConnAdmin.GetEndPoints();
foreach (var endPoint in listEndpoints)
{
var server = redisConnAdmin.GetServer(endPoint);
if (server != null)
{
redisDb.KeyDelete((RedisKey)keyVal);
answ = true;
}
}
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"RedisDelKey | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return answ;
}
/// <summary>
/// Reset della cache IO post operazioni come setup ODL...
/// </summary>
/// <param name="baseMem">Indirizzo base da cui rimuovere memoria cache</param>
/// <returns></returns>
public async Task<bool> ResetIoCache(string baseMem)
{
using var activity = ActivitySource.StartActivity("ResetIoCache");
string source = "REDIS";
// patterna a partire da cache IO...
RedisValue pattern = new RedisValue($"{MpIoNS}:*");
if (!string.IsNullOrEmpty(baseMem))
{
pattern = new RedisValue($"{MpIoNS}:{baseMem}:*");
}
bool answ = await ExecFlushRedisPatternAsync(pattern);
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"ResetIoCache | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return answ;
}
/// <summary>
/// Statistiche ODL calcolate (da stored stp_STAT_ODL)
/// </summary>
/// <returns></returns>
public Task<List<StatODLModel>> StatOdl(int IdxOdl)
{
using var activity = ActivitySource.StartActivity("StatOdl");
string source = "DB";
var result = dbController.OdlStart(IdxOdl);
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"StatOdl | {source} | {activity?.Duration.TotalMilliseconds}ms");
return result;
}
/// <summary>
/// Stato macchina
/// </summary>
/// <param name="idxMacchina"></param>
/// <returns></returns>
public async Task<StatoMacchineModel> StatoMacchinaAsync(string idxMacchina)
{
string currKey = $"{Utils.redisStatoMacch}:{idxMacchina}";
return await GetOrFetchAsync(
operationName: "StatoMacchinaAsync",
cacheKey: currKey,
expiration: getRandTOut(redisLongTimeCache),
fetchFunc: async () => await dbController.StatoMacchinaAsync(idxMacchina) ?? new(),
tagList: [Utils.redisStatoMacch]
);
}
/// <summary>
/// Elimina record + svuotamento cache
/// </summary>
/// <param name="currRecord"></param>
public async Task<bool> TemplateKitDeleteAsync(TemplateKitModel currRecord)
{
using var activity = ActivitySource.StartActivity("TemplateKitDeleteAsync");
string source = "DB";
bool fatto = false;
// salvo
fatto = dbController.TemplateKitDelete(currRecord);
await FlushCacheByTagAsync(Utils.redisKitTempl);
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"TemplateKitDeleteAsync | {source} | {activity?.Duration.TotalMilliseconds}ms");
return fatto;
}
/// <summary>
/// Elenco Template KIT da ricerca
/// </summary>
/// <param name="codParent">Codice articolo padre</param>
/// <param name="codChild">Codice articolo figlio</param>
/// <returns></returns>
public async Task<List<TemplateKitModel>> TemplateKitFiltAsync(string codParent, string codChild)
{
string currKey = $"{Utils.redisKitTempl}:{codParent}:{codChild}";
return await GetOrFetchAsync(
operationName: "TemplateKitFiltAsync",
cacheKey: currKey,
expiration: getRandTOut(redisLongTimeCache),
fetchFunc: async () => await dbController.TemplateKitFiltAsync(codParent, codChild) ?? new List<TemplateKitModel>(),
tagList: [Utils.redisKitTempl]
);
}
/// <summary>
/// Esegue salvataggio record + svuotamento cache
/// </summary>
/// <param name="currRecord"></param>
/// <param name="codAzienda"></param>
public async Task<bool> TemplateKitUpsertAsync(TemplateKitModel currRecord, string codAzienda)
{
using var activity = ActivitySource.StartActivity("TemplateKitUpsertAsync");
string source = "DB";
bool fatto = false;
// salvo
fatto = dbController.TemplateKitUpsert(currRecord, codAzienda);
await FlushCacheByTagAsync(Utils.redisKitTempl);
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"TemplateKitUpsertAsync | {source} | {activity?.Duration.TotalMilliseconds}ms");
return fatto;
}
/// <summary>
/// Punteggio compatibilità KIT per KeyFilt indicato
/// </summary>
/// <param name="KeyFilt"></param>
/// <param name="MaxResult"></param>
/// <param name="ForceDb"></param>
/// <returns></returns>
public async Task<List<TksScoreModel>> TksScoreAsync(string KeyFilt, int MaxResult, bool ForceDb)
{
string currKey = $"{Utils.redisKitScore}:{KeyFilt}:{MaxResult}";
if (ForceDb)
{
// Se ForceDb è true, saltiamo il GetOrFetchAsync per forzare il fetch dal DB
// e aggiornare la cache.
var result = await dbController.TksScoreAsync(KeyFilt, MaxResult) ?? new List<TksScoreModel>();
await _cache.SetAsync(currKey, result, TimeSpan.FromMinutes(redisLongTimeCache), tags: [Utils.redisKitScore]);
return result;
}
return await GetOrFetchAsync(
operationName: "TksScoreAsync",
cacheKey: currKey,
expiration: TimeSpan.FromMinutes(redisLongTimeCache),
fetchFunc: async () => await dbController.TksScoreAsync(KeyFilt, MaxResult) ?? new List<TksScoreModel>(),
tagList: [Utils.redisKitScore]
);
}
/// <summary>
/// Esegue traduzione dato vocabolario da Lingua + Lemma
/// </summary>
/// <param name="lemma"></param>
/// <param name="lingua"></param>
/// <returns></returns>
public string Traduci(string lemma, string lingua)
{
if (string.IsNullOrWhiteSpace(lemma)) return string.Empty;
if (string.IsNullOrWhiteSpace(lingua)) return lemma;
string linguaKey = lingua.ToLowerInvariant().Trim();
string cacheKey = $"vocab:{linguaKey}";
// FusionCache gestisce il lock e recupera l'intero dizionario della lingua.
// Se è in L1 (Memory), restituisce l'oggetto C# istantaneamente.
// Se non c'è, passa a L2 (Redis) o invoca la factory per caricarlo.
var dizionarioLingua = _cache.GetOrSet<Dictionary<string, string>>(
cacheKey,
_ => dbController.VocabolarioGetLang(linguaKey),
options => options
.SetDuration(TimeSpan.FromHours(8)) // Durata logica della cache
.SetFailSafe(true, TimeSpan.FromHours(1)) // Se Redis/DB è giù, usa i vecchi dati L1
);
// Ricerca O(1) nel dizionario in memoria
if (dizionarioLingua != null && dizionarioLingua.TryGetValue(lemma, out var traduzione))
{
return traduzione;
}
// Fallback: se la parola non è censita, restituisce il lemma originale
return lemma;
}
/// <summary>
/// Elimina record + svuotamento cache
/// </summary>
/// <param name="currRecord"></param>
public bool WipKitDelete(WipSetupKitModel currRecord)
{
using var activity = ActivitySource.StartActivity("WipKitDelete");
string source = "DB";
bool fatto = false;
// salvo
fatto = dbController.WipKitDelete(currRecord);
// svuoto cache
EmptyWipCache();
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"WipKitDelete Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return fatto;
}
/// <summary>
/// Elimina i record più vecchi della data-ora indicata
/// </summary>
/// <param name="DateLimit"></param>
public bool WipKitDeleteOlder(DateTime DateLimit)
{
using var activity = ActivitySource.StartActivity("WipKitDeleteOlder");
string source = "DB";
bool fatto = false;
// salvo
fatto = dbController.WipKitDeleteOlder(DateLimit);
// svuoto cache
EmptyWipCache();
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"WipKitDeleteOlder Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return fatto;
}
/// <summary>
/// Elenco Template KIT da ricerca
/// </summary>
/// <param name="KeyFilt"></param>
/// <returns></returns>
public async Task<List<WipSetupKitModel>> WipKitFiltAsync(string KeyFilt)
{
string currKey = $"{Utils.redisKitWip}:{KeyFilt}";
return await GetOrFetchAsync(
operationName: "WipKitFiltAsync",
cacheKey: currKey,
expiration: TimeSpan.FromMinutes(redisLongTimeCache),
fetchFunc: async () => await dbController.WipKitFiltAsync(KeyFilt) ?? new List<WipSetupKitModel>(),
tagList: [Utils.redisKitWip]
);
}
/// <summary>
/// Esegue salvataggio record + svuotamento cache
/// </summary>
/// <param name="currRecord"></param>
public bool WipKitUpsert(WipSetupKitModel currRecord)
{
using var activity = ActivitySource.StartActivity("WipKitUpsert");
string source = "DB";
bool fatto = false;
// salvo
fatto = dbController.WipKitUpsert(currRecord);
// svuoto cache KitWip
EmptyWipCache();
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"WipKitUpsert | {source} | {activity?.Duration.TotalMilliseconds}ms");
return fatto;
}
#endregion Public Methods
#region Protected Methods
/// <summary>
/// Restituisce un timeout dal valore secondi richiesti + tempo random +/-3%
/// </summary>
/// <param name="durationSec"></param>
/// <returns></returns>
protected TimeSpan getRandTOut(double durationSec)
{
double noise = (rand.NextDouble() * 0.06) - 0.03;
double rValue = durationSec * (1 + noise);
return TimeSpan.FromSeconds(rValue);
}
/// <summary>
/// Merge statistiche Dedup
/// </summary>
/// <param name="procStats"></param>
/// <returns></returns>
protected bool ProcDedupStatMerge(List<StatDedupDTO> procStats)
{
bool answ = false;
List<StatDedupDTO> actStats = ProcFLStats();
// se fosse vuoto --> add diretto
if (actStats.Count == 0)
{
actStats.AddRange(procStats);
}
else
{
// aggiorno su redis i record statistiche 1:1...
foreach (var recStat in procStats)
{
// cerco se ci fosse x aggiornare
var currRec = actStats.Where(x => x.IdxMacchina == recStat.IdxMacchina
&& x.CodFlux == recStat.CodFlux
&& x.Interval == recStat.Interval
&& x.Num4Int == recStat.Num4Int).FirstOrDefault();
// se trovato aggiorno
if (currRec != null)
{
currRec.ProcTime += recStat.ProcTime;
currRec.NumRec += recStat.NumRec;
}
// altrimenti aggiungo
else
{
actStats.Add(recStat);
}
}
}
// salvo record statistiche
var rawData = JsonConvert.SerializeObject(actStats);
string currKey = $"{Utils.redisStatsProcFL}";
redisDb.StringSet(currKey, rawData);
return answ;
}
/// <summary>
/// Merge statistiche DB Maintenance
/// </summary>
/// <param name="procStats"></param>
/// <returns></returns>
protected bool RecDbMaintStat(TimeSpan duration)
{
bool answ = false;
Dictionary<DateTime, double> actStats = DbDedupStats();
// aggiungo record!
actStats.Add(DateTime.Now, duration.TotalSeconds);
// salvo NUOVO record statistiche
string currKey = $"{Utils.redisStatsDbMaint}";
var rawData = JsonConvert.SerializeObject(actStats);
redisDb.StringSet(currKey, rawData);
return answ;
}
#endregion Protected Methods
#region Private Fields
/// <summary>
/// Oggetto per collezione dati Activity (span in Uptrace)
/// </summary>
private static readonly ActivitySource ActivitySource = new ActivitySource("MP.DATA.Tracer");
private static IConfiguration _configuration = null!;
private static Logger Log = LogManager.GetCurrentClassLogger();
private readonly IFusionCache _cache;
private DateTime _artCacheExpiry = DateTime.MinValue;
private Dictionary<string, string> _configData = new();
private DateTime _dtParamExpiry = DateTime.Now;
/// <summary>
/// Elenco CodArticolo usati in istanza KIT (per verifica eliminabilità)
/// </summary>
private HashSet<string> _listCodArtInKit = new();
/// <summary>
/// Elenco CodArticolo NON usati (per verifica eliminabilità)
/// </summary>
private HashSet<string> _listCodArtNotUsed = new();
/// <summary>
/// Elenco CodArticolo usati (per verifica eliminabilità)
/// </summary>
private HashSet<string> _listCodArtUsed = new();
private string canCacheParametri = "";
private string MpIoNS = "";
private Random rand = new Random();
/// <summary>
/// Oggetto per connessione a REDIS
/// </summary>
private ConnectionMultiplexer redisConn = null!;
/// <summary>
/// Oggetto per connessione a REDIS modalità admin (ex flux dati)
/// </summary>
private ConnectionMultiplexer redisConnAdmin = null!;
/// <summary>
/// Oggetto DB redis da impiegare x chiamate R/W
/// </summary>
private IDatabase redisDb = null!;
/// <summary>
/// Durata cache Lunga standard (300 sec)
/// </summary>
private int redisLongTimeCache = 300;
/// <summary>
/// Durata cache Breve standard (5 sec)
/// </summary>
private int redisShortTimeCache = 5;
/// <summary>
/// Soglia minima (ms) per log timing in console
/// </summary>
private double slowLogThresh = 0;
private bool traceEnabled = false;
#endregion Private Fields
#region Private Methods
/// <summary>
/// Svuota cache creazione KIT
/// </summary>
private void EmptyWipCache()
{
string pattern = $"{Utils.redisKitWip}:*";
if (!string.IsNullOrEmpty(pattern))
{
ExecFlushRedisPattern(pattern);
}
}
/// <summary>
/// Verifica caricamento dizionario ConfigData
/// </summary>
/// <returns></returns>
private async Task EnsureConfigLoadedAsync()
{
if (_configData.Count == 0)
{
var list = await ConfigGetAllAsync();
_configData = list
.GroupBy(x => x.Chiave)
.ToDictionary(g => g.Key, g => g.First().Valore);
}
}
/// <summary>
/// Verifica caricamento Vocabolario
/// </summary>
/// <returns></returns>
private async Task EnsureVocabolarioLoadedAsync()
{
if (_configData.Count == 0)
{
var list = await ConfigGetAllAsync();
_configData = list
.GroupBy(x => x.Chiave)
.ToDictionary(g => g.Key, g => g.First().Valore);
}
}
/// <summary>
/// Implementa gestione recupero cache da memoria o da obj esterno + cache memoria + tracking attività
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="cacheKey"></param>
/// <param name="fetchFunc"></param>
/// <param name="expiration"></param>
/// <returns></returns>
private async Task<T> GetOrFetchAsync<T>(string operationName, string cacheKey, Func<Task<T>> fetchFunc, TimeSpan expiration, params string[] tagList)
{
using var activity = ActivitySource.StartActivity(operationName);
string source;
var tryGet = await _cache.TryGetAsync<T>(cacheKey);
if (tryGet.HasValue)
{
source = "MEMORY";
var result = tryGet.Value!;
activity?.SetTag("data.source", source);
activity?.Stop();
// se supero la soglia loggo...
if (activity?.Duration.TotalMilliseconds > slowLogThresh)
{
LogTrace($"{operationName} | {source} | {activity?.Duration.TotalMilliseconds:F4} ms");
}
return result;
}
bool fromDb = false;
// cache in redis
var cacheOptions = new FusionCacheEntryOptions()
.SetDuration(expiration)
.SetFailSafe(true);
// cache in RAM per 1/3 del tempo x risparmiare risorse
cacheOptions.MemoryCacheDuration = expiration / 3;
var final = await _cache.GetOrSetAsync<T>(
cacheKey,
async _ =>
{
fromDb = true;
return await fetchFunc();
},
options: cacheOptions,
tags: tagList
);
source = fromDb ? "DB" : "REDIS";
activity?.SetTag("data.source", source);
activity?.Stop();
// switch log in base a source..
switch (source)
{
case "DB":
LogTrace($"{operationName} | {source} | {activity?.Duration.TotalMilliseconds:F4} ms", reqLevel: NLog.LogLevel.Info);
break;
case "REDIS":
LogTrace($"{operationName} | {source} | {activity?.Duration.TotalMilliseconds:F4} ms", reqLevel: NLog.LogLevel.Debug);
break;
default:
LogTrace($"{operationName} | {source} | {activity?.Duration.TotalMilliseconds:F4} ms");
break;
}
return final!;
}
/// <summary>
/// Helper trace messaggio log (SE abilitato)
/// </summary>
/// <param name="traceMsg"></param>
private void LogTrace(string traceMsg, NLog.LogLevel? reqLevel = null)
{
if (!traceEnabled)
return;
reqLevel ??= NLog.LogLevel.Trace;
// Loggo!
Log.Log(reqLevel, traceMsg);
}
private async Task<bool> POdlFlushCache()
{
using var activity = ActivitySource.StartActivity("POdlFlushCache");
bool answ = false;
RedisValue pattern = new RedisValue($"{Utils.redisXdlData}:*");
answ = await ExecFlushRedisPatternAsync(pattern);
pattern = new RedisValue($"{Utils.redisPOdlByOdl}:*");
answ = await ExecFlushRedisPatternAsync(pattern);
pattern = new RedisValue($"{Utils.redisPOdlByPOdl}:*");
answ = await ExecFlushRedisPatternAsync(pattern);
pattern = new RedisValue($"{Utils.redisPOdlList}:*");
answ = await ExecFlushRedisPatternAsync(pattern);
activity?.SetTag("data.source", "REDIS");
return answ;
}
private string redHashMpIO(string keyName)
{
string result = keyName;
try
{
result = $"{MpIoNS}:{keyName}".Replace("\\", "_");
}
catch (Exception exc)
{
Log.Error($"Errore in redHashMpIO{Environment.NewLine}{exc}");
}
return result;
}
private async Task resetCacheArticoli()
{
using var activity = ActivitySource.StartActivity("resetCacheArticoli");
RedisValue pattern = new RedisValue($"{Utils.redisArtByDossier}:*");
await ExecFlushRedisPatternAsync(pattern);
pattern = new RedisValue($"{Utils.redisArtList}:*");
await ExecFlushRedisPatternAsync(pattern);
// elimino anche in FusionCache
List<string> tags2del = new List<string>() { Utils.redisArtList, Utils.redisArtByDossier };
await FlushCacheByTagsAsync(tags2del);
activity?.SetTag("data.source", "REDIS");
}
private async Task ResetConfigCache()
{
await redisDb.StringSetAsync(Utils.redisConfKey, "");
List<string> tags2del = new List<string>() { Utils.redisConfKey };
await FlushCacheByTagsAsync(tags2del);
}
/// <summary>
/// Reset macchine e gruppi
/// </summary>
private void ResetMacGrpCache()
{
ExecFlushRedisPattern($"{Utils.redisAnagGruppi}:*");
ExecFlushRedisPattern($"{Utils.redisMacList}:*");
}
/// <summary>
/// Reset cache operatori e gruppi
/// </summary>
private void ResetOprGrpCache()
{
ExecFlushRedisPattern($"{Utils.redisAnagGruppi}:*");
ExecFlushRedisPattern($"{Utils.redisOprList}:*");
}
#endregion Private Methods
}
}