mirror of
https://github.com/facebook/rocksdb.git
synced 2026-07-07 14:47:40 +08:00
a004c2d850
Summary:
Add EXPERIMENTAL embedded blob SST support for SstFileWriter through
OpenWithEmbeddedBlobs(). Eligible large values are written as same-file blob
records inline in a block-based SST as values are added (interleaved with data
blocks), while table entries store same-file BlobIndex references that readers
resolve for Get, MultiGet, and iteration, including mixed embedded and
non-embedded wide-column values.
Embedded-blob handling is folded directly into BlockBasedTableBuilder rather than
living in SstFileWriter or a separate table-builder wrapper: SstFileWriter only
selects the mode (via TableBuilderOptions::embedded_blob_options), and the
builder writes blob records inline using its own file writer and running offset.
This is enabled by disabling index-value delta encoding for these SSTs — delta
encoding reconstructs a data block's offset from the previous block and so
requires byte-contiguous data blocks, which interleaved blob records would break.
With full (non-delta) index values, blob records can sit between data blocks, so
no entry buffering/replay is needed. To keep inline blob appends correctly
ordered with data-block writes, these SSTs use single-threaded (non-parallel)
compression. The mode is the only entry point today but the placement keeps it
open to generalization beyond SstFileWriter; regardless, this experimental
feature is expected only to have niche applications.
(Previous revisions of this change allowed delta-encoded index blocks by
putting all blobs at the beginning of the file, but that was a more awkward
and memory-hungry implementation due to buffering all the data blocks before
writing.)
The on-disk record format (SimpleGen2Blob: payload bytes followed by a 5-byte
trailer of a compression marker plus a builtin checksum that is context-modified
by the record's absolute file offset) lives in db/blob/blob_gen2_format.{h,cc},
which now owns both the read (ReadAndVerifySimpleGen2BlobRecord) and write
(WriteSimpleGen2BlobRecord) sides so the format is defined in one place. This is
expected to be reused for upcoming "blog file" support.
Readers need no record-layout metadata: same-file blob resolution is purely
absolute-offset keyed, and the per-record offset-modified checksum (plus a cheap
"record fits within the file" bound) is the corruption guard. The reader's only
embedded-blob metadata is presence: a best-effort auxiliary table property
(blob count and payload-byte totals, for diagnostics) whose mere presence signals
that the SST contains embedded blobs.
Reads route through the column family's BlobSource when a DB is attached, so
embedded payloads are served from / inserted into the blob value cache and
recorded in BLOB_DB_* statistics; the cache key is derived from the
SimpleGen2Blob offset scheme (the same scheme block-based SST blocks use), so
embedded blob records stay collision-free with data blocks even when the blob
cache and block cache are the same cache. Non-DB openers (SstFileReader,
sst_dump, repair, ingestion prevalidation) have no BlobSource and fall back to a
direct, uncached read.
Same-file BlobIndex references use blob file number 0 as the marker. That value
also serves as the invalid blob-file-number sentinel in broader metadata code,
but the meanings do not conflict when used carefully: only the embedded-SST
reader/writer path interprets 0 as same-file, while generic file-metadata paths
continue to reject it as invalid. Using 1 would be worse because legacy
"stackable" BlobDB can use low blob file numbers, including 1, so reserving it
would collide with real blob files.
Compression options remain in the public API as placeholders, but embedded blob
compression support is deferred. Integrating compression with
BlockBasedTableBuilder while avoiding copied CompressAndVerifyBlock-style logic
is tricky enough to deserve a separate, focused PR.
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14851
Test Plan:
- Added basic feature coverage to the crash test.
- Added BlobIndexTest.SameFileBlobIndex and BlobGarbageMeterTest.SameFileBlobIndex
coverage for same-file BlobIndex encoding, display, recognition, and ignoring
same-file references in blob-garbage accounting.
- Extended FileMetaDataTest.UpdateBoundariesBlobIndex to preserve the generic
zero-file-number corruption check while keeping same-file embedded blob
semantics at the table-reader/writer layers.
- SstFileReader embedded blob coverage: round-trip Get, MultiGet, and iterator
reads; format_version gating; ignored placeholder compression options; the
2048-byte default min_blob_size; wide-column mixed embedded/non-embedded
values; and early append-error surfacing.
- Added an interleaved-layout test (small block_size with alternating
small/large values) asserting the SST property index_value_is_delta_encoded==0,
more than one data block, and that blob records are interspersed with data
blocks (not a strict front prefix), with all values read back correctly via
Get and iteration; replaces the old "ignored bytes before/after the blob record
prefix" test.
- Added an embedded-record corruption test: flipping a byte inside a blob
record's payload yields Corruption on read with verify_checksums (the
offset-keyed record checksum is the guard now that the range pre-check is gone).
- Exercised the cached read path through BlobSource, including blob-cache
hit/miss behavior and the shared blob_cache == block_cache configuration, in
db_blob_index_test.
- Normal-path CPU regression check: release (DEBUG_LEVEL=0) db_bench on a
non-embedded DB comparing this change vs upstream main, 3 interleaved reps of
fillseq, fillrandom, readrandom, and readseq (5M keys, value_size=100,
compression none, DB on /dev/shm). All deltas were within run-to-run noise
(~1%), i.e. no measurable regression from adding the embedded-mode branch to
the builder hot path.
Reviewed By: xingbowang
Differential Revision: D108564468
Pulled By: pdillinger
fbshipit-source-id: 5f01ffb1d40c6fd5b82d2451ec3342abb5040ca6
685 lines
22 KiB
C++
685 lines
22 KiB
C++
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
// 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 "db/blob/blob_source.h"
|
|
|
|
#include <cassert>
|
|
#include <string>
|
|
|
|
#include "cache/cache_reservation_manager.h"
|
|
#include "cache/charged_cache.h"
|
|
#include "db/blob/blob_contents.h"
|
|
#include "db/blob/blob_file_reader.h"
|
|
#include "db/blob/blob_gen2_format.h"
|
|
#include "db/blob/blob_log_format.h"
|
|
#include "file/random_access_file_reader.h"
|
|
#include "memory/memory_allocator_impl.h"
|
|
#include "monitoring/statistics_impl.h"
|
|
#include "options/cf_options.h"
|
|
#include "table/get_context.h"
|
|
#include "table/multiget_context.h"
|
|
|
|
namespace ROCKSDB_NAMESPACE {
|
|
|
|
namespace {
|
|
|
|
Status AppendBlobRefreshRetryFailure(const Status& stale_status,
|
|
const Status& retry_status) {
|
|
assert(stale_status.IsCorruption());
|
|
assert(!retry_status.ok());
|
|
if (retry_status.IsCorruption()) {
|
|
return retry_status;
|
|
}
|
|
return Status::CopyAppendMessage(
|
|
stale_status, "; refresh retry failed: ", retry_status.ToString());
|
|
}
|
|
|
|
} // namespace
|
|
|
|
BlobSource::BlobSource(const ImmutableOptions& immutable_options,
|
|
const MutableCFOptions& mutable_cf_options,
|
|
const std::string& db_id,
|
|
const std::string& db_session_id,
|
|
BlobFileCache* blob_file_cache)
|
|
: db_id_(db_id),
|
|
db_session_id_(db_session_id),
|
|
statistics_(immutable_options.statistics.get()),
|
|
blob_file_cache_(blob_file_cache),
|
|
blob_cache_(immutable_options.blob_cache),
|
|
lowest_used_cache_tier_(immutable_options.lowest_used_cache_tier) {
|
|
auto bbto =
|
|
mutable_cf_options.table_factory->GetOptions<BlockBasedTableOptions>();
|
|
if (bbto &&
|
|
bbto->cache_usage_options.options_overrides.at(CacheEntryRole::kBlobCache)
|
|
.charged == CacheEntryRoleOptions::Decision::kEnabled) {
|
|
blob_cache_ = SharedCacheInterface{std::make_shared<ChargedCache>(
|
|
immutable_options.blob_cache, bbto->block_cache)};
|
|
}
|
|
}
|
|
|
|
BlobSource::~BlobSource() = default;
|
|
|
|
Status BlobSource::GetBlobFromCache(
|
|
const Slice& cache_key, CacheHandleGuard<BlobContents>* cached_blob) const {
|
|
assert(blob_cache_);
|
|
assert(!cache_key.empty());
|
|
assert(cached_blob);
|
|
assert(cached_blob->IsEmpty());
|
|
|
|
Cache::Handle* cache_handle = nullptr;
|
|
cache_handle = GetEntryFromCache(cache_key);
|
|
if (cache_handle != nullptr) {
|
|
*cached_blob =
|
|
CacheHandleGuard<BlobContents>(blob_cache_.get(), cache_handle);
|
|
|
|
assert(cached_blob->GetValue());
|
|
|
|
PERF_COUNTER_ADD(blob_cache_hit_count, 1);
|
|
PERF_COUNTER_ADD(blob_cache_read_byte, cached_blob->GetValue()->size());
|
|
RecordTick(statistics_, BLOB_DB_CACHE_HIT);
|
|
RecordTick(statistics_, BLOB_DB_CACHE_BYTES_READ,
|
|
cached_blob->GetValue()->size());
|
|
|
|
return Status::OK();
|
|
}
|
|
|
|
RecordTick(statistics_, BLOB_DB_CACHE_MISS);
|
|
|
|
return Status::NotFound("Blob not found in cache");
|
|
}
|
|
|
|
Status BlobSource::PutBlobIntoCache(
|
|
const Slice& cache_key, std::unique_ptr<BlobContents>* blob,
|
|
CacheHandleGuard<BlobContents>* cached_blob) const {
|
|
assert(blob_cache_);
|
|
assert(!cache_key.empty());
|
|
assert(blob);
|
|
assert(*blob);
|
|
assert(cached_blob);
|
|
assert(cached_blob->IsEmpty());
|
|
|
|
TypedHandle* cache_handle = nullptr;
|
|
const Status s = InsertEntryIntoCache(cache_key, blob->get(), &cache_handle,
|
|
Cache::Priority::BOTTOM);
|
|
if (s.ok()) {
|
|
blob->release();
|
|
|
|
assert(cache_handle != nullptr);
|
|
*cached_blob =
|
|
CacheHandleGuard<BlobContents>(blob_cache_.get(), cache_handle);
|
|
|
|
assert(cached_blob->GetValue());
|
|
|
|
RecordTick(statistics_, BLOB_DB_CACHE_ADD);
|
|
RecordTick(statistics_, BLOB_DB_CACHE_BYTES_WRITE,
|
|
cached_blob->GetValue()->size());
|
|
|
|
} else {
|
|
RecordTick(statistics_, BLOB_DB_CACHE_ADD_FAILURES);
|
|
}
|
|
|
|
return s;
|
|
}
|
|
|
|
BlobSource::TypedHandle* BlobSource::GetEntryFromCache(const Slice& key) const {
|
|
return blob_cache_.LookupFull(key, nullptr /* context */,
|
|
Cache::Priority::BOTTOM, statistics_,
|
|
lowest_used_cache_tier_);
|
|
}
|
|
|
|
void BlobSource::PinCachedBlob(CacheHandleGuard<BlobContents>* cached_blob,
|
|
PinnableSlice* value) {
|
|
assert(cached_blob);
|
|
assert(cached_blob->GetValue());
|
|
assert(value);
|
|
|
|
// To avoid copying the cached blob into the buffer provided by the
|
|
// application, we can simply transfer ownership of the cache handle to
|
|
// the target PinnableSlice. This has the potential to save a lot of
|
|
// CPU, especially with large blob values.
|
|
|
|
value->Reset();
|
|
|
|
constexpr Cleanable* cleanable = nullptr;
|
|
value->PinSlice(cached_blob->GetValue()->data(), cleanable);
|
|
|
|
cached_blob->TransferTo(value);
|
|
}
|
|
|
|
void BlobSource::PinOwnedBlob(std::unique_ptr<BlobContents>* owned_blob,
|
|
PinnableSlice* value) {
|
|
assert(owned_blob);
|
|
assert(*owned_blob);
|
|
assert(value);
|
|
|
|
BlobContents* const blob = owned_blob->release();
|
|
assert(blob);
|
|
|
|
value->Reset();
|
|
value->PinSlice(
|
|
blob->data(),
|
|
[](void* arg1, void* /* arg2 */) {
|
|
delete static_cast<BlobContents*>(arg1);
|
|
},
|
|
blob, nullptr);
|
|
}
|
|
|
|
Status BlobSource::InsertEntryIntoCache(const Slice& key, BlobContents* value,
|
|
TypedHandle** cache_handle,
|
|
Cache::Priority priority) const {
|
|
return blob_cache_.InsertFull(key, value, value->ApproximateMemoryUsage(),
|
|
cache_handle, priority,
|
|
lowest_used_cache_tier_);
|
|
}
|
|
|
|
Status BlobSource::GetBlob(const ReadOptions& read_options,
|
|
const Slice& user_key, uint64_t file_number,
|
|
uint64_t offset, uint64_t file_size,
|
|
uint64_t value_size,
|
|
CompressionType compression_type,
|
|
FilePrefetchBuffer* prefetch_buffer,
|
|
PinnableSlice* value, uint64_t* bytes_read) {
|
|
assert(value);
|
|
|
|
Status s;
|
|
|
|
const CacheKey cache_key = GetCacheKey(file_number, file_size, offset);
|
|
|
|
CacheHandleGuard<BlobContents> blob_handle;
|
|
|
|
// First, try to get the blob from the cache
|
|
//
|
|
// If blob cache is enabled, we'll try to read from it.
|
|
if (blob_cache_) {
|
|
Slice key = cache_key.AsSlice();
|
|
s = GetBlobFromCache(key, &blob_handle);
|
|
if (s.ok()) {
|
|
PinCachedBlob(&blob_handle, value);
|
|
|
|
// For consistency, the size of on-disk (possibly compressed) blob record
|
|
// is assigned to bytes_read.
|
|
uint64_t adjustment =
|
|
read_options.verify_checksums
|
|
? BlobLogRecord::CalculateAdjustmentForRecordHeader(
|
|
user_key.size())
|
|
: 0;
|
|
assert(offset >= adjustment);
|
|
|
|
uint64_t record_size = value_size + adjustment;
|
|
if (bytes_read) {
|
|
*bytes_read = record_size;
|
|
}
|
|
return s;
|
|
}
|
|
}
|
|
|
|
assert(blob_handle.IsEmpty());
|
|
|
|
const bool no_io = read_options.read_tier == kBlockCacheTier;
|
|
if (no_io) {
|
|
s = Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
|
|
return s;
|
|
}
|
|
|
|
// Can't find the blob from the cache. Since I/O is allowed, read from the
|
|
// file.
|
|
std::unique_ptr<BlobContents> blob_contents;
|
|
|
|
{
|
|
CacheHandleGuard<BlobFileReader> blob_file_reader;
|
|
s = blob_file_cache_->GetBlobFileReader(read_options, file_number,
|
|
&blob_file_reader);
|
|
if (!s.ok()) {
|
|
return s;
|
|
}
|
|
|
|
assert(blob_file_reader.GetValue());
|
|
|
|
if (compression_type != blob_file_reader.GetValue()->GetCompressionType()) {
|
|
return Status::Corruption("Compression type mismatch when reading blob");
|
|
}
|
|
|
|
MemoryAllocator* const allocator =
|
|
(blob_cache_ && read_options.fill_cache)
|
|
? blob_cache_.get()->memory_allocator()
|
|
: nullptr;
|
|
|
|
uint64_t read_size = 0;
|
|
s = blob_file_reader.GetValue()->GetBlob(
|
|
read_options, user_key, offset, value_size, compression_type,
|
|
prefetch_buffer, allocator, &blob_contents, &read_size);
|
|
if (s.IsCorruption()) {
|
|
const Status stale_status = s;
|
|
blob_file_reader.Reset();
|
|
blob_file_cache_->Evict(file_number);
|
|
|
|
std::unique_ptr<BlobFileReader> fresh_reader;
|
|
s = blob_file_cache_->OpenBlobFileReaderUncached(
|
|
read_options, file_number, &fresh_reader,
|
|
/*allow_footer_skip_retry=*/false);
|
|
if (!s.ok()) {
|
|
return AppendBlobRefreshRetryFailure(stale_status, s);
|
|
}
|
|
|
|
if (compression_type != fresh_reader->GetCompressionType()) {
|
|
return Status::Corruption(
|
|
"Compression type mismatch when reading blob");
|
|
}
|
|
|
|
blob_contents.reset();
|
|
read_size = 0;
|
|
s = fresh_reader->GetBlob(read_options, user_key, offset, value_size,
|
|
compression_type, prefetch_buffer, allocator,
|
|
&blob_contents, &read_size);
|
|
if (!s.ok()) {
|
|
s = AppendBlobRefreshRetryFailure(stale_status, s);
|
|
} else {
|
|
CacheHandleGuard<BlobFileReader> ignored_reader;
|
|
blob_file_cache_
|
|
->RefreshBlobFileReader(file_number, &fresh_reader, &ignored_reader)
|
|
.PermitUncheckedError();
|
|
}
|
|
}
|
|
if (!s.ok()) {
|
|
return s;
|
|
}
|
|
if (bytes_read) {
|
|
*bytes_read = read_size;
|
|
}
|
|
}
|
|
|
|
if (blob_cache_ && read_options.fill_cache) {
|
|
// If filling cache is allowed and a cache is configured, try to put the
|
|
// blob to the cache.
|
|
Slice key = cache_key.AsSlice();
|
|
s = PutBlobIntoCache(key, &blob_contents, &blob_handle);
|
|
if (!s.ok()) {
|
|
return s;
|
|
}
|
|
|
|
PinCachedBlob(&blob_handle, value);
|
|
} else {
|
|
PinOwnedBlob(&blob_contents, value);
|
|
}
|
|
|
|
assert(s.ok());
|
|
return s;
|
|
}
|
|
|
|
Status BlobSource::GetSimpleGen2Blob(
|
|
const ReadOptions& read_options, const OffsetableCacheKey& base_cache_key,
|
|
RandomAccessFileReader* file, uint64_t record_offset, uint64_t payload_size,
|
|
ChecksumType checksum_type, uint32_t base_context_checksum,
|
|
CompressionType expected_compression, PinnableSlice* value,
|
|
uint64_t* bytes_read) {
|
|
assert(value);
|
|
assert(file);
|
|
|
|
const uint64_t record_size = payload_size + kSimpleGen2BlobTrailerSize;
|
|
|
|
// The cache key is derived from the SimpleGen2Blob format (shared scheme with
|
|
// block-based SST blocks); see GetSimpleGen2BlobCacheKey.
|
|
const CacheKey cache_key =
|
|
GetSimpleGen2BlobCacheKey(base_cache_key, record_offset);
|
|
|
|
Status s;
|
|
|
|
CacheHandleGuard<BlobContents> blob_handle;
|
|
|
|
// First, try to get the blob from the cache.
|
|
if (blob_cache_) {
|
|
Slice key = cache_key.AsSlice();
|
|
s = GetBlobFromCache(key, &blob_handle);
|
|
if (s.ok()) {
|
|
PinCachedBlob(&blob_handle, value);
|
|
|
|
// For consistency, the on-disk record size is assigned to bytes_read on
|
|
// both cache hits and misses.
|
|
if (bytes_read) {
|
|
*bytes_read = record_size;
|
|
}
|
|
return s;
|
|
}
|
|
}
|
|
|
|
assert(blob_handle.IsEmpty());
|
|
|
|
const bool no_io = read_options.read_tier == kBlockCacheTier;
|
|
if (no_io) {
|
|
return Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
|
|
}
|
|
|
|
// Cache miss (or no cache configured). Read the record into a buffer
|
|
// allocated from the blob cache's memory allocator when we intend to insert
|
|
// it, exposing the uncompressed payload as BlobContents (the trailer just
|
|
// sits unused at the tail of the buffer).
|
|
MemoryAllocator* const allocator = (blob_cache_ && read_options.fill_cache)
|
|
? blob_cache_.get()->memory_allocator()
|
|
: nullptr;
|
|
|
|
CacheAllocationPtr buf =
|
|
AllocateBlock(static_cast<size_t>(record_size), allocator);
|
|
s = ReadAndVerifySimpleGen2BlobRecord(
|
|
read_options, file, record_offset, static_cast<size_t>(payload_size),
|
|
static_cast<size_t>(record_size), checksum_type, base_context_checksum,
|
|
expected_compression, buf.get());
|
|
if (!s.ok()) {
|
|
return s;
|
|
}
|
|
|
|
std::unique_ptr<BlobContents> blob_contents(
|
|
new BlobContents(std::move(buf), static_cast<size_t>(payload_size)));
|
|
|
|
// Record the per-read statistics (mirrors BlobFileReader::GetBlob).
|
|
RecordTick(statistics_, BLOB_DB_BLOB_FILE_BYTES_READ, record_size);
|
|
PERF_COUNTER_ADD(blob_read_count, 1);
|
|
PERF_COUNTER_ADD(blob_read_byte, record_size);
|
|
if (bytes_read) {
|
|
*bytes_read = record_size;
|
|
}
|
|
|
|
if (blob_cache_ && read_options.fill_cache) {
|
|
// If filling cache is allowed and a cache is configured, try to put the
|
|
// blob into the cache.
|
|
Slice key = cache_key.AsSlice();
|
|
s = PutBlobIntoCache(key, &blob_contents, &blob_handle);
|
|
if (!s.ok()) {
|
|
return s;
|
|
}
|
|
|
|
PinCachedBlob(&blob_handle, value);
|
|
} else {
|
|
PinOwnedBlob(&blob_contents, value);
|
|
}
|
|
|
|
assert(s.ok());
|
|
return s;
|
|
}
|
|
|
|
void BlobSource::MultiGetBlob(const ReadOptions& read_options,
|
|
autovector<BlobFileReadRequests>& blob_reqs,
|
|
uint64_t* bytes_read) {
|
|
assert(blob_reqs.size() > 0);
|
|
|
|
uint64_t total_bytes_read = 0;
|
|
uint64_t bytes_read_in_file = 0;
|
|
|
|
for (auto& [file_number, file_size, blob_reqs_in_file] : blob_reqs) {
|
|
// sort blob_reqs_in_file by file offset.
|
|
std::sort(
|
|
blob_reqs_in_file.begin(), blob_reqs_in_file.end(),
|
|
[](const BlobReadRequest& lhs, const BlobReadRequest& rhs) -> bool {
|
|
return lhs.offset < rhs.offset;
|
|
});
|
|
|
|
MultiGetBlobFromOneFile(read_options, file_number, file_size,
|
|
blob_reqs_in_file, &bytes_read_in_file);
|
|
|
|
total_bytes_read += bytes_read_in_file;
|
|
}
|
|
|
|
if (bytes_read) {
|
|
*bytes_read = total_bytes_read;
|
|
}
|
|
}
|
|
|
|
void BlobSource::MultiGetBlobFromOneFile(const ReadOptions& read_options,
|
|
uint64_t file_number,
|
|
uint64_t /*file_size*/,
|
|
autovector<BlobReadRequest>& blob_reqs,
|
|
uint64_t* bytes_read) {
|
|
const size_t num_blobs = blob_reqs.size();
|
|
assert(num_blobs > 0);
|
|
assert(num_blobs <= MultiGetContext::MAX_BATCH_SIZE);
|
|
|
|
#ifndef NDEBUG
|
|
for (size_t i = 0; i < num_blobs - 1; ++i) {
|
|
assert(blob_reqs[i].offset <= blob_reqs[i + 1].offset);
|
|
}
|
|
#endif // !NDEBUG
|
|
|
|
using Mask = uint64_t;
|
|
Mask cache_hit_mask = 0;
|
|
|
|
uint64_t total_bytes = 0;
|
|
const OffsetableCacheKey base_cache_key(db_id_, db_session_id_, file_number);
|
|
|
|
if (blob_cache_) {
|
|
size_t cached_blob_count = 0;
|
|
for (size_t i = 0; i < num_blobs; ++i) {
|
|
auto& req = blob_reqs[i];
|
|
|
|
CacheHandleGuard<BlobContents> blob_handle;
|
|
const CacheKey cache_key = base_cache_key.WithOffset(req.offset);
|
|
const Slice key = cache_key.AsSlice();
|
|
|
|
const Status s = GetBlobFromCache(key, &blob_handle);
|
|
|
|
if (s.ok()) {
|
|
assert(req.status);
|
|
*req.status = s;
|
|
|
|
PinCachedBlob(&blob_handle, req.result);
|
|
|
|
// Update the counter for the number of valid blobs read from the cache.
|
|
++cached_blob_count;
|
|
|
|
// For consistency, the size of each on-disk (possibly compressed) blob
|
|
// record is accumulated to total_bytes.
|
|
uint64_t adjustment =
|
|
read_options.verify_checksums
|
|
? BlobLogRecord::CalculateAdjustmentForRecordHeader(
|
|
req.user_key->size())
|
|
: 0;
|
|
assert(req.offset >= adjustment);
|
|
total_bytes += req.len + adjustment;
|
|
cache_hit_mask |= (Mask{1} << i); // cache hit
|
|
}
|
|
}
|
|
|
|
// All blobs were read from the cache.
|
|
if (cached_blob_count == num_blobs) {
|
|
if (bytes_read) {
|
|
*bytes_read = total_bytes;
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
const bool no_io = read_options.read_tier == kBlockCacheTier;
|
|
if (no_io) {
|
|
for (size_t i = 0; i < num_blobs; ++i) {
|
|
if (!(cache_hit_mask & (Mask{1} << i))) {
|
|
BlobReadRequest& req = blob_reqs[i];
|
|
assert(req.status);
|
|
|
|
*req.status =
|
|
Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
{
|
|
// Find the rest of blobs from the file since I/O is allowed.
|
|
autovector<std::pair<BlobReadRequest*, std::unique_ptr<BlobContents>>>
|
|
_blob_reqs;
|
|
uint64_t _bytes_read = 0;
|
|
|
|
for (size_t i = 0; i < num_blobs; ++i) {
|
|
if (!(cache_hit_mask & (Mask{1} << i))) {
|
|
_blob_reqs.emplace_back(&blob_reqs[i], std::unique_ptr<BlobContents>());
|
|
}
|
|
}
|
|
|
|
CacheHandleGuard<BlobFileReader> blob_file_reader;
|
|
Status s = blob_file_cache_->GetBlobFileReader(read_options, file_number,
|
|
&blob_file_reader);
|
|
if (!s.ok()) {
|
|
for (size_t i = 0; i < _blob_reqs.size(); ++i) {
|
|
BlobReadRequest* const req = _blob_reqs[i].first;
|
|
assert(req);
|
|
assert(req->status);
|
|
|
|
*req->status = s;
|
|
}
|
|
return;
|
|
}
|
|
|
|
assert(blob_file_reader.GetValue());
|
|
|
|
MemoryAllocator* const allocator =
|
|
(blob_cache_ && read_options.fill_cache)
|
|
? blob_cache_.get()->memory_allocator()
|
|
: nullptr;
|
|
|
|
blob_file_reader.GetValue()->MultiGetBlob(read_options, allocator,
|
|
_blob_reqs, &_bytes_read);
|
|
|
|
bool needs_reader_refresh = false;
|
|
for (const auto& blob_req : _blob_reqs) {
|
|
BlobReadRequest* const req = blob_req.first;
|
|
assert(req != nullptr);
|
|
assert(req->status != nullptr);
|
|
if (req->status->IsCorruption()) {
|
|
needs_reader_refresh = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (needs_reader_refresh) {
|
|
blob_file_reader.Reset();
|
|
blob_file_cache_->Evict(file_number);
|
|
|
|
std::unique_ptr<BlobFileReader> fresh_reader;
|
|
s = blob_file_cache_->OpenBlobFileReaderUncached(
|
|
read_options, file_number, &fresh_reader,
|
|
/*allow_footer_skip_retry=*/false);
|
|
if (!s.ok()) {
|
|
for (const auto& blob_req : _blob_reqs) {
|
|
BlobReadRequest* const req = blob_req.first;
|
|
assert(req != nullptr);
|
|
assert(req->status != nullptr);
|
|
if (req->status->IsCorruption()) {
|
|
*req->status = AppendBlobRefreshRetryFailure(*req->status, s);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
autovector<std::pair<BlobReadRequest*, std::unique_ptr<BlobContents>>>
|
|
retry_blob_reqs;
|
|
autovector<Status> stale_statuses;
|
|
for (auto& blob_req : _blob_reqs) {
|
|
BlobReadRequest* const req = blob_req.first;
|
|
assert(req != nullptr);
|
|
assert(req->status != nullptr);
|
|
if (!req->status->IsCorruption()) {
|
|
continue;
|
|
}
|
|
|
|
stale_statuses.emplace_back(*req->status);
|
|
*req->status = Status::OK();
|
|
blob_req.second.reset();
|
|
retry_blob_reqs.emplace_back(req, std::unique_ptr<BlobContents>());
|
|
}
|
|
|
|
uint64_t refreshed_bytes_read = 0;
|
|
fresh_reader->MultiGetBlob(read_options, allocator, retry_blob_reqs,
|
|
&refreshed_bytes_read);
|
|
_bytes_read += refreshed_bytes_read;
|
|
|
|
bool install_fresh_reader = false;
|
|
for (size_t i = 0; i < retry_blob_reqs.size(); ++i) {
|
|
auto& retried_blob_req = retry_blob_reqs[i];
|
|
BlobReadRequest* const retried_req = retried_blob_req.first;
|
|
assert(retried_req != nullptr);
|
|
if (retried_req->status->ok()) {
|
|
install_fresh_reader = true;
|
|
} else {
|
|
*retried_req->status = AppendBlobRefreshRetryFailure(
|
|
stale_statuses[i], *retried_req->status);
|
|
}
|
|
|
|
for (auto& blob_req : _blob_reqs) {
|
|
if (blob_req.first != retried_req) {
|
|
continue;
|
|
}
|
|
|
|
blob_req.second = std::move(retried_blob_req.second);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (install_fresh_reader) {
|
|
CacheHandleGuard<BlobFileReader> ignored_reader;
|
|
blob_file_cache_
|
|
->RefreshBlobFileReader(file_number, &fresh_reader, &ignored_reader)
|
|
.PermitUncheckedError();
|
|
}
|
|
}
|
|
|
|
if (blob_cache_ && read_options.fill_cache) {
|
|
// If filling cache is allowed and a cache is configured, try to put
|
|
// the blob(s) to the cache.
|
|
for (auto& [req, blob_contents] : _blob_reqs) {
|
|
assert(req);
|
|
|
|
if (req->status->ok()) {
|
|
CacheHandleGuard<BlobContents> blob_handle;
|
|
const CacheKey cache_key = base_cache_key.WithOffset(req->offset);
|
|
const Slice key = cache_key.AsSlice();
|
|
s = PutBlobIntoCache(key, &blob_contents, &blob_handle);
|
|
if (!s.ok()) {
|
|
*req->status = s;
|
|
} else {
|
|
PinCachedBlob(&blob_handle, req->result);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
for (auto& [req, blob_contents] : _blob_reqs) {
|
|
assert(req);
|
|
|
|
if (req->status->ok()) {
|
|
PinOwnedBlob(&blob_contents, req->result);
|
|
}
|
|
}
|
|
}
|
|
|
|
total_bytes += _bytes_read;
|
|
if (bytes_read) {
|
|
*bytes_read = total_bytes;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool BlobSource::TEST_BlobInCache(uint64_t file_number, uint64_t file_size,
|
|
uint64_t offset, size_t* charge) const {
|
|
const CacheKey cache_key = GetCacheKey(file_number, file_size, offset);
|
|
const Slice key = cache_key.AsSlice();
|
|
|
|
CacheHandleGuard<BlobContents> blob_handle;
|
|
const Status s = GetBlobFromCache(key, &blob_handle);
|
|
|
|
if (s.ok() && blob_handle.GetValue() != nullptr) {
|
|
if (charge) {
|
|
const Cache* const cache = blob_handle.GetCache();
|
|
assert(cache);
|
|
|
|
Cache::Handle* const handle = blob_handle.GetCacheHandle();
|
|
assert(handle);
|
|
|
|
*charge = cache->GetUsage(handle);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
} // namespace ROCKSDB_NAMESPACE
|