rocketmq_runtime/
config.rs1use std::time::Duration;
16
17use crate::blocking::BlockingLanePolicies;
18use crate::error::RuntimeContractViolation;
19use crate::RuntimeContractPolicy;
20
21pub const MIN_ENTRYPOINT_BLOCKING_THREADS: usize = 3;
23pub const MAX_ENTRYPOINT_BLOCKING_THREADS: usize = 512;
25pub(crate) const DEFAULT_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(30);
30
31#[derive(Debug, Clone)]
42pub struct RuntimeConfig {
43 pub worker_threads: usize,
45 pub max_blocking_threads: usize,
50 pub thread_name: String,
52 pub thread_stack_size: Option<usize>,
54 pub thread_keep_alive: Duration,
56 pub shutdown_timeout: Duration,
65 pub blocking_lane_policies: BlockingLanePolicies,
67 pub enable_io: bool,
69 pub enable_time: bool,
71}
72
73impl RuntimeConfig {
74 pub fn for_parallelism(thread_name: impl Into<String>, available_parallelism: usize) -> Self {
80 let parallelism = available_parallelism.max(1);
81 let max_blocking_threads = parallelism
82 .saturating_mul(4)
83 .clamp(MIN_ENTRYPOINT_BLOCKING_THREADS, MAX_ENTRYPOINT_BLOCKING_THREADS);
84 let mut blocking_lane_policies = BlockingLanePolicies::for_parallelism(parallelism);
85 blocking_lane_policies.cap_concurrency(max_blocking_threads);
86 Self {
87 worker_threads: parallelism,
88 max_blocking_threads,
89 thread_name: thread_name.into(),
90 thread_stack_size: None,
91 thread_keep_alive: Duration::from_secs(30),
92 shutdown_timeout: DEFAULT_SHUTDOWN_TIMEOUT,
93 blocking_lane_policies,
94 enable_io: true,
95 enable_time: true,
96 }
97 }
98
99 pub fn with_max_blocking_threads(mut self, limit: usize) -> Result<Self, RuntimeContractViolation> {
106 if !(MIN_ENTRYPOINT_BLOCKING_THREADS..=MAX_ENTRYPOINT_BLOCKING_THREADS).contains(&limit) {
107 return Err(RuntimeContractViolation::InvalidConfiguration {
108 policy: RuntimeContractPolicy::MaxBlockingThreadsWithinSupportedRange,
109 });
110 }
111 self.max_blocking_threads = limit;
112 self.blocking_lane_policies.cap_concurrency(limit);
113 self.validate()?;
114 Ok(self)
115 }
116
117 pub fn blocking_thread_headroom(&self) -> usize {
125 (self.max_blocking_threads / 8).max(2)
126 }
127
128 pub fn tokio_blocking_threads(&self) -> usize {
131 self.max_blocking_threads
132 .saturating_add(self.blocking_thread_headroom())
133 }
134
135 pub fn server_default(thread_name: impl Into<String>) -> Self {
137 Self {
138 thread_name: thread_name.into(),
139 ..Self::default()
140 }
141 }
142
143 pub fn broker_default() -> Self {
145 Self::server_default("rocketmq-broker")
146 }
147
148 pub fn namesrv_default() -> Self {
150 Self::server_default("rocketmq-namesrv")
151 }
152
153 pub fn proxy_default() -> Self {
155 Self::server_default("rocketmq-proxy")
156 }
157
158 pub fn controller_default() -> Self {
160 Self::server_default("rocketmq-controller")
161 }
162
163 pub fn validate(&self) -> Result<(), RuntimeContractViolation> {
170 if self.worker_threads == 0 {
171 return Err(RuntimeContractViolation::InvalidConfiguration {
172 policy: RuntimeContractPolicy::WorkerThreadsPositive,
173 });
174 }
175 if self.max_blocking_threads == 0 {
176 return Err(RuntimeContractViolation::InvalidConfiguration {
177 policy: RuntimeContractPolicy::MaxBlockingThreadsPositive,
178 });
179 }
180 if self.max_blocking_threads > MAX_ENTRYPOINT_BLOCKING_THREADS {
181 return Err(RuntimeContractViolation::InvalidConfiguration {
182 policy: RuntimeContractPolicy::MaxBlockingThreadsWithinSupportedRange,
183 });
184 }
185 if self.thread_name.trim().is_empty() {
186 return Err(RuntimeContractViolation::InvalidConfiguration {
187 policy: RuntimeContractPolicy::ThreadNameNotBlank,
188 });
189 }
190 if matches!(self.thread_stack_size, Some(0)) {
191 return Err(RuntimeContractViolation::InvalidConfiguration {
192 policy: RuntimeContractPolicy::ThreadStackSizePositive,
193 });
194 }
195 self.blocking_lane_policies
196 .validate_for_global_capacity(self.max_blocking_threads)?;
197 Ok(())
198 }
199}
200
201impl Default for RuntimeConfig {
202 fn default() -> Self {
203 let parallelism = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4);
204 Self::for_parallelism("rocketmq-runtime", parallelism)
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn entrypoint_profiles_derive_bounded_cpu_and_lane_budgets() {
214 for (parallelism, expected_workers, expected_blocking) in [(0, 1, 4), (1, 1, 4), (2, 2, 8), (32, 32, 128)] {
215 let config = RuntimeConfig::for_parallelism("profile-test", parallelism);
216 assert_eq!(config.worker_threads, expected_workers);
217 assert_eq!(config.max_blocking_threads, expected_blocking);
218 assert!(config.blocking_lane_policies.storage_io.max_concurrency <= expected_blocking);
219 assert!(config.blocking_lane_policies.metadata_io.max_concurrency <= expected_blocking);
220 assert!(config.blocking_lane_policies.cpu_crypto.max_concurrency <= expected_blocking);
221 config.validate().expect("derived entrypoint profile is valid");
222 }
223
224 let capped = RuntimeConfig::for_parallelism("large-profile-test", usize::MAX);
225 assert_eq!(capped.max_blocking_threads, MAX_ENTRYPOINT_BLOCKING_THREADS);
226 assert_eq!(
227 RuntimeConfig::for_parallelism("headroom-test", 32).tokio_blocking_threads(),
228 128 + 16
229 );
230 }
231
232 #[test]
233 fn explicit_blocking_limit_has_safe_bounds_and_caps_lanes() {
234 let config = RuntimeConfig::for_parallelism("override-test", 16)
235 .with_max_blocking_threads(6)
236 .expect("explicit safe limit");
237 assert_eq!(config.max_blocking_threads, 6);
238 assert_eq!(config.tokio_blocking_threads(), 8);
239 assert_eq!(config.blocking_lane_policies.storage_io.max_concurrency, 6);
240 assert_eq!(config.blocking_lane_policies.metadata_io.max_concurrency, 6);
241 assert!(config.blocking_lane_policies.cpu_crypto.max_concurrency <= 6);
242
243 for invalid in [0, 1, 2, MAX_ENTRYPOINT_BLOCKING_THREADS + 1] {
244 assert!(RuntimeConfig::for_parallelism("invalid-override", 4)
245 .with_max_blocking_threads(invalid)
246 .is_err());
247 }
248 }
249}