pub trait Atomic: Executor {
// Required method
fn atomic<T, E, F>(&self, f: F) -> impl Future<Output = Result<T, E>>
where F: AsyncFnOnce(&Transaction) -> Result<T, E>,
E: From<ExecError>;
}Expand description
All-or-nothing here, wherever “here” turns out to be: a transaction when nothing is open, a savepoint when a transaction already is.
This is the trait a reusable unit of work is written against. Without it a helper has to pick, and both choices are wrong somewhere:
async fn transfer(db: &dyn Executor) -> … // composes, but cannot be atomic
async fn transfer(tx: &Transaction) -> … // atomic, but demands its caller open one
async fn transfer(db: impl Atomic) -> … // bothThe third accepts a pool, a connection, a &dyn Begin and a
Transaction, and is atomic in all of them:
async fn transfer(db: impl Atomic, from: i64, to: i64) -> Result<(), ExecError> {
db.atomic(async |tx| {
debit(tx, from).await?;
credit(tx, to).await
})
.await
}
transfer(&pool, a, b).await?; // BEGIN … COMMIT
pool.within(async |tx| { // BEGIN …
transfer(tx, a, b).await?; // SAVEPOINT … RELEASE
audit(tx).await // … COMMIT
})
.await?;§What “atomic” promises, and what it does not
The block is all-or-nothing. What an Err discards is not:
- at the top, the block is the transaction, so an error rolls back everything;
- nested, an error rolls back to the savepoint and the caller’s transaction survives — the caller decides whether its own work still makes sense.
That is the useful reading of a nested unit of work, and it is why this is not merely a shorthand for “open a transaction if you can”.
A retry loop belongs at the transaction boundary, not here. Rolling
back to a savepoint does recover a transaction from an error, but a
serialization failure (TxConflict) will recur against the same
snapshot; only re-running the whole transaction can win. Retry where the
transaction begins.
§How to spell the parameter
db: impl Atomic, by value and with no bound beyond the trait. Every
receiver you would want is accepted, because Atomic follows Executor
in being implemented for handles as well as values:
f(&pool) f(pool) f(Arc::clone(&pool)) f(&dyn Begin) f(tx)The last is the &Transaction a scope closure hands you, which is what
makes these functions nest into each other. Writing &impl Atomic instead
would reject &dyn Begin (a trait object is not Sized), so the bare
form is the wider one as well as the shorter one.
§The cost, stated
The method is generic, so Atomic is not object-safe: &dyn Atomic
does not exist and the erased currency stays &dyn Executor, which a
scope parameter is passed on as for everything that is not itself a
scope.
Begin is still deliberately not implemented by Transaction, so
begin/within and savepoint stay
distinguishable wherever the distinction matters. atomic is how a call
site says the other thing on purpose: I do not care which of the two this
is; I care that it is atomic.
There is no atomic_with. Isolation level and access mode are properties
of the outermost transaction and cannot be changed by a nested scope, so a
nested atomic_with could only ignore them — and keelson does not accept
options it would have to ignore. Ask for them where the transaction is
opened: BeginWith::begin_with or BeginWithExt::within_with.
Required Methods§
Dyn Compatibility§
This trait is not dyn compatible.
In older versions of Rust, dyn compatibility was called "object safety".
Implementors§
impl Atomic for &Transaction
A reference to a transaction — what makes the bare impl Atomic
parameter form work, since that is the type a scope closure hands you.
Executor is implemented for &E and Arc<E> for the same reason: a
caller should not have to know whether what it holds is the value or a
handle to it. Coherent with the blanket impl for the same reason
Transaction’s own impl is — &Transaction is not Begin either.
impl Atomic for Transaction
Inside a transaction: atomic is Transaction::savepoint.
Not an overlapping impl, and not by luck: Transaction does not
implement Begin, which is the design decision above holding the two
impls apart.
impl<B: Begin + ?Sized> Atomic for B
Anything a transaction can be begun on: atomic is BeginExt::within.