use std::time::{Duration, Instant};
use rusqlite::DropBehavior;
use crate::{Connection, Filesystem, RootId, sql::SqlStore};
pub(super) const DEFAULT_IDLE_THRESHOLD: Duration = Duration::from_secs(1);
pub(super) const DEFAULT_AGE_CEILING: Duration = Duration::from_secs(30);
#[derive(Debug)]
struct TxState {
began_at: Instant,
last_activity: Instant,
changes_at_begin: u64,
}
impl TxState {
fn new(changes_at_begin: u64) -> Self {
let now = Instant::now();
Self {
began_at: now,
last_activity: now,
changes_at_begin,
}
}
}
#[derive(Debug)]
pub(super) struct ConnTx {
conn: Connection,
tx: Option<TxState>,
root_id: RootId,
}
impl ConnTx {
pub fn new(conn: Connection, root_id: RootId) -> Self {
Self {
conn,
tx: None,
root_id,
}
}
fn ensure_tx_started(&mut self) -> crate::Result<()> {
if self.tx.is_none() {
self.conn.execute_batch("BEGIN DEFERRED")?;
self.tx = Some(TxState::new(self.conn.total_changes()));
}
Ok(())
}
pub fn with_fs<R, E, F>(&mut self, f: F) -> Result<R, E>
where
F: FnOnce(&mut Filesystem) -> Result<R, E>,
E: From<crate::Error>,
{
self.ensure_tx_started().map_err(E::from)?;
self.tx
.as_mut()
.expect("Transaction state should be present at this point.")
.last_activity = Instant::now();
let settings = self.conn.settings().clone();
let default_root_id = self.conn.default_root_id();
let root_id = self.root_id;
let mut savepoint = self.conn.raw_savepoint().map_err(E::from)?;
savepoint.set_drop_behavior(DropBehavior::Commit);
let store = SqlStore::new(savepoint, settings.clone());
let mut fs = Filesystem::new(default_root_id, settings, store);
fs.switch_root(root_id).map_err(E::from)?;
f(&mut fs)
}
pub fn is_dirty(&self) -> bool {
self.tx
.as_ref()
.is_some_and(|t| self.conn.total_changes() != t.changes_at_begin)
}
pub fn idle_for(&self) -> Option<Duration> {
self.tx.as_ref().map(|t| t.last_activity.elapsed())
}
pub fn age(&self) -> Option<Duration> {
self.tx.as_ref().map(|t| t.began_at.elapsed())
}
pub fn commit_if_dirty(&mut self) -> crate::Result<()> {
if self.is_dirty() {
self.commit_unconditionally()
} else {
Ok(())
}
}
pub fn commit_unconditionally(&mut self) -> crate::Result<()> {
if self.tx.is_some() {
if self.is_dirty() {
self.conn.clean_orphan_blocks()?;
}
self.conn.execute_batch("COMMIT")?;
self.tx = None;
}
Ok(())
}
fn commit_if_idle(&mut self, threshold: Duration) -> crate::Result<()> {
if self.idle_for().is_some_and(|idle| idle >= threshold) {
self.commit_if_dirty()
} else {
Ok(())
}
}
fn commit_if_aged(&mut self, threshold: Duration) -> crate::Result<()> {
if self.age().is_some_and(|age| age >= threshold) {
self.commit_if_dirty()
} else {
Ok(())
}
}
pub fn apply_default_commit_triggers(&mut self) -> crate::Result<()> {
self.commit_if_idle(DEFAULT_IDLE_THRESHOLD)?;
self.commit_if_aged(DEFAULT_AGE_CEILING)?;
Ok(())
}
}
impl Drop for ConnTx {
fn drop(&mut self) {
if self.tx.is_some() {
self.commit_unconditionally().ok();
}
}
}