Skip to main content

servers/
prom_store.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
15//! prometheus protocol supportings
16//! handles prometheus remote_write, remote_read logic
17use std::cmp::Ordering;
18use std::collections::HashMap;
19use std::collections::hash_map::DefaultHasher;
20use std::hash::{Hash, Hasher};
21use std::sync::Arc;
22
23use api::prom_store::remote::label_matcher::Type as MatcherType;
24use api::prom_store::remote::{Label, LabelMatcher, Query, Sample, TimeSeries, WriteRequest};
25use api::v1::RowInsertRequests;
26use arrow::array::{
27    Array, ArrayRef, AsArray, DictionaryArray, LargeStringArray, StringArray, StringViewArray,
28};
29use arrow::compute::kernels::cast as casts;
30use arrow::datatypes::{
31    DataType, Float64Type, TimeUnit, TimestampMicrosecondType, TimestampMillisecondType,
32    TimestampNanosecondType, UInt32Type,
33};
34use common_grpc::precision::Precision;
35use common_memory_manager::MemoryGuard;
36use common_query::prelude::{greptime_timestamp, greptime_value};
37use common_recordbatch::{RecordBatch, RecordBatches};
38use common_telemetry::{tracing, warn};
39use datafusion::dataframe::DataFrame;
40use datafusion::prelude::{Expr, ident, lit, regexp_match};
41use datafusion_common::ScalarValue;
42use datafusion_expr::LogicalPlan;
43use snafu::{OptionExt, ResultExt, ensure};
44use snap::raw::{Decoder, Encoder};
45
46use crate::error::{self, Result};
47use crate::prom_remote_write::REMOTE_WRITE_V1_VERSION;
48use crate::request_memory_limiter::ServerMemoryLimiter;
49use crate::request_memory_metrics::RequestMemoryMetrics;
50use crate::row_writer::{self, MultiTableData};
51
52pub const METRIC_NAME_LABEL: &str = "__name__";
53pub const METRIC_NAME_LABEL_BYTES: &[u8] = b"__name__";
54
55/// special label for selecting database name on remote write
56pub const DATABASE_LABEL: &str = "x_greptime_database";
57pub const DATABASE_LABEL_BYTES: &[u8] = b"x_greptime_database";
58pub const DATABASE_LABEL_ALT: &str = "__database__";
59pub const DATABASE_LABEL_ALT_BYTES: &[u8] = b"__database__";
60
61/// deprecated, use DATABASE_LABEL instead
62#[deprecated(note = "use DATABASE_LABEL instead")]
63pub const SCHEMA_LABEL: &str = "__schema__";
64#[deprecated(note = "use DATABASE_LABEL_BYTES instead")]
65pub const SCHEMA_LABEL_BYTES: &[u8] = b"__schema__";
66
67/// special label for selecting physical table name on remote write
68pub const PHYSICAL_TABLE_LABEL: &str = "x_greptime_physical_table";
69pub const PHYSICAL_TABLE_LABEL_BYTES: &[u8] = b"x_greptime_physical_table";
70pub const PHYSICAL_TABLE_LABEL_ALT: &str = "__physical_table__";
71pub const PHYSICAL_TABLE_LABEL_ALT_BYTES: &[u8] = b"__physical_table__";
72
73/// The same as `FIELD_COLUMN_MATCHER` in `promql` crate
74pub const FIELD_NAME_LABEL: &str = "__field__";
75
76/// Check if given label is a special label for remote write
77#[allow(deprecated)]
78pub fn is_remote_write_special_label(label: &str) -> bool {
79    label == DATABASE_LABEL
80        || label == DATABASE_LABEL_ALT
81        || label == PHYSICAL_TABLE_LABEL
82        || label == PHYSICAL_TABLE_LABEL_ALT
83        || label == SCHEMA_LABEL
84}
85
86#[allow(deprecated)]
87pub fn is_remote_read_special_label(label: &str) -> bool {
88    label == METRIC_NAME_LABEL
89        || label == DATABASE_LABEL
90        || label == DATABASE_LABEL_ALT
91        || label == SCHEMA_LABEL
92}
93
94/// Check if given label is a database selection label
95#[allow(deprecated)]
96pub fn is_database_selection_label(label: &str) -> bool {
97    label == DATABASE_LABEL || label == DATABASE_LABEL_ALT || label == SCHEMA_LABEL
98}
99
100/// Check if given label is a physical table selection label
101pub fn is_physical_table_selection_label(label: &str) -> bool {
102    label == PHYSICAL_TABLE_LABEL || label == PHYSICAL_TABLE_LABEL_ALT
103}
104
105/// Get table name from remote query
106pub fn table_name(q: &Query) -> Result<String> {
107    let mut matchers = q
108        .matchers
109        .iter()
110        .filter(|matcher| matcher.name == METRIC_NAME_LABEL);
111    let matcher = matchers
112        .next()
113        .context(error::InvalidPromRemoteRequestSnafu {
114            msg: "missing '__name__' label in timeseries",
115        })?;
116
117    if matcher.r#type != MatcherType::Eq as i32
118        || matcher.value.is_empty()
119        || matchers.next().is_some()
120    {
121        return Err(error::InvalidPromRemoteRequestSnafu {
122            msg: "expected exactly one non-empty equality matcher for '__name__'".to_string(),
123        }
124        .build());
125    }
126
127    Ok(matcher.value.clone())
128}
129
130/// Extract database selector from a remote read query.
131pub fn extract_schema_from_query(query: &Query) -> Option<String> {
132    query
133        .matchers
134        .iter()
135        .find(|matcher| {
136            is_database_selection_label(&matcher.name) && matcher.r#type == MatcherType::Eq as i32
137        })
138        .map(|matcher| matcher.value.clone())
139}
140
141/// Validate matcher syntax independently of whether the metric exists.
142pub fn validate_matchers(matchers: &[LabelMatcher]) -> Result<()> {
143    for matcher in matchers {
144        let matcher_type = MatcherType::try_from(matcher.r#type).map_err(|e| {
145            error::InvalidPromRemoteRequestSnafu {
146                msg: format!("invalid LabelMatcher type, decode error: {e}"),
147            }
148            .build()
149        })?;
150        if matches!(matcher_type, MatcherType::Re | MatcherType::Nre) {
151            // Like Prometheus, validate the raw regex first, then the anchored pattern.
152            // We use the same regex engine as query execution, whose syntax differs
153            // from Prometheus's: e.g. `(?x) a` is accepted here but rejected by Prometheus.
154            regex::Regex::new(&matcher.value)
155                .and_then(|_| regex::Regex::new(&anchored_label_regex(&matcher.value)))
156                .map_err(|e| {
157                    error::InvalidPromRemoteRequestSnafu {
158                        msg: format!("invalid regex for label '{}': {e}", matcher.name),
159                    }
160                    .build()
161                })?;
162        }
163    }
164    Ok(())
165}
166
167/// Create a DataFrame from a remote Query
168#[tracing::instrument(skip_all)]
169pub fn query_to_plan(
170    dataframe: DataFrame,
171    q: &Query,
172    timestamp_column_name: &str,
173) -> Result<LogicalPlan> {
174    let start_timestamp_ms = q.start_timestamp_ms;
175    let end_timestamp_ms = q.end_timestamp_ms;
176
177    let label_matches = &q.matchers;
178
179    let mut conditions = Vec::with_capacity(label_matches.len() + 1);
180
181    conditions
182        .push(ident(timestamp_column_name).gt_eq(lit_timestamp_millisecond(start_timestamp_ms)));
183    conditions
184        .push(ident(timestamp_column_name).lt_eq(lit_timestamp_millisecond(end_timestamp_ms)));
185
186    for m in label_matches {
187        let name = &m.name;
188
189        if is_remote_read_special_label(name) {
190            continue;
191        }
192
193        let value = &m.value;
194        let m_type = MatcherType::try_from(m.r#type).map_err(|e| {
195            error::InvalidPromRemoteRequestSnafu {
196                msg: format!("invalid LabelMatcher type, decode error: {e}",),
197            }
198            .build()
199        })?;
200
201        // Prometheus treats an absent label as "".
202        let label = if dataframe.schema().has_column_with_unqualified_name(name) {
203            ident(name)
204        } else {
205            lit("")
206        };
207
208        // Like PromQL, include NULL when the matcher accepts the empty string.
209        // Keep EQ/NEQ on the original column for index recognition.
210        let condition = match m_type {
211            MatcherType::Eq => {
212                if value.is_empty() {
213                    label.clone().eq(lit(value)).or(label.is_null())
214                } else {
215                    label.eq(lit(value))
216                }
217            }
218            MatcherType::Neq => {
219                if value.is_empty() {
220                    label.not_eq(lit(value))
221                } else {
222                    label.clone().not_eq(lit(value)).or(label.is_null())
223                }
224            }
225            MatcherType::Re | MatcherType::Nre => {
226                let pattern = anchored_label_regex(value);
227                let matches_empty = regex::Regex::new(&pattern)
228                    .map_err(|e| {
229                        error::InvalidPromRemoteRequestSnafu {
230                            msg: format!("invalid regex for label '{name}': {e}"),
231                        }
232                        .build()
233                    })?
234                    .is_match("");
235                let matched = regexp_match(label.clone(), lit(pattern), None);
236                if m_type == MatcherType::Re {
237                    let condition = matched.is_not_null();
238                    if matches_empty {
239                        condition.or(label.is_null())
240                    } else {
241                        condition
242                    }
243                } else {
244                    let condition = matched.is_null();
245                    // A NULL input also produces NULL. Keep it only when the
246                    // negative matcher accepts the empty string.
247                    if matches_empty {
248                        condition.and(label.is_not_null())
249                    } else {
250                        condition
251                    }
252                }
253            }
254        };
255        conditions.push(condition);
256    }
257
258    // Safety: conditions MUST not be empty, reduce always return Some(expr).
259    let conditions = conditions.into_iter().reduce(Expr::and).unwrap();
260
261    let dataframe = dataframe
262        .filter(conditions)
263        .context(error::DataFrameSnafu)?;
264
265    Ok(dataframe.into_parts().1)
266}
267
268/// Prometheus's NewFastRegexMatcher also wraps patterns in ^(?s:...)$:
269/// https://github.com/prometheus/prometheus/blob/main/model/labels/regexp.go
270fn anchored_label_regex(value: &str) -> String {
271    format!("^(?s:{value})$")
272}
273
274#[inline]
275fn new_label(name: String, value: String) -> Label {
276    Label { name, value }
277}
278
279fn lit_timestamp_millisecond(ts: i64) -> Expr {
280    Expr::Literal(ScalarValue::TimestampMillisecond(Some(ts), None), None)
281}
282
283/// Sort timeseries by labels, matching the former `BTreeMap` order.
284fn compare_timeseries_labels(left: &[Label], right: &[Label]) -> Ordering {
285    let ordering = left.len().cmp(&right.len());
286    if ordering != Ordering::Equal {
287        return ordering;
288    }
289
290    for (left, right) in left.iter().zip(right) {
291        let ordering = left.name.cmp(&right.name);
292        if ordering != Ordering::Equal {
293            return ordering;
294        }
295
296        let ordering = left.value.cmp(&right.value);
297        if ordering != Ordering::Equal {
298            return ordering;
299        }
300    }
301
302    Ordering::Equal
303}
304
305enum LabelValues<'a> {
306    Utf8(&'a StringArray),
307    LargeUtf8(&'a LargeStringArray),
308    Utf8View(&'a StringViewArray),
309    DictionaryUtf8 {
310        dictionary: &'a DictionaryArray<UInt32Type>,
311        values: &'a StringArray,
312    },
313    Other(Vec<Option<String>>),
314}
315
316impl LabelValues<'_> {
317    fn value(&self, row: usize) -> Option<&str> {
318        match self {
319            Self::Utf8(values) => values.is_valid(row).then(|| values.value(row)),
320            Self::LargeUtf8(values) => values.is_valid(row).then(|| values.value(row)),
321            Self::Utf8View(values) => values.is_valid(row).then(|| values.value(row)),
322            Self::DictionaryUtf8 { dictionary, values } => dictionary
323                .key(row)
324                .and_then(|key| values.is_valid(key).then(|| values.value(key))),
325            Self::Other(values) => values.get(row).and_then(Option::as_deref),
326        }
327    }
328}
329
330fn row_labels<'a>(
331    columns: &'a [LabelColumn<'a>],
332    row: usize,
333) -> impl Iterator<Item = (&'a str, &'a str)> {
334    columns
335        .iter()
336        .filter_map(move |column| column.values.value(row).map(|value| (column.name, value)))
337}
338
339struct LabelColumn<'a> {
340    name: &'a str,
341    values: LabelValues<'a>,
342}
343
344fn label_columns<'a>(
345    recordbatch: &'a RecordBatch,
346    timestamp_column_name: &str,
347    value_column_name: &str,
348) -> Result<Vec<LabelColumn<'a>>> {
349    recordbatch
350        .schema
351        .column_schemas()
352        .iter()
353        .enumerate()
354        .filter(|(_, column_schema)| {
355            column_schema.name != timestamp_column_name && column_schema.name != value_column_name
356        })
357        .map(|(index, column_schema)| {
358            let array = recordbatch.column(index);
359            let values = match array.data_type() {
360                arrow::datatypes::DataType::Utf8 => LabelValues::Utf8(array.as_string::<i32>()),
361                arrow::datatypes::DataType::LargeUtf8 => {
362                    LabelValues::LargeUtf8(array.as_string::<i64>())
363                }
364                arrow::datatypes::DataType::Utf8View => {
365                    LabelValues::Utf8View(array.as_string_view())
366                }
367                arrow::datatypes::DataType::Dictionary(key, value)
368                    if key.as_ref() == &arrow::datatypes::DataType::UInt32
369                        && value.as_ref() == &arrow::datatypes::DataType::Utf8 =>
370                {
371                    let dictionary = array.as_dictionary::<UInt32Type>();
372                    LabelValues::DictionaryUtf8 {
373                        dictionary,
374                        values: dictionary.values().as_string::<i32>(),
375                    }
376                }
377                _ => {
378                    let values = recordbatch.iter_column_as_string(index).collect::<Vec<_>>();
379                    ensure!(
380                        values.len() == recordbatch.num_rows(),
381                        error::InvalidPromRemoteReadQueryResultSnafu {
382                            msg: format!(
383                                "Cannot convert label column '{}' of datatype {:?} to string",
384                                column_schema.name,
385                                array.data_type()
386                            ),
387                        }
388                    );
389                    LabelValues::Other(values)
390                }
391            };
392            Ok(LabelColumn {
393                name: &column_schema.name,
394                values,
395            })
396        })
397        .collect()
398}
399
400fn hash_timeseries(columns: &[LabelColumn<'_>], row: usize) -> u64 {
401    let mut hasher = DefaultHasher::new();
402
403    for (name, value) in row_labels(columns, row) {
404        name.hash(&mut hasher);
405        value.hash(&mut hasher);
406    }
407
408    hasher.finish()
409}
410
411fn matches_timeseries(labels: &[Label], columns: &[LabelColumn<'_>], row: usize) -> bool {
412    let mut labels = labels.iter().skip(1);
413    for (name, value) in row_labels(columns, row) {
414        let Some(label) = labels.next() else {
415            return false;
416        };
417        if label.name != name || label.value != value {
418            return false;
419        }
420    }
421
422    labels.next().is_none()
423}
424
425fn new_timeseries(table: &str, columns: &[LabelColumn<'_>], row: usize) -> TimeSeries {
426    let mut labels = Vec::with_capacity(columns.len() + 1);
427    labels.push(new_label(METRIC_NAME_LABEL.to_string(), table.to_string()));
428
429    for (name, value) in row_labels(columns, row) {
430        labels.push(new_label(name.to_string(), value.to_string()));
431    }
432
433    TimeSeries {
434        labels,
435        ..Default::default()
436    }
437}
438
439pub fn recordbatches_to_timeseries(
440    table_name: &str,
441    timestamp_column_name: &str,
442    value_column_name: &str,
443    recordbatches: RecordBatches,
444) -> Result<Vec<TimeSeries>> {
445    let mut timeseries: Vec<TimeSeries> = Vec::new();
446    let mut timeseries_by_hash: HashMap<u64, Vec<usize>> = HashMap::new();
447
448    for recordbatch in recordbatches.take() {
449        append_recordbatch_to_timeseries(
450            table_name,
451            timestamp_column_name,
452            value_column_name,
453            recordbatch,
454            &mut timeseries,
455            &mut timeseries_by_hash,
456        )?;
457    }
458
459    for ts in &mut timeseries {
460        ts.samples.sort_unstable_by_key(|s| s.timestamp);
461    }
462
463    timeseries
464        .sort_unstable_by(|left, right| compare_timeseries_labels(&left.labels, &right.labels));
465    Ok(timeseries)
466}
467
468fn append_recordbatch_to_timeseries(
469    table: &str,
470    timestamp_column_name: &str,
471    value_column_name: &str,
472    recordbatch: RecordBatch,
473    timeseries: &mut Vec<TimeSeries>,
474    timeseries_by_hash: &mut HashMap<u64, Vec<usize>>,
475) -> Result<()> {
476    let ts_column = recordbatch
477        .column_by_name(timestamp_column_name)
478        .with_context(|| error::InvalidPromRemoteReadQueryResultSnafu {
479            msg: format!("missing timestamp column '{timestamp_column_name}' in query result"),
480        })?;
481    // The Prometheus remote read wire format carries millisecond timestamps,
482    // while the table's time index can use any time unit (e.g. a metric
483    // physical table created with TIMESTAMP(6)). Narrowing floors towards
484    // negative infinity, consistent with `Timestamp::convert_to` on the
485    // ingestion path; arrow's cast would truncate towards zero, so a
486    // pre-epoch sample (e.g. -1001us) would round to -1ms instead of -2ms
487    // and disagree with what ingesting the same instant into a millisecond
488    // table would have stored.
489    let ts_column: ArrayRef = match ts_column.data_type() {
490        DataType::Timestamp(TimeUnit::Millisecond, _) => ts_column.clone(),
491        // Second -> millisecond is a widening (exact) conversion.
492        DataType::Timestamp(TimeUnit::Second, _) => casts::cast(
493            ts_column,
494            &DataType::Timestamp(TimeUnit::Millisecond, None),
495        )
496        .map_err(|e| {
497            error::InvalidPromRemoteReadQueryResultSnafu {
498                msg: format!(
499                    "failed to cast timestamp column '{timestamp_column_name}' of datatype {:?} to millisecond: {e}",
500                    ts_column.data_type()
501                ),
502            }
503            .build()
504        })?,
505        DataType::Timestamp(TimeUnit::Microsecond, _) => Arc::new(
506            ts_column
507                .as_primitive::<TimestampMicrosecondType>()
508                .unary::<_, TimestampMillisecondType>(|v| v.div_euclid(1_000)),
509        ),
510        DataType::Timestamp(TimeUnit::Nanosecond, _) => Arc::new(
511            ts_column
512                .as_primitive::<TimestampNanosecondType>()
513                .unary::<_, TimestampMillisecondType>(|v| v.div_euclid(1_000_000)),
514        ),
515        _ => {
516            return error::InvalidPromRemoteReadQueryResultSnafu {
517                msg: format!(
518                    "Expect timestamp column of datatype Timestamp(Millisecond), actual {:?}",
519                    ts_column.data_type()
520                ),
521            }
522            .fail();
523        }
524    };
525    let ts_column = ts_column
526        .as_primitive_opt::<TimestampMillisecondType>()
527        .with_context(|| error::InvalidPromRemoteReadQueryResultSnafu {
528            msg: format!(
529                "Expect timestamp column of datatype Timestamp(Millisecond), actual {:?}",
530                ts_column.data_type()
531            ),
532        })?;
533
534    // TODO: Add native-histogram encoding when Prometheus Remote Read support is prioritized.
535    // The current path intentionally returns scalar samples only.
536    let field_column = recordbatch
537        .column_by_name(value_column_name)
538        .with_context(|| error::InvalidPromRemoteReadQueryResultSnafu {
539            msg: format!("missing value column '{value_column_name}' in query result"),
540        })?;
541    let field_column = field_column
542        .as_primitive_opt::<Float64Type>()
543        .with_context(|| error::InvalidPromRemoteReadQueryResultSnafu {
544            msg: format!(
545                "Expect value column of datatype Float64, actual {:?}",
546                field_column.data_type()
547            ),
548        })?;
549
550    let columns = label_columns(&recordbatch, timestamp_column_name, value_column_name)?;
551    let mut previous_timeseries: Option<usize> = None;
552
553    for row in 0..recordbatch.num_rows() {
554        let timeseries_index = match previous_timeseries {
555            Some(index) if matches_timeseries(&timeseries[index].labels, &columns, row) => index,
556            _ => {
557                let hash = hash_timeseries(&columns, row);
558                let candidates = timeseries_by_hash.entry(hash).or_default();
559                match candidates
560                    .iter()
561                    .copied()
562                    .find(|index| matches_timeseries(&timeseries[*index].labels, &columns, row))
563                {
564                    Some(index) => index,
565                    None => {
566                        let index = timeseries.len();
567                        timeseries.push(new_timeseries(table, &columns, row));
568                        candidates.push(index);
569                        index
570                    }
571                }
572            }
573        };
574        previous_timeseries = Some(timeseries_index);
575
576        if ts_column.is_null(row) || field_column.is_null(row) {
577            continue;
578        }
579
580        let value = field_column.value(row);
581        let timestamp = ts_column.value(row);
582        let sample = Sample { value, timestamp };
583
584        timeseries[timeseries_index].samples.push(sample);
585    }
586
587    Ok(())
588}
589
590pub fn to_grpc_row_insert_requests(request: &WriteRequest) -> Result<(RowInsertRequests, usize)> {
591    let _timer = crate::metrics::METRIC_HTTP_PROM_STORE_CODEC_ELAPSED
592        .with_label_values(&["convert", REMOTE_WRITE_V1_VERSION])
593        .start_timer();
594
595    let mut multi_table_data = MultiTableData::new();
596
597    for series in &request.timeseries {
598        let table_name = &series
599            .labels
600            .iter()
601            .find(|label| {
602                // The metric name is a special label
603                label.name == METRIC_NAME_LABEL
604            })
605            .context(error::InvalidPromRemoteRequestSnafu {
606                msg: "missing '__name__' label in time-series",
607            })?
608            .value;
609
610        // The metric name is a special label,
611        // num_columns = labels.len() - 1 + 1 (value) + 1 (timestamp)
612        let num_columns = series.labels.len() + 1;
613
614        let table_data = multi_table_data.get_or_default_table_data(
615            table_name,
616            num_columns,
617            series.samples.len(),
618        );
619
620        // labels
621        let kvs = series.labels.iter().filter_map(|label| {
622            if label.name == METRIC_NAME_LABEL {
623                None
624            } else {
625                Some((label.name.clone(), label.value.clone()))
626            }
627        });
628
629        if series.samples.len() == 1 {
630            let mut one_row = table_data.alloc_one_row();
631
632            row_writer::write_tags(table_data, kvs, &mut one_row)?;
633            // value
634            row_writer::write_f64(
635                table_data,
636                greptime_value(),
637                series.samples[0].value,
638                &mut one_row,
639            )?;
640            // timestamp
641            row_writer::write_ts_to_millis(
642                table_data,
643                greptime_timestamp(),
644                Some(series.samples[0].timestamp),
645                Precision::Millisecond,
646                &mut one_row,
647            )?;
648
649            table_data.add_row(one_row);
650        } else {
651            for Sample { value, timestamp } in &series.samples {
652                let mut one_row = table_data.alloc_one_row();
653
654                // labels
655                let kvs = kvs.clone();
656                row_writer::write_tags(table_data, kvs, &mut one_row)?;
657                // value
658                row_writer::write_f64(table_data, greptime_value(), *value, &mut one_row)?;
659                // timestamp
660                row_writer::write_ts_to_millis(
661                    table_data,
662                    greptime_timestamp(),
663                    Some(*timestamp),
664                    Precision::Millisecond,
665                    &mut one_row,
666                )?;
667
668                table_data.add_row(one_row);
669            }
670        }
671
672        if !series.histograms.is_empty() {
673            warn!("Native histograms are not supported yet, data ignored");
674        }
675    }
676
677    Ok(multi_table_data.into_row_insert_requests())
678}
679
680/// Hard cap on the decompressed size of a handler-decompressed request body
681/// (Prometheus remote read/write, Loki push).
682///
683/// The HTTP body limit only bounds the *encoded* bytes on the wire. Without
684/// this cap a tiny compressed payload (e.g. a five-byte raw-Snappy varint or a
685/// few KiB of zstd RLE blocks) could declare/expand to multiple GiB of heap
686/// before any protobuf validation runs, so the decompressed size must be
687/// bounded on its own.
688pub const MAX_DECOMPRESSED_REQUEST_SIZE: usize = 512 * 1024 * 1024;
689
690/// Size of the chunks a zstd body is decompressed and charged in.
691const ZSTD_DECOMPRESS_CHUNK: usize = 256 * 1024;
692
693/// Decompressed bytes together with the memory permits charged for them.
694/// The permits are released when the buffer is dropped, so the aggregate
695/// memory accounting covers the whole lifetime of the decompressed data.
696pub(crate) struct ChargedBuffer {
697    pub data: Vec<u8>,
698    guards: Vec<MemoryGuard<RequestMemoryMetrics>>,
699}
700
701impl std::ops::Deref for ChargedBuffer {
702    type Target = [u8];
703
704    fn deref(&self) -> &Self::Target {
705        &self.data
706    }
707}
708
709impl std::fmt::Debug for ChargedBuffer {
710    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
711        f.debug_struct("ChargedBuffer")
712            .field("data_len", &self.data.len())
713            .field("guards", &self.guards.len())
714            .finish()
715    }
716}
717
718impl ChargedBuffer {
719    /// Creates a buffer with the permits charged for it.
720    pub(crate) fn new(data: Vec<u8>, guards: Vec<MemoryGuard<RequestMemoryMetrics>>) -> Self {
721        Self { data, guards }
722    }
723
724    /// Splits the buffer into the raw bytes and the memory permits charged for
725    /// them. Hold the permits as long as the bytes are kept.
726    pub(crate) fn into_parts(self) -> (Vec<u8>, Vec<MemoryGuard<RequestMemoryMetrics>>) {
727        (self.data, self.guards)
728    }
729}
730
731/// Decompresses a raw Snappy body after enforcing a hard cap on the decoded
732/// size and charging the aggregate request-memory limiter.
733///
734/// The raw Snappy format prefixes the stream with a varint of the decoded
735/// length, so the output size is known (and validated) before the output
736/// buffer is allocated by `decompress_vec`.
737pub(crate) async fn snappy_decompress_limited(
738    buf: &[u8],
739    max_decompressed: usize,
740    limiter: &ServerMemoryLimiter,
741) -> Result<ChargedBuffer> {
742    let decoded_len =
743        snap::raw::decompress_len(buf).context(error::DecompressSnappyPromRemoteRequestSnafu)?;
744    ensure!(
745        decoded_len <= max_decompressed,
746        error::DecompressedBodyTooLargeSnafu {
747            size: decoded_len as u64,
748            limit: max_decompressed as u64,
749        }
750    );
751    // Reserve the declared size before the allocation happens so concurrent
752    // decompressions cannot exceed the aggregate quota.
753    let guard = limiter.acquire(decoded_len as u64).await?;
754    let mut decoder = Decoder::new();
755    let data = decoder
756        .decompress_vec(buf)
757        .context(error::DecompressSnappyPromRemoteRequestSnafu)?;
758    Ok(ChargedBuffer {
759        data,
760        guards: vec![guard],
761    })
762}
763
764/// Decompresses a zstd body with a hard cap on the decoded size, charging the
765/// aggregate request-memory limiter incrementally while the output is
766/// materialized (zstd streams don't declare their decoded size up front).
767pub(crate) async fn zstd_decompress_limited(
768    buf: &[u8],
769    max_decompressed: usize,
770    limiter: &ServerMemoryLimiter,
771) -> Result<ChargedBuffer> {
772    use std::io::Read;
773
774    let decoder = zstd::stream::read::Decoder::new(buf)
775        .context(error::DecompressZstdPromRemoteRequestSnafu)?;
776    // Allow reading one byte past the limit so oversized streams are detected
777    // instead of being silently truncated.
778    let mut limited = decoder.take(max_decompressed as u64 + 1);
779    let mut data = Vec::new();
780    let mut guards = Vec::new();
781    let mut chunk = vec![0u8; ZSTD_DECOMPRESS_CHUNK];
782    loop {
783        let n = limited
784            .read(&mut chunk)
785            .context(error::DecompressZstdPromRemoteRequestSnafu)?;
786        if n == 0 {
787            break;
788        }
789        ensure!(
790            data.len() + n <= max_decompressed,
791            error::DecompressedBodyTooLargeSnafu {
792                size: (data.len() + n) as u64,
793                limit: max_decompressed as u64,
794            }
795        );
796        // Charge each chunk before it is appended so the aggregate quota
797        // bounds the memory while it is being materialized.
798        guards.push(limiter.acquire(n as u64).await?);
799        data.extend_from_slice(&chunk[..n]);
800    }
801    Ok(ChargedBuffer { data, guards })
802}
803
804/// Decompresses a raw Snappy body with the hard decoded-size cap enforced,
805/// without charging the aggregate request-memory limiter.
806///
807/// Used for response bodies (remote read replies) and tests; request bodies
808/// must use [`snappy_decompress_limited`] instead so their decoded size is
809/// also charged to the aggregate quota.
810#[inline]
811pub fn snappy_decompress(buf: &[u8]) -> Result<Vec<u8>> {
812    let decoded_len =
813        snap::raw::decompress_len(buf).context(error::DecompressSnappyPromRemoteRequestSnafu)?;
814    ensure!(
815        decoded_len <= MAX_DECOMPRESSED_REQUEST_SIZE,
816        error::DecompressedBodyTooLargeSnafu {
817            size: decoded_len as u64,
818            limit: MAX_DECOMPRESSED_REQUEST_SIZE as u64,
819        }
820    );
821    let mut decoder = Decoder::new();
822    decoder
823        .decompress_vec(buf)
824        .context(error::DecompressSnappyPromRemoteRequestSnafu)
825}
826
827#[inline]
828pub fn snappy_compress(buf: &[u8]) -> Result<Vec<u8>> {
829    let mut encoder = Encoder::new();
830    encoder
831        .compress_vec(buf)
832        .context(error::CompressPromRemoteRequestSnafu)
833}
834
835/// Mock timeseries for test, it is both used in servers and frontend crate
836/// So we present it here
837pub fn mock_timeseries() -> Vec<TimeSeries> {
838    vec![
839        TimeSeries {
840            labels: vec![
841                new_label(METRIC_NAME_LABEL.to_string(), "metric1".to_string()),
842                new_label("job".to_string(), "spark".to_string()),
843            ],
844            samples: vec![
845                Sample {
846                    value: 1.0f64,
847                    timestamp: 1000,
848                },
849                Sample {
850                    value: 2.0f64,
851                    timestamp: 2000,
852                },
853            ],
854            ..Default::default()
855        },
856        TimeSeries {
857            labels: vec![
858                new_label(METRIC_NAME_LABEL.to_string(), "metric2".to_string()),
859                new_label("instance".to_string(), "test_host1".to_string()),
860                new_label("idc".to_string(), "z001".to_string()),
861            ],
862            samples: vec![
863                Sample {
864                    value: 3.0f64,
865                    timestamp: 1000,
866                },
867                Sample {
868                    value: 4.0f64,
869                    timestamp: 2000,
870                },
871            ],
872            ..Default::default()
873        },
874        TimeSeries {
875            labels: vec![
876                new_label(METRIC_NAME_LABEL.to_string(), "metric3".to_string()),
877                new_label("idc".to_string(), "z002".to_string()),
878                new_label("app".to_string(), "biz".to_string()),
879            ],
880            samples: vec![
881                Sample {
882                    value: 5.0f64,
883                    timestamp: 1000,
884                },
885                Sample {
886                    value: 6.0f64,
887                    timestamp: 2000,
888                },
889                Sample {
890                    value: 7.0f64,
891                    timestamp: 3000,
892                },
893            ],
894            ..Default::default()
895        },
896    ]
897}
898
899/// Add new labels to the mock timeseries.
900pub fn mock_timeseries_new_label() -> Vec<TimeSeries> {
901    let ts_demo_metrics = TimeSeries {
902        labels: vec![
903            new_label(METRIC_NAME_LABEL.to_string(), "demo_metrics".to_string()),
904            new_label("idc".to_string(), "idc3".to_string()),
905            new_label("new_label1".to_string(), "foo".to_string()),
906        ],
907        samples: vec![Sample {
908            value: 42.0,
909            timestamp: 3000,
910        }],
911        ..Default::default()
912    };
913    let ts_multi_labels = TimeSeries {
914        labels: vec![
915            new_label(METRIC_NAME_LABEL.to_string(), "metric1".to_string()),
916            new_label("idc".to_string(), "idc4".to_string()),
917            new_label("env".to_string(), "prod".to_string()),
918            new_label("host".to_string(), "host9".to_string()),
919            new_label("new_label2".to_string(), "bar".to_string()),
920        ],
921        samples: vec![Sample {
922            value: 99.0,
923            timestamp: 4000,
924        }],
925        ..Default::default()
926    };
927
928    vec![ts_demo_metrics, ts_multi_labels]
929}
930
931/// Add new labels to the mock timeseries.
932pub fn mock_timeseries_special_labels() -> Vec<TimeSeries> {
933    let idc3_schema = TimeSeries {
934        labels: vec![
935            new_label(METRIC_NAME_LABEL.to_string(), "idc3_lo_table".to_string()),
936            new_label(DATABASE_LABEL.to_string(), "idc3".to_string()),
937            new_label(PHYSICAL_TABLE_LABEL.to_string(), "f1".to_string()),
938        ],
939        samples: vec![Sample {
940            value: 42.0,
941            timestamp: 3000,
942        }],
943        ..Default::default()
944    };
945    let idc4_schema = TimeSeries {
946        labels: vec![
947            new_label(
948                METRIC_NAME_LABEL.to_string(),
949                "idc4_local_table".to_string(),
950            ),
951            new_label(DATABASE_LABEL.to_string(), "idc4".to_string()),
952            new_label(PHYSICAL_TABLE_LABEL.to_string(), "f2".to_string()),
953        ],
954        samples: vec![Sample {
955            value: 99.0,
956            timestamp: 4000,
957        }],
958        ..Default::default()
959    };
960
961    vec![idc3_schema, idc4_schema]
962}
963
964#[cfg(test)]
965mod tests {
966    use std::sync::Arc;
967
968    use api::prom_store::remote::LabelMatcher;
969    use api::v1::{ColumnDataType, Row, SemanticType};
970    use arrow::array::{
971        DictionaryArray, Float64Array, StringArray, TimestampMillisecondArray, UInt32Array,
972    };
973    use arrow::datatypes::{DataType as ArrowDataType, Field, Schema as ArrowSchema, UInt32Type};
974    use common_memory_manager::OnExhaustedPolicy;
975    use common_recordbatch::DfRecordBatch;
976    use datafusion::prelude::SessionContext;
977    use datatypes::data_type::ConcreteDataType;
978    use datatypes::schema::{ColumnSchema, Schema};
979    use datatypes::vectors::{
980        Float64Vector, Int32Vector, StringVector, TimestampMillisecondVector,
981    };
982    use table::table::adapter::DfTableProviderAdapter;
983    use table::test_util::MemTable;
984
985    use super::*;
986
987    const EQ_TYPE: i32 = MatcherType::Eq as i32;
988    const NEQ_TYPE: i32 = MatcherType::Neq as i32;
989    const RE_TYPE: i32 = MatcherType::Re as i32;
990
991    #[test]
992    fn test_validate_matchers_regex_syntax() {
993        for op in [MatcherType::Re, MatcherType::Nre] {
994            for (value, valid) in [
995                ("api|web", true),
996                ("(?s)api.web", true),
997                ("(?x) api", true),
998                ("(?x) api # comment", false),
999                ("[", false),
1000                // Wrapping must not make an invalid original pattern valid.
1001                ("a)|(?:b", false),
1002            ] {
1003                let matcher = LabelMatcher {
1004                    name: "job".into(),
1005                    value: value.into(),
1006                    r#type: op as i32,
1007                };
1008                assert_eq!(
1009                    valid,
1010                    validate_matchers(&[matcher]).is_ok(),
1011                    "{op:?}: {value}"
1012                );
1013            }
1014        }
1015    }
1016
1017    #[test]
1018    fn test_table_name() {
1019        let q = Query {
1020            start_timestamp_ms: 1000,
1021            end_timestamp_ms: 2000,
1022            matchers: vec![],
1023            ..Default::default()
1024        };
1025        let err = table_name(&q).unwrap_err();
1026        assert!(matches!(err, error::Error::InvalidPromRemoteRequest { .. }));
1027
1028        let q = Query {
1029            start_timestamp_ms: 1000,
1030            end_timestamp_ms: 2000,
1031            matchers: vec![LabelMatcher {
1032                name: METRIC_NAME_LABEL.to_string(),
1033                value: "test".to_string(),
1034                r#type: EQ_TYPE,
1035            }],
1036            ..Default::default()
1037        };
1038        assert_eq!("test", table_name(&q).unwrap());
1039
1040        for matchers in [
1041            vec![LabelMatcher {
1042                name: METRIC_NAME_LABEL.to_string(),
1043                value: "test.*".to_string(),
1044                r#type: RE_TYPE,
1045            }],
1046            vec![LabelMatcher {
1047                name: METRIC_NAME_LABEL.to_string(),
1048                value: String::new(),
1049                r#type: EQ_TYPE,
1050            }],
1051            vec![
1052                LabelMatcher {
1053                    name: METRIC_NAME_LABEL.to_string(),
1054                    value: "test".to_string(),
1055                    r#type: EQ_TYPE,
1056                },
1057                LabelMatcher {
1058                    name: METRIC_NAME_LABEL.to_string(),
1059                    value: "other".to_string(),
1060                    r#type: EQ_TYPE,
1061                },
1062            ],
1063        ] {
1064            let q = Query {
1065                matchers,
1066                ..Default::default()
1067            };
1068            assert!(matches!(
1069                table_name(&q),
1070                Err(error::Error::InvalidPromRemoteRequest { .. })
1071            ));
1072        }
1073    }
1074
1075    #[test]
1076    #[allow(deprecated)]
1077    fn test_extract_schema_from_query() {
1078        let query = Query::default();
1079        assert_eq!(None, extract_schema_from_query(&query));
1080
1081        for label in [DATABASE_LABEL, DATABASE_LABEL_ALT, SCHEMA_LABEL] {
1082            let query = Query {
1083                matchers: vec![LabelMatcher {
1084                    name: label.to_string(),
1085                    value: "selected_schema".to_string(),
1086                    r#type: EQ_TYPE,
1087                }],
1088                ..Default::default()
1089            };
1090            assert_eq!(
1091                Some("selected_schema".to_string()),
1092                extract_schema_from_query(&query)
1093            );
1094        }
1095
1096        let query = Query {
1097            matchers: vec![LabelMatcher {
1098                name: DATABASE_LABEL.to_string(),
1099                value: "selected_schema".to_string(),
1100                r#type: NEQ_TYPE,
1101            }],
1102            ..Default::default()
1103        };
1104        assert_eq!(None, extract_schema_from_query(&query));
1105    }
1106
1107    #[test]
1108    fn test_query_to_plan() {
1109        let q = Query {
1110            start_timestamp_ms: 1000,
1111            end_timestamp_ms: 2000,
1112            matchers: vec![LabelMatcher {
1113                name: METRIC_NAME_LABEL.to_string(),
1114                value: "test".to_string(),
1115                r#type: EQ_TYPE,
1116            }],
1117            ..Default::default()
1118        };
1119
1120        let schema = Arc::new(Schema::new(vec![
1121            ColumnSchema::new(
1122                greptime_timestamp(),
1123                ConcreteDataType::timestamp_millisecond_datatype(),
1124                true,
1125            ),
1126            ColumnSchema::new(greptime_value(), ConcreteDataType::float64_datatype(), true),
1127            ColumnSchema::new("instance", ConcreteDataType::string_datatype(), true),
1128            ColumnSchema::new("job", ConcreteDataType::string_datatype(), true),
1129            ColumnSchema::new("service.name", ConcreteDataType::string_datatype(), true),
1130            ColumnSchema::new("Region", ConcreteDataType::string_datatype(), true),
1131        ]));
1132        let recordbatch = RecordBatch::new(
1133            schema,
1134            vec![
1135                Arc::new(TimestampMillisecondVector::from_vec(vec![1000])) as _,
1136                Arc::new(Float64Vector::from_vec(vec![3.0])) as _,
1137                Arc::new(StringVector::from(vec!["host1"])) as _,
1138                Arc::new(StringVector::from(vec!["job"])) as _,
1139                Arc::new(StringVector::from(vec!["api"])) as _,
1140                Arc::new(StringVector::from(vec!["us"])) as _,
1141            ],
1142        )
1143        .unwrap();
1144
1145        let ctx = SessionContext::new();
1146        let table = MemTable::table("test", recordbatch);
1147        let table_provider = Arc::new(DfTableProviderAdapter::new(table));
1148
1149        let dataframe = ctx.read_table(table_provider.clone()).unwrap();
1150        let plan = query_to_plan(dataframe, &q, greptime_timestamp()).unwrap();
1151        let display_string = format!("{}", plan.display_indent());
1152
1153        let ts_col = greptime_timestamp();
1154        let expected = format!(
1155            "Filter: ?table?.{} >= TimestampMillisecond(1000, None) AND ?table?.{} <= TimestampMillisecond(2000, None)\n  TableScan: ?table?",
1156            ts_col, ts_col
1157        );
1158        assert_eq!(expected, display_string);
1159
1160        let q = Query {
1161            start_timestamp_ms: 1000,
1162            end_timestamp_ms: 2000,
1163            matchers: vec![
1164                LabelMatcher {
1165                    name: METRIC_NAME_LABEL.to_string(),
1166                    value: "test".to_string(),
1167                    r#type: EQ_TYPE,
1168                },
1169                LabelMatcher {
1170                    name: "job".to_string(),
1171                    value: ".*prom.*".to_string(),
1172                    r#type: RE_TYPE,
1173                },
1174                LabelMatcher {
1175                    name: "instance".to_string(),
1176                    value: "localhost".to_string(),
1177                    r#type: NEQ_TYPE,
1178                },
1179                LabelMatcher {
1180                    name: "service.name".to_string(),
1181                    value: "api".to_string(),
1182                    r#type: EQ_TYPE,
1183                },
1184                LabelMatcher {
1185                    name: "Region".to_string(),
1186                    value: "us".to_string(),
1187                    r#type: EQ_TYPE,
1188                },
1189            ],
1190            ..Default::default()
1191        };
1192
1193        let dataframe = ctx.read_table(table_provider).unwrap();
1194        let plan = query_to_plan(dataframe, &q, greptime_timestamp()).unwrap();
1195        let display_string = format!("{}", plan.display_indent());
1196
1197        let ts_col = greptime_timestamp();
1198        let expected = format!(
1199            "Filter: ?table?.{} >= TimestampMillisecond(1000, None) AND ?table?.{} <= TimestampMillisecond(2000, None) AND regexp_match(?table?.job, Utf8(\"^(?s:.*prom.*)$\")) IS NOT NULL AND (?table?.instance != Utf8(\"localhost\") OR ?table?.instance IS NULL) AND ?table?.service.name = Utf8(\"api\") AND ?table?.Region = Utf8(\"us\")\n  TableScan: ?table?",
1200            ts_col, ts_col
1201        );
1202        assert_eq!(expected, display_string);
1203    }
1204
1205    #[tokio::test]
1206    async fn test_remote_read_label_matcher_semantics() {
1207        let ctx = SessionContext::new();
1208        let batch = DfRecordBatch::try_from_iter(vec![
1209            (
1210                greptime_timestamp(),
1211                Arc::new(TimestampMillisecondArray::from(vec![1000; 6])) as ArrayRef,
1212            ),
1213            (
1214                "job",
1215                Arc::new(StringArray::from(vec![
1216                    Some("api"),
1217                    Some("my-api-server"),
1218                    Some(""),
1219                    None,
1220                    Some("web"),
1221                    Some("api\nweb"),
1222                ])) as ArrayRef,
1223            ),
1224            (
1225                "id",
1226                Arc::new(UInt32Array::from(vec![0, 1, 2, 3, 4, 5])) as ArrayRef,
1227            ),
1228        ])
1229        .unwrap();
1230        ctx.register_batch("metrics", batch).unwrap();
1231        for (name, op, value, expected) in [
1232            ("job", MatcherType::Eq, "api", vec![0]),
1233            ("job", MatcherType::Neq, "api", vec![1, 2, 3, 4, 5]),
1234            ("job", MatcherType::Eq, "", vec![2, 3]),
1235            ("job", MatcherType::Neq, "", vec![0, 1, 4, 5]),
1236            ("job", MatcherType::Re, "api", vec![0]),
1237            ("job", MatcherType::Nre, "api", vec![1, 2, 3, 4, 5]),
1238            ("job", MatcherType::Re, "api|web", vec![0, 4]),
1239            ("job", MatcherType::Nre, "api|web", vec![1, 2, 3, 5]),
1240            ("job", MatcherType::Re, "", vec![2, 3]),
1241            ("job", MatcherType::Nre, "", vec![0, 1, 4, 5]),
1242            ("job", MatcherType::Re, ".*", vec![0, 1, 2, 3, 4, 5]),
1243            ("job", MatcherType::Nre, ".*", vec![]),
1244            ("job", MatcherType::Re, ".+", vec![0, 1, 4, 5]),
1245            ("job", MatcherType::Nre, ".+", vec![2, 3]),
1246            ("job", MatcherType::Re, "(?m)api", vec![0]),
1247            ("job", MatcherType::Re, "api.web", vec![5]),
1248            ("job", MatcherType::Nre, "api.web", vec![0, 1, 2, 3, 4]),
1249            ("job", MatcherType::Re, "(?-s:api.web)", vec![]),
1250            ("job", MatcherType::Re, "(api)?", vec![0, 2, 3]),
1251            ("missing", MatcherType::Eq, "", vec![0, 1, 2, 3, 4, 5]),
1252            ("missing", MatcherType::Eq, "us", vec![]),
1253            ("missing", MatcherType::Neq, "us", vec![0, 1, 2, 3, 4, 5]),
1254            ("missing", MatcherType::Neq, "", vec![]),
1255            ("missing", MatcherType::Re, ".*", vec![0, 1, 2, 3, 4, 5]),
1256            ("missing", MatcherType::Re, ".+", vec![]),
1257            ("missing", MatcherType::Nre, ".*", vec![]),
1258            ("missing", MatcherType::Nre, ".+", vec![0, 1, 2, 3, 4, 5]),
1259            ("missing", MatcherType::Re, "", vec![0, 1, 2, 3, 4, 5]),
1260            ("missing", MatcherType::Nre, "", vec![]),
1261        ] {
1262            let query = Query {
1263                start_timestamp_ms: 1000,
1264                end_timestamp_ms: 1000,
1265                matchers: vec![LabelMatcher {
1266                    name: name.into(),
1267                    value: value.into(),
1268                    r#type: op as i32,
1269                }],
1270                ..Default::default()
1271            };
1272            let plan = query_to_plan(
1273                ctx.table("metrics").await.unwrap(),
1274                &query,
1275                greptime_timestamp(),
1276            )
1277            .unwrap_or_else(|err| panic!("{name} {op:?} {value:?}: {err}"));
1278            let batches = ctx
1279                .execute_logical_plan(plan)
1280                .await
1281                .unwrap()
1282                .collect()
1283                .await
1284                .unwrap();
1285            let actual: Vec<u32> = batches
1286                .iter()
1287                .flat_map(|batch| {
1288                    batch
1289                        .column(2)
1290                        .as_primitive::<UInt32Type>()
1291                        .values()
1292                        .to_vec()
1293                })
1294                .collect();
1295            assert_eq!(expected, actual, "{name} {op:?} {value:?}");
1296        }
1297    }
1298
1299    #[tokio::test]
1300    async fn test_remote_read_matcher_combinations_and_label_types() {
1301        let labels = StringArray::from(vec![Some("api"), Some("api"), None, Some("api")]);
1302        for label_type in [
1303            DataType::Utf8,
1304            DataType::LargeUtf8,
1305            DataType::Utf8View,
1306            DataType::Dictionary(Box::new(DataType::UInt32), Box::new(DataType::Utf8)),
1307        ] {
1308            let ctx = SessionContext::new();
1309            let batch = DfRecordBatch::try_from_iter(vec![
1310                (
1311                    greptime_timestamp(),
1312                    Arc::new(TimestampMillisecondArray::from(vec![999, 1000, 2000, 2001]))
1313                        as ArrayRef,
1314                ),
1315                ("Job.Name", casts::cast(&labels, &label_type).unwrap()),
1316            ])
1317            .unwrap();
1318            ctx.register_batch("metrics", batch).unwrap();
1319            let mut cases = vec![
1320                (vec![("Job.Name", MatcherType::Re, ".*")], vec![1000, 2000]),
1321                (vec![("Job.Name", MatcherType::Nre, ".+")], vec![2000]),
1322            ];
1323            // Exercise combinations once; other representations only need regex/NULL coverage.
1324            if label_type == DataType::Utf8 {
1325                cases.extend([
1326                    (
1327                        vec![
1328                            ("Job.Name", MatcherType::Re, "api|web"),
1329                            ("Job.Name", MatcherType::Neq, "web"),
1330                        ],
1331                        vec![1000],
1332                    ),
1333                    (
1334                        vec![
1335                            ("Job.Name", MatcherType::Eq, ""),
1336                            ("Job.Name", MatcherType::Neq, "api"),
1337                        ],
1338                        vec![2000],
1339                    ),
1340                    (
1341                        vec![
1342                            ("Job.Name", MatcherType::Eq, "api"),
1343                            ("Job.Name", MatcherType::Eq, "web"),
1344                        ],
1345                        vec![],
1346                    ),
1347                    (
1348                        vec![
1349                            ("job.name", MatcherType::Eq, ""),
1350                            ("Job.Name", MatcherType::Re, ".*"),
1351                        ],
1352                        vec![1000, 2000],
1353                    ),
1354                ]);
1355            }
1356            for (matchers, expected) in cases {
1357                let mut query = Query {
1358                    start_timestamp_ms: 1000,
1359                    end_timestamp_ms: 2000,
1360                    matchers: matchers
1361                        .iter()
1362                        .map(|(name, op, value)| LabelMatcher {
1363                            name: (*name).into(),
1364                            value: (*value).into(),
1365                            r#type: *op as i32,
1366                        })
1367                        .collect(),
1368                    ..Default::default()
1369                };
1370                query.matchers.push(LabelMatcher {
1371                    name: METRIC_NAME_LABEL.into(),
1372                    value: "metrics".into(),
1373                    r#type: EQ_TYPE,
1374                });
1375                let plan = query_to_plan(
1376                    ctx.table("metrics").await.unwrap(),
1377                    &query,
1378                    greptime_timestamp(),
1379                )
1380                .unwrap();
1381                let batches = ctx
1382                    .execute_logical_plan(plan)
1383                    .await
1384                    .unwrap()
1385                    .collect()
1386                    .await
1387                    .unwrap();
1388                let actual: Vec<i64> = batches
1389                    .iter()
1390                    .flat_map(|batch| {
1391                        batch
1392                            .column(0)
1393                            .as_primitive::<TimestampMillisecondType>()
1394                            .values()
1395                            .to_vec()
1396                    })
1397                    .collect();
1398                assert_eq!(expected, actual, "{label_type:?}: {matchers:?}");
1399            }
1400        }
1401    }
1402
1403    fn column_schemas_with(
1404        mut kts_iter: Vec<(&str, ColumnDataType, SemanticType)>,
1405    ) -> Vec<api::v1::ColumnSchema> {
1406        kts_iter.push((
1407            greptime_value(),
1408            ColumnDataType::Float64,
1409            SemanticType::Field,
1410        ));
1411        kts_iter.push((
1412            greptime_timestamp(),
1413            ColumnDataType::TimestampMillisecond,
1414            SemanticType::Timestamp,
1415        ));
1416
1417        kts_iter
1418            .into_iter()
1419            .map(|(k, t, s)| api::v1::ColumnSchema {
1420                column_name: k.to_string(),
1421                datatype: t as i32,
1422                semantic_type: s as i32,
1423                ..Default::default()
1424            })
1425            .collect()
1426    }
1427
1428    fn make_row_with_label(l1: &str, value: f64, timestamp: i64) -> Row {
1429        Row {
1430            values: vec![
1431                api::v1::Value {
1432                    value_data: Some(api::v1::value::ValueData::StringValue(l1.to_string())),
1433                },
1434                api::v1::Value {
1435                    value_data: Some(api::v1::value::ValueData::F64Value(value)),
1436                },
1437                api::v1::Value {
1438                    value_data: Some(api::v1::value::ValueData::TimestampMillisecondValue(
1439                        timestamp,
1440                    )),
1441                },
1442            ],
1443        }
1444    }
1445
1446    fn make_row_with_2_labels(l1: &str, l2: &str, value: f64, timestamp: i64) -> Row {
1447        Row {
1448            values: vec![
1449                api::v1::Value {
1450                    value_data: Some(api::v1::value::ValueData::StringValue(l1.to_string())),
1451                },
1452                api::v1::Value {
1453                    value_data: Some(api::v1::value::ValueData::StringValue(l2.to_string())),
1454                },
1455                api::v1::Value {
1456                    value_data: Some(api::v1::value::ValueData::F64Value(value)),
1457                },
1458                api::v1::Value {
1459                    value_data: Some(api::v1::value::ValueData::TimestampMillisecondValue(
1460                        timestamp,
1461                    )),
1462                },
1463            ],
1464        }
1465    }
1466
1467    #[test]
1468    fn test_write_request_to_row_insert_exprs() {
1469        let write_request = WriteRequest {
1470            timeseries: mock_timeseries(),
1471            ..Default::default()
1472        };
1473
1474        let mut exprs = to_grpc_row_insert_requests(&write_request)
1475            .unwrap()
1476            .0
1477            .inserts;
1478        exprs.sort_unstable_by(|l, r| l.table_name.cmp(&r.table_name));
1479        assert_eq!(3, exprs.len());
1480        assert_eq!("metric1", exprs[0].table_name);
1481        assert_eq!("metric2", exprs[1].table_name);
1482        assert_eq!("metric3", exprs[2].table_name);
1483
1484        let rows = exprs[0].rows.as_ref().unwrap();
1485        let schema = &rows.schema;
1486        let rows = &rows.rows;
1487        assert_eq!(2, rows.len());
1488        assert_eq!(3, schema.len());
1489        assert_eq!(
1490            column_schemas_with(vec![("job", ColumnDataType::String, SemanticType::Tag)]),
1491            *schema
1492        );
1493        assert_eq!(
1494            &vec![
1495                make_row_with_label("spark", 1.0, 1000),
1496                make_row_with_label("spark", 2.0, 2000),
1497            ],
1498            rows
1499        );
1500
1501        let rows = exprs[1].rows.as_ref().unwrap();
1502        let schema = &rows.schema;
1503        let rows = &rows.rows;
1504        assert_eq!(2, rows.len());
1505        assert_eq!(4, schema.len());
1506        assert_eq!(
1507            column_schemas_with(vec![
1508                ("instance", ColumnDataType::String, SemanticType::Tag),
1509                ("idc", ColumnDataType::String, SemanticType::Tag)
1510            ]),
1511            *schema
1512        );
1513        assert_eq!(
1514            &vec![
1515                make_row_with_2_labels("test_host1", "z001", 3.0, 1000),
1516                make_row_with_2_labels("test_host1", "z001", 4.0, 2000),
1517            ],
1518            rows
1519        );
1520
1521        let rows = exprs[2].rows.as_ref().unwrap();
1522        let schema = &rows.schema;
1523        let rows = &rows.rows;
1524        assert_eq!(3, rows.len());
1525        assert_eq!(4, schema.len());
1526        assert_eq!(
1527            column_schemas_with(vec![
1528                ("idc", ColumnDataType::String, SemanticType::Tag),
1529                ("app", ColumnDataType::String, SemanticType::Tag)
1530            ]),
1531            *schema
1532        );
1533        assert_eq!(
1534            &vec![
1535                make_row_with_2_labels("z002", "biz", 5.0, 1000),
1536                make_row_with_2_labels("z002", "biz", 6.0, 2000),
1537                make_row_with_2_labels("z002", "biz", 7.0, 3000),
1538            ],
1539            rows
1540        );
1541    }
1542
1543    #[test]
1544    fn test_recordbatches_to_timeseries() {
1545        let schema = Arc::new(Schema::new(vec![
1546            ColumnSchema::new(
1547                greptime_timestamp(),
1548                ConcreteDataType::timestamp_millisecond_datatype(),
1549                true,
1550            ),
1551            ColumnSchema::new(greptime_value(), ConcreteDataType::float64_datatype(), true),
1552            ColumnSchema::new("instance", ConcreteDataType::string_datatype(), true),
1553        ]));
1554
1555        let recordbatches = RecordBatches::try_new(
1556            schema.clone(),
1557            vec![
1558                RecordBatch::new(
1559                    schema.clone(),
1560                    vec![
1561                        Arc::new(TimestampMillisecondVector::from_vec(vec![1000])) as _,
1562                        Arc::new(Float64Vector::from_vec(vec![3.0])) as _,
1563                        Arc::new(StringVector::from(vec!["host1"])) as _,
1564                    ],
1565                )
1566                .unwrap(),
1567                RecordBatch::new(
1568                    schema,
1569                    vec![
1570                        Arc::new(TimestampMillisecondVector::from_vec(vec![2000])) as _,
1571                        Arc::new(Float64Vector::from_vec(vec![7.0])) as _,
1572                        Arc::new(StringVector::from(vec!["host2"])) as _,
1573                    ],
1574                )
1575                .unwrap(),
1576            ],
1577        )
1578        .unwrap();
1579
1580        let timeseries = recordbatches_to_timeseries(
1581            "metric1",
1582            greptime_timestamp(),
1583            greptime_value(),
1584            recordbatches,
1585        )
1586        .unwrap();
1587        assert_eq!(2, timeseries.len());
1588
1589        assert_eq!(
1590            vec![
1591                Label {
1592                    name: METRIC_NAME_LABEL.to_string(),
1593                    value: "metric1".to_string(),
1594                },
1595                Label {
1596                    name: "instance".to_string(),
1597                    value: "host1".to_string(),
1598                },
1599            ],
1600            timeseries[0].labels
1601        );
1602
1603        assert_eq!(
1604            timeseries[0].samples,
1605            vec![Sample {
1606                value: 3.0,
1607                timestamp: 1000,
1608            }]
1609        );
1610
1611        assert_eq!(
1612            vec![
1613                Label {
1614                    name: METRIC_NAME_LABEL.to_string(),
1615                    value: "metric1".to_string(),
1616                },
1617                Label {
1618                    name: "instance".to_string(),
1619                    value: "host2".to_string(),
1620                },
1621            ],
1622            timeseries[1].labels
1623        );
1624        assert_eq!(
1625            timeseries[1].samples,
1626            vec![Sample {
1627                value: 7.0,
1628                timestamp: 2000,
1629            }]
1630        );
1631    }
1632
1633    #[test]
1634    fn test_recordbatches_to_timeseries_merges_across_batches() {
1635        let schema = Arc::new(Schema::new(vec![
1636            ColumnSchema::new(
1637                greptime_timestamp(),
1638                ConcreteDataType::timestamp_millisecond_datatype(),
1639                true,
1640            ),
1641            ColumnSchema::new(greptime_value(), ConcreteDataType::float64_datatype(), true),
1642            ColumnSchema::new("instance", ConcreteDataType::string_datatype(), true),
1643        ]));
1644
1645        let recordbatches = RecordBatches::try_new(
1646            schema.clone(),
1647            vec![
1648                RecordBatch::new(
1649                    schema.clone(),
1650                    vec![
1651                        Arc::new(TimestampMillisecondVector::from_vec(vec![3000, 1500])) as _,
1652                        Arc::new(Float64Vector::from_vec(vec![30.0, 15.0])) as _,
1653                        Arc::new(StringVector::from(vec!["host1", "host2"])) as _,
1654                    ],
1655                )
1656                .unwrap(),
1657                RecordBatch::new(
1658                    schema.clone(),
1659                    vec![
1660                        Arc::new(TimestampMillisecondVector::from_vec(vec![1000])) as _,
1661                        Arc::new(Float64Vector::from_vec(vec![10.0])) as _,
1662                        Arc::new(StringVector::from(vec!["host1"])) as _,
1663                    ],
1664                )
1665                .unwrap(),
1666                RecordBatch::new(
1667                    schema,
1668                    vec![
1669                        Arc::new(TimestampMillisecondVector::from_vec(vec![2000, 2500])) as _,
1670                        Arc::new(Float64Vector::from_vec(vec![20.0, 25.0])) as _,
1671                        Arc::new(StringVector::from(vec!["host1", "host2"])) as _,
1672                    ],
1673                )
1674                .unwrap(),
1675            ],
1676        )
1677        .unwrap();
1678
1679        let timeseries = recordbatches_to_timeseries(
1680            "cpu_usage",
1681            greptime_timestamp(),
1682            greptime_value(),
1683            recordbatches,
1684        )
1685        .unwrap();
1686
1687        assert_eq!(2, timeseries.len());
1688
1689        assert_eq!(
1690            vec![
1691                Label {
1692                    name: METRIC_NAME_LABEL.to_string(),
1693                    value: "cpu_usage".to_string(),
1694                },
1695                Label {
1696                    name: "instance".to_string(),
1697                    value: "host1".to_string(),
1698                },
1699            ],
1700            timeseries[0].labels
1701        );
1702        assert_eq!(
1703            vec![
1704                Sample {
1705                    value: 10.0,
1706                    timestamp: 1000,
1707                },
1708                Sample {
1709                    value: 20.0,
1710                    timestamp: 2000,
1711                },
1712                Sample {
1713                    value: 30.0,
1714                    timestamp: 3000,
1715                },
1716            ],
1717            timeseries[0].samples
1718        );
1719
1720        assert_eq!(
1721            vec![
1722                Label {
1723                    name: METRIC_NAME_LABEL.to_string(),
1724                    value: "cpu_usage".to_string(),
1725                },
1726                Label {
1727                    name: "instance".to_string(),
1728                    value: "host2".to_string(),
1729                },
1730            ],
1731            timeseries[1].labels
1732        );
1733        assert_eq!(
1734            vec![
1735                Sample {
1736                    value: 15.0,
1737                    timestamp: 1500,
1738                },
1739                Sample {
1740                    value: 25.0,
1741                    timestamp: 2500,
1742                },
1743            ],
1744            timeseries[1].samples
1745        );
1746    }
1747
1748    #[test]
1749    fn test_recordbatches_to_timeseries_borrows_and_groups_dictionary_labels() {
1750        let arrow_schema = Arc::new(ArrowSchema::new(vec![
1751            Field::new(
1752                greptime_timestamp(),
1753                ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
1754                false,
1755            ),
1756            Field::new(greptime_value(), ArrowDataType::Float64, false),
1757            Field::new_dictionary("instance", ArrowDataType::UInt32, ArrowDataType::Utf8, true),
1758        ]));
1759        let schema = Arc::new(Schema::try_from(arrow_schema.clone()).unwrap());
1760        let instance = DictionaryArray::<UInt32Type>::new(
1761            UInt32Array::from(vec![Some(0), None, Some(1), Some(0), Some(2)]),
1762            Arc::new(StringArray::from(vec![Some("host2"), Some("host1"), None])),
1763        );
1764        let batch = DfRecordBatch::try_new(
1765            arrow_schema,
1766            vec![
1767                Arc::new(TimestampMillisecondArray::from(vec![
1768                    1000, 2000, 3000, 4000, 5000,
1769                ])),
1770                Arc::new(Float64Array::from(vec![1.0, 2.0, 3.0, 4.0, 5.0])),
1771                Arc::new(instance),
1772            ],
1773        )
1774        .unwrap();
1775        let recordbatch = RecordBatch::from_df_record_batch(schema.clone(), batch);
1776        let columns = label_columns(&recordbatch, greptime_timestamp(), greptime_value()).unwrap();
1777        assert!(matches!(
1778            columns[0].values,
1779            LabelValues::DictionaryUtf8 { .. }
1780        ));
1781        drop(columns);
1782        let recordbatches = RecordBatches::try_new(schema, vec![recordbatch]).unwrap();
1783
1784        let timeseries = recordbatches_to_timeseries(
1785            "metric1",
1786            greptime_timestamp(),
1787            greptime_value(),
1788            recordbatches,
1789        )
1790        .unwrap();
1791
1792        assert_eq!(3, timeseries.len());
1793        assert_eq!(
1794            vec![Label {
1795                name: METRIC_NAME_LABEL.to_string(),
1796                value: "metric1".to_string(),
1797            }],
1798            timeseries[0].labels
1799        );
1800        assert_eq!(
1801            vec![
1802                Sample {
1803                    value: 2.0,
1804                    timestamp: 2000,
1805                },
1806                Sample {
1807                    value: 5.0,
1808                    timestamp: 5000,
1809                },
1810            ],
1811            timeseries[0].samples
1812        );
1813        assert_eq!("host1", timeseries[1].labels[1].value);
1814        assert_eq!(
1815            vec![Sample {
1816                value: 3.0,
1817                timestamp: 3000,
1818            }],
1819            timeseries[1].samples
1820        );
1821        assert_eq!("host2", timeseries[2].labels[1].value);
1822        assert_eq!(
1823            vec![
1824                Sample {
1825                    value: 1.0,
1826                    timestamp: 1000,
1827                },
1828                Sample {
1829                    value: 4.0,
1830                    timestamp: 4000,
1831                },
1832            ],
1833            timeseries[2].samples
1834        );
1835    }
1836
1837    #[test]
1838    fn test_recordbatches_to_timeseries_groups_non_contiguous_series() {
1839        let schema = Arc::new(Schema::new(vec![
1840            ColumnSchema::new(
1841                greptime_timestamp(),
1842                ConcreteDataType::timestamp_millisecond_datatype(),
1843                true,
1844            ),
1845            ColumnSchema::new(greptime_value(), ConcreteDataType::float64_datatype(), true),
1846            ColumnSchema::new("instance", ConcreteDataType::string_datatype(), true),
1847        ]));
1848        let recordbatch = RecordBatch::new(
1849            schema.clone(),
1850            vec![
1851                Arc::new(TimestampMillisecondVector::from_vec(vec![1000, 2000, 3000])) as _,
1852                Arc::new(Float64Vector::from_vec(vec![1.0, 2.0, 3.0])) as _,
1853                Arc::new(StringVector::from(vec!["host2", "host1", "host2"])) as _,
1854            ],
1855        )
1856        .unwrap();
1857
1858        let recordbatches = RecordBatches::try_new(schema, vec![recordbatch]).unwrap();
1859        let timeseries = recordbatches_to_timeseries(
1860            "metric1",
1861            greptime_timestamp(),
1862            greptime_value(),
1863            recordbatches,
1864        )
1865        .unwrap();
1866
1867        // The result stays sorted by labels as it was with the previous BTreeMap.
1868        assert_eq!("host1", timeseries[0].labels[1].value);
1869        assert_eq!("host2", timeseries[1].labels[1].value);
1870        assert_eq!(
1871            vec![
1872                Sample {
1873                    value: 1.0,
1874                    timestamp: 1000,
1875                },
1876                Sample {
1877                    value: 3.0,
1878                    timestamp: 3000,
1879                },
1880            ],
1881            timeseries[1].samples
1882        );
1883    }
1884
1885    #[test]
1886    fn test_recordbatches_to_timeseries_arrow_label_types_and_nulls() {
1887        let schema = Arc::new(Schema::new(vec![
1888            ColumnSchema::new(
1889                greptime_timestamp(),
1890                ConcreteDataType::timestamp_millisecond_datatype(),
1891                true,
1892            ),
1893            ColumnSchema::new(greptime_value(), ConcreteDataType::float64_datatype(), true),
1894            ColumnSchema::new("instance", ConcreteDataType::large_string_datatype(), true),
1895            ColumnSchema::new("zone", ConcreteDataType::utf8_view_datatype(), true),
1896            ColumnSchema::new("shard", ConcreteDataType::int32_datatype(), true),
1897        ]));
1898        let recordbatch = RecordBatch::new(
1899            schema.clone(),
1900            vec![
1901                Arc::new(TimestampMillisecondVector::from_vec(vec![1000, 2000, 3000])) as _,
1902                Arc::new(Float64Vector::from_vec(vec![1.0, 2.0, 3.0])) as _,
1903                Arc::new(StringVector::from(LargeStringArray::from(vec![
1904                    "host2", "host1", "host2",
1905                ]))) as _,
1906                Arc::new(StringVector::from(StringViewArray::from(vec![
1907                    Some("west"),
1908                    None,
1909                    Some("west"),
1910                ]))) as _,
1911                Arc::new(Int32Vector::from_vec(vec![2, 1, 2])) as _,
1912            ],
1913        )
1914        .unwrap();
1915
1916        let recordbatches = RecordBatches::try_new(schema, vec![recordbatch]).unwrap();
1917        let timeseries = recordbatches_to_timeseries(
1918            "metric1",
1919            greptime_timestamp(),
1920            greptime_value(),
1921            recordbatches,
1922        )
1923        .unwrap();
1924
1925        assert_eq!(2, timeseries.len());
1926        assert_eq!(
1927            vec![
1928                new_label(METRIC_NAME_LABEL.to_string(), "metric1".to_string()),
1929                new_label("instance".to_string(), "host1".to_string()),
1930                new_label("shard".to_string(), "1".to_string()),
1931            ],
1932            timeseries[0].labels
1933        );
1934        assert_eq!(
1935            vec![
1936                new_label(METRIC_NAME_LABEL.to_string(), "metric1".to_string()),
1937                new_label("instance".to_string(), "host2".to_string()),
1938                new_label("zone".to_string(), "west".to_string()),
1939                new_label("shard".to_string(), "2".to_string()),
1940            ],
1941            timeseries[1].labels
1942        );
1943        assert_eq!(
1944            vec![
1945                Sample {
1946                    value: 1.0,
1947                    timestamp: 1000,
1948                },
1949                Sample {
1950                    value: 3.0,
1951                    timestamp: 3000,
1952                },
1953            ],
1954            timeseries[1].samples
1955        );
1956    }
1957
1958    #[tokio::test]
1959    async fn test_snappy_varint_bomb_is_rejected_before_allocation() {
1960        // A five-byte raw Snappy header declaring a ~4 GiB decoded length.
1961        // Regression test for the compression-bomb finding: the declared size
1962        // must be rejected by the cap check before any buffer is allocated.
1963        let bomb = [0xffu8, 0xff, 0xff, 0xff, 0x0f];
1964        let limiter = ServerMemoryLimiter::default();
1965
1966        let err = snappy_decompress_limited(&bomb, MAX_DECOMPRESSED_REQUEST_SIZE, &limiter)
1967            .await
1968            .unwrap_err();
1969        assert!(matches!(
1970            err,
1971            crate::error::Error::DecompressedBodyTooLarge { .. }
1972        ));
1973        assert_eq!(0, limiter.used_bytes());
1974    }
1975
1976    #[tokio::test]
1977    async fn test_snappy_decompress_limited_roundtrip() {
1978        let raw = vec![b'a'; 1024 * 1024];
1979        let mut encoder = snap::raw::Encoder::new();
1980        let compressed = encoder.compress_vec(&raw).unwrap();
1981        let limiter = ServerMemoryLimiter::default();
1982
1983        let decoded =
1984            snappy_decompress_limited(&compressed, MAX_DECOMPRESSED_REQUEST_SIZE, &limiter)
1985                .await
1986                .unwrap();
1987        assert_eq!(&decoded[..], &raw[..]);
1988    }
1989
1990    #[tokio::test]
1991    async fn test_snappy_charges_and_releases_quota() {
1992        let raw = vec![b'a'; 64 * 1024];
1993        let mut encoder = snap::raw::Encoder::new();
1994        let compressed = encoder.compress_vec(&raw).unwrap();
1995        let limiter = ServerMemoryLimiter::new(128 * 1024, OnExhaustedPolicy::Fail);
1996
1997        let decoded =
1998            snappy_decompress_limited(&compressed, MAX_DECOMPRESSED_REQUEST_SIZE, &limiter)
1999                .await
2000                .unwrap();
2001        // Rounding to the 1 KiB permit granularity.
2002        assert!(limiter.used_bytes() >= 64 * 1024);
2003        assert!(limiter.used_bytes() <= 65 * 1024);
2004        drop(decoded);
2005        assert_eq!(0, limiter.used_bytes());
2006    }
2007
2008    #[tokio::test]
2009    async fn test_snappy_rejects_when_quota_exhausted() {
2010        let raw = vec![b'a'; 64 * 1024];
2011        let mut encoder = snap::raw::Encoder::new();
2012        let compressed = encoder.compress_vec(&raw).unwrap();
2013        // Quota smaller than the decoded size.
2014        let limiter = ServerMemoryLimiter::new(1024, OnExhaustedPolicy::Fail);
2015
2016        let err = snappy_decompress_limited(&compressed, MAX_DECOMPRESSED_REQUEST_SIZE, &limiter)
2017            .await
2018            .unwrap_err();
2019        assert!(matches!(
2020            err,
2021            crate::error::Error::MemoryLimitExceeded { .. }
2022        ));
2023        assert_eq!(0, limiter.used_bytes(), "failed charge must be released");
2024    }
2025
2026    #[tokio::test]
2027    async fn test_zstd_at_and_over_the_cap() {
2028        let limiter = ServerMemoryLimiter::default();
2029        let small_cap = 8 * 1024;
2030
2031        // Exactly at the cap: accepted.
2032        let raw = vec![0u8; small_cap];
2033        let compressed = zstd::stream::encode_all(&raw[..], 3).unwrap();
2034        let decoded = zstd_decompress_limited(&compressed, small_cap, &limiter)
2035            .await
2036            .unwrap();
2037        assert_eq!(decoded.len(), small_cap);
2038
2039        // One byte past the cap: rejected.
2040        let raw = vec![0u8; small_cap + 1];
2041        let compressed = zstd::stream::encode_all(&raw[..], 3).unwrap();
2042        let err = zstd_decompress_limited(&compressed, small_cap, &limiter)
2043            .await
2044            .unwrap_err();
2045        assert!(matches!(
2046            err,
2047            crate::error::Error::DecompressedBodyTooLarge { .. }
2048        ));
2049    }
2050
2051    #[tokio::test]
2052    async fn test_zstd_charges_incrementally() {
2053        // 64 KiB wire that expands to 2 MiB must be charged chunk by chunk
2054        // against a quota that only fits the expanded size.
2055        let raw = vec![0u8; 2 * 1024 * 1024];
2056        let compressed = zstd::stream::encode_all(&raw[..], 19).unwrap();
2057        assert!(compressed.len() < 16 * 1024);
2058        let limiter = ServerMemoryLimiter::new(4 * 1024 * 1024, OnExhaustedPolicy::Fail);
2059
2060        let decoded = zstd_decompress_limited(&compressed, MAX_DECOMPRESSED_REQUEST_SIZE, &limiter)
2061            .await
2062            .unwrap();
2063        assert_eq!(decoded.len(), raw.len());
2064        assert!(limiter.used_bytes() >= raw.len() as u64);
2065        drop(decoded);
2066        assert_eq!(0, limiter.used_bytes());
2067    }
2068
2069    #[test]
2070    fn test_snappy_response_decompress_is_capped() {
2071        // The response-side (uncharged) helper must enforce the cap too.
2072        let bomb = [0xffu8, 0xff, 0xff, 0xff, 0x0f];
2073        let err = snappy_decompress(&bomb).unwrap_err();
2074        assert!(matches!(
2075            err,
2076            crate::error::Error::DecompressedBodyTooLarge { .. }
2077        ));
2078    }
2079}