This is an automated email from the ASF dual-hosted git repository.

jiacai2050 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-horaedb.git


The following commit(s) were added to refs/heads/main by this push:
     new 629bf39b feat(horaectl): impl horaectl in rs (#1481)
629bf39b is described below

commit 629bf39bea932aa048001ff374255c80892212c7
Author: 鲍金日 <[email protected]>
AuthorDate: Fri Mar 15 17:25:51 2024 +0800

    feat(horaectl): impl horaectl in rs (#1481)
    
    ## Rationale
    Implement horaectl using rust
    
    ## Detailed Changes
    - Support `cluster list`, `cluster diagnose`, `cluster schedule`
    
    ```
    $ target/debug/horaectl -h
    HoraeCTL is a command line tool for HoraeDB
    
    Usage: horaectl [OPTIONS] [COMMAND]
    
    Commands:
      cluster  Operations on cluster
      help     Print this message or the help of the given subcommand(s)
    
    Options:
      -m, --meta <META_ADDR>        Meta addr [env: HORAECTL_META_ADDR=] 
[default: 127.0.0.1:8080]
      -c, --cluster <CLUSTER_NAME>  Cluster name [env: HORAECTL_CLUSTER=] 
[default: defaultCluster]
      -i, --interactive             Enter interactive mode
      -h, --help                    Print help
    
    $ target/debug/horaectl cluster -h
    Operations on cluster
    
    Usage: horaectl cluster [OPTIONS] <COMMAND>
    
    Commands:
      list      List cluster
      diagnose  Diagnose cluster
      schedule  Schedule cluster
      help      Print this message or the help of the given subcommand(s)
    
    Options:
      -m, --meta <META_ADDR>        Meta addr [env: HORAECTL_META_ADDR=] 
[default: 127.0.0.1:8080]
      -c, --cluster <CLUSTER_NAME>  Cluster name [env: HORAECTL_CLUSTER=] 
[default: defaultCluster]
      -h, --help                    Print help
    
    
    ```
    
    ## Test Plan
    - Manual tests
    
    ---------
    
    Co-authored-by: jiacai2050 <[email protected]>
---
 Cargo.lock                         | 191 ++++++++++++++++++++++++++-----------
 Cargo.toml                         |   4 +-
 {src/tools => horaectl}/Cargo.toml |  26 ++---
 horaectl/src/cmd/cluster.rs        |  67 +++++++++++++
 horaectl/src/cmd/mod.rs            | 140 +++++++++++++++++++++++++++
 horaectl/src/main.rs               |  54 +++++++++++
 horaectl/src/operation/cluster.rs  | 147 ++++++++++++++++++++++++++++
 horaectl/src/operation/mod.rs      |  80 ++++++++++++++++
 horaectl/src/util/mod.rs           |  60 ++++++++++++
 src/tools/Cargo.toml               |   2 +-
 10 files changed, 698 insertions(+), 73 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index fca1cab2..d6c3ead8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1330,9 +1330,9 @@ checksum = 
"baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
 [[package]]
 name = "chrono"
-version = "0.4.31"
+version = "0.4.33"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
+checksum = "9f13690e35a5e4ace198e7beea2895d29f3a9cc55015fcebe6336bd2010af9eb"
 dependencies = [
  "android-tzdata",
  "iana-time-zone",
@@ -1340,7 +1340,7 @@ dependencies = [
  "num-traits",
  "serde",
  "wasm-bindgen",
- "windows-targets 0.48.1",
+ "windows-targets 0.52.0",
 ]
 
 [[package]]
@@ -1405,9 +1405,9 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "4.5.1"
+version = "4.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da"
+checksum = "b230ab84b0ffdf890d5a10abdbc8b83ae1c4918275daea1ab8801f71536b2651"
 dependencies = [
  "clap_builder",
  "clap_derive",
@@ -1415,9 +1415,9 @@ dependencies = [
 
 [[package]]
 name = "clap_builder"
-version = "4.5.1"
+version = "4.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb"
+checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
 dependencies = [
  "anstream",
  "anstyle",
@@ -1571,7 +1571,7 @@ version = "0.15.7"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8"
 dependencies = [
- "encode_unicode",
+ "encode_unicode 0.3.6",
  "lazy_static",
  "libc",
  "windows-sys 0.45.0",
@@ -1647,11 +1647,21 @@ version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b"
 
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
 [[package]]
 name = "core-foundation-sys"
-version = "0.8.3"
+version = "0.8.6"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
+checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
 
 [[package]]
 name = "cpp_demangle"
@@ -2411,6 +2421,12 @@ version = "0.3.6"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
 
+[[package]]
+name = "encode_unicode"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
+
 [[package]]
 name = "encoding_rs"
 version = "0.8.32"
@@ -3019,6 +3035,21 @@ dependencies = [
  "digest",
 ]
 
+[[package]]
+name = "horaectl"
+version = "2.0.0"
+dependencies = [
+ "anyhow",
+ "chrono",
+ "clap",
+ "lazy_static",
+ "prettytable",
+ "reqwest",
+ "serde",
+ "shell-words",
+ "tokio",
+]
+
 [[package]]
 name = "horaedb"
 version = "2.0.0"
@@ -3185,15 +3216,16 @@ dependencies = [
 
 [[package]]
 name = "hyper-rustls"
-version = "0.23.2"
+version = "0.24.2"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c"
+checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
 dependencies = [
+ "futures-util",
  "http",
  "hyper",
- "rustls 0.20.8",
+ "rustls 0.21.6",
  "tokio",
- "tokio-rustls 0.23.4",
+ "tokio-rustls 0.24.1",
 ]
 
 [[package]]
@@ -3551,9 +3583,9 @@ dependencies = [
 
 [[package]]
 name = "js-sys"
-version = "0.3.61"
+version = "0.3.67"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730"
+checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1"
 dependencies = [
  "wasm-bindgen",
 ]
@@ -5084,6 +5116,20 @@ dependencies = [
  "syn 2.0.48",
 ]
 
+[[package]]
+name = "prettytable"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "46480520d1b77c9a3482d39939fcf96831537a250ec62d4fd8fbdf8e0302e781"
+dependencies = [
+ "csv",
+ "encode_unicode 1.0.0",
+ "is-terminal",
+ "lazy_static",
+ "term",
+ "unicode-width",
+]
+
 [[package]]
 name = "proc-macro-crate"
 version = "0.1.5"
@@ -5815,9 +5861,9 @@ dependencies = [
 
 [[package]]
 name = "reqwest"
-version = "0.11.16"
+version = "0.11.24"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "27b71749df584b7f4cac2c426c127a7c785a5106cc98f7a8feb044115f0fa254"
+checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251"
 dependencies = [
  "base64 0.21.0",
  "bytes",
@@ -5836,13 +5882,15 @@ dependencies = [
  "once_cell",
  "percent-encoding",
  "pin-project-lite",
- "rustls 0.20.8",
+ "rustls 0.21.6",
  "rustls-pemfile 1.0.2",
  "serde",
  "serde_json",
  "serde_urlencoded",
+ "sync_wrapper",
+ "system-configuration",
  "tokio",
- "tokio-rustls 0.23.4",
+ "tokio-rustls 0.24.1",
  "tokio-util",
  "tower-service",
  "url",
@@ -5850,7 +5898,7 @@ dependencies = [
  "wasm-bindgen-futures",
  "wasm-streams",
  "web-sys",
- "webpki-roots 0.22.6",
+ "webpki-roots 0.25.4",
  "winreg",
 ]
 
@@ -6268,9 +6316,9 @@ checksum = 
"e6b44e8fc93a14e66336d230954dda83d18b4605ccace8fe09bc7514a71ad0bc"
 
 [[package]]
 name = "serde"
-version = "1.0.159"
+version = "1.0.196"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065"
+checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
 dependencies = [
  "serde_derive",
 ]
@@ -6286,9 +6334,9 @@ dependencies = [
 
 [[package]]
 name = "serde_derive"
-version = "1.0.159"
+version = "1.0.196"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585"
+checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -6432,6 +6480,12 @@ dependencies = [
  "lazy_static",
 ]
 
+[[package]]
+name = "shell-words"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
+
 [[package]]
 name = "shlex"
 version = "1.3.0"
@@ -6879,6 +6933,27 @@ dependencies = [
  "windows 0.52.0",
 ]
 
+[[package]]
+name = "system-configuration"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation",
+ "system-configuration-sys",
+]
+
+[[package]]
+name = "system-configuration-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
 [[package]]
 name = "system_catalog"
 version = "2.0.0"
@@ -7220,6 +7295,16 @@ dependencies = [
  "webpki",
 ]
 
+[[package]]
+name = "tokio-rustls"
+version = "0.24.1"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
+dependencies = [
+ "rustls 0.21.6",
+ "tokio",
+]
+
 [[package]]
 name = "tokio-rustls"
 version = "0.25.0"
@@ -7872,9 +7957,9 @@ checksum = 
"9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
 
 [[package]]
 name = "wasm-bindgen"
-version = "0.2.84"
+version = "0.2.90"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b"
+checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406"
 dependencies = [
  "cfg-if 1.0.0",
  "wasm-bindgen-macro",
@@ -7882,24 +7967,24 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-backend"
-version = "0.2.84"
+version = "0.2.90"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9"
+checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd"
 dependencies = [
  "bumpalo",
  "log",
  "once_cell",
  "proc-macro2",
  "quote",
- "syn 1.0.109",
+ "syn 2.0.48",
  "wasm-bindgen-shared",
 ]
 
 [[package]]
 name = "wasm-bindgen-futures"
-version = "0.4.34"
+version = "0.4.40"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454"
+checksum = "bde2032aeb86bdfaecc8b261eef3cba735cc426c1f3a3416d1e0791be95fc461"
 dependencies = [
  "cfg-if 1.0.0",
  "js-sys",
@@ -7909,9 +7994,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro"
-version = "0.2.84"
+version = "0.2.90"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5"
+checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999"
 dependencies = [
  "quote",
  "wasm-bindgen-macro-support",
@@ -7919,28 +8004,28 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro-support"
-version = "0.2.84"
+version = "0.2.90"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6"
+checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 1.0.109",
+ "syn 2.0.48",
  "wasm-bindgen-backend",
  "wasm-bindgen-shared",
 ]
 
 [[package]]
 name = "wasm-bindgen-shared"
-version = "0.2.84"
+version = "0.2.90"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
+checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b"
 
 [[package]]
 name = "wasm-streams"
-version = "0.2.3"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "6bbae3363c08332cadccd13b67db371814cd214c2524020932f0804b8cf7c078"
+checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129"
 dependencies = [
  "futures-util",
  "js-sys",
@@ -7951,9 +8036,9 @@ dependencies = [
 
 [[package]]
 name = "web-sys"
-version = "0.3.61"
+version = "0.3.67"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97"
+checksum = "58cd2333b6e0be7a39605f0e255892fd7418a682d8da8fe042fe25128794d2ed"
 dependencies = [
  "js-sys",
  "wasm-bindgen",
@@ -7971,21 +8056,18 @@ dependencies = [
 
 [[package]]
 name = "webpki-roots"
-version = "0.22.6"
+version = "0.23.1"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87"
+checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338"
 dependencies = [
- "webpki",
+ "rustls-webpki 0.100.2",
 ]
 
 [[package]]
 name = "webpki-roots"
-version = "0.23.1"
+version = "0.25.4"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338"
-dependencies = [
- "rustls-webpki 0.100.2",
-]
+checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
 
 [[package]]
 name = "which"
@@ -8323,11 +8405,12 @@ dependencies = [
 
 [[package]]
 name = "winreg"
-version = "0.10.1"
+version = "0.50.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
+checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
 dependencies = [
- "winapi",
+ "cfg-if 1.0.0",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
diff --git a/Cargo.toml b/Cargo.toml
index ec6fcd4e..77277e08 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -25,6 +25,7 @@ license = "Apache-2.0"
 resolver = "2"
 # In alphabetical order
 members = [
+    "horaectl",
     "integration_tests",
     "integration_tests/sdk/rust",
     "src/analytic_engine",
@@ -89,6 +90,7 @@ arrow = { version = "49.0.0", features = ["prettyprint"] }
 arrow_ipc = { version = "49.0.0" }
 arrow_ext = { path = "src/components/arrow_ext" }
 analytic_engine = { path = "src/analytic_engine" }
+anyhow = { version = "1.0" }
 arena = { path = "src/components/arena" }
 async-stream = "0.3.4"
 async-trait = "0.1.72"
@@ -101,7 +103,7 @@ catalog_impls = { path = "src/catalog_impls" }
 horaedbproto = { git = 
"https://github.com/apache/incubator-horaedb-proto.git";, rev = 
"19ece8f771fc0b3e8e734072cc3d8040de6c74cb" }
 codec = { path = "src/components/codec" }
 chrono = "0.4"
-clap = "4.5.1"
+clap = { version = "4.5.1", features = ["derive"] }
 clru = "0.6.1"
 cluster = { path = "src/cluster" }
 criterion = "0.5"
diff --git a/src/tools/Cargo.toml b/horaectl/Cargo.toml
similarity index 64%
copy from src/tools/Cargo.toml
copy to horaectl/Cargo.toml
index fb391066..580c0e79 100644
--- a/src/tools/Cargo.toml
+++ b/horaectl/Cargo.toml
@@ -16,7 +16,7 @@
 # under the License.
 
 [package]
-name = "tools"
+name = "horaectl"
 
 [package.license]
 workspace = true
@@ -24,24 +24,16 @@ workspace = true
 [package.version]
 workspace = true
 
-[package.authors]
-workspace = true
-
 [package.edition]
 workspace = true
 
 [dependencies]
-analytic_engine = { workspace = true }
-anyhow = { version = "1.0", features = ["backtrace"] }
-clap = { workspace = true, features = ["derive"] }
-common_types = { workspace = true }
-futures = { workspace = true }
-generic_error = { workspace = true }
-num_cpus = "1.15.0"
-object_store = { workspace = true }
-parquet = { workspace = true }
-parquet_ext = { workspace = true }
-runtime = { workspace = true }
-table_engine = { workspace = true }
-time_ext = { workspace = true }
+anyhow = { workspace = true, features = ["backtrace"] }
+chrono = { workspace = true }
+clap = { workspace = true, features = ["env", "derive"] }
+lazy_static = { workspace = true }
+prettytable = "0.10.0"
+reqwest = { workspace = true }
+serde = { workspace = true }
+shell-words = "1.1.0"
 tokio = { workspace = true }
diff --git a/horaectl/src/cmd/cluster.rs b/horaectl/src/cmd/cluster.rs
new file mode 100644
index 00000000..cd22c0cc
--- /dev/null
+++ b/horaectl/src/cmd/cluster.rs
@@ -0,0 +1,67 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use anyhow::Result;
+use clap::Subcommand;
+
+use crate::operation::cluster::ClusterOp;
+
+#[derive(Subcommand)]
+pub enum ClusterCommand {
+    /// List cluster
+    List,
+
+    /// Diagnose cluster
+    Diagnose,
+
+    /// Schedule cluster
+    Schedule {
+        #[clap(subcommand)]
+        cmd: Option<ScheduleCommand>,
+    },
+}
+
+#[derive(Subcommand)]
+pub enum ScheduleCommand {
+    /// Get the schedule status
+    Get,
+
+    /// Enable schedule
+    On,
+
+    /// Disable schedule
+    Off,
+}
+
+pub async fn run(cmd: ClusterCommand) -> Result<()> {
+    let op = ClusterOp::try_new()?;
+    match cmd {
+        ClusterCommand::List => op.list().await,
+        ClusterCommand::Diagnose => op.diagnose().await,
+        ClusterCommand::Schedule { cmd } => {
+            if let Some(cmd) = cmd {
+                match cmd {
+                    ScheduleCommand::Get => op.get_schedule_status().await,
+                    ScheduleCommand::On => 
op.update_schedule_status(true).await,
+                    ScheduleCommand::Off => 
op.update_schedule_status(false).await,
+                }
+            } else {
+                op.get_schedule_status().await
+            }
+        }
+    }
+}
diff --git a/horaectl/src/cmd/mod.rs b/horaectl/src/cmd/mod.rs
new file mode 100644
index 00000000..5906ef18
--- /dev/null
+++ b/horaectl/src/cmd/mod.rs
@@ -0,0 +1,140 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+mod cluster;
+use std::{io, io::Write};
+
+use anyhow::Result;
+use clap::{Args, Parser, Subcommand};
+
+use crate::{
+    cmd::cluster::ClusterCommand,
+    util::{CLUSTER_NAME, META_ADDR},
+};
+
+#[derive(Parser)]
+#[clap(name = "horaectl")]
+#[clap(about = "HoraeCTL is a command line tool for HoraeDB", version)]
+pub struct App {
+    #[clap(flatten)]
+    pub global_opts: GlobalOpts,
+
+    /// Enter interactive mode
+    #[clap(short, long, default_value_t = false)]
+    pub interactive: bool,
+
+    #[clap(subcommand)]
+    pub command: Option<SubCommand>,
+}
+
+#[derive(Debug, Args)]
+pub struct GlobalOpts {
+    /// Meta addr
+    #[clap(
+        short,
+        long = "meta",
+        global = true,
+        env = "HORAECTL_META_ADDR",
+        default_value = "127.0.0.1:8080"
+    )]
+    pub meta_addr: String,
+
+    /// Cluster name
+    #[clap(
+        short,
+        long = "cluster",
+        global = true,
+        env = "HORAECTL_CLUSTER",
+        default_value = "defaultCluster"
+    )]
+    pub cluster_name: String,
+}
+
+#[derive(Subcommand)]
+pub enum SubCommand {
+    /// Operations on cluster
+    #[clap(alias = "c")]
+    Cluster {
+        #[clap(subcommand)]
+        commands: ClusterCommand,
+    },
+}
+
+pub async fn run_command(cmd: SubCommand) -> Result<()> {
+    match cmd {
+        SubCommand::Cluster { commands } => cluster::run(commands).await,
+    }
+}
+
+pub async fn repl_loop() {
+    loop {
+        print_prompt(
+            META_ADDR.lock().unwrap().as_str(),
+            CLUSTER_NAME.lock().unwrap().as_str(),
+        );
+
+        let args = match read_args() {
+            Ok(args) => args,
+            Err(e) => {
+                println!("Read input failed, err:{}", e);
+                continue;
+            }
+        };
+
+        if let Some(cmd) = args.get(1) {
+            if ["quit", "exit", "q"].iter().any(|v| v == cmd) {
+                break;
+            }
+        }
+
+        match App::try_parse_from(args) {
+            Ok(horaectl) => {
+                if let Some(cmd) = horaectl.command {
+                    if let Err(e) = match cmd {
+                        SubCommand::Cluster { commands } => 
cluster::run(commands).await,
+                    } {
+                        println!("Run command failed, err:{e}");
+                    }
+                }
+            }
+            Err(e) => {
+                println!("Parse command failed, err:{e}");
+            }
+        }
+    }
+}
+
+fn read_args() -> Result<Vec<String>, String> {
+    io::stdout().flush().unwrap();
+    let mut input = String::new();
+    io::stdin()
+        .read_line(&mut input)
+        .map_err(|e| e.to_string())?;
+
+    let input = input.trim();
+    if input.is_empty() {
+        return Err("No arguments provided".into());
+    }
+
+    let mut args = vec!["horaectl".to_string()];
+    args.extend(shell_words::split(input).map_err(|e| e.to_string())?);
+    Ok(args)
+}
+
+fn print_prompt(address: &str, cluster: &str) {
+    print!("{}({}) > ", address, cluster);
+}
diff --git a/horaectl/src/main.rs b/horaectl/src/main.rs
new file mode 100644
index 00000000..24658ab6
--- /dev/null
+++ b/horaectl/src/main.rs
@@ -0,0 +1,54 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+mod cmd;
+mod operation;
+mod util;
+
+use clap::{CommandFactory, Parser};
+
+use crate::{
+    cmd::{repl_loop, run_command, App},
+    util::{CLUSTER_NAME, META_ADDR},
+};
+
+#[tokio::main]
+async fn main() {
+    let app = App::parse();
+    {
+        let mut meta_addr = META_ADDR.lock().unwrap();
+        *meta_addr = app.global_opts.meta_addr;
+    }
+    {
+        let mut cluster_name = CLUSTER_NAME.lock().unwrap();
+        *cluster_name = app.global_opts.cluster_name;
+    }
+
+    if app.interactive {
+        repl_loop().await;
+        return;
+    }
+
+    if let Some(cmd) = app.command {
+        if let Err(e) = run_command(cmd).await {
+            println!("Run command failed, err:{e}");
+            std::process::exit(1);
+        }
+    } else {
+        App::command().print_help().expect("print help failed");
+    }
+}
diff --git a/horaectl/src/operation/cluster.rs 
b/horaectl/src/operation/cluster.rs
new file mode 100644
index 00000000..709d44be
--- /dev/null
+++ b/horaectl/src/operation/cluster.rs
@@ -0,0 +1,147 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::time::Duration;
+
+use anyhow::Result;
+use prettytable::row;
+use reqwest::Client;
+
+use crate::{
+    operation::{
+        ClusterResponse, DiagnoseShardResponse, EnableScheduleRequest, 
EnableScheduleResponse,
+    },
+    util::{
+        format_time_milli, table_writer, API, CLUSTERS, 
CLUSTERS_DIAGNOSE_HEADER,
+        CLUSTERS_ENABLE_SCHEDULE_HEADER, CLUSTERS_LIST_HEADER, CLUSTER_NAME, 
DEBUG, HTTP,
+        META_ADDR,
+    },
+};
+
+fn list_url() -> String {
+    HTTP.to_string() + META_ADDR.lock().unwrap().as_str() + API + CLUSTERS
+}
+
+fn diagnose_url() -> String {
+    HTTP.to_string()
+        + META_ADDR.lock().unwrap().as_str()
+        + DEBUG
+        + "/diagnose"
+        + "/"
+        + CLUSTER_NAME.lock().unwrap().as_str()
+        + "/shards"
+}
+
+fn schedule_url() -> String {
+    HTTP.to_string()
+        + META_ADDR.lock().unwrap().as_str()
+        + DEBUG
+        + CLUSTERS
+        + "/"
+        + CLUSTER_NAME.lock().unwrap().as_str()
+        + "/enableSchedule"
+}
+
+pub struct ClusterOp {
+    http_client: Client,
+}
+
+impl ClusterOp {
+    pub fn try_new() -> Result<Self> {
+        let hc = Client::builder()
+            .timeout(Duration::from_secs(30))
+            .user_agent("horaectl")
+            .build()?;
+
+        Ok(Self { http_client: hc })
+    }
+
+    pub async fn list(&self) -> Result<()> {
+        let res = self.http_client.get(list_url()).send().await?;
+        let response: ClusterResponse = res.json().await?;
+
+        let mut table = table_writer(&CLUSTERS_LIST_HEADER);
+        for cluster in response.data {
+            table.add_row(row![
+                cluster.id,
+                cluster.name,
+                cluster.shard_total.to_string(),
+                cluster.topology_type,
+                cluster.procedure_executing_batch_size.to_string(),
+                format_time_milli(cluster.created_at),
+                format_time_milli(cluster.modified_at)
+            ]);
+        }
+        table.printstd();
+
+        Ok(())
+    }
+
+    pub async fn diagnose(&self) -> Result<()> {
+        let res = self.http_client.get(diagnose_url()).send().await?;
+        let response: DiagnoseShardResponse = res.json().await?;
+        let mut table = table_writer(&CLUSTERS_DIAGNOSE_HEADER);
+        table.add_row(row![response
+            .data
+            .unregistered_shards
+            .iter()
+            .map(|shard_id| shard_id.to_string())
+            .collect::<Vec<_>>()
+            .join(", ")]);
+        for (shard_id, data) in response.data.unready_shards {
+            table.add_row(row!["", shard_id, data.node_name, data.status]);
+        }
+        table.printstd();
+
+        Ok(())
+    }
+
+    pub async fn get_schedule_status(&self) -> Result<()> {
+        let res = self.http_client.get(schedule_url()).send().await?;
+        let response: EnableScheduleResponse = res.json().await?;
+        let mut table = table_writer(&CLUSTERS_ENABLE_SCHEDULE_HEADER);
+        let row = match response.data {
+            Some(data) => row![data],
+            None => row!["topology should in dynamic mode"],
+        };
+        table.add_row(row);
+        table.printstd();
+
+        Ok(())
+    }
+
+    pub async fn update_schedule_status(&self, enable: bool) -> Result<()> {
+        let request = EnableScheduleRequest { enable };
+
+        let res = self
+            .http_client
+            .put(schedule_url())
+            .json(&request)
+            .send()
+            .await?;
+        let response: EnableScheduleResponse = res.json().await?;
+        let mut table = table_writer(&CLUSTERS_ENABLE_SCHEDULE_HEADER);
+        let row = match response.data {
+            Some(data) => row![data],
+            None => row!["topology should in dynamic mode"],
+        };
+        table.add_row(row);
+        table.printstd();
+
+        Ok(())
+    }
+}
diff --git a/horaectl/src/operation/mod.rs b/horaectl/src/operation/mod.rs
new file mode 100644
index 00000000..f8723467
--- /dev/null
+++ b/horaectl/src/operation/mod.rs
@@ -0,0 +1,80 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+pub mod cluster;
+
+use std::collections::HashMap;
+
+use serde::{Deserialize, Serialize};
+#[derive(Deserialize, Debug)]
+pub struct Cluster {
+    #[serde(rename = "ID")]
+    id: u32,
+    #[serde(rename = "Name")]
+    name: String,
+    #[serde(rename = "ShardTotal")]
+    shard_total: u32,
+    #[serde(rename = "TopologyType")]
+    topology_type: String,
+    #[serde(rename = "ProcedureExecutingBatchSize")]
+    procedure_executing_batch_size: u32,
+    #[serde(rename = "CreatedAt")]
+    created_at: i64,
+    #[serde(rename = "ModifiedAt")]
+    modified_at: i64,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct ClusterResponse {
+    #[allow(unused)]
+    status: String,
+    data: Vec<Cluster>,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct DiagnoseShardStatus {
+    #[serde(rename = "nodeName")]
+    node_name: String,
+    status: String,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct DiagnoseShard {
+    #[serde(rename = "unregisteredShards")]
+    unregistered_shards: Vec<u32>,
+    #[serde(rename = "unreadyShards")]
+    unready_shards: HashMap<u32, DiagnoseShardStatus>,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct DiagnoseShardResponse {
+    #[allow(unused)]
+    status: String,
+    data: DiagnoseShard,
+}
+
+#[derive(Serialize)]
+pub struct EnableScheduleRequest {
+    enable: bool,
+}
+
+#[derive(Deserialize)]
+pub struct EnableScheduleResponse {
+    #[allow(unused)]
+    status: String,
+    data: Option<bool>,
+}
diff --git a/horaectl/src/util/mod.rs b/horaectl/src/util/mod.rs
new file mode 100644
index 00000000..d6a43113
--- /dev/null
+++ b/horaectl/src/util/mod.rs
@@ -0,0 +1,60 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::sync::Mutex;
+
+use chrono::{TimeZone, Utc};
+use lazy_static::lazy_static;
+use prettytable::{Cell, Row, Table};
+
+lazy_static! {
+    pub static ref META_ADDR: Mutex<String> = Mutex::new(String::new());
+    pub static ref CLUSTER_NAME: Mutex<String> = Mutex::new(String::new());
+}
+
+pub const HTTP: &str = "http://";;
+pub const API: &str = "/api/v1";
+pub const DEBUG: &str = "/debug";
+pub const CLUSTERS: &str = "/clusters";
+pub static CLUSTERS_LIST_HEADER: [&str; 7] = [
+    "ID",
+    "Name",
+    "ShardTotal",
+    "TopologyType",
+    "ProcedureExecutingBatchSize",
+    "CreatedAt",
+    "ModifiedAt",
+];
+pub static CLUSTERS_DIAGNOSE_HEADER: [&str; 4] = [
+    "unregistered_shards",
+    "unready_shards:shard_id",
+    "unready_shards:node_name",
+    "unready_shards:status",
+];
+pub static CLUSTERS_ENABLE_SCHEDULE_HEADER: [&str; 1] = ["enable_schedule"];
+
+pub fn table_writer(header: &[&str]) -> Table {
+    let mut table = Table::new();
+    let header_row = Row::from_iter(header.iter().map(|&entry| 
Cell::new(entry)));
+    table.add_row(header_row);
+    table
+}
+
+pub fn format_time_milli(milli: i64) -> String {
+    let datetime = Utc.timestamp_millis_opt(milli).single().unwrap();
+    datetime.format("%Y-%m-%d %H:%M:%S%.3f").to_string()
+}
diff --git a/src/tools/Cargo.toml b/src/tools/Cargo.toml
index fb391066..1a3231cb 100644
--- a/src/tools/Cargo.toml
+++ b/src/tools/Cargo.toml
@@ -32,7 +32,7 @@ workspace = true
 
 [dependencies]
 analytic_engine = { workspace = true }
-anyhow = { version = "1.0", features = ["backtrace"] }
+anyhow = { workspace = true, features = ["backtrace"] }
 clap = { workspace = true, features = ["derive"] }
 common_types = { workspace = true }
 futures = { workspace = true }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to