55use std:: sync:: Arc ;
66
77use anyhow:: Context ;
8+ use tokio:: net:: TcpListener ;
89use tracing:: info;
910
1011use crate :: ServerConfig ;
@@ -17,9 +18,14 @@ use crate::control::shutdown::ShutdownBus;
1718use crate :: control:: startup:: StartupGate ;
1819use crate :: control:: state:: SharedState ;
1920
20- /// 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.
2125pub struct ProtocolListeners {
2226 pub pg_listener : PgListener ,
27+ pub http_listener : TcpListener ,
28+ pub sync_listener : TcpListener ,
2329 pub ilp_listener : Option < IlpListener > ,
2430 pub resp_listener : Option < RespListener > ,
2531}
@@ -36,20 +42,21 @@ pub struct ListenerInfra {
3642/// The native listener is not spawned here — it is run on the main task
3743/// by the caller after this returns.
3844///
39- /// Returns `Err` if the sync WebSocket listener fails to bind — that
40- /// failure is boot-fatal rather than a silent warning, since a server
41- /// that comes up without a reachable sync listener leaves NodeDB-Lite
42- /// clients unable to connect with no indication anything is wrong.
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.
4348pub async fn spawn_protocol_listeners (
4449 listeners : ProtocolListeners ,
4550 shared : Arc < SharedState > ,
4651 config : & ServerConfig ,
4752 infra : ListenerInfra ,
4853 base_acceptor : Option < tokio_rustls:: TlsAcceptor > ,
4954 cluster_handle : & Option < Arc < ClusterHandle > > ,
50- ) -> anyhow :: Result < ( ) > {
55+ ) {
5156 let ProtocolListeners {
5257 pg_listener,
58+ http_listener,
59+ sync_listener,
5360 ilp_listener,
5461 resp_listener,
5562 } = listeners;
@@ -90,10 +97,9 @@ pub async fn spawn_protocol_listeners(
9097 }
9198 } ) ;
9299
93- // HTTP API server.
100+ // HTTP API server (on the socket bound by `bind_listeners`) .
94101 let shared_http = Arc :: clone ( & shared) ;
95102 let http_auth_mode = config. auth . mode . clone ( ) ;
96- let http_listen = config. http_addr ( ) ;
97103 let http_tls = if http_tls_enabled {
98104 config. server . tls . clone ( )
99105 } else {
@@ -102,7 +108,7 @@ pub async fn spawn_protocol_listeners(
102108 let bus_http = shutdown_bus. clone ( ) ;
103109 tokio:: spawn ( async move {
104110 if let Err ( e) = crate :: control:: server:: http:: server:: run (
105- http_listen ,
111+ http_listener ,
106112 shared_http,
107113 http_auth_mode,
108114 http_tls. as_ref ( ) ,
@@ -162,51 +168,59 @@ pub async fn spawn_protocol_listeners(
162168 } ) ;
163169 }
164170
165- // Sync WebSocket listener for NodeDB-Lite clients.
171+ // Sync WebSocket listener for NodeDB-Lite clients (socket already bound) .
166172 let sync_config = crate :: control:: server:: sync:: listener:: SyncListenerConfig {
167173 listen_addr : config. sync_addr ( ) ,
168174 ..Default :: default ( )
169175 } ;
170- match crate :: control:: server:: sync:: listener:: start_sync_listener (
176+ let sync_state = crate :: control:: server:: sync:: listener:: serve_sync_listener (
177+ sync_listener,
171178 sync_config,
172179 Some ( Arc :: clone ( & shared) ) ,
173180 )
174- . await
175- {
176- Ok ( sync_state) => {
177- info ! (
178- addr = %sync_state. config. listen_addr,
179- max_sessions = sync_state. config. max_sessions,
180- "sync WebSocket listener started"
181- ) ;
182- }
183- Err ( e) => {
184- return Err ( e) . context ( "sync listener failed to bind" ) ;
185- }
186- }
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+ ) ;
187187
188188 // Signal readiness to systemd and cluster lifecycle.
189189 if let Some ( handle) = cluster_handle {
190190 let nodes = handle. topology . read ( ) . map ( |t| t. node_count ( ) ) . unwrap_or ( 1 ) ;
191191 handle. lifecycle . to_ready ( nodes) ;
192192 }
193193 nodedb_cluster:: readiness:: notify_ready ( ) ;
194+ }
194195
195- Ok ( ( ) )
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 > ,
196204}
197205
198206/// Bind all protocol listeners to their configured addresses.
199- pub async fn bind_listeners (
200- config : & ServerConfig ,
201- ) -> anyhow :: Result < (
202- Listener ,
203- PgListener ,
204- Option < IlpListener > ,
205- Option < RespListener > ,
206- ) > {
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 > {
207215 let native = crate :: control:: server:: listener:: Listener :: bind ( config. native_addr ( ) ) . await ?;
208216 let pgwire =
209217 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" ) ?;
210224 let ilp = if let Some ( ilp_addr) = config. ilp_addr ( ) {
211225 Some ( crate :: control:: server:: ilp_listener:: IlpListener :: bind ( ilp_addr) . await ?)
212226 } else {
@@ -217,5 +231,12 @@ pub async fn bind_listeners(
217231 } else {
218232 None
219233 } ;
220- Ok ( ( native, pgwire, ilp, resp) )
234+ Ok ( BoundListeners {
235+ native,
236+ pgwire,
237+ http,
238+ sync,
239+ ilp,
240+ resp,
241+ } )
221242}
0 commit comments