using System.Globalization;
using SQLInsight.Core.Data;
using SQLInsight.Core.Findings;
using SQLInsight.Core.Instances;
using SQLInsight.Rules.Engine;
namespace SQLInsight.Rules.Concurrency;
///
/// Blocking chains that have lasted long enough to hurt users.
///
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();
var findings = new List();
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
{
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 AllBlockedBy(BlockingSnapshot snapshot, int headSessionId)
{
var result = new List();
var queue = new Queue([headSessionId]);
var seen = new HashSet { 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;
}
}
///
/// Deadlocks since the previous collection.
///
public sealed class DeadlockRule : Rule
{
public override string Id => "SQLI-DLK-001";
public override string Title => "Deadlocks";
public override IReadOnlyCollection 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().NewDeadlocks;
if (deadlocks.Count == 0)
{
return RuleEvaluation.Passed();
}
var evidence = new List { 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")));
}
}