1use 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
41const RESP_BUFFER: usize = 64;
43const INTAKE: usize = 32;
45const MAX_PENDING: usize = 256;
47const REFUSALS: u32 = 8;
49const REFUSAL_WINDOW: Duration = Duration::from_secs(10);
50
51type Pending = Arc<DashMap<(String, u64), mpsc::Sender<Vec<u8>>>>;
52
53pub 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 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
83fn 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 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 pub fn attach(&self, table: Arc<PlacementTable>) {
122 let _ = self.table.set(table);
123 }
124
125 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 Err(RecvError::Lagged(_)) => hub.grants.clear(),
141 Err(RecvError::Closed) => break,
142 }
143 }
144 });
145 }
146
147 pub fn set_max_pending(&self, n: usize) {
149 self.max_pending.store(n, Ordering::Relaxed);
150 }
151
152 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 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 pub async fn announce(&self, now_secs: u64) {
190 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 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 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 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 "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 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 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 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
404struct 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
434struct 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 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}