Skip to content

Fix deadlocks when concurrent requests write tracked options - #81

Merged
ilicfilip merged 1 commit into
developfrom
filip/fix-tracking-deadlock
Oct 8, 2026
Merged

ilicfilip merged 1 commit into
developfrom
filip/fix-tracking-deadlock

Conversation

@ilicfilip

Copy link
Copy Markdown
Collaborator

Problem

A user reported Deadlock found when trying to get lock errors in their logs, attributed to Option Optimizer.

Every request writes the options it read to wp_option_optimizer_tracked on shutdown, via Database::batch_insert() (INSERT … ON DUPLICATE KEY UPDATE inside a transaction). InnoDB locks rows in VALUES order, which followed each request's option access order. Concurrent requests (front end, admin, REST, cron) therefore locked the same hot rows (siteurl, active_plugins, …) in different orders and deadlocked. MySQL rolled back the victim and that request's counts were lost. The COMMIT sent afterwards also cleared $wpdb->last_error, so the failure was silent in code.

Fix

  • ksort( $options, SORT_STRING ) before writing, so all writers lock rows in one order. SORT_STRING because numeric option names become int keys.
  • On a failed insert: ROLLBACK (instead of falling through to COMMIT), preserving the error.
  • Retry once on a deadlock. The deadlock rolls back the whole transaction, so the retry can't double count.

Covers both callers: the per-request shutdown write and the legacy-data migration.

Verification (local, MySQL 8.0.33)

Load test: 20 parallel processes × 50 batch_insert() calls of 200 shuffled real option names. Deadlocks counted with information_schema.INNODB_METRICS.lock_deadlocks.

Deadlocks / 1000 writes
Before (develop) 919, 913
After (this PR) 0, 0, 0 (all 200,000 increments landed each run)
  • Retry path: forced a real deadlock against a second MySQL session. The plugin's write was the victim, retried and succeeded; each row got exactly +1.
  • PHPCS and lint clean. PHPStan unchanged vs develop (all 44 existing errors are in node_modules).

Not tested: MariaDB. A deadlock that the retry recovers from is still logged once by wpdb, but these should now be rare.

🤖 Generated with Claude Code

Every request writes the options it read to wp_option_optimizer_tracked
on shutdown with INSERT ... ON DUPLICATE KEY UPDATE. InnoDB locks rows in
VALUES order, which followed per-request access order, so concurrent
requests locked the same rows in different orders and deadlocked
("Deadlock found when trying to get lock"), losing that request's counts.

Sort the options before writing so all writers lock in one order. On a
failed insert, ROLLBACK instead of sending COMMIT (which also hid the
error by clearing $wpdb->last_error), and retry once on a deadlock; the
deadlock rolls back the whole transaction, so the retry can't double count.

Reproduced locally with 20 parallel writers x 50 batch_insert() calls of
200 shuffled options: ~920/1000 deadlocks before, 0/1000 after with all
200000 increments landed. Retry path verified with a forced deadlock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown

Test on Playground
Test this pull request on the Playground
or download the zip

@ilicfilip
ilicfilip marked this pull request as ready for review October 5, 2026 07:07
@ilicfilip
ilicfilip merged commit b5bb99f into develop Oct 8, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants