Stm and commit — Building Transactions
The stm! macro produces Stm<A> values — descriptions of transactional computations. To execute them, you use commit or atomically.
commit: Lift Stm into Effect
#![allow(unused)] fn main() { use id_effect::{commit, Stm, Effect}; let transaction: Stm<i32> = stm! { let a = ~ ref_a.read_stm(); let b = ~ ref_b.read_stm(); a + b }; // Lift into Effect let effect: Effect<i32, Never, ()> = commit(transaction); // Now run it let result = run_blocking(effect)?; }
commit wraps a Stm in an effect that, when run, executes the transaction and retries if there's a conflict. The E type of commit(stm) is Never unless the Stm can fail (see stm::fail).
atomically: Direct Execution
#![allow(unused)] fn main() { use id_effect::atomically; // Run a transaction immediately in the current context let value: i32 = atomically(stm! { ~ counter.modify_stm(|n| n + 1); ~ counter.read_stm() }); }
atomically is the synchronous equivalent of commit + run_blocking. Use it when you're already outside the effect system and need a quick transactional update.
stm::fail: Transactional Errors
Transactions can fail with typed errors:
#![allow(unused)] fn main() { use id_effect::stm; fn withdraw(account: &TRef<u64>, amount: u64) -> Stm<u64> { stm! { let balance = ~ account.read_stm(); if balance < amount { ~ stm::fail(InsufficientFunds); // abort the transaction } ~ account.write_stm(balance - amount); balance - amount } } // commit propagates the error into E let effect: Effect<u64, InsufficientFunds, ()> = commit(withdraw(&account, 100)); }
stm::fail(e) aborts the current transaction with error e. The transaction is not retried — it fails immediately with the given error.
stm::retry: Block Until Condition
Sometimes a transaction should wait until a condition is true rather than failing:
#![allow(unused)] fn main() { // Block (retry) until the queue has items fn dequeue(queue: &TRef<Vec<Item>>) -> Stm<Item> { stm! { let items = ~ queue.read_stm(); if items.is_empty() { ~ stm::retry(); // block until queue changes, then retry } let item = items[0].clone(); ~ queue.write_stm(items[1..].to_vec()); item } } }
stm::retry() doesn't mean "try again immediately." It means "block until any TRef I read has changed, then try again." This is how TQueue implements blocking dequeue without busy-waiting.
Composing Transactions
Transactions compose by sequencing stm! blocks:
#![allow(unused)] fn main() { let big_transaction: Stm<()> = stm! { // Sub-transaction 1 let _ = ~ transfer_funds(&from, &to, amount); // Sub-transaction 2 let _ = ~ record_audit_log(&from, &to, amount); () }; // Both operations commit atomically or neither does let effect = commit(big_transaction); }
The composed transaction retries as a unit — if either sub-operation sees a conflict, the whole thing restarts from the beginning.