use std::collections::HashSet;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, RwLock};
use std::time::Duration;
use async_trait::async_trait;
use dashmap::DashMap;
use super::links::LicenceLinks;
use crate::error::KernelResult;
use crate::ipc::{KernelMessage, MessagePayload, MessageTarget};
use crate::mesh_artifact_tunnel::{PeerSender, licensed_peer};
use crate::mesh_delivery::PeerCtx;
use crate::mesh_discovery::{MeshPeerEvent, MeshPeerEventBus};
use crate::mesh_runtime::{COG_BINDING_TOPIC, COG_GRANT_TOPIC, COG_SYNC_TOPIC, PeerControlSink};
pub const LICENCE_TOPICS: [&str; 3] = [COG_BINDING_TOPIC, COG_GRANT_TOPIC, COG_SYNC_TOPIC];
pub const PEER_REFRESH: Duration = Duration::from_secs(10);
pub const MAX_REPLY_SENDS: usize = 64;
pub const MAX_REPLY_SENDS_PER_PEER: usize = 4;
pub fn is_licence_topic(msg: &KernelMessage) -> bool {
matches!(&msg.target, MessageTarget::Topic(t) if LICENCE_TOPICS.contains(&t.as_str()))
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PeerSnapshot {
pub connected: Vec<String>,
pub licensed: Vec<String>,
pub reserved_holder: Option<bool>,
pub reserved_holder_uid: Option<u32>,
}
#[async_trait]
pub trait PeerDirectory: Send + Sync + 'static {
async fn peers(&self) -> Result<PeerSnapshot, String>;
}
#[derive(Debug, Default)]
pub struct ServiceLinksCounters {
pub delivered: AtomicU64,
pub refused_unverified: AtomicU64,
pub unhandled: AtomicU64,
pub refresh_failed: AtomicU64,
pub refreshed: AtomicU64,
pub replies_dropped: AtomicU64,
}
#[derive(Default)]
struct View {
connected: Vec<String>,
licensed: HashSet<String>,
}
pub struct ServiceLicenceLinks {
sender: Arc<dyn PeerSender>,
directory: Arc<dyn PeerDirectory>,
sinks: DashMap<String, Arc<dyn PeerControlSink>>,
view: RwLock<View>,
events: MeshPeerEventBus,
replies: Arc<tokio::sync::Semaphore>,
per_peer: DashMap<String, Arc<tokio::sync::Semaphore>>,
per_peer_cap: usize,
holder: tokio::sync::watch::Sender<Option<bool>>,
holder_uid: RwLock<Option<u32>>,
pub counters: ServiceLinksCounters,
}
impl ServiceLicenceLinks {
pub fn new(sender: Arc<dyn PeerSender>, directory: Arc<dyn PeerDirectory>) -> Arc<Self> {
Self::with_reply_caps(sender, directory, MAX_REPLY_SENDS, MAX_REPLY_SENDS_PER_PEER)
}
pub fn with_reply_caps(
sender: Arc<dyn PeerSender>,
directory: Arc<dyn PeerDirectory>,
total: usize,
per_peer: usize,
) -> Arc<Self> {
Arc::new(Self {
sender,
directory,
sinks: DashMap::new(),
view: RwLock::new(View::default()),
events: MeshPeerEventBus::new(),
replies: Arc::new(tokio::sync::Semaphore::new(total.max(1))),
per_peer: DashMap::new(),
per_peer_cap: per_peer.max(1),
holder: tokio::sync::watch::channel(None).0,
holder_uid: RwLock::new(None),
counters: ServiceLinksCounters::default(),
})
}
pub fn holder(&self) -> Option<bool> {
*self.holder.borrow()
}
pub fn holder_uid(&self) -> Option<u32> {
*self.holder_uid.read().unwrap_or_else(|p| p.into_inner())
}
pub fn subscribe_holder(&self) -> tokio::sync::watch::Receiver<Option<bool>> {
self.holder.subscribe()
}
fn set_holder(&self, v: Option<bool>) {
self.holder.send_if_modified(|h| {
let changed = *h != v;
*h = v;
changed
});
}
fn reserve(&self, peer: &str) -> Option<(tokio::sync::OwnedSemaphorePermit, tokio::sync::OwnedSemaphorePermit)> {
let mine = self
.per_peer
.entry(peer.to_owned())
.or_insert_with(|| Arc::new(tokio::sync::Semaphore::new(self.per_peer_cap)))
.clone();
let p = mine.try_acquire_owned().ok()?;
let g = self.replies.clone().try_acquire_owned().ok()?;
Some((g, p))
}
pub async fn refresh(&self) -> Result<(), String> {
let snap = match self.directory.peers().await {
Ok(s) => s,
Err(e) => {
*self.view.write().unwrap_or_else(|p| p.into_inner()) = View::default();
self.set_holder(None);
self.counters.refresh_failed.fetch_add(1, Ordering::Relaxed);
return Err(e);
}
};
self.counters.refreshed.fetch_add(1, Ordering::Relaxed);
self.set_holder(snap.reserved_holder);
*self.holder_uid.write().unwrap_or_else(|p| p.into_inner()) = snap.reserved_holder_uid;
let licensed: HashSet<String> = snap.licensed.into_iter().collect();
let joined: Vec<String> = {
let mut v = self.view.write().unwrap_or_else(|p| p.into_inner());
let joined = licensed.difference(&v.licensed).cloned().collect();
v.connected = snap.connected;
v.licensed = licensed;
joined
};
for node_id in joined {
self.events.emit(MeshPeerEvent::Joined { node_id, address: None, platform: None, verified: true });
}
Ok(())
}
pub fn spawn_refresh(self: &Arc<Self>, every: Duration) -> Option<tokio::task::JoinHandle<()>> {
let handle = tokio::runtime::Handle::try_current().ok()?;
let w = Arc::downgrade(self);
Some(handle.spawn(async move {
loop {
let Some(me) = w.upgrade() else { break };
if let Err(e) = me.refresh().await {
tracing::debug!(error = %e, "licence peer view not refreshed");
}
drop(me);
tokio::time::sleep(every).await;
}
}))
}
pub async fn deliver(&self, from: &PeerCtx, msg: KernelMessage) -> bool {
let MessageTarget::Topic(topic) = &msg.target else { return false };
if !LICENCE_TOPICS.contains(&topic.as_str()) {
return false;
}
if !licensed_peer(from) {
self.counters.refused_unverified.fetch_add(1, Ordering::Relaxed);
return true;
}
let Some(sink) = self.sinks.get(topic.as_str()).map(|s| s.clone()) else {
self.counters.unhandled.fetch_add(1, Ordering::Relaxed);
return true;
};
let MessagePayload::Json(payload) = &msg.payload else { return true };
let new = self
.view
.write()
.unwrap_or_else(|p| p.into_inner())
.licensed
.insert(from.peer_id.clone());
if new {
self.events.emit(MeshPeerEvent::Joined {
node_id: from.peer_id.clone(),
address: None,
platform: None,
verified: true,
});
}
let mut slot = None;
if topic.as_str() == COG_SYNC_TOPIC && payload.get("op").and_then(|o| o.as_str()) == Some("request") {
match self.reserve(&from.peer_id) {
Some(s) => slot = Some(s),
None => {
self.counters.replies_dropped.fetch_add(1, Ordering::Relaxed);
return true;
}
}
}
self.counters.delivered.fetch_add(1, Ordering::Relaxed);
for reply in sink.on_peer_control(from, conn_of(&from.peer_id), payload) {
let Some(permit) = slot.take().or_else(|| self.reserve(&from.peer_id)) else {
self.counters.replies_dropped.fetch_add(1, Ordering::Relaxed);
continue;
};
let m = KernelMessage::new(0, MessageTarget::Topic(topic.clone()), MessagePayload::Json(reply));
let (sender, peer, topic) = (self.sender.clone(), from.peer_id.clone(), topic.clone());
tokio::spawn(async move {
if let Err(e) = sender.send_to_node(&peer, m).await {
tracing::debug!(%peer, %topic, error = %e, "licence reply failed");
}
drop(permit);
});
}
true
}
}
fn conn_of(peer: &str) -> u64 {
let h = blake3::hash(peer.as_bytes());
let mut b = [0u8; 8];
b.copy_from_slice(&h.as_bytes()[..8]);
u64::from_le_bytes(b) | 1
}
#[async_trait]
impl LicenceLinks for ServiceLicenceLinks {
fn set_control_sink(&self, topic: &str, sink: Arc<dyn PeerControlSink>) {
self.sinks.entry(topic.to_owned()).or_insert(sink);
}
fn peer_ids(&self) -> Vec<String> {
self.view.read().unwrap_or_else(|p| p.into_inner()).connected.clone()
}
fn peer_licensed(&self, peer: &str) -> bool {
self.view.read().unwrap_or_else(|p| p.into_inner()).licensed.contains(peer)
}
async fn route_to_remote(&self, peer: &str, msg: KernelMessage) -> KernelResult<()> {
self.sender.send_to_node(peer, msg).await
}
fn subscribe_peer_events(&self) -> Option<tokio::sync::broadcast::Receiver<MeshPeerEvent>> {
Some(self.events.subscribe())
}
}