465 lines
18 KiB
C#
465 lines
18 KiB
C#
using EgwCoreLib.Lux.Data.Services.Internal;
|
|
using System.Runtime.CompilerServices;
|
|
|
|
namespace EgwCoreLib.Lux.Data.Services
|
|
{
|
|
/// <summary>
|
|
/// Classe base per i servizi che fornisce funzionalità comuni come
|
|
/// - connessione a Redis
|
|
/// - configurazione
|
|
/// - gestione dei messaggi
|
|
/// - strategie di caching.
|
|
///
|
|
/// Questa classe agisce come modello per altri servizi derivati.
|
|
/// </summary>
|
|
public class BaseServ
|
|
{
|
|
#region Public Constructors
|
|
|
|
/// <summary>
|
|
/// Inizializza una nuova istanza della classe BaseServ.
|
|
/// Configura la connessione Redis, carica le impostazioni di configurazione e inizializza il serializzatore JSON.
|
|
/// </summary>
|
|
/// <param name="Configuration">Oggetto di configurazione per recuperare le impostazioni dell'applicazione.</param>
|
|
/// <param name="RedisConn">Multiplexer di connessione Redis per operazioni sul database.</param>
|
|
public BaseServ(IConfiguration Configuration, IConnectionMultiplexer RedisConn)
|
|
{
|
|
_config = Configuration;
|
|
_redisConn = RedisConn;
|
|
_redisDb = _redisConn.GetDatabase();
|
|
// configuro la base key x la cache Redis, con verifica contenga Cache finale
|
|
_redisBaseKey = _config.GetValue<string>("ServerConf:RedisBaseKey") ?? "Lux:Cache";
|
|
// aggiungo cache se non finisse per ":cache"
|
|
if (!_redisBaseKey.EndsWith(":Cache"))
|
|
{
|
|
_redisBaseKey += ":Cache";
|
|
}
|
|
// setup tracing
|
|
// Verifica conf trace...
|
|
_traceEnabled = _config.GetValue<bool>("Otel:EnableTracing", false);
|
|
// receving channel name setup
|
|
chBom = _config.GetValue<string>("ServerConf:ChannelBom") ?? "lux:bom";
|
|
chHwList = _config.GetValue<string>("ServerConf:ChannelHwList") ?? "lux:hw:list";
|
|
chHwOpt = _config.GetValue<string>("ServerConf:ChannelHwOpt") ?? "lux:hw:opt";
|
|
chPng = _config.GetValue<string>("ServerConf:ChannelPng") ?? "lux:png:img";
|
|
chProfElem = _config.GetValue<string>("ServerConf:ChannelProfElem") ?? "lux:prof:elem";
|
|
chProfList = _config.GetValue<string>("ServerConf:ChannelProfList") ?? "lux:prof:list";
|
|
chProd = _config.GetValue<string>("ServerConf:ChannelProd") ?? "lux:prod";
|
|
chShape = _config.GetValue<string>("ServerConf:ChannelShape") ?? "lux:shape:curr";
|
|
chSvg = _config.GetValue<string>("ServerConf:ChannelSvg") ?? "lux:svg:img";
|
|
chUpdate = _config.GetValue<string>("ServerConf:ChannelUpdate") ?? "lux:update";
|
|
// Appends ":*" to the channels to enable wildcard subscription for dynamic events
|
|
fixRecChannel(ref chBom);
|
|
fixRecChannel(ref chHwList);
|
|
fixRecChannel(ref chHwOpt);
|
|
fixRecChannel(ref chPng);
|
|
fixRecChannel(ref chProfElem);
|
|
fixRecChannel(ref chProfList);
|
|
fixRecChannel(ref chProd);
|
|
fixRecChannel(ref chShape);
|
|
fixRecChannel(ref chSvg);
|
|
fixRecChannel(ref chUpdate);
|
|
|
|
// Configurazione serializzatore JSON per risolvere errore di loop circolare
|
|
JSSettings = new JsonSerializerSettings()
|
|
{
|
|
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
|
|
};
|
|
|
|
// Configurazione pipe dei messaggi
|
|
PipeBom = new MessagePipe(_redisConn, chBom);
|
|
PipeHwList = new MessagePipe(_redisConn, chHwList);
|
|
PipeHwOpt = new MessagePipe(_redisConn, chHwOpt);
|
|
PipePng = new MessagePipe(_redisConn, chPng);
|
|
PipeProd = new MessagePipe(_redisConn, chProd);
|
|
PipeProfElement = new MessagePipe(_redisConn, chProfElem);
|
|
PipeProfList = new MessagePipe(_redisConn, chProfList);
|
|
PipeShape = new MessagePipe(_redisConn, chShape);
|
|
PipeSvg = new MessagePipe(_redisConn, chSvg);
|
|
PipeUpdate = new MessagePipe(_redisConn, chUpdate);
|
|
}
|
|
|
|
#endregion Public Constructors
|
|
|
|
#region Public Properties
|
|
|
|
/// <summary>
|
|
/// Pipe dei messaggi per la comunicazione riguardo update calcolo BOM
|
|
/// </summary>
|
|
public MessagePipe PipeBom { get; set; } = null!;
|
|
|
|
/// <summary>
|
|
/// Pipe dei messaggi per ritorno HwList da Engine di calcolo verso interfaccia utente.
|
|
/// I messaggi vengono inviati sul canale Redis definito da ChannelHwList.
|
|
/// </summary>
|
|
public MessagePipe PipeHwList { get; set; } = null!;
|
|
|
|
/// <summary>
|
|
/// Pipe dei messaggi per ritorno HwOptions calcolate da Engine di calcolo verso interfaccia utente.
|
|
/// I messaggi vengono inviati sul canale Redis definito da ChannelHwOpt.
|
|
/// </summary>
|
|
public MessagePipe PipeHwOpt { get; set; } = null!;
|
|
|
|
/// <summary>
|
|
/// Pipe dei messaggi per ritorno PNG calcolati da Engine di calcolo verso interfaccia utente.
|
|
/// I messaggi vengono inviati sul canale Redis definito da ChannelPng.
|
|
/// </summary>
|
|
public MessagePipe PipePng { get; set; } = null!;
|
|
|
|
/// <summary>
|
|
/// Canale informazioni relativo ad attività relative alla gesitone PROD:
|
|
/// - carico macchine
|
|
/// - scheduling
|
|
/// </summary>
|
|
public MessagePipe PipeProd { get; set; } = null!;
|
|
|
|
/// <summary>
|
|
/// Pipe dei messaggi per ritorno info elementi del profile
|
|
/// </summary>
|
|
public MessagePipe PipeProfElement { get; set; } = null!;
|
|
|
|
/// <summary>
|
|
/// Pipe dei messaggi per ritorno ProfileListAsync calcolate da Engine di calcolo verso interfaccia utente.
|
|
/// I messaggi vengono inviati sul canale Redis definito da ChannelProfList.
|
|
/// </summary>
|
|
public MessagePipe PipeProfList { get; set; } = null!;
|
|
|
|
/// <summary>
|
|
/// Pipe dei messaggi per ritorno Shape calcolate da Engine di calcolo verso interfaccia utente.
|
|
/// I messaggi vengono inviati sul canale Redis definito da ChannelShape.
|
|
/// </summary>
|
|
public MessagePipe PipeShape { get; set; } = null!;
|
|
|
|
/// <summary>
|
|
/// Pipe dei messaggi per ritorno SVG calcolati da Engine di calcolo verso interfaccia utente.
|
|
/// I messaggi vengono inviati sul canale Redis definito da ChannelSvg.
|
|
/// </summary>
|
|
public MessagePipe PipeSvg { get; set; } = null!;
|
|
|
|
/// <summary>
|
|
/// Pipe dei messaggi per la comunicazione riguardo update generico UI
|
|
/// </summary>
|
|
public MessagePipe PipeUpdate { get; set; } = null!;
|
|
|
|
#endregion Public Properties
|
|
|
|
#region Protected Fields
|
|
|
|
/// <summary>
|
|
/// Oggetto per collezione dati Activity (span in Uptrace)
|
|
/// </summary>
|
|
protected static readonly ActivitySource ActivitySource = new ActivitySource("Lux.DATA");
|
|
|
|
/// <summary>
|
|
/// Oggetto logger utilizzato per registrare eventi e errori a livello di classe.
|
|
/// Utile per il monitoraggio del comportamento dell'applicazione e la risoluzione di problemi.
|
|
/// </summary>
|
|
protected static Logger Log = LogManager.GetCurrentClassLogger();
|
|
|
|
/// <summary>
|
|
/// Oggetto di configurazione statico per accedere alle impostazioni dell'applicazione (es. stringhe di connessione).
|
|
/// Condiviso tra tutte le istanze di BaseServ.
|
|
/// </summary>
|
|
protected readonly IConfiguration _config = null!;
|
|
|
|
/// <summary>
|
|
/// Path base chiavi REDIS
|
|
/// </summary>
|
|
protected readonly string _redisBaseKey = "Lux:Cache";
|
|
|
|
/// <summary>
|
|
/// Oggetto per la connessione a Redis utilizzato per operazioni di lettura/scrittura.
|
|
/// </summary>
|
|
protected readonly IConnectionMultiplexer _redisConn = null!;
|
|
|
|
/// <summary>
|
|
/// Database Redis utilizzato per le operazioni di lettura/scrittura
|
|
/// nb: ottenuto tramite _redisConn.GetDatabase()
|
|
/// </summary>
|
|
protected readonly IDatabase _redisDb = null!;
|
|
|
|
/// <summary>
|
|
/// Abilitazione operazioni tracing generiche
|
|
/// </summary>
|
|
protected readonly bool _traceEnabled = false;
|
|
|
|
/// <summary>
|
|
/// Impostazioni del serializzatore JSON utilizzato per gestire oggetti con riferimenti circolari
|
|
/// (es. oggetti che si fanno riferimento reciprocamente).
|
|
/// </summary>
|
|
protected JsonSerializerSettings? JSSettings;
|
|
|
|
#endregion Protected Fields
|
|
|
|
#region Protected Properties
|
|
|
|
/// <summary>
|
|
/// Durata della cache breve (circa 1 minuto + variazione del +/-10%)
|
|
/// </summary>
|
|
protected TimeSpan FastCache
|
|
{
|
|
get => TimeSpan.FromSeconds(cacheTtlShort * rnd.Next(900, 1100) / 1000);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Durata della cache lunga (+ variazione del +/-10%)
|
|
/// </summary>
|
|
protected TimeSpan LongCache
|
|
{
|
|
get => TimeSpan.FromSeconds(cacheTtlLong * rnd.Next(900, 1100) / 1000);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Durata della cache molto breve (circa 10 secondi + variazione del +/-10%)
|
|
/// </summary>
|
|
protected TimeSpan UltraFastCache
|
|
{
|
|
get => TimeSpan.FromSeconds(cacheTtlShort / 6 * rnd.Next(900, 1100) / 1000);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Durata della cache molto lunga (+ variazione del +/-10%)
|
|
/// </summary>
|
|
protected TimeSpan UltraLongCache
|
|
{
|
|
get => TimeSpan.FromSeconds(cacheTtlLong * 10 * rnd.Next(900, 1100) / 1000);
|
|
}
|
|
|
|
#endregion Protected Properties
|
|
|
|
#region Protected Methods
|
|
|
|
/// <summary>
|
|
/// Helper avvio attività per la funzione tracciata
|
|
/// </summary>
|
|
/// <param name="methodName"></param>
|
|
/// <returns></returns>
|
|
protected static Activity? StartActivity([CallerMemberName] string? methodName = null)
|
|
{
|
|
var activity = ActivitySource.StartActivity(methodName ?? "UNDEF");
|
|
activity?.SetTag("host.name", Environment.MachineName);
|
|
return activity;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invalida una o più chiavi/pattern in Redis
|
|
/// </summary>
|
|
protected async Task ClearCacheAsync(params string[] patterns)
|
|
{
|
|
foreach (var pattern in patterns)
|
|
{
|
|
// Chiamata al tuo metodo esistente
|
|
await ExecFlushRedisPatternAsync((RedisValue)pattern);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Metodo di flush dati cache Redis
|
|
/// </summary>
|
|
/// <param name="pattern"></param>
|
|
/// <returns></returns>
|
|
protected async Task ExecFlushRedisPatternAsync(RedisValue pattern)
|
|
{
|
|
// Qui inserisci la tua logica attuale (es. via Lua script o Keys/Scan)
|
|
// Esempio rapido via server scan:
|
|
var endpoints = _redisConn.GetEndPoints();
|
|
foreach (var endpoint in endpoints)
|
|
{
|
|
var server = _redisConn.GetServer(endpoint);
|
|
await foreach (var key in server.KeysAsync(_redisDb.Database, pattern))
|
|
{
|
|
await _redisDb.KeyDeleteAsync(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper generale di lettura da cache o da funzione (DB) con caching successivo
|
|
/// </summary>
|
|
/// <typeparam name="T"></typeparam>
|
|
/// <param name="key"></param>
|
|
/// <param name="factory"></param>
|
|
/// <param name="expiration"></param>
|
|
/// <returns></returns>
|
|
protected async Task<T> GetOrSetCacheAsync<T>(string key, Func<Task<T>> factory, TimeSpan? expiration = null, [CallerMemberName] string? caller = null)
|
|
{
|
|
using var activity = StartActivity();
|
|
string source = "DB";
|
|
|
|
// 1. Provo Redis
|
|
var cached = await _redisDb.StringGetAsync(key);
|
|
if (cached.HasValue)
|
|
{
|
|
source = "REDIS";
|
|
var cachedResult = JsonConvert.DeserializeObject<T>(cached!)!;
|
|
|
|
activity?.SetTag("data.source", source);
|
|
LogTrace($"{source} | trace: {activity?.TraceId} | {activity?.Duration.TotalMilliseconds}ms", NLog.LogLevel.Trace, caller);
|
|
|
|
return cachedResult;
|
|
}
|
|
|
|
// 2. Chiamo il factory (DB)
|
|
T result = await factory();
|
|
|
|
if (result != null)
|
|
{
|
|
// 3. Salva in Redis per la prossima volta
|
|
var serialized = JsonConvert.SerializeObject(result, JSSettings);
|
|
await _redisDb.StringSetAsync(key, serialized, expiration ?? LongCache);
|
|
}
|
|
|
|
// sistemo activity tracking data
|
|
activity?.SetTag("data.source", source);
|
|
activity?.Stop();
|
|
|
|
// log in console
|
|
LogTrace($"GetOrSetCacheAsync | {source} | trace: {activity?.TraceId} | {activity?.Duration.TotalMilliseconds:N3}ms", NLog.LogLevel.Trace, caller);
|
|
|
|
return result!;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper trace messaggio log (SE abilitato)
|
|
/// </summary>
|
|
/// <param name="traceMsg"></param>
|
|
/// <param name="reqLevel"></param>
|
|
/// <param name="methodName"></param>
|
|
protected void LogTrace(string traceMsg, NLog.LogLevel? reqLevel = null, [CallerMemberName] string? methodName = null)
|
|
{
|
|
if (!_traceEnabled)
|
|
return;
|
|
|
|
reqLevel ??= NLog.LogLevel.Debug;
|
|
|
|
// Loggo!
|
|
Log.Log(reqLevel, $"{methodName} | {traceMsg}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper generale per la telemetria e gestione eccezioni
|
|
/// </summary>
|
|
/// <typeparam name="T"></typeparam>
|
|
/// <param name="name"></param>
|
|
/// <param name="body"></param>
|
|
/// <param name="parameters"></param>
|
|
/// <returns></returns>
|
|
protected async Task<T> TraceAsync<T>(string name, Func<Activity?, Task<T>> body, object? parameters = null)
|
|
{
|
|
using var activity = ActivitySource.StartActivity(name);
|
|
try
|
|
{
|
|
if (parameters != null)
|
|
{
|
|
activity?.SetTag("params", JsonConvert.SerializeObject(parameters));
|
|
}
|
|
var result = await body(activity);
|
|
activity?.SetStatus(ActivityStatusCode.Ok);
|
|
activity?.Stop();
|
|
LogTrace($"TraceAsync | trace: {activity?.TraceId} | {activity?.Duration.TotalMilliseconds}ms", methodName: name);
|
|
return result;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
|
|
//Log.Error(ex, "Errore in {MethodName}", name);
|
|
LogTrace($"Errore in {name}", NLog.LogLevel.Error, name);
|
|
throw; // Riesponi l'eccezione per il tracking globale
|
|
}
|
|
}
|
|
|
|
#endregion Protected Methods
|
|
|
|
#region Private Fields
|
|
|
|
private static readonly ConcurrentDictionary<string, SemaphoreSlim> _locks = new();
|
|
|
|
/// <summary>
|
|
/// Durata della cache lunga in secondi (predefinito: 5 minuti)
|
|
/// Utilizzato nella proprietà LongCache per definire quanto a lungo i dati devono essere memorizzati in cache.
|
|
/// </summary>
|
|
private int cacheTtlLong = 60 * 5;
|
|
|
|
/// <summary>
|
|
/// Durata della cache breve in secondi (predefinito: 1 minuto)
|
|
/// Utilizzato nelle proprietà FastCache e UltraFastCache per definire la durata della cache breve.
|
|
/// </summary>
|
|
private int cacheTtlShort = 60 * 1;
|
|
|
|
/// <summary>
|
|
/// Redis channel for BOM related info
|
|
/// </summary>
|
|
private string chBom = "";
|
|
|
|
/// <summary>
|
|
/// Canale ritorno Hw List
|
|
/// </summary>
|
|
private string chHwList = "";
|
|
|
|
/// <summary>
|
|
/// Canale ritorno Hw Options
|
|
/// </summary>
|
|
private string chHwOpt = "";
|
|
|
|
/// <summary>
|
|
/// Nome del canale Redis utilizzato per l'invio/ricezione di messaggi relativi a img png.
|
|
/// Predefinito a "png:img" con suffisso ":*".
|
|
/// </summary>
|
|
private string chPng = "";
|
|
|
|
/// <summary>
|
|
/// Canale ritorno info relative ad attività PROD
|
|
/// </summary>
|
|
private string chProd = "";
|
|
|
|
/// <summary>
|
|
/// Canale ritorno Profile Elements
|
|
/// </summary>
|
|
private string chProfElem = "";
|
|
|
|
/// <summary>
|
|
/// Canale ritorno Profile List
|
|
/// </summary>
|
|
private string chProfList = "";
|
|
|
|
/// <summary>
|
|
/// Canale ritorno shape calcolate
|
|
/// </summary>
|
|
private string chShape = "";
|
|
|
|
/// <summary>
|
|
/// Nome del canale Redis utilizzato per l'invio/ricezione di messaggi relativi a img svg.
|
|
/// Predefinito a "svg:img" con suffisso ":*".
|
|
/// </summary>
|
|
private string chSvg = "";
|
|
|
|
/// <summary>
|
|
/// Nome del canale Redis utilizzato per l'invio/ricezione di messaggi di update
|
|
/// </summary>
|
|
private string chUpdate = "";
|
|
|
|
/// <summary>
|
|
/// Generatore di numeri casuali utilizzato per introdurre variabilità dinamica nelle durate della cache
|
|
/// (simula variazioni reali nella freschezza dei dati e nei tempi di scadenza).
|
|
/// </summary>
|
|
private Random rnd = new Random();
|
|
|
|
#endregion Private Fields
|
|
|
|
#region Private Methods
|
|
|
|
/// <summary>
|
|
/// Fix Receive Channel (ricerca "like")
|
|
/// </summary>
|
|
/// <param name="currCh"></param>
|
|
private void fixRecChannel(ref string currCh)
|
|
{
|
|
if (!currCh.EndsWith("*"))
|
|
{
|
|
currCh += "*";
|
|
}
|
|
}
|
|
|
|
#endregion Private Methods
|
|
}
|
|
} |