use std::convert::Infallible;
use std::time::Duration;
use ruststream::runtime::RunningApp;
use ruststream::{Outgoing, nonzero};
use ruststream_sqlx::prelude::*;
use serde::Serialize;
use sqlx::Pool;
use crate::live;
use crate::locks::HeldLocks;
use crate::pool::{every_connection_answers, fresh_pool};
use crate::probe::{GUARD, Probe, until};
const POLL: Duration = Duration::from_millis(20);
#[derive(Debug, Clone, Copy)]
enum Mount {
Settles,
Routed,
Typed,
Queries,
TwoAtOnce,
}
#[derive(Debug, Serialize, Outgoing)]
#[outgoing(name = "replies")]
struct Reply {
id: i64,
}
live::matrix! {
async fn take_every_connection(pool: &Pool<Db>, probe: &Probe) {
let held = usize::from(!LEASED);
loop {
while let Some(conn) = pool.try_acquire() {
probe.keep(conn);
}
let size = usize::try_from(pool.size()).expect("fits");
if probe.kept() + held >= size {
return;
}
tokio::task::yield_now().await;
}
}
#[subscriber(InboxQueue::<Plain>::new("plain"), threads(2))]
async fn settled(id: &i64, Ctx(pool): Ctx<keys::Pool<Db>>, State(probe): State<Probe>) {
take_every_connection(&pool, &probe).await;
probe.handled("plain", *id);
}
#[subscriber(InboxQueue::<Plain>::new("plain"), threads(2), reply)]
async fn answered(
id: &i64,
Ctx(pool): Ctx<keys::Pool<Db>>,
State(probe): State<Probe>,
) -> Reply {
take_every_connection(&pool, &probe).await;
probe.handled("plain", *id);
Reply { id: *id }
}
#[subscriber(InboxQueue::<Plain>::new("plain"), threads(2))]
async fn queried(id: &i64, Ctx(pool): Ctx<keys::Pool<Db>>, State(probe): State<Probe>) {
take_every_connection(&pool, &probe).await;
let mut conn = pool.acquire().await.expect("a connection");
sqlx::raw_sql("SELECT 1").execute(&mut *conn).await.expect("the handler's query");
drop(conn);
probe.handled("plain", *id);
}
#[subscriber(InboxQueue::<Plain>::new("plain"))]
async fn paired(id: &i64, Ctx(pool): Ctx<keys::Pool<Db>>, State(probe): State<Probe>) {
let mut first = pool.acquire().await.expect("a connection");
let mut second = pool.acquire().await.expect("a second connection");
sqlx::raw_sql("SELECT 1").execute(&mut *first).await.expect("the first query");
sqlx::raw_sql("SELECT 1").execute(&mut *second).await.expect("the second query");
probe.handled("plain", *id);
}
async fn started(pool: &Pool<Db>, probe: &Probe, mount: Mount) -> RunningApp {
let probe = probe.clone();
let broker = SqlxBroker::new(pool.clone())
.poll_interval(POLL)
.route::<SendEmail>("replies");
let app = RustStream::new(AppInfo::new("threads", "0.0.0"))
.on_startup(async move |()| Ok::<_, Infallible>(probe))
.with_broker(broker, move |b| match mount {
Mount::Settles => {
b.include(settled);
}
Mount::Routed => {
b.include(answered);
}
Mount::Typed => {
b.include(answered).out_reply(Repository::<SendEmail>::default());
}
Mount::Queries => {
b.include(queried);
}
Mount::TwoAtOnce => {
let threads = InboxThreads::new(nonzero!(2)).connections(nonzero!(2));
b.include(paired.on_threads(threads));
}
});
tokio::time::timeout(GUARD, app.start())
.await
.expect("the service starts in time")
.expect("the service starts")
}
async fn run(db: &live::Database<Db>, pool: &Pool<Db>, mount: Mount, run: usize) {
let probe = Probe::default();
let running = started(pool, &probe, mount).await;
db.plain(&[run.to_string()]).await;
probe.reached(1).await;
until("the row settled and its connection returned", async || {
pool.num_idle() >= 1 && db.count("plain_jobs").await == 0
})
.await;
tokio::time::timeout(GUARD, running.shutdown())
.await
.expect("the service stops in time")
.expect("the service stops");
probe.release();
let runs = i64::try_from(run).expect("a count");
assert_eq!(db.count("plain_jobs").await, 0, "run {run}: the row settled");
let replies = if matches!(mount, Mount::Routed | Mount::Typed) { runs } else { 0 };
assert_eq!(db.count("email_jobs").await, replies, "run {run}: the replies written");
let ids: Vec<i64> = (1..=runs).collect();
assert_eq!(
Db::held_locks(&db.pool, "plain_jobs", &ids).await,
0,
"run {run}: the stopped service holds no lock"
);
}
async fn outlived(mount: Mount) {
let Some(db) = database().await else { return };
let pool = fresh_pool(&db);
run(&db, &pool, mount, 1).await;
every_connection_answers(&pool, "after the first run").await;
run(&db, &pool, mount, 2).await;
every_connection_answers(&pool, "after the restart").await;
pool.close().await;
db.finish().await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_settlement_opens_its_connection_on_the_app_runtime() {
outlived(Mount::Settles).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_routed_reply_opens_its_connection_on_the_app_runtime() {
outlived(Mount::Routed).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_typed_reply_opens_its_connection_on_the_app_runtime() {
outlived(Mount::Typed).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_handler_query_leaves_no_connection_that_dies_with_its_thread() {
outlived(Mount::Queries).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_thread_pool_opens_the_connections_the_mount_sets() {
outlived(Mount::TwoAtOnce).await;
}
}