Merge branch 'Release/CommonFusionCacheService_01'

This commit is contained in:
Samuele Locatelli
2026-06-22 17:25:28 +02:00
59 changed files with 375 additions and 790 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<Version>8.16.2606.1312</Version>
<Version>8.16.2606.2208</Version>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>MP_TAB3</RootNamespace>
</PropertyGroup>
+1 -1
View File
@@ -1,6 +1,6 @@
<body>
<i>Modulo MAPOSPEC </i>
<h4>Versione: 8.16.2606.1312</h4>
<h4>Versione: 8.16.2606.2208</h4>
<br /> Note di rilascio:
<ul>
<li>
+1 -1
View File
@@ -1 +1 @@
8.16.2606.1312
8.16.2606.2208
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>8.16.2606.1312</version>
<version>8.16.2606.2208</version>
<url>https://nexus.steamware.net/repository/SWS/MP-TAB3/stable/LAST/MP-TAB3.zip</url>
<changelog>https://nexus.steamware.net/repository/SWS/MP-TAB3/stable/LAST/ChangeLog.html</changelog>
<mandatory>false</mandatory>
-1
View File
@@ -945,7 +945,6 @@ namespace MP.Data.Controllers
#region Private Fields
private static NLog.Logger Log = LogManager.GetCurrentClassLogger();
private DbContextOptions<MoonPro_FluxContext> optionsFlux;
#endregion Private Fields
}
@@ -55,6 +55,25 @@ namespace MP.Data
return services;
}
/// <summary>
/// Aggiunta repository/servizi specifici per RIOC
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddRIocDataLayer(this IServiceCollection services)
{
// Repository Scoped x registrazione statistiche chiamate
services.TryAddScoped<IStatsAggrRepository, StatsAggrRepository>();
services.TryAddScoped<IStatsDetailRepository, StatsDetailRepository>();
// Servizi Scoped statistiche
services.TryAddScoped<IStatsAggrService, StatsAggrService>();
services.TryAddScoped<IStatsDetailService, StatsDetailService>();
return services;
}
/// <summary>
/// Aggiunta repository/servizi specifici per LAND
/// </summary>
-1
View File
@@ -557,7 +557,6 @@ namespace MP.Data.Services.IOC
/// <returns></returns>
private async Task<bool> IobSLogEnabAsync(string idxMacchina)
{
bool answ = false;
// ORA recupero da memoria redis...
var rKey = MP.Data.Utils.RedKeyDatiMacc(idxMacchina, MpIoNS);
RedisValue rawData = await _redisDb.HashGetAsync(rKey, (RedisValue)"sLogEnabled");
@@ -1,6 +1,8 @@
using MP.Core.DTO;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace MP.IOC.Services
namespace MP.Data.Services.RouteWeight
{
public interface IWeightProvider
{
@@ -1,14 +1,15 @@
using MP.Core.DTO;
using Microsoft.Extensions.Configuration;
using MP.Core.DTO;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace MP.IOC.Services
namespace MP.Data.Services.RouteWeight
{
public class InMemoryWeightProvider : IWeightProvider
{
private readonly ConcurrentDictionary<string, (int oldW, int newW)> _map = new();
private readonly int _defaultOld;
private readonly int _defaultNew;
#region Public Constructors
public InMemoryWeightProvider(IConfiguration config)
{
@@ -16,16 +17,9 @@ namespace MP.IOC.Services
_defaultNew = config.GetValue<int>("RouteMan:DefaultWeightNew", 0);
}
public (int oldWeight, int newWeight) GetWeightsFor(string method)
{
if (string.IsNullOrEmpty(method)) method = "unknown";
return _map.GetOrAdd(method, _ => (_defaultOld, _defaultNew));
}
#endregion Public Constructors
public void SetWeights(string method, int oldWeight, int newWeight)
{
_map[method] = (Math.Clamp(oldWeight, 0, 100), Math.Clamp(newWeight, 0, 100));
}
#region Public Methods
public async Task<List<WeightDTO>> GetAllWeightsAsync()
{
@@ -44,6 +38,17 @@ namespace MP.IOC.Services
return result;
}
public (int oldWeight, int newWeight) GetWeightsFor(string method)
{
if (string.IsNullOrEmpty(method)) method = "unknown";
return _map.GetOrAdd(method, _ => (_defaultOld, _defaultNew));
}
public void SetWeights(string method, int oldWeight, int newWeight)
{
_map[method] = (Math.Clamp(oldWeight, 0, 100), Math.Clamp(newWeight, 0, 100));
}
public bool UpsertWeight(WeightDTO updRecord)
{
if (updRecord == null || string.IsNullOrEmpty(updRecord.Method))
@@ -53,7 +58,15 @@ namespace MP.IOC.Services
return true;
}
#endregion Public Methods
#region Private Fields
private readonly int _defaultNew;
private readonly int _defaultOld;
private readonly ConcurrentDictionary<string, (int oldW, int newW)> _map = new();
#endregion Private Fields
}
}
}
@@ -1,14 +1,24 @@
using MP.Core.DTO;
using Microsoft.Extensions.Configuration;
using MP.Core.DTO;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ZiggyCreatures.Caching.Fusion;
namespace MP.IOC.Services
namespace MP.Data.Services.RouteWeight
{
public class RedisWeightProvider : IWeightProvider
{
#region Public Constructors
public RedisWeightProvider(IConnectionMultiplexer mux, IConfiguration config)
public RedisWeightProvider(
IConnectionMultiplexer mux,
IFusionCache cache,
IConfiguration config)
{
_cache = cache;
_config = config;
_db = mux.GetDatabase();
_mux = mux;
@@ -23,59 +33,16 @@ namespace MP.IOC.Services
#region Public Methods
/// <summary>
/// Ritorna (oldWeight, newWeight) per il metodo. Se non esiste, crea la chiave con i default.
/// Eliminazione esplicita cache data chiave
/// </summary>
public (int oldWeight, int newWeight) GetWeightsFor(string method)
/// <param name="method"></param>
public void EvictLocalCacheFor(string method)
{
if (string.IsNullOrEmpty(method)) method = "unknown";
var key = _keyPrefix + method;
if (string.IsNullOrEmpty(method)) return;
// Leggi entrambi i campi
var oldVal = _db.HashGet(key, "old");
var newVal = _db.HashGet(key, "new");
// Se entrambi mancanti, inizializza con default (usando HSet con When.NotExists per evitare overwrite)
if (oldVal.IsNull && newVal.IsNull)
{
// Imposta i campi singolarmente con When.NotExists per evitare overwrite
_db.HashSet(key, "old", _defaultOld, When.NotExists);
_db.HashSet(key, "new", _defaultNew, When.NotExists);
// Rileggi per essere sicuri
oldVal = _db.HashGet(key, "old");
newVal = _db.HashGet(key, "new");
}
// Se uno dei due manca, impostalo al default (non sovrascrive l'altro)
if (oldVal.IsNull)
{
_db.HashSet(key, "old", _defaultOld, When.NotExists);
oldVal = _defaultOld;
}
if (newVal.IsNull)
{
_db.HashSet(key, "new", _defaultNew, When.NotExists);
newVal = _defaultNew;
}
if (!int.TryParse(oldVal.ToString(), out var oldW)) oldW = _defaultOld;
if (!int.TryParse(newVal.ToString(), out var newW)) newW = _defaultNew;
// clamp 0..100
oldW = Math.Clamp(oldW, 0, 100);
newW = Math.Clamp(newW, 0, 100);
return (oldW, newW);
}
// API per aggiornare i pesi a runtime (opzionale)
public void SetWeights(string method, int oldWeight, int newWeight)
{
var key = _keyPrefix + (string.IsNullOrEmpty(method) ? "unknown" : method);
_db.HashSet(key, new HashEntry[] {
new HashEntry("old", Math.Clamp(oldWeight,0,100)),
new HashEntry("new", Math.Clamp(newWeight,0,100))
});
var cacheKey = $"weights:{method}";
// Rimuove la chiave dalla RAM (L1) dell'istanza corrente del Gateway
_cache.Remove(cacheKey);
}
public async Task<List<WeightDTO>> GetAllWeightsAsync()
@@ -117,20 +84,164 @@ namespace MP.IOC.Services
return result;
}
/// <summary>
/// Ritorna (oldWeight, newWeight) per il metodo con FusionCache e poi con dati Redis.
/// Se non esiste, crea la chiave con i default value.
/// </summary>
public (int oldWeight, int newWeight) GetWeightsFor(string method)
{
if (string.IsNullOrEmpty(method)) method = "unknown";
var cacheKey = $"weights:{method}";
// FusionCache gestisce L1, L2 e se non trova nulla esegue la factory sotto
var weights = _cache.GetOrSet<RouteWeights>(
cacheKey,
_ =>
{
// FACTORY DI INIZIALIZZAZIONE (Viene eseguita solo se la cache è vuota ovunque)
var redisKey = _keyPrefix + method;
var oldVal = _db.HashGet(redisKey, "old");
var newVal = _db.HashGet(redisKey, "new");
if (oldVal.IsNull && newVal.IsNull)
{
_db.HashSet(redisKey, "old", _defaultOld, When.NotExists);
_db.HashSet(redisKey, "new", _defaultNew, When.NotExists);
oldVal = _defaultOld;
newVal = _defaultNew;
}
if (oldVal.IsNull) { _db.HashSet(redisKey, "old", _defaultOld, When.NotExists); oldVal = _defaultOld; }
if (newVal.IsNull) { _db.HashSet(redisKey, "new", _defaultNew, When.NotExists); newVal = _defaultNew; }
if (!int.TryParse(oldVal.ToString(), out var oldW)) oldW = _defaultOld;
if (!int.TryParse(newVal.ToString(), out var newW)) newW = _defaultNew;
return new RouteWeights(Math.Clamp(oldW, 0, 100), Math.Clamp(newW, 0, 100));
},
// Durata specifica per questa tipologia di dato
options => options.SetDuration(TimeSpan.FromMinutes(10))
);
return (weights.OldWeight, weights.NewWeight);
#if false
var key = _keyPrefix + method;
// Leggi entrambi i campi
var oldVal = _db.HashGet(key, "old");
var newVal = _db.HashGet(key, "new");
// Se entrambi mancanti, inizializza con default (usando HSet con When.NotExists per evitare overwrite)
if (oldVal.IsNull && newVal.IsNull)
{
// Imposta i campi singolarmente con When.NotExists per evitare overwrite
_db.HashSet(key, "old", _defaultOld, When.NotExists);
_db.HashSet(key, "new", _defaultNew, When.NotExists);
// Rileggi per essere sicuri
oldVal = _db.HashGet(key, "old");
newVal = _db.HashGet(key, "new");
}
// Se uno dei due manca, impostalo al default (non sovrascrive l'altro)
if (oldVal.IsNull)
{
_db.HashSet(key, "old", _defaultOld, When.NotExists);
oldVal = _defaultOld;
}
if (newVal.IsNull)
{
_db.HashSet(key, "new", _defaultNew, When.NotExists);
newVal = _defaultNew;
}
if (!int.TryParse(oldVal.ToString(), out var oldW)) oldW = _defaultOld;
if (!int.TryParse(newVal.ToString(), out var newW)) newW = _defaultNew;
// clamp 0..100
oldW = Math.Clamp(oldW, 0, 100);
newW = Math.Clamp(newW, 0, 100);
return (oldW, newW);
#endif
}
// API per aggiornare i pesi a runtime (opzionale)
public void SetWeights(string method, int oldWeight, int newWeight)
{
// 1. Scrittura su Redis
var key = _keyPrefix + (string.IsNullOrEmpty(method) ? "unknown" : method);
_db.HashSet(key, new HashEntry[] {
new HashEntry("old", Math.Clamp(oldWeight,0,100)),
new HashEntry("new", Math.Clamp(newWeight,0,100))
});
// 2. Rimozione della chiave da FusionCache.
// Grazie al Backplane Pub/Sub, la RAM verrà azzerata istantaneamente su TUTTI i server.
var cacheKey = $"weights:{method}";
_cache.Remove(cacheKey);
}
public bool UpsertWeight(WeightDTO updRecord)
{
if (updRecord == null || string.IsNullOrEmpty(updRecord.Method))
return false;
var key = _keyPrefix + updRecord.Method;
_db.HashSet(key, new HashEntry[] {
new HashEntry("old", Math.Clamp(updRecord.OldWeight, 0, 100)),
new HashEntry("new", Math.Clamp(updRecord.NewWeight, 0, 100))
});
// 1. Scrivi su Redis (Sorgente dati reale)
var redisKey = _keyPrefix + updRecord.Method;
_db.HashSet(redisKey, new HashEntry[] {
new HashEntry("old", Math.Clamp(updRecord.OldWeight, 0, 100)),
new HashEntry("new", Math.Clamp(updRecord.NewWeight, 0, 100))
});
// 2. Rimozione della chiave da FusionCache.
// Grazie al Backplane Pub/Sub, la RAM verrà azzerata istantaneamente su TUTTI i server.
var cacheKey = $"weights:{updRecord.Method}";
_cache.Remove(cacheKey);
return true;
#if false
var key = _keyPrefix + updRecord.Method;
_db.HashSet(key, new HashEntry[] {
new HashEntry("old", Math.Clamp(updRecord.OldWeight, 0, 100)),
new HashEntry("new", Math.Clamp(updRecord.NewWeight, 0, 100))
});
return true;
#endif
}
#endregion Public Methods
#region Private Fields
private static string _keyPrefix = "route_weight:";
private static string _redisBaseKey = "";
private readonly IFusionCache _cache;
/// <summary>
/// Definizione Record leggero per la cache
/// </summary>
/// <param name="OldWeight"></param>
/// <param name="NewWeight"></param>
public record RouteWeights(int OldWeight, int NewWeight);
private readonly IConfiguration _config;
private readonly IDatabase _db;
private readonly int _defaultNew;
private readonly int _defaultOld;
private readonly IConnectionMultiplexer _mux;
#endregion Private Fields
#region Private Methods
private string KeyToString(string key)
{
if (string.IsNullOrEmpty(key)) return "";
@@ -140,18 +251,6 @@ namespace MP.IOC.Services
return key;
}
#endregion Public Methods
#region Private Fields
private static string _keyPrefix = "route_weight:";
private static string _redisBaseKey = "";
private readonly IConfiguration _config;
private readonly IDatabase _db;
private readonly IConnectionMultiplexer _mux;
private readonly int _defaultNew;
private readonly int _defaultOld;
#endregion Private Fields
#endregion Private Methods
}
}
+1 -1
View File
@@ -5,7 +5,7 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>MP.INVE</RootNamespace>
<Version>8.16.2606.1312</Version>
<Version>8.16.2606.2208</Version>
</PropertyGroup>
<ItemGroup>
+1 -1
View File
@@ -1,6 +1,6 @@
<body>
<i>Modulo MAPOINVE </i>
<h4>Versione: 8.16.2606.1312</h4>
<h4>Versione: 8.16.2606.2208</h4>
<br /> Note di rilascio:
<ul>
<li>
+1 -1
View File
@@ -1 +1 @@
8.16.2606.1312
8.16.2606.2208
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>8.16.2606.1312</version>
<version>8.16.2606.2208</version>
<url>https://nexus.steamware.net/repository/SWS/MP-INVE/stable/LAST/MP.INVE.zip</url>
<changelog>https://nexus.steamware.net/repository/SWS/MP-INVE/stable/LAST/ChangeLog.html</changelog>
<mandatory>false</mandatory>
+1 -1
View File
@@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using MP.Core.DTO;
using MP.IOC.Services;
using MP.Data.Services.RouteWeight;
namespace MP.IOC.Components.Pages
{
+8 -1
View File
@@ -4,9 +4,16 @@
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<Version>8.16.2606.2010</Version>
<Version>8.16.2606.2208</Version>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Services\**" />
<Content Remove="Services\**" />
<EmbeddedResource Remove="Services\**" />
<None Remove="Services\**" />
</ItemGroup>
<ItemGroup>
<Content Remove="compilerconfig.json" />
</ItemGroup>
+6 -3
View File
@@ -4,10 +4,10 @@ using Microsoft.Extensions.Caching.Distributed;
using Microsoft.OpenApi.Models;
using MP.Core.Conf;
using MP.Data;
using MP.Data.Services.RouteWeight;
using MP.IOC.Components;
using MP.IOC.Data;
using MP.IOC.Endpoints;
using MP.IOC.Services;
using NLog;
using NLog.Web;
using StackExchange.Redis;
@@ -107,8 +107,8 @@ builder.Services.AddSingleton<MpDataService>();
// 1. Registra il serializzatore NewtonsoftJson per FusionCache
builder.Services.AddSingleton<IFusionCacheSerializer>(new FusionCacheNewtonsoftJsonSerializer());
// 2. Configura FusionCache (L1 Memory + L2 Redis Distributed + L3 DB via factory)
//builder.Services.AddFusionCache("MAPO_MES_FusionCache")
builder.Services.AddFusionCache()
.WithDistributedCache(sp => sp.GetRequiredService<IDistributedCache>())
.WithSerializer(new FusionCacheNewtonsoftJsonSerializer())
@@ -119,12 +119,15 @@ builder.Services.AddFusionCache()
.WithDefaultEntryOptions(options =>
{
// Durata di default dei dati in memoria
options.Duration = TimeSpan.FromMinutes(1);
options.Duration = TimeSpan.FromMinutes(5);
// Jitter: variazione casuale alla scadenza per evitare scadenze in blocco
options.JitterMaxDuration = TimeSpan.FromSeconds(5);
});
// aggiunta http client x chiamare REST esterne (reset cache su RIOC, in attesa fix fusionCache)
builder.Services.AddHttpClient();
logger.Info("Standard service configured");
// WeightProvider: Redis/Memory da config
+1 -1
View File
@@ -1,6 +1,6 @@
<body>
<i>Modulo MP-IOC </i>
<h4>Versione: 8.16.2606.2010</h4>
<h4>Versione: 8.16.2606.2208</h4>
<br /> Note di rilascio:
<ul>
<li>
+1 -1
View File
@@ -1 +1 @@
8.16.2606.2010
8.16.2606.2208
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>8.16.2606.2010</version>
<version>8.16.2606.2208</version>
<url>https://nexus.steamware.net/repository/SWS/MP-IOC/stable/LAST/MP.IOC.zip</url>
<changelog>https://nexus.steamware.net/repository/SWS/MP-IOC/stable/LAST/ChangeLog.html</changelog>
<mandatory>false</mandatory>
-372
View File
@@ -1,372 +0,0 @@
using MP.Data;
using Newtonsoft.Json;
using NLog;
using StackExchange.Redis;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace MP.IOC.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";
}
// Configurazione serializzatore JSON per risolvere errore di loop circolare
JSSettings = new JsonSerializerSettings()
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
};
}
#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>
/// 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
#endregion Private Methods
}
}
+1
View File
@@ -16,6 +16,7 @@
"mdbConnString": "mongodb://localhost:27017"
},
"ServerConf": {
"GatewayPurgeUrl": "http://maposrv.egalware.com/MP/RIOC/api/admin/cache/purge-route/",
"useFactory": false
}
}
+1
View File
@@ -64,6 +64,7 @@
},
"ServerConf": {
"BaseUrlIoc": "/MP/IOC/",
"GatewayPurgeUrl": "http://iis01.egalware.com/MP/RIOC/api/admin/cache/purge-route/",
"MpIoNS": "MoonPro:SQL2016DEV:MoonPro",
"RedisBaseKey": "MP-IOC",
"RedisWeight": true,
+1 -1
View File
@@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>MP.Land</RootNamespace>
<Version>8.16.2606.1312</Version>
<Version>8.16.2606.2208</Version>
<Configurations>Debug;Release;Debug_LiManDebug</Configurations>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<RunAnalyzersDuringBuild>True</RunAnalyzersDuringBuild>
+1 -1
View File
@@ -1,6 +1,6 @@
<body>
<i>Modulo Tablet MAPO - DotNet6</i>
<h4>Versione: 8.16.2606.1312</h4>
<h4>Versione: 8.16.2606.2208</h4>
<br />
Note di rilascio:
<ul>
+1 -1
View File
@@ -1 +1 @@
8.16.2606.1312
8.16.2606.2208
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>8.16.2606.1312</version>
<version>8.16.2606.2208</version>
<url>https://nexus.steamware.net/repository/SWS/MP-LAND/stable/LAST/MP.Land.zip</url>
<changelog>https://nexus.steamware.net/repository/SWS/MP-LAND/stable/LAST/ChangeLog.html</changelog>
<mandatory>false</mandatory>
+1 -1
View File
@@ -6,7 +6,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>MP.MON</RootNamespace>
<AssemblyName>$(AssemblyName.Replace(' ', '_'))</AssemblyName>
<Version>8.16.2606.1312</Version>
<Version>8.16.2606.2208</Version>
</PropertyGroup>
<ItemGroup>
+1 -1
View File
@@ -1,6 +1,6 @@
<body>
<i>Modulo MAPOSPEC </i>
<h4>Versione: 8.16.2606.1312</h4>
<h4>Versione: 8.16.2606.2208</h4>
<br /> Note di rilascio:
<ul>
<li>
+1 -1
View File
@@ -1 +1 @@
8.16.2606.1312
8.16.2606.2208
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>8.16.2606.1312</version>
<version>8.16.2606.2208</version>
<url>https://nexus.steamware.net/repository/SWS/MP-MON/stable/LAST/MP.MON.zip</url>
<changelog>https://nexus.steamware.net/repository/SWS/MP-MON/stable/LAST/ChangeLog.html</changelog>
<mandatory>false</mandatory>
+1 -1
View File
@@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>MP.Prog</RootNamespace>
<Version>8.16.2606.1312</Version>
<Version>8.16.2606.2208</Version>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
</PropertyGroup>
+1 -1
View File
@@ -1,6 +1,6 @@
<body>
<i>Modulo gestione Programmi MAPO</i>
<h4>Versione: 8.16.2606.1312</h4>
<h4>Versione: 8.16.2606.2208</h4>
<br />
Note di rilascio:
<ul>
+1 -1
View File
@@ -1 +1 @@
8.16.2606.1312
8.16.2606.2208
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>8.16.2606.1312</version>
<version>8.16.2606.2208</version>
<url>https://nexus.steamware.net/repository/SWS/MP-PROG/stable/LAST/MP.Prog.zip</url>
<changelog>https://nexus.steamware.net/repository/SWS/MP-PROG/stable/LAST/ChangeLog.html</changelog>
<mandatory>false</mandatory>
+7 -1
View File
@@ -5,10 +5,16 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>MP.RIOC</RootNamespace>
<Version>8.16.2606.1312</Version>
<Version>8.16.2606.2208</Version>
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Services\InMemoryWeightProvider.cs" />
<Compile Remove="Services\IWeightProvider.cs" />
<Compile Remove="Services\RedisWeightProvider.cs" />
</ItemGroup>
<ItemGroup>
<Content Remove="compilerconfig.json" />
</ItemGroup>
+63 -35
View File
@@ -1,7 +1,8 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Caching.Distributed;
using MP.Core.Conf;
using MP.Data;
using MP.Data.Services.RouteWeight;
using MP.RIOC.Services;
using NLog;
using NLog.Web;
@@ -9,6 +10,10 @@ using StackExchange.Redis;
using System.Diagnostics;
using System.Net;
using System.Reflection;
using ZiggyCreatures.Caching.Fusion;
using ZiggyCreatures.Caching.Fusion.Backplane.StackExchangeRedis;
using ZiggyCreatures.Caching.Fusion.Serialization;
using ZiggyCreatures.Caching.Fusion.Serialization.NewtonsoftJson;
// Forza il ThreadPool a tenere pronti almeno 500 thread per i calcoli e 500 per l'I/O di rete
@@ -19,6 +24,11 @@ var builder = WebApplication.CreateBuilder(args);
// RECUPERO L'AMBIENTE REALE (Che ora IIS passa correttamente come 'Staging')
var env = builder.Environment;
// recupero env corrente
var logger = LogManager.Setup()
.LoadConfigurationFromAppSettings()
.GetCurrentClassLogger();
// FORZA IL CARICAMENTO CORRETTO DEI JSON CON LA GERARCHIA DI AMBIENTE
builder.Configuration
.SetBasePath(env.ContentRootPath)
@@ -26,10 +36,6 @@ builder.Configuration
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables();
// recupero env corrente
var logger = LogManager.Setup()
.LoadConfigurationFromAppSettings()
.GetCurrentClassLogger();
builder.Logging.ClearProviders();
builder.Host.UseNLog();
@@ -50,29 +56,14 @@ builder.Services.Configure<RedisScriptsConfig>(
builder.Configuration.GetSection("RedisScripts"));
logger.Info("RedisScript Provider configured");
// Metodi principali x accesso dati
var connStr = builder.Configuration.GetConnectionString("MP.Data")
?? throw new InvalidOperationException("ConnString 'MP.Data' mancante.");
builder.Services.AddDbContextFactory<MoonProContext>(options =>
options.UseSqlServer(connStr)
.EnableSensitiveDataLogging(false) // true solo in Sviluppo
.ConfigureWarnings(w => w.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning)));
var connStrFL = builder.Configuration.GetConnectionString("MP.Flux")
?? throw new InvalidOperationException("ConnString 'MP.Flux' mancante.");
builder.Services.AddDbContextFactory<MoonPro_FluxContext>(options =>
options.UseSqlServer(connStrFL)
.EnableSensitiveDataLogging(false) // true solo in Sviluppo
.ConfigureWarnings(w => w.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning)));
// MP.Data DbContext for Stats repositories
string utilsConnString = builder.Configuration.GetConnectionString("MP.Utils") ?? "Server=localhost;Database=MoonPro_Utils; integrated security=True; MultipleActiveResultSets=True; App=MP.IOC;";
builder.Services.AddDbContextFactory<MoonPro_UtilsContext>(options =>
options.UseSqlServer(utilsConnString));
// MP.Data Services Utils - Statistiche DB
builder.Services.AddIocDataLayer();
builder.Services.AddRIocDataLayer();
// 1. Configurazione dell'invoker personalizzato (Risolve i tuoi errori)
// 1. Configurazione dell'invoker personalizzato (Potenziato con il Pooling)
var httpClientInvoker = new HttpMessageInvoker(new SocketsHttpHandler
{
@@ -109,8 +100,25 @@ builder.Services.AddHttpForwarder();
var redisMux = ConnectionMultiplexer.Connect(confRedis);
builder.Services.AddSingleton<IConnectionMultiplexer>(redisMux);
// registrazione FusionCache
builder.Services.AddFusionCache();
// 1. Registra il serializzatore NewtonsoftJson per FusionCache
builder.Services.AddSingleton<IFusionCacheSerializer>(new FusionCacheNewtonsoftJsonSerializer());
// 2. Configura FusionCache (L1 Memory + L2 Redis Distributed + L3 DB via factory)
//builder.Services.AddFusionCache("MAPO_MES_FusionCache")
builder.Services.AddFusionCache()
.WithDistributedCache(sp => sp.GetRequiredService<IDistributedCache>())
.WithSerializer(new FusionCacheNewtonsoftJsonSerializer())
.WithBackplane(new RedisBackplane(new RedisBackplaneOptions
{
ConnectionMultiplexerFactory = () => Task.FromResult<IConnectionMultiplexer>(redisMux)
}))
.WithDefaultEntryOptions(options =>
{
// Durata di default dei dati in memoria
options.Duration = TimeSpan.FromMinutes(5);
// Jitter: variazione casuale alla scadenza per evitare scadenze in blocco
options.JitterMaxDuration = TimeSpan.FromSeconds(5);
});
// Registrazione dei servizi custom
builder.Services.AddSingleton<PreserveBodyTransformer>();
@@ -175,6 +183,38 @@ app.Use(async (ctx, next) =>
await next();
});
// test per ambiente di esecuzione InProcess...
app.MapGet("api/alive", () =>
$"OK - Girando in: {System.Diagnostics.Process.GetCurrentProcess().ProcessName}");
app.MapGet("/router-status", (RouteStatsManager stats) => Results.Ok(new
{
Status = "Online",
Version = assemblyVersion,
Mode = weightOnRedis ? "Redis" : "InMemory",
Time = DateTime.Now,
Metrics = stats.Snapshot()
}));
// Endpoint per la rimozione mirata di una singola rotta dalla cache
app.MapPost("/api/admin/cache/purge-route/{method}", (string method, IWeightProvider weightProvider) =>
{
try
{
if (weightProvider is RedisWeightProvider redisProvider)
{
redisProvider.EvictLocalCacheFor(method);
return Results.Ok(new { Success = true, Message = $"Cache RAM per '{method}' svuotata." });
}
return Results.Problem("Provider non valido.");
}
catch (Exception ex)
{
return Results.Problem($"Errore: {ex.Message}");
}
});
// 5. Il cuore del Proxy (MapWhen è terminale per le richieste che lo soddisfano)
string routePath = configuration.GetValue<string>("ServerConf:RoutePath") ?? "/api/IOB";
string fullPath = $"{baseUrl}{routePath}".Replace("//", "/");
@@ -189,22 +229,10 @@ app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments(routePath, StringComparis
});
});
// test per ambiente di esecuzione InProcess...
app.MapGet("api/alive", () =>
$"OK - Girando in: {System.Diagnostics.Process.GetCurrentProcess().ProcessName}");
// 6. Definizione degli Endpoints locali
app.MapRazorPages();
app.MapGet("/router-status", (RouteStatsManager stats) => Results.Ok(new
{
Status = "Online",
Version = assemblyVersion,
Mode = weightOnRedis ? "Redis" : "InMemory",
Time = DateTime.Now,
Metrics = stats.Snapshot()
}));
// 7. Fallback "intelligente"
// Invece di app.Run, usiamo MapFallback che viene eseguito SOLO se nessun altro endpoint o MapWhen ha risposto
app.MapFallback(async context =>
+2 -2
View File
@@ -14,7 +14,7 @@
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "MP/RIOC/api/IOB/",
"applicationUrl": "http://localhost:5290",
"applicationUrl": "http://*:5290",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
@@ -24,7 +24,7 @@
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "MP/RIOC/api/IOB/",
"applicationUrl": "https://localhost:7120;http://localhost:5290",
"applicationUrl": "https://*:7120;http://*:5290",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
+1 -1
View File
@@ -1,6 +1,6 @@
<body>
<i>Modulo MP-RIOC </i>
<h4>Versione: 8.16.2606.1312</h4>
<h4>Versione: 8.16.2606.2208</h4>
<br /> Note di rilascio:
<ul>
<li>
+1 -1
View File
@@ -1 +1 @@
8.16.2606.1312
8.16.2606.2208
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>8.16.2606.1312</version>
<version>8.16.2606.2208</version>
<url>https://nexus.steamware.net/repository/SWS/MP-RIOC/stable/LAST/MP.RIOC.zip</url>
<changelog>https://nexus.steamware.net/repository/SWS/MP-RIOC/stable/LAST/ChangeLog.html</changelog>
<mandatory>false</mandatory>
-29
View File
@@ -1,29 +0,0 @@
using MP.Core.DTO;
namespace MP.RIOC.Services
{
public interface IWeightProvider
{
#region Public Methods
/// <summary>
/// Ritorna l'intero elenco dei weight attivi nel formato WeightDTO
/// </summary>
/// <returns></returns>
Task<List<WeightDTO>> GetAllWeightsAsync();
/// <summary>
/// Ritorna la coppia (oldWeight, newWeight) per scegliere dove instradare il metodo tra i 2 sistemi API.
/// </summary>
(int oldWeight, int newWeight) GetWeightsFor(string method);
/// <summary>
/// Aggiorna/Aggiuinge il valore del weight richiesto
/// </summary>
/// <param name=""></param>
/// <returns></returns>
bool UpsertWeight(WeightDTO updRecord);
#endregion Public Methods
}
}
@@ -1,56 +0,0 @@
using MP.Core.DTO;
using System.Collections.Concurrent;
namespace MP.RIOC.Services
{
public class InMemoryWeightProvider : IWeightProvider
{
private readonly ConcurrentDictionary<string, (int oldW, int newW)> _map = new();
private readonly int _defaultOld;
private readonly int _defaultNew;
public InMemoryWeightProvider(IConfiguration config)
{
_defaultOld = config.GetValue<int>("RouteMan:DefaultWeightOld", 100);
_defaultNew = config.GetValue<int>("RouteMan:DefaultWeightNew", 0);
}
public (int oldWeight, int newWeight) GetWeightsFor(string method)
{
if (string.IsNullOrEmpty(method)) method = "unknown";
return _map.GetOrAdd(method, _ => (_defaultOld, _defaultNew));
}
public void SetWeights(string method, int oldWeight, int newWeight)
{
_map[method] = (Math.Clamp(oldWeight, 0, 100), Math.Clamp(newWeight, 0, 100));
}
public async Task<List<WeightDTO>> GetAllWeightsAsync()
{
var result = new List<WeightDTO>();
await Task.Delay(1);
foreach (var kvp in _map)
{
result.Add(new WeightDTO
{
Method = kvp.Key,
OldWeight = Math.Clamp(kvp.Value.oldW, 0, 100),
NewWeight = Math.Clamp(kvp.Value.newW, 0, 100)
});
}
return result;
}
public bool UpsertWeight(WeightDTO updRecord)
{
if (updRecord == null || string.IsNullOrEmpty(updRecord.Method))
return false;
_map[updRecord.Method] = (Math.Clamp(updRecord.OldWeight, 0, 100), Math.Clamp(updRecord.NewWeight, 0, 100));
return true;
}
}
}
-157
View File
@@ -1,157 +0,0 @@
using MP.Core.DTO;
using StackExchange.Redis;
namespace MP.RIOC.Services
{
public class RedisWeightProvider : IWeightProvider
{
#region Public Constructors
public RedisWeightProvider(IConnectionMultiplexer mux, IConfiguration config)
{
_config = config;
_db = mux.GetDatabase();
_mux = mux;
_defaultOld = config.GetValue<int>("RouteMan:DefaultWeightOld", 100);
_defaultNew = config.GetValue<int>("RouteMan:DefaultWeightNew", 0);
_redisBaseKey = config.GetValue<string>("ServerConf:RedisBaseKey") ?? "MP_IOC";
_keyPrefix = $"{_redisBaseKey}:route_weight:";
}
#endregion Public Constructors
#region Public Methods
/// <summary>
/// Ritorna (oldWeight, newWeight) per il metodo. Se non esiste, crea la chiave con i default.
/// </summary>
public (int oldWeight, int newWeight) GetWeightsFor(string method)
{
if (string.IsNullOrEmpty(method)) method = "unknown";
var key = _keyPrefix + method;
// Leggi entrambi i campi
var oldVal = _db.HashGet(key, "old");
var newVal = _db.HashGet(key, "new");
// Se entrambi mancanti, inizializza con default (usando HSet con When.NotExists per evitare overwrite)
if (oldVal.IsNull && newVal.IsNull)
{
// Imposta i campi singolarmente con When.NotExists per evitare overwrite
_db.HashSet(key, "old", _defaultOld, When.NotExists);
_db.HashSet(key, "new", _defaultNew, When.NotExists);
// Rileggi per essere sicuri
oldVal = _db.HashGet(key, "old");
newVal = _db.HashGet(key, "new");
}
// Se uno dei due manca, impostalo al default (non sovrascrive l'altro)
if (oldVal.IsNull)
{
_db.HashSet(key, "old", _defaultOld, When.NotExists);
oldVal = _defaultOld;
}
if (newVal.IsNull)
{
_db.HashSet(key, "new", _defaultNew, When.NotExists);
newVal = _defaultNew;
}
if (!int.TryParse(oldVal.ToString(), out var oldW)) oldW = _defaultOld;
if (!int.TryParse(newVal.ToString(), out var newW)) newW = _defaultNew;
// clamp 0..100
oldW = Math.Clamp(oldW, 0, 100);
newW = Math.Clamp(newW, 0, 100);
return (oldW, newW);
}
// API per aggiornare i pesi a runtime (opzionale)
public void SetWeights(string method, int oldWeight, int newWeight)
{
var key = _keyPrefix + (string.IsNullOrEmpty(method) ? "unknown" : method);
_db.HashSet(key, new HashEntry[] {
new HashEntry("old", Math.Clamp(oldWeight,0,100)),
new HashEntry("new", Math.Clamp(newWeight,0,100))
});
}
public async Task<List<WeightDTO>> GetAllWeightsAsync()
{
var result = new List<WeightDTO>();
var server = _mux.GetServer(_mux.GetEndPoints().First());
if (server.IsReplica)
{
return result;
}
await foreach (var key in server.KeysAsync(pattern: $"{_keyPrefix}*"))
{
var methodName = KeyToString(key.ToString());
if (string.IsNullOrEmpty(methodName)) continue;
var oldVal = _db.HashGet(key, "old");
var newVal = _db.HashGet(key, "new");
int oldW = 100;
int newW = 0;
if (!oldVal.IsNull && int.TryParse(oldVal.ToString(), out var parsedOld))
oldW = Math.Clamp(parsedOld, 0, 100);
if (!newVal.IsNull && int.TryParse(newVal.ToString(), out var parsedNew))
newW = Math.Clamp(parsedNew, 0, 100);
result.Add(new WeightDTO { Method = methodName, OldWeight = oldW, NewWeight = newW });
}
// riordino desc x NEW poi alfabetico...
result = result
.OrderByDescending(x => x.NewWeight)
.ThenBy(x => x.Method)
.ToList();
return result;
}
public bool UpsertWeight(WeightDTO updRecord)
{
if (updRecord == null || string.IsNullOrEmpty(updRecord.Method))
return false;
var key = _keyPrefix + updRecord.Method;
_db.HashSet(key, new HashEntry[] {
new HashEntry("old", Math.Clamp(updRecord.OldWeight, 0, 100)),
new HashEntry("new", Math.Clamp(updRecord.NewWeight, 0, 100))
});
return true;
}
private string KeyToString(string key)
{
if (string.IsNullOrEmpty(key)) return "";
var prefix = _keyPrefix ?? "";
if (key.StartsWith(prefix))
return key.Substring(prefix.Length);
return key;
}
#endregion Public Methods
#region Private Fields
private static string _keyPrefix = "route_weight:";
private static string _redisBaseKey = "";
private readonly IConfiguration _config;
private readonly IDatabase _db;
private readonly IConnectionMultiplexer _mux;
private readonly int _defaultNew;
private readonly int _defaultOld;
#endregion Private Fields
}
}
+2 -1
View File
@@ -1,4 +1,5 @@
using NLog;
using MP.Data.Services.RouteWeight;
using NLog;
using System.Diagnostics;
using Yarp.ReverseProxy.Forwarder;
+2 -1
View File
@@ -3,7 +3,8 @@
"LogLevel": {
"Default": "Information",
"Yarp": "Warning",
"Microsoft.AspNetCore": "Warning"
"Microsoft.AspNetCore": "Warning",
"FusionCache": "Warning"
}
}
}
+2 -1
View File
@@ -3,7 +3,8 @@
"LogLevel": {
"Default": "Information",
"Yarp": "Warning",
"Microsoft.AspNetCore": "Warning"
"Microsoft.AspNetCore": "Warning",
"FusionCache": "Warning"
}
},
"AllowedHosts": "*",
+7 -1
View File
@@ -5,11 +5,17 @@
"Yarp": "Warning",
"Microsoft": "Warning",
"Microsoft.AspNetCore": "Warning",
"Microsoft.AspNetCore.Hosting.Diagnostics": "Warning"
"Microsoft.AspNetCore.Hosting.Diagnostics": "Warning",
"FusionCache": "Warning"
}
},
"NLog": {
"rules": [
{
"logger": "FusionCache",
"maxLevel": "Info",
"final": true
},
{
"logger": "Microsoft.*",
"maxLevel": "Fatal",
+2 -1
View File
@@ -4,7 +4,8 @@
"Default": "Warning",
"Yarp": "Warning",
"Microsoft.EntityFrameworkCore": "Warning",
"Microsoft.AspNetCore.Hosting.Diagnostics": "Information"
"Microsoft.AspNetCore.Hosting.Diagnostics": "Information",
"FusionCache": "Warning"
}
},
"NLog": {
+7 -1
View File
@@ -5,11 +5,17 @@
"Yarp": "Warning",
"Microsoft": "Warning",
"Microsoft.AspNetCore": "Warning",
"Microsoft.AspNetCore.Hosting.Diagnostics": "Warning"
"Microsoft.AspNetCore.Hosting.Diagnostics": "Warning",
"FusionCache": "Warning"
}
},
"NLog": {
"rules": [
{
"logger": "FusionCache",
"maxLevel": "Info",
"final": true
},
{
"logger": "Microsoft.*",
"maxLevel": "Fatal",
+7 -1
View File
@@ -6,7 +6,8 @@
"Microsoft.AspNetCore.Hosting.Diagnostics": "Information",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"Microsoft.EntityFrameworkCore": "Warning",
"Microsoft.AspNetCore": "Warning"
"Microsoft.AspNetCore": "Warning",
"FusionCache": "Warning"
}
},
"AllowedHosts": "*",
@@ -40,6 +41,11 @@
}
},
"rules": [
{
"logger": "FusionCache*",
"maxLevel": "Info",
"final": true
},
{
"logger": "Microsoft.EntityFrameworkCore.*",
"maxLevel": "Info",
+1 -1
View File
@@ -5,7 +5,7 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>MP.SPEC</RootNamespace>
<Version>8.16.2606.1312</Version>
<Version>8.16.2606.2208</Version>
<UserSecretsId>1800a78a-6ff1-40f9-b490-87fb8bfc1394</UserSecretsId>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
</PropertyGroup>
+1 -1
View File
@@ -1,6 +1,6 @@
<body>
<i>Modulo MAPOSPEC </i>
<h4>Versione: 8.16.2606.1312</h4>
<h4>Versione: 8.16.2606.2208</h4>
<br /> Note di rilascio:
<ul>
<li>
+1 -1
View File
@@ -1 +1 @@
8.16.2606.1312
8.16.2606.2208
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>8.16.2606.1312</version>
<version>8.16.2606.2208</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>
+1 -1
View File
@@ -4,7 +4,7 @@
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>MP.Stats</RootNamespace>
<UserSecretsId>826e877c-ba70-4253-84cb-d0b1cafd4440</UserSecretsId>
<Version>8.16.2606.1312</Version>
<Version>8.16.2606.2208</Version>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
</PropertyGroup>
+1 -1
View File
@@ -1,6 +1,6 @@
<body>
<i>Modulo statistiche MAPO</i>
<h4>Versione: 8.16.2606.1312</h4>
<h4>Versione: 8.16.2606.2208</h4>
<br />
Note di rilascio:
<ul>
+1 -1
View File
@@ -1 +1 @@
8.16.2606.1312
8.16.2606.2208
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>8.16.2606.1312</version>
<version>8.16.2606.2208</version>
<url>https://nexus.steamware.net/repository/SWS/MP-STATS/stable/LAST/MP.Stats.zip</url>
<changelog>https://nexus.steamware.net/repository/SWS/MP-STATS/stable/LAST/ChangeLog.html</changelog>
<mandatory>false</mandatory>