Is there a good way to roll back partial io (or resource allocation) operations?


#1

Hi
I am writing an application with many io operations which might fail. I do e.g. create files, open them for writing, allocate buffers, start child processes etc. If the application fails at some point I don’t want to panic!() but I want to exit cleanly. Right now I am creating all single steps by using match clauses on io::Results and functions. But I have to create a execution path for every single possible point of failure and some way to go back (kill child process, delete files, …). Are there some common idioms to do this?
Right now my code looks like this (only parts of the code):

pub fn backup(&mut self) -> Result<()> {
    println!("[INFO] Found disk {} (UUID: {}), unlocking…",
    self.target_disk.backup_partition_name,
    self.target_disk.luks_partition_uuid);

    match mount_utils::mount_disk(&self.target_disk) {
        Ok(_) => {
            let retval: Result<()>;
             if fs_utils::does_file_or_directory_exist(&fs_utils::get_full_disk_path(self.target_disk.backup_partition_name)) {
                retval = self.do_backup();
                if retval.is_ok() { self.wait_for_user_feedback(); }
            } else {
                retval = Err(Error::new(ErrorKind::NotFound, "The backup disk does not have the expected layout. It looks like something went wrong mounting the backup disk"));
             }
             let ret_unmount = mount_utils::unmount_disk(&self.target_disk);
             match retval {
                Ok(_) => ret_unmount,
                Err(e) => Err(e),
            }
        },
        Err(e) => Err(e),
    }
}

or

fn spawn_children(&mut self) -> Result<(Child, Child)>{
    let mut checksum_cmd = match self.spawn_checksum_cmd() {
        Ok(x) => x,
        Err(e) => return Err(e),
    };
    let tar_cmd = match self.spawn_tar_cmd() {
        Ok(x) => x,
        Err(e) => {
            checksum_cmd.kill();
            return Err(e);
        },
    };
    Ok((checksum_cmd, tar_cmd))
}

At least the second piece of code is considered bad by some people because of the returns inside of the function. Any ideas?

To be more specific: I don’t search for a specific problem solution but for some idioms used to handle these kind of problems.


#2

RAII.

For example, if you need to create a bunch of files and only want to keep them if you finish generating them, you can use the tempdir crate to create a temporary directory that will be deleted on drop unless into_path() is called. When you’re done creating files in it, just call my_temp_dir.into_path() and then rename the directory (which might fail so you’ll still need a little cleanup unless you can convince the create maintainer to add a into_permanent_at(path) method.

For mounting, I’d define a custom temporary mount type that unmounts on drop. On linux, you can also transition into a new mount namespace but that might take a little work.


#3

Problem is I will want to store several tens of gigabytes in those files. Way too much for /tmp.
I currently do mount using udisksctl, I don’t think temporary mounts are useful.

How does RAII apply here? Every time I aquire a resource I check that it is successfully initialized. Is that all of it?

Is there some kind of “destructor” I could use to clean up? Or something like java’s “finally” block?


#4

That’s what RAII is: cleanup on drop (i.e. a destructor). And you can use tempdir to create temporary directories in directories other than tmp (TempDir::new_in("/my/directory/")).


#5

Ok, I will try this. Thanks!


#6

As a feedback to you and for other people having similar questions:
From reading the Wikipedia article I didn’t really understand what RAII means, the name is quite misleading. This article helped me understanding that basically a drop is a destructor (or finalizer as it is called in Java). And drop actually refers to the trait std::ops::Drop.

So when using resources outside of rust (e.g. files, processes, …) try to use an implementation with Drop so everything will be cleaned up when you are done. Otherwise you have to do this the old-fashioned way as you are used to it in C.

Furthermore I found this macro

macro_rules! unwrap_or_return {
    ( $result:expr ) => {
        match $result {
            Ok(x) => x,
            Err(e) => return Err(e),
        }
    };
}

quite useful. With that I write code like this:

fn step3_create_files(&mut self) -> Result<()> {

    // create on disk because of space requirements
    let tmp_backup_dir = unwrap_or_return!(
        TempDir::new_in(
            fs_utils::get_full_backup_folder_path(
                self.target_disk
            ), "backup-rust"
        )
    );
    let backup_file = unwrap_or_return!(
        File::create(tmp_backup_dir.path().join("backup.tar.lzo"))
    );

    // create in temporary folder to avoid I/O bottleneck on hdd
    let tmp_dir = unwrap_or_return!(TempDir::new("backup-rust"));
    let tar_log_file = unwrap_or_return!(
        File::create(tmp_dir.path().join("tar.log"))
    );
    let hash_file = unwrap_or_return!(
        File::create(tmp_dir.path().join("SHA256SUMS"))
    );
    unwrap_or_return!(
        self.step4_spawn_children(
            &backup_file, &tar_log_file, &hash_file
        )
    );

    //if // success
    //    tmpdir.into_path()
    // cleanup on success: move files to some useful destination
}

#7

That’s try! without the error conversion, FYI.


Thoughts on resource management and program structure