Skip to main content

mz_persist_client/
usage.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Introspection of storage utilization by persist
11
12use std::collections::BTreeMap;
13use std::sync::Arc;
14use std::time::Instant;
15
16use futures::stream::{FuturesUnordered, StreamExt};
17use mz_ore::cast::CastFrom;
18use mz_persist::location::Blob;
19use tokio::sync::Semaphore;
20use tracing::{error, info};
21
22use crate::cfg::{PersistConfig, USAGE_STATE_FETCH_CONCURRENCY_LIMIT};
23use crate::internal::paths::{BlobKey, BlobKeyPrefix, PartialBlobKey, WriterKey};
24use crate::internal::state::HollowBlobRef;
25use crate::internal::state_versions::StateVersions;
26use crate::{Metrics, PersistClient, ShardId, retry_external};
27
28/// A breakdown of the size of various contributions to a shard's blob
29/// usage that is actively referenced by any live state in Consensus.
30#[derive(Clone, Debug)]
31pub struct ShardUsageReferenced {
32    pub(crate) batches_bytes: u64,
33    pub(crate) rollup_bytes: u64,
34}
35
36impl ShardUsageReferenced {
37    /// Byte size of all data referenced in state for the shard.
38    pub fn size_bytes(&self) -> u64 {
39        let Self {
40            batches_bytes,
41            rollup_bytes,
42        } = self;
43        *batches_bytes + *rollup_bytes
44    }
45}
46
47/// The referenced blob usage for a set of shards.
48#[derive(Debug)]
49pub struct ShardsUsageReferenced {
50    /// The data for each shard.
51    pub by_shard: BTreeMap<ShardId, ShardUsageReferenced>,
52}
53
54/// A breakdown of the size of various contributions to a shard's blob (S3)
55/// usage.
56///
57/// This is structured as a "funnel", in which the steps are additive.
58/// Specifically `1=2a+2b`, `2a=3a+3b`, `3a=4a+4b`, `4a=5a+5b` (so the "a"s are
59/// the funnel and the "b"s are places where data splits out of the funnel).
60#[derive(Clone, Debug)]
61pub struct ShardUsageAudit {
62    /// 5a: Data in batches/parts referenced by the most recent version of
63    /// state.
64    pub current_state_batches_bytes: u64,
65    /// 5b: Data in rollups referenced by the most recent version of state.
66    pub current_state_rollups_bytes: u64,
67    /// 4b: Data referenced by a live version of state that is not the most
68    /// recent.
69    ///
70    /// Possible causes:
71    /// - SeqNo hold
72    /// - Waiting for a GC run
73    pub referenced_not_current_state_bytes: u64,
74    /// 3b: Data not referenced by any live version of state.
75    ///
76    /// Possible causes:
77    /// - A batch or rollup that's about to be linked into state
78    /// - A batch leaked by a crash, but the writer has not yet been force
79    ///   expired
80    /// - A rollup leaked by a crash, but GC has not yet advanced past the
81    ///   SeqNo
82    pub not_leaked_not_referenced_bytes: u64,
83    /// 2b: Data that is eligible for reclamation by a (future) leaked blob
84    /// cleanup task (database-issues#5018).
85    ///
86    /// Possible causes:
87    /// - A batch or rollup written by a process which crashed (or was rolled)
88    ///   before it could be linked into state.
89    pub leaked_bytes: u64,
90}
91
92impl ShardUsageAudit {
93    /// 4a: Data referenced by the most recent version of state.
94    pub fn current_state_bytes(&self) -> u64 {
95        self.current_state_batches_bytes + self.current_state_rollups_bytes
96    }
97
98    /// 3a: Data referenced by any live version of state.
99    pub fn referenced_bytes(&self) -> u64 {
100        self.current_state_bytes() + self.referenced_not_current_state_bytes
101    }
102
103    /// 2a: Data that would not be reclaimed by a (future) leaked blob
104    /// cleanup task (database-issues#5018).
105    pub fn not_leaked_bytes(&self) -> u64 {
106        self.referenced_bytes() + self.not_leaked_not_referenced_bytes
107    }
108
109    /// 1: Raw blob (S3) usage.
110    ///
111    /// NB: Due to race conditions between reads of blob and consensus in the
112    /// usage code, this might be a slight under-counting.
113    pub fn total_bytes(&self) -> u64 {
114        self.not_leaked_bytes() + self.leaked_bytes
115    }
116}
117
118/// The blob (S3) usage of all shards in an environment.
119#[derive(Clone, Debug)]
120pub struct ShardsUsageAudit {
121    /// The data for each shard.
122    pub by_shard: BTreeMap<ShardId, ShardUsageAudit>,
123    /// Data not attributable to any particular shard. This _should_ always be
124    /// 0; a nonzero value indicates either persist wrote an invalid blob key,
125    /// or another process is storing data under the same path (!)
126    pub unattributable_bytes: u64,
127}
128
129#[derive(Clone, Debug, Default)]
130struct BlobUsage {
131    by_shard: BTreeMap<ShardId, ShardBlobUsage>,
132    unattributable_bytes: u64,
133    batch_part_bytes: u64,
134    batch_part_count: u64,
135    rollup_size: u64,
136    rollup_count: u64,
137    total_size: u64,
138    total_count: u64,
139}
140
141#[derive(Clone, Debug, Default)]
142struct ShardBlobUsage {
143    by_writer: BTreeMap<WriterKey, u64>,
144    rollup_bytes: u64,
145}
146
147impl ShardBlobUsage {
148    fn total_bytes(&self) -> u64 {
149        self.by_writer.values().copied().sum::<u64>() + self.rollup_bytes
150    }
151}
152
153/// Provides access to storage usage metrics for a specific Blob
154#[derive(Clone, Debug)]
155pub struct StorageUsageClient {
156    cfg: PersistConfig,
157    blob: Arc<dyn Blob>,
158    metrics: Arc<Metrics>,
159    state_versions: Arc<StateVersions>,
160}
161
162impl StorageUsageClient {
163    /// Creates a new StorageUsageClient.
164    pub fn open(client: PersistClient) -> Self {
165        let state_versions = Arc::new(StateVersions::new(
166            client.cfg.clone(),
167            Arc::clone(&client.consensus),
168            Arc::clone(&client.blob),
169            Arc::clone(&client.metrics),
170        ));
171        StorageUsageClient {
172            cfg: client.cfg,
173            blob: client.blob,
174            metrics: client.metrics,
175            state_versions,
176        }
177    }
178
179    /// Computes [ShardUsageReferenced] for a single shard. Suitable for customer billing.
180    pub async fn shard_usage_referenced(&self, shard_id: ShardId) -> ShardUsageReferenced {
181        let mut start = Instant::now();
182        let states_iter = self
183            .state_versions
184            .fetch_all_live_states::<u64>(shard_id)
185            .await;
186        let states_iter = match states_iter {
187            Some(x) => x,
188            None => {
189                return ShardUsageReferenced {
190                    batches_bytes: 0,
191                    rollup_bytes: 0,
192                };
193            }
194        };
195        let mut states_iter = states_iter
196            .check_ts_codec()
197            .expect("ts should be a u64 in all prod shards");
198
199        let shard_metrics = &self.metrics.shards.shard(&shard_id, "unknown");
200        shard_metrics
201            .gc_live_diffs
202            .set(u64::cast_from(states_iter.len()));
203
204        let now = Instant::now();
205        self.metrics
206            .audit
207            .step_state
208            .inc_by(now.duration_since(start).as_secs_f64());
209        start = now;
210
211        let mut batches_bytes = 0;
212        let mut rollup_bytes = 0;
213        while let Some(_) = states_iter.next(|diff| {
214            diff.referenced_blobs().for_each(|blob| match blob {
215                HollowBlobRef::Batch(batch) => {
216                    batches_bytes += batch.encoded_size_bytes();
217                }
218                HollowBlobRef::Rollup(rollup) => {
219                    rollup_bytes += rollup.encoded_size_bytes.unwrap_or(1);
220                }
221            })
222        }) {}
223
224        let referenced = ShardUsageReferenced {
225            batches_bytes: u64::cast_from(batches_bytes),
226            rollup_bytes: u64::cast_from(rollup_bytes),
227        };
228
229        let current_state_sizes = states_iter.state().size_metrics();
230        shard_metrics
231            .usage_current_state_batches_bytes
232            .set(u64::cast_from(current_state_sizes.state_batches_bytes));
233        shard_metrics
234            .usage_current_state_rollups_bytes
235            .set(u64::cast_from(current_state_sizes.state_rollups_bytes));
236        shard_metrics.usage_referenced_not_current_state_bytes.set(
237            referenced.size_bytes()
238                - u64::cast_from(
239                    current_state_sizes.state_batches_bytes
240                        + current_state_sizes.state_rollups_bytes,
241                ),
242        );
243
244        self.metrics
245            .audit
246            .step_math
247            .inc_by(now.duration_since(start).as_secs_f64());
248
249        referenced
250    }
251
252    /// Computes [ShardUsageReferenced] for a given set of shards. Suitable for customer billing.
253    pub async fn shards_usage_referenced(
254        &self,
255        shard_ids: impl IntoIterator<Item = ShardId>,
256    ) -> ShardsUsageReferenced {
257        let semaphore = Arc::new(Semaphore::new(
258            USAGE_STATE_FETCH_CONCURRENCY_LIMIT.get(&self.cfg),
259        ));
260        let by_shard_futures = FuturesUnordered::new();
261        for shard_id in shard_ids {
262            let semaphore = Arc::clone(&semaphore);
263            let shard_usage_fut = async move {
264                let _permit = semaphore
265                    .acquire()
266                    .await
267                    .expect("acquiring permit from open semaphore");
268                let shard_usage = self.shard_usage_referenced(shard_id).await;
269                (shard_id, shard_usage)
270            };
271            by_shard_futures.push(shard_usage_fut);
272        }
273        let by_shard = by_shard_futures.collect().await;
274        ShardsUsageReferenced { by_shard }
275    }
276
277    /// Computes [ShardUsageAudit] for a single shard.
278    ///
279    /// Performs a full scan of [Blob] and [mz_persist::location::Consensus] to compute a full audit
280    /// of blob usage, categorizing both referenced and unreferenced blobs (see [ShardUsageAudit]
281    /// for full details). While [ShardUsageAudit::referenced_bytes] is suitable for billing, prefer
282    /// [Self::shard_usage_referenced] to avoid the (costly!) scan of [Blob] if the additional
283    /// categorizations are not needed.
284    pub async fn shard_usage_audit(&self, shard_id: ShardId) -> ShardUsageAudit {
285        let mut blob_usage = self.blob_raw_usage(BlobKeyPrefix::Shard(&shard_id)).await;
286        let blob_usage = blob_usage.by_shard.remove(&shard_id).unwrap_or_default();
287        self.shard_usage_given_blob_usage(shard_id, &blob_usage)
288            .await
289    }
290
291    /// Computes [ShardUsageAudit] for every shard in an env.
292    ///
293    /// See [Self::shard_usage_audit] for more details on when to use a full audit.
294    pub async fn shards_usage_audit(&self) -> ShardsUsageAudit {
295        let blob_usage = self.blob_raw_usage(BlobKeyPrefix::All).await;
296        self.metrics
297            .audit
298            .blob_batch_part_bytes
299            .set(blob_usage.batch_part_bytes);
300        self.metrics
301            .audit
302            .blob_batch_part_count
303            .set(blob_usage.batch_part_count);
304        self.metrics
305            .audit
306            .blob_rollup_bytes
307            .set(blob_usage.rollup_size);
308        self.metrics
309            .audit
310            .blob_rollup_count
311            .set(blob_usage.rollup_count);
312        self.metrics.audit.blob_bytes.set(blob_usage.total_size);
313        self.metrics.audit.blob_count.set(blob_usage.total_count);
314
315        let semaphore = Semaphore::new(USAGE_STATE_FETCH_CONCURRENCY_LIMIT.get(&self.cfg));
316        let by_shard_futures = FuturesUnordered::new();
317        for (shard_id, total_bytes) in blob_usage.by_shard.iter() {
318            let shard_usage_fut = async {
319                let _permit = semaphore
320                    .acquire()
321                    .await
322                    .expect("acquiring permit from open semaphore");
323                let shard_usage = self
324                    .shard_usage_given_blob_usage(*shard_id, total_bytes)
325                    .await;
326                (*shard_id, shard_usage)
327            };
328            by_shard_futures.push(shard_usage_fut);
329        }
330
331        let by_shard = by_shard_futures.collect().await;
332        ShardsUsageAudit {
333            by_shard,
334            unattributable_bytes: blob_usage.unattributable_bytes,
335        }
336    }
337
338    async fn blob_raw_usage(&self, prefix: BlobKeyPrefix<'_>) -> BlobUsage {
339        retry_external(
340            &self.metrics.retries.external.storage_usage_shard_size,
341            || async {
342                let mut start = Instant::now();
343                let mut keys = 0;
344                let mut usage = BlobUsage::default();
345                self.blob
346                    .list_keys_and_metadata(&prefix.to_string(), &mut |metadata| {
347                        // Increment the step timing metrics as we go, so it
348                        // doesn't all show up at the end.
349                        keys += 1;
350                        if keys % 100 == 0 {
351                            let now = Instant::now();
352                            self.metrics
353                                .audit
354                                .step_blob_metadata
355                                .inc_by(now.duration_since(start).as_secs_f64());
356                            start = now;
357                        }
358
359                        match BlobKey::parse_ids(metadata.key) {
360                            Ok((shard, partial_blob_key)) => {
361                                let shard_usage = usage.by_shard.entry(shard).or_default();
362
363                                match partial_blob_key {
364                                    PartialBlobKey::Batch(writer_id, _) => {
365                                        usage.batch_part_bytes += metadata.size_in_bytes;
366                                        usage.batch_part_count += 1;
367                                        *shard_usage.by_writer.entry(writer_id).or_default() +=
368                                            metadata.size_in_bytes;
369                                    }
370                                    PartialBlobKey::Rollup(_, _) => {
371                                        usage.rollup_size += metadata.size_in_bytes;
372                                        usage.rollup_count += 1;
373                                        shard_usage.rollup_bytes += metadata.size_in_bytes;
374                                    }
375                                }
376                            }
377                            _ => {
378                                info!("unknown blob: {}: {}", metadata.key, metadata.size_in_bytes);
379                                usage.unattributable_bytes += metadata.size_in_bytes;
380                            }
381                        }
382                        usage.total_size += metadata.size_in_bytes;
383                        usage.total_count += 1;
384                    })
385                    .await?;
386                self.metrics
387                    .audit
388                    .step_blob_metadata
389                    .inc_by(start.elapsed().as_secs_f64());
390                Ok(usage)
391            },
392        )
393        .await
394    }
395
396    async fn shard_usage_given_blob_usage(
397        &self,
398        shard_id: ShardId,
399        blob_usage: &ShardBlobUsage,
400    ) -> ShardUsageAudit {
401        let mut start = Instant::now();
402        let states_iter = self
403            .state_versions
404            .fetch_all_live_states::<u64>(shard_id)
405            .await;
406        let states_iter = match states_iter {
407            Some(x) => x,
408            None => {
409                // It's unexpected for a shard to exist in blob but not in
410                // consensus, but it could happen. For example, if an initial
411                // rollup has been written but the initial CaS hasn't yet
412                // succeeded (or if a `bin/environmentd --reset` is interrupted
413                // in dev). Be loud because it's unexpected, but handle it
414                // because it can happen.
415                error!(
416                    concat!(
417                        "shard {} existed in blob but not in consensus. This should be quite rare in ",
418                        "prod, but is semi-expected in development if `bin/environmentd --reset` gets ",
419                        "interrupted"
420                    ),
421                    shard_id
422                );
423                return ShardUsageAudit {
424                    current_state_batches_bytes: 0,
425                    current_state_rollups_bytes: 0,
426                    referenced_not_current_state_bytes: 0,
427                    not_leaked_not_referenced_bytes: 0,
428                    leaked_bytes: blob_usage.total_bytes(),
429                };
430            }
431        };
432        let mut states_iter = states_iter
433            .check_ts_codec()
434            .expect("ts should be a u64 in all prod shards");
435        let now = Instant::now();
436        self.metrics
437            .audit
438            .step_state
439            .inc_by(now.duration_since(start).as_secs_f64());
440        start = now;
441
442        let shard_metrics = self.metrics.shards.shard(&shard_id, "unknown");
443        shard_metrics
444            .gc_live_diffs
445            .set(u64::cast_from(states_iter.len()));
446
447        let mut referenced_batches_bytes = BTreeMap::new();
448        let mut referenced_other_bytes = 0;
449        while let Some(_) = states_iter.next(|x| {
450            x.referenced_blobs().for_each(|x| match x {
451                HollowBlobRef::Batch(x) => {
452                    for part in x.parts.iter() {
453                        if let Some(writer_id) = part.writer_key() {
454                            let writer_referenced_batches_bytes =
455                                referenced_batches_bytes.entry(writer_id).or_default();
456                            *writer_referenced_batches_bytes += u64::cast_from(part.hollow_bytes());
457                        } else {
458                            referenced_other_bytes += u64::cast_from(part.hollow_bytes());
459                        }
460                    }
461                }
462                HollowBlobRef::Rollup(x) => {
463                    referenced_other_bytes +=
464                        u64::cast_from(x.encoded_size_bytes.unwrap_or_default());
465                }
466            })
467        }) {}
468
469        let mut current_state_batches_bytes = 0;
470        let mut current_state_rollups_bytes = 0;
471        states_iter.state().blobs().for_each(|x| match x {
472            HollowBlobRef::Batch(x) => {
473                for part in x.parts.iter() {
474                    current_state_batches_bytes += u64::cast_from(part.hollow_bytes());
475                }
476            }
477            HollowBlobRef::Rollup(x) => {
478                current_state_rollups_bytes +=
479                    u64::cast_from(x.encoded_size_bytes.unwrap_or_default());
480            }
481        });
482        let current_state_bytes = current_state_batches_bytes + current_state_rollups_bytes;
483
484        let ret = ShardUsageAudit::from(ShardUsageCumulativeMaybeRacy {
485            current_state_batches_bytes,
486            current_state_bytes,
487            referenced_other_bytes,
488            referenced_batches_bytes: &referenced_batches_bytes,
489            // In the future, this is likely to include a "grace period" so recent but non-current
490            // versions are also considered live
491            minimum_key: WriterKey::for_version(&self.cfg.build_version),
492            blob_usage,
493        });
494
495        // Sanity check that we didn't obviously do anything wrong.
496        assert_eq!(ret.total_bytes(), blob_usage.total_bytes());
497
498        shard_metrics
499            .usage_current_state_batches_bytes
500            .set(ret.current_state_batches_bytes);
501        shard_metrics
502            .usage_current_state_rollups_bytes
503            .set(ret.current_state_rollups_bytes);
504        shard_metrics
505            .usage_referenced_not_current_state_bytes
506            .set(ret.referenced_not_current_state_bytes);
507        shard_metrics
508            .usage_not_leaked_not_referenced_bytes
509            .set(ret.not_leaked_not_referenced_bytes);
510        shard_metrics.usage_leaked_bytes.set(ret.leaked_bytes);
511
512        self.metrics
513            .audit
514            .step_math
515            .inc_by(start.elapsed().as_secs_f64());
516        ret
517    }
518
519    /// Returns the size (in bytes) of a subset of blobs specified by
520    /// [BlobKeyPrefix]
521    ///
522    /// Can be safely called within retry_external to ensure it succeeds
523    #[cfg(test)]
524    async fn size(
525        &self,
526        prefix: BlobKeyPrefix<'_>,
527    ) -> Result<u64, mz_persist::location::ExternalError> {
528        let mut total_size = 0;
529        self.blob
530            .list_keys_and_metadata(&prefix.to_string(), &mut |metadata| {
531                total_size += metadata.size_in_bytes;
532            })
533            .await?;
534        Ok(total_size)
535    }
536}
537
538#[derive(Debug)]
539struct ShardUsageCumulativeMaybeRacy<'a> {
540    current_state_batches_bytes: u64,
541    current_state_bytes: u64,
542    referenced_other_bytes: u64,
543    referenced_batches_bytes: &'a BTreeMap<WriterKey, u64>,
544    minimum_key: WriterKey,
545    blob_usage: &'a ShardBlobUsage,
546}
547
548impl From<ShardUsageCumulativeMaybeRacy<'_>> for ShardUsageAudit {
549    fn from(x: ShardUsageCumulativeMaybeRacy<'_>) -> Self {
550        let mut not_leaked_bytes = 0;
551        let mut total_bytes = 0;
552        for (writer_key, bytes) in x.blob_usage.by_writer.iter() {
553            total_bytes += *bytes;
554            let writer_key_is_live = *writer_key >= x.minimum_key;
555            if writer_key_is_live {
556                not_leaked_bytes += *bytes;
557            } else {
558                // This writer is no longer live, so it can never again link
559                // anything into state. As a result, we know that anything it
560                // hasn't linked into state is now leaked and eligible for
561                // reclamation by a (future) leaked blob detector.
562                let writer_referenced =
563                    x.referenced_batches_bytes.get(writer_key).map_or(0, |x| *x);
564                // It's possible, due to races, that a writer has more
565                // referenced batches in state than we saw for that writer in
566                // blob. Cap it at the number of bytes we saw in blob, otherwise
567                // we could hit the "blob inputs should be cumulative" panic
568                // below.
569                not_leaked_bytes += std::cmp::min(*bytes, writer_referenced);
570            }
571        }
572        // For now, assume rollups aren't leaked. We could compute which rollups
573        // are leaked by plumbing things more precisely, if that's necessary.
574        total_bytes += x.blob_usage.rollup_bytes;
575        not_leaked_bytes += x.blob_usage.rollup_bytes;
576
577        let leaked_bytes = total_bytes
578            .checked_sub(not_leaked_bytes)
579            .expect("blob inputs should be cumulative");
580        let referenced_batches_bytes = x.referenced_batches_bytes.values().sum::<u64>();
581        let referenced_bytes = referenced_batches_bytes + x.referenced_other_bytes;
582        let mut referenced_not_current_state_bytes = referenced_bytes
583            .checked_sub(x.current_state_bytes)
584            .expect("state inputs should be cumulative");
585        let mut current_state_rollups_bytes = x
586            .current_state_bytes
587            .checked_sub(x.current_state_batches_bytes)
588            .expect("state inputs should be cumulative");
589        let mut current_state_batches_bytes = x.current_state_batches_bytes;
590
591        // If we could transactionally read both blob and consensus, the
592        // cumulative numbers would all line up. We can't, so we have to adjust
593        // them up a bit to account for the race condition. We read blob first,
594        // and then consensus, but the race could go either way: a blob that is
595        // currently in state could be deleted from both in between the reads,
596        // OR a blob could be written and linked into state in between the
597        // reads. We could do a blob-state-blob sandwich, and then use
598        // differences between the two blob reads to reason about what
599        // specifically happens in a race, but this: (a) takes memory
600        // proportional to `O(blobs)` and (b) is overkill. Instead, we adjust by
601        // category.
602        //
603        // In the event of a discrepancy, we ensure that numbers will only get
604        // smaller (by policy, we prefer to under-count for billing).
605        // Concretely:
606        // - If referenced_bytes (which comes from state) is > not_leaked_bytes
607        //   (which is a subset of what we read from blob), then we've
608        //   definitely hit the race and the funnel doesn't make sense (some of
609        //   the things that are supposed to be smaller are actually bigger).
610        //   Figure out how much we have to fix up the numbers and call it
611        //   "possible_over_count".
612        // - Then go "down" ("up"?) the funnel category by category (each of
613        //   which represented here by diffs from the previous category)
614        //   reducing them until we've adjusted them collectively down by
615        //   "possible_over_count".
616        // - First is not_leaked_not_referenced_bytes (the diff from
617        //   referenced_bytes to not_leaked_bytes).
618        // - Then, if necessary, carry the adjustment to
619        //   referenced_not_current_state_bytes (the diff from
620        //   current_state_bytes to referenced_bytes).
621        // - And so on.
622        // - Note that the largest possible value for possible_over_count is
623        //   referenced_bytes (e.g. if we read nothing from blob). Because all
624        //   the diffs add up to referenced_bytes, we're guaranteed that
625        //   "possible_over_count" will have reached 0 by the time we've
626        //   finished adjusting all the categories.
627        let mut not_leaked_not_referenced_bytes = not_leaked_bytes.saturating_sub(referenced_bytes);
628        let mut possible_over_count = referenced_bytes.saturating_sub(not_leaked_bytes);
629        fn adjust(adjustment: &mut u64, val: &mut u64) {
630            let x = std::cmp::min(*adjustment, *val);
631            *adjustment -= x;
632            *val -= x;
633        }
634        adjust(
635            &mut possible_over_count,
636            &mut not_leaked_not_referenced_bytes,
637        );
638        adjust(
639            &mut possible_over_count,
640            &mut referenced_not_current_state_bytes,
641        );
642        adjust(&mut possible_over_count, &mut current_state_rollups_bytes);
643        adjust(&mut possible_over_count, &mut current_state_batches_bytes);
644        assert_eq!(possible_over_count, 0);
645
646        let ret = ShardUsageAudit {
647            current_state_batches_bytes,
648            current_state_rollups_bytes,
649            referenced_not_current_state_bytes,
650            not_leaked_not_referenced_bytes,
651            leaked_bytes,
652        };
653
654        // These ones are guaranteed to be equal.
655        mz_ore::soft_assert_eq_no_log!(ret.total_bytes(), total_bytes);
656        mz_ore::soft_assert_eq_no_log!(ret.not_leaked_bytes(), not_leaked_bytes);
657        // The rest might have been reduced because of the race condition.
658        mz_ore::soft_assert_no_log!(ret.referenced_bytes() <= referenced_bytes);
659        mz_ore::soft_assert_no_log!(ret.current_state_bytes() <= x.current_state_bytes);
660        mz_ore::soft_assert_no_log!(
661            ret.current_state_batches_bytes <= x.current_state_batches_bytes
662        );
663        ret
664    }
665}
666
667impl std::fmt::Display for ShardUsageAudit {
668    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
669        write!(
670            f,
671            concat!(
672                "total s3 contents:                  {}\n",
673                "  leaked:                           {}\n",
674                "  not leaked:                       {}\n",
675                "    not leaked not referenced:      {}\n",
676                "    referenced:                     {}\n",
677                "      referenced not current state: {}\n",
678                "      current state:                {}\n",
679                "        current rollups:            {}\n",
680                "        current batches:            {}",
681            ),
682            HumanBytes(self.total_bytes()),
683            HumanBytes(self.leaked_bytes),
684            HumanBytes(self.not_leaked_bytes()),
685            HumanBytes(self.not_leaked_not_referenced_bytes),
686            HumanBytes(self.referenced_bytes()),
687            HumanBytes(self.referenced_not_current_state_bytes),
688            HumanBytes(self.current_state_bytes()),
689            HumanBytes(self.current_state_rollups_bytes),
690            HumanBytes(self.current_state_batches_bytes),
691        )
692    }
693}
694
695pub(crate) struct HumanBytes(pub u64);
696
697impl std::fmt::Display for HumanBytes {
698    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
699        if self.0 < 1_240u64 {
700            return write!(f, "{}B", self.0);
701        }
702        #[allow(clippy::as_conversions)]
703        let mut bytes = self.0 as f64 / 1_024f64;
704        if bytes < 1_240f64 {
705            return write!(f, "{:.1}KiB", bytes);
706        }
707        bytes = bytes / 1_024f64;
708        if bytes < 1_240f64 {
709            return write!(f, "{:.1}MiB", bytes);
710        }
711        bytes = bytes / 1_024f64;
712        if bytes < 1_240f64 {
713            return write!(f, "{:.1}GiB", bytes);
714        }
715        bytes = bytes / 1_024f64;
716        write!(f, "{:.1}TiB", bytes)
717    }
718}
719
720#[cfg(test)]
721mod tests {
722    use bytes::Bytes;
723    use mz_dyncfg::ConfigUpdates;
724    use mz_persist::location::SeqNo;
725    use semver::Version;
726    use timely::progress::Antichain;
727
728    use crate::ShardId;
729    use crate::batch::{
730        BLOB_TARGET_SIZE, BatchBuilderConfig, INLINE_WRITES_SINGLE_MAX_BYTES,
731        INLINE_WRITES_TOTAL_MAX_BYTES,
732    };
733    use crate::internal::paths::{PartialRollupKey, RollupId};
734    use crate::tests::new_test_client;
735
736    use super::*;
737
738    #[mz_persist_proc::test(tokio::test)]
739    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
740    async fn size(dyncfgs: ConfigUpdates) {
741        let data = [
742            (("1".to_owned(), "one".to_owned()), 1, 1),
743            (("2".to_owned(), "two".to_owned()), 2, 1),
744            (("3".to_owned(), "three".to_owned()), 3, 1),
745            (("4".to_owned(), "four".to_owned()), 4, 1),
746        ];
747
748        let client = new_test_client(&dyncfgs).await;
749        let inline_writes_enabled = INLINE_WRITES_SINGLE_MAX_BYTES.get(&client.cfg) > 0;
750        let build_version = client.cfg.build_version.clone();
751        let shard_id_one = ShardId::new();
752        let shard_id_two = ShardId::new();
753
754        // write one row into shard 1
755        let (mut write, _) = client
756            .expect_open::<String, String, u64, i64>(shard_id_one)
757            .await;
758        write.expect_append(&data[..1], vec![0], vec![2]).await;
759
760        // write two rows into shard 2 from writer 1
761        let (mut write, _) = client
762            .expect_open::<String, String, u64, i64>(shard_id_two)
763            .await;
764        write.expect_append(&data[1..3], vec![0], vec![4]).await;
765        let writer_one = WriterKey::Id(write.writer_id.clone());
766
767        // write one row into shard 2 from writer 2
768        let (mut write, _) = client
769            .expect_open::<String, String, u64, i64>(shard_id_two)
770            .await;
771        write.expect_append(&data[4..], vec![0], vec![5]).await;
772        let writer_two = WriterKey::Id(write.writer_id.clone());
773
774        let usage = StorageUsageClient::open(client);
775
776        let shard_one_size = usage
777            .size(BlobKeyPrefix::Shard(&shard_id_one))
778            .await
779            .expect("must have shard size");
780        let shard_two_size = usage
781            .size(BlobKeyPrefix::Shard(&shard_id_two))
782            .await
783            .expect("must have shard size");
784        let writer_one_size = usage
785            .size(BlobKeyPrefix::Writer(&shard_id_two, &writer_one))
786            .await
787            .expect("must have shard size");
788        let writer_two_size = usage
789            .size(BlobKeyPrefix::Writer(&shard_id_two, &writer_two))
790            .await
791            .expect("must have shard size");
792        let versioned_size = usage
793            .size(BlobKeyPrefix::Writer(
794                &shard_id_two,
795                &WriterKey::for_version(&build_version),
796            ))
797            .await
798            .expect("must have shard size");
799        let rollups_size = usage
800            .size(BlobKeyPrefix::Rollups(&shard_id_two))
801            .await
802            .expect("must have shard size");
803        let all_size = usage
804            .size(BlobKeyPrefix::All)
805            .await
806            .expect("must have shard size");
807
808        assert!(shard_one_size > 0);
809        assert!(shard_two_size > 0);
810        if inline_writes_enabled {
811            // Allow equality, but only if inline writes are enabled.
812            assert!(shard_one_size <= shard_two_size);
813        } else {
814            assert!(shard_one_size < shard_two_size);
815        }
816        assert_eq!(
817            shard_two_size,
818            writer_one_size + writer_two_size + versioned_size + rollups_size
819        );
820        assert_eq!(all_size, shard_one_size + shard_two_size);
821
822        assert_eq!(
823            usage.shard_usage_audit(shard_id_one).await.total_bytes(),
824            shard_one_size
825        );
826        assert_eq!(
827            usage.shard_usage_audit(shard_id_two).await.total_bytes(),
828            shard_two_size
829        );
830
831        let shards_usage = usage.shards_usage_audit().await;
832        assert_eq!(shards_usage.by_shard.len(), 2);
833        assert_eq!(
834            shards_usage
835                .by_shard
836                .get(&shard_id_one)
837                .map(|x| x.total_bytes()),
838            Some(shard_one_size)
839        );
840        assert_eq!(
841            shards_usage
842                .by_shard
843                .get(&shard_id_two)
844                .map(|x| x.total_bytes()),
845            Some(shard_two_size)
846        );
847    }
848
849    /// This is just a sanity check for the overall flow of computing ShardUsage.
850    /// The edge cases are exercised in separate tests.
851    #[mz_persist_proc::test(tokio::test)]
852    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
853    async fn usage_sanity(dyncfgs: ConfigUpdates) {
854        let data = [
855            (("1".to_owned(), "one".to_owned()), 1, 1),
856            (("2".to_owned(), "two".to_owned()), 2, 1),
857            (("3".to_owned(), "three".to_owned()), 3, 1),
858            (("4".to_owned(), "four".to_owned()), 4, 1),
859        ];
860
861        let shard_id = ShardId::new();
862        let mut client = new_test_client(&dyncfgs).await;
863        let inline_writes_enabled = INLINE_WRITES_SINGLE_MAX_BYTES.get(&client.cfg) > 0;
864
865        let (mut write0, _) = client
866            .expect_open::<String, String, u64, i64>(shard_id)
867            .await;
868        // Successfully link in a batch from a writer that stays registered.
869        write0.expect_compare_and_append(&data[..2], 0, 3).await;
870        // Leak a batch from a writer that stays registered.
871        let batch = write0
872            .batch(&data[..2], Antichain::from_elem(0), Antichain::from_elem(3))
873            .await
874            .unwrap();
875        std::mem::forget(batch);
876
877        let (mut write1, _) = client
878            .expect_open::<String, String, u64, i64>(shard_id)
879            .await;
880
881        // Successfully link in a batch from a writer that gets expired.
882        write1.expect_compare_and_append(&data[2..], 3, 5).await;
883        // Leak a batch from a writer that gets expired.
884        let batch = write1
885            .batch(&data[2..], Antichain::from_elem(3), Antichain::from_elem(5))
886            .await
887            .unwrap();
888        std::mem::forget(batch);
889        write1.expire().await;
890
891        // Write a rollup that has an encoded size (the initial rollup has size 0);
892        let maintenance = write0.machine.add_rollup_for_current_seqno().await;
893        maintenance.perform(&write0.machine, &write0.gc).await;
894
895        client.cfg.build_version.minor += 1;
896        let usage = StorageUsageClient::open(client);
897        let shard_usage_audit = usage.shard_usage_audit(shard_id).await;
898        let shard_usage_referenced = usage.shard_usage_referenced(shard_id).await;
899        if !inline_writes_enabled {
900            // We've written data.
901            assert!(shard_usage_audit.current_state_batches_bytes > 0);
902            assert!(shard_usage_referenced.batches_bytes > 0);
903        }
904        // There's always at least one rollup.
905        assert!(shard_usage_audit.current_state_rollups_bytes > 0);
906        assert!(shard_usage_referenced.rollup_bytes > 0);
907        // Sadly, it's tricky (and brittle) to ensure that there is data
908        // referenced by some live state, but no longer referenced by the
909        // current one, so no asserts on referenced_not_current_state_bytes for
910        // now.
911        //
912        // write0 wrote a batch, but never linked it in, but is still active.
913        assert!(shard_usage_audit.not_leaked_not_referenced_bytes > 0);
914        if !inline_writes_enabled {
915            // write0 wrote a batch, but never linked it in, and is now expired.
916            assert!(shard_usage_audit.leaked_bytes > 0);
917        }
918    }
919
920    #[mz_persist_proc::test(tokio::test)]
921    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
922    async fn usage_referenced(dyncfgs: ConfigUpdates) {
923        mz_ore::test::init_logging();
924
925        let data = [
926            (("1".to_owned(), "one".to_owned()), 1, 1),
927            (("2".to_owned(), "two".to_owned()), 2, 1),
928            (("3".to_owned(), "three".to_owned()), 3, 1),
929            (("4".to_owned(), "four".to_owned()), 4, 1),
930        ];
931
932        let shard_id = ShardId::new();
933        let mut client = new_test_client(&dyncfgs).await;
934        // make our bookkeeping simple by skipping compaction blobs writes
935        client.cfg.compaction_enabled = false;
936        // make things interesting and create multiple parts per batch
937        client.cfg.set_config(&BLOB_TARGET_SIZE, 0);
938        // Inline write backpressure will change the encoded size, but the CaAB
939        // call consumes the Batch, so we don't have any way of getting the new
940        // one. So, sniff out whether backpressure would flush out the part and
941        // do it before we get the sizes.
942        let backpressure_would_flush = INLINE_WRITES_TOTAL_MAX_BYTES.get(&client.cfg) == 0;
943
944        let (mut write, _read) = client
945            .expect_open::<String, String, u64, i64>(shard_id)
946            .await;
947
948        let mut b1 = write.expect_batch(&data[..2], 0, 3).await;
949        let mut b2 = write.expect_batch(&data[2..], 2, 5).await;
950        if backpressure_would_flush {
951            let cfg = BatchBuilderConfig::new(&client.cfg, shard_id);
952            b1.flush_to_blob(
953                &cfg,
954                &client.metrics.user,
955                &client.isolated_runtime,
956                &write.write_schemas,
957            )
958            .await;
959            b2.flush_to_blob(
960                &cfg,
961                &client.metrics.user,
962                &client.isolated_runtime,
963                &write.write_schemas,
964            )
965            .await;
966        }
967
968        let batches_size =
969            u64::cast_from(b1.batch.encoded_size_bytes() + b2.batch.encoded_size_bytes());
970
971        write
972            .expect_compare_and_append_batch(&mut [&mut b1], 0, 3)
973            .await;
974        write
975            .expect_compare_and_append_batch(&mut [&mut b2], 3, 5)
976            .await;
977
978        let usage = StorageUsageClient::open(client);
979        let shard_usage_referenced = usage.shard_usage_referenced(shard_id).await;
980
981        // with compaction disabled, we can do an exact match on batch part byte size
982        assert_eq!(shard_usage_referenced.batches_bytes, batches_size);
983    }
984
985    struct TestCase {
986        current_state_batches_bytes: u64,
987        current_state_bytes: u64,
988        referenced_other_bytes: u64,
989        referenced_batches_bytes: Vec<(WriterKey, u64)>,
990        min_writer_key: WriterKey,
991        blob_usage_by_writer: Vec<(WriterKey, u64)>,
992        blob_usage_rollups: u64,
993    }
994
995    impl TestCase {
996        #[track_caller]
997        fn run(&self, expected: &str) {
998            let referenced_batches_bytes = self
999                .referenced_batches_bytes
1000                .iter()
1001                .map(|(id, b)| (id.clone(), *b))
1002                .collect();
1003            let blob_usage = ShardBlobUsage {
1004                by_writer: self
1005                    .blob_usage_by_writer
1006                    .iter()
1007                    .map(|(id, b)| (id.clone(), *b))
1008                    .collect(),
1009                rollup_bytes: self.blob_usage_rollups,
1010            };
1011            let input = ShardUsageCumulativeMaybeRacy {
1012                current_state_batches_bytes: self.current_state_batches_bytes,
1013                current_state_bytes: self.current_state_bytes,
1014                referenced_other_bytes: self.referenced_other_bytes,
1015                referenced_batches_bytes: &referenced_batches_bytes,
1016                minimum_key: self.min_writer_key.clone(),
1017                blob_usage: &blob_usage,
1018            };
1019            let usage = ShardUsageAudit::from(input);
1020            let actual = format!(
1021                "{} {}/{} {}/{} {}/{} {}/{}",
1022                usage.total_bytes(),
1023                usage.leaked_bytes,
1024                usage.not_leaked_bytes(),
1025                usage.not_leaked_not_referenced_bytes,
1026                usage.referenced_bytes(),
1027                usage.referenced_not_current_state_bytes,
1028                usage.current_state_bytes(),
1029                usage.current_state_rollups_bytes,
1030                usage.current_state_batches_bytes
1031            );
1032            assert_eq!(actual, expected);
1033        }
1034    }
1035
1036    fn version(minor: u64) -> WriterKey {
1037        WriterKey::for_version(&Version::new(0, minor, 0))
1038    }
1039
1040    #[mz_ore::test]
1041    fn usage_kitchen_sink() {
1042        TestCase {
1043            // - Some data in current batches
1044            current_state_batches_bytes: 1,
1045            // - Some data in current rollups: this - current_state_batches_bytes
1046            current_state_bytes: 2,
1047            // - Some data in a key we couldn't parse: this-(rollup)
1048            //   - This one is unexpected in prod, but it seemed nicer than a
1049            //     panic, ymmv
1050            referenced_other_bytes: 3,
1051            // - Some data written by a still active writer: (a, 4)
1052            // - Some data written by a now-expired writer: (b, 5)
1053            referenced_batches_bytes: vec![(version(3), 4), (version(2), 5)],
1054            min_writer_key: version(3),
1055            // - Some data leaked by a still active writer: (v3, 7) - (a, 4)
1056            // - Some data leaked by a now-expired writer: (v2, 8) - (b, 5)
1057            blob_usage_by_writer: vec![(version(3), 7), (version(2), 8)],
1058            // - Some data in rollups
1059            blob_usage_rollups: 6,
1060        }
1061        .run("21 3/18 6/12 10/2 1/1");
1062    }
1063
1064    #[mz_ore::test]
1065    fn usage_funnel() {
1066        // All data in current_state_batches_bytes
1067        TestCase {
1068            current_state_batches_bytes: 1,
1069            current_state_bytes: 1,
1070            referenced_other_bytes: 0,
1071            referenced_batches_bytes: vec![(version(3), 1)],
1072            min_writer_key: version(3),
1073            blob_usage_by_writer: vec![(version(3), 1)],
1074            blob_usage_rollups: 0,
1075        }
1076        .run("1 0/1 0/1 0/1 0/1");
1077
1078        // All data in current_state_rollups_bytes
1079        TestCase {
1080            current_state_batches_bytes: 0,
1081            current_state_bytes: 1,
1082            referenced_other_bytes: 0,
1083            referenced_batches_bytes: vec![(version(3), 1)],
1084            min_writer_key: version(3),
1085            blob_usage_by_writer: vec![(version(3), 1)],
1086            blob_usage_rollups: 0,
1087        }
1088        .run("1 0/1 0/1 0/1 1/0");
1089
1090        // All data in referenced_not_current_state_bytes
1091        TestCase {
1092            current_state_batches_bytes: 0,
1093            current_state_bytes: 0,
1094            referenced_other_bytes: 0,
1095            referenced_batches_bytes: vec![(version(3), 1)],
1096            min_writer_key: version(3),
1097            blob_usage_by_writer: vec![(version(3), 1)],
1098            blob_usage_rollups: 0,
1099        }
1100        .run("1 0/1 0/1 1/0 0/0");
1101
1102        // All data in not_leaked_not_referenced_bytes
1103        TestCase {
1104            current_state_batches_bytes: 0,
1105            current_state_bytes: 0,
1106            referenced_other_bytes: 0,
1107            referenced_batches_bytes: vec![],
1108            min_writer_key: version(3),
1109            blob_usage_by_writer: vec![(version(3), 1)],
1110            blob_usage_rollups: 0,
1111        }
1112        .run("1 0/1 1/0 0/0 0/0");
1113
1114        // All data in leaked_bytes
1115        TestCase {
1116            current_state_batches_bytes: 0,
1117            current_state_bytes: 0,
1118            referenced_other_bytes: 0,
1119            referenced_batches_bytes: vec![],
1120            min_writer_key: version(3),
1121            blob_usage_by_writer: vec![(version(2), 1)],
1122            blob_usage_rollups: 0,
1123        }
1124        .run("1 1/0 0/0 0/0 0/0");
1125
1126        // No data
1127        TestCase {
1128            current_state_batches_bytes: 0,
1129            current_state_bytes: 0,
1130            referenced_other_bytes: 0,
1131            referenced_batches_bytes: vec![],
1132            min_writer_key: version(3),
1133            blob_usage_by_writer: vec![],
1134            blob_usage_rollups: 0,
1135        }
1136        .run("0 0/0 0/0 0/0 0/0");
1137    }
1138
1139    #[mz_ore::test]
1140    fn usage_races() {
1141        // We took a snapshot of blob, and then before getting our states, a
1142        // bunch of interesting things happened to persist state. We adjust to
1143        // account for the race down the funnel.
1144
1145        // Base case: no race
1146        TestCase {
1147            current_state_batches_bytes: 2,
1148            current_state_bytes: 4,
1149            referenced_other_bytes: 2,
1150            referenced_batches_bytes: vec![(version(3), 4)],
1151            min_writer_key: version(3),
1152            blob_usage_by_writer: vec![(version(3), 8), (version(2), 2)],
1153            blob_usage_rollups: 0,
1154        }
1155        .run("10 2/8 2/6 2/4 2/2");
1156
1157        // Race was enough to affect into leaked
1158        TestCase {
1159            current_state_batches_bytes: 2,
1160            current_state_bytes: 4,
1161            referenced_other_bytes: 2,
1162            referenced_batches_bytes: vec![(version(3), 4)],
1163            min_writer_key: version(3),
1164            blob_usage_by_writer: vec![(version(3), 8), (version(2), 1)],
1165            blob_usage_rollups: 0,
1166        }
1167        .run("9 1/8 2/6 2/4 2/2");
1168
1169        // Race was enough to affect into not_leaked_not_referenced_bytes
1170        TestCase {
1171            current_state_batches_bytes: 2,
1172            current_state_bytes: 4,
1173            referenced_other_bytes: 2,
1174            referenced_batches_bytes: vec![(version(3), 4)],
1175            min_writer_key: version(3),
1176            blob_usage_by_writer: vec![(version(3), 7)],
1177            blob_usage_rollups: 0,
1178        }
1179        .run("7 0/7 1/6 2/4 2/2");
1180
1181        // Race was enough to affect into referenced_not_current_state_bytes
1182        TestCase {
1183            current_state_batches_bytes: 2,
1184            current_state_bytes: 4,
1185            referenced_other_bytes: 2,
1186            referenced_batches_bytes: vec![(version(3), 4)],
1187            min_writer_key: version(3),
1188            blob_usage_by_writer: vec![(version(3), 5)],
1189            blob_usage_rollups: 0,
1190        }
1191        .run("5 0/5 0/5 1/4 2/2");
1192
1193        // Race was enough to affect into current_state_rollups_bytes
1194        TestCase {
1195            current_state_batches_bytes: 2,
1196            current_state_bytes: 4,
1197            referenced_other_bytes: 2,
1198            referenced_batches_bytes: vec![(version(3), 4)],
1199            min_writer_key: version(3),
1200            blob_usage_by_writer: vec![(version(3), 3)],
1201            blob_usage_rollups: 0,
1202        }
1203        .run("3 0/3 0/3 0/3 1/2");
1204
1205        // Race was enough to affect into current_state_batches_bytes
1206        TestCase {
1207            current_state_batches_bytes: 2,
1208            current_state_bytes: 4,
1209            referenced_other_bytes: 2,
1210            referenced_batches_bytes: vec![(version(3), 4)],
1211            min_writer_key: version(3),
1212            blob_usage_by_writer: vec![(version(3), 1)],
1213            blob_usage_rollups: 0,
1214        }
1215        .run("1 0/1 0/1 0/1 0/1");
1216    }
1217
1218    /// A regression test for (part of) database-issues#5170, which led to seeing the "blob
1219    /// inputs should be cumulative" should be cumulative panic in
1220    /// staging/canary.
1221    #[mz_ore::test]
1222    fn usage_regression_referenced_greater_than_blob() {
1223        TestCase {
1224            current_state_batches_bytes: 0,
1225            current_state_bytes: 0,
1226            referenced_other_bytes: 0,
1227            referenced_batches_bytes: vec![(version(3), 5)],
1228            min_writer_key: version(10),
1229            blob_usage_by_writer: vec![(version(3), 3)],
1230            blob_usage_rollups: 0,
1231        }
1232        .run("3 0/3 0/3 3/0 0/0");
1233    }
1234
1235    /// Regression test for (part of) database-issues#5170, where an interrupted
1236    /// `bin/environmentd --reset` resulted in panic in persist usage code.
1237    ///
1238    /// This also tests a (hypothesized) race that's possible in prod where an
1239    /// initial rollup is written for a shard, but the initial CaS hasn't yet
1240    /// succeeded.
1241    #[mz_persist_proc::test(tokio::test)]
1242    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
1243    async fn usage_regression_shard_in_blob_not_consensus(dyncfgs: ConfigUpdates) {
1244        let client = new_test_client(&dyncfgs).await;
1245        let shard_id = ShardId::new();
1246
1247        // Somewhat unsatisfying, we manually construct a rollup blob key.
1248        let key = PartialRollupKey::new(SeqNo(1), &RollupId::new());
1249        let key = key.complete(&shard_id);
1250        let () = client
1251            .blob
1252            .set(&key, Bytes::from(vec![0, 1, 2]))
1253            .await
1254            .unwrap();
1255        let usage = StorageUsageClient::open(client);
1256        let shards_usage = usage.shards_usage_audit().await;
1257        assert_eq!(shards_usage.by_shard.len(), 1);
1258        assert_eq!(
1259            shards_usage.by_shard.get(&shard_id).unwrap().leaked_bytes,
1260            3
1261        );
1262    }
1263}