1use 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
52pub enum FlightRecordBatchStreamInput<F = std::future::Ready<TonicResult<FlightRecordBatchSource>>>
54{
55 Ready(FlightRecordBatchSource),
56 Initializer(F),
57}
58
59impl FlightRecordBatchStreamInput {
60 pub fn ready(source: FlightRecordBatchSource) -> Self {
62 Self::Ready(source)
63 }
64}
65
66impl<F> FlightRecordBatchStreamInput<F> {
67 pub fn initializer(initializer: F) -> Self {
71 Self::Initializer(initializer)
72 }
73}
74
75struct 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 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 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}