Skip to main content

sqlness_runner/env/
bare.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::collections::HashMap;
16use std::fmt::Display;
17use std::fs::OpenOptions;
18use std::io;
19use std::io::Write;
20use std::path::{Path, PathBuf};
21use std::process::{Child, Command};
22use std::sync::atomic::{AtomicU32, Ordering};
23use std::sync::{Arc, Mutex};
24use std::time::Duration;
25
26use async_trait::async_trait;
27use common_error::ext::ErrorExt;
28use sqlness::{Database, EnvController, QueryContext};
29use tokio::sync::Mutex as TokioMutex;
30
31use crate::client::MultiProtocolClient;
32use crate::cmd::bare::ServerAddr;
33use crate::cmd::compat_case::try_infer_version;
34use crate::cmd::datanode_overlay::PreparedDatanodeOverlay;
35use crate::formatter::{ErrorFormatter, MysqlFormatter, OutputFormatter, PostgresqlFormatter};
36use crate::protocol_interceptor::{MYSQL, PROTOCOL_KEY};
37use crate::server_mode::{GrpcArgStyle, ServerMode};
38use crate::util;
39use crate::util::{PROGRAM, get_workspace_root, maybe_pull_binary};
40
41// standalone mode
42const SERVER_MODE_STANDALONE_IDX: usize = 0;
43// distributed mode
44const SERVER_MODE_METASRV_IDX: usize = 0;
45const SERVER_MODE_DATANODE_START_IDX: usize = 1;
46const SERVER_MODE_FRONTEND_IDX: usize = 4;
47const SERVER_MODE_FLOWNODE_IDX: usize = 5;
48// Number of datanodes in distributed mode
49const DISTRIBUTED_DATANODE_COUNT: usize = 3;
50
51#[derive(Clone)]
52pub enum WalConfig {
53    RaftEngine,
54    Kafka {
55        /// Indicates whether the runner needs to start a kafka cluster
56        /// (it might be available in the external system environment).
57        needs_kafka_cluster: bool,
58        broker_endpoints: Vec<String>,
59    },
60}
61
62#[derive(Debug, Clone)]
63pub(crate) enum ServiceProvider {
64    Create,
65    External(String),
66}
67
68impl From<&str> for ServiceProvider {
69    fn from(value: &str) -> Self {
70        if value.is_empty() {
71            Self::Create
72        } else {
73            Self::External(value.to_string())
74        }
75    }
76}
77
78#[derive(Clone)]
79pub struct StoreConfig {
80    pub store_addrs: Vec<String>,
81    pub setup_etcd: bool,
82    pub(crate) setup_pg: Option<ServiceProvider>,
83    pub(crate) setup_mysql: Option<ServiceProvider>,
84    pub enable_flat_format: bool,
85    pub enable_gc: bool,
86}
87
88#[derive(Clone)]
89pub struct Env {
90    sqlness_home: PathBuf,
91    server_addrs: ServerAddr,
92    wal: WalConfig,
93
94    /// The path to the directory that contains the pre-built GreptimeDB binary.
95    /// When running in CI, this is expected to be set.
96    /// If not set, this runner will build the GreptimeDB binary itself when needed, and set this field by then.
97    bins_dir: Arc<Mutex<Option<PathBuf>>>,
98    /// The path to the directory that contains the old pre-built GreptimeDB binaries.
99    versioned_bins_dirs: Arc<Mutex<HashMap<String, PathBuf>>>,
100    /// Pull different versions of GreptimeDB on need.
101    pull_version_on_need: bool,
102    /// Store address for metasrv metadata
103    store_config: StoreConfig,
104    /// Extra command line arguments when starting GreptimeDB binaries.
105    extra_args: Vec<String>,
106    /// Cache for the inferred gRPC argument style per `bins_dir`.
107    grpc_arg_style_cache: Arc<Mutex<HashMap<PathBuf, GrpcArgStyle>>>,
108    compat_config_stage: Arc<Mutex<CompatConfigStage>>,
109}
110
111/// Kills a process unless ownership has been transferred to [`GreptimeDB`].
112struct ChildGuard(Option<Child>);
113
114impl ChildGuard {
115    fn new(child: Child) -> Self {
116        Self(Some(child))
117    }
118
119    fn into_inner(mut self) -> Child {
120        self.0.take().unwrap()
121    }
122}
123
124impl Drop for ChildGuard {
125    fn drop(&mut self) {
126        if let Some(child) = self.0.as_mut() {
127            Env::stop_server(child);
128        }
129    }
130}
131
132/// Compatibility configuration selected for future server renders.
133#[derive(Clone, Debug)]
134pub(crate) enum CompatConfigStage {
135    /// Ordinary and baseline compatibility renders use the template unchanged.
136    Baseline,
137    /// Old compatibility renders apply this datanode overlay.
138    Old(Arc<PreparedDatanodeOverlay>),
139    /// Current compatibility renders use the template unchanged.
140    Current,
141}
142
143#[async_trait]
144impl EnvController for Env {
145    type DB = GreptimeDB;
146
147    async fn start(&self, mode: &str, id: usize, _config: Option<&Path>) -> Self::DB {
148        if self.server_addrs.server_addr.is_some() && id > 0 {
149            panic!("Parallel test mode is not supported when server address is already set.");
150        }
151
152        unsafe {
153            std::env::set_var(
154                "SQLNESS_HOME",
155                self.sqlness_home.join("copy").display().to_string(),
156            );
157        }
158        match mode {
159            "standalone" => self.start_standalone(id).await,
160            "distributed" => self.start_distributed(id).await,
161            _ => panic!("Unexpected mode: {mode}"),
162        }
163    }
164
165    /// Stop one [`Database`].
166    async fn stop(&self, _mode: &str, mut database: Self::DB) {
167        database.stop();
168    }
169}
170
171impl Env {
172    pub fn new(
173        data_home: PathBuf,
174        server_addrs: ServerAddr,
175        wal: WalConfig,
176        pull_version_on_need: bool,
177        bins_dir: Option<PathBuf>,
178        store_config: StoreConfig,
179        extra_args: Vec<String>,
180    ) -> Self {
181        Self {
182            sqlness_home: data_home,
183            server_addrs,
184            wal,
185            pull_version_on_need,
186            bins_dir: Arc::new(Mutex::new(bins_dir.clone())),
187            versioned_bins_dirs: Arc::new(Mutex::new(HashMap::from_iter([(
188                "latest".to_string(),
189                bins_dir.clone().unwrap_or(util::get_binary_dir("debug")),
190            )]))),
191            store_config,
192            extra_args,
193            grpc_arg_style_cache: Arc::new(Mutex::new(HashMap::new())),
194            compat_config_stage: Arc::new(Mutex::new(CompatConfigStage::Baseline)),
195        }
196    }
197
198    /// Selects the old-stage overlay for subsequent compatibility renders.
199    pub(crate) fn activate_compat_old(&self, overlay: Arc<PreparedDatanodeOverlay>) {
200        *self.compat_config_stage.lock().unwrap() = CompatConfigStage::Old(overlay);
201    }
202
203    /// Selects clean current-stage rendering for subsequent compatibility renders.
204    pub(crate) fn activate_compat_current(&self) {
205        *self.compat_config_stage.lock().unwrap() = CompatConfigStage::Current;
206    }
207
208    /// Takes a cheap compatibility-stage snapshot before rendering or spawning.
209    pub(crate) fn compat_config_stage(&self) -> CompatConfigStage {
210        self.compat_config_stage.lock().unwrap().clone()
211    }
212
213    async fn start_standalone(&self, id: usize) -> GreptimeDB {
214        println!("Starting standalone instance id: {id}");
215
216        if self.server_addrs.server_addr.is_some() {
217            self.connect_db(&self.server_addrs, id).await
218        } else {
219            self.build_db();
220            self.setup_wal();
221            let mut db_ctx = GreptimeDBContext::new(self.wal.clone(), self.store_config.clone());
222
223            let server_mode = ServerMode::random_standalone();
224            db_ctx.set_server_mode(server_mode.clone(), SERVER_MODE_STANDALONE_IDX);
225            let server_addr = server_mode.server_addr().unwrap();
226            let server_process = self.start_server(server_mode, &db_ctx, id, true).await;
227
228            let mut greptimedb = self.connect_db(&server_addr, id).await;
229            greptimedb.server_processes =
230                Some(Arc::new(Mutex::new(vec![server_process.into_inner()])));
231            greptimedb.is_standalone = true;
232            greptimedb.ctx = db_ctx;
233
234            greptimedb
235        }
236    }
237
238    async fn start_distributed(&self, id: usize) -> GreptimeDB {
239        self.start_distributed_inner(id).await
240    }
241
242    /// Internal: start a distributed cluster with flownode.
243    async fn start_distributed_inner(&self, id: usize) -> GreptimeDB {
244        if self.server_addrs.server_addr.is_some() {
245            self.connect_db(&self.server_addrs, id).await
246        } else {
247            self.build_db();
248            self.setup_wal();
249            self.setup_etcd();
250            self.setup_pg();
251            self.setup_mysql().await;
252            let mut db_ctx = GreptimeDBContext::new(self.wal.clone(), self.store_config.clone());
253
254            // start a distributed GreptimeDB
255            let meta_server_mode = ServerMode::random_metasrv();
256            let metasrv_port = match &meta_server_mode {
257                ServerMode::Metasrv {
258                    rpc_server_addr, ..
259                } => rpc_server_addr
260                    .split(':')
261                    .nth(1)
262                    .unwrap()
263                    .parse::<u16>()
264                    .unwrap(),
265                _ => panic!(
266                    "metasrv mode not set, maybe running in remote mode which doesn't support restart?"
267                ),
268            };
269            db_ctx.set_server_mode(meta_server_mode.clone(), SERVER_MODE_METASRV_IDX);
270            let meta_server = self.start_server(meta_server_mode, &db_ctx, id, true).await;
271
272            let mut datanodes = Vec::with_capacity(DISTRIBUTED_DATANODE_COUNT);
273            for i in 0..DISTRIBUTED_DATANODE_COUNT {
274                let datanode_mode = ServerMode::random_datanode(metasrv_port, i as u32);
275                db_ctx.set_server_mode(datanode_mode.clone(), SERVER_MODE_DATANODE_START_IDX + i);
276                let datanode = self.start_server(datanode_mode, &db_ctx, id, true).await;
277                datanodes.push(datanode);
278            }
279
280            let frontend_mode = ServerMode::random_frontend(metasrv_port);
281            let server_addr = frontend_mode.server_addr().unwrap();
282            db_ctx.set_server_mode(frontend_mode.clone(), SERVER_MODE_FRONTEND_IDX);
283            let frontend = self.start_server(frontend_mode, &db_ctx, id, true).await;
284
285            let flownode_mode = ServerMode::random_flownode(metasrv_port, 0);
286            db_ctx.set_server_mode(flownode_mode.clone(), SERVER_MODE_FLOWNODE_IDX);
287            let flownode = self.start_server(flownode_mode, &db_ctx, id, true).await;
288
289            let mut greptimedb = self.connect_db(&server_addr, id).await;
290
291            greptimedb.metasrv_process = Some(meta_server.into_inner()).into();
292            greptimedb.server_processes = Some(Arc::new(Mutex::new(
293                datanodes.into_iter().map(ChildGuard::into_inner).collect(),
294            )));
295            greptimedb.frontend_process = Some(frontend.into_inner()).into();
296            greptimedb.flownode_process = Some(flownode.into_inner()).into();
297            greptimedb.is_standalone = false;
298            greptimedb.ctx = db_ctx;
299
300            greptimedb
301        }
302    }
303
304    async fn connect_db(&self, server_addr: &ServerAddr, id: usize) -> GreptimeDB {
305        let grpc_server_addr = server_addr.server_addr.as_ref().unwrap();
306        let pg_server_addr = server_addr.pg_server_addr.as_ref().unwrap();
307        let mysql_server_addr = server_addr.mysql_server_addr.as_ref().unwrap();
308
309        let client =
310            MultiProtocolClient::connect(grpc_server_addr, pg_server_addr, mysql_server_addr).await;
311        GreptimeDB {
312            client: TokioMutex::new(client),
313            server_processes: None,
314            metasrv_process: None.into(),
315            frontend_process: None.into(),
316            flownode_process: None.into(),
317            active_bins_dir: Mutex::new(self.bins_dir.lock().unwrap().clone()),
318            ctx: GreptimeDBContext {
319                time: 0,
320                datanode_id: Default::default(),
321                wal: self.wal.clone(),
322                store_config: self.store_config.clone(),
323                server_modes: Vec::new(),
324            },
325            is_standalone: false,
326            env: self.clone(),
327            id,
328        }
329    }
330
331    fn stop_server(process: &mut Child) {
332        let _ = process.kill();
333        let _ = process.wait();
334    }
335
336    /// Infers which gRPC argument style to use for the binary at `bins_dir`.
337    fn infer_grpc_arg_style(&self, bins_dir: &Path) -> GrpcArgStyle {
338        let cache_key = bins_dir.to_path_buf();
339
340        // Fast path: already cached.
341        {
342            let cache = self.grpc_arg_style_cache.lock().unwrap();
343            if let Some(style) = cache.get(&cache_key) {
344                return *style;
345            }
346        }
347
348        let version = try_infer_version(bins_dir);
349        let style = GrpcArgStyle::for_version(version.as_ref());
350
351        // Insert into cache (may race with another thread, but both detect
352        // the same value, so it's harmless).
353        {
354            let mut cache = self.grpc_arg_style_cache.lock().unwrap();
355            cache.entry(cache_key).or_insert(style);
356        }
357
358        style
359    }
360
361    async fn start_server(
362        &self,
363        mode: ServerMode,
364        db_ctx: &GreptimeDBContext,
365        id: usize,
366        truncate_log: bool,
367    ) -> ChildGuard {
368        let bins_dir = self.bins_dir.lock().unwrap().clone().expect(
369            "GreptimeDB binary is not available. Please pass in the path to the directory that contains the pre-built GreptimeDB binary. Or you may call `self.build_db()` beforehand.",
370        );
371
372        self.start_server_with_bins_dir(mode, db_ctx, id, truncate_log, bins_dir)
373            .await
374    }
375
376    async fn start_server_with_bins_dir(
377        &self,
378        mode: ServerMode,
379        db_ctx: &GreptimeDBContext,
380        id: usize,
381        truncate_log: bool,
382        bins_dir: PathBuf,
383    ) -> ChildGuard {
384        let log_file_name = match mode {
385            ServerMode::Datanode { node_id, .. } => {
386                db_ctx.incr_datanode_id();
387                format!("greptime-{}-sqlness-datanode-{}.log", id, node_id)
388            }
389            ServerMode::Flownode { .. } => format!("greptime-{}-sqlness-flownode.log", id),
390            ServerMode::Frontend { .. } => format!("greptime-{}-sqlness-frontend.log", id),
391            ServerMode::Metasrv { .. } => format!("greptime-{}-sqlness-metasrv.log", id),
392            ServerMode::Standalone { .. } => format!("greptime-{}-sqlness-standalone.log", id),
393        };
394        let stdout_file_name = self.sqlness_home.join(log_file_name).display().to_string();
395
396        println!("DB instance {id} log file at {stdout_file_name}");
397
398        let stdout_file = OpenOptions::new()
399            .create(true)
400            .write(true)
401            .truncate(truncate_log)
402            .append(!truncate_log)
403            .open(&stdout_file_name)
404            .unwrap();
405
406        let arg_style = self.infer_grpc_arg_style(&bins_dir);
407        let compat_stage = self.compat_config_stage();
408        let args = mode.get_args(
409            &self.sqlness_home,
410            self,
411            db_ctx,
412            id,
413            arg_style,
414            &compat_stage,
415        );
416        let check_ip_addrs = mode.check_addrs();
417
418        for check_ip_addr in &check_ip_addrs {
419            if util::check_port(check_ip_addr.parse().unwrap(), Duration::from_secs(1)).await {
420                panic!(
421                    "Port {check_ip_addr} is already in use, please check and retry.",
422                    check_ip_addr = check_ip_addr
423                );
424            }
425        }
426
427        let program = PROGRAM;
428
429        let abs_bins_dir = bins_dir
430            .canonicalize()
431            .expect("Failed to canonicalize bins_dir");
432
433        let process = Command::new(abs_bins_dir.join(program))
434            .current_dir(bins_dir.clone())
435            .env("TZ", "UTC")
436            .args(args)
437            .stdout(stdout_file)
438            .spawn()
439            .unwrap_or_else(|error| {
440                panic!(
441                    "Failed to start the DB with subcommand {}, Error: {error}, path: {:?}",
442                    mode.name(),
443                    bins_dir.join(program)
444                );
445            });
446        let process = ChildGuard::new(process);
447
448        for check_ip_addr in &check_ip_addrs {
449            if !util::check_port(check_ip_addr.parse().unwrap(), Duration::from_secs(30)).await {
450                panic!(
451                    "{} doesn't up in 30 seconds, check {} for more details.",
452                    mode.name(),
453                    stdout_file_name
454                )
455            }
456        }
457
458        process
459    }
460
461    /// stop and restart the server process
462    pub(crate) async fn restart_server(&self, db: &GreptimeDB, is_full_restart: bool) {
463        let bins_dir = db.active_bins_dir.lock().unwrap().clone().expect(
464            "GreptimeDB binary is not available. Please pass in the path to the directory that contains the pre-built GreptimeDB binary. Or you may call `self.build_db()` beforehand.",
465        );
466
467        {
468            if let Some(server_process) = db.server_processes.clone() {
469                let mut server_processes = server_process.lock().unwrap();
470                for server_process in server_processes.iter_mut() {
471                    Env::stop_server(server_process);
472                }
473            }
474
475            if is_full_restart {
476                if let Some(mut metasrv_process) =
477                    db.metasrv_process.lock().expect("poisoned lock").take()
478                {
479                    Env::stop_server(&mut metasrv_process);
480                }
481                if let Some(mut frontend_process) =
482                    db.frontend_process.lock().expect("poisoned lock").take()
483                {
484                    Env::stop_server(&mut frontend_process);
485                }
486            }
487
488            // Stop flownode if present.
489            if let Some(mut flownode_process) =
490                db.flownode_process.lock().expect("poisoned lock").take()
491            {
492                Env::stop_server(&mut flownode_process);
493            }
494        }
495
496        // check if the server is distributed or standalone
497        let new_server_processes = if db.is_standalone {
498            let server_mode = db
499                .ctx
500                .get_server_mode(SERVER_MODE_STANDALONE_IDX)
501                .cloned()
502                .unwrap();
503            let server_addr = server_mode.server_addr().unwrap();
504            let new_server_process = self
505                .start_server_with_bins_dir(server_mode, &db.ctx, db.id, false, bins_dir.clone())
506                .await;
507
508            let mut client = db.client.lock().await;
509            client
510                .reconnect_mysql_client(&server_addr.mysql_server_addr.unwrap())
511                .await;
512            client
513                .reconnect_pg_client(&server_addr.pg_server_addr.unwrap())
514                .await;
515            vec![new_server_process]
516        } else {
517            db.ctx.reset_datanode_id();
518            let mut new_metasrv = None;
519            if is_full_restart {
520                let metasrv_mode = db
521                    .ctx
522                    .get_server_mode(SERVER_MODE_METASRV_IDX)
523                    .cloned()
524                    .unwrap();
525                let metasrv = self
526                    .start_server_with_bins_dir(
527                        metasrv_mode,
528                        &db.ctx,
529                        db.id,
530                        false,
531                        bins_dir.clone(),
532                    )
533                    .await;
534                new_metasrv = Some(metasrv);
535
536                // wait for metasrv to start
537                // since it seems older version of db might take longer to complete election
538                tokio::time::sleep(Duration::from_secs(5)).await;
539            }
540
541            let mut processes = vec![];
542            for i in 0..DISTRIBUTED_DATANODE_COUNT {
543                let datanode_mode = db
544                    .ctx
545                    .get_server_mode(SERVER_MODE_DATANODE_START_IDX + i)
546                    .cloned()
547                    .unwrap();
548                let new_server_process = self
549                    .start_server_with_bins_dir(
550                        datanode_mode,
551                        &db.ctx,
552                        db.id,
553                        false,
554                        bins_dir.clone(),
555                    )
556                    .await;
557                processes.push(new_server_process);
558            }
559
560            let mut new_frontend = None;
561            if is_full_restart {
562                let frontend_mode = db
563                    .ctx
564                    .get_server_mode(SERVER_MODE_FRONTEND_IDX)
565                    .cloned()
566                    .unwrap();
567                let server_addr = frontend_mode.server_addr().unwrap();
568                let frontend = self
569                    .start_server_with_bins_dir(
570                        frontend_mode,
571                        &db.ctx,
572                        db.id,
573                        false,
574                        bins_dir.clone(),
575                    )
576                    .await;
577
578                // Reconnect protocol clients to the new frontend process
579                // so that MySQL/Postgres queries use the restarted frontend,
580                // not stale connections to the old (killed) process.
581                let mut client = db.client.lock().await;
582                client
583                    .reconnect_mysql_client(server_addr.mysql_server_addr.as_ref().unwrap())
584                    .await;
585                client
586                    .reconnect_pg_client(server_addr.pg_server_addr.as_ref().unwrap())
587                    .await;
588                new_frontend = Some(frontend);
589            }
590
591            // Restart flownode.
592            let mut new_flownode = None;
593            if let Some(flownode_mode) = db.ctx.get_server_mode(SERVER_MODE_FLOWNODE_IDX).cloned() {
594                let flownode = self
595                    .start_server_with_bins_dir(
596                        flownode_mode,
597                        &db.ctx,
598                        db.id,
599                        false,
600                        bins_dir.clone(),
601                    )
602                    .await;
603                new_flownode = Some(flownode);
604            }
605
606            if let Some(metasrv) = new_metasrv {
607                let mut metasrv_process = db.metasrv_process.lock().expect("lock poisoned");
608                metasrv_process.replace(metasrv.into_inner());
609            }
610            if let Some(frontend) = new_frontend {
611                let mut frontend_process = db.frontend_process.lock().expect("lock poisoned");
612                frontend_process.replace(frontend.into_inner());
613            }
614            if let Some(flownode) = new_flownode {
615                let mut flownode_process = db.flownode_process.lock().expect("lock poisoned");
616                flownode_process.replace(flownode.into_inner());
617            }
618
619            processes
620        };
621
622        if let Some(server_processes) = db.server_processes.clone() {
623            let mut server_processes = server_processes.lock().unwrap();
624            *server_processes = new_server_processes
625                .into_iter()
626                .map(ChildGuard::into_inner)
627                .collect();
628        }
629    }
630
631    /// Setup kafka wal cluster if needed. The counterpart is in [GreptimeDB::stop].
632    fn setup_wal(&self) {
633        if matches!(self.wal, WalConfig::Kafka { needs_kafka_cluster, .. } if needs_kafka_cluster) {
634            util::setup_wal();
635        }
636    }
637
638    /// Setup etcd if needed.
639    fn setup_etcd(&self) {
640        if self.store_config.setup_etcd {
641            let client_ports = self
642                .store_config
643                .store_addrs
644                .iter()
645                .map(|s| s.split(':').nth(1).unwrap().parse::<u16>().unwrap())
646                .collect::<Vec<_>>();
647            util::setup_etcd(client_ports, None, None);
648        }
649    }
650
651    /// Setup PostgreSql if needed.
652    fn setup_pg(&self) {
653        if matches!(self.store_config.setup_pg, Some(ServiceProvider::Create)) {
654            let client_ports = self
655                .store_config
656                .store_addrs
657                .iter()
658                .map(|s| s.split(':').nth(1).unwrap().parse::<u16>().unwrap())
659                .collect::<Vec<_>>();
660            let client_port = client_ports.first().unwrap_or(&5432);
661            util::setup_pg(*client_port, None);
662        }
663    }
664
665    /// Setup MySql if needed.
666    async fn setup_mysql(&self) {
667        if matches!(self.store_config.setup_mysql, Some(ServiceProvider::Create)) {
668            let client_ports = self
669                .store_config
670                .store_addrs
671                .iter()
672                .map(|s| s.split(':').nth(1).unwrap().parse::<u16>().unwrap())
673                .collect::<Vec<_>>();
674            let client_port = client_ports.first().unwrap_or(&3306);
675            util::setup_mysql(*client_port, None);
676
677            // Docker of MySQL starts slowly, so we need to wait for a while
678            tokio::time::sleep(Duration::from_secs(10)).await;
679        }
680    }
681
682    /// Build the DB with `cargo build --bin greptime`
683    fn build_db(&self) {
684        let mut bins_dir = self.bins_dir.lock().unwrap();
685        if bins_dir.is_some() {
686            return;
687        }
688
689        println!("Going to build the DB...");
690        let output = Command::new("cargo")
691            .current_dir(util::get_workspace_root())
692            .args([
693                "build",
694                "--bin",
695                "greptime",
696                "--features",
697                "pg_kvbackend,mysql_kvbackend,vector_index",
698            ])
699            .output()
700            .expect("Failed to start GreptimeDB");
701        if !output.status.success() {
702            println!("Failed to build GreptimeDB, {}", output.status);
703            println!("Cargo build stdout:");
704            io::stdout().write_all(&output.stdout).unwrap();
705            println!("Cargo build stderr:");
706            io::stderr().write_all(&output.stderr).unwrap();
707            panic!();
708        }
709
710        bins_dir.replace(util::get_binary_dir("debug"));
711    }
712
713    pub(crate) fn extra_args(&self) -> &Vec<String> {
714        &self.extra_args
715    }
716
717    /// Start a distributed GreptimeDB cluster. Exposed for compat runner.
718    pub(crate) async fn compat_start_distributed(&self, id: usize) -> GreptimeDB {
719        self.start_distributed(id).await
720    }
721
722    /// Start a standalone GreptimeDB instance. Exposed for compat runner.
723    pub(crate) async fn compat_start_standalone(&self, id: usize) -> GreptimeDB {
724        self.start_standalone(id).await
725    }
726
727    /// Restart a compatibility instance with a new binary directory.
728    pub(crate) async fn compat_restart(&self, db: &GreptimeDB, bins_dir: PathBuf) {
729        *db.active_bins_dir.lock().unwrap() = Some(bins_dir);
730        self.restart_server(db, true).await;
731        self.wait_query_ready(db).await;
732    }
733
734    /// Wait for the query endpoint to become ready after restart.
735    async fn wait_query_ready(&self, db: &GreptimeDB) {
736        let server_mode_idx = if db.is_standalone {
737            SERVER_MODE_STANDALONE_IDX
738        } else {
739            SERVER_MODE_FRONTEND_IDX
740        };
741        let server_mode = db.ctx.get_server_mode(server_mode_idx).cloned().unwrap();
742        if let Some(addr) = server_mode.check_addrs().first() {
743            println!("Waiting for query endpoint readiness at {addr}...");
744            crate::util::retry_with_backoff(
745                || async {
746                    let mut client = db.client.lock().await;
747                    match client.grpc_query("SELECT 1").await {
748                        Ok(_) => Ok(()),
749                        Err(e) => Err(format!("Query endpoint not ready: {e}")),
750                    }
751                },
752                10,
753                std::time::Duration::from_secs(1),
754            )
755            .await
756            .unwrap_or_else(|e| panic!("Query endpoint failed to become ready: {e}"));
757        }
758    }
759}
760
761pub struct GreptimeDB {
762    server_processes: Option<Arc<Mutex<Vec<Child>>>>,
763    metasrv_process: Mutex<Option<Child>>,
764    frontend_process: Mutex<Option<Child>>,
765    flownode_process: Mutex<Option<Child>>,
766    client: TokioMutex<MultiProtocolClient>,
767    active_bins_dir: Mutex<Option<PathBuf>>,
768    ctx: GreptimeDBContext,
769    is_standalone: bool,
770    env: Env,
771    id: usize,
772}
773
774impl GreptimeDB {
775    async fn postgres_query(&self, _ctx: QueryContext, query: String) -> Box<dyn Display> {
776        let mut client = self.client.lock().await;
777
778        match client.postgres_query(&query).await {
779            Ok(rows) => Box::new(PostgresqlFormatter::from(rows)),
780            Err(e) => Box::new(e),
781        }
782    }
783
784    async fn mysql_query(&self, _ctx: QueryContext, query: String) -> Box<dyn Display> {
785        let mut client = self.client.lock().await;
786
787        match client.mysql_query(&query).await {
788            Ok(res) => Box::new(MysqlFormatter::from(res)),
789            Err(e) => Box::new(e),
790        }
791    }
792
793    async fn grpc_query(&self, _ctx: QueryContext, query: String) -> Box<dyn Display> {
794        let mut client = self.client.lock().await;
795
796        match client.grpc_query(&query).await {
797            Ok(rows) => Box::new(OutputFormatter::from(rows)),
798            Err(e) => Box::new(ErrorFormatter::from(e)),
799        }
800    }
801
802    /// Handle `QueryContext` directives for compat statement execution.
803    ///
804    /// Inspects `QueryContext` keys set by sqlness interceptors:
805    /// - `restart`: restarts the server (datanode-only) if not using external address.
806    /// - `version`: switches to the specified binary version and performs a full restart.
807    ///
808    /// This does **not** execute queries itself; it only prepares the server state.
809    /// Used by the compat runner.
810    pub(crate) async fn compat_prepare_query_context(&self, ctx: &QueryContext) {
811        if ctx.context.contains_key("restart") && self.env.server_addrs.server_addr.is_none() {
812            self.env.restart_server(self, false).await;
813        } else if let Some(version) = ctx.context.get("version") {
814            let version_bin_dir = self
815                .env
816                .versioned_bins_dirs
817                .lock()
818                .expect("lock poison")
819                .get(version.as_str())
820                .cloned();
821
822            match version_bin_dir {
823                Some(path) if path.join(PROGRAM).is_file() => {
824                    *self.active_bins_dir.lock().unwrap() = Some(path);
825                }
826                _ => {
827                    maybe_pull_binary(version, self.env.pull_version_on_need).await;
828                    let root = get_workspace_root();
829                    let new_path = PathBuf::from_iter([&root, version]);
830                    *self.active_bins_dir.lock().unwrap() = Some(new_path);
831                }
832            }
833
834            self.env.restart_server(self, true).await;
835            // sleep for a while to wait for the server to fully boot up
836            tokio::time::sleep(Duration::from_secs(5)).await;
837        }
838    }
839
840    pub(crate) async fn compat_query(
841        &self,
842        query: &str,
843        ctx: &QueryContext,
844    ) -> Result<String, String> {
845        let mut client = self.client.lock().await;
846
847        // Handle protocol switching
848        if let Some(protocol) = ctx.context.get(PROTOCOL_KEY) {
849            if protocol == MYSQL {
850                return match client.mysql_query(query).await {
851                    Ok(res) => Ok(crate::formatter::MysqlFormatter::from(res).to_string()),
852                    Err(e) => Err(e),
853                };
854            } else {
855                // postgres
856                return match client.postgres_query(query).await {
857                    Ok(rows) => Ok(crate::formatter::PostgresqlFormatter::from(rows).to_string()),
858                    Err(e) => Err(e),
859                };
860            }
861        }
862
863        // Default: gRPC
864        match client.grpc_query(query).await {
865            Ok(output) => Ok(OutputFormatter::from(output).to_string()),
866            Err(e) => {
867                let status_code = e.status_code();
868                let root_cause = e.output_msg();
869                Err(format!(
870                    "Error: {}({status_code}), {root_cause}",
871                    status_code as u32
872                ))
873            }
874        }
875    }
876}
877
878#[async_trait]
879impl Database for GreptimeDB {
880    async fn query(&self, ctx: QueryContext, query: String) -> Box<dyn Display> {
881        if ctx.context.contains_key("restart") && self.env.server_addrs.server_addr.is_none() {
882            self.env.restart_server(self, false).await;
883        } else if let Some(version) = ctx.context.get("version") {
884            let version_bin_dir = self
885                .env
886                .versioned_bins_dirs
887                .lock()
888                .expect("lock poison")
889                .get(version.as_str())
890                .cloned();
891
892            match version_bin_dir {
893                Some(path) if path.join(PROGRAM).is_file() => {
894                    // use version in versioned_bins_dirs
895                    *self.active_bins_dir.lock().unwrap() = Some(path);
896                }
897                _ => {
898                    // use version in dir files
899                    maybe_pull_binary(version, self.env.pull_version_on_need).await;
900                    let root = get_workspace_root();
901                    let new_path = PathBuf::from_iter([&root, version]);
902                    *self.active_bins_dir.lock().unwrap() = Some(new_path);
903                }
904            }
905
906            self.env.restart_server(self, true).await;
907            // sleep for a while to wait for the server to fully boot up
908            tokio::time::sleep(Duration::from_secs(5)).await;
909        }
910
911        if let Some(protocol) = ctx.context.get(PROTOCOL_KEY) {
912            // protocol is bound to be either "mysql" or "postgres"
913            if protocol == MYSQL {
914                self.mysql_query(ctx, query).await
915            } else {
916                self.postgres_query(ctx, query).await
917            }
918        } else {
919            self.grpc_query(ctx, query).await
920        }
921    }
922}
923
924impl GreptimeDB {
925    fn stop(&mut self) {
926        if let Some(server_processes) = self.server_processes.clone() {
927            let mut server_processes = server_processes.lock().unwrap();
928            for mut server_process in server_processes.drain(..) {
929                Env::stop_server(&mut server_process);
930                println!(
931                    "Standalone or Datanode (pid = {}) is stopped",
932                    server_process.id()
933                );
934            }
935        }
936        if let Some(mut metasrv) = self
937            .metasrv_process
938            .lock()
939            .expect("someone else panic when holding lock")
940            .take()
941        {
942            Env::stop_server(&mut metasrv);
943            println!("Metasrv (pid = {}) is stopped", metasrv.id());
944        }
945        if let Some(mut frontend) = self
946            .frontend_process
947            .lock()
948            .expect("someone else panic when holding lock")
949            .take()
950        {
951            Env::stop_server(&mut frontend);
952            println!("Frontend (pid = {}) is stopped", frontend.id());
953        }
954        if let Some(mut flownode) = self
955            .flownode_process
956            .lock()
957            .expect("someone else panic when holding lock")
958            .take()
959        {
960            Env::stop_server(&mut flownode);
961            println!("Flownode (pid = {}) is stopped", flownode.id());
962        }
963        if matches!(self.ctx.wal, WalConfig::Kafka { needs_kafka_cluster, .. } if needs_kafka_cluster)
964        {
965            util::teardown_wal();
966        }
967    }
968
969    /// Stop all processes managed by this GreptimeDB. Exposed for compat runner.
970    pub(crate) fn compat_stop(&mut self) {
971        self.stop();
972    }
973}
974
975impl Drop for GreptimeDB {
976    fn drop(&mut self) {
977        if self.env.server_addrs.server_addr.is_none() {
978            self.stop();
979        }
980    }
981}
982
983pub struct GreptimeDBContext {
984    /// Start time in millisecond
985    time: i64,
986    datanode_id: AtomicU32,
987    wal: WalConfig,
988    store_config: StoreConfig,
989    server_modes: Vec<ServerMode>,
990}
991
992impl GreptimeDBContext {
993    pub fn new(wal: WalConfig, store_config: StoreConfig) -> Self {
994        Self {
995            time: common_time::util::current_time_millis(),
996            datanode_id: AtomicU32::new(0),
997            wal,
998            store_config,
999            server_modes: Vec::new(),
1000        }
1001    }
1002
1003    pub(crate) fn time(&self) -> i64 {
1004        self.time
1005    }
1006
1007    pub fn is_raft_engine(&self) -> bool {
1008        matches!(self.wal, WalConfig::RaftEngine)
1009    }
1010
1011    pub fn kafka_wal_broker_endpoints(&self) -> String {
1012        match &self.wal {
1013            WalConfig::RaftEngine => String::new(),
1014            WalConfig::Kafka {
1015                broker_endpoints, ..
1016            } => serde_json::to_string(&broker_endpoints).unwrap(),
1017        }
1018    }
1019
1020    fn incr_datanode_id(&self) {
1021        let _ = self.datanode_id.fetch_add(1, Ordering::Relaxed);
1022    }
1023
1024    fn reset_datanode_id(&self) {
1025        self.datanode_id.store(0, Ordering::Relaxed);
1026    }
1027
1028    pub(crate) fn store_config(&self) -> StoreConfig {
1029        self.store_config.clone()
1030    }
1031
1032    fn set_server_mode(&mut self, mode: ServerMode, idx: usize) {
1033        if idx >= self.server_modes.len() {
1034            self.server_modes.resize(idx + 1, mode.clone());
1035        }
1036        self.server_modes[idx] = mode;
1037    }
1038
1039    fn get_server_mode(&self, idx: usize) -> Option<&ServerMode> {
1040        self.server_modes.get(idx)
1041    }
1042}
1043
1044#[cfg(test)]
1045mod tests {
1046    use super::*;
1047    use crate::cmd::bare::ServerAddr;
1048    use crate::cmd::datanode_overlay::{DatanodeOverlay, DatanodeProtectionPolicy};
1049
1050    fn test_env(temp_dir: &Path) -> Env {
1051        Env::new(
1052            temp_dir.to_path_buf(),
1053            ServerAddr::default(),
1054            WalConfig::RaftEngine,
1055            false,
1056            None,
1057            StoreConfig {
1058                store_addrs: vec![],
1059                setup_etcd: false,
1060                setup_pg: None,
1061                setup_mysql: None,
1062                enable_flat_format: false,
1063                enable_gc: false,
1064            },
1065            vec![],
1066        )
1067    }
1068
1069    #[test]
1070    fn compat_config_stage_is_shared_by_env_clones_and_transitions_cleanly() {
1071        let temp_dir = tempfile::tempdir().unwrap();
1072        std::fs::write(temp_dir.path().join("overlay.toml"), "value = 1").unwrap();
1073        let overlay = DatanodeOverlay::load(temp_dir.path(), Path::new("overlay.toml"))
1074            .unwrap()
1075            .prepare(&DatanodeProtectionPolicy::for_wal(&WalConfig::RaftEngine))
1076            .unwrap();
1077        let env = test_env(temp_dir.path());
1078        let clone = env.clone();
1079
1080        assert!(matches!(
1081            clone.compat_config_stage(),
1082            CompatConfigStage::Baseline
1083        ));
1084        env.activate_compat_old(Arc::new(overlay));
1085        assert!(matches!(
1086            clone.compat_config_stage(),
1087            CompatConfigStage::Old(_)
1088        ));
1089        clone.activate_compat_current();
1090        assert!(matches!(
1091            env.compat_config_stage(),
1092            CompatConfigStage::Current
1093        ));
1094    }
1095
1096    #[cfg(unix)]
1097    #[test]
1098    fn child_guard_kills_untransferred_processes() {
1099        let child = std::process::Command::new("sleep")
1100            .arg("60")
1101            .spawn()
1102            .unwrap();
1103        let pid = child.id().to_string();
1104
1105        drop(ChildGuard::new(child));
1106
1107        assert!(
1108            !std::process::Command::new("kill")
1109                .args(["-0", &pid])
1110                .status()
1111                .unwrap()
1112                .success()
1113        );
1114    }
1115
1116    #[cfg(unix)]
1117    #[test]
1118    fn child_guard_kills_process_on_forced_unwind() {
1119        let child = std::process::Command::new("sleep")
1120            .arg("60")
1121            .spawn()
1122            .unwrap();
1123        let pid = child.id().to_string();
1124
1125        let unwind = std::panic::catch_unwind(|| {
1126            let _guard = ChildGuard::new(child);
1127            panic!("forced startup unwind");
1128        });
1129
1130        assert!(unwind.is_err());
1131        assert!(
1132            !std::process::Command::new("kill")
1133                .args(["-0", &pid])
1134                .status()
1135                .unwrap()
1136                .success()
1137        );
1138    }
1139}