refactor(#1019): unify agent + manager server dispatch via dispatch_shared

This commit is contained in:
damocles 2026-06-01 21:03:17 +02:00
commit 68f488d81f
2 changed files with 112 additions and 332 deletions

View file

@ -6,7 +6,7 @@
use std::sync::Arc;
use anyhow::{Context, Result};
use hive_sh4re::{MANAGER_AGENT, ManagerRequest, ManagerResponse, Message};
use hive_sh4re::{MANAGER_AGENT, ManagerRequest, ManagerResponse};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::{UnixListener, UnixStream};
@ -74,123 +74,13 @@ async fn serve(stream: UnixStream, coord: Arc<Coordinator>) -> Result<()> {
}
}
/// Max long-poll window for manager `Recv`. Same semantics as the
/// sub-agent socket: omitted `wait_seconds` (or `0`) = peek and
/// return immediately, positive value = park up to that many
/// seconds (clamped at MAX).
const MANAGER_RECV_LONG_POLL_MAX: std::time::Duration = std::time::Duration::from_secs(180);
/// Same shape + rationale as `agent_server::RECV_BATCH_MAX`. Kept
/// numerically aligned across surfaces so a tool description that
/// quotes the cap stays accurate either way.
const MANAGER_RECV_BATCH_MAX: u32 = 32;
fn manager_recv_timeout(wait_seconds: Option<u64>) -> std::time::Duration {
match wait_seconds {
Some(s) => std::time::Duration::from_secs(s).min(MANAGER_RECV_LONG_POLL_MAX),
None => std::time::Duration::ZERO,
}
}
#[allow(clippy::too_many_lines)]
async fn dispatch(req: &ManagerRequest, coord: &Arc<Coordinator>) -> ManagerResponse {
// Delegate all variants shared with the agent socket to the common handler.
if let Some(resp) = crate::agent_server::dispatch_shared(req, MANAGER_AGENT, coord).await {
return resp;
}
match req {
ManagerRequest::Send {
to,
body,
in_reply_to,
} => {
if let Err(message) = crate::limits::check_size("send", body) {
return ManagerResponse::Err { message };
}
if to == "*" {
let errors = coord.broadcast_send(MANAGER_AGENT, body);
if errors.is_empty() {
ManagerResponse::Ok
} else {
ManagerResponse::Err {
message: format!("broadcast failed for agents: {}", errors.join(", ")),
}
}
} else {
// Resolve magic-recipient sentinels (currently `<parent>`)
// against topology.json; no-op for ordinary names. The
// manager has no parent in topology, so `<parent>`
// resolves to OPERATOR_RECIPIENT — the "no parent → tell
// the operator" fallback. See `docs/conventions.md::
// Recipient sentinels`.
let resolved = crate::topology::resolve_recipient(MANAGER_AGENT, to);
match coord.broker.send(&Message {
from: MANAGER_AGENT.to_owned(),
to: resolved,
body: body.clone(),
in_reply_to: *in_reply_to,
}) {
Ok(()) => ManagerResponse::Ok,
Err(e) => ManagerResponse::Err {
message: format!("{e:#}"),
},
}
}
}
ManagerRequest::Wake { from, body } => match coord.broker.send(&Message {
from: from.clone(),
to: MANAGER_AGENT.to_owned(),
body: body.clone(),
in_reply_to: None,
}) {
Ok(()) => ManagerResponse::Ok,
Err(e) => ManagerResponse::Err {
message: format!("{e:#}"),
},
},
ManagerRequest::OperatorMsg { body } => match coord.broker.send(&Message {
from: hive_sh4re::OPERATOR_RECIPIENT.to_owned(),
to: MANAGER_AGENT.to_owned(),
body: body.clone(),
in_reply_to: None,
}) {
Ok(()) => ManagerResponse::Ok,
Err(e) => ManagerResponse::Err {
message: format!("{e:#}"),
},
},
ManagerRequest::Status => match coord.broker.count_pending(MANAGER_AGENT) {
Ok(unread) => ManagerResponse::Status { unread },
Err(e) => ManagerResponse::Err {
message: format!("{e:#}"),
},
},
ManagerRequest::Recent { limit } => match coord.broker.recent_for(MANAGER_AGENT, *limit) {
Ok(rows) => ManagerResponse::Recent { rows },
Err(e) => ManagerResponse::Err {
message: format!("{e:#}"),
},
},
ManagerRequest::Recv { wait_seconds, max } => {
let cap = max.unwrap_or(1).min(MANAGER_RECV_BATCH_MAX) as usize;
match coord
.broker
.recv_blocking_batch(MANAGER_AGENT, manager_recv_timeout(*wait_seconds), cap)
.await
{
Ok(deliveries) => ManagerResponse::Messages {
messages: deliveries
.into_iter()
.map(|d| hive_sh4re::DeliveredMessage {
from: d.message.from,
body: d.message.body,
id: d.id,
redelivered: d.redelivered,
in_reply_to: d.message.in_reply_to,
})
.collect(),
},
Err(e) => ManagerResponse::Err {
message: format!("{e:#}"),
},
}
}
ManagerRequest::RequestInitConfig { name, description } => {
tracing::info!(%name, "manager: request_init_config");
let proposed_dir = crate::coordinator::Coordinator::agent_proposed_dir(name);
@ -375,31 +265,6 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc<Coordinator>) -> ManagerResp
ManagerRequest::FireScheduleNow { id } => {
handle_fire_schedule_now(coord, hive_sh4re::MANAGER_AGENT, *id).await
}
ManagerRequest::Ask {
question,
options,
multi,
ttl_seconds,
to,
} => crate::questions::handle_ask(
coord,
MANAGER_AGENT,
question,
options,
*multi,
*ttl_seconds,
to.as_deref(),
)
.map_or_else(
|message| ManagerResponse::Err { message },
|id| ManagerResponse::QuestionQueued { id },
),
ManagerRequest::Answer { id, answer } => {
crate::questions::handle_answer(coord, MANAGER_AGENT, *id, answer).map_or_else(
|message| ManagerResponse::Err { message },
|()| ManagerResponse::Ok,
)
}
ManagerRequest::GetLogs { agent, lines } => {
let n = lines.unwrap_or(50);
// `journalctl -M` wants the *machine* name, not the
@ -439,20 +304,6 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc<Coordinator>) -> ManagerResp
},
}
}
ManagerRequest::Remind {
message,
timing,
file_path,
} => match crate::agent_server::store_remind(
coord,
MANAGER_AGENT,
message,
timing,
file_path.as_deref(),
) {
Ok(()) => ManagerResponse::Ok,
Err(message) => ManagerResponse::Err { message },
},
ManagerRequest::RequestApplyCommit {
agent,
commit_ref,
@ -500,94 +351,9 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc<Coordinator>) -> ManagerResp
},
}
}
ManagerRequest::SetStatus { text } => {
// Cap length + reject multi-line so a confused caller
// can't dump a multi-paragraph session report into the
// dashboard chip.
if let Err(message) = crate::limits::check_status_text(text) {
return ManagerResponse::Err { message };
}
let path = Coordinator::agent_notes_dir(MANAGER_AGENT).join("hyperhive-status");
let result = if text.trim().is_empty() {
std::fs::remove_file(&path).or_else(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
Ok(())
} else {
Err(e)
}
})
} else {
std::fs::write(&path, format!("{}\n", text.trim()))
};
match result {
Ok(()) => {
let coord2 = Arc::clone(coord);
tokio::spawn(async move { coord2.rescan_containers_and_emit().await });
ManagerResponse::Ok
}
Err(e) => ManagerResponse::Err {
message: format!("set_status write failed: {e}"),
},
}
}
ManagerRequest::GetAgentMeta { name } => {
let target = name.as_deref().unwrap_or(MANAGER_AGENT);
// Gate status on the target's running state so a stopped
// container's stale on-disk status doesn't leak through.
// Also surface `running` itself so callers can tell
// (e.g. "iris is down" vs "iris has no status set").
let (status_text, status_set_at, running) =
crate::container_view::read_agent_status_live(target).await;
let role = if target == MANAGER_AGENT {
"manager"
} else {
"agent"
}
.to_owned();
let (hive_name, swarm_name) = crate::container_view::hive_swarm_names();
ManagerResponse::AgentMeta {
name: target.to_owned(),
role,
running,
hyperhive_rev: crate::auto_update::current_flake_rev(&coord.hyperhive_flake),
status_text,
status_set_at,
hive_name,
swarm_name,
}
}
ManagerRequest::CancelLooseEnd { kind, id } => {
crate::questions::handle_cancel_loose_end(coord, MANAGER_AGENT, *kind, *id).map_or_else(
|message| ManagerResponse::Err { message },
|()| ManagerResponse::Ok,
)
}
ManagerRequest::AckTurn => match coord.broker.ack_turn(MANAGER_AGENT) {
Ok(_n) => ManagerResponse::Ok,
Err(e) => ManagerResponse::Err {
message: format!("{e:#}"),
},
_ => ManagerResponse::Err {
message: "request not handled on manager socket".to_owned(),
},
ManagerRequest::RequeueInflight => match coord.broker.requeue_inflight(MANAGER_AGENT) {
Ok(n) => {
if n > 0 {
tracing::info!(agent = %MANAGER_AGENT, requeued = %n, "requeued in-flight messages");
}
ManagerResponse::Ok
}
Err(e) => ManagerResponse::Err {
message: format!("{e:#}"),
},
},
// GetHostJournal is an agent-socket-only capability-gated variant.
// The manager can use the existing GetLogs tool for per-container
// logs. Route to the agent_server handler for consistency.
ManagerRequest::GetHostJournal { unit, container, lines, priority, grep, since, until } => {
crate::agent_server::dispatch_host_journal(
MANAGER_AGENT, unit, container, lines, priority, grep, since, until,
)
.await
}
}
}