use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::Instant;
use crate::pool::ConnectionPool;
#[cfg(unix)]
use super::{
complete_walpin_attribution, run_walpin_housekeeping_if_due, WalpinSidecarState,
DEFAULT_SESSION_SWEEP_INTERVAL,
};
use super::{
crossing_warn, log_tx_age_emission, log_tx_registry_oldest_debug, log_tx_registry_oldest_warn,
log_tx_registry_snapshot_warn, log_wal_high_water_warn, note_checkpoint_pressure_observation,
note_checkpoint_skipped, off_worker, CheckpointConfig, CheckpointConnection,
CheckpointLifecycleEmitter, CheckpointLifecycleOwner, CheckpointPressureEpisode,
CheckpointRunTaskGuard, CheckpointSeverityRung, CheckpointSeverityState, CheckpointTick,
ReaderCheckoutSweepState, TruncateState, TxAgeSweepState, CHECKPOINT_LIFECYCLE_ENQUEUE_DROPS,
};
pub(super) async fn run_fts_maintenance_off_worker(
conn: rusqlite::Connection,
config: crate::fts_maintenance::FtsMaintenanceConfig,
mut state: crate::fts_maintenance::FtsMaintenanceState,
now: Instant,
) -> Result<
(
rusqlite::Connection,
crate::fts_maintenance::FtsMaintenanceState,
Result<Option<crate::fts_maintenance::FtsMaintenanceStep>, String>,
),
tokio::task::JoinError,
> {
tokio::task::spawn_blocking(move || {
let result = crate::fts_maintenance::run_if_due(&conn, &config, &mut state, now);
(conn, state, result)
})
.await
}
pub async fn run_checkpoint_task(
pool: Arc<ConnectionPool>,
config: CheckpointConfig,
lifecycle_owner: Option<CheckpointLifecycleOwner>,
mut shutdown_rx: tokio::sync::watch::Receiver<()>,
is_main: bool,
) {
let _checkpoint_run_guard = CheckpointRunTaskGuard::start(&pool, config.interval);
match pool.claim_checkpoint_ownership() {
Ok(()) => {
if let Err(e) = pool.propagate_checkpoint_claim_to_writer_task().await {
tracing::warn!(target: "khive_db::checkpoint",
error = %e,
"checkpoint task could not reach the writer task's connection; it keeps the \
bounded autocheckpoint fallback"
);
}
}
Err(e) => {
tracing::warn!(target: "khive_db::checkpoint",
error = %e,
"checkpoint task could not re-apply the ownership pragma on the pooled writer; \
writer connections keep the bounded autocheckpoint fallback unless ownership is \
claimed later"
);
}
}
let mut interval = tokio::time::interval(config.interval);
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
let mut severity_state = CheckpointSeverityState::default();
let mut tx_age_state = TxAgeSweepState::default();
let mut reader_checkout_state = ReaderCheckoutSweepState::default();
let mut was_above_high_water = false;
#[cfg(unix)]
let legacy_walpin_fallback_interval = DEFAULT_SESSION_SWEEP_INTERVAL;
#[cfg(unix)]
let mut truncate_state =
TruncateState::with_legacy_walpin_fallback(legacy_walpin_fallback_interval);
#[cfg(not(unix))]
let mut truncate_state = TruncateState::default();
let mut lifecycle_emitter = CheckpointLifecycleEmitter::new(lifecycle_owner);
let mut event_elevation_open = false;
let mut pressure_episode: Option<CheckpointPressureEpisode> = None;
let mut pending_recovery: Option<khive_storage::CheckpointOutcomeRecordedPayload> = None;
let mut was_observed_above_warn = false;
let tx_filter = match pool.origin() {
khive_storage::tx_registry::TxOrigin::Database(id) => Some(if is_main {
khive_storage::tx_registry::TxOriginFilter::Main(id)
} else {
khive_storage::tx_registry::TxOriginFilter::Secondary(id)
}),
khive_storage::tx_registry::TxOrigin::Memory
| khive_storage::tx_registry::TxOrigin::Unscoped => None,
};
#[cfg(unix)]
let mut walpin_state =
WalpinSidecarState::new(pool.canonical_path(), true, "daemon", config.interval);
#[cfg(unix)]
if let Some(sidecar) = walpin_state.as_mut() {
sidecar.register_beacon().await;
}
let mut checkpoint_conn = CheckpointConnection::new();
checkpoint_conn.ensure_open(&pool);
let mut fts_maintenance_config = crate::fts_maintenance::FtsMaintenanceConfig::from_env();
fts_maintenance_config.enabled &= is_main;
let mut fts_maintenance_state =
crate::fts_maintenance::FtsMaintenanceState::new(Instant::now());
loop {
tokio::select! {
_ = interval.tick() => {}
_ = shutdown_rx.changed() => break,
}
#[cfg(unix)]
truncate_state.begin_tick();
#[cfg(unix)]
let mut pending_sidecar_attribution = None;
let tick = if checkpoint_conn.ensure_open(&pool).is_none() {
note_checkpoint_skipped();
CheckpointTick::Skipped
} else {
let conn = checkpoint_conn
.conn
.take()
.expect("ensure_open just confirmed a connection is open");
match off_worker::run_checkpoint_core_off_worker(
Arc::clone(&pool),
conn,
config.clone(),
truncate_state,
)
.await
{
Ok((conn, state, Ok(outcome))) => {
truncate_state = state;
#[cfg(unix)]
{
pending_sidecar_attribution = outcome.sidecar_attribution;
}
#[cfg(not(unix))]
let _ = outcome.sidecar_attribution;
let fts_result = if !fts_maintenance_state
.is_due(&fts_maintenance_config, Instant::now())
{
checkpoint_conn.conn = Some(conn);
Ok(None)
} else {
match run_fts_maintenance_off_worker(
conn,
fts_maintenance_config.clone(),
fts_maintenance_state,
Instant::now(),
)
.await
{
Ok((conn, state, fts_result)) => {
fts_maintenance_state = state;
checkpoint_conn.conn = Some(conn);
fts_result
}
Err(join_err) => {
fts_maintenance_state =
crate::fts_maintenance::FtsMaintenanceState::new(Instant::now());
Err(format!(
"bounded FTS5 segment maintenance task panicked: {join_err}"
))
}
}
};
match fts_result {
Ok(Some(step)) => match step.outcome {
crate::fts_maintenance::FtsMaintenanceOutcome::Worked => {
tracing::info!(target: "khive_db::checkpoint",
table = step.table,
requested_pages = step.requested_pages,
segments_before = step.segments_before,
segments_after = step.segments_after,
"bounded FTS5 segment maintenance made progress"
);
}
crate::fts_maintenance::FtsMaintenanceOutcome::Busy => {
tracing::debug!(target: "khive_db::checkpoint",
table = step.table,
requested_pages = step.requested_pages,
segments = step.segments_before,
"bounded FTS5 segment maintenance skipped a busy writer"
);
}
crate::fts_maintenance::FtsMaintenanceOutcome::Noop
| crate::fts_maintenance::FtsMaintenanceOutcome::BelowThreshold => {
tracing::debug!(target: "khive_db::checkpoint",
table = step.table,
outcome = ?step.outcome,
segments = step.segments_before,
"bounded FTS5 segment maintenance had no work"
);
}
},
Ok(None) => {}
Err(error) => {
tracing::warn!(target: "khive_db::checkpoint",
error = %error,
"bounded FTS5 segment maintenance failed"
);
}
}
match outcome.wal_pages {
Some(wal_pages) => CheckpointTick::Observed(wal_pages),
None => {
note_checkpoint_skipped();
CheckpointTick::Skipped
}
}
}
Ok((_conn, state, Err(e))) => {
truncate_state = state;
tracing::warn!(target: "khive_db::checkpoint",
error = %e,
"dedicated checkpoint connection failed a pragma; \
dropping it for a fresh reopen next tick"
);
note_checkpoint_skipped();
CheckpointTick::Skipped
}
Err(panicked) => {
truncate_state = panicked.truncate_state;
tracing::warn!(target: "khive_db::checkpoint",
error = %panicked.join_error,
"WAL checkpoint cycle panicked on its blocking thread; \
dropping the connection for a fresh reopen next tick"
);
note_checkpoint_skipped();
CheckpointTick::Skipped
}
}
};
#[cfg(unix)]
if let Err(error) =
complete_walpin_attribution(pending_sidecar_attribution, &mut truncate_state).await
{
tracing::warn!(target: "khive_db::checkpoint",
error = %error,
failure_kind = error.kind(),
"ADR-091 Amendment 2 Plank B: no-progress sidecar attribution failed"
);
}
reader_checkout_state.sweep_at(&pool, Instant::now());
let oldest_tx = tx_filter
.as_ref()
.and_then(khive_storage::tx_registry::oldest_for);
for emission in tx_age_state.observe(
oldest_tx.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);
}
#[cfg(unix)]
if let Some(sidecar) = walpin_state.as_mut() {
sidecar
.observe(oldest_tx.clone(), config.tx_warn_secs)
.await;
let _ = run_walpin_housekeeping_if_due(
sidecar,
&mut truncate_state,
legacy_walpin_fallback_interval,
)
.await;
}
let wal_pages = match tick {
CheckpointTick::Skipped => continue,
CheckpointTick::Observed(n) => n,
};
let above_warn = wal_pages >= config.warn_pages;
let above_high_water = wal_pages >= config.high_water_pages;
let above_truncate_high_water = wal_pages >= config.truncate_high_water_pages;
note_checkpoint_pressure_observation(above_warn, was_observed_above_warn);
was_observed_above_warn = above_warn;
log_tx_registry_oldest_debug(wal_pages, oldest_tx.as_ref());
for emission in severity_state.observe_wal_pages(wal_pages, &config) {
match emission.rung {
CheckpointSeverityRung::Info => {
log_tx_registry_oldest_warn(wal_pages, oldest_tx.as_ref());
tracing::info!(target: "khive_db::checkpoint",
wal_pages = emission.wal_pages,
warn_threshold = emission.threshold_pages,
"WAL page count crossed warn threshold"
);
}
CheckpointSeverityRung::Warn => {
tracing::warn!(target: "khive_db::checkpoint",
wal_pages = emission.wal_pages,
warn_threshold = emission.threshold_pages,
consecutive_cycles = emission.consecutive_cycles,
"WAL page count failed to drain below warn threshold"
);
}
CheckpointSeverityRung::Alarm => {
}
}
}
let high_water_crossed = crossing_warn(above_high_water, &mut was_above_high_water);
if high_water_crossed {
log_tx_registry_snapshot_warn(wal_pages);
log_wal_high_water_warn(
wal_pages,
config.high_water_pages,
oldest_tx.as_ref(),
config.tx_warn_secs,
);
}
observe_checkpoint_pressure_tick(
above_warn,
wal_pages,
above_high_water,
above_truncate_high_water,
&config,
&mut event_elevation_open,
&mut pressure_episode,
&mut pending_recovery,
|payload| {
lifecycle_emitter
.try_emit(khive_types::EventKind::CheckpointOutcomeRecorded, payload)
},
);
}
lifecycle_emitter.shutdown().await;
#[cfg(unix)]
if let Some(sidecar) = walpin_state.as_mut() {
sidecar.shutdown().await;
}
}
pub(super) fn checkpoint_outcome_should_emit(above_warn: bool, was_elevated: bool) -> bool {
above_warn != was_elevated
}
#[allow(clippy::too_many_arguments)]
pub(super) fn observe_checkpoint_pressure_tick(
above_warn: bool,
wal_pages: u64,
above_high_water: bool,
above_truncate_high_water: bool,
config: &CheckpointConfig,
event_elevation_open: &mut bool,
pressure_episode: &mut Option<CheckpointPressureEpisode>,
pending_recovery: &mut Option<khive_storage::CheckpointOutcomeRecordedPayload>,
mut try_emit: impl FnMut(khive_storage::CheckpointOutcomeRecordedPayload) -> bool,
) {
let pending_blocks_emission = if let Some(payload) = pending_recovery.clone() {
if try_emit(payload) {
*pending_recovery = None;
false
} else {
true
}
} else {
false
};
if above_warn {
match pressure_episode.as_mut() {
Some(episode) => episode.observe(wal_pages),
None => *pressure_episode = Some(CheckpointPressureEpisode::start(wal_pages)),
}
} else if !*event_elevation_open {
if pending_blocks_emission && pressure_episode.is_some() {
CHECKPOINT_LIFECYCLE_ENQUEUE_DROPS.fetch_add(1, Ordering::Relaxed);
tracing::warn!(target: "khive_db::checkpoint",
wal_pages,
"checkpoint pressure episode elapsed unreported behind an undelivered recovery summary"
);
}
*pressure_episode = None;
}
if pending_blocks_emission || !checkpoint_outcome_should_emit(above_warn, *event_elevation_open)
{
return;
}
let Some(episode) = *pressure_episode else {
tracing::warn!(target: "khive_db::checkpoint",
above_warn,
event_elevation_open = *event_elevation_open,
"checkpoint pressure transition has no episode aggregate"
);
return;
};
let payload = khive_storage::CheckpointOutcomeRecordedPayload {
wal_pages,
warn_pages: config.warn_pages,
high_water_pages: config.high_water_pages,
truncate_high_water_pages: config.truncate_high_water_pages,
above_warn,
above_high_water,
above_truncate_high_water,
episode_elevated_ticks: Some(episode.elevated_ticks),
episode_peak_wal_pages: Some(episode.peak_wal_pages),
};
if try_emit(payload.clone()) {
*event_elevation_open = above_warn;
if !above_warn {
*pressure_episode = None;
}
} else if !above_warn {
debug_assert!(
pending_recovery.is_none(),
"recovery emission attempted while an earlier summary was still pending"
);
*event_elevation_open = false;
*pressure_episode = None;
*pending_recovery = Some(payload);
}
}