44
55use std:: sync:: Arc ;
66
7+ use anyhow:: Context ;
8+ use tokio:: net:: TcpListener ;
79use tracing:: info;
810
911use crate :: ServerConfig ;
@@ -16,9 +18,14 @@ use crate::control::shutdown::ShutdownBus;
1618use crate :: control:: startup:: StartupGate ;
1719use crate :: control:: state:: SharedState ;
1820
19- /// The three protocol listeners passed to [`spawn_protocol_listeners`].
21+ /// The pre-bound protocol listeners passed to [`spawn_protocol_listeners`].
22+ ///
23+ /// Every socket here is already bound by [`bind_listeners`], so spawning
24+ /// cannot fail on a port conflict.
2025pub struct ProtocolListeners {
2126 pub pg_listener : PgListener ,
27+ pub http_listener : TcpListener ,
28+ pub sync_listener : TcpListener ,
2229 pub ilp_listener : Option < IlpListener > ,
2330 pub resp_listener : Option < RespListener > ,
2431}
@@ -34,6 +41,10 @@ pub struct ListenerInfra {
3441///
3542/// The native listener is not spawned here — it is run on the main task
3643/// by the caller after this returns.
44+ ///
45+ /// Infallible by construction: every socket was bound by [`bind_listeners`]
46+ /// before this point, so a port conflict has already aborted boot while
47+ /// nothing was exposed. Nothing here may silently swallow a bind failure.
3748pub async fn spawn_protocol_listeners (
3849 listeners : ProtocolListeners ,
3950 shared : Arc < SharedState > ,
@@ -44,6 +55,8 @@ pub async fn spawn_protocol_listeners(
4455) {
4556 let ProtocolListeners {
4657 pg_listener,
58+ http_listener,
59+ sync_listener,
4760 ilp_listener,
4861 resp_listener,
4962 } = listeners;
@@ -84,10 +97,9 @@ pub async fn spawn_protocol_listeners(
8497 }
8598 } ) ;
8699
87- // HTTP API server.
100+ // HTTP API server (on the socket bound by `bind_listeners`) .
88101 let shared_http = Arc :: clone ( & shared) ;
89102 let http_auth_mode = config. auth . mode . clone ( ) ;
90- let http_listen = config. http_addr ( ) ;
91103 let http_tls = if http_tls_enabled {
92104 config. server . tls . clone ( )
93105 } else {
@@ -96,7 +108,7 @@ pub async fn spawn_protocol_listeners(
96108 let bus_http = shutdown_bus. clone ( ) ;
97109 tokio:: spawn ( async move {
98110 if let Err ( e) = crate :: control:: server:: http:: server:: run (
99- http_listen ,
111+ http_listener ,
100112 shared_http,
101113 http_auth_mode,
102114 http_tls. as_ref ( ) ,
@@ -156,25 +168,22 @@ pub async fn spawn_protocol_listeners(
156168 } ) ;
157169 }
158170
159- // Sync WebSocket listener for NodeDB-Lite clients.
160- let sync_config = crate :: control:: server:: sync:: listener:: SyncListenerConfig :: default ( ) ;
161- match crate :: control:: server:: sync:: listener:: start_sync_listener (
171+ // Sync WebSocket listener for NodeDB-Lite clients (socket already bound).
172+ let sync_config = crate :: control:: server:: sync:: listener:: SyncListenerConfig {
173+ listen_addr : config. sync_addr ( ) ,
174+ ..Default :: default ( )
175+ } ;
176+ let sync_state = crate :: control:: server:: sync:: listener:: serve_sync_listener (
177+ sync_listener,
162178 sync_config,
163179 Some ( Arc :: clone ( & shared) ) ,
164180 )
165- . await
166- {
167- Ok ( sync_state) => {
168- info ! (
169- addr = %sync_state. config. listen_addr,
170- max_sessions = sync_state. config. max_sessions,
171- "sync WebSocket listener started"
172- ) ;
173- }
174- Err ( e) => {
175- tracing:: warn!( error = %e, "sync listener failed to start (non-fatal)" ) ;
176- }
177- }
181+ . await ;
182+ info ! (
183+ addr = %sync_state. config. listen_addr,
184+ max_sessions = sync_state. config. max_sessions,
185+ "sync WebSocket listener started"
186+ ) ;
178187
179188 // Signal readiness to systemd and cluster lifecycle.
180189 if let Some ( handle) = cluster_handle {
@@ -184,18 +193,34 @@ pub async fn spawn_protocol_listeners(
184193 nodedb_cluster:: readiness:: notify_ready ( ) ;
185194}
186195
196+ /// Every protocol socket, bound before any accept loop starts.
197+ pub struct BoundListeners {
198+ pub native : Listener ,
199+ pub pgwire : PgListener ,
200+ pub http : TcpListener ,
201+ pub sync : TcpListener ,
202+ pub ilp : Option < IlpListener > ,
203+ pub resp : Option < RespListener > ,
204+ }
205+
187206/// Bind all protocol listeners to their configured addresses.
188- pub async fn bind_listeners (
189- config : & ServerConfig ,
190- ) -> anyhow :: Result < (
191- Listener ,
192- PgListener ,
193- Option < IlpListener > ,
194- Option < RespListener > ,
195- ) > {
207+ ///
208+ /// This is the single fail-fast point for listener setup: it runs before the
209+ /// node waits on cluster readiness and before any accept loop is spawned, so
210+ /// a port conflict on *any* protocol — including HTTP and sync, which serve
211+ /// from detached tasks — aborts boot while nothing is exposed yet. Never
212+ /// move a bind out of here into a spawned task; that is how a server ends up
213+ /// running for days missing a listener behind one warning line.
214+ pub async fn bind_listeners ( config : & ServerConfig ) -> anyhow :: Result < BoundListeners > {
196215 let native = crate :: control:: server:: listener:: Listener :: bind ( config. native_addr ( ) ) . await ?;
197216 let pgwire =
198217 crate :: control:: server:: pgwire:: listener:: PgListener :: bind ( config. pgwire_addr ( ) ) . await ?;
218+ let http = TcpListener :: bind ( config. http_addr ( ) )
219+ . await
220+ . with_context ( || format ! ( "bind HTTP API listener to {}" , config. http_addr( ) ) ) ?;
221+ let sync = crate :: control:: server:: sync:: listener:: bind_sync_listener ( config. sync_addr ( ) )
222+ . await
223+ . context ( "sync listener failed to bind" ) ?;
199224 let ilp = if let Some ( ilp_addr) = config. ilp_addr ( ) {
200225 Some ( crate :: control:: server:: ilp_listener:: IlpListener :: bind ( ilp_addr) . await ?)
201226 } else {
@@ -206,5 +231,12 @@ pub async fn bind_listeners(
206231 } else {
207232 None
208233 } ;
209- Ok ( ( native, pgwire, ilp, resp) )
234+ Ok ( BoundListeners {
235+ native,
236+ pgwire,
237+ http,
238+ sync,
239+ ilp,
240+ resp,
241+ } )
210242}
0 commit comments