diff --git a/MP.Data/DbModels/Utils/StatsAggregatedModel.cs b/MP.Data/DbModels/Utils/StatsAggregatedModel.cs index ece10c4b..3ecf54ad 100644 --- a/MP.Data/DbModels/Utils/StatsAggregatedModel.cs +++ b/MP.Data/DbModels/Utils/StatsAggregatedModel.cs @@ -15,11 +15,16 @@ namespace MP.Data.DbModels.Utils { [Key] public int Id { get; set; } + /// /// Ambiente di destinazione IOC/IO (new/old) /// public string Destination { get; set; } = ""; /// + /// ID Macchina di riferimento + /// + public string MachineId { get; set; } = ""; + /// /// DataOra riferimento /// public DateTime Hour { get; set; } diff --git a/MP.Data/Migrations/MoonPro_Utils/20260422155939_UpdateAggrStat.Designer.cs b/MP.Data/Migrations/MoonPro_Utils/20260422155939_UpdateAggrStat.Designer.cs new file mode 100644 index 00000000..ab0d6df4 --- /dev/null +++ b/MP.Data/Migrations/MoonPro_Utils/20260422155939_UpdateAggrStat.Designer.cs @@ -0,0 +1,114 @@ +// +using System; +using MP.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace MP.Data.Migrations.MoonPro_Utils +{ + [DbContext(typeof(MoonPro_UtilsContext))] + [Migration("20260422155939_UpdateAggrStat")] + partial class UpdateAggrStat + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .UseCollation("SQL_Latin1_General_CP1_CI_AS") + .HasAnnotation("ProductVersion", "6.0.36") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("MP.Data.DbModels.Utils.StatsAggregatedModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("AvgDuration") + .HasColumnType("float"); + + b.Property("Destination") + .HasColumnType("nvarchar(450)"); + + b.Property("Hour") + .HasColumnType("datetime2"); + + b.Property("MachineId") + .HasColumnType("nvarchar(450)"); + + b.Property("MaxDuration") + .HasColumnType("float"); + + b.Property("MinDuration") + .HasColumnType("float"); + + b.Property("NoReply") + .HasColumnType("bigint"); + + b.Property("RequestCount") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Destination", "MachineId", "Hour") + .IsUnique() + .HasDatabaseName("idx_statsaggr_env_mach_hour") + .HasFilter("[Destination] IS NOT NULL AND [MachineId] IS NOT NULL"); + + b.ToTable("stats_aggr"); + }); + + modelBuilder.Entity("MP.Data.DbModels.Utils.StatsDetailModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("AvgDuration") + .HasColumnType("float"); + + b.Property("Destination") + .HasColumnType("nvarchar(450)"); + + b.Property("Hour") + .HasColumnType("datetime2"); + + b.Property("MaxDuration") + .HasColumnType("float"); + + b.Property("MinDuration") + .HasColumnType("float"); + + b.Property("NoReply") + .HasColumnType("bigint"); + + b.Property("RequestCount") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Destination", "Type", "Hour") + .IsUnique() + .HasDatabaseName("idx_statsdet_hour_env_type") + .HasFilter("[Destination] IS NOT NULL AND [Type] IS NOT NULL"); + + b.ToTable("stats_detail"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MP.Data/Migrations/MoonPro_Utils/20260422155939_UpdateAggrStat.cs b/MP.Data/Migrations/MoonPro_Utils/20260422155939_UpdateAggrStat.cs new file mode 100644 index 00000000..2ceae2e4 --- /dev/null +++ b/MP.Data/Migrations/MoonPro_Utils/20260422155939_UpdateAggrStat.cs @@ -0,0 +1,47 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MP.Data.Migrations.MoonPro_Utils +{ + public partial class UpdateAggrStat : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "idx_statsaggr_env_hour", + table: "stats_aggr"); + + migrationBuilder.AddColumn( + name: "MachineId", + table: "stats_aggr", + type: "nvarchar(450)", + nullable: true); + + migrationBuilder.CreateIndex( + name: "idx_statsaggr_env_mach_hour", + table: "stats_aggr", + columns: new[] { "Destination", "MachineId", "Hour" }, + unique: true, + filter: "[Destination] IS NOT NULL AND [MachineId] IS NOT NULL"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "idx_statsaggr_env_mach_hour", + table: "stats_aggr"); + + migrationBuilder.DropColumn( + name: "MachineId", + table: "stats_aggr"); + + migrationBuilder.CreateIndex( + name: "idx_statsaggr_env_hour", + table: "stats_aggr", + columns: new[] { "Destination", "Hour" }, + unique: true, + filter: "[Destination] IS NOT NULL"); + } + } +} diff --git a/MP.Data/Migrations/MoonPro_Utils/MoonPro_UtilsContextModelSnapshot.cs b/MP.Data/Migrations/MoonPro_Utils/MoonPro_UtilsContextModelSnapshot.cs index ab41958b..f935fcb6 100644 --- a/MP.Data/Migrations/MoonPro_Utils/MoonPro_UtilsContextModelSnapshot.cs +++ b/MP.Data/Migrations/MoonPro_Utils/MoonPro_UtilsContextModelSnapshot.cs @@ -40,6 +40,9 @@ namespace MP.Data.Migrations.MoonPro_Utils b.Property("Hour") .HasColumnType("datetime2"); + b.Property("MachineId") + .HasColumnType("nvarchar(450)"); + b.Property("MaxDuration") .HasColumnType("float"); @@ -54,10 +57,10 @@ namespace MP.Data.Migrations.MoonPro_Utils b.HasKey("Id"); - b.HasIndex("Destination", "Hour") + b.HasIndex("Destination", "MachineId", "Hour") .IsUnique() - .HasDatabaseName("idx_statsaggr_env_hour") - .HasFilter("[Destination] IS NOT NULL"); + .HasDatabaseName("idx_statsaggr_env_mach_hour") + .HasFilter("[Destination] IS NOT NULL AND [MachineId] IS NOT NULL"); b.ToTable("stats_aggr"); }); diff --git a/MP.Data/MoonPro_UtilsContext.cs b/MP.Data/MoonPro_UtilsContext.cs index 9b4eb6eb..bbcc6ac7 100644 --- a/MP.Data/MoonPro_UtilsContext.cs +++ b/MP.Data/MoonPro_UtilsContext.cs @@ -83,43 +83,12 @@ namespace MP.Data .HasIndex(x => new { x.Destination, x.Type, x.Hour }) .HasDatabaseName("idx_statsdet_hour_env_type") .IsUnique(); + modelBuilder.Entity() - .HasIndex(x => new { x.Destination, x.Hour }) - .HasDatabaseName("idx_statsaggr_env_hour") + .HasIndex(x => new { x.Destination, x.MachineId, x.Hour }) + .HasDatabaseName("idx_statsaggr_env_mach_hour") .IsUnique(); - // modelBuilder.Entity(entity => - // { - // entity.HasKey(e => e.CodArticolo); - - // entity.ToView("AnagArticoli"); - - // entity.Property(e => e.CodArticolo) - // .IsRequired() - // .HasMaxLength(50); - - // entity.Property(e => e.DescArticolo) - // .IsRequired() - // .HasMaxLength(250); - - // entity.Property(e => e.Disegno) - // .IsRequired() - // .HasMaxLength(50); - - // entity.Property(e => e.Tipo) - // .IsRequired() - // .HasMaxLength(50); - - // entity.Property(e => e.Azienda) - // .IsRequired() - // .HasMaxLength(50); - // }); - - // modelBuilder.Entity(entity => - // { - // entity.HasKey(e => new { e.IdxMacchina, e.CodFlux }); - // }); - OnModelCreatingPartial(modelBuilder); } diff --git a/MP.Data/Repository/Utils/StatsAggrRepository.cs b/MP.Data/Repository/Utils/StatsAggrRepository.cs index a2ad622d..22fd2f55 100644 --- a/MP.Data/Repository/Utils/StatsAggrRepository.cs +++ b/MP.Data/Repository/Utils/StatsAggrRepository.cs @@ -93,13 +93,13 @@ namespace MP.Data.Repository.Utils // Creiamo un dizionario per ricerca rapida O(1) basato sulla chiave univoca (Dest + Hour) // Usiamo una Tupla come chiave del dizionario var lookup = existingRecords.ToDictionary( - x => (x.Destination, x.Hour), + x => (x.Destination, x.MachineId, x.Hour), x => x ); foreach (var incoming in listRecords) { - var key = (incoming.Destination, incoming.Hour); + var key = (incoming.Destination, incoming.MachineId, incoming.Hour); if (lookup.TryGetValue(key, out var existing)) { // --- CASO: UPDATE --- @@ -134,57 +134,6 @@ namespace MP.Data.Repository.Utils } } -#if false - /// - public async Task UpsertManyAsyncOrig(List listRecords, bool removeOld) - { - int answ = 0; - await using var dbCtx = await CreateContextAsync(); - await using var tx = await dbCtx.Database.BeginTransactionAsync(); - try - { - // in primis se richiesto calcolo range periodo e svuoto... - if (removeOld) - { - var firstRec = listRecords.OrderBy(x => x.Hour).FirstOrDefault(); - var lastRec = listRecords.OrderByDescending(x => x.Hour).FirstOrDefault(); - - if (firstRec != null && lastRec != null) - { - DateTime startDate = firstRec.Hour; - DateTime endDate = lastRec.Hour; - - var items = await dbCtx.DbSetStatsAggr - .Where(x => x.Hour >= startDate && x.Hour <= endDate) - .ToListAsync(); - - dbCtx.DbSetStatsAggr.RemoveRange(items); - await dbCtx.SaveChangesAsync(); - } - } - - await dbCtx - .DbSetStatsAggr - .AddRangeAsync(listRecords); - - // salvo! - answ = await dbCtx.SaveChangesAsync(); - - // commit transazione - await tx.CommitAsync(); - - // libero memoria del changeTracker - dbCtx.ChangeTracker.Clear(); - return answ; - } - catch - { - await tx.RollbackAsync(); - throw; - } - } -#endif - #endregion Public Methods #region Protected Fields diff --git a/MP.IOC/Components/Compo/ParetoDetail.razor b/MP.IOC/Components/Compo/ParetoDetail.razor index d9e1ece9..268dc234 100644 --- a/MP.IOC/Components/Compo/ParetoDetail.razor +++ b/MP.IOC/Components/Compo/ParetoDetail.razor @@ -1,6 +1,6 @@ 
-
+
@Title
@@ -11,7 +11,7 @@
-
    +
      @foreach (var itemDet in ListPaged) {
    • diff --git a/MP.IOC/Components/Pages/CallStats.razor b/MP.IOC/Components/Pages/CallStats.razor index f1f40dcf..a6ff3ac0 100644 --- a/MP.IOC/Components/Pages/CallStats.razor +++ b/MP.IOC/Components/Pages/CallStats.razor @@ -4,11 +4,11 @@

      Current Call Stats (24h)

      -
      +
      -
      +
      Avail Data
      @@ -89,7 +89,7 @@
      - +
      } diff --git a/MP.IOC/MP.IOC.csproj b/MP.IOC/MP.IOC.csproj index 01f8e2f1..c4a30446 100644 --- a/MP.IOC/MP.IOC.csproj +++ b/MP.IOC/MP.IOC.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - 6.16.2604.2111 + 6.16.2604.2218 diff --git a/MP.IOC/Resources/ChangeLog.html b/MP.IOC/Resources/ChangeLog.html index e38800ea..09bec829 100644 --- a/MP.IOC/Resources/ChangeLog.html +++ b/MP.IOC/Resources/ChangeLog.html @@ -1,6 +1,6 @@ Modulo MP-IOC -

      Versione: 6.16.2604.2111

      +

      Versione: 6.16.2604.2218


      Note di rilascio:
      • diff --git a/MP.IOC/Resources/VersNum.txt b/MP.IOC/Resources/VersNum.txt index 5c00dd54..2624fad6 100644 --- a/MP.IOC/Resources/VersNum.txt +++ b/MP.IOC/Resources/VersNum.txt @@ -1 +1 @@ -6.16.2604.2111 +6.16.2604.2218 diff --git a/MP.IOC/Resources/manifest.xml b/MP.IOC/Resources/manifest.xml index aed9404f..9951641d 100644 --- a/MP.IOC/Resources/manifest.xml +++ b/MP.IOC/Resources/manifest.xml @@ -1,6 +1,6 @@ - 6.16.2604.2111 + 6.16.2604.2218 https://nexus.steamware.net/repository/SWS/MP-IOC/stable/LAST/MP.IOC.zip https://nexus.steamware.net/repository/SWS/MP-IOC/stable/LAST/ChangeLog.html false diff --git a/MP.IOC/Services/MetricsCalcService.cs b/MP.IOC/Services/MetricsCalcService.cs index 32ed4ca7..b9918619 100644 --- a/MP.IOC/Services/MetricsCalcService.cs +++ b/MP.IOC/Services/MetricsCalcService.cs @@ -43,75 +43,6 @@ namespace MP.IOC.Services /// private readonly string _updateScript; -#if false - /// - /// Script update Redis in Lua, definito come costante per efficienza. - /// Lo script gestisce l'incremento e la logica condizionale per Min/Max in un'unica operazione atomica. - /// - private const string RedisUpdateScript_v6 = @" - local key = KEYS[1] - local countInc = tonumber(ARGV[1]) - local totalMsInc = tonumber(ARGV[2]) - local newMax = tonumber(ARGV[3]) - local newMin = tonumber(ARGV[4]) - local sentinel = tonumber(ARGV[5]) - - -- 1. Incremento Count e TotalDuration - redis.call('HINCRBY', key, 'count', countInc) - redis.call('HINCRBYFLOAT', key, 'totalMs', totalMsInc) - - -- 2. Aggiornamento Max (solo se il nuovo valore è maggiore del precedente E non � la SentinelValue) - local currentMax = redis.call('HGET', key, 'maxMs') - if newMax < sentinel and (not currentMax or newMax > tonumber(currentMax)) then - redis.call('HSET', key, 'maxMs', tostring(newMax)) - end - - -- 3. Aggiornamento Min (solo se il nuovo valore è minore del precedente E non � la SentinelValue) - local currentMin = redis.call('HGET', key, 'minMs') - if newMin < sentinel and (not currentMin or newMin < tonumber(currentMin)) then - redis.call('HSET', key, 'minMs', tostring(newMin)) - end - return 1 - "; - - - private const string RedisUpdateScript_v5 = @" - local key = KEYS[1] - local countInc = tonumber(ARGV[1]) or 0 - local totalMsInc = tonumber(ARGV[2]) or 0 - local newMax = tonumber(ARGV[3]) - local newMin = tonumber(ARGV[4]) - local sentinel = tonumber(ARGV[5]) - - -- Incrementi base - redis.call('HINCRBY', key, 'count', countInc) - redis.call('HINCRBYFLOAT', key, 'totalMs', totalMsInc) - - -- MAX - local currentMaxStr = redis.call('HGET', key, 'maxMs') - local currentMax = tonumber(currentMaxStr) - - if newMax ~= nil and newMax < sentinel then - if currentMax == nil or newMax > currentMax then - redis.call('HSET', key, 'maxMs', newMax) - end - end - - -- MIN - local currentMinStr = redis.call('HGET', key, 'minMs') - local currentMin = tonumber(currentMinStr) - - if newMin ~= nil and newMin < sentinel then - if currentMin == nil or newMin < currentMin then - redis.call('HSET', key, 'minMs', newMin) - end - end - - return 1 - - "; -#endif - // Classe di supporto per l'aggregazione locale dei valori Daily private class AggregatedStats @@ -150,14 +81,21 @@ namespace MP.IOC.Services { string dest = "IO"; string method = "NA"; + string machineId = "ALL"; string rawKey = kv.Key; if (rawKey.Contains("|")) { var splitVal = rawKey.Split("|"); dest = splitVal[0]; - method = splitVal[1]; + if (splitVal.Length > 1) + method = splitVal[1]; + if (splitVal.Length > 2) + machineId = splitVal[2]; + } + else + { + method = rawKey; } - else { method = rawKey; } var stat = kv.Value; var count = Interlocked.Read(ref stat.Count); @@ -168,7 +106,7 @@ namespace MP.IOC.Services ? double.Parse(SentinelValue) : stat.MinDuration.TotalMilliseconds; - // --- LOGICA HOURLY (Per ogni route/method � una chiave distinta) --- + // --- LOGICA HOURLY (Per ogni route/method e' una chiave distinta) --- var hourKey = HourBucketKey(dest, method, hourStart); var hoursIndex = HoursIndexKey(dest, method); var hourScore = ToEpochSeconds(hourStart); @@ -187,8 +125,8 @@ namespace MP.IOC.Services tasks.Add(batch.SortedSetAddAsync(hoursIndex, hourKey, hourScore)); // --- LOGICA DAILY (Aggregazione locale per evitare sovrascritture nel loop) --- - var dayKey = DayBucketKey(dest, dayStart); - var daysIndex = DaysIndexKey(dest); + var dayKey = DayBucketKey(dest, machineId, dayStart); + var daysIndex = DaysIndexKey(dest, machineId); var dayScore = ToEpochSeconds(dayStart); if (!dailyAggregates.TryGetValue(dayKey, out var agg)) @@ -255,18 +193,14 @@ namespace MP.IOC.Services #region Private Methods - //private static string DayBucketKey(string dest, string method, DateTime dtRif) - private static string DayBucketKey(string dest, DateTime dtRif) + private static string DayBucketKey(string dest, string machId, DateTime dtRif) { - return $"{_redisBaseKey}:stats:day:{dest}:{dtRif.ToString("yyyyMMdd", CultureInfo.InvariantCulture)}"; - //return $"{_redisBaseKey}:stats:day:{dest}:{method}:{dtRif.ToString("yyyyMMdd", CultureInfo.InvariantCulture)}"; + return $"{_redisBaseKey}:stats:day:{dest}:{machId}:{dtRif.ToString("yyyyMMdd", CultureInfo.InvariantCulture)}"; } - //private static string DaysIndexKey(string dest, string method) - private static string DaysIndexKey(string dest) + private static string DaysIndexKey(string dest, string machId) { - return $"{_redisBaseKey}:stats:days:{dest}"; - //return $"{_redisBaseKey}:stats:days:{dest}:{method}"; + return $"{_redisBaseKey}:stats:days:{dest}:{machId}"; } private static string HourBucketKey(string dest, string method, DateTime dtRif) diff --git a/MP.IOC/Services/MetricsDbFlushService.cs b/MP.IOC/Services/MetricsDbFlushService.cs index f5107234..4aef9eb7 100644 --- a/MP.IOC/Services/MetricsDbFlushService.cs +++ b/MP.IOC/Services/MetricsDbFlushService.cs @@ -110,7 +110,7 @@ namespace MP.IOC.Services foreach (var statKey in memberKeys) { var sKey = (RedisKey)$"{statKey}"; - if (!TryParseKeyMetadata(sKey, out string dest, out string method, out DateTime timestamp, out bool isHourType)) + if (!TryParseKeyMetadata(sKey, out string dest, out string method, out string machId, out DateTime timestamp, out bool isHourType)) continue; // Verifica se la chiave fosse scaduta rispetto all'orario corrente @@ -150,6 +150,7 @@ namespace MP.IOC.Services aggrRecordsToInsert.Add(new StatsAggregatedModel { Destination = dest, + MachineId = machId, Hour = timestamp, RequestCount = count, AvgDuration = totalMs / count, @@ -231,7 +232,7 @@ namespace MP.IOC.Services foreach (var statKey in memberKeys) { var sKey = (RedisKey)$"{statKey}"; - if (!TryParseKeyMetadata(sKey, out string dest, out string method, out DateTime timestamp, out bool isHourType)) + if (!TryParseKeyMetadata(sKey, out string dest, out string method, out string machId, out DateTime timestamp, out bool isHourType)) continue; // Verifica se la chiave fosse scaduta rispetto all'orario corrente @@ -309,9 +310,13 @@ namespace MP.IOC.Services } } - private bool TryParseKeyMetadata(RedisKey key, out string dest, out string method, out DateTime timestamp, out bool isHourType) + private bool TryParseKeyMetadata(RedisKey key, out string dest, out string method, out string machId, out DateTime timestamp, out bool isHourType) { - dest = "NA"; method = "NA"; timestamp = DateTime.MinValue; isHourType = true; + dest = "NA"; + method = "NA"; + machId = "ALL"; + timestamp = DateTime.MinValue; + isHourType = true; try { string k = key.ToString(); @@ -337,7 +342,17 @@ namespace MP.IOC.Services { isHourType = false; method = "DAILY"; - if (DateTime.TryParseExact(parts[3], "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var dt)) + string rawDate = ""; + if (parts.Length >= 5) + { + machId = parts[3]; + rawDate = parts[4]; + } + else + { + rawDate = parts[3]; + } + if (DateTime.TryParseExact(rawDate, "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var dt)) { timestamp = dt; return true; diff --git a/MP.IOC/Services/RouteManager.cs b/MP.IOC/Services/RouteManager.cs index 0df30063..529996e6 100644 --- a/MP.IOC/Services/RouteManager.cs +++ b/MP.IOC/Services/RouteManager.cs @@ -63,10 +63,24 @@ namespace MP.IOC.Services Log.Debug($"PathBase={context.Request.PathBase} | Path={context.Request.Path} | relativePath={relativePath}"); - // da calcolare metodo... + // Procedo a calcolare metodo e ID... string metodo = "/"; + string id = "ALL"; if (!string.IsNullOrEmpty(relativePath)) { + // Rimuovo eventuale query string + var pathOnly = relativePath.Split('?')[0]; + + // Splitto per / + var parts = pathOnly.Split('/', StringSplitOptions.RemoveEmptyEntries); + + if (parts.Length > 0) + metodo = parts[0]; + + if (parts.Length > 1) + id = parts[1]; + +#if false // splitto se ho /... if (relativePath.Contains("/")) { @@ -75,9 +89,11 @@ namespace MP.IOC.Services else { metodo = relativePath; - } + } +#endif } - Log.Debug($"Metodo: {metodo}"); + + Log.Debug($"Metodo: {metodo} | machineId: {id}"); var (oldW, newW) = _weightProvider.GetWeightsFor(metodo); var pickNew = DecideByWeights(oldW, newW); @@ -102,7 +118,7 @@ namespace MP.IOC.Services try { - string sKey = $"{target}|{metodo}"; + string sKey = $"{target}|{metodo}|{id}"; // Registra scelta _stats.Record(sKey);