Skip to content

[glue] DatabaseSet::finalize holds the write lock across the commit fsync, serializing readers #4137

Description

@christopherwxyz

Summary

The DatabaseSet blanket impl for Arc<AsyncRwLock<T>> (glue/src/stateful/db/mod.rs) holds the exclusive write lock across the entire finalize — both apply_batch (the in-memory index/bitmap mutation) and the durability fsync:

async fn finalize(&self, batches: Self::Merkleized) {
    let mut database = self.write().await;
    finalize_or_panic(&mut *database, batches, None).await; // apply_batch + sync, both under write()
}

finalize_or_panic calls ManagedDb::finalize, whose concrete impls are self.apply_batch(..).await?; self.sync().await. Any task reading committed state through the same AsyncRwLock (e.g. proof/query reads served from a database while it applies blocks) therefore blocks for the whole finalize, including the fsync — the long, variable pole.

Proposal

Hold the exclusive lock only for apply_batch, then run the fsync under a shared lock:

{ let mut db = self.write().await; db.apply_batch(b).await... }  // exclusive, brief
{ let db = self.read().await;      db.commit().await... }        // shared — fsync off readers

Mechanically: add apply_batch(&mut self) + commit(&self) to ManagedDb (the two primitives the concrete impls already call inherently), make finalize a defaulted combinator over them, and split the blanket + tuple DatabaseSet::finalize into the two lock scopes above. ManagedDb::finalize stays as a convenience default.

Soundness

apply_batch publishes the new logical state in memory; sync is durability-only. A reader entering the apply→commit gap observes the committed-in-memory state, and crash semantics are unchanged (apply_batch was never durable before sync). This assumes a single writer per database (the stateful actor serializes finalizes); concurrent writers are out of scope.

Dependency

Requires storage commit/sync to take &self — i.e. #3118. They were &self in v2026.5.0 but are currently &mut self on main (e.g. qmdb::any::Db::sync(&mut self)), so this is gated on #3118. With &self storage the glue change is small and additive.

Out of scope

Owned snapshots / COW / MVCC / snapshot isolation. apply_batch stays briefly exclusive.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    Status
    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions