Skip to main content

flow/batching_mode/
frontend_client.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Frontend client to run flow as batching task which is time-window-aware normal query triggered every tick set by user
16
17use 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/// Adapter trait for [`GrpcQueryHandler`] that boxes the underlying error into [`BoxedError`].
50///
51/// This is mainly used by flownode to invoke a frontend instance in standalone mode.
52#[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/// auto impl
62#[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        // Ignore the error, as we allow the handler to be set multiple times.
83        let _ = self.is_initialized.set(());
84    }
85}
86
87/// A simple frontend client able to execute sql using grpc protocol
88///
89/// This is for computation-heavy query which need to offload computation to frontend, lifting the load from flownode
90#[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        /// for the sake of simplicity still use grpc even in standalone mode
100        /// notice the client here should all be lazy, so that can wait after frontend is booted then make conn
101        database_client: HandlerMutable,
102        query: QueryOptions,
103    },
104}
105
106impl FrontendClient {
107    /// Create a new empty frontend client, with a `HandlerMutable` to set the grpc handler later
108    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    /// Waits until the frontend client is initialized.
124    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    /// Try sending a "SELECT 1" to the database
184    async fn try_select_one(&self) -> Result<(), Error> {
185        // notice here use `sql` for `SELECT 1` return 1 row
186        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    /// scan for available frontend from metadata
199    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    /// Probes all discovered frontends without auth.
213    ///
214    /// Returns non-auth failures to allow callers to retry transient connectivity
215    /// errors. Authentication failures are returned immediately because they mean
216    /// a frontend advertised an auth-protected endpoint to flownodes.
217    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    /// Get a frontend discovered by metasrv and verified with a query probe.
270    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            // shuffle the frontends to avoid always pick the same one
293            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            // no available frontend
311            // sleep and retry
312            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    /// Execute a SQL statement on the frontend.
344    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    /// Execute a flow query and return terminal metrics. `snapshot_seqs` are
391    /// optional read upper bounds used only by snapshot-fenced repair chunks.
392    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    /// Handle a request to frontend
477    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/// Describe a peer of frontend
596#[derive(Debug, Default, Clone)]
597pub(crate) enum PeerDesc {
598    /// The query failed before a frontend peer was selected.
599    #[default]
600    Unknown,
601    /// Distributed mode's frontend peer address
602    Dist {
603        /// frontend peer address
604        peer: Peer,
605    },
606    /// Standalone mode
607    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}