From a2eb94155f6056caf447549e22cf72299a315774 Mon Sep 17 00:00:00 2001 From: Lingjing You Date: Fri, 12 Jun 2026 17:54:27 +0800 Subject: [PATCH 1/6] fix: PXB-3808: fix .ren nameswap collisions during prepare with two-pass temporary renames --- .../innobase/xtrabackup/src/ddl_tracker.cc | 141 ++++++++++++++++-- storage/innobase/xtrabackup/src/ddl_tracker.h | 4 + storage/innobase/xtrabackup/src/xtrabackup.cc | 5 + .../rename_table_atomic_swap_2way.sh | 60 ++++++++ 4 files changed, 197 insertions(+), 13 deletions(-) create mode 100644 storage/innobase/xtrabackup/test/suites/reducedlock/rename_table_atomic_swap_2way.sh diff --git a/storage/innobase/xtrabackup/src/ddl_tracker.cc b/storage/innobase/xtrabackup/src/ddl_tracker.cc index 76843c43c449..8c39fcaf0959 100644 --- a/storage/innobase/xtrabackup/src/ddl_tracker.cc +++ b/storage/innobase/xtrabackup/src/ddl_tracker.cc @@ -43,9 +43,72 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA #include "xtrabackup.h" // datafiles_iter_t static const int REN_FILE_VERSION = 1; +static const char *REN_TMP_SUFFIX = ".temp"; +static const char *REN_TMP_SCHEMA_PREFIX = "xbt"; + +struct pending_ren_file_t { + space_id_t space_id{}; + bool rename_tablespace{false}; + std::string ren_path; + std::string temp_space_name; + std::string temp_path; + std::string dest_space_name; + std::string temp_delta_path; + std::string dest_delta_path; + std::string temp_meta_path; + std::string dest_meta_path; +}; + +static std::vector pending_ren_files; +static std::string pending_ren_schema_name; extern bool xb_read_delta_metadata(const char *filepath, xb_delta_info_t *info); +static bool schema_name_exists(const std::string &schema_name) { + bool exists = false; + + Fil_space_iterator::for_each_space([&](fil_space_t *space) { + if (space == nullptr || space->name == nullptr) { + return DB_SUCCESS; + } + + const std::string_view space_name(space->name); + const std::string_view schema_prefix(schema_name); + + if (space_name.size() > schema_prefix.size() && + space_name.compare(0, schema_prefix.size(), schema_prefix) == 0 && + space_name[schema_prefix.size()] == '/') { + exists = true; + } + + return DB_SUCCESS; + }); + + return exists; +} + +static std::string get_temp_schema_name() { + if (!pending_ren_schema_name.empty()) { + return pending_ren_schema_name; + } + + for (uint32_t suffix = 0;; ++suffix) { + std::string candidate_schema(REN_TMP_SCHEMA_PREFIX); + if (suffix != 0) { + candidate_schema.append(std::to_string(suffix)); + } + + if (!schema_name_exists(candidate_schema)) { + pending_ren_schema_name = candidate_schema; + return pending_ren_schema_name; + } + } +} + +static std::string make_temp_space_name(space_id_t space_id) { + return get_temp_schema_name() + "/s" + std::to_string(space_id); +} + void ddl_tracker_t::backup_file_op(uint32_t space_id, mlog_id_t type, const byte *buf, ulint len, lsn_t record_lsn) { @@ -988,6 +1051,11 @@ bool prepare_handle_ren_files(const datadir_entry_t &entry, void *) { fil_space_t *fil_space = fil_space_get(source_space_id); + pending_ren_file_t pending_ren; + pending_ren.space_id = source_space_id; + pending_ren.ren_path = ren_path; + pending_ren.dest_space_name = dest_space_name; + if (fil_space != nullptr) { char *source_path = nullptr, *source_space_name = nullptr; bool res = fil_space_read_name_and_filepath( @@ -1014,18 +1082,23 @@ bool prepare_handle_ren_files(const datadir_entry_t &entry, void *) { xb::info() << "prepare_handle_ren_files: ren_file: " << ren_path << " already has desired file name: " << dest_path << " source path is: " << source_path; + os_file_delete(0, ren_path.c_str()); return true; } - ut_ad(!os_file_exists(dest_path)); + pending_ren.rename_tablespace = true; + pending_ren.temp_space_name = make_temp_space_name(source_space_id); + pending_ren.temp_path = std::string(source_path) + REN_TMP_SUFFIX; - xb::info() << "prepare_handle_ren_files: renaming " << fil_space->name - << " to " << dest_space_name; + xb::info() << "prepare_handle_ren_files: staging " << fil_space->name + << " to temporary path " << pending_ren.temp_path; if (!fil_rename_tablespace(fil_space->id, source_path, - dest_space_name.c_str(), NULL)) { + pending_ren.temp_space_name.c_str(), + pending_ren.temp_path.c_str())) { xb::error() << "prepare_handle_ren_files: Cannot rename " - << fil_space->name << " to " << dest_space_name; + << fil_space->name << " to temporary path " + << pending_ren.temp_path; return false; } } else { @@ -1056,15 +1129,17 @@ bool prepare_handle_ren_files(const datadir_entry_t &entry, void *) { truncate_suffix(EXT_META, delta_file); delta_file.append(EXT_DELTA); - std::string to_delta(to_path + EXT_DELTA); + pending_ren.temp_delta_path = delta_file + REN_TMP_SUFFIX; + pending_ren.dest_delta_path = to_path + EXT_DELTA; xb::info() << "Renaming incremental delta file from: " << delta_file - << " to: " << to_delta; - rename_force(delta_file, to_delta); + << " to temporary path: " << pending_ren.temp_delta_path; + rename_force(delta_file, pending_ren.temp_delta_path); - std::string to_meta(to_path + EXT_META); + pending_ren.temp_meta_path = meta_file + REN_TMP_SUFFIX; + pending_ren.dest_meta_path = to_path + EXT_META; xb::info() << "Renaming incremental meta file from: " << meta_file - << " to: " << to_meta; - rename_force(meta_file, to_meta); + << " to temporary path: " << pending_ren.temp_meta_path; + rename_force(meta_file, pending_ren.temp_meta_path); } else if (fil_space == nullptr) { // This means the tablespace is neither found in the fullbackup dir // nor in the inc backup directory as .meta and .delta @@ -1078,8 +1153,48 @@ bool prepare_handle_ren_files(const datadir_entry_t &entry, void *) { } } - // delete the .ren file, we don't need it anymore - os_file_delete(0, ren_path.c_str()); + pending_ren_files.push_back(std::move(pending_ren)); + return true; +} + +bool prepare_finalize_ren_files() { + for (const auto &pending_ren : pending_ren_files) { + if (pending_ren.rename_tablespace) { + xb::info() << "prepare_finalize_ren_files: renaming " + << pending_ren.temp_path << " to " + << pending_ren.dest_space_name; + + if (!fil_rename_tablespace(pending_ren.space_id, + pending_ren.temp_path.c_str(), + pending_ren.dest_space_name.c_str(), NULL)) { + xb::error() << "prepare_finalize_ren_files: Cannot rename " + << pending_ren.temp_path << " to " + << pending_ren.dest_space_name; + pending_ren_files.clear(); + pending_ren_schema_name.clear(); + return false; + } + } + + if (!pending_ren.temp_delta_path.empty()) { + xb::info() << "prepare_finalize_ren_files: renaming incremental delta " + << pending_ren.temp_delta_path << " to " + << pending_ren.dest_delta_path; + rename_force(pending_ren.temp_delta_path, pending_ren.dest_delta_path); + } + + if (!pending_ren.temp_meta_path.empty()) { + xb::info() << "prepare_finalize_ren_files: renaming incremental meta " + << pending_ren.temp_meta_path << " to " + << pending_ren.dest_meta_path; + rename_force(pending_ren.temp_meta_path, pending_ren.dest_meta_path); + } + + os_file_delete(0, pending_ren.ren_path.c_str()); + } + + pending_ren_files.clear(); + pending_ren_schema_name.clear(); return true; } diff --git a/storage/innobase/xtrabackup/src/ddl_tracker.h b/storage/innobase/xtrabackup/src/ddl_tracker.h index ae0d07ef0a2d..dd0adc6cf260 100644 --- a/storage/innobase/xtrabackup/src/ddl_tracker.h +++ b/storage/innobase/xtrabackup/src/ddl_tracker.h @@ -184,6 +184,10 @@ bool prepare_handle_ren_files( const datadir_entry_t &entry, /*! >( tee $topdir/backup.log)& + +job_pid=$! +pid_file=$topdir/backup/xtrabackup_debug_sync +wait_for_xb_to_suspend $pid_file +xb_pid=`cat $pid_file` +echo "backup pid is $job_pid" + +$MYSQL $MYSQL_ARGS -Ns -e " +RENAME TABLE test.t1 TO test.t_tmp, + test.t2 TO test.t1, + test.t_tmp TO test.t2;" test + +vlog "Resuming xtrabackup" +kill -SIGCONT $xb_pid +run_cmd wait $job_pid + +xtrabackup --prepare --target-dir=$topdir/backup +record_db_state test +stop_server +rm -rf $mysql_datadir +mkdir $mysql_datadir +xtrabackup --copy-back --target-dir=$topdir/backup +start_server +verify_db_state test + +# After t1 <-> t2 swap : t1=5, t2=2. +T1_ROWS=`${MYSQL} ${MYSQL_ARGS} -Ns -e "SELECT COUNT(*) FROM test.t1"` +T2_ROWS=`${MYSQL} ${MYSQL_ARGS} -Ns -e "SELECT COUNT(*) FROM test.t2"` +if [ "$T1_ROWS" != "5" ] || [ "$T2_ROWS" != "2" ]; then + die "atomic 2-way swap produced wrong rows: t1=$T1_ROWS (want 5) t2=$T2_ROWS (want 2)" +fi + +stop_server +rm -rf $mysql_datadir $topdir/backup $topdir/backup.log From f99436df0eef17a7b6cfa9ca7bc1b3808b34ced0 Mon Sep 17 00:00:00 2001 From: Lingjing You Date: Fri, 12 Jun 2026 17:54:27 +0800 Subject: [PATCH 2/6] fix: PXB-3809: fix dropped recopied tables missing .del markers in ddl_tracker --- .../innobase/xtrabackup/src/ddl_tracker.cc | 5 +- .../inc_create_copy_drop_phantom.sh | 63 +++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 storage/innobase/xtrabackup/test/suites/reducedlock/inc_create_copy_drop_phantom.sh diff --git a/storage/innobase/xtrabackup/src/ddl_tracker.cc b/storage/innobase/xtrabackup/src/ddl_tracker.cc index 8c39fcaf0959..bbcf42a938a7 100644 --- a/storage/innobase/xtrabackup/src/ddl_tracker.cc +++ b/storage/innobase/xtrabackup/src/ddl_tracker.cc @@ -683,10 +683,11 @@ dberr_t ddl_tracker_t::handle_ddl_operations() { /* Remove from missing */ missing_after_discovery.erase(space_id); - /* Remove from new tables and skip drop*/ + /* Remove from new tables. Recopy can temporarily promote an already + copied tablespace into new_tables, but a captured DROP must still leave + a .del marker. */ if (new_tables.find(space_id) != new_tables.end()) { new_tables.erase(space_id); - continue; } /* Table not in the backup, nothing to drop, skip drop*/ diff --git a/storage/innobase/xtrabackup/test/suites/reducedlock/inc_create_copy_drop_phantom.sh b/storage/innobase/xtrabackup/test/suites/reducedlock/inc_create_copy_drop_phantom.sh new file mode 100644 index 000000000000..7abc2f24d0c1 --- /dev/null +++ b/storage/innobase/xtrabackup/test/suites/reducedlock/inc_create_copy_drop_phantom.sh @@ -0,0 +1,63 @@ +############################################################################### +# Incremental --lock-ddl=REDUCED: a tablespace created before the inc backup, +# captured by the inc copy phase, then ADD-INDEX-ed (inplace) and DROP-ed +# inside the same no-lock window must not reappear in the prepared backup. +# handle_ddl_operations() must still emit a .del marker even when the recopy +# loop has just put the sid back into new_tables. +############################################################################### + +. inc/common.sh + +require_debug_pxb_version +start_server + +xtrabackup --backup --target-dir=$topdir/backup_base --lock-ddl=REDUCED +innodb_wait_for_flush_all + +# Create the table between full and inc so the inc copy phase produces +# t1.ibd.delta + t1.ibd.meta. +$MYSQL $MYSQL_ARGS -Ns -e "CREATE TABLE test.t1 (id INT PRIMARY KEY AUTO_INCREMENT, payload VARCHAR(32)); INSERT INTO test.t1 (payload) VALUES ('a'),('b'),('c');" test +innodb_wait_for_flush_all + +xtrabackup --backup --target-dir=$topdir/backup_inc \ + --incremental-basedir=$topdir/backup_base \ + --debug-sync="ddl_tracker_before_lock_ddl" --lock-ddl=REDUCED \ + 2> >( tee $topdir/backup_inc.log)& + +job_pid=$! +pid_file=$topdir/backup_inc/xtrabackup_debug_sync +wait_for_xb_to_suspend $pid_file +xb_pid=`cat $pid_file` +echo "backup pid is $job_pid" + +# MLOG_INDEX_LOAD followed by DROP inside the same no-lock window. +$MYSQL $MYSQL_ARGS -Ns -e "ALTER TABLE test.t1 ADD INDEX idx_payload(payload), ALGORITHM=INPLACE;" test +$MYSQL $MYSQL_ARGS -Ns -e "DROP TABLE test.t1;" test + +vlog "Resuming xtrabackup" +kill -SIGCONT $xb_pid +run_cmd wait $job_pid + +xtrabackup --prepare --apply-log-only --target-dir=$topdir/backup_base +xtrabackup --prepare --target-dir=$topdir/backup_base --incremental-dir=$topdir/backup_inc + +record_db_state test +stop_server +rm -rf $mysql_datadir +mkdir $mysql_datadir +xtrabackup --copy-back --target-dir=$topdir/backup_base +start_server +verify_db_state test + +EXISTS=`${MYSQL} ${MYSQL_ARGS} -Ns -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='test' AND table_name='t1'"` +if [ "$EXISTS" != "0" ]; then + die "test.t1 should be absent after restore, information_schema reports $EXISTS" +fi + +# Orphan .ibd from apply_delta would yield ER_TABLESPACE_EXISTS. +if ! ${MYSQL} ${MYSQL_ARGS} -Ns -e "CREATE TABLE test.t1 (id INT PRIMARY KEY, note VARCHAR(16)); INSERT INTO test.t1 VALUES (1,'fresh'); DROP TABLE test.t1;" ; then + die "CREATE TABLE test.t1 after restore failed (orphan .ibd)" +fi + +stop_server +rm -rf $mysql_datadir $topdir/backup_base $topdir/backup_inc $topdir/backup_inc.log From d698f21b7e0736dbd0ecf5dbd6d1aed298084add Mon Sep 17 00:00:00 2001 From: Lingjing You Date: Fri, 12 Jun 2026 17:54:27 +0800 Subject: [PATCH 3/6] fix: PXB-3810: fix incremental .ren prepare when tablespace already has target name --- .../innobase/xtrabackup/src/ddl_tracker.cc | 38 +++++---- .../incremental_rename_cycle_back.sh | 81 +++++++++++++++++++ 2 files changed, 102 insertions(+), 17 deletions(-) create mode 100644 storage/innobase/xtrabackup/test/suites/reducedlock/incremental_rename_cycle_back.sh diff --git a/storage/innobase/xtrabackup/src/ddl_tracker.cc b/storage/innobase/xtrabackup/src/ddl_tracker.cc index bbcf42a938a7..361b7a2be1bc 100644 --- a/storage/innobase/xtrabackup/src/ddl_tracker.cc +++ b/storage/innobase/xtrabackup/src/ddl_tracker.cc @@ -1077,30 +1077,34 @@ bool prepare_handle_ren_files(const datadir_entry_t &entry, void *) { return false; } - // space_id.ren is already with the desired name. Nothing to do. - if (source_path != nullptr && dest_path != nullptr && - strcmp(source_path, dest_path) == 0) { + const bool source_equals_dest = + source_path != nullptr && dest_path != nullptr && + strcmp(source_path, dest_path) == 0; + + // The .ibd may already have the desired name, but for incremental backups + // we still need to move the corresponding .delta/.meta files. + if (source_equals_dest) { xb::info() << "prepare_handle_ren_files: ren_file: " << ren_path << " already has desired file name: " << dest_path << " source path is: " << source_path; - os_file_delete(0, ren_path.c_str()); - return true; } - pending_ren.rename_tablespace = true; - pending_ren.temp_space_name = make_temp_space_name(source_space_id); - pending_ren.temp_path = std::string(source_path) + REN_TMP_SUFFIX; + if (!source_equals_dest) { + pending_ren.rename_tablespace = true; + pending_ren.temp_space_name = make_temp_space_name(source_space_id); + pending_ren.temp_path = std::string(source_path) + REN_TMP_SUFFIX; - xb::info() << "prepare_handle_ren_files: staging " << fil_space->name - << " to temporary path " << pending_ren.temp_path; + xb::info() << "prepare_handle_ren_files: staging " << fil_space->name + << " to temporary path " << pending_ren.temp_path; - if (!fil_rename_tablespace(fil_space->id, source_path, - pending_ren.temp_space_name.c_str(), - pending_ren.temp_path.c_str())) { - xb::error() << "prepare_handle_ren_files: Cannot rename " - << fil_space->name << " to temporary path " - << pending_ren.temp_path; - return false; + if (!fil_rename_tablespace(fil_space->id, source_path, + pending_ren.temp_space_name.c_str(), + pending_ren.temp_path.c_str())) { + xb::error() << "prepare_handle_ren_files: Cannot rename " + << fil_space->name << " to temporary path " + << pending_ren.temp_path; + return false; + } } } else { // In case source file doesn't exist we check if destination file is already diff --git a/storage/innobase/xtrabackup/test/suites/reducedlock/incremental_rename_cycle_back.sh b/storage/innobase/xtrabackup/test/suites/reducedlock/incremental_rename_cycle_back.sh new file mode 100644 index 000000000000..4e20641b0931 --- /dev/null +++ b/storage/innobase/xtrabackup/test/suites/reducedlock/incremental_rename_cycle_back.sh @@ -0,0 +1,81 @@ +############################################################################### +# Incremental --lock-ddl=REDUCED: a tablespace renamed to an intermediate +# name before the inc copy phase, and renamed back to its original name +# while the inc backup is running, must end up under the original name in +# the prepared backup. prepare_handle_ren_files() must rename the captured +# .delta/.meta to the final name even when source path == dest path. +############################################################################### + +. inc/common.sh + +require_debug_pxb_version +start_server + +$MYSQL $MYSQL_ARGS -Ns -e "CREATE TABLE test.t1 (id INT PRIMARY KEY AUTO_INCREMENT, payload VARCHAR(32)) ENGINE=InnoDB; INSERT INTO test.t1 (payload) VALUES ('a'),('b'),('c');" test +innodb_wait_for_flush_all + +xtrabackup --backup --target-dir=$topdir/backup_base --lock-ddl=REDUCED +innodb_wait_for_flush_all + +# Rename to the intermediate name; inc copy will capture the file under +# this name. +$MYSQL $MYSQL_ARGS -Ns -e "RENAME TABLE test.t1 TO test.t1_mid;" test +$MYSQL $MYSQL_ARGS -Ns -e "INSERT INTO test.t1_mid (payload) VALUES ('d'),('e');" test +innodb_wait_for_flush_all + +xtrabackup --backup --target-dir=$topdir/backup_inc \ + --incremental-basedir=$topdir/backup_base \ + --debug-sync="ddl_tracker_before_lock_ddl" --lock-ddl=REDUCED \ + 2> >( tee $topdir/backup_inc.log)& + +job_pid=$! +pid_file=$topdir/backup_inc/xtrabackup_debug_sync +wait_for_xb_to_suspend $pid_file +xb_pid=`cat $pid_file` +echo "backup pid is $job_pid" + +# Rename back to the original name inside the inc no-lock window. +$MYSQL $MYSQL_ARGS -Ns -e "RENAME TABLE test.t1_mid TO test.t1;" test +$MYSQL $MYSQL_ARGS -Ns -e "INSERT INTO test.t1 (payload) VALUES ('f');" test + +vlog "Resuming xtrabackup" +kill -SIGCONT $xb_pid +run_cmd wait $job_pid + +if ! egrep -q 'DDL tracking : LSN: [0-9]* rename space ID: [0-9]* From: test/t1_mid.ibd To: test/t1.ibd' $topdir/backup_inc.log ; then + die "xtrabackup did not record the cycle-back RENAME TABLE" +fi + +xtrabackup --prepare --apply-log-only --target-dir=$topdir/backup_base +xtrabackup --prepare --target-dir=$topdir/backup_base --incremental-dir=$topdir/backup_inc + +record_db_state test +stop_server +rm -rf $mysql_datadir +mkdir $mysql_datadir +xtrabackup --copy-back --target-dir=$topdir/backup_base +start_server +verify_db_state test + +EXISTS_ORIG=`${MYSQL} ${MYSQL_ARGS} -Ns -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='test' AND table_name='t1'"` +if [ "$EXISTS_ORIG" != "1" ]; then + die "test.t1 should exist after restore, information_schema reports $EXISTS_ORIG" +fi +EXISTS_MID=`${MYSQL} ${MYSQL_ARGS} -Ns -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='test' AND table_name='t1_mid'"` +if [ "$EXISTS_MID" != "0" ]; then + die "test.t1_mid must not exist after restore, information_schema reports $EXISTS_MID" +fi + +# 3 + 2 (between full and inc) + 1 (inside inc window) = 6 +ROWS=`${MYSQL} ${MYSQL_ARGS} -Ns -e "SELECT COUNT(*) FROM test.t1"` +if [ "$ROWS" != "6" ]; then + die "test.t1 should have 6 rows after restore, got $ROWS" +fi + +# Orphan .ibd under the intermediate name would yield ER_TABLESPACE_EXISTS. +if ! ${MYSQL} ${MYSQL_ARGS} -Ns -e "CREATE TABLE test.t1_mid (id INT PRIMARY KEY); DROP TABLE test.t1_mid;" ; then + die "CREATE TABLE test.t1_mid after restore failed (orphan .ibd)" +fi + +stop_server +rm -rf $mysql_datadir $topdir/backup_base $topdir/backup_inc $topdir/backup_inc.log From 2e3dc026b21dbcad835b9233cc9396f379312587 Mon Sep 17 00:00:00 2001 From: Lingjing You Date: Fri, 12 Jun 2026 17:54:27 +0800 Subject: [PATCH 4/6] fix: PXB-3813: ensure destination schema directory exists before processing .ren files to fix cross-db RENAME failure when target database was newly created during backup --- .../innobase/xtrabackup/src/ddl_tracker.cc | 11 +++ .../inc_cross_db_rename_with_new_db.sh | 71 +++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 storage/innobase/xtrabackup/test/suites/reducedlock/inc_cross_db_rename_with_new_db.sh diff --git a/storage/innobase/xtrabackup/src/ddl_tracker.cc b/storage/innobase/xtrabackup/src/ddl_tracker.cc index 361b7a2be1bc..fc8118a7bed0 100644 --- a/storage/innobase/xtrabackup/src/ddl_tracker.cc +++ b/storage/innobase/xtrabackup/src/ddl_tracker.cc @@ -1050,6 +1050,17 @@ bool prepare_handle_ren_files(const datadir_entry_t &entry, void *) { } }); + // CREATE DATABASE during the backup does not produce MLOG_FILE_* records, + // so the destination schema directory may not exist yet in the backup dir. + // fil_rename_tablespace() -> os_file_rename() will not create parent dirs, + // so make sure the destination schema dir exists before any rename happens. + if (dest_path != nullptr && + os_file_create_subdirs_if_needed(dest_path) != DB_SUCCESS) { + xb::error() << "prepare_handle_ren_files: cannot create parent directory " + << "for " << dest_path; + return false; + } + fil_space_t *fil_space = fil_space_get(source_space_id); pending_ren_file_t pending_ren; diff --git a/storage/innobase/xtrabackup/test/suites/reducedlock/inc_cross_db_rename_with_new_db.sh b/storage/innobase/xtrabackup/test/suites/reducedlock/inc_cross_db_rename_with_new_db.sh new file mode 100644 index 000000000000..6969f910d78e --- /dev/null +++ b/storage/innobase/xtrabackup/test/suites/reducedlock/inc_cross_db_rename_with_new_db.sh @@ -0,0 +1,71 @@ +############################################################################### +# Incremental --lock-ddl=REDUCED: destination DB is created inside the inc +# no-lock window, then a table is cross-db renamed into it. prepare must +# materialize the destination directory and move the base .ibd under the new +# schema; otherwise the restored instance misses the table or leaves orphan +# files behind. +############################################################################### + +. inc/common.sh + +require_debug_pxb_version +start_server + +$MYSQL $MYSQL_ARGS -Ns -e "CREATE TABLE test.cross_db_t (id INT PRIMARY KEY AUTO_INCREMENT, payload VARCHAR(32)); INSERT INTO test.cross_db_t VALUES (1,'a'),(2,'b'),(3,'c');" test +innodb_wait_for_flush_all + +xtrabackup --backup --target-dir=$topdir/backup_base --lock-ddl=REDUCED +innodb_wait_for_flush_all + +xtrabackup --backup --target-dir=$topdir/backup_inc \ + --incremental-basedir=$topdir/backup_base \ + --debug-sync="ddl_tracker_before_lock_ddl" --lock-ddl=REDUCED \ + 2> >( tee $topdir/backup_inc.log)& + +job_pid=$! +pid_file=$topdir/backup_inc/xtrabackup_debug_sync +wait_for_xb_to_suspend $pid_file +xb_pid=`cat $pid_file` +echo "backup pid is $job_pid" + +# Inside the inc no-lock window: create the destination DB and move the table +# across. CREATE DATABASE itself leaves no MLOG_FILE_* signal for ddl_tracker. +$MYSQL $MYSQL_ARGS -Ns -e "CREATE DATABASE test_dst;" +$MYSQL $MYSQL_ARGS -Ns -e "RENAME TABLE test.cross_db_t TO test_dst.cross_db_t;" +$MYSQL $MYSQL_ARGS -Ns -e "INSERT INTO test_dst.cross_db_t VALUES (4,'d');" test_dst + +vlog "Resuming xtrabackup" +kill -SIGCONT $xb_pid +run_cmd wait $job_pid + +if ! egrep -q 'DDL tracking : LSN: [0-9]* rename space ID: [0-9]* From: test/cross_db_t.ibd To: test_dst/cross_db_t.ibd' $topdir/backup_inc.log ; then + die "xtrabackup did not record the cross-database RENAME TABLE during the incremental backup" +fi + +xtrabackup --prepare --apply-log-only --target-dir=$topdir/backup_base +xtrabackup --prepare --target-dir=$topdir/backup_base --incremental-dir=$topdir/backup_inc + +record_db_state test_dst +stop_server +rm -rf $mysql_datadir +mkdir $mysql_datadir +xtrabackup --copy-back --target-dir=$topdir/backup_base +start_server +verify_db_state test_dst + +ROWS=`${MYSQL} ${MYSQL_ARGS} -Ns -e "SELECT COUNT(*) FROM test_dst.cross_db_t"` +if [ "$ROWS" != "4" ]; then + die "test_dst.cross_db_t should have 4 rows after restore, got $ROWS" +fi + +EXISTS_OLD=`${MYSQL} ${MYSQL_ARGS} -Ns -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='test' AND table_name='cross_db_t'"` +if [ "$EXISTS_OLD" != "0" ]; then + die "test.cross_db_t should not exist after incremental restore, but information_schema reports it" +fi + +if ! ${MYSQL} ${MYSQL_ARGS} -Ns -e "DROP TABLE test_dst.cross_db_t; CREATE TABLE test_dst.cross_db_t (id INT PRIMARY KEY); INSERT INTO test_dst.cross_db_t VALUES (1); SELECT COUNT(*) FROM test_dst.cross_db_t;" ; then + die "CREATE TABLE test_dst.cross_db_t after incremental restore failed — likely orphan .ibd left over from cross-db RENAME" +fi + +stop_server +rm -rf $mysql_datadir $topdir/backup_base $topdir/backup_inc $topdir/backup_inc.log From 4df8f5a571b6774ff845b232187fd6a6ee831189 Mon Sep 17 00:00:00 2001 From: Lingjing You Date: Fri, 12 Jun 2026 17:54:28 +0800 Subject: [PATCH 5/6] fix: PXB-3812: fail backup on uncovered corrupted tablespaces --- .../innobase/xtrabackup/src/ddl_tracker.cc | 21 +++++++ storage/innobase/xtrabackup/src/xtrabackup.cc | 3 +- .../corrupted_tablespace_no_ddl.sh | 58 +++++++++++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 storage/innobase/xtrabackup/test/suites/reducedlock/corrupted_tablespace_no_ddl.sh diff --git a/storage/innobase/xtrabackup/src/ddl_tracker.cc b/storage/innobase/xtrabackup/src/ddl_tracker.cc index fc8118a7bed0..fc147a51728e 100644 --- a/storage/innobase/xtrabackup/src/ddl_tracker.cc +++ b/storage/innobase/xtrabackup/src/ddl_tracker.cc @@ -825,6 +825,27 @@ dberr_t ddl_tracker_t::handle_ddl_operations() { new_tables[elem.second] = elem.first; } + /* Every .crpt marker must be paired with either a DROP (.del) or a + successful recopy (.new). Otherwise prepare_handle_corrupt_files() would + silently delete the .ibd in the backup with no replacement, producing a + successful-looking backup that has actually lost the table. This must be + checked here, before the early-return below: when there is no other DDL, + new_tables is empty and the recopy loop is skipped, but the .crpt marker + still ends up on disk and must not be allowed to silently destroy data. */ + for (const auto &cor : corrupted_tablespaces) { + space_id_t sid = cor.first; + const std::string &name = cor.second.first; + const bool dropped = drops.find(sid) != drops.end(); + const bool recopied = new_tables.find(sid) != new_tables.end(); + if (!dropped && !recopied) { + xb::error() << "DDL tracking : corrupted tablespace " << name + << " (space_id=" << sid + << ") is neither dropped nor recopied; aborting backup to" + " avoid silent data loss."; + return DB_ERROR; + } + } + if (new_tables.empty()) { xb::info() << "DDL tracking : no new files are being copied."; return DB_SUCCESS; diff --git a/storage/innobase/xtrabackup/src/xtrabackup.cc b/storage/innobase/xtrabackup/src/xtrabackup.cc index a47d92b56cab..0310b81c4ac8 100644 --- a/storage/innobase/xtrabackup/src/xtrabackup.cc +++ b/storage/innobase/xtrabackup/src/xtrabackup.cc @@ -3315,7 +3315,8 @@ bool xtrabackup_copy_datafile_func(fil_node_t *node, uint thread_n, if (res == XB_FIL_CUR_ERROR || (res == XB_FIL_CUR_CORRUPTED && - (ddl_tracker == nullptr || opt_lock_ddl != LOCK_DDL_REDUCED))) { + (ddl_tracker == nullptr || opt_lock_ddl != LOCK_DDL_REDUCED || + is_server_locked()))) { goto error; } diff --git a/storage/innobase/xtrabackup/test/suites/reducedlock/corrupted_tablespace_no_ddl.sh b/storage/innobase/xtrabackup/test/suites/reducedlock/corrupted_tablespace_no_ddl.sh new file mode 100644 index 000000000000..7a5465cd8aaa --- /dev/null +++ b/storage/innobase/xtrabackup/test/suites/reducedlock/corrupted_tablespace_no_ddl.sh @@ -0,0 +1,58 @@ +############################################################################### +# Verifies that when a tablespace becomes corrupted during backup but is NEVER +# covered by a DDL (no drop, no recopy trigger), xtrabackup must fail rather +# than produce a "successful" backup with a phantom .crpt that destroys the +# table at prepare time. +############################################################################### + +. inc/common.sh + +require_debug_pxb_version +start_server + +$MYSQL $MYSQL_ARGS -Ns -e " + CREATE TABLE test.t ( + id INT PRIMARY KEY AUTO_INCREMENT, + payload CHAR(200)) ENGINE=InnoDB; + INSERT INTO test.t (payload) VALUES + (REPEAT('x',200)),(REPEAT('y',200)),(REPEAT('z',200)); +" test +innodb_wait_for_flush_all + +# Pause xtrabackup right after tablespace discovery, before data copy starts. +xtrabackup --backup --target-dir=$topdir/backup_corrupt \ + --debug-sync="xtrabackup_load_tablespaces_pause" --lock-ddl=REDUCED \ + 2> >( tee $topdir/backup_corrupt.log)& + +job_pid=$! +pid_file=$topdir/backup_corrupt/xtrabackup_debug_sync +wait_for_xb_to_suspend $pid_file +xb_pid=`cat $pid_file` + +# Inject corruption into page 4 of the source .ibd (16K page size assumed). +# 200 bytes of /dev/urandom is enough to break the checksum reliably. +TABLE_FILE=$mysql_datadir/test/t.ibd +[ -f "$TABLE_FILE" ] || die "expected $TABLE_FILE to exist" +dd if=/dev/urandom of=$TABLE_FILE bs=1 count=200 seek=65536 conv=notrunc \ + status=none + +# Resume xtrabackup. NO DDL is issued, so the corrupted sid will NOT enter +# drops nor new_tables. +kill -SIGCONT $xb_pid + +# After fix: backup must fail. Use 'wait' and capture exit code. +set +e +wait $job_pid +xb_rc=$? +set -e + +if [ $xb_rc -eq 0 ]; then + die "xtrabackup unexpectedly succeeded with an uncovered corrupted tablespace" +fi + +# Sanity check on the error message produced by handle_ddl_operations. +if ! grep -q "corrupted tablespace" $topdir/backup_corrupt.log; then + die "expected 'corrupted tablespace' error in backup log" +fi + +vlog "OK: backup correctly aborted on uncovered corrupted tablespace" \ No newline at end of file From a17f3f9f928f62c630f62eaf0c9ff2726de9b079 Mon Sep 17 00:00:00 2001 From: Lingjing You Date: Fri, 12 Jun 2026 17:54:59 +0800 Subject: [PATCH 6/6] fix: PXB-3816: fix hole pages in reduced-lock incremental backup when a recopied tablespace is also renamed --- .../innobase/xtrabackup/src/ddl_tracker.cc | 10 +++ storage/innobase/xtrabackup/src/ddl_tracker.h | 11 ++++ storage/innobase/xtrabackup/src/write_filt.cc | 6 +- storage/innobase/xtrabackup/src/write_filt.h | 4 ++ storage/innobase/xtrabackup/src/xtrabackup.cc | 10 +++ .../incremental_recopy_rename_hole.sh | 66 +++++++++++++++++++ 6 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 storage/innobase/xtrabackup/test/suites/reducedlock/incremental_recopy_rename_hole.sh diff --git a/storage/innobase/xtrabackup/src/ddl_tracker.cc b/storage/innobase/xtrabackup/src/ddl_tracker.cc index fc147a51728e..b910cba4a821 100644 --- a/storage/innobase/xtrabackup/src/ddl_tracker.cc +++ b/storage/innobase/xtrabackup/src/ddl_tracker.cc @@ -323,6 +323,11 @@ bool ddl_tracker_t::is_tablespace_dropped(const space_id_t space_id) { return (drops.find(space_id) != drops.end()); } +bool ddl_tracker_t::is_recopy_renamed(const space_id_t space_id) { + std::lock_guard lock(m_ddl_tracker_mutex); + return (recopy_renamed_spaces.find(space_id) != recopy_renamed_spaces.end()); +} + void ddl_tracker_t::add_rename_ibd_scan(const space_id_t &space_id, std::string new_name) { // undo tablespaces are tracked separately. @@ -658,6 +663,11 @@ dberr_t ddl_tracker_t::handle_ddl_operations() { backup_file_printf( convert_file_name(table, old_table_name, flags, EXT_DEL).c_str(), "%s", ""); + /* The old-name base file is deleted via the .del above and the new + name is reconstructed from the .new file during prepare. A sparse + incremental delta would leave hole pages for such a table, so mark it + to force a full (write-through) recopy. */ + recopy_renamed_spaces.insert(table); } string table_name = tables_copied_no_lock[table].first; new_tables[table] = table_name; diff --git a/storage/innobase/xtrabackup/src/ddl_tracker.h b/storage/innobase/xtrabackup/src/ddl_tracker.h index dd0adc6cf260..de010e7fa004 100644 --- a/storage/innobase/xtrabackup/src/ddl_tracker.h +++ b/storage/innobase/xtrabackup/src/ddl_tracker.h @@ -51,6 +51,12 @@ class ddl_tracker_t { name_to_space_id_t after_lock_undo; /** Tablespaces involved in encryption or bulk index load.*/ std::unordered_set recopy_tables; + /** Tablespaces that are both recopied and renamed during the backup. For + these, prepare deletes the old-name base file (.del) and reconstructs the + new name by applying the recopied .new.delta onto a freshly-created file. + A sparse incremental delta would leave hole pages, so the recopy must emit a + delta containing every page (full_copy) for these tablespaces. */ + std::unordered_set recopy_renamed_spaces; /** Drop operations found in redo log. */ space_id_to_name_t drops; /* For DDL operation found in redo log, */ @@ -159,6 +165,11 @@ class ddl_tracker_t { /** @return true if tablespace is dropped @param[in] space_id tablespace id */ bool is_tablespace_dropped(const space_id_t space_id); + + /** @return true if the tablespace is both recopied and renamed during the + backup, in which case the recopy must write a full (non-sparse) copy. + @param[in] space_id tablespace id */ + bool is_recopy_renamed(const space_id_t space_id); }; /** Insert into meta files map. This map is later used to delete the right diff --git a/storage/innobase/xtrabackup/src/write_filt.cc b/storage/innobase/xtrabackup/src/write_filt.cc index 295cb4825f8c..3476a9438598 100644 --- a/storage/innobase/xtrabackup/src/write_filt.cc +++ b/storage/innobase/xtrabackup/src/write_filt.cc @@ -128,8 +128,10 @@ static bool wf_incremental_process(xb_write_filt_ctxt_t *ctxt, * since the last backup. Hence we copy all changes to mysql.ibd since last * backup start_lsn instead of last backup end_lsn. */ - if (cursor->space_id == dict_sys_t::s_dict_space_id && - metadata_from_lsn > mach_read_from_8(page + FIL_PAGE_LSN)) + if (cp->full_copy) { + /* copy every page so the .delta can rebuild a complete file */ + } else if (cursor->space_id == dict_sys_t::s_dict_space_id && + metadata_from_lsn > mach_read_from_8(page + FIL_PAGE_LSN)) continue; else if (incremental_lsn > mach_read_from_8(page + FIL_PAGE_LSN)) continue; diff --git a/storage/innobase/xtrabackup/src/write_filt.h b/storage/innobase/xtrabackup/src/write_filt.h index 92e1004f5592..4c2d5a4ea505 100644 --- a/storage/innobase/xtrabackup/src/write_filt.h +++ b/storage/innobase/xtrabackup/src/write_filt.h @@ -33,6 +33,10 @@ typedef struct { byte *delta_buf_base; byte *delta_buf; ulint npages; + /* When true, copy every page regardless of its LSN so the resulting .delta + can rebuild a complete file (used for tablespaces recopied after a rename, + whose old-name base file is deleted during prepare). */ + bool full_copy; } xb_wf_incremental_ctxt_t; /* Page filter context used as an opaque structure by callers */ diff --git a/storage/innobase/xtrabackup/src/xtrabackup.cc b/storage/innobase/xtrabackup/src/xtrabackup.cc index 0310b81c4ac8..9b79ce94574b 100644 --- a/storage/innobase/xtrabackup/src/xtrabackup.cc +++ b/storage/innobase/xtrabackup/src/xtrabackup.cc @@ -3282,6 +3282,16 @@ bool xtrabackup_copy_datafile_func(fil_node_t *node, uint thread_n, goto error; } + /* A tablespace that is both recopied and renamed during the backup has its + old-name base file deleted (.del) and its new name reconstructed from the + recopied .new.delta during prepare. A sparse incremental delta would leave + hole pages on the freshly-created file, so force the delta to include every + page for such tablespaces. */ + if (write_filter == &wf_incremental && ddl_tracker != nullptr && + ddl_tracker->is_recopy_renamed(node->space->id)) { + write_filt_ctxt.wf_incremental_ctxt.full_copy = true; + } + /* do not compress encrypted tablespaces */ if (cursor.is_encrypted) { dstfile = diff --git a/storage/innobase/xtrabackup/test/suites/reducedlock/incremental_recopy_rename_hole.sh b/storage/innobase/xtrabackup/test/suites/reducedlock/incremental_recopy_rename_hole.sh new file mode 100644 index 000000000000..1c436b28af7d --- /dev/null +++ b/storage/innobase/xtrabackup/test/suites/reducedlock/incremental_recopy_rename_hole.sh @@ -0,0 +1,66 @@ +. inc/common.sh + +require_debug_pxb_version +start_server + +$MYSQL $MYSQL_ARGS -Ns -e "CREATE TABLE test.t1 (id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50), pad CHAR(200)); INSERT INTO test.t1(name,pad) VALUES ('a',REPEAT('x',200)),('b',REPEAT('y',200));" test +for i in $(seq 1 16); do + $MYSQL $MYSQL_ARGS -Ns -e "INSERT INTO test.t1(name,pad) SELECT name,pad FROM test.t1;" test +done + +innodb_wait_for_flush_all + +xtrabackup --backup --target-dir=$topdir/backup_base --lock-ddl=REDUCED + +innodb_wait_for_flush_all + +xtrabackup --backup --target-dir=$topdir/backup_inc --incremental-basedir=$topdir/backup_base \ + --debug-sync="ddl_tracker_before_lock_ddl" --lock-ddl=REDUCED \ + 2> >( tee $topdir/backup_inc.log)& + +job_pid=$! +pid_file=$topdir/backup_inc/xtrabackup_debug_sync +wait_for_xb_to_suspend $pid_file +xb_pid=`cat $pid_file` +echo "backup pid is $job_pid" + +# ADD INDEX puts t1 into the recopy set; RENAME makes the recopy target a name +# that does not exist in the base backup. +$MYSQL $MYSQL_ARGS -Ns -e "ALTER TABLE test.t1 ADD INDEX name_idx(name); RENAME TABLE test.t1 TO test.t2;" test + +vlog "Resuming xtrabackup" +kill -SIGCONT $xb_pid +run_cmd wait $job_pid + +xtrabackup --prepare --apply-log-only --target-dir=$topdir/backup_base +xtrabackup --prepare --target-dir=$topdir/backup_base --incremental-dir=$topdir/backup_inc + +original_count=`$MYSQL $MYSQL_ARGS -Ns -e "SELECT COUNT(*) FROM test.t2 FORCE INDEX(name_idx);"` + +record_db_state test +stop_server +rm -rf $mysql_datadir +mkdir $mysql_datadir +xtrabackup --copy-back --target-dir=$topdir/backup_base +start_server + + +$MYSQL $MYSQL_ARGS -Ns -e "SELECT COUNT(*) FROM test.t2; CHECKSUM TABLE test.t2;" test +scan_rc=$? +if ! $MYSQL $MYSQL_ARGS -Ns -e "SELECT 1" >/dev/null 2>&1; then + die "mysqld crashed while reading restored t2 (lost connection): the restored tablespace is corrupt" +fi +if [ "$scan_rc" != "0" ]; then + die "full scan of restored t2 failed (rc=$scan_rc): the restored tablespace is corrupt" +fi + +verify_db_state test + +restored_count=`$MYSQL $MYSQL_ARGS -Ns -e "SELECT COUNT(*) FROM test.t2 FORCE INDEX(name_idx);"` + +if [ "$original_count" != "$restored_count" ]; then + die "rows in t2 via secondary index mismatch: original=$original_count restored=$restored_count" +fi + +stop_server +rm -rf $mysql_datadir $topdir/backup_base $topdir/backup_inc $topdir/backup_inc.log \ No newline at end of file