Skip to content

Commit

Permalink
MDEV-30671 InnoDB undo log truncation fails to wait for purge of history
Browse files Browse the repository at this point in the history
It is not safe to invoke trx_purge_free_segment() or execute
innodb_undo_log_truncate=ON before all undo log records in
the rollback segment has been processed.

A prominent failure that would occur due to premature freeing of
undo log pages is that trx_undo_get_undo_rec() would crash when
trying to copy an undo log record to fetch the previous version
of a record.

If trx_undo_get_undo_rec() was not invoked in the unlucky time frame,
then the symptom would be that some committed transaction history is
never removed. This would be detected by CHECK TABLE...EXTENDED that
was impleented in commit ab01901.
Such a garbage collection leak should be possible even when using
innodb_undo_log_truncate=OFF, just involving trx_purge_free_segment().

trx_rseg_t::needs_purge: Change the type from Boolean to a transaction
identifier, noting the most recent non-purged transaction, or 0 if
everything has been purged. On transaction start, we initialize this
to 1 more than the transaction start ID. On recovery, the field may be
adjusted to the transaction end ID (TRX_UNDO_TRX_NO) if it is larger.

The field TRX_UNDO_NEEDS_PURGE becomes write-only; only some debug
assertions that would validate the value. The field reflects the old
inaccurate Boolean field trx_rseg_t::needs_purge.

trx_undo_mem_create_at_db_start(), trx_undo_lists_init(),
trx_rseg_mem_restore(): Remove the parameter max_trx_id.
Instead, store the maximum in trx_rseg_t::needs_purge,
where trx_rseg_array_init() will find it.

trx_purge_free_segment(): Contiguously hold a lock on
trx_rseg_t to prevent any concurrent allocation of undo log.

trx_purge_truncate_rseg_history(): Only invoke trx_purge_free_segment()
if the rollback segment is empty and there are no pending transactions
associated with it.

trx_purge_truncate_history(): Only proceed with innodb_undo_log_truncate=ON
if trx_rseg_t::needs_purge indicates that all history has been purged.

Tested by: Matthias Leich
  • Loading branch information
dr-m committed Feb 24, 2023
1 parent d3f35aa commit 0de3be8
Show file tree
Hide file tree
Showing 16 changed files with 206 additions and 200 deletions.
7 changes: 6 additions & 1 deletion mysql-test/suite/gcol/r/gcol_purge.result
@@ -1,7 +1,11 @@
SET @save_frequency=@@GLOBAL.innodb_purge_rseg_truncate_frequency;
SET @save_dbug=@@GLOBAL.debug_dbug;
SET GLOBAL innodb_purge_rseg_truncate_frequency=1;
CREATE TABLE t1(f1 INT NOT NULL, f2 int not null,
f3 int generated always as (f2 * 2) VIRTUAL,
primary key(f1), INDEX (f3))ENGINE=InnoDB;
connect con1,localhost,root,,,;
InnoDB 0 transactions not purged
START TRANSACTION WITH CONSISTENT SNAPSHOT;
connection default;
INSERT INTO t1(f1, f2) VALUES(1,2);
Expand All @@ -18,5 +22,6 @@ commit;
disconnect con1;
disconnect con2;
connection default;
set global debug_dbug=default;
SET GLOBAL innodb_purge_rseg_truncate_frequency=@save_frequency;
SET GLOBAL debug_dbug=@save_dbug;
DROP TABLE t1;
8 changes: 7 additions & 1 deletion mysql-test/suite/gcol/t/gcol_purge.test
@@ -1,9 +1,14 @@
--source include/have_innodb.inc
--source include/have_debug.inc

SET @save_frequency=@@GLOBAL.innodb_purge_rseg_truncate_frequency;
SET @save_dbug=@@GLOBAL.debug_dbug;
SET GLOBAL innodb_purge_rseg_truncate_frequency=1;
CREATE TABLE t1(f1 INT NOT NULL, f2 int not null,
f3 int generated always as (f2 * 2) VIRTUAL,
primary key(f1), INDEX (f3))ENGINE=InnoDB;
connect(con1,localhost,root,,,);
--source ../innodb/include/wait_all_purged.inc
START TRANSACTION WITH CONSISTENT SNAPSHOT;

