From 0235a528e3c2cf8c3197e4817596c9fa1947d2b8 Mon Sep 17 00:00:00 2001 From: Vlad Lesin Date: Thu, 3 Nov 2022 10:00:03 +0300 Subject: MDEV-10087 mysqld_update()/mysql_delete() continues execution even after subquery with JOIN gets error from storage engine The issue is that record_should_be_deleted() returns true in mysql_delete() even if sub-select with join gets error from storage engine when DELETE FROM ... WHERE ... IN (SELECT ...) statement is executed. The same is true for mysql_update() where select->skip_record() returns true even if sub-select with join gets error from storage engine. In the test case if sub-select is chosen as deadlock victim the whole transaction is rolled back during sub-select execution, but mysql_delete()/mysql_update() continues transaction execution and invokes table->delete_row() as record_should_be_deleted() wrongly returns true in mysql_delete() and table->update_row() as select->skip_record(thd) wrongly returns 1 for mysql_update(). record_should_be_deleted() wrogly returns true because thd->is_error() returns false SQL_SELECT::skip_record() invoked from record_should_be_deleted(). It's supposed that THD error should be set in rr_handle_error() called from rr_sequential() during sub-select JOIN::exec_inner() execution. But rr_handle_error() does not set THD error because READ_RECORD::print_error is not set in JOIN_TAB::read_record. READ_RECORD::print_error should be initialized in init_read_record()/init_read_record_idx(). But make_join_readinfo() does not invoke init_read_record()/init_read_record_idx() for JOIN_TAB::read_record. The fix is to set JOIN_TAB::read_record.print_error in make_join_readinfo(), i.e. in the same place where JOIN_TAB::read_record.table is set. Reviewed by Sergey Petrunya. --- .../innodb/r/deadlock_in_subqueries_join.result | 50 +++++++++++++ .../innodb/t/deadlock_in_subqueries_join.test | 81 ++++++++++++++++++++++ sql/sql_select.cc | 1 + 3 files changed, 132 insertions(+) create mode 100644 mysql-test/suite/innodb/r/deadlock_in_subqueries_join.result create mode 100644 mysql-test/suite/innodb/t/deadlock_in_subqueries_join.test diff --git a/mysql-test/suite/innodb/r/deadlock_in_subqueries_join.result b/mysql-test/suite/innodb/r/deadlock_in_subqueries_join.result new file mode 100644 index 00000000000..2e82b0662f8 --- /dev/null +++ b/mysql-test/suite/innodb/r/deadlock_in_subqueries_join.result @@ -0,0 +1,50 @@ +CREATE TABLE t1 ( +pkey int NOT NULL PRIMARY KEY, +c int +) ENGINE=InnoDB; +INSERT INTO t1 VALUES(1,1); +CREATE TABLE t2 ( +pkey int NOT NULL PRIMARY KEY, +c int +) ENGINE=InnoDB DEFAULT CHARSET=latin1; +INSERT INTO t2 VALUES (2, NULL); +CREATE TABLE t3 (c int) engine = InnoDB; +INSERT INTO t3 VALUES (10), (20), (30), (40), (50); +connect con1, localhost,root,,; +connection default; +START TRANSACTION; +UPDATE t3 SET c=c+1000; +SELECT * FROM t1 FOR UPDATE; +pkey c +1 1 +connection con1; +START TRANSACTION; +DELETE FROM t2 WHERE c NOT IN (SELECT ref_0.pkey FROM t1 AS ref_0 INNER JOIN t1 AS ref_1 ON ref_0.c = ref_0.pkey); +connection default; +SELECT * FROM t2 FOR UPDATE; +pkey c +2 NULL +COMMIT; +connection con1; +ERROR 40001: Deadlock found when trying to get lock; try restarting transaction +COMMIT; +connection default; +START TRANSACTION; +UPDATE t3 SET c=c+1000; +SELECT * FROM t1 FOR UPDATE; +pkey c +1 1 +connection con1; +START TRANSACTION; +UPDATE t2 SET pkey=pkey+10 WHERE c NOT IN (SELECT ref_0.pkey FROM t1 AS ref_0 INNER JOIN t1 AS ref_1 ON ref_0.c = ref_0.pkey); +connection default; +SELECT * FROM t2 FOR UPDATE; +pkey c +2 NULL +COMMIT; +connection con1; +ERROR 40001: Deadlock found when trying to get lock; try restarting transaction +COMMIT; +disconnect con1; +connection default; +DROP TABLE t1,t2,t3; diff --git a/mysql-test/suite/innodb/t/deadlock_in_subqueries_join.test b/mysql-test/suite/innodb/t/deadlock_in_subqueries_join.test new file mode 100644 index 00000000000..b3adfb3b02d --- /dev/null +++ b/mysql-test/suite/innodb/t/deadlock_in_subqueries_join.test @@ -0,0 +1,81 @@ +--source include/have_innodb.inc +--source include/count_sessions.inc + +CREATE TABLE t1 ( + pkey int NOT NULL PRIMARY KEY, + c int +) ENGINE=InnoDB; + +INSERT INTO t1 VALUES(1,1); + +CREATE TABLE t2 ( + pkey int NOT NULL PRIMARY KEY, + c int +) ENGINE=InnoDB DEFAULT CHARSET=latin1; + +INSERT INTO t2 VALUES (2, NULL); + +# The following table is to increase tansaction weight on deadlock resolution +CREATE TABLE t3 (c int) engine = InnoDB; +INSERT INTO t3 VALUES (10), (20), (30), (40), (50); + +--let $i= 2 +--let $delete= 2 +--let $update= 1 +--connect(con1, localhost,root,,) + +while($i) { +--connection default +START TRANSACTION; # trx 1 +# The following update is necessary to increase the transaction weight, which is +# calculated as the number of locks + the number of undo records during deadlock +# report. Victim's transaction should have minimum weight. We need trx 2 to be +# choosen as victim, that's why we need to increase the current transaction +# weight. +UPDATE t3 SET c=c+1000; +SELECT * FROM t1 FOR UPDATE; + +--connection con1 +START TRANSACTION; # trx 2 +# 1) read record from t2, lock it +# 2) check if the read record should be deleted, i.e. read record from t1, +# as the record from t1 is locked by trx 1, the subselect will be suspended. +# see 'while' loop in mysql_delete() or mysql_update() and +# select->skip_record(thd) call for details. +if ($i == $delete) { +--send DELETE FROM t2 WHERE c NOT IN (SELECT ref_0.pkey FROM t1 AS ref_0 INNER JOIN t1 AS ref_1 ON ref_0.c = ref_0.pkey) +} +if ($i == $update) { +--send UPDATE t2 SET pkey=pkey+10 WHERE c NOT IN (SELECT ref_0.pkey FROM t1 AS ref_0 INNER JOIN t1 AS ref_1 ON ref_0.c = ref_0.pkey) +} + +--connection default +let $wait_condition= + SELECT count(*) = 1 FROM information_schema.processlist + WHERE (state = 'Sending data' OR state = "Updating") + AND (info LIKE 'delete from t2 where%' OR + info LIKE 'UPDATE t2 SET pkey=pkey+10 WHERE%'); +--source include/wait_condition.inc + +# The record from t2 is locked by the previous delete, so trx 2 is waiting for +# trx 1, and trx 1 will be blocked by trx 2 with the following SELECT. So we +# have deadlock here. And trx 2 is chosen as deadlock victim as trx 1 has +# greater weight. +SELECT * FROM t2 FOR UPDATE; +COMMIT; + +--connection con1 +# If the bug is not fixed, there will be assertion failure as +# mysql_delete()/mysql_update() will continue execution despite its subselect +# got deadlock error +--error ER_LOCK_DEADLOCK +--reap +COMMIT; +--dec $i +} + +--disconnect con1 + +--connection default +DROP TABLE t1,t2,t3; +--source include/wait_until_count_sessions.inc diff --git a/sql/sql_select.cc b/sql/sql_select.cc index 0b330528452..a85dc71e9e4 100644 --- a/sql/sql_select.cc +++ b/sql/sql_select.cc @@ -12509,6 +12509,7 @@ make_join_readinfo(JOIN *join, ulonglong options, uint no_jbuf_after) uint jcl= tab->used_join_cache_level; tab->read_record.table= table; tab->read_record.unlock_row= rr_unlock_row; + tab->read_record.print_error= true; tab->sorted= sorted; sorted= 0; // only first must be sorted -- cgit v1.2.1