1use std::cmp;
13use std::fmt::{Debug, Formatter};
14use std::ops::Range;
15use std::sync::Arc;
16use std::sync::atomic::{self, AtomicU64};
17use std::time::{Duration, Instant};
18
19use anyhow::{Context, anyhow};
20use async_trait::async_trait;
21use aws_config::sts::AssumeRoleProvider;
22use aws_config::timeout::TimeoutConfig;
23use aws_credential_types::Credentials;
24use aws_sdk_s3::Client as S3Client;
25use aws_sdk_s3::config::{AsyncSleep, Sleep};
26use aws_sdk_s3::error::{ProvideErrorMetadata, SdkError};
27use aws_sdk_s3::primitives::ByteStream;
28use aws_sdk_s3::types::{CompletedMultipartUpload, CompletedPart};
29use aws_types::region::Region;
30use bytes::{Bytes, BytesMut};
31use futures_util::stream::FuturesOrdered;
32use futures_util::{FutureExt, StreamExt};
33use mz_ore::bytes::SegmentedBytes;
34use mz_ore::cast::CastFrom;
35use mz_ore::metrics::MetricsRegistry;
36use mz_ore::task::RuntimeExt;
37use tokio::runtime::Handle as AsyncHandle;
38use tracing::{Instrument, debug, debug_span, trace, trace_span};
39use uuid::Uuid;
40
41use crate::cfg::BlobKnobs;
42use crate::error::Error;
43use crate::location::{Blob, BlobMetadata, Determinate, ExternalError};
44use crate::metrics::S3BlobMetrics;
45
46#[derive(Clone, Debug)]
52pub struct S3BlobConfig {
53 metrics: S3BlobMetrics,
54 client: S3Client,
55 bucket: String,
56 prefix: String,
57}
58
59const OPERATION_TIMEOUT_MARKER: Duration = Duration::new(111, 1111);
64const OPERATION_ATTEMPT_TIMEOUT_MARKER: Duration = Duration::new(222, 2222);
65const CONNECT_TIMEOUT_MARKER: Duration = Duration::new(333, 3333);
66const READ_TIMEOUT_MARKER: Duration = Duration::new(444, 4444);
67
68#[derive(Debug)]
69struct MetricsSleep {
70 knobs: Box<dyn BlobKnobs>,
71 metrics: S3BlobMetrics,
72}
73
74impl AsyncSleep for MetricsSleep {
75 fn sleep(&self, duration: Duration) -> Sleep {
76 let (duration, metric) = match duration {
77 OPERATION_TIMEOUT_MARKER => (
78 self.knobs.operation_timeout(),
79 Some(self.metrics.operation_timeouts.clone()),
80 ),
81 OPERATION_ATTEMPT_TIMEOUT_MARKER => (
82 self.knobs.operation_attempt_timeout(),
83 Some(self.metrics.operation_attempt_timeouts.clone()),
84 ),
85 CONNECT_TIMEOUT_MARKER => (
86 self.knobs.connect_timeout(),
87 Some(self.metrics.connect_timeouts.clone()),
88 ),
89 READ_TIMEOUT_MARKER => (
90 self.knobs.read_timeout(),
91 Some(self.metrics.read_timeouts.clone()),
92 ),
93 duration => (duration, None),
94 };
95
96 Sleep::new(tokio::time::sleep(duration).map(|x| {
101 if let Some(counter) = metric {
102 counter.inc();
103 }
104 x
105 }))
106 }
107}
108
109impl S3BlobConfig {
110 const EXTERNAL_TESTS_S3_BUCKET: &'static str = "MZ_PERSIST_EXTERNAL_STORAGE_TEST_S3_BUCKET";
111
112 pub async fn new(
118 bucket: String,
119 prefix: String,
120 role_arn: Option<String>,
121 endpoint: Option<String>,
122 region: Option<String>,
123 credentials: Option<(String, String)>,
124 knobs: Box<dyn BlobKnobs>,
125 metrics: S3BlobMetrics,
126 ) -> Result<Self, Error> {
127 let mut loader = mz_aws_util::defaults();
128
129 if let Some(region) = region {
130 loader = loader.region(Region::new(region));
131 };
132
133 if let Some(role_arn) = role_arn {
134 let assume_role_sdk_config = mz_aws_util::defaults().load().await;
135 let role_provider = AssumeRoleProvider::builder(role_arn)
136 .configure(&assume_role_sdk_config)
137 .session_name("persist")
138 .build()
139 .await;
140 loader = loader.credentials_provider(role_provider);
141 }
142
143 if let Some((access_key_id, secret_access_key)) = credentials {
144 loader = loader.credentials_provider(Credentials::from_keys(
145 access_key_id,
146 secret_access_key,
147 None,
148 ));
149 }
150
151 if let Some(endpoint) = endpoint {
152 loader = loader.endpoint_url(endpoint)
153 }
154
155 loader = loader.sleep_impl(MetricsSleep {
157 knobs,
158 metrics: metrics.clone(),
159 });
160 loader = loader.timeout_config(
161 TimeoutConfig::builder()
162 .operation_timeout(OPERATION_TIMEOUT_MARKER)
164 .operation_attempt_timeout(OPERATION_ATTEMPT_TIMEOUT_MARKER)
166 .connect_timeout(CONNECT_TIMEOUT_MARKER)
168 .read_timeout(READ_TIMEOUT_MARKER)
170 .build(),
171 );
172
173 let client = mz_aws_util::s3::new_client(&loader.load().await);
174 Ok(S3BlobConfig {
175 metrics,
176 client,
177 bucket,
178 prefix,
179 })
180 }
181
182 pub async fn new_for_test() -> Result<Option<Self>, Error> {
219 let bucket = match std::env::var(Self::EXTERNAL_TESTS_S3_BUCKET) {
220 Ok(bucket) => bucket,
221 Err(_) => {
222 if mz_ore::env::is_var_truthy("CI") {
223 panic!("CI is supposed to run this test but something has gone wrong!");
224 }
225 return Ok(None);
226 }
227 };
228
229 struct TestBlobKnobs;
230 impl std::fmt::Debug for TestBlobKnobs {
231 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
232 f.debug_struct("TestBlobKnobs").finish_non_exhaustive()
233 }
234 }
235 impl BlobKnobs for TestBlobKnobs {
236 fn operation_timeout(&self) -> Duration {
237 OPERATION_TIMEOUT_MARKER
238 }
239
240 fn operation_attempt_timeout(&self) -> Duration {
241 OPERATION_ATTEMPT_TIMEOUT_MARKER
242 }
243
244 fn connect_timeout(&self) -> Duration {
245 CONNECT_TIMEOUT_MARKER
246 }
247
248 fn read_timeout(&self) -> Duration {
249 READ_TIMEOUT_MARKER
250 }
251
252 fn is_cc_active(&self) -> bool {
253 false
254 }
255 }
256
257 let prefix = Uuid::new_v4().to_string();
261 let role_arn = None;
262 let metrics = S3BlobMetrics::new(&MetricsRegistry::new());
263 let config = S3BlobConfig::new(
264 bucket,
265 prefix,
266 role_arn,
267 None,
268 None,
269 None,
270 Box::new(TestBlobKnobs),
271 metrics,
272 )
273 .await?;
274 Ok(Some(config))
275 }
276
277 pub fn clone_with_new_uuid_prefix(&self) -> Self {
279 let mut ret = self.clone();
280 ret.prefix = Uuid::new_v4().to_string();
281 ret
282 }
283}
284
285#[derive(Debug)]
287pub struct S3Blob {
288 metrics: S3BlobMetrics,
289 client: S3Client,
290 bucket: String,
291 prefix: String,
292 max_keys: i32,
296 multipart_config: MultipartConfig,
297}
298
299impl S3Blob {
300 pub async fn open(config: S3BlobConfig) -> Result<Self, ExternalError> {
302 let ret = S3Blob {
303 metrics: config.metrics,
304 client: config.client,
305 bucket: config.bucket,
306 prefix: config.prefix,
307 max_keys: 1_000,
308 multipart_config: MultipartConfig::default(),
309 };
310 let _ = ret.get("HEALTH_CHECK").await?;
314 Ok(ret)
315 }
316
317 fn get_path(&self, key: &str) -> String {
318 format!("{}/{}", self.prefix, key)
319 }
320}
321
322#[async_trait]
323impl Blob for S3Blob {
324 async fn get(&self, key: &str) -> Result<Option<SegmentedBytes>, ExternalError> {
325 let start_overall = Instant::now();
326 let path = self.get_path(key);
327
328 let min_body_elapsed = Arc::new(MinElapsed::default());
363 let min_header_elapsed = Arc::new(MinElapsed::default());
364 self.metrics.get_part.inc();
365
366 let header_start = Instant::now();
368 let object = self
369 .client
370 .get_object()
371 .bucket(&self.bucket)
372 .key(&path)
373 .part_number(1)
374 .send()
375 .await;
376 let elapsed = header_start.elapsed();
377 min_header_elapsed.observe(elapsed, "s3 download first part header");
378
379 let first_part = match object {
380 Ok(object) => object,
381 Err(SdkError::ServiceError(err)) if err.err().is_no_such_key() => return Ok(None),
382 Err(err) => {
383 self.update_error_metrics("GetObject", &err);
384 Err(anyhow!(err).context("s3 get meta err"))?
385 }
386 };
387
388 let num_parts = match first_part.parts_count() {
390 None => 1,
393 Some(parts @ 1..) => parts,
395 Some(bad) => {
397 assert!(bad <= 0);
398 return Err(anyhow!("unexpected number of s3 object parts: {}", bad).into());
399 }
400 };
401
402 trace!(
403 "s3 download first header took {:?} ({num_parts} parts)",
404 start_overall.elapsed(),
405 );
406
407 let mut body_futures = FuturesOrdered::new();
408 let mut first_part = Some(first_part);
409
410 for part_num in 1..=num_parts {
413 let min_header_elapsed = Arc::clone(&min_header_elapsed);
416 let min_body_elapsed = Arc::clone(&min_body_elapsed);
417 let get_invalid_resp = self.metrics.get_invalid_resp.clone();
418 let first_part = first_part.take();
419 let path = &path;
420 let request_future = async move {
421 let mut object = match first_part {
424 Some(first_part) => {
425 assert_eq!(part_num, 1, "only the first part should be prefetched");
426 first_part
427 }
428 None => {
429 assert_ne!(part_num, 1, "first part should be prefetched");
430 let header_start = Instant::now();
432 let object = self
433 .client
434 .get_object()
435 .bucket(&self.bucket)
436 .key(path)
437 .part_number(part_num)
438 .send()
439 .await
440 .inspect_err(|err| self.update_error_metrics("GetObject", err))
441 .context("s3 get meta err")?;
442 min_header_elapsed
443 .observe(header_start.elapsed(), "s3 download part header");
444 object
445 }
446 };
447
448 let body_start = Instant::now();
450
451 let mut buf = match object.content_length() {
459 Some(len @ 1..) => BytesMut::with_capacity(usize::cast_from(
460 u64::try_from(len).expect("positive integer"),
461 )),
462 Some(len @ ..=-1) => {
463 tracing::trace!(?len, "found invalid content-length");
464 get_invalid_resp.inc();
465 BytesMut::new()
466 }
467 Some(0) | None => BytesMut::new(),
468 };
469
470 while let Some(data) = object.body.next().await {
471 let data = data.context("s3 get body err")?;
472 buf.extend_from_slice(&data);
473 }
474
475 let body_elapsed = body_start.elapsed();
476 min_body_elapsed.observe(body_elapsed, "s3 download part body");
477
478 let body_parts = if buf.is_empty() {
479 Vec::new()
480 } else {
481 vec![buf.freeze()]
482 };
483 Ok::<_, anyhow::Error>(body_parts)
484 };
485
486 body_futures.push_back(request_future);
487 }
488
489 let mut segments = vec![];
491 while let Some(result) = body_futures.next().await {
492 let mut part_body = result
494 .inspect_err(|e| {
495 self.metrics
496 .error_counts
497 .with_label_values(&["GetObjectStream", e.to_string().as_str()])
498 .inc()
499 })
500 .context("s3 get body err")?;
501
502 segments.append(&mut part_body);
504 }
505
506 debug!(
507 "s3 GetObject took {:?} ({} parts)",
508 start_overall.elapsed(),
509 num_parts
510 );
511 Ok(Some(SegmentedBytes::from(segments)))
512 }
513
514 async fn list_keys_and_metadata(
515 &self,
516 key_prefix: &str,
517 f: &mut (dyn FnMut(BlobMetadata) + Send + Sync),
518 ) -> Result<(), ExternalError> {
519 let mut continuation_token = None;
520 let blob_key_prefix = self.get_path(key_prefix);
522 let strippable_root_prefix = format!("{}/", self.prefix);
525
526 loop {
527 self.metrics.list_objects.inc();
528 let resp = self
529 .client
530 .list_objects_v2()
531 .bucket(&self.bucket)
532 .prefix(&blob_key_prefix)
533 .max_keys(self.max_keys)
534 .set_continuation_token(continuation_token)
535 .send()
536 .await
537 .inspect_err(|err| self.update_error_metrics("ListObjectsV2", err))
538 .context("list bucket error")?;
539 if let Some(contents) = resp.contents {
540 for object in contents.iter() {
541 if let Some(key) = object.key.as_ref() {
542 if let Some(key) = key.strip_prefix(&strippable_root_prefix) {
543 let size_in_bytes = match object.size {
544 None => {
545 return Err(ExternalError::from(anyhow!(
546 "object missing size: {key}"
547 )));
548 }
549 Some(size) => size
550 .try_into()
551 .expect("file in S3 cannot have negative size"),
552 };
553 f(BlobMetadata { key, size_in_bytes });
554 } else {
555 return Err(ExternalError::from(anyhow!(
556 "found key with invalid prefix: {}",
557 key
558 )));
559 }
560 }
561 }
562 }
563
564 if resp.next_continuation_token.is_some() {
565 continuation_token = resp.next_continuation_token;
566 } else {
567 break;
568 }
569 }
570
571 Ok(())
572 }
573
574 async fn set(&self, key: &str, value: Bytes) -> Result<(), ExternalError> {
575 let value_len = value.len();
576 if self
577 .multipart_config
578 .should_multipart(value_len)
579 .map_err(anyhow::Error::msg)?
580 {
581 self.set_multi_part(key, value)
582 .instrument(debug_span!("s3set_multi", payload_len = value_len))
583 .await
584 } else {
585 self.set_single_part(key, value).await
586 }
587 }
588
589 async fn delete(&self, key: &str) -> Result<Option<usize>, ExternalError> {
590 let path = self.get_path(key);
595 self.metrics.delete_head.inc();
596 let head_res = self
597 .client
598 .head_object()
599 .bucket(&self.bucket)
600 .key(&path)
601 .send()
602 .await;
603 let size_bytes = match head_res {
604 Ok(x) => match x.content_length {
605 None => {
606 return Err(ExternalError::from(anyhow!(
607 "s3 delete content length was none"
608 )));
609 }
610 Some(content_length) => {
611 u64::try_from(content_length).expect("file in S3 cannot have negative size")
612 }
613 },
614 Err(SdkError::ServiceError(err)) if err.err().is_not_found() => return Ok(None),
615 Err(err) => {
616 self.update_error_metrics("HeadObject", &err);
617 return Err(ExternalError::from(
618 anyhow!(err).context("s3 delete head err"),
619 ));
620 }
621 };
622 self.metrics.delete_object.inc();
623 let _ = self
624 .client
625 .delete_object()
626 .bucket(&self.bucket)
627 .key(&path)
628 .send()
629 .await
630 .inspect_err(|err| self.update_error_metrics("DeleteObject", err))
631 .context("s3 delete object err")?;
632 Ok(Some(usize::cast_from(size_bytes)))
633 }
634
635 async fn restore(&self, key: &str) -> Result<(), ExternalError> {
636 let path = self.get_path(key);
637 loop {
642 let list_res = self
646 .client
647 .list_object_versions()
648 .bucket(&self.bucket)
649 .prefix(&path)
650 .max_keys(1)
651 .send()
652 .await
653 .inspect_err(|err| self.update_error_metrics("ListObjectVersions", err))
654 .context("listing object versions during restore")?;
655
656 let current_delete = list_res
657 .delete_markers()
658 .into_iter()
659 .filter(|d| {
660 d.key() == Some(path.as_str())
663 })
664 .find(|d| d.is_latest().unwrap_or(false))
665 .and_then(|d| d.version_id());
666
667 if let Some(version) = current_delete {
668 let deleted = self
669 .client
670 .delete_object()
671 .bucket(&self.bucket)
672 .key(&path)
673 .version_id(version)
674 .send()
675 .await
676 .inspect_err(|err| self.update_error_metrics("DeleteObject", err))
677 .context("deleting a delete marker")?;
678 assert!(
679 deleted.delete_marker().unwrap_or(false),
680 "deleting a delete marker"
681 );
682 } else {
683 let has_current_version = list_res
684 .versions()
685 .into_iter()
686 .filter(|d| d.key() == Some(path.as_str()))
687 .any(|v| v.is_latest().unwrap_or(false));
688
689 if !has_current_version {
690 return Err(Determinate::new(anyhow!(
691 "unable to restore {key} in s3: no valid version exists"
692 ))
693 .into());
694 }
695 return Ok(());
696 }
697 }
698 }
699}
700
701impl S3Blob {
702 async fn set_single_part(&self, key: &str, value: Bytes) -> Result<(), ExternalError> {
703 let start_overall = Instant::now();
704 let path = self.get_path(key);
705
706 let value_len = value.len();
707 let part_span = trace_span!("s3set_single", payload_len = value_len);
708 self.metrics.set_single.inc();
709 self.client
710 .put_object()
711 .bucket(&self.bucket)
712 .key(path)
713 .body(ByteStream::from(value))
714 .send()
715 .instrument(part_span)
716 .await
717 .inspect_err(|err| self.update_error_metrics("PutObject", err))
718 .context("set single part")?;
719 debug!(
720 "s3 PutObject single done {}b / {:?}",
721 value_len,
722 start_overall.elapsed()
723 );
724 Ok(())
725 }
726
727 #[allow(clippy::as_conversions)]
730 async fn set_multi_part(&self, key: &str, value: Bytes) -> Result<(), ExternalError> {
731 let start_overall = Instant::now();
732 let path = self.get_path(key);
733
734 trace!("s3 PutObject multi start {}b", value.len());
736 self.metrics.set_multi_create.inc();
737 let upload_res = self
738 .client
739 .create_multipart_upload()
740 .bucket(&self.bucket)
741 .key(&path)
742 .customize()
743 .mutate_request(|req| {
744 req.headers_mut().insert("Content-Length", "0");
749 })
750 .send()
751 .instrument(debug_span!("s3set_multi_start"))
752 .await
753 .inspect_err(|err| self.update_error_metrics("CreateMultipartUpload", err))
754 .context("create_multipart_upload err")?;
755 let upload_id = upload_res
756 .upload_id()
757 .ok_or_else(|| anyhow!("create_multipart_upload response missing upload_id"))?;
758 trace!(
759 "s3 create_multipart_upload took {:?}",
760 start_overall.elapsed()
761 );
762
763 let async_runtime = AsyncHandle::try_current().map_err(anyhow::Error::new)?;
764
765 let start_parts = Instant::now();
770 let mut part_futs = Vec::new();
771 for (part_num, part_range) in self.multipart_config.part_iter(value.len()) {
772 let part_span = debug_span!("s3set_multi_part", payload_len = part_range.len());
776 let part_fut = async_runtime.spawn_named(
777 || "persist_s3blob_put_part",
780 {
781 self.metrics.set_multi_part.inc();
782 self.client
783 .upload_part()
784 .bucket(&self.bucket)
785 .key(&path)
786 .upload_id(upload_id)
787 .part_number(part_num as i32)
788 .body(ByteStream::from(value.slice(part_range)))
789 .send()
790 .instrument(part_span)
791 .map(move |res| (start_parts.elapsed(), res))
792 },
793 );
794 part_futs.push((part_num, part_fut));
795 }
796 let parts_len = part_futs.len();
797
798 let min_part_elapsed = MinElapsed::default();
806 let mut parts = Vec::with_capacity(parts_len);
807 for (part_num, part_fut) in part_futs.into_iter() {
808 let (this_part_elapsed, part_res) = part_fut.await;
809 let part_res = part_res
810 .inspect_err(|err| self.update_error_metrics("UploadPart", err))
811 .context("s3 upload_part err")?;
812 let part_e_tag = part_res.e_tag().ok_or_else(|| {
813 self.metrics
814 .error_counts
815 .with_label_values(&["UploadPart", "MissingEtag"])
816 .inc();
817 anyhow!("s3 upload part missing e_tag")
818 })?;
819 parts.push(
820 CompletedPart::builder()
821 .e_tag(part_e_tag)
822 .part_number(part_num as i32)
823 .build(),
824 );
825 min_part_elapsed.observe(this_part_elapsed, "s3 upload_part took");
826 }
827 trace!(
828 "s3 upload_parts overall took {:?} ({} parts)",
829 start_parts.elapsed(),
830 parts_len
831 );
832
833 let start_complete = Instant::now();
845 self.metrics.set_multi_complete.inc();
846 self.client
847 .complete_multipart_upload()
848 .bucket(&self.bucket)
849 .key(&path)
850 .upload_id(upload_id)
851 .multipart_upload(
852 CompletedMultipartUpload::builder()
853 .set_parts(Some(parts))
854 .build(),
855 )
856 .send()
857 .instrument(debug_span!("s3set_multi_complete", num_parts = parts_len))
858 .await
859 .inspect_err(|err| self.update_error_metrics("CompleteMultipartUpload", err))
860 .context("complete_multipart_upload err")?;
861 trace!(
862 "s3 complete_multipart_upload took {:?}",
863 start_complete.elapsed()
864 );
865
866 debug!(
867 "s3 PutObject multi done {}b / {:?} ({} parts)",
868 value.len(),
869 start_overall.elapsed(),
870 parts_len
871 );
872 Ok(())
873 }
874
875 fn update_error_metrics<E, R>(&self, op: &str, err: &SdkError<E, R>)
876 where
877 E: ProvideErrorMetadata,
878 {
879 let code = match err {
880 SdkError::ServiceError(e) => match e.err().code() {
881 Some(code) => code,
882 None => "UnknownServiceError",
883 },
884 SdkError::DispatchFailure(e) => {
885 if let Some(other_error) = e.as_other() {
886 match other_error {
887 aws_config::retry::ErrorKind::TransientError => "TransientError",
888 aws_config::retry::ErrorKind::ThrottlingError => "ThrottlingError",
889 aws_config::retry::ErrorKind::ServerError => "ServerError",
890 aws_config::retry::ErrorKind::ClientError => "ClientError",
891 _ => "UnknownDispatchFailure",
892 }
893 } else if e.is_timeout() {
894 "TimeoutError"
895 } else if e.is_io() {
896 "IOError"
897 } else if e.is_user() {
898 "UserError"
899 } else {
900 "UnknownDispathFailure"
901 }
902 }
903 SdkError::ResponseError(_) => "ResponseError",
904 SdkError::ConstructionFailure(_) => "ConstructionFailure",
905 SdkError::TimeoutError(_) => "TimeoutError",
908 _ => "UnknownSdkError",
910 };
911 self.metrics
912 .error_counts
913 .with_label_values(&[op, code])
914 .inc();
915 }
916}
917
918#[derive(Clone, Debug)]
919struct MultipartConfig {
920 multipart_threshold: usize,
921 multipart_chunk_size: usize,
922}
923
924impl Default for MultipartConfig {
925 fn default() -> Self {
926 Self {
927 multipart_threshold: Self::DEFAULT_MULTIPART_THRESHOLD,
928 multipart_chunk_size: Self::DEFAULT_MULTIPART_CHUNK_SIZE,
929 }
930 }
931}
932
933const MB: usize = 1024 * 1024;
934const TB: usize = 1024 * 1024 * MB;
935
936impl MultipartConfig {
937 const DEFAULT_MULTIPART_THRESHOLD: usize = 8 * MB;
943 const DEFAULT_MULTIPART_CHUNK_SIZE: usize = 8 * MB;
949
950 const MAX_SINGLE_UPLOAD_SIZE: usize = 5 * TB;
954 const MIN_UPLOAD_CHUNK_SIZE: usize = 5 * MB;
960 const MIN_PART_NUM: u32 = 1;
964 const MAX_PART_NUM: u32 = 10_000;
968
969 fn should_multipart(&self, blob_len: usize) -> Result<bool, String> {
970 if blob_len > Self::MAX_SINGLE_UPLOAD_SIZE {
971 return Err(format!(
972 "S3 does not support blobs larger than {} bytes got: {}",
973 Self::MAX_SINGLE_UPLOAD_SIZE,
974 blob_len
975 ));
976 }
977 Ok(blob_len > self.multipart_threshold)
978 }
979
980 fn part_iter(&self, blob_len: usize) -> MultipartChunkIter {
981 mz_ore::soft_assert_no_log!(
982 self.multipart_chunk_size >= MultipartConfig::MIN_UPLOAD_CHUNK_SIZE
983 );
984 MultipartChunkIter::new(self.multipart_chunk_size, blob_len)
985 }
986}
987
988#[derive(Clone, Debug)]
989struct MultipartChunkIter {
990 total_len: usize,
991 part_size: usize,
992 part_idx: u32,
993}
994
995impl MultipartChunkIter {
996 fn new(default_part_size: usize, blob_len: usize) -> Self {
997 let max_parts: usize = usize::cast_from(MultipartConfig::MAX_PART_NUM);
998
999 let min_part_size = (blob_len + max_parts - 1) / max_parts;
1004 let part_size = cmp::max(min_part_size, default_part_size);
1005
1006 let part_idx = MultipartConfig::MIN_PART_NUM - 1;
1009 MultipartChunkIter {
1010 total_len: blob_len,
1011 part_size,
1012 part_idx,
1013 }
1014 }
1015}
1016
1017impl Iterator for MultipartChunkIter {
1018 type Item = (u32, Range<usize>);
1019
1020 fn next(&mut self) -> Option<Self::Item> {
1021 let part_idx = self.part_idx;
1022 self.part_idx += 1;
1023
1024 let start = usize::cast_from(part_idx) * self.part_size;
1025 if start >= self.total_len {
1026 return None;
1027 }
1028 let end = cmp::min(start + self.part_size, self.total_len);
1029 let part_num = part_idx + 1;
1030 Some((part_num, start..end))
1031 }
1032}
1033
1034#[derive(Debug)]
1036struct MinElapsed {
1037 min: AtomicU64,
1038 alert_factor: u64,
1039}
1040
1041impl Default for MinElapsed {
1042 fn default() -> Self {
1043 MinElapsed {
1044 min: AtomicU64::new(u64::MAX),
1045 alert_factor: 8,
1046 }
1047 }
1048}
1049
1050impl MinElapsed {
1051 fn observe(&self, x: Duration, msg: &'static str) {
1052 let nanos = x.as_nanos();
1053 let nanos = u64::try_from(nanos).unwrap_or(u64::MAX);
1054
1055 let prev_min = self.min.fetch_min(nanos, atomic::Ordering::SeqCst);
1057
1058 let new_min = std::cmp::min(prev_min, nanos);
1060 if nanos > new_min.saturating_mul(self.alert_factor) {
1061 let min_duration = Duration::from_nanos(new_min);
1062 let factor = self.alert_factor;
1063 debug!("{msg} took {x:?} more than {factor}x the min {min_duration:?}");
1064 } else {
1065 trace!("{msg} took {x:?}");
1066 }
1067 }
1068}
1069
1070#[allow(dead_code)]
1075fn openssl_sys_hack() {
1076 openssl_sys::init();
1077}
1078
1079#[cfg(test)]
1080mod tests {
1081 use tracing::info;
1082
1083 use crate::location::tests::blob_impl_test;
1084
1085 use super::*;
1086
1087 #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
1088 #[cfg_attr(coverage, ignore)] #[cfg_attr(miri, ignore)] #[ignore] async fn s3_blob() -> Result<(), ExternalError> {
1092 let config = match S3BlobConfig::new_for_test().await? {
1093 Some(client) => client,
1094 None => {
1095 info!(
1096 "{} env not set: skipping test that uses external service",
1097 S3BlobConfig::EXTERNAL_TESTS_S3_BUCKET
1098 );
1099 return Ok(());
1100 }
1101 };
1102 let config_multipart = config.clone_with_new_uuid_prefix();
1103
1104 blob_impl_test(move |path| {
1105 let path = path.to_owned();
1106 let config = config.clone();
1107 async move {
1108 let config = S3BlobConfig {
1109 metrics: config.metrics.clone(),
1110 client: config.client.clone(),
1111 bucket: config.bucket.clone(),
1112 prefix: format!("{}/s3_blob_impl_test/{}", config.prefix, path),
1113 };
1114 let mut blob = S3Blob::open(config).await?;
1115 blob.max_keys = 2;
1116 Ok(blob)
1117 }
1118 })
1119 .await?;
1120
1121 {
1125 let blob = S3Blob::open(config_multipart).await?;
1126 blob.set_multi_part("multipart", "foobar".into()).await?;
1127 assert_eq!(
1128 blob.get("multipart").await?,
1129 Some(b"foobar".to_vec().into())
1130 );
1131 }
1132
1133 Ok(())
1134 }
1135
1136 #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
1142 #[cfg_attr(coverage, ignore)] #[cfg_attr(miri, ignore)] #[ignore] async fn s3_blob_hedged() -> Result<(), ExternalError> {
1146 use crate::hedge::{
1147 BLOB_HEDGED_GET_BUDGET_RATIO, BLOB_HEDGED_GET_DELAY, BLOB_HEDGED_GET_ENABLED,
1148 HedgeSibling, HedgedBlob,
1149 };
1150 use crate::metrics::BlobHedgeMetrics;
1151 use mz_dyncfg::{ConfigSet, ConfigUpdates};
1152
1153 let config = match S3BlobConfig::new_for_test().await? {
1154 Some(client) => client,
1155 None => return Ok(()),
1156 };
1157 let sibling = match S3BlobConfig::new_for_test().await? {
1160 Some(client) => client,
1161 None => return Ok(()),
1162 };
1163
1164 let cfg = crate::cfg::all_dyn_configs(ConfigSet::default());
1165 let mut updates = ConfigUpdates::default();
1166 updates.add(&BLOB_HEDGED_GET_ENABLED, true);
1167 updates.add(&BLOB_HEDGED_GET_DELAY, Duration::ZERO);
1168 updates.add(&BLOB_HEDGED_GET_BUDGET_RATIO, 1.0);
1169 updates.apply(&cfg);
1170 let cfg = Arc::new(cfg);
1171
1172 blob_impl_test(move |path| {
1173 let path = path.to_owned();
1174 let config = config.clone();
1175 let sibling = sibling.clone();
1176 let cfg = Arc::clone(&cfg);
1177 async move {
1178 let prefix = format!("{}/s3_blob_hedged_test/{}", config.prefix, path);
1179 let primary_config = S3BlobConfig {
1180 metrics: config.metrics.clone(),
1181 client: config.client.clone(),
1182 bucket: config.bucket.clone(),
1183 prefix: prefix.clone(),
1184 };
1185 let hedge_config = S3BlobConfig {
1186 metrics: sibling.metrics.clone(),
1187 client: sibling.client.clone(),
1188 bucket: config.bucket.clone(),
1189 prefix,
1190 };
1191 let primary: Arc<dyn Blob> = Arc::new(S3Blob::open(primary_config).await?);
1192 let hedge: Arc<dyn Blob> = Arc::new(S3Blob::open(hedge_config).await?);
1193 Ok(HedgedBlob::new(
1194 primary,
1195 HedgeSibling::Isolated(hedge),
1196 cfg,
1197 BlobHedgeMetrics::new(&MetricsRegistry::new()),
1198 ))
1199 }
1200 })
1201 .await?;
1202
1203 Ok(())
1204 }
1205
1206 #[mz_ore::test]
1207 fn should_multipart() {
1208 let config = MultipartConfig::default();
1209 assert_eq!(config.should_multipart(0), Ok(false));
1210 assert_eq!(config.should_multipart(1), Ok(false));
1211 assert_eq!(
1212 config.should_multipart(MultipartConfig::DEFAULT_MULTIPART_THRESHOLD),
1213 Ok(false)
1214 );
1215 assert_eq!(
1216 config.should_multipart(MultipartConfig::DEFAULT_MULTIPART_THRESHOLD + 1),
1217 Ok(true)
1218 );
1219 assert_eq!(
1220 config.should_multipart(MultipartConfig::DEFAULT_MULTIPART_THRESHOLD * 2),
1221 Ok(true)
1222 );
1223 assert_eq!(
1224 config.should_multipart(MultipartConfig::MAX_SINGLE_UPLOAD_SIZE),
1225 Ok(true)
1226 );
1227 assert_eq!(
1228 config.should_multipart(MultipartConfig::MAX_SINGLE_UPLOAD_SIZE + 1),
1229 Err(
1230 "S3 does not support blobs larger than 5497558138880 bytes got: 5497558138881"
1231 .into()
1232 )
1233 );
1234 }
1235
1236 #[mz_ore::test]
1237 fn multipart_iter() {
1238 let iter = MultipartChunkIter::new(10, 0);
1239 assert_eq!(iter.collect::<Vec<_>>(), vec![]);
1240
1241 let iter = MultipartChunkIter::new(10, 9);
1242 assert_eq!(iter.collect::<Vec<_>>(), vec![(1, 0..9)]);
1243
1244 let iter = MultipartChunkIter::new(10, 10);
1245 assert_eq!(iter.collect::<Vec<_>>(), vec![(1, 0..10)]);
1246
1247 let iter = MultipartChunkIter::new(10, 11);
1248 assert_eq!(iter.collect::<Vec<_>>(), vec![(1, 0..10), (2, 10..11)]);
1249
1250 let iter = MultipartChunkIter::new(10, 19);
1251 assert_eq!(iter.collect::<Vec<_>>(), vec![(1, 0..10), (2, 10..19)]);
1252
1253 let iter = MultipartChunkIter::new(10, 20);
1254 assert_eq!(iter.collect::<Vec<_>>(), vec![(1, 0..10), (2, 10..20)]);
1255
1256 let iter = MultipartChunkIter::new(10, 21);
1257 assert_eq!(
1258 iter.collect::<Vec<_>>(),
1259 vec![(1, 0..10), (2, 10..20), (3, 20..21)]
1260 );
1261 }
1262}