use std::sync::Arc;
use std::time::Instant;
use runlight::BoxFuture;
use runlight::store::{
Db, DbError, Dialect, Hold, Param, Row, TablesBusyError, fill_placeholders, lock_wait, mysql_literal,
};
use sqlx::pool::PoolConnection;
use sqlx::{AssertSqlSafe, MySql, MySqlPool};
use tokio::sync::Mutex;
use crate::rows::{db_error, mysql_row};
const LOCK: &str = "CONCAT('runlight:', LEFT(SHA2(COALESCE(DATABASE(), ''), 256), 48))";
#[derive(Clone)]
pub struct MysqlDb {
pool: MySqlPool,
}
impl MysqlDb {
pub fn new(pool: MySqlPool) -> MysqlDb {
MysqlDb { pool }
}
}
const NO_BACKSLASH_ESCAPES_OFF: &str = "SET SESSION sql_mode = TRIM(BOTH ',' FROM REPLACE(CONCAT(',', @@SESSION.sql_mode, ','), ',NO_BACKSLASH_ESCAPES,', ','))";
fn text(sql: &str, params: &[Param]) -> Result<AssertSqlSafe<String>, DbError> {
Ok(AssertSqlSafe(format!("{NO_BACKSLASH_ESCAPES_OFF}; {}", fill_placeholders(sql, params, true, mysql_literal)?)))
}
async fn exec(conn: &mut PoolConnection<MySql>, sql: &str) -> Result<Vec<Row>, DbError> {
let rows = sqlx::raw_sql(AssertSqlSafe(sql.to_string())).fetch_all(&mut **conn).await.map_err(db_error)?;
rows.iter().map(mysql_row).collect()
}
impl Db for MysqlDb {
fn dialect(&self) -> Dialect {
Dialect::Mysql
}
fn all<'a>(&'a self, sql: &'a str, params: Vec<Param>) -> BoxFuture<'a, Result<Vec<Row>, DbError>> {
Box::pin(async move {
let rows = sqlx::raw_sql(text(sql, ¶ms)?).fetch_all(&self.pool).await.map_err(db_error)?;
rows.iter().map(mysql_row).collect()
})
}
fn run<'a>(&'a self, sql: &'a str, params: Vec<Param>) -> BoxFuture<'a, Result<u64, DbError>> {
Box::pin(async move {
Ok(sqlx::raw_sql(text(sql, ¶ms)?).execute(&self.pool).await.map_err(db_error)?.rows_affected())
})
}
fn hold(&self, kind: Hold) -> BoxFuture<'_, Result<Arc<dyn Db>, DbError>> {
Box::pin(async move {
let mut slot = Slot { conn: Some(self.pool.acquire().await.map_err(db_error)?), nested: false };
let conn = slot.conn.as_mut().ok_or_else(ended)?;
match kind {
Hold::Transaction => begin(conn).await?,
Hold::Exclusive => {
let wait = lock_wait();
let until = Instant::now() + wait;
loop {
let left = until.saturating_duration_since(Instant::now()).as_millis().div_ceil(1000);
let seconds = left.clamp(1, 5);
let rows = exec(conn, &format!("SELECT GET_LOCK({LOCK}, {seconds}) AS ok")).await?;
let row = rows.into_iter().next().unwrap_or_default();
if row.is_null("ok") {
return Err(DbError("Runlight: MySQL refused the lock for creating tables".into()));
}
if row.num("ok") == 1.0 {
break;
}
if Instant::now() >= until {
return Err(TablesBusyError { waited: wait }.into());
}
}
}
}
Ok(Arc::new(Held { conn: Arc::new(Mutex::new(slot)), kind, role: Role::Owner }) as Arc<dyn Db>)
})
}
}
async fn begin(conn: &mut PoolConnection<MySql>) -> Result<(), DbError> {
exec(conn, "SET TRANSACTION ISOLATION LEVEL READ COMMITTED").await?;
exec(conn, "START TRANSACTION").await?;
Ok(())
}
struct Slot {
conn: Option<PoolConnection<MySql>>,
nested: bool,
}
impl Slot {
fn close(&mut self) {
if let Some(conn) = self.conn.take() {
drop(conn.detach());
}
}
}
impl Drop for Slot {
fn drop(&mut self) {
self.close();
}
}
struct Held {
conn: Arc<Mutex<Slot>>,
kind: Hold,
role: Role,
}
impl Drop for Held {
fn drop(&mut self) {
if self.role == Role::Owner
&& let Ok(mut slot) = self.conn.try_lock()
{
slot.close();
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Role {
Owner,
Nested,
Joined,
}
fn ended() -> DbError {
DbError("Runlight: the held connection has been let go".into())
}
impl Db for Held {
fn dialect(&self) -> Dialect {
Dialect::Mysql
}
fn all<'a>(&'a self, sql: &'a str, params: Vec<Param>) -> BoxFuture<'a, Result<Vec<Row>, DbError>> {
Box::pin(async move {
let mut slot = self.conn.lock().await;
let conn = slot.conn.as_mut().ok_or_else(ended)?;
let rows = sqlx::raw_sql(text(sql, ¶ms)?).fetch_all(&mut **conn).await.map_err(db_error)?;
rows.iter().map(mysql_row).collect()
})
}
fn run<'a>(&'a self, sql: &'a str, params: Vec<Param>) -> BoxFuture<'a, Result<u64, DbError>> {
Box::pin(async move {
let mut slot = self.conn.lock().await;
let conn = slot.conn.as_mut().ok_or_else(ended)?;
Ok(sqlx::raw_sql(text(sql, ¶ms)?).execute(&mut **conn).await.map_err(db_error)?.rows_affected())
})
}
fn hold(&self, kind: Hold) -> BoxFuture<'_, Result<Arc<dyn Db>, DbError>> {
Box::pin(async move {
if kind == Hold::Transaction && self.kind == Hold::Exclusive {
let mut slot = self.conn.lock().await;
let slot = &mut *slot;
let conn = slot.conn.as_mut().ok_or_else(ended)?;
slot.nested = true;
begin(conn).await?;
return Ok(Arc::new(Held { conn: self.conn.clone(), kind, role: Role::Nested }) as Arc<dyn Db>);
}
Ok(Arc::new(Held { conn: self.conn.clone(), kind: self.kind, role: Role::Joined }) as Arc<dyn Db>)
})
}
fn finish(&self, ok: bool) -> BoxFuture<'_, Result<(), DbError>> {
Box::pin(async move {
let mut slot = self.conn.lock().await;
match self.role {
Role::Joined => Ok(()),
Role::Nested => {
let slot = &mut *slot;
let Some(conn) = slot.conn.as_mut() else { return Ok(()) };
exec(conn, if ok { "COMMIT" } else { "ROLLBACK" }).await?;
slot.nested = false;
Ok(())
}
Role::Owner if slot.nested => {
slot.close();
Ok(())
}
Role::Owner => {
let Some(mut conn) = slot.conn.take() else { return Ok(()) };
let result = match self.kind {
Hold::Transaction => exec(&mut conn, if ok { "COMMIT" } else { "ROLLBACK" }).await.map(|_| ()),
Hold::Exclusive => exec(&mut conn, &format!("DO RELEASE_LOCK({LOCK})")).await.map(|_| ()),
};
if result.is_err() {
drop(conn.detach());
}
if self.kind == Hold::Exclusive { Ok(()) } else { result }
}
}
})
}
}