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