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]