#[cfg(unix)]
use std::path::PathBuf;
#[cfg(unix)]
use std::time::{Duration, Instant};
#[cfg(unix)]
use crate::pool::ConnectionPool;
#[cfg(unix)]
use super::{now_epoch_secs, CachedWalpinAttribution, TruncateState, WalpinFullScanPlan};
#[cfg(all(test, unix))]
pub(super) mod walpin_attribution_test_sync {
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::mpsc::{sync_channel, Receiver, SyncSender};
use std::sync::{Arc, Mutex};
enum Behavior {
Pause {
reached_tx: tokio::sync::oneshot::Sender<std::thread::ThreadId>,
proceed_rx: Receiver<()>,
},
Panic,
}
struct Hook {
dir: PathBuf,
behavior: Behavior,
}
static HOOK: Mutex<Option<Hook>> = Mutex::new(None);
static REPORT_COUNTER: Mutex<Option<Arc<AtomicUsize>>> = Mutex::new(None);
pub(crate) fn install_pause(
dir: PathBuf,
) -> (
tokio::sync::oneshot::Receiver<std::thread::ThreadId>,
SyncSender<()>,
Arc<AtomicUsize>,
) {
let (reached_tx, reached_rx) = tokio::sync::oneshot::channel();
let (proceed_tx, proceed_rx) = sync_channel(0);
let report_counter = Arc::new(AtomicUsize::new(0));
let replaced = HOOK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.replace(Hook {
dir,
behavior: Behavior::Pause {
reached_tx,
proceed_rx,
},
});
assert!(
replaced.is_none(),
"walpin attribution hook already installed"
);
*REPORT_COUNTER
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(Arc::clone(&report_counter));
(reached_rx, proceed_tx, report_counter)
}
pub(crate) fn install_panic(dir: PathBuf) {
let replaced = HOOK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.replace(Hook {
dir,
behavior: Behavior::Panic,
});
assert!(
replaced.is_none(),
"walpin attribution hook already installed"
);
}
pub(crate) fn uninstall() {
*HOOK.lock().unwrap_or_else(|poisoned| poisoned.into_inner()) = None;
*REPORT_COUNTER
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner()) = None;
}
pub(crate) fn before_enumeration(dir: &Path) {
let hook = {
let mut guard = HOOK.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
match guard.as_ref() {
Some(hook) if hook.dir == dir => guard.take(),
_ => None,
}
};
let Some(hook) = hook else {
return;
};
match hook.behavior {
Behavior::Pause {
reached_tx,
proceed_rx,
} => {
if reached_tx.send(std::thread::current().id()).is_ok() {
let _ = proceed_rx.recv();
}
}
Behavior::Panic => panic!("injected walpin attribution worker panic"),
}
}
pub(crate) fn report_used() {
if let Some(counter) = REPORT_COUNTER
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.as_ref()
{
counter.fetch_add(1, Ordering::SeqCst);
}
}
}
#[cfg(unix)]
#[derive(Debug)]
pub(super) enum WalpinAttributionRequest {
Fresh {
dir: PathBuf,
census: Result<crate::walpin::CensusResult, String>,
legacy_fallback_interval: Duration,
previous_last_attempt: Option<Instant>,
},
Cached(CachedWalpinAttribution),
Suppressed,
}
#[cfg(unix)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum WalpinReportFreshness {
Fresh,
Cached { age: Duration },
}
#[cfg(unix)]
impl WalpinReportFreshness {
fn is_fresh(self) -> bool {
self == Self::Fresh
}
}
#[cfg(not(unix))]
pub(super) type WalpinAttributionRequest = ();
#[cfg(unix)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum WalpinAttributionFailure {
Enumeration(String),
Worker(String),
}
#[cfg(unix)]
impl WalpinAttributionFailure {
pub(super) fn kind(&self) -> &'static str {
match self {
Self::Enumeration(_) => "enumeration",
Self::Worker(_) => "blocking_worker",
}
}
}
#[cfg(unix)]
impl std::fmt::Display for WalpinAttributionFailure {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Enumeration(error) => write!(
formatter,
"sidecar directory failed the trust-boundary enumeration; cross-process \
WAL-pin attribution is unestablished for this tick: {error}"
),
Self::Worker(error) => write!(
formatter,
"sidecar attribution blocking worker failed; cross-process WAL-pin \
attribution is unestablished for this tick: {error}"
),
}
}
}
#[cfg(unix)]
pub(super) fn capture_walpin_attribution_request(
pool: &ConnectionPool,
state: &mut TruncateState,
) -> Option<WalpinAttributionRequest> {
let path = pool.canonical_path()?;
if !crate::walpin::sidecar_enabled(true) {
return None;
}
let legacy_fallback_interval = state.legacy_walpin_fallback_interval;
Some(match state.plan_walpin_attribution_at(Instant::now()) {
WalpinFullScanPlan::Refresh {
previous_last_attempt,
} => WalpinAttributionRequest::Fresh {
dir: crate::walpin::sidecar_dir_for(path),
census: crate::walpin::census_holders(path).map_err(|error| error.to_string()),
legacy_fallback_interval,
previous_last_attempt,
},
WalpinFullScanPlan::Cached(cached) => WalpinAttributionRequest::Cached(cached),
WalpinFullScanPlan::Suppressed => WalpinAttributionRequest::Suppressed,
})
}
#[cfg(unix)]
pub(super) async fn complete_walpin_attribution(
request: Option<WalpinAttributionRequest>,
state: &mut TruncateState,
) -> Result<bool, WalpinAttributionFailure> {
let Some(request) = request else {
return Ok(false);
};
match request {
WalpinAttributionRequest::Suppressed => Ok(false),
WalpinAttributionRequest::Cached(cached) => {
log_walpin_sidecar_report(
&cached.report,
cached.census,
WalpinReportFreshness::Cached {
age: Instant::now().saturating_duration_since(cached.captured_at),
},
);
Ok(true)
}
WalpinAttributionRequest::Fresh {
dir,
census,
legacy_fallback_interval,
previous_last_attempt: _,
} => {
state.sidecar_attribution_attempted_this_tick = true;
if state.walpin_full_scan_last_attempt.is_none() {
state.walpin_full_scan_last_attempt = Some(Instant::now());
}
let fallback = state.walpin_cached_attribution.clone();
let result = tokio::task::spawn_blocking(move || {
#[cfg(test)]
walpin_attribution_test_sync::before_enumeration(&dir);
crate::walpin::enumerate_live(&dir, legacy_fallback_interval)
})
.await
.map_err(|error| WalpinAttributionFailure::Worker(error.to_string()))
.and_then(|result| {
result.map_err(|error| WalpinAttributionFailure::Enumeration(error.to_string()))
});
match result {
Ok(report) => {
let captured_at = Instant::now();
log_walpin_sidecar_report(
&report,
census.clone(),
WalpinReportFreshness::Fresh,
);
state.cache_walpin_attribution(report, census, captured_at);
Ok(true)
}
Err(error) => {
if let Some(cached) = fallback {
log_walpin_sidecar_report(
&cached.report,
cached.census,
WalpinReportFreshness::Cached {
age: Instant::now().saturating_duration_since(cached.captured_at),
},
);
}
Err(error)
}
}
}
}
}
#[cfg(unix)]
pub(super) fn log_walpin_sidecar_report(
report: &crate::walpin::WalpinReport,
census: Result<crate::walpin::CensusResult, String>,
freshness: WalpinReportFreshness,
) {
#[cfg(test)]
walpin_attribution_test_sync::report_used();
let now = now_epoch_secs();
for hb in report.reporting() {
tracing::warn!(target: "khive_db::checkpoint",
walpin_pid = hb.pid,
walpin_role = %hb.process_role,
walpin_oldest_tx_age_secs = hb.current_oldest_tx_age_secs(now),
walpin_oldest_tx_label = hb.oldest_tx_label.as_deref().unwrap_or("<unlabeled>"),
walpin_attribution_basis = hb.attribution_basis.as_deref().unwrap_or("<unspecified>"),
walpin_attribution_evidence_backed = hb.attribution_is_evidence_backed(),
walpin_attribution_fresh = freshness.is_fresh(),
walpin_health = "reporting",
"ADR-091 Amendment 2 Plank B: live cross-process WAL-pin attribution report"
);
}
for pid in report.registered_silent_pids() {
tracing::debug!(target: "khive_db::checkpoint",
walpin_pid = pid,
walpin_health = "registered_silent",
walpin_attribution_fresh = freshness.is_fresh(),
"ADR-091 Amendment 2 Plank B: process affirmatively reports no over-threshold span"
);
}
let mut unknown_pids: Vec<u32> = report.unknown_pids().collect();
if let WalpinReportFreshness::Cached { age } = freshness {
tracing::warn!(target: "khive_db::checkpoint",
walpin_cache_age_ms = age.as_millis() as u64,
"cached WAL-pin attribution is diagnostic-only; fully-attributed \
conclusion is not licensed"
);
unknown_pids.push(0);
}
match census {
Ok(census) => {
let sidecar_known: std::collections::HashSet<u32> = report
.reporting()
.map(|hb| hb.pid)
.chain(report.registered_silent_pids())
.chain(unknown_pids.iter().copied())
.collect();
let mut census_only: Vec<u32> =
census.holders.difference(&sidecar_known).copied().collect();
if !census_only.is_empty() {
census_only.sort_unstable();
tracing::warn!(target: "khive_db::checkpoint",
?census_only,
"ADR-091 Amendment 2: these PIDs hold the database file open \
at the OS level but have no sidecar data at all (pre-feature binary, \
sidecar disabled, or wedged before its first write)"
);
unknown_pids.extend(census_only);
}
if !census.is_complete() {
let mut uninspectable = census.uninspectable_pids.clone();
uninspectable.sort_unstable();
tracing::warn!(target: "khive_db::checkpoint",
?uninspectable,
truncated = census.truncated,
"ADR-091 Amendment 2: the OS-derived holder census is \
INCOMPLETE — either specific PIDs' open file descriptors could not be \
inspected (permission denied, or a listing race), or the enumeration walk \
itself has positive evidence it did not see the full live-process universe \
(namespace/visibility check, directory-iterator error, self-canary, or a \
libproc buffer that stayed at capacity after bounded retries) — cannot \
rule out an unregistered holder"
);
if uninspectable.is_empty() {
unknown_pids.push(0);
} else {
unknown_pids.extend(uninspectable);
}
}
}
Err(e) => {
tracing::warn!(target: "khive_db::checkpoint",
error = %e,
"ADR-091 Amendment 2: OS-derived holder census failed; \
attribution cannot rule out an unregistered database holder this tick"
);
unknown_pids.push(0);
}
}
unknown_pids.sort_unstable();
unknown_pids.dedup();
if !unknown_pids.is_empty() {
tracing::warn!(target: "khive_db::checkpoint",
?unknown_pids,
"ADR-091 Amendment 2 Plank B: sidecar health unestablished for these PIDs; \
attribution is inconclusive and the native/unregistered-mechanism conclusion \
is NOT licensed this tick"
);
} else if report.reporting().next().is_none() {
tracing::info!(target: "khive_db::checkpoint",
"ADR-091 Amendment 2 Plank B: every live PID is reporting or registered-silent \
with none pinning; the WAL pin is not attributable to any in-process registry \
span this sidecar covers"
);
}
}