diff --git a/Cargo.lock b/Cargo.lock index b0eb4b6d..c75e4d64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,195 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "actix-codec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" -dependencies = [ - "bitflags", - "bytes", - "futures-core", - "futures-sink", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "actix-http" -version = "3.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7926860314cbe2fb5d1f13731e387ab43bd32bca224e82e6e2db85de0a3dba49" -dependencies = [ - "actix-codec", - "actix-rt", - "actix-service", - "actix-utils", - "base64", - "bitflags", - "brotli", - "bytes", - "bytestring", - "derive_more 2.1.1", - "encoding_rs", - "flate2", - "foldhash", - "futures-core", - "h2 0.3.27", - "http 0.2.12", - "httparse", - "httpdate", - "itoa", - "language-tags", - "local-channel", - "mime", - "percent-encoding", - "pin-project-lite", - "rand 0.9.2", - "sha1", - "smallvec", - "tokio", - "tokio-util", - "tracing", - "zstd", -] - -[[package]] -name = "actix-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" -dependencies = [ - "quote", - "syn 2.0.114", -] - -[[package]] -name = "actix-router" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" -dependencies = [ - "bytestring", - "cfg-if", - "http 0.2.12", - "regex", - "regex-lite", - "serde", - "tracing", -] - -[[package]] -name = "actix-rt" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63" -dependencies = [ - "futures-core", - "tokio", -] - -[[package]] -name = "actix-server" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "futures-util", - "mio", - "socket2 0.5.10", - "tokio", - "tracing", -] - -[[package]] -name = "actix-service" -version = "2.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" -dependencies = [ - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "actix-utils" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" -dependencies = [ - "local-waker", - "pin-project-lite", -] - -[[package]] -name = "actix-web" -version = "4.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1654a77ba142e37f049637a3e5685f864514af11fcbc51cb51eb6596afe5b8d6" -dependencies = [ - "actix-codec", - "actix-http", - "actix-macros", - "actix-router", - "actix-rt", - "actix-server", - "actix-service", - "actix-utils", - "actix-web-codegen", - "bytes", - "bytestring", - "cfg-if", - "cookie", - "derive_more 2.1.1", - "encoding_rs", - "foldhash", - "futures-core", - "futures-util", - "impl-more", - "itoa", - "language-tags", - "log", - "mime", - "once_cell", - "pin-project-lite", - "regex", - "regex-lite", - "serde", - "serde_json", - "serde_urlencoded", - "smallvec", - "socket2 0.6.2", - "time", - "tracing", - "url", -] - -[[package]] -name = "actix-web-codegen" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" -dependencies = [ - "actix-router", - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - [[package]] name = "ahash" version = "0.7.8" @@ -224,21 +35,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - [[package]] name = "allocator-api2" version = "0.2.21" @@ -418,6 +214,49 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "axum" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +dependencies = [ + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "sync_wrapper", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", +] + [[package]] name = "base64" version = "0.22.1" @@ -484,15 +323,6 @@ dependencies = [ "wyz", ] -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - [[package]] name = "borsh" version = "1.6.0" @@ -516,27 +346,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "brotli" -version = "8.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - [[package]] name = "bumpalo" version = "3.19.1" @@ -598,15 +407,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bytestring" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289" -dependencies = [ - "bytes", -] - [[package]] name = "bzip2-sys" version = "0.1.13+1.0.8" @@ -781,69 +581,22 @@ dependencies = [ "serde", "serde_ini", "snafu", + "tempfile", "validator", ] -[[package]] -name = "convert_case" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "cookie" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" -dependencies = [ - "percent-encoding", - "time", - "version_check", -] - -[[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.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - [[package]] name = "crc16" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "338089f42c427b86394a5ee60ff321da23a5c89c9d89514c829687b26359fcff" -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -868,16 +621,6 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - [[package]] name = "darling" version = "0.14.4" @@ -913,31 +656,13 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "deranged" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" -dependencies = [ - "powerfmt", -] - [[package]] name = "derive_more" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" dependencies = [ - "derive_more-impl 1.0.0", -] - -[[package]] -name = "derive_more" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl 2.1.1", + "derive_more-impl", ] [[package]] @@ -952,20 +677,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "derive_more-impl" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.114", - "unicode-xid", -] - [[package]] name = "derive_utils" version = "0.15.0" @@ -977,16 +688,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - [[package]] name = "displaydoc" version = "0.2.5" @@ -1016,15 +717,6 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - [[package]] name = "engine" version = "0.1.0" @@ -1126,14 +818,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" [[package]] -name = "flate2" -version = "1.1.8" +name = "fixedbitset" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" -dependencies = [ - "crc32fast", - "miniz_oxide", -] +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flume" @@ -1159,21 +847,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1393,16 +1066,6 @@ dependencies = [ "slab", ] -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - [[package]] name = "getrandom" version = "0.2.17" @@ -1434,25 +1097,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" -[[package]] -name = "h2" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "h2" version = "0.4.13" @@ -1464,7 +1108,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http 1.4.0", + "http", "indexmap", "slab", "tokio", @@ -1519,17 +1163,6 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - [[package]] name = "http" version = "1.4.0" @@ -1547,7 +1180,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.4.0", + "http", ] [[package]] @@ -1558,7 +1191,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.4.0", + "http", "http-body", "pin-project-lite", ] @@ -1585,10 +1218,11 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "h2 0.4.13", - "http 1.4.0", + "h2", + "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "pin-utils", @@ -1598,34 +1232,15 @@ dependencies = [ ] [[package]] -name = "hyper-rustls" -version = "0.27.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" -dependencies = [ - "http 1.4.0", - "hyper", - "hyper-util", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", -] - -[[package]] -name = "hyper-tls" -version = "0.6.0" +name = "hyper-timeout" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" dependencies = [ - "bytes", - "http-body-util", "hyper", "hyper-util", - "native-tls", + "pin-project-lite", "tokio", - "tokio-native-tls", "tower-service", ] @@ -1635,24 +1250,19 @@ version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" dependencies = [ - "base64", "bytes", "futures-channel", "futures-core", "futures-util", - "http 1.4.0", + "http", "http-body", "hyper", - "ipnet", "libc", - "percent-encoding", "pin-project-lite", - "socket2 0.6.2", - "system-configuration", + "socket2", "tokio", "tower-service", "tracing", - "windows-registry", ] [[package]] @@ -1803,12 +1413,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd62e6b5e86ea8eeeb8db1de02880a6abc01a397b2ebb64b5d74ac255318f5cb" -[[package]] -name = "impl-more" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" - [[package]] name = "indexmap" version = "2.13.0" @@ -1819,22 +1423,6 @@ dependencies = [ "hashbrown 0.16.1", ] -[[package]] -name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "iri-string" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -1927,12 +1515,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "language-tags" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" - [[package]] name = "lazy_static" version = "1.5.0" @@ -1978,23 +1560,6 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" -[[package]] -name = "local-channel" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" -dependencies = [ - "futures-core", - "futures-sink", - "local-waker", -] - -[[package]] -name = "local-waker" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" - [[package]] name = "lock_api" version = "0.4.14" @@ -2090,6 +1655,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "memchr" version = "2.7.6" @@ -2117,16 +1688,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - [[package]] name = "mio" version = "1.1.1" @@ -2134,7 +1695,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", - "log", "wasi", "windows-sys 0.61.2", ] @@ -2166,6 +1726,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + [[package]] name = "murmur3" version = "0.1.0" @@ -2190,23 +1756,6 @@ dependencies = [ "getrandom 0.2.17", ] -[[package]] -name = "native-tls" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - [[package]] name = "net" version = "0.1.0" @@ -2256,12 +1805,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "num-conv" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" - [[package]] name = "num-traits" version = "0.2.19" @@ -2303,7 +1846,7 @@ dependencies = [ "byte-unit", "chrono", "clap", - "derive_more 1.0.0", + "derive_more", "futures", "maplit", "openraft-macros", @@ -2329,50 +1872,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "openssl" -version = "0.10.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" -dependencies = [ - "bitflags", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - -[[package]] -name = "openssl-sys" -version = "0.9.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "ordered_hash_map" version = "0.4.0" @@ -2429,6 +1928,17 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap", +] + [[package]] name = "pin-project" version = "1.1.10" @@ -2491,12 +2001,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - [[package]] name = "ppv-lite86" version = "0.2.21" @@ -2506,6 +2010,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.114", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -2567,6 +2081,59 @@ dependencies = [ "unarray", ] +[[package]] +name = "prost" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" +dependencies = [ + "heck", + "itertools 0.14.0", + "log", + "multimap", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "pulldown-cmark", + "pulldown-cmark-to-cmark", + "regex", + "syn 2.0.114", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "prost-types" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" +dependencies = [ + "prost", +] + [[package]] name = "ptr_meta" version = "0.1.4" @@ -2587,6 +2154,26 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "pulldown-cmark" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" +dependencies = [ + "bitflags", + "memchr", + "unicase", +] + +[[package]] +name = "pulldown-cmark-to-cmark" +version = "22.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50793def1b900256624a709439404384204a5dc3a6ec580281bfaac35e882e90" +dependencies = [ + "pulldown-cmark", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -2618,22 +2205,26 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" name = "raft" version = "0.1.0" dependencies = [ - "actix-web", "anyhow", "async-trait", + "bincode", "conf", "engine", "log", "openraft", "parking_lot", "proptest", - "reqwest", + "prost", "rust-rocksdb", "serde", "serde_json", "storage", "tempfile", "tokio", + "tokio-stream", + "tonic", + "tonic-prost", + "tonic-prost-build", "tracing", ] @@ -2767,67 +2358,21 @@ dependencies = [ ] [[package]] -name = "regex-lite" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da" - -[[package]] -name = "regex-syntax" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" - -[[package]] -name = "rend" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" -dependencies = [ - "bytecheck", -] - -[[package]] -name = "reqwest" -version = "0.12.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" -dependencies = [ - "base64", - "bytes", - "encoding_rs", - "futures-core", - "h2 0.4.13", - "http 1.4.0", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-tls", - "hyper-util", - "js-sys", - "log", - "mime", - "native-tls", - "percent-encoding", - "pin-project-lite", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-native-tls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] name = "resp" version = "0.1.0" dependencies = [ @@ -2842,20 +2387,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "194d8e591e405d1eecf28819740abed6d719d1a2db87fc0bcdedee9a26d55560" -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.17", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - [[package]] name = "rkyv" version = "0.7.46" @@ -2952,15 +2483,6 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - [[package]] name = "rustix" version = "1.1.3" @@ -2974,39 +2496,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "rustls" -version = "0.23.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" -dependencies = [ - "once_cell", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pki-types" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" -dependencies = [ - "zeroize", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - [[package]] name = "rustversion" version = "1.0.22" @@ -3025,21 +2514,6 @@ dependencies = [ "wait-timeout", ] -[[package]] -name = "ryu" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" - -[[package]] -name = "schannel" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "schemars" version = "1.2.0" @@ -3064,29 +2538,6 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "semver" version = "1.0.27" @@ -3156,23 +2607,10 @@ dependencies = [ "serde_core", ] -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - [[package]] name = "server" version = "0.1.0" dependencies = [ - "actix-web", "clap", "conf", "env_logger", @@ -3182,17 +2620,8 @@ dependencies = [ "runtime", "storage", "tokio", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", + "tonic", + "tonic-reflection", ] [[package]] @@ -3220,12 +2649,6 @@ dependencies = [ "libc", ] -[[package]] -name = "simd-adler32" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" - [[package]] name = "simdutf8" version = "0.1.5" @@ -3265,16 +2688,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "socket2" version = "0.6.2" @@ -3342,12 +2755,6 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - [[package]] name = "syn" version = "1.0.109" @@ -3375,9 +2782,6 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] [[package]] name = "synstructure" @@ -3390,27 +2794,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "system-configuration" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" -dependencies = [ - "bitflags", - "core-foundation", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "tagptr" version = "0.2.0" @@ -3485,37 +2868,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "time" -version = "0.3.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde_core", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" - -[[package]] -name = "time-macros" -version = "0.2.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78cc610bac2dcee56805c99642447d4c5dbde4d01f752ffea0199aee1f601dc4" -dependencies = [ - "num-conv", - "time-core", -] - [[package]] name = "tinystr" version = "0.8.2" @@ -3553,7 +2905,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.2", + "socket2", "tokio-macros", "windows-sys 0.61.2", ] @@ -3569,26 +2921,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - [[package]] name = "tokio-stream" version = "0.1.18" @@ -3676,36 +3008,104 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" [[package]] -name = "tower" -version = "0.5.3" +name = "tonic" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +checksum = "a286e33f82f8a1ee2df63f4fa35c0becf4a85a0cb03091a15fd7bf0b402dc94a" dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", + "async-trait", + "axum", + "base64", + "bytes", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "socket2", "sync_wrapper", "tokio", + "tokio-stream", + "tower", "tower-layer", "tower-service", + "tracing", ] [[package]] -name = "tower-http" -version = "0.6.8" +name = "tonic-build" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "27aac809edf60b741e2d7db6367214d078856b8a5bff0087e94ff330fb97b6fc" +dependencies = [ + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "tonic-prost" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6c55a2d6a14174563de34409c9f92ff981d006f56da9c6ecd40d9d4a31500b0" dependencies = [ - "bitflags", "bytes", + "prost", + "tonic", +] + +[[package]] +name = "tonic-prost-build" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4556786613791cfef4ed134aa670b61a85cfcacf71543ef33e8d801abae988f" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "prost-types", + "quote", + "syn 2.0.114", + "tempfile", + "tonic-build", +] + +[[package]] +name = "tonic-reflection" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758112f988818866f38face806ebf8c8961ad2c087e2ba89ad30010ba5fd80c1" +dependencies = [ + "prost", + "prost-types", + "tokio", + "tokio-stream", + "tonic", + "tonic-prost", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", "futures-util", - "http 1.4.0", - "http-body", - "iri-string", + "indexmap", "pin-project-lite", - "tower", + "slab", + "sync_wrapper", + "tokio", + "tokio-util", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -3726,7 +3126,6 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ - "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -3803,18 +3202,18 @@ dependencies = [ "rand 0.9.2", ] -[[package]] -name = "typenum" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - [[package]] name = "unarray" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + [[package]] name = "unicode-bidi" version = "0.3.18" @@ -3836,24 +3235,12 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - [[package]] name = "url" version = "2.5.8" @@ -4017,20 +3404,6 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" -dependencies = [ - "cfg-if", - "futures-util", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "wasm-bindgen-macro" version = "0.2.108" @@ -4063,16 +3436,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "web-sys" -version = "0.3.85" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "windows-core" version = "0.62.2" @@ -4114,17 +3477,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-registry" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" -dependencies = [ - "windows-link", - "windows-result", - "windows-strings", -] - [[package]] name = "windows-result" version = "0.4.1" @@ -4143,15 +3495,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.59.0" @@ -4402,12 +3745,6 @@ dependencies = [ "synstructure", ] -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" - [[package]] name = "zerotrie" version = "0.2.3" diff --git a/scripts/start_node_cluster.sh b/scripts/start_node_cluster.sh new file mode 100755 index 00000000..be4a3f98 --- /dev/null +++ b/scripts/start_node_cluster.sh @@ -0,0 +1,510 @@ +#!/bin/bash +# Copyright (c) 2024-present, arana-db Community. All rights reserved. +# +# 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. +# +# Script to start a multi-node Raft cluster for testing Kiwi. +# Each node runs in its own directory with separate storage. +# Logs are written to files, viewable with `tail -f`. +# Uses gRPC for cluster management (`grpcurl` required). +# +# Usage: ./start_node_cluster.sh [NODE_COUNT] +# NODE_COUNT: Number of nodes to start (default: 3, range: 1-9) + +# Exit on error, undefined variables, and pipe failures +set -euo pipefail + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Base directory for the cluster +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +CLUSTER_BASE_DIR="$PROJECT_ROOT/cluster_test" +BINARY_PATH="$PROJECT_ROOT/target/debug/kiwi" + +# Default node count +NODE_COUNT=3 + +# Base ports +RAFT_PORT_BASE=8081 +RESP_PORT_BASE=7379 + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + -h|--help) + echo "Usage: $0 [NODE_COUNT]" + echo "" + echo "Arguments:" + echo " NODE_COUNT Number of nodes to start (default: 3, range: 1-9)" + echo "" + echo "Options:" + echo " -h, --help Show this help message" + echo "" + echo "Examples:" + echo " $0 # Start 3 nodes (default)" + echo " $0 5 # Start 5 nodes" + echo " $0 1 # Start 1 node (single node mode)" + exit 0 + ;; + *) + if [[ $1 =~ ^[0-9]+$ ]] && [ $1 -ge 1 ] && [ $1 -le 9 ]; then + NODE_COUNT=$1 + else + echo -e "${RED}Error: Invalid node count '$1'${NC}" + echo -e "${YELLOW}Node count must be between 1 and 9${NC}" + echo "Use -h or --help for usage information" + exit 1 + fi + shift + ;; + esac +done + +# Check if grpcurl is installed +GRPCURL="" +find_grpcurl() { + # Try to find grpcurl in various locations + if command -v grpcurl &> /dev/null; then + GRPCURL="grpcurl" + elif [ -f "$HOME/go/bin/grpcurl" ]; then + GRPCURL="$HOME/go/bin/grpcurl" + elif [ -f "/usr/local/bin/grpcurl" ]; then + GRPCURL="/usr/local/bin/grpcurl" + else + echo -e "${RED}Error: grpcurl is not installed${NC}" + echo -e "${YELLOW}Install grpcurl:${NC}" + echo " go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest" + echo " or: brew install grpcurl (macOS)" + echo -e "\n${YELLOW}Then make sure \$HOME/go/bin is in your PATH:${NC}" + echo " export PATH=\"\$PATH:\$HOME/go/bin\"" + exit 1 + fi +} + +# Check if binary exists +if [ ! -f "$BINARY_PATH" ]; then + echo -e "${RED}Error: Binary not found at $BINARY_PATH${NC}" + echo -e "${YELLOW}Building the project...${NC}" + if ! (cd "$PROJECT_ROOT" && cargo build --bin kiwi); then + echo -e "${RED}Failed to build project${NC}" + exit 1 + fi +fi + +# Find grpcurl +find_grpcurl + +# Dynamically generate node configurations +# Format: "node_id:raft_port:resp_port" +NODES=() +for ((i=1; i<=NODE_COUNT; i++)); do + raft_port=$((RAFT_PORT_BASE + i - 1)) + resp_port=$((RESP_PORT_BASE + i - 1)) + NODES+=("$i:$raft_port:$resp_port") +done + +# PIDs file for tracking only the PIDs started by this script +PIDS_FILE="$CLUSTER_BASE_DIR/.pids" + +# Function to cleanup on exit +cleanup() { + echo -e "\n${YELLOW}Stopping cluster...${NC}" + + # Kill only the PIDs tracked by this script + if [ -f "$PIDS_FILE" ]; then + while IFS= read -r pid; do + if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then + kill "$pid" 2>/dev/null || true + fi + done < "$PIDS_FILE" + fi + + sleep 1 + + # Force kill only the tracked PIDs if any remain + if [ -f "$PIDS_FILE" ]; then + while IFS= read -r pid; do + if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then + kill -9 "$pid" 2>/dev/null || true + fi + done < "$PIDS_FILE" + fi + + # Remove only the cluster directory (not $PROJECT_ROOT/db) + rm -rf "$CLUSTER_BASE_DIR" + + echo -e "${GREEN}Cleanup complete${NC}" +} + +# Register cleanup on Ctrl+C +trap cleanup INT TERM + +# Function to create node directory and config +create_node() { + local node_id=$1 + local raft_port=$2 + local resp_port=$3 + local node_dir="$CLUSTER_BASE_DIR/node$node_id" + + echo -e "${BLUE}Creating node $node_id...${NC}" + + # Create node directory + mkdir -p "$node_dir" + + # Create config file + cat > "$node_dir/config.toml" << EOF +# Node $node_id configuration +binding = 127.0.0.1 +port = $resp_port +network_threads = 1 +storage_threads = 2 + +raft-node-id = $node_id +raft-addr = 127.0.0.1:$raft_port +raft-resp-addr = 127.0.0.1:$resp_port +raft-data-dir = ./raft_data +EOF + + # Copy binary to node directory + cp "$BINARY_PATH" "$node_dir/kiwi" + chmod +x "$node_dir/kiwi" + + echo -e "${GREEN}Node $node_id created at $node_dir${NC}" +} + +# Function to start a node +start_node() { + local node_id=$1 + local node_dir="$CLUSTER_BASE_DIR/node$node_id" + local log_file="$node_dir/kiwi.log" + + echo -e "${BLUE}Starting node $node_id...${NC}" + + # Start node from its own directory (so ./db is per-node) + cd "$node_dir" + RUST_LOG=info ./kiwi --config config.toml > "$log_file" 2>&1 & + local pid=$! + cd - > /dev/null + + # Save PID to file for cleanup tracking + echo "$pid" >> "$PIDS_FILE" + echo "$pid" > "$node_dir/kiwi.pid" + + echo -e "${GREEN}Node $node_id started (PID: $pid)${NC}" + echo -e "${YELLOW} Log: $log_file${NC}" +} + +# Function to build the cluster initialization JSON +build_cluster_json() { + local json="{\"nodes\": [" + local first=true + + for node_info in "${NODES[@]}"; do + IFS=':' read -r node_id raft_port resp_port <<< "$node_info" + if [ "$first" = true ]; then + first=false + else + json+=", " + fi + json+="{\"node_id\": $node_id, \"raft_addr\": \"127.0.0.1:$raft_port\", \"resp_addr\": \"127.0.0.1:$resp_port\"}" + done + + json+="]}" + echo "$json" +} + +# Function to build membership JSON +build_membership_json() { + local json="{\"members\": [" + local first=true + + for node_info in "${NODES[@]}"; do + IFS=':' read -r node_id raft_port resp_port <<< "$node_info" + if [ "$first" = true ]; then + first=false + else + json+=", " + fi + json+="{\"node_id\": $node_id, \"raft_addr\": \"127.0.0.1:$raft_port\", \"resp_addr\": \"127.0.0.1:$resp_port\"}" + done + + json+="], \"retain\": false}" + echo "$json" +} + +# Function to initialize the cluster using gRPC +init_cluster() { + echo -e "\n${YELLOW}Waiting for nodes to initialize...${NC}" + sleep 3 + + echo -e "${YELLOW}Initializing Raft cluster via gRPC...${NC}" + + # Get the first node's Raft address + local first_node_raft="127.0.0.1:$RAFT_PORT_BASE" + + # Build cluster JSON dynamically + local cluster_json=$(build_cluster_json) + + # Initialize the cluster through node 1 using gRPC + echo -e "${BLUE}Initializing cluster via node 1...${NC}" + local init_result=$(echo "$cluster_json" | $GRPCURL -plaintext -d @ "$first_node_raft" raft_proto.RaftAdminService/Initialize 2>/dev/null || echo "error") + + echo " Init result: $init_result" + + # If init failed, try adding nodes step by step + if [[ "$init_result" == *"error"* ]] || [[ "$init_result" == *"failed"* ]]; then + echo -e "${YELLOW}Direct init may have failed, trying step-by-step setup...${NC}" + + # Add nodes 2..N as learners + for node_info in "${NODES[@]}"; do + IFS=':' read -r node_id raft_port resp_port <<< "$node_info" + if [ $node_id -ne 1 ]; then + echo -e "${BLUE}Adding node $node_id as learner...${NC}" + local learner_result=$($GRPCURL -plaintext -d '{ + "node_id": '$node_id', + "node": {"raft_addr": "127.0.0.1:'$raft_port'", "resp_addr": "127.0.0.1:'$resp_port'"} + }' "$first_node_raft" raft_proto.RaftAdminService/AddLearner 2>/dev/null || echo "error") + echo " Add learner $node_id result: $learner_result" + fi + done + + # Change membership to include all nodes + echo -e "${BLUE}Changing cluster membership...${NC}" + local membership_json=$(build_membership_json) + local membership_result=$(echo "$membership_json" | $GRPCURL -plaintext -d @ "$first_node_raft" raft_proto.RaftAdminService/ChangeMembership 2>/dev/null || echo "error") + echo " Membership result: $membership_result" + fi + + sleep 2 + + echo -e "${GREEN}Cluster initialization complete${NC}" +} + +# Function to show cluster status using gRPC +show_status() { + echo -e "\n${YELLOW}Cluster Status:${NC}" + + for node_info in "${NODES[@]}"; do + IFS=':' read -r node_id raft_port resp_port <<< "$node_info" + + echo -e "\n${BLUE}Node $node_id:${NC}" + + # Check if process is running + local node_dir="$CLUSTER_BASE_DIR/node$node_id" + if [ -f "$node_dir/kiwi.pid" ]; then + local pid=$(cat "$node_dir/kiwi.pid") + if kill -0 "$pid" 2>/dev/null; then + echo -e " ${GREEN}✓ Running (PID: $pid)${NC}" + else + echo -e " ${RED}✗ Not running${NC}" + fi + fi + + # Check if port is listening + if nc -z 127.0.0.1 "$raft_port" 2>/dev/null; then + echo -e " ${GREEN}✓ gRPC listening on port $raft_port${NC}" + else + echo -e " ${RED}✗ gRPC not responding on port $raft_port${NC}" + fi + + if nc -z 127.0.0.1 "$resp_port" 2>/dev/null; then + echo -e " ${GREEN}✓ RESP listening on port $resp_port${NC}" + else + echo -e " ${RED}✗ RESP not responding on port $resp_port${NC}" + fi + + # Check leader via gRPC + local leader=$($GRPCURL -plaintext "127.0.0.1:$raft_port" raft_proto.RaftMetricsService/Leader 2>/dev/null || echo "error") + if [ "$leader" != "error" ] && [ "$leader" != "null" ]; then + echo -e " Leader info: $leader" + fi + + # Check metrics via gRPC + local metrics=$($GRPCURL -plaintext "127.0.0.1:$raft_port" raft_proto.RaftMetricsService/Metrics 2>/dev/null || echo "error") + if [ "$metrics" != "error" ] && [ "$metrics" != "null" ]; then + echo -e " Metrics: $metrics" + fi + done +} + +# Function to run tests using gRPC +run_tests() { + echo -e "\n${YELLOW}Running basic Raft tests...${NC}" + + # Find the leader + local leader_raft="" + local leader_id="" + + for node_info in "${NODES[@]}"; do + IFS=':' read -r node_id raft_port resp_port <<< "$node_info" + local leader_check=$($GRPCURL -plaintext "127.0.0.1:$raft_port" raft_proto.RaftMetricsService/Metrics 2>/dev/null || echo "error") + if [ "$leader_check" != "error" ] && [[ "$leader_check" == *"isLeader"* ]]; then + # Check if this node is the leader (isLeader: true) + if [[ "$leader_check" == *"isLeader\": true"* ]]; then + leader_raft="127.0.0.1:$raft_port" + leader_id="$node_id" + echo -e "${GREEN}Leader is node $node_id (port $raft_port)${NC}" + break + fi + fi + done + + if [ -z "$leader_raft" ]; then + echo -e "${RED}No leader found, skipping tests${NC}" + return + fi + + # Test write operation via gRPC + echo -e "${BLUE}Testing write operation via leader ($leader_raft)...${NC}" + local write_response=$($GRPCURL -plaintext -d '{ + "binlog": { + "db_id": 0, + "slot_idx": 0, + "entries": [ + { + "cf_idx": 0, + "op_type": "Put", + "key": "dGVzdF9r", + "value": "dGVzdF92" + } + ] + } + }' "$leader_raft" raft_proto.RaftClientService/Write 2>/dev/null || echo "error") + + if [ "$write_response" != "error" ]; then + echo -e "${GREEN}Write response: $write_response${NC}" + else + echo -e "${RED}Write failed${NC}" + fi + + # Test read operation via gRPC + echo -e "${BLUE}Testing read operation...${NC}" + local read_response=$($GRPCURL -plaintext -d '{ + "key": "dGVzdF9r" + }' "$leader_raft" raft_proto.RaftClientService/Read 2>/dev/null || echo "error") + + if [ "$read_response" != "error" ]; then + echo -e "${GREEN}Read response: $read_response${NC}" + else + echo -e "${RED}Read failed${NC}" + fi +} + +# Main execution +main() { + set -euo pipefail + + echo -e "${GREEN}========================================${NC}" + echo -e "${GREEN} Kiwi ${NODE_COUNT}-Node Raft Cluster${NC}" + echo -e "${GREEN} (using gRPC for cluster management)${NC}" + echo -e "${GREEN}========================================${NC}" + + # Clean up any existing cluster (only within CLUSTER_BASE_DIR) + if [ -d "$CLUSTER_BASE_DIR" ]; then + echo -e "${YELLOW}Removing existing cluster directory...${NC}" + + # Kill only tracked PIDs from previous run + if [ -f "$CLUSTER_BASE_DIR/.pids" ]; then + while IFS= read -r pid; do + if [ -n "$pid" ]; then + kill "$pid" 2>/dev/null || true + fi + done < "$CLUSTER_BASE_DIR/.pids" + sleep 1 + while IFS= read -r pid; do + if [ -n "$pid" ]; then + kill -9 "$pid" 2>/dev/null || true + fi + done < "$CLUSTER_BASE_DIR/.pids" + fi + + rm -rf "$CLUSTER_BASE_DIR" + fi + + sleep 1 + + # Create cluster base directory + mkdir -p "$CLUSTER_BASE_DIR" + # Initialize PIDS_FILE (truncate if exists) + : > "$PIDS_FILE" + + # Create all nodes + for node_info in "${NODES[@]}"; do + IFS=':' read -r node_id raft_port resp_port <<< "$node_info" + create_node "$node_id" "$raft_port" "$resp_port" + done + + # Start all nodes + echo -e "\n${YELLOW}Starting all nodes...${NC}" + for node_info in "${NODES[@]}"; do + IFS=':' read -r node_id raft_port resp_port <<< "$node_info" + start_node "$node_id" + done + + # Wait for nodes to be ready + sleep 3 + + # Initialize the cluster + init_cluster + + # Show cluster status + show_status + + # Run tests + run_tests + + echo -e "\n${GREEN}========================================${NC}" + echo -e "${GREEN}Cluster is running!${NC}" + echo -e "${GREEN}========================================${NC}" + echo -e "${YELLOW}Nodes:${NC}" + for node_info in "${NODES[@]}"; do + IFS=':' read -r node_id raft_port resp_port <<< "$node_info" + local node_dir="$CLUSTER_BASE_DIR/node$node_id" + echo -e " ${BLUE}Node $node_id:${NC}" + echo -e " gRPC: 127.0.0.1:$raft_port" + echo -e " RESP: 127.0.0.1:$resp_port" + echo -e " Log: $node_dir/kiwi.log" + echo -e " Data: $node_dir" + done + + echo -e "\n${YELLOW}View logs with:${NC}" + echo -e " ${BLUE}tail -f $CLUSTER_BASE_DIR/node1/kiwi.log${NC}" + for ((i=2; i<=NODE_COUNT; i++)); do + echo -e " ${BLUE}tail -f $CLUSTER_BASE_DIR/node${i}/kiwi.log${NC}" + done + echo -e "\n${YELLOW}Or view all at once:${NC}" + echo -e " ${BLUE}tail -f $CLUSTER_BASE_DIR/node*/kiwi.log${NC}" + + echo -e "\n${YELLOW}gRPC service examples:${NC}" + echo -e " ${BLUE}$GRPCURL -plaintext 127.0.0.1:$RAFT_PORT_BASE raft_proto.RaftMetricsService/Leader${NC}" + echo -e " ${BLUE}$GRPCURL -plaintext 127.0.0.1:$RAFT_PORT_BASE raft_proto.RaftMetricsService/Metrics${NC}" + echo -e " ${BLUE}$GRPCURL -plaintext 127.0.0.1:$RAFT_PORT_BASE raft_proto.RaftMetricsService/Members${NC}" + echo -e " ${BLUE}$GRPCURL -plaintext 127.0.0.1:$RAFT_PORT_BASE list${NC}" + + echo -e "\n${YELLOW}Press Ctrl+C to stop the cluster${NC}" + + # Wait indefinitely for user to press Ctrl+C + wait +} + +# Run main +main diff --git a/src/conf/Cargo.toml b/src/conf/Cargo.toml index 6f96c0a4..12a86dce 100644 --- a/src/conf/Cargo.toml +++ b/src/conf/Cargo.toml @@ -12,4 +12,7 @@ serde_ini = "0.2.0" validator = { version = "0.16", features = ["derive"] } snafu = "0.8.5" rocksdb.workspace = true -openraft.workspace = true \ No newline at end of file +openraft.workspace = true + +[dev-dependencies] +tempfile = "3" \ No newline at end of file diff --git a/src/conf/src/config.rs b/src/conf/src/config.rs index 49e0c24a..c85baa7f 100644 --- a/src/conf/src/config.rs +++ b/src/conf/src/config.rs @@ -64,6 +64,9 @@ pub struct RaftClusterConfig { pub raft_addr: String, pub resp_addr: String, pub data_dir: String, + pub heartbeat_interval_ms: Option, + pub election_timeout_min_ms: Option, + pub election_timeout_max_ms: Option, /// 是否使用内存日志存储,默认 false(使用 RocksDB 持久化存储) pub use_memory_log_store: bool, } @@ -121,6 +124,9 @@ impl Config { let mut raft_addr: Option = None; let mut raft_resp_addr: Option = None; let mut raft_data_dir: Option = None; + let mut raft_heartbeat_interval: Option = None; + let mut raft_election_timeout_min: Option = None; + let mut raft_election_timeout_max: Option = None; let mut raft_use_memory_log_store: bool = false; // Parse each configuration value @@ -316,7 +322,7 @@ impl Config { )), })?; } - "raft-node-id" => { + "raft-node-id" | "cluster-node-id" => { raft_node_id = Some(value.parse().map_err(|e| Error::InvalidConfig { source: serde_ini::de::Error::Custom(format!( "Invalid raft-node-id: {}", @@ -324,15 +330,42 @@ impl Config { )), })?); } - "raft-addr" => { + "raft-addr" | "cluster-addr" => { raft_addr = Some(value); } - "raft-resp-addr" => { + "raft-resp-addr" | "cluster-resp-addr" => { raft_resp_addr = Some(value); } - "raft-data-dir" => { + "raft-data-dir" | "cluster-data-dir" => { raft_data_dir = Some(value); } + "raft-heartbeat-interval" | "cluster-heartbeat-interval" => { + raft_heartbeat_interval = + Some(value.parse().map_err(|e| Error::InvalidConfig { + source: serde_ini::de::Error::Custom(format!( + "Invalid raft-heartbeat-interval: {}", + e + )), + })?); + } + "raft-election-timeout-min" | "cluster-election-timeout-min" => { + raft_election_timeout_min = + Some(value.parse().map_err(|e| Error::InvalidConfig { + source: serde_ini::de::Error::Custom(format!( + "Invalid raft-election-timeout-min: {}", + e + )), + })?); + } + "raft-election-timeout-max" | "cluster-election-timeout-max" => { + raft_election_timeout_max = + Some(value.parse().map_err(|e| Error::InvalidConfig { + source: serde_ini::de::Error::Custom(format!( + "Invalid raft-election-timeout-max: {}", + e + )), + })?); + } "raft-use-memory-log-store" => { raft_use_memory_log_store = parse_bool_from_string(&value).map_err(|e| Error::InvalidConfig { @@ -360,6 +393,9 @@ impl Config { raft_addr: addr, resp_addr, data_dir, + heartbeat_interval_ms: raft_heartbeat_interval, + election_timeout_min_ms: raft_election_timeout_min, + election_timeout_max_ms: raft_election_timeout_max, use_memory_log_store: raft_use_memory_log_store, }); } @@ -370,7 +406,9 @@ impl Config { Ok(config) } +} +impl Config { // TODO: Due to API issues, the rocksdb_ttl_second parameter is temporarily missing pub fn get_rocksdb_options(&self) -> rocksdb::Options { let mut options = rocksdb::Options::default(); diff --git a/src/conf/src/lib.rs b/src/conf/src/lib.rs index 338f91a1..5179b654 100644 --- a/src/conf/src/lib.rs +++ b/src/conf/src/lib.rs @@ -32,7 +32,7 @@ mod tests { assert!( config.is_ok(), "Config loading failed: {:?}", - config.err().unwrap() + config.as_ref().err() ); let config = config.unwrap(); diff --git a/src/conf/src/raft_type.rs b/src/conf/src/raft_type.rs index 23d2a4ce..67934b10 100644 --- a/src/conf/src/raft_type.rs +++ b/src/conf/src/raft_type.rs @@ -79,6 +79,9 @@ impl fmt::Display for KiwiNode { pub struct BinlogResponse { pub success: bool, pub message: Option, + /// 写入的日志位置(提交后的 log_id) + /// 客户端可以使用此信息追踪写入状态 + pub log_id: Option, } impl BinlogResponse { @@ -86,6 +89,7 @@ impl BinlogResponse { Self { success: true, message: None, + log_id: None, } } @@ -93,6 +97,7 @@ impl BinlogResponse { Self { success: false, message: Some(msg.into()), + log_id: None, } } } diff --git a/src/raft/Cargo.toml b/src/raft/Cargo.toml index dd13029b..277fdb7f 100644 --- a/src/raft/Cargo.toml +++ b/src/raft/Cargo.toml @@ -13,23 +13,26 @@ rocksdb.workspace = true # Serialization serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" - -# HTTP -actix-web = "4.0" -reqwest = { version = "0.12", features = ["json"] } +bincode = "1.3" # Async tokio.workspace = true -async-trait = "0.1" +async-trait = "0.1.89" # Utils -anyhow = "1.0" -tracing = "0.1" +anyhow = "1.0.100" +tracing = "0.1.44" log.workspace = true - -# LogIndex / RocksDB parking_lot.workspace = true +tonic = "0.14.3" +prost = "0.14.3" +tonic-prost = "0.14.3" +tokio-stream = "0.1" + +[build-dependencies] +tonic-prost-build = "0.14.3" + [dev-dependencies] tempfile.workspace = true proptest = "1" diff --git a/src/raft/build.rs b/src/raft/build.rs new file mode 100644 index 00000000..d3f9e0ee --- /dev/null +++ b/src/raft/build.rs @@ -0,0 +1,37 @@ +// Copyright (c) 2024-present, arana-db Community. All rights reserved. +// +// 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::env; +use std::path::PathBuf; + +fn main() -> Result<(), Box> { + let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set")); + tonic_prost_build::configure() + .file_descriptor_set_path(out_dir.join("raft_proto_descriptor.bin")) + .build_server(true) + .build_client(true) + .compile_protos( + &[ + "proto/types.proto", + "proto/raft.proto", + "proto/admin.proto", + "proto/client.proto", + ], + &["proto"], + )?; + Ok(()) +} diff --git a/src/raft/docs/network_optimization.md b/src/raft/docs/network_optimization.md new file mode 100644 index 00000000..446232ca --- /dev/null +++ b/src/raft/docs/network_optimization.md @@ -0,0 +1,199 @@ +# KiwiNetwork 连接缓存优化设计文档 + +## 背景 + +当前 `KiwiNetworkFactory` 实现存在两个潜在的性能优化点: + +1. **每个 target 创建 2 个 Network 实例** - openraft 的 `spawn_replication_stream` 为每个目标节点创建 2 个 Network 实例 +2. **重建 replication 时不缓存旧连接** - 成员变更导致 replication stream 重建时,之前创建的 Channel 被丢弃 + +> **注意**:这里需要的是**连接缓存**(Connection Cache),而非传统意义上的**连接池**(Connection Pool)。 +> - 连接池:对同一个目标建立多个连接,提高并发 +> - 连接缓存:避免对同一个目标重复创建连接,复用已建立的 Channel +> +> Raft 场景下每个目标节点只需要一个连接,因此只需要缓存,不需要池。 + +## 问题分析 + +### 问题 1: 双重 Network 实例 + +openraft 源码 ([raft_core.rs](file:///home/mcig/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/openraft-0.9.21/src/core/raft_core.rs)): + +```rust +pub(crate) async fn spawn_replication_stream( + &mut self, + target: C::NodeId, + progress_entry: ProgressEntry, +) -> ReplicationHandle { + let target_node = ...; + + // 为该 target 创建 Network 实例 + let network = self.network.new_client(target.clone(), target_node).await; + let snapshot_network = self.network.new_client(target.clone(), target_node).await; + // ... +} +``` + +这意味着对同一个目标节点,创建了: +- 1 个普通复制用的 Network +- 1 个快照复制用的 Network + +每个 Network 内部都包含独立的 gRPC Channel,造成资源浪费。 + +### 问题 2: 无连接缓存 + +当前 `KiwiNetworkFactory` 是单元结构,没有任何状态: + +```rust +pub struct KiwiNetworkFactory; + +impl KiwiNetworkFactory { + pub fn new() -> Self { + Self // 没有任何内部状态 + } +} +``` + +当成员变更时,openraft 会调用 `spawn_replication_stream` 重建所有 replication streams,此时会再次调用 `new_client`,创建新的 Channel,而旧的 Channel(如果底层连接已建立)被丢弃。 + +## 优化方案 + +### 设计思路 + +使用 `Arc>>` 在 `KiwiNetworkFactory` 中缓存已创建的 Network 实例: + +```rust +use std::sync::Arc; +use tokio::sync::RwLock; +use std::collections::HashMap; + +pub struct KiwiNetworkFactory { + // 缓存每个目标节点的 Network 实例 + // Arc 确保多线程安全共享 + // RwLock 允许并发读、独占写 + networks: Arc>>, +} + +impl KiwiNetworkFactory { + pub fn new() -> Self { + Self { + networks: Arc::new(RwLock::new(HashMap::new())), + } + } +} + +impl RaftNetworkFactory for KiwiNetworkFactory { + type Network = Arc; + + async fn new_client(&mut self, target: NodeId, node: &Node) -> Self::Network { + // 先尝试获取读锁,从缓存获取 + { + let networks = self.networks.read().await; + if let Some(network) = networks.get(&target) { + return Arc::clone(network); + } + } + + // 缓存未命中,获取写锁,创建新的 Network + let mut networks = self.networks.write().await; + + // 双重检查:可能有其他请求刚刚创建了 + if let Some(network) = networks.get(&target) { + return Arc::clone(network); + } + + // 创建新的 Network + let addr = node.raft_addr.clone(); + let endpoint = ...; + let client = RaftCoreServiceClient::new(endpoint); + let network = Arc::new(KiwiNetwork { client }); + + // 存入缓存 + networks.insert(target, Arc::clone(&network)); + + network + } +} +``` + +### 为什么要用 Arc? + +openraft 可能同时持有同一个 Network 的多个引用(例如同时进行日志复制和快照复制),所以返回的 Network 必须是 `Clone` 的。使用 `Arc` 可以: + +1. **共享底层 Channel** - 多个引用共享同一个 gRPC Channel,避免重复创建 +2. **引用计数** - 最后所有引用销毁时自动清理 + +### KiwiNetwork 的修改 + +```rust +// 方案 A: 直接使用 Arc +pub struct KiwiNetwork { + client: RaftCoreServiceClient, +} + +// 方案 B: 如果需要在 Network 内部保持可变引用 +// 可以用 Arc> +pub struct KiwiNetwork { + inner: Arc>, +} + +pub struct KiwiNetworkInner { + client: RaftCoreServiceClient, +} +``` + +### 连接失效处理 + +当节点地址变更或连接断开时,需要能够移除缓存: + +```rust +impl KiwiNetworkFactory { + /// 移除指定节点的缓存(当节点被移除或地址变更时调用) + pub async fn remove_client(&mut self, target: NodeId) { + let mut networks = self.networks.write().await; + networks.remove(&target); + } + + /// 清空所有缓存(用于测试或重置) + pub async fn clear(&mut self) { + let mut networks = self.networks.write().await; + networks.clear(); + } +} +``` + +## 实现步骤 + +1. **修改 `KiwiNetworkFactory` 结构体** + - 添加 `networks: Arc>>>` 字段 + +2. **修改 `new_client` 方法** + - 先查询缓存,有则返回 Arc 克隆 + - 无则创建新实例并缓存 + +3. **修改 `KiwiNetwork` 返回类型** + - 将 `type Network = KiwiNetwork` 改为 `type Network = Arc` + +4. **实现清理方法** + - 添加 `remove_client` 和 `clear` 方法 + +5. **集成到 Raft 节点** + - 在成员变更时调用清理方法 + +## 性能对比 + +| 场景 | 优化前 | 优化后 | +|-----|-------|-------| +| 3 节点集群,每个 2 个 replication | 6 个 Channel | 3 个 Channel | +| 成员变更后重建 replication | 丢弃旧 Channel | 复用已有 Channel | +| 高频心跳 (100ms) | 连接复用 | 连接复用 | + +## 注意事项 + +1. **内存泄漏风险**: 如果节点永远不删除,HashMap 会持续增长。建议在节点移除时调用 `remove_client`。 + +2. **连接健康检查**: 当前实现不检查连接是否失效。可以考虑添加缓存健康检查机制。 + +3. **地址变更**: 如果节点地址变更,需要先调用 `remove_client` 再创建新连接。 + +4. **测试**: 需要添加单元测试验证缓存逻辑和并发安全性。 diff --git a/src/raft/proto/admin.proto b/src/raft/proto/admin.proto new file mode 100644 index 00000000..f22f170b --- /dev/null +++ b/src/raft/proto/admin.proto @@ -0,0 +1,76 @@ +// Copyright (c) 2024-present, arana-db Community. All rights reserved. +// +// 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. + +syntax = "proto3"; + +package raft_proto; + +import "types.proto"; + +// RaftAdminService - 集群管理服务 +service RaftAdminService { + // 初始化集群 + rpc Initialize (InitializeRequest) returns (InitializeResponse); + + // 添加学习者节点 + rpc AddLearner (AddLearnerRequest) returns (AddLearnerResponse); + + // 修改集群成员 + rpc ChangeMembership (ChangeMembershipRequest) returns (ChangeMembershipResponse); + + // 移除节点 + rpc RemoveNode (RemoveNodeRequest) returns (RemoveNodeResponse); +} + +// Initialize - 初始化集群 +message InitializeRequest { + repeated NodeConfig nodes = 1; +} + +message InitializeResponse { + Response response = 1; + uint64 leader_id = 2; +} + +// AddLearner - 添加学习者节点 +message AddLearnerRequest { + uint64 node_id = 1; + NodeConfig node = 2; +} + +message AddLearnerResponse { + Response response = 1; +} + +// ChangeMembership - 修改集群成员 +message ChangeMembershipRequest { + repeated NodeConfig members = 1; + bool retain = 2; +} + +message ChangeMembershipResponse { + Response response = 1; +} + +// RemoveNode - 移除节点 +message RemoveNodeRequest { + uint64 node_id = 1; +} + +message RemoveNodeResponse { + Response response = 1; +} diff --git a/src/raft/proto/client.proto b/src/raft/proto/client.proto new file mode 100644 index 00000000..0e407aea --- /dev/null +++ b/src/raft/proto/client.proto @@ -0,0 +1,88 @@ +// Copyright (c) 2024-present, arana-db Community. All rights reserved. +// +// 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. + +syntax = "proto3"; + +package raft_proto; + +import "types.proto"; + +// RaftClientService - 客户端服务 +service RaftClientService { + // 写入数据(通过 Raft 共识) + rpc Write (WriteRequest) returns (WriteResponse); + + // 读取数据 + rpc Read (ReadRequest) returns (ReadResponse); +} + +// RaftMetricsService - 集群状态服务 +service RaftMetricsService { + // 获取集群指标 + rpc Metrics (MetricsRequest) returns (MetricsResponse); + + // 获取 Leader 信息 + rpc Leader (LeaderRequest) returns (LeaderResponse); + + // 获取集群成员列表 + rpc Members (MembersRequest) returns (MembersResponse); +} + +// Write / Read - 客户端读写 +message WriteRequest { + Binlog binlog = 1; +} + +message WriteResponse { + Response response = 1; + LogId log_id = 2; +} + +message ReadRequest { + bytes key = 1; +} + +message ReadResponse { + Response response = 1; + bytes value = 2; +} + +// Metrics / Leader / Members - 集群状态查询 +message MetricsRequest {} + +message MetricsResponse { + Response response = 1; + bool is_leader = 2; + uint64 replication_lag = 3; + uint64 current_leader = 4; +} + +message LeaderRequest {} + +message LeaderResponse { + Response response = 1; + uint64 leader_id = 2; + NodeConfig leader_node = 3; +} + +message MembersRequest {} + +message MembersResponse { + Response response = 1; + repeated NodeConfig members = 2; + repeated uint64 learners = 3; +} diff --git a/src/raft/proto/raft.proto b/src/raft/proto/raft.proto new file mode 100644 index 00000000..1910e732 --- /dev/null +++ b/src/raft/proto/raft.proto @@ -0,0 +1,76 @@ +// Copyright (c) 2024-present, arana-db Community. All rights reserved. +// +// 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. + +syntax = "proto3"; + +package raft_proto; + +import "types.proto"; + +// RaftCoreService - Raft 协议核心服务 +service RaftCoreService { + // 请求投票 RPC - 选举阶段使用 + rpc Vote (VoteRequest) returns (VoteResponse); + + // 追加日志条目 RPC - 日志复制使用 + rpc AppendEntries (AppendEntriesRequest) returns (AppendEntriesResponse); + + // 流式追加日志 RPC - pipeline 复制(生产阶段使用) + rpc StreamAppend (stream AppendEntriesRequest) returns (stream AppendEntriesResponse); + + // 安装快照 RPC - 流式传输(POC 不实现) + rpc InstallSnapshot (stream InstallSnapshotRequest) returns (InstallSnapshotResponse); +} + +// Vote RPC - 请求投票 +message VoteRequest { + Vote vote = 1; + LogId last_log_id = 2; +} + +message VoteResponse { + Vote vote = 1; + bool vote_granted = 2; + LogId last_log_id = 3; +} + +// AppendEntries RPC - 追加日志条目 +message AppendEntriesRequest { + Vote vote = 1; + LogId prev_log_id = 2; + repeated Entry entries = 3; + LogId leader_commit = 4; +} + +// TODO: AppendEntriesResponse 可以包含更多信息,例如 conflict_index、conflict_term等 +message AppendEntriesResponse { + bool success = 1; +} + +// InstallSnapshot RPC - 安装快照 +message InstallSnapshotRequest { + uint64 term = 1; + uint64 leader_id = 2; + uint64 last_included_index = 3; + uint64 last_included_term = 4; + bytes data = 5; + bool done = 6; +} + +message InstallSnapshotResponse { + bool success = 1; +} diff --git a/src/raft/proto/types.proto b/src/raft/proto/types.proto new file mode 100644 index 00000000..809660b4 --- /dev/null +++ b/src/raft/proto/types.proto @@ -0,0 +1,101 @@ +// Copyright (c) 2024-present, arana-db Community. All rights reserved. +// +// 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. + +syntax = "proto3"; + +package raft_proto; + + +// 节点 ID +message NodeId { + uint64 id = 1; +} + +// Leader ID:包含 term 和 node_id +message LeaderId { + uint64 term = 1; + NodeId node_id = 2; +} + +// Log ID:包含 leader_id 和 index +message LogId { + LeaderId leader_id = 1; + uint64 index = 2; +} + +// Vote:投票信息 +message Vote { + LeaderId leader_id = 1; + bool committed = 2; +} + +// 节点配置信息 +message NodeConfig { + uint64 node_id = 1; + string raft_addr = 2; // gRPC 地址,用于 Raft 内部通信 + string resp_addr = 3; // RESP 协议地址,用于客户端连接 +} + +// 空日志条目(用于心跳等) +message BlankPayload {} + +// 普通日志条目(用户数据) +message NormalPayload { + bytes data = 1; +} + +// 成员变更日志条目 +message Membership { + repeated uint64 node_ids = 1; + repeated NodeConfig nodes = 2; +} + +// 日志条目 Payload(使用 oneof 实现枚举) +message EntryPayload { + oneof payload { + BlankPayload blank = 1; + NormalPayload normal = 2; + Membership membership = 3; + } +} + +// 日志条目 +message Entry { + LogId log_id = 1; + EntryPayload payload = 2; +} + +// Binlog 条目 +message BinlogEntry { + uint32 cf_idx = 1; + string op_type = 2; // "Put", "Delete", etc. + bytes key = 3; + bytes value = 4; +} + +// Binlog 请求 +message Binlog { + uint64 db_id = 1; + uint64 slot_idx = 2; + repeated BinlogEntry entries = 3; +} + +// 通用响应包装 +message Response { + bool success = 1; + string message = 2; +} diff --git a/src/raft/src/api.rs b/src/raft/src/api.rs deleted file mode 100644 index b483e38c..00000000 --- a/src/raft/src/api.rs +++ /dev/null @@ -1,247 +0,0 @@ -// Copyright (c) 2024-present, arana-db Community. All rights reserved. -// -// 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::collections::BTreeMap; -use std::sync::Arc; - -use conf::raft_type::{Binlog, KiwiNode, KiwiTypeConfig}; -use openraft::ChangeMembers; -use openraft::raft::{AppendEntriesRequest, VoteRequest}; -use serde::{Deserialize, Serialize}; - -use crate::node::RaftApp; -use actix_web::{ - HttpResponse, Responder, get, post, - web::{self, Json}, -}; - -#[derive(Clone)] -pub struct RaftAppData { - pub app: Arc, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct WriteRequest { - pub binlog: Binlog, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct ReadRequest { - pub key: Vec, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct ReadResponse { - pub value: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct LeaderResponse { - pub leader_id: u64, - pub node: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct MetricsResponse { - pub is_leader: bool, - pub replication_lag: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct InitRequest { - pub nodes: Vec<(u64, KiwiNode)>, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct AddLearnerRequest { - pub node_id: u64, - pub node: KiwiNode, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct ChangeMembershipRequest { - pub members: Vec<(u64, KiwiNode)>, - pub retain: bool, -} - -#[derive(Debug, Serialize)] -pub struct ApiResponse { - pub success: bool, - pub message: String, - pub data: Option, -} - -impl ApiResponse { - fn success(data: T) -> Self { - Self { - success: true, - message: "OK".to_string(), - data: Some(data), - } - } - - fn error(message: String) -> Self { - Self { - success: false, - message, - data: None, - } - } -} - -#[post("/raft/write")] -pub async fn write(app_data: web::Data, req: Json) -> impl Responder { - match app_data.app.client_write(req.0.binlog).await { - Ok(response) => HttpResponse::Ok().json(ApiResponse::success(response)), - Err(e) => { - log::error!("Failed to write to Raft: {}", e); - HttpResponse::InternalServerError().json(ApiResponse::<()>::error(e.to_string())) - } - } -} - -#[post("/raft/read")] -pub async fn read(app_data: web::Data, req: Json) -> impl Responder { - if !app_data.app.is_leader() { - if let Some((_, node)) = app_data.app.get_leader() { - let message = format!("Not leader, redirect to: {}", node.raft_addr); - return HttpResponse::TemporaryRedirect() - .insert_header(("Location", node.raft_addr)) - .json(ApiResponse::<()>::error(message)); - } - return HttpResponse::ServiceUnavailable() - .json(ApiResponse::<()>::error("No leader available".to_string())); - } - - let value = app_data.app.storage.get(&req.key); - match value { - Ok(v) => HttpResponse::Ok().json(ApiResponse::success(ReadResponse { value: Some(v) })), - Err(e) => { - log::error!("Failed to read from storage: {}", e); - HttpResponse::InternalServerError().json(ApiResponse::<()>::error(e.to_string())) - } - } -} - -#[get("/raft/metrics")] -pub async fn metrics(app_data: web::Data) -> impl Responder { - let is_leader = app_data.app.is_leader(); - let leader_info = app_data.app.get_leader(); - - let replication_lag = leader_info.and(if !is_leader { Some(0) } else { None }); - - HttpResponse::Ok().json(ApiResponse::success(MetricsResponse { - is_leader, - replication_lag, - })) -} - -#[get("/raft/leader")] -pub async fn leader(app_data: web::Data) -> impl Responder { - match app_data.app.get_leader() { - Some((leader_id, node)) => HttpResponse::Ok().json(ApiResponse::success(LeaderResponse { - leader_id, - node: Some(node), - })), - None => HttpResponse::ServiceUnavailable() - .json(ApiResponse::<()>::error("No leader available".to_string())), - } -} - -#[post("/raft/init")] -pub async fn init(app_data: web::Data, req: Json) -> impl Responder { - log::info!("Initializing cluster with {} nodes", req.nodes.len()); - - let raft = app_data.app.raft.clone(); - let init_req = req.into_inner(); - let nodes: BTreeMap = init_req.nodes.into_iter().collect(); - - match raft.initialize(nodes).await { - Ok(_) => HttpResponse::Ok().json(ApiResponse::success(())), - Err(e) => { - log::error!("Failed to initialize cluster: {}", e); - HttpResponse::InternalServerError().json(ApiResponse::<()>::error(e.to_string())) - } - } -} - -#[post("/raft/add_learner")] -pub async fn add_learner( - app_data: web::Data, - req: Json, -) -> impl Responder { - let add_req = req.into_inner(); - log::info!("Adding learner node: {}", add_req.node_id); - let raft = app_data.app.raft.clone(); - - match raft.add_learner(add_req.node_id, add_req.node, true).await { - Ok(_) => HttpResponse::Ok().json(ApiResponse::success(())), - Err(e) => { - log::error!("Failed to add learner: {}", e); - HttpResponse::InternalServerError().json(ApiResponse::<()>::error(e.to_string())) - } - } -} - -#[post("/raft/change_membership")] -pub async fn change_membership( - app_data: web::Data, - req: Json, -) -> impl Responder { - let change_req = req.into_inner(); - log::info!( - "Changing membership with {} members, retain={}", - change_req.members.len(), - change_req.retain - ); - - let raft = app_data.app.raft.clone(); - let members_map: BTreeMap = change_req.members.into_iter().collect(); - let changes = ChangeMembers::ReplaceAllNodes(members_map); - - match raft.change_membership(changes, change_req.retain).await { - Ok(_) => HttpResponse::Ok().json(ApiResponse::success(())), - Err(e) => { - log::error!("Failed to change membership: {}", e); - HttpResponse::InternalServerError().json(ApiResponse::<()>::error(e.to_string())) - } - } -} - -// --- Raft protocol RPC (used by KiwiNetwork when nodes talk to each other) - -#[post("/raft/vote")] -pub async fn raft_vote( - app_data: web::Data, - req: Json::NodeId>>, -) -> impl Responder { - match app_data.app.raft.vote(req.0).await { - Ok(res) => HttpResponse::Ok().json(res), - Err(e) => HttpResponse::InternalServerError().body(e.to_string()), - } -} - -#[post("/raft/append")] -pub async fn raft_append( - app_data: web::Data, - req: Json>, -) -> impl Responder { - match app_data.app.raft.append_entries(req.0).await { - Ok(res) => HttpResponse::Ok().json(res), - Err(e) => HttpResponse::InternalServerError().body(e.to_string()), - } -} diff --git a/src/raft/src/conversion.rs b/src/raft/src/conversion.rs new file mode 100644 index 00000000..b68249a2 --- /dev/null +++ b/src/raft/src/conversion.rs @@ -0,0 +1,359 @@ +// Copyright (c) 2024-present, arana-db Community. All rights reserved. +// +// 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. + +// 类型转换模块:Proto 类型 ↔ OpenRaft 类型 +use crate::raft_proto as proto; +use conf::raft_type::{Binlog, KiwiNode, KiwiTypeConfig}; +use openraft::CommittedLeaderId; +use openraft::LeaderId; +use openraft::LogId; +use openraft::Vote; +use openraft::entry::{Entry, EntryPayload}; +use openraft::raft::{AppendEntriesRequest, AppendEntriesResponse, VoteRequest, VoteResponse}; +use std::convert::TryInto; + +// 辅助函数:创建 LeaderId、LogId 和 Vote +pub fn proto_to_leader_id(lid: &Option<&proto::LeaderId>) -> LeaderId { + match lid { + Some(l) => LeaderId::new(l.term, l.node_id.as_ref().map(|n| n.id).unwrap_or(0)), + None => LeaderId::default(), + } +} + +pub fn leader_id_to_proto(lid: &LeaderId) -> proto::LeaderId { + proto::LeaderId { + term: lid.term, + node_id: Some(proto::NodeId { id: lid.node_id }), + } +} + +pub fn proto_to_committed_leader_id(lid: &Option<&proto::LeaderId>) -> CommittedLeaderId { + match lid { + Some(l) => CommittedLeaderId::new(l.term, l.node_id.as_ref().map(|n| n.id).unwrap_or(0)), + None => CommittedLeaderId::default(), + } +} + +pub fn proto_to_log_id(lid: &Option<&proto::LogId>) -> Option> { + lid.map(|l| LogId { + leader_id: proto_to_committed_leader_id(&l.leader_id.as_ref()), + index: l.index, + }) +} + +pub fn log_id_to_proto(lid: &LogId) -> proto::LogId { + proto::LogId { + leader_id: Some(proto::LeaderId { + term: lid.leader_id.term, + node_id: Some(proto::NodeId { + id: lid.leader_id.node_id, + }), + }), + index: lid.index, + } +} + +pub fn log_id_option_to_proto(lid: &Option>) -> Option { + lid.as_ref().map(log_id_to_proto) +} + +pub fn proto_to_vote(vote: &Option<&proto::Vote>) -> Vote { + match vote { + Some(v) => { + let term = v.leader_id.as_ref().map(|l| l.term).unwrap_or(0); + let node_id = v + .leader_id + .as_ref() + .and_then(|l| l.node_id.as_ref().map(|n| n.id)) + .unwrap_or(0); + if v.committed { + Vote::new_committed(term, node_id) + } else { + Vote::new(term, node_id) + } + } + None => Vote::default(), + } +} + +pub fn vote_to_proto(vote: &Vote) -> proto::Vote { + proto::Vote { + leader_id: Some(leader_id_to_proto(&vote.leader_id)), + committed: vote.committed, + } +} + +// Proto → OpenRaft 转换 (gRPC Server 使用) +// ----- VoteRequest ----- + +impl TryInto> for &proto::VoteRequest { + type Error = tonic::Status; + + fn try_into(self) -> Result, Self::Error> { + Ok(VoteRequest { + vote: proto_to_vote(&self.vote.as_ref()), + last_log_id: proto_to_log_id(&self.last_log_id.as_ref()), + }) + } +} + +// ----- AppendEntriesRequest ----- + +impl TryInto> for &proto::AppendEntriesRequest { + type Error = tonic::Status; + + fn try_into(self) -> Result, Self::Error> { + let vote = proto_to_vote(&self.vote.as_ref()); + let prev_log_id = proto_to_log_id(&self.prev_log_id.as_ref()); + + // 转换 entries + let mut entries = Vec::new(); + for proto_entry in &self.entries { + match proto_entry.try_into() { + Ok(entry) => entries.push(entry), + Err(e) => { + return Err(tonic::Status::invalid_argument(format!( + "invalid entry: {}", + e + ))); + } + } + } + + let leader_commit = proto_to_log_id(&self.leader_commit.as_ref()); + + Ok(AppendEntriesRequest { + vote, + prev_log_id, + entries, + leader_commit, + }) + } +} + +// 转换单个 Entry (Proto → OpenRaft) +impl TryInto> for &proto::Entry { + type Error = tonic::Status; + + fn try_into(self) -> Result, Self::Error> { + let log_id = proto_to_log_id(&self.log_id.as_ref()) + .ok_or_else(|| tonic::Status::invalid_argument("missing log_id in entry"))?; + + let payload = match &self.payload { + Some(p) => match &p.payload { + Some(proto::entry_payload::Payload::Blank(_)) => EntryPayload::Blank, + Some(proto::entry_payload::Payload::Normal(normal)) => { + // 反序列化 Binlog + match bincode::deserialize::(&normal.data) { + Ok(binlog) => EntryPayload::Normal(binlog), + Err(e) => { + return Err(tonic::Status::invalid_argument(format!( + "failed to deserialize binlog: {}", + e + ))); + } + } + } + Some(proto::entry_payload::Payload::Membership(membership)) => { + // 转换 Membership + use openraft::Membership; + use std::collections::BTreeSet; + + // Membership::new 的参数是 Vec> 和 nodes + // 单个配置是 vec![BTreeSet] + let node_ids: BTreeSet = membership.node_ids.iter().copied().collect(); + + // 构建 BTreeMap + let mut nodes = std::collections::BTreeMap::new(); + for node_config in &membership.nodes { + let kiwi_node = KiwiNode { + raft_addr: node_config.raft_addr.clone(), + resp_addr: node_config.resp_addr.clone(), + }; + nodes.insert(node_config.node_id, kiwi_node); + } + + // Membership::new(configs, nodes) 其中 configs: Vec> + EntryPayload::Membership(Membership::new(vec![node_ids], nodes)) + } + None => return Err(tonic::Status::invalid_argument("empty payload")), + }, + None => return Err(tonic::Status::invalid_argument("empty payload")), + }; + + Ok(Entry { log_id, payload }) + } +} + +// OpenRaft → Proto 转换 (gRPC Server 响应使用) +// ----- VoteResponse ----- + +impl From> for proto::VoteResponse { + fn from(resp: VoteResponse) -> Self { + proto::VoteResponse { + vote: Some(vote_to_proto(&resp.vote)), + vote_granted: resp.vote_granted, + last_log_id: resp.last_log_id.as_ref().map(log_id_to_proto), + } + } +} + +// ----- AppendEntriesResponse ----- +// 注意:OpenRaft 的 AppendEntriesResponse 是 enum (Success/PartialSuccess) +// 简化版本:只返回 success 布尔值 + +impl From> for proto::AppendEntriesResponse { + fn from(resp: AppendEntriesResponse) -> Self { + let success = matches!(resp, AppendEntriesResponse::Success); + proto::AppendEntriesResponse { success } + } +} + +// OpenRaft → Proto 转换 (gRPC Client 请求使用) + +// ----- VoteRequest ----- + +impl From> for proto::VoteRequest { + fn from(req: VoteRequest) -> Self { + proto::VoteRequest { + vote: Some(vote_to_proto(&req.vote)), + last_log_id: req.last_log_id.as_ref().map(log_id_to_proto), + } + } +} + +// ----- AppendEntriesRequest ----- + +impl TryFrom> for proto::AppendEntriesRequest { + type Error = tonic::Status; + + fn try_from(req: AppendEntriesRequest) -> Result { + let vote = Some(vote_to_proto(&req.vote)); + let prev_log_id = log_id_option_to_proto(&req.prev_log_id); + + // 转换 entries,记录转换失败的条目而非静默丢弃 + let mut entries = Vec::new(); + let mut errors = Vec::new(); + for (idx, e) in req.entries.into_iter().enumerate() { + match e.try_into() { + Ok(entry) => entries.push(entry), + Err(e) => errors.push(format!("entry[{}]: {}", idx, e)), + } + } + if !errors.is_empty() { + return Err(tonic::Status::invalid_argument(format!( + "failed to convert {} entries: {}", + errors.len(), + errors.join("; ") + ))); + } + + let leader_commit = log_id_option_to_proto(&req.leader_commit); + + Ok(proto::AppendEntriesRequest { + vote, + prev_log_id, + entries, + leader_commit, + }) + } +} + +// 转换单个 Entry (OpenRaft → Proto) +impl TryInto for Entry { + type Error = anyhow::Error; + + fn try_into(self) -> Result { + let log_id = Some(log_id_to_proto(&self.log_id)); + + let payload = match self.payload { + EntryPayload::Blank => Some(proto::EntryPayload { + payload: Some(proto::entry_payload::Payload::Blank(proto::BlankPayload {})), + }), + EntryPayload::Normal(binlog) => { + // 序列化 Binlog + let data = bincode::serialize(&binlog)?; + Some(proto::EntryPayload { + payload: Some(proto::entry_payload::Payload::Normal( + proto::NormalPayload { data }, + )), + }) + } + EntryPayload::Membership(membership) => { + // 转换 Membership + // 获取 voter_ids (所有配置中的节点 ID) + let mut all_node_ids = std::collections::BTreeSet::new(); + for config in membership.get_joint_config() { + for node_id in config { + all_node_ids.insert(*node_id); + } + } + let node_ids: Vec = all_node_ids.iter().copied().collect(); + + let nodes: Vec = membership + .nodes() + .map(|(id, node)| proto::NodeConfig { + node_id: *id, + raft_addr: node.raft_addr.clone(), + resp_addr: node.resp_addr.clone(), + }) + .collect(); + + Some(proto::EntryPayload { + payload: Some(proto::entry_payload::Payload::Membership( + proto::Membership { node_ids, nodes }, + )), + }) + } + }; + + Ok(proto::Entry { log_id, payload }) + } +} + +// Proto → OpenRaft 转换 (gRPC Client 响应使用) +// ----- VoteResponse ----- + +impl TryInto> for &proto::VoteResponse { + type Error = tonic::Status; + + fn try_into(self) -> Result, Self::Error> { + Ok(VoteResponse { + vote: proto_to_vote(&self.vote.as_ref()), + vote_granted: self.vote_granted, + last_log_id: proto_to_log_id(&self.last_log_id.as_ref()), + }) + } +} + +// ----- AppendEntriesResponse ----- +// Proto 的 success: true 映射为 Success,false 也映射为 Success(简化处理) +// 生产环境应该根据情况返回 Success 或错误 + +impl TryInto> for &proto::AppendEntriesResponse { + type Error = tonic::Status; + + fn try_into(self) -> Result, Self::Error> { + if self.success { + Ok(AppendEntriesResponse::Success) + } else { + Err(tonic::Status::aborted( + "AppendEntries failed: leader rejected the entries", + )) + } + } +} diff --git a/src/raft/src/grpc/admin.rs b/src/raft/src/grpc/admin.rs new file mode 100644 index 00000000..30f4479b --- /dev/null +++ b/src/raft/src/grpc/admin.rs @@ -0,0 +1,256 @@ +// Copyright (c) 2024-present, arana-db Community. All rights reserved. +// +// 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. + +// RaftAdminService 实现 - 集群管理服务 +// 用途:集群初始化、成员变更等管理操作 + +use conf::raft_type::KiwiNode; +use openraft::ChangeMembers; +use std::collections::BTreeMap; +use std::sync::Arc; + +// 导入 proto 生成的类型 +use crate::raft_proto::{ + AddLearnerRequest, AddLearnerResponse, ChangeMembershipRequest, ChangeMembershipResponse, + InitializeRequest, InitializeResponse, RemoveNodeRequest, RemoveNodeResponse, + Response as ProtoResponse, + raft_admin_service_server::{RaftAdminService, RaftAdminServiceServer}, +}; +use tonic::{Request, Response as TonicResponse, Status}; + +use crate::node::RaftApp; + +/// Raft 管理服务实现 +pub struct RaftAdminServiceImpl { + app: Arc, +} + +impl RaftAdminServiceImpl { + pub fn new(app: Arc) -> Self { + Self { app } + } +} + +/// 创建 RaftAdminService 服务器 +pub fn create_admin_service(app: Arc) -> RaftAdminServiceServer { + RaftAdminServiceServer::new(RaftAdminServiceImpl::new(app)) +} + +/// NodeConfig 转 KiwiNode +fn proto_to_node(node: &crate::raft_proto::NodeConfig) -> KiwiNode { + KiwiNode { + raft_addr: node.raft_addr.clone(), + resp_addr: node.resp_addr.clone(), + } +} + +/// 成功响应 +fn ok_response() -> ProtoResponse { + ProtoResponse { + success: true, + message: "OK".to_string(), + } +} + +/// 错误响应 +fn error_response(msg: String) -> ProtoResponse { + ProtoResponse { + success: false, + message: msg, + } +} + +#[tonic::async_trait] +impl RaftAdminService for RaftAdminServiceImpl { + /// 初始化集群 + async fn initialize( + &self, + request: Request, + ) -> Result, Status> { + let proto_req = request.into_inner(); + + log::info!("Initializing cluster with {} nodes", proto_req.nodes.len()); + + // 转换 Proto → BTreeMap + let mut nodes = BTreeMap::new(); + for node_config in &proto_req.nodes { + let node = proto_to_node(node_config); + log::info!( + " Node {}: raft_addr={}, resp_addr={}", + node_config.node_id, + node_config.raft_addr, + node_config.resp_addr + ); + nodes.insert(node_config.node_id, node); + } + + // 调用 OpenRaft initialize + match self.app.raft.initialize(nodes).await { + Ok(_) => { + log::info!("Cluster initialized successfully"); + + // 获取 leader 信息 + let leader_id = self.app.raft.metrics().borrow().current_leader; + + Ok(TonicResponse::new(InitializeResponse { + response: Some(ok_response()), + leader_id: leader_id.unwrap_or(0), + })) + } + Err(e) => { + log::error!("Failed to initialize cluster: {}", e); + Ok(TonicResponse::new(InitializeResponse { + response: Some(error_response(format!( + "Failed to initialize cluster: {}", + e + ))), + leader_id: 0, + })) + } + } + } + + /// 添加学习者节点 + async fn add_learner( + &self, + request: Request, + ) -> Result, Status> { + let proto_req = request.into_inner(); + + log::info!("Adding learner node: {}", proto_req.node_id); + + let node = match proto_req.node { + Some(n) => proto_to_node(&n), + None => { + return Ok(TonicResponse::new(AddLearnerResponse { + response: Some(error_response("Missing node config".to_string())), + })); + } + }; + + match self + .app + .raft + .add_learner(proto_req.node_id, node, true) + .await + { + Ok(_) => { + log::info!("Learner node {} added successfully", proto_req.node_id); + Ok(TonicResponse::new(AddLearnerResponse { + response: Some(ok_response()), + })) + } + Err(e) => { + log::error!("Failed to add learner: {}", e); + Ok(TonicResponse::new(AddLearnerResponse { + response: Some(error_response(format!("Failed to add learner: {}", e))), + })) + } + } + } + + /// 修改集群成员 + async fn change_membership( + &self, + request: Request, + ) -> Result, Status> { + let proto_req = request.into_inner(); + + log::info!( + "Changing membership with {} members, retain={}", + proto_req.members.len(), + proto_req.retain + ); + + // 转换 Proto → BTreeMap + let mut members = BTreeMap::new(); + for node_config in &proto_req.members { + let node = proto_to_node(node_config); + members.insert(node_config.node_id, node); + } + + let changes = ChangeMembers::ReplaceAllNodes(members); + + match self + .app + .raft + .change_membership(changes, proto_req.retain) + .await + { + Ok(_) => { + log::info!("Membership changed successfully"); + Ok(TonicResponse::new(ChangeMembershipResponse { + response: Some(ok_response()), + })) + } + Err(e) => { + log::error!("Failed to change membership: {}", e); + Ok(TonicResponse::new(ChangeMembershipResponse { + response: Some(error_response(format!( + "Failed to change membership: {}", + e + ))), + })) + } + } + } + + /// 移除节点 + async fn remove_node( + &self, + request: Request, + ) -> Result, Status> { + let proto_req = request.into_inner(); + + log::info!("Removing node: {}", proto_req.node_id); + + // 获取当前成员并提前提取数据 + let new_members = { + let metrics = self.app.raft.metrics(); + let guard = metrics.borrow(); + let membership = guard.membership_config.membership(); + + // 构建不包含要移除节点的新成员集合 + let mut new_members = BTreeMap::new(); + let nodes_iter = membership.nodes().collect::>(); + for (node_id, node) in nodes_iter { + if *node_id != proto_req.node_id { + new_members.insert(*node_id, node.clone()); + } + } + new_members + }; + + // 使用 ReplaceAllNodes 替换成员(排除要移除的节点) + let changes = ChangeMembers::ReplaceAllNodes(new_members); + + match self.app.raft.change_membership(changes, false).await { + Ok(_) => { + log::info!("Node {} removed successfully", proto_req.node_id); + Ok(TonicResponse::new(RemoveNodeResponse { + response: Some(ok_response()), + })) + } + Err(e) => { + log::error!("Failed to remove node: {}", e); + Ok(TonicResponse::new(RemoveNodeResponse { + response: Some(error_response(format!("Failed to remove node: {}", e))), + })) + } + } + } +} diff --git a/src/raft/src/grpc/client.rs b/src/raft/src/grpc/client.rs new file mode 100644 index 00000000..6ca9bb2a --- /dev/null +++ b/src/raft/src/grpc/client.rs @@ -0,0 +1,290 @@ +// Copyright (c) 2024-present, arana-db Community. All rights reserved. +// +// 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. + +// RaftClientService 和 RaftMetricsService 实现 +// 用途:客户端读写数据、查询集群状态 + +use conf::raft_type::{Binlog, BinlogEntry, OperateType}; +use std::sync::Arc; +use storage::ColumnFamilyIndex; +use storage::slot_indexer::key_to_slot_id; + +// 导入 proto 生成的类型 +use crate::raft_proto::{ + LeaderRequest, LeaderResponse, MembersRequest, MembersResponse, MetricsRequest, + MetricsResponse, NodeConfig, ReadRequest, ReadResponse, Response as ProtoResponse, + WriteRequest, WriteResponse, + raft_client_service_server::{RaftClientService, RaftClientServiceServer}, + raft_metrics_service_server::{RaftMetricsService, RaftMetricsServiceServer}, +}; +use tonic::{Request, Response as TonicResponse, Status}; + +use crate::node::RaftApp; + +/// Raft 客户端服务实现 +pub struct RaftClientServiceImpl { + app: Arc, +} + +impl RaftClientServiceImpl { + pub fn new(app: Arc) -> Self { + Self { app } + } +} + +/// 创建 RaftClientService 服务器 +pub fn create_client_service(app: Arc) -> RaftClientServiceServer { + RaftClientServiceServer::new(RaftClientServiceImpl::new(app)) +} + +/// Raft 指标服务实现 +pub struct RaftMetricsServiceImpl { + app: Arc, +} + +impl RaftMetricsServiceImpl { + pub fn new(app: Arc) -> Self { + Self { app } + } +} + +/// 创建 RaftMetricsService 服务器 +pub fn create_metrics_service( + app: Arc, +) -> RaftMetricsServiceServer { + RaftMetricsServiceServer::new(RaftMetricsServiceImpl::new(app)) +} + +/// 成功响应 +fn ok_response() -> ProtoResponse { + ProtoResponse { + success: true, + message: "OK".to_string(), + } +} + +/// 错误响应 +fn error_response(msg: String) -> ProtoResponse { + ProtoResponse { + success: false, + message: msg, + } +} + +// RaftClientService 实现 +#[tonic::async_trait] +impl RaftClientService for RaftClientServiceImpl { + /// 写入数据(通过 Raft 共识) + async fn write( + &self, + request: Request, + ) -> Result, Status> { + let proto_req = request.into_inner(); + + // Proto Binlog → 实际 Binlog + let binlog = proto_binlog_to_binlog(proto_req.binlog)?; + + match self.app.client_write(binlog).await { + Ok(response) => { + let proto_response = if response.success { + ok_response() + } else { + error_response( + response + .message + .unwrap_or_else(|| "Unknown error".to_string()), + ) + }; + // 返回实际提交的 log_id(index 来自 response,term 信息不可用故设为 None) + let log_id = response.log_id.map(|idx| crate::raft_proto::LogId { + leader_id: None, + index: idx, + }); + Ok(TonicResponse::new(WriteResponse { + response: Some(proto_response), + log_id, + })) + } + Err(e) => { + log::error!("Failed to write to Raft: {}", e); + Err(Status::internal(format!("Write failed: {}", e))) + } + } + } + + /// 读取数据 + async fn read( + &self, + request: Request, + ) -> Result, Status> { + let proto_req = request.into_inner(); + let key = &proto_req.key; + + // 检查是否为 leader + if !self.app.is_leader() { + if let Some((_, node)) = self.app.get_leader() { + return Err(Status::failed_precondition(format!( + "Not leader, redirect to: {}", + node.raft_addr + ))); + } + return Err(Status::unavailable("No leader available".to_string())); + } + + // 计算实例 ID + let slot_id = key_to_slot_id(key); + let instance_id = self.app.storage.slot_indexer.get_instance_id(slot_id); + let instance = &self.app.storage.insts[instance_id]; + + // 从 MetaCF 读取 + match instance.get_cf_handle(ColumnFamilyIndex::MetaCF) { + Some(cf) => { + let db = instance + .db + .as_ref() + .ok_or_else(|| Status::internal("Database not initialized".to_string()))?; + match db.get_cf(&cf, key) { + Ok(Some(val)) => Ok(TonicResponse::new(ReadResponse { + response: Some(ok_response()), + value: val, + })), + Ok(None) => Ok(TonicResponse::new(ReadResponse { + response: Some(error_response(format!( + "Key not found: {:?}", + String::from_utf8_lossy(key) + ))), + value: vec![], + })), + Err(e) => Err(Status::internal(format!("Read failed: {}", e))), + } + } + None => Err(Status::internal("MetaCF not found".to_string())), + } + } +} + +// RaftMetricsService 实现 +#[tonic::async_trait] +impl RaftMetricsService for RaftMetricsServiceImpl { + /// 获取集群指标 + async fn metrics( + &self, + _request: Request, + ) -> Result, Status> { + let is_leader = self.app.is_leader(); + let metrics = self.app.raft.metrics(); + let guard = metrics.borrow(); + let current_leader = guard.current_leader.unwrap_or(0); + drop(guard); + + // TODO: 实现 replication_lag 计算 + // 需要获取 state_machine.last_applied 与 committed 的差异 + let replication_lag = 0; + + Ok(TonicResponse::new(MetricsResponse { + response: Some(ok_response()), + is_leader, + replication_lag, + current_leader, + })) + } + + /// 获取 Leader 信息 + async fn leader( + &self, + _request: Request, + ) -> Result, Status> { + match self.app.get_leader() { + Some((leader_id, node)) => Ok(TonicResponse::new(LeaderResponse { + response: Some(ok_response()), + leader_id, + leader_node: Some(NodeConfig { + node_id: leader_id, + raft_addr: node.raft_addr.clone(), + resp_addr: node.resp_addr.clone(), + }), + })), + None => Err(Status::unavailable("No leader available".to_string())), + } + } + + /// 获取集群成员列表 + async fn members( + &self, + _request: Request, + ) -> Result, Status> { + let metrics = self.app.raft.metrics(); + let guard = metrics.borrow(); + let membership = guard.membership_config.membership(); + + // 获取所有节点配置 + let members: Vec = membership + .nodes() + .map(|(id, node)| NodeConfig { + node_id: *id, + raft_addr: node.raft_addr.clone(), + resp_addr: node.resp_addr.clone(), + }) + .collect(); + + // 获取 learner ids + let learner_ids = membership.learner_ids(); + let learners: Vec = learner_ids.collect(); + + drop(guard); + + Ok(TonicResponse::new(MembersResponse { + response: Some(ok_response()), + members, + learners, + })) + } +} + +// 辅助函数 +/// Proto Binlog → 实际 Binlog +fn proto_binlog_to_binlog( + proto_binlog: Option, +) -> Result { + let proto_binlog = + proto_binlog.ok_or_else(|| Status::invalid_argument("Missing binlog".to_string()))?; + + // 转换 Proto Binlog → 实际 Binlog + let entries: Vec = proto_binlog + .entries + .into_iter() + .map(|proto_entry| { + let op_type = match proto_entry.op_type.as_str() { + "Put" => OperateType::Put, + "Delete" => OperateType::Delete, + _ => OperateType::Put, // 默认 + }; + BinlogEntry { + cf_idx: proto_entry.cf_idx, + op_type, + key: proto_entry.key, + value: Some(proto_entry.value), + } + }) + .collect(); + + Ok(Binlog { + db_id: proto_binlog.db_id as u32, + slot_idx: proto_binlog.slot_idx as u32, + entries, + }) +} diff --git a/src/raft/src/grpc/core.rs b/src/raft/src/grpc/core.rs new file mode 100644 index 00000000..490e41d1 --- /dev/null +++ b/src/raft/src/grpc/core.rs @@ -0,0 +1,242 @@ +// Copyright (c) 2024-present, arana-db Community. All rights reserved. +// +// 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. + +// RaftCoreService 实现 - Raft 协议核心服务 +// 用途:节点间通信,实现 Raft 共识算法 + +use conf::raft_type::KiwiTypeConfig; +use openraft::Raft; +use std::pin::Pin; +use tokio_stream::Stream; + +// 导入 proto 生成的类型 +use crate::raft_proto::{ + AppendEntriesRequest, AppendEntriesResponse, Entry as ProtoEntry, InstallSnapshotRequest, + InstallSnapshotResponse, VoteRequest, VoteResponse, entry_payload, + raft_core_service_server::{RaftCoreService, RaftCoreServiceServer}, +}; +use tonic::{Request, Response, Status}; + +// 定义流式响应类型 +type StreamAppendStream = + Pin> + Send + 'static>>; + +/// Raft 核心服务实现 +pub struct RaftCoreServiceImpl { + raft: Raft, +} + +impl RaftCoreServiceImpl { + pub fn new(raft: Raft) -> Self { + Self { raft } + } +} + +/// 创建 RaftCoreService 服务器 +pub fn create_core_service( + raft: Raft, +) -> RaftCoreServiceServer { + RaftCoreServiceServer::new(RaftCoreServiceImpl::new(raft)) +} + +#[tonic::async_trait] +impl RaftCoreService for RaftCoreServiceImpl { + type StreamAppendStream = StreamAppendStream; + + /// 请求投票 RPC - 选举阶段使用 + async fn vote(&self, request: Request) -> Result, Status> { + use crate::conversion::{ + log_id_option_to_proto, proto_to_log_id, proto_to_vote, vote_to_proto, + }; + use openraft::raft::VoteRequest as OpenRaftVoteRequest; + + // Proto → OpenRaft + let proto_req = request.into_inner(); + let vote = proto_to_vote(&proto_req.vote.as_ref()); + let last_log_id = proto_to_log_id(&proto_req.last_log_id.as_ref()); + + let raft_req = OpenRaftVoteRequest { vote, last_log_id }; + + // 调用 OpenRaft + let raft_resp = self + .raft + .vote(raft_req) + .await + .map_err(|e| Status::internal(format!("Raft vote error: {}", e)))?; + + // OpenRaft → Proto + let proto_resp = VoteResponse { + vote: Some(vote_to_proto(&raft_resp.vote)), + vote_granted: raft_resp.vote_granted, + last_log_id: log_id_option_to_proto(&raft_resp.last_log_id), + }; + + Ok(Response::new(proto_resp)) + } + + /// 追加日志条目 RPC - 日志复制使用 + async fn append_entries( + &self, + request: Request, + ) -> Result, Status> { + use crate::conversion::{proto_to_log_id, proto_to_vote}; + use openraft::raft::AppendEntriesResponse as OpenRaftAppendEntriesResponse; + + // Proto → OpenRaft + let proto_req = request.into_inner(); + let vote = proto_to_vote(&proto_req.vote.as_ref()); + let prev_log_id = proto_to_log_id(&proto_req.prev_log_id.as_ref()); + + // 转换 entries + let mut entries = Vec::new(); + for proto_entry in &proto_req.entries { + let entry = convert_proto_entry(proto_entry)?; + entries.push(entry); + } + + let leader_commit = proto_to_log_id(&proto_req.leader_commit.as_ref()); + + let raft_req = openraft::raft::AppendEntriesRequest { + vote, + prev_log_id, + entries, + leader_commit, + }; + + // 调用 OpenRaft + let raft_resp = self + .raft + .append_entries(raft_req) + .await + .map_err(|e| Status::internal(format!("Raft append_entries error: {}", e)))?; + + // OpenRaft → Proto + // 这些都应该返回 success=false,而不是 gRPC 错误 + match raft_resp { + OpenRaftAppendEntriesResponse::Success => { + Ok(Response::new(AppendEntriesResponse { success: true })) + } + OpenRaftAppendEntriesResponse::PartialSuccess(_) => { + // PartialSuccess 表示部分成功,对客户端来说算成功 + Ok(Response::new(AppendEntriesResponse { success: true })) + } + OpenRaftAppendEntriesResponse::Conflict => { + // Conflict 是正常的协议响应:日志不匹配 + Ok(Response::new(AppendEntriesResponse { success: false })) + } + OpenRaftAppendEntriesResponse::HigherVote(_) => { + // HigherVote 是正常的协议响应:对方有更高的 term 或 vote + Ok(Response::new(AppendEntriesResponse { success: false })) + } + } + } + + /// 流式追加日志 RPC - pipeline 复制 + /// + /// # 当前状态 + /// 此 RPC 尚未实现,返回 `Status::unimplemented`。 + /// openraft 在某些场景下会依赖 stream_append 做 pipeline 复制。 + /// 如果这个 RPC 不工作,节点间日志复制会回退到同步方式,影响性能。 + /// 将在后续版本中实现流水线复制优化。 + async fn stream_append( + &self, + _request: Request>, + ) -> Result, Status> { + Err(Status::unimplemented( + "StreamAppend RPC is not implemented yet: nodes will use synchronous log replication", + )) + } + + /// 安装快照 RPC + /// + /// # 当前状态 - 临时 workaround + /// 此 RPC 尚未实现。当前实现消费整个流然后返回 `failed_precondition` 错误。 + /// + /// ## 已知问题 + /// - 客户端会接收到 `failed_precondition` 错误,看起来像服务端不支持此 RPC + /// - 但实际上服务端接收了所有数据只是丢弃了 + /// - 如果将来实现了快照安装,这个 workaround 会被删除 + async fn install_snapshot( + &self, + request: Request>, + ) -> Result, Status> { + // FIXME: 实现快照安装 + // 当前故意丢弃所有快照分片,将来实现时必须删除此 workaround + let mut stream = request.into_inner(); + while let Some(_chunk) = stream.message().await? { + // 丢弃所有快照分片 + } + + Err(Status::failed_precondition( + "InstallSnapshot RPC is not implemented yet: received snapshot data was discarded", + )) + } +} + +/// 转换 Proto Entry 到 OpenRaft Entry +fn convert_proto_entry( + proto_entry: &ProtoEntry, +) -> Result, Status> { + use crate::conversion::proto_to_log_id; + use conf::raft_type::Binlog; + use openraft::Membership; + use openraft::entry::{Entry, EntryPayload}; + use std::collections::BTreeMap; + use std::collections::BTreeSet; + + let log_id = proto_to_log_id(&proto_entry.log_id.as_ref()) + .ok_or_else(|| Status::invalid_argument("missing log_id in entry"))?; + + let payload = match &proto_entry.payload { + Some(p) => match &p.payload { + Some(entry_payload::Payload::Blank(_)) => EntryPayload::Blank, + Some(entry_payload::Payload::Normal(normal)) => { + // 反序列化 Binlog + match bincode::deserialize::(&normal.data) { + Ok(binlog) => EntryPayload::Normal(binlog), + Err(e) => { + return Err(Status::invalid_argument(format!( + "failed to deserialize binlog: {}", + e + ))); + } + } + } + Some(entry_payload::Payload::Membership(membership)) => { + // 转换 Membership + let node_ids: BTreeSet = membership.node_ids.iter().copied().collect(); + + // 构建 BTreeMap + let mut nodes = BTreeMap::new(); + for node_config in &membership.nodes { + use conf::raft_type::KiwiNode; + let kiwi_node = KiwiNode { + raft_addr: node_config.raft_addr.clone(), + resp_addr: node_config.resp_addr.clone(), + }; + nodes.insert(node_config.node_id, kiwi_node); + } + + EntryPayload::Membership(Membership::new(vec![node_ids], nodes)) + } + None => return Err(Status::invalid_argument("empty payload")), + }, + None => return Err(Status::invalid_argument("empty payload")), + }; + + Ok(Entry { log_id, payload }) +} diff --git a/src/raft/src/grpc/mod.rs b/src/raft/src/grpc/mod.rs new file mode 100644 index 00000000..3324e990 --- /dev/null +++ b/src/raft/src/grpc/mod.rs @@ -0,0 +1,32 @@ +// Copyright (c) 2024-present, arana-db Community. All rights reserved. +// +// 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. + +// gRPC 服务模块 - 模块化实现 +// +// 该模块包含所有 gRPC 服务的实现,按服务类型分为三个子模块: +// - core: RaftCoreService (Vote, AppendEntries, StreamAppend, InstallSnapshot) +// - admin: RaftAdminService (Initialize, AddLearner, ChangeMembership, RemoveNode) +// - client: RaftClientService + RaftMetricsService (Write, Read, Metrics, Leader, Members) + +pub mod admin; +pub mod client; +pub mod core; + +// 导出服务创建器,便于 main.rs 使用 +pub use admin::create_admin_service; +pub use client::{create_client_service, create_metrics_service}; +pub use core::create_core_service; diff --git a/src/raft/src/lib.rs b/src/raft/src/lib.rs index ae672a1f..786e9b07 100644 --- a/src/raft/src/lib.rs +++ b/src/raft/src/lib.rs @@ -15,22 +15,29 @@ // See the License for the specific language governing permissions and // limitations under the License. -pub mod api; pub mod cf_tracker; pub mod collector; +pub mod conversion; pub mod db_access; pub mod event_listener; +pub mod grpc; pub mod log_store; pub mod log_store_rocksdb; pub mod network; pub mod node; pub mod state_machine; +pub mod raft_proto { + tonic::include_proto!("raft_proto"); + pub const FILE_DESCRIPTOR_SET: &[u8] = + tonic::include_file_descriptor_set!("raft_proto_descriptor"); +} + pub mod table_properties; pub mod types; -pub use cf_tracker::{LogIndexOfColumnFamilies, SmallestIndexRes}; -pub use collector::LogIndexAndSequenceCollector; -pub use event_listener::LogIndexAndSequenceCollectorPurger; +pub use crate::cf_tracker::{LogIndexOfColumnFamilies, SmallestIndexRes}; +pub use crate::collector::LogIndexAndSequenceCollector; +pub use crate::event_listener::LogIndexAndSequenceCollectorPurger; pub use table_properties::{ LogIndexTablePropertiesCollectorFactory, PROPERTY_KEY, get_largest_log_index_from_collection, read_stats_from_table_props, diff --git a/src/raft/src/network.rs b/src/raft/src/network.rs index ff0311b5..34cb5d3b 100644 --- a/src/raft/src/network.rs +++ b/src/raft/src/network.rs @@ -15,6 +15,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::raft_proto::raft_core_service_client::RaftCoreServiceClient; use conf::raft_type::{KiwiNode, KiwiTypeConfig}; use openraft::error::{NetworkError, RaftError}; use openraft::network::{RPCOption, RaftNetwork, RaftNetworkFactory}; @@ -22,10 +23,14 @@ use openraft::raft::{ AppendEntriesRequest, AppendEntriesResponse, InstallSnapshotRequest, InstallSnapshotResponse, VoteRequest, VoteResponse, }; -use reqwest::Client; use std::io; +use std::sync::Arc; +use tokio::sync::Mutex; +use tokio::sync::RwLock; +use tonic::Request as TonicRequest; +use tonic::transport::Channel; -// 类型别名,简化 RaftNetwork 的返回类型(参考 openraft 示例) +// 类型别名,简化 RaftNetwork 的返回类型 type NodeId = ::NodeId; type Node = KiwiNode; type RPCErr = openraft::error::RPCError>; @@ -36,16 +41,13 @@ type RPCErrSnapshot = openraft::error::RPCError< >; pub struct KiwiNetworkFactory { - client: Client, + networks: Arc>>>, } impl KiwiNetworkFactory { pub fn new() -> Self { Self { - client: Client::builder() - .timeout(std::time::Duration::from_secs(5)) - .build() - .expect("Failed to create HTTP client"), + networks: Arc::new(RwLock::new(std::collections::HashMap::new())), } } } @@ -59,56 +61,111 @@ impl Default for KiwiNetworkFactory { impl RaftNetworkFactory for KiwiNetworkFactory { type Network = KiwiNetwork; - async fn new_client(&mut self, _target: NodeId, node: &Node) -> Self::Network { - KiwiNetwork { - client: self.client.clone(), - target_addr: node.raft_addr.clone(), + async fn new_client(&mut self, target: NodeId, node: &Node) -> Self::Network { + // Get the read lock to check if a client already exists for the target node + let networks = self.networks.read().await; + if let Some(network) = networks.get(&target) { + return KiwiNetwork { + client: Arc::clone(&network.client), + }; } + + // Drop the read lock before acquiring the write lock + drop(networks); + + // Acquire the write lock to create a new client for the target node + let mut networks = self.networks.write().await; + + // Double-check if another async task has already created the client while we were waiting for the write lock + if let Some(network) = networks.get(&target) { + return KiwiNetwork { + client: Arc::clone(&network.client), + }; + } + + // Get or create gRPC client for the target node + let addr = node.raft_addr.clone(); + + // 地址格式错误时直接 panic,而不是静默 fallback 到无效地址 + let endpoint = tonic::transport::Endpoint::from_shared(format!("http://{}", addr)) + .unwrap_or_else(|e| { + panic!( + "invalid raft address '{}': failed to parse as gRPC endpoint: {}", + addr, e + ) + }); + + let endpoint = endpoint + .connect_timeout(std::time::Duration::from_secs(5)) + .timeout(std::time::Duration::from_secs(30)) + .connect_lazy(); + let client = RaftCoreServiceClient::new(endpoint); + + let network = KiwiNetwork { + client: Arc::new(Mutex::new(client)), + }; + + // 存入缓存(存入 Arc 包装的版本以便共享) + networks.insert(target, Arc::new(network.clone())); + + // 返回克隆而非 Arc + network } } +/// KiwiNetwork 实现 RaftNetwork trait,用于与远程 Raft 节点通信。 +/// +/// 注意:此结构体不能跨线程共享。内部使用 `Arc>` 来让 gRPC 客户端 +/// 可在被多个 async task 共享的同时保持线程安全,但 Clone 实现是私有的, +/// 防止外部代码克隆并造成并发 mutability 问题。 pub struct KiwiNetwork { - client: Client, - target_addr: String, + client: Arc>>, } -impl KiwiNetwork { - async fn post(&self, endpoint: &str, req: &Req) -> Result - where - Req: serde::Serialize, - Res: serde::de::DeserializeOwned, - { - let url = format!("http://{}{}", self.target_addr, endpoint); - - let resp = self - .client - .post(&url) - .json(req) - .send() - .await - .map_err(|e| RPCErr::Network(NetworkError::new(&e)))?; - - if !resp.status().is_success() { - let status = resp.status(); - let body = resp.text().await.unwrap_or_default(); - return Err(RPCErr::Network(NetworkError::new(&io::Error::other( - format!("HTTP error {}: {}", status, body), - )))); +// 私有 Clone 实现,仅供内部工厂使用 +impl Clone for KiwiNetwork { + fn clone(&self) -> Self { + Self { + client: Arc::clone(&self.client), } - - resp.json() - .await - .map_err(|e| RPCErr::Network(NetworkError::new(&e))) } } +// Impl the RaftNetwork trait for KiwiNetwork according to openraft requirements impl RaftNetwork for KiwiNetwork { async fn append_entries( &mut self, rpc: AppendEntriesRequest, _option: RPCOption, ) -> Result, RPCErr> { - self.post("/raft/append", &rpc).await + // OpenRaft → Proto + let proto_req: crate::raft_proto::AppendEntriesRequest = rpc.try_into().map_err(|e| { + RPCErr::Network(NetworkError::new(&io::Error::new( + io::ErrorKind::InvalidData, + format!("failed to convert AppendEntriesRequest: {}", e), + ))) + })?; + + // 调用 gRPC + let mut client = self.client.lock().await; + let response = client + .append_entries(TonicRequest::new(proto_req)) + .await + .map_err(|e| { + RPCErr::Network(NetworkError::new(&io::Error::new( + io::ErrorKind::ConnectionRefused, + format!("gRPC error: {}", e), + ))) + })?; + + let proto_resp = response.into_inner(); + + if proto_resp.success { + Ok(openraft::raft::AppendEntriesResponse::Success) + } else { + // TODO: 完善返回错误信息,目前仅返回 Conflict,后续可以根据实际情况返回更详细的错误类型 + Ok(openraft::raft::AppendEntriesResponse::Conflict) + } } async fn install_snapshot( @@ -127,6 +184,29 @@ impl RaftNetwork for KiwiNetwork { rpc: VoteRequest, _option: RPCOption, ) -> Result, RPCErr> { - self.post("/raft/vote", &rpc).await + // OpenRaft → Proto + let proto_req: crate::raft_proto::VoteRequest = rpc.into(); + + // 调用 gRPC + let mut client = self.client.lock().await; + let response = client + .vote(TonicRequest::new(proto_req)) + .await + .map_err(|e| { + RPCErr::Network(NetworkError::new(&io::Error::new( + io::ErrorKind::ConnectionRefused, + format!("gRPC error: {}", e), + ))) + })?; + + let proto_resp = response.into_inner(); + + // Proto → OpenRaft + (&proto_resp).try_into().map_err(|e| { + RPCErr::Network(NetworkError::new(&io::Error::new( + io::ErrorKind::InvalidData, + format!("Failed to convert vote response: {}", e), + ))) + }) } } diff --git a/src/raft/src/node.rs b/src/raft/src/node.rs index 5955f1f6..42242320 100644 --- a/src/raft/src/node.rs +++ b/src/raft/src/node.rs @@ -20,9 +20,16 @@ use openraft::{Config, Raft}; use std::path::PathBuf; use std::sync::Arc; +use crate::grpc::{ + create_admin_service, create_client_service, create_core_service, create_metrics_service, +}; use crate::log_store::LogStore; use crate::log_store_rocksdb::RocksdbLogStore; use crate::network::KiwiNetworkFactory; +use crate::raft_proto::raft_admin_service_server::RaftAdminServiceServer; +use crate::raft_proto::raft_client_service_server::RaftClientServiceServer; +use crate::raft_proto::raft_core_service_server::RaftCoreServiceServer; +use crate::raft_proto::raft_metrics_service_server::RaftMetricsServiceServer; use crate::state_machine::KiwiStateMachine; use storage::storage::Storage; @@ -55,7 +62,30 @@ impl RaftApp { pub async fn client_write(&self, binlog: Binlog) -> Result { let res = self.raft.client_write(binlog).await?; - Ok(res.data) + // 从响应中提取 log_id + let log_id = Some(res.log_id.index); + Ok(BinlogResponse { + success: res.data.success, + message: res.data.message, + log_id, + }) + } + + /// 创建所有 gRPC 服务 + pub fn create_grpc_services( + app: Arc, + ) -> ( + RaftCoreServiceServer, + RaftAdminServiceServer, + RaftClientServiceServer, + RaftMetricsServiceServer, + ) { + ( + create_core_service(app.raft.clone()), + create_admin_service(app.clone()), + create_client_service(app.clone()), + create_metrics_service(app), + ) } } @@ -80,7 +110,7 @@ impl Default for RaftConfig { data_dir: PathBuf::from("/tmp/kiwi/raft"), heartbeat_interval: 200, election_timeout_min: 500, - election_timeout_max: 1000, + election_timeout_max: 1500, use_memory_log_store: false, } } diff --git a/src/server/Cargo.toml b/src/server/Cargo.toml index f4d1a298..1eb37915 100644 --- a/src/server/Cargo.toml +++ b/src/server/Cargo.toml @@ -20,7 +20,8 @@ log.workspace = true conf.workspace = true clap = { version = "4.0", features = ["derive"] } raft.workspace = true -actix-web = "4" +tonic = "0.14" +tonic-reflection = { version = "0.14", features = ["server"] } [lints] diff --git a/src/server/src/main.rs b/src/server/src/main.rs index 07aa225e..22a22682 100644 --- a/src/server/src/main.rs +++ b/src/server/src/main.rs @@ -25,12 +25,8 @@ use std::sync::Arc; use storage::StorageOptions; use storage::storage::Storage; -use actix_web::{App, HttpServer, web}; -use raft::api::{ - RaftAppData, add_learner, change_membership, init, leader, metrics, raft_append, raft_vote, - read, write, -}; -use raft::node::{RaftConfig, create_raft_node}; +use raft::node::{RaftApp, RaftConfig, create_raft_node}; +use raft::raft_proto; /// Kiwi - A Redis-compatible key-value database built in Rust #[derive(Parser)] @@ -237,15 +233,16 @@ async fn start_server( }); if let Some(raft_config) = &config.raft { - info!("Starting Raft HTTP server on {}", raft_config.raft_addr); - let raft_config = RaftConfig { node_id: raft_config.node_id, raft_addr: raft_config.raft_addr.clone(), resp_addr: raft_config.resp_addr.clone(), data_dir: PathBuf::from(&raft_config.data_dir), + // 使用配置值或默认值 + heartbeat_interval: raft_config.heartbeat_interval_ms.unwrap_or(200), + election_timeout_min: raft_config.election_timeout_min_ms.unwrap_or(500), + election_timeout_max: raft_config.election_timeout_max_ms.unwrap_or(1500), use_memory_log_store: raft_config.use_memory_log_store, - ..Default::default() }; let raft_app = create_raft_node(raft_config, storage.clone()) @@ -253,33 +250,39 @@ async fn start_server( .map_err(|e| std::io::Error::other(format!("Failed to create Raft node: {}", e)))?; let raft_addr = raft_app.raft_addr.clone(); - let app_data = web::Data::new(RaftAppData { app: raft_app }); + let grpc_addr = raft_addr.parse::().map_err(|e| { + std::io::Error::other(format!("Invalid Raft address '{}': {}", raft_addr, e)) + })?; + + // 创建所有 gRPC 服务 + let (core_svc, admin_svc, client_svc, metrics_svc) = + RaftApp::create_grpc_services(raft_app.clone()); + + info!("Starting Raft gRPC server on {}", raft_addr); + + let reflect_svc = tonic_reflection::server::Builder::configure() + .register_encoded_file_descriptor_set(raft_proto::FILE_DESCRIPTOR_SET) + .build_v1() + .map_err(|e| { + std::io::Error::other(format!("Failed to create reflection service: {}", e)) + })?; + // 启动 gRPC 服务器 tokio::spawn(async move { - info!("Starting Raft HTTP server..."); - let server = match HttpServer::new(move || { - App::new() - .app_data(app_data.clone()) - .service(write) - .service(read) - .service(metrics) - .service(leader) - .service(init) - .service(add_learner) - .service(change_membership) - .service(raft_vote) - .service(raft_append) - }) - .bind(&raft_addr) + use tonic::transport::Server; + + info!("Raft gRPC server listening on {}", grpc_addr); + + if let Err(e) = Server::builder() + .add_service(reflect_svc) + .add_service(core_svc) + .add_service(admin_svc) + .add_service(client_svc) + .add_service(metrics_svc) + .serve(grpc_addr) + .await { - Ok(s) => s, - Err(e) => { - error!("Failed to bind Raft HTTP server: {}", e); - return; - } - }; - if let Err(e) = server.run().await { - error!("Raft HTTP server error: {}", e); + error!("Raft gRPC server error: {}", e); } }); }