Skip to content

Commit

Permalink
MDEV-24402: InnoDB CHECK TABLE ... EXTENDED
Browse files Browse the repository at this point in the history
Until now, the attribute EXTENDED of CHECK TABLE was ignored by InnoDB,
and InnoDB only counted the records in each index according
to the current read view. Unless the attribute QUICK was specified, the
function btr_validate_index() would be invoked to validate the B-tree
structure (the sibling and child links between index pages).

The EXTENDED check will not only count all index records according to the
current read view, but also ensure that any delete-marked records in the
clustered index are waiting for the purge of history, and that all
secondary index records point to a version of the clustered index record
that is waiting for the purge of history. In other words, no index may
contain orphan records. Normal MVCC reads and the non-EXTENDED version
of CHECK TABLE would ignore these orphans.

Unpurged records merely result in warnings (at most one per index),
not errors, and no indexes will be flagged as corrupted due to such
garbage. It will remain possible to SELECT data from such indexes or
tables (which will skip such records) or to rebuild the table to
reclaim some space.

We introduce purge_sys.end_view that will be (almost) a copy of
purge_sys.view at the end of a batch of purging committed transaction
history. It is not an exact copy, because if the size of a purge batch
is limited by innodb_purge_batch_size, some records that
purge_sys.view would allow to be purged will be left over for
subsequent batches.

The purge_sys.view is relevant in the purge of committed transaction
history, to determine if records are safe to remove. The new
purge_sys.end_view is relevant in MVCC operations and in
CHECK TABLE ... EXTENDED. It tells which undo log records are
safe to access (have not been discarded at the end of a purge batch).

purge_sys.clone_oldest_view<true>(): In trx_lists_init_at_db_start(),
clone the oldest read view similar to purge_sys_t::clone_end_view()
so that CHECK TABLE ... EXTENDED will not report bogus failures between
InnoDB restart and the completed purge of committed transaction history.

purge_sys_t::is_purgeable(): Replaces purge_sys_t::changes_visible()
in the case that purge_sys.latch will not be held by the caller.
Among other things, this guards access to BLOBs. It is not safe to
dereference any BLOBs of a delete-marked purgeable record, because
they may have already been freed.

purge_sys_t::view_guard::view(): Return a reference to purge_sys.view
that will be protected by purge_sys.latch, held by purge_sys_t::view_guard.

purge_sys_t::end_view_guard::view(): Return a reference to
purge_sys.end_view while it is protected by purge_sys.end_latch.
Whenever a thread needs to retrieve an older version of a clustered
index record, it will hold a page latch on the clustered index page
and potentially also on a secondary index page that points to the
clustered index page. If these pages contain purgeable records that
would be accessed by a currently running purge batch, the progress of
the purge batch would be blocked by the page latches. Hence, it is
safe to make a copy of purge_sys.end_view while holding an index page
latch, and consult the copy of the view to determine whether a record
should already have been purged.

btr_validate_index(): Remove a redundant check.

row_check_index_match(): Check if a secondary index record and a
version of a clustered index record match each other.

row_check_index(): Replaces row_scan_index_for_mysql().
Count the records in each index directly, duplicating the relevant
logic from row_search_mvcc(). Initialize check_table_extended_view
for CHECK ... EXTENDED while holding an index leaf page latch.
If we encounter an orphan record, the copy of purge_sys.end_view that
we make is safe for visibility checks, and trx_undo_get_undo_rec() will
check for the safety to access each undo log record. Should that check
fail, we should return DB_MISSING_HISTORY to report a corrupted index.
The EXTENDED check tries to match each secondary index record with
every available clustered index record version, by duplicating the logic
of row_vers_build_for_consistent_read() and invoking
trx_undo_prev_version_build() directly.

Before invoking row_check_index_match() on delete-marked clustered index
record versions, we will consult purge_sys.is_purgeable() in order to
avoid accessing freed BLOBs.

We will always check that the DB_TRX_ID or PAGE_MAX_TRX_ID does not
exceed the global maximum. Orphan secondary index records will be
flagged only if everything up to PAGE_MAX_TRX_ID has been purged.
We warn also about clustered index records whose nonzero DB_TRX_ID
should have been reset in purge or rollback.

