1use std::collections::HashMap;
18use std::sync::{Arc, Mutex, RwLock, Weak};
19
20use api::v1::greptime_request::Request;
21use api::v1::query_request::Query;
22use api::v1::{CreateTableExpr, QueryRequest};
23use client::{Client, DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME, 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_telemetry::warn;
29use futures::stream::{FuturesUnordered, StreamExt};
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::Error;
43use crate::batching_mode::BatchingModeOptions;
44use crate::error::{
45 CreateSinkTableSnafu, ExternalSnafu, InvalidClientConfigSnafu, InvalidRequestSnafu,
46 NoAvailableFrontendSnafu, UnexpectedSnafu,
47};
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 query: QueryOptions,
96 batch_opts: BatchingModeOptions,
97 },
98 Standalone {
99 database_client: HandlerMutable,
102 query: QueryOptions,
103 },
104}
105
106impl FrontendClient {
107 pub fn from_empty_grpc_handler(query: QueryOptions) -> (Self, HandlerMutable) {
109 let is_initialized = Arc::new(SetOnce::new());
110 let handler = HandlerMutable {
111 handler: Arc::new(Mutex::new(None)),
112 is_initialized,
113 };
114 (
115 Self::Standalone {
116 database_client: handler.clone(),
117 query,
118 },
119 handler,
120 )
121 }
122
123 pub async fn wait_initialized(&self) {
125 if let FrontendClient::Standalone {
126 database_client, ..
127 } = self
128 {
129 database_client.is_initialized.wait().await;
130 }
131 }
132
133 pub fn from_meta_client(
134 meta_client: Arc<MetaClient>,
135 query: QueryOptions,
136 batch_opts: BatchingModeOptions,
137 ) -> Result<Self, Error> {
138 common_telemetry::info!("Frontend client build without auth");
139 Ok(Self::Distributed {
140 meta_client,
141 chnl_mgr: {
142 let cfg = ChannelConfig::new()
143 .connect_timeout(batch_opts.grpc_conn_timeout)
144 .timeout(Some(batch_opts.query_timeout));
145
146 let tls_config = load_client_tls_config(batch_opts.frontend_tls.clone())
147 .context(InvalidClientConfigSnafu)?;
148 ChannelManager::with_config(cfg, tls_config)
149 },
150 query,
151 batch_opts,
152 })
153 }
154
155 pub fn from_grpc_handler(
156 grpc_handler: Weak<dyn GrpcQueryHandlerWithBoxedError>,
157 query: QueryOptions,
158 ) -> Self {
159 let is_initialized = Arc::new(SetOnce::new_with(Some(())));
160 let handler = HandlerMutable {
161 handler: Arc::new(Mutex::new(Some(grpc_handler))),
162 is_initialized: is_initialized.clone(),
163 };
164
165 Self::Standalone {
166 database_client: handler,
167 query,
168 }
169 }
170}
171
172#[derive(Debug, Clone)]
173pub struct DatabaseWithPeer {
174 pub database: Database,
175 pub peer: Peer,
176}
177
178impl DatabaseWithPeer {
179 fn new(database: Database, peer: Peer) -> Self {
180 Self { database, peer }
181 }
182
183 async fn try_select_one(&self) -> Result<(), Error> {
185 let _ = self
187 .database
188 .sql("SELECT 1")
189 .await
190 .with_context(|_| InvalidRequestSnafu {
191 context: format!("Failed to handle `SELECT 1` request at {:?}", self.peer),
192 })?;
193 Ok(())
194 }
195}
196
197impl FrontendClient {
198 pub(crate) async fn scan_for_frontend(&self) -> Result<Vec<Peer>, Error> {
200 let Self::Distributed { meta_client, .. } = self else {
201 return Ok(vec![]);
202 };
203
204 meta_client
205 .active_frontends()
206 .await
207 .map(|nodes| nodes.into_iter().map(|node| node.peer).collect())
208 .map_err(BoxedError::new)
209 .context(ExternalSnafu)
210 }
211
212 pub(crate) async fn check_all_frontends_without_auth(
218 &self,
219 frontends: &[Peer],
220 ) -> Result<Vec<String>, Error> {
221 let Self::Distributed {
222 chnl_mgr,
223 batch_opts,
224 ..
225 } = self
226 else {
227 return Ok(vec![]);
228 };
229
230 let probe_timeout = batch_opts.grpc_conn_timeout;
231 let mut probes = frontends
232 .iter()
233 .map(|peer| {
234 let addr = peer.addr.clone();
235 let chnl_mgr = chnl_mgr.clone();
236
237 async move {
238 let client = Client::with_manager_and_urls(chnl_mgr, vec![addr.clone()]);
239 let database = Database::new(DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME, client);
240
241 match tokio::time::timeout(probe_timeout, database.sql("SELECT 1")).await {
242 Ok(Ok(_)) => Ok(None),
243 Ok(Err(err)) if err.tonic_code() == Some(tonic::Code::Unauthenticated) => {
244 Err(err).context(InvalidRequestSnafu {
245 context: format!(
246 "Frontend {addr} rejected unauthenticated flownode probe; ensure frontend internal_grpc is advertised to metasrv"
247 ),
248 })
249 }
250 Ok(Err(err)) => Ok(Some(format!("{addr}: {err}"))),
251 Err(_) => Ok(Some(format!(
252 "{addr}: health check timed out after {probe_timeout:?}"
253 ))),
254 }
255 }
256 })
257 .collect::<FuturesUnordered<_>>();
258
259 let mut failures = Vec::new();
260 while let Some(probe_result) = probes.next().await {
261 if let Some(failure) = probe_result? {
262 failures.push(failure);
263 }
264 }
265
266 Ok(failures)
267 }
268
269 async fn get_random_active_frontend(
271 &self,
272 catalog: &str,
273 schema: &str,
274 ) -> Result<DatabaseWithPeer, Error> {
275 let Self::Distributed {
276 meta_client: _,
277 chnl_mgr,
278 query: _,
279 batch_opts,
280 } = self
281 else {
282 return UnexpectedSnafu {
283 reason: "Expect distributed mode",
284 }
285 .fail();
286 };
287
288 let mut interval = tokio::time::interval(batch_opts.grpc_conn_timeout);
289 interval.tick().await;
290 for retry in 0..batch_opts.experimental_grpc_max_retries {
291 let mut frontends = self.scan_for_frontend().await?;
292 frontends.shuffle(&mut rng());
294
295 for peer in frontends {
296 let addr = peer.addr.clone();
297 let client = Client::with_manager_and_urls(chnl_mgr.clone(), vec![addr.clone()]);
298 let database = Database::new(catalog, schema, client);
299 let db = DatabaseWithPeer::new(database, peer);
300 match db.try_select_one().await {
301 Ok(_) => return Ok(db),
302 Err(e) => {
303 warn!(
304 "Failed to connect to frontend {} on retry={}: \n{e:?}",
305 addr, retry
306 );
307 }
308 }
309 }
310 interval.tick().await;
313 }
314
315 NoAvailableFrontendSnafu {
316 timeout: batch_opts.grpc_conn_timeout,
317 context: "No available frontend found that is able to process query",
318 }
319 .fail()
320 }
321
322 pub async fn create(
323 &self,
324 create: CreateTableExpr,
325 catalog: &str,
326 schema: &str,
327 ) -> Result<u32, Error> {
328 self.handle(
329 Request::Ddl(api::v1::DdlRequest {
330 expr: Some(api::v1::ddl_request::Expr::CreateTable(create.clone())),
331 }),
332 catalog,
333 schema,
334 &mut None,
335 )
336 .await
337 .map_err(BoxedError::new)
338 .with_context(|_| CreateSinkTableSnafu {
339 create: create.clone(),
340 })
341 }
342
343 pub async fn sql(&self, catalog: &str, schema: &str, sql: &str) -> Result<Output, Error> {
345 match self {
346 FrontendClient::Distributed { .. } => {
347 let db = self.get_random_active_frontend(catalog, schema).await?;
348 db.database
349 .sql(sql)
350 .await
351 .map_err(BoxedError::new)
352 .context(ExternalSnafu)
353 }
354 FrontendClient::Standalone {
355 database_client, ..
356 } => {
357 let ctx = QueryContextBuilder::default()
358 .current_catalog(catalog.to_string())
359 .current_schema(schema.to_string())
360 .build();
361 let ctx = Arc::new(ctx);
362 {
363 let database_client = {
364 database_client
365 .handler
366 .lock()
367 .unwrap()
368 .as_ref()
369 .context(UnexpectedSnafu {
370 reason: "Standalone's frontend instance is not set",
371 })?
372 .upgrade()
373 .context(UnexpectedSnafu {
374 reason: "Failed to upgrade database client",
375 })?
376 };
377 let req = Request::Query(QueryRequest {
378 query: Some(Query::Sql(sql.to_string())),
379 });
380 database_client
381 .do_query(req, ctx)
382 .await
383 .map_err(BoxedError::new)
384 .context(ExternalSnafu)
385 }
386 }
387 }
388 }
389
390 pub(crate) async fn query_with_terminal_metrics(
393 &self,
394 catalog: &str,
395 schema: &str,
396 request: QueryRequest,
397 extensions: &[(&str, &str)],
398 snapshot_seqs: &HashMap<u64, u64>,
399 peer_desc: &mut Option<PeerDesc>,
400 ) -> Result<OutputWithMetrics, Error> {
401 let flow_extensions = build_flow_extensions(extensions)?;
402 match self {
403 FrontendClient::Distributed {
404 query, batch_opts, ..
405 } => {
406 let query_parallelism = query.parallelism.to_string();
407 let hints = vec![
408 (QUERY_PARALLELISM_HINT, query_parallelism.as_str()),
409 (READ_PREFERENCE_HINT, batch_opts.read_preference.as_ref()),
410 ];
411 let db = self.get_random_active_frontend(catalog, schema).await?;
412 *peer_desc = Some(PeerDesc::Dist {
413 peer: db.peer.clone(),
414 });
415 db.database
416 .flight_request()
417 .with_hints(&hints)
418 .with_flow_extensions(extensions)
419 .with_snapshot_seqs(snapshot_seqs)
420 .with_timeout(batch_opts.experimental_flight_do_get_timeout)
421 .query_with_terminal_metrics(request)
422 .await
423 .map_err(BoxedError::new)
424 .context(ExternalSnafu)
425 }
426 FrontendClient::Standalone {
427 database_client,
428 query,
429 } => {
430 *peer_desc = Some(PeerDesc::Standalone);
431 let mut extensions_map = HashMap::from([(
432 QUERY_PARALLELISM_HINT.to_string(),
433 query.parallelism.to_string(),
434 )]);
435 for (key, value) in extensions {
436 extensions_map.insert((*key).to_string(), (*value).to_string());
437 }
438 let ctx = QueryContextBuilder::default()
439 .current_catalog(catalog.to_string())
440 .current_schema(schema.to_string())
441 .extensions(extensions_map)
442 .snapshot_seqs(Arc::new(RwLock::new(snapshot_seqs.clone())))
443 .build();
444 let ctx = Arc::new(ctx);
445 let database_client = {
446 database_client
447 .handler
448 .lock()
449 .map_err(|e| {
450 UnexpectedSnafu {
451 reason: format!("Failed to lock database client: {e}"),
452 }
453 .build()
454 })?
455 .as_ref()
456 .context(UnexpectedSnafu {
457 reason: "Standalone's frontend instance is not set",
458 })?
459 .upgrade()
460 .context(UnexpectedSnafu {
461 reason: "Failed to upgrade database client",
462 })?
463 };
464 database_client
465 .do_query(Request::Query(request), ctx.clone())
466 .await
467 .map(|output| {
468 wrap_standalone_output_with_terminal_metrics(output, &flow_extensions)
469 })
470 .map_err(BoxedError::new)
471 .context(ExternalSnafu)
472 }
473 }
474 }
475
476 pub(crate) async fn handle(
478 &self,
479 req: api::v1::greptime_request::Request,
480 catalog: &str,
481 schema: &str,
482 peer_desc: &mut Option<PeerDesc>,
483 ) -> Result<u32, Error> {
484 match self {
485 FrontendClient::Distributed {
486 query, batch_opts, ..
487 } => {
488 let db = self.get_random_active_frontend(catalog, schema).await?;
489
490 *peer_desc = Some(PeerDesc::Dist {
491 peer: db.peer.clone(),
492 });
493
494 db.database
495 .handle_with_retry(
496 req.clone(),
497 batch_opts.experimental_grpc_max_retries,
498 &[
499 (QUERY_PARALLELISM_HINT, &query.parallelism.to_string()),
500 (READ_PREFERENCE_HINT, batch_opts.read_preference.as_ref()),
501 ],
502 )
503 .await
504 .with_context(|_| InvalidRequestSnafu {
505 context: format!("Failed to handle request at {:?}: {:?}", db.peer, req),
506 })
507 }
508 FrontendClient::Standalone {
509 database_client,
510 query,
511 } => {
512 let ctx = QueryContextBuilder::default()
513 .current_catalog(catalog.to_string())
514 .current_schema(schema.to_string())
515 .extensions(HashMap::from([(
516 QUERY_PARALLELISM_HINT.to_string(),
517 query.parallelism.to_string(),
518 )]))
519 .build();
520 let ctx = Arc::new(ctx);
521 {
522 let database_client = {
523 database_client
524 .handler
525 .lock()
526 .unwrap()
527 .as_ref()
528 .context(UnexpectedSnafu {
529 reason: "Standalone's frontend instance is not set",
530 })?
531 .upgrade()
532 .context(UnexpectedSnafu {
533 reason: "Failed to upgrade database client",
534 })?
535 };
536 let resp: common_query::Output = database_client
537 .do_query(req, ctx)
538 .await
539 .map_err(BoxedError::new)
540 .context(ExternalSnafu)?;
541 match resp.data {
542 common_query::OutputData::AffectedRows(rows) => {
543 Ok(rows.try_into().map_err(|_| {
544 UnexpectedSnafu {
545 reason: format!("Failed to convert rows to u32: {}", rows),
546 }
547 .build()
548 })?)
549 }
550 _ => UnexpectedSnafu {
551 reason: "Unexpected output data",
552 }
553 .fail(),
554 }
555 }
556 }
557 }
558 }
559}
560
561fn build_flow_extensions(extensions: &[(&str, &str)]) -> Result<FlowQueryExtensions, Error> {
562 let flow_extensions = HashMap::from_iter(
563 extensions
564 .iter()
565 .map(|(key, value)| ((*key).to_string(), (*value).to_string())),
566 );
567 FlowQueryExtensions::parse_flow_extensions(&flow_extensions)
568 .map_err(BoxedError::new)
569 .context(ExternalSnafu)
570 .map(|extensions| extensions.unwrap_or_default())
571}
572
573fn wrap_standalone_output_with_terminal_metrics(
574 output: Output,
575 flow_extensions: &FlowQueryExtensions,
576) -> OutputWithMetrics {
577 let should_collect_region_watermark = flow_extensions.should_collect_region_watermark();
578 let terminal_metrics =
579 if should_collect_region_watermark && !matches!(&output.data, OutputData::Stream(_)) {
580 output
581 .meta
582 .plan
583 .clone()
584 .and_then(terminal_recordbatch_metrics_from_plan)
585 } else {
586 None
587 };
588 let result = OutputWithMetrics::from_output(output);
589 if let Some(metrics) = terminal_metrics {
590 result.metrics.update(Some(metrics));
591 }
592 result
593}
594
595#[derive(Debug, Default, Clone)]
597pub(crate) enum PeerDesc {
598 #[default]
600 Unknown,
601 Dist {
603 peer: Peer,
605 },
606 Standalone,
608}
609
610impl std::fmt::Display for PeerDesc {
611 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
612 match self {
613 PeerDesc::Unknown => write!(f, "unknown"),
614 PeerDesc::Dist { peer } => write!(f, "{}", peer.addr),
615 PeerDesc::Standalone => write!(f, "standalone"),
616 }
617 }
618}
619
620#[cfg(test)]
621mod tests {
622 use std::pin::Pin;
623 use std::task::{Context, Poll};
624 use std::time::Duration;
625
626 use api::v1::query_request::Query;
627 use arrow_flight::flight_service_server::FlightServiceServer;
628 use arrow_flight::{FlightData, Ticket};
629 use common_query::{Output, OutputData};
630 use common_recordbatch::adapter::RecordBatchMetrics;
631 use common_recordbatch::{OrderOption, RecordBatch, RecordBatchStream};
632 use datatypes::prelude::{ConcreteDataType, VectorRef};
633 use datatypes::schema::{ColumnSchema, Schema};
634 use datatypes::vectors::Int32Vector;
635 use futures::StreamExt;
636 use servers::grpc::flight::{FlightCraft, FlightCraftWrapper, TonicStream};
637 use tokio::net::TcpListener;
638 use tokio::task::JoinHandle;
639 use tokio::time::timeout;
640 use tokio_stream::wrappers::TcpListenerStream;
641 use tonic::{Request as TonicRequest, Response as TonicResponse, Status};
642
643 use super::*;
644
645 #[derive(Debug)]
646 struct NoopHandler;
647
648 struct MockMetricsStream {
649 schema: datatypes::schema::SchemaRef,
650 batch: Option<RecordBatch>,
651 metrics: RecordBatchMetrics,
652 terminal_metrics_only: bool,
653 }
654
655 impl futures::Stream for MockMetricsStream {
656 type Item = common_recordbatch::error::Result<RecordBatch>;
657
658 fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
659 Poll::Ready(self.batch.take().map(Ok))
660 }
661
662 fn size_hint(&self) -> (usize, Option<usize>) {
663 (
664 usize::from(self.batch.is_some()),
665 Some(usize::from(self.batch.is_some())),
666 )
667 }
668 }
669
670 impl RecordBatchStream for MockMetricsStream {
671 fn name(&self) -> &str {
672 "MockMetricsStream"
673 }
674
675 fn schema(&self) -> datatypes::schema::SchemaRef {
676 self.schema.clone()
677 }
678
679 fn output_ordering(&self) -> Option<&[OrderOption]> {
680 None
681 }
682
683 fn metrics(&self) -> Option<RecordBatchMetrics> {
684 if self.terminal_metrics_only && self.batch.is_some() {
685 return None;
686 }
687 Some(self.metrics.clone())
688 }
689 }
690
691 #[derive(Debug)]
692 struct MetricsHandler;
693
694 #[derive(Debug)]
695 struct ExtensionAwareHandler;
696
697 #[derive(Debug)]
698 struct SnapshotBindingHandler;
699
700 #[derive(Debug)]
701 struct RejectUnauthenticatedFlight;
702
703 #[derive(Debug)]
704 struct SlowFlight;
705
706 struct WaitForConcurrentFlight {
707 barrier: Arc<tokio::sync::Barrier>,
708 }
709
710 #[async_trait::async_trait]
711 impl GrpcQueryHandlerWithBoxedError for NoopHandler {
712 async fn do_query(
713 &self,
714 _query: Request,
715 _ctx: QueryContextRef,
716 ) -> std::result::Result<Output, BoxedError> {
717 Ok(Output::new_with_affected_rows(0))
718 }
719 }
720
721 #[async_trait::async_trait]
722 impl GrpcQueryHandlerWithBoxedError for MetricsHandler {
723 async fn do_query(
724 &self,
725 _query: Request,
726 _ctx: QueryContextRef,
727 ) -> std::result::Result<Output, BoxedError> {
728 let schema = Arc::new(Schema::new(vec![ColumnSchema::new(
729 "v",
730 ConcreteDataType::int32_datatype(),
731 false,
732 )]));
733 let batch = RecordBatch::new(
734 schema.clone(),
735 vec![Arc::new(Int32Vector::from_slice([1, 2])) as VectorRef],
736 )
737 .unwrap();
738 Ok(Output::new_with_stream(Box::pin(MockMetricsStream {
739 schema,
740 batch: Some(batch),
741 metrics: RecordBatchMetrics {
742 region_watermarks: vec![common_recordbatch::adapter::RegionWatermarkEntry {
743 region_id: 42,
744 watermark: Some(99),
745 }],
746 ..Default::default()
747 },
748 terminal_metrics_only: true,
749 })))
750 }
751 }
752
753 #[async_trait::async_trait]
754 impl GrpcQueryHandlerWithBoxedError for ExtensionAwareHandler {
755 async fn do_query(
756 &self,
757 _query: Request,
758 ctx: QueryContextRef,
759 ) -> std::result::Result<Output, BoxedError> {
760 assert_eq!(ctx.extension("flow.return_region_seq"), Some("true"));
761 Ok(Output::new_with_affected_rows(1))
762 }
763 }
764
765 #[async_trait::async_trait]
766 impl GrpcQueryHandlerWithBoxedError for SnapshotBindingHandler {
767 async fn do_query(
768 &self,
769 _query: Request,
770 ctx: QueryContextRef,
771 ) -> std::result::Result<Output, BoxedError> {
772 assert_eq!(ctx.extension("flow.return_region_seq"), Some("true"));
773 assert_eq!(ctx.get_snapshot(1), Some(10));
774 assert_eq!(ctx.get_snapshot(2), Some(20));
775 ctx.set_snapshot(42, 99);
776 Ok(Output::new_with_affected_rows(1))
777 }
778 }
779
780 #[async_trait::async_trait]
781 impl FlightCraft for RejectUnauthenticatedFlight {
782 async fn do_get(
783 &self,
784 _request: TonicRequest<Ticket>,
785 ) -> std::result::Result<TonicResponse<TonicStream<FlightData>>, Status> {
786 Err(Status::unauthenticated("auth failed"))
787 }
788 }
789
790 #[async_trait::async_trait]
791 impl FlightCraft for SlowFlight {
792 async fn do_get(
793 &self,
794 _request: TonicRequest<Ticket>,
795 ) -> std::result::Result<TonicResponse<TonicStream<FlightData>>, Status> {
796 tokio::time::sleep(Duration::from_secs(60)).await;
797 Err(Status::unavailable("slow response"))
798 }
799 }
800
801 #[async_trait::async_trait]
802 impl FlightCraft for WaitForConcurrentFlight {
803 async fn do_get(
804 &self,
805 _request: TonicRequest<Ticket>,
806 ) -> std::result::Result<TonicResponse<TonicStream<FlightData>>, Status> {
807 self.barrier.wait().await;
808 Err(Status::unavailable("probe started concurrently"))
809 }
810 }
811
812 async fn start_flight_server<T: FlightCraft>(handler: T) -> (String, JoinHandle<()>) {
813 let listener = TcpListener::bind("127.0.0.1:0")
814 .await
815 .expect("bind test flight server");
816 let addr = listener.local_addr().expect("local addr").to_string();
817 let server = tokio::spawn(async move {
818 tonic::transport::Server::builder()
819 .add_service(FlightServiceServer::new(FlightCraftWrapper(handler)))
820 .serve_with_incoming(TcpListenerStream::new(listener))
821 .await
822 .expect("serve test flight server");
823 });
824
825 (addr, server)
826 }
827
828 #[tokio::test]
829 async fn wait_initialized() {
830 let (client, handler_mut) =
831 FrontendClient::from_empty_grpc_handler(QueryOptions::default());
832
833 assert!(
834 timeout(Duration::from_millis(50), client.wait_initialized())
835 .await
836 .is_err()
837 );
838
839 let handler: Arc<dyn GrpcQueryHandlerWithBoxedError> = Arc::new(NoopHandler);
840 handler_mut.set_handler(Arc::downgrade(&handler)).await;
841
842 timeout(Duration::from_secs(1), client.wait_initialized())
843 .await
844 .expect("wait_initialized should complete after handler is set");
845
846 timeout(Duration::from_millis(10), client.wait_initialized())
847 .await
848 .expect("wait_initialized should be a no-op once initialized");
849
850 let handler: Arc<dyn GrpcQueryHandlerWithBoxedError> = Arc::new(NoopHandler);
851 let client =
852 FrontendClient::from_grpc_handler(Arc::downgrade(&handler), QueryOptions::default());
853 assert!(
854 timeout(Duration::from_millis(10), client.wait_initialized())
855 .await
856 .is_ok()
857 );
858
859 let meta_client = Arc::new(MetaClient::new(0, api::v1::meta::Role::Frontend));
860 let client = FrontendClient::from_meta_client(
861 meta_client,
862 QueryOptions::default(),
863 BatchingModeOptions::default(),
864 )
865 .unwrap();
866 assert!(
867 timeout(Duration::from_millis(10), client.wait_initialized())
868 .await
869 .is_ok()
870 );
871 }
872
873 #[tokio::test]
874 async fn test_query_with_terminal_metrics_tracks_watermark_in_standalone_mode() {
875 let handler: Arc<dyn GrpcQueryHandlerWithBoxedError> = Arc::new(MetricsHandler);
876 let client =
877 FrontendClient::from_grpc_handler(Arc::downgrade(&handler), QueryOptions::default());
878 let mut peer_desc = None;
879
880 let result = client
881 .query_with_terminal_metrics(
882 "greptime",
883 "public",
884 QueryRequest {
885 query: Some(Query::Sql("select 1".to_string())),
886 },
887 &[],
888 &HashMap::new(),
889 &mut peer_desc,
890 )
891 .await
892 .unwrap();
893 assert!(matches!(peer_desc, Some(PeerDesc::Standalone)));
894
895 let terminal_metrics = result.metrics.clone();
896 assert!(!result.metrics.is_ready());
897 assert!(terminal_metrics.get().is_none());
898
899 let OutputData::Stream(mut stream) = result.output.data else {
900 panic!("expected stream output");
901 };
902 while stream.next().await.is_some() {}
903
904 assert!(terminal_metrics.is_ready());
905 assert_eq!(
906 terminal_metrics.region_watermark_map(),
907 Some(HashMap::from([(42_u64, 99_u64)]))
908 );
909 }
910
911 #[tokio::test]
912 async fn test_query_with_terminal_metrics_forwards_flow_extensions_in_standalone_mode() {
913 let handler: Arc<dyn GrpcQueryHandlerWithBoxedError> = Arc::new(ExtensionAwareHandler);
914 let client =
915 FrontendClient::from_grpc_handler(Arc::downgrade(&handler), QueryOptions::default());
916 let mut peer_desc = None;
917
918 let result = client
919 .query_with_terminal_metrics(
920 "greptime",
921 "public",
922 QueryRequest {
923 query: Some(Query::Sql("insert into t select 1".to_string())),
924 },
925 &[("flow.return_region_seq", "true")],
926 &HashMap::new(),
927 &mut peer_desc,
928 )
929 .await
930 .unwrap();
931 assert!(matches!(peer_desc, Some(PeerDesc::Standalone)));
932
933 assert!(result.metrics.is_ready());
934 assert!(result.region_watermark_map().is_none());
935 }
936
937 #[tokio::test]
938 async fn test_query_with_terminal_metrics_uses_standalone_snapshot_bounds() {
939 let handler: Arc<dyn GrpcQueryHandlerWithBoxedError> = Arc::new(SnapshotBindingHandler);
940 let client =
941 FrontendClient::from_grpc_handler(Arc::downgrade(&handler), QueryOptions::default());
942 let mut peer_desc = None;
943
944 let result = client
945 .query_with_terminal_metrics(
946 "greptime",
947 "public",
948 QueryRequest {
949 query: Some(Query::Sql("insert into t select * from src".to_string())),
950 },
951 &[("flow.return_region_seq", "true")],
952 &HashMap::from([(1, 10), (2, 20)]),
953 &mut peer_desc,
954 )
955 .await
956 .unwrap();
957 assert!(matches!(peer_desc, Some(PeerDesc::Standalone)));
958
959 assert!(result.metrics.is_ready());
960 assert_eq!(result.region_watermark_map(), None);
961 }
962
963 #[tokio::test]
964 async fn test_query_with_terminal_metrics_rejects_invalid_flow_extensions() {
965 let handler: Arc<dyn GrpcQueryHandlerWithBoxedError> = Arc::new(NoopHandler);
966 let client =
967 FrontendClient::from_grpc_handler(Arc::downgrade(&handler), QueryOptions::default());
968 let mut peer_desc = None;
969
970 let err = client
971 .query_with_terminal_metrics(
972 "greptime",
973 "public",
974 QueryRequest {
975 query: Some(Query::Sql("select 1".to_string())),
976 },
977 &[("flow.return_region_seq", "not-a-bool")],
978 &HashMap::new(),
979 &mut peer_desc,
980 )
981 .await
982 .unwrap_err();
983
984 assert!(format!("{err:?}").contains("Invalid value for flow.return_region_seq"));
985 }
986
987 #[tokio::test]
988 async fn test_check_all_frontends_without_auth_fails_fast_on_unauthenticated_frontend() {
989 let (addr, server) = start_flight_server(RejectUnauthenticatedFlight).await;
990 let client = FrontendClient::from_meta_client(
991 Arc::new(MetaClient::new(0, api::v1::meta::Role::Frontend)),
992 QueryOptions::default(),
993 BatchingModeOptions::default(),
994 )
995 .unwrap();
996
997 let err = client
998 .check_all_frontends_without_auth(&[Peer {
999 id: 1,
1000 addr: addr.clone(),
1001 }])
1002 .await
1003 .unwrap_err();
1004 server.abort();
1005
1006 let Error::InvalidRequest {
1007 context, source, ..
1008 } = err
1009 else {
1010 panic!("expected InvalidRequest, got {err:?}");
1011 };
1012 assert!(context.contains(&addr));
1013 assert!(context.contains("rejected unauthenticated flownode probe"));
1014 assert_eq!(source.tonic_code(), Some(tonic::Code::Unauthenticated));
1015 }
1016
1017 #[tokio::test]
1018 async fn test_check_all_frontends_without_auth_uses_grpc_connection_timeout() {
1019 let (addr, server) = start_flight_server(SlowFlight).await;
1020 let client = FrontendClient::from_meta_client(
1021 Arc::new(MetaClient::new(0, api::v1::meta::Role::Frontend)),
1022 QueryOptions::default(),
1023 BatchingModeOptions {
1024 grpc_conn_timeout: Duration::from_millis(50),
1025 ..Default::default()
1026 },
1027 )
1028 .unwrap();
1029
1030 let failures = client
1031 .check_all_frontends_without_auth(&[Peer {
1032 id: 1,
1033 addr: addr.clone(),
1034 }])
1035 .await
1036 .unwrap();
1037 server.abort();
1038
1039 assert_eq!(failures.len(), 1);
1040 assert!(failures[0].contains(&addr));
1041 assert!(failures[0].contains("health check timed out"));
1042 }
1043
1044 #[tokio::test]
1045 async fn test_check_all_frontends_without_auth_checks_frontends_concurrently() {
1046 let barrier = Arc::new(tokio::sync::Barrier::new(2));
1047 let (addr1, server1) = start_flight_server(WaitForConcurrentFlight {
1048 barrier: barrier.clone(),
1049 })
1050 .await;
1051 let (addr2, server2) = start_flight_server(WaitForConcurrentFlight { barrier }).await;
1052 let client = FrontendClient::from_meta_client(
1053 Arc::new(MetaClient::new(0, api::v1::meta::Role::Frontend)),
1054 QueryOptions::default(),
1055 BatchingModeOptions {
1056 grpc_conn_timeout: Duration::from_millis(500),
1057 ..Default::default()
1058 },
1059 )
1060 .unwrap();
1061
1062 let failures = timeout(
1063 Duration::from_secs(2),
1064 client.check_all_frontends_without_auth(&[
1065 Peer {
1066 id: 1,
1067 addr: addr1.clone(),
1068 },
1069 Peer {
1070 id: 2,
1071 addr: addr2.clone(),
1072 },
1073 ]),
1074 )
1075 .await
1076 .expect("concurrent probes should complete before per-peer timeouts")
1077 .unwrap();
1078 server1.abort();
1079 server2.abort();
1080
1081 assert_eq!(failures.len(), 2);
1082 assert!(failures.iter().any(|failure| failure.contains(&addr1)));
1083 assert!(failures.iter().any(|failure| failure.contains(&addr2)));
1084 assert!(
1085 failures
1086 .iter()
1087 .all(|failure| !failure.contains("health check timed out")),
1088 "sequential probes would time out before both requests reach the barrier: {failures:?}"
1089 );
1090 }
1091}