use std::collections::HashSet;
use std::io::Write;
use std::sync::atomic::{AtomicBool, Ordering};
use crate::error::Error;
pub(crate) type EntryId = String;
#[derive(Debug, Clone)]
pub(crate) struct Entry {
pub id: EntryId,
pub timestamp: i64,
pub is_oo_entry: bool,
}
pub(crate) trait ProjectEntries {
fn list(&self, limit: usize) -> Result<Vec<Entry>, Error>;
fn list_ids(&self, limit: usize) -> Result<Vec<EntryId>, Error>;
fn delete(&self, id: &str) -> Result<bool, Error>;
}
pub(crate) const LIST_CAP: usize = 10_000;
pub(crate) const MAX_PASSES: usize = 1_000;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct CleanupOutcome {
pub deleted: usize,
pub failed: usize,
pub window_capped: bool,
}
pub(crate) fn stale_cutoff(now: i64, max_age_secs: i64) -> i64 {
now.saturating_sub(max_age_secs)
}
pub(crate) fn partial_delete_error(deleted: usize, failed: usize) -> Error {
Error::Store(format!(
"partial delete: deleted {deleted}, failed {failed}; entries remain"
))
}
fn detail(e: &Error) -> String {
match e {
Error::Store(msg) => msg.clone(),
other => other.to_string(),
}
}
fn warn_delete_failed(id: &str, e: &Error) {
eprintln!("oo: warning: failed to delete entry {id}: {}", detail(e));
}
pub(crate) fn drain_project(
store: &dyn ProjectEntries,
page: usize,
max_passes: usize,
) -> Result<usize, Error> {
let mut deleted = 0usize;
let mut failed: HashSet<EntryId> = HashSet::new();
let mut previous_pass_gone = false;
for _ in 0..max_passes {
let ids = match store.list_ids(page) {
Ok(ids) => ids,
Err(e) => {
return Err(Error::Store(format!(
"partial delete: deleted {deleted}, failed {}; entries may remain; listing failed: {}",
failed.len(),
detail(&e)
)));
}
};
if ids.is_empty() {
return Ok(deleted);
}
let mut deletions = 0usize;
let mut pass_errors = 0usize;
for id in ids {
match store.delete(&id) {
Ok(true) => {
deleted += 1;
deletions += 1;
failed.remove(&id);
}
Ok(false) => {
failed.remove(&id);
}
Err(e) => {
pass_errors += 1;
if failed.insert(id.clone()) {
warn_delete_failed(&id, &e);
}
}
}
}
if deletions == 0 && (pass_errors > 0 || previous_pass_gone) {
return Err(partial_delete_error(deleted, failed.len()));
}
previous_pass_gone = deletions == 0;
}
Err(partial_delete_error(deleted, failed.len()))
}
pub(crate) fn cleanup_stale_in(
store: &dyn ProjectEntries,
cutoff: i64,
page: usize,
) -> Result<CleanupOutcome, Error> {
let entries = store.list(page)?;
let mut out = CleanupOutcome {
deleted: 0,
failed: 0,
window_capped: entries.len() == page,
};
for entry in entries {
if !entry.is_oo_entry || entry.timestamp >= cutoff {
continue;
}
match store.delete(&entry.id) {
Ok(true) => out.deleted += 1,
Ok(false) => {}
Err(e) => {
out.failed += 1;
warn_delete_failed(&entry.id, &e);
}
}
}
Ok(out)
}
pub(crate) fn warn_window_capped_once(out: &mut dyn Write, gate: &AtomicBool) {
if gate.swap(true, Ordering::SeqCst) {
return;
}
let _ = writeln!(
out,
"oo: warning: project has 10000 or more entries; stale cleanup only inspected the newest 10000"
);
}
#[cfg(test)]
#[path = "project_drain_tests.rs"]
mod tests;