1use std::collections::{BTreeMap, BTreeSet};
24
25use bytes::Bytes;
26use tokio::time::{timeout_at, Instant};
27use tracing::{debug, info};
28use ts_wire::{
29 decode_bytes, encode_frames,
30 handshake::{STATUS_HELLO, STATUS_READY},
31 Dict, EngineCoreReadyResponse, HandshakeAddresses, HandshakeInitMessage, ReadyMessage,
32};
33use zeromq::{PullSocket, RouterSocket, Socket, SocketRecv, SocketSend, ZmqMessage};
34
35use crate::{
36 config::ClientConfig,
37 error::{Error, Result},
38};
39
40#[derive(Debug, Clone)]
42pub struct EngineInfo {
43 pub index: u16,
45 pub ready: EngineCoreReadyResponse,
47}
48
49pub(crate) struct Bound {
51 pub handshake_address: String,
53 handshake: RouterSocket,
54 input: RouterSocket,
55 output: PullSocket,
56 init: Vec<Bytes>,
58 expected: BTreeSet<u16>,
59}
60
61pub(crate) struct Connected {
63 pub engines: Vec<EngineInfo>,
64 pub input: RouterSocket,
65 pub output: PullSocket,
66}
67
68pub(crate) async fn bind(config: &ClientConfig) -> Result<Bound> {
70 let last_index = u32::from(config.first_engine_index) + u32::from(config.num_engines);
71 if config.num_engines == 0 || last_index > u32::from(u16::MAX) + 1 {
72 return Err(Error::Handshake(format!(
73 "engine indices {}..{last_index} are empty or exceed the two-byte identity",
74 config.first_engine_index
75 )));
76 }
77 let expected: BTreeSet<u16> = (config.first_engine_index..=(last_index - 1) as u16).collect();
78
79 let mut handshake = RouterSocket::new();
80 let handshake_address = handshake.bind(&config.handshake_address).await?.to_string();
81 let mut input = RouterSocket::new();
82 let input_address = input.bind(&config.input_address).await?.to_string();
83 let mut output = PullSocket::new();
84 let output_address = output.bind(&config.output_address).await?.to_string();
85 info!(
86 handshake = %handshake_address,
87 input = %input_address,
88 output = %output_address,
89 engines = ?expected,
90 "frontend sockets bound"
91 );
92
93 let init = encode_frames(&HandshakeInitMessage {
94 addresses: HandshakeAddresses {
95 inputs: vec![input_address],
96 outputs: vec![output_address],
97 coordinator_input: None,
98 coordinator_output: None,
99 frontend_stats_publish_address: None,
100 },
101 parallel_config: Dict::default(),
102 });
103 Ok(Bound {
104 handshake_address,
105 handshake,
106 input,
107 output,
108 init,
109 expected,
110 })
111}
112
113pub(crate) async fn accept(bound: Bound, config: &ClientConfig) -> Result<Connected> {
115 let deadline = Instant::now() + config.handshake_timeout;
116 let Bound {
117 mut handshake,
118 mut input,
119 output,
120 init,
121 expected,
122 ..
123 } = bound;
124
125 let mut awaiting_hello = expected.clone();
127 let mut awaiting_ready = expected.clone();
128 while !awaiting_ready.is_empty() {
129 let message =
130 recv_before(&mut handshake, deadline, config, "READY", &awaiting_ready).await?;
131 let (index, payload) = engine_message(message, &expected)?;
132 let status: ReadyMessage = decode_bytes(&payload)?;
133 match status.status.as_deref() {
134 Some(STATUS_HELLO) => {
135 if !awaiting_hello.remove(&index) {
136 return Err(Error::Handshake(format!("engine {index} sent HELLO twice")));
137 }
138 let mut reply = ZmqMessage::from(Bytes::copy_from_slice(&index.to_le_bytes()));
139 for frame in &init {
140 reply.push_back(frame.clone());
141 }
142 handshake.send(reply).await?;
143 debug!(engine = index, "sent INIT");
144 }
145 Some(STATUS_READY) => {
146 if awaiting_hello.contains(&index) || !awaiting_ready.remove(&index) {
147 return Err(Error::Handshake(format!(
148 "engine {index} sent READY out of order"
149 )));
150 }
151 debug!(engine = index, "engine READY");
152 }
153 other => {
154 return Err(Error::Handshake(format!(
155 "engine {index} sent unknown status {other:?}"
156 )))
157 }
158 }
159 }
160
161 let mut registered: BTreeMap<u16, EngineCoreReadyResponse> = BTreeMap::new();
163 while registered.len() < expected.len() {
164 let pending: BTreeSet<u16> = expected
165 .iter()
166 .filter(|index| !registered.contains_key(index))
167 .copied()
168 .collect();
169 let message = recv_before(&mut input, deadline, config, "registration", &pending).await?;
170 let (index, payload) = engine_message(message, &expected)?;
171 let ready: EngineCoreReadyResponse = decode_bytes(&payload)?;
172 check_rank(config, index, &ready)?;
173 if registered.insert(index, ready).is_some() {
174 return Err(Error::Handshake(format!("engine {index} registered twice")));
175 }
176 }
177 info!(engines = registered.len(), "all engines registered");
178
179 Ok(Connected {
180 engines: registered
181 .into_iter()
182 .map(|(index, ready)| EngineInfo { index, ready })
183 .collect(),
184 input,
185 output,
186 })
187}
188
189async fn recv_before(
190 socket: &mut RouterSocket,
191 deadline: Instant,
192 config: &ClientConfig,
193 stage: &'static str,
194 pending: &BTreeSet<u16>,
195) -> Result<ZmqMessage> {
196 match timeout_at(deadline, socket.recv()).await {
197 Ok(message) => Ok(message?),
198 Err(_) => Err(Error::HandshakeTimeout {
199 stage,
200 pending: pending.iter().copied().collect(),
201 timeout: config.handshake_timeout,
202 }),
203 }
204}
205
206fn engine_message(message: ZmqMessage, expected: &BTreeSet<u16>) -> Result<(u16, Bytes)> {
208 let frames = message.into_vec();
209 let [identity, payload] = frames.as_slice() else {
210 return Err(Error::Handshake(format!(
211 "expected an identity and a payload, got {} frames",
212 frames.len()
213 )));
214 };
215 let index = match identity.as_ref() {
216 [low, high] => u16::from_le_bytes([*low, *high]),
217 other => {
218 return Err(Error::Handshake(format!(
219 "peer identity {other:02x?} is not a two-byte engine index"
220 )))
221 }
222 };
223 if !expected.contains(&index) {
224 return Err(Error::Handshake(format!(
225 "unexpected engine {index}; expecting {expected:?}"
226 )));
227 }
228 Ok((index, payload.clone()))
229}
230
231fn check_rank(config: &ClientConfig, index: u16, ready: &EngineCoreReadyResponse) -> Result<()> {
233 let rank = i64::from(index - config.first_engine_index);
234 if ready.data_parallel_size != i64::from(config.num_engines) || ready.data_parallel_rank != rank
235 {
236 return Err(Error::Handshake(format!(
237 "engine {index} reports DP rank {} of {}, expected rank {rank} of {}",
238 ready.data_parallel_rank, ready.data_parallel_size, config.num_engines
239 )));
240 }
241 Ok(())
242}