push green meta builds to the attic cache and record cached
This commit is contained in:
parent
c4e8d5b540
commit
0849afec80
1 changed files with 779 additions and 31 deletions
|
|
@ -16,6 +16,8 @@
|
|||
//! and an `internal/meta` commit, and the answer is the store path of that
|
||||
//! agent's toplevel, built at the flake's own lock. They take the build slot
|
||||
//! PR checks take. The controller records the path; the builder writes nothing.
|
||||
//! When a cache is set (see [`push`]), a green meta build is pushed to it before
|
||||
//! the reply, and the reply says whether that worked.
|
||||
//!
|
||||
//! The store identity comes from the `BAO_*` environment. The queue client
|
||||
//! secret is read at start and again when authelia refuses it, the forge token
|
||||
|
|
@ -39,6 +41,7 @@ use swarm_wire::builder::{
|
|||
|
||||
mod archetypes;
|
||||
mod nix_build;
|
||||
mod push;
|
||||
|
||||
/// The cert-auth role this service logs in to the store as. `swarm-bao.nix`
|
||||
/// writes it, matching the leaf's subject.
|
||||
|
|
@ -64,6 +67,17 @@ const MAX_SLOT_WAIT: Duration = REPLY_DEADLINE
|
|||
.saturating_sub(BUILD_DEADLINE)
|
||||
.saturating_sub(SLOT_MARGIN);
|
||||
|
||||
/// What is held back from a meta build's reply deadline for publishing the reply
|
||||
/// after its push.
|
||||
const REPLY_MARGIN: Duration = Duration::from_secs(10);
|
||||
|
||||
/// How long the push of a meta build that ends at now may run: at most
|
||||
/// [`push::PUSH_DEADLINE`], and never past `reply_by`, so a push cannot make the
|
||||
/// reply later than the controller waits.
|
||||
fn push_budget(reply_by: tokio::time::Instant) -> Duration {
|
||||
push::PUSH_DEADLINE.min(reply_by.saturating_duration_since(tokio::time::Instant::now()))
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
hive_log::init();
|
||||
|
|
@ -83,6 +97,7 @@ async fn main() -> Result<()> {
|
|||
deadline: BUILD_DEADLINE,
|
||||
lock_deadline: LOCK_DEADLINE,
|
||||
system: var("SYSTEM")?,
|
||||
cache: push_target()?,
|
||||
});
|
||||
let secret =
|
||||
swarm_secret_client::read_with_startup_retry("reading the queue client secret", || {
|
||||
|
|
@ -300,7 +315,7 @@ async fn answer_meta_builds(
|
|||
&slot,
|
||||
received,
|
||||
|| busy_meta(&job),
|
||||
handle_meta(&job, &builder),
|
||||
handle_meta(&job, &builder, received + REPLY_DEADLINE - REPLY_MARGIN),
|
||||
)
|
||||
.await
|
||||
else {
|
||||
|
|
@ -383,19 +398,36 @@ fn busy_meta(job: &BuildFromMeta) -> FromMetaReply {
|
|||
}
|
||||
|
||||
/// Build one meta request and log the build.
|
||||
async fn handle_meta(job: &BuildFromMeta, builder: &nix_build::Builder) -> FromMetaReply {
|
||||
async fn handle_meta(
|
||||
job: &BuildFromMeta,
|
||||
builder: &nix_build::Builder,
|
||||
reply_by: tokio::time::Instant,
|
||||
) -> FromMetaReply {
|
||||
let reply = match within(TOKEN_DEADLINE, forge_token()).await {
|
||||
Ok(token) => builder.build_from_meta(job, &token).await,
|
||||
Err(e) => FromMetaReply::Unavailable {
|
||||
reason: format!("{e:#}"),
|
||||
},
|
||||
};
|
||||
let reply = push::cache_built(
|
||||
reply,
|
||||
builder.cache.as_ref(),
|
||||
attic_push_token(),
|
||||
push_budget(reply_by),
|
||||
&job.agent,
|
||||
)
|
||||
.await;
|
||||
match &reply {
|
||||
FromMetaReply::Built { out_path, system } => tracing::info!(
|
||||
FromMetaReply::Built {
|
||||
out_path,
|
||||
system,
|
||||
cached,
|
||||
} => tracing::info!(
|
||||
agent = %job.agent,
|
||||
meta_rev = %job.meta_rev,
|
||||
%out_path,
|
||||
%system,
|
||||
cached,
|
||||
"agent meta build built"
|
||||
),
|
||||
FromMetaReply::Failed { error_tail } => tracing::warn!(
|
||||
|
|
@ -458,6 +490,23 @@ fn log_build(job: &BuildAgent, reply: &Reply) {
|
|||
}
|
||||
}
|
||||
|
||||
/// The token the builder pushes closures to the cache with, read on every push so
|
||||
/// a rotation takes effect on the next one.
|
||||
async fn attic_push_token() -> Result<String> {
|
||||
let path = swarm_secret_client::builder::attic_push_token_path()?;
|
||||
let store = swarm_secret_client::SecretStore::from_env(CERT_ROLE)
|
||||
.await
|
||||
.context("logging in to the secret store")?;
|
||||
let stored: Option<swarm_secret_client::builder::AtticPushToken> = store
|
||||
.read_optional(&path)
|
||||
.await
|
||||
.with_context(|| format!("reading {path}"))?;
|
||||
match stored {
|
||||
Some(t) if !t.value.trim().is_empty() => Ok(t.value.trim().to_owned()),
|
||||
_ => bail!("nothing at {path}: the cache push token has not been written"),
|
||||
}
|
||||
}
|
||||
|
||||
/// The forge token the builder fetches config repos with, read on every
|
||||
/// build so a rotation takes effect on the next one.
|
||||
async fn forge_token() -> Result<String> {
|
||||
|
|
@ -495,6 +544,17 @@ async fn client_secret(client_id: &str) -> Result<String> {
|
|||
}
|
||||
}
|
||||
|
||||
/// The cache the unit's environment names, if any. systemd sets
|
||||
/// `RUNTIME_DIRECTORY` for the unit's runtime directory.
|
||||
fn push_target() -> Result<Option<push::Remote>> {
|
||||
let var = |name: &str| std::env::var(name).ok();
|
||||
push::Remote::from_settings(
|
||||
var(&format!("{PREFIX}_ATTIC_DOMAIN")),
|
||||
var(&format!("{PREFIX}_ATTIC_CACHE")),
|
||||
var("RUNTIME_DIRECTORY"),
|
||||
)
|
||||
}
|
||||
|
||||
/// The forge's base URL, which must be `https`: the credential helper offers
|
||||
/// the token to `https` requests for its host alone.
|
||||
fn forge_url() -> Result<String> {
|
||||
|
|
@ -580,6 +640,7 @@ mod tests {
|
|||
FromMetaReply::Built {
|
||||
out_path: "/nix/store/x".to_owned(),
|
||||
system: "s".to_owned(),
|
||||
cached: false,
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
|
@ -594,6 +655,7 @@ mod tests {
|
|||
FromMetaReply::Built {
|
||||
out_path: "/nix/store/x".to_owned(),
|
||||
system: "s".to_owned(),
|
||||
cached: false,
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
|
@ -616,6 +678,20 @@ mod tests {
|
|||
assert_eq!(prompt.expect("read"), "t");
|
||||
}
|
||||
|
||||
#[tokio::test(start_paused = true)]
|
||||
async fn a_push_gets_the_deadline_unless_the_reply_is_due_sooner() {
|
||||
let now = Instant::now();
|
||||
assert_eq!(
|
||||
push_budget(now + Duration::from_hours(1)),
|
||||
push::PUSH_DEADLINE
|
||||
);
|
||||
assert_eq!(
|
||||
push_budget(now + Duration::from_secs(90)),
|
||||
Duration::from_secs(90)
|
||||
);
|
||||
assert_eq!(push_budget(now), Duration::ZERO);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_build_started_at_the_slot_limit_ends_inside_the_reply_deadline() {
|
||||
assert!(MAX_SLOT_WAIT > Duration::ZERO);
|
||||
|
|
|
|||
Loading…
Reference in a new issue