Skip to content

Commit 110f548

Browse files
authored
Lock Sliding Sync connections when inserting lazy members, to prevent repeated deadlocks. (#19826)
Got paged today for this. The sliding sync worker in question had loads of deadlocks in the logs. I restarted it and it got unwedged, but we should have a more robust defence, which this PR proposes. ``` psycopg2.errors.DeadlockDetected: deadlock detected DETAIL: Process 257324 waits for ShareLock on transaction 688227036; blocked by process 254908. Process 254908 waits for ShareLock on transaction 688222971; blocked by process 256179. Process 256179 waits for ExclusiveLock on tuple (302352,92) of relation 2962200779 of database 16403; blocked by process 257213. Process 257213 waits for ShareLock on transaction 688225005; blocked by process 254905. Process 254905 waits for ShareLock on transaction 688228814; blocked by process 257324. HINT: See server log for query details. CONTEXT: while inserting index tuple (183070,103) in relation "sliding_sync_connection_lazy_members" ``` I wonder if an unfortunate side effect is that these repeated attempts leave a lot of dead tuples on the table, which would then harm the performance of the next attempt to insert the tuples, I suspect making it more likely that they will deadlock again (?). --- By acquring a `FOR NO KEY UPDATE` lock upfront before beginning work, we can ensure that one of the transactions gets queued behind the other one, meaning the first one can succeed unimpeded. `FOR NO KEY UPDATE` blocks other `FOR NO KEY UPDATE` locks and is the weakest lock level that blocks itself. --------- Signed-off-by: Olivier 'reivilibre <oliverw@matrix.org>
1 parent 0ab9291 commit 110f548

2 files changed

Lines changed: 22 additions & 1 deletion

File tree

changelog.d/19826.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Lock Sliding Sync connections when inserting lazy members, to prevent repeated deadlocks.

synapse/storage/databases/main/sliding_sync.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,15 +186,35 @@ def persist_per_connection_state_txn(
186186
# First we fetch (or create) the connection key associated with the
187187
# previous connection position.
188188
if previous_connection_position is not None:
189+
lock_clause = ""
190+
if isinstance(self.database_engine, PostgresEngine):
191+
# Lock the sliding sync connection row for update upfront,
192+
# to prevent deadlocks between concurrent transactions
193+
# (which can retry again and again without making progress).
194+
#
195+
# (We don't need to explicitly lock in the other branch,
196+
# where we re-create the connection, as that implies a lock
197+
# anyway)
198+
#
199+
# Specifically, the statements seen to deadlock against
200+
# each other were
201+
# `INSERT INTO sliding_sync_connection_lazy_members`
202+
# with conflicting tuples on
203+
# "sliding_sync_connection_lazy_members_idx" UNIQUE, btree
204+
# (connection_key, room_id, user_id)
205+
# https://www.postgresql.org/docs/current/explicit-locking.html#LOCKING-ROWS
206+
lock_clause = "FOR NO KEY UPDATE OF sliding_sync_connections"
207+
189208
# The `previous_connection_position` is a user-supplied value, so we
190209
# need to make sure that the one they supplied is actually theirs.
191-
sql = """
210+
sql = f"""
192211
SELECT connection_key
193212
FROM sliding_sync_connection_positions
194213
INNER JOIN sliding_sync_connections USING (connection_key)
195214
WHERE
196215
connection_position = ?
197216
AND user_id = ? AND effective_device_id = ? AND conn_id = ?
217+
{lock_clause}
198218
"""
199219
txn.execute(
200220
sql, (previous_connection_position, user_id, device_id, conn_id)

0 commit comments

Comments
 (0)