1010 reason = "Found 3 occurrences after enabling the lint."
1111) ]
1212
13- use parking_lot:: { Mutex , RwLock } ;
13+ use parking_lot:: { Mutex , MutexGuard , RwLock } ;
1414use std:: collections:: { HashMap , VecDeque } ;
1515use std:: io;
1616use std:: num:: NonZero ;
@@ -29,7 +29,7 @@ use firewood_metrics::{firewood_increment, firewood_set};
2929pub use firewood_storage:: CacheReadStrategy ;
3030use firewood_storage:: {
3131 BranchNode , Committed , FileBacked , FileIoError , HashedNodeReader , ImmutableProposal ,
32- NodeHashAlgorithm , NodeStore , TrieHash ,
32+ NodeHashAlgorithm , NodeStore , NodeStoreHeader , TrieHash ,
3333} ;
3434
3535pub ( crate ) const DB_FILE_NAME : & str = "firewood.db" ;
@@ -89,6 +89,12 @@ pub(crate) struct RevisionManager {
8989 /// is preserved on disk for historical queries.
9090 max_revisions : usize ,
9191
92+ /// Persisted metadata for the database.
93+ ///
94+ /// Loaded from disk on startup and updated during commits when nodes
95+ /// are persisted or deleted nodes are added to the free lists.
96+ header : Mutex < NodeStoreHeader > ,
97+
9298 /// FIFO queue of committed revisions kept in memory. The queue always
9399 /// contains at least one revision.
94100 in_memory_revisions : RwLock < VecDeque < CommittedRevision > > ,
@@ -157,7 +163,15 @@ impl RevisionManager {
157163 fb. lock ( ) ?;
158164
159165 let storage = Arc :: new ( fb) ;
160- let nodestore = Arc :: new ( NodeStore :: open ( storage. clone ( ) ) ?) ;
166+ let header = match NodeStoreHeader :: read_from_storage ( storage. as_ref ( ) ) {
167+ Ok ( header) => header,
168+ Err ( err) if err. kind ( ) == io:: ErrorKind :: UnexpectedEof => {
169+ // Empty file - create a new header for a fresh database
170+ NodeStoreHeader :: new ( config. node_hash_algorithm )
171+ }
172+ Err ( err) => return Err ( err. into ( ) ) ,
173+ } ;
174+ let nodestore = Arc :: new ( NodeStore :: open ( & header, storage. clone ( ) ) ?) ;
161175 let root_store = config
162176 . root_store
163177 . then ( || {
@@ -167,23 +181,26 @@ impl RevisionManager {
167181 } )
168182 . transpose ( ) ?;
169183
184+ if config. truncate {
185+ header. flush_to ( storage. as_ref ( ) ) ?;
186+ storage. set_len ( NodeStoreHeader :: SIZE ) ?;
187+ }
188+
189+ let mut by_hash = HashMap :: new ( ) ;
190+ if let Some ( hash) = nodestore. root_hash ( ) . or_default_root_hash ( ) {
191+ by_hash. insert ( hash, nodestore. clone ( ) ) ;
192+ }
193+
170194 let manager = Self {
171195 max_revisions : config. manager . max_revisions ,
196+ header : Mutex :: new ( header) ,
172197 in_memory_revisions : RwLock :: new ( VecDeque :: from ( [ nodestore. clone ( ) ] ) ) ,
173- by_hash : RwLock :: new ( Default :: default ( ) ) ,
198+ by_hash : RwLock :: new ( by_hash ) ,
174199 proposals : Mutex :: new ( Default :: default ( ) ) ,
175200 threadpool : OnceLock :: new ( ) ,
176201 root_store,
177202 } ;
178203
179- if let Some ( hash) = nodestore. root_hash ( ) . or_default_root_hash ( ) {
180- manager. by_hash . write ( ) . insert ( hash, nodestore. clone ( ) ) ;
181- }
182-
183- if config. truncate {
184- nodestore. flush_header_with_padding ( ) ?;
185- }
186-
187204 // On startup, we always write the latest revision to RootStore
188205 if let Some ( root_hash) = manager. current_revision ( ) . root_hash ( ) {
189206 let root_address = manager. current_revision ( ) . root_address ( ) . ok_or (
@@ -231,7 +248,10 @@ impl RevisionManager {
231248 } ) ;
232249 }
233250
234- let mut committed = proposal. as_committed ( & current_revision) ;
251+ let committed = proposal. as_committed ( ) ;
252+
253+ // Lock header for the duration of reaping and persistence
254+ let mut header = self . header . lock ( ) ;
235255
236256 // 2. Revision reaping
237257 // When we exceed max_revisions, remove the oldest revision from memory.
@@ -257,7 +277,7 @@ impl RevisionManager {
257277 // This guarantee is there because we have a `&mut self` reference to the manager, so
258278 // the compiler guarantees we are the only one using this manager.
259279 match Arc :: try_unwrap ( oldest) {
260- Ok ( oldest) => oldest. reap_deleted ( & mut committed ) ?,
280+ Ok ( oldest) => oldest. reap_deleted ( & mut header ) ?,
261281 Err ( original) => {
262282 warn ! ( "Oldest revision could not be reaped; still referenced" ) ;
263283 self . in_memory_revisions . write ( ) . push_front ( original) ;
@@ -273,9 +293,7 @@ impl RevisionManager {
273293 }
274294
275295 // 3. Persist to disk.
276- // TODO: We can probably do this in another thread, but it requires that
277- // we move the header out of NodeStore, which is in a future PR.
278- committed. persist ( ) ?;
296+ committed. persist ( & mut header) ?;
279297
280298 // 4. Persist revision to root store
281299 if let Some ( store) = & self . root_store
@@ -298,6 +316,10 @@ impl RevisionManager {
298316 self . by_hash . write ( ) . insert ( hash, committed. clone ( ) ) ;
299317 }
300318
319+ // At this point, we can release the lock on the header as the header
320+ // and the last committed revision are up-to-date.
321+ drop ( header) ;
322+
301323 // 6. Proposal Cleanup
302324 // Free proposal that is being committed as well as any proposals no longer
303325 // referenced by anyone else. Track how many were discarded (dropped without commit).
@@ -405,6 +427,11 @@ impl RevisionManager {
405427 . clone ( )
406428 }
407429
430+ /// Acquires a lock on the header and returns a guard.
431+ pub ( crate ) fn locked_header ( & self ) -> MutexGuard < ' _ , NodeStoreHeader > {
432+ self . header . lock ( )
433+ }
434+
408435 /// Gets or creates a threadpool associated with the revision manager.
409436 ///
410437 /// # Panics
0 commit comments