micromegas_object_cache/range_cache/scheduler.rs
1use std::collections::HashMap;
2use std::sync::atomic::{AtomicU8, AtomicUsize, Ordering};
3use std::sync::{Arc, Mutex as StdMutex, Weak};
4
5use anyhow::{Result, anyhow};
6use bytes::Bytes;
7use micromegas_tracing::prelude::*;
8use tokio::sync::{Notify, OwnedSemaphorePermit, Semaphore, watch};
9
10use crate::metric_tags;
11
12/// Relative urgency of an origin fetch. Lower is more urgent.
13#[derive(Copy, Clone, Debug, PartialEq, Eq)]
14#[repr(u8)]
15pub(super) enum Priority {
16 Demand = 0,
17 Prefetch = 1,
18}
19
20impl Priority {
21 fn from_u8(v: u8) -> Self {
22 if v == Priority::Demand as u8 {
23 Priority::Demand
24 } else {
25 Priority::Prefetch
26 }
27 }
28
29 /// The `class` metric-tag value for this priority (see `metric_tags`).
30 pub(super) fn class_label(self) -> &'static str {
31 match self {
32 Priority::Demand => metric_tags::CLASS_DEMAND,
33 Priority::Prefetch => metric_tags::CLASS_PREFETCH,
34 }
35 }
36}
37
38/// The `class` tag for one coalesced run: `Demand` if any of its entries is
39/// currently demand priority, else `Prefetch`. Mirrors `acquire_run_permit`'s
40/// own `all_prefetch` check, evaluated once the permit has been acquired so a
41/// promotion that raced the wait is reflected in the tag.
42pub(super) fn effective_priority(entries: &[Arc<InFlight>]) -> Priority {
43 if entries.iter().any(|e| e.priority() == Priority::Demand) {
44 Priority::Demand
45 } else {
46 Priority::Prefetch
47 }
48}
49
50/// The set of entries submitted in one prefetch call. Used only when
51/// `promote_whole_batch` is enabled: a demand joiner into any sibling entry
52/// promotes every other entry in the batch, not just the one it joined.
53///
54/// Siblings are tracked by `Weak<InFlight>`, captured once each entry is
55/// created or joined, rather than by key string: a key can be removed from
56/// `FetchScheduler::inflight` and later reused by an unrelated fetch (e.g.
57/// after eviction), and promoting by key alone would then spuriously promote
58/// that new, logically unrelated `InFlight`.
59pub(super) struct BatchState {
60 entries: StdMutex<Vec<Weak<InFlight>>>,
61}
62
63impl BatchState {
64 pub(super) fn new(capacity: usize) -> Self {
65 Self {
66 entries: StdMutex::new(Vec::with_capacity(capacity)),
67 }
68 }
69}
70
71type FetchResult = Result<Bytes, Arc<anyhow::Error>>;
72
73/// One outstanding origin fetch (a single block or a `size()` head), shared
74/// across every concurrent caller asking for the same key.
75pub(super) struct InFlight {
76 priority: AtomicU8,
77 promote: Notify,
78 result: watch::Sender<Option<FetchResult>>,
79 batch: Option<Arc<BatchState>>,
80 /// Set by the first `fulfill()` call. Lets a `FulfillGuard`'s panic-path
81 /// fallback tell which entries in a partially-completed run already got
82 /// a real result, so it never clobbers one with a synthesized error.
83 fulfilled: std::sync::atomic::AtomicBool,
84}
85
86impl InFlight {
87 fn new(priority: Priority, batch: Option<Arc<BatchState>>) -> Self {
88 let (result, _rx) = watch::channel(None);
89 Self {
90 priority: AtomicU8::new(priority as u8),
91 promote: Notify::new(),
92 result,
93 batch,
94 fulfilled: std::sync::atomic::AtomicBool::new(false),
95 }
96 }
97
98 fn priority(&self) -> Priority {
99 Priority::from_u8(self.priority.load(Ordering::Acquire))
100 }
101
102 fn promote_to_demand(&self) {
103 self.priority
104 .store(Priority::Demand as u8, Ordering::Release);
105 self.promote.notify_one();
106 }
107
108 /// Deliver `result` to every joiner. Idempotent: only the first call
109 /// actually sends — later calls (notably a `FulfillGuard`'s panic-path
110 /// fallback racing a fetch that in fact completed normally) are no-ops,
111 /// so a real result is never overwritten by a synthesized error.
112 pub(super) fn fulfill(&self, result: FetchResult) {
113 if self.fulfilled.swap(true, Ordering::AcqRel) {
114 return;
115 }
116 // `send_replace`, not `send`: `send` drops the value without storing
117 // it when the channel currently has zero receivers, and joiners
118 // subscribe lazily inside `join()`. A fetch task that completes
119 // before any joiner's `subscribe()` would lose the result and hang
120 // every later joiner forever. `send_replace` stores the value
121 // unconditionally.
122 self.result.send_replace(Some(result));
123 }
124
125 /// Wait for the fetch to complete, returning immediately if it already
126 /// has. Safe against the "subscribe after send" race: a fresh `watch`
127 /// receiver's initial value counts as unseen until `borrow_and_update`
128 /// checks it, so a result sent before this call is still observed here.
129 pub(super) async fn join(&self) -> FetchResult {
130 let mut rx = self.result.subscribe();
131 loop {
132 if let Some(r) = rx.borrow_and_update().clone() {
133 return r;
134 }
135 if rx.changed().await.is_err() {
136 return Err(Arc::new(anyhow!(
137 "in-flight fetch entry dropped without a result"
138 )));
139 }
140 }
141 }
142}
143
144pub(super) enum Ownership {
145 Owner(Arc<InFlight>),
146 Joiner(Arc<InFlight>),
147}
148
149/// One priority-tiered pool of semaphore permits: `shared` sized `total`,
150/// `prefetch` sized `total - demand_reserved` so prefetch work never starves
151/// demand of its reserved slice. `FetchScheduler` instantiates this twice --
152/// once denominated in runs (`count`), once in bytes (`bytes`) -- so the two
153/// resources (origin-GET parallelism vs. transient fetch memory) are bounded
154/// independently but share the same reservation shape.
155struct PriorityBudget {
156 shared: Arc<Semaphore>,
157 /// Total capacity of `shared`, stored alongside it since
158 /// `tokio::sync::Semaphore` has no capacity accessor. Used by `stats`
159 /// for the saturation sampler.
160 shared_total: usize,
161 prefetch: Arc<Semaphore>,
162 /// Total capacity of `prefetch`, for the same reason as `shared_total`.
163 prefetch_total: usize,
164}
165
166impl PriorityBudget {
167 fn new(total: usize, demand_reserved: usize) -> Self {
168 assert!(total > 0, "fetch budget total must be > 0");
169 // Strictly less: `demand_reserved == total` would leave the prefetch
170 // semaphore with zero permits, hanging every prefetch run forever.
171 assert!(
172 demand_reserved < total,
173 "demand_reserved ({demand_reserved}) must be < total ({total})"
174 );
175 let prefetch_total = total - demand_reserved;
176 Self {
177 shared: Arc::new(Semaphore::new(total)),
178 shared_total: total,
179 prefetch: Arc::new(Semaphore::new(prefetch_total)),
180 prefetch_total,
181 }
182 }
183
184 /// Acquire `n` permits for one coalesced GET covering `entries`, honoring
185 /// promotion: the run's effective priority is the most urgent (minimum)
186 /// of its entries', re-checked every time a promotion wakes this loop so
187 /// a promotion mid-wait drops the prefetch-class requirement.
188 async fn acquire(&self, n: u32, entries: &[Arc<InFlight>]) -> BudgetPermit {
189 loop {
190 let all_prefetch = entries.iter().all(|e| e.priority() == Priority::Prefetch);
191 if !all_prefetch {
192 let shared = self
193 .shared
194 .clone()
195 .acquire_many_owned(n)
196 .await
197 .expect("shared semaphore is never closed");
198 return BudgetPermit {
199 _shared: shared,
200 _prefetch: None,
201 };
202 }
203
204 tokio::select! {
205 prefetch = self.prefetch.clone().acquire_many_owned(n) => {
206 let prefetch = prefetch.expect("prefetch semaphore is never closed");
207 tokio::select! {
208 shared = self.shared.clone().acquire_many_owned(n) => {
209 let shared = shared.expect("shared semaphore is never closed");
210 return BudgetPermit { _shared: shared, _prefetch: Some(prefetch) };
211 }
212 _ = any_entry_promoted(entries) => {
213 drop(prefetch);
214 continue;
215 }
216 }
217 }
218 _ = any_entry_promoted(entries) => {
219 continue;
220 }
221 }
222 }
223 }
224
225 fn stats(&self) -> BudgetStats {
226 BudgetStats {
227 shared_available: self.shared.available_permits(),
228 shared_total: self.shared_total,
229 prefetch_available: self.prefetch.available_permits(),
230 prefetch_total: self.prefetch_total,
231 }
232 }
233}
234
235/// Held for the duration of one origin GET's share of one `PriorityBudget`;
236/// dropping it frees the slot(s) for the next waiter. Fields are never read,
237/// only held for their `Drop` effect.
238pub(super) struct BudgetPermit {
239 _shared: OwnedSemaphorePermit,
240 _prefetch: Option<OwnedSemaphorePermit>,
241}
242
243/// A fetch-permit budget's current occupancy (one `PriorityBudget`'s worth),
244/// for the saturation sampler (`object-cache-srv/src/saturation_monitor.rs`).
245pub struct BudgetStats {
246 pub shared_available: usize,
247 pub shared_total: usize,
248 pub prefetch_available: usize,
249 pub prefetch_total: usize,
250}
251
252/// Both of `FetchScheduler`'s budgets' occupancy: `count` (one permit per
253/// in-flight run, bounding origin-GET parallelism) and `bytes` (one permit
254/// per in-flight byte, bounding transient fetch memory).
255pub struct FetchBudgetStats {
256 pub count: BudgetStats,
257 pub bytes: BudgetStats,
258}
259
260/// Owns the in-flight single-flight map and the priority-aware origin-fetch
261/// budget.
262pub(super) struct FetchScheduler {
263 inflight: StdMutex<HashMap<String, Arc<InFlight>>>,
264 /// One permit per in-flight coalesced run (blocks + size heads don't
265 /// count against this; only run GETs acquire it). Bounds origin-GET
266 /// parallelism, independent of `bytes`.
267 count: PriorityBudget,
268 /// One permit per byte of an in-flight coalesced run's buffer. Bounds the
269 /// transient origin-fetch memory ceiling, independent of `count`.
270 bytes: PriorityBudget,
271 promote_whole_batch: bool,
272 /// Count of detached fetch tasks (`spawn_run_fetch` runs + `size()` HEADs)
273 /// currently in flight, tracked by `FetchTaskGuard`. Used by graceful
274 /// shutdown to wait for in-flight origin GETs instead of letting the
275 /// tokio runtime drop them.
276 outstanding_tasks: AtomicUsize,
277 /// Notified when `outstanding_tasks` drops to zero. See `wait_drained`.
278 drained: Notify,
279}
280
281impl FetchScheduler {
282 /// `budget_bytes`/`reserved_bytes` are the byte-denominated analog of
283 /// `total`/`demand_reserved`; `max_run_bytes` is the largest byte span one
284 /// coalesced run can reach (`blocks::max_run_bytes`), asserted against
285 /// both so a run can never exceed what either budget can ever grant.
286 pub(super) fn new(
287 total: usize,
288 demand_reserved: usize,
289 budget_bytes: u64,
290 reserved_bytes: u64,
291 max_run_bytes: u64,
292 promote_whole_batch: bool,
293 ) -> Self {
294 // `acquire_many_owned` takes a `u32` permit count, so one run's byte
295 // charge must fit in a `u32` -- checked here once at construction
296 // rather than per-run in `acquire_run_permit`.
297 assert!(
298 max_run_bytes <= u32::MAX as u64,
299 "max_run_bytes ({max_run_bytes}) must fit in u32: acquire_many_owned takes a u32 permit count"
300 );
301 assert!(
302 reserved_bytes < budget_bytes,
303 "fetch_memory_budget reserved_bytes ({reserved_bytes}) must be < budget_bytes ({budget_bytes})"
304 );
305 // Without this, a run larger than the prefetch byte pool would never
306 // complete (and never error) acquiring `bytes` permits: the same hang
307 // the `fetch_memory_budget_mb` floor in `object-cache-srv`'s
308 // `cli.rs::validate` guards against at the CLI boundary.
309 assert!(
310 budget_bytes - reserved_bytes >= max_run_bytes,
311 "fetch memory budget's prefetch pool ({} bytes = budget_bytes {budget_bytes} - \
312 reserved_bytes {reserved_bytes}) must be >= max_run_bytes ({max_run_bytes}), or a \
313 full-size run would hang forever acquiring its byte-budget permits",
314 budget_bytes - reserved_bytes
315 );
316 Self {
317 inflight: StdMutex::new(HashMap::new()),
318 count: PriorityBudget::new(total, demand_reserved),
319 bytes: PriorityBudget::new(
320 usize::try_from(budget_bytes).expect("fetch_memory_budget_bytes fits in usize"),
321 usize::try_from(reserved_bytes).expect("reserved fetch memory bytes fit in usize"),
322 ),
323 promote_whole_batch,
324 outstanding_tasks: AtomicUsize::new(0),
325 drained: Notify::new(),
326 }
327 }
328
329 /// Register one detached fetch task as outstanding. Call *before*
330 /// `tokio::spawn`, synchronously, so there is no window where a
331 /// queued-but-not-yet-polled task is invisible to `wait_drained()`.
332 pub(super) fn track_task(scheduler: &Arc<FetchScheduler>) -> FetchTaskGuard {
333 scheduler.outstanding_tasks.fetch_add(1, Ordering::AcqRel);
334 FetchTaskGuard(scheduler.clone())
335 }
336
337 /// Number of detached fetch tasks currently in flight to origin.
338 pub(super) fn outstanding_tasks(&self) -> usize {
339 self.outstanding_tasks.load(Ordering::Acquire)
340 }
341
342 /// Resolves once no detached fetch task is outstanding. Race-free by
343 /// construction: `Notified::enable`'s docs guarantee that a
344 /// `notify_waiters()` call is observed by a `Notified` future as long as
345 /// that call happens after the `Notified` was created, regardless of
346 /// whether `enable`/`poll` has run yet -- so creating the `Notified`
347 /// *before* re-checking the count (rather than after) means a
348 /// `notify_waiters()` racing the check in between is never missed. This
349 /// is a different guarantee than `any_entry_promoted`'s below, which
350 /// relies on `notify_one`'s stored-permit semantics; `notify_waiters`
351 /// stores no permit, so the ordering above -- not a permit -- is what
352 /// makes this race-free.
353 pub(super) async fn wait_drained(&self) {
354 loop {
355 if self.outstanding_tasks() == 0 {
356 return;
357 }
358 let notified = self.drained.notified();
359 if self.outstanding_tasks() == 0 {
360 return;
361 }
362 notified.await;
363 }
364 }
365
366 /// Both fetch-permit budgets' current occupancy, for the saturation
367 /// sampler (`object-cache-srv/src/saturation_monitor.rs`).
368 pub(super) fn fetch_budget_stats(&self) -> FetchBudgetStats {
369 FetchBudgetStats {
370 count: self.count.stats(),
371 bytes: self.bytes.stats(),
372 }
373 }
374
375 /// Number of keys (blocks or `size()` heads) currently in flight to
376 /// origin.
377 pub(super) fn inflight_len(&self) -> usize {
378 self.inflight.lock().expect("inflight lock").len()
379 }
380
381 /// Look up `key` in the in-flight map: become the owner if absent, or a
382 /// joiner if present. A demand joiner into a prefetch-priority entry
383 /// promotes it (and, if `promote_whole_batch`, its batch siblings) to
384 /// demand so it competes for reserved capacity instead of sitting behind
385 /// other prefetch work.
386 ///
387 /// The entry is registered in `batch` (owner or joiner alike) while the
388 /// inflight lock is still held, i.e. before any concurrent demand joiner
389 /// can find the entry and run `promote_batch_siblings` over a
390 /// partially-populated list.
391 pub(super) fn own_or_join(
392 &self,
393 key: String,
394 prio: Priority,
395 batch: Option<Arc<BatchState>>,
396 ) -> Ownership {
397 let mut promote_batch: Option<(Arc<BatchState>, Arc<InFlight>)> = None;
398 let ownership = {
399 let mut map = self.inflight.lock().expect("inflight lock");
400 if let Some(existing) = map.get(&key) {
401 if prio == Priority::Demand && existing.priority() == Priority::Prefetch {
402 existing.promote_to_demand();
403 if self.promote_whole_batch
404 && let Some(bs) = existing.batch.clone()
405 {
406 promote_batch = Some((bs, existing.clone()));
407 }
408 }
409 if let Some(bs) = &batch {
410 bs.entries
411 .lock()
412 .expect("batch lock")
413 .push(Arc::downgrade(existing));
414 }
415 Ownership::Joiner(existing.clone())
416 } else {
417 let entry = Arc::new(InFlight::new(prio, batch.clone()));
418 if let Some(bs) = &batch {
419 bs.entries
420 .lock()
421 .expect("batch lock")
422 .push(Arc::downgrade(&entry));
423 }
424 map.insert(key.clone(), entry.clone());
425 Ownership::Owner(entry)
426 }
427 };
428 if let Some((bs, skip)) = promote_batch {
429 self.promote_batch_siblings(&bs, &skip);
430 }
431 ownership
432 }
433
434 /// Promote every sibling in `batch` other than `skip` to demand priority.
435 /// Siblings are resolved through `Weak<InFlight>` references captured at
436 /// batch-membership time, not by re-looking up their key in `inflight`:
437 /// the key may since have been removed and reused by an unrelated fetch,
438 /// and identity (not the key string) is what must match here.
439 fn promote_batch_siblings(&self, batch: &BatchState, skip: &Arc<InFlight>) {
440 let entries = batch.entries.lock().expect("batch lock");
441 for weak in entries.iter() {
442 if let Some(entry) = weak.upgrade()
443 && !Arc::ptr_eq(&entry, skip)
444 {
445 entry.promote_to_demand();
446 }
447 }
448 }
449
450 pub(super) fn remove_entry(&self, key: &str) {
451 self.inflight.lock().expect("inflight lock").remove(key);
452 }
453}
454
455/// Scope guard held by the task that owns one or more in-flight entries
456/// (either a single `size()` head or the members of one coalesced block
457/// run). If the owning task exits normally, it calls `disarm()` after
458/// fulfilling every entry and removing it from `FetchScheduler::inflight`.
459///
460/// If instead the task panics (e.g. `Bytes::slice` on a short origin read)
461/// tokio catches the unwind at the task boundary and silently drops the
462/// result, but this guard's `Drop` still runs during that unwind: it
463/// fulfills any not-yet-fulfilled entry with an error (the `fulfilled` flag
464/// on `InFlight` makes this a no-op for entries that already got a real
465/// result) and removes every entry from the map. Without this, a panicking
466/// owner would leave `fulfill()` never called, hanging every joiner
467/// (including the owner itself) forever and leaking the entry permanently.
468pub(super) struct FulfillGuard {
469 scheduler: Arc<FetchScheduler>,
470 entries: Vec<(String, Arc<InFlight>)>,
471 armed: bool,
472}
473
474impl FulfillGuard {
475 pub(super) fn new(
476 scheduler: Arc<FetchScheduler>,
477 entries: Vec<(String, Arc<InFlight>)>,
478 ) -> Self {
479 Self {
480 scheduler,
481 entries,
482 armed: true,
483 }
484 }
485
486 /// Call once the normal completion path has fulfilled and removed every
487 /// entry, so the guard's `Drop` becomes a no-op.
488 pub(super) fn disarm(mut self) {
489 self.armed = false;
490 }
491}
492
493impl Drop for FulfillGuard {
494 fn drop(&mut self) {
495 if !self.armed {
496 return;
497 }
498 let n = self.entries.len();
499 // `std::thread::panicking()` is `true` only while the current thread
500 // is unwinding from a real panic; it is `false` when a task future is
501 // simply dropped without being polled to completion (e.g. the tokio
502 // runtime shutting down), so it's exactly the signal needed to tell
503 // the two cases apart.
504 let panicking = std::thread::panicking();
505 if panicking {
506 warn!("fetch task panicked; fulfilling {n} in-flight entries with an error");
507 } else {
508 warn!(
509 "cache service shutting down; abandoning {n} in-flight fetch entries \
510 (joiners see a synthesized error and must refetch after restart)"
511 );
512 }
513 let msg = if panicking {
514 "fetch task panicked before producing a result"
515 } else {
516 "fetch task was abandoned (cache service shutting down)"
517 };
518 for (key, entry) in &self.entries {
519 entry.fulfill(Err(Arc::new(anyhow!(msg))));
520 self.scheduler.remove_entry(key);
521 }
522 }
523}
524
525/// Scope guard tracking the lifetime of one detached fetch task, held for
526/// its whole body. Distinct from `FulfillGuard`, which tracks *entry
527/// fulfillment*, not task lifetime -- the two guards serve different
528/// purposes and both must survive a panicking OR shutdown-dropped task.
529/// Because this is a plain local variable inside the spawned async block,
530/// its `Drop` runs whether the task finishes normally, panics, or is dropped
531/// without being polled to completion, so `outstanding_tasks` stays accurate
532/// in all three cases without any special-casing.
533pub(super) struct FetchTaskGuard(Arc<FetchScheduler>);
534
535impl Drop for FetchTaskGuard {
536 fn drop(&mut self) {
537 if self.0.outstanding_tasks.fetch_sub(1, Ordering::AcqRel) == 1 {
538 self.0.drained.notify_waiters();
539 }
540 }
541}
542
543/// Held for the duration of one coalesced run: `count` bounds origin-GET
544/// parallelism and is dropped right after the GET returns; `bytes` bounds the
545/// run's transient memory and is held through backend admission. See
546/// `acquire_run_permit` and its caller in `fetch.rs`.
547pub(super) struct RunPermit {
548 pub(super) count: BudgetPermit,
549 pub(super) bytes: BudgetPermit,
550}
551
552/// Resolves when any of `entries`' priority has been promoted since this call
553/// started. Constructing the `Notified` future(s) up front (before the caller
554/// re-checks priority) is what makes this race-free: tokio guarantees a
555/// `notify_one()` that happens after a `Notified` is created is never missed,
556/// even if that `Notified` is later dropped without completing.
557async fn any_entry_promoted(entries: &[Arc<InFlight>]) {
558 match entries {
559 [] => std::future::pending::<()>().await,
560 [only] => only.promote.notified().await,
561 many => {
562 let futs: Vec<_> = many
563 .iter()
564 .map(|e| Box::pin(e.promote.notified()))
565 .collect();
566 futures::future::select_all(futs).await;
567 }
568 }
569}
570
571/// Acquire the permits needed to run one coalesced GET covering `entries`,
572/// spanning `run_bytes` bytes: bytes first, then count. Every run acquires in
573/// this order, so there is no hold-and-wait cycle between the two budgets;
574/// the cost is that a run holding its bytes while queued for a count slot
575/// parks that byte budget idle for the wait, which only bites when the byte
576/// budget is deliberately sized larger than `count * typical run`. Each
577/// underlying `PriorityBudget::acquire` call independently honors promotion
578/// (see its doc).
579pub(super) async fn acquire_run_permit(
580 scheduler: &FetchScheduler,
581 entries: &[Arc<InFlight>],
582 run_bytes: u64,
583) -> RunPermit {
584 let n = u32::try_from(run_bytes)
585 .expect("run size validated <= u32::MAX at construction (FetchScheduler::new asserts max_run_bytes fits)");
586 let bytes = scheduler.bytes.acquire(n, entries).await;
587 let count = scheduler.count.acquire(1, entries).await;
588 RunPermit { count, bytes }
589}
590
591/// Reconstruct an owned error from a shared in-flight failure. `anyhow::Error`
592/// is not `Clone`, so a joiner cannot move it out of the `Arc`; and it must
593/// not be stringified, or a missing key would stop downcasting to
594/// `object_store::Error::NotFound` and would surface as a 500 instead of a
595/// 404 (see `validation::is_not_found`).
596pub(super) fn reconstruct_shared_error(shared: &Arc<anyhow::Error>) -> anyhow::Error {
597 if let Some(object_store::Error::NotFound { path, source }) =
598 shared.downcast_ref::<object_store::Error>()
599 {
600 let rebuilt = object_store::Error::NotFound {
601 path: path.clone(),
602 source: Box::new(std::io::Error::new(
603 std::io::ErrorKind::NotFound,
604 source.to_string(),
605 )),
606 };
607 return anyhow::Error::from(rebuilt);
608 }
609 // `{shared:?}` keeps the full context chain (and backtrace, if captured)
610 // so joiners' 500 log lines are as informative as the owner's.
611 anyhow!("{shared:?}")
612}
613
614pub(super) fn decode_size(data: &Bytes) -> Result<u64> {
615 Ok(u64::from_le_bytes(
616 data[..8].try_into().expect("8-byte size slice"),
617 ))
618}