Update caching SPEC

This commit is contained in:
Samuele Locatelli
2026-05-26 19:01:01 +02:00
parent 1cefa18895
commit 8c995d4c44
12 changed files with 272 additions and 91 deletions
+13 -13
View File
@@ -456,18 +456,18 @@ namespace MP.Data.Controllers
/// <param name="azienda"></param>
/// <param name="searchVal"></param>
/// <returns></returns>
public List<AnagArticoliModel> ArticoliGetSearch(int numRecord, string azienda = "*", string searchVal = "")
public async Task<List<AnagArticoliModel>> ArticoliGetSearchAsync(int numRecord, string azienda = "*", string searchVal = "")
{
List<AnagArticoliModel> dbResult = new List<AnagArticoliModel>();
using (var dbCtx = new MoonProContext(options))
{
dbResult = dbCtx
.DbSetArticoli
.AsNoTracking()
.Where(x => (azienda == "*" || x.Azienda.ToUpper() == azienda.ToUpper()) && (string.IsNullOrEmpty(searchVal) || x.CodArticolo.Contains(searchVal) || x.DescArticolo.Contains(searchVal) || x.Disegno.Contains(searchVal)))
.OrderBy(x => x.CodArticolo)
.Take(numRecord)
.ToList();
dbResult = await dbCtx
.DbSetArticoli
.AsNoTracking()
.Where(x => (azienda == "*" || x.Azienda.ToUpper() == azienda.ToUpper()) && (string.IsNullOrEmpty(searchVal) || x.CodArticolo.Contains(searchVal) || x.DescArticolo.Contains(searchVal) || x.Disegno.Contains(searchVal)))
.OrderBy(x => x.CodArticolo)
.Take(numRecord)
.ToListAsync();
}
return dbResult;
}
@@ -1891,15 +1891,15 @@ namespace MP.Data.Controllers
/// Recupero Odl CORRENTI
/// </summary>
/// <returns></returns>
public List<ODLModel> OdlGetCurrent()
public async Task<List<ODLModel>> OdlGetCurrentAsync()
{
List<ODLModel> dbResult = new List<ODLModel>();
using (var dbCtx = new MoonProContext(options))
{
dbResult = dbCtx
.DbSetODL
.Where(x => x.DataInizio != null && x.DataFine == null)
.ToList();
dbResult = await dbCtx
.DbSetODL
.Where(x => x.DataInizio != null && x.DataFine == null)
.ToListAsync();
}
return dbResult;
}
+2 -5
View File
@@ -70,10 +70,7 @@ namespace MP.Data.Services
}
else
{
result = dbController.ArticoliGetSearch(numRecord, azienda, searchVal);
#if false
result = await Task.FromResult(dbController.ArticoliGetSearch(numRecord, azienda, searchVal));
#endif
result = await dbController.ArticoliGetSearchAsync(numRecord, azienda, searchVal);
// serializzp e salvo...
rawData = JsonConvert.SerializeObject(result);
await _redisDb.StringSetAsync(currKey, rawData, LongCache);
@@ -83,7 +80,7 @@ namespace MP.Data.Services
result = new List<AnagArticoliModel>();
}
sw.Stop();
Log.Debug($"ArticoliGetSearch | azienda: {azienda} | searchVal: {searchVal} | numRecord: {numRecord} | {source} | {sw.Elapsed.TotalMilliseconds}ms");
Log.Debug($"ArticoliGetSearchAsync | azienda: {azienda} | searchVal: {searchVal} | numRecord: {numRecord} | {source} | {sw.Elapsed.TotalMilliseconds}ms");
return result;
}
+1 -1
View File
@@ -234,7 +234,7 @@ namespace MP.SPEC.Components
ListStati = await MDService.AnagStatiComm();
selAzienda = await MDService.ConfigTryGetAsync("AZIENDA");
giacenzeConf = await MDService.ConfigTryGetAsync("SPEC_ShowGiacenze");
ListArticoli = await MDService.ArticoliGetSearch(100000, selAzienda, "");
ListArticoli = await MDService.ArticoliGetSearchAsync(100000, selAzienda, "");
ListMacchine = MDService.MacchineGetFilt("*");
await ReloadData(true);
}
+14 -13
View File
@@ -204,6 +204,9 @@ namespace MP.SPEC.Components
ListRecords = null;
isLoading = true;
var list = await MDService.OdlGetCurrentAsync();
_odlCurrSet = list.ToHashSet();
var machines = await MDService.MacchineGetFiltAsync("*");
_machinesWithConf = machines
@@ -244,7 +247,7 @@ namespace MP.SPEC.Components
{
ListRecords = null;
isLoading = true;
await UpdateOdlList();
// verifico filtro odl...
if (actFilter.ShowKit)
{
@@ -259,8 +262,7 @@ namespace MP.SPEC.Components
totalCount = SearchRecords.Count;
}
ListRecords = SearchRecords.Skip(numRecord * (currPage - 1)).Take(numRecord).ToList();
await Task.Delay(1);
await InvokeAsync(() => StateHasChanged());
//await InvokeAsync(() => StateHasChanged());
isLoading = false;
}
@@ -356,6 +358,7 @@ namespace MP.SPEC.Components
private static Logger Log = LogManager.GetCurrentClassLogger();
private HashSet<string> _machinesWithArch = new();
private HashSet<string> _machinesWithConf = new();
private HashSet<string> _odlCurrSet = new();
private string currRecipeArchPath = "";
/// <summary>
@@ -567,18 +570,16 @@ namespace MP.SPEC.Components
/// <returns></returns>
private bool canStartOdl(string idxMacchina)
{
// controllo se lista scaduta...
bool answ = false;
DateTime adesso = DateTime.Now;
if (adesso > odlCurrExp || odlCurrList == null || odlCurrList.Count == 0)
{
odlCurrList = MDService.OdlGetCurrent();
odlCurrExp = adesso.AddSeconds(2);
}
answ = !odlCurrList.Contains(idxMacchina);
return answ;
return !_odlCurrSet.Contains(idxMacchina);
}
private async Task UpdateOdlList()
{
var list = await MDService.OdlGetCurrentAsync();
_odlCurrSet = list.ToHashSet();
}
/// <summary>
/// Verifica se la idxMaccSel abbia associata un path x ricette (elenco)
/// </summary>
+227 -44
View File
@@ -1,5 +1,6 @@
using EgwCoreLib.Utils;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using MP.Core.Conf;
using MP.Core.DTO;
using MP.Core.Objects;
@@ -20,10 +21,11 @@ namespace MP.SPEC.Data
{
#region Public Constructors
public MpDataService(IConfiguration configuration)
public MpDataService(IConfiguration configuration, IMemoryCache memoryCache)
{
// fix oggetto configurazion
// salvataggio oggetti
_configuration = configuration;
_memoryCache = memoryCache;
// Verifica conf trace...
traceEnabled = _configuration.GetValue<bool>("Otel:EnableTracing", false);
Log.Info($"MpDataService | INIT | Trace enabled: {traceEnabled}");
@@ -298,7 +300,18 @@ namespace MP.SPEC.Data
LogTrace($"AnagKeyValGetAll Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return result;
}
public async Task<List<ListValuesModel>> AnagStatiComm()
{
return await GetOrCreateCachedAsync(
operationName: "AnagStatiComm",
memKey: "ANAG_STATI_COMM_MEM",
redisKey: Utils.redisStatoCom,
memoryTtl: TimeSpan.FromMinutes(5),
dbFactory: () => Task.FromResult(dbController.AnagStatiComm() ?? new List<ListValuesModel>())
);
}
#if false
public async Task<List<ListValuesModel>> AnagStatiComm()
{
using var activity = ActivitySource.StartActivity("AnagStatiComm");
@@ -327,7 +340,8 @@ namespace MP.SPEC.Data
activity?.Stop();
LogTrace($"AnagStatiComm Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return result;
}
}
#endif
public async Task<List<ListValuesModel>> AnagTipoArtLV()
{
@@ -417,6 +431,25 @@ namespace MP.SPEC.Data
/// <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}:Tipo:{sKey}";
string memKey = $"MEM:{redisKey}";
return await GetOrCreateCachedAsync(
operationName: "ArticoliGetByTipoAsync",
memKey: memKey,
redisKey: redisKey,
// ✅ TTL lungo (dato abbastanza statico)
memoryTtl: TimeSpan.FromMinutes(5),
dbFactory: async () =>
await dbController.ArticoliGetByTipoAsync(tipo, azienda)
?? new List<AnagArticoliModel>()
);
}
#if false
public async Task<List<AnagArticoliModel>> ArticoliGetByTipoAsync(string tipo, string azienda = "*")
{
using var activity = ActivitySource.StartActivity("ArticoliGetByTipoAsync");
@@ -447,7 +480,8 @@ namespace MP.SPEC.Data
activity?.Stop();
LogTrace($"ArticoliGetByTipoAsync | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return result;
}
}
#endif
/// <summary>
/// Restitusice elenco articoli cercati
@@ -455,38 +489,115 @@ namespace MP.SPEC.Data
/// <param name="numRecord"></param>
/// <param name="searchVal"></param>
/// <returns></returns>
public async Task<List<AnagArticoliModel>> ArticoliGetSearch(int numRecord, string azienda, string searchVal)
public async Task<List<AnagArticoliModel>> ArticoliGetSearchAsync(int numRecord, string azienda, string searchVal)
{
using var activity = ActivitySource.StartActivity("ArticoliGetSearch");
List<AnagArticoliModel>? result = new List<AnagArticoliModel>();
string source = "DB";
string sKey = string.IsNullOrEmpty(searchVal) ? "***" : searchVal;
string currKey = $"{Utils.redisArtList}:{azienda}:{sKey}";
// cerco in redis dato valore sel idxMaccSel...
RedisValue rawData = redisDb.StringGet(currKey);
if (rawData.HasValue)
string sKey = string.IsNullOrWhiteSpace(searchVal) ? "***" : searchVal.Trim();
string memKey = $"ART_SEARCH_MEM:{azienda}:{sKey}:{numRecord}";
string redisKey = $"{Utils.redisArtList}:{azienda}:{sKey}:{numRecord}";
return await GetOrCreateCachedAsync(
operationName: "ArticoliGetSearchAsync",
memKey: memKey,
redisKey: redisKey,
memoryTtl: TimeSpan.FromMinutes(2),
dbFactory: async () =>
await dbController.ArticoliGetSearchAsync(numRecord, azienda, searchVal)
?? new List<AnagArticoliModel>()
);
}
/// <summary>
/// Helper per gestione cache a 3 livelli: MEMORY, REDIS e DB con opzioni
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="operationName"></param>
/// <param name="memKey"></param>
/// <param name="redisKey"></param>
/// <param name="memoryTtl"></param>
/// <param name="dbFactory"></param>
/// <param name="serialize"></param>
/// <param name="deserialize"></param>
/// <returns></returns>
private async Task<T> GetOrCreateCachedAsync<T>(string operationName, string memKey, string redisKey, TimeSpan memoryTtl, Func<Task<T>> dbFactory, Func<T, string>? serialize = null, Func<string, T>? deserialize = null)
{
using var activity = ActivitySource.StartActivity(operationName);
string source = "NA";
T result;
// ✅ default serializer (fallback)
serialize ??= (obj) => JsonConvert.SerializeObject(obj);
deserialize ??= (str) => JsonConvert.DeserializeObject<T>(str)!;
// ✅ 1. MEMORY
if (_memoryCache.TryGetValue(memKey, out T cached))
{
result = JsonConvert.DeserializeObject<List<AnagArticoliModel>>($"{rawData}");
source = "REDIS";
result = cached;
source = "MEMORY";
}
else
{
result = await Task.FromResult(dbController.ArticoliGetSearch(numRecord, azienda, searchVal));
// serializzo e salvo...
rawData = JsonConvert.SerializeObject(result);
redisDb.StringSet(currKey, rawData, getRandTOut(redisLongTimeCache / 5));
}
if (result == null)
{
result = new List<AnagArticoliModel>();
// ✅ 2. MISS → factory
result = await _memoryCache.GetOrCreateAsync(memKey, async entry =>
{
entry.AbsoluteExpirationRelativeToNow = memoryTtl;
// 👉 REDIS
try
{
var rawData = await redisDb.StringGetAsync(redisKey);
if (rawData.HasValue)
{
source = "REDIS";
return deserialize(rawData!);
}
}
catch (Exception ex)
{
LogTrace($"Redis error on {operationName}: {ex.Message}");
}
// 👉 DB
source = "DB";
var dbResult = await dbFactory();
var safeResult = dbResult == null ? default! : dbResult;
try
{
await redisDb.StringSetAsync(
redisKey,
serialize(safeResult),
getRandTOut(redisLongTimeCache)
);
}
catch (Exception ex)
{
LogTrace($"Redis SET error on {operationName}: {ex.Message}");
}
return safeResult;
})!;
}
// ✅ logging e tracing centralizzati
activity?.SetTag("data.source", source);
activity?.SetTag("result.count", result.Count);
activity?.Stop();
LogTrace($"ArticoliGetSearch | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
if (result is System.Collections.ICollection coll)
activity?.SetTag("result.count", coll.Count);
else
activity?.SetTag("result.count", result != null ? 1 : 0);
LogTrace($"{operationName} | {source} | {activity?.Duration.TotalMilliseconds}ms");
return result;
}
/// <summary>
/// Aggiornamento record selezionato
/// </summary>
@@ -704,7 +815,7 @@ namespace MP.SPEC.Data
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"ConfigTryGetAsync | {keyName} | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
LogTrace($"ConfigTryGetAsync | {keyName} | {source} | {activity?.Duration.TotalMilliseconds}ms");
return value ?? "";
}
@@ -1687,7 +1798,7 @@ namespace MP.SPEC.Data
LogTrace($"MacchineGetFilt | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return result;
}
private readonly IMemoryCache _memoryCache;
/// <summary>
/// Elenco di tutte le macchine filtrate x gruppo
/// </summary>
@@ -1695,32 +1806,51 @@ namespace MP.SPEC.Data
/// <returns></returns>
public async Task<List<MacchineModel>> MacchineGetFiltAsync(string codGruppo)
{
using var activity = ActivitySource.StartActivity("MacchineGetFilt");
List<MacchineModel>? result = new List<MacchineModel>();
using var activity = ActivitySource.StartActivity("MacchineGetFiltAsync");
string source = "DB";
string keyGrp = codGruppo != "*" ? codGruppo : "ALL";
string currKey = $"{Utils.redisMacList}:{keyGrp}";
// cerco in redis dato valore sel idxMaccSel...
RedisValue rawData = await redisDb.StringGetAsync(currKey);
string memKey = $"MACCHINE_MEM:{keyGrp}";
string redisKey = $"{Utils.redisMacList}:{keyGrp}";
// ✅ 1. MEMORY CACHE
if (_memoryCache.TryGetValue(memKey, out List<MacchineModel> cached))
{
source = "MEMORY";
activity?.SetTag("data.source", source);
activity?.Stop();
LogTrace($"MacchineGetFiltAsync | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return cached;
}
List<MacchineModel> result;
// ✅ 2. REDIS
var rawData = await redisDb.StringGetAsync(redisKey);
if (rawData.HasValue)
{
result = JsonConvert.DeserializeObject<List<MacchineModel>>($"{rawData}");
result = JsonConvert.DeserializeObject<List<MacchineModel>>(rawData!) ?? new();
source = "REDIS";
}
else
{
// ✅ 3. DB
result = await dbController.MacchineGetFiltAsync(codGruppo);
// serializzo e salvo...
rawData = JsonConvert.SerializeObject(result);
await redisDb.StringSetAsync(currKey, rawData, getRandTOut(redisLongTimeCache));
}
if (result == null)
{
result = new List<MacchineModel>();
await redisDb.StringSetAsync(
redisKey,
JsonConvert.SerializeObject(result),
getRandTOut(redisLongTimeCache)
);
}
// ✅ salva in RAM (IMPORTANTISSIMO), TTL 1 minuto
_memoryCache.Set(memKey, result, TimeSpan.FromMinutes(1));
activity?.SetTag("data.source", source);
activity?.SetTag("result.count", 1);
activity?.Stop();
LogTrace($"MacchineGetFiltAsync | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return result;
}
@@ -2040,6 +2170,32 @@ namespace MP.SPEC.Data
/// </summary>
/// <param name="idxMacchina"></param>
/// <returns></returns>
public async Task<List<string>> OdlGetCurrentAsync()
{
string redisKey = Utils.redisOdlCurrByMac;
string memKey = $"MEM:{redisKey}";
return await GetOrCreateCachedAsync(
operationName: "OdlGetCurrentAsync",
memKey: memKey,
redisKey: redisKey,
// ✅ TTL molto corto (come avevi: 3 secondi)
memoryTtl: TimeSpan.FromSeconds(3),
dbFactory: async () =>
{
var rawData = await dbController.OdlGetCurrentAsync();
var dbResult = rawData
.Select(x => x.IdxMacchina)
.Distinct()
.ToList();
return dbResult ?? new List<string>();
}
);
}
#if false
public List<string> OdlGetCurrent()
{
using var activity = ActivitySource.StartActivity("OdlGetCurrent");
@@ -2068,7 +2224,8 @@ namespace MP.SPEC.Data
activity?.Stop();
LogTrace($"OdlGetCurrent | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return dbResult;
}
}
#endif
/// <summary>
/// elenco TUTTI gli ODL
@@ -2479,6 +2636,31 @@ namespace MP.SPEC.Data
/// <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 currKey = $"{Utils.redisPOdlList}_kit:{codGruppo}:{idxMacchina}:{keyRichPart}:{lanciato}:{startDate:yyyyMMdd_HHmmss}:{endDate:yyyyMMdd_HHmmss}";
// ✅ stessa chiave per memoria (puoi anche prefissare)
string memKey = $"MEM:{currKey}";
return await GetOrCreateCachedAsync(
operationName: "POdlToKitListGetFiltAsync",
memKey: memKey,
redisKey: currKey,
// ✅ TTL RAM breve (coerente con redisShortTimeCache)
memoryTtl: TimeSpan.FromSeconds(redisShortTimeCache),
dbFactory: async () =>
await dbController.ListPODL_KitFiltAsync(
lanciato,
keyRichPart,
idxMacchina,
codGruppo,
startDate,
endDate
) ?? new List<PODLExpModel>()
);
}
#if false
public async Task<List<PODLExpModel>> POdlToKitListGetFiltAsync(bool lanciato, string keyRichPart, string idxMacchina, string codGruppo, DateTime startDate, DateTime endDate)
{
using var activity = ActivitySource.StartActivity("POdlToKitListGetFiltAsync");
@@ -2508,7 +2690,8 @@ namespace MP.SPEC.Data
activity?.Stop();
LogTrace($"POdlToKitListGetFiltAsync | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return result;
}
}
#endif
/// <summary>
/// Chiamata salvataggio ricetta + refresh REDIS
+1 -1
View File
@@ -5,7 +5,7 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>MP.SPEC</RootNamespace>
<Version>8.16.2605.2617</Version>
<Version>8.16.2605.2619</Version>
<UserSecretsId>1800a78a-6ff1-40f9-b490-87fb8bfc1394</UserSecretsId>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
</PropertyGroup>
+1 -1
View File
@@ -335,7 +335,7 @@ namespace MP.SPEC.Pages
private async Task ReloadData()
{
isLoading = true;
SearchRecords = await MDService.ArticoliGetSearch(100000, selAzienda, SearchVal);
SearchRecords = await MDService.ArticoliGetSearchAsync(100000, selAzienda, SearchVal);
ListRecords = SearchRecords.Skip(numRecord * (currPage - 1)).Take(numRecord).ToList();
isLoading = false;
}
+4 -8
View File
@@ -1,17 +1,14 @@
#if false
using Blazored.LocalStorage;
#endif
using EgwCoreLib.Razor;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using MP.Data.DbModels;
using MP.SPEC.Components;
using MP.Data.Services;
using MP.SPEC.Data;
using MP.SPEC.Services;
using NLog;
using System.Reflection.PortableExecutable;
using EgwCoreLib.Razor;
using Microsoft.AspNetCore.DataProtection;
using MP.Data.Services;
namespace MP.SPEC.Pages
{
@@ -501,9 +498,8 @@ namespace MP.SPEC.Pages
private async Task ReloadData()
{
isLoading = true;
await Task.Delay(1);
ListMacchine = MDService.MacchineGetFilt(selReparto);
ListArticoli = await MDService.ArticoliGetSearch(100, currAzienda, artSearch);
ListMacchine = await MDService.MacchineGetFiltAsync(selReparto);
ListArticoli = await MDService.ArticoliGetSearchAsync(100, currAzienda, artSearch);
if (ListGruppiFase != null)
{
var firstGroup = ListGruppiFase.Where(x => x.CodGruppo == selReparto).FirstOrDefault();
+6 -2
View File
@@ -132,9 +132,13 @@ builder.Services.AddAuthorization(options =>
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
// redis preliminare
builder.Services.AddSingleton<IConnectionMultiplexer>(redisMultiplexer);
builder.Services.AddRazorPages();
// memory + redis preliminare
builder.Services.AddMemoryCache();
builder.Services.AddSingleton<IConnectionMultiplexer>(redisMultiplexer);
// altri servizi
builder.Services.AddSingleton<MpDataService>();
builder.Services.AddSingleton<ListSelectDataSrv>();
builder.Services.AddSingleton<IOApiService>();
+1 -1
View File
@@ -1,6 +1,6 @@
<body>
<i>Modulo MAPOSPEC </i>
<h4>Versione: 8.16.2605.2617</h4>
<h4>Versione: 8.16.2605.2619</h4>
<br /> Note di rilascio:
<ul>
<li>
+1 -1
View File
@@ -1 +1 @@
8.16.2605.2617
8.16.2605.2619
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>8.16.2605.2617</version>
<version>8.16.2605.2619</version>
<url>https://nexus.steamware.net/repository/SWS/MP-SPEC/stable/LAST/MP.SPEC.zip</url>
<changelog>https://nexus.steamware.net/repository/SWS/MP-SPEC/stable/LAST/ChangeLog.html</changelog>
<mandatory>false</mandatory>