diff --git a/src/SQLInsight.Repository/InsightRepository.InstanceConfiguration.cs b/src/SQLInsight.Repository/InsightRepository.InstanceConfiguration.cs
index f089a9c..8c6a60d 100644
--- a/src/SQLInsight.Repository/InsightRepository.InstanceConfiguration.cs
+++ b/src/SQLInsight.Repository/InsightRepository.InstanceConfiguration.cs
@@ -240,6 +240,24 @@ public sealed partial class InsightRepository
return updated > 0 ? ConfigurationChangeOutcome.Changed : await WhyNotAsync(id, cancellationToken).ConfigureAwait(false);
}
+ ///
+ /// The web UI (a choice of databases) and the agent (a discovery) both update many rows of one instance's
+ /// insight.ConfiguredDatabases in one transaction, in different orders; without taking turns they could deadlock.
+ /// Like the finding saves, they take an application lock first (one per instance; sp_getapplock needs no grant).
+ ///
+ private static async Task LockDatabasesOfAsync(System.Data.Common.DbConnection connection, System.Data.Common.DbTransaction transaction, int id)
+ {
+ var result = await connection.ExecuteScalarAsync("""
+ DECLARE @result int;
+ EXEC @result = sp_getapplock @Resource = @resource, @LockMode = N'Exclusive', @LockOwner = N'Transaction', @LockTimeout = 60000;
+ SELECT @result;
+ """, new { resource = "SQLInsight:ConfiguredDatabases:" + id.ToString(System.Globalization.CultureInfo.InvariantCulture) }, transaction).ConfigureAwait(false);
+ if (result < 0)
+ {
+ throw new TimeoutException($"Kunne ikke få lås på databasevalget (sp_getapplock {result}).");
+ }
+ }
+
private async Task WhyNotAsync(int id, CancellationToken cancellationToken) =>
await GetConfiguredInstanceAsync(id, cancellationToken).ConfigureAwait(false) is { FromConfigFile: true }
? ConfigurationChangeOutcome.FromConfigFile
@@ -264,6 +282,7 @@ public sealed partial class InsightRepository
ArgumentNullException.ThrowIfNull(choices);
await using var connection = await OpenAsync(cancellationToken).ConfigureAwait(false);
await using var transaction = await connection.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
+ await LockDatabasesOfAsync(connection, transaction, id).ConfigureAwait(false);
var unknown = new List();
foreach (var choice in choices)
{
@@ -439,6 +458,7 @@ public sealed partial class InsightRepository
await using var connection = await OpenAsync(cancellationToken).ConfigureAwait(false);
await using var transaction = await connection.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
var at = Utc(now);
+ await LockDatabasesOfAsync(connection, transaction, id).ConfigureAwait(false);
var monitorNew = await connection.ExecuteScalarAsync(
"SELECT MonitorNewDatabases FROM insight.ConfiguredInstances WITH (UPDLOCK) WHERE ConfiguredInstanceId = @id;", new { id }, transaction).ConfigureAwait(false)
?? throw new InvalidOperationException($"Instans {id} findes ikke.");