use serde::{Deserialize, Serialize};
#[cfg(unix)]
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[cfg(doc)]
use super::PROTOCOL_VERSION;
use super::{ConnectionCapSnapshot, DaemonLifecycleSnapshot, RecallLedgerSnapshot};
#[cfg(unix)]
use super::{DAEMON_LEXICAL_TIMEOUT_MARKER, MAX_FRAME_BYTES};
#[derive(Serialize, Deserialize, Default)]
pub struct DaemonRequestFrame {
pub ops: String,
#[serde(default)]
pub plan: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub presentation: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub presentation_per_op: Option<Vec<Option<String>>>,
pub namespace: String,
#[serde(default)]
pub actor_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub process_ref: Option<String>,
#[serde(default)]
pub visible_namespaces: Vec<String>,
#[serde(default)]
pub config_id: String,
#[serde(default)]
pub protocol_version: u32,
#[serde(default)]
pub probe_only: bool,
#[serde(default)]
pub metrics_only: bool,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub format_per_op: Option<Vec<Option<String>>>,
#[serde(default)]
pub from_wire: bool,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub request_id: Option<u64>,
}
#[derive(Debug, Clone)]
pub struct DaemonDispatchError {
pub message: String,
pub error_detail: serde_json::Value,
}
pub const ERROR_DETAIL_NESTING_DEPTH_LIMIT: usize = 64;
fn error_detail_value_within_limit(value: &serde_json::Value) -> bool {
let mut pending = vec![(value, 0_usize)];
while let Some((value, depth)) = pending.pop() {
match value {
serde_json::Value::Array(items) if depth < ERROR_DETAIL_NESTING_DEPTH_LIMIT => {
pending.extend(items.iter().map(|child| (child, depth + 1)));
}
serde_json::Value::Object(fields) if depth < ERROR_DETAIL_NESTING_DEPTH_LIMIT => {
pending.extend(fields.values().map(|child| (child, depth + 1)));
}
serde_json::Value::Array(_) | serde_json::Value::Object(_) => return false,
_ => {}
}
}
true
}
fn drop_error_detail_iteratively(value: serde_json::Value) {
let mut pending = vec![value];
while let Some(value) = pending.pop() {
match value {
serde_json::Value::Array(items) => pending.extend(items),
serde_json::Value::Object(fields) => pending.extend(fields.into_values()),
_ => {}
}
}
}
impl DaemonDispatchError {
pub fn new(message: impl Into<String>, error_detail: Option<serde_json::Value>) -> Self {
let message = message.into();
let mut fields = match error_detail {
Some(serde_json::Value::Object(fields)) => fields,
Some(data) => serde_json::Map::from_iter([("data".to_string(), data)]),
None => serde_json::Map::new(),
};
let disposition = match fields
.get("domain_disposition")
.and_then(serde_json::Value::as_str)
{
Some("committed") => crate::DomainDisposition::Committed,
Some("not_committed") => crate::DomainDisposition::NotCommitted,
_ => crate::DomainDisposition::Unknown,
};
if disposition != crate::DomainDisposition::Committed {
if let Some(result) = fields.remove("domain_result") {
drop_error_detail_iteratively(result);
}
}
let rejected: Vec<String> = fields
.iter()
.filter(|(_, value)| !error_detail_value_within_limit(value))
.map(|(name, _)| name.clone())
.collect();
let omitted_result = rejected.iter().any(|name| name == "domain_result");
let omitted_detail = !rejected.is_empty();
for name in rejected {
if let Some(value) = fields.remove(&name) {
drop_error_detail_iteratively(value);
}
}
let mut error_detail = serde_json::Value::Object(fields);
if error_detail["kind"].as_str().is_none() {
error_detail["kind"] = serde_json::json!("internal");
}
if error_detail["message"].as_str().is_none() {
error_detail["message"] = serde_json::json!(message);
}
error_detail["domain_disposition"] = serde_json::json!(disposition.as_str());
if omitted_detail {
error_detail["code"] = serde_json::json!(if omitted_result {
"result_too_deep"
} else {
"error_detail_too_deep"
});
}
Self {
message,
error_detail,
}
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct DaemonResponseFrame {
pub ok: bool,
pub result: Option<String>,
pub error: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error_detail: Option<serde_json::Value>,
pub namespace_mismatch: bool,
#[serde(default)]
pub config_mismatch: bool,
#[serde(default)]
pub served_config_id: Option<String>,
#[serde(default)]
pub version_mismatch: bool,
#[serde(default)]
pub daemon_protocol_version: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metrics: Option<MetricsSnapshot>,
#[serde(default)]
pub request_id: Option<u64>,
}
#[cfg(unix)]
pub(super) fn take_daemon_lexical_timeout_marker(
raw: String,
) -> (String, Option<serde_json::Value>) {
if !raw.contains(DAEMON_LEXICAL_TIMEOUT_MARKER) {
return (raw, None);
}
let Ok(mut value) = serde_json::from_str::<serde_json::Value>(&raw) else {
return (raw, None);
};
let Some(fields) = value.as_object_mut() else {
return (raw, None);
};
if !fields
.get("results")
.is_some_and(serde_json::Value::is_array)
{
return (raw, None);
}
let Some(marker) = fields.remove(DAEMON_LEXICAL_TIMEOUT_MARKER) else {
return (raw, None);
};
let detail =
(marker.as_bool() == Some(true)).then(|| serde_json::json!({"lexical_timeout": true}));
(
serde_json::to_string(&value).expect("serde_json::Value is serializable"),
detail,
)
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
#[serde(default)]
pub struct CheckpointStoreMetrics {
pub store_id: String,
pub role: String,
pub database: Option<String>,
#[serde(flatten)]
pub timing: khive_db::checkpoint::CheckpointTiming,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
pub struct MetricsSnapshot {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lifecycle: Option<DaemonLifecycleSnapshot>,
pub wal_pages: Option<u64>,
#[serde(default)]
pub wal_log_frames: Option<u64>,
#[serde(default)]
pub wal_checkpointed_frames: Option<u64>,
#[serde(default)]
pub wal_pending_frames: Option<u64>,
#[serde(default)]
pub wal_physical_bytes: Option<u64>,
#[serde(default)]
pub wal_observed_at_unix_ms: Option<u64>,
#[serde(default)]
pub wal_checkpoint_stores: Vec<CheckpointStoreMetrics>,
pub wal_truncate_attempts: u64,
pub wal_truncate_consecutive_failures: u64,
#[serde(default)]
pub wal_checkpoint_skipped_ticks: u64,
#[serde(default)]
pub wal_checkpoint_consecutive_skips: u64,
#[serde(default)]
pub wal_checkpoint_last_skip_wal_pages: Option<u64>,
pub oldest_pinned_tx_micros: Option<u64>,
pub oldest_pinned_tx_label: Option<String>,
pub open_tx_count: usize,
pub write_queue_depth: Option<usize>,
pub write_queue_capacity: Option<usize>,
#[serde(default)]
pub write_last_queue_wait_micros: Option<u64>,
#[serde(default)]
pub write_last_transaction_acquire_micros: Option<u64>,
#[serde(default)]
pub write_last_body_micros: Option<u64>,
#[serde(default)]
pub write_last_commit_micros: Option<u64>,
#[serde(default)]
pub write_last_total_micros: Option<u64>,
#[serde(default)]
pub write_last_observed_at_unix_ms: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub connections: Option<ConnectionCapSnapshot>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub recall_ledger: Option<RecallLedgerSnapshot>,
}
#[cfg(unix)]
pub async fn read_frame<R>(stream: &mut R) -> std::io::Result<Vec<u8>>
where
R: tokio::io::AsyncRead + Unpin,
{
let mut len_buf = [0u8; 4];
stream.read_exact(&mut len_buf).await?;
let len = u32::from_be_bytes(len_buf) as usize;
if len > MAX_FRAME_BYTES {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("daemon frame of {len} bytes exceeds {MAX_FRAME_BYTES} cap"),
));
}
let mut buf = vec![0u8; len];
stream.read_exact(&mut buf).await?;
Ok(buf)
}
#[cfg(unix)]
fn initial_frame_timeout_error() -> std::io::Error {
std::io::Error::new(
std::io::ErrorKind::TimedOut,
"daemon initial request frame read timed out",
)
}
#[cfg(unix)]
pub(super) async fn read_initial_frame<R>(
stream: &mut R,
deadline: tokio::time::Instant,
) -> std::io::Result<Vec<u8>>
where
R: tokio::io::AsyncRead + Unpin,
{
if tokio::time::Instant::now() >= deadline {
return Err(initial_frame_timeout_error());
}
let raw = tokio::time::timeout_at(deadline, read_frame(stream))
.await
.map_err(|_| initial_frame_timeout_error())??;
if tokio::time::Instant::now() >= deadline {
return Err(initial_frame_timeout_error());
}
Ok(raw)
}
#[cfg(unix)]
pub async fn write_frame<W>(stream: &mut W, payload: &[u8]) -> std::io::Result<()>
where
W: tokio::io::AsyncWrite + Unpin,
{
if payload.len() > MAX_FRAME_BYTES {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"daemon frame of {} bytes exceeds {MAX_FRAME_BYTES} cap",
payload.len()
),
));
}
let len = (payload.len() as u32).to_be_bytes();
stream.write_all(&len).await?;
stream.write_all(payload).await?;
stream.flush().await?;
Ok(())
}