use std::{collections::HashSet, net::IpAddr, sync::Arc};
use kameo::{
actor::ActorRef,
message::{Context, Message},
reply::ReplySender,
};
use ts_control::Node;
use ts_transport::PeerId;
use crate::{Error, env::Env};
mod peer_db;
pub use peer_db::PeerDb;
pub struct PeerTracker {
peer_db: PeerDb,
seen_state_update: bool,
pending_requests: Vec<Pending>,
env: Env,
}
impl PeerTracker {
fn peer_by_name_opt(&self, name: &str) -> Option<&Node> {
let name = name.trim_end_matches('.');
self.peer_db.get(&name).map(|(_id, node)| node)
}
fn peer_by_tailnet_ip_opt(&self, ip: IpAddr) -> Option<&Node> {
self.peer_db.get(&ip).map(|(_id, node)| node)
}
}
impl kameo::Actor for PeerTracker {
type Args = Env;
type Error = Error;
async fn on_start(env: Self::Args, slf: ActorRef<Self>) -> Result<Self, Self::Error> {
env.subscribe::<Arc<ts_control::StateUpdate>>(&slf).await?;
env.register(None, &slf).await?;
Ok(Self {
peer_db: PeerDb::default(),
pending_requests: Default::default(),
seen_state_update: false,
env,
})
}
}
enum Pending {
PeerByName(PeerByName, ReplySender<Option<Node>>),
AcceptedRoute(PeerByAcceptedRoute, ReplySender<Vec<Node>>),
TailnetIp(PeerByTailnetIp, ReplySender<Option<Node>>),
}
#[allow(missing_docs)]
mod msg_impl {
use std::net::IpAddr;
use kameo::prelude::DelegatedReply;
use super::*;
#[kameo::messages]
impl PeerTracker {
#[message(ctx)]
pub async fn peer_by_name(
&mut self,
ctx: &mut Context<Self, DelegatedReply<Option<Node>>>,
name: String,
) -> DelegatedReply<Option<Node>> {
let (deleg, sender) = ctx.reply_sender();
let Some(sender) = sender else { return deleg };
if !self.seen_state_update {
tracing::debug!(query = name, "no peer state seen yet, queueing request");
self.pending_requests
.push(Pending::PeerByName(PeerByName { name }, sender));
return deleg;
}
sender.send(self.peer_by_name_opt(&name).cloned());
deleg
}
#[message(ctx)]
pub fn peer_by_accepted_route(
&mut self,
ctx: &mut Context<Self, DelegatedReply<Vec<Node>>>,
ip: IpAddr,
) -> DelegatedReply<Vec<Node>> {
let (deleg, sender) = ctx.reply_sender();
let Some(sender) = sender else { return deleg };
if !self.seen_state_update {
tracing::debug!(query = %ip, "no peer state seen yet, queueing request");
self.pending_requests
.push(Pending::AcceptedRoute(PeerByAcceptedRoute { ip }, sender));
return deleg;
}
sender.send(
self.peer_db
.get_route(ip.into())
.map(|(_id, node)| node.clone())
.collect(),
);
deleg
}
#[message(ctx)]
pub fn peer_by_tailnet_ip(
&mut self,
ctx: &mut Context<Self, DelegatedReply<Option<Node>>>,
ip: IpAddr,
) -> DelegatedReply<Option<Node>> {
let (deleg, sender) = ctx.reply_sender();
let Some(sender) = sender else { return deleg };
if !self.seen_state_update {
tracing::debug!(query = %ip, "no peer state seen yet, queueing request");
self.pending_requests
.push(Pending::TailnetIp(PeerByTailnetIp { ip }, sender));
return deleg;
}
sender.send(self.peer_by_tailnet_ip_opt(ip).cloned());
deleg
}
}
}
pub use msg_impl::*;
#[derive(Debug, Clone)]
pub(crate) struct PeerState {
#[allow(unused)]
pub deletions: HashSet<PeerId>,
#[allow(unused)]
pub upserts: HashSet<PeerId>,
pub peers: Arc<PeerDb>,
}
impl Message<Arc<ts_control::StateUpdate>> for PeerTracker {
type Reply = ();
async fn handle(
&mut self,
msg: Arc<ts_control::StateUpdate>,
_ctx: &mut Context<Self, Self::Reply>,
) {
let Some(peer_update) = &msg.peer_update else {
return;
};
let mut upserts = HashSet::default();
let mut deletions = HashSet::default();
match peer_update {
ts_control::PeerUpdate::Full(new_nodes) => {
tracing::trace!("full peer update");
let new_ids = new_nodes
.iter()
.map(|x| &x.stable_id)
.collect::<HashSet<_>>();
self.peer_db.retain(|id, peer| {
let retain = new_ids.contains(&peer.stable_id);
if !retain {
deletions.insert(id);
}
retain
});
for node in new_nodes {
let peer_id = self.peer_db.upsert(node);
upserts.insert(peer_id);
}
}
ts_control::PeerUpdate::Delta {
patch,
remove,
upsert,
} => {
tracing::trace!("delta peer update");
for peer in remove {
let Some((id, _node)) = self.peer_db.remove(peer) else {
tracing::error!(control_node_id = peer, "removed peer was unknown");
continue;
};
deletions.insert(id);
}
for peer in upsert {
let id = self.peer_db.upsert(peer);
upserts.insert(id);
}
for update in patch {
if let Some(id) = self.peer_db.patch(update) {
upserts.insert(id);
} else {
tracing::warn!(?update, "no peer for update");
}
}
}
}
tracing::debug!(
n_upsert = upserts.len(),
n_delete = deletions.len(),
peer_count = self.peer_db.peers().len(),
"new peer state"
);
self.service_pending_requests();
if let Err(e) = self
.env
.publish(Arc::new(PeerState {
upserts,
deletions,
peers: Arc::new(self.peer_db.clone()),
}))
.await
{
tracing::error!(error = %e, "publishing peer state update");
}
}
}
impl PeerTracker {
fn service_pending_requests(&mut self) {
if self.seen_state_update {
return;
}
self.seen_state_update = true;
if !self.pending_requests.is_empty() {
tracing::debug!(
n_pending = self.pending_requests.len(),
"state update received, servicing pending requests"
);
}
for req in core::mem::take(&mut self.pending_requests) {
match req {
Pending::PeerByName(PeerByName { name }, reply) => {
reply.send(self.peer_by_name_opt(&name).cloned());
}
Pending::TailnetIp(PeerByTailnetIp { ip }, reply) => {
reply.send(self.peer_by_tailnet_ip_opt(ip).cloned());
}
Pending::AcceptedRoute(PeerByAcceptedRoute { ip }, reply) => {
reply.send(
self.peer_db
.get_route(ip.into())
.map(|(_id, node)| node.clone())
.collect(),
);
}
}
}
}
}