use std::sync::Arc;
use crate::policy::footprint::Footprint;
use crate::policy::maintenance::{MaintenanceState, RebuildRefused, StopRefused};
use crate::policy::outbox::Receipt;
use crate::serving::batch::PoolUsage;
use axum::extract::State;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::Json;
use serde::Deserialize;
use crate::AppState;
pub(crate) async fn cache_status(State(state): State<Arc<AppState>>) -> Json<serde_json::Value> {
let gate = state
.maintenance
.lock()
.unwrap_or_else(|p| p.into_inner())
.state();
let kv = kv_pages(&state);
Json(serde_json::json!({
"state": gate.as_str(),
"kv": kv.map(|(usage, page_size, evictable)| serde_json::json!({
"num_pages": usage.total,
"used_pages": usage.used,
"evictable_pages": evictable,
"page_size": page_size,
"num_tokens": usage.total * page_size,
})),
"moe": serde_json::Value::Null,
"mamba": serde_json::Value::Null,
"swa": serde_json::Value::Null,
"resizable": kv.is_some().then(|| serde_json::json!(["kv"])),
}))
}
fn sealed_response(
state: &AppState,
sealed: &crate::policy::maintenance::SealedAccounting,
) -> Response {
match persist_receipt(state, sealed) {
Ok(receipt) => {
let mut body = sealed_json(sealed);
if let Some(receipt) = receipt {
body["receipt_id"] = serde_json::json!(receipt.id);
body["receipt_status"] = serde_json::json!(receipt.status.as_str());
}
Json(body).into_response()
}
Err(why) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": why.to_string(),
"drain_complete": true,
"engine_preserved": true,
"retryable": true,
})),
)
.into_response(),
}
}
fn outbox_dir() -> Option<std::path::PathBuf> {
std::env::var_os("FERROX_ACCOUNTING_OUTBOX").map(std::path::PathBuf::from)
}
fn engine_identity(state: &AppState) -> String {
if let Ok(id) = std::env::var("FERROX_INSTANCE_ID") {
return id;
}
format!("pid:{}:started:{}", std::process::id(), state.started_unix)
}
fn persist_receipt(
state: &AppState,
sealed: &crate::policy::maintenance::SealedAccounting,
) -> Result<Option<Receipt>, std::io::Error> {
let Some(dir) = outbox_dir() else {
return Ok(None);
};
let receipt = Receipt::from_sealed(&engine_identity(state), sealed);
let final_path = dir.join(format!("{}.json", receipt.id));
let result = crate::policy::outbox::finish_stop(
receipt,
|receipt| {
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
if final_path.exists() {
return Ok(());
}
let tmp = dir.join(format!("{}.json.partial", receipt.id));
let body = serde_json::json!({
"id": receipt.id,
"model_id": receipt.model_id,
"prompt_tokens_total": receipt.prompt_tokens_total,
"completion_tokens_total": receipt.completion_tokens_total,
"uptime_s": receipt.uptime_seconds,
"status": receipt.status.as_str(),
});
std::fs::write(&tmp, serde_json::to_vec_pretty(&body).unwrap_or_default())
.map_err(|e| e.to_string())?;
std::fs::rename(&tmp, &final_path).map_err(|e| e.to_string())
},
|_| Ok(()),
);
match result {
Ok(receipt) => Ok(Some(receipt)),
Err(why) => Err(std::io::Error::other(why.to_string())),
}
}
fn kv_pages(state: &AppState) -> Option<(PoolUsage, usize, usize)> {
let cfg = state.kv_pool.as_ref()?;
let pool = cfg.pool.lock().unwrap_or_else(|p| p.into_inner());
let evictable = 0;
Some((
PoolUsage::from_available(pool.total_blocks(), pool.free_blocks() + evictable),
pool.block_size(),
evictable,
))
}
pub(crate) fn pool_gauges(state: &AppState) -> serde_json::Value {
let kv = kv_pages(state);
serde_json::json!({
"kv_pages": kv.map(|(usage, page_size, evictable)| serde_json::json!({
"used": usage.used,
"total": usage.total,
"evictable": evictable,
"page_size": page_size,
})),
"window_slots": serde_json::Value::Null,
"state_slots": serde_json::Value::Null,
})
}
#[cfg(target_os = "linux")]
fn read_footprint() -> Option<Footprint> {
use crate::policy::footprint::FootprintKind;
if let Some(bytes) = std::fs::read_to_string("/proc/self/smaps_rollup")
.ok()
.as_deref()
.and_then(crate::policy::footprint::parse_smaps_rollup_pss)
{
return Some(Footprint {
bytes,
kind: FootprintKind::Pss,
});
}
let bytes = std::fs::read_to_string("/proc/self/status")
.ok()
.as_deref()
.and_then(crate::policy::footprint::parse_status_rss)?;
Some(Footprint {
bytes,
kind: FootprintKind::Rss,
})
}
#[cfg(not(target_os = "linux"))]
fn read_footprint() -> Option<Footprint> {
None
}
pub(crate) fn footprint_json(state: &AppState) -> serde_json::Value {
let now_ms = state.uptime().as_millis().min(u64::MAX as u128) as u64;
let reading = state
.footprint
.lock()
.unwrap_or_else(|p| p.into_inner())
.get_or_probe(now_ms, read_footprint);
match reading {
Some(f) => serde_json::json!({
"bytes": f.bytes,
"kind": f.kind.as_str(),
}),
None => serde_json::Value::Null,
}
}
#[derive(Debug, Deserialize)]
pub(crate) struct CacheRebuildRequest {
#[serde(default)]
kv: Option<u64>,
#[serde(default)]
moe: Option<u64>,
#[serde(default)]
mamba: Option<u64>,
#[serde(default)]
swa: Option<u64>,
}
fn refusal(status: StatusCode, status_word: &str, message: String) -> Response {
(
status,
Json(serde_json::json!({"status": status_word, "error": message})),
)
.into_response()
}
pub(crate) async fn cache_rebuild(
State(state): State<Arc<AppState>>,
Json(req): Json<CacheRebuildRequest>,
) -> Response {
{
let mut gate = state.maintenance.lock().unwrap_or_else(|p| p.into_inner());
if let Err(why) = gate.begin_rebuild() {
let status = match why {
RebuildRefused::NotReady | RebuildRefused::Latched => {
StatusCode::SERVICE_UNAVAILABLE
}
RebuildRefused::Busy(_) => StatusCode::CONFLICT,
};
let word = match why {
RebuildRefused::NotReady => "loading",
RebuildRefused::Latched => "failed",
RebuildRefused::Busy(_) => "busy",
};
return refusal(status, word, why.to_string());
}
}
let reopen = |ok: bool| {
state
.maintenance
.lock()
.unwrap_or_else(|p| p.into_inner())
.finish_rebuild(ok);
};
for (asked, pool) in [(req.moe, "moe"), (req.mamba, "mamba"), (req.swa, "swa")] {
if asked.is_some() {
reopen(true);
return refusal(
StatusCode::BAD_REQUEST,
"failed",
format!(
"this deployment has no {pool} pool to rebuild; see \
GET /v1/cache/status for what it does have"
),
);
}
}
let Some(kv_tokens) = req.kv else {
reopen(true);
return refusal(
StatusCode::BAD_REQUEST,
"failed",
"nothing to rebuild: pass `kv` in tokens".to_string(),
);
};
let Some(cfg) = state.kv_pool.as_ref() else {
reopen(true);
return refusal(
StatusCode::BAD_REQUEST,
"failed",
"this deployment has no shared KV pool; every request allocates privately \
(set FERROX_KV_POOL_BLOCKS to enable one)"
.to_string(),
);
};
let mut pool = cfg.pool.lock().unwrap_or_else(|p| p.into_inner());
let page_size = pool.block_size() as u64;
let pages = kv_tokens / page_size;
if pages == 0 {
let held = (pool.total_blocks() - pool.free_blocks()) as u64;
drop(pool);
reopen(true);
return refusal(
StatusCode::BAD_REQUEST,
"failed",
format!(
"kv={kv_tokens} tokens is below one {page_size}-token page; \
the pool is currently holding {held} page(s)"
),
);
}
let before = pool.total_blocks() as u64;
let current = crate::policy::pool_budget::PoolSizes {
moe_cache_slots: 0,
kv_pages: before,
prefill_overlap: false,
};
let target = crate::policy::pool_budget::PoolSizes {
kv_pages: pages,
..current
};
let txn = crate::policy::rebuild::RebuildTxn::open(
&crate::policy::pool_budget::RebuildRequest {
moe_cache_slots: None,
kv_pages: Some(pages),
mamba_slots: None,
swa_pages: None,
},
¤t,
);
let mut refused_floor: Option<usize> = None;
let outcome = txn.run(
target,
|_mark_teardown, target| {
pool.resize(target.kv_pages as usize).map_err(|held| {
refused_floor = Some(held);
format!("{held} page(s) are held by in-flight requests")
})
},
|_old, _touched| unreachable!("nothing was torn down, so nothing can be restored"),
);
match outcome {
crate::policy::rebuild::RebuildOutcome::Applied(_) => {
let after = pool.total_blocks() as u64;
drop(pool);
let invalidated = state.prefix_cache.is_some();
if let Some(pc) = &state.prefix_cache {
pc.lock().unwrap_or_else(|p| p.into_inner()).clear();
}
reopen(true);
tracing::info!(
"cache rebuilt: kv {before} -> {after} pages of {page_size} tokens \
(prefix cache invalidated: {invalidated})"
);
Json(serde_json::json!({
"status": "ok",
"kv": {
"num_pages": after,
"page_size": page_size,
"num_tokens": after * page_size,
},
"prefix_cache_invalidated": invalidated,
}))
.into_response()
}
crate::policy::rebuild::RebuildOutcome::RejectedIntact { .. } => {
drop(pool);
reopen(true);
let held = refused_floor.unwrap_or(0);
refusal(
StatusCode::CONFLICT,
"busy",
format!(
"kv={kv_tokens} tokens is {pages} page(s), below the {held} page(s) \
currently held by in-flight requests; the engine is unchanged. \
Retry when they finish, or ask for at least {} tokens",
held as u64 * page_size
),
)
}
crate::policy::rebuild::RebuildOutcome::RolledBack { reason, .. } => {
drop(pool);
reopen(true);
refusal(StatusCode::CONFLICT, "busy", reason)
}
crate::policy::rebuild::RebuildOutcome::Latched {
reason,
rollback_error,
} => {
drop(pool);
state
.maintenance
.lock()
.unwrap_or_else(|p| p.into_inner())
.latch_failed();
refusal(
StatusCode::INTERNAL_SERVER_ERROR,
"failed",
format!("{reason}; and the rollback failed too: {rollback_error}"),
)
}
}
}
#[derive(Debug, Deserialize)]
pub(crate) struct PrepareStopRequest {
#[serde(default)]
#[allow(dead_code)]
drain_timeout_s: Option<f64>,
#[serde(default)]
#[allow(dead_code)]
abort_timeout_s: Option<f64>,
}
pub(crate) async fn prepare_stop(
State(state): State<Arc<AppState>>,
body: Option<Json<PrepareStopRequest>>,
) -> Response {
let _ = body;
let mut gate = state.maintenance.lock().unwrap_or_else(|p| p.into_inner());
if let Some(sealed) = gate.sealed().cloned() {
drop(gate);
return sealed_response(&state, &sealed);
}
if let Err(why) = gate.begin_stop() {
return (
StatusCode::SERVICE_UNAVAILABLE,
Json(serde_json::json!({
"error": why.to_string(),
"drain_complete": false,
"engine_preserved": true,
})),
)
.into_response();
}
let active = state.cancels.live_count();
let model_id = state.active_model_name();
let uptime = state.uptime().as_secs();
let (prompt_total, completion_total) = (
state.stats.tokens_prompt_total(),
state.stats.tokens_generated_total(),
);
match gate.seal(active, active > 0, || {
crate::policy::maintenance::SealedAccounting {
model_id,
prompt_tokens_total: prompt_total,
completion_tokens_total: completion_total,
uptime_seconds: uptime,
drain_complete: true,
}
}) {
Ok(sealed) => {
drop(gate);
sealed_response(&state, &sealed)
}
Err(why) => {
let engine_preserved = !matches!(why, StopRefused::RebuildInProgress);
(
StatusCode::SERVICE_UNAVAILABLE,
Json(serde_json::json!({
"error": why.to_string(),
"drain_complete": false,
"engine_preserved": engine_preserved,
"active": active,
})),
)
.into_response()
}
}
}
fn sealed_json(sealed: &crate::policy::maintenance::SealedAccounting) -> serde_json::Value {
serde_json::json!({
"model_id": sealed.model_id,
"prompt_tokens_total": sealed.prompt_tokens_total,
"completion_tokens_total": sealed.completion_tokens_total,
"uptime_s": sealed.uptime_seconds,
"drain_complete": sealed.drain_complete,
})
}
pub(crate) fn check_admission(state: &AppState) -> Result<(), crate::ApiError> {
let gate = state.maintenance.lock().unwrap_or_else(|p| p.into_inner());
gate.check_admission().map_err(|closed| {
(
StatusCode::SERVICE_UNAVAILABLE,
Json(serde_json::json!({"error": {
"message": closed.to_string(),
"type": match closed.state {
MaintenanceState::Loading => "model_loading",
MaintenanceState::Rebuilding => "cache_rebuilding",
MaintenanceState::Stopping => "server_stopping",
MaintenanceState::Failed => "server_failed",
MaintenanceState::Serving => unreachable!("serving admits"),
},
}})),
)
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::policy::maintenance::MaintenanceGate;
#[test]
fn a_rebuild_is_refused_by_the_gate_before_anything_is_measured() {
let mut gate = MaintenanceGate::new();
assert_eq!(gate.begin_rebuild(), Err(RebuildRefused::NotReady));
gate.finish_loading(true);
gate.begin_rebuild().expect("the first one starts");
assert!(matches!(
gate.begin_rebuild(),
Err(RebuildRefused::Busy(MaintenanceState::Rebuilding))
));
gate.finish_rebuild(true);
gate.begin_stop().expect("stop starts");
assert!(matches!(
gate.begin_rebuild(),
Err(RebuildRefused::Busy(MaintenanceState::Stopping))
));
}
#[test]
fn a_rebuild_below_what_is_held_leaves_the_pool_untouched() {
use ferrox_core::cache::{KvBlockPool, KvCache};
use std::sync::Mutex;
let pool = Arc::new(Mutex::new(KvBlockPool::new(16, 64)));
let held = KvCache::with_pool(2, 4, Arc::clone(&pool), 64).expect("blocks");
let mut p = pool.lock().unwrap();
let in_use = p.total_blocks() - p.free_blocks();
assert!(in_use > 0);
assert_eq!(p.resize(in_use - 1), Err(in_use));
assert_eq!(p.total_blocks(), 64, "a refused rebuild changes nothing");
drop(p);
drop(held);
}
#[test]
fn a_kv_rebuild_drops_every_stored_prefix_but_keeps_the_counters() {
use ferrox_core::cache::KvCache;
use ferrox_models::PrefixCache;
let mut kv = KvCache::new(2, 4);
for _ in 0..3 {
kv.push(&[0.0; 8], &[0.0; 8]).expect("unpooled push");
}
let mut pc = PrefixCache::new(4);
pc.store(vec![1, 2, 3], vec![kv], vec![0.0; 8]);
assert!(pc.find_longest_prefix(&[1, 2, 3, 4]).matched_len > 0);
let hits_before = pc.stats().hits;
pc.clear();
assert_eq!(
pc.find_longest_prefix(&[1, 2, 3, 4]).matched_len,
0,
"nothing may survive a re-split"
);
assert_eq!(
pc.stats().hits,
hits_before,
"the counters describe what this process served, which a \
re-split does not undo"
);
}
#[test]
fn sealing_a_stop_is_idempotent_for_the_life_of_the_process() {
let mut gate = MaintenanceGate::new();
gate.finish_loading(true);
gate.begin_stop().expect("stop starts");
let snapshot = |completion| {
move || crate::policy::maintenance::SealedAccounting {
model_id: Some("m".to_string()),
prompt_tokens_total: 10,
completion_tokens_total: completion,
uptime_seconds: 5,
drain_complete: true,
}
};
let first = gate.seal(0, false, snapshot(20)).expect("seals");
let again = gate.seal(0, false, snapshot(99_999)).expect("seals again");
assert_eq!(first, again, "a retry must not re-measure");
}
#[test]
fn a_stop_with_work_still_in_flight_seals_nothing_and_reopens_nothing() {
let mut gate = MaintenanceGate::new();
gate.finish_loading(true);
gate.begin_stop().expect("stop starts");
let err = gate
.seal(2, true, || unreachable!("must not snapshot"))
.expect_err("refused");
assert_eq!(err, StopRefused::AbortBarrierTimedOut(2));
assert_eq!(gate.state(), MaintenanceState::Stopping);
assert!(gate.check_admission().is_err());
assert!(gate.sealed().is_none());
}
#[test]
fn an_evictable_page_is_memory_and_not_occupancy() {
let usage = PoolUsage::from_available(100, 40 + 20);
assert_eq!(usage.used, 40, "evictable pages are not occupancy");
assert_eq!(usage.total, 100);
assert_eq!(PoolUsage::from_available(10, 12).used, 0);
}
#[test]
fn a_receipt_lands_on_disk_and_a_retry_reuses_the_same_document() {
use crate::policy::maintenance::SealedAccounting;
let dir = std::env::temp_dir().join(format!(
"ferrox-outbox-test-{}-{}",
std::process::id(),
line!()
));
let _ = std::fs::remove_dir_all(&dir);
let sealed = SealedAccounting {
model_id: Some("glm-5.2".to_string()),
prompt_tokens_total: 100,
completion_tokens_total: 50,
uptime_seconds: 7,
drain_complete: true,
};
let receipt = Receipt::from_sealed("stable-identity", &sealed);
let path = dir.join(format!("{}.json", receipt.id));
let write = |r: &Receipt| -> Result<(), String> {
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
if path.exists() {
return Ok(());
}
let tmp = dir.join(format!("{}.json.partial", r.id));
std::fs::write(&tmp, b"{}").map_err(|e| e.to_string())?;
std::fs::rename(&tmp, &path).map_err(|e| e.to_string())
};
crate::policy::outbox::finish_stop(receipt.clone(), write, |_| Ok(())).expect("first stop");
assert!(path.exists(), "the receipt must be durable");
let retry = Receipt::from_sealed("stable-identity", &sealed);
assert_eq!(retry.id, receipt.id);
crate::policy::outbox::finish_stop(retry, write, |_| Ok(())).expect("retry");
let count = std::fs::read_dir(&dir).unwrap().count();
assert_eq!(count, 1, "one generation, one receipt");
assert!(std::fs::read_dir(&dir).unwrap().all(|e| !e
.unwrap()
.file_name()
.to_string_lossy()
.ends_with(".partial")));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn no_configured_outbox_means_nothing_is_written_and_that_is_not_a_failure() {
assert_eq!(
std::env::var_os("FERROX_ACCOUNTING_OUTBOX").is_none(),
outbox_dir().is_none()
);
}
}