From 92aa15aed2ef1cf106625ef35f0b09b7cf2fce33 Mon Sep 17 00:00:00 2001 From: Arcadiy Ivanov Date: Sat, 21 Feb 2026 22:47:06 -0500 Subject: [PATCH] MDEV-37977 InnoDB deadlock report incorrectly reports rolled back transaction number The "WE ROLL BACK TRANSACTION (N)" message in the deadlock report referred to the wrong transaction number. The victim selection loop and the display loop in `Deadlock::report()` traversed the cycle in the same order (`cycle->wait_trx, ..., cycle`) but used misaligned position numbering: - **Victim selection** initialized `victim = cycle` at position 1 *before* the loop, then started iterating from `cycle->wait_trx` at position 2. - **Display loop** started from `cycle->wait_trx` at label `(1)`, with `cycle` displayed last at label `(N)`. This caused `victim_pos` to be off by one relative to the displayed transaction labels. Fix: restructure the victim selection loop to start with `l=0` and `victim=nullptr`, letting the loop handle all transactions uniformly. The first iteration unconditionally picks `cycle->wait_trx` as the initial victim at position 1, matching the display loop. The `thd_deadlock_victim_preference()` call is guarded with a `victim != nullptr` check to skip it on the first iteration (where no prior victim exists to compare against). --- mysql-test/suite/innodb/r/mdev_37977.result | 36 +++++++++++++ mysql-test/suite/innodb/t/mdev_37977.test | 59 +++++++++++++++++++++ storage/innobase/lock/lock0lock.cc | 33 +++++++----- 3 files changed, 116 insertions(+), 12 deletions(-) create mode 100644 mysql-test/suite/innodb/r/mdev_37977.result create mode 100644 mysql-test/suite/innodb/t/mdev_37977.test diff --git a/mysql-test/suite/innodb/r/mdev_37977.result b/mysql-test/suite/innodb/r/mdev_37977.result new file mode 100644 index 0000000000000..4d7a440405635 --- /dev/null +++ b/mysql-test/suite/innodb/r/mdev_37977.result @@ -0,0 +1,36 @@ +# +# MDEV-37977 InnoDB deadlock report incorrectly reports +# rolled back transaction number +# +SET @save_print_all_deadlocks= @@GLOBAL.innodb_print_all_deadlocks; +SET GLOBAL innodb_print_all_deadlocks= ON; +CREATE TABLE t1 (id INT PRIMARY KEY, val INT) ENGINE=InnoDB; +INSERT INTO t1 VALUES (1, 10), (2, 20); +# +# Classic deadlock: con1 locks row 1 then tries row 2; +# default locks row 2 then tries row 1. +# The requesting transaction (default) is always the preferred +# victim due to bit 0 in calc_victim_weight(). In a 2-transaction +# cycle, find_cycle() returns the other transaction, so the +# requesting transaction is displayed as "(1) TRANSACTION". +# The rollback message must say "WE ROLL BACK TRANSACTION (1)". +# +connect con1,localhost,root,,; +BEGIN; +UPDATE t1 SET val= 11 WHERE id= 1; +connection default; +BEGIN; +UPDATE t1 SET val= 22 WHERE id= 2; +connection con1; +UPDATE t1 SET val= 12 WHERE id= 2; +connection default; +UPDATE t1 SET val= 21 WHERE id= 1; +ERROR 40001: Deadlock found when trying to get lock; try restarting transaction +ROLLBACK; +connection con1; +ROLLBACK; +disconnect con1; +connection default; +FOUND 1 /WE ROLL BACK TRANSACTION \(1\)/ in mysqld.1.err +SET GLOBAL innodb_print_all_deadlocks= @save_print_all_deadlocks; +DROP TABLE t1; diff --git a/mysql-test/suite/innodb/t/mdev_37977.test b/mysql-test/suite/innodb/t/mdev_37977.test new file mode 100644 index 0000000000000..4b4078c18f1f1 --- /dev/null +++ b/mysql-test/suite/innodb/t/mdev_37977.test @@ -0,0 +1,59 @@ +--echo # +--echo # MDEV-37977 InnoDB deadlock report incorrectly reports +--echo # rolled back transaction number +--echo # + +--source include/not_embedded.inc +--source include/have_innodb.inc +--source include/count_sessions.inc + +SET @save_print_all_deadlocks= @@GLOBAL.innodb_print_all_deadlocks; +SET GLOBAL innodb_print_all_deadlocks= ON; + +CREATE TABLE t1 (id INT PRIMARY KEY, val INT) ENGINE=InnoDB; +INSERT INTO t1 VALUES (1, 10), (2, 20); + +--echo # +--echo # Classic deadlock: con1 locks row 1 then tries row 2; +--echo # default locks row 2 then tries row 1. +--echo # The requesting transaction (default) is always the preferred +--echo # victim due to bit 0 in calc_victim_weight(). In a 2-transaction +--echo # cycle, find_cycle() returns the other transaction, so the +--echo # requesting transaction is displayed as "(1) TRANSACTION". +--echo # The rollback message must say "WE ROLL BACK TRANSACTION (1)". +--echo # + +--connect (con1,localhost,root,,) +BEGIN; +UPDATE t1 SET val= 11 WHERE id= 1; + +--connection default +BEGIN; +UPDATE t1 SET val= 22 WHERE id= 2; + +--connection con1 +--send UPDATE t1 SET val= 12 WHERE id= 2 + +--connection default +let $wait_condition= + SELECT COUNT(*) >= 2 FROM INFORMATION_SCHEMA.INNODB_LOCKS + WHERE lock_table LIKE '%t1%'; +--source include/wait_condition.inc + +--error ER_LOCK_DEADLOCK +UPDATE t1 SET val= 21 WHERE id= 1; +ROLLBACK; + +--connection con1 +--reap +ROLLBACK; +--disconnect con1 + +--connection default +let SEARCH_FILE= $MYSQLTEST_VARDIR/log/mysqld.1.err; +let SEARCH_PATTERN= WE ROLL BACK TRANSACTION \(1\); +--source include/search_pattern_in_file.inc + +SET GLOBAL innodb_print_all_deadlocks= @save_print_all_deadlocks; +DROP TABLE t1; +--source include/wait_until_count_sessions.inc diff --git a/storage/innobase/lock/lock0lock.cc b/storage/innobase/lock/lock0lock.cc index 572a7591a775d..58723fcd5968a 100644 --- a/storage/innobase/lock/lock0lock.cc +++ b/storage/innobase/lock/lock0lock.cc @@ -6959,7 +6959,7 @@ and less modified rows. Bit 0 is used to prefer orig_trx in case of a tie. } { - unsigned l= 1; + unsigned l= 0; /* Now that we are holding lock_sys.wait_mutex again, check whether a cycle still exists. */ trx_t *cycle= find_cycle(trx); @@ -6967,22 +6967,30 @@ and less modified rows. Bit 0 is used to prefer orig_trx in case of a tie. goto func_exit; /* One of the transactions was already aborted. */ lock_sys.deadlocks++; - victim= cycle; - undo_no_t victim_weight= calc_victim_weight(victim, trx); - unsigned victim_pos= l; + /* Select the victim among the cycle participants. Traverse + the cycle in the same order as the display loop below + (cycle->wait_trx, ..., cycle as positions 1, 2, ..., N) + so that victim_pos matches the displayed transaction number. */ + undo_no_t victim_weight= 0; + unsigned victim_pos= 0; for (trx_t *next= cycle;;) { next= next->lock.wait_trx; l++; const undo_no_t next_weight= calc_victim_weight(next, trx); #ifdef HAVE_REPLICATION - const int pref= - thd_deadlock_victim_preference(victim->mysql_thd, next->mysql_thd); - /* Set bit 63 for any non-preferred victim to make such preference take - priority in the weight comparison. - -1 means victim is preferred. 1 means next is preferred. */ - undo_no_t victim_not_pref= (1ULL << 63) & (undo_no_t)(int64_t)(-pref); - undo_no_t next_not_pref= (1ULL << 63) & (undo_no_t)(int64_t)pref; + undo_no_t victim_not_pref= 0; + undo_no_t next_not_pref= 0; + if (UNIV_LIKELY(victim != nullptr)) + { + const int pref= + thd_deadlock_victim_preference(victim->mysql_thd, next->mysql_thd); + /* Set bit 63 for any non-preferred victim to make such preference + take priority in the weight comparison. + -1 means victim is preferred. 1 means next is preferred. */ + victim_not_pref= (1ULL << 63) & (undo_no_t)(int64_t)(-pref); + next_not_pref= (1ULL << 63) & (undo_no_t)(int64_t)pref; + } #else undo_no_t victim_not_pref= 0; undo_no_t next_not_pref= 0; @@ -6996,7 +7004,8 @@ and less modified rows. Bit 0 is used to prefer orig_trx in case of a tie. - Else the TRX_WEIGHT in bits 1-61 will decide, if not equal. - Else, if one of them is the original trx, bit 0 will decide. - If all is equal, previous victim will arbitrarily be chosen. */ - if ((next_weight|next_not_pref) < (victim_weight|victim_not_pref)) + if (UNIV_UNLIKELY(victim == nullptr) || + (next_weight|next_not_pref) < (victim_weight|victim_not_pref)) { victim_weight= next_weight; victim= next;