Skip to content

MDEV-39091 BACKUP SERVER of ENGINE=RocksDB to the local file system - #5388

Open
Thirunarayanan wants to merge 5 commits into
MDEV-14992from
MDEV-39091
Open

MDEV-39091 BACKUP SERVER of ENGINE=RocksDB to the local file system#5388
Thirunarayanan wants to merge 5 commits into
MDEV-14992from
MDEV-39091

Conversation

@Thirunarayanan

Copy link
Copy Markdown
Member

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.

@Thirunarayanan
Thirunarayanan requested a review from dr-m July 15, 2026 10:32
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements BACKUP SERVER support for the RocksDB (MyRocks) storage engine, introducing new backup hooks to create consistent checkpoints and copy or stream the immutable checkpoint files. The review feedback highlights several critical issues: a potential security and correctness risk where an empty datadir could write to the root directory, potential backup corruption due to an unrestored file offset after calling lseek, a resource leak on backup abort because cleanup is only executed during the finish phase, and a potential crash on Windows if target.path is null.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread storage/rocksdb/rdb_backup_server.cc Outdated
Comment on lines +47 to +48
const char *datadir= rdb_get_datadir();
std::string dir(datadir ? datadir : "");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If rdb_get_datadir() returns nullptr or an empty string, dir will resolve to / after appending the slash, causing the checkpoint directory to be created at /mariabackup-checkpoint. This is a security and correctness risk as it attempts to write to the root of the filesystem. Defaulting to the current directory . when the datadir is empty or null is much safer.

  const char *datadir= rdb_get_datadir();
  if (!datadir || !*datadir)
    datadir= ".";
  std::string dir(datadir);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rocksdb_datadir can never be nullptr. Elsewhere, it is assumed that it cannot refer to an empty string either, even though this might not be enforced:

std::string rdb_corruption_marker_file_name() {
  std::string ret(rocksdb_datadir);
  ret.append("/ROCKSDB_CORRUPTED");
  return ret;
}

Comment thread storage/rocksdb/rdb_backup_server.cc Outdated
Comment on lines +249 to +251
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))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using lseek(src, 0, SEEK_END) to determine the file size leaves the file offset at the end of the file. If the underlying implementation of backup_stream_append_async (or any fallback path) relies on the current file offset (e.g., using read() instead of pread()), it will read 0 bytes, leading to silent backup corruption. The file offset must be restored to the beginning of the file using lseek(src, 0, SEEK_SET) before proceeding.

    uint64_t end= uint64_t(lseek(src, 0, SEEK_END));
    if (lseek(src, 0, SEEK_SET) == (off_t)-1 ||
        backup_stream_start(sink.stream, rel.c_str(), 0644, end, nullptr, 0) ||
        backup_stream_append_async(src, sink.stream, 0, end))

