@@ -126,12 +126,13 @@ where
126126 // memcpy, the on-disk frame either has len == 0 (unwritten) or a
127127 // truncated body under a valid len — both are discarded on recovery.
128128 let header_offset = self . current_offset ;
129- let header = & frame [ .. size_of :: < u32 > ( ) ] ;
130- // Reserve the header slot first (as zeros = "not committed"), then
131- // write the body, then patch the header last (see #268).
132- self . append_frame_to_mmap ( header ) . await ?;
129+ // Reserve the header slot as zeros first: an unwritten slot can never
130+ // be mistaken for a committed frame. Then write the body, and patch
131+ // the real length header last (see #268).
132+ self . append_frame_to_mmap ( & [ 0u8 ; size_of :: < u32 > ( ) ] ) . await ?;
133133 let body = & frame[ size_of :: < u32 > ( ) ..] ;
134134 self . append_frame_to_mmap ( body) . await ?;
135+ let header = & frame[ ..size_of :: < u32 > ( ) ] ;
135136 self . patch_frame_header_at ( header_offset, header) . await ?;
136137
137138 self . unsynced_bytes += frame. len ( ) ;
@@ -734,6 +735,47 @@ mod tests {
734735 assert_eq ! ( payloads, vec![ b"complete" . to_vec( ) ] ) ;
735736 }
736737
738+ /// #268: after discarding a torn tail the file itself must be truncated,
739+ /// so the segment stays clean even after it later becomes an intermediate
740+ /// segment (rotation) — otherwise the next restart would fail.
741+ #[ tokio:: test]
742+ async fn test_build_truncates_torn_tail_from_disk ( ) {
743+ let wal_dir = setup_test_wal_dir ( "truncate_torn_tail" ) . await ;
744+ let config = get_test_config ( & wal_dir) ;
745+ write_wal_file (
746+ & config,
747+ 1 ,
748+ & vec ! [ create_entry( EntryType :: Insert , Some ( "complete" ) ) ] ,
749+ )
750+ . await ;
751+
752+ let path = wal_dir. join ( format ! ( "00000001.{}" , config. wal_extension) ) ;
753+ let intact_len = tokio:: fs:: metadata ( & path) . await . unwrap ( ) . len ( ) ;
754+ {
755+ let mut file = tokio:: fs:: OpenOptions :: new ( )
756+ . append ( true )
757+ . open ( & path)
758+ . await
759+ . unwrap ( ) ;
760+ file. write_all ( & 8u32 . to_le_bytes ( ) ) . await . unwrap ( ) ;
761+ file. write_all ( & [ 1 , 2 , 3 ] ) . await . unwrap ( ) ;
762+ }
763+ assert ! ( tokio:: fs:: metadata( & path) . await . unwrap( ) . len( ) > intact_len) ;
764+
765+ let wal_manager = WALBuilder :: new ( & config)
766+ . build ( BincodeDecoder :: new ( ) , BincodeEncoder :: new ( ) )
767+ . await
768+ . expect ( "a torn tail must not prevent startup" ) ;
769+ assert_eq ! ( wal_manager. pending_entries( ) . len( ) , 1 ) ;
770+
771+ // The torn bytes must be gone from the file itself.
772+ assert_eq ! (
773+ tokio:: fs:: metadata( & path) . await . unwrap( ) . len( ) ,
774+ intact_len,
775+ "the torn tail must be truncated from disk, not just ignored"
776+ ) ;
777+ }
778+
737779 #[ tokio:: test]
738780 async fn test_build_rejects_corrupt_intermediate_segment ( ) {
739781 let wal_dir = setup_test_wal_dir ( "corrupt_intermediate_segment" ) . await ;
0 commit comments