public inbox for pdm-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Dominik Csapak <d.csapak@proxmox.com>
To: Lukas Wagner <l.wagner@proxmox.com>, pdm-devel@lists.proxmox.com
Subject: Re: [PATCH datacenter-manager 10/15] task cache: introduce ArchiveFileWriter
Date: Fri, 10 Jul 2026 13:36:01 +0200	[thread overview]
Message-ID: <06d0190e-34d7-4449-92e8-6bc2c379e3ce@proxmox.com> (raw)
In-Reply-To: <20260702092258.174740-11-l.wagner@proxmox.com>

comments inline

On 7/2/26 11:23 AM, Lukas Wagner wrote:
> This is nicely abstracts handling of compressed and uncompressed archive
> files behind a common interface, as well as atomically replacing any
> existing archive file by using the usual tmpfile + rename approach.
> 
> The 'active' file has the same file structure as regular archive files,
> so we can use ArchiveFileWriter for these as well.
> 
> Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
> ---
>   server/src/remote_tasks/task_cache.rs | 251 +++++++++++++++++---------
>   1 file changed, 163 insertions(+), 88 deletions(-)
> 
> diff --git a/server/src/remote_tasks/task_cache.rs b/server/src/remote_tasks/task_cache.rs
> index c7057786..969a8124 100644
> --- a/server/src/remote_tasks/task_cache.rs
> +++ b/server/src/remote_tasks/task_cache.rs
> @@ -12,6 +12,7 @@ use std::{
>   
>   use anyhow::{Context, Error};
>   use serde::{Deserialize, Serialize};
> +use zstd::Encoder;
>   
>   use proxmox_sys::fs::CreateOptions;
>   
> @@ -665,36 +666,13 @@ impl<'a> WritableTaskCache<'a> {
>       /// The tasks are first written to a temporary file, which is then used
>       /// to atomically replace the original.
>       fn write_active_tasks(&self, tasks: impl Iterator<Item = TaskCacheItem>) -> Result<(), Error> {
> -        let target = self.cache.active_path();
> +        let file = self.cache.active_file();
> +        let mut writer = file.writer(self.cache.create_options)?;
>   
> -        let (fd, path) = proxmox_sys::fs::make_tmp_file(target, self.cache.create_options)?;
> -        let mut fd = BufWriter::new(fd);
> -
> -        Self::write_tasks(&mut fd, tasks)?;
> -
> -        if let Err(err) = fd.flush() {
> -            log::error!("could not flush 'active' file: {err:#}");
> -        }
> -        drop(fd);
> -
> -        let res = std::fs::rename(&path, target).with_context(|| {
> -            format!(
> -                "failed to replace {} with {}",
> -                target.display(),
> -                path.display(),
> -            )
> -        });
> -
> -        if let Err(err) = res {
> -            if let Err(err) = std::fs::remove_file(&path) {
> -                log::error!(
> -                    "failed to cleanup temporary file {}: {err:#}",
> -                    path.display()
> -                );
> -            }
> -
> -            return Err(err);
> -        }
> +        writer.write_tasks(tasks)?;
> +        writer
> +            .finalize_and_replace()
> +            .context("could not finalize 'active' file")?;
>   
>           Ok(())
>       }
> @@ -712,17 +690,7 @@ impl<'a> WritableTaskCache<'a> {
>               return Ok(());
>           }
>   
> -        // TODO: Might be nice to also move this to ArchiveFile
> -        let (temp_file, temp_file_path) =
> -            proxmox_sys::fs::make_tmp_file(&file.path, self.cache.create_options)?;
> -        let mut writer = if file.compressed {
> -            let encoder =
> -                zstd::stream::write::Encoder::new(temp_file, zstd::DEFAULT_COMPRESSION_LEVEL)?
> -                    .auto_finish();
> -            Box::new(BufWriter::new(encoder)) as Box<dyn Write>
> -        } else {
> -            Box::new(BufWriter::new(temp_file)) as Box<dyn Write>
> -        };
> +        let mut writer = file.writer(self.cache.create_options)?;
>   
>           let archive_iter = file
>               .iter()?
> @@ -742,43 +710,8 @@ impl<'a> WritableTaskCache<'a> {
>               .peekable();
>           let task_iter = tasks.into_iter().peekable();
>   
> -        Self::write_tasks(&mut writer, MergeTaskIterator::new(archive_iter, task_iter))?;
> -
> -        if let Err(err) = writer.flush() {
> -            log::error!("could not flush BufWriter for {file:?}: {err:#}");
> -        }
> -        drop(writer);
> -
> -        if let Err(err) = std::fs::rename(&temp_file_path, &file.path).with_context(|| {
> -            format!(
> -                "failed to replace {} with {}",
> -                file.path.display(),
> -                temp_file_path.display()
> -            )
> -        }) {
> -            if let Err(err) = std::fs::remove_file(&temp_file_path) {
> -                log::error!(
> -                    "failed to clean up temporary file {}: {err:#}",
> -                    temp_file_path.display()
> -                );
> -            }
> -
> -            return Err(err);
> -        }
> -
> -        Ok(())
> -    }
> -
> -    /// Write an iterator of [`TaskCacheItem`] to a something that implements [`Write`].
> -    /// The individual items are encoded as JSON followed by a newline.
> -    fn write_tasks(
> -        writer: &mut impl Write,
> -        tasks: impl Iterator<Item = TaskCacheItem>,
> -    ) -> Result<(), Error> {
> -        for task in tasks {
> -            serde_json::to_writer(&mut *writer, &task)?;
> -            writeln!(writer)?;
> -        }
> +        writer.write_tasks(MergeTaskIterator::new(archive_iter, task_iter))?;
> +        writer.finalize_and_replace()?;
>   
>           Ok(())
>       }
> @@ -872,21 +805,12 @@ impl TaskCache {
>               GetTasks::All => {
>                   let mut archive_files = self.archive_files(lock)?;
>                   archive_files.reverse();
> -
> -                archive_files.push(ArchiveFile {
> -                    path: self.active_path().into(),
> -                    compressed: false,
> -                    starttime: 0,
> -                });
> +                archive_files.push(self.active_file());
>   
>                   TaskArchiveIterator::new(Some(journal_file.into()), archive_files, lock)
>               }
>               GetTasks::Active => {
> -                let archive_files = vec![ArchiveFile {
> -                    path: self.active_path().into(),
> -                    compressed: false,
> -                    starttime: 0,
> -                }];
> +                let archive_files = vec![self.active_file()];
>   
>                   TaskArchiveIterator::new(None, archive_files, lock)
>               }
> @@ -977,6 +901,15 @@ impl TaskCache {
>           self.base_path
>               .join(format!("{ARCHIVE_FILENAME_PREFIX}{starttime}{suffix}"))
>       }
> +
> +    /// Return the [`ArchiveFile`] instance for the `active` file.
> +    fn active_file(&self) -> ArchiveFile {
> +        ArchiveFile {
> +            path: self.active_path().into(),
> +            compressed: false,
> +            starttime: 0,
> +        }
> +    }
>   }
>   
>   /// Comparison function for sorting tasks.
> @@ -1174,6 +1107,28 @@ impl ArchiveFile {
>           Ok(Some(iter))
>       }
>   
> +    /// Create an [`ArchiveFileWriter`] for this archive file.
> +    fn writer(&self, create_options: CreateOptions) -> Result<ArchiveFileWriter<'_>, Error> {
> +        let (temp_file, temp_file_path) =
> +            proxmox_sys::fs::make_tmp_file(&self.path, create_options)?;
> +
> +        if self.compressed {
> +            let encoder =
> +                zstd::stream::write::Encoder::new(temp_file, zstd::DEFAULT_COMPRESSION_LEVEL)?;
> +            Ok(ArchiveFileWriter {
> +                archive_file: self,
> +                writer: Some(ArchiveFileWriterInner::ZstdEncoder(encoder)),
> +                path: temp_file_path,
> +            })
> +        } else {
> +            Ok(ArchiveFileWriter {
> +                archive_file: self,
> +                writer: Some(ArchiveFileWriterInner::Plain(BufWriter::new(temp_file))),
> +                path: temp_file_path,
> +            })
> +        }
> +    }
> +
>       fn compress(&mut self, options: CreateOptions) -> Result<(), Error> {
>           let uncompressed_file_path = &self.path;
>   
> @@ -1207,6 +1162,98 @@ impl ArchiveFile {
>       }
>   }
>   
> +/// Writer for an [`ArchiveFile`].
> +///
> +/// Instantiate this via [`ArchiveFile::writer`]. When calling [`Self::finalize_and_replace`], the
> +/// original archive file is replaced with the contents that were written to the writer.
> +struct ArchiveFileWriter<'a> {
> +    archive_file: &'a ArchiveFile,
> +    // writer: Option<Box<dyn Write>>,
> +    writer: Option<ArchiveFileWriterInner<'a>>,
> +    path: PathBuf,
> +}
> +
> +enum ArchiveFileWriterInner<'a> {
> +    ZstdEncoder(Encoder<'a, File>),
> +    Plain(BufWriter<File>),
> +}
> +
> +impl<'a> ArchiveFileWriter<'a> {
> +    /// Write the contents of an iterator of [`TaskCacheItem`] to this archive file.
> +    /// The individual items are encoded as JSON followed by a newline.
> +    fn write_tasks(&mut self, tasks: impl Iterator<Item = TaskCacheItem>) -> Result<(), Error> {
> +        if let Some(writer) = self.writer.as_mut() {
> +            let writer = match writer {
> +                ArchiveFileWriterInner::ZstdEncoder(encoder) => encoder as &mut dyn Write,
> +                ArchiveFileWriterInner::Plain(buf_writer) => buf_writer as &mut dyn Write,
> +            };
> +
> +            for task in tasks {
> +                serde_json::to_writer(&mut *writer, &task)?;
> +                writeln!(writer)?;
> +            }
> +        }
> +
> +        Ok(())
> +    }
> +
> +    /// Call [`ArchiveFileWriter::finalize_and_replace`] or just drop `self`, if you don't need to
> +    /// handle the error.
> +    fn finalize_impl(&mut self) -> Result<(), Error> {
> +        if let Some(writer) = self.writer.take() {
> +            let file = match writer {
> +                ArchiveFileWriterInner::ZstdEncoder(mut encoder) => {
> +                    encoder.flush()?;
> +                    encoder.finish()?
> +                }
> +                ArchiveFileWriterInner::Plain(mut buf_writer) => {
> +                    buf_writer.flush()?;
> +                    buf_writer.into_inner()?
> +                }
> +            };
> +
> +            file.sync_all()
> +                .with_context(|| format!("could not fsync {}", self.path.display()))?;
> +
> +            if let Err(err) =
> +                std::fs::rename(&self.path, &self.archive_file.path).with_context(|| {
> +                    format!(
> +                        "failed to replace {} with {}",
> +                        self.path.display(),
> +                        self.archive_file.path.display()
> +                    )
> +                })
> +            {
> +                if let Err(err) = std::fs::remove_file(&self.path) {
> +                    log::error!(
> +                        "failed to clean up temporary file {path}: {err:#}",
> +                        path = self.path.display()
> +                    );
> +                }
> +
> +                return Err(err);
> +            }
> +        }
> +
> +        Ok(())
> +    }
> +
> +    /// Finalize and drop the internal writer and replace the original
> +    /// archive file with the new contents. This method consumes `self`.
> +    ///
> +    /// [`ArchiveFileWriter::drop`] also finalizes the writer properly, but does not
> +    /// allow to catch errors.
> +    fn finalize_and_replace(mut self) -> Result<(), Error> {
> +        self.finalize_impl()
> +    }
> +}
> +
> +impl<'a> Drop for ArchiveFileWriter<'a> {
> +    fn drop(&mut self) {
> +        let _ = self.finalize_impl();
> +    }
> +}
> +
isn't this a bit dangerous?

