Skip to main content

reinhardt_grpc/
settings.rs

1//! Settings fragment for the gRPC server configuration.
2//!
3//! This module provides [`GrpcServerSettings`], a `#[settings]` fragment that
4//! maps to the `[grpc_server]` configuration section. It is the settings-first
5//! replacement for the deprecated [`GrpcServerConfig`] DoS-protection config.
6//!
7//! Durations are stored as integer seconds in the fragment so they can be
8//! expressed naturally in TOML, and are converted back into [`std::time::Duration`]
9//! by the [`From`] bridge.
10
11#![allow(deprecated)] // Settings conversion targets the legacy config during the compatibility window.
12
13use crate::server::GrpcServerConfig;
14use reinhardt_core::macros::settings;
15use serde::{Deserialize, Serialize};
16
17/// Default maximum decoding (incoming) message size: 4MB.
18const DEFAULT_MAX_DECODING_MESSAGE_SIZE: usize = 4 * 1024 * 1024;
19
20/// Default maximum encoding (outgoing) message size: 4MB.
21const DEFAULT_MAX_ENCODING_MESSAGE_SIZE: usize = 4 * 1024 * 1024;
22
23/// Default request timeout in seconds: 30 seconds.
24const DEFAULT_REQUEST_TIMEOUT_SECS: u64 = 30;
25
26/// Default maximum concurrent connections: 1000.
27const DEFAULT_MAX_CONCURRENT_CONNECTIONS: usize = 1000;
28
29fn default_max_decoding_message_size() -> usize {
30	DEFAULT_MAX_DECODING_MESSAGE_SIZE
31}
32
33fn default_max_encoding_message_size() -> usize {
34	DEFAULT_MAX_ENCODING_MESSAGE_SIZE
35}
36
37fn default_request_timeout_secs() -> u64 {
38	DEFAULT_REQUEST_TIMEOUT_SECS
39}
40
41fn default_max_concurrent_connections() -> usize {
42	DEFAULT_MAX_CONCURRENT_CONNECTIONS
43}
44
45/// gRPC server configuration fragment.
46///
47/// This fragment maps to the `[grpc_server]` section and can be composed with
48/// the `#[settings]` macro from downstream applications. It configures
49/// DoS-protection limits for gRPC services: message size limits, request
50/// timeouts, and connection limits.
51///
52/// # Example
53///
54/// ```rust
55/// use reinhardt_grpc::GrpcServerSettings;
56///
57/// let settings: GrpcServerSettings = toml::from_str(r#"
58/// max_decoding_message_size = 8388608
59/// request_timeout_secs = 60
60/// max_concurrent_connections = 500
61/// "#).unwrap();
62///
63/// assert_eq!(settings.max_decoding_message_size, 8 * 1024 * 1024);
64/// assert_eq!(settings.request_timeout_secs, 60);
65/// assert_eq!(settings.max_concurrent_connections, 500);
66/// ```
67#[settings(fragment = true, section = "grpc_server")]
68#[derive(Clone, Debug, Serialize, Deserialize)]
69pub struct GrpcServerSettings {
70	/// Maximum decoding (incoming) message size in bytes.
71	#[serde(default = "default_max_decoding_message_size")]
72	pub max_decoding_message_size: usize,
73	/// Maximum encoding (outgoing) message size in bytes.
74	#[serde(default = "default_max_encoding_message_size")]
75	pub max_encoding_message_size: usize,
76	/// Request timeout in seconds.
77	#[serde(default = "default_request_timeout_secs")]
78	pub request_timeout_secs: u64,
79	/// Maximum number of concurrent connections allowed.
80	#[serde(default = "default_max_concurrent_connections")]
81	pub max_concurrent_connections: usize,
82}
83
84impl Default for GrpcServerSettings {
85	fn default() -> Self {
86		Self {
87			max_decoding_message_size: DEFAULT_MAX_DECODING_MESSAGE_SIZE,
88			max_encoding_message_size: DEFAULT_MAX_ENCODING_MESSAGE_SIZE,
89			request_timeout_secs: DEFAULT_REQUEST_TIMEOUT_SECS,
90			max_concurrent_connections: DEFAULT_MAX_CONCURRENT_CONNECTIONS,
91		}
92	}
93}
94
95impl From<&GrpcServerSettings> for GrpcServerConfig {
96	fn from(settings: &GrpcServerSettings) -> Self {
97		// `GrpcServerConfig` has private fields, so rebuild it through its builder.
98		GrpcServerConfig::builder()
99			.max_decoding_message_size(settings.max_decoding_message_size)
100			.max_encoding_message_size(settings.max_encoding_message_size)
101			.request_timeout(std::time::Duration::from_secs(
102				settings.request_timeout_secs,
103			))
104			.max_concurrent_connections(settings.max_concurrent_connections)
105			.build()
106	}
107}
108
109/// Create a [`GrpcServerConfig`] from a [`GrpcServerSettings`] fragment.
110///
111/// This is the settings-first entry point that replaces direct construction of
112/// the deprecated [`GrpcServerConfig`].
113pub fn create_grpc_server_config_from_settings(settings: &GrpcServerSettings) -> GrpcServerConfig {
114	GrpcServerConfig::from(settings)
115}
116
117#[cfg(test)]
118mod tests {
119	use super::*;
120	use std::time::Duration;
121
122	#[test]
123	fn default_settings_match_config_defaults() {
124		// Arrange & Act
125		let settings = GrpcServerSettings::default();
126		let config = GrpcServerConfig::from(&settings);
127		let default_config = GrpcServerConfig::default();
128
129		// Assert
130		assert_eq!(
131			config.max_decoding_message_size(),
132			default_config.max_decoding_message_size()
133		);
134		assert_eq!(
135			config.max_encoding_message_size(),
136			default_config.max_encoding_message_size()
137		);
138		assert_eq!(config.request_timeout(), default_config.request_timeout());
139		assert_eq!(
140			config.max_concurrent_connections(),
141			default_config.max_concurrent_connections()
142		);
143	}
144
145	#[test]
146	fn custom_settings_convert_to_config() {
147		// Arrange
148		let settings = GrpcServerSettings {
149			max_decoding_message_size: 8 * 1024 * 1024,
150			max_encoding_message_size: 16 * 1024 * 1024,
151			request_timeout_secs: 60,
152			max_concurrent_connections: 500,
153		};
154
155		// Act
156		let config = create_grpc_server_config_from_settings(&settings);
157
158		// Assert
159		assert_eq!(config.max_decoding_message_size(), 8 * 1024 * 1024);
160		assert_eq!(config.max_encoding_message_size(), 16 * 1024 * 1024);
161		assert_eq!(config.request_timeout(), Duration::from_secs(60));
162		assert_eq!(config.max_concurrent_connections(), 500);
163	}
164
165	#[test]
166	fn settings_deserialize_from_toml_with_partial_fields() {
167		// Arrange & Act
168		let settings: GrpcServerSettings = toml::from_str(
169			r#"
170			max_decoding_message_size = 2097152
171			"#,
172		)
173		.expect("settings should deserialize");
174
175		// Assert: provided field is honored, missing fields fall back to defaults.
176		assert_eq!(settings.max_decoding_message_size, 2 * 1024 * 1024);
177		assert_eq!(
178			settings.max_encoding_message_size,
179			DEFAULT_MAX_ENCODING_MESSAGE_SIZE
180		);
181		assert_eq!(settings.request_timeout_secs, DEFAULT_REQUEST_TIMEOUT_SECS);
182		assert_eq!(
183			settings.max_concurrent_connections,
184			DEFAULT_MAX_CONCURRENT_CONNECTIONS
185		);
186	}
187}