1use super::allocation::{AllocationError, AllocationLedger, LogicalVecCapacity, VisitedObjectSet};
4use super::codec::{
5 CanonicalDecodeError, DecodeLimits, DecodedPageKind, DecodedPageShape, Decoder, Encoder,
6};
7use super::frontier;
8use super::frontier::Machine as _;
9use crate::async_storage::{
10 AsyncObjectStore, DecodedCacheAdmission, DecodedCacheKey, DecodedCacheValue,
11};
12use crate::cancellation::CancellationToken;
13use crate::performance::{OperationFailure, WorkBudget, WorkCounters, WorkError};
14use crate::speculation::{ResidencyHint, ResidencyReason};
15use crate::storage::{
16 ByteRange, ObjectId, ObjectKind, ObjectRead, ObjectReadRequest, ObjectReadRetention,
17 ObjectReceipt, ObjectStore, ObjectStoreError, object_digest,
18};
19use bytes::Bytes;
20use std::future::Future;
21use std::io::Read;
22use std::mem::size_of;
23use std::sync::Arc;
24use thiserror::Error;
25
26const DOMAIN: &[u8; 8] = b"ACYFSBLB";
27const VERSION: u16 = 1;
28const BLOB_PAGE_HEADER_BYTES: usize = 8 + 2 + 8 + 8 + 1 + 4;
29const BLOB_PAGE_ITEM_BYTES: usize = 8 + 8 + 32;
30
31#[derive(Clone, Copy, Debug, Eq, PartialEq)]
33pub struct BlobChunkRef {
34 pub first_offset: u64,
36 pub end_offset: u64,
38 pub chunk: ObjectId,
40}
41
42#[derive(Clone, Copy, Debug, Eq, PartialEq)]
44pub struct BlobChild {
45 pub first_offset: u64,
47 pub end_offset: u64,
49 pub page: ObjectId,
51}
52
53#[derive(Clone, Debug, Eq, PartialEq)]
55pub enum BlobNode {
56 Leaf(Vec<BlobChunkRef>),
58 Internal(Vec<BlobChild>),
60}
61
62#[derive(Clone, Debug, Eq, PartialEq)]
64pub struct BlobPage {
65 pub first_offset: u64,
67 pub end_offset: u64,
69 pub node: BlobNode,
71}
72
73impl BlobPage {
74 fn validate(&self, maximum_items: u32) -> Result<(), CanonicalDecodeError> {
75 let (count, ranges_valid, kinds_valid) = match &self.node {
76 BlobNode::Leaf(items) => (
77 items.len(),
78 contiguous(
79 self.first_offset,
80 self.end_offset,
81 items
82 .iter()
83 .map(|item| (item.first_offset, item.end_offset)),
84 ),
85 items
86 .iter()
87 .all(|item| item.chunk.kind == ObjectKind::BlobChunk),
88 ),
89 BlobNode::Internal(items) => (
90 items.len(),
91 contiguous(
92 self.first_offset,
93 self.end_offset,
94 items
95 .iter()
96 .map(|item| (item.first_offset, item.end_offset)),
97 ),
98 items.iter().all(|item| item.page.kind == ObjectKind::Blob),
99 ),
100 };
101 if u32::try_from(count).unwrap_or(u32::MAX) > maximum_items {
102 return Err(CanonicalDecodeError::FieldTooLarge {
103 observed: u32::try_from(count).unwrap_or(u32::MAX),
104 maximum: maximum_items,
105 });
106 }
107 if count == 0 && (self.first_offset != 0 || self.end_offset != 0) {
108 return Err(invariant("non-empty blob bounds require entries"));
109 }
110 if count != 0 && (!ranges_valid || self.first_offset >= self.end_offset) {
111 return Err(invariant("blob ranges are not positive and contiguous"));
112 }
113 if !kinds_valid {
114 return Err(invariant("blob reference has the wrong object kind"));
115 }
116 Ok(())
117 }
118}
119
120fn contiguous(first: u64, end: u64, mut ranges: impl Iterator<Item = (u64, u64)>) -> bool {
121 let Some((item_first, item_end)) = ranges.next() else {
122 return first == 0 && end == 0;
123 };
124 if item_first != first || item_first >= item_end {
125 return false;
126 }
127 let mut prior_end = item_end;
128 for (next_first, next_end) in ranges {
129 if next_first != prior_end || next_first >= next_end {
130 return false;
131 }
132 prior_end = next_end;
133 }
134 prior_end == end
135}
136
137pub fn encode_blob_page(
143 page: &BlobPage,
144 maximum_items: u32,
145) -> Result<Vec<u8>, CanonicalDecodeError> {
146 let encoded_length = blob_page_encoded_length(page, maximum_items)?;
147 let mut encoder = Encoder::with_exact_capacity(DOMAIN, VERSION, encoded_length)?;
148 encoder.u64(page.first_offset);
149 encoder.u64(page.end_offset);
150 match &page.node {
151 BlobNode::Leaf(items) => {
152 encoder.u8(1);
153 encoder.u32(count(items.len())?);
154 for item in items {
155 encoder.u64(item.first_offset);
156 encoder.u64(item.end_offset);
157 encoder.fixed(item.chunk.digest.as_bytes());
158 }
159 }
160 BlobNode::Internal(items) => {
161 encoder.u8(2);
162 encoder.u32(count(items.len())?);
163 for item in items {
164 encoder.u64(item.first_offset);
165 encoder.u64(item.end_offset);
166 encoder.fixed(item.page.digest.as_bytes());
167 }
168 }
169 }
170 Ok(encoder.finish())
171}
172
173fn blob_page_encoded_length(
174 page: &BlobPage,
175 maximum_items: u32,
176) -> Result<usize, CanonicalDecodeError> {
177 page.validate(maximum_items)?;
178 let items = match &page.node {
179 BlobNode::Leaf(items) => items.len(),
180 BlobNode::Internal(items) => items.len(),
181 };
182 items
183 .checked_mul(BLOB_PAGE_ITEM_BYTES)
184 .and_then(|bytes| BLOB_PAGE_HEADER_BYTES.checked_add(bytes))
185 .ok_or(CanonicalDecodeError::LengthOverflow)
186}
187
188pub fn decode_blob_page(
194 bytes: &[u8],
195 limits: DecodeLimits,
196) -> Result<BlobPage, CanonicalDecodeError> {
197 let mut decoder = Decoder::new(bytes, DOMAIN, VERSION, limits.maximum_page_object_bytes())?;
198 let first_offset = decoder.u64()?;
199 let end_offset = decoder.u64()?;
200 let tag = decoder.u8()?;
201 let item_count = decoder.u32()?;
202 if item_count > limits.maximum_page_items {
203 return Err(CanonicalDecodeError::FieldTooLarge {
204 observed: item_count,
205 maximum: limits.maximum_page_items,
206 });
207 }
208 let mut leaf = Vec::new();
209 let mut internal = Vec::new();
210 match tag {
211 1 => {
212 leaf.try_reserve_exact(capacity(item_count)?)
213 .map_err(|_| CanonicalDecodeError::AllocationFailed)?;
214 for _ in 0..item_count {
215 leaf.push(BlobChunkRef {
216 first_offset: decoder.u64()?,
217 end_offset: decoder.u64()?,
218 chunk: ObjectId {
219 kind: ObjectKind::BlobChunk,
220 digest: crate::foundation::Digest::from_bytes(decoder.fixed()?),
221 },
222 });
223 }
224 }
225 2 => {
226 internal
227 .try_reserve_exact(capacity(item_count)?)
228 .map_err(|_| CanonicalDecodeError::AllocationFailed)?;
229 for _ in 0..item_count {
230 internal.push(BlobChild {
231 first_offset: decoder.u64()?,
232 end_offset: decoder.u64()?,
233 page: ObjectId {
234 kind: ObjectKind::Blob,
235 digest: crate::foundation::Digest::from_bytes(decoder.fixed()?),
236 },
237 });
238 }
239 }
240 tag => {
241 return Err(CanonicalDecodeError::UnknownTag {
242 field: "blob_page",
243 tag,
244 });
245 }
246 }
247 decoder.finish()?;
248 let page = BlobPage {
249 first_offset,
250 end_offset,
251 node: if tag == 1 {
252 BlobNode::Leaf(leaf)
253 } else {
254 BlobNode::Internal(internal)
255 },
256 };
257 page.validate(limits.maximum_page_items)?;
258 Ok(page)
259}
260
261fn blob_page_decode_shape(
262 bytes: &[u8],
263 limits: DecodeLimits,
264) -> Result<DecodedPageShape, CanonicalDecodeError> {
265 let mut decoder = Decoder::new(bytes, DOMAIN, VERSION, limits.maximum_page_object_bytes())?;
266 decoder.u64()?;
267 decoder.u64()?;
268 let tag = decoder.u8()?;
269 let item_count = decoder.u32()?;
270 if item_count > limits.maximum_page_items {
271 return Err(CanonicalDecodeError::FieldTooLarge {
272 observed: item_count,
273 maximum: limits.maximum_page_items,
274 });
275 }
276 let kind = match tag {
277 1 | 2 => {
278 for _ in 0..item_count {
279 decoder.u64()?;
280 decoder.u64()?;
281 let _: [u8; 32] = decoder.fixed()?;
282 }
283 if tag == 1 {
284 DecodedPageKind::Leaf
285 } else {
286 DecodedPageKind::Internal
287 }
288 }
289 tag => {
290 return Err(CanonicalDecodeError::UnknownTag {
291 field: "blob_page",
292 tag,
293 });
294 }
295 };
296 decoder.finish()?;
297 Ok(DecodedPageShape {
298 kind,
299 items: usize::try_from(item_count).map_err(|_| CanonicalDecodeError::LengthOverflow)?,
300 nested_bytes: 0,
301 })
302}
303
304pub fn blob_page_id(page: &BlobPage, maximum_items: u32) -> Result<ObjectId, CanonicalDecodeError> {
310 let bytes = encode_blob_page(page, maximum_items)?;
311 Ok(ObjectId {
312 kind: ObjectKind::Blob,
313 digest: object_digest(ObjectKind::Blob, &bytes),
314 })
315}
316
317#[derive(Clone, Copy, Debug, Eq, PartialEq)]
319pub struct BlobBuildOptions {
320 pub chunk_bytes: u32,
322 pub page_items: u32,
324 pub page_bytes: u32,
326 pub maximum_blob_bytes: u64,
328}
329
330#[derive(Clone, Debug, Eq, PartialEq)]
332pub struct BlobBuild {
333 pub root: ObjectId,
335 pub logical_bytes: u64,
337 pub work: WorkCounters,
339}
340
341pub trait AsyncBlobSource {
343 fn read<'a>(
345 &'a mut self,
346 destination: &'a mut [u8],
347 cancellation: &'a CancellationToken,
348 ) -> impl Future<Output = std::io::Result<usize>>;
349
350 fn read_owned(
352 &mut self,
353 _maximum: usize,
354 _cancellation: &CancellationToken,
355 ) -> impl Future<Output = std::io::Result<Option<Bytes>>> {
356 async { Ok(None) }
357 }
358
359 fn take_work(&mut self) -> Option<WorkCounters> {
363 None
364 }
365}
366
367impl<T: Read> AsyncBlobSource for T {
368 async fn read<'a>(
369 &'a mut self,
370 destination: &'a mut [u8],
371 cancellation: &'a CancellationToken,
372 ) -> std::io::Result<usize> {
373 if cancellation.is_cancelled() {
374 return Err(std::io::Error::new(
375 std::io::ErrorKind::Interrupted,
376 "blob source read cancelled",
377 ));
378 }
379 Read::read(self, destination)
380 }
381}
382
383pub fn build_blob<S: crate::ImmediateObjectStore, R: Read>(
391 store: &S,
392 source: &mut R,
393 options: BlobBuildOptions,
394 budget: WorkBudget,
395) -> Result<BlobBuild, BlobBuildFailure> {
396 let cancellation = CancellationToken::new();
397 crate::async_storage::poll_immediate(build_blob_async(
398 store,
399 source,
400 options,
401 budget,
402 &cancellation,
403 ))
404}
405
406pub async fn build_blob_async<S: AsyncObjectStore, R: AsyncBlobSource>(
414 store: &S,
415 source: &mut R,
416 options: BlobBuildOptions,
417 budget: WorkBudget,
418 cancellation: &CancellationToken,
419) -> Result<BlobBuild, BlobBuildFailure> {
420 let chunk_capacity = validate_blob_build(options, cancellation)?;
421 let (mut batching, mut work) = BlobBatchStore::new(store, budget)?;
422 let mut index = BlobIndexBuilder::new(options);
423 let mut logical_bytes = 0_u64;
424 loop {
425 let detection_bytes = options
426 .maximum_blob_bytes
427 .saturating_sub(logical_bytes)
428 .saturating_add(1);
429 let allocation = chunk_capacity.min(usize::try_from(detection_bytes).unwrap_or(usize::MAX));
430 let retained = batching.retained_bytes();
431 let owned = source.read_owned(allocation, cancellation).await;
432 let source_work = source.take_work();
433 if let Some(measured) = source_work {
434 work = charge_source_work(
435 work,
436 measured,
437 retained.saturating_add(index.live_allocation_bytes),
438 budget,
439 )?;
440 }
441 let owned = owned.map_err(|error| build_failed(BlobBuildError::Source(error), work))?;
442 if let Some(chunk) = owned {
443 if chunk.is_empty() {
444 break;
445 }
446 let filled = chunk.len();
447 (logical_bytes, work) = accept_owned_blob_chunk(
448 &mut batching,
449 &mut index,
450 chunk,
451 allocation,
452 retained,
453 source_work.is_none(),
454 logical_bytes,
455 options.maximum_blob_bytes,
456 budget,
457 work,
458 cancellation,
459 )
460 .await?;
461 if filled < allocation {
462 break;
463 }
464 continue;
465 }
466 let (bytes, filled, retained_capacity, simultaneous, next_work) = read_blob_chunk(
467 source,
468 allocation,
469 index.live_allocation_bytes,
470 retained,
471 work,
472 budget,
473 cancellation,
474 )
475 .await?;
476 work = next_work;
477 if filled == 0 {
478 break;
479 }
480 let bytes = if filled == bytes.len() {
481 Bytes::from(bytes)
482 } else {
483 Bytes::copy_from_slice(
484 bytes
485 .get(..filled)
486 .ok_or_else(|| build_failed(BlobBuildError::TooLarge, work))?,
487 )
488 };
489 (logical_bytes, work) = accept_owned_blob_chunk(
490 &mut batching,
491 &mut index,
492 bytes,
493 allocation,
494 retained,
495 false,
496 logical_bytes,
497 options.maximum_blob_bytes,
498 budget,
499 work,
500 cancellation,
501 )
502 .await?;
503 let _ = (retained_capacity, simultaneous);
504 if filled < allocation {
505 break;
506 }
507 }
508 let root = index
509 .finish(&mut batching, budget, &mut work, cancellation)
510 .await?;
511 work = finish_blob_batch(&mut batching, work, budget, cancellation).await?;
512 Ok(BlobBuild {
513 root,
514 logical_bytes,
515 work,
516 })
517}
518
519#[allow(clippy::too_many_arguments)]
520async fn read_blob_chunk<R: AsyncBlobSource>(
521 source: &mut R,
522 allocation: usize,
523 index_bytes: u64,
524 retained: u64,
525 mut work: WorkCounters,
526 budget: WorkBudget,
527 cancellation: &CancellationToken,
528) -> Result<(Vec<u8>, usize, u64, u64, WorkCounters), BlobBuildFailure> {
529 let (mut bytes, prospective, simultaneous, retained_capacity) =
530 allocate_chunk_buffer(allocation, index_bytes, retained, work, budget)?;
531 work = prospective;
532 let mut filled = 0_usize;
533 while filled < bytes.len() {
534 if cancellation.is_cancelled() {
535 return Err(build_failed(BlobBuildError::Cancelled, work));
536 }
537 #[allow(clippy::indexing_slicing, reason = "bounded by while loop above")]
538 let read = AsyncBlobSource::read(source, &mut bytes[filled..], cancellation).await;
539 let measured = source.take_work();
540 if let Some(measured) = measured {
541 work = charge_source_work(work, measured, simultaneous, budget)?;
542 }
543 match read {
544 Ok(0) => break,
545 Ok(count) => {
546 if count > bytes.len() - filled {
547 return Err(build_failed(BlobBuildError::TooLarge, work));
548 }
549 filled = filled.saturating_add(count);
550 if measured.is_none() {
551 work = build_add(
552 work,
553 WorkCounters {
554 source_bytes_read: u64::try_from(count).unwrap_or(u64::MAX),
555 ..WorkCounters::default()
556 },
557 )?;
558 }
559 build_verify(work, budget)?;
560 }
561 Err(error) => return Err(build_failed(BlobBuildError::Source(error), work)),
562 }
563 }
564 Ok((bytes, filled, retained_capacity, simultaneous, work))
565}
566
567fn charge_source_work(
568 work: WorkCounters,
569 mut measured: WorkCounters,
570 live_bytes: u64,
571 budget: WorkBudget,
572) -> Result<WorkCounters, BlobBuildFailure> {
573 let simultaneous = live_bytes
574 .checked_add(measured.peak_allocation_bytes)
575 .ok_or_else(|| build_failed(BlobBuildError::TooLarge, work))?;
576 measured.peak_allocation_bytes = 0;
577 let mut combined = build_add(work, measured)?;
578 combined.peak_allocation_bytes = combined.peak_allocation_bytes.max(simultaneous);
579 build_verify(combined, budget)?;
580 Ok(combined)
581}
582
583#[allow(clippy::too_many_arguments)]
584async fn accept_owned_blob_chunk<S: AsyncObjectStore>(
585 batching: &mut BlobBatchStore<'_, S>,
586 index: &mut BlobIndexBuilder,
587 chunk: Bytes,
588 allocation: usize,
589 retained: u64,
590 owned_source: bool,
591 logical_bytes: u64,
592 maximum_blob_bytes: u64,
593 budget: WorkBudget,
594 work: WorkCounters,
595 cancellation: &CancellationToken,
596) -> Result<(u64, WorkCounters), BlobBuildFailure> {
597 if chunk.len() > allocation {
598 return Err(build_failed(BlobBuildError::TooLarge, work));
599 }
600 let retained_capacity = chunk.len() as u64;
601 let prospective = build_add(
602 work,
603 WorkCounters {
604 source_bytes_read: if owned_source { retained_capacity } else { 0 },
605 peak_allocation_bytes: index
606 .live_allocation_bytes
607 .saturating_add(retained)
608 .saturating_add(retained_capacity),
609 ..WorkCounters::default()
610 },
611 )?;
612 build_verify(prospective, budget)?;
613 let logical_bytes = logical_bytes
614 .checked_add(retained_capacity)
615 .ok_or_else(|| build_failed(BlobBuildError::TooLarge, prospective))?;
616 if logical_bytes > maximum_blob_bytes {
617 return Err(build_failed(BlobBuildError::TooLarge, prospective));
618 }
619 let first_offset = logical_bytes - retained_capacity;
620 let chunk_id = ObjectId {
621 kind: ObjectKind::BlobChunk,
622 digest: object_digest(ObjectKind::BlobChunk, &chunk),
623 };
624 let mut work = build_put(
625 batching,
626 chunk_id,
627 chunk,
628 PutAllocation {
629 retained: retained_capacity,
630 live: index
631 .live_allocation_bytes
632 .saturating_add(retained_capacity),
633 },
634 budget,
635 prospective,
636 cancellation,
637 )
638 .await?;
639 index
640 .push_chunk(
641 batching,
642 BlobChunkRef {
643 first_offset,
644 end_offset: logical_bytes,
645 chunk: chunk_id,
646 },
647 budget,
648 &mut work,
649 cancellation,
650 )
651 .await?;
652 Ok((logical_bytes, work))
653}
654
655fn validate_blob_build(
656 options: BlobBuildOptions,
657 cancellation: &CancellationToken,
658) -> Result<usize, BlobBuildFailure> {
659 if cancellation.is_cancelled() {
660 return Err(OperationFailure::before_work(BlobBuildError::Cancelled));
661 }
662 if options.chunk_bytes == 0
663 || effective_page_width(options).is_none()
664 || options.maximum_blob_bytes == 0
665 {
666 return Err(build_failed(
667 BlobBuildError::InvalidOptions,
668 WorkCounters::default(),
669 ));
670 }
671 usize::try_from(options.chunk_bytes)
672 .map_err(|_| build_failed(BlobBuildError::InvalidOptions, WorkCounters::default()))
673}
674
675async fn finish_blob_batch<S: AsyncObjectStore>(
676 batching: &mut BlobBatchStore<'_, S>,
677 work: WorkCounters,
678 budget: WorkBudget,
679 cancellation: &CancellationToken,
680) -> Result<WorkCounters, BlobBuildFailure> {
681 let retained = batching.retained_bytes();
682 let mut remaining = work
683 .remaining(budget)
684 .map_err(|error| build_failed(BlobBuildError::Work(error), work))?;
685 remaining.peak_allocation_bytes = remaining
686 .peak_allocation_bytes
687 .checked_sub(retained)
688 .ok_or_else(|| build_failed(BlobBuildError::Work(WorkError::Overflow), work))?;
689 let receipt = batching
690 .finish(remaining, cancellation)
691 .await
692 .map_err(|failure| failure.map_with_prior_work(work, BlobBuildError::Storage))?;
693 let backend_peak = receipt.work.peak_allocation_bytes;
694 let mut backend_work = receipt.work;
695 backend_work.peak_allocation_bytes = 0;
696 let mut combined = build_add(work, backend_work)?;
697 let simultaneous = retained
698 .checked_add(backend_peak)
699 .ok_or_else(|| build_failed(BlobBuildError::Work(WorkError::Overflow), work))?;
700 combined.peak_allocation_bytes = combined.peak_allocation_bytes.max(simultaneous);
701 build_verify(combined, budget)?;
702 Ok(combined)
703}
704
705const BLOB_BATCH_OBJECTS: usize = 4;
706const BLOB_BATCH_BYTES: u64 = 4 * 1024 * 1024;
707
708struct BlobBatchState {
709 writes: Vec<crate::storage::ObjectWrite>,
710 bytes: u64,
711}
712
713struct BlobBatchStore<'a, S> {
714 inner: &'a S,
715 state: BlobBatchState,
716 vector_bytes: u64,
717}
718
719impl<'a, S: AsyncObjectStore> BlobBatchStore<'a, S> {
720 fn new(inner: &'a S, budget: WorkBudget) -> Result<(Self, WorkCounters), BlobBuildFailure> {
721 let minimum = WorkCounters {
722 allocation_operations: 1,
723 peak_allocation_bytes: u64::try_from(BLOB_BATCH_OBJECTS)
724 .unwrap_or(u64::MAX)
725 .saturating_mul(
726 u64::try_from(size_of::<crate::storage::ObjectWrite>()).unwrap_or(u64::MAX),
727 ),
728 ..WorkCounters::default()
729 };
730 minimum
731 .verify(budget)
732 .map_err(|error| build_failed(BlobBuildError::Work(error), WorkCounters::default()))?;
733 let mut writes = Vec::new();
734 writes
735 .try_reserve_exact(BLOB_BATCH_OBJECTS)
736 .map_err(|_| build_failed(BlobBuildError::AllocationFailed, WorkCounters::default()))?;
737 let vector_bytes = u64::try_from(writes.capacity())
738 .unwrap_or(u64::MAX)
739 .saturating_mul(
740 u64::try_from(size_of::<crate::storage::ObjectWrite>()).unwrap_or(u64::MAX),
741 );
742 let admitted = WorkCounters {
743 allocation_operations: 1,
744 peak_allocation_bytes: vector_bytes,
745 ..WorkCounters::default()
746 };
747 admitted
748 .verify(budget)
749 .map_err(|error| build_failed(BlobBuildError::Work(error), admitted))?;
750 Ok((
751 Self {
752 inner,
753 state: BlobBatchState { writes, bytes: 0 },
754 vector_bytes,
755 },
756 admitted,
757 ))
758 }
759
760 fn retained_bytes(&self) -> u64 {
761 self.vector_bytes.saturating_add(self.state.bytes)
762 }
763
764 async fn flush_locked(
765 &mut self,
766 backend_budget: WorkBudget,
769 cancellation: &CancellationToken,
770 ) -> crate::storage::ObjectResult<()> {
771 if self.state.writes.is_empty() {
772 return Ok(crate::storage::ObjectReceipt {
773 value: (),
774 work: WorkCounters::default(),
775 });
776 }
777 let receipt = self
778 .inner
779 .put_many(&self.state.writes, backend_budget, cancellation)
780 .await?;
781 self.state.writes.clear();
782 self.state.bytes = 0;
783 Ok(crate::storage::ObjectReceipt {
784 value: (),
785 work: receipt.work,
786 })
787 }
788
789 async fn finish(
790 &mut self,
791 budget: WorkBudget,
792 cancellation: &CancellationToken,
793 ) -> crate::storage::ObjectResult<()> {
794 self.flush_locked(budget, cancellation).await
795 }
796
797 async fn put_with_retained(
798 &mut self,
799 object_id: ObjectId,
800 bytes: Bytes,
801 retained_bytes: u64,
802 budget: WorkBudget,
803 cancellation: &CancellationToken,
804 ) -> crate::storage::ObjectResult<()> {
805 cancellation.check().map_err(|_| {
806 crate::storage::ObjectFailure::before_work(crate::storage::ObjectStoreError::Cancelled)
807 })?;
808 self.state.bytes = self
809 .state
810 .bytes
811 .checked_add(retained_bytes)
812 .ok_or_else(|| {
813 crate::storage::ObjectFailure::before_work(
814 crate::storage::ObjectStoreError::Rejected(
815 "object write batch length overflowed".to_owned(),
816 ),
817 )
818 })?;
819 self.state
820 .writes
821 .push(crate::storage::ObjectWrite { object_id, bytes });
822 if self.state.writes.len() >= BLOB_BATCH_OBJECTS || self.state.bytes >= BLOB_BATCH_BYTES {
823 return self.flush_locked(budget, cancellation).await;
824 }
825 Ok(crate::storage::ObjectReceipt {
826 value: (),
827 work: WorkCounters::default(),
828 })
829 }
830}
831
832fn allocate_chunk_buffer(
833 allocation: usize,
834 index_live_bytes: u64,
835 retained_batch_bytes: u64,
836 work: WorkCounters,
837 budget: WorkBudget,
838) -> Result<(Vec<u8>, WorkCounters, u64, u64), BlobBuildFailure> {
839 let allocation_bytes = u64::try_from(allocation).unwrap_or(u64::MAX);
840 let simultaneous = index_live_bytes
841 .checked_add(allocation_bytes)
842 .and_then(|bytes| bytes.checked_add(retained_batch_bytes))
843 .ok_or_else(|| build_failed(BlobBuildError::TooLarge, work))?;
844 let mut prospective = build_add(
845 work,
846 WorkCounters {
847 allocation_operations: 1,
848 ..WorkCounters::default()
849 },
850 )?;
851 prospective.peak_allocation_bytes = prospective.peak_allocation_bytes.max(simultaneous);
852 build_verify(prospective, budget)?;
853 let mut bytes = Vec::new();
854 if bytes.try_reserve_exact(allocation).is_err() {
855 return Err(build_failed(BlobBuildError::AllocationFailed, work));
856 }
857 bytes.resize(allocation, 0);
858 let retained_capacity = u64::try_from(bytes.capacity()).unwrap_or(u64::MAX);
859 let actual_simultaneous = index_live_bytes
860 .checked_add(retained_capacity)
861 .and_then(|value| value.checked_add(retained_batch_bytes))
862 .ok_or_else(|| build_failed(BlobBuildError::TooLarge, prospective))?;
863 prospective.peak_allocation_bytes = prospective.peak_allocation_bytes.max(actual_simultaneous);
864 build_verify(prospective, budget)?;
865 Ok((bytes, prospective, actual_simultaneous, retained_capacity))
866}
867
868struct BlobIndexBuilder {
869 options: BlobBuildOptions,
870 width: usize,
871 leaf: Vec<BlobChunkRef>,
872 levels: Vec<Vec<BlobChild>>,
873 live_allocation_bytes: u64,
874}
875
876impl BlobIndexBuilder {
877 fn new(options: BlobBuildOptions) -> Self {
878 Self {
879 options,
880 width: effective_page_width(options).unwrap_or(0),
881 leaf: Vec::new(),
882 levels: Vec::new(),
883 live_allocation_bytes: 0,
884 }
885 }
886
887 async fn push_chunk<S: AsyncObjectStore>(
888 &mut self,
889 store: &mut BlobBatchStore<'_, S>,
890 chunk: BlobChunkRef,
891 budget: WorkBudget,
892 work: &mut WorkCounters,
893 cancellation: &CancellationToken,
894 ) -> Result<(), BlobBuildFailure> {
895 reserve_page_items(
896 &mut self.leaf,
897 self.width,
898 &mut self.live_allocation_bytes,
899 work,
900 budget,
901 )?;
902 self.leaf.push(chunk);
903 if self.leaf.len() == self.width {
904 self.flush_leaf(store, budget, work, cancellation).await?;
905 }
906 Ok(())
907 }
908
909 async fn flush_leaf<S: AsyncObjectStore>(
910 &mut self,
911 store: &mut BlobBatchStore<'_, S>,
912 budget: WorkBudget,
913 work: &mut WorkCounters,
914 cancellation: &CancellationToken,
915 ) -> Result<(), BlobBuildFailure> {
916 let mut items = std::mem::take(&mut self.leaf);
917 if items.is_empty() {
918 return Ok(());
919 }
920 let (first_offset, end_offset) = match items.as_slice() {
921 [only] => (only.first_offset, only.end_offset),
922 [first, .., last] => (first.first_offset, last.end_offset),
923 [] => unreachable!("checked items.is_empty() above"),
924 };
925 let page = BlobPage {
926 first_offset,
927 end_offset,
928 node: BlobNode::Leaf(items),
929 };
930 let page_id = put_blob_page(
931 store,
932 &page,
933 self.options,
934 self.live_allocation_bytes,
935 budget,
936 work,
937 cancellation,
938 )
939 .await?;
940 let child = BlobChild {
941 first_offset: page.first_offset,
942 end_offset: page.end_offset,
943 page: page_id,
944 };
945 let BlobNode::Leaf(reusable) = page.node else {
946 unreachable!();
947 };
948 items = reusable;
949 items.clear();
950 self.leaf = items;
951 self.push_child(store, 0, child, budget, work, cancellation)
952 .await
953 }
954
955 #[allow(
956 clippy::indexing_slicing,
957 reason = "`self.ensure_level(level, ...)` runs immediately above every use of \
958 `self.levels[level]` in this loop iteration; it loops `while self.levels.len() \
959 <= level` pushing new levels, so afterwards `level < self.levels.len()` and \
960 `self.levels` is not resized again before this iteration's accesses"
961 )]
962 async fn push_child<S: AsyncObjectStore>(
963 &mut self,
964 store: &mut BlobBatchStore<'_, S>,
965 mut level: usize,
966 mut child: BlobChild,
967 budget: WorkBudget,
968 work: &mut WorkCounters,
969 cancellation: &CancellationToken,
970 ) -> Result<(), BlobBuildFailure> {
971 loop {
972 self.ensure_level(level, work, budget)?;
973 reserve_page_items(
974 &mut self.levels[level],
975 self.width,
976 &mut self.live_allocation_bytes,
977 work,
978 budget,
979 )?;
980 self.levels[level].push(child);
981 if self.levels[level].len() < self.width {
982 return Ok(());
983 }
984 child = self
985 .flush_internal(store, level, budget, work, cancellation)
986 .await?;
987 level = level
988 .checked_add(1)
989 .ok_or_else(|| build_failed(BlobBuildError::TooLarge, *work))?;
990 }
991 }
992
993 #[allow(
994 clippy::indexing_slicing,
995 reason = "`flush_internal` is only called from `push_child` (right after \
996 `self.ensure_level(level, ...)` established `level < self.levels.len()`, with \
997 no intervening resize) and from `finish` (with `level` taken from \
998 `self.levels.iter().position(...)`, which is inherently `< self.levels.len()`)"
999 )]
1000 async fn flush_internal<S: AsyncObjectStore>(
1001 &mut self,
1002 store: &mut BlobBatchStore<'_, S>,
1003 level: usize,
1004 budget: WorkBudget,
1005 work: &mut WorkCounters,
1006 cancellation: &CancellationToken,
1007 ) -> Result<BlobChild, BlobBuildFailure> {
1008 let items = std::mem::take(&mut self.levels[level]);
1009 let (first_offset, end_offset) = match items.as_slice() {
1010 [only] => (only.first_offset, only.end_offset),
1011 [first, .., last] => (first.first_offset, last.end_offset),
1012 [] => unreachable!(
1013 "flush_internal is only called once self.levels[level] is non-empty: from \
1014 push_child at self.width (>= 2) items, or from finish() for a level chosen via \
1015 self.levels.iter().position(|items| !items.is_empty())"
1016 ),
1017 };
1018 let page = BlobPage {
1019 first_offset,
1020 end_offset,
1021 node: BlobNode::Internal(items),
1022 };
1023 let page_id = put_blob_page(
1024 store,
1025 &page,
1026 self.options,
1027 self.live_allocation_bytes,
1028 budget,
1029 work,
1030 cancellation,
1031 )
1032 .await?;
1033 let child = BlobChild {
1034 first_offset: page.first_offset,
1035 end_offset: page.end_offset,
1036 page: page_id,
1037 };
1038 let BlobNode::Internal(mut reusable) = page.node else {
1039 unreachable!();
1040 };
1041 reusable.clear();
1042 self.levels[level] = reusable;
1043 Ok(child)
1044 }
1045
1046 fn ensure_level(
1047 &mut self,
1048 level: usize,
1049 work: &mut WorkCounters,
1050 budget: WorkBudget,
1051 ) -> Result<(), BlobBuildFailure> {
1052 while self.levels.len() <= level {
1053 let bytes = u64::try_from(size_of::<Vec<BlobChild>>()).unwrap_or(u64::MAX);
1054 let next_live = self
1055 .live_allocation_bytes
1056 .checked_add(bytes)
1057 .ok_or_else(|| build_failed(BlobBuildError::TooLarge, *work))?;
1058 let mut prospective = work
1059 .checked_add(WorkCounters {
1060 allocation_operations: 1,
1061 ..WorkCounters::default()
1062 })
1063 .map_err(|error| build_failed(error.into(), *work))?;
1064 prospective.peak_allocation_bytes = prospective.peak_allocation_bytes.max(next_live);
1065 build_verify(prospective, budget)?;
1066 if self.levels.try_reserve_exact(1).is_err() {
1067 return Err(build_failed(BlobBuildError::AllocationFailed, *work));
1068 }
1069 self.levels.push(Vec::new());
1070 self.live_allocation_bytes = next_live;
1071 *work = prospective;
1072 }
1073 Ok(())
1074 }
1075
1076 async fn finish<S: AsyncObjectStore>(
1077 mut self,
1078 store: &mut BlobBatchStore<'_, S>,
1079 budget: WorkBudget,
1080 work: &mut WorkCounters,
1081 cancellation: &CancellationToken,
1082 ) -> Result<ObjectId, BlobBuildFailure> {
1083 if self.leaf.is_empty() && self.levels.is_empty() {
1084 return put_blob_page(
1085 store,
1086 &BlobPage {
1087 first_offset: 0,
1088 end_offset: 0,
1089 node: BlobNode::Leaf(Vec::new()),
1090 },
1091 self.options,
1092 self.live_allocation_bytes,
1093 budget,
1094 work,
1095 cancellation,
1096 )
1097 .await;
1098 }
1099 self.flush_leaf(store, budget, work, cancellation).await?;
1100 loop {
1101 let Some(level) = self.levels.iter().position(|items| !items.is_empty()) else {
1102 return Err(build_failed(BlobBuildError::InvalidIndexState, *work));
1103 };
1104 #[allow(
1105 clippy::indexing_slicing,
1106 reason = "`level` comes from `self.levels.iter().position(...)` above, so `level \
1107 < self.levels.len()` and `level + 1 <= self.levels.len()`, which is \
1108 in-bounds for a `RangeFrom` slice"
1109 )]
1110 let higher_is_empty = self.levels[level + 1..].iter().all(Vec::is_empty);
1111 #[allow(
1112 clippy::indexing_slicing,
1113 reason = "`level < self.levels.len()` from `position(...)` above; the \
1114 `levels[level][0]` access is additionally guarded by the \
1115 `levels[level].len() == 1` check in this same condition"
1116 )]
1117 if higher_is_empty && self.levels[level].len() == 1 {
1118 return Ok(self.levels[level][0].page);
1119 }
1120 let child = self
1121 .flush_internal(store, level, budget, work, cancellation)
1122 .await?;
1123 self.push_child(store, level + 1, child, budget, work, cancellation)
1124 .await?;
1125 }
1126 }
1127}
1128
1129fn effective_page_width(options: BlobBuildOptions) -> Option<usize> {
1130 let item_bound = usize::try_from(options.page_items).ok()?;
1131 let byte_bound = usize::try_from(options.page_bytes)
1132 .ok()?
1133 .checked_sub(BLOB_PAGE_HEADER_BYTES)?
1134 / BLOB_PAGE_ITEM_BYTES;
1135 let width = item_bound.min(byte_bound);
1136 (width >= 2).then_some(width)
1137}
1138
1139fn reserve_page_items<T>(
1140 items: &mut Vec<T>,
1141 width: usize,
1142 live_allocation_bytes: &mut u64,
1143 work: &mut WorkCounters,
1144 budget: WorkBudget,
1145) -> Result<(), BlobBuildFailure> {
1146 if items.capacity() >= width {
1147 return Ok(());
1148 }
1149 let bytes = width
1150 .checked_mul(size_of::<T>())
1151 .map(crate::foundation::usize_to_u64)
1152 .ok_or_else(|| build_failed(BlobBuildError::TooLarge, *work))?;
1153 let next_live = live_allocation_bytes
1154 .checked_add(bytes)
1155 .ok_or_else(|| build_failed(BlobBuildError::TooLarge, *work))?;
1156 let mut prospective = work
1157 .checked_add(WorkCounters {
1158 allocation_operations: 1,
1159 ..WorkCounters::default()
1160 })
1161 .map_err(|error| build_failed(error.into(), *work))?;
1162 prospective.peak_allocation_bytes = prospective.peak_allocation_bytes.max(next_live);
1163 build_verify(prospective, budget)?;
1164 if items.try_reserve_exact(width - items.capacity()).is_err() {
1165 return Err(build_failed(BlobBuildError::AllocationFailed, *work));
1166 }
1167 *live_allocation_bytes = next_live;
1168 *work = prospective;
1169 Ok(())
1170}
1171
1172async fn put_blob_page<S: AsyncObjectStore>(
1173 store: &mut BlobBatchStore<'_, S>,
1174 page: &BlobPage,
1175 options: BlobBuildOptions,
1176 live_allocation_bytes: u64,
1177 budget: WorkBudget,
1178 work: &mut WorkCounters,
1179 cancellation: &CancellationToken,
1180) -> Result<ObjectId, BlobBuildFailure> {
1181 let retained = store.retained_bytes();
1182 let encoded = encode_blob_page(page, options.page_items)
1183 .map_err(|error| build_failed(BlobBuildError::Encode(error), *work))?;
1184 if encoded.len() > usize::try_from(options.page_bytes).unwrap_or(usize::MAX) {
1185 return Err(build_failed(
1186 BlobBuildError::PageTooLarge {
1187 observed: u64::try_from(encoded.len()).unwrap_or(u64::MAX),
1188 maximum: u64::from(options.page_bytes),
1189 },
1190 *work,
1191 ));
1192 }
1193 let page_id = ObjectId {
1194 kind: ObjectKind::Blob,
1195 digest: object_digest(ObjectKind::Blob, &encoded),
1196 };
1197 let encoded_bytes = u64::try_from(encoded.capacity()).unwrap_or(u64::MAX);
1198 let simultaneous = live_allocation_bytes
1199 .checked_add(encoded_bytes)
1200 .and_then(|bytes| bytes.checked_add(retained))
1201 .ok_or_else(|| build_failed(BlobBuildError::TooLarge, *work))?;
1202 let mut encoded_work = build_add(
1203 *work,
1204 WorkCounters {
1205 page_writes: 1,
1206 bytes_encoded: u64::try_from(encoded.len()).unwrap_or(u64::MAX),
1207 allocation_operations: 1,
1208 ..WorkCounters::default()
1209 },
1210 )?;
1211 encoded_work.peak_allocation_bytes = encoded_work.peak_allocation_bytes.max(simultaneous);
1212 build_verify(encoded_work, budget)?;
1213 *work = build_put(
1214 store,
1215 page_id,
1216 Bytes::from(encoded),
1217 PutAllocation {
1218 retained: encoded_bytes,
1219 live: simultaneous,
1220 },
1221 budget,
1222 encoded_work,
1223 cancellation,
1224 )
1225 .await?;
1226 Ok(page_id)
1227}
1228
1229#[derive(Clone, Copy)]
1230struct PutAllocation {
1231 retained: u64,
1232 live: u64,
1233}
1234
1235async fn build_put<S: AsyncObjectStore>(
1236 store: &mut BlobBatchStore<'_, S>,
1237 object: ObjectId,
1238 bytes: Bytes,
1239 allocation: PutAllocation,
1240 budget: WorkBudget,
1241 work: WorkCounters,
1242 cancellation: &CancellationToken,
1243) -> Result<WorkCounters, BlobBuildFailure> {
1244 let mut remaining = work
1245 .remaining(budget)
1246 .map_err(|error| build_failed(BlobBuildError::Work(error), work))?;
1247 remaining.peak_allocation_bytes = budget
1248 .peak_allocation_bytes
1249 .checked_sub(allocation.live)
1250 .ok_or_else(|| build_failed(BlobBuildError::Work(WorkError::Overflow), work))?;
1251 match store
1252 .put_with_retained(object, bytes, allocation.retained, remaining, cancellation)
1253 .await
1254 {
1255 Ok(receipt) => {
1256 let backend_peak = allocation
1257 .live
1258 .checked_add(receipt.work.peak_allocation_bytes)
1259 .ok_or_else(|| build_failed(BlobBuildError::Work(WorkError::Overflow), work))?;
1260 let mut backend_work = receipt.work;
1261 backend_work.peak_allocation_bytes = 0;
1262 let mut combined = build_add(work, backend_work)?;
1263 combined.peak_allocation_bytes = combined.peak_allocation_bytes.max(backend_peak);
1264 build_verify(combined, budget)?;
1265 Ok(combined)
1266 }
1267 Err(failure) => Err(failure.map_with_prior_work(work, BlobBuildError::Storage)),
1268 }
1269}
1270
1271fn build_add(left: WorkCounters, right: WorkCounters) -> Result<WorkCounters, BlobBuildFailure> {
1272 left.checked_add(right)
1273 .map_err(|error| build_failed(BlobBuildError::Work(error), left))
1274}
1275
1276fn build_verify(work: WorkCounters, budget: WorkBudget) -> Result<(), BlobBuildFailure> {
1277 work.verify(budget)
1278 .map_err(|error| build_failed(BlobBuildError::Work(error), work))
1279}
1280
1281pub type BlobBuildFailure = OperationFailure<BlobBuildError>;
1283
1284#[derive(Debug, Error)]
1286pub enum BlobBuildError {
1287 #[error("blob build was cancelled")]
1289 Cancelled,
1290 #[error("blob build options are invalid")]
1292 InvalidOptions,
1293 #[error("blob index page has {observed} bytes; maximum is {maximum}")]
1295 PageTooLarge {
1296 observed: u64,
1298 maximum: u64,
1300 },
1301 #[error("blob build scratch allocation failed")]
1303 AllocationFailed,
1304 #[error("blob build index state is invalid")]
1306 InvalidIndexState,
1307 #[error("blob source exceeds its admitted logical size")]
1309 TooLarge,
1310 #[error("blob source failed: {0}")]
1312 Source(#[source] std::io::Error),
1313 #[error(transparent)]
1315 Encode(#[from] CanonicalDecodeError),
1316 #[error(transparent)]
1318 Storage(#[from] ObjectStoreError),
1319 #[error(transparent)]
1321 Work(#[from] WorkError),
1322}
1323
1324fn build_failed(error: BlobBuildError, work: WorkCounters) -> BlobBuildFailure {
1325 OperationFailure::new(error, work)
1326}
1327
1328#[derive(Clone, Debug, Eq, PartialEq)]
1330pub struct BlobRead {
1331 pub bytes: Bytes,
1333 pub work: WorkCounters,
1335 pub next_residency: Option<ResidencyHint>,
1337}
1338
1339pub fn read_blob_range<S: ObjectStore>(
1346 store: &S,
1347 root: ObjectId,
1348 range: ByteRange,
1349 limits: DecodeLimits,
1350 budget: WorkBudget,
1351) -> Result<BlobRead, BlobReadFailure> {
1352 let mut machine = BlobRangeMachine::new(root, range, limits, budget)?;
1353 frontier::drive_sync(store, &mut machine)
1354}
1355
1356pub async fn read_blob_range_async<S: AsyncObjectStore>(
1364 store: &S,
1365 root: ObjectId,
1366 range: ByteRange,
1367 limits: DecodeLimits,
1368 budget: WorkBudget,
1369 cancellation: &CancellationToken,
1370) -> Result<BlobRead, BlobReadFailure> {
1371 if cancellation.is_cancelled() {
1372 return Err(failed(BlobReadError::Cancelled, WorkCounters::default()));
1373 }
1374 let mut machine = BlobRangeMachine::new(root, range, limits, budget)?;
1375 drive_blob_async(store, &mut machine, cancellation).await
1376}
1377
1378async fn drive_blob_async<S: AsyncObjectStore>(
1379 store: &S,
1380 machine: &mut BlobRangeMachine,
1381 cancellation: &CancellationToken,
1382) -> Result<BlobRead, BlobReadFailure> {
1383 loop {
1384 if cancellation.is_cancelled() {
1385 return Err(machine.cancelled());
1386 }
1387 if let Some(output) = machine.complete()? {
1388 return Ok(output);
1389 }
1390 if let Some(batch) = machine.prepare_chunk_batch()? {
1391 let receipt =
1392 AsyncObjectStore::read_many(store, &batch.requests, batch.remaining, cancellation)
1393 .await
1394 .map_err(|failure| machine.batch_storage_failure(batch.prospective, failure))?;
1395 machine.accept_chunk_batch(batch, receipt)?;
1396 continue;
1397 }
1398 let request = machine.prepare_read()?;
1399 let key = (request.page.kind == ObjectKind::Blob)
1400 .then(|| DecodedCacheKey::new::<BlobPage>(request.page, machine.limits));
1401 if let Some(key) = key {
1402 let cached = store
1403 .decoded_cache_get(key)
1404 .map_err(|error| failed(error.into(), machine.work))?;
1405 if let Some(cached) = cached {
1406 machine.accept_cached_page(cached)?;
1407 continue;
1408 }
1409 }
1410 let receipt = AsyncObjectStore::read(
1411 store,
1412 request.page,
1413 request.maximum_bytes,
1414 request.remaining,
1415 cancellation,
1416 )
1417 .await
1418 .map_err(|failure| machine.storage_failure(request.prospective, failure))?;
1419 machine.accept_read_with_cache(store, key, request.prospective, &receipt)?;
1420 }
1421}
1422
1423struct PendingChunkBatch {
1424 chunks: Vec<BlobChunkRef>,
1425 requests: Vec<ObjectReadRequest>,
1426 scratch_bytes: u64,
1427 remaining: WorkBudget,
1428 prospective: WorkCounters,
1429}
1430
1431#[derive(Clone, Copy)]
1432enum PendingBlobRead {
1433 Page {
1434 page: ObjectId,
1435 expected_first: u64,
1436 expected_end: Option<u64>,
1437 height: u16,
1438 },
1439 Chunk(BlobChunkRef),
1440}
1441
1442struct BlobRangeMachine {
1443 range: ByteRange,
1444 range_end: u64,
1445 limits: DecodeLimits,
1446 budget: WorkBudget,
1447 allocation: usize,
1448 maximum_pending: usize,
1449 pending: Vec<PendingBlobRead>,
1450 pending_capacity: LogicalVecCapacity,
1451 awaiting: Option<PendingBlobRead>,
1452 visited: VisitedObjectSet,
1453 output: Vec<u8>,
1454 work: WorkCounters,
1455 allocations: AllocationLedger,
1456 successor: Option<(u64, ResidencyHint)>,
1457}
1458
1459impl BlobRangeMachine {
1460 fn new(
1461 root: ObjectId,
1462 range: ByteRange,
1463 limits: DecodeLimits,
1464 budget: WorkBudget,
1465 ) -> Result<Self, BlobReadFailure> {
1466 if root.kind != ObjectKind::Blob {
1467 return Err(failed(
1468 BlobReadError::WrongRootKind,
1469 WorkCounters::default(),
1470 ));
1471 }
1472 let range_end = range
1473 .offset
1474 .checked_add(range.length)
1475 .ok_or_else(|| failed(BlobReadError::InvalidRange, WorkCounters::default()))?;
1476 let allocation = usize::try_from(range.length)
1477 .map_err(|_| failed(BlobReadError::InvalidRange, WorkCounters::default()))?;
1478 if !limits.page_limits_valid(1) {
1479 return Err(failed(
1480 BlobReadError::InvalidLimits,
1481 WorkCounters::default(),
1482 ));
1483 }
1484 let maximum_visited = usize::try_from(limits.maximum_visited_pages)
1485 .map_err(|_| failed(BlobReadError::InvalidLimits, WorkCounters::default()))?;
1486 let maximum_page_items = usize::try_from(limits.maximum_page_items)
1487 .map_err(|_| failed(BlobReadError::InvalidLimits, WorkCounters::default()))?;
1488 let maximum_pending = maximum_visited
1489 .checked_add(maximum_page_items)
1490 .ok_or_else(|| failed(BlobReadError::InvalidLimits, WorkCounters::default()))?;
1491 let mut allocations = AllocationLedger::default();
1492 let mut work = WorkCounters::default();
1493 allocations
1494 .claim_bytes(range.length, u64::from(allocation != 0), &mut work, budget)
1495 .map_err(|error| failed(error.into(), work))?;
1496 let visited = VisitedObjectSet::new(maximum_visited, &mut allocations, &mut work, budget)
1497 .map_err(|error| failed(error.into(), work))?;
1498 let mut pending = Vec::new();
1499 let mut pending_capacity = LogicalVecCapacity::default();
1500 pending_capacity
1501 .ensure_for_push(
1502 &mut pending,
1503 maximum_pending,
1504 &mut allocations,
1505 &mut work,
1506 budget,
1507 )
1508 .map_err(|error| failed(error.into(), work))?;
1509 pending.push(PendingBlobRead::Page {
1510 page: root,
1511 expected_first: 0,
1512 expected_end: None,
1513 height: 1,
1514 });
1515 let mut output = Vec::new();
1516 if output.try_reserve_exact(allocation).is_err() {
1517 return Err(failed(BlobReadError::AllocationFailed, work));
1518 }
1519 Ok(Self {
1520 range,
1521 range_end,
1522 limits,
1523 budget,
1524 allocation,
1525 maximum_pending,
1526 pending,
1527 pending_capacity,
1528 awaiting: None,
1529 visited,
1530 output,
1531 work,
1532 allocations,
1533 successor: None,
1534 })
1535 }
1536
1537 fn finish(&mut self) -> Result<BlobRead, BlobReadFailure> {
1538 if self.output.len() != self.allocation {
1539 return Err(failed(BlobReadError::IncompleteCoverage, self.work));
1540 }
1541 Ok(BlobRead {
1542 bytes: Bytes::from(std::mem::take(&mut self.output)),
1543 work: self.work,
1544 next_residency: self.successor.map(|(_, hint)| hint),
1545 })
1546 }
1547
1548 fn prepare_read(&mut self) -> Result<frontier::ReadRequest, BlobReadFailure> {
1549 let pending = self
1550 .pending
1551 .pop()
1552 .ok_or_else(|| failed(BlobReadError::TraversalState, self.work))?;
1553 let (object, maximum_bytes) = match pending {
1554 PendingBlobRead::Page { page, height, .. } => {
1555 if height > self.limits.maximum_page_height {
1556 return Err(failed(BlobReadError::CycleOrHeight, self.work));
1557 }
1558 let visited = self
1559 .visited
1560 .insert(page, &mut self.allocations, &mut self.work, self.budget)
1561 .map_err(|error| failed(error.into(), self.work))?;
1562 if !visited.inserted {
1563 return Err(failed(BlobReadError::CycleOrHeight, self.work));
1564 }
1565 (page, self.limits.maximum_page_object_bytes())
1566 }
1567 PendingBlobRead::Chunk(chunk) => (chunk.chunk, chunk.end_offset - chunk.first_offset),
1568 };
1569 let mut remaining = self
1570 .work
1571 .remaining(self.budget)
1572 .map_err(|error| failed(error.into(), self.work))?;
1573 remaining.peak_allocation_bytes = self
1574 .budget
1575 .peak_allocation_bytes
1576 .checked_sub(self.allocations.live_bytes())
1577 .ok_or_else(|| failed(BlobReadError::Work(WorkError::Overflow), self.work))?;
1578 self.awaiting = Some(pending);
1579 Ok(frontier::ReadRequest {
1580 page: object,
1581 maximum_bytes,
1582 remaining,
1583 prospective: self.work,
1584 })
1585 }
1586
1587 fn prepare_chunk_batch(&mut self) -> Result<Option<PendingChunkBatch>, BlobReadFailure> {
1588 let chunk_count = self
1589 .pending
1590 .iter()
1591 .rev()
1592 .take_while(|pending| matches!(pending, PendingBlobRead::Chunk(_)))
1593 .count();
1594 if chunk_count < 2 {
1595 return Ok(None);
1596 }
1597 let scratch_bytes = u64::try_from(chunk_count)
1598 .unwrap_or(u64::MAX)
1599 .checked_mul(
1600 u64::try_from(size_of::<BlobChunkRef>() + size_of::<ObjectReadRequest>())
1601 .unwrap_or(u64::MAX),
1602 )
1603 .ok_or_else(|| failed(BlobReadError::Work(WorkError::Overflow), self.work))?;
1604 let live_bytes = self
1605 .allocations
1606 .live_bytes()
1607 .checked_add(scratch_bytes)
1608 .ok_or_else(|| failed(BlobReadError::Work(WorkError::Overflow), self.work))?;
1609 let mut prospective = self.work;
1610 prospective.allocation_operations = prospective
1611 .allocation_operations
1612 .checked_add(2)
1613 .ok_or_else(|| failed(BlobReadError::Work(WorkError::Overflow), self.work))?;
1614 prospective.peak_allocation_bytes = prospective.peak_allocation_bytes.max(live_bytes);
1615 verify_work(prospective, self.budget)?;
1616 let mut chunks = Vec::new();
1617 let mut requests = Vec::new();
1618 chunks
1619 .try_reserve_exact(chunk_count)
1620 .map_err(|_| failed(BlobReadError::AllocationFailed, self.work))?;
1621 requests
1622 .try_reserve_exact(chunk_count)
1623 .map_err(|_| failed(BlobReadError::AllocationFailed, self.work))?;
1624 for pending in self.pending.iter().rev().take(chunk_count) {
1625 let PendingBlobRead::Chunk(chunk) = pending else {
1626 return Err(failed(BlobReadError::TraversalState, self.work));
1627 };
1628 requests.push(ObjectReadRequest {
1629 object_id: chunk.chunk,
1630 maximum_bytes: chunk.end_offset - chunk.first_offset,
1631 });
1632 chunks.push(*chunk);
1633 }
1634 let mut remaining = prospective
1635 .remaining(self.budget)
1636 .map_err(|error| failed(error.into(), prospective))?;
1637 remaining.peak_allocation_bytes = self
1638 .budget
1639 .peak_allocation_bytes
1640 .checked_sub(live_bytes)
1641 .ok_or_else(|| failed(BlobReadError::Work(WorkError::Overflow), prospective))?;
1642 Ok(Some(PendingChunkBatch {
1643 chunks,
1644 requests,
1645 scratch_bytes,
1646 remaining,
1647 prospective,
1648 }))
1649 }
1650
1651 fn accept_chunk_batch(
1652 &mut self,
1653 batch: PendingChunkBatch,
1654 receipt: ObjectReceipt<Vec<ObjectRead>>,
1655 ) -> Result<(), BlobReadFailure> {
1656 if receipt.value.len() != batch.chunks.len() {
1657 return Err(failed(BlobReadError::TraversalState, batch.prospective));
1658 }
1659 let retained = self
1660 .pending
1661 .len()
1662 .checked_sub(batch.chunks.len())
1663 .ok_or_else(|| failed(BlobReadError::TraversalState, batch.prospective))?;
1664 if self.pending.get(retained..).is_none_or(|pending| {
1665 !pending
1666 .iter()
1667 .rev()
1668 .zip(&batch.chunks)
1669 .all(|(pending, chunk)| matches!(pending, PendingBlobRead::Chunk(found) if found == chunk))
1670 }) {
1671 return Err(failed(BlobReadError::TraversalState, batch.prospective));
1672 }
1673 self.pending.truncate(retained);
1674 self.work = merge_blob_backend_work(
1675 batch.prospective,
1676 receipt.work,
1677 self.allocations.live_bytes(),
1678 )
1679 .map_err(|error| failed(error.into(), batch.prospective))?;
1680 let retained_bytes = receipt.value.iter().try_fold(0_u64, |total, value| {
1681 total.checked_add(match value.retention {
1682 ObjectReadRetention::Shared => 0,
1683 ObjectReadRetention::Owned { logical_bytes } => logical_bytes,
1684 })
1685 });
1686 let retained_bytes = retained_bytes
1687 .ok_or_else(|| failed(BlobReadError::Work(WorkError::Overflow), self.work))?;
1688 let simultaneous = self
1689 .allocations
1690 .live_bytes()
1691 .checked_add(batch.scratch_bytes)
1692 .and_then(|bytes| bytes.checked_add(retained_bytes))
1693 .ok_or_else(|| failed(BlobReadError::Work(WorkError::Overflow), self.work))?;
1694 self.work.peak_allocation_bytes = self.work.peak_allocation_bytes.max(simultaneous);
1695 verify_work(self.work, self.budget)?;
1696 for (chunk, value) in batch.chunks.into_iter().zip(receipt.value) {
1697 self.accept_chunk(
1698 &ObjectReceipt {
1699 value,
1700 work: WorkCounters::default(),
1701 },
1702 chunk,
1703 )?;
1704 }
1705 Ok(())
1706 }
1707
1708 fn batch_storage_failure(
1709 &self,
1710 prospective: WorkCounters,
1711 failure: crate::storage::ObjectFailure,
1712 ) -> BlobReadFailure {
1713 match merge_blob_backend_work(prospective, *failure.work, self.allocations.live_bytes()) {
1714 Ok(combined) => failed(failure.error.into(), combined),
1715 Err(error) => failed(error.into(), prospective),
1716 }
1717 }
1718
1719 fn accept_read(
1720 &mut self,
1721 prospective: WorkCounters,
1722 receipt: &ObjectReceipt<ObjectRead>,
1723 ) -> Result<Option<BlobRead>, BlobReadFailure> {
1724 let pending = self
1725 .awaiting
1726 .take()
1727 .ok_or_else(|| failed(BlobReadError::TraversalState, self.work))?;
1728 self.work =
1729 merge_blob_backend_work(prospective, receipt.work, self.allocations.live_bytes())
1730 .map_err(|error| failed(error.into(), prospective))?;
1731 match pending {
1732 PendingBlobRead::Page {
1733 expected_first,
1734 expected_end,
1735 height,
1736 ..
1737 } => self.accept_page(receipt, expected_first, expected_end, height)?,
1738 PendingBlobRead::Chunk(chunk) => self.accept_chunk(receipt, chunk)?,
1739 }
1740 Ok(None)
1741 }
1742
1743 fn accept_read_with_cache<S: AsyncObjectStore>(
1744 &mut self,
1745 store: &S,
1746 key: Option<DecodedCacheKey>,
1747 prospective: WorkCounters,
1748 receipt: &ObjectReceipt<ObjectRead>,
1749 ) -> Result<(), BlobReadFailure> {
1750 let pending = self
1751 .awaiting
1752 .take()
1753 .ok_or_else(|| failed(BlobReadError::TraversalState, self.work))?;
1754 self.work =
1755 merge_blob_backend_work(prospective, receipt.work, self.allocations.live_bytes())
1756 .map_err(|error| failed(error.into(), prospective))?;
1757 match pending {
1758 PendingBlobRead::Page {
1759 expected_first,
1760 expected_end,
1761 height,
1762 ..
1763 } => self.accept_page_with_cache(
1764 store,
1765 key.ok_or_else(|| failed(BlobReadError::TraversalState, self.work))?,
1766 receipt,
1767 expected_first,
1768 expected_end,
1769 height,
1770 ),
1771 PendingBlobRead::Chunk(chunk) => self.accept_chunk(receipt, chunk),
1772 }
1773 }
1774
1775 fn add_items(&mut self, count: u64) -> Result<(), BlobReadFailure> {
1776 self.work = add_work(
1777 self.work,
1778 WorkCounters {
1779 items_examined: count,
1780 ..WorkCounters::default()
1781 },
1782 )?;
1783 verify_work(self.work, self.budget)
1784 }
1785
1786 fn accept_page(
1787 &mut self,
1788 receipt: &ObjectReceipt<ObjectRead>,
1789 expected_first: u64,
1790 expected_end: Option<u64>,
1791 height: u16,
1792 ) -> Result<(), BlobReadFailure> {
1793 let (page, decoded_bytes, retained_bytes) = self.decode_page(receipt)?;
1794 let result = self.accept_decoded_page(&page, expected_first, expected_end, height);
1795 self.allocations
1796 .release(decoded_bytes)
1797 .map_err(|error| failed(error.into(), self.work))?;
1798 self.allocations
1799 .release(retained_bytes)
1800 .map_err(|error| failed(error.into(), self.work))?;
1801 result
1802 }
1803
1804 fn decode_page(
1805 &mut self,
1806 receipt: &ObjectReceipt<ObjectRead>,
1807 ) -> Result<(BlobPage, u64, u64), BlobReadFailure> {
1808 self.work = add_work(
1809 self.work,
1810 WorkCounters {
1811 page_reads: 1,
1812 ..WorkCounters::default()
1813 },
1814 )?;
1815 verify_work(self.work, self.budget)?;
1816 let retained_bytes = match receipt.value.retention {
1817 ObjectReadRetention::Shared => 0,
1818 ObjectReadRetention::Owned { logical_bytes } => logical_bytes,
1819 };
1820 self.allocations
1821 .claim_bytes(retained_bytes, 0, &mut self.work, self.budget)
1822 .map_err(|error| failed(error.into(), self.work))?;
1823 let shape = blob_page_decode_shape(&receipt.value, self.limits)
1824 .map_err(|error| failed(error.into(), self.work))?;
1825 self.add_items(u64::try_from(shape.items).unwrap_or(u64::MAX))?;
1826 let item_bytes = match shape.kind {
1827 DecodedPageKind::Leaf => size_of::<BlobChunkRef>(),
1828 DecodedPageKind::Internal => size_of::<BlobChild>(),
1829 };
1830 let decoded_bytes = shape
1831 .items
1832 .checked_mul(item_bytes)
1833 .map(crate::foundation::usize_to_u64)
1834 .ok_or_else(|| failed(BlobReadError::AllocationFailed, self.work))?;
1835 self.allocations
1836 .claim_bytes(
1837 decoded_bytes,
1838 u64::from(decoded_bytes != 0),
1839 &mut self.work,
1840 self.budget,
1841 )
1842 .map_err(|error| failed(error.into(), self.work))?;
1843 let page = decode_blob_page(&receipt.value, self.limits)
1844 .map_err(|error| failed(BlobReadError::Decode(error), self.work))?;
1845 Ok((page, decoded_bytes, retained_bytes))
1846 }
1847
1848 fn accept_cached_page(&mut self, cached: DecodedCacheValue) -> Result<(), BlobReadFailure> {
1849 let pending = self
1850 .awaiting
1851 .take()
1852 .ok_or_else(|| failed(BlobReadError::TraversalState, self.work))?;
1853 let PendingBlobRead::Page {
1854 expected_first,
1855 expected_end,
1856 height,
1857 ..
1858 } = pending
1859 else {
1860 return Err(failed(BlobReadError::TraversalState, self.work));
1861 };
1862 self.work = add_work(
1863 self.work,
1864 WorkCounters {
1865 page_reads: 1,
1866 ..WorkCounters::default()
1867 },
1868 )?;
1869 verify_work(self.work, self.budget)?;
1870 let page = cached
1871 .value
1872 .downcast::<BlobPage>()
1873 .map_err(|_| failed(ObjectStoreError::Corrupt.into(), self.work))?;
1874 self.accept_decoded_page(&page, expected_first, expected_end, height)
1875 }
1876
1877 fn accept_page_with_cache<S: AsyncObjectStore>(
1878 &mut self,
1879 store: &S,
1880 key: DecodedCacheKey,
1881 receipt: &ObjectReceipt<ObjectRead>,
1882 expected_first: u64,
1883 expected_end: Option<u64>,
1884 height: u16,
1885 ) -> Result<(), BlobReadFailure> {
1886 let (page, decoded_bytes, retained_bytes) = self.decode_page(receipt)?;
1887 let admitted = store.decoded_cache_admit(
1888 key,
1889 DecodedCacheValue {
1890 value: Arc::new(page),
1891 logical_bytes: decoded_bytes,
1892 },
1893 );
1894 let result = match admitted {
1895 Ok(DecodedCacheAdmission::Shared(value)) => {
1896 self.allocations
1897 .release(decoded_bytes)
1898 .map_err(|error| failed(error.into(), self.work))?;
1899 let page = value
1900 .value
1901 .downcast::<BlobPage>()
1902 .map_err(|_| failed(ObjectStoreError::Corrupt.into(), self.work))?;
1903 self.accept_decoded_page(&page, expected_first, expected_end, height)
1904 }
1905 Ok(DecodedCacheAdmission::Uncached(value)) => {
1906 let page = value
1907 .value
1908 .downcast::<BlobPage>()
1909 .map_err(|_| failed(ObjectStoreError::Corrupt.into(), self.work))?;
1910 let result = self.accept_decoded_page(&page, expected_first, expected_end, height);
1911 self.allocations
1912 .release(decoded_bytes)
1913 .map_err(|error| failed(error.into(), self.work))?;
1914 result
1915 }
1916 Err(error) => {
1917 self.allocations
1918 .release(decoded_bytes)
1919 .map_err(|release| failed(release.into(), self.work))?;
1920 Err(failed(error.into(), self.work))
1921 }
1922 };
1923 self.allocations
1924 .release(retained_bytes)
1925 .map_err(|error| failed(error.into(), self.work))?;
1926 result
1927 }
1928
1929 fn accept_decoded_page(
1930 &mut self,
1931 page: &BlobPage,
1932 expected_first: u64,
1933 expected_end: Option<u64>,
1934 height: u16,
1935 ) -> Result<(), BlobReadFailure> {
1936 if page.first_offset != expected_first
1937 || expected_end.is_some_and(|end| page.end_offset != end)
1938 {
1939 return Err(failed(BlobReadError::ChildBoundsMismatch, self.work));
1940 }
1941 if expected_end.is_none() && self.range_end > page.end_offset {
1942 return Err(failed(BlobReadError::InvalidRange, self.work));
1943 }
1944 match &page.node {
1945 BlobNode::Internal(children) => self.push_children(children, height)?,
1946 BlobNode::Leaf(chunks) => self.push_chunks(chunks)?,
1947 }
1948 Ok(())
1949 }
1950
1951 fn push_children(
1952 &mut self,
1953 children: &[BlobChild],
1954 height: u16,
1955 ) -> Result<(), BlobReadFailure> {
1956 let child_height = height
1957 .checked_add(1)
1958 .ok_or_else(|| failed(BlobReadError::CycleOrHeight, self.work))?;
1959 for child in children.iter().rev() {
1960 self.examine_item()?;
1961 if child.first_offset < self.range_end && child.end_offset > self.range.offset {
1962 self.pending_capacity
1963 .ensure_for_push(
1964 &mut self.pending,
1965 self.maximum_pending,
1966 &mut self.allocations,
1967 &mut self.work,
1968 self.budget,
1969 )
1970 .map_err(|error| failed(error.into(), self.work))?;
1971 self.pending.push(PendingBlobRead::Page {
1972 page: child.page,
1973 expected_first: child.first_offset,
1974 expected_end: Some(child.end_offset),
1975 height: child_height,
1976 });
1977 } else if self.range.length != 0 && child.first_offset >= self.range_end {
1978 self.retain_successor(
1979 child.first_offset,
1980 ObjectReadRequest {
1981 object_id: child.page,
1982 maximum_bytes: self.limits.maximum_page_object_bytes(),
1983 },
1984 );
1985 }
1986 }
1987 Ok(())
1988 }
1989
1990 fn push_chunks(&mut self, chunks: &[BlobChunkRef]) -> Result<(), BlobReadFailure> {
1991 for chunk in chunks.iter().rev() {
1992 self.examine_item()?;
1993 if chunk.first_offset < self.range_end && chunk.end_offset > self.range.offset {
1994 self.pending_capacity
1995 .ensure_for_push(
1996 &mut self.pending,
1997 self.maximum_pending,
1998 &mut self.allocations,
1999 &mut self.work,
2000 self.budget,
2001 )
2002 .map_err(|error| failed(error.into(), self.work))?;
2003 self.pending.push(PendingBlobRead::Chunk(*chunk));
2004 } else if self.range.length != 0 && chunk.first_offset >= self.range_end {
2005 self.retain_successor(
2006 chunk.first_offset,
2007 ObjectReadRequest {
2008 object_id: chunk.chunk,
2009 maximum_bytes: chunk.end_offset - chunk.first_offset,
2010 },
2011 );
2012 }
2013 }
2014 Ok(())
2015 }
2016
2017 fn retain_successor(&mut self, offset: u64, request: ObjectReadRequest) {
2018 if self
2019 .successor
2020 .is_none_or(|(current_offset, _)| offset < current_offset)
2021 {
2022 self.successor = Some((
2023 offset,
2024 ResidencyHint {
2025 request,
2026 reason: ResidencyReason::SequentialRange,
2027 },
2028 ));
2029 }
2030 }
2031
2032 fn examine_item(&mut self) -> Result<(), BlobReadFailure> {
2033 self.work = add_work(
2034 self.work,
2035 WorkCounters {
2036 items_examined: 1,
2037 ..WorkCounters::default()
2038 },
2039 )?;
2040 verify_work(self.work, self.budget)
2041 }
2042
2043 fn accept_chunk(
2044 &mut self,
2045 receipt: &ObjectReceipt<ObjectRead>,
2046 chunk: BlobChunkRef,
2047 ) -> Result<(), BlobReadFailure> {
2048 let retained_bytes = match receipt.value.retention {
2049 ObjectReadRetention::Shared => 0,
2050 ObjectReadRetention::Owned { logical_bytes } => logical_bytes,
2051 };
2052 self.allocations
2053 .claim_bytes(retained_bytes, 0, &mut self.work, self.budget)
2054 .map_err(|error| failed(error.into(), self.work))?;
2055 let chunk_length = chunk.end_offset - chunk.first_offset;
2056 if u64::try_from(receipt.value.len()).unwrap_or(u64::MAX) != chunk_length {
2057 return Err(failed(BlobReadError::ChunkLengthMismatch, self.work));
2058 }
2059 let start = self.range.offset.max(chunk.first_offset) - chunk.first_offset;
2060 let end = self.range_end.min(chunk.end_offset) - chunk.first_offset;
2061 let start =
2062 usize::try_from(start).map_err(|_| failed(BlobReadError::InvalidRange, self.work))?;
2063 let end =
2064 usize::try_from(end).map_err(|_| failed(BlobReadError::InvalidRange, self.work))?;
2065 #[allow(
2066 clippy::indexing_slicing,
2067 reason = "start <= end <= receipt.value.len() by construction: `BlobPage::validate` \
2068 (run by `decode_blob_page` before any chunk reaches this machine) requires \
2069 `contiguous()` to hold, so `chunk.first_offset < chunk.end_offset`; only \
2070 chunks with `chunk.first_offset < self.range_end && chunk.end_offset > \
2071 self.range.offset` are ever queued (see the frontier filter above); \
2072 `self.range_end` is `range.offset + range.length` via `checked_add`, so \
2073 `range.offset <= self.range_end`; together these bound \
2074 `max(range.offset, chunk.first_offset) <= min(range_end, \
2075 chunk.end_offset)`, i.e. start <= end; and the length check just above \
2076 establishes `end <= chunk_length == receipt.value.len()`"
2077 )]
2078 self.output.extend_from_slice(&receipt.value[start..end]);
2079 self.work = add_work(
2080 self.work,
2081 WorkCounters {
2082 bytes_copied: u64::try_from(end - start).unwrap_or(u64::MAX),
2083 output_bytes: u64::try_from(end - start).unwrap_or(u64::MAX),
2084 items_returned: 1,
2085 ..WorkCounters::default()
2086 },
2087 )?;
2088 verify_work(self.work, self.budget)?;
2089 self.allocations
2090 .release(retained_bytes)
2091 .map_err(|error| failed(error.into(), self.work))
2092 }
2093}
2094
2095impl frontier::Machine for BlobRangeMachine {
2096 type Output = BlobRead;
2097 type Failure = BlobReadFailure;
2098
2099 fn complete(&mut self) -> Result<Option<Self::Output>, Self::Failure> {
2100 if self.pending.is_empty() && self.awaiting.is_none() {
2101 return self.finish().map(Some);
2102 }
2103 Ok(None)
2104 }
2105
2106 fn prepare_read(&mut self) -> Result<frontier::ReadRequest, Self::Failure> {
2107 BlobRangeMachine::prepare_read(self)
2108 }
2109
2110 fn accept(
2111 &mut self,
2112 prospective: WorkCounters,
2113 receipt: &ObjectReceipt<ObjectRead>,
2114 ) -> Result<Option<Self::Output>, Self::Failure> {
2115 self.accept_read(prospective, receipt)
2116 }
2117
2118 fn storage_failure(
2119 &self,
2120 prospective: WorkCounters,
2121 failure: crate::storage::ObjectFailure,
2122 ) -> Self::Failure {
2123 match merge_blob_backend_work(prospective, *failure.work, self.allocations.live_bytes()) {
2124 Ok(combined) => failed(failure.error.into(), combined),
2125 Err(error) => failed(error.into(), prospective),
2126 }
2127 }
2128
2129 fn cancelled(&self) -> Self::Failure {
2130 failed(BlobReadError::Cancelled, self.work)
2131 }
2132}
2133
2134fn add_work(left: WorkCounters, right: WorkCounters) -> Result<WorkCounters, BlobReadFailure> {
2135 left.checked_add(right)
2136 .map_err(|error| failed(BlobReadError::Work(error), left))
2137}
2138
2139fn verify_work(work: WorkCounters, budget: WorkBudget) -> Result<(), BlobReadFailure> {
2140 work.verify(budget)
2141 .map_err(|error| failed(BlobReadError::Work(error), work))
2142}
2143
2144fn merge_blob_backend_work(
2145 prior: WorkCounters,
2146 mut backend: WorkCounters,
2147 live_bytes: u64,
2148) -> Result<WorkCounters, WorkError> {
2149 let simultaneous_peak = live_bytes
2150 .checked_add(backend.peak_allocation_bytes)
2151 .ok_or(WorkError::Overflow)?;
2152 backend.peak_allocation_bytes = 0;
2153 let mut merged = prior.checked_add(backend)?;
2154 merged.peak_allocation_bytes = merged.peak_allocation_bytes.max(simultaneous_peak);
2155 Ok(merged)
2156}
2157
2158pub type BlobReadFailure = OperationFailure<BlobReadError>;
2160
2161fn failed(error: BlobReadError, work: WorkCounters) -> BlobReadFailure {
2162 OperationFailure::new(error, work)
2163}
2164
2165#[derive(Debug, Error)]
2167pub enum BlobReadError {
2168 #[error("blob range reading was cancelled")]
2170 Cancelled,
2171 #[error("blob range transition state is invalid")]
2173 TraversalState,
2174 #[error("blob root is not a blob-index page")]
2176 WrongRootKind,
2177 #[error("requested blob range is invalid")]
2179 InvalidRange,
2180 #[error("blob read limits are invalid")]
2182 InvalidLimits,
2183 #[error("blob read scratch allocation failed")]
2185 AllocationFailed,
2186 #[error("blob index contains a cycle or exceeds its height bound")]
2188 CycleOrHeight,
2189 #[error("blob child bounds do not match its page")]
2191 ChildBoundsMismatch,
2192 #[error("blob chunks do not cover the requested range exactly")]
2194 IncompleteCoverage,
2195 #[error("blob chunk length does not match its index span")]
2197 ChunkLengthMismatch,
2198 #[error(transparent)]
2200 Storage(#[from] ObjectStoreError),
2201 #[error(transparent)]
2203 Decode(#[from] CanonicalDecodeError),
2204 #[error(transparent)]
2206 Work(#[from] WorkError),
2207}
2208
2209impl From<AllocationError> for BlobReadError {
2210 fn from(error: AllocationError) -> Self {
2211 match error {
2212 AllocationError::Work(error) => Self::Work(error),
2213 AllocationError::Overflow | AllocationError::ReleaseInvariant => {
2214 Self::Work(WorkError::Overflow)
2215 }
2216 AllocationError::InvalidCapacity
2217 | AllocationError::CapacityExceeded
2218 | AllocationError::AllocationFailed => Self::AllocationFailed,
2219 }
2220 }
2221}
2222
2223fn count(value: usize) -> Result<u32, CanonicalDecodeError> {
2224 u32::try_from(value).map_err(|_| CanonicalDecodeError::LengthOverflow)
2225}
2226
2227fn capacity(value: u32) -> Result<usize, CanonicalDecodeError> {
2228 usize::try_from(value).map_err(|_| CanonicalDecodeError::LengthOverflow)
2229}
2230
2231fn invariant(message: &str) -> CanonicalDecodeError {
2232 CanonicalDecodeError::Invariant(message.to_owned())
2233}
2234
2235#[cfg(all(test, feature = "memory"))]
2236#[path = "tests/blob.rs"]
2237mod tests;