use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::{broadcast, mpsc};
use tokio_util::sync::CancellationToken;
use iroh::{EndpointAddr, SecretKey};
use crate::actor::Actor;
use crate::cluster::allowlist::Allowlist;
use crate::cluster::config::{NodeClass, NodeIdentity, TransportTuning};
use crate::cluster::error::ClusterError;
use crate::cluster::framing::ControlMessage;
use crate::cluster::membership::ClusterEvent;
use crate::cluster::net::{Net, RecvHalf, run_control_stream_reader};
use crate::cluster::sync::{TypeRegistry, apply_remote_ops};
use crate::cluster::transport::{Transport, peer_addr};
use crate::endpoint::Endpoint;
use crate::receptionist::{ActorEvent, Receptionist, ReceptionistConfig};
#[derive(Default)]
pub struct ClientOptions {
pub type_registry: Option<TypeRegistry>,
pub transport_tuning: TransportTuning,
pub sync_interval: Option<Duration>,
}
pub struct MurmerClient {
receptionist: Receptionist,
net: Arc<dyn Net>,
#[allow(dead_code)]
type_registry: Arc<TypeRegistry>,
server_node_id: String,
shutdown: CancellationToken,
}
impl MurmerClient {
pub async fn connect(
addr: EndpointAddr,
cookie: impl Into<String>,
) -> Result<Self, ClusterError> {
Self::connect_with_options(addr, cookie.into(), ClientOptions::default()).await
}
pub async fn connect_with_options(
addr: EndpointAddr,
cookie: String,
options: ClientOptions,
) -> Result<Self, ClusterError> {
let shutdown = CancellationToken::new();
let type_registry = Arc::new(
options
.type_registry
.unwrap_or_else(TypeRegistry::from_auto),
);
let secret_key = SecretKey::generate();
let identity = NodeIdentity::new(
format!("edge-{}", rand::random::<u32>()),
secret_key.public(),
"0.0.0.0",
0,
);
let (transport, _conn_events) = Transport::connect_only(
identity,
secret_key,
cookie,
NodeClass::Edge,
HashMap::new(),
options.transport_tuning,
Allowlist::open(),
shutdown.clone(),
)
.await?;
let net: Arc<dyn Net> = transport;
let ic = net.connect(peer_addr(&addr)).await?;
let server_node_id = ic.remote_identity.node_id_string();
let local_addr = net.local_addr();
let receptionist = Receptionist::with_config(ReceptionistConfig {
node_id: local_addr.to_string(),
origin_addr: local_addr.to_string(),
..Default::default()
});
let (event_tx, _) = broadcast::channel::<ClusterEvent>(16);
spawn_edge_event_loop(
ic.control_recv,
Arc::clone(&net),
receptionist.clone(),
Arc::clone(&type_registry),
server_node_id.clone(),
event_tx,
options.sync_interval,
shutdown.clone(),
);
let vv = receptionist.version_vector();
if let Err(e) = net
.send_control(&server_node_id, ControlMessage::RegistrySyncRequest(vv))
.await
{
tracing::warn!("Failed to send initial sync request to {server_node_id}: {e}");
}
Ok(Self {
receptionist,
net,
type_registry,
server_node_id,
shutdown,
})
}
pub fn lookup<A: Actor + 'static>(&self, label: &str) -> Option<Endpoint<A>> {
self.receptionist.lookup(label)
}
pub async fn lookup_wait<A: Actor + 'static>(
&self,
label: &str,
timeout: Duration,
) -> Result<Endpoint<A>, ClusterError> {
if let Some(ep) = self.receptionist.lookup::<A>(label) {
return Ok(ep);
}
let mut events = self.receptionist.subscribe_events();
let vv = self.receptionist.version_vector();
if let Err(e) = self
.net
.send_control(
&self.server_node_id,
ControlMessage::RegistrySyncRequest(vv),
)
.await
{
tracing::warn!("lookup_wait: failed to send sync request: {e}");
}
let deadline = tokio::time::Instant::now() + timeout;
let mut poll_ticker = tokio::time::interval(Duration::from_millis(500));
poll_ticker.tick().await;
loop {
let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
if remaining.is_zero() {
return Err(ClusterError::Timeout(format!(
"actor '{label}' not available after {timeout:?}"
)));
}
tokio::select! {
result = tokio::time::timeout(remaining, events.recv()) => {
match result {
Ok(Some(ActorEvent::Registered { label: ref l, .. })) if l == label => {
if let Some(ep) = self.receptionist.lookup::<A>(label) {
return Ok(ep);
}
}
Ok(Some(_)) => {
if let Some(ep) = self.receptionist.lookup::<A>(label) {
return Ok(ep);
}
}
Ok(None) => {
return Err(ClusterError::Timeout(format!(
"event channel closed while waiting for '{label}'"
)));
}
Err(_elapsed) => {
return Err(ClusterError::Timeout(format!(
"actor '{label}' not available after {timeout:?}"
)));
}
}
}
_ = poll_ticker.tick() => {
let vv = self.receptionist.version_vector();
if let Err(e) = self
.net
.send_control(&self.server_node_id, ControlMessage::RegistrySyncRequest(vv))
.await
{
tracing::debug!("lookup_wait: re-poll failed: {e}");
}
}
}
}
}
pub async fn disconnect(self) {
self.net.remove_connection(&self.server_node_id).await;
self.shutdown.cancel();
}
pub fn is_connected(&self) -> bool {
!self.shutdown.is_cancelled()
}
}
#[allow(clippy::too_many_arguments)]
fn spawn_edge_event_loop(
control_recv: Box<dyn RecvHalf>,
net: Arc<dyn Net>,
receptionist: Receptionist,
type_registry: Arc<TypeRegistry>,
server_node_id: String,
event_tx: broadcast::Sender<ClusterEvent>,
sync_interval: Option<Duration>,
shutdown: CancellationToken,
) {
let (control_in_tx, mut control_in_rx) = mpsc::unbounded_channel::<(String, ControlMessage)>();
tokio::spawn(run_control_stream_reader(
control_recv,
control_in_tx,
server_node_id.clone(),
shutdown.clone(),
));
let (sync_tick_tx, mut sync_tick_rx) = mpsc::unbounded_channel::<()>();
if let Some(interval) = sync_interval {
let tx = sync_tick_tx;
let shutdown_clone = shutdown.clone();
tokio::spawn(async move {
let mut timer = tokio::time::interval(interval);
timer.tick().await; loop {
tokio::select! {
_ = timer.tick() => {
if tx.send(()).is_err() {
break;
}
}
_ = shutdown_clone.cancelled() => break,
}
}
});
}
tokio::spawn(async move {
loop {
tokio::select! {
Some((_node_id, msg)) = control_in_rx.recv() => {
if let ControlMessage::RegistrySync(ops) = msg {
apply_remote_ops(
ops,
&receptionist,
&type_registry,
&server_node_id,
&event_tx,
&net,
);
}
}
Some(()) = sync_tick_rx.recv() => {
let vv = receptionist.version_vector();
if let Err(e) = net
.send_control(&server_node_id, ControlMessage::RegistrySyncRequest(vv))
.await
{
tracing::debug!("Edge periodic sync failed: {e}");
}
}
_ = shutdown.cancelled() => break,
}
}
});
}