use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::Instant;
use crate::pool::ConnectionPool;
#[cfg(doc)]
use super::run_checkpoint_task;
use super::{
log_tx_age_emission, ReaderCheckoutSweepState, SessionSweepConfig, TxAgeSweepState,
WalpinSidecarState, CHECKPOINT_LIFECYCLE_APPEND_ATTEMPTS, CHECKPOINT_LIFECYCLE_APPEND_FAILURES,
CHECKPOINT_LIFECYCLE_ENQUEUE_DROPS,
};
pub struct SweepBackend {
pub pool: Arc<ConnectionPool>,
pub is_main: bool,
}
struct BackendSweep {
pool: Arc<ConnectionPool>,
filter: khive_storage::tx_registry::TxOriginFilter,
tx_age_state: TxAgeSweepState,
reader_checkout_state: ReaderCheckoutSweepState,
sidecar: Option<WalpinSidecarState>,
}
pub async fn run_session_sweep_task(
backends: Vec<SweepBackend>,
config: SessionSweepConfig,
mut shutdown_rx: tokio::sync::watch::Receiver<()>,
) {
let mut interval = tokio::time::interval(config.interval);
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
let mut sweeps: Vec<BackendSweep> = Vec::with_capacity(backends.len());
for backend in backends {
let identity = match backend.pool.origin() {
khive_storage::tx_registry::TxOrigin::Database(id) => id,
khive_storage::tx_registry::TxOrigin::Memory
| khive_storage::tx_registry::TxOrigin::Unscoped => continue,
};
let filter = if backend.is_main {
khive_storage::tx_registry::TxOriginFilter::Main(identity)
} else {
khive_storage::tx_registry::TxOriginFilter::Secondary(identity)
};
let sidecar = WalpinSidecarState::new(
backend.pool.canonical_path(),
true,
"session",
config.interval,
);
sweeps.push(BackendSweep {
pool: backend.pool,
filter,
tx_age_state: TxAgeSweepState::default(),
reader_checkout_state: ReaderCheckoutSweepState::default(),
sidecar,
});
}
for sweep in sweeps.iter_mut() {
if let Some(sidecar) = sweep.sidecar.as_mut() {
sidecar.register_beacon().await;
}
}
loop {
tokio::select! {
_ = interval.tick() => {}
_ = shutdown_rx.changed() => break,
}
for sweep in sweeps.iter_mut() {
sweep
.reader_checkout_state
.sweep_at(&sweep.pool, Instant::now());
let oldest = khive_storage::tx_registry::oldest_for(&sweep.filter);
for emission in sweep.tx_age_state.observe(
oldest.as_ref().map(|s| (s.id, s.age, s.label.clone())),
config.tx_warn_secs,
config.tx_max_age_secs,
) {
log_tx_age_emission(&emission);
}
if let Some(sidecar) = sweep.sidecar.as_mut() {
sidecar.observe(oldest, config.tx_warn_secs).await;
}
}
}
for sweep in sweeps.iter_mut() {
if let Some(sidecar) = sweep.sidecar.as_mut() {
sidecar.shutdown().await;
}
}
}
#[derive(Clone)]
pub struct CheckpointLifecycleOwner {
event_store: Arc<dyn khive_storage::EventStore>,
namespace: String,
}
impl CheckpointLifecycleOwner {
pub fn new(
event_store: Arc<dyn khive_storage::EventStore>,
namespace: impl Into<String>,
) -> Self {
Self {
event_store,
namespace: namespace.into(),
}
}
}
const CHECKPOINT_LIFECYCLE_QUEUE_CAPACITY: usize = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct CheckpointPressureEpisode {
pub(super) elevated_ticks: u64,
pub(super) peak_wal_pages: u64,
}
impl CheckpointPressureEpisode {
pub(super) fn start(wal_pages: u64) -> Self {
Self {
elevated_ticks: 1,
peak_wal_pages: wal_pages,
}
}
pub(super) fn observe(&mut self, wal_pages: u64) {
self.elevated_ticks = self.elevated_ticks.saturating_add(1);
self.peak_wal_pages = self.peak_wal_pages.max(wal_pages);
}
}
pub(super) struct CheckpointLifecycleEmitter {
namespace: Option<String>,
sender: Option<tokio::sync::mpsc::Sender<khive_storage::Event>>,
worker: Option<tokio::task::JoinHandle<()>>,
busy_warning_emitted: bool,
}
impl CheckpointLifecycleEmitter {
pub(super) fn new(owner: Option<CheckpointLifecycleOwner>) -> Self {
let Some(owner) = owner else {
return Self {
namespace: None,
sender: None,
worker: None,
busy_warning_emitted: false,
};
};
let namespace = owner.namespace.clone();
let (sender, mut receiver) =
tokio::sync::mpsc::channel::<khive_storage::Event>(CHECKPOINT_LIFECYCLE_QUEUE_CAPACITY);
let worker = tokio::spawn(async move {
while let Some(event) = receiver.recv().await {
let kind = event.kind;
CHECKPOINT_LIFECYCLE_APPEND_ATTEMPTS.fetch_add(1, Ordering::Relaxed);
if let Err(err) = owner.event_store.append_event(event).await {
CHECKPOINT_LIFECYCLE_APPEND_FAILURES.fetch_add(1, Ordering::Relaxed);
tracing::warn!(target: "khive_db::checkpoint",
error = %err,
event_kind = %kind.name(),
"checkpoint lifecycle event append failed"
);
}
}
});
Self {
namespace: Some(namespace),
sender: Some(sender),
worker: Some(worker),
busy_warning_emitted: false,
}
}
pub(super) fn try_emit<P: serde::Serialize>(
&mut self,
kind: khive_types::EventKind,
payload: P,
) -> bool {
let (Some(namespace), Some(sender)) = (&self.namespace, &self.sender) else {
return true;
};
let payload_value = match serde_json::to_value(&payload) {
Ok(value) => value,
Err(err) => {
tracing::warn!(target: "khive_db::checkpoint",
error = %err,
event_kind = %kind.name(),
"failed to serialize checkpoint lifecycle event payload"
);
CHECKPOINT_LIFECYCLE_ENQUEUE_DROPS.fetch_add(1, Ordering::Relaxed);
return false;
}
};
let payload_schema_version = match kind {
khive_types::EventKind::CheckpointOutcomeRecorded => 2,
_ => 1,
};
let event = khive_storage::Event::new(
namespace,
"checkpoint.lifecycle",
kind,
khive_types::SubstrateKind::Event,
"daemon:checkpoint_task",
)
.with_payload(payload_value)
.with_payload_schema_version(payload_schema_version);
match sender.try_send(event) {
Ok(()) => {
self.busy_warning_emitted = false;
true
}
Err(tokio::sync::mpsc::error::TrySendError::Full(event)) => {
CHECKPOINT_LIFECYCLE_ENQUEUE_DROPS.fetch_add(1, Ordering::Relaxed);
if !self.busy_warning_emitted {
tracing::warn!(target: "khive_db::checkpoint",
event_kind = %event.kind.name(),
queue_capacity = CHECKPOINT_LIFECYCLE_QUEUE_CAPACITY,
"checkpoint lifecycle event dropped because the append worker is busy"
);
self.busy_warning_emitted = true;
}
false
}
Err(tokio::sync::mpsc::error::TrySendError::Closed(event)) => {
CHECKPOINT_LIFECYCLE_ENQUEUE_DROPS.fetch_add(1, Ordering::Relaxed);
tracing::warn!(target: "khive_db::checkpoint",
event_kind = %event.kind.name(),
"checkpoint lifecycle event dropped because the append worker stopped"
);
false
}
}
}
pub(super) async fn shutdown(mut self) {
drop(self.sender.take());
let Some(worker) = self.worker.take() else {
return;
};
worker.abort();
match worker.await {
Ok(()) => {}
Err(err) if err.is_cancelled() => {}
Err(err) => tracing::warn!(target: "khive_db::checkpoint",
error = %err,
"checkpoint lifecycle event append worker terminated unexpectedly"
),
}
}
}
impl Drop for CheckpointLifecycleEmitter {
fn drop(&mut self) {
if let Some(worker) = &self.worker {
worker.abort();
}
}
}
pub(super) struct CheckpointConnection {
pub(super) conn: Option<rusqlite::Connection>,
consecutive_open_failures: u32,
}
impl CheckpointConnection {
pub(super) fn new() -> Self {
Self {
conn: None,
consecutive_open_failures: 0,
}
}
pub(super) fn ensure_open(&mut self, pool: &ConnectionPool) -> Option<&rusqlite::Connection> {
if self.conn.is_none() {
match pool.open_standalone_writer_untracked() {
Ok(conn) => {
if let Err(e) = conn.pragma_update(None, "wal_autocheckpoint", 0) {
tracing::warn!(target: "khive_db::checkpoint",
error = %e,
"could not disable autocheckpoint on the dedicated checkpoint \
connection"
);
}
if self.consecutive_open_failures > 0 {
tracing::info!(target: "khive_db::checkpoint",
prior_consecutive_failures = self.consecutive_open_failures,
"dedicated checkpoint connection opened successfully, ending a \
failure streak"
);
}
self.consecutive_open_failures = 0;
self.conn = Some(conn);
}
Err(e) => {
if self.consecutive_open_failures == 0 {
tracing::warn!(target: "khive_db::checkpoint",
error = %e,
"failed to open the dedicated checkpoint connection; \
this tick is skipped and the open retried next tick"
);
} else {
tracing::debug!(target: "khive_db::checkpoint",
error = %e,
consecutive_failures = self.consecutive_open_failures,
"dedicated checkpoint connection still unavailable; \
this tick is skipped and the open retried next tick"
);
}
self.consecutive_open_failures =
self.consecutive_open_failures.saturating_add(1);
return None;
}
}
}
self.conn.as_ref()
}
}