Rollback pattern

Say you have a program that does this:

fn install_service() -> Result<(), Error> {
  let service_name = "myservice";
  register_event_souce(service_name)?;
  register_service(service_name)?;
  add_reg_param(service_name, "dbfile", "c:\\ProgramData\\foo\\bar\\db.sqlite")?;
}

All but the first line in the function body make persistent system configuration changes that must be explicitly undone. If I want to make this function only succeed if every system change is successful, and roll back each change that has been done if one of them fails -- are there any crates to help with this? The project I'm working on now tend to do this a lot, and the chains can be pretty long.

Historically I've done variations of things like:

  • Let the caller (of, in this instance, install_service()) be responsible for calling the uninstall_service() function on error and make it assumes that partial installations are a thing. (Which can be really annoying in some cases).
  • A bunch of nested match or if/else.
  • Use a ordinal enum to keep track of how far initialization has progressed, and call a cleanup function that uses the enum to uninitialize in the reverse order of the initialization. This has proven to be somewhat error prone and finicky to maintain if new steps are added.

It occurred to me that it should be possible to express this using something along the line of:

let mut chain = CleanupChain::new();
chain.push(|| { /* do initialization */ }, || { /* cleanup */ });

// If successful, none of the cleanups are called.
// If any error occurs, the cleanups are called in reverse order.
chain.run()

It also occurred to me that if I keep running into this pattern, I'm 100% sure others have as well, and that there's probably some ergonomic Rust crate for it already.

Are there any crates that facilitate rollbacks of persistent changes if there are errors along the way? I have a feeling there's a terminology for the thing I'm looking for, but I don't know it. "transaction-based error handling"?

After each operation, you can create a drop guard that will roll back the changes when the function returns or panics. By adding a completion flag that each guard checks, you can prevent the rollback in the case that the whole sequence is successful.

2 Likes