mirror of
https://github.com/MariaDB/server.git
synced 2026-10-23 18:49:29 +03:00
MDEV-41398 Stale optimistic btr_pcur_t restore after buffer pool resize
Problem: btr_pcur_t::restore_position() accepts a stored position optimistically when the block at the stored descriptor address has the same page id and the same buf_block_t::modify_clock. When buf_pool_t::resize() shrinks the buffer pool, the withdrawn block descriptors are freed. When it grows the buffer pool again, the new descriptors at the same addresses start with modify_clock=0. A cursor that was stored before the shrink can then find a different instance of the page with an equal modify_clock, and it restores to a record that is no longer correct. In a release build, an UPDATE can change a row that does not match its WHERE condition and skip the rows that match. Fix: Add buf_pool_t::withdrawn_modify_clock. buf_pool_t::shrink() sets it to the maximum modify_clock of the blocks that it withdraws. buf_pool_t::resize() sets the initial modify_clock of each block that it adds to one more than this. A relocation in shrink() increments only the modify_clock of the destination block, so a withdrawn block can keep the value that a cursor stored. Thus the modify_clock at a block descriptor address never repeats an earlier value, and a stale optimistic restore fails. The new member uses a hole before buf_pool_t::flush_list_mutex, so the layout of the other members does not change. The test innodb.buf_pool_resize_pcur_restore reads a page into the first new block after a grow, and makes a stored cursor for a read and for an UPDATE restore its position across a page reorganization, a shrink and a grow.
This commit is contained in:
122
mysql-test/suite/innodb/r/buf_pool_resize_pcur_restore.result
Normal file
122
mysql-test/suite/innodb/r/buf_pool_resize_pcur_restore.result
Normal file
@@ -0,0 +1,122 @@
|
||||
create table t1 (pk int primary key, val varchar(255) not null)
|
||||
engine=innodb stats_persistent=0 charset=latin1;
|
||||
insert into t1 select seq, repeat('a', 100) from seq_1_to_20 order by seq desc;
|
||||
create table t2 like t1;
|
||||
insert into t2 select seq, repeat('a', 100) from seq_1_to_20 order by seq desc;
|
||||
create table t3 (pk int primary key) engine=innodb stats_persistent=0
|
||||
partition by hash(pk) partitions 256;
|
||||
insert into t3 select seq from seq_0_to_255;
|
||||
create table t4 (a int primary key, b varchar(7000)) engine=innodb
|
||||
stats_persistent=0;
|
||||
insert into t4 select seq, repeat('x', 7000) from seq_1_to_3000;
|
||||
create function hold(x int) returns int
|
||||
begin
|
||||
if x = 1 then
|
||||
set debug_sync= 'now SIGNAL paused WAIT_FOR go';
|
||||
end if;
|
||||
return 1;
|
||||
end|
|
||||
create procedure read_into_first_new_block(tbl varchar(64))
|
||||
begin
|
||||
declare attempts int default 5;
|
||||
declare blk bigint default -1;
|
||||
declare p int;
|
||||
set @read_tbl= concat('select count(*) into @n from ', tbl);
|
||||
prepare read_tbl from @read_tbl;
|
||||
while blk != @first_new do
|
||||
if attempts = 0 then
|
||||
signal sqlstate '45000' set message_text= 'Wrong placement';
|
||||
end if;
|
||||
set attempts= attempts - 1;
|
||||
if blk != -1 then
|
||||
set global innodb_buffer_pool_size= @old_size;
|
||||
end if;
|
||||
# Evict all pages of t1, t2 and t3.
|
||||
select sum(length(b)) into @n from t4;
|
||||
if @first_new = 0 then
|
||||
set @first_new= (select pool_size
|
||||
from information_schema.innodb_buffer_pool_stats);
|
||||
end if;
|
||||
set global innodb_buffer_pool_size= @new_size;
|
||||
# A grow appends the new blocks to the end of the free list. Read pages
|
||||
# of t3 until only the new blocks are free.
|
||||
set p= 0;
|
||||
while (select free_buffers > pool_size - @first_new
|
||||
from information_schema.innodb_buffer_pool_stats) do
|
||||
if p = 256 then
|
||||
signal sqlstate '45000' set message_text= 'Out of partitions';
|
||||
end if;
|
||||
select pk into @n from t3 where pk= p;
|
||||
set p= p + 1;
|
||||
end while;
|
||||
execute read_tbl;
|
||||
select block_id into blk from information_schema.innodb_buffer_page
|
||||
where table_name = concat('`test`.`', tbl, '`');
|
||||
end while;
|
||||
deallocate prepare read_tbl;
|
||||
end|
|
||||
set global innodb_max_purge_lag_wait= 0;
|
||||
set @old_size= @@global.innodb_buffer_pool_size;
|
||||
set @new_size= 12582912;
|
||||
set @first_new= 0;
|
||||
set @save_scan_depth= @@global.innodb_lru_scan_depth;
|
||||
set global innodb_lru_scan_depth= 100;
|
||||
#
|
||||
# A read that continues from a stale position
|
||||
#
|
||||
# Read the page of t1 into the first new block.
|
||||
call read_into_first_new_block('t1');
|
||||
connect con1,localhost,root;
|
||||
select group_concat(pk) from t1 where hold(pk);
|
||||
connection default;
|
||||
set debug_sync= 'now WAIT_FOR paused';
|
||||
# Reorganize the page while the read holds a stored position on pk=1.
|
||||
set @save_dbug= @@debug_dbug;
|
||||
set debug_dbug= '+d,do_page_reorganize';
|
||||
insert into t1 values (21, repeat('a', 100));
|
||||
set debug_dbug= @save_dbug;
|
||||
# Shrink, which moves the page out of the block.
|
||||
set global innodb_buffer_pool_size= @old_size;
|
||||
# Read the page of t1 into the same block again.
|
||||
call read_into_first_new_block('t1');
|
||||
# Resume the read, which must return pk=2 to pk=20.
|
||||
set debug_sync= 'now SIGNAL go';
|
||||
connection con1;
|
||||
group_concat(pk)
|
||||
1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20
|
||||
connection default;
|
||||
set global innodb_buffer_pool_size= @old_size;
|
||||
set global innodb_max_purge_lag_wait= 0;
|
||||
#
|
||||
# An UPDATE that continues from a stale position
|
||||
#
|
||||
# Read the page of t2 into the first new block.
|
||||
call read_into_first_new_block('t2');
|
||||
connection con1;
|
||||
update t2 set val= 'b' where hold(pk);
|
||||
connection default;
|
||||
set debug_sync= 'now WAIT_FOR paused';
|
||||
# Reorganize the page while the UPDATE holds a stored position on pk=1.
|
||||
set debug_dbug= '+d,do_page_reorganize';
|
||||
insert into t2 values (21, repeat('a', 100));
|
||||
set debug_dbug= @save_dbug;
|
||||
# Shrink, which moves the page out of the block.
|
||||
set global innodb_buffer_pool_size= @old_size;
|
||||
# Read the page of t2 into the same block again.
|
||||
call read_into_first_new_block('t2');
|
||||
# Resume the UPDATE, which must change pk=1, not pk=20.
|
||||
set debug_sync= 'now SIGNAL go';
|
||||
connection con1;
|
||||
affected rows: 21
|
||||
info: Rows matched: 21 Changed: 21 Warnings: 0
|
||||
disconnect con1;
|
||||
connection default;
|
||||
select group_concat(pk) from t2 where val = 'b';
|
||||
group_concat(pk)
|
||||
1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21
|
||||
set debug_sync= 'reset';
|
||||
drop procedure read_into_first_new_block;
|
||||
drop function hold;
|
||||
drop table t1, t2, t3, t4;
|
||||
set global innodb_buffer_pool_size= @old_size;
|
||||
set global innodb_lru_scan_depth= @save_scan_depth;
|
||||
@@ -0,0 +1,3 @@
|
||||
--innodb-buffer-pool-size=8M
|
||||
--innodb-buffer-pool-size-max=16M
|
||||
--innodb-read-ahead-threshold=0
|
||||
173
mysql-test/suite/innodb/t/buf_pool_resize_pcur_restore.test
Normal file
173
mysql-test/suite/innodb/t/buf_pool_resize_pcur_restore.test
Normal file
@@ -0,0 +1,173 @@
|
||||
#
|
||||
# A persistent cursor that is restored after the buffer pool shrank and
|
||||
# grew again
|
||||
#
|
||||
# btr_pcur_t::restore_position() accepts the stored position without a
|
||||
# search when the block descriptor that it remembers holds the same page
|
||||
# id and the same modify_clock. The descriptors are at fixed addresses,
|
||||
# and a grow initializes the descriptors that an earlier shrink withdrew.
|
||||
# If that reset modify_clock, a page that is read into such a descriptor
|
||||
# after the grow could have the modify_clock that a cursor stored before
|
||||
# the shrink, when the page had been read into the same descriptor after
|
||||
# an earlier grow. The page can have changed in between, and the cursor
|
||||
# would continue from a stale record pointer.
|
||||
#
|
||||
--source include/have_innodb.inc
|
||||
--source include/have_innodb_16k.inc
|
||||
--source include/have_partition.inc
|
||||
--source include/have_sequence.inc
|
||||
--source include/have_debug.inc
|
||||
--source include/have_debug_sync.inc
|
||||
--source include/not_embedded.inc
|
||||
--source include/count_sessions.inc
|
||||
|
||||
# One leaf page each, with the records in descending key order in the page.
|
||||
# A reorganization writes them in key order, so pk=20 moves to the offset
|
||||
# of pk=1.
|
||||
create table t1 (pk int primary key, val varchar(255) not null)
|
||||
engine=innodb stats_persistent=0 charset=latin1;
|
||||
insert into t1 select seq, repeat('a', 100) from seq_1_to_20 order by seq desc;
|
||||
create table t2 like t1;
|
||||
insert into t2 select seq, repeat('a', 100) from seq_1_to_20 order by seq desc;
|
||||
|
||||
# One page per partition. A lookup of an evicted partition reads one page.
|
||||
# The page cleaner keeps about innodb_lru_scan_depth blocks free, and the
|
||||
# partitions must outnumber them.
|
||||
create table t3 (pk int primary key) engine=innodb stats_persistent=0
|
||||
partition by hash(pk) partitions 256;
|
||||
insert into t3 select seq from seq_0_to_255;
|
||||
|
||||
# Three times innodb_buffer_pool_size, to evict everything else.
|
||||
create table t4 (a int primary key, b varchar(7000)) engine=innodb
|
||||
stats_persistent=0;
|
||||
insert into t4 select seq, repeat('x', 7000) from seq_1_to_3000;
|
||||
|
||||
delimiter |;
|
||||
# Stop the statement after it read pk=1 and stored its position.
|
||||
create function hold(x int) returns int
|
||||
begin
|
||||
if x = 1 then
|
||||
set debug_sync= 'now SIGNAL paused WAIT_FOR go';
|
||||
end if;
|
||||
return 1;
|
||||
end|
|
||||
|
||||
# Grow the buffer pool from @old_size to @new_size and read the only page
|
||||
# of the table tbl into the first block that the grow adds. A concurrent
|
||||
# page read can take that block; then shrink and try again. The first call
|
||||
# sets @first_new, the block id of the first new block. Later calls require
|
||||
# the same @first_new.
|
||||
create procedure read_into_first_new_block(tbl varchar(64))
|
||||
begin
|
||||
declare attempts int default 5;
|
||||
declare blk bigint default -1;
|
||||
declare p int;
|
||||
set @read_tbl= concat('select count(*) into @n from ', tbl);
|
||||
prepare read_tbl from @read_tbl;
|
||||
while blk != @first_new do
|
||||
if attempts = 0 then
|
||||
signal sqlstate '45000' set message_text= 'Wrong placement';
|
||||
end if;
|
||||
set attempts= attempts - 1;
|
||||
if blk != -1 then
|
||||
set global innodb_buffer_pool_size= @old_size;
|
||||
end if;
|
||||
# Evict all pages of t1, t2 and t3.
|
||||
select sum(length(b)) into @n from t4;
|
||||
if @first_new = 0 then
|
||||
set @first_new= (select pool_size
|
||||
from information_schema.innodb_buffer_pool_stats);
|
||||
end if;
|
||||
set global innodb_buffer_pool_size= @new_size;
|
||||
# A grow appends the new blocks to the end of the free list. Read pages
|
||||
# of t3 until only the new blocks are free.
|
||||
set p= 0;
|
||||
while (select free_buffers > pool_size - @first_new
|
||||
from information_schema.innodb_buffer_pool_stats) do
|
||||
if p = 256 then
|
||||
signal sqlstate '45000' set message_text= 'Out of partitions';
|
||||
end if;
|
||||
select pk into @n from t3 where pk= p;
|
||||
set p= p + 1;
|
||||
end while;
|
||||
execute read_tbl;
|
||||
select block_id into blk from information_schema.innodb_buffer_page
|
||||
where table_name = concat('`test`.`', tbl, '`');
|
||||
end while;
|
||||
deallocate prepare read_tbl;
|
||||
end|
|
||||
delimiter ;|
|
||||
|
||||
# Purge resets DB_TRX_ID in the inserted records. Let it finish, so that it
|
||||
# reads no pages into the free blocks that the procedure counts.
|
||||
set global innodb_max_purge_lag_wait= 0;
|
||||
|
||||
set @old_size= @@global.innodb_buffer_pool_size;
|
||||
set @new_size= 12582912;
|
||||
set @first_new= 0;
|
||||
# The minimum, which keeps fewer blocks free than t3 has partitions.
|
||||
set @save_scan_depth= @@global.innodb_lru_scan_depth;
|
||||
set global innodb_lru_scan_depth= 100;
|
||||
|
||||
--echo #
|
||||
--echo # A read that continues from a stale position
|
||||
--echo #
|
||||
--echo # Read the page of t1 into the first new block.
|
||||
call read_into_first_new_block('t1');
|
||||
connect (con1,localhost,root);
|
||||
--send select group_concat(pk) from t1 where hold(pk)
|
||||
connection default;
|
||||
set debug_sync= 'now WAIT_FOR paused';
|
||||
--echo # Reorganize the page while the read holds a stored position on pk=1.
|
||||
set @save_dbug= @@debug_dbug;
|
||||
set debug_dbug= '+d,do_page_reorganize';
|
||||
insert into t1 values (21, repeat('a', 100));
|
||||
set debug_dbug= @save_dbug;
|
||||
--echo # Shrink, which moves the page out of the block.
|
||||
set global innodb_buffer_pool_size= @old_size;
|
||||
--echo # Read the page of t1 into the same block again.
|
||||
call read_into_first_new_block('t1');
|
||||
--echo # Resume the read, which must return pk=2 to pk=20.
|
||||
set debug_sync= 'now SIGNAL go';
|
||||
connection con1;
|
||||
--reap
|
||||
connection default;
|
||||
set global innodb_buffer_pool_size= @old_size;
|
||||
# Purge was blocked by the read view of the paused read. Let it finish, so
|
||||
# that it reads no pages into the new blocks of the next case.
|
||||
set global innodb_max_purge_lag_wait= 0;
|
||||
|
||||
--echo #
|
||||
--echo # An UPDATE that continues from a stale position
|
||||
--echo #
|
||||
--echo # Read the page of t2 into the first new block.
|
||||
call read_into_first_new_block('t2');
|
||||
connection con1;
|
||||
--send update t2 set val= 'b' where hold(pk)
|
||||
connection default;
|
||||
set debug_sync= 'now WAIT_FOR paused';
|
||||
--echo # Reorganize the page while the UPDATE holds a stored position on pk=1.
|
||||
set debug_dbug= '+d,do_page_reorganize';
|
||||
insert into t2 values (21, repeat('a', 100));
|
||||
set debug_dbug= @save_dbug;
|
||||
--echo # Shrink, which moves the page out of the block.
|
||||
set global innodb_buffer_pool_size= @old_size;
|
||||
--echo # Read the page of t2 into the same block again.
|
||||
call read_into_first_new_block('t2');
|
||||
--echo # Resume the UPDATE, which must change pk=1, not pk=20.
|
||||
set debug_sync= 'now SIGNAL go';
|
||||
connection con1;
|
||||
--enable_info
|
||||
--reap
|
||||
--disable_info
|
||||
disconnect con1;
|
||||
connection default;
|
||||
select group_concat(pk) from t2 where val = 'b';
|
||||
|
||||
set debug_sync= 'reset';
|
||||
drop procedure read_into_first_new_block;
|
||||
drop function hold;
|
||||
drop table t1, t2, t3, t4;
|
||||
set global innodb_buffer_pool_size= @old_size;
|
||||
set global innodb_lru_scan_depth= @save_scan_depth;
|
||||
--source include/wait_until_count_sessions.inc
|
||||
@@ -1661,12 +1661,15 @@ ATTRIBUTE_COLD buf_pool_t::shrink_status buf_pool_t::shrink(size_t size)
|
||||
{
|
||||
withdraw_done:
|
||||
first_to_withdraw= nullptr;
|
||||
uint64_t base{0};
|
||||
while (buf_page_t *b= UT_LIST_GET_FIRST(withdrawn))
|
||||
{
|
||||
UT_LIST_REMOVE(withdrawn, b);
|
||||
base= std::max(base, reinterpret_cast<buf_block_t*>(b)->modify_clock);
|
||||
/* satisfy the check in lazy_allocate() */
|
||||
ut_d(memset((void*) b, 0, sizeof(buf_block_t)));
|
||||
}
|
||||
withdrawn_modify_clock= std::max(withdrawn_modify_clock, base);
|
||||
return SHRINK_DONE;
|
||||
}
|
||||
|
||||
@@ -1976,6 +1979,7 @@ ATTRIBUTE_COLD void buf_pool_t::resize(size_t size, THD *thd) noexcept
|
||||
const size_t ssize= srv_page_size_shift - UNIV_PAGE_SIZE_SHIFT_MIN;
|
||||
const size_t pages= pages_in_extent[ssize];
|
||||
const size_t first_extent= n_blocks / pages;
|
||||
const uint64_t modify_clock= withdrawn_modify_clock + 1;
|
||||
|
||||
char *extent= memory + first_extent * innodb_buffer_pool_extent_size;
|
||||
|
||||
@@ -1997,6 +2001,7 @@ ATTRIBUTE_COLD void buf_pool_t::resize(size_t size, THD *thd) noexcept
|
||||
{
|
||||
block->page.frame= frame;
|
||||
block->page.lock.init();
|
||||
block->modify_clock= modify_clock;
|
||||
UT_LIST_ADD_LAST(free, &block->page);
|
||||
ut_d(block->page.in_free_list= true);
|
||||
}
|
||||
@@ -2020,6 +2025,7 @@ ATTRIBUTE_COLD void buf_pool_t::resize(size_t size, THD *thd) noexcept
|
||||
{
|
||||
block->page.frame= frame;
|
||||
block->page.lock.init();
|
||||
block->modify_clock= modify_clock;
|
||||
UT_LIST_ADD_LAST(free, &block->page);
|
||||
ut_d(block->page.in_free_list= true);
|
||||
}
|
||||
|
||||
@@ -1715,6 +1715,13 @@ public:
|
||||
|
||||
/** number of index page splits */
|
||||
Atomic_counter<ulint> pages_split;
|
||||
/** the maximum modify_clock of any block that shrink() withdrew;
|
||||
resize() initializes the modify_clock of the blocks that it adds to
|
||||
one more than this, so that the modify_clock at a block descriptor
|
||||
address never repeats an earlier value and a stale
|
||||
btr_pcur_t::restore_position() cannot succeed optimistically;
|
||||
protected by mutex */
|
||||
uint64_t withdrawn_modify_clock;
|
||||
|
||||
/** @name Page flushing algorithm fields */
|
||||
/* @{ */
|
||||
|
||||
Reference in New Issue
Block a user