/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 |