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>
83 lines
4.4 KiB
C#
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);
|
|
}
|