11use std:: fs:: { File , OpenOptions } ;
2- use std:: io;
2+ use std:: io:: { self , BufWriter , Seek , SeekFrom , Write } ;
33use std:: os:: unix:: fs:: FileExt ;
44use std:: path:: Path ;
55
@@ -11,9 +11,8 @@ const TXPTR_LEN_BYTES: usize = 28;
1111const BLOCK_TX_END_LEN_BYTES : usize = 4 ;
1212const LINK_LEN_BYTES : usize = 8 ;
1313
14- /// Target size of the in-memory append buffer, in bytes. Chosen to amortize
15- /// the per-write syscall cost without making tail reads (which must scan the
16- /// buffer) expensive. Each index instance holds at most one such buffer.
14+ /// Capacity of the [`BufWriter`] wrapping each index file. Chosen to amortize
15+ /// the per-write syscall cost. Each index instance holds at most one such buffer.
1716const APPEND_BUF_CAP_BYTES : usize = 64 * 1024 ;
1817
1918pub const OUTID_NONE : u64 = u64:: MAX ;
@@ -95,22 +94,13 @@ impl TxPtr {
9594
9695/// Fixed-width append-mostly log of `N`-byte records.
9796///
98- /// Appends are staged in an in-memory buffer and flushed to disk in batches
99- /// via positional writes (`pwrite`), avoiding the per-record `lseek + write`
100- /// pair that dominates parser IO. Random-access `get`/`set` operations are
101- /// served from the buffer when they target not-yet-flushed records, so callers
102- /// that read the tail mid-build (e.g. the parser resolving prevouts against
103- /// just-appended tx pointers) see a consistent view without explicit flushing.
97+ /// Appends are buffered via [`BufWriter`] and flushed to disk in batches.
98+ /// `get_bytes` serves buffered tail records directly from the writer's buffer.
99+ /// `set_bytes` flushes the buffer first, then uses a positional write so the
100+ /// file cursor is undisturbed for subsequent appends.
104101#[ derive( Debug ) ]
105102struct FixedWidthIndex < const N : usize > {
106- /// Handle used for all positional IO. The file cursor is never read nor
107- /// modified; we rely on `pread`/`pwrite` via `FileExt`.
108- file : File ,
109- /// Bytes appended since the last flush. Always a multiple of `N`.
110- buf : Vec < u8 > ,
111- /// Number of records currently persisted to disk.
112- flushed_len : u64 ,
113- /// Total number of records (persisted + buffered). `flushed_len <= len`.
103+ writer : BufWriter < File > ,
114104 len : u64 ,
115105 mmap : Option < Mmap > ,
116106}
@@ -124,16 +114,14 @@ impl<const N: usize> FixedWidthIndex<N> {
124114 . truncate ( true )
125115 . open ( path) ?;
126116 Ok ( Self {
127- file,
128- buf : Vec :: with_capacity ( APPEND_BUF_CAP_BYTES ) ,
129- flushed_len : 0 ,
117+ writer : BufWriter :: with_capacity ( APPEND_BUF_CAP_BYTES , file) ,
130118 len : 0 ,
131119 mmap : None ,
132120 } )
133121 }
134122
135123 fn open ( path : impl AsRef < Path > , len_error : & ' static str ) -> io:: Result < Self > {
136- let file = OpenOptions :: new ( ) . read ( true ) . write ( true ) . open ( path) ?;
124+ let mut file = OpenOptions :: new ( ) . read ( true ) . write ( true ) . open ( path) ?;
137125 let len_bytes = file. metadata ( ) ?. len ( ) ;
138126 if len_bytes % ( N as u64 ) != 0 {
139127 return Err ( io:: Error :: new ( io:: ErrorKind :: InvalidData , len_error) ) ;
@@ -145,18 +133,17 @@ impl<const N: usize> FixedWidthIndex<N> {
145133 } else {
146134 None
147135 } ;
136+ file. seek ( SeekFrom :: End ( 0 ) ) ?;
148137 Ok ( Self {
149- file,
150- buf : Vec :: with_capacity ( APPEND_BUF_CAP_BYTES ) ,
151- flushed_len : len,
138+ writer : BufWriter :: with_capacity ( APPEND_BUF_CAP_BYTES , file) ,
152139 len,
153140 mmap,
154141 } )
155142 }
156143
157144 /// Open an existing file or create a new one without truncating existing content.
158145 fn open_or_create ( path : impl AsRef < Path > , len_error : & ' static str ) -> io:: Result < Self > {
159- let file = OpenOptions :: new ( )
146+ let mut file = OpenOptions :: new ( )
160147 . read ( true )
161148 . write ( true )
162149 . truncate ( false )
@@ -167,35 +154,26 @@ impl<const N: usize> FixedWidthIndex<N> {
167154 return Err ( io:: Error :: new ( io:: ErrorKind :: InvalidData , len_error) ) ;
168155 }
169156 let len = len_bytes / ( N as u64 ) ;
157+ file. seek ( SeekFrom :: End ( 0 ) ) ?;
170158 Ok ( Self {
171- file,
172- buf : Vec :: with_capacity ( APPEND_BUF_CAP_BYTES ) ,
173- flushed_len : len,
159+ writer : BufWriter :: with_capacity ( APPEND_BUF_CAP_BYTES , file) ,
174160 len,
175161 mmap : None ,
176162 } )
177163 }
178164
179- /// Flush the in-memory append buffer to disk via a single positional write.
180165 fn flush ( & mut self ) -> io:: Result < ( ) > {
181- if self . buf . is_empty ( ) {
182- return Ok ( ( ) ) ;
183- }
184- let offset = self . flushed_len * ( N as u64 ) ;
185- self . file . write_all_at ( & self . buf , offset) ?;
186- self . flushed_len = self . len ;
187- self . buf . clear ( ) ;
188- Ok ( ( ) )
166+ self . writer . flush ( )
189167 }
190168
191169 /// Remap the file for read access after all writes are complete.
192170 ///
193171 /// Must not be called while any concurrent writes to this file are in flight.
194172 fn remap ( & mut self ) -> io:: Result < ( ) > {
195- self . flush ( ) ?;
173+ self . writer . flush ( ) ?;
196174 self . mmap = if self . len > 0 {
197175 // Safety: no more writes will occur on this handle after remap.
198- Some ( unsafe { Mmap :: map ( & self . file ) ? } )
176+ Some ( unsafe { Mmap :: map ( self . writer . get_ref ( ) ) ? } )
199177 } else {
200178 None
201179 } ;
@@ -211,11 +189,8 @@ impl<const N: usize> FixedWidthIndex<N> {
211189 }
212190
213191 fn append_bytes ( & mut self , bytes : & [ u8 ; N ] ) -> io:: Result < u64 > {
214- self . buf . extend_from_slice ( bytes) ;
192+ self . writer . write_all ( bytes) ? ;
215193 self . len += 1 ;
216- if self . buf . len ( ) >= APPEND_BUF_CAP_BYTES {
217- self . flush ( ) ?;
218- }
219194 Ok ( self . len - 1 )
220195 }
221196
@@ -226,44 +201,34 @@ impl<const N: usize> FixedWidthIndex<N> {
226201 "set_bytes index out of range" ,
227202 ) ) ;
228203 }
229- if index >= self . flushed_len {
230- let buf_offset = ( ( index - self . flushed_len ) as usize ) * N ;
231- self . buf [ buf_offset..buf_offset + N ] . copy_from_slice ( bytes) ;
232- return Ok ( ( ) ) ;
233- }
204+ // Flush so the record is on disk before the positional write.
205+ self . writer . flush ( ) ?;
234206 let offset = index * ( N as u64 ) ;
235- self . file . write_all_at ( bytes, offset)
207+ self . writer . get_ref ( ) . write_all_at ( bytes, offset)
236208 }
237209
238210 fn get_bytes ( & self , index : u64 ) -> io:: Result < Option < [ u8 ; N ] > > {
239211 if index >= self . len {
240212 return Ok ( None ) ;
241213 }
242- if index >= self . flushed_len {
243- let buf_offset = ( ( index - self . flushed_len ) as usize ) * N ;
214+ let buf = self . writer . buffer ( ) ;
215+ let flushed_count = self . len - ( buf. len ( ) / N ) as u64 ;
216+ if index >= flushed_count {
217+ let buf_offset = ( ( index - flushed_count) as usize ) * N ;
244218 let mut out = [ 0u8 ; N ] ;
245- out. copy_from_slice ( & self . buf [ buf_offset..buf_offset + N ] ) ;
219+ out. copy_from_slice ( & buf[ buf_offset..buf_offset + N ] ) ;
246220 return Ok ( Some ( out) ) ;
247221 }
248222 let offset = index * ( N as u64 ) ;
249223 if let Some ( mmap) = & self . mmap {
250224 let offset = offset as usize ;
251- let mut buf = [ 0u8 ; N ] ;
252- buf . copy_from_slice ( & mmap[ offset..offset + N ] ) ;
253- return Ok ( Some ( buf ) ) ;
225+ let mut out = [ 0u8 ; N ] ;
226+ out . copy_from_slice ( & mmap[ offset..offset + N ] ) ;
227+ return Ok ( Some ( out ) ) ;
254228 }
255- let mut buf = [ 0u8 ; N ] ;
256- self . file . read_exact_at ( & mut buf, offset) ?;
257- Ok ( Some ( buf) )
258- }
259- }
260-
261- impl < const N : usize > Drop for FixedWidthIndex < N > {
262- fn drop ( & mut self ) {
263- // Best-effort flush so that simply dropping an index persists all
264- // appended records, matching the previous unbuffered behaviour where
265- // every `append_bytes` landed on disk synchronously.
266- let _ = self . flush ( ) ;
229+ let mut out = [ 0u8 ; N ] ;
230+ self . writer . get_ref ( ) . read_exact_at ( & mut out, offset) ?;
231+ Ok ( Some ( out) )
267232 }
268233}
269234
@@ -513,6 +478,17 @@ impl DenseIndexSet {
513478 } )
514479 }
515480
481+ /// Flush all buffered appends to disk.
482+ ///
483+ /// Must be called before any `set` on recently appended records so those
484+ /// records are on disk for the positional write.
485+ pub fn flush ( & mut self ) -> io:: Result < ( ) > {
486+ self . txptr . inner . flush ( ) ?;
487+ self . block_tx . inner . flush ( ) ?;
488+ self . in_prevout . inner . flush ( ) ?;
489+ self . out_spent . inner . flush ( )
490+ }
491+
516492 /// Map all four index files into memory for zero-syscall reads.
517493 ///
518494 /// Call once after all writes are complete.
0 commit comments