Skip to main content

Atomic

Trait Atomic 

Source
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) -> …     // both

The 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§

Source

fn atomic<T, E, F>(&self, f: F) -> impl Future<Output = Result<T, E>>
where F: AsyncFnOnce(&Transaction) -> Result<T, E>, E: From<ExecError>,

Run f as one all-or-nothing block, nesting if the receiver is already a transaction.

Dyn Compatibility§

This trait is not dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety".

Implementors§

Source§

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.

Source§

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.

Source§

impl<B: Begin + ?Sized> Atomic for B

Anything a transaction can be begun on: atomic is BeginExt::within.