Skip to main content

frontend/
frontend.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
15use std::sync::Arc;
16
17use common_base::readable_size::ReadableSize;
18use common_config::config::Configurable;
19use common_event_recorder::EventRecorderOptions;
20use common_memory_manager::OnExhaustedPolicy;
21use common_options::datanode::DatanodeClientOptions;
22use common_options::memory::MemoryOptions;
23use common_telemetry::logging::{LoggingOptions, SlowQueryOptions, TracingOptions};
24use meta_client::MetaClientOptions;
25use pipeline::PipelineOptions;
26use query::options::QueryOptions;
27use serde::{Deserialize, Serialize};
28use servers::grpc::GrpcOptions;
29use servers::http::HttpOptions;
30use servers::server::ServerHandlers;
31use snafu::ResultExt;
32
33use crate::error;
34use crate::error::Result;
35use crate::heartbeat::HeartbeatTask;
36use crate::instance::Instance;
37use crate::service_config::{
38    InfluxdbOptions, JaegerOptions, MysqlOptions, OpentsdbOptions, OtlpOptions,
39    PendingRowsBatcherOptions, PostgresOptions, PromStoreOptions, WriteRetryOptions,
40};
41
42/// Configures frontend protocols, query execution, and distributed write retries.
43#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
44#[serde(default)]
45pub struct FrontendOptions {
46    pub node_id: Option<String>,
47    pub default_timezone: Option<String>,
48    pub default_column_prefix: Option<String>,
49    /// Server-side global switch for auto table creation on write.
50    /// Acts as an upper bound: when `false`, missing tables are never auto-created
51    /// even if a request sets the `auto_create_table` hint to `true`. When `true`
52    /// (default), the per-request hint still applies. Default: `true`.
53    pub auto_create_table: bool,
54    /// Maximum total memory for all concurrent write request bodies and messages (HTTP, gRPC, Flight).
55    /// Set to 0 to disable the limit. Default: "0" (unlimited)
56    pub max_in_flight_write_bytes: ReadableSize,
57    /// Policy when write bytes quota is exhausted.
58    /// Options: "wait" (default, 10s), "wait(<duration>)", "fail"
59    pub write_bytes_exhausted_policy: OnExhaustedPolicy,
60    pub http: HttpOptions,
61    pub grpc: GrpcOptions,
62    /// The internal gRPC options for the frontend service.
63    /// It serves the same services as the public one plus the internal handler.
64    /// CORS is always off on it.
65    pub internal_grpc: Option<GrpcOptions>,
66    pub mysql: MysqlOptions,
67    pub postgres: PostgresOptions,
68    pub opentsdb: OpentsdbOptions,
69    pub influxdb: InfluxdbOptions,
70    /// Ordinary-table batching with independent logical-table controls.
71    pub pending_rows_batcher: PendingRowsBatcherOptions,
72    pub prom_store: PromStoreOptions,
73    pub jaeger: JaegerOptions,
74    pub otlp: OtlpOptions,
75    pub meta_client: Option<MetaClientOptions>,
76    pub logging: LoggingOptions,
77    pub datanode: DatanodeClientOptions,
78    /// Retry policy for retryable distributed write failures.
79    pub write_retry: WriteRetryOptions,
80    pub user_provider: Option<String>,
81    pub tracing: TracingOptions,
82    pub query: QueryOptions,
83    pub slow_query: SlowQueryOptions,
84    pub memory: MemoryOptions,
85    /// The pipeline options.
86    pub pipeline: PipelineOptions,
87    /// The event recorder options.
88    pub event_recorder: EventRecorderOptions,
89    /// Environment variable keys to read and report in heartbeat messages.
90    pub heartbeat_env_vars: Vec<String>,
91}
92
93impl Default for FrontendOptions {
94    fn default() -> Self {
95        Self {
96            node_id: None,
97            default_timezone: None,
98            default_column_prefix: None,
99            auto_create_table: true,
100            max_in_flight_write_bytes: ReadableSize(0),
101            write_bytes_exhausted_policy: OnExhaustedPolicy::default(),
102            http: HttpOptions::default(),
103            grpc: GrpcOptions::default(),
104            internal_grpc: None,
105            mysql: MysqlOptions::default(),
106            postgres: PostgresOptions::default(),
107            opentsdb: OpentsdbOptions::default(),
108            influxdb: InfluxdbOptions::default(),
109            pending_rows_batcher: PendingRowsBatcherOptions::default(),
110            jaeger: JaegerOptions::default(),
111            prom_store: PromStoreOptions::default(),
112            otlp: OtlpOptions::default(),
113            meta_client: None,
114            logging: LoggingOptions::default(),
115            datanode: DatanodeClientOptions::default(),
116            write_retry: WriteRetryOptions::default(),
117            user_provider: None,
118            tracing: TracingOptions::default(),
119            query: QueryOptions::default(),
120            slow_query: SlowQueryOptions::default(),
121            memory: MemoryOptions::default(),
122            pipeline: PipelineOptions::default(),
123            event_recorder: EventRecorderOptions::default(),
124            heartbeat_env_vars: vec![],
125        }
126    }
127}
128
129impl Configurable for FrontendOptions {
130    fn env_list_keys() -> Option<&'static [&'static str]> {
131        Some(&[
132            "heartbeat_env_vars",
133            "meta_client.metasrv_addrs",
134            "event_recorder.event_types",
135            "pending_rows_batcher.protocols",
136            "pending_rows_batcher.logical_table.protocols",
137        ])
138    }
139}
140
141/// The [`Frontend`] struct is the main entry point for the frontend service
142/// which contains server handlers, frontend instance and some background tasks.
143pub struct Frontend {
144    pub instance: Arc<Instance>,
145    pub servers: ServerHandlers,
146    pub heartbeat_task: Option<HeartbeatTask>,
147}
148
149impl Frontend {
150    pub async fn start(&mut self) -> Result<()> {
151        if let Some(t) = &self.heartbeat_task
152            && let Err(error) = t.start().await
153        {
154            t.shutdown().await;
155            return Err(error);
156        }
157
158        if let Err(source) = self.servers.start_all().await {
159            if let Some(t) = &self.heartbeat_task {
160                t.shutdown().await;
161            }
162            return Err(source).context(error::StartServerSnafu);
163        }
164        Ok(())
165    }
166
167    pub async fn shutdown(&mut self) -> Result<()> {
168        if let Some(t) = &self.heartbeat_task {
169            t.shutdown().await;
170        }
171        self.servers
172            .shutdown_all()
173            .await
174            .context(error::ShutdownServerSnafu)
175    }
176
177    pub fn server_handlers(&self) -> &ServerHandlers {
178        &self.servers
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use std::any::Any;
185    use std::net::SocketAddr;
186    use std::pin::Pin;
187    use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
188    use std::time::Duration;
189
190    use api::v1::meta::heartbeat_server::HeartbeatServer;
191    use api::v1::meta::mailbox_message::Payload;
192    use api::v1::meta::{
193        AskLeaderRequest, AskLeaderResponse, HeartbeatRequest, HeartbeatResponse, MailboxMessage,
194        Peer, ResponseHeader, Role, heartbeat_server,
195    };
196    use async_trait::async_trait;
197    use client::{Client, Database};
198    use common_catalog::consts::{DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME};
199    use common_error::ext::ErrorExt;
200    use common_error::from_header_to_err_code_msg;
201    use common_error::status_code::StatusCode;
202    use common_grpc::channel_manager::ChannelManager;
203    use common_meta::heartbeat::handler::HandlerGroupExecutor;
204    use common_meta::heartbeat::handler::parse_mailbox_message::ParseMailboxMessageHandler;
205    use common_meta::heartbeat::handler::suspend::SuspendHandler;
206    use common_meta::instruction::Instruction;
207    use common_stat::ResourceStatImpl;
208    use futures::Stream;
209    use meta_client::MetaClientRef;
210    use meta_client::client::MetaClientBuilder;
211    use servers::batcher::BatchingProtocol;
212    use servers::grpc::{FlightCompression, GRPC_SERVER};
213    use servers::http::HTTP_SERVER;
214    use servers::http::result::greptime_result_v1::GreptimedbV1Response;
215    use servers::server::Server;
216    use tokio::sync::mpsc;
217    use tonic::codec::CompressionEncoding;
218    use tonic::codegen::tokio_stream::StreamExt;
219    use tonic::codegen::tokio_stream::wrappers::ReceiverStream;
220    use tonic::{Request, Response, Status, Streaming};
221
222    use crate::frontend::*;
223    use crate::heartbeat::{
224        FrontendHeartbeatExtension, FrontendHeartbeatExtensionResult, FrontendHeartbeatExtensions,
225    };
226    use crate::instance::builder::FrontendBuilder;
227    use crate::server::Services;
228
229    type GrpcStream<T> =
230        Pin<Box<dyn Stream<Item = std::result::Result<T, Status>> + Send + Sync + 'static>>;
231
232    #[test]
233    fn test_pipeline_host_access_config() {
234        let defaults: FrontendOptions = toml::from_str("").unwrap();
235        assert!(!defaults.pipeline.allow_vrl_host_access);
236        let options: FrontendOptions =
237            toml::from_str("[pipeline]\nallow_vrl_host_access = true").unwrap();
238        assert!(options.pipeline.allow_vrl_host_access);
239        let serialized = toml::to_string(&options).unwrap();
240        let restored: FrontendOptions = toml::from_str(&serialized).unwrap();
241        assert_eq!(options.pipeline, restored.pipeline);
242    }
243
244    #[test]
245    fn test_write_retry_config() {
246        let defaults: FrontendOptions = toml::from_str("").unwrap();
247        assert_eq!(defaults.write_retry, WriteRetryOptions::default());
248        let options: FrontendOptions = toml::from_str(
249            r#"
250[write_retry]
251max_retries = 5
252initial_backoff = "250ms"
253max_backoff = "5s"
254backoff_multiplier = 3.0
255"#,
256        )
257        .unwrap();
258        assert_eq!(
259            options.write_retry,
260            WriteRetryOptions {
261                max_retries: 5,
262                initial_backoff: Duration::from_millis(250),
263                max_backoff: Duration::from_secs(5),
264                backoff_multiplier: 3.0,
265            }
266        );
267        options.write_retry.validate().unwrap();
268        let serialized = toml::to_string(&options).unwrap();
269        let parsed: FrontendOptions = toml::from_str(&serialized).unwrap();
270        assert_eq!(options.write_retry, parsed.write_retry);
271        let disabled: FrontendOptions = toml::from_str("[write_retry]\nmax_retries = 0").unwrap();
272        assert_eq!(disabled.write_retry.max_retries, 0);
273        assert_eq!(
274            disabled.write_retry.initial_backoff,
275            defaults.write_retry.initial_backoff
276        );
277    }
278
279    #[test]
280    fn test_write_retry_config_from_env() {
281        temp_env::with_vars(
282            [
283                ("FRONTEND_RETRY_TEST__WRITE_RETRY__MAX_RETRIES", Some("4")),
284                (
285                    "FRONTEND_RETRY_TEST__WRITE_RETRY__INITIAL_BACKOFF",
286                    Some("500ms"),
287                ),
288                ("FRONTEND_RETRY_TEST__WRITE_RETRY__MAX_BACKOFF", Some("8s")),
289                (
290                    "FRONTEND_RETRY_TEST__WRITE_RETRY__BACKOFF_MULTIPLIER",
291                    Some("1.5"),
292                ),
293            ],
294            || {
295                let options =
296                    FrontendOptions::load_layered_options(None, "FRONTEND_RETRY_TEST").unwrap();
297                assert_eq!(
298                    options.write_retry,
299                    WriteRetryOptions {
300                        max_retries: 4,
301                        initial_backoff: Duration::from_millis(500),
302                        max_backoff: Duration::from_secs(8),
303                        backoff_multiplier: 1.5,
304                    }
305                );
306            },
307        );
308    }
309
310    #[test]
311    fn test_logical_batcher_protocols_from_env() {
312        temp_env::with_vars(
313            [
314                (
315                    "FRONTEND_LOGICAL_TEST__PENDING_ROWS_BATCHER__PROTOCOLS",
316                    Some("otlp,influxdb"),
317                ),
318                (
319                    "FRONTEND_LOGICAL_TEST__PENDING_ROWS_BATCHER__LOGICAL_TABLE__PROTOCOLS",
320                    Some("prom,otlp"),
321                ),
322            ],
323            || {
324                let options =
325                    FrontendOptions::load_layered_options(None, "FRONTEND_LOGICAL_TEST").unwrap();
326                assert_eq!(options.pending_rows_batcher.table.protocols.len(), 2);
327                assert_eq!(
328                    options
329                        .pending_rows_batcher
330                        .logical_table
331                        .unwrap()
332                        .protocols,
333                    vec![BatchingProtocol::Prom, BatchingProtocol::Otlp]
334                );
335            },
336        );
337    }
338
339    #[test]
340    fn test_batcher_protocols_from_env() {
341        temp_env::with_vars(
342            [(
343                "FRONTEND_BATCHER_TEST__PENDING_ROWS_BATCHER__PROTOCOLS",
344                Some("influxdb,http_sql"),
345            )],
346            || {
347                let options =
348                    FrontendOptions::load_layered_options(None, "FRONTEND_BATCHER_TEST").unwrap();
349                assert_eq!(
350                    options.pending_rows_batcher.table.protocols,
351                    vec![BatchingProtocol::Influxdb, BatchingProtocol::HttpSql]
352                );
353            },
354        );
355    }
356
357    #[test]
358    fn test_protocol_pending_rows_batcher_config() {
359        let defaults: FrontendOptions = toml::from_str("").unwrap();
360        assert!(
361            !defaults
362                .pending_rows_batcher
363                .table
364                .pending_rows_batching_enabled()
365        );
366        let options: FrontendOptions = toml::from_str(
367            r#"
368[pending_rows_batcher]
369protocols = ["influxdb", "http_sql"]
370pending_rows_flush_interval = "5ms"
371max_batch_rows = 25
372"#,
373        )
374        .unwrap();
375        assert_eq!(options.pending_rows_batcher.table.max_batch_rows, 25);
376        assert_eq!(options.pending_rows_batcher.table.protocols.len(), 2);
377        assert!(
378            options
379                .pending_rows_batcher
380                .table
381                .pending_rows_batching_enabled()
382        );
383        let serialized = toml::to_string(&options).unwrap();
384        let parsed: FrontendOptions = toml::from_str(&serialized).unwrap();
385        assert_eq!(options.influxdb, parsed.influxdb);
386        assert_eq!(options.opentsdb, parsed.opentsdb);
387        assert_eq!(options.pending_rows_batcher, parsed.pending_rows_batcher);
388    }
389
390    #[test]
391    fn test_toml() {
392        let opts = FrontendOptions::default();
393        let toml_string = toml::to_string(&opts).unwrap();
394        let parsed: FrontendOptions = toml::from_str(&toml_string).unwrap();
395        assert_eq!(parsed.otlp, opts.otlp);
396        assert_eq!(parsed.influxdb, opts.influxdb);
397        assert_eq!(parsed.opentsdb, opts.opentsdb);
398    }
399
400    #[test]
401    fn test_http_api_server_defaults_on_when_absent() {
402        // When `[http]` is not present in the config, the dedicated API server is
403        // disabled by default (its port/host defaults are still 4006 / 127.0.0.1).
404        let parsed: FrontendOptions = toml::from_str("").unwrap();
405        assert!(!parsed.http.enable_api_server);
406        assert_eq!(parsed.http.api_server_addr, "127.0.0.1:4006");
407    }
408
409    struct SuspendableHeartbeatServer {
410        suspend: Arc<AtomicBool>,
411        fail_heartbeat: bool,
412    }
413
414    struct FailingServer;
415
416    struct ShutdownTrackingExtension {
417        shutdown_calls: AtomicUsize,
418    }
419
420    #[async_trait]
421    impl FrontendHeartbeatExtension for ShutdownTrackingExtension {
422        fn name(&self) -> &str {
423            "shutdown-tracking"
424        }
425
426        async fn shutdown(&self) -> FrontendHeartbeatExtensionResult<()> {
427            self.shutdown_calls.fetch_add(1, Ordering::AcqRel);
428            Ok(())
429        }
430    }
431
432    #[async_trait]
433    impl Server for FailingServer {
434        async fn shutdown(&self) -> servers::error::Result<()> {
435            Ok(())
436        }
437
438        async fn start(&mut self, _listening: SocketAddr) -> servers::error::Result<()> {
439            Err(servers::error::Error::Internal {
440                err_msg: "mock server start failure".to_string(),
441            })
442        }
443
444        fn name(&self) -> &str {
445            "FAILING_SERVER"
446        }
447
448        fn as_any(&self) -> &dyn Any {
449            self
450        }
451    }
452
453    #[async_trait]
454    impl heartbeat_server::Heartbeat for SuspendableHeartbeatServer {
455        type HeartbeatStream = GrpcStream<HeartbeatResponse>;
456
457        async fn heartbeat(
458            &self,
459            request: Request<Streaming<HeartbeatRequest>>,
460        ) -> std::result::Result<Response<Self::HeartbeatStream>, Status> {
461            if self.fail_heartbeat {
462                return Err(Status::unavailable("mock initial heartbeat failure"));
463            }
464
465            let (tx, rx) = mpsc::channel(4);
466
467            common_runtime::spawn_global({
468                let mut requests = request.into_inner();
469                let suspend = self.suspend.clone();
470                async move {
471                    // Make the heartbeat interval short in unit tests to reduce the waiting time.
472                    // Only the handshake response needs to populate it (as metasrv does).
473                    let heartbeat_interval_ms = Duration::from_millis(200).as_millis() as u64;
474                    let mut is_handshake = true;
475                    while let Some(request) = requests.next().await {
476                        if let Err(e) = request {
477                            let _ = tx.send(Err(e)).await;
478                            return;
479                        }
480
481                        let mailbox_message =
482                            suspend.load(Ordering::Relaxed).then(|| MailboxMessage {
483                                payload: Some(Payload::Json(
484                                    serde_json::to_string(&Instruction::Suspend).unwrap(),
485                                )),
486                                ..Default::default()
487                            });
488                        let heartbeat_config =
489                            is_handshake.then_some(api::v1::meta::HeartbeatConfig {
490                                heartbeat_interval_ms,
491                                retry_interval_ms: heartbeat_interval_ms,
492                                gc_enabled: false,
493                            });
494                        is_handshake = false;
495                        let response = HeartbeatResponse {
496                            header: Some(ResponseHeader::success()),
497                            mailbox_message,
498                            heartbeat_config,
499                            ..Default::default()
500                        };
501
502                        let _ = tx.send(Ok(response)).await;
503                    }
504                }
505            });
506
507            Ok(Response::new(Box::pin(ReceiverStream::new(rx))))
508        }
509
510        async fn ask_leader(
511            &self,
512            _: Request<AskLeaderRequest>,
513        ) -> std::result::Result<Response<AskLeaderResponse>, Status> {
514            Ok(Response::new(AskLeaderResponse {
515                header: Some(ResponseHeader::success()),
516                leader: Some(Peer {
517                    addr: "localhost:0".to_string(),
518                    ..Default::default()
519                }),
520            }))
521        }
522    }
523
524    async fn create_meta_client(
525        options: &MetaClientOptions,
526        heartbeat_server: Arc<SuspendableHeartbeatServer>,
527    ) -> MetaClientRef {
528        let (client, server) = tokio::io::duplex(1024);
529
530        // create the heartbeat server:
531        common_runtime::spawn_global(async move {
532            let mut router = tonic::transport::Server::builder();
533            let router = router.add_service(
534                HeartbeatServer::from_arc(heartbeat_server)
535                    .accept_compressed(CompressionEncoding::Zstd)
536                    .send_compressed(CompressionEncoding::Zstd),
537            );
538            router
539                .serve_with_incoming(futures::stream::iter([Ok::<_, std::io::Error>(server)]))
540                .await
541        });
542
543        // Move client to an option so we can _move_ the inner value
544        // on the first attempt to connect. All other attempts will fail.
545        let mut client = Some(client);
546        let connector = tower::service_fn(move |_| {
547            let client = client.take();
548            async move {
549                if let Some(client) = client {
550                    Ok(hyper_util::rt::TokioIo::new(client))
551                } else {
552                    Err(std::io::Error::other("client already taken"))
553                }
554            }
555        });
556        let manager = ChannelManager::new();
557        manager
558            .reset_with_connector("localhost:0", connector)
559            .unwrap();
560
561        // create the heartbeat client:
562        let mut client = MetaClientBuilder::new(0, Role::Frontend)
563            .enable_heartbeat()
564            .heartbeat_channel_manager(manager)
565            .build();
566        client.start(&options.metasrv_addrs).await.unwrap();
567        Arc::new(client)
568    }
569
570    async fn create_frontend(
571        options: &FrontendOptions,
572        meta_client: MetaClientRef,
573    ) -> Result<Frontend> {
574        let instance = Arc::new(
575            FrontendBuilder::new_test(options, meta_client.clone())
576                .try_build()
577                .await?,
578        );
579
580        let servers =
581            Services::new(options.clone(), instance.clone(), Default::default()).build()?;
582
583        let executor = Arc::new(HandlerGroupExecutor::new(vec![
584            Arc::new(ParseMailboxMessageHandler),
585            Arc::new(SuspendHandler::new(instance.suspend_state())),
586        ]));
587        let heartbeat_task = Some(HeartbeatTask::new(
588            instance.frontend_peer_addr().to_string(),
589            options,
590            meta_client,
591            executor,
592            Arc::new(ResourceStatImpl::default()),
593        ));
594
595        let mut frontend = Frontend {
596            instance,
597            servers,
598            heartbeat_task,
599        };
600        frontend.start().await?;
601        Ok(frontend)
602    }
603
604    #[tokio::test]
605    async fn test_server_start_failure_shuts_down_heartbeat() {
606        let meta_client_options = MetaClientOptions {
607            metasrv_addrs: vec!["localhost:0".to_string()],
608            ..Default::default()
609        };
610        let options = FrontendOptions {
611            meta_client: Some(meta_client_options.clone()),
612            ..Default::default()
613        };
614        let heartbeat_server = Arc::new(SuspendableHeartbeatServer {
615            suspend: Arc::new(AtomicBool::new(false)),
616            fail_heartbeat: false,
617        });
618        let meta_client = create_meta_client(&meta_client_options, heartbeat_server).await;
619        let instance = Arc::new(
620            FrontendBuilder::new_test(&options, meta_client.clone())
621                .try_build()
622                .await
623                .unwrap(),
624        );
625        let heartbeat_task = HeartbeatTask::new(
626            instance.frontend_peer_addr().to_string(),
627            &options,
628            meta_client,
629            Arc::new(HandlerGroupExecutor::new(vec![])),
630            Arc::new(ResourceStatImpl::default()),
631        );
632        let heartbeat_probe = heartbeat_task.clone();
633        let servers = ServerHandlers::default();
634        servers.insert((Box::new(FailingServer), "127.0.0.1:0".parse().unwrap()));
635        let mut frontend = Frontend {
636            instance,
637            servers,
638            heartbeat_task: Some(heartbeat_task),
639        };
640
641        assert!(frontend.start().await.is_err());
642        assert!(heartbeat_probe.is_shutdown());
643    }
644
645    #[tokio::test]
646    async fn test_heartbeat_start_failure_shuts_down_extensions() {
647        let meta_client_options = MetaClientOptions {
648            metasrv_addrs: vec!["localhost:0".to_string()],
649            ..Default::default()
650        };
651        let options = FrontendOptions {
652            meta_client: Some(meta_client_options.clone()),
653            ..Default::default()
654        };
655        let heartbeat_server = Arc::new(SuspendableHeartbeatServer {
656            suspend: Arc::new(AtomicBool::new(false)),
657            fail_heartbeat: true,
658        });
659        let meta_client = create_meta_client(&meta_client_options, heartbeat_server).await;
660        let instance = Arc::new(
661            FrontendBuilder::new_test(&options, meta_client.clone())
662                .try_build()
663                .await
664                .unwrap(),
665        );
666        let extension = Arc::new(ShutdownTrackingExtension {
667            shutdown_calls: AtomicUsize::new(0),
668        });
669        let extensions = FrontendHeartbeatExtensions::default();
670        assert!(extensions.register(extension.clone()));
671        let heartbeat_task = HeartbeatTask::new(
672            instance.frontend_peer_addr().to_string(),
673            &options,
674            meta_client,
675            Arc::new(HandlerGroupExecutor::new(vec![])),
676            Arc::new(ResourceStatImpl::default()),
677        )
678        .with_extensions(extensions);
679        let heartbeat_probe = heartbeat_task.clone();
680        let mut frontend = Frontend {
681            instance,
682            servers: ServerHandlers::default(),
683            heartbeat_task: Some(heartbeat_task),
684        };
685
686        assert!(frontend.start().await.is_err());
687        assert!(heartbeat_probe.is_shutdown());
688        assert_eq!(extension.shutdown_calls.load(Ordering::Acquire), 1);
689    }
690
691    async fn verify_suspend_state_by_http(
692        frontend: &Frontend,
693        expected: std::result::Result<&str, (StatusCode, &str)>,
694    ) {
695        let addr = frontend.server_handlers().addr(HTTP_SERVER).unwrap();
696        let response = reqwest::get(format!("http://{}/v1/sql?sql=SELECT 1", addr))
697            .await
698            .unwrap();
699
700        let headers = response.headers();
701        let response = if let Some((code, error)) = from_header_to_err_code_msg(headers) {
702            Err((code, error))
703        } else {
704            Ok(response.text().await.unwrap())
705        };
706
707        match (response, expected) {
708            (Ok(response), Ok(expected)) => {
709                let response: GreptimedbV1Response = serde_json::from_str(&response).unwrap();
710                let response = serde_json::to_string(response.output()).unwrap();
711                assert_eq!(&response, expected);
712            }
713            (Err(actual), Err(expected)) => assert_eq!(actual, expected),
714            _ => unreachable!(),
715        }
716    }
717
718    async fn verify_suspend_state_by_grpc(
719        frontend: &Frontend,
720        expected: std::result::Result<&str, (StatusCode, &str)>,
721    ) {
722        let addr = frontend.server_handlers().addr(GRPC_SERVER).unwrap();
723        let client = Client::with_query_and_control_managers(
724            ChannelManager::new(),
725            ChannelManager::new(),
726            [addr.to_string()],
727        );
728        let client = Database::new(DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME, client);
729        let response = client.sql("SELECT 1").await;
730
731        match (response, expected) {
732            (Ok(response), Ok(expected)) => {
733                let response = response.data.pretty_print().await;
734                assert_eq!(&response, expected.trim());
735            }
736            (Err(actual), Err(expected)) => {
737                assert_eq!(actual.status_code(), expected.0);
738                assert_eq!(actual.output_msg(), expected.1);
739            }
740            _ => unreachable!(),
741        }
742    }
743
744    async fn wait_for_suspend_state(frontend: &Frontend, expected: bool) {
745        let check = || frontend.instance.is_suspended() == expected;
746        if check() {
747            return;
748        }
749
750        tokio::time::timeout(Duration::from_secs(5), async move {
751            while !check() {
752                tokio::time::sleep(Duration::from_millis(20)).await;
753            }
754        })
755        .await
756        .unwrap();
757    }
758
759    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
760    async fn test_suspend_frontend() -> Result<()> {
761        common_telemetry::init_default_ut_logging();
762
763        let meta_client_options = MetaClientOptions {
764            metasrv_addrs: vec!["localhost:0".to_string()],
765            ..Default::default()
766        };
767        let options = FrontendOptions {
768            http: HttpOptions {
769                addr: "127.0.0.1:0".to_string(),
770                ..Default::default()
771            },
772            grpc: GrpcOptions {
773                bind_addr: "127.0.0.1:0".to_string(),
774                flight_compression: FlightCompression::None,
775                ..Default::default()
776            },
777            mysql: MysqlOptions {
778                enable: false,
779                ..Default::default()
780            },
781            postgres: PostgresOptions {
782                enable: false,
783                ..Default::default()
784            },
785            meta_client: Some(meta_client_options.clone()),
786            ..Default::default()
787        };
788
789        let server = Arc::new(SuspendableHeartbeatServer {
790            suspend: Arc::new(AtomicBool::new(false)),
791            fail_heartbeat: false,
792        });
793        let meta_client = create_meta_client(&meta_client_options, server.clone()).await;
794        let frontend = create_frontend(&options, meta_client).await?;
795
796        // initial state: not suspend:
797        assert!(!frontend.instance.is_suspended());
798        verify_suspend_state_by_http(&frontend, Ok(r#"[{"records":{"schema":{"column_schemas":[{"name":"Int64(1)","data_type":"Int64"}]},"rows":[[1]],"total_rows":1}}]"#)).await;
799        verify_suspend_state_by_grpc(
800            &frontend,
801            Ok(r#"
802+----------+
803| Int64(1) |
804+----------+
805| 1        |
806+----------+"#),
807        )
808        .await;
809
810        // make heartbeat server returned "suspend" instruction,
811        server.suspend.store(true, Ordering::Relaxed);
812        wait_for_suspend_state(&frontend, true).await;
813        // ... then the frontend is suspended:
814        assert!(frontend.instance.is_suspended());
815        verify_suspend_state_by_http(
816            &frontend,
817            Err((
818                StatusCode::Suspended,
819                "error: Service suspended, execution_time_ms: 0",
820            )),
821        )
822        .await;
823        verify_suspend_state_by_grpc(&frontend, Err((StatusCode::Suspended, "Service suspended")))
824            .await;
825
826        // make heartbeat server NOT returned "suspend" instruction,
827        server.suspend.store(false, Ordering::Relaxed);
828        wait_for_suspend_state(&frontend, false).await;
829        // ... then frontend's suspend state is cleared:
830        assert!(!frontend.instance.is_suspended());
831        verify_suspend_state_by_http(&frontend, Ok(r#"[{"records":{"schema":{"column_schemas":[{"name":"Int64(1)","data_type":"Int64"}]},"rows":[[1]],"total_rows":1}}]"#)).await;
832        verify_suspend_state_by_grpc(
833            &frontend,
834            Ok(r#"
835+----------+
836| Int64(1) |
837+----------+
838| 1        |
839+----------+"#),
840        )
841        .await;
842        Ok(())
843    }
844}