From 9199727c4eca5d3e380bfe896a0cc70091e5116a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20M=C3=A4kel=C3=A4?= Date: Thu, 28 May 2026 08:43:04 +0300 Subject: [PATCH 01/20] MDEV-39770 log_t::persist(): Assertion is_opened() == archive failed log_t::persist(): Move a debug assertion after the point that we are actually about to write to the log. For crash recovery, this function may be invoked in such a way that log_sys.archive disagrees with the format of the log file. Reviewed by: Thirunarayanan Balathandayuthapani --- storage/innobase/log/log0log.cc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/storage/innobase/log/log0log.cc b/storage/innobase/log/log0log.cc index 6364f9fc49615..aad4f768f0867 100644 --- a/storage/innobase/log/log0log.cc +++ b/storage/innobase/log/log0log.cc @@ -1544,13 +1544,14 @@ void log_t::persist(lsn_t lsn) noexcept ut_ad(!write_lock.is_owner()); ut_ad(!flush_lock.is_owner()); ut_ad(latch_have_wr()); - ut_ad(is_opened() == archive); lsn_t old= flushed_to_disk_lsn.load(std::memory_order_relaxed); if (old > lsn) return; + ut_ad(is_mmap_writeable()); + ut_ad(is_opened() == archive); const size_t start(calc_lsn_offset(old)); const size_t end(calc_lsn_offset(lsn)); From 09b9772eec85b72bca633f5885b4c4a029326214 Mon Sep 17 00:00:00 2001 From: Daniel Bartholomew Date: Fri, 29 May 2026 15:34:03 -0400 Subject: [PATCH 02/20] bump the VERSION --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index a8c500363bbb8..51f2704d067c7 100644 --- a/VERSION +++ b/VERSION @@ -1,4 +1,4 @@ MYSQL_VERSION_MAJOR=13 MYSQL_VERSION_MINOR=0 -MYSQL_VERSION_PATCH=1 +MYSQL_VERSION_PATCH=2 SERVER_MATURITY=gamma From 3a19b9afd042343921ac9658c276710861d8069a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20M=C3=A4kel=C3=A4?= Date: Fri, 5 Jun 2026 15:15:14 +0300 Subject: [PATCH 03/20] MDEV-39861 innodb_log_recovery_target wrongly opens log in read-write mode log_t::archived_switch_recovery_prepare(): Observe recv_sys.rpo similar to what recv_sys_t::find_checkpoint() does. --- storage/innobase/log/log0recv.cc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/storage/innobase/log/log0recv.cc b/storage/innobase/log/log0recv.cc index 12b0715e439bc..59dd21fddd917 100644 --- a/storage/innobase/log/log0recv.cc +++ b/storage/innobase/log/log0recv.cc @@ -3768,7 +3768,8 @@ bool log_t::archived_switch_recovery_prepare(lsn_t lsn) noexcept static_assert(int{READ_WRITE} == 0, ""); static_assert(int{READ_ONLY} == 1, ""); resize_log.m_file= os_file_create_func(fn, OS_FILE_OPEN, OS_LOG_FILE, - int{i->second.access} > 0, &success); + int{i->second.access > 0} || + recv_sys.rpo, &success); ut_ad(success == (resize_log.m_file != OS_FILE_CLOSED)); if (resize_log.m_file == OS_FILE_CLOSED) { From 10f3acb01596ce664b8e7a96498eb05ca2385dd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20M=C3=A4kel=C3=A4?= Date: Mon, 8 Jun 2026 12:31:00 +0300 Subject: [PATCH 04/20] WIP MDEV-14992 BACKUP SERVER This introduces a basic driver Sql_cmd_backup, storage engine interfaces, and basic copying of InnoDB data files. On Windows, we pass a target directory name; elsewhere, we pass a target directory handle. backup_target: A structured data type to represent a directory or a stream. On Microsoft Windows, we must use directory paths because there is no variant of CopyFileEx() that would work on file handles. copy_entire_file(): A file copying service for POSIX systems. copy_file(): A sparse file-copying service for all systems. backup_context: An InnoDB backup context, attached to trx->lock.backup so that context can exist between InnoDB_backup::end(), which is releasing all locks, and InnoDB_backup::fini() in the same thread, which is expected to finalize the backup without modifying files in the server data directory. fil_space_t::write_or_backup: Keep track of in-flight page writes and pending backup operation. We must not allow them concurrently, because that could lead into torn pages in the backup. fil_space_t::backup_end: The first page number that is not being backed up (by default 0, to indicate that no backup is in progress). fil_space_t::BACKUP_BATCH_SIZE: The number of preceding pages that will be covered by fil_space_t::backup_end. This is the unit of "page range locking" during InnoDB backup. TRX_STATE_BACKUP: A special InnoDB transaction state indicating association with BACKUP SERVER, which allows us to pass some context in trx_t from innodb_backup_end() to innodb_backup_finalize(). log_t::backup: Whether BACKUP SERVER is in progress. The purpose of this is to make BACKUP SERVER prevent the concurrent execution of SET GLOBAL innodb_log_archive=OFF or SET GLOBAL innodb_log_file_size when innodb_log_archive=OFF. log_sys.archived_checkpoint: Keep track of the earliest available checkpoint, corresponding to log_sys.archived_lsn. This reflects SET GLOBAL innodb_log_recovery_start (which is settable now), for incremental backup. buf_flush_list_space(): Check for concurrent backup before writing each page. This is inefficient, but this function may be invoked from multiple threads concurrently, and it cannot be changed easily, especially for fil_crypt_thread(). --- libmysqld/CMakeLists.txt | 1 + mysql-test/collections/buildbot_suites.bat | 1 + mysql-test/main/backup_server.result | 6 + mysql-test/main/backup_server.test | 10 + mysql-test/main/backup_server_locking.result | 17 + mysql-test/main/backup_server_locking.test | 31 + mysql-test/main/grant_backup_server.result | 27 + mysql-test/main/grant_backup_server.test | 29 + mysql-test/main/mysqld--help.result | 2 +- mysql-test/mariadb-test-run.pl | 1 + .../suite/backup/backup_innodb,debug.rdiff | 16 + .../suite/backup/backup_innodb.combinations | 4 + mysql-test/suite/backup/backup_innodb.result | 24 + mysql-test/suite/backup/backup_innodb.test | 49 + mysql-test/suite/backup/suite.pm | 10 + .../perfschema/r/max_program_zero.result | 2 +- .../suite/perfschema/r/ortho_iter.result | 2 +- .../perfschema/r/privilege_table_io.result | 2 +- .../r/start_server_disable_idle.result | 2 +- .../r/start_server_disable_stages.result | 2 +- .../r/start_server_disable_statements.result | 2 +- .../start_server_disable_transactions.result | 2 +- .../r/start_server_disable_waits.result | 2 +- .../perfschema/r/start_server_innodb.result | 2 +- .../r/start_server_low_index.result | 2 +- .../r/start_server_low_table_lock.result | 2 +- .../r/start_server_no_account.result | 2 +- .../r/start_server_no_cond_class.result | 2 +- .../r/start_server_no_cond_inst.result | 2 +- .../r/start_server_no_file_class.result | 2 +- .../r/start_server_no_file_inst.result | 2 +- .../perfschema/r/start_server_no_host.result | 2 +- .../perfschema/r/start_server_no_index.result | 2 +- .../perfschema/r/start_server_no_mdl.result | 2 +- .../r/start_server_no_memory_class.result | 2 +- .../r/start_server_no_mutex_class.result | 2 +- .../r/start_server_no_mutex_inst.result | 2 +- ..._server_no_prepared_stmts_instances.result | 2 +- .../r/start_server_no_rwlock_class.result | 2 +- .../r/start_server_no_rwlock_inst.result | 2 +- .../r/start_server_no_setup_actors.result | 2 +- .../r/start_server_no_setup_objects.result | 2 +- .../r/start_server_no_socket_class.result | 2 +- .../r/start_server_no_socket_inst.result | 2 +- .../r/start_server_no_stage_class.result | 2 +- .../r/start_server_no_stages_history.result | 2 +- ...start_server_no_stages_history_long.result | 2 +- .../start_server_no_statements_history.result | 2 +- ...t_server_no_statements_history_long.result | 2 +- .../r/start_server_no_table_hdl.result | 2 +- .../r/start_server_no_table_inst.result | 2 +- .../r/start_server_no_table_lock.result | 2 +- .../r/start_server_no_thread_class.result | 2 +- .../r/start_server_no_thread_inst.result | 2 +- ...tart_server_no_transactions_history.result | 2 +- ...server_no_transactions_history_long.result | 2 +- .../perfschema/r/start_server_no_user.result | 2 +- .../r/start_server_no_waits_history.result | 2 +- .../start_server_no_waits_history_long.result | 2 +- .../perfschema/r/start_server_off.result | 2 +- .../suite/perfschema/r/start_server_on.result | 2 +- .../r/start_server_variables.result | 2 +- .../r/statement_program_lost_inst.result | 2 +- .../suite/sys_vars/r/sysvars_innodb.result | 2 +- sql/CMakeLists.txt | 1 + sql/handler.h | 56 ++ sql/mysqld.cc | 1 + sql/sql_backup.cc | 373 +++++++ sql/sql_backup.h | 36 + sql/sql_backup_interface.h | 71 ++ sql/sql_command.h | 1 + sql/sql_parse.cc | 4 +- sql/sql_yacc.yy | 6 + sql/sys_vars.inl | 2 + storage/innobase/CMakeLists.txt | 2 + storage/innobase/buf/buf0flu.cc | 101 +- storage/innobase/handler/backup_innodb.cc | 937 ++++++++++++++++++ storage/innobase/handler/backup_innodb.h | 54 + storage/innobase/handler/ha_innodb.cc | 69 +- storage/innobase/include/fil0fil.h | 47 + storage/innobase/include/log0log.h | 62 +- storage/innobase/include/trx0trx.h | 12 +- storage/innobase/include/trx0trx.inl | 1 + storage/innobase/include/trx0types.h | 23 +- storage/innobase/log/log0log.cc | 78 +- storage/innobase/log/log0recv.cc | 4 +- storage/innobase/os/os0file.cc | 10 +- storage/innobase/trx/trx0roll.cc | 3 + storage/innobase/trx/trx0sys.cc | 2 + storage/innobase/trx/trx0trx.cc | 7 + 90 files changed, 2151 insertions(+), 138 deletions(-) create mode 100644 mysql-test/main/backup_server.result create mode 100644 mysql-test/main/backup_server.test create mode 100644 mysql-test/main/backup_server_locking.result create mode 100644 mysql-test/main/backup_server_locking.test create mode 100644 mysql-test/main/grant_backup_server.result create mode 100644 mysql-test/main/grant_backup_server.test create mode 100644 mysql-test/suite/backup/backup_innodb,debug.rdiff create mode 100644 mysql-test/suite/backup/backup_innodb.combinations create mode 100644 mysql-test/suite/backup/backup_innodb.result create mode 100644 mysql-test/suite/backup/backup_innodb.test create mode 100644 mysql-test/suite/backup/suite.pm create mode 100644 sql/sql_backup.cc create mode 100644 sql/sql_backup.h create mode 100644 sql/sql_backup_interface.h create mode 100644 storage/innobase/handler/backup_innodb.cc create mode 100644 storage/innobase/handler/backup_innodb.h diff --git a/libmysqld/CMakeLists.txt b/libmysqld/CMakeLists.txt index 80b202aa4c03e..59f386a86edae 100644 --- a/libmysqld/CMakeLists.txt +++ b/libmysqld/CMakeLists.txt @@ -166,6 +166,7 @@ SET(SQL_EMBEDDED_SOURCES emb_qcache.cc libmysqld.c lib_sql.cc ../sql/opt_hints.cc ../sql/opt_hints.h ../sql/opt_trace_ddl_info.cc ../sql/opt_trace_ddl_info.h ../sql/sql_path.cc + ../sql/sql_backup.cc ${GEN_SOURCES} ${MYSYS_LIBWRAP_SOURCE} ) diff --git a/mysql-test/collections/buildbot_suites.bat b/mysql-test/collections/buildbot_suites.bat index 053057872bf40..129a2f67d79de 100644 --- a/mysql-test/collections/buildbot_suites.bat +++ b/mysql-test/collections/buildbot_suites.bat @@ -6,6 +6,7 @@ innodb,^ versioning,^ plugins,^ mariabackup,^ +backup,^ roles,^ auth_gssapi,^ mysql_sha2,^ diff --git a/mysql-test/main/backup_server.result b/mysql-test/main/backup_server.result new file mode 100644 index 0000000000000..7a064d7e5fe11 --- /dev/null +++ b/mysql-test/main/backup_server.result @@ -0,0 +1,6 @@ +BACKUP SERVER TO '$datadir/some_directory'; +ERROR HY000: Incorrect arguments to BACKUP SERVER TO +BACKUP SERVER TO '$MYSQLTEST_VARDIR/some_directory'; +BACKUP SERVER TO '$MYSQLTEST_VARDIR/some_directory'; +ERROR HY000: Can't create directory 'MYSQLTEST_VARDIR/some_directory' (Errcode: 17 "File exists") +BACKUP SERVER TO '$MYSQLTEST_VARDIR/some_directory'; diff --git a/mysql-test/main/backup_server.test b/mysql-test/main/backup_server.test new file mode 100644 index 0000000000000..50cf326d39838 --- /dev/null +++ b/mysql-test/main/backup_server.test @@ -0,0 +1,10 @@ +--let $datadir=`select @@datadir` +--error ER_WRONG_ARGUMENTS +evalp BACKUP SERVER TO '$datadir/some_directory'; +evalp BACKUP SERVER TO '$MYSQLTEST_VARDIR/some_directory'; +--replace_result $MYSQLTEST_VARDIR MYSQLTEST_VARDIR +--error 21 +evalp BACKUP SERVER TO '$MYSQLTEST_VARDIR/some_directory'; +--rmdir $MYSQLTEST_VARDIR/some_directory +evalp BACKUP SERVER TO '$MYSQLTEST_VARDIR/some_directory'; +--rmdir $MYSQLTEST_VARDIR/some_directory diff --git a/mysql-test/main/backup_server_locking.result b/mysql-test/main/backup_server_locking.result new file mode 100644 index 0000000000000..6e49a09e3e873 --- /dev/null +++ b/mysql-test/main/backup_server_locking.result @@ -0,0 +1,17 @@ +BACKUP SERVER TO '$MYSQLTEST_VARDIR/some_directory'; +ERROR HY000: Can't create directory 'MYSQLTEST_VARDIR/some_directory' (Errcode: 17 "File exists") +BACKUP STAGE START; +connect backup,localhost,root; +SET STATEMENT max_statement_time=0.1 FOR +BACKUP SERVER TO '$MYSQLTEST_VARDIR/some_directory'; +ERROR 70100: Query was interrupted: execution time limit 0.1 sec exceeded +connection default; +BACKUP SERVER TO '$MYSQLTEST_VARDIR/some_directory'; +ERROR HY000: Can't execute the command as you have a BACKUP STAGE active +BACKUP STAGE END; +connection backup; +SET STATEMENT max_statement_time=0.1 FOR +BACKUP SERVER TO '$MYSQLTEST_VARDIR/some_directory'; +ERROR HY000: Can't create directory 'MYSQLTEST_VARDIR/some_directory' (Errcode: 17 "File exists") +disconnect backup; +connection default; diff --git a/mysql-test/main/backup_server_locking.test b/mysql-test/main/backup_server_locking.test new file mode 100644 index 0000000000000..4e769b61b2f20 --- /dev/null +++ b/mysql-test/main/backup_server_locking.test @@ -0,0 +1,31 @@ +--source include/not_embedded.inc +--source include/count_sessions.inc + +--mkdir $MYSQLTEST_VARDIR/some_directory +--replace_result $MYSQLTEST_VARDIR MYSQLTEST_VARDIR +--error 21 +evalp BACKUP SERVER TO '$MYSQLTEST_VARDIR/some_directory'; + +BACKUP STAGE START; +--connect (backup,localhost,root) +--error ER_STATEMENT_TIMEOUT +evalp SET STATEMENT max_statement_time=0.1 FOR +BACKUP SERVER TO '$MYSQLTEST_VARDIR/some_directory'; + +--connection default + +--error ER_BACKUP_LOCK_IS_ACTIVE +evalp BACKUP SERVER TO '$MYSQLTEST_VARDIR/some_directory'; + +BACKUP STAGE END; +--connection backup +--replace_result $MYSQLTEST_VARDIR MYSQLTEST_VARDIR +--error 21 +evalp SET STATEMENT max_statement_time=0.1 FOR +BACKUP SERVER TO '$MYSQLTEST_VARDIR/some_directory'; +--disconnect backup +--connection default + +--rmdir $MYSQLTEST_VARDIR/some_directory + +--source include/wait_until_count_sessions.inc diff --git a/mysql-test/main/grant_backup_server.result b/mysql-test/main/grant_backup_server.result new file mode 100644 index 0000000000000..a4d85292dee45 --- /dev/null +++ b/mysql-test/main/grant_backup_server.result @@ -0,0 +1,27 @@ +CREATE USER user1@localhost IDENTIFIED BY ''; +connect con1,localhost,user1; +BACKUP SERVER TO 'some_directory'; +ERROR 42000: Access denied; you need (at least one of) the RELOAD privilege(s) for this operation +disconnect con1; +connection default; +GRANT SELECT ON test.* TO user1@localhost; +connect con1,localhost,user1; +BACKUP SERVER TO 'some_directory'; +ERROR 42000: Access denied; you need (at least one of) the RELOAD privilege(s) for this operation +disconnect con1; +connection default; +GRANT RELOAD ON test.* TO user1@localhost; +ERROR HY000: Incorrect usage of DB GRANT and GLOBAL PRIVILEGES +GRANT RELOAD ON *.* TO user1@localhost; +connect con1,localhost,user1; +BACKUP SERVER TO 'some_directory'; +ERROR 42000: Access denied; you need (at least one of) the SELECT privilege(s) for this operation +disconnect con1; +connection default; +GRANT SELECT ON *.* TO user1@localhost; +connect con1,localhost,user1; +BACKUP SERVER TO '$datadir/some_directory'; +ERROR HY000: Incorrect arguments to BACKUP SERVER TO +disconnect con1; +connection default; +DROP USER user1@localhost; diff --git a/mysql-test/main/grant_backup_server.test b/mysql-test/main/grant_backup_server.test new file mode 100644 index 0000000000000..726abb19908bb --- /dev/null +++ b/mysql-test/main/grant_backup_server.test @@ -0,0 +1,29 @@ +--source include/not_embedded.inc +CREATE USER user1@localhost IDENTIFIED BY ''; +--connect con1,localhost,user1 +--error ER_SPECIFIC_ACCESS_DENIED_ERROR +BACKUP SERVER TO 'some_directory'; +--disconnect con1 +--connection default +GRANT SELECT ON test.* TO user1@localhost; +--connect con1,localhost,user1 +--error ER_SPECIFIC_ACCESS_DENIED_ERROR +BACKUP SERVER TO 'some_directory'; +--disconnect con1 +--connection default +--error ER_WRONG_USAGE +GRANT RELOAD ON test.* TO user1@localhost; +GRANT RELOAD ON *.* TO user1@localhost; +--connect con1,localhost,user1 +--error ER_SPECIFIC_ACCESS_DENIED_ERROR +BACKUP SERVER TO 'some_directory'; +--disconnect con1 +--connection default +GRANT SELECT ON *.* TO user1@localhost; +--connect con1,localhost,user1 +--let $datadir=`select @@datadir` +--error ER_WRONG_ARGUMENTS +evalp BACKUP SERVER TO '$datadir/some_directory'; +--disconnect con1 +--connection default +DROP USER user1@localhost; diff --git a/mysql-test/main/mysqld--help.result b/mysql-test/main/mysqld--help.result index d87ce7f156a06..268efe5c8b960 100644 --- a/mysql-test/main/mysqld--help.result +++ b/mysql-test/main/mysqld--help.result @@ -2053,7 +2053,7 @@ performance-schema-max-socket-classes 10 performance-schema-max-socket-instances -1 performance-schema-max-sql-text-length 1024 performance-schema-max-stage-classes 170 -performance-schema-max-statement-classes 227 +performance-schema-max-statement-classes 228 performance-schema-max-statement-stack 10 performance-schema-max-table-handles -1 performance-schema-max-table-instances -1 diff --git a/mysql-test/mariadb-test-run.pl b/mysql-test/mariadb-test-run.pl index 7bea206fb2ff5..1182109f7119b 100755 --- a/mysql-test/mariadb-test-run.pl +++ b/mysql-test/mariadb-test-run.pl @@ -180,6 +180,7 @@ END main- archive- atomic- + backup- binlog- binlog_encryption- binlog_in_engine- diff --git a/mysql-test/suite/backup/backup_innodb,debug.rdiff b/mysql-test/suite/backup/backup_innodb,debug.rdiff new file mode 100644 index 0000000000000..02d25801b75a7 --- /dev/null +++ b/mysql-test/suite/backup/backup_innodb,debug.rdiff @@ -0,0 +1,16 @@ +--- backup_innodb.result ++++ backup_innodb,debug.result +@@ -10,7 +10,13 @@ + BEGIN; + DELETE FROM t; + connect backup,localhost,root; ++SET DEBUG_SYNC='innodb_backup_start SIGNAL start WAIT_FOR resume'; + BACKUP SERVER TO 'MYSQLTEST_VARDIR/some_directory'; ++connection default; ++SET DEBUG_SYNC='now WAIT_FOR start'; ++INSERT INTO t(a) SELECT * FROM seq_1_to_30000; ++SET DEBUG_SYNC='now SIGNAL resume'; ++connection backup; + disconnect backup; + connection default; + ROLLBACK; diff --git a/mysql-test/suite/backup/backup_innodb.combinations b/mysql-test/suite/backup/backup_innodb.combinations new file mode 100644 index 0000000000000..7fd419bba7692 --- /dev/null +++ b/mysql-test/suite/backup/backup_innodb.combinations @@ -0,0 +1,4 @@ +[archived] +innodb_log_archive=ON +[circular] +innodb_log_archive=OFF diff --git a/mysql-test/suite/backup/backup_innodb.result b/mysql-test/suite/backup/backup_innodb.result new file mode 100644 index 0000000000000..e1e856e93bae0 --- /dev/null +++ b/mysql-test/suite/backup/backup_innodb.result @@ -0,0 +1,24 @@ +CREATE TABLE t(a INT PRIMARY KEY, b CHAR(255) DEFAULT '' NOT NULL, INDEX(b)) +ENGINE=INNODB; +BEGIN; +INSERT INTO t SET a=1; +BACKUP SERVER TO '$MYSQLTEST_VARDIR/some_directory'; +ROLLBACK; +SELECT * FROM t; +a b +1 +BEGIN; +DELETE FROM t; +connect backup,localhost,root; +BACKUP SERVER TO 'MYSQLTEST_VARDIR/some_directory'; +disconnect backup; +connection default; +ROLLBACK; +SELECT * FROM t; +a b +1 +# restart +SELECT * FROM t; +a b +1 +DROP TABLE t; diff --git a/mysql-test/suite/backup/backup_innodb.test b/mysql-test/suite/backup/backup_innodb.test new file mode 100644 index 0000000000000..5f7bde34a31dd --- /dev/null +++ b/mysql-test/suite/backup/backup_innodb.test @@ -0,0 +1,49 @@ +--source include/have_sequence.inc +--source include/have_innodb.inc +--source include/maybe_debug.inc + +CREATE TABLE t(a INT PRIMARY KEY, b CHAR(255) DEFAULT '' NOT NULL, INDEX(b)) +ENGINE=INNODB; +BEGIN; +INSERT INTO t SET a=1; + +evalp BACKUP SERVER TO '$MYSQLTEST_VARDIR/some_directory'; +--rmdir $MYSQLTEST_VARDIR/some_directory +ROLLBACK; +# BACKUP SERVER will implicitly commit the current transaction +SELECT * FROM t; + +BEGIN; +DELETE FROM t; + +--connect backup,localhost,root +if ($have_debug) { +SET DEBUG_SYNC='innodb_backup_start SIGNAL start WAIT_FOR resume'; +--replace_result $MYSQLTEST_VARDIR MYSQLTEST_VARDIR +send_eval BACKUP SERVER TO '$MYSQLTEST_VARDIR/some_directory'; +--connection default +SET DEBUG_SYNC='now WAIT_FOR start'; +INSERT INTO t(a) SELECT * FROM seq_1_to_30000; +SET DEBUG_SYNC='now SIGNAL resume'; +--connection backup +--reap +} +if (!$have_debug) { +--replace_result $MYSQLTEST_VARDIR MYSQLTEST_VARDIR +eval BACKUP SERVER TO '$MYSQLTEST_VARDIR/some_directory'; +} + +--disconnect backup +--connection default +ROLLBACK; +SELECT * FROM t; + +let $datadir=`SELECT @@datadir`; +--source include/shutdown_mysqld.inc +#--rmdir $datadir +#--move_file $MYSQLTEST_VARDIR/some_directory $datadir +--rmdir $MYSQLTEST_VARDIR/some_directory +--source include/start_mysqld.inc + +SELECT * FROM t; +DROP TABLE t; diff --git a/mysql-test/suite/backup/suite.pm b/mysql-test/suite/backup/suite.pm new file mode 100644 index 0000000000000..e24cb5b5185b1 --- /dev/null +++ b/mysql-test/suite/backup/suite.pm @@ -0,0 +1,10 @@ +package My::Suite::Backup; + +@ISA = qw(My::Suite); +use My::Find; +use File::Basename; +use strict; + +return "Not run for embedded server" if $::opt_embedded_server; + +bless { }; diff --git a/mysql-test/suite/perfschema/r/max_program_zero.result b/mysql-test/suite/perfschema/r/max_program_zero.result index 047643e06988d..a0e486b2af9c0 100644 --- a/mysql-test/suite/perfschema/r/max_program_zero.result +++ b/mysql-test/suite/perfschema/r/max_program_zero.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 1 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/ortho_iter.result b/mysql-test/suite/perfschema/r/ortho_iter.result index 56c22c8453d8e..589704f4056de 100644 --- a/mysql-test/suite/perfschema/r/ortho_iter.result +++ b/mysql-test/suite/perfschema/r/ortho_iter.result @@ -251,7 +251,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/privilege_table_io.result b/mysql-test/suite/perfschema/r/privilege_table_io.result index 0c82be9f05810..b518052613308 100644 --- a/mysql-test/suite/perfschema/r/privilege_table_io.result +++ b/mysql-test/suite/perfschema/r/privilege_table_io.result @@ -57,7 +57,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_disable_idle.result b/mysql-test/suite/perfschema/r/start_server_disable_idle.result index d0665e3bf4c65..12b956d90769c 100644 --- a/mysql-test/suite/perfschema/r/start_server_disable_idle.result +++ b/mysql-test/suite/perfschema/r/start_server_disable_idle.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_disable_stages.result b/mysql-test/suite/perfschema/r/start_server_disable_stages.result index 2ef68328144ff..30ce9d12e56a0 100644 --- a/mysql-test/suite/perfschema/r/start_server_disable_stages.result +++ b/mysql-test/suite/perfschema/r/start_server_disable_stages.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_disable_statements.result b/mysql-test/suite/perfschema/r/start_server_disable_statements.result index 0ece2a0c52ed1..4bacdbdfedf68 100644 --- a/mysql-test/suite/perfschema/r/start_server_disable_statements.result +++ b/mysql-test/suite/perfschema/r/start_server_disable_statements.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_disable_transactions.result b/mysql-test/suite/perfschema/r/start_server_disable_transactions.result index ededc09aac95d..3a6831eb2c7ff 100644 --- a/mysql-test/suite/perfschema/r/start_server_disable_transactions.result +++ b/mysql-test/suite/perfschema/r/start_server_disable_transactions.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_disable_waits.result b/mysql-test/suite/perfschema/r/start_server_disable_waits.result index 23db9362161e4..a864576dfa979 100644 --- a/mysql-test/suite/perfschema/r/start_server_disable_waits.result +++ b/mysql-test/suite/perfschema/r/start_server_disable_waits.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_innodb.result b/mysql-test/suite/perfschema/r/start_server_innodb.result index cff87457b3bef..09cba65c57ce0 100644 --- a/mysql-test/suite/perfschema/r/start_server_innodb.result +++ b/mysql-test/suite/perfschema/r/start_server_innodb.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_low_index.result b/mysql-test/suite/perfschema/r/start_server_low_index.result index 11cade0a2132f..dbd36d2eaa92d 100644 --- a/mysql-test/suite/perfschema/r/start_server_low_index.result +++ b/mysql-test/suite/perfschema/r/start_server_low_index.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_low_table_lock.result b/mysql-test/suite/perfschema/r/start_server_low_table_lock.result index 484550095202e..fab64f49d45e6 100644 --- a/mysql-test/suite/perfschema/r/start_server_low_table_lock.result +++ b/mysql-test/suite/perfschema/r/start_server_low_table_lock.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_no_account.result b/mysql-test/suite/perfschema/r/start_server_no_account.result index aab8d3eba9caa..e2cccdba19b43 100644 --- a/mysql-test/suite/perfschema/r/start_server_no_account.result +++ b/mysql-test/suite/perfschema/r/start_server_no_account.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_no_cond_class.result b/mysql-test/suite/perfschema/r/start_server_no_cond_class.result index 4dfdab9de9f30..44b114013ace2 100644 --- a/mysql-test/suite/perfschema/r/start_server_no_cond_class.result +++ b/mysql-test/suite/perfschema/r/start_server_no_cond_class.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_no_cond_inst.result b/mysql-test/suite/perfschema/r/start_server_no_cond_inst.result index a8d0cbeca3855..27ecb59a40f17 100644 --- a/mysql-test/suite/perfschema/r/start_server_no_cond_inst.result +++ b/mysql-test/suite/perfschema/r/start_server_no_cond_inst.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_no_file_class.result b/mysql-test/suite/perfschema/r/start_server_no_file_class.result index fcc01880a7107..5189d36618b05 100644 --- a/mysql-test/suite/perfschema/r/start_server_no_file_class.result +++ b/mysql-test/suite/perfschema/r/start_server_no_file_class.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_no_file_inst.result b/mysql-test/suite/perfschema/r/start_server_no_file_inst.result index c56201e7d0a80..533c02383c7b8 100644 --- a/mysql-test/suite/perfschema/r/start_server_no_file_inst.result +++ b/mysql-test/suite/perfschema/r/start_server_no_file_inst.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_no_host.result b/mysql-test/suite/perfschema/r/start_server_no_host.result index 662beb3b88a49..e328ea3d3c26a 100644 --- a/mysql-test/suite/perfschema/r/start_server_no_host.result +++ b/mysql-test/suite/perfschema/r/start_server_no_host.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_no_index.result b/mysql-test/suite/perfschema/r/start_server_no_index.result index ccff0cb113faa..686f430cdc440 100644 --- a/mysql-test/suite/perfschema/r/start_server_no_index.result +++ b/mysql-test/suite/perfschema/r/start_server_no_index.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_no_mdl.result b/mysql-test/suite/perfschema/r/start_server_no_mdl.result index ebe64409deb0c..c2b6dc3d9eb74 100644 --- a/mysql-test/suite/perfschema/r/start_server_no_mdl.result +++ b/mysql-test/suite/perfschema/r/start_server_no_mdl.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_no_memory_class.result b/mysql-test/suite/perfschema/r/start_server_no_memory_class.result index 01a217c534bfd..2a4cdcf8ddd37 100644 --- a/mysql-test/suite/perfschema/r/start_server_no_memory_class.result +++ b/mysql-test/suite/perfschema/r/start_server_no_memory_class.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_no_mutex_class.result b/mysql-test/suite/perfschema/r/start_server_no_mutex_class.result index 1b3efda5210a9..9b77c7b897c47 100644 --- a/mysql-test/suite/perfschema/r/start_server_no_mutex_class.result +++ b/mysql-test/suite/perfschema/r/start_server_no_mutex_class.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_no_mutex_inst.result b/mysql-test/suite/perfschema/r/start_server_no_mutex_inst.result index 599498915f3f1..3a62653efc18f 100644 --- a/mysql-test/suite/perfschema/r/start_server_no_mutex_inst.result +++ b/mysql-test/suite/perfschema/r/start_server_no_mutex_inst.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_no_prepared_stmts_instances.result b/mysql-test/suite/perfschema/r/start_server_no_prepared_stmts_instances.result index 73ac1acb9f145..7dd5842809135 100644 --- a/mysql-test/suite/perfschema/r/start_server_no_prepared_stmts_instances.result +++ b/mysql-test/suite/perfschema/r/start_server_no_prepared_stmts_instances.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_no_rwlock_class.result b/mysql-test/suite/perfschema/r/start_server_no_rwlock_class.result index 04aa037b960e3..3598c722b9453 100644 --- a/mysql-test/suite/perfschema/r/start_server_no_rwlock_class.result +++ b/mysql-test/suite/perfschema/r/start_server_no_rwlock_class.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_no_rwlock_inst.result b/mysql-test/suite/perfschema/r/start_server_no_rwlock_inst.result index e8156711eb3f2..7a60805d2d03f 100644 --- a/mysql-test/suite/perfschema/r/start_server_no_rwlock_inst.result +++ b/mysql-test/suite/perfschema/r/start_server_no_rwlock_inst.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_no_setup_actors.result b/mysql-test/suite/perfschema/r/start_server_no_setup_actors.result index 2d17bd6f49203..76da2883ba6b2 100644 --- a/mysql-test/suite/perfschema/r/start_server_no_setup_actors.result +++ b/mysql-test/suite/perfschema/r/start_server_no_setup_actors.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_no_setup_objects.result b/mysql-test/suite/perfschema/r/start_server_no_setup_objects.result index 16afee28ee70b..58ac763394a96 100644 --- a/mysql-test/suite/perfschema/r/start_server_no_setup_objects.result +++ b/mysql-test/suite/perfschema/r/start_server_no_setup_objects.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_no_socket_class.result b/mysql-test/suite/perfschema/r/start_server_no_socket_class.result index 9de0006aa7abc..5a86d51a06231 100644 --- a/mysql-test/suite/perfschema/r/start_server_no_socket_class.result +++ b/mysql-test/suite/perfschema/r/start_server_no_socket_class.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 0 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_no_socket_inst.result b/mysql-test/suite/perfschema/r/start_server_no_socket_inst.result index bcef0e5c01b05..75ab84f4b3cb8 100644 --- a/mysql-test/suite/perfschema/r/start_server_no_socket_inst.result +++ b/mysql-test/suite/perfschema/r/start_server_no_socket_inst.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 0 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_no_stage_class.result b/mysql-test/suite/perfschema/r/start_server_no_stage_class.result index 1dda39dc79e92..bb750ebd34a26 100644 --- a/mysql-test/suite/perfschema/r/start_server_no_stage_class.result +++ b/mysql-test/suite/perfschema/r/start_server_no_stage_class.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 0 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_no_stages_history.result b/mysql-test/suite/perfschema/r/start_server_no_stages_history.result index 95584521eceb2..6cf3f740fb1be 100644 --- a/mysql-test/suite/perfschema/r/start_server_no_stages_history.result +++ b/mysql-test/suite/perfschema/r/start_server_no_stages_history.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_no_stages_history_long.result b/mysql-test/suite/perfschema/r/start_server_no_stages_history_long.result index da6c54b6bbafa..1f2716410a9bc 100644 --- a/mysql-test/suite/perfschema/r/start_server_no_stages_history_long.result +++ b/mysql-test/suite/perfschema/r/start_server_no_stages_history_long.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_no_statements_history.result b/mysql-test/suite/perfschema/r/start_server_no_statements_history.result index 09a9a544f5b13..f1078542f69ef 100644 --- a/mysql-test/suite/perfschema/r/start_server_no_statements_history.result +++ b/mysql-test/suite/perfschema/r/start_server_no_statements_history.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_no_statements_history_long.result b/mysql-test/suite/perfschema/r/start_server_no_statements_history_long.result index 5ce9874799e8b..a6fc523df2c60 100644 --- a/mysql-test/suite/perfschema/r/start_server_no_statements_history_long.result +++ b/mysql-test/suite/perfschema/r/start_server_no_statements_history_long.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_no_table_hdl.result b/mysql-test/suite/perfschema/r/start_server_no_table_hdl.result index a170fe097fd23..304732cb37927 100644 --- a/mysql-test/suite/perfschema/r/start_server_no_table_hdl.result +++ b/mysql-test/suite/perfschema/r/start_server_no_table_hdl.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 0 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_no_table_inst.result b/mysql-test/suite/perfschema/r/start_server_no_table_inst.result index 3f009de021d1a..3ef43495d0f22 100644 --- a/mysql-test/suite/perfschema/r/start_server_no_table_inst.result +++ b/mysql-test/suite/perfschema/r/start_server_no_table_inst.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 0 diff --git a/mysql-test/suite/perfschema/r/start_server_no_table_lock.result b/mysql-test/suite/perfschema/r/start_server_no_table_lock.result index db4fe3413106b..20cf1a531ca51 100644 --- a/mysql-test/suite/perfschema/r/start_server_no_table_lock.result +++ b/mysql-test/suite/perfschema/r/start_server_no_table_lock.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_no_thread_class.result b/mysql-test/suite/perfschema/r/start_server_no_thread_class.result index b1b6e614c43ac..e8eb4589fce56 100644 --- a/mysql-test/suite/perfschema/r/start_server_no_thread_class.result +++ b/mysql-test/suite/perfschema/r/start_server_no_thread_class.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_no_thread_inst.result b/mysql-test/suite/perfschema/r/start_server_no_thread_inst.result index e540f3ce78cbd..4dbf292fc3c10 100644 --- a/mysql-test/suite/perfschema/r/start_server_no_thread_inst.result +++ b/mysql-test/suite/perfschema/r/start_server_no_thread_inst.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_no_transactions_history.result b/mysql-test/suite/perfschema/r/start_server_no_transactions_history.result index 80da238ba0ed7..09168c68f9fe8 100644 --- a/mysql-test/suite/perfschema/r/start_server_no_transactions_history.result +++ b/mysql-test/suite/perfschema/r/start_server_no_transactions_history.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_no_transactions_history_long.result b/mysql-test/suite/perfschema/r/start_server_no_transactions_history_long.result index f5d32f0100968..9ec63e991deda 100644 --- a/mysql-test/suite/perfschema/r/start_server_no_transactions_history_long.result +++ b/mysql-test/suite/perfschema/r/start_server_no_transactions_history_long.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_no_user.result b/mysql-test/suite/perfschema/r/start_server_no_user.result index cb249b4e242d3..861b7d897c9c5 100644 --- a/mysql-test/suite/perfschema/r/start_server_no_user.result +++ b/mysql-test/suite/perfschema/r/start_server_no_user.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_no_waits_history.result b/mysql-test/suite/perfschema/r/start_server_no_waits_history.result index c419aad2995f3..710cef232da58 100644 --- a/mysql-test/suite/perfschema/r/start_server_no_waits_history.result +++ b/mysql-test/suite/perfschema/r/start_server_no_waits_history.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_no_waits_history_long.result b/mysql-test/suite/perfschema/r/start_server_no_waits_history_long.result index 0b2bf39ff874f..df4351a8f358f 100644 --- a/mysql-test/suite/perfschema/r/start_server_no_waits_history_long.result +++ b/mysql-test/suite/perfschema/r/start_server_no_waits_history_long.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_off.result b/mysql-test/suite/perfschema/r/start_server_off.result index 17db0395d894b..f3bf7bf7fb15e 100644 --- a/mysql-test/suite/perfschema/r/start_server_off.result +++ b/mysql-test/suite/perfschema/r/start_server_off.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_on.result b/mysql-test/suite/perfschema/r/start_server_on.result index cff87457b3bef..09cba65c57ce0 100644 --- a/mysql-test/suite/perfschema/r/start_server_on.result +++ b/mysql-test/suite/perfschema/r/start_server_on.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/start_server_variables.result b/mysql-test/suite/perfschema/r/start_server_variables.result index f25f9ab69d1c3..b86116e099cdd 100644 --- a/mysql-test/suite/perfschema/r/start_server_variables.result +++ b/mysql-test/suite/perfschema/r/start_server_variables.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 10 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/perfschema/r/statement_program_lost_inst.result b/mysql-test/suite/perfschema/r/statement_program_lost_inst.result index 442e212557b55..3e8469b881801 100644 --- a/mysql-test/suite/perfschema/r/statement_program_lost_inst.result +++ b/mysql-test/suite/perfschema/r/statement_program_lost_inst.result @@ -129,7 +129,7 @@ performance_schema_max_socket_classes 10 performance_schema_max_socket_instances 1000 performance_schema_max_sql_text_length 1024 performance_schema_max_stage_classes 170 -performance_schema_max_statement_classes 227 +performance_schema_max_statement_classes 228 performance_schema_max_statement_stack 2 performance_schema_max_table_handles 1000 performance_schema_max_table_instances 500 diff --git a/mysql-test/suite/sys_vars/r/sysvars_innodb.result b/mysql-test/suite/sys_vars/r/sysvars_innodb.result index 11a75e91ae34a..d8afac2c2c63b 100644 --- a/mysql-test/suite/sys_vars/r/sysvars_innodb.result +++ b/mysql-test/suite/sys_vars/r/sysvars_innodb.result @@ -1052,7 +1052,7 @@ NUMERIC_MIN_VALUE 0 NUMERIC_MAX_VALUE 18446744073709551615 NUMERIC_BLOCK_SIZE 0 ENUM_VALUE_LIST NULL -READ_ONLY YES +READ_ONLY NO COMMAND_LINE_ARGUMENT REQUIRED VARIABLE_NAME INNODB_LOG_RECOVERY_TARGET SESSION_VALUE NULL diff --git a/sql/CMakeLists.txt b/sql/CMakeLists.txt index e54e894e1d0fc..fd16d901d64fb 100644 --- a/sql/CMakeLists.txt +++ b/sql/CMakeLists.txt @@ -163,6 +163,7 @@ SET (SQL_SOURCE grant.cc sql_explain.cc sql_analyze_stmt.cc + sql_backup.cc sql_join_cache.cc create_options.cc multi_range_read.cc opt_histogram_json.cc diff --git a/sql/handler.h b/sql/handler.h index 3ab9e0bcd1a8e..e9a2f7e164e08 100644 --- a/sql/handler.h +++ b/sql/handler.h @@ -1496,6 +1496,27 @@ struct transaction_participant ulonglong (*prepare_commit_versioned)(THD *thd, ulonglong *trx_id); }; +/** BACKUP SERVER target */ +struct backup_target +{ +#ifdef _WIN32 + /** Target directory path name */ + const char *path; + union + { + /** Target pipe, if path==reinterpret_cast(-1) */ + HANDLE pipe; + /** Target socket, if path==nullptr */ + SOCKET socket; + }; +#else + /** Target file descriptor */ + int fd; + /** whether the fd is a directory handle */ + bool directory; +#endif +}; + /* handlerton is a singleton structure - one instance per storage engine - to provide access to storage engine functionality that works on the @@ -1892,9 +1913,44 @@ struct handlerton : public transaction_participant /********************************************************************* backup **********************************************************************/ + + /** BACKUP STAGE START */ void (*prepare_for_backup)(void); + /** BACKUP STAGE END */ void (*end_backup)(void); + /** + Start of BACKUP SERVER: collect all files to be backed up + @param thd current session + @param target backup target + @return error code + @retval 0 on success + */ + int (*backup_start)(THD *thd, backup_target target); + /** + Process a file that was collected in backup_start(). + @param thd current session + @return number of files remaining, or negative on error + @retval 0 on completion + */ + int (*backup_step)(THD *thd); + /** + Finish copying and determine the logical time of the backup snapshot. + @param thd current sesssion + @param abort whether BACKUP SERVER was aborted + @return error code + @retval 0 on success + */ + int (*backup_end)(THD *thd, bool abort); + /** + Clean up after any backup_end(). + @param thd the parameter on which backup_end() was invoked + @param target backup target + @return error code + @retval 0 on success + */ + int (*backup_finalize)(THD *thd, backup_target target); + /********************************************************************** WSREP specific **********************************************************************/ diff --git a/sql/mysqld.cc b/sql/mysqld.cc index de79500825457..111fcb0aa7505 100644 --- a/sql/mysqld.cc +++ b/sql/mysqld.cc @@ -3543,6 +3543,7 @@ SHOW_VAR com_status_vars[]= { {"assign_to_keycache", STMT_STATUS(SQLCOM_ASSIGN_TO_KEYCACHE)}, {"backup", STMT_STATUS(SQLCOM_BACKUP)}, {"backup_lock", STMT_STATUS(SQLCOM_BACKUP_LOCK)}, + {"backup_server", STMT_STATUS(SQLCOM_BACKUP_SERVER)}, {"begin", STMT_STATUS(SQLCOM_BEGIN)}, {"binlog", STMT_STATUS(SQLCOM_BINLOG_BASE64_EVENT)}, {"call_procedure", STMT_STATUS(SQLCOM_CALL)}, diff --git a/sql/sql_backup.cc b/sql/sql_backup.cc new file mode 100644 index 0000000000000..607116e725441 --- /dev/null +++ b/sql/sql_backup.cc @@ -0,0 +1,373 @@ +/* Copyright (c) 2026, MariaDB plc + + 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 Foundation; version 2 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1335 USA */ + +#include "my_global.h" +#include "mdl.h" +#include "mysys_err.h" +#include "sql_class.h" +#include "sql_backup.h" +#include "sql_backup_interface.h" +#include "sql_parse.h" +#ifdef _WIN32 +# include "aligned.h" +# include "tpool.h" +#endif + +#if defined __linux__ || defined __FreeBSD__ +using copying_step= ssize_t(int,int,size_t,off_t*); +template +static ssize_t copy(int in_fd, int out_fd, off_t offset, off_t end) noexcept +{ + for (;;) + { + const size_t c{size_t(std::min(end - offset, INT_MAX >> 20 << 20))}; + ssize_t ret= step(in_fd, out_fd, c, &offset); + if (ret < 0) + return ret; + if (offset == end) + return 0; + if (!ret) + return -1; + } +} + +/* Copy between files in a single (type of) file system */ +static inline ssize_t +copy_step(int in_fd, int out_fd, size_t count, off_t *offset) noexcept +{ + return copy_file_range(in_fd, offset, out_fd, offset, count, 0); +} +# define cfr(src,dst,start,end) copy(src, dst, start, end) +#endif +#ifdef __linux__ +# include +/* Copy a file to a stream or to a regular file. */ +static inline ssize_t +send_step(int in_fd, int out_fd, size_t count, off_t *offset) noexcept +{ + return sendfile(out_fd, in_fd, offset, count); +} +#else +# ifndef _WIN32 +# include "aligned.h" +# include +/** Copy a file using a memory mapping. +@param in_fd source file +@param out_fd destination +@param o start offset +@param end last offset (exclusive) +@return error code +@retval 0 on success +@retval 1 if a memory mapping failed */ +static ssize_t mmap_copy(int in_fd, int out_fd, uint64_t o, uint64_t end) +{ +#if SIZEOF_SIZE_T < 8 + if (end != ssize_t(end)) + return 1; +#endif + const size_t count= size_t(end - o); + void *p= mmap(nullptr, count, PROT_READ, MAP_SHARED, in_fd, off_t(o)); + if (p == MAP_FAILED) + return 1; + ssize_t ret; + size_t c{count}; + for (const char *b= static_cast(p);; b+= ret, o+= uint64_t(ret)) + { + ret= pwrite(out_fd, b, std::min(c, size_t(INT_MAX >> 20 << 20)), off_t(o)); + if (ret < 0) + break; + c-= ret; + if (!c) + { + ret= 0; + break; + } + if (!ret) + { + ret= -1; + break; + } + } + munmap(p, c); + return ret; +} +# endif + +/** Copy a file using positioned reads and writes. +@param in_fd source file +@param out_fd destination +@param o start offset +@param end last offset (exclusive) +@return error code +@retval 0 on success +@retval 1 if a memory mapping failed */ +static ssize_t pread_pwrite(IF_WIN(const native_file_handle&,int) in_fd, + IF_WIN(const native_file_handle&,int) out_fd, + uint64_t o, uint64_t end) + noexcept +{ +#ifdef _WIN32 + using tpool::pread; + using tpool::pwrite; +#endif + constexpr size_t READ_WRITE_SIZE= 65536; + char *b= static_cast(aligned_malloc(READ_WRITE_SIZE, 4096)); + if (!b) + return -1; + ssize_t ret; + for (uint64_t count{end - o};; o+= ret) + { + ret= pread(in_fd, b, + ssize_t(std::min(count, READ_WRITE_SIZE)), o); + if (ret > 0) + ret= pwrite(out_fd, b, ret, o); + if (ret < 0) + break; + count-= uint64_t(ret); + if (!count) + { + ret= 0; + break; + } + if (!ret) + { + ret= -1; + break; + } + } + aligned_free(b); + return ret; +} +#endif + +#ifdef __APPLE__ +/* The inline copy_entire_file() invokes fcopyfile() */ +#elif defined _WIN32 +/* CopyFileEx() should be used */ +#else +/** Copy a file (whole content). +@param src source file descriptor +@param dst target to append src to +@return error code (non-positive) +@retval 0 on success */ +extern "C" int copy_entire_file(int src, int dst) +{ + return copy_file(src, dst, 0, lseek(src, 0, SEEK_END)); +} +#endif + +/** Copy a portion of a file. +@param src source file descriptor +@param dst target to append src to +@param start first offset to copy +@param end last offset to copy (exclusive) +@return error code (non-positive) +@retval 0 on success */ +extern "C" int copy_file(IF_WIN(const native_file_handle&,int) src, + IF_WIN(const native_file_handle&,int) dst, + uint64_t start, uint64_t end) +{ + assert(end >= start); + ssize_t ret; +# ifdef cfr + if (!(ret= cfr(src, dst, off_t(start), off_t(end)))) + return int(ret); +# ifdef __linux__ + if (errno == EOPNOTSUPP || errno == EXDEV) +# endif +# endif +# ifdef __linux__ // starting with Linux 2.6.33, we can rely on sendfile(2) + ret= (start != 0 && off_t(start) != lseek(dst, start, SEEK_SET)) + ? -1 + : copy(src, dst, off_t(start), off_t(end)); +# else +# ifndef _WIN32 + if ((ret= mmap_copy(src, dst, start, end)) == 1) +# endif + ret= pread_pwrite(src, dst, start, end); +# endif + assert(ret <= 0); + return int(ret); +} + +/** Append to the configuration file. +@param target backup target +@param config the configuration file snippet to append +@param size length of the snippet +@return error code (non-positive) +@retval 0 on success */ +extern "C" int backup_config_append(const backup_target &target, + const char *config, size_t size) +{ + /* FIXME: append to a pre-created configuration file */ +#ifdef _WIN32 + HANDLE dst; + { + std::string path{target.path}; + path.append("/backup.cnf"); + dst= CreateFile(path.c_str(), GENERIC_WRITE, 0, + my_win_file_secattr(), CREATE_NEW, + FILE_ATTRIBUTE_NORMAL, nullptr); + if (dst != INVALID_HANDLE_VALUE) + { + BOOL ok; + for (;;) + { + DWORD written; + ok= WriteFile(dst, config, DWORD(size), &written, nullptr); + if (ok || !written || GetLastError() != ERROR_IO_PENDING) + break; + assert(written < DWORD(size)); + config+= written; + size-= size_t(written); + } + if (CloseHandle(dst) & ok) + return 0; + } + } +#else + assert(target.directory); + int dst= openat(target.fd, "backup.cnf", + O_CREAT | O_EXCL | O_TRUNC | O_WRONLY, 0666); + if (dst < 0) + return dst; + ssize_t ret; + for (; (ret= write(dst, config, size)) >= 0; config+= ret, size -= ret) + { + assert(size_t(ret) <= size); + if (!(size-= size_t(ret))) + { + ret= 0; + break; + } + } + if (!(close(dst) | ret)) + return 0; +#endif + my_error(ER_CANT_CREATE_FILE, MYF(0), "backup.cnf", errno); + return -1; +} + +static my_bool backup_start(THD *thd, plugin_ref plugin, void *dst) noexcept +{ + handlerton *hton= plugin_hton(plugin); + if (hton->backup_start) + return hton->backup_start(thd, *static_cast(dst)); + return false; +} + +static my_bool backup_end(THD *thd, plugin_ref plugin, void *arg) noexcept +{ + handlerton *hton= plugin_hton(plugin); + if (hton->backup_end) + return hton->backup_end(thd, arg != nullptr); + return false; +} + +static my_bool backup_step(THD *thd, plugin_ref plugin, void *) noexcept +{ + handlerton *hton= plugin_hton(plugin); + int res= 0; + if (hton->backup_step) + while ((res= hton->backup_step(thd))) + if (res < 0) + break; + return res != 0; +} + +static my_bool backup_finalize(THD *thd, plugin_ref plugin, void *dst) noexcept +{ + handlerton *hton= plugin_hton(plugin); + if (hton->backup_finalize) + return hton->backup_finalize(thd, *static_cast(dst)); + return 0; +} + +bool Sql_cmd_backup::execute(THD *thd) +{ + if (check_global_access(thd, RELOAD_ACL) || + check_global_access(thd, SELECT_ACL) || + error_if_data_home_dir(target.str, "BACKUP SERVER TO")) + return true; + + if (thd->current_backup_stage != BACKUP_FINISHED) + { + my_error(ER_BACKUP_LOCK_IS_ACTIVE, MYF(0)); + return true; + } + + /* Block concurrent BACKUP SERVER and BACKUP STAGE */ + MDL_request mdl_request; + MDL_REQUEST_INIT(&mdl_request, MDL_key::BACKUP, "", "", MDL_BACKUP_START, + MDL_EXPLICIT); + + if (thd->mdl_context.acquire_lock(&mdl_request, + thd->variables.lock_wait_timeout)) + return true; + + if (my_mkdir(target.str, 0755, MYF(MY_WME))) + { +#ifndef _WIN32 + err_exit: +#endif + thd->mdl_context.release_lock(mdl_request.ticket); + return true; + } + +#ifdef _WIN32 + backup_target dir{target.str, INVALID_HANDLE_VALUE}; +#else + backup_target dir{open(target.str, O_DIRECTORY), true}; + if (dir.fd < 0) + { + my_error(EE_CANT_MKDIR, MYF(ME_BELL), target.str, errno); + goto err_exit; + } +#endif + + bool fail= plugin_foreach_with_mask(thd, backup_start, + MYSQL_STORAGE_ENGINE_PLUGIN, + PLUGIN_IS_DELETED|PLUGIN_IS_READY, &dir); + + /* The backup_step may be invoked in multiple concurrent threads. + At the time backup_end is invoked, all backup_step will have to complete. */ + if (!fail) + fail= plugin_foreach_with_mask(thd, backup_step, + MYSQL_STORAGE_ENGINE_PLUGIN, + PLUGIN_IS_DELETED|PLUGIN_IS_READY, nullptr); + + fail= + thd->mdl_context.upgrade_shared_lock(mdl_request.ticket, + MDL_BACKUP_WAIT_COMMIT, + thd->variables.lock_wait_timeout) || + plugin_foreach_with_mask(thd, backup_end, MYSQL_STORAGE_ENGINE_PLUGIN, + PLUGIN_IS_DELETED|PLUGIN_IS_READY, + reinterpret_cast(fail)) || fail; + + /* The final part must not interfere with the use of the server datadir. + Release the locks. */ + thd->mdl_context.release_lock(mdl_request.ticket); + fail= plugin_foreach_with_mask(thd, backup_finalize, + MYSQL_STORAGE_ENGINE_PLUGIN, + PLUGIN_IS_DELETED|PLUGIN_IS_READY, &dir) || + fail; +#ifndef _WIN32 + close(dir.fd); +#endif + + if (!fail) + my_ok(thd); + return fail; +} diff --git a/sql/sql_backup.h b/sql/sql_backup.h new file mode 100644 index 0000000000000..9aba2404dac58 --- /dev/null +++ b/sql/sql_backup.h @@ -0,0 +1,36 @@ +/***************************************************************************** +Copyright (c) 2026 MariaDB plc. + +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 +Foundation; version 2 of the License. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1335 USA + +*****************************************************************************/ + +#pragma once + +/** BACKUP SERVER */ +class Sql_cmd_backup : public Sql_cmd +{ + /** target directory */ + const LEX_CSTRING target; + +public: + explicit Sql_cmd_backup(LEX_CSTRING target) : target(target) {} + ~Sql_cmd_backup() = default; + + bool execute(THD *thd) override; + + enum_sql_command sql_command_code() const override + { + return SQLCOM_BACKUP_SERVER; + } +}; diff --git a/sql/sql_backup_interface.h b/sql/sql_backup_interface.h new file mode 100644 index 0000000000000..478107e1e2e50 --- /dev/null +++ b/sql/sql_backup_interface.h @@ -0,0 +1,71 @@ +/* Copyright (c) 2026, MariaDB plc + + 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 Foundation; version 2 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1335 USA */ + +struct backup_target; +#ifdef _WIN32 +/* Use CopyFileEx() to copy entire files */ +struct native_file_handle; +#elif defined __APPLE__ +/* You should invoke fclonefileat(2) manually before attempting +copy_entire_file() or copy_file() */ +# include +# include +# include +/** Copy an entire file. +@param src source file descriptor +@param dst target to append src to +@return error code (negative) +@retval 0 on success */ +inline int copy_entire_file(int src, int dst) +{ + return fcopyfile(src, dst, NULL, COPYFILE_ALL | COPYFILE_CLONE); +} +#else +# ifdef __cplusplus +extern "C" +# endif +/** Copy an entire file. +@param src source file descriptor +@param dst target to append src to +@return error code (non-positive) +@retval 0 on success */ +int copy_entire_file(int src, int dst); +#endif + +#ifdef __cplusplus +extern "C" +#endif +/** Copy a portion of a file. +@param src source file descriptor +@param dst target to append src to +@param start first offset to copy +@param end last offset to copy (exclusive) +@return error code (non-positive) +@retval 0 on success */ +int copy_file(IF_WIN(const native_file_handle&,int) src, + IF_WIN(const native_file_handle&,int) dst, + uint64_t start, uint64_t end); + +#ifdef __cplusplus +extern "C" +#endif +/** Append to the configuration file. +@param target backup target +@param config the configuration file snippet to append +@param size length of the snippet +@return error code (non-positive) +@retval 0 on success */ +int backup_config_append(const backup_target &target, + const char *config, size_t size); diff --git a/sql/sql_command.h b/sql/sql_command.h index 9c9166706a034..b8903399711f0 100644 --- a/sql/sql_command.h +++ b/sql/sql_command.h @@ -103,6 +103,7 @@ enum enum_sql_command { SQLCOM_SHOW_PACKAGE_BODY_CODE, SQLCOM_BACKUP, SQLCOM_BACKUP_LOCK, SQLCOM_SHOW_CREATE_SERVER, + SQLCOM_BACKUP_SERVER, /* When a command is added here, be sure it's also added in mysqld.cc diff --git a/sql/sql_parse.cc b/sql/sql_parse.cc index bcd6de564f550..b549017622332 100644 --- a/sql/sql_parse.cc +++ b/sql/sql_parse.cc @@ -781,6 +781,7 @@ void init_update_queries(void) sql_command_flags[SQLCOM_DROP_SERVER]|= CF_AUTO_COMMIT_TRANS; sql_command_flags[SQLCOM_BACKUP]= CF_AUTO_COMMIT_TRANS; sql_command_flags[SQLCOM_BACKUP_LOCK]= CF_AUTO_COMMIT_TRANS; + sql_command_flags[SQLCOM_BACKUP_SERVER]= CF_AUTO_COMMIT_TRANS; /* The following statements can deal with temporary tables, @@ -5899,6 +5900,7 @@ mysql_execute_command(THD *thd, bool is_called_from_prepared_stmt) case SQLCOM_CALL: case SQLCOM_REVOKE: case SQLCOM_GRANT: + case SQLCOM_BACKUP_SERVER: if (thd->variables.option_bits & OPTION_IF_EXISTS) lex->create_info.set(DDL_options_st::OPT_IF_EXISTS); DBUG_ASSERT(lex->m_sql_cmd != NULL); @@ -10254,7 +10256,7 @@ int test_if_data_home_dir(const char *dir) if (!dir) DBUG_RETURN(0); - (void) fn_format(path, dir, "", "", MY_RETURN_REAL_PATH); + (void) fn_format(path, dir, "", "", MY_RETURN_REAL_PATH|MY_RESOLVE_SYMLINKS); DBUG_RETURN(path_starts_from_data_home_dir(path)); } diff --git a/sql/sql_yacc.yy b/sql/sql_yacc.yy index df4395395dcde..ec2c3449f6c53 100644 --- a/sql/sql_yacc.yy +++ b/sql/sql_yacc.yy @@ -50,6 +50,7 @@ #include "sql_alter.h" // Sql_cmd_alter_table* #include "sql_truncate.h" // Sql_cmd_truncate_table #include "sql_admin.h" // Sql_cmd_analyze/Check..._table +#include "sql_backup.h" #include "sql_partition_admin.h" // Sql_cmd_alter_table_*_part. #include "sql_handler.h" // Sql_cmd_handler_* #include "sql_signal.h" @@ -15557,6 +15558,11 @@ backup_statements: /* Table list is empty for unlock */ Lex->sql_command= SQLCOM_BACKUP_LOCK; } + | SERVER_SYM TO_SYM TEXT_STRING_sys + { + Lex->sql_command= SQLCOM_BACKUP_SERVER; + Lex->m_sql_cmd= new (thd->mem_root) Sql_cmd_backup($3); + } ; opt_delete_gtid_domain: diff --git a/sql/sys_vars.inl b/sql/sys_vars.inl index 0f8aa1eb63bf6..8e5f0b984e81e 100644 --- a/sql/sys_vars.inl +++ b/sql/sys_vars.inl @@ -2506,6 +2506,7 @@ public: bool session_update(THD *thd, set_var *var) override; }; +#ifdef HAVE_REPLICATION /* Class for replicate_events_marked_for_skip. We need a custom update function that ensures the slave is stopped when @@ -2647,6 +2648,7 @@ public: } const uchar *global_value_ptr(THD *thd, const LEX_CSTRING *base) const override; }; +#endif /* HAVE_REPLICATION */ /** diff --git a/storage/innobase/CMakeLists.txt b/storage/innobase/CMakeLists.txt index 9e3a23b34ab46..d63751a16af08 100644 --- a/storage/innobase/CMakeLists.txt +++ b/storage/innobase/CMakeLists.txt @@ -185,6 +185,8 @@ SET(INNOBASE_SOURCES handler/handler0alter.cc handler/innodb_binlog.cc handler/i_s.cc + handler/backup_innodb.h + handler/backup_innodb.cc ibuf/ibuf0ibuf.cc include/btr0btr.h include/btr0btr.inl diff --git a/storage/innobase/buf/buf0flu.cc b/storage/innobase/buf/buf0flu.cc index 326374cd2d194..cd3fc58c830b4 100644 --- a/storage/innobase/buf/buf0flu.cc +++ b/storage/innobase/buf/buf0flu.cc @@ -35,6 +35,7 @@ Created 11/11/1995 Heikki Tuuri #include "buf0buf.h" #include "buf0checksum.h" #include "buf0dblwr.h" +#include "backup_innodb.h" #include "srv0start.h" #include "page0zip.h" #include "fil0fil.h" @@ -954,6 +955,13 @@ uint32_t fil_space_t::flush_freed(bool writable) noexcept mysql_mutex_assert_not_owner(&buf_pool.flush_list_mutex); mysql_mutex_assert_not_owner(&buf_pool.mutex); + /* Note: There is no need to invoke writing_start() or + writing_stop() here, because we are only overwriting freed (garbage) + pages. If backup reads a torn page, it will also have copied a + corresponding FREE_PAGE record, which would be applied on recovery. + Besides, the freed page should never be reachable from other pages + that are part of the snapshot. */ + const bool punch_hole= chain.start->punch_hole == 1; if (!punch_hole && !srv_immediate_scrub_data_uncompressed) return 0; @@ -1229,6 +1237,16 @@ ATTRIBUTE_COLD static size_t buf_flush_LRU_to_withdraw(size_t to_withdraw, return to_withdraw; } +/** Stop writing to a tablespace. +@param space tablespace +@return nullptr */ +static fil_space_t *writing_stop(fil_space_t *space) noexcept +{ + space->writing_stop(); + space->release(); + return nullptr; +} + /** Flush dirty blocks from the end buf_pool.LRU, and move clean blocks to buf_pool.free. @param max maximum number of blocks to flush @@ -1246,6 +1264,7 @@ static void buf_flush_LRU_list_batch(ulint max, flush_counters_t *n, ? 0 : buf_pool.flush_neighbors; fil_space_t *space= nullptr; uint32_t last_space_id= FIL_NULL; + uint32_t backup_page_end= 0; static_assert(FIL_NULL > SRV_TMP_SPACE_ID, "consistency"); static_assert(FIL_NULL > SRV_SPACE_ID_UPPER_BOUND, "consistency"); @@ -1325,7 +1344,7 @@ static void buf_flush_LRU_list_batch(ulint max, flush_counters_t *n, buf_pool.lru_hp.set(bpage); mysql_mutex_unlock(&buf_pool.mutex); if (space) - space->release(); + writing_stop(space); auto p= buf_flush_space(space_id); space= p.first; last_space_id= space_id; @@ -1334,6 +1353,10 @@ static void buf_flush_LRU_list_batch(ulint max, flush_counters_t *n, mysql_mutex_lock(&buf_pool.mutex); goto no_space; } + + backup_page_end= space->writing_start() + ? space->backup_page_end() : 0; + mysql_mutex_lock(&buf_pool.mutex); buf_pool.stat.n_pages_written+= p.second; } @@ -1345,8 +1368,7 @@ static void buf_flush_LRU_list_batch(ulint max, flush_counters_t *n, } else if (space->is_stopping_writes()) { - space->release(); - space= nullptr; + space= writing_stop(space); no_space: mysql_mutex_lock(&buf_pool.flush_list_mutex); buf_flush_discard_page(bpage); @@ -1363,7 +1385,8 @@ static void buf_flush_LRU_list_batch(ulint max, flush_counters_t *n, break; } - if (neighbors && space->is_rotational() && UNIV_LIKELY(!to_withdraw) && + if (neighbors && UNIV_LIKELY(!(to_withdraw | backup_page_end)) && + space->is_rotational() && /* Skip neighbourhood flush from LRU list if we haven't yet reached half of the free page target. */ UT_LIST_GET_LEN(buf_pool.free) * 2 >= free_limit) @@ -1375,10 +1398,17 @@ static void buf_flush_LRU_list_batch(ulint max, flush_counters_t *n, flush: if (UNIV_UNLIKELY(to_withdraw != 0)) to_withdraw= buf_flush_LRU_to_withdraw(to_withdraw, *bpage); - if (bpage->flush(space)) + const uint32_t page{bpage->id().page_no()}; + if (page < backup_page_end && + page >= backup_page_end - space->BACKUP_BATCH_SIZE) + bpage->lock.u_unlock(true); + else if (bpage->flush(space)) + { ++n->flushed; - else - continue; + goto reacquire_mutex; + } + + continue; } goto reacquire_mutex; @@ -1391,7 +1421,7 @@ static void buf_flush_LRU_list_batch(ulint max, flush_counters_t *n, buf_pool.lru_hp.set(nullptr); if (space) - space->release(); + writing_stop(space); if (scanned) { @@ -1438,6 +1468,7 @@ static ulint buf_do_flush_list_batch(ulint max_n, lsn_t lsn) noexcept ? 0 : buf_pool.flush_neighbors; fil_space_t *space= nullptr; uint32_t last_space_id= FIL_NULL; + uint32_t backup_page_end= 0; static_assert(FIL_NULL > SRV_TMP_SPACE_ID, "consistency"); static_assert(FIL_NULL > SRV_SPACE_ID_UPPER_BOUND, "consistency"); @@ -1509,10 +1540,12 @@ static ulint buf_do_flush_list_batch(ulint max_n, lsn_t lsn) noexcept mysql_mutex_unlock(&buf_pool.flush_list_mutex); mysql_mutex_unlock(&buf_pool.mutex); if (space) - space->release(); + writing_stop(space); auto p= buf_flush_space(space_id); space= p.first; last_space_id= space_id; + backup_page_end= space && space->writing_start() + ? space->backup_page_end() : 0; mysql_mutex_lock(&buf_pool.mutex); buf_pool.stat.n_pages_written+= p.second; mysql_mutex_lock(&buf_pool.flush_list_mutex); @@ -1521,10 +1554,7 @@ static ulint buf_do_flush_list_batch(ulint max_n, lsn_t lsn) noexcept ut_ad(!space); } else if (space->is_stopping_writes()) - { - space->release(); - space= nullptr; - } + space= writing_stop(space); if (!space) buf_flush_discard_page(bpage); @@ -1533,9 +1563,17 @@ static ulint buf_do_flush_list_batch(ulint max_n, lsn_t lsn) noexcept mysql_mutex_unlock(&buf_pool.flush_list_mutex); do { - if (neighbors && space->is_rotational()) + if (neighbors && UNIV_LIKELY(!backup_page_end) && + space->is_rotational()) count+= buf_flush_try_neighbors(space, page_id, bpage, neighbors == 1, count, max_n); + else if (page_id.page_no() < backup_page_end && + page_id.page_no() >= + backup_page_end - space->BACKUP_BATCH_SIZE) + { + bpage->lock.u_unlock(true); + continue; + } else if (bpage->flush(space)) ++count; else @@ -1554,7 +1592,7 @@ static ulint buf_do_flush_list_batch(ulint max_n, lsn_t lsn) noexcept buf_pool.flush_hp.set(nullptr); if (space) - space->release(); + writing_stop(space); if (scanned) { @@ -1645,6 +1683,7 @@ bool buf_flush_list_space(fil_space_t *space, ulint *n_flushed) noexcept if (written) buf_pool.stat.n_pages_written+= written; } + mysql_mutex_lock(&buf_pool.flush_list_mutex); for (buf_page_t *bpage= UT_LIST_GET_LAST(buf_pool.flush_list); bpage; ) @@ -1687,17 +1726,35 @@ bool buf_flush_list_space(fil_space_t *space, ulint *n_flushed) noexcept acquired= false; goto was_freed; } + mysql_mutex_unlock(&buf_pool.flush_list_mutex); - if (bpage->flush(space)) + uint32_t page, backup_page_end; + + if (UNIV_UNLIKELY(space->writing_start())) { - ++n_flush; - if (!--max_n_flush) + page= bpage->id().page_no(); + backup_page_end= space->backup_page_end(); + if (page < backup_page_end && + page >= backup_page_end - space->BACKUP_BATCH_SIZE) { + bpage->lock.u_unlock(true); + space->writing_stop(); + skip: mysql_mutex_lock(&buf_pool.mutex); mysql_mutex_lock(&buf_pool.flush_list_mutex); may_have_skipped= true; goto done; } + } + + const bool written{bpage->flush(space)}; + space->writing_stop(); + + if (written) + { + ++n_flush; + if (!--max_n_flush) + goto skip; mysql_mutex_lock(&buf_pool.mutex); } } @@ -2013,14 +2070,17 @@ inline lsn_t log_t::write_checkpoint(lsn_t checkpoint, lsn_t end_lsn) noexcept last_checkpoint_lsn= checkpoint; this->end_lsn= end_lsn; if (!archive) + { + archived_checkpoint= checkpoint; archived_lsn= end_lsn; + } else if (archive_header_was_reset) { ut_ad(resize_log.m_file != log.m_file); /* Make the previous archived log file read-only */ #ifdef _WIN32 resize_log.close(); - SetFileAttributesA(get_archive_path().c_str(), + SetFileAttributesA(get_archive_path(get_first_lsn() - capacity()).c_str(), FILE_ATTRIBUTE_READONLY | FILE_ATTRIBUTE_ARCHIVE); #else struct stat st; @@ -2030,9 +2090,10 @@ inline lsn_t log_t::write_checkpoint(lsn_t checkpoint, lsn_t end_lsn) noexcept st.st_mode= 0444; if (fchmod(resize_log.m_file, st.st_mode)) my_error(ER_ERROR_ON_CLOSE, MYF(ME_ERROR_LOG), - get_archive_path().c_str(), errno); + get_archive_path(get_first_lsn() - capacity()).c_str(), errno); resize_log.close(); #endif + innodb_backup_checkpoint(); } else if (resize_log.m_file == log.m_file) { diff --git a/storage/innobase/handler/backup_innodb.cc b/storage/innobase/handler/backup_innodb.cc new file mode 100644 index 0000000000000..fee9196ffb1bb --- /dev/null +++ b/storage/innobase/handler/backup_innodb.cc @@ -0,0 +1,937 @@ +/* Copyright (c) 2026, MariaDB plc + + 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 Foundation; version 2 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1335 USA */ + +#include "my_global.h" +#include "sql_class.h" +#include "backup_innodb.h" +#include "sql_backup_interface.h" +#include "trx0trx.h" +#include "buf0flu.h" +#include "log0crypt.h" +#include +#ifdef __linux__ +# include +# include +#endif + +/** Associate a transaction with the current session +@param thd session +@return InnoDB transaction */ +trx_t *check_trx_exists(THD *thd) noexcept; + +namespace +{ +/** Backup state; protected by log_sys.latch */ +class InnoDB_backup +{ + /** pointer to backup context, or nullptr if no backup is active */ + trx_t *trx; + /** the original innodb_log_file_size, or 0 */ + uint64_t old_size; + + /** collection of files to be copied */ + std::vector queue; + /** collection of completed log archive files to be + hard-linked, copied, or moved */ + std::vector logs; + + /** backup target */ + backup_target target; + + /** @return the backup context */ + backup_context &context() const noexcept + { ut_ad(log_sys.latch_have_any()); ut_ad(trx); return trx->lock.backup; } + +public: + /** + Start of BACKUP SERVER: collect all files to be backed up + @param thd current session + @param target backup target + @return error code + @retval 0 on success + */ + int init(THD *thd, const backup_target &target) noexcept + { + trx_t *trx= check_trx_exists(thd); + if (trx->id || trx->state != TRX_STATE_NOT_STARTED) + { + ut_ad(trx->state != TRX_STATE_BACKUP); + my_error(ER_CANT_DO_THIS_DURING_AN_TRANSACTION, MYF(0)); + return 1; + } + + log_sys.latch.wr_lock(); + ut_ad(!this->trx); + ut_ad(queue.empty()); + if (!logs.empty()) + { + /* A new BACKUP SERVER is being invoked before a previous one + had been fully finalized. Clean up any log files. */ + if (old_size) + delete_logs(); + logs.clear(); + } + + const bool fail{log_sys.backup_start(&old_size, thd)}; + + if (!fail) + { + this->trx= trx; + trx->state= TRX_STATE_BACKUP; + backup_context &ctx{trx->lock.backup}; + ctx.first_lsn= log_sys.get_first_lsn();; + ctx.max_first_lsn= 1; + ctx.first_size= log_sys.file_size; + const lsn_t start= ctx.checkpoint= +#if 1 /* TODO: for incremental backup, allow the start to be specified */ + log_sys.get_latest_checkpoint(ctx.checkpoint_end_lsn); +#else + log_sys.archived_checkpoint; + ctx.checkpoint_end_lsn= log_sys.archived_lsn; +#endif + ctx.last_lsn= 0; + ctx.archived= !old_size; + + this->target= target; + /* Collect all tablespaces that have been created before our + start checkpoint. Newer tablespaces will be recovered by the + innodb_log_archive=ON recovery. + + If a tablespace is deleted before step() is invoked, the file + will not be copied, and a FILE_DELETE record in the log will + ensure correct recovery. + + If a tablespace is renamed between this and end(), the recovery + of a FILE_RENAME record will ensure the correct file name, + no matter which name was used by step(). */ + mysql_mutex_lock(&fil_system.mutex); + for (fil_space_t &space : fil_system.space_list) + if (space.id < SRV_SPACE_ID_UPPER_BOUND && + !space.is_being_imported() && + /* FIXME: how to initialize create_lsn for old files, to + have efficient incremental backup? + fil_node_t::read_page0() cannot assign it from + FIL_PAGE_LSN because that would not reflect the file + creation but for example allocating or freeing a page. + + The easy parts of initializing space->create_lsn are + as follows: + (1) In log_parse_file() when processing FILE_CREATE + (2) In deferred_spaces.create() */ + space.get_create_lsn() < start) + queue.emplace_back(space.id); + mysql_mutex_unlock(&fil_system.mutex); + } + log_sys.latch.wr_unlock(); + DEBUG_SYNC(thd, "innodb_backup_start"); + return fail; + } + + /** + Process a file that was collected at init(). + This may be invoked from multiple concurrent threads. + @param thd current session + @return number of files remaining, or negative on error + @retval 0 on completion + */ + int step(THD *thd) noexcept + { + uint32_t id= FIL_NULL; + lsn_t lsn= 0; + log_sys.latch.wr_lock(); + backup_context &ctx{context()}; + ut_ad(ctx.max_first_lsn); + size_t size{queue.size()}; + if (!logs.empty()) + { + lsn= logs.back(); + if (ctx.max_first_lsn < lsn) + ctx.max_first_lsn= lsn; + logs.pop_back(); + if (!size) + size= logs.size(); + } + else if (size) + { + size--; + id= queue.back(); + queue.pop_back(); + } + log_sys.latch.wr_unlock(); + + if (lsn) + { + if (link_or_move(lsn, nullptr, ctx, target)) + return -1; + } + else if (fil_space_t *space= fil_space_t::get(id)) + { + int res= -1; + uint32_t start{0}; + for (fil_node_t *node= UT_LIST_GET_FIRST(space->chain); node; + start+= node->size, node= UT_LIST_GET_NEXT(chain, node)) + if ((res= backup(node, start))) + break; + space->release(); + if (res) + return res; + } + + size= std::min(size_t{std::numeric_limits::max()}, size); + return int(size); + } + + /** + Finish copying and determine the logical time of the backup snapshot. + fini() must be invoked on the same thd. + @param thd current session + @param abort whether BACKUP SERVER was aborted + @return error code + @retval 0 on success + */ + int end(THD *thd, bool abort) noexcept + { + int fail= 0; + log_sys.latch.wr_lock(); + if (abort) + { + skip_log_dup: + queue.clear(); + if (old_size) + delete_logs(); + logs.clear(); + } + else + { + ut_ad(trx); + ut_ad(queue.empty()); + ut_ad(thd_to_trx(thd) == trx); + if (!trx || trx->state != TRX_STATE_BACKUP) + goto skip_log_dup; + backup_context &ctx= trx->lock.backup; + ut_ad(ctx.max_first_lsn); + ctx.last_lsn= log_sys.get_flushed_lsn(std::memory_order_relaxed); + while (!logs.empty()) + { + lsn_t lsn{logs.back()}; + if (lsn > ctx.last_lsn) + break; + if (lsn > ctx.max_first_lsn) + ctx.max_first_lsn= lsn; + logs.pop_back(); + log_sys.latch.wr_unlock(); + fail= link_or_move(lsn, nullptr, ctx, target); + log_sys.latch.wr_lock(); + if (fail) + goto skip_log_dup; + } + + { + lsn_t lsn{log_sys.get_first_lsn()}; + if (lsn > ctx.max_first_lsn && lsn < ctx.last_lsn) + { + const lsn_t end_lsn{lsn + log_sys.capacity()}; + ctx.max_first_lsn= lsn; + log_sys.latch.wr_unlock(); + bool live_hardlink; + if (UNIV_UNLIKELY(ctx.last_lsn > end_lsn)) + { + live_hardlink= true; + fail= link_or_move(lsn, &live_hardlink, ctx, target); + if (fail) + goto skip_log_dup; + /* Wait for checkpoint_complete(). If the previous link_or_move() + set live_hardlink, the file will be a read-only clone by now. */ + buf_flush_sync_batch(end_lsn, true); + ut_ad(logs.size() == 1); + ut_ad(logs.back() == lsn); + logs.clear(); + lsn= log_sys.get_first_lsn(); + ut_ad(lsn == end_lsn); + ctx.max_first_lsn= lsn; + ctx.last_lsn= log_get_lsn(); + ut_ad(ctx.last_lsn >= end_lsn); + } + + live_hardlink= false; + fail= link_or_move(lsn, &live_hardlink, ctx, target); + log_sys.latch.wr_lock(); + if (fail) + goto skip_log_dup; + if (!live_hardlink) + { + fail= write_config(target, ctx); + if (fail) + goto skip_log_dup; + ctx.max_first_lsn= 0; + } + } + else + goto skip_log_dup; + } + } + + ut_ad(!log_sys.resize_in_progress()); + ut_ad(log_sys.archive); + + /* Note: If we temporarily made a hard link to the last log file + which is writeable by the server, fini() will copy the file. + If it is also the first (and only) log file in our backup, + write_checkpoint() will write a checkpoint header that identifies + the starting point of recovering the backup. */ + + if (old_size) + { + log_sys.latch.wr_unlock(); + log_sys.backup_stop_archiving(thd); + log_sys.latch.wr_lock(); + } + + trx= nullptr; + log_sys.backup_stop(old_size, thd); + return fail; + } + + /** + Clean up after end(). + @param thd the parameter that had been passed to end() + @param target backup target + @return error code + @retval 0 on success + */ + int fini(THD *thd, const backup_target &target) noexcept + { + int fail= 0; + log_sys.latch.wr_lock(); + if (!trx) + { + ut_ad(queue.empty()); + if (old_size) + delete_logs(); + logs.clear(); + } + log_sys.latch.wr_unlock(); + + trx_t *const trx= thd_to_trx(thd); + if (!trx || trx->state != TRX_STATE_BACKUP) + ut_ad("invalid state" == 0); + else + { + ut_ad(!trx->id); + const backup_context &ctx{trx->lock.backup}; + if (ctx.max_first_lsn) + { + /* Copy our clone of the last log until the final LSN */ +#ifdef _WIN32 + std::string src{target.path}; + src.push_back('/'); + std::string dst{src}; + src.append("ib_logfile101"); + log_sys.append_archive_name(dst, ctx.max_first_lsn); + const char *s_= src.c_str(), *d_= dst.c_str(); + HANDLE s, d; + for (;;) + { + s= CreateFile(s_, GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + my_win_file_secattr(), OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, nullptr); + if (s != INVALID_HANDLE_VALUE) + break; + switch (GetLastError()) { + case ERROR_SHARING_VIOLATION: + case ERROR_LOCK_VIOLATION: + std::this_thread::sleep_for(std::chrono::seconds(1)); + continue; + } + my_error(ER_FILE_NOT_FOUND, MYF(ME_ERROR_LOG), s_, errno); + fail= 1; + goto done; + } + d= CreateFile(d_, GENERIC_WRITE, 0, my_win_file_secattr(), + CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr); + if (d == INVALID_HANDLE_VALUE) + { + fail: + fail= 1; + my_osmaperr(GetLastError()); + CloseHandle(s); + my_error(ER_ERROR_ON_RENAME, MYF(ME_ERROR_LOG), s_, d_, errno); + } + else + { + const uint64_t payload_end= log_sys.START_OFFSET + + ctx.last_lsn - ctx.max_first_lsn; + /* First, extend the file to a valid size. */ + { + LARGE_INTEGER li; + li.QuadPart= + std::max(log_sys.FILE_SIZE_MIN, + (payload_end + 4095) & ~4095ULL); + fail= !SetFilePointerEx(d, li, nullptr, FILE_BEGIN) || + !SetEndOfFile(d); + } + if (!fail) + fail= copy_file(s, d, log_sys.START_OFFSET, payload_end) || + (ctx.max_first_lsn == ctx.first_lsn && + write_checkpoint(d, ctx.checkpoint_end_lsn - ctx.first_lsn + + log_sys.START_OFFSET)); + if (!CloseHandle(d) || fail) + goto fail; + + CloseHandle(s); + + if (!DeleteFile(s_)) + { + my_osmaperr(GetLastError()); + my_error(ER_CANT_DELETE_FILE, MYF(ME_ERROR_LOG), s_, errno); + fail= 1; + } + } +#else + ut_ad(target.directory); + int s= openat(target.fd, "ib_logfile101", O_RDONLY); + std::string dst; + log_sys.append_archive_name(dst, ctx.max_first_lsn); + int d{-1}; + if (s == -1) + { + my_error(ER_FILE_NOT_FOUND, MYF(ME_ERROR_LOG), "ib_logfile101", + errno); + fail= 1; + goto done; + } + ut_ad(target.directory); + d= openat(target.fd, dst.c_str(), + O_CREAT | O_EXCL | O_TRUNC | O_WRONLY, 0666); + if (d < 0) + { + fail: + fail= 1; + my_error(ER_ERROR_ON_RENAME, MYF(ME_ERROR_LOG), + "ib_logfile101", dst.c_str(), errno); + close(s); + } + else + { + const uint64_t payload_end= log_sys.START_OFFSET + + ctx.last_lsn - ctx.max_first_lsn; + /* First, extend the file to a valid size. */ + fail= ftruncate(d, std::max(log_sys.FILE_SIZE_MIN, + (payload_end + 4095) & ~4095LL)); + if (!fail) + fail= copy_file(s, d, log_sys.START_OFFSET, payload_end) || + (ctx.max_first_lsn == ctx.first_lsn && + write_checkpoint(d, ctx.checkpoint_end_lsn - ctx.first_lsn + + log_sys.START_OFFSET)); + if (close(d) || fail) + goto fail; + if (unlinkat(target.fd, "ib_logfile101", 0)) + { + my_error(ER_CANT_DELETE_FILE, MYF(ME_ERROR_LOG), + "ib_logfile101", errno); + fail= 1; + } + std::ignore= close(s); + } +#endif + done: + fail= write_config(target, ctx); + } + trx->lock.backup= {}; + trx->state= TRX_STATE_NOT_STARTED; + } + return fail; + } + + /** + Complete the first checkpoint in a new archive log file. + */ + void checkpoint_complete() noexcept + { + ut_ad(log_sys.latch_have_wr()); + if (trx) + logs.emplace_back(log_sys.get_first_lsn() - log_sys.capacity()); + } + +private: + /** Safely start backing up a tablespace file + @param end last page to copy */ + static void backup_start(fil_space_t *space, uint32_t end) noexcept + { + if (space->backup_start(end)) + os_aio_wait_until_no_pending_writes(false); + } + /* Stop backing up a tablespace */ + static void backup_stop(fil_space_t *space) noexcept + { space->backup_stop(); } + + /** Delete unnecessary logs that had been created for backup. */ + void delete_logs() noexcept + { + ut_ad(old_size); + for (const lsn_t lsn : logs) + IF_WIN(DeleteFile,unlink)(log_sys.get_archive_path(lsn).c_str()); + } + + /** + Back up a persistent InnoDB data file. + @param node InnoDB data file + @param start first page number + */ + int backup(fil_node_t *node, uint32_t start) noexcept + { + for (bool tried_mkdir{false};;) + { +#ifdef _WIN32 + std::string path{target.path}; + path.push_back('/'); + path.append(node->name); + HANDLE f= CreateFile(path.c_str(), GENERIC_WRITE, 0, + my_win_file_secattr(), CREATE_NEW, + FILE_ATTRIBUTE_NORMAL, nullptr); + if (f == INVALID_HANDLE_VALUE) + { + unsigned long err= GetLastError(); + if (err == ERROR_PATH_NOT_FOUND && !tried_mkdir && + node->space->id && !srv_is_undo_tablespace(node->space->id)) + { + tried_mkdir= true; + path.erase(path.rfind('/')); + if (CreateDirectory(path.c_str(), + my_dir_security_attributes.lpSecurityDescriptor + ? &my_dir_security_attributes : nullptr) || + (err= GetLastError()) == ERROR_ALREADY_EXISTS) + continue; + } + + my_osmaperr(err); + goto fail; + } +#else + int f; + ut_ad(target.directory); +# ifdef __APPLE__ + backup_start(node->space, + (node->space->size + fil_space_t::BACKUP_BATCH_SIZE - 1) & + ~fil_space_t::BACKUP_BATCH_SIZE); + f= fclonefileat(node->handle, target.fd, node->name, 0); + backup_stop(node->space); + if (!f) + break; + switch (errno) { + case ENOENT: + goto try_mkdir; + case ENOTSUP: + break; + default: + goto fail; + } +# endif + f= openat(target.fd, node->name, + O_CREAT | O_EXCL | O_TRUNC | O_WRONLY, 0666); + if (f < 0) + { + if (errno == ENOENT) + { +# ifdef __APPLE__ + try_mkdir: +# endif + if (!tried_mkdir && node->space->id && + !srv_is_undo_tablespace(node->space->id)) + { + tried_mkdir= true; + const char *sep= strchr(node->name, '/'); + ut_ad(sep); + sep= strchr(sep + 1, '/'); + ut_ad(sep); + std::string dir{node->name, size_t(sep - node->name)}; + if (!mkdirat(target.fd, dir.c_str(), 0777) || errno == EEXIST) + continue; + } + } + goto fail; + } +#endif + int err{0}; + for (const uint32_t file_size{node->size}, + page_size{node->space->physical_size()};;) + { + const uint32_t end{start + fil_space_t::BACKUP_BATCH_SIZE}; + backup_start(node->space, end); + /* TODO: avoid copying freed page ranges */ + err= copy_file(node->handle, f, start * uint64_t{page_size}, + std::min(end, file_size) * uint64_t{page_size}); + backup_stop(node->space); + if (err || (start= end) >= file_size) + break; + } + + if (IF_WIN(!CloseHandle(f), close(f)) || err) + goto fail; + break; + } + return 0; + fail: + my_error(ER_CANT_CREATE_FILE, MYF(0), node->name, errno); + return -1; + } + + /** Write a checkpoint header pointing to the start of the backup. + @param dst target file + @param c offset of the FILE_CHECKPOINT mini-transaction + @return error code + @retval 0 on success */ + static int write_checkpoint(int dst, uint64_t c) noexcept + { +#ifdef _WIN32 + using tpool::pwrite; +#endif + uint64_t buf[8]{}; + ut_ad(c >= log_sys.START_OFFSET); + if (log_sys.is_encrypted()) + log_crypt_write_header(reinterpret_cast(buf), true); + buf[4 * log_sys.is_encrypted()]= my_htobe64(c); + + for (ssize_t o= 0, count= sizeof buf; count;) + { + ssize_t ret= + pwrite(dst, reinterpret_cast(buf) + o, count, o); + if (ret <= 0 || ret > count) + return -1; + o+= ret; + count-= ret; + } + return 0; + } + + /** Write the configuration parameters for restoring the backup + @param target backup target + @param ctx backup context + @return error code (non-positive) + @retval 0 on success */ + static int write_config(const backup_target &target, + const backup_context &ctx) noexcept + { + char config[sizeof "[server]\n# checkpoint=" + + sizeof "innodb_log_recovery_start=" + + sizeof "innodb_log_recovery_target=\n" + 45 * 3]; + const int size= + snprintf(config, sizeof config, + "[server]\n# checkpoint=" LSN_PF "\n" + "innodb_log_recovery_start=" LSN_PF "\n" + "innodb_log_recovery_target=" LSN_PF "\n", + ctx.checkpoint, ctx.checkpoint_end_lsn, ctx.last_lsn); + return backup_config_append(target, config, size_t(size)); + } + + /** Hard-link (copy) or rename (move) an archive log file. + @param lsn The first LSN in the file + @param clone pointer to a flag that will be set if a live log was + hard-linked (needing deduplication), + or nullptr if the source log file is known to be read-only + @param ctx backup context + @param target backup target + @return error code + @retval 0 on success */ + static int link_or_move(lsn_t lsn, bool *clone, + const backup_context &ctx, + const backup_target &target) noexcept + { + const std::string p{log_sys.get_archive_path(lsn)}; + const char *const path= p.c_str(), *basename= strrchr(path, '/'); + if (!basename) + basename= path; + else + basename++; + const bool move{!clone && !ctx.archived}; + +#ifdef _WIN32 + std::string b{target.path}; + b.push_back('/'); + b.append((clone && !*clone) ? "ib_logfile101" : basename); + const char *destname= b.c_str(); + + unsigned long err; + if (move) + { + if (!MoveFileEx(path, destname, MOVEFILE_COPY_ALLOWED)) + { + fail: + err= GetLastError(); + got_err: + my_osmaperr(err); + my_error(ER_ERROR_ON_RENAME, MYF(ME_ERROR_LOG), path, basename, errno); + return -1; + } + + if (lsn < ctx.checkpoint) + { + if (!SetFileAttributes(destname, FILE_ATTRIBUTE_NORMAL)) + goto fail; + HANDLE dh= CreateFile(destname, GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (dh == INVALID_HANDLE_VALUE) + goto fail; + if (os_file_set_sparse_win32(dh)) + std::ignore= + os_file_punch_hole(dh, 0, log_sys.START_OFFSET + + ((ctx.checkpoint - lsn) & ~4095ULL)); + int fail= write_checkpoint(dh, ctx.checkpoint_end_lsn - lsn + + log_sys.START_OFFSET); + CloseHandle(dh); + if (fail) + goto fail; + } + } + else if (!CreateHardLink(destname, path, nullptr)) + { + if ((err= GetLastError()) != ERROR_NOT_SAME_DEVICE) + goto got_err; + /* Hard-linking failed. Try copying with the final name. */ + b= target.path; + b.push_back('/'); + b.append(basename); + destname= b.c_str(); + + if (lsn >= ctx.checkpoint && (lsn < ctx.max_first_lsn || !ctx.last_lsn)) + { + /* Copy a middle log file entirely. */ + sql_print_information("CopyFileEx %s, %s", path, destname); + if (!CopyFileEx(path, destname, nullptr, nullptr, nullptr, + COPY_FILE_NO_BUFFERING)) + goto fail; + } + else + { + HANDLE s, d; + for (;;) + { + s= CreateFile(path, GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + my_win_file_secattr(), OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, nullptr); + if (s != INVALID_HANDLE_VALUE) + break; + switch (GetLastError()) { + case ERROR_SHARING_VIOLATION: + case ERROR_LOCK_VIOLATION: + std::this_thread::sleep_for(std::chrono::seconds(1)); + continue; + } + goto fail; + } + d= CreateFile(destname, GENERIC_WRITE, 0, my_win_file_secattr(), + CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr); + if (d == INVALID_HANDLE_VALUE) + { + CloseHandle(s); + goto fail; + } + + uint64_t payload_start{log_sys.START_OFFSET}; + uint64_t payload_end{payload_start + ctx.last_lsn - lsn}; + + if (lsn < ctx.checkpoint) + { + /* Copy the necessary part of the first log file. */ + ut_ad(lsn == ctx.first_lsn); + payload_start= ((ctx.checkpoint - lsn) + 4095) & ~4095ULL; + if (!ctx.last_lsn || ctx.last_lsn >= ctx.first_lsn + ctx.first_size) + payload_end= ctx.first_size; + } + + /* First, extend the file to a valid size. */ + { + LARGE_INTEGER li; + li.QuadPart= + std::max(log_sys.FILE_SIZE_MIN, + (payload_end + 4095) & ~4095ULL); + err= !SetFilePointerEx(d, li, nullptr, FILE_BEGIN) || + !SetEndOfFile(d); + } + + if (!err) + { + err= copy_file(s, d, payload_start, payload_end); + if (!err && lsn < ctx.checkpoint) + err= write_checkpoint(d, ctx.checkpoint_end_lsn - lsn + + log_sys.START_OFFSET); + } + + if (err | !(CloseHandle(s) & CloseHandle(d))) + goto fail; + } + } + else if (clone) + *clone= true; +#else + ut_ad(target.directory); + if (move + ? !renameat(AT_FDCWD, path, target.fd, basename) + : !linkat(AT_FDCWD, path, target.fd, + (clone && !*clone) ? "ib_logfile101" : basename, + AT_SYMLINK_FOLLOW)) + { +# ifdef __linux__ + if (!move || lsn != ctx.first_lsn); + else if (off_t garbage= (ctx.checkpoint - lsn) & ~4095ULL) + /* Best effort to punch a hole to free up some garbage in + the first file. We do not care about failures. */ + if (!fchmodat(target.fd, basename, 0644, 0)) + { + int dst= openat(target.fd, basename, O_RDWR); + if (dst >= 0) + fallocate(dst, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, + log_sys.START_OFFSET, garbage); + close(dst); + std::ignore= fchmodat(target.fd, basename, 0444, 0); + } +# endif + if (clone) + *clone= !move; + return 0; + } + else if (errno != EXDEV) + { + fail: + my_error(ER_ERROR_ON_RENAME, MYF(ME_ERROR_LOG), path, basename, errno); + return -1; + } + else + { + int src= open(path, O_RDONLY); + if (src < 0) + goto fail; + if (move && unlink(path)) + { + close_and_fail: + std::ignore= close(src); + goto fail; + } + int dst= openat(target.fd, basename, + O_CREAT | O_EXCL | O_TRUNC | O_WRONLY, 0666); + if (dst < 0) + goto close_and_fail; + int err; + if (lsn >= ctx.checkpoint && (lsn < ctx.max_first_lsn || !ctx.last_lsn)) + /* Copy a middle log file entirely. */ + err= copy_entire_file(src, dst); + else + { + uint64_t payload_start{log_sys.START_OFFSET}; + uint64_t payload_end{payload_start + ctx.last_lsn - lsn}; + + if (lsn < ctx.checkpoint) + { + /* Copy the necessary part of the first log file. */ + ut_ad(lsn == ctx.first_lsn); + payload_start= ((ctx.checkpoint - lsn) + 4095) & ~4095ULL; + if (!ctx.last_lsn || ctx.last_lsn >= ctx.first_lsn + ctx.first_size) + payload_end= ctx.first_size; + } + + /* First, extend the file to a valid size. */ + err= ftruncate(dst, std::max(log_sys.FILE_SIZE_MIN, + (payload_end + 4095) & ~4095LL)); + if (!err) + { + err= copy_file(src, dst, payload_start, payload_end); + if (!err && lsn < ctx.checkpoint) + err= write_checkpoint(dst, ctx.checkpoint_end_lsn - lsn + + log_sys.START_OFFSET); + } + } + + if (err | close(dst) | close(src)) + goto fail; + } +#endif + return 0; + } +}; + +/** The backup context; protected by log_sys.latch */ +static InnoDB_backup innodb_backup; +} + +bool log_t::backup_start(uint64_t *old_size, THD *thd) noexcept +{ + ut_ad(latch_have_wr()); + ut_ad(!backup); + backup= true; + *old_size= 0; + if (archive) + return false; + const uint64_t old_file_size{file_size}; + latch.wr_unlock(); + const bool fail{set_archive(true, thd, true)}; + latch.wr_lock(); + if (!fail) + { + *old_size= old_file_size; + return false; + } + ut_ad(backup); + backup= false; + const uint64_t new_file_size{file_size}; + latch.wr_unlock(); + if (old_file_size != new_file_size && old_file_size && + resize_start(old_file_size, thd) == RESIZE_STARTED) + resize_finish(thd); + latch.wr_lock(); + return true; +} + +void log_t::backup_stop(uint64_t old_size, THD *thd) noexcept +{ + ut_ad(latch_have_wr()); + /* We will be invoked with old_size=0 after a failed backup_start(), + or if innodb_log_archive=ON held during a successful backup_start(). */ + ut_ad(!old_size || !resize_in_progress()); + ut_ad(!old_size || backup); + backup= false; + const uint64_t new_size{file_size}; + latch.wr_unlock(); + if (old_size && old_size != new_size && + resize_start(old_size, thd) == RESIZE_STARTED) + resize_finish(thd); +} + +int innodb_backup_start(THD *thd, backup_target target) noexcept +{ + return innodb_backup.init(thd, target); +} + +int innodb_backup_step(THD *thd) noexcept +{ + return innodb_backup.step(thd); +} + +int innodb_backup_end(THD *thd, bool abort) noexcept +{ + return innodb_backup.end(thd, abort); +} + +int innodb_backup_finalize(THD *thd, backup_target target) noexcept +{ + return innodb_backup.fini(thd, target); +} + +void innodb_backup_checkpoint() noexcept +{ + innodb_backup.checkpoint_complete(); +} diff --git a/storage/innobase/handler/backup_innodb.h b/storage/innobase/handler/backup_innodb.h new file mode 100644 index 0000000000000..8d5f81ca35898 --- /dev/null +++ b/storage/innobase/handler/backup_innodb.h @@ -0,0 +1,54 @@ +/* Copyright (c) 2026, MariaDB plc + + 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 Foundation; version 2 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1335 USA */ + +/** + Start of BACKUP SERVER: collect all files to be backed up + @param thd current session + @param target backup target + @return error code + @retval 0 on success +*/ +int innodb_backup_start(THD *thd, backup_target target) noexcept; + +/** + Process a file that was collected in backup_start(). + @param thd current session + @return number of files remaining, or negative on error + @retval 0 on completion +*/ +int innodb_backup_step(THD *thd) noexcept; + +/** + Finish copying and determine the logical time of the backup snapshot. + @param thd current session + @param abort whether BACKUP SERVER was aborted + @return error code + @retval 0 on success +*/ +int innodb_backup_end(THD *thd, bool abort) noexcept; + +/** + Clean up after innodb_backup_end(). + @param thd the parameter on which innodb_backup_end() had been invoked + @param target backup target + @return error code + @retval 0 on success +*/ +int innodb_backup_finalize(THD *thd, backup_target target) noexcept; + +/** + Complete the first checkpoint in a new archive log file. +*/ +void innodb_backup_checkpoint() noexcept; diff --git a/storage/innobase/handler/ha_innodb.cc b/storage/innobase/handler/ha_innodb.cc index 3a05963b4081e..5c28d270d13ff 100644 --- a/storage/innobase/handler/ha_innodb.cc +++ b/storage/innobase/handler/ha_innodb.cc @@ -152,6 +152,7 @@ MDL_ticket *get_mdl_ticket(TABLE *table) noexcept; #include "ha_innodb.h" #include "i_s.h" +#include "backup_innodb.h" #include #include @@ -2606,16 +2607,10 @@ innobase_trx_allocate( DBUG_RETURN(trx); } -/*********************************************************************//** -Gets the InnoDB transaction handle for a MySQL handler object, creates -an InnoDB transaction struct if the corresponding MySQL thread struct still -lacks one. -@return InnoDB transaction handle */ -static -trx_t* -check_trx_exists( -/*=============*/ - THD* thd) /*!< in: user thread handle */ +/** Associate a transaction with the current session +@param thd session +@return InnoDB transaction */ +trx_t *check_trx_exists(THD *thd) noexcept { if (trx_t* trx = thd_to_trx(thd)) { ut_a(trx->magic_n == TRX_MAGIC_N); @@ -4148,6 +4143,10 @@ static int innodb_init(void* p) = innodb_prepare_commit_versioned; innobase_hton->update_optimizer_costs= innobase_update_optimizer_costs; + innobase_hton->backup_start = innodb_backup_start; + innobase_hton->backup_step = innodb_backup_step; + innobase_hton->backup_end = innodb_backup_end; + innobase_hton->backup_finalize = innodb_backup_finalize; innobase_hton->binlog_init= innodb_binlog_init; innobase_hton->set_binlog_max_size= ibb_set_max_size; innobase_hton->binlog_write_direct_ordered= @@ -18816,39 +18815,7 @@ static void innodb_log_file_size_update(THD *thd, st_mysql_sys_var*, ib_senderrf(thd, IB_LOG_LEVEL_ERROR, ER_CANT_CREATE_HANDLER_FILE); break; case log_t::RESIZE_STARTED: - for (timespec abstime;;) - { - if (thd_kill_level(thd)) - { - log_sys.resize_abort(thd); - break; - } - - set_timespec(abstime, 5); - mysql_mutex_lock(&buf_pool.flush_list_mutex); - lsn_t resizing= log_sys.resize_in_progress(); - if (resizing > buf_pool.get_oldest_modification(0)) - { - buf_pool.page_cleaner_wakeup(true); - my_cond_timedwait(&buf_pool.done_flush_list, - &buf_pool.flush_list_mutex.m_mutex, &abstime); - resizing= log_sys.resize_in_progress(); - } - mysql_mutex_unlock(&buf_pool.flush_list_mutex); - if (!resizing || !log_sys.resize_running(thd)) - break; - log_sys.latch.wr_lock(); - while (resizing > log_sys.get_lsn()) - { - ut_ad(!log_sys.is_mmap()); - /* The server is almost idle. Write dummy FILE_CHECKPOINT records - to ensure that the log resizing will complete. */ - mtr_t mtr{nullptr}; - mtr.start(); - mtr.commit_files(log_sys.last_checkpoint_lsn); - } - log_sys.latch.wr_unlock(); - } + log_sys.resize_finish(thd); } } mysql_mutex_lock(&LOCK_global_system_variables); @@ -19703,7 +19670,9 @@ static MYSQL_SYSVAR_BOOL(data_file_write_through, fil_system.write_through, static void innodb_log_archive_update(THD *thd, st_mysql_sys_var*, void *, const void *save) noexcept { + mysql_mutex_unlock(&LOCK_global_system_variables); log_sys.set_archive(*static_cast(save), thd); + mysql_mutex_lock(&LOCK_global_system_variables); } static MYSQL_SYSVAR_BOOL(log_archive, log_sys.archive, @@ -19716,10 +19685,20 @@ static MYSQL_SYSVAR_UINT64_T(log_archive_start, innodb_log_archive_start, "initial value of innodb_lsn_archived; 0=auto-detect", nullptr, nullptr, 0, 0, std::numeric_limits::max(), 0); +static void innodb_log_recovery_start_update(THD *, st_mysql_sys_var*, + void *, const void *save) noexcept +{ + const lsn_t lsn{*static_cast(save)}; + recv_sys.recovery_start= lsn; + if (lsn && log_sys.archive) + log_sys.archived_checkpoint= lsn; +} + static MYSQL_SYSVAR_UINT64_T(log_recovery_start, recv_sys.recovery_start, - PLUGIN_VAR_RQCMDARG | PLUGIN_VAR_READONLY, + PLUGIN_VAR_RQCMDARG, "LSN to start recovery from (0=automatic)", - nullptr, nullptr, 0, 0, std::numeric_limits::max(), 0); + nullptr, innodb_log_recovery_start_update, + 0, 0, std::numeric_limits::max(), 0); static MYSQL_SYSVAR_UINT64_T(log_recovery_target, recv_sys.rpo, PLUGIN_VAR_RQCMDARG | PLUGIN_VAR_READONLY, diff --git a/storage/innobase/include/fil0fil.h b/storage/innobase/include/fil0fil.h index a43020daf7b63..6a6680e667ae6 100644 --- a/storage/innobase/include/fil0fil.h +++ b/storage/innobase/include/fil0fil.h @@ -408,6 +408,13 @@ struct fil_space_t final /** Whether any corruption of this tablespace has been reported */ mutable std::atomic_flag is_corrupted= ATOMIC_FLAG_INIT; + /** BACKUP SERVER flag in write_or_backup */ + static constexpr uint8_t BACKUP{128}; + /** whether there is a pending write or backup */ + std::atomic write_or_backup{0}; + /** first page number that is not being backed up */ + std::atomic backup_end{0}; + public: /** mutex to protect freed_ranges and last_freed_lsn */ std::mutex freed_range_mutex; @@ -1044,6 +1051,46 @@ struct fil_space_t final VALIDATE_IMPORT }; + /** Note that writes are being submitted to the tablespace. + @return whether a backup is pending */ + bool writing_start() noexcept + { + uint8_t wb{write_or_backup.fetch_add(1, std::memory_order_acq_rel)}; + ut_ad(~wb & (BACKUP - 1)); + return wb & BACKUP; + } + + /** Note that we there are no more pending writes to the tablespace. */ + void writing_stop() noexcept + { + ut_d(uint8_t wb=) write_or_backup.fetch_sub(1, std::memory_order_release); + ut_ad(wb & ~BACKUP); + } + + /** Note that we backing up some pages of the underlying files. + @param last_page the last page that is being backed up */ + bool backup_start(uint32_t last_page) noexcept + { + backup_end.store(last_page, std::memory_order_relaxed); + uint8_t wb{write_or_backup.fetch_add(BACKUP, std::memory_order_acq_rel)}; + ut_ad(!(wb & BACKUP)); + return wb & ~BACKUP; + } + /** Note that we are not currently backing up the underlying files. */ + void backup_stop() noexcept + { + backup_end.store(0, std::memory_order_relaxed); + ut_d(uint8_t wb=) + write_or_backup.fetch_sub(BACKUP, std::memory_order_release); + ut_ad(wb & BACKUP); + } + /** @return the first page number that is not being backed up */ + uint32_t backup_page_end() const noexcept + { return backup_end.load(std::memory_order_relaxed); } + + /** The size of a backup copy_file() batch in pages */ + static constexpr uint32_t BACKUP_BATCH_SIZE{64}; + /** Update the data structures on write completion */ void complete_write() noexcept; diff --git a/storage/innobase/include/log0log.h b/storage/innobase/include/log0log.h index 44a827dbf636d..6d07fa25e013d 100644 --- a/storage/innobase/include/log0log.h +++ b/storage/innobase/include/log0log.h @@ -221,6 +221,8 @@ struct log_t /** whether !archive log records may have been written with get_sequence_bit()==0 */ bool circular_recovery_from_sequence_bit_0:1; + /** whether we are between backup_start() and backup_stop() */ + bool backup:1; public: /** the default value of log_mmap */ static constexpr bool log_mmap_default= @@ -288,6 +290,8 @@ struct log_t Atomic_relaxed last_checkpoint_lsn; /** The log writer (protected by latch.wr_lock()) */ lsn_t (*writer)() noexcept; + /** the earliest available checkpoint; protected by latch.wr_lock() */ + lsn_t archived_checkpoint; /** end_lsn of the first available checkpoint, or 0; protected by latch.wr_lock() */ lsn_t archived_lsn; @@ -369,11 +373,24 @@ struct log_t RESIZE_NO_CHANGE, RESIZE_IN_PROGRESS, RESIZE_STARTED, RESIZE_FAILED }; +private: /** Start resizing the log and release the exclusive latch. + @param size requested new file_size + @param thd the current thread identifier + @param backup whether the caller is backup_start() or backup_stop() + @return whether the resizing was started successfully */ + resize_start_status resize_start(uint64_t size, void *thd, bool backup) + noexcept; +public: + /** Start resizing the log. @param size requested new file_size @param thd the current thread identifier @return whether the resizing was started successfully */ - resize_start_status resize_start(os_offset_t size, void *thd) noexcept; + resize_start_status resize_start(uint64_t size, void *thd) noexcept + { return resize_start(size, thd, false); } + + /** Wait for the completion of resize_start() == RESIZE_STARTED */ + void resize_finish(THD *thd) noexcept; /** Abort a resize_start() that we started. @param thd thread identifier that had been passed to resize_start() */ @@ -397,10 +414,37 @@ struct log_t resize_write_low(lsn, end, len, seq); } +private: + /** SET GLOBAL innodb_log_archive, or start/stop BACKUP SERVER + @param archive the new value of innodb_log_archive + @param thd SQL connection + @param backup whether the caller is backup_start() or backup_stop() + @return whether the operation failed */ + bool set_archive(my_bool archive, THD *thd, bool backup) noexcept; +public: /** SET GLOBAL innodb_log_archive @param archive the new value of innodb_log_archive - @param thd SQL connection */ - void set_archive(my_bool archive, THD *thd) noexcept; + @param thd SQL connection + @return whether the operation failed */ + bool set_archive(my_bool archive, THD *thd) noexcept + { return set_archive(archive, thd, false); } + + /** Start BACKUP SERVER. + @param old_size the old file_size, or 0 on failure or when + already running innodb_log_archive=ON + @param thd SQL connection + @return whether the operation failed */ + bool backup_start(uint64_t *old_size, THD *thd) noexcept; + /** Stop log archiving in BACKUP SERVER clean-up + @param thd SQL connection + @return whether the operation failed */ + bool backup_stop_archiving(THD *thd) noexcept + { return set_archive(false, thd, true); } + + /** Stop BACKUP SERVER. + @param old_size the value returned by backup_start() + @param thd SQL connection */ + void backup_stop(uint64_t old_size, THD *thd) noexcept; private: /** Replicate a write to the log. @@ -695,6 +739,18 @@ struct log_t /** @return the first LSN of the log file */ lsn_t get_first_lsn() const noexcept { return first_lsn; } + /** + Determine the latest checkpoint. + @param end LSN leading to the FILE_CHECKPOINT record + @return the latest checkpoint LSN + */ + lsn_t get_latest_checkpoint(lsn_t &end) const noexcept + { + ut_ad(latch_have_any()); + end= end_lsn; + return last_checkpoint_lsn; + } + /** Set the recovered checkpoint. @param lsn log sequence number of the checkpoint @param end_lsn LSN passed to write_checkpoint() diff --git a/storage/innobase/include/trx0trx.h b/storage/innobase/include/trx0trx.h index 9a9bd152bd0ed..d09a1ffe17f9e 100644 --- a/storage/innobase/include/trx0trx.h +++ b/storage/innobase/include/trx0trx.h @@ -348,10 +348,14 @@ struct trx_lock_t only be modified by the thread that is serving the running transaction. */ - /** Pre-allocated record locks */ - struct { - alignas(CPU_LEVEL1_DCACHE_LINESIZE) ib_lock_t lock; - } rec_pool[8]; + union + { + /** Context for finalizing BACKUP SERVER */ + backup_context backup; + + /** Pre-allocated record locks */ + struct { alignas(CPU_LEVEL1_DCACHE_LINESIZE) ib_lock_t lock; } rec_pool[8]; + }; /** Pre-allocated table locks */ ib_lock_t table_pool[8]; diff --git a/storage/innobase/include/trx0trx.inl b/storage/innobase/include/trx0trx.inl index 317f1f5cd0d27..4aff8ef96c58f 100644 --- a/storage/innobase/include/trx0trx.inl +++ b/storage/innobase/include/trx0trx.inl @@ -68,6 +68,7 @@ trx_state_eq( return(true); case TRX_STATE_ABORTED: + case TRX_STATE_BACKUP: break; } ut_error; diff --git a/storage/innobase/include/trx0types.h b/storage/innobase/include/trx0types.h index 6fde1e831e5bd..300f8c8e9f4ab 100644 --- a/storage/innobase/include/trx0types.h +++ b/storage/innobase/include/trx0types.h @@ -62,7 +62,28 @@ enum trx_state_t { /** XA PREPARE transaction that was returned to ha_recover() */ TRX_STATE_PREPARED_RECOVERED, /** The transaction has been committed (or completely rolled back) */ - TRX_STATE_COMMITTED_IN_MEMORY + TRX_STATE_COMMITTED_IN_MEMORY, + /** The transaction holds context for BACKUP SERVER */ + TRX_STATE_BACKUP +}; + +/** TRX_STATE_BACKUP context */ +struct backup_context +{ + /** Start LSN of the first backed up log file */ + lsn_t first_lsn; + /** Start LSN of the latest copied log file, or 1 if none yet */ + lsn_t max_first_lsn; + /** size of the first log file */ + uint64_t first_size; + /** Checkpoint at the start of the backup */ + lsn_t checkpoint; + /** Log record pointing to the checkpoint */ + lsn_t checkpoint_end_lsn; + /** Final LSN of the backup */ + lsn_t last_lsn; + /** the original state of innodb_log_archive before/after backup */ + bool archived; }; /** Transaction bulk insert operation @see trx_t::bulk_insert */ diff --git a/storage/innobase/log/log0log.cc b/storage/innobase/log/log0log.cc index aad4f768f0867..f4519aa6767a7 100644 --- a/storage/innobase/log/log0log.cc +++ b/storage/innobase/log/log0log.cc @@ -634,7 +634,7 @@ void log_t::set_buffered(bool buffered) noexcept } #endif - /** Try to enable or disable durable writes (update log_write_through) */ +/** Try to enable or disable durable writes (update log_write_through) */ void log_t::set_write_through(bool write_through) { if (is_mmap() || high_level_read_only || recv_sys.rpo) @@ -763,9 +763,12 @@ void log_t::header_rewrite(my_bool archive) noexcept /** SET GLOBAL innodb_log_archive @param archive the new value of innodb_log_archive -@param thd SQL connection */ -void log_t::set_archive(my_bool archive, THD *thd) noexcept +@param thd SQL connection +@param backup whether the caller is backup_start() or backup_stop() +@return whether the operation failed */ +bool log_t::set_archive(my_bool archive, THD *thd, bool backup) noexcept { + bool fail= false; thd_wait_begin(thd, THD_WAIT_DISKIO); tpool::tpool_wait_begin(); lsn_t wait_lsn; @@ -779,12 +782,20 @@ void log_t::set_archive(my_bool archive, THD *thd) noexcept my_printf_error(ER_WRONG_USAGE, "SET GLOBAL innodb_log_file_size is in progress", MYF(0)); + fail: + fail= true; + wait_lsn= 0; break; } if (archive == this->archive) break; - if (thd_kill_level(thd)) - break; + if ((!backup || archive) && thd_kill_level(thd)) + goto fail; + if (!backup && this->backup) + { + my_printf_error(ER_WRONG_USAGE, "BACKUP SERVER is in progress", MYF(0)); + goto fail; + } if (resize_log.is_opened()) { @@ -893,7 +904,7 @@ void log_t::set_archive(my_bool archive, THD *thd) noexcept if (!log.is_opened()) { my_error(ER_ERROR_ON_READ, MYF(0), old_name, errno); - break; + goto fail; } } #endif @@ -918,7 +929,7 @@ void log_t::set_archive(my_bool archive, THD *thd) noexcept { my_error(ER_ERROR_ON_RENAME, MYF(0), old_name, new_name, my_errno); first_lsn= old_first_lsn; - break; + goto fail; } if (archive) @@ -950,14 +961,16 @@ void log_t::set_archive(my_bool archive, THD *thd) noexcept thd_wait_end(thd); if (wait_lsn) mtr_flush_ahead(wait_lsn); + return fail; } -/** Start resizing the log and release the exclusive latch. -@param size requested new file_size -@param thd the current thread identifier +/** Start resizing the log. +@param size requested new file_size +@param thd the current thread identifier +@param backup whether the caller is backup_start() or backup_stop() @return whether the resizing was started successfully */ -log_t::resize_start_status log_t::resize_start(os_offset_t size, void *thd) - noexcept +log_t::resize_start_status log_t::resize_start(uint64_t size, void *thd, + bool backup) noexcept { ut_ad(size >= 4U << 20); ut_ad(!(size & 4095)); @@ -988,6 +1001,9 @@ log_t::resize_start_status log_t::resize_start(os_offset_t size, void *thd) resize_target= size; } } + else if (!backup && this->backup) + /* backup_start() or backup_stop() is running */ + status= RESIZE_FAILED; else { lsn_t start_lsn; @@ -1089,6 +1105,44 @@ log_t::resize_start_status log_t::resize_start(os_offset_t size, void *thd) return status; } +/** Wait for the completion of resize_start() == RESIZE_STARTED */ +void log_t::resize_finish(THD *thd) noexcept +{ + for (timespec abstime;;) + { + if (thd_kill_level(thd)) + { + resize_abort(thd); + break; + } + + set_timespec(abstime, 5); + mysql_mutex_lock(&buf_pool.flush_list_mutex); + lsn_t resizing= resize_in_progress(); + if (resizing > buf_pool.get_oldest_modification(0)) + { + buf_pool.page_cleaner_wakeup(true); + my_cond_timedwait(&buf_pool.done_flush_list, + &buf_pool.flush_list_mutex.m_mutex, &abstime); + resizing= resize_in_progress(); + } + mysql_mutex_unlock(&buf_pool.flush_list_mutex); + if (!resizing || !resize_running(thd)) + break; + latch.wr_lock(); + while (resizing > get_lsn()) + { + ut_ad(!is_mmap()); + /* The server is almost idle. Write dummy FILE_CHECKPOINT records + to ensure that the log resizing will complete. */ + mtr_t mtr{nullptr}; + mtr.start(); + mtr.commit_files(last_checkpoint_lsn); + } + latch.wr_unlock(); + } +} + /** Abort a resize_start() that we started. */ void log_t::resize_abort(void *thd) noexcept { diff --git a/storage/innobase/log/log0recv.cc b/storage/innobase/log/log0recv.cc index 59dd21fddd917..0f56add395cc0 100644 --- a/storage/innobase/log/log0recv.cc +++ b/storage/innobase/log/log0recv.cc @@ -2099,6 +2099,7 @@ dberr_t recv_sys_t::find_checkpoint() memset_aligned<4096>(const_cast(field_ref_zero), 0, 4096); /* Mark the redo log for upgrading. */ lsn= file_checkpoint= log_sys.last_checkpoint_lsn; + log_sys.archived_checkpoint= lsn; log_sys.set_recovered_lsn(lsn); if (rpo && rpo != lsn) { @@ -2177,7 +2178,8 @@ dberr_t recv_sys_t::find_checkpoint() log_sys.set_recovered_checkpoint(checkpoint_lsn, lsn= end_lsn, field == log_t::CHECKPOINT_1); } - if (!log_sys.last_checkpoint_lsn) + log_sys.archived_checkpoint= log_sys.last_checkpoint_lsn; + if (!log_sys.archived_checkpoint) goto got_no_checkpoint; else if (!log_sys.archived_lsn) log_sys.archived_lsn= lsn; diff --git a/storage/innobase/os/os0file.cc b/storage/innobase/os/os0file.cc index 6494c5e21b96e..4ef86095f78a3 100644 --- a/storage/innobase/os/os0file.cc +++ b/storage/innobase/os/os0file.cc @@ -2017,7 +2017,15 @@ os_file_create_func( ); DWORD create_flag = OPEN_EXISTING; - DWORD share_mode = read_only + /* BACKUP SERVER may invoke CreateHardLink() on a log file that + may concurrently be written to. This is why we must allow + FILE_SHARE_WRITE. This has the side effect that multiple InnoDB + instances may be concurrently started on the same log file. + However, InnoDB will not write any log before it has successfully + opened data files. As long as the multiple instances are also + opening the same InnoDB data files (such as the system tablespace), + they should fail to start up concurrently. */ + DWORD share_mode = read_only || type == OS_LOG_FILE ? FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE : FILE_SHARE_READ | FILE_SHARE_DELETE; diff --git a/storage/innobase/trx/trx0roll.cc b/storage/innobase/trx/trx0roll.cc index 11d411d7a42f2..02e78965c1383 100644 --- a/storage/innobase/trx/trx0roll.cc +++ b/storage/innobase/trx/trx0roll.cc @@ -174,6 +174,7 @@ dberr_t trx_t::rollback(const undo_no_t *savept) noexcept case TRX_STATE_PREPARED: case TRX_STATE_PREPARED_RECOVERED: case TRX_STATE_COMMITTED_IN_MEMORY: + case TRX_STATE_BACKUP: ut_ad("invalid state" == 0); /* fall through */ case TRX_STATE_ACTIVE: @@ -260,6 +261,8 @@ dberr_t trx_rollback_for_mysql(trx_t* trx) case TRX_STATE_COMMITTED_IN_MEMORY: ut_ad(!trx->is_autocommit_non_locking()); break; + case TRX_STATE_BACKUP: + break; } ut_error; diff --git a/storage/innobase/trx/trx0sys.cc b/storage/innobase/trx/trx0sys.cc index 2f2265a3df1fd..7c67841911bac 100644 --- a/storage/innobase/trx/trx0sys.cc +++ b/storage/innobase/trx/trx0sys.cc @@ -54,6 +54,7 @@ void rw_trx_hash_t::validate_element(trx_t *trx) switch (trx->state) { case TRX_STATE_NOT_STARTED: case TRX_STATE_ABORTED: + case TRX_STATE_BACKUP: ut_error; case TRX_STATE_PREPARED: case TRX_STATE_PREPARED_RECOVERED: @@ -380,6 +381,7 @@ size_t trx_sys_t::any_active_transactions(size_t *prepared) switch (trx.state) { case TRX_STATE_NOT_STARTED: case TRX_STATE_ABORTED: + case TRX_STATE_BACKUP: break; case TRX_STATE_ACTIVE: if (!trx.id) diff --git a/storage/innobase/trx/trx0trx.cc b/storage/innobase/trx/trx0trx.cc index 551819aef4cf0..9f45dfbe8930d 100644 --- a/storage/innobase/trx/trx0trx.cc +++ b/storage/innobase/trx/trx0trx.cc @@ -1660,6 +1660,7 @@ trx_commit_or_rollback_prepare( case TRX_STATE_COMMITTED_IN_MEMORY: case TRX_STATE_ABORTED: + case TRX_STATE_BACKUP: break; } @@ -1744,6 +1745,7 @@ void trx_commit_for_mysql(trx_t *trx) noexcept trx->op_info= ""; break; case TRX_STATE_COMMITTED_IN_MEMORY: + case TRX_STATE_BACKUP: ut_error; break; } @@ -1813,6 +1815,9 @@ trx_print_low( case TRX_STATE_COMMITTED_IN_MEMORY: fputs(", COMMITTED IN MEMORY", f); goto state_ok; + case TRX_STATE_BACKUP: + fputs(", BACKUP SERVER", f); + goto state_ok; } fprintf(f, ", state %lu", (ulong) trx->state); ut_ad(0); @@ -2172,6 +2177,7 @@ trx_start_if_not_started_xa_low( case TRX_STATE_PREPARED: case TRX_STATE_PREPARED_RECOVERED: case TRX_STATE_COMMITTED_IN_MEMORY: + case TRX_STATE_BACKUP: break; } @@ -2201,6 +2207,7 @@ trx_start_if_not_started_low( case TRX_STATE_PREPARED: case TRX_STATE_PREPARED_RECOVERED: case TRX_STATE_COMMITTED_IN_MEMORY: + case TRX_STATE_BACKUP: break; } From 10539aa463aceed07c0c08b52d944a0d5e054525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20M=C3=A4kel=C3=A4?= Date: Mon, 8 Jun 2026 12:34:53 +0300 Subject: [PATCH 05/20] WIP MDEV-39092, and back up non-InnoDB files --- mysql-test/main/backup_server_restore.result | 41 ++ mysql-test/main/backup_server_restore.test | 38 ++ .../suite/backup/backup_innodb,debug.rdiff | 2 +- mysql-test/suite/backup/backup_innodb.result | 12 +- mysql-test/suite/backup/backup_innodb.test | 52 ++- storage/innobase/handler/backup_innodb.cc | 2 +- storage/maria/CMakeLists.txt | 1 + storage/maria/ha_maria.cc | 4 + storage/maria/ma_backup.cc | 362 ++++++++++++++++++ storage/maria/ma_backup.h | 47 +++ 10 files changed, 544 insertions(+), 17 deletions(-) create mode 100644 mysql-test/main/backup_server_restore.result create mode 100644 mysql-test/main/backup_server_restore.test create mode 100644 storage/maria/ma_backup.cc create mode 100644 storage/maria/ma_backup.h diff --git a/mysql-test/main/backup_server_restore.result b/mysql-test/main/backup_server_restore.result new file mode 100644 index 0000000000000..83bf230bc8619 --- /dev/null +++ b/mysql-test/main/backup_server_restore.result @@ -0,0 +1,41 @@ +Prepare database +CREATE TABLE tinno (i INTEGER) ENGINE=InnoDB; +INSERT INTO tinno VALUES (1), (2), (3), (4); +CREATE TABLE tariatr (i INTEGER) ENGINE=Aria TRANSACTIONAL=1; +INSERT INTO tariatr VALUES (2), (3), (5), (7); +CREATE TABLE tariant (i INTEGER) ENGINE=Aria TRANSACTIONAL=0; +INSERT INTO tariant VALUES (1), (1), (2), (3), (5); +Back up the database +BACKUP SERVER TO '$MYSQLTEST_VARDIR/some_directory'; +Restore the database +# restart: --datadir=MYSQLTEST_VARDIR/some_directory +Check contents after restore +SELECT * FROM tinno; +i +1 +2 +3 +4 +SELECT * FROM tariatr; +i +2 +3 +5 +7 +SELECT * FROM tariant; +i +1 +1 +2 +3 +5 +Warnings: +Error 145 Got error '145 "Table was marked as crashed and should be repaired"' for './test/tariant' +Warning 1034 1 client is using or hasn't closed the table properly +Note 1034 Table is fixed +Restart database in original data directory +# restart +Clean up +DROP TABLE tinno; +DROP TABLE tariatr; +DROP TABLE tariant; diff --git a/mysql-test/main/backup_server_restore.test b/mysql-test/main/backup_server_restore.test new file mode 100644 index 0000000000000..ef1f5af27a68b --- /dev/null +++ b/mysql-test/main/backup_server_restore.test @@ -0,0 +1,38 @@ +--source include/not_windows.inc +--source include/have_innodb.inc + +--echo Prepare database +CREATE TABLE tinno (i INTEGER) ENGINE=InnoDB; +INSERT INTO tinno VALUES (1), (2), (3), (4); +CREATE TABLE tariatr (i INTEGER) ENGINE=Aria TRANSACTIONAL=1; +INSERT INTO tariatr VALUES (2), (3), (5), (7); +CREATE TABLE tariant (i INTEGER) ENGINE=Aria TRANSACTIONAL=0; +INSERT INTO tariant VALUES (1), (1), (2), (3), (5); + +--echo Back up the database +evalp BACKUP SERVER TO '$MYSQLTEST_VARDIR/some_directory'; + +--echo Restore the database +--disable_query_log +call mtr.add_suppression("InnoDB: Did not find any checkpoint after LSN="); +call mtr.add_suppression("InnoDB: Renaming ib_[0-9]+.log to ib_logfile0"); +call mtr.add_suppression("mariadbd: Got error '145 \"Table was marked as crashed and should be repaired\"' for "); +call mtr.add_suppression("Checking table: "); +--enable_query_log +--let $restart_parameters=--datadir=$MYSQLTEST_VARDIR/some_directory +--source include/restart_mysqld.inc + +--echo Check contents after restore +SELECT * FROM tinno; +SELECT * FROM tariatr; +SELECT * FROM tariant; + +--echo Restart database in original data directory +--let $restart_parameters= +--source include/restart_mysqld.inc + +--echo Clean up +DROP TABLE tinno; +DROP TABLE tariatr; +DROP TABLE tariant; +--rmdir $MYSQLTEST_VARDIR/some_directory diff --git a/mysql-test/suite/backup/backup_innodb,debug.rdiff b/mysql-test/suite/backup/backup_innodb,debug.rdiff index 02d25801b75a7..ed165bdf9e59e 100644 --- a/mysql-test/suite/backup/backup_innodb,debug.rdiff +++ b/mysql-test/suite/backup/backup_innodb,debug.rdiff @@ -5,7 +5,7 @@ DELETE FROM t; connect backup,localhost,root; +SET DEBUG_SYNC='innodb_backup_start SIGNAL start WAIT_FOR resume'; - BACKUP SERVER TO 'MYSQLTEST_VARDIR/some_directory'; + BACKUP SERVER TO 'target_directory'; +connection default; +SET DEBUG_SYNC='now WAIT_FOR start'; +INSERT INTO t(a) SELECT * FROM seq_1_to_30000; diff --git a/mysql-test/suite/backup/backup_innodb.result b/mysql-test/suite/backup/backup_innodb.result index e1e856e93bae0..7ab700975ac78 100644 --- a/mysql-test/suite/backup/backup_innodb.result +++ b/mysql-test/suite/backup/backup_innodb.result @@ -2,7 +2,7 @@ CREATE TABLE t(a INT PRIMARY KEY, b CHAR(255) DEFAULT '' NOT NULL, INDEX(b)) ENGINE=INNODB; BEGIN; INSERT INTO t SET a=1; -BACKUP SERVER TO '$MYSQLTEST_VARDIR/some_directory'; +BACKUP SERVER TO '$target_directory'; ROLLBACK; SELECT * FROM t; a b @@ -10,15 +10,21 @@ a b BEGIN; DELETE FROM t; connect backup,localhost,root; -BACKUP SERVER TO 'MYSQLTEST_VARDIR/some_directory'; +BACKUP SERVER TO 'target_directory'; disconnect backup; connection default; ROLLBACK; SELECT * FROM t; a b 1 -# restart +DELETE FROM t; +# restart: --defaults-file=MYSQLTEST_VARDIR/some_directory/backup.cnf --datadir=MYSQLTEST_VARDIR/some_directory SELECT * FROM t; a b 1 +DELETE FROM t; +ERROR HY000: Table 't' is read only +# restart +SELECT * FROM t; +a b DROP TABLE t; diff --git a/mysql-test/suite/backup/backup_innodb.test b/mysql-test/suite/backup/backup_innodb.test index 5f7bde34a31dd..769c256f8c3d0 100644 --- a/mysql-test/suite/backup/backup_innodb.test +++ b/mysql-test/suite/backup/backup_innodb.test @@ -7,8 +7,17 @@ ENGINE=INNODB; BEGIN; INSERT INTO t SET a=1; -evalp BACKUP SERVER TO '$MYSQLTEST_VARDIR/some_directory'; ---rmdir $MYSQLTEST_VARDIR/some_directory +--let $target_directory=/tmp/some_directory$MTR_COMBINATION_ARCHIVED +# comment out the following line (and some more, including rmdir), +# to test cross-filesystem copy +--let $target_directory=$MYSQLTEST_VARDIR/some_directory + +# Clean up after a previous failed test, in case we are retrying. +--error 0,1 +--rmdir $target_directory + +evalp BACKUP SERVER TO '$target_directory'; +--rmdir $target_directory ROLLBACK; # BACKUP SERVER will implicitly commit the current transaction SELECT * FROM t; @@ -19,31 +28,50 @@ DELETE FROM t; --connect backup,localhost,root if ($have_debug) { SET DEBUG_SYNC='innodb_backup_start SIGNAL start WAIT_FOR resume'; ---replace_result $MYSQLTEST_VARDIR MYSQLTEST_VARDIR -send_eval BACKUP SERVER TO '$MYSQLTEST_VARDIR/some_directory'; +--replace_result $target_directory target_directory +send_eval BACKUP SERVER TO '$target_directory'; --connection default SET DEBUG_SYNC='now WAIT_FOR start'; INSERT INTO t(a) SELECT * FROM seq_1_to_30000; SET DEBUG_SYNC='now SIGNAL resume'; --connection backup +# FIXME: outside PMEM we may get ER_ERROR_ON_RENAME --reap } if (!$have_debug) { ---replace_result $MYSQLTEST_VARDIR MYSQLTEST_VARDIR -eval BACKUP SERVER TO '$MYSQLTEST_VARDIR/some_directory'; +--replace_result $target_directory target_directory +eval BACKUP SERVER TO '$target_directory'; } --disconnect backup --connection default ROLLBACK; SELECT * FROM t; +DELETE FROM t; -let $datadir=`SELECT @@datadir`; ---source include/shutdown_mysqld.inc -#--rmdir $datadir -#--move_file $MYSQLTEST_VARDIR/some_directory $datadir ---rmdir $MYSQLTEST_VARDIR/some_directory ---source include/start_mysqld.inc +if ($MARIADB_UPGRADE_EXE) { +let target_directory=$target_directory; +perl; +open IN, "<", "$ENV{MYSQLTEST_VARDIR}/my.cnf"; +open OUT, ">>", "$ENV{target_directory}/backup.cnf"; +print OUT while (); +close(IN); +close(OUT); +EOF +} +if (!$MARIADB_UPGRADE_EXE) { + --exec cat $MYSQLTEST_VARDIR/my.cnf >> $target_directory/backup.cnf +} +--let $restart_parameters=--defaults-file=$target_directory/backup.cnf --datadir=$target_directory +--source include/restart_mysqld.inc +SELECT * FROM t; +# we must have started up with nonzero innodb_log_recovery_target +--error ER_OPEN_AS_READONLY +DELETE FROM t; +--let $restart_parameters= +--source include/restart_mysqld.inc SELECT * FROM t; DROP TABLE t; + +--rmdir $target_directory diff --git a/storage/innobase/handler/backup_innodb.cc b/storage/innobase/handler/backup_innodb.cc index fee9196ffb1bb..37b0848d415d5 100644 --- a/storage/innobase/handler/backup_innodb.cc +++ b/storage/innobase/handler/backup_innodb.cc @@ -594,7 +594,7 @@ class InnoDB_backup @param c offset of the FILE_CHECKPOINT mini-transaction @return error code @retval 0 on success */ - static int write_checkpoint(int dst, uint64_t c) noexcept + static int write_checkpoint(IF_WIN(HANDLE,int) dst, uint64_t c) noexcept { #ifdef _WIN32 using tpool::pwrite; diff --git a/storage/maria/CMakeLists.txt b/storage/maria/CMakeLists.txt index 9bdd729840077..7b4270ed4366d 100644 --- a/storage/maria/CMakeLists.txt +++ b/storage/maria/CMakeLists.txt @@ -45,6 +45,7 @@ SET(ARIA_SOURCES ma_init.c ma_open.c ma_extra.c ma_info.c ma_rkey.c ha_maria.h maria_def.h ma_recovery_util.c ma_servicethread.c ma_norec.c ma_crypt.c ma_backup.c + ma_backup.cc ma_backup.h ) IF(APPLE) diff --git a/storage/maria/ha_maria.cc b/storage/maria/ha_maria.cc index 8f5e47daea728..624a7f0a043d3 100644 --- a/storage/maria/ha_maria.cc +++ b/storage/maria/ha_maria.cc @@ -23,6 +23,7 @@ #include #include #include "ha_maria.h" +#include "ma_backup.h" #include "trnman_public.h" #include "trnman.h" @@ -3942,6 +3943,9 @@ static int ha_maria_init(void *p) maria_hton->prepare_for_backup= maria_prepare_for_backup; maria_hton->end_backup= maria_end_backup; maria_hton->update_optimizer_costs= aria_update_optimizer_costs; + maria_hton->backup_start= aria_backup_start; + maria_hton->backup_step= aria_backup_step; + maria_hton->backup_end= aria_backup_end; /* TODO: decide if we support Maria being used for log tables */ maria_hton->flags= (HTON_CAN_RECREATE | HTON_SUPPORT_LOG_TABLES | diff --git a/storage/maria/ma_backup.cc b/storage/maria/ma_backup.cc new file mode 100644 index 0000000000000..9d3e0c1a3d82e --- /dev/null +++ b/storage/maria/ma_backup.cc @@ -0,0 +1,362 @@ +/* Copyright (c) 2026, MariaDB plc + + 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 Foundation; version 2 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1335 USA */ + +#include "maria_def.h" +#include "ma_backup.h" +#include +#include +#include +#include +#include + +#ifdef __APPLE__ +#include +#include +#include +#endif + +/* + Implementation of functions declatred in ma_backup.h: + BACKUP SERVER support for Aria engine +*/ + +using namespace std::string_literals; +namespace +{ + class Source_dir + { + public: + Source_dir(const char* path, myf flags) noexcept + { + dir_info= my_dir(path, flags); + if (!dir_info) + { + my_error(ER_CANT_READ_DIR, MYF(0), path, my_errno); + } + } + ~Source_dir() noexcept + { + my_dirend(dir_info); + } + bool is_error() const noexcept + { + return !dir_info; + } + template + int for_each(Fn fn) const noexcept + { + for (size_t i= 0; i < dir_info->number_of_files; i++) + { + if (fn(dir_info->dir_entry[i]) != 0) + return 1; + } + return 0; + } + + private: + MY_DIR *dir_info {nullptr}; + }; + + + /** Backup state; protected by log_sys.latch */ + class Aria_backup + { + public: + explicit Aria_backup(THD *thd, backup_target tgt) noexcept + : target(tgt) +#ifndef _WIN32 + , datadir_fd(open(maria_data_root, O_DIRECTORY)) + { + if (datadir_fd < 0) + { + my_error(ER_CANT_READ_DIR, MYF(0), maria_data_root, errno); + return; + } +#else + { +#endif // _WIN32 + translog_disable_purge(); + } + + bool is_initialized() const noexcept + { +#ifndef _WIN32 + return datadir_fd >= 0; +#else + return true; +#endif // _WIN32 + } + + ~Aria_backup() noexcept + { +#ifndef _WIN32 + if (datadir_fd >= 0) + close(datadir_fd); +#endif // _WIN32 + } + + int end(THD *thd, bool abort) noexcept + { + int ret_val = 0; + if (!abort) { + if (int err= perform_backup() != 0) + { + ret_val= err; + }; + } + translog_enable_purge(); + return ret_val; + } + private: + backup_target target; +#ifndef _WIN32 + const int datadir_fd; +#endif + static const std::vector data_exts; + static const std::string log_file_prefix; + using dir_name = std::string; + using dir_contents = std::vector; + using database_dir = std::pair; + std::vector database_dirs; + std::vector log_files; + bool have_control_file = false; + + int perform_backup() noexcept + { + if (scan_datadir()) + return 1; + if (copy_databases()) + return 1; + if (copy_control_file()) + return 1; + if(translog_flush(translog_get_horizon())) + return 1; + if (copy_logs()) + return 1; + return 0; + } + + int scan_datadir() noexcept + { + const char* base_dir = maria_data_root; + Source_dir datadir(base_dir, MYF(MY_WANT_STAT)); + if (datadir.is_error()) + return 1; + datadir.for_each([this](const fileinfo &fi) + { + if (fi.mystat->st_mode & S_IFDIR) + { + if (scan_database_dir(fi.name) != 0) + return 1; + } else if (begins_with(fi.name, log_file_prefix)) + log_files.emplace_back(fi.name); + else if (strcmp(fi.name, "aria_log_control") == 0) + have_control_file = true; + return 0; + }); + return 0; + } + + int scan_database_dir(const char* dir_name) noexcept + { + const char* base_dir = maria_data_root; + std::string dir_path = std::string(base_dir) + "/" + dir_name; + Source_dir db_dir(dir_path.c_str(), MYF(0)); + if (db_dir.is_error()) + return 1; + std::vector files_to_backup; + db_dir.for_each([&files_to_backup](const fileinfo &fi) + { + if (is_db_file(fi.name)) + files_to_backup.emplace_back(fi.name); + return 0; + }); + if (!files_to_backup.empty()) + database_dirs.emplace_back(dir_name, std::move(files_to_backup)); + return 0; + } + + int copy_databases() noexcept + { + for (const database_dir& dir : database_dirs) + { + const char* dir_name = dir.first.c_str(); + if (ensure_target_subdir(dir_name) != 0) + { + my_error(ER_CANT_CREATE_FILE, MYF(0), dir_name, errno); + return 1; + } + if (copy_database(dir) != 0) + return 1; + } + return 0; + } + + /* + Create directory in the target directory if it does not exist. + Return 0 on success, non-0 on failure. Set errno in case of failure + */ + int ensure_target_subdir(const char* name) noexcept + { +#ifdef _WIN32 + std::string dir_path= targetPath() + "/" + name; + if (!CreateDirectory(dir_path.c_str(), nullptr)) + { + DWORD err = GetLastError(); + if (err != ERROR_ALREADY_EXISTS) + { + my_osmaperr(err); + return 1; + } + } +#else + if (mkdirat(target.fd, name, 0777) != 0) + return (errno != EEXIST); +#endif + return 0; + } + + int copy_database(const database_dir& dir) noexcept + { + for (const std::string& file : dir.second) + { + std::string file_path= dir.first + "/" + file; + if (copy_file(file_path) != 0) + return 1; + } + return 0; + } + + int copy_control_file() noexcept + { + if (!have_control_file) + return 0; + return copy_file("aria_log_control"); + } + + int copy_logs() noexcept + { + for (const std::string& file : log_files) + { + if (copy_file(file) != 0) + return 1; + } + return 0; + } + + int copy_file(const std::string &path) const noexcept + { +#ifndef _WIN32 + int ret_val = 0; + int src_fd = openat(datadir_fd, path.c_str(), O_RDONLY); + if (src_fd < 0) + { + my_error(ER_CANT_OPEN_FILE, MYF(0), path.c_str(), errno); + return 1; + } + int tgt_fd = openat(target.fd, path.c_str(), + O_CREAT | O_EXCL | O_WRONLY, 0777); + if (tgt_fd < 0) + { + my_error(ER_CANT_CREATE_FILE, MYF(0), path.c_str(), errno); + ret_val = 1; + goto finish; + } + if (copy_entire_file(src_fd, tgt_fd) != 0) + { + my_error(ER_ERROR_ON_WRITE, MYF(0), path.c_str(), errno); + ret_val = 1; + } + close(tgt_fd); + finish: + close(src_fd); + return ret_val; +#else + std::string src_path= std::string(maria_data_root) + "/" + path; + std::string dest_path= targetPath() + "/" + path; + if(!CopyFileExA(src_path.c_str(), dest_path.c_str(), nullptr, nullptr, nullptr, + COPY_FILE_NO_BUFFERING)) + { + my_osmaperr(GetLastError()); + my_error(ER_CANT_CREATE_FILE, MYF(0), dest_path.c_str(), errno); + return 1; + } + return 0; +#endif + } + + + static bool is_db_file(const char* file_name) noexcept + { + for (const std::string& ext : data_exts) + { + if (ends_with(file_name, ext)) + return true; + } + /* As a stop-gap db/opt files are also copied here, this should be done in SQL layer. */ + return !strcmp(file_name, "db.opt"); + } + + static bool ends_with(const char* str, const std::string& suffix) noexcept + { + size_t str_len = strlen(str); + size_t suffix_len = suffix.size(); + if (str_len < suffix_len) + return false; + return memcmp(str + str_len - suffix_len, + suffix.data(), + suffix_len) == 0; + } + + static bool begins_with(const char* str, const std::string& prefix) noexcept + { + return strncmp(str, prefix.data(), prefix.size()) == 0; + } + +#ifdef _WIN32 + /** @return the target directory path */ + std::string targetPath() const + { + return std::string(target.path); + } +#endif + }; + + /* TODO: .frm failes are not Aria-specific, .MYD and .MYI are MyISAM files; + they are copied here as a stop-gap */ + const std::vector + Aria_backup::data_exts {".MAD"s, ".MAI"s, "MYD"s, "MYI"s, "frm"s}; + const std::string Aria_backup::log_file_prefix {"aria_log."}; + + std::unique_ptr aria_backup; +} + +int aria_backup_start(THD *thd, backup_target target) noexcept +{ + aria_backup= std::make_unique(thd, target); + return !aria_backup->is_initialized(); +} + +int aria_backup_step(THD *thd) noexcept +{ + return 0; +} + +int aria_backup_end(THD *thd, bool abort) noexcept +{ + int ret_val= aria_backup->end(thd, abort); + aria_backup.reset(); + return ret_val; +} diff --git a/storage/maria/ma_backup.h b/storage/maria/ma_backup.h new file mode 100644 index 0000000000000..3bec606648dac --- /dev/null +++ b/storage/maria/ma_backup.h @@ -0,0 +1,47 @@ +/* Copyright (c) 2026, MariaDB plc + + 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 Foundation; version 2 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1335 USA */ + +#pragma once + +/* BACKUP SERVER support for Aria engine. */ + +#include +#include + +/** + Start of BACKUP SERVER: collect all files to be backed up + @param thd current session + @param target target directory + @return error code + @retval 0 on success +*/ +int aria_backup_start(THD *thd, backup_target target) noexcept; + +/** + Process a file that was collected in backup_start(). + @param thd current session + @return number of files remaining, or negative on error + @retval 0 on completion +*/ +int aria_backup_step(THD *thd) noexcept; + +/** + Finish copying and determine the logical time of the backup snapshot. + @param thd current session + @param abort whether BACKUP SERVER was aborted + @return error code + @retval 0 on success +*/ +int aria_backup_end(THD *thd, bool abort) noexcept; From ef9193898a5071bd44a098bb527ebf70f8655973 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20M=C3=A4kel=C3=A4?= Date: Tue, 9 Jun 2026 11:19:08 +0300 Subject: [PATCH 06/20] fixup! 10f3acb01596ce664b8e7a96498eb05ca2385dd5 Simplify the backup_target --- sql/handler.h | 19 ++++++++----------- sql/sql_backup.cc | 6 +++--- storage/innobase/handler/backup_innodb.cc | 7 ++----- 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/sql/handler.h b/sql/handler.h index e9a2f7e164e08..10e51eb3bca86 100644 --- a/sql/handler.h +++ b/sql/handler.h @@ -1500,21 +1500,18 @@ struct transaction_participant struct backup_target { #ifdef _WIN32 - /** Target directory path name */ + /** Target or spare directory path name */ const char *path; - union - { - /** Target pipe, if path==reinterpret_cast(-1) */ - HANDLE pipe; - /** Target socket, if path==nullptr */ - SOCKET socket; - }; + /** A value indicating an invalid stream */ + static constexpr HANDLE NO_STREAM{INVALID_HANDLE_VALUE}; #else - /** Target file descriptor */ + /** Target or spare directory descriptor */ int fd; - /** whether the fd is a directory handle */ - bool directory; + /** A value indicating an invalid stream */ + static constexpr int NO_STREAM{-1}; #endif + /** Target pipe, or NO_STREAM if copying to the target directory */ + static constexpr IF_WIN(HANDLE,int) stream{NO_STREAM}; }; /* diff --git a/sql/sql_backup.cc b/sql/sql_backup.cc index 607116e725441..4e8c557aebe9c 100644 --- a/sql/sql_backup.cc +++ b/sql/sql_backup.cc @@ -238,7 +238,7 @@ extern "C" int backup_config_append(const backup_target &target, } } #else - assert(target.directory); + assert(target.stream == target.NO_STREAM); int dst= openat(target.fd, "backup.cnf", O_CREAT | O_EXCL | O_TRUNC | O_WRONLY, 0666); if (dst < 0) @@ -327,9 +327,9 @@ bool Sql_cmd_backup::execute(THD *thd) } #ifdef _WIN32 - backup_target dir{target.str, INVALID_HANDLE_VALUE}; + backup_target dir{target.str}; #else - backup_target dir{open(target.str, O_DIRECTORY), true}; + backup_target dir{open(target.str, O_DIRECTORY)}; if (dir.fd < 0) { my_error(EE_CANT_MKDIR, MYF(ME_BELL), target.str, errno); diff --git a/storage/innobase/handler/backup_innodb.cc b/storage/innobase/handler/backup_innodb.cc index 37b0848d415d5..0726edf0d50fe 100644 --- a/storage/innobase/handler/backup_innodb.cc +++ b/storage/innobase/handler/backup_innodb.cc @@ -334,6 +334,7 @@ class InnoDB_backup if (ctx.max_first_lsn) { /* Copy our clone of the last log until the final LSN */ + ut_ad(target.stream == target.NO_STREAM); #ifdef _WIN32 std::string src{target.path}; src.push_back('/'); @@ -401,7 +402,6 @@ class InnoDB_backup } } #else - ut_ad(target.directory); int s= openat(target.fd, "ib_logfile101", O_RDONLY); std::string dst; log_sys.append_archive_name(dst, ctx.max_first_lsn); @@ -413,7 +413,6 @@ class InnoDB_backup fail= 1; goto done; } - ut_ad(target.directory); d= openat(target.fd, dst.c_str(), O_CREAT | O_EXCL | O_TRUNC | O_WRONLY, 0666); if (d < 0) @@ -495,6 +494,7 @@ class InnoDB_backup { for (bool tried_mkdir{false};;) { + ut_ad(target.stream == target.NO_STREAM); #ifdef _WIN32 std::string path{target.path}; path.push_back('/'); @@ -522,7 +522,6 @@ class InnoDB_backup } #else int f; - ut_ad(target.directory); # ifdef __APPLE__ backup_start(node->space, (node->space->size + fil_space_t::BACKUP_BATCH_SIZE - 1) & @@ -657,7 +656,6 @@ class InnoDB_backup else basename++; const bool move{!clone && !ctx.archived}; - #ifdef _WIN32 std::string b{target.path}; b.push_back('/'); @@ -779,7 +777,6 @@ class InnoDB_backup else if (clone) *clone= true; #else - ut_ad(target.directory); if (move ? !renameat(AT_FDCWD, path, target.fd, basename) : !linkat(AT_FDCWD, path, target.fd, From dec5d45424898517aa053d6b73d3e6edb9ac20b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20M=C3=A4kel=C3=A4?= Date: Tue, 9 Jun 2026 11:39:57 +0300 Subject: [PATCH 07/20] fixup! ef9193898a5071bd44a098bb527ebf70f8655973 backup_config_append(): C compatible API --- sql/sql_backup.cc | 8 ++++---- sql/sql_backup_interface.h | 2 +- storage/innobase/handler/backup_innodb.cc | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/sql/sql_backup.cc b/sql/sql_backup.cc index 4e8c557aebe9c..76aea09a859aa 100644 --- a/sql/sql_backup.cc +++ b/sql/sql_backup.cc @@ -208,14 +208,15 @@ extern "C" int copy_file(IF_WIN(const native_file_handle&,int) src, @param size length of the snippet @return error code (non-positive) @retval 0 on success */ -extern "C" int backup_config_append(const backup_target &target, +extern "C" int backup_config_append(const backup_target *target, const char *config, size_t size) { /* FIXME: append to a pre-created configuration file */ + assert(target->stream == backup_target::NO_STREAM); #ifdef _WIN32 HANDLE dst; { - std::string path{target.path}; + std::string path{target->path}; path.append("/backup.cnf"); dst= CreateFile(path.c_str(), GENERIC_WRITE, 0, my_win_file_secattr(), CREATE_NEW, @@ -238,8 +239,7 @@ extern "C" int backup_config_append(const backup_target &target, } } #else - assert(target.stream == target.NO_STREAM); - int dst= openat(target.fd, "backup.cnf", + int dst= openat(target->fd, "backup.cnf", O_CREAT | O_EXCL | O_TRUNC | O_WRONLY, 0666); if (dst < 0) return dst; diff --git a/sql/sql_backup_interface.h b/sql/sql_backup_interface.h index 478107e1e2e50..1593ede060470 100644 --- a/sql/sql_backup_interface.h +++ b/sql/sql_backup_interface.h @@ -67,5 +67,5 @@ extern "C" @param size length of the snippet @return error code (non-positive) @retval 0 on success */ -int backup_config_append(const backup_target &target, +int backup_config_append(const backup_target *target, const char *config, size_t size); diff --git a/storage/innobase/handler/backup_innodb.cc b/storage/innobase/handler/backup_innodb.cc index 0726edf0d50fe..68fcc01e709d6 100644 --- a/storage/innobase/handler/backup_innodb.cc +++ b/storage/innobase/handler/backup_innodb.cc @@ -633,7 +633,7 @@ class InnoDB_backup "innodb_log_recovery_start=" LSN_PF "\n" "innodb_log_recovery_target=" LSN_PF "\n", ctx.checkpoint, ctx.checkpoint_end_lsn, ctx.last_lsn); - return backup_config_append(target, config, size_t(size)); + return backup_config_append(&target, config, size_t(size)); } /** Hard-link (copy) or rename (move) an archive log file. From b868d24c7fb9d1409dd4cb25fc737b219ee84154 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20M=C3=A4kel=C3=A4?= Date: Tue, 9 Jun 2026 11:54:40 +0300 Subject: [PATCH 08/20] fixup! dec5d45424898517aa053d6b73d3e6edb9ac20b8 --- sql/sql_backup_interface.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sql/sql_backup_interface.h b/sql/sql_backup_interface.h index 1593ede060470..39bebc635ffbc 100644 --- a/sql/sql_backup_interface.h +++ b/sql/sql_backup_interface.h @@ -67,5 +67,5 @@ extern "C" @param size length of the snippet @return error code (non-positive) @retval 0 on success */ -int backup_config_append(const backup_target *target, +int backup_config_append(const struct backup_target *target, const char *config, size_t size); From 75ff32fb3b75ac75cf50eed136df712bf5e66d71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20M=C3=A4kel=C3=A4?= Date: Tue, 9 Jun 2026 12:53:15 +0300 Subject: [PATCH 09/20] fixup! b868d24c7fb9d1409dd4cb25fc737b219ee84154 --- sql/sql_backup.cc | 8 ++++---- sql/sql_backup_interface.h | 4 ++-- storage/innobase/handler/backup_innodb.cc | 7 ++++--- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/sql/sql_backup.cc b/sql/sql_backup.cc index 76aea09a859aa..4e1c1db5cb529 100644 --- a/sql/sql_backup.cc +++ b/sql/sql_backup.cc @@ -175,8 +175,8 @@ extern "C" int copy_entire_file(int src, int dst) @param end last offset to copy (exclusive) @return error code (non-positive) @retval 0 on success */ -extern "C" int copy_file(IF_WIN(const native_file_handle&,int) src, - IF_WIN(const native_file_handle&,int) dst, +extern "C" int copy_file(IF_WIN(const native_file_handle*,int) src, + IF_WIN(const native_file_handle*,int) dst, uint64_t start, uint64_t end) { assert(end >= start); @@ -192,10 +192,10 @@ extern "C" int copy_file(IF_WIN(const native_file_handle&,int) src, ret= (start != 0 && off_t(start) != lseek(dst, start, SEEK_SET)) ? -1 : copy(src, dst, off_t(start), off_t(end)); +# elif defined _WIN32 + ret= pread_pwrite(*src, *dst, start, end); # else -# ifndef _WIN32 if ((ret= mmap_copy(src, dst, start, end)) == 1) -# endif ret= pread_pwrite(src, dst, start, end); # endif assert(ret <= 0); diff --git a/sql/sql_backup_interface.h b/sql/sql_backup_interface.h index 39bebc635ffbc..5abcf3d52caa3 100644 --- a/sql/sql_backup_interface.h +++ b/sql/sql_backup_interface.h @@ -54,8 +54,8 @@ extern "C" @param end last offset to copy (exclusive) @return error code (non-positive) @retval 0 on success */ -int copy_file(IF_WIN(const native_file_handle&,int) src, - IF_WIN(const native_file_handle&,int) dst, +int copy_file(IF_WIN(const native_file_handle*,int) src, + IF_WIN(const native_file_handle*,int) dst, uint64_t start, uint64_t end); #ifdef __cplusplus diff --git a/storage/innobase/handler/backup_innodb.cc b/storage/innobase/handler/backup_innodb.cc index 68fcc01e709d6..57ce9d71dbe9e 100644 --- a/storage/innobase/handler/backup_innodb.cc +++ b/storage/innobase/handler/backup_innodb.cc @@ -385,7 +385,7 @@ class InnoDB_backup !SetEndOfFile(d); } if (!fail) - fail= copy_file(s, d, log_sys.START_OFFSET, payload_end) || + fail= copy_file(&s, &d, log_sys.START_OFFSET, payload_end) || (ctx.max_first_lsn == ctx.first_lsn && write_checkpoint(d, ctx.checkpoint_end_lsn - ctx.first_lsn + log_sys.START_OFFSET)); @@ -571,7 +571,8 @@ class InnoDB_backup const uint32_t end{start + fil_space_t::BACKUP_BATCH_SIZE}; backup_start(node->space, end); /* TODO: avoid copying freed page ranges */ - err= copy_file(node->handle, f, start * uint64_t{page_size}, + err= copy_file(IF_WIN(&,)node->handle, IF_WIN(&,)f, + start * uint64_t{page_size}, std::min(end, file_size) * uint64_t{page_size}); backup_stop(node->space); if (err || (start= end) >= file_size) @@ -764,7 +765,7 @@ class InnoDB_backup if (!err) { - err= copy_file(s, d, payload_start, payload_end); + err= copy_file(&s, &d, payload_start, payload_end); if (!err && lsn < ctx.checkpoint) err= write_checkpoint(d, ctx.checkpoint_end_lsn - lsn + log_sys.START_OFFSET); From f257584af9db2d2d435bdb2696515c54311663bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20M=C3=A4kel=C3=A4?= Date: Tue, 9 Jun 2026 14:16:35 +0300 Subject: [PATCH 10/20] squash! 10f3acb01596ce664b8e7a96498eb05ca2385dd5 MDEV-39101: BACKUP STAGE compatible locking handlerton::backup_end: Includes backup_finalize --- mysql-test/main/backup_server_restore.result | 4 - sql/handler.h | 53 ++++++++---- sql/sql_backup.cc | 85 +++++++++++++------- storage/innobase/handler/backup_innodb.cc | 62 +++++++------- storage/innobase/handler/backup_innodb.h | 34 ++++---- storage/innobase/handler/ha_innodb.cc | 1 - storage/maria/ma_backup.cc | 36 +++++---- storage/maria/ma_backup.h | 26 +++--- 8 files changed, 181 insertions(+), 120 deletions(-) diff --git a/mysql-test/main/backup_server_restore.result b/mysql-test/main/backup_server_restore.result index 83bf230bc8619..689c4955efb70 100644 --- a/mysql-test/main/backup_server_restore.result +++ b/mysql-test/main/backup_server_restore.result @@ -29,10 +29,6 @@ i 2 3 5 -Warnings: -Error 145 Got error '145 "Table was marked as crashed and should be repaired"' for './test/tariant' -Warning 1034 1 client is using or hasn't closed the table properly -Note 1034 Table is fixed Restart database in original data directory # restart Clean up diff --git a/sql/handler.h b/sql/handler.h index 10e51eb3bca86..f10410f685009 100644 --- a/sql/handler.h +++ b/sql/handler.h @@ -1514,6 +1514,29 @@ struct backup_target static constexpr IF_WIN(HANDLE,int) stream{NO_STREAM}; }; +/** BACKUP SERVER execution phase */ +enum backup_phase +{ + /** finish backup, possibly after BACKUP_PHASE_ABORT */ + BACKUP_PHASE_FINISH= -2, + /** abort any operation */ + BACKUP_PHASE_ABORT= -1, + /** initial phase; @see MDL_BACKUP_START */ + BACKUP_PHASE_START= 0, + /** copy while new writes to non-transactional tables are blocked; + @see MDL_BACKUP_FLUSH */ + BACKUP_PHASE_NO_BEGIN_NON_TRANS, + /** copy while any writes to non-transactional tables are blocked; + @see MDL_BACKUP_WAIT_FLUSH */ + BACKUP_PHASE_NO_DML_NON_TRANS, + /** copy files while DDL is blocked; @see MDL_BACKUP_WAIT_DDL */ + BACKUP_PHASE_NO_DDL, + /** determine the logical time of the backup and copy any + remaining files while MDL_BACKUP_WAIT_COMMIT is active; + this is followed by BACKUP_PHASE_FINISH */ + BACKUP_PHASE_NO_COMMIT +}; + /* handlerton is a singleton structure - one instance per storage engine - to provide access to storage engine functionality that works on the @@ -1917,36 +1940,36 @@ struct handlerton : public transaction_participant void (*end_backup)(void); /** - Start of BACKUP SERVER: collect all files to be backed up + Start of a BACKUP SERVER phase, + when no backup_step() or backup_end() is pending. @param thd current session @param target backup target + @param phase BACKUP_PHASE_START, ... @return error code @retval 0 on success */ - int (*backup_start)(THD *thd, backup_target target); + int (*backup_start)(THD *thd, const backup_target &target, + backup_phase phase); /** Process a file that was collected in backup_start(). - @param thd current session + @param thd current session + @param target backup target + @param phase last phase on which backup_start() was successfully invoked @return number of files remaining, or negative on error @retval 0 on completion */ - int (*backup_step)(THD *thd); - /** - Finish copying and determine the logical time of the backup snapshot. - @param thd current sesssion - @param abort whether BACKUP SERVER was aborted - @return error code - @retval 0 on success - */ - int (*backup_end)(THD *thd, bool abort); + int (*backup_step)(THD *thd, const backup_target &target, + backup_phase phase); /** - Clean up after any backup_end(). - @param thd the parameter on which backup_end() was invoked + Finish a phase, once all calls for the current phase are completed. + @param thd current sesssion @param target backup target + @param phase current backup phase, or + one of the special values BACKUP_PHASE_ABORT or BACKUP_PHASE_FINISH @return error code @retval 0 on success */ - int (*backup_finalize)(THD *thd, backup_target target); + int (*backup_end)(THD *thd, const backup_target &target, backup_phase phase); /********************************************************************** WSREP specific diff --git a/sql/sql_backup.cc b/sql/sql_backup.cc index 4e1c1db5cb529..1b9350d5acf94 100644 --- a/sql/sql_backup.cc +++ b/sql/sql_backup.cc @@ -260,41 +260,44 @@ extern "C" int backup_config_append(const backup_target *target, return -1; } -static my_bool backup_start(THD *thd, plugin_ref plugin, void *dst) noexcept +struct backup_target_phase +{ + const backup_target ⌖ + backup_phase phase; +}; + +static my_bool backup_start(THD *thd, plugin_ref plugin, void *arg) noexcept { handlerton *hton= plugin_hton(plugin); + const backup_target_phase &t{*static_cast(arg)}; + assert(int{t.phase} >= 0); if (hton->backup_start) - return hton->backup_start(thd, *static_cast(dst)); + return hton->backup_start(thd, t.target, t.phase); return false; } static my_bool backup_end(THD *thd, plugin_ref plugin, void *arg) noexcept { handlerton *hton= plugin_hton(plugin); + const backup_target_phase &t{*static_cast(arg)}; if (hton->backup_end) - return hton->backup_end(thd, arg != nullptr); + return hton->backup_end(thd, t.target, t.phase); return false; } -static my_bool backup_step(THD *thd, plugin_ref plugin, void *) noexcept +static my_bool backup_step(THD *thd, plugin_ref plugin, void *arg) noexcept { handlerton *hton= plugin_hton(plugin); + const backup_target_phase &t{*static_cast(arg)}; + assert(int{t.phase} >= 0); int res= 0; if (hton->backup_step) - while ((res= hton->backup_step(thd))) + while ((res= hton->backup_step(thd, t.target, t.phase))) if (res < 0) break; return res != 0; } -static my_bool backup_finalize(THD *thd, plugin_ref plugin, void *dst) noexcept -{ - handlerton *hton= plugin_hton(plugin); - if (hton->backup_finalize) - return hton->backup_finalize(thd, *static_cast(dst)); - return 0; -} - bool Sql_cmd_backup::execute(THD *thd) { if (check_global_access(thd, RELOAD_ACL) || @@ -336,32 +339,54 @@ bool Sql_cmd_backup::execute(THD *thd) goto err_exit; } #endif + bool fail{false}; - bool fail= plugin_foreach_with_mask(thd, backup_start, - MYSQL_STORAGE_ENGINE_PLUGIN, - PLUGIN_IS_DELETED|PLUGIN_IS_READY, &dir); + static constexpr struct { + backup_phase name; enum_mdl_type mdl; + } phases[] { + { BACKUP_PHASE_START, MDL_BACKUP_START }, + { BACKUP_PHASE_NO_BEGIN_NON_TRANS, MDL_BACKUP_FLUSH }, + { BACKUP_PHASE_NO_DML_NON_TRANS, MDL_BACKUP_WAIT_FLUSH }, + { BACKUP_PHASE_NO_DDL, MDL_BACKUP_WAIT_DDL }, + { BACKUP_PHASE_NO_COMMIT, MDL_BACKUP_WAIT_COMMIT } + }; - /* The backup_step may be invoked in multiple concurrent threads. - At the time backup_end is invoked, all backup_step will have to complete. */ - if (!fail) + for (const auto &phase : phases) + { + backup_target_phase t{dir, phase.name}; + assert(!fail); + fail= phase.mdl != MDL_BACKUP_START && + thd->mdl_context.upgrade_shared_lock(mdl_request.ticket, + phase.mdl, + thd->variables.lock_wait_timeout); + if (fail) + break; + fail= plugin_foreach_with_mask(thd, backup_start, + MYSQL_STORAGE_ENGINE_PLUGIN, + PLUGIN_IS_DELETED|PLUGIN_IS_READY, &t); + if (fail) + break; + /* TO DO: invoke backup_step from multiple concurrent threads. */ fail= plugin_foreach_with_mask(thd, backup_step, MYSQL_STORAGE_ENGINE_PLUGIN, - PLUGIN_IS_DELETED|PLUGIN_IS_READY, nullptr); - - fail= - thd->mdl_context.upgrade_shared_lock(mdl_request.ticket, - MDL_BACKUP_WAIT_COMMIT, - thd->variables.lock_wait_timeout) || - plugin_foreach_with_mask(thd, backup_end, MYSQL_STORAGE_ENGINE_PLUGIN, - PLUGIN_IS_DELETED|PLUGIN_IS_READY, - reinterpret_cast(fail)) || fail; + PLUGIN_IS_DELETED|PLUGIN_IS_READY, &t); + /* After all backup_step are completed, finish the phase. */ + if (fail) + break; + fail= + plugin_foreach_with_mask(thd, backup_end, MYSQL_STORAGE_ENGINE_PLUGIN, + PLUGIN_IS_DELETED|PLUGIN_IS_READY, &t); + if (fail) + break; + } /* The final part must not interfere with the use of the server datadir. Release the locks. */ thd->mdl_context.release_lock(mdl_request.ticket); - fail= plugin_foreach_with_mask(thd, backup_finalize, + backup_target_phase finish{dir, BACKUP_PHASE_FINISH}; + fail= plugin_foreach_with_mask(thd, backup_end, MYSQL_STORAGE_ENGINE_PLUGIN, - PLUGIN_IS_DELETED|PLUGIN_IS_READY, &dir) || + PLUGIN_IS_DELETED|PLUGIN_IS_READY, &finish) || fail; #ifndef _WIN32 close(dir.fd); diff --git a/storage/innobase/handler/backup_innodb.cc b/storage/innobase/handler/backup_innodb.cc index 57ce9d71dbe9e..d13bdd881de13 100644 --- a/storage/innobase/handler/backup_innodb.cc +++ b/storage/innobase/handler/backup_innodb.cc @@ -47,9 +47,6 @@ class InnoDB_backup hard-linked, copied, or moved */ std::vector logs; - /** backup target */ - backup_target target; - /** @return the backup context */ backup_context &context() const noexcept { ut_ad(log_sys.latch_have_any()); ut_ad(trx); return trx->lock.backup; } @@ -104,7 +101,6 @@ class InnoDB_backup ctx.last_lsn= 0; ctx.archived= !old_size; - this->target= target; /* Collect all tablespaces that have been created before our start checkpoint. Newer tablespaces will be recovered by the innodb_log_archive=ON recovery. @@ -142,11 +138,12 @@ class InnoDB_backup /** Process a file that was collected at init(). This may be invoked from multiple concurrent threads. - @param thd current session + @param thd current session + @param target backup target @return number of files remaining, or negative on error @retval 0 on completion */ - int step(THD *thd) noexcept + int step(THD *thd, const backup_target &target) noexcept { uint32_t id= FIL_NULL; lsn_t lsn= 0; @@ -182,7 +179,7 @@ class InnoDB_backup uint32_t start{0}; for (fil_node_t *node= UT_LIST_GET_FIRST(space->chain); node; start+= node->size, node= UT_LIST_GET_NEXT(chain, node)) - if ((res= backup(node, start))) + if ((res= backup(target, node, start))) break; space->release(); if (res) @@ -194,18 +191,29 @@ class InnoDB_backup } /** - Finish copying and determine the logical time of the backup snapshot. - fini() must be invoked on the same thd. - @param thd current session - @param abort whether BACKUP SERVER was aborted + Finish copying or finalize the backup. + @param thd current session + @param target backup target + @param phase backup phase @return error code @retval 0 on success */ - int end(THD *thd, bool abort) noexcept + int end(THD *thd, const backup_target &target, backup_phase phase) noexcept { + switch (phase) { + default: + return 0; + case BACKUP_PHASE_FINISH: + return fini(thd, target); + case BACKUP_PHASE_NO_COMMIT: + /* Determine the logical time of the backup snapshot */ + case BACKUP_PHASE_ABORT: + break; + } + int fail= 0; log_sys.latch.wr_lock(); - if (abort) + if (phase == BACKUP_PHASE_ABORT) { skip_log_dup: queue.clear(); @@ -487,10 +495,12 @@ class InnoDB_backup /** Back up a persistent InnoDB data file. - @param node InnoDB data file - @param start first page number + @param target backup target + @param node InnoDB data file + @param start first page number */ - int backup(fil_node_t *node, uint32_t start) noexcept + int backup(const backup_target &target, fil_node_t *node, uint32_t start) + noexcept { for (bool tried_mkdir{false};;) { @@ -909,24 +919,22 @@ void log_t::backup_stop(uint64_t old_size, THD *thd) noexcept resize_finish(thd); } -int innodb_backup_start(THD *thd, backup_target target) noexcept -{ - return innodb_backup.init(thd, target); -} - -int innodb_backup_step(THD *thd) noexcept +int innodb_backup_start(THD *thd, const backup_target &target, + backup_phase phase) noexcept { - return innodb_backup.step(thd); + return phase == BACKUP_PHASE_START && innodb_backup.init(thd, target); } -int innodb_backup_end(THD *thd, bool abort) noexcept +int innodb_backup_step(THD *thd, const backup_target &target, + backup_phase phase) noexcept { - return innodb_backup.end(thd, abort); + return phase == BACKUP_PHASE_START && innodb_backup.step(thd, target); } -int innodb_backup_finalize(THD *thd, backup_target target) noexcept +int innodb_backup_end(THD *thd, const backup_target &target, + backup_phase phase) noexcept { - return innodb_backup.fini(thd, target); + return innodb_backup.end(thd, target, phase); } void innodb_backup_checkpoint() noexcept diff --git a/storage/innobase/handler/backup_innodb.h b/storage/innobase/handler/backup_innodb.h index 8d5f81ca35898..163d511b548c5 100644 --- a/storage/innobase/handler/backup_innodb.h +++ b/storage/innobase/handler/backup_innodb.h @@ -14,39 +14,39 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1335 USA */ /** - Start of BACKUP SERVER: collect all files to be backed up + Start of a BACKUP SERVER phase, + when no innodb_backup_step() or innodb_backup_end() is pending. @param thd current session @param target backup target + @param phase BACKUP_PHASE_START, ... @return error code @retval 0 on success */ -int innodb_backup_start(THD *thd, backup_target target) noexcept; +int innodb_backup_start(THD *thd, const backup_target &target, + backup_phase phase) noexcept; /** - Process a file that was collected in backup_start(). - @param thd current session + Process a file that was collected in innodb_backup_start(). + @param thd current session + @param target backup target + @param phase last phase on which backup_start() was successfully invoked @return number of files remaining, or negative on error @retval 0 on completion */ -int innodb_backup_step(THD *thd) noexcept; - -/** - Finish copying and determine the logical time of the backup snapshot. - @param thd current session - @param abort whether BACKUP SERVER was aborted - @return error code - @retval 0 on success -*/ -int innodb_backup_end(THD *thd, bool abort) noexcept; +int innodb_backup_step(THD *thd, const backup_target &target, + backup_phase phase) noexcept; /** - Clean up after innodb_backup_end(). - @param thd the parameter on which innodb_backup_end() had been invoked + Finish a phase, once all calls for the current phase are completed. + @param thd current sesssion @param target backup target + @param phase current backup phase, or + one of the special values BACKUP_PHASE_ABORT or BACKUP_PHASE_FINISH @return error code @retval 0 on success */ -int innodb_backup_finalize(THD *thd, backup_target target) noexcept; +int innodb_backup_end(THD *thd, const backup_target &target, + backup_phase phase) noexcept; /** Complete the first checkpoint in a new archive log file. diff --git a/storage/innobase/handler/ha_innodb.cc b/storage/innobase/handler/ha_innodb.cc index 5c28d270d13ff..578c92de9eb3e 100644 --- a/storage/innobase/handler/ha_innodb.cc +++ b/storage/innobase/handler/ha_innodb.cc @@ -4146,7 +4146,6 @@ static int innodb_init(void* p) innobase_hton->backup_start = innodb_backup_start; innobase_hton->backup_step = innodb_backup_step; innobase_hton->backup_end = innodb_backup_end; - innobase_hton->backup_finalize = innodb_backup_finalize; innobase_hton->binlog_init= innodb_binlog_init; innobase_hton->set_binlog_max_size= ibb_set_max_size; innobase_hton->binlog_write_direct_ordered= diff --git a/storage/maria/ma_backup.cc b/storage/maria/ma_backup.cc index 9d3e0c1a3d82e..41c7a788be8f4 100644 --- a/storage/maria/ma_backup.cc +++ b/storage/maria/ma_backup.cc @@ -15,18 +15,17 @@ #include "maria_def.h" #include "ma_backup.h" -#include +#include "sql_backup_interface.h" +#include "mysqld_error.h" +#if 1 // tc_purge(), tdc_purge() +# include "sql_class.h" +# include "table_cache.h" +#endif #include #include #include #include -#ifdef __APPLE__ -#include -#include -#include -#endif - /* Implementation of functions declatred in ma_backup.h: BACKUP SERVER support for Aria engine @@ -107,14 +106,15 @@ namespace #endif // _WIN32 } - int end(THD *thd, bool abort) noexcept + int end(THD *thd, backup_phase phase) noexcept { int ret_val = 0; - if (!abort) { - if (int err= perform_backup() != 0) - { - ret_val= err; - }; + if (phase == BACKUP_PHASE_NO_COMMIT) { +#if 1 // FIXME: invoke these only for Aria, MyISAM, CSV but not InnoDB, RocksDB + tc_purge(); + tdc_purge(true); +#endif + ret_val= perform_backup(); } translog_enable_purge(); return ret_val; @@ -343,20 +343,22 @@ namespace std::unique_ptr aria_backup; } -int aria_backup_start(THD *thd, backup_target target) noexcept +int aria_backup_start(THD *thd, const backup_target &target, backup_phase) + noexcept { aria_backup= std::make_unique(thd, target); return !aria_backup->is_initialized(); } -int aria_backup_step(THD *thd) noexcept +int aria_backup_step(THD *, const backup_target &, backup_phase) noexcept { return 0; } -int aria_backup_end(THD *thd, bool abort) noexcept +int aria_backup_end(THD *thd, const backup_target &, + backup_phase phase) noexcept { - int ret_val= aria_backup->end(thd, abort); + int ret_val= aria_backup->end(thd, phase); aria_backup.reset(); return ret_val; } diff --git a/storage/maria/ma_backup.h b/storage/maria/ma_backup.h index 3bec606648dac..ce2aeee212992 100644 --- a/storage/maria/ma_backup.h +++ b/storage/maria/ma_backup.h @@ -21,27 +21,35 @@ #include /** - Start of BACKUP SERVER: collect all files to be backed up + Start of a BACKUP SERVER phase, + when no aria_backup_step() or aria_backup_end() is pending. @param thd current session - @param target target directory + @param target backup target + @param phase BACKUP_PHASE_START, ... @return error code @retval 0 on success */ -int aria_backup_start(THD *thd, backup_target target) noexcept; +int aria_backup_start(THD *thd, const backup_target &target, + backup_phase phase) noexcept; /** - Process a file that was collected in backup_start(). + Process a file that was collected in aria_backup_start(). @param thd current session - @return number of files remaining, or negative on error + @param target backup target + @param phase last phase on which backup_start() was successfully invoked @retval 0 on completion */ -int aria_backup_step(THD *thd) noexcept; +int aria_backup_step(THD *thd, const backup_target &target, + backup_phase phase) noexcept; /** - Finish copying and determine the logical time of the backup snapshot. + Finish a phase, once all calls for the current phase are completed. @param thd current session - @param abort whether BACKUP SERVER was aborted + @param target backup target + @param phase current backup phase, or + one of the special values BACKUP_PHASE_ABORT or BACKUP_PHASE_FINISH @return error code @retval 0 on success */ -int aria_backup_end(THD *thd, bool abort) noexcept; +int aria_backup_end(THD *thd, const backup_target &target, + backup_phase phase) noexcept; From bd50329d8dace8337b25d8cadb65d64a61e17f5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20M=C3=A4kel=C3=A4?= Date: Tue, 9 Jun 2026 14:37:16 +0300 Subject: [PATCH 11/20] Revert "fixup! b868d24c7fb9d1409dd4cb25fc737b219ee84154" This reverts commit 75ff32fb3b75ac75cf50eed136df712bf5e66d71. --- sql/sql_backup.cc | 8 ++++---- sql/sql_backup_interface.h | 4 ++-- storage/innobase/handler/backup_innodb.cc | 7 +++---- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/sql/sql_backup.cc b/sql/sql_backup.cc index 1b9350d5acf94..828b384ef48f8 100644 --- a/sql/sql_backup.cc +++ b/sql/sql_backup.cc @@ -175,8 +175,8 @@ extern "C" int copy_entire_file(int src, int dst) @param end last offset to copy (exclusive) @return error code (non-positive) @retval 0 on success */ -extern "C" int copy_file(IF_WIN(const native_file_handle*,int) src, - IF_WIN(const native_file_handle*,int) dst, +extern "C" int copy_file(IF_WIN(const native_file_handle&,int) src, + IF_WIN(const native_file_handle&,int) dst, uint64_t start, uint64_t end) { assert(end >= start); @@ -192,10 +192,10 @@ extern "C" int copy_file(IF_WIN(const native_file_handle*,int) src, ret= (start != 0 && off_t(start) != lseek(dst, start, SEEK_SET)) ? -1 : copy(src, dst, off_t(start), off_t(end)); -# elif defined _WIN32 - ret= pread_pwrite(*src, *dst, start, end); # else +# ifndef _WIN32 if ((ret= mmap_copy(src, dst, start, end)) == 1) +# endif ret= pread_pwrite(src, dst, start, end); # endif assert(ret <= 0); diff --git a/sql/sql_backup_interface.h b/sql/sql_backup_interface.h index 5abcf3d52caa3..39bebc635ffbc 100644 --- a/sql/sql_backup_interface.h +++ b/sql/sql_backup_interface.h @@ -54,8 +54,8 @@ extern "C" @param end last offset to copy (exclusive) @return error code (non-positive) @retval 0 on success */ -int copy_file(IF_WIN(const native_file_handle*,int) src, - IF_WIN(const native_file_handle*,int) dst, +int copy_file(IF_WIN(const native_file_handle&,int) src, + IF_WIN(const native_file_handle&,int) dst, uint64_t start, uint64_t end); #ifdef __cplusplus diff --git a/storage/innobase/handler/backup_innodb.cc b/storage/innobase/handler/backup_innodb.cc index d13bdd881de13..ea99a255061f7 100644 --- a/storage/innobase/handler/backup_innodb.cc +++ b/storage/innobase/handler/backup_innodb.cc @@ -393,7 +393,7 @@ class InnoDB_backup !SetEndOfFile(d); } if (!fail) - fail= copy_file(&s, &d, log_sys.START_OFFSET, payload_end) || + fail= copy_file(s, d, log_sys.START_OFFSET, payload_end) || (ctx.max_first_lsn == ctx.first_lsn && write_checkpoint(d, ctx.checkpoint_end_lsn - ctx.first_lsn + log_sys.START_OFFSET)); @@ -581,8 +581,7 @@ class InnoDB_backup const uint32_t end{start + fil_space_t::BACKUP_BATCH_SIZE}; backup_start(node->space, end); /* TODO: avoid copying freed page ranges */ - err= copy_file(IF_WIN(&,)node->handle, IF_WIN(&,)f, - start * uint64_t{page_size}, + err= copy_file(node->handle, f, start * uint64_t{page_size}, std::min(end, file_size) * uint64_t{page_size}); backup_stop(node->space); if (err || (start= end) >= file_size) @@ -775,7 +774,7 @@ class InnoDB_backup if (!err) { - err= copy_file(&s, &d, payload_start, payload_end); + err= copy_file(s, d, payload_start, payload_end); if (!err && lsn < ctx.checkpoint) err= write_checkpoint(d, ctx.checkpoint_end_lsn - lsn + log_sys.START_OFFSET); From 7cd70f4c1e4f77b76b392112570b6787a6517849 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20M=C3=A4kel=C3=A4?= Date: Tue, 9 Jun 2026 14:41:32 +0300 Subject: [PATCH 12/20] fixup! f257584af9db2d2d435bdb2696515c54311663bc --- storage/maria/ma_backup.cc | 1 - 1 file changed, 1 deletion(-) diff --git a/storage/maria/ma_backup.cc b/storage/maria/ma_backup.cc index 41c7a788be8f4..363b7d9ca437c 100644 --- a/storage/maria/ma_backup.cc +++ b/storage/maria/ma_backup.cc @@ -15,7 +15,6 @@ #include "maria_def.h" #include "ma_backup.h" -#include "sql_backup_interface.h" #include "mysqld_error.h" #if 1 // tc_purge(), tdc_purge() # include "sql_class.h" From 7f48935f42b8c0cd9baf6373b4d96db7444a73e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20M=C3=A4kel=C3=A4?= Date: Wed, 10 Jun 2026 08:08:51 +0300 Subject: [PATCH 13/20] fixup! f257584af9db2d2d435bdb2696515c54311663bc --- storage/maria/ma_backup.cc | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/storage/maria/ma_backup.cc b/storage/maria/ma_backup.cc index 363b7d9ca437c..9c65b8f6a09e4 100644 --- a/storage/maria/ma_backup.cc +++ b/storage/maria/ma_backup.cc @@ -105,16 +105,9 @@ namespace #endif // _WIN32 } - int end(THD *thd, backup_phase phase) noexcept + int end(THD *thd) noexcept { - int ret_val = 0; - if (phase == BACKUP_PHASE_NO_COMMIT) { -#if 1 // FIXME: invoke these only for Aria, MyISAM, CSV but not InnoDB, RocksDB - tc_purge(); - tdc_purge(true); -#endif - ret_val= perform_backup(); - } + int ret_val= perform_backup(); translog_enable_purge(); return ret_val; } @@ -342,9 +335,11 @@ namespace std::unique_ptr aria_backup; } -int aria_backup_start(THD *thd, const backup_target &target, backup_phase) - noexcept +int aria_backup_start(THD *thd, const backup_target &target, + backup_phase phase) noexcept { + if (phase != BACKUP_PHASE_NO_COMMIT) + return 0; aria_backup= std::make_unique(thd, target); return !aria_backup->is_initialized(); } @@ -357,7 +352,13 @@ int aria_backup_step(THD *, const backup_target &, backup_phase) noexcept int aria_backup_end(THD *thd, const backup_target &, backup_phase phase) noexcept { - int ret_val= aria_backup->end(thd, phase); + if (phase != BACKUP_PHASE_NO_COMMIT) + return 0; +#if 1 // FIXME: invoke these only for Aria, MyISAM, CSV but not InnoDB, RocksDB + tc_purge(); + tdc_purge(true); +#endif + int ret_val= aria_backup->end(thd); aria_backup.reset(); return ret_val; } From 6c1d7e0836c2bcbe5040e700aeed1215e52f91f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20M=C3=A4kel=C3=A4?= Date: Wed, 10 Jun 2026 13:36:48 +0300 Subject: [PATCH 14/20] squash! 10539aa463aceed07c0c08b52d944a0d5e054525 --- mysql-test/main/backup_server_restore.result | 37 ------------------- mysql-test/main/backup_server_restore.test | 38 -------------------- mysql-test/suite/backup/backup_innodb.result | 24 +++++++++++++ mysql-test/suite/backup/backup_innodb.test | 18 +++++++++- 4 files changed, 41 insertions(+), 76 deletions(-) delete mode 100644 mysql-test/main/backup_server_restore.result delete mode 100644 mysql-test/main/backup_server_restore.test diff --git a/mysql-test/main/backup_server_restore.result b/mysql-test/main/backup_server_restore.result deleted file mode 100644 index 689c4955efb70..0000000000000 --- a/mysql-test/main/backup_server_restore.result +++ /dev/null @@ -1,37 +0,0 @@ -Prepare database -CREATE TABLE tinno (i INTEGER) ENGINE=InnoDB; -INSERT INTO tinno VALUES (1), (2), (3), (4); -CREATE TABLE tariatr (i INTEGER) ENGINE=Aria TRANSACTIONAL=1; -INSERT INTO tariatr VALUES (2), (3), (5), (7); -CREATE TABLE tariant (i INTEGER) ENGINE=Aria TRANSACTIONAL=0; -INSERT INTO tariant VALUES (1), (1), (2), (3), (5); -Back up the database -BACKUP SERVER TO '$MYSQLTEST_VARDIR/some_directory'; -Restore the database -# restart: --datadir=MYSQLTEST_VARDIR/some_directory -Check contents after restore -SELECT * FROM tinno; -i -1 -2 -3 -4 -SELECT * FROM tariatr; -i -2 -3 -5 -7 -SELECT * FROM tariant; -i -1 -1 -2 -3 -5 -Restart database in original data directory -# restart -Clean up -DROP TABLE tinno; -DROP TABLE tariatr; -DROP TABLE tariant; diff --git a/mysql-test/main/backup_server_restore.test b/mysql-test/main/backup_server_restore.test deleted file mode 100644 index ef1f5af27a68b..0000000000000 --- a/mysql-test/main/backup_server_restore.test +++ /dev/null @@ -1,38 +0,0 @@ ---source include/not_windows.inc ---source include/have_innodb.inc - ---echo Prepare database -CREATE TABLE tinno (i INTEGER) ENGINE=InnoDB; -INSERT INTO tinno VALUES (1), (2), (3), (4); -CREATE TABLE tariatr (i INTEGER) ENGINE=Aria TRANSACTIONAL=1; -INSERT INTO tariatr VALUES (2), (3), (5), (7); -CREATE TABLE tariant (i INTEGER) ENGINE=Aria TRANSACTIONAL=0; -INSERT INTO tariant VALUES (1), (1), (2), (3), (5); - ---echo Back up the database -evalp BACKUP SERVER TO '$MYSQLTEST_VARDIR/some_directory'; - ---echo Restore the database ---disable_query_log -call mtr.add_suppression("InnoDB: Did not find any checkpoint after LSN="); -call mtr.add_suppression("InnoDB: Renaming ib_[0-9]+.log to ib_logfile0"); -call mtr.add_suppression("mariadbd: Got error '145 \"Table was marked as crashed and should be repaired\"' for "); -call mtr.add_suppression("Checking table: "); ---enable_query_log ---let $restart_parameters=--datadir=$MYSQLTEST_VARDIR/some_directory ---source include/restart_mysqld.inc - ---echo Check contents after restore -SELECT * FROM tinno; -SELECT * FROM tariatr; -SELECT * FROM tariant; - ---echo Restart database in original data directory ---let $restart_parameters= ---source include/restart_mysqld.inc - ---echo Clean up -DROP TABLE tinno; -DROP TABLE tariatr; -DROP TABLE tariant; ---rmdir $MYSQLTEST_VARDIR/some_directory diff --git a/mysql-test/suite/backup/backup_innodb.result b/mysql-test/suite/backup/backup_innodb.result index 7ab700975ac78..e9124a8249eed 100644 --- a/mysql-test/suite/backup/backup_innodb.result +++ b/mysql-test/suite/backup/backup_innodb.result @@ -2,6 +2,10 @@ CREATE TABLE t(a INT PRIMARY KEY, b CHAR(255) DEFAULT '' NOT NULL, INDEX(b)) ENGINE=INNODB; BEGIN; INSERT INTO t SET a=1; +CREATE TABLE at1(i INTEGER) ENGINE=Aria TRANSACTIONAL=1; +INSERT INTO at1 VALUES (2), (3), (5), (7); +CREATE TABLE at0 (i INTEGER) ENGINE=Aria TRANSACTIONAL=0; +INSERT INTO at0 VALUES (1), (1), (2), (3), (5); BACKUP SERVER TO '$target_directory'; ROLLBACK; SELECT * FROM t; @@ -18,12 +22,32 @@ SELECT * FROM t; a b 1 DELETE FROM t; +DROP TABLE at0, at1; # restart: --defaults-file=MYSQLTEST_VARDIR/some_directory/backup.cnf --datadir=MYSQLTEST_VARDIR/some_directory SELECT * FROM t; a b 1 DELETE FROM t; ERROR HY000: Table 't' is read only +SELECT * FROM at0; +i +1 +1 +2 +3 +5 +SELECT * FROM at1; +i +2 +3 +5 +7 +DROP TABLE t, at0, at1; +ERROR HY000: Table 't' is read only +SELECT * FROM at0; +ERROR 42S02: Table 'test.at0' doesn't exist +SELECT * FROM at1; +ERROR 42S02: Table 'test.at1' doesn't exist # restart SELECT * FROM t; a b diff --git a/mysql-test/suite/backup/backup_innodb.test b/mysql-test/suite/backup/backup_innodb.test index 769c256f8c3d0..895377f2e09a7 100644 --- a/mysql-test/suite/backup/backup_innodb.test +++ b/mysql-test/suite/backup/backup_innodb.test @@ -7,6 +7,11 @@ ENGINE=INNODB; BEGIN; INSERT INTO t SET a=1; +CREATE TABLE at1(i INTEGER) ENGINE=Aria TRANSACTIONAL=1; +INSERT INTO at1 VALUES (2), (3), (5), (7); +CREATE TABLE at0 (i INTEGER) ENGINE=Aria TRANSACTIONAL=0; +INSERT INTO at0 VALUES (1), (1), (2), (3), (5); + --let $target_directory=/tmp/some_directory$MTR_COMBINATION_ARCHIVED # comment out the following line (and some more, including rmdir), # to test cross-filesystem copy @@ -48,6 +53,7 @@ eval BACKUP SERVER TO '$target_directory'; ROLLBACK; SELECT * FROM t; DELETE FROM t; +DROP TABLE at0, at1; if ($MARIADB_UPGRADE_EXE) { let target_directory=$target_directory; @@ -66,9 +72,19 @@ if (!$MARIADB_UPGRADE_EXE) { --source include/restart_mysqld.inc SELECT * FROM t; -# we must have started up with nonzero innodb_log_recovery_target +# A nonzero innodb_log_recovery_target makes InnoDB read-only. --error ER_OPEN_AS_READONLY DELETE FROM t; +# Non-InnoDB tables are read-write. +SELECT * FROM at0; +SELECT * FROM at1; +--error ER_OPEN_AS_READONLY +DROP TABLE t, at0, at1; +--error ER_NO_SUCH_TABLE +SELECT * FROM at0; +--error ER_NO_SUCH_TABLE +SELECT * FROM at1; + --let $restart_parameters= --source include/restart_mysqld.inc SELECT * FROM t; From 8077134777e3d3fda875413b45f440873c46e358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20M=C3=A4kel=C3=A4?= Date: Thu, 11 Jun 2026 13:43:46 +0300 Subject: [PATCH 15/20] Multi-threaded backup and stub of streaming backup --- .../suite/backup/backup_innodb,debug.rdiff | 2 +- mysql-test/suite/backup/backup_innodb.result | 12 +- mysql-test/suite/backup/backup_innodb.test | 17 +- sql/handler.h | 2 +- sql/sql_backup.cc | 189 +++++++++++++----- sql/sql_backup.h | 14 +- sql/sql_backup_interface.h | 42 ++++ sql/sql_yacc.yy | 26 ++- 8 files changed, 246 insertions(+), 58 deletions(-) diff --git a/mysql-test/suite/backup/backup_innodb,debug.rdiff b/mysql-test/suite/backup/backup_innodb,debug.rdiff index ed165bdf9e59e..c6cc65ee312c3 100644 --- a/mysql-test/suite/backup/backup_innodb,debug.rdiff +++ b/mysql-test/suite/backup/backup_innodb,debug.rdiff @@ -5,7 +5,7 @@ DELETE FROM t; connect backup,localhost,root; +SET DEBUG_SYNC='innodb_backup_start SIGNAL start WAIT_FOR resume'; - BACKUP SERVER TO 'target_directory'; + BACKUP SERVER TO 'target_directory' 4 CONCURRENT; +connection default; +SET DEBUG_SYNC='now WAIT_FOR start'; +INSERT INTO t(a) SELECT * FROM seq_1_to_30000; diff --git a/mysql-test/suite/backup/backup_innodb.result b/mysql-test/suite/backup/backup_innodb.result index e9124a8249eed..7da14595f7120 100644 --- a/mysql-test/suite/backup/backup_innodb.result +++ b/mysql-test/suite/backup/backup_innodb.result @@ -7,6 +7,16 @@ INSERT INTO at1 VALUES (2), (3), (5), (7); CREATE TABLE at0 (i INTEGER) ENGINE=Aria TRANSACTIONAL=0; INSERT INTO at0 VALUES (1), (1), (2), (3), (5); BACKUP SERVER TO '$target_directory'; +BACKUP SERVER TO '$target_directory' WITH '/bin/false'; +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'WITH '/bin/false'' at line 1 +BACKUP SERVER TO '$target_directory' WITH 4 CONCURRENT '/bin/false'; +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'WITH 4 CONCURRENT '/bin/false'' at line 1 +BACKUP SERVER USING '$target_directory'; +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '' at line 1 +BACKUP SERVER USING '$target_directory' WITH '/bin/false'; +ERROR 42000: This version of MariaDB doesn't yet support 'BACKUP SERVER USING' +BACKUP SERVER USING '$target_directory' WITH 4 CONCURRENT '/bin/false'; +ERROR 42000: This version of MariaDB doesn't yet support 'BACKUP SERVER USING' ROLLBACK; SELECT * FROM t; a b @@ -14,7 +24,7 @@ a b BEGIN; DELETE FROM t; connect backup,localhost,root; -BACKUP SERVER TO 'target_directory'; +BACKUP SERVER TO 'target_directory' 4 CONCURRENT; disconnect backup; connection default; ROLLBACK; diff --git a/mysql-test/suite/backup/backup_innodb.test b/mysql-test/suite/backup/backup_innodb.test index 895377f2e09a7..d0298883fe08c 100644 --- a/mysql-test/suite/backup/backup_innodb.test +++ b/mysql-test/suite/backup/backup_innodb.test @@ -22,6 +22,19 @@ INSERT INTO at0 VALUES (1), (1), (2), (3), (5); --rmdir $target_directory evalp BACKUP SERVER TO '$target_directory'; + +--error ER_PARSE_ERROR +evalp BACKUP SERVER TO '$target_directory' WITH '/bin/false'; +--error ER_PARSE_ERROR +evalp BACKUP SERVER TO '$target_directory' WITH 4 CONCURRENT '/bin/false'; +--error ER_PARSE_ERROR +evalp BACKUP SERVER USING '$target_directory'; +--error ER_NOT_SUPPORTED_YET +evalp BACKUP SERVER USING '$target_directory' WITH '/bin/false'; +--error ER_NOT_SUPPORTED_YET +evalp BACKUP SERVER USING '$target_directory' WITH 4 CONCURRENT '/bin/false'; + + --rmdir $target_directory ROLLBACK; # BACKUP SERVER will implicitly commit the current transaction @@ -34,7 +47,7 @@ DELETE FROM t; if ($have_debug) { SET DEBUG_SYNC='innodb_backup_start SIGNAL start WAIT_FOR resume'; --replace_result $target_directory target_directory -send_eval BACKUP SERVER TO '$target_directory'; +send_eval BACKUP SERVER TO '$target_directory' 4 CONCURRENT; --connection default SET DEBUG_SYNC='now WAIT_FOR start'; INSERT INTO t(a) SELECT * FROM seq_1_to_30000; @@ -45,7 +58,7 @@ SET DEBUG_SYNC='now SIGNAL resume'; } if (!$have_debug) { --replace_result $target_directory target_directory -eval BACKUP SERVER TO '$target_directory'; +eval BACKUP SERVER TO '$target_directory' 4 CONCURRENT; } --disconnect backup diff --git a/sql/handler.h b/sql/handler.h index f10410f685009..59948016e1956 100644 --- a/sql/handler.h +++ b/sql/handler.h @@ -1511,7 +1511,7 @@ struct backup_target static constexpr int NO_STREAM{-1}; #endif /** Target pipe, or NO_STREAM if copying to the target directory */ - static constexpr IF_WIN(HANDLE,int) stream{NO_STREAM}; + IF_WIN(HANDLE,int) stream; }; /** BACKUP SERVER execution phase */ diff --git a/sql/sql_backup.cc b/sql/sql_backup.cc index 828b384ef48f8..9fc44d52dd6c9 100644 --- a/sql/sql_backup.cc +++ b/sql/sql_backup.cc @@ -20,10 +20,8 @@ #include "sql_backup.h" #include "sql_backup_interface.h" #include "sql_parse.h" -#ifdef _WIN32 -# include "aligned.h" -# include "tpool.h" -#endif +#include "my_atomic_wrapper.h" +#include "tpool.h" #if defined __linux__ || defined __FreeBSD__ using copying_step= ssize_t(int,int,size_t,off_t*); @@ -60,8 +58,11 @@ send_step(int in_fd, int out_fd, size_t count, off_t *offset) noexcept return sendfile(out_fd, in_fd, offset, count); } #else -# ifndef _WIN32 -# include "aligned.h" +# include "aligned.h" +# ifdef _WIN32 +using tpool::pread; +using tpool::pwrite; +# else # include /** Copy a file using a memory mapping. @param in_fd source file @@ -118,10 +119,6 @@ static ssize_t pread_pwrite(IF_WIN(const native_file_handle&,int) in_fd, uint64_t o, uint64_t end) noexcept { -#ifdef _WIN32 - using tpool::pread; - using tpool::pwrite; -#endif constexpr size_t READ_WRITE_SIZE= 65536; char *b= static_cast(aligned_malloc(READ_WRITE_SIZE, 4096)); if (!b) @@ -260,10 +257,15 @@ extern "C" int backup_config_append(const backup_target *target, return -1; } +/** backup context */ struct backup_target_phase { - const backup_target ⌖ + /** target directory or stream */ + const backup_target target; + /** current phase of backup */ backup_phase phase; + /** handlerton::backup_step return value in multi-threaded operation */ + int ret; }; static my_bool backup_start(THD *thd, plugin_ref plugin, void *arg) noexcept @@ -292,12 +294,74 @@ static my_bool backup_step(THD *thd, plugin_ref plugin, void *arg) noexcept assert(int{t.phase} >= 0); int res= 0; if (hton->backup_step) + { while ((res= hton->backup_step(thd, t.target, t.phase))) if (res < 0) break; + } return res != 0; } +/** Number of background tasks executing backup_step_callback */ +static Atomic_counter backup_step_callback_pending{0}; + +/** Invoke backup_step() in a background task */ +static void backup_step_callback(void *arg) noexcept +{ + backup_target_phase &t{*static_cast(arg)}; + assert(!t.ret); + t.ret= plugin_foreach_with_mask(nullptr, backup_step, + MYSQL_STORAGE_ENGINE_PLUGIN, + PLUGIN_IS_DELETED|PLUGIN_IS_READY, &t); +#ifndef NDEBUG + auto was_pending= +#endif + backup_step_callback_pending--; + assert(was_pending); +} + +/** + Execute all handlerton::backup_step() until completion or failure. + @param thd current connection + @param target_phase backup target and phase + @param threads number of execution threads + @param tp thread pool +*/ +static bool backup_steps(THD *thd, backup_target_phase *target_phase, + int threads, tpool::thread_pool *tp) +{ + assert(!backup_step_callback_pending); + if (threads == 1) + return plugin_foreach_with_mask(thd, backup_step, + MYSQL_STORAGE_ENGINE_PLUGIN, + PLUGIN_IS_DELETED|PLUGIN_IS_READY, + target_phase); + tpool::task *const tasks= + static_cast(alloca(threads * sizeof *tasks)); + backup_step_callback_pending= threads - 1; + for (int n{threads}; --n; ) + tp->submit_task(new (&tasks[n]) tpool::task{backup_step_callback, + &target_phase[n]}); + bool fail= plugin_foreach_with_mask(thd, backup_step, + MYSQL_STORAGE_ENGINE_PLUGIN, + PLUGIN_IS_DELETED|PLUGIN_IS_READY, + target_phase); + while (backup_step_callback_pending) + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + if (fail) + return fail; + + for (int n{threads}; --n; ) + if (target_phase[n].ret) + { + my_error(ER_UNKNOWN_ERROR, MYF(0)); + return true; + } + + return false; +} + bool Sql_cmd_backup::execute(THD *thd) { if (check_global_access(thd, RELOAD_ACL) || @@ -305,6 +369,12 @@ bool Sql_cmd_backup::execute(THD *thd) error_if_data_home_dir(target.str, "BACKUP SERVER TO")) return true; + if (command) + { + my_error(ER_NOT_SUPPORTED_YET, MYF(0), "BACKUP SERVER USING"); + return true; + } + if (thd->current_backup_stage != BACKUP_FINISHED) { my_error(ER_BACKUP_LOCK_IS_ACTIVE, MYF(0)); @@ -329,67 +399,90 @@ bool Sql_cmd_backup::execute(THD *thd) return true; } -#ifdef _WIN32 - backup_target dir{target.str}; -#else - backup_target dir{open(target.str, O_DIRECTORY)}; - if (dir.fd < 0) + tpool::thread_pool *tp= nullptr; + backup_target_phase *target_phase= static_cast + (alloca(threads * sizeof *target_phase)); + if (threads > 1 && !(tp= tpool::create_thread_pool_generic())) + { + my_error(ER_OUT_OF_RESOURCES, MYF(0)); + return true; + } + +#ifndef _WIN32 + const int dir{open(target.str, O_DIRECTORY)}; + if (dir < 0) { my_error(EE_CANT_MKDIR, MYF(ME_BELL), target.str, errno); + delete tp; goto err_exit; } #endif + + if (command) + assert("not implemented yet" == 0); + else + for (int t= threads; t--; ) + { + constexpr IF_WIN(HANDLE sink{INVALID_HANDLE_VALUE}, int sink{-1}); + new (&target_phase[t]) backup_target_phase{ + backup_target{IF_WIN(target.str, dir), sink}, BACKUP_PHASE_START, 0}; + } + bool fail{false}; - static constexpr struct { - backup_phase name; enum_mdl_type mdl; - } phases[] { - { BACKUP_PHASE_START, MDL_BACKUP_START }, - { BACKUP_PHASE_NO_BEGIN_NON_TRANS, MDL_BACKUP_FLUSH }, - { BACKUP_PHASE_NO_DML_NON_TRANS, MDL_BACKUP_WAIT_FLUSH }, - { BACKUP_PHASE_NO_DDL, MDL_BACKUP_WAIT_DDL }, - { BACKUP_PHASE_NO_COMMIT, MDL_BACKUP_WAIT_COMMIT } - }; - - for (const auto &phase : phases) + static_assert(int{MDL_BACKUP_START} + 1 == int{MDL_BACKUP_FLUSH}, ""); + static_assert(int{MDL_BACKUP_START} + 2 == int{MDL_BACKUP_WAIT_FLUSH}, ""); + static_assert(int{MDL_BACKUP_START} + 3 == int{MDL_BACKUP_WAIT_DDL}, ""); + static_assert(int{MDL_BACKUP_START} + 4 == int{MDL_BACKUP_WAIT_COMMIT}, ""); + static_assert(int{BACKUP_PHASE_START} + 1 == + int{BACKUP_PHASE_NO_BEGIN_NON_TRANS}, ""); + static_assert(int{BACKUP_PHASE_START} + 2 == + int{BACKUP_PHASE_NO_DML_NON_TRANS}, ""); + static_assert(int{BACKUP_PHASE_START} + 3 == int{BACKUP_PHASE_NO_DDL}, ""); + static_assert(int{BACKUP_PHASE_START} + 4 == + int{BACKUP_PHASE_NO_COMMIT}, ""); + int phase= int{BACKUP_PHASE_START}; + goto backup_phase_start; + + for (; phase <= int{BACKUP_PHASE_NO_COMMIT}; phase++) { - backup_target_phase t{dir, phase.name}; assert(!fail); - fail= phase.mdl != MDL_BACKUP_START && - thd->mdl_context.upgrade_shared_lock(mdl_request.ticket, - phase.mdl, - thd->variables.lock_wait_timeout); - if (fail) - break; + { + const enum_mdl_type mdl= + enum_mdl_type(phase - int{BACKUP_PHASE_START} + int{MDL_BACKUP_START}); + fail= + thd->mdl_context.upgrade_shared_lock(mdl_request.ticket, mdl, + thd->variables.lock_wait_timeout); + if (fail) + break; + } + backup_phase_start: + target_phase->phase= backup_phase(phase); fail= plugin_foreach_with_mask(thd, backup_start, MYSQL_STORAGE_ENGINE_PLUGIN, - PLUGIN_IS_DELETED|PLUGIN_IS_READY, &t); - if (fail) - break; - /* TO DO: invoke backup_step from multiple concurrent threads. */ - fail= plugin_foreach_with_mask(thd, backup_step, - MYSQL_STORAGE_ENGINE_PLUGIN, - PLUGIN_IS_DELETED|PLUGIN_IS_READY, &t); - /* After all backup_step are completed, finish the phase. */ + PLUGIN_IS_DELETED|PLUGIN_IS_READY, + target_phase); if (fail) break; - fail= + fail= backup_steps(thd, target_phase, threads, tp) || plugin_foreach_with_mask(thd, backup_end, MYSQL_STORAGE_ENGINE_PLUGIN, - PLUGIN_IS_DELETED|PLUGIN_IS_READY, &t); + PLUGIN_IS_DELETED|PLUGIN_IS_READY, + target_phase); if (fail) break; } + delete tp; /* The final part must not interfere with the use of the server datadir. Release the locks. */ thd->mdl_context.release_lock(mdl_request.ticket); - backup_target_phase finish{dir, BACKUP_PHASE_FINISH}; + target_phase->phase= BACKUP_PHASE_FINISH; fail= plugin_foreach_with_mask(thd, backup_end, MYSQL_STORAGE_ENGINE_PLUGIN, - PLUGIN_IS_DELETED|PLUGIN_IS_READY, &finish) || - fail; + PLUGIN_IS_DELETED|PLUGIN_IS_READY, + target_phase) || fail; #ifndef _WIN32 - close(dir.fd); + close(dir); #endif if (!fail) diff --git a/sql/sql_backup.h b/sql/sql_backup.h index 9aba2404dac58..239de7aa23eff 100644 --- a/sql/sql_backup.h +++ b/sql/sql_backup.h @@ -20,11 +20,21 @@ this program; if not, write to the Free Software Foundation, Inc., /** BACKUP SERVER */ class Sql_cmd_backup : public Sql_cmd { - /** target directory */ + /** target or working directory */ const LEX_CSTRING target; + /** number of concurrent threads to use */ + const int threads; + /** argument of my_popen() for streaming backup, or nullptr */ + const char *const command; public: - explicit Sql_cmd_backup(LEX_CSTRING target) : target(target) {} + /** + Constructor. + @param target name of target or scratch directory + @param threads number of concurrent threads to use + @param command nullptr, or a shell command for handling a backup stream */ + Sql_cmd_backup(LEX_CSTRING target, int threads, const char *command) : + target(target), threads(threads), command(command) {} ~Sql_cmd_backup() = default; bool execute(THD *thd) override; diff --git a/sql/sql_backup_interface.h b/sql/sql_backup_interface.h index 39bebc635ffbc..f1b24f3ef436e 100644 --- a/sql/sql_backup_interface.h +++ b/sql/sql_backup_interface.h @@ -14,6 +14,17 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1335 USA */ struct backup_target; + +/** A hole in a file that is being streamed */ +struct backup_hole +{ + /** byte offset of the start of the hole, from the start of the file, + in multiples of 512 bytes */ + uint64_t offset; + /** length of the hole, in multiples of 512 bytes */ + uint64_t length; +}; + #ifdef _WIN32 /* Use CopyFileEx() to copy entire files */ struct native_file_handle; @@ -69,3 +80,34 @@ extern "C" @retval 0 on success */ int backup_config_append(const struct backup_target *target, const char *config, size_t size); + +#ifdef __cplusplus +extern "C" +#endif +/** Start streaming a file. +@param target backup target +@param name file name +@param size logical length of the file, in bytes +@param holes description of holes (for sparse files) +@param n_holes number of holes elements +@return error code (non-positive) +@retval 0 on success */ +int backup_stream_start(const struct backup_target *target, + const char *name, uint64_t size, + const struct backup_hole *holes, + size_t n_holes); + +#ifdef __cplusplus +extern "C" +#endif +/** Append a file snippet to stream, +after a corresponding call to backup_stream_start(). +@param target backup target +@param src source file +@param start first offset to copy +@param end last offset to copy (exclusive) +@return error code (non-positive) +@retval 0 on success */ +int backup_stream_append(const struct backup_target *target, + IF_WIN(const native_file_handle&,int) src, + uint64_t start, uint64_t end); diff --git a/sql/sql_yacc.yy b/sql/sql_yacc.yy index ec2c3449f6c53..48041e6138822 100644 --- a/sql/sql_yacc.yy +++ b/sql/sql_yacc.yy @@ -1472,7 +1472,7 @@ bool my_yyoverflow(short **a, YYSTYPE **b, size_t *yystacksize); %type json_on_response %type json_type_constraint -%type json_key_unique_constraint +%type json_key_unique_constraint opt_concurrent %type json_predicate %type field_type field_type_all field_type_all_builtin @@ -15558,11 +15558,31 @@ backup_statements: /* Table list is empty for unlock */ Lex->sql_command= SQLCOM_BACKUP_LOCK; } - | SERVER_SYM TO_SYM TEXT_STRING_sys + | SERVER_SYM TO_SYM TEXT_STRING_sys opt_concurrent { Lex->sql_command= SQLCOM_BACKUP_SERVER; - Lex->m_sql_cmd= new (thd->mem_root) Sql_cmd_backup($3); + Lex->m_sql_cmd= new (thd->mem_root) Sql_cmd_backup($3, $4, nullptr); } + | SERVER_SYM USING TEXT_STRING_sys WITH opt_concurrent TEXT_STRING_sys + { + Lex->sql_command= SQLCOM_BACKUP_SERVER; + Lex->m_sql_cmd= new (thd->mem_root) Sql_cmd_backup($3, $5, $6.str); + } + ; + +opt_concurrent: + /* empty */ + { $$= 1; } + | ulonglong_num CONCURRENT + { + $$= int($1); + if ($1 < 1 || $1 > 256) + { + my_error(ER_DATA_OUT_OF_RANGE, myf(0), "CONCURRENT", + "BACKUP SERVER"); + MYSQL_YYABORT; + } + } ; opt_delete_gtid_domain: From 17ce1be1ed9adf7f61a74a0ffceefe45ef64756e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20M=C3=A4kel=C3=A4?= Date: Thu, 11 Jun 2026 14:07:20 +0300 Subject: [PATCH 16/20] fixup! 8077134777e3d3fda875413b45f440873c46e358 --- sql/sql_backup.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sql/sql_backup.cc b/sql/sql_backup.cc index 9fc44d52dd6c9..e1e7f55f951f3 100644 --- a/sql/sql_backup.cc +++ b/sql/sql_backup.cc @@ -423,7 +423,7 @@ bool Sql_cmd_backup::execute(THD *thd) else for (int t= threads; t--; ) { - constexpr IF_WIN(HANDLE sink{INVALID_HANDLE_VALUE}, int sink{-1}); + IF_WIN(HANDLE sink{INVALID_HANDLE_VALUE}, constexpr int sink{-1}); new (&target_phase[t]) backup_target_phase{ backup_target{IF_WIN(target.str, dir), sink}, BACKUP_PHASE_START, 0}; } From cb1768c697480105f00da6df1331584bd4252536 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20M=C3=A4kel=C3=A4?= Date: Fri, 12 Jun 2026 08:25:21 +0300 Subject: [PATCH 17/20] No target directory for streaming --- .../suite/backup/backup_innodb,debug.rdiff | 2 +- mysql-test/suite/backup/backup_innodb.result | 10 ++-- mysql-test/suite/backup/backup_innodb.test | 6 +-- sql/handler.h | 4 +- sql/sql_backup.cc | 52 +++++++++---------- sql/sql_backup.h | 23 +++++--- sql/sql_yacc.yy | 6 +-- 7 files changed, 53 insertions(+), 50 deletions(-) diff --git a/mysql-test/suite/backup/backup_innodb,debug.rdiff b/mysql-test/suite/backup/backup_innodb,debug.rdiff index c6cc65ee312c3..e5a54cb3b7121 100644 --- a/mysql-test/suite/backup/backup_innodb,debug.rdiff +++ b/mysql-test/suite/backup/backup_innodb,debug.rdiff @@ -1,6 +1,6 @@ --- backup_innodb.result +++ backup_innodb,debug.result -@@ -10,7 +10,13 @@ +@@ -22,7 +22,13 @@ BEGIN; DELETE FROM t; connect backup,localhost,root; diff --git a/mysql-test/suite/backup/backup_innodb.result b/mysql-test/suite/backup/backup_innodb.result index 7da14595f7120..a398a4af4bbe6 100644 --- a/mysql-test/suite/backup/backup_innodb.result +++ b/mysql-test/suite/backup/backup_innodb.result @@ -11,12 +11,10 @@ BACKUP SERVER TO '$target_directory' WITH '/bin/false'; ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'WITH '/bin/false'' at line 1 BACKUP SERVER TO '$target_directory' WITH 4 CONCURRENT '/bin/false'; ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'WITH 4 CONCURRENT '/bin/false'' at line 1 -BACKUP SERVER USING '$target_directory'; -ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '' at line 1 -BACKUP SERVER USING '$target_directory' WITH '/bin/false'; -ERROR 42000: This version of MariaDB doesn't yet support 'BACKUP SERVER USING' -BACKUP SERVER USING '$target_directory' WITH 4 CONCURRENT '/bin/false'; -ERROR 42000: This version of MariaDB doesn't yet support 'BACKUP SERVER USING' +BACKUP SERVER WITH '/bin/false'; +ERROR 42000: This version of MariaDB doesn't yet support 'BACKUP SERVER WITH' +BACKUP SERVER WITH 4 CONCURRENT '/bin/false'; +ERROR 42000: This version of MariaDB doesn't yet support 'BACKUP SERVER WITH' ROLLBACK; SELECT * FROM t; a b diff --git a/mysql-test/suite/backup/backup_innodb.test b/mysql-test/suite/backup/backup_innodb.test index d0298883fe08c..9dba6021c947f 100644 --- a/mysql-test/suite/backup/backup_innodb.test +++ b/mysql-test/suite/backup/backup_innodb.test @@ -27,12 +27,10 @@ evalp BACKUP SERVER TO '$target_directory'; evalp BACKUP SERVER TO '$target_directory' WITH '/bin/false'; --error ER_PARSE_ERROR evalp BACKUP SERVER TO '$target_directory' WITH 4 CONCURRENT '/bin/false'; ---error ER_PARSE_ERROR -evalp BACKUP SERVER USING '$target_directory'; --error ER_NOT_SUPPORTED_YET -evalp BACKUP SERVER USING '$target_directory' WITH '/bin/false'; +evalp BACKUP SERVER WITH '/bin/false'; --error ER_NOT_SUPPORTED_YET -evalp BACKUP SERVER USING '$target_directory' WITH 4 CONCURRENT '/bin/false'; +evalp BACKUP SERVER WITH 4 CONCURRENT '/bin/false'; --rmdir $target_directory diff --git a/sql/handler.h b/sql/handler.h index 59948016e1956..97c9b28dfff6d 100644 --- a/sql/handler.h +++ b/sql/handler.h @@ -1500,12 +1500,12 @@ struct transaction_participant struct backup_target { #ifdef _WIN32 - /** Target or spare directory path name */ + /** Target directory path name, or nullptr if streaming */ const char *path; /** A value indicating an invalid stream */ static constexpr HANDLE NO_STREAM{INVALID_HANDLE_VALUE}; #else - /** Target or spare directory descriptor */ + /** Target or spare directory descriptor, or -1 if streaming */ int fd; /** A value indicating an invalid stream */ static constexpr int NO_STREAM{-1}; diff --git a/sql/sql_backup.cc b/sql/sql_backup.cc index e1e7f55f951f3..fcf905f6e039c 100644 --- a/sql/sql_backup.cc +++ b/sql/sql_backup.cc @@ -364,17 +364,13 @@ static bool backup_steps(THD *thd, backup_target_phase *target_phase, bool Sql_cmd_backup::execute(THD *thd) { + assert(!!target == !command); + if (check_global_access(thd, RELOAD_ACL) || check_global_access(thd, SELECT_ACL) || - error_if_data_home_dir(target.str, "BACKUP SERVER TO")) + (target && error_if_data_home_dir(target, "BACKUP SERVER TO"))) return true; - if (command) - { - my_error(ER_NOT_SUPPORTED_YET, MYF(0), "BACKUP SERVER USING"); - return true; - } - if (thd->current_backup_stage != BACKUP_FINISHED) { my_error(ER_BACKUP_LOCK_IS_ACTIVE, MYF(0)); @@ -390,43 +386,46 @@ bool Sql_cmd_backup::execute(THD *thd) thd->variables.lock_wait_timeout)) return true; - if (my_mkdir(target.str, 0755, MYF(MY_WME))) - { -#ifndef _WIN32 - err_exit: -#endif - thd->mdl_context.release_lock(mdl_request.ticket); - return true; - } - tpool::thread_pool *tp= nullptr; backup_target_phase *target_phase= static_cast (alloca(threads * sizeof *target_phase)); if (threads > 1 && !(tp= tpool::create_thread_pool_generic())) { my_error(ER_OUT_OF_RESOURCES, MYF(0)); + err_exit: + thd->mdl_context.release_lock(mdl_request.ticket); + delete tp; return true; } #ifndef _WIN32 - const int dir{open(target.str, O_DIRECTORY)}; - if (dir < 0) + int dir{-1}; +#endif + + if (!target) { - my_error(EE_CANT_MKDIR, MYF(ME_BELL), target.str, errno); - delete tp; + my_error(ER_NOT_SUPPORTED_YET, MYF(0), "BACKUP SERVER WITH"); goto err_exit; } -#endif - - if (command) - assert("not implemented yet" == 0); + else if (my_mkdir(target, 0755, MYF(MY_WME))) + goto err_exit; else + { +#ifndef _WIN32 + dir= open(target, O_DIRECTORY); + if (dir < 0) + { + my_error(EE_CANT_MKDIR, MYF(ME_BELL), target, errno); + goto err_exit; + } +#endif for (int t= threads; t--; ) { IF_WIN(HANDLE sink{INVALID_HANDLE_VALUE}, constexpr int sink{-1}); new (&target_phase[t]) backup_target_phase{ - backup_target{IF_WIN(target.str, dir), sink}, BACKUP_PHASE_START, 0}; + backup_target{IF_WIN(target, dir), sink}, BACKUP_PHASE_START, 0}; } + } bool fail{false}; @@ -482,7 +481,8 @@ bool Sql_cmd_backup::execute(THD *thd) PLUGIN_IS_DELETED|PLUGIN_IS_READY, target_phase) || fail; #ifndef _WIN32 - close(dir); + if (dir != -1) + close(dir); #endif if (!fail) diff --git a/sql/sql_backup.h b/sql/sql_backup.h index 239de7aa23eff..2759e0d64b43d 100644 --- a/sql/sql_backup.h +++ b/sql/sql_backup.h @@ -20,22 +20,29 @@ this program; if not, write to the Free Software Foundation, Inc., /** BACKUP SERVER */ class Sql_cmd_backup : public Sql_cmd { - /** target or working directory */ - const LEX_CSTRING target; + /** target directory, or nullptr when streaming */ + const char *const target{nullptr}; + /** argument of my_popen() for streaming backup, or nullptr */ + const char *const command{nullptr}; /** number of concurrent threads to use */ const int threads; - /** argument of my_popen() for streaming backup, or nullptr */ - const char *const command; public: /** Constructor. @param target name of target or scratch directory @param threads number of concurrent threads to use - @param command nullptr, or a shell command for handling a backup stream */ - Sql_cmd_backup(LEX_CSTRING target, int threads, const char *command) : - target(target), threads(threads), command(command) {} - ~Sql_cmd_backup() = default; + */ + Sql_cmd_backup(LEX_CSTRING target, int threads) : + target(target.str), threads(threads) {} + /** + Constructor. + @param threads number of concurrent threads to use + @param command nullptr, or a shell command for handling a backup stream + */ + Sql_cmd_backup(int threads, LEX_CSTRING command) : + command(command.str), threads(threads) {} + ~Sql_cmd_backup()= default; bool execute(THD *thd) override; diff --git a/sql/sql_yacc.yy b/sql/sql_yacc.yy index 48041e6138822..6675254d4baaa 100644 --- a/sql/sql_yacc.yy +++ b/sql/sql_yacc.yy @@ -15561,12 +15561,12 @@ backup_statements: | SERVER_SYM TO_SYM TEXT_STRING_sys opt_concurrent { Lex->sql_command= SQLCOM_BACKUP_SERVER; - Lex->m_sql_cmd= new (thd->mem_root) Sql_cmd_backup($3, $4, nullptr); + Lex->m_sql_cmd= new (thd->mem_root) Sql_cmd_backup($3, $4); } - | SERVER_SYM USING TEXT_STRING_sys WITH opt_concurrent TEXT_STRING_sys + | SERVER_SYM WITH opt_concurrent TEXT_STRING_sys { Lex->sql_command= SQLCOM_BACKUP_SERVER; - Lex->m_sql_cmd= new (thd->mem_root) Sql_cmd_backup($3, $5, $6.str); + Lex->m_sql_cmd= new (thd->mem_root) Sql_cmd_backup($3, $4); } ; From 00667433f33a8c6e113565ba43e7cfca8e7a2563 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20M=C3=A4kel=C3=A4?= Date: Fri, 12 Jun 2026 12:38:16 +0300 Subject: [PATCH 18/20] More stub for streaming backup TODO: Cover the InnoDB log copying TODO: Cover non-InnoDB files TODO: Actually implement the tar format --- sql/handler.h | 12 ++-- sql/sql_backup.cc | 76 ++++++++++++++++++++--- storage/innobase/handler/backup_innodb.cc | 46 ++++++++++++-- 3 files changed, 115 insertions(+), 19 deletions(-) diff --git a/sql/handler.h b/sql/handler.h index 97c9b28dfff6d..d480dfae98736 100644 --- a/sql/handler.h +++ b/sql/handler.h @@ -1500,17 +1500,17 @@ struct transaction_participant struct backup_target { #ifdef _WIN32 - /** Target directory path name, or nullptr if streaming */ - const char *path; /** A value indicating an invalid stream */ static constexpr HANDLE NO_STREAM{INVALID_HANDLE_VALUE}; + /** Target directory path name, or nullptr if streaming */ + const char *path; #else - /** Target or spare directory descriptor, or -1 if streaming */ - int fd; - /** A value indicating an invalid stream */ + /** A value indicating an invalid file descriptor or stream */ static constexpr int NO_STREAM{-1}; + /** Target directory descriptor, or NO_STREAM if streaming */ + int fd; #endif - /** Target pipe, or NO_STREAM if copying to the target directory */ + /** Target pipe, or NO_STREAM if copying to a directory */ IF_WIN(HANDLE,int) stream; }; diff --git a/sql/sql_backup.cc b/sql/sql_backup.cc index fcf905f6e039c..7e10bb9a836f7 100644 --- a/sql/sql_backup.cc +++ b/sql/sql_backup.cc @@ -266,6 +266,8 @@ struct backup_target_phase backup_phase phase; /** handlerton::backup_step return value in multi-threaded operation */ int ret; + /** stream object for streaming backup */ + FILE *stream; }; static my_bool backup_start(THD *thd, plugin_ref plugin, void *arg) noexcept @@ -391,6 +393,7 @@ bool Sql_cmd_backup::execute(THD *thd) (alloca(threads * sizeof *target_phase)); if (threads > 1 && !(tp= tpool::create_thread_pool_generic())) { + oor: my_error(ER_OUT_OF_RESOURCES, MYF(0)); err_exit: thd->mdl_context.release_lock(mdl_request.ticket); @@ -398,21 +401,36 @@ bool Sql_cmd_backup::execute(THD *thd) return true; } -#ifndef _WIN32 - int dir{-1}; -#endif - - if (!target) + if (command) { +#if 1 /* FIXME: remove this */ my_error(ER_NOT_SUPPORTED_YET, MYF(0), "BACKUP SERVER WITH"); goto err_exit; +#endif + for (int t= threads; t; ) + { + FILE *f= my_popen(command, "w"); + if (!f) + { + while (t < threads) + my_pclose(target_phase[t++].stream); + goto oor; + } +#ifdef _WIN + HANDLE sink= (HANDLE) _get_osfhandle(_fileno(f)); +#else + int sink= fileno(f); +#endif + new (&target_phase[t--]) backup_target_phase{ + backup_target{IF_WIN(nullptr, -1), sink}, BACKUP_PHASE_START, 0, f}; + } } else if (my_mkdir(target, 0755, MYF(MY_WME))) goto err_exit; else { #ifndef _WIN32 - dir= open(target, O_DIRECTORY); + const int dir{open(target, O_DIRECTORY)}; if (dir < 0) { my_error(EE_CANT_MKDIR, MYF(ME_BELL), target, errno); @@ -423,7 +441,7 @@ bool Sql_cmd_backup::execute(THD *thd) { IF_WIN(HANDLE sink{INVALID_HANDLE_VALUE}, constexpr int sink{-1}); new (&target_phase[t]) backup_target_phase{ - backup_target{IF_WIN(target, dir), sink}, BACKUP_PHASE_START, 0}; + backup_target{IF_WIN(target, dir), sink}, BACKUP_PHASE_START, 0, 0}; } } @@ -480,12 +498,52 @@ bool Sql_cmd_backup::execute(THD *thd) MYSQL_STORAGE_ENGINE_PLUGIN, PLUGIN_IS_DELETED|PLUGIN_IS_READY, target_phase) || fail; + if (command) + for (int t= threads; t--; ) + my_pclose(target_phase[t].stream); #ifndef _WIN32 - if (dir != -1) - close(dir); + else + std::ignore= close(target_phase->target.fd); #endif if (!fail) my_ok(thd); return fail; } + +/** Start streaming a file. +@param target backup target +@param name file name +@param size logical length of the file, in bytes +@param holes description of holes (for sparse files) +@param n_holes number of holes elements +@return error code (non-positive) +@retval 0 on success */ +extern "C" +int backup_stream_start(const struct backup_target *target, + const char *name, uint64_t size, + const struct backup_hole *holes, + size_t n_holes) +{ + assert(target->stream != target->NO_STREAM); + return -1; +} + +#ifdef __cplusplus +#endif +/** Append a file snippet to stream, +after a corresponding call to backup_stream_start(). +@param target backup target +@param src source file +@param start first offset to copy +@param end last offset to copy (exclusive) +@return error code (non-positive) +@retval 0 on success */ +extern "C" +int backup_stream_append(const struct backup_target *target, + IF_WIN(const native_file_handle&,int) src, + uint64_t start, uint64_t end) +{ + assert(target->stream != target->NO_STREAM); + return -1; +} diff --git a/storage/innobase/handler/backup_innodb.cc b/storage/innobase/handler/backup_innodb.cc index ea99a255061f7..0ac33f0e595b9 100644 --- a/storage/innobase/handler/backup_innodb.cc +++ b/storage/innobase/handler/backup_innodb.cc @@ -177,9 +177,10 @@ class InnoDB_backup { int res= -1; uint32_t start{0}; + auto method= target.stream == target.NO_STREAM ? backup : stream; for (fil_node_t *node= UT_LIST_GET_FIRST(space->chain); node; start+= node->size, node= UT_LIST_GET_NEXT(chain, node)) - if ((res= backup(target, node, start))) + if ((res= (*method)(target, node, start))) break; space->release(); if (res) @@ -498,13 +499,15 @@ class InnoDB_backup @param target backup target @param node InnoDB data file @param start first page number + @return error code (non-positive) + @retval 0 on success */ - int backup(const backup_target &target, fil_node_t *node, uint32_t start) - noexcept + static int backup(const backup_target &target, fil_node_t *node, + uint32_t start) noexcept { + ut_ad(target.stream == target.NO_STREAM); for (bool tried_mkdir{false};;) { - ut_ad(target.stream == target.NO_STREAM); #ifdef _WIN32 std::string path{target.path}; path.push_back('/'); @@ -598,6 +601,41 @@ class InnoDB_backup return -1; } + /** + Stream a persistent InnoDB data file. + @param target backup target + @param node InnoDB data file + @param start first page number + @return error code (non-positive) + @retval 0 on success + */ + static int stream(const backup_target &target, fil_node_t *node, + uint32_t start) noexcept + { + const uint32_t file_size{node->size}, + page_size{node->space->physical_size()}; + int err= backup_stream_start(&target, node->name, + uint64_t{file_size} * page_size, + /* TODO: leave holes for freed page ranges */ + nullptr, 0); + while (!err && start < file_size) + { + const uint32_t end{start + fil_space_t::BACKUP_BATCH_SIZE}; + backup_start(node->space, end); + err= backup_stream_append(&target, node->handle, + start * uint64_t{page_size}, + std::min(end, file_size) * + uint64_t{page_size}); + backup_stop(node->space); + start= end; + } + + if (err) + my_error(ER_IO_WRITE_ERROR, MYF(0), errno, strerror(errno), + "BACKUP SERVER"); + return err; + } + /** Write a checkpoint header pointing to the start of the backup. @param dst target file @param c offset of the FILE_CHECKPOINT mini-transaction From ab851de5682a374362f530219e063a95c7f18d37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20M=C3=A4kel=C3=A4?= Date: Fri, 12 Jun 2026 14:04:01 +0300 Subject: [PATCH 19/20] fixup! 00667433f33a8c6e113565ba43e7cfca8e7a2563 --- sql/handler.h | 6 ++++-- sql/sql_backup.cc | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/sql/handler.h b/sql/handler.h index d480dfae98736..1ae9198160abb 100644 --- a/sql/handler.h +++ b/sql/handler.h @@ -1504,14 +1504,16 @@ struct backup_target static constexpr HANDLE NO_STREAM{INVALID_HANDLE_VALUE}; /** Target directory path name, or nullptr if streaming */ const char *path; + /** Target pipe, or NO_STREAM if path!=nullptr */ + HANDLE stream; #else /** A value indicating an invalid file descriptor or stream */ static constexpr int NO_STREAM{-1}; /** Target directory descriptor, or NO_STREAM if streaming */ int fd; -#endif /** Target pipe, or NO_STREAM if copying to a directory */ - IF_WIN(HANDLE,int) stream; + int stream; +#endif }; /** BACKUP SERVER execution phase */ diff --git a/sql/sql_backup.cc b/sql/sql_backup.cc index 7e10bb9a836f7..08861cfab2e72 100644 --- a/sql/sql_backup.cc +++ b/sql/sql_backup.cc @@ -416,7 +416,7 @@ bool Sql_cmd_backup::execute(THD *thd) my_pclose(target_phase[t++].stream); goto oor; } -#ifdef _WIN +#ifdef _WIN32 HANDLE sink= (HANDLE) _get_osfhandle(_fileno(f)); #else int sink= fileno(f); From d9ef0340e4e5d87378381629b57a6d204ef7e708 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20M=C3=A4kel=C3=A4?= Date: Fri, 12 Jun 2026 16:00:13 +0300 Subject: [PATCH 20/20] Implement most of backup_stream_start() --- sql/sql_backup.cc | 120 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 118 insertions(+), 2 deletions(-) diff --git a/sql/sql_backup.cc b/sql/sql_backup.cc index 08861cfab2e72..1146d391e3f83 100644 --- a/sql/sql_backup.cc +++ b/sql/sql_backup.cc @@ -225,7 +225,7 @@ extern "C" int backup_config_append(const backup_target *target, { DWORD written; ok= WriteFile(dst, config, DWORD(size), &written, nullptr); - if (ok || !written || GetLastError() != ERROR_IO_PENDING) + if (ok || GetLastError() != ERROR_IO_PENDING) break; assert(written < DWORD(size)); config+= written; @@ -511,6 +511,70 @@ bool Sql_cmd_backup::execute(THD *thd) return fail; } +/** + Encode an octal string. + @param start first byte of buffer + @param end first byte after buffer + @param n number to encode +*/ +static void ustar_write_octal(char *start, char *end, uint64_t n) noexcept +{ + for (*--end= '\0'; --end >= start; n>>= 3) + *end= char('0' + (n & 7)); +} + +/** + Encode a quantity in 12 bytes. + @param start first byte of the buffer + @param n number to encode +*/ +static void ustar_write_dozen(char *start, uint64_t n) noexcept +{ + if (n < 1ULL << 33) + ustar_write_octal(start, start + 12, n); + else + { + const uint32_t head{my_htobe32(1U << 31)}; + n= my_htobe64(n); + memcpy(start, &head, 4); + memcpy(start + 4, &n, 8); + } +} + +/** Initialize a ustar block +@param buf GNU tape archiver --format=oldgnu header block +@param name name of the block +@param size physical size of the following block */ +static void ustar_block_init(char *buf, const char *name, uint64_t size) + noexcept +{ + strncpy(buf, name, 100); + ustar_write_octal(buf + 100, buf + 108, 0644/* file permissions */); + ustar_write_octal(buf + 108, buf + 116, 0/* POSIX uid */); + ustar_write_octal(buf + 116, buf + 124, 0/* POSIX gid */); + ustar_write_dozen(buf + 124, size); + /* last modification time */ + ustar_write_octal(buf + 136, buf + 148, 0); + memset(buf + 148, ' ', 9); /* initial block checksum and dummy type */ + memset(buf + 157, '\0', 100); /* name of linked file (unused) */ + memcpy(buf + 257, "ustar ", 8); + strncpy(buf + 265, "root" /* POSIX owner name */, 32); + strncpy(buf + 297, "root" /* POSIX group name */, 512 - 297); + /* The caller will fill in the rest and invoke ustar_block_checksum() */ +} + +/** + Compute and write the POSIX tar block checksum. + @param buf POSIX tar block +*/ +static void ustar_block_checksum(char *buf) noexcept +{ + uint16_t sum{0}; + for (int i{0}; i < 512; i++) + sum+= uint16_t{uint8_t(buf[i])}; + ustar_write_octal(buf + 148, buf + 155, sum); +} + /** Start streaming a file. @param target backup target @param name file name @@ -526,7 +590,59 @@ int backup_stream_start(const struct backup_target *target, size_t n_holes) { assert(target->stream != target->NO_STREAM); - return -1; + char buf[512]; + size_t s= strlen(name); + if (s > 100) + { + ustar_block_init(buf, "././@LongLink", s); + return -1; // FIXME: write the block and the file name + } + + ustar_block_init(buf, name, size); + if (!n_holes) + buf[156]= '0'; + else + { + buf[156]= 'S'; + char *h= &buf[386]; + if (n_holes > 4) + return -1; // FIXME; support more holes + for (size_t i= 0; i < n_holes; i++, h+= 24) + { + ustar_write_dozen(h, holes[i].offset); + ustar_write_dozen(h + 12, holes[i].length); + } + } + ustar_block_checksum(buf); + char *b= buf; +#ifdef _WIN32 + for (DWORD sz{sizeof buf};;) + { + DWORD wrote; + if (WriteFile(target->stream, b, sz, &wrote, nullptr)) + { + assert(wrote == sz); + return 0; + } + else if (GetLastError() != ERROR_IO_PENDING) + return -1; + b+= wrote; + sz-= wrote; + } +#else + s= sizeof buf; + do + { + ssize_t wrote= write(target->stream, b, s); + assert(wrote <= ssize_t(s)); + if (wrote < 0) + return -1; + b+= wrote; + s-= size_t(wrote); + } + while (s); +#endif + return 0; } #ifdef __cplusplus