@@ -75,6 +75,9 @@ pub struct Batcher {
7575 s3_client : S3Client ,
7676 s3_bucket_name : String ,
7777 download_endpoint : String ,
78+ s3_client_secondary : S3Client ,
79+ s3_bucket_name_secondary : String ,
80+ download_endpoint_secondary : String ,
7881 eth_ws_url : String ,
7982 eth_ws_url_fallback : String ,
8083 batcher_signer : Arc < SignerMiddlewareT > ,
@@ -106,15 +109,36 @@ impl Batcher {
106109 dotenv ( ) . ok ( ) ;
107110
108111 // https://docs.aws.amazon.com/sdk-for-rust/latest/dg/localstack.html
109- let upload_endpoint = env:: var ( "UPLOAD_ENDPOINT" ) . ok ( ) ;
112+ // Primary S3 configuration
113+ let s3_config_primary = s3:: S3Config {
114+ access_key_id : env:: var ( "AWS_ACCESS_KEY_ID" ) . ok ( ) ,
115+ secret_access_key : env:: var ( "AWS_SECRET_ACCESS_KEY" ) . ok ( ) ,
116+ region : env:: var ( "AWS_REGION" ) . ok ( ) ,
117+ endpoint_url : env:: var ( "UPLOAD_ENDPOINT" ) . ok ( ) ,
118+ } ;
110119
111120 let s3_bucket_name =
112121 env:: var ( "AWS_BUCKET_NAME" ) . expect ( "AWS_BUCKET_NAME not found in environment" ) ;
113122
114123 let download_endpoint =
115124 env:: var ( "DOWNLOAD_ENDPOINT" ) . expect ( "DOWNLOAD_ENDPOINT not found in environment" ) ;
116125
117- let s3_client = s3:: create_client ( upload_endpoint) . await ;
126+ let s3_client = s3:: create_client ( s3_config_primary) . await ;
127+
128+ // Secondary S3 configuration
129+ let s3_config_secondary = s3:: S3Config {
130+ access_key_id : env:: var ( "AWS_ACCESS_KEY_ID_SECONDARY" ) . ok ( ) ,
131+ secret_access_key : env:: var ( "AWS_SECRET_ACCESS_KEY_SECONDARY" ) . ok ( ) ,
132+ region : env:: var ( "AWS_REGION_SECONDARY" ) . ok ( ) ,
133+ endpoint_url : env:: var ( "UPLOAD_ENDPOINT_SECONDARY" ) . ok ( ) ,
134+ } ;
135+
136+ let s3_bucket_name_secondary = env:: var ( "AWS_BUCKET_NAME_SECONDARY" )
137+ . expect ( "AWS_BUCKET_NAME_SECONDARY not found in environment" ) ;
138+ let download_endpoint_secondary = env:: var ( "DOWNLOAD_ENDPOINT_SECONDARY" )
139+ . expect ( "DOWNLOAD_ENDPOINT_SECONDARY not found in environment" ) ;
140+
141+ let s3_client_secondary = s3:: create_client ( s3_config_secondary) . await ;
118142
119143 let config = ConfigFromYaml :: new ( config_file) ;
120144 // Ensure max_batch_bytes_size can at least hold one proof of max_proof_size,
@@ -252,6 +276,9 @@ impl Batcher {
252276 s3_client,
253277 s3_bucket_name,
254278 download_endpoint,
279+ s3_client_secondary,
280+ s3_bucket_name_secondary,
281+ download_endpoint_secondary,
255282 eth_ws_url : config. eth_ws_url ,
256283 eth_ws_url_fallback : config. eth_ws_url_fallback ,
257284 batcher_signer,
@@ -1541,7 +1568,16 @@ impl Batcher {
15411568 let batch_merkle_root_hex = hex:: encode ( batch_merkle_root) ;
15421569 info ! ( "Batch merkle root: 0x{}" , batch_merkle_root_hex) ;
15431570 let file_name = batch_merkle_root_hex. clone ( ) + ".json" ;
1544- let batch_data_pointer: String = "" . to_owned ( ) + & self . download_endpoint + "/" + & file_name;
1571+
1572+ let batch_data_pointer = self . upload_batch_to_multiple_s3 ( batch_bytes, & file_name) . await ?;
1573+ if let Err ( e) = self
1574+ . telemetry
1575+ . task_uploaded_to_s3 ( & batch_merkle_root_hex)
1576+ . await
1577+ {
1578+ warn ! ( "Failed to send task status to telemetry: {:?}" , e) ;
1579+ } ;
1580+ info ! ( "Batch upload to: {}" , batch_data_pointer) ;
15451581
15461582 let num_proofs_in_batch = leaves. len ( ) ;
15471583 let gas_per_proof = ( self . constant_gas_cost ( )
@@ -1577,16 +1613,6 @@ impl Batcher {
15771613 . gas_price_used_on_latest_batch
15781614 . set ( gas_price. as_u64 ( ) as i64 ) ;
15791615
1580- info ! ( "Uploading batch to S3..." ) ;
1581- self . upload_batch_to_s3 ( batch_bytes, & file_name) . await ?;
1582- if let Err ( e) = self
1583- . telemetry
1584- . task_uploaded_to_s3 ( & batch_merkle_root_hex)
1585- . await
1586- {
1587- warn ! ( "Failed to send task status to telemetry: {:?}" , e) ;
1588- } ;
1589- info ! ( "Batch sent to S3 with name: {}" , file_name) ;
15901616 if let Err ( e) = self
15911617 . telemetry
15921618 . task_created (
@@ -1857,22 +1883,61 @@ impl Batcher {
18571883 unlocked
18581884 }
18591885
1886+ /// Uploads the batch to both S3 buckets and returns the comma-separated URLs of successful uploads.
1887+ /// Returns an error only if all uploads fail.
1888+ async fn upload_batch_to_multiple_s3 (
1889+ & self ,
1890+ batch_bytes : & [ u8 ] ,
1891+ file_name : & str ,
1892+ ) -> Result < String , BatcherError > {
1893+ // Upload to both S3 buckets and collect successful URLs
1894+ let mut successful_urls = Vec :: new ( ) ;
1895+
1896+ // Try primary S3 upload
1897+ if let Ok ( _) = self . upload_batch_to_s3 ( & self . s3_client , batch_bytes, file_name, & self . s3_bucket_name ) . await {
1898+ let primary_url = format ! ( "{}/{}" , self . download_endpoint, file_name) ;
1899+ successful_urls. push ( primary_url. clone ( ) ) ;
1900+ info ! ( "Successfully uploaded batch to primary S3: {}" , primary_url) ;
1901+ } else {
1902+ warn ! ( "Failed to upload batch to primary S3" ) ;
1903+ }
1904+
1905+ // Try secondary S3 upload
1906+ if let Ok ( _) = self . upload_batch_to_s3 ( & self . s3_client_secondary , batch_bytes, file_name, & self . s3_bucket_name_secondary ) . await {
1907+ let secondary_url = format ! ( "{}/{}" , self . download_endpoint_secondary, file_name) ;
1908+ successful_urls. push ( secondary_url. clone ( ) ) ;
1909+ info ! ( "Successfully uploaded batch to secondary S3: {}" , secondary_url) ;
1910+ } else {
1911+ warn ! ( "Failed to upload batch to secondary S3" ) ;
1912+ }
1913+
1914+ // If no uploads succeeded, return error
1915+ if successful_urls. is_empty ( ) {
1916+ error ! ( "Failed to upload batch to both S3 buckets" ) ;
1917+ return Err ( BatcherError :: BatchUploadError ( "Failed to upload to any S3 bucket" . to_string ( ) ) ) ;
1918+ }
1919+
1920+ Ok ( successful_urls. join ( "," ) )
1921+ }
1922+
18601923 /// Uploads the batch to s3.
18611924 /// Retries on recoverable errors using exponential backoff up to `ETHEREUM_CALL_MAX_RETRIES` times:
18621925 /// (0,5 secs - 1 secs - 2 secs - 4 secs - 8 secs).
18631926 async fn upload_batch_to_s3 (
18641927 & self ,
1928+ s3_client : & S3Client ,
18651929 batch_bytes : & [ u8 ] ,
18661930 file_name : & str ,
1931+ bucket_name : & str ,
18671932 ) -> Result < ( ) , BatcherError > {
18681933 let start = Instant :: now ( ) ;
18691934 let result = retry_function (
18701935 || {
18711936 Self :: upload_batch_to_s3_retryable (
18721937 batch_bytes,
18731938 file_name,
1874- self . s3_client . clone ( ) ,
1875- & self . s3_bucket_name ,
1939+ s3_client. clone ( ) ,
1940+ bucket_name ,
18761941 )
18771942 } ,
18781943 ETHEREUM_CALL_MIN_RETRY_DELAY ,
0 commit comments