Skip to content

Commit 3635bce

Browse files
authored
log and fail fast on Rocket bind/listen failures (bind target + nonzero exit) (#63)
# What Improve error handling and diagnostics for Rocket HTTP server launch failures. # Why Previously, Rocket launch errors (such as port bind failures) were silently ignored, making it difficult to diagnose server startup issues. This change ensures that launch failures are properly logged and propagated, causing the application to exit with a non-zero status code. # Risks `join_all` (wait for all futures to exit) was changed and now exits `lit-node` when a single rocket fails to launch and with nonzero exit, which could have downstream effects on systemd and other supervisors.
1 parent c697c92 commit 3635bce

2 files changed

Lines changed: 92 additions & 7 deletions

File tree

rust/lit-core/lit-api-core/src/http/rocket/engine.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use rocket::Error as RocketError;
1313
use rocket::async_main;
1414
use sd_notify::NotifyState;
1515
use tokio::runtime::Runtime;
16-
use tracing::warn;
16+
use tracing::{error, warn};
1717

1818
use crate::Event;
1919
use crate::http::rocket::launcher::{Launcher, Shutdown};
@@ -161,7 +161,13 @@ fn spawn_launcher(
161161
let mut launcher_join_handles = launcher_join_handles.lock().unwrap();
162162

163163
launcher_join_handles.push(thread::spawn(move || {
164-
let _ = async_main(launcher.launch());
164+
let res = async_main(launcher.launch());
165+
if let Err(e) = res {
166+
// A Rocket launch error (commonly port bind/listen failure) is a failure condition
167+
// exit with log and nonzero exit code to distinguish from normal shutdown
168+
error!(error = ?e, "rocket engine - launcher exited with error (fatal)");
169+
panic!("rocket engine - launcher exited with error (fatal): {e:?}");
170+
}
165171
}));
166172

167173
Ok(())

rust/lit-core/lit-api-core/src/http/rocket/launcher.rs

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@ use std::collections::HashMap;
22
use std::fmt;
33
use std::result::Result as StdResult;
44

5-
use futures::future::{BoxFuture, join_all};
5+
use futures::future::BoxFuture;
6+
use futures::stream::{FuturesUnordered, StreamExt};
67
use rocket::catcher::Handler;
78
use rocket::fairing::Fairing;
89
use rocket::http::Status;
910
use rocket::http::uri::Origin;
1011
use rocket::response::{Redirect, Responder, status};
1112
use rocket::serde::json::Value;
1213
use rocket::{Build, Catcher, Error as RocketError, Ignite, Request, Rocket, Route, catcher};
14+
use tracing::{error, info};
1315

1416
use tokio::sync::mpsc;
1517
use tokio::task_local;
@@ -35,6 +37,53 @@ task_local! {
3537
pub static CONFIG: ReloadableLitConfig;
3638
}
3739

40+
#[derive(Debug, Clone)]
41+
struct RocketTarget {
42+
address: String,
43+
port: u16,
44+
tls_enabled: bool,
45+
role: &'static str,
46+
}
47+
48+
#[derive(Debug, Clone)]
49+
struct RocketTargets(Vec<RocketTarget>);
50+
51+
impl From<&[Rocket<Ignite>]> for RocketTargets {
52+
fn from(ignited: &[Rocket<Ignite>]) -> Self {
53+
Self(
54+
ignited
55+
.iter()
56+
.enumerate()
57+
.map(|(idx, r)| {
58+
let cfg = r.config();
59+
RocketTarget {
60+
address: cfg.address.to_string(),
61+
port: cfg.port,
62+
tls_enabled: cfg.tls.is_some(),
63+
role: if idx == 0 { "primary" } else { "aux" },
64+
}
65+
})
66+
.collect(),
67+
)
68+
}
69+
}
70+
71+
impl RocketTargets {
72+
fn log(&self) {
73+
for (idx, t) in self.0.iter().enumerate() {
74+
let proto = if t.tls_enabled { "https" } else { "http" };
75+
info!(
76+
rocket_index = idx,
77+
proto,
78+
role = t.role,
79+
address = %t.address,
80+
port = t.port,
81+
"rocket launch starting"
82+
);
83+
}
84+
}
85+
}
86+
3887
pub struct Launcher {
3988
cfg: ReloadableLitConfig,
4089
rocket: Option<Rocket<Build>>,
@@ -154,15 +203,45 @@ impl Launcher {
154203

155204
pub async fn launch(&mut self) -> StdResult<(), RocketError> {
156205
if self.ignited.is_empty() {
206+
error!("rocket launcher - launch called before ignite (no ignited rockets)");
157207
panic!("ignite must be called prior to launch");
158208
}
159209

160-
let mut futures = Vec::new();
161-
while !self.ignited.is_empty() {
162-
futures.push(self.ignited.remove(0).launch());
210+
// Extra diagnostics: log the configured bind targets and surface bind/listen failures
211+
let targets = RocketTargets::from(self.ignited.as_slice());
212+
targets.log();
213+
214+
// FuturesUnordered so we can fail fast on the first launch error (irrespective of other launches)
215+
let mut futures: FuturesUnordered<_> = FuturesUnordered::new();
216+
for (idx, rocket) in self.ignited.drain(..).enumerate() {
217+
futures.push(async move { (idx, rocket.launch().await) });
163218
}
164219

165-
join_all(futures).await;
220+
// Each `launch()` future will typically run indefinitely while the server is up.
221+
// We await launch results as they complete and fail fast on the first error.
222+
while let Some((idx, res)) = futures.next().await {
223+
if let Err(e) = res {
224+
let t = targets.0.get(idx).cloned().unwrap_or(RocketTarget {
225+
address: "<unknown>".to_string(),
226+
port: 0,
227+
tls_enabled: false,
228+
role: "unknown",
229+
});
230+
let proto = if t.tls_enabled { "https" } else { "http" };
231+
232+
error!(
233+
rocket_index = idx,
234+
proto,
235+
role = t.role,
236+
address = %t.address,
237+
port = t.port,
238+
error = ?e,
239+
"rocket launch failed (likely bind/listen failure for configured address/port)"
240+
);
241+
242+
return Err(e);
243+
}
244+
}
166245

167246
Ok(())
168247
}

0 commit comments

Comments
 (0)