Watch
0
0
Fork
You've already forked hyperhive
0

download an agent's cached closure in prebuild, building locally when that fails

This commit is contained in:
atlas 2026-10-28 02:02:29 +01:00
commit c16cf1756f
4 changed files with 1032 additions and 81 deletions

View file

@ -0,0 +1,62 @@
//! The closure the swarm built for an agent at a given `internal/meta` commit,
//! as a rebuild's `Prebuild` downloads it instead of building.
use anyhow::{Result, anyhow};
use swarm_wire::agent_closures::AgentClosure;
use super::probe::{self, Unpullable};
/// The store path of the closure `agent`'s record names, when the record is a
/// build of `rev` that the builder pushed to the cache.
///
/// # Errors
/// No record, one for another commit, or one the builder did not push; or the
/// queue cannot be reached. The message is what a deploy log says.
pub async fn path_for(agent: &str, rev: &str) -> Result<String> {
let client = crate::swarm_queue::client()
.await
.ok_or_else(|| anyhow!("this hive has no connection to the swarm queue"))?;
let record = probe::read_record(&client, agent).await?;
path_in(record.as_ref(), rev)
}
fn path_in(record: Option<&AgentClosure>, rev: &str) -> Result<String> {
probe::current_record(record, Some(rev))
.map(|record| record.store_path.clone())
.map_err(|Unpullable { reason, detail }| anyhow!("{}: {detail}", reason.label()))
}
#[cfg(test)]
mod tests {
use super::*;
const REV: &str = "a1b2c3";
const PATH: &str = "/nix/store/0123456789abcdfghijklmnpqrsvwxyz-nixos-system-atlas";
fn record(rev: &str, cached: bool) -> AgentClosure {
AgentClosure {
meta_rev: rev.to_owned(),
store_path: PATH.to_owned(),
system: "x86_64-linux".to_owned(),
built_unix: 1,
cached,
}
}
#[test]
fn a_pushed_build_of_the_rev_names_its_path() {
assert_eq!(path_in(Some(&record(REV, true)), REV).expect("path"), PATH);
}
#[test]
fn anything_else_names_why_not() {
for (record, why) in [
(None, "no-record"),
(Some(record("other", true)), "rev-mismatch"),
(Some(record(REV, false)), "not-cached"),
] {
let err = path_in(record.as_ref(), REV).expect_err(why);
assert!(format!("{err}").starts_with(why), "{err}");
}
}
}

View file

@ -1,7 +1,9 @@
//! The swarm's binary cache, as this hive sees it: the pull credentials it is
//! given ([`reader`]), the narinfo check ([`narinfo`]), and the probe that
//! reports per agent whether the latest closure could be pulled ([`probe`]).
//! given ([`reader`]), the narinfo check ([`narinfo`]), the probe that reports
//! per agent whether the latest closure could be pulled ([`probe`]), and the
//! lookup a rebuild uses to download that closure ([`closure`]).
pub mod closure;
pub mod narinfo;
pub mod probe;
pub mod reader;

View file

@ -3,7 +3,8 @@
//!
//! Every [`PASS_INTERVAL`] each agent's record in `agent-closures` is looked up
//! and its narinfo fetched with the pull token and checked against the cache's
//! public key. Nothing is downloaded and nothing is deployed from it.
//! public key. Nothing is downloaded here; a rebuild downloads through
//! [`super::closure`].
//!
//! The verdict is the first of these that fails, in this order:
//! no credentials (`no-credentials`), no record (`no-record`), a record built
@ -82,20 +83,12 @@ impl Cache for Http {
}
}
/// Whether the closure `record` names can be pulled with `facts`. `head` is the
/// `internal/meta` commit a deploy would build from.
pub(crate) async fn judge(
facts: Option<&Facts>,
record: Option<&AgentClosure>,
/// `record` when it is the latest build of the `internal/meta` commit `head`
/// and the builder pushed it to the cache.
pub(crate) fn current_record<'a>(
record: Option<&'a AgentClosure>,
head: Option<&str>,
cache: &impl Cache,
) -> Result<(), Unpullable> {
let facts = facts.ok_or_else(|| {
no(
Reason::NoCredentials,
"the swarm has given this hive no cache facts",
)
})?;
) -> Result<&'a AgentClosure, Unpullable> {
let record =
record.ok_or_else(|| no(Reason::NoRecord, "no build is recorded for the agent"))?;
if head != Some(record.meta_rev.as_str()) {
@ -114,6 +107,24 @@ pub(crate) async fn judge(
"the builder did not push the closure",
));
}
Ok(record)
}
/// Whether the closure `record` names can be pulled with `facts`. `head` is the
/// `internal/meta` commit a deploy would build from.
pub(crate) async fn judge(
facts: Option<&Facts>,
record: Option<&AgentClosure>,
head: Option<&str>,
cache: &impl Cache,
) -> Result<(), Unpullable> {
let facts = facts.ok_or_else(|| {
no(
Reason::NoCredentials,
"the swarm has given this hive no cache facts",
)
})?;
let record = current_record(record, head)?;
let text = match cache.narinfo(facts, &record.store_path).await {
Ok(Some(text)) => text,
Ok(None) => return Err(no(Reason::NotCached, "the cache does not hold the narinfo")),
@ -195,7 +206,10 @@ async fn pass(client: &async_nats::Client) -> Result<()> {
Ok(())
}
async fn read_record(client: &async_nats::Client, agent: &str) -> Result<Option<AgentClosure>> {
pub(crate) async fn read_record(
client: &async_nats::Client,
agent: &str,
) -> Result<Option<AgentClosure>> {
match agent_closures::read(client, agent)
.await
.map_err(|e| anyhow::anyhow!(e))

View file

@ -3,7 +3,8 @@
//! ([`swarm_secret_client::attic`]), read here under the hive's own identity.
//!
//! [`Facts::publish`] puts them in `/run/hyperhive/attic/` as a netrc, the
//! substituter URL and the public key, where nix and the root helper read them.
//! substituter URL and the public key, which the pull probe reads. The root helper
//! reads its own copies, kept by a root unit.
use std::path::Path;