Watch
0
0
Fork
You've already forked hyperhive
0

pin the declared closure once per rebuild and seed the marker from the running closure

This commit is contained in:
atlas 2026-11-02 00:42:45 +01:00 • committed by mara
commit 4356f3840c

View file

@ -1,8 +1,11 @@
//! The closure the swarm declares for an agent: its record in `agent-closures`
//! read before a deploy downloads it, and the marker of the store path the
//! hive last applied, which a changed declaration is compared with.
//! read before a deploy downloads it, the store path a rebuild pinned from it,
//! and the marker of the store path the hive last applied, which a changed
//! declaration is compared with.
use std::path::Path;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use anyhow::{Context as _, Result, anyhow};
use swarm_wire::agent_closures::AgentClosure;
@ -29,12 +32,71 @@ pub fn mark_deployed(agent: &str, store_path: &str) -> Result<()> {
mark_deployed_in(&crate::paths::applied_closure_marker(agent), store_path)
}
/// The store path `agent` was last swapped to; `None` when the hive has no
/// record of one: it never applied a closure, or last did before the marker
/// existed.
/// The store path `agent` runs: its marker, or on the first read after the
/// marker was introduced the closure its container profile points at, which is
/// then written as the marker. `None` when neither can be read: the hive cannot
/// tell what runs.
#[must_use]
pub fn deployed(agent: &str) -> Option<String> {
deployed_in(&crate::paths::applied_closure_marker(agent))
deployed_or_seed_in(
&crate::paths::applied_closure_marker(agent),
&crate::paths::container_profile(agent),
Path::new(STORE),
)
}
/// The nix store; a running closure must resolve into it.
const STORE: &str = "/nix/store";
fn deployed_or_seed_in(marker: &Path, profile: &Path, store: &Path) -> Option<String> {
if let Some(deployed) = deployed_in(marker) {
return Some(deployed);
}
let running = running_in(profile, store)?;
if let Err(e) = mark_deployed_in(marker, &running) {
tracing::warn!(marker = %marker.display(), error = %format!("{e:#}"), "cannot seed the deployed-closure marker");
}
Some(running)
}
/// The store path `profile` (a container's `system` link) resolves to; `None`
/// when the link is absent or unreadable, or does not end in `store`.
fn running_in(profile: &Path, store: &Path) -> Option<String> {
let resolved: PathBuf = std::fs::canonicalize(profile).ok()?;
resolved
.starts_with(store)
.then(|| resolved.to_str().map(str::to_owned))
.flatten()
}
/// The store path each agent's running rebuild pinned from its record.
static PINNED: Mutex<BTreeMap<String, String>> = Mutex::new(BTreeMap::new());
/// Pin `store_path` as the closure `agent`'s rebuild deploys, replacing any
/// earlier pin.
pub fn pin(agent: &str, store_path: &str) {
PINNED
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.insert(agent.to_owned(), store_path.to_owned());
}
/// The store path pinned for `agent`'s rebuild.
#[must_use]
pub fn pinned(agent: &str) -> Option<String> {
PINNED
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get(agent)
.cloned()
}
/// Drop `agent`'s pin, once its closure is applied.
pub fn unpin(agent: &str) {
PINNED
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.remove(agent);
}
/// Written whole to a temp file beside the marker and renamed over it, so a
@ -58,10 +120,8 @@ fn deployed_in(marker: &Path) -> Option<String> {
/// Whether the swarm's declared closure is one the hive should now deploy: the
/// builder pushed it and it differs from what `deployed` says runs.
///
/// An agent with no `deployed` marker is not pending, since the hive cannot
/// tell what it runs: every agent starts that way when the hive first reads
/// this marker, and deploying all of them at once is worse than waiting for the
/// swarm's next deploy request, which writes the marker.
/// An agent whose `deployed` closure is unknown is not pending: the hive cannot
/// tell what it runs, so it deploys nothing it was not told to.
#[must_use]
pub fn pending(record: &AgentClosure, deployed: Option<&str>) -> bool {
record.cached && deployed.is_some_and(|deployed| deployed != record.store_path)
@ -156,8 +216,8 @@ mod tests {
}
/// A hive that ran the previous marker format has a config commit in its
/// old files and none at the new path: the agent is not pending, and the
/// old files are not read or touched.
/// old files and none at the new path: the old files are not read, and
/// neither seeding nor a deploy touches them.
#[test]
fn markers_in_the_old_format_are_neither_read_nor_removed() {
let dir = tempfile::tempdir().unwrap();
@ -165,8 +225,14 @@ mod tests {
std::fs::write(&old, "1111111111111111111111111111111111111111").unwrap();
let marker = dir.path().join(".alice.closure");
assert_eq!(deployed_in(&marker), None);
assert!(!pending(&declared(true), deployed_in(&marker).as_deref()));
assert!(old.exists());
let (store, profile) = profile_running(dir.path(), "run-alice");
let seeded = deployed_or_seed_in(&marker, &profile, &store);
assert!(seeded.is_some(), "the running closure seeds the new marker");
assert_eq!(
std::fs::read_to_string(&old).unwrap(),
"1111111111111111111111111111111111111111",
"seeding leaves the old marker as it was"
);
mark_deployed_in(&marker, DECLARED).unwrap();
assert!(old.exists(), "a successful deploy leaves old markers alone");
}
@ -178,4 +244,86 @@ mod tests {
std::fs::write(&marker, "\n").unwrap();
assert_eq!(deployed_in(&marker), None);
}
/// A store with one closure, and a profile whose `system` link reaches it
/// the way `nix-env --set` leaves it: through a generation link.
fn profile_running(dir: &Path, closure: &str) -> (PathBuf, PathBuf) {
let store = dir.join("store");
std::fs::create_dir_all(store.join(closure)).unwrap();
let profile = dir.join("profiles").join("h-alice");
std::fs::create_dir_all(&profile).unwrap();
std::os::unix::fs::symlink(store.join(closure), profile.join("system-3-link")).unwrap();
std::os::unix::fs::symlink("system-3-link", profile.join("system")).unwrap();
(store, profile.join("system"))
}
#[test]
fn a_missing_marker_is_seeded_from_the_running_closure() {
let dir = tempfile::tempdir().unwrap();
let (store, profile) = profile_running(dir.path(), "run-alice");
let marker = dir.path().join("applied").join(".alice.closure");
let seeded = deployed_or_seed_in(&marker, &profile, &store);
let running = std::fs::canonicalize(store.join("run-alice")).unwrap();
assert_eq!(seeded.as_deref(), running.to_str());
assert_eq!(deployed_in(&marker), seeded, "the seed is written");
// The marker wins from then on, whatever the profile says.
let (_, other) = profile_running(&dir.path().join("elsewhere"), "run-other");
assert_eq!(deployed_or_seed_in(&marker, &other, &store), seeded);
}
/// The point of seeding: an agent that runs something other than the
/// declaration is pending from the first read.
#[test]
fn a_seeded_agent_running_another_closure_than_declared_is_pending() {
let dir = tempfile::tempdir().unwrap();
let (store, profile) = profile_running(dir.path(), "run-alice");
let marker = dir.path().join(".alice.closure");
let seeded = deployed_or_seed_in(&marker, &profile, &store);
assert!(pending(&declared(true), seeded.as_deref()));
let record = AgentClosure {
store_path: seeded.clone().unwrap(),
..declared(true)
};
assert!(
!pending(&record, seeded.as_deref()),
"control: running what is declared"
);
}
#[test]
fn a_running_closure_that_cannot_be_read_stays_unknown() {
let dir = tempfile::tempdir().unwrap();
let (store, _) = profile_running(dir.path(), "run-alice");
let marker = dir.path().join(".alice.closure");
// No profile at all, as for a container that was never created.
let absent = dir.path().join("profiles").join("h-bob").join("system");
assert_eq!(deployed_or_seed_in(&marker, &absent, &store), None);
// A dangling link, as after a garbage collection.
let dangling = dir.path().join("dangling");
std::os::unix::fs::symlink(store.join("gone"), &dangling).unwrap();
assert_eq!(deployed_or_seed_in(&marker, &dangling, &store), None);
// A link that leaves the store.
let outside = dir.path().join("outside");
std::fs::create_dir_all(&outside).unwrap();
let escape = dir.path().join("escape");
std::os::unix::fs::symlink(&outside, &escape).unwrap();
assert_eq!(deployed_or_seed_in(&marker, &escape, &store), None);
assert!(!marker.exists(), "nothing is seeded from an unknown");
assert!(!pending(&declared(true), None));
}
#[test]
fn a_pin_lasts_until_it_is_dropped() {
assert_eq!(pinned("pin-test"), None);
pin("pin-test", DECLARED);
assert_eq!(pinned("pin-test").as_deref(), Some(DECLARED));
pin("pin-test", RUNNING);
assert_eq!(
pinned("pin-test").as_deref(),
Some(RUNNING),
"a new pin replaces"
);
unpin("pin-test");
assert_eq!(pinned("pin-test"), None);
}
}