1use crate::error::{Result, WorkerError};
7use somatize_compiler::ExecutionPlan;
8use somatize_core::value::Value;
9use somatize_runtime::executor::RunMode;
10use somatize_runtime::node_catalog::NodeCatalog;
11use somatize_runtime::runner::Transport;
12use std::collections::HashMap;
13
14use crate::protocol::*;
15
16pub struct WsTransport {
18 pub address: String,
21 pub token: Option<String>,
24}
25
26fn on_own_runtime<F, T>(fut: F) -> Result<T>
43where
44 F: std::future::Future<Output = Result<T>> + Send,
45 T: Send,
46{
47 std::thread::scope(|scope| {
48 scope
49 .spawn(|| {
50 tokio::runtime::Builder::new_current_thread()
51 .enable_all()
52 .build()
53 .map_err(|e| WorkerError::Concurrency(format!("tokio: {e}")))?
54 .block_on(fut)
55 })
56 .join()
57 .map_err(|_| WorkerError::Concurrency("transport thread panicked".into()))?
58 })
59}
60
61impl WsTransport {
62 pub fn new(address: impl Into<String>, token: Option<String>) -> Self {
65 Self {
66 address: address.into(),
67 token,
68 }
69 }
70
71 fn http_addr(&self) -> String {
73 self.address
74 .replace("ws://", "http://")
75 .replace("wss://", "https://")
76 }
77
78 pub fn send_msg(&self, msg: &CoordinatorToWorker) -> Result<WorkerToCoordinator> {
86 on_own_runtime(async {
87 let url = if let Some(t) = &self.token {
88 format!("{}/ws?token={t}", self.address)
89 } else {
90 format!("{}/ws", self.address)
91 };
92
93 let ws_config = {
94 let mut c = tokio_tungstenite::tungstenite::protocol::WebSocketConfig::default();
95 c.max_message_size = None;
96 c.max_frame_size = None;
97 c
98 };
99
100 let (mut ws, _) =
101 tokio_tungstenite::connect_async_with_config(&url, Some(ws_config), false)
102 .await
103 .map_err(|e| WorkerError::Transport(format!("WS connect: {e}")))?;
104
105 use futures_util::{SinkExt, StreamExt};
106 use tokio_tungstenite::tungstenite::Message;
107
108 let json = serde_json::to_string(msg)
109 .map_err(|e| WorkerError::Encoding(format!("serialize: {e}")))?;
110
111 ws.send(Message::Text(json.into()))
112 .await
113 .map_err(|e| WorkerError::Transport(format!("WS send: {e}")))?;
114
115 while let Some(frame) = ws.next().await {
116 let Ok(Message::Text(response)) = frame else {
119 continue;
120 };
121 let parsed = serde_json::from_str::<WorkerToCoordinator>(&response);
122 let _ = ws.close(None).await;
123 return match parsed {
124 Ok(WorkerToCoordinator::Error { message }) => {
127 Err(WorkerError::Transport(format!("remote worker: {message}")))
128 }
129 Ok(result) => Ok(result),
130 Err(e) => Err(WorkerError::Encoding(format!(
135 "cannot read the worker's reply: {e}. Raw: {}",
136 response.chars().take(200).collect::<String>()
137 ))),
138 };
139 }
140
141 Err(WorkerError::Transport(
142 "worker closed without response".into(),
143 ))
144 })
145 }
146
147 pub fn notify(&self, msg: &CoordinatorToWorker) -> Result<()> {
153 let url = match &self.token {
154 Some(t) => format!("{}/ws?token={t}", self.address),
155 None => format!("{}/ws", self.address),
156 };
157 let json = serde_json::to_string(msg)
158 .map_err(|e| WorkerError::Encoding(format!("serialize: {e}")))?;
159
160 on_own_runtime(async move {
161 use futures_util::SinkExt;
162 use tokio_tungstenite::tungstenite::Message;
163
164 let (mut ws, _) = tokio_tungstenite::connect_async(&url)
165 .await
166 .map_err(|e| WorkerError::Transport(format!("WS connect: {e}")))?;
167 ws.send(Message::Text(json.into()))
168 .await
169 .map_err(|e| WorkerError::Transport(format!("WS send: {e}")))
170 })
171 }
172
173 pub fn upload(&self, value: &Value) -> Result<somatize_core::store::DataRef> {
176 let url = format!("{}/upload", self.http_addr());
177 let body = serde_json::to_vec(value)
178 .map_err(|e| WorkerError::Encoding(format!("serialize upload: {e}")))?;
179 let token = self.token.clone();
180
181 std::thread::spawn(move || {
184 let client = reqwest::blocking::Client::new();
185 let mut req = client
186 .post(&url)
187 .header("Content-Type", "application/json")
188 .body(body);
189 if let Some(t) = &token {
190 req = req.query(&[("token", t.as_str())]);
191 }
192 let resp = req
193 .send()
194 .map_err(|e| WorkerError::Transport(format!("HTTP upload: {e}")))?;
195 if !resp.status().is_success() {
196 return Err(WorkerError::Transport(format!(
197 "HTTP upload failed: {}",
198 resp.status()
199 )));
200 }
201 resp.json::<somatize_core::store::DataRef>()
202 .map_err(|e| WorkerError::Encoding(format!("parse upload response: {e}")))
203 })
204 .join()
205 .map_err(|_| WorkerError::Concurrency("upload thread panicked".into()))?
206 }
207
208 pub fn stream_plan(&self, plan: SerializedPlan, chunks: Vec<Value>) -> Result<Value> {
216 let stream_id = plan.plan_id.clone();
217 let total_chunks = chunks.len();
218 let url = match &self.token {
219 Some(t) => format!("{}/ws?token={t}", self.address),
220 None => format!("{}/ws", self.address),
221 };
222
223 on_own_runtime(async move {
224 use futures_util::StreamExt;
225 use tokio_tungstenite::tungstenite::Message;
226
227 let (mut ws, _) = tokio_tungstenite::connect_async(&url)
228 .await
229 .map_err(|e| WorkerError::Transport(format!("WS connect: {e}")))?;
230
231 send_frame(
232 &mut ws,
233 StreamMessage::StreamBegin {
234 stream_id: stream_id.clone(),
235 plan_id: stream_id.clone(),
236 total_chunks: Some(total_chunks),
237 plan: Box::new(plan),
238 },
239 )
240 .await?;
241
242 let mut results: Vec<Value> = Vec::new();
243
244 for (i, chunk) in chunks.into_iter().enumerate() {
245 send_frame(
246 &mut ws,
247 StreamMessage::ChunkData {
248 stream_id: stream_id.clone(),
249 chunk_index: i,
250 value: chunk,
251 },
252 )
253 .await?;
254
255 while let Ok(Some(Ok(Message::Binary(resp)))) =
258 tokio::time::timeout(std::time::Duration::from_millis(1), ws.next()).await
259 {
260 if let Ok(StreamMessage::ChunkResult { value, .. }) =
261 crate::protocol::decode_frame(&resp)
262 {
263 results.push(value);
264 }
265 }
266 }
267
268 send_frame(
269 &mut ws,
270 StreamMessage::StreamEnd {
271 stream_id: stream_id.clone(),
272 },
273 )
274 .await?;
275
276 let mut flushed: Option<Value> = None;
278 while let Some(Ok(Message::Binary(resp))) = ws.next().await {
279 match crate::protocol::decode_frame(&resp) {
280 Ok(StreamMessage::ChunkResult { value, .. }) => results.push(value),
281 Ok(StreamMessage::StreamComplete { result, .. }) => match result {
282 PlanResult::Success { output, .. } => {
283 let v = self.resolve_output(&output)?;
284 if !v.is_empty() {
285 flushed = Some(v);
286 }
287 break;
288 }
289 PlanResult::Failed { error, .. } => {
290 return Err(WorkerError::Remote(format!("stream error: {error}")));
291 }
292 },
293 _ => {}
294 }
295 }
296
297 if let Some(v) = flushed {
298 results.push(v);
299 }
300 match results.len() {
301 0 => Ok(Value::Empty),
302 1 => Ok(results.into_iter().next().unwrap()),
303 _ => Ok(somatize_runtime::executors::materialize_buffer(&results)?),
304 }
305 })
306 }
307
308 pub fn resolve_output(&self, delivery: &OutputDelivery) -> Result<Value> {
310 match delivery {
311 OutputDelivery::Inline { value } => Ok(value.clone()),
312 OutputDelivery::Reference { data_ref } => {
313 let url = format!("{}/download", self.http_addr());
314 let ref_json = serde_json::to_string(data_ref)
315 .map_err(|e| WorkerError::Encoding(format!("serialize ref: {e}")))?;
316 let token = self.token.clone();
317
318 std::thread::spawn(move || {
319 let client = reqwest::blocking::Client::new();
320 let mut req = client.get(&url).query(&[("ref", &ref_json)]);
321 if let Some(t) = &token {
322 req = req.query(&[("token", t.as_str())]);
323 }
324 let resp = req
325 .send()
326 .map_err(|e| WorkerError::Transport(format!("HTTP download: {e}")))?;
327 let bytes = resp
328 .bytes()
329 .map_err(|e| WorkerError::Transport(format!("read response: {e}")))?;
330 serde_json::from_slice(&bytes)
331 .map_err(|e| WorkerError::Encoding(format!("deserialize: {e}")))
332 })
333 .join()
334 .map_err(|_| WorkerError::Concurrency("download thread panicked".into()))?
335 }
336 }
337 }
338}
339
340async fn send_frame<S>(ws: &mut S, msg: StreamMessage) -> Result<()>
342where
343 S: futures_util::Sink<tokio_tungstenite::tungstenite::Message> + Unpin,
344 S::Error: std::fmt::Display,
345{
346 use futures_util::SinkExt;
347 let bytes = crate::protocol::encode_frame(&msg)?;
348 ws.send(tokio_tungstenite::tungstenite::Message::Binary(
349 bytes.into(),
350 ))
351 .await
352 .map_err(|e| WorkerError::Transport(format!("WS send: {e}")))
353}
354
355fn wire_plan(
379 plan: &ExecutionPlan,
380 input: &Value,
381 mode: &RunMode,
382 seed: Option<i64>,
383) -> SerializedPlan {
384 SerializedPlan {
385 protocol_version: PROTOCOL_VERSION,
386 plan_id: somatize_core::util::timestamp_id("remote"),
387 plan: plan.clone(),
388 input: Some(InputSource::Inline {
389 value: input.clone(),
390 }),
391 filters: vec![],
392 mode: match mode {
393 RunMode::Fit { y } => ExecutionMode::Fit {
394 y: y.clone(),
395 batch_size: None,
396 },
397 RunMode::Forward => ExecutionMode::Forward,
398 },
399 seed,
400 metadata: serde_json::json!({}),
401 }
402}
403
404impl Transport for WsTransport {
405 fn execute(
406 &self,
407 plan: &ExecutionPlan,
408 _filters: &NodeCatalog,
409 input: &Value,
410 mode: &RunMode,
411 seed: Option<i64>,
412 ) -> somatize_core::error::Result<(Value, HashMap<String, Value>)> {
413 let serialized = wire_plan(plan, input, mode, seed);
414
415 let msg = CoordinatorToWorker::AssignPlan { plan: serialized };
416 match self.send_msg(&msg)? {
417 WorkerToCoordinator::PlanResult { result, .. } => match result {
418 PlanResult::Success { output, states, .. } => {
419 let value = self.resolve_output(&output)?;
420 Ok((value, states))
421 }
422 PlanResult::Failed { error, .. } => {
423 Err(WorkerError::Remote(format!("remote: {error}")).into())
424 }
425 },
426 other => {
427 Err(WorkerError::Transport(format!("expected PlanResult, got: {other:?}")).into())
428 }
429 }
430 }
431
432 fn get_state(
433 &self,
434 node_ids: &[String],
435 ) -> somatize_core::error::Result<HashMap<String, Value>> {
436 let msg = CoordinatorToWorker::GetState {
437 plan_id: String::new(),
438 node_ids: node_ids.to_vec(),
439 };
440 match self.send_msg(&msg)? {
441 WorkerToCoordinator::StateResult { states, .. } => Ok(states),
442 other => {
443 Err(WorkerError::Transport(format!("expected StateResult, got: {other:?}")).into())
444 }
445 }
446 }
447
448 fn set_state(&self, states: &HashMap<String, Value>) -> somatize_core::error::Result<()> {
449 let msg = CoordinatorToWorker::SetState {
450 plan_id: String::new(),
451 states: states.clone(),
452 };
453 self.send_msg(&msg)?;
454 Ok(())
455 }
456
457 fn get_gradients(
458 &self,
459 node_ids: &[String],
460 ) -> somatize_core::error::Result<HashMap<String, Value>> {
461 let msg = CoordinatorToWorker::GetGradients {
462 plan_id: String::new(),
463 node_ids: node_ids.to_vec(),
464 };
465 match self.send_msg(&msg)? {
466 WorkerToCoordinator::GradientsResult { gradients, .. } => Ok(gradients),
467 other => Err(WorkerError::Transport(format!(
468 "expected GradientsResult, got: {other:?}"
469 ))
470 .into()),
471 }
472 }
473
474 fn apply_gradients(
475 &self,
476 gradients: &HashMap<String, Value>,
477 ) -> somatize_core::error::Result<()> {
478 let msg = CoordinatorToWorker::ApplyGradients {
479 plan_id: String::new(),
480 gradients: gradients.clone(),
481 };
482 self.send_msg(&msg)?;
483 Ok(())
484 }
485}
486
487#[cfg(test)]
488mod tests {
489 use super::*;
490
491 fn plan() -> ExecutionPlan {
492 ExecutionPlan::Execute {
493 node_id: "n".into(),
494 }
495 }
496
497 #[test]
502 fn wire_plan_carries_the_run_seed() {
503 let p = wire_plan(&plan(), &Value::Empty, &RunMode::Forward, Some(7));
504 assert_eq!(p.seed, Some(7));
505 }
506
507 #[test]
508 fn wire_plan_without_a_seed_stays_unseeded() {
509 let p = wire_plan(&plan(), &Value::Empty, &RunMode::Forward, None);
510 assert_eq!(p.seed, None);
511 }
512
513 #[test]
516 fn different_seeds_differ_on_the_wire() {
517 let a = wire_plan(&plan(), &Value::Empty, &RunMode::Forward, Some(1));
518 let b = wire_plan(&plan(), &Value::Empty, &RunMode::Forward, Some(2));
519 assert_ne!(a.seed, b.seed);
520 }
521
522 #[test]
527 fn wire_plan_sends_no_filters() {
528 let p = wire_plan(&plan(), &Value::Empty, &RunMode::Forward, Some(1));
529 assert!(p.filters.is_empty());
530 }
531
532 #[test]
533 fn fit_mode_carries_labels_and_the_seed_together() {
534 let y = Value::tensor(vec![1.0], vec![1]);
535 let p = wire_plan(
536 &plan(),
537 &Value::Empty,
538 &RunMode::Fit { y: Some(y.clone()) },
539 Some(3),
540 );
541 assert_eq!(p.seed, Some(3));
542 match p.mode {
543 ExecutionMode::Fit { y: got, .. } => assert_eq!(got, Some(y)),
544 other => panic!("expected Fit, got {other:?}"),
545 }
546 }
547}