commit 433d7119621b8cd5ff3c55706373322c8d47d0c1 Author: zhongbei.yk Date: Tue Feb 27 12:00:10 2024 +0800 Buffer pool resize cancel Background ========== When a transaction needs to apply for a row or table lock, it is first applied from the memory heap. When memory heap is used up, memory pages are requested from buffer pool to hold row or table locks. Memory pages cannot be reallocated in the resize process of buffer pool. When resize buffer pool to a smaller size, the withdraw stage may be blocked by the transaction that holding a memory bp page. In that situation, the workload may increase a lot and we need to increase the size of bp. Thus we need to cancel the resize process to make buffer pool can be scale up. Solution ======== A new global system variable called innodb_buffer_pool_resize_cancel is introduced. When in the process of resize, you can set it to OFF or ON to intruct the resize thread to cancel the resizing. User Interface ============== * innodb_buffer_pool_resize_cancel Trigger the cancel of innodb buffer pool resize.This value can be changed dynamically. Default:OFF Implementation ============== The shrink of buffer pool can divided into two stage: 1. Withdraw stage 2. Delete chunk stage. Withdraw stage can be blocked. Cancel resize is only supported during the withdraw stage. Cancel resize process: (1) Stop resize. (2) Move page from withdraw list to free list. Reset some properties like curr_size, n_chunks_new and withdraw_target; (3) Reset buffer pool status. Set some global variables of the instance to its original values. srv_buf_pool_size, srv_buf_pool_curr_size should be set to the same as srv_buf_pool_old_size, (4) Enable the AHI if needed; (5) Set the resize status to BP_RESIZE_NONE. diff --git a/mysql-test/suite/innodb/include/buffer_pool_resize_cancel.inc b/mysql-test/suite/innodb/include/buffer_pool_resize_cancel.inc new file mode 100644 index 00000000000..22d479206b6 --- /dev/null +++ b/mysql-test/suite/innodb/include/buffer_pool_resize_cancel.inc @@ -0,0 +1,115 @@ +# We use this inc file to validate the cancel of resize. The new size of bp +# needs to be passed to this inc file. this inc file +# contains the following test: +# 1. Launch a resize and then cancel the resize which is on none stage. The +# resize should be canceled failed. +# 2. Launch a resize and then cancel the resize which is on prepare stage. +# The resize should be canceled successfully. +# 3. Launch a resize and then cancel the resize which is on lock stage. The +# resize should be canceled failed. +# +# USAGE +# +# let $new_bp_size = XXXX +# --source suite/innodb/include/buffer_pool_resize_cancel.inc +# + +let $log_error_= `SELECT @@GLOBAL.log_error`; +--exec echo '' > $log_error_ + +--echo # +--echo # 1. Trigger innodb_buffer_pool_resize_cancel when resize is none. +--echo # + +--disable_result_log +SELECT * from t1; +--enable_result_log + +--echo # should have warnings +SET GLOBAL innodb_buffer_pool_resize_cancel = ON; + +--let $assert_text= Try to cancel buffer pool resize: failed should be written to error log +--let $assert_select= Try to cancel buffer pool resize +--let $assert_match= .*Try to cancel buffer pool resize: failed. It is not currently in the buffer pool resize process. +--let $assert_file= $log_error_ +--source include/assert_grep.inc + +--echo # +--echo # 2. Cancel resize which is on prepare stage +--echo # Should be canceled successfully. + +--disable_result_log +SELECT * from t1; +--enable_result_log + +SET GLOBAL debug = '+d,ib_buf_pool_resize_after_prepare'; + +SELECT @@innodb_buffer_pool_size; +eval SET GLOBAL innodb_buffer_pool_size = $new_bp_size; + +let $wait_condition = + SELECT variable_value = 'Preparations are complete.' + FROM performance_schema.global_status + WHERE LOWER(variable_name) = 'innodb_buffer_pool_resize_status'; +let $wait_timeout = 1800; +--source include/wait_condition.inc + +--echo # should have no warning +SET GLOBAL innodb_buffer_pool_resize_cancel = ON; + +--let $assert_text= Try to cancel buffer pool resize: successed should be written to error log +--let $assert_select= Try to cancel buffer pool resize: +--let $assert_match= .*Try to cancel buffer pool resize: successed.* +--let $assert_file= $log_error_ +--source include/assert_grep.inc + + +SET GLOBAL debug = '-d,ib_buf_pool_resize_after_prepare'; + +let $wait_condition = + SELECT variable_value like 'Resizing buffer pool failed, canceled resizing at %' + FROM performance_schema.global_status + WHERE LOWER(variable_name) = 'innodb_buffer_pool_resize_status'; +--source include/wait_condition.inc + +SELECT @@innodb_buffer_pool_size; + + +--echo # +--echo # 3. Cancel resize which is on lock stage +--echo # Should be canceled failed + +--disable_result_log +SELECT * from t1; +--enable_result_log + +SET GLOBAL debug = '+d,ib_buf_pool_resize_wait_before_resize'; + +SELECT @@innodb_buffer_pool_size; + +eval SET GLOBAL innodb_buffer_pool_size = $new_bp_size; + +let $wait_condition = + SELECT variable_value = 'Latching whole of buffer pool.' + FROM performance_schema.global_status + WHERE LOWER(variable_name) = 'innodb_buffer_pool_resize_status'; +--source include/wait_condition.inc + +--echo # should have warnings +SET GLOBAL innodb_buffer_pool_resize_cancel = ON; + +--let $assert_text= Try to cancel buffer pool resize: should be written to error log +--let $assert_select= Try to cancel buffer pool resize +--let $assert_match= .*Try to cancel buffer pool resize: failed. Can not cancel buffer pool resize in lock stage. Buffer pool resize will be completed soon. +--let $assert_file= $log_error_ +--source include/assert_grep.inc + +SET GLOBAL debug = '-d,ib_buf_pool_resize_wait_before_resize'; + +let $wait_condition = + SELECT variable_value like 'Completed resizing buffer pool at%' + FROM performance_schema.global_status + WHERE LOWER(variable_name) = 'innodb_buffer_pool_resize_status'; +--source include/wait_condition.inc + +SELECT @@innodb_buffer_pool_size; diff --git a/mysql-test/suite/innodb/r/feature_buffer_pool_resize_cancel.result b/mysql-test/suite/innodb/r/feature_buffer_pool_resize_cancel.result new file mode 100644 index 00000000000..bdcee4e8eee --- /dev/null +++ b/mysql-test/suite/innodb/r/feature_buffer_pool_resize_cancel.result @@ -0,0 +1,118 @@ +########################### +# 1. Prepare +########################### +######################################### +# 2. begin test +######################################### +set @old_innodb_adaptive_hash_index = @@GLOBAL.innodb_adaptive_hash_index; +SET GLOBAL innodb_adaptive_hash_index = ON; +# should be 16M +SELECT @@innodb_buffer_pool_size; +@@innodb_buffer_pool_size +16777216 +########################################################################### +# 2.1. Let resize argument '16M -> 32M', and then invoke the inc file to +# perform the actual test when AHI is on. +########################################################################### +# +# 1. Trigger innodb_buffer_pool_resize_cancel when resize is none. +# +SELECT * from t1; +# should have warnings +SET GLOBAL innodb_buffer_pool_resize_cancel = ON; +Warnings: +Warning 4167 Cancel failed. It is not currently in the buffer pool resize process. +include/assert_grep.inc [Try to cancel buffer pool resize: failed should be written to error log] +# +# 2. Cancel resize which is on prepare stage +# Should be canceled successfully. +SELECT * from t1; +SET GLOBAL debug = '+d,ib_buf_pool_resize_after_prepare'; +SELECT @@innodb_buffer_pool_size; +@@innodb_buffer_pool_size +16777216 +SET GLOBAL innodb_buffer_pool_size = 33554432; +# should have no warning +SET GLOBAL innodb_buffer_pool_resize_cancel = ON; +include/assert_grep.inc [Try to cancel buffer pool resize: successed should be written to error log] +SET GLOBAL debug = '-d,ib_buf_pool_resize_after_prepare'; +SELECT @@innodb_buffer_pool_size; +@@innodb_buffer_pool_size +16777216 +# +# 3. Cancel resize which is on lock stage +# Should be canceled failed +SELECT * from t1; +SET GLOBAL debug = '+d,ib_buf_pool_resize_wait_before_resize'; +SELECT @@innodb_buffer_pool_size; +@@innodb_buffer_pool_size +16777216 +SET GLOBAL innodb_buffer_pool_size = 33554432; +# should have warnings +SET GLOBAL innodb_buffer_pool_resize_cancel = ON; +Warnings: +Warning 4167 Cancel failed. Can not cancel buffer pool resize in lock stage. +include/assert_grep.inc [Try to cancel buffer pool resize: should be written to error log] +SET GLOBAL debug = '-d,ib_buf_pool_resize_wait_before_resize'; +SELECT @@innodb_buffer_pool_size; +@@innodb_buffer_pool_size +33554432 +SELECT @@innodb_adaptive_hash_index; +@@innodb_adaptive_hash_index +1 +SET GLOBAL innodb_adaptive_hash_index = OFF; +########################################################################### +# 2.2. Let resize argument '32M -> 8M', and then invoke the inc file to +# perform the actual test when AHI is off. +########################################################################### +# +# 1. Trigger innodb_buffer_pool_resize_cancel when resize is none. +# +SELECT * from t1; +# should have warnings +SET GLOBAL innodb_buffer_pool_resize_cancel = ON; +Warnings: +Warning 4167 Cancel failed. It is not currently in the buffer pool resize process. +include/assert_grep.inc [Try to cancel buffer pool resize: failed should be written to error log] +# +# 2. Cancel resize which is on prepare stage +# Should be canceled successfully. +SELECT * from t1; +SET GLOBAL debug = '+d,ib_buf_pool_resize_after_prepare'; +SELECT @@innodb_buffer_pool_size; +@@innodb_buffer_pool_size +33554432 +SET GLOBAL innodb_buffer_pool_size = 8388608; +# should have no warning +SET GLOBAL innodb_buffer_pool_resize_cancel = ON; +include/assert_grep.inc [Try to cancel buffer pool resize: successed should be written to error log] +SET GLOBAL debug = '-d,ib_buf_pool_resize_after_prepare'; +SELECT @@innodb_buffer_pool_size; +@@innodb_buffer_pool_size +33554432 +# +# 3. Cancel resize which is on lock stage +# Should be canceled failed +SELECT * from t1; +SET GLOBAL debug = '+d,ib_buf_pool_resize_wait_before_resize'; +SELECT @@innodb_buffer_pool_size; +@@innodb_buffer_pool_size +33554432 +SET GLOBAL innodb_buffer_pool_size = 8388608; +# should have warnings +SET GLOBAL innodb_buffer_pool_resize_cancel = ON; +Warnings: +Warning 4167 Cancel failed. Can not cancel buffer pool resize in lock stage. +include/assert_grep.inc [Try to cancel buffer pool resize: should be written to error log] +SET GLOBAL debug = '-d,ib_buf_pool_resize_wait_before_resize'; +SELECT @@innodb_buffer_pool_size; +@@innodb_buffer_pool_size +8388608 +SELECT @@innodb_adaptive_hash_index; +@@innodb_adaptive_hash_index +0 +# +# cleanup +# +DROP TABLE t1; +set global innodb_adaptive_hash_index = @old_innodb_adaptive_hash_index; diff --git a/mysql-test/suite/innodb/r/feature_buffer_pool_resize_cancel_when_shrink_blocked.result b/mysql-test/suite/innodb/r/feature_buffer_pool_resize_cancel_when_shrink_blocked.result new file mode 100644 index 00000000000..67f66c18568 --- /dev/null +++ b/mysql-test/suite/innodb/r/feature_buffer_pool_resize_cancel_when_shrink_blocked.result @@ -0,0 +1,89 @@ +########################### +# 1. Prepare +########################### +set @old_innodb_buffer_pool_size = @@GLOBAL.innodb_buffer_pool_size; +CREATE TABLE t1( +id bigint auto_increment, +c1 varchar(3000) default '', +primary key(id) +); +CREATE PROCEDURE insert_into_tables(IN num INTEGER) +BEGIN +declare x INT; +set x=1; +while x 32M', and then invoke the inc file to +# perform the actual test when AHI is on. +# 3. Let resize argument '32M -> 8M', and then invoke the inc file to +# perform the actual test when AHI is off. + +--source include/have_innodb_max_16k.inc +--source include/have_debug.inc +--source include/have_binlog_format_row.inc + +--let MYSQLD_LOG=$MYSQLTEST_VARDIR/log/mysqld.1.err + +--echo ########################### +--echo # 1. Prepare +--echo ########################### + +--let $MYSQLD_LOG= $MYSQLTEST_VARDIR/log/tmp_error.log +--let $restart_parameters = restart: --log-error=$MYSQLD_LOG +--source include/restart_mysqld_no_echo.inc + +--disable_query_log +CREATE TABLE t1 (c1 int auto_increment primary key, c2 varchar(110)) engine=InnoDB; +INSERT INTO t1(c2) VALUES(repeat('a', 100)); +let $i = 8; +while ($i > 0) +{ + INSERT INTO t1(c2) SELECT c2 FROM t1; + --dec $i +} +--enable_query_log + +--echo ######################################### +--echo # 2. begin test +--echo ######################################### +--let $restart_parameters = restart: --log-error=$MYSQLD_LOG +--source include/restart_mysqld_no_echo.inc + +set @old_innodb_adaptive_hash_index = @@GLOBAL.innodb_adaptive_hash_index; + +# configure ahi on +SET GLOBAL innodb_adaptive_hash_index = ON; + +--echo # should be 16M +SELECT @@innodb_buffer_pool_size; +--echo ########################################################################### +--echo # 2.1. Let resize argument '16M -> 32M', and then invoke the inc file to +--echo # perform the actual test when AHI is on. +--echo ########################################################################### +let $new_bp_size=33554432; +--source suite/innodb/include/buffer_pool_resize_cancel.inc + +SELECT @@innodb_adaptive_hash_index; + +# configure ahi off +SET GLOBAL innodb_adaptive_hash_index = OFF; +--echo ########################################################################### +--echo # 2.2. Let resize argument '32M -> 8M', and then invoke the inc file to +--echo # perform the actual test when AHI is off. +--echo ########################################################################### +let $new_bp_size=8388608; +--source suite/innodb/include/buffer_pool_resize_cancel.inc + +SELECT @@innodb_adaptive_hash_index; + +--echo # +--echo # cleanup +--echo # + +DROP TABLE t1; +set global innodb_adaptive_hash_index = @old_innodb_adaptive_hash_index; + +--let $restart_parameters = restart: +--source include/restart_mysqld_no_echo.inc diff --git a/mysql-test/suite/innodb/t/feature_buffer_pool_resize_cancel_when_shrink_blocked-master.opt b/mysql-test/suite/innodb/t/feature_buffer_pool_resize_cancel_when_shrink_blocked-master.opt new file mode 100644 index 00000000000..76c2a0b16e3 --- /dev/null +++ b/mysql-test/suite/innodb/t/feature_buffer_pool_resize_cancel_when_shrink_blocked-master.opt @@ -0,0 +1,3 @@ +--innodb-buffer-pool-size=128M +--innodb-buffer-pool-instances=1 +--innodb-buffer-pool-chunk-size=1M diff --git a/mysql-test/suite/innodb/t/feature_buffer_pool_resize_cancel_when_shrink_blocked.test b/mysql-test/suite/innodb/t/feature_buffer_pool_resize_cancel_when_shrink_blocked.test new file mode 100644 index 00000000000..4750bee4edf --- /dev/null +++ b/mysql-test/suite/innodb/t/feature_buffer_pool_resize_cancel_when_shrink_blocked.test @@ -0,0 +1,177 @@ +# ==== Purpose ==== +# +# Buffer pool resize can be blocked by the transaction that holding a +# memory buffer pool page when shrink the bp size. This test reproduces +# this scenario, and then cancel the resize, make the bp can be resized again. +# +# ==== Implementation ==== +# 1. Prepare a table and insert some records. +# 2. Begin a transaction. +# 3. Add a lot of locks in the transaction, thus it will request a lot of memory +# pages from bp. The transaction will hold the memory pages. +# 4. Resize bp to a very small size. The resize will be blocked by the transaction +# that held the memory pages. And we check the error log to make sure that it +# is blocked. +# 5. Launch cancel command, thus the resize will be canceled. +# 6. Commit the transaction, thus the memory page will be released. And now we +# can resize the bp to a very small size. + +--source include/have_innodb_max_16k.inc +--source include/have_debug.inc +--source include/have_binlog_format_row.inc + +--let MYSQLD_LOG=$MYSQLTEST_VARDIR/log/mysqld.1.err + +--let $MYSQLD_LOG= $MYSQLTEST_VARDIR/log/tmp_error.log +--let $restart_parameters = restart: --log-error=$MYSQLD_LOG +--source include/restart_mysqld_no_echo.inc + +--disable_query_log +call mtr.add_suppression("The following trx might hold the blocks in buffer pool to be withdrawn"); +call mtr.add_suppression("Received a command to cancel the buffer pool resize and try to set cancel"); +--enable_query_log + + +--echo ########################### +--echo # 1. Prepare +--echo ########################### + +set @old_innodb_buffer_pool_size = @@GLOBAL.innodb_buffer_pool_size; + +CREATE TABLE t1( + id bigint auto_increment, + c1 varchar(3000) default '', + primary key(id) +); + +delimiter $$; +CREATE PROCEDURE insert_into_tables(IN num INTEGER) + BEGIN + declare x INT; + set x=1; + while x(UT_NEW_THIS_FILE_PSI_KEY); + buf_pool_ptr = (buf_pool_t *)ut::zalloc_withkey( UT_NEW_THIS_FILE_PSI_KEY, n_instances * sizeof *buf_pool_ptr); @@ -1753,6 +1771,12 @@ void buf_pool_resize_wait_for_test() { should_wait_for_test = true; std::this_thread::sleep_for(std::chrono::milliseconds(10));); break; + case BUF_POOL_RESIZE_CANCEL: + DBUG_EXECUTE_IF( + "ib_buf_pool_resize_cancel_status_code", + should_wait_for_test = true; + std::this_thread::sleep_for(std::chrono::milliseconds(10));); + break; } } } @@ -1835,6 +1859,10 @@ until withdrawn by buf_pool->withdraw_target. @param[in] buf_pool buffer pool instance @retval true if retry is needed */ static bool buf_pool_withdraw_blocks(buf_pool_t *buf_pool) { + /* When set BP_RESIZE_CANCEL, no need to withdraw blocks. */ + if (buf_resize_control->is_cancel()) + return true; + buf_block_t *block; ulint loop_count = 0; ulint i = buf_pool_index(buf_pool); @@ -1882,6 +1910,12 @@ static bool buf_pool_withdraw_blocks(buf_pool_t *buf_pool) { mutex_enter(&buf_pool->LRU_list_mutex); for (auto bpage : buf_pool->LRU.removable()) { + /* When set BP_RESIZE_CANCEL, no need to continue withdrawing. */ + if (buf_resize_control->is_cancel()) { + mutex_exit(&buf_pool->LRU_list_mutex); + return true; + } + BPageMutex *block_mutex; block_mutex = buf_page_get_mutex(bpage); @@ -2136,6 +2170,7 @@ static void buf_pool_resize() { new_instance_size = srv_buf_pool_size / srv_buf_pool_instances; new_instance_size /= UNIV_PAGE_SIZE; + buf_resize_control->set_state(BP_RESIZE_PREPARE); buf_resize_status( BUF_POOL_RESIZE_START, "Resizing buffer pool from " ULINTPF " to " ULINTPF " (unit=%llu).", @@ -2224,6 +2259,10 @@ withdraw_retry: return; } + if (buf_resize_control->check_cancel(btr_search_was_enabled)) { + return; + } + /* abort buffer pool load */ buf_load_abort(); @@ -2272,6 +2311,21 @@ withdraw_retry: trx_sys_mutex_exit(); } +#ifdef UNIV_DEBUG + { + DBUG_EXECUTE_IF("ib_buf_pool_resize_is_blocked", + buf_resize_status(BUF_POOL_RESIZE_CANCEL, + "Buffer pool resize is blocked.");); + bool should_wait = true; + while (should_wait) { + should_wait = false; + DBUG_EXECUTE_IF( + "ib_buf_pool_resize_is_blocked", should_wait = true; + std::this_thread::sleep_for(std::chrono::milliseconds(10));); + } + } +#endif /* UNIV_DEBUG */ + withdraw_start_time = std::chrono::system_clock::now(); } @@ -2290,6 +2344,29 @@ withdraw_retry: } buf_resize_status_progress_reset(); + +#ifdef UNIV_DEBUG + { + DBUG_EXECUTE_IF("ib_buf_pool_resize_after_prepare", + buf_resize_status(BUF_POOL_RESIZE_CANCEL, + "Preparations are complete.");); + bool should_wait = true; + while (should_wait) { + should_wait = false; + DBUG_EXECUTE_IF( + "ib_buf_pool_resize_after_prepare", should_wait = true; + std::this_thread::sleep_for(std::chrono::milliseconds(10));); + } + } +#endif /* UNIV_DEBUG */ + + if (buf_resize_control->check_cancel(btr_search_was_enabled)) { + return; + } + /* Note that if cancel is set between check_cancel and the following line. + The cancel state would be overwritten. */ + buf_resize_control->set_state(BP_RESIZE_LOCK); + buf_resize_status(BUF_POOL_RESIZE_GLOBAL_LOCK, "Latching whole of buffer pool."); @@ -2613,6 +2690,7 @@ withdraw_retry: ut_a(buf_validate()); #endif /* UNIV_DEBUG || UNIV_BUF_DEBUG */ + buf_resize_control->set_state(BP_RESIZE_NONE); return; } @@ -6852,6 +6930,114 @@ const char *buf_block_t::get_page_type_str() const noexcept { return fil_get_page_type_str(type); } +/** Cancel the buffer pool resizing process. +@param[in] enable_ahi true if need to enable ahi. */ +void buf_pool_resize_cancel(bool enable_ahi) { + /* Reset buf_pool_resize_status_progress. */ + buf_pool_resize_status_progress.store(0); + buf_resize_status(BUF_POOL_RESIZE_CANCEL, + "Start to cancel the resizing of buffer pool."); + /* Move pages in withdraw list to free list */ + for (ulint i = 0; i < srv_buf_pool_instances; i++) { + buf_pool_t *buf_pool = buf_pool_from_array(i); + + mutex_enter(&buf_pool->free_list_mutex); + +/* block->page.in_free_list and block->in_withdraw_list need to be reset. */ +#ifdef UNIV_DEBUG + buf_block_t *block = + reinterpret_cast(UT_LIST_GET_FIRST(buf_pool->withdraw)); + + while (block != nullptr) { + ut_ad(block->in_withdraw_list); + ut_ad(!block->page.in_flush_list); + ut_ad(!block->page.in_LRU_list); + ut_a(!buf_page_in_file(&block->page)); + + buf_block_t *next_block = + reinterpret_cast(UT_LIST_GET_NEXT(list, &block->page)); + + ut_d(block->page.in_free_list = true); + ut_d(block->in_withdraw_list = false); + + block = next_block; + } +#endif /* UNIV_DEBUG */ + + /* Both withdraw list and free list use the same node, just merge withdraw + list into free list. */ + if (UT_LIST_GET_LEN(buf_pool->withdraw) > 0) { + UT_LIST_APPEND_LIST(buf_pool->free, buf_pool->withdraw); + } + + ut_ad(UT_LIST_GET_LEN(buf_pool->withdraw) == 0); + + mutex_exit(&buf_pool->free_list_mutex); + + /* Reset buffer pool status. */ + buf_pool->curr_size = buf_pool->old_size; + buf_pool->n_chunks_new = buf_pool->n_chunks; + buf_pool->withdraw_target = 0; + + buf_resize_status_progress_update(i + 1, srv_buf_pool_instances); + } + + srv_buf_pool_curr_size = srv_buf_pool_old_size; + srv_buf_pool_size = srv_buf_pool_old_size; + os_wmb; + + /* enable AHI if needed */ + if (enable_ahi) { + btr_search_enable(); + ib::info(ER_IB_MSG_61) << "Re-enabled adaptive hash index."; + } + + char now[32]; + ut_sprintf_timestamp(now); + buf_resize_status_progress_reset(); + /* Set buf_pool_resize_status_code to BUF_POOL_RESIZE_FAILED, thus another + resize can be launched in innodb_buffer_pool_size_update function after the + cancel progress. */ + buf_resize_status( + BUF_POOL_RESIZE_FAILED, + "Resizing buffer pool failed," + " canceled resizing at %s.", + now); + buf_resize_status_progress_update(1, 1); + +#if defined UNIV_DEBUG || defined UNIV_BUF_DEBUG + ut_a(buf_validate()); +#endif /* UNIV_DEBUG || UNIV_BUF_DEBUG */ +} + +void Buf_resize_control::try_set_cancel(THD *thd) { + mutex_enter(&m_mutex); + if (m_state == BP_RESIZE_NONE) { + push_warning(thd, Sql_condition::SL_WARNING, + ER_INNODB_BUFFER_POOL_RESIZE_CANCEL_FAILED, + "Cancel failed. It is not currently in the buffer pool resize process. "); + ib::warn(ER_REQUEST_CANCEL_BUFFER_POOL_RESIZE) + << "failed. It is not currently in the buffer pool resize process. "; + } else if (m_state == BP_RESIZE_LOCK) { + push_warning(thd, Sql_condition::SL_WARNING, + ER_INNODB_BUFFER_POOL_RESIZE_CANCEL_FAILED, + "Cancel failed. Can not cancel buffer pool resize in lock stage. "); + ib::warn(ER_REQUEST_CANCEL_BUFFER_POOL_RESIZE) + << "failed. Can not cancel buffer pool resize in lock stage. " + << "Buffer pool resize will be completed soon. "; + } else if (m_state == BP_RESIZE_CANCEL) { + push_warning(thd, Sql_condition::SL_WARNING, + ER_INNODB_BUFFER_POOL_RESIZE_CANCEL_FAILED, + "Cancel failed. Already in the buffer pool resize cancel process. "); + ib::warn(ER_REQUEST_CANCEL_BUFFER_POOL_RESIZE) + << "failed. Already in the buffer pool resize cancel process. "; + } else { + ib::info(ER_REQUEST_CANCEL_BUFFER_POOL_RESIZE) << "successed."; + set_state_low(BP_RESIZE_CANCEL); + } + mutex_exit(&m_mutex); +} + #ifndef UNIV_HOTBACKUP /** Frees the buffer pool instances and the global data structures. */ void buf_pool_free_all() { diff --git a/storage/innobase/handler/ha_innodb.cc b/storage/innobase/handler/ha_innodb.cc index 95c59c0e592..b41d9a6e435 100644 --- a/storage/innobase/handler/ha_innodb.cc +++ b/storage/innobase/handler/ha_innodb.cc @@ -86,6 +86,7 @@ this program; if not, write to the Free Software Foundation, Inc., #include "btr0btr.h" #include "btr0cur.h" #include "btr0sea.h" +#include "buf0buf.h" #include "buf0dblwr.h" #include "buf0dump.h" #include "buf0flu.h" @@ -779,7 +780,8 @@ static PSI_mutex_info all_innodb_mutexes[] = { PSI_MUTEX_KEY(sync_array_mutex, 0, 0, PSI_DOCUMENT_ME), PSI_MUTEX_KEY(row_drop_list_mutex, 0, 0, PSI_DOCUMENT_ME), PSI_MUTEX_KEY(ahi_enabled_mutex, 0, 0, - "Mutex used for AHI disabling and enabling.")}; + "Mutex used for AHI disabling and enabling."), + PSI_MUTEX_KEY(buffer_resize_control_mutex, 0, 0, PSI_DOCUMENT_ME)}; #endif /* UNIV_PFS_MUTEX */ #ifdef UNIV_PFS_RWLOCK @@ -20531,6 +20533,20 @@ in status code only if no other resize is in progress */ } } +/** Update the system variables srv_buf_pool_resize_cancel. +This function is registered as a callback with MySQL. +@param[in] thd thread handle +@param[in] var pointer to system variable +@param[out] var_ptr where the formal string goes (ignored) +@param[in] save immediate result from check function */ +static void buffer_pool_resize_cancel_update(THD *thd, SYS_VAR *var, + void *var_ptr, const void *save) { + if (!*(bool *)save) { + return; + } + buf_resize_control->try_set_cancel(thd); +} + /** Update the system variable innobase_deadlock_detect using the "saved" value. Makes sure to "wake up" the dedicated deadlock detector thread if needed. This function is registered as a callback with MySQL. @@ -22345,6 +22361,12 @@ static MYSQL_SYSVAR_ULONG( " idle.", nullptr, nullptr, srv_idle_flush_pct_default, 0, 100, 0); +static MYSQL_SYSVAR_BOOL( + buffer_pool_resize_cancel, srv_buf_pool_resize_cancel, + PLUGIN_VAR_NOCMDARG, + "Trigger the cancel of innodb buffer pool resize.", NULL, + buffer_pool_resize_cancel_update, false); + #ifdef UNIV_DEBUG static MYSQL_SYSVAR_STR(buffer_pool_evict, srv_buffer_pool_evict, PLUGIN_VAR_RQCMDARG, "Evict pages from the buffer pool", @@ -23136,6 +23158,7 @@ static SYS_VAR *innobase_system_variables[] = { MYSQL_SYSVAR(buffer_pool_dump_at_shutdown), MYSQL_SYSVAR(buffer_pool_in_core_file), MYSQL_SYSVAR(buffer_pool_dump_pct), + MYSQL_SYSVAR(buffer_pool_resize_cancel), #ifdef UNIV_DEBUG MYSQL_SYSVAR(buffer_pool_evict), #endif /* UNIV_DEBUG */ diff --git a/storage/innobase/include/buf0buf.h b/storage/innobase/include/buf0buf.h index 6251597b210..d2d5b8f0300 100644 --- a/storage/innobase/include/buf0buf.h +++ b/storage/innobase/include/buf0buf.h @@ -113,6 +113,14 @@ constexpr ulint MAX_PAGE_HASH_LOCKS = 1024; /** The buffer pools of the database */ extern buf_pool_t *buf_pool_ptr; +class Buf_resize_control; +extern Buf_resize_control *buf_resize_control; +extern bool srv_buf_pool_resize_cancel; + +#ifdef UNIV_PFS_MUTEX +extern mysql_pfs_key_t buffer_resize_control_mutex_key; +#endif + #ifdef UNIV_HOTBACKUP /** first block, for --apply-log */ extern buf_block_t *back_block1; @@ -149,6 +157,110 @@ enum buf_page_state : uint8_t { BUF_BLOCK_REMOVE_HASH }; +/** Cancel the buffer pool resizing process. +@param[in] enable_ahi true if need to enable ahi. */ +void buf_pool_resize_cancel(bool enable_ahi); + +/** The resize state. */ +enum resize_state_t : int { + /* No resize */ + BP_RESIZE_NONE = 0, + /* Resize is processing pages, can be canceled only at this stage. */ + BP_RESIZE_PREPARE, + /* Resize has entered the lock phase */ + BP_RESIZE_LOCK, + /* Resize is being canceled */ + BP_RESIZE_CANCEL +}; + +/** Struct used to control resize, used to cancel buffer pool resize process. +Buffer pool resize can be roughly divided into two stages: + 1. prepare stage. + This stage is marked by BP_RESIZE_PREPARE and starts when BP_RESIZE_PREPARE + is set, ends when BP_RESIZE_LOCK is set. In prepare stage, resize can be + blocked by memory page that is held by an active transaction. + + 2. lock stage. + This stage is marked by BP_RESIZE_LOCK and starts when BP_RESIZE_LOCK is + set, ends when BP_RESIZE_NONE is set. In lock stage, the main thing that + resize does is to lock buffer pool and add/delete chunks. This stage can be + completed soon. + +Buffer pool resize can be canceled only at prepare stage. When cancel is needed, +just set global innodb_buffer_pool_resize_cancel variable. When +innodb_buffer_pool_resize_cancel is set, the buffer pool resize cancel process +can be active. For expansion, cancel just reset the state of buffer +pool. For shrink, cancel will move withdrawed page to free page list and reset +the state of buffer pool. */ +class Buf_resize_control { + public: + explicit Buf_resize_control() : m_state(BP_RESIZE_NONE) { + mutex_create(LATCH_ID_BUF_RESIZE_CONTROL_MUTEX, &m_mutex); + } + + ~Buf_resize_control() { mutex_free(&m_mutex); } + + void set_state(resize_state_t state) { + mutex_enter(&m_mutex); + set_state_low(state); + mutex_exit(&m_mutex); + } + + bool is_cancel() { + mutex_enter(&m_mutex); + bool ret = (m_state == BP_RESIZE_CANCEL); + mutex_exit(&m_mutex); + return ret; + } + + /** Try to set cancel state, only success in prepare phase. + @param[in/out] state return current state to write log + @return true if set cancel success, false otherwise. */ + void try_set_cancel(THD *thd); + + /** Do the cancel if state was set. + @param[in] enable_ahi true if need to enable ahi. + @return true if success, false otherwise. */ + bool check_cancel(bool enable_ahi) { + mutex_enter(&m_mutex); + if (m_state != BP_RESIZE_CANCEL) { + mutex_exit(&m_mutex); + return false; + } + + buf_pool_resize_cancel(enable_ahi); + + set_state_low(BP_RESIZE_NONE); + mutex_exit(&m_mutex); + + return true; + } + + private: + void set_state_low(resize_state_t state) { + ut_ad(mutex_own(&m_mutex)); + switch (state) { + case BP_RESIZE_NONE: + ut_ad(m_state == BP_RESIZE_LOCK || m_state == BP_RESIZE_CANCEL); + break; + case BP_RESIZE_PREPARE: + ut_ad(m_state == BP_RESIZE_NONE); + break; + case BP_RESIZE_LOCK: + ut_ad(m_state == BP_RESIZE_PREPARE || m_state == BP_RESIZE_CANCEL); + break; + case BP_RESIZE_CANCEL: + ut_ad(m_state == BP_RESIZE_PREPARE); + break; + } + m_state = state; + } + + private: + ib_mutex_t m_mutex; + resize_state_t m_state; +}; + /** This structure defines information we will fetch from each buffer pool. It will be used to print table IO stats */ struct buf_pool_info_t { diff --git a/storage/innobase/include/buf0types.h b/storage/innobase/include/buf0types.h index 22be70a08a3..576a47e9338 100644 --- a/storage/innobase/include/buf0types.h +++ b/storage/innobase/include/buf0types.h @@ -158,7 +158,10 @@ enum buf_pool_resize_status_code_t { BUF_POOL_RESIZE_HASH = 6, /** Resizing failed */ - BUF_POOL_RESIZE_FAILED = 7 + BUF_POOL_RESIZE_FAILED = 7, + + /** Resizing cancel */ + BUF_POOL_RESIZE_CANCEL = 8 }; inline bool is_checksum_strict(srv_checksum_algorithm_t algo) { diff --git a/storage/innobase/include/sync0types.h b/storage/innobase/include/sync0types.h index 945dd3f61a4..af169c2a2e0 100644 --- a/storage/innobase/include/sync0types.h +++ b/storage/innobase/include/sync0types.h @@ -459,6 +459,7 @@ enum latch_id_t { LATCH_ID_DBLR, LATCH_ID_REDO_LOG_ARCHIVE_ADMIN_MUTEX, LATCH_ID_REDO_LOG_ARCHIVE_QUEUE_MUTEX, + LATCH_ID_BUF_RESIZE_CONTROL_MUTEX, LATCH_ID_TEST_MUTEX, LATCH_ID_MAX = LATCH_ID_TEST_MUTEX }; diff --git a/storage/innobase/include/ut0lst.h b/storage/innobase/include/ut0lst.h index 33ccc894dea..44acf371f53 100644 --- a/storage/innobase/include/ut0lst.h +++ b/storage/innobase/include/ut0lst.h @@ -352,6 +352,43 @@ void ut_list_append(List &list, typename List::elem_type *elem) { @param ELEM the element to add */ #define UT_LIST_ADD_LAST(LIST, ELEM) ut_list_append(LIST, ELEM) +/** Appends a list to another two-way linked list. + @param list list + @param list2 the list to add */ +template +void ut_list_append(List &list, List &list2) { + ut_ad(UT_LIST_IS_INITIALISED(list)); + ut_ad(UT_LIST_IS_INITIALISED(list2)); + ut_ad(&list != &list2); + + if (list2.get_length() > 0) { + typename List::node_type &l2_head_node = + List::get_node(*list2.first_element); + ut_ad(l2_head_node.prev == nullptr); + l2_head_node.prev = list.last_element; + + if (list.last_element != nullptr) { + typename List::node_type &base_node = List::get_node(*list.last_element); + ut_ad(base_node.next == nullptr); + base_node.next = list2.first_element; + } + + list.last_element = list2.last_element; + + if (list.first_element == nullptr) { + list.first_element = list2.first_element; + } + + list.update_length(list2.get_length()); + list2.clear(); + } +} + +/** Appends a list to another two-way linked list. + @param LIST list base node (not a pointer to it) + @param LIST2 list to append */ +#define UT_LIST_APPEND_LIST(LIST, LIST2) ut_list_append(LIST, LIST2) + /** Inserts a ELEM2 after ELEM1 in a list. @param list the base node @param elem1 node after which ELEM2 is inserted diff --git a/storage/innobase/sync/sync0debug.cc b/storage/innobase/sync/sync0debug.cc index 05732514e15..8573c993fa2 100644 --- a/storage/innobase/sync/sync0debug.cc +++ b/storage/innobase/sync/sync0debug.cc @@ -1486,6 +1486,9 @@ static void sync_latch_meta_init() UNIV_NOTHROW { LATCH_ADD_MUTEX(TEST_MUTEX, SYNC_NO_ORDER_CHECK, PFS_NOT_INSTRUMENTED); + LATCH_ADD_MUTEX(BUF_RESIZE_CONTROL_MUTEX, SYNC_NO_ORDER_CHECK, + buffer_resize_control_mutex_key); + latch_id_t id = LATCH_ID_NONE; /* The array should be ordered on latch ID.We need to