Skip to main content

astraea_server/
server.rs

1use std::sync::Arc;
2use std::time::Instant;
3
4use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
5use tokio::net::TcpListener;
6use tokio_rustls::TlsAcceptor;
7use tracing::{debug, error, info, warn};
8
9use crate::auth::AuthManager;
10use crate::connection::{ConnectionConfig, ConnectionManager};
11use crate::handler::RequestHandler;
12use crate::metrics::ServerMetrics;
13use crate::protocol::{Request, Response};
14use crate::tls::{TlsConfig, extract_client_cn};
15
16/// Configuration for the AstraeaDB server.
17#[derive(Debug, Clone)]
18pub struct ServerConfig {
19    pub bind_address: String,
20    pub port: u16,
21    pub connection: ConnectionConfig,
22    /// Optional TLS configuration. When set, enables TLS/mTLS.
23    pub tls: Option<TlsConfig>,
24}
25
26impl Default for ServerConfig {
27    fn default() -> Self {
28        Self {
29            bind_address: "127.0.0.1".into(),
30            port: 7687,
31            connection: ConnectionConfig::default(),
32            tls: None,
33        }
34    }
35}
36
37/// TCP server that accepts newline-delimited JSON requests.
38///
39/// Protocol: each request is a single JSON line, each response is a single JSON line.
40/// Supports connection limits, request timeouts, idle timeouts, metrics, auth, TLS/mTLS, and graceful shutdown.
41pub struct AstraeaServer {
42    config: ServerConfig,
43    handler: Arc<RequestHandler>,
44    auth: Arc<AuthManager>,
45    metrics: Arc<ServerMetrics>,
46    connection_manager: Arc<ConnectionManager>,
47    tls_acceptor: Option<TlsAcceptor>,
48}
49
50impl AstraeaServer {
51    /// Create a new server. If TLS is configured, this will load the certificates.
52    ///
53    /// # Errors
54    /// Returns an error if TLS is configured but certificates cannot be loaded.
55    pub fn new(
56        config: ServerConfig,
57        handler: RequestHandler,
58    ) -> Result<Self, crate::tls::TlsError> {
59        let connection_manager = Arc::new(ConnectionManager::new(config.connection.clone()));
60
61        // Build TLS acceptor if TLS is configured
62        let tls_acceptor = if let Some(ref tls_config) = config.tls {
63            info!("TLS enabled, loading certificates...");
64            let acceptor = tls_config.build_acceptor()?;
65            info!(
66                "TLS configured: cert={}, require_client_cert={}",
67                tls_config.cert_path.display(),
68                tls_config.require_client_cert
69            );
70            Some(acceptor)
71        } else {
72            None
73        };
74
75        Ok(Self {
76            config,
77            handler: Arc::new(handler),
78            auth: Arc::new(AuthManager::disabled()),
79            metrics: Arc::new(ServerMetrics::new()),
80            connection_manager,
81            tls_acceptor,
82        })
83    }
84
85    /// Create a new server without TLS validation at construction time.
86    /// Use this for testing or when you want to defer TLS setup.
87    pub fn new_without_tls(config: ServerConfig, handler: RequestHandler) -> Self {
88        let connection_manager = Arc::new(ConnectionManager::new(config.connection.clone()));
89        Self {
90            config,
91            handler: Arc::new(handler),
92            auth: Arc::new(AuthManager::disabled()),
93            metrics: Arc::new(ServerMetrics::new()),
94            connection_manager,
95            tls_acceptor: None,
96        }
97    }
98
99    /// Create a server with authentication enabled.
100    pub fn with_auth(mut self, auth: AuthManager) -> Self {
101        self.auth = Arc::new(auth);
102        self
103    }
104
105    /// Get a reference to the metrics collector.
106    pub fn metrics(&self) -> &Arc<ServerMetrics> {
107        &self.metrics
108    }
109
110    /// Get a reference to the connection manager (for external shutdown).
111    pub fn connection_manager(&self) -> &Arc<ConnectionManager> {
112        &self.connection_manager
113    }
114
115    /// Run the server, accepting connections until shutdown is initiated.
116    pub async fn run(&self) -> std::io::Result<()> {
117        let addr = format!("{}:{}", self.config.bind_address, self.config.port);
118        let listener = TcpListener::bind(&addr).await?;
119
120        if self.tls_acceptor.is_some() {
121            info!("AstraeaDB server listening on {} (TLS enabled)", addr);
122        } else {
123            info!("AstraeaDB server listening on {} (plaintext)", addr);
124        }
125
126        loop {
127            // Check for shutdown.
128            if self.connection_manager.is_shutting_down() {
129                info!("Server shutting down, stopping accept loop");
130                break;
131            }
132
133            // Accept with a timeout so we can check shutdown periodically.
134            let accept_result =
135                tokio::time::timeout(std::time::Duration::from_secs(1), listener.accept()).await;
136
137            let (stream, peer_addr) = match accept_result {
138                Ok(Ok(conn)) => conn,
139                Ok(Err(e)) => {
140                    error!("Accept error: {}", e);
141                    continue;
142                }
143                Err(_) => continue, // timeout, loop to check shutdown
144            };
145
146            // Check connection limits.
147            let guard = match self.connection_manager.try_accept() {
148                Some(g) => g,
149                None => {
150                    warn!("Connection limit reached, rejecting {}", peer_addr);
151                    // Send rejection and close.
152                    let mut stream = stream;
153                    let msg = r#"{"status":"error","message":"server connection limit reached"}"#;
154                    let _ = tokio::io::AsyncWriteExt::write_all(
155                        &mut stream,
156                        format!("{}\n", msg).as_bytes(),
157                    )
158                    .await;
159                    continue;
160                }
161            };
162
163            self.metrics.connection_opened();
164            info!("New connection from {}", peer_addr);
165
166            let handler = Arc::clone(&self.handler);
167            let auth = Arc::clone(&self.auth);
168            let metrics = Arc::clone(&self.metrics);
169            let idle_timeout = self.connection_manager.idle_timeout();
170            let request_timeout = self.connection_manager.request_timeout();
171            let tls_acceptor = self.tls_acceptor.clone();
172
173            tokio::spawn(async move {
174                let result = if let Some(acceptor) = tls_acceptor {
175                    // TLS connection
176                    match acceptor.accept(stream).await {
177                        Ok(tls_stream) => {
178                            // Extract client certificate CN if available
179                            let client_cn = tls_stream
180                                .get_ref()
181                                .1
182                                .peer_certificates()
183                                .and_then(|certs| extract_client_cn(certs));
184
185                            if let Some(ref cn) = client_cn {
186                                debug!("TLS client authenticated: CN={}", cn);
187                            }
188
189                            handle_connection(
190                                tls_stream,
191                                handler,
192                                auth,
193                                metrics.clone(),
194                                idle_timeout,
195                                request_timeout,
196                                client_cn,
197                            )
198                            .await
199                        }
200                        Err(e) => {
201                            error!("TLS handshake error from {}: {}", peer_addr, e);
202                            Err(std::io::Error::other(e))
203                        }
204                    }
205                } else {
206                    // Plain TCP connection
207                    handle_connection(
208                        stream,
209                        handler,
210                        auth,
211                        metrics.clone(),
212                        idle_timeout,
213                        request_timeout,
214                        None,
215                    )
216                    .await
217                };
218
219                if let Err(e) = result {
220                    error!("Connection error from {}: {}", peer_addr, e);
221                }
222                metrics.connection_closed();
223                info!("Connection closed: {}", peer_addr);
224                drop(guard); // explicitly release the connection slot
225            });
226        }
227
228        // Graceful shutdown: wait for in-flight connections.
229        info!("Waiting for in-flight connections to drain...");
230        self.connection_manager.wait_for_drain().await;
231        info!("All connections drained. Server stopped.");
232
233        Ok(())
234    }
235
236    /// Check if TLS is enabled for this server.
237    pub fn is_tls_enabled(&self) -> bool {
238        self.tls_acceptor.is_some()
239    }
240}
241
242async fn handle_connection<S>(
243    stream: S,
244    handler: Arc<RequestHandler>,
245    auth: Arc<AuthManager>,
246    metrics: Arc<ServerMetrics>,
247    idle_timeout: std::time::Duration,
248    request_timeout: std::time::Duration,
249    _client_cn: Option<String>,
250) -> std::io::Result<()>
251where
252    S: AsyncRead + AsyncWrite + Unpin,
253{
254    let (reader, mut writer) = tokio::io::split(stream);
255    let mut reader = BufReader::new(reader);
256    let mut line = String::new();
257
258    loop {
259        line.clear();
260
261        // Apply idle timeout on reading.
262        let read_result = tokio::time::timeout(idle_timeout, reader.read_line(&mut line)).await;
263
264        let bytes_read = match read_result {
265            Ok(Ok(n)) => n,
266            Ok(Err(e)) => return Err(e),
267            Err(_) => {
268                // Idle timeout expired.
269                let msg = r#"{"status":"error","message":"idle timeout"}"#;
270                let _ = writer.write_all(format!("{}\n", msg).as_bytes()).await;
271                return Ok(());
272            }
273        };
274
275        if bytes_read == 0 {
276            break; // client disconnected
277        }
278
279        let trimmed = line.trim();
280        if trimmed.is_empty() {
281            continue;
282        }
283
284        let start = Instant::now();
285
286        // Parse the request.
287        let request = match serde_json::from_str::<Request>(trimmed) {
288            Ok(req) => req,
289            Err(e) => {
290                let response = Response::error(format!("invalid request: {e}"));
291                let mut response_json = serde_json::to_string(&response).unwrap_or_else(|_| {
292                    r#"{"status":"error","message":"serialization failed"}"#.into()
293                });
294                response_json.push('\n');
295                writer.write_all(response_json.as_bytes()).await?;
296                continue;
297            }
298        };
299
300        let request_type = request_type_name(&request);
301
302        // Authentication check.
303        if auth.is_enabled() {
304            // Extract auth_token from the raw JSON (simple approach).
305            let auth_token = extract_auth_token(trimmed);
306            match auth_token {
307                Some(token) => {
308                    if let Some(role) = auth.authenticate(token) {
309                        if !AuthManager::authorize(role, request_type) {
310                            auth.audit(token, role, request_type, false);
311                            metrics.record_request(request_type);
312                            metrics.record_error(request_type);
313                            let response = Response::error(format!(
314                                "access denied: role '{}' cannot perform '{}'",
315                                role, request_type
316                            ));
317                            let mut rj = serde_json::to_string(&response).unwrap_or_default();
318                            rj.push('\n');
319                            writer.write_all(rj.as_bytes()).await?;
320                            continue;
321                        }
322                        auth.audit(token, role, request_type, true);
323                    } else {
324                        metrics.record_request(request_type);
325                        metrics.record_error(request_type);
326                        let response = Response::error("invalid credentials");
327                        let mut rj = serde_json::to_string(&response).unwrap_or_default();
328                        rj.push('\n');
329                        writer.write_all(rj.as_bytes()).await?;
330                        continue;
331                    }
332                }
333                None => {
334                    metrics.record_request(request_type);
335                    metrics.record_error(request_type);
336                    let response = Response::error("authentication required: provide auth_token");
337                    let mut rj = serde_json::to_string(&response).unwrap_or_default();
338                    rj.push('\n');
339                    writer.write_all(rj.as_bytes()).await?;
340                    continue;
341                }
342            }
343        }
344
345        metrics.record_request(request_type);
346
347        // Execute with request timeout.
348        let handler_ref = Arc::clone(&handler);
349        let response =
350            match tokio::time::timeout(request_timeout, async move { handler_ref.handle(request) })
351                .await
352            {
353                Ok(resp) => resp,
354                Err(_) => {
355                    metrics.record_error(request_type);
356                    Response::error("request timeout")
357                }
358            };
359
360        let duration = start.elapsed();
361        metrics.record_duration(request_type, duration);
362
363        if matches!(response, Response::Error { .. }) {
364            metrics.record_error(request_type);
365        }
366
367        let mut response_json = serde_json::to_string(&response)
368            .unwrap_or_else(|_| r#"{"status":"error","message":"serialization failed"}"#.into());
369        response_json.push('\n');
370        writer.write_all(response_json.as_bytes()).await?;
371    }
372
373    Ok(())
374}
375
376/// Extract the request type name for metrics/auth.
377fn request_type_name(request: &Request) -> &'static str {
378    match request {
379        Request::CreateNode { .. } => "CreateNode",
380        Request::CreateEdge { .. } => "CreateEdge",
381        Request::GetNode { .. } => "GetNode",
382        Request::GetEdge { .. } => "GetEdge",
383        Request::UpdateNode { .. } => "UpdateNode",
384        Request::UpdateEdge { .. } => "UpdateEdge",
385        Request::DeleteNode { .. } => "DeleteNode",
386        Request::DeleteEdge { .. } => "DeleteEdge",
387        Request::Neighbors { .. } => "Neighbors",
388        Request::NeighborsAt { .. } => "NeighborsAt",
389        Request::Bfs { .. } => "Bfs",
390        Request::BfsAt { .. } => "BfsAt",
391        Request::ShortestPath { .. } => "ShortestPath",
392        Request::ShortestPathAt { .. } => "ShortestPathAt",
393        Request::VectorSearch { .. } => "VectorSearch",
394        Request::HybridSearch { .. } => "HybridSearch",
395        Request::SemanticNeighbors { .. } => "SemanticNeighbors",
396        Request::SemanticWalk { .. } => "SemanticWalk",
397        Request::Query { .. } => "Query",
398        Request::ExtractSubgraph { .. } => "ExtractSubgraph",
399        Request::GraphRag { .. } => "GraphRag",
400        Request::Dfs { .. } => "Dfs",
401        Request::DfsAt { .. } => "DfsAt",
402        Request::FindByLabel { .. } => "FindByLabel",
403        Request::DeleteByLabel { .. } => "DeleteByLabel",
404        Request::FindEdgeByType { .. } => "FindEdgeByType",
405        Request::RunPageRank { .. } => "RunPageRank",
406        Request::RunLouvain { .. } => "RunLouvain",
407        Request::RunConnectedComponents { .. } => "RunConnectedComponents",
408        Request::RunDegreeCentrality { .. } => "RunDegreeCentrality",
409        Request::RunBetweennessCentrality { .. } => "RunBetweennessCentrality",
410        Request::GraphStats => "GraphStats",
411        Request::GetSubgraph { .. } => "GetSubgraph",
412        Request::Ping => "Ping",
413    }
414}
415
416/// Extract auth_token from a raw JSON request string.
417/// Looks for "auth_token":"<value>" in the JSON.
418fn extract_auth_token(json: &str) -> Option<&str> {
419    let marker = "\"auth_token\":\"";
420    if let Some(start) = json.find(marker) {
421        let rest = &json[start + marker.len()..];
422        if let Some(end) = rest.find('"') {
423            return Some(&rest[..end]);
424        }
425    }
426    None
427}
428
429#[cfg(test)]
430mod tests {
431    use super::*;
432
433    #[test]
434    fn default_config() {
435        let config = ServerConfig::default();
436        assert_eq!(config.bind_address, "127.0.0.1");
437        assert_eq!(config.port, 7687);
438        assert_eq!(config.connection.max_connections, 1024);
439    }
440
441    #[test]
442    fn request_type_name_matches() {
443        let req = Request::Ping;
444        assert_eq!(request_type_name(&req), "Ping");
445
446        let req = Request::CreateNode {
447            labels: vec![],
448            properties: serde_json::json!({}),
449            embedding: None,
450        };
451        assert_eq!(request_type_name(&req), "CreateNode");
452    }
453
454    #[test]
455    fn extract_auth_token_works() {
456        let json = r#"{"type":"Ping","auth_token":"my-secret-key"}"#;
457        assert_eq!(extract_auth_token(json), Some("my-secret-key"));
458    }
459
460    #[test]
461    fn extract_auth_token_missing() {
462        let json = r#"{"type":"Ping"}"#;
463        assert_eq!(extract_auth_token(json), None);
464    }
465
466    #[test]
467    fn temporal_request_types() {
468        let req = Request::NeighborsAt {
469            id: 1,
470            direction: "outgoing".into(),
471            timestamp: 100,
472            edge_type: None,
473        };
474        assert_eq!(request_type_name(&req), "NeighborsAt");
475
476        let req = Request::BfsAt {
477            start: 1,
478            max_depth: 3,
479            timestamp: 100,
480        };
481        assert_eq!(request_type_name(&req), "BfsAt");
482
483        let req = Request::ShortestPathAt {
484            from: 1,
485            to: 2,
486            timestamp: 100,
487            weighted: false,
488        };
489        assert_eq!(request_type_name(&req), "ShortestPathAt");
490    }
491
492    // -----------------------------------------------------------------
493    // astraeadb-issues.md #6 (VectorSearch `distance` vs proto `score`):
494    // end-to-end regression covering BOTH transports against a single
495    // shared handler/graph, driving the real wire-framing code
496    // (`handle_connection`, over an in-memory `tokio::io::duplex` pipe
497    // standing in for a TCP socket) and the real gRPC service impl.
498    // -----------------------------------------------------------------
499    #[tokio::test]
500    async fn tcp_and_grpc_vector_search_return_matching_distance() {
501        use astraea_core::traits::VectorIndex;
502        use astraea_core::types::{DistanceMetric, NodeId};
503        use astraea_graph::Graph;
504        use astraea_graph::test_utils::InMemoryStorage;
505        use astraea_vector::HnswVectorIndex;
506        use tokio::io::AsyncWriteExt;
507
508        use crate::grpc::AstraeaGrpcService;
509        use crate::grpc::proto::VectorSearchRequest;
510        use crate::grpc::proto::astraea_service_server::AstraeaService as _;
511
512        // One handler, one graph, one vector index -- shared by both
513        // transports so a divergence would only be explainable by the
514        // transport-specific mapping code, not by different data.
515        let storage = InMemoryStorage::new();
516        let vector_index: Arc<dyn VectorIndex> =
517            Arc::new(HnswVectorIndex::new(3, DistanceMetric::Cosine));
518        let graph = Graph::with_vector_index(Box::new(storage), Arc::clone(&vector_index));
519        vector_index.insert(NodeId(1), &[1.0, 0.0, 0.0]).unwrap();
520        vector_index.insert(NodeId(2), &[0.0, 1.0, 0.0]).unwrap();
521        let handler = Arc::new(RequestHandler::new(Arc::new(graph), Some(vector_index)));
522
523        // -- TCP surface: real newline-delimited JSON framing over an
524        // in-memory duplex pipe (stands in for a TCP socket; `handle_connection`
525        // is generic over `AsyncRead + AsyncWrite` so this exercises the
526        // exact same code the real TCP listener uses).
527        let (client, server_side) = tokio::io::duplex(4096);
528        let tcp_handler = Arc::clone(&handler);
529        let server_task = tokio::spawn(async move {
530            handle_connection(
531                server_side,
532                tcp_handler,
533                Arc::new(AuthManager::disabled()),
534                Arc::new(ServerMetrics::new()),
535                std::time::Duration::from_secs(5),
536                std::time::Duration::from_secs(5),
537                None,
538            )
539            .await
540        });
541
542        let (read_half, mut write_half) = tokio::io::split(client);
543        let query = r#"{"type":"VectorSearch","query":[1.0,0.0,0.0],"k":2}"#;
544        write_half
545            .write_all(format!("{query}\n").as_bytes())
546            .await
547            .unwrap();
548
549        let mut reader = BufReader::new(read_half);
550        let mut line = String::new();
551        reader.read_line(&mut line).await.unwrap();
552
553        // Close the write side so `handle_connection`'s read loop sees EOF
554        // and returns; then reap the spawned task.
555        drop(write_half);
556        server_task.await.unwrap().unwrap();
557
558        let tcp_resp: serde_json::Value = serde_json::from_str(line.trim()).unwrap();
559        assert_eq!(tcp_resp["status"], "ok", "TCP response: {tcp_resp}");
560        let tcp_results = tcp_resp["data"]["results"].as_array().unwrap();
561        assert_eq!(tcp_results.len(), 2);
562        assert_eq!(tcp_results[0]["node_id"].as_u64().unwrap(), 1);
563        // Canonical field name on the wire is `distance`.
564        let tcp_distance = tcp_results[0]["distance"]
565            .as_f64()
566            .expect("TCP JSON result must carry a `distance` field");
567        assert!(
568            tcp_distance.abs() < 1e-5,
569            "expected ~0 distance for an exact match, got {tcp_distance}"
570        );
571
572        // -- gRPC surface: drive the real tonic service impl directly
573        // (same handler/graph/vector index as above).
574        let svc = AstraeaGrpcService::new(Arc::clone(&handler));
575        let grpc_resp = svc
576            .vector_search(tonic::Request::new(VectorSearchRequest {
577                query: vec![1.0, 0.0, 0.0],
578                k: 2,
579            }))
580            .await
581            .unwrap()
582            .into_inner();
583        assert!(
584            grpc_resp.error.is_empty(),
585            "gRPC error: {}",
586            grpc_resp.error
587        );
588        assert_eq!(grpc_resp.results.len(), 2);
589        assert_eq!(grpc_resp.results[0].node_id, 1);
590        // `.distance` is the renamed proto field (astraeadb-issues.md #6);
591        // this line would not compile against the old `score` field name.
592        let grpc_distance = grpc_resp.results[0].distance as f64;
593
594        // Both transports must agree on the value under the shared
595        // `distance` name.
596        assert!(
597            (tcp_distance - grpc_distance).abs() < 1e-4,
598            "TCP distance {tcp_distance} != gRPC distance {grpc_distance}"
599        );
600    }
601}