@@ -76,7 +76,12 @@ impl<B: Blob> BlobState<B> {
7676 self . write_at ( offset, bufs) . await ?;
7777 self . sync ( ) . await
7878 } else {
79- self . blob . write_at_sync ( offset, bufs) . await
79+ // If `write_at_sync` fails, a later sync must not treat the drained
80+ // buffer as durable.
81+ self . needs_sync = true ;
82+ self . blob . write_at_sync ( offset, bufs) . await ?;
83+ self . needs_sync = false ;
84+ Ok ( ( ) )
8085 }
8186 }
8287
@@ -1750,6 +1755,30 @@ mod tests {
17501755 } ) ;
17511756 }
17521757
1758+ #[ test_traced( "DEBUG" ) ]
1759+ fn test_sync_failed_range_sync_does_not_mark_clean ( ) {
1760+ let executor = deterministic:: Runner :: default ( ) ;
1761+ executor. start ( |context : deterministic:: Context | async move {
1762+ let name = b"failed_range_sync" ;
1763+ let ( blob, size) = context. open ( "test_partition" , name) . await . unwrap ( ) ;
1764+ let cache_ref = CacheRef :: from_pooler ( & context, PAGE_SIZE , NZUsize ! ( BUFFER_SIZE ) ) ;
1765+ let append = Append :: new ( blob, size, BUFFER_SIZE , cache_ref)
1766+ . await
1767+ . unwrap ( ) ;
1768+
1769+ // Keep the write buffered so sync attempts the clean `write_at_sync` path.
1770+ append. append ( b"abc" ) . await . unwrap ( ) ;
1771+
1772+ // Removing the blob makes the range-sync flush fail.
1773+ context. remove ( "test_partition" , Some ( name) ) . await . unwrap ( ) ;
1774+ assert ! ( append. sync( ) . await . is_err( ) ) ;
1775+
1776+ // The failed `write_at_sync` must leave a pending full-sync barrier, so a
1777+ // later sync cannot report success.
1778+ assert ! ( append. sync( ) . await . is_err( ) ) ;
1779+ } ) ;
1780+ }
1781+
17531782 #[ test_traced( "DEBUG" ) ]
17541783 fn test_sync_uses_full_sync_after_prior_plain_flush ( ) {
17551784 let executor = deterministic:: Runner :: default ( ) ;
0 commit comments