hubcio commented on code in PR #4278:
URL: https://github.com/apache/iggy/pull/4278#discussion_r4165416928


##########
core/partitions/src/persistence.rs:
##########
@@ -903,6 +912,27 @@ impl<S: DurableStorage> PartitionPersistence<S> {
         });
     }
 
+    /// Queue a durability barrier through every retained original writer,
+    /// including offset files restored from the backup, without advancing
+    /// [`PartitionPersistence::checkpoint_op`].
+    pub(crate) fn barrier_files(&self, barriers: Vec<FileSyncBarrier>) -> 
BTreeSet<PathBuf> {
+        let synced_files = self
+            .offset_files
+            .borrow()
+            .keys()
+            .map(PathBuf::from)
+            .chain(barriers.iter().map(|barrier| barrier.path.clone()))
+            .collect();
+        let mut offset_files = std::mem::take(&mut 
*self.retired_offset_files.borrow_mut());
+        offset_files.extend(self.offset_files.borrow_mut().drain().map(|(_, 
file)| file));
+        self.queue.borrow_mut().push_back(Mutation::Barrier {

Review Comment:
   warning: if the drain times out, this barrier stays queued, and a 
view-change truncate drops it at line 982, closing its offset writers unsynced. 
move a dropped barrier's `offset_files` into `retired_offset_files` there - 
re-stamping like `Checkpoint` republishes the pre-truncate heads.



##########
core/simulator/src/storage/tests.rs:
##########
@@ -812,12 +814,14 @@ fn 
given_a_failed_writeback_when_beginning_an_install_backup_should_refuse_publi
         storage.sync_directory(Path::new(DIRECTORY)).await.unwrap();
 
         storage.fail_writeback(path).unwrap();
-        let result = install_backup::begin_with_storage(Path::new(DIRECTORY), 
&storage).await;
+        let barrier = FileSyncBarrier::from_file(path, writer);
+        let result = install_backup::begin_with_storage_and_barriers(

Review Comment:
   warning: this only exercises the test-only shim, so it stays green with the 
production barrier stubbed out, and nothing in `partitions` or `simulator` 
fails if the messages-writer barrier or the offset half of `barrier_files` goes 
away. drive `barrier_files` with failed-writeback `SimStorage` writers and drop 
the shim.



##########
core/partitions/src/install_backup.rs:
##########
@@ -65,17 +70,47 @@ pub async fn recover_with_storage<S: DurableStorage>(
 /// # Errors
 /// Returns an error if the rollback state cannot be made durable.
 /// After any failure the caller must stop serving until recovery.
-pub async fn begin(directory: &Path) -> io::Result<()> {
-    begin_with_storage(directory, &DiskStorage)
+pub async fn begin(directory: &Path, synced_files: BTreeSet<PathBuf>) -> 
io::Result<()> {
+    begin_with_synced_files(directory, synced_files, &DiskStorage)
         .await
         .note_descriptor_exhaustion(|| format!("starting an install in {}", 
directory.display()))
 }
 
+/// Storage-generic form of [`begin`] used by deterministic storage tests.
+///
 /// # Errors
 /// Returns an error when the install transaction cannot complete durably.
 pub async fn begin_with_storage<S: DurableStorage>(
     directory: &Path,
     storage: &S,
+) -> io::Result<()> {
+    begin_with_synced_files(directory, BTreeSet::new(), storage).await
+}
+
+/// Simulator form of [`begin_with_storage`] that retains original writers in
+/// [`FileSyncBarrier`] values until their durability barriers complete.
+///
+/// # Errors
+/// Returns an error when an original-writer barrier fails or the install
+/// transaction cannot complete durably.
+#[cfg(feature = "simulator")]
+pub async fn begin_with_storage_and_barriers<S: DurableStorage>(
+    directory: &Path,
+    barriers: Vec<FileSyncBarrier>,
+    storage: &S,
+) -> io::Result<()> {
+    let synced_files =
+        
futures::future::try_join_all(barriers.into_iter().map(FileSyncBarrier::run))
+            .await?
+            .into_iter()
+            .collect();
+    begin_with_synced_files(directory, synced_files, storage).await
+}
+
+async fn begin_with_synced_files<S: DurableStorage>(
+    directory: &Path,
+    synced_files: BTreeSet<PathBuf>,

Review Comment:
   nit: the set is only lent to `link_tree`, so take `&BTreeSet<PathBuf>` here 
and in `begin` (line 73), like `PartitionPrepareJournal::checkpoint_files` does.



##########
core/partitions/src/iggy_partition.rs:
##########
@@ -1046,13 +1046,52 @@ where
                 });
                 return;
             }
-            barriers.push(CheckpointBarrier::already_synced(writer.path()));
+            // [`IggyIndexWriter::fsync`] used the original descriptor above.
+            // The marker tells [`PartitionPersistence::checkpoint_files`] not
+            // to replace that proof with a fresh-handle sync.
+            barriers.push(FileSyncBarrier::already_synced(writer.path()));
         }
         let (files, directories) = self.persistence_checkpoint_files(config);
         persistence.checkpoint_files(through_op, files, directories, barriers);
         self.start_persistence();
     }
 
+    /// Synchronize every live original writer while the caller holds
+    /// [`IggyPartition::write_lock`].
+    ///
+    /// Unlike [`IggyPartition::checkpoint_persistence`], this barrier runs 
even
+    /// when the checkpoint frontier cannot advance.
+    pub(crate) async fn barrier_install_files_locked(&self) -> 
std::io::Result<BTreeSet<PathBuf>> {

Review Comment:
   nit: nothing else in the rust code uses a `_locked` suffix - other functions 
state the lock precondition in their doc. drop the suffix, for example 
`sync_install_writers`.



##########
core/partitions/src/iggy_partition.rs:
##########
@@ -1046,13 +1046,52 @@ where
                 });
                 return;
             }
