pub(crate) mod builders;
mod peer_registry;
use anyhow::Result;
use std::sync::Arc;
use std::time::Duration;
use crate::messenger::PeerDiscovery;
use crate::messenger::common::{ActiveMessage, responses::ResponseManager};
use crate::observability::{ClientResolution, VeloMetrics};
use crate::transports::{SendOutcome, TransportErrorHandler, VeloBackend};
use peer_registry::PeerRegistry;
use velo_ext::InstanceId;
const DEFAULT_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(30);
pub(crate) struct ActiveMessageClient {
pub(crate) response_manager: ResponseManager,
pub(crate) backend: Arc<VeloBackend>,
error_handler: Arc<dyn TransportErrorHandler>,
peer_registry: Arc<PeerRegistry>,
discovery: Option<Arc<dyn PeerDiscovery>>,
handshake_timeout: Duration,
observability: Option<Arc<VeloMetrics>>,
pub(crate) large_payload_stager:
Arc<std::sync::OnceLock<Arc<dyn crate::messenger::large_payload::LargePayloadStager>>>,
}
impl ActiveMessageClient {
pub(crate) fn new(
response_manager: ResponseManager,
backend: Arc<VeloBackend>,
error_handler: Arc<dyn TransportErrorHandler>,
discovery: Option<Arc<dyn PeerDiscovery>>,
observability: Option<Arc<VeloMetrics>>,
) -> Self {
Self {
response_manager,
backend,
error_handler,
peer_registry: Arc::new(PeerRegistry::new()),
discovery,
handshake_timeout: DEFAULT_HANDSHAKE_TIMEOUT,
observability,
large_payload_stager: Arc::new(std::sync::OnceLock::new()),
}
}
#[allow(unused_mut)]
pub(crate) fn send_message(
&self,
target: InstanceId,
mut message: ActiveMessage,
) -> Result<SendOutcome> {
if let Some(stager) = self.large_payload_stager.get()
&& message.payload.len() > stager.threshold()
{
let staged_payload = std::mem::replace(&mut message.payload, bytes::Bytes::new());
let handle_str = stager.stage(staged_payload);
message
.metadata
.headers
.get_or_insert_with(std::collections::HashMap::new)
.insert(
crate::messenger::large_payload::RV_HEADER_KEY.to_string(),
handle_str,
);
}
#[cfg(feature = "distributed-tracing")]
crate::observability::inject_current_context(&mut message.metadata.headers);
let (header, payload, message_type) = message.encode()?;
#[cfg(feature = "distributed-tracing")]
{
let span = tracing::info_span!(
"velo.messenger.client_send",
target = %target,
message_type = ?message_type,
bytes = header.len() + payload.len()
);
let _entered = span.enter();
self.backend.send_message(
target,
header,
payload,
message_type,
self.error_handler.clone(),
)
}
#[cfg(not(feature = "distributed-tracing"))]
self.backend.send_message(
target,
header,
payload,
message_type,
self.error_handler.clone(),
)
}
pub(crate) fn register_peer(&self, instance_id: InstanceId) {
self.peer_registry.register_peer(instance_id);
}
pub(crate) fn is_peer_registered(&self, instance_id: InstanceId) -> bool {
self.backend.is_registered(instance_id)
}
pub(crate) fn has_handler_info(&self, instance_id: InstanceId) -> bool {
self.peer_registry.has_handler_info(instance_id)
}
pub(crate) fn can_send_directly(&self, target: InstanceId, handler: &str) -> bool {
if !self.is_peer_registered(target) {
return false;
}
if handler.starts_with('_') {
return true;
}
self.peer_registry.handler_exists(target, handler)
}
async fn handshake_with_peer(&self, target: InstanceId) -> Result<()> {
use crate::messenger::server::system_handlers::{HandlersResponse, HelloRequest};
tracing::debug!(
target: "crate::messenger::client",
target_instance = %target,
"Initiating handshake with peer"
);
if let Some(metrics) = self.observability.as_ref() {
metrics.record_client_resolution(ClientResolution::HandshakeAttempt);
}
let request = HelloRequest {
peer_info: self.backend.peer_info(),
};
let payload = serde_json::to_vec(&request)
.map_err(|e| anyhow::anyhow!("Failed to serialize _hello request: {}", e))?;
let mut outcome = self.register_outcome()?;
let response_id = outcome.response_id();
let message = crate::messenger::common::ActiveMessage {
metadata: crate::messenger::common::messages::MessageMetadata::new_unary(
response_id,
"_hello".to_string(),
None,
),
payload: bytes::Bytes::from(payload),
};
let send_outcome = self.send_message(target, message)?;
let result = tokio::time::timeout(self.handshake_timeout, async {
if let SendOutcome::Backpressured(bp) = send_outcome {
bp.await;
}
outcome.recv().await
})
.await;
let response_bytes = match result {
Ok(Ok(Some(bytes))) => bytes,
Ok(Ok(None)) => {
anyhow::bail!("Expected response from _hello, got empty acknowledgment");
}
Ok(Err(err)) => {
anyhow::bail!("Handshake failed: {}", err);
}
Err(_elapsed) => {
anyhow::bail!(
"Handshake with peer {} timed out after {:?}",
target,
self.handshake_timeout
);
}
};
let response: HandlersResponse = serde_json::from_slice(&response_bytes)
.map_err(|e| anyhow::anyhow!("Failed to deserialize _hello response: {}", e))?;
self.peer_registry
.update_handlers(target, response.handlers.clone());
tracing::debug!(
target: "crate::messenger::client",
target_instance = %target,
handler_count = response.handlers.len(),
"Handshake completed successfully"
);
if let Some(metrics) = self.observability.as_ref() {
metrics.record_client_resolution(ClientResolution::HandshakeSuccess);
}
Ok(())
}
pub(crate) async fn ensure_peer_ready(&self, target: InstanceId, handler: &str) -> Result<()> {
if !self.is_peer_registered(target) {
anyhow::bail!(
"Peer {} not registered. Call messenger.register_peer() first.",
target
);
}
if handler.starts_with('_') {
return Ok(());
}
if !self.has_handler_info(target) {
self.handshake_with_peer(target).await?;
}
if !self.peer_registry.handler_exists(target, handler) {
self.handshake_with_peer(target).await?;
}
if !self.peer_registry.handler_exists(target, handler) {
anyhow::bail!(
"Handler '{}' not found on instance {}. Available handlers: {:?}",
handler,
target,
self.peer_registry.get_handlers(target).unwrap_or_default()
);
}
Ok(())
}
pub(crate) async fn get_peer_handlers(&self, instance_id: InstanceId) -> Result<Vec<String>> {
if !self.has_handler_info(instance_id) {
self.handshake_with_peer(instance_id).await?;
}
self.peer_registry
.get_handlers(instance_id)
.ok_or_else(|| anyhow::anyhow!("Failed to get handlers for instance {}", instance_id))
}
pub(crate) async fn refresh_handler_list(&self, instance_id: InstanceId) -> Result<()> {
self.handshake_with_peer(instance_id).await
}
pub(crate) async fn resolve_peer_via_discovery(
&self,
worker_id: velo_ext::WorkerId,
) -> Result<InstanceId> {
tracing::debug!(
target: "crate::messenger::client",
worker_id = %worker_id,
"Resolving peer via discovery"
);
let discovery = self.discovery.as_ref().ok_or_else(|| {
anyhow::anyhow!(
"No discovery backend configured. Cannot resolve worker {}",
worker_id
)
})?;
let peer_info = discovery.discover_by_worker_id(worker_id).await?;
let instance_id = peer_info.instance_id();
if let Some(metrics) = self.observability.as_ref() {
metrics.record_client_resolution(ClientResolution::DiscoverySuccess);
}
tracing::debug!(
target: "crate::messenger::client",
worker_id = %worker_id,
instance_id = %instance_id,
"Discovery resolved peer, performing registration"
);
self.backend.register_peer(peer_info)?;
self.peer_registry.register_peer(instance_id);
Ok(instance_id)
}
pub(crate) fn register_outcome(
&self,
) -> Result<
crate::messenger::common::responses::ResponseAwaiter,
crate::messenger::common::responses::ResponseRegistrationError,
> {
self.response_manager.register_outcome()
}
}