@@ -10,7 +10,9 @@ use std::io::Cursor;
1010
1111use crate :: retry:: { RetryPolicy , with_retry} ;
1212
13- use super :: { AsyncReader , CopySource , ObjectStream , Result , Storage , StorageError } ;
13+ use super :: {
14+ AsyncReader , CopySource , ObjectStream , PutIfAbsentOutcome , Result , Storage , StorageError ,
15+ } ;
1416
1517/// Buffer-then-retry threshold for put bodies (matches wal-g's small-object
1618/// path: sentinels, history files, manifest fragments)
@@ -54,6 +56,31 @@ impl<S: Storage + 'static> Storage for RetryingStorage<S> {
5456 . await
5557 }
5658
59+ async fn put_if_absent (
60+ & self ,
61+ key : & str ,
62+ mut body : AsyncReader ,
63+ size_hint : Option < u64 > ,
64+ ) -> Result < PutIfAbsentOutcome > {
65+ // Only retry small known-size bodies;
66+ let bufferable = matches ! ( size_hint, Some ( s) if s <= PUT_RETRY_BUFFER_THRESHOLD ) ;
67+ if !bufferable {
68+ return self . inner . put_if_absent ( key, body, size_hint) . await ;
69+ }
70+ let mut buf = Vec :: with_capacity ( size_hint. unwrap_or ( 0 ) as usize ) ;
71+ tokio:: io:: copy ( & mut body, & mut buf) . await ?;
72+ let bytes = Bytes :: from ( buf) ;
73+ let len = bytes. len ( ) as u64 ;
74+ with_retry ( & self . policy , StorageError :: is_transient, || {
75+ let bytes = bytes. clone ( ) ;
76+ async move {
77+ let reader: AsyncReader = Box :: pin ( Cursor :: new ( bytes) ) ;
78+ self . inner . put_if_absent ( key, reader, Some ( len) ) . await
79+ }
80+ } )
81+ . await
82+ }
83+
5784 async fn get ( & self , key : & str ) -> Result < AsyncReader > {
5885 with_retry ( & self . policy , StorageError :: is_transient, || async {
5986 self . inner . get ( key) . await
@@ -113,6 +140,8 @@ mod tests {
113140 put_bodies : Mutex < Vec < Vec < u8 > > > ,
114141 get_script : Mutex < Vec < StubResult > > ,
115142 put_script : Mutex < Vec < StubResult > > ,
143+ put_if_absent_calls : AtomicU32 ,
144+ put_if_absent_script : Mutex < Vec < StubIfAbsent > > ,
116145 }
117146
118147 enum StubResult {
@@ -121,6 +150,12 @@ mod tests {
121150 Ok ,
122151 }
123152
153+ enum StubIfAbsent {
154+ Transient ,
155+ Created ,
156+ AlreadyExists ,
157+ }
158+
124159 impl StubStorage {
125160 fn new ( ) -> Self {
126161 Self {
@@ -129,6 +164,8 @@ mod tests {
129164 put_bodies : Mutex :: new ( Vec :: new ( ) ) ,
130165 get_script : Mutex :: new ( Vec :: new ( ) ) ,
131166 put_script : Mutex :: new ( Vec :: new ( ) ) ,
167+ put_if_absent_calls : AtomicU32 :: new ( 0 ) ,
168+ put_if_absent_script : Mutex :: new ( Vec :: new ( ) ) ,
132169 }
133170 }
134171 }
@@ -181,6 +218,25 @@ mod tests {
181218 async fn delete ( & self , _key : & str ) -> Result < ( ) > {
182219 Ok ( ( ) )
183220 }
221+ async fn put_if_absent (
222+ & self ,
223+ _key : & str ,
224+ mut body : AsyncReader ,
225+ _size_hint : Option < u64 > ,
226+ ) -> Result < PutIfAbsentOutcome > {
227+ self . put_if_absent_calls . fetch_add ( 1 , Ordering :: SeqCst ) ;
228+ let mut buf = Vec :: new ( ) ;
229+ body. read_to_end ( & mut buf) . await ?;
230+ self . put_bodies . lock ( ) . unwrap ( ) . push ( buf) ;
231+ match self . put_if_absent_script . lock ( ) . unwrap ( ) . remove ( 0 ) {
232+ StubIfAbsent :: Transient => Err ( StorageError :: Http {
233+ status : 503 ,
234+ body : "stub down" . into ( ) ,
235+ } ) ,
236+ StubIfAbsent :: Created => Ok ( PutIfAbsentOutcome :: Created ) ,
237+ StubIfAbsent :: AlreadyExists => Ok ( PutIfAbsentOutcome :: AlreadyExists ) ,
238+ }
239+ }
184240 }
185241
186242 fn fast_policy ( ) -> RetryPolicy {
@@ -275,4 +331,53 @@ mod tests {
275331 assert ! ( matches!( r, Err ( StorageError :: Http { status: 503 , .. } ) ) ) ;
276332 assert_eq ! ( retry. inner. put_calls. load( Ordering :: SeqCst ) , 1 ) ;
277333 }
334+
335+ #[ tokio:: test]
336+ async fn put_if_absent_retries_transient_then_returns_already_exists ( ) {
337+ let stub = StubStorage :: new ( ) ;
338+ stub. put_if_absent_script . lock ( ) . unwrap ( ) . extend ( [
339+ StubIfAbsent :: Transient ,
340+ StubIfAbsent :: Transient ,
341+ StubIfAbsent :: AlreadyExists ,
342+ ] ) ;
343+ let retry = RetryingStorage :: new ( stub, fast_policy ( ) ) ;
344+ let body: AsyncReader = Box :: pin ( Cursor :: new ( b"seg" . to_vec ( ) ) ) ;
345+ let outcome = retry. put_if_absent ( "k" , body, Some ( 3 ) ) . await . unwrap ( ) ;
346+ // AlreadyExists on a retry counts as success (object exists), not an error.
347+ assert ! ( matches!( outcome, PutIfAbsentOutcome :: AlreadyExists ) ) ;
348+ assert_eq ! ( retry. inner. put_if_absent_calls. load( Ordering :: SeqCst ) , 3 ) ;
349+ // body replayed byte-identically across attempts
350+ let bodies = retry. inner . put_bodies . lock ( ) . unwrap ( ) ;
351+ assert_eq ! ( bodies. len( ) , 3 ) ;
352+ assert ! ( bodies. iter( ) . all( |b| b == b"seg" ) ) ;
353+ }
354+
355+ #[ tokio:: test]
356+ async fn put_if_absent_retries_transient_then_created ( ) {
357+ let stub = StubStorage :: new ( ) ;
358+ stub. put_if_absent_script
359+ . lock ( )
360+ . unwrap ( )
361+ . extend ( [ StubIfAbsent :: Transient , StubIfAbsent :: Created ] ) ;
362+ let retry = RetryingStorage :: new ( stub, fast_policy ( ) ) ;
363+ let body: AsyncReader = Box :: pin ( Cursor :: new ( b"seg" . to_vec ( ) ) ) ;
364+ let outcome = retry. put_if_absent ( "k" , body, Some ( 3 ) ) . await . unwrap ( ) ;
365+ assert ! ( matches!( outcome, PutIfAbsentOutcome :: Created ) ) ;
366+ assert_eq ! ( retry. inner. put_if_absent_calls. load( Ordering :: SeqCst ) , 2 ) ;
367+ }
368+
369+ #[ tokio:: test]
370+ async fn put_if_absent_bypasses_retry_when_size_unknown ( ) {
371+ let stub = StubStorage :: new ( ) ;
372+ stub. put_if_absent_script
373+ . lock ( )
374+ . unwrap ( )
375+ . push ( StubIfAbsent :: Transient ) ;
376+ let retry = RetryingStorage :: new ( stub, fast_policy ( ) ) ;
377+ // size_hint = None → not bufferable → single pass-through attempt (mirrors put).
378+ let body: AsyncReader = Box :: pin ( Cursor :: new ( b"seg" . to_vec ( ) ) ) ;
379+ let r = retry. put_if_absent ( "k" , body, None ) . await ;
380+ assert ! ( matches!( r, Err ( StorageError :: Http { status: 503 , .. } ) ) ) ;
381+ assert_eq ! ( retry. inner. put_if_absent_calls. load( Ordering :: SeqCst ) , 1 ) ;
382+ }
278383}
0 commit comments