Coverage Report

Created: 2026-11-12 08:06

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
#include <vector>
11
12
#include "cache/cache_reservation_manager.h"
13
#include "cache/charged_cache.h"
14
#include "db/blob/blob_contents.h"
15
#include "db/blob/blob_file_reader.h"
16
#include "db/blob/blob_gen2_format.h"
17
#include "db/blob/blob_log_format.h"
18
#include "file/random_access_file_reader.h"
19
#include "memory/memory_allocator_impl.h"
20
#include "monitoring/statistics_impl.h"
21
#include "options/cf_options.h"
22
#include "table/get_context.h"
23
#include "table/multiget_context.h"
24
25
namespace ROCKSDB_NAMESPACE {
26
27
namespace {
28
29
Status AppendBlobRefreshRetryFailure(const Status& stale_status,
30
0
                                     const Status& retry_status) {
31
0
  assert(stale_status.IsCorruption());
32
0
  assert(!retry_status.ok());
33
0
  if (retry_status.IsCorruption()) {
34
0
    return retry_status;
35
0
  }
36
0
  return Status::CopyAppendMessage(
37
0
      stale_status, "; refresh retry failed: ", retry_status.ToString());
38
0
}
39
40
// Runs read(reader) against the blob-file reader for `file_number`. On a
41
// Corruption -- which a stale cached reader (e.g. the physical file was
42
// replaced) can surface -- evicts the cached reader, reopens it uncached,
43
// retries read() once, and refreshes the cache on success. Shared by the
44
// single-blob read paths (GetBlob / GetBlobRange) so the stale-file recovery
45
// lives in one place.
46
//
47
// `read` is invoked with a BlobFileReader* and returns a Status; because it may
48
// run twice, it must re-initialize any output it populates on each call (and is
49
// taken by const reference rather than forwarded, since it is not moved-from).
50
template <typename ReadFn>
51
Status ReadBlobWithReaderRetry(BlobFileCache* blob_file_cache,
52
                               const ReadOptions& read_options,
53
                               const BlobFileOpenInfo& blob_file,
54
0
                               const ReadFn& read) {
55
0
  CacheHandleGuard<BlobFileReader> blob_file_reader;
56
0
  Status s = blob_file_cache->GetBlobFileReader(read_options, blob_file,
57
0
                                                &blob_file_reader);
58
0
  if (!s.ok()) {
59
0
    return s;
60
0
  }
61
0
  assert(blob_file_reader.GetValue());
62
63
0
  s = read(blob_file_reader.GetValue());
64
0
  if (!s.IsCorruption()) {
65
0
    return s;
66
0
  }
67
68
0
  const Status stale_status = s;
69
0
  blob_file_reader.Reset();
70
0
  blob_file_cache->Evict(blob_file.file_number);
71
72
0
  std::unique_ptr<BlobFileReader> fresh_reader;
73
0
  s = blob_file_cache->OpenBlobFileReaderUncached(
74
0
      read_options, blob_file, &fresh_reader,
75
0
      /*allow_footer_skip_retry=*/false);
76
0
  if (!s.ok()) {
77
0
    return AppendBlobRefreshRetryFailure(stale_status, s);
78
0
  }
79
80
0
  s = read(fresh_reader.get());
81
0
  if (!s.ok()) {
82
0
    return AppendBlobRefreshRetryFailure(stale_status, s);
83
0
  }
84
85
0
  CacheHandleGuard<BlobFileReader> ignored_reader;
86
0
  blob_file_cache
87
0
      ->RefreshBlobFileReader(blob_file, &fresh_reader, &ignored_reader)
88
0
      .PermitUncheckedError();
89
0
  return s;
90
0
}
Unexecuted instantiation: blob_source.cc:rocksdb::Status rocksdb::(anonymous namespace)::ReadBlobWithReaderRetry<rocksdb::BlobSource::GetBlob(rocksdb::ReadOptions const&, rocksdb::Slice const&, rocksdb::BlobFileOpenInfo const&, unsigned long, unsigned long, unsigned long, rocksdb::CompressionType, rocksdb::FilePrefetchBuffer*, rocksdb::PinnableSlice*, unsigned long*)::$_0>(rocksdb::BlobFileCache*, rocksdb::ReadOptions const&, rocksdb::BlobFileOpenInfo const&, rocksdb::BlobSource::GetBlob(rocksdb::ReadOptions const&, rocksdb::Slice const&, rocksdb::BlobFileOpenInfo const&, 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&, rocksdb::BlobFileOpenInfo const&, unsigned long, unsigned long, unsigned long, rocksdb::CompressionType, unsigned long, unsigned long, rocksdb::PinnableSlice*, unsigned long*)::$_0>(rocksdb::BlobFileCache*, rocksdb::ReadOptions const&, rocksdb::BlobFileOpenInfo const&, rocksdb::BlobSource::GetBlobRange(rocksdb::ReadOptions const&, rocksdb::Slice const&, rocksdb::BlobFileOpenInfo const&, unsigned long, unsigned long, unsigned long, rocksdb::CompressionType, unsigned long, unsigned long, rocksdb::PinnableSlice*, unsigned long*)::$_0 const&)
91
92
// Records lazy wide-column read metrics for one actual storage read issued
93
// while resolving a lazy result (attributed via Env::IOActivity::kLazyResolve):
94
// `bytes_read` bytes were read from storage. Covers both whole-column and
95
// partial reads; not called for cache hits (no storage read).
96
0
void RecordLazyRead(Statistics* statistics, uint64_t bytes_read) {
97
0
  RecordTick(statistics, BLOB_DB_LAZY_READ_COUNT);
98
0
  RecordTick(statistics, BLOB_DB_LAZY_READ_BYTES, bytes_read);
99
0
}
100
101
// Additionally records the partial-read metrics for one actual partial
102
// (byte-range) read: `bytes_read` bytes were fetched instead of the column's
103
// full `value_size`. Call alongside RecordLazyRead (a partial read is also a
104
// lazy read).
105
void RecordLazyPartialRead(Statistics* statistics, uint64_t bytes_read,
106
0
                           uint64_t value_size) {
107
0
  RecordTick(statistics, BLOB_DB_LAZY_PARTIAL_READ_COUNT);
108
0
  if (value_size > bytes_read) {
109
0
    RecordTick(statistics, BLOB_DB_LAZY_PARTIAL_BYTES_SAVED,
110
0
               value_size - bytes_read);
111
0
  }
112
0
}
113
114
// Pins the sub-range [range_offset, range_offset + range_length) of a cache-hit
115
// whole blob value into *value (clamped: an offset at/past the end yields
116
// empty, length is clamped to the remainder), keeping the cached bytes alive by
117
// transferring the cache handle into *value. Shared by the range-read cache-hit
118
// paths (GetBlobRange / GetSimpleGen2BlobRange).
119
void PinCacheHitSubRange(CacheHandleGuard<BlobContents>* blob_handle,
120
                         uint64_t range_offset, size_t range_length,
121
0
                         PinnableSlice* value) {
122
0
  const Slice full = blob_handle->GetValue()->data();
123
0
  Slice sub;
124
0
  if (range_offset < full.size()) {
125
0
    const size_t off = static_cast<size_t>(range_offset);
126
0
    const size_t avail = full.size() - off;
127
0
    const size_t len = range_length > avail ? avail : range_length;
128
0
    sub = Slice(full.data() + off, len);
129
0
  }  // else: offset at/past end -> empty (not an error)
130
131
0
  value->Reset();
132
0
  constexpr Cleanable* cleanable = nullptr;
133
0
  value->PinSlice(sub, cleanable);
134
0
  blob_handle->TransferTo(value);
135
0
}
136
137
}  // namespace
138
139
BlobSource::BlobSource(const ImmutableOptions& immutable_options,
140
                       const MutableCFOptions& mutable_cf_options,
141
                       const std::string& db_id,
142
                       const std::string& db_session_id,
143
                       BlobFileCache* blob_file_cache)
144
87.5k
    : db_id_(db_id),
145
87.5k
      db_session_id_(db_session_id),
146
87.5k
      statistics_(immutable_options.statistics.get()),
147
87.5k
      blob_file_cache_(blob_file_cache),
148
87.5k
      blob_cache_(immutable_options.blob_cache),
149
87.5k
      lowest_used_cache_tier_(immutable_options.lowest_used_cache_tier) {
150
87.5k
  auto bbto =
151
87.5k
      mutable_cf_options.table_factory->GetOptions<BlockBasedTableOptions>();
152
87.5k
  if (bbto &&
153
87.5k
      bbto->cache_usage_options.options_overrides.at(CacheEntryRole::kBlobCache)
154
87.5k
              .charged == CacheEntryRoleOptions::Decision::kEnabled) {
155
0
    blob_cache_ = SharedCacheInterface{std::make_shared<ChargedCache>(
156
0
        immutable_options.blob_cache, bbto->block_cache)};
157
0
  }
158
87.5k
}
159
160
87.5k
BlobSource::~BlobSource() = default;
161
162
Status BlobSource::GetBlobFromCache(
163
0
    const Slice& cache_key, CacheHandleGuard<BlobContents>* cached_blob) const {
164
0
  assert(blob_cache_);
165
0
  assert(!cache_key.empty());
166
0
  assert(cached_blob);
167
0
  assert(cached_blob->IsEmpty());
168
169
0
  Cache::Handle* cache_handle = nullptr;
170
0
  cache_handle = GetEntryFromCache(cache_key);
171
0
  if (cache_handle != nullptr) {
172
0
    *cached_blob =
173
0
        CacheHandleGuard<BlobContents>(blob_cache_.get(), cache_handle);
174
175
0
    assert(cached_blob->GetValue());
176
177
0
    PERF_COUNTER_ADD(blob_cache_hit_count, 1);
178
0
    PERF_COUNTER_ADD(blob_cache_read_byte, cached_blob->GetValue()->size());
179
0
    RecordTick(statistics_, BLOB_DB_CACHE_HIT);
180
0
    RecordTick(statistics_, BLOB_DB_CACHE_BYTES_READ,
181
0
               cached_blob->GetValue()->size());
182
183
0
    return Status::OK();
184
0
  }
185
186
0
  RecordTick(statistics_, BLOB_DB_CACHE_MISS);
187
188
0
  return Status::NotFound("Blob not found in cache");
189
0
}
190
191
Status BlobSource::PutBlobIntoCache(
192
    const Slice& cache_key, std::unique_ptr<BlobContents>* blob,
193
0
    CacheHandleGuard<BlobContents>* cached_blob) const {
194
0
  assert(blob_cache_);
195
0
  assert(!cache_key.empty());
196
0
  assert(blob);
197
0
  assert(*blob);
198
0
  assert(cached_blob);
199
0
  assert(cached_blob->IsEmpty());
200
201
0
  TypedHandle* cache_handle = nullptr;
202
0
  const Status s = InsertEntryIntoCache(cache_key, blob->get(), &cache_handle,
203
0
                                        Cache::Priority::BOTTOM);
204
0
  if (s.ok()) {
205
0
    [[maybe_unused]] BlobContents* released_blob = blob->release();
206
207
0
    assert(cache_handle != nullptr);
208
0
    *cached_blob =
209
0
        CacheHandleGuard<BlobContents>(blob_cache_.get(), cache_handle);
210
211
0
    assert(cached_blob->GetValue());
212
213
0
    RecordTick(statistics_, BLOB_DB_CACHE_ADD);
214
0
    RecordTick(statistics_, BLOB_DB_CACHE_BYTES_WRITE,
215
0
               cached_blob->GetValue()->size());
216
217
0
  } else {
218
0
    RecordTick(statistics_, BLOB_DB_CACHE_ADD_FAILURES);
219
0
  }
220
221
0
  return s;
222
0
}
223
224
0
BlobSource::TypedHandle* BlobSource::GetEntryFromCache(const Slice& key) const {
225
0
  return blob_cache_.LookupFull(key, nullptr /* context */,
226
0
                                Cache::Priority::BOTTOM, statistics_,
227
0
                                lowest_used_cache_tier_);
228
0
}
229
230
void BlobSource::PinCachedBlob(CacheHandleGuard<BlobContents>* cached_blob,
231
0
                               PinnableSlice* value) {
232
0
  assert(cached_blob);
233
0
  assert(cached_blob->GetValue());
234
0
  assert(value);
235
236
  // To avoid copying the cached blob into the buffer provided by the
237
  // application, we can simply transfer ownership of the cache handle to
238
  // the target PinnableSlice. This has the potential to save a lot of
239
  // CPU, especially with large blob values.
240
241
0
  value->Reset();
242
243
0
  constexpr Cleanable* cleanable = nullptr;
244
0
  value->PinSlice(cached_blob->GetValue()->data(), cleanable);
245
246
0
  cached_blob->TransferTo(value);
247
0
}
248
249
void BlobSource::PinOwnedBlob(std::unique_ptr<BlobContents>* owned_blob,
250
0
                              PinnableSlice* value) {
251
0
  assert(owned_blob);
252
0
  assert(*owned_blob);
253
0
  assert(value);
254
255
0
  BlobContents* const blob = owned_blob->release();
256
0
  assert(blob);
257
258
0
  value->Reset();
259
0
  value->PinSlice(
260
0
      blob->data(),
261
0
      [](void* arg1, void* /* arg2 */) {
262
0
        delete static_cast<BlobContents*>(arg1);
263
0
      },
264
0
      blob, nullptr);
265
0
}
266
267
Status BlobSource::InsertEntryIntoCache(const Slice& key, BlobContents* value,
268
                                        TypedHandle** cache_handle,
269
0
                                        Cache::Priority priority) const {
270
0
  return blob_cache_.InsertFull(key, value, value->ApproximateMemoryUsage(),
271
0
                                cache_handle, priority,
272
0
                                lowest_used_cache_tier_);
273
0
}
274
275
Status BlobSource::GetBlob(const ReadOptions& read_options,
276
                           const Slice& user_key,
277
                           const BlobFileOpenInfo& blob_file, uint64_t offset,
278
                           uint64_t file_size, uint64_t value_size,
279
                           CompressionType compression_type,
280
                           FilePrefetchBuffer* prefetch_buffer,
281
0
                           PinnableSlice* value, uint64_t* bytes_read) {
282
0
  assert(value);
283
284
0
  Status s;
285
286
0
  const CacheKey cache_key =
287
0
      GetCacheKey(blob_file.file_number, file_size, offset);
288
289
0
  CacheHandleGuard<BlobContents> blob_handle;
290
291
  // First, try to get the blob from the cache
292
  //
293
  // If blob cache is enabled, we'll try to read from it.
294
0
  if (blob_cache_) {
295
0
    Slice key = cache_key.AsSlice();
296
0
    s = GetBlobFromCache(key, &blob_handle);
297
0
    if (s.ok()) {
298
0
      PinCachedBlob(&blob_handle, value);
299
300
      // For consistency, the size of on-disk (possibly compressed) blob record
301
      // is assigned to bytes_read.
302
0
      uint64_t adjustment =
303
0
          read_options.verify_checksums
304
0
              ? BlobLogRecord::CalculateAdjustmentForRecordHeader(
305
0
                    user_key.size())
306
0
              : 0;
307
0
      assert(offset >= adjustment);
308
309
0
      uint64_t record_size = value_size + adjustment;
310
0
      if (bytes_read) {
311
0
        *bytes_read = record_size;
312
0
      }
313
0
      return s;
314
0
    }
315
0
  }
316
317
0
  assert(blob_handle.IsEmpty());
318
319
0
  const bool no_io = read_options.read_tier == kBlockCacheTier;
320
0
  if (no_io) {
321
0
    s = Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
322
0
    return s;
323
0
  }
324
325
  // Can't find the blob from the cache. Since I/O is allowed, read from the
326
  // file.
327
0
  std::unique_ptr<BlobContents> blob_contents;
328
329
0
  {
330
0
    MemoryAllocator* const allocator =
331
0
        (blob_cache_ && read_options.fill_cache)
332
0
            ? blob_cache_.get()->memory_allocator()
333
0
            : nullptr;
334
335
0
    uint64_t read_size = 0;
336
0
    s = ReadBlobWithReaderRetry(
337
0
        blob_file_cache_, read_options, blob_file, [&](BlobFileReader* reader) {
338
0
          if (compression_type != reader->GetCompressionType()) {
339
0
            return Status::Corruption(
340
0
                "Compression type mismatch when reading blob");
341
0
          }
342
0
          blob_contents.reset();
343
0
          read_size = 0;
344
0
          return reader->GetBlob(read_options, user_key, offset, value_size,
345
0
                                 compression_type, prefetch_buffer, allocator,
346
0
                                 &blob_contents, &read_size);
347
0
        });
348
0
    if (!s.ok()) {
349
0
      return s;
350
0
    }
351
0
    if (bytes_read) {
352
0
      *bytes_read = read_size;
353
0
    }
354
    // Whole-column read on the lazy resolve path (partial reads go through
355
    // GetBlobRange). Counts the storage read; partial reads are not counted
356
    // here.
357
0
    if (read_options.io_activity == Env::IOActivity::kLazyResolve) {
358
0
      RecordLazyRead(statistics_, read_size);
359
0
    }
360
0
  }
361
362
0
  if (blob_cache_ && read_options.fill_cache) {
363
    // If filling cache is allowed and a cache is configured, try to put the
364
    // blob to the cache.
365
0
    Slice key = cache_key.AsSlice();
366
0
    s = PutBlobIntoCache(key, &blob_contents, &blob_handle);
367
0
    if (!s.ok()) {
368
0
      return s;
369
0
    }
370
371
0
    PinCachedBlob(&blob_handle, value);
372
0
  } else {
373
0
    PinOwnedBlob(&blob_contents, value);
374
0
  }
375
376
0
  assert(s.ok());
377
0
  return s;
378
0
}
379
380
Status BlobSource::GetBlobRange(const ReadOptions& read_options,
381
                                const Slice& user_key,
382
                                const BlobFileOpenInfo& blob_file,
383
                                uint64_t offset, uint64_t file_size,
384
                                uint64_t value_size,
385
                                CompressionType compression_type,
386
                                uint64_t range_offset, size_t range_length,
387
0
                                PinnableSlice* value, uint64_t* bytes_read) {
388
0
  assert(value);
389
  // Partial reads are for uncompressed blobs only; the caller decides this (a
390
  // compressed column takes the whole-value GetBlob path and slices).
391
0
  assert(compression_type == kNoCompression);
392
  // Range reads are (currently) only issued while resolving a lazy result, so
393
  // the lazy read stats below are recorded unconditionally (unlike GetBlob,
394
  // which gates on the activity). Enforce that invariant here.
395
0
  assert(read_options.io_activity == Env::IOActivity::kLazyResolve);
396
397
0
  Status s;
398
399
0
  const CacheKey cache_key =
400
0
      GetCacheKey(blob_file.file_number, file_size, offset);
401
402
0
  CacheHandleGuard<BlobContents> blob_handle;
403
404
  // First, probe the blob cache for the whole value. On a hit, slice the
405
  // requested sub-range out of the cached value while pinning the cache handle
406
  // (zero-copy, no disk read). A partial read never inserts into the cache.
407
0
  if (blob_cache_) {
408
0
    Slice key = cache_key.AsSlice();
409
0
    s = GetBlobFromCache(key, &blob_handle);
410
0
    if (s.ok()) {
411
0
      PinCacheHitSubRange(&blob_handle, range_offset, range_length, value);
412
0
      if (bytes_read) {
413
0
        *bytes_read = 0;  // served from cache; no disk read
414
0
      }
415
0
      return s;
416
0
    }
417
0
  }
418
419
0
  assert(blob_handle.IsEmpty());
420
421
0
  const bool no_io = read_options.read_tier == kBlockCacheTier;
422
0
  if (no_io) {
423
0
    s = Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
424
0
    return s;
425
0
  }
426
427
  // Cache miss: read only the requested sub-range from the file. The result is
428
  // never inserted into the blob cache (see the header comment).
429
0
  std::unique_ptr<BlobContents> blob_contents;
430
0
  {
431
    // No cache-fill allocator: a partial value is never inserted into the
432
    // cache.
433
0
    constexpr MemoryAllocator* allocator = nullptr;
434
435
0
    uint64_t read_size = 0;
436
    // Reuses the stale-reader retry shared with GetBlob. A range read skips
437
    // whole-record checksum verification, so the retry handles the stale-file
438
    // case (offset/size mismatch surfacing as Corruption), not a payload
439
    // checksum failure.
440
0
    s = ReadBlobWithReaderRetry(
441
0
        blob_file_cache_, read_options, blob_file, [&](BlobFileReader* reader) {
442
0
          if (compression_type != reader->GetCompressionType()) {
443
0
            return Status::Corruption(
444
0
                "Compression type mismatch when reading blob");
445
0
          }
446
0
          blob_contents.reset();
447
0
          read_size = 0;
448
0
          return reader->GetBlobRange(read_options, user_key, offset,
449
0
                                      value_size, range_offset, range_length,
450
0
                                      allocator, &blob_contents, &read_size);
451
0
        });
452
0
    if (!s.ok()) {
453
0
      return s;
454
0
    }
455
0
    if (bytes_read) {
456
0
      *bytes_read = read_size;
457
0
    }
458
0
    RecordLazyRead(statistics_, read_size);
459
0
    RecordLazyPartialRead(statistics_, read_size, value_size);
460
0
  }
461
462
0
  PinOwnedBlob(&blob_contents, value);
463
464
0
  assert(s.ok());
465
0
  return s;
466
0
}
467
468
Status BlobSource::GetSimpleGen2Blob(
469
    const ReadOptions& read_options, const OffsetableCacheKey& base_cache_key,
470
    RandomAccessFileReader* file, uint64_t record_offset, uint64_t payload_size,
471
    ChecksumType checksum_type, uint32_t base_context_checksum,
472
    CompressionType expected_compression, PinnableSlice* value,
473
0
    uint64_t* bytes_read) {
474
0
  assert(value);
475
0
  assert(file);
476
477
0
  const uint64_t record_size = payload_size + kSimpleGen2BlobTrailerSize;
478
479
  // The cache key is derived from the SimpleGen2Blob format (shared scheme with
480
  // block-based SST blocks); see GetSimpleGen2BlobCacheKey.
481
0
  const CacheKey cache_key =
482
0
      GetSimpleGen2BlobCacheKey(base_cache_key, record_offset);
483
484
0
  Status s;
485
486
0
  CacheHandleGuard<BlobContents> blob_handle;
487
488
  // First, try to get the blob from the cache.
489
0
  if (blob_cache_) {
490
0
    Slice key = cache_key.AsSlice();
491
0
    s = GetBlobFromCache(key, &blob_handle);
492
0
    if (s.ok()) {
493
0
      PinCachedBlob(&blob_handle, value);
494
495
      // For consistency, the on-disk record size is assigned to bytes_read on
496
      // both cache hits and misses.
497
0
      if (bytes_read) {
498
0
        *bytes_read = record_size;
499
0
      }
500
0
      return s;
501
0
    }
502
0
  }
503
504
0
  assert(blob_handle.IsEmpty());
505
506
0
  const bool no_io = read_options.read_tier == kBlockCacheTier;
507
0
  if (no_io) {
508
0
    s = Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
509
0
    return s;
510
0
  }
511
512
  // Cache miss (or no cache configured). Read the record into a buffer
513
  // allocated from the blob cache's memory allocator when we intend to insert
514
  // it, exposing the uncompressed payload as BlobContents (the trailer just
515
  // sits unused at the tail of the buffer).
516
0
  MemoryAllocator* const allocator = (blob_cache_ && read_options.fill_cache)
517
0
                                         ? blob_cache_.get()->memory_allocator()
518
0
                                         : nullptr;
519
520
0
  CacheAllocationPtr buf =
521
0
      AllocateBlock(static_cast<size_t>(record_size), allocator);
522
0
  s = ReadAndVerifySimpleGen2BlobRecord(
523
0
      read_options, file, record_offset, static_cast<size_t>(payload_size),
524
0
      static_cast<size_t>(record_size), checksum_type, base_context_checksum,
525
0
      expected_compression, buf.get());
526
0
  if (!s.ok()) {
527
0
    return s;
528
0
  }
529
530
0
  std::unique_ptr<BlobContents> blob_contents(
531
0
      new BlobContents(std::move(buf), static_cast<size_t>(payload_size)));
532
533
  // Record the per-read statistics (mirrors BlobFileReader::GetBlob).
534
0
  RecordTick(statistics_, BLOB_DB_BLOB_FILE_BYTES_READ, record_size);
535
0
  PERF_COUNTER_ADD(blob_read_count, 1);
536
0
  PERF_COUNTER_ADD(blob_read_byte, record_size);
537
0
  if (bytes_read) {
538
0
    *bytes_read = record_size;
539
0
  }
540
  // Whole-column read on the lazy resolve path (partial reads go through
541
  // GetSimpleGen2BlobRange).
542
0
  if (read_options.io_activity == Env::IOActivity::kLazyResolve) {
543
0
    RecordLazyRead(statistics_, record_size);
544
0
  }
545
546
0
  if (blob_cache_ && read_options.fill_cache) {
547
    // If filling cache is allowed and a cache is configured, try to put the
548
    // blob into the cache.
549
0
    Slice key = cache_key.AsSlice();
550
0
    s = PutBlobIntoCache(key, &blob_contents, &blob_handle);
551
0
    if (!s.ok()) {
552
0
      return s;
553
0
    }
554
555
0
    PinCachedBlob(&blob_handle, value);
556
0
  } else {
557
0
    PinOwnedBlob(&blob_contents, value);
558
0
  }
559
560
0
  assert(s.ok());
561
0
  return s;
562
0
}
563
564
Status BlobSource::GetSimpleGen2BlobRange(
565
    const ReadOptions& read_options, const OffsetableCacheKey& base_cache_key,
566
    RandomAccessFileReader* file, uint64_t record_offset, uint64_t payload_size,
567
    ChecksumType /*checksum_type*/, uint32_t /*base_context_checksum*/,
568
    CompressionType expected_compression, uint64_t range_offset,
569
0
    size_t range_length, PinnableSlice* value, uint64_t* bytes_read) {
570
0
  assert(value);
571
0
  assert(file);
572
  // Partial reads are for uncompressed payloads only; the caller decides this
573
  // (a compressed column takes the whole-payload GetSimpleGen2Blob path +
574
  // slice).
575
0
  assert(expected_compression == kNoCompression);
576
  // Range reads are (currently) only issued while resolving a lazy result, so
577
  // the lazy read stats below are recorded unconditionally (unlike
578
  // GetSimpleGen2Blob, which gates on the activity). Enforce that invariant
579
  // here.
580
0
  assert(read_options.io_activity == Env::IOActivity::kLazyResolve);
581
582
0
  const CacheKey cache_key =
583
0
      GetSimpleGen2BlobCacheKey(base_cache_key, record_offset);
584
585
0
  Status s;
586
587
0
  CacheHandleGuard<BlobContents> blob_handle;
588
589
  // First, probe the blob cache for the whole payload. On a hit, slice the
590
  // requested sub-range out of the cached payload while pinning the cache
591
  // handle (zero-copy, no disk read). A partial read never inserts into the
592
  // cache.
593
0
  if (blob_cache_) {
594
0
    Slice key = cache_key.AsSlice();
595
0
    s = GetBlobFromCache(key, &blob_handle);
596
0
    if (s.ok()) {
597
0
      PinCacheHitSubRange(&blob_handle, range_offset, range_length, value);
598
0
      if (bytes_read) {
599
0
        *bytes_read = 0;  // served from cache; no disk read
600
0
      }
601
0
      return s;
602
0
    }
603
0
  }
604
605
0
  assert(blob_handle.IsEmpty());
606
607
0
  const bool no_io = read_options.read_tier == kBlockCacheTier;
608
0
  if (no_io) {
609
0
    s = Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
610
0
    return s;
611
0
  }
612
613
  // Cache miss: read only the requested sub-range from the file. The result is
614
  // never inserted into the blob cache (a partial payload cannot represent the
615
  // whole-record cache entry).
616
0
  CacheAllocationPtr buf = AllocateBlock(range_length, /*allocator=*/nullptr);
617
0
  s = ReadSimpleGen2BlobRange(read_options, file, record_offset,
618
0
                              static_cast<size_t>(payload_size), range_offset,
619
0
                              range_length, expected_compression, buf.get());
620
0
  if (!s.ok()) {
621
0
    return s;
622
0
  }
623
624
0
  std::unique_ptr<BlobContents> blob_contents(
625
0
      new BlobContents(std::move(buf), range_length));
626
627
0
  RecordTick(statistics_, BLOB_DB_BLOB_FILE_BYTES_READ, range_length);
628
0
  PERF_COUNTER_ADD(blob_read_count, 1);
629
0
  PERF_COUNTER_ADD(blob_read_byte, range_length);
630
0
  RecordLazyRead(statistics_, range_length);
631
0
  RecordLazyPartialRead(statistics_, range_length, payload_size);
632
0
  if (bytes_read) {
633
0
    *bytes_read = range_length;
634
0
  }
635
636
0
  PinOwnedBlob(&blob_contents, value);
637
638
0
  assert(s.ok());
639
0
  return s;
640
0
}
641
642
void BlobSource::MultiGetSimpleGen2Blob(
643
    const ReadOptions& read_options, const OffsetableCacheKey& base_cache_key,
644
    RandomAccessFileReader* file, ChecksumType checksum_type,
645
    uint32_t base_context_checksum, size_t num_records,
646
0
    SimpleGen2BlobReadRequest* reqs) {
647
0
  assert(file);
648
0
  if (num_records == 0) {
649
0
    return;
650
0
  }
651
652
0
  const bool no_io = read_options.read_tier == kBlockCacheTier;
653
0
  MemoryAllocator* const allocator = (blob_cache_ && read_options.fill_cache)
654
0
                                         ? blob_cache_.get()->memory_allocator()
655
0
                                         : nullptr;
656
657
  // Records that missed the cache and must be read from disk, in read order;
658
  // each owns its buffer until we build a BlobContents from it below.
659
0
  struct Pending {
660
0
    size_t idx;
661
0
    CacheAllocationPtr buf;
662
0
    uint64_t record_size;
663
0
  };
664
0
  std::vector<Pending> pending;
665
0
  std::vector<SimpleGen2RecordReadRequest> read_reqs;
666
667
0
  for (size_t i = 0; i < num_records; ++i) {
668
0
    SimpleGen2BlobReadRequest& req = reqs[i];
669
0
    assert(req.result);
670
0
    assert(req.status);
671
672
0
    const uint64_t record_size = req.payload_size + kSimpleGen2BlobTrailerSize;
673
0
    const CacheKey cache_key =
674
0
        GetSimpleGen2BlobCacheKey(base_cache_key, req.record_offset);
675
676
0
    if (blob_cache_) {
677
0
      CacheHandleGuard<BlobContents> blob_handle;
678
0
      const Status cs = GetBlobFromCache(cache_key.AsSlice(), &blob_handle);
679
0
      if (cs.ok()) {
680
0
        *req.status = cs;
681
0
        PinCachedBlob(&blob_handle, req.result);
682
0
        continue;
683
0
      }
684
0
    }
685
686
0
    if (no_io) {
687
0
      *req.status =
688
0
          Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
689
0
      continue;
690
0
    }
691
692
0
    CacheAllocationPtr buf =
693
0
        AllocateBlock(static_cast<size_t>(record_size), allocator);
694
0
    read_reqs.push_back(SimpleGen2RecordReadRequest{
695
0
        req.record_offset, static_cast<size_t>(req.payload_size),
696
0
        static_cast<size_t>(record_size), req.expected_compression, buf.get(),
697
0
        req.status});
698
0
    pending.push_back(Pending{i, std::move(buf), record_size});
699
0
  }
700
701
0
  if (read_reqs.empty()) {
702
0
    return;
703
0
  }
704
705
0
  ReadAndVerifySimpleGen2BlobRecords(read_options, file, checksum_type,
706
0
                                     base_context_checksum, read_reqs.size(),
707
0
                                     read_reqs.data());
708
709
0
  for (size_t k = 0; k < pending.size(); ++k) {
710
0
    SimpleGen2BlobReadRequest& req = reqs[pending[k].idx];
711
0
    if (!req.status->ok()) {
712
0
      continue;
713
0
    }
714
0
    const uint64_t record_size = pending[k].record_size;
715
0
    std::unique_ptr<BlobContents> blob_contents(new BlobContents(
716
0
        std::move(pending[k].buf), static_cast<size_t>(req.payload_size)));
717
718
0
    RecordTick(statistics_, BLOB_DB_BLOB_FILE_BYTES_READ, record_size);
719
0
    PERF_COUNTER_ADD(blob_read_count, 1);
720
0
    PERF_COUNTER_ADD(blob_read_byte, record_size);
721
0
    if (read_options.io_activity == Env::IOActivity::kLazyResolve) {
722
0
      RecordLazyRead(statistics_, record_size);
723
0
    }
724
725
0
    if (blob_cache_ && read_options.fill_cache) {
726
0
      const CacheKey cache_key =
727
0
          GetSimpleGen2BlobCacheKey(base_cache_key, req.record_offset);
728
0
      CacheHandleGuard<BlobContents> blob_handle;
729
0
      const Status ps =
730
0
          PutBlobIntoCache(cache_key.AsSlice(), &blob_contents, &blob_handle);
731
0
      if (!ps.ok()) {
732
0
        *req.status = ps;
733
0
      } else {
734
0
        PinCachedBlob(&blob_handle, req.result);
735
0
      }
736
0
    } else {
737
0
      PinOwnedBlob(&blob_contents, req.result);
738
0
    }
739
0
  }
740
0
}
741
742
void BlobSource::MultiGetSimpleGen2BlobRange(
743
    const ReadOptions& read_options, const OffsetableCacheKey& base_cache_key,
744
    RandomAccessFileReader* file, size_t num_records,
745
0
    SimpleGen2BlobRangeReadRequest* reqs) {
746
0
  assert(file);
747
  // Range reads are only issued while resolving a lazy result (partial reads
748
  // are lazy-only), so the lazy read stats below are recorded unconditionally.
749
0
  assert(read_options.io_activity == Env::IOActivity::kLazyResolve);
750
0
  if (num_records == 0) {
751
0
    return;
752
0
  }
753
754
0
  const bool no_io = read_options.read_tier == kBlockCacheTier;
755
756
0
  struct Pending {
757
0
    size_t idx;
758
0
    CacheAllocationPtr buf;
759
0
  };
760
0
  std::vector<Pending> pending;
761
0
  std::vector<SimpleGen2RangeReadRequest> read_reqs;
762
763
0
  for (size_t i = 0; i < num_records; ++i) {
764
0
    SimpleGen2BlobRangeReadRequest& req = reqs[i];
765
0
    assert(req.result);
766
0
    assert(req.status);
767
768
0
    const CacheKey cache_key =
769
0
        GetSimpleGen2BlobCacheKey(base_cache_key, req.record_offset);
770
771
    // Probe the whole-payload cache; on a hit slice the requested sub-range.
772
0
    if (blob_cache_) {
773
0
      CacheHandleGuard<BlobContents> blob_handle;
774
0
      const Status cs = GetBlobFromCache(cache_key.AsSlice(), &blob_handle);
775
0
      if (cs.ok()) {
776
0
        *req.status = cs;
777
0
        PinCacheHitSubRange(&blob_handle, req.range_offset, req.range_length,
778
0
                            req.result);
779
0
        continue;
780
0
      }
781
0
    }
782
783
0
    if (no_io) {
784
0
      *req.status =
785
0
          Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
786
0
      continue;
787
0
    }
788
789
    // No cache-fill allocator: a partial payload is never inserted.
790
0
    CacheAllocationPtr buf =
791
0
        AllocateBlock(req.range_length, /*allocator=*/nullptr);
792
0
    read_reqs.push_back(SimpleGen2RangeReadRequest{
793
0
        req.record_offset, static_cast<size_t>(req.payload_size),
794
0
        req.range_offset, req.range_length, req.expected_compression, buf.get(),
795
0
        req.status});
796
0
    pending.push_back(Pending{i, std::move(buf)});
797
0
  }
798
799
0
  if (read_reqs.empty()) {
800
0
    return;
801
0
  }
802
803
0
  ReadSimpleGen2BlobRanges(read_options, file, read_reqs.size(),
804
0
                           read_reqs.data());
805
806
0
  for (size_t k = 0; k < pending.size(); ++k) {
807
0
    SimpleGen2BlobRangeReadRequest& req = reqs[pending[k].idx];
808
0
    if (!req.status->ok()) {
809
0
      continue;
810
0
    }
811
0
    std::unique_ptr<BlobContents> blob_contents(
812
0
        new BlobContents(std::move(pending[k].buf), req.range_length));
813
814
0
    RecordTick(statistics_, BLOB_DB_BLOB_FILE_BYTES_READ, req.range_length);
815
0
    PERF_COUNTER_ADD(blob_read_count, 1);
816
0
    PERF_COUNTER_ADD(blob_read_byte, req.range_length);
817
0
    RecordLazyRead(statistics_, req.range_length);
818
0
    RecordLazyPartialRead(statistics_, req.range_length, req.payload_size);
819
820
0
    PinOwnedBlob(&blob_contents, req.result);
821
0
  }
822
0
}
823
824
void BlobSource::MultiGetBlob(const ReadOptions& read_options,
825
                              autovector<BlobFileReadRequests>& blob_reqs,
826
0
                              uint64_t* bytes_read) {
827
0
  assert(blob_reqs.size() > 0);
828
829
0
  uint64_t total_bytes_read = 0;
830
0
  uint64_t bytes_read_in_file = 0;
831
832
0
  for (auto& [blob_file, file_size, blob_reqs_in_file] : blob_reqs) {
833
    // sort blob_reqs_in_file by file offset.
834
0
    std::sort(
835
0
        blob_reqs_in_file.begin(), blob_reqs_in_file.end(),
836
0
        [](const BlobReadRequest& lhs, const BlobReadRequest& rhs) -> bool {
837
0
          return lhs.offset < rhs.offset;
838
0
        });
839
840
0
    MultiGetBlobFromOneFile(read_options, blob_file, file_size,
841
0
                            blob_reqs_in_file, &bytes_read_in_file);
842
843
0
    total_bytes_read += bytes_read_in_file;
844
0
  }
845
846
0
  if (bytes_read) {
847
0
    *bytes_read = total_bytes_read;
848
0
  }
849
0
}
850
851
void BlobSource::MultiGetBlobFromOneFile(const ReadOptions& read_options,
852
                                         const BlobFileOpenInfo& blob_file,
853
                                         uint64_t /*file_size*/,
854
                                         autovector<BlobReadRequest>& blob_reqs,
855
0
                                         uint64_t* bytes_read) {
856
0
  const size_t num_blobs = blob_reqs.size();
857
0
  assert(num_blobs > 0);
858
0
  assert(num_blobs <= MultiGetContext::MAX_BATCH_SIZE);
859
860
#ifndef NDEBUG
861
  for (size_t i = 0; i < num_blobs - 1; ++i) {
862
    assert(blob_reqs[i].offset <= blob_reqs[i + 1].offset);
863
  }
864
#endif  // !NDEBUG
865
866
0
  using Mask = uint64_t;
867
0
  Mask cache_hit_mask = 0;
868
869
0
  uint64_t total_bytes = 0;
870
0
  const uint64_t file_number = blob_file.file_number;
871
0
  const OffsetableCacheKey base_cache_key(db_id_, db_session_id_, file_number);
872
873
0
  if (blob_cache_) {
874
0
    size_t cached_blob_count = 0;
875
0
    for (size_t i = 0; i < num_blobs; ++i) {
876
0
      auto& req = blob_reqs[i];
877
878
0
      CacheHandleGuard<BlobContents> blob_handle;
879
0
      const CacheKey cache_key = base_cache_key.WithOffset(req.offset);
880
0
      const Slice key = cache_key.AsSlice();
881
882
0
      const Status s = GetBlobFromCache(key, &blob_handle);
883
884
0
      if (s.ok()) {
885
0
        assert(req.status);
886
0
        *req.status = s;
887
888
0
        PinCachedBlob(&blob_handle, req.result);
889
890
        // Update the counter for the number of valid blobs read from the cache.
891
0
        ++cached_blob_count;
892
893
        // For consistency, the size of each on-disk (possibly compressed) blob
894
        // record is accumulated to total_bytes.
895
0
        uint64_t adjustment =
896
0
            read_options.verify_checksums
897
0
                ? BlobLogRecord::CalculateAdjustmentForRecordHeader(
898
0
                      req.user_key->size())
899
0
                : 0;
900
0
        assert(req.offset >= adjustment);
901
0
        total_bytes += req.len + adjustment;
902
0
        cache_hit_mask |= (Mask{1} << i);  // cache hit
903
0
      }
904
0
    }
905
906
    // All blobs were read from the cache.
907
0
    if (cached_blob_count == num_blobs) {
908
0
      if (bytes_read) {
909
0
        *bytes_read = total_bytes;
910
0
      }
911
0
      return;
912
0
    }
913
0
  }
914
915
0
  const bool no_io = read_options.read_tier == kBlockCacheTier;
916
0
  if (no_io) {
917
0
    for (size_t i = 0; i < num_blobs; ++i) {
918
0
      if (!(cache_hit_mask & (Mask{1} << i))) {
919
0
        BlobReadRequest& req = blob_reqs[i];
920
0
        assert(req.status);
921
922
0
        *req.status =
923
0
            Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
924
0
      }
925
0
    }
926
0
    return;
927
0
  }
928
929
0
  {
930
    // Find the rest of blobs from the file since I/O is allowed.
931
0
    autovector<std::pair<BlobReadRequest*, std::unique_ptr<BlobContents>>>
932
0
        _blob_reqs;
933
0
    uint64_t _bytes_read = 0;
934
935
0
    for (size_t i = 0; i < num_blobs; ++i) {
936
0
      if (!(cache_hit_mask & (Mask{1} << i))) {
937
0
        _blob_reqs.emplace_back(&blob_reqs[i], std::unique_ptr<BlobContents>());
938
0
      }
939
0
    }
940
941
0
    CacheHandleGuard<BlobFileReader> blob_file_reader;
942
0
    Status s = blob_file_cache_->GetBlobFileReader(read_options, blob_file,
943
0
                                                   &blob_file_reader);
944
0
    if (!s.ok()) {
945
0
      for (size_t i = 0; i < _blob_reqs.size(); ++i) {
946
0
        BlobReadRequest* const req = _blob_reqs[i].first;
947
0
        assert(req);
948
0
        assert(req->status);
949
950
0
        *req->status = s;
951
0
      }
952
0
      return;
953
0
    }
954
955
0
    assert(blob_file_reader.GetValue());
956
957
0
    MemoryAllocator* const allocator =
958
0
        (blob_cache_ && read_options.fill_cache)
959
0
            ? blob_cache_.get()->memory_allocator()
960
0
            : nullptr;
961
962
0
    blob_file_reader.GetValue()->MultiGetBlob(read_options, allocator,
963
0
                                              _blob_reqs, &_bytes_read);
964
965
0
    bool needs_reader_refresh = false;
966
0
    for (const auto& blob_req : _blob_reqs) {
967
0
      BlobReadRequest* const req = blob_req.first;
968
0
      assert(req != nullptr);
969
0
      assert(req->status != nullptr);
970
0
      if (req->status->IsCorruption()) {
971
0
        needs_reader_refresh = true;
972
0
        break;
973
0
      }
974
0
    }
975
976
0
    if (needs_reader_refresh) {
977
0
      blob_file_reader.Reset();
978
0
      blob_file_cache_->Evict(file_number);
979
980
0
      std::unique_ptr<BlobFileReader> fresh_reader;
981
0
      const Status open_status = blob_file_cache_->OpenBlobFileReaderUncached(
982
0
          read_options, blob_file, &fresh_reader,
983
0
          /*allow_footer_skip_retry=*/false);
984
0
      if (!open_status.ok()) {
985
        // Only the corrupted requests fail. The other requests in the batch
986
        // already read their blobs, so fall through to pin them and update
987
        // the stats below.
988
0
        for (const auto& blob_req : _blob_reqs) {
989
0
          BlobReadRequest* const req = blob_req.first;
990
0
          assert(req != nullptr);
991
0
          assert(req->status != nullptr);
992
0
          if (req->status->IsCorruption()) {
993
0
            *req->status =
994
0
                AppendBlobRefreshRetryFailure(*req->status, open_status);
995
0
          }
996
0
        }
997
0
      } else {
998
0
        autovector<std::pair<BlobReadRequest*, std::unique_ptr<BlobContents>>>
999
0
            retry_blob_reqs;
1000
0
        autovector<Status> stale_statuses;
1001
0
        for (auto& blob_req : _blob_reqs) {
1002
0
          BlobReadRequest* const req = blob_req.first;
1003
0
          assert(req != nullptr);
1004
0
          assert(req->status != nullptr);
1005
0
          if (!req->status->IsCorruption()) {
1006
0
            continue;
1007
0
          }
1008
1009
0
          stale_statuses.emplace_back(*req->status);
1010
0
          *req->status = Status::OK();
1011
0
          blob_req.second.reset();
1012
0
          retry_blob_reqs.emplace_back(req, std::unique_ptr<BlobContents>());
1013
0
        }
1014
1015
0
        uint64_t refreshed_bytes_read = 0;
1016
0
        fresh_reader->MultiGetBlob(read_options, allocator, retry_blob_reqs,
1017
0
                                   &refreshed_bytes_read);
1018
0
        _bytes_read += refreshed_bytes_read;
1019
1020
0
        bool install_fresh_reader = false;
1021
0
        for (size_t i = 0; i < retry_blob_reqs.size(); ++i) {
1022
0
          auto& retried_blob_req = retry_blob_reqs[i];
1023
0
          BlobReadRequest* const retried_req = retried_blob_req.first;
1024
0
          assert(retried_req != nullptr);
1025
0
          if (retried_req->status->ok()) {
1026
0
            install_fresh_reader = true;
1027
0
          } else {
1028
0
            *retried_req->status = AppendBlobRefreshRetryFailure(
1029
0
                stale_statuses[i], *retried_req->status);
1030
0
          }
1031
1032
0
          for (auto& blob_req : _blob_reqs) {
1033
0
            if (blob_req.first != retried_req) {
1034
0
              continue;
1035
0
            }
1036
1037
0
            blob_req.second = std::move(retried_blob_req.second);
1038
0
            break;
1039
0
          }
1040
0
        }
1041
1042
0
        if (install_fresh_reader) {
1043
0
          CacheHandleGuard<BlobFileReader> ignored_reader;
1044
0
          blob_file_cache_
1045
0
              ->RefreshBlobFileReader(blob_file, &fresh_reader, &ignored_reader)
1046
0
              .PermitUncheckedError();
1047
0
        }
1048
0
      }
1049
0
    }
1050
1051
0
    if (blob_cache_ && read_options.fill_cache) {
1052
      // If filling cache is allowed and a cache is configured, try to put
1053
      // the blob(s) to the cache.
1054
0
      for (auto& [req, blob_contents] : _blob_reqs) {
1055
0
        assert(req);
1056
1057
0
        if (req->status->ok()) {
1058
0
          CacheHandleGuard<BlobContents> blob_handle;
1059
0
          const CacheKey cache_key = base_cache_key.WithOffset(req->offset);
1060
0
          const Slice key = cache_key.AsSlice();
1061
0
          s = PutBlobIntoCache(key, &blob_contents, &blob_handle);
1062
0
          if (!s.ok()) {
1063
0
            *req->status = s;
1064
0
          } else {
1065
0
            PinCachedBlob(&blob_handle, req->result);
1066
0
          }
1067
0
        }
1068
0
      }
1069
0
    } else {
1070
0
      for (auto& [req, blob_contents] : _blob_reqs) {
1071
0
        assert(req);
1072
1073
0
        if (req->status->ok()) {
1074
0
          PinOwnedBlob(&blob_contents, req->result);
1075
0
        }
1076
0
      }
1077
0
    }
1078
1079
    // Whole-column reads on the lazy resolve path count as lazy reads (mirrors
1080
    // BlobSource::GetBlob, which records only on a disk read, not a cache hit).
1081
    // Gated on the activity so regular MultiGet is unaffected; the misses read
1082
    // above are exactly the disk reads for these blob columns.
1083
0
    if (read_options.io_activity == Env::IOActivity::kLazyResolve) {
1084
0
      for (auto& [req, blob_contents] : _blob_reqs) {
1085
0
        assert(req);
1086
0
        if (req->status->ok()) {
1087
0
          const uint64_t adjustment =
1088
0
              read_options.verify_checksums
1089
0
                  ? BlobLogRecord::CalculateAdjustmentForRecordHeader(
1090
0
                        req->user_key->size())
1091
0
                  : 0;
1092
0
          RecordLazyRead(statistics_, req->len + adjustment);
1093
0
        }
1094
0
      }
1095
0
    }
1096
1097
0
    total_bytes += _bytes_read;
1098
0
    if (bytes_read) {
1099
0
      *bytes_read = total_bytes;
1100
0
    }
1101
0
  }
1102
0
}
1103
1104
void BlobSource::MultiGetBlobRange(
1105
    const ReadOptions& read_options,
1106
0
    autovector<BlobFileRangeReadRequests>& blob_reqs, uint64_t* bytes_read) {
1107
0
  assert(blob_reqs.size() > 0);
1108
1109
0
  uint64_t total_bytes_read = 0;
1110
0
  uint64_t bytes_read_in_file = 0;
1111
1112
0
  for (auto& [blob_file, file_size, blob_reqs_in_file] : blob_reqs) {
1113
    // Sort by effective read offset so the file system layer can coalesce
1114
    // adjacent sub-range reads within one MultiRead.
1115
0
    std::sort(blob_reqs_in_file.begin(), blob_reqs_in_file.end(),
1116
0
              [](const BlobRangeReadRequest& lhs,
1117
0
                 const BlobRangeReadRequest& rhs) -> bool {
1118
0
                return lhs.offset + lhs.range_offset <
1119
0
                       rhs.offset + rhs.range_offset;
1120
0
              });
1121
1122
0
    MultiGetBlobRangeFromOneFile(read_options, blob_file, file_size,
1123
0
                                 blob_reqs_in_file, &bytes_read_in_file);
1124
1125
0
    total_bytes_read += bytes_read_in_file;
1126
0
    bytes_read_in_file = 0;  // reset: some early-return paths do not write it
1127
0
  }
1128
1129
0
  if (bytes_read) {
1130
0
    *bytes_read = total_bytes_read;
1131
0
  }
1132
0
}
1133
1134
void BlobSource::MultiGetBlobRangeFromOneFile(
1135
    const ReadOptions& read_options, const BlobFileOpenInfo& blob_file,
1136
    uint64_t /*file_size*/, autovector<BlobRangeReadRequest>& blob_reqs,
1137
0
    uint64_t* bytes_read) {
1138
0
  const size_t num_blobs = blob_reqs.size();
1139
0
  assert(num_blobs > 0);
1140
0
  assert(num_blobs <= MultiGetContext::MAX_BATCH_SIZE);
1141
  // Range reads are only issued while resolving a lazy result (partial reads
1142
  // are lazy-only), so the lazy read stats below are recorded unconditionally.
1143
0
  assert(read_options.io_activity == Env::IOActivity::kLazyResolve);
1144
1145
0
  using Mask = uint64_t;
1146
0
  Mask cache_hit_mask = 0;
1147
1148
0
  uint64_t total_bytes = 0;
1149
0
  const uint64_t file_number = blob_file.file_number;
1150
0
  const OffsetableCacheKey base_cache_key(db_id_, db_session_id_, file_number);
1151
1152
  // Probe the whole-value cache per request; on a hit, slice the requested
1153
  // sub-range (zero-copy, no disk read). A partial read is never cache-filled.
1154
0
  if (blob_cache_) {
1155
0
    size_t cached_blob_count = 0;
1156
0
    for (size_t i = 0; i < num_blobs; ++i) {
1157
0
      BlobRangeReadRequest& req = blob_reqs[i];
1158
0
      assert(req.status);
1159
1160
0
      CacheHandleGuard<BlobContents> blob_handle;
1161
0
      const CacheKey cache_key = base_cache_key.WithOffset(req.offset);
1162
0
      const Status s = GetBlobFromCache(cache_key.AsSlice(), &blob_handle);
1163
0
      if (s.ok()) {
1164
0
        *req.status = s;
1165
0
        PinCacheHitSubRange(&blob_handle, req.range_offset, req.range_length,
1166
0
                            req.result);
1167
0
        ++cached_blob_count;
1168
0
        cache_hit_mask |= (Mask{1} << i);  // hit -> no disk read
1169
0
      }
1170
0
    }
1171
1172
0
    if (cached_blob_count == num_blobs) {
1173
0
      if (bytes_read) {
1174
0
        *bytes_read = 0;
1175
0
      }
1176
0
      return;
1177
0
    }
1178
0
  }
1179
1180
0
  const bool no_io = read_options.read_tier == kBlockCacheTier;
1181
0
  if (no_io) {
1182
0
    for (size_t i = 0; i < num_blobs; ++i) {
1183
0
      if (!(cache_hit_mask & (Mask{1} << i))) {
1184
0
        assert(blob_reqs[i].status);
1185
0
        *blob_reqs[i].status =
1186
0
            Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
1187
0
      }
1188
0
    }
1189
0
    return;
1190
0
  }
1191
1192
0
  autovector<std::pair<BlobRangeReadRequest*, std::unique_ptr<BlobContents>>>
1193
0
      _blob_reqs;
1194
0
  for (size_t i = 0; i < num_blobs; ++i) {
1195
0
    if (!(cache_hit_mask & (Mask{1} << i))) {
1196
0
      _blob_reqs.emplace_back(&blob_reqs[i], std::unique_ptr<BlobContents>());
1197
0
    }
1198
0
  }
1199
1200
  // Lazy range reads run against a pinned SuperVersion (max_open_files == -1),
1201
  // and blob file numbers are never reused, so the stale cached-reader race
1202
  // that MultiGetBlobFromOneFile guards against cannot occur here; a Corruption
1203
  // is simply surfaced per request. Hence no reader-refresh retry and no
1204
  // cache-fill (a partial value cannot represent the whole-record cache entry).
1205
0
  CacheHandleGuard<BlobFileReader> blob_file_reader;
1206
0
  Status s = blob_file_cache_->GetBlobFileReader(read_options, blob_file,
1207
0
                                                 &blob_file_reader);
1208
0
  if (!s.ok()) {
1209
0
    for (auto& blob_req : _blob_reqs) {
1210
0
      assert(blob_req.first);
1211
0
      assert(blob_req.first->status);
1212
0
      *blob_req.first->status = s;
1213
0
    }
1214
0
    return;
1215
0
  }
1216
0
  assert(blob_file_reader.GetValue());
1217
1218
0
  uint64_t _bytes_read = 0;
1219
0
  blob_file_reader.GetValue()->MultiGetBlobRange(read_options, _blob_reqs,
1220
0
                                                 &_bytes_read);
1221
1222
0
  for (auto& [req, blob_contents] : _blob_reqs) {
1223
0
    assert(req);
1224
0
    if (req->status->ok()) {
1225
0
      PinOwnedBlob(&blob_contents, req->result);
1226
0
      RecordLazyRead(statistics_, req->range_length);
1227
0
      RecordLazyPartialRead(statistics_, req->range_length, req->value_size);
1228
0
    }
1229
0
  }
1230
1231
0
  total_bytes += _bytes_read;
1232
0
  if (bytes_read) {
1233
0
    *bytes_read = total_bytes;
1234
0
  }
1235
0
}
1236
1237
bool BlobSource::TEST_BlobInCache(uint64_t file_number, uint64_t file_size,
1238
0
                                  uint64_t offset, size_t* charge) const {
1239
0
  const CacheKey cache_key = GetCacheKey(file_number, file_size, offset);
1240
0
  const Slice key = cache_key.AsSlice();
1241
1242
0
  CacheHandleGuard<BlobContents> blob_handle;
1243
0
  const Status s = GetBlobFromCache(key, &blob_handle);
1244
1245
0
  if (s.ok() && blob_handle.GetValue() != nullptr) {
1246
0
    if (charge) {
1247
0
      const Cache* const cache = blob_handle.GetCache();
1248
0
      assert(cache);
1249
1250
0
      Cache::Handle* const handle = blob_handle.GetCacheHandle();
1251
0
      assert(handle);
1252
1253
0
      *charge = cache->GetUsage(handle);
1254
0
    }
1255
1256
0
    return true;
1257
0
  }
1258
1259
0
  return false;
1260
0
}
1261
1262
}  // namespace ROCKSDB_NAMESPACE