sqlinsight-pro/src/SQLInsight.Collectors/Concurrency/BlockingCollector.cs
Søren Eggert Lundsteen Olsen 6b945295d0 Dynamics AX collectors: shared detector, batch jobs, number sequences and RecId
dynamics-ax (hourly) lists the monitored AX databases with version, schema,
companies and read access; discovery and the AX community rules (now marked
dynamicsAx) use the same detection. dynamics-ax-batch (5 min) reads BATCH
(3.0/4.0) or BATCHJOB/BATCHJOBHISTORY (2009+), keeps each job's own run
history; dynamics-ax-sequences (hourly) reads NUMBERSEQUENCETABLE and
SYSTEMSEQUENCES and forecasts from SQLInsight's own daily samples. All reads
run through [db].sys.sp_executesql under read committed with a 2 s lock
timeout (snapshot when allowed), bounded with TOP, never NOLOCK, never
written. Blocking now captures CONTEXT_INFO for the AX user mapping.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 13:53:34 +02:00

83 lines
4.4 KiB
C#

using SQLInsight.Core.Data;
using SQLInsight.Core.Instances;
namespace SQLInsight.Collectors.Concurrency;
/// <summary>
/// Blocked sessions and their blockers right now. Head blockers are often sleeping sessions with an open
/// transaction, which have no row in <c>sys.dm_exec_requests</c>, so sessions are the starting point.
/// </summary>
public sealed class BlockingCollector : Collector<BlockingSnapshot>
{
public override string Name => "blocking";
public override TimeSpan DefaultInterval => TimeSpan.FromMinutes(1);
protected override async Task<BlockingSnapshot> 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);
}