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..0393e63e 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,67 +59,75 @@ 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() { - return await TraceAsync($"{_className}.GetAllAsync", async (activity) => - { - return await GetOrSetCacheAsync( - $"{_redisBaseKey}:{_className}:ALL", - async () => - { - var ctx = await _factory.CreateDbContextAsync(); - return await ctx.DbSetVocabolario.ToListAsync(); - } - ); - }); + return await GetOrFetchAsync( + "GetAllAsync", + $"{_redisBaseKey}:{_className}:ALL", + () => _repo.GetAllAsync(), + base.LongCache, + _className + ); } /// public async Task GetByIdAsync(string lingua, string lemma) { - return await TraceAsync($"{_className}.GetById", async (activity) => - { - 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); - } - ); - }); + return await GetOrFetchAsync( + "GetByIdAsync", + $"{_redisBaseKey}:{_className}:GetById:{lingua}:{lemma}", + () => _repo.GetByIdAsync(lingua, lemma), + base.LongCache, + _className + ); } /// public async Task> GetByLang(string lingua) { - // Fallback: DB + Redis cache - return await GetOrSetCacheAsync( + return await GetOrFetchAsync( + "GetByLang", $"{_redisBaseKey}:{_className}:GetByLang:{lingua}", - async () => - { - var ctx = await _factory.CreateDbContextAsync(); - return await ctx.DbSetVocabolario.Where(o => o.Lingua == lingua).ToListAsync(); - } + () => _repo.GetByLang(lingua), + base.LongCache, + _className ); } /// public async Task> ListLingueAsync() { - return await TraceAsync($"{_className}.ListLingueAsync", async (activity) => - { - return await GetOrSetCacheAsync( - $"{_redisBaseKey}:{_className}:Lingue", - async () => - { - var ctx = await _factory.CreateDbContextAsync(); - return await ctx.DbSetLingua.ToListAsync(); - } - ); - }); + return await GetOrFetchAsync( + "ListLingueAsync", + $"{_redisBaseKey}:{_className}:Lingue", + () => _repo.ListLingueAsync(), + base.LongCache, + _className + ); } /// @@ -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.UI/Components/Compo/Admin/VocabMan.razor b/Lux.UI/Components/Compo/Admin/VocabMan.razor index f7c07e43..e63c0527 100644 --- a/Lux.UI/Components/Compo/Admin/VocabMan.razor +++ b/Lux.UI/Components/Compo/Admin/VocabMan.razor @@ -5,8 +5,8 @@
- + @if (string.IsNullOrEmpty(SelLingua)) { } @@ -15,7 +15,7 @@ } - @if (!string.IsNullOrEmpty(selLingua)) + @if (!string.IsNullOrEmpty(SelLingua)) { } diff --git a/Lux.UI/Components/Compo/Admin/VocabMan.razor.cs b/Lux.UI/Components/Compo/Admin/VocabMan.razor.cs index a7a0a402..c5bc3f1a 100644 --- a/Lux.UI/Components/Compo/Admin/VocabMan.razor.cs +++ b/Lux.UI/Components/Compo/Admin/VocabMan.razor.cs @@ -21,6 +21,9 @@ namespace Lux.UI.Components.Compo.Admin [Parameter] public string SearchVal { get; set; } = string.Empty; + [Parameter] + public string SelLingua { get; set; } = ""; + #endregion Public Properties #region Protected Methods @@ -36,8 +39,8 @@ namespace Lux.UI.Components.Compo.Admin private async Task DoClone() { - if (!string.IsNullOrEmpty(selLingua)) - await EC_ReqClone.InvokeAsync(selLingua); + if (!string.IsNullOrEmpty(SelLingua)) + await EC_ReqClone.InvokeAsync(SelLingua); } #endregion Protected Methods @@ -49,7 +52,6 @@ namespace Lux.UI.Components.Compo.Admin private bool isLoading = false; private List ListPaged = new(); private int numRecord = 10; - private string selLingua = ""; private int totalCount = 0; @@ -97,9 +99,9 @@ namespace Lux.UI.Components.Compo.Admin private void UpdateTable() { ListPaged.Clear(); - if (!string.IsNullOrEmpty(selLingua)) + if (!string.IsNullOrEmpty(SelLingua)) { - var rawList = AllRecord.Where(x => x.Lingua == selLingua).ToList(); + var rawList = AllRecord.Where(x => x.Lingua == SelLingua).ToList(); if (!string.IsNullOrEmpty(SearchVal)) { rawList = rawList.Where(x => diff --git a/Lux.UI/Components/Pages/Vocabulary.razor b/Lux.UI/Components/Pages/Vocabulary.razor index 2d68a8dc..42bce7b8 100644 --- a/Lux.UI/Components/Pages/Vocabulary.razor +++ b/Lux.UI/Components/Pages/Vocabulary.razor @@ -39,10 +39,10 @@ {
- +
- +
} 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/manifest.xml b/Resources/manifest.xml index 717d32fc..17dbd791 100644 --- a/Resources/manifest.xml +++ b/Resources/manifest.xml @@ -5,3 +5,10 @@ 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 +