1use std::num::NonZeroUsize;
16use std::time::Duration;
17
18use common_batcher::flush_policy::timing::TimingFlushPolicy;
19use serde::{Deserialize, Serialize};
20use servers::batcher::BatchingProtocol;
21use tokio::sync::Semaphore;
22
23use crate::frontend::FrontendOptions;
24
25#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
27#[serde(default)]
28pub struct PendingRowsBatcherOptions {
29 #[serde(flatten)]
31 pub table: BatcherOptions,
32 #[serde(
34 default,
35 skip_serializing_if = "Option::is_none",
36 deserialize_with = "deserialize_logical_options"
37 )]
38 pub logical_table: Option<BatcherOptions>,
39}
40
41#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
43#[serde(default)]
44pub struct BatcherOptions {
45 pub protocols: Vec<BatchingProtocol>,
47 #[serde(with = "humantime_serde")]
49 pub pending_rows_flush_interval: Duration,
50 pub max_batch_rows: usize,
52 pub max_concurrent_flushes: usize,
54 pub worker_channel_capacity: usize,
56 pub max_inflight_requests: usize,
58 pub flow_notification_queue_capacity: NonZeroUsize,
60}
61
62impl BatcherOptions {
63 pub fn pending_rows_batching_enabled(&self) -> bool {
65 !self.protocols.is_empty()
66 && TimingFlushPolicy::validate(self.pending_rows_flush_interval)
67 && self.max_batch_rows > 0
68 && [
69 self.max_concurrent_flushes,
70 self.worker_channel_capacity,
71 self.max_inflight_requests,
72 self.flow_notification_queue_capacity.get(),
73 ]
74 .into_iter()
75 .all(|capacity| (1..=Semaphore::MAX_PERMITS).contains(&capacity))
76 }
77}
78
79impl Default for BatcherOptions {
80 fn default() -> Self {
81 Self {
82 protocols: Vec::new(),
83 pending_rows_flush_interval: Duration::ZERO,
84 max_batch_rows: 100_000,
85 max_concurrent_flushes: 256,
86 worker_channel_capacity: 65_536,
87 max_inflight_requests: 3000,
88 flow_notification_queue_capacity: NonZeroUsize::new(1024).unwrap_or(NonZeroUsize::MIN),
89 }
90 }
91}
92
93fn deserialize_logical_options<'de, D>(deserializer: D) -> Result<Option<BatcherOptions>, D::Error>
95where
96 D: serde::Deserializer<'de>,
97{
98 let options = Option::<BatcherOptions>::deserialize(deserializer)?;
99 if options.as_ref().is_some_and(|options| {
100 options
101 .protocols
102 .iter()
103 .any(|protocol| !matches!(protocol, BatchingProtocol::Prom | BatchingProtocol::Otlp))
104 }) {
105 return Err(serde::de::Error::custom(
106 "pending_rows_batcher.logical_table only supports prom and otlp",
107 ));
108 }
109 Ok(options)
110}
111
112impl FrontendOptions {
113 pub(crate) fn table_batcher_options(&self) -> &BatcherOptions {
115 &self.pending_rows_batcher.table
116 }
117
118 pub(crate) fn logical_batcher_options(&self) -> BatcherOptions {
120 if let Some(options) = &self.pending_rows_batcher.logical_table {
121 return options.clone();
122 }
123 let shared = &self.pending_rows_batcher.table;
124 if shared.protocols.contains(&BatchingProtocol::Prom)
125 && shared.pending_rows_batching_enabled()
126 {
127 let mut options = shared.clone();
128 options.protocols = vec![BatchingProtocol::Prom];
129 return options;
130 }
131 let prom = &self.prom_store;
132 BatcherOptions {
133 protocols: vec![BatchingProtocol::Prom],
134 pending_rows_flush_interval: prom.pending_rows_flush_interval,
135 max_batch_rows: prom.max_batch_rows,
136 max_concurrent_flushes: prom.max_concurrent_flushes,
137 worker_channel_capacity: prom.worker_channel_capacity,
138 max_inflight_requests: prom.max_inflight_requests,
139 flow_notification_queue_capacity: prom.flow_notification_queue_capacity,
140 }
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use crate::service_config::pending_rows_batcher::*;
147
148 #[test]
149 fn test_batcher_config_precedence() {
150 use crate::frontend::FrontendOptions;
151 let legacy = "[prom_store]\nenable = true\nwith_metric_engine = true\npending_rows_flush_interval = '2s'\nmax_batch_rows = 9\n";
152 let shared = "[pending_rows_batcher]\nprotocols = ['prom', 'otlp']\npending_rows_flush_interval = '3s'\n";
153 let options: FrontendOptions = toml::from_str(legacy).unwrap();
154 assert_eq!(options.logical_batcher_options().max_batch_rows, 9);
155 assert_eq!(
156 options.logical_batcher_options().protocols,
157 vec![BatchingProtocol::Prom]
158 );
159 let options: FrontendOptions = toml::from_str(&format!("{legacy}{shared}")).unwrap();
160 assert_eq!(
161 options
162 .logical_batcher_options()
163 .pending_rows_flush_interval,
164 Duration::from_secs(3)
165 );
166 assert_eq!(options.logical_batcher_options().max_batch_rows, 100_000);
167 assert_eq!(
168 options.logical_batcher_options().protocols,
169 vec![BatchingProtocol::Prom]
170 );
171 for explicit in [
172 "",
173 "protocols = ['otlp']\npending_rows_flush_interval = '5s'",
174 ] {
175 let options: FrontendOptions = toml::from_str(&format!(
176 "{legacy}{shared}[pending_rows_batcher.logical_table]\n{explicit}"
177 ))
178 .unwrap();
179 assert_eq!(
180 &options.logical_batcher_options(),
181 options.pending_rows_batcher.logical_table.as_ref().unwrap()
182 );
183 assert_eq!(
184 options.table_batcher_options(),
185 &toml::from_str::<FrontendOptions>(&format!("{legacy}{shared}"))
186 .unwrap()
187 .pending_rows_batcher
188 .table
189 );
190 let restored: FrontendOptions =
191 toml::from_str(&toml::to_string(&options).unwrap()).unwrap();
192 assert_eq!(
193 options.pending_rows_batcher.logical_table,
194 restored.pending_rows_batcher.logical_table
195 );
196 assert_eq!(options.pending_rows_batcher, restored.pending_rows_batcher);
197 }
198 }
199
200 #[test]
201 fn test_batcher_config_independent_defaults() {
202 use crate::frontend::FrontendOptions;
203
204 let parent = "[pending_rows_batcher]\nprotocols = ['prom']\npending_rows_flush_interval = '3s'\nmax_batch_rows = 7\n";
205 let options: FrontendOptions = toml::from_str(parent).unwrap();
206 let serialized = toml::to_string(&options).unwrap();
207 assert!(!serialized.contains("[pending_rows_batcher.logical_table]"));
208 assert!(!serialized.contains("[pending_rows_batcher.table]"));
209 let restored: FrontendOptions = toml::from_str(&serialized).unwrap();
210 assert_eq!(restored.logical_batcher_options().max_batch_rows, 7);
211
212 let options: FrontendOptions =
213 toml::from_str(&format!("{parent}[pending_rows_batcher.logical_table]")).unwrap();
214 assert_eq!(options.table_batcher_options().max_batch_rows, 7);
215 assert_eq!(options.logical_batcher_options(), BatcherOptions::default());
216 assert!(
217 !options
218 .logical_batcher_options()
219 .pending_rows_batching_enabled()
220 );
221 }
222
223 #[test]
224 fn test_logical_batcher_config_protocols() {
225 use crate::frontend::FrontendOptions;
226 for protocols in ["[]", "['prom']", "['otlp']", "['prom', 'otlp']"] {
227 let options: FrontendOptions = toml::from_str(&format!(
228 "[pending_rows_batcher.logical_table]\nprotocols = {protocols}"
229 ))
230 .unwrap();
231 assert!(options.pending_rows_batcher.logical_table.is_some());
232 }
233 for protocol in [
234 "influxdb",
235 "logs",
236 "loki",
237 "http_sql",
238 "opentsdb",
239 "elasticsearch",
240 "splunk",
241 "mysql",
242 "postgres",
243 ] {
244 assert!(
245 toml::from_str::<FrontendOptions>(&format!(
246 "[pending_rows_batcher.logical_table]\nprotocols = ['{protocol}']"
247 ))
248 .is_err()
249 );
250 }
251 }
252
253 #[test]
254 fn test_protocols() {
255 let options: BatcherOptions = toml::from_str(
256 "protocols = ['influxdb', 'opentsdb', 'otlp', 'logs', 'loki', 'splunk', 'elasticsearch', 'http_sql', 'prom', 'mysql', 'postgres']",
257 ).unwrap();
258 assert_eq!(options.protocols.len(), 11);
259 assert!(options.protocols.contains(&BatchingProtocol::Mysql));
260 assert!(options.protocols.contains(&BatchingProtocol::Postgres));
261 assert!(options.protocols.contains(&BatchingProtocol::HttpSql));
262 assert!(BatcherOptions::default().protocols.is_empty());
263 for invalid in ["sql", "jaeger", "unknown"] {
264 assert!(
265 toml::from_str::<BatcherOptions>(&format!("protocols = ['{invalid}']")).is_err()
266 );
267 }
268 }
269
270 #[test]
271 fn test_notification_capacity() {
272 let default = BatcherOptions::default();
273 assert_eq!(default.flow_notification_queue_capacity.get(), 1024);
274 let configured: BatcherOptions =
275 toml::from_str("flow_notification_queue_capacity = 8").unwrap();
276 assert_eq!(configured.flow_notification_queue_capacity.get(), 8);
277 assert!(toml::from_str::<BatcherOptions>("flow_notification_queue_capacity = 0").is_err());
278 }
279
280 #[test]
281 fn test_defaults_and_roundtrip() {
282 let options: BatcherOptions = toml::from_str("").unwrap();
283 assert_eq!(options, BatcherOptions::default());
284 assert!(!options.pending_rows_batching_enabled());
285 assert_eq!(options.max_batch_rows, 100_000);
286 assert_eq!(options.max_concurrent_flushes, 256);
287 assert_eq!(options.worker_channel_capacity, 65_536);
288 assert_eq!(options.max_inflight_requests, 3000);
289 let serialized = toml::to_string(&options).unwrap();
290 assert_eq!(
291 options,
292 toml::from_str::<BatcherOptions>(&serialized).unwrap()
293 );
294 }
295
296 #[test]
297 fn test_worker_capacity_override() {
298 let options: FrontendOptions = toml::from_str(
299 r#"
300[pending_rows_batcher]
301worker_channel_capacity = 12345
302[pending_rows_batcher.logical_table]
303worker_channel_capacity = 12345
304[prom_store]
305enable = true
306with_metric_engine = true
307worker_channel_capacity = 12345
308"#,
309 )
310 .unwrap();
311 assert_eq!(
312 options.pending_rows_batcher.table.worker_channel_capacity,
313 12_345
314 );
315 assert_eq!(
316 options
317 .pending_rows_batcher
318 .logical_table
319 .unwrap()
320 .worker_channel_capacity,
321 12_345
322 );
323 assert_eq!(options.prom_store.worker_channel_capacity, 12_345);
324 }
325
326 #[test]
327 fn test_partial_options_and_zero_controls() {
328 let options: BatcherOptions =
329 toml::from_str("pending_rows_flush_interval = '5ms'").unwrap();
330 assert_eq!(
331 options.pending_rows_flush_interval,
332 Duration::from_millis(5)
333 );
334 assert!(!options.pending_rows_batching_enabled());
335 let enabled: BatcherOptions =
336 toml::from_str("protocols = ['http_sql']\npending_rows_flush_interval = '5ms'")
337 .unwrap();
338 assert!(enabled.pending_rows_batching_enabled());
339 for field in [
340 "max_batch_rows",
341 "max_concurrent_flushes",
342 "worker_channel_capacity",
343 "max_inflight_requests",
344 ] {
345 let options: BatcherOptions = toml::from_str(&format!(
346 "protocols = ['influxdb']\npending_rows_flush_interval = '5ms'\n{field} = 0"
347 ))
348 .unwrap();
349 assert!(!options.pending_rows_batching_enabled(), "{field}");
350 }
351 }
352}