Skip to main content

servers/http/
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
15use std::sync::Arc;
16
17use api::prom_store::remote::ReadRequest;
18use api::v1::RowInsertRequests;
19use async_trait::async_trait;
20use axum::Extension;
21use axum::body::Bytes;
22use axum::extract::{Query, State};
23use axum::http::{Extensions, HeaderMap, HeaderValue, StatusCode, header};
24use axum::response::IntoResponse;
25use axum_extra::TypedHeader;
26use common_catalog::consts::DEFAULT_SCHEMA_NAME;
27use common_query::prelude::GREPTIME_PHYSICAL_TABLE;
28use common_telemetry::tracing;
29use mime_guess::mime;
30use pipeline::util::to_pipeline_version;
31use pipeline::{ContextReq, PipelineDefinition};
32use prometheus::HistogramTimer;
33use prost::Message;
34use serde::{Deserialize, Serialize};
35use session::context::{Channel, QueryContext, QueryContextRef};
36use snafu::prelude::*;
37use table::requests::{
38    METADATA_QUALITY_INFERRED, SEMANTIC_METRIC_METADATA_QUALITY, SEMANTIC_PER_TABLE_INDEX_KEY,
39    SEMANTIC_SIGNAL_TYPE, SEMANTIC_SOURCE, SEMANTIC_SOURCE_VERSION, SIGNAL_TYPE_METRIC,
40    SOURCE_PROMETHEUS,
41};
42
43use crate::batcher::logical_table::LogicalTablePendingRowsBatcher;
44use crate::error::{self, InternalSnafu, PipelineSnafu, Result};
45use crate::http::extractor::PipelineInfo;
46use crate::http::header::{
47    CONTENT_TYPE_PROTOBUF_STR, GREPTIME_DB_HEADER_METRICS, write_cost_header_map,
48};
49use crate::http::memory_limit::BodyMemoryAccounting;
50use crate::prom_remote_write::decode::PromSeriesProcessor;
51use crate::prom_remote_write::v2::decode_remote_write_v2;
52use crate::prom_remote_write::validation::PromValidationMode;
53use crate::prom_remote_write::{
54    REMOTE_WRITE_V1_VERSION, REMOTE_WRITE_V2_VERSION, decode_remote_write_request,
55};
56use crate::prom_store::{MAX_DECOMPRESSED_REQUEST_SIZE, snappy_decompress_limited};
57use crate::query_handler::{PipelineHandlerRef, PromStoreProtocolHandlerRef, PromStoreResponse};
58use crate::request_memory_limiter::ServerMemoryLimiter;
59
60pub const PHYSICAL_TABLE_PARAM: &str = "physical_table";
61pub const DEFAULT_ENCODING: &str = "snappy";
62pub const VM_ENCODING: &str = "zstd";
63pub const VM_PROTO_VERSION: &str = "1";
64const REMOTE_WRITE_V1_PROTO: &str = "prometheus.WriteRequest";
65const REMOTE_WRITE_V2_PROTO: &str = "io.prometheus.write.v2.Request";
66const CONTENT_TYPE_PROTO_PARAM: &str = "proto";
67const REMOTE_WRITE_V2_SAMPLES_WRITTEN_HEADER: &str = "x-prometheus-remote-write-samples-written";
68const REMOTE_WRITE_V2_HISTOGRAMS_WRITTEN_HEADER: &str =
69    "x-prometheus-remote-write-histograms-written";
70const REMOTE_WRITE_V2_EXEMPLARS_WRITTEN_HEADER: &str =
71    "x-prometheus-remote-write-exemplars-written";
72
73#[derive(Clone)]
74pub struct PromStoreState {
75    pub prom_store_handler: PromStoreProtocolHandlerRef,
76    pub pipeline_handler: Option<PipelineHandlerRef>,
77    pub prom_store_with_metric_engine: bool,
78    pub prom_validation_mode: PromValidationMode,
79    pub pending_rows_batcher: Option<Arc<LogicalTablePendingRowsBatcher>>,
80    /// Shared request-memory limiter used to charge decompressed remote
81    /// read/write bodies against the aggregate quota.
82    pub memory_limiter: ServerMemoryLimiter,
83}
84
85#[derive(Debug, Serialize, Deserialize)]
86pub struct RemoteWriteQuery {
87    pub db: Option<String>,
88    /// Specify which physical table to use for storing metrics.
89    /// This only works on remote write requests.
90    pub physical_table: Option<String>,
91    /// For VictoriaMetrics modified remote write protocol
92    pub get_vm_proto_version: Option<String>,
93}
94
95impl Default for RemoteWriteQuery {
96    fn default() -> RemoteWriteQuery {
97        Self {
98            db: Some(DEFAULT_SCHEMA_NAME.to_string()),
99            physical_table: Some(GREPTIME_PHYSICAL_TABLE.to_string()),
100            get_vm_proto_version: None,
101        }
102    }
103}
104
105#[axum_macros::debug_handler]
106#[tracing::instrument(
107    skip_all,
108    fields(protocol = "prometheus", request_type = "remote_write")
109)]
110pub async fn remote_write(
111    State(state): State<PromStoreState>,
112    Query(params): Query<RemoteWriteQuery>,
113    (Extension(query_ctx), extensions): (Extension<QueryContext>, Extensions),
114    content_type: Option<TypedHeader<headers::ContentType>>,
115    pipeline_info: PipelineInfo,
116    content_encoding: TypedHeader<headers::ContentEncoding>,
117    body: Bytes,
118) -> Result<axum::response::Response> {
119    let is_zstd = content_encoding.contains(VM_ENCODING);
120
121    match remote_write_proto(content_type) {
122        RemoteWriteProto::V1 => {
123            remote_write_v1(state, params, query_ctx, pipeline_info, is_zstd, body).await
124        }
125        RemoteWriteProto::V2 => {
126            if let Some(response) = unsupported_remote_write_v2_encoding_response(&content_encoding)
127            {
128                return Ok(response);
129            }
130            // Read after Bytes has collected every frame, including chunked-body reservations.
131            let body_reserved_bytes = extensions
132                .get::<BodyMemoryAccounting>()
133                .map_or(0, BodyMemoryAccounting::reserved_bytes);
134            remote_write_v2(
135                state,
136                params,
137                query_ctx,
138                pipeline_info,
139                is_zstd,
140                body,
141                body_reserved_bytes,
142            )
143            .await
144        }
145        RemoteWriteProto::Unsupported(content_type) => Ok((
146            StatusCode::UNSUPPORTED_MEDIA_TYPE,
147            format!("unsupported prometheus remote write content type: {content_type}"),
148        )
149            .into_response()),
150    }
151}
152
153async fn remote_write_v1(
154    state: PromStoreState,
155    params: RemoteWriteQuery,
156    query_ctx: QueryContext,
157    pipeline_info: PipelineInfo,
158    is_zstd: bool,
159    body: Bytes,
160) -> Result<axum::response::Response> {
161    let PromStoreState {
162        prom_store_handler,
163        pipeline_handler,
164        prom_store_with_metric_engine,
165        prom_validation_mode,
166        pending_rows_batcher,
167        memory_limiter,
168    } = state;
169
170    if let Some(response) = vm_proto_version_response(&params) {
171        return Ok(response);
172    }
173
174    let (db, query_ctx, _timer) =
175        prepare_remote_write_context(&params, query_ctx, REMOTE_WRITE_V1_VERSION);
176    let query_ctx = Arc::new(query_ctx);
177
178    let mut processor = PromSeriesProcessor::default_processor();
179
180    if let Some(pipeline_name) = pipeline_info.pipeline_name {
181        let pipeline_def = PipelineDefinition::from_name(
182            &pipeline_name,
183            to_pipeline_version(pipeline_info.pipeline_version.as_deref())
184                .context(PipelineSnafu)?,
185            None,
186        )
187        .context(PipelineSnafu)?;
188        let pipeline_handler = pipeline_handler.context(InternalSnafu {
189            err_msg: "pipeline handler is not set".to_string(),
190        })?;
191
192        processor.set_pipeline(pipeline_handler, query_ctx.clone(), pipeline_def);
193    }
194
195    // Keep the decode guards alive until the batches are built: the
196    // `TablesBuilder` retains the decompressed buffer as its raw data.
197    let (mut decoded, decode_guards) = decode_remote_write_request(
198        is_zstd,
199        body,
200        prom_validation_mode,
201        &mut processor,
202        &memory_limiter,
203    )
204    .await?;
205
206    // Parsing borrows the decode buffer, but row building copies out of it: tag
207    // values through `decode_string`, column names through `to_owned`, and the
208    // borrowing `col_indexes` dies inside `as_insert_requests`. Nothing below
209    // references the buffer, so it need not span the write — the same goes for
210    // the decode guards charged against the aggregate memory quota.
211    let req = if processor.use_pipeline {
212        drop(decoded);
213        processor.exec_pipeline().await?
214    } else {
215        let req = decoded.as_insert_requests();
216        drop(decoded);
217        req
218    };
219    drop(decode_guards);
220    let batches = into_prom_write_batches(req, query_ctx);
221
222    let outcome = match write_prometheus_rows_with_progress(
223        prom_store_handler,
224        pending_rows_batcher,
225        prom_store_with_metric_engine,
226        batches,
227    )
228    .await
229    {
230        Ok(outcome) => outcome,
231        Err(error) => {
232            record_remote_write_samples(&db, REMOTE_WRITE_V1_VERSION, error.rows_written);
233            return Err(error.error);
234        }
235    };
236    record_remote_write_samples(&db, REMOTE_WRITE_V1_VERSION, outcome.rows_written);
237
238    Ok((
239        StatusCode::NO_CONTENT,
240        write_cost_header_map(outcome.write_cost),
241    )
242        .into_response())
243}
244
245async fn remote_write_v2(
246    state: PromStoreState,
247    params: RemoteWriteQuery,
248    query_ctx: QueryContext,
249    pipeline_info: PipelineInfo,
250    is_zstd: bool,
251    body: Bytes,
252    body_reserved_bytes: u64,
253) -> Result<axum::response::Response> {
254    let PromStoreState {
255        prom_store_handler,
256        pipeline_handler: _,
257        prom_store_with_metric_engine,
258        prom_validation_mode: _,
259        pending_rows_batcher,
260        memory_limiter,
261    } = state;
262
263    if let Some(response) = vm_proto_version_response(&params) {
264        return Ok(response);
265    }
266
267    // Pipeline processing is not supported for remote write v2 yet. Ignore the
268    // optional pipeline parameter and ingest samples directly.
269    let _ = pipeline_info;
270
271    let (db, mut query_ctx, _timer) =
272        prepare_remote_write_context(&params, query_ctx, REMOTE_WRITE_V2_VERSION);
273
274    let req =
275        match decode_remote_write_v2(is_zstd, body, &memory_limiter, body_reserved_bytes).await {
276            Ok(req) => req,
277            Err(error) => return Ok(remote_write_v2_error_response(error, 0, 0, 0)),
278        };
279    // The v2 per-series metadata upgrades the written tables' semantic options
280    // (metric type/unit, declared quality) at auto-create time.
281    if let Some(index) = req.semantic_index.encode(&query_ctx.current_schema()) {
282        query_ctx.set_extension(SEMANTIC_PER_TABLE_INDEX_KEY, index);
283    }
284    let query_ctx = Arc::new(query_ctx);
285    let (sample_count, histogram_count) = (req.sample_count, req.histogram_count);
286    let mut sample_batches = into_prom_write_batches(req.samples, query_ctx.clone());
287    let mut histogram_batches = into_prom_write_batches(req.histograms, query_ctx);
288    for (batches, histogram) in [(&mut sample_batches, false), (&mut histogram_batches, true)] {
289        let counter = if histogram {
290            crate::metrics::PROM_STORE_REMOTE_WRITE_HISTOGRAMS
291                .with_label_values(&[&db, REMOTE_WRITE_V2_VERSION])
292        } else {
293            crate::metrics::PROM_STORE_REMOTE_WRITE_SAMPLES
294                .with_label_values(&[&db, REMOTE_WRITE_V2_VERSION])
295        };
296        for (ctx, _) in batches {
297            let counter = counter.clone();
298            let progress =
299                session::write_progress::WriteProgress::with_reservation(req.memory_guard.clone())
300                    .with_observer(move |rows| counter.inc_by(rows));
301            let mut fork = ctx.fork();
302            fork.set_write_progress(Arc::new(progress));
303            *ctx = Arc::new(fork);
304        }
305    }
306    let outcome = match write_prometheus_v2_rows_with_progress(
307        prom_store_handler,
308        pending_rows_batcher,
309        prom_store_with_metric_engine,
310        sample_batches,
311        histogram_batches,
312    )
313    .await
314    {
315        Ok(outcome) => outcome,
316        Err(error) => {
317            return Ok(remote_write_v2_error_response(
318                error.error,
319                error.samples_written,
320                error.histograms_written,
321                0,
322            ));
323        }
324    };
325    debug_assert!(outcome.samples_written <= sample_count);
326    debug_assert!(outcome.histograms_written <= histogram_count);
327    let mut headers = write_cost_header_map(outcome.write_cost);
328    append_remote_write_v2_written_headers(
329        &mut headers,
330        outcome.samples_written,
331        outcome.histograms_written,
332        0,
333    );
334
335    let status = if outcome.accepted {
336        StatusCode::ACCEPTED
337    } else {
338        StatusCode::NO_CONTENT
339    };
340    Ok((status, headers).into_response())
341}
342
343fn vm_proto_version_response(params: &RemoteWriteQuery) -> Option<axum::response::Response> {
344    params
345        .get_vm_proto_version
346        .as_ref()
347        .map(|_| VM_PROTO_VERSION.into_response())
348}
349
350/// Returns the context still un-shared so the caller can attach
351/// request-derived extensions (the v2 per-table metadata index) before
352/// wrapping it in an `Arc`.
353fn prepare_remote_write_context(
354    params: &RemoteWriteQuery,
355    mut query_ctx: QueryContext,
356    remote_write_version: &str,
357) -> (String, QueryContext, HistogramTimer) {
358    let db = params.db.clone().unwrap_or_default();
359    query_ctx.set_channel(Channel::Prometheus);
360    let physical_table = params
361        .physical_table
362        .clone()
363        .unwrap_or_else(|| GREPTIME_PHYSICAL_TABLE.to_string());
364    query_ctx.set_extension(PHYSICAL_TABLE_PARAM, physical_table);
365    // Stamp the Prometheus metric identity here, before `as_req_iter` splits into the
366    // batched and direct write paths, so both inherit it (the batched path bypasses
367    // `PromStoreProtocolHandler::write`). Prometheus remote-write metadata is weak
368    // here, so the type is inferred from naming; v2 upgrades tables whose series
369    // carry inline metadata via the per-table index.
370    query_ctx.set_extension(SEMANTIC_SIGNAL_TYPE, SIGNAL_TYPE_METRIC);
371    query_ctx.set_extension(SEMANTIC_SOURCE, SOURCE_PROMETHEUS);
372    query_ctx.set_extension(SEMANTIC_SOURCE_VERSION, remote_write_version);
373    query_ctx.set_extension(SEMANTIC_METRIC_METADATA_QUALITY, METADATA_QUALITY_INFERRED);
374    let timer = crate::metrics::METRIC_HTTP_PROM_STORE_WRITE_ELAPSED
375        .with_label_values(&[db.as_str(), remote_write_version])
376        .start_timer();
377
378    (db, query_ctx, timer)
379}
380
381struct PromWriteOutcome {
382    write_cost: usize,
383    rows_written: u64,
384}
385
386struct PromWriteError {
387    error: error::Error,
388    rows_written: u64,
389}
390
391struct PromWriteV2Outcome {
392    accepted: bool,
393    write_cost: usize,
394    samples_written: u64,
395    histograms_written: u64,
396}
397
398struct PromWriteV2Error {
399    error: error::Error,
400    samples_written: u64,
401    histograms_written: u64,
402}
403
404type PromWriteBatch = (QueryContextRef, RowInsertRequests);
405
406#[async_trait]
407trait PromWriteBatcher: Send + Sync {
408    async fn submit(&self, requests: RowInsertRequests, ctx: QueryContextRef) -> Result<u64>;
409}
410
411#[async_trait]
412impl PromWriteBatcher for LogicalTablePendingRowsBatcher {
413    async fn submit(&self, requests: RowInsertRequests, ctx: QueryContextRef) -> Result<u64> {
414        LogicalTablePendingRowsBatcher::submit(self, requests, ctx).await
415    }
416}
417
418fn into_prom_write_batches(req: ContextReq, query_ctx: QueryContextRef) -> Vec<PromWriteBatch> {
419    req.as_req_iter(query_ctx).collect()
420}
421
422async fn preflight_prometheus_rows(
423    prom_store_handler: &PromStoreProtocolHandlerRef,
424    batches: &mut [PromWriteBatch],
425) -> Result<()> {
426    for (ctx, reqs) in batches.iter_mut() {
427        prom_store_handler.pre_write(reqs, ctx.clone()).await?;
428        // Detach from context clones retained by pre-write hooks so the checked
429        // schema cannot change before this prepared batch is written.
430        *ctx = Arc::new(ctx.fork());
431    }
432    operator::insert::admit_row_insert_batches(batches)
433        .await
434        .map_err(common_error::ext::BoxedError::new)
435        .context(error::ExecuteGrpcQuerySnafu)?;
436    Ok(())
437}
438
439/// Writes preflighted PRW batches and keeps the number of persisted rows on error.
440///
441/// The v2 handler uses that partial progress to return Prometheus' written
442/// sample/histogram headers even when a later table write fails.
443async fn write_prometheus_rows_with_progress(
444    prom_store_handler: PromStoreProtocolHandlerRef,
445    pending_rows_batcher: Option<Arc<LogicalTablePendingRowsBatcher>>,
446    prom_store_with_metric_engine: bool,
447    mut batches: Vec<PromWriteBatch>,
448) -> std::result::Result<PromWriteOutcome, PromWriteError> {
449    if prom_store_with_metric_engine && let Some(batcher) = pending_rows_batcher {
450        // Preflight before the bulk eligibility decision: pre_write hooks
451        // may redirect the contexts (e.g. to a per-tenant schema), so
452        // eligibility must be evaluated against the prepared batches —
453        // and the fallback below reuses them without re-running hooks or
454        // admission.
455        preflight_prometheus_rows(&prom_store_handler, &mut batches)
456            .await
457            .map_err(|error| PromWriteError {
458                error,
459                rows_written: 0,
460            })?;
461        // Destinations bound to another physical table must stay on the
462        // ordinary insert path, which routes per destination; time index
463        // units need no check — the bulk encode converts each request to
464        // its destination's unit.
465        if batcher.accepts_bulk_destinations(batches.iter()).await {
466            let mut rows_written = 0;
467            for (temp_ctx, reqs) in batches {
468                let rows =
469                    batcher
470                        .submit(reqs, temp_ctx)
471                        .await
472                        .map_err(|error| PromWriteError {
473                            error,
474                            rows_written,
475                        })?;
476                rows_written += rows;
477            }
478            return Ok(PromWriteOutcome {
479                write_cost: 0,
480                rows_written,
481            });
482        }
483        return write_prepared_prometheus_rows_with_progress(
484            prom_store_handler,
485            batches,
486            prom_store_with_metric_engine,
487        )
488        .await;
489    }
490
491    let row_counts = batches
492        .iter()
493        .map(|(_, request)| prom_write_row_count(request))
494        .collect::<Vec<_>>();
495    let batch_count = batches.len();
496    let outputs = prom_store_handler
497        .write_all(batches, prom_store_with_metric_engine)
498        .await
499        .map_err(|error| PromWriteError {
500            error,
501            rows_written: 0,
502        })?;
503    let output_count = outputs.len();
504    let mut write_cost = 0;
505    let mut rows_written = 0;
506    for (output, rows) in outputs.into_iter().zip(row_counts) {
507        let output = output.map_err(|error| PromWriteError {
508            error,
509            rows_written,
510        })?;
511        write_cost += output.meta.cost;
512        rows_written += rows;
513    }
514    if output_count != batch_count {
515        return Err(PromWriteError {
516            error: incomplete_prom_write_error(),
517            rows_written,
518        });
519    }
520
521    Ok(PromWriteOutcome {
522        write_cost,
523        rows_written,
524    })
525}
526
527/// Writes already-preflighted batches through the ordinary (prepared) write
528/// path; the pre_write hooks and admission have already run.
529async fn write_prepared_prometheus_rows_with_progress(
530    prom_store_handler: PromStoreProtocolHandlerRef,
531    batches: Vec<PromWriteBatch>,
532    prom_store_with_metric_engine: bool,
533) -> std::result::Result<PromWriteOutcome, PromWriteError> {
534    let mut write_cost = 0;
535    let mut rows_written = 0;
536    for (ctx, request) in batches {
537        let rows = prom_write_row_count(&request);
538        let output = prom_store_handler
539            .write_prepared(request, ctx, prom_store_with_metric_engine)
540            .await
541            .map_err(|error| PromWriteError {
542                error,
543                rows_written,
544            })?;
545        write_cost += output.meta.cost;
546        rows_written += rows;
547    }
548    Ok(PromWriteOutcome {
549        write_cost,
550        rows_written,
551    })
552}
553
554async fn write_prometheus_v2_rows_with_progress(
555    prom_store_handler: PromStoreProtocolHandlerRef,
556    pending_rows_batcher: Option<Arc<LogicalTablePendingRowsBatcher>>,
557    prom_store_with_metric_engine: bool,
558    sample_batches: Vec<PromWriteBatch>,
559    histogram_batches: Vec<PromWriteBatch>,
560) -> std::result::Result<PromWriteV2Outcome, PromWriteV2Error> {
561    let sample_batch_count = sample_batches.len();
562    let mut batches = sample_batches;
563    batches.extend(histogram_batches);
564    preflight_prometheus_rows(&prom_store_handler, &mut batches)
565        .await
566        .map_err(|error| PromWriteV2Error {
567            error,
568            samples_written: 0,
569            histograms_written: 0,
570        })?;
571    let batcher = if prom_store_with_metric_engine
572        && let Some(batcher) = pending_rows_batcher.as_ref()
573        && batcher.accepts_bulk_destinations(batches.iter()).await
574    {
575        Some(batcher.as_ref() as &dyn PromWriteBatcher)
576    } else {
577        None
578    };
579    write_batched_prometheus_v2_rows_with_progress(
580        prom_store_handler,
581        batcher,
582        prom_store_with_metric_engine,
583        sample_batch_count,
584        batches,
585    )
586    .await
587}
588
589async fn write_batched_prometheus_v2_rows_with_progress(
590    prom_store_handler: PromStoreProtocolHandlerRef,
591    batcher: Option<&dyn PromWriteBatcher>,
592    prom_store_with_metric_engine: bool,
593    sample_batch_count: usize,
594    mut batches: Vec<PromWriteBatch>,
595) -> std::result::Result<PromWriteV2Outcome, PromWriteV2Error> {
596    let progress = batches
597        .iter_mut()
598        .map(|(ctx, _)| {
599            if ctx.write_progress().is_none() {
600                let mut fork = ctx.fork();
601                fork.set_write_progress(
602                    Arc::new(session::write_progress::WriteProgress::default()),
603                );
604                *ctx = Arc::new(fork);
605            }
606            ctx.write_progress().cloned().unwrap_or_default()
607        })
608        .collect::<Vec<_>>();
609    let counts = || {
610        let samples = progress[..sample_batch_count]
611            .iter()
612            .map(|p| p.written())
613            .sum();
614        let histograms = progress[sample_batch_count..]
615            .iter()
616            .map(|p| p.written())
617            .sum();
618        (samples, histograms)
619    };
620    let mut write_cost = 0;
621    let mut samples_accepted = 0;
622    let mut histograms_accepted = 0;
623    for (index, (ctx, requests)) in batches.into_iter().enumerate() {
624        let result = if index < sample_batch_count
625            && let Some(batcher) = batcher
626        {
627            batcher
628                .submit(requests, ctx)
629                .await
630                .map(|rows| (rows, 0, false))
631        } else {
632            prom_store_handler
633                .write_prepared(requests, ctx, prom_store_with_metric_engine)
634                .await
635                .map(|output| {
636                    let pending = !output.meta.write_completions.is_empty();
637                    let (rows, cost) = output.extract_rows_and_cost();
638                    (rows as u64, cost, pending)
639                })
640        };
641        match result {
642            Ok((rows, cost, pending)) => {
643                write_cost += cost;
644                if index < sample_batch_count {
645                    samples_accepted += rows;
646                } else {
647                    histograms_accepted += rows;
648                }
649                if pending {
650                    progress[index].mark_accepted();
651                }
652                if !progress[index].accepted() {
653                    progress[index].confirm(rows);
654                }
655            }
656            Err(error) => {
657                for progress in &progress {
658                    progress.wait().await;
659                }
660                let (samples_written, histograms_written) = counts();
661                return Err(PromWriteV2Error {
662                    error,
663                    samples_written,
664                    histograms_written,
665                });
666            }
667        }
668    }
669    Ok(PromWriteV2Outcome {
670        write_cost,
671        // PRW counts accepted rows on success; errors above report confirmed storage writes.
672        samples_written: samples_accepted,
673        histograms_written: histograms_accepted,
674        accepted: progress.iter().any(|p| p.accepted()),
675    })
676}
677
678fn prom_write_row_count(request: &RowInsertRequests) -> u64 {
679    request
680        .inserts
681        .iter()
682        .filter_map(|insert| insert.rows.as_ref().map(|rows| rows.rows.len() as u64))
683        .sum()
684}
685
686fn incomplete_prom_write_error() -> error::Error {
687    InternalSnafu {
688        err_msg: "prometheus write handler returned before processing every batch".to_string(),
689    }
690    .build()
691}
692
693fn record_remote_write_samples(db: &str, version: &str, rows: u64) {
694    if rows == 0 {
695        return;
696    }
697    crate::metrics::PROM_STORE_REMOTE_WRITE_SAMPLES
698        .with_label_values(&[db, version])
699        .inc_by(rows);
700}
701
702fn remote_write_v2_error_response(
703    error: error::Error,
704    samples: u64,
705    histograms: u64,
706    exemplars: u64,
707) -> axum::response::Response {
708    let mut response = error.into_response();
709    append_remote_write_v2_written_headers(response.headers_mut(), samples, histograms, exemplars);
710    response
711}
712
713fn append_remote_write_v2_written_headers(
714    headers: &mut HeaderMap,
715    samples: u64,
716    histograms: u64,
717    exemplars: u64,
718) {
719    headers.insert(
720        REMOTE_WRITE_V2_SAMPLES_WRITTEN_HEADER,
721        HeaderValue::from_str(&samples.to_string()).expect("u64 header value is valid"),
722    );
723    headers.insert(
724        REMOTE_WRITE_V2_HISTOGRAMS_WRITTEN_HEADER,
725        HeaderValue::from_str(&histograms.to_string()).expect("u64 header value is valid"),
726    );
727    headers.insert(
728        REMOTE_WRITE_V2_EXEMPLARS_WRITTEN_HEADER,
729        HeaderValue::from_str(&exemplars.to_string()).expect("u64 header value is valid"),
730    );
731}
732
733enum RemoteWriteProto {
734    V1,
735    V2,
736    Unsupported(mime::Mime),
737}
738
739// ref: https://github.com/prometheus/client_golang/blob/74560058a7af7a695db8196c8e84a0754032c6af/exp/api/remote/remote_api.go#L544
740fn remote_write_proto(content_type: Option<TypedHeader<headers::ContentType>>) -> RemoteWriteProto {
741    let Some(TypedHeader(content_type)) = content_type else {
742        return RemoteWriteProto::V1;
743    };
744
745    let mime_type: mime::Mime = content_type.into();
746    if !mime_type
747        .essence_str()
748        .eq_ignore_ascii_case(CONTENT_TYPE_PROTOBUF_STR)
749    {
750        return RemoteWriteProto::Unsupported(mime_type);
751    }
752
753    for (name, value) in mime_type.params() {
754        if !name.as_str().eq_ignore_ascii_case(CONTENT_TYPE_PROTO_PARAM) {
755            continue;
756        }
757
758        return match value.as_str() {
759            REMOTE_WRITE_V1_PROTO => RemoteWriteProto::V1,
760            REMOTE_WRITE_V2_PROTO => RemoteWriteProto::V2,
761            _ => RemoteWriteProto::Unsupported(mime_type.clone()),
762        };
763    }
764
765    RemoteWriteProto::V1
766}
767
768fn unsupported_remote_write_v2_encoding_response(
769    content_encoding: &headers::ContentEncoding,
770) -> Option<axum::response::Response> {
771    if content_encoding.contains(DEFAULT_ENCODING) || content_encoding.contains(VM_ENCODING) {
772        return None;
773    }
774
775    Some((
776        StatusCode::UNSUPPORTED_MEDIA_TYPE,
777        format!(
778            "unsupported prometheus remote write content encoding: only {DEFAULT_ENCODING} and {VM_ENCODING} are supported"
779        ),
780    )
781        .into_response())
782}
783
784impl IntoResponse for PromStoreResponse {
785    fn into_response(self) -> axum::response::Response {
786        let mut header_map = HeaderMap::new();
787        header_map.insert(&header::CONTENT_TYPE, self.content_type);
788        header_map.insert(&header::CONTENT_ENCODING, self.content_encoding);
789
790        let metrics = if self.resp_metrics.is_empty() {
791            None
792        } else {
793            serde_json::to_string(&self.resp_metrics).ok()
794        };
795        if let Some(m) = metrics.and_then(|m| HeaderValue::from_str(&m).ok()) {
796            header_map.insert(&GREPTIME_DB_HEADER_METRICS, m);
797        }
798
799        (header_map, self.body).into_response()
800    }
801}
802
803#[axum_macros::debug_handler]
804#[tracing::instrument(
805    skip_all,
806    fields(protocol = "prometheus", request_type = "remote_read")
807)]
808pub async fn remote_read(
809    State(state): State<PromStoreState>,
810    Query(params): Query<RemoteWriteQuery>,
811    Extension(mut query_ctx): Extension<QueryContext>,
812    body: Bytes,
813) -> Result<PromStoreResponse> {
814    let db = params.db.clone().unwrap_or_default();
815    query_ctx.set_channel(Channel::Prometheus);
816
817    let request = decode_remote_read_request(body, &state.memory_limiter).await?;
818
819    let query_ctx = Arc::new(query_ctx);
820    let _timer = crate::metrics::METRIC_HTTP_PROM_STORE_READ_ELAPSED
821        .with_label_values(&[db.as_str()])
822        .start_timer();
823
824    state.prom_store_handler.read(request, query_ctx).await
825}
826
827async fn decode_remote_read_request(
828    body: Bytes,
829    limiter: &ServerMemoryLimiter,
830) -> Result<ReadRequest> {
831    // Holds the memory permits for the decompressed bytes until the protobuf
832    // decoding is finished.
833    let buf =
834        snappy_decompress_limited(&body[..], MAX_DECOMPRESSED_REQUEST_SIZE, limiter, 0).await?;
835
836    ReadRequest::decode(&buf[..]).context(error::DecodePromRemoteRequestSnafu)
837}
838
839#[cfg(test)]
840mod tests {
841    use std::sync::Mutex;
842    use std::time::Duration;
843
844    use api::prom_store::remote::ReadRequest;
845    use api::v1::{Row, RowInsertRequest, Rows};
846    use async_trait::async_trait;
847    use axum::body::Body;
848    use axum::http::Request;
849    use axum::{Router, middleware, routing};
850    use common_memory_manager::OnExhaustedPolicy;
851    use common_query::Output;
852    use pipeline::GreptimePipelineParams;
853    use session::context::{QueryContext, QueryContextRef};
854    use tower::ServiceExt;
855
856    use super::*;
857    use crate::http::memory_limit::memory_limit_middleware;
858    use crate::prom_remote_write::validation::PromValidationMode;
859    use crate::query_handler::PromStoreProtocolHandler;
860
861    #[test]
862    fn test_remote_write_proto() {
863        assert!(matches!(
864            remote_write_proto(content_type(
865                "application/x-protobuf;proto=io.prometheus.write.v2.Request"
866            )),
867            RemoteWriteProto::V2
868        ));
869        assert!(matches!(
870            remote_write_proto(content_type(
871                "application/x-protobuf; proto=\"io.prometheus.write.v2.Request\""
872            )),
873            RemoteWriteProto::V2
874        ));
875        assert!(matches!(
876            remote_write_proto(content_type(
877                "APPLICATION/X-PROTOBUF;proto=io.prometheus.write.v2.Request"
878            )),
879            RemoteWriteProto::V2
880        ));
881        assert!(matches!(
882            remote_write_proto(content_type("application/x-protobuf")),
883            RemoteWriteProto::V1
884        ));
885        assert!(matches!(
886            remote_write_proto(content_type(
887                "application/x-protobuf;proto=prometheus.WriteRequest"
888            )),
889            RemoteWriteProto::V1
890        ));
891        assert!(matches!(
892            remote_write_proto(content_type(
893                "application/x-protobuf;proto=unknown.WriteRequest"
894            )),
895            RemoteWriteProto::Unsupported(_)
896        ));
897        assert!(matches!(
898            remote_write_proto(content_type(
899                "application/json;proto=io.prometheus.write.v2.Request"
900            )),
901            RemoteWriteProto::Unsupported(_)
902        ));
903        assert!(matches!(remote_write_proto(None), RemoteWriteProto::V1));
904    }
905
906    fn content_type(value: &str) -> Option<TypedHeader<headers::ContentType>> {
907        Some(TypedHeader(std::str::FromStr::from_str(value).unwrap()))
908    }
909
910    #[test]
911    fn test_prepare_remote_write_context_stamps_semantics() {
912        let (_, query_ctx, _timer) = prepare_remote_write_context(
913            &RemoteWriteQuery::default(),
914            QueryContext::with("greptime", "public"),
915            REMOTE_WRITE_V2_VERSION,
916        );
917
918        assert_eq!(
919            query_ctx.extension(SEMANTIC_SIGNAL_TYPE),
920            Some(SIGNAL_TYPE_METRIC)
921        );
922        assert_eq!(
923            query_ctx.extension(SEMANTIC_SOURCE),
924            Some(SOURCE_PROMETHEUS)
925        );
926        assert_eq!(
927            query_ctx.extension(SEMANTIC_SOURCE_VERSION),
928            Some(REMOTE_WRITE_V2_VERSION)
929        );
930        assert_eq!(
931            query_ctx.extension(SEMANTIC_METRIC_METADATA_QUALITY),
932            Some(METADATA_QUALITY_INFERRED)
933        );
934    }
935
936    #[tokio::test]
937    async fn test_mixed_v2_preflights_all_then_batches_only_samples() {
938        let events = Arc::new(Mutex::new(Vec::new()));
939        let handler: PromStoreProtocolHandlerRef = Arc::new(RecordingPromStoreHandler {
940            events: events.clone(),
941        });
942        let batcher = RecordingPromWriteBatcher {
943            events: events.clone(),
944        };
945
946        let mut batches = vec![
947            test_prom_write_batch("sample"),
948            test_prom_write_batch("histogram"),
949        ];
950        // The caller preflights before choosing the bulk path; the batched
951        // writer consumes the prepared batches.
952        preflight_prometheus_rows(&handler, &mut batches)
953            .await
954            .unwrap();
955        let Ok(outcome) = write_batched_prometheus_v2_rows_with_progress(
956            handler,
957            Some(&batcher),
958            true,
959            1,
960            batches,
961        )
962        .await
963        else {
964            panic!("mixed remote write should succeed")
965        };
966
967        assert_eq!(1, outcome.samples_written);
968        assert_eq!(1, outcome.histograms_written);
969        assert_eq!(
970            vec![
971                "pre:sample".to_string(),
972                "pre:histogram".to_string(),
973                "batch:sample".to_string(),
974                "direct:histogram".to_string(),
975            ],
976            *events.lock().unwrap()
977        );
978    }
979
980    fn test_prom_write_batch(table_name: &str) -> PromWriteBatch {
981        (
982            Arc::new(QueryContext::with("greptime", "public")),
983            RowInsertRequests {
984                inserts: vec![RowInsertRequest {
985                    table_name: table_name.to_string(),
986                    rows: Some(Rows {
987                        schema: Vec::new(),
988                        rows: vec![Row { values: Vec::new() }],
989                    }),
990                }],
991            },
992        )
993    }
994
995    #[tokio::test]
996    async fn test_async_v2_counts_acceptance_without_confirming_storage() {
997        let handler: PromStoreProtocolHandlerRef = Arc::new(RecordingPromStoreHandler {
998            events: Default::default(),
999        });
1000        let mut batches = vec![
1001            test_prom_write_batch("queued"),
1002            test_prom_write_batch("queued"),
1003        ];
1004        let mut progress = Vec::new();
1005        for (ctx, _) in &mut batches {
1006            let tracker = Arc::new(session::write_progress::WriteProgress::default());
1007            Arc::get_mut(ctx)
1008                .unwrap()
1009                .set_write_progress(tracker.clone());
1010            progress.push(tracker);
1011        }
1012        preflight_prometheus_rows(&handler, &mut batches)
1013            .await
1014            .unwrap();
1015        let Ok(outcome) =
1016            write_batched_prometheus_v2_rows_with_progress(handler, None, true, 1, batches).await
1017        else {
1018            panic!("queued writes should be accepted")
1019        };
1020        assert!(outcome.accepted);
1021        assert_eq!(outcome.samples_written, 1);
1022        assert_eq!(outcome.histograms_written, 1);
1023        assert!(progress.iter().all(|tracker| tracker.written() == 0));
1024    }
1025
1026    fn record_write_event(events: &Mutex<Vec<String>>, phase: &str, request: &RowInsertRequests) {
1027        events.lock().unwrap().push(format!(
1028            "{phase}:{}",
1029            request.inserts.first().unwrap().table_name
1030        ));
1031    }
1032
1033    #[tokio::test]
1034    async fn test_failed_request_waits_for_earlier_queued_writes() {
1035        struct DelayedBatcher(
1036            Mutex<
1037                Option<(
1038                    Arc<session::write_progress::WriteProgress>,
1039                    session::write_progress::PendingWrite,
1040                )>,
1041            >,
1042        );
1043        #[async_trait]
1044        impl PromWriteBatcher for DelayedBatcher {
1045            async fn submit(
1046                &self,
1047                request: RowInsertRequests,
1048                ctx: QueryContextRef,
1049            ) -> Result<u64> {
1050                let progress = ctx.write_progress().unwrap().clone();
1051                let pending = progress.start();
1052                progress.mark_accepted();
1053                *self.0.lock().unwrap() = Some((progress, pending));
1054                Ok(prom_write_row_count(&request))
1055            }
1056        }
1057        let handler: PromStoreProtocolHandlerRef = Arc::new(RecordingPromStoreHandler {
1058            events: Default::default(),
1059        });
1060        let batcher = DelayedBatcher(Mutex::new(None));
1061        let mut batches = vec![
1062            test_prom_write_batch("queued"),
1063            test_prom_write_batch("partial"),
1064        ];
1065        preflight_prometheus_rows(&handler, &mut batches)
1066            .await
1067            .unwrap();
1068        let write = write_batched_prometheus_v2_rows_with_progress(
1069            handler,
1070            Some(&batcher),
1071            true,
1072            1,
1073            batches,
1074        );
1075        tokio::pin!(write);
1076        assert!(futures::poll!(write.as_mut()).is_pending());
1077        let (progress, pending) = batcher.0.lock().unwrap().take().unwrap();
1078        progress.add_written(1);
1079        drop(pending);
1080        let Err(error) = write.await else {
1081            panic!("the second batch must fail")
1082        };
1083        assert_eq!(error.samples_written, 1);
1084        assert_eq!(error.histograms_written, 1);
1085    }
1086
1087    struct RecordingPromWriteBatcher {
1088        events: Arc<Mutex<Vec<String>>>,
1089    }
1090
1091    #[async_trait]
1092    impl PromWriteBatcher for RecordingPromWriteBatcher {
1093        async fn submit(&self, requests: RowInsertRequests, ctx: QueryContextRef) -> Result<u64> {
1094            assert_eq!(ctx.write_rows_to_admit("greptime", "public", 1), 0);
1095            record_write_event(&self.events, "batch", &requests);
1096            Ok(prom_write_row_count(&requests))
1097        }
1098    }
1099
1100    struct RecordingPromStoreHandler {
1101        events: Arc<Mutex<Vec<String>>>,
1102    }
1103
1104    #[async_trait]
1105    impl PromStoreProtocolHandler for RecordingPromStoreHandler {
1106        async fn pre_write(
1107            &self,
1108            request: &RowInsertRequests,
1109            _ctx: QueryContextRef,
1110        ) -> Result<()> {
1111            record_write_event(&self.events, "pre", request);
1112            Ok(())
1113        }
1114
1115        async fn write_prepared(
1116            &self,
1117            request: RowInsertRequests,
1118            ctx: QueryContextRef,
1119            _with_metric_engine: bool,
1120        ) -> Result<Output> {
1121            assert_eq!(ctx.write_rows_to_admit("greptime", "public", 1), 0);
1122            record_write_event(&self.events, "direct", &request);
1123            match request.inserts[0].table_name.as_str() {
1124                "partial" => {
1125                    ctx.write_progress().unwrap().add_written(1);
1126                    return Err(InternalSnafu {
1127                        err_msg: "one region failed",
1128                    }
1129                    .build());
1130                }
1131                "queued" => ctx.write_progress().unwrap().mark_accepted(),
1132                _ => {}
1133            }
1134            Ok(Output::new_with_affected_rows(
1135                prom_write_row_count(&request) as usize,
1136            ))
1137        }
1138
1139        async fn write(
1140            &self,
1141            _request: RowInsertRequests,
1142            _ctx: QueryContextRef,
1143            _with_metric_engine: bool,
1144        ) -> Result<Output> {
1145            unreachable!("mixed v2 writes use preflighted execution")
1146        }
1147
1148        async fn write_all(
1149            &self,
1150            _requests: Vec<(QueryContextRef, RowInsertRequests)>,
1151            _with_metric_engine: bool,
1152        ) -> Result<Vec<Result<Output>>> {
1153            unreachable!("mixed v2 writes preserve sample and histogram routing")
1154        }
1155
1156        async fn read(
1157            &self,
1158            _request: ReadRequest,
1159            _ctx: QueryContextRef,
1160        ) -> Result<PromStoreResponse> {
1161            unimplemented!()
1162        }
1163    }
1164
1165    #[tokio::test]
1166    async fn test_remote_write_v2_reports_confirmed_partial_and_queued_writes() {
1167        for (name, expected_status, expected_count) in [
1168            ("confirmed", StatusCode::NO_CONTENT, "2"),
1169            ("partial", StatusCode::INTERNAL_SERVER_ERROR, "1"),
1170            ("queued", StatusCode::ACCEPTED, "2"),
1171        ] {
1172            let request = crate::prom_remote_write::v2::test_util::request_with_labels_and_samples(
1173                vec![("__name__", name)],
1174                vec![api::greptime_proto::io::prometheus::write::v2::Sample::default(); 2],
1175            );
1176            let body =
1177                Bytes::from(crate::prom_store::snappy_compress(&request.encode_to_vec()).unwrap());
1178            let mut state = test_state();
1179            state.prom_store_handler = Arc::new(RecordingPromStoreHandler {
1180                events: Default::default(),
1181            });
1182            let response = remote_write_v2(
1183                state,
1184                RemoteWriteQuery::default(),
1185                QueryContext::with("greptime", "public"),
1186                pipeline_info(None),
1187                false,
1188                body,
1189                0,
1190            )
1191            .await
1192            .unwrap();
1193            assert_eq!(response.status(), expected_status);
1194            assert_eq!(
1195                response.headers()[REMOTE_WRITE_V2_SAMPLES_WRITTEN_HEADER],
1196                expected_count
1197            );
1198            assert_eq!(
1199                response.headers()[REMOTE_WRITE_V2_HISTOGRAMS_WRITTEN_HEADER],
1200                "0"
1201            );
1202            assert_eq!(
1203                response.headers()[REMOTE_WRITE_V2_EXEMPLARS_WRITTEN_HEADER],
1204                "0"
1205            );
1206        }
1207    }
1208
1209    fn memory_limited_prom_write_app(limiter: ServerMemoryLimiter) -> Router {
1210        let mut state = test_state();
1211        state.memory_limiter = limiter.clone();
1212        Router::new()
1213            .route("/write", routing::post(remote_write))
1214            .with_state(state)
1215            .layer(Extension(QueryContext::with("greptime", "public")))
1216            .layer(middleware::from_fn_with_state(
1217                limiter,
1218                memory_limit_middleware,
1219            ))
1220    }
1221
1222    #[tokio::test(start_paused = true)]
1223    async fn test_remote_write_v2_accounts_for_body_when_rejecting_or_waiting() {
1224        let wire = api::greptime_proto::io::prometheus::write::v2::Request {
1225            symbols: vec![
1226                String::new(),
1227                format!("{:?}", (0..2048).collect::<Vec<_>>()),
1228            ],
1229            timeseries: Vec::new(),
1230        }
1231        .encode_to_vec();
1232        let decoded_bytes = wire.len().div_ceil(1024) * 1024;
1233
1234        for encoding in [DEFAULT_ENCODING, VM_ENCODING] {
1235            let body = Bytes::from(if encoding == DEFAULT_ENCODING {
1236                crate::prom_store::snappy_compress(&wire).unwrap()
1237            } else {
1238                zstd::stream::encode_all(wire.as_slice(), 3).unwrap()
1239            });
1240            for chunked in [false, true] {
1241                // Small frames make their rounded permits exceed the rounded total body length.
1242                let chunk_size = if chunked { 513 } else { body.len() };
1243                let frames = body
1244                    .chunks(chunk_size)
1245                    .map(Bytes::copy_from_slice)
1246                    .collect::<Vec<_>>();
1247                let body_reserved: usize =
1248                    frames.iter().map(|f| f.len().div_ceil(1024) * 1024).sum();
1249                if chunked {
1250                    assert!(body_reserved > body.len().div_ceil(1024) * 1024);
1251                }
1252                // Reject during decompression or scratch allocation; otherwise wait for contention.
1253                for decoder_quota in [decoded_bytes - 1024, decoded_bytes, decoded_bytes + 1024] {
1254                    let limit = (body_reserved + decoder_quota) as u64;
1255                    let limiter = ServerMemoryLimiter::new(limit, OnExhaustedPolicy::default());
1256                    let fits = decoder_quota > decoded_bytes;
1257                    let occupied = fits.then(|| limiter.try_acquire(1024).unwrap());
1258                    let app = memory_limited_prom_write_app(limiter.clone());
1259                    let mut request = Request::builder()
1260                        .uri("/write")
1261                        .method("POST")
1262                        .header(
1263                            header::CONTENT_TYPE,
1264                            "application/x-protobuf;proto=io.prometheus.write.v2.Request",
1265                        )
1266                        .header(header::CONTENT_ENCODING, encoding);
1267                    let request_body = if chunked {
1268                        Body::from_stream(futures::stream::iter(
1269                            frames.clone().into_iter().map(Ok::<_, std::io::Error>),
1270                        ))
1271                    } else {
1272                        request = request.header(header::CONTENT_LENGTH, body.len().to_string());
1273                        Body::from(body.clone())
1274                    };
1275                    let started = tokio::time::Instant::now();
1276                    let response = app.oneshot(request.body(request_body).unwrap());
1277                    tokio::pin!(response);
1278                    if let Some(occupied) = occupied {
1279                        assert!(futures::poll!(response.as_mut()).is_pending());
1280                        // A failed attempt releases decoder permits, retaining only the HTTP body.
1281                        assert_eq!(limiter.used_bytes(), body_reserved as u64 + 1024);
1282                        drop(occupied);
1283                    }
1284                    let response = response.await.unwrap();
1285                    assert_eq!(
1286                        response.status(),
1287                        if fits {
1288                            StatusCode::NO_CONTENT
1289                        } else {
1290                            StatusCode::TOO_MANY_REQUESTS
1291                        },
1292                        "encoding={encoding}, chunked={chunked}, decoder_quota={decoder_quota}"
1293                    );
1294                    assert_eq!(started.elapsed(), Duration::ZERO);
1295                    assert_eq!(
1296                        response.headers()[REMOTE_WRITE_V2_SAMPLES_WRITTEN_HEADER],
1297                        "0"
1298                    );
1299                    assert_eq!(limiter.used_bytes(), 0);
1300                }
1301            }
1302        }
1303    }
1304
1305    #[tokio::test]
1306    async fn test_remote_write_v2_ignores_pipeline() {
1307        let request = api::greptime_proto::io::prometheus::write::v2::Request {
1308            symbols: vec![String::new()],
1309            timeseries: Vec::new(),
1310        };
1311        let body =
1312            Bytes::from(crate::prom_store::snappy_compress(&request.encode_to_vec()).unwrap());
1313
1314        let response = remote_write_v2(
1315            test_state(),
1316            RemoteWriteQuery::default(),
1317            QueryContext::with("greptime", "public"),
1318            pipeline_info(Some("pipeline")),
1319            false,
1320            body,
1321            0,
1322        )
1323        .await
1324        .unwrap();
1325
1326        assert_eq!(response.status(), StatusCode::NO_CONTENT);
1327        assert_eq!(
1328            Some("0"),
1329            response
1330                .headers()
1331                .get(REMOTE_WRITE_V2_SAMPLES_WRITTEN_HEADER)
1332                .map(|x| x.to_str().unwrap())
1333        );
1334    }
1335
1336    fn test_state() -> PromStoreState {
1337        PromStoreState {
1338            prom_store_handler: Arc::new(NoopPromStoreHandler),
1339            pipeline_handler: None,
1340            prom_store_with_metric_engine: false,
1341            prom_validation_mode: PromValidationMode::Strict,
1342            pending_rows_batcher: None,
1343            memory_limiter: ServerMemoryLimiter::default(),
1344        }
1345    }
1346
1347    fn pipeline_info(pipeline_name: Option<&str>) -> PipelineInfo {
1348        PipelineInfo {
1349            pipeline_name: pipeline_name.map(ToString::to_string),
1350            pipeline_version: None,
1351            pipeline_params: GreptimePipelineParams::default(),
1352        }
1353    }
1354
1355    struct NoopPromStoreHandler;
1356
1357    #[async_trait]
1358    impl PromStoreProtocolHandler for NoopPromStoreHandler {
1359        async fn pre_write(
1360            &self,
1361            _request: &RowInsertRequests,
1362            _ctx: QueryContextRef,
1363        ) -> Result<()> {
1364            Ok(())
1365        }
1366
1367        async fn write_prepared(
1368            &self,
1369            _request: RowInsertRequests,
1370            _ctx: QueryContextRef,
1371            _with_metric_engine: bool,
1372        ) -> Result<Output> {
1373            unreachable!("empty remote write v2 request should not write")
1374        }
1375
1376        async fn write(
1377            &self,
1378            _request: RowInsertRequests,
1379            _ctx: QueryContextRef,
1380            _with_metric_engine: bool,
1381        ) -> Result<Output> {
1382            unreachable!("empty remote write v2 request should not write")
1383        }
1384
1385        async fn write_all(
1386            &self,
1387            requests: Vec<(QueryContextRef, RowInsertRequests)>,
1388            _with_metric_engine: bool,
1389        ) -> Result<Vec<Result<Output>>> {
1390            assert!(requests.is_empty());
1391            Ok(Vec::new())
1392        }
1393
1394        async fn read(
1395            &self,
1396            _request: ReadRequest,
1397            _ctx: QueryContextRef,
1398        ) -> Result<PromStoreResponse> {
1399            unimplemented!()
1400        }
1401    }
1402}