this will be called whenever the writer is dropped regardless of error
conditions and will rotate/overwrite the files.

e.g. if any write in between fails, the drop handler will be called and 
overwrite an existing archive with the partially written one?

or am i missing something here?

>   /// Iterator that merges two _sorted_ `Iterator<Item = TaskCacheItem>`, returning the items
>   /// from both iterators sorted.
>   /// The two iterators are expected to be sorted descendingly based on the task's starttime and
> @@ -1715,4 +1762,32 @@ mod tests {
>   
>           assert_eq!(cache.get_tasks(GetTasks::Archived).unwrap().count(), 0);
>       }
> +
> +    #[test]
> +    fn temporary_file_when_writer_lives_is_not_enumerated() {
> +        let (_tmp_dir, mut cache) = make_cache().unwrap();
> +        cache.rotate_after = 100;
> +        cache.uncompressed_files = 1;
> +        cache.max_files = 2;
> +
> +        let cache = cache.write().unwrap();
> +        cache.init(1000).unwrap();
> +
> +        let files = cache.cache.archive_files(&cache.lock).unwrap();
> +
> +        assert_eq!(files.len(), 2);
> +
> +        let _writer = files
> +            .get(0)
> +            .unwrap()
> +            .writer(cache.cache.create_options)
> +            .unwrap();
> +
> +        let files = cache.cache.archive_files(&cache.lock).unwrap();
> +        assert_eq!(
> +            files.len(),
> +            2,
> +            "the tmp file created by the writer should not show up in the list of archive files"
> +        );
> +    }
>   }





  reply	other threads:[~2026-07-10 11:37 UTC|newest]

