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