Watch
0
0
Fork
You've already forked hyperhive
0

swarm-builder: lock the swarm meta flake on request

A request on $SWARM.builder.lock-meta carries a rendered flake.nix. The
builder writes it into a scratch directory, runs `nix flake lock` there
with the same forge credential helper its builds use, answers with the
flake.lock nix wrote, and removes the directory. It commits nothing; the
controller commits the lock beside the flake.

Locks are answered in their own loop, like archetype evaluations, so one
does not wait behind an hour-long build. The auth callout grants the
subject to the builder (subscribe) and the controller (publish) only.

Ported from c574a1694 onto the swarm-wire crate.

Refs #4857
This commit is contained in:
atlas 2026-10-22 01:23:02 +02:00
commit de323a72dd
4 changed files with 320 additions and 42 deletions

View file

@ -1,5 +1,5 @@
//! The swarm builder's requests: the subjects swarm-controller sends a build
//! and an archetype evaluation on, and what travels each way.
//! The swarm builder's requests: the subjects swarm-controller sends a build,
//! an archetype evaluation and a meta-flake lock on, and what travels each way.
//!
//! Three crates name these: the controller sends the requests, the builder
//! answers them, and the auth-callout responder grants the subjects to each.
@ -80,6 +80,38 @@ pub enum ArchetypesReply {
Failed { error_tail: String },
}
/// The subject a swarm meta-flake lock goes to, with the same one publisher and
/// one subscriber as [`SUBJECT`].
pub const LOCK_SUBJECT: &str = "$SWARM.builder.lock-meta";
/// The longest one lock may take. A lock fetches every input's source and
/// builds nothing.
pub const LOCK_DEADLINE: Duration = Duration::from_mins(10);
/// How long the controller waits for a lock's reply: the lock at its deadline,
/// plus a minute for the rest.
pub const LOCK_REPLY_DEADLINE: Duration = Duration::from_mins(11);
/// One lock: the swarm meta flake's `flake.nix`, locked on its own in a scratch
/// directory. The builder writes nothing anywhere else and commits nothing; the
/// controller commits the returned lock beside the flake.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LockMeta {
/// The rendered `flake.nix`.
pub flake_nix: String,
}
/// The builder's answer to one [`LockMeta`].
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "state", rename_all = "kebab-case")]
pub enum LockReply {
/// The flake locked; `flake_lock` is the `flake.lock` `nix` wrote.
Locked { flake_lock: String },
/// The lock did not finish: the last lines `nix` printed, or why the
/// builder could not start it.
Failed { error_tail: String },
}
#[cfg(test)]
mod tests {
use serde_json::json;
@ -168,6 +200,7 @@ mod tests {
fn the_subjects_are_pinned() {
assert_eq!(SUBJECT, "$SWARM.builder.build");
assert_eq!(ARCHETYPES_SUBJECT, "$SWARM.builder.archetypes");
assert_eq!(LOCK_SUBJECT, "$SWARM.builder.lock-meta");
assert_eq!(QUEUE_GROUP, "builder");
}
@ -175,4 +208,42 @@ mod tests {
fn the_reply_deadline_outlasts_the_build() {
assert!(REPLY_DEADLINE > BUILD_DEADLINE);
}
#[test]
fn the_lock_reply_deadline_outlasts_the_lock() {
assert!(LOCK_REPLY_DEADLINE > LOCK_DEADLINE);
}
#[test]
fn the_lock_request_and_reply_serialize_as_pinned() {
let req = LockMeta {
flake_nix: "{ }".to_owned(),
};
let value = serde_json::to_value(&req).expect("encodes");
assert_eq!(value, json!({"flake_nix": "{ }"}));
assert_eq!(
serde_json::from_value::<LockMeta>(value).expect("decodes"),
req
);
for (reply, wire) in [
(
LockReply::Locked {
flake_lock: "{}".to_owned(),
},
json!({"state": "locked", "flake_lock": "{}"}),
),
(
LockReply::Failed {
error_tail: "e".to_owned(),
},
json!({"state": "failed", "error_tail": "e"}),
),
] {
assert_eq!(serde_json::to_value(&reply).expect("encodes"), wire);
assert_eq!(
serde_json::from_value::<LockReply>(wire).expect("decodes"),
reply
);
}
}
}