Coverage Report

Created: 2026-08-14 08:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/rocksdb/db/blob/blob_source.cc
Line
Count
Source
1
//  Copyright (c) Meta Platforms, Inc. and affiliates.
2
//  This source code is licensed under both the GPLv2 (found in the
3
//  COPYING file in the root directory) and Apache 2.0 License
4
//  (found in the LICENSE.Apache file in the root directory).
5
6
#include "db/blob/blob_source.h"
7
8
#include <cassert>
9
#include <string>
10
11
#include "cache/cache_reservation_manager.h"
12
#include "cache/charged_cache.h"
13
#include "db/blob/blob_contents.h"
14
#include "db/blob/blob_file_reader.h"
15
#include "db/blob/blob_gen2_format.h"
16
#include "db/blob/blob_log_format.h"
17
#include "file/random_access_file_reader.h"
18
#include "memory/memory_allocator_impl.h"
19
#include "monitoring/statistics_impl.h"
20
#include "options/cf_options.h"
21
#include "table/get_context.h"
22
#include "table/multiget_context.h"
23
24
namespace ROCKSDB_NAMESPACE {
25
26
namespace {
27
28
Status AppendBlobRefreshRetryFailure(const Status& stale_status,
29
0
                                     const Status& retry_status) {
30
0
  assert(stale_status.IsCorruption());
31
0
  assert(!retry_status.ok());
32
0
  if (retry_status.IsCorruption()) {
33
0
    return retry_status;
34
0
  }
35
0
  return Status::CopyAppendMessage(
36
0
      stale_status, "; refresh retry failed: ", retry_status.ToString());
37
0
}
38
39
// Runs read(reader) against the blob-file reader for `file_number`. On a
40
// Corruption -- which a stale cached reader (e.g. the physical file was
41
// replaced) can surface -- evicts the cached reader, reopens it uncached,
42
// retries read() once, and refreshes the cache on success. Shared by the
43
// single-blob read paths (GetBlob / GetBlobRange) so the stale-file recovery
44
// lives in one place.
45
//
46
// `read` is invoked with a BlobFileReader* and returns a Status; because it may
47
// run twice, it must re-initialize any output it populates on each call (and is
48
// taken by const reference rather than forwarded, since it is not moved-from).
49
template <typename ReadFn>
50
Status ReadBlobWithReaderRetry(BlobFileCache* blob_file_cache,
51
                               const ReadOptions& read_options,
52
0
                               uint64_t file_number, const ReadFn& read) {
53
0
  CacheHandleGuard<BlobFileReader> blob_file_reader;
54
0
  Status s = blob_file_cache->GetBlobFileReader(read_options, file_number,
55
0
                                                &blob_file_reader);
56
0
  if (!s.ok()) {
57
0
    return s;
58
0
  }
59
0
  assert(blob_file_reader.GetValue());
60
61
0
  s = read(blob_file_reader.GetValue());
62
0
  if (!s.IsCorruption()) {
63
0
    return s;
64
0
  }
65
66
0
  const Status stale_status = s;
67
0
  blob_file_reader.Reset();
68
0
  blob_file_cache->Evict(file_number);
69
70
0
  std::unique_ptr<BlobFileReader> fresh_reader;
71
0
  s = blob_file_cache->OpenBlobFileReaderUncached(
72
0
      read_options, file_number, &fresh_reader,
73
0
      /*allow_footer_skip_retry=*/false);
74
0
  if (!s.ok()) {
75
0
    return AppendBlobRefreshRetryFailure(stale_status, s);
76
0
  }
77
78
0
  s = read(fresh_reader.get());
79
0
  if (!s.ok()) {
80
0
    return AppendBlobRefreshRetryFailure(stale_status, s);
81
0
  }
82
83
0
  CacheHandleGuard<BlobFileReader> ignored_reader;
84
0
  blob_file_cache
85
0
      ->RefreshBlobFileReader(file_number, &fresh_reader, &ignored_reader)
86
0
      .PermitUncheckedError();
87
0
  return s;
88
0
}
Unexecuted instantiation: blob_source.cc:rocksdb::Status rocksdb::(anonymous namespace)::ReadBlobWithReaderRetry<rocksdb::BlobSource::GetBlob(rocksdb::ReadOptions const&, rocksdb::Slice const&, unsigned long, unsigned long, unsigned long, unsigned long, rocksdb::CompressionType, rocksdb::FilePrefetchBuffer*, rocksdb::PinnableSlice*, unsigned long*)::$_0>(rocksdb::BlobFileCache*, rocksdb::ReadOptions const&, unsigned long, rocksdb::BlobSource::GetBlob(rocksdb::ReadOptions const&, rocksdb::Slice const&, unsigned long, unsigned long, unsigned long, unsigned long, rocksdb::CompressionType, rocksdb::FilePrefetchBuffer*, rocksdb::PinnableSlice*, unsigned long*)::$_0 const&)
Unexecuted instantiation: blob_source.cc:rocksdb::Status rocksdb::(anonymous namespace)::ReadBlobWithReaderRetry<rocksdb::BlobSource::GetBlobRange(rocksdb::ReadOptions const&, rocksdb::Slice const&, unsigned long, unsigned long, unsigned long, unsigned long, rocksdb::CompressionType, unsigned long, unsigned long, rocksdb::PinnableSlice*, unsigned long*)::$_0>(rocksdb::BlobFileCache*, rocksdb::ReadOptions const&, unsigned long, rocksdb::BlobSource::GetBlobRange(rocksdb::ReadOptions const&, rocksdb::Slice const&, unsigned long, unsigned long, unsigned long, unsigned long, rocksdb::CompressionType, unsigned long, unsigned long, rocksdb::PinnableSlice*, unsigned long*)::$_0 const&)
89
90
// Records lazy wide-column read metrics for one actual storage read issued
91
// while resolving a lazy result (attributed via Env::IOActivity::kLazyResolve):
92
// `bytes_read` bytes were read from storage. Covers both whole-column and
93
// partial reads; not called for cache hits (no storage read).
94
0
void RecordLazyRead(Statistics* statistics, uint64_t bytes_read) {
95
0
  RecordTick(statistics, BLOB_DB_LAZY_READ_COUNT);
96
0
  RecordTick(statistics, BLOB_DB_LAZY_READ_BYTES, bytes_read);
97
0
}
98
99
// Additionally records the partial-read metrics for one actual partial
100
// (byte-range) read: `bytes_read` bytes were fetched instead of the column's
101
// full `value_size`. Call alongside RecordLazyRead (a partial read is also a
102
// lazy read).
103
void RecordLazyPartialRead(Statistics* statistics, uint64_t bytes_read,
104
0
                           uint64_t value_size) {
105
0
  RecordTick(statistics, BLOB_DB_LAZY_PARTIAL_READ_COUNT);
106
0
  if (value_size > bytes_read) {
107
0
    RecordTick(statistics, BLOB_DB_LAZY_PARTIAL_BYTES_SAVED,
108
0
               value_size - bytes_read);
109
0
  }
110
0
}
111
112
// Pins the sub-range [range_offset, range_offset + range_length) of a cache-hit
113
// whole blob value into *value (clamped: an offset at/past the end yields
114
// empty, length is clamped to the remainder), keeping the cached bytes alive by
115
// transferring the cache handle into *value. Shared by the range-read cache-hit
116
// paths (GetBlobRange / GetSimpleGen2BlobRange).
117
void PinCacheHitSubRange(CacheHandleGuard<BlobContents>* blob_handle,
118
                         uint64_t range_offset, size_t range_length,
119
0
                         PinnableSlice* value) {
120
0
  const Slice full = blob_handle->GetValue()->data();
121
0
  Slice sub;
122
0
  if (range_offset < full.size()) {
123
0
    const size_t off = static_cast<size_t>(range_offset);
124
0
    const size_t avail = full.size() - off;
125
0
    const size_t len = range_length > avail ? avail : range_length;
126
0
    sub = Slice(full.data() + off, len);
127
0
  }  // else: offset at/past end -> empty (not an error)
128
129
0
  value->Reset();
130
0
  constexpr Cleanable* cleanable = nullptr;
131
0
  value->PinSlice(sub, cleanable);
132
0
  blob_handle->TransferTo(value);
133
0
}
134
135
}  // namespace
136
137
BlobSource::BlobSource(const ImmutableOptions& immutable_options,
138
                       const MutableCFOptions& mutable_cf_options,
139
                       const std::string& db_id,
140
                       const std::string& db_session_id,
141
                       BlobFileCache* blob_file_cache)
142
107k
    : db_id_(db_id),
143
107k
      db_session_id_(db_session_id),
144
107k
      statistics_(immutable_options.statistics.get()),
145
107k
      blob_file_cache_(blob_file_cache),
146
107k
      blob_cache_(immutable_options.blob_cache),
147
107k
      lowest_used_cache_tier_(immutable_options.lowest_used_cache_tier) {
148
107k
  auto bbto =
149
107k
      mutable_cf_options.table_factory->GetOptions<BlockBasedTableOptions>();
150
107k
  if (bbto &&
151
107k
      bbto->cache_usage_options.options_overrides.at(CacheEntryRole::kBlobCache)
152
107k
              .charged == CacheEntryRoleOptions::Decision::kEnabled) {
153
0
    blob_cache_ = SharedCacheInterface{std::make_shared<ChargedCache>(
154
0
        immutable_options.blob_cache, bbto->block_cache)};
155
0
  }
156
107k
}
157
158
107k
BlobSource::~BlobSource() = default;
159
160
Status BlobSource::GetBlobFromCache(
161
0
    const Slice& cache_key, CacheHandleGuard<BlobContents>* cached_blob) const {
162
0
  assert(blob_cache_);
163
0
  assert(!cache_key.empty());
164
0
  assert(cached_blob);
165
0
  assert(cached_blob->IsEmpty());
166
167
0
  Cache::Handle* cache_handle = nullptr;
168
0
  cache_handle = GetEntryFromCache(cache_key);
169
0
  if (cache_handle != nullptr) {
170
0
    *cached_blob =
171
0
        CacheHandleGuard<BlobContents>(blob_cache_.get(), cache_handle);
172
173
0
    assert(cached_blob->GetValue());
174
175
0
    PERF_COUNTER_ADD(blob_cache_hit_count, 1);
176
0
    PERF_COUNTER_ADD(blob_cache_read_byte, cached_blob->GetValue()->size());
177
0
    RecordTick(statistics_, BLOB_DB_CACHE_HIT);
178
0
    RecordTick(statistics_, BLOB_DB_CACHE_BYTES_READ,
179
0
               cached_blob->GetValue()->size());
180
181
0
    return Status::OK();
182
0
  }
183
184
0
  RecordTick(statistics_, BLOB_DB_CACHE_MISS);
185
186
0
  return Status::NotFound("Blob not found in cache");
187
0
}
188
189
Status BlobSource::PutBlobIntoCache(
190
    const Slice& cache_key, std::unique_ptr<BlobContents>* blob,
191
0
    CacheHandleGuard<BlobContents>* cached_blob) const {
192
0
  assert(blob_cache_);
193
0
  assert(!cache_key.empty());
194
0
  assert(blob);
195
0
  assert(*blob);
196
0
  assert(cached_blob);
197
0
  assert(cached_blob->IsEmpty());
198
199
0
  TypedHandle* cache_handle = nullptr;
200
0
  const Status s = InsertEntryIntoCache(cache_key, blob->get(), &cache_handle,
201
0
                                        Cache::Priority::BOTTOM);
202
0
  if (s.ok()) {
203
0
    blob->release();
204
205
0
    assert(cache_handle != nullptr);
206
0
    *cached_blob =
207
0
        CacheHandleGuard<BlobContents>(blob_cache_.get(), cache_handle);
208
209
0
    assert(cached_blob->GetValue());
210
211
0
    RecordTick(statistics_, BLOB_DB_CACHE_ADD);
212
0
    RecordTick(statistics_, BLOB_DB_CACHE_BYTES_WRITE,
213
0
               cached_blob->GetValue()->size());
214
215
0
  } else {
216
0
    RecordTick(statistics_, BLOB_DB_CACHE_ADD_FAILURES);
217
0
  }
218
219
0
  return s;
220
0
}
221
222
0
BlobSource::TypedHandle* BlobSource::GetEntryFromCache(const Slice& key) const {
223
0
  return blob_cache_.LookupFull(key, nullptr /* context */,
224
0
                                Cache::Priority::BOTTOM, statistics_,
225
0
                                lowest_used_cache_tier_);
226
0
}
227
228
void BlobSource::PinCachedBlob(CacheHandleGuard<BlobContents>* cached_blob,
229
0
                               PinnableSlice* value) {
230
0
  assert(cached_blob);
231
0
  assert(cached_blob->GetValue());
232
0
  assert(value);
233
234
  // To avoid copying the cached blob into the buffer provided by the
235
  // application, we can simply transfer ownership of the cache handle to
236
  // the target PinnableSlice. This has the potential to save a lot of
237
  // CPU, especially with large blob values.
238
239
0
  value->Reset();
240
241
0
  constexpr Cleanable* cleanable = nullptr;
242
0
  value->PinSlice(cached_blob->GetValue()->data(), cleanable);
243
244
0
  cached_blob->TransferTo(value);
245
0
}
246
247
void BlobSource::PinOwnedBlob(std::unique_ptr<BlobContents>* owned_blob,
248
0
                              PinnableSlice* value) {
249
0
  assert(owned_blob);
250
0
  assert(*owned_blob);
251
0
  assert(value);
252
253
0
  BlobContents* const blob = owned_blob->release();
254
0
  assert(blob);
255
256
0
  value->Reset();
257
0
  value->PinSlice(
258
0
      blob->data(),
259
0
      [](void* arg1, void* /* arg2 */) {
260
0
        delete static_cast<BlobContents*>(arg1);
261
0
      },
262
0
      blob, nullptr);
263
0
}
264
265
Status BlobSource::InsertEntryIntoCache(const Slice& key, BlobContents* value,
266
                                        TypedHandle** cache_handle,
267
0
                                        Cache::Priority priority) const {
268
0
  return blob_cache_.InsertFull(key, value, value->ApproximateMemoryUsage(),
269
0
                                cache_handle, priority,
270
0
                                lowest_used_cache_tier_);
271
0
}
272
273
Status BlobSource::GetBlob(const ReadOptions& read_options,
274
                           const Slice& user_key, uint64_t file_number,
275
                           uint64_t offset, uint64_t file_size,
276
                           uint64_t value_size,
277
                           CompressionType compression_type,
278
                           FilePrefetchBuffer* prefetch_buffer,
279
0
                           PinnableSlice* value, uint64_t* bytes_read) {
280
0
  assert(value);
281
282
0
  Status s;
283
284
0
  const CacheKey cache_key = GetCacheKey(file_number, file_size, offset);
285
286
0
  CacheHandleGuard<BlobContents> blob_handle;
287
288
  // First, try to get the blob from the cache
289
  //
290
  // If blob cache is enabled, we'll try to read from it.
291
0
  if (blob_cache_) {
292
0
    Slice key = cache_key.AsSlice();
293
0
    s = GetBlobFromCache(key, &blob_handle);
294
0
    if (s.ok()) {
295
0
      PinCachedBlob(&blob_handle, value);
296
297
      // For consistency, the size of on-disk (possibly compressed) blob record
298
      // is assigned to bytes_read.
299
0
      uint64_t adjustment =
300
0
          read_options.verify_checksums
301
0
              ? BlobLogRecord::CalculateAdjustmentForRecordHeader(
302
0
                    user_key.size())
303
0
              : 0;
304
0
      assert(offset >= adjustment);
305
306
0
      uint64_t record_size = value_size + adjustment;
307
0
      if (bytes_read) {
308
0
        *bytes_read = record_size;
309
0
      }
310
0
      return s;
311
0
    }
312
0
  }
313
314
0
  assert(blob_handle.IsEmpty());
315
316
0
  const bool no_io = read_options.read_tier == kBlockCacheTier;
317
0
  if (no_io) {
318
0
    s = Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
319
0
    return s;
320
0
  }
321
322
  // Can't find the blob from the cache. Since I/O is allowed, read from the
323
  // file.
324
0
  std::unique_ptr<BlobContents> blob_contents;
325
326
0
  {
327
0
    MemoryAllocator* const allocator =
328
0
        (blob_cache_ && read_options.fill_cache)
329
0
            ? blob_cache_.get()->memory_allocator()
330
0
            : nullptr;
331
332
0
    uint64_t read_size = 0;
333
0
    s = ReadBlobWithReaderRetry(
334
0
        blob_file_cache_, read_options, file_number,
335
0
        [&](BlobFileReader* reader) {
336
0
          if (compression_type != reader->GetCompressionType()) {
337
0
            return Status::Corruption(
338
0
                "Compression type mismatch when reading blob");
339
0
          }
340
0
          blob_contents.reset();
341
0
          read_size = 0;
342
0
          return reader->GetBlob(read_options, user_key, offset, value_size,
343
0
                                 compression_type, prefetch_buffer, allocator,
344
0
                                 &blob_contents, &read_size);
345
0
        });
346
0
    if (!s.ok()) {
347
0
      return s;
348
0
    }
349
0
    if (bytes_read) {
350
0
      *bytes_read = read_size;
351
0
    }
352
    // Whole-column read on the lazy resolve path (partial reads go through
353
    // GetBlobRange). Counts the storage read; partial reads are not counted
354
    // here.
355
0
    if (read_options.io_activity == Env::IOActivity::kLazyResolve) {
356
0
      RecordLazyRead(statistics_, read_size);
357
0
    }
358
0
  }
359
360
0
  if (blob_cache_ && read_options.fill_cache) {
361
    // If filling cache is allowed and a cache is configured, try to put the
362
    // blob to the cache.
363
0
    Slice key = cache_key.AsSlice();
364
0
    s = PutBlobIntoCache(key, &blob_contents, &blob_handle);
365
0
    if (!s.ok()) {
366
0
      return s;
367
0
    }
368
369
0
    PinCachedBlob(&blob_handle, value);
370
0
  } else {
371
0
    PinOwnedBlob(&blob_contents, value);
372
0
  }
373
374
0
  assert(s.ok());
375
0
  return s;
376
0
}
377
378
Status BlobSource::GetBlobRange(const ReadOptions& read_options,
379
                                const Slice& user_key, uint64_t file_number,
380
                                uint64_t offset, uint64_t file_size,
381
                                uint64_t value_size,
382
                                CompressionType compression_type,
383
                                uint64_t range_offset, size_t range_length,
384
0
                                PinnableSlice* value, uint64_t* bytes_read) {
385
0
  assert(value);
386
  // Partial reads are for uncompressed blobs only; the caller decides this (a
387
  // compressed column takes the whole-value GetBlob path and slices).
388
0
  assert(compression_type == kNoCompression);
389
  // Range reads are (currently) only issued while resolving a lazy result, so
390
  // the lazy read stats below are recorded unconditionally (unlike GetBlob,
391
  // which gates on the activity). Enforce that invariant here.
392
0
  assert(read_options.io_activity == Env::IOActivity::kLazyResolve);
393
394
0
  Status s;
395
396
0
  const CacheKey cache_key = GetCacheKey(file_number, file_size, offset);
397
398
0
  CacheHandleGuard<BlobContents> blob_handle;
399
400
  // First, probe the blob cache for the whole value. On a hit, slice the
401
  // requested sub-range out of the cached value while pinning the cache handle
402
  // (zero-copy, no disk read). A partial read never inserts into the cache.
403
0
  if (blob_cache_) {
404
0
    Slice key = cache_key.AsSlice();
405
0
    s = GetBlobFromCache(key, &blob_handle);
406
0
    if (s.ok()) {
407
0
      PinCacheHitSubRange(&blob_handle, range_offset, range_length, value);
408
0
      if (bytes_read) {
409
0
        *bytes_read = 0;  // served from cache; no disk read
410
0
      }
411
0
      return s;
412
0
    }
413
0
  }
414
415
0
  assert(blob_handle.IsEmpty());
416
417
0
  const bool no_io = read_options.read_tier == kBlockCacheTier;
418
0
  if (no_io) {
419
0
    return Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
420
0
  }
421
422
  // Cache miss: read only the requested sub-range from the file. The result is
423
  // never inserted into the blob cache (see the header comment).
424
0
  std::unique_ptr<BlobContents> blob_contents;
425
0
  {
426
    // No cache-fill allocator: a partial value is never inserted into the
427
    // cache.
428
0
    constexpr MemoryAllocator* allocator = nullptr;
429
430
0
    uint64_t read_size = 0;
431
    // Reuses the stale-reader retry shared with GetBlob. A range read skips
432
    // whole-record checksum verification, so the retry handles the stale-file
433
    // case (offset/size mismatch surfacing as Corruption), not a payload
434
    // checksum failure.
435
0
    s = ReadBlobWithReaderRetry(
436
0
        blob_file_cache_, read_options, file_number,
437
0
        [&](BlobFileReader* reader) {
438
0
          if (compression_type != reader->GetCompressionType()) {
439
0
            return Status::Corruption(
440
0
                "Compression type mismatch when reading blob");
441
0
          }
442
0
          blob_contents.reset();
443
0
          read_size = 0;
444
0
          return reader->GetBlobRange(read_options, user_key, offset,
445
0
                                      value_size, range_offset, range_length,
446
0
                                      allocator, &blob_contents, &read_size);
447
0
        });
448
0
    if (!s.ok()) {
449
0
      return s;
450
0
    }
451
0
    if (bytes_read) {
452
0
      *bytes_read = read_size;
453
0
    }
454
0
    RecordLazyRead(statistics_, read_size);
455
0
    RecordLazyPartialRead(statistics_, read_size, value_size);
456
0
  }
457
458
0
  PinOwnedBlob(&blob_contents, value);
459
460
0
  assert(s.ok());
461
0
  return s;
462
0
}
463
464
Status BlobSource::GetSimpleGen2Blob(
465
    const ReadOptions& read_options, const OffsetableCacheKey& base_cache_key,
466
    RandomAccessFileReader* file, uint64_t record_offset, uint64_t payload_size,
467
    ChecksumType checksum_type, uint32_t base_context_checksum,
468
    CompressionType expected_compression, PinnableSlice* value,
469
0
    uint64_t* bytes_read) {
470
0
  assert(value);
471
0
  assert(file);
472
473
0
  const uint64_t record_size = payload_size + kSimpleGen2BlobTrailerSize;
474
475
  // The cache key is derived from the SimpleGen2Blob format (shared scheme with
476
  // block-based SST blocks); see GetSimpleGen2BlobCacheKey.
477
0
  const CacheKey cache_key =
478
0
      GetSimpleGen2BlobCacheKey(base_cache_key, record_offset);
479
480
0
  Status s;
481
482
0
  CacheHandleGuard<BlobContents> blob_handle;
483
484
  // First, try to get the blob from the cache.
485
0
  if (blob_cache_) {
486
0
    Slice key = cache_key.AsSlice();
487
0
    s = GetBlobFromCache(key, &blob_handle);
488
0
    if (s.ok()) {
489
0
      PinCachedBlob(&blob_handle, value);
490
491
      // For consistency, the on-disk record size is assigned to bytes_read on
492
      // both cache hits and misses.
493
0
      if (bytes_read) {
494
0
        *bytes_read = record_size;
495
0
      }
496
0
      return s;
497
0
    }
498
0
  }
499
500
0
  assert(blob_handle.IsEmpty());
501
502
0
  const bool no_io = read_options.read_tier == kBlockCacheTier;
503
0
  if (no_io) {
504
0
    return Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
505
0
  }
506
507
  // Cache miss (or no cache configured). Read the record into a buffer
508
  // allocated from the blob cache's memory allocator when we intend to insert
509
  // it, exposing the uncompressed payload as BlobContents (the trailer just
510
  // sits unused at the tail of the buffer).
511
0
  MemoryAllocator* const allocator = (blob_cache_ && read_options.fill_cache)
512
0
                                         ? blob_cache_.get()->memory_allocator()
513
0
                                         : nullptr;
514
515
0
  CacheAllocationPtr buf =
516
0
      AllocateBlock(static_cast<size_t>(record_size), allocator);
517
0
  s = ReadAndVerifySimpleGen2BlobRecord(
518
0
      read_options, file, record_offset, static_cast<size_t>(payload_size),
519
0
      static_cast<size_t>(record_size), checksum_type, base_context_checksum,
520
0
      expected_compression, buf.get());
521
0
  if (!s.ok()) {
522
0
    return s;
523
0
  }
524
525
0
  std::unique_ptr<BlobContents> blob_contents(
526
0
      new BlobContents(std::move(buf), static_cast<size_t>(payload_size)));
527
528
  // Record the per-read statistics (mirrors BlobFileReader::GetBlob).
529
0
  RecordTick(statistics_, BLOB_DB_BLOB_FILE_BYTES_READ, record_size);
530
0
  PERF_COUNTER_ADD(blob_read_count, 1);
531
0
  PERF_COUNTER_ADD(blob_read_byte, record_size);
532
0
  if (bytes_read) {
533
0
    *bytes_read = record_size;
534
0
  }
535
  // Whole-column read on the lazy resolve path (partial reads go through
536
  // GetSimpleGen2BlobRange).
537
0
  if (read_options.io_activity == Env::IOActivity::kLazyResolve) {
538
0
    RecordLazyRead(statistics_, record_size);
539
0
  }
540
541
0
  if (blob_cache_ && read_options.fill_cache) {
542
    // If filling cache is allowed and a cache is configured, try to put the
543
    // blob into the cache.
544
0
    Slice key = cache_key.AsSlice();
545
0
    s = PutBlobIntoCache(key, &blob_contents, &blob_handle);
546
0
    if (!s.ok()) {
547
0
      return s;
548
0
    }
549
550
0
    PinCachedBlob(&blob_handle, value);
551
0
  } else {
552
0
    PinOwnedBlob(&blob_contents, value);
553
0
  }
554
555
0
  assert(s.ok());
556
0
  return s;
557
0
}
558
559
Status BlobSource::GetSimpleGen2BlobRange(
560
    const ReadOptions& read_options, const OffsetableCacheKey& base_cache_key,
561
    RandomAccessFileReader* file, uint64_t record_offset, uint64_t payload_size,
562
    ChecksumType /*checksum_type*/, uint32_t /*base_context_checksum*/,
563
    CompressionType expected_compression, uint64_t range_offset,
564
0
    size_t range_length, PinnableSlice* value, uint64_t* bytes_read) {
565
0
  assert(value);
566
0
  assert(file);
567
  // Partial reads are for uncompressed payloads only; the caller decides this
568
  // (a compressed column takes the whole-payload GetSimpleGen2Blob path +
569
  // slice).
570
0
  assert(expected_compression == kNoCompression);
571
  // Range reads are (currently) only issued while resolving a lazy result, so
572
  // the lazy read stats below are recorded unconditionally (unlike
573
  // GetSimpleGen2Blob, which gates on the activity). Enforce that invariant
574
  // here.
575
0
  assert(read_options.io_activity == Env::IOActivity::kLazyResolve);
576
577
0
  const CacheKey cache_key =
578
0
      GetSimpleGen2BlobCacheKey(base_cache_key, record_offset);
579
580
0
  Status s;
581
582
0
  CacheHandleGuard<BlobContents> blob_handle;
583
584
  // First, probe the blob cache for the whole payload. On a hit, slice the
585
  // requested sub-range out of the cached payload while pinning the cache
586
  // handle (zero-copy, no disk read). A partial read never inserts into the
587
  // cache.
588
0
  if (blob_cache_) {
589
0
    Slice key = cache_key.AsSlice();
590
0
    s = GetBlobFromCache(key, &blob_handle);
591
0
    if (s.ok()) {
592
0
      PinCacheHitSubRange(&blob_handle, range_offset, range_length, value);
593
0
      if (bytes_read) {
594
0
        *bytes_read = 0;  // served from cache; no disk read
595
0
      }
596
0
      return s;
597
0
    }
598
0
  }
599
600
0
  assert(blob_handle.IsEmpty());
601
602
0
  const bool no_io = read_options.read_tier == kBlockCacheTier;
603
0
  if (no_io) {
604
0
    return Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
605
0
  }
606
607
  // Cache miss: read only the requested sub-range from the file. The result is
608
  // never inserted into the blob cache (a partial payload cannot represent the
609
  // whole-record cache entry).
610
0
  CacheAllocationPtr buf = AllocateBlock(range_length, /*allocator=*/nullptr);
611
0
  s = ReadSimpleGen2BlobRange(read_options, file, record_offset,
612
0
                              static_cast<size_t>(payload_size), range_offset,
613
0
                              range_length, expected_compression, buf.get());
614
0
  if (!s.ok()) {
615
0
    return s;
616
0
  }
617
618
0
  std::unique_ptr<BlobContents> blob_contents(
619
0
      new BlobContents(std::move(buf), range_length));
620
621
0
  RecordTick(statistics_, BLOB_DB_BLOB_FILE_BYTES_READ, range_length);
622
0
  PERF_COUNTER_ADD(blob_read_count, 1);
623
0
  PERF_COUNTER_ADD(blob_read_byte, range_length);
624
0
  RecordLazyRead(statistics_, range_length);
625
0
  RecordLazyPartialRead(statistics_, range_length, payload_size);
626
0
  if (bytes_read) {
627
0
    *bytes_read = range_length;
628
0
  }
629
630
0
  PinOwnedBlob(&blob_contents, value);
631
632
0
  assert(s.ok());
633
0
  return s;
634
0
}
635
636
void BlobSource::MultiGetBlob(const ReadOptions& read_options,
637
                              autovector<BlobFileReadRequests>& blob_reqs,
638
0
                              uint64_t* bytes_read) {
639
0
  assert(blob_reqs.size() > 0);
640
641
0
  uint64_t total_bytes_read = 0;
642
0
  uint64_t bytes_read_in_file = 0;
643
644
0
  for (auto& [file_number, file_size, blob_reqs_in_file] : blob_reqs) {
645
    // sort blob_reqs_in_file by file offset.
646
0
    std::sort(
647
0
        blob_reqs_in_file.begin(), blob_reqs_in_file.end(),
648
0
        [](const BlobReadRequest& lhs, const BlobReadRequest& rhs) -> bool {
649
0
          return lhs.offset < rhs.offset;
650
0
        });
651
652
0
    MultiGetBlobFromOneFile(read_options, file_number, file_size,
653
0
                            blob_reqs_in_file, &bytes_read_in_file);
654
655
0
    total_bytes_read += bytes_read_in_file;
656
0
  }
657
658
0
  if (bytes_read) {
659
0
    *bytes_read = total_bytes_read;
660
0
  }
661
0
}
662
663
void BlobSource::MultiGetBlobFromOneFile(const ReadOptions& read_options,
664
                                         uint64_t file_number,
665
                                         uint64_t /*file_size*/,
666
                                         autovector<BlobReadRequest>& blob_reqs,
667
0
                                         uint64_t* bytes_read) {
668
0
  const size_t num_blobs = blob_reqs.size();
669
0
  assert(num_blobs > 0);
670
0
  assert(num_blobs <= MultiGetContext::MAX_BATCH_SIZE);
671
672
#ifndef NDEBUG
673
  for (size_t i = 0; i < num_blobs - 1; ++i) {
674
    assert(blob_reqs[i].offset <= blob_reqs[i + 1].offset);
675
  }
676
#endif  // !NDEBUG
677
678
0
  using Mask = uint64_t;
679
0
  Mask cache_hit_mask = 0;
680
681
0
  uint64_t total_bytes = 0;
682
0
  const OffsetableCacheKey base_cache_key(db_id_, db_session_id_, file_number);
683
684
0
  if (blob_cache_) {
685
0
    size_t cached_blob_count = 0;
686
0
    for (size_t i = 0; i < num_blobs; ++i) {
687
0
      auto& req = blob_reqs[i];
688
689
0
      CacheHandleGuard<BlobContents> blob_handle;
690
0
      const CacheKey cache_key = base_cache_key.WithOffset(req.offset);
691
0
      const Slice key = cache_key.AsSlice();
692
693
0
      const Status s = GetBlobFromCache(key, &blob_handle);
694
695
0
      if (s.ok()) {
696
0
        assert(req.status);
697
0
        *req.status = s;
698
699
0
        PinCachedBlob(&blob_handle, req.result);
700
701
        // Update the counter for the number of valid blobs read from the cache.
702
0
        ++cached_blob_count;
703
704
        // For consistency, the size of each on-disk (possibly compressed) blob
705
        // record is accumulated to total_bytes.
706
0
        uint64_t adjustment =
707
0
            read_options.verify_checksums
708
0
                ? BlobLogRecord::CalculateAdjustmentForRecordHeader(
709
0
                      req.user_key->size())
710
0
                : 0;
711
0
        assert(req.offset >= adjustment);
712
0
        total_bytes += req.len + adjustment;
713
0
        cache_hit_mask |= (Mask{1} << i);  // cache hit
714
0
      }
715
0
    }
716
717
    // All blobs were read from the cache.
718
0
    if (cached_blob_count == num_blobs) {
719
0
      if (bytes_read) {
720
0
        *bytes_read = total_bytes;
721
0
      }
722
0
      return;
723
0
    }
724
0
  }
725
726
0
  const bool no_io = read_options.read_tier == kBlockCacheTier;
727
0
  if (no_io) {
728
0
    for (size_t i = 0; i < num_blobs; ++i) {
729
0
      if (!(cache_hit_mask & (Mask{1} << i))) {
730
0
        BlobReadRequest& req = blob_reqs[i];
731
0
        assert(req.status);
732
733
0
        *req.status =
734
0
            Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
735
0
      }
736
0
    }
737
0
    return;
738
0
  }
739
740
0
  {
741
    // Find the rest of blobs from the file since I/O is allowed.
742
0
    autovector<std::pair<BlobReadRequest*, std::unique_ptr<BlobContents>>>
743
0
        _blob_reqs;
744
0
    uint64_t _bytes_read = 0;
745
746
0
    for (size_t i = 0; i < num_blobs; ++i) {
747
0
      if (!(cache_hit_mask & (Mask{1} << i))) {
748
0
        _blob_reqs.emplace_back(&blob_reqs[i], std::unique_ptr<BlobContents>());
749
0
      }
750
0
    }
751
752
0
    CacheHandleGuard<BlobFileReader> blob_file_reader;
753
0
    Status s = blob_file_cache_->GetBlobFileReader(read_options, file_number,
754
0
                                                   &blob_file_reader);
755
0
    if (!s.ok()) {
756
0
      for (size_t i = 0; i < _blob_reqs.size(); ++i) {
757
0
        BlobReadRequest* const req = _blob_reqs[i].first;
758
0
        assert(req);
759
0
        assert(req->status);
760
761
0
        *req->status = s;
762
0
      }
763
0
      return;
764
0
    }
765
766
0
    assert(blob_file_reader.GetValue());
767
768
0
    MemoryAllocator* const allocator =
769
0
        (blob_cache_ && read_options.fill_cache)
770
0
            ? blob_cache_.get()->memory_allocator()
771
0
            : nullptr;
772
773
0
    blob_file_reader.GetValue()->MultiGetBlob(read_options, allocator,
774
0
                                              _blob_reqs, &_bytes_read);
775
776
0
    bool needs_reader_refresh = false;
777
0
    for (const auto& blob_req : _blob_reqs) {
778
0
      BlobReadRequest* const req = blob_req.first;
779
0
      assert(req != nullptr);
780
0
      assert(req->status != nullptr);
781
0
      if (req->status->IsCorruption()) {
782
0
        needs_reader_refresh = true;
783
0
        break;
784
0
      }
785
0
    }
786
787
0
    if (needs_reader_refresh) {
788
0
      blob_file_reader.Reset();
789
0
      blob_file_cache_->Evict(file_number);
790
791
0
      std::unique_ptr<BlobFileReader> fresh_reader;
792
0
      s = blob_file_cache_->OpenBlobFileReaderUncached(
793
0
          read_options, file_number, &fresh_reader,
794
0
          /*allow_footer_skip_retry=*/false);
795
0
      if (!s.ok()) {
796
0
        for (const auto& blob_req : _blob_reqs) {
797
0
          BlobReadRequest* const req = blob_req.first;
798
0
          assert(req != nullptr);
799
0
          assert(req->status != nullptr);
800
0
          if (req->status->IsCorruption()) {
801
0
            *req->status = AppendBlobRefreshRetryFailure(*req->status, s);
802
0
          }
803
0
        }
804
0
        return;
805
0
      }
806
807
0
      autovector<std::pair<BlobReadRequest*, std::unique_ptr<BlobContents>>>
808
0
          retry_blob_reqs;
809
0
      autovector<Status> stale_statuses;
810
0
      for (auto& blob_req : _blob_reqs) {
811
0
        BlobReadRequest* const req = blob_req.first;
812
0
        assert(req != nullptr);
813
0
        assert(req->status != nullptr);
814
0
        if (!req->status->IsCorruption()) {
815
0
          continue;
816
0
        }
817
818
0
        stale_statuses.emplace_back(*req->status);
819
0
        *req->status = Status::OK();
820
0
        blob_req.second.reset();
821
0
        retry_blob_reqs.emplace_back(req, std::unique_ptr<BlobContents>());
822
0
      }
823
824
0
      uint64_t refreshed_bytes_read = 0;
825
0
      fresh_reader->MultiGetBlob(read_options, allocator, retry_blob_reqs,
826
0
                                 &refreshed_bytes_read);
827
0
      _bytes_read += refreshed_bytes_read;
828
829
0
      bool install_fresh_reader = false;
830
0
      for (size_t i = 0; i < retry_blob_reqs.size(); ++i) {
831
0
        auto& retried_blob_req = retry_blob_reqs[i];
832
0
        BlobReadRequest* const retried_req = retried_blob_req.first;
833
0
        assert(retried_req != nullptr);
834
0
        if (retried_req->status->ok()) {
835
0
          install_fresh_reader = true;
836
0
        } else {
837
0
          *retried_req->status = AppendBlobRefreshRetryFailure(
838
0
              stale_statuses[i], *retried_req->status);
839
0
        }
840
841
0
        for (auto& blob_req : _blob_reqs) {
842
0
          if (blob_req.first != retried_req) {
843
0
            continue;
844
0
          }
845
846
0
          blob_req.second = std::move(retried_blob_req.second);
847
0
          break;
848
0
        }
849
0
      }
850
851
0
      if (install_fresh_reader) {
852
0
        CacheHandleGuard<BlobFileReader> ignored_reader;
853
0
        blob_file_cache_
854
0
            ->RefreshBlobFileReader(file_number, &fresh_reader, &ignored_reader)
855
0
            .PermitUncheckedError();
856
0
      }
857
0
    }
858
859
0
    if (blob_cache_ && read_options.fill_cache) {
860
      // If filling cache is allowed and a cache is configured, try to put
861
      // the blob(s) to the cache.
862
0
      for (auto& [req, blob_contents] : _blob_reqs) {
863
0
        assert(req);
864
865
0
        if (req->status->ok()) {
866
0
          CacheHandleGuard<BlobContents> blob_handle;
867
0
          const CacheKey cache_key = base_cache_key.WithOffset(req->offset);
868
0
          const Slice key = cache_key.AsSlice();
869
0
          s = PutBlobIntoCache(key, &blob_contents, &blob_handle);
870
0
          if (!s.ok()) {
871
0
            *req->status = s;
872
0
          } else {
873
0
            PinCachedBlob(&blob_handle, req->result);
874
0
          }
875
0
        }
876
0
      }
877
0
    } else {
878
0
      for (auto& [req, blob_contents] : _blob_reqs) {
879
0
        assert(req);
880
881
0
        if (req->status->ok()) {
882
0
          PinOwnedBlob(&blob_contents, req->result);
883
0
        }
884
0
      }
885
0
    }
886
887
0
    total_bytes += _bytes_read;
888
0
    if (bytes_read) {
889
0
      *bytes_read = total_bytes;
890
0
    }
891
0
  }
892
0
}
893
894
bool BlobSource::TEST_BlobInCache(uint64_t file_number, uint64_t file_size,
895
0
                                  uint64_t offset, size_t* charge) const {
896
0
  const CacheKey cache_key = GetCacheKey(file_number, file_size, offset);
897
0
  const Slice key = cache_key.AsSlice();
898
899
0
  CacheHandleGuard<BlobContents> blob_handle;
900
0
  const Status s = GetBlobFromCache(key, &blob_handle);
901
902
0
  if (s.ok() && blob_handle.GetValue() != nullptr) {
903
0
    if (charge) {
904
0
      const Cache* const cache = blob_handle.GetCache();
905
0
      assert(cache);
906
907
0
      Cache::Handle* const handle = blob_handle.GetCacheHandle();
908
0
      assert(handle);
909
910
0
      *charge = cache->GetUsage(handle);
911
0
    }
912
913
0
    return true;
914
0
  }
915
916
0
  return false;
917
0
}
918
919
}  // namespace ROCKSDB_NAMESPACE