commit e508dbe1219eb203c9740ad54371ee8fd76bb086 Author: Shaohua Wang Date: Wed Dec 1 16:30:10 2021 +0800 Support SELECT FOR UPDATE WAIT N Currently we support SELECT FOR UPDATE NOWAIT/SKIP LOCKED in InnoDB. In this task, we support SELECT FOR UPDATE WAIT N(timeout in seconds). The wait timeout means that we wait for N seconds for a row lock before return lock wait timeout error. The wait timeout is for a single row. If a SELECT FOR UPDATE has to lock multiple rows, we reset wait timeout to N before lock wait. Note: we use minimum value of (N, innodb_lock_wait_timeout) as final wait timeout. the default value of innodb_lock_wait_timeout is 50 seconds. diff --git a/include/thr_lock.h b/include/thr_lock.h index 2637b7fbf50..a1564865b5f 100644 --- a/include/thr_lock.h +++ b/include/thr_lock.h @@ -98,6 +98,7 @@ enum thr_locked_row_action { THR_DEFAULT, THR_WAIT, THR_NOWAIT, THR_SKIP }; struct Lock_descriptor { thr_lock_type type{TL_UNLOCK}; thr_locked_row_action action{THR_DEFAULT}; + ulong wait_timeout{0}; }; enum enum_thr_lock_result { diff --git a/mysql-test/r/locking_clause.result b/mysql-test/r/locking_clause.result index 37d5036b5c6..e7deab553a7 100644 --- a/mysql-test/r/locking_clause.result +++ b/mysql-test/r/locking_clause.result @@ -28,6 +28,9 @@ DROP PROCEDURE p3; SELECT * FROM t1 FOR UPDATE; a b 1 1 +SELECT * FROM t1 FOR UPDATE WAIT 1; +a b +1 1 SELECT * FROM t1 FOR UPDATE NOWAIT; a b 1 1 @@ -40,6 +43,9 @@ a b SELECT * FROM t1 FOR SHARE; a b 1 1 +SELECT * FROM t1 FOR SHARE WAIT 1; +a b +1 1 SELECT * FROM t1 FOR SHARE NOWAIT; a b 1 1 @@ -52,6 +58,9 @@ a b SELECT * FROM t1 FOR SHARE OF t1; a b 1 1 +SELECT * FROM t1 FOR SHARE WAIT 1; +a b +1 1 SELECT * FROM t1 FOR SHARE OF t1 NOWAIT; a b 1 1 @@ -61,6 +70,9 @@ a b SELECT * FROM t1 FOR UPDATE OF t1; a b 1 1 +SELECT * FROM t1 FOR UPDATE OF t1 WAIT 1; +a b +1 1 SELECT * FROM t1 FOR UPDATE OF t1 NOWAIT; a b 1 1 @@ -73,6 +85,9 @@ a b SELECT * FROM t1, t2 FOR SHARE OF t1, t2; a b a 1 1 1 +SELECT * FROM t1, t2 FOR SHARE OF t1, t2 WAIT 1; +a b a +1 1 1 SELECT * FROM t1, t2 FOR SHARE OF t1, t2 NOWAIT; a b a 1 1 1 @@ -82,6 +97,9 @@ a b a SELECT * FROM t1, t2 FOR UPDATE OF t1, t2; a b a 1 1 1 +SELECT * FROM t1, t2 FOR UPDATE OF t1, t2 WAIT 1; +a b a +1 1 1 SELECT * FROM t1, t2 FOR UPDATE OF t1, t2 NOWAIT; a b a 1 1 1 @@ -127,12 +145,18 @@ a b a SELECT * FROM t1, t2 FOR SHARE OF t1 FOR SHARE OF t2; a b a 1 1 1 +SELECT * FROM t1 FOR SHARE OF t1 WAIT 1; +a b +1 1 SELECT * FROM t1 FOR SHARE OF t1 NOWAIT; a b 1 1 SELECT * FROM t1 FOR SHARE OF t1 SKIP LOCKED; a b 1 1 +SELECT * FROM t1, t2 FOR SHARE OF t1 WAIT 1 FOR SHARE OF t2 WAIT 1; +a b a +1 1 1 SELECT * FROM t1, t2 FOR SHARE OF t1 NOWAIT FOR SHARE OF t2 NOWAIT; a b a 1 1 1 @@ -149,11 +173,11 @@ SELECT 1 FROM v1 FOR UPDATE; # Test of syntax errors. # SELECT * FROM t1 FOR SHARE WAIT WAIT; -ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'WAIT WAIT' at line 1 +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'WAIT' at line 1 SELECT * FROM t1 FOR SHARE WAIT NOWAIT; -ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'WAIT NOWAIT' at line 1 +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'NOWAIT' at line 1 SELECT * FROM t1 FOR SHARE WAIT SKIP LOCKED; -ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'WAIT SKIP LOCKED' at line 1 +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'SKIP LOCKED' at line 1 SELECT 1 FOR UPDATE UNION SELECT 2; ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'UNION SELECT 2' at line 1 SELECT 1 LOCK IN SHARE MODE UNION SELECT 2; @@ -265,6 +289,15 @@ SELECT * FROM t1, t2 FOR UPDATE OF t1 FOR SHARE; ERROR HY000: Table t1 appears in multiple locking clauses. SELECT * FROM t1, t2 FOR UPDATE OF t2 FOR SHARE; ERROR HY000: Table t2 appears in multiple locking clauses. +SELECT MIN(a) FROM t1 FOR UPDATE WAIT 1; +MIN(a) +1 +SELECT MAX(a) FROM t1 FOR UPDATE WAIT 1; +MAX(a) +10 +SELECT SUM(a) FROM t1 FOR UPDATE WAIT 1; +SUM(a) +11 SELECT MIN(a) FROM t1 FOR UPDATE SKIP LOCKED; MIN(a) 1 @@ -283,6 +316,10 @@ MAX(a) SELECT SUM(a) FROM t1 FOR UPDATE NOWAIT; SUM(a) 11 +SELECT DISTINCT * FROM t1 FOR UPDATE WAIT 1; +a b +1 1 +10 10 SELECT DISTINCT * FROM t1 FOR UPDATE SKIP LOCKED; a b 1 1 diff --git a/mysql-test/suite/innodb/r/select_for_update_wait.result b/mysql-test/suite/innodb/r/select_for_update_wait.result new file mode 100644 index 00000000000..8b4dd6a18fe --- /dev/null +++ b/mysql-test/suite/innodb/r/select_for_update_wait.result @@ -0,0 +1,189 @@ +# Case 1: Test primary index +CREATE TABLE t1( +seat_id INT, +PRIMARY KEY(seat_id) +) ENGINE=InnoDB; +INSERT INTO t1 VALUES(1), (2), (3), (4); +BEGIN; +SELECT * FROM t1 FOR UPDATE; +seat_id +1 +2 +3 +4 +BEGIN; +SELECT * FROM t1 FOR UPDATE WAIT 3; +ERROR HY000: Lock wait timeout exceeded; try restarting transaction +SELECT * FROM t1 FOR UPDATE WAIT 3;; +COMMIT; +seat_id +1 +2 +3 +4 +COMMIT; +BEGIN; +SELECT * FROM t1 FOR UPDATE; +seat_id +1 +2 +3 +4 +BEGIN; +SELECT * FROM t1 FOR UPDATE WAIT 3;; +COMMIT; +ERROR HY000: Lock wait timeout exceeded; try restarting transaction +COMMIT; +DROP TABLE t1; +# Case 2: Test primary index & secondary index +CREATE TABLE t1( +seat_id INT, +row_id INT, +PRIMARY KEY(seat_id), +KEY(row_id) +) ENGINE=InnoDB; +INSERT INTO t1 VALUES(1,1), (2,1), (3,2), (4,2); +BEGIN; +SELECT * FROM t1 WHERE row_id = 1 FOR UPDATE; +seat_id row_id +1 1 +2 1 +BEGIN; +SELECT * FROM t1 WHERE row_id = 1 LIMIT 1 FOR UPDATE WAIT 3; +ERROR HY000: Lock wait timeout exceeded; try restarting transaction +SELECT * FROM t1 WHERE row_id = 1 LIMIT 1 FOR UPDATE WAIT 3;; +COMMIT; +seat_id row_id +1 1 +COMMIT; +DROP TABLE t1; +# Case 3: Test primary index & spatial index +CREATE TABLE t1( +seat_id INT, +pos POINT NOT NULL, +PRIMARY KEY(seat_id), +SPATIAL KEY(pos) +) ENGINE=InnoDB; +Warnings: +Warning 3674 The spatial index on column 'pos' will not be used by the query optimizer since the column does not have an SRID attribute. Consider adding an SRID attribute to the column. +INSERT INTO t1 VALUES +(1,ST_PointFromText('POINT(1 0)')), +(2,ST_PointFromText('POINT(1 1)')), +(3,ST_PointFromText('POINT(2 0)')), +(4,ST_PointFromText('POINT(2 1)')), +(5,ST_PointFromText('POINT(3 0)')), +(6,ST_PointFromText('POINT(3 1)')); +BEGIN; +SET @g = ST_GeomFromText('POLYGON((0 0,0 2,2 2,0 2,0 0))'); +SELECT seat_id, ST_AsText(pos) FROM t1 FORCE INDEX (pos) +WHERE MBRWithin(pos, @g) FOR UPDATE; +seat_id ST_AsText(pos) +2 POINT(1 1) +BEGIN; +SET @g = ST_GeomFromText('POLYGON((0 0,0 2,2 2,0 2,0 0))'); +SELECT seat_id, ST_AsText(pos) FROM t1 FORCE INDEX (pos) +WHERE MBRWithin(pos, @g) FOR UPDATE WAIT 3; +ERROR HY000: Lock wait timeout exceeded; try restarting transaction +SELECT seat_id, ST_AsText(pos) FROM t1 FORCE INDEX (pos) WHERE MBRWithin(pos, @g) FOR UPDATE WAIT 3;; +COMMIT; +seat_id ST_AsText(pos) +2 POINT(1 1) +COMMIT; +DROP TABLE t1; +# Case 4: Test wait multiple rows +CREATE TABLE t1( +seat_id INT, +PRIMARY KEY(seat_id) +) ENGINE=InnoDB; +INSERT INTO t1 VALUES(1), (2), (3), (4); +BEGIN; +SELECT * FROM t1 WHERE seat_id = 1 FOR UPDATE; +seat_id +1 +BEGIN; +SELECT * FROM t1 WHERE seat_id = 2 FOR UPDATE; +seat_id +2 +BEGIN; +SELECT * FROM t1 FOR UPDATE WAIT 3;; +COMMIT; +COMMIT; +seat_id +1 +2 +3 +4 +COMMIT; +DROP TABLE t1; +# Case 5: Test default behaviour of WAIT +CREATE TABLE t1( +seat_id INT, +PRIMARY KEY(seat_id) +) ENGINE=InnoDB; +INSERT INTO t1 VALUES(1), (2), (3), (4); +BEGIN; +SELECT * FROM t1 FOR UPDATE; +seat_id +1 +2 +3 +4 +SET SESSION innodb_lock_wait_timeout=1; +BEGIN; +SELECT * FROM t1 FOR UPDATE;; +COMMIT; +ERROR HY000: Lock wait timeout exceeded; try restarting transaction +COMMIT; +SET SESSION innodb_lock_wait_timeout=default; +DROP TABLE t1; +# Case 6: Test innodb_lock_wait_timeout +CREATE TABLE t1( +seat_id INT, +PRIMARY KEY(seat_id) +) ENGINE=InnoDB; +INSERT INTO t1 VALUES(1), (2), (3), (4); +BEGIN; +SELECT * FROM t1 FOR UPDATE; +seat_id +1 +2 +3 +4 +SET SESSION innodb_lock_wait_timeout=1; +BEGIN; +SELECT * FROM t1 FOR UPDATE WAIT 3; +ERROR HY000: Lock wait timeout exceeded; try restarting transaction +SELECT * FROM t1 FOR UPDATE WAIT 3;; +COMMIT; +ERROR HY000: Lock wait timeout exceeded; try restarting transaction +COMMIT; +SET SESSION innodb_lock_wait_timeout=default; +DROP TABLE t1; +# Case 7: Test WAIT timeout values +CREATE TABLE t1( +seat_id INT, +PRIMARY KEY(seat_id) +) ENGINE=InnoDB; +INSERT INTO t1 VALUES(1); +SELECT * FROM t1 FOR SHARE WAIT AA; +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'AA' at line 1 +SELECT * FROM t1 FOR SHARE WAIT 'AA'; +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''AA'' at line 1 +SELECT * FROM t1 FOR SHARE WAIT 1.9; +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '1.9' at line 1 +SELECT * FROM t1 FOR UPDATE WAIT -2147483648; +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '-2147483648' at line 1 +SELECT * FROM t1 FOR UPDATE WAIT -1; +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '-1' at line 1 +SELECT * FROM t1 FOR UPDATE WAIT 0; +seat_id +1 +SELECT * FROM t1 FOR UPDATE WAIT 1; +seat_id +1 +SELECT * FROM t1 FOR UPDATE WAIT 2147483647; +seat_id +1 +SELECT * FROM t1 FOR UPDATE WAIT 2147483648; +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '2147483648' at line 1 +DROP TABLE t1; diff --git a/mysql-test/suite/innodb/t/select_for_update_wait.test b/mysql-test/suite/innodb/t/select_for_update_wait.test new file mode 100644 index 00000000000..bd29f76d3db --- /dev/null +++ b/mysql-test/suite/innodb/t/select_for_update_wait.test @@ -0,0 +1,271 @@ +# +# SELECT FOR UPDATE WAIT N +# + +--source include/count_sessions.inc + +connect (con1,localhost,root,,); + +connection default; + +--echo # Case 1: Test primary index +CREATE TABLE t1( + seat_id INT, + PRIMARY KEY(seat_id) +) ENGINE=InnoDB; + +INSERT INTO t1 VALUES(1), (2), (3), (4); + +BEGIN; +SELECT * FROM t1 FOR UPDATE; + +connection con1; +BEGIN; + +--error ER_LOCK_WAIT_TIMEOUT +SELECT * FROM t1 FOR UPDATE WAIT 3; +# wait successfully +--send SELECT * FROM t1 FOR UPDATE WAIT 3; + +connection default; +--sleep 2 +COMMIT; + +connection con1; +--reap +COMMIT; + +connection default; +BEGIN; +SELECT * FROM t1 FOR UPDATE; + +connection con1; +BEGIN; +# wait timeout +--send SELECT * FROM t1 FOR UPDATE WAIT 3; + +connection default; +--sleep 4 +COMMIT; + +connection con1; +--error ER_LOCK_WAIT_TIMEOUT +--reap +COMMIT; + +connection default; +DROP TABLE t1; + +--echo # Case 2: Test primary index & secondary index +CREATE TABLE t1( + seat_id INT, + row_id INT, + PRIMARY KEY(seat_id), + KEY(row_id) +) ENGINE=InnoDB; + +INSERT INTO t1 VALUES(1,1), (2,1), (3,2), (4,2); + +BEGIN; +SELECT * FROM t1 WHERE row_id = 1 FOR UPDATE; + +connection con1; +BEGIN; +--error ER_LOCK_WAIT_TIMEOUT +SELECT * FROM t1 WHERE row_id = 1 LIMIT 1 FOR UPDATE WAIT 3; + +--send SELECT * FROM t1 WHERE row_id = 1 LIMIT 1 FOR UPDATE WAIT 3; + +connection default; +--sleep 2 +COMMIT; + +connection con1; +--reap +COMMIT; + +connection default; +DROP TABLE t1; + +--echo # Case 3: Test primary index & spatial index +CREATE TABLE t1( + seat_id INT, + pos POINT NOT NULL, + PRIMARY KEY(seat_id), + SPATIAL KEY(pos) +) ENGINE=InnoDB; + +INSERT INTO t1 VALUES +(1,ST_PointFromText('POINT(1 0)')), +(2,ST_PointFromText('POINT(1 1)')), +(3,ST_PointFromText('POINT(2 0)')), +(4,ST_PointFromText('POINT(2 1)')), +(5,ST_PointFromText('POINT(3 0)')), +(6,ST_PointFromText('POINT(3 1)')); + +BEGIN; +SET @g = ST_GeomFromText('POLYGON((0 0,0 2,2 2,0 2,0 0))'); +SELECT seat_id, ST_AsText(pos) FROM t1 FORCE INDEX (pos) +WHERE MBRWithin(pos, @g) FOR UPDATE; + +connection con1; +BEGIN; +SET @g = ST_GeomFromText('POLYGON((0 0,0 2,2 2,0 2,0 0))'); +--error ER_LOCK_WAIT_TIMEOUT +SELECT seat_id, ST_AsText(pos) FROM t1 FORCE INDEX (pos) +WHERE MBRWithin(pos, @g) FOR UPDATE WAIT 3; + +--send SELECT seat_id, ST_AsText(pos) FROM t1 FORCE INDEX (pos) WHERE MBRWithin(pos, @g) FOR UPDATE WAIT 3; + +connection default; +--sleep 2 +COMMIT; + +connection con1; +--reap +COMMIT; + +connection default; +DROP TABLE t1; + +--echo # Case 4: Test wait multiple rows +# wait_n is timeout value for each single row. +CREATE TABLE t1( + seat_id INT, + PRIMARY KEY(seat_id) +) ENGINE=InnoDB; + +INSERT INTO t1 VALUES(1), (2), (3), (4); + +connection con1; +BEGIN; +SELECT * FROM t1 WHERE seat_id = 1 FOR UPDATE; + +connect (con2,localhost,root,,); +connection con2; +BEGIN; +SELECT * FROM t1 WHERE seat_id = 2 FOR UPDATE; + +connection default; +BEGIN; +--send SELECT * FROM t1 FOR UPDATE WAIT 3; + +connection con1; +--sleep 2 +COMMIT; + +connection con2; +--sleep 2 +COMMIT; + +connection default; +# wait successfully! +--reap +COMMIT; + +disconnect con2; + +DROP TABLE t1; + +--echo # Case 5: Test default behaviour of WAIT +CREATE TABLE t1( + seat_id INT, + PRIMARY KEY(seat_id) +) ENGINE=InnoDB; + +INSERT INTO t1 VALUES(1), (2), (3), (4); + +BEGIN; +SELECT * FROM t1 FOR UPDATE; + +connection con1; +SET SESSION innodb_lock_wait_timeout=1; +BEGIN; +--send SELECT * FROM t1 FOR UPDATE; + +connection default; +--sleep 2 +COMMIT; + +connection con1; +--error ER_LOCK_WAIT_TIMEOUT +--reap +COMMIT; + +SET SESSION innodb_lock_wait_timeout=default; + +connection default; +DROP TABLE t1; + +--echo # Case 6: Test innodb_lock_wait_timeout +# we choose min(innodb_lock_wait_timeout, wait_n) as timeout value. +CREATE TABLE t1( + seat_id INT, + PRIMARY KEY(seat_id) +) ENGINE=InnoDB; + +INSERT INTO t1 VALUES(1), (2), (3), (4); + +BEGIN; +SELECT * FROM t1 FOR UPDATE; + +connection con1; +SET SESSION innodb_lock_wait_timeout=1; +BEGIN; + +--error ER_LOCK_WAIT_TIMEOUT +SELECT * FROM t1 FOR UPDATE WAIT 3; + +--send SELECT * FROM t1 FOR UPDATE WAIT 3; + +connection default; +--sleep 2 +COMMIT; + +connection con1; +--error ER_LOCK_WAIT_TIMEOUT +--reap +COMMIT; + +SET SESSION innodb_lock_wait_timeout=default; + +connection default; +DROP TABLE t1; + +--echo # Case 7: Test WAIT timeout values +CREATE TABLE t1( + seat_id INT, + PRIMARY KEY(seat_id) +) ENGINE=InnoDB; + +INSERT INTO t1 VALUES(1); + +--error ER_PARSE_ERROR +SELECT * FROM t1 FOR SHARE WAIT AA; + +--error ER_PARSE_ERROR +SELECT * FROM t1 FOR SHARE WAIT 'AA'; + +--error ER_PARSE_ERROR +SELECT * FROM t1 FOR SHARE WAIT 1.9; + +--error ER_PARSE_ERROR +SELECT * FROM t1 FOR UPDATE WAIT -2147483648; + +--error ER_PARSE_ERROR +SELECT * FROM t1 FOR UPDATE WAIT -1; + +SELECT * FROM t1 FOR UPDATE WAIT 0; + +SELECT * FROM t1 FOR UPDATE WAIT 1; + +SELECT * FROM t1 FOR UPDATE WAIT 2147483647; + +--error ER_PARSE_ERROR +SELECT * FROM t1 FOR UPDATE WAIT 2147483648; + +DROP TABLE t1; + +disconnect con1; + +--source include/wait_until_count_sessions.inc diff --git a/mysql-test/suite/rpl/r/rpl_unsafe_skip_locked_nowait.result b/mysql-test/suite/rpl/r/rpl_unsafe_skip_locked_nowait.result index 30d29b06b77..74d9f589cce 100644 --- a/mysql-test/suite/rpl/r/rpl_unsafe_skip_locked_nowait.result +++ b/mysql-test/suite/rpl/r/rpl_unsafe_skip_locked_nowait.result @@ -19,6 +19,8 @@ a b 2 1 # Step-1.3: Populate another table with the unlocked rows. connect conn, localhost, root,; +INSERT INTO t2 SELECT * FROM t1 FOR UPDATE WAIT 1; +ERROR HY000: Lock wait timeout exceeded; try restarting transaction INSERT INTO t2 SELECT * FROM t1 FOR UPDATE NOWAIT; ERROR HY000: Statement aborted because lock(s) could not be acquired immediately and NOWAIT is set. INSERT INTO t2 SELECT * FROM t1 FOR UPDATE SKIP LOCKED; diff --git a/mysql-test/suite/rpl/t/rpl_unsafe_skip_locked_nowait.test b/mysql-test/suite/rpl/t/rpl_unsafe_skip_locked_nowait.test index 75e220aa965..e8ae461aabc 100644 --- a/mysql-test/suite/rpl/t/rpl_unsafe_skip_locked_nowait.test +++ b/mysql-test/suite/rpl/t/rpl_unsafe_skip_locked_nowait.test @@ -20,6 +20,8 @@ SELECT * FROM t1 WHERE a = 2 FOR UPDATE; --echo # Step-1.3: Populate another table with the unlocked rows. --connect (conn, localhost, root,) +--error ER_LOCK_WAIT_TIMEOUT +INSERT INTO t2 SELECT * FROM t1 FOR UPDATE WAIT 1; --error ER_LOCK_NOWAIT INSERT INTO t2 SELECT * FROM t1 FOR UPDATE NOWAIT; INSERT INTO t2 SELECT * FROM t1 FOR UPDATE SKIP LOCKED; diff --git a/mysql-test/t/locking_clause.test b/mysql-test/t/locking_clause.test index 5f82b06a3be..8b0137c3db8 100644 --- a/mysql-test/t/locking_clause.test +++ b/mysql-test/t/locking_clause.test @@ -40,6 +40,7 @@ DROP PROCEDURE p3; --echo # UPDATE ... --echo # SELECT * FROM t1 FOR UPDATE; +SELECT * FROM t1 FOR UPDATE WAIT 1; SELECT * FROM t1 FOR UPDATE NOWAIT; SELECT * FROM t1 FOR UPDATE SKIP LOCKED; @@ -47,6 +48,7 @@ SELECT * FROM t1 FOR UPDATE SKIP LOCKED; --echo # SHARE ... --echo # SELECT * FROM t1 FOR SHARE; +SELECT * FROM t1 FOR SHARE WAIT 1; SELECT * FROM t1 FOR SHARE NOWAIT; SELECT * FROM t1 FOR SHARE SKIP LOCKED; @@ -54,10 +56,12 @@ SELECT * FROM t1 FOR SHARE SKIP LOCKED; --echo # OF ... --echo # SELECT * FROM t1 FOR SHARE OF t1; +SELECT * FROM t1 FOR SHARE WAIT 1; SELECT * FROM t1 FOR SHARE OF t1 NOWAIT; SELECT * FROM t1 FOR SHARE OF t1 SKIP LOCKED; SELECT * FROM t1 FOR UPDATE OF t1; +SELECT * FROM t1 FOR UPDATE OF t1 WAIT 1; SELECT * FROM t1 FOR UPDATE OF t1 NOWAIT; SELECT * FROM t1 FOR UPDATE OF t1 SKIP LOCKED; @@ -65,10 +69,12 @@ SELECT * FROM t1 FOR UPDATE OF t1 SKIP LOCKED; --echo # OF ... --echo # SELECT * FROM t1, t2 FOR SHARE OF t1, t2; +SELECT * FROM t1, t2 FOR SHARE OF t1, t2 WAIT 1; SELECT * FROM t1, t2 FOR SHARE OF t1, t2 NOWAIT; SELECT * FROM t1, t2 FOR SHARE OF t1, t2 SKIP LOCKED; SELECT * FROM t1, t2 FOR UPDATE OF t1, t2; +SELECT * FROM t1, t2 FOR UPDATE OF t1, t2 WAIT 1; SELECT * FROM t1, t2 FOR UPDATE OF t1, t2 NOWAIT; SELECT * FROM t1, t2 FOR UPDATE OF t1, t2 SKIP LOCKED; @@ -87,8 +93,10 @@ SELECT * FROM t1 JOIN t2 FOR SHARE OF t1 FOR UPDATE OF t2; SELECT * FROM t1 NATURAL JOIN t2 FOR SHARE OF t1 FOR UPDATE OF t2; SELECT * FROM t1, t2 FOR SHARE OF t1 FOR UPDATE OF t2; SELECT * FROM t1, t2 FOR SHARE OF t1 FOR SHARE OF t2; +SELECT * FROM t1 FOR SHARE OF t1 WAIT 1; SELECT * FROM t1 FOR SHARE OF t1 NOWAIT; SELECT * FROM t1 FOR SHARE OF t1 SKIP LOCKED; +SELECT * FROM t1, t2 FOR SHARE OF t1 WAIT 1 FOR SHARE OF t2 WAIT 1; SELECT * FROM t1, t2 FOR SHARE OF t1 NOWAIT FOR SHARE OF t2 NOWAIT; --echo # @@ -213,6 +221,10 @@ SELECT * FROM t1, t2 FOR UPDATE OF t1 FOR SHARE; --error ER_DUPLICATE_TABLE_LOCK SELECT * FROM t1, t2 FOR UPDATE OF t2 FOR SHARE; +SELECT MIN(a) FROM t1 FOR UPDATE WAIT 1; +SELECT MAX(a) FROM t1 FOR UPDATE WAIT 1; +SELECT SUM(a) FROM t1 FOR UPDATE WAIT 1; + SELECT MIN(a) FROM t1 FOR UPDATE SKIP LOCKED; SELECT MAX(a) FROM t1 FOR UPDATE SKIP LOCKED; SELECT SUM(a) FROM t1 FOR UPDATE SKIP LOCKED; @@ -221,6 +233,7 @@ SELECT MIN(a) FROM t1 FOR UPDATE NOWAIT; SELECT MAX(a) FROM t1 FOR UPDATE NOWAIT; SELECT SUM(a) FROM t1 FOR UPDATE NOWAIT; +SELECT DISTINCT * FROM t1 FOR UPDATE WAIT 1; SELECT DISTINCT * FROM t1 FOR UPDATE SKIP LOCKED; SELECT DISTINCT * FROM t1 FOR UPDATE NOWAIT; diff --git a/sql/parse_tree_nodes.cc b/sql/parse_tree_nodes.cc index a44079711e1..59959fc3a12 100644 --- a/sql/parse_tree_nodes.cc +++ b/sql/parse_tree_nodes.cc @@ -1746,12 +1746,16 @@ bool PT_locking_clause::contextualize(Parse_context *pc) { if (lex->is_explain()) return false; - if (m_locked_row_action == Locked_row_action::SKIP) + if (m_locked_row_option.action == Locked_row_action::SKIP) lex->set_stmt_unsafe(LEX::BINLOG_STMT_UNSAFE_SKIP_LOCKED); - if (m_locked_row_action == Locked_row_action::NOWAIT) + if (m_locked_row_option.action == Locked_row_action::NOWAIT) lex->set_stmt_unsafe(LEX::BINLOG_STMT_UNSAFE_NOWAIT); + if (m_locked_row_option.action == Locked_row_action::WAIT && + m_locked_row_option.wait_timeout > 0) + lex->set_stmt_unsafe(LEX::BINLOG_STMT_UNSAFE_WAIT); + lex->safe_to_cache_query = false; return set_lock_for_tables(pc); diff --git a/sql/parse_tree_nodes.h b/sql/parse_tree_nodes.h index 09a05a76c16..ba8ece737dd 100644 --- a/sql/parse_tree_nodes.h +++ b/sql/parse_tree_nodes.h @@ -643,14 +643,14 @@ class PT_order : public Parse_tree_node { class PT_locking_clause : public Parse_tree_node { public: - PT_locking_clause(Lock_strength strength, Locked_row_action action) - : m_lock_strength(strength), m_locked_row_action(action) {} + PT_locking_clause(Lock_strength strength, Locked_row_option option) + : m_lock_strength(strength), m_locked_row_option(option) {} bool contextualize(Parse_context *pc) final; virtual bool set_lock_for_tables(Parse_context *pc) = 0; - Locked_row_action action() const { return m_locked_row_action; } + Locked_row_option option() const { return m_locked_row_option; } protected: Lock_descriptor get_lock_descriptor() const { @@ -664,20 +664,21 @@ class PT_locking_clause : public Parse_tree_node { break; } - return {lock_type, static_cast(action())}; + return {lock_type, static_cast(option().action), + option().wait_timeout}; } private: Lock_strength m_lock_strength; - Locked_row_action m_locked_row_action; + Locked_row_option m_locked_row_option; }; class PT_query_block_locking_clause : public PT_locking_clause { public: explicit PT_query_block_locking_clause( Lock_strength strength, - Locked_row_action action = Locked_row_action::WAIT) - : PT_locking_clause(strength, action) {} + Locked_row_option option = {Locked_row_action::WAIT, 0}) + : PT_locking_clause(strength, option) {} bool set_lock_for_tables(Parse_context *pc) override; }; @@ -688,8 +689,8 @@ class PT_table_locking_clause : public PT_locking_clause { PT_table_locking_clause(Lock_strength strength, Mem_root_array_YY tables, - Locked_row_action action) - : PT_locking_clause(strength, action), m_tables(tables) {} + Locked_row_option option) + : PT_locking_clause(strength, option), m_tables(tables) {} bool set_lock_for_tables(Parse_context *pc) override; diff --git a/sql/parser_yystype.h b/sql/parser_yystype.h index fca4a183b7b..d8fc945a7bf 100644 --- a/sql/parser_yystype.h +++ b/sql/parser_yystype.h @@ -216,6 +216,15 @@ enum class Locked_row_action { SKIP = THR_SKIP }; +/** + Locked row option for SELECT FOR UPDATE/SHARE + NOWAIT/SKIPLOCKED/WAIT N. +*/ +struct Locked_row_option { + enum Locked_row_action action; + ulong wait_timeout; +}; + /** Internally there is no CROSS JOIN join type, as cross joins are just a special case of inner joins with a join condition that is always true. The @@ -586,7 +595,7 @@ union YYSTYPE { PT_query_primary *opt_query_expression; } create_table_tail; Lock_strength lock_strength; - Locked_row_action locked_row_action; + Locked_row_option locked_row_option; PT_locking_clause *locking_clause; PT_locking_clause_list *locking_clause_list; Mem_root_array *jtc_list; diff --git a/sql/sql_lex.h b/sql/sql_lex.h index 0b76927120a..2dc15fc26ae 100644 --- a/sql/sql_lex.h +++ b/sql/sql_lex.h @@ -2711,6 +2711,7 @@ class Query_tables_list { BINLOG_STMT_UNSAFE_FULLTEXT_PLUGIN, BINLOG_STMT_UNSAFE_SKIP_LOCKED, BINLOG_STMT_UNSAFE_NOWAIT, + BINLOG_STMT_UNSAFE_WAIT, /** XA transactions and statements. diff --git a/sql/sql_yacc.yy b/sql/sql_yacc.yy index 583ae49c26f..f91bcea9f45 100644 --- a/sql/sql_yacc.yy +++ b/sql/sql_yacc.yy @@ -1510,7 +1510,7 @@ void warn_about_deprecated_binary(THD *thd) %type replace_lock_option opt_low_priority insert_lock_option load_data_lock -%type locked_row_action opt_locked_row_action +%type locked_row_action opt_locked_row_action %type literal insert_ident temporal_literal @@ -10138,13 +10138,30 @@ table_locking_list: ; opt_locked_row_action: - /* Empty */ { $$= Locked_row_action::WAIT; } + /* Empty */ + { + $$.action= Locked_row_action::WAIT; + $$.wait_timeout= 0; + } | locked_row_action ; locked_row_action: - SKIP_SYM LOCKED_SYM { $$= Locked_row_action::SKIP; } - | NOWAIT_SYM { $$= Locked_row_action::NOWAIT; } + SKIP_SYM LOCKED_SYM + { + $$.action= Locked_row_action::SKIP; + $$.wait_timeout= 0; + } + | NOWAIT_SYM + { + $$.action= Locked_row_action::NOWAIT; + $$.wait_timeout= 0; + } + | WAIT_SYM NUM + { + $$.action= Locked_row_action::WAIT; + $$.wait_timeout= atoi($2.str); + } ; select_item_list: diff --git a/storage/innobase/handler/ha_innodb.cc b/storage/innobase/handler/ha_innodb.cc index 53f9c0d9c9a..bed22ba06eb 100644 --- a/storage/innobase/handler/ha_innodb.cc +++ b/storage/innobase/handler/ha_innodb.cc @@ -3078,6 +3078,7 @@ void ha_innobase::init_table_handle_for_HANDLER(void) { m_prebuilt->select_lock_type = LOCK_NONE; m_prebuilt->select_mode = SELECT_ORDINARY; + m_prebuilt->wait_timeout = 0; m_stored_select_lock_type = LOCK_NONE; /* Always fetch all columns in the index record */ @@ -19238,14 +19239,18 @@ THR_LOCK_DATA **ha_innobase::store_lock( m_stored_select_lock_type = LOCK_NONE; } - /* Set select mode for SKIP LOCKED / NOWAIT */ + /* Set select mode for SKIP LOCKED / NOWAIT / WAIT N */ if (lock_type != TL_IGNORE) { + m_prebuilt->wait_timeout = + table->pos_in_table_list->lock_descriptor().wait_timeout; switch (table->pos_in_table_list->lock_descriptor().action) { case THR_SKIP: m_prebuilt->select_mode = SELECT_SKIP_LOCKED; + ut_ad(m_prebuilt->wait_timeout == 0); break; case THR_NOWAIT: m_prebuilt->select_mode = SELECT_NOWAIT; + ut_ad(m_prebuilt->wait_timeout == 0); break; default: m_prebuilt->select_mode = SELECT_ORDINARY; @@ -19253,9 +19258,10 @@ THR_LOCK_DATA **ha_innobase::store_lock( } } - /* Ignore SKIP LOCKED / NO_WAIT for high priority transaction */ + /* Ignore SKIP LOCKED / NO_WAIT / WAIT N for high priority transaction */ if (trx_is_high_priority(trx)) { m_prebuilt->select_mode = SELECT_ORDINARY; + m_prebuilt->wait_timeout = 0; } if (!trx_is_started(trx) && (m_prebuilt->select_lock_type != LOCK_NONE || diff --git a/storage/innobase/handler/ha_innopart.cc b/storage/innobase/handler/ha_innopart.cc index 3ead23af6bf..c99b1de66b6 100644 --- a/storage/innobase/handler/ha_innopart.cc +++ b/storage/innobase/handler/ha_innopart.cc @@ -1173,6 +1173,7 @@ handler *ha_innopart::clone(const char *name, MEM_ROOT *mem_root) { new_handler->m_prebuilt->select_lock_type = m_prebuilt->select_lock_type; new_handler->m_prebuilt->select_mode = m_prebuilt->select_mode; + new_handler->m_prebuilt->wait_timeout = m_prebuilt->wait_timeout; } return new_handler; diff --git a/storage/innobase/include/que0que.h b/storage/innobase/include/que0que.h index 34510af19ff..dc683bee455 100644 --- a/storage/innobase/include/que0que.h +++ b/storage/innobase/include/que0que.h @@ -265,6 +265,7 @@ struct que_thr_t { thus far */ ulint lock_state; /*!< lock state of thread (table or row) */ + ulint wait_timeout; /*!< row lock wait timeout in seconds */ /** The thread slot in the lock_sys->waiting_threads array protected by lock_sys->wait_mutex when writing to it, and also by trx->mutex when changing diff --git a/storage/innobase/include/row0mysql.h b/storage/innobase/include/row0mysql.h index 810e3fb1e27..b0d116e83dd 100644 --- a/storage/innobase/include/row0mysql.h +++ b/storage/innobase/include/row0mysql.h @@ -704,6 +704,7 @@ struct row_prebuilt_t { ulint select_lock_type; /*!< LOCK_NONE, LOCK_S, or LOCK_X */ enum select_mode select_mode; /*!< SELECT_ORDINARY, SELECT_SKIP_LOKCED, or SELECT_NO_WAIT */ + ulint wait_timeout; /*!< row lock wait timeout in seconds */ ulint row_read_type; /*!< ROW_READ_WITH_LOCKS if row locks should be the obtained for records under an UPDATE or DELETE cursor. diff --git a/storage/innobase/lock/lock0wait.cc b/storage/innobase/lock/lock0wait.cc index 155140f939f..24fbadfed2b 100644 --- a/storage/innobase/lock/lock0wait.cc +++ b/storage/innobase/lock/lock0wait.cc @@ -199,6 +199,7 @@ void lock_wait_suspend_thread(que_thr_t *thr) /*!< in: query thread associated ibool was_declared_inside_innodb; ib_time_monotonic_ms_t start_time = 0; ulong lock_wait_timeout; + ulong trx_wait_timeout; trx = thr_get_trx(thr); @@ -210,7 +211,17 @@ void lock_wait_suspend_thread(que_thr_t *thr) /*!< in: query thread associated incomplete transactions that are being rolled back after crash recovery) will use the global value of innodb_lock_wait_timeout, because trx->mysql_thd == NULL. */ - lock_wait_timeout = trx_lock_wait_timeout_get(trx); + trx_wait_timeout = trx_lock_wait_timeout_get(trx); + /* if innodb_lock_wait_timeout and wait_timeout for update are both + non-zero, use minimum one; otherwise use maximum one. + Fixme: lock_wait_timeout means to be effective for row lock only, + but it's effective for table lock too. Usually table lock would't + be blocked because of MDL lock protection. */ + if (trx_wait_timeout != 0 && thr->wait_timeout != 0) { + lock_wait_timeout = ut_min(trx_wait_timeout, thr->wait_timeout); + } else { + lock_wait_timeout = ut_max(trx_wait_timeout, thr->wait_timeout); + } lock_wait_mutex_enter(); diff --git a/storage/innobase/que/que0que.cc b/storage/innobase/que/que0que.cc index 9f42ce6a571..879abd39455 100644 --- a/storage/innobase/que/que0que.cc +++ b/storage/innobase/que/que0que.cc @@ -175,6 +175,8 @@ que_thr_t *que_thr_create(que_fork_t *parent, mem_heap_t *heap, thr->lock_state = QUE_THR_LOCK_NOLOCK; + thr->wait_timeout = 0; + thr->prebuilt = prebuilt; UT_LIST_ADD_LAST(parent->thrs, thr); diff --git a/storage/innobase/row/row0mysql.cc b/storage/innobase/row/row0mysql.cc index b96c29dbf8b..6d177cae37d 100644 --- a/storage/innobase/row/row0mysql.cc +++ b/storage/innobase/row/row0mysql.cc @@ -881,6 +881,7 @@ Max size Secondary index: 16 * 8 bytes + PK = 256 bytes. */ prebuilt->select_lock_type = LOCK_NONE; prebuilt->select_mode = SELECT_ORDINARY; + prebuilt->wait_timeout = 0; prebuilt->search_tuple = dtuple_create(heap, search_tuple_n_fields); prebuilt->m_stop_tuple = dtuple_create(heap, search_tuple_n_fields); diff --git a/storage/innobase/row/row0sel.cc b/storage/innobase/row/row0sel.cc index 76723dfa0dc..9d740f6e507 100644 --- a/storage/innobase/row/row0sel.cc +++ b/storage/innobase/row/row0sel.cc @@ -4790,6 +4790,9 @@ dberr_t row_search_mvcc(byte *buf, page_cur_mode_t mode, } thr = que_fork_get_first_thr(prebuilt->sel_graph); + /* Set wait timeout in thr, we will check it + in lock_wait_suspend_thread(). */ + thr->wait_timeout = prebuilt->wait_timeout; que_thr_move_to_run_state_for_mysql(thr, trx); diff --git a/unittest/gunit/locking_clause_syntax-t.cc b/unittest/gunit/locking_clause_syntax-t.cc index ec707cbe783..9925e40d162 100644 --- a/unittest/gunit/locking_clause_syntax-t.cc +++ b/unittest/gunit/locking_clause_syntax-t.cc @@ -188,36 +188,55 @@ TEST_F(LockingClauseSyntaxTest, BinlogSafety) { const auto unsafe_skip_locked = Query_tables_list::BINLOG_STMT_UNSAFE_SKIP_LOCKED; const auto unsafe_nowait = Query_tables_list::BINLOG_STMT_UNSAFE_NOWAIT; + const auto unsafe_wait = Query_tables_list::BINLOG_STMT_UNSAFE_WAIT; parse("INSERT INTO t1 SELECT * FROM t1 FOR UPDATE"); EXPECT_FALSE(thd()->lex->is_stmt_unsafe(unsafe_skip_locked)); EXPECT_FALSE(thd()->lex->is_stmt_unsafe(unsafe_nowait)); + EXPECT_FALSE(thd()->lex->is_stmt_unsafe(unsafe_wait)); parse("INSERT INTO t1 SELECT * FROM t1 FOR UPDATE SKIP LOCKED"); EXPECT_TRUE(thd()->lex->is_stmt_unsafe(unsafe_skip_locked)); EXPECT_FALSE(thd()->lex->is_stmt_unsafe(unsafe_nowait)); + EXPECT_FALSE(thd()->lex->is_stmt_unsafe(unsafe_wait)); parse("INSERT INTO t1 SELECT * FROM t1 FOR UPDATE NOWAIT"); EXPECT_FALSE(thd()->lex->is_stmt_unsafe(unsafe_skip_locked)); EXPECT_TRUE(thd()->lex->is_stmt_unsafe(unsafe_nowait)); + EXPECT_FALSE(thd()->lex->is_stmt_unsafe(unsafe_wait)); + + parse("INSERT INTO t1 SELECT * FROM t1 FOR UPDATE WAIT 10"); + EXPECT_FALSE(thd()->lex->is_stmt_unsafe(unsafe_skip_locked)); + EXPECT_FALSE(thd()->lex->is_stmt_unsafe(unsafe_nowait)); + EXPECT_TRUE(thd()->lex->is_stmt_unsafe(unsafe_wait)); parse( "INSERT INTO t1 SELECT * FROM t1, t2 " "FOR UPDATE OF t1 SKIP LOCKED FOR UPDATE OF t2 NOWAIT"); EXPECT_TRUE(thd()->lex->is_stmt_unsafe(unsafe_skip_locked)); EXPECT_TRUE(thd()->lex->is_stmt_unsafe(unsafe_nowait)); + EXPECT_FALSE(thd()->lex->is_stmt_unsafe(unsafe_wait)); parse( "UPDATE t3 SET a = " "(SELECT b FROM t1 WHERE a >= 2 LIMIT 1 FOR UPDATE SKIP LOCKED)"); EXPECT_TRUE(thd()->lex->is_stmt_unsafe(unsafe_skip_locked)); EXPECT_FALSE(thd()->lex->is_stmt_unsafe(unsafe_nowait)); + EXPECT_FALSE(thd()->lex->is_stmt_unsafe(unsafe_wait)); parse( "UPDATE t3 SET a = " "(SELECT b FROM t1 WHERE a >= 2 LIMIT 1 FOR UPDATE NOWAIT)"); EXPECT_FALSE(thd()->lex->is_stmt_unsafe(unsafe_skip_locked)); EXPECT_TRUE(thd()->lex->is_stmt_unsafe(unsafe_nowait)); + EXPECT_FALSE(thd()->lex->is_stmt_unsafe(unsafe_wait)); + + parse( + "UPDATE t3 SET a = " + "(SELECT b FROM t1 WHERE a >= 2 LIMIT 1 FOR UPDATE WAIT 10)"); + EXPECT_FALSE(thd()->lex->is_stmt_unsafe(unsafe_skip_locked)); + EXPECT_FALSE(thd()->lex->is_stmt_unsafe(unsafe_nowait)); + EXPECT_TRUE(thd()->lex->is_stmt_unsafe(unsafe_wait)); } } // namespace locking_clause_syntax_unittest