pin the declared closure once per rebuild and seed the marker from the running closure
This commit is contained in:
parent
47c2acb640
commit
4356f3840c
1 changed files with 456 additions and 95 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue