Skip to main content

Connections

Each message in work holds pool connections by its form; workers(n) claims in parallel but always leaves the pool one connection for handlers.

A subscription mounted with workers(n) keeps a claim in flight for each free worker. It never holds more than the pool's size less one connection, so the pool always keeps one for the handlers' own queries, their publishes and the settlements that take one. A second claim starts only while the pool has a connection to spare, so subscriptions that share a pool leave each other room, and a pool short of room runs fewer deliveries at once. A subscription keeps one claim in flight on SQLite, which takes one writer at a time, on a table whose groups keep their order, and with a partition_key column under workers_by_key(n). On SQLite that one claim takes a row for every worker in the lease form; in transactional mode each of those rows holds its transaction's connection while it waits for a worker, within the connections the subscription may hold.

What a delivery holds depends on the form:

FormConnections
row lockone per delivery in work and one per claim in flight, for the whole handler
leaseone for each claim's and each settlement's statement, and one per subscription each half lease to extend the leases
advisory lockone per delivery in work until it settles, in a batch too
transactional mode, any formone per delivery in work, for the transaction its handler writes through

A publish takes a connection of its own for its insert, and a handler that reads keys::Pool takes one for each query. So a pool is sized for every subscription: n for each workers(n), one more for the pool, and the connections the handlers take for their own work. In the lease form a pool of n + 1 lets every free worker claim at once. Under sqlx's default test_before_acquire, each acquire pings the connection first, a round trip a pool built with test_before_acquire(false) skips. Subscriptions on dedicated threads open connections of their own on top of the pool.

Example​

Four workers resize images, and each handler records its image through the pool; the pool has room for the four workers, the connection the subscription leaves free and the handlers' writes.

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

#[derive(Inbox, sqlx::FromRow)]
#[inbox(table = "resize_jobs")]
struct ResizeImage {
#[field(id, generated)]
id: i64,
#[field(locked_until)]
locked_until: Option<DateTime<Utc>>,
#[field(payload)]
payload: Vec<u8>,
}

// The handler takes a connection of the pool for its own write.
#[subscriber(InboxQueue::<ResizeImage>::new("resizes"))]
async fn resize(image: &Image, Ctx(pool): Ctx<keys::Pool<Sqlite>>) -> HandlerOutcome {
let recorded = sqlx::query("INSERT INTO resized (path) VALUES (?)")
.bind(&image.path)
.execute(&pool)
.await;
match recorded {
Ok(_) => HandlerOutcome::ack(),
Err(error) => {
tracing::warn!(subscription = "resizes", path = %image.path, %error, "not recorded");
HandlerOutcome::retry()
}
}
}

/// The service on the database `url` names.
fn service(url: &str) -> RustStream {
let schema_url = url.to_owned();
// Four workers, one connection the subscription always leaves free, and one for the
// handlers' own writes and the publishes.
// Nothing connects here: the broker builds its pool when the service starts.
let broker = SqlxBroker::from_url(url, SqlitePoolOptions::new().max_connections(6));
RustStream::new(AppInfo::new("resizer", "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::<ResizeImage>("resizes"), |b| {
b.include(resize.workers(nonzero!(4)));
})
}

#[ruststream::app]
fn app() -> impl App {
service("sqlite:resizes.db?mode=rwc")
}