diff --git a/EgwCoreLib.Lux.Data/Repository/Admin/IVocabolarioRepository.cs b/EgwCoreLib.Lux.Data/Repository/Admin/IVocabolarioRepository.cs index c0e453ce..f18dca74 100644 --- a/EgwCoreLib.Lux.Data/Repository/Admin/IVocabolarioRepository.cs +++ b/EgwCoreLib.Lux.Data/Repository/Admin/IVocabolarioRepository.cs @@ -11,7 +11,7 @@ Task AddAsync(VocabolarioModel entity); /// - /// Clona un Vocabolario dalla linga originale a quella target + /// Clona un vocabolario data lingua origine/destinazione /// /// /// @@ -46,11 +46,28 @@ Task> GetByLang(string lingua); /// - /// Recuperoelenco lingue + /// Recupero elenco lingue /// /// Task> ListLingueAsync(); + /// + /// Recupera un dizionario lemma->traduzione per una data lingua (sync, via CreateDbContext). + /// Usato per caricamenti sincroni da FusionCache factory. + /// + Dictionary GetDizionarioLinguaSync(string lingua); + + /// + /// Recupera un dizionario lemma->traduzione per una data lingua (async). + /// + Task> GetDizionarioLinguaAsync(string lingua); + + /// + /// Inserisce un record Vocabolario usando DbContext sincrono. + /// Usato per autogrowth del dizionario in Traduci(). + /// + bool InsertSync(VocabolarioModel entity); + /// /// Aggiorna un record Vocabolario esistente nel database. /// diff --git a/EgwCoreLib.Lux.Data/Repository/Admin/VocabolarioRepository.cs b/EgwCoreLib.Lux.Data/Repository/Admin/VocabolarioRepository.cs index 63d85bef..b5f015a6 100644 --- a/EgwCoreLib.Lux.Data/Repository/Admin/VocabolarioRepository.cs +++ b/EgwCoreLib.Lux.Data/Repository/Admin/VocabolarioRepository.cs @@ -117,6 +117,32 @@ .ToListAsync(); } + /// + public Dictionary GetDizionarioLinguaSync(string lingua) + { + using var dbCtx = _ctxFactory.CreateDbContext(); + return dbCtx.DbSetVocabolario + .Where(x => x.Lingua == lingua) + .ToDictionary(d => d.Lemma, d => d.Traduzione); + } + + /// + public async Task> GetDizionarioLinguaAsync(string lingua) + { + await using var dbCtx = await CreateContextAsync(); + return await dbCtx.DbSetVocabolario + .Where(x => x.Lingua == lingua) + .ToDictionaryAsync(d => d.Lemma, d => d.Traduzione); + } + + /// + public bool InsertSync(VocabolarioModel entity) + { + using var dbCtx = _ctxFactory.CreateDbContext(); + dbCtx.DbSetVocabolario.Add(entity); + return dbCtx.SaveChanges() > 0; + } + /// public async Task UpdateAsync(VocabolarioModel entity) { diff --git a/EgwCoreLib.Lux.Data/Services/Admin/IVocabolarioService.cs b/EgwCoreLib.Lux.Data/Services/Admin/IVocabolarioService.cs index abce0b89..7deeee1a 100644 --- a/EgwCoreLib.Lux.Data/Services/Admin/IVocabolarioService.cs +++ b/EgwCoreLib.Lux.Data/Services/Admin/IVocabolarioService.cs @@ -30,6 +30,20 @@ /// Task FlushFusionCacheAsync(); + /// + /// Cancellazione FusionCache dato tag + /// + /// + /// + Task FlushFusionCacheAsync(string tag); + + /// + /// Cancellazione FusionCache data lista tag + /// + /// + /// + Task FlushFusionCacheAsync(List listTags); + /// /// Recupera un Vocabolario per ID /// diff --git a/EgwCoreLib.Lux.Data/Services/Admin/VocabolarioService.cs b/EgwCoreLib.Lux.Data/Services/Admin/VocabolarioService.cs index 5cfceb12..e75b8d40 100644 --- a/EgwCoreLib.Lux.Data/Services/Admin/VocabolarioService.cs +++ b/EgwCoreLib.Lux.Data/Services/Admin/VocabolarioService.cs @@ -2,11 +2,6 @@ namespace EgwCoreLib.Lux.Data.Services.Admin { - /// - /// Service per la gestione del vocabolario traduzioni interfaccia. - /// Il dizionario viene caricato in-memory all'avvio dal BackgroundService. - /// Ogni modifica CRUD aggiorna il dizionario senza passare per Redis. - /// public class VocabolarioService : BaseServ, IVocabolarioService { #region Public Constructors @@ -14,12 +9,13 @@ namespace EgwCoreLib.Lux.Data.Services.Admin public VocabolarioService( IConfiguration config, IConnectionMultiplexer redis, - IFusionCache cache, - IDbContextFactory factory) : base(config, redis) + IVocabolarioRepository repo, + IFusionCache cache) : base(config, redis) { _className = "Vocabolario"; + slowLogThresh = config.GetValue("ServerConf:slowLogThresh", 1); + _repo = repo; _cache = cache; - _factory = factory; } #endregion Public Constructors @@ -31,58 +27,27 @@ namespace EgwCoreLib.Lux.Data.Services.Admin { return await TraceAsync($"{_className}.Clone", async (activity) => { - var ctx = await _factory.CreateDbContextAsync(); - var list = await ctx.DbSetVocabolario - .Where(x => x.Lingua == linguaOrig) - .ToListAsync(); + bool result = await _repo.CloneAsync(linguaOrig, linguaDest); - if (!list.Any()) return false; - - await using var tx = await ctx.Database.BeginTransactionAsync(); - bool done = false; - try + if (result) { - var recLang = await ctx.DbSetLingua.FirstOrDefaultAsync(x => x.Lingua == linguaDest); - if (recLang == null) - { - recLang = new LinguaModel { Lingua = linguaDest, Note = $"Cloned from {linguaOrig}" }; - await ctx.DbSetLingua.AddAsync(recLang); - await ctx.SaveChangesAsync(); - } - - var listNew = list.Select(x => new VocabolarioModel - { - Lingua = linguaDest, - Lemma = x.Lemma, - Traduzione = x.Traduzione - }).ToList(); - - ctx.DbSetVocabolario.AddRange(listNew); - done = await ctx.SaveChangesAsync() > 0; - await tx.CommitAsync(); + await FlushFusionCacheAsync(); } - catch - { - await tx.RollbackAsync(); - throw; - } - return done; + return result; }); } /// - public async Task DeleteAsync(VocabolarioModel rec2del) + public async Task DeleteAsync(VocabolarioModel model) { return await TraceAsync($"{_className}.Delete", async (activity) => { - var ctx = await _factory.CreateDbContextAsync(); - var dbRec = await ctx.DbSetVocabolario - .FirstOrDefaultAsync(x => x.Lingua == rec2del.Lingua && x.Lemma == rec2del.Lemma); + bool success = await _repo.DeleteAsync(model); - if (dbRec == null) return false; - - ctx.DbSetVocabolario.Remove(dbRec); - bool success = await ctx.SaveChangesAsync() > 0; + if (success) + { + await FlushFusionCacheAsync(); + } return success; }); } @@ -94,6 +59,29 @@ namespace EgwCoreLib.Lux.Data.Services.Admin return true; } + /// + public async Task FlushFusionCacheAsync(string tag) + { + if (string.IsNullOrWhiteSpace(tag)) return false; + + await _cache.RemoveByTagAsync(tag); + return true; + } + + /// + public async Task FlushFusionCacheAsync(List listTags) + { + if (listTags == null || listTags.Count == 0) return false; + + // Generiamo i Task di rimozione ed eseguiamoli in parallelo su Redis/L1 + var tasks = listTags + .Where(tag => !string.IsNullOrWhiteSpace(tag)) + .Select(tag => _cache.RemoveByTagAsync(tag).AsTask()); + + await Task.WhenAll(tasks); + return true; + } + /// public async Task> GetAllAsync() { @@ -101,11 +89,7 @@ namespace EgwCoreLib.Lux.Data.Services.Admin { return await GetOrSetCacheAsync( $"{_redisBaseKey}:{_className}:ALL", - async () => - { - var ctx = await _factory.CreateDbContextAsync(); - return await ctx.DbSetVocabolario.ToListAsync(); - } + async () => await _repo.GetAllAsync() ); }); } @@ -117,12 +101,7 @@ namespace EgwCoreLib.Lux.Data.Services.Admin { return await GetOrSetCacheAsync( $"{_redisBaseKey}:{_className}:GetById:{lingua}:{lemma}", - async () => - { - var ctx = await _factory.CreateDbContextAsync(); - return await ctx.DbSetVocabolario - .FirstOrDefaultAsync(x => x.Lingua == lingua && x.Lemma == lemma); - } + async () => await _repo.GetByIdAsync(lingua, lemma) ); }); } @@ -130,15 +109,13 @@ namespace EgwCoreLib.Lux.Data.Services.Admin /// public async Task> GetByLang(string lingua) { - // Fallback: DB + Redis cache - return await GetOrSetCacheAsync( - $"{_redisBaseKey}:{_className}:GetByLang:{lingua}", - async () => - { - var ctx = await _factory.CreateDbContextAsync(); - return await ctx.DbSetVocabolario.Where(o => o.Lingua == lingua).ToListAsync(); - } - ); + return await TraceAsync($"{_className}.GetByLang", async (activity) => + { + return await GetOrSetCacheAsync( + $"{_redisBaseKey}:{_className}:GetByLang:{lingua}", + async () => await _repo.GetByLang(lingua) + ); + }); } /// @@ -148,11 +125,7 @@ namespace EgwCoreLib.Lux.Data.Services.Admin { return await GetOrSetCacheAsync( $"{_redisBaseKey}:{_className}:Lingue", - async () => - { - var ctx = await _factory.CreateDbContextAsync(); - return await ctx.DbSetLingua.ToListAsync(); - } + async () => await _repo.ListLingueAsync() ); }); } @@ -166,49 +139,46 @@ namespace EgwCoreLib.Lux.Data.Services.Admin string linguaKey = lingua.ToLowerInvariant().Trim(); string cacheKey = $"vocab:{linguaKey}"; - var ctx = _factory.CreateDbContext(); + // preparo opzioni + var cacheOptions = new FusionCacheEntryOptions() + .SetDuration(TimeSpan.FromMinutes(5)) + .SetFailSafe(true, TimeSpan.FromMinutes(15)); + // preparo tags + List tagsList = new List() { _className }; - // FusionCache gestisce il lock e recupera l'intero dizionario della lingua. - // Se è in L1 (Memory), restituisce l'oggetto C# istantaneamente. - // Se non c'è, passa a L2 (Redis) o invoca la factory per caricarlo. + // FusionCache gestisce lock + L1 memory + L2 redis + // Il factory usa repo.GetDizionarioLinguaSync (CreateDbContext sync) var dizionarioLingua = _cache.GetOrSet>( cacheKey, - _ => ctx - .DbSetVocabolario - .Where(x => x.Lingua == linguaKey) - .ToDictionary(d => d.Lemma, d => d.Traduzione), - options => options - //.SetDuration(TimeSpan.FromHours(8)) // Durata logica della cache - //.SetFailSafe(true, TimeSpan.FromHours(1)) // Se Redis/DB è giù, usa i vecchi dati L1 - .SetDuration(TimeSpan.FromMinutes(5)) // Durata logica della cache - .SetFailSafe(true, TimeSpan.FromMinutes(15)) // Se Redis/DB è giù, usa i vecchi dati L1 + _ => _repo.GetDizionarioLinguaSync(linguaKey), + options: cacheOptions, + tags: tagsList ); - // Ricerca O(1) nel dizionario in memoria if (dizionarioLingua != null && dizionarioLingua.TryGetValue(lemma, out var traduzione)) { return traduzione; } - // Fallback: se la parola non è censita inserisce su DB + // Lemma non trovato: inserisco nel DB e aggiornero la cache string answ = $"[[{lingua}_{lemma}]]"; - var newRec = new VocabolarioModel() - { - Lingua = lingua, - Lemma = lemma, - Traduzione = answ - }; try { - ctx.DbSetVocabolario.Add(newRec); - ctx.SaveChanges(); + _repo.InsertSync(new VocabolarioModel + { + Lingua = lingua, + Lemma = lemma, + Traduzione = answ + }); + // svuoto cache... + //_cache.Clear(allowFailSafe: false); + FlushFusionCache(_className); } - catch (Exception exc) + catch { - Log.Error(exc); + // Silenzio: il lemma non è stato inserito } - // ...e restituisce il lemma originale (lingua + lemma) return answ; } @@ -217,25 +187,27 @@ namespace EgwCoreLib.Lux.Data.Services.Admin { return await TraceAsync($"{_className}.Upsert", async (activity) => { - var ctx = await _factory.CreateDbContextAsync(); - var currRec = await ctx.DbSetVocabolario - .FirstOrDefaultAsync(x => x.Lingua == upsRec.Lingua && x.Lemma == upsRec.Lemma); + var currRec = await _repo.GetByIdAsync(upsRec.Lingua, upsRec.Lemma); string operation = "UPDATE"; bool success = false; if (currRec != null) { - ctx.Entry(currRec).CurrentValues.SetValues(upsRec); - success = await ctx.SaveChangesAsync() > 0; + success = await _repo.UpdateAsync(upsRec); } else { operation = "INSERT"; - await ctx.DbSetVocabolario.AddAsync(upsRec); - success = await ctx.SaveChangesAsync() > 0; + success = await _repo.AddAsync(upsRec); } activity?.SetTag("db.operation", operation); + + if (success) + { + await FlushFusionCacheAsync(); + } + return success; }); } @@ -245,62 +217,122 @@ namespace EgwCoreLib.Lux.Data.Services.Admin { return await TraceAsync($"{_className}.UpsertMany", async (activity) => { - var ctx = await _factory.CreateDbContextAsync(); - - var listLang = upsList.Select(d => d.Lingua).Distinct().ToList(); - var listLemmi = upsList.Select(d => d.Lemma).Distinct().ToList(); - var dbList = await ctx.DbSetVocabolario - .Where(t => listLang.Contains(t.Lingua) && listLemmi.Contains(t.Lemma)) - .ToListAsync(); - - var listExist = dbList - .Where(t => upsList.Any(d => d.Lingua == t.Lingua && d.Lemma == t.Lemma)) - .ToList(); - - foreach (var dto in upsList) - { - var existing = listExist - .FirstOrDefault(r => r.Lingua == dto.Lingua && r.Lemma == dto.Lemma); - - if (existing != null) - { - existing.Traduzione = dto.Traduzione; - } - else - { - await ctx.DbSetVocabolario.AddAsync(new VocabolarioModel - { - Lingua = dto.Lingua, - Lemma = dto.Lemma, - Traduzione = dto.Traduzione - }); - } - } - string operation = "MERGE"; - bool success = await ctx.SaveChangesAsync() > 0; + bool success = await _repo.UpsertManyAsync(upsList); activity?.SetTag("db.operation", operation); + + if (success) + { + await FlushFusionCacheAsync(); + } + return success; }); } #endregion Public Methods - #region Protected Fields + /// + /// Soglia minima (ms) per log timing in console + /// + private double slowLogThresh = 0; /// - /// Oggetto gestione FusionCache + /// Implementa gestione FusionCache+ tracking attività + /// - recupero cache da memoria o da obj esterno + cache memoria + /// - recupero da fetchFunc se mancasse + store in cache L1/L2 /// - protected readonly IFusionCache _cache; + /// + /// + /// + /// + /// + protected async Task GetOrFetchAsync(string operationName, string cacheKey, Func> fetchFunc, TimeSpan expiration, params string[] tagList) + { + using var activity = ActivitySource.StartActivity(operationName); + string source; + var tryGet = await _cache.TryGetAsync(cacheKey); + if (tryGet.HasValue) + { + source = "MEMORY"; + var result = tryGet.Value!; - #endregion Protected Fields + activity?.SetTag("data.source", source); + activity?.Stop(); + // se supero la soglia loggo... + if (activity?.Duration.TotalMilliseconds > slowLogThresh) + { + LogTrace($"{operationName} | {source} | {activity?.Duration.TotalMilliseconds:F4} ms"); + } + return result; + } + bool fromDb = false; + // cache in redis + var cacheOptions = new FusionCacheEntryOptions() + .SetDuration(expiration) + .SetFailSafe(true); + // cache in RAM per 1/3 del tempo x risparmiare risorse + cacheOptions.MemoryCacheDuration = expiration / 3; + + var final = await _cache.GetOrSetAsync( + cacheKey, + async _ => + { + fromDb = true; + return await fetchFunc(); + }, + options: cacheOptions, + tags: tagList + ); + + source = fromDb ? "DB" : "REDIS"; + activity?.SetTag("data.source", source); + activity?.Stop(); + // switch log in base a source.. + switch (source) + { + case "DB": + LogTrace($"{operationName} | {source} | {activity?.Duration.TotalMilliseconds:F4} ms", reqLevel: NLog.LogLevel.Info); + break; + + case "REDIS": + LogTrace($"{operationName} | {source} | {activity?.Duration.TotalMilliseconds:F4} ms", reqLevel: NLog.LogLevel.Debug); + break; + + default: + LogTrace($"{operationName} | {source} | {activity?.Duration.TotalMilliseconds:F4} ms"); + break; + } + return final!; + } #region Private Fields + private readonly IFusionCache _cache; + private readonly string _className; - private readonly IDbContextFactory _factory; + + private readonly IVocabolarioRepository _repo; #endregion Private Fields + + #region Private Methods + + private bool FlushFusionCache() + { + _cache.Clear(allowFailSafe: false); + return true; + } + + private bool FlushFusionCache(string tag) + { + if (string.IsNullOrWhiteSpace(tag)) return false; + + _cache.RemoveByTag(tag); + return true; + } + + #endregion Private Methods } } \ No newline at end of file diff --git a/Lux.API/Lux.API.csproj b/Lux.API/Lux.API.csproj index 063c6799..6b37a198 100644 --- a/Lux.API/Lux.API.csproj +++ b/Lux.API/Lux.API.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - 1.1.2606.0410 + 1.1.2606.0411 diff --git a/Lux.UI/Lux.UI.csproj b/Lux.UI/Lux.UI.csproj index 8e33a5a9..d4ff7e9a 100644 --- a/Lux.UI/Lux.UI.csproj +++ b/Lux.UI/Lux.UI.csproj @@ -5,7 +5,7 @@ enable enable aspnet-Lux.UI-a758c101-a2f4-4e38-977d-1c4887dbbd50 - 1.1.2606.0410 + 1.1.2606.0411 diff --git a/Lux.UI/appsettings.Production.json b/Lux.UI/appsettings.Production.json index fc72d3fc..1a493df1 100644 --- a/Lux.UI/appsettings.Production.json +++ b/Lux.UI/appsettings.Production.json @@ -19,6 +19,7 @@ "ChannelPub": "EgwEngineInput", "ChannelSub": "EgwEngineOutput", "Prog.ApiUrl": "https://office.egalware.com/lux/srv/api", - "ReportUrl": "https://office.egalware.com/Lux/RepSrv" + "ReportUrl": "https://office.egalware.com/Lux/RepSrv", + "slowLogThresh": 100 } } diff --git a/Lux.UI/appsettings.json b/Lux.UI/appsettings.json index db1c5eab..74212d38 100644 --- a/Lux.UI/appsettings.json +++ b/Lux.UI/appsettings.json @@ -87,6 +87,7 @@ "BaseUrl": "/LUX/UI/", "FileSharePath": "\\\\stor01\\TEAM DRIVES\\40_FileUpload\\LuxUploads", "RedisBaseKey": "Lux", - "CleanupDayTTL": 180 + "CleanupDayTTL": 180, + "slowLogThresh": 1 } } diff --git a/Resources/ChangeLog.html b/Resources/ChangeLog.html index 80c5bb5a..5df71938 100644 --- a/Resources/ChangeLog.html +++ b/Resources/ChangeLog.html @@ -1,6 +1,33 @@ LUX - Web Windows MES -

