Description:
On MySQL Server 9.7.2, Linux x86_64, debug build (GCC 13.3.1), a `KILL QUERY`
issued during an InnoDB in-place table rebuild can leave a `Page_load` with an
outstanding buffer fix. The debug server then aborts while destroying the DDL
Builder instead of returning `ER_QUERY_INTERRUPTED` to the client.
The issue is deterministic with Debug Sync; it is not a timing-only failure.
`Page_load::release()` intentionally increments `m_n_blocks_buf_fixed` before
committing its mini-transaction, because the page is expected to be re-pinned
soon. `Page_load::latch()` performs the matching decrement.
The failing path is:
```text
Btree_load::release()
-> Page_load::release() increments the extra buffer fix
-> Debug Sync waits
KILL QUERY
-> Parallel_reader::worker() returns DB_INTERRUPTED after traversal
-> Parallel_cursor::scan() returns the error without Builder cleanup
-> Builder::~Builder() directly deletes Btree_load
-> Page_load::~Page_load() asserts on the outstanding buffer fix
```
There is already cleanup for an error returned directly by
`Builder::add_row()`: `Parallel_cursor::bulk_inserter` iterates over all
Builders and calls `Builder::handle_error(err)`. That method correctly calls
`latch()`, `finish(err)`, and deletes the active `Btree_load`.
However, `DB_INTERRUPTED` detected by `Parallel_reader::worker()` after its
traversal callback bypasses that cleanup. `Parallel_reader::run()` returns the
error, `Parallel_cursor::scan()` returns it, and
`Loader::scan_and_build_indexes()` only calls `Cursor::finish(err)`, which is a
no-op for a non-FTS cursor.
How to repeat:
diff --git a/mysql-test/suite/innodb/t/builder_error_case_kill.test b/mysql-test/suite/innodb/t/builder_error_case_kill.test
new file mode 100644
index 00000000000..8caf0167b8c
--- /dev/null
+++ b/mysql-test/suite/innodb/t/builder_error_case_kill.test
@@ -0,0 +1,57 @@
+--source include/have_debug.inc
+--source include/have_debug_sync.inc
+
+# Interrupt a table rebuild after the clustered-index Btree_load has released
+# its page. The interrupted parallel reader must clean up every Builder.
+CREATE TABLE t1 (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ c1 INT,
+ KEY idx_1(c1)
+) ENGINE=InnoDB;
+
+--disable_query_log
+DELIMITER |;
+CREATE PROCEDURE populate_t1(IN BASE INT, IN SIZE INT)
+BEGIN
+ DECLARE i INT DEFAULT BASE;
+ WHILE (i + 10 <= SIZE) DO
+ INSERT INTO t1(c1) VALUES
+ (i), (i+1), (i+2), (i+3), (i+4),
+ (i+5), (i+6), (i+7), (i+8), (i+9);
+ SET i = i + 10;
+ END WHILE;
+END|
+DELIMITER ;|
+
+CALL populate_t1(0, 11000);
+DROP PROCEDURE populate_t1;
+--enable_query_log
+
+SELECT COUNT(*) FROM t1;
+
+connect (con1,localhost,root,,);
+connection con1;
+let $ID= `SELECT @id := CONNECTION_ID()`;
+SET DEBUG_SYNC=
+ 'ddl_btree_load_released SIGNAL btree_load_released WAIT_FOR kill_ddl';
+--send ALTER TABLE t1 ADD COLUMN c2 INT DEFAULT 20, ALGORITHM=INPLACE;
+
+connection default;
+let $ignore= `SELECT @id := $ID`;
+SET DEBUG_SYNC='now WAIT_FOR btree_load_released';
+KILL QUERY @id;
+SET DEBUG_SYNC='now SIGNAL kill_ddl';
+
+connection con1;
+--error ER_QUERY_INTERRUPTED
+--reap
+SET DEBUG_SYNC='RESET';
+
+connection default;
+disconnect con1;
+
+# A debug build detects a buffer-fixed page during DDL teardown.
+--source include/shutdown_mysqld.inc
+--source include/start_mysqld.inc
+
+DROP TABLE t1;
diff --git a/storage/innobase/btr/btr0load.cc b/storage/innobase/btr/btr0load.cc
index 9cdd01463a4..79afb081526 100644
--- a/storage/innobase/btr/btr0load.cc
+++ b/storage/innobase/btr/btr0load.cc
@@ -1009,6 +1009,7 @@ Btree_load::~Btree_load() noexcept {
void Btree_load::release() noexcept {
ut_ad(m_n_recs > 0);
m_page_loaders[0]->release();
+ DEBUG_SYNC_C("ddl_btree_load_released");
}
void Btree_load::latch() noexcept {
Description: On MySQL Server 9.7.2, Linux x86_64, debug build (GCC 13.3.1), a `KILL QUERY` issued during an InnoDB in-place table rebuild can leave a `Page_load` with an outstanding buffer fix. The debug server then aborts while destroying the DDL Builder instead of returning `ER_QUERY_INTERRUPTED` to the client. The issue is deterministic with Debug Sync; it is not a timing-only failure. `Page_load::release()` intentionally increments `m_n_blocks_buf_fixed` before committing its mini-transaction, because the page is expected to be re-pinned soon. `Page_load::latch()` performs the matching decrement. The failing path is: ```text Btree_load::release() -> Page_load::release() increments the extra buffer fix -> Debug Sync waits KILL QUERY -> Parallel_reader::worker() returns DB_INTERRUPTED after traversal -> Parallel_cursor::scan() returns the error without Builder cleanup -> Builder::~Builder() directly deletes Btree_load -> Page_load::~Page_load() asserts on the outstanding buffer fix ``` There is already cleanup for an error returned directly by `Builder::add_row()`: `Parallel_cursor::bulk_inserter` iterates over all Builders and calls `Builder::handle_error(err)`. That method correctly calls `latch()`, `finish(err)`, and deletes the active `Btree_load`. However, `DB_INTERRUPTED` detected by `Parallel_reader::worker()` after its traversal callback bypasses that cleanup. `Parallel_reader::run()` returns the error, `Parallel_cursor::scan()` returns it, and `Loader::scan_and_build_indexes()` only calls `Cursor::finish(err)`, which is a no-op for a non-FTS cursor. How to repeat: diff --git a/mysql-test/suite/innodb/t/builder_error_case_kill.test b/mysql-test/suite/innodb/t/builder_error_case_kill.test new file mode 100644 index 00000000000..8caf0167b8c --- /dev/null +++ b/mysql-test/suite/innodb/t/builder_error_case_kill.test @@ -0,0 +1,57 @@ +--source include/have_debug.inc +--source include/have_debug_sync.inc + +# Interrupt a table rebuild after the clustered-index Btree_load has released +# its page. The interrupted parallel reader must clean up every Builder. +CREATE TABLE t1 ( + id INT AUTO_INCREMENT PRIMARY KEY, + c1 INT, + KEY idx_1(c1) +) ENGINE=InnoDB; + +--disable_query_log +DELIMITER |; +CREATE PROCEDURE populate_t1(IN BASE INT, IN SIZE INT) +BEGIN + DECLARE i INT DEFAULT BASE; + WHILE (i + 10 <= SIZE) DO + INSERT INTO t1(c1) VALUES + (i), (i+1), (i+2), (i+3), (i+4), + (i+5), (i+6), (i+7), (i+8), (i+9); + SET i = i + 10; + END WHILE; +END| +DELIMITER ;| + +CALL populate_t1(0, 11000); +DROP PROCEDURE populate_t1; +--enable_query_log + +SELECT COUNT(*) FROM t1; + +connect (con1,localhost,root,,); +connection con1; +let $ID= `SELECT @id := CONNECTION_ID()`; +SET DEBUG_SYNC= + 'ddl_btree_load_released SIGNAL btree_load_released WAIT_FOR kill_ddl'; +--send ALTER TABLE t1 ADD COLUMN c2 INT DEFAULT 20, ALGORITHM=INPLACE; + +connection default; +let $ignore= `SELECT @id := $ID`; +SET DEBUG_SYNC='now WAIT_FOR btree_load_released'; +KILL QUERY @id; +SET DEBUG_SYNC='now SIGNAL kill_ddl'; + +connection con1; +--error ER_QUERY_INTERRUPTED +--reap +SET DEBUG_SYNC='RESET'; + +connection default; +disconnect con1; + +# A debug build detects a buffer-fixed page during DDL teardown. +--source include/shutdown_mysqld.inc +--source include/start_mysqld.inc + +DROP TABLE t1; diff --git a/storage/innobase/btr/btr0load.cc b/storage/innobase/btr/btr0load.cc index 9cdd01463a4..79afb081526 100644 --- a/storage/innobase/btr/btr0load.cc +++ b/storage/innobase/btr/btr0load.cc @@ -1009,6 +1009,7 @@ Btree_load::~Btree_load() noexcept { void Btree_load::release() noexcept { ut_ad(m_n_recs > 0); m_page_loaders[0]->release(); + DEBUG_SYNC_C("ddl_btree_load_released"); } void Btree_load::latch() noexcept {