use std::fmt;
use std::future::Future;
use std::ops::Deref;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::sync::{OwnedSemaphorePermit, Semaphore};
use crate::error::{Error, Result};
use crate::executor::Conn;
use crate::options::{ConnectOptions, Source};
use turso_sql::Statement;
macro_rules! retry_on_replica {
($options:expr, $statement:ident, $call:expr) => {
match $options.replica_retry_budget() {
None => $call.await,
Some(budget) => {
let $statement = &$statement;
retry_busy(budget, || {
let $statement = $statement.clone();
$call
})
.await
}
}
};
}
pub(crate) use retry_on_replica;
enum Engine {
Local(turso::Database),
#[cfg(feature = "sync")]
Sync(turso::sync::Database),
#[cfg(feature = "serverless")]
Remote(turso_serverless::Database),
}
pub(crate) struct Inner {
engine: Engine,
pub(crate) options: ConnectOptions,
idle: Mutex<Vec<Conn>>,
permits: Arc<Semaphore>,
#[cfg(feature = "sync")]
sync_task: AtomicBool,
}
#[derive(Clone)]
pub struct Database {
pub(crate) inner: Arc<Inner>,
}
impl fmt::Debug for Database {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Database")
.field("source", &self.inner.options.source)
.field("max_connections", &self.inner.options.max_connections)
.finish_non_exhaustive()
}
}
#[cfg(feature = "sync")]
#[cfg_attr(docsrs, doc(cfg(feature = "sync")))]
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct ReplicaStats {
pub pending_operations: i64,
pub wal_bytes: u64,
pub revert_wal_bytes: u64,
pub last_pull_unix_time: Option<i64>,
pub last_push_unix_time: Option<i64>,
pub revision: Option<String>,
pub network_sent_bytes: u64,
pub network_received_bytes: u64,
}
#[cfg(feature = "sync")]
impl ReplicaStats {
fn from_engine(stats: turso::sync::DatabaseSyncStats) -> Self {
Self {
pending_operations: stats.cdc_operations,
wal_bytes: stats.main_wal_size,
revert_wal_bytes: stats.revert_wal_size,
last_pull_unix_time: stats.last_pull_unix_time,
last_push_unix_time: stats.last_push_unix_time,
revision: stats.revision,
network_sent_bytes: u64::try_from(stats.network_sent_bytes).unwrap_or(u64::MAX),
network_received_bytes: u64::try_from(stats.network_received_bytes).unwrap_or(u64::MAX),
}
}
}
impl Database {
pub async fn connect(options: impl Into<ConnectOptions>) -> Result<Self> {
let options = options.into();
let engine = match &options.source {
Source::Memory => Engine::Local(options.local_builder(":memory:").build().await?),
Source::File(path) => Engine::Local(
options
.local_builder(&path.to_string_lossy())
.build()
.await?,
),
#[cfg(feature = "sync")]
Source::Sync(sync) => {
if options.encryption.is_some() {
return Err(Error::InvalidOptions(
"local encryption is not supported together with sync".into(),
));
}
let mut builder = turso::sync::Builder::new_remote(&sync.path.to_string_lossy())
.with_remote_url(&sync.remote_url)
.bootstrap_if_empty(sync.bootstrap_if_empty);
if let Some(token) = &sync.auth_token {
builder = builder.with_auth_token(token);
}
Engine::Sync(builder.build().await?)
}
#[cfg(feature = "serverless")]
Source::Remote(remote) => {
if options.encryption.is_some() {
return Err(Error::InvalidOptions(
"local encryption does not apply to a remote database".into(),
));
}
let mut builder = turso_serverless::Builder::new_remote(remote.url.clone());
if let Some(token) = &remote.auth_token {
builder = builder.with_auth_token(token.clone());
}
if let Some(key) = &remote.remote_encryption_key {
builder = builder.with_remote_encryption_key(key.clone());
}
Engine::Remote(builder.build().await?)
}
};
let db = Self {
inner: Arc::new(Inner {
engine,
permits: Arc::new(Semaphore::new(options.max_connections)),
idle: Mutex::new(Vec::with_capacity(options.max_connections)),
options,
#[cfg(feature = "sync")]
sync_task: AtomicBool::new(false),
}),
};
drop(db.acquire().await?);
tracing::info!(
source = ?db.inner.options.source,
max_connections = db.inner.options.max_connections,
"database opened"
);
Ok(db)
}
pub fn options(&self) -> &ConnectOptions {
&self.inner.options
}
#[cfg(feature = "sync")]
fn replica_budget(&self) -> Duration {
self.inner.options.busy_timeout.unwrap_or_default()
}
#[cfg(feature = "sync")]
pub(crate) fn is_replica(&self) -> bool {
matches!(self.inner.engine, Engine::Sync(_))
}
#[cfg(feature = "sync")]
pub(crate) fn claim_sync_task(&self) -> bool {
self.inner
.sync_task
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
}
#[cfg(feature = "sync")]
pub(crate) fn release_sync_task(&self) {
self.inner.sync_task.store(false, Ordering::Release);
}
pub async fn ping(&self) -> Result<()> {
let conn = self.acquire().await?;
crate::executor::query_one(&conn, Statement::from_string("SELECT 1")).await?;
Ok(())
}
#[cfg(feature = "sync")]
#[cfg_attr(docsrs, doc(cfg(feature = "sync")))]
pub async fn push(&self) -> Result<()> {
let Engine::Sync(db) = &self.inner.engine else {
return Err(Error::InvalidOptions("not an embedded replica".into()));
};
retry_busy(self.replica_budget(), || async { Ok(db.push().await?) }).await?;
tracing::info!("replica pushed");
Ok(())
}
#[cfg(feature = "sync")]
#[cfg_attr(docsrs, doc(cfg(feature = "sync")))]
pub async fn pull(&self) -> Result<bool> {
let Engine::Sync(db) = &self.inner.engine else {
return Err(Error::InvalidOptions("not an embedded replica".into()));
};
let changed = retry_busy(self.replica_budget(), || async { Ok(db.pull().await?) }).await?;
tracing::info!(changed, "replica pulled");
Ok(changed)
}
#[cfg(feature = "sync")]
#[cfg_attr(docsrs, doc(cfg(feature = "sync")))]
pub async fn checkpoint(&self) -> Result<()> {
let Engine::Sync(db) = &self.inner.engine else {
return Err(Error::InvalidOptions("not an embedded replica".into()));
};
retry_busy(self.replica_budget(), || async {
Ok(db.checkpoint().await?)
})
.await?;
tracing::info!("replica checkpointed");
Ok(())
}
#[cfg(feature = "sync")]
#[cfg_attr(docsrs, doc(cfg(feature = "sync")))]
pub async fn stats(&self) -> Result<ReplicaStats> {
let Engine::Sync(db) = &self.inner.engine else {
return Err(Error::InvalidOptions("not an embedded replica".into()));
};
let stats = ReplicaStats::from_engine(db.stats().await?);
tracing::debug!(
pending = stats.pending_operations,
wal_bytes = stats.wal_bytes,
"replica stats read"
);
Ok(stats)
}
pub(crate) async fn acquire(&self) -> Result<PooledConnection> {
let permit = match Arc::clone(&self.inner.permits).try_acquire_owned() {
Ok(permit) => permit,
Err(tokio::sync::TryAcquireError::Closed) => {
return Err(Error::Misuse("connection pool closed".into()));
}
Err(tokio::sync::TryAcquireError::NoPermits) => tokio::time::timeout(
self.inner.options.acquire_timeout,
Arc::clone(&self.inner.permits).acquire_owned(),
)
.await
.map_err(|_| Error::PoolTimeout)?
.map_err(|_| Error::Misuse("connection pool closed".into()))?,
};
let idle = self
.inner
.idle
.lock()
.map_err(|_| Error::Misuse("pool mutex poisoned".into()))?
.pop();
let reused = idle.is_some();
let conn = match idle {
Some(conn) => conn,
None => self.open_connection().await?,
};
tracing::trace!(reused, "connection acquired");
Ok(PooledConnection {
conn: Some(conn),
pool: Arc::clone(&self.inner),
_permit: permit,
discard: AtomicBool::new(false),
})
}
async fn open_connection(&self) -> Result<Conn> {
let options = &self.inner.options;
let conn = match &self.inner.engine {
Engine::Local(db) => Conn::Embedded(db.connect()?),
#[cfg(feature = "sync")]
Engine::Sync(db) => Conn::Embedded(
retry_busy(self.replica_budget(), || async { Ok(db.connect().await?) }).await?,
),
#[cfg(feature = "serverless")]
Engine::Remote(db) => Conn::Remote(db.connect()?),
};
if let Some(timeout) = options.busy_timeout {
conn.busy_timeout(timeout)?;
}
match options.replica_retry_budget() {
None => Self::configure(&conn, options).await?,
Some(budget) => retry_busy(budget, || Self::configure(&conn, options)).await?,
}
tracing::debug!("connection opened");
Ok(conn)
}
async fn configure(conn: &Conn, options: &ConnectOptions) -> Result<()> {
if options.foreign_keys {
conn.pragma_update("foreign_keys", "ON").await?;
}
if options.mvcc && matches!(conn, Conn::Embedded(_)) {
conn.pragma_update("journal_mode", "'mvcc'").await?;
}
for (name, value) in &options.pragmas {
conn.pragma_update(name, value).await?;
}
Ok(())
}
}
pub(crate) struct PooledConnection {
conn: Option<Conn>,
pool: Arc<Inner>,
_permit: OwnedSemaphorePermit,
discard: AtomicBool,
}
impl PooledConnection {
pub(crate) fn discard(&self) {
self.discard.store(true, Ordering::Release);
}
pub(crate) fn options(&self) -> &ConnectOptions {
&self.pool.options
}
}
impl Deref for PooledConnection {
type Target = Conn;
fn deref(&self) -> &Self::Target {
self.conn.as_ref().expect("connection present until drop")
}
}
impl Drop for PooledConnection {
fn drop(&mut self) {
let Some(conn) = self.conn.take() else {
return;
};
if !self.discard.load(Ordering::Acquire)
&& conn.is_autocommit().unwrap_or(false)
&& let Ok(mut idle) = self.pool.idle.lock()
{
idle.push(conn);
tracing::trace!("connection returned");
} else {
tracing::debug!("connection discarded");
}
}
}
impl fmt::Debug for PooledConnection {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PooledConnection").finish_non_exhaustive()
}
}
pub(crate) async fn retry_busy<T, F, Fut>(budget: Duration, mut op: F) -> Result<T>
where
F: FnMut() -> Fut,
Fut: Future<Output = Result<T>>,
{
let start = std::time::Instant::now();
let mut delay = Duration::from_millis(5);
loop {
match op().await {
Err(err) if err.is_busy() && start.elapsed() < budget => {
tracing::debug!(%err, ?delay, "busy, retrying");
tokio::time::sleep(delay).await;
delay = (delay * 2).min(Duration::from_millis(250));
}
other => return other,
}
}
}