@@ -375,8 +375,7 @@ impl FileSink {
375375 let input_next = input. next ( ) ;
376376
377377 let next_timer_deadline = flush_deadlines. peek ( )
378- . map ( |r| ( r. 0 ) . 0 )
379- . copied ( ) ;
378+ . map ( |& std:: cmp:: Reverse ( ( d, _, _) ) | d) ;
380379
381380 tokio:: select! {
382381 event = input_next => {
@@ -458,9 +457,6 @@ impl FileSink {
458457 ) ;
459458 * generation += 1 ;
460459 }
461- // Reset the file-handle idle deadline so the file stays open while
462- // events accumulate for this path.
463- self . files. reset_at( & path, self . deadline_at( ) ) ;
464460 // Flush immediately when the batch reaches the item or byte limit.
465461 let needs_flush = buffers. get( & path) . map_or( false , |( events, _) | {
466462 let total_size: usize = events. iter( )
@@ -637,18 +633,21 @@ impl FileSink {
637633 self . files . get_mut ( & path) . unwrap ( )
638634 } ;
639635
640- // Encode the entire batch into one buffer, then issue a single write_all per partition.
641- // This reduces write syscalls from O(events) to O(1) per batch.
642- let mut batch_buffer = BytesMut :: new ( ) ;
643- let mut succeeded: Vec < ( EventFinalizers , JsonSize ) > = Vec :: with_capacity ( events. len ( ) ) ;
636+ // Encode each event individually so we can write them one at a time.
637+ // This ensures that if a write fails partway through (e.g. ENOSPC),
638+ // events already written are acknowledged Delivered and only the
639+ // remaining events are retried, avoiding silent duplicates.
640+ let mut encoded: Vec < ( BytesMut , EventFinalizers , JsonSize ) > =
641+ Vec :: with_capacity ( events. len ( ) ) ;
644642
645643 trace ! ( message = "Encoding batch." , batch_size = events. len( ) , path = ?path) ;
646644 for mut event in events {
647645 let event_size = event. estimated_json_encoded_size_of ( ) ;
648646 let finalizers = event. take_finalizers ( ) ;
649647 self . transformer . transform ( & mut event) ;
650- match self . encoder . encode ( event, & mut batch_buffer) {
651- Ok ( ( ) ) => succeeded. push ( ( finalizers, event_size) ) ,
648+ let mut buf = BytesMut :: new ( ) ;
649+ match self . encoder . encode ( event, & mut buf) {
650+ Ok ( ( ) ) => encoded. push ( ( buf, finalizers, event_size) ) ,
652651 Err ( error) => {
653652 finalizers. update_status ( EventStatus :: Errored ) ;
654653 emit ! ( FileIoError {
@@ -662,37 +661,51 @@ impl FileSink {
662661 }
663662 }
664663
665- if succeeded . is_empty ( ) {
664+ if encoded . is_empty ( ) {
666665 return ;
667666 }
668667
669- let byte_size = batch_buffer. len ( ) ;
670- match file. write_all ( & batch_buffer) . await {
671- Ok ( ( ) ) => {
672- for ( finalizers, event_size) in succeeded {
668+ // Write each encoded event separately so a mid-batch failure does
669+ // not retroactively erase acknowledgements for already-persisted records.
670+ let n = encoded. len ( ) ;
671+ let mut total_bytes = 0usize ;
672+ for i in 0 ..n {
673+ let ( buf, finalizers, event_size) = encoded. remove ( 0 ) ;
674+ match file. write_all ( & buf) . await {
675+ Ok ( ( ) ) => {
676+ total_bytes += buf. len ( ) ;
673677 finalizers. update_status ( EventStatus :: Delivered ) ;
674678 self . events_sent . emit ( CountByteSize ( 1 , event_size) ) ;
675679 }
676- emit ! ( FileBytesSent {
677- byte_size,
678- file: String :: from_utf8_lossy( & path) ,
679- include_file_metric_tag: self . include_file_metric_tag,
680- } ) ;
681- }
682- Err ( error) => {
683- let dropped_events = succeeded. len ( ) ;
684- for ( finalizers, _) in succeeded {
680+ Err ( error) => {
681+ // This event failed. Mark all remaining encoded events as
682+ // Errored so they are retried instead of silently delivered.
685683 finalizers. update_status ( EventStatus :: Errored ) ;
684+ for ( _, f, _) in encoded. drain ( ..) {
685+ f. update_status ( EventStatus :: Errored ) ;
686+ }
687+ emit ! ( FileIoError {
688+ code: "failed_writing_file" ,
689+ message: "Failed to write the file." ,
690+ error,
691+ path: & path,
692+ dropped_events: n - i,
693+ } ) ;
694+ emit ! ( FileBytesSent {
695+ byte_size: total_bytes,
696+ file: String :: from_utf8_lossy( & path) ,
697+ include_file_metric_tag: self . include_file_metric_tag,
698+ } ) ;
699+ return ;
686700 }
687- emit ! ( FileIoError {
688- code: "failed_writing_file" ,
689- message: "Failed to write the file." ,
690- error,
691- path: & path,
692- dropped_events,
693- } ) ;
694701 }
695702 }
703+
704+ emit ! ( FileBytesSent {
705+ byte_size: total_bytes,
706+ file: String :: from_utf8_lossy( & path) ,
707+ include_file_metric_tag: self . include_file_metric_tag,
708+ } ) ;
696709 }
697710
698711 async fn should_truncate ( & mut self , bytes_path : & BytesPath , path : & bytes:: Bytes ) -> bool {
0 commit comments