Skip to content

Commit 3688c49

Browse files
committed
Execute message-append interrupt as a separate command to avoid lock contention
Routing single appends through AppendMessages merged the INSERT and the interrupt UPDATE into one command/connection in the SqlServer and MariaDB stores. Holding the insert's locks while the interrupt UPDATE ran deadlocked tight message-exchange loops (ping-pong) where two executing flows interrupt each other - SQL Server's unbounded lock-wait made it hang indefinitely. Execute the interrupt as a separate command after the insert (matching the previous append-then-schedule design). PostgreSQL already runs them as separate auto-committing batch commands and is unaffected.
1 parent 0fa440b commit 3688c49

2 files changed

Lines changed: 17 additions & 10 deletions

File tree

Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB/MariaDbMessageStore.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,17 @@ public async Task AppendMessages(IReadOnlyList<StoredIdAndMessage> messages)
5858
var appendMessagesCommand = _sqlGenerator.AppendMessages(messages);
5959
var interruptsCommand = _sqlGenerator.Interrupt(storedIds);
6060

61-
var command = StoreCommand.Merge(appendMessagesCommand, interruptsCommand);
62-
6361
await using var conn = await DatabaseHelper.CreateOpenConnection(_connectionString);
64-
await using var sqlCommand = command.ToSqlCommand(conn);
6562

66-
await sqlCommand.ExecuteNonQueryAsync();
63+
// The append and the interrupt are executed as separate commands - not merged into one - so the
64+
// insert's locks are released before the interrupt UPDATE runs. Merging them caused lock contention
65+
// that deadlocked tight message-exchange loops (e.g. ping-pong) where two executing flows interrupt
66+
// each other.
67+
await using var appendCommand = appendMessagesCommand.ToSqlCommand(conn);
68+
await appendCommand.ExecuteNonQueryAsync();
69+
70+
await using var interruptCommand = interruptsCommand.ToSqlCommand(conn);
71+
await interruptCommand.ExecuteNonQueryAsync();
6772
}
6873

6974
private string? _replaceMessageSql;

Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer/SqlServerMessageStore.cs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,7 @@ public async Task AppendMessages(IReadOnlyList<StoredIdAndMessage> messages)
7575
INSERT INTO {_tablePrefix}_Messages
7676
(Id, Replica, Content)
7777
VALUES
78-
{messages.Select((_, i) => $"(@Id{i}, COALESCE((SELECT Owner FROM {_tablePrefix} WHERE Id = @Id{i}), @Replica{i}), @Content{i})").StringJoin($",{Environment.NewLine}")};
79-
80-
{interuptsSql.Sql}";
78+
{messages.Select((_, i) => $"(@Id{i}, COALESCE((SELECT Owner FROM {_tablePrefix} WHERE Id = @Id{i}), @Replica{i}), @Content{i})").StringJoin($",{Environment.NewLine}")};";
8179

8280
await using var command = new SqlCommand(sql, conn);
8381
for (var i = 0; i < messages.Count; i++)
@@ -88,10 +86,14 @@ INSERT INTO {_tablePrefix}_Messages
8886
command.Parameters.AddWithValue($"@Replica{i}", replica.AsGuid);
8987
command.Parameters.AddWithValue($"@Content{i}", content);
9088
}
91-
foreach (var (value, name) in interuptsSql.Parameters)
92-
command.Parameters.AddWithValue(name, value);
93-
9489
await command.ExecuteNonQueryAsync();
90+
91+
// The interrupt is executed as a separate command - not merged into the insert above - so the
92+
// insert's locks are released before the interrupt UPDATE runs. Merging them caused lock contention
93+
// that deadlocked tight message-exchange loops (e.g. ping-pong) where two executing flows interrupt
94+
// each other (SQL Server's lock-wait is unbounded, so it hung indefinitely).
95+
await using var interruptCommand = interuptsSql.ToSqlCommand(conn);
96+
await interruptCommand.ExecuteNonQueryAsync();
9597
}
9698

9799
private string? _replaceMessageSql;

0 commit comments

Comments
 (0)