connection default;
Expand All @@ -26,5 +31,6 @@ commit;
disconnect con1;
disconnect con2;
connection default;
set global debug_dbug=default;
SET GLOBAL innodb_purge_rseg_truncate_frequency=@save_frequency;
SET GLOBAL debug_dbug=@save_dbug;
DROP TABLE t1;
4 changes: 4 additions & 0 deletions mysql-test/suite/innodb/r/cursor-restore-locking.result
@@ -1,4 +1,7 @@
SET @save_freq=@@GLOBAL.innodb_purge_rseg_truncate_frequency;
SET GLOBAL innodb_purge_rseg_truncate_frequency=1;
CREATE TABLE t (a int PRIMARY KEY, b int NOT NULL UNIQUE) engine = InnoDB;
InnoDB 0 transactions not purged
connect prevent_purge,localhost,root,,;
start transaction with consistent snapshot;
connect con_del_1,localhost,root,,;
Expand Down Expand Up @@ -34,3 +37,4 @@ disconnect con_del_2;
connection default;
SET DEBUG_SYNC = 'RESET';
DROP TABLE t;
SET GLOBAL innodb_purge_rseg_truncate_frequency=@save_freq;
5 changes: 5 additions & 0 deletions mysql-test/suite/innodb/r/dml_purge.result
Expand Up @@ -7,6 +7,7 @@ SET GLOBAL innodb_purge_rseg_truncate_frequency = 1;
SET GLOBAL innodb_purge_rseg_truncate_frequency = 1;
CREATE TABLE t1(a INT PRIMARY KEY, b INT NOT NULL)
ROW_FORMAT=REDUNDANT ENGINE=InnoDB;
InnoDB 0 transactions not purged
connect prevent_purge,localhost,root;
START TRANSACTION WITH CONSISTENT SNAPSHOT;
connection default;
Expand All @@ -19,7 +20,11 @@ UPDATE t1 SET b=4 WHERE a=3;
disconnect prevent_purge;
connection default;
InnoDB 0 transactions not purged
connection con1;
ROLLBACK;
disconnect con1;
connection default;
InnoDB 0 transactions not purged
FLUSH TABLE t1 FOR EXPORT;
Clustered index root page contents:
N_RECS=3; LEVEL=0
Expand Down
1 change: 1 addition & 0 deletions mysql-test/suite/innodb/r/gap_lock_split.result
Expand Up @@ -3,6 +3,7 @@ SET GLOBAL innodb_purge_rseg_truncate_frequency=1;
CREATE TABLE t1(id INT PRIMARY key, val VARCHAR(16000)) ENGINE=InnoDB;
INSERT INTO t1 (id,val) SELECT 2*seq,'x' FROM seq_0_to_1023;
connect con1,localhost,root,,;
InnoDB 0 transactions not purged
START TRANSACTION WITH CONSISTENT SNAPSHOT;
connection default;
DELETE FROM t1 WHERE id=1788;
Expand Down
11 changes: 7 additions & 4 deletions mysql-test/suite/innodb/r/innodb_bug84958.result
Expand Up @@ -9,12 +9,10 @@ SET GLOBAL innodb_purge_rseg_truncate_frequency= 1;
CREATE PROCEDURE insert_n(start int, end int)
BEGIN
DECLARE i INT DEFAULT start;
START TRANSACTION;
WHILE i <= end do
INSERT INTO t1 VALUES (1, 2, 3) ON DUPLICATE KEY UPDATE c = i;
SET i = i + 1;
END WHILE;
COMMIT;
END~~
CREATE FUNCTION num_pages_get()
RETURNS INT
Expand All @@ -29,7 +27,8 @@ END~~
# Create a table with one record in it and start an RR transaction
#
CREATE TABLE t1 (a INT, b INT, c INT, PRIMARY KEY(a,b), KEY (b,c))
ENGINE=InnoDB;
ENGINE=InnoDB STATS_PERSISTENT=0;
InnoDB 0 transactions not purged
BEGIN;
SELECT * FROM t1;
a b c
Expand All @@ -38,20 +37,24 @@ a b c
#
connect con2, localhost, root,,;
connection con2;
BEGIN;
INSERT INTO t1 VALUES (1, 2, 3) ON DUPLICATE KEY UPDATE c = NULL;
CALL insert_n(1, 50);;
connect con3, localhost, root,,;
connection con3;
BEGIN;
CALL insert_n(51, 100);;
connection con2;
COMMIT;
connection con3;
INSERT INTO t1 VALUES (1, 2, 1) ON DUPLICATE KEY UPDATE c = NULL;
COMMIT;
connection default;
#
# Connect to default and record how many pages were accessed
# when selecting the record using the secondary key.
#
InnoDB 4 transactions not purged
InnoDB 2 transactions not purged
SET @num_pages_1 = num_pages_get();
SELECT * FROM t1 force index (b);
a b c
Expand Down
4 changes: 4 additions & 0 deletions mysql-test/suite/innodb/t/cursor-restore-locking.test
Expand Up @@ -3,8 +3,11 @@
source include/have_debug.inc;
source include/have_debug_sync.inc;

