use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use sqlx::sqlite::SqliteConnectOptions;
use sqlx::{Connection, SqliteConnection};
use thiserror::Error;
use tokio_util::task::AbortOnDropHandle;
use tracing::warn;
use crate::sqlite::Info;
use crate::sync::EagerFutureCell;
#[derive(Debug, Error)]
enum WalCompactionError {
#[error("failed to compact the WAL due to a sqlx error: {0}")]
Sqlx(#[from] sqlx::Error),
#[error("failed to compact the WAL due to an IO error: {0}")]
Io(#[from] std::io::Error),
}
#[derive(Debug, Clone)]
struct ActiveCompactor {
_task: Arc<AbortOnDropHandle<()>>,
}
impl ActiveCompactor {
const THRESHOLD_BYTES: u64 = 32 * 1024 * 1024;
const PERIOD: Duration = Duration::from_mins(1);
const MAX_TIMEOUT: Duration = Duration::from_millis(500);
fn spawn(conn: SqliteConnection, info: EagerFutureCell<Info>) -> Self {
let task = tokio::spawn(Self::run(conn, info));
Self {
_task: Arc::new(AbortOnDropHandle::new(task)),
}
}
async fn run(mut conn: SqliteConnection, info: EagerFutureCell<Info>) {
let wal_path = match info.get().await.wal_path().map(Path::to_path_buf) {
Ok(wal_path) => wal_path,
Err(error) => {
warn!(%error, "could not resolve the WAL path; WAL compactor disabled");
return;
}
};
let wal = match tokio::fs::File::open(&wal_path).await {
Ok(wal) => wal,
Err(error) => {
warn!(%error, "could not open the WAL file; WAL compactor disabled");
return;
}
};
let mut ticker = tokio::time::interval(Self::PERIOD);
ticker.tick().await;
loop {
ticker.tick().await;
if let Err(error) = Self::compact_wal(&mut conn, &wal).await {
warn!(%error, "failed to compact the WAL");
}
}
}
async fn compact_wal(
conn: &mut SqliteConnection,
wal: &tokio::fs::File,
) -> Result<(), WalCompactionError> {
let meta = wal.metadata().await?;
if meta.len() < Self::THRESHOLD_BYTES {
return Ok(());
}
let (busy, log, checkpointed): (i64, i64, i64) =
sqlx::query_as("PRAGMA wal_checkpoint(PASSIVE)").fetch_one(&mut *conn).await?;
if busy != 0 || checkpointed < log {
sqlx::query("PRAGMA wal_checkpoint(RESTART)").execute(conn).await?;
}
Ok(())
}
async fn connect(opts: SqliteConnectOptions) -> Result<SqliteConnection, WalCompactionError> {
let conn = SqliteConnection::connect_with(&opts.busy_timeout(Self::MAX_TIMEOUT)).await?;
Ok(conn)
}
}
#[derive(Debug, Clone)]
enum CompactorInner {
Active {
_compactor: ActiveCompactor,
},
Inactive,
}
#[derive(Debug, Clone)]
pub(super) struct Compactor {
_inner: CompactorInner,
}
impl Compactor {
pub(super) async fn spawn_active(
opts: SqliteConnectOptions,
info: EagerFutureCell<Info>,
) -> Self {
match ActiveCompactor::connect(opts).await {
Ok(conn) => Self {
_inner: CompactorInner::Active {
_compactor: ActiveCompactor::spawn(conn, info),
},
},
Err(error) => {
warn!(%error, "failed to open the WAL compactor connection; WAL compactor disabled");
Self::inactive()
}
}
}
pub(super) fn inactive() -> Self {
Self {
_inner: CompactorInner::Inactive,
}
}
}