mirror of
https://github.com/MariaDB/server.git
synced 2026-10-23 18:49:29 +03:00
1108 lines
32 KiB
C++
1108 lines
32 KiB
C++
/* 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_base.h"
|
|
#include "sql_backup.h"
|
|
#include "sql_backup_interface.h"
|
|
#include "sql_parse.h"
|
|
#include "my_atomic_wrapper.h"
|
|
#include "tpool.h"
|
|
#include "aligned.h"
|
|
|
|
static bool backup_execute(THD *thd, const char *target, const char *command,
|
|
int threads);
|
|
|
|
/**
|
|
@brief BACKUP SERVER command
|
|
|
|
BACKUP SERVER copies the storage of a running server optionally
|
|
using multiple CONCURRENT threads, each one writing to the target
|
|
directory or its own tar stream.
|
|
|
|
BACKUP SERVER TO '/path/to/directory' [ 1 CONCURRENT ];
|
|
BACKUP SERVER WITH [ 1 CONCURRENT ] 'command';
|
|
|
|
The operation is divided into multiple phases; @see backup_phase
|
|
|
|
Each phase is divided into "start" and "end", which are invoked
|
|
by this thread, and a number of "step" in between that may be
|
|
invoked by multiple CONCURRENT threads.
|
|
@see handlerton::backup_start
|
|
@see handlerton::backup_step
|
|
@see handlerton::backup_end
|
|
|
|
Most phases are tied with BACKUP STAGE locks; the actual first work phase:
|
|
@see BACKUP_PHASE_START
|
|
@see MDL_BACKUP_START
|
|
|
|
The snapshot for transactional storage engines is determined in:
|
|
@see BACKUP_PHASE_NO_COMMIT
|
|
@see MDL_BACKUP_WAIT_COMMIT
|
|
|
|
Some backup_phase for preparation and clean-up are executed while
|
|
not holding any MDL.
|
|
|
|
@param thd client connection
|
|
@return whether the operation failed
|
|
*/
|
|
bool Sql_cmd_backup::execute(THD *thd)
|
|
{
|
|
return backup_execute(thd, target, command, threads);
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
using tpool::pread;
|
|
using tpool::pwrite;
|
|
#else
|
|
# include <sys/mman.h>
|
|
/**
|
|
Copy a file using a memory mapping.
|
|
@tparam stream true=write to a stream, false=pwrite to a file
|
|
@param p map to 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
|
|
*/
|
|
template<bool stream>
|
|
static ssize_t mmap_copy(const void *p, int out_fd, uint64_t o, uint64_t end)
|
|
{
|
|
size_t c= size_t(end - o);
|
|
ssize_t ret;
|
|
for (const char *b= static_cast<const char*>(p) + o;;
|
|
b+= ret, o+= uint64_t(ret))
|
|
{
|
|
const size_t size{std::min(c, size_t(INT_MAX >> 20 << 20))};
|
|
if (stream)
|
|
if ((ret= backup_stream_write(out_fd, b, size)))
|
|
break;
|
|
ret= stream ? size : pwrite(out_fd, b, size, off_t(o));
|
|
if (ret < 0)
|
|
break;
|
|
c-= ret;
|
|
if (!c)
|
|
{
|
|
ret= 0;
|
|
break;
|
|
}
|
|
if (!ret)
|
|
{
|
|
ret= -1;
|
|
break;
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
#endif
|
|
|
|
/**
|
|
Copy a file using positioned reads.
|
|
@tparam stream true=write to a stream, false=pwrite to a file
|
|
@param in_fd source file
|
|
@param out_fd destination
|
|
@param o start offset
|
|
@param end last offset (exclusive)
|
|
@return error code (non-positive)
|
|
@retval 0 on success
|
|
*/
|
|
template<bool stream>
|
|
static ssize_t pread_write(backup::handle in_fd, backup_fd out_fd,
|
|
uint64_t o, uint64_t end) noexcept
|
|
{
|
|
assert(end >= o);
|
|
uint64_t count{end - o};
|
|
#if SIZEOF_SIZE_T < 8
|
|
size_t size{65536};
|
|
#else
|
|
constexpr size_t max_size{1 << 20};
|
|
/* An integer multiple of 4096 bytes, up to max_size */
|
|
size_t size{count >= max_size ? max_size : (size_t(count) + 4095) & ~4095};
|
|
#endif
|
|
char *b= static_cast<char*>(aligned_malloc(size, 4096));
|
|
if (!b)
|
|
return -1;
|
|
ssize_t ret;
|
|
for (;; o+= ret)
|
|
{
|
|
if (count < size)
|
|
size= count;
|
|
ret= pread(in_fd, b, ssize_t(size), o);
|
|
if (ret > 0)
|
|
{
|
|
if (!stream)
|
|
ret= pwrite(out_fd, b, size_t(ret), o);
|
|
else if (backup_stream_write(out_fd, b, size_t(ret)))
|
|
{
|
|
ret= -1;
|
|
break;
|
|
}
|
|
}
|
|
if (ret < 0)
|
|
break;
|
|
count-= uint64_t(ret);
|
|
if (!count)
|
|
{
|
|
ret= 0;
|
|
break;
|
|
}
|
|
if (!ret)
|
|
{
|
|
ret= -1;
|
|
break;
|
|
}
|
|
}
|
|
aligned_free(b);
|
|
return ret;
|
|
}
|
|
|
|
/* stepwise() is used both by copy_file_range() and by sendfile() */
|
|
#if defined HAVE_COPY_FILE_RANGE || defined __linux__
|
|
using copying_step= ssize_t(int,int,size_t,off_t*);
|
|
template<copying_step step,bool nonblocking>
|
|
static ssize_t stepwise(int in_fd, int out_fd, off_t offset, off_t end)
|
|
noexcept
|
|
{
|
|
for (;;)
|
|
{
|
|
const size_t c{size_t(std::min<off_t>(end - offset, INT_MAX >> 20 << 20))};
|
|
ssize_t ret= step(in_fd, out_fd, c, &offset);
|
|
if (ret < 0)
|
|
{
|
|
if (nonblocking && errno == EAGAIN)
|
|
continue;
|
|
return ret;
|
|
}
|
|
if (offset == end)
|
|
return 0;
|
|
if (!ret)
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|
|
#ifdef HAVE_COPY_FILE_RANGE
|
|
/* 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) stepwise<copy_step,false>(src, dst, start, end)
|
|
#endif
|
|
|
|
namespace backup {
|
|
/** 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(handle src, backup_fd dst, uint64_t start, uint64_t end) noexcept
|
|
{
|
|
assert(end >= start);
|
|
#if defined __FreeBSD__ && defined HAVE_COPY_FILE_RANGE
|
|
/* On FreeBSD, copy_file_range() without flags just works */
|
|
return int(cfr(src, dst, off_t(start), off_t(end)));
|
|
#else
|
|
ssize_t ret{pread_write<false>(src, dst, start, end)};
|
|
assert(ret <= 0);
|
|
return int(ret);
|
|
#endif
|
|
}
|
|
|
|
/**
|
|
Append a file snippet to stream,
|
|
after a corresponding call to backup_stream_start().
|
|
|
|
Note that tar uses 512-byte blocks. If end-start is not a multiple of
|
|
512 bytes, backup_stream_zeropad() must be invoked.
|
|
@param src source file
|
|
@param stream backup stream
|
|
@param start first offset to copy
|
|
@param end last offset to copy (exclusive)
|
|
@return error code (non-positive)
|
|
@retval 0 on success
|
|
*/
|
|
int append(handle src, backup_fd stream, uint64_t start, uint64_t end) noexcept
|
|
{
|
|
assert(stream != backup_sink::NO_STREAM);
|
|
assert(end >= start);
|
|
#if 0 /* On Linux, this turned out to be slower in our tests! */
|
|
/*
|
|
It is unsafe to zero-copy send from src unless we can guarantee
|
|
that the receiving end of the pipe has consumed everything before
|
|
our caller re-enables writes to this src region.
|
|
|
|
On Linux, even for MAP_PRIVATE, the following has been documented:
|
|
It is unspecified whether changes made to the file after the
|
|
mmap() call are visible in the mapped region.
|
|
*/
|
|
# ifdef __linux__
|
|
const int pipe_size{fcntl(stream, F_GETPIPE_SZ)};
|
|
# elif defined __FreeBSD__ || defined __APPLE__
|
|
// https://unix.stackexchange.com/questions/11946/how-big-is-the-pipe-buffer
|
|
constexpr int pipe_size{65536};
|
|
# else
|
|
constexpr int pipe_size{0};
|
|
# endif
|
|
if (pipe_size > 0)
|
|
{
|
|
/*
|
|
It should be safe to invoke the zero-copy path if we can
|
|
guarantee that the recipient will drain the buffer before the
|
|
final nonzero-copy path completes. We assume that this is
|
|
guaranteed by writing at least pipe_size bytes via the
|
|
nonzero-copy path after this shortcut.
|
|
*/
|
|
const uint64_t fast_end{start + uint64_t(pipe_size)};
|
|
if (fast_end < end)
|
|
{
|
|
if (int ret= backup_stream_append_async(src, stream, start, fast_end))
|
|
return ret;
|
|
start= fast_end;
|
|
}
|
|
}
|
|
#endif
|
|
return int(pread_write<true>(src, stream, start, end));
|
|
}
|
|
}
|
|
|
|
extern "C" int backup_stream_append_plain(backup_fd src, backup_fd stream,
|
|
uint64_t start, uint64_t end)
|
|
{
|
|
/*
|
|
On Windows, this invokes native_file_handle::native_file_handle(HANDLE).
|
|
|
|
Elsewhere, this should be a tail-call, for example JMP rel32
|
|
(0xe9) on IA-32 or AMD64. This non-inline wrapper exists only
|
|
because the basic API target is C, not C++, which is required
|
|
because of Windows.
|
|
*/
|
|
return backup::append(src, stream, start, end);
|
|
}
|
|
|
|
/**
|
|
Zero-pad a the stream to a multiple of 512 bytes.
|
|
|
|
@param stream backup stream
|
|
@param written least significant bits of the number of payload appended
|
|
@return error code (non-positive)
|
|
@retval 0 on success
|
|
*/
|
|
extern "C" int backup_stream_zeropad(backup_fd stream, size_t written)
|
|
{
|
|
static constexpr const char zerobuf[511]{'\0'};
|
|
written&= 511;
|
|
return written ? backup_stream_write(stream, zerobuf, 512 - written) : 0;
|
|
}
|
|
|
|
#ifdef copy_file_shortcut
|
|
/**
|
|
Try to copy a portion of a file via copy_file_range(2).
|
|
@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
|
|
@retval 1 if a fallback to copy_mmap() or backup::copy() is needed
|
|
*/
|
|
extern "C"
|
|
int copy_file_range_try(int src, int dst, uint64_t start, uint64_t end)
|
|
{
|
|
assert(end >= start);
|
|
ssize_t ret{cfr(src, dst, off_t(start), off_t(end))};
|
|
assert(ret <= 0);
|
|
if (ret && (errno == EOPNOTSUPP || errno == EXDEV))
|
|
return 1;
|
|
return int(ret);
|
|
}
|
|
#endif
|
|
|
|
#ifdef copy_file_mmap
|
|
/**
|
|
Copy from a memory mapping to a file.
|
|
@param map source file mapping
|
|
@param dst target to append map 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_mmap(const void *map, int dst, uint64_t start, uint64_t end)
|
|
{
|
|
return int(mmap_copy<false>(map, dst, start, end));
|
|
}
|
|
#endif
|
|
|
|
/** Append to the configuration file.
|
|
@param target backup target directory
|
|
@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(IF_WIN(const char*, int) target,
|
|
const char *config, size_t size)
|
|
{
|
|
/* FIXME: append to a pre-created configuration file */
|
|
#ifdef _WIN32
|
|
HANDLE dst;
|
|
{
|
|
std::string path{target};
|
|
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 || GetLastError() != ERROR_IO_PENDING)
|
|
break;
|
|
assert(written < DWORD(size));
|
|
config+= written;
|
|
size-= size_t(written);
|
|
}
|
|
if (CloseHandle(dst) & ok)
|
|
return 0;
|
|
}
|
|
}
|
|
#else
|
|
int dst= openat(target, "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;
|
|
}
|
|
|
|
/** backup context */
|
|
struct backup_target_phase
|
|
{
|
|
/** target directory or stream */
|
|
backup_target target;
|
|
/** current phase of backup */
|
|
backup_phase phase;
|
|
/** backup worker state (output stream) */
|
|
backup_sink sink;
|
|
/** stream object for streaming backup */
|
|
FILE *stream;
|
|
/** handlerton::backup_step return value in multi-threaded operation */
|
|
int ret;
|
|
/*
|
|
Engine-specific backup context, shared by all threads. Only
|
|
modified by the calling thread while no backup_step() is pending;
|
|
backup_step() must use find(), never operator[], because
|
|
operator[] is not safe for concurrent use.
|
|
*/
|
|
std::unordered_map<const handlerton*,void*> &ha_data;
|
|
};
|
|
|
|
/**
|
|
Inform a storage engine of an upcoming backup by invoking
|
|
handlerton::backup_start(BACKUP_PHASE_PREPARE_START) before
|
|
acquiring any locks.
|
|
@param thd current session
|
|
@param plugin storage engine
|
|
@return whether the operation failed
|
|
*/
|
|
#ifdef NOT_USED
|
|
static my_bool backup_preparation(THD *thd, plugin_ref plugin, void*) noexcept
|
|
{
|
|
const auto bs= plugin_hton(plugin)->backup_start;
|
|
return bs && bs(thd, nullptr, BACKUP_PHASE_PREPARE_START, nullptr);
|
|
}
|
|
#endif
|
|
|
|
/**
|
|
Invoke handlerton::backup_start() on a storage engine,
|
|
when there are no pending handlerton::backup_step() in any thread.
|
|
@param thd current session
|
|
@param plugin storage engine
|
|
@param arg the backup_target_phase context
|
|
@return whether the operation failed
|
|
*/
|
|
static my_bool backup_start(THD *thd, plugin_ref plugin, void *arg) noexcept
|
|
{
|
|
const handlerton *hton= plugin_hton(plugin);
|
|
backup_target_phase &t{*static_cast<backup_target_phase*>(arg)};
|
|
assert(int{t.phase} >= 0 || t.phase == BACKUP_PHASE_FINISH);
|
|
if (hton->backup_start)
|
|
{
|
|
t.sink.ha_data= t.ha_data[hton];
|
|
void *data= hton->backup_start(thd, &t.target, t.phase, &t.sink);
|
|
if (data == reinterpret_cast<void*>(-1))
|
|
return true;
|
|
assert(!t.ha_data[hton] || t.ha_data[hton] == data);
|
|
t.ha_data[hton]= data;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
Invoke handlerton::backup_end() on a storage engine,
|
|
when there are no pending handlerton::backup_step() in any thread.
|
|
@param thd current session
|
|
@param plugin storage engine
|
|
@param arg the backup_target_phase context
|
|
@return whether the operation failed
|
|
*/
|
|
static my_bool backup_end(THD *thd, plugin_ref plugin, void *arg) noexcept
|
|
{
|
|
const handlerton *hton= plugin_hton(plugin);
|
|
backup_target_phase &t{*static_cast<backup_target_phase*>(arg)};
|
|
if (hton->backup_end)
|
|
{
|
|
t.sink.ha_data= t.ha_data[hton];
|
|
return hton->backup_end(thd, &t.target, t.phase, &t.sink);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
Invoke handlerton::backup_step() on a storage engine in a thread
|
|
that may or may not be associated with a BACKUP SERVER connection,
|
|
between handlerton::backup_start() and handlerton::backup_end()
|
|
of the same backup_phase.
|
|
@param thd the BACKUP SERVER session
|
|
@param plugin storage engine
|
|
@param arg the backup_target_phase context
|
|
@return whether the operation failed
|
|
*/
|
|
static my_bool backup_step(THD *thd, plugin_ref plugin, void *arg) noexcept
|
|
{
|
|
const handlerton *hton= plugin_hton(plugin);
|
|
backup_target_phase &t{*static_cast<backup_target_phase*>(arg)};
|
|
assert(int{t.phase} >= 0 || t.phase == BACKUP_PHASE_FINISH);
|
|
int res= 0;
|
|
if (hton->backup_step)
|
|
{
|
|
const auto it= t.ha_data.find(hton);
|
|
t.sink.ha_data= it == t.ha_data.end() ? nullptr : it->second;
|
|
while ((res= hton->backup_step(thd, &t.target, t.phase, &t.sink)))
|
|
if (res < 0)
|
|
break;
|
|
}
|
|
return res != 0;
|
|
}
|
|
|
|
/**
|
|
Initialize a thread that was created by the thread pool.
|
|
|
|
Any thread that uses mysys functions must call my_thread_init()
|
|
before it does so; my_errno and other mysys variables are stored in
|
|
a thread local variable that my_thread_init() allocates.
|
|
*/
|
|
static void backup_thread_init() noexcept
|
|
{
|
|
PSI_thread *psi;
|
|
my_thread_init();
|
|
my_thread_set_name("backup_worker");
|
|
/* Make the thread visible in performance_schema.threads */
|
|
psi= PSI_CALL_new_thread(key_thread_backup_worker, NULL, 0);
|
|
PSI_CALL_set_thread_os_id(psi);
|
|
PSI_CALL_set_thread(psi);
|
|
}
|
|
|
|
|
|
/** Clean up a thread that was initialized by backup_thread_init() */
|
|
static void backup_thread_end() noexcept
|
|
{
|
|
PSI_CALL_delete_current_thread();
|
|
my_thread_end();
|
|
}
|
|
|
|
|
|
/** Number of background tasks executing backup_step_callback */
|
|
static Atomic_counter<int> 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<backup_target_phase*>(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<tpool::task*>(alloca(threads * sizeof(*tasks)));
|
|
backup_step_callback_pending= threads - 1;
|
|
for (int n= threads; --n; )
|
|
{
|
|
target_phase[n].phase= target_phase->phase;
|
|
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;
|
|
}
|
|
|
|
/*
|
|
Map from backup_phase to backup_stages, starting from
|
|
BACKUP_PHASE_PREPARE_START
|
|
*/
|
|
|
|
enum backup_stages backup_stage_map[]=
|
|
{
|
|
BACKUP_NONE, /* BACKUP_PHASE_PREPARE_START */
|
|
BACKUP_NONE, /* BACKUP_PHASE_START */
|
|
BACKUP_FLUSH, /* BACKUP_PHASE_NO_BEGIN_NON_TRANS */
|
|
/* Note that WAIT_FOR_FLUSH also blocks DDL */
|
|
BACKUP_WAIT_FOR_FLUSH, /* BACKUP_PHASE_NO_DML_NON_TRANS */
|
|
BACKUP_NONE, /* BACKUP_PHASE_NO_DDL */
|
|
BACKUP_LOCK_COMMIT /* BACKUP_PHASE_NO_COMMIT */
|
|
};
|
|
|
|
|
|
/**
|
|
BACKUP SERVER driver
|
|
|
|
@param thd client connection
|
|
@param target BACKUP SERVER TO directory to be created
|
|
@param command BACKUP SERVER WITH command
|
|
@param threads number of CONCURRENT threads to employ
|
|
@return whether the operation failed
|
|
*/
|
|
static bool backup_execute(THD *thd, const char *target, const char *command,
|
|
int threads)
|
|
{
|
|
assert(!!target == !command);
|
|
assert(threads > 0);
|
|
tpool::thread_pool *tp= nullptr;
|
|
bool fail= 0;
|
|
int phase;
|
|
|
|
if (!target)
|
|
{
|
|
/*
|
|
Disallow path separators and the quote character.
|
|
*/
|
|
if (false ||
|
|
#ifndef _WIN32
|
|
strchr(command, '\'') /* quote */ ||
|
|
#else
|
|
strchr(command, '"') /* quote */ ||
|
|
strchr(command, '\\') /* special inside quotes */ ||
|
|
#endif
|
|
strchr(command, '/') /* disallow path separator */)
|
|
{
|
|
my_error(ER_WRONG_ARGUMENTS, MYF(0), "BACKUP SERVER WITH");
|
|
return true;
|
|
}
|
|
}
|
|
else if (error_if_data_home_dir(target, "BACKUP SERVER TO"))
|
|
return true;
|
|
|
|
/* Initials backup and calls plugin_prepare_for_backup() for all engines */
|
|
if (run_backup_stage(thd, BACKUP_START))
|
|
return true;
|
|
|
|
#ifdef OLD_CODE
|
|
fail= plugin_foreach_with_mask(thd, backup_preparation,
|
|
MYSQL_STORAGE_ENGINE_PLUGIN,
|
|
PLUGIN_IS_DELETED|PLUGIN_IS_READY,
|
|
nullptr);
|
|
if (fail)
|
|
{
|
|
my_error(ER_OUT_OF_RESOURCES, MYF(0));
|
|
goto err_exit;
|
|
}
|
|
#endif
|
|
|
|
std::unordered_map<const handlerton*,void*> ha_data{};
|
|
backup_target_phase *target_phase= static_cast<backup_target_phase*>
|
|
(alloca(threads * sizeof *target_phase));
|
|
if (threads > 1)
|
|
{
|
|
if (!(tp= tpool::create_thread_pool_generic()))
|
|
goto oor;
|
|
tp->set_thread_callbacks(backup_thread_init, backup_thread_end);
|
|
}
|
|
|
|
if (command)
|
|
{
|
|
char cmd[1024];
|
|
for (int t{threads}; t; )
|
|
{
|
|
#ifndef _WIN32
|
|
/*
|
|
popen(3) will prepend "/bin/sh -c" which preserves the literal
|
|
meaning of all characters enclosed in single quotes.
|
|
*/
|
|
constexpr const char cmd_fmt[]= "'mariadb-backup-%s' %d";
|
|
#else
|
|
/*
|
|
my_win_popen() will prepend "cmd.exe /c ";
|
|
add double quotes around its first argument
|
|
*/
|
|
constexpr const char cmd_fmt[]= "\"mariadb-backup-%s\" %d";
|
|
#endif
|
|
if (size_t(snprintf(cmd, sizeof cmd, cmd_fmt, command, t)) >= sizeof cmd)
|
|
goto oor;
|
|
FILE *f= my_popen(cmd, "w");
|
|
if (!f)
|
|
{
|
|
while (t < threads)
|
|
my_pclose(target_phase[t++].stream);
|
|
goto oor;
|
|
}
|
|
#ifdef _WIN32
|
|
HANDLE sink= (HANDLE) _get_osfhandle(_fileno(f));
|
|
#else
|
|
int sink= fileno(f);
|
|
# ifdef __linux__
|
|
/* Request a larger pipe buffer; the default appears to be 64 KiB. */
|
|
fcntl(sink, F_SETPIPE_SZ, 1 << 20/* 1 MiB */);
|
|
# endif
|
|
#endif
|
|
new (&target_phase[--t])
|
|
backup_target_phase{backup_target{nullptr, -1},
|
|
BACKUP_PHASE_START, backup_sink{sink, nullptr}, f, 0, ha_data};
|
|
}
|
|
}
|
|
else if (my_mkdir(target, 0755, MYF(MY_WME)))
|
|
goto err_exit;
|
|
else
|
|
{
|
|
#ifdef _WIN32
|
|
const int dir= -1; /* The path is used on Windows */
|
|
#else
|
|
const int 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; )
|
|
{
|
|
new (&target_phase[--t])
|
|
backup_target_phase{backup_target{target, dir},
|
|
BACKUP_PHASE_START,
|
|
backup_sink{backup_sink::NO_STREAM, nullptr}, nullptr, 0, ha_data};
|
|
}
|
|
}
|
|
|
|
assert(!fail);
|
|
|
|
for (phase= (int) BACKUP_PHASE_START ;
|
|
phase <= (int) BACKUP_PHASE_NO_COMMIT;
|
|
phase++)
|
|
{
|
|
if (backup_stage_map[phase] != BACKUP_NONE &&
|
|
run_backup_stage(thd, backup_stage_map[phase]))
|
|
{
|
|
fail=1;
|
|
break;
|
|
}
|
|
|
|
target_phase->phase= (backup_phase) phase;
|
|
fail= plugin_foreach_with_mask(thd, backup_start,
|
|
MYSQL_STORAGE_ENGINE_PLUGIN,
|
|
PLUGIN_IS_DELETED|PLUGIN_IS_READY,
|
|
target_phase);
|
|
if (fail)
|
|
break;
|
|
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,
|
|
target_phase);
|
|
if (fail)
|
|
break;
|
|
}
|
|
|
|
/* Would probaby be better if plugin_end_backup call would do the following */
|
|
|
|
if (!fail)
|
|
{
|
|
target_phase->phase= BACKUP_PHASE_FINISH;
|
|
fail= plugin_foreach_with_mask(thd, backup_start,
|
|
MYSQL_STORAGE_ENGINE_PLUGIN,
|
|
PLUGIN_IS_DELETED|PLUGIN_IS_READY,
|
|
target_phase) ||
|
|
backup_steps(thd, target_phase, threads, tp);
|
|
}
|
|
else
|
|
{
|
|
target_phase->phase= BACKUP_PHASE_ABORT;
|
|
plugin_foreach_with_mask(thd, backup_end, MYSQL_STORAGE_ENGINE_PLUGIN,
|
|
PLUGIN_IS_DELETED|PLUGIN_IS_READY, target_phase);
|
|
target_phase->phase= BACKUP_PHASE_FINISH;
|
|
}
|
|
|
|
if (plugin_foreach_with_mask(thd, backup_end, MYSQL_STORAGE_ENGINE_PLUGIN,
|
|
PLUGIN_IS_DELETED|PLUGIN_IS_READY,
|
|
target_phase))
|
|
fail= 1;
|
|
|
|
/*
|
|
We cannot call BACKUP_END earlier as the Aria logs must be copied under
|
|
BLOCK_COMMIT. If we relase the commit lock, the redo log will be filled
|
|
with data that we do not want.
|
|
*/
|
|
if (run_backup_stage(thd, BACKUP_END))
|
|
fail= 1;
|
|
|
|
delete tp;
|
|
|
|
if (command)
|
|
{
|
|
int pfail{0};
|
|
for (int t= threads; t--; )
|
|
pfail|= my_pclose(target_phase[t].stream);
|
|
if (!fail && (fail= !!pfail))
|
|
my_error(ER_NET_ERROR_ON_WRITE, MYF(0));
|
|
}
|
|
#ifndef _WIN32
|
|
else
|
|
std::ignore= close(target_phase->target.fd);
|
|
#endif
|
|
|
|
if (!fail)
|
|
my_ok(thd);
|
|
return fail;
|
|
|
|
oor:
|
|
my_error(ER_OUT_OF_RESOURCES, MYF(0));
|
|
err_exit:
|
|
run_backup_stage(thd, BACKUP_END);
|
|
delete tp;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
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 mode file access mode
|
|
@param size physical size of the following block */
|
|
static void ustar_block_init(char *buf, const char *name, mode_t mode,
|
|
uint64_t size) noexcept
|
|
{
|
|
strncpy(buf, name, 100);
|
|
ustar_write_octal(buf + 100, buf + 108, uint64_t(mode));
|
|
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);
|
|
}
|
|
|
|
/**
|
|
Write data to a stream.
|
|
@param stream backup stream
|
|
@param buf source buffer
|
|
@param size length of the buffer (usually an integer multiple of 512)
|
|
@return error code (non-positive)
|
|
@retval 0 on success
|
|
*/
|
|
extern "C" int backup_stream_write(backup_fd stream, const void *buf,
|
|
size_t size)
|
|
{
|
|
#ifdef _WIN32
|
|
for (DWORD sz= DWORD(size);;)
|
|
{
|
|
DWORD wrote;
|
|
if (WriteFile(stream, buf, sz, &wrote, nullptr))
|
|
{
|
|
assert(wrote == sz);
|
|
return 0;
|
|
}
|
|
else if (GetLastError() != ERROR_IO_PENDING)
|
|
return -1;
|
|
buf= static_cast<const char*>(buf) + wrote;
|
|
sz-= wrote;
|
|
}
|
|
#else
|
|
do
|
|
{
|
|
ssize_t wrote= write(stream, buf, size);
|
|
assert(wrote <= ssize_t(size));
|
|
if (wrote < 0)
|
|
{
|
|
if (errno == EAGAIN)
|
|
continue;
|
|
return -1;
|
|
}
|
|
buf= static_cast<const char*>(buf) + wrote;
|
|
size-= size_t(wrote);
|
|
}
|
|
while (size);
|
|
#endif
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
Copy a prefix of a NUL terminated string to a buffer, NUL-padded.
|
|
@param b output buffer
|
|
@param s NUL terminated string
|
|
@param size size of buf, in bytes
|
|
*/
|
|
static inline char *ustar_zeropad(char *b, const char *s, size_t size) noexcept
|
|
{
|
|
#if defined __GNUC__ && __GNUC__ >= 8
|
|
# pragma GCC diagnostic push
|
|
# pragma GCC diagnostic ignored "-Wstringop-truncation"
|
|
#endif
|
|
return strncpy(b, s, size);
|
|
#if defined __GNUC__ && __GNUC__ >= 8
|
|
# pragma GCC diagnostic pop
|
|
#endif
|
|
}
|
|
|
|
/** Start streaming a file.
|
|
@param stream backup stream
|
|
@param name file name
|
|
@param mode file access mode
|
|
@param size physical length of the file, in bytes
|
|
@param chunks payload chunks of a sparse file, or nullptr
|
|
@param n_chunks number of chunks; 0 unless sparse file
|
|
@return error code (non-positive)
|
|
@retval 0 on success */
|
|
extern "C"
|
|
int backup_stream_start(backup_fd stream,
|
|
const char *name, mode_t mode, uint64_t size,
|
|
const struct backup_chunk *chunks, size_t n_chunks)
|
|
{
|
|
assert(stream != backup_sink::NO_STREAM);
|
|
char buf[512];
|
|
size_t s= strlen(name);
|
|
if (s > 100)
|
|
{
|
|
/* Write a block that contains the full name length,
|
|
followed by blocks that contain the full name, in
|
|
tar --format=oldgnu */
|
|
ustar_block_init(buf, "././@LongLink", 0644, s);
|
|
ustar_block_checksum(buf);
|
|
if (int err= backup_stream_write(stream, buf, sizeof buf))
|
|
return err;
|
|
const size_t whole{s & ~(sizeof buf)};
|
|
if (whole)
|
|
if (int err= backup_stream_write(stream, name, whole))
|
|
return err;
|
|
if (s - whole)
|
|
{
|
|
ustar_zeropad(buf, name + whole, sizeof buf);
|
|
if (int err= backup_stream_write(stream, buf, sizeof buf))
|
|
return err;
|
|
}
|
|
}
|
|
|
|
ustar_block_init(buf, name, mode, size);
|
|
if (!n_chunks)
|
|
buf[156]= '0';
|
|
else
|
|
{
|
|
buf[156]= 'S';
|
|
char *h= &buf[386];
|
|
if (n_chunks > 4)
|
|
return -1; // FIXME; support more chunks
|
|
for (size_t i= 0; i < n_chunks; i++, h+= 24)
|
|
{
|
|
ustar_write_dozen(h, chunks[i].offset);
|
|
ustar_write_dozen(h + 12, chunks[i].length);
|
|
}
|
|
ustar_write_dozen(&buf[0x1e3], chunks[n_chunks - 1].offset +
|
|
chunks[n_chunks - 1].length);
|
|
}
|
|
ustar_block_checksum(buf);
|
|
return backup_stream_write(stream, buf, sizeof buf);
|
|
}
|
|
|
|
/** Append to the configuration file.
|
|
@param target backup stream
|
|
@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_stream_config(backup_fd stream,
|
|
const char *config, size_t size)
|
|
{
|
|
/* FIXME: append to a pre-created configuration file */
|
|
if (int ret=
|
|
backup_stream_start(stream, "backup.cnf", 0644, size, nullptr, 0))
|
|
return ret;
|
|
char buf[512];
|
|
const size_t whole{size & ~((sizeof buf) - 1)};
|
|
if (whole)
|
|
if (int err= backup_stream_write(stream, config, whole))
|
|
return err;
|
|
if (size == whole)
|
|
return 0;
|
|
ustar_zeropad(buf, config + whole, sizeof buf);
|
|
return backup_stream_write(stream, buf, sizeof buf);
|
|
}
|
|
|
|
#ifndef _WIN32
|
|
# ifdef __linux__
|
|
# include <sys/sendfile.h>
|
|
/** 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);
|
|
}
|
|
# endif
|
|
|
|
/**
|
|
Zero-copy append an immutable file snippet to a stream.
|
|
|
|
The caller guarantees that this section of the file will remain
|
|
intact until the stream is closed. This guarantee is needed
|
|
because the receiving end of the stream pipe might delay consuming
|
|
the data, and the operating system might point the pipe buffer
|
|
to the block cache for a long time.
|
|
|
|
Note that tar uses 512-byte blocks. If end-start is not a multiple of
|
|
512 bytes, backup_stream_zeropad() must be invoked.
|
|
@param src source file
|
|
@param stream backup stream
|
|
@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_async(int src, int stream,
|
|
uint64_t start, uint64_t end)
|
|
{
|
|
assert(stream != backup_sink::NO_STREAM);
|
|
# ifdef __linux__
|
|
return int(stepwise<send_step,true>(src, stream, off_t(start), off_t(end)));
|
|
# else
|
|
# if SIZEOF_SIZE_T > 4
|
|
void *p= mmap(nullptr, size_t{end}, PROT_READ, MAP_SHARED, src, 0);
|
|
if (p != MAP_FAILED)
|
|
{
|
|
ssize_t ret= mmap_copy<true>(p, stream, start, end);
|
|
munmap(p, size_t{end});
|
|
return int(ret);
|
|
}
|
|
# endif
|
|
return int(pread_write<true>(src, stream, start, end));
|
|
# endif
|
|
}
|
|
#endif
|