You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
bug #54105 [Messenger] Improve deadlock handling on ack() and reject() (jwage)
This PR was squashed before being merged into the 6.4 branch.
Discussion
----------
[Messenger] Improve deadlock handling on `ack()` and `reject()`
| Q | A
| ------------- | ---
| Branch? | 6.4
| Bug fix? | yes
| New feature? | no
| Deprecations? | no
| Issues | Fix#54103
| License | MIT
We started getting this deadlock recently. It has happened only twice so far under high load.
```
SQLSTATE[40P01]: Deadlock detected: 7 ERROR: deadlock detected
DETAIL: Process 221664 waits for ShareLock on transaction 59539641; blocked by process 221671.
Process 221671 waits for AccessExclusiveLock on tuple (77,27) of relation 16455 of database 16385; blocked by process 221605.
Process 221605 waits for ShareLock on transaction 59539646; blocked by process 221606.
Process 221606 waits for AccessExclusiveLock on tuple (69,16) of relation 16455 of database 16385; blocked by process 221664.
HINT: See server log for query details.
CONTEXT: while deleting tuple (69,16) in relation "messenger_messages"
Process 221664 waits for ShareLock on transaction 59539641;
blocked by process 221671.
```
Here are the queries for each process:
```
Process 221664 waits for ShareLock on transaction 59539641;
blocked by process 221671.
221671
SELECT m.* FROM messenger_messages m WHERE (m.queue_name = $1) AND (m.delivered_at is null OR m.delivered_at < $2) AND (m.available_at <= $3) ORDER BY available_at ASC LIMIT 1 FOR UPDATE
221605
SELECT m.* FROM messenger_messages m WHERE (m.queue_name = $1) AND (m.delivered_at is null OR m.delivered_at < $2) AND (m.available_at <= $3) ORDER BY available_at ASC LIMIT 1 FOR UPDATE
221606
SELECT m.* FROM messenger_messages m WHERE (m.queue_name = $1) AND (m.delivered_at is null OR m.delivered_at < $2) AND (m.available_at <= $3) ORDER BY available_at ASC LIMIT 1 FOR UPDATE
221664
DELETE FROM messenger_messages WHERE id = $1
```
Open for discussion if this is the right way to handle this or not.
TODO:
- [x] Should there be a retry delay/exponential backoff/jitter? Retrying the failed delete that deadlocked immediately may not help.
- [x] Should `skip_locked` even be an option or should we always use skip locked?
- [x] Should we add `SKIP LOCKED` to the `FOR UPDATE`? It will reduce contention further. I was looking at how SolidQueue in Ruby On Rails handles this and it appears they use `SKIP LOCKED FOR UPDATE` https://github.com/basecamp/solid_queue/blob/fe57349a126efc381fe0adf4c1ec444bd8a4f53f/app/models/solid_queue/record.rb#L11
Commits
-------
38b67e7 [Messenger] Improve deadlock handling on `ack()` and `reject()`
'SELECT m.* FROM messenger_messages m WITH (UPDLOCK, ROWLOCK) WHERE (m.queue_name = ?) AND (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) ORDER BY available_at ASC OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY ',
587
+
'SELECT m.* FROM messenger_messages m WITH (UPDLOCK, ROWLOCK, READPAST) WHERE (m.queue_name = ?) AND (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) ORDER BY available_at ASC OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY ',
511
588
];
512
589
513
590
if (!class_exists(MySQL57Platform::class)) {
514
591
// DBAL >= 4
515
592
yield'Oracle' => [
516
593
newOraclePlatform(),
517
-
'SELECT w.id AS "id", w.body AS "body", w.headers AS "headers", w.queue_name AS "queue_name", w.created_at AS "created_at", w.available_at AS "available_at", w.delivered_at AS "delivered_at" FROM messenger_messages w WHERE w.id IN (SELECT m.id FROM messenger_messages m WHERE (m.queue_name = ?) AND (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) ORDER BY available_at ASC FETCH NEXT 1 ROWS ONLY) FOR UPDATE',
594
+
'SELECT w.id AS "id", w.body AS "body", w.headers AS "headers", w.queue_name AS "queue_name", w.created_at AS "created_at", w.available_at AS "available_at", w.delivered_at AS "delivered_at" FROM messenger_messages w WHERE w.id IN (SELECT m.id FROM messenger_messages m WHERE (m.queue_name = ?) AND (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) ORDER BY available_at ASC FETCH NEXT 1 ROWS ONLY) FOR UPDATE SKIP LOCKED',
518
595
];
519
596
} else {
520
597
// DBAL < 4
521
598
yield'Oracle' => [
522
599
newOraclePlatform(),
523
-
'SELECT w.id AS "id", w.body AS "body", w.headers AS "headers", w.queue_name AS "queue_name", w.created_at AS "created_at", w.available_at AS "available_at", w.delivered_at AS "delivered_at" FROM messenger_messages w WHERE w.id IN (SELECT a.id FROM (SELECT m.id FROM messenger_messages m WHERE (m.queue_name = ?) AND (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) ORDER BY available_at ASC) a WHERE ROWNUM <= 1) FOR UPDATE',
600
+
'SELECT w.id AS "id", w.body AS "body", w.headers AS "headers", w.queue_name AS "queue_name", w.created_at AS "created_at", w.available_at AS "available_at", w.delivered_at AS "delivered_at" FROM messenger_messages w WHERE w.id IN (SELECT a.id FROM (SELECT m.id FROM messenger_messages m WHERE (m.queue_name = ?) AND (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) ORDER BY available_at ASC) a WHERE ROWNUM <= 1) FOR UPDATE SKIP LOCKED',
Copy file name to clipboardExpand all lines: src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/DoctrinePostgreSqlRegularIntegrationTest.php
+14Lines changed: 14 additions & 0 deletions
Original file line number
Diff line number
Diff line change
@@ -57,6 +57,20 @@ public function testSendAndGetWithAutoSetupEnabledAndSetupAlready()
0 commit comments