High level transactions in Rust
On top of the durability controls and retry controls, the golem-rust crate (https://crates.io/crates/golem-rust (opens in a new tab)) also provides a high level library for defining transactions supporting compensation actions in case of getting reverted.
Although Golem's automatic retry policies and low level atomic regions provide a lot of power automatically, many times a set of external operations such as HTTP requests needs to be executed transactionally; if one of the operations fails, the whole transaction need to be rolled back by executing some compensation actions.
The golem-rust
crate provides support for two different types of transactions:
- fallible transactions are only dealing with domain errors
- infallible transactions must always succeed, and Golem applies its active retry policy to it
Fallible transactions
Many times external operations (such as HTTP calls to remote hosts) need to be executed transactionally. If some of the operations failed the transaction need to be rolled back - compensation actions need to undo whatever the already successfully performed operations did.
A fallible transaction only deals with domain errors. Within the transaction every operation that succeeds gets recorded. If an operation fails, all the recorded operations get compensated in reverse order before the transaction block returns with a failure.
A fallible transaction can be executed using the fallible_transaction
function, by passing a closure that can execute operations on the open transaction (see below).
Infallible transactions
An infallible transaction must always succeed - in case of a failure or interruption, it gets retried. If there is a domain error, the compensation actions are executed before the retry.
An infallible transaction can be executed using the infallible_transaction
function, by passing a closure that can execute operations on the open transaction (see below).
Operations
Both transaction types require the definition of operations.
It is defined with the following trait:
/// Represents an atomic operation of the transaction which has a rollback action.
///
/// Implement this trait and use it within a `transaction` block.
/// Operations can also be constructed from closures using `operation`.
pub trait Operation: Clone {
type In: Clone;
type Out: Clone;
type Err: Clone;
/// Executes the operation which may fail with a domain error
fn execute(&self, input: Self::In) -> Result<Self::Out, Self::Err>;
/// Executes a compensation action for the operation.
fn compensate(&self, input: Self::In, result: Self::Out) -> Result<(), Self::Err>;
}
There are multiple ways to define an operation:
-
Implement the trait manually
-
Use the
operation
function to create an operation from a pair of closures
pub fn operation<In: Clone, Out: Clone, Err: Clone>(
execute_fn: impl Fn(In) -> Result<Out, Err> + 'static,
compensate_fn: impl Fn(In, Out) -> Result<(), Err> + 'static,
) -> impl Operation<In = In, Out = Out, Err = Err>
- Use the
golem_operation
macro
The #[golem_operation(compensation=xyz)]
annotation can be applied to a function that takes any number of inputs, and returns a Result
. The compensation parameter must point to another function which can have one of the following forms:
- no parameter
- single parameter, getting the result of the successful operation
- multiple parameters where the first one is the result of the successful operation, and the rest of them are the inputs of the operation
When using this macro, it generates associated functions for the transaction so they can be directly called within the transaction in the following way:
#[golem_operation(compensation = compensation_step)]
fn transaction_step(step: u64) -> Result<bool, String> {
println!("Step {step}");
Ok(remote_call(step))
}
fn compensation_step(_: bool, step: u64) -> Result<(), String> {
println!("Compensating step {step}");
remote_call_undo(step);
Ok(())
}
fallible_transaction(|tx| {
tx.transaction_step(1)?;
tx.transaction_step(2)?;
Ok(11)
})?