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