sqlinsight-pro/src/SQLInsight.Rules/Concurrency/ConcurrencyRules.cs
Søren Eggert Lundsteen Olsen 0dc7866808 Separate "check by hand" from "not relevant" for version-gated rules
A rule whose topic applies but whose way of measuring needs a newer
version or patch now gives the new outcome ManualCheck with concrete
Danish guidance, instead of NotApplicable. It is marked explicitly per
rule (IRule.ManualCheck) and only used when every missing capability is
a measurement capability (InstanceCapabilities.IsMeasurement): CHECKDB
status (SQLI-DB-009), VLFs (SQLI-DB-011), spills and memory grants
(SQLI-PLAN-004/005) and deadlocks on 2005 (SQLI-DLK-001). Query Store,
AG and Azure-managed topics stay "not relevant".

Shown in a visible "Skal tjekkes manuelt" section on the instance page,
a KPI on the overview, a "Tjek manuelt" column in the management
report's controls, manualChecks in /api/v1/instances and manualCheck in
rule-states; not counted as not measurable.

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

134 lines
6.7 KiB
C#

using System.Globalization;
using SQLInsight.Core.Data;
using SQLInsight.Core.Findings;
using SQLInsight.Core.Instances;
using SQLInsight.Rules.Engine;
namespace SQLInsight.Rules.Concurrency;
/// <summary>
/// Blocking chains that have lasted long enough to hurt users.
/// </summary>
public sealed class BlockingRule : Rule
{
public const long MinBlockedMs = 30_000;
public override string Id => "SQLI-BLK-001";
public override string Title => "Langvarig blocking";
public override RuleEvaluation Evaluate(RuleContext context)
{
ArgumentNullException.ThrowIfNull(context);
var snapshot = context.Data.Get<BlockingSnapshot>();
var findings = new List<Finding>();
foreach (var head in snapshot.HeadBlockers)
{
var victims = AllBlockedBy(snapshot, head.SessionId);
var longest = victims.Max(v => v.WaitTimeMs ?? 0);
if (longest < MinBlockedMs)
{
continue;
}
var idleInTransaction = head.Status == "sleeping" && head.OpenTransactionCount > 0;
var evidence = new List<Evidence>
{
new("Head blocker", $"session {head.SessionId} ({head.Status}), {head.LoginName} fra {head.HostName} / {head.ProgramName}", Sensitive: true),
new("Åbne transaktioner", head.OpenTransactionCount.ToString(CultureInfo.InvariantCulture)),
new("Blokerede sessioner", victims.Count.ToString(CultureInfo.InvariantCulture)),
new("Længste ventetid", $"{Format.Number(longest / 1000.0)} s"),
new("Head blockers SQL", head.SqlText ?? "(ukendt)", Sensitive: true),
};
evidence.AddRange(victims.OrderByDescending(v => v.WaitTimeMs).Take(3).Select(v =>
new Evidence($"Blokeret session {v.SessionId}", $"{v.WaitType} på {v.WaitResource}: {v.SqlText}", Sensitive: true)));
findings.Add(CreateFinding(
longest >= 300_000 || victims.Count >= 10 ? Severity.High : Severity.Medium,
idleInTransaction
? $"Session {head.SessionId} har en åben transaktion uden at lave noget og blokerer {victims.Count} session(er)"
: $"Session {head.SessionId} blokerer {victims.Count} session(er) i op til {Format.Number(longest / 1000.0)} s",
idleInTransaction
? "Head blocker er 'sleeping' med en åben transaktion: applikationen har startet en transaktion, udført en ændring "
+ "og venter nu på noget andet (brugerinput, en webservice, en fejl der ikke blev håndteret) uden at committe. "
+ "Låsene holdes, til transaktionen afsluttes."
: "En session holder låse, som andre venter på. Så længe den kører, står alle bag den i kø.",
idleInTransaction
? "Find koden ud fra program, host og seneste SQL. Sørg for, at transaktioner altid afsluttes (også ved fejl: "
+ "try/finally, SET XACT_ABORT ON), og at der aldrig ventes på noget eksternt inde i en transaktion. "
+ "Akut: KILL af sessionen ruller transaktionen tilbage (det kan tage tid)."
: "Optimér head blockers forespørgsel (typisk manglende index), del store opdateringer i mindre batches, "
+ "og overvej READ_COMMITTED_SNAPSHOT, så læsere ikke blokeres.",
evidence,
subject: $"session: {head.SessionId}",
references: Docs.Learn("Understand and resolve blocking", "relational-databases/sql-server-transaction-locking-and-row-versioning-guide")));
}
return Result(findings);
}
private static List<SessionInfo> AllBlockedBy(BlockingSnapshot snapshot, int headSessionId)
{
var result = new List<SessionInfo>();
var queue = new Queue<int>([headSessionId]);
var seen = new HashSet<int> { headSessionId };
while (queue.Count > 0)
{
foreach (var blocked in snapshot.BlockedBy(queue.Dequeue()))
{
if (seen.Add(blocked.SessionId))
{
result.Add(blocked);
queue.Enqueue(blocked.SessionId);
}
}
}
return result;
}
}
/// <summary>
/// Deadlocks since the previous collection.
/// </summary>
public sealed class DeadlockRule : Rule
{
public override string Id => "SQLI-DLK-001";
public override string Title => "Deadlocks";
public override IReadOnlyCollection<Capability> RequiredCapabilities => [Capability.SystemHealthSession];
public override string ManualCheck =>
"SQL Server 2005 har ingen system_health-session. Slå trace flag 1222 til (DBCC TRACEON (1222, -1) og -T1222 i opstartsparametrene), "
+ "så skrives hver deadlock i SQL Servers fejllog, og gennemgå fejlloggen jævnligt.";
public override RuleEvaluation Evaluate(RuleContext context)
{
ArgumentNullException.ThrowIfNull(context);
var deadlocks = context.Data.Get<DeadlockSnapshot>().NewDeadlocks;
if (deadlocks.Count == 0)
{
return RuleEvaluation.Passed();
}
var evidence = new List<Evidence> { new("Antal", deadlocks.Count.ToString(CultureInfo.InvariantCulture)) };
foreach (var deadlock in deadlocks.TakeLast(3))
{
foreach (var p in deadlock.Processes)
{
evidence.Add(new Evidence(
$"{deadlock.Time:yyyy-MM-dd HH:mm:ss} {(p.IsVictim ? "offer" : "vinder")} session {p.SessionId}",
$"{p.LockMode} på {p.WaitResource}, {p.ClientApp}: {p.InputBuffer}", Sensitive: true));
}
}
return RuleEvaluation.Found(CreateFinding(
deadlocks.Count >= 10 ? Severity.High : Severity.Medium,
$"{deadlocks.Count} deadlock(s) siden sidste måling",
"To eller flere sessioner ventede på låse, som de andre holdt. SQL Server afbrød én af dem (offeret) med fejl 1205, "
+ "og dens transaktion blev rullet tilbage. Håndterer applikationen ikke fejlen, går arbejdet tabt.",
"Se deadlock-grafen for, hvilke objekter og forespørgsler der er involveret. Typiske løsninger: tilgå tabeller i samme "
+ "rækkefølge i alle transaktioner, gør transaktioner kortere, tilføj indexes så færre rækker låses, og lad "
+ "applikationen genforsøge ved fejl 1205. READ_COMMITTED_SNAPSHOT fjerner deadlocks mellem læsere og skrivere.",
evidence,
references: Docs.Learn("Deadlocks guide", "relational-databases/sql-server-deadlocks-guide")));
}
}