trx_set_rw_mode(): Move an assertion from ReadView::set_creator_trx_id().

trx_undo_prev_version_build(): Remove two debug-only parameters,
and return an error code instead of a Boolean.

trx_undo_get_undo_rec(): Return a pointer to the undo log record,
or nullptr if one cannot be retrieved. Instead of consulting the
purge_sys.view, consult the purge_sys.end_view to determine which
records can be accessed.

trx_undo_get_rec_if_purgeable(): A variant of trx_undo_get_undo_rec()
that will consult purge_sys.view instead of purge_sys.end_view.

TRX_UNDO_CHECK_PURGEABILITY: A new parameter to
trx_undo_prev_version_build(), passed by row_vers_old_has_index_entry()
so that purge_sys.view instead of purge_sys.end_view will be consulted
to determine whether a secondary index record may be safely purged.

row_upd_changes_disowned_external(): Remove. This should be more
expensive than briefly latching purge_sys in trx_undo_prev_version_build()
(which may make use of transactional memory).

row_sel_reset_old_vers_heap(): New function, split from
row_sel_build_prev_vers_for_mysql().

row_sel_build_prev_vers_for_mysql(): Reorder some parameters
to simplify the call to row_sel_reset_old_vers_heap().

row_search_for_mysql(): Replaced with direct calls to row_search_mvcc().

sel_node_get_nth_plan(): Define inline in row0sel.h

open_step(): Define at the call site, in simplified form.

sel_node_reset_cursor(): Merged with the only caller open_step().
---
ReadViewBase::check_trx_id_sanity(): Remove.
Let us handle "future" DB_TRX_ID in a more meaningful way:

row_sel_clust_sees(): Return DB_SUCCESS if the record is visible,
DB_SUCCESS_LOCKED_REC if it is invisible, and DB_CORRUPTION if
the DB_TRX_ID is in the future.

row_undo_mod_must_purge(), row_undo_mod_clust(): Silently ignore
corrupted DB_TRX_ID. We are in ROLLBACK, and we should have noticed
that corruption when we were about to modify the record in the first
place (leading us to refuse the operation).

row_vers_build_for_consistent_read(): Return DB_CORRUPTION if
DB_TRX_ID is in the future.

Tested by: Matthias Leich
Reviewed by: Vladislav Lesin
  • Loading branch information
dr-m committed Oct 21, 2022
1 parent 44f2ece commit ab01901
Show file tree
Hide file tree
Showing 30 changed files with 1,246 additions and 877 deletions.
6 changes: 3 additions & 3 deletions mysql-test/suite/gcol/r/innodb_virtual_purge.result
Expand Up @@ -24,7 +24,7 @@ COMMIT;
UPDATE t1 SET a=1;
connection default;
InnoDB 0 transactions not purged
CHECK TABLE t1;
CHECK TABLE t1 EXTENDED;
Table Op Msg_type Msg_text
test.t1 check status OK
SELECT b1 FROM t1;
Expand Down Expand Up @@ -123,7 +123,7 @@ COMMIT;
disconnect con1;
connection default;
InnoDB 0 transactions not purged
CHECK TABLE t1;
CHECK TABLE t1 EXTENDED;
Table Op Msg_type Msg_text
test.t1 check status OK
SELECT b1 FROM t1;
Expand All @@ -134,7 +134,7 @@ SELECT * FROM t1;
a b b1 a1 a4 b3
100 10 10 100 90 100
100 10 10 100 90 100
CHECK TABLE t2;
CHECK TABLE t2 EXTENDED;
Table Op Msg_type Msg_text
test.t2 check status OK
DROP TABLE t2, t1, t0;
Expand Down
6 changes: 3 additions & 3 deletions mysql-test/suite/gcol/t/innodb_virtual_purge.test
Expand Up @@ -38,7 +38,7 @@ UPDATE t1 SET a=1;
connection default;
--source ../../innodb/include/wait_all_purged.inc

CHECK TABLE t1;
CHECK TABLE t1 EXTENDED;
SELECT b1 FROM t1;


Expand Down Expand Up @@ -123,11 +123,11 @@ disconnect con1;
connection default;
--source ../../innodb/include/wait_all_purged.inc

