use std::sync::{Arc, Mutex, PoisonError};
use std::time::Instant;
use haematite::db::ShardReclaimAnswer;
use haematite::{Database, EventStore, ReclaimPassOutcome};
pub const WATCH_THREAD: &str = "liminal-spine-reclaim-watch";
pub const RELEASE_THREAD: &str = "liminal-spine-reclaim-release";
pub fn release_after_bind(spine: &Arc<EventStore>, began: Instant) {
let spine = Arc::clone(spine);
off_the_start_path(move || release(&spine, began));
}
pub fn off_the_start_path<F: FnOnce() + Send + 'static>(work: F) {
let shared = Arc::new(Mutex::new(Some(work)));
let taken = Arc::clone(&shared);
let spawned = std::thread::Builder::new()
.name(RELEASE_THREAD.to_owned())
.spawn(move || run_once(&taken));
if let Err(error) = spawned {
tracing::warn!(
error = %error,
"spine reclaim release thread did not start; released on the start path"
);
run_once(&shared);
}
}
fn run_once<F: FnOnce()>(work: &Mutex<Option<F>>) {
let taken = work.lock().unwrap_or_else(PoisonError::into_inner).take();
if let Some(work) = taken {
work();
}
}
pub fn arm_watch(database: &Database) {
let capacity = database.shard_count().saturating_mul(2).max(1);
let watch = match database.watch_reclaim_passes(capacity) {
Ok(watch) => watch,
Err(error) => {
tracing::warn!(error = %error, "spine reclaim pass watch refused");
return;
}
};
let armed_at = Instant::now();
let spawned = std::thread::Builder::new()
.name(WATCH_THREAD.to_owned())
.spawn(move || {
while let Ok(report) = watch.recv() {
let (outcome, failed_reason) = match &report.outcome {
ReclaimPassOutcome::Passed => ("passed", None),
ReclaimPassOutcome::Failed { reason } => ("failed", Some(reason.clone())),
};
tracing::info!(
shard = report.shard_id,
outcome,
passes_finished = report.passes_finished,
since_start_ms =
u64::try_from(armed_at.elapsed().as_millis()).unwrap_or(u64::MAX),
unmarked_bytes = report.unmarked_bytes,
retained_bytes = report.retained_bytes,
peak_bytes = report.peak,
ended_at_ms = report.ended_at_ms,
failed_steps = failed_reason,
"spine reclaim pass ended"
);
}
});
if let Err(error) = spawned {
tracing::warn!(error = %error, "spine reclaim pass watch thread did not start");
}
}
pub fn release(spine: &EventStore, began: Instant) {
let database = spine.database();
if let Err(error) = database.release_reclaim() {
tracing::warn!(error = %error, "spine reclaim release refused");
return;
}
let status = database.reclaim_status();
let phases: Vec<String> = status
.shards
.iter()
.map(|shard| match shard {
ShardReclaimAnswer::Answered { shard, status } => {
format!("{shard}:{:?}", status.phase)
}
ShardReclaimAnswer::NotAnswering { shard, reason } => {
format!("{shard}:not-answering({reason})")
}
})
.collect();
tracing::info!(
boot_elapsed_ms = u64::try_from(began.elapsed().as_millis()).unwrap_or(u64::MAX),
phases = %phases.join(" "),
"spine reclaim released"
);
for shard in &status.shards {
if let ShardReclaimAnswer::Answered { shard, status } = shard {
log_resume(*shard, status.resume.as_deref());
}
}
}
const RESUME_TAKEN: &str = "reclaim_resume_taken";
fn log_resume(shard: usize, resume: Option<&str>) {
match resume {
Some(note) if note.starts_with(RESUME_TAKEN) => {
tracing::info!(shard, resume = note, "spine reclaim resumed");
}
note => tracing::info!(
shard,
resume = note.unwrap_or("none"),
"spine reclaim starts from nothing"
),
}
}