use std::{marker::PhantomData, num::NonZero, time::Duration};
use futures::future::BoxFuture;
use magic::Bool;
use sqlx::{
migrate::MigrateDatabase,
sqlite::SqlitePoolOptions,
sqlite::{SqliteConnectOptions, SqliteJournalMode, SqliteSynchronous},
Connection as _, Sqlite, SqliteConnection, SqlitePool,
};
use conn::{Conn, NoTransaction, Reader, Tx, Writer};
pub use magic::{False, True};
pub type WriterPool = Pool<True>;
pub type ReaderPool = Pool<False>;
pub mod conn;
pub trait Connection {
type Writable: Bool;
type Transaction: Bool;
fn get_inner(&mut self) -> &mut SqliteConnection;
#[allow(async_fn_in_trait)]
async fn transaction<O, E, F>(&mut self, f: F) -> Result<O, E>
where
for<'t> F: FnOnce(Conn<Self::Writable, Tx<'t, '_>>) -> BoxFuture<'t, Result<O, E>>
+ Send
+ Sync
+ 't,
O: Send,
E: From<sqlx::Error> + Send,
{
self.get_inner()
.transaction(move |inner| {
let conn = Conn::from_tx(inner);
f(conn)
})
.await
}
}
pub trait WriterConnection: Connection<Writable = True> {
type Transaction: Bool;
}
impl<T> WriterConnection for T
where
T: Connection<Writable = True>,
{
type Transaction = <T as Connection>::Transaction;
}
impl<C> Connection for &mut C
where
C: Connection,
{
type Writable = <C as Connection>::Writable;
type Transaction = <C as Connection>::Transaction;
fn get_inner(&mut self) -> &mut SqliteConnection {
<C as Connection>::get_inner(*self)
}
}
#[derive(Debug, Clone)]
pub struct DBPools {
read_pool: Pool<False>,
write_pool: Pool<True>,
}
impl DBPools {
#[cfg(test)]
pub(crate) fn from_test_pool(pool: &sqlx::SqlitePool) -> Self {
DBPools {
read_pool: Pool::new(pool.clone()),
write_pool: Pool::new(pool.clone()),
}
}
pub async fn check_health(&self) -> bool {
match self.writer_conn().await {
Ok(mut conn) => conn
.transaction(|mut tx| {
Box::pin(async move {
let _count =
crate::common::submission::db::count_submissions(&mut tx).await?;
Ok::<_, anyhow::Error>(())
})
})
.await
.is_ok(),
Err(error) => {
tracing::error!("DB unhealthy; could not acquire DB connection: {error:?}");
false
}
}
}
pub fn reader_pool(&self) -> &ReaderPool {
&self.read_pool
}
pub fn writer_pool(&self) -> &WriterPool {
&self.write_pool
}
pub async fn reader_conn(&self) -> sqlx::Result<Reader<NoTransaction>> {
self.read_pool.reader_conn().await
}
pub async fn writer_conn(&self) -> sqlx::Result<Writer<NoTransaction>> {
self.write_pool.writer_conn().await
}
}
#[derive(Debug, Clone)]
pub struct Pool<Writer> {
pub(super) inner: SqlitePool,
_type: PhantomData<Writer>,
}
impl<W> Pool<W>
where
W: magic::Bool,
{
pub fn new(pool: SqlitePool) -> Pool<W> {
Pool {
inner: pool,
_type: PhantomData,
}
}
pub async fn acquire(&self) -> sqlx::Result<Conn<W>> {
self.inner.acquire().await.map(Conn::new)
}
}
impl WriterPool {
pub async fn writer_conn(&self) -> sqlx::Result<Writer<NoTransaction>> {
self.acquire().await
}
}
impl ReaderPool {
pub async fn reader_conn(&self) -> sqlx::Result<Reader<NoTransaction>> {
self.acquire().await
}
}
pub mod magic {
#[derive(Debug, Clone, Copy)]
pub enum True {}
#[derive(Debug, Clone, Copy)]
pub enum False {}
mod s {
pub trait Sealed {}
impl Sealed for super::True {}
impl Sealed for super::False {}
}
pub trait Bool: s::Sealed + Send + 'static {}
impl Bool for True {}
impl Bool for False {}
}
pub async fn open_and_setup(database_filename: &str, max_read_pool_size: NonZero<u32>) -> DBPools {
ensure_db_exists(database_filename).await;
let read_pool = db_connect_read_pool(database_filename, max_read_pool_size).await;
let write_pool = db_connect_write_pool(database_filename).await;
ensure_db_migrated(&write_pool).await;
DBPools {
read_pool,
write_pool,
}
}
fn db_options(database_filename: &str) -> SqliteConnectOptions {
SqliteConnectOptions::new()
.filename(database_filename)
.journal_mode(SqliteJournalMode::Wal) .synchronous(SqliteSynchronous::Normal) .busy_timeout(Duration::from_secs(5)) .foreign_keys(true) .pragma("mmap_size", "134217728")
.pragma("cache_size", "-1000000") }
async fn ensure_db_exists(database_filename: &str) {
if !Sqlite::database_exists(database_filename)
.await
.unwrap_or(false)
{
tracing::info!("Creating backing sqlite DB {}", database_filename);
Sqlite::create_database(database_filename)
.await
.expect("Could not create backing sqlite DB");
tracing::info!("Finished creating backing sqlite DB {}", database_filename);
} else {
tracing::info!("Starting up using existing sqlite DB {}", database_filename);
}
}
async fn ensure_db_migrated(db: &WriterPool) {
tracing::info!("Migrating backing DB");
sqlx::migrate!("./migrations")
.set_ignore_missing(true)
.run(&db.inner)
.await
.expect("DB migrations failed");
tracing::info!("Finished migrating backing DB");
}
async fn db_connect_read_pool(
database_filename: &str,
max_read_pool_size: NonZero<u32>,
) -> ReaderPool {
SqlitePoolOptions::new()
.min_connections(16)
.max_connections(max_read_pool_size.into())
.connect_with(db_options(database_filename))
.await
.map(Pool::new)
.expect("Could not connect to sqlite DB")
}
async fn db_connect_write_pool(database_filename: &str) -> WriterPool {
SqlitePoolOptions::new()
.min_connections(1)
.max_connections(1)
.connect_with(db_options(database_filename))
.await
.map(Pool::new)
.expect("Could not connect to sqlite DB")
}