CHECK TABLE t1;
CHECK TABLE t1 EXTENDED;
SELECT b1 FROM t1;

SELECT * FROM t1;
CHECK TABLE t2;
CHECK TABLE t2 EXTENDED;
DROP TABLE t2, t1, t0;

CREATE TABLE t1 (a VARCHAR(30), b INT, a2 VARCHAR(30) GENERATED ALWAYS AS (a) VIRTUAL);
Expand Down
10 changes: 3 additions & 7 deletions mysql-test/suite/innodb/r/trx_id_future.result
Expand Up @@ -6,13 +6,9 @@ SET GLOBAL innodb_purge_rseg_truncate_frequency=1;
CREATE TABLE t1(a INT) row_format=redundant engine=innoDB;
INSERT INTO t1 VALUES(1);
InnoDB 0 transactions not purged
NOT FOUND /\[Warning\] InnoDB: A transaction id in a record of table `test`\.`t1` is newer than the system-wide maximum/ in mysqld.1.err
call mtr.add_suppression("\\[Warning\\] InnoDB: A transaction id in a record of table `test`\\.`t1` is newer than the system-wide maximum");
SET @save_count = @@max_error_count;
SET max_error_count = 1;
call mtr.add_suppression("\\[ERROR\\] InnoDB: We detected index corruption");
call mtr.add_suppression("Index for table 't1' is corrupt; try to repair it");
SELECT * FROM t1;
a
Warnings:
Warning 1642 InnoDB: Transaction id in a record of table `test`.`t1` is newer than system-wide maximum.
SET max_error_count = @save_count;
ERROR HY000: Index for table 't1' is corrupt; try to repair it
DROP TABLE t1;
14 changes: 3 additions & 11 deletions mysql-test/suite/innodb/t/trx_id_future.test
Expand Up @@ -57,19 +57,11 @@ syswrite(FILE, $page, $ps)==$ps || die "Unable to write $file\n";
close(FILE) || die "Unable to close $file";
EOF

# Debug assertions would fail due to the injected corruption.
--let $restart_parameters= --loose-skip-debug-assert
--source include/start_mysqld.inc

let SEARCH_FILE= $MYSQLTEST_VARDIR/log/mysqld.1.err;
let SEARCH_PATTERN= \[Warning\] InnoDB: A transaction id in a record of table `test`\.`t1` is newer than the system-wide maximum;
--source include/search_pattern_in_file.inc

call mtr.add_suppression("\\[Warning\\] InnoDB: A transaction id in a record of table `test`\\.`t1` is newer than the system-wide maximum");
call mtr.add_suppression("\\[ERROR\\] InnoDB: We detected index corruption");
call mtr.add_suppression("Index for table 't1' is corrupt; try to repair it");