SET @save_freq=@@GLOBAL.innodb_purge_rseg_truncate_frequency;
SET GLOBAL innodb_purge_rseg_truncate_frequency=1;
CREATE TABLE t (a int PRIMARY KEY, b int NOT NULL UNIQUE) engine = InnoDB;

--source include/wait_all_purged.inc
--connect(prevent_purge,localhost,root,,)
start transaction with consistent snapshot;

Expand Down Expand Up @@ -80,4 +83,5 @@ INSERT INTO t VALUES(30, 20);

SET DEBUG_SYNC = 'RESET';
DROP TABLE t;
SET GLOBAL innodb_purge_rseg_truncate_frequency=@save_freq;
--source include/wait_until_count_sessions.inc
6 changes: 6 additions & 0 deletions mysql-test/suite/innodb/t/dml_purge.test
Expand Up @@ -14,6 +14,7 @@ SET GLOBAL innodb_purge_rseg_truncate_frequency = 1;

CREATE TABLE t1(a INT PRIMARY KEY, b INT NOT NULL)
ROW_FORMAT=REDUNDANT ENGINE=InnoDB;
--source include/wait_all_purged.inc

--connect (prevent_purge,localhost,root)
START TRANSACTION WITH CONSISTENT SNAPSHOT;
Expand All @@ -33,7 +34,12 @@ UPDATE t1 SET b=4 WHERE a=3;
# Initiate a full purge, which should reset the DB_TRX_ID except for a=3.
--source include/wait_all_purged.inc
# Initiate a ROLLBACK of the update, which should reset the DB_TRX_ID for a=3.
--connection con1
ROLLBACK;
--disconnect con1
--connection default
# Reset the DB_TRX_ID for the hidden ADD COLUMN metadata record.
--source include/wait_all_purged.inc

FLUSH TABLE t1 FOR EXPORT;
# The following is based on innodb.table_flags:
Expand Down
1 change: 1 addition & 0 deletions mysql-test/suite/innodb/t/gap_lock_split.test
Expand Up @@ -9,6 +9,7 @@ CREATE TABLE t1(id INT PRIMARY key, val VARCHAR(16000)) ENGINE=InnoDB;
INSERT INTO t1 (id,val) SELECT 2*seq,'x' FROM seq_0_to_1023;

connect(con1,localhost,root,,);
source include/wait_all_purged.inc;
# Prevent purge.
START TRANSACTION WITH CONSISTENT SNAPSHOT;
connection default;
Expand Down
11 changes: 7 additions & 4 deletions mysql-test/suite/innodb/t/innodb_bug84958.test
Expand Up @@ -13,12 +13,10 @@ DELIMITER ~~;
CREATE PROCEDURE insert_n(start int, end int)
BEGIN
DECLARE i INT DEFAULT start;
START TRANSACTION;
WHILE i <= end do
INSERT INTO t1 VALUES (1, 2, 3) ON DUPLICATE KEY UPDATE c = i;
SET i = i + 1;
END WHILE;
COMMIT;
END~~

