Skip to main content

frontend/service_config/
pending_rows_batcher.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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/// Independent ordinary-table and logical-table batching configuration.
26#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
27#[serde(default)]
28pub struct PendingRowsBatcherOptions {
29    /// Existing ordinary-table controls retain their original TOML paths.
30    #[serde(flatten)]
31    pub table: BatcherOptions,
32    /// Independent logical-table controls; absence retains the legacy Prom fallback.
33    #[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/// Write batching controls shared by ingestion protocols.
42#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
43#[serde(default)]
44pub struct BatcherOptions {
45    /// Write protocols sharing this batcher; empty disables all entrances.
46    pub protocols: Vec<BatchingProtocol>,
47    /// Time from the first pending submission to a timed flush. Zero disables batching.
48    #[serde(with = "humantime_serde")]
49    pub pending_rows_flush_interval: Duration,
50    /// Row threshold checked after appending a complete submission.
51    pub max_batch_rows: usize,
52    /// Maximum concurrent flushes shared by the frontend batcher.
53    pub max_concurrent_flushes: usize,
54    /// Maximum number of queued submissions per table worker.
55    pub worker_channel_capacity: usize,
56    /// Maximum number of original requests awaiting completion.
57    pub max_inflight_requests: usize,
58    /// Maximum number of table Flow notifications waiting in the shared queue.
59    pub flow_notification_queue_capacity: NonZeroUsize,
60}
61
62impl BatcherOptions {
63    /// Returns whether a protocol opts in and its controls pass construction validation.
64    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
93/// Rejects protocols that cannot write metric-engine logical tables.
94fn 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    /// Explicit configuration wins even when it disables batching.
114    pub(crate) fn table_batcher_options(&self) -> &BatcherOptions {
115        &self.pending_rows_batcher.table
116    }
117
118    /// Resolve legacy Prom controls once without merging fields across config blocks.
119    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}