manabrew_server/
server.rs1use std::net::SocketAddr;
2use std::sync::Arc;
3use std::time::Duration;
4
5use axum::extract::State;
6use axum::{response::Json, routing::get, Router};
7use metrics_exporter_prometheus::PrometheusHandle;
8use serde_json::{json, Value};
9use socket2::{SockRef, TcpKeepalive};
10use tokio::net::TcpListener;
11use tokio::sync::Notify;
12use tokio_tungstenite::tungstenite::Message;
13use tracing::{debug, error, info, warn};
14
15use crate::cleanup::cleanup_loop;
16use crate::connection::handle_connection;
17use crate::protocol::ServerMessage;
18use crate::state::ServerState;
19
20const SHUTDOWN_GRACE: Duration = Duration::from_secs(10);
21const SHUTDOWN_RECONNECT_HINT_S: u32 = 5;
22
23pub async fn run_server(
24 state: Arc<ServerState>,
25 addr: SocketAddr,
26 health_addr: SocketAddr,
27 metrics_handle: PrometheusHandle,
28) {
29 let listener = TcpListener::bind(addr)
30 .await
31 .expect("Failed to bind to address");
32
33 let shutdown = Arc::new(Notify::new());
34 tokio::spawn(wait_for_shutdown_signal(shutdown.clone()));
35
36 serve(state, listener, health_addr, metrics_handle, shutdown).await;
37}
38
39pub async fn serve(
40 state: Arc<ServerState>,
41 listener: TcpListener,
42 health_addr: SocketAddr,
43 metrics_handle: PrometheusHandle,
44 shutdown: Arc<Notify>,
45) {
46 if let Ok(addr) = listener.local_addr() {
47 info!("[server] Forge Server listening on ws://{}", addr);
48 }
49 info!("[server] Server key: {}", mask_key(&state.server_key));
50 info!("[server] Max rooms: {}", state.max_rooms);
51
52 tokio::spawn(cleanup_loop(state.clone()));
53 tokio::spawn(run_health_listener(
54 state.clone(),
55 health_addr,
56 metrics_handle,
57 ));
58
59 let accept = accept_loop(state.clone(), listener, shutdown.clone());
60
61 tokio::select! {
62 _ = accept => {}
63 _ = shutdown.notified() => {
64 info!("[server] shutdown signal received — draining");
65 }
66 }
67
68 drain_and_exit(state).await;
69}
70
71async fn accept_loop(state: Arc<ServerState>, listener: TcpListener, shutdown: Arc<Notify>) {
72 loop {
73 tokio::select! {
74 res = listener.accept() => match res {
75 Ok((stream, peer_addr)) => {
76 debug!("[server] accepted connection from {}", peer_addr);
77 apply_tcp_socket_options(&stream, peer_addr);
78 let state = state.clone();
79 tokio::spawn(async move {
80 if let Err(e) = handle_connection(stream, peer_addr, state).await {
81 error!("[server] connection error: {}", e);
82 }
83 });
84 }
85 Err(e) => {
86 error!("[server] accept error: {}", e);
87 }
88 },
89 _ = shutdown.notified() => {
90 info!("[server] accept loop stopping");
91 return;
92 }
93 }
94 }
95}
96
97fn apply_tcp_socket_options(stream: &tokio::net::TcpStream, peer_addr: SocketAddr) {
98 if let Err(e) = stream.set_nodelay(true) {
99 warn!("[server] failed to disable Nagle on {}: {}", peer_addr, e);
100 }
101 let keepalive = TcpKeepalive::new()
102 .with_time(Duration::from_secs(30))
103 .with_interval(Duration::from_secs(10));
104 if let Err(e) = SockRef::from(stream).set_tcp_keepalive(&keepalive) {
105 warn!(
106 "[server] failed to enable TCP keepalive on {}: {}",
107 peer_addr, e
108 );
109 }
110}
111
112async fn wait_for_shutdown_signal(shutdown: Arc<Notify>) {
113 #[cfg(unix)]
114 {
115 use tokio::signal::unix::{signal, SignalKind};
116 let mut term = match signal(SignalKind::terminate()) {
117 Ok(s) => s,
118 Err(e) => {
119 warn!("[server] failed to install SIGTERM handler: {}", e);
120 return;
121 }
122 };
123 let mut int = match signal(SignalKind::interrupt()) {
124 Ok(s) => s,
125 Err(e) => {
126 warn!("[server] failed to install SIGINT handler: {}", e);
127 return;
128 }
129 };
130 tokio::select! {
131 _ = term.recv() => info!("[server] SIGTERM received"),
132 _ = int.recv() => info!("[server] SIGINT received"),
133 }
134 }
135 #[cfg(not(unix))]
136 {
137 if let Err(e) = tokio::signal::ctrl_c().await {
138 warn!("[server] ctrl_c handler error: {}", e);
139 return;
140 }
141 info!("[server] ctrl_c received");
142 }
143 shutdown.notify_waiters();
144}
145
146async fn drain_and_exit(state: Arc<ServerState>) {
147 let msg = ServerMessage::ServerShuttingDown {
148 reconnect_in_s: SHUTDOWN_RECONNECT_HINT_S,
149 };
150 let json = match serde_json::to_string(&msg) {
151 Ok(s) => s,
152 Err(e) => {
153 error!("[server] failed to serialize shutdown message: {}", e);
154 return;
155 }
156 };
157
158 let mut notified = 0usize;
159 for entry in state.players.iter() {
160 let player = entry.value();
161 if !player.connected {
162 continue;
163 }
164 if player.sender.send(Message::Text(json.clone())).is_ok() {
165 notified += 1;
166 }
167 }
168 info!(
169 "[server] broadcast ServerShuttingDown to {} players",
170 notified
171 );
172
173 tokio::time::sleep(SHUTDOWN_GRACE).await;
174 info!("[server] shutdown grace elapsed — exiting");
175}
176
177async fn run_health_listener(
178 state: Arc<ServerState>,
179 addr: SocketAddr,
180 metrics_handle: PrometheusHandle,
181) {
182 let app = Router::new()
183 .route("/health", get(health_handler))
184 .route("/metrics", get(metrics_handler))
185 .with_state((state, metrics_handle));
186
187 let listener = match TcpListener::bind(addr).await {
188 Ok(l) => l,
189 Err(e) => {
190 error!("[server] failed to bind health listener on {}: {}", addr, e);
191 return;
192 }
193 };
194 info!("[server] health endpoint on http://{}/health", addr);
195
196 if let Err(e) = axum::serve(listener, app).await {
197 error!("[server] health listener stopped: {}", e);
198 }
199}
200
201async fn health_handler(
202 State((state, _)): State<(Arc<ServerState>, PrometheusHandle)>,
203) -> Json<Value> {
204 let connected_players = state.players.iter().filter(|e| e.value().connected).count();
205 Json(json!({
206 "status": "ok",
207 "rooms": state.rooms.len(),
208 "connected_players": connected_players,
209 "uptime_s": uptime_secs(),
210 }))
211}
212
213async fn metrics_handler(
214 State((state, metrics_handle)): State<(Arc<ServerState>, PrometheusHandle)>,
215) -> String {
216 crate::metrics::refresh_gauges(&state);
217 metrics_handle.run_upkeep();
218 metrics_handle.render()
219}
220
221fn uptime_secs() -> u64 {
222 use std::sync::OnceLock;
223 use std::time::Instant;
224 static START: OnceLock<Instant> = OnceLock::new();
225 START.get_or_init(Instant::now).elapsed().as_secs()
226}
227
228fn mask_key(key: &str) -> String {
229 if key.len() <= 2 {
230 "*".repeat(key.len())
231 } else {
232 format!(
233 "{}{}{}",
234 &key[..1],
235 "*".repeat(key.len() - 2),
236 &key[key.len() - 1..]
237 )
238 }
239}