use super::*;
#[cfg(unix)]
#[async_trait]
pub trait DaemonDispatch: Clone + Send + Sync + 'static {
fn idle_retirement_blockers(&self) -> Vec<String> {
vec!["dispatcher_resource_inventory_unknown".to_owned()]
}
fn plan(&self, ops: &str) -> String;
#[allow(clippy::too_many_arguments)]
async fn dispatch(
&self,
ops: String,
presentation: Option<String>,
presentation_per_op: Option<Vec<Option<String>>>,
format: Option<String>,
format_per_op: Option<Vec<Option<String>>>,
from_wire: bool,
identity: Option<RequestIdentity>,
) -> Result<String, String>;
fn request_read_timeout(&self, _ops: &str) -> std::time::Duration {
khive_storage::request_read_timeout_from_env()
}
#[allow(clippy::too_many_arguments)]
async fn dispatch_with_error_detail(
&self,
ops: String,
presentation: Option<String>,
presentation_per_op: Option<Vec<Option<String>>>,
format: Option<String>,
format_per_op: Option<Vec<Option<String>>>,
from_wire: bool,
identity: Option<RequestIdentity>,
) -> Result<String, DaemonDispatchError> {
self.dispatch(
ops,
presentation,
presentation_per_op,
format,
format_per_op,
from_wire,
identity,
)
.await
.map_err(|message| DaemonDispatchError::new(message, None))
}
fn events_readiness(&self) -> Option<crate::events_split::EventsReadiness> {
None
}
async fn warm_all(&self);
fn namespace(&self) -> &str;
fn config_id(&self) -> &str;
fn pool_for_checkpoint(&self) -> Option<Arc<ConnectionPool>> {
None
}
fn secondary_pools_for_checkpoint(&self) -> Vec<Arc<ConnectionPool>> {
Vec::new()
}
fn event_store_for_checkpoint(&self) -> Option<Arc<dyn khive_storage::EventStore>> {
None
}
}
#[cfg(unix)]
pub(super) struct CheckpointTaskSpec {
pub(super) pool: Arc<ConnectionPool>,
pub(super) lifecycle_owner: Option<CheckpointLifecycleOwner>,
pub(super) is_main: bool,
}
#[cfg(unix)]
pub(super) fn checkpoint_task_specs(
main_pool: Option<Arc<ConnectionPool>>,
secondary_pools: Vec<Arc<ConnectionPool>>,
event_store: Option<Arc<dyn khive_storage::EventStore>>,
namespace: String,
) -> Vec<CheckpointTaskSpec> {
let mut tasks = Vec::with_capacity(usize::from(main_pool.is_some()) + secondary_pools.len());
if let Some(pool) = main_pool {
tasks.push(CheckpointTaskSpec {
pool,
lifecycle_owner: None,
is_main: true,
});
}
tasks.extend(secondary_pools.into_iter().map(|pool| CheckpointTaskSpec {
pool,
lifecycle_owner: None,
is_main: false,
}));
if let (Some(task), Some(event_store)) = (tasks.first_mut(), event_store) {
task.lifecycle_owner = Some(CheckpointLifecycleOwner::new(event_store, namespace));
}
tasks
}
pub(super) static WARM_INDEX_HOST: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
pub fn mark_warm_index_host() {
WARM_INDEX_HOST.store(true, std::sync::atomic::Ordering::Release);
}
pub fn is_warm_index_host() -> bool {
WARM_INDEX_HOST.load(std::sync::atomic::Ordering::Acquire)
}
pub(super) static BACKGROUND_TASKS: std::sync::OnceLock<Arc<std::sync::atomic::AtomicUsize>> =
std::sync::OnceLock::new();
pub(super) fn background_tasks() -> &'static Arc<std::sync::atomic::AtomicUsize> {
BACKGROUND_TASKS.get_or_init(|| Arc::new(std::sync::atomic::AtomicUsize::new(0)))
}
pub(super) static BACKGROUND_TASK_NAMES: std::sync::OnceLock<
std::sync::Mutex<std::collections::HashMap<&'static str, usize>>,
> = std::sync::OnceLock::new();
pub(super) fn background_task_names_registry(
) -> &'static std::sync::Mutex<std::collections::HashMap<&'static str, usize>> {
BACKGROUND_TASK_NAMES.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()))
}
pub const UNNAMED_BACKGROUND_TASK: &str = "unnamed";
pub(super) fn register_background_task_name(name: &'static str) {
let mut names = background_task_names_registry()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*names.entry(name).or_insert(0) += 1;
}
pub(super) fn release_background_task_name(name: &'static str) {
let mut names = background_task_names_registry()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(count) = names.get_mut(name) {
*count -= 1;
if *count == 0 {
names.remove(name);
}
}
}
pub fn background_task_names() -> Vec<String> {
let names = background_task_names_registry()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let mut out: Vec<String> = names.keys().map(|name| (*name).to_string()).collect();
out.sort();
out
}
#[cfg(unix)]
pub(super) fn idle_retirement_blockers<D: DaemonDispatch>(dispatcher: &D) -> Vec<String> {
let mut blockers = dispatcher.idle_retirement_blockers();
if !khive_storage::tx_registry::snapshot().is_empty() {
blockers.push("open_sql_transaction".to_owned());
}
blockers.extend(
active_phase_names()
.into_iter()
.map(|name| format!("active_phase:{name}")),
);
let count = background_task_count();
let names = background_task_names_registry()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if names.values().sum::<usize>() != count {
blockers.push("tracked_worker_inventory_unsettled".to_owned());
}
for name in names.keys() {
if !matches!(
*name,
"wal_checkpoint" | "memory_ann_rotation_watch" | "knowledge_ann_rotation_watch"
) {
blockers.push(format!("unsettled_worker:{name}"));
}
}
blockers.sort();
blockers.dedup();
blockers
}
#[cfg(unix)]
pub(super) async fn wait_for_idle<D: DaemonDispatch>(dispatcher: &D, lifecycle: &DaemonLifecycle) {
if lifecycle.options.lifetime == DaemonLifetime::Persistent {
std::future::pending::<()>().await;
}
loop {
if lifecycle.try_idle(|| idle_retirement_blockers(dispatcher)) {
return;
}
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
}
pub(super) struct BackgroundTaskGuard {
pub(super) counter: Arc<std::sync::atomic::AtomicUsize>,
pub(super) name: &'static str,
}
impl Drop for BackgroundTaskGuard {
fn drop(&mut self) {
release_background_task_name(self.name);
self.counter
.fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
}
}
pub fn spawn_tracked_task<F, T>(fut: F) -> tokio::task::JoinHandle<T>
where
F: std::future::Future<Output = T> + Send + 'static,
T: Send + 'static,
{
spawn_named_tracked_task(UNNAMED_BACKGROUND_TASK, fut)
}
pub fn spawn_named_tracked_task<F, T>(name: &'static str, fut: F) -> tokio::task::JoinHandle<T>
where
F: std::future::Future<Output = T> + Send + 'static,
T: Send + 'static,
{
background_tasks().fetch_add(1, std::sync::atomic::Ordering::SeqCst);
register_background_task_name(name);
let guard = BackgroundTaskGuard {
counter: background_tasks().clone(),
name,
};
tokio::spawn(async move {
let _guard = guard;
fut.await
})
}
pub fn track_background_task<F>(fut: F)
where
F: std::future::Future<Output = ()> + Send + 'static,
{
track_named_background_task(UNNAMED_BACKGROUND_TASK, fut);
}
pub fn track_named_background_task<F>(name: &'static str, fut: F)
where
F: std::future::Future<Output = ()> + Send + 'static,
{
drop(spawn_named_tracked_task(name, fut));
}
pub fn background_task_count() -> usize {
background_tasks().load(std::sync::atomic::Ordering::SeqCst)
}
pub fn daemon_shutdown_token() -> tokio_util::sync::CancellationToken {
static TOKEN: std::sync::OnceLock<tokio_util::sync::CancellationToken> =
std::sync::OnceLock::new();
TOKEN
.get_or_init(tokio_util::sync::CancellationToken::new)
.clone()
}
pub(super) static ACTIVE_PHASES: std::sync::OnceLock<
std::sync::Mutex<std::collections::HashMap<String, usize>>,
> = std::sync::OnceLock::new();
pub(super) fn active_phases() -> &'static std::sync::Mutex<std::collections::HashMap<String, usize>>
{
ACTIVE_PHASES.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()))
}
pub struct PhaseGuard {
pub(super) name: String,
}
impl Drop for PhaseGuard {
fn drop(&mut self) {
let mut map = active_phases()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(count) = map.get_mut(&self.name) {
*count -= 1;
if *count == 0 {
map.remove(&self.name);
}
}
}
}
pub fn register_active_phase(name: &str) -> PhaseGuard {
let mut map = active_phases()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*map.entry(name.to_string()).or_insert(0) += 1;
PhaseGuard {
name: name.to_string(),
}
}
pub fn active_phase_names() -> Vec<String> {
let map = active_phases()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let mut names: Vec<String> = map.keys().cloned().collect();
names.sort();
names
}
#[cfg(unix)]
pub(super) fn build_metrics_snapshot<D: DaemonDispatch>(dispatcher: &D) -> MetricsSnapshot {
let open_tx_count = khive_storage::tx_registry::snapshot().len();
let (oldest_pinned_tx_micros, oldest_pinned_tx_label) =
match khive_storage::tx_registry::oldest() {
Some((_id, age, label)) => (Some(age.as_micros() as u64), label),
None => (None, None),
};
let checkpoint_pool = dispatcher.pool_for_checkpoint();
let mut secondary_index = 0;
let wal_checkpoint_stores = checkpoint_task_specs(
checkpoint_pool.clone(),
dispatcher.secondary_pools_for_checkpoint(),
None,
String::new(),
)
.into_iter()
.map(|task| {
let (store_id, role) = if task.is_main {
("main".to_string(), "main".to_string())
} else {
let store_id = format!("secondary:{secondary_index}");
secondary_index += 1;
(store_id, "secondary".to_string())
};
CheckpointStoreMetrics {
store_id,
role,
database: task
.pool
.canonical_path()
.and_then(std::path::Path::file_name)
.map(|name| name.to_string_lossy().into_owned()),
timing: khive_db::checkpoint::checkpoint_timing(&task.pool),
}
})
.collect();
let routine_wal = checkpoint_pool
.as_deref()
.and_then(khive_db::checkpoint::routine_wal_observation);
let writer_stages = checkpoint_pool
.as_deref()
.and_then(khive_db::writer_task::last_writer_stage_observation);
let (write_queue_depth, write_queue_capacity) = checkpoint_pool
.as_ref()
.and_then(|pool| pool.writer_task_handle().ok().flatten())
.map(|handle| (Some(handle.queue_depth()), Some(handle.capacity())))
.unwrap_or((None, None));
MetricsSnapshot {
lifecycle: None,
wal_pages: routine_wal.as_ref().map(|sample| sample.log_frames),
wal_log_frames: routine_wal.as_ref().map(|sample| sample.log_frames),
wal_checkpointed_frames: routine_wal
.as_ref()
.map(|sample| sample.checkpointed_frames),
wal_pending_frames: routine_wal.as_ref().map(|sample| sample.pending_frames),
wal_physical_bytes: routine_wal
.as_ref()
.and_then(|sample| sample.physical_wal_bytes),
wal_observed_at_unix_ms: routine_wal
.as_ref()
.map(|sample| sample.observed_at_unix_ms),
wal_checkpoint_stores,
wal_truncate_attempts: khive_db::checkpoint::truncate_attempts(),
wal_truncate_consecutive_failures: khive_db::checkpoint::truncate_consecutive_failures(),
wal_checkpoint_skipped_ticks: khive_db::checkpoint::checkpoint_skipped_ticks(),
wal_checkpoint_consecutive_skips: khive_db::checkpoint::checkpoint_consecutive_skips(),
wal_checkpoint_last_skip_wal_pages: khive_db::checkpoint::checkpoint_last_skip_wal_pages(),
oldest_pinned_tx_micros,
oldest_pinned_tx_label,
open_tx_count,
write_queue_depth,
write_queue_capacity,
write_last_queue_wait_micros: writer_stages
.as_ref()
.map(|sample| sample.queue_wait_micros),
write_last_transaction_acquire_micros: writer_stages
.as_ref()
.map(|sample| sample.transaction_acquire_micros),
write_last_body_micros: writer_stages.as_ref().map(|sample| sample.body_micros),
write_last_commit_micros: writer_stages.as_ref().map(|sample| sample.commit_micros),
write_last_total_micros: writer_stages.as_ref().map(|sample| sample.total_micros),
write_last_observed_at_unix_ms: writer_stages
.as_ref()
.map(|sample| sample.observed_at_unix_ms),
connections: None,
recall_ledger: Some(recall_ledger_snapshot()),
}
}