Skip to main content

amaters_net/
server_builder.rs

1//! Builder for [`crate::server::AqlServiceImpl`].
2//!
3//! Extracted from `server.rs` to keep that module under the workspace's
4//! per-file size policy.  All fields are private; configuration flows
5//! through fluent `with_*` methods (every `with_*` returns `Self` so calls
6//! chain).  Read-back accessors (`logging_verbosity()`, `metrics_addr()`,
7//! …) exist for [`crate::config::NetConfig::apply_to`] and tests.
8
9use std::sync::Arc;
10
11use amaters_core::traits::StorageEngine;
12
13use crate::error::NetResult;
14use crate::server::AqlServiceImpl;
15
16/// Server builder for creating AQL service instances.
17///
18/// Configuration is layered via fluent setters.  Defaults match
19/// `AqlServiceImpl::new(storage)` exactly; nothing is spawned until
20/// [`Self::build`] runs.
21pub struct AqlServerBuilder<S: StorageEngine> {
22    storage: Arc<S>,
23    /// Optional logging verbosity for the `LoggingLayer`.
24    logging_verbosity: Option<crate::logging_layer::LogVerbosity>,
25    /// Optional slow-request threshold (ms) for the `LoggingLayer`.
26    slow_threshold_ms: Option<u64>,
27    /// Optional address for the Prometheus metrics HTTP server.
28    metrics_addr: Option<std::net::SocketAddr>,
29    /// Optional gRPC bind address.  Recorded only — actual bind happens in
30    /// the caller's tonic `Server::bind` (out of this crate's scope).
31    bind_addr: Option<std::net::SocketAddr>,
32    /// Optional rate-limit QPS for callers that wire a `RateLimiter`.
33    rate_limit_qps: Option<f64>,
34    /// Optional path to a JWT secret for bearer-token auth.
35    jwt_secret_path: Option<std::path::PathBuf>,
36    /// Optional store of swappable rustls server config.  When `Some`, callers
37    /// build a [`crate::tls_acceptor::LiveTlsAcceptor`] from this store and
38    /// hand the resulting stream to tonic via `serve_with_incoming`.
39    #[cfg(feature = "mtls")]
40    tls_config_store: Option<Arc<arc_swap::ArcSwap<rustls::ServerConfig>>>,
41    /// Shared metrics registry — exposed so callers can wire `MetricsLayer`.
42    metrics: Arc<crate::metrics_layer::NetMetrics>,
43}
44
45impl<S: StorageEngine + Send + Sync + 'static> AqlServerBuilder<S> {
46    /// Create a new server builder with the given storage engine.
47    pub fn new(storage: Arc<S>) -> Self {
48        Self {
49            storage,
50            logging_verbosity: None,
51            slow_threshold_ms: None,
52            metrics_addr: None,
53            bind_addr: None,
54            rate_limit_qps: None,
55            jwt_secret_path: None,
56            #[cfg(feature = "mtls")]
57            tls_config_store: None,
58            metrics: crate::metrics_layer::NetMetrics::new(),
59        }
60    }
61
62    /// Configure request/response logging verbosity.
63    ///
64    /// Returns `self` for chaining.  The stored verbosity can be retrieved via
65    /// [`Self::logging_verbosity`] so callers can apply a [`LoggingLayer`]
66    /// around the tonic service.
67    ///
68    /// [`LoggingLayer`]: crate::logging_layer::LoggingLayer
69    pub fn with_logging(mut self, verbosity: crate::logging_layer::LogVerbosity) -> Self {
70        self.logging_verbosity = Some(verbosity);
71        self
72    }
73
74    /// Return the configured logging verbosity (if any).
75    pub fn logging_verbosity(&self) -> Option<crate::logging_layer::LogVerbosity> {
76        self.logging_verbosity
77    }
78
79    /// Configure the slow-request threshold (ms) for the `LoggingLayer`.
80    pub fn with_slow_threshold_ms(mut self, ms: u64) -> Self {
81        self.slow_threshold_ms = Some(ms);
82        self
83    }
84
85    /// Return the configured slow-request threshold (if any).
86    pub fn slow_threshold_ms(&self) -> Option<u64> {
87        self.slow_threshold_ms
88    }
89
90    /// Set the gRPC server bind address.  Recorded for callers that wire a
91    /// tonic `Server::bind`; this builder does not itself spawn a tonic
92    /// server.
93    pub fn with_bind_addr(mut self, addr: std::net::SocketAddr) -> Self {
94        self.bind_addr = Some(addr);
95        self
96    }
97
98    /// Return the configured gRPC bind address (if any).
99    pub fn bind_addr(&self) -> Option<std::net::SocketAddr> {
100        self.bind_addr
101    }
102
103    /// Configure the steady-state QPS for the rate limiter.  Recorded for
104    /// callers that wire a [`crate::rate_limiter::RateLimiter`].
105    pub fn with_rate_limit_qps(mut self, qps: f64) -> Self {
106        self.rate_limit_qps = Some(qps);
107        self
108    }
109
110    /// Return the configured rate-limit QPS (if any).
111    pub fn rate_limit_qps(&self) -> Option<f64> {
112        self.rate_limit_qps
113    }
114
115    /// Configure the JWT secret path used by the bearer-token auth middleware.
116    pub fn with_jwt_secret_path(mut self, path: std::path::PathBuf) -> Self {
117        self.jwt_secret_path = Some(path);
118        self
119    }
120
121    /// Return the configured JWT secret path (if any).
122    pub fn jwt_secret_path(&self) -> Option<&std::path::Path> {
123        self.jwt_secret_path.as_deref()
124    }
125
126    /// Install initial TLS credentials for live cert rotation.
127    ///
128    /// Builds a `rustls::ServerConfig` from `creds`, wraps it in an
129    /// [`arc_swap::ArcSwap`], and stores the handle on the builder.  Callers
130    /// retrieve the store via [`Self::tls_config_store`] and pass it to a
131    /// [`crate::tls_acceptor::LiveTlsAcceptor`] so each new TLS handshake
132    /// reads the latest cert.
133    ///
134    /// # Errors
135    ///
136    /// Returns [`crate::error::NetError::TlsError`] if the credentials cannot
137    /// be parsed into a `rustls::ServerConfig`.
138    #[cfg(feature = "mtls")]
139    pub fn with_tls_creds(
140        mut self,
141        creds: &crate::tls_acceptor::TlsCredsRef<'_>,
142    ) -> NetResult<Self> {
143        let config = crate::tls_acceptor::build_rustls_config(creds)?;
144        self.tls_config_store = Some(Arc::new(arc_swap::ArcSwap::from_pointee(config)));
145        Ok(self)
146    }
147
148    /// Return a clone of the current TLS config store (if installed).
149    ///
150    /// Callers feed this into [`crate::tls_acceptor::LiveTlsAcceptor::new`]
151    /// to enable per-connection cert pickup; the same store can later be
152    /// updated atomically via `store.store(Arc::new(new_config))`.
153    #[cfg(feature = "mtls")]
154    pub fn tls_config_store(&self) -> Option<Arc<arc_swap::ArcSwap<rustls::ServerConfig>>> {
155        self.tls_config_store.as_ref().map(Arc::clone)
156    }
157
158    /// Set the `SocketAddr` on which the Prometheus metrics HTTP server will
159    /// listen.  When set, [`Self::build`] spawns a background task serving
160    /// `GET /metrics`.
161    ///
162    /// The metrics server runs on a separate port from gRPC so that scrape
163    /// traffic never reaches the tonic transport.
164    pub fn with_metrics_addr(mut self, addr: std::net::SocketAddr) -> Self {
165        self.metrics_addr = Some(addr);
166        self
167    }
168
169    /// Return the configured metrics HTTP address (if any).
170    pub fn metrics_addr(&self) -> Option<std::net::SocketAddr> {
171        self.metrics_addr
172    }
173
174    /// Return a clone of the shared [`crate::metrics_layer::NetMetrics`]
175    /// registry.
176    ///
177    /// Use this to apply [`crate::metrics_layer::MetricsLayer`] around the
178    /// tonic service so that gRPC request metrics flow into the same registry
179    /// that the HTTP endpoint serves.
180    pub fn metrics(&self) -> Arc<crate::metrics_layer::NetMetrics> {
181        Arc::clone(&self.metrics)
182    }
183
184    /// Build the service implementation.
185    ///
186    /// If [`Self::with_metrics_addr`] was called, also spawns the Prometheus
187    /// HTTP server as a background tokio task.  The returned handle is
188    /// discarded here; the task runs until the process exits or the tokio
189    /// runtime shuts down.
190    pub fn build(self) -> AqlServiceImpl<S> {
191        if let Some(addr) = self.metrics_addr {
192            crate::metrics_layer::spawn_metrics_server(addr, Arc::clone(&self.metrics));
193        }
194        AqlServiceImpl::new(self.storage)
195    }
196
197    /// Build a tonic-ready gRPC service (wrapped in `AqlServiceServer`).
198    ///
199    /// When the `compression` feature is enabled the server is configured to
200    /// accept and send gzip-compressed messages.
201    pub fn build_grpc_service(
202        self,
203    ) -> crate::proto::aql::aql_service_server::AqlServiceServer<
204        crate::grpc_service::AqlGrpcService<S>,
205    > {
206        use crate::grpc_service::AqlGrpcService;
207        use crate::proto::aql::aql_service_server::AqlServiceServer;
208
209        let service_impl = Arc::new(AqlServiceImpl::new(self.storage));
210        let grpc_service = AqlGrpcService::new(service_impl);
211
212        #[allow(unused_mut)]
213        let mut server = AqlServiceServer::new(grpc_service);
214
215        #[cfg(feature = "compression")]
216        {
217            server = server
218                .accept_compressed(tonic::codec::CompressionEncoding::Gzip)
219                .send_compressed(tonic::codec::CompressionEncoding::Gzip);
220        }
221
222        server
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229    use amaters_core::storage::MemoryStorage;
230
231    /// `build_grpc_service` compiles and produces a server regardless of the
232    /// `compression` feature.
233    #[tokio::test]
234    async fn test_build_grpc_service_compression_feature_gate() {
235        let storage = Arc::new(MemoryStorage::new());
236        let builder = AqlServerBuilder::new(storage);
237        let _server = builder.build_grpc_service();
238    }
239}