mirror of
https://github.com/facebook/rocksdb.git
synced 2026-10-23 08:09:02 +03:00
Summary: Pull Request resolved: https://github.com/facebook/rocksdb/pull/15305 Why the change is required: Column-family topology is durable in MANIFEST, while its operational configuration snapshot is stored in a separate OPTIONS file. CreateColumnFamily and DropColumnFamily previously committed MANIFEST first and wrote OPTIONS afterward. An OPTIONS I/O failure could therefore make the API report failure after the topology change was already durable. Persisted-options readers also selected the highest file number and could consume an orphan left by a failed publication. Previous behavior: RocksDB had no durable correspondence between a MANIFEST state and the OPTIONS snapshot describing it. LoadLatestOptions and GetLatestOptionsFileName treated the highest-numbered OPTIONS file as authoritative. Column-family creation and deletion returned the later OPTIONS persistence error even when their MANIFEST edit had already succeeded. Multi-column-family operations wrote a complete OPTIONS snapshot after every successful member, producing quadratic OPTIONS bytes. Ordinary DB::Open consumes caller-supplied DB and column-family options rather than loading OPTIONS. This change preserves that contract. New behavior: Add the default-disabled experimental DB option track_options_file_number_in_manifest. Its value is serialized into each OPTIONS file so the selection rule is self-describing and participates in the normal DB-options compatibility checks. When enabled, RocksDB first durably creates and renames OPTIONS-N and then commits N with one safely ignorable MANIFEST field. A normal OPTIONS rotation uses one MANIFEST commit. Column-family creation and deletion prewrite the proposed snapshot and commit its number together with a topology edit in the same VersionEdit. A failure before the OPTIONS file is durable changes no topology. A failure after the file is durable but before MANIFEST commit leaves an orphan that binding-aware readers ignore, while the previous committed snapshot remains authoritative. Each multi-CF create or drop writes one proposed snapshot, independent of the batch size. Create commits the new number with the first successful add; drop commits it with the last successful drop. Existing partial-success semantics can therefore leave a committed snapshot with extra CFs, but never one missing a live MANIFEST CF. Persisted-options APIs filter this safe superset against recovered MANIFEST membership. A snapshot missing a live CF is corruption. DB::Open with create_missing_column_families uses the same batched path. If the newest OPTIONS file has tracking disabled, it retains the legacy highest-numbered-file behavior, including after an old or disabled writer has run. If it has tracking enabled, readers use the recovered MANIFEST commit and reject newer tracked orphans. A missing usable commit falls back to the highest tracked snapshot for RepairDB and legacy-rewrite recovery. DB open logs that fallback through its configured logger; logger-less utility APIs remain silent. Writable open removes rejected tracked orphans when local obsolete-file deletion is enabled. Read-only open never mutates the DB directory. A DB with no recovered commit and tracking disabled takes the legacy fast path without parsing the newest OPTIONS file. When inspection is required, RocksDB reads only the tracking marker. Marker-read failures used for selection do not fail DB open; a recovered commit remains authoritative and ambiguous newer candidates are conservatively retained. The first topology change after enabling tracking first binds the current topology, so failure of the proposed change cannot make its orphan authoritative. MANIFEST failure during an ordinary tracked OPTIONS rotation also enters the existing manifest-write background-error path. Implementation: - Add the safely ignorable CommittedOptionsFileNumber VersionEdit field and carry it through recovery, MANIFEST snapshots, rollover, and subset checkpoints. - Persist the tracking marker through C++, C, and Java options, generated round-trip coverage, compatibility checking, and db_stress configuration. - Centralize binding-aware selection for DB open and public persisted-options APIs. Reuse RecoveryContext's directory listing, use a narrow marker scan, and recover live CF names while replaying MANIFEST. - Filter manifest-selected safe-superset snapshots to live MANIFEST CFs in LoadLatestOptions and compatibility verification without discarding their option maps. - Prevalidate and sanitize CF batches outside the DB mutex, write one complete snapshot while options_mutex_ serializes topology changes, and hold the DB mutex only for the existing individual MANIFEST commits. Batch missing-CF creation during DB open follows the same locking boundary. - Keep the committed snapshot live in cleanup and live-file APIs, and make subset checkpoints write an OPTIONS snapshot and filtered MANIFEST with identical membership in tracked and legacy modes. - Preserve the first partial multi-CF drop error while consuming legacy OPTIONS persistence Status, and sync the checkpoint MANIFEST directory after replacement. - Pass checkpoint helper callbacks by reference and group VersionEdit storage by alignment so the new paths add neither callback copies nor avoidable object padding. Safety boundaries and unsupported cases: - Tracking defaults to false. Disabled writers and never-tracked databases retain legacy write ordering and highest-numbered selection. - Older binaries safely ignore the new MANIFEST tag and continue choosing the highest OPTIONS number. Downgrade is readable but not crash-atomic: an old binary can consume a tracked orphan. That is the same class of OPTIONS/MANIFEST inconsistency as the previous behavior. - A rollback binary must know the serialized option name before the feature is enabled. Exact compatibility checks intentionally report a mismatch when the caller's tracking setting differs from the persisted snapshot. - A newer untracked OPTIONS file deliberately supersedes an older committed number, allowing an old or disabled writer to become authoritative without rewriting MANIFEST. - Reactive secondary and follower instances do not enforce or repair the binding. Ordinary DB::Open still uses caller-supplied options. - Writable open does not delete rejected tracked or ambiguous files when a compaction service owns OPTIONS-file retention. Read-only open performs no repair writes. - Direct consumers that scan OPTIONS filenames themselves are unchanged; the binding is enforced by RocksDB's persisted-options APIs. - If the committed snapshot is missing and only tracked candidates remain, selection intentionally falls back to the highest candidate so RepairDB output remains usable. Validation coverage and known limitations: Focused coverage exercises encoding compatibility, tracked and legacy selection, the never-tracked open fast path, marker-read failures, exact tracking-marker compatibility, activation and disable/re-enable transitions, batched create/drop and create-missing behavior, partial-batch safe supersets, OPTIONS/MANIFEST failure injection, background-error propagation, orphan cleanup and retention, read-only non-mutation, subset-checkpoint behavior, stale OPTIONS recovery, RepairDB option sanitization, and transient read-corruption reconstruction. The assertion-enabled full executable suite passes. The repository check-format and check-sources wrappers cannot run because they invoke Git inside this Sapling checkout; fbsource lint is used instead. Cross-platform CI is not part of local validation. Reviewed By: pdillinger Differential Revision: D123104845 fbshipit-source-id: 77d94c989243c3f3f9fedfa81fa364c166250303
802 lines
29 KiB
C++
802 lines
29 KiB
C++
// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
|
|
// This source code is licensed under both the GPLv2 (found in the
|
|
// COPYING file in the root directory) and Apache 2.0 License
|
|
// (found in the LICENSE.Apache file in the root directory).
|
|
|
|
#include "options/options_parser.h"
|
|
|
|
#include <cmath>
|
|
#include <map>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "file/file_util.h"
|
|
#include "file/line_file_reader.h"
|
|
#include "file/writable_file_writer.h"
|
|
#include "options/cf_options.h"
|
|
#include "options/db_options.h"
|
|
#include "options/options_helper.h"
|
|
#include "port/port.h"
|
|
#include "rocksdb/convenience.h"
|
|
#include "rocksdb/db.h"
|
|
#include "rocksdb/utilities/options_type.h"
|
|
#include "test_util/sync_point.h"
|
|
#include "util/cast_util.h"
|
|
#include "util/string_util.h"
|
|
|
|
namespace ROCKSDB_NAMESPACE {
|
|
|
|
static const std::string option_file_header =
|
|
"# This is a RocksDB option file.\n"
|
|
"#\n"
|
|
"# For detailed file format spec, please refer to the example file\n"
|
|
"# in examples/rocksdb_option_file_example.ini\n"
|
|
"#\n"
|
|
"\n";
|
|
|
|
Status PersistRocksDBOptions(const WriteOptions& write_options,
|
|
const DBOptions& db_opt,
|
|
const std::vector<std::string>& cf_names,
|
|
const std::vector<ColumnFamilyOptions>& cf_opts,
|
|
const std::string& file_name, FileSystem* fs) {
|
|
ConfigOptions
|
|
config_options; // Use default for escaped(true) and check (exact)
|
|
config_options.delimiter = "\n ";
|
|
// Do not invoke PrepareOptions when we are doing validation.
|
|
config_options.invoke_prepare_options = false;
|
|
// If a readahead size was set in the input options, use it
|
|
if (db_opt.log_readahead_size > 0) {
|
|
config_options.file_readahead_size = db_opt.log_readahead_size;
|
|
}
|
|
return PersistRocksDBOptions(write_options, config_options, db_opt, cf_names,
|
|
cf_opts, file_name, fs);
|
|
}
|
|
|
|
Status PersistRocksDBOptions(const WriteOptions& write_options,
|
|
const ConfigOptions& config_options_in,
|
|
const DBOptions& db_opt,
|
|
const std::vector<std::string>& cf_names,
|
|
const std::vector<ColumnFamilyOptions>& cf_opts,
|
|
const std::string& file_name, FileSystem* fs) {
|
|
ConfigOptions config_options = config_options_in;
|
|
config_options.delimiter = "\n "; // Override the default to nl
|
|
|
|
TEST_SYNC_POINT("PersistRocksDBOptions:start");
|
|
if (cf_names.size() != cf_opts.size()) {
|
|
return Status::InvalidArgument(
|
|
"cf_names.size() and cf_opts.size() must be the same");
|
|
}
|
|
std::unique_ptr<FSWritableFile> wf;
|
|
|
|
FileOptions file_options;
|
|
file_options.temperature = db_opt.metadata_write_temperature;
|
|
Status s = fs->NewWritableFile(file_name, file_options, &wf, nullptr);
|
|
if (!s.ok()) {
|
|
return s;
|
|
}
|
|
std::unique_ptr<WritableFileWriter> writable;
|
|
writable.reset(new WritableFileWriter(std::move(wf), file_name, EnvOptions(),
|
|
nullptr /* statistics */));
|
|
TEST_SYNC_POINT("PersistRocksDBOptions:create");
|
|
|
|
std::string options_file_content;
|
|
|
|
IOOptions opts;
|
|
s = WritableFileWriter::PrepareIOOptions(write_options, opts);
|
|
if (s.ok()) {
|
|
s = writable->Append(option_file_header + "[" +
|
|
opt_section_titles[kOptionSectionVersion] +
|
|
"]\n"
|
|
" rocksdb_version=" +
|
|
std::to_string(ROCKSDB_MAJOR) + "." +
|
|
std::to_string(ROCKSDB_MINOR) + "." +
|
|
std::to_string(ROCKSDB_PATCH) + "\n",
|
|
opts);
|
|
}
|
|
if (s.ok()) {
|
|
s = writable->Append(
|
|
" options_file_version=" + std::to_string(ROCKSDB_OPTION_FILE_MAJOR) +
|
|
"." + std::to_string(ROCKSDB_OPTION_FILE_MINOR) + "\n",
|
|
opts);
|
|
}
|
|
if (s.ok()) {
|
|
s = writable->Append(
|
|
"\n[" + opt_section_titles[kOptionSectionDBOptions] + "]\n ", opts);
|
|
}
|
|
|
|
if (s.ok()) {
|
|
s = GetStringFromDBOptions(config_options, db_opt, &options_file_content);
|
|
}
|
|
if (s.ok()) {
|
|
s = writable->Append(options_file_content + "\n", opts);
|
|
}
|
|
|
|
for (size_t i = 0; s.ok() && i < cf_opts.size(); ++i) {
|
|
// CFOptions section
|
|
s = writable->Append("\n[" + opt_section_titles[kOptionSectionCFOptions] +
|
|
" \"" + EscapeOptionString(cf_names[i]) +
|
|
"\"]\n ",
|
|
opts);
|
|
if (s.ok()) {
|
|
s = GetStringFromColumnFamilyOptions(config_options, cf_opts[i],
|
|
&options_file_content);
|
|
}
|
|
if (s.ok()) {
|
|
s = writable->Append(options_file_content + "\n", opts);
|
|
}
|
|
// TableOptions section
|
|
auto* tf = cf_opts[i].table_factory.get();
|
|
if (tf != nullptr) {
|
|
if (s.ok()) {
|
|
s = writable->Append(
|
|
"[" + opt_section_titles[kOptionSectionTableOptions] + tf->Name() +
|
|
" \"" + EscapeOptionString(cf_names[i]) + "\"]\n ",
|
|
opts);
|
|
}
|
|
if (s.ok()) {
|
|
options_file_content.clear();
|
|
s = tf->GetOptionString(config_options, &options_file_content);
|
|
}
|
|
if (s.ok()) {
|
|
s = writable->Append(options_file_content + "\n", opts);
|
|
}
|
|
}
|
|
}
|
|
if (s.ok()) {
|
|
s = writable->Fsync(opts);
|
|
}
|
|
if (s.ok()) {
|
|
s = writable->Close(opts);
|
|
}
|
|
TEST_SYNC_POINT("PersistRocksDBOptions:written");
|
|
if (s.ok()) {
|
|
return RocksDBOptionsParser::VerifyRocksDBOptionsFromFile(
|
|
config_options, db_opt, cf_names, cf_opts, file_name, fs);
|
|
}
|
|
return s;
|
|
}
|
|
|
|
RocksDBOptionsParser::RocksDBOptionsParser() { Reset(); }
|
|
|
|
void RocksDBOptionsParser::Reset() {
|
|
db_opt_ = DBOptions();
|
|
db_opt_map_.clear();
|
|
cf_names_.clear();
|
|
cf_opts_.clear();
|
|
cf_opt_maps_.clear();
|
|
has_version_section_ = false;
|
|
has_db_options_ = false;
|
|
has_default_cf_options_ = false;
|
|
for (int i = 0; i < 3; ++i) {
|
|
db_version[i] = 0;
|
|
opt_file_version[i] = 0;
|
|
}
|
|
}
|
|
|
|
bool RocksDBOptionsParser::IsSection(const std::string& line) {
|
|
if (line.size() < 2) {
|
|
return false;
|
|
}
|
|
if (line[0] != '[' || line[line.size() - 1] != ']') {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
Status RocksDBOptionsParser::ParseSection(OptionSection* section,
|
|
std::string* title,
|
|
std::string* argument,
|
|
const std::string& line,
|
|
const int line_num) {
|
|
*section = kOptionSectionUnknown;
|
|
// A section is of the form [<SectionName> "<SectionArg>"], where
|
|
// "<SectionArg>" is optional.
|
|
size_t arg_start_pos = line.find('\"');
|
|
size_t arg_end_pos = line.rfind('\"');
|
|
// The following if-then check tries to identify whether the input
|
|
// section has the optional section argument.
|
|
if (arg_start_pos != std::string::npos && arg_start_pos != arg_end_pos) {
|
|
*title = TrimAndRemoveComment(line.substr(1, arg_start_pos - 1), true);
|
|
*argument = UnescapeOptionString(
|
|
line.substr(arg_start_pos + 1, arg_end_pos - arg_start_pos - 1));
|
|
} else {
|
|
*title = TrimAndRemoveComment(line.substr(1, line.size() - 2), true);
|
|
*argument = "";
|
|
}
|
|
for (int i = 0; i < kOptionSectionUnknown; ++i) {
|
|
if (title->find(opt_section_titles[i]) == 0) {
|
|
if (i == kOptionSectionVersion || i == kOptionSectionDBOptions ||
|
|
i == kOptionSectionCFOptions) {
|
|
if (title->size() == opt_section_titles[i].size()) {
|
|
// if true, then it indicats equal
|
|
*section = static_cast<OptionSection>(i);
|
|
return CheckSection(*section, *argument, line_num);
|
|
}
|
|
} else if (i == kOptionSectionTableOptions) {
|
|
// This type of sections has a sufffix at the end of the
|
|
// section title
|
|
if (title->size() > opt_section_titles[i].size()) {
|
|
*section = static_cast<OptionSection>(i);
|
|
return CheckSection(*section, *argument, line_num);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return Status::InvalidArgument(std::string("Unknown section ") + line);
|
|
}
|
|
|
|
Status RocksDBOptionsParser::InvalidArgument(const int line_num,
|
|
const std::string& message) {
|
|
return Status::InvalidArgument(
|
|
"[RocksDBOptionsParser Error] ",
|
|
message + " (at line " + std::to_string(line_num) + ")");
|
|
}
|
|
|
|
Status RocksDBOptionsParser::ParseStatement(std::string* name,
|
|
std::string* value,
|
|
const std::string& line,
|
|
const int line_num) {
|
|
size_t eq_pos = line.find('=');
|
|
if (eq_pos == std::string::npos) {
|
|
return InvalidArgument(line_num, "A valid statement must have a '='.");
|
|
}
|
|
|
|
*name = TrimAndRemoveComment(line.substr(0, eq_pos), true);
|
|
*value =
|
|
TrimAndRemoveComment(line.substr(eq_pos + 1, line.size() - eq_pos - 1));
|
|
if (name->empty()) {
|
|
return InvalidArgument(line_num,
|
|
"A valid statement must have a variable name.");
|
|
}
|
|
return Status::OK();
|
|
}
|
|
|
|
Status RocksDBOptionsParser::Parse(const std::string& file_name, FileSystem* fs,
|
|
bool ignore_unknown_options,
|
|
size_t file_readahead_size) {
|
|
ConfigOptions
|
|
config_options; // Use default for escaped(true) and check (exact)
|
|
config_options.ignore_unknown_options = ignore_unknown_options;
|
|
if (file_readahead_size > 0) {
|
|
config_options.file_readahead_size = file_readahead_size;
|
|
}
|
|
return Parse(config_options, file_name, fs);
|
|
}
|
|
|
|
Status RocksDBOptionsParser::Parse(const ConfigOptions& config_options_in,
|
|
const std::string& file_name,
|
|
FileSystem* fs) {
|
|
Reset();
|
|
ConfigOptions config_options = config_options_in;
|
|
|
|
Status s;
|
|
bool retry = false;
|
|
do {
|
|
std::unique_ptr<FSSequentialFile> seq_file;
|
|
s = fs->NewSequentialFile(file_name, FileOptions(), &seq_file, nullptr);
|
|
if (!s.ok()) {
|
|
return s;
|
|
}
|
|
|
|
LineFileReader lf_reader(
|
|
std::move(seq_file), file_name, config_options.file_readahead_size,
|
|
nullptr, std::vector<std::shared_ptr<EventListener>>{}, nullptr, retry);
|
|
|
|
OptionSection section = kOptionSectionUnknown;
|
|
std::string title;
|
|
std::string argument;
|
|
std::unordered_map<std::string, std::string> opt_map;
|
|
std::string line;
|
|
// we only support single-lined statement.
|
|
while (
|
|
lf_reader.ReadLine(&line, Env::IO_TOTAL /* rate_limiter_priority */)) {
|
|
int line_num = static_cast<int>(lf_reader.GetLineNumber());
|
|
line = TrimAndRemoveComment(line);
|
|
if (line.empty()) {
|
|
continue;
|
|
}
|
|
if (IsSection(line)) {
|
|
s = EndSection(config_options, section, title, argument, opt_map);
|
|
opt_map.clear();
|
|
if (!s.ok()) {
|
|
break;
|
|
}
|
|
|
|
// If the option file is not generated by a higher version, unknown
|
|
// option should only mean corruption.
|
|
if (config_options.ignore_unknown_options &&
|
|
section == kOptionSectionVersion) {
|
|
using VTuple = std::tuple<int, int, int>;
|
|
if (VTuple(db_version[0], db_version[1], db_version[2]) <=
|
|
VTuple(ROCKSDB_MAJOR, ROCKSDB_MINOR, ROCKSDB_PATCH)) {
|
|
config_options.ignore_unknown_options = false;
|
|
}
|
|
}
|
|
|
|
s = ParseSection(§ion, &title, &argument, line, line_num);
|
|
if (!s.ok()) {
|
|
break;
|
|
}
|
|
} else {
|
|
std::string name;
|
|
std::string value;
|
|
s = ParseStatement(&name, &value, line, line_num);
|
|
if (!s.ok()) {
|
|
break;
|
|
}
|
|
opt_map.insert({name, value});
|
|
}
|
|
}
|
|
if (s.ok()) {
|
|
s = lf_reader.GetStatus();
|
|
}
|
|
if (s.ok()) {
|
|
s = EndSection(config_options, section, title, argument, opt_map);
|
|
opt_map.clear();
|
|
}
|
|
if (s.ok()) {
|
|
s = ValidityCheck();
|
|
}
|
|
if (!s.ok()) {
|
|
if ((s.IsCorruption() || s.IsInvalidArgument()) && !retry &&
|
|
CheckFSFeatureSupport(fs,
|
|
FSSupportedOps::kVerifyAndReconstructRead)) {
|
|
retry = true;
|
|
Reset();
|
|
} else {
|
|
return s;
|
|
}
|
|
} else {
|
|
return s;
|
|
}
|
|
} while (retry);
|
|
|
|
return s;
|
|
}
|
|
|
|
Status RocksDBOptionsParser::CheckSection(const OptionSection section,
|
|
const std::string& section_arg,
|
|
const int line_num) {
|
|
if (section == kOptionSectionDBOptions) {
|
|
if (has_db_options_) {
|
|
return InvalidArgument(
|
|
line_num,
|
|
"More than one DBOption section found in the option config file");
|
|
}
|
|
has_db_options_ = true;
|
|
} else if (section == kOptionSectionCFOptions) {
|
|
bool is_default_cf = (section_arg == kDefaultColumnFamilyName);
|
|
if (cf_opts_.size() == 0 && !is_default_cf) {
|
|
return InvalidArgument(
|
|
line_num,
|
|
"Default column family must be the first CFOptions section "
|
|
"in the option config file");
|
|
} else if (cf_opts_.size() != 0 && is_default_cf) {
|
|
return InvalidArgument(
|
|
line_num,
|
|
"Default column family must be the first CFOptions section "
|
|
"in the optio/n config file");
|
|
} else if (GetCFOptions(section_arg) != nullptr) {
|
|
return InvalidArgument(
|
|
line_num,
|
|
"Two identical column families found in option config file");
|
|
}
|
|
has_default_cf_options_ |= is_default_cf;
|
|
} else if (section == kOptionSectionTableOptions) {
|
|
if (GetCFOptions(section_arg) == nullptr) {
|
|
return InvalidArgument(
|
|
line_num, std::string("Does not find a matched column family name in "
|
|
"TableOptions section. Column Family Name:") +
|
|
section_arg);
|
|
}
|
|
} else if (section == kOptionSectionVersion) {
|
|
if (has_version_section_) {
|
|
return InvalidArgument(
|
|
line_num,
|
|
"More than one Version section found in the option config file.");
|
|
}
|
|
has_version_section_ = true;
|
|
}
|
|
return Status::OK();
|
|
}
|
|
|
|
Status RocksDBOptionsParser::ParseVersionNumber(const std::string& ver_name,
|
|
const std::string& ver_string,
|
|
const int max_count,
|
|
int* version) {
|
|
int version_index = 0;
|
|
int current_number = 0;
|
|
int current_digit_count = 0;
|
|
bool has_dot = false;
|
|
for (int i = 0; i < max_count; ++i) {
|
|
version[i] = 0;
|
|
}
|
|
constexpr int kBufferSize = 200;
|
|
char buffer[kBufferSize];
|
|
for (size_t i = 0; i < ver_string.size(); ++i) {
|
|
if (ver_string[i] == '.') {
|
|
if (version_index >= max_count - 1) {
|
|
snprintf(buffer, sizeof(buffer) - 1,
|
|
"A valid %s can only contains at most %d dots.",
|
|
ver_name.c_str(), max_count - 1);
|
|
return Status::InvalidArgument(buffer);
|
|
}
|
|
if (current_digit_count == 0) {
|
|
snprintf(buffer, sizeof(buffer) - 1,
|
|
"A valid %s must have at least one digit before each dot.",
|
|
ver_name.c_str());
|
|
return Status::InvalidArgument(buffer);
|
|
}
|
|
version[version_index++] = current_number;
|
|
current_number = 0;
|
|
current_digit_count = 0;
|
|
has_dot = true;
|
|
} else if (isdigit(ver_string[i])) {
|
|
current_number = current_number * 10 + (ver_string[i] - '0');
|
|
current_digit_count++;
|
|
} else {
|
|
snprintf(buffer, sizeof(buffer) - 1,
|
|
"A valid %s can only contains dots and numbers.",
|
|
ver_name.c_str());
|
|
return Status::InvalidArgument(buffer);
|
|
}
|
|
}
|
|
version[version_index] = current_number;
|
|
if (has_dot && current_digit_count == 0) {
|
|
snprintf(buffer, sizeof(buffer) - 1,
|
|
"A valid %s must have at least one digit after each dot.",
|
|
ver_name.c_str());
|
|
return Status::InvalidArgument(buffer);
|
|
}
|
|
return Status::OK();
|
|
}
|
|
|
|
Status RocksDBOptionsParser::EndSection(
|
|
const ConfigOptions& config_options, const OptionSection section,
|
|
const std::string& section_title, const std::string& section_arg,
|
|
const std::unordered_map<std::string, std::string>& opt_map) {
|
|
Status s;
|
|
if (section == kOptionSectionDBOptions) {
|
|
s = GetDBOptionsFromMap(config_options, DBOptions(), opt_map, &db_opt_);
|
|
if (!s.ok()) {
|
|
return s;
|
|
}
|
|
db_opt_map_ = opt_map;
|
|
} else if (section == kOptionSectionCFOptions) {
|
|
// This condition should be ensured earlier in ParseSection
|
|
// so we make an assertion here.
|
|
assert(GetCFOptions(section_arg) == nullptr);
|
|
cf_names_.emplace_back(section_arg);
|
|
cf_opts_.emplace_back();
|
|
s = GetColumnFamilyOptionsFromMap(config_options, ColumnFamilyOptions(),
|
|
opt_map, &cf_opts_.back());
|
|
if (!s.ok()) {
|
|
return s;
|
|
}
|
|
// keep the parsed string.
|
|
cf_opt_maps_.emplace_back(opt_map);
|
|
} else if (section == kOptionSectionTableOptions) {
|
|
assert(GetCFOptions(section_arg) != nullptr);
|
|
auto* cf_opt = GetCFOptionsImpl(section_arg);
|
|
if (cf_opt == nullptr) {
|
|
return Status::InvalidArgument(
|
|
"The specified column family must be defined before the "
|
|
"TableOptions section:",
|
|
section_arg);
|
|
}
|
|
// Ignore error as table factory deserialization is optional
|
|
cf_opt->table_factory.reset();
|
|
s = TableFactory::CreateFromString(
|
|
config_options,
|
|
section_title.substr(
|
|
opt_section_titles[kOptionSectionTableOptions].size()),
|
|
&(cf_opt->table_factory));
|
|
if (s.ok() && cf_opt->table_factory != nullptr) {
|
|
s = cf_opt->table_factory->ConfigureFromMap(config_options, opt_map);
|
|
// Translate any errors (NotFound, NotSupported, to InvalidArgument
|
|
if (s.ok() || s.IsInvalidArgument()) {
|
|
return s;
|
|
} else {
|
|
return Status::InvalidArgument(s.getState());
|
|
}
|
|
} else {
|
|
// Return OK for not supported table factories as TableFactory
|
|
// Deserialization is optional.
|
|
cf_opt->table_factory.reset();
|
|
return Status::OK();
|
|
}
|
|
} else if (section == kOptionSectionVersion) {
|
|
for (const auto& pair : opt_map) {
|
|
if (pair.first == "rocksdb_version") {
|
|
s = ParseVersionNumber(pair.first, pair.second, 3, db_version);
|
|
if (!s.ok()) {
|
|
return s;
|
|
}
|
|
} else if (pair.first == "options_file_version") {
|
|
s = ParseVersionNumber(pair.first, pair.second, 2, opt_file_version);
|
|
if (!s.ok()) {
|
|
return s;
|
|
}
|
|
if (opt_file_version[0] < 1) {
|
|
return Status::InvalidArgument(
|
|
"A valid options_file_version must be at least 1.");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return s;
|
|
}
|
|
|
|
Status RocksDBOptionsParser::ValidityCheck() {
|
|
if (!has_db_options_) {
|
|
return Status::Corruption(
|
|
"A RocksDB Option file must have a single DBOptions section");
|
|
}
|
|
if (!has_default_cf_options_) {
|
|
return Status::Corruption(
|
|
"A RocksDB Option file must have a single CFOptions:default section");
|
|
}
|
|
|
|
return Status::OK();
|
|
}
|
|
|
|
std::string RocksDBOptionsParser::TrimAndRemoveComment(const std::string& line,
|
|
bool trim_only) {
|
|
size_t start = 0;
|
|
size_t end = line.size();
|
|
|
|
// we only support "#" style comment
|
|
if (!trim_only) {
|
|
size_t search_pos = 0;
|
|
while (search_pos < line.size()) {
|
|
size_t comment_pos = line.find('#', search_pos);
|
|
if (comment_pos == std::string::npos) {
|
|
break;
|
|
}
|
|
if (comment_pos == 0 || line[comment_pos - 1] != '\\') {
|
|
end = comment_pos;
|
|
break;
|
|
}
|
|
search_pos = comment_pos + 1;
|
|
}
|
|
}
|
|
|
|
while (start < end && isspace(line[start]) != 0) {
|
|
++start;
|
|
}
|
|
|
|
// start < end implies end > 0.
|
|
while (start < end && isspace(line[end - 1]) != 0) {
|
|
--end;
|
|
}
|
|
|
|
if (start < end) {
|
|
return line.substr(start, end - start);
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
namespace {
|
|
Status VerifyRocksDBOptionsFromFileImpl(
|
|
const ConfigOptions& config_options_in, const DBOptions& db_opt,
|
|
const std::vector<std::string>& cf_names,
|
|
const std::vector<ColumnFamilyOptions>& cf_opts,
|
|
const std::unordered_set<std::string>* live_cf_names,
|
|
const std::string& file_name, FileSystem* fs) {
|
|
RocksDBOptionsParser parser;
|
|
ConfigOptions config_options = config_options_in;
|
|
config_options.invoke_prepare_options =
|
|
false; // No need to do a prepare for verify
|
|
if (config_options.sanity_level < ConfigOptions::kSanityLevelExactMatch) {
|
|
// If we are not doing an exact comparison, we should ignore
|
|
// unsupported options, as they may cause the Parse to fail
|
|
// (if the ObjectRegistry is not initialized)
|
|
config_options.ignore_unsupported_options = true;
|
|
}
|
|
Status s = parser.Parse(config_options, file_name, fs);
|
|
if (!s.ok()) {
|
|
return s;
|
|
}
|
|
|
|
// Verify DBOptions
|
|
s = RocksDBOptionsParser::VerifyDBOptions(
|
|
config_options, db_opt, *parser.db_opt(), parser.db_opt_map());
|
|
if (!s.ok()) {
|
|
return s;
|
|
}
|
|
|
|
std::vector<size_t> persisted_cf_indices;
|
|
persisted_cf_indices.reserve(parser.cf_names()->size());
|
|
for (size_t i = 0; i < parser.cf_names()->size(); ++i) {
|
|
if (live_cf_names == nullptr ||
|
|
live_cf_names->count(parser.cf_names()->at(i)) != 0) {
|
|
persisted_cf_indices.push_back(i);
|
|
}
|
|
}
|
|
|
|
// Verify ColumnFamily Name
|
|
if (cf_names.size() != persisted_cf_indices.size()) {
|
|
if (config_options.sanity_level >=
|
|
ConfigOptions::kSanityLevelLooselyCompatible) {
|
|
return Status::InvalidArgument(
|
|
"[RocksDBOptionParser Error] The persisted options does not have "
|
|
"the same number of column family names as the db instance.");
|
|
} else if (cf_opts.size() > persisted_cf_indices.size()) {
|
|
return Status::InvalidArgument(
|
|
"[RocksDBOptionsParser Error]",
|
|
"The persisted options file has less number of column family "
|
|
"names than that of the specified one.");
|
|
}
|
|
}
|
|
for (size_t i = 0; i < cf_names.size(); ++i) {
|
|
if (cf_names[i] != parser.cf_names()->at(persisted_cf_indices[i])) {
|
|
return Status::InvalidArgument(
|
|
"[RocksDBOptionParser Error] The persisted options and the db"
|
|
"instance does not have the same name for column family ",
|
|
std::to_string(i));
|
|
}
|
|
}
|
|
|
|
// Verify Column Family Options
|
|
if (cf_opts.size() != persisted_cf_indices.size()) {
|
|
if (config_options.sanity_level >=
|
|
ConfigOptions::kSanityLevelLooselyCompatible) {
|
|
return Status::InvalidArgument(
|
|
"[RocksDBOptionsParser Error]",
|
|
"The persisted options does not have the same number of "
|
|
"column families as the db instance.");
|
|
} else if (cf_opts.size() > persisted_cf_indices.size()) {
|
|
return Status::InvalidArgument(
|
|
"[RocksDBOptionsParser Error]",
|
|
"The persisted options file has less number of column families "
|
|
"than that of the specified number.");
|
|
}
|
|
}
|
|
for (size_t i = 0; i < cf_opts.size(); ++i) {
|
|
const size_t persisted_index = persisted_cf_indices[i];
|
|
s = RocksDBOptionsParser::VerifyCFOptions(
|
|
config_options, cf_opts[i], parser.cf_opts()->at(persisted_index),
|
|
&(parser.cf_opt_maps()->at(persisted_index)));
|
|
if (!s.ok()) {
|
|
return s;
|
|
}
|
|
s = RocksDBOptionsParser::VerifyTableFactory(
|
|
config_options, cf_opts[i].table_factory.get(),
|
|
parser.cf_opts()->at(persisted_index).table_factory.get());
|
|
if (!s.ok()) {
|
|
return s;
|
|
}
|
|
}
|
|
|
|
return Status::OK();
|
|
}
|
|
} // namespace
|
|
|
|
Status RocksDBOptionsParser::VerifyRocksDBOptionsFromFile(
|
|
const ConfigOptions& config_options, const DBOptions& db_opt,
|
|
const std::vector<std::string>& cf_names,
|
|
const std::vector<ColumnFamilyOptions>& cf_opts,
|
|
const std::string& file_name, FileSystem* fs) {
|
|
return VerifyRocksDBOptionsFromFileImpl(config_options, db_opt, cf_names,
|
|
cf_opts, nullptr, file_name, fs);
|
|
}
|
|
|
|
Status RocksDBOptionsParser::VerifyRocksDBOptionsFromFile(
|
|
const ConfigOptions& config_options, const DBOptions& db_opt,
|
|
const std::vector<std::string>& cf_names,
|
|
const std::vector<ColumnFamilyOptions>& cf_opts,
|
|
const std::unordered_set<std::string>& live_cf_names,
|
|
const std::string& file_name, FileSystem* fs) {
|
|
return VerifyRocksDBOptionsFromFileImpl(
|
|
config_options, db_opt, cf_names, cf_opts, &live_cf_names, file_name, fs);
|
|
}
|
|
|
|
Status RocksDBOptionsParser::VerifyDBOptions(
|
|
const ConfigOptions& config_options, const DBOptions& base_opt,
|
|
const DBOptions& file_opt,
|
|
const std::unordered_map<std::string, std::string>* opt_map) {
|
|
auto base_config = DBOptionsAsConfigurable(base_opt, opt_map);
|
|
auto file_config = DBOptionsAsConfigurable(file_opt, opt_map);
|
|
std::string mismatch;
|
|
if (!base_config->AreEquivalent(config_options, file_config.get(),
|
|
&mismatch)) {
|
|
const size_t kBufferSize = 2048;
|
|
char buffer[kBufferSize];
|
|
std::string base_value;
|
|
std::string file_value;
|
|
int offset = snprintf(buffer, sizeof(buffer),
|
|
"[RocksDBOptionsParser]: "
|
|
"failed the verification on DBOptions::%s -- ",
|
|
mismatch.c_str());
|
|
Status s = base_config->GetOption(config_options, mismatch, &base_value);
|
|
if (s.ok()) {
|
|
s = file_config->GetOption(config_options, mismatch, &file_value);
|
|
}
|
|
assert(offset >= 0);
|
|
assert(static_cast<size_t>(offset) < sizeof(buffer));
|
|
if (s.ok()) {
|
|
snprintf(buffer + offset, sizeof(buffer) - static_cast<size_t>(offset),
|
|
"-- The specified one is %s while the persisted one is %s.\n",
|
|
base_value.c_str(), file_value.c_str());
|
|
} else {
|
|
snprintf(buffer + offset, sizeof(buffer) - static_cast<size_t>(offset),
|
|
"-- Unable to re-serialize an option: %s.\n",
|
|
s.ToString().c_str());
|
|
}
|
|
return Status::InvalidArgument(Slice(buffer, strlen(buffer)));
|
|
}
|
|
return Status::OK();
|
|
}
|
|
|
|
Status RocksDBOptionsParser::VerifyCFOptions(
|
|
const ConfigOptions& config_options, const ColumnFamilyOptions& base_opt,
|
|
const ColumnFamilyOptions& file_opt,
|
|
const std::unordered_map<std::string, std::string>* opt_map) {
|
|
auto base_config = CFOptionsAsConfigurable(base_opt, opt_map);
|
|
auto file_config = CFOptionsAsConfigurable(file_opt, opt_map);
|
|
std::string mismatch;
|
|
if (!base_config->AreEquivalent(config_options, file_config.get(),
|
|
&mismatch)) {
|
|
std::string base_value;
|
|
std::string file_value;
|
|
// The options do not match
|
|
const size_t kBufferSize = 2048;
|
|
char buffer[kBufferSize];
|
|
Status s = base_config->GetOption(config_options, mismatch, &base_value);
|
|
if (s.ok()) {
|
|
s = file_config->GetOption(config_options, mismatch, &file_value);
|
|
// In file_opt, certain options like MergeOperator may be nullptr due to
|
|
// factor methods not available. So we use opt_map to get
|
|
// option value to use in the error message below.
|
|
if (s.ok() && file_value == kNullptrString && opt_map) {
|
|
auto const& opt_val_str = (opt_map->find(mismatch));
|
|
if (opt_val_str != opt_map->end()) {
|
|
file_value = opt_val_str->second;
|
|
}
|
|
}
|
|
}
|
|
int offset = snprintf(buffer, sizeof(buffer),
|
|
"[RocksDBOptionsParser]: "
|
|
"failed the verification on ColumnFamilyOptions::%s",
|
|
mismatch.c_str());
|
|
assert(offset >= 0);
|
|
assert(static_cast<size_t>(offset) < sizeof(buffer));
|
|
if (s.ok()) {
|
|
snprintf(buffer + offset, sizeof(buffer) - static_cast<size_t>(offset),
|
|
"--- The specified one is %s while the persisted one is %s.\n",
|
|
base_value.c_str(), file_value.c_str());
|
|
} else {
|
|
snprintf(buffer + offset, sizeof(buffer) - static_cast<size_t>(offset),
|
|
"--- Unable to re-serialize an option: %s.\n",
|
|
s.ToString().c_str());
|
|
}
|
|
return Status::InvalidArgument(Slice(buffer, sizeof(buffer)));
|
|
} // For each option
|
|
return Status::OK();
|
|
}
|
|
|
|
Status RocksDBOptionsParser::VerifyTableFactory(
|
|
const ConfigOptions& config_options, const TableFactory* base_tf,
|
|
const TableFactory* file_tf) {
|
|
std::string mismatch;
|
|
if (base_tf && file_tf) {
|
|
if (config_options.sanity_level > ConfigOptions::kSanityLevelNone &&
|
|
std::string(base_tf->Name()) != std::string(file_tf->Name())) {
|
|
return Status::Corruption(
|
|
"[RocksDBOptionsParser]: "
|
|
"failed the verification on TableFactory->Name()");
|
|
} else if (!base_tf->AreEquivalent(config_options, file_tf, &mismatch)) {
|
|
return Status::Corruption(std::string("[RocksDBOptionsParser]:"
|
|
"failed the verification on ") +
|
|
base_tf->Name() + "::",
|
|
mismatch);
|
|
}
|
|
} else {
|
|
// TODO(yhchiang): further support sanity check here
|
|
}
|
|
return Status::OK();
|
|
}
|
|
} // namespace ROCKSDB_NAMESPACE
|