@@ -30,10 +30,11 @@ use aws_sdk_s3::config::{
3030use aws_sdk_s3:: error:: { ProvideErrorMetadata , SdkError } ;
3131use aws_sdk_s3:: operation:: delete_objects:: DeleteObjectsOutput ;
3232use aws_sdk_s3:: operation:: get_object:: { GetObjectError , GetObjectOutput } ;
33- use aws_sdk_s3:: primitives:: ByteStream ;
33+ use aws_sdk_s3:: primitives:: { AggregatedBytes , ByteStream } ;
3434use aws_sdk_s3:: types:: builders:: ObjectIdentifierBuilder ;
3535use aws_sdk_s3:: types:: { CompletedMultipartUpload , CompletedPart , Delete , ObjectIdentifier } ;
3636use base64:: prelude:: { BASE64_STANDARD , Engine } ;
37+ use bytes:: Bytes ;
3738use futures:: { StreamExt , stream} ;
3839use quickwit_aws:: retry:: { AwsRetryable , aws_retry} ;
3940use quickwit_aws:: { aws_behavior_version, get_aws_config} ;
@@ -48,6 +49,7 @@ use tracing::{info, instrument, warn};
4849
4950use crate :: metrics:: object_storage_get_slice_in_flight_guards;
5051use crate :: object_storage:: MultiPartPolicy ;
52+ use crate :: stable_deref_bytes:: into_owned_bytes;
5153use crate :: storage:: SendableAsync ;
5254use crate :: {
5355 BulkDeleteError , DeleteFailure , OwnedBytes , STORAGE_METRICS , Storage , StorageError ,
@@ -445,11 +447,11 @@ impl S3CompatibleObjectStorage {
445447 . upload_id ( upload_id. 0 )
446448 . send ( )
447449 . await
448- . map_err ( |s3_err | {
449- if s3_err . is_retryable ( ) {
450- Retry :: Transient ( StorageError :: from ( s3_err ) )
450+ . map_err ( |sdk_error | {
451+ if sdk_error . is_retryable ( ) {
452+ Retry :: Transient ( StorageError :: from ( sdk_error ) )
451453 } else {
452- Retry :: Permanent ( StorageError :: from ( s3_err ) )
454+ Retry :: Permanent ( StorageError :: from ( sdk_error ) )
453455 }
454456 } ) ?;
455457
@@ -484,7 +486,7 @@ impl S3CompatibleObjectStorage {
484486 . collect :: < Vec < _ > > ( )
485487 . await
486488 . into_iter ( )
487- . map ( |res| res. map_err ( |e| e . into_inner ( ) ) )
489+ . map ( |res| res. map_err ( |error| error . into_inner ( ) ) )
488490 . collect ( ) ;
489491 match completed_parts_res {
490492 Ok ( completed_parts) => {
@@ -564,22 +566,19 @@ impl S3CompatibleObjectStorage {
564566 Ok ( get_object_output)
565567 }
566568
567- async fn get_to_vec (
569+ async fn get_to_bytes (
568570 & self ,
569571 path : & Path ,
570572 range_opt : Option < Range < usize > > ,
571- ) -> StorageResult < Vec < u8 > > {
572- let cap = range_opt. as_ref ( ) . map ( Range :: len) . unwrap_or ( 0 ) ;
573+ ) -> StorageResult < Bytes > {
573574 let get_object_output = aws_retry ( & self . retry_params , || {
574575 self . get_object ( path, range_opt. clone ( ) )
575576 } )
576577 . await ?;
577578 // only record ranged get request as being in flight
578579 let _in_flight_guards =
579580 range_opt. map ( |range| object_storage_get_slice_in_flight_guards ( range. len ( ) ) ) ;
580- let mut buf: Vec < u8 > = Vec :: with_capacity ( cap) ;
581- download_all ( get_object_output. body , & mut buf) . await ?;
582- Ok ( buf)
581+ download_all ( get_object_output. body ) . await
583582 }
584583
585584 /// Bulk delete implementation based on the DeleteObject API:
@@ -720,16 +719,35 @@ impl S3CompatibleObjectStorage {
720719 }
721720}
722721
723- async fn download_all ( byte_stream : ByteStream , output : & mut Vec < u8 > ) -> io:: Result < ( ) > {
724- output. clear ( ) ;
725- let mut body_stream_reader = BufReader :: new ( byte_stream. into_async_read ( ) ) ;
726- let num_bytes_copied = tokio:: io:: copy_buf ( & mut body_stream_reader, output) . await ?;
722+ async fn download_all ( byte_stream : ByteStream ) -> StorageResult < Bytes > {
723+ let aggregated: AggregatedBytes = byte_stream
724+ . collect ( )
725+ . await
726+ . map_err ( byte_stream_to_storage_error) ?;
727+ // `AggregatedBytes::into_bytes` returns the underlying `Bytes` without copying when the body
728+ // was received as a single segment, and concatenates into a fresh `Bytes` otherwise.
729+ let bytes = aggregated. into_bytes ( ) ;
727730 STORAGE_METRICS
728731 . object_storage_download_num_bytes
729- . inc_by ( num_bytes_copied) ;
730- // When calling `get_all`, the Vec capacity is not properly set.
731- output. shrink_to_fit ( ) ;
732- Ok ( ( ) )
732+ . inc_by ( bytes. len ( ) as u64 ) ;
733+ Ok ( bytes)
734+ }
735+
736+ /// Classifies a `ByteStream::collect` failure. The response headers were already received
737+ /// successfully at this point; what fails here is draining the body. The underlying cause can be a
738+ /// local `io::Error` (which we classify as `Io`) or a transport-level streaming failure — stalled
739+ /// stream, malformed body, checksum mismatch — which we classify as `Internal` and report with the
740+ /// original error preserved as source.
741+ fn byte_stream_to_storage_error ( error : aws_sdk_s3:: primitives:: ByteStreamError ) -> StorageError {
742+ let is_io_error = std:: error:: Error :: source ( & error)
743+ . and_then ( |source| source. downcast_ref :: < io:: Error > ( ) )
744+ . is_some ( ) ;
745+ let kind = if is_io_error {
746+ StorageErrorKind :: Io
747+ } else {
748+ StorageErrorKind :: Internal
749+ } ;
750+ kind. with_error ( error)
733751}
734752
735753#[ async_trait]
@@ -816,9 +834,9 @@ impl Storage for S3CompatibleObjectStorage {
816834 #[ instrument( level = "debug" , skip( self , range) , fields( range. start = range. start, range. end = range. end) ) ]
817835 async fn get_slice ( & self , path : & Path , range : Range < usize > ) -> StorageResult < OwnedBytes > {
818836 let _permit = REQUEST_SEMAPHORE . acquire ( ) . await ;
819- self . get_to_vec ( path, Some ( range. clone ( ) ) )
837+ self . get_to_bytes ( path, Some ( range. clone ( ) ) )
820838 . await
821- . map ( OwnedBytes :: new )
839+ . map ( into_owned_bytes )
822840 . map_err ( |err| {
823841 err. add_context ( format ! (
824842 "failed to fetch slice {:?} for object: {}/{}" ,
@@ -850,9 +868,9 @@ impl Storage for S3CompatibleObjectStorage {
850868 async fn get_all ( & self , path : & Path ) -> StorageResult < OwnedBytes > {
851869 let _permit = REQUEST_SEMAPHORE . acquire ( ) . await ;
852870 let bytes = self
853- . get_to_vec ( path, None )
871+ . get_to_bytes ( path, None )
854872 . await
855- . map ( OwnedBytes :: new )
873+ . map ( into_owned_bytes )
856874 . map_err ( |err| {
857875 err. add_context ( format ! (
858876 "failed to fetch object: {}/{}" ,
0 commit comments