Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Lite.Tests/DismissedArchiveSidecarTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -269,14 +269,14 @@ SELECT 1 FROM dismissed_archive_alerts d
}

[Fact]
public async Task SchemaVersion_IsUpdatedTo23()
public async Task SchemaVersion_IsUpdatedTo24()
{
using var connection = await InitializeDatabaseAsync();

using var cmd = connection.CreateCommand();
cmd.CommandText = "SELECT MAX(version) FROM schema_version";
var version = Convert.ToInt32(await cmd.ExecuteScalarAsync(TestContext.Current.CancellationToken));

Assert.Equal(23, version);
Assert.Equal(DuckDbInitializer.CurrentSchemaVersion, version);
}
}
4 changes: 2 additions & 2 deletions Lite/Analysis/DuckDbFactCollector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1363,8 +1363,8 @@ private async Task CollectServerPropertiesFactsAsync(AnalysisContext context, Li

using var cmd = connection.CreateCommand();
cmd.CommandText = @"
SELECT cpu_count, hyperthread_ratio, physical_memory_mb, socket_count, cores_per_socket,
is_hadr_enabled, edition, product_version
SELECT COALESCE(vcore_count, cpu_count) AS cpu_count, hyperthread_ratio, physical_memory_mb,
socket_count, cores_per_socket, is_hadr_enabled, edition, product_version
FROM server_properties
WHERE server_id = $1
ORDER BY collection_time DESC
Expand Down
35 changes: 31 additions & 4 deletions Lite/Analysis/TestDataSeeder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1525,7 +1525,8 @@ INSERT INTO trace_flags
/// </summary>
internal async Task SeedServerPropertiesAsync(int cpuCount, int htRatio,
long physicalMemMb, int socketCount = 2, int coresPerSocket = 0,
bool hadrEnabled = false, string edition = "Standard Edition")
bool hadrEnabled = false, string edition = "Standard Edition",
int engineEdition = 2, string? serviceObjective = null, int? vcoreCount = null)
{
if (coresPerSocket == 0) coresPerSocket = cpuCount / (socketCount * 2); // assume HT

Expand All @@ -1539,9 +1540,10 @@ INSERT INTO server_properties
(collection_id, collection_time, server_id, server_name,
edition, product_version, product_level, engine_edition,
cpu_count, hyperthread_ratio, physical_memory_mb,
socket_count, cores_per_socket, is_hadr_enabled)
VALUES ($1, $2, $3, $4, $5, '16.0.4150.1', 'RTM', 2,
$6, $7, $8, $9, $10, $11)";
socket_count, cores_per_socket, is_hadr_enabled,
service_objective, vcore_count)
VALUES ($1, $2, $3, $4, $5, '16.0.4150.1', 'RTM', $12,
$6, $7, $8, $9, $10, $11, $13, $14)";

cmd.Parameters.Add(new DuckDBParameter { Value = _nextId-- });
cmd.Parameters.Add(new DuckDBParameter { Value = TestPeriodEnd });
Expand All @@ -1554,6 +1556,9 @@ INSERT INTO server_properties
cmd.Parameters.Add(new DuckDBParameter { Value = socketCount });
cmd.Parameters.Add(new DuckDBParameter { Value = coresPerSocket });
cmd.Parameters.Add(new DuckDBParameter { Value = hadrEnabled });
cmd.Parameters.Add(new DuckDBParameter { Value = engineEdition });
cmd.Parameters.Add(new DuckDBParameter { Value = (object?)serviceObjective ?? DBNull.Value });
cmd.Parameters.Add(new DuckDBParameter { Value = (object?)vcoreCount ?? DBNull.Value });

await cmd.ExecuteNonQueryAsync();
}
Expand Down Expand Up @@ -2034,6 +2039,28 @@ public async Task SeedBurstyCpuAsync()
await SeedCpuUtilizationAlternatingAsync(low: 5, high: 85);
}

/// <summary>
/// Scenario 11: Azure SQL DB vCore tier.
/// sys.dm_os_sys_info reports 20 cores (compute node total), but the database
/// is provisioned as HS_Gen5_14 (14 vCores). FinOps should use 14, not 20.
///
/// Expected: CPU right-sizing uses vcore_count (14) instead of cpu_count (20).
/// </summary>
public async Task SeedAzureSqlDbVcoreAsync()
{
await ClearTestDataAsync();
await SeedTestServerAsync();

// Azure SQL DB: node has 20 cores, but this DB has HS_Gen5_14 (14 vCores)
// CPU at 8% avg — overprovisioned relative to 14 vCores
await SeedCpuUtilizationAsync(8, 2);
await SeedMemoryStatsAsync(totalPhysicalMb: 65_536, bufferPoolMb: 40_960, targetMb: 57_344);
await SeedServerPropertiesAsync(cpuCount: 20, htRatio: 1, physicalMemMb: 65_536,
edition: "SQL Azure", engineEdition: 5,
serviceObjective: "HS_Gen5_14", vcoreCount: 14);
await SeedFileSizeAsync(totalDataSizeMb: 51_200);
}

