1use std::collections::HashMap;
2use std::sync::{Arc, OnceLock};
3use std::time::Duration;
4
5use anyhow::{Context, Result, bail};
6use async_trait::async_trait;
7use futures::Future;
8use hydro_deploy_integration::{InitConfig, ServerPort};
9use memo_map::MemoMap;
10use serde::Serialize;
11use tokio::sync::{OnceCell, RwLock, mpsc};
12
13use super::build::{BuildError, BuildOutput, BuildParams, build_crate_memoized};
14use super::ports::{self, RustCratePortConfig};
15use super::tracing_options::TracingOptions;
16#[cfg(feature = "profile-folding")]
17use crate::TracingResults;
18use crate::progress::ProgressTracker;
19use crate::{
20 BaseServerStrategy, Host, LaunchedBinary, LaunchedHost, PortNetworkHint, ResourceBatch,
21 ResourceResult, ServerStrategy, Service,
22};
23
24pub struct RustCrateService {
25 id: usize,
26 pub(super) on: Arc<dyn Host>,
27 build_params: BuildParams,
28 tracing: Option<TracingOptions>,
29 args: Option<Vec<String>>,
30 display_id: Option<String>,
31 external_ports: Vec<u16>,
32 env: HashMap<String, String>,
33 pin_to_core: Option<usize>,
34
35 meta: OnceLock<String>,
36
37 pub(super) port_to_server: MemoMap<String, ports::ServerConfig>,
39 pub(super) port_to_bind: MemoMap<String, ServerStrategy>,
41
42 launched_host: OnceCell<Arc<dyn LaunchedHost>>,
43
44 pub(super) server_defns: Arc<RwLock<HashMap<String, ServerPort>>>,
48
49 launched_binary: OnceCell<Box<dyn LaunchedBinary>>,
50 started: OnceCell<()>,
51}
52
53impl RustCrateService {
54 #[expect(clippy::too_many_arguments, reason = "internal use")]
55 pub fn new(
56 id: usize,
57 on: Arc<dyn Host>,
58 build_params: BuildParams,
59 tracing: Option<TracingOptions>,
60 args: Option<Vec<String>>,
61 display_id: Option<String>,
62 external_ports: Vec<u16>,
63 env: HashMap<String, String>,
64 pin_to_core: Option<usize>,
65 ) -> Self {
66 Self {
67 id,
68 on,
69 build_params,
70 tracing,
71 args,
72 display_id,
73 external_ports,
74 env,
75 pin_to_core,
76 meta: OnceLock::new(),
77 port_to_server: MemoMap::new(),
78 port_to_bind: MemoMap::new(),
79 launched_host: OnceCell::new(),
80 server_defns: Arc::new(RwLock::new(HashMap::new())),
81 launched_binary: OnceCell::new(),
82 started: OnceCell::new(),
83 }
84 }
85
86 pub fn update_meta<T: Serialize>(&self, meta: T) {
87 assert!(
88 self.launched_binary.get().is_none(),
89 "Cannot update meta after binary has been launched"
90 );
91 self.meta
92 .set(serde_json::to_string(&meta).unwrap())
93 .expect("Cannot set meta twice.");
94 }
95
96 pub fn get_port(self: &Arc<Self>, name: String) -> RustCratePortConfig {
97 RustCratePortConfig {
98 service: Arc::downgrade(self),
99 service_host: self.on.clone(),
100 service_server_defns: self.server_defns.clone(),
101 network_hint: PortNetworkHint::Auto,
102 port: name,
103 merge: false,
104 }
105 }
106
107 pub fn get_port_with_hint(
108 self: &Arc<Self>,
109 name: String,
110 network_hint: PortNetworkHint,
111 ) -> RustCratePortConfig {
112 RustCratePortConfig {
113 service: Arc::downgrade(self),
114 service_host: self.on.clone(),
115 service_server_defns: self.server_defns.clone(),
116 network_hint,
117 port: name,
118 merge: false,
119 }
120 }
121
122 pub fn stdout(&self) -> mpsc::UnboundedReceiver<String> {
123 self.launched_binary.get().unwrap().stdout()
124 }
125
126 pub fn stderr(&self) -> mpsc::UnboundedReceiver<String> {
127 self.launched_binary.get().unwrap().stderr()
128 }
129
130 pub fn stdout_filter(&self, prefix: String) -> mpsc::UnboundedReceiver<String> {
131 self.launched_binary.get().unwrap().stdout_filter(prefix)
132 }
133
134 pub fn stderr_filter(&self, prefix: String) -> mpsc::UnboundedReceiver<String> {
135 self.launched_binary.get().unwrap().stderr_filter(prefix)
136 }
137
138 #[cfg(feature = "profile-folding")]
139 pub fn tracing_results(&self) -> Option<&TracingResults> {
140 self.launched_binary.get().unwrap().tracing_results()
141 }
142
143 pub fn exit_code(&self) -> Option<i32> {
144 self.launched_binary.get().unwrap().exit_code()
145 }
146
147 fn build(
148 &self,
149 ) -> impl use<> + 'static + Future<Output = Result<&'static BuildOutput, BuildError>> {
150 build_crate_memoized(self.build_params.clone())
152 }
153}
154
155#[async_trait]
156impl Service for RustCrateService {
157 fn collect_resources(&self, _resource_batch: &mut ResourceBatch) {
158 if self.launched_host.get().is_some() {
159 return;
160 }
161
162 tokio::task::spawn(self.build());
163
164 let host = &self.on;
165
166 host.request_custom_binary();
167 for (_, bind_type) in self.port_to_bind.iter() {
168 host.request_port(bind_type);
169 }
170
171 for port in self.external_ports.iter() {
172 host.request_port_base(&BaseServerStrategy::ExternalTcpPort(*port));
173 }
174 }
175
176 async fn deploy(&self, resource_result: &Arc<ResourceResult>) -> Result<()> {
177 self.launched_host
178 .get_or_try_init::<anyhow::Error, _, _>(|| {
179 ProgressTracker::with_group(
180 self.display_id
181 .clone()
182 .unwrap_or_else(|| format!("service/{}", self.id)),
183 None,
184 || async {
185 let built = self.build().await?;
186
187 let host = &self.on;
188 let launched = host.provision(resource_result);
189
190 launched.copy_binary(built).await?;
191 Ok(launched)
192 },
193 )
194 })
195 .await?;
196 Ok(())
197 }
198
199 async fn ready(&self) -> Result<()> {
200 self.launched_binary
201 .get_or_try_init(|| {
202 ProgressTracker::with_group(
203 self.display_id
204 .clone()
205 .unwrap_or_else(|| format!("service/{}", self.id)),
206 None,
207 || async {
208 let launched_host = self.launched_host.get().unwrap();
209
210 let built = self.build().await?;
211 let args = self.args.clone().unwrap_or_default();
212
213 let binary = launched_host
214 .launch_binary(
215 self.display_id
216 .clone()
217 .unwrap_or_else(|| format!("service/{}", self.id)),
218 built,
219 &args,
220 self.tracing.clone(),
221 &self.env,
222 self.pin_to_core,
223 )
224 .await?;
225
226 let bind_config = self
227 .port_to_bind
228 .iter()
229 .map(|(port_name, bind_type)| {
230 (port_name.clone(), launched_host.server_config(bind_type))
231 })
232 .collect::<HashMap<_, _>>();
233
234 let formatted_bind_config = serde_json::to_string::<InitConfig<'_>>(&(
235 bind_config,
236 self.meta.get().map(|s| s.as_str().into()),
237 ))
238 .unwrap();
239
240 let stdout_receiver = binary.deploy_stdout();
242
243 binary.stdin().send(format!("{formatted_bind_config}\n"))?;
244
245 let ready_line = ProgressTracker::leaf(
246 "waiting for ready",
247 tokio::time::timeout(Duration::from_secs(60), stdout_receiver),
248 )
249 .await
250 .context("Timed out waiting for ready")?
251 .context("Program unexpectedly quit")?;
252 if let Some(line_rest) = ready_line.strip_prefix("ready: ") {
253 *self.server_defns.try_write().unwrap() =
254 serde_json::from_str(line_rest).unwrap();
255 } else {
256 bail!("expected ready");
257 }
258 Ok(binary)
259 },
260 )
261 })
262 .await?;
263 Ok(())
264 }
265
266 async fn start(&self) -> Result<()> {
267 self.started
268 .get_or_try_init(|| async {
269 let sink_ports_futures =
270 self.port_to_server
271 .iter()
272 .map(|(port_name, outgoing)| async {
273 (&**port_name, outgoing.load_instantiated(&|p| p).await)
274 });
275 let sink_ports = futures::future::join_all(sink_ports_futures)
276 .await
277 .into_iter()
278 .collect::<HashMap<_, _>>();
279
280 let formatted_defns = serde_json::to_string(&sink_ports).unwrap();
281
282 let stdout_receiver = self.launched_binary.get().unwrap().deploy_stdout();
283
284 self.launched_binary
285 .get()
286 .unwrap()
287 .stdin()
288 .send(format!("start: {formatted_defns}\n"))
289 .unwrap();
290
291 let start_ack_line = ProgressTracker::leaf(
292 format!(
293 "{} / waiting for ack start",
294 self.display_id
295 .clone()
296 .unwrap_or_else(|| format!("service/{}", self.id))
297 ),
298 tokio::time::timeout(Duration::from_secs(60), stdout_receiver),
299 )
300 .await??;
301 if !start_ack_line.starts_with("ack start") {
302 bail!("expected ack start");
303 }
304
305 Ok(())
306 })
307 .await?;
308
309 Ok(())
310 }
311
312 async fn stop(&self) -> Result<()> {
313 ProgressTracker::with_group(
314 self.display_id
315 .clone()
316 .unwrap_or_else(|| format!("service/{}", self.id)),
317 None,
318 || async {
319 let launched_binary = self.launched_binary.get().unwrap();
320 launched_binary.stdin().send("stop\n".to_owned())?;
321
322 let timeout_result = ProgressTracker::leaf(
323 "waiting for exit",
324 tokio::time::timeout(Duration::from_secs(60), launched_binary.wait()),
325 )
326 .await;
327 match timeout_result {
328 Err(_timeout) => {} Ok(Err(unexpected_error)) => return Err(unexpected_error), Ok(Ok(_exit_status)) => {}
331 }
332 launched_binary.stop().await?;
333
334 Ok(())
335 },
336 )
337 .await
338 }
339}