This is an automated email from the ASF dual-hosted git repository.

spetz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iggy.git


The following commit(s) were added to refs/heads/master by this push:
     new 262117cd1 perf(partitions): cut persisted-mode disk traffic per 
acknowledgment (#4146)
262117cd1 is described below

commit 262117cd143d7d1a14d69c674791eb297078a0e8
Author: Piotr Gankiewicz <[email protected]>
AuthorDate: Sat Sep 12 19:31:00 2026 +0200

    perf(partitions): cut persisted-mode disk traffic per acknowledgment (#4146)
    
    A persisted topic drove far more disk traffic than the bytes it
    ingested, and the excess barely moved when the ingest rate rose.
    The cost was per acknowledgment, not per byte.
    
    Publishing the WAL frontier created a temporary inode, wrote one
    block, fdatasynced it, renamed it over the frontier and fsynced
    the directory, once per batch. Three barriers on three inodes at
    three instants, so none could share a filesystem journal commit.
    It was also the fourth serialized barrier in a path that needs two.
    
    The frontier is now two slots in one file, published by
    overwriting the older slot in place: one aligned block into a
    file that never resizes, so no create, no rename, no directory
    barrier. The body and WAL barriers overlap and land in one
    journal window.
    
    Dropping the publication entirely was tried first and rejected.
    Without a durable length, a zeroed acknowledged record and one
    never written are indistinguishable, and recovery silently
    truncates. Two slots keep that length at no metadata cost: a
    torn slot fails its checksum and its partner holds the previous
    publication, as the rename did. An unreadable newest slot is the
    one case that loses, so recovery then walks past the frontier it
    could read and adopts the records already on disk, refusing
    rather than truncating when any fails to verify.
    
    The per-flush index fdatasync moves to the seal and checkpoint
    barriers WAL reclamation already requires, through the original
    writer handle so a writeback error still reaches it, and a failed
    sync fences before any history is reclaimed. The group-commit
    budget stops counting body bytes the WAL only references.
    
    A cheaper barrier groups fewer prepares, since grouping was only
    ever whatever queued during the previous barrier. Latency falls.
    Write count does not, until grouping is made explicit.
---
 core/configs/src/server_config/defaults.rs         |   2 +
 core/configs/src/server_config/displays.rs         |   3 +-
 core/configs/src/server_config/partition.rs        |  30 +
 core/integration/tests/cluster/crash_durability.rs |  12 +-
 core/journal/src/partition_journal.rs              | 701 ++++++++++++++++++---
 core/journal/src/partition_journal/segments.rs     |  18 +-
 core/partitions/src/iggy_index_writer.rs           |  23 +-
 core/partitions/src/iggy_partition.rs              |  64 +-
 core/partitions/src/install_backup.rs              |  21 +
 core/partitions/src/persistence.rs                 | 223 +++++--
 core/server/config.toml                            |  12 +
 core/server/src/dispatch/partition.rs              |   6 +-
 core/server/src/partition_helpers.rs               |  38 +-
 core/server/src/segment_recovery.rs                |  13 +-
 core/server/src/server_error.rs                    |   7 +-
 core/simulator/src/storage/tests.rs                | 138 +++-
 16 files changed, 1117 insertions(+), 194 deletions(-)

diff --git a/core/configs/src/server_config/defaults.rs 
b/core/configs/src/server_config/defaults.rs
index ccf5b8dfa..feb83d5f5 100644
--- a/core/configs/src/server_config/defaults.rs
+++ b/core/configs/src/server_config/defaults.rs
@@ -188,6 +188,8 @@ impl Default for PartitionConfig {
                 .wal_bytes_max
                 .parse()
                 .expect("embedded WAL capacity is valid"),
+            wal_group_commit_delay_micros: 
u64::try_from(partition.wal_group_commit_delay_micros)
+                .expect("embedded WAL group commit delay is valid"),
             validate_checksum: SERVER_CONFIG.partition.validate_checksum,
             prepare_queue_depth: partition.prepare_queue_depth as usize,
             dedup_clients_max: partition.dedup_clients_max as usize,
diff --git a/core/configs/src/server_config/displays.rs 
b/core/configs/src/server_config/displays.rs
index 98bcb4644..ee96a0bbe 100644
--- a/core/configs/src/server_config/displays.rs
+++ b/core/configs/src/server_config/displays.rs
@@ -63,11 +63,12 @@ impl Display for PartitionConfig {
     fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
         write!(
             f,
-            "{{ wal_bytes_max: {}, validate_checksum: {}, prepare_queue_depth: 
{}, dedup_clients_max: {}, consumer_offsets_max: {}, \
+            "{{ wal_bytes_max: {}, wal_group_commit_delay_micros: {}, 
validate_checksum: {}, prepare_queue_depth: {}, dedup_clients_max: {}, 
consumer_offsets_max: {}, \
              offset_reservation_lease: {}, \
              evicted_ring_capacity: {}, evicted_ring_bytes_max: {}, \
              transfer_served_cache_bytes_max: {}, transfer_artifact_bytes_max: 
{} }}",
             self.wal_bytes_max,
+            self.wal_group_commit_delay_micros,
             self.validate_checksum,
             self.prepare_queue_depth,
             self.dedup_clients_max,
diff --git a/core/configs/src/server_config/partition.rs 
b/core/configs/src/server_config/partition.rs
index 4f2c98678..6589001b5 100644
--- a/core/configs/src/server_config/partition.rs
+++ b/core/configs/src/server_config/partition.rs
@@ -164,6 +164,14 @@ fn default_wal_bytes_max() -> IggyByteSize {
     IggyByteSize::from(DEFAULT_PARTITION_WAL_BYTES_MAX)
 }
 
+/// Ceiling on the group-commit delay. A longer wait costs more than the
+/// barrier it is meant to amortize.
+pub const MAX_PARTITION_WAL_GROUP_COMMIT_DELAY_MICROS: u64 = 10_000;
+
+fn default_wal_group_commit_delay_micros() -> u64 {
+    0
+}
+
 /// Capacity tunables for the per-partition consensus plane.
 #[derive(Debug, Deserialize, Serialize, Clone, ConfigEnv)]
 pub struct PartitionConfig {
@@ -172,6 +180,21 @@ pub struct PartitionConfig {
     #[serde(default = "default_wal_bytes_max")]
     #[config_env(leaf)]
     pub wal_bytes_max: IggyByteSize,
+    /// Bounded wait, in microseconds, for more prepares before a persisted
+    /// partition's WAL writer starts its durability barrier. Zero disables it.
+    ///
+    /// Spends up to this much acknowledgment latency to cut device writes: one
+    /// barrier and one frontier write then cover a whole group of prepares
+    /// instead of a single one. Durability is unchanged. The same barrier runs
+    /// over the same bytes, later, and the quorum gate is untouched.
+    ///
+    /// Skipped while prepares arrive further apart than the delay, so an idle
+    /// partition never waits. Earns nothing until the barrier completes faster
+    /// than prepares arrive, which is where the writer stops grouping by
+    /// itself. Start near the measured barrier duration.
+    #[serde(default = "default_wal_group_commit_delay_micros")]
+    #[config_env(leaf)]
+    pub wal_group_commit_delay_micros: u64,
     #[serde(default = "default_validate_checksum")]
     pub validate_checksum: bool,
     /// Depth of a partition's prepare queue: how many uncommitted produce /
@@ -268,6 +291,13 @@ impl Validatable<ConfigurationError> for PartitionConfig {
             return Err(ConfigurationError::InvalidConfigurationValue);
         }
 
+        if self.wal_group_commit_delay_micros > 
MAX_PARTITION_WAL_GROUP_COMMIT_DELAY_MICROS {
+            eprintln!(
+                "{COMPONENT} partition.wal_group_commit_delay_micros must not 
exceed {MAX_PARTITION_WAL_GROUP_COMMIT_DELAY_MICROS}"
+            );
+            return Err(ConfigurationError::InvalidConfigurationValue);
+        }
+
         if self.prepare_queue_depth == 0 {
             eprintln!("{COMPONENT} partition.prepare_queue_depth must be > 0");
             return Err(ConfigurationError::InvalidConfigurationValue);
diff --git a/core/integration/tests/cluster/crash_durability.rs 
b/core/integration/tests/cluster/crash_durability.rs
index 4967966e0..a1eaa9c83 100644
--- a/core/integration/tests/cluster/crash_durability.rs
+++ b/core/integration/tests/cluster/crash_durability.rs
@@ -691,9 +691,15 @@ async fn 
given_all_replicas_checkpointed_when_restarted_should_elect_and_extend_
                         && std::fs::read(entry.path().join("frontier"))
                             .ok()
                             .is_some_and(|bytes| {
-                                bytes.len() == 4096
-                                    && 
u64::from_le_bytes(bytes[48..56].try_into().unwrap()) == 2
-                                    && 
u64::from_le_bytes(bytes[72..80].try_into().unwrap()) == 2
+                                // The frontier alternates two slots, so scan
+                                // both: a slot only ever holds a state that 
was
+                                // published, and the checkpoint never goes
+                                // backwards, so either copy naming op 2 proves
+                                // this replica reached it.
+                                bytes.as_chunks::<4096>().0.iter().any(|slot| {
+                                    
u64::from_le_bytes(slot[48..56].try_into().unwrap()) == 2
+                                        && 
u64::from_le_bytes(slot[72..80].try_into().unwrap()) == 2
+                                })
                             })
                 })
             })
diff --git a/core/journal/src/partition_journal.rs 
b/core/journal/src/partition_journal.rs
index 76d5b8223..9610ad37d 100644
--- a/core/journal/src/partition_journal.rs
+++ b/core/journal/src/partition_journal.rs
@@ -42,9 +42,38 @@ pub const PARTITION_WAL_BYTES_MAX: u64 = 256 * 1024 * 1024;
 pub const PARTITION_WAL_CAPACITY_MIN: u64 = 2 * (64 * 1024 * 1024 + 4096);
 pub const PARTITION_WAL_CAPACITY_MAX: u64 = 4 * 1024 * 1024 * 1024;
 const RECORD_PREFIX: usize = 32;
+/// The published frontier. Rewritten IN PLACE, so anything that freezes a
+/// partition's files must copy this one rather than retain it by hard link.
+pub const FRONTIER_FILE_NAME: &str = "frontier";
+/// Scratch name the complete slot file is built under before it is renamed 
over
+/// the frontier. Only [`PartitionPrepareJournal::install_frontier`] uses it, 
so
+/// it appears once per open and never on an acknowledgment path.
+const FRONTIER_TEMPORARY_NAME: &str = "frontier.tmp";
+/// Fixed slots the frontier alternates between, so a publication overwrites 
the
+/// older copy in place instead of creating and renaming a temporary file.
+const FRONTIER_SLOTS: usize = 2;
+const FRONTIER_BYTES: usize = FRONTIER_SLOTS * PARTITION_WAL_BLOCK_SIZE;
+/// Publication counter, placed directly after the last field the frontier 
block
+/// encodes so that adding a field cannot silently move it onto this one. 
Inside
+/// the range the block's own checksum covers, and reserved in every frontier
+/// this build and its predecessor wrote, so a block from before the two-slot
+/// layout reads back as sequence zero.
+const FRONTIER_SEQUENCE_OFFSET: usize = SEALED_STATE_MAGIC_OFFSET + 
size_of::<u64>();
+const _: () = assert!(FRONTIER_SEQUENCE_OFFSET + size_of::<u64>() <= 
PARTITION_WAL_BLOCK_SIZE);
 pub const PREPARE_BYTES_MAX: usize = 64 * 1024 * 1024;
-const STATE_MAGIC: &[u8; 8] = b"IGGYWAL1";
-const REFERENCE_STATE_MAGIC: &[u8; 8] = b"IGGYWAL2";
+/// Two-slot frontier. `IGGYWAL1` and `IGGYWAL2` were the single-block layout,
+/// which this build neither writes nor reads.
+///
+/// The value has to differ from those two. A build that predates the slots
+/// reads block 0 and truncates the data file to the length it finds there, and
+/// after an acknowledgment lands in slot 1 that block is one publication 
stale.
+/// Rejecting the magic makes such a build refuse the open instead of dropping
+/// acknowledged records.
+const STATE_MAGIC: &[u8; 8] = b"IGGYWAL3";
+/// Segment references live in a flag inside the block, not in the magic, so a
+/// later format bump costs one constant rather than doubling the set.
+const SEGMENT_REFERENCES_FLAG: usize = 100;
+const _: () = assert!(SEGMENT_REFERENCES_FLAG < 
segments::SEGMENT_STATE_OFFSET);
 const SEALED_STATE_MAGIC_OFFSET: usize =
     segments::SEGMENT_STATE_OFFSET + segments::SEGMENT_STATE_BYTES;
 const INLINE_RECORD: u32 = 0;
@@ -77,6 +106,8 @@ pub trait DurableAppend {
 pub struct PartitionPrepareJournal<S: DurableStorage = DiskStorage> {
     directory: PathBuf,
     file: S::File,
+    frontier: S::File,
+    frontier_sequence: u64,
     storage: S,
     capacity: u64,
     state: JournalState,
@@ -183,19 +214,17 @@ impl<S: DurableStorage> PartitionPrepareJournal<S> {
         }
         storage.create_directories(directory).await?;
         storage.sync_directory(parent).await?;
-        let state_path = directory.join("frontier");
-        let existing = match storage.open(&state_path, OpenMode::Read).await {
-            Ok(file) => {
-                let bytes = file.read(0, PARTITION_WAL_BLOCK_SIZE).await?;
-                let state = JournalState::decode(&bytes)?;
-                if state.group != group || state.incarnation != incarnation {
-                    return Err(invalid("partition WAL identity mismatch"));
-                }
-                Some(state)
-            }
-            Err(error) if error.kind() == io::ErrorKind::NotFound => None,
-            Err(error) => return Err(error),
-        };
+        let state_path = directory.join(FRONTIER_FILE_NAME);
+        let (published, slots) = Self::open_frontier(&storage, 
&state_path).await?;
+        let (existing, sequence, verified) = 
Self::read_frontier(published.as_ref(), slots).await?;
+        if slots > 0 && verified == 0 {
+            return Err(invalid("unreadable partition WAL frontier"));
+        }
+        if let Some(state) = existing
+            && (state.group != group || state.incarnation != incarnation)
+        {
+            return Err(invalid("partition WAL identity mismatch"));
+        }
         if existing.is_none() {
             Self::validate_unpublished_history(&storage, directory).await?;
         }
@@ -216,9 +245,26 @@ impl<S: DurableStorage> PartitionPrepareJournal<S> {
         if file.length().await? < state.length {
             return Err(invalid("partition WAL lost acknowledged bytes"));
         }
+        // After the data file it names, never before: a frontier is visible 
the
+        // moment its rename lands, and one naming a generation that does not
+        // exist is unrecoverable history rather than a fresh journal.
+        let (frontier, frontier_sequence) = match published {
+            Some(file) if slots == FRONTIER_SLOTS => (file, sequence),
+            _ => {
+                let sequence =
+                    Self::install_frontier(&storage, directory, &state_path, 
state, sequence)
+                        .await?;
+                (
+                    storage.open(&state_path, OpenMode::ReadWrite).await?,
+                    sequence,
+                )
+            }
+        };
         let mut journal = Self {
             directory: directory.to_path_buf(),
             file,
+            frontier,
+            frontier_sequence,
             storage,
             capacity,
             state,
@@ -234,27 +280,126 @@ impl<S: DurableStorage> PartitionPrepareJournal<S> {
             preallocate_segments,
             retained_bytes: 0,
         };
-        journal.recover_entries().await?;
-        // Only bytes covered by the durable frontier could have released an 
ack.
-        journal.file.truncate(state.length).await?;
+        // A slot that did not verify may have been the newest, so the frontier
+        // this open read can name less than an acknowledgment already covered.
+        // Only then does recovery walk past the frontier.
+        journal.recover_entries(verified < slots).await?;
+        // Only verified bytes could have released an ack. Recovery adopted 
every
+        // record past the published frontier whose envelope, chain and body 
all
+        // verify; what follows them is a tail no barrier ever covered.
+        let recovered = journal.state;
+        journal.durable_head = recovered.head;
+        journal.file.truncate(recovered.length).await?;
         journal.file.sync().await?;
         journal.storage.sync_directory(directory).await?;
-        if existing.is_none() {
-            journal.publish(state).await?;
-        }
-        journal.discover_obsolete().await?;
-        loop {
-            let remaining = journal.obsolete.len();
-            journal.cleanup_obsolete().await;
-            if journal.obsolete.is_empty() || journal.obsolete.len() == 
remaining {
-                break;
-            }
-        }
+        // Also when nothing changed but a slot did not verify: leaving it
+        // damaged keeps every later open on the tail-walking path, where an
+        // ordinary unacknowledged tail reads as damage and refuses the open.
+        // Publication targets the slot that is not the newest, which is the
+        // damaged one.
+        if recovered != state || verified < slots {
+            journal.publish(recovered).await?;
+        }
+        journal.remove_obsolete_history().await?;
         journal.recover_segment_files().await?;
         journal.migrate_segment_prepares().await?;
         Ok(journal)
     }
 
+    /// Open the frontier slot file without creating it, with the number of 
whole
+    /// slots it holds.
+    ///
+    /// A visible frontier always holds whole slots: it is only ever created, 
or
+    /// grown to its full slot count, by [`Self::install_frontier`], through a
+    /// rename. Publication then overwrites one slot of a file it never 
resizes.
+    /// A length in between belongs to no protocol this build can read.
+    async fn open_frontier(storage: &S, path: &Path) -> 
io::Result<(Option<S::File>, usize)> {
+        let published = match storage.open(path, OpenMode::ReadWrite).await {
+            Ok(file) => Some(file),
+            Err(error) if error.kind() == io::ErrorKind::NotFound => None,
+            Err(error) => return Err(error),
+        };
+        let length = match &published {
+            Some(file) => file.length().await?,
+            None => 0,
+        };
+        if length > FRONTIER_BYTES as u64 || 
!length.is_multiple_of(PARTITION_WAL_BLOCK_SIZE as u64)
+        {
+            return Err(invalid("unknown partition WAL frontier size"));
+        }
+        let slots = usize::try_from(length)
+            .map_err(|_| invalid("unknown partition WAL frontier size"))?
+            / PARTITION_WAL_BLOCK_SIZE;
+        Ok((published, slots))
+    }
+
+    /// Install a complete slot file, atomically, and return its newest 
sequence.
+    ///
+    /// Runs once per open: for a journal that has no frontier yet, and for one
+    /// whose frontier predates the second slot. Both slots carry `state`, so 
the
+    /// first in-place publication always has an intact partner to fall back 
on,
+    /// and a visible frontier never holds a slot no publication completed. 
That
+    /// is what lets [`Self::open_frontier`] trust the file it finds: a torn
+    /// write inside this temporary name never becomes the frontier, and a torn
+    /// write afterwards can only damage the slot being published.
+    async fn install_frontier(
+        storage: &S,
+        directory: &Path,
+        path: &Path,
+        state: JournalState,
+        sequence: u64,
+    ) -> io::Result<u64> {
+        let temporary = directory.join(FRONTIER_TEMPORARY_NAME);
+        let mut file = storage.open(&temporary, OpenMode::Create).await?;
+        let mut newest = sequence;
+        for _ in 0..FRONTIER_SLOTS {
+            newest = newest
+                .checked_add(1)
+                .ok_or_else(|| invalid("WAL frontier sequence exhausted"))?;
+            file.write_aligned(frontier_offset(newest), state.encode(newest))
+                .await?;
+        }
+        file.sync().await?;
+        storage.rename(&temporary, path).await?;
+        storage.sync_directory(directory).await?;
+        Ok(newest)
+    }
+
+    /// The newest intact slot wins. A slot that fails verification is either 
the
+    /// interrupted half of the publication in flight, which never released an
+    /// acknowledgment, or a copy the newer one superseded; either way its
+    /// partner is the frontier. Losing the NEWEST slot rolls the published
+    /// prefix back one publication, which [`Self::recover_unpublished_tail`]
+    /// then re-adopts from the records themselves.
+    ///
+    /// Returns the newest intact slot, its publication sequence, and how many
+    /// of the `slots` whole blocks verified. A slot that does not verify is
+    /// either the interrupted half of a publication in flight or a copy the
+    /// newer one superseded, so the count is what tells the caller whether the
+    /// frontier it got could be older than an acknowledgment already covered.
+    async fn read_frontier(
+        file: Option<&S::File>,
+        slots: usize,
+    ) -> io::Result<(Option<JournalState>, u64, usize)> {
+        let mut newest = None;
+        let mut sequence = 0;
+        let mut verified = 0;
+        let Some(file) = file.filter(|_| slots > 0) else {
+            return Ok((newest, sequence, verified));
+        };
+        let bytes = file.read(0, slots * PARTITION_WAL_BLOCK_SIZE).await?;
+        for slot in bytes.as_chunks::<PARTITION_WAL_BLOCK_SIZE>().0 {
+            if let Ok((state, slot_sequence)) = JournalState::decode(slot) {
+                verified += 1;
+                if newest.is_none() || slot_sequence > sequence {
+                    newest = Some(state);
+                    sequence = slot_sequence;
+                }
+            }
+        }
+        Ok((newest, sequence, verified))
+    }
+
     pub const fn certified_log_view(&self) -> Option<u32> {
         self.state.certified_log_view
     }
@@ -350,7 +495,8 @@ impl<S: DurableStorage> PartitionPrepareJournal<S> {
     pub async fn prepares(&self) -> io::Result<Vec<Message<PrepareHeader>>> {
         let mut prepares = Vec::with_capacity(self.entries.len());
         for entry in self.entries.values() {
-            let (_, length, prepare, _) = 
self.read_record(entry.position).await?;
+            let (_, length, prepare, _) =
+                self.read_record(entry.position, self.state.length).await?;
             if length != entry.length {
                 return Err(invalid("partition WAL index length mismatch"));
             }
@@ -534,6 +680,11 @@ impl<S: DurableStorage> PartitionPrepareJournal<S> {
 
     /// Make every buffered predecessor recoverable with one frontier 
publication.
     ///
+    /// The body barrier and the WAL barrier cover different inodes, so they 
run
+    /// under one `join` and a batch pays one barrier latency rather than two.
+    /// Their completion order does not matter, because neither is the
+    /// acknowledgment point: the frontier publication is, and it follows both.
+    ///
     /// # Errors
     /// Returns an error unless the buffered prefix and its frontier are 
durable.
     pub async fn sync(&mut self) -> io::Result<()> {
@@ -542,8 +693,12 @@ impl<S: DurableStorage> PartitionPrepareJournal<S> {
             return Ok(());
         }
         self.poisoned = true;
-        self.sync_segment_files().await?;
-        self.file.sync().await?;
+        let (bodies, records) =
+            futures::future::join(self.segment_barrier(), 
self.file.sync()).await;
+        bodies?;
+        records?;
+        self.segment_files_dirty = false;
+        self.segment_links_dirty = false;
         self.publish(self.state).await?;
         self.durable_head = self.state.head;
         self.retain_active_segment_file();
@@ -741,13 +896,17 @@ impl<S: DurableStorage> PartitionPrepareJournal<S> {
         Ok(())
     }
 
-    async fn recover_entries(&mut self) -> io::Result<()> {
+    /// Rebuild the entry index from the published prefix, which must verify in
+    /// full. `lost_publication` extends the walk past that prefix through
+    /// [`Self::recover_unpublished_tail`].
+    async fn recover_entries(&mut self, lost_publication: bool) -> 
io::Result<()> {
         let state = self.state;
         let mut position = 0;
         let mut previous = state.checkpoint;
         let mut checksum = state.checkpoint_checksum;
         while position < state.length {
-            let (header, length, prepare, reference) = 
self.read_record(position).await?;
+            let (header, length, prepare, reference) =
+                self.read_record(position, state.length).await?;
             let next_offset = if state.segment_storage.is_some() && 
reference.is_some() {
                 Some(segments::batch_next_offset(prepare.as_slice())?)
             } else {
@@ -790,9 +949,110 @@ impl<S: DurableStorage> PartitionPrepareJournal<S> {
             .values()
             .map(|entry| entry.retained_bytes)
             .sum();
+        if lost_publication {
+            self.recover_unpublished_tail(previous, checksum).await
+        } else {
+            Ok(())
+        }
+    }
+
+    /// Recover the records a frontier publication this open could not read had
+    /// already covered.
+    ///
+    /// Runs only when a slot failed to verify, which can happen only while a
+    /// publication was in flight. The writer is serial and publishes after 
both
+    /// data barriers, so a publication in flight proves every record before it
+    /// was already durable: between the surviving frontier and the end of the
+    /// data file the records are complete, and every one of them is adopted.
+    ///
+    /// That is why this walk REFUSES instead of stopping. Once a slot is lost,
+    /// nothing left on disk says how far acknowledgment had reached, so a 
record
+    /// that does not verify there cannot be dismissed as an unwritten tail. It
+    /// is damage to history that may have been acknowledged, and the partition
+    /// has to fence and rebuild from its peers rather than open a truncated 
log.
+    /// When both slots verify the frontier is exact and the tail is discarded 
as
+    /// it always was.
+    async fn recover_unpublished_tail(
+        &mut self,
+        mut previous: u64,
+        mut checksum: u128,
+    ) -> io::Result<()> {
+        // Bounded by the protocol, never by `capacity`: that budget is
+        // configurable and only gates admission, so a lowered one must still
+        // reopen the history it already accepted.
+        let limit = self.file.length().await?;
+        if limit > PARTITION_WAL_CAPACITY_MAX {
+            return Err(invalid("partition WAL recovered tail exceeds its 
bounds"));
+        }
+        let mut position = self.state.length;
+        while position < limit {
+            let (header, length, prepare, reference) = 
self.read_record(position, limit).await?;
+            let next_op = previous
+                .checked_add(1)
+                .ok_or_else(|| invalid("WAL op overflow"))?;
+            if header.op != next_op || (self.state.anchor_known && 
header.parent != checksum) {
+                return Err(invalid("partition WAL prepare chain is broken"));
+            }
+            let body_bytes = record_length(header.size as usize)? as u64;
+            let mut state = self.state;
+            let next_offset = if let Some(segments) = &mut 
state.segment_storage {
+                let (reserved, next_offset) = segments.reserve(&header, 
prepare.as_slice())?;
+                if reserved != reference || !segments.valid() {
+                    return Err(invalid("partition WAL recovered segment 
boundary mismatch"));
+                }
+                next_offset
+            } else {
+                None
+            };
+            state.length = position + length as u64;
+            state.head = header.op;
+            state.head_checksum = header.checksum;
+            state.segment_references |= reference.is_some();
+            if state
+                .certified_log_view
+                .is_some_and(|view| header.view > view)
+            {
+                state.certified_log_view = None;
+            }
+            if !state.anchor_known {
+                state.checkpoint_checksum = header.parent;
+                state.anchor_known = true;
+            }
+            self.recovered_prepares.push(prepare);
+            self.entries.insert(
+                header.op,
+                StoredPrepare {
+                    position,
+                    length,
+                    checksum: header.checksum,
+                    reference,
+                    next_offset,
+                    retained_bytes: body_bytes,
+                },
+            );
+            self.retained_bytes += body_bytes;
+            self.state = state;
+            previous = header.op;
+            checksum = header.checksum;
+            position += length as u64;
+        }
         Ok(())
     }
 
+    /// Drop every file the recovered history does not retain, repeating while
+    /// the queue shrinks: `cleanup_obsolete` removes a bounded batch per call
+    /// and re-queues what it could not remove.
+    async fn remove_obsolete_history(&mut self) -> io::Result<()> {
+        self.discover_obsolete().await?;
+        loop {
+            let remaining = self.obsolete.len();
+            self.cleanup_obsolete().await;
+            if self.obsolete.is_empty() || self.obsolete.len() == remaining {
+                return Ok(());
+            }
+        }
+    }
+
     async fn discover_obsolete(&mut self) -> io::Result<()> {
         let retained = self.retained_segment_paths(&self.entries, 
self.state.segment_storage);
         for entry in self.storage.entries(&self.directory).await? {
@@ -805,7 +1065,7 @@ impl<S: DurableStorage> PartitionPrepareJournal<S> {
                 .and_then(|value| value.parse::<u64>().ok());
             if !entry.directory
                 && (generation.is_some_and(|generation| generation != 
self.state.generation)
-                    || name == "frontier.tmp"
+                    || name == FRONTIER_TEMPORARY_NAME
                     || (is_retained_segment_name(name)
                         && 
!retained.contains(&self.directory.join(&entry.name))))
             {
@@ -842,7 +1102,7 @@ impl<S: DurableStorage> PartitionPrepareJournal<S> {
 
     async fn validate_unpublished_history(storage: &S, directory: &Path) -> 
io::Result<()> {
         for entry in storage.entries(directory).await? {
-            if entry.directory || entry.name == "frontier.tmp" {
+            if entry.directory || entry.name == FRONTIER_TEMPORARY_NAME {
                 continue;
             }
             // An interrupted first open can leave only its empty 
generation-zero file.
@@ -876,13 +1136,15 @@ impl<S: DurableStorage> PartitionPrepareJournal<S> {
     async fn read_record(
         &self,
         position: u64,
+        limit: u64,
     ) -> io::Result<(
         PrepareHeader,
         usize,
         Message<PrepareHeader>,
         Option<SegmentReference>,
     )> {
-        let (mut buffer, frame_length, reference) = 
self.read_encoded_record(position).await?;
+        let (mut buffer, frame_length, reference) =
+            self.read_encoded_record(position, limit).await?;
         let length = buffer.as_slice().len();
         if let Some(reference) = reference {
             let payload = &buffer.as_slice()[RECORD_PREFIX..RECORD_PREFIX + 
frame_length];
@@ -935,6 +1197,7 @@ impl<S: DurableStorage> PartitionPrepareJournal<S> {
     async fn read_encoded_record(
         &self,
         position: u64,
+        limit: u64,
     ) -> io::Result<(Owned<4096>, usize, Option<SegmentReference>)> {
         let prefix = self
             .file
@@ -948,7 +1211,7 @@ impl<S: DurableStorage> PartitionPrepareJournal<S> {
         let length = record_length(frame_length)?;
         if position
             .checked_add(length as u64)
-            .is_none_or(|end| end > self.state.length)
+            .is_none_or(|end| end > limit)
         {
             return Err(invalid("partition WAL record crosses durable 
frontier"));
         }
@@ -1018,21 +1281,41 @@ impl<S: DurableStorage> PartitionPrepareJournal<S> {
         Ok((buffer, frame_length, reference))
     }
 
-    async fn publish(&self, state: JournalState) -> io::Result<()> {
+    /// Publish the frontier by overwriting the older of two fixed slots.
+    ///
+    /// Both slots exist and hold a complete record from the moment the journal
+    /// opens ([`Self::install_frontier`]), so a publication is one 4096-byte
+    /// overwrite plus `fdatasync`: no create, no truncate, no size change, no
+    /// rename and no directory barrier. On a journaling filesystem that is the
+    /// difference between zero metadata transactions per acknowledgment and
+    /// roughly two, which at thousands of acknowledgments per second per node
+    /// costs more than the prepare bytes the batch carries.
+    ///
+    /// A torn slot fails its checksum and its partner still holds the previous
+    /// publication, the guarantee the temporary-file rename used to provide.
+    /// Unlike the rename, an unreadable NEWEST slot leaves the published 
prefix
+    /// one publication behind; [`Self::recover_unpublished_tail`] recovers it
+    /// from the records themselves.
+    ///
+    /// The slot follows the sequence's parity, so a publication never 
overwrites
+    /// the copy it would have to fall back on.
+    async fn publish(&mut self, state: JournalState) -> io::Result<()> {
         if state
             .segment_storage
             .is_some_and(|segments| !segments.valid())
         {
             return Err(invalid("invalid durable segment boundaries"));
         }
-        let temporary = self.directory.join("frontier.tmp");
-        let mut file = self.storage.open(&temporary, OpenMode::Create).await?;
-        file.write(0, state.encode()).await?;
-        file.sync().await?;
-        self.storage
-            .rename(&temporary, &self.directory.join("frontier"))
+        let sequence = self
+            .frontier_sequence
+            .checked_add(1)
+            .ok_or_else(|| invalid("WAL frontier sequence exhausted"))?;
+        self.frontier
+            .write_aligned(frontier_offset(sequence), state.encode(sequence))
             .await?;
-        self.storage.sync_directory(&self.directory).await
+        self.frontier.sync().await?;
+        self.frontier_sequence = sequence;
+        Ok(())
     }
 
     #[allow(clippy::too_many_lines)]
@@ -1113,8 +1396,9 @@ impl<S: DurableStorage> PartitionPrepareJournal<S> {
             if op < checkpoint || truncate.is_some_and(|from| op >= from) {
                 continue;
             }
-            let (record, payload_length, mut reference) =
-                self.read_encoded_record(entry.position).await?;
+            let (record, payload_length, mut reference) = self
+                .read_encoded_record(entry.position, self.state.length)
+                .await?;
             let mut next_offset = entry.next_offset;
             // Purge removes polled data, but these operations still 
participate
             // in repair. Inline their bodies before releasing whole segment 
inodes.
@@ -1122,7 +1406,8 @@ impl<S: DurableStorage> PartitionPrepareJournal<S> {
                 && reference.is_some()
                 && op <= state.purge_floor
             {
-                let (_, _, prepare, _) = 
self.read_record(entry.position).await?;
+                let (_, _, prepare, _) =
+                    self.read_record(entry.position, self.state.length).await?;
                 reference = None;
                 next_offset = None;
                 Some(prepare)
@@ -1198,6 +1483,11 @@ impl<S: DurableStorage> PartitionPrepareJournal<S> {
         self.sync_segment_files().await?;
         file.sync().await?;
         self.storage.sync_directory(&self.directory).await?;
+        // The outgoing generation too, through the handle that wrote it. A 
torn
+        // publication leaves the older slot naming this generation, and 
recovery
+        // then walks its tail. An unsynced record there is indistinguishable
+        // from damage, which would refuse an otherwise sound history.
+        self.file.sync().await?;
         self.publish(state).await?;
         let obsolete = data_path(&self.directory, self.state.generation);
         let retained = self.retained_segment_paths(&entries, 
state.segment_storage);
@@ -1231,13 +1521,18 @@ impl<S: DurableStorage> DurableAppend for 
PartitionPrepareJournal<S> {
 }
 
 impl JournalState {
-    fn encode(self) -> Vec<u8> {
-        let mut bytes = vec![0; PARTITION_WAL_BLOCK_SIZE];
-        bytes[..8].copy_from_slice(if self.segment_references {
-            REFERENCE_STATE_MAGIC
-        } else {
-            STATE_MAGIC
-        });
+    /// Encode one frontier slot into an aligned single-block buffer.
+    ///
+    /// Aligned, block-sized and written at a block-aligned offset into a file
+    /// that never resizes, so the publication already satisfies what 
`O_DIRECT`
+    /// requires of a write and needs no read-modify-write to get there.
+    fn encode(self, sequence: u64) -> Owned<4096> {
+        let mut buffer = Owned::zeroed(PARTITION_WAL_BLOCK_SIZE);
+        let bytes = buffer.as_mut_slice();
+        bytes[FRONTIER_SEQUENCE_OFFSET..FRONTIER_SEQUENCE_OFFSET + 
size_of::<u64>()]
+            .copy_from_slice(&sequence.to_le_bytes());
+        bytes[..8].copy_from_slice(STATE_MAGIC);
+        bytes[SEGMENT_REFERENCES_FLAG] = u8::from(self.segment_references);
         for (offset, value) in [
             (16, self.group),
             (24, self.incarnation),
@@ -1268,13 +1563,11 @@ impl JournalState {
         bytes.copy_within(..8, SEALED_STATE_MAGIC_OFFSET);
         let checksum = XxHash3_64::oneshot(&bytes[16..]);
         bytes[8..16].copy_from_slice(&checksum.to_le_bytes());
-        bytes
+        buffer
     }
 
-    fn decode(bytes: &[u8]) -> io::Result<Self> {
-        if bytes.len() != PARTITION_WAL_BLOCK_SIZE
-            || (&bytes[..8] != STATE_MAGIC && &bytes[..8] != 
REFERENCE_STATE_MAGIC)
-        {
+    fn decode(bytes: &[u8]) -> io::Result<(Self, u64)> {
+        if bytes.len() != PARTITION_WAL_BLOCK_SIZE || &bytes[..8] != 
STATE_MAGIC {
             return Err(invalid("unknown partition WAL frontier format"));
         }
         let read_u64 = |offset| -> io::Result<u64> {
@@ -1287,15 +1580,21 @@ impl JournalState {
         if read_u64(8)? != XxHash3_64::oneshot(&bytes[16..]) {
             return Err(invalid("partition WAL frontier checksum mismatch"));
         }
-        let sealed_magic = 
&bytes[SEALED_STATE_MAGIC_OFFSET..SEALED_STATE_MAGIC_OFFSET + 8];
-        if sealed_magic != [0; 8] && sealed_magic != &bytes[..8] {
+        // The magic sits outside the checksummed range, so this copy inside it
+        // is what proves the magic itself was not damaged.
+        if bytes[SEALED_STATE_MAGIC_OFFSET..SEALED_STATE_MAGIC_OFFSET + 8] != 
bytes[..8] {
             return Err(invalid("partition WAL frontier format checksum 
mismatch"));
         }
+        let segment_references = match bytes[SEGMENT_REFERENCES_FLAG] {
+            0 => false,
+            1 => true,
+            _ => return Err(invalid("invalid segment reference flag")),
+        };
         let state = Self {
-            segment_references: &bytes[..8] == REFERENCE_STATE_MAGIC,
+            segment_references,
             segment_storage: match bytes[segments::SEGMENT_STATE_FLAG] {
                 0 => None,
-                1 if &bytes[..8] == REFERENCE_STATE_MAGIC => 
Some(SegmentState::decode(
+                1 if segment_references => Some(SegmentState::decode(
                     &bytes[segments::SEGMENT_STATE_OFFSET
                         ..segments::SEGMENT_STATE_OFFSET + 
segments::SEGMENT_STATE_BYTES],
                 )?),
@@ -1348,10 +1647,16 @@ impl JournalState {
         {
             return Err(invalid("invalid partition WAL frontier bounds"));
         }
-        Ok(state)
+        Ok((state, read_u64(FRONTIER_SEQUENCE_OFFSET)?))
     }
 }
 
+/// Byte offset of the slot a publication sequence owns. Alternating by parity 
is
+/// what keeps a publication off the copy it would have to fall back on.
+const fn frontier_offset(sequence: u64) -> u64 {
+    (sequence % FRONTIER_SLOTS as u64) * PARTITION_WAL_BLOCK_SIZE as u64
+}
+
 /// Padded size of a prepare record, including its envelope.
 ///
 /// # Errors
@@ -1681,9 +1986,14 @@ mod tests {
             .await
             .unwrap();
         journal.sync().await.unwrap();
-        assert_eq!(
-            &std::fs::read(directory.join("frontier")).unwrap()[..8],
-            REFERENCE_STATE_MAGIC
+        let published = std::fs::read(directory.join("frontier")).unwrap();
+        assert_eq!(&published[..8], STATE_MAGIC);
+        assert!(
+            published
+                .as_chunks::<PARTITION_WAL_BLOCK_SIZE>()
+                .0
+                .iter()
+                .any(|slot| slot[SEGMENT_REFERENCES_FLAG] == 1)
         );
         journal.mark_purge(1, 2).await.unwrap();
         DiskStorage
@@ -1916,6 +2226,76 @@ mod tests {
         );
     }
 
+    #[compio::test]
+    async fn a_lost_frontier_slot_recovers_the_acknowledged_tail() {
+        let directory = tempdir().unwrap();
+        let mut journal = PartitionPrepareJournal::open(directory.path(), 42, 
7)
+            .await
+            .unwrap();
+        let first = prepare(1, 0);
+        let second = prepare(2, first.header().checksum);
+        journal.append(first.into_frozen()).await.unwrap();
+        journal.append(second.into_frozen()).await.unwrap();
+        // Publication alternates two slots of one file in place: no temporary
+        // name is created and renamed per acknowledgment.
+        let path = directory.path().join("frontier");
+        assert_eq!(
+            std::fs::metadata(&path).unwrap().len(),
+            FRONTIER_BYTES as u64
+        );
+        assert!(!directory.path().join("frontier.tmp").exists());
+        let newest = usize::try_from(journal.frontier_sequence % 
FRONTIER_SLOTS as u64).unwrap()
+            * PARTITION_WAL_BLOCK_SIZE;
+        drop(journal);
+        let mut bytes = std::fs::read(&path).unwrap();
+        bytes[newest..newest + PARTITION_WAL_BLOCK_SIZE].fill(0);
+        std::fs::write(&path, bytes).unwrap();
+        // The surviving slot names op 1, but op 2 was acknowledged: recovery
+        // walks past the frontier it could read and adopts the verified 
record.
+        let journal = PartitionPrepareJournal::open(directory.path(), 42, 7)
+            .await
+            .unwrap();
+        assert_eq!(journal.head(), 2);
+        assert_eq!(journal.prepares().await.unwrap().len(), 2);
+    }
+
+    #[compio::test]
+    async fn a_lost_frontier_slot_does_not_hide_damaged_acknowledged_records() 
{
+        for damage in ["zeroed", "shortened"] {
+            let directory = tempdir().unwrap();
+            let mut journal = PartitionPrepareJournal::open(directory.path(), 
42, 7)
+                .await
+                .unwrap();
+            let first = prepare(1, 0);
+            let second = prepare(2, first.header().checksum);
+            journal.append(first.into_frozen()).await.unwrap();
+            journal.append(second.into_frozen()).await.unwrap();
+            let newest = 
usize::try_from(frontier_offset(journal.frontier_sequence)).unwrap();
+            let position = 
usize::try_from(journal.entries[&2].position).unwrap();
+            let data = data_path(directory.path(), journal.state.generation);
+            drop(journal);
+            let frontier = directory.path().join(FRONTIER_FILE_NAME);
+            let mut slots = std::fs::read(&frontier).unwrap();
+            slots[newest..newest + PARTITION_WAL_BLOCK_SIZE].fill(0);
+            std::fs::write(&frontier, &slots).unwrap();
+            let mut records = std::fs::read(&data).unwrap();
+            match damage {
+                "zeroed" => records[position..].fill(0),
+                "shortened" => records.truncate(position + 
PARTITION_WAL_BLOCK_SIZE / 2),
+                _ => unreachable!(),
+            }
+            std::fs::write(&data, &records).unwrap();
+            assert!(
+                PartitionPrepareJournal::open(directory.path(), 42, 7)
+                    .await
+                    .is_err(),
+                "{damage}: recovery must refuse uncertain acknowledged history"
+            );
+            assert_eq!(std::fs::read(&data).unwrap(), records);
+            assert_eq!(std::fs::read(&frontier).unwrap(), slots);
+        }
+    }
+
     #[compio::test]
     async fn durable_append_covers_buffered_predecessors() {
         let directory = tempdir().unwrap();
@@ -2184,10 +2564,10 @@ mod tests {
             incarnation: 7,
             ..JournalState::default()
         };
-        let mut bytes = state.encode();
-        assert_eq!(JournalState::decode(&bytes).unwrap(), state);
-        bytes[24] ^= 1;
-        assert!(JournalState::decode(&bytes).is_err());
+        let mut bytes = state.encode(1);
+        assert_eq!(JournalState::decode(bytes.as_slice()).unwrap().0, state);
+        bytes.as_mut_slice()[24] ^= 1;
+        assert!(JournalState::decode(bytes.as_slice()).is_err());
     }
 
     #[compio::test]
@@ -2457,9 +2837,12 @@ mod tests {
                 }
                 _ => unreachable!(),
             }
-            let encoded = state.encode();
-            assert!(JournalState::decode(&encoded).is_err(), "{invalid}");
-            std::fs::write(directory.join("frontier"), encoded).unwrap();
+            let encoded = state.encode(1);
+            assert!(
+                JournalState::decode(encoded.as_slice()).is_err(),
+                "{invalid}"
+            );
+            std::fs::write(directory.join("frontier"), 
encoded.as_slice()).unwrap();
             drop(journal);
             let error = PartitionPrepareJournal::open(&directory, 42, 7)
                 .await
@@ -2509,9 +2892,9 @@ mod tests {
             .unwrap();
         let mut state = journal.state;
         state.segment_storage.as_mut().unwrap().max_size = 
iggy_common::MAX_TOPIC_SEGMENT_SIZE + 1;
-        let encoded = state.encode();
-        assert!(JournalState::decode(&encoded).is_err());
-        std::fs::write(directory.join("frontier"), encoded).unwrap();
+        let encoded = state.encode(1);
+        assert!(JournalState::decode(encoded.as_slice()).is_err());
+        std::fs::write(directory.join("frontier"), 
encoded.as_slice()).unwrap();
         drop(journal);
         assert!(
             PartitionPrepareJournal::open_with_storage_and_capacity(
@@ -2537,24 +2920,32 @@ mod tests {
                 segment_references,
                 ..JournalState::default()
             };
-            let mut encoded = state.encode();
-            assert_eq!(JournalState::decode(&encoded).unwrap(), state);
+            let encoded = state.encode(1);
+            assert_eq!(JournalState::decode(encoded.as_slice()).unwrap().0, 
state);
             // The old reader hashes the same payload and ignores reserved 
bytes.
             assert_eq!(
-                u64::from_le_bytes(encoded[8..16].try_into().unwrap()),
-                XxHash3_64::oneshot(&encoded[16..])
+                
u64::from_le_bytes(encoded.as_slice()[8..16].try_into().unwrap()),
+                XxHash3_64::oneshot(&encoded.as_slice()[16..])
             );
-            encoded[..8].copy_from_slice(if segment_references {
-                STATE_MAGIC
-            } else {
-                REFERENCE_STATE_MAGIC
-            });
-            assert!(JournalState::decode(&encoded).is_err());
-            let mut legacy = state.encode();
-            legacy[SEALED_STATE_MAGIC_OFFSET..SEALED_STATE_MAGIC_OFFSET + 
8].fill(0);
-            let checksum = XxHash3_64::oneshot(&legacy[16..]);
-            legacy[8..16].copy_from_slice(&checksum.to_le_bytes());
-            assert_eq!(JournalState::decode(&legacy).unwrap(), state);
+            assert_eq!(
+                encoded.as_slice()[SEGMENT_REFERENCES_FLAG],
+                u8::from(segment_references)
+            );
+            // Both single-block magics are refused, which is what keeps a 
build
+            // that predates the slots from reading block 0 as the whole 
record.
+            for magic in [b"IGGYWAL1", b"IGGYWAL2"] {
+                let mut legacy = state.encode(1);
+                legacy.as_mut_slice()[..8].copy_from_slice(magic);
+                assert!(JournalState::decode(legacy.as_slice()).is_err());
+            }
+            // The magic is outside the checksummed range, so damaging it is
+            // caught only by the copy inside that range.
+            let mut damaged = state.encode(1);
+            
damaged.as_mut_slice()[SEALED_STATE_MAGIC_OFFSET..SEALED_STATE_MAGIC_OFFSET + 8]
+                .fill(0);
+            let checksum = XxHash3_64::oneshot(&damaged.as_slice()[16..]);
+            
damaged.as_mut_slice()[8..16].copy_from_slice(&checksum.to_le_bytes());
+            assert!(JournalState::decode(damaged.as_slice()).is_err());
         }
     }
 
@@ -2968,6 +3359,128 @@ mod tests {
         Message::try_from(bytes).unwrap()
     }
 
+    /// A build that predates the two-slot layout reads block 0 and truncates 
to
+    /// the length it finds there. After an acknowledgment lands in slot 1 that
+    /// block is one publication stale, so the magic has to be one such a build
+    /// rejects, and a frontier it wrote has to be rejected here in turn.
+    #[compio::test]
+    async fn 
the_two_slot_frontier_shares_no_magic_with_the_single_block_layout() {
+        let directory = tempdir().unwrap();
+        let mut journal = PartitionPrepareJournal::open(directory.path(), 42, 
7)
+            .await
+            .unwrap();
+        journal.append(prepare(1, 0).into_frozen()).await.unwrap();
+        drop(journal);
+        let path = directory.path().join(FRONTIER_FILE_NAME);
+        let bytes = std::fs::read(&path).unwrap();
+        assert_eq!(bytes.len(), FRONTIER_BYTES);
+        for slot in bytes.as_chunks::<PARTITION_WAL_BLOCK_SIZE>().0 {
+            assert_eq!(&slot[..8], STATE_MAGIC);
+            assert_ne!(&slot[..8], b"IGGYWAL1");
+            assert_ne!(&slot[..8], b"IGGYWAL2");
+        }
+
+        // The other direction. A frontier a single-block build wrote is not a
+        // format this one reads, so it refuses rather than adopting a length
+        // that predates the slots.
+        let single = tempdir().unwrap();
+        let mut block = JournalState {
+            group: 42,
+            incarnation: 7,
+            ..JournalState::default()
+        }
+        .encode(0);
+        let legacy = block.as_mut_slice();
+        legacy[..8].copy_from_slice(b"IGGYWAL1");
+        legacy.copy_within(..8, SEALED_STATE_MAGIC_OFFSET);
+        let checksum = XxHash3_64::oneshot(&legacy[16..]);
+        legacy[8..16].copy_from_slice(&checksum.to_le_bytes());
+        std::fs::write(single.path().join(FRONTIER_FILE_NAME), 
block.as_slice()).unwrap();
+        std::fs::write(data_path(single.path(), 0), []).unwrap();
+        assert!(
+            PartitionPrepareJournal::open(single.path(), 42, 7)
+                .await
+                .is_err()
+        );
+    }
+
+    /// A slot left damaged keeps every later open on the tail-walking path,
+    /// where an ordinary unacknowledged tail reads as damage. Reopening has to
+    /// repair it even when recovery itself adopted nothing.
+    #[compio::test]
+    async fn 
reopening_repairs_a_damaged_slot_even_when_recovery_changes_nothing() {
+        let directory = tempdir().unwrap();
+        let mut journal = PartitionPrepareJournal::open(directory.path(), 42, 
7)
+            .await
+            .unwrap();
+        journal.append(prepare(1, 0).into_frozen()).await.unwrap();
+        let older = usize::try_from(frontier_offset(journal.frontier_sequence 
+ 1)).unwrap();
+        let data = data_path(directory.path(), journal.state.generation);
+        drop(journal);
+        let path = directory.path().join(FRONTIER_FILE_NAME);
+        let mut bytes = std::fs::read(&path).unwrap();
+        bytes[older..older + PARTITION_WAL_BLOCK_SIZE].fill(0);
+        std::fs::write(&path, &bytes).unwrap();
+
+        // Nothing to adopt, so the recovered state matches the published one.
+        let journal = PartitionPrepareJournal::open(directory.path(), 42, 7)
+            .await
+            .unwrap();
+        assert_eq!(journal.head(), 1);
+        drop(journal);
+        let repaired = std::fs::read(&path).unwrap();
+        for slot in repaired.as_chunks::<PARTITION_WAL_BLOCK_SIZE>().0 {
+            assert!(JournalState::decode(slot).is_ok());
+        }
+
+        // With both slots intact an unacknowledged partial tail is truncated
+        // rather than walked, so it cannot refuse the open.
+        let mut records = std::fs::read(&data).unwrap();
+        records.extend_from_slice(&[0; PARTITION_WAL_BLOCK_SIZE / 2]);
+        std::fs::write(&data, &records).unwrap();
+        let journal = PartitionPrepareJournal::open(directory.path(), 42, 7)
+            .await
+            .unwrap();
+        assert_eq!(journal.head(), 1);
+    }
+
+    /// `capacity` gates admission, not recovery. Lowering it must still reopen
+    /// the history the previous budget already accepted, including on the
+    /// tail-walking path a damaged slot forces.
+    #[compio::test]
+    async fn a_lowered_capacity_still_reopens_history_through_a_damaged_slot() 
{
+        let directory = tempdir().unwrap();
+        let mut journal = PartitionPrepareJournal::open(directory.path(), 42, 
7)
+            .await
+            .unwrap();
+        let first = prepare(1, 0);
+        let second = sized_prepare(2, first.header().checksum, 
PREPARE_BYTES_MAX);
+        let third = sized_prepare(3, second.header().checksum, 
PREPARE_BYTES_MAX);
+        journal.append(first.into_frozen()).await.unwrap();
+        // Past the published frontier, so reopening has to walk them, and past
+        // the lowered budget, so a budget check there would refuse them.
+        journal.append_buffered(second.into_frozen()).await.unwrap();
+        journal.append_buffered(third.into_frozen()).await.unwrap();
+        let newest = 
usize::try_from(frontier_offset(journal.frontier_sequence)).unwrap();
+        drop(journal);
+        let path = directory.path().join(FRONTIER_FILE_NAME);
+        let mut bytes = std::fs::read(&path).unwrap();
+        bytes[newest..newest + PARTITION_WAL_BLOCK_SIZE].fill(0);
+        std::fs::write(&path, &bytes).unwrap();
+        let journal = PartitionPrepareJournal::open_with_storage_and_capacity(
+            directory.path(),
+            42,
+            7,
+            DiskStorage,
+            PARTITION_WAL_CAPACITY_MIN,
+            false,
+        )
+        .await
+        .unwrap();
+        assert_eq!(journal.head(), 3);
+        assert!(journal.size_bytes() > PARTITION_WAL_CAPACITY_MIN);
+    }
+
     fn prepare(op: u64, parent: u128) -> Message<PrepareHeader> {
         sized_prepare(op, parent, size_of::<PrepareHeader>() + 16)
     }
diff --git a/core/journal/src/partition_journal/segments.rs 
b/core/journal/src/partition_journal/segments.rs
index 6ff31cbb8..84ca7e57b 100644
--- a/core/journal/src/partition_journal/segments.rs
+++ b/core/journal/src/partition_journal/segments.rs
@@ -335,7 +335,8 @@ impl<S: DurableStorage> PartitionPrepareJournal<S> {
             if entry.reference.is_some() {
                 continue;
             }
-            let (header, _, prepare, _) = 
self.read_record(entry.position).await?;
+            let (header, _, prepare, _) =
+                self.read_record(entry.position, self.state.length).await?;
             if header.operation == Operation::SendMessages
                 && decode_batch(prepare.as_slice())?.base_offset
                     >= segments.tail.position.next_offset
@@ -425,25 +426,34 @@ impl<S: DurableStorage> PartitionPrepareJournal<S> {
             .ok_or_else(|| invalid("segment writing handle is absent"))
     }
 
-    pub(super) async fn sync_segment_files(&mut self) -> io::Result<()> {
+    /// Barrier over every body and link this journal wrote, leaving the dirty
+    /// flags alone. Split out of [`Self::sync_segment_files`] so that
+    /// `PartitionPrepareJournal::sync` can overlap it with the WAL file's own
+    /// barrier and clear both flags once the pair has completed.
+    pub(super) async fn segment_barrier(&self) -> io::Result<()> {
         if self.segment_files_dirty {
             for file in self.segment_files.values() {
                 // Keep the writing handle: reopening after an errseq 
writeback error
                 // could turn a failed body barrier into a successful 
acknowledgment.
                 file.sync().await?;
             }
-            self.segment_files_dirty = false;
         }
         if self.segment_links_dirty {
             self.storage.sync_directory(&self.directory).await?;
             self.storage
                 .sync_directory(self.segment_directory()?)
                 .await?;
-            self.segment_links_dirty = false;
         }
         Ok(())
     }
 
+    pub(super) async fn sync_segment_files(&mut self) -> io::Result<()> {
+        self.segment_barrier().await?;
+        self.segment_files_dirty = false;
+        self.segment_links_dirty = false;
+        Ok(())
+    }
+
     pub(super) fn retain_active_segment_file(&mut self) {
         // Buffered rotations must retain their original writers until 
publication.
         if let Some(segments) = self.state.segment_storage {
diff --git a/core/partitions/src/iggy_index_writer.rs 
b/core/partitions/src/iggy_index_writer.rs
index 78ab2c0eb..ddd6f294d 100644
--- a/core/partitions/src/iggy_index_writer.rs
+++ b/core/partitions/src/iggy_index_writer.rs
@@ -106,6 +106,14 @@ impl IggyIndexWriter {
     ///
     /// Returns an error if the index bytes cannot be written or synced to 
disk.
     pub(crate) async fn save_indexes(&self, indexes: Vec<u8>) -> Result<u64, 
IggyError> {
+        let saved = self.save_indexes_buffered(indexes).await?;
+        if saved > 0 && self.fsync {
+            self.fsync().await?;
+        }
+        Ok(saved)
+    }
+
+    pub(crate) async fn save_indexes_buffered(&self, indexes: Vec<u8>) -> 
Result<u64, IggyError> {
         if indexes.is_empty() {
             return Ok(0);
         }
@@ -119,10 +127,6 @@ impl IggyIndexWriter {
             .0
             .map_err(|_| IggyError::CannotSaveIndexToSegment)?;
 
-        if self.fsync {
-            self.fsync().await?;
-        }
-
         trace!(
             target: "iggy.partitions.storage",
             file = self.file_path.as_str(),
@@ -162,6 +166,17 @@ impl IggyIndexWriter {
 mod tests {
     use super::*;
 
+    #[cfg(target_os = "linux")]
+    #[compio::test]
+    async fn 
buffered_indexes_defer_sync_errors_to_the_original_writer_barrier() {
+        let writer = IggyIndexWriter::new("/dev/null", 
Rc::new(AtomicU64::new(0)), true, false)
+            .await
+            .unwrap();
+        assert_eq!(writer.save_indexes_buffered(vec![1; 32]).await.unwrap(), 
32);
+        assert!(writer.fsync().await.is_err());
+        assert!(writer.save_indexes(vec![1; 32]).await.is_err());
+    }
+
     #[compio::test]
     async fn 
given_seeded_size_diverging_from_disk_when_opening_existing_file_should_return_size_mismatch_error()
      {
diff --git a/core/partitions/src/iggy_partition.rs 
b/core/partitions/src/iggy_partition.rs
index b4849701b..4f399f074 100644
--- a/core/partitions/src/iggy_partition.rs
+++ b/core/partitions/src/iggy_partition.rs
@@ -737,14 +737,22 @@ where
     /// # Errors
     /// Returns an error if durable prepare history cannot be opened or 
replayed.
     pub async fn open_persistence(&mut self) -> Result<(), IggyError> {
-        
self.open_persistence_with_capacity(journal::partition_journal::PARTITION_WAL_BYTES_MAX)
-            .await
+        self.open_persistence_with_capacity(
+            journal::partition_journal::PARTITION_WAL_BYTES_MAX,
+            std::time::Duration::ZERO,
+        )
+        .await
     }
 
     /// # Errors
     /// Returns an error if durable prepare history cannot be opened or 
replayed.
-    pub async fn open_persistence_with_capacity(&mut self, capacity: u64) -> 
Result<(), IggyError> {
-        self.open_persistence_with_recovered(capacity, None).await
+    pub async fn open_persistence_with_capacity(
+        &mut self,
+        capacity: u64,
+        group_commit_delay: std::time::Duration,
+    ) -> Result<(), IggyError> {
+        self.open_persistence_with_recovered(capacity, group_commit_delay, 
None)
+            .await
     }
 
     /// # Errors
@@ -753,6 +761,7 @@ where
     pub async fn open_persistence_with_recovered(
         &mut self,
         capacity: u64,
+        group_commit_delay: std::time::Duration,
         recovered: Option<(Rc<PartitionPersistence>, 
Vec<Message<PrepareHeader>>)>,
     ) -> Result<(), IggyError> {
         if self.consensus.replica_count() > 1
@@ -795,6 +804,7 @@ where
                 IggyError::CannotReadFile
             })?
         };
+        persistence.set_group_commit_delay(group_commit_delay);
         if !self.materialization_missing {
             let segment = self.log.active_segment();
             let length = segment.size.as_bytes_u64();
@@ -975,6 +985,17 @@ where
                 return;
             }
         }
+        if let Some(writer) = 
self.log.index_writers().last().and_then(Option::as_ref)
+            && let Err(error) = writer.fsync().await
+        {
+            error!(%error, namespace_raw = self.namespace().inner(), 
"partition checkpoint index sync failed");
+            self.fatal = Some(FatalCommit {
+                namespace_raw: self.namespace().inner(),
+                op: through_op,
+                operation: Operation::SendMessages,
+            });
+            return;
+        }
         let (files, directories) = self.persistence_checkpoint_files(config);
         persistence.checkpoint_files(through_op, files, directories);
         self.start_persistence();
@@ -6583,7 +6604,7 @@ where
                 .last()
                 .and_then(|writer| writer.as_ref())
                 .ok_or(IggyError::CannotWriteToFile)?;
-            let saved_indexes = index_writer.save_indexes(index_bytes).await?;
+            let saved_indexes = 
index_writer.save_indexes_buffered(index_bytes).await?;
             index_writer.advance(saved_indexes);
             if let Some(writer) = self
                 .log
@@ -6747,6 +6768,11 @@ where
                 "a plant at {start_offset} leaves a gap past {sealed_end} with 
no anchor"
             );
         }
+        if self.persistence.is_some()
+            && let Some(writer) = &self.log.index_writers()[sealed_index]
+        {
+            writer.fsync().await?;
+        }
         self.log.active_segment_mut().sealed = true;
         self.install_empty_segment(config, start_offset).await?;
         self.stats.increment_segments_count(1);
@@ -8694,6 +8720,34 @@ mod tests {
         assert!(!directory.path().join("prepares-0").exists());
     }
 
+    #[cfg(target_os = "linux")]
+    #[compio::test]
+    async fn 
checkpoint_index_sync_failure_fences_before_reclaiming_wal_history() {
+        let directory = tempfile::tempdir().unwrap();
+        let (mut partition, _) = recording_partition_at(0, 3);
+        
partition.set_partition_dir(directory.path().to_string_lossy().into_owned());
+        partition.runtime_options.durability = 
iggy_common::Durability::Persisted;
+        partition.open_persistence().await.unwrap();
+        let persistence = Rc::clone(partition.persistence.as_ref().unwrap());
+        let prepare = checksummed_segment_prepare(1, 0, 0, b"durable");
+        persistence.append(prepare.into_frozen(), true).unwrap();
+        assert!(persistence.start());
+        Rc::clone(&persistence).run().await;
+        assert!(persistence.is_durable_through(1));
+        partition.consensus.restore_commit_state(1, 1);
+        let writer = IggyIndexWriter::new("/dev/null", 
Rc::new(AtomicU64::new(0)), true, false)
+            .await
+            .unwrap();
+        assert_eq!(writer.save_indexes_buffered(vec![1; 32]).await.unwrap(), 
32);
+        let active = partition.log.index_writers().len() - 1;
+        partition.log.index_writers_mut()[active] = Some(Rc::new(writer));
+        persistence.request_checkpoint();
+        partition.checkpoint_persistence(&repair_config()).await;
+        assert!(partition.fatal().is_some());
+        assert!(!persistence.checkpoint_pending());
+        assert!(persistence.is_durable_through(1));
+    }
+
     #[compio::test]
     async fn 
pending_wal_prefix_keeps_pipeline_replies_and_does_not_partially_flush() {
         for durability in [
diff --git a/core/partitions/src/install_backup.rs 
b/core/partitions/src/install_backup.rs
index 37a9e2bb0..c07c3c771 100644
--- a/core/partitions/src/install_backup.rs
+++ b/core/partitions/src/install_backup.rs
@@ -16,6 +16,7 @@
 // under the License.
 
 use journal::durable_storage::{DiskStorage, DurableFile, DurableStorage, 
OpenMode};
+use journal::partition_journal::FRONTIER_FILE_NAME;
 use std::io;
 use std::path::Path;
 
@@ -124,6 +125,12 @@ async fn link_tree<S: DurableStorage>(
             if entry.directory {
                 storage.create_directories(&destination).await?;
                 pending.push((source.join(&name), destination));
+            } else if name == FRONTIER_FILE_NAME {
+                // The partition WAL publishes its frontier by overwriting one 
of
+                // two slots in place, so a hard link would not freeze it: the
+                // WAL reset this snapshot exists to roll back would rewrite 
the
+                // snapshot's own bytes. Two blocks, copied once per install.
+                copy_file(&source.join(&name), &destination, storage).await?;
             } else {
                 // Transfer unlinks or atomically replaces these frozen files.
                 // Hard links retain the old bytes without copying segment 
data.
@@ -142,6 +149,20 @@ async fn link_tree<S: DurableStorage>(
     Ok(())
 }
 
+async fn copy_file<S: DurableStorage>(
+    source: &Path,
+    destination: &Path,
+    storage: &S,
+) -> io::Result<()> {
+    let original = storage.open(source, OpenMode::Read).await?;
+    let length = usize::try_from(original.length().await?)
+        .map_err(|_| io::Error::other("partition WAL frontier is too large to 
copy"))?;
+    let bytes = original.read(0, length).await?;
+    let mut copy = storage.open(destination, OpenMode::Create).await?;
+    copy.write(0, bytes).await?;
+    copy.sync().await
+}
+
 fn is_scratch(name: &str) -> bool {
     matches!(name, BACKUP | BUILDING | RETIRED)
         || Path::new(name)
diff --git a/core/partitions/src/persistence.rs 
b/core/partitions/src/persistence.rs
index 6a1e4b3dc..3f1bb8870 100644
--- a/core/partitions/src/persistence.rs
+++ b/core/partitions/src/persistence.rs
@@ -30,13 +30,20 @@ use std::path::{Path, PathBuf};
 use std::rc::Rc;
 use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
 use std::sync::{Arc, LazyLock, Mutex, Weak};
-use std::time::Duration;
+use std::time::{Duration, Instant};
 
 #[cfg(unix)]
 use nix::sys::resource::{Resource, getrlimit};
 
-const APPEND_BATCH_BYTES_MAX: u64 = 1024 * 1024;
-const APPEND_BATCH_OPS_MAX: usize = 64;
+// Group commit bounds, not throughput bounds. Every prepare in a group is
+// already queued and waiting, so widening the group moves work off the barrier
+// and onto a buffered memcpy: one body write and one durability barrier serve
+// the whole group instead of each prepare paying its own. The byte budget is
+// charged against the padded BODY size even when the WAL stores a segment
+// reference and writes 4096 bytes per record, so a tight budget caps grouping
+// far below what the write itself costs.
+const APPEND_BATCH_BYTES_MAX: u64 = 8 * 1024 * 1024;
+const APPEND_BATCH_OPS_MAX: usize = 256;
 const CHECKPOINT_DIRTY_FILES_MAX: usize = 1024;
 #[cfg(unix)]
 const OFFSET_FILES_TOTAL_MAX: usize = 1024;
@@ -146,6 +153,11 @@ pub struct PartitionPersistence<S: DurableStorage = 
DiskStorage> {
     failure: RefCell<Option<Arc<io::Error>>>,
     failure_operation: Cell<Operation>,
     notifier: RefCell<Option<PersistenceNotifier>>,
+    group_commit_delay: Cell<Duration>,
+    /// Interval between the two most recent submissions. Decides whether a
+    /// group-commit wait would see another prepare before it expires.
+    append_gap: Cell<Duration>,
+    last_append: Cell<Option<Instant>>,
     completed_batches: Cell<u64>,
     batched_prepares: Cell<u64>,
     completed_checkpoints: Cell<u64>,
@@ -526,6 +538,9 @@ impl<S: DurableStorage> PartitionPersistence<S> {
             failure: RefCell::new(None),
             failure_operation: Cell::new(Operation::SendMessages),
             notifier: RefCell::new(None),
+            group_commit_delay: Cell::new(Duration::ZERO),
+            append_gap: Cell::new(Duration::MAX),
+            last_append: Cell::new(None),
             completed_batches: Cell::new(0),
             batched_prepares: Cell::new(0),
             completed_checkpoints: Cell::new(0),
@@ -578,6 +593,12 @@ impl<S: DurableStorage> PartitionPersistence<S> {
         false
     }
 
+    /// Bound on the wait the writer may take before a barrier, to let more
+    /// prepares join the group. Zero keeps the writer's barrier-paced 
grouping.
+    pub fn set_group_commit_delay(&self, delay: Duration) {
+        self.group_commit_delay.set(delay);
+    }
+
     pub fn set_notifier(&self, notifier: PersistenceNotifier) {
         *self.notifier.borrow_mut() = Some(notifier);
     }
@@ -696,6 +717,14 @@ impl<S: DurableStorage> PartitionPersistence<S> {
                 "partition WAL submission is out of order",
             ));
         }
+        let now = Instant::now();
+        self.append_gap.set(
+            self.last_append
+                .replace(Some(now))
+                .map_or(Duration::MAX, |previous| {
+                    now.saturating_duration_since(previous)
+                }),
+        );
         self.queued_bytes.set(self.queued_bytes.get() + bytes);
         self.accepted
             .borrow_mut()
@@ -1227,46 +1256,28 @@ impl<S: DurableStorage> PartitionPersistence<S> {
         &self,
         journal: &mut PartitionPrepareJournal<S>,
         first: Frozen<4096>,
-        mut durable: bool,
+        durable: bool,
         epoch: u64,
         first_bytes: u64,
     ) -> io::Result<()> {
         let mut batch = SmallVec::<[Frozen<4096>; 8]>::new();
         batch.push(first);
         let mut bytes = first_bytes;
-        {
-            let mut queue = self.queue.borrow_mut();
-            while batch.len() < APPEND_BATCH_OPS_MAX {
-                let Some(Mutation::Append {
-                    epoch: next_epoch,
-                    bytes: next_bytes,
-                    ..
-                }) = queue.front()
-                else {
-                    break;
-                };
-                if *next_epoch != epoch
-                    || bytes.saturating_add(*next_bytes) > 
APPEND_BATCH_BYTES_MAX
-                {
-                    break;
-                }
-                let Some(Mutation::Append {
-                    prepare,
-                    durable: requires_sync,
-                    bytes: record_bytes,
-                    ..
-                }) = queue.pop_front()
-                else {
-                    unreachable!("append prefix was checked");
-                };
-                bytes += record_bytes;
-                self.queued_bytes
-                    .set(self.queued_bytes.get().saturating_sub(record_bytes));
-                durable |= requires_sync;
-                batch.push(prepare);
-            }
-        }
+        let mut durable = durable;
+        self.collect_queued(&mut batch, &mut bytes, &mut durable, epoch);
+        // Charged before the wait: `collect_queued` took these bytes out of 
the
+        // queued total, and admission and checkpoint pacing sum queued and
+        // in-flight bytes against the budget, so a gap here would admit a full
+        // group past it.
         self.in_flight_bytes.set(bytes);
+        // The barrier is what groups prepares, so a barrier cheaper than the
+        // interval between arrivals groups nothing and every prepare pays its
+        // own writes. This wait puts that grouping back under operator 
control.
+        if durable && let Some(delay) = self.group_commit_wait(&batch, bytes) {
+            compio::runtime::time::sleep(delay).await;
+            self.collect_queued(&mut batch, &mut bytes, &mut durable, epoch);
+            self.in_flight_bytes.set(bytes);
+        }
         let count = batch.len() as u64;
         journal.append_batch_buffered(&batch).await?;
         if durable {
@@ -1278,6 +1289,58 @@ impl<S: DurableStorage> PartitionPersistence<S> {
         Ok(())
     }
 
+    /// Move every queued append that still fits into `batch`.
+    fn collect_queued(
+        &self,
+        batch: &mut SmallVec<[Frozen<4096>; 8]>,
+        bytes: &mut u64,
+        durable: &mut bool,
+        epoch: u64,
+    ) {
+        let mut queue = self.queue.borrow_mut();
+        while batch.len() < APPEND_BATCH_OPS_MAX {
+            let Some(Mutation::Append {
+                epoch: next_epoch,
+                bytes: next_bytes,
+                ..
+            }) = queue.front()
+            else {
+                break;
+            };
+            if *next_epoch != epoch || bytes.saturating_add(*next_bytes) > 
APPEND_BATCH_BYTES_MAX {
+                break;
+            }
+            let Some(Mutation::Append {
+                prepare,
+                durable: requires_sync,
+                bytes: record_bytes,
+                ..
+            }) = queue.pop_front()
+            else {
+                unreachable!("append prefix was checked");
+            };
+            *bytes += record_bytes;
+            self.queued_bytes
+                .set(self.queued_bytes.get().saturating_sub(record_bytes));
+            *durable |= requires_sync;
+            batch.push(prepare);
+        }
+    }
+
+    /// How long to wait for more prepares before the barrier, if at all.
+    ///
+    /// `None` for a disabled delay, a group already at its bounds, or arrivals
+    /// spaced wider than the delay, where the wait would expire before the 
next
+    /// prepare reached the queue.
+    fn group_commit_wait(&self, batch: &[Frozen<4096>], bytes: u64) -> 
Option<Duration> {
+        let delay = self.group_commit_delay.get();
+        if delay.is_zero() || batch.len() >= APPEND_BATCH_OPS_MAX || bytes >= 
APPEND_BATCH_BYTES_MAX
+        {
+            return None;
+        }
+        (self.append_gap.get() <= delay).then_some(delay)
+    }
+
     fn notify(&self) {
         if let Some(notifier) = self.notifier.borrow().as_ref() {
             notifier(PersistenceCompletion {
@@ -1532,6 +1595,94 @@ mod tests {
         assert_eq!(persistence.disk_bytes.get(), 0);
     }
 
+    /// The barrier is what groups prepares, so a barrier cheaper than the
+    /// interval between arrivals leaves every prepare paying its own writes.
+    /// The delay restores the grouping without changing what the barrier
+    /// covers, and it must not fire on a partition whose arrivals are spaced
+    /// wider than the wait.
+    #[compio::test]
+    async fn 
a_group_commit_delay_admits_prepares_that_arrive_during_the_wait() {
+        const DELAY: Duration = Duration::from_millis(200);
+        let directory = tempdir().unwrap();
+        let (persistence, _) =
+            PartitionPersistence::open(&directory.path().join("prepares-7"), 
42, 7)
+                .await
+                .unwrap();
+        persistence.set_group_commit_delay(DELAY);
+        let first = prepare(1, 0);
+        let second = prepare(2, first.header().checksum);
+        let third = prepare(3, second.header().checksum);
+        // Two back-to-back submissions put the arrival estimate under the 
delay.
+        persistence.append(first.into_frozen(), true).unwrap();
+        persistence.append(second.into_frozen(), true).unwrap();
+        assert!(persistence.start());
+        let writer = Rc::clone(&persistence);
+        let late = async {
+            compio::runtime::time::sleep(DELAY / 4).await;
+            // Collected bytes stay charged against the budget while the writer
+            // waits; they left the queue but have not reached the barrier.
+            let waiting = persistence.take_metrics();
+            assert_eq!(waiting.queued_bytes, 0);
+            assert_eq!(waiting.in_flight_bytes, 2 * 4096);
+            persistence.append(third.into_frozen(), true).unwrap();
+        };
+        futures::future::join(writer.run(), late).await;
+        assert!(persistence.failure().is_none());
+        assert!(persistence.is_durable_through(3));
+        let metrics = persistence.take_metrics();
+        assert_eq!(metrics.completed_batches, 1);
+        assert_eq!(metrics.batched_prepares, 3);
+        assert_eq!(metrics.in_flight_bytes, 0);
+    }
+
+    /// A group with no barrier to amortize gains nothing from waiting, and a
+    /// wait there would only delay the durable group queued behind it.
+    #[compio::test]
+    async fn a_group_commit_delay_is_skipped_for_a_group_without_a_barrier() {
+        let directory = tempdir().unwrap();
+        let (persistence, _) =
+            PartitionPersistence::open(&directory.path().join("prepares-7"), 
42, 7)
+                .await
+                .unwrap();
+        // Wide apart on purpose: a taken wait is at least the delay, a slow
+        // append on a loaded runner is milliseconds, so the bound cannot be
+        // crossed by either for the wrong reason.
+        persistence.set_group_commit_delay(Duration::from_secs(2));
+        let first = prepare(1, 0);
+        let second = prepare(2, first.header().checksum);
+        persistence.append(first.into_frozen(), false).unwrap();
+        persistence.append(second.into_frozen(), false).unwrap();
+        assert!(persistence.start());
+        let started = Instant::now();
+        Rc::clone(&persistence).run().await;
+        assert!(started.elapsed() < Duration::from_secs(1));
+        assert!(persistence.is_written_through(2));
+        assert_eq!(persistence.take_metrics().completed_batches, 0);
+    }
+
+    /// A partition whose prepares arrive further apart than the delay would 
pay
+    /// the wait for nothing, so the estimate has to keep it off.
+    #[compio::test]
+    async fn a_group_commit_delay_is_skipped_when_arrivals_outlast_it() {
+        let directory = tempdir().unwrap();
+        let (persistence, _) =
+            PartitionPersistence::open(&directory.path().join("prepares-7"), 
42, 7)
+                .await
+                .unwrap();
+        persistence.set_group_commit_delay(Duration::from_millis(1));
+        let first = prepare(1, 0);
+        let second = prepare(2, first.header().checksum);
+        persistence.append(first.into_frozen(), true).unwrap();
+        compio::runtime::time::sleep(Duration::from_millis(20)).await;
+        persistence.append(second.into_frozen(), true).unwrap();
+        assert!(persistence.start());
+        Rc::clone(&persistence).run().await;
+        assert!(persistence.is_durable_through(2));
+        // Both were queued before the writer started, so they share one 
barrier
+        // regardless. What matters is that no wait was taken to get there.
+        assert_eq!(persistence.take_metrics().completed_batches, 1);
+    }
+
     fn prepare(op: u64, parent: u128) -> Message<PrepareHeader> {
         let mut owned = Owned::<4096>::zeroed(size_of::<PrepareHeader>());
         let header = 
bytemuck::checked::from_bytes_mut::<PrepareHeader>(owned.as_mut_slice());
diff --git a/core/server/config.toml b/core/server/config.toml
index 9d51eb0a8..0b8cdb518 100644
--- a/core/server/config.toml
+++ b/core/server/config.toml
@@ -907,6 +907,18 @@ clients_table_max = 8192
 # partition_wal_disk_bytes measures WAL file length, excluding referenced 
segment bodies.
 wal_bytes_max = "256 MiB"
 
+# Bounded wait for more prepares before a persisted partition's WAL writer
+# starts its durability barrier, in microseconds. 0 disables it. Max 10000.
+# Spends up to this much acknowledgment latency to cut device writes: one
+# barrier and one frontier write then cover a whole group of prepares instead 
of
+# a single one. Durability is unchanged, the same barrier runs over the same
+# bytes, later, and the quorum gate is untouched.
+# Skipped while prepares arrive further apart than the delay, so an idle
+# partition never waits. Earns nothing until the barrier completes faster than
+# prepares arrive, which is where the writer stops grouping by itself. Start
+# near the measured barrier duration.
+wal_group_commit_delay_micros = 0
+
 # Verify the checksum of batches read from segment storage. A mismatch is 
reported rather than served.
 validate_checksum = true
 
diff --git a/core/server/src/dispatch/partition.rs 
b/core/server/src/dispatch/partition.rs
index dbed56cfa..eadc9d829 100644
--- a/core/server/src/dispatch/partition.rs
+++ b/core/server/src/dispatch/partition.rs
@@ -1473,7 +1473,11 @@ mod tests {
         .await
         .unwrap();
         partition
-            .open_persistence_with_recovered(capacity, 
Some((Rc::clone(&persistence), prepares)))
+            .open_persistence_with_recovered(
+                capacity,
+                std::time::Duration::ZERO,
+                Some((Rc::clone(&persistence), prepares)),
+            )
             .await
             .unwrap();
 
diff --git a/core/server/src/partition_helpers.rs 
b/core/server/src/partition_helpers.rs
index cebe04091..121df8de7 100644
--- a/core/server/src/partition_helpers.rs
+++ b/core/server/src/partition_helpers.rs
@@ -960,13 +960,7 @@ async fn load_partition(
     )
     .await?;
 
-    partition
-        .open_persistence_with_recovered(
-            config.partition.wal_bytes_max.as_bytes_u64(),
-            recovered_persistence,
-        )
-        .await
-        .map_err(|error| ServerError::Iggy(Box::new(error)))?;
+    open_partition_persistence(&mut partition, config, 
recovered_persistence).await?;
     Ok(partition)
 }
 
@@ -1507,11 +1501,28 @@ pub async fn build_partition_fresh(
         });
     }
 
+    open_partition_persistence(&mut partition, config, None).await?;
+    Ok(partition)
+}
+
+/// Open a partition's prepare WAL with the budget and the group-commit delay
+/// this server was configured with.
+async fn open_partition_persistence(
+    partition: &mut IggyPartition<Rc<IggyMessageBus>>,
+    config: &ServerConfig,
+    recovered: Option<(
+        Rc<PartitionPersistence>,
+        Vec<server_common::Message<iggy_binary_protocol::PrepareHeader>>,
+    )>,
+) -> Result<(), ServerError> {
     partition
-        
.open_persistence_with_capacity(config.partition.wal_bytes_max.as_bytes_u64())
+        .open_persistence_with_recovered(
+            config.partition.wal_bytes_max.as_bytes_u64(),
+            
std::time::Duration::from_micros(config.partition.wal_group_commit_delay_micros),
+            recovered,
+        )
         .await
-        .map_err(|error| ServerError::Iggy(Box::new(error)))?;
-    Ok(partition)
+        .map_err(|error| ServerError::Iggy(Box::new(error)))
 }
 
 async fn persist_partition_hierarchy(
@@ -1749,7 +1760,12 @@ mod tests {
         drop(store);
         let frontier = Path::new(&directory).join("prepares-0/frontier");
         let mut corrupt = std::fs::read(&frontier).unwrap();
-        corrupt[0] ^= u8::MAX;
+        // Every slot: the frontier alternates between two of them, and one
+        // damaged copy is recoverable by design, so damaging a single slot
+        // would open the partition instead of fencing it.
+        for slot in 
corrupt.chunks_mut(journal::partition_journal::PARTITION_WAL_BLOCK_SIZE) {
+            slot[0] ^= u8::MAX;
+        }
         std::fs::write(&frontier, &corrupt).unwrap();
         let partitions = solo_partitions();
         let metadata = Partition::new(0, namespace.inner(), 
IggyTimestamp::now(), 0, 0);
diff --git a/core/server/src/segment_recovery.rs 
b/core/server/src/segment_recovery.rs
index ca82b42bf..d637dfa8c 100644
--- a/core/server/src/segment_recovery.rs
+++ b/core/server/src/segment_recovery.rs
@@ -78,13 +78,12 @@ const INDEX_SCAN_YIELD_STRIDE: u64 = 1024;
 
 /// Index entries the log may legitimately fail to back under 
`durable_segments`.
 /// Persistence writes exactly one entry per flush chunk and chunks never
-/// overlap. The two halves fdatasync concurrently WITHIN one flush, but
-/// flushes are serialized, and the log's fdatasync covers the whole file: an
-/// entry existing above entry N therefore proves the log was synced through
-/// chunk N. Only the chunk in flight when the process died can leave an entry
-/// the log never backed. See [`PartitionRecoveryRefusal::FsyncedLogLoss`] for
-/// why a deeper step-back is evidence about the log rather than about the
-/// index.
+/// overlap. The WAL makes a body durable before it acknowledges it, and the
+/// flush that indexes that body runs later still, so an entry existing on disk
+/// proves the log bytes it names were already fdatasynced. Only the chunk in
+/// flight when the process died can leave an entry the log never backed. See
+/// [`PartitionRecoveryRefusal::FsyncedLogLoss`] for why a deeper step-back is
+/// evidence about the log rather than about the index.
 const MAX_FSYNCED_INDEX_STEP_BACK_ENTRIES: u64 = 1;
 
 /// Index entries the backward anchor search probes before giving up, at one
diff --git a/core/server/src/server_error.rs b/core/server/src/server_error.rs
index b32a8ab7f..5e9bc6188 100644
--- a/core/server/src/server_error.rs
+++ b/core/server/src/server_error.rs
@@ -436,9 +436,10 @@ pub enum PartitionRecoveryRefusal {
     },
     /// The sparse index of a topic running under `persisted durability` 
outruns its
     /// log by more than the one entry a crash can legitimately strand there.
-    /// Persistence writes exactly one entry per flush chunk, chunks never
-    /// overlap, and flushes are serialized, so every entry below the last one
-    /// names a chunk whose log bytes completed their fdatasync. A completed
+    /// Persistence writes exactly one entry per flush chunk and chunks never
+    /// overlap. The WAL makes a body durable before acknowledging it and the
+    /// flush indexes it later, so every entry on disk names a chunk whose log
+    /// bytes completed their fdatasync. A completed
     /// chunk can contain batches acknowledged before the flush threshold was
     /// reached, while the in-flight chunk can do so too. Reply timing is not
     /// the proof. Only the chunk in flight when the process died can have an
diff --git a/core/simulator/src/storage/tests.rs 
b/core/simulator/src/storage/tests.rs
index c64ecb5dd..20a0b77d1 100644
--- a/core/simulator/src/storage/tests.rs
+++ b/core/simulator/src/storage/tests.rs
@@ -58,6 +58,11 @@ enum Mutation {
     Append,
     CertifyView,
     Checkpoint,
+    /// A checkpoint whose rewrite runs while the outgoing generation still
+    /// holds a buffered record. A torn publication leaves the older slot 
naming
+    /// that generation, so recovery walks its tail and must not read an
+    /// unsynced record there as damage.
+    CheckpointBufferedTail,
     Truncate,
     Reset,
     Purge,
@@ -123,6 +128,7 @@ fn 
wal_fault_sweep_preserves_acknowledged_history_at_every_io_boundary() {
             Mutation::Append,
             Mutation::CertifyView,
             Mutation::Checkpoint,
+            Mutation::CheckpointBufferedTail,
             Mutation::Truncate,
             Mutation::Reset,
             Mutation::Purge,
@@ -740,8 +746,8 @@ fn 
checkpoint_skips_duplicate_offset_sync_but_still_refuses_a_missing_path() {
                         .iter()
                         .filter(|operation| **operation == 
StorageOperation::FileSync)
                         .count(),
-                    3,
-                    "original offset writer, replacement WAL, frontier"
+                    4,
+                    "original offset writer, outgoing WAL, replacement WAL, 
frontier"
                 );
             }
         }
@@ -974,27 +980,22 @@ fn 
queued_prepares_share_a_barrier_and_survive_power_loss_together() {
         Rc::clone(&persistence).run().await;
         assert!(persistence.failure().is_none());
         let trace = storage.trace();
-        assert_eq!(
-            trace
-                .iter()
-                .filter(|operation| **operation == StorageOperation::FileSync)
-                .count(),
-            4
-        );
-        assert_eq!(
-            trace
-                .iter()
-                .filter(|operation| **operation == 
StorageOperation::DirectorySync)
-                .count(),
-            2
-        );
-        assert_eq!(
+        let count = |wanted: StorageOperation| {
             trace
                 .iter()
-                .filter(|operation| **operation == StorageOperation::Write)
-                .count(),
-            4
-        );
+                .filter(|operation| **operation == wanted)
+                .count()
+        };
+        // One group: the WAL extent and the frontier slot, one barrier each.
+        assert_eq!(count(StorageOperation::Write), 2);
+        assert_eq!(count(StorageOperation::FileSync), 2);
+        // Publication overwrites a pre-existing slot in place, so an
+        // acknowledgment creates no file, renames nothing and leaves no
+        // directory to make durable. Those are the filesystem metadata
+        // transactions this path must never pay per batch.
+        assert_eq!(count(StorageOperation::Create), 0);
+        assert_eq!(count(StorageOperation::Rename), 0);
+        assert_eq!(count(StorageOperation::DirectorySync), 0);
         assert!(persistence.is_durable_through(65));
         storage.crash(Crash::PowerLoss);
         let recovered = 
PartitionPrepareJournal::open_with_storage(Path::new(WAL), 42, 7, storage)
@@ -1004,6 +1005,66 @@ fn 
queued_prepares_share_a_barrier_and_survive_power_loss_together() {
     });
 }
 
+#[test]
+fn 
queued_owned_prepares_share_three_file_barriers_without_directory_mutations() {
+    block_on(async {
+        for count in [65, 256, 257] {
+            let storage = storage_for_partition().await;
+            let (persistence, _) =
+                PartitionPersistence::open_with_storage(Path::new(WAL), 42, 7, 
storage.clone())
+                    .await
+                    .unwrap();
+            persistence.enable_segment_storage(SegmentPosition::default(), 64 
* 1024 * 1024);
+            let first = owned_prepare(1, 0, 0);
+            let mut parent = first.header().checksum;
+            persistence.append(first.into_frozen(), true).unwrap();
+            assert!(persistence.start());
+            Rc::clone(&persistence).run().await;
+            assert!(persistence.failure().is_none());
+            persistence.take_metrics();
+            for index in 1..=count {
+                let prepare = owned_prepare(1, parent, index).transmute_header(
+                    |original, header: &mut PrepareHeader| {
+                        *header = original;
+                        header.op = index + 1;
+                        header.checksum = header.identity_checksum();
+                    },
+                );
+                parent = prepare.header().checksum;
+                persistence.append(prepare.into_frozen(), true).unwrap();
+            }
+            storage.clear_trace();
+            assert!(persistence.start());
+            Rc::clone(&persistence).run().await;
+            assert!(persistence.failure().is_none());
+            let trace = storage.trace();
+            let operations = |wanted: StorageOperation| {
+                trace
+                    .iter()
+                    .filter(|operation| **operation == wanted)
+                    .count() as u64
+            };
+            let groups = count.div_ceil(256);
+            assert_eq!(operations(StorageOperation::Write), 3 * groups);
+            assert_eq!(operations(StorageOperation::FileSync), 3 * groups);
+            assert_eq!(operations(StorageOperation::Create), 0);
+            assert_eq!(operations(StorageOperation::Rename), 0);
+            assert_eq!(operations(StorageOperation::DirectorySync), 0);
+            let metrics = persistence.take_metrics();
+            assert_eq!(metrics.completed_batches, groups);
+            assert_eq!(metrics.batched_prepares, count);
+            assert!(persistence.is_durable_through(count + 1));
+            storage.crash(Crash::PowerLoss);
+            let recovered =
+                PartitionPrepareJournal::open_with_storage(Path::new(WAL), 42, 
7, storage)
+                    .await
+                    .unwrap();
+            assert_eq!(recovered.head(), count + 1);
+            assert_eq!(recovered.prepares().await.unwrap().len() as u64, count 
+ 1);
+        }
+    });
+}
+
 #[test]
 fn failed_group_barrier_never_acknowledges_a_partial_batch() {
     block_on(async {
@@ -2003,7 +2064,7 @@ async fn mutate_owned_segments(
                 .append(owned_prepare(3, second.header().checksum, 
2).into_frozen())
                 .await
         }
-        Mutation::Checkpoint => journal.checkpoint(2).await,
+        Mutation::Checkpoint | Mutation::CheckpointBufferedTail => 
journal.checkpoint(2).await,
         Mutation::Truncate => journal.truncate_from(2).await,
         Mutation::CertifyView => {
             journal
@@ -2234,7 +2295,7 @@ async fn mutate_referenced(
                 .certify_log_view(2, 2, second.header().checksum)
                 .await
         }
-        Mutation::Checkpoint => journal.checkpoint(2).await,
+        Mutation::Checkpoint | Mutation::CheckpointBufferedTail => 
journal.checkpoint(2).await,
         Mutation::Truncate => journal.truncate_from(2).await,
         Mutation::Reset => journal.reset(7, None).await,
         Mutation::Purge => {
@@ -2282,7 +2343,7 @@ async fn assert_referenced_recovery(
                 assert_eq!(journal.head(), 7, "{context}");
             }
         }
-        Mutation::Checkpoint => {
+        Mutation::Checkpoint | Mutation::CheckpointBufferedTail => {
             assert_eq!(journal.head(), 2, "{context}");
             assert!([0, 2].contains(&journal.checkpoint_op()), "{context}");
             if completed {
@@ -2367,6 +2428,17 @@ async fn mutate(
             replace(storage, Path::new("/partition/materialized"), 
b"1,2").await?;
             journal.checkpoint(2).await
         }
+        Mutation::CheckpointBufferedTail => {
+            let entries = journal.prepares().await?;
+            let last = bytemuck::checked::from_bytes::<PrepareHeader>(
+                
&entries.last().unwrap().as_slice()[..size_of::<PrepareHeader>()],
+            );
+            journal
+                .append_buffered(prepare(4, last.checksum).into_frozen())
+                .await?;
+            replace(storage, Path::new("/partition/materialized"), 
b"1,2").await?;
+            journal.checkpoint(2).await
+        }
         Mutation::Truncate => journal.truncate_from(3).await,
         Mutation::Reset => {
             replace(storage, Path::new("/partition/materialized"), 
b"1-7").await?;
@@ -2403,6 +2475,16 @@ async fn replace(storage: &SimStorage, path: &Path, 
bytes: &[u8]) -> io::Result<
     storage.sync_directory(path.parent().unwrap()).await
 }
 
+/// The buffered record is acknowledged by nothing, so recovery may keep or 
drop
+/// it. Refusing the open is the failure this covers.
+fn assert_buffered_tail_checkpoint(journal: 
&PartitionPrepareJournal<SimStorage>, completed: bool) {
+    assert!((3..=4).contains(&journal.head()));
+    assert!([0, 2].contains(&journal.checkpoint_op()));
+    if completed {
+        assert_eq!(journal.checkpoint_op(), 2);
+    }
+}
+
 async fn assert_recovery(
     storage: &SimStorage,
     journal: &PartitionPrepareJournal<SimStorage>,
@@ -2425,6 +2507,7 @@ async fn assert_recovery(
                 assert_eq!(journal.head(), 4);
             }
         }
+        Mutation::CheckpointBufferedTail => 
assert_buffered_tail_checkpoint(journal, completed),
         Mutation::Checkpoint => {
             assert_eq!(journal.head(), 3);
             assert!([0, 2].contains(&journal.checkpoint_op()));
@@ -2495,7 +2578,12 @@ async fn assert_recovery(
     assert_eq!(
         entries.len() as u64,
         journal.head() - journal.checkpoint_op()
-            + u64::from(matches!(mutation, Mutation::Checkpoint) && 
journal.checkpoint_op() > 0)
+            + u64::from(
+                matches!(
+                    mutation,
+                    Mutation::Checkpoint | Mutation::CheckpointBufferedTail
+                ) && journal.checkpoint_op() > 0,
+            )
     );
 }
 

Reply via email to