Review cache con FusionCache

This commit is contained in:
Samuele Locatelli
2026-05-27 08:51:30 +02:00
parent 20a16471a9
commit 9e4594f8b4
9 changed files with 397 additions and 101 deletions
+342 -90
View File
@@ -1,6 +1,5 @@
using EgwCoreLib.Utils;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using MP.Core.Conf;
using MP.Core.DTO;
using MP.Core.Objects;
@@ -14,6 +13,7 @@ using NLog;
using StackExchange.Redis;
using System.Data;
using System.Diagnostics;
using ZiggyCreatures.Caching.Fusion;
namespace MP.SPEC.Data
{
@@ -21,11 +21,11 @@ namespace MP.SPEC.Data
{
#region Public Constructors
public MpDataService(IConfiguration configuration, IMemoryCache memoryCache)
public MpDataService(IConfiguration configuration, IFusionCache cache)
{
// salvataggio oggetti
_configuration = configuration;
_memoryCache = memoryCache;
_cache = cache;
// Verifica conf trace...
traceEnabled = _configuration.GetValue<bool>("Otel:EnableTracing", false);
Log.Info($"MpDataService | INIT | Trace enabled: {traceEnabled}");
@@ -300,15 +300,16 @@ 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>())
);
return await GetOrFetchAsync(
operationName: "AnagStatiCommAsync",
cacheKey: Utils.redisStatoCom,
expiration: TimeSpan.FromMinutes(5),
fetchFunc: async () =>
await dbController.AnagStatiCommAsync() ?? new List<ListValuesModel>()
);
}
#if false
@@ -345,7 +346,7 @@ namespace MP.SPEC.Data
public async Task<List<ListValuesModel>> AnagTipoArtLV()
{
using var activity = ActivitySource.StartActivity("AnagStatiComm");
using var activity = ActivitySource.StartActivity("AnagStatiCommAsync");
string source = "DB";
List<ListValuesModel>? result = new List<ListValuesModel>();
// cerco in redis...
@@ -438,16 +439,28 @@ namespace MP.SPEC.Data
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)
return await GetOrFetchAsync(
operationName: "ArticoliGetByTipoAsync",
cacheKey: redisKey,
expiration: TimeSpan.FromMinutes(2),
fetchFunc: async () =>
await dbController.ArticoliGetByTipoAsync(tipo, azienda)
?? new List<AnagArticoliModel>()
);
);
#if false
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>()
);
#endif
}
#if false
public async Task<List<AnagArticoliModel>> ArticoliGetByTipoAsync(string tipo, string azienda = "*")
@@ -496,18 +509,161 @@ namespace MP.SPEC.Data
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)
return await GetOrFetchAsync(
operationName: "ArticoliGetSearchAsync",
cacheKey: redisKey,
expiration: TimeSpan.FromMinutes(2),
fetchFunc: async () =>
await dbController.ArticoliGetSearchAsync(numRecord, azienda, searchVal)
?? new List<AnagArticoliModel>()
);
);
#if false
return await GetOrCreateCachedAsync(
operationName: "ArticoliGetSearchAsync",
memKey: memKey,
redisKey: redisKey,
memoryTtl: TimeSpan.FromMinutes(2),
dbFactory: async () =>
await dbController.ArticoliGetSearchAsync(numRecord, azienda, searchVal)
?? new List<AnagArticoliModel>()
);
#endif
}
/// <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[] tags)
{
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);
LogTrace($"{operationName} | {source} | {activity?.Duration.TotalMilliseconds:F4} ms");
return result;
}
bool fromDb = false;
var final = await _cache.GetOrSetAsync<T>(
cacheKey,
async _ =>
{
fromDb = true;
return await fetchFunc();
},
opt =>
{
opt.SetDuration(expiration)
.SetFailSafe(true);
//if (tags != null && tags.Length > 0)
// opt.SetTags(tags);
});
source = fromDb ? "DB" : "REDIS";
activity?.SetTag("data.source", source);
LogTrace($"{operationName} | {source} | {activity?.Duration.TotalMilliseconds:F4} ms");
return final!;
}
#if false
private async Task<T> GetOrFetchAsync<T>(string operationName, string cacheKey, Func<Task<T>> fetchFunc, TimeSpan expiration)
{
using var activity = ActivitySource.StartActivity(operationName);
string source;
T result;
// ✅ 1. Tenta MEMORY / DISTRIBUTED (senza factory)
var memTry = await _cache.TryGetAsync<T>(cacheKey);
if (memTry.HasValue)
{
result = memTry.Value!;
source = "MEMORY";
}
else
{
bool fromDb = false;
// ✅ 2. fallback con factory
result = await _cache.GetOrSetAsync<T>(
cacheKey,
async _ =>
{
fromDb = true;
return await fetchFunc();
},
options => options.SetDuration(expiration)
)!;
source = fromDb ? "DB" : "REDIS";
}
// ✅ tracing e log
activity?.SetTag("data.source", source);
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:F4} ms");
return result;
}
#endif
/// <summary>
/// Cancellazione FusionCache (totale)
/// </summary>
/// <returns></returns>
public async Task<bool> FlushCacheAsync()
{
bool fatto = false;
await _cache.ClearAsync();
fatto = true;
return fatto;
}
/// <summary>
/// Cancellazione FusionCache dato elenco tags
/// </summary>
/// <returns></returns>
public async Task<bool> FlushCacheByTagsAsync(List<string> listTags)
{
bool fatto = false;
foreach (var item in listTags)
{
await _cache.RemoveByTagAsync(item);
}
fatto = true;
return fatto;
}
/// <summary>
/// Cancellazione FusionCache dato singolo tag
/// </summary>
/// <returns></returns>
public async Task<bool> FlushCacheByTagAsync(string tag)
{
bool fatto = false;
await _cache.RemoveByTagAsync(tag);
fatto = true;
return fatto;
}
#if false
/// <summary>
/// Helper per gestione cache a 3 livelli: MEMORY, REDIS e DB con opzioni
/// </summary>
@@ -527,12 +683,20 @@ namespace MP.SPEC.Data
string source = "NA";
T result;
string groupKey = memKey.Split(':')[0]; // es: "MACCHINE_MEM"
_memoryKeys.AddOrUpdate(
groupKey,
_ => new HashSet<string> { memKey },
(_, set) => { set.Add(memKey); return set; }
);
// ✅ default serializer (fallback)
serialize ??= (obj) => JsonConvert.SerializeObject(obj);
deserialize ??= (str) => JsonConvert.DeserializeObject<T>(str)!;
// ✅ 1. MEMORY
if (_memoryCache.TryGetValue(memKey, out T cached))
if (_cache.TryGetValue(memKey, out T cached))
{
result = cached;
source = "MEMORY";
@@ -540,24 +704,17 @@ namespace MP.SPEC.Data
else
{
// ✅ 2. MISS → factory
result = await _memoryCache.GetOrCreateAsync(memKey, async entry =>
result = await _cache.GetOrCreateAsync(memKey, async entry =>
{
entry.AbsoluteExpirationRelativeToNow = memoryTtl;
// 👉 REDIS
try
{
var rawData = await redisDb.StringGetAsync(redisKey);
var rawData = await redisDb.StringGetAsync(redisKey);
if (rawData.HasValue)
{
source = "REDIS";
return deserialize(rawData!);
}
}
catch (Exception ex)
if (rawData.HasValue)
{
LogTrace($"Redis error on {operationName}: {ex.Message}");
source = "REDIS";
return deserialize(rawData!);
}
// 👉 DB
@@ -567,18 +724,11 @@ namespace MP.SPEC.Data
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}");
}
await redisDb.StringSetAsync(
redisKey,
serialize(safeResult),
getRandTOut(redisLongTimeCache)
);
return safeResult;
})!;
@@ -595,8 +745,64 @@ namespace MP.SPEC.Data
LogTrace($"{operationName} | {source} | {activity?.Duration.TotalMilliseconds}ms");
return result;
}
}
/// <summary>
/// Invalidazione di una chiave in memoria e Redis
/// </summary>
/// <param name="memKey"></param>
/// <param name="redisKey"></param>
/// <returns></returns>
public async Task InvalidateCacheAsync(string memKey, string redisKey)
{
// ✅ memoria
_cache.Remove(memKey);
// ✅ redis
await redisDb.KeyDeleteAsync(redisKey);
LogTrace($"Cache invalidated | {memKey}");
}
private readonly ConcurrentDictionary<string, HashSet<string>> _memoryKeys = new();
/// <summary>
/// Invalidazione cache da pattern
/// </summary>
/// <param name="pattern"></param>
/// <returns></returns>
public async Task InvalidateCacheByPatternAsync(string pattern)
{
// ✅ MEMORY (pattern match semplice)
var keysToRemove = _memoryKeys.Keys
.Where(k => k.Contains(pattern.Replace("*", "")))
.ToList();
foreach (var key in keysToRemove)
{
if (_memoryKeys.TryRemove(key, out var subKeys))
{
foreach (var k in subKeys)
{
_cache.Remove(k);
}
}
}
// ✅ REDIS
var masterEndpoint = redisConn.GetEndPoints()
.Where(ep => redisConn.GetServer(ep).IsConnected && !redisConn.GetServer(ep).IsReplica)
.FirstOrDefault();
var server = redisConn.GetServer(masterEndpoint);
var redisKeys = server.Keys(pattern: pattern);
foreach (var rk in redisKeys)
{
await redisDb.KeyDeleteAsync(rk);
}
LogTrace($"Cache invalidated by pattern | {pattern}");
}
#endif
/// <summary>
/// Aggiornamento record selezionato
@@ -1798,7 +2004,7 @@ namespace MP.SPEC.Data
LogTrace($"MacchineGetFilt | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
return result;
}
private readonly IMemoryCache _memoryCache;
private readonly IFusionCache _cache;
/// <summary>
/// Elenco di tutte le macchine filtrate x gruppo
/// </summary>
@@ -1811,16 +2017,27 @@ namespace MP.SPEC.Data
string redisKey = $"{Utils.redisMacList}:{keyGrp}";
string memKey = $"MACCHINE_MEM:{keyGrp}";
return await GetOrCreateCachedAsync(
operationName: "MacchineGetFiltAsync",
memKey: memKey,
redisKey: redisKey,
// ✅ TTL coerente con il tuo requisito (prima avevi 1 minuto)
memoryTtl: TimeSpan.FromMinutes(1),
dbFactory: async () =>
await dbController.MacchineGetFiltAsync(codGruppo)
return await GetOrFetchAsync(
operationName: "MacchineGetFiltAsync",
cacheKey: redisKey,
expiration: TimeSpan.FromMinutes(5),
fetchFunc: async () =>
await dbController.MacchineGetFiltAsync(codGruppo)
?? new List<MacchineModel>()
);
);
#if false
return await GetOrCreateCachedAsync(
operationName: "MacchineGetFiltAsync",
memKey: memKey,
redisKey: redisKey,
// ✅ TTL coerente con il tuo requisito (prima avevi 1 minuto)
memoryTtl: TimeSpan.FromMinutes(1),
dbFactory: async () =>
await dbController.MacchineGetFiltAsync(codGruppo)
?? new List<MacchineModel>()
);
#endif
}
#if false
public async Task<List<MacchineModel>> MacchineGetFiltAsync(string codGruppo)
@@ -2196,23 +2413,41 @@ namespace MP.SPEC.Data
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 await GetOrFetchAsync(
operationName: "OdlGetCurrentAsync",
cacheKey: redisKey,
expiration: TimeSpan.FromSeconds(3),
fetchFunc: async () =>
{
var rawData = await dbController.OdlGetCurrentAsync();
var dbResult = rawData
.Select(x => x.IdxMacchina)
.Distinct()
.ToList();
return dbResult ?? new List<string>();
}
);
return dbResult ?? new List<string>();
}
);
#if false
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>();
}
);
#endif
}
#if false
@@ -2658,18 +2893,16 @@ namespace MP.SPEC.Data
/// <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}";
string redisKey = $"{Utils.redisPOdlList}_kit:{codGruppo}:{idxMacchina}:{keyRichPart}:{lanciato}:{startDate:yyyyMMdd_HHmmss}:{endDate:yyyyMMdd_HHmmss}";
// ✅ stessa chiave per memoria (puoi anche prefissare)
string memKey = $"MEM:{currKey}";
string memKey = $"MEM:{redisKey}";
return await GetOrCreateCachedAsync(
operationName: "POdlToKitListGetFiltAsync",
memKey: memKey,
redisKey: currKey,
// ✅ TTL RAM breve (coerente con redisShortTimeCache)
memoryTtl: TimeSpan.FromSeconds(redisShortTimeCache),
dbFactory: async () =>
return await GetOrFetchAsync(
operationName: "POdlToKitListGetFiltAsync",
cacheKey: redisKey,
expiration: TimeSpan.FromSeconds(redisShortTimeCache),
fetchFunc: async () =>
await dbController.ListPODL_KitFiltAsync(
lanciato,
keyRichPart,
@@ -2678,7 +2911,26 @@ namespace MP.SPEC.Data
startDate,
endDate
) ?? new List<PODLExpModel>()
);
);
#if false
return await GetOrCreateCachedAsync(
operationName: "POdlToKitListGetFiltAsync",
memKey: memKey,
redisKey: redisKey,
// ✅ 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>()
);
#endif
}
#if false
public async Task<List<PODLExpModel>> POdlToKitListGetFiltAsync(bool lanciato, string keyRichPart, string idxMacchina, string codGruppo, DateTime startDate, DateTime endDate)