A new light collector samples sys.dm_exec_requests every 15 seconds:
waiting user sessions (not the agent's own) by wait type, database and
statement (same key as top SQL), with blocking. Sampling-only cycles
reuse the last probe and skip rule evaluation. Stored in
insight.ActiveWaitSamples (migration 0010) for RawDays.
Wait types on the instance page and in the analysis link to
/instanser/{navn}/ventetyper/{type}: Danish explanation from a new
knowledge base (shared with the SQLI-WAIT rules, with Dynamics AX notes),
trend over time, the statements that waited with share and estimated
wait, a link to the full text (/instanser/{navn}/sql) and, for lock
waits, to blocking in the same window. SQL Server 2005 compatible.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
130 lines
6.3 KiB
C#
130 lines
6.3 KiB
C#
using Microsoft.AspNetCore.Hosting;
|
|
using Microsoft.AspNetCore.Mvc.Testing;
|
|
using Microsoft.Data.SqlClient;
|
|
using SQLInsight.Collectors.Performance;
|
|
using SQLInsight.Core.Data;
|
|
using SQLInsight.Core.Instances;
|
|
using SQLInsight.Repository;
|
|
|
|
namespace SQLInsight.IntegrationTests;
|
|
|
|
/// <summary>Active session sampling against real SQL Server, and the wait type page.</summary>
|
|
[Collection(SqlServerTestGroup.Name)]
|
|
[Trait("Category", "Integration")]
|
|
public class WaitSamplingTests(SqlServerFixture sql)
|
|
{
|
|
private async Task<SqlConnection> OpenAsync(string application)
|
|
{
|
|
var connection = new SqlConnection(new SqlConnectionStringBuilder(sql.DatabaseConnectionString(SqlServerFixture.TestDatabase))
|
|
{
|
|
ApplicationName = application,
|
|
CommandTimeout = 120,
|
|
}.ConnectionString);
|
|
await connection.OpenAsync();
|
|
return connection;
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_blocked_session_is_sampled_with_its_wait_type_and_statement_but_the_agent_is_not()
|
|
{
|
|
var table = "WaitSample_" + Guid.NewGuid().ToString("N")[..6];
|
|
await sql.ExecuteAsSaAsync($"USE [{SqlServerFixture.TestDatabase}]; CREATE TABLE dbo.{table} (Id int PRIMARY KEY, Name nvarchar(20)); INSERT dbo.{table} VALUES (1, N'a');");
|
|
await using var holder = await OpenAsync("Lock holder");
|
|
await using var transaction = holder.BeginTransaction();
|
|
await using (var update = new SqlCommand($"UPDATE dbo.{table} SET Name = N'b' WHERE Id = 1;", holder, transaction))
|
|
{
|
|
await update.ExecuteNonQueryAsync();
|
|
}
|
|
|
|
await using var user = await OpenAsync("AX32");
|
|
await using var agentLike = await OpenAsync("SQLInsight Pro (test)");
|
|
var blocked = new SqlCommand($"SELECT Name FROM dbo.{table} WHERE Id = 1;", user).ExecuteScalarAsync();
|
|
var ignored = new SqlCommand($"SELECT Name FROM dbo.{table} WHERE Id = 1;", agentLike).ExecuteScalarAsync();
|
|
try
|
|
{
|
|
await using var session = await AgentSession.OpenAsync(sql);
|
|
WaitSampleSnapshot snapshot;
|
|
var attempts = 0;
|
|
do
|
|
{
|
|
await Task.Delay(500);
|
|
snapshot = await session.CollectAsync<WaitSampleSnapshot>(new WaitSamplingCollector());
|
|
}
|
|
while (snapshot.Rows.All(r => r.WaitType != "LCK_M_S") && ++attempts < 20);
|
|
|
|
var row = Assert.Single(snapshot.Rows, r => r.WaitType == "LCK_M_S");
|
|
Assert.Equal(1, row.Sessions); // the "SQLInsight Pro" connection waits too, but is the agent's own kind
|
|
Assert.Equal(1, row.BlockedSessions);
|
|
Assert.Equal(SqlServerFixture.TestDatabase, row.DatabaseName);
|
|
Assert.NotNull(row.QueryKey);
|
|
var statement = Assert.Single(snapshot.Statements, s => s.QueryKey == row.QueryKey);
|
|
Assert.Contains(table, statement.Text, StringComparison.Ordinal);
|
|
|
|
// The same key as top SQL, so the text and use can be joined.
|
|
Assert.StartsWith("0x", row.QueryKey, StringComparison.Ordinal);
|
|
}
|
|
finally
|
|
{
|
|
await transaction.RollbackAsync();
|
|
await Task.WhenAll(blocked, ignored);
|
|
await sql.ExecuteAsSaAsync($"USE [{SqlServerFixture.TestDatabase}]; DROP TABLE dbo.{table};");
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Wait_type_page_shows_the_explanation_trend_and_statements()
|
|
{
|
|
var database = "SQLInsightWait_" + Guid.NewGuid().ToString("N")[..8];
|
|
await sql.ExecuteAsSaAsync($"CREATE DATABASE [{database}];");
|
|
try
|
|
{
|
|
var repository = new InsightRepository(sql.DatabaseConnectionString(database));
|
|
await repository.MigrateAsync();
|
|
var info = new InstanceInfo
|
|
{
|
|
ServerName = "AX01",
|
|
Version = SqlServerVersion.Parse("16.0.4135.4"),
|
|
ProductLevel = "RTM",
|
|
Edition = "Standard Edition",
|
|
EngineEdition = EngineEdition.Standard,
|
|
};
|
|
var now = DateTimeOffset.UtcNow;
|
|
var id = await repository.UpsertInstanceAsync(@"AX01\AX30", info, "SqlClient", false, now);
|
|
for (var i = 0; i < 4; i++)
|
|
{
|
|
await repository.SaveCollectedAsync(id, "wait-sampling", WaitSampling.Aggregate(
|
|
[
|
|
new WaitingRequest(60 + i, "LCK_M_U", 3000, "AXDB", "0x1234ABCD", "UPDATE NUMBERSEQUENCETABLE SET NEXTREC = @P1", 55),
|
|
new WaitingRequest(70, "LCK_M_U", 1000, "AXDB", "0x9999", "SELECT 1", 0),
|
|
], now.AddSeconds(-15 * i)));
|
|
}
|
|
|
|
await using var factory = new WebApplicationFactory<Program>().WithWebHostBuilder(b =>
|
|
{
|
|
b.UseEnvironment("Development");
|
|
b.UseSetting("SQLInsight:Repository:ConnectionString", sql.DatabaseConnectionString(database));
|
|
b.UseSetting("SQLInsight:Web:Authentication", "None");
|
|
});
|
|
var client = factory.CreateClient();
|
|
var name = Uri.EscapeDataString(@"AX01\AX30");
|
|
var page = await client.GetStringAsync($"/instanser/{name}/ventetyper/LCK_M_U");
|
|
var text = await client.GetStringAsync($"/instanser/{name}/sql?noegle=0x1234ABCD");
|
|
var unknown = await client.GetStringAsync($"/instanser/{name}/ventetyper/NEW_WAIT_X");
|
|
|
|
Assert.Contains("Venter på en lås", page, StringComparison.Ordinal);
|
|
Assert.Contains("NUMBERSEQUENCETABLE", page, StringComparison.Ordinal);
|
|
Assert.Contains("50 %", page, StringComparison.Ordinal); // 4 of 8 samples
|
|
Assert.Contains("#blocking", page, StringComparison.Ordinal); // lock waits link to blocking
|
|
Assert.Contains("sql?noegle=0x1234ABCD", page, StringComparison.Ordinal);
|
|
Assert.Contains("UPDATE NUMBERSEQUENCETABLE", text, StringComparison.Ordinal);
|
|
Assert.Contains("uden dansk forklaring", unknown, StringComparison.Ordinal);
|
|
|
|
var statements = await repository.GetWaitTypeStatementsAsync(id, "LCK_M_U", now.AddMinutes(-5), now.AddMinutes(1));
|
|
Assert.Equal(4, statements.Single(s => s.QueryKey == "0x1234ABCD").Samples);
|
|
}
|
|
finally
|
|
{
|
|
await sql.ExecuteAsSaAsync($"ALTER DATABASE [{database}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE [{database}];");
|
|
}
|
|
}
|
|
}
|