-            barriers.push(CheckpointBarrier::already_synced(writer.path()));
+            // [`IggyIndexWriter::fsync`] used the original descriptor above.
+            // The marker tells [`PartitionPersistence::checkpoint_files`] not
+            // to replace that proof with a fresh-handle sync.
+            barriers.push(FileSyncBarrier::already_synced(writer.path()));
         }
         let (files, directories) = self.persistence_checkpoint_files(config);
         persistence.checkpoint_files(through_op, files, directories, barriers);
         self.start_persistence();
     }
 
+    /// Synchronize every live original writer while the caller holds
+    /// [`IggyPartition::write_lock`].
+    ///
+    /// Unlike [`IggyPartition::checkpoint_persistence`], this barrier runs 
even
+    /// when the checkpoint frontier cannot advance.
+    pub(crate) async fn barrier_install_files_locked(&self) -> 
std::io::Result<BTreeSet<PathBuf>> {
+        let Some(persistence) = &self.persistence else {
+            return Ok(BTreeSet::new());
+        };
+        let mut barriers = Vec::with_capacity(2);
+        if let Some(writer) = 
self.log.messages_writers().last().and_then(Option::as_ref) {

Review Comment:
   simplification: `.map_err(std::io::Error::other)` keeps the same error text 
and drops the `to_string()` allocation, about 7 lines across both blocks. the 
index path still needs `writer.path().to_owned()`.



##########
core/partitions/src/install_backup.rs:
##########
@@ -65,17 +70,47 @@ pub async fn recover_with_storage<S: DurableStorage>(
 /// # Errors
 /// Returns an error if the rollback state cannot be made durable.
 /// After any failure the caller must stop serving until recovery.
-pub async fn begin(directory: &Path) -> io::Result<()> {
-    begin_with_storage(directory, &DiskStorage)
+pub async fn begin(directory: &Path, synced_files: BTreeSet<PathBuf>) -> 
io::Result<()> {
+    begin_with_synced_files(directory, synced_files, &DiskStorage)
         .await
         .note_descriptor_exhaustion(|| format!("starting an install in {}", 
directory.display()))
 }
 
+/// Storage-generic form of [`begin`] used by deterministic storage tests.
+///
 /// # Errors
 /// Returns an error when the install transaction cannot complete durably.
 pub async fn begin_with_storage<S: DurableStorage>(
     directory: &Path,
     storage: &S,
+) -> io::Result<()> {
+    begin_with_synced_files(directory, BTreeSet::new(), storage).await
+}
+
+/// Simulator form of [`begin_with_storage`] that retains original writers in
+/// [`FileSyncBarrier`] values until their durability barriers complete.
+///
+/// # Errors
+/// Returns an error when an original-writer barrier fails or the install
+/// transaction cannot complete durably.
+#[cfg(feature = "simulator")]
+pub async fn begin_with_storage_and_barriers<S: DurableStorage>(

Review Comment:
   simplification: this shim and its `simulator`-gated import exist for one 
test. once that test drives `barrier_files`, delete both - about 20 lines - and 
`FileSyncBarrier::run` can go back to private.



##########
core/partitions/src/install_backup.rs:
##########
@@ -65,17 +70,47 @@ pub async fn recover_with_storage<S: DurableStorage>(
 /// # Errors
 /// Returns an error if the rollback state cannot be made durable.
 /// After any failure the caller must stop serving until recovery.
-pub async fn begin(directory: &Path) -> io::Result<()> {
-    begin_with_storage(directory, &DiskStorage)
+pub async fn begin(directory: &Path, synced_files: BTreeSet<PathBuf>) -> 
io::Result<()> {

Review Comment:
   nit: the doc doesn't say what `synced_files` promises, and every listed path 
skips the fresh sync. spell it out: the original writer synced the path, the 
path still names that inode, and nothing writes it before `begin` returns.



##########
core/partitions/src/persistence.rs:
##########
@@ -903,6 +912,27 @@ impl<S: DurableStorage> PartitionPersistence<S> {
         });
     }
 
+    /// Queue a durability barrier through every retained original writer,
+    /// including offset files restored from the backup, without advancing

Review Comment:
   nit: files restored from the backup never get a retained writer, so this 
barrier doesn't cover them. drop that phrase and say the returned paths only 
count as synced once the queued barrier succeeds.



##########
core/partitions/src/install_backup.rs:
##########
@@ -119,6 +154,7 @@ async fn link_tree<S: DurableStorage>(
     source: &Path,
     target: &Path,
     skip_scratch: bool,
+    synced_files: &BTreeSet<std::path::PathBuf>,

Review Comment:
   nit: `PathBuf` is already imported at line 20.



##########
core/partitions/src/install_backup.rs:
##########
@@ -65,17 +70,47 @@ pub async fn recover_with_storage<S: DurableStorage>(
 /// # Errors
 /// Returns an error if the rollback state cannot be made durable.
 /// After any failure the caller must stop serving until recovery.
-pub async fn begin(directory: &Path) -> io::Result<()> {
-    begin_with_storage(directory, &DiskStorage)
+pub async fn begin(directory: &Path, synced_files: BTreeSet<PathBuf>) -> 
io::Result<()> {
+    begin_with_synced_files(directory, synced_files, &DiskStorage)
         .await
         .note_descriptor_exhaustion(|| format!("starting an install in {}", 
directory.display()))
 }
 
+/// Storage-generic form of [`begin`] used by deterministic storage tests.

Review Comment:
   nit: this isn't the storage-generic form of `begin` - it passes an empty set 
and fresh-syncs every link. say that instead.



##########
core/partitions/src/state_transfer.rs:
##########
@@ -2743,15 +2743,28 @@ where
 
         let write_lock = self.write_lock.clone();
         let _guard = write_lock.lock().await;
-        if let Some(persistence) = &self.persistence {
-            self.start_persistence();
-            persistence.drain_with_timeout().await.map_err(|source| {
-                PartitionInstallError::SwapIo {
-                    path: partition_dir.clone(),
-                    source,
+        if self.persistence.is_some() {
+            // [`IggyPartition::barrier_install_files_locked`] covers original
+            // writers even when no [`crate::PartitionPersistence`] checkpoint 
can advance.
+            let synced_files = match self.barrier_install_files_locked().await 
{
+                Ok(synced_files) => synced_files,
+                Err(source) => {
+                    if self
+                        .persistence
+                        .as_ref()
+                        .is_some_and(|persistence| 
persistence.failure().is_some())
+                    {
+                        self.fence_install_failure(commit_op);
+                    }
+                    return Err(PartitionInstallError::SwapIo {

Review Comment:
   nit: the `SwapIo` doc promises a converged, post-swap partition, but this 
returns it before the swap. widen the doc to cover pre-swap failures.



##########
core/partitions/src/install_backup.rs:
##########
@@ -143,12 +179,15 @@ async fn link_tree<S: DurableStorage>(
             } else {
                 // Transfer unlinks or atomically replaces these frozen files.
                 // Hard links retain the old bytes without copying segment 
data.
-                storage.hard_link(&source.join(&name), &destination).await?;
-                storage
-                    .open(&destination, OpenMode::Read)
-                    .await?
-                    .sync()
-                    .await?;
+                let source_file = source.join(&name);
+                storage.hard_link(&source_file, &destination).await?;
+                if !synced_files.contains(&source_file) {
+                    storage
+                        .open(&destination, OpenMode::Read)
+                        .await?
+                        .sync()

Review Comment:
   warning: not new, but this opens and syncs every file one by one under the 
write lock, which stalls the whole shard pump on partitions with thousands of 
offset files. collect them and sync through `try_for_each_concurrent(16, ...)` 
like `checkpoint_files` does.



##########
core/partitions/src/iggy_partition.rs:
##########
@@ -1046,13 +1046,52 @@ where
                 });
                 return;
             }
-            barriers.push(CheckpointBarrier::already_synced(writer.path()));
+            // [`IggyIndexWriter::fsync`] used the original descriptor above.
+            // The marker tells [`PartitionPersistence::checkpoint_files`] not
+            // to replace that proof with a fresh-handle sync.
+            barriers.push(FileSyncBarrier::already_synced(writer.path()));
         }
         let (files, directories) = self.persistence_checkpoint_files(config);
         persistence.checkpoint_files(through_op, files, directories, barriers);
         self.start_persistence();
     }
 
+    /// Synchronize every live original writer while the caller holds
+    /// [`IggyPartition::write_lock`].
+    ///
+    /// Unlike [`IggyPartition::checkpoint_persistence`], this barrier runs 
even
+    /// when the checkpoint frontier cannot advance.
+    pub(crate) async fn barrier_install_files_locked(&self) -> 
std::io::Result<BTreeSet<PathBuf>> {
+        let Some(persistence) = &self.persistence else {
+            return Ok(BTreeSet::new());
+        };
+        let mut barriers = Vec::with_capacity(2);
+        if let Some(writer) = 
self.log.messages_writers().last().and_then(Option::as_ref) {
+            let path = writer.path();
+            let writer = Rc::clone(writer);
+            barriers.push(FileSyncBarrier::from_future(path, async move {
+                writer
+                    .fsync()
+                    .await
+                    .map_err(|error| std::io::Error::other(error.to_string()))

Review Comment:
   nit: both writers' `fsync` drop the `io::Error`, so this can only ever 
report `Cannot write to file` without the errno. log it in `fsync` before 
mapping, like `write_frozen_chunked` does.



##########
core/partitions/src/install_backup.rs:
##########
@@ -143,12 +179,15 @@ async fn link_tree<S: DurableStorage>(
             } else {
                 // Transfer unlinks or atomically replaces these frozen files.
                 // Hard links retain the old bytes without copying segment 
data.
-                storage.hard_link(&source.join(&name), &destination).await?;
-                storage
-                    .open(&destination, OpenMode::Read)
-                    .await?
-                    .sync()
-                    .await?;
+                let source_file = source.join(&name);
+                storage.hard_link(&source_file, &destination).await?;
+                if !synced_files.contains(&source_file) {

Review Comment:
   simplification: the barrier finishes before `begin`, so this fresh sync can 
only add an error, never hide one - skipping it saves at most 65 syncs on a 
cold path. dropping `synced_files` cuts about 25-30 lines, your call since it 
undoes the earlier review ask.



##########
core/partitions/src/install_backup.rs:
##########
@@ -65,17 +70,47 @@ pub async fn recover_with_storage<S: DurableStorage>(
 /// # Errors
 /// Returns an error if the rollback state cannot be made durable.
 /// After any failure the caller must stop serving until recovery.
-pub async fn begin(directory: &Path) -> io::Result<()> {
-    begin_with_storage(directory, &DiskStorage)
+pub async fn begin(directory: &Path, synced_files: BTreeSet<PathBuf>) -> 
io::Result<()> {
+    begin_with_synced_files(directory, synced_files, &DiskStorage)
         .await
         .note_descriptor_exhaustion(|| format!("starting an install in {}", 
directory.display()))
 }
 
+/// Storage-generic form of [`begin`] used by deterministic storage tests.
+///
 /// # Errors
 /// Returns an error when the install transaction cannot complete durably.
 pub async fn begin_with_storage<S: DurableStorage>(

Review Comment:
   simplification: if you keep the skip, fold `begin_with_synced_files` into 
this and have it take `synced_files`, so `begin` wraps one function like 
`recover` and `finish` do. that saves about 15 lines and replaces the four 
empty-set comments with one doc line.



##########
core/partitions/src/persistence.rs:
##########
@@ -903,6 +912,27 @@ impl<S: DurableStorage> PartitionPersistence<S> {
         });
     }
 
+    /// Queue a durability barrier through every retained original writer,
+    /// including offset files restored from the backup, without advancing
+    /// [`PartitionPersistence::checkpoint_op`].
+    pub(crate) fn barrier_files(&self, barriers: Vec<FileSyncBarrier>) -> 
BTreeSet<PathBuf> {
+        let synced_files = self

Review Comment:
   simplification: this repeats the snapshot-then-drain from `checkpoint_files` 
(lines 896-903). a shared private helper keeps that order in one place, though 
it only saves a couple of lines.



##########
core/partitions/src/iggy_partition.rs:
##########
@@ -1046,13 +1046,52 @@ where
                 });
                 return;
             }
-            barriers.push(CheckpointBarrier::already_synced(writer.path()));
+            // [`IggyIndexWriter::fsync`] used the original descriptor above.
+            // The marker tells [`PartitionPersistence::checkpoint_files`] not
+            // to replace that proof with a fresh-handle sync.
+            barriers.push(FileSyncBarrier::already_synced(writer.path()));
         }
         let (files, directories) = self.persistence_checkpoint_files(config);
         persistence.checkpoint_files(through_op, files, directories, barriers);
         self.start_persistence();
     }
 
+    /// Synchronize every live original writer while the caller holds
+    /// [`IggyPartition::write_lock`].
+    ///
+    /// Unlike [`IggyPartition::checkpoint_persistence`], this barrier runs 
even
+    /// when the checkpoint frontier cannot advance.
+    pub(crate) async fn barrier_install_files_locked(&self) -> 
std::io::Result<BTreeSet<PathBuf>> {
+        let Some(persistence) = &self.persistence else {

Review Comment:
   simplification: the only caller already checks `self.persistence.is_some()`, 
so this `else` arm is dead, and the caller re-reads the same `Option` at 
state_transfer.rs:2752. bind it once there and pass the persistence in.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to