1use std::collections::HashSet;
29use std::sync::atomic::{AtomicU64, Ordering};
30use std::sync::{Arc, RwLock};
31use std::time::Duration;
32
33use async_trait::async_trait;
34use dashmap::DashMap;
35
36use super::links::LicenceLinks;
37use crate::error::KernelResult;
38use crate::ipc::{KernelMessage, MessagePayload, MessageTarget};
39use crate::mesh_artifact_tunnel::{PeerSender, licensed_peer};
40use crate::mesh_delivery::PeerCtx;
41use crate::mesh_discovery::{MeshPeerEvent, MeshPeerEventBus};
42use crate::mesh_runtime::{COG_BINDING_TOPIC, COG_GRANT_TOPIC, COG_SYNC_TOPIC, PeerControlSink};
43
44pub const LICENCE_TOPICS: [&str; 3] = [COG_BINDING_TOPIC, COG_GRANT_TOPIC, COG_SYNC_TOPIC];
46
47pub const PEER_REFRESH: Duration = Duration::from_secs(10);
49
50pub const MAX_REPLY_SENDS: usize = 64;
53
54pub const MAX_REPLY_SENDS_PER_PEER: usize = 4;
56
57pub fn is_licence_topic(msg: &KernelMessage) -> bool {
59 matches!(&msg.target, MessageTarget::Topic(t) if LICENCE_TOPICS.contains(&t.as_str()))
60}
61
62#[derive(Debug, Clone, Default, PartialEq, Eq)]
64pub struct PeerSnapshot {
65 pub connected: Vec<String>,
67 pub licensed: Vec<String>,
69 pub reserved_holder: Option<bool>,
72 pub reserved_holder_uid: Option<u32>,
75}
76
77#[async_trait]
79pub trait PeerDirectory: Send + Sync + 'static {
80 async fn peers(&self) -> Result<PeerSnapshot, String>;
82}
83
84#[derive(Debug, Default)]
86pub struct ServiceLinksCounters {
87 pub delivered: AtomicU64,
89 pub refused_unverified: AtomicU64,
91 pub unhandled: AtomicU64,
93 pub refresh_failed: AtomicU64,
95 pub refreshed: AtomicU64,
97 pub replies_dropped: AtomicU64,
100}
101
102#[derive(Default)]
103struct View {
104 connected: Vec<String>,
105 licensed: HashSet<String>,
106}
107
108pub struct ServiceLicenceLinks {
110 sender: Arc<dyn PeerSender>,
111 directory: Arc<dyn PeerDirectory>,
112 sinks: DashMap<String, Arc<dyn PeerControlSink>>,
113 view: RwLock<View>,
114 events: MeshPeerEventBus,
115 replies: Arc<tokio::sync::Semaphore>,
116 per_peer: DashMap<String, Arc<tokio::sync::Semaphore>>,
117 per_peer_cap: usize,
118 holder: tokio::sync::watch::Sender<Option<bool>>,
119 holder_uid: RwLock<Option<u32>>,
120 pub counters: ServiceLinksCounters,
122}
123
124impl ServiceLicenceLinks {
125 pub fn new(sender: Arc<dyn PeerSender>, directory: Arc<dyn PeerDirectory>) -> Arc<Self> {
127 Self::with_reply_caps(sender, directory, MAX_REPLY_SENDS, MAX_REPLY_SENDS_PER_PEER)
128 }
129
130 pub fn with_reply_caps(
132 sender: Arc<dyn PeerSender>,
133 directory: Arc<dyn PeerDirectory>,
134 total: usize,
135 per_peer: usize,
136 ) -> Arc<Self> {
137 Arc::new(Self {
138 sender,
139 directory,
140 sinks: DashMap::new(),
141 view: RwLock::new(View::default()),
142 events: MeshPeerEventBus::new(),
143 replies: Arc::new(tokio::sync::Semaphore::new(total.max(1))),
144 per_peer: DashMap::new(),
145 per_peer_cap: per_peer.max(1),
146 holder: tokio::sync::watch::channel(None).0,
147 holder_uid: RwLock::new(None),
148 counters: ServiceLinksCounters::default(),
149 })
150 }
151
152 pub fn holder(&self) -> Option<bool> {
156 *self.holder.borrow()
157 }
158
159 pub fn holder_uid(&self) -> Option<u32> {
161 *self.holder_uid.read().unwrap_or_else(|p| p.into_inner())
162 }
163
164 pub fn subscribe_holder(&self) -> tokio::sync::watch::Receiver<Option<bool>> {
166 self.holder.subscribe()
167 }
168
169 fn set_holder(&self, v: Option<bool>) {
170 self.holder.send_if_modified(|h| {
171 let changed = *h != v;
172 *h = v;
173 changed
174 });
175 }
176
177 fn reserve(&self, peer: &str) -> Option<(tokio::sync::OwnedSemaphorePermit, tokio::sync::OwnedSemaphorePermit)> {
179 let mine = self
180 .per_peer
181 .entry(peer.to_owned())
182 .or_insert_with(|| Arc::new(tokio::sync::Semaphore::new(self.per_peer_cap)))
183 .clone();
184 let p = mine.try_acquire_owned().ok()?;
185 let g = self.replies.clone().try_acquire_owned().ok()?;
186 Some((g, p))
187 }
188
189 pub async fn refresh(&self) -> Result<(), String> {
192 let snap = match self.directory.peers().await {
193 Ok(s) => s,
194 Err(e) => {
195 *self.view.write().unwrap_or_else(|p| p.into_inner()) = View::default();
198 self.set_holder(None);
199 self.counters.refresh_failed.fetch_add(1, Ordering::Relaxed);
200 return Err(e);
201 }
202 };
203 self.counters.refreshed.fetch_add(1, Ordering::Relaxed);
204 self.set_holder(snap.reserved_holder);
205 *self.holder_uid.write().unwrap_or_else(|p| p.into_inner()) = snap.reserved_holder_uid;
206 let licensed: HashSet<String> = snap.licensed.into_iter().collect();
207 let joined: Vec<String> = {
208 let mut v = self.view.write().unwrap_or_else(|p| p.into_inner());
209 let joined = licensed.difference(&v.licensed).cloned().collect();
210 v.connected = snap.connected;
211 v.licensed = licensed;
212 joined
213 };
214 for node_id in joined {
215 self.events.emit(MeshPeerEvent::Joined { node_id, address: None, platform: None, verified: true });
216 }
217 Ok(())
218 }
219
220 pub fn spawn_refresh(self: &Arc<Self>, every: Duration) -> Option<tokio::task::JoinHandle<()>> {
222 let handle = tokio::runtime::Handle::try_current().ok()?;
223 let w = Arc::downgrade(self);
224 Some(handle.spawn(async move {
225 loop {
226 let Some(me) = w.upgrade() else { break };
227 if let Err(e) = me.refresh().await {
228 tracing::debug!(error = %e, "licence peer view not refreshed");
229 }
230 drop(me);
231 tokio::time::sleep(every).await;
232 }
233 }))
234 }
235
236 pub async fn deliver(&self, from: &PeerCtx, msg: KernelMessage) -> bool {
240 let MessageTarget::Topic(topic) = &msg.target else { return false };
241 if !LICENCE_TOPICS.contains(&topic.as_str()) {
242 return false;
243 }
244 if !licensed_peer(from) {
245 self.counters.refused_unverified.fetch_add(1, Ordering::Relaxed);
246 return true;
247 }
248 let Some(sink) = self.sinks.get(topic.as_str()).map(|s| s.clone()) else {
249 self.counters.unhandled.fetch_add(1, Ordering::Relaxed);
250 return true;
251 };
252 let MessagePayload::Json(payload) = &msg.payload else { return true };
253 let new = self
257 .view
258 .write()
259 .unwrap_or_else(|p| p.into_inner())
260 .licensed
261 .insert(from.peer_id.clone());
262 if new {
263 self.events.emit(MeshPeerEvent::Joined {
264 node_id: from.peer_id.clone(),
265 address: None,
266 platform: None,
267 verified: true,
268 });
269 }
270 let mut slot = None;
274 if topic.as_str() == COG_SYNC_TOPIC && payload.get("op").and_then(|o| o.as_str()) == Some("request") {
275 match self.reserve(&from.peer_id) {
276 Some(s) => slot = Some(s),
277 None => {
278 self.counters.replies_dropped.fetch_add(1, Ordering::Relaxed);
279 return true;
280 }
281 }
282 }
283 self.counters.delivered.fetch_add(1, Ordering::Relaxed);
284 for reply in sink.on_peer_control(from, conn_of(&from.peer_id), payload) {
287 let Some(permit) = slot.take().or_else(|| self.reserve(&from.peer_id)) else {
288 self.counters.replies_dropped.fetch_add(1, Ordering::Relaxed);
289 continue;
290 };
291 let m = KernelMessage::new(0, MessageTarget::Topic(topic.clone()), MessagePayload::Json(reply));
292 let (sender, peer, topic) = (self.sender.clone(), from.peer_id.clone(), topic.clone());
293 tokio::spawn(async move {
294 if let Err(e) = sender.send_to_node(&peer, m).await {
295 tracing::debug!(%peer, %topic, error = %e, "licence reply failed");
296 }
297 drop(permit);
298 });
299 }
300 true
301 }
302}
303
304fn conn_of(peer: &str) -> u64 {
306 let h = blake3::hash(peer.as_bytes());
307 let mut b = [0u8; 8];
308 b.copy_from_slice(&h.as_bytes()[..8]);
309 u64::from_le_bytes(b) | 1
310}
311
312#[async_trait]
313impl LicenceLinks for ServiceLicenceLinks {
314 fn set_control_sink(&self, topic: &str, sink: Arc<dyn PeerControlSink>) {
315 self.sinks.entry(topic.to_owned()).or_insert(sink);
316 }
317
318 fn peer_ids(&self) -> Vec<String> {
319 self.view.read().unwrap_or_else(|p| p.into_inner()).connected.clone()
320 }
321
322 fn peer_licensed(&self, peer: &str) -> bool {
323 self.view.read().unwrap_or_else(|p| p.into_inner()).licensed.contains(peer)
324 }
325
326 async fn route_to_remote(&self, peer: &str, msg: KernelMessage) -> KernelResult<()> {
327 self.sender.send_to_node(peer, msg).await
328 }
329
330 fn subscribe_peer_events(&self) -> Option<tokio::sync::broadcast::Receiver<MeshPeerEvent>> {
331 Some(self.events.subscribe())
332 }
333}