Compare commits

...

4 Commits

Author SHA1 Message Date
Josh Kang 1fba33d246 Update
[ghstack-poisoned]
2026-06-08 15:57:55 -07:00
Josh Kang 5853adab23 Update (base update)
[ghstack-poisoned]
2026-06-08 15:57:55 -07:00
Josh Kang b63b4f8c45 Update
[ghstack-poisoned]
2026-06-05 16:52:16 -07:00
Josh Kang 7161711e8c Update (base update)
[ghstack-poisoned]
2026-06-05 16:52:16 -07:00
19 changed files with 765 additions and 132 deletions
+41 -4
View File
@@ -6675,6 +6675,13 @@ Status DBImpl::IngestExternalFile(
Status DBImpl::IngestExternalFiles(
const std::vector<IngestExternalFileArg>& args) {
PERF_TIMER_GUARD(file_ingestion_nanos);
// Prepare/run ingestion latency, recorded only on success (below).
const bool record_ingest_micros =
stats_ != nullptr &&
stats_->get_stats_level() > StatsLevel::kExceptTimers;
const uint64_t ingest_start_micros =
record_ingest_micros ? immutable_db_options_.clock->NowMicros() : 0;
uint64_t prepare_micros = 0;
// TODO: plumb Env::IOActivity, Env::IOPriority
const WriteOptions write_options;
@@ -6701,6 +6708,19 @@ Status DBImpl::IngestExternalFiles(
"external_files[" + std::to_string(i) + "] is empty";
return Status::InvalidArgument(err_msg);
}
if (!args[i].file_infos.empty()) {
if (args[i].file_infos.size() != args[i].external_files.size()) {
return Status::InvalidArgument("file_infos[" + std::to_string(i) +
"] size must match external_files[" +
std::to_string(i) + "] size");
}
// file_infos avoids opening the file, so it cannot write a global seqno
// back into it. (write_global_seqno is deprecated and defaults to false.)
if (args[i].options.write_global_seqno) {
return Status::InvalidArgument(
"write_global_seqno is not supported when file_infos is set");
}
}
if (i && args[i].options.fill_cache != args[i - 1].options.fill_cache) {
return Status::InvalidArgument(
"fill_cache should be the same across ingestion options.");
@@ -6794,8 +6814,9 @@ Status DBImpl::IngestExternalFiles(
this);
Status es = ingestion_jobs[i].Prepare(
args[i].external_files, args[i].files_checksums,
args[i].files_checksum_func_names, args[i].atomic_replace_range,
args[i].file_temperature, start_file_number, super_version);
args[i].files_checksum_func_names, args[i].file_infos,
args[i].atomic_replace_range, args[i].file_temperature,
start_file_number, super_version);
// capture first error only
if (!es.ok() && status.ok()) {
status = es;
@@ -6810,8 +6831,9 @@ Status DBImpl::IngestExternalFiles(
this);
Status es = ingestion_jobs[0].Prepare(
args[0].external_files, args[0].files_checksums,
args[0].files_checksum_func_names, args[0].atomic_replace_range,
args[0].file_temperature, next_file_number, super_version);
args[0].files_checksum_func_names, args[0].file_infos,
args[0].atomic_replace_range, args[0].file_temperature,
next_file_number, super_version);
if (!es.ok()) {
status = es;
}
@@ -6826,6 +6848,13 @@ Status DBImpl::IngestExternalFiles(
return status;
}
// End of prepare phase; run phase starts here.
uint64_t run_start_micros = 0;
if (record_ingest_micros) {
run_start_micros = immutable_db_options_.clock->NowMicros();
prepare_micros = run_start_micros - ingest_start_micros;
}
std::vector<SuperVersionContext> sv_ctxs;
for (size_t i = 0; i != num_cfs; ++i) {
sv_ctxs.emplace_back(true /* create_superversion */);
@@ -7067,6 +7096,14 @@ Status DBImpl::IngestExternalFiles(
}
}
}
// Record latency only for successful ingestions.
if (record_ingest_micros && status.ok()) {
RecordTimeToHistogram(stats_, INGEST_EXTERNAL_FILE_PREPARE_TIME,
prepare_micros);
RecordTimeToHistogram(
stats_, INGEST_EXTERNAL_FILE_RUN_TIME,
immutable_db_options_.clock->NowMicros() - run_start_micros);
}
return status;
}
+202 -67
View File
@@ -31,18 +31,22 @@ Status ExternalSstFileIngestionJob::Prepare(
const std::vector<std::string>& external_files_paths,
const std::vector<std::string>& files_checksums,
const std::vector<std::string>& files_checksum_func_names,
const std::vector<ExternalSstFileInfo>& file_infos,
const std::optional<RangeOpt>& atomic_replace_range,
const Temperature& file_temperature, uint64_t next_file_number,
SuperVersion* sv) {
Status status;
// Read the information of files we are ingesting
for (const std::string& file_path : external_files_paths) {
// Read the information of files we are ingesting.
for (size_t i = 0; i < external_files_paths.size(); i++) {
const std::string& file_path = external_files_paths[i];
IngestedFileInfo file_to_ingest;
// For temperature, first assume it matches provided hint
file_to_ingest.file_temperature = file_temperature;
status =
GetIngestedFileInfo(file_path, next_file_number++, &file_to_ingest, sv);
const ExternalSstFileInfo* file_info =
file_infos.empty() ? nullptr : &file_infos[i];
status = GetIngestedFileInfo(file_path, next_file_number++, file_info,
&file_to_ingest, sv);
if (!status.ok()) {
ROCKS_LOG_WARN(db_options_.info_log,
"Failed to get ingested file info: %s: %s",
@@ -970,18 +974,14 @@ Status ExternalSstFileIngestionJob::ResetTableReader(
}
Status ExternalSstFileIngestionJob::SanityCheckTableProperties(
const std::string& external_file, uint64_t new_file_number,
SuperVersion* sv, IngestedFileInfo* file_to_ingest,
std::unique_ptr<TableReader>* table_reader) {
// Get the external file properties
auto props = table_reader->get()->GetTableProperties();
assert(props.get());
const auto& uprops = props->user_collected_properties;
const std::string& external_file, const TableProperties& props,
IngestedFileInfo* file_to_ingest) {
const auto& uprops = props.user_collected_properties;
// Get table version
auto version_iter = uprops.find(ExternalSstFilePropertyNames::kVersion);
if (version_iter == uprops.end()) {
assert(!SstFileWriter::CreatedBySstFileWriter(*props));
assert(!SstFileWriter::CreatedBySstFileWriter(props));
if (!ingestion_options_.allow_db_generated_files) {
return Status::Corruption("External file version not found");
} else {
@@ -990,7 +990,7 @@ Status ExternalSstFileIngestionJob::SanityCheckTableProperties(
file_to_ingest->version = 0;
}
} else {
assert(SstFileWriter::CreatedBySstFileWriter(*props));
assert(SstFileWriter::CreatedBySstFileWriter(props));
file_to_ingest->version = DecodeFixed32(version_iter->second.c_str());
}
@@ -1004,12 +1004,17 @@ Status ExternalSstFileIngestionJob::SanityCheckTableProperties(
// Set the global sequence number
file_to_ingest->original_seqno = DecodeFixed64(seqno_iter->second.c_str());
if (props->external_sst_file_global_seqno_offset == 0) {
file_to_ingest->global_seqno_offset = 0;
file_to_ingest->global_seqno_offset =
static_cast<size_t>(props.external_sst_file_global_seqno_offset);
// The on-disk offset is only needed if we will write the global seqno back
// into the file (write_global_seqno). The metadata fast-path does not open
// the file, and its in-memory table properties do not carry the offset
// (it is only computed while reading the file back); that is fine as long
// as write_global_seqno is not requested.
if (ingestion_options_.write_global_seqno &&
file_to_ingest->global_seqno_offset == 0) {
return Status::Corruption("Was not able to find file global seqno field");
}
file_to_ingest->global_seqno_offset =
static_cast<size_t>(props->external_sst_file_global_seqno_offset);
} else if (file_to_ingest->version == 1) {
// SST file V1 should not have global seqno field
assert(seqno_iter == uprops.end());
@@ -1030,23 +1035,20 @@ Status ExternalSstFileIngestionJob::SanityCheckTableProperties(
" is not supported");
}
file_to_ingest->cf_id = static_cast<uint32_t>(props->column_family_id);
// This assignment works fine even though `table_reader` may later be reset,
// since that will not affect how table properties are parsed, and this
// assignment is making a copy.
file_to_ingest->table_properties = *props;
file_to_ingest->cf_id = static_cast<uint32_t>(props.column_family_id);
file_to_ingest->table_properties = props;
// Get number of entries in table
file_to_ingest->num_entries = props->num_entries;
file_to_ingest->num_range_deletions = props->num_range_deletions;
file_to_ingest->num_entries = props.num_entries;
file_to_ingest->num_range_deletions = props.num_range_deletions;
// Validate table properties related to comparator name and user defined
// timestamps persisted flag.
file_to_ingest->user_defined_timestamps_persisted =
static_cast<bool>(props->user_defined_timestamps_persisted);
static_cast<bool>(props.user_defined_timestamps_persisted);
bool mark_sst_file_has_no_udt = false;
Status s = ValidateUserDefinedTimestampsOptions(
cfd_->user_comparator(), props->comparator_name,
cfd_->user_comparator(), props.comparator_name,
cfd_->ioptions().persist_user_defined_timestamps,
file_to_ingest->user_defined_timestamps_persisted,
&mark_sst_file_has_no_udt);
@@ -1054,7 +1056,9 @@ Status ExternalSstFileIngestionJob::SanityCheckTableProperties(
// A column family that enables user-defined timestamps in Memtable only
// feature can also ingest external files created by a setting that disables
// user-defined timestamps. In that case, we need to re-mark the
// user_defined_timestamps_persisted flag for the file.
// user_defined_timestamps_persisted flag for the file. The open-and-scan
// caller is then responsible for reopening its `TableReader` with the
// updated flag.
file_to_ingest->user_defined_timestamps_persisted = false;
} else if (!s.ok()) {
ROCKS_LOG_WARN(
@@ -1064,22 +1068,88 @@ Status ExternalSstFileIngestionJob::SanityCheckTableProperties(
return s;
}
// `TableReader` is initialized with `user_defined_timestamps_persisted` flag
// to be true. If its value changed to false after this sanity check, we
// need to reset the `TableReader`.
if (ucmp_->timestamp_size() > 0 &&
!file_to_ingest->user_defined_timestamps_persisted) {
s = ResetTableReader(external_file, new_file_number,
file_to_ingest->user_defined_timestamps_persisted, sv,
file_to_ingest, table_reader);
}
return s;
}
Status ExternalSstFileIngestionJob::GetIngestedFileInfo(
Status ExternalSstFileIngestionJob::GetIngestedFileInfoFromFileInfo(
const std::string& external_file, const ExternalSstFileInfo* file_info,
IngestedFileInfo* file_to_ingest) {
// Boundaries, size, and table properties were obtained without opening the
// file (e.g. produced by SstFileWriter::Finish). We reuse them directly.
file_to_ingest->file_size = file_info->file_size;
file_to_ingest->smallest_internal_key.DecodeFrom(
file_info->smallest_internal_key);
file_to_ingest->largest_internal_key.DecodeFrom(
file_info->largest_internal_key);
Status status = SanityCheckTableProperties(
external_file, file_info->table_properties, file_to_ingest);
if (!status.ok()) {
ROCKS_LOG_WARN(
db_options_.info_log,
"Failed to sanity check table properties for external file %s: %s",
external_file.c_str(), status.ToString().c_str());
return status;
}
// When the column family enables user-defined timestamps but this file was
// written without persisting them (the "UDT in Memtables only" feature), the
// supplied boundary keys carry no timestamp (SstFileWriter strips it). Pad
// them with the min timestamp so they match the format the open-and-scan path
// produces (its reopened `TableReader` pads keys the same way) and the bounds
// recorded in the new FileMetaData.
const size_t ts_sz = ucmp_->timestamp_size();
if (ts_sz > 0 && !file_to_ingest->user_defined_timestamps_persisted) {
std::string padded;
PadInternalKeyWithMinTimestamp(
&padded, file_to_ingest->smallest_internal_key.Encode(), ts_sz);
file_to_ingest->smallest_internal_key.DecodeFrom(padded);
padded.clear();
PadInternalKeyWithMinTimestamp(
&padded, file_to_ingest->largest_internal_key.Encode(), ts_sz);
file_to_ingest->largest_internal_key.DecodeFrom(padded);
}
// Extend the key bounds to cover any range tombstones. Their bounds are not
// part of the point-key internal keys, but the file's recorded range must
// include them (the open-and-scan path does this while scanning tombstones).
// SstFileWriter writes range tombstones at sequence number 0.
if (!file_info->smallest_range_del_key.empty()) {
RangeTombstone tombstone(Slice(file_info->smallest_range_del_key),
Slice(file_info->largest_range_del_key),
/*sn=*/0);
file_range_checker_.MaybeUpdateRange(
tombstone.SerializeKey(), tombstone.SerializeEndKey(), file_to_ingest);
}
if (ingestion_options_.allow_db_generated_files) {
// Sequence numbers are preserved (not reassigned), so the bounds come from
// the supplied table properties; the GetSeqnoBoundaryForFile scan is
// skipped.
file_to_ingest->smallest_seqno =
file_to_ingest->table_properties.key_smallest_seqno;
file_to_ingest->largest_seqno =
file_to_ingest->table_properties.key_largest_seqno;
} else {
// Normal ingestion reassigns a global sequence number later, so the file's
// keys must currently be at seqno 0 (mirror of the open-and-scan check).
SequenceNumber largest_seqno =
file_to_ingest->table_properties.key_largest_seqno;
// UINT64_MAX means unknown and the file is generated before table property
// `key_largest_seqno` is introduced.
if (largest_seqno != UINT64_MAX && largest_seqno > 0) {
return Status::Corruption(
"External file has non zero largest sequence number " +
std::to_string(largest_seqno));
}
}
return Status::OK();
}
Status ExternalSstFileIngestionJob::GetIngestedFileInfoFromFile(
const std::string& external_file, uint64_t new_file_number,
IngestedFileInfo* file_to_ingest, SuperVersion* sv) {
file_to_ingest->external_file_path = external_file;
IngestedFileInfo* file_to_ingest, SuperVersion* sv,
std::unique_ptr<TableReader>* out_table_reader) {
TEST_SYNC_POINT("ExternalSstFileIngestionJob::GetIngestedFileInfo:ReadPath");
// Get external file size
Status status = fs_->GetFileSize(external_file, IOOptions(),
@@ -1091,15 +1161,11 @@ Status ExternalSstFileIngestionJob::GetIngestedFileInfo(
return status;
}
// Assign FD with number
file_to_ingest->fd =
FileDescriptor(new_file_number, 0, file_to_ingest->file_size);
// Create TableReader for external file
// Create TableReader for external file.
std::unique_ptr<TableReader> table_reader;
// Initially create the `TableReader` with flag
// `user_defined_timestamps_persisted` to be true since that's the most common
// case
// `user_defined_timestamps_persisted` to be true since that's the most
// common case
status = ResetTableReader(external_file, new_file_number,
/*user_defined_timestamps_persisted=*/true, sv,
file_to_ingest, &table_reader);
@@ -1110,8 +1176,8 @@ Status ExternalSstFileIngestionJob::GetIngestedFileInfo(
return status;
}
status = SanityCheckTableProperties(external_file, new_file_number, sv,
file_to_ingest, &table_reader);
status = SanityCheckTableProperties(
external_file, *table_reader->GetTableProperties(), file_to_ingest);
if (!status.ok()) {
ROCKS_LOG_WARN(
db_options_.info_log,
@@ -1120,6 +1186,23 @@ Status ExternalSstFileIngestionJob::GetIngestedFileInfo(
return status;
}
// The `TableReader` above was opened with `user_defined_timestamps_persisted`
// assumed true. If the sanity check determined the file has no persisted
// timestamps (UDT-in-Memtable-only feature), reopen it with the corrected
// flag so keys are parsed properly by the scan below.
if (ucmp_->timestamp_size() > 0 &&
!file_to_ingest->user_defined_timestamps_persisted) {
status = ResetTableReader(external_file, new_file_number,
file_to_ingest->user_defined_timestamps_persisted,
sv, file_to_ingest, &table_reader);
if (!status.ok()) {
ROCKS_LOG_WARN(db_options_.info_log,
"Failed to reset table reader for external file %s: %s",
external_file.c_str(), status.ToString().c_str());
return status;
}
}
const bool allow_data_in_errors = db_options_.allow_data_in_errors;
ParsedInternalKey key;
if (ingestion_options_.allow_db_generated_files) {
@@ -1141,8 +1224,8 @@ Status ExternalSstFileIngestionJob::GetIngestedFileInfo(
} else {
SequenceNumber largest_seqno =
table_reader.get()->GetTableProperties()->key_largest_seqno;
// UINT64_MAX means unknown and the file is generated before table property
// `key_largest_seqno` is introduced.
// UINT64_MAX means unknown and the file is generated before table
// property `key_largest_seqno` is introduced.
if (largest_seqno != UINT64_MAX && largest_seqno > 0) {
return Status::Corruption(
"External file has non zero largest sequence number " +
@@ -1150,23 +1233,6 @@ Status ExternalSstFileIngestionJob::GetIngestedFileInfo(
}
}
if (ingestion_options_.verify_checksums_before_ingest) {
// If customized readahead size is needed, we can pass a user option
// all the way to here. Right now we just rely on the default readahead
// to keep things simple.
// TODO: plumb Env::IOActivity, Env::IOPriority
ReadOptions ro;
ro.readahead_size = ingestion_options_.verify_checksums_readahead_size;
ro.fill_cache = ingestion_options_.fill_cache;
status = table_reader->VerifyChecksum(
ro, TableReaderCaller::kExternalSSTIngestion);
if (!status.ok()) {
ROCKS_LOG_WARN(db_options_.info_log,
"Failed to verify checksum for table reader: %s",
status.ToString().c_str());
return status;
}
}
// TODO: plumb Env::IOActivity, Env::IOPriority
ReadOptions ro;
@@ -1254,6 +1320,75 @@ Status ExternalSstFileIngestionJob::GetIngestedFileInfo(
}
}
*out_table_reader = std::move(table_reader);
return Status::OK();
}
Status ExternalSstFileIngestionJob::GetIngestedFileInfo(
const std::string& external_file, uint64_t new_file_number,
const ExternalSstFileInfo* file_info, IngestedFileInfo* file_to_ingest,
SuperVersion* sv) {
file_to_ingest->external_file_path = external_file;
std::unique_ptr<TableReader> table_reader;
Status status =
file_info != nullptr
? GetIngestedFileInfoFromFileInfo(external_file, file_info,
file_to_ingest)
: GetIngestedFileInfoFromFile(external_file, new_file_number,
file_to_ingest, sv, &table_reader);
if (!status.ok()) {
return status;
}
assert(file_to_ingest->file_size > 0);
assert(!file_to_ingest->unset());
assert(file_to_ingest->table_properties.num_entries > 0 ||
file_to_ingest->table_properties.num_range_deletions > 0);
if (ingestion_options_.allow_db_generated_files) {
// These files keep their original sequence numbers (derived, not
// reassigned), so the bounds must be valid here.
assert(file_to_ingest->smallest_seqno <= file_to_ingest->largest_seqno);
assert(file_to_ingest->largest_seqno < kMaxSequenceNumber);
}
// Verify the file checksum if requested. The open-and-scan path already has a
// `TableReader` open; the fast-path opens one here only when verification is
// requested -- otherwise it performs no file I/O.
if (ingestion_options_.verify_checksums_before_ingest) {
if (table_reader == nullptr) {
status =
ResetTableReader(external_file, new_file_number,
file_to_ingest->user_defined_timestamps_persisted,
sv, file_to_ingest, &table_reader);
if (!status.ok()) {
ROCKS_LOG_WARN(db_options_.info_log,
"Failed to reset table reader for external file %s: %s",
external_file.c_str(), status.ToString().c_str());
return status;
}
}
// If customized readahead size is needed, we can pass a user option all the
// way to here. Right now we just rely on the default readahead to keep
// things simple.
// TODO: plumb Env::IOActivity, Env::IOPriority
ReadOptions ro;
ro.readahead_size = ingestion_options_.verify_checksums_readahead_size;
ro.fill_cache = ingestion_options_.fill_cache;
status = table_reader->VerifyChecksum(
ro, TableReaderCaller::kExternalSSTIngestion);
if (!status.ok()) {
ROCKS_LOG_WARN(db_options_.info_log,
"Failed to verify checksum for external file %s: %s",
external_file.c_str(), status.ToString().c_str());
return status;
}
}
// Assign FD with number.
file_to_ingest->fd =
FileDescriptor(new_file_number, 0, file_to_ingest->file_size);
const size_t ts_sz = ucmp_->timestamp_size();
Slice smallest = file_to_ingest->smallest_internal_key.user_key();
Slice largest = file_to_ingest->largest_internal_key.user_key();
+21 -3
View File
@@ -248,6 +248,7 @@ class ExternalSstFileIngestionJob {
Status Prepare(const std::vector<std::string>& external_files_paths,
const std::vector<std::string>& files_checksums,
const std::vector<std::string>& files_checksum_func_names,
const std::vector<ExternalSstFileInfo>& file_infos,
const std::optional<RangeOpt>& atomic_replace_range,
const Temperature& file_temperature, uint64_t next_file_number,
SuperVersion* sv);
@@ -315,17 +316,34 @@ class ExternalSstFileIngestionJob {
// different options. For example: when external file does not contain
// timestamps while column family enables UDT in Memtables only feature.
Status SanityCheckTableProperties(const std::string& external_file,
uint64_t new_file_number, SuperVersion* sv,
IngestedFileInfo* file_to_ingest,
std::unique_ptr<TableReader>* table_reader);
const TableProperties& props,
IngestedFileInfo* file_to_ingest);
// Open the external file and populate `file_to_ingest` with all the
// external information we need to ingest this file.
Status GetIngestedFileInfo(const std::string& external_file,
uint64_t new_file_number,
const ExternalSstFileInfo* file_info,
IngestedFileInfo* file_to_ingest,
SuperVersion* sv);
// Acquire the per-file metadata from caller-supplied `file_info` instead
// of opening the file (e.g. the ExternalSstFileInfo produced by
// SstFileWriter::Finish).
Status GetIngestedFileInfoFromFileInfo(const std::string& external_file,
const ExternalSstFileInfo* file_info,
IngestedFileInfo* file_to_ingest);
// Acquire the per-file metadata by opening the external file and scanning it
// (table properties, sequence number bounds, and boundary keys including any
// range-tombstone extensions). Used when no file_info is available. The
// opened `TableReader` is returned via `*table_reader` so the caller can
// reuse it (e.g. to verify the file checksum) without re-opening the file.
Status GetIngestedFileInfoFromFile(
const std::string& external_file, uint64_t new_file_number,
IngestedFileInfo* file_to_ingest, SuperVersion* sv,
std::unique_ptr<TableReader>* out_table_reader);
// If the input files' key range overlaps themselves, this function divides
// them in the user specified order into multiple batches. Where the files
// within a batch do not overlap with each other, but key range could overlap
+189 -4
View File
@@ -152,7 +152,8 @@ class ExternalSSTFileTest
bool verify_checksums_before_ingest = true, bool ingest_behind = false,
bool sort_data = false,
std::map<std::string, std::string>* true_data = nullptr,
ColumnFamilyHandle* cfh = nullptr, bool fill_cache = false) {
ColumnFamilyHandle* cfh = nullptr, bool fill_cache = false,
bool ingest_with_file_info = false) {
// Generate a file id if not provided
if (file_id == -1) {
file_id = last_file_id_ + 1;
@@ -188,7 +189,8 @@ class ExternalSSTFileTest
return s;
}
}
s = sst_file_writer.Finish();
ExternalSstFileInfo file_info;
s = sst_file_writer.Finish(&file_info);
if (s.ok()) {
IngestExternalFileOptions ifo;
@@ -197,7 +199,14 @@ class ExternalSSTFileTest
ifo.verify_checksums_before_ingest = verify_checksums_before_ingest;
ifo.ingest_behind = ingest_behind;
ifo.fill_cache = fill_cache;
if (cfh) {
if (ingest_with_file_info) {
IngestExternalFileArg arg;
arg.column_family = cfh ? cfh : db_->DefaultColumnFamily();
arg.external_files = {file_path};
arg.file_infos = {file_info};
arg.options = ifo;
s = db_->IngestExternalFiles({arg});
} else if (cfh) {
s = db_->IngestExternalFile(cfh, {file_path}, ifo);
} else {
s = db_->IngestExternalFile({file_path}, ifo);
@@ -298,6 +307,181 @@ class ExternalSSTFileTest
int last_file_id_ = 0;
};
TEST_F(ExternalSSTFileTest, IngestionTimingHistogram) {
Options options = CurrentOptions();
options.statistics = CreateDBStatistics();
options.statistics->set_stats_level(StatsLevel::kAll); // enable timers
DestroyAndReopen(options);
ASSERT_OK(GenerateAndAddExternalFile(
options, {{"k1", "v1"}, {"k2", "v2"}, {"k3", "v3"}}, /*file_id=*/-1,
/*allow_global_seqno=*/true, /*write_global_seqno=*/false,
/*verify_checksums_before_ingest=*/true, /*ingest_behind=*/false,
/*sort_data=*/true));
auto hist = [&](Histograms h) {
HistogramData hd;
options.statistics->histogramData(h, &hd);
return hd;
};
// One sample per call (not per CF) in each phase histogram.
HistogramData prepare_hd = hist(INGEST_EXTERNAL_FILE_PREPARE_TIME);
HistogramData run_hd = hist(INGEST_EXTERNAL_FILE_RUN_TIME);
ASSERT_EQ(1, prepare_hd.count);
ASSERT_EQ(1, run_hd.count);
ASSERT_GT(prepare_hd.max, 0.0);
ASSERT_GT(run_hd.max, 0.0);
// A second call adds exactly one more sample to each histogram.
ASSERT_OK(GenerateAndAddExternalFile(
options, {{"k4", "v4"}, {"k5", "v5"}}, /*file_id=*/-1,
/*allow_global_seqno=*/true, /*write_global_seqno=*/false,
/*verify_checksums_before_ingest=*/true, /*ingest_behind=*/false,
/*sort_data=*/true));
ASSERT_EQ(2, hist(INGEST_EXTERNAL_FILE_PREPARE_TIME).count);
ASSERT_EQ(2, hist(INGEST_EXTERNAL_FILE_RUN_TIME).count);
// A failed ingestion records nothing (latency is logged only on success).
ASSERT_NOK(db_->IngestExternalFile({"/path/does/not/exist.sst"},
IngestExternalFileOptions()));
ASSERT_EQ(2, hist(INGEST_EXTERNAL_FILE_PREPARE_TIME).count);
ASSERT_EQ(2, hist(INGEST_EXTERNAL_FILE_RUN_TIME).count);
}
// Ingestion that reuses the SstFileWriter's metadata via
// IngestExternalFileArg::file_infos skips the open-and-scan path and still
// produces correct data, including overlapping data that is reassigned a new
// global sequence number.
TEST_F(ExternalSSTFileTest, IngestWithFileInfo) {
Options options = CurrentOptions();
DestroyAndReopen(options);
std::map<std::string, std::string> true_data;
// Count entries into the open-and-scan path so we can assert it is skipped.
std::atomic<int> read_path_count{0};
SyncPoint::GetInstance()->SetCallBack(
"ExternalSstFileIngestionJob::GetIngestedFileInfo:ReadPath",
[&](void*) { read_path_count.fetch_add(1); });
SyncPoint::GetInstance()->EnableProcessing();
// Reuse the writer's metadata, with the normal ingestion options: a global
// sequence number is assigned (allow_db_generated_files is NOT set) and the
// checksum is verified (the fast-path opens the file only to verify it, which
// is not a metadata scan).
auto ingest_fi = [&](std::vector<std::pair<std::string, std::string>> data) {
return GenerateAndAddExternalFile(
options, std::move(data), /*file_id=*/-1, /*allow_global_seqno=*/true,
/*write_global_seqno=*/false, /*verify_checksums_before_ingest=*/true,
/*ingest_behind=*/false, /*sort_data=*/true, &true_data,
/*cfh=*/nullptr, /*fill_cache=*/false, /*ingest_with_file_info=*/true);
};
// Non-overlapping ingest: data reads back and the file is not scanned.
ASSERT_OK(ingest_fi({{Key(1), "a1"}, {Key(2), "a2"}, {Key(3), "a3"}}));
ASSERT_EQ(read_path_count.load(), 0);
// Overlap existing (flushed) data so a non-zero global seqno is assigned and
// the newer values win -- the normal seqno-reassignment path through
// file_infos.
ASSERT_OK(db_->Put(WriteOptions(), Key(2), "old2"));
true_data[Key(2)] = "old2";
ASSERT_OK(Flush());
ASSERT_OK(ingest_fi({{Key(2), "new2"}, {Key(4), "new4"}}));
ASSERT_EQ(read_path_count.load(), 0);
// Control: ingesting the same way but WITHOUT file_infos does enter the scan.
ASSERT_OK(GenerateAndAddExternalFile(
options, {{Key(8), "c8"}}, /*file_id=*/-1, /*allow_global_seqno=*/true,
/*write_global_seqno=*/false, /*verify_checksums_before_ingest=*/true,
/*ingest_behind=*/false, /*sort_data=*/true, &true_data));
ASSERT_GT(read_path_count.load(), 0);
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
size_t kcnt = 0;
VerifyDBFromMap(true_data, &kcnt, false);
}
// Range deletions are honored on the file_infos fast-path: the writer's
// range-del bounds are carried in the metadata, so a DeleteRange covering
// existing keys still shadows them.
TEST_F(ExternalSSTFileTest, IngestWithFileInfoRangeDeletion) {
Options options = CurrentOptions();
DestroyAndReopen(options);
ASSERT_OK(db_->Put(WriteOptions(), Key(20), "live20"));
ASSERT_OK(db_->Put(WriteOptions(), Key(25), "live25"));
ASSERT_OK(Flush());
const std::string f = sst_files_dir_ + "rangedel.sst";
SstFileWriter w(EnvOptions(), options);
ASSERT_OK(w.Open(f));
ASSERT_OK(w.Put(Key(30), "v30"));
ASSERT_OK(w.DeleteRange(Key(20), Key(26))); // covers keys 20..25
ExternalSstFileInfo info;
ASSERT_OK(w.Finish(&info));
IngestExternalFileArg arg;
arg.column_family = db_->DefaultColumnFamily();
arg.external_files = {f};
arg.file_infos = {info};
arg.options.allow_global_seqno = true;
ASSERT_OK(db_->IngestExternalFiles({arg}));
ReadOptions ro;
std::string val;
ASSERT_TRUE(db_->Get(ro, Key(20), &val).IsNotFound());
ASSERT_TRUE(db_->Get(ro, Key(25), &val).IsNotFound());
ASSERT_OK(db_->Get(ro, Key(30), &val));
ASSERT_EQ(val, "v30");
}
// verify_checksums_before_ingest is honored on the file_infos fast-path: it
// opens the file just to verify, so corruption is still detected.
TEST_F(ExternalSSTFileTest, IngestWithFileInfoVerifiesChecksum) {
Options options = CurrentOptions();
DestroyAndReopen(options);
const std::string f = sst_files_dir_ + "corrupt.sst";
SstFileWriter w(EnvOptions(), options);
ASSERT_OK(w.Open(f));
for (int k = 0; k < 100; k++) {
ASSERT_OK(w.Put(Key(k), "v" + Key(k)));
}
ExternalSstFileInfo info;
ASSERT_OK(w.Finish(&info));
ASSERT_OK(test::CorruptFile(options.env, f, /*offset=*/64,
/*bytes_to_corrupt=*/8));
IngestExternalFileArg arg;
arg.column_family = db_->DefaultColumnFamily();
arg.external_files = {f};
arg.file_infos = {info};
arg.options.verify_checksums_before_ingest = true;
ASSERT_TRUE(db_->IngestExternalFiles({arg}).IsCorruption());
}
// write_global_seqno is incompatible with file_infos: the file is not opened,
// so the seqno cannot be written back into it.
TEST_F(ExternalSSTFileTest, IngestWithFileInfoRejectsWriteGlobalSeqno) {
Options options = CurrentOptions();
DestroyAndReopen(options);
const std::string f = sst_files_dir_ + "wgs.sst";
SstFileWriter w(EnvOptions(), options);
ASSERT_OK(w.Open(f));
ASSERT_OK(w.Put(Key(1), "v1"));
ExternalSstFileInfo info;
ASSERT_OK(w.Finish(&info));
IngestExternalFileArg arg;
arg.column_family = db_->DefaultColumnFamily();
arg.external_files = {f};
arg.file_infos = {info};
arg.options.write_global_seqno = true;
ASSERT_TRUE(db_->IngestExternalFiles({arg}).IsInvalidArgument());
}
TEST_F(ExternalSSTFileTest, ComparatorMismatch) {
Options options = CurrentOptions();
Options options_diff_ucmp = options;
@@ -1816,7 +2000,8 @@ TEST_P(ExternalSSTFileTest, IngestFileWithGlobalSeqnoRandomized) {
for (int i = 0; i < 500; i++) {
std::vector<std::pair<std::string, std::string>> random_data;
for (int j = 0; j < 100; j++) {
std::string k = rnd.RandomString(rnd.Next() % 20);
std::string k =
rnd.RandomString(rnd.Next() % 20 + 1); // requires non-empty keys
std::string v = rnd.RandomString(rnd.Next() % 50);
random_data.emplace_back(k, v);
}
+1
View File
@@ -228,6 +228,7 @@ DECLARE_uint64(backup_max_size);
DECLARE_int32(checkpoint_one_in);
DECLARE_int32(ingest_external_file_one_in);
DECLARE_int32(ingest_external_file_width);
DECLARE_int32(ingest_external_file_use_file_info_one_in);
DECLARE_int32(compact_files_one_in);
DECLARE_int32(compact_range_one_in);
DECLARE_int32(promote_l0_one_in);
+6
View File
@@ -862,6 +862,12 @@ DEFINE_int32(ingest_external_file_one_in, 0,
DEFINE_int32(ingest_external_file_width, 100,
"The width of the ingested external files.");
DEFINE_int32(ingest_external_file_use_file_info_one_in, 0,
"If non-zero, the ingestexternalfile flow reuses each file's "
"metadata via IngestExternalFileArg::file_infos (from "
"SstFileWriter::Finish) once every N ingestions on average, so "
"ingestion skips re-opening and scanning the files.");
DEFINE_int32(compact_files_one_in, 0,
"If non-zero, then CompactFiles() will be called once for every N "
"operations on average. 0 indicates CompactFiles() is disabled.");
+21 -4
View File
@@ -2326,6 +2326,12 @@ class NonBatchedOpsStressTest : public StressTest {
// deletion file's compaction input optimization.
bool test_standalone_range_deletion = thread->rand.OneInOpt(
FLAGS_test_ingest_standalone_range_deletion_one_in);
// When true, reuse the writer's metadata via IngestExternalFileArg's
// file_infos so ingestion skips re-opening and scanning the file. Not
// combined with the standalone range deletion mode (a range-del-only file).
bool use_file_info =
!test_standalone_range_deletion &&
thread->rand.OneInOpt(FLAGS_ingest_external_file_use_file_info_one_in);
std::vector<std::string> external_files;
const std::string sst_filename =
GetDbPath() + "/." + std::to_string(thread->tid) + ".sst";
@@ -2369,6 +2375,7 @@ class NonBatchedOpsStressTest : public StressTest {
SstFileWriter sst_file_writer(EnvOptions(options_), options_);
SstFileWriter standalone_rangedel_sst_file_writer(EnvOptions(options_),
options_);
ExternalSstFileInfo sst_file_info;
if (s.ok()) {
s = sst_file_writer.Open(sst_filename);
}
@@ -2462,7 +2469,7 @@ class NonBatchedOpsStressTest : public StressTest {
}
}
if (s.ok() && !keys.empty()) {
s = sst_file_writer.Finish();
s = sst_file_writer.Finish(&sst_file_info);
}
if (s.ok() && total_keys != 0 && test_standalone_range_deletion) {
@@ -2494,9 +2501,19 @@ class NonBatchedOpsStressTest : public StressTest {
<< ingest_options.verify_checksums_readahead_size
<< ", fill_cache: " << ingest_options.fill_cache
<< ", test_standalone_range_deletion: "
<< test_standalone_range_deletion;
s = db_->IngestExternalFile(column_families_[column_family],
external_files, ingest_options);
<< test_standalone_range_deletion
<< ", use_file_info: " << use_file_info;
if (use_file_info) {
IngestExternalFileArg arg;
arg.column_family = column_families_[column_family];
arg.external_files = external_files;
arg.file_infos = {sst_file_info};
arg.options = ingest_options;
s = db_->IngestExternalFiles({arg});
} else {
s = db_->IngestExternalFile(column_families_[column_family],
external_files, ingest_options);
}
}
if (!s.ok()) {
for (PendingExpectedValue& pending_expected_value :
+72
View File
@@ -27,6 +27,7 @@
#include "rocksdb/file_checksum.h"
#include "rocksdb/listener.h"
#include "rocksdb/sst_partitioner.h"
#include "rocksdb/table_properties.h"
#include "rocksdb/types.h"
#include "rocksdb/universal_compaction.h"
#include "rocksdb/version.h"
@@ -2871,6 +2872,69 @@ struct IngestExternalFileOptions {
bool fill_cache = true;
};
// ExternalSstFileInfo includes information about an SST file. It is produced by
// SstFileWriter. When ExternalSstFileInfo is included in IngestExternalFileArg,
// it can significantly improve ingestion performance by avoiding additional IO
// on the file.
//
// TODO: This should be opaque to the user, consider removing the internal
// details in next major release.
struct ExternalSstFileInfo {
ExternalSstFileInfo()
: file_path(""),
smallest_key(""),
largest_key(""),
smallest_range_del_key(""),
largest_range_del_key(""),
file_checksum(""),
file_checksum_func_name(""),
sequence_number(0),
file_size(0),
num_entries(0),
num_range_del_entries(0),
version(0) {}
ExternalSstFileInfo(const std::string& _file_path,
const std::string& _smallest_key,
const std::string& _largest_key,
SequenceNumber _sequence_number, uint64_t _file_size,
uint64_t _num_entries, int32_t _version)
: file_path(_file_path),
smallest_key(_smallest_key),
largest_key(_largest_key),
smallest_range_del_key(""),
largest_range_del_key(""),
file_checksum(""),
file_checksum_func_name(""),
sequence_number(_sequence_number),
file_size(_file_size),
num_entries(_num_entries),
num_range_del_entries(0),
version(_version) {}
std::string file_path; // external sst file path
std::string smallest_key; // smallest user key in file, TODO: remove in favor
// of smallest_internal_key
std::string largest_key; // largest user key in file, TODO: remove in favor
// of largest_internal_key
std::string
smallest_range_del_key; // smallest range deletion user key in file
std::string largest_range_del_key; // largest range deletion user key in file
std::string file_checksum; // sst file checksum;
std::string file_checksum_func_name; // The name of file checksum function
SequenceNumber
sequence_number; // smallest sequence number of all keys in file
uint64_t file_size; // file size in bytes
uint64_t num_entries; // number of entries in file
uint64_t num_range_del_entries; // number of range deletion entries in file
int32_t version; // file version
std::string smallest_internal_key; // smallest internal key in file
std::string largest_internal_key; // largest internal key in file
TableProperties table_properties; // other additional table metadata
};
// It is valid that files_checksums and files_checksum_func_names are both
// empty (no checksum information is provided for ingestion). Otherwise,
// their sizes should be the same as external_files. The file order should
@@ -2907,6 +2971,14 @@ struct IngestExternalFileArg {
// exclusive, so it is best not to depend on one or the other until it is
// sorted out.
std::optional<RangeOpt> atomic_replace_range;
// Optional per-file metadata, parallel to `external_files` (same size and
// order). When provided (e.g. the ExternalSstFileInfo returned by
// SstFileWriter::Finish), ingestion reuses this metadata instead of
// re-opening and scanning each file to recompute it, avoiding that extra I/O.
// Not compatible with options.write_global_seqno (the file is not opened, so
// a global seqno cannot be written back into it).
std::vector<ExternalSstFileInfo> file_infos;
};
enum TraceFilterType : uint64_t {
-50
View File
@@ -19,56 +19,6 @@ namespace ROCKSDB_NAMESPACE {
class Comparator;
// ExternalSstFileInfo include information about sst files created
// using SstFileWriter.
struct ExternalSstFileInfo {
ExternalSstFileInfo()
: file_path(""),
smallest_key(""),
largest_key(""),
smallest_range_del_key(""),
largest_range_del_key(""),
file_checksum(""),
file_checksum_func_name(""),
sequence_number(0),
file_size(0),
num_entries(0),
num_range_del_entries(0),
version(0) {}
ExternalSstFileInfo(const std::string& _file_path,
const std::string& _smallest_key,
const std::string& _largest_key,
SequenceNumber _sequence_number, uint64_t _file_size,
uint64_t _num_entries, int32_t _version)
: file_path(_file_path),
smallest_key(_smallest_key),
largest_key(_largest_key),
smallest_range_del_key(""),
largest_range_del_key(""),
file_checksum(""),
file_checksum_func_name(""),
sequence_number(_sequence_number),
file_size(_file_size),
num_entries(_num_entries),
num_range_del_entries(0),
version(_version) {}
std::string file_path; // external sst file path
std::string smallest_key; // smallest user key in file
std::string largest_key; // largest user key in file
std::string
smallest_range_del_key; // smallest range deletion user key in file
std::string largest_range_del_key; // largest range deletion user key in file
std::string file_checksum; // sst file checksum;
std::string file_checksum_func_name; // The name of file checksum function
SequenceNumber sequence_number; // sequence number of all keys in file
uint64_t file_size; // file size in bytes
uint64_t num_entries; // number of entries in file
uint64_t num_range_del_entries; // number of range deletion entries in file
int32_t version; // file version
};
// SstFileWriter is used to create sst files that can be added to database later
// All keys in files generated by SstFileWriter will have sequence number = 0.
//
+10
View File
@@ -765,6 +765,16 @@ enum Histograms : uint32_t {
// blocks for uniform key distribution tracking.
BLOCK_KEY_DISTRIBUTION_CV,
// Time (microseconds) in the "prepare" phase of an IngestExternalFile(s) call
// (validation and reading/linking the files; does not block writes). One
// sample per successful call; requires stats level > kExceptTimers.
INGEST_EXTERNAL_FILE_PREPARE_TIME,
// Time (microseconds) in the "run" phase of an IngestExternalFile(s) call
// (work under the DB mutex while writes are blocked). One sample per
// successful call; requires stats level > kExceptTimers.
INGEST_EXTERNAL_FILE_RUN_TIME,
HISTOGRAM_ENUM_MAX
};
+8
View File
@@ -5998,6 +5998,10 @@ class HistogramTypeJni {
return 0x41;
case ROCKSDB_NAMESPACE::Histograms::BLOCK_KEY_DISTRIBUTION_CV:
return 0x42;
case ROCKSDB_NAMESPACE::Histograms::INGEST_EXTERNAL_FILE_PREPARE_TIME:
return 0x43;
case ROCKSDB_NAMESPACE::Histograms::INGEST_EXTERNAL_FILE_RUN_TIME:
return 0x44;
case ROCKSDB_NAMESPACE::Histograms::HISTOGRAM_ENUM_MAX:
// 0x3E is reserved for backwards compatibility on current minor
// version.
@@ -6151,6 +6155,10 @@ class HistogramTypeJni {
return ROCKSDB_NAMESPACE::Histograms::MULTISCAN_BLOCKS_PER_PREPARE;
case 0x42:
return ROCKSDB_NAMESPACE::Histograms::BLOCK_KEY_DISTRIBUTION_CV;
case 0x43:
return ROCKSDB_NAMESPACE::Histograms::INGEST_EXTERNAL_FILE_PREPARE_TIME;
case 0x44:
return ROCKSDB_NAMESPACE::Histograms::INGEST_EXTERNAL_FILE_RUN_TIME;
case 0x3E:
// 0x3E is reserved for backwards compatibility on current minor
// version.
@@ -231,6 +231,16 @@ public enum HistogramType {
*/
BLOCK_KEY_DISTRIBUTION_CV((byte) 0x42),
/**
* Time (us) in the prepare phase of an IngestExternalFile(s) call.
*/
INGEST_EXTERNAL_FILE_PREPARE_TIME((byte) 0x43),
/**
* Time (us) in the run phase of an IngestExternalFile(s) call.
*/
INGEST_EXTERNAL_FILE_RUN_TIME((byte) 0x44),
// 0x3E is reserved for backwards compatibility on current minor version.
HISTOGRAM_ENUM_MAX((byte) 0x3E);
+3
View File
@@ -386,6 +386,9 @@ const std::vector<std::pair<Histograms, std::string>> HistogramsNameMap = {
{MULTISCAN_PREPARE_MICROS, "rocksdb.multiscan.prepare.micros"},
{MULTISCAN_BLOCKS_PER_PREPARE, "rocksdb.multiscan.blocks.per.prepare"},
{BLOCK_KEY_DISTRIBUTION_CV, "rocksdb.block.key.distribution.cv"},
{INGEST_EXTERNAL_FILE_PREPARE_TIME,
"rocksdb.ingest.external.file.prepare.micros"},
{INGEST_EXTERNAL_FILE_RUN_TIME, "rocksdb.ingest.external.file.run.micros"},
};
std::shared_ptr<Statistics> CreateDBStatistics() {
+20
View File
@@ -124,8 +124,17 @@ struct SstFileWriter::Rep {
builder->Add(ikey.Encode(), value);
// update file info
Slice encoded_ikey = ikey.Encode();
if (file_info.num_entries == 0) {
file_info.smallest_internal_key.assign(encoded_ikey.data(),
encoded_ikey.size());
}
file_info.num_entries++;
file_info.largest_key.assign(user_key.data(), user_key.size());
// Keys are added in strictly increasing order, so the last point key is the
// largest.
file_info.largest_internal_key.assign(encoded_ikey.data(),
encoded_ikey.size());
file_info.file_size = builder->FileSize();
InvalidatePageCache(false /* closing */).PermitUncheckedError();
@@ -505,6 +514,7 @@ Status SstFileWriter::Finish(ExternalSstFileInfo* file_info) {
r->file_info.file_checksum = r->file_writer->GetFileChecksum();
r->file_info.file_checksum_func_name =
r->file_writer->GetFileChecksumFuncName();
r->file_info.table_properties = r->builder->GetTableProperties();
}
if (!s.ok()) {
Status status = r->ioptions.env->DeleteFile(r->file_info.file_path);
@@ -529,6 +539,16 @@ Status SstFileWriter::Finish(ExternalSstFileInfo* file_info) {
assert(largest_key.size() >= r->ts_sz);
file_info->smallest_key.resize(smallest_key.size() - r->ts_sz);
file_info->largest_key.resize(largest_key.size() - r->ts_sz);
// Strip the timestamp (located just before the 8-byte internal-key
// footer) from the internal-key boundaries as well.
file_info->smallest_internal_key.erase(
file_info->smallest_internal_key.size() - kNumInternalBytes -
r->ts_sz,
r->ts_sz);
file_info->largest_internal_key.erase(
file_info->largest_internal_key.size() - kNumInternalBytes -
r->ts_sz,
r->ts_sz);
}
if (!smallest_range_del_key.empty()) {
assert(smallest_range_del_key.size() >= r->ts_sz);
+133
View File
@@ -62,6 +62,7 @@
#include "rocksdb/secondary_cache.h"
#include "rocksdb/slice.h"
#include "rocksdb/slice_transform.h"
#include "rocksdb/sst_file_writer.h"
#include "rocksdb/stats_history.h"
#include "rocksdb/table.h"
#include "rocksdb/tool_hooks.h"
@@ -235,6 +236,8 @@ DEFINE_string(
"\tcompact1 -- compact L1 into L2\n"
"\twaitforcompaction - pause until compaction is (probably) done\n"
"\tflush - flush the memtable\n"
"\tingestexternalfile -- Create external SST files and ingest them into "
"the DB via IngestExternalFile\n"
"\topenandcompact -- Open DB and compact all files to bottommost level, "
"writing output to separate directory without modifying source DB. "
"Designed for remote compaction service testing\n"
@@ -1309,6 +1312,21 @@ DEFINE_string(backup_dir, "",
DEFINE_string(restore_dir, "",
"If not empty string, use the given dir for restore.");
DEFINE_int32(ingest_external_file_batch_size, 10,
"Number of SST files ingested per IngestExternalFile call (one "
"batch) in the ingestexternalfile benchmark.");
DEFINE_int32(ingest_external_file_num_batches, 1,
"Number of IngestExternalFile calls (batches) in the "
"ingestexternalfile benchmark. Total files ingested is "
"batch_size * num_batches; each file holds --num keys.");
DEFINE_bool(
ingest_external_file_use_file_info, false,
"If true, the ingestexternalfile benchmark passes each file's metadata "
"(from SstFileWriter::Finish) via IngestExternalFileArg::file_infos, so "
"ingestion reuses it instead of re-opening and scanning the files.");
DEFINE_uint64(
initial_auto_readahead_size,
ROCKSDB_NAMESPACE::BlockBasedTableOptions().initial_auto_readahead_size,
@@ -3972,6 +3990,9 @@ class Benchmark {
method = &Benchmark::WriteSeqSeekSeq;
} else if (name == "compact") {
method = &Benchmark::Compact;
} else if (name == "ingestexternalfile") {
num_threads = 1;
method = &Benchmark::IngestExternalFile;
} else if (name == "compactall") {
CompactAll();
} else if (name == "compact0") {
@@ -9288,6 +9309,118 @@ class Benchmark {
db->CompactRange(cro, nullptr, nullptr);
}
// Creates SST files with SstFileWriter and ingests them into the DB via
// IngestExternalFile, reporting throughput. Batching is used only to group
// multiple files into a single IngestExternalFile call:
// --ingest_external_file_batch_size files per call,
// --ingest_external_file_num_batches calls. Each file holds --num keys over
// the same key range, so the files overlap and are assigned new global
// sequence numbers. With --ingest_external_file_use_file_info the writer's
// metadata is passed via IngestExternalFileArg::file_infos so ingestion
// reuses it instead of re-opening and scanning the files. Single threaded.
//
// NOTE: db_bench's reported micros/op includes file generation; use the
// rocksdb.ingest.external.file.micros histogram (--statistics) for the
// isolated per-call ingest latency.
void IngestExternalFile(ThreadState* thread) {
DB* db = SelectDB(thread);
const int batch_size = std::max(1, FLAGS_ingest_external_file_batch_size);
const int num_batches = std::max(1, FLAGS_ingest_external_file_num_batches);
const bool use_file_info = FLAGS_ingest_external_file_use_file_info;
// --num is the number of keys in each file; all files cover this same key
// range, so they overlap (which is fine -- a global seqno is assigned).
const int64_t keys_per_file = num_;
const int64_t key_space = keys_per_file;
const std::string tmp_dir = FLAGS_db + "/ingest_tmp";
Status s = FLAGS_env->CreateDirIfMissing(tmp_dir);
if (!s.ok()) {
fprintf(stderr, "CreateDirIfMissing(%s) failed: %s\n", tmp_dir.c_str(),
s.ToString().c_str());
db_bench_exit(1);
}
RandomGenerator gen;
std::unique_ptr<const char[]> key_guard;
Slice key = AllocateKey(&key_guard);
int64_t bytes = 0;
for (int b = 0; b < num_batches; ++b) {
// Write batch_size SstFileWriter files (all covering the same key range);
// they are batched into a single ingest call below.
std::vector<std::string> files;
std::vector<ExternalSstFileInfo> file_infos;
for (int j = 0; j < batch_size; ++j) {
std::string file_path = tmp_dir + "/ingest_" + std::to_string(b) + "_" +
std::to_string(j) + ".sst";
SstFileWriter sst_file_writer(EnvOptions(), open_options_);
s = sst_file_writer.Open(file_path);
if (!s.ok()) {
fprintf(stderr, "SstFileWriter::Open(%s) failed: %s\n",
file_path.c_str(), s.ToString().c_str());
db_bench_exit(1);
}
for (int64_t i = 0; i < keys_per_file; ++i) {
GenerateKeyFromInt(i, key_space, &key);
Slice value = gen.Generate();
s = sst_file_writer.Put(key, value);
if (!s.ok()) {
sst_file_writer.Finish().PermitUncheckedError();
fprintf(stderr, "SstFileWriter::Put failed: %s\n",
s.ToString().c_str());
db_bench_exit(1);
}
bytes += key.size() + value.size();
}
ExternalSstFileInfo info;
s = sst_file_writer.Finish(&info);
if (!s.ok()) {
fprintf(stderr, "SstFileWriter::Finish(%s) failed: %s\n",
file_path.c_str(), s.ToString().c_str());
db_bench_exit(1);
}
files.emplace_back(file_path);
file_infos.emplace_back(std::move(info));
}
// Link the files into the DB instead of copying them, so the benchmark
// measures the ingest path itself (which the file_info fast-path speeds
// up by skipping the re-open and scan of each file) rather than file-copy
// throughput. allow_global_seqno (default) handles the overlap.
IngestExternalFileOptions ingest_options;
ingest_options.move_files = true;
if (use_file_info) {
// Reuse the writer's metadata so ingestion skips re-opening/scanning.
IngestExternalFileArg arg;
arg.column_family = db->DefaultColumnFamily();
arg.external_files = files;
arg.file_infos = std::move(file_infos);
arg.options = ingest_options;
s = db->IngestExternalFiles({arg});
} else {
s = db->IngestExternalFile(files, ingest_options);
}
if (!s.ok()) {
fprintf(stderr, "IngestExternalFile failed: %s\n",
s.ToString().c_str());
db_bench_exit(1);
}
thread->stats.FinishedOps(&db_, db, 1, kWrite);
for (const auto& f : files) {
FLAGS_env->DeleteFile(f).PermitUncheckedError();
}
}
thread->stats.AddBytes(bytes);
char msg[100];
snprintf(msg, sizeof(msg),
"(%d batches x %d files, %" PRId64 " keys/file%s)", num_batches,
batch_size, keys_per_file, use_file_info ? ", file_info" : "");
thread->stats.AddMessage(msg);
}
void CompactAll() {
CompactRangeOptions cro;
cro.max_subcompactions = static_cast<uint32_t>(FLAGS_subcompactions);
+25
View File
@@ -85,6 +85,21 @@ class DBBenchTest : public testing::Test {
ASSERT_EQ(0, db_bench_tool(argc(), argv()));
}
// Every flag is passed explicitly because gflags state persists across
// db_bench_tool() calls within the same test process.
void RunIngestBench(int batch_size, int num_batches, bool use_file_info) {
ResetArgs();
AppendArgs(
{"./db_bench", "--benchmarks=ingestexternalfile", "--use_existing_db=0",
"--num=2000", "--compression_type=none", "--db=" + db_path_,
"--wal_dir=" + wal_path_,
"--ingest_external_file_batch_size=" + std::to_string(batch_size),
"--ingest_external_file_num_batches=" + std::to_string(num_batches),
"--ingest_external_file_use_file_info=" +
std::string(use_file_info ? "true" : "false")});
ASSERT_EQ(0, db_bench_tool(argc(), argv()));
}
void VerifyOptions(const Options& opt) {
DBOptions loaded_db_opts;
ConfigOptions config_opts;
@@ -144,6 +159,16 @@ TEST_F(DBBenchTest, OptionsFile) {
VerifyOptions(opt);
}
TEST_F(DBBenchTest, IngestExternalFile) {
RunIngestBench(/*batch_size=*/3, /*num_batches=*/2,
/*use_file_info=*/false);
}
TEST_F(DBBenchTest, IngestExternalFileWithFileInfo) {
RunIngestBench(/*batch_size=*/3, /*num_batches=*/2,
/*use_file_info=*/true);
}
TEST_F(DBBenchTest, OptionsFileUniversal) {
const std::string kOptionsFileName = test_path_ + "/OPTIONS_test";
+1
View File
@@ -215,6 +215,7 @@ default_params = {
"index_block_search_type": lambda: random.choice([0, 1, 2]),
"uniform_cv_threshold": lambda: random.choice([-1, 0.2, 1000]),
"ingest_external_file_one_in": lambda: random.choice([1000, 1000000]),
"ingest_external_file_use_file_info_one_in": lambda: random.choice([0, 2]),
"test_ingest_standalone_range_deletion_one_in": lambda: random.choice([0, 5, 10]),
"iterpercent": 10,
"lock_wal_one_in": lambda: random.choice([10000, 1000000]),
@@ -0,0 +1 @@
Added two stats histograms reporting the per-call latency (in microseconds) of each successful `IngestExternalFile`/`IngestExternalFiles` call, split by phase: `rocksdb.ingest.external.file.prepare.micros` (argument validation and reading/validating/linking the external files, which does not block live writes) and `rocksdb.ingest.external.file.run.micros` (the ingestion performed under the DB mutex while live writes are blocked). Failed ingestions are not recorded.
@@ -0,0 +1 @@
`IngestExternalFileArg` gained a `file_infos` field, a vector of `ExternalSstFileInfo` parallel to `external_files`. When set, `IngestExternalFiles()` reuses the supplied per-file metadata (boundary keys, sequence-number bounds, and table properties) instead of re-opening and scanning each file to recompute it, avoiding that extra I/O.