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