# A debug assertion would cause a duplicated message to be output.
SET @save_count = @@max_error_count;
SET max_error_count = 1;
--error ER_NOT_KEYFILE
SELECT * FROM t1;
SET max_error_count = @save_count;
DROP TABLE t1;
1 change: 0 additions & 1 deletion storage/innobase/CMakeLists.txt
Expand Up @@ -338,7 +338,6 @@ SET(INNOBASE_SOURCES
include/row0row.h
include/row0row.inl
include/row0sel.h
include/row0sel.inl
include/row0types.h
include/row0uins.h
include/row0umod.h
Expand Down
5 changes: 0 additions & 5 deletions storage/innobase/btr/btr0btr.cc
Expand Up @@ -5276,11 +5276,6 @@ btr_validate_index(
dict_index_t* index, /*!< in: index */
const trx_t* trx) /*!< in: transaction or NULL */
{
/* Full Text index are implemented by auxiliary tables, not the B-tree */
if (index->online_status != ONLINE_INDEX_COMPLETE ||
(index->type & (DICT_FTS | DICT_CORRUPT)))
return DB_SUCCESS;

const bool lockout= index->is_spatial();

mtr_t mtr;
Expand Down
1 change: 1 addition & 0 deletions storage/innobase/dict/dict0stats.cc
Expand Up @@ -33,6 +33,7 @@ Created Jan 06, 2010 Vasil Dimov
#include <mysql_com.h>
#include "log.h"
#include "btr0btr.h"
#include "que0que.h"

#include <algorithm>
#include <map>
Expand Down
3 changes: 2 additions & 1 deletion storage/innobase/fts/fts0ast.cc
@@ -1,7 +1,7 @@
/*****************************************************************************
Copyright (c) 2007, 2020, Oracle and/or its affiliates. All Rights Reserved.
Copyright (c) 2018, MariaDB Corporation.
Copyright (c) 2018, 2022, MariaDB Corporation.
This program is free software; you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Expand All @@ -28,6 +28,7 @@ Created 2007/3/16 Sunny Bains.
#include "fts0ast.h"
#include "fts0pars.h"
#include "fts0fts.h"
#include "trx0trx.h"

/* The FTS ast visit pass. */
enum fts_ast_visit_pass_t {
Expand Down
83 changes: 46 additions & 37 deletions storage/innobase/handler/ha_innodb.cc
Expand Up @@ -3662,7 +3662,7 @@ ha_innobase::init_table_handle_for_HANDLER(void)
innobase_register_trx(ht, m_user_thd, m_prebuilt->trx);

/* We did the necessary inits in this function, no need to repeat them
in row_search_for_mysql */
in row_search_mvcc() */

m_prebuilt->sql_stat_start = FALSE;

Expand Down Expand Up @@ -7411,7 +7411,7 @@ ha_innobase::build_template(
/* We must at least fetch all primary key cols. Note
that if the clustered index was internally generated
by InnoDB on the row id (no primary key was
defined), then row_search_for_mysql() will always
defined), then row_search_mvcc() will always
retrieve the row id to a special buffer in the
m_prebuilt struct. */

Expand Down Expand Up @@ -8943,7 +8943,7 @@ statement issued by the user. We also increment trx->n_mysql_tables_in_use.
instructions to m_prebuilt->template of the table handle instance in
::index_read. The template is used to save CPU time in large joins.

3) In row_search_for_mysql, if m_prebuilt->sql_stat_start is true, we
3) In row_search_mvcc(), if m_prebuilt->sql_stat_start is true, we
allocate a new consistent read view for the trx if it does not yet have one,
or in the case of a locking read, set an InnoDB 'intention' table level
lock on the table.
Expand Down Expand Up @@ -9245,7 +9245,7 @@ ha_innobase::change_active_index(
}

/* The caller seems to ignore this. Thus, we must check
this again in row_search_for_mysql(). */
this again in row_search_mvcc(). */
DBUG_RETURN(convert_error_code_to_mysql(DB_MISSING_HISTORY,
0, NULL));
}
Expand Down Expand Up @@ -9845,9 +9845,9 @@ ha_innobase::ft_read(

int error;

switch (dberr_t ret = row_search_for_mysql(buf, PAGE_CUR_GE,
m_prebuilt,
ROW_SEL_EXACT, 0)) {
switch (dberr_t ret = row_search_mvcc(buf, PAGE_CUR_GE,
m_prebuilt,
ROW_SEL_EXACT, 0)) {
case DB_SUCCESS:
error = 0;
table->status = 0;
Expand Down Expand Up @@ -15186,8 +15186,10 @@ ha_innobase::check(

DBUG_ENTER("ha_innobase::check");
DBUG_ASSERT(thd == ha_thd());
DBUG_ASSERT(thd == m_user_thd);
ut_a(m_prebuilt->trx->magic_n == TRX_MAGIC_N);
ut_a(m_prebuilt->trx == thd_to_trx(thd));
ut_ad(m_prebuilt->trx->mysql_thd == thd);

if (m_prebuilt->mysql_template == NULL) {
/* Build the template; we will use a dummy template
Expand All @@ -15197,18 +15199,14 @@ ha_innobase::check(
}

if (!m_prebuilt->table->space) {

ib_senderrf(
thd,
IB_LOG_LEVEL_ERROR,
ER_TABLESPACE_DISCARDED,
table->s->table_name.str);

DBUG_RETURN(HA_ADMIN_CORRUPT);

} else if (!m_prebuilt->table->is_readable() &&
!m_prebuilt->table->space) {

} else if (!m_prebuilt->table->is_readable()) {
ib_senderrf(
thd, IB_LOG_LEVEL_ERROR,
ER_TABLESPACE_MISSING,
Expand All @@ -15229,6 +15227,9 @@ ha_innobase::check(
? TRX_ISO_READ_UNCOMMITTED
: TRX_ISO_REPEATABLE_READ;

trx_start_if_not_started(m_prebuilt->trx, false);
m_prebuilt->trx->read_view.open(m_prebuilt->trx);

for (dict_index_t* index
= dict_table_get_first_index(m_prebuilt->table);
index;
Expand All @@ -15237,25 +15238,22 @@ ha_innobase::check(
if (!index->is_committed()) {
continue;
}
if (index->type & DICT_FTS) {
/* We do not check any FULLTEXT INDEX. */
continue;
}

if (!(check_opt->flags & T_QUICK)
&& !index->is_corrupted()) {

dberr_t err = btr_validate_index(
index, m_prebuilt->trx);

if (err != DB_SUCCESS) {
is_ok = false;

push_warning_printf(
thd,
Sql_condition::WARN_LEVEL_WARN,
ER_NOT_KEYFILE,
"InnoDB: The B-tree of"
" index %s is corrupted.",
index->name());
continue;
}
if ((check_opt->flags & T_QUICK) || index->is_corrupted()) {
} else if (btr_validate_index(index, m_prebuilt->trx)
!= DB_SUCCESS) {
is_ok = false;
push_warning_printf(thd,
Sql_condition::WARN_LEVEL_WARN,
ER_NOT_KEYFILE,
"InnoDB: The B-tree of"
" index %s is corrupted.",
index->name());
continue;
}

/* Instead of invoking change_active_index(), set up
Expand All @@ -15277,7 +15275,7 @@ ha_innobase::check(
if (UNIV_UNLIKELY(!m_prebuilt->index_usable)) {
if (index->is_corrupted()) {
push_warning_printf(
m_user_thd,
thd,
Sql_condition::WARN_LEVEL_WARN,
HA_ERR_INDEX_CORRUPT,
"InnoDB: Index %s is marked as"
Expand All @@ -15286,7 +15284,7 @@ ha_innobase::check(
is_ok = false;
} else {
push_warning_printf(
m_user_thd,
thd,
Sql_condition::WARN_LEVEL_WARN,
HA_ERR_TABLE_DEF_CHANGED,
"InnoDB: Insufficient history for"
Expand All @@ -15299,18 +15297,22 @@ ha_innobase::check(
m_prebuilt->sql_stat_start = TRUE;
m_prebuilt->template_type = ROW_MYSQL_DUMMY_TEMPLATE;
m_prebuilt->n_template = 0;
m_prebuilt->need_to_access_clustered = FALSE;
m_prebuilt->read_just_key = 0;
m_prebuilt->autoinc_error = DB_SUCCESS;
m_prebuilt->need_to_access_clustered =
!!(check_opt->flags & T_EXTEND);

dtuple_set_n_fields(m_prebuilt->search_tuple, 0);

m_prebuilt->select_lock_type = LOCK_NONE;

/* Scan this index. */
if (dict_index_is_spatial(index)) {
if (index->is_spatial()) {
ret = row_count_rtree_recs(m_prebuilt, &n_rows);
} else if (index->type & DICT_FTS) {
ret = DB_SUCCESS;
} else {
ret = row_scan_index_for_mysql(
m_prebuilt, index, &n_rows);
ret = row_check_index(m_prebuilt, &n_rows);
}

DBUG_EXECUTE_IF(
Expand All @@ -15319,11 +15321,18 @@ ha_innobase::check(
ret = DB_CORRUPTION;
});

if (ret == DB_INTERRUPTED || thd_killed(m_user_thd)) {
if (ret == DB_INTERRUPTED || thd_killed(thd)) {
/* Do not report error since this could happen
during shutdown */
break;
}

if (ret == DB_SUCCESS
&& m_prebuilt->autoinc_error != DB_MISSING_HISTORY) {
/* See if any non-fatal errors were reported. */
ret = m_prebuilt->autoinc_error;
}

if (ret != DB_SUCCESS) {
/* Assume some kind of corruption. */
push_warning_printf(
Expand Down

0 comments on commit ab01901

Please sign in to comment.