1use std::borrow::BorrowMut;
16use std::cmp;
17use std::collections::VecDeque;
18use std::pin::Pin;
19use std::sync::Arc;
20use std::task::{Context, Poll, ready};
21use std::time::Duration;
22
23use await_tree::{InstrumentAwait, SpanExt};
24use aws_sdk_s3::Client;
25use aws_sdk_s3::config::{Credentials, Region};
26use aws_sdk_s3::error::BoxError;
27use aws_sdk_s3::operation::abort_multipart_upload::AbortMultipartUploadError;
28use aws_sdk_s3::operation::complete_multipart_upload::CompleteMultipartUploadError;
29use aws_sdk_s3::operation::create_multipart_upload::CreateMultipartUploadError;
30use aws_sdk_s3::operation::delete_object::DeleteObjectError;
31use aws_sdk_s3::operation::delete_objects::DeleteObjectsError;
32use aws_sdk_s3::operation::get_object::GetObjectError;
33use aws_sdk_s3::operation::get_object::builders::GetObjectFluentBuilder;
34use aws_sdk_s3::operation::head_object::HeadObjectError;
35use aws_sdk_s3::operation::list_objects_v2::ListObjectsV2Error;
36use aws_sdk_s3::operation::put_object::PutObjectError;
37use aws_sdk_s3::operation::upload_part::UploadPartOutput;
38use aws_sdk_s3::primitives::ByteStream;
39use aws_sdk_s3::types::{
40 AbortIncompleteMultipartUpload, BucketLifecycleConfiguration, CompletedMultipartUpload,
41 CompletedPart, Delete, ExpirationStatus, LifecycleRule, LifecycleRuleFilter, ObjectIdentifier,
42};
43use aws_smithy_http::futures_stream_adapter::FuturesStreamCompatByteStream;
44use aws_smithy_http_client::{
45 Builder as SmithyHttpClientBuilder, Connector as SmithyConnector, tls,
46};
47use aws_smithy_runtime_api::client::http::HttpClient;
48use aws_smithy_runtime_api::client::result::SdkError;
49use aws_smithy_types::body::SdkBody;
50use aws_smithy_types::error::metadata::ProvideErrorMetadata;
51use bytes::BytesMut;
52use fail::fail_point;
53use futures::future::{BoxFuture, FutureExt, try_join_all};
54use futures::{Stream, StreamExt, TryStreamExt};
55use itertools::Itertools;
56use risingwave_common::config::ObjectStoreConfig;
57use risingwave_common::range::RangeBoundsExt;
58use thiserror_ext::AsReport;
59use tokio::task::JoinHandle;
60
61use super::object_metrics::ObjectStoreMetrics;
62use super::{
63 Bytes, ObjectError, ObjectErrorInner, ObjectMetadata, ObjectRangeBounds, ObjectResult,
64 ObjectStore, StreamingUploader, prefix, retry_request,
65};
66use crate::object::{
67 ObjectDataStream, ObjectMetadataIter, OperationType, try_update_failure_metric,
68};
69
70type PartId = i32;
71
72const MIN_PART_ID: PartId = 1;
74const S3_INCOMPLETE_MULTIPART_UPLOAD_RETENTION_DAYS: i32 = 1;
77
78pub struct S3StreamingUploader {
83 client: Client,
84 part_size: usize,
85 bucket: String,
86 key: String,
88 upload_id: Option<String>,
90 next_part_id: PartId,
92 join_handles: Vec<JoinHandle<ObjectResult<(PartId, UploadPartOutput)>>>,
94 buf: Vec<Bytes>,
97 not_uploaded_len: usize,
99 metrics: Arc<ObjectStoreMetrics>,
101
102 config: Arc<ObjectStoreConfig>,
103}
104
105impl S3StreamingUploader {
106 const MEDIA_TYPE: &'static str = "s3";
107
108 pub fn new(
109 client: Client,
110 bucket: String,
111 key: String,
112 metrics: Arc<ObjectStoreMetrics>,
113 config: Arc<ObjectStoreConfig>,
114 ) -> S3StreamingUploader {
115 const MIN_PART_SIZE: usize = 5 * 1024 * 1024;
120 const MAX_PART_SIZE: usize = 5 * 1024 * 1024 * 1024;
121 let part_size = config.upload_part_size.clamp(MIN_PART_SIZE, MAX_PART_SIZE);
122
123 Self {
124 client,
125 bucket,
126 part_size,
127 key,
128 upload_id: None,
129 next_part_id: MIN_PART_ID,
130 join_handles: Default::default(),
131 buf: Default::default(),
132 not_uploaded_len: 0,
133 metrics,
134 config,
135 }
136 }
137
138 async fn upload_next_part(&mut self) -> ObjectResult<()> {
139 let operation_type = OperationType::StreamingUpload;
140 let operation_type_str = operation_type.as_str();
141
142 if self.upload_id.is_none() {
144 let builder = || async {
145 self.client
146 .create_multipart_upload()
147 .bucket(&self.bucket)
148 .key(&self.key)
149 .send()
150 .await
151 .map_err(|err| {
152 set_error_should_retry::<CreateMultipartUploadError>(
153 self.config.clone(),
154 err.into(),
155 )
156 })
157 };
158
159 let resp = retry_request(
160 builder,
161 &self.config,
162 OperationType::StreamingUploadInit,
163 self.metrics.clone(),
164 Self::MEDIA_TYPE,
165 )
166 .await;
167
168 try_update_failure_metric(
169 &self.metrics,
170 &resp,
171 OperationType::StreamingUploadInit.as_str(),
172 );
173
174 self.upload_id = Some(resp?.upload_id.unwrap());
175 }
176
177 let data = self.buf.drain(..).collect_vec();
179 let len = self.not_uploaded_len;
180 debug_assert_eq!(
181 data.iter().map(|b| b.len()).sum::<usize>(),
182 self.not_uploaded_len
183 );
184
185 let part_id = self.next_part_id;
187 self.next_part_id += 1;
188
189 let client_cloned = self.client.clone();
191 let bucket = self.bucket.clone();
192 let key = self.key.clone();
193 let upload_id = self.upload_id.clone().unwrap();
194
195 let metrics = self.metrics.clone();
196 metrics
197 .operation_size
198 .with_label_values(&[operation_type_str])
199 .observe(len as f64);
200 let config = self.config.clone();
201
202 self.join_handles.push(tokio::spawn(async move {
203 let _timer = metrics
204 .operation_latency
205 .with_label_values(&["s3", operation_type_str])
206 .start_timer();
207
208 let builder = || async {
209 client_cloned
210 .upload_part()
211 .bucket(bucket.clone())
212 .key(key.clone())
213 .upload_id(upload_id.clone())
214 .part_number(part_id)
215 .body(get_upload_body(data.clone()))
216 .content_length(len as i64)
217 .send()
218 .await
219 .map_err(|err| {
220 set_error_should_retry::<CreateMultipartUploadError>(
221 config.clone(),
222 err.into(),
223 )
224 })
225 };
226
227 let res = retry_request(
228 builder,
229 &config,
230 operation_type,
231 metrics.clone(),
232 Self::MEDIA_TYPE,
233 )
234 .await;
235 try_update_failure_metric(&metrics, &res, operation_type_str);
236 Ok((part_id, res?))
237 }));
238
239 Ok(())
240 }
241
242 async fn flush_multipart_and_complete(&mut self) -> ObjectResult<()> {
243 let operation_type = OperationType::StreamingUploadFinish;
244
245 if !self.buf.is_empty() {
246 self.upload_next_part().await?;
247 }
248
249 let join_handles = self.join_handles.drain(..).collect_vec();
251
252 let mut uploaded_parts = Vec::with_capacity(join_handles.len());
253 for result in try_join_all(join_handles)
254 .await
255 .map_err(ObjectError::internal)?
256 {
257 uploaded_parts.push(result?);
258 }
259
260 let completed_parts = Some(
261 uploaded_parts
262 .iter()
263 .map(|(part_id, output)| {
264 CompletedPart::builder()
265 .set_e_tag(output.e_tag.clone())
266 .set_part_number(Some(*part_id))
267 .build()
268 })
269 .collect_vec(),
270 );
271
272 let builder = || async {
273 self.client
274 .complete_multipart_upload()
275 .bucket(&self.bucket)
276 .key(&self.key)
277 .upload_id(self.upload_id.as_ref().unwrap())
278 .multipart_upload(
279 CompletedMultipartUpload::builder()
280 .set_parts(completed_parts.clone())
281 .build(),
282 )
283 .send()
284 .await
285 .map_err(|err| {
286 set_error_should_retry::<CompleteMultipartUploadError>(
287 self.config.clone(),
288 err.into(),
289 )
290 })
291 };
292
293 let res = retry_request(
294 builder,
295 &self.config,
296 operation_type,
297 self.metrics.clone(),
298 Self::MEDIA_TYPE,
299 )
300 .await;
301 try_update_failure_metric(&self.metrics, &res, operation_type.as_str());
302 let _res = res?;
303
304 Ok(())
305 }
306
307 async fn abort_multipart_upload(&self) -> ObjectResult<()> {
308 self.client
309 .abort_multipart_upload()
310 .bucket(&self.bucket)
311 .key(&self.key)
312 .upload_id(self.upload_id.as_ref().unwrap())
313 .send()
314 .await
315 .map_err(|err| {
316 set_error_should_retry::<AbortMultipartUploadError>(self.config.clone(), err.into())
317 })?;
318 Ok(())
319 }
320}
321
322impl StreamingUploader for S3StreamingUploader {
323 async fn write_bytes(&mut self, data: Bytes) -> ObjectResult<()> {
324 fail_point!("s3_write_bytes_err", |_| Err(ObjectError::internal(
325 "s3 write bytes error"
326 )));
327 let data_len = data.len();
328 self.not_uploaded_len += data_len;
329 self.buf.push(data);
330
331 if self.not_uploaded_len >= self.part_size {
332 self.upload_next_part()
333 .instrument_await("s3_upload_next_part".verbose())
334 .await?;
335 self.not_uploaded_len = 0;
336 }
337 Ok(())
338 }
339
340 async fn finish(mut self) -> ObjectResult<()> {
344 fail_point!("s3_finish_streaming_upload_err", |_| Err(
345 ObjectError::internal("s3 finish streaming upload error")
346 ));
347
348 if self.upload_id.is_none() {
349 debug_assert!(self.join_handles.is_empty());
350 if self.buf.is_empty() {
351 debug_assert_eq!(self.not_uploaded_len, 0);
352 Err(ObjectError::internal("upload empty object"))
353 } else {
354 let operation_type = OperationType::Upload;
355 let builder = || async {
356 self.client
357 .put_object()
358 .bucket(&self.bucket)
359 .body(get_upload_body(self.buf.clone()))
360 .content_length(self.not_uploaded_len as i64)
361 .key(&self.key)
362 .send()
363 .instrument_await("s3_put_object".verbose())
364 .await
365 .map_err(|err| {
366 set_error_should_retry::<PutObjectError>(
367 self.config.clone(),
368 err.into(),
369 )
370 })
371 };
372
373 let res = retry_request(
374 builder,
375 &self.config,
376 operation_type,
377 self.metrics.clone(),
378 Self::MEDIA_TYPE,
379 )
380 .await;
381 try_update_failure_metric(&self.metrics, &res, operation_type.as_str());
382 res?;
383 Ok(())
384 }
385 } else {
386 match self
387 .flush_multipart_and_complete()
388 .instrument_await("s3_flush_multipart_and_complete".verbose())
389 .await
390 {
391 Err(e) => {
392 tracing::warn!(key = self.key, error = %e.as_report(), "Failed to upload object");
393 self.abort_multipart_upload().await?;
394 Err(e)
395 }
396 _ => Ok(()),
397 }
398 }
399 }
400
401 fn get_memory_usage(&self) -> u64 {
402 self.part_size as u64
403 }
404}
405
406fn get_upload_body(data: Vec<Bytes>) -> ByteStream {
407 let total_len: usize = data.iter().map(|b| b.len()).sum();
410 let mut buf = BytesMut::with_capacity(total_len);
411 for chunk in data {
412 buf.extend_from_slice(&chunk);
413 }
414 ByteStream::from(buf.freeze())
415}
416
417#[derive(Clone)]
420pub struct S3ObjectStore {
421 client: Client,
422 bucket: String,
423 metrics: Arc<ObjectStoreMetrics>,
425
426 config: Arc<ObjectStoreConfig>,
427}
428
429#[async_trait::async_trait]
430impl ObjectStore for S3ObjectStore {
431 type StreamingUploader = S3StreamingUploader;
432
433 fn get_object_prefix(&self, obj_id: u64, _use_new_object_prefix_strategy: bool) -> String {
434 prefix::s3::get_object_prefix(obj_id)
437 }
438
439 async fn upload(&self, path: &str, obj: Bytes) -> ObjectResult<()> {
440 fail_point!("s3_upload_err", |_| Err(ObjectError::internal(
441 "s3 upload error"
442 )));
443 if obj.is_empty() {
444 Err(ObjectError::internal("upload empty object"))
445 } else {
446 self.client
447 .put_object()
448 .bucket(&self.bucket)
449 .body(ByteStream::from(obj))
450 .key(path)
451 .send()
452 .await
453 .map_err(|err| {
454 set_error_should_retry::<PutObjectError>(self.config.clone(), err.into())
455 })?;
456 Ok(())
457 }
458 }
459
460 async fn streaming_upload(&self, path: &str) -> ObjectResult<Self::StreamingUploader> {
461 fail_point!("s3_streaming_upload_err", |_| Err(ObjectError::internal(
462 "s3 streaming upload error"
463 )));
464 Ok(S3StreamingUploader::new(
465 self.client.clone(),
466 self.bucket.clone(),
467 path.to_owned(),
468 self.metrics.clone(),
469 self.config.clone(),
470 ))
471 }
472
473 async fn read(&self, path: &str, range: impl ObjectRangeBounds) -> ObjectResult<Bytes> {
475 fail_point!("s3_read_err", |_| Err(ObjectError::internal(
476 "s3 read error"
477 )));
478
479 let val = match self.obj_store_request(path, range.clone()).send().await {
480 Ok(resp) => resp
481 .body
482 .collect()
483 .await
484 .map_err(|err| {
485 set_error_should_retry::<GetObjectError>(self.config.clone(), err.into())
486 })?
487 .into_bytes(),
488 Err(sdk_err) => {
489 return Err(set_error_should_retry::<GetObjectError>(
490 self.config.clone(),
491 sdk_err.into(),
492 ));
493 }
494 };
495
496 if let Some(len) = range.len()
497 && len != val.len()
498 {
499 return Err(ObjectError::internal(format!(
500 "mismatched size: expected {}, found {} when reading {} at {:?}",
501 len,
502 val.len(),
503 path,
504 range,
505 )));
506 }
507
508 Ok(val)
509 }
510
511 async fn metadata(&self, path: &str) -> ObjectResult<ObjectMetadata> {
512 fail_point!("s3_metadata_err", |_| Err(ObjectError::internal(
513 "s3 metadata error"
514 )));
515 let resp = self
516 .client
517 .head_object()
518 .bucket(&self.bucket)
519 .key(path)
520 .send()
521 .await
522 .map_err(|err| {
523 set_error_should_retry::<HeadObjectError>(self.config.clone(), err.into())
524 })?;
525 Ok(ObjectMetadata {
526 key: path.to_owned(),
527 last_modified: resp
528 .last_modified()
529 .expect("last_modified required")
530 .as_secs_f64(),
531 total_size: resp.content_length.unwrap_or_default() as usize,
532 })
533 }
534
535 async fn streaming_read(
539 &self,
540 path: &str,
541 range: impl ObjectRangeBounds,
542 ) -> ObjectResult<ObjectDataStream> {
543 fail_point!("s3_streaming_read_init_err", |_| Err(
544 ObjectError::internal("s3 streaming read init error")
545 ));
546
547 let resp = match self.obj_store_request(path, range.clone()).send().await {
548 Ok(resp) => resp,
549 Err(sdk_err) => {
550 return Err(set_error_should_retry::<GetObjectError>(
551 self.config.clone(),
552 sdk_err.into(),
553 ));
554 }
555 };
556
557 let reader = FuturesStreamCompatByteStream::new(resp.body);
558
559 Ok(Box::pin(
560 reader
561 .into_stream()
562 .map(|item| item.map_err(ObjectError::from)),
563 ))
564 }
565
566 async fn delete(&self, path: &str) -> ObjectResult<()> {
569 fail_point!("s3_delete_err", |_| Err(ObjectError::internal(
570 "s3 delete error"
571 )));
572 self.client
573 .delete_object()
574 .bucket(&self.bucket)
575 .key(path)
576 .send()
577 .await
578 .map_err(|err| {
579 set_error_should_retry::<DeleteObjectError>(self.config.clone(), err.into())
580 })?;
581 Ok(())
582 }
583
584 async fn delete_objects(&self, paths: &[String]) -> ObjectResult<()> {
589 const MAX_LEN: usize = 1000;
591 let mut all_errors = Vec::new();
592
593 for start_idx in (0..paths.len()).step_by(MAX_LEN) {
595 let end_idx = cmp::min(paths.len(), start_idx + MAX_LEN);
596 let slice = &paths[start_idx..end_idx];
597 let mut obj_ids = Vec::with_capacity(slice.len());
599 for path in slice {
600 obj_ids.push(ObjectIdentifier::builder().key(path).build().unwrap());
601 }
602
603 let delete_builder = Delete::builder().set_objects(Some(obj_ids));
605 let delete_output = self
606 .client
607 .delete_objects()
608 .bucket(&self.bucket)
609 .delete(delete_builder.build().unwrap()).send()
610 .await.map_err(|err| {
611 set_error_should_retry::<DeleteObjectsError>(self.config.clone(),err.into())
612 })?;
613
614 if !delete_output.errors().is_empty() {
616 all_errors.append(&mut delete_output.errors().to_owned());
617 }
618 }
619 if !all_errors.is_empty() {
620 return Err(ObjectError::internal(format!(
621 "DeleteObjects request returned exception for some objects: {:?}",
622 all_errors
623 )));
624 }
625
626 Ok(())
627 }
628
629 async fn list(
630 &self,
631 prefix: &str,
632 start_after: Option<String>,
633 limit: Option<usize>,
634 ) -> ObjectResult<ObjectMetadataIter> {
635 Ok(Box::pin(
636 S3ObjectIter::new(
637 self.client.clone(),
638 self.bucket.clone(),
639 prefix.to_owned(),
640 self.config.clone(),
641 start_after,
642 )
643 .take(limit.unwrap_or(usize::MAX)),
644 ))
645 }
646
647 fn store_media_type(&self) -> &'static str {
648 "s3"
649 }
650}
651
652impl S3ObjectStore {
653 pub fn new_http_client(config: &ObjectStoreConfig) -> impl HttpClient + use<> {
654 let nodelay = config.s3.nodelay;
655 let pool_idle_timeout = config.s3.keepalive_ms.map(Duration::from_millis);
656
657 let tls_provider = tls::Provider::Rustls(tls::rustls_provider::CryptoMode::AwsLc);
660 SmithyHttpClientBuilder::new().build_with_connector_fn(move |settings, _components| {
661 let mut builder = SmithyConnector::builder();
662
663 if let Some(settings) = settings {
664 builder = builder.connector_settings(settings.clone());
665 }
666
667 if let Some(nodelay) = nodelay {
668 builder = builder.enable_tcp_nodelay(nodelay);
669 }
670
671 if let Some(pool_idle_timeout) = pool_idle_timeout {
672 builder = builder.pool_idle_timeout(pool_idle_timeout);
673 }
674
675 builder.tls_provider(tls_provider.clone()).build()
676 })
677 }
678
679 pub async fn new_with_config(
683 bucket: String,
684 metrics: Arc<ObjectStoreMetrics>,
685 config: Arc<ObjectStoreConfig>,
686 ) -> Self {
687 let sdk_config_loader = aws_config::from_env().http_client(Self::new_http_client(&config));
688
689 let client = match std::env::var("RW_S3_ENDPOINT") {
691 Ok(endpoint) => {
692 let is_force_path_style = match std::env::var("RW_IS_FORCE_PATH_STYLE") {
694 Ok(value) => value == "true",
695 Err(_) => false,
696 };
697
698 let sdk_config = Box::pin(sdk_config_loader.load()).await;
699 #[cfg(madsim)]
700 let client = Client::new(&sdk_config);
701 #[cfg(not(madsim))]
702 let client = Client::from_conf(
703 aws_sdk_s3::config::Builder::from(&sdk_config)
704 .endpoint_url(endpoint)
705 .force_path_style(is_force_path_style)
706 .identity_cache(
707 aws_sdk_s3::config::IdentityCache::lazy()
708 .load_timeout(Duration::from_secs(
709 config.s3.identity_resolution_timeout_s,
710 ))
711 .build(),
712 )
713 .stalled_stream_protection(
714 aws_sdk_s3::config::StalledStreamProtectionConfig::disabled(),
715 )
716 .build(),
717 );
718 client
719 }
720 Err(_) => {
721 let sdk_config = Box::pin(sdk_config_loader.load()).await;
723 #[cfg(madsim)]
724 let client = Client::new(&sdk_config);
725 #[cfg(not(madsim))]
726 let client = Client::from_conf(
727 aws_sdk_s3::config::Builder::from(&sdk_config)
728 .identity_cache(
729 aws_sdk_s3::config::IdentityCache::lazy()
730 .load_timeout(Duration::from_secs(
731 config.s3.identity_resolution_timeout_s,
732 ))
733 .build(),
734 )
735 .stalled_stream_protection(
736 aws_sdk_s3::config::StalledStreamProtectionConfig::disabled(),
737 )
738 .build(),
739 );
740 client
741 }
742 };
743
744 Self {
745 client,
746 bucket,
747 metrics,
748 config,
749 }
750 }
751
752 pub async fn new_minio_engine(
754 server: &str,
755 metrics: Arc<ObjectStoreMetrics>,
756 object_store_config: Arc<ObjectStoreConfig>,
757 ) -> Self {
758 let server = server.strip_prefix("minio://").unwrap();
759 let (access_key_id, rest) = server.split_once(':').unwrap();
760 let (secret_access_key, mut rest) = rest.split_once('@').unwrap();
761
762 let endpoint_prefix = if let Some(rest_stripped) = rest.strip_prefix("https://") {
763 rest = rest_stripped;
764 "https://"
765 } else if let Some(rest_stripped) = rest.strip_prefix("http://") {
766 rest = rest_stripped;
767 "http://"
768 } else {
769 "http://"
770 };
771 let (address, bucket) = rest.split_once('/').unwrap();
772
773 #[cfg(madsim)]
774 let builder = aws_sdk_s3::config::Builder::new().credentials_provider(
775 Credentials::from_keys(access_key_id, secret_access_key, None),
776 );
777 #[cfg(not(madsim))]
778 let builder = aws_sdk_s3::config::Builder::from(
779 &Box::pin(
780 aws_config::ConfigLoader::default()
781 .credentials_provider(Credentials::from_keys(
783 access_key_id,
784 secret_access_key,
785 None,
786 ))
787 .load(),
788 )
789 .await,
790 )
791 .force_path_style(true)
792 .identity_cache(
793 aws_sdk_s3::config::IdentityCache::lazy()
794 .load_timeout(Duration::from_secs(
795 object_store_config.s3.identity_resolution_timeout_s,
796 ))
797 .build(),
798 )
799 .http_client(Self::new_http_client(&object_store_config))
800 .behavior_version_latest()
801 .stalled_stream_protection(aws_sdk_s3::config::StalledStreamProtectionConfig::disabled());
802 let config = builder
803 .region(Region::new("custom"))
804 .endpoint_url(format!("{}{}", endpoint_prefix, address))
805 .build();
806 let client = Client::from_conf(config);
807
808 Self {
809 client,
810 bucket: bucket.to_owned(),
811 metrics,
812 config: object_store_config,
813 }
814 }
815
816 fn obj_store_request(
821 &self,
822 path: &str,
823 range: impl ObjectRangeBounds,
824 ) -> GetObjectFluentBuilder {
825 let req = self.client.get_object().bucket(&self.bucket).key(path);
826 if range.is_full() {
827 return req;
828 }
829
830 let start = range.start().map(|v| v.to_string()).unwrap_or_default();
831 let end = range.end().map(|v| (v - 1).to_string()).unwrap_or_default(); req.range(format!("bytes={}-{}", start, end))
834 }
835
836 pub async fn configure_bucket_lifecycle(&self, data_directory: &str) -> bool {
852 let bucket = self.bucket.as_str();
854 let mut configured_rules = vec![];
855 let get_config_result = self
856 .client
857 .get_bucket_lifecycle_configuration()
858 .bucket(bucket)
859 .send()
860 .await;
861 let mut is_expiration_configured = false;
862
863 if let Ok(config) = &get_config_result {
864 for rule in config.rules() {
865 if rule.expiration().is_some() {
866 is_expiration_configured |= rule.status == ExpirationStatus::Enabled && match rule.filter().as_ref() {
875 None => true,
877 Some(filter) => {
879 let is_empty = filter.prefix().is_none()
884 && filter.tag().is_none()
885 && filter.and().is_none()
886 && filter.object_size_greater_than().is_none()
887 && filter.object_size_less_than().is_none();
888 if is_empty {
889 true
890 } else {
891 filter
892 .prefix()
893 .is_some_and(|prefix| data_directory.starts_with(prefix))
894 }
895 }
896 };
897
898 if matches!(rule.status(), ExpirationStatus::Enabled)
899 && rule.abort_incomplete_multipart_upload().is_some()
900 {
901 configured_rules.push(rule);
902 }
903 }
904 }
905 }
906
907 if !configured_rules.is_empty() {
908 tracing::info!(
909 "S3 bucket {} has already configured AbortIncompleteMultipartUpload: {:?}",
910 bucket,
911 configured_rules,
912 );
913 } else {
914 let bucket_lifecycle_rule = LifecycleRule::builder()
915 .id("abort-incomplete-multipart-upload")
916 .status(ExpirationStatus::Enabled)
917 .filter(LifecycleRuleFilter::builder().prefix("").build())
919 .abort_incomplete_multipart_upload(
920 AbortIncompleteMultipartUpload::builder()
921 .days_after_initiation(S3_INCOMPLETE_MULTIPART_UPLOAD_RETENTION_DAYS)
922 .build(),
923 )
924 .build()
925 .unwrap();
926 let bucket_lifecycle_config = BucketLifecycleConfiguration::builder()
927 .rules(bucket_lifecycle_rule)
928 .build()
929 .unwrap();
930 if self
931 .client
932 .put_bucket_lifecycle_configuration()
933 .bucket(bucket)
934 .lifecycle_configuration(bucket_lifecycle_config)
935 .send()
936 .await
937 .is_ok()
938 {
939 tracing::info!(
940 "S3 bucket {:?} is configured to automatically purge abandoned MultipartUploads after {} days",
941 bucket,
942 S3_INCOMPLETE_MULTIPART_UPLOAD_RETENTION_DAYS,
943 );
944 } else {
945 tracing::warn!(
946 "Failed to configure life cycle rule for S3 bucket: {:?}. It is recommended to configure it manually to avoid unnecessary storage cost.",
947 bucket
948 );
949 }
950 }
951 if is_expiration_configured {
952 tracing::info!(
953 "S3 bucket {} has already configured the expiration for the lifecycle.",
954 bucket,
955 );
956 }
957 is_expiration_configured
958 }
959}
960
961struct S3ObjectIter {
962 buffer: VecDeque<ObjectMetadata>,
963 client: Client,
964 bucket: String,
965 prefix: String,
966 next_continuation_token: Option<String>,
967 is_truncated: Option<bool>,
968 #[expect(clippy::type_complexity)]
969 send_future: Option<
970 BoxFuture<
971 'static,
972 Result<(Vec<ObjectMetadata>, Option<String>, Option<bool>), ObjectError>,
973 >,
974 >,
975
976 config: Arc<ObjectStoreConfig>,
977 start_after: Option<String>,
978}
979
980impl S3ObjectIter {
981 fn new(
982 client: Client,
983 bucket: String,
984 prefix: String,
985 config: Arc<ObjectStoreConfig>,
986 start_after: Option<String>,
987 ) -> Self {
988 Self {
989 buffer: VecDeque::default(),
990 client,
991 bucket,
992 prefix,
993 next_continuation_token: None,
994 is_truncated: Some(true),
995 send_future: None,
996 config,
997 start_after,
998 }
999 }
1000}
1001
1002impl Stream for S3ObjectIter {
1003 type Item = ObjectResult<ObjectMetadata>;
1004
1005 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
1006 if let Some(e) = self.buffer.pop_front() {
1007 return Poll::Ready(Some(Ok(e)));
1008 }
1009 if let Some(f) = self.send_future.as_mut() {
1010 return match ready!(f.poll_unpin(cx)) {
1011 Ok((more, next_continuation_token, is_truncated)) => {
1012 self.next_continuation_token = next_continuation_token;
1013 self.is_truncated = is_truncated;
1014 self.buffer.extend(more);
1015 self.send_future = None;
1016 self.start_after = None;
1018 self.poll_next(cx)
1019 }
1020 Err(e) => {
1021 self.send_future = None;
1022 Poll::Ready(Some(Err(e)))
1023 }
1024 };
1025 }
1026 if !self.is_truncated.unwrap_or_default() {
1027 return Poll::Ready(None);
1028 }
1029 let mut request = self
1030 .client
1031 .list_objects_v2()
1032 .bucket(&self.bucket)
1033 .prefix(&self.prefix);
1034 #[cfg(not(madsim))]
1035 if let Some(start_after) = self.start_after.as_ref() {
1036 request = request.start_after(start_after);
1037 }
1038 if let Some(continuation_token) = self.next_continuation_token.as_ref() {
1039 request = request.continuation_token(continuation_token);
1040 }
1041 let config = self.config.clone();
1042 let f = async move {
1043 match request.send().await {
1044 Ok(r) => {
1045 let more = r
1046 .contents()
1047 .iter()
1048 .map(|obj| ObjectMetadata {
1049 key: obj.key().expect("key required").to_owned(),
1050 last_modified: obj
1051 .last_modified()
1052 .map(|l| l.as_secs_f64())
1053 .unwrap_or(0f64),
1054 total_size: obj.size().unwrap_or_default() as usize,
1055 })
1056 .collect_vec();
1057 let is_truncated = r.is_truncated;
1058 let next_continuation_token = r.next_continuation_token;
1059 Ok((more, next_continuation_token, is_truncated))
1060 }
1061 Err(e) => Err(set_error_should_retry::<ListObjectsV2Error>(
1062 config,
1063 e.into(),
1064 )),
1065 }
1066 };
1067 self.send_future = Some(Box::pin(f));
1068 self.poll_next(cx)
1069 }
1070}
1071
1072fn set_error_should_retry<E>(config: Arc<ObjectStoreConfig>, object_err: ObjectError) -> ObjectError
1073where
1074 E: ProvideErrorMetadata + Into<BoxError> + Sync + Send + std::error::Error + 'static,
1075{
1076 let not_found = object_err.is_object_not_found_error();
1077
1078 if not_found {
1079 return object_err;
1080 }
1081
1082 let mut inner = object_err.into_inner();
1083 match inner.borrow_mut() {
1084 ObjectErrorInner::S3 {
1085 should_retry,
1086 inner,
1087 } => {
1088 let sdk_err = inner
1089 .as_ref()
1090 .downcast_ref::<SdkError<E, aws_smithy_runtime_api::http::Response<SdkBody>>>();
1091
1092 let err_should_retry = match sdk_err {
1093 Some(SdkError::DispatchFailure(e)) => {
1094 if e.is_timeout() {
1095 tracing::warn!(target: "http_timeout_retry", "{e:?} occurs, retry S3 get_object request.");
1096 true
1097 } else {
1098 false
1099 }
1100 }
1101
1102 Some(SdkError::ServiceError(e)) => match e.err().code() {
1103 None => {
1104 if config.s3.developer.retry_unknown_service_error
1105 || config.s3.retry_unknown_service_error
1106 {
1107 tracing::warn!(target: "unknown_service_error", "{e:?} occurs, retry S3 get_object request.");
1108 true
1109 } else {
1110 false
1111 }
1112 }
1113 Some(code) => {
1114 if config
1115 .s3
1116 .developer
1117 .retryable_service_error_codes
1118 .iter()
1119 .any(|s| s.as_str().eq_ignore_ascii_case(code))
1120 {
1121 tracing::warn!(target: "retryable_service_error", "{e:?} occurs, retry S3 get_object request.");
1122 true
1123 } else {
1124 false
1125 }
1126 }
1127 },
1128
1129 Some(SdkError::TimeoutError(_err)) => true,
1130
1131 _ => false,
1132 };
1133
1134 *should_retry = err_should_retry;
1135 }
1136
1137 _ => unreachable!(),
1138 }
1139
1140 ObjectError::from(inner)
1141}
1142
1143#[cfg(test)]
1144#[cfg(not(madsim))]
1145mod tests {
1146 use crate::object::prefix::s3::{NUM_BUCKET_PREFIXES, get_object_prefix};
1147
1148 fn get_hash_of_object(obj_id: u64) -> u32 {
1149 let crc_hash = crc32fast::hash(&obj_id.to_be_bytes());
1150 crc_hash % NUM_BUCKET_PREFIXES
1151 }
1152
1153 #[tokio::test]
1154 async fn test_get_object_prefix() {
1155 for obj_id in 0..99999 {
1156 let hash = get_hash_of_object(obj_id);
1157 let prefix = get_object_prefix(obj_id);
1158 assert_eq!(format!("{}/", hash), prefix);
1159 }
1160
1161 let obj_prefix = String::default();
1162 let path = format!("{}/{}{}.data", "hummock_001", obj_prefix, 101);
1163 assert_eq!("hummock_001/101.data", path);
1164 }
1165}