log failed controller jobs, re-run the meta update every ten minutes
This commit is contained in:
parent
acfdcd725f
commit
0f1ba6e630
4 changed files with 153 additions and 17 deletions
|
|
@ -143,7 +143,8 @@ 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, before it deploys a merged config PR, and
|
repo. It re-renders when it starts and every ten minutes after (a tick skips
|
||||||
|
while an update is pending or running), 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
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,9 @@ The swarm's control plane. The swarm UI and `swarmctl` are its clients.
|
||||||
reported takes its hive and declared state in `/api/agents/status` from the
|
reported takes its hive and declared state in `/api/agents/status` from the
|
||||||
one hive whose declaration places it.
|
one hive whose declaration places it.
|
||||||
- **Job graph** — a `hive-jobq` scheduler, served at `GET /api/jobq/graph`.
|
- **Job graph** — a `hive-jobq` scheduler, served at `GET /api/jobq/graph`.
|
||||||
Every provisioning step below runs as a node in it.
|
Every provisioning step below runs as a node in it. A node that fails logs
|
||||||
|
one `warn` line, `swarm jobq: node failed`, with its `node` id, `kind` and
|
||||||
|
`error`.
|
||||||
- **Agent creation** — `POST /api/agents` queues the SSO identity (through
|
- **Agent creation** — `POST /api/agents` queues the SSO identity (through
|
||||||
`swarm-authelia-bridge`), forge user, config repo, store identity, forge
|
`swarm-authelia-bridge`), forge user, config repo, store identity, forge
|
||||||
token and matrix account, declares the agent `paused`, then sends its hive
|
token and matrix account, declares the agent `paused`, then sends its hive
|
||||||
|
|
|
||||||
|
|
@ -291,11 +291,28 @@ struct MetaDeps {
|
||||||
const NEW_AGENT_WANTED_STATE: swarm_wire::wanted::AgentState =
|
const NEW_AGENT_WANTED_STATE: swarm_wire::wanted::AgentState =
|
||||||
swarm_wire::wanted::AgentState::Paused;
|
swarm_wire::wanted::AgentState::Paused;
|
||||||
|
|
||||||
|
/// Run a claimed node and log one warn when it fails, whatever its kind.
|
||||||
|
pub(crate) async fn run_swarm_node(
|
||||||
|
id: hive_jobq::NodeId,
|
||||||
|
kind: SwarmNodeKind,
|
||||||
|
builder: hive_jobq::builder::JobBuilder<SwarmNodeKind, SwarmResourceKind>,
|
||||||
|
deps: WorkerDeps,
|
||||||
|
) -> (
|
||||||
|
hive_jobq::builder::JobBuilder<SwarmNodeKind, SwarmResourceKind>,
|
||||||
|
hive_jobq::scheduler::Outcome,
|
||||||
|
) {
|
||||||
|
let node_kind = hive_jobq_wire::WireNode::label(&kind);
|
||||||
|
let (builder, outcome) = run_node_work(kind, builder, deps).await;
|
||||||
|
if let hive_jobq::scheduler::Outcome::Failed(error) = &outcome {
|
||||||
|
tracing::warn!(node = id.get(), kind = %node_kind, %error, "swarm jobq: node failed");
|
||||||
|
}
|
||||||
|
(builder, outcome)
|
||||||
|
}
|
||||||
|
|
||||||
/// Run a claimed node's actual work. Mirrors `hive-c0re/src/job_queue/
|
/// Run a claimed node's actual work. Mirrors `hive-c0re/src/job_queue/
|
||||||
/// exec.rs::run_node`'s role exactly — the one place a `SwarmNodeKind`
|
/// exec.rs::run_node`'s role exactly — the one place a `SwarmNodeKind`
|
||||||
/// variant turns into a real effect.
|
/// variant turns into a real effect.
|
||||||
pub(crate) async fn run_swarm_node(
|
async fn run_node_work(
|
||||||
_id: hive_jobq::NodeId,
|
|
||||||
kind: SwarmNodeKind,
|
kind: SwarmNodeKind,
|
||||||
builder: hive_jobq::builder::JobBuilder<SwarmNodeKind, SwarmResourceKind>,
|
builder: hive_jobq::builder::JobBuilder<SwarmNodeKind, SwarmResourceKind>,
|
||||||
deps: WorkerDeps,
|
deps: WorkerDeps,
|
||||||
|
|
@ -431,7 +448,7 @@ async fn forge_node<T>(
|
||||||
/// together.
|
/// together.
|
||||||
const MINT_IDENTITY_FAILED: &str = "swarm jobq: mint_agent_identity failed";
|
const MINT_IDENTITY_FAILED: &str = "swarm jobq: mint_agent_identity failed";
|
||||||
|
|
||||||
/// The `MintAgentIdentity` arm, lifted out so `run_swarm_node` stays under
|
/// The `MintAgentIdentity` arm, lifted out so `run_node_work` stays under
|
||||||
/// `clippy::too_many_lines`.
|
/// `clippy::too_many_lines`.
|
||||||
pub(crate) async fn mint_identity(agent: &str) -> hive_jobq::scheduler::Outcome {
|
pub(crate) async fn mint_identity(agent: &str) -> hive_jobq::scheduler::Outcome {
|
||||||
use hive_jobq::scheduler::Outcome;
|
use hive_jobq::scheduler::Outcome;
|
||||||
|
|
@ -446,7 +463,7 @@ pub(crate) async fn mint_identity(agent: &str) -> hive_jobq::scheduler::Outcome
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The `MintAgentForgeToken` arm, lifted out so `run_swarm_node` stays under
|
/// The `MintAgentForgeToken` arm, lifted out so `run_node_work` stays under
|
||||||
/// `clippy::too_many_lines`.
|
/// `clippy::too_many_lines`.
|
||||||
async fn mint_forge_token(
|
async fn mint_forge_token(
|
||||||
forge: Option<Arc<forge::Client>>,
|
forge: Option<Arc<forge::Client>>,
|
||||||
|
|
@ -681,7 +698,7 @@ fn finish_update_meta(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The `RecordFanout` arm, lifted out so `run_swarm_node` stays under the line limit.
|
/// The `RecordFanout` arm, lifted out so `run_node_work` stays under the line limit.
|
||||||
async fn record_fanout(
|
async fn record_fanout(
|
||||||
queue: Option<&async_nats::Client>,
|
queue: Option<&async_nats::Client>,
|
||||||
pin: &str,
|
pin: &str,
|
||||||
|
|
@ -1450,18 +1467,42 @@ pub(crate) fn queue_builder_forge_token_mint(
|
||||||
.map_err(|e| anyhow::anyhow!("{e}"))
|
.map_err(|e| anyhow::anyhow!("{e}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Insert one `UpdateMeta` job unless one is already pending, which reads the
|
/// How often [`spawn_meta_update_timer`] queues an `UpdateMeta`.
|
||||||
|
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
|
||||||
|
/// boot run that failed or hung, and drift the config-merge path did not see,
|
||||||
|
/// are picked up. A tick that finds the previous run pending, running or
|
||||||
|
/// finishing queues nothing.
|
||||||
|
pub(crate) fn spawn_meta_update_timer(
|
||||||
|
sched: Arc<Mutex<hive_jobq::scheduler::Scheduler<SwarmNodeKind, SwarmResourceKind>>>,
|
||||||
|
) {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let mut ticker = tokio::time::interval(META_UPDATE_INTERVAL);
|
||||||
|
loop {
|
||||||
|
ticker.tick().await;
|
||||||
|
if let Err(e) = queue_meta_update(&sched) {
|
||||||
|
tracing::warn!(error = %format!("{e:#}"), "update_meta: queueing failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert one `UpdateMeta` job unless one is still in flight, which reads the
|
||||||
/// same heads when it runs. Returns whether a job was inserted.
|
/// same heads when it runs. Returns whether a job was inserted.
|
||||||
pub(crate) 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 pending = sched.graph().nodes().any(|n| {
|
let in_flight = sched.graph().nodes().any(|n| {
|
||||||
n.state == hive_jobq::State::Pending && matches!(n.payload, SwarmNodeKind::UpdateMeta)
|
matches!(
|
||||||
|
n.state,
|
||||||
|
hive_jobq::State::Pending | hive_jobq::State::Running | hive_jobq::State::Finishing
|
||||||
|
) && matches!(n.payload, SwarmNodeKind::UpdateMeta)
|
||||||
});
|
});
|
||||||
if pending {
|
if in_flight {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
sched
|
sched
|
||||||
|
|
@ -3073,6 +3114,42 @@ mod tests {
|
||||||
labels.sort();
|
labels.sort();
|
||||||
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
|
||||||
|
/// queues nothing while one is pending or running; once it is done the
|
||||||
|
/// next tick queues again.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn the_meta_timer_queues_unless_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(),
|
||||||
|
)));
|
||||||
|
let update_metas = || {
|
||||||
|
sched
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.graph()
|
||||||
|
.nodes()
|
||||||
|
.filter(|n| matches!(n.payload, SwarmNodeKind::UpdateMeta))
|
||||||
|
.count()
|
||||||
|
};
|
||||||
|
assert!(super::queue_meta_update(&sched).expect("tick"));
|
||||||
|
assert_eq!(update_metas(), 1);
|
||||||
|
assert!(!super::queue_meta_update(&sched).expect("tick"));
|
||||||
|
assert_eq!(update_metas(), 1, "a pending one blocks the tick");
|
||||||
|
|
||||||
|
let running = hive_jobq::scheduler::Scheduler::claim_next(&sched, |_, _, b| async move {
|
||||||
|
(b, hive_jobq::scheduler::Outcome::Done)
|
||||||
|
})
|
||||||
|
.expect("runnable");
|
||||||
|
assert!(!super::queue_meta_update(&sched).expect("tick"));
|
||||||
|
assert_eq!(update_metas(), 1, "a running one blocks the tick");
|
||||||
|
|
||||||
|
running.await.1.expect("no growth");
|
||||||
|
assert!(super::queue_meta_update(&sched).expect("tick"));
|
||||||
|
assert_eq!(update_metas(), 2, "a finished one does not");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
@ -3082,7 +3159,7 @@ mod mint_identity_tests {
|
||||||
use hive_jobq::scheduler::Outcome;
|
use hive_jobq::scheduler::Outcome;
|
||||||
use tracing_subscriber::layer::SubscriberExt as _;
|
use tracing_subscriber::layer::SubscriberExt as _;
|
||||||
|
|
||||||
use super::mint_identity;
|
use super::{SwarmNodeKind, WorkerDeps, mint_identity, run_swarm_node};
|
||||||
|
|
||||||
/// One event's level and its fields, each by name.
|
/// One event's level and its fields, each by name.
|
||||||
type Event = (tracing::Level, Vec<(String, String)>);
|
type Event = (tracing::Level, Vec<(String, String)>);
|
||||||
|
|
@ -3139,4 +3216,62 @@ mod mint_identity_tests {
|
||||||
assert_eq!(field("agent"), "a/b");
|
assert_eq!(field("agent"), "a/b");
|
||||||
assert_eq!(field("error"), error);
|
assert_eq!(field("error"), error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A node of any kind whose work returns `Failed` logs one warn naming the
|
||||||
|
/// node's id, its kind and the error, from the one place that runs nodes.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn a_failed_node_logs_its_kind_id_and_error() {
|
||||||
|
let events = Events::default();
|
||||||
|
let _guard =
|
||||||
|
tracing::subscriber::set_default(tracing_subscriber::registry().with(events.clone()));
|
||||||
|
let sched = Arc::new(Mutex::new(hive_jobq::scheduler::Scheduler::new(
|
||||||
|
hive_jobq::Graph::new(),
|
||||||
|
hive_jobq::resources::ResourceTable::new(),
|
||||||
|
)));
|
||||||
|
let ids = sched
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.insert_job(None, |b| {
|
||||||
|
vec![
|
||||||
|
b.node(SwarmNodeKind::SetAgentWanted {
|
||||||
|
hive: "h".to_owned(),
|
||||||
|
agent: "a".to_owned(),
|
||||||
|
})
|
||||||
|
.guid(),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.expect("inserted");
|
||||||
|
let deps = WorkerDeps {
|
||||||
|
auth: None,
|
||||||
|
forge: super::Late::default(),
|
||||||
|
queue: None,
|
||||||
|
wanted: None,
|
||||||
|
matrix_homeserver: None,
|
||||||
|
};
|
||||||
|
hive_jobq::scheduler::Scheduler::claim_next(&sched, move |id, kind, builder| {
|
||||||
|
run_swarm_node(id, kind, builder, deps)
|
||||||
|
})
|
||||||
|
.expect("runnable")
|
||||||
|
.await
|
||||||
|
.1
|
||||||
|
.expect("no growth declared");
|
||||||
|
|
||||||
|
let got = events.0.lock().expect("unpoisoned").clone();
|
||||||
|
let [(level, fields)] = got.as_slice() else {
|
||||||
|
panic!("one event, got {got:?}");
|
||||||
|
};
|
||||||
|
assert_eq!(*level, tracing::Level::WARN);
|
||||||
|
let field = |name: &str| {
|
||||||
|
fields
|
||||||
|
.iter()
|
||||||
|
.find(|(n, _)| n == name)
|
||||||
|
.map_or_else(|| panic!("no {name} in {fields:?}"), |(_, v)| v.clone())
|
||||||
|
};
|
||||||
|
assert_eq!(field("kind"), "set_agent_wanted");
|
||||||
|
assert_eq!(field("node"), ids[0].get().to_string());
|
||||||
|
assert!(
|
||||||
|
field("error").contains("no swarm queue is configured"),
|
||||||
|
"{fields:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -288,10 +288,8 @@ fn spawn_forge_workers(
|
||||||
) {
|
) {
|
||||||
// `internal/meta` appears, catches up with every config head that moved
|
// `internal/meta` appears, catches up with every config head that moved
|
||||||
// while this daemon was down, and takes this daemon's rev as the hyperhive
|
// while this daemon was down, and takes this daemon's rev as the hyperhive
|
||||||
// pin when that is newer.
|
// pin when that is newer. The timer's first tick is this boot run.
|
||||||
if let Err(e) = crate::jobq::queue_meta_update(jobq) {
|
crate::jobq::spawn_meta_update_timer(Arc::clone(jobq));
|
||||||
tracing::warn!(error = %format!("{e:#}"), "update_meta: queueing failed");
|
|
||||||
}
|
|
||||||
forge::objects::spawn(Arc::clone(&client));
|
forge::objects::spawn(Arc::clone(&client));
|
||||||
if let (Some(bridge), Some(queue)) = (auth, queue) {
|
if let (Some(bridge), Some(queue)) = (auth, queue) {
|
||||||
forge::operator_profiles::spawn(Arc::clone(&client), bridge, queue);
|
forge::operator_profiles::spawn(Arc::clone(&client), bridge, queue);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue