@@ -2,9 +2,12 @@ use std::pin::Pin;
22
33use async_stream:: stream;
44use clp_rust_utils:: {
5- clp_config:: package:: {
6- config:: { Config , StorageEngine , StreamOutputStorage } ,
7- credentials:: Credentials ,
5+ clp_config:: {
6+ AwsAuthentication ,
7+ package:: {
8+ config:: { Config , StorageEngine , StreamOutputStorage } ,
9+ credentials:: Credentials ,
10+ } ,
811 } ,
912 database:: mysql:: create_mysql_pool,
1013 job_config:: { QUERY_JOBS_TABLE_NAME , QueryJobStatus , QueryJobType , SearchJobConfig } ,
@@ -165,6 +168,7 @@ impl Client {
165168 SearchResultStream <
166169 impl Stream < Item = Result < String , ClientError > > + use <> ,
167170 impl Stream < Item = Result < String , ClientError > > + use < > ,
171+ impl Stream < Item = Result < String , ClientError > > + use < > ,
168172 > ,
169173 ClientError ,
170174 > {
@@ -186,11 +190,19 @@ impl Client {
186190 }
187191
188192 let job_config = self . get_job_config ( search_job_id) . await ?;
193+
189194 if job_config. write_to_file {
190- return Ok ( SearchResultStream :: File {
191- inner : self . fetch_results_from_file ( search_job_id) ,
192- } ) ;
195+ let stream = match & self . config . stream_output . storage {
196+ StreamOutputStorage :: Fs { .. } => SearchResultStream :: File {
197+ inner : self . fetch_results_from_file ( search_job_id) ,
198+ } ,
199+ StreamOutputStorage :: S3 { .. } => SearchResultStream :: S3 {
200+ inner : self . fetch_results_from_s3 ( search_job_id) . await ,
201+ } ,
202+ } ;
203+ return Ok ( stream) ;
193204 }
205+
194206 self . fetch_results_from_mongo ( search_job_id)
195207 . await
196208 . map ( |s| SearchResultStream :: Mongo { inner : s } )
@@ -257,7 +269,7 @@ impl Client {
257269 search_job_id : u64 ,
258270 ) -> impl Stream < Item = Result < String , ClientError > > + use < > {
259271 let StreamOutputStorage :: Fs { directory } = & self . config . stream_output . storage else {
260- todo ! ( "Outputting query results to S3 is not supported for now." ) ;
272+ unreachable ! ( ) ;
261273 } ;
262274 let search_job_output_dir = std:: path:: Path :: new ( "/" )
263275 . join ( directory)
@@ -277,6 +289,92 @@ impl Client {
277289 stream
278290 }
279291
292+ /// Asynchronously fetches results of a completed search job from S3.
293+ ///
294+ /// # Returns
295+ ///
296+ /// A stream of the job's results on success. Each item in the stream is a [`Result`] that:
297+ ///
298+ /// ## Returns
299+ ///
300+ /// The log message in string representation on success.
301+ ///
302+ /// ## Errors
303+ ///
304+ /// Returns an error if:
305+ ///
306+ /// * Forwards the pagination stream's error returned from S3 list-object-v2 operation's
307+ /// paginator (i.e., from
308+ /// [`aws_smithy_async::future::pagination_stream::PaginationStream::next`]).
309+ /// * Forwards S3 get-object operation's return values on failure (i.e., from
310+ /// [`aws_sdk_s3::operation::get_object::builders::GetObjectFluentBuilder::send`]).
311+ /// * Forwards [`aws_smithy_types::byte_stream::ByteStream::collect`]'s return values on
312+ /// failure.
313+ ///
314+ /// # Panics
315+ ///
316+ /// Panics if the stream output storage is not S3.
317+ async fn fetch_results_from_s3(
318+ & self ,
319+ search_job_id : u64 ,
320+ ) -> impl Stream < Item = Result < String , ClientError > > + use <> {
321+ tracing:: info!( "Streaming results from S3" ) ;
322+ let StreamOutputStorage :: S3 { s3_config, .. } = & self . config . stream_output . storage else {
323+ unreachable ! ( ) ;
324+ } ;
325+
326+ let AwsAuthentication :: Credentials { credentials } = & s3_config. aws_authentication ;
327+
328+ let s3_config = s3_config. clone ( ) ;
329+ let credentials = credentials. clone ( ) ;
330+
331+ let s3_client = clp_rust_utils:: s3:: create_new_client (
332+ s3_config. region_code . as_str ( ) ,
333+ credentials. access_key_id . as_str ( ) ,
334+ credentials. secret_access_key . as_str ( ) ,
335+ None ,
336+ )
337+ . await ;
338+
339+ let key_prefix = format ! ( "{}{}/" , s3_config. key_prefix, search_job_id) ;
340+ tracing:: info!( "Streaming results from S3 prefix: {}" , key_prefix) ;
341+ let mut object_pages = s3_client
342+ . list_objects_v2 ( )
343+ . bucket ( s3_config. bucket . as_str ( ) )
344+ . prefix ( key_prefix)
345+ . into_paginator ( )
346+ . send ( ) ;
347+
348+ stream ! {
349+ while let Some ( object_page) = object_pages. next( ) . await {
350+ tracing:: debug!( "Received S3 object page: {:?}" , object_page) ;
351+ for object in object_page?. contents( ) {
352+ let Some ( key) = object. key( ) else {
353+ tracing:: info!( "S3 object {:?} doesn't have a key" , object) ;
354+ continue ;
355+ } ;
356+ if key. ends_with( '/' ) {
357+ tracing:: info!( "Skipping S3 object with key {} as it is a directory" , key) ;
358+ continue ;
359+ }
360+ tracing:: info!( "Streaming results from S3 object with key: {}" , key) ;
361+ let obj = s3_client
362+ . get_object( )
363+ . bucket( s3_config. bucket. as_str( ) )
364+ . key( key)
365+ . send( )
366+ . await ?;
367+ let bytes = obj. body. collect( ) . await ?. into_bytes( ) ;
368+ let mut deserializer = rmp_serde:: Deserializer :: from_read_ref( bytes. as_ref( ) ) ;
369+ while let Ok ( event) = Deserialize :: deserialize( & mut deserializer) {
370+ let event: ( i64 , String , String , String , i64 ) = event;
371+ yield Ok ( event. 1 ) ;
372+ }
373+ }
374+ }
375+ }
376+ }
377+
280378 /// Asynchronously fetches results of a completed search job from `MongoDB`.
281379 ///
282380 /// # Returns
@@ -333,21 +431,26 @@ pin_project! {
333431 ///
334432 /// * `FileStream`: Streaming from file system storage.
335433 /// * `MongoStream`: Streaming from MongoDB storage.
434+ /// * `S3Stream`: Streaming from S3 storage.
336435 #[ project = SearchResultStreamProj ]
337- pub enum SearchResultStream <FileStream , MongoStream >
436+ pub enum SearchResultStream <FileStream , MongoStream , S3Stream >
338437 where
339438 FileStream : Stream <Item = Result <String , ClientError >>,
340- MongoStream : Stream <Item = Result <String , ClientError >>
439+ MongoStream : Stream <Item = Result <String , ClientError >>,
440+ S3Stream : Stream <Item = Result <String , ClientError >>
341441 {
342442 File { #[ pin] inner: FileStream } ,
343443 Mongo { #[ pin] inner: MongoStream } ,
444+ S3 { #[ pin] inner: S3Stream } ,
344445 }
345446}
346447
347- impl < FileStream , MongoStream > Stream for SearchResultStream < FileStream , MongoStream >
448+ impl < FileStream , MongoStream , S3Stream > Stream
449+ for SearchResultStream < FileStream , MongoStream , S3Stream >
348450where
349451 FileStream : Stream < Item = Result < String , ClientError > > ,
350452 MongoStream : Stream < Item = Result < String , ClientError > > ,
453+ S3Stream : Stream < Item = Result < String , ClientError > > ,
351454{
352455 type Item = Result < String , ClientError > ;
353456
@@ -358,6 +461,7 @@ where
358461 let poll = match self . project ( ) {
359462 SearchResultStreamProj :: File { inner } => inner. poll_next ( cx) ,
360463 SearchResultStreamProj :: Mongo { inner } => inner. poll_next ( cx) ,
464+ SearchResultStreamProj :: S3 { inner } => inner. poll_next ( cx) ,
361465 } ;
362466 if let std:: task:: Poll :: Ready ( Some ( Err ( err) ) ) = & poll {
363467 tracing:: error!( "An error occurred when streaming results: {}" , err) ;
0 commit comments