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}