use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
const TRACE_TARGET: &str = "studio_worker::residency";
const VERSION: u32 = 1;
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct ResidencyFile {
version: u32,
resident: BTreeSet<String>,
}
#[derive(Debug, Default)]
pub struct Residency {
resident: BTreeSet<String>,
path: Option<PathBuf>,
}
impl Residency {
pub fn load_for_serving(path: Option<PathBuf>) -> Self {
let Some(path) = path else {
return Self::default();
};
match std::fs::read_to_string(&path) {
Ok(contents) => match parse(&contents) {
Some(resident) => Self {
resident,
path: Some(path),
},
None => {
quarantine(&path);
Self {
resident: BTreeSet::new(),
path: Some(path),
}
}
},
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Self {
resident: BTreeSet::new(),
path: Some(path),
},
Err(err) => {
tracing::warn!(
target: TRACE_TARGET,
op = "load",
path = %path.display(),
error = %err,
"residency unreadable; nothing restored and persistence \
disabled so the file is never clobbered"
);
Self::default()
}
}
}
pub fn is_resident(&self, id: &str) -> bool {
self.resident.contains(id)
}
pub fn ids(&self) -> impl Iterator<Item = &str> {
self.resident.iter().map(String::as_str)
}
pub fn persistence(&self) -> Option<&Path> {
self.path.as_deref()
}
pub fn set(&mut self, id: &str, resident: bool) -> std::io::Result<bool> {
if self.is_resident(id) == resident {
return Ok(false);
}
let mut next = self.resident.clone();
if resident {
next.insert(id.to_string());
} else {
next.remove(id);
}
if let Some(path) = &self.path {
save(path, &next)?;
tracing::info!(
target: TRACE_TARGET,
op = "save",
path = %path.display(),
model = id,
resident,
"residency saved"
);
}
self.resident = next;
Ok(true)
}
}
fn parse(contents: &str) -> Option<BTreeSet<String>> {
let file: ResidencyFile = serde_json::from_str(contents).ok()?;
(file.version == VERSION).then_some(file.resident)
}
fn quarantine(path: &Path) {
let target = crate::catalog::quarantine_path(path);
match std::fs::rename(path, &target) {
Ok(()) => tracing::warn!(
target: TRACE_TARGET,
op = "load",
path = %path.display(),
quarantine = %target.display(),
"residency is not valid JSON (or an unknown version); quarantined it, \
nothing restored"
),
Err(err) => tracing::error!(
target: TRACE_TARGET,
op = "load",
path = %path.display(),
error = %err,
"residency is not valid JSON and could not be quarantined; nothing restored"
),
}
}
fn save(path: &Path, resident: &BTreeSet<String>) -> std::io::Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let json = serde_json::to_string_pretty(&ResidencyFile {
version: VERSION,
resident: resident.clone(),
})
.map_err(std::io::Error::other)?;
crate::config::write_atomic(path, json.as_bytes()).map_err(std::io::Error::other)
}
#[cfg(test)]
mod tests {
use super::*;
fn tmp() -> (tempfile::TempDir, PathBuf) {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("residency.json");
(dir, path)
}
#[test]
fn a_missing_file_means_nothing_is_resident() {
let (_d, path) = tmp();
let r = Residency::load_for_serving(Some(path.clone()));
assert_eq!(r.ids().count(), 0);
assert_eq!(r.persistence(), Some(path.as_path()));
assert!(!path.exists(), "loading alone writes nothing");
}
#[test]
fn set_persists_and_reloads() {
let (_d, path) = tmp();
let mut r = Residency::load_for_serving(Some(path.clone()));
r.set("stt-a", true).unwrap();
r.set("llm-b", true).unwrap();
r.set("stt-a", false).unwrap();
let again = Residency::load_for_serving(Some(path.clone()));
assert!(again.is_resident("llm-b"));
assert!(!again.is_resident("stt-a"));
let json: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(
json,
serde_json::json!({ "version": 1, "resident": ["llm-b"] })
);
}
#[test]
fn set_reports_whether_anything_changed() {
let (_d, path) = tmp();
let mut r = Residency::load_for_serving(Some(path));
assert!(r.set("m", true).unwrap());
assert!(!r.set("m", true).unwrap());
assert!(r.set("m", false).unwrap());
assert!(!r.set("m", false).unwrap());
}
#[test]
fn a_corrupt_file_is_quarantined_and_nothing_is_resident() {
let (dir, path) = tmp();
std::fs::write(&path, "{ not json").unwrap();
let logs = crate::test_support::capture({
let path = path.clone();
move || {
let r = Residency::load_for_serving(Some(path.clone()));
assert_eq!(r.ids().count(), 0);
assert_eq!(r.persistence(), Some(path.as_path()));
}
});
assert!(logs.contains("residency is not valid JSON"), "{logs}");
let quarantined = std::fs::read_dir(dir.path())
.unwrap()
.filter_map(|e| e.ok())
.any(|e| {
e.file_name()
.to_string_lossy()
.starts_with("residency.json.corrupt-")
});
assert!(quarantined, "the corrupt bytes are kept for recovery");
}
#[cfg(unix)]
#[test]
fn a_corrupt_file_that_cannot_be_quarantined_is_reported() {
use std::os::unix::fs::PermissionsExt;
let (dir, path) = tmp();
std::fs::write(&path, "nope").unwrap();
std::fs::set_permissions(dir.path(), std::fs::Permissions::from_mode(0o500)).unwrap();
let logs = crate::test_support::capture({
let path = path.clone();
move || {
let r = Residency::load_for_serving(Some(path));
assert_eq!(r.ids().count(), 0);
}
});
std::fs::set_permissions(dir.path(), std::fs::Permissions::from_mode(0o700)).unwrap();
assert!(logs.contains("could not be quarantined"), "{logs}");
assert!(path.exists(), "the original bytes stay where they were");
}
#[test]
fn an_unknown_version_is_quarantined_too() {
let (_d, path) = tmp();
std::fs::write(&path, r#"{"version":2,"resident":["x"]}"#).unwrap();
let r = Residency::load_for_serving(Some(path));
assert!(!r.is_resident("x"));
}
#[test]
fn an_unreadable_file_disables_persistence() {
let (_d, path) = tmp();
std::fs::create_dir(&path).unwrap();
let logs = crate::test_support::capture({
let path = path.clone();
move || {
let mut r = Residency::load_for_serving(Some(path));
assert_eq!(r.persistence(), None);
assert!(r.set("m", true).unwrap(), "still tracked in memory");
assert!(r.is_resident("m"));
}
});
assert!(logs.contains("residency unreadable"), "{logs}");
assert!(path.is_dir(), "never clobbers what it could not read");
}
#[test]
fn no_path_keeps_residency_in_memory_only() {
let mut r = Residency::load_for_serving(None);
assert!(r.set("m", true).unwrap());
assert!(r.is_resident("m"));
assert_eq!(r.persistence(), None);
}
#[test]
fn a_failed_save_leaves_memory_unchanged() {
let (dir, path) = tmp();
let mut r = Residency::load_for_serving(Some(path.clone()));
std::fs::create_dir(&path).unwrap();
assert!(r.set("m", true).is_err());
assert!(!r.is_resident("m"));
drop(dir);
}
#[test]
fn saves_leave_a_breadcrumb() {
let (_d, path) = tmp();
let logs = crate::test_support::capture(move || {
let mut r = Residency::load_for_serving(Some(path));
r.set("llm-b", true).unwrap();
});
assert!(logs.contains("residency saved"), "{logs}");
assert!(logs.contains("llm-b"), "{logs}");
}
}