CREATE FUNCTION num_pages_get()
Expand All @@ -36,7 +34,8 @@ DELIMITER ;~~
--echo # Create a table with one record in it and start an RR transaction
--echo #
CREATE TABLE t1 (a INT, b INT, c INT, PRIMARY KEY(a,b), KEY (b,c))
ENGINE=InnoDB;
ENGINE=InnoDB STATS_PERSISTENT=0;
--source include/wait_all_purged.inc
BEGIN;
SELECT * FROM t1;

Expand All @@ -45,26 +44,30 @@ SELECT * FROM t1;
--echo #
connect (con2, localhost, root,,);
connection con2;
BEGIN;
INSERT INTO t1 VALUES (1, 2, 3) ON DUPLICATE KEY UPDATE c = NULL;
--send CALL insert_n(1, 50);

connect (con3, localhost, root,,);
connection con3;
BEGIN;
--send CALL insert_n(51, 100);

connection con2;
reap;
COMMIT;
connection con3;
reap;
INSERT INTO t1 VALUES (1, 2, 1) ON DUPLICATE KEY UPDATE c = NULL;
COMMIT;

connection default;

--echo #
--echo # Connect to default and record how many pages were accessed
--echo # when selecting the record using the secondary key.
--echo #
--let $wait_all_purged=4
--let $wait_all_purged=2
--source include/wait_all_purged.inc
SET @num_pages_1 = num_pages_get();
SELECT * FROM t1 force index (b);
Expand Down
22 changes: 13 additions & 9 deletions storage/innobase/include/trx0rseg.h
Expand Up @@ -128,15 +128,19 @@ struct trx_rseg_t {
/** trx_t::no | last_offset << 48 */
uint64_t last_commit_and_offset;

/** Whether the log segment needs purge */
bool needs_purge;

/** Reference counter to track rseg allocated transactions. */
ulint trx_ref_count;

/** If true, then skip allocating this rseg as it reside in
UNDO-tablespace marked for truncate. */
bool skip_allocation;
/** Last known transaction that has not been purged yet,
or 0 if everything has been purged. */
trx_id_t needs_purge;

/** Number of active (non-committed) transactions associated with a
an is_persistent() rollback segment. Needed for protecting
trx->rsegs.m_redo.rseg assignments
before trx->rsegs.m_redo.undo has been assigned. */
ulint trx_ref_count;

/** whether undo log truncation was initiated, and transactions
cannot be allocated in this is_persistent() rollback segment */
bool skip_allocation;

/** @return the commit ID of the last committed transaction */
trx_id_t last_trx_no() const
Expand Down
6 changes: 3 additions & 3 deletions storage/innobase/include/trx0undo.h
Expand Up @@ -258,12 +258,10 @@ trx_undo_free_at_shutdown(trx_t *trx);
@param[in,out] rseg rollback segment
@param[in] id rollback segment slot
@param[in] page_no undo log segment page number
@param[in,out] max_trx_id the largest observed transaction ID
@return the undo log
@retval nullptr on error */
trx_undo_t *
trx_undo_mem_create_at_db_start(trx_rseg_t *rseg, ulint id, uint32_t page_no,
trx_id_t &max_trx_id);
trx_undo_mem_create_at_db_start(trx_rseg_t *rseg, ulint id, uint32_t page_no);

#endif /* !UNIV_INNOCHECKSUM */

Expand Down Expand Up @@ -407,6 +405,8 @@ or 0 if the transaction has not been committed */
/** Before MariaDB 10.3.1, when purge did not reset DB_TRX_ID of
surviving user records, this used to be called TRX_UNDO_DEL_MARKS.
This field is redundant; it is only being read by some debug assertions.
The value 1 indicates that purge needs to process the undo log segment.
The value 0 indicates that all of it has been processed, and
trx_purge_free_segment() has been invoked, so the log is not safe to access.
Expand Down

0 comments on commit 0de3be8

Please sign in to comment.