Read-Only State Endpoints#95
Conversation
- GET /api/services — all services with replica addresses and active session counts - GET /api/nodes — connected agent nodes and their hosted services - GET /api/pool — net ID pool capacity, in-use, and free counts - GET /api/config — raw services.toml as text/plain - GET /api/graph — live graphviz dot graph as text/plain Supporting changes: - NetIdPool::stats() returns (capacity, in_use, free); unit tests added - Orchestrator::connected_node_ips() and pool_stats() expose state for HTTP layer - NullnetGrpcImpl::services() and orchestrator() promoted from test-only - http_server refactored into a module with one file per handler
Adds render_graph_json() alongside the existing render_graphviz(). Extracts the shared initiators() helper so both renderers use the same active-replica logic. The handler now returns axum::Json with nodes and edges; via_proxy is omitted on service-to-service edges.
| } | ||
|
|
||
| pub(crate) async fn connected_node_ips(&self) -> Vec<IpAddr> { | ||
| self.clients.read().await.keys().cloned().collect() |
There was a problem hiding this comment.
minor, not needed to fix now but IIRC IP data structures implement Copy so no need to clone them. I'm just doing a static review so no 100% sure but this may become a .copied()
| pub(crate) fn stats(&self) -> (u32, u32, u32) { | ||
| let capacity = *MAX_NET_ID - MIN_NET_ID + 1; | ||
| let in_use = (self.next_fresh - MIN_NET_ID) - self.freed.len() as u32; | ||
| (capacity, in_use, capacity - in_use) |
There was a problem hiding this comment.
ok for now but maybe consider computing free client-side to simplify this method's signature
| .with_state(state); | ||
|
|
||
| let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), HTTP_PORT); | ||
| let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); |
There was a problem hiding this comment.
let's try not to use any unwrap going forward. when not possible to handle error in any way, use expect.
|
@antoncxx merged but left some minor comments for you to later fix. Another tip: to visually test if your graphs are correct, you can run tests against the service files I have in the test directory, and see if they look consistent (in their nodes / edges / labels representation) to the corresponding graphviz files, always in the test dir |
Summary
Adds five read-only HTTP endpoints to the control plane server, exposing the full in-memory state to clients over the existing port 8080 server introduced in PR 1. No new logic is introduced — this is serialisation and routing only.
Endpoints
GET/api/servicesGET/api/nodesGET/api/poolGET/api/configservices.tomlastext/plainGET/api/graph/api/graphresponse shape{ "nodes": [ { "id": "color.com", "registered": true, "entry_point": true, "replica_count": 1, "active_replica_count": 1 } ], "edges": [ { "from": "color.com", "to": "fs.color.com", "net_id": 101, "setup_ms": 23 }, { "from": "10.0.0.50", "via_proxy": "10.0.0.10", "to": "color.com", "net_id": 102, "setup_ms": 15 } ] }via_proxyis omitted on service-to-service edges.Changes
http_server— split into a modulehttp_server.rsis replaced byhttp_server/with one file per handler:AppState(holdingArc<RwLock<HashMap<String, ServiceInfo>>>andOrchestrator) is passed intoserve()frommain.rsand threaded to handlers via axumState.graphviz.rsinitiators()helper (previously inlined inrender_graphviz)render_graph_json()returning a serialisableGraphJsonstruct; both renderers use the same active-replica logic via the shared helperorchestrator.rsconnected_node_ips()— returns IPs of all nodes with an active control channelpool_stats()— delegates toNetIdPool::stats()net_id_pool.rsstats()— returns(total_capacity, in_use, free)as a tupletotal == in_use + freeinvariantnullnet_grpc_impl.rsservices()andorchestrator()accessors promoted from#[cfg(test)]to always-available somain.rscan extract shared state refs before handing the struct to tonic