@@ -28,11 +28,12 @@ use std::cell::RefCell;
2828use std:: collections:: btree_map:: Entry ;
2929use std:: collections:: { BTreeMap , HashMap , VecDeque } ;
3030use std:: io:: Write ;
31+ use std:: time:: Duration ;
3132use tlog_tiles:: { LookupKey , PendingLogEntry , SequenceMetadata } ;
3233use tokio:: sync:: Mutex ;
3334use util:: now_millis;
3435use worker:: {
35- js_sys, kv, kv:: KvStore , wasm_bindgen, Bucket , Env , Error , HttpMetadata , Result , State ,
36+ js_sys, kv, kv:: KvStore , wasm_bindgen, Bucket , Delay , Env , Error , HttpMetadata , Result , State ,
3637 Storage , Stub ,
3738} ;
3839
@@ -513,6 +514,36 @@ impl LockBackend for State {
513514 }
514515}
515516
517+ // R2 retry config: 3 retries with exponential backoff (100ms -> 200ms -> 400ms).
518+ pub const R2_MAX_RETRIES : u32 = 3 ;
519+ pub const R2_BASE_DELAY_MS : u64 = 100 ;
520+
521+ /// Retries an async operation with exponential backoff.
522+ ///
523+ /// # Errors
524+ ///
525+ /// Returns the last error if all retry attempts fail.
526+ pub async fn with_retry < T , F , Fut > ( max_retries : u32 , base_delay_ms : u64 , operation : F ) -> Result < T >
527+ where
528+ F : Fn ( ) -> Fut ,
529+ Fut : std:: future:: Future < Output = Result < T > > ,
530+ {
531+ let mut last_error = None ;
532+ for attempt in 0 ..=max_retries {
533+ match operation ( ) . await {
534+ Ok ( result) => return Ok ( result) ,
535+ Err ( e) => {
536+ last_error = Some ( e) ;
537+ if attempt < max_retries {
538+ let delay_ms = base_delay_ms * ( 1 << attempt) ;
539+ Delay :: from ( Duration :: from_millis ( delay_ms) ) . await ;
540+ }
541+ }
542+ }
543+ }
544+ Err ( last_error. expect ( "with_retry: at least one attempt should have been made" ) )
545+ }
546+
516547pub trait ObjectBackend {
517548 /// Upload the object with the given key and data to the object backend,
518549 /// adding additional HTTP metadata headers based on the provided options.
@@ -561,31 +592,39 @@ impl ObjectBackend for ObjectBucket {
561592 opts : & UploadOptions ,
562593 ) -> Result < ( ) > {
563594 let start = now_millis ( ) ;
564- let mut metadata = HttpMetadata :: default ( ) ;
565- if let Some ( content_type) = & opts. content_type {
566- metadata. content_type = Some ( content_type. to_string ( ) ) ;
595+ let content_type = opts
596+ . content_type
597+ . clone ( )
598+ . unwrap_or_else ( || "application/octet-stream" . into ( ) ) ;
599+ let cache_control = if opts. immutable {
600+ "public, max-age=604800, immutable"
567601 } else {
568- metadata. content_type = Some ( "application/octet-stream" . into ( ) ) ;
569- }
570- if opts. immutable {
571- metadata. cache_control = Some ( "public, max-age=604800, immutable" . into ( ) ) ;
572- } else {
573- metadata. cache_control = Some ( "no-store" . into ( ) ) ;
574- }
602+ "no-store"
603+ } ;
575604 let value: Vec < u8 > = data. into ( ) ;
605+ let key_str = key. as_ref ( ) ;
576606 self . metrics
577607 . as_ref ( )
578608 . inspect ( |& m| m. upload_size_bytes . observe ( value. len ( ) . as_f64 ( ) ) ) ;
579- self . bucket
580- . put ( key. as_ref ( ) , value. clone ( ) )
581- . http_metadata ( metadata)
582- . execute ( )
583- . await
584- . inspect_err ( |_| {
585- self . metrics
586- . as_ref ( )
587- . inspect ( |& m| m. errors . with_label_values ( & [ "put" ] ) . inc ( ) ) ;
588- } ) ?;
609+
610+ with_retry ( R2_MAX_RETRIES , R2_BASE_DELAY_MS , || async {
611+ let metadata = HttpMetadata {
612+ content_type : Some ( content_type. clone ( ) ) ,
613+ cache_control : Some ( cache_control. into ( ) ) ,
614+ ..Default :: default ( )
615+ } ;
616+ self . bucket
617+ . put ( key_str, value. clone ( ) )
618+ . http_metadata ( metadata)
619+ . execute ( )
620+ . await
621+ } )
622+ . await
623+ . inspect_err ( |_| {
624+ self . metrics
625+ . as_ref ( )
626+ . inspect ( |& m| m. errors . with_label_values ( & [ "put" ] ) . inc ( ) ) ;
627+ } ) ?;
589628
590629 self . metrics . as_ref ( ) . inspect ( |& m| {
591630 m. duration
@@ -598,20 +637,25 @@ impl ObjectBackend for ObjectBucket {
598637
599638 async fn fetch < S : AsRef < str > > ( & self , key : S ) -> Result < Option < Vec < u8 > > > {
600639 let start = now_millis ( ) ;
601- let res = match self . bucket . get ( key. as_ref ( ) ) . execute ( ) . await ? {
602- Some ( obj ) => {
603- let body = obj
604- . body ( )
605- . ok_or_else ( || format ! ( "missing object body: {}" , key . as_ref ( ) ) ) ? ;
606- let bytes = body . bytes ( ) . await . inspect_err ( |_| {
607- self . metrics . as_ref ( ) . inspect ( | & m| {
608- m . errors . with_label_values ( & [ "get" ] ) . inc ( ) ;
609- } ) ;
610- } ) ? ;
611- Ok ( Some ( bytes ) )
640+ let key_str = key. as_ref ( ) ;
641+ let res = with_retry ( R2_MAX_RETRIES , R2_BASE_DELAY_MS , || async {
642+ match self . bucket . get ( key_str ) . execute ( ) . await ? {
643+ Some ( obj ) => {
644+ let body = obj
645+ . body ( )
646+ . ok_or_else ( || format ! ( "missing object body: {}" , key_str ) ) ? ;
647+ let bytes = body . bytes ( ) . await ? ;
648+ Ok ( Some ( bytes ) )
649+ }
650+ None => Ok ( None ) ,
612651 }
613- None => Ok ( None ) ,
614- } ;
652+ } )
653+ . await
654+ . inspect_err ( |_| {
655+ self . metrics
656+ . as_ref ( )
657+ . inspect ( |& m| m. errors . with_label_values ( & [ "get" ] ) . inc ( ) ) ;
658+ } ) ;
615659 self . metrics . as_ref ( ) . inspect ( |& m| {
616660 m. duration
617661 . with_label_values ( & [ "get" ] )
0 commit comments