// ============================================
// Phase 3 Seed Helpers
// ============================================
Expand Down
16 changes: 15 additions & 1 deletion Lite/Database/DuckDbInitializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public void Dispose()
/// <summary>
/// Current schema version. Increment this when schema changes require table rebuilds.
/// </summary>
internal const int CurrentSchemaVersion = 23;
internal const int CurrentSchemaVersion = 24;

private readonly string _archivePath;

Expand Down Expand Up @@ -614,6 +614,20 @@ New tables only — no existing table changes needed. Tables created by
throw;
}
}

if (fromVersion < 24)
{
_logger?.LogInformation("Running migration to v24: adding vcore_count column to server_properties for Azure SQL DB vCore tracking");
try
{
await ExecuteNonQueryAsync(connection, "ALTER TABLE server_properties ADD COLUMN IF NOT EXISTS vcore_count INTEGER");
}
catch (Exception ex)
{
_logger?.LogError(ex, "Migration to v24 failed");
throw;
}
}
}

/// <summary>
Expand Down
3 changes: 2 additions & 1 deletion Lite/Database/Schema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -642,7 +642,8 @@ CREATE TABLE IF NOT EXISTS server_properties (
is_hadr_enabled BOOLEAN,
is_clustered BOOLEAN,
enterprise_features VARCHAR,
service_objective VARCHAR
service_objective VARCHAR,
vcore_count INTEGER
)";

public const string CreateServerPropertiesIndex = @"
Expand Down
4 changes: 2 additions & 2 deletions Lite/Services/LocalDataService.FinOps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ GROUP BY server_id
sp.product_level,
sp.product_update_level,
sp.engine_edition,
sp.cpu_count,
COALESCE(sp.vcore_count, sp.cpu_count) AS cpu_count,
sp.physical_memory_mb,
sp.socket_count,
sp.cores_per_socket,
Expand Down Expand Up @@ -476,7 +476,7 @@ ORDER BY collection_time DESC
LIMIT 1
),
server_info AS (
SELECT cpu_count
SELECT COALESCE(vcore_count, cpu_count) AS cpu_count
FROM v_server_properties
WHERE server_id = $1
ORDER BY collection_time DESC
Expand Down
27 changes: 27 additions & 0 deletions Lite/Services/RemoteCollectorService.ServerProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,15 @@ FROM sys.dm_os_sys_info AS osi
bool? isClustered = reader.IsDBNull(12) ? null : reader.GetBoolean(12);
var serviceObjective = reader.IsDBNull(13) ? null : reader.GetString(13);

/* For Azure SQL DB, sys.dm_os_sys_info.cpu_count returns the compute node's
total cores, not the per-database vCore allocation. Parse the actual vCore
count from the service_objective string (e.g. "HS_Gen5_14" → 14). */
int? vcoreCount = null;
if (isAzureSqlDb && !string.IsNullOrEmpty(serviceObjective))
{
vcoreCount = ParseVcoreFromServiceObjective(serviceObjective);
}

sqlSw.Stop();

var duckSw = Stopwatch.StartNew();
Expand Down Expand Up @@ -125,6 +134,7 @@ FROM sys.dm_os_sys_info AS osi
.AppendValue(isClustered)
.AppendValue((string?)null) // enterprise_features — not collected in Lite (requires cross-database cursor)
.AppendValue(serviceObjective)
.AppendValue(vcoreCount)
.EndRow();
rowsCollected++;
}
Expand All @@ -143,4 +153,21 @@ FROM sys.dm_os_sys_info AS osi
_logger?.LogDebug("Collected {RowCount} server properties row(s) for server '{Server}'", rowsCollected, server.DisplayName);
return rowsCollected;
}

/// <summary>
/// Parses the vCore count from an Azure SQL DB service objective string.
/// vCore tiers follow the pattern {Tier}_{Gen}_{VcoreCount}
/// (e.g. "HS_Gen5_14", "GP_Gen5_6", "BC_Gen5_8", "GP_S_Gen5_2").
/// DTU tiers (e.g. "P1", "S0") and elastic pools return null.
/// </summary>
internal static int? ParseVcoreFromServiceObjective(string serviceObjective)
{
var parts = serviceObjective.Split('_');
if (parts.Length >= 3 && int.TryParse(parts[^1], out var vcores) && vcores > 0)
{
return vcores;
}

return null;
}
}