use std::collections::{BTreeMap, BTreeSet};
use bytes::Bytes;
use tokio::time::{timeout_at, Instant};
use tracing::{debug, info};
use ts_wire::{
decode_bytes, encode_frames,
handshake::{STATUS_HELLO, STATUS_READY},
Dict, EngineCoreReadyResponse, HandshakeAddresses, HandshakeInitMessage, ReadyMessage,
};
use zeromq::{PullSocket, RouterSocket, Socket, SocketRecv, SocketSend, ZmqMessage};
use crate::{
config::ClientConfig,
error::{Error, Result},
};
#[derive(Debug, Clone)]
pub struct EngineInfo {
pub index: u16,
pub ready: EngineCoreReadyResponse,
}
pub(crate) struct Bound {
pub handshake_address: String,
handshake: RouterSocket,
input: RouterSocket,
output: PullSocket,
init: Vec<Bytes>,
expected: BTreeSet<u16>,
}
pub(crate) struct Connected {
pub engines: Vec<EngineInfo>,
pub input: RouterSocket,
pub output: PullSocket,
}
pub(crate) async fn bind(config: &ClientConfig) -> Result<Bound> {
let last_index = u32::from(config.first_engine_index) + u32::from(config.num_engines);
if config.num_engines == 0 || last_index > u32::from(u16::MAX) + 1 {
return Err(Error::Handshake(format!(
"engine indices {}..{last_index} are empty or exceed the two-byte identity",
config.first_engine_index
)));
}
let expected: BTreeSet<u16> = (config.first_engine_index..=(last_index - 1) as u16).collect();
let mut handshake = RouterSocket::new();
let handshake_address = handshake.bind(&config.handshake_address).await?.to_string();
let mut input = RouterSocket::new();
let input_address = input.bind(&config.input_address).await?.to_string();
let mut output = PullSocket::new();
let output_address = output.bind(&config.output_address).await?.to_string();
info!(
handshake = %handshake_address,
input = %input_address,
output = %output_address,
engines = ?expected,
"frontend sockets bound"
);
let init = encode_frames(&HandshakeInitMessage {
addresses: HandshakeAddresses {
inputs: vec![input_address],
outputs: vec![output_address],
coordinator_input: None,
coordinator_output: None,
frontend_stats_publish_address: None,
},
parallel_config: Dict::default(),
});
Ok(Bound {
handshake_address,
handshake,
input,
output,
init,
expected,
})
}
pub(crate) async fn accept(bound: Bound, config: &ClientConfig) -> Result<Connected> {
let deadline = Instant::now() + config.handshake_timeout;
let Bound {
mut handshake,
mut input,
output,
init,
expected,
..
} = bound;
let mut awaiting_hello = expected.clone();
let mut awaiting_ready = expected.clone();
while !awaiting_ready.is_empty() {
let message =
recv_before(&mut handshake, deadline, config, "READY", &awaiting_ready).await?;
let (index, payload) = engine_message(message, &expected)?;
let status: ReadyMessage = decode_bytes(&payload)?;
match status.status.as_deref() {
Some(STATUS_HELLO) => {
if !awaiting_hello.remove(&index) {
return Err(Error::Handshake(format!("engine {index} sent HELLO twice")));
}
let mut reply = ZmqMessage::from(Bytes::copy_from_slice(&index.to_le_bytes()));
for frame in &init {
reply.push_back(frame.clone());
}
handshake.send(reply).await?;
debug!(engine = index, "sent INIT");
}
Some(STATUS_READY) => {
if awaiting_hello.contains(&index) || !awaiting_ready.remove(&index) {
return Err(Error::Handshake(format!(
"engine {index} sent READY out of order"
)));
}
debug!(engine = index, "engine READY");
}
other => {
return Err(Error::Handshake(format!(
"engine {index} sent unknown status {other:?}"
)))
}
}
}
let mut registered: BTreeMap<u16, EngineCoreReadyResponse> = BTreeMap::new();
while registered.len() < expected.len() {
let pending: BTreeSet<u16> = expected
.iter()
.filter(|index| !registered.contains_key(index))
.copied()
.collect();
let message = recv_before(&mut input, deadline, config, "registration", &pending).await?;
let (index, payload) = engine_message(message, &expected)?;
let ready: EngineCoreReadyResponse = decode_bytes(&payload)?;
check_rank(config, index, &ready)?;
if registered.insert(index, ready).is_some() {
return Err(Error::Handshake(format!("engine {index} registered twice")));
}
}
info!(engines = registered.len(), "all engines registered");
Ok(Connected {
engines: registered
.into_iter()
.map(|(index, ready)| EngineInfo { index, ready })
.collect(),
input,
output,
})
}
async fn recv_before(
socket: &mut RouterSocket,
deadline: Instant,
config: &ClientConfig,
stage: &'static str,
pending: &BTreeSet<u16>,
) -> Result<ZmqMessage> {
match timeout_at(deadline, socket.recv()).await {
Ok(message) => Ok(message?),
Err(_) => Err(Error::HandshakeTimeout {
stage,
pending: pending.iter().copied().collect(),
timeout: config.handshake_timeout,
}),
}
}
fn engine_message(message: ZmqMessage, expected: &BTreeSet<u16>) -> Result<(u16, Bytes)> {
let frames = message.into_vec();
let [identity, payload] = frames.as_slice() else {
return Err(Error::Handshake(format!(
"expected an identity and a payload, got {} frames",
frames.len()
)));
};
let index = match identity.as_ref() {
[low, high] => u16::from_le_bytes([*low, *high]),
other => {
return Err(Error::Handshake(format!(
"peer identity {other:02x?} is not a two-byte engine index"
)))
}
};
if !expected.contains(&index) {
return Err(Error::Handshake(format!(
"unexpected engine {index}; expecting {expected:?}"
)));
}
Ok((index, payload.clone()))
}
fn check_rank(config: &ClientConfig, index: u16, ready: &EngineCoreReadyResponse) -> Result<()> {
let rank = i64::from(index - config.first_engine_index);
if ready.data_parallel_size != i64::from(config.num_engines) || ready.data_parallel_rank != rank
{
return Err(Error::Handshake(format!(
"engine {index} reports DP rank {} of {}, expected rank {rank} of {}",
ready.data_parallel_rank, ready.data_parallel_size, config.num_engines
)));
}
Ok(())
}