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}];"); } } }