Comment thread storage/rocksdb/rdb_backup_server.cc Outdated
Comment on lines +395 to +399
if (phase == BACKUP_PHASE_FINISH)
{
bk->remove_checkpoint();
delete bk;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If the backup is aborted, rocksdb_backup_end is called with BACKUP_PHASE_ABORT. Currently, the cleanup logic (removing the checkpoint and deleting the bk context) is only executed when phase == BACKUP_PHASE_FINISH. This causes a memory leak and leaves stale checkpoint directories on disk when a backup fails or is aborted. The cleanup should be performed on both BACKUP_PHASE_FINISH and BACKUP_PHASE_ABORT.

  if (phase == BACKUP_PHASE_FINISH || phase == BACKUP_PHASE_ABORT)
  {
    bk->remove_checkpoint();
    delete bk;
  }

Comment thread storage/rocksdb/rdb_backup_server.cc Outdated

@dr-m dr-m left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks mostly OK to me. The interesting part is RocksDB_backup::copy_one(), which copies or streams a file.

We are lacking some test coverage.

Comment on lines +46 to +47
--replace_result $script rstream.bat
eval BACKUP SERVER WITH 2 CONCURRENT '$script';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is failing on Windows:

backup.backup_rocksdb                    w8 [ fail ]
        Test ended at 2026-07-15 12:53:53
CURRENT_TEST: backup.backup_rocksdb
mysqltest: At line 47: query 'BACKUP SERVER WITH 2 CONCURRENT '$script'' failed: ER_IO_WRITE_ERROR (1811): IO Write error: (2, No such file or directory) BACKUP SERVER

Curiously, for the CMAKE_BUILD_TYPE=Debug build on Windows, this test is being skipped:

backup.backup_rocksdb                    w15 [ skipped ]  Test requires MyRocks engine

Comment thread mysql-test/suite/backup/backup_rocksdb.test Outdated
Comment thread mysql-test/suite/backup/backup_rocksdb.test
Comment thread mysql-test/suite/backup/suite.pm Outdated
Comment on lines +15 to +16
$skip{'backup_stream.test'} = 'needs cat,tar' unless $have_cat && $have_tar;
$skip{'backup_rocksdb.test'} = 'needs cat,tar' unless $have_cat && $have_tar;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the streaming tools are missing, we’re skipping all test coverage, even though it is technically possible to cover backup to a mounted file system.

Comment thread storage/rocksdb/ha_rocksdb.cc Outdated
Comment thread storage/rocksdb/rdb_backup_server.cc Outdated
Comment thread storage/rocksdb/rdb_backup_server.cc
Comment thread storage/rocksdb/rdb_backup_server.cc Outdated
Comment thread storage/rocksdb/rdb_backup_server.cc Outdated
Comment thread storage/rocksdb/rdb_backup_server.cc Outdated
file_name_t::page0_lsn: Keep track of the last applied
recv_sys_t::parse_page0() so that a multi-batch recovery
will not reset the file to a smaller size.

Reviewed by: Thirunarayanan Balathandayuthapani

(cherry picked from commit 8f00e6c)
@Thirunarayanan
Thirunarayanan changed the base branch from MDEV-39061 to MDEV-14992 August 25, 2026 14:15
@Thirunarayanan
Thirunarayanan force-pushed the MDEV-39091 branch 2 times, most recently from 2891f61 to aba1a16 Compare August 25, 2026 15:06
@Thirunarayanan
Thirunarayanan requested a review from dr-m August 26, 2026 00:40

@dr-m dr-m left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to reduce the peak memory usage.

Comment thread storage/rocksdb/rdb_backup_server.cc Outdated
log_t::set_archive(archive=false): Ensure that both the latest checkpoint
and the latest log record (which has possibly not been written out yet)
will carry the log_sys.get_sequence_bit(lsn)==1, to guarantee a successful
recovery after the switch to the innodb_log_archive=OFF format.

Tested by: Matthias Leich
Reviewed by: Thirunarayanan Balathandayuthapani

(cherry picked from commit 1637316)
log_t::set_archive(): Prevent a crash in log_t::header_rewrite()
by refusing the operation if the log is read-only.
The following SQL statements will be introduced:

BACKUP SERVER TO '/path/to/directory' [ 1 CONCURRENT ];
BACKUP SERVER WITH [ 1 CONCURRENT ] 'command';

In place of the 1, any positive number of threads may be specified.
For the first variant, '/path/to' must exist and '/path/to/directory'
must be compatible with secure_file_priv and not exist; that is where
the backup will be written to.

For the second variant, 'command' must be the name of a script or
command that will be executed in a child process. The standard input
of that command will be in a format that is compatible with
GNU tar --format=oldgnu (and also BSD tar variants that are also part of
Microsoft Windows and Apple macOS). The command is expected to optionally
compress and encrypt the stream and redirect it to a file on a local or
a remote server. The BACKUP SERVER WITH will append an additional argument,
a positive base-ten number in ASCII, starting with 1, to identify the
current thread. In this way, each concurrent stream can write a separate
file. The InnoDB write-ahead log will be streamed near the end, because
the stream format requires the file sizes to be declared upfront.

TODO: implement the following:
In BACKUP SERVER TO ... 2 CONCURRENT or more, one thread will be
responsible for copying the InnoDB write-ahead log from the beginning
of the backup. Other files will be copied by other threads.

Note: In single-threaded BACKUP SERVER TO, the parameter
innodb_log_recovery_start that is written out to backup.cnf is
STRICTLY NECESSARY TO AVOID CORRUPTION during recovery! By default,
InnoDB crash recovery starts from the latest available log
checkpoint. However, for restoring a backup, recovery must start from
the checkpoint that was the latest when the backup was
started. Starting recovery from a possible later checkpoint will
result in a corrupted database!

The backup or the first stream will contain a file backup.cnf, which
includes parameters needed for restoring the backup. Currently,
these are innodb_log_recovery_start and innodb_log_recovery_target.
If innodb_log_recovery_target>0, InnoDB will be in read-only mode,
not allowing any writes to persistent files other than via the log
application.

To restore a streaming backup made with BACKUP SERVER WITH, an empty
directory needs to be created and all streams be extracted there using
the standard tar utility of the operating system, optionally after
undoing any encryption or compression that had been added by the
backup command. Then, the backup is prepared or MariaDB server started
up on the extracted directory, similar to as if the BACKUP SERVER TO
statement had been used.

The following will be implemented separately:

MDEV-39061 mariadb-backup compatible wrapper script for BACKUP SERVER
MDEV-40163 Partial backup and restore
MDEV-39091 Back up ENGINE=RocksDB
MDEV-40333 Concurrent DDL for Aria tables in BACKUP SERVER

The implementation introduces a basic multi-threaded driver
Sql_cmd_backup, storage engine interfaces, and basic copying of the
storage engines InnoDB, Aria, MyISAM, MERGE (MyISAM), Archive, CSV.

backup_target: A structured data type to represent a target directory.
On Microsoft Windows, we must use directory paths because there is
no variant of CopyFileEx() that would work on file handles.

backup_sink: Wraps a per-thread output stream as well as storage engine
specific context.

handlerton::backup_start(), handlerton::backup_end(): Invoked at the
start or end of a backup phase, in the thread that executes a
BACKUP SERVER statement.

handlerton::backup_step(): A backup step that can be invoked from
multiple threads concurrently, between the execution of the corresponding
handlerton::backup_start() and handlerton::backup_end() of the same
phase.

copy_entire_file(): A file copying service for POSIX systems.

copy_mmap(): A zero-copy alternative to backup::copy(), to copy from a
memory-mapped buffer.

copy_file_range_try(): A wrapper for Linux copy_file_range(2), which
may fail with EOPNOTSUPP or EXDEV and thus require a fallback to
copy_mmap() or backup::copy().

backup::copy(): A partial or sparse file-copying service.  On other
platforms than FreeBSD or Microsoft Windows, there are shortcut
alternatives to this. Note: On Linux we never invoke sendfile(2) for
copying between files, because can be much slower than the
alternatives.

backup_stream_append_plain(): A wrapper of backup::append(), which is
the streaming equivalent of backup::copy().

backup_stream_zeropad(): Zero-pad the last tar block if needed.

backup_stream_append_async(): A variant of backup_stream_append_plain()
where the source file region is guaranteed to be immutable after the
call returns. Zero-copy mmap(2) or Linux sendfile(2) are inherently
risky for copying data files that may be modified in place, because it
could introduce a race condition between a page write that runs
concurrently with a child process that is reading the data from the
pipe.

backup::append(): On systems where we can determine the size of the
pipe buffer, invoke backup_stream_append_async() for the initial
write, and pread_write() for the last part, to guarantee that the data
written by the zero-copy shortcut will have been consumed before the
call returns and the caller is able to resume writes to the source
region.

pread_write(): On 64-bit systems, allocate a buffer of up to 1 MiB.
This is the "slow path" of copying or streaming files.

struct Aria_backup: Context for multi-threaded backup,
comprising directory handles, a mutex and enum Aria_backup_status.

aria_backup_start(): Prepare the context for aria_backup_step().
Most files are copied in BACKUP_PHASE_NO_DDL after flush_tables(thd,
FLUSH_NON_TRANS_TABLES) has been invoked. All ENGINE=Aria files
(including TRANSACTIONAL=0) are copied in
BACKUP_PHASE_NO_COMMIT. Thanks to Andrzej Jarząbek for writing
test cases and suggesting this logic.

aria_backup_step(): Copy one non-ACID file. Acquires
Aria_backup::mutex, traverses directories to construct one file name,
releases the mutex, and copies the file if one was found.

aria_backup_data(): Copy one data file. On Microsoft Windows, this
assumes that the current directory is the datadir. This assumption
would not hold in the Embedded Server library, which is not supported
on Microsoft Windows.

aria_backup_log(): Copy one ENGINE=Aria log file.

aria_backup_end(): Finish a copying phase and clean up the context.

InnoDB_backup::init(): Wait for a possible previous BACKUP SERVER
operation to reach the very end of InnoDB_backup::context::cleanup()
so that the context can be safely reused.

InnoDB_backup::queue: Collection of tablespace IDs and payload sizes
at the start of the backup, and the log_sys.first_lsn of log files
that have to be included in the backup. If any data file is created or
extended while the backup is executing, we must have the corresponding
write-ahead-log entries that we are copying since the latest
checkpoint that was completed when the backup started. If any
tablespaces are deleted during the backup, we may or may not copy
them, and the application of a FILE_DELETE record will remove them.
Similarly, applying FILE_RENAME or FILE_CREATE records will rename or
create files during recovery as needed.

log_sys.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.

fil_system.have_all_spaces: Whether all tablespace metadata is guaranteed
to be known. To speed up startup, InnoDB does not normally open
all tablespace files.

fil_space_t::create_lsn: Change to Atomic_relaxed and use this to
indicate tablespace creation LSN, in addition to indicate undo
tablespace rebuild LSN.

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.

buf_page_t::write_fix_try(), buf_page_t::write_unfix_try(): Try to set
or unset a fake "write fix" on a page, to prevent concurrent flush()
during a backup batch. The atomic operations may run concurrently with
set_reinit() and set_freed(). The fake "write fix" does not prevent
any concurrent read or write of the page data in the buffer pool; it
only blocks writes to the underlying data file.

buf_page_t::flush(): Atomically test and set write fix, and
skip the operation if the fake "write fix" was set.

buf_page_t::set_freed(), buf_page_t::set_reinit(): Employ a
compare-and-exchange loop to accommodate for the "write fix".

innodb_backup_batch_wait(): Look up any pages that we are about to
back up. For any dirty pages, invoke buf_page_t::write_fix_try() to
try to set a fake "write fix" lock-free. If the page is currently
write-fixed between buf_page_t::flush() and
buf_page_t::write_complete(), acquire and release a page U-latch to
wait for the conflicting write to complete.

InnoDB_backup::backup_batch_start(),
InnoDB_backup::backup_batch_stop(): Adjust fil_space_t::backup_end and
fake "write fix" of dirty pages to protect the copying of a range of
pages from the underlying file.

InnoDB_backup::commit(): Enqueue the remaining log to be copied.

InnoDB_backup::checkpoint_complete(): If backup is running and
commit() has not been called, add each completed innodb_archive_log=ON
file to the queue. Else, skip or delete, as appropriate.

log_t::backup_start(): If we were running with innodb_log_archive=ON,
ensure that the latest file is a valid recovery starting point.
That is, wait for the latest log checkpoint to be within the file.

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().

fil_ibd_create(): Set fil_space_t::create_lsn after the file has been
created.

dict_load_tablespaces(): Determine the size of each file if
upgrade==true. Backup depends on that.

buf_dblwr_t::begin(), buf_dblwr_t::end(), buf_dblwr_t::size():
Accessors to allow BACKUP SERVER to skip the contents of the
doublewrite buffer in the system tablespace. It is only useful for
crash recovery in case a data page had been incompletely written by
the time the server was killed. If the server is killed during a
backup, the backup will be incomplete and unusable anyway.
Furthermore, the page range locking makes page writes and backup
mutually exclusive.
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 for a consistent snapshot; 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 <datadir>/#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 directory is iterated with readdir(3) or
FindFirstFileA()/FindNextFile() and drained across N CONCURRENT
step threads: each rocksdb_backup_step() claims one file name
inside a critical section, copies the name to the stack, and
copies the file itself outside the critical section. 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 out the checkpoint creation.
rdb_remove_checkpoint(): Remove the checkpoint directory.
rocksdb_datadir: No longer static, so the backup hooks can read it.

mysql-test/suite/backup/backup_rocksdb.test: BACKUP SERVER TO a directory.
mysql-test/suite/backup/backup_rocksdb_stream.test: BACKUP SERVER WITH
2 CONCURRENT into an oldgnu tar stream.
mysql-test/suite/backup/suite.pm: Skip the streamed test without cat,tar.
Comment on lines +165 to +172
int rocksdb_backup_file(const backup_target *target,
const backup_sink *sink,
#ifndef _WIN32
int dfd,
#else
const char *dir,
#endif
const char *name)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of implementing this function, please rebase the branch to and use either variant of backup::copy_or_stream() that were implemented in 69dad73.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

4 participants