Transaction API
The closure API is the safest default. It commits when the closure returns Ok and rolls back on every error:
let post = client
.transaction(async |tx| {
let post = tx
.create_post()
.title("Atomic work")
.author_id(user.id)
.save()
.await?;
tx.create_comment()
.body("Created with the post")
.post_id(post.id)
.author_id(user.id)
.save()
.await?;
Ok(post)
})
.await?;There is no efficiency penalty compared with manually issuing BEGIN, the operations, and COMMIT; the helper is
that lifecycle with automatic rollback and typed error preservation.
Application errors
Use transaction_app(async |tx| ...), or transaction_app_with(options, ...), when the closure returns your own
error type; the result is a TransactionError<E>. Rent distinguishes database
setup/commit failures, the application failure, and the rare case where both the operation and rollback fail.
Explicit lifecycle
let tx = client.tx().await?;
tx.client()
.create_post()
.title("Manual transaction")
.author_id(user.id)
.save()
.await?;
tx.commit().await?;Call rollback() instead of commit() to discard the transaction. Every clone of a transaction client becomes
closed after either operation.
Isolation and read-only work
use rent::driver::{IsolationLevel, TransactionOptions};
let options = TransactionOptions::new()
.isolation(IsolationLevel::Serializable)
.read_only();
let tx = client.tx_with(options).await?;transaction_with(options, async |tx| ...) runs a closure under the same options. Support varies by database. PostgreSQL additionally supports deferrable read-only serializable transactions through
.deferrable().
Retry transient conflicts
let result = client
.transaction_retry(3, async |tx| {
tx.update_counter_one(&counter)
.value(counter.value + 1)
.save_one()
.await
})
.await?;The number is a total-attempt budget: 3 means one initial attempt and at most two retries. For compatibility, zero
is normalized to one attempt. Only serialization failures, deadlocks, and lock-contention errors are retried. Rent
classifies failures from transaction setup, the operation, and commit; every retry opens a fresh transaction. A
rollback failure is never retried because Rent can no longer prove that the failed attempt ended cleanly.
The closure may run more than once, so keep external side effects outside it or make them idempotent.
TransactionOptions and retry policy are deliberately separate. The former controls database semantics such as
isolation and read-only mode. Use transaction_retry_with to pair those settings with the default retry policy, or
configure both explicitly:
use std::time::Duration;
use rent::driver::{IsolationLevel, TransactionOptions};
use rent::runtime::{RetryJitter, TransactionRetryPolicy};
let transaction_options = TransactionOptions::new()
.isolation(IsolationLevel::Serializable);
let retry_policy = TransactionRetryPolicy::new(5)
.initial_delay(Duration::from_millis(25))
.max_delay(Duration::from_millis(500))
.backoff_factor(2)
.jitter(RetryJitter::Full);
let result = client
.transaction_retry_with_policy(
transaction_options,
retry_policy,
async |tx| update_inventory(tx).await,
)
.await?;The convenience methods use a 10 ms initial delay, a factor of two, a one-second ceiling, and full jitter
(RetryJitter::None disables it). Those are
defaults rather than generated constants; TransactionRetryPolicy can replace each setting.
Savepoints provide partial rollback inside a transaction; see the savepoint tutorial.