Problem/Motivation
On a production site if I have two runners both processing the same queues I hit deadlocks.
Executing: /var/www/html/vendor/bin/drush advancedqueue:queue:process entity_usage_custom_views --timeout=20 --uri=REDACTED [1088.15 sec, 14.75 MB]
>
> In ExceptionHandler.php line 96:
>
> SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to
> get lock; try restarting transaction: UPDATE "advancedqueue" SET "payload"=
> :db_update_placeholder_0, "state"=:db_update_placeholder_1, "message"=:db_u
> pdate_placeholder_2, "num_retries"=:db_update_placeholder_3, "available"=:d
> b_update_placeholder_4, "processed"=:db_update_placeholder_5, "expires"=:db
> _update_placeholder_6
> WHERE "job_id" = :db_condition_placeholder_0; Array
> (
> [:db_update_placeholder_0] => {"entity_type":"media","entity_id":2468}
> [:db_update_placeholder_1] => success
> [:db_update_placeholder_2] =>
> [:db_update_placeholder_3] => 0
> [:db_update_placeholder_4] => 1787741344
> [:db_update_placeholder_5] => 1787741580
> [:db_update_placeholder_6] => 0
> [:db_condition_placeholder_0] => 237422
> )
>
>
> In PdoTrait.php line 109:
>
> SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to
> get lock; try restarting transaction
Proposed resolution
This occurs because we call cleanupQueue a lot and this updates lots of rows using the secondary index and claim and update use the primary index to grab a single row.
The fix is to:
- Scope cleanup to the current queue: add ->condition('queue_id', $this->queueId) to the cleanupQueue() update. Cuts cross-queue contention entirely (the biggest win, one line).
- Replace the range UPDATE with select-ids-then-update-by-PK: SELECT job_id FROM {advancedqueue} WHERE queue_id = :qid AND expires <> 0 AND expires < :now ORDER BY job_id ASC, then UPDATE ... WHERE job_id IN (:ids). This swaps secondary-index gap locks for primary-key row locks — the same lock type claimJob()/updateJob() already take — and locking in ascending job_id order gives all code paths a consistent global lock order, which is the standard deadlock-avoidance technique.
- Retry on deadlock: wrap the cleanup update (and claimJob()'s claim update) in a small retry loop that catches \Drupal\Core\Database\DatabaseExceptionWrapper, checks for SQLSTATE 40001, and retries a few times with jitter. Deadlocks can't be fully eliminated under arbitrary concurrency — MySQL always picks a victim to roll back — so the app must be able to retry that victim.
Remaining tasks
Implement and test.
User interface changes
API changes
Data model changes
Issue fork advancedqueue-3619249
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
alexpottComment #4
alexpottComment #5
alexpottI've tested this on a production release that was getting deadlocks and now it's not deadlocking so that's great.
Comment #7
alexpott