Aggiunta migration + modifica record statistiche con MachineId (dest) in stats DAY

This commit is contained in:
Samuele Locatelli
2026-04-22 18:27:50 +02:00
parent cd9f7c5da5
commit 586a5b24d1
15 changed files with 242 additions and 190 deletions
@@ -15,11 +15,16 @@ namespace MP.Data.DbModels.Utils
{
[Key]
public int Id { get; set; }
/// <summary>
/// Ambiente di destinazione IOC/IO (new/old)
/// </summary>
public string Destination { get; set; } = "";
/// <summary>
/// ID Macchina di riferimento
/// </summary>
public string MachineId { get; set; } = "";
/// <summary>
/// DataOra riferimento
/// </summary>
public DateTime Hour { get; set; }
@@ -0,0 +1,114 @@
// <auto-generated />
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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"), 1L, 1);
b.Property<double>("AvgDuration")
.HasColumnType("float");
b.Property<string>("Destination")
.HasColumnType("nvarchar(450)");
b.Property<DateTime>("Hour")
.HasColumnType("datetime2");
b.Property<string>("MachineId")
.HasColumnType("nvarchar(450)");
b.Property<double>("MaxDuration")
.HasColumnType("float");
b.Property<double>("MinDuration")
.HasColumnType("float");
b.Property<long>("NoReply")
.HasColumnType("bigint");
b.Property<long>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"), 1L, 1);
b.Property<double>("AvgDuration")
.HasColumnType("float");
b.Property<string>("Destination")
.HasColumnType("nvarchar(450)");
b.Property<DateTime>("Hour")
.HasColumnType("datetime2");
b.Property<double>("MaxDuration")
.HasColumnType("float");
b.Property<double>("MinDuration")
.HasColumnType("float");
b.Property<long>("NoReply")
.HasColumnType("bigint");
b.Property<long>("RequestCount")
.HasColumnType("bigint");
b.Property<string>("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
}
}
}
@@ -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<string>(
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");
}
}
}
@@ -40,6 +40,9 @@ namespace MP.Data.Migrations.MoonPro_Utils
b.Property<DateTime>("Hour")
.HasColumnType("datetime2");
b.Property<string>("MachineId")
.HasColumnType("nvarchar(450)");
b.Property<double>("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");
});
+3 -34
View File
@@ -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<StatsAggregatedModel>()
.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<AnagArticoliModel>(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<ParetoFluxLogDTO>(entity =>
// {
// entity.HasKey(e => new { e.IdxMacchina, e.CodFlux });
// });
OnModelCreatingPartial(modelBuilder);
}
@@ -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
/// <inheritdoc />
public async Task<int> UpsertManyAsyncOrig(List<StatsAggregatedModel> 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
+2 -2
View File
@@ -1,6 +1,6 @@
<div class="card shadow">
<div class="card-header">
<div class="card-header py-1">
<div class="d-flex justify-content-between">
<div class="px-0 fw-bold">@Title</div>
<div class="px-0">
@@ -11,7 +11,7 @@
</div>
</div>
<div class="card-body p-1">
<ul class="list-group">
<ul class="list-group small">
@foreach (var itemDet in ListPaged)
{
<li class="list-group-item @CheckSelect(itemDet.Label) d-flex justify-content-between align-items-start small py-1">
+3 -3
View File
@@ -4,11 +4,11 @@
<div class="card-header">
<h3>Current Call Stats (24h)</h3>
</div>
<div class="card-body p-1">
<div class="card-body px-1 py-0">
<div class="row mb-2">
<div class="col-3">
<div class="card shadow">
<div class="card-header">
<div class="card-header py-1">
<div class="d-flex justify-content-between">
<div class="px-0 fw-bold">Avail Data</div>
<div class="px-0">
@@ -89,7 +89,7 @@
</div>
</div>
<div class="card-body">
<MultiLine Id="@currTsId" AspRatio="2.5" DataTSList="@TSDataMulti" Labels="@LabelPlot" lineColor="@lineColorsMLine" backColor="@bgColorsMLine" lTens="0" Titles="@lineTitles" Stepped="false" yScale="logarithmic"></MultiLine>
<MultiLine Id="@currTsId" AspRatio="3" DataTSList="@TSDataMulti" Labels="@LabelPlot" lineColor="@lineColorsMLine" backColor="@bgColorsMLine" lTens="0" Titles="@lineTitles" Stepped="false" yScale="logarithmic"></MultiLine>
</div>
</div>
}
+1 -1
View File
@@ -4,7 +4,7 @@
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<Version>6.16.2604.2111</Version>
<Version>6.16.2604.2218</Version>
</PropertyGroup>
<ItemGroup>
+1 -1
View File
@@ -1,6 +1,6 @@
<body>
<i>Modulo MP-IOC </i>
<h4>Versione: 6.16.2604.2111</h4>
<h4>Versione: 6.16.2604.2218</h4>
<br /> Note di rilascio:
<ul>
<li>
+1 -1
View File
@@ -1 +1 @@
6.16.2604.2111
6.16.2604.2218
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>6.16.2604.2111</version>
<version>6.16.2604.2218</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>
+16 -82
View File
@@ -43,75 +43,6 @@ namespace MP.IOC.Services
/// </summary>
private readonly string _updateScript;
#if false
/// <summary>
/// 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.
/// </summary>
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)
+20 -5
View File
@@ -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;
+20 -4
View File
@@ -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);