Files
rocksdb/table/block_based/embedded_blob_resolving_iterator.h
T
Peter Dillinger 741539da9b Fix backward iteration over embedded blob SSTs (#14902)
Summary:
D110140135/https://github.com/facebook/rocksdb/issues/14896 worked around a db_stress failure by disabling test_backward_scan whenever embedded-blob SSTs are ingested. The real limitation was in EmbeddedBlobResolvingIterator: DBIter requires the underlying iterator's value to be pinned for backward iteration (Prev/SeekForPrev), and rebuilt wide-column entity values (with same-file blob columns) were stored in an iterator-owned std::string that was cleared on every reposition, so IsValuePinned() returned false and DBIter returned NotSupported (or tripped the IsValuePinned() assertion in FindValueForCurrentKeyUsingSeek). Whole-value same-file blobs were already pinned and worked.

This change makes the rebuilt wide-column value pinnable like a whole-value blob: it is moved into a heap buffer and pinned into resolved_pinned_value_, whose cleanup ResetState() hands to the PinnedIteratorsManager across repositioning so the value survives backward iteration. The now-redundant resolved_value_ member is removed and value()/IsValuePinned() simplified. The Get/MultiGet resolution path (MaybeResolveEmbeddedValue) is unchanged. The db_stress test_backward_scan workaround is removed now that backward iteration over embedded blobs (including wide-column entities) is supported.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14902

Test Plan:
Added DBBlobIndexTest.EmbeddedBlobBackwardIteration: ingests an embedded-blob SST with whole-value blobs plus a mixed wide-column entity (inline default column + embedded same-file blob column), then iterates backward via SeekToLast/Prev and SeekForPrev, verifying values and columns. Verified it fails before the fix (NotSupported at the entity) and passes after.

Full db_blob_index_test and sst_file_reader_test suites pass. make format-auto clean. Direct db_stress run with
ingest_external_file_with_embedded_blobs=1, use_put_entity_one_in=3, test_backward_scan=1, iterpercent>0 ran with no divergence/NotSupported/ assert.

Reviewed By: anand1976

Differential Revision: D110271453

Pulled By: pdillinger

fbshipit-source-id: 501a50d3c97bac9a217b604c43c8748abbb563c9
2026-07-01 10:02:52 -07:00

379 lines
14 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).
// 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).
#pragma once
#include <limits>
#include <string>
#include "db/blob/blob_index.h"
#include "db/dbformat.h"
#include "db/pinned_iterators_manager.h"
#include "rocksdb/slice.h"
#include "table/block_based/block_based_table_reader.h"
#include "table/internal_iterator.h"
namespace ROCKSDB_NAMESPACE {
// Wraps a block-based table iterator and resolves same-file ("embedded") blob
// references so that callers never see an unresolved same-file BlobIndex. It
// makes an embedded entry look like an ordinary key/value entry:
// - key(): a whole-value same-file BlobIndex's internal key has its value
// type rewritten from kTypeBlobIndex back to kTypeValue.
// - value(): the referenced same-file blob payload is materialized (a wide-
// column entity's same-file blob columns are rewritten inline).
// Traditional (separate-file) BlobIndex entries and plain entries pass through
// untouched.
//
// Why a wrapper instead of putting this in BlockBasedTableIterator:
// - The wrapped iterator is created with allow_unprepared_value=false, so it
// always materializes the data block and never sits in the "first key from
// index" deferred state. That keeps the "never expose an unresolved
// same-file blob index" invariant structural rather than relying on every
// downstream consumer re-checking the key type after PrepareValue().
// - The wrapper preserves value laziness on its own: key() does only the
// cheap key-type rewrite (no blob-region I/O), while the payload is read
// lazily in value()/PrepareValue().
// - It keeps the embedded-blob concern out of the hot, complex
// BlockBasedTableIterator, and is only instantiated for SSTs that actually
// carry an embedded blob segment.
//
// Only created when the table advertises an embedded blob segment and the
// caller is not the SST dump tool (which must keep seeing raw BlobIndex
// values).
class EmbeddedBlobResolvingIterator : public InternalIterator {
public:
// `iter` is owned by this wrapper. `arena_mode` must match how `iter` (and
// this wrapper) were allocated so the destructor frees `iter` correctly.
EmbeddedBlobResolvingIterator(const BlockBasedTable* table,
const ReadOptions& read_options,
InternalIterator* iter, bool arena_mode)
: table_(table),
read_options_(read_options),
iter_(iter),
arena_mode_(arena_mode) {
status_.PermitUncheckedError();
}
~EmbeddedBlobResolvingIterator() override {
if (arena_mode_) {
iter_->~InternalIteratorBase<Slice>();
} else {
delete iter_;
}
}
// No copying allowed
EmbeddedBlobResolvingIterator(const EmbeddedBlobResolvingIterator&) = delete;
EmbeddedBlobResolvingIterator& operator=(
const EmbeddedBlobResolvingIterator&) = delete;
bool Valid() const override { return iter_->Valid() && status_.ok(); }
void SeekToFirst() override {
ResetState();
iter_->SeekToFirst();
}
void SeekToLast() override {
ResetState();
iter_->SeekToLast();
}
void Seek(const Slice& target) override {
ResetState();
iter_->Seek(target);
}
void SeekForPrev(const Slice& target) override {
ResetState();
iter_->SeekForPrev(target);
}
void Next() override {
ResetState();
iter_->Next();
}
bool NextAndGetResult(IterateResult* result) override {
ResetState();
iter_->Next();
if (!Valid()) {
return false;
}
result->key = key();
result->bound_check_result = iter_->UpperBoundCheckResult();
result->value_prepared = false;
// key() may have set a (corruption) status while resolving the key type.
return Valid();
}
void Prev() override {
ResetState();
iter_->Prev();
}
Slice key() const override {
assert(Valid());
if (ResolveKeyType() && key_resolved_) {
return Slice(resolved_internal_key_);
}
// ResolveKeyType() may have set a (corruption-only, no I/O) error. This
// const accessor falls back to the raw key; the error is still surfaced
// through status()/Valid(), so permit it from being unchecked here.
status_.PermitUncheckedError();
return iter_->key();
}
Slice user_key() const override { return ExtractUserKey(key()); }
Slice value() const override {
assert(Valid());
if (MaterializeValue() && value_resolved_) {
// The resolved value -- a whole-value blob payload (pinned in the blob
// cache or an owned buffer) or a rebuilt wide-column value (an owned
// buffer) -- lives in resolved_pinned_value_ and is returned without a
// copy.
return Slice(resolved_pinned_value_);
}
// MaterializeValue() may have set an error (corruption or blob-region I/O).
// Fall back to the raw value; the error is surfaced via status()/Valid().
status_.PermitUncheckedError();
return iter_->value();
}
bool PrepareValue() override {
assert(Valid());
if (!iter_->PrepareValue()) {
return false;
}
return MaterializeValue();
}
Status status() const override {
if (!status_.ok()) {
return status_;
}
return iter_->status();
}
uint64_t write_unix_time() const override { return iter_->write_unix_time(); }
bool IsKeyPinned() const override {
// The rewritten key is owned by this wrapper, so it is not pinned.
if (key_resolved_) {
return false;
}
return iter_->IsKeyPinned();
}
bool IsValuePinned() const override {
if (value_resolved_) {
// The resolved value -- a whole-value blob payload (pinned in the blob
// cache or an owned buffer) or a rebuilt wide-column value (an owned
// buffer) -- lives in resolved_pinned_value_. The IsValuePinned()
// contract requires the value to stay valid until the iterator is deleted
// / ReleasePinnedData is called, so only advertise it as pinned when a
// PinnedIteratorsManager is active to take over the pin's cleanup across
// repositioning (see ResetState).
return value_is_pinned_ && pinned_iters_mgr_ != nullptr &&
pinned_iters_mgr_->PinningEnabled();
}
return iter_->IsValuePinned();
}
void SetPinnedItersMgr(PinnedIteratorsManager* pinned_iters_mgr) override {
pinned_iters_mgr_ = pinned_iters_mgr;
iter_->SetPinnedItersMgr(pinned_iters_mgr);
}
IterBoundCheck UpperBoundCheckResult() override {
return iter_->UpperBoundCheckResult();
}
bool MayBeOutOfLowerBound() override { return iter_->MayBeOutOfLowerBound(); }
void SetRangeDelReadSeqno(SequenceNumber read_seqno) override {
iter_->SetRangeDelReadSeqno(read_seqno);
}
bool IsDeleteRangeSentinelKey() const override {
return iter_->IsDeleteRangeSentinelKey();
}
void GetReadaheadState(ReadaheadFileInfo* readahead_file_info) override {
iter_->GetReadaheadState(readahead_file_info);
}
void SetReadaheadState(ReadaheadFileInfo* readahead_file_info) override {
iter_->SetReadaheadState(readahead_file_info);
}
Status GetProperty(std::string prop_name, std::string* prop) override {
return iter_->GetProperty(std::move(prop_name), prop);
}
void Prepare(const MultiScanArgs* scan_opts) override {
iter_->Prepare(scan_opts);
}
private:
// Cleanup for a heap-allocated std::string holding a rebuilt (wide-column)
// value pinned into resolved_pinned_value_.
static void ReleaseResolvedValueBuffer(void* arg1, void* /*arg2*/) {
delete static_cast<std::string*>(arg1);
}
void ResetState() {
status_.PermitUncheckedError();
status_ = Status::OK();
key_prepared_ = false;
value_prepared_ = false;
key_resolved_ = false;
value_resolved_ = false;
resolved_internal_key_.clear();
if (value_is_pinned_) {
// If a PinnedIteratorsManager is active it has been told (via
// IsValuePinned) that the previous entry's pinned value stays valid until
// ReleasePinnedData; hand the pin's cleanup to it so the slice outlives
// this reposition. Otherwise just release the pin now.
if (pinned_iters_mgr_ != nullptr && pinned_iters_mgr_->PinningEnabled()) {
resolved_pinned_value_.DelegateCleanupsTo(pinned_iters_mgr_);
}
value_is_pinned_ = false;
}
resolved_pinned_value_.Reset();
}
// Cheap, key-only resolution: for a whole-value same-file BlobIndex entry,
// computes the logical internal key with its value type rewritten from
// kTypeBlobIndex to kTypeValue. Only touches data already in the data block
// (the key and the inline serialized BlobIndex) -- it does NOT read the blob
// region. No-op (returns true) for entries that are not a same-file
// BlobIndex. Returns false and sets `status_` (corruption only) on a
// malformed key/BlobIndex.
bool ResolveKeyType() const {
if (!iter_->Valid()) {
return true;
}
if (key_prepared_) {
return status_.ok();
}
key_prepared_ = true;
const Slice current_key = iter_->key();
if (ExtractValueType(current_key) != kTypeBlobIndex) {
return true;
}
ParsedInternalKey parsed_key;
// Only data-corruption errors are possible here (no I/O).
status_ = ParseInternalKey(current_key, &parsed_key, false /* log_err */);
if (!status_.ok()) {
return false;
}
BlobIndex blob_index;
// Decodes the inline BlobIndex from the data block; still no blob-region
// IO.
status_ = blob_index.DecodeFrom(iter_->value());
if (!status_.ok()) {
return false;
}
if (!blob_index.IsSameFile()) {
// Traditional (separate-file) BlobDB index; leave the key type as
// kTypeBlobIndex for the higher layers to resolve.
return true;
}
InternalKey resolved_key(parsed_key.user_key, parsed_key.sequence,
kTypeValue);
const Slice encoded_resolved_key = resolved_key.Encode();
resolved_internal_key_.assign(encoded_resolved_key.data(),
encoded_resolved_key.size());
key_resolved_ = true;
return true;
}
// Heavyweight value resolution: materializes the logical value for the
// current entry. For a same-file BlobIndex entry this reads the blob payload
// (may do I/O); for a wide-column entity it rewrites same-file blob columns
// inline. For a BlobIndex entry it first resolves the key type so key() and
// value() agree. No-op (returns true) for plain entries. Returns false and
// sets `status_` (corruption or I/O error) on failure.
bool MaterializeValue() const {
if (!iter_->Valid()) {
return true;
}
const Slice current_key = iter_->key();
const ValueType value_type = ExtractValueType(current_key);
if (value_type != kTypeBlobIndex && value_type != kTypeWideColumnEntity) {
return true;
}
if (value_type == kTypeBlobIndex && !ResolveKeyType()) {
return false;
}
if (value_prepared_) {
return status_.ok();
}
value_prepared_ = true;
std::string resolved_key;
std::string resolved_value;
bool resolved = false;
bool value_pinned = false;
status_ = table_->MaybeResolveEmbeddedValue(
read_options_, current_key, iter_->value(), &resolved_key,
&resolved_value, &resolved, &resolved_pinned_value_, &value_pinned);
if (!status_.ok()) {
return false;
}
if (!resolved) {
return true;
}
if (!resolved_key.empty() && !key_resolved_) {
resolved_internal_key_ = std::move(resolved_key);
key_resolved_ = true;
}
if (!value_pinned) {
// Built (wide-column) value: move it into a heap buffer and pin it into
// resolved_pinned_value_ so the value is pinnable like a whole-value
// blob. DBIter requires a pinned value to back up over an entry
// (Prev/SeekForPrev); the buffer's cleanup is handed to the
// PinnedIteratorsManager across repositioning (see ResetState).
auto* owned_value = new std::string(std::move(resolved_value));
resolved_pinned_value_.PinSlice(Slice(*owned_value),
&ReleaseResolvedValueBuffer, owned_value,
nullptr);
}
// Either way the resolved value now lives, pinned, in
// resolved_pinned_value_.
value_is_pinned_ = true;
value_resolved_ = true;
return true;
}
const BlockBasedTable* table_;
const ReadOptions& read_options_;
InternalIterator* iter_;
const bool arena_mode_;
PinnedIteratorsManager* pinned_iters_mgr_ = nullptr;
// Cached resolution state for the current entry, reset on every reposition.
// Mutable because resolution is driven lazily from the const key()/value()
// accessors.
mutable Status status_;
mutable std::string resolved_internal_key_;
// Holds the resolved value for the current entry without a copy: a
// whole-value same-file blob payload (pinned in the blob cache, or an owned
// buffer when no cache is configured) or a rebuilt wide-column value (an
// owned heap buffer). Populated whenever value_is_pinned_ is true.
mutable PinnableSlice resolved_pinned_value_;
mutable bool key_prepared_ = false;
mutable bool value_prepared_ = false;
mutable bool key_resolved_ = false;
mutable bool value_resolved_ = false;
mutable bool value_is_pinned_ = false;
};
} // namespace ROCKSDB_NAMESPACE