Skip to main content

servers/grpc/flight/
stream.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::collections::VecDeque;
16use std::future::Future;
17use std::pin::Pin;
18use std::task::{Context, Poll};
19use std::time::{Duration, Instant};
20
21use arrow_flight::FlightData;
22use common_error::ext::ErrorExt;
23use common_grpc::flight::{FlightEncoder, FlightMessage};
24use common_recordbatch::SendableRecordBatchStream;
25use common_telemetry::tracing::{Instrument, info_span};
26use common_telemetry::tracing_context::{FutureExt, TracingContext};
27use common_telemetry::{error, info, warn};
28use futures::channel::mpsc;
29use futures::channel::mpsc::Sender;
30use futures::{SinkExt, Stream, StreamExt};
31use pin_project::{pin_project, pinned_drop};
32use session::context::{
33    FLIGHT_METRICS_HEARTBEAT_INTERVAL, QueryContextRef,
34    SUPPORT_FLIGHT_METRICS_BEFORE_BATCH_EXTENSION_KEY,
35};
36use snafu::ResultExt;
37use tokio::task::JoinHandle;
38use tokio::time;
39
40use crate::error;
41use crate::grpc::FlightCompression;
42use crate::grpc::flight::TonicResult;
43
44pub enum FlightRecordBatchSource {
45    RecordBatches(SendableRecordBatchStream),
46    AffectedRows {
47        rows: usize,
48        metrics: Option<String>,
49    },
50}
51
52/// Determines whether a Flight result is ready now or initialized asynchronously.
53pub enum FlightRecordBatchStreamInput<F = std::future::Ready<TonicResult<FlightRecordBatchSource>>>
54{
55    Ready(FlightRecordBatchSource),
56    Initializer(F),
57}
58
59impl FlightRecordBatchStreamInput {
60    /// Creates an input from a source that is already available.
61    pub fn ready(source: FlightRecordBatchSource) -> Self {
62        Self::Ready(source)
63    }
64}
65
66impl<F> FlightRecordBatchStreamInput<F> {
67    /// Creates an input that obtains its source asynchronously.
68    ///
69    /// Errors from the initializer are returned through the Flight response stream.
70    pub fn initializer(initializer: F) -> Self {
71        Self::Initializer(initializer)
72    }
73}
74
75/// Metrics collector for Flight stream with RAII logging pattern
76struct StreamMetrics {
77    send_schema_duration: Duration,
78    send_record_batch_duration: Duration,
79    send_metrics_duration: Duration,
80    fetch_content_duration: Duration,
81    record_batch_count: usize,
82    metrics_count: usize,
83    total_rows: usize,
84    total_bytes: usize,
85    should_log: bool,
86}
87
88impl StreamMetrics {
89    fn new(should_log: bool) -> Self {
90        Self {
91            send_schema_duration: Duration::ZERO,
92            send_record_batch_duration: Duration::ZERO,
93            send_metrics_duration: Duration::ZERO,
94            fetch_content_duration: Duration::ZERO,
95            record_batch_count: 0,
96            metrics_count: 0,
97            total_rows: 0,
98            total_bytes: 0,
99            should_log,
100        }
101    }
102}
103
104impl Drop for StreamMetrics {
105    fn drop(&mut self) {
106        if self.should_log {
107            info!(
108                "flight_data_stream finished: \
109                send_schema_duration={:?}, \
110                send_record_batch_duration={:?}, \
111                send_metrics_duration={:?}, \
112                fetch_content_duration={:?}, \
113                record_batch_count={}, \
114                metrics_count={}, \
115                total_rows={}, \
116                total_bytes={}",
117                self.send_schema_duration,
118                self.send_record_batch_duration,
119                self.send_metrics_duration,
120                self.fetch_content_duration,
121                self.record_batch_count,
122                self.metrics_count,
123                self.total_rows,
124                self.total_bytes
125            );
126        }
127    }
128}
129
130#[pin_project(PinnedDrop)]
131pub struct FlightRecordBatchStream {
132    #[pin]
133    rx: mpsc::Receiver<Result<FlightMessage, tonic::Status>>,
134    join_handle: JoinHandle<()>,
135    done: bool,
136    encoder: FlightEncoder,
137    buffer: VecDeque<FlightData>,
138}
139
140impl FlightRecordBatchStream {
141    async fn send_metrics(
142        tx: &mut Sender<TonicResult<FlightMessage>>,
143        metrics: &mut StreamMetrics,
144        metrics_str: String,
145    ) -> bool {
146        metrics.metrics_count += 1;
147        let start = Instant::now();
148        if let Err(e) = tx.send(Ok(FlightMessage::Metrics(metrics_str))).await {
149            warn!(e; "stop sending Flight data");
150            return false;
151        }
152        metrics.send_metrics_duration += start.elapsed();
153        true
154    }
155
156    async fn send_metrics_if_changed(
157        tx: &mut Sender<TonicResult<FlightMessage>>,
158        metrics: &mut StreamMetrics,
159        last_metrics_str: &mut Option<String>,
160        metrics_str: String,
161    ) -> bool {
162        if last_metrics_str.as_deref() == Some(metrics_str.as_str()) {
163            return true;
164        }
165
166        *last_metrics_str = Some(metrics_str.clone());
167        Self::send_metrics(tx, metrics, metrics_str).await
168    }
169
170    pub fn new<F>(
171        input: FlightRecordBatchStreamInput<F>,
172        tracing_context: TracingContext,
173        compression: FlightCompression,
174        query_ctx: QueryContextRef,
175    ) -> Self
176    where
177        F: Future<Output = TonicResult<FlightRecordBatchSource>> + Send + 'static,
178    {
179        let (mut tx, rx) = mpsc::channel::<TonicResult<FlightMessage>>(1);
180        let source_type = match &input {
181            FlightRecordBatchStreamInput::Ready(FlightRecordBatchSource::RecordBatches(_)) => {
182                "record_batches"
183            }
184            FlightRecordBatchStreamInput::Ready(FlightRecordBatchSource::AffectedRows {
185                ..
186            }) => "affected_rows",
187            FlightRecordBatchStreamInput::Initializer(_) => "initializer",
188        };
189        let initializer_tracing_context = tracing_context.clone();
190        let join_handle = common_runtime::spawn_global(
191            async move {
192                let source = async move {
193                    match input {
194                        FlightRecordBatchStreamInput::Ready(source) => Ok(source),
195                        FlightRecordBatchStreamInput::Initializer(initializer) => initializer.await,
196                    }
197                }
198                .trace(
199                    initializer_tracing_context
200                        .attach(info_span!("flight_data_stream_init", source_type)),
201                )
202                .await;
203
204                match source {
205                    Ok(FlightRecordBatchSource::RecordBatches(recordbatches)) => {
206                        let should_send_partial_metrics = query_ctx.explain_verbose();
207                        let can_send_metrics_before_batch =
208                            query_ctx.explain_verbose()
209                                && query_ctx.live_analyze_metrics_enabled()
210                                && query_ctx
211                                    .remote_query_id()
212                                    .zip(query_ctx.extension(
213                                        SUPPORT_FLIGHT_METRICS_BEFORE_BATCH_EXTENSION_KEY,
214                                    ))
215                                    .is_some_and(|(remote_query_id, capability)| {
216                                        capability == remote_query_id
217                                    });
218                        Self::flight_data_stream(
219                            recordbatches,
220                            tx,
221                            should_send_partial_metrics,
222                            can_send_metrics_before_batch,
223                        )
224                        .await;
225                    }
226                    Ok(FlightRecordBatchSource::AffectedRows { rows, metrics }) => {
227                        let _ = tx
228                            .send(Ok(FlightMessage::AffectedRows { rows, metrics }))
229                            .await;
230                    }
231                    Err(status) => {
232                        let _ = tx.send(Err(status)).await;
233                    }
234                }
235            }
236            .trace(tracing_context.attach(info_span!("flight_data_stream"))),
237        );
238        let encoder = if compression.arrow_compression() {
239            FlightEncoder::default()
240        } else {
241            FlightEncoder::with_compression_disabled()
242        };
243        Self {
244            rx,
245            join_handle,
246            done: false,
247            encoder,
248            buffer: VecDeque::new(),
249        }
250    }
251
252    async fn flight_data_stream(
253        mut recordbatches: SendableRecordBatchStream,
254        mut tx: Sender<TonicResult<FlightMessage>>,
255        should_send_partial_metrics: bool,
256        can_send_metrics_before_batch: bool,
257    ) {
258        let mut metrics = StreamMetrics::new(should_send_partial_metrics);
259        let mut last_metrics_str = None;
260
261        let schema = recordbatches.schema().arrow_schema().clone();
262        let start = Instant::now();
263        if let Err(e) = tx.send(Ok(FlightMessage::Schema(schema))).await {
264            warn!(e; "stop sending Flight data");
265            return;
266        }
267        metrics.send_schema_duration += start.elapsed();
268
269        loop {
270            let start = Instant::now();
271            let batch_or_err = if should_send_partial_metrics && can_send_metrics_before_batch {
272                match time::timeout(
273                    FLIGHT_METRICS_HEARTBEAT_INTERVAL,
274                    recordbatches.next().in_current_span(),
275                )
276                .await
277                {
278                    Ok(result) => result,
279                    Err(_) => {
280                        if let Some(metrics_str) = recordbatches
281                            .metrics()
282                            .and_then(|m| serde_json::to_string(&m).ok())
283                            && !Self::send_metrics_if_changed(
284                                &mut tx,
285                                &mut metrics,
286                                &mut last_metrics_str,
287                                metrics_str,
288                            )
289                            .await
290                        {
291                            return;
292                        }
293                        metrics.fetch_content_duration += start.elapsed();
294                        continue;
295                    }
296                }
297            } else {
298                recordbatches.next().in_current_span().await
299            };
300            metrics.fetch_content_duration += start.elapsed();
301            let Some(batch_or_err) = batch_or_err else {
302                break;
303            };
304            match batch_or_err {
305                Ok(recordbatch) => {
306                    metrics.total_rows += recordbatch.num_rows();
307                    metrics.record_batch_count += 1;
308                    metrics.total_bytes += recordbatch.df_record_batch().get_array_memory_size();
309
310                    let start = Instant::now();
311                    if let Err(e) = tx
312                        .send(Ok(FlightMessage::RecordBatch(
313                            recordbatch.into_df_record_batch(),
314                        )))
315                        .await
316                    {
317                        warn!(e; "stop sending Flight data");
318                        return;
319                    }
320                    metrics.send_record_batch_duration += start.elapsed();
321
322                    if should_send_partial_metrics
323                        && let Some(metrics_str) = recordbatches
324                            .metrics()
325                            .and_then(|m| serde_json::to_string(&m).ok())
326                        && {
327                            last_metrics_str = Some(metrics_str.clone());
328                            !Self::send_metrics(&mut tx, &mut metrics, metrics_str).await
329                        }
330                    {
331                        return;
332                    }
333                }
334                Err(e) => {
335                    if e.status_code().should_log_error() {
336                        error!("{e:?}");
337                    }
338
339                    let e = Err(e).context(error::CollectRecordbatchSnafu);
340                    if let Err(e) = tx.send(e.map_err(|x| x.into())).await {
341                        warn!(e; "stop sending Flight data");
342                    }
343                    return;
344                }
345            }
346        }
347        // make last package to pass metrics
348        if let Some(metrics_str) = recordbatches
349            .metrics()
350            .and_then(|m| serde_json::to_string(&m).ok())
351        {
352            let _ = Self::send_metrics(&mut tx, &mut metrics, metrics_str).await;
353        }
354    }
355}
356
357#[pinned_drop]
358impl PinnedDrop for FlightRecordBatchStream {
359    fn drop(self: Pin<&mut Self>) {
360        self.join_handle.abort();
361    }
362}
363
364impl Stream for FlightRecordBatchStream {
365    type Item = TonicResult<FlightData>;
366
367    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
368        let this = self.project();
369        if *this.done {
370            Poll::Ready(None)
371        } else {
372            if let Some(x) = this.buffer.pop_front() {
373                return Poll::Ready(Some(Ok(x)));
374            }
375            match this.rx.poll_next(cx) {
376                Poll::Ready(None) => {
377                    *this.done = true;
378                    Poll::Ready(None)
379                }
380                Poll::Ready(Some(result)) => match result {
381                    Ok(flight_message) => {
382                        let mut iter = this.encoder.encode(flight_message).into_iter();
383                        let Some(first) = iter.next() else {
384                            // Safety: `iter` on a type of `Vec1`, which is guaranteed to have
385                            // at least one element.
386                            unreachable!()
387                        };
388                        this.buffer.extend(iter);
389                        Poll::Ready(Some(Ok(first)))
390                    }
391                    Err(e) => {
392                        *this.done = true;
393                        Poll::Ready(Some(Err(e)))
394                    }
395                },
396                Poll::Pending => Poll::Pending,
397            }
398        }
399    }
400}
401
402#[cfg(test)]
403mod test {
404    use std::pin::Pin;
405    use std::sync::Arc;
406    use std::task::{Context, Poll};
407    use std::time::Duration;
408
409    use common_grpc::flight::{FlightDecoder, FlightMessage};
410    use common_recordbatch::adapter::RecordBatchMetrics;
411    use common_recordbatch::{OrderOption, RecordBatch, RecordBatchStream, RecordBatches};
412    use datatypes::prelude::*;
413    use datatypes::schema::{ColumnSchema, Schema, SchemaRef};
414    use datatypes::vectors::Int32Vector;
415    use futures::StreamExt;
416    use session::context::{
417        LIVE_ANALYZE_METRICS_EXTENSION_KEY, QueryContext,
418        SUPPORT_FLIGHT_METRICS_BEFORE_BATCH_EXTENSION_KEY,
419    };
420
421    use super::*;
422
423    struct PendingMetricsStream {
424        schema: SchemaRef,
425        metrics: RecordBatchMetrics,
426    }
427
428    struct MetricsThenBatchStream {
429        schema: SchemaRef,
430        metrics: RecordBatchMetrics,
431        rx: tokio::sync::mpsc::UnboundedReceiver<common_recordbatch::error::Result<RecordBatch>>,
432    }
433
434    fn query_context_with_matching_capability() -> Arc<QueryContext> {
435        let query_ctx = QueryContext::arc();
436        let remote_query_id = query_ctx
437            .remote_query_id()
438            .expect("query context must have remote query id")
439            .to_string();
440        let mut query_ctx = (*query_ctx).clone();
441        query_ctx.set_extension(
442            SUPPORT_FLIGHT_METRICS_BEFORE_BATCH_EXTENSION_KEY,
443            remote_query_id,
444        );
445        Arc::new(query_ctx)
446    }
447
448    fn query_context_with_live_metrics_and_matching_capability() -> Arc<QueryContext> {
449        let mut query_ctx = (*query_context_with_matching_capability()).clone();
450        query_ctx.enable_live_analyze_metrics();
451        Arc::new(query_ctx)
452    }
453
454    impl RecordBatchStream for PendingMetricsStream {
455        fn schema(&self) -> SchemaRef {
456            self.schema.clone()
457        }
458
459        fn output_ordering(&self) -> Option<&[OrderOption]> {
460            None
461        }
462
463        fn metrics(&self) -> Option<RecordBatchMetrics> {
464            Some(self.metrics.clone())
465        }
466    }
467
468    impl Stream for PendingMetricsStream {
469        type Item = common_recordbatch::error::Result<RecordBatch>;
470
471        fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
472            Poll::Pending
473        }
474    }
475
476    impl RecordBatchStream for MetricsThenBatchStream {
477        fn schema(&self) -> SchemaRef {
478            self.schema.clone()
479        }
480
481        fn output_ordering(&self) -> Option<&[OrderOption]> {
482            None
483        }
484
485        fn metrics(&self) -> Option<RecordBatchMetrics> {
486            Some(self.metrics.clone())
487        }
488    }
489
490    impl Stream for MetricsThenBatchStream {
491        type Item = common_recordbatch::error::Result<RecordBatch>;
492
493        fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
494            self.rx.poll_recv(cx)
495        }
496    }
497
498    #[tokio::test]
499    async fn test_flight_record_batch_stream() {
500        let schema = Arc::new(Schema::new(vec![ColumnSchema::new(
501            "a",
502            ConcreteDataType::int32_datatype(),
503            false,
504        )]));
505
506        let v: VectorRef = Arc::new(Int32Vector::from_slice([1, 2]));
507        let recordbatch = RecordBatch::new(schema.clone(), vec![v]).unwrap();
508
509        let recordbatches = RecordBatches::try_new(schema.clone(), vec![recordbatch.clone()])
510            .unwrap()
511            .as_stream();
512        let mut stream = FlightRecordBatchStream::new(
513            FlightRecordBatchStreamInput::ready(FlightRecordBatchSource::RecordBatches(
514                recordbatches,
515            )),
516            TracingContext::default(),
517            FlightCompression::default(),
518            QueryContext::arc(),
519        );
520
521        let mut raw_data = Vec::with_capacity(2);
522        raw_data.push(stream.next().await.unwrap().unwrap());
523        raw_data.push(stream.next().await.unwrap().unwrap());
524        assert!(stream.next().await.is_none());
525        assert!(stream.done);
526
527        let decoder = &mut FlightDecoder::default();
528        let mut flight_messages = raw_data
529            .into_iter()
530            .map(|x| decoder.try_decode(&x).unwrap().unwrap())
531            .collect::<Vec<FlightMessage>>();
532        assert_eq!(flight_messages.len(), 2);
533
534        match flight_messages.remove(0) {
535            FlightMessage::Schema(actual_schema) => {
536                assert_eq!(&actual_schema, schema.arrow_schema());
537            }
538            _ => unreachable!(),
539        }
540
541        match flight_messages.remove(0) {
542            FlightMessage::RecordBatch(actual_recordbatch) => {
543                assert_eq!(&actual_recordbatch, recordbatch.df_record_batch());
544            }
545            _ => unreachable!(),
546        }
547    }
548
549    #[tokio::test]
550    async fn test_flight_record_batch_stream_encodes_affected_rows() {
551        let mut stream = FlightRecordBatchStream::new(
552            FlightRecordBatchStreamInput::ready(FlightRecordBatchSource::AffectedRows {
553                rows: 42,
554                metrics: Some(r#"{"region_watermarks":[]}"#.to_string()),
555            }),
556            TracingContext::default(),
557            FlightCompression::default(),
558            QueryContext::arc(),
559        );
560
561        let data = stream.next().await.unwrap().unwrap();
562        let message = FlightDecoder::default().try_decode(&data).unwrap().unwrap();
563        assert!(matches!(
564            message,
565            FlightMessage::AffectedRows {
566                rows: 42,
567                metrics: Some(_),
568            }
569        ));
570        assert!(stream.next().await.is_none());
571    }
572
573    #[tokio::test]
574    async fn test_flight_record_batch_stream_forwards_initializer_error() {
575        let mut stream = FlightRecordBatchStream::new(
576            FlightRecordBatchStreamInput::initializer(async {
577                Err(tonic::Status::unavailable(
578                    "remote read initialization failed",
579                ))
580            }),
581            TracingContext::default(),
582            FlightCompression::default(),
583            QueryContext::arc(),
584        );
585
586        let error = stream.next().await.unwrap().unwrap_err();
587        assert_eq!(tonic::Code::Unavailable, error.code());
588        assert!(stream.next().await.is_none());
589    }
590    #[tokio::test]
591    async fn test_flight_record_batch_stream_emits_metrics_while_pending() {
592        let schema = Arc::new(Schema::new(vec![ColumnSchema::new(
593            "a",
594            ConcreteDataType::int32_datatype(),
595            false,
596        )]));
597        let metrics = RecordBatchMetrics {
598            elapsed_compute: 42,
599            ..Default::default()
600        };
601        let recordbatches = Box::pin(PendingMetricsStream {
602            schema: schema.clone(),
603            metrics,
604        });
605        let query_ctx = query_context_with_live_metrics_and_matching_capability();
606        let initializer_query_ctx = query_ctx.clone();
607        let mut stream = FlightRecordBatchStream::new(
608            FlightRecordBatchStreamInput::initializer(async move {
609                initializer_query_ctx.set_explain_verbose(true);
610                Ok(FlightRecordBatchSource::RecordBatches(recordbatches))
611            }),
612            TracingContext::default(),
613            FlightCompression::default(),
614            query_ctx,
615        );
616
617        let decoder = &mut FlightDecoder::default();
618        let schema_data = stream.next().await.unwrap().unwrap();
619        match decoder.try_decode(&schema_data).unwrap().unwrap() {
620            FlightMessage::Schema(actual_schema) => {
621                assert_eq!(&actual_schema, schema.arrow_schema());
622            }
623            _ => unreachable!(),
624        }
625
626        let metrics_data = tokio::time::timeout(Duration::from_secs(2), stream.next())
627            .await
628            .unwrap()
629            .unwrap()
630            .unwrap();
631        match decoder.try_decode(&metrics_data).unwrap().unwrap() {
632            FlightMessage::Metrics(metrics) => {
633                let metrics: RecordBatchMetrics = serde_json::from_str(&metrics).unwrap();
634                assert_eq!(metrics.elapsed_compute, 42);
635            }
636            other => panic!("expected metrics message, got {other:?}"),
637        }
638    }
639
640    #[tokio::test]
641    async fn test_flight_record_batch_stream_continues_after_pending_metrics() {
642        let schema = Arc::new(Schema::new(vec![ColumnSchema::new(
643            "a",
644            ConcreteDataType::int32_datatype(),
645            false,
646        )]));
647        let metrics = RecordBatchMetrics {
648            elapsed_compute: 42,
649            ..Default::default()
650        };
651        let recordbatch = RecordBatch::new(
652            schema.clone(),
653            vec![Arc::new(Int32Vector::from_slice([1])) as VectorRef],
654        )
655        .unwrap();
656        let expected_recordbatch = recordbatch.df_record_batch().clone();
657        let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
658        let recordbatches = Box::pin(MetricsThenBatchStream {
659            schema: schema.clone(),
660            metrics,
661            rx,
662        });
663        let query_ctx = query_context_with_live_metrics_and_matching_capability();
664        query_ctx.set_explain_verbose(true);
665        let mut stream = FlightRecordBatchStream::new(
666            FlightRecordBatchStreamInput::ready(FlightRecordBatchSource::RecordBatches(
667                recordbatches,
668            )),
669            TracingContext::default(),
670            FlightCompression::default(),
671            query_ctx,
672        );
673
674        let decoder = &mut FlightDecoder::default();
675        let schema_data = stream.next().await.unwrap().unwrap();
676        assert!(matches!(
677            decoder.try_decode(&schema_data).unwrap().unwrap(),
678            FlightMessage::Schema(_)
679        ));
680
681        let metrics_data = tokio::time::timeout(Duration::from_secs(2), stream.next())
682            .await
683            .unwrap()
684            .unwrap()
685            .unwrap();
686        assert!(matches!(
687            decoder.try_decode(&metrics_data).unwrap().unwrap(),
688            FlightMessage::Metrics(_)
689        ));
690
691        tx.send(Ok(recordbatch)).unwrap();
692        let batch_data = tokio::time::timeout(Duration::from_secs(2), stream.next())
693            .await
694            .unwrap()
695            .unwrap()
696            .unwrap();
697        match decoder.try_decode(&batch_data).unwrap().unwrap() {
698            FlightMessage::RecordBatch(actual_recordbatch) => {
699                assert_eq!(&actual_recordbatch, &expected_recordbatch);
700            }
701            other => panic!("expected record batch after pending metrics, got {other:?}"),
702        }
703
704        drop(tx);
705        let final_metrics_data = tokio::time::timeout(Duration::from_secs(2), stream.next())
706            .await
707            .unwrap()
708            .unwrap()
709            .unwrap();
710        assert!(matches!(
711            decoder.try_decode(&final_metrics_data).unwrap().unwrap(),
712            FlightMessage::Metrics(_)
713        ));
714    }
715
716    #[tokio::test]
717    async fn test_flight_record_batch_stream_requires_live_metrics_for_pre_batch_metrics() {
718        let schema = Arc::new(Schema::new(vec![ColumnSchema::new(
719            "a",
720            ConcreteDataType::int32_datatype(),
721            false,
722        )]));
723        let recordbatches = Box::pin(PendingMetricsStream {
724            schema: schema.clone(),
725            metrics: RecordBatchMetrics {
726                elapsed_compute: 42,
727                ..Default::default()
728            },
729        });
730        let query_ctx = query_context_with_matching_capability();
731        query_ctx.set_explain_verbose(true);
732        let mut stream = FlightRecordBatchStream::new(
733            FlightRecordBatchStreamInput::ready(FlightRecordBatchSource::RecordBatches(
734                recordbatches,
735            )),
736            TracingContext::default(),
737            FlightCompression::default(),
738            query_ctx,
739        );
740
741        let decoder = &mut FlightDecoder::default();
742        let schema_data = stream.next().await.unwrap().unwrap();
743        assert!(matches!(
744            decoder.try_decode(&schema_data).unwrap().unwrap(),
745            FlightMessage::Schema(_)
746        ));
747        assert!(
748            tokio::time::timeout(
749                FLIGHT_METRICS_HEARTBEAT_INTERVAL + Duration::from_millis(200),
750                stream.next()
751            )
752            .await
753            .is_err(),
754            "pre-batch Metrics must be gated by live analyze metrics"
755        );
756    }
757
758    #[tokio::test]
759    async fn test_flight_record_batch_stream_rejects_spoofed_live_metrics_for_pre_batch_metrics() {
760        let schema = Arc::new(Schema::new(vec![ColumnSchema::new(
761            "a",
762            ConcreteDataType::int32_datatype(),
763            false,
764        )]));
765        let recordbatches = Box::pin(PendingMetricsStream {
766            schema: schema.clone(),
767            metrics: RecordBatchMetrics {
768                elapsed_compute: 42,
769                ..Default::default()
770            },
771        });
772        let query_ctx = query_context_with_live_metrics_and_matching_capability();
773        let mut query_ctx = (*query_ctx).clone();
774        query_ctx.set_extension(LIVE_ANALYZE_METRICS_EXTENSION_KEY, "true");
775        let query_ctx = Arc::new(query_ctx);
776        query_ctx.set_explain_verbose(true);
777        let mut stream = FlightRecordBatchStream::new(
778            FlightRecordBatchStreamInput::ready(FlightRecordBatchSource::RecordBatches(
779                recordbatches,
780            )),
781            TracingContext::default(),
782            FlightCompression::default(),
783            query_ctx,
784        );
785
786        let decoder = &mut FlightDecoder::default();
787        let schema_data = stream.next().await.unwrap().unwrap();
788        assert!(matches!(
789            decoder.try_decode(&schema_data).unwrap().unwrap(),
790            FlightMessage::Schema(_)
791        ));
792        assert!(
793            tokio::time::timeout(
794                FLIGHT_METRICS_HEARTBEAT_INTERVAL + Duration::from_millis(200),
795                stream.next()
796            )
797            .await
798            .is_err(),
799            "pre-batch Metrics must reject spoofed live analyze metrics"
800        );
801    }
802
803    #[tokio::test]
804    async fn test_flight_record_batch_stream_requires_explain_verbose_for_pre_batch_metrics() {
805        let schema = Arc::new(Schema::new(vec![ColumnSchema::new(
806            "a",
807            ConcreteDataType::int32_datatype(),
808            false,
809        )]));
810        let recordbatches = Box::pin(PendingMetricsStream {
811            schema: schema.clone(),
812            metrics: RecordBatchMetrics {
813                elapsed_compute: 42,
814                ..Default::default()
815            },
816        });
817        let query_ctx = query_context_with_matching_capability();
818        let mut stream = FlightRecordBatchStream::new(
819            FlightRecordBatchStreamInput::ready(FlightRecordBatchSource::RecordBatches(
820                recordbatches,
821            )),
822            TracingContext::default(),
823            FlightCompression::default(),
824            query_ctx,
825        );
826
827        let decoder = &mut FlightDecoder::default();
828        let schema_data = stream.next().await.unwrap().unwrap();
829        assert!(matches!(
830            decoder.try_decode(&schema_data).unwrap().unwrap(),
831            FlightMessage::Schema(_)
832        ));
833        assert!(
834            tokio::time::timeout(
835                FLIGHT_METRICS_HEARTBEAT_INTERVAL + Duration::from_millis(200),
836                stream.next()
837            )
838            .await
839            .is_err(),
840            "pre-batch Metrics must be gated by explain verbose even when capability is set"
841        );
842    }
843}