2026-08-15 14:10:52 +00:00
|
|
|
//! Demo-only control plane: write actions out of the dashboard.
|
|
|
|
|
//!
|
|
|
|
|
//! This module exists only under the `demo-control` feature. The dashboard's
|
|
|
|
|
//! data path stays read-only in every regular build; the provisioning
|
|
|
|
|
//! reconciler demo turns this feature on so a human can kill provisioned
|
|
|
|
|
//! processes and request new ones from the fleet view.
|
|
|
|
|
//!
|
|
|
|
|
//! Routes (only present when the feature is enabled and a control sink is
|
|
|
|
|
//! installed):
|
|
|
|
|
//! - `POST /control/kill` body `{"node": "<stream node id>"}`
|
|
|
|
|
//! - `POST /control/provision` body `{"count": 1}`
|
|
|
|
|
|
|
|
|
|
use std::sync::mpsc::Sender;
|
|
|
|
|
use std::sync::OnceLock;
|
|
|
|
|
|
|
|
|
|
use serde::Deserialize;
|
|
|
|
|
|
|
|
|
|
/// A control command issued from the dashboard UI.
|
|
|
|
|
#[derive(Clone, Debug, Deserialize)]
|
|
|
|
|
pub enum ControlCommand {
|
|
|
|
|
/// Kill the process backing the fleet card identified by its stream node.
|
|
|
|
|
Kill { node: String },
|
|
|
|
|
/// Ask the reconciler to provision `count` additional nodes.
|
|
|
|
|
Provision { count: u32 },
|
|
|
|
|
/// Lower the desired cluster size by `count` nodes (graceful scale
|
|
|
|
|
/// down: teardown through the reconciler, not a kill).
|
|
|
|
|
Remove { count: u32 },
|
demo: rename xtask demo command; dashboard-established data-plane edges
Rename `cargo xtask provisioning-reconciler-demo` to `cargo xtask demo`
(CLI dispatch, help, child re-exec argv, launch spec strings, module dir
xtask/src/provisioning_demo -> xtask/src/demo).
Add iteration-1 data-plane edges, established from Fleet Control:
- Fleet Control "edge" button -> POST /control/edge (new
ControlCommand::EstablishEdge) -> supervisor actor resolves the node's
advertised EndpointAddr (stashed in NodeRuntime by AnnounceActor) and
provisions a real outbound EdgeRuntime (arena ring lease, recorder
WorkerPort, EDGE_ALPN send pump) in a new edge pump thread.
- Node gains EDGE_ALPN, an actor bridge decoding EdgeProvision gossip,
and a NodeEdgeAgent that provisions its (single) inbound edge, polls
it, mirrors observations onto the node.edge telemetry channel
(render-only), and answers EdgeAck gossip which terminates the
supervisor's provision retries. Node teardown replaces its inbound on
re-provision; supervisor replaces sessions per node and tears them
down on node exit/replacement/shutdown.
- The edge pump runs on the engine's blocking pool with sole session
ownership (commands in, state mirror + feed lines out): the connect
handshake blocks its thread and must not run on a Tokio worker or
share a lock with the actor. Connects are bounded (10s) so a dead
node faults its session instead of wedging edge polling.
- iroh-driver: retain_telemetry_connections() opts an application out
of the driver-owned TELEMETRY_ALPN ingress so the node's pull server
can drain those connections itself (the actor-bridge pump would
otherwise claim them).
- Dashboard: edges array in the reconciler snapshot, per-node edge
badges and edge button in Fleet Control, node_edges render mirror.
2026-08-16 20:11:20 +00:00
|
|
|
/// Establish (or replace) the data-plane edge toward one node. The
|
|
|
|
|
/// supervisor provisions the node's inbound edge over the control
|
|
|
|
|
/// plane and dials it over EDGE_ALPN.
|
|
|
|
|
EstablishEdge { node: String },
|
2026-08-15 14:10:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static CONTROL_SENDER: OnceLock<Sender<ControlCommand>> = OnceLock::new();
|
|
|
|
|
|
|
|
|
|
/// Install the sink that receives dashboard-issued control commands.
|
|
|
|
|
///
|
|
|
|
|
/// Called once by the embedding demo before the HTTP server starts. Without a
|
|
|
|
|
/// sink the control routes answer `503 Service Unavailable`.
|
|
|
|
|
pub fn set_control_sender(sender: Sender<ControlCommand>) {
|
|
|
|
|
let _ = CONTROL_SENDER.set(sender);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) fn dispatch(command: ControlCommand) -> bool {
|
|
|
|
|
CONTROL_SENDER
|
|
|
|
|
.get()
|
|
|
|
|
.is_some_and(|sender| sender.send(command).is_ok())
|
|
|
|
|
}
|