use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
use rusqlite::{Connection, Transaction, TransactionBehavior, params};
use super::logged;
use crate::config::History;
const BATCH: u64 = 2000;
pub(super) struct Db<'prune> {
pub(super) connection: &'prune Connection,
pub(super) stop: &'prune AtomicBool,
}
impl Db<'_> {
fn stopped(&self) -> bool {
self.stop.load(Ordering::Relaxed)
}
}
impl std::ops::Deref for Db<'_> {
type Target = Connection;
fn deref(&self) -> &Connection {
self.connection
}
}
pub(super) fn run(db: &Db<'_>, now: u64, history: &History) {
let before =
|keep: Duration| now.saturating_sub(u64::try_from(keep.as_millis()).unwrap_or(u64::MAX));
logged("prune samples", samples(db, before(history.samples)));
logged(
"prune host samples",
batches(
db,
"DELETE FROM host_samples WHERE at IN (SELECT at FROM host_samples WHERE at < ?1 LIMIT ?2)",
before(history.samples),
),
);
logged("prune logs", logs(db, before(history.logs)));
logged(
"prune events",
batches(
db,
"DELETE FROM events WHERE id IN (SELECT id FROM events WHERE at < ?1 \
AND id NOT IN (SELECT event FROM announced) \
AND id NOT IN (SELECT event FROM announced_profiles) LIMIT ?2)",
before(history.events),
),
);
logged(
"prune cache sizes",
db.execute(
"DELETE FROM cache_sizes WHERE at < ?1",
[before(history.events)],
),
);
logged("prune jobs", jobs(db, before(history.jobs)));
}
fn batched(
db: &Db<'_>,
mut delete: impl FnMut() -> rusqlite::Result<usize>,
) -> rusqlite::Result<()> {
loop {
if db.stopped() {
return Ok(());
}
let started = Instant::now();
if delete()? == 0 {
return Ok(());
}
std::thread::sleep(started.elapsed());
}
}
fn batches(db: &Db<'_>, sql: &str, before: u64) -> rusqlite::Result<()> {
let mut delete = db.prepare(sql)?;
batched(db, || delete.execute(params![before, BATCH]))
}
fn samples(db: &Db<'_>, before: u64) -> rusqlite::Result<()> {
let runners: Vec<String> = db
.prepare(
"SELECT name FROM runners WHERE started_at < ?1 \
AND EXISTS (SELECT 1 FROM samples WHERE runner = name AND at < ?1)",
)?
.query_map([before], |row| row.get(0))?
.collect::<rusqlite::Result<_>>()?;
delete_samples(db, &runners, before)
}
fn delete_samples(db: &Db<'_>, runners: &[String], before: u64) -> rusqlite::Result<()> {
let mut delete = db.prepare(
"DELETE FROM samples WHERE runner = ?1 AND at IN \
(SELECT at FROM samples WHERE runner = ?1 AND at < ?2 LIMIT ?3)",
)?;
for runner in runners {
batched(db, || delete.execute(params![runner, before, BATCH]))?;
}
Ok(())
}
fn logs(db: &Db<'_>, before: u64) -> rusqlite::Result<()> {
let jobs: Vec<u64> = db
.prepare(
"SELECT id FROM jobs WHERE finished_at < ?1 AND state IN ('completed', 'lost', 'elsewhere') \
AND EXISTS (SELECT 1 FROM log_lines WHERE job = id)",
)?
.query_map([before], |row| row.get(0))?
.collect::<rusqlite::Result<_>>()?;
delete_logs(db, &jobs)
}
fn delete_logs(db: &Db<'_>, jobs: &[u64]) -> rusqlite::Result<()> {
let mut delete = db.prepare(
"DELETE FROM log_lines WHERE job = ?1 AND line IN \
(SELECT line FROM log_lines WHERE job = ?1 LIMIT ?2)",
)?;
for job in jobs {
batched(db, || delete.execute(params![job, BATCH]))?;
}
Ok(())
}
fn jobs(db: &Db<'_>, before: u64) -> rusqlite::Result<()> {
let jobs: Vec<u64> = db
.prepare(
"SELECT id FROM jobs WHERE coalesce(finished_at, queued_at) < ?1 \
AND state IN ('completed', 'lost', 'elsewhere')",
)?
.query_map([before], |row| row.get(0))?
.collect::<rusqlite::Result<_>>()?;
delete_logs(db, &jobs)?;
let mut steps = db.prepare("DELETE FROM steps WHERE job = ?1")?;
let mut kept = db.prepare("DELETE FROM records WHERE job = ?1")?;
let mut records = db.prepare("DELETE FROM jobs WHERE id = ?1")?;
for chunk in jobs.chunks(usize::try_from(BATCH).unwrap_or(usize::MAX)) {
batched_once(db, || {
for job in chunk {
steps.execute([job])?;
kept.execute([job])?;
records.execute([job])?;
}
Ok(())
})?;
}
let runners: Vec<String> = db
.prepare(
"SELECT name FROM runners WHERE removed_at < ?1 \
AND name NOT IN (SELECT runner FROM jobs WHERE runner IS NOT NULL)",
)?
.query_map([before], |row| row.get(0))?
.collect::<rusqlite::Result<_>>()?;
delete_samples(db, &runners, before)?;
let mut delete = db.prepare("DELETE FROM runners WHERE name = ?1")?;
for chunk in runners.chunks(usize::try_from(BATCH).unwrap_or(usize::MAX)) {
batched_once(db, || {
for runner in chunk {
delete.execute([runner])?;
}
Ok(())
})?;
}
Ok(())
}
fn batched_once(
db: &Db<'_>,
delete: impl FnOnce() -> rusqlite::Result<()>,
) -> rusqlite::Result<()> {
if db.stopped() {
return Ok(());
}
let started = Instant::now();
let transaction = Transaction::new_unchecked(db, TransactionBehavior::Immediate)?;
delete()?;
transaction.commit()?;
std::thread::sleep(started.elapsed());
Ok(())
}