#ifndef ROCKSDB_LITE
#include <cstdint>
+#include <forward_list>
#include <functional>
#include <map>
#include <string>
#include "rocksdb/status.h"
namespace ROCKSDB_NAMESPACE {
+class BackupEngineReadOnlyBase;
+class BackupEngine;
// The default DB file checksum function name.
constexpr char kDbFileChecksumFuncName[] = "FileChecksumCrc32c";
return static_cast<BackupEngineOptions::ShareFilesNaming>(l | r);
}
+// Identifying information about a backup shared file that is (or might be)
+// excluded from a backup using exclude_files_callback.
+struct BackupExcludedFileInfo {
+ explicit BackupExcludedFileInfo(const std::string& _relative_file)
+ : relative_file(_relative_file) {}
+
+ // File name and path relative to the backup dir.
+ std::string relative_file;
+};
+
+// An auxiliary structure for exclude_files_callback
+struct MaybeExcludeBackupFile {
+ explicit MaybeExcludeBackupFile(BackupExcludedFileInfo&& _info)
+ : info(std::move(_info)) {}
+
+ // Identifying information about a backup shared file that could be excluded
+ const BackupExcludedFileInfo info;
+
+ // API user sets to true if the file should be excluded from this backup
+ bool exclude_decision = false;
+};
+
struct CreateBackupOptions {
// Flush will always trigger if 2PC is enabled.
// If write-ahead logs are disabled, set flush_before_backup=true to
// Callback for reporting progress, based on callback_trigger_interval_size.
//
- // RocksDB callbacks are NOT exception-safe. A callback completing with an
- // exception can lead to undefined behavior in RocksDB, including data loss,
- // unreported corruption, deadlocks, and more.
- std::function<void()> progress_callback = []() {};
+ // An exception thrown from the callback will result in Status::Aborted from
+ // the operation.
+ std::function<void()> progress_callback = {};
+
+ // A callback that allows the API user to select files for exclusion, such
+ // as if the files are known to exist in an alternate backup directory.
+ // Only "shared" files can be excluded from backups. This is an advanced
+ // feature because the BackupEngine user is trusted to keep track of files
+ // such that the DB can be restored.
+ //
+ // Input to the callback is a [begin,end) range of sharable files live in
+ // the DB being backed up, and the callback implementation sets
+ // exclude_decision=true for files to exclude. A callback offers maximum
+ // flexibility, e.g. if remote files are unavailable at backup time but
+ // whose existence has been recorded somewhere. In case of an empty or
+ // no-op callback, all files are included in the backup .
+ //
+ // To restore the DB, RestoreOptions::alternate_dirs must be used to provide
+ // the excluded files.
+ //
+ // An exception thrown from the callback will result in Status::Aborted from
+ // the operation.
+ std::function<void(MaybeExcludeBackupFile* files_begin,
+ MaybeExcludeBackupFile* files_end)>
+ exclude_files_callback = {};
// If false, background_thread_cpu_priority is ignored.
// Otherwise, the cpu priority can be decreased,
// Default: false
bool keep_log_files;
+ // For backups that were created using exclude_files_callback, this
+ // option enables restoring those backups by providing BackupEngines on
+ // directories known to contain the required files.
+ std::forward_list<BackupEngineReadOnlyBase*> alternate_dirs;
+
explicit RestoreOptions(bool _keep_log_files = false)
: keep_log_files(_keep_log_files) {}
};
// Backup API user metadata
std::string app_metadata;
- // Backup file details, if requested with include_file_details=true
+ // Backup file details, if requested with include_file_details=true.
+ // Does not include excluded_files.
std::vector<BackupFileInfo> file_details;
+ // Identifying information about shared files that were excluded from the
+ // created backup. See exclude_files_callback and alternate_dirs.
+ // This information is only provided if include_file_details=true.
+ std::vector<BackupExcludedFileInfo> excluded_files;
+
// DB "name" (a directory in the backup_env) for opening this backup as a
// read-only DB. This should also be used as the DBOptions::wal_dir, such
// as by default setting wal_dir="". See also env_for_open.
BackupInfo() {}
- BackupInfo(BackupID _backup_id, int64_t _timestamp, uint64_t _size,
- uint32_t _number_files, const std::string& _app_metadata)
+ explicit BackupInfo(BackupID _backup_id, int64_t _timestamp, uint64_t _size,
+ uint32_t _number_files, const std::string& _app_metadata)
: backup_id(_backup_id),
timestamp(_timestamp),
size(_size),
number_fail_backup = 0;
}
- BackupStatistics(uint32_t _number_success_backup,
- uint32_t _number_fail_backup)
+ explicit BackupStatistics(uint32_t _number_success_backup,
+ uint32_t _number_fail_backup)
: number_success_backup(_number_success_backup),
number_fail_backup(_number_fail_backup) {}
// Returns Status::OK() if all checks are good
virtual IOStatus VerifyBackup(BackupID backup_id,
bool verify_with_checksum = false) const = 0;
+
+ // Internal use only
+ virtual BackupEngine* AsBackupEngine() = 0;
};
// Append-only functions of a BackupEngine. See BackupEngine comment for
#include <atomic>
#include <cinttypes>
#include <cstdlib>
+#include <exception>
#include <functional>
#include <future>
#include <limits>
#include <map>
#include <mutex>
+#include <optional>
#include <sstream>
#include <string>
#include <thread>
void GetCorruptedBackups(std::vector<BackupID>* corrupt_backup_ids) const;
- IOStatus RestoreDBFromBackup(const RestoreOptions& options,
- BackupID backup_id, const std::string& db_dir,
- const std::string& wal_dir) const;
-
- IOStatus RestoreDBFromLatestBackup(const RestoreOptions& options,
- const std::string& db_dir,
- const std::string& wal_dir) const {
- // Note: don't read latest_valid_backup_id_ outside of lock
- return RestoreDBFromBackup(options, kLatestBackupIDMarker, db_dir, wal_dir);
- }
+ IOStatus RestoreDBFromBackup(
+ const RestoreOptions& options, BackupID backup_id,
+ const std::string& db_dir, const std::string& wal_dir,
+ const std::list<const BackupEngineImpl*>& locked_restore_from_dirs) const;
IOStatus VerifyBackup(BackupID backup_id,
bool verify_with_checksum = false) const;
FileInfo& operator=(const FileInfo&) = delete;
int refs;
+ // Relative path from backup dir
const std::string filename;
const uint64_t size;
// crc32c checksum as hex. empty == unknown / unavailable
const std::string db_session_id;
Temperature temp;
- std::string GetDbFileName() {
+ std::string GetDbFileName() const {
std::string rv;
// extract the filename part
size_t slash = filename.find_last_of('/');
IOStatus AddFile(std::shared_ptr<FileInfo> file_info);
+ void AddExcludedFile(const std::string& relative_file) {
+ excluded_files_.emplace_back(relative_file);
+ }
+
IOStatus Delete(bool delete_meta = true);
bool Empty() const { return files_.empty(); }
return files_;
}
+ const std::vector<BackupExcludedFileInfo>& GetExcludedFiles() const {
+ return excluded_files_;
+ }
+
// @param abs_path_to_size Pre-fetched file sizes (bytes).
IOStatus LoadFromFile(
const std::string& backup_dir,
std::string const meta_tmp_filename_;
// files with relative paths (without "/" prefix!!)
std::vector<std::shared_ptr<FileInfo>> files_;
+ std::vector<BackupExcludedFileInfo> excluded_files_;
std::unordered_map<std::string, std::shared_ptr<FileInfo>>* file_infos_;
Env* env_;
mutable std::shared_ptr<Env> env_for_open_;
return *this;
}
- CopyOrCreateWorkItem(
- std::string _src_path, std::string _dst_path,
- const Temperature _src_temperature, const Temperature _dst_temperature,
- std::string _contents, Env* _src_env, Env* _dst_env,
- EnvOptions _src_env_options, bool _sync, RateLimiter* _rate_limiter,
- uint64_t _size_limit, Statistics* _stats,
- std::function<void()> _progress_callback = []() {},
- const std::string& _src_checksum_func_name =
- kUnknownFileChecksumFuncName,
- const std::string& _src_checksum_hex = "",
- const std::string& _db_id = "", const std::string& _db_session_id = "")
+ CopyOrCreateWorkItem(std::string _src_path, std::string _dst_path,
+ const Temperature _src_temperature,
+ const Temperature _dst_temperature,
+ std::string _contents, Env* _src_env, Env* _dst_env,
+ EnvOptions _src_env_options, bool _sync,
+ RateLimiter* _rate_limiter, uint64_t _size_limit,
+ Statistics* _stats,
+ std::function<void()> _progress_callback = {},
+ const std::string& _src_checksum_func_name =
+ kUnknownFileChecksumFuncName,
+ const std::string& _src_checksum_hex = "",
+ const std::string& _db_id = "",
+ const std::string& _db_session_id = "")
: src_path(std::move(_src_path)),
dst_path(std::move(_dst_path)),
src_temperature(_src_temperature),
dst_relative(std::move(_dst_relative)) {}
};
+ using BackupWorkItemPair =
+ std::pair<CopyOrCreateWorkItem, BackupAfterCopyOrCreateWorkItem>;
+
struct RestoreAfterCopyOrCreateWorkItem {
std::future<CopyOrCreateResult> result;
std::string from_file;
// @param contents If non-empty, the file will be created with these contents.
IOStatus AddBackupFileWorkItem(
std::unordered_set<std::string>& live_dst_paths,
- std::vector<BackupAfterCopyOrCreateWorkItem>& backup_items_to_finish,
- BackupID backup_id, bool shared, const std::string& src_dir,
+ std::deque<BackupAfterCopyOrCreateWorkItem>& backup_items_to_finish,
+ std::deque<BackupWorkItemPair>* excludable_items, BackupID backup_id,
+ bool shared, const std::string& src_dir,
const std::string& fname, // starts with "/"
const EnvOptions& src_env_options, RateLimiter* rate_limiter,
FileType file_type, uint64_t size_bytes, Statistics* stats,
uint64_t size_limit = 0, bool shared_checksum = false,
- std::function<void()> progress_callback = []() {},
+ std::function<void()> progress_callback = {},
const std::string& contents = std::string(),
const std::string& src_checksum_func_name = kUnknownFileChecksumFuncName,
const std::string& src_checksum_str = kUnknownFileChecksum,
IOStatus RestoreDBFromBackup(const RestoreOptions& options,
BackupID backup_id, const std::string& db_dir,
const std::string& wal_dir) const override {
- ReadLock lock(&mutex_);
- return impl_.RestoreDBFromBackup(options, backup_id, db_dir, wal_dir);
+ // TSAN reports a lock inversion (potential deadlock) if we acquire read
+ // locks in different orders. Assuming the implementation of RWMutex
+ // allows simultaneous read locks, there should be no deadlock, because
+ // there is no write lock involved here. Nevertheless, to appease TSAN and
+ // in case of degraded RWMutex implementation, we lock the BackupEngines
+ // including this one and those in options.alternate_dirs in a consistent
+ // order.
+ // However, locked_restore_from_dirs is kept in "search" order.
+ std::list<const BackupEngineImpl*> locked_restore_from_dirs;
+ std::vector<port::RWMutex*> mutexes;
+
+ // Add `this`
+ locked_restore_from_dirs.emplace_back(&impl_);
+ mutexes.push_back(&mutex_);
+
+ // Add alternates
+ for (BackupEngineReadOnlyBase* be : options.alternate_dirs) {
+ BackupEngineImplThreadSafe* bets =
+ static_cast_with_check<BackupEngineImplThreadSafe>(
+ be->AsBackupEngine());
+ locked_restore_from_dirs.emplace_back(&bets->impl_);
+ mutexes.push_back(&bets->mutex_);
+ }
+
+ // Acquire read locks in pointer order
+ std::sort(mutexes.begin(), mutexes.end());
+ std::vector<ReadLock> locks(mutexes.begin(), mutexes.end());
+
+ // Impl
+ return impl_.RestoreDBFromBackup(options, backup_id, db_dir, wal_dir,
+ locked_restore_from_dirs);
}
using BackupEngine::RestoreDBFromLatestBackup;
return impl_.VerifyBackup(backup_id, verify_with_checksum);
}
+ BackupEngine* AsBackupEngine() override { return this; }
+
// Not public API but needed
IOStatus Initialize() {
// No locking needed
mutable port::RWMutex mutex_;
BackupEngineImpl impl_;
};
+
} // namespace
IOStatus BackupEngine::Open(const BackupEngineOptions& options, Env* env,
return IOStatus::InvalidArgument("App metadata too large");
}
+ bool maybe_exclude_items = bool{options.exclude_files_callback};
+ if (maybe_exclude_items && options_.schema_version < 2) {
+ return IOStatus::InvalidArgument(
+ "exclude_files_callback requires schema_version >= 2");
+ }
+
if (options.decrease_background_thread_cpu_priority) {
if (options.background_thread_cpu_priority < threads_cpu_priority_) {
threads_cpu_priority_.store(options.background_thread_cpu_priority);
// live file.
std::unordered_set<std::string> live_dst_paths;
- std::vector<BackupAfterCopyOrCreateWorkItem> backup_items_to_finish;
+ std::deque<BackupWorkItemPair> excludable_items;
+ std::deque<BackupAfterCopyOrCreateWorkItem> backup_items_to_finish;
// Add a CopyOrCreateWorkItem to the channel for each live file
Status disabled = db->DisableFileDeletions();
DBOptions db_options = db->GetDBOptions();
break;
}
io_st = AddBackupFileWorkItem(
- live_dst_paths, backup_items_to_finish, new_backup_id,
+ live_dst_paths, backup_items_to_finish,
+ maybe_exclude_items ? &excludable_items : nullptr, new_backup_id,
options_.share_table_files &&
(type == kTableFile || type == kBlobFile),
src_dirname, fname, src_env_options, rate_limiter, type,
FileType type) {
Log(options_.info_log, "add file for backup %s", fname.c_str());
return AddBackupFileWorkItem(
- live_dst_paths, backup_items_to_finish, new_backup_id,
+ live_dst_paths, backup_items_to_finish,
+ maybe_exclude_items ? &excludable_items : nullptr, new_backup_id,
false /* shared */, "" /* src_dir */, fname,
EnvOptions() /* src_env_options */, rate_limiter, type,
contents.size(), db_options.statistics.get(), 0 /* size_limit */,
new_backup->SetSequenceNumber(sequence_number);
}
}
- ROCKS_LOG_INFO(options_.info_log, "add files for backup done, wait finish.");
+ ROCKS_LOG_INFO(options_.info_log, "add files for backup done.");
+ if (io_s.ok() && maybe_exclude_items) {
+ assert(options.exclude_files_callback);
+ size_t count = excludable_items.size();
+ std::vector<MaybeExcludeBackupFile> maybe_exclude_files;
+ maybe_exclude_files.reserve(count);
+ for (auto& e : excludable_items) {
+ maybe_exclude_files.emplace_back(
+ BackupExcludedFileInfo(e.second.dst_relative));
+ }
+ if (count > 0) {
+ try {
+ options.exclude_files_callback(
+ &maybe_exclude_files.front(),
+ /*end pointer*/ &maybe_exclude_files.back() + 1);
+ } catch (const std::exception& exn) {
+ io_s = IOStatus::Aborted("Exception in exclude_files_callback: " +
+ std::string(exn.what()));
+ } catch (...) {
+ io_s = IOStatus::Aborted("Unknown exception in exclude_files_callback");
+ }
+ }
+ if (io_s.ok()) {
+ for (size_t i = 0; i < count; ++i) {
+ auto& e = excludable_items[i];
+ if (maybe_exclude_files[i].exclude_decision) {
+ new_backup.get()->AddExcludedFile(e.second.dst_relative);
+ } else {
+ files_to_copy_or_create_.write(std::move(e.first));
+ backup_items_to_finish.push_back(std::move(e.second));
+ }
+ }
+ }
+ excludable_items.clear();
+ } else {
+ assert(!options.exclude_files_callback);
+ assert(excludable_items.empty());
+ }
+ ROCKS_LOG_INFO(options_.info_log,
+ "dispatch files for backup done, wait for finish.");
IOStatus item_io_status;
for (auto& item : backup_items_to_finish) {
item.result.wait();
auto& file_details = backup_info->file_details;
file_details.reserve(meta.GetFiles().size());
for (auto& file_ptr : meta.GetFiles()) {
- BackupFileInfo& finfo = *file_details.emplace(file_details.end());
+ BackupFileInfo& finfo = file_details.emplace_back();
finfo.relative_filename = file_ptr->filename;
finfo.size = file_ptr->size;
finfo.directory = dir;
finfo.file_type = type;
}
// TODO: temperature, file_checksum, file_checksum_func_name
+ // finfo.temperature = file_ptr->temp;
}
+ backup_info->excluded_files = meta.GetExcludedFiles();
+
backup_info->name_for_open = GetAbsolutePath(GetPrivateFileRel(id));
backup_info->name_for_open.pop_back(); // remove trailing '/'
backup_info->env_for_open = meta.GetEnvForOpen();
IOStatus BackupEngineImpl::RestoreDBFromBackup(
const RestoreOptions& options, BackupID backup_id,
- const std::string& db_dir, const std::string& wal_dir) const {
+ const std::string& db_dir, const std::string& wal_dir,
+ const std::list<const BackupEngineImpl*>& locked_restore_from_dirs) const {
assert(initialized_);
if (backup_id == kLatestBackupIDMarker) {
// Note: Read latest_valid_backup_id_ inside of lock
DeleteChildren(db_dir);
}
+ // Files to restore, and from where (taking into account excluded files)
+ std::vector<std::pair<const BackupEngineImpl*, const FileInfo*>>
+ restore_file_infos;
+ restore_file_infos.reserve(backup->GetFiles().size() +
+ backup->GetExcludedFiles().size());
+
+ for (const auto& ef : backup->GetExcludedFiles()) {
+ const std::string& file = ef.relative_file;
+
+ bool found = false;
+ for (auto be : locked_restore_from_dirs) {
+ auto it = be->backuped_file_infos_.find(file);
+ if (it != backuped_file_infos_.end()) {
+ restore_file_infos.emplace_back(be, &*it->second);
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ return IOStatus::InvalidArgument(
+ "Excluded file " + file + " not found in other backups nor in " +
+ std::to_string(locked_restore_from_dirs.size() - 1) +
+ " alternate backup directories");
+ }
+ }
+
+ // Non-excluded files
+ for (const auto& file_info_shared : backup->GetFiles()) {
+ restore_file_infos.emplace_back(this, &*file_info_shared);
+ }
+
IOStatus io_s;
std::vector<RestoreAfterCopyOrCreateWorkItem> restore_items_to_finish;
std::string temporary_current_file;
std::unique_ptr<FSDirectory> db_dir_for_fsync;
std::unique_ptr<FSDirectory> wal_dir_for_fsync;
- for (const auto& file_info : backup->GetFiles()) {
+ for (const auto& engine_and_file_info : restore_file_infos) {
+ const FileInfo* file_info = engine_and_file_info.second;
const std::string& file = file_info->filename;
+ std::string absolute_file =
+ engine_and_file_info.first->GetAbsolutePath(file);
+ Env* src_env = engine_and_file_info.first->backup_env_;
+
// 1. get DB filename
std::string dst = file_info->GetDbFileName();
ROCKS_LOG_INFO(options_.info_log, "Restoring %s to %s\n", file.c_str(),
dst.c_str());
CopyOrCreateWorkItem copy_or_create_work_item(
- GetAbsolutePath(file), dst, Temperature::kUnknown /* src_temp */,
- file_info->temp, "" /* contents */, backup_env_, db_env_,
+ absolute_file, dst, Temperature::kUnknown /* src_temp */,
+ file_info->temp, "" /* contents */, src_env, db_env_,
EnvOptions() /* src_env_options */, options_.sync,
options_.restore_rate_limiter.get(), file_info->size,
nullptr /* stats */);
while (*bytes_toward_next_callback >=
options_.callback_trigger_interval_size) {
*bytes_toward_next_callback -= options_.callback_trigger_interval_size;
- std::lock_guard<std::mutex> lock(byte_report_mutex_);
- progress_callback();
+ if (progress_callback) {
+ std::lock_guard<std::mutex> lock(byte_report_mutex_);
+ try {
+ progress_callback();
+ } catch (const std::exception& exn) {
+ io_s = IOStatus::Aborted("Exception in progress_callback: " +
+ std::string(exn.what()));
+ break;
+ } catch (...) {
+ io_s = IOStatus::Aborted("Unknown exception in progress_callback");
+ break;
+ }
+ }
}
} while (io_s.ok() && contents.empty() && data.size() > 0 && size_limit > 0);
// fname will always start with "/"
IOStatus BackupEngineImpl::AddBackupFileWorkItem(
std::unordered_set<std::string>& live_dst_paths,
- std::vector<BackupAfterCopyOrCreateWorkItem>& backup_items_to_finish,
- BackupID backup_id, bool shared, const std::string& src_dir,
- const std::string& fname, const EnvOptions& src_env_options,
- RateLimiter* rate_limiter, FileType file_type, uint64_t size_bytes,
- Statistics* stats, uint64_t size_limit, bool shared_checksum,
+ std::deque<BackupAfterCopyOrCreateWorkItem>& backup_items_to_finish,
+ std::deque<BackupWorkItemPair>* excludable_items, BackupID backup_id,
+ bool shared, const std::string& src_dir, const std::string& fname,
+ const EnvOptions& src_env_options, RateLimiter* rate_limiter,
+ FileType file_type, uint64_t size_bytes, Statistics* stats,
+ uint64_t size_limit, bool shared_checksum,
std::function<void()> progress_callback, const std::string& contents,
const std::string& src_checksum_func_name,
const std::string& src_checksum_str, const Temperature src_temperature) {
// Step 3: Add work item
if (!contents.empty() || need_to_copy) {
- ROCKS_LOG_INFO(options_.info_log, "Copying %s to %s", fname.c_str(),
- copy_dest_path->c_str());
CopyOrCreateWorkItem copy_or_create_work_item(
src_dir.empty() ? "" : src_path, *copy_dest_path, src_temperature,
Temperature::kUnknown /*dst_temp*/, contents, db_env_, backup_env_,
BackupAfterCopyOrCreateWorkItem after_copy_or_create_work_item(
copy_or_create_work_item.result.get_future(), shared, need_to_copy,
backup_env_, temp_dest_path, final_dest_path, dst_relative);
- files_to_copy_or_create_.write(std::move(copy_or_create_work_item));
- backup_items_to_finish.push_back(std::move(after_copy_or_create_work_item));
+ if (excludable_items != nullptr && shared && shared_checksum &&
+ need_to_copy) {
+ ROCKS_LOG_INFO(options_.info_log, "Copying (if not excluded) %s to %s",
+ fname.c_str(), copy_dest_path->c_str());
+ excludable_items->emplace_back(std::move(copy_or_create_work_item),
+ std::move(after_copy_or_create_work_item));
+ } else {
+ // For files known not excluded, can start copying even before finishing
+ // the checkpoint
+ ROCKS_LOG_INFO(options_.info_log, "Copying %s to %s", fname.c_str(),
+ copy_dest_path->c_str());
+ files_to_copy_or_create_.write(std::move(copy_or_create_work_item));
+ backup_items_to_finish.push_back(
+ std::move(after_copy_or_create_work_item));
+ }
} else {
std::promise<CopyOrCreateResult> promise_result;
BackupAfterCopyOrCreateWorkItem after_copy_or_create_work_item(
const std::string kFileCrc32cFieldName{"crc32"};
const std::string kFileSizeFieldName{"size"};
const std::string kTemperatureFieldName{"temp"};
+const std::string kExcludedFieldName{"ni::excluded"};
// Marks a (future) field that should cause failure if not recognized.
// Other fields are assumed to be ignorable. For example, in the future
// ...
//----------------------------------------------------------
//
-// For schema version 2.x (not in public APIs, but
-// forward-compatibility started):
+// For schema version 2.x:
//----------------------------------------------------------
// schema_version <ver>
// <timestamp>
const std::string& filename = components[0];
- uint64_t actual_size;
- const std::shared_ptr<FileInfo> file_info = GetFile(filename);
- if (file_info) {
- actual_size = file_info->size;
- } else {
- std::string abs_path = backup_dir + "/" + filename;
- auto e = abs_path_to_size.find(abs_path);
- if (e == abs_path_to_size.end()) {
- return IOStatus::Corruption(
- "Pathname in meta file not found on disk: " + abs_path);
- }
- actual_size = e->second;
- }
-
if (schema_major_version >= 2) {
if (components.size() % 2 != 1) {
return IOStatus::Corruption(
}
}
+ std::optional<uint64_t> expected_size{};
std::string checksum_hex;
Temperature temp = Temperature::kUnknown;
+ bool excluded = false;
for (unsigned i = 1; i < components.size(); i += 2) {
const std::string& field_name = components[i];
const std::string& field_data = components[i + 1];
}
checksum_hex = ChecksumInt32ToHex(checksum_value);
} else if (field_name == kFileSizeFieldName) {
- uint64_t ex_size =
- std::strtoull(field_data.c_str(), nullptr, /*base*/ 10);
- if (ex_size != actual_size) {
- return IOStatus::Corruption(
- "For file " + filename + " expected size " +
- std::to_string(ex_size) + " but found size" +
- std::to_string(actual_size));
- }
+ expected_size = std::strtoull(field_data.c_str(), nullptr, /*base*/ 10);
} else if (field_name == kTemperatureFieldName) {
auto iter = temperature_string_map.find(field_data);
if (iter != temperature_string_map.end()) {
// be safe.
temp = Temperature::kUnknown;
}
+ } else if (field_name == kExcludedFieldName) {
+ if (field_data == "true") {
+ excluded = true;
+ } else if (field_data == "false") {
+ excluded = false;
+ } else {
+ return IOStatus::NotSupported("Unrecognized value \"" + field_data +
+ "\" for field " + field_name);
+ }
} else if (StartsWith(field_name, kNonIgnorableFieldPrefix)) {
return IOStatus::NotSupported("Unrecognized non-ignorable file field " +
field_name + " (from future version?)");
}
}
- files.emplace_back(new FileInfo(filename, actual_size, checksum_hex,
- /*id*/ "", /*sid*/ "", temp));
+ if (excluded) {
+ excluded_files_.emplace_back(filename);
+ } else {
+ // Verify file exists, with expected size
+ std::string abs_path = backup_dir + "/" + filename;
+ auto e = abs_path_to_size.find(abs_path);
+ if (e == abs_path_to_size.end()) {
+ return IOStatus::Corruption(
+ "Pathname in meta file not found on disk: " + abs_path);
+ }
+ uint64_t actual_size = e->second;
+ if (expected_size.has_value() && *expected_size != actual_size) {
+ return IOStatus::Corruption("For file " + filename + " expected size " +
+ std::to_string(*expected_size) +
+ " but found size" +
+ std::to_string(actual_size));
+ }
+
+ // NOTE: FileInfo will be coalesced for sharing later (AddFile below)
+ files.emplace_back(
+ std::make_shared<FileInfo>(filename, actual_size, checksum_hex,
+ /*id*/ "", /*sid*/ "", temp));
+ }
}
if (footer_present) {
const std::vector<std::string> minor_version_strings{
"", // invalid major version 0
"", // implicit major version 1
- "2.0",
+ "2.1",
};
IOStatus BackupEngineImpl::BackupMeta::StoreToFile(
buf << "\n";
}
+ for (const auto& file : excluded_files_) {
+ assert(schema_version >= 2);
+ buf << file.relative_file << " " << kExcludedFieldName << " true\n";
+ }
+
if (schema_test_options && !schema_test_options->footer_fields.empty()) {
buf << kFooterMarker << "\n";
for (auto& e : schema_test_options->footer_fields) {
#include <atomic>
#include <cstddef>
#include <cstdint>
+#include <exception>
#include <limits>
#include <memory>
#include <random>
+#include <stdexcept>
#include <string>
#include <utility>
void CloseBackupEngine() { backup_engine_.reset(nullptr); }
// cross-cutting test of GetBackupInfo
- void AssertBackupInfoConsistency() {
+ void AssertBackupInfoConsistency(bool allow_excluded = false) {
std::vector<BackupInfo> backup_info;
backup_engine_->GetBackupInfo(&backup_info, /*with file details*/ true);
std::map<std::string, uint64_t> file_sizes;
sum_for_backup += file.size;
}
ASSERT_EQ(backup.size, sum_for_backup);
+ if (!allow_excluded) {
+ ASSERT_EQ(backup.excluded_files.size(), 0);
+ }
}
std::vector<BackupID> corrupt_backup_ids;
TEST_F(BackupEngineTest, ProgressCallbackDuringBackup) {
DestroyDBWithoutCheck(dbname_, options_);
- // Too big for this small DB
- engine_options_->callback_trigger_interval_size = 100000;
+
OpenDBAndBackupEngine(true);
FillDB(db_.get(), 0, 100);
+
+ // First test exception handling
+ // Easily small enough for this small DB
+ engine_options_->callback_trigger_interval_size = 1000;
+ OpenBackupEngine();
+ ASSERT_TRUE(
+ backup_engine_->CreateNewBackup(db_.get(), true, []() { throw 42; })
+ .IsAborted());
+ ASSERT_TRUE(backup_engine_
+ ->CreateNewBackup(db_.get(), true,
+ []() { throw std::out_of_range("blah"); })
+ .IsAborted());
+
+ // Too big for this small DB
+ engine_options_->callback_trigger_interval_size = 100000;
+ OpenBackupEngine();
bool is_callback_invoked = false;
ASSERT_OK(backup_engine_->CreateNewBackup(
db_.get(), true,
&info, /*include_file_details*/ true));
ASSERT_GT(info.file_details.size(), 2);
for (auto& e : info.file_details) {
- ASSERT_EQ(expected_temps[e.file_number], e.temperature);
+ EXPECT_EQ(expected_temps[e.file_number], e.temperature);
}
// Restore backup to another virtual (tiered) dir
}
}
+TEST_F(BackupEngineTest, ExcludeFiles) {
+ // Required for excluding files
+ engine_options_->schema_version = 2;
+
+ // Need a sufficent set of file numbers
+ options_.level0_file_num_compaction_trigger = 100;
+
+ OpenDBAndBackupEngine(true, false, kShareWithChecksum);
+ // Need a sufficent set of file numbers
+ const int keys_iteration = 5000;
+ FillDB(db_.get(), 0, keys_iteration / 3);
+ FillDB(db_.get(), keys_iteration / 3, keys_iteration * 2 / 3);
+ FillDB(db_.get(), keys_iteration * 2 / 3, keys_iteration);
+ CloseAndReopenDB();
+
+ BackupEngine* alt_backup_engine;
+ BackupEngineOptions alt_engine_options{*engine_options_};
+ // Use an alternate Env to test that support
+ std::string backup_alt_chroot = test::PerThreadDBPath("db_alt_backups");
+ EXPECT_OK(Env::Default()->CreateDirIfMissing(backup_alt_chroot));
+ alt_engine_options.backup_dir = "/altbk";
+ std::shared_ptr<FileSystem> alt_fs{
+ NewChrootFileSystem(FileSystem::Default(), backup_alt_chroot)};
+ std::unique_ptr<Env> alt_env{new CompositeEnvWrapper(Env::Default(), alt_fs)};
+ alt_engine_options.backup_env = alt_env.get();
+
+ ASSERT_OK(BackupEngine::Open(test_db_env_.get(), alt_engine_options,
+ &alt_backup_engine));
+
+ // Ensure each backup is same set of files
+ db_.reset();
+ DB* db = nullptr;
+ ASSERT_OK(DB::OpenForReadOnly(options_, dbname_, &db));
+
+ // A callback that throws should cleanly fail the backup creation.
+ // Do this early to ensure later operations still work.
+ CreateBackupOptions cbo;
+ cbo.exclude_files_callback = [](MaybeExcludeBackupFile* /*files_begin*/,
+ MaybeExcludeBackupFile* /*files_end*/) {
+ throw 42;
+ };
+ ASSERT_TRUE(backup_engine_->CreateNewBackup(cbo, db).IsAborted());
+ cbo.exclude_files_callback = [](MaybeExcludeBackupFile* /*files_begin*/,
+ MaybeExcludeBackupFile* /*files_end*/) {
+ throw std::out_of_range("blah");
+ };
+ ASSERT_TRUE(backup_engine_->CreateNewBackup(cbo, db).IsAborted());
+
+ // Include files only in given bucket, based on modulus and remainder
+ constexpr int modulus = 4;
+ int remainder = 0;
+
+ cbo.exclude_files_callback = [&remainder](MaybeExcludeBackupFile* files_begin,
+ MaybeExcludeBackupFile* files_end) {
+ for (auto* f = files_begin; f != files_end; ++f) {
+ std::string s = StringSplit(f->info.relative_file, '/').back();
+ s = s.substr(0, s.find("_"));
+ int64_t num = std::strtoll(s.c_str(), nullptr, /*base*/ 10);
+ // Exclude if not a match
+ f->exclude_decision = (num % modulus) != remainder;
+ }
+ };
+
+ BackupID first_id{};
+ BackupID last_alt_id{};
+ remainder = 0;
+ ASSERT_OK(backup_engine_->CreateNewBackup(cbo, db, &first_id));
+ AssertBackupInfoConsistency(/*allow excluded*/ true);
+ remainder = 1;
+ ASSERT_OK(alt_backup_engine->CreateNewBackup(cbo, db));
+ AssertBackupInfoConsistency(/*allow excluded*/ true);
+ remainder = 2;
+ ASSERT_OK(backup_engine_->CreateNewBackup(cbo, db));
+ AssertBackupInfoConsistency(/*allow excluded*/ true);
+ remainder = 3;
+ ASSERT_OK(alt_backup_engine->CreateNewBackup(cbo, db, &last_alt_id));
+ AssertBackupInfoConsistency(/*allow excluded*/ true);
+
+ // Close DB
+ ASSERT_OK(db->Close());
+ delete db;
+ db = nullptr;
+
+ for (auto be_pair :
+ {std::make_pair(backup_engine_.get(), alt_backup_engine),
+ std::make_pair(alt_backup_engine, backup_engine_.get())}) {
+ DestroyDB(dbname_, options_);
+ RestoreOptions ro;
+ // Fails without alternate dir
+ ASSERT_TRUE(be_pair.first->RestoreDBFromLatestBackup(dbname_, dbname_, ro)
+ .IsInvalidArgument());
+
+ DestroyDB(dbname_, options_);
+ // Works with alternate dir
+ ro.alternate_dirs.push_front(be_pair.second);
+ ASSERT_OK(be_pair.first->RestoreDBFromLatestBackup(dbname_, dbname_, ro));
+
+ // Check DB contents
+ db = OpenDB();
+ AssertExists(db, 0, keys_iteration);
+ delete db;
+ }
+
+ // Should still work after close and re-open
+ CloseBackupEngine();
+ OpenBackupEngine();
+
+ for (auto be_pair :
+ {std::make_pair(backup_engine_.get(), alt_backup_engine),
+ std::make_pair(alt_backup_engine, backup_engine_.get())}) {
+ DestroyDB(dbname_, options_);
+ RestoreOptions ro;
+ ro.alternate_dirs.push_front(be_pair.second);
+ ASSERT_OK(be_pair.first->RestoreDBFromLatestBackup(dbname_, dbname_, ro));
+ }
+
+ // Deletion semantics are tricky when within a single backup dir one backup
+ // includes a file and the other backup excluded the file. The excluded one
+ // does not have a persistent record of metadata like file checksum, etc.
+ // Although it would be possible to amend the backup with the excluded file,
+ // that is not currently supported (unless you open the backup as read-only
+ // DB and take another backup of it). The "excluded" reference to the file
+ // is like a weak reference: it doesn't prevent the file from being deleted
+ // if all the backups with "included" references to it are deleted.
+ CloseBackupEngine();
+ OpenBackupEngine();
+
+ AssertBackupInfoConsistency(/*allow excluded*/ true);
+
+ ASSERT_OK(backup_engine_->DeleteBackup(first_id));
+ ASSERT_OK(alt_backup_engine->DeleteBackup(last_alt_id));
+
+ // Includes check for any leaked backup files
+ AssertBackupInfoConsistency(/*allow excluded*/ true);
+
+ // Excluded file(s) deleted, unable to restore
+ for (auto be_pair :
+ {std::make_pair(backup_engine_.get(), alt_backup_engine),
+ std::make_pair(alt_backup_engine, backup_engine_.get())}) {
+ RestoreOptions ro;
+ ro.alternate_dirs.push_front(be_pair.second);
+ ASSERT_TRUE(be_pair.first->RestoreDBFromLatestBackup(dbname_, dbname_, ro)
+ .IsInvalidArgument());
+ }
+
+ // Close & Re-open (no crash, etc.)
+ CloseBackupEngine();
+ OpenBackupEngine();
+
+ AssertBackupInfoConsistency(/*allow excluded*/ true);
+
+ // Excluded file(s) deleted, unable to restore
+ for (auto be_pair :
+ {std::make_pair(backup_engine_.get(), alt_backup_engine),
+ std::make_pair(alt_backup_engine, backup_engine_.get())}) {
+ RestoreOptions ro;
+ ro.alternate_dirs.push_front(be_pair.second);
+ ASSERT_TRUE(be_pair.first->RestoreDBFromLatestBackup(dbname_, dbname_, ro)
+ .IsInvalidArgument());
+ }
+
+ // Ensure files are not leaked after removing everything.
+ ASSERT_OK(backup_engine_->DeleteBackup(first_id + 1));
+ ASSERT_OK(alt_backup_engine->DeleteBackup(last_alt_id - 1));
+
+ // Includes check for leaked backups files
+ AssertBackupInfoConsistency(/*allow excluded*/ false);
+}
+
} // namespace
} // namespace ROCKSDB_NAMESPACE