Skip to main content

clawft_kernel/infer_proxy/
hub.rs

1//! The mesh side of the stable inference address in a daemon that owns its
2//! mesh connections (collapsed or legacy mode): adverts and forwarded
3//! requests travel as control messages on [`INFER_TOPIC`], handed to the
4//! hub with the connection's own [`PeerCtx`].
5//!
6//! Everything that decides trust comes from that context, never from the
7//! payload: the sender of an advert is `ctx.peer_id`, the standing of a peer
8//! is the grant its connection holds ([`PeerCtx::grant`]), and a served
9//! request is checked against it by [`serve_infer`]. The hub never runs in
10//! service mode: there the daemon is handed deliveries as an unverified peer
11//! and holds no grant for anyone (see `mesh_local_sink`), so nothing could be
12//! served or ingested safely.
13//!
14//! Each forwarded request is one exchange keyed by `(peer, id)`; the
15//! exchange is presented to [`forward_remote`] and [`serve_infer`] as a
16//! [`MeshStream`], so the forwarding code is the same one the tests drive
17//! over in-memory streams.
18
19use std::net::SocketAddr;
20use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
21use std::time::{Duration, Instant};
22use std::sync::{Arc, OnceLock};
23
24use async_trait::async_trait;
25use dashmap::DashMap;
26use serde_json::{Value, json};
27use tokio::sync::{Semaphore, mpsc};
28
29use super::mesh_forward::{InferPeer, ServeGate, serve_infer};
30use super::table::PlacementTable;
31use super::types::{MeshDialer, ProxyAudit, ProxyError, ProxyLimits, qualifies};
32use super::upstream::Upstream;
33use crate::ipc::{KernelMessage, MessagePayload, MessageTarget};
34use crate::mesh::{MeshError, MeshStream};
35use crate::mesh_admit::{Grant, hex_decode, hex_encode};
36use crate::mesh_delivery::PeerCtx;
37use crate::mesh_framing::{FrameType, MeshFrame};
38use crate::mesh_runtime::{INFER_TOPIC, MeshRuntime, PeerControlSink};
39use crate::mesh_service_adv::ServiceAdvertisement;
40
41/// Responses buffered per exchange before the exchange is cancelled.
42const RESP_BUFFER: usize = 64;
43/// Requests being taken in at once, over all peers.
44const INTAKE: usize = 32;
45/// Exchanges in flight at once, over all peers.
46const MAX_PENDING: usize = 256;
47/// Refusals answered per peer per window; the rest are dropped silently.
48const REFUSALS: u32 = 8;
49const REFUSAL_WINDOW: Duration = Duration::from_secs(10);
50
51type Pending = Arc<DashMap<(String, u64), mpsc::Sender<Vec<u8>>>>;
52
53/// Adverts, forwarded requests and their responses between peers.
54pub struct InferHub {
55    rt: Arc<MeshRuntime>,
56    table: OnceLock<Arc<PlacementTable>>,
57    upstream: Arc<Upstream>,
58    gate: Arc<ServeGate>,
59    audit: Option<Arc<dyn ProxyAudit>>,
60    /// Per peer: the connection a grant was seen on, and the grant.
61    grants: DashMap<String, (u64, Grant)>,
62    max_pending: AtomicUsize,
63    refusals: DashMap<String, (Instant, u32)>,
64    pending: Pending,
65    next_id: AtomicU64,
66    intake: Arc<Semaphore>,
67    limits: ProxyLimits,
68    me: OnceLock<std::sync::Weak<Self>>,
69}
70
71fn msg(value: Value) -> KernelMessage {
72    KernelMessage::new(
73        0,
74        MessageTarget::Topic(INFER_TOPIC.to_string()),
75        MessagePayload::Json(value),
76    )
77}
78
79fn mesh_err(e: impl std::fmt::Display) -> MeshError {
80    MeshError::Transport(e.to_string())
81}
82
83/// `[len][type][payload]` -> `(type, payload)`.
84fn split_frame(data: &[u8]) -> Result<MeshFrame, MeshError> {
85    if data.len() < 5 {
86        return Err(mesh_err("short frame"));
87    }
88    MeshFrame::decode(&data[4..])
89}
90
91impl InferHub {
92    /// A hub over `rt`. Call [`attach`](Self::attach) with the table built
93    /// around it, then [`install`](Self::install).
94    pub fn new(
95        rt: Arc<MeshRuntime>,
96        upstream: Arc<Upstream>,
97        gate: Arc<ServeGate>,
98        audit: Option<Arc<dyn ProxyAudit>>,
99    ) -> Arc<Self> {
100        let limits = upstream.limits().clone();
101        let hub = Arc::new(Self {
102            rt,
103            table: OnceLock::new(),
104            upstream,
105            gate,
106            audit,
107            grants: DashMap::new(),
108            max_pending: AtomicUsize::new(MAX_PENDING),
109            refusals: DashMap::new(),
110            pending: Arc::new(DashMap::new()),
111            next_id: AtomicU64::new(1),
112            intake: Arc::new(Semaphore::new(INTAKE)),
113            limits,
114            me: OnceLock::new(),
115        });
116        let _ = hub.me.set(Arc::downgrade(&hub));
117        hub
118    }
119
120    /// The placement table this hub serves and feeds.
121    pub fn attach(&self, table: Arc<PlacementTable>) {
122        let _ = self.table.set(table);
123    }
124
125    /// Register as the runtime's control sink for [`INFER_TOPIC`], and
126    /// forget a peer's standing when it leaves.
127    pub fn install(self: &Arc<Self>) {
128        self.rt.set_control_sink(INFER_TOPIC, self.clone());
129        let hub = self.clone();
130        let mut rx = self.rt.subscribe_peer_events();
131        tokio::spawn(async move {
132            use tokio::sync::broadcast::error::RecvError;
133            loop {
134                match rx.recv().await {
135                    Ok(crate::mesh_discovery::MeshPeerEvent::Left { node_id }) => {
136                        hub.grants.remove(&node_id);
137                    }
138                    Ok(_) => {}
139                    // Missed events: forget everything, standing is relearned.
140                    Err(RecvError::Lagged(_)) => hub.grants.clear(),
141                    Err(RecvError::Closed) => break,
142                }
143            }
144        });
145    }
146
147    /// Cap on exchanges in flight (default 256).
148    pub fn set_max_pending(&self, n: usize) {
149        self.max_pending.store(n, Ordering::Relaxed);
150    }
151
152    /// The standing of `node` on the connection in use now: the grant seen on
153    /// that very connection, and only while the route to it is verified. A
154    /// peer that reconnected (a new connection, maybe as a Leaf) has no
155    /// standing until it is heard from again.
156    fn grant_of(&self, node: &str) -> Option<Grant> {
157        let (conn, verified) = self.rt.peer_route(node)?;
158        let entry = self.grants.get(node).map(|e| e.clone())?;
159        if !verified || entry.0 != conn {
160            self.grants.remove(node);
161            return None;
162        }
163        Some(entry.1)
164    }
165
166    /// Whether a refusal may be sent to `peer` now (a few per window).
167    fn refusal_allowed(&self, peer: &str) -> bool {
168        let mut e = self.refusals.entry(peer.to_string()).or_insert((Instant::now(), 0));
169        if e.0.elapsed() > REFUSAL_WINDOW {
170            *e = (Instant::now(), 0);
171        }
172        e.1 += 1;
173        e.1 <= REFUSALS
174    }
175
176    fn table(&self) -> Option<&Arc<PlacementTable>> {
177        self.table.get()
178    }
179
180    fn note(&self, kind: &str, payload: Value) {
181        if let Some(a) = &self.audit {
182            a.record(kind, payload);
183        }
184    }
185
186    /// Send this node's adverts for every exposed role to each connected
187    /// peer that is allowed to use it and qualifies. Called periodically;
188    /// receivers expire adverts they stop hearing.
189    pub async fn announce(&self, now_secs: u64) {
190        // A hello to every connected peer lets each side learn the standing
191        // the other's connection holds (grants are only known from traffic).
192        for peer in self.rt.peer_ids() {
193            let _ = self.rt.route_to_remote(&peer, msg(json!({"t": "hello"}))).await;
194        }
195        let Some(table) = self.table() else { return };
196        for ad in table.advertisements(now_secs) {
197            let Some(role) = ad.name.strip_prefix(super::table::SERVICE_PREFIX) else {
198                continue;
199            };
200            for peer in self.rt.peer_ids() {
201                let ok = table.mesh_peer_allowed(role, &peer)
202                    && self.grant_of(&peer).is_some_and(|g| qualifies(&g));
203                if !ok {
204                    continue;
205                }
206                let m = msg(json!({"t": "advert", "ad": ad}));
207                if let Err(e) = self.rt.route_to_remote(&peer, m).await {
208                    tracing::debug!(peer, error = %e, "infer advert send failed");
209                }
210            }
211        }
212    }
213
214    /// Peers this node currently holds a qualifying grant for.
215    pub fn qualifying_peers(&self) -> Vec<String> {
216        let mut v: Vec<String> = self
217            .rt
218            .peer_ids()
219            .into_iter()
220            .filter(|p| self.grant_of(p).is_some_and(|g| qualifies(&g)))
221            .collect();
222        v.sort();
223        v
224    }
225
226    fn serve(self: &Arc<Self>, ctx: &PeerCtx, id: u64, data: &str) {
227        let Some(table) = self.table().cloned() else { return };
228        // Cheap standing checks before any work or memory is committed. A
229        // peer with no verified standing at all (no grant) is dropped without
230        // a word: no reply task, no audit line from a flood. A verified peer
231        // that does not qualify (a leaf), is not listed, or arrives while
232        // intake is full is told, a few times per window, so its consumer
233        // falls back at once instead of waiting out the stall timeout.
234        let grant = ctx.grant();
235        if grant.is_none() {
236            return;
237        }
238        if !grant.as_ref().is_some_and(qualifies) {
239            if self.refusal_allowed(&ctx.peer_id) {
240                self.note(
241                    "infer.mesh.failed",
242                    json!({"peer": ctx.peer_id, "why": "peer does not qualify"}),
243                );
244                self.spawn_refusal(ctx.peer_id.clone(), id);
245            }
246            return;
247        }
248        if !table.peer_listed_any(&ctx.peer_id) {
249            if self.refusal_allowed(&ctx.peer_id) {
250                self.note(
251                    "infer.mesh.failed",
252                    json!({"peer": ctx.peer_id, "why": "peer is on no serve allowlist"}),
253                );
254                self.spawn_refusal(ctx.peer_id.clone(), id);
255            }
256            return;
257        }
258        let Ok(permit) = self.intake.clone().try_acquire_owned() else {
259            if self.refusal_allowed(&ctx.peer_id) {
260                self.spawn_refusal(ctx.peer_id.clone(), id);
261            }
262            return;
263        };
264        let Some(payload) = hex_decode(data) else {
265            return;
266        };
267        let frame = MeshFrame {
268            frame_type: FrameType::InferRequest,
269            payload,
270        };
271        let Ok(first) = frame.encode() else { return };
272        let hub = self.clone();
273        let peer = InferPeer {
274            node_id: ctx.peer_id.clone(),
275            grant,
276        };
277        tokio::spawn(async move {
278            let mut stream = ServeStream {
279                rt: hub.rt.clone(),
280                peer: peer.node_id.clone(),
281                id,
282                first: Some(first),
283            };
284            let _ = serve_infer(
285                &mut stream,
286                &peer,
287                table.as_ref(),
288                &hub.upstream,
289                &hub.gate,
290                hub.audit.as_deref(),
291            )
292            .await;
293            drop(permit);
294        });
295    }
296
297    fn spawn_refusal(self: &Arc<Self>, peer: String, id: u64) {
298        let rt = self.rt.clone();
299        tokio::spawn(async move {
300            let payload = super::wire::encode_resp(&super::wire::Resp::Error("refused".into()));
301            let m = msg(json!({"t": "resp", "id": id, "data": hex_encode(&payload)}));
302            let _ = rt.route_to_remote(&peer, m).await;
303        });
304    }
305}
306
307impl PeerControlSink for InferHub {
308    fn on_peer_control(&self, ctx: &PeerCtx, conn: u64, payload: &Value) -> Vec<Value> {
309        // The standing this connection holds, as of this message.
310        let first_contact = match ctx.grant() {
311            Some(g) => self
312                .grants
313                .insert(ctx.peer_id.clone(), (conn, g))
314                .is_none_or(|old| old.0 != conn),
315            None => {
316                self.grants.remove(&ctx.peer_id);
317                false
318            }
319        };
320        let Some(kind) = payload.get("t").and_then(Value::as_str) else {
321            return Vec::new();
322        };
323        match kind {
324            // Answer a first hello so both sides learn at once.
325            "hello" if first_contact => return vec![json!({"t": "hello"})],
326            "hello" => {}
327            "advert" => {
328                let Some(table) = self.table() else { return Vec::new() };
329                if let Some(ad) = payload
330                    .get("ad")
331                    .and_then(|a| serde_json::from_value::<ServiceAdvertisement>(a.clone()).ok())
332                {
333                    // The sender is the connection's peer, never a field.
334                    table.ingest_advertisement(&ctx.peer_id, &ad);
335                }
336            }
337            "req" => {
338                let (Some(id), Some(data)) = (
339                    payload.get("id").and_then(Value::as_u64),
340                    payload.get("data").and_then(Value::as_str),
341                ) else {
342                    return Vec::new();
343                };
344                // `serve` needs an Arc<Self>; the sink is only ever held as one.
345                if let Some(me) = self.self_arc() {
346                    me.serve(ctx, id, data);
347                }
348            }
349            "resp" => {
350                let (Some(id), Some(data)) = (
351                    payload.get("id").and_then(Value::as_u64),
352                    payload.get("data").and_then(Value::as_str),
353                ) else {
354                    return Vec::new();
355                };
356                let key = (ctx.peer_id.clone(), id);
357                if let Some(tx) = self.pending.get(&key).map(|e| e.clone()) {
358                    let delivered = hex_decode(data).is_some_and(|b| tx.try_send(b).is_ok());
359                    if !delivered {
360                        // Garbage, or the exchange is not draining: cancel it.
361                        self.pending.remove(&key);
362                    }
363                }
364            }
365            _ => {}
366        }
367        Vec::new()
368    }
369}
370
371impl InferHub {
372    fn self_arc(&self) -> Option<Arc<Self>> {
373        self.me.get().and_then(std::sync::Weak::upgrade)
374    }
375}
376
377#[async_trait]
378impl MeshDialer for InferHub {
379    fn standing(&self, node_id: &str) -> Option<Grant> {
380        self.grant_of(node_id)
381    }
382
383    async fn dial(&self, node_id: &str) -> Result<Box<dyn MeshStream>, ProxyError> {
384        if !self.is_admitted(node_id) {
385            return Err(ProxyError::Refused(format!("peer {node_id} is not admitted")));
386        }
387        if self.pending.len() >= self.max_pending.load(Ordering::Relaxed) {
388            return Err(ProxyError::NoInstance("too many exchanges in flight".into()));
389        }
390        let id = self.next_id.fetch_add(1, Ordering::Relaxed);
391        let (tx, rx) = mpsc::channel(RESP_BUFFER);
392        self.pending.insert((node_id.to_string(), id), tx);
393        Ok(Box::new(ClientStream {
394            rt: self.rt.clone(),
395            peer: node_id.to_string(),
396            id,
397            rx,
398            pending: self.pending.clone(),
399            max_hex: self.limits.max_mesh_request_body.saturating_mul(2) + 4096,
400        }))
401    }
402}
403
404/// Server side of one exchange: the request frame was handed in, response
405/// frames go out as control messages.
406struct ServeStream {
407    rt: Arc<MeshRuntime>,
408    peer: String,
409    id: u64,
410    first: Option<Vec<u8>>,
411}
412
413#[async_trait]
414impl MeshStream for ServeStream {
415    async fn send(&mut self, data: &[u8]) -> Result<(), MeshError> {
416        let f = split_frame(data)?;
417        if f.frame_type != FrameType::InferResponse {
418            return Err(mesh_err("not a response frame"));
419        }
420        let m = msg(json!({"t": "resp", "id": self.id, "data": hex_encode(&f.payload)}));
421        self.rt.route_to_remote(&self.peer, m).await.map_err(mesh_err)
422    }
423    async fn recv(&mut self) -> Result<Vec<u8>, MeshError> {
424        self.first.take().ok_or(MeshError::ConnectionClosed)
425    }
426    async fn close(&mut self) -> Result<(), MeshError> {
427        Ok(())
428    }
429    fn remote_addr(&self) -> Option<SocketAddr> {
430        None
431    }
432}
433
434/// Consumer side of one exchange.
435struct ClientStream {
436    rt: Arc<MeshRuntime>,
437    peer: String,
438    id: u64,
439    rx: mpsc::Receiver<Vec<u8>>,
440    pending: Pending,
441    max_hex: usize,
442}
443
444#[async_trait]
445impl MeshStream for ClientStream {
446    async fn send(&mut self, data: &[u8]) -> Result<(), MeshError> {
447        let f = split_frame(data)?;
448        if f.frame_type != FrameType::InferRequest {
449            return Err(mesh_err("not a request frame"));
450        }
451        let hex = hex_encode(&f.payload);
452        if hex.len() > self.max_hex {
453            return Err(mesh_err("request too large for the mesh path"));
454        }
455        let m = msg(json!({"t": "req", "id": self.id, "data": hex}));
456        self.rt.route_to_remote(&self.peer, m).await.map_err(mesh_err)
457    }
458    async fn recv(&mut self) -> Result<Vec<u8>, MeshError> {
459        let payload = self.rx.recv().await.ok_or(MeshError::ConnectionClosed)?;
460        MeshFrame {
461            frame_type: FrameType::InferResponse,
462            payload,
463        }
464        .encode()
465    }
466    async fn close(&mut self) -> Result<(), MeshError> {
467        self.pending.remove(&(self.peer.clone(), self.id));
468        Ok(())
469    }
470    fn remote_addr(&self) -> Option<SocketAddr> {
471        None
472    }
473}
474
475impl Drop for ClientStream {
476    fn drop(&mut self) {
477        self.pending.remove(&(self.peer.clone(), self.id));
478    }
479}
480
481#[cfg(test)]
482impl InferHub {
483    pub(super) fn grants_len(&self) -> usize {
484        self.grants.len()
485    }
486}
487
488#[cfg(test)]
489pub(super) struct Probe(Pending);
490
491#[cfg(test)]
492impl Probe {
493    pub(super) fn pending_len(&self) -> usize {
494        self.0.len()
495    }
496}
497
498#[cfg(test)]
499impl InferHub {
500    /// Open an exchange (as `dial` does) and keep it alive for inspection.
501    pub(super) async fn dial_for_test(&self, node: &str) -> TestExchange {
502        let s = self.dial(node).await.unwrap();
503        TestExchange { _s: s, probe: Probe(self.pending.clone()) }
504    }
505}
506
507#[cfg(test)]
508pub(super) struct TestExchange {
509    _s: Box<dyn MeshStream>,
510    probe: Probe,
511}
512
513#[cfg(test)]
514impl TestExchange {
515    pub(super) fn pending_len(&self) -> usize {
516        self.probe.pending_len()
517    }
518}