Skip to main content

cmd/
datanode.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
15pub mod builder;
16#[allow(clippy::print_stdout)]
17pub(crate) mod objbench;
18#[allow(clippy::print_stdout)]
19pub mod parquetbench;
20#[allow(clippy::print_stdout)]
21pub mod scanbench;
22
23use std::path::Path;
24use std::time::Duration;
25
26use async_trait::async_trait;
27use clap::Parser;
28use common_config::Configurable;
29use common_telemetry::logging::{DEFAULT_LOGGING_DIR, TracingOptions};
30use common_telemetry::{info, warn};
31use common_wal::config::DatanodeWalConfig;
32use datanode::config::RegionEngineConfig;
33use datanode::datanode::Datanode;
34use meta_client::MetaClientOptions;
35use serde::{Deserialize, Serialize};
36use snafu::{ResultExt, ensure};
37use tracing_appender::non_blocking::WorkerGuard;
38
39use crate::App;
40use crate::datanode::builder::InstanceBuilder;
41use crate::datanode::objbench::ObjbenchCommand;
42use crate::datanode::parquetbench::ParquetbenchCommand;
43use crate::datanode::scanbench::ScanbenchCommand;
44use crate::error::{
45    LoadLayeredConfigSnafu, MissingConfigSnafu, Result, ShutdownDatanodeSnafu, StartDatanodeSnafu,
46};
47use crate::options::{GlobalOptions, GreptimeOptions};
48
49pub const APP_NAME: &str = "greptime-datanode";
50
51type DatanodeOptions = GreptimeOptions<datanode::config::DatanodeOptions>;
52
53pub struct Instance {
54    datanode: Datanode,
55
56    // Keep the logging guard to prevent the worker from being dropped.
57    _guard: Vec<WorkerGuard>,
58}
59
60impl Instance {
61    pub fn new(datanode: Datanode, guard: Vec<WorkerGuard>) -> Self {
62        Self {
63            datanode,
64            _guard: guard,
65        }
66    }
67
68    pub fn datanode(&self) -> &Datanode {
69        &self.datanode
70    }
71
72    /// allow customizing datanode for downstream projects
73    pub fn datanode_mut(&mut self) -> &mut Datanode {
74        &mut self.datanode
75    }
76}
77
78#[async_trait]
79impl App for Instance {
80    fn name(&self) -> &str {
81        APP_NAME
82    }
83
84    async fn start(&mut self) -> Result<()> {
85        plugins::start_datanode_plugins(&self.datanode)
86            .await
87            .context(StartDatanodeSnafu)?;
88
89        self.datanode.start().await.context(StartDatanodeSnafu)
90    }
91
92    async fn stop(&mut self) -> Result<()> {
93        self.datanode
94            .shutdown()
95            .await
96            .context(ShutdownDatanodeSnafu)
97    }
98}
99
100#[derive(Parser)]
101pub struct Command {
102    #[clap(subcommand)]
103    pub subcmd: SubCommand,
104}
105
106impl Command {
107    pub async fn build_with(&self, builder: InstanceBuilder) -> Result<Instance> {
108        self.subcmd.build_with(builder).await
109    }
110
111    pub fn load_options(&self, global_options: &GlobalOptions) -> Result<DatanodeOptions> {
112        match &self.subcmd {
113            SubCommand::Start(cmd) => cmd.load_options(global_options),
114            // Bench commands are standalone utilities and don't need to load DatanodeOptions.
115            SubCommand::Objbench(_) | SubCommand::Scanbench(_) => Self::default_bench_options(),
116            SubCommand::Parquetbench(_) => Self::default_bench_options(),
117        }
118    }
119
120    /// Builds default [`DatanodeOptions`] for standalone bench commands that don't
121    /// load a real datanode config.
122    fn default_bench_options() -> Result<DatanodeOptions> {
123        let mut opts = datanode::config::DatanodeOptions::default();
124        opts.sanitize();
125        Ok(DatanodeOptions {
126            runtime: Default::default(),
127            plugins: Default::default(),
128            component: opts,
129        })
130    }
131}
132
133#[derive(Parser)]
134pub enum SubCommand {
135    Start(StartCommand),
136    /// Object storage benchmark tool
137    Objbench(ObjbenchCommand),
138    /// Scan benchmark tool - benchmarks scanning a region directly from storage
139    Scanbench(ScanbenchCommand),
140    /// Benchmark scanning a single parquet SST.
141    Parquetbench(ParquetbenchCommand),
142}
143
144impl SubCommand {
145    async fn build_with(&self, builder: InstanceBuilder) -> Result<Instance> {
146        match self {
147            SubCommand::Start(cmd) => {
148                info!("Building datanode with {:#?}", cmd);
149                builder.build().await
150            }
151            SubCommand::Objbench(cmd) => {
152                cmd.run().await?;
153                std::process::exit(0);
154            }
155            SubCommand::Scanbench(cmd) => {
156                cmd.run().await?;
157                std::process::exit(0);
158            }
159            SubCommand::Parquetbench(cmd) => {
160                cmd.run().await?;
161                std::process::exit(0);
162            }
163        }
164    }
165}
166
167/// Storage engine config
168#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
169#[serde(default)]
170pub struct StorageConfig {
171    /// The working directory of database
172    pub data_home: String,
173    #[serde(flatten)]
174    pub store: object_store::config::ObjectStoreConfig,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
178#[serde(default)]
179struct StorageConfigWrapper {
180    storage: StorageConfig,
181    region_engine: Vec<RegionEngineConfig>,
182    #[serde(default, deserialize_with = "deserialize_wal_config")]
183    wal: DatanodeWalConfig,
184}
185
186/// Deserializes [`DatanodeWalConfig`], defaulting `provider` to `"raft_engine"` when
187/// the `[wal]` section is present but omits it. This mirrors the behavior of the
188/// datanode's layered config loader which merges over a default that already contains
189/// `provider`.
190fn deserialize_wal_config<'de, D>(
191    deserializer: D,
192) -> std::result::Result<DatanodeWalConfig, D::Error>
193where
194    D: serde::Deserializer<'de>,
195{
196    use serde::de::Error as _;
197
198    let mut table = <toml::value::Table as serde::Deserialize>::deserialize(deserializer)?;
199    if !table.contains_key("provider") {
200        table.insert(
201            "provider".to_string(),
202            toml::Value::String("raft_engine".to_string()),
203        );
204    }
205    DatanodeWalConfig::deserialize(toml::Value::Table(table)).map_err(D::Error::custom)
206}
207
208#[derive(Debug, Parser, Default)]
209pub struct StartCommand {
210    #[clap(long)]
211    node_id: Option<u64>,
212    /// The address to bind the gRPC server.
213    #[clap(long = "grpc-bind-addr", alias = "rpc-bind-addr", alias = "rpc-addr")]
214    grpc_bind_addr: Option<String>,
215    /// The address advertised to the metasrv, and used for connections from outside the host.
216    /// If left empty or unset, the server will automatically use the IP address of the first network interface
217    /// on the host, with the same port number as the one specified in `grpc_bind_addr`.
218    #[clap(
219        long = "grpc-server-addr",
220        alias = "rpc-server-addr",
221        alias = "rpc-hostname"
222    )]
223    grpc_server_addr: Option<String>,
224    #[clap(long, value_delimiter = ',', num_args = 1..)]
225    metasrv_addrs: Option<Vec<String>>,
226    #[clap(short, long)]
227    config_file: Option<String>,
228    #[clap(long)]
229    data_home: Option<String>,
230    #[clap(long)]
231    wal_dir: Option<String>,
232    #[clap(long)]
233    http_addr: Option<String>,
234    #[clap(long)]
235    http_timeout: Option<u64>,
236    #[clap(long, default_value = "GREPTIMEDB_DATANODE")]
237    env_prefix: String,
238}
239
240impl StartCommand {
241    pub fn load_options(&self, global_options: &GlobalOptions) -> Result<DatanodeOptions> {
242        let mut opts = DatanodeOptions::load_layered_options(
243            self.config_file.as_deref(),
244            self.env_prefix.as_ref(),
245        )
246        .context(LoadLayeredConfigSnafu)?;
247
248        self.merge_with_cli_options(global_options, &mut opts)?;
249        opts.component.sanitize();
250
251        Ok(opts)
252    }
253
254    // The precedence order is: cli > config file > environment variables > default values.
255    #[allow(deprecated)]
256    fn merge_with_cli_options(
257        &self,
258        global_options: &GlobalOptions,
259        opts: &mut DatanodeOptions,
260    ) -> Result<()> {
261        let opts = &mut opts.component;
262
263        if let Some(dir) = &global_options.log_dir {
264            opts.logging.dir.clone_from(dir);
265        }
266
267        if global_options.log_level.is_some() {
268            opts.logging.level.clone_from(&global_options.log_level);
269        }
270
271        opts.tracing = TracingOptions {
272            #[cfg(feature = "tokio-console")]
273            tokio_console_addr: global_options.tokio_console_addr.clone(),
274        };
275
276        if let Some(addr) = &self.grpc_bind_addr {
277            opts.grpc.bind_addr.clone_from(addr);
278        } else if let Some(addr) = &opts.rpc_addr {
279            warn!(
280                "Use the deprecated attribute `DatanodeOptions.rpc_addr`, please use `grpc.bind_addr` instead."
281            );
282            opts.grpc.bind_addr.clone_from(addr);
283        }
284
285        if let Some(server_addr) = &self.grpc_server_addr {
286            opts.grpc.server_addr.clone_from(server_addr);
287        } else if let Some(server_addr) = &opts.rpc_hostname {
288            warn!(
289                "Use the deprecated attribute `DatanodeOptions.rpc_hostname`, please use `grpc.server_addr` instead."
290            );
291            opts.grpc.server_addr.clone_from(server_addr);
292        }
293
294        if let Some(runtime_size) = opts.rpc_runtime_size {
295            warn!(
296                "Use the deprecated attribute `DatanodeOptions.rpc_runtime_size`, please use `grpc.runtime_size` instead."
297            );
298            opts.grpc.runtime_size = runtime_size;
299        }
300
301        if let Some(max_recv_message_size) = opts.rpc_max_recv_message_size {
302            warn!(
303                "Use the deprecated attribute `DatanodeOptions.rpc_max_recv_message_size`, please use `grpc.max_recv_message_size` instead."
304            );
305            opts.grpc.max_recv_message_size = max_recv_message_size;
306        }
307
308        if let Some(max_send_message_size) = opts.rpc_max_send_message_size {
309            warn!(
310                "Use the deprecated attribute `DatanodeOptions.rpc_max_send_message_size`, please use `grpc.max_send_message_size` instead."
311            );
312            opts.grpc.max_send_message_size = max_send_message_size;
313        }
314
315        if let Some(node_id) = self.node_id {
316            opts.node_id = Some(node_id);
317        }
318
319        if let Some(metasrv_addrs) = &self.metasrv_addrs {
320            opts.meta_client
321                .get_or_insert_with(MetaClientOptions::default)
322                .metasrv_addrs
323                .clone_from(metasrv_addrs);
324        }
325
326        ensure!(
327            opts.node_id.is_some(),
328            MissingConfigSnafu {
329                msg: "Missing node id option"
330            }
331        );
332
333        if let Some(data_home) = &self.data_home {
334            opts.storage.data_home.clone_from(data_home);
335        }
336
337        // `wal_dir` only affects raft-engine config.
338        if let Some(wal_dir) = &self.wal_dir
339            && let DatanodeWalConfig::RaftEngine(raft_engine_config) = &mut opts.wal
340        {
341            if raft_engine_config
342                .dir
343                .as_ref()
344                .is_some_and(|original_dir| original_dir != wal_dir)
345            {
346                info!("The wal dir of raft-engine is altered to {wal_dir}");
347            }
348            raft_engine_config.dir.replace(wal_dir.clone());
349        }
350
351        // If the logging dir is not set, use the default logs dir in the data home.
352        if opts.logging.dir.is_empty() {
353            opts.logging.dir = Path::new(&opts.storage.data_home)
354                .join(DEFAULT_LOGGING_DIR)
355                .to_string_lossy()
356                .to_string();
357        }
358
359        if let Some(http_addr) = &self.http_addr {
360            opts.http.addr.clone_from(http_addr);
361        }
362
363        if let Some(http_timeout) = self.http_timeout {
364            opts.http.timeout = Duration::from_secs(http_timeout)
365        }
366
367        // Disable dashboard in datanode.
368        opts.http.disable_dashboard = true;
369
370        Ok(())
371    }
372}
373
374#[cfg(test)]
375mod tests {
376    use std::assert_matches;
377    use std::io::Write;
378    use std::time::Duration;
379
380    use clap::{CommandFactory, Parser};
381    use common_config::ENV_VAR_SEP;
382    use common_test_util::temp_dir::create_named_temp_file;
383    use object_store::config::{FileConfig, GcsConfig, ObjectStoreConfig, S3Config};
384
385    use super::*;
386    use crate::options::GlobalOptions;
387
388    #[test]
389    fn test_deprecated_cli_options() {
390        common_telemetry::init_default_ut_logging();
391        let mut file = create_named_temp_file();
392        let toml_str = r#"
393            enable_memory_catalog = false
394            node_id = 42
395
396            rpc_addr = "127.0.0.1:4001"
397            rpc_hostname = "192.168.0.1"
398            [grpc]
399            bind_addr = "127.0.0.1:3001"
400            server_addr = "127.0.0.1"
401            runtime_size = 8
402        "#;
403        write!(file, "{}", toml_str).unwrap();
404
405        let cmd = StartCommand {
406            config_file: Some(file.path().to_str().unwrap().to_string()),
407            ..Default::default()
408        };
409
410        let options = cmd.load_options(&Default::default()).unwrap().component;
411        assert_eq!("127.0.0.1:4001".to_string(), options.grpc.bind_addr);
412        assert_eq!("192.168.0.1".to_string(), options.grpc.server_addr);
413    }
414
415    #[test]
416    fn test_read_from_config_file() {
417        let mut file = create_named_temp_file();
418        let toml_str = r#"
419            enable_memory_catalog = false
420            node_id = 42
421
422            [grpc]
423            bind_addr = "127.0.0.1:3001"
424            server_addr = "127.0.0.1"
425            runtime_size = 8
426
427            [meta_client]
428            metasrv_addrs = ["127.0.0.1:3002"]
429            timeout = "3s"
430            connect_timeout = "5s"
431            ddl_timeout = "10s"
432            tcp_nodelay = true
433
434            [wal]
435            provider = "raft_engine"
436            dir = "/other/wal"
437            file_size = "1GB"
438            purge_threshold = "50GB"
439            purge_interval = "10m"
440            read_batch_size = 128
441            sync_write = false
442
443            [storage]
444            data_home = "./greptimedb_data/"
445            type = "File"
446
447            [[storage.providers]]
448            type = "Gcs"
449            bucket = "foo"
450            endpoint = "bar"
451
452            [[storage.providers]]
453            type = "S3"
454            bucket = "foo"
455
456            [logging]
457            level = "debug"
458            dir = "./greptimedb_data/test/logs"
459        "#;
460        write!(file, "{}", toml_str).unwrap();
461
462        let cmd = StartCommand {
463            config_file: Some(file.path().to_str().unwrap().to_string()),
464            ..Default::default()
465        };
466
467        let options = cmd.load_options(&Default::default()).unwrap().component;
468
469        assert_eq!("127.0.0.1:3001".to_string(), options.grpc.bind_addr);
470        assert_eq!("127.0.0.1".to_string(), options.grpc.server_addr);
471        assert_eq!(Some(42), options.node_id);
472
473        let DatanodeWalConfig::RaftEngine(raft_engine_config) = options.wal else {
474            unreachable!()
475        };
476        assert_eq!("/other/wal", raft_engine_config.dir.unwrap());
477        assert_eq!(Duration::from_secs(600), raft_engine_config.purge_interval);
478        assert_eq!(1024 * 1024 * 1024, raft_engine_config.file_size.0);
479        assert_eq!(
480            1024 * 1024 * 1024 * 50,
481            raft_engine_config.purge_threshold.0
482        );
483        assert!(!raft_engine_config.sync_write);
484
485        let MetaClientOptions {
486            metasrv_addrs: metasrv_addr,
487            timeout,
488            connect_timeout,
489            ddl_timeout,
490            tcp_nodelay,
491            ..
492        } = options.meta_client.unwrap();
493
494        assert_eq!(vec!["127.0.0.1:3002".to_string()], metasrv_addr);
495        assert_eq!(5000, connect_timeout.as_millis());
496        assert_eq!(10000, ddl_timeout.as_millis());
497        assert_eq!(3000, timeout.as_millis());
498        assert!(tcp_nodelay);
499        assert_eq!("./greptimedb_data/", options.storage.data_home);
500        assert!(matches!(
501            &options.storage.store,
502            ObjectStoreConfig::File(FileConfig { .. })
503        ));
504        assert_eq!(options.storage.providers.len(), 2);
505        assert!(matches!(
506            options.storage.providers[0],
507            ObjectStoreConfig::Gcs(GcsConfig { .. })
508        ));
509        assert!(matches!(
510            options.storage.providers[1],
511            ObjectStoreConfig::S3(S3Config { .. })
512        ));
513
514        assert_eq!("debug", options.logging.level.unwrap());
515        assert_eq!(
516            "./greptimedb_data/test/logs".to_string(),
517            options.logging.dir
518        );
519    }
520
521    #[test]
522    fn test_try_from_cmd() {
523        assert!(
524            (StartCommand {
525                metasrv_addrs: Some(vec!["127.0.0.1:3002".to_string()]),
526                ..Default::default()
527            })
528            .load_options(&GlobalOptions::default())
529            .is_err()
530        );
531
532        // Providing node_id but leave metasrv_addr absent is ok since metasrv_addr has default value
533        assert!(
534            (StartCommand {
535                node_id: Some(42),
536                ..Default::default()
537            })
538            .load_options(&GlobalOptions::default())
539            .is_ok()
540        );
541    }
542
543    #[test]
544    fn test_load_log_options_from_cli() {
545        let mut cmd = StartCommand::default();
546
547        let result = cmd.load_options(&GlobalOptions {
548            log_dir: Some("./greptimedb_data/test/logs".to_string()),
549            log_level: Some("debug".to_string()),
550
551            #[cfg(feature = "tokio-console")]
552            tokio_console_addr: None,
553        });
554        // Missing node_id.
555        assert_matches!(result, Err(crate::error::Error::MissingConfig { .. }));
556
557        cmd.node_id = Some(42);
558
559        let options = cmd
560            .load_options(&GlobalOptions {
561                log_dir: Some("./greptimedb_data/test/logs".to_string()),
562                log_level: Some("debug".to_string()),
563
564                #[cfg(feature = "tokio-console")]
565                tokio_console_addr: None,
566            })
567            .unwrap()
568            .component;
569
570        let logging_opt = options.logging;
571        assert_eq!("./greptimedb_data/test/logs", logging_opt.dir);
572        assert_eq!("debug", logging_opt.level.as_ref().unwrap());
573    }
574
575    #[test]
576    fn test_config_precedence_order() {
577        let mut file = create_named_temp_file();
578        let toml_str = r#"
579            enable_memory_catalog = false
580            node_id = 42
581            rpc_addr = "127.0.0.1:3001"
582            rpc_runtime_size = 8
583            rpc_hostname = "10.103.174.219"
584
585            [meta_client]
586            timeout = "3s"
587            connect_timeout = "5s"
588            tcp_nodelay = true
589
590            [wal]
591            provider = "raft_engine"
592            file_size = "1GB"
593            purge_threshold = "50GB"
594            purge_interval = "5m"
595            sync_write = false
596
597            [storage]
598            type = "File"
599            data_home = "./greptimedb_data/"
600
601            [logging]
602            level = "debug"
603            dir = "./greptimedb_data/test/logs"
604        "#;
605        write!(file, "{}", toml_str).unwrap();
606
607        let env_prefix = "DATANODE_UT";
608        temp_env::with_vars(
609            [
610                (
611                    // wal.purge_interval = 1m
612                    [
613                        env_prefix.to_string(),
614                        "wal".to_uppercase(),
615                        "purge_interval".to_uppercase(),
616                    ]
617                    .join(ENV_VAR_SEP),
618                    Some("1m"),
619                ),
620                (
621                    // wal.read_batch_size = 100
622                    [
623                        env_prefix.to_string(),
624                        "wal".to_uppercase(),
625                        "read_batch_size".to_uppercase(),
626                    ]
627                    .join(ENV_VAR_SEP),
628                    Some("100"),
629                ),
630                (
631                    // meta_client.metasrv_addrs = 127.0.0.1:3001,127.0.0.1:3002,127.0.0.1:3003
632                    [
633                        env_prefix.to_string(),
634                        "meta_client".to_uppercase(),
635                        "metasrv_addrs".to_uppercase(),
636                    ]
637                    .join(ENV_VAR_SEP),
638                    Some("127.0.0.1:3001,127.0.0.1:3002,127.0.0.1:3003"),
639                ),
640            ],
641            || {
642                let command = StartCommand {
643                    config_file: Some(file.path().to_str().unwrap().to_string()),
644                    wal_dir: Some("/other/wal/dir".to_string()),
645                    env_prefix: env_prefix.to_string(),
646                    ..Default::default()
647                };
648
649                let opts = command.load_options(&Default::default()).unwrap().component;
650
651                // Should be read from env, env > default values.
652                let DatanodeWalConfig::RaftEngine(raft_engine_config) = opts.wal else {
653                    unreachable!()
654                };
655                assert_eq!(raft_engine_config.read_batch_size, 100);
656                assert_eq!(
657                    opts.meta_client.unwrap().metasrv_addrs,
658                    vec![
659                        "127.0.0.1:3001".to_string(),
660                        "127.0.0.1:3002".to_string(),
661                        "127.0.0.1:3003".to_string()
662                    ]
663                );
664
665                // Should be read from config file, config file > env > default values.
666                assert_eq!(
667                    raft_engine_config.purge_interval,
668                    Duration::from_secs(60 * 5)
669                );
670
671                // Should be read from cli, cli > config file > env > default values.
672                assert_eq!(raft_engine_config.dir.unwrap(), "/other/wal/dir");
673
674                // Should be default value.
675                assert_eq!(
676                    opts.http.addr,
677                    DatanodeOptions::default().component.http.addr
678                );
679                assert_eq!(opts.grpc.server_addr, "10.103.174.219");
680            },
681        );
682    }
683
684    #[test]
685    fn test_parse_grpc_cli_aliases() {
686        let command = StartCommand::try_parse_from([
687            "datanode",
688            "--grpc-bind-addr",
689            "127.0.0.1:13001",
690            "--grpc-server-addr",
691            "10.0.0.1:13001",
692        ])
693        .unwrap();
694        assert_eq!(command.grpc_bind_addr.as_deref(), Some("127.0.0.1:13001"));
695        assert_eq!(command.grpc_server_addr.as_deref(), Some("10.0.0.1:13001"));
696
697        let command = StartCommand::try_parse_from([
698            "datanode",
699            "--rpc-bind-addr",
700            "127.0.0.1:23001",
701            "--rpc-server-addr",
702            "10.0.0.2:23001",
703        ])
704        .unwrap();
705        assert_eq!(command.grpc_bind_addr.as_deref(), Some("127.0.0.1:23001"));
706        assert_eq!(command.grpc_server_addr.as_deref(), Some("10.0.0.2:23001"));
707
708        let command = StartCommand::try_parse_from([
709            "datanode",
710            "--rpc-addr",
711            "127.0.0.1:33001",
712            "--rpc-hostname",
713            "10.0.0.3:33001",
714        ])
715        .unwrap();
716        assert_eq!(command.grpc_bind_addr.as_deref(), Some("127.0.0.1:33001"));
717        assert_eq!(command.grpc_server_addr.as_deref(), Some("10.0.0.3:33001"));
718    }
719
720    #[test]
721    fn test_help_uses_grpc_option_names() {
722        let mut cmd = StartCommand::command();
723        let mut help = Vec::new();
724        cmd.write_long_help(&mut help).unwrap();
725        let help = String::from_utf8(help).unwrap();
726
727        assert!(help.contains("--grpc-bind-addr"));
728        assert!(help.contains("--grpc-server-addr"));
729        assert!(!help.contains("--rpc-bind-addr"));
730        assert!(!help.contains("--rpc-server-addr"));
731        assert!(!help.contains("--rpc-addr"));
732        assert!(!help.contains("--rpc-hostname"));
733    }
734}