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, RowInsertRequests};
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 output = 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
194 if let OutputData::Stream(stream) = output.data {
195 common_recordbatch::util::collect(stream)
196 .await
197 .map_err(BoxedError::new)
198 .context(ExternalSnafu)?;
199 }
200
201 Ok(())
202 }
203}
204
205impl FrontendClient {
206 pub(crate) async fn scan_for_frontend(&self) -> Result<Vec<Peer>, Error> {
208 let Self::Distributed { meta_client, .. } = self else {
209 return Ok(vec![]);
210 };
211
212 meta_client
213 .active_frontends()
214 .await
215 .map(|nodes| nodes.into_iter().map(|node| node.peer).collect())
216 .map_err(BoxedError::new)
217 .context(ExternalSnafu)
218 }
219
220 pub(crate) async fn check_all_frontends_without_auth(
226 &self,
227 frontends: &[Peer],
228 ) -> Result<Vec<String>, Error> {
229 let Self::Distributed {
230 chnl_mgr,
231 batch_opts,
232 ..
233 } = self
234 else {
235 return Ok(vec![]);
236 };
237
238 let probe_timeout = batch_opts.grpc_conn_timeout;
239 let mut probes = frontends
240 .iter()
241 .map(|peer| {
242 let addr = peer.addr.clone();
243 let chnl_mgr = chnl_mgr.clone();
244
245 async move {
246 let client = Client::with_manager_and_urls(chnl_mgr, vec![addr.clone()]);
247 let database = Database::new(DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME, client);
248
249 match tokio::time::timeout(probe_timeout, database.sql("SELECT 1")).await {
250 Ok(Ok(_)) => Ok(None),
251 Ok(Err(err)) if err.tonic_code() == Some(tonic::Code::Unauthenticated) => {
252 Err(err).context(InvalidRequestSnafu {
253 context: format!(
254 "Frontend {addr} rejected unauthenticated flownode probe; ensure frontend internal_grpc is advertised to metasrv"
255 ),
256 })
257 }
258 Ok(Err(err)) => Ok(Some(format!("{addr}: {err}"))),
259 Err(_) => Ok(Some(format!(
260 "{addr}: health check timed out after {probe_timeout:?}"
261 ))),
262 }
263 }
264 })
265 .collect::<FuturesUnordered<_>>();
266
267 let mut failures = Vec::new();
268 while let Some(probe_result) = probes.next().await {
269 if let Some(failure) = probe_result? {
270 failures.push(failure);
271 }
272 }
273
274 Ok(failures)
275 }
276
277 async fn get_random_active_frontend(
279 &self,
280 catalog: &str,
281 schema: &str,
282 ) -> Result<DatabaseWithPeer, Error> {
283 let Self::Distributed {
284 meta_client: _,
285 chnl_mgr,
286 query: _,
287 batch_opts,
288 } = self
289 else {
290 return UnexpectedSnafu {
291 reason: "Expect distributed mode",
292 }
293 .fail();
294 };
295
296 let mut interval = tokio::time::interval(batch_opts.grpc_conn_timeout);
297 interval.tick().await;
298 for retry in 0..batch_opts.experimental_grpc_max_retries {
299 let mut frontends = self.scan_for_frontend().await?;
300 frontends.shuffle(&mut rng());
302
303 for peer in frontends {
304 let addr = peer.addr.clone();
305 let client = Client::with_manager_and_urls(chnl_mgr.clone(), vec![addr.clone()]);
306 let database = Database::new(catalog, schema, client);
307 let db = DatabaseWithPeer::new(database, peer);
308 match db.try_select_one().await {
309 Ok(_) => return Ok(db),
310 Err(e) => {
311 warn!(
312 "Failed to connect to frontend {} on retry={}: \n{e:?}",
313 addr, retry
314 );
315 }
316 }
317 }
318 interval.tick().await;
321 }
322
323 NoAvailableFrontendSnafu {
324 timeout: batch_opts.grpc_conn_timeout,
325 context: "No available frontend found that is able to process query",
326 }
327 .fail()
328 }
329
330 pub async fn create(
331 &self,
332 create: CreateTableExpr,
333 catalog: &str,
334 schema: &str,
335 ) -> Result<u32, Error> {
336 self.handle(
337 Request::Ddl(api::v1::DdlRequest {
338 expr: Some(api::v1::ddl_request::Expr::CreateTable(create.clone())),
339 }),
340 catalog,
341 schema,
342 &mut None,
343 )
344 .await
345 .map_err(BoxedError::new)
346 .with_context(|_| CreateSinkTableSnafu {
347 create: create.clone(),
348 })
349 }
350
351 pub async fn sql(&self, catalog: &str, schema: &str, sql: &str) -> Result<Output, Error> {
353 match self {
354 FrontendClient::Distributed { .. } => {
355 let db = self.get_random_active_frontend(catalog, schema).await?;
356 db.database
357 .sql(sql)
358 .await
359 .map_err(BoxedError::new)
360 .context(ExternalSnafu)
361 }
362 FrontendClient::Standalone {
363 database_client, ..
364 } => {
365 let ctx = QueryContextBuilder::default()
366 .current_catalog(catalog.to_string())
367 .current_schema(schema.to_string())
368 .build();
369 let ctx = Arc::new(ctx);
370 {
371 let database_client = {
372 database_client
373 .handler
374 .lock()
375 .map_err(|e| {
376 UnexpectedSnafu {
377 reason: format!("Failed to lock database client: {e}"),
378 }
379 .build()
380 })?
381 .as_ref()
382 .context(UnexpectedSnafu {
383 reason: "Standalone's frontend instance is not set",
384 })?
385 .upgrade()
386 .context(UnexpectedSnafu {
387 reason: "Failed to upgrade database client",
388 })?
389 };
390 let req = Request::Query(QueryRequest {
391 query: Some(Query::Sql(sql.to_string())),
392 });
393 database_client
394 .do_query(req, ctx)
395 .await
396 .map_err(BoxedError::new)
397 .context(ExternalSnafu)
398 }
399 }
400 }
401 }
402
403 pub async fn row_inserts(
405 &self,
406 catalog: &str,
407 schema: &str,
408 requests: RowInsertRequests,
409 hints: &[(&str, &str)],
410 ) -> Result<u32, Error> {
411 match self {
412 FrontendClient::Distributed { .. } => {
413 let db = self.get_random_active_frontend(catalog, schema).await?;
414 db.database
415 .row_inserts_with_hints(requests, hints)
416 .await
417 .with_context(|_| InvalidRequestSnafu {
418 context: format!("Failed to handle row inserts at {:?}", db.peer),
419 })
420 }
421 FrontendClient::Standalone {
422 database_client, ..
423 } => {
424 let extensions = HashMap::from_iter(
425 hints
426 .iter()
427 .map(|(key, value)| ((*key).to_string(), (*value).to_string())),
428 );
429 let ctx = QueryContextBuilder::default()
430 .current_catalog(catalog.to_string())
431 .current_schema(schema.to_string())
432 .extensions(extensions)
433 .build();
434 let ctx = Arc::new(ctx);
435 {
436 let database_client = {
437 database_client
438 .handler
439 .lock()
440 .unwrap()
441 .as_ref()
442 .context(UnexpectedSnafu {
443 reason: "Standalone's frontend instance is not set",
444 })?
445 .upgrade()
446 .context(UnexpectedSnafu {
447 reason: "Failed to upgrade database client",
448 })?
449 };
450 let resp: common_query::Output = database_client
451 .do_query(Request::RowInserts(requests), ctx)
452 .await
453 .map_err(BoxedError::new)
454 .context(ExternalSnafu)?;
455 match resp.data {
456 OutputData::AffectedRows(rows) => Ok(rows.try_into().map_err(|_| {
457 UnexpectedSnafu {
458 reason: format!("Failed to convert rows to u32: {}", rows),
459 }
460 .build()
461 })?),
462 _ => UnexpectedSnafu {
463 reason: "Unexpected output data",
464 }
465 .fail(),
466 }
467 }
468 }
469 }
470 }
471
472 pub(crate) async fn query_with_terminal_metrics(
475 &self,
476 catalog: &str,
477 schema: &str,
478 request: QueryRequest,
479 extensions: &[(&str, &str)],
480 snapshot_seqs: &HashMap<u64, u64>,
481 peer_desc: &mut Option<PeerDesc>,
482 ) -> Result<OutputWithMetrics, Error> {
483 let flow_extensions = build_flow_extensions(extensions)?;
484 match self {
485 FrontendClient::Distributed {
486 query, batch_opts, ..
487 } => {
488 let query_parallelism = query.parallelism.to_string();
489 let hints = vec![
490 (QUERY_PARALLELISM_HINT, query_parallelism.as_str()),
491 (READ_PREFERENCE_HINT, batch_opts.read_preference.as_ref()),
492 ];
493 let db = self.get_random_active_frontend(catalog, schema).await?;
494 *peer_desc = Some(PeerDesc::Dist {
495 peer: db.peer.clone(),
496 });
497 db.database
498 .flight_request()
499 .with_hints(&hints)
500 .with_flow_extensions(extensions)
501 .with_snapshot_seqs(snapshot_seqs)
502 .with_timeout(batch_opts.experimental_flight_do_get_timeout)
503 .query_with_terminal_metrics(request)
504 .await
505 .map_err(BoxedError::new)
506 .context(ExternalSnafu)
507 }
508 FrontendClient::Standalone {
509 database_client,
510 query,
511 } => {
512 *peer_desc = Some(PeerDesc::Standalone);
513 let mut extensions_map = HashMap::from([(
514 QUERY_PARALLELISM_HINT.to_string(),
515 query.parallelism.to_string(),
516 )]);
517 for (key, value) in extensions {
518 extensions_map.insert((*key).to_string(), (*value).to_string());
519 }
520 let ctx = QueryContextBuilder::default()
521 .current_catalog(catalog.to_string())
522 .current_schema(schema.to_string())
523 .extensions(extensions_map)
524 .snapshot_seqs(Arc::new(RwLock::new(snapshot_seqs.clone())))
525 .build();
526 let ctx = Arc::new(ctx);
527 let database_client = {
528 database_client
529 .handler
530 .lock()
531 .map_err(|e| {
532 UnexpectedSnafu {
533 reason: format!("Failed to lock database client: {e}"),
534 }
535 .build()
536 })?
537 .as_ref()
538 .context(UnexpectedSnafu {
539 reason: "Standalone's frontend instance is not set",
540 })?
541 .upgrade()
542 .context(UnexpectedSnafu {
543 reason: "Failed to upgrade database client",
544 })?
545 };
546 database_client
547 .do_query(Request::Query(request), ctx.clone())
548 .await
549 .map(|output| {
550 wrap_standalone_output_with_terminal_metrics(output, &flow_extensions)
551 })
552 .map_err(BoxedError::new)
553 .context(ExternalSnafu)
554 }
555 }
556 }
557
558 pub(crate) async fn handle(
560 &self,
561 req: api::v1::greptime_request::Request,
562 catalog: &str,
563 schema: &str,
564 peer_desc: &mut Option<PeerDesc>,
565 ) -> Result<u32, Error> {
566 match self {
567 FrontendClient::Distributed {
568 query, batch_opts, ..
569 } => {
570 let db = self.get_random_active_frontend(catalog, schema).await?;
571
572 *peer_desc = Some(PeerDesc::Dist {
573 peer: db.peer.clone(),
574 });
575
576 db.database
577 .handle_with_retry(
578 req.clone(),
579 batch_opts.experimental_grpc_max_retries,
580 &[
581 (QUERY_PARALLELISM_HINT, &query.parallelism.to_string()),
582 (READ_PREFERENCE_HINT, batch_opts.read_preference.as_ref()),
583 ],
584 )
585 .await
586 .with_context(|_| InvalidRequestSnafu {
587 context: format!("Failed to handle request at {:?}: {:?}", db.peer, req),
588 })
589 }
590 FrontendClient::Standalone {
591 database_client,
592 query,
593 } => {
594 let ctx = QueryContextBuilder::default()
595 .current_catalog(catalog.to_string())
596 .current_schema(schema.to_string())
597 .extensions(HashMap::from([(
598 QUERY_PARALLELISM_HINT.to_string(),
599 query.parallelism.to_string(),
600 )]))
601 .build();
602 let ctx = Arc::new(ctx);
603 {
604 let database_client = {
605 database_client
606 .handler
607 .lock()
608 .unwrap()
609 .as_ref()
610 .context(UnexpectedSnafu {
611 reason: "Standalone's frontend instance is not set",
612 })?
613 .upgrade()
614 .context(UnexpectedSnafu {
615 reason: "Failed to upgrade database client",
616 })?
617 };
618 let resp: common_query::Output = database_client
619 .do_query(req, ctx)
620 .await
621 .map_err(BoxedError::new)
622 .context(ExternalSnafu)?;
623 match resp.data {
624 OutputData::AffectedRows(rows) => Ok(rows.try_into().map_err(|_| {
625 UnexpectedSnafu {
626 reason: format!("Failed to convert rows to u32: {}", rows),
627 }
628 .build()
629 })?),
630 _ => UnexpectedSnafu {
631 reason: "Unexpected output data",
632 }
633 .fail(),
634 }
635 }
636 }
637 }
638 }
639}
640
641fn build_flow_extensions(extensions: &[(&str, &str)]) -> Result<FlowQueryExtensions, Error> {
642 let flow_extensions = HashMap::from_iter(
643 extensions
644 .iter()
645 .map(|(key, value)| ((*key).to_string(), (*value).to_string())),
646 );
647 FlowQueryExtensions::parse_flow_extensions(&flow_extensions)
648 .map_err(BoxedError::new)
649 .context(ExternalSnafu)
650 .map(|extensions| extensions.unwrap_or_default())
651}
652
653fn wrap_standalone_output_with_terminal_metrics(
654 output: Output,
655 flow_extensions: &FlowQueryExtensions,
656) -> OutputWithMetrics {
657 let should_collect_region_watermark = flow_extensions.should_collect_region_watermark();
658 let terminal_metrics =
659 if should_collect_region_watermark && !matches!(&output.data, OutputData::Stream(_)) {
660 output
661 .meta
662 .plan
663 .clone()
664 .and_then(terminal_recordbatch_metrics_from_plan)
665 } else {
666 None
667 };
668 let result = OutputWithMetrics::from_output(output);
669 if let Some(metrics) = terminal_metrics {
670 result.metrics.update(Some(metrics));
671 }
672 result
673}
674
675#[derive(Debug, Default, Clone)]
677pub(crate) enum PeerDesc {
678 #[default]
680 Unknown,
681 Dist {
683 peer: Peer,
685 },
686 Standalone,
688}
689
690impl std::fmt::Display for PeerDesc {
691 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
692 match self {
693 PeerDesc::Unknown => write!(f, "unknown"),
694 PeerDesc::Dist { peer } => write!(f, "{}", peer.addr),
695 PeerDesc::Standalone => write!(f, "standalone"),
696 }
697 }
698}
699
700#[cfg(test)]
701mod tests {
702 use std::pin::Pin;
703 use std::task::{Context, Poll};
704 use std::time::Duration;
705
706 use api::v1::query_request::Query;
707 use arrow_flight::flight_service_server::FlightServiceServer;
708 use arrow_flight::{FlightData, Ticket};
709 use common_grpc::flight::FlightEncoder;
710 use common_query::{Output, OutputData};
711 use common_recordbatch::adapter::RecordBatchMetrics;
712 use common_recordbatch::{OrderOption, RecordBatch, RecordBatchStream};
713 use datatypes::arrow::datatypes::Schema as ArrowSchema;
714 use datatypes::prelude::{ConcreteDataType, VectorRef};
715 use datatypes::schema::{ColumnSchema, Schema};
716 use datatypes::vectors::Int32Vector;
717 use futures::StreamExt;
718 use servers::grpc::flight::{FlightCraft, FlightCraftWrapper, TonicStream};
719 use tokio::net::TcpListener;
720 use tokio::task::JoinHandle;
721 use tokio::time::timeout;
722 use tokio_stream::wrappers::TcpListenerStream;
723 use tonic::{Request as TonicRequest, Response as TonicResponse, Status};
724
725 use super::*;
726
727 #[derive(Debug)]
728 struct NoopHandler;
729
730 struct MockMetricsStream {
731 schema: datatypes::schema::SchemaRef,
732 batch: Option<RecordBatch>,
733 metrics: RecordBatchMetrics,
734 terminal_metrics_only: bool,
735 }
736
737 impl futures::Stream for MockMetricsStream {
738 type Item = common_recordbatch::error::Result<RecordBatch>;
739
740 fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
741 Poll::Ready(self.batch.take().map(Ok))
742 }
743
744 fn size_hint(&self) -> (usize, Option<usize>) {
745 (
746 usize::from(self.batch.is_some()),
747 Some(usize::from(self.batch.is_some())),
748 )
749 }
750 }
751
752 impl RecordBatchStream for MockMetricsStream {
753 fn name(&self) -> &str {
754 "MockMetricsStream"
755 }
756
757 fn schema(&self) -> datatypes::schema::SchemaRef {
758 self.schema.clone()
759 }
760
761 fn output_ordering(&self) -> Option<&[OrderOption]> {
762 None
763 }
764
765 fn metrics(&self) -> Option<RecordBatchMetrics> {
766 if self.terminal_metrics_only && self.batch.is_some() {
767 return None;
768 }
769 Some(self.metrics.clone())
770 }
771 }
772
773 #[derive(Debug)]
774 struct MetricsHandler;
775
776 #[derive(Debug)]
777 struct ExtensionAwareHandler;
778
779 #[derive(Debug)]
780 struct SnapshotBindingHandler;
781
782 #[derive(Debug)]
783 struct RejectUnauthenticatedFlight;
784
785 #[derive(Debug)]
786 struct SlowFlight;
787
788 struct DelayedEofFlight {
789 schema_sent: Mutex<Option<tokio::sync::oneshot::Sender<()>>>,
790 release: Arc<tokio::sync::Notify>,
791 }
792
793 #[derive(Debug)]
794 struct LateStreamErrorFlight;
795
796 struct WaitForConcurrentFlight {
797 barrier: Arc<tokio::sync::Barrier>,
798 }
799
800 #[async_trait::async_trait]
801 impl GrpcQueryHandlerWithBoxedError for NoopHandler {
802 async fn do_query(
803 &self,
804 _query: Request,
805 _ctx: QueryContextRef,
806 ) -> std::result::Result<Output, BoxedError> {
807 Ok(Output::new_with_affected_rows(0))
808 }
809 }
810
811 #[async_trait::async_trait]
812 impl GrpcQueryHandlerWithBoxedError for MetricsHandler {
813 async fn do_query(
814 &self,
815 _query: Request,
816 _ctx: QueryContextRef,
817 ) -> std::result::Result<Output, BoxedError> {
818 let schema = Arc::new(Schema::new(vec![ColumnSchema::new(
819 "v",
820 ConcreteDataType::int32_datatype(),
821 false,
822 )]));
823 let batch = RecordBatch::new(
824 schema.clone(),
825 vec![Arc::new(Int32Vector::from_slice([1, 2])) as VectorRef],
826 )
827 .unwrap();
828 Ok(Output::new_with_stream(Box::pin(MockMetricsStream {
829 schema,
830 batch: Some(batch),
831 metrics: RecordBatchMetrics {
832 region_watermarks: vec![common_recordbatch::adapter::RegionWatermarkEntry {
833 region_id: 42,
834 watermark: Some(99),
835 }],
836 ..Default::default()
837 },
838 terminal_metrics_only: true,
839 })))
840 }
841 }
842
843 #[async_trait::async_trait]
844 impl GrpcQueryHandlerWithBoxedError for ExtensionAwareHandler {
845 async fn do_query(
846 &self,
847 _query: Request,
848 ctx: QueryContextRef,
849 ) -> std::result::Result<Output, BoxedError> {
850 assert_eq!(ctx.extension("flow.return_region_seq"), Some("true"));
851 Ok(Output::new_with_affected_rows(1))
852 }
853 }
854
855 #[async_trait::async_trait]
856 impl GrpcQueryHandlerWithBoxedError for SnapshotBindingHandler {
857 async fn do_query(
858 &self,
859 _query: Request,
860 ctx: QueryContextRef,
861 ) -> std::result::Result<Output, BoxedError> {
862 assert_eq!(ctx.extension("flow.return_region_seq"), Some("true"));
863 assert_eq!(ctx.get_snapshot(1), Some(10));
864 assert_eq!(ctx.get_snapshot(2), Some(20));
865 ctx.set_snapshot(42, 99);
866 Ok(Output::new_with_affected_rows(1))
867 }
868 }
869
870 #[async_trait::async_trait]
871 impl FlightCraft for RejectUnauthenticatedFlight {
872 async fn do_get(
873 &self,
874 _request: TonicRequest<Ticket>,
875 ) -> std::result::Result<TonicResponse<TonicStream<FlightData>>, Status> {
876 Err(Status::unauthenticated("auth failed"))
877 }
878 }
879
880 #[async_trait::async_trait]
881 impl FlightCraft for SlowFlight {
882 async fn do_get(
883 &self,
884 _request: TonicRequest<Ticket>,
885 ) -> std::result::Result<TonicResponse<TonicStream<FlightData>>, Status> {
886 tokio::time::sleep(Duration::from_secs(60)).await;
887 Err(Status::unavailable("slow response"))
888 }
889 }
890
891 #[async_trait::async_trait]
892 impl FlightCraft for DelayedEofFlight {
893 async fn do_get(
894 &self,
895 _request: TonicRequest<Ticket>,
896 ) -> std::result::Result<TonicResponse<TonicStream<FlightData>>, Status> {
897 let schema = FlightEncoder::default().encode_schema(&ArrowSchema::empty());
898 let schema_sent = self.schema_sent.lock().unwrap().take();
899 let schema_stream = futures::stream::once(async move {
900 if let Some(schema_sent) = schema_sent {
901 let _ = schema_sent.send(());
902 }
903 Ok(schema)
904 });
905 let release = self.release.clone();
906 let delayed_eof = futures::stream::unfold(release, |release| async move {
907 release.notified().await;
908 None::<(std::result::Result<FlightData, Status>, _)>
909 });
910
911 Ok(TonicResponse::new(Box::pin(
912 schema_stream.chain(delayed_eof),
913 )))
914 }
915 }
916
917 #[async_trait::async_trait]
918 impl FlightCraft for LateStreamErrorFlight {
919 async fn do_get(
920 &self,
921 _request: TonicRequest<Ticket>,
922 ) -> std::result::Result<TonicResponse<TonicStream<FlightData>>, Status> {
923 let schema = FlightEncoder::default().encode_schema(&ArrowSchema::empty());
924 let stream =
925 futures::stream::iter([Ok(schema), Err(Status::unavailable("late stream error"))]);
926 Ok(TonicResponse::new(Box::pin(stream)))
927 }
928 }
929
930 #[async_trait::async_trait]
931 impl FlightCraft for WaitForConcurrentFlight {
932 async fn do_get(
933 &self,
934 _request: TonicRequest<Ticket>,
935 ) -> std::result::Result<TonicResponse<TonicStream<FlightData>>, Status> {
936 self.barrier.wait().await;
937 Err(Status::unavailable("probe started concurrently"))
938 }
939 }
940
941 async fn start_flight_server<T: FlightCraft>(handler: T) -> (String, JoinHandle<()>) {
942 let listener = TcpListener::bind("127.0.0.1:0")
943 .await
944 .expect("bind test flight server");
945 let addr = listener.local_addr().expect("local addr").to_string();
946 let server = tokio::spawn(async move {
947 tonic::transport::Server::builder()
948 .add_service(FlightServiceServer::new(FlightCraftWrapper(handler)))
949 .serve_with_incoming(TcpListenerStream::new(listener))
950 .await
951 .expect("serve test flight server");
952 });
953
954 (addr, server)
955 }
956
957 #[tokio::test]
958 async fn wait_initialized() {
959 let (client, handler_mut) =
960 FrontendClient::from_empty_grpc_handler(QueryOptions::default());
961
962 assert!(
963 timeout(Duration::from_millis(50), client.wait_initialized())
964 .await
965 .is_err()
966 );
967
968 let handler: Arc<dyn GrpcQueryHandlerWithBoxedError> = Arc::new(NoopHandler);
969 handler_mut.set_handler(Arc::downgrade(&handler)).await;
970
971 timeout(Duration::from_secs(1), client.wait_initialized())
972 .await
973 .expect("wait_initialized should complete after handler is set");
974
975 timeout(Duration::from_millis(10), client.wait_initialized())
976 .await
977 .expect("wait_initialized should be a no-op once initialized");
978
979 let handler: Arc<dyn GrpcQueryHandlerWithBoxedError> = Arc::new(NoopHandler);
980 let client =
981 FrontendClient::from_grpc_handler(Arc::downgrade(&handler), QueryOptions::default());
982 assert!(
983 timeout(Duration::from_millis(10), client.wait_initialized())
984 .await
985 .is_ok()
986 );
987
988 let meta_client = Arc::new(MetaClient::new(0, api::v1::meta::Role::Frontend));
989 let client = FrontendClient::from_meta_client(
990 meta_client,
991 QueryOptions::default(),
992 BatchingModeOptions::default(),
993 )
994 .unwrap();
995 assert!(
996 timeout(Duration::from_millis(10), client.wait_initialized())
997 .await
998 .is_ok()
999 );
1000 }
1001
1002 #[tokio::test]
1003 async fn test_query_with_terminal_metrics_tracks_watermark_in_standalone_mode() {
1004 let handler: Arc<dyn GrpcQueryHandlerWithBoxedError> = Arc::new(MetricsHandler);
1005 let client =
1006 FrontendClient::from_grpc_handler(Arc::downgrade(&handler), QueryOptions::default());
1007 let mut peer_desc = None;
1008
1009 let result = client
1010 .query_with_terminal_metrics(
1011 "greptime",
1012 "public",
1013 QueryRequest {
1014 query: Some(Query::Sql("select 1".to_string())),
1015 },
1016 &[],
1017 &HashMap::new(),
1018 &mut peer_desc,
1019 )
1020 .await
1021 .unwrap();
1022 assert!(matches!(peer_desc, Some(PeerDesc::Standalone)));
1023
1024 let terminal_metrics = result.metrics.clone();
1025 assert!(!result.metrics.is_ready());
1026 assert!(terminal_metrics.get().is_none());
1027
1028 let OutputData::Stream(mut stream) = result.output.data else {
1029 panic!("expected stream output");
1030 };
1031 while stream.next().await.is_some() {}
1032
1033 assert!(terminal_metrics.is_ready());
1034 assert_eq!(
1035 terminal_metrics.region_watermark_map(),
1036 Some(HashMap::from([(42_u64, 99_u64)]))
1037 );
1038 }
1039
1040 #[tokio::test]
1041 async fn test_query_with_terminal_metrics_forwards_flow_extensions_in_standalone_mode() {
1042 let handler: Arc<dyn GrpcQueryHandlerWithBoxedError> = Arc::new(ExtensionAwareHandler);
1043 let client =
1044 FrontendClient::from_grpc_handler(Arc::downgrade(&handler), QueryOptions::default());
1045 let mut peer_desc = None;
1046
1047 let result = client
1048 .query_with_terminal_metrics(
1049 "greptime",
1050 "public",
1051 QueryRequest {
1052 query: Some(Query::Sql("insert into t select 1".to_string())),
1053 },
1054 &[("flow.return_region_seq", "true")],
1055 &HashMap::new(),
1056 &mut peer_desc,
1057 )
1058 .await
1059 .unwrap();
1060 assert!(matches!(peer_desc, Some(PeerDesc::Standalone)));
1061
1062 assert!(result.metrics.is_ready());
1063 assert!(result.region_watermark_map().is_none());
1064 }
1065
1066 #[tokio::test]
1067 async fn test_query_with_terminal_metrics_uses_standalone_snapshot_bounds() {
1068 let handler: Arc<dyn GrpcQueryHandlerWithBoxedError> = Arc::new(SnapshotBindingHandler);
1069 let client =
1070 FrontendClient::from_grpc_handler(Arc::downgrade(&handler), QueryOptions::default());
1071 let mut peer_desc = None;
1072
1073 let result = client
1074 .query_with_terminal_metrics(
1075 "greptime",
1076 "public",
1077 QueryRequest {
1078 query: Some(Query::Sql("insert into t select * from src".to_string())),
1079 },
1080 &[("flow.return_region_seq", "true")],
1081 &HashMap::from([(1, 10), (2, 20)]),
1082 &mut peer_desc,
1083 )
1084 .await
1085 .unwrap();
1086 assert!(matches!(peer_desc, Some(PeerDesc::Standalone)));
1087
1088 assert!(result.metrics.is_ready());
1089 assert_eq!(result.region_watermark_map(), None);
1090 }
1091
1092 #[tokio::test]
1093 async fn test_query_with_terminal_metrics_rejects_invalid_flow_extensions() {
1094 let handler: Arc<dyn GrpcQueryHandlerWithBoxedError> = Arc::new(NoopHandler);
1095 let client =
1096 FrontendClient::from_grpc_handler(Arc::downgrade(&handler), QueryOptions::default());
1097 let mut peer_desc = None;
1098
1099 let err = client
1100 .query_with_terminal_metrics(
1101 "greptime",
1102 "public",
1103 QueryRequest {
1104 query: Some(Query::Sql("select 1".to_string())),
1105 },
1106 &[("flow.return_region_seq", "not-a-bool")],
1107 &HashMap::new(),
1108 &mut peer_desc,
1109 )
1110 .await
1111 .unwrap_err();
1112
1113 assert!(format!("{err:?}").contains("Invalid value for flow.return_region_seq"));
1114 }
1115
1116 #[tokio::test]
1117 async fn test_try_select_one_waits_for_stream_eof() {
1118 let (schema_sent, schema_sent_rx) = tokio::sync::oneshot::channel();
1119 let release = Arc::new(tokio::sync::Notify::new());
1120 let (addr, server) = start_flight_server(DelayedEofFlight {
1121 schema_sent: Mutex::new(Some(schema_sent)),
1122 release: release.clone(),
1123 })
1124 .await;
1125 let database = Database::new(
1126 DEFAULT_CATALOG_NAME,
1127 DEFAULT_SCHEMA_NAME,
1128 Client::with_urls([addr.as_str()]),
1129 );
1130 let db = DatabaseWithPeer::new(
1131 database,
1132 Peer {
1133 id: 1,
1134 addr: addr.clone(),
1135 },
1136 );
1137 let mut probe = tokio::spawn(async move { db.try_select_one().await });
1138
1139 timeout(Duration::from_secs(1), schema_sent_rx)
1140 .await
1141 .expect("server should send the schema")
1142 .expect("schema signal should be sent");
1143 assert!(
1144 timeout(Duration::from_millis(100), &mut probe)
1145 .await
1146 .is_err(),
1147 "SELECT 1 must wait for the delayed stream tail and EOF"
1148 );
1149
1150 release.notify_one();
1151 timeout(Duration::from_secs(1), &mut probe)
1152 .await
1153 .expect("SELECT 1 should complete after EOF")
1154 .expect("probe task should not panic")
1155 .expect("SELECT 1 should succeed after EOF");
1156 server.abort();
1157 }
1158
1159 #[tokio::test]
1160 async fn test_try_select_one_propagates_late_stream_error() {
1161 let (addr, server) = start_flight_server(LateStreamErrorFlight).await;
1162 let database = Database::new(
1163 DEFAULT_CATALOG_NAME,
1164 DEFAULT_SCHEMA_NAME,
1165 Client::with_urls([addr.as_str()]),
1166 );
1167 let db = DatabaseWithPeer::new(
1168 database,
1169 Peer {
1170 id: 1,
1171 addr: addr.clone(),
1172 },
1173 );
1174
1175 let err = db.try_select_one().await.unwrap_err();
1176 server.abort();
1177
1178 assert!(format!("{err:?}").contains("late stream error"));
1179 }
1180
1181 #[tokio::test]
1182 async fn test_check_all_frontends_without_auth_fails_fast_on_unauthenticated_frontend() {
1183 let (addr, server) = start_flight_server(RejectUnauthenticatedFlight).await;
1184 let client = FrontendClient::from_meta_client(
1185 Arc::new(MetaClient::new(0, api::v1::meta::Role::Frontend)),
1186 QueryOptions::default(),
1187 BatchingModeOptions::default(),
1188 )
1189 .unwrap();
1190
1191 let err = client
1192 .check_all_frontends_without_auth(&[Peer {
1193 id: 1,
1194 addr: addr.clone(),
1195 }])
1196 .await
1197 .unwrap_err();
1198 server.abort();
1199
1200 let Error::InvalidRequest {
1201 context, source, ..
1202 } = err
1203 else {
1204 panic!("expected InvalidRequest, got {err:?}");
1205 };
1206 assert!(context.contains(&addr));
1207 assert!(context.contains("rejected unauthenticated flownode probe"));
1208 assert_eq!(source.tonic_code(), Some(tonic::Code::Unauthenticated));
1209 }
1210
1211 #[tokio::test]
1212 async fn test_check_all_frontends_without_auth_uses_grpc_connection_timeout() {
1213 let (addr, server) = start_flight_server(SlowFlight).await;
1214 let client = FrontendClient::from_meta_client(
1215 Arc::new(MetaClient::new(0, api::v1::meta::Role::Frontend)),
1216 QueryOptions::default(),
1217 BatchingModeOptions {
1218 grpc_conn_timeout: Duration::from_millis(50),
1219 ..Default::default()
1220 },
1221 )
1222 .unwrap();
1223
1224 let failures = client
1225 .check_all_frontends_without_auth(&[Peer {
1226 id: 1,
1227 addr: addr.clone(),
1228 }])
1229 .await
1230 .unwrap();
1231 server.abort();
1232
1233 assert_eq!(failures.len(), 1);
1234 assert!(failures[0].contains(&addr));
1235 assert!(failures[0].contains("health check timed out"));
1236 }
1237
1238 #[tokio::test]
1239 async fn test_check_all_frontends_without_auth_checks_frontends_concurrently() {
1240 let barrier = Arc::new(tokio::sync::Barrier::new(2));
1241 let (addr1, server1) = start_flight_server(WaitForConcurrentFlight {
1242 barrier: barrier.clone(),
1243 })
1244 .await;
1245 let (addr2, server2) = start_flight_server(WaitForConcurrentFlight { barrier }).await;
1246 let client = FrontendClient::from_meta_client(
1247 Arc::new(MetaClient::new(0, api::v1::meta::Role::Frontend)),
1248 QueryOptions::default(),
1249 BatchingModeOptions {
1250 grpc_conn_timeout: Duration::from_millis(500),
1251 ..Default::default()
1252 },
1253 )
1254 .unwrap();
1255
1256 let failures = timeout(
1257 Duration::from_secs(2),
1258 client.check_all_frontends_without_auth(&[
1259 Peer {
1260 id: 1,
1261 addr: addr1.clone(),
1262 },
1263 Peer {
1264 id: 2,
1265 addr: addr2.clone(),
1266 },
1267 ]),
1268 )
1269 .await
1270 .expect("concurrent probes should complete before per-peer timeouts")
1271 .unwrap();
1272 server1.abort();
1273 server2.abort();
1274
1275 assert_eq!(failures.len(), 2);
1276 assert!(failures.iter().any(|failure| failure.contains(&addr1)));
1277 assert!(failures.iter().any(|failure| failure.contains(&addr2)));
1278 assert!(
1279 failures
1280 .iter()
1281 .all(|failure| !failure.contains("health check timed out")),
1282 "sequential probes would time out before both requests reach the barrier: {failures:?}"
1283 );
1284 }
1285}