From 531c846861df11362ce4b1a9fce9f5bdc0f872fd Mon Sep 17 00:00:00 2001 From: Adrien Zinger Date: Sat, 2 Dec 2023 10:50:01 +0100 Subject: [PATCH] Rework about the full solution + Add an example which can be used to test + Fix most of typo mistakes + Remove dyn timeout, fix commit filter, modify term construction + Test and fix script calls + Merge term management with log_entries + Simplify status management + Remove remote follower acknowledgement logic + Remove the aggregation mechanism for now It also stop to lead when quorum is unreachable for that version. --- Cargo.lock | 418 +++++-- README.md | 197 ++-- example/hook/.gitignore | 4 + example/hook/Cargo.lock | 1260 +++++++++++++++++++++ example/hook/Makefile | 12 + example/hook/src/main.rs | 11 + example/hook/test/.gitignore | 1 + example/hook/test/run.sh | 82 ++ example/hook/test/server1/append_term | 3 + example/hook/test/server1/commit_term | 2 + example/hook/test/server1/pre_append_term | 16 + example/hook/test/server1/prepare_term | 3 + example/hook/test/server1/retreive_n_term | 13 + example/hook/test/server1/retreive_term | 3 + example/hook/test/server1/switch_status | 2 + example/hook/test/server2/append_term | 3 + example/hook/test/server2/commit_term | 2 + example/hook/test/server2/pre_append_term | 16 + example/hook/test/server2/prepare_term | 3 + example/hook/test/server2/retreive_n_term | 13 + example/hook/test/server2/retreive_term | 3 + example/hook/test/server2/switch_status | 2 + example/hook/test/server3/append_term | 3 + example/hook/test/server3/commit_term | 2 + example/hook/test/server3/pre_append_term | 16 + example/hook/test/server3/prepare_term | 3 + example/hook/test/server3/retreive_n_term | 13 + example/hook/test/server3/retreive_term | 3 + example/hook/test/server3/switch_status | 2 + example/hook/test/server4/append_term | 3 + example/hook/test/server4/commit_term | 2 + example/hook/test/server4/pre_append_term | 16 + example/hook/test/server4/prepare_term | 3 + example/hook/test/server4/retreive_n_term | 13 + example/hook/test/server4/retreive_term | 3 + example/hook/test/server4/switch_status | 2 + example/hook/test/server5/append_term | 3 + example/hook/test/server5/commit_term | 2 + example/hook/test/server5/pre_append_term | 16 + example/hook/test/server5/prepare_term | 3 + example/hook/test/server5/retreive_n_term | 13 + example/hook/test/server5/retreive_term | 3 + example/hook/test/server5/switch_status | 2 + src/api/client.rs | 13 +- src/api/io_msg.rs | 12 +- src/api/server.rs | 50 +- src/api/tests/mod.rs | 6 +- src/common/config.rs | 37 +- src/common/error.rs | 6 +- src/common/hook_trait.rs | 20 +- src/common/scripts.rs | 204 +++- src/lib.rs | 1 + src/log_entry.rs | 231 ++-- src/node.rs | 127 +-- src/state/mod.rs | 154 ++- src/state/node.rs | 54 +- src/state/sm_impl.rs | 99 -- src/workflow/append_term.rs | 252 +++-- src/workflow/candidate.rs | 85 +- src/workflow/follower.rs | 51 +- src/workflow/init.rs | 157 +-- src/workflow/leader.rs | 258 ++--- src/workflow/mod.rs | 2 +- src/workflow/request_vote.rs | 26 +- src/workflow/test/tests_send_term.rs | 22 +- src/workflow/tools/commiting.rs | 48 +- src/workflow/tools/leader_tools.rs | 143 ++- 67 files changed, 3029 insertions(+), 1224 deletions(-) create mode 100644 example/hook/.gitignore create mode 100644 example/hook/Cargo.lock create mode 100644 example/hook/Makefile create mode 100644 example/hook/src/main.rs create mode 100644 example/hook/test/.gitignore create mode 100755 example/hook/test/run.sh create mode 100755 example/hook/test/server1/append_term create mode 100755 example/hook/test/server1/commit_term create mode 100755 example/hook/test/server1/pre_append_term create mode 100755 example/hook/test/server1/prepare_term create mode 100755 example/hook/test/server1/retreive_n_term create mode 100755 example/hook/test/server1/retreive_term create mode 100755 example/hook/test/server1/switch_status create mode 100755 example/hook/test/server2/append_term create mode 100755 example/hook/test/server2/commit_term create mode 100755 example/hook/test/server2/pre_append_term create mode 100755 example/hook/test/server2/prepare_term create mode 100755 example/hook/test/server2/retreive_n_term create mode 100755 example/hook/test/server2/retreive_term create mode 100755 example/hook/test/server2/switch_status create mode 100755 example/hook/test/server3/append_term create mode 100755 example/hook/test/server3/commit_term create mode 100755 example/hook/test/server3/pre_append_term create mode 100755 example/hook/test/server3/prepare_term create mode 100755 example/hook/test/server3/retreive_n_term create mode 100755 example/hook/test/server3/retreive_term create mode 100755 example/hook/test/server3/switch_status create mode 100755 example/hook/test/server4/append_term create mode 100755 example/hook/test/server4/commit_term create mode 100755 example/hook/test/server4/pre_append_term create mode 100755 example/hook/test/server4/prepare_term create mode 100755 example/hook/test/server4/retreive_n_term create mode 100755 example/hook/test/server4/retreive_term create mode 100755 example/hook/test/server4/switch_status create mode 100755 example/hook/test/server5/append_term create mode 100755 example/hook/test/server5/commit_term create mode 100755 example/hook/test/server5/pre_append_term create mode 100755 example/hook/test/server5/prepare_term create mode 100755 example/hook/test/server5/retreive_n_term create mode 100755 example/hook/test/server5/retreive_term create mode 100755 example/hook/test/server5/switch_status delete mode 100644 src/state/sm_impl.rs diff --git a/Cargo.lock b/Cargo.lock index 7328707..a85593d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,42 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "ahash" version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "ansi_term" version = "0.12.1" @@ -17,12 +47,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "anyhow" -version = "1.0.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27" - [[package]] name = "async-trait" version = "0.1.52" @@ -31,7 +55,7 @@ checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.84", ] [[package]] @@ -40,6 +64,21 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "base64" version = "0.13.0" @@ -73,6 +112,12 @@ dependencies = [ "byte-tools", ] +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + [[package]] name = "byte-tools" version = "0.3.1" @@ -91,12 +136,35 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets", +] + [[package]] name = "config" version = "0.12.0" @@ -116,6 +184,12 @@ dependencies = [ "yaml-rust", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + [[package]] name = "digest" version = "0.8.1" @@ -134,17 +208,6 @@ dependencies = [ "rand", ] -[[package]] -name = "dyn-timeout" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65ef802ca835c9ee71cf55cfce7ed1938d3d89d7f6aa4de6bad115496d53ecd6" -dependencies = [ - "anyhow", - "lazy_static", - "tokio", -] - [[package]] name = "fake-simd" version = "0.1.2" @@ -213,14 +276,20 @@ checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.10.2+wasi-snapshot-preview1", ] +[[package]] +name = "gimli" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" + [[package]] name = "h2" -version = "0.3.11" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9f1f717ddc7b2ba36df7e871fd88db79326551d3d6f1fc406fbfd28b582ff8e" +checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" dependencies = [ "bytes", "fnv", @@ -263,8 +332,8 @@ dependencies = [ name = "hook-raft" version = "0.1.0" dependencies = [ + "chrono", "config", - "dyn-timeout", "hyper", "lazy_static", "rand", @@ -301,9 +370,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9100414882e15fb7feccb4897e5f0ff0ff1ca7d1a86a23208ada4d7a18e6c6c4" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" @@ -313,9 +382,9 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" [[package]] name = "hyper" -version = "0.14.17" +version = "0.14.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "043f0e083e9901b6cc658a77d1eb86f4fc650bbb977a4337dd63192826aa85dd" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" dependencies = [ "bytes", "futures-channel", @@ -335,6 +404,29 @@ dependencies = [ "want", ] +[[package]] +name = "iana-time-zone" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "indexmap" version = "1.8.0" @@ -360,6 +452,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" +[[package]] +name = "js-sys" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "json5" version = "0.4.1" @@ -379,9 +480,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.119" +version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" [[package]] name = "linked-hash-map" @@ -435,25 +536,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] -name = "mio" -version = "0.8.0" +name = "miniz_oxide" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba272f85fa0b41fc91872be579b3bbe0f56b792aa361a380eb669469f68dafb2" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ - "libc", - "log", - "miow", - "ntapi", - "winapi", + "adler", ] [[package]] -name = "miow" -version = "0.3.7" +name = "mio" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" dependencies = [ - "winapi", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.48.0", ] [[package]] @@ -468,12 +567,12 @@ dependencies = [ ] [[package]] -name = "ntapi" -version = "0.3.7" +name = "num-traits" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ - "winapi", + "autocfg", ] [[package]] @@ -486,11 +585,20 @@ dependencies = [ "libc", ] +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" -version = "1.9.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "opaque-debug" @@ -553,7 +661,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-sys", + "windows-sys 0.32.0", ] [[package]] @@ -591,7 +699,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn", + "syn 1.0.84", ] [[package]] @@ -632,7 +740,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", - "syn", + "syn 1.0.84", "version_check", ] @@ -649,18 +757,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.34" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f84e92c0f7c9d58328b85a78557813e4bd845130db68d7184635344399423b1" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] name = "quote" -version = "1.0.10" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -749,6 +857,12 @@ dependencies = [ "ordered-multimap", ] +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + [[package]] name = "rustversion" version = "1.0.6" @@ -784,7 +898,7 @@ checksum = "ecc0db5cb2556c0e558887d9bbdcf6ac4471e83ff66cf696e5419024d1606276" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.84", ] [[package]] @@ -819,7 +933,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn", + "syn 1.0.84", ] [[package]] @@ -866,9 +980,9 @@ checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" [[package]] name = "socket2" -version = "0.4.4" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" dependencies = [ "libc", "winapi", @@ -885,6 +999,17 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "syn" +version = "2.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "thread_local" version = "1.1.4" @@ -896,47 +1021,47 @@ dependencies = [ [[package]] name = "tokio" -version = "1.17.0" +version = "1.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee" +checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" dependencies = [ + "autocfg", + "backtrace", "bytes", "libc", - "memchr", "mio", "num_cpus", - "once_cell", "parking_lot 0.12.0", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", - "winapi", + "windows-sys 0.48.0", ] [[package]] name = "tokio-macros" -version = "1.7.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.39", ] [[package]] name = "tokio-util" -version = "0.6.9" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" dependencies = [ "bytes", "futures-core", "futures-sink", - "log", "pin-project-lite", "tokio", + "tracing", ] [[package]] @@ -974,7 +1099,7 @@ checksum = "2e65ce065b4b5c53e73bb28912318cb8c9e9ad3921f1d669eb0e68b4c8143a2b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.84", ] [[package]] @@ -1036,7 +1161,7 @@ checksum = "4801dca35e4e2cee957c469bd4a1c370fadb7894c0d50721a40eba3523e6e91c" dependencies = [ "lazy_static", "quote", - "syn", + "syn 1.0.84", ] [[package]] @@ -1057,6 +1182,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + [[package]] name = "unicode-xid" version = "0.2.2" @@ -1091,6 +1222,66 @@ version = "0.10.2+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.39", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" + [[package]] name = "winapi" version = "0.3.9" @@ -1113,49 +1304,124 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3df6e476185f92a12c072be4a189a0210dcdcf512a1891d6dff9edb874deadc6" dependencies = [ - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_msvc", + "windows_aarch64_msvc 0.32.0", + "windows_i686_gnu 0.32.0", + "windows_i686_msvc 0.32.0", + "windows_x86_64_gnu 0.32.0", + "windows_x86_64_msvc 0.32.0", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc 0.48.5", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_msvc" version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_i686_gnu" version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_msvc" version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_x86_64_gnu" version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_msvc" version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "yaml-rust" version = "0.4.5" diff --git a/README.md b/README.md index 3520689..1860430 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,41 @@ # Hook / Raft Library -Hook is the _Proof Of Concept_ of an implementation of the raft algorithm. It want to divide the work of a node from the raft logic (inspiration from hooks in git). +Hook is an implementation of the raft algorithm. It wants to extracts the +algorithm, just doing the election and manage the logs transfers between +nodes. The implementation follow the rules of the Raft algorithm during his -execution expect for the connection of new nodes. The connection -process is described in [the init specification](./specs/initial flow charts/init.md). +execution. ## _Principia raftica_ -The raft algorithm works with terms, and that's what hook does. By default the terms are empty because it doesn't have to be important in a first place. Even if you'll be able to hack this project and fills terms, the main target of hook is to know who's the leader in the network. - -So Hook will connect all nodes of the network (accepting new nodes with a bootstrap strategy) and send basic pings as terms for the raft. +The raft algorithm works with terms, and that's what hook does. +The hook's goal is to commit a consensual approved a maximum of terms. +By default, terms are empty because it doesn't have to be important in a first +place. But you can choose what's inside a term by using a *hook*. ## Hook-Raft? πŸͺ -That's all? Noooo, I called the project Hook because it implements a logic as the hooks in git repository. +Hook implements a logic as the hooks in a git repository. You're able +to implement some behaviors that interact with the basic Raft algorithm. + +First, it allows any user to define a content of terms. But it can also +interact with the default behaviors. That's let anyone to hack/try some +configuration easily. -There is a trait named *Hook* that you can implement however you want. That trait is given to the hook library through the entry point that is: +There is a trait named *Hook*. That trait is given to the hook library through +the entry point that is: ```rust -/// Start a new node -fn main() { - let rt = tokio::runtime::Runtime::new().expect("Runtime expected to start but failed"); - match Node::new(DefaultHook {}).start(rt) { - Ok(_) => println!("Successfully exit"), - Err(err) => eprintln!("Node crash with error: {:?}", err), - } +pub trait Hook: Send + Sync { + fn update_node(&self) -> bool; + fn pre_append_term(&self, term: &Term) -> Option; + fn append_term(&self, term: &Term) -> bool; + fn commit_term(&self, term: &Term) -> bool; + fn prepare_term(&self) -> String; + fn retreive_term(&self, index: usize) -> Option; + fn retreive_terms(&self, from: usize, to: usize) -> Option>; + fn switch_status(&self, status: EStatus); } ``` @@ -33,106 +43,119 @@ You can also define the settings manually with the function `new_with_settings`, otherwise the library will look at a file named `settings.toml` in the root folder. Look below what are the settings. - The Trait `Hook` can be a default **VOID** with the `DefaulHook` object but can be whatever you want. This object is basically an observer -that the *Raft* algorithm will trigger on change. +that the *Raft* algorithm will trigger any time it require. + +## Default Hook -Let's look at the disponible hooks trigerred: +The default hook binary will react with the following scripts or executable. +All of that script are optional, put a '.sample' extension or remove it to +enable the internal default behavior. ```bash -└── hooks - β”œβ”€β”€ pre-append-term - β”œβ”€β”€ append-term - β”œβ”€β”€ apply-term # todo - β”œβ”€β”€ commit-term - β”œβ”€β”€ update-node - β”œβ”€β”€ leader-change # todo - β”œβ”€β”€ pre-add-connection # todo - β”œβ”€β”€ post-add-connection # todo - β”œβ”€β”€ remove-connection # todo - β”œβ”€β”€ lost-connection # todo - β”œβ”€β”€ request-vote # todo - β”œβ”€β”€ receive-vote # todo - β”œβ”€β”€ prepare-term - └── send-term # todo +└── hook + β”œβ”€β”€ hook.bin + β”œβ”€β”€ append_term + β”œβ”€β”€ commit_term + β”œβ”€β”€ pre_append_term + β”œβ”€β”€ prepare_term + β”œβ”€β”€ retreive_n_term + β”œβ”€β”€ retreive_term + └── switch_status ``` - -- _pre-append-term_: A term append from the leader, the result of the script has to be "true", otherwise the term is rejected (Hook accepts all term by default) -- _apply-term_: You have a term with a content and you want to apply something to a state machine or something? It's here -- _append-term_: A term as just passed all the test and is stored in cache, waiting to be commited. The same term can be append several times -- _commit-term_: The term is considered as definitive by the current leader -- _leader-change_: A new leader won the election -- _pre-add-connection_: A new node want to join the network, the return of the script has to be "true" you accept the connection. All other response is considered has rejected. (Look at [default hook requirement](#default-hook-node-requirements)) -- _post-add-connection_: A connection has been added in the network -- _remove-connection_: A node has been removed from the network -- _lost-connecton_: A node as failed to answer, or the leader failed to send heartbeat -- _request-vote_: Vote requested for a new leader -- _receive-vote_: Received a response for your personal vote request if you are a candidate -- _prepare-term_: If you are the leader, you can fill the content with the of `terms` with this script, the returned value is considered as the full raw content. This script is called `PREPARING_OFFSET` ms before each heartbeat. -- _send-term_: It's call when you're a leader, and you send a term, you can check if you successfully prepare the last term here -- _update-node_: A node require to connect or to change his status (follower/not follower) You can dismiss every connection if you want or limit the number of nodes - -The status of the implementation is watched with the `config` crates and is updated on save in an acceptable time. +- _append_term_: A new term has to be applied. This might be volatile and you + may apply multiple times the same term. That's up to the user to manage his + own logic with that behavior. It takes 2 arguments, the term id and the + content. It doesn't have to dump anything on the standard output. In case of + failure, if you're a follower, remote leader will receive an error, if you're + a leader, you'll turn in idle and start a candidature. +- _commit_term_: The term is considered as definitive by the current leader. + Append once. It takes 2 arguments, the term id and its content. +- _pre_append_term_: A term append from a potential leader but it has to pass the user checks. + It takes 2 arguments, the id of the term and the content. To avoid gaps, the user should put + in the standard output the `latest term id + 1`. The default behavior is to accept gaps and + always print the first argument. +- _prepare_term_: If you are the leader, you can fill the terms by writing in + the standard output there content. Hook cares about its id and its + replication. As a leader, don't append the term now, wait the `append_term` + call. Called each `prepare_term_period` +- _retrieve_term_: If you're a leader, that hook serves to rebuild a term which + isn't in cache anymore. The terms to rebuild are supposed to be committed + previously. It takes 1 argument, the term id. It expect to read the + content of the term in the standard output. If the hook failed, the node + turns in idle until the next election. The default behavior is to create + a new "default" term (a term with default written in the content). +- _retrieve_n_term_: If Hook needs more than one term to rebuild, it will first + try to use that one instead of the *retrieve_term* hook. It takes 2 + arguments, the begin and the end id. It expect to read on the + standard output a JSON formatted list of terms + with the format `[{'id':12,'content':'hello world'}]`. +- _switch_status_: Notification of all changes of status over the time, it + takes one argument "candidate"|"follower"|"leader". It doesn't expect any + output. ### Raft settings -When starting a new node, you can target a settings file. Note that you shouldn't run a node on a network with random settings because you may fail to connect. +When you start a node, you can target a settings file. ```toml -timeout_min = 150 -timeout_max = 300 -# Min and max value in milisecond of the election timeout. Timeout is randomly choosen between these two values - -max_timeout_value = 300 -# Maximum value in millisecond of the heartbeat timeout if you're a potential candidate -min_inc_timeout = 150 -min_inc_timeout = 300 -# Min and max value in millisecond of the random incrementation of ou timeout each time we received a new term. +# Min and max value in milisecond of the election timeout. Timeout is randomly +# choosen between these two values. +timeout_min = 500 +timeout_max = 800 -prepare_term_period = 80 # Value in milisecond that separe term preparations, default 80 # If this time is too short to finish the term preparation, an empty heartbeat # will be send and the content will be used for the next term. The hook doesn't # implement any problem management if you fail multiple times to send a term. # You can manage it yourself with the `send-term` script +prepare_term_period = 80 -nodes = ['12.158.20.36'] -# Optional list of public known nodes in a network. If this list appear to be empty, the node won't connect to anything and will be the current leader of his own network. - -# todo followers = ['15.126.208.72'] -# Optional list of known followers +# List of public known nodes in the network. +nodes = ['12.13.14.15:8080'] -addr = "127.0.0.1" # Server local address, default "127.0.0.1" -port = "3000" +addr = "127.0.0.1" # Port used between nodes to communicate, default "3000" +port = "3000" -follower = true -# If true, the current node will never ask for an election and will never be able to vote. Nevertheless you will receive all heartbeat and all information like a normal node. Some hooks will never be called obviously but you are a part of the network. If false, you will be considered as a potential candidate after a successfull bootstrap and will be able to vote. -# default true +# If true, the current node will never ask for an election. Nevertheless you +# will receive all heartbeat and all information like a normal node. Some hooks +# will never be called obviously but you are a part of the network. If false, +# you will be considered as a potential candidate. +# +# default false +follower = false -response_timeout = 20 -# Value in millisecond before considering that a node will never respond +# Value in millisecond before considering that a remote node will never respond +response_timeout = 200 ``` -## Run The node! - -_todo: can use hook raft as a library, or use a ready to use executable that is in a subproject._ +## Run The node -## Default Hook Node Requirements +That repository contains a rust library with all the tools to make a private +implementation. The basic implementation as simple as: -Hook accepts all connection by default if requirements are ok. +```Rust +use hook_raft::*; -Rejected causes: - -- The node want to be candidate but failed to communicate with another randomly chosen node in the follower list (I don't know if it's really useful) -- One of the nodes rejected your connection - -## Bootstrapping - -The bootstrap system isn't managed here. If you want to implement a bootstrap strategy, you can develop it your own server who use hook and check in the `update-node` script if the node who attempt to connect fill requirements. +/// Start a new node +fn main() { + let rt = tokio::runtime::Runtime::new().expect("Runtime expected to start but failed"); + match Node::new(DefaultHook {}).start(rt) { + Ok(_) => println!("Successfully exit"), + Err(err) => eprintln!("Node crash with error: {:?}", err), + } +} +``` -However, _Hook-Raft_ implementation is bootstrap friendly and will send by default all logs commited from the latest log known by the connecting node. That part is, after all, suceptible to move soon with the nexts _PR_! The ideas tempt to extract that process of logs retreivals from here. So in the future, the connecting node will certainly receive only the latest log commited. +## Some information +- Hook nodes communication is over HTTP. +- Hook scripts have to be executable by the local user to work properly. +- The default binary is agnostic to the content of terms. The diffusion, the reason + of why it's diffused, and the usage of the content is deferred to the user. +- Bootstrapping isn't managed. As well as the change of the cluster membership and + the log compaction. diff --git a/example/hook/.gitignore b/example/hook/.gitignore new file mode 100644 index 0000000..b525126 --- /dev/null +++ b/example/hook/.gitignore @@ -0,0 +1,4 @@ +/target +term_* +*.log +test/hook diff --git a/example/hook/Cargo.lock b/example/hook/Cargo.lock new file mode 100644 index 0000000..695c9b8 --- /dev/null +++ b/example/hook/Cargo.lock @@ -0,0 +1,1260 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0453232ace82dee0dd0b4c87a59bd90f7b53b314f3e0f61fe2ee7c8a16482289" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + +[[package]] +name = "async-trait" +version = "0.1.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[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 = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets", +] + +[[package]] +name = "config" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54ad70579325f1a38ea4c13412b82241c5900700a69785d73e2736bd65a33f86" +dependencies = [ + "async-trait", + "json5", + "lazy_static", + "nom", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml", + "yaml-rust", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "cpufeatures" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[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 = "dlv-list" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68df3f2b690c1b86e65ef7830956aededf3cb0a16f898f79b9a6f421a7b6211b" +dependencies = [ + "rand", +] + +[[package]] +name = "dyn-timeout" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65ef802ca835c9ee71cf55cfce7ed1938d3d89d7f6aa4de6bad115496d53ecd6" +dependencies = [ + "anyhow", + "lazy_static", + "tokio", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "futures-channel" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" + +[[package]] +name = "futures-sink" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" + +[[package]] +name = "futures-task" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" + +[[package]] +name = "futures-util" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[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.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" + +[[package]] +name = "h2" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" + +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + +[[package]] +name = "hook" +version = "0.1.0" +dependencies = [ + "hook-raft", + "tokio", +] + +[[package]] +name = "hook-raft" +version = "0.1.0" +dependencies = [ + "chrono", + "config", + "dyn-timeout", + "hyper", + "lazy_static", + "rand", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "http" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.4.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown 0.14.2", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "js-sys" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "ordered-multimap" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c672c7ad9ec066e428c00eb917124a06f08db19e2584de982cc34b1f4c12485" +dependencies = [ + "dlv-list", + "hashbrown 0.9.1", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + +[[package]] +name = "pest" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae9cee2a55a544be8b89dc6848072af97a20f2422603c10865be2a42b580fff5" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81d78524685f5ef2a3b3bd1cafbc9fcabb036253d9b1463e726a91cd16e2dfc2" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68bd1206e71118b5356dae5ddc61c8b11e28b09ef6a31acbd15ea48a28e0c227" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c747191d4ad9e4a4ab9c8798f1e82a39affe7ef9648390b7e5548d18e099de6" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ron" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a" +dependencies = [ + "base64", + "bitflags", + "serde", +] + +[[package]] +name = "rust-ini" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63471c4aa97a1cf8332a5f97709a79a4234698de6a1f5087faf66f2dae810e22" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.192" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.192" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" + +[[package]] +name = "socket2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "syn" +version = "2.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tokio" +version = "1.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.5.5", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] diff --git a/example/hook/Makefile b/example/hook/Makefile new file mode 100644 index 0000000..1912ebc --- /dev/null +++ b/example/hook/Makefile @@ -0,0 +1,12 @@ +.PHONY: test +test: cargo_build_debug test/hook + cd test && sudo ./run.sh + +test/hook: target/debug/hook + cp target/debug/hook test/hook + +target/debug/hook: cargo_build_debug + +.PHONY: cargo_build_debug +cargo_build_debug: + cargo build diff --git a/example/hook/src/main.rs b/example/hook/src/main.rs new file mode 100644 index 0000000..78cb677 --- /dev/null +++ b/example/hook/src/main.rs @@ -0,0 +1,11 @@ +use hook_raft::*; + +/// Start a new node +fn main() { + let rt = tokio::runtime::Runtime::new().expect("Runtime expected to start but failed"); + match Node::new(DefaultHook {}).start(rt) { + Ok(_) => println!("Successfully exit"), + Err(err) => eprintln!("Node crash with error: {:?}", err), + } +} + diff --git a/example/hook/test/.gitignore b/example/hook/test/.gitignore new file mode 100644 index 0000000..397b4a7 --- /dev/null +++ b/example/hook/test/.gitignore @@ -0,0 +1 @@ +*.log diff --git a/example/hook/test/run.sh b/example/hook/test/run.sh new file mode 100755 index 0000000..c0209aa --- /dev/null +++ b/example/hook/test/run.sh @@ -0,0 +1,82 @@ +subnet=255.255.255.248/29 +img=dokken/centos-stream-9 +volume=./:/servers + +net=hook-net + +flags="--network ${net} -dit -v ${volume}" + +ip1=255.255.255.250 +ip2=255.255.255.251 +ip3=255.255.255.252 +ip4=255.255.255.253 +ip5=255.255.255.254 + +srv_port=8080 + +cmd_pre="chmod +x commit_term && \ + chmod +x switch_status && \ + chmod +x append_term && \ + chmod +x pre_append_term && \ + chmod +x retreive_term && \ + chmod +x retreive_n_term && \ + chmod +x prepare_term" + +# Make settings +i=1 +while test -le 5; do + rm -f server$i/settings > /dev/null + cat common_settings.toml >> server$i/settings.toml + cat server$i/server_settings.toml >> server$i/settings.toml + i=$((i + 1)) +done + +timeout=10 +kill_timeout=5 +cmd1="cd /servers/server1 && \ + ${cmd_pre} && \ + timeout ${kill_timeout} ../hook > output.log 2>&1" +cmd2="cd /servers/server2 && \ + ${cmd_pre} && \ + timeout ${kill_timeout} ../hook > output.log 2>&1" +cmd3="cd /servers/server3 && \ + ${cmd_pre} && \ + timeout ${timeout} ../hook > output.log 2>&1" +cmd4="cd /servers/server4 && \ + ${cmd_pre} && \ + timeout ${timeout} ../hook > output.log 2>&1" +cmd5="cd /servers/server5 && \ + ${cmd_pre} && \ + timeout ${timeout} ../hook > output.log 2>&1" + +rm -f server*/*.log > /dev/null &2>1 +rm -f server*/term_* > /dev/null &2>1 +docker rm -f srv1 srv2 srv3 srv4 srv5 > /dev/null &2>1 + +docker network ls | grep ${net} > /dev/null +if test $? = 1; then + docker network create --subnet $subnet ${net} +fi + +docker run $flags --ip $ip1 --expose $srv_port --name srv1 $img bash +docker run $flags --ip $ip2 --expose $srv_port --name srv2 $img bash +docker run $flags --ip $ip3 --expose $srv_port --name srv3 $img bash +docker run $flags --ip $ip4 --expose $srv_port --name srv4 $img bash +docker run $flags --ip $ip5 --expose $srv_port --name srv5 $img bash + +docker exec srv1 bash -c "${cmd1}" & +docker exec srv2 bash -c "${cmd2}" & +docker exec srv3 bash -c "${cmd3}" & +docker exec srv4 bash -c "${cmd4}" & +docker exec srv5 bash -c "${cmd5}" & + +docker logs srv1 +docker logs srv2 +docker logs srv3 +docker logs srv4 +docker logs srv5 + +sleep $timeout + +echo clean containers +docker rm -f srv1 srv2 srv3 srv4 srv5 diff --git a/example/hook/test/server1/append_term b/example/hook/test/server1/append_term new file mode 100755 index 0000000..828dd7a --- /dev/null +++ b/example/hook/test/server1/append_term @@ -0,0 +1,3 @@ +#! /bin/bash + +echo $2 > term_$1 diff --git a/example/hook/test/server1/commit_term b/example/hook/test/server1/commit_term new file mode 100755 index 0000000..cb57924 --- /dev/null +++ b/example/hook/test/server1/commit_term @@ -0,0 +1,2 @@ +#! /bin/bash +echo term $1 $2 commited `date --rfc-3339=n` >> commit.log diff --git a/example/hook/test/server1/pre_append_term b/example/hook/test/server1/pre_append_term new file mode 100755 index 0000000..4602d21 --- /dev/null +++ b/example/hook/test/server1/pre_append_term @@ -0,0 +1,16 @@ +#! /bin/bash + +# Check in local files all terms we got. +# Return the first id we fail to find. +id=1 +while ! test $id < $1 +do + if ! test -f "./term_${id}"; then + echo -n $id + exit 0 + fi + id=$(($id + 1)) +done + +echo -n $1 +exit 0 diff --git a/example/hook/test/server1/prepare_term b/example/hook/test/server1/prepare_term new file mode 100755 index 0000000..a0c182f --- /dev/null +++ b/example/hook/test/server1/prepare_term @@ -0,0 +1,3 @@ +#! /bin/bash + +echo "from server 1: $RANDOM" diff --git a/example/hook/test/server1/retreive_n_term b/example/hook/test/server1/retreive_n_term new file mode 100755 index 0000000..a1bbfa8 --- /dev/null +++ b/example/hook/test/server1/retreive_n_term @@ -0,0 +1,13 @@ +#! /bin/bash + +i=$1 +output="[" +while test $i -lt $2 +do + output="${output}{'id':${i},'content':'`cat term_$i`'}," + i=$(($i+1)) +done + +output="${output}{'id':$2,'content':'`cat term_$i`'}]" + +echo -n $output diff --git a/example/hook/test/server1/retreive_term b/example/hook/test/server1/retreive_term new file mode 100755 index 0000000..ef5dd26 --- /dev/null +++ b/example/hook/test/server1/retreive_term @@ -0,0 +1,3 @@ +#! /bin/bash + +cat term_$1 diff --git a/example/hook/test/server1/switch_status b/example/hook/test/server1/switch_status new file mode 100755 index 0000000..6ad9a26 --- /dev/null +++ b/example/hook/test/server1/switch_status @@ -0,0 +1,2 @@ +#! /bin/bash +echo switch to $1 >> "status.log" diff --git a/example/hook/test/server2/append_term b/example/hook/test/server2/append_term new file mode 100755 index 0000000..828dd7a --- /dev/null +++ b/example/hook/test/server2/append_term @@ -0,0 +1,3 @@ +#! /bin/bash + +echo $2 > term_$1 diff --git a/example/hook/test/server2/commit_term b/example/hook/test/server2/commit_term new file mode 100755 index 0000000..cb57924 --- /dev/null +++ b/example/hook/test/server2/commit_term @@ -0,0 +1,2 @@ +#! /bin/bash +echo term $1 $2 commited `date --rfc-3339=n` >> commit.log diff --git a/example/hook/test/server2/pre_append_term b/example/hook/test/server2/pre_append_term new file mode 100755 index 0000000..4602d21 --- /dev/null +++ b/example/hook/test/server2/pre_append_term @@ -0,0 +1,16 @@ +#! /bin/bash + +# Check in local files all terms we got. +# Return the first id we fail to find. +id=1 +while ! test $id < $1 +do + if ! test -f "./term_${id}"; then + echo -n $id + exit 0 + fi + id=$(($id + 1)) +done + +echo -n $1 +exit 0 diff --git a/example/hook/test/server2/prepare_term b/example/hook/test/server2/prepare_term new file mode 100755 index 0000000..d553bb8 --- /dev/null +++ b/example/hook/test/server2/prepare_term @@ -0,0 +1,3 @@ +#! /bin/bash + +echo "from server 2" diff --git a/example/hook/test/server2/retreive_n_term b/example/hook/test/server2/retreive_n_term new file mode 100755 index 0000000..a1bbfa8 --- /dev/null +++ b/example/hook/test/server2/retreive_n_term @@ -0,0 +1,13 @@ +#! /bin/bash + +i=$1 +output="[" +while test $i -lt $2 +do + output="${output}{'id':${i},'content':'`cat term_$i`'}," + i=$(($i+1)) +done + +output="${output}{'id':$2,'content':'`cat term_$i`'}]" + +echo -n $output diff --git a/example/hook/test/server2/retreive_term b/example/hook/test/server2/retreive_term new file mode 100755 index 0000000..ef5dd26 --- /dev/null +++ b/example/hook/test/server2/retreive_term @@ -0,0 +1,3 @@ +#! /bin/bash + +cat term_$1 diff --git a/example/hook/test/server2/switch_status b/example/hook/test/server2/switch_status new file mode 100755 index 0000000..6ad9a26 --- /dev/null +++ b/example/hook/test/server2/switch_status @@ -0,0 +1,2 @@ +#! /bin/bash +echo switch to $1 >> "status.log" diff --git a/example/hook/test/server3/append_term b/example/hook/test/server3/append_term new file mode 100755 index 0000000..828dd7a --- /dev/null +++ b/example/hook/test/server3/append_term @@ -0,0 +1,3 @@ +#! /bin/bash + +echo $2 > term_$1 diff --git a/example/hook/test/server3/commit_term b/example/hook/test/server3/commit_term new file mode 100755 index 0000000..cb57924 --- /dev/null +++ b/example/hook/test/server3/commit_term @@ -0,0 +1,2 @@ +#! /bin/bash +echo term $1 $2 commited `date --rfc-3339=n` >> commit.log diff --git a/example/hook/test/server3/pre_append_term b/example/hook/test/server3/pre_append_term new file mode 100755 index 0000000..4602d21 --- /dev/null +++ b/example/hook/test/server3/pre_append_term @@ -0,0 +1,16 @@ +#! /bin/bash + +# Check in local files all terms we got. +# Return the first id we fail to find. +id=1 +while ! test $id < $1 +do + if ! test -f "./term_${id}"; then + echo -n $id + exit 0 + fi + id=$(($id + 1)) +done + +echo -n $1 +exit 0 diff --git a/example/hook/test/server3/prepare_term b/example/hook/test/server3/prepare_term new file mode 100755 index 0000000..eac3805 --- /dev/null +++ b/example/hook/test/server3/prepare_term @@ -0,0 +1,3 @@ +#! /bin/bash + +echo "from server 3" diff --git a/example/hook/test/server3/retreive_n_term b/example/hook/test/server3/retreive_n_term new file mode 100755 index 0000000..a1bbfa8 --- /dev/null +++ b/example/hook/test/server3/retreive_n_term @@ -0,0 +1,13 @@ +#! /bin/bash + +i=$1 +output="[" +while test $i -lt $2 +do + output="${output}{'id':${i},'content':'`cat term_$i`'}," + i=$(($i+1)) +done + +output="${output}{'id':$2,'content':'`cat term_$i`'}]" + +echo -n $output diff --git a/example/hook/test/server3/retreive_term b/example/hook/test/server3/retreive_term new file mode 100755 index 0000000..ef5dd26 --- /dev/null +++ b/example/hook/test/server3/retreive_term @@ -0,0 +1,3 @@ +#! /bin/bash + +cat term_$1 diff --git a/example/hook/test/server3/switch_status b/example/hook/test/server3/switch_status new file mode 100755 index 0000000..6ad9a26 --- /dev/null +++ b/example/hook/test/server3/switch_status @@ -0,0 +1,2 @@ +#! /bin/bash +echo switch to $1 >> "status.log" diff --git a/example/hook/test/server4/append_term b/example/hook/test/server4/append_term new file mode 100755 index 0000000..828dd7a --- /dev/null +++ b/example/hook/test/server4/append_term @@ -0,0 +1,3 @@ +#! /bin/bash + +echo $2 > term_$1 diff --git a/example/hook/test/server4/commit_term b/example/hook/test/server4/commit_term new file mode 100755 index 0000000..cb57924 --- /dev/null +++ b/example/hook/test/server4/commit_term @@ -0,0 +1,2 @@ +#! /bin/bash +echo term $1 $2 commited `date --rfc-3339=n` >> commit.log diff --git a/example/hook/test/server4/pre_append_term b/example/hook/test/server4/pre_append_term new file mode 100755 index 0000000..4602d21 --- /dev/null +++ b/example/hook/test/server4/pre_append_term @@ -0,0 +1,16 @@ +#! /bin/bash + +# Check in local files all terms we got. +# Return the first id we fail to find. +id=1 +while ! test $id < $1 +do + if ! test -f "./term_${id}"; then + echo -n $id + exit 0 + fi + id=$(($id + 1)) +done + +echo -n $1 +exit 0 diff --git a/example/hook/test/server4/prepare_term b/example/hook/test/server4/prepare_term new file mode 100755 index 0000000..12767a9 --- /dev/null +++ b/example/hook/test/server4/prepare_term @@ -0,0 +1,3 @@ +#! /bin/bash + +echo "from server 4" diff --git a/example/hook/test/server4/retreive_n_term b/example/hook/test/server4/retreive_n_term new file mode 100755 index 0000000..a1bbfa8 --- /dev/null +++ b/example/hook/test/server4/retreive_n_term @@ -0,0 +1,13 @@ +#! /bin/bash + +i=$1 +output="[" +while test $i -lt $2 +do + output="${output}{'id':${i},'content':'`cat term_$i`'}," + i=$(($i+1)) +done + +output="${output}{'id':$2,'content':'`cat term_$i`'}]" + +echo -n $output diff --git a/example/hook/test/server4/retreive_term b/example/hook/test/server4/retreive_term new file mode 100755 index 0000000..ef5dd26 --- /dev/null +++ b/example/hook/test/server4/retreive_term @@ -0,0 +1,3 @@ +#! /bin/bash + +cat term_$1 diff --git a/example/hook/test/server4/switch_status b/example/hook/test/server4/switch_status new file mode 100755 index 0000000..6ad9a26 --- /dev/null +++ b/example/hook/test/server4/switch_status @@ -0,0 +1,2 @@ +#! /bin/bash +echo switch to $1 >> "status.log" diff --git a/example/hook/test/server5/append_term b/example/hook/test/server5/append_term new file mode 100755 index 0000000..828dd7a --- /dev/null +++ b/example/hook/test/server5/append_term @@ -0,0 +1,3 @@ +#! /bin/bash + +echo $2 > term_$1 diff --git a/example/hook/test/server5/commit_term b/example/hook/test/server5/commit_term new file mode 100755 index 0000000..cb57924 --- /dev/null +++ b/example/hook/test/server5/commit_term @@ -0,0 +1,2 @@ +#! /bin/bash +echo term $1 $2 commited `date --rfc-3339=n` >> commit.log diff --git a/example/hook/test/server5/pre_append_term b/example/hook/test/server5/pre_append_term new file mode 100755 index 0000000..4602d21 --- /dev/null +++ b/example/hook/test/server5/pre_append_term @@ -0,0 +1,16 @@ +#! /bin/bash + +# Check in local files all terms we got. +# Return the first id we fail to find. +id=1 +while ! test $id < $1 +do + if ! test -f "./term_${id}"; then + echo -n $id + exit 0 + fi + id=$(($id + 1)) +done + +echo -n $1 +exit 0 diff --git a/example/hook/test/server5/prepare_term b/example/hook/test/server5/prepare_term new file mode 100755 index 0000000..a665e79 --- /dev/null +++ b/example/hook/test/server5/prepare_term @@ -0,0 +1,3 @@ +#! /bin/bash + +echo "from server 5" diff --git a/example/hook/test/server5/retreive_n_term b/example/hook/test/server5/retreive_n_term new file mode 100755 index 0000000..a1bbfa8 --- /dev/null +++ b/example/hook/test/server5/retreive_n_term @@ -0,0 +1,13 @@ +#! /bin/bash + +i=$1 +output="[" +while test $i -lt $2 +do + output="${output}{'id':${i},'content':'`cat term_$i`'}," + i=$(($i+1)) +done + +output="${output}{'id':$2,'content':'`cat term_$i`'}]" + +echo -n $output diff --git a/example/hook/test/server5/retreive_term b/example/hook/test/server5/retreive_term new file mode 100755 index 0000000..ef5dd26 --- /dev/null +++ b/example/hook/test/server5/retreive_term @@ -0,0 +1,3 @@ +#! /bin/bash + +cat term_$1 diff --git a/example/hook/test/server5/switch_status b/example/hook/test/server5/switch_status new file mode 100755 index 0000000..6ad9a26 --- /dev/null +++ b/example/hook/test/server5/switch_status @@ -0,0 +1,2 @@ +#! /bin/bash +echo switch to $1 >> "status.log" diff --git a/src/api/client.rs b/src/api/client.rs index a240c5c..b53d9c1 100644 --- a/src/api/client.rs +++ b/src/api/client.rs @@ -1,7 +1,7 @@ use super::{ io_msg::{ - AppendTermInput, AppendTermResult, RequestVoteInput, RequestVoteResult, - UpdateNodeInput, UpdateNodeResult, + AppendTermInput, AppendTermResult, RequestVoteInput, RequestVoteResult, UpdateNodeInput, + UpdateNodeResult, }, Url, }; @@ -17,10 +17,7 @@ use serde::Serialize; use std::time::Duration; use tracing::trace; -async fn run_request( - req: Request, - timeout: Duration, -) -> WarnResult> { +async fn run_request(req: Request, timeout: Duration) -> WarnResult> { let client = Client::new(); match tokio::time::timeout(timeout, client.request(req)).await { Ok(Ok(result)) => Ok(result), @@ -46,14 +43,13 @@ async fn build( serde_json::to_string(¬_serialized_body).unwrap(), ))?; // TODO could be simplified let mut resp = run_request(req, timeout).await?; - trace!("post_update_node response: {}", resp.status()); let body_resp = resp.body_mut(); if let Ok(resp_bytes) = hyper::body::to_bytes(body_resp).await { match serde_json::from_slice(&resp_bytes) { Ok(http_result) => Ok(http_result), Err(err) => { throw!(Warning::CommandFail(format!( - "parse http response failed with err:\n{:indent$}", + "parse HTTP response failed with err:\n{:indent$}", err, indent = 2 ))) @@ -82,7 +78,6 @@ pub(crate) async fn post_update_node( let body = UpdateNodeInput { hash: uuid, port: settings.port.clone(), - follower: settings.follower, }; let target_uri = format!("http://{}/update_node", target); match build( diff --git a/src/api/io_msg.rs b/src/api/io_msg.rs index 7fff3be..e8cc3de 100644 --- a/src/api/io_msg.rs +++ b/src/api/io_msg.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::log_entry::{Entries, Term}; +use crate::log_entry::Term; #[derive(Debug, Deserialize, Serialize)] pub enum HttpResult { @@ -29,7 +29,8 @@ impl std::fmt::Display for HttpErrorResult { pub struct RequestVoteInput { pub candidate_id: String, pub term: Term, - pub last_term: Term, + // commit index + pub last_term: usize, } #[derive(Debug, Deserialize, Serialize)] @@ -43,7 +44,7 @@ pub struct AppendTermInput { pub term: Term, pub leader_id: String, pub prev_term: Term, - pub entries: Entries, + pub entries: Vec, pub leader_commit_index: usize, } @@ -59,8 +60,6 @@ pub struct UpdateNodeInput { pub hash: [u8; 16], /// Open server port pub port: String, - /// Follower flag - pub follower: bool, } impl PartialEq for UpdateNodeInput { @@ -72,6 +71,5 @@ impl PartialEq for UpdateNodeInput { #[derive(Debug, Deserialize, Serialize)] pub struct UpdateNodeResult { pub leader_id: String, - pub node_list: Vec, // todo how is it with hashset here ? - pub follower_list: Vec, + pub node_list: Vec, } diff --git a/src/api/server.rs b/src/api/server.rs index 74a9461..03cc245 100644 --- a/src/api/server.rs +++ b/src/api/server.rs @@ -9,7 +9,7 @@ use hyper::{Body, Request, Response, Server}; use hyper::{Method, StatusCode}; use serde::Deserialize; use std::{convert::Infallible, net::SocketAddr}; -use tracing::error; +use tracing::{error, trace}; async fn shutdown_signal() { tokio::signal::ctrl_c() @@ -20,18 +20,14 @@ async fn shutdown_signal() { async fn body_to_bytes(body: Body) -> Result { match hyper::body::to_bytes(body).await { Ok(body) => Ok(body), - Err(err) => { - return Err(ServerError::CannotDeserializeBody(format!( - "error while reading the body: {:?}", - err - ))) - } + Err(err) => Err(ServerError::CannotDeserializeBody(format!( + "error while reading the body: {:?}", + err + ))), } } -fn deserialize_body<'a, T: Deserialize<'a>>( - body_bytes: &'a Bytes, -) -> Result { +fn deserialize_body<'a, T: Deserialize<'a>>(body_bytes: &'a Bytes) -> Result { match serde_json::from_slice(body_bytes) { Ok(res) => Ok(res), Err(err) => Err(ServerError::CannotDeserializeBody(format!( @@ -49,21 +45,20 @@ async fn on_receive_update_node( ) { // todo: check in the body if the `node` // value is similar to the caller. (should return an error if not) + trace!("receive update node request"); let input: UpdateNodeInput = deserialize_body(bytes).unwrap(); let addr = format!("{}:{}", remote.ip(), input.port); let res = node .receive_connection_request(NodeInfo { hash: input.hash, addr, - follower: input.follower, }) .await; match res { Some(res) => { - *response.body_mut() = - serde_json::to_string(&HttpResult::UpdateNode(res)) - .unwrap() - .into() + *response.body_mut() = serde_json::to_string(&HttpResult::UpdateNode(res)) + .unwrap() + .into() } None => { *response.body_mut() = errors::I_DONT_NOW_THE_LEADER.clone().into(); @@ -71,11 +66,7 @@ async fn on_receive_update_node( } } -async fn on_receive_append_term( - node: &Node, - bytes: &Bytes, - response: &mut Response, -) { +async fn on_receive_append_term(node: &Node, bytes: &Bytes, response: &mut Response) { // todo: check in the body if the `node` // value is similar to the caller. (should return an error if not) let res = node @@ -83,23 +74,17 @@ async fn on_receive_append_term( .await; match res { Ok(res) => { - *response.body_mut() = - serde_json::to_string(&HttpResult::AppendTerm(res)) - .unwrap() - .into() + *response.body_mut() = serde_json::to_string(&HttpResult::AppendTerm(res)) + .unwrap() + .into() } Err(_) => { - *response.body_mut() = - errors::ERR_APPEND_TERM_SERVER_GENERIC.clone().into(); + *response.body_mut() = errors::ERR_APPEND_TERM_SERVER_GENERIC.clone().into(); } } } -async fn on_receive_request_vote( - node: &Node, - bytes: &Bytes, - response: &mut Response, -) { +async fn on_receive_request_vote(node: &Node, bytes: &Bytes, response: &mut Response) { // todo: check in the body if the `node` // value is similar to the caller. (should return an error if not) let res = node @@ -151,7 +136,7 @@ fn manage_server_error( } } -// todo, remove all the "unwrap" here and try to make a nice http response +// todo, remove all the "unwrap" here and try to make a nice HTTP response async fn service( req: Request, node: Node, @@ -168,7 +153,6 @@ async fn service( pub async fn new(node: Node) -> ErrorResult<()> { use crate::common::error::throw; use hyper::server::conn::AddrStream; - use tracing::trace; let full_addr = &format!("{}:{}", node.settings.addr, node.settings.port); trace!("Startup server on {}", full_addr); diff --git a/src/api/tests/mod.rs b/src/api/tests/mod.rs index 5bf7c65..04ba8eb 100644 --- a/src/api/tests/mod.rs +++ b/src/api/tests/mod.rs @@ -63,11 +63,7 @@ async fn update_node_and_call_directly_the_leader() { let settings = Settings::default(); // server settings let node = Node { leader: Url::get_ptr("127.0.0.1:3001"), - ..Node::_init( - settings.clone(), - Status::::create(), - DefaultHook {}, - ) + ..Node::_init(settings.clone(), Status::::create(), DefaultHook {}) }; let sig_stop = Arc::new(AtomicBool::new(true)); let sig_stop_c = sig_stop.clone(); diff --git a/src/common/config.rs b/src/common/config.rs index d7c5d55..c9c41cf 100644 --- a/src/common/config.rs +++ b/src/common/config.rs @@ -16,7 +16,7 @@ fn default_port() -> String { "3000".to_string() } const fn default_follower() -> bool { - true + false } const fn default_response_timeout() -> usize { 20 @@ -33,8 +33,8 @@ const fn default_timeout_max() -> usize { const fn default_prepare_term_period() -> u64 { 80 } -const fn default_send_term_period() -> u64 { - 80 +const fn default_node_id() -> String { + String::new() } /// Represent the user settings in the settings.toml @@ -56,14 +56,8 @@ pub struct Settings { pub response_timeout: usize, #[serde(default = "default_prepare_term_period")] pub prepare_term_period: u64, - #[serde(default = "default_send_term_period")] - pub send_term_period: u64, - #[serde(default = "default_timeout_max")] - pub max_timeout_value: usize, - #[serde(default = "default_timeout_min")] - pub min_inc_timeout: usize, - #[serde(default = "default_timeout_max")] - pub max_inc_timeout: usize, + #[serde(default = "default_node_id")] + pub node_id: String, } impl Settings { @@ -73,19 +67,9 @@ impl Settings { let mut rng = rand::thread_rng(); Duration::from_millis(rng.gen_range(self.timeout_min..=self.timeout_max) as u64) } - pub fn get_randomized_inc_timeout(&self) -> Duration { - let mut rng = rand::thread_rng(); - Duration::from_millis(rng.gen_range(self.min_inc_timeout..=self.max_inc_timeout) as u64) - } pub fn get_prepare_term_sleep_duration(&self) -> Duration { Duration::from_millis(self.prepare_term_period) } - pub fn get_send_term_sleep_duration(&self) -> Duration { - Duration::from_millis(self.send_term_period) - } - pub fn get_max_timeout_value(&self) -> Duration { - Duration::from_millis(self.max_timeout_value as u64) - } } impl Default for Settings { @@ -99,27 +83,24 @@ impl Default for Settings { follower: default_follower(), response_timeout: default_response_timeout(), prepare_term_period: default_prepare_term_period(), - send_term_period: default_send_term_period(), - max_timeout_value: default_timeout_max(), - min_inc_timeout: default_timeout_min(), - max_inc_timeout: default_timeout_max(), + node_id: default_node_id(), } } } /// Read the config file `settings.toml` and parse the node configuration. /// -/// Done when we are creating (dereferencing) the `StatusPtr` in `node.rs` for +/// Done when we are creating (de-referencing) the `StatusPtr` in `node.rs` for /// the first time. pub fn read(opt_path: Option) -> ErrorResult { let path = opt_path.unwrap_or_else(|| "settings.toml".to_string()); let config = Config::builder() .add_source(config::File::with_name(&path)) .build() - .unwrap(); // todo remove the unwrap, prefere a nice error + .unwrap(); // todo remove the unwrap, prefer a nice error match config.try_deserialize::() { Ok(settings) => Ok(settings), Err(error) => throw!(Error::CannotReadSettings(error)), } - // todo check if configuration is ok, (example: timeout_min < timeout_max) + // todo check if configuration is OK, (example: timeout_min < timeout_max) } diff --git a/src/common/error.rs b/src/common/error.rs index f3852f4..cf3a3d4 100644 --- a/src/common/error.rs +++ b/src/common/error.rs @@ -48,7 +48,7 @@ pub enum Warning { impl std::fmt::Display for Warning { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match &*self { + match self { Warning::CommandFail(str) => f.write_str(&format!("warning: command fail, {str}")), Warning::Timeout(str) => f.write_str(&format!("warning: timeout, {str}")), Warning::BadResult(err) => f.write_str(&format!( @@ -89,10 +89,6 @@ pub mod errors { /***********************************************/ /* Const defined errors and warnings */ - pub const ERR_FOLLOWER_MUST_HAVE_KNOWN: Error = Error::InitializationFail( - "Followers must know some nodes to start, look at the `nodes` variable in your `settings.toml`", - ); - pub const WARN_INFO_TIMEOUT: Warning = Warning::Timeout("Timeout on get information client request"); diff --git a/src/common/hook_trait.rs b/src/common/hook_trait.rs index 68b0541..49820b3 100644 --- a/src/common/hook_trait.rs +++ b/src/common/hook_trait.rs @@ -1,10 +1,10 @@ -use crate::log_entry::Term; +use crate::{log_entry::Term, state::EStatus}; pub trait Hook: Send + Sync { // all script method are fn // when declaring a Node, give an struct that impl Hook // Node(hooked: Box) - // have a default implemention ? <-- + // have a default implementation ? <-- // (this is consensus stuffs) // // VS @@ -14,22 +14,14 @@ pub trait Hook: Send + Sync { // - binary is lib + reading script folder // - service.systemd reading sockets fn update_node(&self) -> bool; - fn pre_append_term(&self, term: &Term) -> bool; + fn pre_append_term(&self, term: &Term) -> Option; fn append_term(&self, term: &Term) -> bool; fn commit_term(&self, term: &Term) -> bool; fn prepare_term(&self) -> String; - // TODO + fn retreive_term(&self, index: usize) -> Option; + fn retreive_terms(&self, from: usize, to: usize) -> Option>; + fn switch_status(&self, status: EStatus); } -// todo, according to the README specification we should implement other methods, -// one for each functions here. - // todo, the user should be able to provide a dir where the scripts are located // instead of the current_dir - -// todo, remove unwrap and consider using a warning, we need to define the behavior -// for each script when we catch a fail. - -// todo, that file should be reworked, each function should have a timeout taken from -// the setting file (cannot be staticly compute on start), each should also have a -// guard prevention from panic, instead, return the default value. diff --git a/src/common/scripts.rs b/src/common/scripts.rs index 352692d..2941dae 100644 --- a/src/common/scripts.rs +++ b/src/common/scripts.rs @@ -3,16 +3,19 @@ //! as a library) use super::hook_trait::Hook; -use crate::log_entry::Term; +use crate::{log_entry::Term, state::EStatus}; +use chrono::{SecondsFormat, Utc}; +use serde::Deserialize; use std::{env, fs, process::Command}; +use tracing::{debug, warn}; -pub(crate) struct DefaultHook; +pub struct DefaultHook; impl Hook for DefaultHook { fn update_node(&self) -> bool { update_node() } - fn pre_append_term(&self, _term: &Term) -> bool { + fn pre_append_term(&self, _term: &Term) -> Option { pre_append_term(_term) } @@ -27,82 +30,201 @@ impl Hook for DefaultHook { fn prepare_term(&self) -> String { prepare_term() } + + fn retreive_term(&self, index: usize) -> Option { + retreive_term(index) + } + + fn retreive_terms(&self, from: usize, to: usize) -> Option> { + retreive_terms(from, to) + } + + fn switch_status(&self, status: EStatus) { + switch_status(status) + } } fn get_script_path(prefix: &'static str) -> Option { let current_dir = match env::current_dir() { Ok(dir) => dir, - _ => return None, + _ => { + debug!("no {prefix} found script"); + return None; + } }; for entry in fs::read_dir(current_dir).unwrap().flatten() { - let path = entry.path(); - let path = path.to_str().unwrap(); - if path.starts_with(prefix) && !path.ends_with(&".sample") { - return Some(path.to_string()); + let name = entry.file_name(); + let name = name.to_str().unwrap(); + if name.starts_with(prefix) && !name.ends_with(&".sample") { + return Some(entry.path().to_str().unwrap().to_string()); } } + debug!("no {prefix} found script"); None } +fn exec_cmd(script: String, input: Option>) -> Option { + let mut cmd = Command::new(script.clone()); + debug!("exec script {} with args: {:?}", script, input); + if let Some(args) = input { + cmd.args(args); + } + let output = cmd.output().expect("failed reading output"); + debug!("{}: {:#?}", script, output); + Some(String::from_utf8(output.stdout).unwrap().to_lowercase()) +} + fn update_node() -> bool { match get_script_path("update_node") { Some(script) => { - let mut cmd = Command::new(script); - let output = cmd.spawn().unwrap().wait_with_output().unwrap(); - let res = String::from_utf8(output.stdout).unwrap().to_lowercase(); - res != "false" + if let Some(res) = exec_cmd(script, None) { + res == "true" + } else { + false + } } None => true, } } -// todo, pass in arguments term and other flavours -fn pre_append_term(_term: &Term) -> bool { - match get_script_path("pre_append_term") { - Some(script) => { - let mut cmd = Command::new(script); - let output = cmd.spawn().unwrap().wait_with_output().unwrap(); - let res = String::from_utf8(output.stdout).unwrap().to_lowercase(); - res != "false" - } - None => true, +fn pre_append_term(term: &Term) -> Option { + let script = if let Some(script) = get_script_path("pre_append_term") { + script + } else { + return Some(term.id); + }; + let res = exec_cmd( + script, + Some(vec![format!("{}", term.id), term.content.clone()]), + )?; + if res.is_empty() { + None + } else { + Some(res.parse().expect("Failed to parse pre_append_term output")) } } -// todo, pass in arguments term and other flavours -fn append_term(_term: &Term) -> bool { +fn append_term(term: &Term) -> bool { match get_script_path("append_term") { Some(script) => { - let mut cmd = Command::new(script); - let output = cmd.spawn().unwrap().wait_with_output().unwrap(); - let res = String::from_utf8(output.stdout).unwrap().to_lowercase(); - res != "false" + if let Some(res) = exec_cmd( + script, + Some(vec![format!("{}", term.id), term.content.clone()]), + ) { + res == "true" + } else { + false + } } None => true, } } -// todo, pass in arguments term and other flavours -fn commit_term(_term: &Term) -> bool { +fn commit_term(term: &Term) -> bool { match get_script_path("commit_term") { Some(script) => { - let mut cmd = Command::new(script); - let output = cmd.spawn().unwrap().wait_with_output().unwrap(); - let res = String::from_utf8(output.stdout).unwrap().to_lowercase(); - res != "false" + if let Some(res) = exec_cmd( + script, + Some(vec![format!("{}", term.id), term.content.clone()]), + ) { + res == "true" + } else { + false + } } None => true, } } -// todo, pass in arguments term and other flavours fn prepare_term() -> String { - match get_script_path("prepare_term") { - Some(script) => { - let mut cmd = Command::new(script); - let output = cmd.spawn().unwrap().wait_with_output().unwrap(); - String::from_utf8(output.stdout).unwrap().to_lowercase() + if let Some(script) = get_script_path("prepare_term") { + if let Some(output) = exec_cmd(script, None) { + return output; } - None => String::new(), + } + "default".into() +} + +fn retreive_term(index: usize) -> Option { + debug!("call retrieve term script"); + let timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, false); + let script = if let Some(script) = get_script_path("retrieve_term") { + script + } else { + return Some(Term { + id: index, + timestamp, + content: "default".into(), + }); + }; + let content = exec_cmd(script, Some(vec![format!("{index}")]))?; + debug!("content retrieved {} {}", index, content); + Some(Term { + id: index, + timestamp, + content, + }) +} + +fn retreive_terms(from: usize, to: usize) -> Option> { + debug!("call retrieve termS script"); + let timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, false); + let script = get_script_path("retrieve_n_term"); + if script.is_none() { + return Some( + (from..=to) + .map(|id| Term { + id, + timestamp: timestamp.clone(), + content: "default".into(), + }) + .collect(), + ); + } + + let output = exec_cmd( + script.unwrap(), + Some(vec![format!("{from}"), format!("{to}")]), + )?; + + #[derive(Deserialize)] + struct TermWithoutTimestamp { + id: usize, + content: String, + } + + match serde_json::from_str::>(&output) { + Ok(terms) => { + debug!("parse retrieve termS succeed"); + Some( + terms + .into_iter() + .map(|term| Term { + id: term.id, + timestamp: timestamp.clone(), + content: term.content, + }) + .collect(), + ) + } + Err(err) => { + warn!("{:?} failed to parse retrieve termS output {}", err, output); + None + } + } +} + +fn switch_status(status: EStatus) { + if let Some(script) = get_script_path("switch_status") { + exec_cmd( + script, + Some(vec![match status { + EStatus::ConnectionPending => "pending", + EStatus::Follower => "follower", + EStatus::Leader => "leader", + EStatus::Candidate => "candidate", + } + .to_string()]), + ); } } diff --git a/src/lib.rs b/src/lib.rs index d117550..df6dacb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,5 +23,6 @@ mod workflow; pub use common::config::Settings; pub use common::hook_trait::Hook; +pub use common::scripts::DefaultHook; pub use log_entry::Term; pub use node::Node; diff --git a/src/log_entry.rs b/src/log_entry.rs index 3af9763..20aed02 100644 --- a/src/log_entry.rs +++ b/src/log_entry.rs @@ -2,168 +2,169 @@ // This source code is licensed under the GPLv3 license, you can found the // LICENSE file in the root directory of this source tree. -use std::{ - cmp::Ordering, - collections::{hash_map::Iter, HashMap}, - fmt::Display, - iter::Map, - ops::Range, -}; - -use crate::node::NodeInfo; +use std::{cmp::Ordering, collections::HashMap, fmt::Display}; + +use chrono::{SecondsFormat, Utc}; use serde::{Deserialize, Serialize}; +use tracing::{debug, warn}; -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Default, Deserialize, Serialize, Clone)] pub struct LogEntry { pub id: usize, + pub timestamp: String, pub content: String, } -impl Default for LogEntry { - fn default() -> Self { - Self { - id: 0, - content: "".into(), - } - } -} - -impl Default for Entries { - fn default() -> Self { - let mut h = HashMap::default(); - h.insert(0, "".to_string()); - Self(h) - } -} - impl PartialEq for Term { fn eq(&self, other: &Self) -> bool { - self.id == other.id && self.content == other.content + self.id == other.id && self.content == other.content && self.timestamp == other.timestamp } } + impl PartialOrd for Term { fn partial_cmp(&self, other: &Self) -> Option { match self.id.partial_cmp(&other.id) { + Some(core::cmp::Ordering::Equal) => {} + ord => return ord, + } + match self.timestamp.partial_cmp(&other.timestamp) { Some(core::cmp::Ordering::Equal) => Some(Ordering::Equal), ord => ord, } } } -#[derive(Debug, Deserialize, Serialize)] -pub struct Entries(HashMap); + +#[derive(Debug, Default, Deserialize, Serialize)] +pub struct Entries { + inner: HashMap, + latest: usize, + commit_index: usize, + current: LogEntry, +} + pub type Term = LogEntry; + impl Term { pub fn _new(id: usize, content: T) -> Self { + let timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, false); Term { id, + timestamp, content: content.to_string(), } } - pub fn parse_conn(&self) -> Option { - if !self.content.starts_with("conn:") { - return None; - } - let mut c = self.content.clone(); - c.drain(.."conn:".len()); - match serde_json::from_str::(&c.to_string()) { - Ok(res) => Some(res), - _ => None, - } - } } -type F = fn((&usize, &String)) -> LogEntry; + impl Entries { - /// Same as default but let the map empty + /// Same as default pub fn new() -> Self { - Self(HashMap::default()) - } - pub fn iter(&self) -> Map, F> { - self.0.iter().map(|(id, content)| Term { - id: *id, - content: content.clone(), - }) + Self { + inner: HashMap::default(), + latest: 0, + commit_index: 0, + current: LogEntry::default(), + } } - pub fn contains(&self, term: &LogEntry) -> bool { - self.0.contains_key(&term.id) + + pub fn contains(&self, term: &Term) -> bool { + self.inner.contains_key(&term.id) } - pub fn insert(&mut self, term: &LogEntry) -> bool { - if self.contains(term) { - if let Some(content) = self.0.get_mut(&term.id) { - if *content != term.content { - *content = term.content.clone(); - let mut to_remove_list = vec![]; - for (i, _) in self.0.iter() { - if *i > term.id { - to_remove_list.push(*i) - } - } - for to_remove in to_remove_list { - self.0.remove(&to_remove); - } - true - } else { - false - } - } else { - false + + /// Insert or replace a term. Used in the leader and append_term workflow. + /// If the term exists, we would like to rollback to avoid conflicts. + /// + /// The call of the hook is deferred to the caller if that methods return true. + pub fn insert(&mut self, term: &Term) { + if term.id <= self.commit_index { + panic!("Trying to re-insert an already committed term") + } + if term.id <= self.latest { + for i in term.id..=self.latest { + self.inner.remove(&i); } - } else { - self.0.insert(term.id, term.content.clone()); - true } + self.latest = term.id; + self.current = term.clone(); + self.inner + .insert(term.id, (term.timestamp.clone(), term.content.clone())); } - pub fn append(&mut self, term_content: String) -> Term { + + /// Create a new term from a content + /// Return the created term + pub fn append(&mut self, content: String) -> Term { + let id = self.latest + 1; + let timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, false); + debug!("log entry: append term {} {} {}", id, content, timestamp); let t = Term { - id: self.len(), - content: term_content, + id, + timestamp, + content, }; + self.latest = id; + self.current = t.clone(); self.insert(&t); t } - /// Find the term befor the given input - pub fn previous(&self, term: &Term) -> Option { - for i in term.id - 1..0 { - if let Some(t) = self.find(i) { - return Some(t); - } - } - None - } - pub fn find(&self, index: usize) -> Option { - self.0.get(&index).map(|c| LogEntry { + + pub fn find(&self, index: usize) -> Option { + self.inner.get(&index).map(|(t, c)| LogEntry { id: index, + timestamp: t.clone(), content: c.clone(), }) } - pub fn back(&self) -> Option { - self.0.get(&(self.0.len() - 1)).map(|c| LogEntry { - id: self.0.len() - 1, - content: c.clone(), - }) - } - pub fn len(&self) -> usize { - self.0.len() + + pub fn check_commit(&self, index: usize) -> bool { + if index <= self.commit_index { + warn!("Trying to re-commit index {index}"); + return false; + } + if index > self.latest { + warn!("Trying to commit an unknown index"); + return false; + } + true } - pub fn get_copy_range(&self, range: Range) -> Option { - // todo: that can be very better, but waiting for a full development of a kind - // of memmapped-file for that data structure. - let entries = self.iter_range(range); - let mut ret = Entries::new(); - for e in entries { - match self.find(e.id) { - Some(_) => ret.insert(&e), - _ => return None, - }; + + pub fn set_commit(&mut self, index: usize) { + if self.check_commit(index) { + self.inner.remove(&index); + self.commit_index = index; } - Some(ret) } - pub fn iter_range(&self, range: Range) -> Vec { - let mut ret = vec![]; - for index in range { - if let Some(t) = self.find(index) { - ret.push(t) - } + + /// Find the latest log entry. Creates a new one if + /// empty. + pub fn latest(&mut self) -> (bool, Term) { + if let Some((t, c)) = self.inner.get(&self.latest) { + debug!("log entry: latest found {}", self.latest); + ( + false, + LogEntry { + id: self.latest, + timestamp: t.clone(), + content: c.clone(), + }, + ) + } else { + debug!("log entry: latest: append a new empty entry"); + (true, self.append("default".into())) } - ret + } + + pub fn last_index(&self) -> usize { + self.latest + } + + pub fn set_last_index(&mut self, index: usize) { + self.latest = index; + } + + pub fn commit_index(&self) -> usize { + self.commit_index + } + + pub fn current_term(&self) -> Term { + self.current.clone() } } diff --git a/src/node.rs b/src/node.rs index d943bfd..a640f67 100644 --- a/src/node.rs +++ b/src/node.rs @@ -9,72 +9,74 @@ use crate::{ error::{throw, Error, ErrorResult}, hook_trait::Hook, }, - log_entry::{Entries, Term}, - state::{ConnectionPending, EStatus, EmptyStatus, Status, StatusPtr}, + log_entry::Entries, + state::{EStatus, Status}, }; -use dyn_timeout::tokio_impl::DynTimeout; use serde::{Deserialize, Serialize}; use std::{ collections::{HashMap, HashSet, VecDeque}, sync::Arc, - time::Duration, }; use tokio::{ runtime::Runtime, - sync::{Mutex, RwLock}, + sync::{oneshot::Sender, Mutex, RwLock}, task::JoinHandle, }; -use tracing::{metadata::LevelFilter, trace}; +use tracing::metadata::LevelFilter; use tracing_subscriber::{ - filter::filter_fn, prelude::__tracing_subscriber_SubscriberExt, - util::SubscriberInitExt, Layer, + filter::filter_fn, prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt, Layer, }; +pub enum NextIndex { + Validated(usize), + Pending(usize), +} + +impl NextIndex { + pub fn unwrap(&self) -> usize { + match self { + NextIndex::Validated(val) => *val, + NextIndex::Pending(val) => *val, + } + } + + pub fn validated(&self) -> usize { + match self { + NextIndex::Validated(val) => *val, + NextIndex::Pending(_) => 1, /* return minimum */ + } + } +} + #[derive(Clone)] pub struct Node { /// Current state of the local node - pub p_status: StatusPtr, + pub p_status: Status, /// Settings of the node pub settings: Settings, /// Heartbeat dynamic timeout used by follower (who can be candidates) - pub opt_heartbeat: Arc>>, - /// Log entries, each entry contains command for state machine, and term - /// when entry was received by Leader. + pub heartbeat: Arc>>>, + /// Log entries, terms are stored here until they are committed pub logs: Arc>, - /// latest term server has seen (initialized to 0 on first boot, increases - /// monotonically), usually it's the latest log in self.logs - pub p_current_term: Arc>, - /// Stored commited index (usually min(leader.commited_index, - /// logs.max_index)) - pub p_commit_index: Arc>, /// Next indexes by know nodes (table of None at initialization) /// Is initialized on comes to power. - pub next_indexes: Arc>>, - // todo: node waiting could be a simple vector of tuple + pub next_indexes: Arc>>, /// Wait to connect pub waiting_nodes: Arc>>, /// List of nodes that can be potential leader and candidates /// Note only these nodes votes pub node_list: Arc>>, - /// List of nodes that are only followers - pub follower_list: Arc>>, - /// Leader id that is also his IP - pub leader: Arc>>, /// Last vote Some(node id, last log term) if voted, None otherwise - pub vote_for: Arc>>, + pub vote_for: Arc>>, /// hook interface pub hook: Arc>, /// Unique node id, used as a temporary identifier in the network pub uuid: [u8; 16], } -// todo: define a nodelist pointer structure and implement, insert, to_vec, -// contains... -// todo: make the ctrl-c very sensitive (you'll debug a lot if you start that -// quest) // todo: verify if leader correctly update the `last_applied` and call the // `apply_term` script each time he create a term -// todo: the logs (Entries structure) should prune the oldest commited terms +// todo: the logs (Entries structure) should prune the oldest committed terms // with a kind of buffer (size in the settings) // todo: add a maximum for logs production // todo: we need to define what should be in the debug level of tracing. @@ -83,19 +85,15 @@ impl Node { /// Private default implementation fn default(settings: Settings, hook: impl Hook + 'static) -> Self { Self { - p_status: Status::::create(), - opt_heartbeat: Default::default(), + p_status: Status::connection_pending(), + heartbeat: Default::default(), logs: Default::default(), - p_current_term: Default::default(), - p_commit_index: Default::default(), next_indexes: Default::default(), waiting_nodes: Default::default(), node_list: Arc::new(RwLock::new(HashSet::from_iter( settings.nodes.iter().cloned(), ))), settings, - follower_list: Default::default(), - leader: Default::default(), vote_for: Default::default(), hook: Arc::new(Box::new(hook)), uuid: generate_uuid(), @@ -105,9 +103,7 @@ impl Node { /// Creates a new default node pub fn new(hook: impl Hook + 'static) -> Self { let layer = tracing_subscriber::fmt::layer() - .with_filter(filter_fn(|metadata| { - metadata.target().starts_with("hook") - })) + .with_filter(filter_fn(|metadata| metadata.target().starts_with("hook"))) .with_filter(LevelFilter::TRACE); // todo: use an input or a setting for log level tracing_subscriber::registry() // add the console layer to the subscriber or default layers... @@ -134,21 +130,14 @@ impl Node { #[cfg(test)] /// Creates a node with the given settings and status - pub fn _init( - settings: Settings, - p_status: StatusPtr, - hook: impl Hook + 'static, - ) -> Self { + pub fn _init(settings: Settings, p_status: StatusPtr, hook: impl Hook + 'static) -> Self { Self { p_status, ..Self::default(settings, hook) } } - pub fn new_with_settings( - settings: Settings, - hook: impl Hook + 'static, - ) -> Self { + pub fn new_with_settings(settings: Settings, hook: impl Hook + 'static) -> Self { Self { ..Self::default(settings, hook) } @@ -157,36 +146,15 @@ impl Node { async fn internal_main_loop(&self) -> ErrorResult<()> { self.initialize().await?; loop { - let st = { - match &*self.p_status.read().await { - EStatus::Follower(_) => EmptyStatus::Follower, - EStatus::Candidate(_) => EmptyStatus::Candidate, - EStatus::Leader(_) => EmptyStatus::Leader, - EStatus::ConnectionPending(_) => { - EmptyStatus::ConnectionPending - } - } - }; - if matches!(st.clone(), EmptyStatus::ConnectionPending) { - trace!("waiting to connect"); - let sleep = tokio::time::sleep(Duration::from_millis(500)); - tokio::pin!(sleep); - tokio::select! { - _ = sleep => { - continue - }, - _ = tokio::signal::ctrl_c() => { - println!("Handle a gracefull shotdown"); - break - }, - } + if self.p_status.is_pending().await { + self.p_status.wait(); } let status_loop = async { - match st { - EmptyStatus::Leader => self.run_leader().await, - EmptyStatus::Follower => self.run_follower().await, - EmptyStatus::Candidate => self.run_candidate().await, - EmptyStatus::ConnectionPending => { + match self.p_status.status().await { + EStatus::Leader => self.run_leader().await, + EStatus::Follower => self.run_follower().await, + EStatus::Candidate => self.run_candidate().await, + EStatus::ConnectionPending => { throw!(Error::WrongStatus) } } @@ -199,7 +167,7 @@ impl Node { } }, _ = tokio::signal::ctrl_c() => { - println!("Handle a gracefull shotdown"); + println!("Handle a graceful shutdown"); break }, } @@ -216,13 +184,16 @@ impl Node { pub fn spawn(self) -> JoinHandle> { tokio::spawn(async move { self.internal_main_loop().await }) } + + pub(crate) async fn get_node_list(&self) -> Vec { + self.node_list.read().await.iter().cloned().collect() + } } #[derive(Deserialize, Serialize, Debug)] pub struct NodeInfo { pub hash: [u8; 16], pub addr: String, - pub follower: bool, } pub(crate) fn generate_uuid() -> [u8; 16] { diff --git a/src/state/mod.rs b/src/state/mod.rs index cbf019f..c3487f0 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -24,81 +24,115 @@ //! [Status] and force the modification through the //! implementation of some `From` traits. Implementations are in [sm_impl]. -use std::{marker::PhantomData, sync::Arc}; -use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; -pub mod node; -pub mod sm_impl; +use crate::{ + api::Url, + common::error::{throw, Error, ErrorResult}, +}; +use std::sync::{Arc, Condvar, Mutex}; +use tokio::sync::RwLock; +use tracing::trace; -#[derive(Clone)] -pub struct ConnectionPending; -#[derive(Clone)] -pub struct Follower; -#[derive(Clone)] -pub struct Candidate; -#[derive(Clone)] -pub struct Leader; +mod node; +pub use node::*; -/// Enum used by the Node to contain his current state. -/// Look at StatusPtr that Arc the Enum. +#[derive(Clone, Copy)] pub enum EStatus { - /// The node is trying to connect to others - ConnectionPending(Status), - /// The node is a follower inthe network - /// It can be a candidate in a while if the configuration - /// allow it. - Follower(Status), - /// The node is a candidate in his network - Candidate(Status), - /// The node is the leader in his network - Leader(Status), -} - -/// Same as the [EStatus] but used in case we just want to conserve the last -/// state without interact with the internal status. -#[derive(Clone)] -pub enum EmptyStatus { - Leader, + ConnectionPending, Follower, Candidate, - ConnectionPending, + Leader, } +/***********************************************/ +/* "public" access to the status */ /***********************************************/ #[derive(Clone)] -pub struct StatusPtr(Arc>); - -// todo: Upgrade that implementation, allow user to give an opional guard, -// change the names of the functions. -impl StatusPtr { - /// Check if leader - pub async fn _is_leader(&self) -> bool { - let guard = self.0.read().await; - matches!(*guard, EStatus::Leader(_)) +pub struct Status { + inner: Arc)>>, + cv: Arc<(Mutex<()>, Condvar)>, +} + +impl Status { + /// Wait a modification of the status + pub(crate) fn wait(&self) { + let (mutex, condvar) = &*self.cv; + let guard = mutex.lock().unwrap(); + let _unused = condvar.wait(guard).unwrap(); + } + + pub(crate) async fn status(&self) -> EStatus { + self.inner.read().await.0 } - /// Check if connection pending - pub async fn _is_pending(&self) -> bool { - let guard = self.0.read().await; - matches!(*guard, EStatus::ConnectionPending(_)) + + /// Switch the current status to candidate. + /// Follower -> Candidate + pub(crate) async fn switch_to_candidate(&self) -> ErrorResult<()> { + let mut inner = self.inner.write().await; + match inner.0 { + EStatus::Follower => { /* OK */ } + EStatus::ConnectionPending => { /* OK */ } + _ => throw!(Error::WrongStatus), + } + trace!("switch to candidate"); + *inner = (EStatus::Candidate, None); + let (mutex, condvar) = &*self.cv; + let _guard = mutex.lock().unwrap(); + condvar.notify_all(); + Ok(()) } - /// Check if connection is follower - pub async fn _is_follower(&self) -> bool { - let guard = self.0.read().await; - matches!(*guard, EStatus::Follower(_)) + + /// Switch the current status to leader. + /// Candidate -> Leader + pub(crate) async fn switch_to_leader(&self) -> ErrorResult<()> { + let mut inner = self.inner.write().await; + match inner.0 { + EStatus::Candidate => { /* OK */ } + _ => throw!(Error::WrongStatus), + } + trace!("switch to leader"); + *inner = (EStatus::Leader, None); + let (mutex, condvar) = &*self.cv; + let _guard = mutex.lock().unwrap(); + condvar.notify_all(); + Ok(()) } - pub async fn _is_candidate(&self) -> bool { - let guard = self.0.read().await; - matches!(*guard, EStatus::Candidate(_)) + + /// Switch the current status to follower. + /// Every state can turn into a follower + pub(crate) async fn switch_to_follower(&self, leader: Url) -> ErrorResult<()> { + trace!("switch to follower"); + let mut inner = self.inner.write().await; + *inner = (EStatus::Follower, Some(leader)); + let (mutex, condvar) = &*self.cv; + let _guard = mutex.lock().unwrap(); + condvar.notify_all(); + Ok(()) } - pub async fn read(&'_ self) -> RwLockReadGuard<'_, EStatus> { - self.0.read().await + + pub fn connection_pending() -> Status { + let inner = Arc::new(RwLock::new((EStatus::ConnectionPending, None))); + let cv = Arc::new((Mutex::new(()), Condvar::new())); + Status { inner, cv } } - pub async fn write(&'_ self) -> RwLockWriteGuard<'_, EStatus> { - self.0.write().await + + pub async fn leader(&self) -> Option { + self.inner.read().await.1.clone() } -} -#[derive(Clone)] -pub struct Status { - t: PhantomData, + pub async fn is_pending(&self) -> bool { + matches!(&self.inner.read().await.0, EStatus::ConnectionPending) + } + + pub async fn is_candidate(&self) -> bool { + matches!(&self.inner.read().await.0, EStatus::Candidate) + } + + pub async fn is_leader(&self) -> bool { + matches!(&self.inner.read().await.0, EStatus::Leader) + } + + pub async fn is_follower(&self) -> bool { + matches!(&self.inner.read().await.0, EStatus::Follower) + } } diff --git a/src/state/node.rs b/src/state/node.rs index 44e7de3..520f455 100644 --- a/src/state/node.rs +++ b/src/state/node.rs @@ -1,57 +1,27 @@ // License: // This source code is licensed under the GPLv3 license, you can found the // LICENSE file in the root directory of this source tree. - -//! Implementation of useful [Node] methods for changing his state. - use super::EStatus; -use crate::{ - common::error::{throw, Error, ErrorResult}, - Node, -}; -use tracing::trace; +use crate::{common::error::ErrorResult, state::Url, Node}; impl Node { - /// Set the `node` status to candidate, return an error if he can't. - pub(crate) async fn set_status_to_candidate(&self) -> ErrorResult<()> { - let mut e_status = self.p_status.write().await; - let status = match &*e_status { - EStatus::Follower(status) => status.clone(), - _ => throw!(Error::WrongStatus), - }; - *e_status = EStatus::Candidate(status.into()); + pub(crate) async fn switch_to_candidate(&self) -> ErrorResult<()> { + self.p_status.switch_to_candidate().await?; + self.hook.switch_status(EStatus::Candidate); Ok(()) } - /// Set the `node` status to leader, return an error if he can't. - pub(crate) async fn set_status_leader(&self) -> ErrorResult<()> { - let mut e_status = self.p_status.write().await; - let status = match &*e_status { - EStatus::Candidate(status) => status.clone().into(), - EStatus::ConnectionPending(status) => status.clone().into(), - _ => throw!(Error::WrongStatus), - }; - *e_status = EStatus::Leader(status); + pub(crate) async fn switch_to_leader(&self) -> ErrorResult<()> { + self.p_status.switch_to_leader().await?; + self.hook.switch_status(EStatus::Leader); Ok(()) } - /// Candidate --> Follower - /// Leader --> Follower - /// Follower --> Follower - pub(crate) async fn set_status_to_follower( - &self, - leader: String, - ) -> ErrorResult<()> { - trace!("set status to follower"); - let mut guard = self.p_status.write().await; - *self.leader.write().await = Some(leader.into()); - let status = match &*guard { - EStatus::Follower(_) => return Ok(()), - EStatus::Candidate(status) => status.clone().into(), - EStatus::Leader(status) => status.clone().into(), - EStatus::ConnectionPending(status) => status.clone().into(), - }; - *guard = EStatus::Follower(status); + pub(crate) async fn switch_to_follower(&self, leader: Url) -> ErrorResult<()> { + if !self.p_status.is_follower().await { + self.p_status.switch_to_follower(leader).await?; + self.hook.switch_status(EStatus::Follower); + } Ok(()) } } diff --git a/src/state/sm_impl.rs b/src/state/sm_impl.rs deleted file mode 100644 index 952f3a4..0000000 --- a/src/state/sm_impl.rs +++ /dev/null @@ -1,99 +0,0 @@ -// License: -// This source code is licensed under the GPLv3 license, you can found the -// LICENSE file in the root directory of this source tree. - -use super::{ - Candidate, ConnectionPending, EStatus, Follower, Leader, Status, StatusPtr, -}; -use std::sync::Arc; -use tokio::sync::RwLock; -use tracing::trace; - -// todo: we can try to create our own traits that allow us to give some -// arguments and check if the state flow is forbidden or not. -// (ex: assert!(!settings.follower, "A follower cannot be a leader");) - -impl From> for Status { - fn from(_: Status) -> Self { - // todo: assert!(!settings.follower, "A follower cannot be a leader"); - trace!("become a leader"); - Status::::default() - } -} - -impl From> for Status { - fn from(_: Status) -> Self { - trace!("become a follower"); - Status::::default() - } -} - -impl From> for Status { - fn from(_: Status) -> Self { - trace!("become a candidate"); - Status::::default() - } -} - -impl From> for Status { - fn from(_: Status) -> Self { - trace!("become a follower"); - Status::::default() - } -} - -impl From> for Status { - fn from(_: Status) -> Self { - trace!("become a leader"); - Status::::default() - } -} - -impl From> for Status { - fn from(_: Status) -> Self { - trace!("become a follower"); - Status::::default() - } -} - -impl Status { - /// Create a StatusPtr at the initial and unique status that we - /// can create: `ConnectionPending`. - pub fn create() -> StatusPtr { - trace!("initialize status to ConnectionPending"); - StatusPtr(Arc::new(RwLock::new(EStatus::ConnectionPending(Status::< - ConnectionPending, - > { - t: Default::default(), - })))) - } -} - -impl Status { - #[cfg(test)] - pub fn create() -> StatusPtr { - StatusPtr(Arc::new(RwLock::new(EStatus::Leader(Status:: { - t: Default::default(), - })))) - } -} - -impl Status { - #[cfg(test)] - pub fn create() -> StatusPtr { - StatusPtr(Arc::new(RwLock::new(EStatus::Follower( - Status:: { - t: Default::default(), - }, - )))) - } -} - -// Contain the number of nodes -impl Status { - fn default() -> Self { - Status:: { - t: Default::default(), - } - } -} diff --git a/src/workflow/append_term.rs b/src/workflow/append_term.rs index ca20502..13ec5be 100644 --- a/src/workflow/append_term.rs +++ b/src/workflow/append_term.rs @@ -10,22 +10,14 @@ use crate::{ api::io_msg::{AppendTermInput, AppendTermResult}, common::error::ErrorResult, - log_entry::Term, node::Node, }; -use std::cmp::min; -use tracing::{debug, trace, trace_span}; +use tracing::{debug, trace, trace_span, warn}; impl Node { - // todo: a new incommers should be able to connect with a prepared - // list of logs. (Previous term and commit index inside the settings? - // or a memmapped file? or a hook?) - // The leader can be able to reject a really - // unsynchronized node. - /// Reception of a append_term request. /// - /// - check inputs, if it's enough uptodate, if we have the previous term. + /// - check inputs, if it's enough updated, if we have the previous term. /// - increment the heartbeat timeout /// - call hook pre_append_term /// - set status to follower if input term > local term @@ -41,103 +33,117 @@ impl Node { input.term.id, input.term.content, ); - debug!( - "received new term {}, prev {}, entries size {}", - input.term.id, - input.prev_term.id, - input.entries.len() - ); + debug!("received new term {:#?}", input,); self.internal_receive_append_term(input).await } /// internal implementation of receive append term + /// + /// Specification + /// 1. Reply false if term < currentTerm (Β§5.1) + /// 2. Reply false if log doesn’t contain an entry at prevLogIndex + /// whose term matches prevLogTerm (Β§5.3) + /// 3. If an existing entry conflicts with a new one (same index + /// but different terms), delete the existing entry and all that + /// follow it (Β§5.3) + /// 4. Append any new entries not already in the log + /// 5. If leaderCommit > commitIndex, set commitIndex = + /// min(leaderCommit, index of last new entry async fn internal_receive_append_term( &self, input: AppendTermInput, ) -> ErrorResult { - let mut current_term = self.p_current_term.lock().await.clone(); - let checks = self.check_input(&input, ¤t_term).await; + let checks = self.check_input(&input).await; if let Err(res) = checks { trace!("request rejected by checks"); return Ok(res); } - self.increment_heartbeat_timeout().await; - if !self.hook.pre_append_term(&input.term) { - trace!("request rejected by script"); - return Ok(AppendTermResult { - current_term, - success: false, - }); - } - trace!("request {} has passed checks", input.term.id); - if input.term.id > current_term.id { - // If RPC request or response contains term T > currentTerm: - // set currentTerm = T, convert to follower - trace!("_input.term.id > current_term.id_"); - self.set_status_to_follower(input.leader_id.clone()).await?; - } - self.update_local_entries(&input).await; - self.replace_latest_if_necessary(&input, &mut current_term) - .await; - *self.p_current_term.lock().await = current_term.clone(); - Ok(AppendTermResult { - current_term, - success: true, - }) - } + self.reset_timeout().await; - /// Update the local entries if there is a diff. And commit the entry if - /// the commit index is updated. - async fn update_local_entries(&self, input: &AppendTermInput) { - let mut latest_id = self.append_entries(input).await; - let mut index_guard = self.p_commit_index.lock().await; - if input.leader_commit_index > *index_guard { - latest_id = min(input.leader_commit_index, latest_id); - self.commit_entries(*index_guard, latest_id).await; - *index_guard = latest_id; + if input.prev_term.id == 1 { + trace!("pre/append root term"); + if let Some(index) = self.hook.pre_append_term(&input.prev_term) { + if index < input.prev_term.id { + trace!("root term rejected by checks pre append term"); + return Ok(AppendTermResult { + current_term: self.logs.lock().await.current_term(), + success: false, + }); + } + self.logs.lock().await.insert(&input.prev_term); + self.hook.append_term(&input.prev_term); + } else { + panic!("request rejected in pre append term"); + } } - } - /// Append all entries in the local logs. If we found an entry with a - /// conflict, we remove all the successors (done in the insert function) - /// - /// - If two entries in different logs have the same index - /// and term, then they store the same command. - /// - If two entries in different logs have the same index - /// and term, then the logs are identical in all preceding - /// entries. - async fn append_entries(&self, input: &AppendTermInput) -> usize { - let mut a = input.entries.iter().collect::>(); - a.sort_by_key(|t| t.id); - let mut logs = self.logs.lock().await; - for entry in a.iter() { - trace!("append entry {}", entry.id); - if logs.insert(entry) { - self.hook.append_term(entry); + // Pre/Append entries (from prev term to term excluded) + if !input.entries.is_empty() { + let mut entries = input.entries.iter().collect::>(); + // Sort entries from oldest to newest + entries.sort_by_key(|t| t.id); + for term in entries { + if term.id <= self.logs.lock().await.commit_index() { + // ignore committed term + continue; + } + + // pre append term send the last index I don't have + // (the first missing term). + // + // - if term from input == last I don't have => OK + // - return that index - 1 (the last I have / current term) otherwise + if let Some(index) = self.hook.pre_append_term(term) { + if index < term.id { + trace!( + "term (entries) {} rejected by checks pre append term", + index + ); + return Ok(AppendTermResult { + current_term: self.logs.lock().await.current_term(), + success: false, + }); + } + self.logs.lock().await.insert(term); + self.hook.append_term(term); + } else { + // todo: throw an internal error + panic!("request rejected in pre append term"); + } } } - logs.insert(&input.term); - trace!("append entry {}", input.term.id); - self.hook.append_term(&input.term); - input.term.id - } - /// If local current term is different, replace it with the input. - async fn replace_latest_if_necessary( - &self, - input: &AppendTermInput, - current_term: &mut Term, - ) { - if input.term.id != current_term.id - || input.term.content != current_term.content - { - trace!( - "update local term from {} to {}", - current_term.id, - input.term.id - ); - *current_term = input.term.clone(); + if let Some(index) = self.hook.pre_append_term(&input.term) { + if index < input.term.id { + trace!("term {} rejected by checks pre append term", index); + return Ok(AppendTermResult { + current_term: self.logs.lock().await.current_term(), + success: false, + }); + } + self.logs.lock().await.insert(&input.term); + self.hook.append_term(&input.term); + } else { + panic!("request rejected in pre append term"); } + + trace!("request {} has passed checks", input.term.id); + + // Finally commit the entries up to leader_commit_index, + // stopping at the latest entry we have in cache + self.commit_entries(input.leader_commit_index).await; + + // Since leader care about that the last term (term in the input) + // is the biggest term we should know at the end of AppendEntries. + // We can be almost sure that current term IS the input term here. + let current_term = self.logs.lock().await.current_term(); + let current_term_id = current_term.id; + + trace!("append term success. latest term: {:#?}", current_term); + Ok(AppendTermResult { + current_term, + success: current_term_id <= input.term.id, + }) } /// Check if the current term is at least equals to the term local. @@ -145,51 +151,53 @@ impl Node { /// Also send an error and inform the leader about his last current_term /// to adapt his own `next_indexes` table and ensure the logs consistency /// in the next call. - async fn check_input( - &self, - input: &AppendTermInput, - current_term: &Term, - ) -> Result<(), AppendTermResult> { + async fn check_input(&self, input: &AppendTermInput) -> Result<(), AppendTermResult> { + let mut logs_guard = self.logs.lock().await; + let current_term = logs_guard.current_term(); if input.term.id < current_term.id { trace!("term id older than local state"); return Err(AppendTermResult { - current_term: current_term.clone(), + current_term, success: false, }); } - let mut logs_guard = self.logs.lock().await; - if self.p_status._is_pending().await { - trace!("append entry {}", input.prev_term.id); - logs_guard.insert(&input.prev_term); - return Ok(()); - // todo: we can write a setting that allow user to force to be hardly - // synchronized for the connection. In that case, just do the - // next check and skip this one. - } - if !logs_guard.contains(&input.prev_term) { - trace!("unknow previous term of the request"); + + if input.leader_commit_index < logs_guard.commit_index() { + trace!("leader commit index invalid"); return Err(AppendTermResult { - current_term: current_term.clone(), + current_term, success: false, }); } - Ok(()) - } - /// Increment the heartbeat timeout if it's initialized - async fn increment_heartbeat_timeout(&self) { - // todo: in the dyn timeout crate, add a max value to wait - let opt_heartbeat_guard = self.opt_heartbeat.lock().await; - if opt_heartbeat_guard.is_none() { - trace!("heartbeat not initialized"); - return; - } - trace!("increment heartbeat timeout"); - if let Some(heartbeat) = &*opt_heartbeat_guard { - heartbeat - .add(self.settings.get_randomized_inc_timeout()) - .await - .unwrap(); // todo: add from settings, remove unwraps (and dismiss lib panic) + // 2. Reply false if log doesn’t contain an entry at prevLogIndex + // whose term matches prevLogTerm (Β§5.3) + if let Some(local_term) = logs_guard.find(input.prev_term.id) { + // found in cache + if local_term != input.prev_term { + // 3. If an existing entry conflicts with a new one (same index + // but different terms), delete the existing entry and all that + // follow it (Β§5.3) + logs_guard.insert(&input.prev_term); + self.hook.append_term(&input.prev_term); + } + } else if input.prev_term.id <= logs_guard.commit_index() { + // has been committed + // todo: add a hook here like "check terms validity" to verify + // if it match correctly with local terms + } else if input.prev_term.id == 1 { + // its also OK to receive a root term once. + // todo: accept once + } else { + warn!("unable to find the previous term"); + return Err(AppendTermResult { + current_term, + success: false, + }); } + + let _ = self.switch_to_follower(input.leader_id.clone().into()) + .await; + Ok(()) } } diff --git a/src/workflow/candidate.rs b/src/workflow/candidate.rs index f9a366e..84d53a5 100644 --- a/src/workflow/candidate.rs +++ b/src/workflow/candidate.rs @@ -13,7 +13,7 @@ //! - if alone in the network, start to be a leader //! //! When the candidature process finish, if the node is still a candidate, wait -//! a random time and restat a candidature. +//! a random time and restart a candidature. //! //! If candidate is a leader at the end of the workflow, start the leader //! workflow. [crate::workflow::leader] @@ -29,7 +29,7 @@ use crate::{ node::Node, Settings, }; -use tracing::{trace, warn}; +use tracing::{debug, trace, warn}; impl Node { /// - On conversion to candidate, start election: @@ -42,47 +42,53 @@ impl Node { /// follower /// - If election timeout elapses: start new election pub async fn run_candidate(&self) -> ErrorResult<()> { - let (last_term, term) = { + let (commit_index, mut last_term) = { let mut logs = self.logs.lock().await; - (logs.back().unwrap(), logs.append(String::new())) + let commit_index = logs.last_index(); // todo: wrong names + if let Some((_, id)) = &*self.vote_for.read().await { + if commit_index <= *id { + return Ok(()); + } + } + (commit_index, logs.append("candidature".into())) }; - { - *self.p_current_term.lock().await = term.clone(); - *self.vote_for.write().await = Some(( - format!("{}:{}", self.settings.addr, self.settings.port), - last_term.clone(), - )); - } + self.next_indexes.write().await.clear(); + self.hook.append_term(&last_term); + *self.vote_for.write().await = Some((self.settings.node_id.clone(), commit_index)); + while self - .start_candidature(last_term.clone(), term.clone()) + .start_candidature(commit_index, last_term.clone()) .await - {} + { + last_term = self.logs.lock().await.append("candidature".into()); + self.hook.append_term(&last_term); + *self.vote_for.write().await = Some((self.settings.node_id.clone(), commit_index)); + } + Ok(()) } - async fn start_candidature( - &self, - last_term: LogEntry, - term: LogEntry, - ) -> bool { - let res = self.async_calls_candidature(last_term, term).await; + async fn start_candidature(&self, commit_index: usize, last_term: LogEntry) -> bool { + let res = self.async_calls_candidature(commit_index, last_term).await; + // clean vote + *self.vote_for.write().await = None; + trace!("candidature finished!"); if res { - self.set_status_leader().await.unwrap(); + self.switch_to_leader().await.unwrap(); + return false; + } + if self.p_status.is_follower().await { return false; } let dur = self.settings.get_randomized_timeout(); - let p_st = self.p_status.clone(); + debug!("wait {:?} before a new timeout", dur); tokio::time::sleep(dur).await; - p_st._is_candidate().await && !res + self.p_status.is_candidate().await } - async fn async_calls_candidature( - &self, - last_term: LogEntry, - term: LogEntry, - ) -> bool { + async fn async_calls_candidature(&self, commit_index: usize, last_term: LogEntry) -> bool { let nodes = self.node_list.read().await.clone(); if nodes.is_empty() { trace!("no other nodes, will turn into a leader by default"); @@ -91,19 +97,27 @@ impl Node { let len = nodes.len(); let mut granted_vote_count = 0; for node in nodes { + if self.p_status.is_follower().await { + return false; + } // todo: manage all warning, (return an error and stop the node) call_candidature( &node.into(), &self.settings, - &term, &last_term, + commit_index, &mut granted_vote_count, ) .await } - let r = granted_vote_count as f64 / len as f64; - trace!("candidature finished with score {}", r); - r >= 0.5 // todo: make that test in settings or in a hook + + trace!("candidature finished with score {}", granted_vote_count); + if let Some((vote, _)) = &*self.vote_for.read().await { + if *vote == format!("{}:{}", self.settings.addr, self.settings.port) { + return granted_vote_count + 1 > (len / 2); // todo use quorum from settings + } + } + granted_vote_count > (len / 2) // todo use quorum from settings } } @@ -111,8 +125,8 @@ impl Node { async fn call_candidature( target: &Url, settings: &Settings, - term: &LogEntry, last_term: &LogEntry, + commit_index: usize, granted_vote_count: &mut usize, ) { // todo: we may want in case of fail make a hook @@ -120,14 +134,15 @@ async fn call_candidature( target, settings, RequestVoteInput { - candidate_id: format!("{}:{}", settings.addr, settings.port), - term: term.clone(), - last_term: last_term.clone(), + candidate_id: settings.node_id.clone(), + term: last_term.clone(), + last_term: commit_index, }, ) .await { Ok(res) => { + debug!("vote request response received {:#?}", res); if res.vote_granted { *granted_vote_count += 1; } diff --git a/src/workflow/follower.rs b/src/workflow/follower.rs index bb3d135..f8e441c 100644 --- a/src/workflow/follower.rs +++ b/src/workflow/follower.rs @@ -2,14 +2,12 @@ // This source code is licensed under the GPLv3 license, you can found the // LICENSE file in the root directory of this source tree. -//! Implementation of the folower workflow, a follower start a heartbeat +//! Implementation of the follower workflow, a follower start a heartbeat //! timeout if node's settings say it's not a pure follower (can be candidate) //! If the node is a follower follower, doesn't start any timeout. use crate::{common::error::ErrorResult, node::Node}; -use dyn_timeout::tokio_impl::DynTimeout; -use tokio::sync::mpsc; -use tracing::trace; +use tracing::{debug, trace}; impl Node { /// Start follower workflow @@ -20,22 +18,39 @@ impl Node { tokio::select! { _ = tokio::signal::ctrl_c() => {} }; Ok(()) } else { - let (sender, mut receiver) = mpsc::channel::<()>(1); - let dur = self.settings.get_randomized_timeout(); - let mut dyn_timeout = DynTimeout::with_sender(dur, sender); - dyn_timeout - .set_max_waiting_time(self.settings.get_max_timeout_value()); - trace!("start timeout"); - *self.opt_heartbeat.lock().await = Some(dyn_timeout); - tokio::select! { - _ = receiver.recv() => { - trace!("heartbeat timeout!"); - *self.opt_heartbeat.lock().await = None; - self.set_status_to_candidate().await? - } - _ = tokio::signal::ctrl_c() => {} + self.reset_timeout().await; + while self.p_status.is_follower().await { + self.p_status.wait(); } Ok(()) } } + + pub async fn reset_timeout(&self) { + let p_heartbeat = self.heartbeat.clone(); + let p_status = self.p_status.clone(); + let (send, mut recv) = tokio::sync::oneshot::channel::<()>(); + let dur = self.settings.get_randomized_timeout(); + + let mut heartbeat = self.heartbeat.lock().await; + if heartbeat.is_some() { + // cancel previous heartbeat timeout by triggering + // the `recv` branch in the select bellow + heartbeat.take(); + } + *heartbeat = Some(send); + tokio::spawn(async move { + debug!("start new timeout"); + let sleep = tokio::time::sleep(dur); + tokio::pin!(sleep); + tokio::select! { + _ = &mut recv => debug!("cancel previous timeout"), + _ = &mut sleep => { + debug!("branch heartbeat timeout reached"); + p_heartbeat.lock().await.take(); + let _ = p_status.switch_to_candidate().await; + } + } + }); + } } diff --git a/src/workflow/init.rs b/src/workflow/init.rs index 6a073c6..099c691 100644 --- a/src/workflow/init.rs +++ b/src/workflow/init.rs @@ -4,14 +4,13 @@ use crate::{ api::{client, io_msg::UpdateNodeResult, server}, - common::error::{errors, throw, Error, ErrorResult}, + common::error::{throw, Error, ErrorResult}, node::{Node, NodeInfo}, - state::EStatus, }; -use tracing::trace; +use tracing::{trace, warn}; impl Node { - /// First workflow described in `init.md` specification. The folowing workflow + /// First workflow described in `init.md` specification. The following workflow /// is called only once time when the node start. /// /// - Start a server in a tokio task (then run in background). @@ -26,109 +25,54 @@ impl Node { /// The error should be managed by the root caller of the function and cause /// at the end a graceful stop of the node. pub async fn initialize(&self) -> ErrorResult<()> { - { - let e_status = self.p_status.read().await; - match &*e_status { - EStatus::ConnectionPending(status) => status.clone(), - _ => throw!(Error::WrongStatus), - } - }; + if !self.p_status.is_pending().await { + throw!(Error::WrongStatus) + } let node_clone = self.clone(); tokio::spawn(async move { server::new(node_clone).await }); if self.settings.nodes.is_empty() { eprintln!("warn: No nodes known, may be a configuration error"); - if self.settings.follower { - throw!(errors::ERR_FOLLOWER_MUST_HAVE_KNOWN) - } else { - self.set_status_leader().await?; - return Ok(()); + } + if !self.connect_to_leader().await? { + // No connection possible, turn into a follower. + if !self.settings.follower { + self.switch_to_candidate().await?; } } - self.connect_to_leader().await?; Ok(()) } /// Workflow function on receive a connection request. Connection request are - /// done by a `update_node` call. Take a `UpdateNodeInput` conatining the + /// done by a `update_node` call. Take a `UpdateNodeInput` containing the /// calling node and if he want to be a follower. /// /// If you are a leader, you add the follower into a pool that is managed in /// the workflow `On heartbeat timeout` in `leader_workflow`. /// - /// Whatever your status, if the script `update-node` succed it returns + /// Whatever your status, if the script `update-node` succeed it returns /// an `UpdateNodeResult` and none otherwise. - pub async fn receive_connection_request( - &self, - input: NodeInfo, - ) -> Option { + pub async fn receive_connection_request(&self, input: NodeInfo) -> Option { trace!("receive connection request from {}", input.addr); if !self.hook.update_node() { return None; } - if let EStatus::Leader(_) = &*self.p_status.read().await { - self.node_list.write().await.remove(&input.addr); - self.follower_list.write().await.remove(&input.addr); - self.push_new_waiting_node(input).await; + if self.p_status.is_leader().await { + // self.node_list.write().await.remove(&input.addr); return Some(UpdateNodeResult { - leader_id: format!( - "{}:{}", - self.settings.addr, self.settings.port - ), - node_list: self - .node_list - .read() - .await - .iter() - .cloned() - .collect(), - follower_list: self - .follower_list - .read() - .await - .iter() - .cloned() - .collect(), + leader_id: format!("{}:{}", self.settings.addr, self.settings.port), + node_list: self.get_node_list().await, }); }; - match &*self.leader.read().await { - Some(leader_id) => Some(UpdateNodeResult { - leader_id: leader_id.to_string(), - node_list: self - .node_list - .read() - .await - .iter() - .cloned() - .collect(), - follower_list: self - .follower_list - .read() - .await - .iter() - .cloned() - .collect(), - }), - None => { - eprintln!( - "cannot create an `UpdateNodeResult` without a leader" - ); - None - } - } - } - async fn push_new_waiting_node(&self, node_info: NodeInfo) { - trace!("leader event: push a new waiting node {}", node_info.addr); - let ser = serde_json::to_string(&node_info).unwrap(); - let mut waiting_nodes = self.waiting_nodes.lock().await; - if waiting_nodes.contains(&ser) { - eprintln!( - "warn: url `{}` already in the connection pool", - &node_info.addr - ); + let leader_id = if let Some(leader_id) = self.p_status.leader().await { + leader_id.to_string() } else { - waiting_nodes.push_back(ser); - } + String::new() + }; + Some(UpdateNodeResult { + leader_id, + node_list: self.get_node_list().await, + }) } /// Called in `initialize` for the connection to a leader. Try to connect to each known @@ -137,18 +81,13 @@ impl Node { /// # Error /// The error should be managed by the root caller of the function and cause /// at the end a graceful stop of the node. - async fn connect_to_leader(&self) -> ErrorResult<()> { + async fn connect_to_leader(&self) -> ErrorResult { let mut success = false; let mut to_leader = false; for url in self.settings.nodes.iter() { - match client::post_update_node( - &url.into(), - &self.settings, - self.uuid, - ) - .await - { + match client::post_update_node(&url.into(), &self.settings, self.uuid).await { Ok(result) => { + /* Succeed to send an update node request */ success = true; to_leader = result.leader_id == *url; self.update(result).await; @@ -162,40 +101,44 @@ impl Node { }; } if !success { - throw!(Error::ImpossibleToBootstrap); + /* No connection possible */ + return Ok(false); } trace!("connection {} to leader {}", success, to_leader); if !to_leader { - let leader = self.leader.read().await.clone().unwrap(); - match client::post_update_node( - &leader.clone(), - &self.settings, - self.uuid, - ) - .await - { + /* On bootstrap, if the leader is unknown, we just wait + * for an event. A new candidate can show up, or we can + * become a leader on a timeout. + * + * If we know who's the leader, we want to signal that we + * exist and we're actually running. */ + let leader = match self.p_status.leader().await { + Some(leader) => leader, + _ => return Ok(false), + }; + match client::post_update_node(&leader.clone(), &self.settings, self.uuid).await { Ok(result) => self.update(result).await, Err(warn) => { - throw!(Error::CannotStartRpcServer(format!( + warn!( "Failed to connect to the leader: `{}`\n{:indent$?}", leader, *warn, indent = 2 - ))) + ); + return Ok(false); } } } trace!("connection success"); - Ok(()) + Ok(true) } async fn update(&self, result: UpdateNodeResult) { trace!("update leader {}", result.leader_id); - self.node_list.write().await.extend(result.node_list); - self.follower_list - .write() + // self.node_list.write().await.extend(result.node_list); + self.p_status + .switch_to_follower(result.leader_id.into()) .await - .extend(result.follower_list); - *self.leader.write().await = Some(result.leader_id.into()); + .unwrap(); } } diff --git a/src/workflow/leader.rs b/src/workflow/leader.rs index 2eb3fb6..4e14127 100644 --- a/src/workflow/leader.rs +++ b/src/workflow/leader.rs @@ -5,17 +5,17 @@ use crate::{ api::{client, io_msg::AppendTermResult, Url}, common::error::ErrorResult, - log_entry::Entries, + log_entry::{Entries, Term}, node::Node, - state::StatusPtr, + state::Status, Hook, }; use std::{ - collections::{HashMap, HashSet, VecDeque}, + collections::{HashSet, VecDeque}, sync::Arc, }; use tokio::sync::{Mutex, RwLock}; -use tracing::{trace, warn}; +use tracing::{debug, trace, warn}; /// Local enum used to trace how `post_new_append_term` worked /// See also `Node::manage_append_term_result` @@ -24,7 +24,7 @@ enum ReactResult { Retry, /// Break the call loop Break, - /// Continue th call loop + /// Continue the call loop Continue, } @@ -37,25 +37,17 @@ impl Node { /// /// Run the loops until someone else take the lead or handle ctrl_c. /// Leader understand if someone took the lead if another node is more - /// uptodate. + /// updated. /// /// Look at the Raft documentation for more information. pub async fn run_leader(&self) -> ErrorResult<()> { self.start_loop_term_preparation(); loop { - if !self.p_status._is_leader().await { + if !self.p_status.is_leader().await { + trace!("stop lead"); break; } - let send_term_period = self.settings.get_send_term_sleep_duration(); - if matches!(self.internal_run_leader().await?, ReactResult::Break) { - break; - } - let sleep = tokio::time::sleep(send_term_period); - tokio::pin!(sleep); - tokio::select! { - _ = sleep => {} - _ = tokio::signal::ctrl_c() => break, - }; + let _ = self.internal_run_leader().await?; } Ok(()) } @@ -64,73 +56,65 @@ impl Node { /// /// Sends new terms for each nodes in the network async fn internal_run_leader(&self) -> ErrorResult { - self.increment_commit_term().await; - let mut nodes = self.node_list.read().await.clone(); - nodes.extend(self.follower_list.read().await.clone()); - if nodes.is_empty() { - let logs = self.logs.lock().await; - if let Some(term) = logs.back() { - if let Some(u) = term.parse_conn() { - nodes.insert(u.addr); - } - } - } - // todo: try to randomize the nodes list. We want to test with - // measurements if it's more efficients with a lot of nodes. - // (need to define a test case before!!) + let nodes = self.node_list.read().await.clone(); + let mut fail_count = 0; + trace!("start a sending session as leader"); for node in nodes.iter() { - if let ReactResult::Break = - self.post_new_append_term(node.into()).await? + if let ReactResult::Break = self + .post_new_append_term(node.into(), &mut fail_count) + .await? { return Ok(ReactResult::Break); } } + + // Increment the commit term after the calls + self.increment_commit_term().await; + + if fail_count > (self.node_list.read().await.len() / 2) { + warn!("quorum is unreachable, switch to candidate"); + self.switch_to_candidate().await?; + return Ok(ReactResult::Break); + } + Ok(ReactResult::Continue) } /// Compute and post a new [AppendTermInput](crate::api::io_msg::AppendTermInput) - /// to the `target`. Manage internaly the result. + /// to the `target`. Manage internally the result. /// See `manage_append_term_result`. async fn post_new_append_term( &self, target: Url, + fail_count: &mut usize, ) -> ErrorResult { - let mut retry = true; + let mut retry = 100; loop { + if !self.p_status.is_leader().await { + return Ok(ReactResult::Break); + } let url = target.clone(); let append_term_input = self.create_term_input(&url).await; - match client::post_append_term( - &url, - &self.settings, - append_term_input, - ) - .await - { + match client::post_append_term(&url, &self.settings, append_term_input).await { Ok(result) => { - let react = - self.manage_append_term_result(url, result).await?; + let react = self.manage_append_term_result(url, result).await?; match react { ReactResult::Retry => { warn!("retry call to {}", target); - if retry { - retry = false; - } else { + retry -= 1; + if retry == 0 { warn!("failed multiple call to {}", target); return Ok(ReactResult::Continue); } } ReactResult::Break => return Ok(ReactResult::Break), - ReactResult::Continue => { - return Ok(ReactResult::Continue) - } + ReactResult::Continue => return Ok(ReactResult::Continue), } } Err(p_warn) => { - warn!("{}, remove node from our index", *p_warn); - let mut a = self.node_list.write().await; - a.remove(&target.to_string()); - let mut a = self.follower_list.write().await; - a.remove(&target.to_string()); + warn!("{}", *p_warn); + // Not sure if we want to ban node, call a hook instead + *fail_count += 1; return Ok(ReactResult::Continue); } } @@ -151,7 +135,7 @@ impl Node { /// # Result /// /// Can return a [ReactResult] - /// - continue means we're ok, send to next node. + /// - continue means we're OK, send to next node. /// - retry means that we should retry to send an append_term message to /// the node. /// - break means that we're now a follower, break all previous loops and @@ -161,77 +145,110 @@ impl Node { target: Url, result: AppendTermResult, ) -> ErrorResult { + use crate::node::NextIndex::{Pending, Validated}; + { let mut next_indexes_guard = self.next_indexes.write().await; - if result.current_term > *self.p_current_term.lock().await { + if result.current_term.id > self.logs.lock().await.last_index() { trace!( - "{} became leader with term {}", - target, + "{target} became leader with term {}", result.current_term.id ); - self.set_status_to_follower(target.to_string()).await?; + self.switch_to_follower(target).await?; return Ok(ReactResult::Break); } - next_indexes_guard.insert(target.clone(), result.current_term.id); + + if result.current_term.id > 0 { + if let Some(local_term) = self.leader_retreive_term(result.current_term.id).await { + if local_term == result.current_term { + debug!( + "node return a current term {:#?} validated", + result.current_term + ); + next_indexes_guard + .insert(target.clone(), Validated(result.current_term.id)); + } else { + debug!( + "node return a unmatched current term {:#?}", + result.current_term + ); + next_indexes_guard + .insert(target.clone(), Pending(result.current_term.id - 1)); + } + } else { + warn!("leader can't find a term"); + } + } else { + next_indexes_guard.insert(target.clone(), Validated(1)); + } } + if result.success { - trace!("successfuly sent term to {}", target); + trace!("successfully sent term to {}", target); Ok(ReactResult::Continue) } else { - trace!("retry to send to {}", target); + trace!("retry to send to {} after {:#?}", target, result); Ok(ReactResult::Retry) } } + async fn leader_retreive_term(&self, index: usize) -> Option { + if let Some(term) = self.logs.lock().await.find(index) { + return Some(term); + } + if let Some(term) = self.hook.retreive_term(index) { + return Some(term); + } + None + } + async fn increment_commit_term(&self) { - let mut rates = HashMap::::new(); - let mut max = 0; - let mut max_v = 0; - // todo: put that percent value in the setting file - // todo: assert if 0 in node creation - let percent = 55; - // todo: allow user to be explicit (consider also follower or waiting - // nodes in tt_len) let nodes = self.node_list.read().await; let len = nodes.len(); if nodes.is_empty() { - std::mem::drop(nodes); - let latest = self.logs.lock().await.back().unwrap().id; - let mut index = self.p_commit_index.lock().await; - if latest != *index { - trace!("update commited index to {} by default", latest); - self.commit_entries(*index, latest).await; - *index = latest; - } + trace!("pass commit phase with no nodes"); return; } - for (url, next_index) in self.next_indexes.read().await.iter() { - if !nodes.contains(&url.to_string()) { - continue; + std::mem::drop(nodes); + + let mut votes = Vec::<(usize, usize)>::new(); + let mut set = HashSet::::new(); + + for index in self + .next_indexes + .read() + .await + .iter() + .map(|(_, v)| v.validated()) + { + if !set.contains(&index) { + set.insert(index); + votes.push((index, 0)); + votes.sort_by_key(|(index, _)| *index); } - let n = if let Some(v) = rates.get_mut(next_index) { - *v += 1; - *v - } else { - rates.insert(*next_index, 1); - 1 - }; - if n > max_v || n == max_v && max < *next_index { - max = *next_index; - max_v = n; + for (i, n) in votes.iter_mut() { + if *i <= index { + *n += 1 + } else { + break; + } } } - std::mem::drop(nodes); - let mut index = self.p_commit_index.lock().await; - trace!("better rated term {} scored {}", max, max_v); - if max_v > len * percent / 100 && max > *index { - trace!("update commited index to {} thanks to majority", max); - self.commit_entries(*index, max).await; - *index = max; + + let mut max_term_id = 0; + debug!("check latest commit: votes {:?}", votes); + for (i, n) in votes { + // todo: take quorum from settings + if n >= (len / 2) { + max_term_id = i; + } } + + trace!("better rated term {max_term_id}"); + self.commit_entries(max_term_id).await; } - /// Start a loop that prepare terms in parrallel. Fill the local `logs` + /// Start a loop that prepare terms in parallel. Fill the local `logs` /// parameter of the node fn start_loop_term_preparation(&self) { let p_logs = self.logs.clone(); @@ -240,19 +257,12 @@ impl Node { let waiting_nodes = self.waiting_nodes.clone(); let hook = self.hook.clone(); let nodes = self.node_list.clone(); - let followers = self.follower_list.clone(); // todo: remove unwraps and handle errors tokio::spawn(async move { loop { - let should_break = internal_term_preparation( - &p_logs, - &p_status, - &waiting_nodes, - &nodes, - &followers, - &hook, - ) - .await; + let should_break = + internal_term_preparation(&p_logs, &p_status, &waiting_nodes, &nodes, &hook) + .await; if should_break { break; } @@ -270,45 +280,36 @@ impl Node { #[cfg(test)] pub async fn _term_preparation( p_logs: &Arc>, - p_status: &StatusPtr, + p_status: &Status, waiting_nodes: &Arc>>, nodes: &Arc>>, - followers: &Arc>>, hook: &Arc>, ) -> bool { - internal_term_preparation( - p_logs, - p_status, - waiting_nodes, - nodes, - followers, - hook, - ) - .await + internal_term_preparation(p_logs, p_status, waiting_nodes, nodes, hook).await } async fn internal_term_preparation( p_logs: &Arc>, - p_status: &StatusPtr, + p_status: &Status, waiting_nodes: &Arc>>, nodes: &Arc>>, - followers: &Arc>>, hook: &Arc>, ) -> bool { - if !p_status._is_leader().await { + if !p_status.is_leader().await { // prepare term only if we are a Leader return true; } - if nodes.read().await.is_empty() && followers.read().await.is_empty() { + if nodes.read().await.is_empty() { // prepare term only if there is someone listening :-) let mut waiting_nodes_guard = waiting_nodes.lock().await; if !waiting_nodes_guard.is_empty() { - // create a term for the waiting node ;-) + // create a term for the waiting node trace!("starter connect term"); - p_logs + let term = p_logs .lock() .await .append(conn_term_preparation(&mut waiting_nodes_guard, hook)); + hook.append_term(&term); } return false; } @@ -320,7 +321,8 @@ async fn internal_term_preparation( } else { conn_term_preparation(&mut waiting_nodes_guard, hook) }; - p_logs.lock().await.append(term_content); + let term = p_logs.lock().await.append(term_content); + hook.append_term(&term); false } diff --git a/src/workflow/mod.rs b/src/workflow/mod.rs index e1d794f..0324159 100644 --- a/src/workflow/mod.rs +++ b/src/workflow/mod.rs @@ -3,7 +3,7 @@ // LICENSE file in the root directory of this source tree. //! Module declaration for all workflows described in initial specifications -//! Look at dev documentation for more informations +//! Look at dev documentation for more information pub mod append_term; pub mod candidate; diff --git a/src/workflow/request_vote.rs b/src/workflow/request_vote.rs index 9d1472b..c3f66e7 100644 --- a/src/workflow/request_vote.rs +++ b/src/workflow/request_vote.rs @@ -12,32 +12,38 @@ use crate::{ Node, }; +use tracing::{debug, trace}; + impl Node { /// Node reaction on receive a vote request. - pub async fn receive_request_vote( - &self, - input: RequestVoteInput, - ) -> RequestVoteResult { - let current_term = self.p_current_term.lock().await.clone(); + pub async fn receive_request_vote(&self, input: RequestVoteInput) -> RequestVoteResult { + trace!("receive a vote request {:#?}", input); + let current_term = self.logs.lock().await.current_term(); // todo: hook receive request vote if input.term.id < current_term.id { + debug!("refuse candidates because term < current"); return RequestVoteResult { current_term, vote_granted: false, }; } - let latest = self.logs.lock().await.back().unwrap(); let mut opt_vote = self.vote_for.write().await; + let vote_granted = if let Some(vote) = opt_vote.clone() { - input.last_term.id > vote.1.id + debug!("compare to previous vote"); + input.last_term > vote.1 || input.candidate_id == vote.0 } else { - input.last_term.id >= latest.id + debug!("compare to previous vote"); + input.last_term >= self.logs.lock().await.commit_index() }; + + debug!("vote granted: {vote_granted}"); if vote_granted { - *opt_vote = Some((input.candidate_id, input.last_term)) + *opt_vote = Some((input.candidate_id, input.last_term)); + self.reset_timeout().await } RequestVoteResult { - current_term, + current_term: self.logs.lock().await.current_term(), vote_granted, } } diff --git a/src/workflow/test/tests_send_term.rs b/src/workflow/test/tests_send_term.rs index c8518c8..55dac9a 100644 --- a/src/workflow/test/tests_send_term.rs +++ b/src/workflow/test/tests_send_term.rs @@ -47,7 +47,6 @@ async fn term_preparation_1() { &node.p_status, &node.waiting_nodes, &node.node_list, - &node.follower_list, &node.hook, ) .await; @@ -64,8 +63,7 @@ async fn term_preparation_1() { let mut c = logs.back().unwrap().content; c.drain(.."conn:".len()); - let conn_to: UpdateNodeInput = - serde_json::from_str(&c.to_string()).unwrap(); + let conn_to: UpdateNodeInput = serde_json::from_str(&c.to_string()).unwrap(); assert_eq!(conn_to.hash, hash); assert!(conn_to.follower); } @@ -76,13 +74,9 @@ async fn term_preparation_1() { async fn create_term_for_node_1() { let known_follower = Url::from("127.0.0.1:8081"); - let mut follower_list = HashSet::new(); - follower_list.insert(known_follower.to_string()); - // first time we speak with him, we don't know where he is in his logs let node = Node { - follower_list: Arc::new(RwLock::new(follower_list)), leader: Arc::new(RwLock::new(Some(Url::from("127.0.0.1:8080")))), logs: Arc::new(Mutex::new(Entries::default())), ..Node::_init( @@ -127,9 +121,6 @@ async fn create_term_for_node_1() { async fn create_term_for_node_2() { let known_follower = Url::from("127.0.0.1:8081"); - let mut follower_list = HashSet::new(); - follower_list.insert(known_follower.to_string()); - // first time we speak with him, we don't know where he is in his logs // initialize some logs @@ -140,7 +131,6 @@ async fn create_term_for_node_2() { entries.append("4th term".to_string()); let node = Node { - follower_list: Arc::new(RwLock::new(follower_list)), leader: Arc::new(RwLock::new(Some(Url::from("127.0.0.1:8080")))), logs: Arc::new(Mutex::new(entries)), ..Node::_init( @@ -163,9 +153,6 @@ async fn create_term_for_node_3() { // same as create_term_for_node_2 but target already know some logs let known_follower = Url::from("127.0.0.1:8081"); - let mut follower_list = HashSet::new(); - follower_list.insert(known_follower.to_string()); - let mut next_indexes = HashMap::default(); // target know 3 entries in logs next_indexes.insert(known_follower.clone(), 2); @@ -206,9 +193,6 @@ async fn create_term_for_node_4() { // same as create_term_for_node_3 but target already know other logs let known_follower = Url::from("127.0.0.1:8081"); - let mut follower_list = HashSet::new(); - follower_list.insert(known_follower.to_string()); - let mut next_indexes = HashMap::default(); // target know 2 entries in logs next_indexes.insert(known_follower.clone(), 1); @@ -221,7 +205,6 @@ async fn create_term_for_node_4() { entries.append("4th term".to_string()); let node = Node { - follower_list: Arc::new(RwLock::new(follower_list)), next_indexes: Arc::new(RwLock::new(next_indexes)), leader: Arc::new(RwLock::new(Some(Url::from("127.0.0.1:8080")))), logs: Arc::new(Mutex::new(entries)), @@ -250,9 +233,6 @@ async fn create_term_for_node_4() { async fn create_term_for_node_5() { let known_follower = Url::from("127.0.0.1:8081"); - let mut follower_list = HashSet::new(); - follower_list.insert(known_follower.to_string()); - let mut next_indexes = HashMap::default(); // target know everything next_indexes.insert(known_follower.clone(), 3); diff --git a/src/workflow/tools/commiting.rs b/src/workflow/tools/commiting.rs index 1cd5a44..27f0ec4 100644 --- a/src/workflow/tools/commiting.rs +++ b/src/workflow/tools/commiting.rs @@ -3,36 +3,32 @@ // LICENSE file in the root directory of this source tree. use crate::Node; -use tracing::{debug, trace}; +use tracing::{debug, trace, warn}; impl Node { - pub(crate) async fn commit_entries( - &self, - old_commit_index: usize, - new_commit_index: usize, - ) { - let logs = self.logs.lock().await; - for entry in logs.iter_range(old_commit_index..new_commit_index + 1) { - debug!("commit entry {}", entry.id); - trace!("commit entry {} {}", entry.id, entry.content); - // todo: make that a constant (the "conn") - if let Some(u) = entry.parse_conn() { - if u.hash == self.uuid { - trace!( - "local node has been accepted by the current leader" - ); - continue; // I'm ok with me - } - trace!("add a new node in the local index {}", u.addr); - if u.follower { - self.follower_list.write().await.insert(u.addr); - trace!("follower list incremented"); - } else { - self.node_list.write().await.insert(u.addr); - trace!("node list incremented"); + /// Commit up to the new commit index included. + /// Saturate and return if the index to commit isn't in cache. + pub(crate) async fn commit_entries(&self, new_commit_index: usize) { + let mut logs = self.logs.lock().await; + let from = logs.commit_index() + 1; + if from <= new_commit_index { + trace!("commit entries term from {from} to {new_commit_index}"); + for index in from..=new_commit_index { + if !logs.check_commit(index) { + debug!("stop commit at log term {index}"); + break; } + let term = match logs.find(index) { + Some(term) => term, + None => { + warn!("unable to find a log while committing"); + break; + } + }; + debug!("commit term {:?}", term); + logs.set_commit(index); + self.hook.commit_term(&term); } - self.hook.commit_term(&entry); } } } diff --git a/src/workflow/tools/leader_tools.rs b/src/workflow/tools/leader_tools.rs index 7fb8cfa..0057ab4 100644 --- a/src/workflow/tools/leader_tools.rs +++ b/src/workflow/tools/leader_tools.rs @@ -8,86 +8,115 @@ use crate::{ node::Node, }; use tokio::sync::MutexGuard; -use tracing::warn; +use tracing::debug; impl Node { - async fn get_prev_term<'a>( + /// Get term for target node + async fn get_target_term<'a>( &self, target_node: &Url, logs_guard: &mut MutexGuard<'a, Entries>, ) -> Term { + // If a node needs a specific term, we try to find it in the logs, + // otherwise we defer the job to the hook. + // + // If the node doesn't have a next_indexes registered, fill with the + // current term. match self.next_indexes.read().await.get(target_node) { Some(node_index) => { - let id = *node_index; + let id = node_index.unwrap(); match logs_guard.find(id) { Some(term) => term, - _ => logs_guard.back().unwrap(), + _ => self + .hook + .retreive_term(id) + .expect("Unable to retrieve term {id} as leader"), } } - _ => match logs_guard.back() { - Some(term) => term, - None => { - warn!("create an empty term in get previous context"); - logs_guard.append(String::new()); - Term { - id: logs_guard.len() - 1, - content: String::new(), - } + _ => { + // suppose the last term is in under the latest + // leader commit. Minimum index is 1. + let mut index = logs_guard.commit_index(); + index = index.saturating_sub(10); + if index == 0 { + index = 1; } - }, + self.hook + .retreive_term(index) + .expect("Unable to retrieve term {id} as leader") + } } } - async fn build_current_term<'a>( - &self, - logs_guard: &mut MutexGuard<'a, Entries>, - ) -> Term { - let ret = { - let o = match logs_guard.back() { - Some(term) => { - if term == *self.p_current_term.lock().await { - None - } else { - Some(term) - } - } - _ => None, + /// Creates a term especially for the `target_node` + pub(crate) async fn create_term_input(&self, target_node: &Url) -> AppendTermInput { + let mut logs_guard = self.logs.lock().await; + // prev term is the latest term the remote node should have + let prev_term = self.get_target_term(target_node, &mut logs_guard).await; + let (created, local_latest_term) = logs_guard.latest(); + if created { + self.hook.append_term(&local_latest_term); + } + let leader_id = self.settings.node_id.clone(); + let leader_commit_index = logs_guard.commit_index(); + + // Add up to 10 entries only + let mut pos = prev_term.id + 1; + let end = pos + 10; + + // Case 1: + // The latest term the remote has is also my term. + if prev_term == local_latest_term { + debug!("just send latest because previous term IS local latest"); + return AppendTermInput { + term: local_latest_term.clone(), + leader_id, + prev_term: local_latest_term, + entries: vec![], + leader_commit_index, }; - match o { - Some(term) => term, - None => { - warn!("create an empty term in get current context"); - logs_guard.append(String::new()); - Term { - id: logs_guard.len() - 1, - content: String::new(), - } + } else if pos >= local_latest_term.id { + debug!("just send latest because previous term is just before our local latest"); + return AppendTermInput { + term: local_latest_term.clone(), + leader_id, + prev_term: local_latest_term, + entries: vec![], + leader_commit_index, + }; + } + + // Case 2: + // The latest term the remote is older than our. + // :=> prev_term.id + 1 <= local_latest_term.id + // <=> pos <= local_latest_term.id + let entries = { + let mut retreived = vec![]; + while pos < end && pos < local_latest_term.id - 1 { + if let Some(term) = logs_guard.find(pos) { + retreived.push(term); + } else if let Some(term) = self.hook.retreive_term(pos) { + retreived.push(term); + } else { + panic!("impossible to retrieve a term as a leader"); } + pos += 1 } + retreived }; - ret - } - /// Creates a term especially for the `target_node` - pub(crate) async fn create_term_input( - &self, - target_node: &Url, - ) -> AppendTermInput { - let mut logs_guard = self.logs.lock().await; - let prev_term = self.get_prev_term(target_node, &mut logs_guard).await; - let term = self.build_current_term(&mut logs_guard).await; - let leader_id = - format!("{}:{}", self.settings.addr, self.settings.port); - let entries = if prev_term.id + 1 < term.id { - match logs_guard.get_copy_range(prev_term.id + 1..term.id) { - Some(e) => e, - _ => Entries::new(), - } + // Limit the knowledge of distant node to the latest + // term. Note: I'm not so sure about that. + let term = if pos == local_latest_term.id { + local_latest_term + } else if let Some(term) = logs_guard.find(pos) { + term + } else if let Some(term) = self.hook.retreive_term(pos) { + term } else { - Entries::new() + panic!("impossible to retrieve a term as a leader"); }; - *self.p_current_term.lock().await = term.clone(); - let leader_commit_index = *self.p_commit_index.lock().await; + AppendTermInput { term, leader_id,