2-Phase Commit for Results

For my current project, I found myself wanting to atomically¹ apply several fallible operations. There didn't seem to be a good abstraction for that (at least in std and a cursory search of crates.io), and so I put one together.

  • Is there an existing solution that I've missed?
  • Have I built any accidental footguns into the API?
  • Is there interest in having this as its own crate, or should I keep it internal to my project?

¹ In the sense that they all succeed or all do nothing, not in the std::sync::atomic sense.

Edit: I've decided that depending on Deref is a mistake; it's better to not require any preview of the committed value-- individual implementations can do what they want. (No updated code yet, though).
Edit 2: The code is now updated to reflect this.

(Playground) Example usage towards the bottom.

/// Represents the result of an operation that can be undone.
///
/// By the time this is created, the operation *must* be complete:
/// Dropping or forgetting this value should leave the original object
/// in a committed state.
trait Revertable {
    type Output: Sized;
    
    /// Confirm that no revert will be necessary.
    fn commit(x: Self)->Self::Output;
    
    /// Cancel this operation.
    ///
    /// The original object will be returned to an "equivalent" state as
    /// before the operation was requested.
    fn revert(x: Self);
}

/// A changeset that can be committed or reverted as a unit 
trait Changeset {
    type Success: Sized;
    type Failure: Sized;
    
    /// Attempt to apply this changeset
    ///
    /// If all the changes succeeded, applies them and returns `Ok(_)`.
    /// Otherwise, reverts those that succeeded and returns `Err(_)`
    fn commit(self)->Result<Self::Success, Self::Failure>;
    
    /// Abandon these changes
    ///
    /// Reverts all successful intermediate results.
    fn revert(self)->Self::Failure;
    
    /// Will a call to `commit` succeed?
    ///
    /// Can cause panics if the implementation doesn't agree with `commit`.
    fn will_commit(&self)->bool;
}

#[derive(Copy,Clone,Debug,Eq,PartialEq,Hash)]
enum CommitErr<E> {
    Reverted,
    Err(E)
}

impl<E> From<E> for CommitErr<E> {
    fn from(e:E)->Self { CommitErr::Err(e) }
}

impl<T,E> Changeset for Result<T,E>
where T:Revertable {
    type Success = T::Output;
    type Failure = CommitErr<E>;
    
    fn commit(self)->Result<Self::Success, Self::Failure> {
        Ok(Revertable::commit(self?))
    }
    
    fn revert(self)->Self::Failure {
        match self {
            Ok(val) => { Revertable::revert(val); CommitErr::Reverted },
            Err(e) => e.into()
        }
    }
    
    fn will_commit(&self)->bool {
        self.is_ok()
    }
}

impl<T:Revertable> Changeset for T {
    type Success = T::Output;
    type Failure = CommitErr<std::convert::Infallible>;
    
    fn commit(self)->Result<Self::Success, Self::Failure> {
        Ok(Revertable::commit(self))
    }
    
    fn revert(self)->Self::Failure {
        Revertable::revert(self);
        CommitErr::Reverted
    }
    
    fn will_commit(&self)->bool {
        true
    }
}

macro_rules! two_phase_tuple {
    ($($metavar:tt)+) => {
        impl<$($metavar,)+> Changeset for ($($metavar,)+)
        where $( $metavar: Changeset, )+ {
            type Success = ($( $metavar::Success, )+);
            type Failure = ($( $metavar::Failure, )+);
            
            fn commit(self)->Result<Self::Success, Self::Failure> {
                if self.will_commit() {
                    #[allow(non_snake_case)]
                    let ( $($metavar,)+ ) = self;
                    Ok(($(
                        $metavar.commit().unwrap_or_else(|_|
                            panic!("{}: Bad will_commit implementation",
                                    std::any::type_name::<$metavar>())
                        ),
                    )+))
                } else {
                    Err(self.revert())
                }
            }
            
            fn revert(self)->Self::Failure {
                #[allow(non_snake_case)]
                let ( $($metavar,)+ ) = self;
                ($(
                    $metavar.revert(),
                )+)
            }
            
            fn will_commit(&self)->bool {
                #[allow(non_snake_case)]
                let ( ref $($metavar,)+ ) = self;
                true $(
                    && $metavar.will_commit()
                )+
            }
        }
    }
}

two_phase_tuple!{A}
two_phase_tuple!{A B}
two_phase_tuple!{A B C}
two_phase_tuple!{A B C D}
two_phase_tuple!{A B C D E}
two_phase_tuple!{A B C D E F}
two_phase_tuple!{A B C D E F G}
two_phase_tuple!{A B C D E F G H}
two_phase_tuple!{A B C D E F G H I}
two_phase_tuple!{A B C D E F G H I J}
two_phase_tuple!{A B C D E F G H I J K}
two_phase_tuple!{A B C D E F G H I J K L}
two_phase_tuple!{A B C D E F G H I J K L M}

// ======================
// Example Implementation
// ======================

trait VecExt<T> {
    fn two_phase_push(&mut self, val:T)->TwoPhasePush<'_,T>;
    fn two_phase_pop(&mut self)->Result<TwoPhasePop<'_,T>, EmptyVec>;
}

impl<T> VecExt<T> for Vec<T> {
    fn two_phase_push(&mut self, x:T)->TwoPhasePush<'_,T> {
        self.push(x);
        TwoPhasePush(self)
    }
    
    fn two_phase_pop(&mut self)->Result<TwoPhasePop<'_,T>, EmptyVec> {
        match self.pop() {
            Some(x) => Ok(TwoPhasePop(self, x)),
            None => Err(EmptyVec)
        }
    }
}

struct TwoPhasePush<'a, T>(&'a mut Vec<T>);

impl<'a,T> Revertable for TwoPhasePush<'a,T> {
    type Output = ();
    fn commit(_: Self)->() { () }
    fn revert(x: Self) {
        x.0.pop().unwrap();
    }
}

#[derive(Copy,Clone,Debug,Eq,PartialEq,Hash)]
struct EmptyVec;
struct TwoPhasePop<'a, T>(&'a mut Vec<T>, T);

impl<'a,T> Revertable for TwoPhasePop<'a,T> {
    type Output = T;
    fn commit(x: Self)->T { x.1 }
    fn revert(x: Self) { x.0.push(x.1) }
}

#[test]
fn test_vec() {
    let mut a:Vec<usize> = vec![];
    let mut b:Vec<usize> = vec![42];
    let mut c:Vec<usize> = vec![];
    
    assert_eq!(
        (a.two_phase_push(3), b.two_phase_pop(), c.two_phase_pop()).commit(),
        Err((CommitErr::Reverted, CommitErr::Reverted, CommitErr::Err(EmptyVec)))
    );
    
    assert_eq!(a, vec![]);
    assert_eq!(b, vec![42]);
    assert_eq!(c, vec![]);
    
    assert_eq!(
        (a.two_phase_push(3), b.two_phase_pop(), c.two_phase_push(7)).commit(),
        Ok(( (), 42, () ))
    );
    
    assert_eq!(a, vec![3]);
    assert_eq!(b, vec![]);
    assert_eq!(c, vec![7]);
}

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.