1use 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#[derive(Clone, Debug)]
31pub struct ShardUsageReferenced {
32 pub(crate) batches_bytes: u64,
33 pub(crate) rollup_bytes: u64,
34}
35
36impl ShardUsageReferenced {
37 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#[derive(Debug)]
49pub struct ShardsUsageReferenced {
50 pub by_shard: BTreeMap<ShardId, ShardUsageReferenced>,
52}
53
54#[derive(Clone, Debug)]
61pub struct ShardUsageAudit {
62 pub current_state_batches_bytes: u64,
65 pub current_state_rollups_bytes: u64,
67 pub referenced_not_current_state_bytes: u64,
74 pub not_leaked_not_referenced_bytes: u64,
83 pub leaked_bytes: u64,
90}
91
92impl ShardUsageAudit {
93 pub fn current_state_bytes(&self) -> u64 {
95 self.current_state_batches_bytes + self.current_state_rollups_bytes
96 }
97
98 pub fn referenced_bytes(&self) -> u64 {
100 self.current_state_bytes() + self.referenced_not_current_state_bytes
101 }
102
103 pub fn not_leaked_bytes(&self) -> u64 {
106 self.referenced_bytes() + self.not_leaked_not_referenced_bytes
107 }
108
109 pub fn total_bytes(&self) -> u64 {
114 self.not_leaked_bytes() + self.leaked_bytes
115 }
116}
117
118#[derive(Clone, Debug)]
120pub struct ShardsUsageAudit {
121 pub by_shard: BTreeMap<ShardId, ShardUsageAudit>,
123 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#[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 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 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 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 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 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 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 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 minimum_key: WriterKey::for_version(&self.cfg.build_version),
492 blob_usage,
493 });
494
495 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 #[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 let writer_referenced =
563 x.referenced_batches_bytes.get(writer_key).map_or(0, |x| *x);
564 not_leaked_bytes += std::cmp::min(*bytes, writer_referenced);
570 }
571 }
572 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 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 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 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)] 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 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 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 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 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 #[mz_persist_proc::test(tokio::test)]
852 #[cfg_attr(miri, ignore)] 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 write0.expect_compare_and_append(&data[..2], 0, 3).await;
870 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 write1.expect_compare_and_append(&data[2..], 3, 5).await;
883 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 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 assert!(shard_usage_audit.current_state_batches_bytes > 0);
902 assert!(shard_usage_referenced.batches_bytes > 0);
903 }
904 assert!(shard_usage_audit.current_state_rollups_bytes > 0);
906 assert!(shard_usage_referenced.rollup_bytes > 0);
907 assert!(shard_usage_audit.not_leaked_not_referenced_bytes > 0);
914 if !inline_writes_enabled {
915 assert!(shard_usage_audit.leaked_bytes > 0);
917 }
918 }
919
920 #[mz_persist_proc::test(tokio::test)]
921 #[cfg_attr(miri, ignore)] 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 client.cfg.compaction_enabled = false;
936 client.cfg.set_config(&BLOB_TARGET_SIZE, 0);
938 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 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 current_state_batches_bytes: 1,
1045 current_state_bytes: 2,
1047 referenced_other_bytes: 3,
1051 referenced_batches_bytes: vec![(version(3), 4), (version(2), 5)],
1054 min_writer_key: version(3),
1055 blob_usage_by_writer: vec![(version(3), 7), (version(2), 8)],
1058 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 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 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 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 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 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 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 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 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 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 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 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 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 #[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 #[mz_persist_proc::test(tokio::test)]
1242 #[cfg_attr(miri, ignore)] 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 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}