Versione: 1.1.2606.0410

+

Versione: 1.1.2606.0411

+
Note di rilascio: +
    +
  • + Ultime modifiche: +
      {{LAST-CHANGES}}
    +
  • +
  • + v.0.9.* → +
      +
    • Versione preliminare
    • +
    • Release dotNet8
    • +
    • Integrazione EFCore
    • +
    +
  • +
+
+
+ +
+ +
+ + + LUX - Web Windows MES +

Versione: 1.1.2606.0411


Note di rilascio:
  • diff --git a/Resources/VersNum.txt b/Resources/VersNum.txt index 1b99c8c6..5035cc6e 100644 --- a/Resources/VersNum.txt +++ b/Resources/VersNum.txt @@ -1 +1,2 @@ -1.1.2606.0410 +1.1.2606.0411 +1.1.2606.0411 diff --git a/Resources/manifest.xml b/Resources/manifest.xml index 1db8b226..17dbd791 100644 --- a/Resources/manifest.xml +++ b/Resources/manifest.xml @@ -1,6 +1,13 @@ - 1.1.2606.0410 + 1.1.2606.0411 + http://nexus.steamware.net/repository/SWS/GPW/stable/GPW.UI.zip + http://nexus.steamware.net/repository/SWS/GPW/stable/ChangeLog.html + false + + + + 1.1.2606.0411 http://nexus.steamware.net/repository/SWS/GPW/stable/GPW.UI.zip http://nexus.steamware.net/repository/SWS/GPW/stable/ChangeLog.html false