1use std::collections::HashMap;
18use std::sync::{Arc, Mutex, Weak};
19
20use api::v1::greptime_request::Request;
21use api::v1::query_request::Query;
22use api::v1::{CreateTableExpr, QueryRequest};
23use client::{Client, Database, OutputWithMetrics};
24use common_error::ext::BoxedError;
25use common_grpc::channel_manager::{ChannelConfig, ChannelManager, load_client_tls_config};
26use common_meta::peer::{Peer, PeerDiscovery};
27use common_query::{Output, OutputData};
28use common_recordbatch::adapter::{RecordBatchMetrics, RegionWatermarkEntry};
29use common_telemetry::warn;
30use meta_client::client::MetaClient;
31use query::datafusion::QUERY_PARALLELISM_HINT;
32use query::metrics::terminal_recordbatch_metrics_from_plan;
33use query::options::{FlowQueryExtensions, QueryOptions};
34use rand::rng;
35use rand::seq::SliceRandom;
36use servers::query_handler::grpc::GrpcQueryHandler;
37use session::context::{QueryContextBuilder, QueryContextRef};
38use session::hints::READ_PREFERENCE_HINT;
39use snafu::{OptionExt, ResultExt};
40use tokio::sync::SetOnce;
41
42use crate::batching_mode::BatchingModeOptions;
43use crate::error::{
44 CreateSinkTableSnafu, ExternalSnafu, InvalidClientConfigSnafu, InvalidRequestSnafu,
45 NoAvailableFrontendSnafu, UnexpectedSnafu,
46};
47use crate::{Error, FlowAuthHeader};
48
49#[async_trait::async_trait]
53pub trait GrpcQueryHandlerWithBoxedError: Send + Sync + 'static {
54 async fn do_query(
55 &self,
56 query: Request,
57 ctx: QueryContextRef,
58 ) -> std::result::Result<Output, BoxedError>;
59}
60
61#[async_trait::async_trait]
63impl<T: GrpcQueryHandler + Send + Sync + 'static> GrpcQueryHandlerWithBoxedError for T {
64 async fn do_query(
65 &self,
66 query: Request,
67 ctx: QueryContextRef,
68 ) -> std::result::Result<Output, BoxedError> {
69 self.do_query(query, ctx).await.map_err(BoxedError::new)
70 }
71}
72
73#[derive(Debug, Clone)]
74pub struct HandlerMutable {
75 handler: Arc<Mutex<Option<Weak<dyn GrpcQueryHandlerWithBoxedError>>>>,
76 is_initialized: Arc<SetOnce<()>>,
77}
78
79impl HandlerMutable {
80 pub async fn set_handler(&self, handler: Weak<dyn GrpcQueryHandlerWithBoxedError>) {
81 *self.handler.lock().unwrap() = Some(handler);
82 let _ = self.is_initialized.set(());
84 }
85}
86
87#[derive(Debug, Clone)]
91pub enum FrontendClient {
92 Distributed {
93 meta_client: Arc<MetaClient>,
94 chnl_mgr: ChannelManager,
95 auth: Option<FlowAuthHeader>,
96 query: QueryOptions,
97 batch_opts: BatchingModeOptions,
98 },
99 Standalone {
100 database_client: HandlerMutable,
103 query: QueryOptions,
104 },
105}
106
107impl FrontendClient {
108 pub fn from_empty_grpc_handler(query: QueryOptions) -> (Self, HandlerMutable) {
110 let is_initialized = Arc::new(SetOnce::new());
111 let handler = HandlerMutable {
112 handler: Arc::new(Mutex::new(None)),
113 is_initialized,
114 };
115 (
116 Self::Standalone {
117 database_client: handler.clone(),
118 query,
119 },
120 handler,
121 )
122 }
123
124 pub async fn wait_initialized(&self) {
126 if let FrontendClient::Standalone {
127 database_client, ..
128 } = self
129 {
130 database_client.is_initialized.wait().await;
131 }
132 }
133
134 pub fn from_meta_client(
135 meta_client: Arc<MetaClient>,
136 auth: Option<FlowAuthHeader>,
137 query: QueryOptions,
138 batch_opts: BatchingModeOptions,
139 ) -> Result<Self, Error> {
140 common_telemetry::info!("Frontend client build with auth={:?}", auth);
141 Ok(Self::Distributed {
142 meta_client,
143 chnl_mgr: {
144 let cfg = ChannelConfig::new()
145 .connect_timeout(batch_opts.grpc_conn_timeout)
146 .timeout(Some(batch_opts.query_timeout));
147
148 let tls_config = load_client_tls_config(batch_opts.frontend_tls.clone())
149 .context(InvalidClientConfigSnafu)?;
150 ChannelManager::with_config(cfg, tls_config)
151 },
152 auth,
153 query,
154 batch_opts,
155 })
156 }
157
158 pub fn from_grpc_handler(
159 grpc_handler: Weak<dyn GrpcQueryHandlerWithBoxedError>,
160 query: QueryOptions,
161 ) -> Self {
162 let is_initialized = Arc::new(SetOnce::new_with(Some(())));
163 let handler = HandlerMutable {
164 handler: Arc::new(Mutex::new(Some(grpc_handler))),
165 is_initialized: is_initialized.clone(),
166 };
167
168 Self::Standalone {
169 database_client: handler,
170 query,
171 }
172 }
173}
174
175#[derive(Debug, Clone)]
176pub struct DatabaseWithPeer {
177 pub database: Database,
178 pub peer: Peer,
179}
180
181impl DatabaseWithPeer {
182 fn new(database: Database, peer: Peer) -> Self {
183 Self { database, peer }
184 }
185
186 async fn try_select_one(&self) -> Result<(), Error> {
188 let _ = self
190 .database
191 .sql("SELECT 1")
192 .await
193 .with_context(|_| InvalidRequestSnafu {
194 context: format!("Failed to handle `SELECT 1` request at {:?}", self.peer),
195 })?;
196 Ok(())
197 }
198}
199
200impl FrontendClient {
201 pub(crate) async fn scan_for_frontend(&self) -> Result<Vec<Peer>, Error> {
203 let Self::Distributed { meta_client, .. } = self else {
204 return Ok(vec![]);
205 };
206
207 meta_client
208 .active_frontends()
209 .await
210 .map(|nodes| nodes.into_iter().map(|node| node.peer).collect())
211 .map_err(BoxedError::new)
212 .context(ExternalSnafu)
213 }
214
215 async fn get_random_active_frontend(
217 &self,
218 catalog: &str,
219 schema: &str,
220 ) -> Result<DatabaseWithPeer, Error> {
221 let Self::Distributed {
222 meta_client: _,
223 chnl_mgr,
224 auth,
225 query: _,
226 batch_opts,
227 } = self
228 else {
229 return UnexpectedSnafu {
230 reason: "Expect distributed mode",
231 }
232 .fail();
233 };
234
235 let mut interval = tokio::time::interval(batch_opts.grpc_conn_timeout);
236 interval.tick().await;
237 for retry in 0..batch_opts.experimental_grpc_max_retries {
238 let mut frontends = self.scan_for_frontend().await?;
239 frontends.shuffle(&mut rng());
241
242 for peer in frontends {
243 let addr = peer.addr.clone();
244 let client = Client::with_manager_and_urls(chnl_mgr.clone(), vec![addr.clone()]);
245 let database = {
246 let mut db = Database::new(catalog, schema, client);
247 if let Some(auth) = auth {
248 db.set_auth(auth.auth().clone());
249 }
250 db
251 };
252 let db = DatabaseWithPeer::new(database, peer);
253 match db.try_select_one().await {
254 Ok(_) => return Ok(db),
255 Err(e) => {
256 warn!(
257 "Failed to connect to frontend {} on retry={}: \n{e:?}",
258 addr, retry
259 );
260 }
261 }
262 }
263 interval.tick().await;
266 }
267
268 NoAvailableFrontendSnafu {
269 timeout: batch_opts.grpc_conn_timeout,
270 context: "No available frontend found that is able to process query",
271 }
272 .fail()
273 }
274
275 pub async fn create(
276 &self,
277 create: CreateTableExpr,
278 catalog: &str,
279 schema: &str,
280 ) -> Result<u32, Error> {
281 self.handle(
282 Request::Ddl(api::v1::DdlRequest {
283 expr: Some(api::v1::ddl_request::Expr::CreateTable(create.clone())),
284 }),
285 catalog,
286 schema,
287 &mut None,
288 )
289 .await
290 .map_err(BoxedError::new)
291 .with_context(|_| CreateSinkTableSnafu {
292 create: create.clone(),
293 })
294 }
295
296 pub async fn sql(&self, catalog: &str, schema: &str, sql: &str) -> Result<Output, Error> {
298 match self {
299 FrontendClient::Distributed { .. } => {
300 let db = self.get_random_active_frontend(catalog, schema).await?;
301 db.database
302 .sql(sql)
303 .await
304 .map_err(BoxedError::new)
305 .context(ExternalSnafu)
306 }
307 FrontendClient::Standalone {
308 database_client, ..
309 } => {
310 let ctx = QueryContextBuilder::default()
311 .current_catalog(catalog.to_string())
312 .current_schema(schema.to_string())
313 .build();
314 let ctx = Arc::new(ctx);
315 {
316 let database_client = {
317 database_client
318 .handler
319 .lock()
320 .unwrap()
321 .as_ref()
322 .context(UnexpectedSnafu {
323 reason: "Standalone's frontend instance is not set",
324 })?
325 .upgrade()
326 .context(UnexpectedSnafu {
327 reason: "Failed to upgrade database client",
328 })?
329 };
330 let req = Request::Query(QueryRequest {
331 query: Some(Query::Sql(sql.to_string())),
332 });
333 database_client
334 .do_query(req, ctx)
335 .await
336 .map_err(BoxedError::new)
337 .context(ExternalSnafu)
338 }
339 }
340 }
341 }
342
343 pub(crate) async fn query_with_terminal_metrics(
344 &self,
345 catalog: &str,
346 schema: &str,
347 request: QueryRequest,
348 extensions: &[(&str, &str)],
349 peer_desc: &mut Option<PeerDesc>,
350 ) -> Result<OutputWithMetrics, Error> {
351 let flow_extensions = build_flow_extensions(extensions)?;
352 match self {
353 FrontendClient::Distributed {
354 query, batch_opts, ..
355 } => {
356 let query_parallelism = query.parallelism.to_string();
357 let hints = vec![
358 (QUERY_PARALLELISM_HINT, query_parallelism.as_str()),
359 (READ_PREFERENCE_HINT, batch_opts.read_preference.as_ref()),
360 ];
361 let db = self.get_random_active_frontend(catalog, schema).await?;
362 *peer_desc = Some(PeerDesc::Dist {
363 peer: db.peer.clone(),
364 });
365 db.database
366 .query_with_terminal_metrics_and_flow_extensions(request, &hints, extensions)
367 .await
368 .map_err(BoxedError::new)
369 .context(ExternalSnafu)
370 }
371 FrontendClient::Standalone {
372 database_client,
373 query,
374 } => {
375 *peer_desc = Some(PeerDesc::Standalone);
376 let mut extensions_map = HashMap::from([(
377 QUERY_PARALLELISM_HINT.to_string(),
378 query.parallelism.to_string(),
379 )]);
380 for (key, value) in extensions {
381 extensions_map.insert((*key).to_string(), (*value).to_string());
382 }
383 let ctx = QueryContextBuilder::default()
384 .current_catalog(catalog.to_string())
385 .current_schema(schema.to_string())
386 .extensions(extensions_map)
387 .build();
388 let ctx = Arc::new(ctx);
389 let database_client = {
390 database_client
391 .handler
392 .lock()
393 .map_err(|e| {
394 UnexpectedSnafu {
395 reason: format!("Failed to lock database client: {e}"),
396 }
397 .build()
398 })?
399 .as_ref()
400 .context(UnexpectedSnafu {
401 reason: "Standalone's frontend instance is not set",
402 })?
403 .upgrade()
404 .context(UnexpectedSnafu {
405 reason: "Failed to upgrade database client",
406 })?
407 };
408 database_client
409 .do_query(Request::Query(request), ctx.clone())
410 .await
411 .map(|output| {
412 wrap_standalone_output_with_terminal_metrics(output, &flow_extensions, &ctx)
413 })
414 .map_err(BoxedError::new)
415 .context(ExternalSnafu)
416 }
417 }
418 }
419
420 pub(crate) async fn handle(
422 &self,
423 req: api::v1::greptime_request::Request,
424 catalog: &str,
425 schema: &str,
426 peer_desc: &mut Option<PeerDesc>,
427 ) -> Result<u32, Error> {
428 match self {
429 FrontendClient::Distributed {
430 query, batch_opts, ..
431 } => {
432 let db = self.get_random_active_frontend(catalog, schema).await?;
433
434 *peer_desc = Some(PeerDesc::Dist {
435 peer: db.peer.clone(),
436 });
437
438 db.database
439 .handle_with_retry(
440 req.clone(),
441 batch_opts.experimental_grpc_max_retries,
442 &[
443 (QUERY_PARALLELISM_HINT, &query.parallelism.to_string()),
444 (READ_PREFERENCE_HINT, batch_opts.read_preference.as_ref()),
445 ],
446 )
447 .await
448 .with_context(|_| InvalidRequestSnafu {
449 context: format!("Failed to handle request at {:?}: {:?}", db.peer, req),
450 })
451 }
452 FrontendClient::Standalone {
453 database_client,
454 query,
455 } => {
456 let ctx = QueryContextBuilder::default()
457 .current_catalog(catalog.to_string())
458 .current_schema(schema.to_string())
459 .extensions(HashMap::from([(
460 QUERY_PARALLELISM_HINT.to_string(),
461 query.parallelism.to_string(),
462 )]))
463 .build();
464 let ctx = Arc::new(ctx);
465 {
466 let database_client = {
467 database_client
468 .handler
469 .lock()
470 .unwrap()
471 .as_ref()
472 .context(UnexpectedSnafu {
473 reason: "Standalone's frontend instance is not set",
474 })?
475 .upgrade()
476 .context(UnexpectedSnafu {
477 reason: "Failed to upgrade database client",
478 })?
479 };
480 let resp: common_query::Output = database_client
481 .do_query(req, ctx)
482 .await
483 .map_err(BoxedError::new)
484 .context(ExternalSnafu)?;
485 match resp.data {
486 common_query::OutputData::AffectedRows(rows) => {
487 Ok(rows.try_into().map_err(|_| {
488 UnexpectedSnafu {
489 reason: format!("Failed to convert rows to u32: {}", rows),
490 }
491 .build()
492 })?)
493 }
494 _ => UnexpectedSnafu {
495 reason: "Unexpected output data",
496 }
497 .fail(),
498 }
499 }
500 }
501 }
502 }
503}
504
505fn build_flow_extensions(extensions: &[(&str, &str)]) -> Result<FlowQueryExtensions, Error> {
506 let flow_extensions = HashMap::from_iter(
507 extensions
508 .iter()
509 .map(|(key, value)| ((*key).to_string(), (*value).to_string())),
510 );
511 FlowQueryExtensions::parse_flow_extensions(&flow_extensions)
512 .map_err(BoxedError::new)
513 .context(ExternalSnafu)
514 .map(|extensions| extensions.unwrap_or_default())
515}
516
517fn wrap_standalone_output_with_terminal_metrics(
518 output: Output,
519 flow_extensions: &FlowQueryExtensions,
520 query_ctx: &QueryContextRef,
521) -> OutputWithMetrics {
522 let should_collect_region_watermark = flow_extensions.should_collect_region_watermark();
523 let terminal_metrics =
524 if should_collect_region_watermark && !matches!(&output.data, OutputData::Stream(_)) {
525 output
526 .meta
527 .plan
528 .clone()
529 .and_then(terminal_recordbatch_metrics_from_plan)
530 .or_else(|| terminal_recordbatch_metrics_from_snapshots(query_ctx))
531 } else {
532 None
533 };
534 let result = OutputWithMetrics::from_output(output);
535 if let Some(metrics) = terminal_metrics {
536 result.metrics.update(Some(metrics));
537 }
538 result
539}
540
541fn terminal_recordbatch_metrics_from_snapshots(
542 query_ctx: &QueryContextRef,
543) -> Option<RecordBatchMetrics> {
544 let mut region_watermarks = query_ctx
545 .snapshots()
546 .into_iter()
547 .map(|(region_id, watermark)| RegionWatermarkEntry {
548 region_id,
549 watermark: Some(watermark),
550 })
551 .collect::<Vec<_>>();
552 if region_watermarks.is_empty() {
553 return None;
554 }
555
556 region_watermarks.sort_by_key(|entry| entry.region_id);
557 Some(RecordBatchMetrics {
558 region_watermarks,
559 ..Default::default()
560 })
561}
562
563#[derive(Debug, Default, Clone)]
565pub(crate) enum PeerDesc {
566 #[default]
568 Unknown,
569 Dist {
571 peer: Peer,
573 },
574 Standalone,
576}
577
578impl std::fmt::Display for PeerDesc {
579 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
580 match self {
581 PeerDesc::Unknown => write!(f, "unknown"),
582 PeerDesc::Dist { peer } => write!(f, "{}", peer.addr),
583 PeerDesc::Standalone => write!(f, "standalone"),
584 }
585 }
586}
587
588#[cfg(test)]
589mod tests {
590 use std::pin::Pin;
591 use std::task::{Context, Poll};
592 use std::time::Duration;
593
594 use common_query::{Output, OutputData};
595 use common_recordbatch::adapter::RecordBatchMetrics;
596 use common_recordbatch::{OrderOption, RecordBatch, RecordBatchStream};
597 use datatypes::prelude::{ConcreteDataType, VectorRef};
598 use datatypes::schema::{ColumnSchema, Schema};
599 use datatypes::vectors::Int32Vector;
600 use futures::StreamExt;
601 use tokio::time::timeout;
602
603 use super::*;
604
605 #[derive(Debug)]
606 struct NoopHandler;
607
608 struct MockMetricsStream {
609 schema: datatypes::schema::SchemaRef,
610 batch: Option<RecordBatch>,
611 metrics: RecordBatchMetrics,
612 terminal_metrics_only: bool,
613 }
614
615 impl futures::Stream for MockMetricsStream {
616 type Item = common_recordbatch::error::Result<RecordBatch>;
617
618 fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
619 Poll::Ready(self.batch.take().map(Ok))
620 }
621
622 fn size_hint(&self) -> (usize, Option<usize>) {
623 (
624 usize::from(self.batch.is_some()),
625 Some(usize::from(self.batch.is_some())),
626 )
627 }
628 }
629
630 impl RecordBatchStream for MockMetricsStream {
631 fn name(&self) -> &str {
632 "MockMetricsStream"
633 }
634
635 fn schema(&self) -> datatypes::schema::SchemaRef {
636 self.schema.clone()
637 }
638
639 fn output_ordering(&self) -> Option<&[OrderOption]> {
640 None
641 }
642
643 fn metrics(&self) -> Option<RecordBatchMetrics> {
644 if self.terminal_metrics_only && self.batch.is_some() {
645 return None;
646 }
647 Some(self.metrics.clone())
648 }
649 }
650
651 #[derive(Debug)]
652 struct MetricsHandler;
653
654 #[derive(Debug)]
655 struct ExtensionAwareHandler;
656
657 #[derive(Debug)]
658 struct SnapshotBindingHandler;
659
660 #[async_trait::async_trait]
661 impl GrpcQueryHandlerWithBoxedError for NoopHandler {
662 async fn do_query(
663 &self,
664 _query: Request,
665 _ctx: QueryContextRef,
666 ) -> std::result::Result<Output, BoxedError> {
667 Ok(Output::new_with_affected_rows(0))
668 }
669 }
670
671 #[async_trait::async_trait]
672 impl GrpcQueryHandlerWithBoxedError for MetricsHandler {
673 async fn do_query(
674 &self,
675 _query: Request,
676 _ctx: QueryContextRef,
677 ) -> std::result::Result<Output, BoxedError> {
678 let schema = Arc::new(Schema::new(vec![ColumnSchema::new(
679 "v",
680 ConcreteDataType::int32_datatype(),
681 false,
682 )]));
683 let batch = RecordBatch::new(
684 schema.clone(),
685 vec![Arc::new(Int32Vector::from_slice([1, 2])) as VectorRef],
686 )
687 .unwrap();
688 Ok(Output::new_with_stream(Box::pin(MockMetricsStream {
689 schema,
690 batch: Some(batch),
691 metrics: RecordBatchMetrics {
692 region_watermarks: vec![common_recordbatch::adapter::RegionWatermarkEntry {
693 region_id: 42,
694 watermark: Some(99),
695 }],
696 ..Default::default()
697 },
698 terminal_metrics_only: true,
699 })))
700 }
701 }
702
703 #[async_trait::async_trait]
704 impl GrpcQueryHandlerWithBoxedError for ExtensionAwareHandler {
705 async fn do_query(
706 &self,
707 _query: Request,
708 ctx: QueryContextRef,
709 ) -> std::result::Result<Output, BoxedError> {
710 assert_eq!(ctx.extension("flow.return_region_seq"), Some("true"));
711 Ok(Output::new_with_affected_rows(1))
712 }
713 }
714
715 #[async_trait::async_trait]
716 impl GrpcQueryHandlerWithBoxedError for SnapshotBindingHandler {
717 async fn do_query(
718 &self,
719 _query: Request,
720 ctx: QueryContextRef,
721 ) -> std::result::Result<Output, BoxedError> {
722 assert_eq!(ctx.extension("flow.return_region_seq"), Some("true"));
723 ctx.set_snapshot(42, 99);
724 Ok(Output::new_with_affected_rows(1))
725 }
726 }
727
728 #[tokio::test]
729 async fn wait_initialized() {
730 let (client, handler_mut) =
731 FrontendClient::from_empty_grpc_handler(QueryOptions::default());
732
733 assert!(
734 timeout(Duration::from_millis(50), client.wait_initialized())
735 .await
736 .is_err()
737 );
738
739 let handler: Arc<dyn GrpcQueryHandlerWithBoxedError> = Arc::new(NoopHandler);
740 handler_mut.set_handler(Arc::downgrade(&handler)).await;
741
742 timeout(Duration::from_secs(1), client.wait_initialized())
743 .await
744 .expect("wait_initialized should complete after handler is set");
745
746 timeout(Duration::from_millis(10), client.wait_initialized())
747 .await
748 .expect("wait_initialized should be a no-op once initialized");
749
750 let handler: Arc<dyn GrpcQueryHandlerWithBoxedError> = Arc::new(NoopHandler);
751 let client =
752 FrontendClient::from_grpc_handler(Arc::downgrade(&handler), QueryOptions::default());
753 assert!(
754 timeout(Duration::from_millis(10), client.wait_initialized())
755 .await
756 .is_ok()
757 );
758
759 let meta_client = Arc::new(MetaClient::new(0, api::v1::meta::Role::Frontend));
760 let client = FrontendClient::from_meta_client(
761 meta_client,
762 None,
763 QueryOptions::default(),
764 BatchingModeOptions::default(),
765 )
766 .unwrap();
767 assert!(
768 timeout(Duration::from_millis(10), client.wait_initialized())
769 .await
770 .is_ok()
771 );
772 }
773
774 #[tokio::test]
775 async fn test_query_with_terminal_metrics_tracks_watermark_in_standalone_mode() {
776 let handler: Arc<dyn GrpcQueryHandlerWithBoxedError> = Arc::new(MetricsHandler);
777 let client =
778 FrontendClient::from_grpc_handler(Arc::downgrade(&handler), QueryOptions::default());
779 let mut peer_desc = None;
780
781 let result = client
782 .query_with_terminal_metrics(
783 "greptime",
784 "public",
785 QueryRequest {
786 query: Some(Query::Sql("select 1".to_string())),
787 },
788 &[],
789 &mut peer_desc,
790 )
791 .await
792 .unwrap();
793 assert!(matches!(peer_desc, Some(PeerDesc::Standalone)));
794
795 let terminal_metrics = result.metrics.clone();
796 assert!(!result.metrics.is_ready());
797 assert!(terminal_metrics.get().is_none());
798
799 let OutputData::Stream(mut stream) = result.output.data else {
800 panic!("expected stream output");
801 };
802 while stream.next().await.is_some() {}
803
804 assert!(terminal_metrics.is_ready());
805 assert_eq!(
806 terminal_metrics.region_watermark_map(),
807 Some(HashMap::from([(42_u64, 99_u64)]))
808 );
809 }
810
811 #[tokio::test]
812 async fn test_query_with_terminal_metrics_forwards_flow_extensions_in_standalone_mode() {
813 let handler: Arc<dyn GrpcQueryHandlerWithBoxedError> = Arc::new(ExtensionAwareHandler);
814 let client =
815 FrontendClient::from_grpc_handler(Arc::downgrade(&handler), QueryOptions::default());
816 let mut peer_desc = None;
817
818 let result = client
819 .query_with_terminal_metrics(
820 "greptime",
821 "public",
822 QueryRequest {
823 query: Some(Query::Sql("insert into t select 1".to_string())),
824 },
825 &[("flow.return_region_seq", "true")],
826 &mut peer_desc,
827 )
828 .await
829 .unwrap();
830 assert!(matches!(peer_desc, Some(PeerDesc::Standalone)));
831
832 assert!(result.metrics.is_ready());
833 assert!(result.region_watermark_map().is_none());
834 }
835
836 #[tokio::test]
837 async fn test_query_with_terminal_metrics_uses_standalone_snapshot_bounds() {
838 let handler: Arc<dyn GrpcQueryHandlerWithBoxedError> = Arc::new(SnapshotBindingHandler);
839 let client =
840 FrontendClient::from_grpc_handler(Arc::downgrade(&handler), QueryOptions::default());
841 let mut peer_desc = None;
842
843 let result = client
844 .query_with_terminal_metrics(
845 "greptime",
846 "public",
847 QueryRequest {
848 query: Some(Query::Sql("insert into t select * from src".to_string())),
849 },
850 &[("flow.return_region_seq", "true")],
851 &mut peer_desc,
852 )
853 .await
854 .unwrap();
855 assert!(matches!(peer_desc, Some(PeerDesc::Standalone)));
856
857 assert!(result.metrics.is_ready());
858 assert_eq!(
859 result.region_watermark_map(),
860 Some(HashMap::from([(42, 99)]))
861 );
862 }
863
864 #[tokio::test]
865 async fn test_query_with_terminal_metrics_rejects_invalid_flow_extensions() {
866 let handler: Arc<dyn GrpcQueryHandlerWithBoxedError> = Arc::new(NoopHandler);
867 let client =
868 FrontendClient::from_grpc_handler(Arc::downgrade(&handler), QueryOptions::default());
869 let mut peer_desc = None;
870
871 let err = client
872 .query_with_terminal_metrics(
873 "greptime",
874 "public",
875 QueryRequest {
876 query: Some(Query::Sql("select 1".to_string())),
877 },
878 &[("flow.return_region_seq", "not-a-bool")],
879 &mut peer_desc,
880 )
881 .await
882 .unwrap_err();
883
884 assert!(format!("{err:?}").contains("Invalid value for flow.return_region_seq"));
885 }
886}