Watch
0
0
Fork
You've already forked hyperhive
0

queue the meta update only until one run succeeds

This commit is contained in:
atlas 2026-10-25 12:56:30 +01:00
commit 31cb6bfa42
2 changed files with 91 additions and 32 deletions

View file

@ -143,8 +143,9 @@ running rev (`services.hyperhive.c0re.hyperhiveRev`, from `self.rev`):
The controller commits `flake.nix` and the returned `flake.lock` to The controller commits `flake.nix` and the returned `flake.lock` to
`internal/meta` in one commit, and gives each hive's forge account read on the `internal/meta` in one commit, and gives each hive's forge account read on the
repo. It re-renders when it starts and every ten minutes after (a tick skips repo. It re-renders when it starts, and again every ten minutes while the latest
while an update is pending or running), before it deploys a merged config PR, and update hasn't succeeded (a tick adds nothing while one is pending or running,
or after a success), before it deploys a merged config PR, and
before the first deploy of a new agent. Those deploys run whether or not the before the first deploy of a new agent. Those deploys run whether or not the
update succeeded: a hive that builds from `internal/meta` does so only while it update succeeded: a hive that builds from `internal/meta` does so only while it
pins the agent's current config (see pins the agent's current config (see

View file

@ -1467,13 +1467,14 @@ pub(crate) fn queue_builder_forge_token_mint(
.map_err(|e| anyhow::anyhow!("{e}")) .map_err(|e| anyhow::anyhow!("{e}"))
} }
/// How often [`spawn_meta_update_timer`] queues an `UpdateMeta`. /// How often [`spawn_meta_update_timer`] looks at the last `UpdateMeta`.
const META_UPDATE_INTERVAL: std::time::Duration = std::time::Duration::from_mins(10); const META_UPDATE_INTERVAL: std::time::Duration = std::time::Duration::from_mins(10);
/// Queue an `UpdateMeta` now and every [`META_UPDATE_INTERVAL`] after, so a /// Look at the last `UpdateMeta` now and every [`META_UPDATE_INTERVAL`] after,
/// boot run that failed or hung, and drift the config-merge path did not see, /// and queue one when none has run yet or the latest did not succeed, so a
/// are picked up. A tick that finds the previous run pending, running or /// boot run that failed or hung is retried. The first tick is the boot run.
/// finishing queues nothing. /// Once a run has succeeded a tick adds nothing: the graph keeps every node,
/// so a clock-driven node per tick would grow it without bound.
pub(crate) fn spawn_meta_update_timer( pub(crate) fn spawn_meta_update_timer(
sched: Arc<Mutex<hive_jobq::scheduler::Scheduler<SwarmNodeKind, SwarmResourceKind>>>, sched: Arc<Mutex<hive_jobq::scheduler::Scheduler<SwarmNodeKind, SwarmResourceKind>>>,
) { ) {
@ -1488,21 +1489,32 @@ pub(crate) fn spawn_meta_update_timer(
}); });
} }
/// Insert one `UpdateMeta` job unless one is still in flight, which reads the /// Insert one `UpdateMeta` job when none is in flight and the latest one, by
/// same heads when it runs. Returns whether a job was inserted. /// node id, is not `Done` (or none exists). Returns whether a job was inserted.
fn queue_meta_update( fn queue_meta_update(
sched: &Mutex<hive_jobq::scheduler::Scheduler<SwarmNodeKind, SwarmResourceKind>>, sched: &Mutex<hive_jobq::scheduler::Scheduler<SwarmNodeKind, SwarmResourceKind>>,
) -> Result<bool> { ) -> Result<bool> {
let mut sched = sched let mut sched = sched
.lock() .lock()
.unwrap_or_else(std::sync::PoisonError::into_inner); .unwrap_or_else(std::sync::PoisonError::into_inner);
let in_flight = sched.graph().nodes().any(|n| { let mut update_metas = sched
.graph()
.nodes()
.filter(|n| matches!(n.payload, SwarmNodeKind::UpdateMeta))
.collect::<Vec<_>>();
if update_metas.iter().any(|n| {
matches!( matches!(
n.state, n.state,
hive_jobq::State::Pending | hive_jobq::State::Running | hive_jobq::State::Finishing hive_jobq::State::Pending | hive_jobq::State::Running | hive_jobq::State::Finishing
) && matches!(n.payload, SwarmNodeKind::UpdateMeta) )
}); }) {
if in_flight { return Ok(false);
}
update_metas.sort_by_key(|n| n.id);
if update_metas
.last()
.is_some_and(|n| n.state == hive_jobq::State::Done)
{
return Ok(false); return Ok(false);
} }
sched sched
@ -3115,17 +3127,29 @@ mod tests {
assert_eq!(labels, ["create_forge_user", "mint_agent_forge_token"]); assert_eq!(labels, ["create_forge_user", "mint_agent_forge_token"]);
} }
/// The timer's tick queues an `UpdateMeta` when none is in flight, and /// A scheduler holding one `UpdateMeta` that ran and ended `outcome`.
/// queues nothing while one is pending or running; once it is done the async fn after_one_update_meta(
/// next tick queues again. outcome: hive_jobq::scheduler::Outcome,
#[tokio::test] ) -> std::sync::Arc<
async fn the_meta_timer_queues_unless_one_is_in_flight() { std::sync::Mutex<hive_jobq::scheduler::Scheduler<SwarmNodeKind, SwarmResourceKind>>,
> {
let sched = let sched =
std::sync::Arc::new(std::sync::Mutex::new(hive_jobq::scheduler::Scheduler::new( std::sync::Arc::new(std::sync::Mutex::new(hive_jobq::scheduler::Scheduler::new(
hive_jobq::Graph::new(), hive_jobq::Graph::new(),
hive_jobq::resources::ResourceTable::new(), hive_jobq::resources::ResourceTable::new(),
))); )));
let update_metas = || { assert!(super::queue_meta_update(&sched).expect("tick"));
hive_jobq::scheduler::Scheduler::claim_next(&sched, |_, _, b| async move { (b, outcome) })
.expect("runnable")
.await
.1
.expect("no growth");
sched
}
fn update_metas(
sched: &std::sync::Mutex<hive_jobq::scheduler::Scheduler<SwarmNodeKind, SwarmResourceKind>>,
) -> usize {
sched sched
.lock() .lock()
.unwrap() .unwrap()
@ -3133,22 +3157,56 @@ mod tests {
.nodes() .nodes()
.filter(|n| matches!(n.payload, SwarmNodeKind::UpdateMeta)) .filter(|n| matches!(n.payload, SwarmNodeKind::UpdateMeta))
.count() .count()
}; }
/// With none run yet the tick queues one, and queues no second while that
/// one is pending or running.
#[tokio::test]
async fn the_meta_timer_queues_first_and_skips_while_one_is_in_flight() {
let sched =
std::sync::Arc::new(std::sync::Mutex::new(hive_jobq::scheduler::Scheduler::new(
hive_jobq::Graph::new(),
hive_jobq::resources::ResourceTable::new(),
)));
assert!(super::queue_meta_update(&sched).expect("tick")); assert!(super::queue_meta_update(&sched).expect("tick"));
assert_eq!(update_metas(), 1);
assert!(!super::queue_meta_update(&sched).expect("tick")); assert!(!super::queue_meta_update(&sched).expect("tick"));
assert_eq!(update_metas(), 1, "a pending one blocks the tick"); assert_eq!(update_metas(&sched), 1, "a pending one blocks the tick");
let running = hive_jobq::scheduler::Scheduler::claim_next(&sched, |_, _, b| async move { let running = hive_jobq::scheduler::Scheduler::claim_next(&sched, |_, _, b| async move {
(b, hive_jobq::scheduler::Outcome::Done) (b, hive_jobq::scheduler::Outcome::Failed("late".to_owned()))
}) })
.expect("runnable"); .expect("runnable");
assert!(!super::queue_meta_update(&sched).expect("tick")); assert!(!super::queue_meta_update(&sched).expect("tick"));
assert_eq!(update_metas(), 1, "a running one blocks the tick"); assert_eq!(update_metas(&sched), 1, "a running one blocks the tick");
running.await.1.expect("no growth"); running.await.1.expect("no growth");
}
/// Once the latest run succeeded, ticks add no node.
#[tokio::test]
async fn the_meta_timer_adds_nothing_after_a_success() {
let sched = after_one_update_meta(hive_jobq::scheduler::Outcome::Done).await;
for _ in 0..3 {
assert!(!super::queue_meta_update(&sched).expect("tick"));
}
assert_eq!(update_metas(&sched), 1);
}
/// A failed latest run is retried; a success after it ends the retries.
#[tokio::test]
async fn the_meta_timer_retries_after_a_failure_until_a_success() {
let sched =
after_one_update_meta(hive_jobq::scheduler::Outcome::Failed("no".to_owned())).await;
assert!(super::queue_meta_update(&sched).expect("tick")); assert!(super::queue_meta_update(&sched).expect("tick"));
assert_eq!(update_metas(), 2, "a finished one does not"); assert_eq!(update_metas(&sched), 2);
hive_jobq::scheduler::Scheduler::claim_next(&sched, |_, _, b| async move {
(b, hive_jobq::scheduler::Outcome::Done)
})
.expect("runnable")
.await
.1
.expect("no growth");
assert!(!super::queue_meta_update(&sched).expect("tick"));
assert_eq!(update_metas(&sched), 2);
} }
} }