From 2b2b223b93f253e2c727d029ddb9445b7852f993 Mon Sep 17 00:00:00 2001 From: Thirunarayanan Balathandayuthapani Date: Wed, 15 Jul 2026 15:59:35 +0530 Subject: [PATCH] MDEV-39091 BACKUP SERVER of ENGINE=RocksDB to the local file system Wire the RocksDB (MyRocks) engine into BACKUP SERVER TO and BACKUP SERVER WITH (streaming). RocksDB SST files are immutable, so a consistent point-in-time snapshot is obtained cheaply with a rocksdb::Checkpoint. The checkpoint is frozen at BACKUP_PHASE_NO_COMMIT because the checkpoint files are immutable, the actual copy is deferred to BACKUP_PHASE_FINISH, after the backup MDL has been released, and the server-side checkpoint is removed at end of FINISH. No user-level lock is needed: MDL_BACKUP_START already serializes concurrent BACKUP SERVER. Files are copied into the target's "#rocksdb" subdirectory. The default rocksdb_datadir ("./#rocksdb") makes restore transparent: on restore the files land at /#rocksdb, so pointing a server at the extracted backup just works, with no copy-back and no RocksDB prepare step. storage/rocksdb/rdb_backup_server.{h,cc}: New RocksDB_backup context and the three handlerton hooks. The checkpoint file list is drained across N CONCURRENT step threads via a lock-free atomic cursor; each file is copied with copy_entire_file() (directory target) or backup_stream_*(). Since SSTs are immutable, the sendfile(2) fast path (backup_stream_append_async) is used for the stream target. rdb_create_checkpoint(): Factor the checkpoint creation rdb_remove_checkpoint(): Remove the checkpoint creation logic. rdb_get_datadir(): Get the rocksdb data directory. --- mysql-test/suite/backup/backup_rocksdb.opt | 1 + mysql-test/suite/backup/backup_rocksdb.result | 61 +++ mysql-test/suite/backup/backup_rocksdb.test | 76 ++++ mysql-test/suite/backup/suite.pm | 1 + storage/rocksdb/CMakeLists.txt | 2 + storage/rocksdb/ha_rocksdb.cc | 91 ++-- storage/rocksdb/ha_rocksdb_proto.h | 7 + storage/rocksdb/rdb_backup_server.cc | 403 ++++++++++++++++++ storage/rocksdb/rdb_backup_server.h | 73 ++++ 9 files changed, 681 insertions(+), 34 deletions(-) create mode 100644 mysql-test/suite/backup/backup_rocksdb.opt create mode 100644 mysql-test/suite/backup/backup_rocksdb.result create mode 100644 mysql-test/suite/backup/backup_rocksdb.test create mode 100644 storage/rocksdb/rdb_backup_server.cc create mode 100644 storage/rocksdb/rdb_backup_server.h diff --git a/mysql-test/suite/backup/backup_rocksdb.opt b/mysql-test/suite/backup/backup_rocksdb.opt new file mode 100644 index 0000000000000..0d7b788c1bbee --- /dev/null +++ b/mysql-test/suite/backup/backup_rocksdb.opt @@ -0,0 +1 @@ +--plugin-load-add=$HA_ROCKSDB_SO diff --git a/mysql-test/suite/backup/backup_rocksdb.result b/mysql-test/suite/backup/backup_rocksdb.result new file mode 100644 index 0000000000000..ca7100f493697 --- /dev/null +++ b/mysql-test/suite/backup/backup_rocksdb.result @@ -0,0 +1,61 @@ +CREATE TABLE ti(a INT PRIMARY KEY, b CHAR(255) DEFAULT '' NOT NULL, INDEX(b)) +ENGINE=INNODB; +INSERT INTO ti SET a=1; +CREATE TABLE tr(a INT PRIMARY KEY, b CHAR(200)) ENGINE=ROCKSDB; +INSERT INTO tr VALUES (1,'r-one'),(2,'r-two'),(3,'r-three'); +SET GLOBAL rocksdb_force_flush_memtable_now=1; +BACKUP SERVER TO 'target_dir'; +INSERT INTO tr VALUES (4,'r-four'); +INSERT INTO ti VALUES (2,'two'); +# restart: --defaults-file=MYSQLTEST_VARDIR/tmp/backup_rocksdb_dir/backup.cnf --datadir=MYSQLTEST_VARDIR/tmp/backup_rocksdb_dir --plugin-load-add=ha_rocksdb.so +SELECT * FROM ti; +a b +1 +SELECT * FROM tr; +a b +1 r-one +2 r-two +3 r-three +# restart on the original datadir +# restart +SELECT * FROM ti; +a b +1 +2 two +SELECT * FROM tr; +a b +1 r-one +2 r-two +3 r-three +4 r-four +# +# BACKUP SERVER WITH ... N CONCURRENT (streamed oldgnu tar) +# +BACKUP SERVER WITH 2 CONCURRENT 'rstream.bat'; +INSERT INTO tr VALUES (5,'r-five'); +INSERT INTO ti VALUES (3,'three'); +# restart: --defaults-file=MYSQLTEST_VARDIR/tmp/backup_rocksdb_stream/backup.cnf --datadir=MYSQLTEST_VARDIR/tmp/backup_rocksdb_stream --plugin-load-add=ha_rocksdb.so +SELECT * FROM ti; +a b +1 +2 two +SELECT * FROM tr; +a b +1 r-one +2 r-two +3 r-three +4 r-four +# restart +SELECT * FROM ti; +a b +1 +3 three +2 two +SELECT * FROM tr; +a b +1 r-one +2 r-two +3 r-three +4 r-four +5 r-five +DROP TABLE ti, tr; diff --git a/mysql-test/suite/backup/backup_rocksdb.test b/mysql-test/suite/backup/backup_rocksdb.test new file mode 100644 index 0000000000000..fea0f1c59d324 --- /dev/null +++ b/mysql-test/suite/backup/backup_rocksdb.test @@ -0,0 +1,76 @@ +--source include/have_rocksdb.inc +--source include/have_innodb.inc + +CREATE TABLE ti(a INT PRIMARY KEY, b CHAR(255) DEFAULT '' NOT NULL, INDEX(b)) +ENGINE=INNODB; +INSERT INTO ti SET a=1; + +CREATE TABLE tr(a INT PRIMARY KEY, b CHAR(200)) ENGINE=ROCKSDB; +INSERT INTO tr VALUES (1,'r-one'),(2,'r-two'),(3,'r-three'); +SET GLOBAL rocksdb_force_flush_memtable_now=1; + +--let $targetdir=$MYSQLTEST_VARDIR/tmp/backup_rocksdb_dir +--error 0,1 +--rmdir $targetdir +--replace_result $targetdir target_dir +--eval BACKUP SERVER TO '$targetdir' +--file_exists $targetdir/#rocksdb + +INSERT INTO tr VALUES (4,'r-four'); +INSERT INTO ti VALUES (2,'two'); + +--exec cat $MYSQLTEST_VARDIR/my.cnf >> $targetdir/backup.cnf +--let $restart_parameters=--defaults-file=$targetdir/backup.cnf --datadir=$targetdir --plugin-load-add=$HA_ROCKSDB_SO +--source include/restart_mysqld.inc + +SELECT * FROM ti; +SELECT * FROM tr; + +--echo # restart on the original datadir +--let $restart_parameters= +--source include/restart_mysqld.inc +SELECT * FROM ti; +SELECT * FROM tr; +--rmdir $targetdir + +--echo # +--echo # BACKUP SERVER WITH ... N CONCURRENT (streamed oldgnu tar) +--echo # +--remove_files_wildcard $MYSQL_TMP_DIR/. rstream.bat +--write_file $MYSQL_TMP_DIR/rstream.bat +#!/bin/sh +exec cat > $MYSQL_TMP_DIR/$1.tar +EOF +--let $script=/bin/sh $MYSQL_TMP_DIR/rstream.bat + +--replace_result $script rstream.bat +eval BACKUP SERVER WITH 2 CONCURRENT '$script'; +--remove_file $MYSQL_TMP_DIR/rstream.bat + +INSERT INTO tr VALUES (5,'r-five'); +INSERT INTO ti VALUES (3,'three'); + +--let $target_directory=$MYSQLTEST_VARDIR/tmp/backup_rocksdb_stream +--error 0,1 +--rmdir $target_directory +--mkdir $target_directory +--exec tar xf $MYSQL_TMP_DIR/1.tar -C $target_directory +--exec tar xf $MYSQL_TMP_DIR/2.tar -C $target_directory +--file_exists $target_directory/#rocksdb +--exec cat $MYSQLTEST_VARDIR/my.cnf >> $target_directory/backup.cnf + +--let $restart_parameters=--defaults-file=$target_directory/backup.cnf --datadir=$target_directory --plugin-load-add=$HA_ROCKSDB_SO +--source include/restart_mysqld.inc +SELECT * FROM ti; +SELECT * FROM tr; + +--remove_file $MYSQL_TMP_DIR/1.tar +--remove_file $MYSQL_TMP_DIR/2.tar + +--let $restart_parameters= +--source include/restart_mysqld.inc +SELECT * FROM ti; +SELECT * FROM tr; + +DROP TABLE ti, tr; +--rmdir $target_directory diff --git a/mysql-test/suite/backup/suite.pm b/mysql-test/suite/backup/suite.pm index 7d36a22655efa..d4d14d0f0cbd9 100644 --- a/mysql-test/suite/backup/suite.pm +++ b/mysql-test/suite/backup/suite.pm @@ -13,6 +13,7 @@ my $have_tar = `tar --version 2>&1` =~ /tar .*\d\.\d/; sub skip_combinations { my %skip; $skip{'backup_stream.test'} = 'needs cat,tar' unless $have_cat && $have_tar; + $skip{'backup_rocksdb.test'} = 'needs cat,tar' unless $have_cat && $have_tar; %skip; } diff --git a/storage/rocksdb/CMakeLists.txt b/storage/rocksdb/CMakeLists.txt index 1c97177887265..e196696b0c174 100644 --- a/storage/rocksdb/CMakeLists.txt +++ b/storage/rocksdb/CMakeLists.txt @@ -99,6 +99,8 @@ SET(ROCKSDB_SE_SOURCES rdb_sst_info.h rdb_converter.cc rdb_converter.h + rdb_backup_server.cc + rdb_backup_server.h ) # MariaDB: the following is added in build_rocksdb.cmake, when appropriate: diff --git a/storage/rocksdb/ha_rocksdb.cc b/storage/rocksdb/ha_rocksdb.cc index 49a11c6285fd8..df0e68e72c5ec 100644 --- a/storage/rocksdb/ha_rocksdb.cc +++ b/storage/rocksdb/ha_rocksdb.cc @@ -73,6 +73,7 @@ /* MyRocks includes */ #include "./event_listener.h" #include "./ha_rocksdb_proto.h" +#include "./rdb_backup_server.h" #include "./logger.h" #include "./nosql_access.h" #include "./rdb_cf_manager.h" @@ -371,6 +372,41 @@ static std::string rdb_normalize_dir(std::string dir) { return dir; } +/** Create a RocksDB checkpoint (a consistent, hardlink-based +snapshot of the live database) in @checkpoint_dir. +@return HA_EXIT_SUCCESS on success +@return HA_EXIT_FAILURE otherwise (my_error() has already been +raised via rdb_error_to_mysql()). */ +int rdb_create_checkpoint(const char *checkpoint_dir_raw) +{ + if (!checkpoint_dir_raw || rdb == nullptr) + return HA_EXIT_FAILURE; + + std::string checkpoint_dir = rdb_normalize_dir(checkpoint_dir_raw); + // NO_LINT_DEBUG + sql_print_information("RocksDB: creating checkpoint in directory : %s\n", + checkpoint_dir.c_str()); + rocksdb::Checkpoint *checkpoint; + auto status = rocksdb::Checkpoint::Create(rdb, &checkpoint); + // We can only return HA_EXIT_FAILURE/HA_EXIT_SUCCESS here which is why + // the return code is ignored, but by calling into rdb_error_to_mysql, + // it will call my_error for us, which will propogate up to the client. + int rc __attribute__((__unused__)); + if (status.ok()) { + status = checkpoint->CreateCheckpoint(checkpoint_dir.c_str()); + + delete checkpoint; + if (status.ok()) { + // NO_LINT_DEBUG + sql_print_information("RocksDB: created checkpoint in directory : %s\n", + checkpoint_dir.c_str()); + return HA_EXIT_SUCCESS; + } + } + rc = ha_rocksdb::rdb_error_to_mysql(status); + return HA_EXIT_FAILURE; +} + static int rocksdb_create_checkpoint( THD *const thd MY_ATTRIBUTE((__unused__)), struct st_mysql_sys_var *const var MY_ATTRIBUTE((__unused__)), @@ -379,36 +415,7 @@ static int rocksdb_create_checkpoint( char buf[FN_REFLEN]; int len = sizeof(buf); const char *const checkpoint_dir_raw = value->val_str(value, buf, &len); - if (checkpoint_dir_raw) { - if (rdb != nullptr) { - std::string checkpoint_dir = rdb_normalize_dir(checkpoint_dir_raw); - // NO_LINT_DEBUG - sql_print_information("RocksDB: creating checkpoint in directory : %s\n", - checkpoint_dir.c_str()); - rocksdb::Checkpoint *checkpoint; - auto status = rocksdb::Checkpoint::Create(rdb, &checkpoint); - // We can only return HA_EXIT_FAILURE/HA_EXIT_SUCCESS here which is why - // the return code is ignored, but by calling into rdb_error_to_mysql, - // it will call my_error for us, which will propogate up to the client. - int rc __attribute__((__unused__)); - if (status.ok()) { - status = checkpoint->CreateCheckpoint(checkpoint_dir.c_str()); - delete checkpoint; - if (status.ok()) { - // NO_LINT_DEBUG - sql_print_information( - "RocksDB: created checkpoint in directory : %s\n", - checkpoint_dir.c_str()); - return HA_EXIT_SUCCESS; - } else { - rc = ha_rocksdb::rdb_error_to_mysql(status); - } - } else { - rc = ha_rocksdb::rdb_error_to_mysql(status); - } - } - } - return HA_EXIT_FAILURE; + return rdb_create_checkpoint(checkpoint_dir_raw); } /* This method is needed to indicate that the @@ -725,6 +732,20 @@ static int rmdir_force(const char *dir) { } +/** Remove a RocksDB checkpoint directory @checkpoint_dir. +The server owns these files, so removal always happens here rather +than by unlinking from the backup code directly. */ +void rdb_remove_checkpoint(const char *checkpoint_dir) { + if (unlink(checkpoint_dir) == 0) + return; + + rmdir_force(checkpoint_dir); +} + +/* Return the configured RocksDB data directory (rocksdb_datadir). */ +const char *rdb_get_datadir() { return rocksdb_datadir; } + + static void rocksdb_remove_mariabackup_checkpoint( my_core::THD *const, struct st_mysql_sys_var *const , @@ -733,10 +754,7 @@ static void rocksdb_remove_mariabackup_checkpoint( mariabackup_checkpoint_dir.append("/mariabackup-checkpoint"); - if (unlink(mariabackup_checkpoint_dir.c_str()) == 0) - return; - - rmdir_force(mariabackup_checkpoint_dir.c_str()); + rdb_remove_checkpoint(mariabackup_checkpoint_dir.c_str()); } @@ -5342,6 +5360,11 @@ static int rocksdb_init_func(void *const p) { */ rocksdb_hton->check_version = rocksdb_check_version; + /* BACKUP SERVER hooks (see rdb_backup_server.cc) */ + rocksdb_hton->backup_start = rocksdb_backup_start; + rocksdb_hton->backup_step = rocksdb_backup_step; + rocksdb_hton->backup_end = rocksdb_backup_end; + rocksdb_hton->flags = HTON_TEMPORARY_NOT_SUPPORTED | HTON_SUPPORTS_EXTENDED_KEYS | HTON_CAN_RECREATE; diff --git a/storage/rocksdb/ha_rocksdb_proto.h b/storage/rocksdb/ha_rocksdb_proto.h index 03d24957a231d..57dd46d7508e7 100644 --- a/storage/rocksdb/ha_rocksdb_proto.h +++ b/storage/rocksdb/ha_rocksdb_proto.h @@ -66,6 +66,13 @@ void rdb_get_global_perf_counters(Rdb_perf_counters *counters) void rdb_queue_save_stats_request(); +/** BACKUP SERVER: RocksDB checkpoint helpers, shared between the +ROCKSDB_CREATE_CHECKPOINT and ROCKSDB_REMOVE_MARIABACKUP_CHECKPOINT +system variables and the handlerton backup hooks. */ +int rdb_create_checkpoint(const char *checkpoint_dir); +void rdb_remove_checkpoint(const char *checkpoint_dir); +const char *rdb_get_datadir(); + /* Access to singleton objects. */ diff --git a/storage/rocksdb/rdb_backup_server.cc b/storage/rocksdb/rdb_backup_server.cc new file mode 100644 index 0000000000000..9cd11ff8a2b28 --- /dev/null +++ b/storage/rocksdb/rdb_backup_server.cc @@ -0,0 +1,403 @@ +/* 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 */ + +/* + BACKUP SERVER support for the RocksDB (MyRocks) engine. +*/ + +#define MYSQL_SERVER 1 + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#ifndef _WIN32 +# include +# include +# include +#endif + +#include "./rdb_mariadb_port.h" +#include "./ha_rocksdb_proto.h" +#include "./rdb_backup_server.h" + +namespace myrocks { + +std::string rocksdb_backup_checkpoint_dir() +{ + const char *datadir= rdb_get_datadir(); + std::string dir(datadir ? datadir : ""); + while (!dir.empty() && dir.back() == '/') + dir.pop_back(); + dir.push_back('/'); + dir.append(ROCKSDB_CHECKPOINT_SUBDIR); + return dir; +} + +/** BACKUP SERVER context for the RocksDB engine, attached to +backup_sink::ha_data. It is created single-threaded at +BACKUP_PHASE_NO_COMMIT (where it freezes a consistent, +hardlink-based rocksdb::Checkpoint and enumerates its files) and +drained by up to N step threads at BACKUP_PHASE_FINISH, which copy +the immutable checkpoint files into the target's "#rocksdb" subdirectory. +Because SST files are immutable, copying can safely happen after +the backup MDL has been released. +Each rocksdb_backup_step() call claims one file index via claim_next(); +a claim past file_count() means that worker is done. */ +class RocksDB_backup +{ +public: + RocksDB_backup()= default; + ~RocksDB_backup() noexcept + { +#ifndef _WIN32 + if (checkpoint_fd >= 0) + std::ignore= close(checkpoint_fd); +#endif + } + + RocksDB_backup(const RocksDB_backup &)= delete; + RocksDB_backup &operator=(const RocksDB_backup &)= delete; + + /** Freeze a rocksdb::Checkpoint into checkpoint_dir. + @return true on failure (my_error() already raised) */ + bool create_checkpoint() noexcept; + + /** Enumerate the checkpoint directory (non-recursive) into files[]. + @return 0 on success, non-zero on error */ + int scan_checkpoint() noexcept; + + /** Copy one checkpoint file @name into the target's "#rocksdb/" directory + or oldgnu tar stream. + @return 0 on success, non-zero on error */ + int copy_one(const backup_target &target, const backup_sink &sink, + const std::string &name) noexcept; + + /** Create the "#rocksdb" subdirectory in a directory target, exactly once. + For a streaming target this is a no-op (the tar entry name carries the + directory). + @return 0 on success, non-zero on error */ + int ensure_dest_subdir(const backup_target &target) noexcept; + + /** Remove the server-side checkpoint directory. It is safe on both + the FINISH and ABORT paths, and when no checkpoint was ever created. */ + void remove_checkpoint() noexcept + { + if (!checkpoint_created) + return; + rdb_remove_checkpoint(checkpoint_dir.c_str()); + checkpoint_created= false; + } + + /* Interface used by rocksdb_backup_step() across N threads. */ + size_t claim_next() noexcept { return next.fetch_add(1); } + size_t file_count() const noexcept { return files.size(); } + const std::string &file_at(size_t i) const noexcept { return files[i]; } + +private: + /** /mariabackup-checkpoint */ + std::string checkpoint_dir; + /** checkpoint entries (SST, MANIFEST, CURRENT, OPTIONS, WAL, ...) */ + std::vector files; + /** next index to hand to a step thread */ + std::atomic next{0}; + /** whether the "#rocksdb" subdir has been created in a directory target */ + std::atomic subdir_made{false}; + /** whether a server-side checkpoint currently exists */ + bool checkpoint_created{false}; +#ifndef _WIN32 + /** open(checkpoint_dir, O_DIRECTORY), for openat() of source files */ + int checkpoint_fd{-1}; +#endif + /** zero padding for the final partial 512-byte tar block */ + static constexpr const char zerobuf[511]{}; +}; + +int RocksDB_backup::ensure_dest_subdir(const backup_target &target) noexcept +{ +#ifndef _WIN32 + if (target.fd < 0) + return 0; + if (subdir_made.exchange(true)) + return 0; + if (mkdirat(target.fd, ROCKSDB_BACKUP_DIR, 0777) && errno != EEXIST) + { + my_error(ER_CANT_CREATE_FILE, MYF(0), ROCKSDB_BACKUP_DIR, errno); + return 1; + } +#else + if (!target.path) + return 0; + if (subdir_made.exchange(true)) + return 0; + std::string path(target.path); + path.push_back('/'); + path.append(ROCKSDB_BACKUP_DIR); + if (my_mkdir(path.c_str(), 0777, MYF(0)) && errno != EEXIST) + { + my_error(ER_CANT_CREATE_FILE, MYF(0), ROCKSDB_BACKUP_DIR, errno); + return 1; + } +#endif + return 0; +} + +bool RocksDB_backup::create_checkpoint() noexcept +{ + checkpoint_dir= rocksdb_backup_checkpoint_dir(); + /* Drop a stale checkpoint left behind by a previous, failed backup. + BACKUP SERVER is serialized by MDL_BACKUP_START, so no user-level lock is + needed to protect the fixed checkpoint path. */ + if (!access(checkpoint_dir.c_str(), F_OK)) + rdb_remove_checkpoint(checkpoint_dir.c_str()); + if (rdb_create_checkpoint(checkpoint_dir.c_str())) + return true; + checkpoint_created= true; + return false; +} + +int RocksDB_backup::scan_checkpoint() noexcept +{ +#ifndef _WIN32 + checkpoint_fd= open(checkpoint_dir.c_str(), O_RDONLY | O_DIRECTORY); + if (checkpoint_fd < 0) + { + my_error(ER_CANT_READ_DIR, MYF(0), checkpoint_dir.c_str(), errno); + return 1; + } +#endif + MY_DIR *dir_info= my_dir(checkpoint_dir.c_str(), MYF(MY_DONT_SORT)); + if (!dir_info) + { + my_error(ER_CANT_READ_DIR, MYF(0), checkpoint_dir.c_str(), my_errno); + return 1; + } + /* A rocksdb::Checkpoint is a flat directory, so a non-recursive scan is + sufficient (SST files, MANIFEST, CURRENT, OPTIONS, *.log). */ + for (size_t i= 0; i < dir_info->number_of_files; i++) + { + const char *name= dir_info->dir_entry[i].name; + if (name[0] == '.' && (!name[1] || (name[1] == '.' && !name[2]))) + continue; + files.emplace_back(name); + } + my_dirend(dir_info); + return 0; +} + +int RocksDB_backup::copy_one(const backup_target &target, + const backup_sink &sink, + const std::string &name) noexcept +{ + /* Destination is relative to the target: "#rocksdb/". The default + rocksdb_datadir "./#rocksdb" makes restore transparent. tar uses forward + slashes; Win32 file APIs accept them too. */ + std::string rel(ROCKSDB_BACKUP_DIR); + rel.push_back('/'); + rel.append(name); +#ifndef _WIN32 + int src= openat(checkpoint_fd, name.c_str(), O_RDONLY); + if (src < 0) + { + my_error(ER_CANT_OPEN_FILE, MYF(0), name.c_str(), errno); + return 1; + } + int ret_val= 0; + if (sink.stream == sink.NO_STREAM) + { + /* Directory target: copy the file into /#rocksdb/. */ + int dst= openat(target.fd, rel.c_str(), O_CREAT | O_EXCL | O_WRONLY, 0666); + if (dst < 0) + { + my_error(ER_CANT_CREATE_FILE, MYF(0), rel.c_str(), errno); + ret_val= 1; + } + else + { + ret_val= copy_entire_file(src, dst); + if (ret_val | close(dst)) + { + my_error(ER_ERROR_ON_WRITE, MYF(0), rel.c_str(), errno); + ret_val= 1; + } + } + } + else + { + /* Stream target. Checkpoint SST files are immutable, so the + Linux sendfile(2) fast path (backup_stream_append_async) is safe here, + unlike live data files, there is no concurrent writer to race. */ + uint64_t end= uint64_t(lseek(src, 0, SEEK_END)); + if (backup_stream_start(sink.stream, rel.c_str(), 0644, end, nullptr, 0) || + backup_stream_append_async(src, sink.stream, 0, end)) + { + my_error(ER_ERROR_ON_WRITE, MYF(0), rel.c_str(), errno); + ret_val= 1; + } + else if (size_t pad= size_t(end) & 511) + if (backup_stream_write(sink.stream, zerobuf, 512 - pad)) + { + my_error(ER_ERROR_ON_WRITE, MYF(0), rel.c_str(), errno); + ret_val= 1; + } + } + std::ignore= close(src); + return ret_val; +#else + std::string src_path(checkpoint_dir); + src_path.push_back('/'); + src_path.append(name); + if (sink.stream == sink.NO_STREAM) + { + /* Directory target */ + std::string dest_path(target.path); + dest_path.push_back('/'); + dest_path.append(rel); + if (!CopyFileEx(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; + } + /* Stream target */ + HANDLE dst= sink.stream; + HANDLE src= CreateFile(src_path.c_str(), GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + my_win_file_secattr(), OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, nullptr); + if (src == INVALID_HANDLE_VALUE) + { + my_osmaperr(GetLastError()); + my_error(ER_FILE_NOT_FOUND, MYF(0), src_path.c_str(), errno); + return 1; + } + LARGE_INTEGER li; + int ret_val= 0; + if (!GetFileSizeEx(src, &li) || + backup_stream_start(dst, rel.c_str(), 0644, li.QuadPart, nullptr, 0) || + backup_stream_append_plain(src, dst, 0, li.QuadPart)) + { + my_osmaperr(GetLastError()); + my_error(ER_ERROR_ON_WRITE, MYF(0), rel.c_str(), errno); + ret_val= 1; + } + else if (size_t pad= size_t(li.LowPart) & 511) + if (backup_stream_write(dst, zerobuf, 512 - pad)) + { + my_error(ER_ERROR_ON_WRITE, MYF(0), rel.c_str(), errno); + ret_val= 1; + } + CloseHandle(src); + return ret_val; +#endif +} + +/** During BACKUP_PHASE_NO_COMMIT phase, create #rocksdb destination subdirectory +in start operation */ +void *rocksdb_backup_start(THD *thd MY_ATTRIBUTE((__unused__)), + const backup_target *target, + backup_phase phase, const backup_sink *sink) noexcept +{ + switch (phase) { + case BACKUP_PHASE_PREPARE_START: + /* Called with no locks held and sink/target == nullptr, before the phase + loop. Nothing to pre-allocate; the checkpoint is frozen at NO_COMMIT. */ + return nullptr; + case BACKUP_PHASE_NO_COMMIT: + { + assert(!sink->ha_data); + RocksDB_backup *bk= new (std::nothrow) RocksDB_backup; + if (!bk) + { + my_error(ER_OUTOFMEMORY, MYF(0), (int) sizeof(RocksDB_backup)); + return reinterpret_cast(-1); + } + if (bk->create_checkpoint() || bk->scan_checkpoint()) + { + bk->remove_checkpoint(); // drop it if create succeeded + delete bk; + return reinterpret_cast(-1); + } + return bk; + } + case BACKUP_PHASE_FINISH: + { + RocksDB_backup *bk= static_cast(sink->ha_data); + /* Create the destination subdirectory single-threaded, before the step + workers fan out, so no worker can race a half-created directory. */ + if (bk && bk->ensure_dest_subdir(*target)) + return reinterpret_cast(-1); + return bk; + } + default: + return sink->ha_data; + } +} + +/** During step process of BACKUP PHASE FINISH phase, copy +the immutable checkpoint files fanned out across N threads +@retval -1 in case of failure +@retval 0 on success */ +int rocksdb_backup_step(THD *thd MY_ATTRIBUTE((__unused__)), + const backup_target *target, + backup_phase phase, const backup_sink *sink) noexcept +{ + RocksDB_backup *bk= static_cast(sink->ha_data); + if (!bk) + return 0; + switch (phase) { + case BACKUP_PHASE_FINISH: + { + size_t i= bk->claim_next(); + if (i >= bk->file_count()) + return 0; + if (bk->copy_one(*target, *sink, bk->file_at(i))) + return -1; + return int(bk->file_count() - i); + } + default: + return 0; + } +} + +/** During end process of BACKUP PHASE FINISH phase, remove +the checkpoint and freeing the context +@retval 0 on success */ +int rocksdb_backup_end(THD *thd MY_ATTRIBUTE((__unused__)), + const backup_target *target MY_ATTRIBUTE((__unused__)), + backup_phase phase, const backup_sink *sink) noexcept +{ + RocksDB_backup *bk= static_cast(sink->ha_data); + if (!bk) + return 0; + if (phase == BACKUP_PHASE_FINISH) + { + bk->remove_checkpoint(); + delete bk; + } + return 0; +} + +} // namespace myrocks diff --git a/storage/rocksdb/rdb_backup_server.h b/storage/rocksdb/rdb_backup_server.h new file mode 100644 index 0000000000000..3f55ca09cb317 --- /dev/null +++ b/storage/rocksdb/rdb_backup_server.h @@ -0,0 +1,73 @@ +/* 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 the RocksDB (MyRocks) engine. */ + +#include +#include +#include + +/* Temporary server-side checkpoint directory, created inside +rocksdb_datadir so that the RocksDB checkpoint can hardlink +the SST files instead of copying them. Kept identical to the +path used by the ROCKSDB_REMOVE_MARIABACKUP_CHECKPOINT system +variable so create and remove agree. */ +#define ROCKSDB_CHECKPOINT_SUBDIR "mariabackup-checkpoint" + +/* Destination subdirectory inside the backup target +that receives the RocksDB checkpoint files. The default +rocksdb_datadir ("./#rocksdb") makes restore transparent: +on restore the files land at /#rocksdb. */ +#define ROCKSDB_BACKUP_DIR "#rocksdb" + +namespace myrocks { + +std::string rocksdb_backup_checkpoint_dir(); + +/** Start of a BACKUP SERVER phase. +@param thd current session +@param target backup target +@param phase BACKUP_PHASE_START, ... (not BACKUP_PHASE_ABORT) +@param sink worker context +@return backup context object to be attached to sink, or nullptr +@retval -1 on failure */ +void *rocksdb_backup_start(THD *thd, const backup_target *target, + backup_phase phase, const backup_sink *sink) + noexcept; + +/** Process a file that was collected in rocksdb_backup_start(). +@param thd current session +@param target backup target +@param phase last phase on which backup_start() was successfully invoked +@param sink worker context +@return number of files remaining, or negative on error +@retval 0 on completion */ +int rocksdb_backup_step(THD *thd, const backup_target *target, + backup_phase phase, const backup_sink *sink) noexcept; + +/** Finish a phase, once all calls for the current phase are completed. +@param thd current session +@param target backup target +@param phase last phase on which backup_start() was successfully + invoked, or BACKUP_PHASE_ABORT or BACKUP_PHASE_FINISH +@param sink worker context +@return error code +@retval 0 on success */ +int rocksdb_backup_end(THD *thd, const backup_target *target, + backup_phase phase, const backup_sink *sink) noexcept; + +} // namespace myrocks