using SQLInsight.Core.Data; using SQLInsight.Core.Instances; namespace SQLInsight.Collectors.Concurrency; /// /// Blocked sessions and their blockers right now. Head blockers are often sleeping sessions with an open /// transaction, which have no row in sys.dm_exec_requests, so sessions are the starting point. /// public sealed class BlockingCollector : Collector { public override string Name => "blocking"; public override TimeSpan DefaultInterval => TimeSpan.FromMinutes(1); protected override async Task CollectAsync(CollectorContext context, BlockingSnapshot? previous) { var version = context.Instance.Version; var sessions = version.Major >= 9 ? await Db.QueryAsync(context.Connection, BuildSql2005(version), Map2005, context.CancellationToken).ConfigureAwait(false) : await Db.QueryAsync(context.Connection, Sql2000, Map2000, context.CancellationToken).ConfigureAwait(false); return new BlockingSnapshot { CapturedAt = context.Now, Sessions = sessions }; } internal static string BuildSql2005(SqlServerVersion version) { var openTran = version.Major >= 11 ? "s.open_transaction_count" : "(SELECT COUNT(*) FROM sys.dm_tran_session_transactions st WHERE st.session_id = s.session_id)"; var database = version.Major >= 11 ? "COALESCE(r.database_id, s.database_id)" : "r.database_id"; // CONTEXT_INFO, where Dynamics AX 2009/2012 AOS can put the AX user (sys.dm_exec_sessions has it from 2008). var contextInfo = version.Major >= 10 ? "s.context_info" : "(SELECT TOP 1 p.context_info FROM sys.sysprocesses p WHERE p.spid = s.session_id AND p.ecid = 0)"; // blocking_session_id < 0 are special cases (orphaned DTC, deferred recovery, latch) and not a session. return $""" SELECT s.session_id, NULLIF(r.blocking_session_id, 0), COALESCE(r.status, s.status), r.wait_type, r.wait_time, r.wait_resource, DB_NAME({database}), s.login_name, s.host_name, s.program_name, {openTran}, LEFT(CASE WHEN r.sql_handle IS NOT NULL AND r.statement_start_offset IS NOT NULL THEN SUBSTRING(t.text, r.statement_start_offset / 2 + 1, (CASE WHEN r.statement_end_offset = -1 THEN DATALENGTH(t.text) ELSE r.statement_end_offset END - r.statement_start_offset) / 2 + 1) ELSE t.text END, 4000), s.last_request_end_time, {contextInfo} FROM sys.dm_exec_sessions s LEFT JOIN sys.dm_exec_requests r ON r.session_id = s.session_id LEFT JOIN sys.dm_exec_connections c ON c.session_id = s.session_id OUTER APPLY sys.dm_exec_sql_text(COALESCE(r.sql_handle, c.most_recent_sql_handle)) t WHERE r.blocking_session_id > 0 OR s.session_id IN (SELECT blocking_session_id FROM sys.dm_exec_requests WHERE blocking_session_id > 0) """; } // blocked = spid is a session waiting on its own latch, not blocking. internal const string Sql2000 = """ SELECT p.spid, NULLIF(CASE WHEN p.blocked = p.spid THEN 0 ELSE p.blocked END, 0), RTRIM(p.status), RTRIM(p.lastwaittype), p.waittime, RTRIM(p.waitresource), DB_NAME(p.dbid), RTRIM(p.loginame), RTRIM(p.hostname), RTRIM(p.program_name), p.open_tran, CAST(NULL AS nvarchar(4000)), p.last_batch, p.context_info FROM master.dbo.sysprocesses p WHERE (p.blocked > 0 AND p.blocked <> p.spid) OR p.spid IN (SELECT blocked FROM master.dbo.sysprocesses WHERE blocked > 0 AND blocked <> spid) """; private static SessionInfo Map2005(Row r) => new() { SessionId = r.Int(0), BlockingSessionId = r.NullableInt(1), Status = r.NullableString(2), WaitType = r.NullableString(3), WaitTimeMs = r.NullableLong(4), WaitResource = r.NullableString(5), DatabaseName = r.NullableString(6), LoginName = r.NullableString(7), HostName = r.NullableString(8), ProgramName = r.NullableString(9), OpenTransactionCount = r.NullableInt(10) ?? 0, SqlText = r.NullableString(11), LastRequestEndTime = r.NullableDateTime(12), ContextInfo = r.NullableHex(13), }; private static SessionInfo Map2000(Row r) => Map2005(r); }