sqlinsight-pro/tests/SQLInsight.Collectors.Tests/DeadlockParserTests.cs
Søren Eggert Lundsteen Olsen 25a67cb98b Stop SQLInsight deadlocking itself, and stop reporting its own deadlocks
At a customer whose repository sits on a monitored instance, the agent
reported 12 deadlocks that were its own: two instances' monitors save
findings in parallel, one transaction each, and while insight.Findings is
small SQL Server scans it, so the two lock each other's rows.

- SaveRuleResultsAsync takes an exclusive application lock for its
  transaction (the agent is the only writer; a save takes milliseconds)
  and retries if chosen as deadlock victim anyway
- repository connections are named "SQLInsight Pro (repository)"
- deadlocks where every participant is SQLInsight Pro are not reported
  (the read position still moves past them); any other session keeps it

The new integration test (two instances saving 25 rules x 40 rounds in
parallel) failed 2 of 2 without the fix and passed 5 of 5 with it.

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

86 lines
3.5 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Xml.Linq;
using SQLInsight.Collectors.Concurrency;
namespace SQLInsight.Collectors.Tests;
public class DeadlockParserTests
{
private const string Graph = """
<deadlock>
<victim-list><victimProcess id="process1" /></victim-list>
<process-list>
<process id="process1" spid="60" waitresource="KEY: 5:72057594043236352 (8194443284a0)" lockMode="U" isolationlevel="read committed (2)"
clientapp="Sales.Api" hostname="WEB01" loginname="app" currentdb="5" currentdbname="Sales">
<inputbuf> UPDATE dbo.Orders SET Status = 2 WHERE Id = @id </inputbuf>
</process>
<process id="process2" spid="61" waitresource="KEY: 5:72057594043236352 (61a06abd401c)" lockMode="U" currentdb="5">
<inputbuf>UPDATE dbo.Orders SET Status = 3 WHERE Id = @id</inputbuf>
</process>
</process-list>
<resource-list />
</deadlock>
""";
[Fact]
public void Parses_event_file_format()
{
var evt = XElement.Parse($"""
<event name="xml_deadlock_report" timestamp="2026-09-29T10:15:30.123Z">
<data name="xml_report"><value>{Graph}</value></data>
</event>
""");
var deadlock = DeadlockParser.ParseEvent(evt)!;
Assert.Equal(new DateTimeOffset(2026, 9, 29, 10, 15, 30, 123, TimeSpan.Zero), deadlock.Time);
Assert.Equal(2, deadlock.Processes.Count);
var victim = Assert.Single(deadlock.Processes, p => p.IsVictim);
Assert.Equal(60, victim.SessionId);
Assert.Equal("Sales", victim.DatabaseName);
Assert.Equal("Sales.Api", victim.ClientApp);
Assert.Equal("UPDATE dbo.Orders SET Status = 2 WHERE Id = @id", victim.InputBuffer);
Assert.Equal("5", deadlock.Processes[1].DatabaseName); // no currentdbname on older builds
Assert.StartsWith("<deadlock>", deadlock.GraphXml, StringComparison.Ordinal);
}
[Fact]
public void Parses_sql2008_escaped_graph()
{
// SQL Server 2008 RTM–SP1 put the graph in the ring buffer as escaped text.
var escaped = System.Security.SecurityElement.Escape(Graph);
var evt = XElement.Parse($"""
<event name="xml_deadlock_report" timestamp="2026-09-29T10:15:30Z">
<data name="xml_report"><type name="unicode_string" /><value>{escaped}</value></data>
</event>
""");
var deadlock = DeadlockParser.ParseEvent(evt)!;
Assert.Equal(2, deadlock.Processes.Count);
}
[Fact]
public void Garbage_is_ignored()
{
var evt = XElement.Parse("""<event name="xml_deadlock_report" timestamp="2026-09-29T10:15:30Z"><data name="xml_report"><value>not xml</value></data></event>""");
Assert.Null(DeadlockParser.ParseEvent(evt));
}
[Theory]
[InlineData("SQLInsight Pro (repository)", "SQLInsight Pro (repository)", true)]
[InlineData("SQLInsight Pro", "SQLInsight Pro (repository)", true)]
[InlineData("SQLInsight Pro (repository)", "Sales.Api", false)]
[InlineData("SQLInsight Pro (repository)", null, false)]
public void Deadlocks_between_sqlinsights_own_sessions_are_recognised(string first, string? second, bool own)
{
var deadlock = new SQLInsight.Core.Data.DeadlockEvent
{
Time = DateTimeOffset.UnixEpoch,
GraphXml = "<deadlock />",
Processes = [new() { Id = "p1", ClientApp = first }, new() { Id = "p2", ClientApp = second }],
};
Assert.Equal(own, deadlock.IsOwn);
}
}