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;
/// Active session sampling against real SQL Server, and the wait type page.
[Collection(SqlServerTestGroup.Name)]
[Trait("Category", "Integration")]
public class WaitSamplingTests(SqlServerFixture sql)
{
private async Task 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(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().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}];");
}
}
}