Перейти к основному содержимому
Эта страница ещё не переведена

Ниже показан английский оригинал.

Context

A handler reads the inbox's context through Ctx<keys::..>: the transaction, the pool and the attempt, in that order, leaving out what it does not need.

The keys module holds three keys, each read as a handler parameter:

KeyValueWhat it is
keys::Tx<DB>Tx<DB>the delivery's transaction, in transactional mode
keys::Pool<DB>Pool<DB>the service's pool, whose writes commit on their own
keys::AttemptOption<u64>the delivery's attempt, 1 for the first, None where the table counts none

A handler's first Ctx names the context the delivery builds for it, and decides which keys compile after it. After keys::Tx come the other two, after keys::Pool only keys::Attempt, and nothing after keys::Attempt. So the order Tx, Pool, Attempt fits every handler, with the keys it does not need left out, and a key in another place does not compile. A key's type names its database, as in keys::Pool<Sqlite>. A delivery builds the context only for a handler that reads it: a pool handle and the attempt are copied, and the transaction is lent. The framework's extractors stand beside these keys: Headers<T>, State<T>, and a &mut Context parameter whose headers() read the delivery's headers. In the manual form the body resolves a key with Ctx::<keys::Attempt>::from_context(ctx), its Handle impl naming the context: keys::InboxContext for the attempt, keys::PoolContext<DB> after the pool, keys::TxContext<DB> after the transaction.

Example​

A transactional signup handler logs every attempt through the pool and opens the account through the delivery's transaction; a retry keeps the log and discards the account.

use ruststream_sqlx::prelude::*;
use sqlx::Connection;
use sqlx::Sqlite;
use sqlx::sqlite::SqlitePoolOptions;

/// One row of `signup_jobs`.
#[derive(Inbox, sqlx::FromRow)]
#[inbox(table = "signup_jobs")]
struct OpenAccount {
#[field(id, generated)]
id: i64,
#[field(attempt, generated)]
attempt: i16,
#[field(locked_until)]
locked_until: Option<DateTime<Utc>>,
#[field(payload)]
payload: Vec<u8>,
}

#[subscriber(InboxQueue::<OpenAccount>::new("signups"))]
async fn open_account(
signup: &Signup,
Ctx(mut tx): Ctx<keys::Tx<Sqlite>>,
Ctx(pool): Ctx<keys::Pool<Sqlite>>,
Ctx(attempt): Ctx<keys::Attempt>,
) -> HandlerOutcome {
// Through the pool: the line stays whatever the outcome. It comes first, because on SQLite
// the delivery's transaction holds the database's write lock from its first write.
let _ = sqlx::query("INSERT INTO signup_log (email, attempt) VALUES (?, ?)")
.bind(&signup.email)
.bind(attempt.and_then(|attempt| i64::try_from(attempt).ok()))
.execute(&pool)
.await;
// Through the transaction: the account commits with the acknowledgement, or not at all.
let opened = sqlx::query("INSERT INTO accounts (email) VALUES (?)")
.bind(&signup.email)
.execute(&mut *tx)
.await;
if opened.is_err() || !register(signup, attempt).await {
return HandlerOutcome::retry();
}
HandlerOutcome::ack()
}

/// The service on the database `url` names.
fn service(url: &str) -> RustStream {
let schema_url = url.to_owned();
// Nothing connects here: the broker builds its pool when the service starts.
let broker = SqlxBroker::from_url(url, SqlitePoolOptions::new());
RustStream::new(AppInfo::new("accounts", "0.1.0"))
// The table, created before the subscription checks it.
.on_startup(async move |()| {
let mut conn = SqliteConnection::connect(&schema_url).await?;
sqlx::raw_sql(SCHEMA).execute(&mut conn).await?;
conn.close().await
})
.with_broker(broker.route::<OpenAccount>("signups"), |b| {
b.include(open_account.transactional());
})
}