Thread overview: 30+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-02  9:22 [PATCH datacenter-manager/proxmox 00/15] task cache improvements (archive corruption handling, error handling) Lukas Wagner
2026-07-02  9:22 ` [PATCH proxmox 01/15] sys: fs: don't replace file extension make_tmp_file Lukas Wagner
2026-07-02  9:29   ` Lukas Wagner
2026-07-02  9:22 ` [PATCH datacenter-manager 02/15] task cache: fix missing cutoff state for PBS remotes Lukas Wagner
2026-07-02  9:22 ` [PATCH datacenter-manager 03/15] task cache: refresh task: don't apply journal if the archive was rotated Lukas Wagner
2026-07-02  9:22 ` [PATCH datacenter-manager 04/15] task cache: rotate: align timestamp for new files to UTC midnight Lukas Wagner
2026-07-10 11:36   ` Dominik Csapak
2026-07-10 12:02     ` Lukas Wagner
2026-07-02  9:22 ` [PATCH datacenter-manager 05/15] task cache: add test case for task cache rotation Lukas Wagner
2026-07-10 11:35   ` Dominik Csapak
2026-07-10 12:13     ` Lukas Wagner
2026-07-02  9:22 ` [PATCH datacenter-manager 06/15] task cache: pre-compute static paths during initialization Lukas Wagner
2026-07-02  9:22 ` [PATCH datacenter-manager 07/15] task cache: only initialize `TaskCache` struct once Lukas Wagner
2026-07-02  9:22 ` [PATCH datacenter-manager 08/15] task cache: archive iterator: don't yield more items if reading from file failed Lukas Wagner
2026-07-10 11:36   ` Dominik Csapak
2026-07-10 12:36     ` Lukas Wagner
2026-07-02  9:22 ` [PATCH datacenter-manager 09/15] task cache: include archive file path in error log messages Lukas Wagner
2026-07-10 11:36   ` Dominik Csapak
2026-07-10 12:53     ` Lukas Wagner
2026-07-02  9:22 ` [PATCH datacenter-manager 10/15] task cache: introduce ArchiveFileWriter Lukas Wagner
2026-07-10 11:36   ` Dominik Csapak [this message]
2026-07-02  9:22 ` [PATCH datacenter-manager 11/15] task cache: use ArchiveFileWriter when creating new archive files Lukas Wagner
2026-07-02  9:22 ` [PATCH datacenter-manager 12/15] task cache: trigger repair of corruption when applying journal Lukas Wagner
2026-07-02  9:22 ` [PATCH datacenter-manager 13/15] task cache: trigger repair of corruption when compressing archive files Lukas Wagner
2026-07-10 11:36   ` Dominik Csapak
2026-07-02  9:22 ` [PATCH datacenter-manager 14/15] task cache: trigger repair of corruption after read-accesses Lukas Wagner
2026-07-10 11:36   ` Dominik Csapak
2026-07-02  9:22 ` [PATCH datacenter-manager 15/15] task cache: handle potentially duplicated archive files after 'compress_archive_file' Lukas Wagner
2026-07-10 11:36   ` Dominik Csapak
2026-07-10 11:36 ` [PATCH datacenter-manager/proxmox 00/15] task cache improvements (archive corruption handling, error handling) Dominik Csapak

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=06d0190e-34d7-4449-92e8-6bc2c379e3ce@proxmox.com \
    --to=d.csapak@proxmox.com \
    --cc=l.wagner@proxmox.com \
    --cc=pdm-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal