diff --git a/.config/nextest.toml b/.config/nextest.toml index fa2933367..0e29314f7 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -3,3 +3,4 @@ retries = 0 slow-timeout = { period = "10s", terminate-after = 3 } status-level = "all" final-status-level = "slow" +fail-fast = true diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 64dc895d9..8d38ad63e 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -17,12 +17,8 @@ jobs: with: submodules: recursive - - name: Configure sccache - uses: actions/github-script@v7 - with: - script: | - core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); - core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + - name: Setup sccache-cache + uses: mozilla-actions/sccache-action@v0.0.5 call_build_xline: name: Build and Upload Artifacts diff --git a/.github/workflows/build_env.yml b/.github/workflows/build_env.yml index d906d8102..6bd756538 100644 --- a/.github/workflows/build_env.yml +++ b/.github/workflows/build_env.yml @@ -1,7 +1,11 @@ name: Build CI Env Image on: - workflow_dispatch: {} + push: + paths: + - "ci/build-env.sh" + - "ci/Dockerfile" + workflow_dispatch: jobs: build_env: diff --git a/.github/workflows/build_xline.yml b/.github/workflows/build_xline.yml index dcb68ef10..754f75eac 100644 --- a/.github/workflows/build_xline.yml +++ b/.github/workflows/build_xline.yml @@ -39,12 +39,9 @@ jobs: with: submodules: recursive - - name: Configure sccache - uses: actions/github-script@v7 - with: - script: | - core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); - core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + - name: Setup sccache-cache + uses: mozilla-actions/sccache-action@v0.0.5 + - name: Prepare release binaries id: prepare_binaries run: | diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 0ea563e48..297603834 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -14,8 +14,15 @@ jobs: run: shell: bash env: - SCCACHE_GHA_ENABLED: "on" - container: ghcr.io/xline-kv/build-env:latest + SCCACHE_GHA_ENABLED: "true" + CARGO_INCREMENTAL: 0 # CI will compile all crates from beginning. So disable incremental compile may reduce compile target size. + container: + image: ghcr.io/xline-kv/build-env:latest + volumes: + - /usr/local/lib/android/:/tmp/android/ + - /usr/share/dotnet:/tmp/dotnet + - /opt/ghc:/tmp/ghc + - /usr/lib/firefox:/tmp/firefox strategy: fail-fast: true matrix: @@ -24,26 +31,35 @@ jobs: name: "Normal", args: "", rustflags: "", - test: "llvm-cov nextest --all-features --workspace --codecov --output-path codecov.info", + test: "llvm-cov nextest --all-features --workspace --codecov --output-path codecov.info && cargo test --doc", } - { name: "Madsim", args: "--package=simulation", rustflags: "--cfg madsim", - test: "nextest run --package=simulation", + test: "nextest run --package=simulation && cargo test -p simulation --doc", } name: Tests ${{ matrix.config.name }} steps: + - name: View free disk space + run: df -h / + + - name: Setup sccache-cache + uses: mozilla-actions/sccache-action@v0.0.5 + - uses: actions/checkout@v4 with: submodules: recursive - - name: Configure sccache - uses: actions/github-script@v7 - with: - script: | - core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); - core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + - name: Free Disk Space + run: | + rm -rf /tmp/android/* || true + rm -rf /tmp/dotnet/* || true + rm -rf /tmp/ghc/* || true + rm -rf /tmp/firefox/* || true + + - name: View free disk space + run: df -h / - name: Trailing spaces check run: ci/scripts/check-trailing-spaces.sh @@ -63,7 +79,7 @@ jobs: - name: Workspace hack check run: cargo hakari generate --diff && cargo hakari manage-deps --dry-run && cargo hakari verify - - run: sccache --zero-stats > /dev/null + - run: ${SCCACHE_PATH} --zero-stats > /dev/null - name: Clippy ${{ matrix.config.name }} env: @@ -71,7 +87,7 @@ jobs: run: cargo clippy ${{ matrix.config.args }} --all-targets --all-features -- -D warnings - name: Sccache stats ${{ matrix.config.name }} - run: sccache --show-stats && sccache --zero-stats > /dev/null + run: ${SCCACHE_PATH} --show-stats && ${SCCACHE_PATH} --zero-stats > /dev/null - name: Test ${{ matrix.config.name }} env: @@ -79,7 +95,7 @@ jobs: run: cargo ${{ matrix.config.test }} - name: Sccache stats ${{ matrix.config.name }} - run: sccache --show-stats + run: ${SCCACHE_PATH} --show-stats - name: Upload coverage to Codecov if: matrix.config.name == 'Normal' @@ -112,7 +128,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Check Spelling - uses: crate-ci/typos@v1.23.1 + uses: crate-ci/typos@v1.24.1 build: name: Build @@ -122,12 +138,8 @@ jobs: with: submodules: recursive - - name: Configure sccache - uses: actions/github-script@v7 - with: - script: | - core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); - core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + - name: Setup sccache-cache + uses: mozilla-actions/sccache-action@v0.0.5 - name: Build xline image run: | diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml index 4e6f4b6e6..cef9c3851 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yml @@ -16,18 +16,14 @@ jobs: with: submodules: recursive - - name: Configure sccache - uses: actions/github-script@v7 - with: - script: | - core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); - core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + - name: Setup sccache-cache + uses: mozilla-actions/sccache-action@v0.0.5 call_build_xline: name: Build and Upload Artifacts uses: ./.github/workflows/build_xline.yml with: - docker_xline_image: 'ghcr.io/xline-kv/build-env:latest' + docker_xline_image: "ghcr.io/xline-kv/build-env:latest" additional_setup_commands: | sudo apt-get install -y --force-yes expect ldd ./xline @@ -35,6 +31,6 @@ jobs: cp ../fixtures/{private,public}.pem . docker build . -t ghcr.io/xline-kv/xline:latest docker pull gcr.io/etcd-development/etcd:v3.5.5 - binaries: 'xline,benchmark' - script_name: 'validation_test.sh' + binaries: "xline,benchmark" + script_name: "validation_test.sh" uploadLogs: true diff --git a/.mergify.yml b/.mergify.yml index ffc9abe75..7fe53c716 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -25,6 +25,7 @@ pull_request_rules: - name: convert to draft conditions: + - base = master - -draft - -label = "CI:fail" - or: @@ -46,6 +47,7 @@ pull_request_rules: - name: convert to ready-to-review conditions: + - base = master - label = "CI:fail" - draft - check-success = "Tests Normal" diff --git a/Cargo.lock b/Cargo.lock index f26c6bec6..41a83869c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -111,9 +111,9 @@ checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3" [[package]] name = "assert_cmd" -version = "2.0.14" +version = "2.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed72493ac66d5804837f480ab3766c72bdfab91a65e565fc54fa9e42db0073a8" +checksum = "bc65048dd435533bb1baf2ed9956b9a278fbfdcf90301b39ee117f06c0199d37" dependencies = [ "anstyle", "bstr", @@ -159,7 +159,7 @@ dependencies = [ "async-trait", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.65", "tokio", ] @@ -206,7 +206,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.65", ] [[package]] @@ -217,15 +217,21 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.80" +version = "0.1.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" +checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.65", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.3.0" @@ -234,18 +240,19 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "axum" -version = "0.6.20" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" +checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" dependencies = [ "async-trait", "axum-core", - "bitflags 1.3.2", "bytes", "futures-util", "http", "http-body", + "http-body-util", "hyper", + "hyper-util", "itoa", "matchit", "memchr", @@ -257,28 +264,33 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 1.0.1", "tokio", "tower", "tower-layer", "tower-service", + "tracing", ] [[package]] name = "axum-core" -version = "0.3.4" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" dependencies = [ "async-trait", "bytes", "futures-util", "http", "http-body", + "http-body-util", "mime", + "pin-project-lite", "rustversion", + "sync_wrapper 0.1.2", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -332,6 +344,7 @@ dependencies = [ "clap", "clippy-utilities", "etcd-client", + "futures", "indicatif", "rand", "thiserror", @@ -360,7 +373,7 @@ version = "0.69.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" dependencies = [ - "bitflags 2.5.0", + "bitflags", "cexpr", "clang-sys", "itertools 0.12.1", @@ -371,15 +384,9 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.63", + "syn 2.0.65", ] -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.5.0" @@ -412,17 +419,11 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - [[package]] name = "bytes" -version = "1.6.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" [[package]] name = "bzip2-sys" @@ -518,10 +519,10 @@ version = "4.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.65", ] [[package]] @@ -573,16 +574,6 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "core-foundation-sys" version = "0.8.6" @@ -600,9 +591,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] @@ -678,7 +669,7 @@ dependencies = [ "madsim-tonic-build", "mockall", "once_cell", - "opentelemetry 0.21.0", + "opentelemetry", "parking_lot", "priority-queue", "prost", @@ -776,7 +767,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.10.0", - "syn 2.0.63", + "syn 2.0.65", ] [[package]] @@ -798,16 +789,17 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ "darling_core 0.20.8", "quote", - "syn 2.0.63", + "syn 2.0.65", ] [[package]] name = "dashmap" -version = "5.5.3" +version = "6.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +checksum = "804c8821570c3f8b70230c2ba75ffa5c0f9a4189b9a432b6656c536712acae28" dependencies = [ "cfg-if", + "crossbeam-utils", "hashbrown 0.14.5", "lock_api", "once_cell", @@ -851,7 +843,7 @@ dependencies = [ "darling 0.20.8", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.65", ] [[package]] @@ -861,7 +853,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" dependencies = [ "derive_builder_core", - "syn 2.0.63", + "syn 2.0.65", ] [[package]] @@ -911,15 +903,6 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" -[[package]] -name = "encoding_rs" -version = "0.8.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" -dependencies = [ - "cfg-if", -] - [[package]] name = "engine" version = "0.1.0" @@ -929,7 +912,7 @@ dependencies = [ "bytes", "clippy-utilities", "madsim-tokio", - "opentelemetry 0.21.0", + "opentelemetry", "parking_lot", "rocksdb", "serde", @@ -960,9 +943,9 @@ dependencies = [ [[package]] name = "etcd-client" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b915bb9b1e143ab7062e0067ed663e3dfeffc69ce0ceb9e93b35fecfc158d28" +checksum = "39bde3ce50a626efeb1caa9ab1083972d178bebb55ca627639c8ded507dfcbde" dependencies = [ "http", "prost", @@ -976,9 +959,9 @@ dependencies = [ [[package]] name = "event-listener" -version = "5.3.0" +version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d9944b8ca13534cdfb2800775f8dd4902ff3fc75a50101466decadfdf322a24" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" dependencies = [ "concurrent-queue", "parking", @@ -1106,7 +1089,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.65", ] [[package]] @@ -1188,15 +1171,15 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "h2" -version = "0.3.26" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" dependencies = [ + "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "futures-util", "http", "indexmap 2.2.6", "slab", @@ -1217,12 +1200,6 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - [[package]] name = "heck" version = "0.5.0" @@ -1252,9 +1229,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.12" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" dependencies = [ "bytes", "fnv", @@ -1263,12 +1240,24 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.6" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", "pin-project-lite", ] @@ -1286,13 +1275,12 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "0.14.28" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" dependencies = [ "bytes", "futures-channel", - "futures-core", "futures-util", "h2", "http", @@ -1301,23 +1289,42 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2", + "smallvec", "tokio", - "tower-service", - "tracing", "want", ] [[package]] name = "hyper-timeout" -version = "0.4.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +checksum = "3203a961e5c83b6f5498933e78b6b263e208c197b63e9c6c53cc82ffd3f63793" dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", "hyper", "pin-project-lite", + "socket2", "tokio", - "tokio-io-timeout", + "tower", + "tower-service", + "tracing", ] [[package]] @@ -1401,12 +1408,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "integer-encoding" -version = "3.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" - [[package]] name = "ipnet" version = "2.9.0" @@ -1565,8 +1566,8 @@ dependencies = [ [[package]] name = "madsim" -version = "0.2.27" -source = "git+https://github.com/Phoenix500526/madsim.git?branch=update-tonic#4df254ae43fe7921a8403873460005379ccb8247" +version = "0.2.30" +source = "git+https://github.com/LucienY01/madsim.git?branch=bz/tonic-0-12#a7d205e8f044876105cb8980c1c5b5231dd9a170" dependencies = [ "ahash", "async-channel", @@ -1596,7 +1597,7 @@ dependencies = [ [[package]] name = "madsim-macros" version = "0.2.12" -source = "git+https://github.com/Phoenix500526/madsim.git?branch=update-tonic#4df254ae43fe7921a8403873460005379ccb8247" +source = "git+https://github.com/LucienY01/madsim.git?branch=bz/tonic-0-12#a7d205e8f044876105cb8980c1c5b5231dd9a170" dependencies = [ "darling 0.14.4", "proc-macro2", @@ -1606,8 +1607,8 @@ dependencies = [ [[package]] name = "madsim-tokio" -version = "0.2.25" -source = "git+https://github.com/Phoenix500526/madsim.git?branch=update-tonic#4df254ae43fe7921a8403873460005379ccb8247" +version = "0.2.28" +source = "git+https://github.com/LucienY01/madsim.git?branch=bz/tonic-0-12#a7d205e8f044876105cb8980c1c5b5231dd9a170" dependencies = [ "madsim", "spin", @@ -1616,8 +1617,8 @@ dependencies = [ [[package]] name = "madsim-tonic" -version = "0.4.2+0.11.0" -source = "git+https://github.com/Phoenix500526/madsim.git?branch=update-tonic#4df254ae43fe7921a8403873460005379ccb8247" +version = "0.5.0+0.12.0" +source = "git+https://github.com/LucienY01/madsim.git?branch=bz/tonic-0-12#a7d205e8f044876105cb8980c1c5b5231dd9a170" dependencies = [ "async-stream", "chrono", @@ -1631,14 +1632,14 @@ dependencies = [ [[package]] name = "madsim-tonic-build" -version = "0.4.3+0.11.0" -source = "git+https://github.com/Phoenix500526/madsim.git?branch=update-tonic#4df254ae43fe7921a8403873460005379ccb8247" +version = "0.5.0+0.12.0" +source = "git+https://github.com/LucienY01/madsim.git?branch=bz/tonic-0-12#a7d205e8f044876105cb8980c1c5b5231dd9a170" dependencies = [ "prettyplease", "proc-macro2", "prost-build", "quote", - "syn 2.0.63", + "syn 2.0.65", "tonic-build", ] @@ -1728,7 +1729,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.65", ] [[package]] @@ -1758,7 +1759,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags 2.5.0", + "bitflags", "cfg-if", "cfg_aliases", "libc", @@ -1857,40 +1858,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "opentelemetry" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e32339a5dc40459130b3bd269e9892439f55b33e772d2a9d402a789baaf4e8a" -dependencies = [ - "futures-core", - "futures-sink", - "indexmap 2.2.6", - "js-sys", - "once_cell", - "pin-project-lite", - "thiserror", - "urlencoding", -] - -[[package]] -name = "opentelemetry" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900d57987be3f2aeb70d385fff9b27fb74c5723cc9a52d904d4f9c807a0667bf" -dependencies = [ - "futures-core", - "futures-sink", - "js-sys", - "once_cell", - "pin-project-lite", - "thiserror", - "urlencoding", -] - -[[package]] -name = "opentelemetry" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b69a91d4893e713e06f724597ad630f1fa76057a5e1026c0ca67054a9032a76" +checksum = "4c365a63eec4f55b7efeceb724f1336f26a9cf3427b70e59e2cd2a5b947fba96" dependencies = [ "futures-core", "futures-sink", @@ -1902,63 +1872,56 @@ dependencies = [ [[package]] name = "opentelemetry-contrib" -version = "0.14.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d4c267ff82b3e9e9f548199267c3f722d9cffe3bfe4318b05fcf56fd5357aad" +checksum = "60741e61c3c2ae6000c7cbb0d8184d4c60571c65bf0af32b418152570c8cb110" dependencies = [ "async-trait", "futures-core", "futures-util", "once_cell", - "opentelemetry 0.22.0", - "opentelemetry-semantic-conventions 0.14.0", - "opentelemetry_sdk 0.22.1", + "opentelemetry", + "opentelemetry-semantic-conventions", + "opentelemetry_sdk", "serde_json", "tokio", ] [[package]] name = "opentelemetry-http" -version = "0.11.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7690dc77bf776713848c4faa6501157469017eaf332baccd4eb1cea928743d94" +checksum = "ad31e9de44ee3538fb9d64fe3376c1362f406162434609e79aea2a41a0af78ab" dependencies = [ "async-trait", "bytes", "http", - "opentelemetry 0.22.0", + "opentelemetry", "reqwest", ] [[package]] -name = "opentelemetry-jaeger" -version = "0.22.0" +name = "opentelemetry-jaeger-propagator" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501b471b67b746d9a07d4c29f8be00f952d1a2eca356922ede0098cbaddff19f" +checksum = "fc0a68a13b92fc708d875ad659b08b35d08b8ef2403e01944b39ca21e5b08b17" dependencies = [ - "async-trait", - "futures-core", - "futures-util", - "opentelemetry 0.23.0", - "opentelemetry-semantic-conventions 0.15.0", - "opentelemetry_sdk 0.23.0", - "thrift", + "opentelemetry", ] [[package]] name = "opentelemetry-otlp" -version = "0.15.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a016b8d9495c639af2145ac22387dcb88e44118e45320d9238fbf4e7889abcb" +checksum = "6b925a602ffb916fb7421276b86756027b37ee708f9dce2dbdcc51739f07e727" dependencies = [ "async-trait", "futures-core", "http", - "opentelemetry 0.22.0", + "opentelemetry", "opentelemetry-http", "opentelemetry-proto", - "opentelemetry-semantic-conventions 0.14.0", - "opentelemetry_sdk 0.22.1", + "opentelemetry_sdk", "prost", "reqwest", "thiserror", @@ -1968,100 +1931,56 @@ dependencies = [ [[package]] name = "opentelemetry-prometheus" -version = "0.15.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bbcf6341cab7e2193e5843f0ac36c446a5b3fccb28747afaeda17996dcd02e" +checksum = "cc4191ce34aa274621861a7a9d68dbcf618d5b6c66b10081631b61fd81fbc015" dependencies = [ "once_cell", - "opentelemetry 0.22.0", - "opentelemetry_sdk 0.22.1", + "opentelemetry", + "opentelemetry_sdk", "prometheus", "protobuf", ] [[package]] name = "opentelemetry-proto" -version = "0.5.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a8fddc9b68f5b80dae9d6f510b88e02396f006ad48cac349411fbecc80caae4" +checksum = "30ee9f20bff9c984511a02f082dc8ede839e4a9bf15cc2487c8d6fea5ad850d9" dependencies = [ - "opentelemetry 0.22.0", - "opentelemetry_sdk 0.22.1", + "opentelemetry", + "opentelemetry_sdk", "prost", "tonic", ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.14.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9ab5bd6c42fb9349dcf28af2ba9a0667f697f9bdcca045d39f2cec5543e2910" - -[[package]] -name = "opentelemetry-semantic-conventions" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1869fb4bb9b35c5ba8a1e40c9b128a7b4c010d07091e864a29da19e4fe2ca4d7" +checksum = "1cefe0543875379e47eb5f1e68ff83f45cc41366a92dfd0d073d513bf68e9a05" [[package]] name = "opentelemetry_sdk" -version = "0.22.1" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e90c7113be649e31e9a0f8b5ee24ed7a16923b322c3c5ab6367469c049d6b7e" +checksum = "692eac490ec80f24a17828d49b40b60f5aeaccdfe6a503f939713afd22bc28df" dependencies = [ "async-trait", - "crossbeam-channel", "futures-channel", "futures-executor", "futures-util", "glob", "once_cell", - "opentelemetry 0.22.0", - "ordered-float 4.2.0", + "opentelemetry", "percent-encoding", "rand", + "serde_json", "thiserror", "tokio", "tokio-stream 0.1.15", ] -[[package]] -name = "opentelemetry_sdk" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae312d58eaa90a82d2e627fd86e075cf5230b3f11794e2ed74199ebbe572d4fd" -dependencies = [ - "async-trait", - "futures-channel", - "futures-executor", - "futures-util", - "lazy_static", - "once_cell", - "opentelemetry 0.23.0", - "ordered-float 4.2.0", - "percent-encoding", - "rand", - "thiserror", -] - -[[package]] -name = "ordered-float" -version = "2.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" -dependencies = [ - "num-traits", -] - -[[package]] -name = "ordered-float" -version = "4.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a76df7075c7d4d01fdcb46c912dd17fba5b60c78ea480b475f2b6ab6f666584e" -dependencies = [ - "num-traits", -] - [[package]] name = "overload" version = "0.1.1" @@ -2169,7 +2088,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.65", ] [[package]] @@ -2242,7 +2161,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ "proc-macro2", - "syn 2.0.63", + "syn 2.0.65", ] [[package]] @@ -2306,9 +2225,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.12.6" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" +checksum = "e13db3d3fde688c61e2446b4d843bc27a7e8af269a69440c0308021dc92333cc" dependencies = [ "bytes", "prost-derive", @@ -2316,13 +2235,13 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.12.6" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" +checksum = "5bb182580f71dd070f88d01ce3de9f4da5021db7115d2e1c3605a754153b77c1" dependencies = [ "bytes", - "heck 0.5.0", - "itertools 0.12.1", + "heck", + "itertools 0.13.0", "log", "multimap", "once_cell", @@ -2331,28 +2250,28 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.63", + "syn 2.0.65", "tempfile", ] [[package]] name = "prost-derive" -version = "0.12.6" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" +checksum = "18bec9b0adc4eba778b33684b7ba3e7137789434769ee3ce3930463ef904cfca" dependencies = [ "anyhow", - "itertools 0.12.1", + "itertools 0.13.0", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.65", ] [[package]] name = "prost-types" -version = "0.12.6" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" +checksum = "cee5168b05f49d4b0ca581206eb14a7b22fafd963efe729ac48eb03266e25cc2" dependencies = [ "prost", ] @@ -2411,13 +2330,19 @@ dependencies = [ "rand_core", ] +[[package]] +name = "rb-interval-map" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d14796e23a9778dec643e93352dc2404004793627102304f99cb164b47635c" + [[package]] name = "redox_syscall" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" dependencies = [ - "bitflags 2.5.0", + "bitflags", ] [[package]] @@ -2466,19 +2391,20 @@ checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "reqwest" -version = "0.11.27" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "bytes", - "encoding_rs", + "futures-channel", "futures-core", "futures-util", - "h2", "http", "http-body", + "http-body-util", "hyper", + "hyper-util", "ipnet", "js-sys", "log", @@ -2489,8 +2415,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sync_wrapper", - "system-configuration", + "sync_wrapper 1.0.1", "tokio", "tower-service", "url", @@ -2552,7 +2477,7 @@ version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.5.0", + "bitflags", "errno", "libc", "linux-raw-sys", @@ -2561,11 +2486,12 @@ dependencies = [ [[package]] name = "rustls" -version = "0.22.4" +version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +checksum = "ebbbdb961df0ad3f2652da8f3fdc4b36122f568f968f45ad3316f26c025c677b" dependencies = [ "log", + "once_cell", "ring", "rustls-pki-types", "rustls-webpki", @@ -2641,16 +2567,17 @@ checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.65", ] [[package]] name = "serde_json" -version = "1.0.117" +version = "1.0.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" +checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] @@ -2826,21 +2753,21 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.26.2" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" [[package]] name = "strum_macros" -version = "0.26.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ - "heck 0.4.1", + "heck", "proc-macro2", "quote", "rustversion", - "syn 2.0.63", + "syn 2.0.65", ] [[package]] @@ -2862,9 +2789,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.63" +version = "2.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf5be731623ca1a1fb7d8be6f261a3be6d3e2337b8a1f97be944d020c8fcb704" +checksum = "d2863d96a84c6439701d7a38f9de935ec562c8832cc55d1dde0f513b52fad106" dependencies = [ "proc-macro2", "quote", @@ -2878,25 +2805,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] -name = "system-configuration" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.5.0" +name = "sync_wrapper" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" -dependencies = [ - "core-foundation-sys", - "libc", -] +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" [[package]] name = "tempfile" @@ -2922,7 +2834,7 @@ version = "0.1.0" dependencies = [ "assert_cmd", "quote", - "syn 2.0.63", + "syn 2.0.65", "tokio", "workspace-hack", ] @@ -2944,7 +2856,7 @@ checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.65", ] [[package]] @@ -2957,28 +2869,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "threadpool" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" -dependencies = [ - "num_cpus", -] - -[[package]] -name = "thrift" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e54bc85fc7faa8bc175c4bab5b92ba8d9a3ce893d0e9f42cc455c8ab16a9e09" -dependencies = [ - "byteorder", - "integer-encoding", - "log", - "ordered-float 2.10.1", - "threadpool", -] - [[package]] name = "time" version = "0.3.36" @@ -3043,16 +2933,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "tokio-io-timeout" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" -dependencies = [ - "pin-project-lite", - "tokio", -] - [[package]] name = "tokio-macros" version = "2.3.0" @@ -3061,14 +2941,14 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.65", ] [[package]] name = "tokio-rustls" -version = "0.25.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ "rustls", "rustls-pki-types", @@ -3145,25 +3025,27 @@ dependencies = [ [[package]] name = "tonic" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76c4eb7a4e9ef9d4763600161f12f5070b92a578e1b634db88a6887844c91a13" +checksum = "38659f4a91aba8598d27821589f5db7dddd94601e7a01b1e485a50e5484c7401" dependencies = [ "async-stream", "async-trait", "axum", - "base64 0.21.7", + "base64 0.22.1", "bytes", "h2", "http", "http-body", + "http-body-util", "hyper", "hyper-timeout", + "hyper-util", "percent-encoding", "pin-project", "prost", "rustls-pemfile", - "rustls-pki-types", + "socket2", "tokio", "tokio-rustls", "tokio-stream 0.1.15", @@ -3175,22 +3057,22 @@ dependencies = [ [[package]] name = "tonic-build" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4ef6dd70a610078cb4e338a0f79d06bc759ff1b22d2120c2ff02ae264ba9c2" +checksum = "568392c5a2bd0020723e3f387891176aabafe36fd9fcd074ad309dfa0c8eb964" dependencies = [ "prettyplease", "proc-macro2", "prost-build", "quote", - "syn 2.0.63", + "syn 2.0.65", ] [[package]] name = "tonic-health" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cef6e24bc96871001a7e48e820ab240b3de2201e59b517cf52835df2f1d2350" +checksum = "e1e10e6a96ee08b6ce443487d4368442d328d0e746f3681f81127f7dc41b4955" dependencies = [ "async-stream", "prost", @@ -3263,7 +3145,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.65", ] [[package]] @@ -3289,14 +3171,14 @@ dependencies = [ [[package]] name = "tracing-opentelemetry" -version = "0.23.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9be14ba1bbe4ab79e9229f7f89fab8d120b865859f10527f31c033e599d2284" +checksum = "a9784ed4da7d921bc8df6963f8c80a0e4ce34ba6ba76668acadd3edbd985ff3b" dependencies = [ "js-sys", "once_cell", - "opentelemetry 0.22.0", - "opentelemetry_sdk 0.22.1", + "opentelemetry", + "opentelemetry_sdk", "smallvec", "tracing", "tracing-core", @@ -3403,12 +3285,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - [[package]] name = "utf8parse" version = "0.2.1" @@ -3428,14 +3304,13 @@ dependencies = [ "getset", "madsim-tokio", "madsim-tonic", - "opentelemetry 0.22.0", - "opentelemetry-jaeger", + "opentelemetry", + "opentelemetry-jaeger-propagator", "opentelemetry-otlp", - "opentelemetry_sdk 0.22.1", + "opentelemetry_sdk", "parking_lot", "pbkdf2", - "petgraph", - "rand", + "rb-interval-map", "regex", "serde", "test-macros", @@ -3450,9 +3325,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ea73390fe27785838dcbf75b91b1d84799e28f1ce71e6f372a5dc2200c80de5" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ "getrandom", ] @@ -3520,7 +3395,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.65", "wasm-bindgen-shared", ] @@ -3554,7 +3429,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.65", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3766,9 +3641,9 @@ dependencies = [ [[package]] name = "winreg" -version = "0.50.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" dependencies = [ "cfg-if", "windows-sys 0.48.0", @@ -3779,6 +3654,7 @@ name = "workspace-hack" version = "0.1.0" dependencies = [ "axum", + "axum-core", "bytes", "cc", "clap", @@ -3788,23 +3664,24 @@ dependencies = [ "futures-channel", "futures-util", "getrandom", - "itertools 0.12.1", + "itertools 0.13.0", "libc", "log", "madsim-tokio", "madsim-tonic", "memchr", - "num-traits", - "opentelemetry_sdk 0.22.1", - "petgraph", + "opentelemetry_sdk", "predicates", + "rand", "serde", "serde_json", "sha2", + "smallvec", "syn 1.0.109", - "syn 2.0.63", + "syn 2.0.65", "time", "tokio", + "tokio-stream 0.1.15", "tokio-util", "tonic", "tower", @@ -3852,6 +3729,7 @@ dependencies = [ "engine", "etcd-client", "event-listener", + "flume", "futures", "hyper", "itertools 0.13.0", @@ -3863,11 +3741,11 @@ dependencies = [ "merged_range", "mockall", "nix", - "opentelemetry 0.22.0", + "opentelemetry", "opentelemetry-contrib", "opentelemetry-otlp", "opentelemetry-prometheus", - "opentelemetry_sdk 0.22.1", + "opentelemetry_sdk", "parking_lot", "pbkdf2", "priority-queue", @@ -3879,6 +3757,7 @@ dependencies = [ "strum", "strum_macros", "test-macros", + "tokio", "tokio-stream 0.1.12", "tokio-util", "toml", @@ -3910,6 +3789,8 @@ dependencies = [ "http", "madsim-tokio", "madsim-tonic", + "madsim-tonic-build", + "prost", "rand", "test-macros", "thiserror", @@ -4005,7 +3886,7 @@ checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.65", ] [[package]] @@ -4025,7 +3906,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.65", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index e0220e105..1d04beb35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,8 @@ ignored = ["prost", "workspace-hack"] [patch.crates-io] # This branch update the tonic version for madsim. We should switch to the original etcd-client crate when new version release. -madsim = { git = "https://github.com/Phoenix500526/madsim.git", branch = "update-tonic" } -madsim-tonic = { git = "https://github.com/Phoenix500526/madsim.git", branch = "update-tonic" } -madsim-tonic-build = { git = "https://github.com/Phoenix500526/madsim.git", branch = "update-tonic" } -madsim-tokio = { git = "https://github.com/Phoenix500526/madsim.git", branch = "update-tonic" } +madsim = { git = "https://github.com/LucienY01/madsim.git", branch = "bz/tonic-0-12" } +madsim-tonic = { git = "https://github.com/LucienY01/madsim.git", branch = "bz/tonic-0-12" } +madsim-tonic-build = { git = "https://github.com/LucienY01/madsim.git", branch = "bz/tonic-0-12" } +madsim-tokio = { git = "https://github.com/LucienY01/madsim.git", branch = "bz/tonic-0-12" } + diff --git a/ci/Dockerfile b/ci/Dockerfile index 6c6d2aa1e..ab5ac71e9 100644 --- a/ci/Dockerfile +++ b/ci/Dockerfile @@ -28,9 +28,7 @@ RUN echo "=== Install rusty stuff 🦀️ ===" && \ rustup component add rustfmt llvm-tools clippy && \ rustup show -v && \ curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash && \ - cargo binstall -y --no-symlinks cargo-llvm-cov cargo-nextest cargo-hakari cargo-sort cargo-cache cargo-audit cargo-machete && \ - cargo install --locked sccache && \ - cargo cache --autoclean && \ + cargo binstall -y --no-symlinks cargo-llvm-cov cargo-nextest cargo-hakari cargo-sort cargo-cache cargo-audit cargo-machete sccache && \ rm -rf "/root/.cargo/registry/index" && \ rm -rf "/root/.cargo/registry/cache" && \ rm -rf "/root/.cargo/git/db" && \ @@ -65,3 +63,4 @@ ENV CARGO_TERM_COLOR=always # Enable sccache ENV RUSTC_WRAPPER="sccache" +ENV SCCACHE_GHA_ENABLED="true" diff --git a/crates/benchmark/Cargo.toml b/crates/benchmark/Cargo.toml index cc6a1c215..c0443fade 100644 --- a/crates/benchmark/Cargo.toml +++ b/crates/benchmark/Cargo.toml @@ -16,7 +16,8 @@ repository = "https://github.com/xline-kv/Xline/tree/master/benchmark" anyhow = "1.0.83" clap = { version = "4", features = ["derive"] } clippy-utilities = "0.2.0" -etcd-client = { version = "0.13.0", features = ["tls"] } +etcd-client = { version = "0.14.0", features = ["tls"] } +futures = "0.3.30" indicatif = "0.17.8" rand = "0.8.5" thiserror = "1.0.61" diff --git a/crates/benchmark/src/bench_client.rs b/crates/benchmark/src/bench_client.rs index e836ab244..15cdd07a8 100644 --- a/crates/benchmark/src/bench_client.rs +++ b/crates/benchmark/src/bench_client.rs @@ -4,10 +4,10 @@ use anyhow::Result; use etcd_client::{Client as EtcdClient, ConnectOptions}; use thiserror::Error; #[cfg(test)] -use xline_client::types::kv::{RangeRequest, RangeResponse}; +use xline_client::types::kv::{RangeOptions, RangeResponse}; use xline_client::{ error::XlineClientError, - types::kv::{PutRequest, PutResponse}, + types::kv::{PutOptions, PutResponse}, Client, ClientOptions, }; use xlineapi::command::Command; @@ -86,6 +86,7 @@ impl BenchClient { } /// Send `PutRequest` by `XlineClient` or `EtcdClient` + /// A `PutRequest` is made by key, value and options. /// /// # Errors /// @@ -93,17 +94,22 @@ impl BenchClient { #[inline] pub(crate) async fn put( &mut self, - request: PutRequest, + key: impl Into>, + value: impl Into>, + options: Option, ) -> Result { match self.kv_client { KVClient::Xline(ref mut xline_client) => { - let response = xline_client.kv_client().put(request).await?; + let response = xline_client.kv_client().put(key, value, options).await?; Ok(response) } KVClient::Etcd(ref mut etcd_client) => { - let opts = convert::put_req(&request); let response = etcd_client - .put(request.key(), request.value(), Some(opts)) + .put( + key, + value, + Some(convert::put_req(&options.unwrap_or_default())), + ) .await?; Ok(convert::put_res(response)) } @@ -119,15 +125,16 @@ impl BenchClient { #[cfg(test)] pub(crate) async fn get( &mut self, - request: RangeRequest, + key: impl Into>, + options: Option, ) -> Result { match self.kv_client { KVClient::Xline(ref mut xline_client) => { - let response = xline_client.kv_client().range(request).await?; + let response = xline_client.kv_client().range(key, options).await?; Ok(response) } KVClient::Etcd(ref mut etcd_client) => { - let response = etcd_client.get(request.key(), None).await?; + let response = etcd_client.get(key.into(), None).await?; Ok(convert::get_res(response)) } } @@ -136,11 +143,11 @@ impl BenchClient { /// Convert utils mod convert { - use xline_client::types::kv::PutRequest; + use xline_client::types::kv::PutOptions; use xlineapi::{KeyValue, PutResponse, ResponseHeader}; - /// transform `PutRequest` into `PutOptions` - pub(super) fn put_req(req: &PutRequest) -> etcd_client::PutOptions { + /// transform `xline_client::types::kv::PutOptions` into `etcd_client::PutOptions` + pub(super) fn put_req(req: &PutOptions) -> etcd_client::PutOptions { let mut opts = etcd_client::PutOptions::new().with_lease(req.lease()); if req.prev_kv() { opts = opts.with_prev_key(); @@ -209,10 +216,9 @@ mod convert { #[allow(clippy::unwrap_used)] #[allow(clippy::indexing_slicing)] mod test { - use xline_client::types::kv::RangeRequest; use xline_test_utils::Cluster; - use crate::bench_client::{BenchClient, ClientOptions, PutRequest}; + use crate::bench_client::{BenchClient, ClientOptions}; #[tokio::test(flavor = "multi_thread")] async fn test_new_xline_client() { @@ -225,10 +231,8 @@ mod test { .await .unwrap(); //check xline client put value exist - let request = PutRequest::new("put", "123"); - let _put_response = client.put(request).await; - let range_request = RangeRequest::new("put"); - let response = client.get(range_request).await.unwrap(); + let _put_response = client.put("put", "123", None).await; + let response = client.get("put", None).await.unwrap(); assert_eq!(response.kvs[0].value, b"123"); } @@ -242,10 +246,8 @@ mod test { .await .unwrap(); - let request = PutRequest::new("put", "123"); - let _put_response = client.put(request).await; - let range_request = RangeRequest::new("put"); - let response = client.get(range_request).await.unwrap(); + let _put_response = client.put("put", "123", None).await; + let response = client.get("put", None).await.unwrap(); assert_eq!(response.kvs[0].value, b"123"); } } diff --git a/crates/benchmark/src/runner.rs b/crates/benchmark/src/runner.rs index 26ae232d3..fb167716f 100644 --- a/crates/benchmark/src/runner.rs +++ b/crates/benchmark/src/runner.rs @@ -9,6 +9,7 @@ use std::{ use anyhow::Result; use clippy_utilities::{NumericCast, OverflowArithmetic}; +use futures::future::join_all; use indicatif::ProgressBar; use rand::RngCore; use tokio::{ @@ -20,7 +21,7 @@ use tokio::{ }; use tracing::debug; use utils::config::ClientConfig; -use xline_client::{types::kv::PutRequest, ClientOptions}; +use xline_client::ClientOptions; use crate::{args::Commands, bench_client::BenchClient, Benchmark}; @@ -158,7 +159,6 @@ impl CommandRunner { /// Create clients async fn create_clients(&self) -> Result> { - let mut clients = Vec::with_capacity(self.args.clients); let client_options = ClientOptions::default().with_client_config(ClientConfig::new( Duration::from_secs(10), Duration::from_secs(5), @@ -180,11 +180,15 @@ impl CommandRunner { } }) .collect::>(); - for _ in 0..self.args.clients { - let client = - BenchClient::new(addrs.clone(), self.args.use_curp, client_options.clone()).await?; - clients.push(client); - } + let clients_futs = std::iter::repeat_with(|| { + BenchClient::new(addrs.clone(), self.args.use_curp, client_options.clone()) + }) + .take(self.args.clients); + let clients = join_all(clients_futs) + .await + .into_iter() + .collect::>()?; + Ok(clients) } @@ -229,9 +233,7 @@ impl CommandRunner { ); } let start = Instant::now(); - let result = client - .put(PutRequest::new(key.as_slice(), val_clone.as_slice())) - .await; + let result = client.put(key.as_slice(), val_clone.as_slice(), None).await; let cmd_result = CmdResult { elapsed: start.elapsed(), error: result.err().map(|e| format!("{e:?}")), diff --git a/crates/curp-external-api/Cargo.toml b/crates/curp-external-api/Cargo.toml index 99d0b212d..fe288e0d8 100644 --- a/crates/curp-external-api/Cargo.toml +++ b/crates/curp-external-api/Cargo.toml @@ -11,10 +11,10 @@ categories = ["API"] keywords = ["API", "Curp"] [dependencies] -async-trait = "0.1.80" +async-trait = "0.1.81" engine = { path = "../engine" } mockall = "0.12.1" -prost = "0.12.3" +prost = "0.13" serde = { version = "1.0.204", features = ["derive", "rc"] } thiserror = "1.0.61" workspace-hack = { version = "0.1", path = "../../workspace-hack" } diff --git a/crates/curp-external-api/src/cmd.rs b/crates/curp-external-api/src/cmd.rs index bbec7a288..5b282b8bd 100644 --- a/crates/curp-external-api/src/cmd.rs +++ b/crates/curp-external-api/src/cmd.rs @@ -28,9 +28,10 @@ impl pri::Serializable for T where T: pri::ThreadSafe + Clone + Serialize + D #[async_trait] pub trait Command: pri::Serializable + ConflictCheck + PbCodec { /// Error type - type Error: pri::Serializable + PbCodec + std::error::Error; + type Error: pri::Serializable + PbCodec + std::error::Error + Clone; /// K (key) is used to tell confliction + /// /// The key can be a single key or a key range type K: pri::Serializable + Eq + Hash + ConflictCheck; @@ -49,45 +50,17 @@ pub trait Command: pri::Serializable + ConflictCheck + PbCodec { /// Returns `true` if the command is read-only fn is_read_only(&self) -> bool; - /// Prepare the command - /// - /// # Errors - /// Return `Self::Error` when `CommandExecutor::prepare` goes wrong - #[inline] - fn prepare(&self, e: &E) -> Result - where - E: CommandExecutor + Send + Sync, - { - >::prepare(e, self) - } - /// Execute the command according to the executor /// /// # Errors - /// Return `Self::Error` when `CommandExecutor::execute` goes wrong - #[inline] - async fn execute(&self, e: &E) -> Result - where - E: CommandExecutor + Send + Sync, - { - >::execute(e, self).await - } - - /// Execute the command after_sync callback /// - /// # Errors - /// Return `Self::Error` when `CommandExecutor::after_sync` goes wrong + /// Return `Self::Error` when `CommandExecutor::execute` goes wrong #[inline] - async fn after_sync( - &self, - e: &E, - index: LogIndex, - prepare_res: Self::PR, - ) -> Result + fn execute(&self, e: &E) -> Result where E: CommandExecutor + Send + Sync, { - >::after_sync(e, self, index, prepare_res).await + >::execute(e, self) } } @@ -116,61 +89,75 @@ impl ConflictCheck for u32 { } /// Command executor which actually executes the command. +/// /// It is usually defined by the protocol user. #[async_trait] pub trait CommandExecutor: pri::ThreadSafe where C: Command, { - /// Prepare the command + /// Execute the command /// /// # Errors - /// This function may return an error if there is a problem preparing the command. - fn prepare(&self, cmd: &C) -> Result; + /// + /// This function may return an error if there is a problem executing the + /// command. + fn execute(&self, cmd: &C) -> Result; - /// Execute the command + /// Execute the read-only command /// /// # Errors - /// This function may return an error if there is a problem executing the command. - async fn execute(&self, cmd: &C) -> Result; + /// + /// This function may return an error if there is a problem executing the + /// command. + fn execute_ro(&self, cmd: &C) -> Result<(C::ER, C::ASR), C::Error>; - /// Execute the after_sync callback + /// Batch execute the after_sync callback /// - /// # Errors - /// This function may return an error if there is a problem executing the after_sync callback. - async fn after_sync( + /// This `highest_index` means the last log index of the `cmds` + fn after_sync( &self, - cmd: &C, - index: LogIndex, - prepare_res: C::PR, - ) -> Result; + cmds: Vec>, + // might be `None` if it's a speculative execution + highest_index: Option, + ) -> Vec, C::Error>>; - /// Set the index of the last log entry that has been successfully applied to the command executor + /// Set the index of the last log entry that has been successfully applied + /// to the command executor /// /// # Errors + /// /// Returns an error if setting the last applied log entry fails. fn set_last_applied(&self, index: LogIndex) -> Result<(), C::Error>; - /// Get the index of the last log entry that has been successfully applied to the command executor + /// Get the index of the last log entry that has been successfully applied + /// to the command executor /// /// # Errors + /// /// Returns an error if retrieval of the last applied log entry fails. fn last_applied(&self) -> Result; /// Take a snapshot /// /// # Errors - /// This function may return an error if there is a problem taking a snapshot. + /// + /// This function may return an error if there is a problem taking a + /// snapshot. async fn snapshot(&self) -> Result; - /// Reset the command executor using the snapshot or to the initial state if None + /// Reset the command executor using the snapshot or to the initial state if + /// None /// /// # Errors - /// This function may return an error if there is a problem resetting the command executor. + /// + /// This function may return an error if there is a problem resetting the + /// command executor. async fn reset(&self, snapshot: Option<(Snapshot, LogIndex)>) -> Result<(), C::Error>; - /// Trigger the barrier of the given trigger id (based on propose id) and log index. - fn trigger(&self, id: InflightId, index: LogIndex); + /// Trigger the barrier of the given trigger id (based on propose id) and + /// log index. + fn trigger(&self, id: InflightId); } /// Codec for encoding and decoding data into/from the Protobuf format @@ -203,3 +190,59 @@ impl From for PbSerializeError { PbSerializeError::RpcDecode(err) } } + +#[allow(clippy::module_name_repetitions)] +/// After sync command type +#[derive(Debug)] +pub struct AfterSyncCmd<'a, C> { + /// The command + cmd: &'a C, + /// Whether the command needs to be executed in after sync stage + to_execute: bool, +} + +impl<'a, C> AfterSyncCmd<'a, C> { + /// Creates a new `AfterSyncCmd` + #[inline] + pub fn new(cmd: &'a C, to_execute: bool) -> Self { + Self { cmd, to_execute } + } + + /// Gets the command + #[inline] + #[must_use] + pub fn cmd(&self) -> &'a C { + self.cmd + } + + /// Convert self into parts + #[inline] + #[must_use] + pub fn into_parts(&'a self) -> (&'a C, bool) { + (self.cmd, self.to_execute) + } +} + +/// Ok type of the after sync result +#[derive(Debug)] +pub struct AfterSyncOk { + /// After Sync Result + asr: C::ASR, + /// Optional Execution Result + er_opt: Option, +} + +impl AfterSyncOk { + /// Creates a new [`AfterSyncOk`]. + #[inline] + pub fn new(asr: C::ASR, er_opt: Option) -> Self { + Self { asr, er_opt } + } + + /// Decomposes `AfterSyncOk` into its constituent parts. + #[inline] + pub fn into_parts(self) -> (C::ASR, Option) { + let Self { asr, er_opt } = self; + (asr, er_opt) + } +} diff --git a/crates/curp-external-api/src/conflict.rs b/crates/curp-external-api/src/conflict.rs index c9d49d73a..7d1ed9a4b 100644 --- a/crates/curp-external-api/src/conflict.rs +++ b/crates/curp-external-api/src/conflict.rs @@ -1,12 +1,23 @@ #![allow(clippy::module_name_repetitions)] +use std::hash::Hash; + +/// Entry with an identifier +pub trait EntryId { + /// The type of the id + type Id: Copy + Hash; + + /// Gets the id of the entry + fn id(&self) -> Self::Id; +} + /// Common operations for conflict pools pub trait ConflictPoolOp { /// Entry of the pool - type Entry; + type Entry: EntryId; /// Removes a command from the pool - fn remove(&mut self, entry: Self::Entry); + fn remove(&mut self, entry: &Self::Entry); /// Returns all commands in the pool fn all(&self) -> Vec; diff --git a/crates/curp-test-utils/Cargo.toml b/crates/curp-test-utils/Cargo.toml index 059239951..622c25696 100644 --- a/crates/curp-test-utils/Cargo.toml +++ b/crates/curp-test-utils/Cargo.toml @@ -11,18 +11,18 @@ license = "Apache-2.0" readme = "README.md" [dependencies] -async-trait = "0.1.80" +async-trait = "0.1.81" bincode = "1.3.3" curp-external-api = { path = "../curp-external-api" } engine = { path = "../engine" } itertools = "0.13" -prost = "0.12.3" +prost = "0.13" serde = { version = "1.0.204", features = ["derive", "rc"] } thiserror = "1.0.61" tokio = { version = "0.2.25", package = "madsim-tokio", features = [ "rt-multi-thread", ] } -tracing = { version = "0.1.34", features = ["std", "log", "attributes"] } +tracing = { version = "0.1.37", features = ["std", "log", "attributes"] } tracing-subscriber = { version = "0.3.16", features = ["env-filter", "time"] } utils = { path = "../utils", version = "0.1.0", features = ["parking_lot"] } workspace-hack = { version = "0.1", path = "../../workspace-hack" } diff --git a/crates/curp-test-utils/src/test_cmd.rs b/crates/curp-test-utils/src/test_cmd.rs index aa6b5bec1..c3fa23895 100644 --- a/crates/curp-test-utils/src/test_cmd.rs +++ b/crates/curp-test-utils/src/test_cmd.rs @@ -9,7 +9,7 @@ use std::{ use async_trait::async_trait; use curp_external_api::{ - cmd::{Command, CommandExecutor, ConflictCheck, PbCodec}, + cmd::{AfterSyncCmd, AfterSyncOk, Command, CommandExecutor, ConflictCheck, PbCodec}, InflightId, LogIndex, }; use engine::{ @@ -18,7 +18,7 @@ use engine::{ use itertools::Itertools; use serde::{Deserialize, Serialize}; use thiserror::Error; -use tokio::{sync::mpsc, time::sleep}; +use tokio::sync::mpsc; use tracing::debug; use utils::config::EngineConfig; @@ -239,32 +239,11 @@ pub struct TestCE { #[async_trait] impl CommandExecutor for TestCE { - fn prepare( - &self, - cmd: &TestCommand, - ) -> Result<::PR, ::Error> { - let rev = if let TestCommandType::Put(_) = cmd.cmd_type { - let rev = self.revision.fetch_add(1, Ordering::Relaxed); - let wr_ops = vec![WriteOperation::new_put( - META_TABLE, - LAST_REVISION_KEY.into(), - rev.to_le_bytes().to_vec(), - )]; - self.store - .write_multi(wr_ops, true) - .map_err(|e| ExecuteError(e.to_string()))?; - rev - } else { - -1 - }; - Ok(rev) - } - - async fn execute( + fn execute( &self, cmd: &TestCommand, ) -> Result<::ER, ::Error> { - sleep(cmd.exe_dur).await; + std::thread::sleep(cmd.exe_dur); if cmd.exe_should_fail { return Err(ExecuteError("fail".to_owned())); } @@ -305,53 +284,101 @@ impl CommandExecutor for TestCE { Ok(result) } - async fn after_sync( + fn execute_ro( &self, cmd: &TestCommand, - index: LogIndex, - revision: ::PR, - ) -> Result<::ASR, ::Error> { - sleep(cmd.as_dur).await; - if cmd.as_should_fail { - return Err(ExecuteError("fail".to_owned())); + ) -> Result< + (::ER, ::ASR), + ::Error, + > { + self.execute(cmd).map(|er| (er, LogIndexResult(0))) + } + + fn after_sync( + &self, + cmds: Vec>, + highest_index: Option, + ) -> Vec, ::Error>> { + let as_duration = cmds + .iter() + .fold(Duration::default(), |acc, c| acc + c.cmd().as_dur); + std::thread::sleep(as_duration); + let total = cmds.len(); + let mut wr_ops = Vec::new(); + + if let Some(index) = highest_index { + for (i, cmd) in cmds.iter().enumerate() { + // Calculate the log index of the current cmd + let index = index - (total - i - 1) as u64; + self.after_sync_sender + .send((cmd.cmd().clone(), index)) + .expect("failed to send after sync msg"); + } + wr_ops.push(WriteOperation::new_put( + META_TABLE, + APPLIED_INDEX_KEY.into(), + index.to_le_bytes().to_vec(), + )); } - self.after_sync_sender - .send((cmd.clone(), index)) - .expect("failed to send after sync msg"); - let mut wr_ops = vec![WriteOperation::new_put( - META_TABLE, - APPLIED_INDEX_KEY.into(), - index.to_le_bytes().to_vec(), - )]; - if let TestCommandType::Put(v) = cmd.cmd_type { - debug!("cmd {:?}-{:?} revision is {}", cmd.cmd_type, cmd, revision); - let value = v.to_le_bytes().to_vec(); - let keys = cmd - .keys - .iter() - .map(|k| k.to_le_bytes().to_vec()) - .collect_vec(); - wr_ops.extend( - keys.clone() - .into_iter() - .map(|key| WriteOperation::new_put(TEST_TABLE, key, value.clone())) - .chain(keys.into_iter().map(|key| { - WriteOperation::new_put( - REVISION_TABLE, - key, - revision.to_le_bytes().to_vec(), - ) - })), + let mut asrs = Vec::new(); + for (i, (cmd, to_execute)) in cmds.iter().map(AfterSyncCmd::into_parts).enumerate() { + let index = highest_index + .map(|index| index - (total - i - 1) as u64) + .unwrap_or(0); + if cmd.as_should_fail { + asrs.push(Err(ExecuteError("fail".to_owned()))); + continue; + } + if let TestCommandType::Put(v) = cmd.cmd_type { + let revision = match self.next_revision(cmd) { + Ok(rev) => rev, + Err(e) => { + asrs.push(Err(e)); + continue; + } + }; + + debug!("cmd {:?}-{:?} revision is {}", cmd.cmd_type, cmd, revision); + let value = v.to_le_bytes().to_vec(); + let keys = cmd + .keys + .iter() + .map(|k| k.to_le_bytes().to_vec()) + .collect_vec(); + wr_ops.extend( + keys.clone() + .into_iter() + .map(|key| WriteOperation::new_put(TEST_TABLE, key, value.clone())) + .chain(keys.into_iter().map(|key| { + WriteOperation::new_put( + REVISION_TABLE, + key, + revision.to_le_bytes().to_vec(), + ) + })), + ); + } + match to_execute.then(|| self.execute(cmd)).transpose() { + Ok(er) => { + asrs.push(Ok(AfterSyncOk::new(LogIndexResult(index), er))); + } + Err(e) => asrs.push(Err(e)), + } + debug!( + "{} after sync cmd({:?} - {:?}), index: {index}", + self.server_name, cmd.cmd_type, cmd ); - self.store - .write_multi(wr_ops, true) - .map_err(|e| ExecuteError(e.to_string()))?; } - debug!( - "{} after sync cmd({:?} - {:?}), index: {index}", - self.server_name, cmd.cmd_type, cmd - ); - Ok(index.into()) + + if let Err(e) = self + .store + .write_multi(wr_ops, true) + .map_err(|e| ExecuteError(e.to_string())) + { + return std::iter::repeat(e).map(Err).take(cmds.len()).collect(); + } + + asrs } fn set_last_applied(&self, index: LogIndex) -> Result<(), ::Error> { @@ -414,7 +441,7 @@ impl CommandExecutor for TestCE { Ok(()) } - fn trigger(&self, _id: InflightId, _index: LogIndex) {} + fn trigger(&self, _id: InflightId) {} } impl TestCE { @@ -444,4 +471,22 @@ impl TestCE { after_sync_sender, } } + + fn next_revision(&self, cmd: &TestCommand) -> Result::Error> { + let rev = if let TestCommandType::Put(_) = cmd.cmd_type { + let rev = self.revision.fetch_add(1, Ordering::Relaxed); + let wr_ops = vec![WriteOperation::new_put( + META_TABLE, + LAST_REVISION_KEY.into(), + rev.to_le_bytes().to_vec(), + )]; + self.store + .write_multi(wr_ops, true) + .map_err(|e| ExecuteError(e.to_string()))?; + rev + } else { + -1 + }; + Ok(rev) + } } diff --git a/crates/curp/Cargo.toml b/crates/curp/Cargo.toml index dbaf9df67..163c9ee19 100644 --- a/crates/curp/Cargo.toml +++ b/crates/curp/Cargo.toml @@ -13,26 +13,26 @@ version = "0.1.0" [dependencies] async-stream = "0.3.4" -async-trait = "0.1.80" +async-trait = "0.1.81" bincode = "1.3.3" -bytes = "1.4.0" +bytes = "1.7.1" clippy-utilities = "0.2.0" curp-external-api = { path = "../curp-external-api" } curp-test-utils = { path = "../curp-test-utils" } -dashmap = "5.5.0" +dashmap = "6.0.1" derive_builder = "0.20.0" engine = { path = "../engine" } -event-listener = "5.3.0" +event-listener = "5.3.1" flume = "0.11.0" fs2 = "0.4.3" futures = "0.3.21" indexmap = "2.2.6" itertools = "0.13" madsim = { version = "0.2.27", features = ["rpc", "macros"] } -opentelemetry = { version = "0.21.0", features = ["metrics"] } +opentelemetry = { version = "0.24.0", features = ["metrics"] } parking_lot = "0.12.3" priority-queue = "2.0.2" -prost = "0.12.3" +prost = "0.13" rand = "0.8.5" serde = { version = "1.0.204", features = ["derive", "rc"] } sha2 = "0.10.8" @@ -44,9 +44,9 @@ tokio-stream = { git = "https://github.com/madsim-rs/tokio.git", rev = "ab251ad" "net", ] } tokio-util = "0.7.11" -tonic = { version = "0.4.2", package = "madsim-tonic", features = ["tls"] } +tonic = { version = "0.5.0", package = "madsim-tonic", features = ["tls"] } tower = { version = "0.4.13", features = ["filter"] } -tracing = { version = "0.1.34", features = ["std", "log", "attributes"] } +tracing = { version = "0.1.37", features = ["std", "log", "attributes"] } utils = { path = "../utils", version = "0.1.0", features = ["parking_lot"] } workspace-hack = { version = "0.1", path = "../../workspace-hack" } @@ -62,8 +62,8 @@ tracing-subscriber = { version = "0.3.16", features = ["env-filter", "time"] } tracing-test = "0.2.4" [build-dependencies] -prost-build = "0.12.6" -tonic-build = { version = "0.4.3", package = "madsim-tonic-build" } +prost-build = "0.13.0" +tonic-build = { version = "0.5.0", package = "madsim-tonic-build" } [features] client-metrics = [] diff --git a/crates/curp/proto/common b/crates/curp/proto/common index 7e2813c48..19cfc8d48 160000 --- a/crates/curp/proto/common +++ b/crates/curp/proto/common @@ -1 +1 @@ -Subproject commit 7e2813c48513235e87e64b9f23fe933c9a13cec4 +Subproject commit 19cfc8d48da30c190e240a477802b2b7f2a14633 diff --git a/crates/curp/src/client/mod.rs b/crates/curp/src/client/mod.rs index 17ded1a7d..475c5c500 100644 --- a/crates/curp/src/client/mod.rs +++ b/crates/curp/src/client/mod.rs @@ -21,11 +21,14 @@ mod state; #[cfg(test)] mod tests; -use std::{collections::HashMap, fmt::Debug, sync::Arc}; +#[cfg(madsim)] +use std::sync::atomic::AtomicU64; +use std::{collections::HashMap, fmt::Debug, ops::Deref, sync::Arc, time::Duration}; use async_trait::async_trait; use curp_external_api::cmd::Command; use futures::{stream::FuturesUnordered, StreamExt}; +use parking_lot::RwLock; use tokio::task::JoinHandle; #[cfg(not(madsim))] use tonic::transport::ClientTlsConfig; @@ -45,6 +48,7 @@ use crate::{ protocol_client::ProtocolClient, ConfChange, FetchClusterRequest, FetchClusterResponse, Member, ProposeId, Protocol, ReadState, }, + tracker::Tracker, }; /// The response of propose command, deserialized from [`crate::rpc::ProposeResponse`] or @@ -97,6 +101,7 @@ pub trait ClientApi { /// Send fetch cluster requests to all servers (That's because initially, we didn't /// know who the leader is.) + /// /// Note: The fetched cluster may still be outdated if `linearizable` is false async fn fetch_cluster(&self, linearizable: bool) -> Result; @@ -105,13 +110,16 @@ pub trait ClientApi { async fn fetch_leader_id(&self, linearizable: bool) -> Result { if linearizable { let resp = self.fetch_cluster(true).await?; - return Ok(resp.leader_id.unwrap_or_else(|| { - unreachable!("linearizable fetch cluster should return a leader id") - })); + return Ok(resp + .leader_id + .unwrap_or_else(|| { + unreachable!("linearizable fetch cluster should return a leader id") + }) + .into()); } let resp = self.fetch_cluster(false).await?; if let Some(id) = resp.leader_id { - return Ok(id); + return Ok(id.into()); } debug!("no leader id in FetchClusterResponse, try to send linearizable request"); // fallback to linearizable fetch @@ -119,11 +127,43 @@ pub trait ClientApi { } } +/// Propose id guard, used to ensure the sequence of propose id is recorded. +struct ProposeIdGuard<'a> { + /// The propose id + propose_id: ProposeId, + /// The tracker + tracker: &'a RwLock, +} + +impl Deref for ProposeIdGuard<'_> { + type Target = ProposeId; + + fn deref(&self) -> &Self::Target { + &self.propose_id + } +} + +impl<'a> ProposeIdGuard<'a> { + /// Create a new propose id guard + fn new(tracker: &'a RwLock, propose_id: ProposeId) -> Self { + Self { + propose_id, + tracker, + } + } +} + +impl Drop for ProposeIdGuard<'_> { + fn drop(&mut self) { + let _ig = self.tracker.write().record(self.propose_id.1); + } +} + /// This trait override some unrepeatable methods in ClientApi, and a client with this trait will be able to retry. #[async_trait] trait RepeatableClientApi: ClientApi { /// Generate a unique propose id during the retry process. - fn gen_propose_id(&self) -> Result; + fn gen_propose_id(&self) -> Result, Self::Error>; /// Send propose to the whole cluster, `use_fast_path` set to `false` to fallback into ordered /// requests (event the requests are commutative). @@ -284,8 +324,8 @@ impl ClientBuilder { match r { Ok(r) => { self.cluster_version = Some(r.cluster_version); - if let Some(id) = r.leader_id { - self.leader_state = Some((id, r.term)); + if let Some(ref id) = r.leader_id { + self.leader_state = Some((id.into(), r.term)); } self.all_members = if self.is_raw_curp { Some(r.into_peer_urls()) @@ -352,6 +392,29 @@ impl ClientBuilder { }) } + /// Wait for client id + async fn wait_for_client_id(&self, state: Arc) -> Result<(), tonic::Status> { + /// Max retry count for waiting for a client ID + /// + /// TODO: This retry count is set relatively high to avoid test cluster startup timeouts. + /// We should consider setting this to a more reasonable value. + const RETRY_COUNT: usize = 30; + /// The interval for each retry + const RETRY_INTERVAL: Duration = Duration::from_secs(1); + + for _ in 0..RETRY_COUNT { + if state.client_id() != 0 { + return Ok(()); + } + debug!("waiting for client_id"); + tokio::time::sleep(RETRY_INTERVAL).await; + } + + Err(tonic::Status::deadline_exceeded( + "timeout waiting for client id", + )) + } + /// Build the client /// /// # Errors @@ -360,17 +423,55 @@ impl ClientBuilder { #[inline] pub async fn build( &self, + ) -> Result + Send + Sync + 'static, tonic::Status> + { + let state = Arc::new( + self.init_state_builder() + .build() + .await + .map_err(|e| tonic::Status::internal(e.to_string()))?, + ); + let client = Retry::new( + Unary::new(Arc::clone(&state), self.init_unary_config()), + self.init_retry_config(), + Some(self.spawn_bg_tasks(Arc::clone(&state))), + ); + self.wait_for_client_id(state).await?; + Ok(client) + } + + #[cfg(madsim)] + /// Build the client, also returns the current client id + /// + /// # Errors + /// + /// Return `tonic::transport::Error` for connection failure. + #[inline] + pub async fn build_with_client_id( + &self, ) -> Result< - impl ClientApi + Send + Sync + 'static, - tonic::transport::Error, + ( + impl ClientApi + Send + Sync + 'static, + Arc, + ), + tonic::Status, > { - let state = Arc::new(self.init_state_builder().build().await?); + let state = Arc::new( + self.init_state_builder() + .build() + .await + .map_err(|e| tonic::Status::internal(e.to_string()))?, + ); + let client = Retry::new( Unary::new(Arc::clone(&state), self.init_unary_config()), self.init_retry_config(), - Some(self.spawn_bg_tasks(state)), + Some(self.spawn_bg_tasks(Arc::clone(&state))), ); - Ok(client) + let client_id = state.clone_client_id(); + self.wait_for_client_id(state).await?; + + Ok((client, client_id)) } } @@ -383,18 +484,20 @@ impl ClientBuilderWithBypass

{ #[inline] pub async fn build( self, - ) -> Result, tonic::transport::Error> { + ) -> Result, tonic::Status> { let state = self .inner .init_state_builder() .build_bypassed::

(self.local_server_id, self.local_server) - .await?; + .await + .map_err(|e| tonic::Status::internal(e.to_string()))?; let state = Arc::new(state); let client = Retry::new( Unary::new(Arc::clone(&state), self.inner.init_unary_config()), self.inner.init_retry_config(), - Some(self.inner.spawn_bg_tasks(state)), + Some(self.inner.spawn_bg_tasks(Arc::clone(&state))), ); + self.inner.wait_for_client_id(state).await?; Ok(client) } } diff --git a/crates/curp/src/client/retry.rs b/crates/curp/src/client/retry.rs index e5cb550b5..06e670a89 100644 --- a/crates/curp/src/client/retry.rs +++ b/crates/curp/src/client/retry.rs @@ -3,7 +3,7 @@ use std::{ops::SubAssign, time::Duration}; use async_trait::async_trait; use futures::Future; use tokio::task::JoinHandle; -use tracing::warn; +use tracing::{info, warn}; use super::{ClientApi, LeaderStateUpdate, ProposeResponse, RepeatableClientApi}; use crate::{ @@ -110,6 +110,7 @@ pub(super) struct Retry { impl Drop for Retry { fn drop(&mut self) { if let Some(handle) = self.bg_handle.as_ref() { + info!("stopping background task"); handle.abort(); } } @@ -174,8 +175,21 @@ where } // update the leader state if got Redirect - CurpError::Redirect(Redirect { leader_id, term }) => { - let _ig = self.inner.update_leader(leader_id, term).await; + CurpError::Redirect(Redirect { + ref leader_id, + term, + }) => { + let _ig = self + .inner + .update_leader(leader_id.as_ref().map(Into::into), term) + .await; + } + + // update the cluster state if got Zombie + CurpError::Zombie(()) => { + if let Err(e) = self.inner.fetch_cluster(true).await { + warn!("fetch cluster failed, error {e:?}"); + } } } @@ -216,9 +230,9 @@ where token: Option<&String>, use_fast_path: bool, ) -> Result, tonic::Status> { - let propose_id = self.inner.gen_propose_id()?; - self.retry::<_, _>(|client| { - RepeatableClientApi::propose(client, propose_id, cmd, token, use_fast_path) + self.retry::<_, _>(|client| async move { + let propose_id = self.inner.gen_propose_id()?; + RepeatableClientApi::propose(client, *propose_id, cmd, token, use_fast_path).await }) .await } @@ -228,19 +242,23 @@ where &self, changes: Vec, ) -> Result, tonic::Status> { - let propose_id = self.inner.gen_propose_id()?; self.retry::<_, _>(|client| { let changes_c = changes.clone(); - RepeatableClientApi::propose_conf_change(client, propose_id, changes_c) + async move { + let propose_id = self.inner.gen_propose_id()?; + RepeatableClientApi::propose_conf_change(client, *propose_id, changes_c).await + } }) .await } /// Send propose to shutdown cluster async fn propose_shutdown(&self) -> Result<(), tonic::Status> { - let propose_id = self.inner.gen_propose_id()?; - self.retry::<_, _>(|client| RepeatableClientApi::propose_shutdown(client, propose_id)) - .await + self.retry::<_, _>(|client| async move { + let propose_id = self.inner.gen_propose_id()?; + RepeatableClientApi::propose_shutdown(client, *propose_id).await + }) + .await } /// Send propose to publish a node id and name @@ -250,17 +268,20 @@ where node_name: String, node_client_urls: Vec, ) -> Result<(), Self::Error> { - let propose_id = self.inner.gen_propose_id()?; self.retry::<_, _>(|client| { let name_c = node_name.clone(); let node_client_urls_c = node_client_urls.clone(); - RepeatableClientApi::propose_publish( - client, - propose_id, - node_id, - name_c, - node_client_urls_c, - ) + async move { + let propose_id = self.inner.gen_propose_id()?; + RepeatableClientApi::propose_publish( + client, + *propose_id, + node_id, + name_c, + node_client_urls_c, + ) + .await + } }) .await } @@ -279,6 +300,7 @@ where /// Send fetch cluster requests to all servers (That's because initially, we didn't /// know who the leader is.) + /// /// Note: The fetched cluster may still be outdated if `linearizable` is false async fn fetch_cluster( &self, diff --git a/crates/curp/src/client/state.rs b/crates/curp/src/client/state.rs index 390169581..8476e46b8 100644 --- a/crates/curp/src/client/state.rs +++ b/crates/curp/src/client/state.rs @@ -2,10 +2,12 @@ use std::{ cmp::Ordering, collections::{hash_map::Entry, HashMap, HashSet}, sync::{atomic::AtomicU64, Arc}, + time::Duration, }; use event_listener::Event; use futures::{stream::FuturesUnordered, Future}; +use rand::seq::IteratorRandom; use tokio::sync::RwLock; #[cfg(not(madsim))] use tonic::transport::ClientTlsConfig; @@ -18,7 +20,7 @@ use crate::{ rpc::{ self, connect::{BypassedConnect, ConnectApi}, - CurpError, FetchClusterResponse, Protocol, + CurpError, FetchClusterRequest, FetchClusterResponse, Protocol, }, }; @@ -127,6 +129,28 @@ impl State { } } + /// Choose a random server to try to refresh the state + /// Use when the current leader is missing. + pub(crate) async fn try_refresh_state(&self) -> Result<(), CurpError> { + /// The timeout for refreshing the state + const REFRESH_TIMEOUT: Duration = Duration::from_secs(1); + + let rand_conn = { + let state = self.mutable.read().await; + state + .connects + .values() + .choose(&mut rand::thread_rng()) + .map(Arc::clone) + .ok_or_else(CurpError::wrong_cluster_version)? + }; + let resp = rand_conn + .fetch_cluster(FetchClusterRequest::default(), REFRESH_TIMEOUT) + .await?; + self.check_and_update(&resp.into_inner()).await?; + Ok(()) + } + /// Get the local server connection pub(super) async fn local_connect(&self) -> Option> { let id = self.immutable.local_server?; @@ -148,6 +172,11 @@ impl State { self.mutable.read().await.leader } + /// Get term of the cluster + pub(super) async fn term(&self) -> u64 { + self.mutable.read().await.term + } + /// Take an async function and map to the dedicated server, return `Err(CurpError:WrongClusterVersion(()))` /// if the server can not found in local state pub(super) async fn map_server>>( @@ -170,6 +199,11 @@ impl State { f(conn).await } + /// Returns the number of members in the cluster + pub(super) async fn connects_len(&self) -> usize { + self.mutable.read().await.connects.len() + } + /// Take an async function and map to all server, returning `FuturesUnordered` pub(super) async fn for_each_server>( &self, @@ -185,6 +219,22 @@ impl State { .collect() } + /// Take an async function and map to all server, returning `FuturesUnordered` + pub(super) async fn for_each_follower>( + &self, + leader_id: u64, + f: impl FnMut(Arc) -> F, + ) -> FuturesUnordered { + let mutable_r = self.mutable.read().await; + mutable_r + .connects + .iter() + .filter_map(|(id, conn)| (*id != leader_id).then_some(conn)) + .map(Arc::clone) + .map(f) + .collect() + } + /// Inner check and update leader fn check_and_update_leader_inner( &self, @@ -242,7 +292,11 @@ impl State { res: &FetchClusterResponse, ) -> Result<(), tonic::transport::Error> { let mut state = self.mutable.write().await; - if !self.check_and_update_leader_inner(&mut state, res.leader_id, res.term) { + if !self.check_and_update_leader_inner( + &mut state, + res.leader_id.as_ref().map(Into::into), + res.term, + ) { return Ok(()); } if state.cluster_version == res.cluster_version { diff --git a/crates/curp/src/client/stream.rs b/crates/curp/src/client/stream.rs index 30dca8f88..9ebeb1599 100644 --- a/crates/curp/src/client/stream.rs +++ b/crates/curp/src/client/stream.rs @@ -1,7 +1,7 @@ use std::{sync::Arc, time::Duration}; use futures::Future; -use tracing::{debug, warn}; +use tracing::{debug, info, warn}; use super::state::State; use crate::rpc::{connect::ConnectApi, CurpError, Redirect}; @@ -29,6 +29,9 @@ pub(super) struct Streaming { config: StreamingConfig, } +/// Prevent lock contention when leader crashed or some unknown errors +const RETRY_DELAY: Duration = Duration::from_millis(100); + impl Streaming { /// Create a stream client pub(super) fn new(state: Arc, config: StreamingConfig) -> Self { @@ -43,8 +46,9 @@ impl Streaming { ) -> Result { loop { let Some(leader_id) = self.state.leader_id().await else { - debug!("cannot find the leader id in state, wait for leadership update"); - self.state.leader_notifier().listen().await; + warn!("cannot find leader_id, refreshing state..."); + let _ig = self.state.try_refresh_state().await; + tokio::time::sleep(RETRY_DELAY).await; continue; }; if let Some(local_id) = self.state.local_server_id() { @@ -61,8 +65,6 @@ impl Streaming { /// Keep heartbeat pub(super) async fn keep_heartbeat(&self) { - /// Prevent lock contention when leader crashed or some unknown errors - const RETRY_DELAY: Duration = Duration::from_millis(100); #[allow(clippy::ignored_unit_patterns)] // tokio select internal triggered loop { let heartbeat = self.map_remote_leader::<(), _>(|conn| async move { @@ -76,7 +78,10 @@ impl Streaming { #[allow(clippy::wildcard_enum_match_arm)] match err { CurpError::Redirect(Redirect { leader_id, term }) => { - let _ig = self.state.check_and_update_leader(leader_id, term).await; + let _ig = self + .state + .check_and_update_leader(leader_id.map(Into::into), term) + .await; } CurpError::WrongClusterVersion(()) => { warn!( @@ -84,9 +89,16 @@ impl Streaming { ); self.state.leader_notifier().listen().await; } + CurpError::RpcTransport(()) => { + warn!( + "got rpc transport error when keep heartbeat, refreshing state..." + ); + let _ig = self.state.try_refresh_state().await; + tokio::time::sleep(RETRY_DELAY).await; + } CurpError::ShuttingDown(()) => { - debug!("shutting down stream client background task"); - break Err(err); + info!("cluster is shutting down, exiting heartbeat task"); + return Ok(()); } _ => { warn!("got unexpected error {err:?} when keep heartbeat, retrying..."); diff --git a/crates/curp/src/client/tests.rs b/crates/curp/src/client/tests.rs index a50df9ce0..f8c0649f3 100644 --- a/crates/curp/src/client/tests.rs +++ b/crates/curp/src/client/tests.rs @@ -1,35 +1,36 @@ use std::{ collections::HashMap, - ops::AddAssign, - sync::{ - atomic::{AtomicBool, AtomicU64}, - Arc, Mutex, - }, - time::Duration, + sync::{atomic::AtomicU64, Arc, Mutex}, + time::{Duration, Instant}, }; -use curp_external_api::LogIndex; use curp_test_utils::test_cmd::{LogIndexResult, TestCommand, TestCommandResult}; -use futures::future::BoxFuture; -use tokio::time::Instant; +use futures::{future::BoxFuture, Stream}; #[cfg(not(madsim))] use tonic::transport::ClientTlsConfig; +use tonic::Status; use tracing_test::traced_test; #[cfg(madsim)] use utils::ClientTlsConfig; use super::{ - retry::{Retry, RetryConfig}, state::State, stream::{Streaming, StreamingConfig}, unary::{Unary, UnaryConfig}, }; use crate::{ - client::ClientApi, + client::{ + retry::{Retry, RetryConfig}, + ClientApi, + }, members::ServerId, rpc::{ connect::{ConnectApi, MockConnectApi}, - *, + CurpError, FetchClusterRequest, FetchClusterResponse, FetchReadStateRequest, + FetchReadStateResponse, Member, MoveLeaderRequest, MoveLeaderResponse, OpResponse, + ProposeConfChangeRequest, ProposeConfChangeResponse, ProposeRequest, ProposeResponse, + PublishRequest, PublishResponse, ReadIndexResponse, RecordRequest, RecordResponse, + ResponseOp, ShutdownRequest, ShutdownResponse, SyncedResponse, }, }; @@ -82,7 +83,7 @@ async fn test_unary_fetch_clusters_serializable() { let connects = init_mocked_connects(3, |_id, conn| { conn.expect_fetch_cluster().return_once(|_req, _timeout| { Ok(tonic::Response::new(FetchClusterResponse { - leader_id: Some(0), + leader_id: Some(0.into()), term: 1, cluster_id: 123, members: vec![ @@ -119,7 +120,7 @@ async fn test_unary_fetch_clusters_serializable_local_first() { panic!("other server's `fetch_cluster` should not be invoked"); }; Ok(tonic::Response::new(FetchClusterResponse { - leader_id: Some(0), + leader_id: Some(0.into()), term: 1, cluster_id: 123, members, @@ -140,7 +141,7 @@ async fn test_unary_fetch_clusters_linearizable() { .return_once(move |_req, _timeout| { let resp = match id { 0 => FetchClusterResponse { - leader_id: Some(0), + leader_id: Some(0.into()), term: 2, cluster_id: 123, members: vec![ @@ -153,7 +154,7 @@ async fn test_unary_fetch_clusters_linearizable() { cluster_version: 1, }, 1 | 4 => FetchClusterResponse { - leader_id: Some(0), + leader_id: Some(0.into()), term: 2, cluster_id: 123, members: vec![], // linearizable read from follower returns empty members @@ -167,8 +168,8 @@ async fn test_unary_fetch_clusters_linearizable() { cluster_version: 1, }, 3 => FetchClusterResponse { - leader_id: Some(3), // imagine this node is a old leader - term: 1, // with the old term + leader_id: Some(3.into()), // imagine this node is a old leader + term: 1, // with the old term cluster_id: 123, members: vec![ Member::new(0, "S0", vec!["B0".to_owned()], [], false), @@ -206,7 +207,7 @@ async fn test_unary_fetch_clusters_linearizable_failed() { .return_once(move |_req, _timeout| { let resp = match id { 0 => FetchClusterResponse { - leader_id: Some(0), + leader_id: Some(0.into()), term: 2, cluster_id: 123, members: vec![ @@ -219,7 +220,7 @@ async fn test_unary_fetch_clusters_linearizable_failed() { cluster_version: 1, }, 1 => FetchClusterResponse { - leader_id: Some(0), + leader_id: Some(0.into()), term: 2, cluster_id: 123, members: vec![], // linearizable read from follower returns empty members @@ -233,8 +234,8 @@ async fn test_unary_fetch_clusters_linearizable_failed() { cluster_version: 1, }, 3 => FetchClusterResponse { - leader_id: Some(3), // imagine this node is a old leader - term: 1, // with the old term + leader_id: Some(3.into()), // imagine this node is a old leader + term: 1, // with the old term cluster_id: 123, members: vec![ Member::new(0, "S0", vec!["B0".to_owned()], [], false), @@ -246,8 +247,8 @@ async fn test_unary_fetch_clusters_linearizable_failed() { cluster_version: 1, }, 4 => FetchClusterResponse { - leader_id: Some(3), // imagine this node is a old follower of old leader(3) - term: 1, // with the old term + leader_id: Some(3.into()), // imagine this node is a old follower of old leader(3) + term: 1, // with the old term cluster_id: 123, members: vec![], cluster_version: 1, @@ -259,231 +260,56 @@ async fn test_unary_fetch_clusters_linearizable_failed() { }); let unary = init_unary_client(connects, None, None, 0, 0, None); let res = unary.fetch_cluster(true).await.unwrap_err(); - // only server(0, 1)'s responses are valid, less than majority quorum(3), got a mocked RpcTransport to retry + // only server(0, 1)'s responses are valid, less than majority quorum(3), got a + // mocked RpcTransport to retry assert_eq!(res, CurpError::RpcTransport(())); } -#[traced_test] -#[tokio::test] -async fn test_unary_fast_round_works() { - let connects = init_mocked_connects(5, |id, conn| { - conn.expect_propose() - .return_once(move |_req, _token, _timeout| { - let resp = match id { - 0 => ProposeResponse::new_result::(&Ok( - TestCommandResult::default(), - )), - 1 | 2 | 3 => ProposeResponse::new_empty(), - 4 => return Err(CurpError::key_conflict()), - _ => unreachable!("there are only 5 nodes"), - }; - Ok(tonic::Response::new(resp)) - }); - }); - let unary = init_unary_client(connects, None, None, 0, 0, None); - let res = unary - .fast_round(ProposeId(0, 0), &TestCommand::default(), None) - .await - .unwrap() - .unwrap(); - assert_eq!(res, TestCommandResult::default()); +fn build_propose_response(conflict: bool) -> OpResponse { + let resp = ResponseOp::Propose(ProposeResponse::new_result::( + &Ok(TestCommandResult::default()), + conflict, + )); + OpResponse { op: Some(resp) } } -#[traced_test] -#[tokio::test] -async fn test_unary_fast_round_return_early_err() { - for early_err in [ - CurpError::duplicated(), - CurpError::shutting_down(), - CurpError::invalid_config(), - CurpError::node_already_exists(), - CurpError::node_not_exist(), - CurpError::learner_not_catch_up(), - CurpError::expired_client_id(), - CurpError::redirect(Some(1), 0), - ] { - assert!(early_err.should_abort_fast_round()); - // record how many times `handle_propose` was invoked. - let counter = Arc::new(Mutex::new(0)); - let connects = init_mocked_connects(3, |_id, conn| { - let counter_c = Arc::clone(&counter); - let err = early_err.clone(); - conn.expect_propose() - .return_once(move |_req, _token, _timeout| { - counter_c.lock().unwrap().add_assign(1); - Err(err) - }); - }); - let unary = init_unary_client(connects, None, None, 0, 0, None); - let err = unary - .fast_round(ProposeId(0, 0), &TestCommand::default(), None) - .await - .unwrap_err(); - assert_eq!(err, early_err); - assert_eq!(*counter.lock().unwrap(), 1); - } +fn build_synced_response() -> OpResponse { + let resp = ResponseOp::Synced(SyncedResponse::new_result::(&Ok(1.into()))); + OpResponse { op: Some(resp) } } -#[traced_test] -#[tokio::test] -async fn test_unary_fast_round_less_quorum() { - let connects = init_mocked_connects(5, |id, conn| { - conn.expect_propose() - .return_once(move |_req, _token, _timeout| { - let resp = match id { - 0 => ProposeResponse::new_result::(&Ok( - TestCommandResult::default(), - )), - 1 | 2 => ProposeResponse::new_empty(), - 3 | 4 => return Err(CurpError::key_conflict()), - _ => unreachable!("there are only 5 nodes"), - }; - Ok(tonic::Response::new(resp)) - }); - }); - let unary = init_unary_client(connects, None, None, 0, 0, None); - let err = unary - .fast_round(ProposeId(0, 0), &TestCommand::default(), None) - .await - .unwrap_err(); - assert_eq!(err, CurpError::KeyConflict(())); -} - -/// FIXME: two leader -/// TODO: fix in subsequence PR -#[traced_test] -#[tokio::test] -#[should_panic(expected = "should not set exe result twice")] -async fn test_unary_fast_round_with_two_leader() { - let connects = init_mocked_connects(5, |id, conn| { - conn.expect_propose() - .return_once(move |_req, _token, _timeout| { - let resp = - match id { - // The execution result has been returned, indicating that server(0) has also recorded the command. - 0 => ProposeResponse::new_result::(&Ok( - TestCommandResult::new(vec![1], vec![1]), - )), - // imagine that server(1) is the new leader - 1 => ProposeResponse::new_result::(&Ok( - TestCommandResult::new(vec![2], vec![2]), - )), - 2 | 3 => ProposeResponse::new_empty(), - 4 => return Err(CurpError::key_conflict()), - _ => unreachable!("there are only 5 nodes"), - }; - Ok(tonic::Response::new(resp)) - }); - }); - // old local leader(0), term 1 - let unary = init_unary_client(connects, None, Some(0), 1, 0, None); - let res = unary - .fast_round(ProposeId(0, 0), &TestCommand::default(), None) - .await - .unwrap() - .unwrap(); - // quorum: server(0, 1, 2, 3) - assert_eq!(res, TestCommandResult::new(vec![2], vec![2])); -} - -// We may encounter this scenario during leader election -#[traced_test] -#[tokio::test] -async fn test_unary_fast_round_without_leader() { - let connects = init_mocked_connects(5, |id, conn| { - conn.expect_propose() - .return_once(move |_req, _token, _timeout| { - let resp = match id { - 0 | 1 | 2 | 3 | 4 => ProposeResponse::new_empty(), - _ => unreachable!("there are only 5 nodes"), - }; - Ok(tonic::Response::new(resp)) - }); - }); - // old local leader(0), term 1 - let unary = init_unary_client(connects, None, Some(0), 1, 0, None); - let res = unary - .fast_round(ProposeId(0, 0), &TestCommand::default(), None) - .await - .unwrap_err(); - // quorum: server(0, 1, 2, 3) - assert_eq!(res, CurpError::WrongClusterVersion(())); -} - -#[traced_test] -#[tokio::test] -async fn test_unary_slow_round_fetch_leader_first() { - let flag = Arc::new(AtomicBool::new(false)); - let connects = init_mocked_connects(3, |id, conn| { - let flag_c = Arc::clone(&flag); - conn.expect_fetch_cluster() - .return_once(move |_req, _timeout| { - flag_c.store(true, std::sync::atomic::Ordering::Relaxed); - Ok(tonic::Response::new(FetchClusterResponse { - leader_id: Some(0), - term: 1, - cluster_id: 123, - members: vec![ - Member::new(0, "S0", vec!["A0".to_owned()], [], false), - Member::new(1, "S1", vec!["A1".to_owned()], [], false), - Member::new(2, "S2", vec!["A2".to_owned()], [], false), - ], - cluster_version: 1, - })) - }); - let flag_c = Arc::clone(&flag); - conn.expect_wait_synced() - .return_once(move |_req, _timeout| { - assert!(id == 0, "wait synced should send to leader"); - assert!( - flag_c.load(std::sync::atomic::Ordering::Relaxed), - "fetch_leader should invoke first" - ); - Ok(tonic::Response::new(WaitSyncedResponse::new_from_result::< - TestCommand, - >( - Ok(TestCommandResult::default()), - Some(Ok(1.into())), - ))) - }); - }); - let unary = init_unary_client(connects, None, None, 0, 0, None); - let res = unary.slow_round(ProposeId(0, 0)).await.unwrap().unwrap(); - assert_eq!(LogIndex::from(res.0), 1); - assert_eq!(res.1, TestCommandResult::default()); +// TODO: rewrite this tests +#[cfg(ignore)] +fn build_empty_response() -> OpResponse { + OpResponse { op: None } } #[traced_test] #[tokio::test] async fn test_unary_propose_fast_path_works() { let connects = init_mocked_connects(5, |id, conn| { - conn.expect_propose() + conn.expect_propose_stream() .return_once(move |_req, _token, _timeout| { - let resp = match id { - 0 => ProposeResponse::new_result::(&Ok( - TestCommandResult::default(), - )), - 1 | 2 | 3 => ProposeResponse::new_empty(), - 4 => return Err(CurpError::key_conflict()), - _ => unreachable!("there are only 5 nodes"), + assert_eq!(id, 0, "followers should not receive propose"); + let resp = async_stream::stream! { + yield Ok(build_propose_response(false)); + yield Ok(build_synced_response()); }; - Ok(tonic::Response::new(resp)) - }); - conn.expect_wait_synced() - .return_once(move |_req, _timeout| { - assert!(id == 0, "wait synced should send to leader"); - std::thread::sleep(Duration::from_millis(100)); - Ok(tonic::Response::new(WaitSyncedResponse::new_from_result::< - TestCommand, - >( - Ok(TestCommandResult::default()), - Some(Ok(1.into())), - ))) + Ok(tonic::Response::new(Box::new(resp))) }); + conn.expect_record().return_once(move |_req, _timeout| { + let resp = match id { + 0 => unreachable!("leader should not receive record request"), + 1 | 2 | 3 => RecordResponse { conflict: false }, + 4 => RecordResponse { conflict: true }, + _ => unreachable!("there are only 5 nodes"), + }; + Ok(tonic::Response::new(resp)) + }); }); let unary = init_unary_client(connects, None, Some(0), 1, 0, None); let res = unary - .propose(&TestCommand::default(), None, true) + .propose(&TestCommand::new_put(vec![1], 1), None, true) .await .unwrap() .unwrap(); @@ -494,34 +320,31 @@ async fn test_unary_propose_fast_path_works() { #[tokio::test] async fn test_unary_propose_slow_path_works() { let connects = init_mocked_connects(5, |id, conn| { - conn.expect_propose() + conn.expect_propose_stream() .return_once(move |_req, _token, _timeout| { - let resp = match id { - 0 => ProposeResponse::new_result::(&Ok( - TestCommandResult::default(), - )), - 1 | 2 | 3 => ProposeResponse::new_empty(), - 4 => return Err(CurpError::key_conflict()), - _ => unreachable!("there are only 5 nodes"), + assert_eq!(id, 0, "followers should not receive propose"); + let resp = async_stream::stream! { + yield Ok(build_propose_response(false)); + tokio::time::sleep(Duration::from_millis(100)).await; + yield Ok(build_synced_response()); }; - Ok(tonic::Response::new(resp)) - }); - conn.expect_wait_synced() - .return_once(move |_req, _timeout| { - assert!(id == 0, "wait synced should send to leader"); - std::thread::sleep(Duration::from_millis(100)); - Ok(tonic::Response::new(WaitSyncedResponse::new_from_result::< - TestCommand, - >( - Ok(TestCommandResult::default()), - Some(Ok(1.into())), - ))) + Ok(tonic::Response::new(Box::new(resp))) }); + conn.expect_record().return_once(move |_req, _timeout| { + let resp = match id { + 0 => unreachable!("leader should not receive record request"), + 1 | 2 | 3 => RecordResponse { conflict: false }, + 4 => RecordResponse { conflict: true }, + _ => unreachable!("there are only 5 nodes"), + }; + Ok(tonic::Response::new(resp)) + }); }); + let unary = init_unary_client(connects, None, Some(0), 1, 0, None); let start_at = Instant::now(); let res = unary - .propose(&TestCommand::default(), None, false) + .propose(&TestCommand::new_put(vec![1], 1), None, false) .await .unwrap() .unwrap(); @@ -538,36 +361,33 @@ async fn test_unary_propose_slow_path_works() { #[traced_test] #[tokio::test] async fn test_unary_propose_fast_path_fallback_slow_path() { + // record how many times `handle_propose` was invoked. let connects = init_mocked_connects(5, |id, conn| { - conn.expect_propose() + conn.expect_propose_stream() .return_once(move |_req, _token, _timeout| { - // insufficient quorum to force slow path. - let resp = match id { - 0 => ProposeResponse::new_result::(&Ok( - TestCommandResult::default(), - )), - 1 | 2 => ProposeResponse::new_empty(), - 3 | 4 => return Err(CurpError::key_conflict()), - _ => unreachable!("there are only 5 nodes"), + assert_eq!(id, 0, "followers should not receive propose"); + let resp = async_stream::stream! { + yield Ok(build_propose_response(false)); + tokio::time::sleep(Duration::from_millis(100)).await; + yield Ok(build_synced_response()); }; - Ok(tonic::Response::new(resp)) - }); - conn.expect_wait_synced() - .return_once(move |_req, _timeout| { - assert!(id == 0, "wait synced should send to leader"); - std::thread::sleep(Duration::from_millis(100)); - Ok(tonic::Response::new(WaitSyncedResponse::new_from_result::< - TestCommand, - >( - Ok(TestCommandResult::default()), - Some(Ok(1.into())), - ))) + Ok(tonic::Response::new(Box::new(resp))) }); + // insufficient quorum + conn.expect_record().return_once(move |_req, _timeout| { + let resp = match id { + 0 => unreachable!("leader should not receive record request"), + 1 | 2 => RecordResponse { conflict: false }, + 3 | 4 => RecordResponse { conflict: true }, + _ => unreachable!("there are only 5 nodes"), + }; + Ok(tonic::Response::new(resp)) + }); }); let unary = init_unary_client(connects, None, Some(0), 1, 0, None); let start_at = Instant::now(); let res = unary - .propose(&TestCommand::default(), None, true) + .propose(&TestCommand::new_put(vec![1], 1), None, true) .await .unwrap() .unwrap(); @@ -575,6 +395,7 @@ async fn test_unary_propose_fast_path_fallback_slow_path() { start_at.elapsed() > Duration::from_millis(100), "slow round takes at least 100ms" ); + // indicate that we actually run out of fast round assert_eq!( res, (TestCommandResult::default(), Some(LogIndexResult::from(1))) @@ -596,26 +417,22 @@ async fn test_unary_propose_return_early_err() { assert!(early_err.should_abort_fast_round()); // record how many times rpc was invoked. let counter = Arc::new(Mutex::new(0)); - let connects = init_mocked_connects(5, |id, conn| { + let connects = init_mocked_connects(5, |_id, conn| { let err = early_err.clone(); let counter_c = Arc::clone(&counter); - conn.expect_propose() + conn.expect_propose_stream() .return_once(move |_req, _token, _timeout| { - counter_c.lock().unwrap().add_assign(1); + *counter_c.lock().unwrap() += 1; Err(err) }); + let err = early_err.clone(); - let counter_c = Arc::clone(&counter); - conn.expect_wait_synced() - .return_once(move |_req, _timeout| { - assert!(id == 0, "wait synced should send to leader"); - counter_c.lock().unwrap().add_assign(1); - Err(err) - }); + conn.expect_record() + .return_once(move |_req, _timeout| Err(err)); }); let unary = init_unary_client(connects, None, Some(0), 1, 0, None); let err = unary - .propose(&TestCommand::default(), None, true) + .propose(&TestCommand::new_put(vec![1], 1), None, true) .await .unwrap_err(); assert_eq!(err, early_err); @@ -637,22 +454,18 @@ async fn test_retry_propose_return_no_retry_error() { ] { // record how many times rpc was invoked. let counter = Arc::new(Mutex::new(0)); - let connects = init_mocked_connects(5, |id, conn| { + let connects = init_mocked_connects(5, |_id, conn| { let err = early_err.clone(); let counter_c = Arc::clone(&counter); - conn.expect_propose() + conn.expect_propose_stream() .return_once(move |_req, _token, _timeout| { - counter_c.lock().unwrap().add_assign(1); + *counter_c.lock().unwrap() += 1; Err(err) }); + let err = early_err.clone(); - let counter_c = Arc::clone(&counter); - conn.expect_wait_synced() - .return_once(move |_req, _timeout| { - assert!(id == 0, "wait synced should send to leader"); - counter_c.lock().unwrap().add_assign(1); - Err(err) - }); + conn.expect_record() + .return_once(move |_req, _timeout| Err(err)); }); let unary = init_unary_client(connects, None, Some(0), 1, 0, None); let retry = Retry::new( @@ -661,12 +474,11 @@ async fn test_retry_propose_return_no_retry_error() { None, ); let err = retry - .propose(&TestCommand::default(), None, false) + .propose(&TestCommand::new_put(vec![1], 1), None, false) .await .unwrap_err(); assert_eq!(err.message(), tonic::Status::from(early_err).message()); - // fast path + slow path = 2 - assert_eq!(*counter.lock().unwrap(), 2); + assert_eq!(*counter.lock().unwrap(), 1); } } @@ -674,17 +486,14 @@ async fn test_retry_propose_return_no_retry_error() { #[tokio::test] async fn test_retry_propose_return_retry_error() { for early_err in [ - CurpError::expired_client_id(), - CurpError::key_conflict(), CurpError::RpcTransport(()), CurpError::internal("No reason"), ] { let connects = init_mocked_connects(5, |id, conn| { - let err = early_err.clone(); conn.expect_fetch_cluster() .returning(move |_req, _timeout| { Ok(tonic::Response::new(FetchClusterResponse { - leader_id: Some(0), + leader_id: Some(0.into()), term: 2, cluster_id: 123, members: vec![ @@ -697,14 +506,16 @@ async fn test_retry_propose_return_retry_error() { cluster_version: 1, })) }); - conn.expect_propose() - .returning(move |_req, _token, _timeout| Err(err.clone())); if id == 0 { let err = early_err.clone(); - conn.expect_wait_synced() - .times(5) // wait synced should be retried in 5 times on leader - .returning(move |_req, _timeout| Err(err.clone())); + conn.expect_propose_stream() + .times(5) // propose should be retried in 5 times on leader + .returning(move |_req, _token, _timeout| Err(err.clone())); } + + let err = early_err.clone(); + conn.expect_record() + .returning(move |_req, _timeout| Err(err.clone())); }); let unary = init_unary_client(connects, None, Some(0), 1, 0, None); let retry = Retry::new( @@ -713,13 +524,75 @@ async fn test_retry_propose_return_retry_error() { None, ); let err = retry - .propose(&TestCommand::default(), None, false) + .propose(&TestCommand::new_put(vec![1], 1), None, false) .await .unwrap_err(); assert!(err.message().contains("request timeout")); } } +#[traced_test] +#[tokio::test] +async fn test_read_index_success() { + let connects = init_mocked_connects(5, |id, conn| { + conn.expect_propose_stream() + .return_once(move |_req, _token, _timeout| { + assert_eq!(id, 0, "followers should not receive propose"); + let resp = async_stream::stream! { + yield Ok(build_propose_response(false)); + yield Ok(build_synced_response()); + }; + Ok(tonic::Response::new(Box::new(resp))) + }); + conn.expect_read_index().return_once(move |_timeout| { + let resp = match id { + 0 => unreachable!("read index should not send to leader"), + 1 | 2 => ReadIndexResponse { term: 1 }, + 3 | 4 => ReadIndexResponse { term: 2 }, + _ => unreachable!("there are only 5 nodes"), + }; + + Ok(tonic::Response::new(resp)) + }); + }); + let unary = init_unary_client(connects, None, Some(0), 1, 0, None); + let res = unary + .propose(&TestCommand::default(), None, true) + .await + .unwrap() + .unwrap(); + assert_eq!(res, (TestCommandResult::default(), None)); +} + +#[traced_test] +#[tokio::test] +async fn test_read_index_fail() { + let connects = init_mocked_connects(5, |id, conn| { + conn.expect_propose_stream() + .return_once(move |_req, _token, _timeout| { + assert_eq!(id, 0, "followers should not receive propose"); + let resp = async_stream::stream! { + yield Ok(build_propose_response(false)); + yield Ok(build_synced_response()); + }; + Ok(tonic::Response::new(Box::new(resp))) + }); + conn.expect_read_index().return_once(move |_timeout| { + let resp = match id { + 0 => unreachable!("read index should not send to leader"), + 1 => ReadIndexResponse { term: 1 }, + 2 | 3 | 4 => ReadIndexResponse { term: 2 }, + _ => unreachable!("there are only 5 nodes"), + }; + + Ok(tonic::Response::new(resp)) + }); + }); + let unary = init_unary_client(connects, None, Some(0), 1, 0, None); + let res = unary.propose(&TestCommand::default(), None, true).await; + assert!(res.is_err()); +} + // Tests for stream client struct MockedStreamConnectApi { @@ -741,12 +614,30 @@ impl ConnectApi for MockedStreamConnectApi { } /// Send `ProposeRequest` - async fn propose( + async fn propose_stream( &self, _request: ProposeRequest, _token: Option, _timeout: Duration, - ) -> Result, CurpError> { + ) -> Result> + Send>>, CurpError> + { + unreachable!("please use MockedConnectApi") + } + + /// Send `RecordRequest` + async fn record( + &self, + _request: RecordRequest, + _timeout: Duration, + ) -> Result, CurpError> { + unreachable!("please use MockedConnectApi") + } + + /// Send `ReadIndexRequest` + async fn read_index( + &self, + _timeout: Duration, + ) -> Result, CurpError> { unreachable!("please use MockedConnectApi") } @@ -768,15 +659,6 @@ impl ConnectApi for MockedStreamConnectApi { unreachable!("please use MockedConnectApi") } - /// Send `WaitSyncedRequest` - async fn wait_synced( - &self, - _request: WaitSyncedRequest, - _timeout: Duration, - ) -> Result, CurpError> { - unreachable!("please use MockedConnectApi") - } - /// Send `ShutdownRequest` async fn shutdown( &self, @@ -820,6 +702,7 @@ impl ConnectApi for MockedStreamConnectApi { } /// Create mocked stream connects +/// /// The leader is S0 #[allow(trivial_casts)] // cannot be inferred fn init_mocked_stream_connects( diff --git a/crates/curp/src/client/unary.rs b/crates/curp/src/client/unary.rs index fdccfdf61..2acf6658a 100644 --- a/crates/curp/src/client/unary.rs +++ b/crates/curp/src/client/unary.rs @@ -1,21 +1,33 @@ -use std::{cmp::Ordering, marker::PhantomData, ops::AddAssign, sync::Arc, time::Duration}; +use std::{ + cmp::Ordering, + marker::PhantomData, + sync::{atomic::AtomicU64, Arc}, + time::Duration, +}; use async_trait::async_trait; use curp_external_api::cmd::Command; -use futures::{Future, StreamExt}; -use tonic::Response; +use futures::{future, stream::FuturesUnordered, Future, Stream, StreamExt}; +use parking_lot::RwLock; +use tonic::{Response, Status}; use tracing::{debug, warn}; -use super::{state::State, ClientApi, LeaderStateUpdate, ProposeResponse, RepeatableClientApi}; +use super::{ + state::State, ClientApi, LeaderStateUpdate, ProposeIdGuard, ProposeResponse, + RepeatableClientApi, +}; use crate::{ members::ServerId, - quorum, recover_quorum, + quorum, + response::ResponseReceiver, rpc::{ connect::ConnectApi, ConfChange, CurpError, FetchClusterRequest, FetchClusterResponse, - FetchReadStateRequest, Member, MoveLeaderRequest, ProposeConfChangeRequest, ProposeId, - ProposeRequest, PublishRequest, ReadState, ShutdownRequest, WaitSyncedRequest, + FetchReadStateRequest, Member, MoveLeaderRequest, OpResponse, ProposeConfChangeRequest, + ProposeId, ProposeRequest, PublishRequest, ReadIndexResponse, ReadState, RecordRequest, + RecordResponse, ShutdownRequest, }, super_quorum, + tracker::Tracker, }; /// The unary client config @@ -24,6 +36,7 @@ pub(super) struct UnaryConfig { /// The rpc timeout of a propose request propose_timeout: Duration, /// The rpc timeout of a 2-RTT request, usually takes longer than propose timeout + /// /// The recommended the values is within (propose_timeout, 2 * propose_timeout]. wait_synced_timeout: Duration, } @@ -45,6 +58,10 @@ pub(super) struct Unary { state: Arc, /// Unary config config: UnaryConfig, + /// Request tracker + tracker: RwLock, + /// Last sent sequence number + last_sent_seq: AtomicU64, /// marker phantom: PhantomData, } @@ -55,14 +72,20 @@ impl Unary { Self { state, config, + tracker: RwLock::new(Tracker::default()), + last_sent_seq: AtomicU64::new(0), phantom: PhantomData, } } /// Get a handle `f` and apply to the leader + /// /// NOTICE: + /// /// The leader might be outdate if the local state is stale. + /// /// `map_leader` should never be invoked in [`ClientApi::fetch_cluster`] + /// /// `map_leader` might call `fetch_leader_id`, `fetch_cluster`, finally /// result in stack overflow. async fn map_leader>>( @@ -78,128 +101,86 @@ impl Unary { self.state.map_server(leader_id, f).await } - /// Send proposal to all servers - pub(super) async fn fast_round( - &self, - propose_id: ProposeId, - cmd: &C, - token: Option<&String>, - ) -> Result, CurpError> { - let req = ProposeRequest::new(propose_id, cmd, self.state.cluster_version().await); - let timeout = self.config.propose_timeout; - - let mut responses = self - .state - .for_each_server(|conn| { - let req_c = req.clone(); - let token_c = token.cloned(); - async move { (conn.id(), conn.propose(req_c, token_c, timeout).await) } - }) - .await; - let super_quorum = super_quorum(responses.len()); - let recover_quorum = recover_quorum(responses.len()); - - let mut err: Option = None; - let mut execute_result: Option = None; - let (mut ok_cnt, mut key_conflict_cnt) = (0, 0); - - while let Some((id, resp)) = responses.next().await { - if key_conflict_cnt >= recover_quorum { - return Err(CurpError::KeyConflict(())); - } - - let resp = match resp { - Ok(resp) => resp.into_inner(), - Err(e) => { - warn!("propose cmd({propose_id}) to server({id}) error: {e:?}"); - if e.should_abort_fast_round() { - return Err(e); - } - if matches!(e, CurpError::KeyConflict(())) { - key_conflict_cnt.add_assign(1); - } - if let Some(old_err) = err.as_ref() { - if old_err.priority() <= e.priority() { - err = Some(e); - } - } else { - err = Some(e); - } - continue; - } - }; - let deserialize_res = resp.map_result::>(|res| { - let er = match res { - Ok(er) => er, - Err(cmd_err) => return Err(cmd_err), - }; - if let Some(er) = er { - assert!(execute_result.is_none(), "should not set exe result twice"); - execute_result = Some(er); - } - ok_cnt.add_assign(1); - Ok(()) - }); - let dr = match deserialize_res { - Ok(dr) => dr, - Err(ser_err) => { - warn!("serialize error: {ser_err}"); - // We blame this error to the server, although it may be a local error. - // We need to retry as same as a server error. - err = Some(CurpError::from(ser_err)); - continue; - } - }; - if let Err(cmd_err) = dr { - // got a command execution error early, abort the next requests and return the cmd error - return Ok(Err(cmd_err)); - } - // if the propose meets the super quorum and we got the execute result, - // that means we can safely abort the next requests - if ok_cnt >= super_quorum { - if let Some(er) = execute_result { - debug!("fast round for cmd({}) succeed", propose_id); - return Ok(Ok(er)); - } - } - } - - if let Some(err) = err { - return Err(err); + /// Gets the leader id + async fn leader_id(&self) -> Result { + let cached_leader = self.state.leader_id().await; + match cached_leader { + Some(id) => Ok(id), + None => as ClientApi>::fetch_leader_id(self, false).await, } - - // We will at least send the request to the leader if no `WrongClusterVersion` returned. - // If no errors occur, the leader should return the ER - // If it is because the super quorum has not been reached, an error will definitely occur. - // Otherwise, there is no leader in the cluster state currently, return wrong cluster version - // and attempt to retrieve the cluster state again. - Err(CurpError::wrong_cluster_version()) - } - - /// Wait synced result from server - pub(super) async fn slow_round( - &self, - propose_id: ProposeId, - ) -> Result, CurpError> { - let timeout = self.config.wait_synced_timeout; - let req = WaitSyncedRequest::new(propose_id, self.state.cluster_version().await); - let resp = self - .map_leader(|conn| async move { conn.wait_synced(req, timeout).await }) - .await? - .into_inner(); - let synced_res = resp.map_result::(|res| res).map_err(|ser_err| { - warn!("serialize error: {ser_err}"); - // Same as fast round, we blame the server for the serializing error. - CurpError::from(ser_err) - })?; - debug!("slow round for cmd({}) succeed", propose_id); - Ok(synced_res) } /// New a seq num and record it #[allow(clippy::unused_self)] // TODO: implement request tracker fn new_seq_num(&self) -> u64 { - rand::random() + self.last_sent_seq + .fetch_add(1, std::sync::atomic::Ordering::Relaxed) + } +} + +impl Unary { + /// Propose for read only commands + /// + /// For read-only commands, we only need to send propose to leader + async fn propose_read_only( + propose_fut: PF, + use_fast_path: bool, + read_index_futs: FuturesUnordered, + term: u64, + quorum: usize, + ) -> Result, CurpError> + where + PF: Future< + Output = Result< + Response> + Send>>, + CurpError, + >, + >, + RIF: Future, CurpError>>, + { + let term_count_fut = read_index_futs + .filter_map(|res| future::ready(res.ok())) + .filter(|resp| future::ready(resp.get_ref().term == term)) + .take(quorum.wrapping_sub(1)) + .count(); + let (propose_res, num_valid) = tokio::join!(propose_fut, term_count_fut); + if num_valid < quorum.wrapping_sub(1) { + return Err(CurpError::WrongClusterVersion(())); + } + let resp_stream = propose_res?.into_inner(); + let mut response_rx = ResponseReceiver::new(resp_stream); + response_rx.recv::(!use_fast_path).await + } + + /// Propose for mutative commands + async fn propose_mutative( + propose_fut: PF, + record_futs: FuturesUnordered, + use_fast_path: bool, + superquorum: usize, + ) -> Result, CurpError> + where + PF: Future< + Output = Result< + Response> + Send>>, + CurpError, + >, + >, + RF: Future, CurpError>>, + { + let record_futs_filtered = record_futs + .filter_map(|res| future::ready(res.ok())) + .filter(|resp| future::ready(!resp.get_ref().conflict)) + .take(superquorum.wrapping_sub(1)) + .collect::>(); + let (propose_res, record_resps) = tokio::join!(propose_fut, record_futs_filtered); + + let resp_stream = propose_res?.into_inner(); + let mut response_rx = ResponseReceiver::new(resp_stream); + let fast_path_failed = record_resps.len() < superquorum.wrapping_sub(1); + response_rx + .recv::(fast_path_failed || !use_fast_path) + .await } } @@ -220,7 +201,7 @@ impl ClientApi for Unary { use_fast_path: bool, ) -> Result, CurpError> { let propose_id = self.gen_propose_id()?; - RepeatableClientApi::propose(self, propose_id, cmd, token, use_fast_path).await + RepeatableClientApi::propose(self, *propose_id, cmd, token, use_fast_path).await } /// Send propose configuration changes to the cluster @@ -229,13 +210,13 @@ impl ClientApi for Unary { changes: Vec, ) -> Result, CurpError> { let propose_id = self.gen_propose_id()?; - RepeatableClientApi::propose_conf_change(self, propose_id, changes).await + RepeatableClientApi::propose_conf_change(self, *propose_id, changes).await } /// Send propose to shutdown cluster async fn propose_shutdown(&self) -> Result<(), CurpError> { let propose_id = self.gen_propose_id()?; - RepeatableClientApi::propose_shutdown(self, propose_id).await + RepeatableClientApi::propose_shutdown(self, *propose_id).await } /// Send propose to publish a node id and name @@ -246,8 +227,14 @@ impl ClientApi for Unary { node_client_urls: Vec, ) -> Result<(), Self::Error> { let propose_id = self.gen_propose_id()?; - RepeatableClientApi::propose_publish(self, propose_id, node_id, node_name, node_client_urls) - .await + RepeatableClientApi::propose_publish( + self, + *propose_id, + node_id, + node_name, + node_client_urls, + ) + .await } /// Send move leader request @@ -282,7 +269,7 @@ impl ClientApi for Unary { /// Send fetch cluster requests to all servers /// Note: The fetched cluster may still be outdated if `linearizable` is false - async fn fetch_cluster(&self, linearizable: bool) -> Result { + async fn fetch_cluster(&self, linearizable: bool) -> Result { let timeout = self.config.wait_synced_timeout; if !linearizable { // firstly, try to fetch the local server @@ -292,12 +279,7 @@ impl ClientApi for Unary { let resp = connect .fetch_cluster(FetchClusterRequest::default(), FETCH_LOCAL_TIMEOUT) - .await - .unwrap_or_else(|e| { - unreachable!( - "fetch cluster from local connect should never failed, err {e:?}" - ) - }) + .await? .into_inner(); debug!("fetch local cluster {resp:?}"); @@ -390,10 +372,13 @@ impl ClientApi for Unary { #[async_trait] impl RepeatableClientApi for Unary { /// Generate a unique propose id during the retry process. - fn gen_propose_id(&self) -> Result { + fn gen_propose_id(&self) -> Result, Self::Error> { let client_id = self.state.client_id(); let seq_num = self.new_seq_num(); - Ok(ProposeId(client_id, seq_num)) + Ok(ProposeIdGuard::new( + &self.tracker, + ProposeId(client_id, seq_num), + )) } /// Send propose to the whole cluster, `use_fast_path` set to `false` to fallback into ordered @@ -405,93 +390,47 @@ impl RepeatableClientApi for Unary { token: Option<&String>, use_fast_path: bool, ) -> Result, Self::Error> { - tokio::pin! { - let fast_round = self.fast_round(propose_id, cmd, token); - let slow_round = self.slow_round(propose_id); - } + let cmd_arc = Arc::new(cmd); + let term = self.state.term().await; + let propose_req = ProposeRequest::new::( + propose_id, + cmd_arc.as_ref(), + self.state.cluster_version().await, + term, + !use_fast_path, + self.tracker.read().first_incomplete(), + ); + let record_req = RecordRequest::new::(propose_id, cmd_arc.as_ref()); + let connects_len = self.state.connects_len().await; + let quorum = quorum(connects_len); + let superquorum = super_quorum(connects_len); + let leader_id = self.leader_id().await?; + let timeout = self.config.propose_timeout; - let res: ProposeResponse = if use_fast_path { - match futures::future::select(fast_round, slow_round).await { - futures::future::Either::Left((fast_result, slow_round)) => match fast_result { - Ok(er) => er.map(|e| { - #[cfg(feature = "client-metrics")] - super::metrics::get().client_fast_path_count.add(1, &[]); - - (e, None) - }), - Err(fast_err) => { - if fast_err.should_abort_slow_round() { - return Err(fast_err); - } - // fallback to slow round if fast round failed - let sr = match slow_round.await { - Ok(sr) => sr, - Err(slow_err) => { - return Err(std::cmp::max_by_key(fast_err, slow_err, |err| { - err.priority() - })) - } - }; - sr.map(|(asr, er)| { - #[cfg(feature = "client-metrics")] - { - super::metrics::get().client_slow_path_count.add(1, &[]); - super::metrics::get() - .client_fast_path_fallback_slow_path_count - .add(1, &[]); - } - - (er, Some(asr)) - }) - } - }, - futures::future::Either::Right((slow_result, fast_round)) => match slow_result { - Ok(er) => er.map(|(asr, e)| { - #[cfg(feature = "client-metrics")] - super::metrics::get().client_slow_path_count.add(1, &[]); - - (e, Some(asr)) - }), - Err(slow_err) => { - if slow_err.should_abort_fast_round() { - return Err(slow_err); - } - // try to poll fast round - let fr = match fast_round.await { - Ok(fr) => fr, - Err(fast_err) => { - return Err(std::cmp::max_by_key(fast_err, slow_err, |err| { - err.priority() - })) - } - }; - fr.map(|er| { - #[cfg(feature = "client-metrics")] - super::metrics::get().client_fast_path_count.add(1, &[]); - - (er, None) - }) - } - }, - } - } else { - match futures::future::join(fast_round, slow_round).await { - (_, Ok(sr)) => sr.map(|(asr, er)| { - #[cfg(feature = "client-metrics")] - super::metrics::get().client_slow_path_count.add(1, &[]); - - (er, Some(asr)) - }), - (Ok(_), Err(err)) => return Err(err), - (Err(fast_err), Err(slow_err)) => { - return Err(std::cmp::max_by_key(fast_err, slow_err, |err| { - err.priority() - })) - } - } - }; + let propose_fut = self.state.map_server(leader_id, |conn| async move { + conn.propose_stream(propose_req, token.cloned(), timeout) + .await + }); + let record_futs = self + .state + .for_each_follower(leader_id, |conn| { + let record_req_c = record_req.clone(); + async move { conn.record(record_req_c, timeout).await } + }) + .await; + let read_index_futs = self + .state + .for_each_follower( + leader_id, + |conn| async move { conn.read_index(timeout).await }, + ) + .await; - Ok(res) + if cmd.is_read_only() { + Self::propose_read_only(propose_fut, use_fast_path, read_index_futs, term, quorum).await + } else { + Self::propose_mutative(propose_fut, record_futs, use_fast_path, superquorum).await + } } /// Send propose configuration changes to the cluster diff --git a/crates/curp/src/lib.rs b/crates/curp/src/lib.rs index a6a337218..e5e5111b6 100644 --- a/crates/curp/src/lib.rs +++ b/crates/curp/src/lib.rs @@ -203,6 +203,9 @@ pub mod rpc; /// Snapshot mod snapshot; +/// Propose response sender +mod response; + /// Calculate the super quorum #[inline] #[must_use] diff --git a/crates/curp/src/log_entry.rs b/crates/curp/src/log_entry.rs index 0cb890332..96ba66d8d 100644 --- a/crates/curp/src/log_entry.rs +++ b/crates/curp/src/log_entry.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; use crate::{ members::ServerId, - rpc::{ConfChange, PoolEntryInner, ProposeId, PublishRequest}, + rpc::{ConfChange, ProposeId, PublishRequest}, }; /// Log entry @@ -53,15 +53,6 @@ impl From> for EntryData { } } -impl From> for EntryData { - fn from(value: PoolEntryInner) -> Self { - match value { - PoolEntryInner::Command(cmd) => EntryData::Command(cmd), - PoolEntryInner::ConfChange(conf_change) => EntryData::ConfChange(conf_change), - } - } -} - impl From for EntryData { fn from(value: PublishRequest) -> Self { EntryData::SetNodeState(value.node_id, value.name, value.client_urls) @@ -93,10 +84,18 @@ where } } +impl AsRef> for LogEntry { + fn as_ref(&self) -> &LogEntry { + self + } +} + /// Propose id to inflight id pub(super) fn propose_id_to_inflight_id(id: ProposeId) -> InflightId { let mut hasher = std::collections::hash_map::DefaultHasher::new(); id.0.hash(&mut hasher); id.1.hash(&mut hasher); - hasher.finish() + let inflight = hasher.finish(); + tracing::debug!("convert: {id:?} to {inflight:?}"); + inflight } diff --git a/crates/curp/src/members.rs b/crates/curp/src/members.rs index 37a39c115..ce2045451 100644 --- a/crates/curp/src/members.rs +++ b/crates/curp/src/members.rs @@ -98,7 +98,9 @@ impl ClusterInfo { } /// Construct a new `ClusterInfo` from members map + /// /// # Panics + /// /// panic if `all_members` is empty #[inline] #[must_use] @@ -131,7 +133,9 @@ impl ClusterInfo { } /// Construct a new `ClusterInfo` from `FetchClusterResponse` + /// /// # Panics + /// /// panic if `cluster.members` doesn't contain `self_addr` #[inline] #[must_use] @@ -234,7 +238,9 @@ impl ClusterInfo { } /// Get the current member + /// /// # Panics + /// /// panic if self member id is not in members #[allow(clippy::unwrap_used)] // self member id must be in members #[must_use] @@ -257,7 +263,7 @@ impl ClusterInfo { self.self_member().client_urls.clone() } - /// Get the current server id + /// Get the current server name #[must_use] #[inline] pub fn self_name(&self) -> String { diff --git a/crates/curp/src/response.rs b/crates/curp/src/response.rs new file mode 100644 index 000000000..e6c5ca7e6 --- /dev/null +++ b/crates/curp/src/response.rs @@ -0,0 +1,134 @@ +use std::{ + pin::Pin, + sync::atomic::{AtomicBool, Ordering}, +}; + +use curp_external_api::cmd::Command; +use futures::Stream; +use tokio_stream::StreamExt; +use tonic::Status; + +use crate::rpc::{CurpError, OpResponse, ProposeResponse, ResponseOp, SyncedResponse}; + +/// The response sender +#[derive(Debug)] +pub(super) struct ResponseSender { + /// The stream sender + tx: flume::Sender>, + /// Whether the command will be speculatively executed + conflict: AtomicBool, +} + +impl ResponseSender { + /// Creates a new `ResponseSender` + pub(super) fn new(tx: flume::Sender>) -> ResponseSender { + ResponseSender { + tx, + conflict: AtomicBool::new(false), + } + } + + /// Gets whether the command associated with this sender will be + /// speculatively executed + pub(super) fn is_conflict(&self) -> bool { + self.conflict.load(Ordering::SeqCst) + } + + /// Sets the the command associated with this sender will be + /// speculatively executed + pub(super) fn set_conflict(&self, conflict: bool) { + let _ignore = self.conflict.fetch_or(conflict, Ordering::SeqCst); + } + + /// Sends propose result + pub(super) fn send_propose(&self, resp: ProposeResponse) { + let resp = OpResponse { + op: Some(ResponseOp::Propose(resp)), + }; + // Ignore the result because the client might close the receiving stream + let _ignore = self.tx.try_send(Ok(resp)); + } + + /// Sends after sync result + pub(super) fn send_synced(&self, resp: SyncedResponse) { + let resp = OpResponse { + op: Some(ResponseOp::Synced(resp)), + }; + // Ignore the result because the client might close the receiving stream + let _ignore = self.tx.try_send(Ok(resp)); + } +} + +/// Receiver for obtaining execution or after sync results +pub(crate) struct ResponseReceiver { + /// The response stream + resp_stream: Pin> + Send>>, +} + +impl ResponseReceiver { + /// Creates a new [`ResponseReceiver`]. + pub(crate) fn new( + resp_stream: Box> + Send>, + ) -> Self { + Self { + resp_stream: Box::into_pin(resp_stream), + } + } + + /// Receives the results + pub(crate) async fn recv( + &mut self, + both: bool, + ) -> Result), C::Error>, CurpError> { + let fst = self.recv_resp().await?; + + match fst { + ResponseOp::Propose(propose_resp) => { + let conflict = propose_resp.conflict; + let er_result = propose_resp.map_result::(|res| { + res.map(|er| er.unwrap_or_else(|| unreachable!())) + })?; + if let Err(e) = er_result { + return Ok(Err(e)); + } + if conflict || both { + let snd = self.recv_resp().await?; + let ResponseOp::Synced(synced_resp) = snd else { + unreachable!() + }; + let asr_result = synced_resp + .map_result::(|res| res.unwrap_or_else(|| unreachable!()))?; + return Ok(er_result.and_then(|er| asr_result.map(|asr| (er, Some(asr))))); + } + Ok(er_result.map(|er| (er, None))) + } + ResponseOp::Synced(synced_resp) => { + let asr_result = synced_resp + .map_result::(|res| res.unwrap_or_else(|| unreachable!()))?; + if let Err(e) = asr_result { + return Ok(Err(e)); + } + let snd = self.recv_resp().await?; + let ResponseOp::Propose(propose_resp) = snd else { + unreachable!("op: {snd:?}") + }; + let er_result = propose_resp.map_result::(|res| { + res.map(|er| er.unwrap_or_else(|| unreachable!())) + })?; + Ok(er_result.and_then(|er| asr_result.map(|asr| (er, Some(asr))))) + } + } + } + + /// Receives a single response from stream + async fn recv_resp(&mut self) -> Result { + let resp = self + .resp_stream + .next() + .await + .ok_or(CurpError::internal("stream reaches on an end".to_owned()))??; + Ok(resp + .op + .unwrap_or_else(|| unreachable!("op should always exist"))) + } +} diff --git a/crates/curp/src/rpc/connect.rs b/crates/curp/src/rpc/connect.rs index cc4a4d175..d438b6c28 100644 --- a/crates/curp/src/rpc/connect.rs +++ b/crates/curp/src/rpc/connect.rs @@ -34,13 +34,17 @@ use crate::{ FetchClusterResponse, FetchReadStateRequest, FetchReadStateResponse, InstallSnapshotRequest, InstallSnapshotResponse, LeaseKeepAliveMsg, MoveLeaderRequest, MoveLeaderResponse, ProposeConfChangeRequest, ProposeConfChangeResponse, ProposeRequest, - ProposeResponse, Protocol, PublishRequest, PublishResponse, ShutdownRequest, - ShutdownResponse, TriggerShutdownRequest, TryBecomeLeaderNowRequest, VoteRequest, - VoteResponse, WaitSyncedRequest, WaitSyncedResponse, + Protocol, PublishRequest, PublishResponse, ShutdownRequest, ShutdownResponse, + TriggerShutdownRequest, TryBecomeLeaderNowRequest, VoteRequest, VoteResponse, }, snapshot::Snapshot, }; +use super::{ + proto::commandpb::{ReadIndexRequest, ReadIndexResponse}, + OpResponse, RecordRequest, RecordResponse, +}; + /// Install snapshot chunk size: 64KB const SNAPSHOT_CHUNK_SIZE: u64 = 64 * 1024; @@ -158,12 +162,28 @@ pub(crate) trait ConnectApi: Send + Sync + 'static { async fn update_addrs(&self, addrs: Vec) -> Result<(), tonic::transport::Error>; /// Send `ProposeRequest` - async fn propose( + async fn propose_stream( &self, request: ProposeRequest, token: Option, timeout: Duration, - ) -> Result, CurpError>; + ) -> Result< + tonic::Response> + Send>>, + CurpError, + >; + + /// Send `RecordRequest` + async fn record( + &self, + request: RecordRequest, + timeout: Duration, + ) -> Result, CurpError>; + + /// Send `ReadIndexRequest` + async fn read_index( + &self, + timeout: Duration, + ) -> Result, CurpError>; /// Send `ProposeRequest` async fn propose_conf_change( @@ -179,13 +199,6 @@ pub(crate) trait ConnectApi: Send + Sync + 'static { timeout: Duration, ) -> Result, CurpError>; - /// Send `WaitSyncedRequest` - async fn wait_synced( - &self, - request: WaitSyncedRequest, - timeout: Duration, - ) -> Result, CurpError>; - /// Send `ShutdownRequest` async fn shutdown( &self, @@ -369,6 +382,15 @@ impl Connect { } } +/// Sets timeout for a client connection +macro_rules! with_timeout { + ($timeout:expr, $client_op:expr) => { + tokio::time::timeout($timeout, $client_op) + .await + .map_err(|_| tonic::Status::deadline_exceeded("timeout"))? + }; +} + #[async_trait] impl ConnectApi for Connect> { /// Get server id @@ -382,21 +404,46 @@ impl ConnectApi for Connect> { } /// Send `ProposeRequest` - #[instrument(skip(self), name = "client propose")] - async fn propose( + async fn propose_stream( &self, request: ProposeRequest, token: Option, timeout: Duration, - ) -> Result, CurpError> { + ) -> Result< + tonic::Response> + Send>>, + CurpError, + > { let mut client = self.rpc_connect.clone(); let mut req = tonic::Request::new(request); - req.set_timeout(timeout); - req.metadata_mut().inject_current(); if let Some(token) = token { _ = req.metadata_mut().insert("token", token.parse()?); } - client.propose(req).await.map_err(Into::into) + let resp = with_timeout!(timeout, client.propose_stream(req))?.into_inner(); + Ok(tonic::Response::new(Box::new(resp))) + + // let resp = client.propose_stream(req).await?.map(Box::new); + // Ok(resp) + } + + /// Send `RecordRequest` + async fn record( + &self, + request: RecordRequest, + timeout: Duration, + ) -> Result, CurpError> { + let mut client = self.rpc_connect.clone(); + let req = tonic::Request::new(request); + with_timeout!(timeout, client.record(req)).map_err(Into::into) + } + + /// Send `ReadIndexRequest` + async fn read_index( + &self, + timeout: Duration, + ) -> Result, CurpError> { + let mut client = self.rpc_connect.clone(); + let req = tonic::Request::new(ReadIndexRequest {}); + with_timeout!(timeout, client.read_index(req)).map_err(Into::into) } /// Send `ShutdownRequest` @@ -408,9 +455,8 @@ impl ConnectApi for Connect> { ) -> Result, CurpError> { let mut client = self.rpc_connect.clone(); let mut req = tonic::Request::new(request); - req.set_timeout(timeout); req.metadata_mut().inject_current(); - client.shutdown(req).await.map_err(Into::into) + with_timeout!(timeout, client.shutdown(req)).map_err(Into::into) } /// Send `ProposeRequest` @@ -422,9 +468,8 @@ impl ConnectApi for Connect> { ) -> Result, CurpError> { let mut client = self.rpc_connect.clone(); let mut req = tonic::Request::new(request); - req.set_timeout(timeout); req.metadata_mut().inject_current(); - client.propose_conf_change(req).await.map_err(Into::into) + with_timeout!(timeout, client.propose_conf_change(req)).map_err(Into::into) } /// Send `PublishRequest` @@ -436,23 +481,8 @@ impl ConnectApi for Connect> { ) -> Result, CurpError> { let mut client = self.rpc_connect.clone(); let mut req = tonic::Request::new(request); - req.set_timeout(timeout); - req.metadata_mut().inject_current(); - client.publish(req).await.map_err(Into::into) - } - - /// Send `WaitSyncedRequest` - #[instrument(skip(self), name = "client propose")] - async fn wait_synced( - &self, - request: WaitSyncedRequest, - timeout: Duration, - ) -> Result, CurpError> { - let mut client = self.rpc_connect.clone(); - let mut req = tonic::Request::new(request); - req.set_timeout(timeout); req.metadata_mut().inject_current(); - client.wait_synced(req).await.map_err(Into::into) + with_timeout!(timeout, client.publish(req)).map_err(Into::into) } /// Send `FetchClusterRequest` @@ -462,9 +492,8 @@ impl ConnectApi for Connect> { timeout: Duration, ) -> Result, CurpError> { let mut client = self.rpc_connect.clone(); - let mut req = tonic::Request::new(request); - req.set_timeout(timeout); - client.fetch_cluster(req).await.map_err(Into::into) + let req = tonic::Request::new(request); + with_timeout!(timeout, client.fetch_cluster(req)).map_err(Into::into) } /// Send `FetchReadStateRequest` @@ -474,9 +503,8 @@ impl ConnectApi for Connect> { timeout: Duration, ) -> Result, CurpError> { let mut client = self.rpc_connect.clone(); - let mut req = tonic::Request::new(request); - req.set_timeout(timeout); - client.fetch_read_state(req).await.map_err(Into::into) + let req = tonic::Request::new(request); + with_timeout!(timeout, client.fetch_read_state(req)).map_err(Into::into) } /// Send `MoveLeaderRequest` @@ -486,9 +514,8 @@ impl ConnectApi for Connect> { timeout: Duration, ) -> Result, CurpError> { let mut client = self.rpc_connect.clone(); - let mut req = tonic::Request::new(request); - req.set_timeout(timeout); - client.move_leader(req).await.map_err(Into::into) + let req = tonic::Request::new(request); + with_timeout!(timeout, client.move_leader(req)).map_err(Into::into) } /// Keep send lease keep alive to server and mutate the client id @@ -533,9 +560,8 @@ impl InnerConnectApi for Connect> { let start_at = self.before_rpc::(); let mut client = self.rpc_connect.clone(); - let mut req = tonic::Request::new(request); - req.set_timeout(timeout); - let result = client.append_entries(req).await; + let req = tonic::Request::new(request); + let result = with_timeout!(timeout, client.append_entries(req)); #[cfg(feature = "client-metrics")] self.after_rpc(start_at, &result); @@ -553,9 +579,8 @@ impl InnerConnectApi for Connect> { let start_at = self.before_rpc::(); let mut client = self.rpc_connect.clone(); - let mut req = tonic::Request::new(request); - req.set_timeout(timeout); - let result = client.vote(req).await; + let req = tonic::Request::new(request); + let result = with_timeout!(timeout, client.vote(req)); #[cfg(feature = "client-metrics")] self.after_rpc(start_at, &result); @@ -601,9 +626,8 @@ impl InnerConnectApi for Connect> { let start_at = self.before_rpc::(); let mut client = self.rpc_connect.clone(); - let mut req = tonic::Request::new(TryBecomeLeaderNowRequest::default()); - req.set_timeout(timeout); - let result = client.try_become_leader_now(req).await; + let req = tonic::Request::new(TryBecomeLeaderNowRequest::default()); + let result = with_timeout!(timeout, client.try_become_leader_now(req)); #[cfg(feature = "client-metrics")] self.after_rpc(start_at, &result); @@ -631,6 +655,7 @@ impl BypassedConnect { const BYPASS_KEY: &str = "bypass"; /// Inject bypassed message into a request's metadata and check if it is a bypassed request. +/// /// A bypass request can skip the check for lease expiration (there will never be a disconnection from oneself). pub(crate) trait Bypass { /// Inject into metadata @@ -674,19 +699,47 @@ where } /// Send `ProposeRequest` - async fn propose( + #[instrument(skip(self), name = "client propose stream")] + async fn propose_stream( &self, request: ProposeRequest, token: Option, _timeout: Duration, - ) -> Result, CurpError> { + ) -> Result< + tonic::Response> + Send>>, + CurpError, + > { let mut req = tonic::Request::new(request); req.metadata_mut().inject_bypassed(); req.metadata_mut().inject_current(); if let Some(token) = token { _ = req.metadata_mut().insert("token", token.parse()?); } - self.server.propose(req).await.map_err(Into::into) + let resp = self.server.propose_stream(req).await?.into_inner(); + Ok(tonic::Response::new(Box::new(resp))) + } + + /// Send `RecordRequest` + #[instrument(skip(self), name = "client record")] + async fn record( + &self, + request: RecordRequest, + _timeout: Duration, + ) -> Result, CurpError> { + let mut req = tonic::Request::new(request); + req.metadata_mut().inject_bypassed(); + req.metadata_mut().inject_current(); + self.server.record(req).await.map_err(Into::into) + } + + async fn read_index( + &self, + _timeout: Duration, + ) -> Result, CurpError> { + let mut req = tonic::Request::new(ReadIndexRequest {}); + req.metadata_mut().inject_bypassed(); + req.metadata_mut().inject_current(); + self.server.read_index(req).await.map_err(Into::into) } /// Send `PublishRequest` @@ -716,18 +769,6 @@ where .map_err(Into::into) } - /// Send `WaitSyncedRequest` - async fn wait_synced( - &self, - request: WaitSyncedRequest, - _timeout: Duration, - ) -> Result, CurpError> { - let mut req = tonic::Request::new(request); - req.metadata_mut().inject_bypassed(); - req.metadata_mut().inject_current(); - self.server.wait_synced(req).await.map_err(Into::into) - } - /// Send `ShutdownRequest` async fn shutdown( &self, diff --git a/crates/curp/src/rpc/mod.rs b/crates/curp/src/rpc/mod.rs index e4d570dc1..c064c3bb0 100644 --- a/crates/curp/src/rpc/mod.rs +++ b/crates/curp/src/rpc/mod.rs @@ -2,6 +2,7 @@ use std::{collections::HashMap, sync::Arc}; use curp_external_api::{ cmd::{ConflictCheck, PbCodec, PbSerializeError}, + conflict::EntryId, InflightId, }; use prost::Message; @@ -22,6 +23,7 @@ pub use self::proto::{ curp_error::Err as CurpError, // easy for match curp_error::Redirect, fetch_read_state_response::{IdSet, ReadState}, + op_response::Op as ResponseOp, propose_conf_change_request::{ConfChange, ConfChangeType}, protocol_client, protocol_server::{Protocol, ProtocolServer}, @@ -34,6 +36,8 @@ pub use self::proto::{ Member, MoveLeaderRequest, MoveLeaderResponse, + OpResponse, + OptionalU64, ProposeConfChangeRequest, ProposeConfChangeResponse, ProposeId as PbProposeId, @@ -41,8 +45,13 @@ pub use self::proto::{ ProposeResponse, PublishRequest, PublishResponse, + ReadIndexRequest, + ReadIndexResponse, + RecordRequest, + RecordResponse, ShutdownRequest, ShutdownResponse, + SyncedResponse, WaitSyncedRequest, WaitSyncedResponse, }, @@ -100,6 +109,27 @@ impl From for PbProposeId { } } +impl From for OptionalU64 { + #[inline] + fn from(value: u64) -> Self { + Self { value } + } +} + +impl From for u64 { + #[inline] + fn from(value: OptionalU64) -> Self { + value.value + } +} + +impl From<&OptionalU64> for u64 { + #[inline] + fn from(value: &OptionalU64) -> Self { + value.value + } +} + impl FetchClusterResponse { /// Create a new `FetchClusterResponse` pub(crate) fn new( @@ -110,7 +140,7 @@ impl FetchClusterResponse { cluster_version: u64, ) -> Self { Self { - leader_id, + leader_id: leader_id.map(Into::into), term, cluster_id, members, @@ -138,11 +168,21 @@ impl FetchClusterResponse { impl ProposeRequest { /// Create a new `Propose` request #[inline] - pub fn new(propose_id: ProposeId, cmd: &C, cluster_version: u64) -> Self { + pub fn new( + propose_id: ProposeId, + cmd: &C, + cluster_version: u64, + term: u64, + slow_path: bool, + first_incomplete: u64, + ) -> Self { Self { propose_id: Some(propose_id.into()), command: cmd.encode(), cluster_version, + term, + slow_path, + first_incomplete, } } @@ -151,13 +191,14 @@ impl ProposeRequest { #[must_use] pub fn propose_id(&self) -> ProposeId { self.propose_id - .clone() .unwrap_or_else(|| unreachable!("propose id must be set in ProposeRequest")) .into() } /// Get command + /// /// # Errors + /// /// Return error if the command can't be decoded #[inline] pub fn cmd(&self) -> Result { @@ -167,7 +208,7 @@ impl ProposeRequest { impl ProposeResponse { /// Create an ok propose response - pub(crate) fn new_result(result: &Result) -> Self { + pub(crate) fn new_result(result: &Result, conflict: bool) -> Self { let result = match *result { Ok(ref er) => Some(CmdResult { result: Some(CmdResultInner::Ok(er.encode())), @@ -176,12 +217,16 @@ impl ProposeResponse { result: Some(CmdResultInner::Error(e.encode())), }), }; - Self { result } + Self { result, conflict } } /// Create an empty propose response + #[allow(unused)] pub(crate) fn new_empty() -> Self { - Self { result: None } + Self { + result: None, + conflict: false, + } } /// Deserialize result in response and take a map function @@ -200,119 +245,60 @@ impl ProposeResponse { } } -impl WaitSyncedRequest { - /// Create a `WaitSynced` request - pub(crate) fn new(id: ProposeId, cluster_version: u64) -> Self { - Self { - propose_id: Some(id.into()), - cluster_version, +impl RecordRequest { + /// Creates a new `RecordRequest` + pub(crate) fn new(propose_id: ProposeId, command: &C) -> Self { + RecordRequest { + propose_id: Some(propose_id.into()), + command: command.encode(), } } - /// Get the `propose_id` reference + /// Get the propose id pub(crate) fn propose_id(&self) -> ProposeId { self.propose_id - .clone() .unwrap_or_else(|| { unreachable!("propose id should be set in propose wait synced request") }) .into() } -} - -impl WaitSyncedResponse { - /// Create a success response - fn new_success(asr: &C::ASR, er: &C::ER) -> Self { - Self { - after_sync_result: Some(CmdResult { - result: Some(CmdResultInner::Ok(asr.encode())), - }), - exe_result: Some(CmdResult { - result: Some(CmdResultInner::Ok(er.encode())), - }), - } - } - - /// Create an error response which includes an execution error - fn new_er_error(er: &C::Error) -> Self { - Self { - after_sync_result: None, - exe_result: Some(CmdResult { - result: Some(CmdResultInner::Error(er.encode())), - }), - } - } - /// Create an error response which includes an `after_sync` error - fn new_asr_error(er: &C::ER, asr_err: &C::Error) -> Self { - Self { - after_sync_result: Some(CmdResult { - result: Some(CmdResultInner::Error(asr_err.encode())), - }), - exe_result: Some(CmdResult { - result: Some(CmdResultInner::Ok(er.encode())), - }), - } + /// Get command + pub(crate) fn cmd(&self) -> Result { + C::decode(&self.command) } +} - /// Create a new response from execution result and `after_sync` result - pub(crate) fn new_from_result( - er: Result, - asr: Option>, - ) -> Self { - match (er, asr) { - (Ok(ref er), Some(Err(ref asr_err))) => { - WaitSyncedResponse::new_asr_error::(er, asr_err) - } - (Ok(ref er), Some(Ok(ref asr))) => WaitSyncedResponse::new_success::(asr, er), - (Ok(ref _er), None) => unreachable!("can't get after sync result"), - (Err(ref err), _) => WaitSyncedResponse::new_er_error::(err), +impl SyncedResponse { + /// Create a new response from `after_sync` result + pub(crate) fn new_result(result: &Result) -> Self { + match *result { + Ok(ref asr) => SyncedResponse { + after_sync_result: Some(CmdResult { + result: Some(CmdResultInner::Ok(asr.encode())), + }), + }, + Err(ref e) => SyncedResponse { + after_sync_result: Some(CmdResult { + result: Some(CmdResultInner::Error(e.encode())), + }), + }, } } - /// Similar to `ProposeResponse::map_result` + /// Deserialize result in response and take a map function pub(crate) fn map_result(self, f: F) -> Result where - F: FnOnce(Result<(C::ASR, C::ER), C::Error>) -> R, + F: FnOnce(Option>) -> R, { - // according to the above methods, we can only get the following response union - // ER: Some(OK), ASR: Some(OK) <- WaitSyncedResponse::new_success - // ER: Some(Err), ASR: None <- WaitSyncedResponse::new_er_error - // ER: Some(OK), ASR: Some(Err) <- WaitSyncedResponse::new_asr_error - let res = match (self.exe_result, self.after_sync_result) { - ( - Some(CmdResult { - result: Some(CmdResultInner::Ok(ref er)), - }), - Some(CmdResult { - result: Some(CmdResultInner::Ok(ref asr)), - }), - ) => { - let er = ::ER::decode(er)?; - let asr = ::ASR::decode(asr)?; - Ok((asr, er)) - } - ( - Some(CmdResult { - result: Some(CmdResultInner::Error(ref buf)), - }), - None, - ) - | ( - Some(CmdResult { - result: Some(CmdResultInner::Ok(_)), - }), - Some(CmdResult { - result: Some(CmdResultInner::Error(ref buf)), - }), - ) => { - let er = ::Error::decode(buf.as_slice())?; - Err(er) - } - _ => unreachable!("got unexpected WaitSyncedResponse"), + let Some(res) = self.after_sync_result.and_then(|res| res.result) else { + return Ok(f(None)); }; - - Ok(f(res)) + let res = match res { + CmdResultInner::Ok(ref buf) => Ok(::ASR::decode(buf)?), + CmdResultInner::Error(ref buf) => Err(::Error::decode(buf)?), + }; + Ok(f(Some(res))) } } @@ -553,7 +539,6 @@ impl ProposeConfChangeRequest { /// Get id of the request pub(crate) fn propose_id(&self) -> ProposeId { self.propose_id - .clone() .unwrap_or_else(|| { unreachable!("propose id should be set in propose conf change request") }) @@ -573,7 +558,6 @@ impl ShutdownRequest { /// Get id of the request pub(crate) fn propose_id(&self) -> ProposeId { self.propose_id - .clone() .unwrap_or_else(|| { unreachable!("propose id should be set in propose conf change request") }) @@ -610,7 +594,6 @@ impl PublishRequest { /// Get id of the request pub(crate) fn propose_id(&self) -> ProposeId { self.propose_id - .clone() .unwrap_or_else(|| { unreachable!("propose id should be set in propose conf change request") }) @@ -619,16 +602,14 @@ impl PublishRequest { } /// NOTICE: -/// Please check test case `test_unary_fast_round_return_early_err` `test_unary_propose_return_early_err` -/// `test_retry_propose_return_no_retry_error` `test_retry_propose_return_retry_error` if you added some -/// new [`CurpError`] +/// +/// Please check test case `test_unary_fast_round_return_early_err` +/// `test_unary_propose_return_early_err` +/// `test_retry_propose_return_no_retry_error` +/// `test_retry_propose_return_retry_error` if you added some new [`CurpError`] impl CurpError { - /// `KeyConflict` error - pub(crate) fn key_conflict() -> Self { - Self::KeyConflict(()) - } - /// `Duplicated` error + #[allow(unused)] pub(crate) fn duplicated() -> Self { Self::Duplicated(()) } @@ -671,7 +652,10 @@ impl CurpError { /// `Redirect` error pub(crate) fn redirect(leader_id: Option, term: u64) -> Self { - Self::Redirect(Redirect { leader_id, term }) + Self::Redirect(Redirect { + leader_id: leader_id.map(Into::into), + term, + }) } /// `Internal` error @@ -695,6 +679,7 @@ impl CurpError { } /// Whether to abort slow round early + #[allow(unused)] pub(crate) fn should_abort_slow_round(&self) -> bool { matches!( *self, @@ -720,7 +705,8 @@ impl CurpError { | CurpError::LearnerNotCatchUp(()) | CurpError::ExpiredClientId(()) | CurpError::Redirect(_) - | CurpError::WrongClusterVersion(()) => CurpErrorPriority::High, + | CurpError::WrongClusterVersion(()) + | CurpError::Zombie(()) => CurpErrorPriority::High, CurpError::RpcTransport(()) | CurpError::Internal(_) | CurpError::KeyConflict(()) @@ -823,6 +809,10 @@ impl From for tonic::Status { tonic::Code::FailedPrecondition, "Leader transfer error: A leader transfer error occurred.", ), + CurpError::Zombie(()) => ( + tonic::Code::FailedPrecondition, + "Zombie leader error: The leader is a zombie with outdated term.", + ), }; let details = CurpErrorWrapper { err: Some(err) }.encode_to_vec(); @@ -834,32 +824,19 @@ impl From for tonic::Status { // User defined types /// Entry of speculative pool -#[derive(Debug, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq))] -pub(crate) struct PoolEntry { +#[derive(Debug, Serialize, Deserialize)] +pub struct PoolEntry { /// Propose id pub(crate) id: ProposeId, /// Inner entry - pub(crate) inner: PoolEntryInner, -} - -/// Inner entry of speculative pool -#[derive(Debug, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq))] -pub(crate) enum PoolEntryInner { - /// Command entry - Command(Arc), - /// ConfChange entry - ConfChange(Vec), + pub(crate) cmd: Arc, } impl PoolEntry { /// Create a new pool entry - pub(crate) fn new(id: ProposeId, inner: impl Into>) -> Self { - Self { - id, - inner: inner.into(), - } + #[inline] + pub fn new(id: ProposeId, inner: Arc) -> Self { + Self { id, cmd: inner } } } @@ -867,30 +844,79 @@ impl ConflictCheck for PoolEntry where C: ConflictCheck, { + #[inline] fn is_conflict(&self, other: &Self) -> bool { - let PoolEntryInner::Command(ref cmd1) = self.inner else { - return true; - }; - let PoolEntryInner::Command(ref cmd2) = other.inner else { - return true; - }; - cmd1.is_conflict(cmd2) + self.cmd.is_conflict(&other.cmd) } } -impl From> for PoolEntryInner { - fn from(value: Arc) -> Self { - Self::Command(value) +impl Clone for PoolEntry { + #[inline] + fn clone(&self) -> Self { + Self { + id: self.id, + cmd: Arc::clone(&self.cmd), + } } } -impl From> for PoolEntryInner { - fn from(value: Vec) -> Self { - Self::ConfChange(value) +impl std::ops::Deref for PoolEntry { + type Target = C; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.cmd + } +} + +impl AsRef for PoolEntry { + #[inline] + fn as_ref(&self) -> &C { + self.cmd.as_ref() + } +} + +impl std::hash::Hash for PoolEntry { + #[inline] + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} + +impl PartialEq for PoolEntry { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.id.eq(&other.id) + } +} + +impl Eq for PoolEntry {} + +impl PartialOrd for PoolEntry { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.id.cmp(&other.id)) + } +} + +impl Ord for PoolEntry { + #[inline] + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.id.cmp(&other.id) + } +} + +impl EntryId for PoolEntry { + type Id = ProposeId; + + #[inline] + fn id(&self) -> Self::Id { + self.id } } /// Command Id wrapper, which is used to identify a command +/// /// The underlying data is a tuple of (`client_id`, `seq_num`) #[derive( Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Ord, PartialOrd, Default, diff --git a/crates/curp/src/server/cmd_board.rs b/crates/curp/src/server/cmd_board.rs index c35c64bef..64169323a 100644 --- a/crates/curp/src/server/cmd_board.rs +++ b/crates/curp/src/server/cmd_board.rs @@ -1,3 +1,5 @@ +#![allow(unused)] // TODO remove + use std::{collections::HashMap, sync::Arc}; use event_listener::{Event, EventListener}; @@ -5,7 +7,7 @@ use indexmap::{IndexMap, IndexSet}; use parking_lot::RwLock; use utils::parking_lot_lock::RwLockMap; -use crate::{cmd::Command, rpc::ProposeId}; +use crate::{cmd::Command, rpc::ProposeId, tracker::Tracker}; /// Ref to the cmd board pub(super) type CmdBoardRef = Arc>>; @@ -21,10 +23,10 @@ pub(super) struct CommandBoard { shutdown_notifier: Event, /// Store all notifiers for conf change results conf_notifier: HashMap, + /// The result trackers track all cmd, this is used for dedup + pub(super) trackers: HashMap, /// Store all conf change propose ids pub(super) conf_buffer: IndexSet, - /// The cmd has been received before, this is used for dedup - pub(super) sync: IndexSet, /// Store all execution results pub(super) er_buffer: IndexMap>, /// Store all after sync results @@ -38,7 +40,7 @@ impl CommandBoard { er_notifiers: HashMap::new(), asr_notifiers: HashMap::new(), shutdown_notifier: Event::new(), - sync: IndexSet::new(), + trackers: HashMap::new(), er_buffer: IndexMap::new(), asr_buffer: IndexMap::new(), conf_notifier: HashMap::new(), @@ -46,6 +48,16 @@ impl CommandBoard { } } + /// Get the tracker for a client id + pub(super) fn tracker(&mut self, client_id: u64) -> &mut Tracker { + self.trackers.entry(client_id).or_default() + } + + /// Remove client result tracker from trackers if it is expired + pub(super) fn client_expired(&mut self, client_id: u64) { + let _ig = self.trackers.remove(&client_id); + } + /// Release notifiers pub(super) fn release_notifiers(&mut self) { self.er_notifiers.drain().for_each(|(_, event)| { @@ -56,10 +68,11 @@ impl CommandBoard { }); } - /// Clear + /// Clear, called when leader retires pub(super) fn clear(&mut self) { self.er_buffer.clear(); self.asr_buffer.clear(); + self.trackers.clear(); self.release_notifiers(); } @@ -181,14 +194,12 @@ impl CommandBoard { pub(super) async fn wait_for_er_asr( cb: &CmdBoardRef, id: ProposeId, - ) -> (Result, Option>) { + ) -> (Result, Result) { loop { { let cb_r = cb.read(); - match (cb_r.er_buffer.get(&id), cb_r.asr_buffer.get(&id)) { - (Some(er), None) if er.is_err() => return (er.clone(), None), - (Some(er), Some(asr)) => return (er.clone(), Some(asr.clone())), - _ => {} + if let (Some(er), Some(asr)) = (cb_r.er_buffer.get(&id), cb_r.asr_buffer.get(&id)) { + return (er.clone(), asr.clone()); } } let listener = cb.write().asr_listener(id); diff --git a/crates/curp/src/server/cmd_worker/conflict_checked_mpmc.rs b/crates/curp/src/server/cmd_worker/conflict_checked_mpmc.rs deleted file mode 100644 index 4946dc479..000000000 --- a/crates/curp/src/server/cmd_worker/conflict_checked_mpmc.rs +++ /dev/null @@ -1,596 +0,0 @@ -#![allow( - clippy::wildcard_enum_match_arm, - clippy::match_wildcard_for_single_variants -)] // wildcard actually is more clear in this module -#![allow(clippy::arithmetic_side_effects)] // u64 is large enough - -use std::{ - collections::{HashMap, HashSet}, - sync::Arc, -}; - -use tokio::sync::oneshot; -use tracing::{debug, error}; -use utils::task_manager::{tasks::TaskName, Listener, State, TaskManager}; - -use self::cart::Cart; -use super::{CEEvent, CEEventTx}; -use crate::{ - cmd::{Command, CommandExecutor}, - log_entry::{EntryData, LogEntry}, - rpc::ProposeId, - snapshot::{Snapshot, SnapshotMeta}, -}; - -/// Cart -mod cart { - /// Cart is a utility that acts as a temporary container. - /// It is usually filled by the provider and consumed by the customer. - /// This is useful when we are sure that the provider will fill the cart and the cart will be consumed by the customer - /// so that we don't need to check whether there is something in the `Option`. - #[derive(Debug)] - pub(super) struct Cart(Option); - - impl Cart { - /// New cart with object - pub(super) fn new(object: T) -> Self { - Self(Some(object)) - } - /// Take the object. Panic if its inner has already been taken. - pub(super) fn take(&mut self) -> T { - #[allow(clippy::expect_used)] - self.0.take().expect("the cart is empty") - } - /// Check whether the object is taken - pub(super) fn is_taken(&self) -> bool { - self.0.is_none() - } - } -} - -/// CE task -pub(in crate::server) struct Task { - /// Corresponding vertex id - vid: u64, - /// Task type - inner: Cart>, -} - -/// Task Type -pub(super) enum TaskType { - /// Execute a cmd - SpecExe(Arc>, Option), - /// After sync a cmd - AS(Arc>, Option), - /// Reset the CE - Reset(Option, oneshot::Sender<()>), - /// Snapshot - Snapshot(SnapshotMeta, oneshot::Sender), -} - -impl Task { - /// Get inner task - pub(super) fn take(&mut self) -> TaskType { - self.inner.take() - } -} - -/// Vertex -#[derive(Debug)] -struct Vertex { - /// Successor cmds that arrive later with keys that conflict this cmd - successors: HashSet, - /// Number of predecessor cmds that arrive earlier with keys that conflict this cmd - predecessor_cnt: u64, - /// Vertex inner - inner: VertexInner, -} - -impl Vertex { - /// Whether two vertex conflict each other - fn is_conflict(&self, other: &Vertex) -> bool { - #[allow(clippy::pattern_type_mismatch)] - // it seems it's impossible to get away with this lint - match (&self.inner, &other.inner) { - ( - VertexInner::Entry { entry: entry1, .. }, - VertexInner::Entry { entry: entry2, .. }, - ) => { - let EntryData::Command(ref cmd1) = entry1.entry_data else { - return true; - }; - let EntryData::Command(ref cmd2) = entry2.entry_data else { - return true; - }; - cmd1.is_conflict(cmd2) - } - _ => true, - } - } -} - -/// Vertex inner -#[derive(Debug)] -enum VertexInner { - /// A entry vertex - Entry { - /// Entry - entry: Arc>, - /// Execution state - exe_st: ExeState, - /// After sync state - as_st: AsState, - }, - /// A reset vertex - Reset { - /// The snapshot and finish notifier - inner: Cart<(Box>, oneshot::Sender<()>)>, // use `Box` to avoid enum members with large size - /// Reset state - st: OnceState, - }, - /// A snapshot vertex - Snapshot { - /// The sender - inner: Cart<(SnapshotMeta, oneshot::Sender)>, - /// Snapshot state - st: OnceState, - }, -} - -/// Execute state of a cmd -#[derive(Debug, Clone, Copy)] -enum ExeState { - /// Is ready to execute - ExecuteReady, - /// Executing - Executing, - /// Has been executed, and the result - Executed(bool), -} - -/// After sync state of a cmd -#[derive(Debug, Clone)] -enum AsState { - /// Not Synced yet - NotSynced(Option), - /// Is ready to do after sync - AfterSyncReady(Option), - /// Is doing after syncing - AfterSyncing, - /// Has been after synced - AfterSynced, -} - -impl AsState { - /// set the prepare result into the `AsState` - #[inline] - fn set_prepare_result(&mut self, res: C::PR) { - match *self { - Self::NotSynced(ref mut pre_res) | Self::AfterSyncReady(ref mut pre_res) => { - *pre_res = Some(res); - } - Self::AfterSyncing | Self::AfterSynced => { - unreachable!("Pre-execute result cannot be set in the {:?} stage", *self) - } - } - } -} - -/// State of a vertex that only has one task -#[derive(Debug, PartialEq, Eq)] -enum OnceState { - /// Reset ready - Ready, - /// Resetting - Doing, - /// Completed - Completed, -} - -/// The filter will block any msg if its predecessors(msgs that arrive earlier and conflict with it) haven't finished process -/// Internally it maintains a dependency graph of conflicting cmds - -struct Filter { - /// Index from `ProposeId` to `vertex` - cmd_vid: HashMap, - /// Conflict graph - vs: HashMap>, - /// Next vertex id - next_id: u64, - /// Send task to users - filter_tx: flume::Sender>, - /// Command Executor - cmd_executor: Arc, -} - -impl> Filter { - /// Create a new filter that checks conflict in between msgs - fn new(filter_tx: flume::Sender>, ce: Arc) -> Self { - Self { - cmd_vid: HashMap::new(), - vs: HashMap::new(), - next_id: 0, - filter_tx, - cmd_executor: ce, - } - } - - /// Next vertex id - fn next_vertex_id(&mut self) -> u64 { - let new_vid = self.next_id; - self.next_id = self.next_id.wrapping_add(1); - new_vid - } - - /// Insert a new vertex to inner graph - fn insert_new_vertex(&mut self, new_vid: u64, mut new_v: Vertex) { - for v in self.vs.values_mut() { - if v.is_conflict(&new_v) { - assert!(v.successors.insert(new_vid), "cannot insert a vertex twice"); - new_v.predecessor_cnt += 1; - } - } - assert!( - self.vs.insert(new_vid, new_v).is_none(), - "cannot insert a vertex twice" - ); - } - - /// Progress a vertex - fn progress(&mut self, vid: u64, succeeded: bool) { - let v = self.get_vertex_mut(vid); - match v.inner { - VertexInner::Entry { - ref mut exe_st, - ref mut as_st, - .. - } => { - if matches!(*exe_st, ExeState::Executing) - && !matches!(*as_st, AsState::AfterSyncing) - { - *exe_st = ExeState::Executed(succeeded); - } else if matches!(*as_st, AsState::AfterSyncing) { - *as_st = AsState::AfterSynced; - } else { - unreachable!("cmd is neither being executed nor being after synced, exe_st: {exe_st:?}, as_st: {as_st:?}") - } - } - VertexInner::Reset { - ref inner, - ref mut st, - } => { - if *st == OnceState::Doing { - debug_assert!(inner.is_taken(), "snapshot and tx is not taken by the user"); - *st = OnceState::Completed; - } else { - unreachable!("reset is not ongoing when it is marked done, reset state: {st:?}") - } - } - VertexInner::Snapshot { - ref inner, - ref mut st, - } => { - if *st == OnceState::Doing { - debug_assert!( - inner.is_taken(), - "snapshot meta and tx is not taken by the user" - ); - *st = OnceState::Completed; - } else { - unreachable!( - "snapshot is not ongoing when it is marked done, reset state: {st:?}" - ) - } - } - } - self.update_graph(vid); - } - - /// Update a graph after a vertex has been updated - fn update_graph(&mut self, vid: u64) { - let vertex_finished = self.update_vertex(vid); - if vertex_finished { - #[allow(clippy::expect_used)] - let v = self - .vs - .remove(&vid) - .expect("no such vertex in conflict graph"); - if let VertexInner::Entry { ref entry, .. } = v.inner { - assert!( - self.cmd_vid.remove(&entry.propose_id).is_some(), - "no such cmd" - ); - } - self.update_successors(&v); - } - } - - /// Update a vertex's successors - fn update_successors(&mut self, v: &Vertex) { - for successor_id in v.successors.iter().copied() { - let successor = self.get_vertex_mut(successor_id); - successor.predecessor_cnt -= 1; - assert!( - !self.update_vertex(successor_id), - "successor can't have finished before predecessor" - ); - } - } - - /// Update the vertex, see if it can progress - /// Return true if it can be removed - #[allow(clippy::expect_used, clippy::too_many_lines)] // TODO: split this function - fn update_vertex(&mut self, vid: u64) -> bool { - let v = self - .vs - .get_mut(&vid) - .expect("no such vertex in conflict graph"); - - if v.predecessor_cnt != 0 { - return false; - } - match v.inner { - VertexInner::Entry { - ref entry, - ref mut exe_st, - ref mut as_st, - } => match (*exe_st, as_st.clone()) { - ( - ExeState::ExecuteReady, - AsState::NotSynced(prepare) | AsState::AfterSyncReady(prepare), - ) => { - assert!(prepare.is_none(), "The prepare result of a given cmd can only be calculated when exe_state change from ExecuteReady to Executing"); - let prepare_err = match entry.entry_data { - EntryData::Command(ref cmd) => { - match self.cmd_executor.prepare(cmd.as_ref()) { - Ok(pre_res) => { - as_st.set_prepare_result(pre_res); - None - } - Err(err) => { - self.cmd_executor.trigger(entry.inflight_id(), entry.index); - Some(err) - } - } - } - EntryData::ConfChange(_) - | EntryData::Shutdown - | EntryData::Empty - | EntryData::SetNodeState(_, _, _) => None, - }; - *exe_st = ExeState::Executing; - let task = Task { - vid, - inner: Cart::new(TaskType::SpecExe(Arc::clone(entry), prepare_err)), - }; - if let Err(e) = self.filter_tx.send(task) { - error!("failed to send task through filter, {e}"); - } - false - } - (ExeState::Executed(true), AsState::AfterSyncReady(prepare)) => { - *as_st = AsState::AfterSyncing; - let task = Task { - vid, - inner: Cart::new(TaskType::AS(Arc::clone(entry), prepare)), - }; - if let Err(e) = self.filter_tx.send(task) { - error!("failed to send task through filter, {e}"); - } - false - } - (ExeState::Executed(false), AsState::AfterSyncReady(_)) - | (ExeState::Executed(_), AsState::AfterSynced) => true, - (ExeState::Executing | ExeState::Executed(_), AsState::NotSynced(_)) - | (ExeState::Executing, AsState::AfterSyncReady(_) | AsState::AfterSyncing) - | (ExeState::Executed(true), AsState::AfterSyncing) => false, - (exe_st, as_st) => { - unreachable!("no such exe and as state can be reached: {exe_st:?}, {as_st:?}") - } - }, - VertexInner::Reset { - ref mut inner, - ref mut st, - } => match *st { - OnceState::Ready => { - let (snapshot, tx) = inner.take(); - let task = Task { - vid, - inner: Cart::new(TaskType::Reset(*snapshot, tx)), - }; - *st = OnceState::Doing; - if let Err(e) = self.filter_tx.send(task) { - error!("failed to send task through filter, {e}"); - } - false - } - OnceState::Doing => false, - OnceState::Completed => true, - }, - VertexInner::Snapshot { - ref mut inner, - ref mut st, - } => match *st { - OnceState::Ready => { - let (meta, tx) = inner.take(); - let task = Task { - vid, - inner: Cart::new(TaskType::Snapshot(meta, tx)), - }; - *st = OnceState::Doing; - if let Err(e) = self.filter_tx.send(task) { - error!("failed to send task through filter, {e}"); - } - false - } - OnceState::Doing => false, - OnceState::Completed => true, - }, - } - } - - /// Get vertex from id - fn get_vertex_mut(&mut self, vid: u64) -> &mut Vertex { - #[allow(clippy::expect_used)] - self.vs - .get_mut(&vid) - .expect("no such vertex in conflict graph") - } - - /// Handle event - fn handle_event(&mut self, event: CEEvent) { - debug!("new ce event: {event:?}"); - let vid = match event { - CEEvent::SpecExeReady(entry) => { - let new_vid = self.next_vertex_id(); - assert!( - self.cmd_vid.insert(entry.propose_id, new_vid).is_none(), - "cannot insert a cmd twice" - ); - let new_v = Vertex { - successors: HashSet::new(), - predecessor_cnt: 0, - inner: VertexInner::Entry { - exe_st: ExeState::ExecuteReady, - as_st: AsState::NotSynced(None), - entry, - }, - }; - self.insert_new_vertex(new_vid, new_v); - new_vid - } - CEEvent::ASReady(entry) => { - if let Some(vid) = self.cmd_vid.get(&entry.propose_id).copied() { - let v = self.get_vertex_mut(vid); - match v.inner { - VertexInner::Entry { ref mut as_st, .. } => { - let AsState::NotSynced(ref mut prepare) = *as_st else { - unreachable!("after sync state should be AsState::NotSynced but found {as_st:?}"); - }; - *as_st = AsState::AfterSyncReady(prepare.take()); - } - _ => unreachable!("impossible vertex type"), - } - vid - } else { - let new_vid = self.next_vertex_id(); - assert!( - self.cmd_vid.insert(entry.propose_id, new_vid).is_none(), - "cannot insert a cmd twice" - ); - let new_v = Vertex { - successors: HashSet::new(), - predecessor_cnt: 0, - inner: VertexInner::Entry { - exe_st: ExeState::ExecuteReady, - as_st: AsState::AfterSyncReady(None), - entry, - }, - }; - self.insert_new_vertex(new_vid, new_v); - new_vid - } - } - CEEvent::Reset(snapshot, finish_tx) => { - // since a reset is needed, all other vertices doesn't matter anymore, so delete them all - self.cmd_vid.clear(); - self.vs.clear(); - - let new_vid = self.next_vertex_id(); - let new_v = Vertex { - successors: HashSet::new(), - predecessor_cnt: 0, - inner: VertexInner::Reset { - inner: Cart::new((Box::new(snapshot), finish_tx)), - st: OnceState::Ready, - }, - }; - self.insert_new_vertex(new_vid, new_v); - new_vid - } - CEEvent::Snapshot(meta, tx) => { - let new_vid = self.next_vertex_id(); - let new_v = Vertex { - successors: HashSet::new(), - predecessor_cnt: 0, - inner: VertexInner::Snapshot { - inner: Cart::new((meta, tx)), - st: OnceState::Ready, - }, - }; - self.insert_new_vertex(new_vid, new_v); - new_vid - } - }; - self.update_graph(vid); - } -} - -/// Create conflict checked channel. The channel guarantees there will be no conflicted msgs received by multiple receivers at the same time. -/// The user should use the `CEEventTx` to send events for command executor. -/// The events will be automatically processed and corresponding ce tasks will be generated and sent through the task receiver. -/// After the task is finished, the user should notify the channel by the done notifier. -// Message flow: -// send_tx -> filter_rx -> filter -> filter_tx -> recv_rx -> done_tx -> done_rx -#[allow(clippy::type_complexity)] // it's clear -pub(in crate::server) fn channel>( - ce: Arc, - task_manager: Arc, -) -> ( - CEEventTx, - flume::Receiver>, - flume::Sender<(Task, bool)>, -) { - // recv from user, insert it into filter - let (send_tx, filter_rx) = flume::unbounded(); - // recv from filter, pass the msg to user - let (filter_tx, recv_rx) = flume::unbounded(); - // recv from user to mark a msg done - let (done_tx, done_rx) = flume::unbounded::<(Task, bool)>(); - task_manager.spawn(TaskName::ConflictCheckedMpmc, |n| { - conflict_checked_mpmc_task(filter_tx, filter_rx, ce, done_rx, n) - }); - let ce_event_tx = CEEventTx(send_tx, task_manager); - (ce_event_tx, recv_rx, done_tx) -} - -/// Conflict checked mpmc task -async fn conflict_checked_mpmc_task>( - filter_tx: flume::Sender>, - filter_rx: flume::Receiver>, - ce: Arc, - done_rx: flume::Receiver<(Task, bool)>, - shutdown_listener: Listener, -) { - let mut filter = Filter::new(filter_tx, ce); - let mut is_shutdown_state = false; - // tokio internal triggers - #[allow(clippy::arithmetic_side_effects, clippy::pattern_type_mismatch)] - loop { - tokio::select! { - biased; // cleanup filter first so that the buffer in filter can be kept as small as possible - state = shutdown_listener.wait_state(), if !is_shutdown_state => { - match state { - State::Running => unreachable!("wait state should not return Run"), - State::Shutdown => return, - State::ClusterShutdown => is_shutdown_state = true, - } - }, - Ok((task, succeeded)) = done_rx.recv_async() => { - filter.progress(task.vid, succeeded); - }, - Ok(event) = filter_rx.recv_async() => { - filter.handle_event(event); - }, - else => { - error!("mpmc channel stopped unexpectedly"); - return; - } - } - - if is_shutdown_state && filter.vs.is_empty() { - shutdown_listener.mark_mpmc_channel_shutdown(); - return; - } - } -} diff --git a/crates/curp/src/server/cmd_worker/mod.rs b/crates/curp/src/server/cmd_worker/mod.rs index f97be228c..d70cc20e7 100644 --- a/crates/curp/src/server/cmd_worker/mod.rs +++ b/crates/curp/src/server/cmd_worker/mod.rs @@ -1,251 +1,254 @@ //! `exe` stands for execution //! `as` stands for after sync -use std::{fmt::Debug, iter, sync::Arc}; +use std::sync::Arc; -use async_trait::async_trait; -use clippy_utilities::NumericCast; -#[cfg(test)] -use mockall::automock; +use curp_external_api::cmd::{AfterSyncCmd, AfterSyncOk}; use tokio::sync::oneshot; use tracing::{debug, error, info, warn}; -use utils::task_manager::{tasks::TaskName, Listener, TaskManager}; -use self::conflict_checked_mpmc::Task; -use super::raw_curp::RawCurp; +use super::{curp_node::AfterSyncEntry, raw_curp::RawCurp}; use crate::{ cmd::{Command, CommandExecutor}, log_entry::{EntryData, LogEntry}, + response::ResponseSender, role_change::RoleChange, - rpc::{ConfChangeType, PoolEntry}, - server::cmd_worker::conflict_checked_mpmc::TaskType, + rpc::{ConfChangeType, PoolEntry, ProposeId, ProposeResponse, SyncedResponse}, snapshot::{Snapshot, SnapshotMeta}, }; -/// The special conflict checked mpmc -pub(super) mod conflict_checked_mpmc; - -/// Event for command executor -pub(super) enum CEEvent { - /// The cmd is ready for speculative execution - SpecExeReady(Arc>), - /// The cmd is ready for after sync - ASReady(Arc>), - /// Reset the command executor, send(()) when finishes - Reset(Option, oneshot::Sender<()>), - /// Take a snapshot - Snapshot(SnapshotMeta, oneshot::Sender), -} - -impl Debug for CEEvent { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match *self { - Self::SpecExeReady(ref entry) => f.debug_tuple("SpecExeReady").field(entry).finish(), - Self::ASReady(ref entry) => f.debug_tuple("ASReady").field(entry).finish(), - Self::Reset(ref ss, _) => { - if ss.is_none() { - write!(f, "Reset(None)") - } else { - write!(f, "Reset(Some(_))") - } - } - Self::Snapshot(meta, _) => f.debug_tuple("Snapshot").field(&meta).finish(), - } +/// Removes an entry from sp and ucp +fn remove_from_sp_ucp(curp: &RawCurp, entries: I) +where + C: Command, + RC: RoleChange, + E: AsRef>, + I: IntoIterator, +{ + let (mut sp, mut ucp) = (curp.spec_pool().lock(), curp.uncommitted_pool().lock()); + for entry in entries { + let entry = entry.as_ref(); + if let EntryData::Command(ref c) = entry.entry_data { + let pool_entry = PoolEntry::new(entry.propose_id, Arc::clone(c)); + sp.remove(&pool_entry); + ucp.remove(&pool_entry); + }; } } -/// Worker that execute commands -async fn cmd_worker, RC: RoleChange>( - dispatch_rx: impl TaskRxApi, - done_tx: flume::Sender<(Task, bool)>, - curp: Arc>, - ce: Arc, - shutdown_listener: Listener, -) { - #[allow(clippy::arithmetic_side_effects, clippy::ignored_unit_patterns)] - // introduced by tokio select - loop { - tokio::select! { - task = dispatch_rx.recv() => { - let Ok(task) = task else { - return; - }; - handle_task(task, &done_tx, ce.as_ref(), curp.as_ref()).await; - } - _ = shutdown_listener.wait() => break, - } - } - while let Ok(task) = dispatch_rx.try_recv() { - handle_task(task, &done_tx, ce.as_ref(), curp.as_ref()).await; - } - debug!("cmd worker exits"); -} +/// ER and ASR +type ErAsr = (::ER, Option<::ASR>); -/// Handle task -async fn handle_task, RC: RoleChange>( - mut task: Task, - done_tx: &flume::Sender<(Task, bool)>, +/// Cmd worker execute handler +pub(super) fn execute, RC: RoleChange>( + entry: &LogEntry, ce: &CE, curp: &RawCurp, -) { - let succeeded = match task.take() { - TaskType::SpecExe(entry, pre_err) => worker_exe(entry, pre_err, ce, curp).await, - TaskType::AS(entry, prepare) => worker_as(entry, prepare, ce, curp).await, - TaskType::Reset(snapshot, finish_tx) => worker_reset(snapshot, finish_tx, ce, curp).await, - TaskType::Snapshot(meta, tx) => worker_snapshot(meta, tx, ce, curp).await, +) -> Result, ::Error> { + let cb = curp.cmd_board(); + let id = curp.id(); + let EntryData::Command(ref cmd) = entry.entry_data else { + unreachable!("should not speculative execute {:?}", entry.entry_data); }; - if let Err(e) = done_tx.send((task, succeeded)) { - if !curp.is_shutdown() { - error!("can't mark a task done, the channel could be closed, {e}"); - } + if cmd.is_read_only() { + ce.execute_ro(cmd).map(|(er, asr)| (er, Some(asr))) + } else { + let er = ce.execute(cmd); + let mut cb_w = cb.write(); + cb_w.insert_er(entry.propose_id, er.clone()); + debug!( + "{id} cmd({}) is speculatively executed, exe status: {}", + entry.propose_id, + er.is_ok(), + ); + er.map(|e| (e, None)) } } -/// Cmd worker execute handler -async fn worker_exe, RC: RoleChange>( - entry: Arc>, - pre_err: Option, +/// After sync cmd entries +#[allow(clippy::pattern_type_mismatch)] // Can't be fixed +fn after_sync_cmds, RC: RoleChange>( + cmd_entries: &[AfterSyncEntry], ce: &CE, curp: &RawCurp, -) -> bool { - let (cb, sp, ucp) = (curp.cmd_board(), curp.spec_pool(), curp.uncommitted_pool()); - let id = curp.id(); - let success = match entry.entry_data { - EntryData::Command(ref cmd) => { - let er = if let Some(err_msg) = pre_err { - Err(err_msg) - } else { - ce.execute(cmd).await +) { + if cmd_entries.is_empty() { + return; + } + info!("after sync: {cmd_entries:?}"); + let resp_txs = cmd_entries + .iter() + .map(|(_, tx)| tx.as_ref().map(AsRef::as_ref)); + let highest_index = cmd_entries + .last() + .map_or_else(|| unreachable!(), |(entry, _)| entry.index); + let cmds: Vec<_> = cmd_entries + .iter() + .map(|(entry, tx)| { + let EntryData::Command(ref cmd) = entry.entry_data else { + unreachable!("only allows command entry"); }; - let er_ok = er.is_ok(); - cb.write().insert_er(entry.propose_id, er); - if !er_ok { - sp.lock() - .remove(PoolEntry::new(entry.propose_id, Arc::clone(cmd))); - ucp.lock() - .remove(PoolEntry::new(entry.propose_id, Arc::clone(cmd))); + AfterSyncCmd::new( + cmd.as_ref(), + // If the response sender is absent, it indicates that a new leader + // has been elected, and the entry has been recovered from the log + // or the speculative pool. In such cases, these entries needs to + // be re-executed. + tx.as_ref().map_or(true, |t| t.is_conflict()), + ) + }) + .collect(); + let propose_ids = cmd_entries.iter().map(|(e, _)| e.propose_id); + + let results = ce.after_sync(cmds, Some(highest_index)); + + send_results(curp, results.into_iter(), resp_txs, propose_ids); + + for (entry, _) in cmd_entries { + curp.trigger(&entry.propose_id); + ce.trigger(entry.inflight_id()); + } + remove_from_sp_ucp(curp, cmd_entries.iter().map(|(e, _)| e)); +} + +/// Send cmd results to clients +fn send_results<'a, C, RC, R, S, P>(curp: &RawCurp, results: R, txs: S, propose_ids: P) +where + C: Command, + RC: RoleChange, + R: Iterator, C::Error>>, + S: Iterator>, + P: Iterator, +{ + let cb = curp.cmd_board(); + let mut cb_w = cb.write(); + + for ((result, tx_opt), id) in results.zip(txs).zip(propose_ids) { + match result { + Ok(r) => { + let (asr, er_opt) = r.into_parts(); + let _ignore_er = tx_opt.as_ref().zip(er_opt.as_ref()).map(|(tx, er)| { + tx.send_propose(ProposeResponse::new_result::(&Ok(er.clone()), true)); + }); + let _ignore = er_opt.map(|er| cb_w.insert_er(id, Ok(er))); + let _ignore_asr = tx_opt + .as_ref() + .map(|tx| tx.send_synced(SyncedResponse::new_result::(&Ok(asr.clone())))); + cb_w.insert_asr(id, Ok(asr)); + } + Err(e) => { + let _ignore = tx_opt + .as_ref() + .map(|tx| tx.send_synced(SyncedResponse::new_result::(&Err(e.clone())))); + cb_w.insert_asr(id, Err(e.clone())); } - debug!( - "{id} cmd({}) is speculatively executed, exe status: {er_ok}", - entry.propose_id - ); - er_ok } - EntryData::ConfChange(_) - | EntryData::Shutdown - | EntryData::Empty - | EntryData::SetNodeState(_, _, _) => true, - }; - if !success { - ce.trigger(entry.inflight_id(), entry.index); } - success } -/// Cmd worker after sync handler -async fn worker_as, RC: RoleChange>( - entry: Arc>, - prepare: Option, +/// After sync entries other than cmd +async fn after_sync_others, RC: RoleChange>( + others: Vec>, ce: &CE, curp: &RawCurp, -) -> bool { - let (cb, sp, ucp) = (curp.cmd_board(), curp.spec_pool(), curp.uncommitted_pool()); +) { let id = curp.id(); - let success = match entry.entry_data { - EntryData::Command(ref cmd) => { - let Some(prepare) = prepare else { - unreachable!("prepare should always be Some(_) when entry is a command"); - }; - let asr = ce.after_sync(cmd.as_ref(), entry.index, prepare).await; - let asr_ok = asr.is_ok(); - cb.write().insert_asr(entry.propose_id, asr); - sp.lock() - .remove(PoolEntry::new(entry.propose_id, Arc::clone(cmd))); - ucp.lock() - .remove(PoolEntry::new(entry.propose_id, Arc::clone(cmd))); - debug!("{id} cmd({}) after sync is called", entry.propose_id); - asr_ok - } - EntryData::Shutdown => { - curp.task_manager().cluster_shutdown(); - if curp.is_leader() { - curp.task_manager().mark_leader_notified(); - } - if let Err(e) = ce.set_last_applied(entry.index) { - error!("failed to set last_applied, {e}"); - } - cb.write().notify_shutdown(); - true - } - EntryData::ConfChange(ref conf_change) => { - if let Err(e) = ce.set_last_applied(entry.index) { - error!("failed to set last_applied, {e}"); - return false; + let cb = curp.cmd_board(); + #[allow(clippy::pattern_type_mismatch)] // Can't be fixed + for (entry, resp_tx) in others { + match (&entry.entry_data, resp_tx) { + (EntryData::Shutdown, _) => { + curp.task_manager().cluster_shutdown(); + if curp.is_leader() { + curp.task_manager().mark_leader_notified(); + } + if let Err(e) = ce.set_last_applied(entry.index) { + error!("failed to set last_applied, {e}"); + } + cb.write().notify_shutdown(); } - let change = conf_change.first().unwrap_or_else(|| { - unreachable!("conf change should always have at least one change") - }); - let shutdown_self = - change.change_type() == ConfChangeType::Remove && change.node_id == id; - cb.write().insert_conf(entry.propose_id); - sp.lock() - .remove(PoolEntry::new(entry.propose_id, conf_change.clone())); - ucp.lock() - .remove(PoolEntry::new(entry.propose_id, conf_change.clone())); - if shutdown_self { - if let Some(maybe_new_leader) = curp.pick_new_leader() { - info!( - "the old leader {} will shutdown, try to move leadership to {}", - id, maybe_new_leader - ); - if curp - .handle_move_leader(maybe_new_leader) - .unwrap_or_default() - { - if let Err(e) = curp - .connects() - .get(&maybe_new_leader) - .unwrap_or_else(|| { - unreachable!("connect to {} should exist", maybe_new_leader) - }) - .try_become_leader_now(curp.cfg().wait_synced_timeout) - .await + (EntryData::ConfChange(ref conf_change), _) => { + if let Err(e) = ce.set_last_applied(entry.index) { + error!("failed to set last_applied, {e}"); + return; + } + let change = conf_change.first().unwrap_or_else(|| { + unreachable!("conf change should always have at least one change") + }); + let shutdown_self = + change.change_type() == ConfChangeType::Remove && change.node_id == id; + cb.write().insert_conf(entry.propose_id); + remove_from_sp_ucp(curp, Some(&entry)); + if shutdown_self { + if let Some(maybe_new_leader) = curp.pick_new_leader() { + info!( + "the old leader {} will shutdown, try to move leadership to {}", + id, maybe_new_leader + ); + if curp + .handle_move_leader(maybe_new_leader) + .unwrap_or_default() { - warn!( - "{} send try become leader now to {} failed: {:?}", - curp.id(), - maybe_new_leader, - e - ); - }; - } - } else { - info!( + if let Err(e) = curp + .connects() + .get(&maybe_new_leader) + .unwrap_or_else(|| { + unreachable!("connect to {} should exist", maybe_new_leader) + }) + .try_become_leader_now(curp.cfg().wait_synced_timeout) + .await + { + warn!( + "{} send try become leader now to {} failed: {:?}", + curp.id(), + maybe_new_leader, + e + ); + }; + } + } else { + info!( "the old leader {} will shutdown, but no other node can be the leader now", id ); + } + curp.task_manager().shutdown(false).await; } - curp.task_manager().shutdown(false).await; } - true - } - EntryData::SetNodeState(node_id, ref name, ref client_urls) => { - if let Err(e) = ce.set_last_applied(entry.index) { - error!("failed to set last_applied, {e}"); - return false; + (EntryData::SetNodeState(node_id, ref name, ref client_urls), _) => { + info!("setting node state: {node_id}, urls: {:?}", client_urls); + if let Err(e) = ce.set_last_applied(entry.index) { + error!("failed to set last_applied, {e}"); + return; + } + curp.cluster() + .set_node_state(*node_id, name.clone(), client_urls.clone()); } - curp.cluster() - .set_node_state(node_id, name.clone(), client_urls.clone()); - true + // The no-op command has been applied to state machine + (EntryData::Empty, _) => curp.set_no_op_applied(), + _ => unreachable!(), } - EntryData::Empty => true, - }; - ce.trigger(entry.inflight_id(), entry.index); - success + ce.trigger(entry.inflight_id()); + debug!("{id} cmd({}) after sync is called", entry.propose_id); + } +} + +/// Cmd worker after sync handler +pub(super) async fn after_sync, RC: RoleChange>( + entries: Vec>, + ce: &CE, + curp: &RawCurp, +) { + #[allow(clippy::pattern_type_mismatch)] // Can't be fixed + let (cmd_entries, others): (Vec<_>, Vec<_>) = entries + .into_iter() + .partition(|(entry, _)| matches!(entry.entry_data, EntryData::Command(_))); + after_sync_cmds(&cmd_entries, ce, curp); + after_sync_others(others, ce, curp).await; } /// Cmd worker reset handler -async fn worker_reset, RC: RoleChange>( +pub(super) async fn worker_reset, RC: RoleChange>( snapshot: Option, finish_tx: oneshot::Sender<()>, ce: &CE, @@ -281,7 +284,7 @@ async fn worker_reset, RC: RoleChange>( } /// Cmd worker snapshot handler -async fn worker_snapshot, RC: RoleChange>( +pub(super) async fn worker_snapshot, RC: RoleChange>( meta: SnapshotMeta, tx: oneshot::Sender, ce: &CE, @@ -307,574 +310,3 @@ async fn worker_snapshot, RC: RoleChange>( } } } - -/// Send event to background command executor workers -#[derive(Debug, Clone)] -pub(super) struct CEEventTx(flume::Sender>, Arc); - -/// Recv cmds that need to be executed -#[derive(Clone)] -struct TaskRx(flume::Receiver>); - -/// Send cmd to background execution worker -#[cfg_attr(test, automock)] -pub(crate) trait CEEventTxApi: Send + Sync + 'static { - /// Send cmd to background cmd worker for speculative execution - fn send_sp_exe(&self, entry: Arc>); - - /// Send after sync event to the background cmd worker so that after sync can be called - fn send_after_sync(&self, entry: Arc>); - - /// Send reset - fn send_reset(&self, snapshot: Option) -> oneshot::Receiver<()>; - - /// Send snapshot - fn send_snapshot(&self, meta: SnapshotMeta) -> oneshot::Receiver; -} - -impl CEEventTx { - /// Send ce event - fn send_event(&self, event: CEEvent) { - if let Err(e) = self.0.send(event) { - if self.1.is_shutdown() { - info!("send event after current node shutdown"); - return; - } - error!("failed to send cmd exe event to background cmd worker, {e}"); - } - } -} - -impl CEEventTxApi for CEEventTx { - fn send_sp_exe(&self, entry: Arc>) { - let event = CEEvent::SpecExeReady(Arc::clone(&entry)); - self.send_event(event); - } - - fn send_after_sync(&self, entry: Arc>) { - let event = CEEvent::ASReady(Arc::clone(&entry)); - self.send_event(event); - } - - fn send_reset(&self, snapshot: Option) -> oneshot::Receiver<()> { - let (tx, rx) = oneshot::channel(); - let event = CEEvent::Reset(snapshot, tx); - self.send_event(event); - rx - } - - fn send_snapshot(&self, meta: SnapshotMeta) -> oneshot::Receiver { - let (tx, rx) = oneshot::channel(); - let event = CEEvent::Snapshot(meta, tx); - self.send_event(event); - rx - } -} - -/// Cmd exe recv interface -#[cfg_attr(test, automock)] -#[async_trait] -trait TaskRxApi { - /// Recv execute msg and done notifier - async fn recv(&self) -> Result, flume::RecvError>; - /// Try recv execute msg and done notifier - fn try_recv(&self) -> Result, flume::TryRecvError>; -} - -#[async_trait] -impl TaskRxApi for TaskRx { - async fn recv(&self) -> Result, flume::RecvError> { - self.0.recv_async().await - } - - fn try_recv(&self) -> Result, flume::TryRecvError> { - self.0.try_recv() - } -} - -/// Run cmd execute workers. Each cmd execute worker will continually fetch task to perform from `task_rx`. -pub(super) fn start_cmd_workers, RC: RoleChange>( - cmd_executor: Arc, - curp: Arc>, - task_rx: flume::Receiver>, - done_tx: flume::Sender<(Task, bool)>, -) { - let n_workers: usize = curp.cfg().cmd_workers.numeric_cast(); - let task_manager = curp.task_manager(); - #[allow(clippy::shadow_unrelated)] // false positive - iter::repeat((task_rx, done_tx, curp, cmd_executor)) - .take(n_workers) - .for_each(|(task_rx, done_tx, curp, ce)| { - task_manager.spawn(TaskName::CmdWorker, |n| { - cmd_worker(TaskRx(task_rx), done_tx, curp, ce, n) - }); - }); -} - -#[cfg(test)] -mod tests { - use std::time::Duration; - - use curp_test_utils::{ - mock_role_change, sleep_millis, sleep_secs, - test_cmd::{TestCE, TestCommand}, - }; - use test_macros::abort_on_panic; - use tokio::{sync::mpsc, time::Instant}; - use tracing_test::traced_test; - use utils::config::EngineConfig; - - use super::*; - use crate::{log_entry::LogEntry, rpc::ProposeId}; - - // This should happen in fast path in most cases - #[traced_test] - #[tokio::test] - #[abort_on_panic] - async fn fast_path_normal() { - let (er_tx, mut er_rx) = mpsc::unbounded_channel(); - let (as_tx, mut as_rx) = mpsc::unbounded_channel(); - let ce = Arc::new(TestCE::new( - "S1".to_owned(), - er_tx, - as_tx, - EngineConfig::Memory, - )); - let task_manager = Arc::new(TaskManager::new()); - let (ce_event_tx, task_rx, done_tx) = - conflict_checked_mpmc::channel(Arc::clone(&ce), Arc::clone(&task_manager)); - start_cmd_workers( - Arc::clone(&ce), - Arc::new(RawCurp::new_test( - 3, - ce_event_tx.clone(), - mock_role_change(), - Arc::clone(&task_manager), - )), - task_rx, - done_tx, - ); - - let entry = Arc::new(LogEntry::new( - 1, - 1, - ProposeId(0, 0), - Arc::new(TestCommand::default()), - )); - - ce_event_tx.send_sp_exe(Arc::clone(&entry)); - assert_eq!(er_rx.recv().await.unwrap().1.values, Vec::::new()); - - ce_event_tx.send_after_sync(entry); - assert_eq!(as_rx.recv().await.unwrap().1, 1); - task_manager.shutdown(true).await; - } - - // When the execution takes more time than sync, `as` should be called after exe has finished - #[traced_test] - #[tokio::test] - #[abort_on_panic] - async fn fast_path_cond1() { - let (er_tx, _er_rx) = mpsc::unbounded_channel(); - let (as_tx, mut as_rx) = mpsc::unbounded_channel(); - let ce = Arc::new(TestCE::new( - "S1".to_owned(), - er_tx, - as_tx, - EngineConfig::Memory, - )); - let task_manager = Arc::new(TaskManager::new()); - let (ce_event_tx, task_rx, done_tx) = - conflict_checked_mpmc::channel(Arc::clone(&ce), Arc::clone(&task_manager)); - start_cmd_workers( - Arc::clone(&ce), - Arc::new(RawCurp::new_test( - 3, - ce_event_tx.clone(), - mock_role_change(), - Arc::clone(&task_manager), - )), - task_rx, - done_tx, - ); - - let begin = Instant::now(); - let entry = Arc::new(LogEntry::new( - 1, - 1, - ProposeId(0, 0), - Arc::new(TestCommand::default().set_exe_dur(Duration::from_secs(1))), - )); - - ce_event_tx.send_sp_exe(Arc::clone(&entry)); - - // at 500ms, sync has completed, call after sync, then needs_as will be updated - sleep_millis(500).await; - ce_event_tx.send_after_sync(entry); - - assert_eq!(as_rx.recv().await.unwrap().1, 1); - - assert!((Instant::now() - begin) >= Duration::from_secs(1)); - task_manager.shutdown(true).await; - } - - // When the execution takes more time than sync and fails, after sync should not be called - #[traced_test] - #[tokio::test] - #[abort_on_panic] - async fn fast_path_cond2() { - let (er_tx, mut er_rx) = mpsc::unbounded_channel(); - let (as_tx, mut as_rx) = mpsc::unbounded_channel(); - let ce = Arc::new(TestCE::new( - "S1".to_owned(), - er_tx, - as_tx, - EngineConfig::Memory, - )); - let task_manager = Arc::new(TaskManager::new()); - let (ce_event_tx, task_rx, done_tx) = - conflict_checked_mpmc::channel(Arc::clone(&ce), Arc::clone(&task_manager)); - start_cmd_workers( - Arc::clone(&ce), - Arc::new(RawCurp::new_test( - 3, - ce_event_tx.clone(), - mock_role_change(), - Arc::clone(&task_manager), - )), - task_rx, - done_tx, - ); - - let entry = Arc::new(LogEntry::new( - 1, - 1, - ProposeId(0, 0), - Arc::new( - TestCommand::default() - .set_exe_dur(Duration::from_secs(1)) - .set_exe_should_fail(), - ), - )); - - ce_event_tx.send_sp_exe(Arc::clone(&entry)); - - // at 500ms, sync has completed - sleep_millis(500).await; - ce_event_tx.send_after_sync(entry); - - // at 1500ms, as should not be called - sleep_secs(1).await; - assert!(er_rx.try_recv().is_err()); - assert!(as_rx.try_recv().is_err()); - task_manager.shutdown(true).await; - } - - // This should happen in slow path in most cases - #[traced_test] - #[tokio::test] - #[abort_on_panic] - async fn slow_path_normal() { - let (er_tx, mut er_rx) = mpsc::unbounded_channel(); - let (as_tx, mut as_rx) = mpsc::unbounded_channel(); - let ce = Arc::new(TestCE::new( - "S1".to_owned(), - er_tx, - as_tx, - EngineConfig::Memory, - )); - let task_manager = Arc::new(TaskManager::new()); - let (ce_event_tx, task_rx, done_tx) = - conflict_checked_mpmc::channel(Arc::clone(&ce), Arc::clone(&task_manager)); - start_cmd_workers( - Arc::clone(&ce), - Arc::new(RawCurp::new_test( - 3, - ce_event_tx.clone(), - mock_role_change(), - Arc::clone(&task_manager), - )), - task_rx, - done_tx, - ); - - let entry = Arc::new(LogEntry::new( - 1, - 1, - ProposeId(0, 0), - Arc::new(TestCommand::default()), - )); - - ce_event_tx.send_after_sync(entry); - - assert_eq!(er_rx.recv().await.unwrap().1.revisions, Vec::::new()); - assert_eq!(as_rx.recv().await.unwrap().1, 1); - task_manager.shutdown(true).await; - } - - // When exe fails - #[traced_test] - #[tokio::test] - #[abort_on_panic] - async fn slow_path_exe_fails() { - let (er_tx, mut er_rx) = mpsc::unbounded_channel(); - let (as_tx, mut as_rx) = mpsc::unbounded_channel(); - let ce = Arc::new(TestCE::new( - "S1".to_owned(), - er_tx, - as_tx, - EngineConfig::Memory, - )); - let task_manager = Arc::new(TaskManager::new()); - let (ce_event_tx, task_rx, done_tx) = - conflict_checked_mpmc::channel(Arc::clone(&ce), Arc::clone(&task_manager)); - start_cmd_workers( - Arc::clone(&ce), - Arc::new(RawCurp::new_test( - 3, - ce_event_tx.clone(), - mock_role_change(), - Arc::clone(&task_manager), - )), - task_rx, - done_tx, - ); - - let entry = Arc::new(LogEntry::new( - 1, - 1, - ProposeId(0, 0), - Arc::new(TestCommand::default().set_exe_should_fail()), - )); - - ce_event_tx.send_after_sync(entry); - - sleep_millis(100).await; - let er = er_rx.try_recv(); - assert!(er.is_err(), "The execute command result is {er:?}"); - let asr = as_rx.try_recv(); - assert!(asr.is_err(), "The after sync result is {asr:?}"); - task_manager.shutdown(true).await; - } - - // If cmd1 and cmd2 conflict, order will be (cmd1 exe) -> (cmd1 as) -> (cmd2 exe) -> (cmd2 as) - #[traced_test] - #[tokio::test] - #[abort_on_panic] - async fn conflict_cmd_order() { - let (er_tx, mut er_rx) = mpsc::unbounded_channel(); - let (as_tx, mut as_rx) = mpsc::unbounded_channel(); - let ce = Arc::new(TestCE::new( - "S1".to_owned(), - er_tx, - as_tx, - EngineConfig::Memory, - )); - let task_manager = Arc::new(TaskManager::new()); - let (ce_event_tx, task_rx, done_tx) = - conflict_checked_mpmc::channel(Arc::clone(&ce), Arc::clone(&task_manager)); - start_cmd_workers( - Arc::clone(&ce), - Arc::new(RawCurp::new_test( - 3, - ce_event_tx.clone(), - mock_role_change(), - Arc::clone(&task_manager), - )), - task_rx, - done_tx, - ); - - let entry1 = Arc::new(LogEntry::new( - 1, - 1, - ProposeId(0, 0), - Arc::new(TestCommand::new_put(vec![1], 1)), - )); - let entry2 = Arc::new(LogEntry::new( - 2, - 1, - ProposeId(0, 1), - Arc::new(TestCommand::new_get(vec![1])), - )); - - ce_event_tx.send_sp_exe(Arc::clone(&entry1)); - ce_event_tx.send_sp_exe(Arc::clone(&entry2)); - - // cmd1 exe done - assert_eq!(er_rx.recv().await.unwrap().1.revisions, Vec::::new()); - - sleep_millis(100).await; - - // cmd2 will not be executed - assert!(er_rx.try_recv().is_err()); - assert!(as_rx.try_recv().is_err()); - - // cmd1 and cmd2 after sync - ce_event_tx.send_after_sync(entry1); - ce_event_tx.send_after_sync(entry2); - - assert_eq!(er_rx.recv().await.unwrap().1.revisions, vec![1]); - assert_eq!(as_rx.recv().await.unwrap().1, 1); - assert_eq!(as_rx.recv().await.unwrap().1, 2); - task_manager.shutdown(true).await; - } - - #[traced_test] - #[tokio::test] - #[abort_on_panic] - async fn reset_will_wipe_all_states_and_outdated_cmds() { - let (er_tx, mut er_rx) = mpsc::unbounded_channel(); - let (as_tx, mut as_rx) = mpsc::unbounded_channel(); - let ce = Arc::new(TestCE::new( - "S1".to_owned(), - er_tx, - as_tx, - EngineConfig::Memory, - )); - let task_manager = Arc::new(TaskManager::new()); - let (ce_event_tx, task_rx, done_tx) = - conflict_checked_mpmc::channel(Arc::clone(&ce), Arc::clone(&task_manager)); - start_cmd_workers( - Arc::clone(&ce), - Arc::new(RawCurp::new_test( - 3, - ce_event_tx.clone(), - mock_role_change(), - Arc::clone(&task_manager), - )), - task_rx, - done_tx, - ); - - let entry1 = Arc::new(LogEntry::new( - 1, - 1, - ProposeId(0, 0), - Arc::new(TestCommand::new_put(vec![1], 1).set_as_dur(Duration::from_millis(50))), - )); - let entry2 = Arc::new(LogEntry::new( - 2, - 1, - ProposeId(0, 1), - Arc::new(TestCommand::new_get(vec![1])), - )); - ce_event_tx.send_sp_exe(Arc::clone(&entry1)); - ce_event_tx.send_sp_exe(Arc::clone(&entry2)); - - assert_eq!(er_rx.recv().await.unwrap().1.revisions, Vec::::new()); - - ce_event_tx.send_reset(None); - - let entry3 = Arc::new(LogEntry::new( - 3, - 1, - ProposeId(0, 2), - Arc::new(TestCommand::new_get(vec![1])), - )); - - ce_event_tx.send_after_sync(entry3); - - assert_eq!(er_rx.recv().await.unwrap().1.revisions, Vec::::new()); - - // there will be only one after sync results - assert!(as_rx.recv().await.is_some()); - assert!(as_rx.try_recv().is_err()); - task_manager.shutdown(true).await; - } - - #[traced_test] - #[tokio::test] - #[abort_on_panic] - async fn test_snapshot() { - let task_manager1 = Arc::new(TaskManager::new()); - let task_manager2 = Arc::new(TaskManager::new()); - - // ce1 - let (er_tx, mut _er_rx) = mpsc::unbounded_channel(); - let (as_tx, mut _as_rx) = mpsc::unbounded_channel(); - let ce1 = Arc::new(TestCE::new( - "S1".to_owned(), - er_tx, - as_tx, - EngineConfig::Memory, - )); - let (ce_event_tx, task_rx, done_tx) = - conflict_checked_mpmc::channel(Arc::clone(&ce1), Arc::clone(&task_manager1)); - let curp = RawCurp::new_test( - 3, - ce_event_tx.clone(), - mock_role_change(), - Arc::clone(&task_manager1), - ); - let s2_id = curp.cluster().get_id_by_name("S2").unwrap(); - curp.handle_append_entries( - 1, - s2_id, - 0, - 0, - vec![LogEntry::new( - 1, - 1, - ProposeId(0, 0), - Arc::new(TestCommand::default()), - )], - 0, - ) - .unwrap(); - start_cmd_workers(Arc::clone(&ce1), Arc::new(curp), task_rx, done_tx); - - let entry = Arc::new(LogEntry::new( - 1, - 1, - ProposeId(0, 1), - Arc::new(TestCommand::new_put(vec![1], 1).set_exe_dur(Duration::from_millis(50))), - )); - - ce_event_tx.send_after_sync(entry); - - let snapshot = ce_event_tx - .send_snapshot(SnapshotMeta { - last_included_index: 1, - last_included_term: 0, - }) - .await - .unwrap(); - - // ce2 - let (er_tx, mut er_rx) = mpsc::unbounded_channel(); - let (as_tx, mut _as_rx) = mpsc::unbounded_channel(); - let ce2 = Arc::new(TestCE::new( - "S1".to_owned(), - er_tx, - as_tx, - EngineConfig::Memory, - )); - let (ce_event_tx, task_rx, done_tx) = - conflict_checked_mpmc::channel(Arc::clone(&ce2), Arc::clone(&task_manager2)); - start_cmd_workers( - Arc::clone(&ce2), - Arc::new(RawCurp::new_test( - 3, - ce_event_tx.clone(), - mock_role_change(), - Arc::clone(&task_manager2), - )), - task_rx, - done_tx, - ); - - ce_event_tx.send_reset(Some(snapshot)).await.unwrap(); - - let entry = Arc::new(LogEntry::new( - 1, - 1, - ProposeId(0, 2), - Arc::new(TestCommand::new_get(vec![1])), - )); - ce_event_tx.send_after_sync(entry); - assert_eq!(er_rx.recv().await.unwrap().1.revisions, vec![1]); - task_manager1.shutdown(true).await; - task_manager2.shutdown(true).await; - } -} diff --git a/crates/curp/src/server/conflict/mod.rs b/crates/curp/src/server/conflict/mod.rs index 45996a3e1..08fb96d65 100644 --- a/crates/curp/src/server/conflict/mod.rs +++ b/crates/curp/src/server/conflict/mod.rs @@ -10,131 +10,3 @@ mod tests; /// Conflict pool used in tests #[doc(hidden)] pub mod test_pools; - -use std::{ops::Deref, sync::Arc}; - -use crate::rpc::{ConfChange, PoolEntry, PoolEntryInner, ProposeId}; - -// TODO: relpace `PoolEntry` with this -/// Entry stored in conflict pools -pub(super) enum ConflictPoolEntry { - /// A command entry - Command(CommandEntry), - /// A conf change entry - ConfChange(ConfChangeEntry), -} - -impl From> for ConflictPoolEntry { - fn from(entry: PoolEntry) -> Self { - match entry.inner { - PoolEntryInner::Command(c) => ConflictPoolEntry::Command(CommandEntry { - id: entry.id, - cmd: c, - }), - PoolEntryInner::ConfChange(c) => ConflictPoolEntry::ConfChange(ConfChangeEntry { - id: entry.id, - conf_change: c, - }), - } - } -} - -/// Command entry type -#[derive(Debug)] -pub struct CommandEntry { - /// The propose id - id: ProposeId, - /// The command - cmd: Arc, -} - -impl CommandEntry { - /// Creates a new `CommandEntry` - #[inline] - pub fn new(id: ProposeId, cmd: Arc) -> Self { - Self { id, cmd } - } -} - -impl Clone for CommandEntry { - #[inline] - fn clone(&self) -> Self { - Self { - id: self.id, - cmd: Arc::clone(&self.cmd), - } - } -} - -impl Deref for CommandEntry { - type Target = C; - - #[inline] - fn deref(&self) -> &Self::Target { - &self.cmd - } -} - -impl AsRef for CommandEntry { - #[inline] - fn as_ref(&self) -> &C { - self.cmd.as_ref() - } -} - -impl std::hash::Hash for CommandEntry { - #[inline] - fn hash(&self, state: &mut H) { - self.id.hash(state); - } -} - -impl PartialEq for CommandEntry { - #[inline] - fn eq(&self, other: &Self) -> bool { - self.id.eq(&other.id) - } -} - -impl Eq for CommandEntry {} - -impl PartialOrd for CommandEntry { - #[inline] - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for CommandEntry { - #[inline] - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.id.cmp(&other.id) - } -} - -impl From> for PoolEntry { - fn from(entry: CommandEntry) -> Self { - PoolEntry { - id: entry.id, - inner: PoolEntryInner::Command(entry.cmd), - } - } -} - -/// Conf change entry type -#[derive(Clone, PartialEq)] -pub(super) struct ConfChangeEntry { - /// The propose id - id: ProposeId, - /// The conf change entry - conf_change: Vec, -} - -impl From for PoolEntry { - fn from(entry: ConfChangeEntry) -> Self { - PoolEntry { - id: entry.id, - inner: PoolEntryInner::ConfChange(entry.conf_change), - } - } -} diff --git a/crates/curp/src/server/conflict/spec_pool_new.rs b/crates/curp/src/server/conflict/spec_pool_new.rs index 88fde0c96..97cded6f3 100644 --- a/crates/curp/src/server/conflict/spec_pool_new.rs +++ b/crates/curp/src/server/conflict/spec_pool_new.rs @@ -1,17 +1,22 @@ -use curp_external_api::conflict::{ConflictPoolOp, SpeculativePoolOp}; +use std::{collections::HashMap, sync::Arc}; -use super::{CommandEntry, ConfChangeEntry, ConflictPoolEntry}; -use crate::rpc::PoolEntry; +use curp_external_api::conflict::SpeculativePoolOp; +use parking_lot::Mutex; + +use crate::rpc::{PoolEntry, ProposeId}; + +/// Ref to `SpeculativePool` +pub(crate) type SpeculativePoolRef = Arc>>; /// A speculative pool object -pub type SpObject = Box> + Send + 'static>; +pub type SpObject = Box> + Send + 'static>; /// Union type of `SpeculativePool` objects pub(crate) struct SpeculativePool { /// Command speculative pools command_sps: Vec>, - /// Conf change speculative pool - conf_change_sp: ConfChangeSp, + /// propose id to entry mapping + entries: HashMap>, } impl SpeculativePool { @@ -19,51 +24,38 @@ impl SpeculativePool { pub(crate) fn new(command_sps: Vec>) -> Self { Self { command_sps, - conf_change_sp: ConfChangeSp::default(), + entries: HashMap::new(), } } /// Inserts an entry into the pool + #[allow(clippy::needless_pass_by_value)] // we need to consume the entry pub(crate) fn insert(&mut self, entry: PoolEntry) -> Option> { - if !self.conf_change_sp.is_empty() { - return Some(entry); - } - - match ConflictPoolEntry::from(entry) { - ConflictPoolEntry::Command(c) => { - for csp in &mut self.command_sps { - if let Some(e) = csp.insert_if_not_conflict(c.clone()) { - return Some(e.into()); - } - } - } - ConflictPoolEntry::ConfChange(c) => { - if !self - .command_sps - .iter() - .map(AsRef::as_ref) - .all(ConflictPoolOp::is_empty) - { - return Some(c.into()); - } - let _ignore = self.conf_change_sp.insert_if_not_conflict(c); + for csp in &mut self.command_sps { + if let Some(e) = csp.insert_if_not_conflict(entry.clone()) { + return Some(e); } } + let _ignore = self.entries.insert(entry.id, entry); + None } - // TODO: Use reference instead of clone /// Removes an entry from the pool - pub(crate) fn remove(&mut self, entry: PoolEntry) { - match ConflictPoolEntry::from(entry) { - ConflictPoolEntry::Command(c) => { - for csp in &mut self.command_sps { - csp.remove(c.clone()); - } - } - ConflictPoolEntry::ConfChange(c) => { - self.conf_change_sp.remove(c); + pub(crate) fn remove(&mut self, entry: &PoolEntry) { + for csp in &mut self.command_sps { + csp.remove(entry); + } + + let _ignore = self.entries.remove(&entry.id); + } + + /// Removes an entry from the pool by it's propose id + pub(crate) fn remove_by_id(&mut self, id: &ProposeId) { + if let Some(entry) = self.entries.remove(id) { + for csp in &mut self.command_sps { + csp.remove(&entry); } } } @@ -74,7 +66,6 @@ impl SpeculativePool { for csp in &self.command_sps { entries.extend(csp.all().into_iter().map(Into::into)); } - entries.extend(self.conf_change_sp.all().into_iter().map(Into::into)); entries } @@ -84,47 +75,5 @@ impl SpeculativePool { self.command_sps .iter() .fold(0, |sum, pool| sum + pool.len()) - + self.conf_change_sp.len() - } -} - -/// Speculative pool for conf change entries -#[derive(Default)] -struct ConfChangeSp { - /// Store current conf change - change: Option, -} - -impl ConflictPoolOp for ConfChangeSp { - type Entry = ConfChangeEntry; - - fn is_empty(&self) -> bool { - self.change.is_none() - } - - fn remove(&mut self, _entry: Self::Entry) { - self.change = None; - } - - fn all(&self) -> Vec { - self.change.clone().into_iter().collect() - } - - fn clear(&mut self) { - self.change = None; - } - - fn len(&self) -> usize { - self.change.iter().count() - } -} - -impl SpeculativePoolOp for ConfChangeSp { - fn insert_if_not_conflict(&mut self, entry: Self::Entry) -> Option { - if self.change.is_some() { - return Some(entry); - } - self.change = Some(entry); - None } } diff --git a/crates/curp/src/server/conflict/test_pools.rs b/crates/curp/src/server/conflict/test_pools.rs index 9ffd2fcbe..1147dff81 100644 --- a/crates/curp/src/server/conflict/test_pools.rs +++ b/crates/curp/src/server/conflict/test_pools.rs @@ -4,15 +4,15 @@ use curp_external_api::{ }; use curp_test_utils::test_cmd::TestCommand; -use super::CommandEntry; +use crate::rpc::PoolEntry; #[derive(Debug, Default)] pub struct TestSpecPool { - cmds: Vec>, + cmds: Vec>, } impl ConflictPoolOp for TestSpecPool { - type Entry = CommandEntry; + type Entry = PoolEntry; #[inline] fn len(&self) -> usize { @@ -25,8 +25,8 @@ impl ConflictPoolOp for TestSpecPool { } #[inline] - fn remove(&mut self, entry: Self::Entry) { - if let Some(idx) = self.cmds.iter().position(|c| *c == entry) { + fn remove(&mut self, entry: &Self::Entry) { + if let Some(idx) = self.cmds.iter().position(|c| c == entry) { let _ignore = self.cmds.remove(idx); } } @@ -55,11 +55,11 @@ impl SpeculativePoolOp for TestSpecPool { #[derive(Debug, Default)] pub struct TestUncomPool { - cmds: Vec>, + cmds: Vec>, } impl ConflictPoolOp for TestUncomPool { - type Entry = CommandEntry; + type Entry = PoolEntry; #[inline] fn all(&self) -> Vec { @@ -77,8 +77,8 @@ impl ConflictPoolOp for TestUncomPool { } #[inline] - fn remove(&mut self, entry: Self::Entry) { - if let Some(idx) = self.cmds.iter().position(|c| *c == entry) { + fn remove(&mut self, entry: &Self::Entry) { + if let Some(idx) = self.cmds.iter().position(|c| c == entry) { let _ignore = self.cmds.remove(idx); } } diff --git a/crates/curp/src/server/conflict/tests.rs b/crates/curp/src/server/conflict/tests.rs index 6acd87947..bc9f1d6d1 100644 --- a/crates/curp/src/server/conflict/tests.rs +++ b/crates/curp/src/server/conflict/tests.rs @@ -1,20 +1,20 @@ -use std::{cmp::Ordering, sync::Arc}; +use std::sync::Arc; use curp_external_api::conflict::{ConflictPoolOp, SpeculativePoolOp, UncommittedPoolOp}; -use super::{spec_pool_new::SpeculativePool, CommandEntry}; +use super::spec_pool_new::SpeculativePool; use crate::{ - rpc::{ConfChange, PoolEntry, PoolEntryInner, ProposeId}, + rpc::{PoolEntry, ProposeId}, server::conflict::uncommitted_pool::UncommittedPool, }; #[derive(Debug, Default)] struct TestSp { - entries: Vec>, + entries: Vec>, } impl ConflictPoolOp for TestSp { - type Entry = CommandEntry; + type Entry = PoolEntry; fn len(&self) -> usize { self.entries.len() @@ -24,7 +24,7 @@ impl ConflictPoolOp for TestSp { self.entries.is_empty() } - fn remove(&mut self, entry: Self::Entry) { + fn remove(&mut self, entry: &Self::Entry) { if let Some(pos) = self .entries .iter() @@ -55,11 +55,11 @@ impl SpeculativePoolOp for TestSp { #[derive(Debug, Default)] struct TestUcp { - entries: Vec>, + entries: Vec>, } impl ConflictPoolOp for TestUcp { - type Entry = CommandEntry; + type Entry = PoolEntry; fn all(&self) -> Vec { self.entries.clone() @@ -73,7 +73,7 @@ impl ConflictPoolOp for TestUcp { self.entries.is_empty() } - fn remove(&mut self, entry: Self::Entry) { + fn remove(&mut self, entry: &Self::Entry) { if let Some(pos) = self .entries .iter() @@ -103,41 +103,6 @@ impl UncommittedPoolOp for TestUcp { } } -impl Eq for PoolEntry {} - -impl PartialOrd for PoolEntry { - fn partial_cmp(&self, other: &Self) -> Option { - #[allow(clippy::pattern_type_mismatch)] - match (&self.inner, &other.inner) { - (PoolEntryInner::Command(a), PoolEntryInner::Command(b)) => a.partial_cmp(&b), - (PoolEntryInner::Command(_), PoolEntryInner::ConfChange(_)) => Some(Ordering::Less), - (PoolEntryInner::ConfChange(_), PoolEntryInner::Command(_)) => Some(Ordering::Greater), - (PoolEntryInner::ConfChange(a), PoolEntryInner::ConfChange(b)) => { - for (ae, be) in a.iter().zip(b.iter()) { - let ord = ae.change_type.cmp(&be.change_type).then( - ae.node_id - .cmp(&be.node_id) - .then(ae.address.cmp(&be.address)), - ); - if !matches!(ord, Ordering::Equal) { - return Some(ord); - } - } - if a.len() > b.len() { - return Some(Ordering::Greater); - } - return Some(Ordering::Less); - } - } - } -} - -impl Ord for PoolEntry { - fn cmp(&self, other: &Self) -> Ordering { - self.partial_cmp(other).unwrap() - } -} - #[test] fn conflict_should_be_detected_in_sp() { let mut sp = SpeculativePool::new(vec![Box::new(TestSp::default())]); @@ -146,31 +111,8 @@ fn conflict_should_be_detected_in_sp() { assert!(sp.insert(entry1.clone()).is_none()); assert!(sp.insert(entry2).is_none()); assert!(sp.insert(entry1.clone()).is_some()); - sp.remove(entry1.clone()); - assert!(sp.insert(entry1).is_none()); -} - -#[test] -fn conf_change_should_conflict_with_all_entries_in_sp() { - let mut sp = SpeculativePool::new(vec![Box::new(TestSp::default())]); - let entry1 = PoolEntry::new(ProposeId::default(), Arc::new(0)); - let entry2 = PoolEntry::new(ProposeId::default(), Arc::new(1)); - let entry3 = PoolEntry::::new(ProposeId::default(), vec![ConfChange::default()]); - let entry4 = PoolEntry::::new( - ProposeId::default(), - vec![ConfChange { - change_type: 0, - node_id: 1, - address: vec![], - }], - ); - assert!(sp.insert(entry3.clone()).is_none()); - assert!(sp.insert(entry1.clone()).is_some()); - assert!(sp.insert(entry2.clone()).is_some()); - assert!(sp.insert(entry4).is_some()); - sp.remove(entry3.clone()); + sp.remove(&entry1); assert!(sp.insert(entry1).is_none()); - assert!(sp.insert(entry3).is_some()); } #[test] @@ -196,39 +138,16 @@ fn conflict_should_be_detected_in_ucp() { let mut ucp = UncommittedPool::new(vec![Box::new(TestUcp::default())]); let entry1 = PoolEntry::new(ProposeId::default(), Arc::new(0)); let entry2 = PoolEntry::new(ProposeId::default(), Arc::new(1)); - assert!(!ucp.insert(entry1.clone())); - assert!(!ucp.insert(entry2)); - assert!(ucp.insert(entry1.clone())); - ucp.remove(entry1.clone()); + assert!(!ucp.insert(&entry1)); + assert!(!ucp.insert(&entry2)); + assert!(ucp.insert(&entry1)); + ucp.remove(&entry1); // Ucp allows conflict cmds to co-exist in the same pool. // Therefore, we should still get `conflict=true` - assert!(ucp.insert(entry1.clone())); - ucp.remove(entry1.clone()); - ucp.remove(entry1.clone()); - assert!(!ucp.insert(entry1)); -} - -#[test] -fn conf_change_should_conflict_with_all_entries_in_ucp() { - let mut ucp = UncommittedPool::new(vec![Box::new(TestUcp::default())]); - let entry1 = PoolEntry::new(ProposeId::default(), Arc::new(0)); - let entry2 = PoolEntry::new(ProposeId::default(), Arc::new(1)); - let entry3 = PoolEntry::::new(ProposeId::default(), vec![ConfChange::default()]); - let entry4 = PoolEntry::::new( - ProposeId::default(), - vec![ConfChange { - change_type: 0, - node_id: 1, - address: vec![], - }], - ); - assert!(!ucp.insert(entry3.clone())); - assert!(ucp.insert(entry1.clone())); - assert!(ucp.insert(entry4.clone())); - ucp.remove(entry3.clone()); - ucp.remove(entry4.clone()); - assert!(!ucp.insert(entry2)); - assert!(ucp.insert(entry3)); + assert!(ucp.insert(&entry1)); + ucp.remove(&entry1); + ucp.remove(&entry1); + assert!(!ucp.insert(&entry1)); } #[test] @@ -237,11 +156,11 @@ fn ucp_should_returns_all_entries() { let entries: Vec<_> = (0..10) .map(|i| PoolEntry::new(ProposeId::default(), Arc::new(i))) .collect(); - for e in entries.clone() { + for e in &entries { ucp.insert(e); } - for e in entries.clone() { - assert!(ucp.insert(e)); + for e in &entries { + assert!(ucp.insert(&e)); } let results = ucp.all(); @@ -256,14 +175,12 @@ fn ucp_should_returns_all_conflict_entries() { .map(|i| PoolEntry::new(ProposeId::default(), Arc::new(i))) .collect(); for e in &entries { - ucp.insert(e.clone()); - ucp.insert(e.clone()); + ucp.insert(e); + ucp.insert(e); } - let conf_change = PoolEntry::::new(ProposeId::default(), vec![ConfChange::default()]); - ucp.insert(conf_change.clone()); for e in entries { - let mut all = ucp.all_conflict(e.clone()); + let mut all = ucp.all_conflict(&e); all.sort(); - assert_eq!(all, vec![e.clone(), e.clone(), conf_change.clone()]); + assert_eq!(all, vec![e.clone(), e.clone()]); } } diff --git a/crates/curp/src/server/conflict/uncommitted_pool.rs b/crates/curp/src/server/conflict/uncommitted_pool.rs index 6b38b34f5..432d72a1d 100644 --- a/crates/curp/src/server/conflict/uncommitted_pool.rs +++ b/crates/curp/src/server/conflict/uncommitted_pool.rs @@ -1,98 +1,46 @@ -use curp_external_api::conflict::{ConflictPoolOp, UncommittedPoolOp}; +use curp_external_api::conflict::UncommittedPoolOp; -use super::{CommandEntry, ConfChangeEntry, ConflictPoolEntry}; use crate::rpc::PoolEntry; /// An uncommitted pool object -pub type UcpObject = Box> + Send + 'static>; +pub type UcpObject = Box> + Send + 'static>; /// Union type of `UncommittedPool` objects pub(crate) struct UncommittedPool { /// Command uncommitted pools command_ucps: Vec>, - /// Conf change uncommitted pools - conf_change_ucp: ConfChangeUcp, } impl UncommittedPool { /// Creates a new `UncomPool` pub(crate) fn new(command_ucps: Vec>) -> Self { - Self { - command_ucps, - conf_change_ucp: ConfChangeUcp::default(), - } + Self { command_ucps } } /// Insert an entry into the pool - pub(crate) fn insert(&mut self, entry: PoolEntry) -> bool { + pub(crate) fn insert(&mut self, entry: &PoolEntry) -> bool { let mut conflict = false; - conflict |= !self.conf_change_ucp.is_empty(); - - match ConflictPoolEntry::from(entry) { - ConflictPoolEntry::Command(c) => { - for cucp in &mut self.command_ucps { - conflict |= cucp.insert(c.clone()); - } - } - ConflictPoolEntry::ConfChange(c) => { - let _ignore = self.conf_change_ucp.insert(c); - conflict |= !self - .command_ucps - .iter() - .map(AsRef::as_ref) - .all(ConflictPoolOp::is_empty); - } + for cucp in &mut self.command_ucps { + conflict |= cucp.insert(entry.clone()); } conflict } /// Removes an entry from the pool - pub(crate) fn remove(&mut self, entry: PoolEntry) { - match ConflictPoolEntry::from(entry) { - ConflictPoolEntry::Command(c) => { - for cucp in &mut self.command_ucps { - cucp.remove(c.clone()); - } - } - ConflictPoolEntry::ConfChange(c) => { - self.conf_change_ucp.remove(c); - } + pub(crate) fn remove(&mut self, entry: &PoolEntry) { + for cucp in &mut self.command_ucps { + cucp.remove(entry); } } /// Returns all entries in the pool that conflict with the given entry - pub(crate) fn all_conflict(&self, entry: PoolEntry) -> Vec> { - match ConflictPoolEntry::from(entry) { - // A command entry conflict with other conflict entries plus all conf change entries - ConflictPoolEntry::Command(ref c) => self - .conf_change_ucp - .all() - .into_iter() - .map(Into::into) - .chain( - self.command_ucps - .iter() - .flat_map(|p| p.all_conflict(c)) - .map(Into::into), - ) - .collect(), - // A conf change entry conflict with all other entries - ConflictPoolEntry::ConfChange(_) => self - .conf_change_ucp - .all() - .into_iter() - .map(Into::into) - .chain( - self.command_ucps - .iter() - .map(AsRef::as_ref) - .flat_map(ConflictPoolOp::all) - .map(Into::into), - ) - .collect(), - } + pub(crate) fn all_conflict(&self, entry: &PoolEntry) -> Vec> { + self.command_ucps + .iter() + .flat_map(|p| p.all_conflict(entry)) + .collect() } #[cfg(test)] @@ -100,16 +48,15 @@ impl UncommittedPool { pub(crate) fn all(&self) -> Vec> { let mut entries = Vec::new(); for csp in &self.command_ucps { - entries.extend(csp.all().into_iter().map(Into::into)); + entries.extend(csp.all().into_iter()); } - entries.extend(self.conf_change_ucp.all().into_iter().map(Into::into)); entries } #[cfg(test)] /// Returns `true` if the pool is empty pub(crate) fn is_empty(&self) -> bool { - self.command_ucps.iter().all(|ucp| ucp.is_empty()) && self.conf_change_ucp.is_empty() + self.command_ucps.iter().all(|ucp| ucp.is_empty()) } /// Clears all entries in the pool @@ -117,51 +64,5 @@ impl UncommittedPool { for ucp in &mut self.command_ucps { ucp.clear(); } - self.conf_change_ucp.clear(); - } -} - -/// Conf change uncommitted pool -#[derive(Default)] -struct ConfChangeUcp { - /// entry count - conf_changes: Vec, -} - -impl ConflictPoolOp for ConfChangeUcp { - type Entry = ConfChangeEntry; - - fn is_empty(&self) -> bool { - self.conf_changes.is_empty() - } - - fn remove(&mut self, entry: Self::Entry) { - if let Some(pos) = self.conf_changes.iter().position(|x| *x == entry) { - let _ignore = self.conf_changes.remove(pos); - } - } - - fn all(&self) -> Vec { - self.conf_changes.clone() - } - - fn clear(&mut self) { - self.conf_changes.clear(); - } - - fn len(&self) -> usize { - self.conf_changes.len() - } -} - -impl UncommittedPoolOp for ConfChangeUcp { - fn insert(&mut self, entry: Self::Entry) -> bool { - let conflict = !self.conf_changes.is_empty(); - self.conf_changes.push(entry); - conflict - } - - fn all_conflict(&self, _entry: &Self::Entry) -> Vec { - self.conf_changes.clone() } } diff --git a/crates/curp/src/server/curp_node.rs b/crates/curp/src/server/curp_node.rs index c4818da9a..4e1c5a552 100644 --- a/crates/curp/src/server/curp_node.rs +++ b/crates/curp/src/server/curp_node.rs @@ -10,9 +10,10 @@ use engine::{SnapshotAllocator, SnapshotApi}; use event_listener::Event; use futures::{pin_mut, stream::FuturesUnordered, Stream, StreamExt}; use madsim::rand::{thread_rng, Rng}; +use opentelemetry::KeyValue; use parking_lot::{Mutex, RwLock}; use tokio::{ - sync::{broadcast, mpsc}, + sync::{broadcast, oneshot}, time::MissedTickBehavior, }; #[cfg(not(madsim))] @@ -21,18 +22,17 @@ use tracing::{debug, error, info, trace, warn}; #[cfg(madsim)] use utils::ClientTlsConfig; use utils::{ + barrier::IdBarrier, config::CurpConfig, task_manager::{tasks::TaskName, Listener, State, TaskManager}, }; use super::{ cmd_board::{CmdBoardRef, CommandBoard}, - cmd_worker::{conflict_checked_mpmc, start_cmd_workers}, - conflict::{ - spec_pool_new::{SpObject, SpeculativePool}, - uncommitted_pool::{UcpObject, UncommittedPool}, - }, - gc::gc_cmd_board, + cmd_worker::execute, + conflict::spec_pool_new::{SpObject, SpeculativePool}, + conflict::uncommitted_pool::{UcpObject, UncommittedPool}, + gc::gc_client_lease, lease_manager::LeaseManager, raw_curp::{AppendEntries, RawCurp, Vote}, storage::StorageApi, @@ -41,6 +41,7 @@ use crate::{ cmd::{Command, CommandExecutor}, log_entry::{EntryData, LogEntry}, members::{ClusterInfo, ServerId}, + response::ResponseSender, role_change::RoleChange, rpc::{ self, @@ -48,58 +49,295 @@ use crate::{ AppendEntriesRequest, AppendEntriesResponse, ConfChange, ConfChangeType, CurpError, FetchClusterRequest, FetchClusterResponse, FetchReadStateRequest, FetchReadStateResponse, InstallSnapshotRequest, InstallSnapshotResponse, LeaseKeepAliveMsg, MoveLeaderRequest, - MoveLeaderResponse, ProposeConfChangeRequest, ProposeConfChangeResponse, ProposeRequest, - ProposeResponse, PublishRequest, PublishResponse, ShutdownRequest, ShutdownResponse, - TriggerShutdownRequest, TriggerShutdownResponse, TryBecomeLeaderNowRequest, - TryBecomeLeaderNowResponse, VoteRequest, VoteResponse, WaitSyncedRequest, - WaitSyncedResponse, + MoveLeaderResponse, PoolEntry, ProposeConfChangeRequest, ProposeConfChangeResponse, + ProposeId, ProposeRequest, ProposeResponse, PublishRequest, PublishResponse, + ReadIndexResponse, RecordRequest, RecordResponse, ShutdownRequest, ShutdownResponse, + SyncedResponse, TriggerShutdownRequest, TriggerShutdownResponse, TryBecomeLeaderNowRequest, + TryBecomeLeaderNowResponse, VoteRequest, VoteResponse, + }, + server::{ + cmd_worker::{after_sync, worker_reset, worker_snapshot}, + metrics, + raw_curp::SyncAction, + storage::db::DB, }, - server::{cmd_worker::CEEventTxApi, metrics, raw_curp::SyncAction, storage::db::DB}, snapshot::{Snapshot, SnapshotMeta}, }; +/// After sync entry, composed of a log entry and response sender +pub(crate) type AfterSyncEntry = (Arc>, Option>); + +/// The after sync task type +#[derive(Debug)] +pub(super) enum TaskType { + /// After sync an entry + Entries(Vec>), + /// Reset the CE + Reset(Option, oneshot::Sender<()>), + /// Snapshot + Snapshot(SnapshotMeta, oneshot::Sender), +} + +/// A propose type +pub(super) struct Propose { + /// The command of the propose + pub(super) cmd: Arc, + /// Propose id + pub(super) id: ProposeId, + /// Term the client proposed + /// NOTE: this term should be equal to the cluster's latest term + /// for the propose to be accepted. + pub(super) term: u64, + /// Tx used for sending the streaming response back to client + pub(super) resp_tx: Arc, +} + +impl Propose +where + C: Command, +{ + /// Attempts to create a new `Propose` from request + fn try_new(req: &ProposeRequest, resp_tx: Arc) -> Result { + let cmd: Arc = Arc::new(req.cmd()?); + Ok(Self { + cmd, + id: req.propose_id(), + term: req.term, + resp_tx, + }) + } + + /// Returns `true` if the proposed command is read-only + fn is_read_only(&self) -> bool { + self.cmd.is_read_only() + } + + /// Gets response sender + fn response_tx(&self) -> Arc { + Arc::clone(&self.resp_tx) + } + + /// Convert self into parts + fn into_parts(self) -> (Arc, ProposeId, u64, Arc) { + let Self { + cmd, + id, + term, + resp_tx, + } = self; + (cmd, id, term, resp_tx) + } +} + +/// Entry to execute +type ExecutorEntry = (Arc>, Arc); + /// `CurpNode` represents a single node of curp cluster -pub(super) struct CurpNode { +pub(super) struct CurpNode, RC: RoleChange> { /// `RawCurp` state machine curp: Arc>, /// Cmd watch board for tracking the cmd sync results cmd_board: CmdBoardRef, - /// CE event tx, - ce_event_tx: Arc>, /// Storage storage: Arc>, /// Snapshot allocator snapshot_allocator: Box, + /// Command Executor + #[allow(unused)] + cmd_executor: Arc, + /// Tx to send entries to after_sync + as_tx: flume::Sender>, + /// Tx to send to propose task + propose_tx: flume::Sender>, } /// Handlers for clients -impl CurpNode { - /// Handle `Propose` requests - pub(super) async fn propose(&self, req: ProposeRequest) -> Result { - if self.curp.is_shutdown() { +impl, RC: RoleChange> CurpNode { + /// Handle `ProposeStream` requests + pub(super) async fn propose_stream( + &self, + req: &ProposeRequest, + resp_tx: Arc, + bypassed: bool, + ) -> Result<(), CurpError> { + if self.curp.is_cluster_shutdown() { return Err(CurpError::shutting_down()); } - let id = req.propose_id(); + self.curp.check_leader_transfer()?; self.check_cluster_version(req.cluster_version)?; + self.curp.check_term(req.term)?; + + if req.slow_path { + resp_tx.set_conflict(true); + } else { + info!("not using slow path for: {req:?}"); + } + + if bypassed { + self.curp.mark_client_id_bypassed(req.propose_id().0); + } + + match self + .curp + .deduplicate(req.propose_id(), Some(req.first_incomplete)) + { + // If the propose is duplicated, return the result directly + Err(CurpError::Duplicated(())) => { + let (er, asr) = + CommandBoard::wait_for_er_asr(&self.cmd_board, req.propose_id()).await; + resp_tx.send_propose(ProposeResponse::new_result::(&er, true)); + resp_tx.send_synced(SyncedResponse::new_result::(&asr)); + } + Err(CurpError::ExpiredClientId(())) => { + metrics::get() + .proposals_failed + .add(1, &[KeyValue::new("reason", "duplicated proposal")]); + return Err(CurpError::expired_client_id()); + } + Err(_) => unreachable!("deduplicate won't return other type of errors"), + Ok(()) => {} + } + + let propose = Propose::try_new(req, resp_tx)?; + let _ignore = self.propose_tx.send(propose); + + Ok(()) + } + + /// Handle `Record` requests + pub(super) fn record(&self, req: &RecordRequest) -> Result { + if self.curp.is_cluster_shutdown() { + return Err(CurpError::shutting_down()); + } + let id = req.propose_id(); let cmd: Arc = Arc::new(req.cmd()?); - // handle proposal - let sp_exec = self.curp.handle_propose(id, Arc::clone(&cmd))?; + let conflict = self.curp.follower_record(id, &cmd); + + Ok(RecordResponse { conflict }) + } + + /// Handle `Record` requests + pub(super) fn read_index(&self) -> Result { + if self.curp.is_cluster_shutdown() { + return Err(CurpError::shutting_down()); + } + Ok(ReadIndexResponse { + term: self.curp.term(), + }) + } + + /// Handle propose task + async fn handle_propose_task( + ce: Arc, + curp: Arc>, + rx: flume::Receiver>, + ) { + /// Max number of propose in a batch + const MAX_BATCH_SIZE: usize = 1024; + + let cmd_executor = Self::build_executor(ce, Arc::clone(&curp)); + loop { + let Ok(first) = rx.recv_async().await else { + info!("handle propose task exit"); + break; + }; + let mut addition: Vec<_> = std::iter::repeat_with(|| rx.try_recv()) + .take(MAX_BATCH_SIZE) + .flatten() + .collect(); + addition.push(first); + let (read_onlys, mutatives): (Vec<_>, Vec<_>) = + addition.into_iter().partition(Propose::is_read_only); + + Self::handle_read_onlys(cmd_executor.clone(), &curp, read_onlys); + Self::handle_mutatives(cmd_executor.clone(), &curp, mutatives); + } + } - // if speculatively executed, wait for the result and return - if sp_exec { - let er_res = CommandBoard::wait_for_er(&self.cmd_board, id).await; - return Ok(ProposeResponse::new_result::(&er_res)); + /// Handle read-only proposes + fn handle_read_onlys( + cmd_executor: Executor, + curp: &RawCurp, + proposes: Vec>, + ) where + Executor: Fn(ExecutorEntry) + Clone + Send + 'static, + { + for propose in proposes { + info!("handle read only cmd: {:?}", propose.cmd); + // TODO: Disable dedup if the command is read only or commute + let Propose { + cmd, resp_tx, id, .. + } = propose; + // Use default value for the entry as we don't need to put it into curp log + let entry = Arc::new(LogEntry::new(0, 0, id, Arc::clone(&cmd))); + let wait_conflict = curp.wait_conflicts_synced(cmd); + let wait_no_op = curp.wait_no_op_applied(); + let cmd_executor_c = cmd_executor.clone(); + let _ignore = tokio::spawn(async move { + tokio::join!(wait_conflict, wait_no_op); + cmd_executor_c((entry, resp_tx)); + }); + } + } + + /// Handle read-only proposes + fn handle_mutatives( + cmd_executor: Executor, + curp: &RawCurp, + proposes: Vec>, + ) where + Executor: Fn(ExecutorEntry), + { + if proposes.is_empty() { + return; } + let pool_entries = proposes + .iter() + .map(|p| PoolEntry::new(p.id, Arc::clone(&p.cmd))); + let conflicts = curp.leader_record(pool_entries); + for (p, conflict) in proposes.iter().zip(conflicts) { + info!("handle mutative cmd: {:?}, conflict: {conflict}", p.cmd); + p.resp_tx.set_conflict(conflict); + } + let resp_txs: Vec<_> = proposes.iter().map(Propose::response_tx).collect(); + let logs: Vec<_> = proposes.into_iter().map(Propose::into_parts).collect(); + let entries = curp.push_logs(logs); + #[allow(clippy::pattern_type_mismatch)] // Can't be fixed + entries + .into_iter() + .zip(resp_txs) + .filter(|(_, tx)| !tx.is_conflict()) + .for_each(cmd_executor); + } - Ok(ProposeResponse::new_empty()) + /// Speculatively execute a command + fn build_executor(ce: Arc, curp: Arc>) -> impl Fn(ExecutorEntry) + Clone { + move |(entry, resp_tx): (_, Arc)| { + info!("spec execute entry: {entry:?}"); + let result = execute(&entry, ce.as_ref(), curp.as_ref()); + match result { + Ok((er, Some(asr))) => { + resp_tx.send_propose(ProposeResponse::new_result::(&Ok(er), false)); + resp_tx.send_synced(SyncedResponse::new_result::(&Ok(asr))); + } + Ok((er, None)) => { + resp_tx.send_propose(ProposeResponse::new_result::(&Ok(er), false)); + } + Err(e) => resp_tx.send_synced(SyncedResponse::new_result::(&Err(e))), + } + } } /// Handle `Shutdown` requests pub(super) async fn shutdown( &self, req: ShutdownRequest, + bypassed: bool, ) -> Result { self.check_cluster_version(req.cluster_version)?; + if bypassed { + self.curp.mark_client_id_bypassed(req.propose_id().0); + } self.curp.handle_shutdown(req.propose_id())?; CommandBoard::wait_for_shutdown_synced(&self.cmd_board).await; Ok(ShutdownResponse::default()) @@ -109,9 +347,13 @@ impl CurpNode { pub(super) async fn propose_conf_change( &self, req: ProposeConfChangeRequest, + bypassed: bool, ) -> Result { self.check_cluster_version(req.cluster_version)?; let id = req.propose_id(); + if bypassed { + self.curp.mark_client_id_bypassed(id.0); + } self.curp.handle_propose_conf_change(id, req.changes)?; CommandBoard::wait_for_conf(&self.cmd_board, id).await; let members = self.curp.cluster().all_members_vec(); @@ -119,7 +361,14 @@ impl CurpNode { } /// Handle `Publish` requests - pub(super) fn publish(&self, req: PublishRequest) -> Result { + pub(super) fn publish( + &self, + req: PublishRequest, + bypassed: bool, + ) -> Result { + if bypassed { + self.curp.mark_client_id_bypassed(req.propose_id().0); + } self.curp.handle_publish(req)?; Ok(PublishResponse::default()) } @@ -131,9 +380,15 @@ impl CurpNode { ) -> Result { pin_mut!(req_stream); while let Some(req) = req_stream.next().await { - if self.curp.is_shutdown() { + // NOTE: The leader may shutdown itself in configuration change. + // We must first check this situation. + self.curp.check_leader_transfer()?; + if self.curp.is_cluster_shutdown() { return Err(CurpError::shutting_down()); } + if self.curp.is_node_shutdown() { + return Err(CurpError::node_not_exist()); + } if !self.curp.is_leader() { let (leader_id, term, _) = self.curp.leader(); return Err(CurpError::redirect(leader_id, term)); @@ -151,7 +406,7 @@ impl CurpNode { } /// Handlers for peers -impl CurpNode { +impl, RC: RoleChange> CurpNode { /// Handle `AppendEntries` requests pub(super) fn append_entries( &self, @@ -168,7 +423,11 @@ impl CurpNode { req.leader_commit, ); let resp = match result { - Ok(term) => AppendEntriesResponse::new_accept(term), + Ok((term, to_persist)) => { + self.storage + .put_log_entries(&to_persist.iter().map(Arc::as_ref).collect::>())?; + AppendEntriesResponse::new_accept(term) + } Err((term, hint)) => AppendEntriesResponse::new_reject(term, hint), }; @@ -176,7 +435,7 @@ impl CurpNode { } /// Handle `Vote` requests - pub(super) async fn vote(&self, req: VoteRequest) -> Result { + pub(super) fn vote(&self, req: &VoteRequest) -> Result { let result = if req.is_pre_vote { self.curp.handle_pre_vote( req.term, @@ -196,7 +455,7 @@ impl CurpNode { let resp = match result { Ok((term, sp)) => { if !req.is_pre_vote { - self.storage.flush_voted_for(term, req.candidate_id).await?; + self.storage.flush_voted_for(term, req.candidate_id)?; } VoteResponse::new_accept(term, sp)? } @@ -208,33 +467,11 @@ impl CurpNode { } /// Handle `TriggerShutdown` requests - pub(super) fn trigger_shutdown( - &self, - _req: &TriggerShutdownRequest, - ) -> TriggerShutdownResponse { + pub(super) fn trigger_shutdown(&self, _req: TriggerShutdownRequest) -> TriggerShutdownResponse { self.curp.task_manager().mark_leader_notified(); TriggerShutdownResponse::default() } - /// handle `WaitSynced` requests - pub(super) async fn wait_synced( - &self, - req: WaitSyncedRequest, - ) -> Result { - if self.curp.is_shutdown() { - return Err(CurpError::shutting_down()); - } - self.check_cluster_version(req.cluster_version)?; - let id = req.propose_id(); - debug!("{} get wait synced request for cmd({id})", self.curp.id()); - if self.curp.get_transferee().is_some() { - return Err(CurpError::leader_transfer("leader transferring")); - } - let (er, asr) = CommandBoard::wait_for_er_asr(&self.cmd_board, id).await; - debug!("{} wait synced for cmd({id}) finishes", self.curp.id()); - Ok(WaitSyncedResponse::new_from_result::(er, asr)) - } - /// Handle `FetchCluster` requests #[allow(clippy::unnecessary_wraps, clippy::needless_pass_by_value)] // To keep type consistent with other request handlers pub(super) fn fetch_cluster( @@ -308,15 +545,14 @@ impl CurpNode { "{} successfully received a snapshot, {snapshot:?}", self.curp.id(), ); - self.ce_event_tx - .send_reset(Some(snapshot)) - .await - .map_err(|err| { - error!("failed to reset the command executor by snapshot, {err}"); - CurpError::internal(format!( - "failed to reset the command executor by snapshot, {err}" - )) - })?; + let (tx, rx) = oneshot::channel(); + self.as_tx.send(TaskType::Reset(Some(snapshot), tx))?; + rx.await.map_err(|err| { + error!("failed to reset the command executor by snapshot, {err}"); + CurpError::internal(format!( + "failed to reset the command executor by snapshot, {err}" + )) + })?; metrics::get().apply_snapshot_in_progress.add(-1, &[]); metrics::get() .snapshot_install_total_duration_seconds @@ -395,7 +631,7 @@ impl CurpNode { } /// Spawned tasks -impl CurpNode { +impl, RC: RoleChange> CurpNode { /// Tick periodically #[allow(clippy::arithmetic_side_effects, clippy::ignored_unit_patterns)] async fn election_task(curp: Arc>, shutdown_listener: Listener) { @@ -572,42 +808,41 @@ impl CurpNode { debug!("{} to {} sync follower task exits", curp.id(), connect.id()); } - /// Log persist task - pub(super) async fn log_persist_task( - mut log_rx: mpsc::UnboundedReceiver>>, - storage: Arc>, - shutdown_listener: Listener, + /// After sync task + async fn after_sync_task( + curp: Arc>, + cmd_executor: Arc, + as_rx: flume::Receiver>, ) { - #[allow(clippy::arithmetic_side_effects, clippy::ignored_unit_patterns)] - // introduced by tokio select - loop { - tokio::select! { - e = log_rx.recv() => { - let Some(e) = e else { - return; - }; - if let Err(err) = storage.put_log_entry(e.as_ref()).await { - error!("storage error, {err}"); - } - } - _ = shutdown_listener.wait() => break, - } + while let Ok(task) = as_rx.recv_async().await { + Self::handle_as_task(&curp, &cmd_executor, task).await; } - while let Ok(e) = log_rx.try_recv() { - if let Err(err) = storage.put_log_entry(e.as_ref()).await { - error!("storage error, {err}"); + debug!("after sync task exits"); + } + + /// Handles a after sync task + async fn handle_as_task(curp: &RawCurp, cmd_executor: &CE, task: TaskType) { + debug!("after sync: {task:?}"); + match task { + TaskType::Entries(entries) => { + after_sync(entries, cmd_executor, curp).await; + } + TaskType::Reset(snap, tx) => { + let _ignore = worker_reset(snap, tx, cmd_executor, curp).await; + } + TaskType::Snapshot(meta, tx) => { + let _ignore = worker_snapshot(meta, tx, cmd_executor, curp).await; } } - debug!("log persist task exits"); } } // utils -impl CurpNode { +impl, RC: RoleChange> CurpNode { /// Create a new server instance #[inline] #[allow(clippy::too_many_arguments)] // TODO: refactor this use builder pattern - pub(super) async fn new>( + pub(super) async fn new( cluster_info: Arc, is_leader: bool, cmd_executor: Arc, @@ -629,28 +864,25 @@ impl CurpNode { .await .map_err(|e| CurpError::internal(format!("parse peers addresses failed, err {e:?}")))? .collect(); - let (log_tx, log_rx) = mpsc::unbounded_channel(); let cmd_board = Arc::new(RwLock::new(CommandBoard::new())); let lease_manager = Arc::new(RwLock::new(LeaseManager::new())); let last_applied = cmd_executor .last_applied() .map_err(|e| CurpError::internal(format!("get applied index error, {e}")))?; - let (ce_event_tx, task_rx, done_tx) = - conflict_checked_mpmc::channel(Arc::clone(&cmd_executor), Arc::clone(&task_manager)); - let ce_event_tx: Arc> = Arc::new(ce_event_tx); - + let (as_tx, as_rx) = flume::unbounded(); + let (propose_tx, propose_rx) = flume::bounded(4096); + let sp = Arc::new(Mutex::new(SpeculativePool::new(sps))); + let ucp = Arc::new(Mutex::new(UncommittedPool::new(ucps))); // create curp state machine - let (voted_for, entries) = storage.recover().await?; + let (voted_for, entries) = storage.recover()?; let curp = Arc::new( RawCurp::builder() .cluster_info(Arc::clone(&cluster_info)) .is_leader(is_leader) .cmd_board(Arc::clone(&cmd_board)) - .lease_manager(lease_manager) + .lease_manager(Arc::clone(&lease_manager)) .cfg(Arc::clone(&curp_cfg)) - .cmd_tx(Arc::clone(&ce_event_tx)) .sync_events(sync_events) - .log_tx(log_tx) .role_change(role_change) .task_manager(Arc::clone(&task_manager)) .connects(connects) @@ -659,36 +891,51 @@ impl CurpNode { .entries(entries) .curp_storage(Arc::clone(&storage)) .client_tls_config(client_tls_config) - .spec_pool(Arc::new(Mutex::new(SpeculativePool::new(sps)))) - .uncommitted_pool(Arc::new(Mutex::new(UncommittedPool::new(ucps)))) + .spec_pool(Arc::clone(&sp)) + .uncommitted_pool(ucp) + .as_tx(as_tx.clone()) + .resp_txs(Arc::new(Mutex::default())) + .id_barrier(Arc::new(IdBarrier::new())) .build_raw_curp() .map_err(|e| CurpError::internal(format!("build raw curp failed, {e}")))?, ); metrics::Metrics::register_callback(Arc::clone(&curp))?; - start_cmd_workers(cmd_executor, Arc::clone(&curp), task_rx, done_tx); - - task_manager.spawn(TaskName::GcCmdBoard, |n| { - gc_cmd_board(Arc::clone(&cmd_board), curp_cfg.gc_interval, n) + task_manager.spawn(TaskName::GcClientLease, |n| { + gc_client_lease( + lease_manager, + Arc::clone(&cmd_board), + sp, + curp_cfg.gc_interval, + n, + ) }); - Self::run_bg_tasks(Arc::clone(&curp), Arc::clone(&storage), log_rx); + Self::run_bg_tasks( + Arc::clone(&curp), + Arc::clone(&cmd_executor), + propose_rx, + as_rx, + ); Ok(Self { curp, cmd_board, - ce_event_tx, storage, snapshot_allocator, + cmd_executor, + as_tx, + propose_tx, }) } /// Run background tasks for Curp server fn run_bg_tasks( curp: Arc>, - storage: Arc + 'static>, - log_rx: mpsc::UnboundedReceiver>>, + cmd_executor: Arc, + propose_rx: flume::Receiver>, + as_rx: flume::Receiver>, ) { let task_manager = curp.task_manager(); @@ -714,16 +961,22 @@ impl CurpNode { } task_manager.spawn(TaskName::ConfChange, |n| { - Self::conf_change_handler(curp, remove_events, n) + Self::conf_change_handler(Arc::clone(&curp), remove_events, n) }); - task_manager.spawn(TaskName::LogPersist, |n| { - Self::log_persist_task(log_rx, storage, n) + task_manager.spawn(TaskName::HandlePropose, |_n| { + Self::handle_propose_task(Arc::clone(&cmd_executor), Arc::clone(&curp), propose_rx) + }); + task_manager.spawn(TaskName::AfterSync, |_n| { + Self::after_sync_task(curp, cmd_executor, as_rx) }); } /// Candidate or pre candidate broadcasts votes - /// Return `Some(vote)` if bcast pre vote and success - /// Return `None` if bcast pre vote and fail or bcast vote + /// + /// # Returns + /// + /// - `Some(vote)` if bcast pre vote and success + /// - `None` if bcast pre vote and fail or bcast vote async fn bcast_vote(curp: &RawCurp, vote: Vote) -> Option { if vote.is_pre_vote { debug!("{} broadcasts pre votes to all servers", curp.id()); @@ -972,7 +1225,7 @@ impl CurpNode { } } -impl Debug for CurpNode { +impl, RC: RoleChange> Debug for CurpNode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("CurpNode") .field("raw_curp", &self.curp) @@ -983,14 +1236,14 @@ impl Debug for CurpNode { #[cfg(test)] mod tests { - use curp_test_utils::{mock_role_change, sleep_secs, test_cmd::TestCommand}; + use curp_test_utils::{ + mock_role_change, sleep_secs, + test_cmd::{TestCE, TestCommand}, + }; use tracing_test::traced_test; use super::*; - use crate::{ - rpc::{connect::MockInnerConnectApi, ConfChange}, - server::cmd_worker::MockCEEventTxApi, - }; + use crate::rpc::{connect::MockInnerConnectApi, ConfChange}; #[traced_test] #[tokio::test] @@ -998,7 +1251,6 @@ mod tests { let task_manager = Arc::new(TaskManager::new()); let curp = Arc::new(RawCurp::new_test( 3, - MockCEEventTxApi::::default(), mock_role_change(), Arc::clone(&task_manager), )); @@ -1011,7 +1263,7 @@ mod tests { mock_connect1.expect_id().return_const(s1_id); let remove_event = Arc::new(Event::new()); task_manager.spawn(TaskName::SyncFollower, |n| { - CurpNode::sync_follower_task( + CurpNode::<_, TestCE, _>::sync_follower_task( Arc::clone(&curp), InnerConnectApiWrapper::new_from_arc(Arc::new(mock_connect1)), Arc::new(Event::new()), @@ -1028,10 +1280,8 @@ mod tests { async fn tick_task_will_bcast_votes() { let task_manager = Arc::new(TaskManager::new()); let curp = { - let exe_tx = MockCEEventTxApi::::default(); Arc::new(RawCurp::new_test( 3, - exe_tx, mock_role_change(), Arc::clone(&task_manager), )) @@ -1066,7 +1316,7 @@ mod tests { InnerConnectApiWrapper::new_from_arc(Arc::new(mock_connect2)), ); task_manager.spawn(TaskName::Election, |n| { - CurpNode::election_task(Arc::clone(&curp), n) + CurpNode::<_, TestCE, _>::election_task(Arc::clone(&curp), n) }); sleep_secs(3).await; assert!(curp.is_leader()); @@ -1078,10 +1328,8 @@ mod tests { async fn vote_will_not_send_to_learner_during_election() { let task_manager = Arc::new(TaskManager::new()); let curp = { - let exe_tx = MockCEEventTxApi::::default(); Arc::new(RawCurp::new_test( 3, - exe_tx, mock_role_change(), Arc::clone(&task_manager), )) @@ -1132,7 +1380,7 @@ mod tests { InnerConnectApiWrapper::new_from_arc(Arc::new(mock_connect_learner)), ); task_manager.spawn(TaskName::Election, |n| { - CurpNode::election_task(Arc::clone(&curp), n) + CurpNode::<_, TestCE, _>::election_task(Arc::clone(&curp), n) }); sleep_secs(3).await; assert!(curp.is_leader()); diff --git a/crates/curp/src/server/gc.rs b/crates/curp/src/server/gc.rs index e1e8c7360..92af3aeb7 100644 --- a/crates/curp/src/server/gc.rs +++ b/crates/curp/src/server/gc.rs @@ -2,20 +2,18 @@ use std::time::Duration; use utils::task_manager::Listener; -use crate::{cmd::Command, server::cmd_board::CmdBoardRef}; +use crate::{cmd::Command, rpc::ProposeId, server::cmd_board::CmdBoardRef}; -// TODO: Speculative pool GC +use super::{conflict::spec_pool_new::SpeculativePoolRef, lease_manager::LeaseManagerRef}; -/// Cleanup cmd board -pub(super) async fn gc_cmd_board( +/// Garbage collects relevant objects when the client lease expires +pub(super) async fn gc_client_lease( + lease_mamanger: LeaseManagerRef, cmd_board: CmdBoardRef, + sp: SpeculativePoolRef, interval: Duration, shutdown_listener: Listener, ) { - let mut last_check_len_er = 0; - let mut last_check_len_asr = 0; - let mut last_check_len_sync = 0; - let mut last_check_len_conf = 0; #[allow(clippy::arithmetic_side_effects, clippy::ignored_unit_patterns)] // introduced by tokio select loop { @@ -23,32 +21,24 @@ pub(super) async fn gc_cmd_board( _ = tokio::time::sleep(interval) => {} _ = shutdown_listener.wait() => break, } - let mut board = cmd_board.write(); - - // last_check_len_xxx should always be smaller than board.xxx_.len(), the check is just for precaution - - if last_check_len_er <= board.er_buffer.len() { - let new_er_buffer = board.er_buffer.split_off(last_check_len_er); - board.er_buffer = new_er_buffer; - last_check_len_er = board.er_buffer.len(); - } - if last_check_len_asr <= board.asr_buffer.len() { - let new_asr_buffer = board.asr_buffer.split_off(last_check_len_asr); - board.asr_buffer = new_asr_buffer; - last_check_len_asr = board.asr_buffer.len(); - } - - if last_check_len_sync <= board.sync.len() { - let new_sync = board.sync.split_off(last_check_len_sync); - board.sync = new_sync; - last_check_len_sync = board.sync.len(); + let mut lm_w = lease_mamanger.write(); + let mut board = cmd_board.write(); + let mut sp_l = sp.lock(); + let expired_ids = lm_w.gc_expired(); + + let mut expired_propose_ids = Vec::new(); + for id in expired_ids { + if let Some(tracker) = board.trackers.get(&id) { + let incompleted_nums = tracker.all_incompleted(); + expired_propose_ids + .extend(incompleted_nums.into_iter().map(|num| ProposeId(id, num))); + } } - - if last_check_len_conf <= board.conf_buffer.len() { - let new_conf = board.conf_buffer.split_off(last_check_len_conf); - board.conf_buffer = new_conf; - last_check_len_conf = board.conf_buffer.len(); + for id in &expired_propose_ids { + let _ignore_er = board.er_buffer.swap_remove(id); + let _ignore_asr = board.asr_buffer.swap_remove(id); + sp_l.remove_by_id(id); } } } @@ -58,15 +48,17 @@ mod tests { use std::{sync::Arc, time::Duration}; use curp_test_utils::test_cmd::{TestCommand, TestCommandResult}; - use parking_lot::RwLock; + use parking_lot::{Mutex, RwLock}; use test_macros::abort_on_panic; use utils::task_manager::{tasks::TaskName, TaskManager}; use crate::{ - rpc::ProposeId, + rpc::{PoolEntry, ProposeId}, server::{ cmd_board::{CmdBoardRef, CommandBoard}, - gc::gc_cmd_board, + conflict::{spec_pool_new::SpeculativePool, test_pools::TestSpecPool}, + gc::gc_client_lease, + lease_manager::LeaseManager, }, }; @@ -75,48 +67,130 @@ mod tests { async fn cmd_board_gc_test() { let task_manager = TaskManager::new(); let board: CmdBoardRef = Arc::new(RwLock::new(CommandBoard::new())); - task_manager.spawn(TaskName::GcCmdBoard, |n| { - gc_cmd_board(Arc::clone(&board), Duration::from_millis(500), n) + let lease_manager = Arc::new(RwLock::new(LeaseManager::new())); + let lease_manager_c = Arc::clone(&lease_manager); + let sp = Arc::new(Mutex::new(SpeculativePool::new(vec![]))); + let sp_c = Arc::clone(&sp); + task_manager.spawn(TaskName::GcClientLease, |n| { + gc_client_lease( + lease_manager_c, + Arc::clone(&board), + sp_c, + Duration::from_millis(500), + n, + ) }); tokio::time::sleep(Duration::from_millis(100)).await; + let id1 = lease_manager + .write() + .grant(Some(Duration::from_millis(900))); + let id2 = lease_manager + .write() + .grant(Some(Duration::from_millis(900))); + let _ignore = board.write().tracker(id1).only_record(1); + let _ignore = board.write().tracker(id2).only_record(2); + sp.lock().insert(PoolEntry::new( + ProposeId(id1, 1), + Arc::new(TestCommand::default()), + )); + sp.lock().insert(PoolEntry::new( + ProposeId(id2, 2), + Arc::new(TestCommand::default()), + )); board .write() .er_buffer - .insert(ProposeId(1, 1), Ok(TestCommandResult::default())); + .insert(ProposeId(id1, 1), Ok(TestCommandResult::default())); tokio::time::sleep(Duration::from_millis(100)).await; board .write() .er_buffer - .insert(ProposeId(2, 2), Ok(TestCommandResult::default())); + .insert(ProposeId(id2, 2), Ok(TestCommandResult::default())); board .write() .asr_buffer - .insert(ProposeId(1, 1), Ok(0.into())); + .insert(ProposeId(id1, 1), Ok(0.into())); tokio::time::sleep(Duration::from_millis(100)).await; board .write() .asr_buffer - .insert(ProposeId(2, 2), Ok(0.into())); + .insert(ProposeId(id2, 2), Ok(0.into())); // at 600ms tokio::time::sleep(Duration::from_millis(400)).await; + let id3 = lease_manager + .write() + .grant(Some(Duration::from_millis(500))); board .write() .er_buffer - .insert(ProposeId(3, 3), Ok(TestCommandResult::default())); + .insert(ProposeId(id3, 3), Ok(TestCommandResult::default())); board .write() .asr_buffer - .insert(ProposeId(3, 3), Ok(0.into())); + .insert(ProposeId(id3, 3), Ok(0.into())); // at 1100ms, the first two kv should be removed tokio::time::sleep(Duration::from_millis(500)).await; let board = board.write(); assert_eq!(board.er_buffer.len(), 1); - assert_eq!(*board.er_buffer.get_index(0).unwrap().0, ProposeId(3, 3)); + assert_eq!(*board.er_buffer.get_index(0).unwrap().0, ProposeId(id3, 3)); assert_eq!(board.asr_buffer.len(), 1); - assert_eq!(*board.asr_buffer.get_index(0).unwrap().0, ProposeId(3, 3)); + assert_eq!(*board.asr_buffer.get_index(0).unwrap().0, ProposeId(id3, 3)); + task_manager.shutdown(true).await; + } + + #[tokio::test] + #[abort_on_panic] + async fn spec_gc_test() { + let task_manager = TaskManager::new(); + let board: CmdBoardRef = Arc::new(RwLock::new(CommandBoard::new())); + let lease_manager = Arc::new(RwLock::new(LeaseManager::new())); + let lease_manager_c = Arc::clone(&lease_manager); + let sp = Arc::new(Mutex::new(SpeculativePool::new(vec![Box::new( + TestSpecPool::default(), + )]))); + let sp_cloned = Arc::clone(&sp); + task_manager.spawn(TaskName::GcClientLease, |n| { + gc_client_lease( + lease_manager_c, + Arc::clone(&board), + sp_cloned, + Duration::from_millis(500), + n, + ) + }); + + tokio::time::sleep(Duration::from_millis(100)).await; + + let id1 = lease_manager + .write() + .grant(Some(Duration::from_millis(900))); + let id2 = lease_manager + .write() + .grant(Some(Duration::from_millis(2000))); + let _ignore = board.write().tracker(id1).only_record(1); + let cmd1 = Arc::new(TestCommand::new_put(vec![1], 1)); + sp.lock().insert(PoolEntry::new(ProposeId(id1, 1), cmd1)); + + tokio::time::sleep(Duration::from_millis(100)).await; + let _ignore = board.write().tracker(id1).only_record(2); + let cmd2 = Arc::new(TestCommand::new_put(vec![2], 1)); + sp.lock().insert(PoolEntry::new(ProposeId(id1, 2), cmd2)); + + // at 600ms + tokio::time::sleep(Duration::from_millis(400)).await; + let _ignore = board.write().tracker(id2).only_record(1); + let cmd3 = Arc::new(TestCommand::new_put(vec![3], 1)); + sp.lock() + .insert(PoolEntry::new(ProposeId(id2, 1), Arc::clone(&cmd3))); + + // at 1100ms, the first two kv should be removed + tokio::time::sleep(Duration::from_millis(500)).await; + let spec = sp.lock(); + assert_eq!(spec.len(), 1); + assert_eq!(spec.all(), vec![PoolEntry::new(ProposeId(id2, 1), cmd3)]); task_manager.shutdown(true).await; } } diff --git a/crates/curp/src/server/lease_manager.rs b/crates/curp/src/server/lease_manager.rs index c16381cfb..2ac1b6fdc 100644 --- a/crates/curp/src/server/lease_manager.rs +++ b/crates/curp/src/server/lease_manager.rs @@ -1,8 +1,9 @@ -use std::{cmp::Reverse, ops::Add, sync::Arc, time::Duration}; +use std::{cmp::Reverse, collections::HashSet, ops::Add, sync::Arc, time::Duration}; use parking_lot::RwLock; use priority_queue::PriorityQueue; use tokio::time::Instant; +use tracing::info; /// Ref to lease manager pub(crate) type LeaseManagerRef = Arc>; @@ -13,8 +14,11 @@ const DEFAULT_LEASE_TTL: Duration = Duration::from_secs(8); /// Lease manager pub(crate) struct LeaseManager { /// client_id => expired_at + /// /// expiry queue to check the smallest expired_at - pub(super) expiry_queue: PriorityQueue>, + expiry_queue: PriorityQueue>, + /// Bypassed client ids + bypassed: HashSet, } impl LeaseManager { @@ -22,11 +26,15 @@ impl LeaseManager { pub(crate) fn new() -> Self { Self { expiry_queue: PriorityQueue::new(), + bypassed: HashSet::from([12345]), } } /// Check if the client is alive pub(crate) fn check_alive(&self, client_id: u64) -> bool { + if self.bypassed.contains(&client_id) { + return true; + } if let Some(expired_at) = self.expiry_queue.get(&client_id).map(|(_, v)| v.0) { expired_at > Instant::now() } else { @@ -35,44 +43,67 @@ impl LeaseManager { } /// Generate a new client id and grant a lease - pub(crate) fn grant(&mut self) -> u64 { + pub(crate) fn grant(&mut self, ttl: Option) -> u64 { let mut client_id: u64 = rand::random(); while self.expiry_queue.get(&client_id).is_some() { client_id = rand::random(); } - let expiry = Instant::now().add(DEFAULT_LEASE_TTL); - let _ig = self.expiry_queue.push(client_id, Reverse(expiry)); - // gc all expired client id while granting a new client id - self.gc_expired(); + let expiry = Instant::now().add(ttl.unwrap_or(DEFAULT_LEASE_TTL)); + _ = self.expiry_queue.push(client_id, Reverse(expiry)); client_id } /// GC the expired client ids - pub(crate) fn gc_expired(&mut self) { + pub(crate) fn gc_expired(&mut self) -> Vec { + let mut expired = Vec::new(); while let Some(expiry) = self.expiry_queue.peek().map(|(_, v)| v.0) { if expiry > Instant::now() { - return; + break; } - let _ig = self.expiry_queue.pop(); + let (id, _) = self + .expiry_queue + .pop() + .unwrap_or_else(|| unreachable!("Expiry queue should not be empty")); + expired.push(id); } + expired } /// Renew a client id - pub(crate) fn renew(&mut self, client_id: u64) { - let expiry = Instant::now().add(DEFAULT_LEASE_TTL); - let _ig = self + pub(crate) fn renew(&mut self, client_id: u64, ttl: Option) { + if self.bypassed.contains(&client_id) { + return; + } + let expiry = Instant::now().add(ttl.unwrap_or(DEFAULT_LEASE_TTL)); + _ = self .expiry_queue .change_priority(&client_id, Reverse(expiry)); } + /// Bypass a client id, the means the client is on the server + pub(crate) fn bypass(&mut self, client_id: u64) { + if self.bypassed.insert(client_id) { + info!("bypassed client_id: {}", client_id); + } + _ = self.expiry_queue.remove(&client_id); + } + /// Clear, called when leader retires pub(crate) fn clear(&mut self) { self.expiry_queue.clear(); + self.bypassed.clear(); + } + + /// Get the online clients count (excluding bypassed clients) + pub(crate) fn online_clients(&self) -> usize { + self.expiry_queue.len() } /// Revoke a lease pub(crate) fn revoke(&mut self, client_id: u64) { - let _ig = self.expiry_queue.remove(&client_id); + _ = self.expiry_queue.remove(&client_id); + _ = self.bypassed.remove(&client_id); + info!("revoked client_id: {}", client_id); } } @@ -84,7 +115,12 @@ mod test { fn test_basic_lease_manager() { let mut lm = LeaseManager::new(); - let client_id = lm.grant(); + let client_id = lm.grant(None); + assert!(lm.check_alive(client_id)); + lm.revoke(client_id); + assert!(!lm.check_alive(client_id)); + + lm.bypass(client_id); assert!(lm.check_alive(client_id)); lm.revoke(client_id); assert!(!lm.check_alive(client_id)); @@ -94,7 +130,7 @@ mod test { async fn test_lease_expire() { let mut lm = LeaseManager::new(); - let client_id = lm.grant(); + let client_id = lm.grant(None); assert!(lm.check_alive(client_id)); tokio::time::sleep(DEFAULT_LEASE_TTL).await; assert!(!lm.check_alive(client_id)); @@ -104,10 +140,10 @@ mod test { async fn test_renew_lease() { let mut lm = LeaseManager::new(); - let client_id = lm.grant(); + let client_id = lm.grant(None); assert!(lm.check_alive(client_id)); tokio::time::sleep(DEFAULT_LEASE_TTL / 2).await; - lm.renew(client_id); + lm.renew(client_id, None); tokio::time::sleep(DEFAULT_LEASE_TTL / 2).await; assert!(lm.check_alive(client_id)); } diff --git a/crates/curp/src/server/metrics.rs b/crates/curp/src/server/metrics.rs index bcc9ba658..e0a9e31c1 100644 --- a/crates/curp/src/server/metrics.rs +++ b/crates/curp/src/server/metrics.rs @@ -120,8 +120,8 @@ impl Metrics { let sp_size = curp.spec_pool().lock().len(); observer.observe_u64(&sp_cnt, sp_size.numeric_cast(), &[]); - let client_ids = curp.lease_manager().read().expiry_queue.len(); - observer.observe_u64(&online_clients, client_ids.numeric_cast(), &[]); + let client_count = curp.lease_manager().read().online_clients(); + observer.observe_u64(&online_clients, client_count.numeric_cast(), &[]); let commit_index = curp.commit_index(); let last_log_index = curp.last_log_index(); diff --git a/crates/curp/src/server/mod.rs b/crates/curp/src/server/mod.rs index 8ee55a599..10e4b23f4 100644 --- a/crates/curp/src/server/mod.rs +++ b/crates/curp/src/server/mod.rs @@ -1,6 +1,7 @@ use std::{fmt::Debug, sync::Arc}; use engine::SnapshotAllocator; +use flume::r#async::RecvStream; use tokio::sync::broadcast; #[cfg(not(madsim))] use tonic::transport::ClientTlsConfig; @@ -14,20 +15,25 @@ pub use self::{ conflict::{spec_pool_new::SpObject, uncommitted_pool::UcpObject}, raw_curp::RawCurp, }; +use crate::rpc::{OpResponse, RecordRequest, RecordResponse}; use crate::{ cmd::{Command, CommandExecutor}, members::{ClusterInfo, ServerId}, role_change::RoleChange, rpc::{ - AppendEntriesRequest, AppendEntriesResponse, FetchClusterRequest, FetchClusterResponse, - FetchReadStateRequest, FetchReadStateResponse, InstallSnapshotRequest, - InstallSnapshotResponse, LeaseKeepAliveMsg, MoveLeaderRequest, MoveLeaderResponse, - ProposeConfChangeRequest, ProposeConfChangeResponse, ProposeRequest, ProposeResponse, + connect::Bypass, AppendEntriesRequest, AppendEntriesResponse, FetchClusterRequest, + FetchClusterResponse, FetchReadStateRequest, FetchReadStateResponse, + InstallSnapshotRequest, InstallSnapshotResponse, LeaseKeepAliveMsg, MoveLeaderRequest, + MoveLeaderResponse, ProposeConfChangeRequest, ProposeConfChangeResponse, ProposeRequest, PublishRequest, PublishResponse, ShutdownRequest, ShutdownResponse, TriggerShutdownRequest, TriggerShutdownResponse, TryBecomeLeaderNowRequest, TryBecomeLeaderNowResponse, - VoteRequest, VoteResponse, WaitSyncedRequest, WaitSyncedResponse, + VoteRequest, VoteResponse, }, }; +use crate::{ + response::ResponseSender, + rpc::{ReadIndexRequest, ReadIndexResponse}, +}; /// Command worker to do execution and after sync mod cmd_worker; @@ -59,14 +65,15 @@ mod metrics; pub use storage::{db::DB, StorageApi, StorageError}; /// The Rpc Server to handle rpc requests +/// /// This Wrapper is introduced due to the `MadSim` rpc lib #[derive(Debug)] -pub struct Rpc { +pub struct Rpc, RC: RoleChange> { /// The inner server is wrapped in an Arc so that its state can be shared while cloning the rpc wrapper - inner: Arc>, + inner: Arc>, } -impl Clone for Rpc { +impl, RC: RoleChange> Clone for Rpc { #[inline] fn clone(&self) -> Self { Self { @@ -76,26 +83,51 @@ impl Clone for Rpc { } #[tonic::async_trait] -impl crate::rpc::Protocol for Rpc { - #[instrument(skip_all, name = "curp_propose")] - async fn propose( +impl, RC: RoleChange> crate::rpc::Protocol for Rpc { + type ProposeStreamStream = RecvStream<'static, Result>; + + #[instrument(skip_all, name = "propose_stream")] + async fn propose_stream( &self, request: tonic::Request, - ) -> Result, tonic::Status> { - request.metadata().extract_span(); + ) -> Result, tonic::Status> { + let bypassed = request.metadata().is_bypassed(); + let (tx, rx) = flume::bounded(2); + let resp_tx = Arc::new(ResponseSender::new(tx)); + self.inner + .propose_stream(&request.into_inner(), resp_tx, bypassed) + .await?; + + Ok(tonic::Response::new(rx.into_stream())) + } + + #[instrument(skip_all, name = "record")] + async fn record( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { Ok(tonic::Response::new( - self.inner.propose(request.into_inner()).await?, + self.inner.record(&request.into_inner())?, )) } + #[instrument(skip_all, name = "read_index")] + async fn read_index( + &self, + _request: tonic::Request, + ) -> Result, tonic::Status> { + Ok(tonic::Response::new(self.inner.read_index()?)) + } + #[instrument(skip_all, name = "curp_shutdown")] async fn shutdown( &self, request: tonic::Request, ) -> Result, tonic::Status> { + let bypassed = request.metadata().is_bypassed(); request.metadata().extract_span(); Ok(tonic::Response::new( - self.inner.shutdown(request.into_inner()).await?, + self.inner.shutdown(request.into_inner(), bypassed).await?, )) } @@ -104,9 +136,12 @@ impl crate::rpc::Protocol for Rpc { &self, request: tonic::Request, ) -> Result, tonic::Status> { + let bypassed = request.metadata().is_bypassed(); request.metadata().extract_span(); Ok(tonic::Response::new( - self.inner.propose_conf_change(request.into_inner()).await?, + self.inner + .propose_conf_change(request.into_inner(), bypassed) + .await?, )) } @@ -115,20 +150,10 @@ impl crate::rpc::Protocol for Rpc { &self, request: tonic::Request, ) -> Result, tonic::Status> { + let bypassed = request.metadata().is_bypassed(); request.metadata().extract_span(); Ok(tonic::Response::new( - self.inner.publish(request.into_inner())?, - )) - } - - #[instrument(skip_all, name = "curp_wait_synced")] - async fn wait_synced( - &self, - request: tonic::Request, - ) -> Result, tonic::Status> { - request.metadata().extract_span(); - Ok(tonic::Response::new( - self.inner.wait_synced(request.into_inner()).await?, + self.inner.publish(request.into_inner(), bypassed)?, )) } @@ -176,7 +201,9 @@ impl crate::rpc::Protocol for Rpc { } #[tonic::async_trait] -impl crate::rpc::InnerProtocol for Rpc { +impl, RC: RoleChange> crate::rpc::InnerProtocol + for Rpc +{ #[instrument(skip_all, name = "curp_append_entries")] async fn append_entries( &self, @@ -193,7 +220,7 @@ impl crate::rpc::InnerProtocol for Rpc { request: tonic::Request, ) -> Result, tonic::Status> { Ok(tonic::Response::new( - self.inner.vote(request.into_inner()).await?, + self.inner.vote(&request.into_inner())?, )) } @@ -203,7 +230,7 @@ impl crate::rpc::InnerProtocol for Rpc { request: tonic::Request, ) -> Result, tonic::Status> { Ok(tonic::Response::new( - self.inner.trigger_shutdown(request.get_ref()), + self.inner.trigger_shutdown(*request.get_ref()), )) } @@ -229,13 +256,15 @@ impl crate::rpc::InnerProtocol for Rpc { } } -impl Rpc { +impl, RC: RoleChange> Rpc { /// New `Rpc` + /// /// # Panics + /// /// Panic if storage creation failed #[inline] #[allow(clippy::too_many_arguments)] // TODO: refactor this use builder pattern - pub async fn new>( + pub async fn new( cluster_info: Arc, is_leader: bool, executor: Arc, @@ -278,12 +307,13 @@ impl Rpc { /// Run a new rpc server on a specific addr, designed to be used in the tests /// /// # Errors - /// `ServerError::ParsingError` if parsing failed for the local server address - /// `ServerError::RpcError` if any rpc related error met + /// + /// - `ServerError::ParsingError` if parsing failed for the local server address + /// - `ServerError::RpcError` if any rpc related error met #[cfg(madsim)] #[allow(clippy::too_many_arguments)] #[inline] - pub async fn run_from_addr( + pub async fn run_from_addr( cluster_info: Arc, is_leader: bool, addr: std::net::SocketAddr, @@ -296,15 +326,14 @@ impl Rpc { client_tls_config: Option, sps: Vec>, ucps: Vec>, - ) -> Result<(), crate::error::ServerError> - where - CE: CommandExecutor, - { + ) -> Result<(), crate::error::ServerError> { use utils::task_manager::tasks::TaskName; use crate::rpc::{InnerProtocolServer, ProtocolServer}; - let n = task_manager.get_shutdown_listener(TaskName::TonicServer); + let n = task_manager + .get_shutdown_listener(TaskName::TonicServer) + .unwrap_or_else(|| unreachable!("cluster should never shutdown before start")); let server = Self::new( cluster_info, is_leader, diff --git a/crates/curp/src/server/raw_curp/log.rs b/crates/curp/src/server/raw_curp/log.rs index f7996649f..5d25e3f3b 100644 --- a/crates/curp/src/server/raw_curp/log.rs +++ b/crates/curp/src/server/raw_curp/log.rs @@ -11,8 +11,7 @@ use std::{ use clippy_utilities::NumericCast; use itertools::Itertools; -use tokio::sync::mpsc; -use tracing::{error, warn}; +use tracing::warn; use crate::{ cmd::Command, @@ -69,8 +68,11 @@ impl From> for LogRange { } } +/// ```text /// Curp logs +/// /// There exists a fake log entry 0 whose term equals 0 +/// /// For the leader, there should never be a gap between snapshot and entries /// /// Examples: @@ -89,6 +91,7 @@ impl From> for LogRange { /// (1, `batch_end[1]`) = (1,2), which means the `entries[1..=2]` is a valid batch whose size is 5+6=11, equal to the `batch_limit` /// ... /// (`first_idx_in_cur_batch`, `batch_end[first_idx_in_cur_batch]`) = (4, 0), which means the `entries[4..]` is a valid batch (aka. current batch) whose size (aka. `cur_batch_size`) is 3+4+2=9, less than the `batch_limit` +/// ``` pub(super) struct Log { /// Log entries, should be persisted /// A VecDeque to store log entries, it will be serialized and persisted @@ -115,8 +118,6 @@ pub(super) struct Log { pub(super) last_exe: LogIndex, /// Contexts of fallback log entries pub(super) fallback_contexts: HashMap>, - /// Tx to send log entries to persist task - log_tx: mpsc::UnboundedSender>>, /// Entries to keep in memory entries_cap: usize, } @@ -314,13 +315,12 @@ type ConfChangeEntries = Vec>>; /// Fallback indexes type type FallbackIndexes = HashSet; +/// Type returned when append success +type AppendSuccess = (Vec>>, ConfChangeEntries, FallbackIndexes); + impl Log { /// Create a new log - pub(super) fn new( - log_tx: mpsc::UnboundedSender>>, - batch_limit: u64, - entries_cap: usize, - ) -> Self { + pub(super) fn new(batch_limit: u64, entries_cap: usize) -> Self { Self { entries: VecDeque::with_capacity(entries_cap), batch_end: VecDeque::with_capacity(entries_cap), @@ -332,7 +332,6 @@ impl Log { base_term: 0, last_as: 0, last_exe: 0, - log_tx, fallback_contexts: HashMap::new(), entries_cap, } @@ -377,7 +376,8 @@ impl Log { entries: Vec>, prev_log_index: LogIndex, prev_log_term: u64, - ) -> Result<(ConfChangeEntries, FallbackIndexes), Vec>> { + ) -> Result, Vec>> { + let mut to_persist = Vec::with_capacity(entries.len()); let mut conf_changes = vec![]; let mut need_fallback_indexes = HashSet::new(); // check if entries can be appended @@ -423,17 +423,10 @@ impl Log { bincode::serialized_size(&entry).expect("log entry {entry:?} cannot be serialized"), ); - self.send_persist(entry); + to_persist.push(entry); } - Ok((conf_changes, need_fallback_indexes)) - } - - /// Send log entries to persist task - pub(super) fn send_persist(&self, entry: Arc>) { - if let Err(err) = self.log_tx.send(entry) { - error!("failed to send log to persist, {err}"); - } + Ok((to_persist, conf_changes, need_fallback_indexes)) } /// Check if the candidate's log is up-to-date @@ -448,18 +441,20 @@ impl Log { } /// Push a log entry into the end of log + // FIXME: persistent other log entries + // TODO: Avoid allocation during locking pub(super) fn push( &mut self, term: u64, propose_id: ProposeId, entry: impl Into>, - ) -> Result>, bincode::Error> { + ) -> Arc> { let index = self.last_log_index() + 1; let entry = Arc::new(LogEntry::new(index, term, propose_id, entry)); - let size = bincode::serialized_size(&entry)?; + let size = bincode::serialized_size(&entry) + .unwrap_or_else(|_| unreachable!("bindcode serialization should always succeed")); self.push_back(Arc::clone(&entry), size); - self.send_persist(Arc::clone(&entry)); - Ok(entry) + entry } /// check whether the log entry range [li,..) exceeds the batch limit or not @@ -615,9 +610,7 @@ mod tests { #[test] fn test_log_up_to_date() { - let (log_tx, _log_rx) = mpsc::unbounded_channel(); - let mut log = - Log::::new(log_tx, default_batch_max_size(), default_log_entries_cap()); + let mut log = Log::::new(default_batch_max_size(), default_log_entries_cap()); let result = log.try_append_entries( vec![ LogEntry::new(1, 1, ProposeId(0, 0), Arc::new(TestCommand::default())), @@ -637,9 +630,7 @@ mod tests { #[test] fn try_append_entries_will_remove_inconsistencies() { - let (log_tx, _log_rx) = mpsc::unbounded_channel(); - let mut log = - Log::::new(log_tx, default_batch_max_size(), default_log_entries_cap()); + let mut log = Log::::new(default_batch_max_size(), default_log_entries_cap()); let result = log.try_append_entries( vec![ LogEntry::new(1, 1, ProposeId(0, 1), Arc::new(TestCommand::default())), @@ -666,9 +657,7 @@ mod tests { #[test] fn try_append_entries_will_not_append() { - let (log_tx, _log_rx) = mpsc::unbounded_channel(); - let mut log = - Log::::new(log_tx, default_batch_max_size(), default_log_entries_cap()); + let mut log = Log::::new(default_batch_max_size(), default_log_entries_cap()); let result = log.try_append_entries( vec![LogEntry::new( 1, @@ -704,16 +693,14 @@ mod tests { #[test] fn get_from_should_success() { - let (tx, _rx) = mpsc::unbounded_channel(); - let mut log = - Log::::new(tx, default_batch_max_size(), default_log_entries_cap()); + let mut log = Log::::new(default_batch_max_size(), default_log_entries_cap()); // Note: this test must use the same test command to ensure the size of the entry is fixed let test_cmd = Arc::new(TestCommand::default()); let _res = repeat(Arc::clone(&test_cmd)) .take(10) .enumerate() - .map(|(idx, cmd)| log.push(1, ProposeId(0, idx.numeric_cast()), cmd).unwrap()) + .map(|(idx, cmd)| log.push(1, ProposeId(0, idx.numeric_cast()), cmd)) .collect::>(); let log_entry_size = log.entries[0].size; @@ -798,9 +785,7 @@ mod tests { ) }) .collect::>>(); - let (tx, _rx) = mpsc::unbounded_channel(); - let mut log = - Log::::new(tx, default_batch_max_size(), default_log_entries_cap()); + let mut log = Log::::new(default_batch_max_size(), default_log_entries_cap()); log.restore_entries(entries).unwrap(); assert_eq!(log.entries.len(), 10); @@ -809,12 +794,10 @@ mod tests { #[test] fn compact_test() { - let (log_tx, _log_rx) = mpsc::unbounded_channel(); - let mut log = Log::::new(log_tx, default_batch_max_size(), 10); + let mut log = Log::::new(default_batch_max_size(), 10); for i in 0..30 { - log.push(0, ProposeId(0, i), Arc::new(TestCommand::default())) - .unwrap(); + log.push(0, ProposeId(0, i), Arc::new(TestCommand::default())); } log.last_as = 22; log.last_exe = 22; @@ -827,11 +810,9 @@ mod tests { #[test] fn get_from_should_success_after_compact() { - let (log_tx, _log_rx) = mpsc::unbounded_channel(); - let mut log = Log::::new(log_tx, default_batch_max_size(), 10); + let mut log = Log::::new(default_batch_max_size(), 10); for i in 0..30 { - log.push(0, ProposeId(0, i), Arc::new(TestCommand::default())) - .unwrap(); + log.push(0, ProposeId(0, i), Arc::new(TestCommand::default())); } let log_entry_size = log.entries[0].size; log.set_batch_limit(2 * log_entry_size); @@ -867,8 +848,7 @@ mod tests { #[test] fn batch_info_should_update_correctly_after_truncated() { - let (log_tx, _log_rx) = mpsc::unbounded_channel(); - let mut log = Log::::new(log_tx, 11, 10); + let mut log = Log::::new(11, 10); let mock_entries_sizes = vec![1, 5, 6, 2, 3, 4, 5]; let test_cmd = Arc::new(TestCommand::default()); diff --git a/crates/curp/src/server/raw_curp/mod.rs b/crates/curp/src/server/raw_curp/mod.rs index e3f24d22b..b6f529c12 100644 --- a/crates/curp/src/server/raw_curp/mod.rs +++ b/crates/curp/src/server/raw_curp/mod.rs @@ -10,7 +10,7 @@ #![allow(clippy::arithmetic_side_effects)] // u64 is large enough and won't overflow use std::{ - cmp::min, + cmp::{self, min}, collections::{HashMap, HashSet}, fmt::Debug, sync::{ @@ -23,10 +23,11 @@ use clippy_utilities::{NumericCast, OverflowArithmetic}; use dashmap::DashMap; use derive_builder::Builder; use event_listener::Event; +use futures::Future; use itertools::Itertools; use opentelemetry::KeyValue; use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard, RwLockWriteGuard}; -use tokio::sync::{broadcast, mpsc, oneshot}; +use tokio::sync::{broadcast, oneshot}; #[cfg(not(madsim))] use tonic::transport::ClientTlsConfig; use tracing::{ @@ -37,6 +38,7 @@ use tracing::{ #[cfg(madsim)] use utils::ClientTlsConfig; use utils::{ + barrier::IdBarrier, config::CurpConfig, parking_lot_lock::{MutexMap, RwLockMap}, task_manager::TaskManager, @@ -47,8 +49,9 @@ use self::{ state::{CandidateState, LeaderState, State}, }; use super::{ - cmd_worker::CEEventTxApi, + cmd_board::CommandBoard, conflict::{spec_pool_new::SpeculativePool, uncommitted_pool::UncommittedPool}, + curp_node::TaskType, lease_manager::LeaseManagerRef, storage::StorageApi, DB, @@ -58,11 +61,12 @@ use crate::{ log_entry::{EntryData, LogEntry}, members::{ClusterInfo, ServerId}, quorum, recover_quorum, + response::ResponseSender, role_change::RoleChange, rpc::{ connect::{InnerConnectApi, InnerConnectApiWrapper}, ConfChange, ConfChangeType, CurpError, IdSet, Member, PoolEntry, ProposeId, PublishRequest, - ReadState, + ReadState, Redirect, }, server::{ cmd_board::CmdBoardRef, @@ -119,10 +123,6 @@ pub(super) struct RawCurpArgs { lease_manager: LeaseManagerRef, /// Config cfg: Arc, - /// Tx to send cmds to execute and do after sync - cmd_tx: Arc>, - /// Tx to send log entries - log_tx: mpsc::UnboundedSender>>, /// Role change callback role_change: RC, /// Task manager @@ -149,6 +149,12 @@ pub(super) struct RawCurpArgs { spec_pool: Arc>>, /// Uncommitted pool uncommitted_pool: Arc>>, + /// Tx to send entries to after_sync + as_tx: flume::Sender>, + /// Response Senders + resp_txs: Arc>>>, + /// Barrier for waiting unsynced commands + id_barrier: Arc>, } impl RawCurpBuilder { @@ -162,18 +168,13 @@ impl RawCurpBuilder { )); let lst = LeaderState::new(&args.cluster_info.peers_ids()); let cst = Mutex::new(CandidateState::new(args.cluster_info.all_ids().into_iter())); - let log = RwLock::new(Log::new( - args.log_tx, - args.cfg.batch_max_size, - args.cfg.log_entries_cap, - )); + let log = RwLock::new(Log::new(args.cfg.batch_max_size, args.cfg.log_entries_cap)); let ctx = Context::builder() .cluster_info(args.cluster_info) .cb(args.cmd_board) .lm(args.lease_manager) .cfg(args.cfg) - .cmd_tx(args.cmd_tx) .sync_events(args.sync_events) .role_change(args.role_change) .connects(args.connects) @@ -181,6 +182,9 @@ impl RawCurpBuilder { .client_tls_config(args.client_tls_config) .spec_pool(args.spec_pool) .uncommitted_pool(args.uncommitted_pool) + .as_tx(args.as_tx) + .resp_txs(args.resp_txs) + .id_barrier(args.id_barrier) .build() .map_err(|e| match e { ContextBuilderError::UninitializedField(s) => { @@ -294,6 +298,10 @@ enum Role { } /// Relevant context for Curp +/// +/// WARN: To avoid deadlock, the lock order should be: +/// 1. `spec_pool` +/// 2. `uncommitted_pool` #[derive(Builder)] #[builder(build_fn(skip))] struct Context { @@ -313,8 +321,6 @@ struct Context { /// Election tick #[builder(setter(skip))] election_tick: AtomicU8, - /// Tx to send cmds to execute and do after sync - cmd_tx: Arc>, /// Followers sync event trigger sync_events: DashMap>, /// Become leader event @@ -339,6 +345,13 @@ struct Context { spec_pool: Arc>>, /// Uncommitted pool uncommitted_pool: Arc>>, + /// Tx to send entries to after_sync + as_tx: flume::Sender>, + /// Response Senders + // TODO: this could be replaced by a queue + resp_txs: Arc>>>, + /// Barrier for waiting unsynced commands + id_barrier: Arc>, } impl Context { @@ -371,10 +384,6 @@ impl ContextBuilder { }, leader_tx: broadcast::channel(1).0, election_tick: AtomicU8::new(0), - cmd_tx: match self.cmd_tx.take() { - Some(value) => value, - None => return Err(ContextBuilderError::UninitializedField("cmd_tx")), - }, sync_events: match self.sync_events.take() { Some(value) => value, None => return Err(ContextBuilderError::UninitializedField("sync_events")), @@ -407,6 +416,18 @@ impl ContextBuilder { Some(value) => value, None => return Err(ContextBuilderError::UninitializedField("uncommitted_pool")), }, + as_tx: match self.as_tx.take() { + Some(value) => value, + None => return Err(ContextBuilderError::UninitializedField("as_tx")), + }, + resp_txs: match self.resp_txs.take() { + Some(value) => value, + None => return Err(ContextBuilderError::UninitializedField("resp_txs")), + }, + id_barrier: match self.id_barrier.take() { + Some(value) => value, + None => return Err(ContextBuilderError::UninitializedField("id_barrier")), + }, }) } } @@ -455,72 +476,158 @@ impl RawCurp { } } +/// Term, entries +type AppendEntriesSuccess = (u64, Vec>>); +/// Term, index +type AppendEntriesFailure = (u64, LogIndex); + // Curp handlers +// TODO: Tidy up the handlers +// Possible improvements: +// * split metrics collection from CurpError into a separate function +// * split the handlers into separate modules impl RawCurp { - /// Handle `propose` request - /// Return `true` if the leader speculatively executed the command - pub(super) fn handle_propose( - &self, - propose_id: ProposeId, - cmd: Arc, - ) -> Result { - debug!("{} gets proposal for cmd({})", self.id(), propose_id); - let mut conflict = self - .ctx - .spec_pool - .map_lock(|mut sp_l| sp_l.insert(PoolEntry::new(propose_id, Arc::clone(&cmd)))) - .is_some(); - + /// Checks the if term are up-to-date + pub(super) fn check_term(&self, term: u64) -> Result<(), CurpError> { let st_r = self.st.read(); - // Non-leader doesn't need to sync or execute - if st_r.role != Role::Leader { - if conflict { - metrics::get() - .proposals_failed - .add(1, &[KeyValue::new("reason", "follower key conflict")]); - return Err(CurpError::key_conflict()); - } - return Ok(false); - } - if self.lst.get_transferee().is_some() { - return Err(CurpError::LeaderTransfer("leader transferring".to_owned())); + + // Rejects the request + // When `st_r.term > term`, the client is using an outdated leader + // When `st_r.term < term`, the current node is a zombie + match st_r.term.cmp(&term) { + // Current node is a zombie + cmp::Ordering::Less => Err(CurpError::Zombie(())), + cmp::Ordering::Greater => Err(CurpError::Redirect(Redirect { + leader_id: st_r.leader_id.map(Into::into), + term: st_r.term, + })), + cmp::Ordering::Equal => Ok(()), } - if !self + } + + /// Handles record + pub(super) fn follower_record(&self, propose_id: ProposeId, cmd: &Arc) -> bool { + let conflict = self .ctx - .cb - .map_write(|mut cb_w| cb_w.sync.insert(propose_id)) - { + .spec_pool + .lock() + .insert(PoolEntry::new(propose_id, Arc::clone(cmd))) + .is_some(); + if conflict { metrics::get() .proposals_failed - .add(1, &[KeyValue::new("reason", "duplicated proposal")]); - return Err(CurpError::duplicated()); + .add(1, &[KeyValue::new("reason", "follower key conflict")]); } + conflict + } - // leader also needs to check if the cmd conflicts un-synced commands - conflict |= self - .ctx - .uncommitted_pool - .map_lock(|mut ucp_l| ucp_l.insert(PoolEntry::new(propose_id, Arc::clone(&cmd)))); + /// Handles record + pub(super) fn leader_record(&self, entries: impl Iterator>) -> Vec { + let mut sp_l = self.ctx.spec_pool.lock(); + let mut ucp_l = self.ctx.uncommitted_pool.lock(); + let mut conflicts = Vec::new(); + for entry in entries { + let mut conflict = sp_l.insert(entry.clone()).is_some(); + conflict |= ucp_l.insert(&entry); + conflicts.push(conflict); + } + metrics::get().proposals_failed.add( + conflicts.iter().filter(|c| **c).count().numeric_cast(), + &[KeyValue::new("reason", "leader key conflict")], + ); + conflicts + } + /// Handles leader propose + pub(super) fn push_logs( + &self, + proposes: Vec<(Arc, ProposeId, u64, Arc)>, + ) -> Vec>> { + let term = proposes + .first() + .unwrap_or_else(|| unreachable!("no propose in proposes")) + .2; + let mut log_entries = Vec::with_capacity(proposes.len()); + let mut to_process = Vec::with_capacity(proposes.len()); let mut log_w = self.log.write(); - let entry = log_w.push(st_r.term, propose_id, cmd).map_err(|e| { - metrics::get() - .proposals_failed - .add(1, &[KeyValue::new("reason", "log serialize failed")]); - e - })?; - debug!("{} gets new log[{}]", self.id(), entry.index); + self.ctx.resp_txs.map_lock(|mut tx_map| { + for propose in proposes { + let (cmd, id, _term, resp_tx) = propose; + let entry = log_w.push(term, id, cmd); + let index = entry.index; + let conflict = resp_tx.is_conflict(); + to_process.push((index, conflict)); + log_entries.push(entry); + assert!( + tx_map.insert(index, Arc::clone(&resp_tx)).is_none(), + "Should not insert resp_tx twice" + ); + } + }); + self.entry_process_multi(&mut log_w, &to_process, term); - self.entry_process(&mut log_w, entry, conflict, st_r.term); + let log_r = RwLockWriteGuard::downgrade(log_w); + self.persistent_log_entries( + &log_entries.iter().map(Arc::as_ref).collect::>(), + &log_r, + ); - if conflict { - metrics::get() - .proposals_failed - .add(1, &[KeyValue::new("reason", "leader key conflict")]); - return Err(CurpError::key_conflict()); + log_entries + } + + /// Persistent log entries + /// + /// NOTE: A `&Log` is required because we do not want the `Log` structure gets mutated + /// during the persistent + #[allow(clippy::panic)] + #[allow(dropping_references)] + fn persistent_log_entries(&self, entries: &[&LogEntry], _log: &Log) { + // We panic when the log persistence fails because it likely indicates an unrecoverable error. + // Our WAL implementation does not support rollback on failure, as a file write syscall is not + // guaranteed to be atomic. + if let Err(e) = self.ctx.curp_storage.put_log_entries(entries) { + panic!("log persistent failed: {e}"); } + } - Ok(true) + /// Wait synced for all conflict commands + pub(super) fn wait_conflicts_synced(&self, cmd: Arc) -> impl Future + Send { + let conflict_cmds: Vec<_> = self + .ctx + .uncommitted_pool + .lock() + .all_conflict(&PoolEntry::new(ProposeId::default(), cmd)) + .into_iter() + .map(|e| e.id) + .collect(); + self.ctx.id_barrier.wait_all(conflict_cmds) + } + + /// Wait all logs in previous term have been applied to state machine + pub(super) fn wait_no_op_applied(&self) -> Box + Send + Unpin> { + // if the leader is at term 1, it won't commit a no-op log + if self.term() == 1 { + return Box::new(futures::future::ready(())); + } + Box::new(self.lst.wait_no_op_applied()) + } + + /// Sets the no-op log as applied + pub(super) fn set_no_op_applied(&self) { + self.lst.set_no_op_applied(); + } + + /// Trigger the barrier of the given inflight id. + pub(super) fn trigger(&self, propose_id: &ProposeId) { + self.ctx.id_barrier.trigger(propose_id); + } + + /// Returns `CurpError::LeaderTransfer` if the leadership is transferring + pub(super) fn check_leader_transfer(&self) -> Result<(), CurpError> { + if self.lst.get_transferee().is_some() { + return Err(CurpError::LeaderTransfer("leader transferring".to_owned())); + } + Ok(()) } /// Handle `shutdown` request @@ -532,17 +639,15 @@ impl RawCurp { if self.lst.get_transferee().is_some() { return Err(CurpError::LeaderTransfer("leader transferring".to_owned())); } + self.deduplicate(propose_id, None)?; let mut log_w = self.log.write(); - let entry = log_w - .push(st_r.term, propose_id, EntryData::Shutdown) - .map_err(|e| { - metrics::get() - .proposals_failed - .add(1, &[KeyValue::new("reason", "log serialize failed")]); - e - })?; + let entry = log_w.push(st_r.term, propose_id, EntryData::Shutdown); debug!("{} gets new log[{}]", self.id(), entry.index); - self.entry_process(&mut log_w, entry, true, st_r.term); + self.entry_process_single(&mut log_w, entry.as_ref(), true, st_r.term); + + let log_r = RwLockWriteGuard::downgrade(log_w); + self.persistent_log_entries(&[entry.as_ref()], &log_r); + Ok(()) } @@ -568,37 +673,25 @@ impl RawCurp { } self.check_new_config(&conf_changes)?; - let mut conflict = self - .ctx - .spec_pool - .lock() - .insert(PoolEntry::new(propose_id, conf_changes.clone())) - .is_some(); - conflict |= self - .ctx - .uncommitted_pool - .lock() - .insert(PoolEntry::new(propose_id, conf_changes.clone())); - + self.deduplicate(propose_id, None)?; let mut log_w = self.log.write(); - let entry = log_w - .push(st_r.term, propose_id, conf_changes.clone()) - .map_err(|e| { - metrics::get() - .proposals_failed - .add(1, &[KeyValue::new("reason", "log serialize failed")]); - e - })?; + let entry = log_w.push(st_r.term, propose_id, conf_changes.clone()); debug!("{} gets new log[{}]", self.id(), entry.index); - let (addrs, name, is_learner) = self.apply_conf_change(conf_changes); + let apply_opt = self.apply_conf_change(conf_changes); self.ctx .last_conf_change_idx .store(entry.index, Ordering::Release); - let _ig = log_w.fallback_contexts.insert( - entry.index, - FallbackContext::new(Arc::clone(&entry), addrs, name, is_learner), - ); - self.entry_process(&mut log_w, entry, conflict, st_r.term); + if let Some((addrs, name, is_learner)) = apply_opt { + let _ig = log_w.fallback_contexts.insert( + entry.index, + FallbackContext::new(Arc::clone(&entry), addrs, name, is_learner), + ); + } + self.entry_process_single(&mut log_w, &entry, false, st_r.term); + + let log_r = RwLockWriteGuard::downgrade(log_w); + self.persistent_log_entries(&[entry.as_ref()], &log_r); + Ok(()) } @@ -616,15 +709,17 @@ impl RawCurp { if self.lst.get_transferee().is_some() { return Err(CurpError::leader_transfer("leader transferring")); } + + self.deduplicate(req.propose_id(), None)?; + let mut log_w = self.log.write(); - let entry = log_w.push(st_r.term, req.propose_id(), req).map_err(|e| { - metrics::get() - .proposals_failed - .add(1, &[KeyValue::new("reason", "log serialize failed")]); - e - })?; + let entry = log_w.push(st_r.term, req.propose_id(), req); debug!("{} gets new log[{}]", self.id(), entry.index); - self.entry_process(&mut log_w, entry, false, st_r.term); + self.entry_process_single(&mut log_w, entry.as_ref(), false, st_r.term); + + let log_r = RwLockWriteGuard::downgrade(log_w); + self.persistent_log_entries(&[entry.as_ref()], &log_r); + Ok(()) } @@ -632,20 +727,20 @@ impl RawCurp { pub(super) fn handle_lease_keep_alive(&self, client_id: u64) -> Option { let mut lm_w = self.ctx.lm.write(); if client_id == 0 { - return Some(lm_w.grant()); + return Some(lm_w.grant(None)); } if lm_w.check_alive(client_id) { - lm_w.renew(client_id); + lm_w.renew(client_id, None); None } else { metrics::get().client_id_revokes.add(1, &[]); lm_w.revoke(client_id); - Some(lm_w.grant()) + Some(lm_w.grant(None)) } } /// Handle `append_entries` - /// Return `Ok(term)` if succeeds + /// Return `Ok(term, entries)` if succeeds /// Return `Err(term, hint_index)` if fails pub(super) fn handle_append_entries( &self, @@ -655,7 +750,7 @@ impl RawCurp { prev_log_term: u64, entries: Vec>, leader_commit: LogIndex, - ) -> Result { + ) -> Result, AppendEntriesFailure> { if entries.is_empty() { trace!( "{} received heartbeat from {}: term({}), commit({}), prev_log_index({}), prev_log_term({})", @@ -692,7 +787,7 @@ impl RawCurp { // append log entries let mut log_w = self.log.write(); - let (cc_entries, fallback_indexes) = log_w + let (to_persist, cc_entries, fallback_indexes) = log_w .try_append_entries(entries, prev_log_index, prev_log_term) .map_err(|_ig| (term, log_w.commit_index + 1))?; // fallback overwritten conf change entries @@ -711,7 +806,9 @@ impl RawCurp { let EntryData::ConfChange(ref cc) = e.entry_data else { unreachable!("cc_entry should be conf change entry"); }; - let (addrs, name, is_learner) = self.apply_conf_change(cc.clone()); + let Some((addrs, name, is_learner)) = self.apply_conf_change(cc.clone()) else { + continue; + }; let _ig = log_w.fallback_contexts.insert( e.index, FallbackContext::new(Arc::clone(&e), addrs, name, is_learner), @@ -723,7 +820,7 @@ impl RawCurp { if prev_commit_index < log_w.commit_index { self.apply(&mut *log_w); } - Ok(term) + Ok((term, to_persist)) } /// Handle `append_entries` response @@ -952,7 +1049,8 @@ impl RawCurp { let prev_last_log_index = log_w.last_log_index(); // TODO: Generate client id in the same way as client let propose_id = ProposeId(rand::random(), 0); - let _ignore = log_w.push(st_w.term, propose_id, EntryData::Empty); + let entry = log_w.push(st_w.term, propose_id, EntryData::Empty); + self.persistent_log_entries(&[&entry], &log_w); self.recover_from_spec_pools(&st_w, &mut log_w, spec_pools); self.recover_ucp_from_log(&log_w); let last_log_index = log_w.last_log_index(); @@ -1059,7 +1157,7 @@ impl RawCurp { let ids: Vec<_> = self .ctx .uncommitted_pool - .map_lock(|ucp| ucp.all_conflict(PoolEntry::new(ProposeId::default(), cmd))) + .map_lock(|ucp| ucp.all_conflict(&PoolEntry::new(ProposeId::default(), cmd))) .into_iter() .map(|entry| entry.id) .collect(); @@ -1195,12 +1293,15 @@ impl RawCurp { // the leader will take a snapshot itself every time `sync` is called in effort to // calibrate it. Since taking a snapshot will block the leader's execute workers, we should // not take snapshot so often. A better solution would be to keep a snapshot cache. - Some(SyncAction::Snapshot(self.ctx.cmd_tx.send_snapshot( - SnapshotMeta { - last_included_index: entry.index, - last_included_term: entry.term, - }, - ))) + let meta = SnapshotMeta { + last_included_index: entry.index, + last_included_term: entry.term, + }; + let (tx, rx) = oneshot::channel(); + if let Err(e) = self.ctx.as_tx.send(TaskType::Snapshot(meta, tx)) { + error!("failed to send task to after sync: {e}"); + } + Some(SyncAction::Snapshot(rx)) } else { let (prev_log_index, prev_log_term) = log_r.get_prev_entry_info(next_index); let entries = log_r.get_from(next_index); @@ -1253,13 +1354,13 @@ impl RawCurp { } /// Get a reference to spec pool - pub(super) fn spec_pool(&self) -> Arc>> { - Arc::clone(&self.ctx.spec_pool) + pub(super) fn spec_pool(&self) -> &Mutex> { + &self.ctx.spec_pool } /// Get a reference to uncommitted pool - pub(super) fn uncommitted_pool(&self) -> Arc>> { - Arc::clone(&self.ctx.uncommitted_pool) + pub(super) fn uncommitted_pool(&self) -> &Mutex> { + &self.ctx.uncommitted_pool } /// Get sync event @@ -1273,9 +1374,14 @@ impl RawCurp { ) } - /// Check if the cluster is shutting down - pub(super) fn is_shutdown(&self) -> bool { - self.task_manager.is_shutdown() + /// Check if the current node is shutting down + pub(super) fn is_node_shutdown(&self) -> bool { + self.task_manager.is_node_shutdown() + } + + /// Check if the current node is shutting down + pub(super) fn is_cluster_shutdown(&self) -> bool { + self.task_manager.is_cluster_shutdown() } /// Get a cloned task manager @@ -1361,7 +1467,7 @@ impl RawCurp { pub(super) fn apply_conf_change( &self, changes: Vec, - ) -> (Vec, String, bool) { + ) -> Option<(Vec, String, bool)> { assert_eq!(changes.len(), 1, "Joint consensus is not supported yet"); let Some(conf_change) = changes.into_iter().next() else { unreachable!("conf change is empty"); @@ -1392,6 +1498,7 @@ impl RawCurp { unreachable!("conf change is empty"); }; let node_id = conf_change.node_id; + #[allow(clippy::explicit_auto_deref)] // Avoid compiler complaint about `Dashmap::Ref` type let fallback_change = match conf_change.change_type() { ConfChangeType::Add | ConfChangeType::AddLearner => { self.cst @@ -1422,7 +1529,7 @@ impl RawCurp { let m = self.ctx.cluster_info.get(&node_id).unwrap_or_else(|| { unreachable!("node {} should exist in cluster info", node_id) }); - let _ig = self.ctx.curp_storage.put_member(&m); + let _ig = self.ctx.curp_storage.put_member(&*m); Some(ConfChange::update(node_id, old_addrs)) } ConfChangeType::Promote => { @@ -1435,7 +1542,7 @@ impl RawCurp { let m = self.ctx.cluster_info.get(&node_id).unwrap_or_else(|| { unreachable!("node {} should exist in cluster info", node_id) }); - let _ig = self.ctx.curp_storage.put_member(&m); + let _ig = self.ctx.curp_storage.put_member(&*m); None } }; @@ -1517,6 +1624,12 @@ impl RawCurp { None } + /// Mark a client id as bypassed + pub(super) fn mark_client_id_bypassed(&self, client_id: u64) { + let mut lm_w = self.ctx.lm.write(); + lm_w.bypass(client_id); + } + /// Get client tls config pub(super) fn client_tls_config(&self) -> Option<&ClientTlsConfig> { self.ctx.client_tls_config.as_ref() @@ -1725,24 +1838,24 @@ impl RawCurp { }) .collect_vec(); - let mut cb_w = self.ctx.cb.write(); let mut sp_l = self.ctx.spec_pool.lock(); let term = st.term; + let mut entries = vec![]; for entry in recovered_cmds { - let _ig_sync = cb_w.sync.insert(entry.id); // may have been inserted before let _ig_spec = sp_l.insert(entry.clone()); // may have been inserted before #[allow(clippy::expect_used)] - let entry = log - .push(term, entry.id, entry.inner) - .expect("cmd {cmd:?} cannot be serialized"); + let entry = log.push(term, entry.id, entry.cmd); debug!( "{} recovers speculatively executed cmd({}) in log[{}]", self.id(), entry.propose_id, entry.index, ); + entries.push(entry); } + + self.persistent_log_entries(&entries.iter().map(Arc::as_ref).collect::>(), log); } /// Recover the ucp from uncommitted log entries @@ -1756,18 +1869,20 @@ impl RawCurp { let propose_id = entry.propose_id; match entry.entry_data { EntryData::Command(ref cmd) => { - let _ignore = ucp_l.insert(PoolEntry::new(propose_id, Arc::clone(cmd))); - } - EntryData::ConfChange(ref conf_change) => { - let _ignore = ucp_l.insert(PoolEntry::new(propose_id, conf_change.clone())); + let _ignore = ucp_l.insert(&PoolEntry::new(propose_id, Arc::clone(cmd))); } - EntryData::Shutdown | EntryData::Empty | EntryData::SetNodeState(_, _, _) => {} + EntryData::ConfChange(_) + | EntryData::Shutdown + | EntryData::Empty + | EntryData::SetNodeState(_, _, _) => {} } } } /// Apply new logs fn apply(&self, log: &mut Log) { + let mut entries = Vec::new(); + let mut resp_txs_l = self.ctx.resp_txs.lock(); for i in (log.last_as + 1)..=log.commit_index { let entry = log.get(i).unwrap_or_else(|| { unreachable!( @@ -1775,7 +1890,8 @@ impl RawCurp { log.last_log_index() ) }); - self.ctx.cmd_tx.send_after_sync(Arc::clone(entry)); + let tx = resp_txs_l.remove(&i); + entries.push((Arc::clone(entry), tx)); log.last_as = i; if log.last_exe < log.last_as { log.last_exe = log.last_as; @@ -1787,6 +1903,8 @@ impl RawCurp { i ); } + debug!("sending {} entries to after sync task", entries.len()); + let _ignore = self.ctx.as_tx.send(TaskType::Entries(entries)); log.compact(); } @@ -1796,12 +1914,18 @@ impl RawCurp { self.ctx.cb.write().clear(); self.ctx.lm.write().clear(); self.ctx.uncommitted_pool.lock().clear(); + self.lst.reset_no_op_state(); } /// Switch to a new config and return old member infos for fallback - fn switch_config(&self, conf_change: ConfChange) -> (Vec, String, bool) { + /// + /// FIXME: The state of `ctx.cluster_info` might be inconsistent with the log. A potential + /// fix would be to include the entire cluster info in the conf change log entry and + /// overwrite `ctx.cluster_info` when switching + fn switch_config(&self, conf_change: ConfChange) -> Option<(Vec, String, bool)> { let node_id = conf_change.node_id; let mut cst_l = self.cst.lock(); + #[allow(clippy::explicit_auto_deref)] // Avoid compiler complaint about `Dashmap::Ref` type let (modified, fallback_info) = match conf_change.change_type() { ConfChangeType::Add | ConfChangeType::AddLearner => { let is_learner = matches!(conf_change.change_type(), ConfChangeType::AddLearner); @@ -1811,7 +1935,7 @@ impl RawCurp { _ = self.ctx.sync_events.insert(node_id, Arc::new(Event::new())); let _ig = self.ctx.curp_storage.put_member(&member); let m = self.ctx.cluster_info.insert(member); - (m.is_none(), (vec![], String::new(), is_learner)) + (m.is_none(), Some((vec![], String::new(), is_learner))) } ConfChangeType::Remove => { _ = cst_l.config.remove(node_id); @@ -1819,16 +1943,15 @@ impl RawCurp { _ = self.ctx.sync_events.remove(&node_id); _ = self.ctx.connects.remove(&node_id); let _ig = self.ctx.curp_storage.remove_member(node_id); - let m = self.ctx.cluster_info.remove(&node_id); - let removed_member = - m.unwrap_or_else(|| unreachable!("the member should exist before remove")); + // The member may not exist because the node could be restarted + // and has fetched the newest cluster info + // + // TODO: Review all the usages of `ctx.cluster_info` to ensure all + // the assertions are correct. + let member_opt = self.ctx.cluster_info.remove(&node_id); ( true, - ( - removed_member.peer_urls, - removed_member.name, - removed_member.is_learner, - ), + member_opt.map(|m| (m.peer_urls, m.name, m.is_learner)), ) } ConfChangeType::Update => { @@ -1839,10 +1962,10 @@ impl RawCurp { let m = self.ctx.cluster_info.get(&node_id).unwrap_or_else(|| { unreachable!("the member should exist after update"); }); - let _ig = self.ctx.curp_storage.put_member(&m); + let _ig = self.ctx.curp_storage.put_member(&*m); ( old_addrs != conf_change.address, - (old_addrs, String::new(), false), + Some((old_addrs, String::new(), false)), ) } ConfChangeType::Promote => { @@ -1853,55 +1976,118 @@ impl RawCurp { let m = self.ctx.cluster_info.get(&node_id).unwrap_or_else(|| { unreachable!("the member should exist after promote"); }); - let _ig = self.ctx.curp_storage.put_member(&m); - (modified, (vec![], String::new(), false)) + let _ig = self.ctx.curp_storage.put_member(&*m); + (modified, Some((vec![], String::new(), false))) } }; if modified { self.ctx.cluster_info.cluster_version_update(); } - if self.is_leader() { - self.ctx - .change_tx - .send(conf_change) - .unwrap_or_else(|_e| unreachable!("change_rx should not be dropped")); - if self + self.ctx + .change_tx + .send(conf_change) + .unwrap_or_else(|_e| unreachable!("change_rx should not be dropped")); + // TODO: We could wrap lst inside a role checking to prevent accidental lst mutation + if self.is_leader() + && self .lst .get_transferee() .is_some_and(|transferee| !cst_l.config.voters().contains(&transferee)) - { - self.lst.reset_transferee(); - } + { + self.lst.reset_transferee(); } fallback_info } + /// Notify sync events + fn notify_sync_events(&self, log: &Log) { + self.ctx.sync_events.iter().for_each(|e| { + if let Some(next) = self.lst.get_next_index(*e.key()) { + if next > log.base_index && log.has_next_batch(next) { + let _ignore = e.notify(1); + } + } + }); + } + + /// Update index in single node cluster + fn update_index_single_node(&self, log: &mut Log, index: u64, term: u64) { + // check if commit_index needs to be updated + if self.can_update_commit_index_to(log, index, term) && index > log.commit_index { + log.commit_to(index); + debug!("{} updates commit index to {index}", self.id()); + self.apply(&mut *log); + } + } + + /// Entry process shared by `handle_xxx` + #[allow(clippy::pattern_type_mismatch)] // Can't be fixed + fn entry_process_multi(&self, log: &mut Log, entries: &[(u64, bool)], term: u64) { + if let Some(last_no_conflict) = entries + .iter() + .rev() + .find(|(_, conflict)| *conflict) + .map(|(index, _)| *index) + { + log.last_exe = last_no_conflict; + } + let highest_index = entries + .last() + .unwrap_or_else(|| unreachable!("no log in entries")) + .0; + self.notify_sync_events(log); + self.update_index_single_node(log, highest_index, term); + } + /// Entry process shared by `handle_xxx` - fn entry_process( + fn entry_process_single( &self, log_w: &mut RwLockWriteGuard<'_, Log>, - entry: Arc>, + entry: &LogEntry, conflict: bool, term: u64, ) { let index = entry.index; if !conflict { log_w.last_exe = index; - self.ctx.cmd_tx.send_sp_exe(entry); } - self.ctx.sync_events.iter().for_each(|e| { - if let Some(next) = self.lst.get_next_index(*e.key()) { - if next > log_w.base_index && log_w.has_next_batch(next) { - let _ignore = e.notify(1); + self.notify_sync_events(log_w); + self.update_index_single_node(log_w, index, term); + } + + /// Process deduplication and acknowledge the `first_incomplete` for this client id + pub(crate) fn deduplicate( + &self, + ProposeId(client_id, seq_num): ProposeId, + first_incomplete: Option, + ) -> Result<(), CurpError> { + // deduplication + if self.ctx.lm.read().check_alive(client_id) { + let mut cb_w = self.ctx.cb.write(); + let tracker = cb_w.tracker(client_id); + if tracker.only_record(seq_num) { + // TODO: obtain the previous ER from cmd_board and packed into CurpError::Duplicated as an entry. + return Err(CurpError::duplicated()); + } + if let Some(first_incomplete) = first_incomplete { + let before = tracker.first_incomplete(); + if tracker.must_advance_to(first_incomplete) { + for seq_num_ack in before..first_incomplete { + Self::ack(ProposeId(client_id, seq_num_ack), &mut cb_w); + } } } - }); - - // check if commit_index needs to be updated - if self.can_update_commit_index_to(log_w, index, term) && index > log_w.commit_index { - log_w.commit_to(index); - debug!("{} updates commit index to {index}", self.id()); - self.apply(&mut *log_w); + } else { + self.ctx.cb.write().client_expired(client_id); + return Err(CurpError::expired_client_id()); } + Ok(()) + } + + /// Acknowledge the propose id and GC it's cmd board result + fn ack(id: ProposeId, cb: &mut CommandBoard) { + let _ignore_er = cb.er_buffer.swap_remove(&id); + let _ignore_asr = cb.asr_buffer.swap_remove(&id); + let _ignore_conf = cb.conf_buffer.swap_remove(&id); } } diff --git a/crates/curp/src/server/raw_curp/state.rs b/crates/curp/src/server/raw_curp/state.rs index d202c6a7a..f1504888c 100644 --- a/crates/curp/src/server/raw_curp/state.rs +++ b/crates/curp/src/server/raw_curp/state.rs @@ -1,6 +1,7 @@ use std::{ collections::{HashMap, HashSet}, - sync::atomic::{AtomicU64, Ordering}, + pin::Pin, + sync::atomic::{AtomicBool, AtomicU64, Ordering}, }; use dashmap::{ @@ -10,6 +11,8 @@ use dashmap::{ }, DashMap, }; +use event_listener::Event; +use futures::{future, Future}; use madsim::rand::{thread_rng, Rng}; use tracing::{debug, warn}; @@ -92,6 +95,38 @@ pub(super) struct LeaderState { statuses: DashMap, /// Leader Transferee leader_transferee: AtomicU64, + /// Event of the application of the no-op log, used for readIndex + no_op_state: NoOpState, +} + +/// The state of the no-op log entry application +#[derive(Debug, Default)] +struct NoOpState { + /// The event that triggers after application + event: Event, + /// Whether the no-op entry has been applied + applied: AtomicBool, +} + +impl NoOpState { + /// Sets the no-op entry as applied + fn set_applied(&self) { + self.applied.store(true, Ordering::Release); + let _ignore = self.event.notify(usize::MAX); + } + + /// Resets the no-op application state + fn reset(&self) { + self.applied.store(false, Ordering::Release); + } + + /// Waits for the no-op log to be applied + fn wait(&self) -> Pin + Send>> { + if self.applied.load(Ordering::Acquire) { + return Box::pin(future::ready(())); + } + Box::pin(self.event.listen()) + } } impl State { @@ -130,6 +165,7 @@ impl LeaderState { .map(|o| (*o, FollowerStatus::default())) .collect(), leader_transferee: AtomicU64::new(0), + no_op_state: NoOpState::default(), } } @@ -231,6 +267,21 @@ impl LeaderState { let val = self.leader_transferee.swap(node_id, Ordering::SeqCst); (val != 0).then_some(val) } + + /// Sets the no-op log as applied + pub(super) fn set_no_op_applied(&self) { + self.no_op_state.set_applied(); + } + + /// Resets the no-op application state + pub(super) fn reset_no_op_state(&self) { + self.no_op_state.reset(); + } + + /// Waits for the no-op log to be applied + pub(super) fn wait_no_op_applied(&self) -> impl Future + Send { + self.no_op_state.wait() + } } impl CandidateState { diff --git a/crates/curp/src/server/raw_curp/tests.rs b/crates/curp/src/server/raw_curp/tests.rs index 5e3896c37..d2eda551a 100644 --- a/crates/curp/src/server/raw_curp/tests.rs +++ b/crates/curp/src/server/raw_curp/tests.rs @@ -1,11 +1,6 @@ -use std::{cmp::Reverse, ops::Add, time::Duration}; - use curp_test_utils::{mock_role_change, test_cmd::TestCommand, TestRoleChange, TEST_CLIENT_ID}; use test_macros::abort_on_panic; -use tokio::{ - sync::oneshot, - time::{sleep, Instant}, -}; +use tokio::time::{sleep, Instant}; use tracing_test::traced_test; use utils::config::{ default_candidate_timeout_ticks, default_follower_timeout_ticks, default_heartbeat_interval, @@ -17,10 +12,10 @@ use crate::{ rpc::{connect::MockInnerConnectApi, Redirect}, server::{ cmd_board::CommandBoard, - cmd_worker::{CEEventTxApi, MockCEEventTxApi}, conflict::test_pools::{TestSpecPool, TestUncomPool}, lease_manager::LeaseManager, }, + tracker::Tracker, LogIndex, }; @@ -38,9 +33,8 @@ impl RawCurp { } #[allow(clippy::mem_forget)] // we should prevent the channel from being dropped - pub(crate) fn new_test>( + pub(crate) fn new_test( n: u64, - exe_tx: Tx, role_change: TestRoleChange, task_manager: Arc, ) -> Self { @@ -50,9 +44,6 @@ impl RawCurp { let cluster_info = Arc::new(ClusterInfo::from_members_map(all_members, [], "S0")); let cmd_board = Arc::new(RwLock::new(CommandBoard::new())); let lease_manager = Arc::new(RwLock::new(LeaseManager::new())); - let (log_tx, log_rx) = mpsc::unbounded_channel(); - // prevent the channel from being closed - std::mem::forget(log_rx); let sync_events = cluster_info .peers_ids() .into_iter() @@ -73,12 +64,10 @@ impl RawCurp { .build() .unwrap(); let curp_storage = Arc::new(DB::open(&curp_config.engine_cfg).unwrap()); + let _ignore = curp_storage.recover().unwrap(); - // grant a infinity expiry lease for test client id - lease_manager.write().expiry_queue.push( - TEST_CLIENT_ID, - Reverse(Instant::now().add(Duration::from_nanos(u64::MAX))), - ); + // bypass test client id + lease_manager.write().bypass(TEST_CLIENT_ID); let sp = Arc::new(Mutex::new(SpeculativePool::new(vec![Box::new( TestSpecPool::default(), @@ -86,6 +75,10 @@ impl RawCurp { let ucp = Arc::new(Mutex::new(UncommittedPool::new(vec![Box::new( TestUncomPool::default(), )]))); + let (as_tx, as_rx) = flume::unbounded(); + std::mem::forget(as_rx); + let resp_txs = Arc::new(Mutex::default()); + let id_barrier = Arc::new(IdBarrier::new()); Self::builder() .cluster_info(cluster_info) @@ -93,15 +86,16 @@ impl RawCurp { .cmd_board(cmd_board) .lease_manager(lease_manager) .cfg(Arc::new(curp_config)) - .cmd_tx(Arc::new(exe_tx)) .sync_events(sync_events) - .log_tx(log_tx) .role_change(role_change) .task_manager(task_manager) .connects(connects) .curp_storage(curp_storage) .spec_pool(sp) .uncommitted_pool(ucp) + .as_tx(as_tx) + .resp_txs(resp_txs) + .id_barrier(id_barrier) .build_raw_curp() .unwrap() } @@ -111,11 +105,21 @@ impl RawCurp { self.ctx.connects.entry(id).and_modify(|c| *c = connect); } + pub(crate) fn tracker(&self, client_id: u64) -> Tracker { + self.ctx + .cb + .read() + .trackers + .get(&client_id) + .cloned() + .unwrap_or_else(|| unreachable!("cannot find {client_id} in result trackers")) + } + /// Add a new cmd to the log, will return log entry index pub(crate) fn push_cmd(&self, propose_id: ProposeId, cmd: Arc) -> LogIndex { let st_r = self.st.read(); let mut log_w = self.log.write(); - log_w.push(st_r.term, propose_id, cmd).unwrap().index + log_w.push(st_r.term, propose_id, cmd).index } pub(crate) fn check_learner(&self, node_id: ServerId, is_learner: bool) -> bool { @@ -136,112 +140,88 @@ impl RawCurp { } /*************** tests for propose **************/ +// TODO: rewrite this test for propose_stream +#[cfg(ignore)] #[traced_test] #[test] fn leader_handle_propose_will_succeed() { let task_manager = Arc::new(TaskManager::new()); - let curp = { - let mut exe_tx = MockCEEventTxApi::::default(); - exe_tx.expect_send_sp_exe().returning(|_| {}); - RawCurp::new_test(3, exe_tx, mock_role_change(), task_manager) - }; + let curp = { RawCurp::new_test(3, mock_role_change(), task_manager) }; let cmd = Arc::new(TestCommand::default()); assert!(curp - .handle_propose(ProposeId(TEST_CLIENT_ID, 0), cmd) + .handle_propose(ProposeId(TEST_CLIENT_ID, 0), cmd, 0) .unwrap()); } +// TODO: rewrite this test for propose_stream +#[cfg(ignore)] #[traced_test] #[test] fn leader_handle_propose_will_reject_conflicted() { let task_manager = Arc::new(TaskManager::new()); - let curp = { - let mut exe_tx = MockCEEventTxApi::::default(); - exe_tx.expect_send_sp_exe().returning(|_| {}); - RawCurp::new_test(3, exe_tx, mock_role_change(), task_manager) - }; + let curp = { RawCurp::new_test(3, mock_role_change(), task_manager) }; let cmd1 = Arc::new(TestCommand::new_put(vec![1], 0)); assert!(curp - .handle_propose(ProposeId(TEST_CLIENT_ID, 0), cmd1) + .handle_propose(ProposeId(TEST_CLIENT_ID, 0), cmd1, 0) .unwrap()); let cmd2 = Arc::new(TestCommand::new_put(vec![1, 2], 1)); - let res = curp.handle_propose(ProposeId(TEST_CLIENT_ID, 1), cmd2); + let res = curp.handle_propose(ProposeId(TEST_CLIENT_ID, 1), cmd2, 1); assert!(matches!(res, Err(CurpError::KeyConflict(())))); // leader will also reject cmds that conflict un-synced cmds let cmd3 = Arc::new(TestCommand::new_put(vec![2], 1)); - let res = curp.handle_propose(ProposeId(TEST_CLIENT_ID, 2), cmd3); + let res = curp.handle_propose(ProposeId(TEST_CLIENT_ID, 2), cmd3, 2); assert!(matches!(res, Err(CurpError::KeyConflict(())))); } +// TODO: rewrite this test for propose_stream +#[cfg(ignore)] #[traced_test] #[test] fn leader_handle_propose_will_reject_duplicated() { let task_manager = Arc::new(TaskManager::new()); - let curp = { - let mut exe_tx = MockCEEventTxApi::::default(); - exe_tx.expect_send_sp_exe().returning(|_| {}); - RawCurp::new_test(3, exe_tx, mock_role_change(), task_manager) - }; + let curp = { RawCurp::new_test(3, mock_role_change(), task_manager) }; let cmd = Arc::new(TestCommand::default()); assert!(curp - .handle_propose(ProposeId(TEST_CLIENT_ID, 0), Arc::clone(&cmd)) + .handle_propose(ProposeId(TEST_CLIENT_ID, 0), Arc::clone(&cmd), 0) .unwrap()); - let res = curp.handle_propose(ProposeId(TEST_CLIENT_ID, 0), cmd); + let res = curp.handle_propose(ProposeId(TEST_CLIENT_ID, 0), cmd, 0); assert!(matches!(res, Err(CurpError::Duplicated(())))); } +// TODO: rewrite this test for propose_stream +#[cfg(ignore)] #[traced_test] #[test] fn follower_handle_propose_will_succeed() { let task_manager = Arc::new(TaskManager::new()); - let curp = { - let mut exe_tx = MockCEEventTxApi::::default(); - exe_tx - .expect_send_reset() - .returning(|_| oneshot::channel().1); - Arc::new(RawCurp::new_test( - 3, - exe_tx, - mock_role_change(), - task_manager, - )) - }; + let curp = { Arc::new(RawCurp::new_test(3, mock_role_change(), task_manager)) }; curp.update_to_term_and_become_follower(&mut *curp.st.write(), 1); let cmd = Arc::new(TestCommand::new_get(vec![1])); assert!(!curp - .handle_propose(ProposeId(TEST_CLIENT_ID, 0), cmd) + .handle_propose(ProposeId(TEST_CLIENT_ID, 0), cmd, 0) .unwrap()); } +// TODO: rewrite this test for propose_stream +#[cfg(ignore)] #[traced_test] #[test] fn follower_handle_propose_will_reject_conflicted() { let task_manager = Arc::new(TaskManager::new()); - let curp = { - let mut exe_tx = MockCEEventTxApi::::default(); - exe_tx - .expect_send_reset() - .returning(|_| oneshot::channel().1); - Arc::new(RawCurp::new_test( - 3, - exe_tx, - mock_role_change(), - task_manager, - )) - }; + let curp = { Arc::new(RawCurp::new_test(3, mock_role_change(), task_manager)) }; curp.update_to_term_and_become_follower(&mut *curp.st.write(), 1); let cmd1 = Arc::new(TestCommand::new_get(vec![1])); assert!(!curp - .handle_propose(ProposeId(TEST_CLIENT_ID, 0), cmd1) + .handle_propose(ProposeId(TEST_CLIENT_ID, 0), cmd1, 0) .unwrap()); let cmd2 = Arc::new(TestCommand::new_get(vec![1])); - let res = curp.handle_propose(ProposeId(TEST_CLIENT_ID, 1), cmd2); + let res = curp.handle_propose(ProposeId(TEST_CLIENT_ID, 1), cmd2, 1); assert!(matches!(res, Err(CurpError::KeyConflict(())))); } @@ -251,13 +231,7 @@ fn follower_handle_propose_will_reject_conflicted() { #[test] fn heartbeat_will_calibrate_term() { let task_manager = Arc::new(TaskManager::new()); - let curp = { - let mut exe_tx = MockCEEventTxApi::::default(); - exe_tx - .expect_send_reset() - .returning(|_| oneshot::channel().1); - RawCurp::new_test(3, exe_tx, mock_role_change(), task_manager) - }; + let curp = { RawCurp::new_test(3, mock_role_change(), task_manager) }; let s1_id = curp.cluster().get_id_by_name("S1").unwrap(); let result = curp.handle_append_entries_resp(s1_id, None, 2, false, 1); @@ -272,12 +246,7 @@ fn heartbeat_will_calibrate_term() { #[test] fn heartbeat_will_calibrate_next_index() { let task_manager = Arc::new(TaskManager::new()); - let curp = RawCurp::new_test( - 3, - MockCEEventTxApi::::default(), - mock_role_change(), - task_manager, - ); + let curp = RawCurp::new_test(3, mock_role_change(), task_manager); let s1_id = curp.cluster().get_id_by_name("S1").unwrap(); let result = curp.handle_append_entries_resp(s1_id, None, 0, false, 1); @@ -292,18 +261,7 @@ fn heartbeat_will_calibrate_next_index() { #[test] fn handle_ae_will_calibrate_term() { let task_manager = Arc::new(TaskManager::new()); - let curp = { - let mut exe_tx = MockCEEventTxApi::::default(); - exe_tx - .expect_send_reset() - .returning(|_| oneshot::channel().1); - Arc::new(RawCurp::new_test( - 3, - exe_tx, - mock_role_change(), - task_manager, - )) - }; + let curp = { Arc::new(RawCurp::new_test(3, mock_role_change(), task_manager)) }; curp.update_to_term_and_become_follower(&mut *curp.st.write(), 1); let s2_id = curp.cluster().get_id_by_name("S2").unwrap(); @@ -320,18 +278,7 @@ fn handle_ae_will_calibrate_term() { #[test] fn handle_ae_will_set_leader_id() { let task_manager = Arc::new(TaskManager::new()); - let curp = { - let mut exe_tx = MockCEEventTxApi::::default(); - exe_tx - .expect_send_reset() - .returning(|_| oneshot::channel().1); - Arc::new(RawCurp::new_test( - 3, - exe_tx, - mock_role_change(), - task_manager, - )) - }; + let curp = { Arc::new(RawCurp::new_test(3, mock_role_change(), task_manager)) }; curp.update_to_term_and_become_follower(&mut *curp.st.write(), 1); let s2_id = curp.cluster().get_id_by_name("S2").unwrap(); @@ -348,18 +295,7 @@ fn handle_ae_will_set_leader_id() { #[test] fn handle_ae_will_reject_wrong_term() { let task_manager = Arc::new(TaskManager::new()); - let curp = { - let mut exe_tx = MockCEEventTxApi::::default(); - exe_tx - .expect_send_reset() - .returning(|_| oneshot::channel().1); - Arc::new(RawCurp::new_test( - 3, - exe_tx, - mock_role_change(), - task_manager, - )) - }; + let curp = { Arc::new(RawCurp::new_test(3, mock_role_change(), task_manager)) }; curp.update_to_term_and_become_follower(&mut *curp.st.write(), 1); let s2_id = curp.cluster().get_id_by_name("S2").unwrap(); @@ -372,18 +308,7 @@ fn handle_ae_will_reject_wrong_term() { #[test] fn handle_ae_will_reject_wrong_log() { let task_manager = Arc::new(TaskManager::new()); - let curp = { - let mut exe_tx = MockCEEventTxApi::::default(); - exe_tx - .expect_send_reset() - .returning(|_| oneshot::channel().1); - Arc::new(RawCurp::new_test( - 3, - exe_tx, - mock_role_change(), - task_manager, - )) - }; + let curp = { Arc::new(RawCurp::new_test(3, mock_role_change(), task_manager)) }; curp.update_to_term_and_become_follower(&mut *curp.st.write(), 1); let s2_id = curp.cluster().get_id_by_name("S2").unwrap(); @@ -410,18 +335,7 @@ fn handle_ae_will_reject_wrong_log() { #[abort_on_panic] async fn follower_will_not_start_election_when_heartbeats_are_received() { let task_manager = Arc::new(TaskManager::new()); - let curp = { - let mut exe_tx = MockCEEventTxApi::::default(); - exe_tx - .expect_send_reset() - .returning(|_| oneshot::channel().1); - Arc::new(RawCurp::new_test( - 3, - exe_tx, - mock_role_change(), - task_manager, - )) - }; + let curp = { Arc::new(RawCurp::new_test(3, mock_role_change(), task_manager)) }; curp.update_to_term_and_become_follower(&mut *curp.st.write(), 1); let curp_c = Arc::clone(&curp); @@ -447,18 +361,7 @@ async fn follower_will_not_start_election_when_heartbeats_are_received() { #[abort_on_panic] async fn follower_or_pre_candidate_will_start_election_if_timeout() { let task_manager = Arc::new(TaskManager::new()); - let curp = { - let mut exe_tx = MockCEEventTxApi::::default(); - exe_tx - .expect_send_reset() - .returning(|_| oneshot::channel().1); - Arc::new(RawCurp::new_test( - 3, - exe_tx, - mock_role_change(), - task_manager, - )) - }; + let curp = { Arc::new(RawCurp::new_test(3, mock_role_change(), task_manager)) }; curp.update_to_term_and_become_follower(&mut *curp.st.write(), 1); let start = Instant::now(); @@ -496,18 +399,7 @@ async fn follower_or_pre_candidate_will_start_election_if_timeout() { #[test] fn handle_vote_will_calibrate_term() { let task_manager = Arc::new(TaskManager::new()); - let curp = { - let mut exe_tx = MockCEEventTxApi::::default(); - exe_tx - .expect_send_reset() - .returning(|_| oneshot::channel().1); - Arc::new(RawCurp::new_test( - 3, - exe_tx, - mock_role_change(), - task_manager, - )) - }; + let curp = { Arc::new(RawCurp::new_test(3, mock_role_change(), task_manager)) }; curp.st.write().leader_id = None; let s1_id = curp.cluster().get_id_by_name("S1").unwrap(); @@ -522,18 +414,7 @@ fn handle_vote_will_calibrate_term() { #[test] fn handle_vote_will_reject_smaller_term() { let task_manager = Arc::new(TaskManager::new()); - let curp = { - let mut exe_tx = MockCEEventTxApi::::default(); - exe_tx - .expect_send_reset() - .returning(|_| oneshot::channel().1); - Arc::new(RawCurp::new_test( - 3, - exe_tx, - mock_role_change(), - task_manager, - )) - }; + let curp = { Arc::new(RawCurp::new_test(3, mock_role_change(), task_manager)) }; curp.update_to_term_and_become_follower(&mut *curp.st.write(), 2); let s1_id = curp.cluster().get_id_by_name("S1").unwrap(); @@ -545,18 +426,7 @@ fn handle_vote_will_reject_smaller_term() { #[test] fn handle_vote_will_reject_outdated_candidate() { let task_manager = Arc::new(TaskManager::new()); - let curp = { - let mut exe_tx = MockCEEventTxApi::::default(); - exe_tx - .expect_send_reset() - .returning(|_| oneshot::channel().1); - Arc::new(RawCurp::new_test( - 3, - exe_tx, - mock_role_change(), - task_manager, - )) - }; + let curp = { Arc::new(RawCurp::new_test(3, mock_role_change(), task_manager)) }; let s2_id = curp.cluster().get_id_by_name("S2").unwrap(); let result = curp.handle_append_entries( 2, @@ -582,18 +452,7 @@ fn handle_vote_will_reject_outdated_candidate() { #[test] fn pre_candidate_will_become_candidate_then_become_leader_after_election_succeeds() { let task_manager = Arc::new(TaskManager::new()); - let curp = { - let mut exe_tx = MockCEEventTxApi::::default(); - exe_tx - .expect_send_reset() - .returning(|_| oneshot::channel().1); - Arc::new(RawCurp::new_test( - 3, - exe_tx, - mock_role_change(), - task_manager, - )) - }; + let curp = { Arc::new(RawCurp::new_test(3, mock_role_change(), task_manager)) }; curp.update_to_term_and_become_follower(&mut *curp.st.write(), 1); // tick till election starts @@ -624,18 +483,7 @@ fn pre_candidate_will_become_candidate_then_become_leader_after_election_succeed #[test] fn vote_will_calibrate_pre_candidate_term() { let task_manager = Arc::new(TaskManager::new()); - let curp = { - let mut exe_tx = MockCEEventTxApi::::default(); - exe_tx - .expect_send_reset() - .returning(|_| oneshot::channel().1); - Arc::new(RawCurp::new_test( - 3, - exe_tx, - mock_role_change(), - task_manager, - )) - }; + let curp = { Arc::new(RawCurp::new_test(3, mock_role_change(), task_manager)) }; curp.update_to_term_and_become_follower(&mut *curp.st.write(), 1); // tick till election starts @@ -658,18 +506,7 @@ fn vote_will_calibrate_pre_candidate_term() { #[test] fn recover_from_spec_pools_will_pick_the_correct_cmds() { let task_manager = Arc::new(TaskManager::new()); - let curp = { - let mut exe_tx = MockCEEventTxApi::::default(); - exe_tx - .expect_send_reset() - .returning(|_| oneshot::channel().1); - Arc::new(RawCurp::new_test( - 5, - exe_tx, - mock_role_change(), - task_manager, - )) - }; + let curp = { Arc::new(RawCurp::new_test(5, mock_role_change(), task_manager)) }; curp.update_to_term_and_become_follower(&mut *curp.st.write(), 1); // cmd1 has already been committed @@ -732,18 +569,7 @@ fn recover_from_spec_pools_will_pick_the_correct_cmds() { #[test] fn recover_ucp_from_logs_will_pick_the_correct_cmds() { let task_manager = Arc::new(TaskManager::new()); - let curp = { - let mut exe_tx = MockCEEventTxApi::::default(); - exe_tx - .expect_send_reset() - .returning(|_| oneshot::channel().1); - Arc::new(RawCurp::new_test( - 5, - exe_tx, - mock_role_change(), - task_manager, - )) - }; + let curp = { Arc::new(RawCurp::new_test(5, mock_role_change(), task_manager)) }; curp.update_to_term_and_become_follower(&mut *curp.st.write(), 1); let cmd0 = Arc::new(TestCommand::new_put(vec![1], 1)); @@ -772,14 +598,11 @@ fn recover_ucp_from_logs_will_pick_the_correct_cmds() { #[test] fn leader_retires_after_log_compact_will_succeed() { let task_manager = Arc::new(TaskManager::new()); - let curp = { - let exe_tx = MockCEEventTxApi::::default(); - RawCurp::new_test(3, exe_tx, mock_role_change(), task_manager) - }; + let curp = { RawCurp::new_test(3, mock_role_change(), task_manager) }; let mut log_w = curp.log.write(); for i in 1..=20 { let cmd = Arc::new(TestCommand::default()); - log_w.push(0, ProposeId(TEST_CLIENT_ID, i), cmd).unwrap(); + log_w.push(0, ProposeId(TEST_CLIENT_ID, i), cmd); } log_w.last_as = 20; log_w.last_exe = 20; @@ -790,23 +613,23 @@ fn leader_retires_after_log_compact_will_succeed() { curp.leader_retires(); } +// TODO: rewrite this test for propose_stream +#[cfg(ignore)] #[traced_test] #[test] fn leader_retires_should_cleanup() { let task_manager = Arc::new(TaskManager::new()); - let curp = { - let mut exe_tx = MockCEEventTxApi::::default(); - exe_tx.expect_send_sp_exe().returning(|_| {}); - RawCurp::new_test(3, exe_tx, mock_role_change(), task_manager) - }; + let curp = { RawCurp::new_test(3, mock_role_change(), task_manager) }; let _ignore = curp.handle_propose( ProposeId(TEST_CLIENT_ID, 0), Arc::new(TestCommand::new_put(vec![1], 0)), + 0, ); let _ignore = curp.handle_propose( ProposeId(TEST_CLIENT_ID, 1), Arc::new(TestCommand::new_get(vec![1])), + 0, ); curp.leader_retires(); @@ -824,10 +647,7 @@ fn leader_retires_should_cleanup() { #[tokio::test] async fn leader_handle_shutdown_will_succeed() { let task_manager = Arc::new(TaskManager::new()); - let curp = { - let exe_tx = MockCEEventTxApi::::default(); - RawCurp::new_test(3, exe_tx, mock_role_change(), task_manager) - }; + let curp = { RawCurp::new_test(3, mock_role_change(), task_manager) }; assert!(curp.handle_shutdown(ProposeId(TEST_CLIENT_ID, 0)).is_ok()); } @@ -835,11 +655,7 @@ async fn leader_handle_shutdown_will_succeed() { #[test] fn follower_handle_shutdown_will_reject() { let task_manager = Arc::new(TaskManager::new()); - let curp = { - let mut exe_tx = MockCEEventTxApi::::default(); - exe_tx.expect_send_sp_exe().returning(|_| {}); - RawCurp::new_test(3, exe_tx, mock_role_change(), task_manager) - }; + let curp = { RawCurp::new_test(3, mock_role_change(), task_manager) }; curp.update_to_term_and_become_follower(&mut *curp.st.write(), 1); let res = curp.handle_shutdown(ProposeId(TEST_CLIENT_ID, 0)); assert!(matches!( @@ -855,10 +671,7 @@ fn follower_handle_shutdown_will_reject() { #[test] fn is_synced_should_return_true_when_followers_caught_up_with_leader() { let task_manager = Arc::new(TaskManager::new()); - let curp = { - let exe_tx = MockCEEventTxApi::::default(); - RawCurp::new_test(3, exe_tx, mock_role_change(), task_manager) - }; + let curp = { RawCurp::new_test(3, mock_role_change(), task_manager) }; let s1_id = curp.cluster().get_id_by_name("S1").unwrap(); let s2_id = curp.cluster().get_id_by_name("S2").unwrap(); @@ -876,19 +689,11 @@ fn is_synced_should_return_true_when_followers_caught_up_with_leader() { #[test] fn add_node_should_add_new_node_to_curp() { let task_manager = Arc::new(TaskManager::new()); - let curp = { - let exe_tx = MockCEEventTxApi::::default(); - Arc::new(RawCurp::new_test( - 3, - exe_tx, - mock_role_change(), - task_manager, - )) - }; + let curp = { Arc::new(RawCurp::new_test(3, mock_role_change(), task_manager)) }; let old_cluster = curp.cluster().clone(); let changes = vec![ConfChange::add(1, vec!["http://127.0.0.1:4567".to_owned()])]; assert!(curp.check_new_config(&changes).is_ok()); - let infos = curp.apply_conf_change(changes.clone()); + let infos = curp.apply_conf_change(changes.clone()).unwrap(); assert!(curp.contains(1)); curp.fallback_conf_change(changes, infos.0, infos.1, infos.2); let cluster_after_fallback = curp.cluster(); @@ -911,15 +716,7 @@ fn add_node_should_add_new_node_to_curp() { #[test] fn add_learner_node_and_promote_should_success() { let task_manager = Arc::new(TaskManager::new()); - let curp = { - let exe_tx = MockCEEventTxApi::::default(); - Arc::new(RawCurp::new_test( - 3, - exe_tx, - mock_role_change(), - task_manager, - )) - }; + let curp = { Arc::new(RawCurp::new_test(3, mock_role_change(), task_manager)) }; let changes = vec![ConfChange::add_learner( 1, vec!["http://127.0.0.1:4567".to_owned()], @@ -930,7 +727,7 @@ fn add_learner_node_and_promote_should_success() { let changes = vec![ConfChange::promote(1)]; assert!(curp.check_new_config(&changes).is_ok()); - let infos = curp.apply_conf_change(changes.clone()); + let infos = curp.apply_conf_change(changes.clone()).unwrap(); assert!(curp.check_learner(1, false)); curp.fallback_conf_change(changes, infos.0, infos.1, infos.2); assert!(curp.check_learner(1, true)); @@ -940,15 +737,7 @@ fn add_learner_node_and_promote_should_success() { #[test] fn add_exists_node_should_return_node_already_exists_error() { let task_manager = Arc::new(TaskManager::new()); - let curp = { - let exe_tx = MockCEEventTxApi::::default(); - Arc::new(RawCurp::new_test( - 3, - exe_tx, - mock_role_change(), - task_manager, - )) - }; + let curp = { Arc::new(RawCurp::new_test(3, mock_role_change(), task_manager)) }; let exists_node_id = curp.cluster().get_id_by_name("S1").unwrap(); let changes = vec![ConfChange::add( exists_node_id, @@ -963,20 +752,12 @@ fn add_exists_node_should_return_node_already_exists_error() { #[test] fn remove_node_should_remove_node_from_curp() { let task_manager = Arc::new(TaskManager::new()); - let curp = { - let exe_tx = MockCEEventTxApi::::default(); - Arc::new(RawCurp::new_test( - 5, - exe_tx, - mock_role_change(), - task_manager, - )) - }; + let curp = { Arc::new(RawCurp::new_test(5, mock_role_change(), task_manager)) }; let old_cluster = curp.cluster().clone(); let follower_id = curp.cluster().get_id_by_name("S1").unwrap(); let changes = vec![ConfChange::remove(follower_id)]; assert!(curp.check_new_config(&changes).is_ok()); - let infos = curp.apply_conf_change(changes.clone()); + let infos = curp.apply_conf_change(changes.clone()).unwrap(); assert_eq!(infos, (vec!["S1".to_owned()], "S1".to_owned(), false)); assert!(!curp.contains(follower_id)); curp.fallback_conf_change(changes, infos.0, infos.1, infos.2); @@ -996,15 +777,7 @@ fn remove_node_should_remove_node_from_curp() { #[test] fn remove_non_exists_node_should_return_node_not_exists_error() { let task_manager = Arc::new(TaskManager::new()); - let curp = { - let exe_tx = MockCEEventTxApi::::default(); - Arc::new(RawCurp::new_test( - 5, - exe_tx, - mock_role_change(), - task_manager, - )) - }; + let curp = { Arc::new(RawCurp::new_test(5, mock_role_change(), task_manager)) }; let changes = vec![ConfChange::remove(1)]; let resp = curp.check_new_config(&changes); assert!(matches!(resp, Err(CurpError::NodeNotExists(())))); @@ -1014,15 +787,7 @@ fn remove_non_exists_node_should_return_node_not_exists_error() { #[test] fn update_node_should_update_the_address_of_node() { let task_manager = Arc::new(TaskManager::new()); - let curp = { - let exe_tx = MockCEEventTxApi::::default(); - Arc::new(RawCurp::new_test( - 3, - exe_tx, - mock_role_change(), - task_manager, - )) - }; + let curp = { Arc::new(RawCurp::new_test(3, mock_role_change(), task_manager)) }; let old_cluster = curp.cluster().clone(); let follower_id = curp.cluster().get_id_by_name("S1").unwrap(); let mut mock_connect = MockInnerConnectApi::new(); @@ -1040,7 +805,7 @@ fn update_node_should_update_the_address_of_node() { vec!["http://127.0.0.1:4567".to_owned()], )]; assert!(curp.check_new_config(&changes).is_ok()); - let infos = curp.apply_conf_change(changes.clone()); + let infos = curp.apply_conf_change(changes.clone()).unwrap(); assert_eq!(infos, (vec!["S1".to_owned()], String::new(), false)); assert_eq!( curp.cluster().peer_urls(follower_id), @@ -1063,16 +828,7 @@ fn update_node_should_update_the_address_of_node() { #[test] fn leader_handle_propose_conf_change() { let task_manager = Arc::new(TaskManager::new()); - let curp = { - let mut exe_tx = MockCEEventTxApi::::default(); - exe_tx.expect_send_sp_exe().returning(|_| {}); - Arc::new(RawCurp::new_test( - 3, - exe_tx, - mock_role_change(), - task_manager, - )) - }; + let curp = { Arc::new(RawCurp::new_test(3, mock_role_change(), task_manager)) }; let follower_id = curp.cluster().get_id_by_name("S1").unwrap(); assert_eq!( curp.cluster().peer_urls(follower_id), @@ -1090,15 +846,7 @@ fn leader_handle_propose_conf_change() { #[test] fn follower_handle_propose_conf_change() { let task_manager = Arc::new(TaskManager::new()); - let curp = { - let exe_tx = MockCEEventTxApi::::default(); - Arc::new(RawCurp::new_test( - 3, - exe_tx, - mock_role_change(), - task_manager, - )) - }; + let curp = { Arc::new(RawCurp::new_test(3, mock_role_change(), task_manager)) }; curp.update_to_term_and_become_follower(&mut *curp.st.write(), 2); let follower_id = curp.cluster().get_id_by_name("S1").unwrap(); @@ -1124,15 +872,7 @@ fn follower_handle_propose_conf_change() { #[test] fn leader_handle_move_leader() { let task_manager = Arc::new(TaskManager::new()); - let curp = { - let exe_tx = MockCEEventTxApi::::default(); - Arc::new(RawCurp::new_test( - 3, - exe_tx, - mock_role_change(), - task_manager, - )) - }; + let curp = { Arc::new(RawCurp::new_test(3, mock_role_change(), task_manager)) }; curp.switch_config(ConfChange::add_learner(1234, vec!["address".to_owned()])); let res = curp.handle_move_leader(1234); @@ -1155,15 +895,7 @@ fn leader_handle_move_leader() { #[test] fn follower_handle_move_leader() { let task_manager = Arc::new(TaskManager::new()); - let curp = { - let exe_tx = MockCEEventTxApi::::default(); - Arc::new(RawCurp::new_test( - 3, - exe_tx, - mock_role_change(), - task_manager, - )) - }; + let curp = { Arc::new(RawCurp::new_test(3, mock_role_change(), task_manager)) }; curp.update_to_term_and_become_follower(&mut *curp.st.write(), 2); let target_id = curp.cluster().get_id_by_name("S1").unwrap(); @@ -1175,15 +907,7 @@ fn follower_handle_move_leader() { #[test] fn leader_will_reset_transferee_after_remove_node() { let task_manager = Arc::new(TaskManager::new()); - let curp = { - let exe_tx = MockCEEventTxApi::::default(); - Arc::new(RawCurp::new_test( - 5, - exe_tx, - mock_role_change(), - task_manager, - )) - }; + let curp = { Arc::new(RawCurp::new_test(5, mock_role_change(), task_manager)) }; let target_id = curp.cluster().get_id_by_name("S1").unwrap(); let res = curp.handle_move_leader(target_id); @@ -1194,19 +918,13 @@ fn leader_will_reset_transferee_after_remove_node() { assert!(curp.get_transferee().is_none()); } +// TODO: rewrite this test for propose_stream +#[cfg(ignore)] #[traced_test] #[test] fn leader_will_reject_propose_when_transferring() { let task_manager = Arc::new(TaskManager::new()); - let curp = { - let exe_tx = MockCEEventTxApi::::default(); - Arc::new(RawCurp::new_test( - 5, - exe_tx, - mock_role_change(), - task_manager, - )) - }; + let curp = { Arc::new(RawCurp::new_test(5, mock_role_change(), task_manager)) }; let target_id = curp.cluster().get_id_by_name("S1").unwrap(); let res = curp.handle_move_leader(target_id); @@ -1214,7 +932,7 @@ fn leader_will_reject_propose_when_transferring() { let propose_id = ProposeId(0, 0); let cmd = Arc::new(TestCommand::new_put(vec![1], 1)); - let res = curp.handle_propose(propose_id, cmd); + let res = curp.handle_propose(propose_id, cmd, 0); assert!(res.is_err()); } @@ -1222,15 +940,7 @@ fn leader_will_reject_propose_when_transferring() { #[test] fn leader_will_reset_transferee_after_it_become_follower() { let task_manager = Arc::new(TaskManager::new()); - let curp = { - let exe_tx = MockCEEventTxApi::::default(); - Arc::new(RawCurp::new_test( - 5, - exe_tx, - mock_role_change(), - task_manager, - )) - }; + let curp = { Arc::new(RawCurp::new_test(5, mock_role_change(), task_manager)) }; let target_id = curp.cluster().get_id_by_name("S1").unwrap(); let res = curp.handle_move_leader(target_id); diff --git a/crates/curp/src/server/storage/db.rs b/crates/curp/src/server/storage/db.rs index 00df60e6a..6d8963508 100644 --- a/crates/curp/src/server/storage/db.rs +++ b/crates/curp/src/server/storage/db.rs @@ -1,11 +1,14 @@ -use std::marker::PhantomData; +use std::ops::Deref; -use async_trait::async_trait; use engine::{Engine, EngineType, StorageEngine, StorageOps, WriteOperation}; +use parking_lot::Mutex; use prost::Message; use utils::config::EngineConfig; -use super::{StorageApi, StorageError}; +use super::{ + wal::{codec::DataFrame, config::WALConfig, WALStorage, WALStorageOps}, + RecoverData, StorageApi, StorageError, +}; use crate::{ cmd::Command, log_entry::LogEntry, @@ -22,27 +25,30 @@ const MEMBER_ID: &[u8] = b"MemberId"; /// Column family name for curp storage const CF: &str = "curp"; -/// Column family name for logs -const LOGS_CF: &str = "logs"; /// Column family name for members const MEMBERS_CF: &str = "members"; +/// The sub dir for `RocksDB` files +const ROCKSDB_SUB_DIR: &str = "rocksdb"; + +/// The sub dir for WAL files +const WAL_SUB_DIR: &str = "wal"; + /// `DB` storage implementation #[derive(Debug)] pub struct DB { + /// The WAL storage + wal: Mutex>, /// DB handle db: Engine, - /// Phantom - phantom: PhantomData, } -#[async_trait] impl StorageApi for DB { /// Command type Command = C; #[inline] - async fn flush_voted_for(&self, term: u64, voted_for: ServerId) -> Result<(), StorageError> { + fn flush_voted_for(&self, term: u64, voted_for: ServerId) -> Result<(), StorageError> { let bytes = bincode::serialize(&(term, voted_for))?; let op = WriteOperation::new_put(CF, VOTE_FOR.to_vec(), bytes); self.db.write_multi(vec![op], true)?; @@ -51,12 +57,17 @@ impl StorageApi for DB { } #[inline] - async fn put_log_entry(&self, entry: &LogEntry) -> Result<(), StorageError> { - let bytes = bincode::serialize(entry)?; - let op = WriteOperation::new_put(LOGS_CF, entry.index.to_le_bytes().to_vec(), bytes); - self.db.write_multi(vec![op], false)?; - - Ok(()) + fn put_log_entries(&self, entry: &[&LogEntry]) -> Result<(), StorageError> { + self.wal + .lock() + .send_sync( + entry + .iter() + .map(Deref::deref) + .map(DataFrame::Entry) + .collect(), + ) + .map_err(Into::into) } #[inline] @@ -135,47 +146,47 @@ impl StorageApi for DB { } #[inline] - async fn recover( - &self, - ) -> Result<(Option<(u64, ServerId)>, Vec>), StorageError> { + fn recover(&self) -> Result, StorageError> { + let entries = self.wal.lock().recover()?; let voted_for = self .db .get(CF, VOTE_FOR)? .map(|bytes| bincode::deserialize::<(u64, ServerId)>(&bytes)) .transpose()?; - - let mut entries = vec![]; - let mut prev_index = 0; - for (_k, v) in self.db.get_all(LOGS_CF)? { - let entry: LogEntry = bincode::deserialize(&v)?; - #[allow(clippy::arithmetic_side_effects)] // won't overflow - if entry.index != prev_index + 1 { - // break when logs are no longer consistent - break; - } - prev_index = entry.index; - entries.push(entry); - } - Ok((voted_for, entries)) } } impl DB { /// Create a new CURP `DB` + /// + /// WARN: The `recover` method must be called before any call to `put_log_entries`. + /// /// # Errors /// Will return `StorageError` if failed to open the storage #[inline] pub fn open(config: &EngineConfig) -> Result { - let engine_type = match *config { - EngineConfig::Memory => EngineType::Memory, - EngineConfig::RocksDB(ref path) => EngineType::Rocks(path.clone()), + let (engine_type, wal_config) = match *config { + EngineConfig::Memory => (EngineType::Memory, WALConfig::Memory), + EngineConfig::RocksDB(ref path) => { + let mut rocksdb_dir = path.clone(); + rocksdb_dir.push(ROCKSDB_SUB_DIR); + let mut wal_dir = path.clone(); + wal_dir.push(WAL_SUB_DIR); + ( + EngineType::Rocks(rocksdb_dir.clone()), + WALConfig::new(wal_dir), + ) + } _ => unreachable!("Not supported storage type"), }; - let db = Engine::new(engine_type, &[CF, LOGS_CF, MEMBERS_CF])?; + + let db = Engine::new(engine_type, &[CF, MEMBERS_CF])?; + let wal = WALStorage::new(wal_config)?; + Ok(Self { + wal: Mutex::new(wal), db, - phantom: PhantomData, }) } } @@ -198,20 +209,23 @@ mod tests { let storage_cfg = EngineConfig::RocksDB(db_dir.clone()); { let s = DB::::open(&storage_cfg)?; - s.flush_voted_for(1, 222).await?; - s.flush_voted_for(3, 111).await?; + let (voted_for, entries) = s.recover()?; + assert!(voted_for.is_none()); + assert!(entries.is_empty()); + s.flush_voted_for(1, 222)?; + s.flush_voted_for(3, 111)?; let entry0 = LogEntry::new(1, 3, ProposeId(1, 1), Arc::new(TestCommand::default())); let entry1 = LogEntry::new(2, 3, ProposeId(1, 2), Arc::new(TestCommand::default())); let entry2 = LogEntry::new(3, 3, ProposeId(1, 3), Arc::new(TestCommand::default())); - s.put_log_entry(&entry0).await?; - s.put_log_entry(&entry1).await?; - s.put_log_entry(&entry2).await?; + s.put_log_entries(&[&entry0])?; + s.put_log_entries(&[&entry1])?; + s.put_log_entries(&[&entry2])?; sleep_secs(2).await; } { let s = DB::::open(&storage_cfg)?; - let (voted_for, entries) = s.recover().await?; + let (voted_for, entries) = s.recover()?; assert_eq!(voted_for, Some((3, 111))); assert_eq!(entries[0].index, 1); assert_eq!(entries[1].index, 2); diff --git a/crates/curp/src/server/storage/mod.rs b/crates/curp/src/server/storage/mod.rs index 029a09415..f07ecc543 100644 --- a/crates/curp/src/server/storage/mod.rs +++ b/crates/curp/src/server/storage/mod.rs @@ -1,4 +1,3 @@ -use async_trait::async_trait; use engine::EngineError; use thiserror::Error; @@ -18,8 +17,11 @@ pub enum StorageError { #[error("codec error, {0}")] Codec(String), /// Rocksdb error - #[error("internal error, {0}")] - Internal(#[from] EngineError), + #[error("rocksdb error, {0}")] + RocksDB(#[from] EngineError), + /// WAL error + #[error("wal error, {0}")] + WAL(#[from] std::io::Error), } impl From for StorageError { @@ -36,8 +38,12 @@ impl From for StorageError { } } +/// Vote info +pub(crate) type VoteInfo = (u64, ServerId); +/// Recovered data +pub(crate) type RecoverData = (Option, Vec>); + /// Curp storage api -#[async_trait] #[allow(clippy::module_name_repetitions)] pub trait StorageApi: Send + Sync { /// Command @@ -47,7 +53,7 @@ pub trait StorageApi: Send + Sync { /// /// # Errors /// Return `StorageError` when it failed to store the `voted_for` info to underlying database. - async fn flush_voted_for(&self, term: u64, voted_for: ServerId) -> Result<(), StorageError>; + fn flush_voted_for(&self, term: u64, voted_for: ServerId) -> Result<(), StorageError>; /// Put `Member` into storage /// @@ -76,16 +82,15 @@ pub trait StorageApi: Send + Sync { /// Put log entries in storage /// /// # Errors - /// Return `StorageError` when it failed to store the given log entry info to underlying database. - async fn put_log_entry(&self, entry: &LogEntry) -> Result<(), StorageError>; + /// Return `StorageError` when it failed to store the log entries to underlying database. + fn put_log_entries(&self, entry: &[&LogEntry]) -> Result<(), StorageError>; /// Recover from persisted storage + /// Return `voted_for` and all log entries /// /// # Errors - /// Return `StorageError` when it failed to recover from underlying database. Otherwise, return recovered `voted_for` and all log entries - async fn recover( - &self, - ) -> Result<(Option<(u64, ServerId)>, Vec>), StorageError>; + /// Return `StorageError` when it failed to recover the log entries and vote info from underlying database. + fn recover(&self) -> Result, StorageError>; } /// CURP `DB` storage implementation diff --git a/crates/curp/src/server/storage/wal/codec.rs b/crates/curp/src/server/storage/wal/codec.rs index fc93801c3..33c7f4226 100644 --- a/crates/curp/src/server/storage/wal/codec.rs +++ b/crates/curp/src/server/storage/wal/codec.rs @@ -295,7 +295,10 @@ impl FrameEncoder for DataFrame<'_, C> where C: Serialize, { - #[allow(clippy::arithmetic_side_effects)] // The integer shift is safe + #[allow( + clippy::arithmetic_side_effects, // The integer shift is safe + clippy::indexing_slicing // The slicing is checked + )] fn encode(&self) -> Vec { match *self { DataFrame::Entry(ref entry) => { diff --git a/crates/curp/src/server/storage/wal/config.rs b/crates/curp/src/server/storage/wal/config.rs index c6e2627b3..70157ce0f 100644 --- a/crates/curp/src/server/storage/wal/config.rs +++ b/crates/curp/src/server/storage/wal/config.rs @@ -5,7 +5,16 @@ const DEFAULT_SEGMENT_SIZE: u64 = 64 * 1024 * 1024; /// The config for WAL #[derive(Debug, Clone)] -pub(crate) struct WALConfig { +pub(crate) enum WALConfig { + /// Persistent implementation + Persistent(PersistentConfig), + /// Mock memory implementation + Memory, +} + +/// The config for persistent WAL +#[derive(Debug, Clone)] +pub(crate) struct PersistentConfig { /// The path of this config pub(super) dir: PathBuf, /// The maximum size of this segment @@ -17,17 +26,28 @@ pub(crate) struct WALConfig { impl WALConfig { /// Creates a new `WALConfig` pub(crate) fn new(dir: impl AsRef) -> Self { - Self { + Self::Persistent(PersistentConfig { dir: dir.as_ref().into(), max_segment_size: DEFAULT_SEGMENT_SIZE, - } + }) + } + + /// Creates a new memory `WALConfig` + pub(crate) fn new_memory() -> Self { + Self::Memory } /// Sets the `max_segment_size` pub(crate) fn with_max_segment_size(self, size: u64) -> Self { - Self { - dir: self.dir, - max_segment_size: size, + match self { + Self::Persistent(PersistentConfig { + dir, + max_segment_size, + }) => Self::Persistent(PersistentConfig { + dir, + max_segment_size: size, + }), + Self::Memory => Self::Memory, } } } diff --git a/crates/curp/src/server/storage/wal/mock/mod.rs b/crates/curp/src/server/storage/wal/mock/mod.rs new file mode 100644 index 000000000..a6f230d50 --- /dev/null +++ b/crates/curp/src/server/storage/wal/mock/mod.rs @@ -0,0 +1,61 @@ +use std::{collections::VecDeque, io, marker::PhantomData}; + +use curp_external_api::LogIndex; +use serde::{de::DeserializeOwned, Serialize}; + +use crate::log_entry::LogEntry; + +use super::{codec::DataFrame, config::WALConfig, WALStorageOps}; + +/// The mock WAL storage +#[derive(Debug)] +pub(crate) struct WALStorage { + /// Storage + entries: VecDeque>, +} + +impl WALStorage { + /// Creates a new mock `WALStorage` + pub(super) fn new() -> WALStorage { + Self { + entries: VecDeque::new(), + } + } +} + +impl WALStorageOps for WALStorage +where + C: Clone, +{ + fn recover(&mut self) -> io::Result>> { + Ok(self.entries.clone().into_iter().collect()) + } + + fn send_sync(&mut self, item: Vec>) -> io::Result<()> { + for frame in item { + if let DataFrame::Entry(entry) = frame { + self.entries.push_back(entry.clone()); + } + } + + Ok(()) + } + + fn truncate_head(&mut self, compact_index: LogIndex) -> io::Result<()> { + while self + .entries + .front() + .is_some_and(|e| e.index <= compact_index) + { + let _ignore = self.entries.pop_front(); + } + Ok(()) + } + + fn truncate_tail(&mut self, max_index: LogIndex) -> io::Result<()> { + while self.entries.back().is_some_and(|e| e.index > max_index) { + let _ignore = self.entries.pop_back(); + } + Ok(()) + } +} diff --git a/crates/curp/src/server/storage/wal/mod.rs b/crates/curp/src/server/storage/wal/mod.rs index fb86b4410..d204aca9e 100644 --- a/crates/curp/src/server/storage/wal/mod.rs +++ b/crates/curp/src/server/storage/wal/mod.rs @@ -32,269 +32,89 @@ mod util; /// Framed mod framed; -use std::{io, marker::PhantomData, ops::Mul}; +/// Mock WAL storage +mod mock; -use clippy_utilities::OverflowArithmetic; +/// WAL storage +mod storage; + +use std::io; + +use codec::DataFrame; +use config::WALConfig; use curp_external_api::LogIndex; -use futures::{future::join_all, Future, SinkExt, StreamExt}; -use itertools::Itertools; use serde::{de::DeserializeOwned, Serialize}; -use tokio_util::codec::Framed; -use tracing::{debug, error, info, warn}; use crate::log_entry::LogEntry; -use self::{ - codec::{DataFrame, DataFrameOwned, WAL}, - config::WALConfig, - error::{CorruptType, WALError}, - pipeline::FilePipeline, - remover::SegmentRemover, - segment::WALSegment, - util::LockedFile, -}; +/// The wal file extension +const WAL_FILE_EXT: &str = ".wal"; -/// The magic of the WAL file -const WAL_MAGIC: u32 = 0xd86e_0be2; +/// Operations of a WAL storage +pub(crate) trait WALStorageOps { + /// Recover from the given directory if there's any segments + fn recover(&mut self) -> io::Result>>; -/// The current WAL version -const WAL_VERSION: u8 = 0x00; + /// Send frames with fsync + fn send_sync(&mut self, item: Vec>) -> io::Result<()>; -/// The wal file extension -const WAL_FILE_EXT: &str = ".wal"; + /// Tuncate all the logs whose index is less than or equal to `compact_index` + /// + /// `compact_index` should be the smallest index required in CURP + fn truncate_head(&mut self, compact_index: LogIndex) -> io::Result<()>; + + /// Tuncate all the logs whose index is greater than `max_index` + fn truncate_tail(&mut self, max_index: LogIndex) -> io::Result<()>; +} -/// The WAL storage +/// The WAL Storage #[derive(Debug)] -pub(super) struct WALStorage { - /// The config of wal files - config: WALConfig, - /// The pipeline that pre-allocates files - pipeline: FilePipeline, - /// WAL segments - segments: Vec, - /// The next segment id - next_segment_id: u64, - /// The next log index - next_log_index: LogIndex, - /// The phantom data - _phantom: PhantomData, +pub(crate) enum WALStorage { + /// Persistent storage + Persistent(storage::WALStorage), + /// Mock memory storage + Memory(mock::WALStorage), } impl WALStorage { - /// Creates a new `LogStorage` - pub(super) fn new(config: WALConfig) -> io::Result> { - if !config.dir.try_exists()? { - std::fs::create_dir_all(&config.dir); - } - let mut pipeline = FilePipeline::new(config.dir.clone(), config.max_segment_size); - Ok(Self { - config, - pipeline, - segments: vec![], - next_segment_id: 0, - next_log_index: 0, - _phantom: PhantomData, + /// Creates a new `WALStorage` + pub(crate) fn new(config: WALConfig) -> io::Result { + Ok(match config { + WALConfig::Persistent(conf) => Self::Persistent(storage::WALStorage::new(conf)?), + WALConfig::Memory => Self::Memory(mock::WALStorage::new()), }) } } -impl WALStorage +impl WALStorageOps for WALStorage where - C: Serialize + DeserializeOwned + Unpin + 'static + std::fmt::Debug, + C: Serialize + DeserializeOwned + std::fmt::Debug + Clone, { - /// Recover from the given directory if there's any segments - pub(super) fn recover(&mut self) -> io::Result>> { - /// Number of lines printed around the missing log in debug information - const NUM_LINES_DEBUG: usize = 3; - // We try to recover the removal first - SegmentRemover::recover(&self.config.dir)?; - - let file_paths = util::get_file_paths_with_ext(&self.config.dir, WAL_FILE_EXT)?; - let lfiles: Vec<_> = file_paths - .into_iter() - .map(LockedFile::open_rw) - .collect::>()?; - - let segment_opening = lfiles - .into_iter() - .map(|f| WALSegment::open(f, self.config.max_segment_size)); - - let mut segments = Self::take_until_io_error(segment_opening)?; - segments.sort_unstable(); - debug!("Recovered segments: {:?}", segments); - - let logs_iter = segments.iter_mut().map(WALSegment::recover_segment_logs); - - let logs_batches = Self::take_until_io_error(logs_iter)?; - let mut logs: Vec<_> = logs_batches.into_iter().flatten().collect(); - - let pos = Self::highest_valid_pos(&logs[..]); - if pos != logs.len() { - let debug_logs: Vec<_> = logs - .iter() - .skip(pos.overflow_sub(pos.min(NUM_LINES_DEBUG))) - .take(NUM_LINES_DEBUG.mul(2)) - .collect(); - error!( - "WAL corrupted: {}, truncated at position: {pos}, logs around this position: {debug_logs:?}", - CorruptType::LogNotContinue - ); - logs.truncate(pos); - } - - let next_segment_id = segments.last().map_or(0, |s| s.id().overflow_add(1)); - let next_log_index = logs.last().map_or(1, |l| l.index.overflow_add(1)); - self.next_segment_id = next_segment_id; - self.next_log_index = next_log_index; - self.segments = segments; - - self.open_new_segment()?; - info!("WAL successfully recovered"); - - Ok(logs) - } - - /// Send frames with fsync - #[allow(clippy::pattern_type_mismatch)] // Cannot satisfy both clippy - pub(super) fn send_sync(&mut self, item: Vec>) -> io::Result<()> { - let last_segment = self - .segments - .last_mut() - .unwrap_or_else(|| unreachable!("there should be at least on segment")); - if let Some(DataFrame::Entry(entry)) = item.last() { - self.next_log_index = entry.index.overflow_add(1); - } - last_segment.write_sync(item, WAL::new())?; - - if last_segment.is_full() { - self.open_new_segment()?; + fn recover(&mut self) -> io::Result>> { + match *self { + WALStorage::Persistent(ref mut s) => s.recover(), + WALStorage::Memory(ref mut s) => s.recover(), } - - Ok(()) } - /// Truncate all the logs whose index is less than or equal to `compact_index` - /// - /// `compact_index` should be the smallest index required in CURP - pub(super) fn truncate_head(&mut self, compact_index: LogIndex) -> io::Result<()> { - if compact_index >= self.next_log_index { - warn!( - "head truncation: compact index too large, compact index: {}, storage next index: {}", - compact_index, self.next_log_index - ); - return Ok(()); - } - - debug!("performing head truncation on index: {compact_index}"); - - let mut to_remove_num = self - .segments - .iter() - .take_while(|s| s.base_index() <= compact_index) - .count() - .saturating_sub(1); - - if to_remove_num == 0 { - return Ok(()); + fn send_sync(&mut self, item: Vec>) -> io::Result<()> { + match *self { + WALStorage::Persistent(ref mut s) => s.send_sync(item), + WALStorage::Memory(ref mut s) => s.send_sync(item), } - - // The last segment does not need to be removed - let to_remove: Vec<_> = self.segments.drain(0..to_remove_num).collect(); - SegmentRemover::new_removal(&self.config.dir, to_remove.iter())?; - - Ok(()) } - /// Truncate all the logs whose index is greater than `max_index` - pub(super) fn truncate_tail(&mut self, max_index: LogIndex) -> io::Result<()> { - // segments to truncate - let segments: Vec<_> = self - .segments - .iter_mut() - .rev() - .take_while_inclusive::<_>(|s| s.base_index() > max_index) - .collect(); - - for segment in segments { - segment.seal::(max_index)?; + fn truncate_head(&mut self, compact_index: LogIndex) -> io::Result<()> { + match *self { + WALStorage::Persistent(ref mut s) => s.truncate_head(compact_index), + WALStorage::Memory(ref mut s) => s.truncate_head(compact_index), } - - let to_remove = self.update_segments(); - SegmentRemover::new_removal(&self.config.dir, to_remove.iter())?; - - self.next_log_index = max_index.overflow_add(1); - self.open_new_segment()?; - - Ok(()) } - /// Opens a new WAL segment - fn open_new_segment(&mut self) -> io::Result<()> { - let lfile = self - .pipeline - .next() - .ok_or(io::Error::from(io::ErrorKind::BrokenPipe))??; - - let segment = WALSegment::create( - lfile, - self.next_log_index, - self.next_segment_id, - self.config.max_segment_size, - )?; - - self.segments.push(segment); - self.next_segment_id = self.next_segment_id.overflow_add(1); - - Ok(()) - } - - /// Removes segments that are no longer needed - #[allow(clippy::pattern_type_mismatch)] // Cannot satisfy both clippy - fn update_segments(&mut self) -> Vec { - let flags: Vec<_> = self.segments.iter().map(WALSegment::is_redundant).collect(); - let (to_remove, remaining): (Vec<_>, Vec<_>) = - self.segments.drain(..).zip(flags).partition(|(_, f)| *f); - - self.segments = remaining.into_iter().map(|(s, _)| s).collect(); - - to_remove.into_iter().map(|(s, _)| s).collect() - } - - /// Returns the highest valid position of the log entries, - /// the logs are continuous before this position - #[allow(clippy::pattern_type_mismatch)] // can't fix - fn highest_valid_pos(entries: &[LogEntry]) -> usize { - let iter = entries.iter(); - iter.clone() - .zip(iter.skip(1)) - .enumerate() - .find(|(_, (x, y))| x.index.overflow_add(1) != y.index) - .map_or(entries.len(), |(i, _)| i) - } - - /// Iterates until an `io::Error` occurs. - fn take_until_io_error(opening: I) -> io::Result> - where - I: IntoIterator>, - { - let mut ts = vec![]; - - for result in opening { - match result { - Ok(t) => ts.push(t), - Err(e) => { - let e = e.io_or_corrupt()?; - error!("WAL corrupted: {e}"); - } - } + fn truncate_tail(&mut self, max_index: LogIndex) -> io::Result<()> { + match *self { + WALStorage::Persistent(ref mut s) => s.truncate_tail(max_index), + WALStorage::Memory(ref mut s) => s.truncate_tail(max_index), } - - Ok(ts) - } -} - -impl Drop for WALStorage { - fn drop(&mut self) { - self.pipeline.stop(); } } diff --git a/crates/curp/src/server/storage/wal/segment.rs b/crates/curp/src/server/storage/wal/segment.rs index c50ab6573..d0eb2c0cb 100644 --- a/crates/curp/src/server/storage/wal/segment.rs +++ b/crates/curp/src/server/storage/wal/segment.rs @@ -22,10 +22,16 @@ use super::{ error::{CorruptType, WALError}, framed::{Decoder, Encoder}, util::{get_checksum, parse_u64, validate_data, LockedFile}, - WAL_FILE_EXT, WAL_MAGIC, WAL_VERSION, + WAL_FILE_EXT, }; use crate::log_entry::LogEntry; +/// The magic of the WAL file +const WAL_MAGIC: u32 = 0xd86e_0be2; + +/// The current WAL version +const WAL_VERSION: u8 = 0x00; + /// The size of wal file header in bytes pub(super) const WAL_HEADER_SIZE: usize = 56; @@ -96,7 +102,7 @@ impl WALSegment { &mut self, ) -> Result>, WALError> where - C: Serialize + DeserializeOwned + 'static + std::fmt::Debug, + C: Serialize + DeserializeOwned + std::fmt::Debug, { let frame_batches = self.read_all(WAL::::new())?; let frame_batches_filtered: Vec<_> = frame_batches diff --git a/crates/curp/src/server/storage/wal/storage.rs b/crates/curp/src/server/storage/wal/storage.rs new file mode 100644 index 000000000..44bbfcf5d --- /dev/null +++ b/crates/curp/src/server/storage/wal/storage.rs @@ -0,0 +1,263 @@ +use std::{io, marker::PhantomData, ops::Mul}; + +use clippy_utilities::OverflowArithmetic; +use curp_external_api::LogIndex; +use futures::{future::join_all, Future, SinkExt, StreamExt}; +use itertools::Itertools; +use serde::{de::DeserializeOwned, Serialize}; +use tokio_util::codec::Framed; +use tracing::{debug, error, info, warn}; + +use crate::log_entry::LogEntry; + +use super::{ + codec::{DataFrame, DataFrameOwned, WAL}, + config::PersistentConfig, + error::{CorruptType, WALError}, + pipeline::FilePipeline, + remover::SegmentRemover, + segment::WALSegment, + util::{self, LockedFile}, + WALStorageOps, WAL_FILE_EXT, +}; + +/// The WAL storage +#[derive(Debug)] +pub(crate) struct WALStorage { + /// The config of wal files + config: PersistentConfig, + /// The pipeline that pre-allocates files + pipeline: FilePipeline, + /// WAL segments + segments: Vec, + /// The next segment id + next_segment_id: u64, + /// The next log index + next_log_index: LogIndex, + /// The phantom data + _phantom: PhantomData, +} + +impl WALStorage { + /// Creates a new `LogStorage` + pub(super) fn new(config: PersistentConfig) -> io::Result> { + if !config.dir.try_exists()? { + std::fs::create_dir_all(&config.dir); + } + let mut pipeline = FilePipeline::new(config.dir.clone(), config.max_segment_size); + Ok(Self { + config, + pipeline, + segments: vec![], + next_segment_id: 0, + next_log_index: 0, + _phantom: PhantomData, + }) + } +} + +impl WALStorageOps for WALStorage +where + C: Serialize + DeserializeOwned + std::fmt::Debug, +{ + /// Recover from the given directory if there's any segments + fn recover(&mut self) -> io::Result>> { + /// Number of lines printed around the missing log in debug information + const NUM_LINES_DEBUG: usize = 3; + // We try to recover the removal first + SegmentRemover::recover(&self.config.dir)?; + + let file_paths = util::get_file_paths_with_ext(&self.config.dir, WAL_FILE_EXT)?; + let lfiles: Vec<_> = file_paths + .into_iter() + .map(LockedFile::open_rw) + .collect::>()?; + + let segment_opening = lfiles + .into_iter() + .map(|f| WALSegment::open(f, self.config.max_segment_size)); + + let mut segments = Self::take_until_io_error(segment_opening)?; + segments.sort_unstable(); + debug!("Recovered segments: {:?}", segments); + + let logs_iter = segments.iter_mut().map(WALSegment::recover_segment_logs); + + let logs_batches = Self::take_until_io_error(logs_iter)?; + let mut logs: Vec<_> = logs_batches.into_iter().flatten().collect(); + + let pos = Self::highest_valid_pos(&logs[..]); + if pos != logs.len() { + let debug_logs: Vec<_> = logs + .iter() + .skip(pos.overflow_sub(pos.min(NUM_LINES_DEBUG))) + .take(NUM_LINES_DEBUG.mul(2)) + .collect(); + error!( + "WAL corrupted: {}, truncated at position: {pos}, logs around this position: {debug_logs:?}", + CorruptType::LogNotContinue + ); + logs.truncate(pos); + } + + let next_segment_id = segments.last().map_or(0, |s| s.id().overflow_add(1)); + let next_log_index = logs.last().map_or(1, |l| l.index.overflow_add(1)); + self.next_segment_id = next_segment_id; + self.next_log_index = next_log_index; + self.segments = segments; + + self.open_new_segment()?; + info!("WAL successfully recovered"); + + Ok(logs) + } + + #[allow(clippy::pattern_type_mismatch)] // Cannot satisfy both clippy + fn send_sync(&mut self, item: Vec>) -> io::Result<()> { + let last_segment = self + .segments + .last_mut() + .unwrap_or_else(|| unreachable!("there should be at least on segment")); + if let Some(DataFrame::Entry(entry)) = item.last() { + self.next_log_index = entry.index.overflow_add(1); + } + last_segment.write_sync(item, WAL::new())?; + + if last_segment.is_full() { + self.open_new_segment()?; + } + + Ok(()) + } + + /// Truncate all the logs whose index is less than or equal to + /// `compact_index` + /// + /// `compact_index` should be the smallest index required in CURP + fn truncate_head(&mut self, compact_index: LogIndex) -> io::Result<()> { + if compact_index >= self.next_log_index { + warn!( + "head truncation: compact index too large, compact index: {}, storage next index: {}", + compact_index, self.next_log_index + ); + return Ok(()); + } + + debug!("performing head truncation on index: {compact_index}"); + + let mut to_remove_num = self + .segments + .iter() + .take_while(|s| s.base_index() <= compact_index) + .count() + .saturating_sub(1); + + if to_remove_num == 0 { + return Ok(()); + } + + // The last segment does not need to be removed + let to_remove: Vec<_> = self.segments.drain(0..to_remove_num).collect(); + SegmentRemover::new_removal(&self.config.dir, to_remove.iter())?; + + Ok(()) + } + + /// Truncate all the logs whose index is greater than `max_index` + fn truncate_tail(&mut self, max_index: LogIndex) -> io::Result<()> { + // segments to truncate + let segments: Vec<_> = self + .segments + .iter_mut() + .rev() + .take_while_inclusive::<_>(|s| s.base_index() > max_index) + .collect(); + + for segment in segments { + segment.seal::(max_index)?; + } + + let to_remove = self.update_segments(); + SegmentRemover::new_removal(&self.config.dir, to_remove.iter())?; + + self.next_log_index = max_index.overflow_add(1); + self.open_new_segment()?; + + Ok(()) + } +} + +impl WALStorage +where + C: Serialize + DeserializeOwned + std::fmt::Debug, +{ + /// Opens a new WAL segment + fn open_new_segment(&mut self) -> io::Result<()> { + let lfile = self + .pipeline + .next() + .ok_or(io::Error::from(io::ErrorKind::BrokenPipe))??; + + let segment = WALSegment::create( + lfile, + self.next_log_index, + self.next_segment_id, + self.config.max_segment_size, + )?; + + self.segments.push(segment); + self.next_segment_id = self.next_segment_id.overflow_add(1); + + Ok(()) + } + + /// Removes segments that are no longer needed + #[allow(clippy::pattern_type_mismatch)] // Cannot satisfy both clippy + fn update_segments(&mut self) -> Vec { + let flags: Vec<_> = self.segments.iter().map(WALSegment::is_redundant).collect(); + let (to_remove, remaining): (Vec<_>, Vec<_>) = + self.segments.drain(..).zip(flags).partition(|(_, f)| *f); + + self.segments = remaining.into_iter().map(|(s, _)| s).collect(); + + to_remove.into_iter().map(|(s, _)| s).collect() + } + + /// Returns the highest valid position of the log entries, + /// the logs are continuous before this position + #[allow(clippy::pattern_type_mismatch)] // can't fix + fn highest_valid_pos(entries: &[LogEntry]) -> usize { + let iter = entries.iter(); + iter.clone() + .zip(iter.skip(1)) + .enumerate() + .find(|(_, (x, y))| x.index.overflow_add(1) != y.index) + .map_or(entries.len(), |(i, _)| i) + } + + /// Iterates until an `io::Error` occurs. + fn take_until_io_error(opening: I) -> io::Result> + where + I: IntoIterator>, + { + let mut ts = vec![]; + + for result in opening { + match result { + Ok(t) => ts.push(t), + Err(e) => { + let e = e.io_or_corrupt()?; + error!("WAL corrupted: {e}"); + } + } + } + + Ok(ts) + } +} + +impl Drop for WALStorage { + fn drop(&mut self) { + self.pipeline.stop(); + } +} diff --git a/crates/curp/src/tracker.rs b/crates/curp/src/tracker.rs index 09ecc18ec..240a7c672 100644 --- a/crates/curp/src/tracker.rs +++ b/crates/curp/src/tracker.rs @@ -14,6 +14,7 @@ const USIZE_BITS: usize = std::mem::size_of::() * 8; const DEFAULT_BIT_VEC_QUEUE_CAP: usize = 1024; /// A one-direction bit vector queue +/// /// It use a ring buffer `VecDeque` to store bits /// /// Memory Layout: @@ -161,7 +162,9 @@ impl BitVecQueue { } /// Split this bit vec queue to `at` + /// /// e.g. + /// /// 001100 -> `split_at(2)` -> 1100 fn split_at(&mut self, at: usize) { if self.store.is_empty() { @@ -263,6 +266,17 @@ impl Tracker { pub(crate) fn first_incomplete(&self) -> u64 { self.first_incomplete } + + /// Gets all uncompleted seq number + pub(crate) fn all_incompleted(&self) -> Vec { + let mut result = Vec::new(); + for i in 0..self.inflight.len() { + if self.inflight.get(i).unwrap_or(false) { + result.push(self.first_incomplete.wrapping_add(i.numeric_cast())); + } + } + result + } } #[cfg(test)] diff --git a/crates/curp/tests/it/common/curp_group.rs b/crates/curp/tests/it/common/curp_group.rs index 6ff65df04..8fe32ae18 100644 --- a/crates/curp/tests/it/common/curp_group.rs +++ b/crates/curp/tests/it/common/curp_group.rs @@ -55,11 +55,7 @@ pub use commandpb::{ /// `BOTTOM_TASKS` are tasks which not dependent on other tasks in the task group. /// `CurpGroup` uses `BOTTOM_TASKS` to detect whether the curp group is closed or not. -const BOTTOM_TASKS: [TaskName; 3] = [ - TaskName::WatchTask, - TaskName::ConfChange, - TaskName::LogPersist, -]; +const BOTTOM_TASKS: [TaskName; 2] = [TaskName::WatchTask, TaskName::ConfChange]; /// The default shutdown timeout used in `wait_for_targets_shutdown` pub(crate) const DEFAULT_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(7); @@ -217,7 +213,7 @@ impl CurpGroup { } async fn run( - server: Arc>, + server: Arc>, listener: TcpListener, shutdown_listener: Listener, ) -> Result<(), tonic::transport::Error> { @@ -322,6 +318,10 @@ impl CurpGroup { &self.nodes[id] } + pub fn get_node_mut(&mut self, id: &ServerId) -> &mut CurpNode { + self.nodes.get_mut(id).unwrap() + } + pub async fn new_client(&self) -> impl ClientApi { let addrs = self.all_addrs().cloned().collect(); ClientBuilder::new(ClientConfig::default(), true) @@ -373,6 +373,8 @@ impl CurpGroup { ) .await .expect("wait for group to shutdown timeout"); + // Sleep for some duration because the tasks may not exit immediately + tokio::time::sleep(Duration::from_secs(2)).await; assert!(self.is_finished(), "The group is not finished yet"); } @@ -381,7 +383,11 @@ impl CurpGroup { .flat_map(|node| { BOTTOM_TASKS .iter() - .map(|task| node.task_manager.get_shutdown_listener(task.to_owned())) + .map(|task| { + node.task_manager + .get_shutdown_listener(task.to_owned()) + .unwrap() + }) .collect::>() }) .collect::>(); @@ -434,7 +440,7 @@ impl CurpGroup { leader = leader_id; } } - leader.map(|l| (l, max_term)) + leader.map(|l| (l.value, max_term)) } pub async fn get_leader(&self) -> (ServerId, u64) { @@ -506,7 +512,7 @@ impl CurpGroup { .map(|m| Member::new(m.id, m.name, m.peer_urls, m.client_urls, m.is_learner)) .collect(); let cluster_res = curp::rpc::FetchClusterResponse { - leader_id: cluster_res_base.leader_id, + leader_id: cluster_res_base.leader_id.map(|l| l.value.into()), term: cluster_res_base.term, cluster_id: cluster_res_base.cluster_id, members, diff --git a/crates/curp/tests/it/main.rs b/crates/curp/tests/it/main.rs index b8174b639..9ce91b3b7 100644 --- a/crates/curp/tests/it/main.rs +++ b/crates/curp/tests/it/main.rs @@ -1,5 +1,3 @@ mod common; -mod read_state; - mod server; diff --git a/crates/curp/tests/it/read_state.rs b/crates/curp/tests/it/read_state.rs deleted file mode 100644 index f47dd303a..000000000 --- a/crates/curp/tests/it/read_state.rs +++ /dev/null @@ -1,59 +0,0 @@ -use std::time::Duration; - -use curp::{client::ClientApi, rpc::ReadState}; -use curp_test_utils::{ - init_logger, sleep_millis, - test_cmd::{TestCommand, TestCommandResult}, -}; -use test_macros::abort_on_panic; - -use crate::common::curp_group::CurpGroup; - -#[tokio::test(flavor = "multi_thread")] -#[abort_on_panic] -async fn read_state() { - init_logger(); - let group = CurpGroup::new(3).await; - let put_client = group.new_client().await; - let put_cmd = TestCommand::new_put(vec![0], 0).set_exe_dur(Duration::from_millis(100)); - tokio::spawn(async move { - assert_eq!( - put_client - .propose(&put_cmd, None, true) - .await - .unwrap() - .unwrap() - .0, - TestCommandResult::default(), - ); - }); - sleep_millis(10).await; - let get_client = group.new_client().await; - let res = get_client - .fetch_read_state(&TestCommand::new_get(vec![0])) - .await - .unwrap(); - if let ReadState::Ids(v) = res { - assert_eq!(v.inflight_ids.len(), 1); - } else { - unreachable!( - "expected result should be ReadState::Ids(v) where len(v) = 1, but received {:?}", - res - ); - } - - sleep_millis(500).await; - - let res = get_client - .fetch_read_state(&TestCommand::new_get(vec![0])) - .await - .unwrap(); - if let ReadState::CommitIndex(index) = res { - assert_eq!(index, 1); - } else { - unreachable!( - "expected result should be ReadState::CommitIndex({:?}), but received {:?}", - 1, res - ); - } -} diff --git a/crates/curp/tests/it/server.rs b/crates/curp/tests/it/server.rs index 3726772f0..9eeb5878a 100644 --- a/crates/curp/tests/it/server.rs +++ b/crates/curp/tests/it/server.rs @@ -12,15 +12,14 @@ use curp_test_utils::{ init_logger, sleep_millis, sleep_secs, test_cmd::{TestCommand, TestCommandResult, TestCommandType}, }; +use futures::stream::FuturesUnordered; use madsim::rand::{thread_rng, Rng}; use test_macros::abort_on_panic; use tokio::net::TcpListener; +use tokio_stream::StreamExt; use utils::{config::ClientConfig, timestamp}; -use crate::common::curp_group::{ - commandpb::ProposeId, CurpGroup, FetchClusterRequest, ProposeRequest, ProposeResponse, - DEFAULT_SHUTDOWN_TIMEOUT, -}; +use crate::common::curp_group::{CurpGroup, FetchClusterRequest, DEFAULT_SHUTDOWN_TIMEOUT}; #[tokio::test(flavor = "multi_thread")] #[abort_on_panic] @@ -58,17 +57,22 @@ async fn synced_propose() { let mut group = CurpGroup::new(5).await; let client = group.new_client().await; - let cmd = TestCommand::new_get(vec![0]); + let cmd = TestCommand::new_put(vec![0], 0); let (er, index) = client.propose(&cmd, None, false).await.unwrap().unwrap(); assert_eq!(er, TestCommandResult::new(vec![], vec![])); assert_eq!(index.unwrap(), 1.into()); // log[0] is a fake one - for exe_rx in group.exe_rxs() { - let (cmd1, er) = exe_rx.recv().await.unwrap(); + { + let mut exe_futs = group + .exe_rxs() + .map(|rx| rx.recv()) + .collect::>(); + let (cmd1, er) = exe_futs.next().await.unwrap().unwrap(); assert_eq!(cmd1, cmd); assert_eq!(er, TestCommandResult::new(vec![], vec![])); } + for as_rx in group.as_rxs() { let (cmd1, index) = as_rx.recv().await.unwrap(); assert_eq!(cmd1, cmd); @@ -76,23 +80,25 @@ async fn synced_propose() { } } -// Each command should be executed once and only once on each node +// Each command should be executed once and only once on leader #[tokio::test(flavor = "multi_thread")] #[abort_on_panic] -async fn exe_exact_n_times() { +async fn exe_exactly_once_on_leader() { init_logger(); let mut group = CurpGroup::new(3).await; let client = group.new_client().await; - let cmd = TestCommand::new_get(vec![0]); + let cmd = TestCommand::new_put(vec![0], 0); let er = client.propose(&cmd, None, true).await.unwrap().unwrap().0; assert_eq!(er, TestCommandResult::new(vec![], vec![])); - for exe_rx in group.exe_rxs() { - let (cmd1, er) = exe_rx.recv().await.unwrap(); + let leader = group.get_leader().await.0; + { + let exec_rx = &mut group.get_node_mut(&leader).exe_rx; + let (cmd1, er) = exec_rx.recv().await.unwrap(); assert!( - tokio::time::timeout(Duration::from_millis(100), exe_rx.recv()) + tokio::time::timeout(Duration::from_millis(100), exec_rx.recv()) .await .is_err() ); @@ -112,6 +118,8 @@ async fn exe_exact_n_times() { } } +// TODO: rewrite this test for propose_stream +#[cfg(ignore)] // To verify PR #86 is fixed #[tokio::test(flavor = "multi_thread")] #[abort_on_panic] @@ -128,11 +136,13 @@ async fn fast_round_is_slower_than_slow_round() { leader_connect .propose(tonic::Request::new(ProposeRequest { propose_id: Some(ProposeId { - client_id: 0, + client_id: TEST_CLIENT_ID, seq_num: 0, }), command: bincode::serialize(&cmd).unwrap(), cluster_version: 0, + term: 0, + first_incomplete: 0, })) .await .unwrap(); @@ -149,11 +159,13 @@ async fn fast_round_is_slower_than_slow_round() { let resp: ProposeResponse = follower_connect .propose(tonic::Request::new(ProposeRequest { propose_id: Some(ProposeId { - client_id: 0, + client_id: TEST_CLIENT_ID, seq_num: 0, }), command: bincode::serialize(&cmd).unwrap(), cluster_version: 0, + term: 0, + first_incomplete: 0, })) .await .unwrap() @@ -161,6 +173,8 @@ async fn fast_round_is_slower_than_slow_round() { assert!(resp.result.is_none()); } +// TODO: rewrite this test for propose_stream +#[cfg(ignore)] #[tokio::test(flavor = "multi_thread")] #[abort_on_panic] async fn concurrent_cmd_order() { @@ -178,11 +192,13 @@ async fn concurrent_cmd_order() { tokio::spawn(async move { c.propose(ProposeRequest { propose_id: Some(ProposeId { - client_id: 0, + client_id: TEST_CLIENT_ID, seq_num: 0, }), command: bincode::serialize(&cmd0).unwrap(), cluster_version: 0, + term: 0, + first_incomplete: 0, }) .await .expect("propose failed"); @@ -192,22 +208,26 @@ async fn concurrent_cmd_order() { let response = leader_connect .propose(ProposeRequest { propose_id: Some(ProposeId { - client_id: 0, + client_id: TEST_CLIENT_ID, seq_num: 1, }), command: bincode::serialize(&cmd1).unwrap(), cluster_version: 0, + term: 0, + first_incomplete: 0, }) .await; assert!(response.is_err()); let response = leader_connect .propose(ProposeRequest { propose_id: Some(ProposeId { - client_id: 0, + client_id: TEST_CLIENT_ID, seq_num: 2, }), command: bincode::serialize(&cmd2).unwrap(), cluster_version: 0, + term: 0, + first_incomplete: 0, }) .await; assert!(response.is_err()); @@ -240,7 +260,7 @@ async fn concurrent_cmd_order_should_have_correct_revision() { let sample_range = 1..=100; for i in sample_range.clone() { - let rand_dur = Duration::from_millis(thread_rng().gen_range(0..500).numeric_cast()); + let rand_dur = Duration::from_millis(thread_rng().gen_range(0..50).numeric_cast()); let _er = client .propose( &TestCommand::new_put(vec![i], i).set_as_dur(rand_dur), @@ -498,9 +518,9 @@ async fn check_new_node(is_learner: bool) { .iter() .any(|m| m.id == node_id && m.name == "new_node" && is_learner == m.is_learner)); - // 4. check if the new node executes the command from old cluster + // 4. check if the new node syncs the command from old cluster let new_node = group.nodes.get_mut(&node_id).unwrap(); - let (cmd, res) = new_node.exe_rx.recv().await.unwrap(); + let (cmd, _) = new_node.as_rx.recv().await.unwrap(); assert_eq!( cmd, TestCommand { @@ -509,7 +529,6 @@ async fn check_new_node(is_learner: bool) { ..Default::default() } ); - assert!(res.values.is_empty()); // 5. check if the old client can propose to the new cluster client diff --git a/crates/engine/Cargo.toml b/crates/engine/Cargo.toml index d0dda117a..dee74b692 100644 --- a/crates/engine/Cargo.toml +++ b/crates/engine/Cargo.toml @@ -12,11 +12,11 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -async-trait = "0.1.80" +async-trait = "0.1.81" bincode = "1.3.3" -bytes = "1.4.0" +bytes = "1.7.1" clippy-utilities = "0.2.0" -opentelemetry = { version = "0.21.0", features = ["metrics"] } +opentelemetry = { version = "0.24.0", features = ["metrics"] } parking_lot = "0.12.3" rocksdb = { version = "0.22.0", features = ["multi-threaded-cf"] } serde = { version = "1.0.204", features = ["derive"] } diff --git a/crates/engine/src/api/engine_api.rs b/crates/engine/src/api/engine_api.rs index 4a7058b03..999e56352 100644 --- a/crates/engine/src/api/engine_api.rs +++ b/crates/engine/src/api/engine_api.rs @@ -14,7 +14,9 @@ pub trait StorageEngine: Send + Sync + 'static + std::fmt::Debug { fn transaction(&self) -> Self::Transaction<'_>; /// Get all the values of the given table + /// /// # Errors + /// /// Return `EngineError::TableNotFound` if the given table does not exist /// Return `EngineError` if met some errors #[allow(clippy::type_complexity)] // it's clear that (Vec, Vec) is a key-value pair @@ -23,6 +25,7 @@ pub trait StorageEngine: Send + Sync + 'static + std::fmt::Debug { /// Get a snapshot of the current state of the database /// /// # Errors + /// /// Return `EngineError` if met some errors when creating the snapshot fn get_snapshot( &self, @@ -33,6 +36,7 @@ pub trait StorageEngine: Send + Sync + 'static + std::fmt::Debug { /// Apply a snapshot to the database /// /// # Errors + /// /// Return `EngineError` if met some errors when applying the snapshot async fn apply_snapshot( &self, @@ -46,6 +50,7 @@ pub trait StorageEngine: Send + Sync + 'static + std::fmt::Debug { /// Get the file size of the engine (Measured in bytes) /// /// # Errors + /// /// Return `EngineError` if met some errors when get file size fn file_size(&self) -> Result; } diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 0c2a5cee2..f10d26158 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -196,6 +196,6 @@ pub use crate::{ transaction_api::TransactionApi, }, error::EngineError, - proxy::{Engine, EngineType, Snapshot}, + proxy::{Engine, EngineType, Snapshot, Transaction}, snapshot_allocator::{MemorySnapshotAllocator, RocksSnapshotAllocator}, }; diff --git a/crates/engine/src/metrics.rs b/crates/engine/src/metrics.rs index b52960b1b..17f5d01e2 100644 --- a/crates/engine/src/metrics.rs +++ b/crates/engine/src/metrics.rs @@ -39,7 +39,9 @@ impl Layer { impl Layer { /// Apply snapshot from file, only works for `RocksEngine` + /// /// # Errors + /// /// Return `EngineError` when `RocksDB` returns an error. #[inline] pub async fn apply_snapshot_from_file( @@ -68,9 +70,11 @@ where } /// Get all the values of the given table + /// /// # Errors - /// Return `EngineError::TableNotFound` if the given table does not exist - /// Return `EngineError` if met some errors + /// + /// - Return `EngineError::TableNotFound` if the given table does not exist + /// - Return `EngineError` if met some errors #[allow(clippy::type_complexity)] // it's clear that (Vec, Vec) is a key-value pair fn get_all(&self, table: &str) -> Result, Vec)>, EngineError> { self.engine.get_all(table) @@ -79,6 +83,7 @@ where /// Get a snapshot of the current state of the database /// /// # Errors + /// /// Return `EngineError` if met some errors when creating the snapshot fn get_snapshot( &self, @@ -91,6 +96,7 @@ where /// Apply a snapshot to the database /// /// # Errors + /// /// Return `EngineError` if met some errors when applying the snapshot async fn apply_snapshot( &self, diff --git a/crates/engine/src/mock_rocksdb_engine.rs b/crates/engine/src/mock_rocksdb_engine.rs index c9f433839..30e75ec24 100644 --- a/crates/engine/src/mock_rocksdb_engine.rs +++ b/crates/engine/src/mock_rocksdb_engine.rs @@ -172,7 +172,9 @@ impl RocksSnapshot { } /// Create a new mock snapshot for receiving + /// /// # Errors + /// /// Return `EngineError` when create directory failed. #[inline] #[allow(clippy::unnecessary_wraps)] // the real rocksdb engine need the Result wrap diff --git a/crates/engine/src/proxy.rs b/crates/engine/src/proxy.rs index 838146e18..6952d1667 100644 --- a/crates/engine/src/proxy.rs +++ b/crates/engine/src/proxy.rs @@ -35,7 +35,9 @@ pub enum Engine { impl Engine { /// Create a new `Engine` instance + /// /// # Errors + /// /// Return `EngineError` when DB open failed. #[inline] pub fn new(engine_type: EngineType, tables: &[&'static str]) -> Result { @@ -48,7 +50,9 @@ impl Engine { } /// Apply snapshot from file, only works for `RocksEngine` + /// /// # Errors + /// /// Return `EngineError` when `RocksDB` returns an error. #[inline] pub async fn apply_snapshot_from_file( @@ -258,7 +262,9 @@ pub enum Snapshot { impl Snapshot { /// Create a new `Snapshot` instance + /// /// # Errors + /// /// Return `EngineError` when DB open failed. #[inline] pub fn new_for_receiving(engine_type: EngineType) -> Result { diff --git a/crates/engine/src/rocksdb_engine/mod.rs b/crates/engine/src/rocksdb_engine/mod.rs index 0fdffcbe3..28270bdb3 100644 --- a/crates/engine/src/rocksdb_engine/mod.rs +++ b/crates/engine/src/rocksdb_engine/mod.rs @@ -91,7 +91,9 @@ impl RocksEngine { } /// Get the total sst file size of all tables + /// /// # WARNING + /// /// This method need to flush memtable to disk. it may be slow. do not call it frequently. fn get_db_size, V: AsRef<[T]>>( db: &OptimisticTransactionDB, @@ -455,7 +457,9 @@ impl RocksSnapshot { } /// Create a new snapshot for receiving + /// /// # Errors + /// /// Return `EngineError` when create directory failed. #[inline] pub fn new_for_receiving

(dir: P) -> Result @@ -470,7 +474,9 @@ impl RocksSnapshot { } /// Create a new snapshot for sending + /// /// # Errors + /// /// Return `EngineError` when read directory failed. #[inline] pub fn new_for_sending

(dir: P) -> Result diff --git a/crates/simulation/Cargo.toml b/crates/simulation/Cargo.toml index 56b1377f2..bbc988f05 100644 --- a/crates/simulation/Cargo.toml +++ b/crates/simulation/Cargo.toml @@ -11,7 +11,7 @@ categories = ["Test"] keywords = ["Test", "Deterministic Simulation"] [dependencies] -async-trait = "0.1.80" +async-trait = "0.1.81" bincode = "1.3.3" curp = { path = "../curp" } curp-test-utils = { path = "../curp-test-utils" } @@ -20,7 +20,7 @@ futures = "0.3.29" itertools = "0.13" madsim = "0.2.27" parking_lot = "0.12.3" -prost = "0.12.3" +prost = "0.13" tempfile = "3" tokio = { version = "0.2.25", package = "madsim-tokio", features = [ "rt", @@ -31,7 +31,7 @@ tokio = { version = "0.2.25", package = "madsim-tokio", features = [ "time", "signal", ] } -tonic = { version = "0.4.2", package = "madsim-tonic" } +tonic = { version = "0.5.0", package = "madsim-tonic" } tracing = { version = "0.1.34", features = ["std", "log", "attributes"] } utils = { path = "../utils", version = "0.1.0", features = ["parking_lot"] } workspace-hack = { version = "0.1", path = "../../workspace-hack" } @@ -40,4 +40,4 @@ xline-client = { path = "../xline-client" } xlineapi = { path = "../xlineapi" } [build-dependencies] -tonic-build = { version = "0.4.3", package = "madsim-tonic-build" } +tonic-build = { version = "0.5.0", package = "madsim-tonic-build" } diff --git a/crates/simulation/src/curp_group.rs b/crates/simulation/src/curp_group.rs index ebca5fa2b..e9d3aebe0 100644 --- a/crates/simulation/src/curp_group.rs +++ b/crates/simulation/src/curp_group.rs @@ -1,16 +1,23 @@ -use std::{collections::HashMap, error::Error, path::PathBuf, sync::Arc, time::Duration}; +use std::{ + collections::HashMap, + error::Error, + path::PathBuf, + sync::{atomic::AtomicU64, Arc}, + time::Duration, +}; use async_trait::async_trait; pub use curp::rpc::{ - protocol_client::ProtocolClient, PbProposeId, ProposeRequest, ProposeResponse, + protocol_client::ProtocolClient, PbProposeId, ProposeRequest, ProposeResponse, RecordRequest, + RecordResponse, }; use curp::{ client::{ClientApi, ClientBuilder}, cmd::Command, members::{ClusterInfo, ServerId}, rpc::{ - ConfChange, FetchClusterRequest, FetchClusterResponse, Member, ProposeConfChangeRequest, - ProposeConfChangeResponse, ReadState, + ConfChange, FetchClusterRequest, FetchClusterResponse, Member, OpResponse, + ProposeConfChangeRequest, ProposeConfChangeResponse, ReadState, }, server::{ conflict::test_pools::{TestSpecPool, TestUncomPool}, @@ -182,14 +189,20 @@ impl CurpGroup { .iter() .map(|(id, node)| (*id, vec![node.addr.clone()])) .collect(); - SimClient { - inner: Arc::new( + let (client, client_id) = self + .client_node + .spawn(async move { ClientBuilder::new(config, true) .all_members(all_members) - .build() + .build_with_client_id() .await - .unwrap(), - ), + }) + .await + .unwrap() + .unwrap(); + SimClient { + inner: Arc::new(client), + client_id, handle: self.client_node.clone(), } } @@ -253,7 +266,7 @@ impl CurpGroup { leader = leader_id; } } - leader.map(|l| (l, max_term)) + leader.map(|l| (l.into(), max_term)) }) .await .unwrap() @@ -400,15 +413,30 @@ pub struct SimProtocolClient { impl SimProtocolClient { #[inline] - pub async fn propose( + pub async fn propose_stream( &mut self, cmd: impl tonic::IntoRequest + 'static + Send, - ) -> Result, tonic::Status> { + ) -> Result>, tonic::Status> { let addr = self.addr.clone(); self.handle .spawn(async move { let mut client = ProtocolClient::connect(addr).await.unwrap(); - client.propose(cmd).await + client.propose_stream(cmd).await + }) + .await + .unwrap() + } + + #[inline] + pub async fn record( + &mut self, + cmd: impl tonic::IntoRequest + 'static + Send, + ) -> Result, tonic::Status> { + let addr = self.addr.clone(); + self.handle + .spawn(async move { + let mut client = ProtocolClient::connect(addr).await.unwrap(); + client.record(cmd).await }) .await .unwrap() @@ -450,6 +478,7 @@ impl SimProtocolClient { pub struct SimClient { inner: Arc>, + client_id: Arc, handle: NodeHandle, } @@ -497,6 +526,11 @@ impl SimClient { .await .unwrap() } + + #[inline] + pub fn client_id(&self) -> u64 { + self.client_id.load(std::sync::atomic::Ordering::Relaxed) + } } impl Drop for CurpGroup { diff --git a/crates/simulation/src/xline_group.rs b/crates/simulation/src/xline_group.rs index 9943400dd..d3a0c41ae 100644 --- a/crates/simulation/src/xline_group.rs +++ b/crates/simulation/src/xline_group.rs @@ -12,16 +12,15 @@ use xline::server::XlineServer; use xline_client::{ error::XlineClientError, types::{ - cluster::{MemberAddRequest, MemberAddResponse, MemberListRequest, MemberListResponse}, - kv::{ - CompactionRequest, CompactionResponse, PutRequest, PutResponse, RangeRequest, - RangeResponse, - }, - watch::{WatchRequest, WatchStreaming, Watcher}, + kv::{CompactionResponse, PutOptions, PutResponse, RangeOptions, RangeResponse}, + watch::{WatchOptions, WatchStreaming, Watcher}, }, Client, ClientOptions, }; -use xlineapi::{command::Command, ClusterClient, KvClient, RequestUnion, WatchClient}; +use xlineapi::{ + command::Command, ClusterClient, KvClient, MemberAddResponse, MemberListResponse, RequestUnion, + WatchClient, +}; pub struct XlineNode { pub client_url: String, @@ -55,7 +54,7 @@ impl XlineGroup { vec!["0.0.0.0:2379".to_owned()], vec![format!("192.168.1.{}:2379", i + 1)], all.clone(), - false, + i == 0, CurpConfig::default(), ClientConfig::default(), ServerTimeout::default(), @@ -159,27 +158,59 @@ pub struct SimClient { handle: NodeHandle, } -macro_rules! impl_client_method { - ($method:ident, $client:ident, $request:ty, $response:ty) => { - pub async fn $method( - &self, - request: $request, - ) -> Result<$response, XlineClientError> { - let client = self.inner.clone(); - self.handle - .spawn(async move { client.$client().$method(request).await }) - .await - .unwrap() - } - }; -} - impl SimClient { - impl_client_method!(put, kv_client, PutRequest, PutResponse); - impl_client_method!(range, kv_client, RangeRequest, RangeResponse); - impl_client_method!(compact, kv_client, CompactionRequest, CompactionResponse); + pub async fn put( + &self, + key: impl Into>, + value: impl Into>, + option: Option, + ) -> Result> { + let client = self.inner.clone(); + let key = key.into(); + let value = value.into(); + self.handle + .spawn(async move { client.kv_client().put(key, value, option).await }) + .await + .unwrap() + } - impl_client_method!(watch, watch_client, WatchRequest, (Watcher, WatchStreaming)); + pub async fn range( + &self, + key: impl Into>, + options: Option, + ) -> Result> { + let client = self.inner.clone(); + let key = key.into(); + self.handle + .spawn(async move { client.kv_client().range(key, options).await }) + .await + .unwrap() + } + + pub async fn compact( + &self, + revision: i64, + physical: bool, + ) -> Result> { + let client = self.inner.clone(); + self.handle + .spawn(async move { client.kv_client().compact(revision, physical).await }) + .await + .unwrap() + } + + pub async fn watch( + &self, + key: impl Into>, + options: Option, + ) -> Result<(Watcher, WatchStreaming), XlineClientError> { + let client = self.inner.clone(); + let key = key.into(); + self.handle + .spawn(async move { client.watch_client().watch(key, options).await }) + .await + .unwrap() + } } impl Drop for XlineGroup { @@ -225,12 +256,21 @@ impl SimEtcdClient { } } - pub async fn put(&self, request: PutRequest) -> Result> { + pub async fn put( + &self, + key: impl Into>, + value: impl Into>, + option: Option, + ) -> Result> { let mut client = self.kv.clone(); + let key = key.into(); + let value = value.into(); self.handle .spawn(async move { client - .put(xlineapi::PutRequest::from(request)) + .put(xlineapi::PutRequest::from( + option.unwrap_or_default().with_kv(key, value), + )) .await .map(|r| r.into_inner()) .map_err(Into::into) @@ -241,13 +281,14 @@ impl SimEtcdClient { pub async fn compact( &self, - request: CompactionRequest, + revision: i64, + physical: bool, ) -> Result> { let mut client = self.kv.clone(); self.handle .spawn(async move { client - .compact(xlineapi::CompactionRequest::from(request)) + .compact(xlineapi::CompactionRequest { revision, physical }) .await .map(|r| r.into_inner()) .map_err(Into::into) @@ -258,17 +299,20 @@ impl SimEtcdClient { pub async fn watch( &self, - request: WatchRequest, + key: impl Into>, + options: Option, ) -> Result<(Watcher, WatchStreaming), XlineClientError> { let mut client = self.watch.clone(); - + let key = key.into(); self.handle .spawn(async move { let (mut request_sender, request_receiver) = futures::channel::mpsc::channel::(128); let request = xlineapi::WatchRequest { - request_union: Some(RequestUnion::CreateRequest(request.into())), + request_union: Some(RequestUnion::CreateRequest( + options.unwrap_or_default().with_key(key).into(), + )), }; request_sender @@ -298,15 +342,20 @@ impl SimEtcdClient { .unwrap() } - pub async fn member_add( - &self, - request: MemberAddRequest, + pub async fn member_add>( + &mut self, + peer_urls: impl Into>, + is_learner: bool, ) -> Result> { let mut client = self.cluster.clone(); + let peer_urls: Vec = peer_urls.into().into_iter().map(Into::into).collect(); self.handle .spawn(async move { client - .member_add(xlineapi::MemberAddRequest::from(request)) + .member_add(xlineapi::MemberAddRequest { + peer_ur_ls: peer_urls, + is_learner, + }) .await .map(|r| r.into_inner()) .map_err(Into::into) @@ -316,14 +365,14 @@ impl SimEtcdClient { } pub async fn member_list( - &self, - request: MemberListRequest, + &mut self, + linearizable: bool, ) -> Result> { let mut client = self.cluster.clone(); self.handle .spawn(async move { client - .member_list(xlineapi::MemberListRequest::from(request)) + .member_list(xlineapi::MemberListRequest { linearizable }) .await .map(|r| r.into_inner()) .map_err(Into::into) diff --git a/crates/simulation/tests/it/curp/server_recovery.rs b/crates/simulation/tests/it/curp/server_recovery.rs index 46a3c26cf..7e8a88ccf 100644 --- a/crates/simulation/tests/it/curp/server_recovery.rs +++ b/crates/simulation/tests/it/curp/server_recovery.rs @@ -2,7 +2,7 @@ use std::{sync::Arc, time::Duration, vec}; -use curp::rpc::{ConfChange, ProposeConfChangeRequest}; +use curp::rpc::{ConfChange, ProposeConfChangeRequest, RecordRequest}; use curp_test_utils::{init_logger, sleep_secs, test_cmd::TestCommand, TEST_TABLE}; use engine::{StorageEngine, StorageOps}; use itertools::Itertools; @@ -51,17 +51,18 @@ async fn leader_crash_and_recovery() { let old_leader = group.nodes.get_mut(&leader).unwrap(); // new leader will push an empty log to commit previous logs, the empty log does - // not call ce.execute and ce.after_sync, therefore, the index of the first item - // received by as_rx is 2 - let (_cmd, er) = old_leader.exe_rx.recv().await.unwrap(); - assert_eq!(er.values, Vec::::new()); + // not call ce.after_sync, therefore, the index of the first item received by + // as_rx is 2 let asr = old_leader.as_rx.recv().await.unwrap(); assert_eq!(asr.1, 3); // log index 1 and 2 is the empty log - let (_cmd, er) = old_leader.exe_rx.recv().await.unwrap(); + let new_leader = group.nodes.get_mut(&leader2).unwrap(); + let (_cmd, er) = new_leader.exe_rx.recv().await.unwrap(); + assert_eq!(er.values, Vec::::new()); + let (_cmd, er) = new_leader.exe_rx.recv().await.unwrap(); assert_eq!(er.values, vec![0]); - let asr = old_leader.as_rx.recv().await.unwrap(); - assert_eq!(asr.1, 4); // log index 1 and 2 is the empty log + let asr = new_leader.as_rx.recv().await.unwrap(); + assert_eq!(asr.1, 3); // log index 1 and 2 is the empty log } #[madsim::test] @@ -100,15 +101,8 @@ async fn follower_crash_and_recovery() { group.restart(follower).await; let follower = group.nodes.get_mut(&follower).unwrap(); - let (_cmd, er) = follower.exe_rx.recv().await.unwrap(); - assert_eq!(er.values, Vec::::new(),); let asr = follower.as_rx.recv().await.unwrap(); - assert_eq!(asr.1, 2); // log index 1 is the empty log - - let (_cmd, er) = follower.exe_rx.recv().await.unwrap(); - assert_eq!(er.values, vec![0]); - let asr = follower.as_rx.recv().await.unwrap(); - assert_eq!(asr.1, 3); + assert_eq!(asr.1, 2); } #[madsim::test] @@ -122,9 +116,15 @@ async fn leader_and_follower_both_crash_and_recovery() { let follower = *group.nodes.keys().find(|&id| id != &leader).unwrap(); group.crash(follower).await; + let _wait_up = client + .propose(TestCommand::new_get(vec![0]), true) + .await + .unwrap() + .unwrap(); + assert_eq!( client - .propose(TestCommand::new_put(vec![0], 0), true) + .propose(TestCommand::new_put(vec![0], 0), false) .await .unwrap() .unwrap() @@ -132,16 +132,6 @@ async fn leader_and_follower_both_crash_and_recovery() { .values, Vec::::new(), ); - assert_eq!( - client - .propose(TestCommand::new_get(vec![0]), true) - .await - .unwrap() - .unwrap() - .0 - .values, - vec![0] - ); group.crash(leader).await; @@ -150,29 +140,15 @@ async fn leader_and_follower_both_crash_and_recovery() { let old_leader = group.nodes.get_mut(&leader).unwrap(); - let (_cmd, er) = old_leader.exe_rx.recv().await.unwrap(); - assert_eq!(er.values, Vec::::new(),); let asr = old_leader.as_rx.recv().await.unwrap(); assert_eq!(asr.1, 2); // log index 1 is the empty log - let (_cmd, er) = old_leader.exe_rx.recv().await.unwrap(); - assert_eq!(er.values, vec![0]); - let asr = old_leader.as_rx.recv().await.unwrap(); - assert_eq!(asr.1, 3); - // restart follower group.restart(follower).await; let follower = group.nodes.get_mut(&follower).unwrap(); - let (_cmd, er) = follower.exe_rx.recv().await.unwrap(); - assert_eq!(er.values, Vec::::new(),); let asr = follower.as_rx.recv().await.unwrap(); assert_eq!(asr.1, 2); // log index 1 is the empty log - - let (_cmd, er) = follower.exe_rx.recv().await.unwrap(); - assert_eq!(er.values, vec![0]); - let asr = follower.as_rx.recv().await.unwrap(); - assert_eq!(asr.1, 3); } #[madsim::test] @@ -186,13 +162,13 @@ async fn new_leader_will_recover_spec_cmds_cond1() { // 1: send cmd1 to all others except the leader let cmd1 = Arc::new(TestCommand::new_put(vec![0], 0)); - let req1 = ProposeRequest { - propose_id: Some(PbProposeId { - client_id: 0, - seq_num: 0, - }), + let propose_id = PbProposeId { + client_id: client.client_id(), + seq_num: 0, + }; + let req1_rec = RecordRequest { + propose_id: Some(propose_id), command: bincode::serialize(&cmd1).unwrap(), - cluster_version: 0, }; for id in group .all_members @@ -201,7 +177,7 @@ async fn new_leader_will_recover_spec_cmds_cond1() { .take(4) { let mut connect = group.get_connect(id).await; - connect.propose(req1.clone()).await.unwrap(); + connect.record(req1_rec.clone()).await.unwrap(); } // 2: disable leader1 and wait election @@ -223,14 +199,14 @@ async fn new_leader_will_recover_spec_cmds_cond1() { // old leader should recover from the new leader group.enable_node(leader1); - // every cmd should be executed and after synced on every node - for rx in group.exe_rxs() { - rx.recv().await; - rx.recv().await; - } + // every cmd should be executed on leader + let leader2 = group.get_leader().await.0; + let new_leader = group.nodes.get_mut(&leader2).unwrap(); + new_leader.exe_rx.recv().await; + + // every cmd should be after synced on every node for rx in group.as_rxs() { rx.recv().await; - rx.recv().await; } } @@ -299,14 +275,17 @@ async fn old_leader_will_keep_original_states() { let cmd1 = Arc::new(TestCommand::new_put(vec![0], 1)); let req1 = ProposeRequest { propose_id: Some(PbProposeId { - client_id: 0, - seq_num: 0, + client_id: client.client_id(), + seq_num: 1, }), command: bincode::serialize(&cmd1).unwrap(), cluster_version: 0, + term: 1, + slow_path: false, + first_incomplete: 0, }; let mut leader1_connect = group.get_connect(&leader1).await; - leader1_connect.propose(req1).await.unwrap(); + leader1_connect.propose_stream(req1).await.unwrap(); // 3: recover all others and disable leader, a new leader will be elected group.disable_node(leader1); @@ -489,11 +468,12 @@ async fn overwritten_config_should_fallback() { let node_id = 123; let address = vec!["127.0.0.1:4567".to_owned()]; let changes = vec![ConfChange::add(node_id, address)]; + let client = group.new_client().await; let res = leader_conn .propose_conf_change( ProposeConfChangeRequest { propose_id: Some(PbProposeId { - client_id: 0, + client_id: client.client_id(), seq_num: 0, }), changes, diff --git a/crates/simulation/tests/it/xline.rs b/crates/simulation/tests/it/xline.rs index 93d1db115..2e9b48501 100644 --- a/crates/simulation/tests/it/xline.rs +++ b/crates/simulation/tests/it/xline.rs @@ -3,11 +3,7 @@ use std::time::Duration; use curp_test_utils::init_logger; use madsim::time::sleep; use simulation::xline_group::{SimEtcdClient, XlineGroup}; -use xline_client::types::{ - cluster::{MemberAddRequest, MemberListRequest}, - kv::{CompactionRequest, PutRequest}, - watch::WatchRequest, -}; +use xline_client::types::watch::WatchOptions; // TODO: Add more tests if needed @@ -16,7 +12,7 @@ async fn basic_put() { init_logger(); let group = XlineGroup::new(3).await; let client = group.client().await; - let res = client.put(PutRequest::new("key", "value")).await; + let res = client.put("key", "value", None).await; assert!(res.is_ok()); } @@ -29,19 +25,15 @@ async fn watch_compacted_revision_should_receive_canceled_response() { let client = SimEtcdClient::new(watch_addr, group.client_handle.clone()).await; for i in 1..=6 { - let result = client - .put(PutRequest::new("key", format!("value{}", i))) - .await; + let result = client.put("key", format!("value{}", i), None).await; assert!(result.is_ok()); } - let result = client - .compact(CompactionRequest::new(5).with_physical()) - .await; + let result = client.compact(5, true).await; assert!(result.is_ok()); let (_, mut watch_stream) = client - .watch(WatchRequest::new("key").with_start_revision(4)) + .watch("key", Some(WatchOptions::default().with_start_revision(4))) .await .unwrap(); let r = watch_stream.message().await.unwrap().unwrap(); @@ -54,29 +46,20 @@ async fn xline_members_restore() { let mut group = XlineGroup::new(3).await; let node = group.get_node("S1"); let addr = node.client_url.clone(); - let client = SimEtcdClient::new(addr, group.client_handle.clone()).await; + let mut client = SimEtcdClient::new(addr, group.client_handle.clone()).await; let res = client - .member_add(MemberAddRequest::new( - vec!["http://192.168.1.4:12345".to_owned()], - true, - )) + .member_add(["http://192.168.1.4:12345"], true) .await .unwrap(); assert_eq!(res.members.len(), 4); - let members = client - .member_list(MemberListRequest::new(false)) - .await - .unwrap(); + let members = client.member_list(false).await.unwrap(); assert_eq!(members.members.len(), 4); group.crash("S1").await; sleep(Duration::from_secs(10)).await; group.restart("S1").await; sleep(Duration::from_secs(10)).await; - let members = client - .member_list(MemberListRequest::new(false)) - .await - .unwrap(); + let members = client.member_list(false).await.unwrap(); assert_eq!(members.members.len(), 4); } diff --git a/crates/test-macros/Cargo.toml b/crates/test-macros/Cargo.toml index 0516322ad..a59381409 100644 --- a/crates/test-macros/Cargo.toml +++ b/crates/test-macros/Cargo.toml @@ -20,4 +20,4 @@ tokio = { version = "1", features = ["rt-multi-thread", "macros"] } workspace-hack = { version = "0.1", path = "../../workspace-hack" } [dev-dependencies] -assert_cmd = "2.0.14" +assert_cmd = "2.0.15" diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index 52e9a471f..f5c150b4b 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -18,19 +18,18 @@ tokio = ["dep:async-trait"] parking_lot = ["dep:parking_lot"] [dependencies] -async-trait = { version = "0.1.80", optional = true } +async-trait = { version = "0.1.81", optional = true } clippy-utilities = "0.2.0" -dashmap = "5.5.3" +dashmap = "6.0.1" derive_builder = "0.20.0" -event-listener = "5.3.0" +event-listener = "5.3.1" futures = "0.3.30" getset = "0.1" -opentelemetry = { version = "0.22.0", features = ["trace"] } -opentelemetry_sdk = { version = "0.22.1", features = ["trace"] } +interval_map = { version = "0.1", package = "rb-interval-map" } +opentelemetry = { version = "0.24.0", features = ["trace"] } +opentelemetry_sdk = { version = "0.24.1", features = ["trace"] } parking_lot = { version = "0.12.3", optional = true } pbkdf2 = { version = "0.12.2", features = ["simple"] } -petgraph = "0.6.4" -rand = "0.8.5" regex = "1.10.5" serde = { version = "1.0.204", features = ["derive"] } thiserror = "1.0.61" @@ -40,19 +39,19 @@ tokio = { version = "0.2.25", package = "madsim-tokio", features = [ "rt-multi-thread", ] } toml = "0.8.14" -tonic = { version = "0.4.2", package = "madsim-tonic" } +tonic = { version = "0.5.0", package = "madsim-tonic" } tracing = "0.1.37" tracing-appender = "0.2" -tracing-opentelemetry = "0.23.0" +tracing-opentelemetry = "0.25.0" workspace-hack = { version = "0.1", path = "../../workspace-hack" } [dev-dependencies] -opentelemetry = { version = "0.22.0", features = ["trace"] } -opentelemetry-jaeger = "0.22.0" -opentelemetry-otlp = { version = "0.15.0", features = [ +opentelemetry = { version = "0.24.0", features = ["trace"] } +opentelemetry-jaeger-propagator = "0.3.0" +opentelemetry-otlp = { version = "0.17.0", features = [ "metrics", "http-proto", "reqwest-client", ] } test-macros = { path = "../test-macros" } -tracing-subscriber = "0.3.16" +tracing-subscriber = "0.3.18" diff --git a/crates/utils/benches/interval_map.rs b/crates/utils/benches/interval_map.rs deleted file mode 100644 index 46e93ec75..000000000 --- a/crates/utils/benches/interval_map.rs +++ /dev/null @@ -1,118 +0,0 @@ -#![cfg(bench)] -#![feature(test)] - -extern crate test; -extern crate utils; - -use std::hint::black_box; - -use test::Bencher; -use utils::interval_map::{Interval, IntervalMap}; - -struct Rng { - state: u32, -} - -impl Rng { - fn new() -> Self { - Self { state: 0x87654321 } - } - - fn gen_u32(&mut self) -> u32 { - self.state ^= self.state << 13; - self.state ^= self.state >> 17; - self.state ^= self.state << 5; - self.state - } - - fn gen_range_i32(&mut self, low: i32, high: i32) -> i32 { - let d = (high - low) as u32; - low + (self.gen_u32() % d) as i32 - } -} - -struct IntervalGenerator { - rng: Rng, - limit: i32, -} - -impl IntervalGenerator { - fn new() -> Self { - const LIMIT: i32 = 1000; - Self { - rng: Rng::new(), - limit: LIMIT, - } - } - - fn next(&mut self) -> Interval { - let low = self.rng.gen_range_i32(0, self.limit - 1); - let high = self.rng.gen_range_i32(low + 1, self.limit); - Interval::new(low, high) - } -} - -fn bench_interval_map_insert(count: usize, bench: &mut Bencher) { - let mut gen = IntervalGenerator::new(); - let intervals: Vec<_> = std::iter::repeat_with(|| gen.next()).take(count).collect(); - bench.iter(|| { - let mut map = IntervalMap::new(); - for i in intervals.clone() { - black_box(map.insert(i, ())); - } - }); -} - -fn bench_interval_map_insert_remove(count: usize, bench: &mut Bencher) { - let mut gen = IntervalGenerator::new(); - let intervals: Vec<_> = std::iter::repeat_with(|| gen.next()).take(count).collect(); - bench.iter(|| { - let mut map = IntervalMap::new(); - for i in intervals.clone() { - black_box(map.insert(i, ())); - } - for i in &intervals { - black_box(map.remove(&i)); - } - }); -} - -#[bench] -fn bench_interval_map_insert_100(bench: &mut Bencher) { - bench_interval_map_insert(100, bench); -} - -#[bench] -fn bench_interval_map_insert_1000(bench: &mut Bencher) { - bench_interval_map_insert(1000, bench); -} - -#[bench] -fn bench_interval_map_insert_10000(bench: &mut Bencher) { - bench_interval_map_insert(10_000, bench); -} - -#[bench] -fn bench_interval_map_insert_100000(bench: &mut Bencher) { - bench_interval_map_insert(100_000, bench); -} - -#[bench] -fn bench_interval_map_insert_remove_100(bench: &mut Bencher) { - bench_interval_map_insert_remove(100, bench); -} - -#[bench] -fn bench_interval_map_insert_remove_1000(bench: &mut Bencher) { - bench_interval_map_insert_remove(1000, bench); -} - -#[bench] -fn bench_interval_map_insert_remove_10000(bench: &mut Bencher) { - bench_interval_map_insert_remove(10_000, bench); -} - -#[bench] -fn bench_interval_map_insert_remove_100000(bench: &mut Bencher) { - bench_interval_map_insert_remove(100_000, bench); -} diff --git a/crates/utils/src/barrier.rs b/crates/utils/src/barrier.rs index dd306d05a..5798af042 100644 --- a/crates/utils/src/barrier.rs +++ b/crates/utils/src/barrier.rs @@ -36,7 +36,7 @@ where /// Wait for a collection of ids. #[inline] - pub fn wait_all(&self, ids: Vec) -> impl Future { + pub fn wait_all(&self, ids: Vec) -> impl Future + Send { let mut barriers_l = self.barriers.lock(); let listeners: FuturesOrdered<_> = ids .into_iter() diff --git a/crates/utils/src/config.rs b/crates/utils/src/config.rs index d37b3c088..0f59dc853 100644 --- a/crates/utils/src/config.rs +++ b/crates/utils/src/config.rs @@ -293,6 +293,7 @@ pub struct CurpConfig { pub rpc_timeout: Duration, /// Curp append entries batch timeout + /// /// If the `batch_timeout` has expired, then it will be dispatched /// whether its size reaches the `BATCHING_MSG_MAX_SIZE` or not. #[builder(default = "default_batch_timeout()")] @@ -305,13 +306,16 @@ pub struct CurpConfig { pub batch_max_size: u64, /// How many ticks a follower is allowed to miss before it starts a new round of election + /// /// The actual timeout will be randomized and in between heartbeat_interval * [follower_timeout_ticks, 2 * follower_timeout_ticks) #[builder(default = "default_follower_timeout_ticks()")] #[serde(default = "default_follower_timeout_ticks")] pub follower_timeout_ticks: u8, /// How many ticks a candidate needs to wait before it starts a new round of election + /// /// It should be smaller than `follower_timeout_ticks` + /// /// The actual timeout will be randomized and in between heartbeat_interval * [candidate_timeout_ticks, 2 * candidate_timeout_ticks) #[builder(default = "default_candidate_timeout_ticks()")] #[serde(default = "default_candidate_timeout_ticks")] @@ -368,6 +372,8 @@ pub const fn default_server_wait_synced_timeout() -> Duration { } /// default initial retry timeout +/// FIXME: etcd client has it's own retry mechanism, which may lead to nested retry timeouts. +/// Consider bypassing for proxied etcd client requests. #[must_use] #[inline] pub const fn default_initial_retry_timeout() -> Duration { diff --git a/crates/utils/src/interval_map/mod.rs b/crates/utils/src/interval_map/mod.rs deleted file mode 100644 index d03297c3e..000000000 --- a/crates/utils/src/interval_map/mod.rs +++ /dev/null @@ -1,1044 +0,0 @@ -use std::collections::VecDeque; - -use petgraph::graph::{DefaultIx, IndexType, NodeIndex}; - -#[cfg(test)] -mod tests; - -/// An interval-value map, which support operations on dynamic sets of intervals. -#[derive(Debug)] -pub struct IntervalMap { - /// Vector that stores nodes - nodes: Vec>, - /// Root of the interval tree - root: NodeIndex, - /// Number of elements in the map - len: usize, -} - -impl IntervalMap -where - T: Ord, - Ix: IndexType, -{ - /// Creates a new `IntervalMap` with estimated capacity. - #[inline] - #[must_use] - pub fn with_capacity(capacity: usize) -> Self { - let mut nodes = vec![Self::new_sentinel()]; - nodes.reserve(capacity); - IntervalMap { - nodes, - root: Self::sentinel(), - len: 0, - } - } - - /// Inserts a interval-value pair into the map. - /// - /// # Panics - /// - /// This method panics when the tree is at the maximum number of nodes for its index - #[inline] - pub fn insert(&mut self, interval: Interval, value: V) -> Option { - let node_idx = NodeIndex::new(self.nodes.len()); - let node = Self::new_node(interval, value, node_idx); - // check for max capacity, except if we use usize - assert!( - ::max().index() == !0 || NodeIndex::end() != node_idx, - "Reached maximum number of nodes" - ); - self.nodes.push(node); - self.insert_inner(node_idx) - } - - /// Removes a interval from the map, returning the value at the interval if the interval - /// was previously in the map. - #[inline] - pub fn remove(&mut self, interval: &Interval) -> Option { - if let Some(node_idx) = self.search_exact(interval) { - self.remove_inner(node_idx); - // Swap the node with the last node stored in the vector and update indices - let mut node = self.nodes.swap_remove(node_idx.index()); - let old = NodeIndex::::new(self.nodes.len()); - self.update_idx(old, node_idx); - - return node.value.take(); - } - None - } - - /// Checks if an interval in the map overlaps with the given interval. - #[inline] - pub fn overlap(&self, interval: &Interval) -> bool { - let node_idx = self.search(interval); - !self.node_ref(node_idx, Node::is_sentinel) - } - - /// Finds all intervals in the map that overlaps with the given interval. - #[inline] - pub fn find_all_overlap(&self, interval: &Interval) -> Vec<(&Interval, &V)> { - if self.node_ref(self.root, Node::is_sentinel) { - Vec::new() - } else { - self.find_all_overlap_inner_unordered(self.root, interval) - } - } - - /// Returns a reference to the value corresponding to the key. - #[inline] - pub fn get(&self, interval: &Interval) -> Option<&V> { - self.search_exact(interval) - .map(|idx| self.node_ref(idx, Node::value)) - } - - /// Returns a reference to the value corresponding to the key. - #[inline] - pub fn get_mut(&mut self, interval: &Interval) -> Option<&mut V> { - self.search_exact(interval) - .map(|idx| self.node_mut(idx, Node::value_mut)) - } - - /// Gets an iterator over the entries of the map, sorted by key. - #[inline] - #[must_use] - pub fn iter(&self) -> Iter<'_, T, V, Ix> { - Iter { - map_ref: self, - stack: None, - } - } - - /// Gets the given key's corresponding entry in the map for in-place manipulation. - #[inline] - pub fn entry(&mut self, interval: Interval) -> Entry<'_, T, V, Ix> { - match self.search_exact(&interval) { - Some(node) => Entry::Occupied(OccupiedEntry { - map_ref: self, - node, - }), - None => Entry::Vacant(VacantEntry { - map_ref: self, - interval, - }), - } - } - - /// Removes all elements from the map - #[inline] - pub fn clear(&mut self) { - self.nodes.clear(); - self.nodes.push(Self::new_sentinel()); - self.root = Self::sentinel(); - self.len = 0; - } - - /// Returns the number of elements in the map. - #[inline] - #[must_use] - pub fn len(&self) -> usize { - self.len - } - - /// Returns `true` if the map contains no elements. - #[inline] - #[must_use] - pub fn is_empty(&self) -> bool { - self.len() == 0 - } -} - -impl IntervalMap -where - T: Ord, -{ - /// Creates an empty `IntervalMap` - #[must_use] - #[inline] - pub fn new() -> Self { - Self { - nodes: vec![Self::new_sentinel()], - root: Self::sentinel(), - len: 0, - } - } -} - -impl Default for IntervalMap -where - T: Ord, -{ - #[inline] - fn default() -> Self { - Self::with_capacity(0) - } -} - -impl IntervalMap -where - T: Ord, - Ix: IndexType, -{ - /// Creates a new sentinel node - fn new_sentinel() -> Node { - Node { - interval: None, - value: None, - max_index: None, - left: None, - right: None, - parent: None, - color: Color::Black, - } - } - - /// Creates a new tree node - fn new_node(interval: Interval, value: V, index: NodeIndex) -> Node { - Node { - max_index: Some(index), - interval: Some(interval), - value: Some(value), - left: Some(Self::sentinel()), - right: Some(Self::sentinel()), - parent: Some(Self::sentinel()), - color: Color::Red, - } - } - - /// Gets the sentinel node index - fn sentinel() -> NodeIndex { - NodeIndex::new(0) - } -} - -impl IntervalMap -where - T: Ord, - Ix: IndexType, -{ - /// Inserts a node into the tree. - fn insert_inner(&mut self, z: NodeIndex) -> Option { - let mut y = Self::sentinel(); - let mut x = self.root; - - while !self.node_ref(x, Node::is_sentinel) { - y = x; - if self.node_ref(z, Node::interval) == self.node_ref(y, Node::interval) { - let zval = self.node_mut(z, Node::take_value); - let old_value = self.node_mut(y, Node::set_value(zval)); - return Some(old_value); - } - if self.node_ref(z, Node::interval) < self.node_ref(x, Node::interval) { - x = self.node_ref(x, Node::left); - } else { - x = self.node_ref(x, Node::right); - } - } - self.node_mut(z, Node::set_parent(y)); - if self.node_ref(y, Node::is_sentinel) { - self.root = z; - } else { - if self.node_ref(z, Node::interval) < self.node_ref(y, Node::interval) { - self.node_mut(y, Node::set_left(z)); - } else { - self.node_mut(y, Node::set_right(z)); - } - self.update_max_bottom_up(y); - } - self.node_mut(z, Node::set_color(Color::Red)); - - self.insert_fixup(z); - - self.len = self.len.wrapping_add(1); - None - } - - /// Removes a node from the tree. - fn remove_inner(&mut self, z: NodeIndex) { - let mut y = z; - let mut y_orig_color = self.node_ref(y, Node::color); - let x; - if self.left_ref(z, Node::is_sentinel) { - x = self.node_ref(z, Node::right); - self.transplant(z, x); - self.update_max_bottom_up(self.node_ref(z, Node::parent)); - } else if self.right_ref(z, Node::is_sentinel) { - x = self.node_ref(z, Node::left); - self.transplant(z, x); - self.update_max_bottom_up(self.node_ref(z, Node::parent)); - } else { - y = self.tree_minimum(self.node_ref(z, Node::right)); - let mut p = y; - y_orig_color = self.node_ref(y, Node::color); - x = self.node_ref(y, Node::right); - if self.node_ref(y, Node::parent) == z { - self.node_mut(x, Node::set_parent(y)); - } else { - self.transplant(y, x); - p = self.node_ref(y, Node::parent); - self.node_mut(y, Node::set_right(self.node_ref(z, Node::right))); - self.right_mut(y, Node::set_parent(y)); - } - self.transplant(z, y); - self.node_mut(y, Node::set_left(self.node_ref(z, Node::left))); - self.left_mut(y, Node::set_parent(y)); - self.node_mut(y, Node::set_color(self.node_ref(z, Node::color))); - - self.update_max_bottom_up(p); - } - - if matches!(y_orig_color, Color::Black) { - self.remove_fixup(x); - } - - self.len = self.len.wrapping_sub(1); - } - - /// Finds all intervals in the map that overlaps with the given interval. - #[cfg(interval_tree_find_overlap_ordered)] - fn find_all_overlap_inner( - &self, - x: NodeIndex, - interval: &Interval, - ) -> Vec<(&Interval, &V)> { - let mut list = vec![]; - if self.node_ref(x, Node::interval).overlap(interval) { - list.push(self.node_ref(x, |nx| (nx.interval(), nx.value()))); - } - if self.max(self.node_ref(x, Node::left)) >= Some(&interval.low) { - list.extend(self.find_all_overlap_inner(self.node_ref(x, Node::left), interval)); - } - if self - .max(self.node_ref(x, Node::right)) - .map(|rmax| IntervalRef::new(&self.node_ref(x, Node::interval).low, rmax)) - .is_some_and(|i| i.overlap(interval)) - { - list.extend(self.find_all_overlap_inner(self.node_ref(x, Node::right), interval)); - } - list - } - - /// Finds all intervals in the map that overlaps with the given interval. - /// - /// The result is unordered because of breadth-first search to save stack size - fn find_all_overlap_inner_unordered( - &self, - x: NodeIndex, - interval: &Interval, - ) -> Vec<(&Interval, &V)> { - let mut list = Vec::new(); - let mut queue = VecDeque::new(); - queue.push_back(x); - while let Some(p) = queue.pop_front() { - if self.node_ref(p, Node::interval).overlap(interval) { - list.push(self.node_ref(p, |np| (np.interval(), np.value()))); - } - let p_left = self.node_ref(p, Node::left); - let p_right = self.node_ref(p, Node::right); - if self.max(p_left) >= Some(&interval.low) { - queue.push_back(p_left); - } - if self - .max(self.node_ref(p, Node::right)) - .map(|rmax| IntervalRef::new(&self.node_ref(p, Node::interval).low, rmax)) - .is_some_and(|i| i.overlap(interval)) - { - queue.push_back(p_right); - } - } - - list - } - - /// Search for an interval that overlaps with the given interval. - fn search(&self, interval: &Interval) -> NodeIndex { - let mut x = self.root; - while self - .node_ref(x, Node::sentinel) - .map(Node::interval) - .is_some_and(|xi| !xi.overlap(interval)) - { - if self.max(self.node_ref(x, Node::left)) > Some(&interval.low) { - x = self.node_ref(x, Node::left); - } else { - x = self.node_ref(x, Node::right); - } - } - x - } - - /// Search for the node with exact the given interval - fn search_exact(&self, interval: &Interval) -> Option> { - let mut x = self.root; - while !self.node_ref(x, Node::is_sentinel) { - if self.node_ref(x, Node::interval) == interval { - return Some(x); - } - if self.max(x) < Some(&interval.high) { - return None; - } - if self.node_ref(x, Node::interval) > interval { - x = self.node_ref(x, Node::left); - } else { - x = self.node_ref(x, Node::right); - } - } - None - } - - /// Restores red-black tree properties after an insert. - fn insert_fixup(&mut self, mut z: NodeIndex) { - while self.parent_ref(z, Node::is_red) { - if self.grand_parent_ref(z, Node::is_sentinel) { - break; - } - if self.is_left_child(self.node_ref(z, Node::parent)) { - let y = self.grand_parent_ref(z, Node::right); - if self.node_ref(y, Node::is_red) { - self.parent_mut(z, Node::set_color(Color::Black)); - self.node_mut(y, Node::set_color(Color::Black)); - self.grand_parent_mut(z, Node::set_color(Color::Red)); - z = self.parent_ref(z, Node::parent); - } else { - if self.is_right_child(z) { - z = self.node_ref(z, Node::parent); - self.left_rotate(z); - } - self.parent_mut(z, Node::set_color(Color::Black)); - self.grand_parent_mut(z, Node::set_color(Color::Red)); - self.right_rotate(self.parent_ref(z, Node::parent)); - } - } else { - let y = self.grand_parent_ref(z, Node::left); - if self.node_ref(y, Node::is_red) { - self.parent_mut(z, Node::set_color(Color::Black)); - self.node_mut(y, Node::set_color(Color::Black)); - self.grand_parent_mut(z, Node::set_color(Color::Red)); - z = self.parent_ref(z, Node::parent); - } else { - if self.is_left_child(z) { - z = self.node_ref(z, Node::parent); - self.right_rotate(z); - } - self.parent_mut(z, Node::set_color(Color::Black)); - self.grand_parent_mut(z, Node::set_color(Color::Red)); - self.left_rotate(self.parent_ref(z, Node::parent)); - } - } - } - self.node_mut(self.root, Node::set_color(Color::Black)); - } - - /// Restores red-black tree properties after a remove. - fn remove_fixup(&mut self, mut x: NodeIndex) { - while x != self.root && self.node_ref(x, Node::is_black) { - let mut w; - if self.is_left_child(x) { - w = self.parent_ref(x, Node::right); - if self.node_ref(w, Node::is_red) { - self.node_mut(w, Node::set_color(Color::Black)); - self.parent_mut(x, Node::set_color(Color::Red)); - self.left_rotate(self.node_ref(x, Node::parent)); - w = self.parent_ref(x, Node::right); - } - if self.node_ref(w, Node::is_sentinel) { - break; - } - if self.left_ref(w, Node::is_black) && self.right_ref(w, Node::is_black) { - self.node_mut(w, Node::set_color(Color::Red)); - x = self.node_ref(x, Node::parent); - } else { - if self.right_ref(w, Node::is_black) { - self.left_mut(w, Node::set_color(Color::Black)); - self.node_mut(w, Node::set_color(Color::Red)); - self.right_rotate(w); - w = self.parent_ref(x, Node::right); - } - self.node_mut(w, Node::set_color(self.parent_ref(x, Node::color))); - self.parent_mut(x, Node::set_color(Color::Black)); - self.right_mut(w, Node::set_color(Color::Black)); - self.left_rotate(self.node_ref(x, Node::parent)); - x = self.root; - } - } else { - w = self.parent_ref(x, Node::left); - if self.node_ref(w, Node::is_red) { - self.node_mut(w, Node::set_color(Color::Black)); - self.parent_mut(x, Node::set_color(Color::Red)); - self.right_rotate(self.node_ref(x, Node::parent)); - w = self.parent_ref(x, Node::left); - } - if self.node_ref(w, Node::is_sentinel) { - break; - } - if self.right_ref(w, Node::is_black) && self.left_ref(w, Node::is_black) { - self.node_mut(w, Node::set_color(Color::Red)); - x = self.node_ref(x, Node::parent); - } else { - if self.left_ref(w, Node::is_black) { - self.right_mut(w, Node::set_color(Color::Black)); - self.node_mut(w, Node::set_color(Color::Red)); - self.left_rotate(w); - w = self.parent_ref(x, Node::left); - } - self.node_mut(w, Node::set_color(self.parent_ref(x, Node::color))); - self.parent_mut(x, Node::set_color(Color::Black)); - self.left_mut(w, Node::set_color(Color::Black)); - self.right_rotate(self.node_ref(x, Node::parent)); - x = self.root; - } - } - } - self.node_mut(x, Node::set_color(Color::Black)); - } - - /// Binary tree left rotate. - fn left_rotate(&mut self, x: NodeIndex) { - if self.right_ref(x, Node::is_sentinel) { - return; - } - let y = self.node_ref(x, Node::right); - self.node_mut(x, Node::set_right(self.node_ref(y, Node::left))); - if !self.left_ref(y, Node::is_sentinel) { - self.left_mut(y, Node::set_parent(x)); - } - - self.replace_parent(x, y); - self.node_mut(y, Node::set_left(x)); - - self.rotate_update_max(x, y); - } - - /// Binary tree right rotate. - fn right_rotate(&mut self, x: NodeIndex) { - if self.left_ref(x, Node::is_sentinel) { - return; - } - let y = self.node_ref(x, Node::left); - self.node_mut(x, Node::set_left(self.node_ref(y, Node::right))); - if !self.right_ref(y, Node::is_sentinel) { - self.right_mut(y, Node::set_parent(x)); - } - - self.replace_parent(x, y); - self.node_mut(y, Node::set_right(x)); - - self.rotate_update_max(x, y); - } - - /// Replaces parent during a rotation. - fn replace_parent(&mut self, x: NodeIndex, y: NodeIndex) { - self.node_mut(y, Node::set_parent(self.node_ref(x, Node::parent))); - if self.parent_ref(x, Node::is_sentinel) { - self.root = y; - } else if self.is_left_child(x) { - self.parent_mut(x, Node::set_left(y)); - } else { - self.parent_mut(x, Node::set_right(y)); - } - self.node_mut(x, Node::set_parent(y)); - } - - /// Updates the max value after a rotation. - fn rotate_update_max(&mut self, x: NodeIndex, y: NodeIndex) { - self.node_mut(y, Node::set_max_index(self.node_ref(x, Node::max_index))); - self.recaculate_max(x); - } - - /// Updates the max value towards the root - fn update_max_bottom_up(&mut self, x: NodeIndex) { - let mut p = x; - while !self.node_ref(p, Node::is_sentinel) { - self.recaculate_max(p); - p = self.node_ref(p, Node::parent); - } - } - - /// Recaculate max value from left and right childrens - fn recaculate_max(&mut self, x: NodeIndex) { - self.node_mut(x, Node::set_max_index(x)); - let x_left = self.node_ref(x, Node::left); - let x_right = self.node_ref(x, Node::right); - if self.max(x_left) > self.max(x) { - self.node_mut( - x, - Node::set_max_index(self.node_ref(x_left, Node::max_index)), - ); - } - if self.max(x_right) > self.max(x) { - self.node_mut( - x, - Node::set_max_index(self.node_ref(x_right, Node::max_index)), - ); - } - } - - /// Finds the node with the minimum interval. - fn tree_minimum(&self, mut x: NodeIndex) -> NodeIndex { - while !self.left_ref(x, Node::is_sentinel) { - x = self.node_ref(x, Node::left); - } - x - } - - /// Replaces one subtree as a child of its parent with another subtree. - fn transplant(&mut self, u: NodeIndex, v: NodeIndex) { - if self.parent_ref(u, Node::is_sentinel) { - self.root = v; - } else if self.is_left_child(u) { - self.parent_mut(u, Node::set_left(v)); - } else { - self.parent_mut(u, Node::set_right(v)); - } - self.node_mut(v, Node::set_parent(self.node_ref(u, Node::parent))); - } - - /// Checks if a node is a left child of its parent. - fn is_left_child(&self, node: NodeIndex) -> bool { - self.parent_ref(node, Node::left) == node - } - - /// Checks if a node is a right child of its parent. - fn is_right_child(&self, node: NodeIndex) -> bool { - self.parent_ref(node, Node::right) == node - } - - /// Updates nodes indices after remove - /// - /// This method has a time complexity of `O(logn)`, as we need to - /// update the max index from bottom to top. - fn update_idx(&mut self, old: NodeIndex, new: NodeIndex) { - if self.root == old { - self.root = new; - } - if self.nodes.get(new.index()).is_some() { - if !self.parent_ref(new, Node::is_sentinel) { - if self.parent_ref(new, Node::left) == old { - self.parent_mut(new, Node::set_left(new)); - } else { - self.parent_mut(new, Node::set_right(new)); - } - } - self.left_mut(new, Node::set_parent(new)); - self.right_mut(new, Node::set_parent(new)); - - let mut p = new; - while !self.node_ref(p, Node::is_sentinel) { - if self.node_ref(p, Node::max_index) == old { - self.node_mut(p, Node::set_max_index(new)); - } - p = self.node_ref(p, Node::parent); - } - } - } -} - -// Convenient methods for reference or mutate current/parent/left/right node -#[allow(clippy::missing_docs_in_private_items)] // Trivial convenient methods -#[allow(clippy::indexing_slicing)] // Won't panic since all the indices we used are inbound -impl<'a, T, V, Ix> IntervalMap -where - Ix: IndexType, -{ - fn node_ref(&'a self, node: NodeIndex, op: F) -> R - where - R: 'a, - F: FnOnce(&'a Node) -> R, - { - op(&self.nodes[node.index()]) - } - - fn node_mut(&'a mut self, node: NodeIndex, op: F) -> R - where - R: 'a, - F: FnOnce(&'a mut Node) -> R, - { - op(&mut self.nodes[node.index()]) - } - - fn left_ref(&'a self, node: NodeIndex, op: F) -> R - where - R: 'a, - F: FnOnce(&'a Node) -> R, - { - let idx = self.nodes[node.index()].left().index(); - op(&self.nodes[idx]) - } - - fn right_ref(&'a self, node: NodeIndex, op: F) -> R - where - R: 'a, - F: FnOnce(&'a Node) -> R, - { - let idx = self.nodes[node.index()].right().index(); - op(&self.nodes[idx]) - } - - fn parent_ref(&'a self, node: NodeIndex, op: F) -> R - where - R: 'a, - F: FnOnce(&'a Node) -> R, - { - let idx = self.nodes[node.index()].parent().index(); - op(&self.nodes[idx]) - } - - fn grand_parent_ref(&'a self, node: NodeIndex, op: F) -> R - where - R: 'a, - F: FnOnce(&'a Node) -> R, - { - let parent_idx = self.nodes[node.index()].parent().index(); - let grand_parent_idx = self.nodes[parent_idx].parent().index(); - op(&self.nodes[grand_parent_idx]) - } - - fn left_mut(&'a mut self, node: NodeIndex, op: F) -> R - where - R: 'a, - F: FnOnce(&'a mut Node) -> R, - { - let idx = self.nodes[node.index()].left().index(); - op(&mut self.nodes[idx]) - } - - fn right_mut(&'a mut self, node: NodeIndex, op: F) -> R - where - R: 'a, - F: FnOnce(&'a mut Node) -> R, - { - let idx = self.nodes[node.index()].right().index(); - op(&mut self.nodes[idx]) - } - - fn parent_mut(&'a mut self, node: NodeIndex, op: F) -> R - where - R: 'a, - F: FnOnce(&'a mut Node) -> R, - { - let idx = self.nodes[node.index()].parent().index(); - op(&mut self.nodes[idx]) - } - - fn grand_parent_mut(&'a mut self, node: NodeIndex, op: F) -> R - where - R: 'a, - F: FnOnce(&'a mut Node) -> R, - { - let parent_idx = self.nodes[node.index()].parent().index(); - let grand_parent_idx = self.nodes[parent_idx].parent().index(); - op(&mut self.nodes[grand_parent_idx]) - } - - fn max(&self, node: NodeIndex) -> Option<&T> { - let max_index = self.nodes[node.index()].max_index?.index(); - self.nodes[max_index].interval.as_ref().map(|i| &i.high) - } -} - -/// An iterator over the entries of a `IntervalMap`. -#[derive(Debug)] -pub struct Iter<'a, T, V, Ix> { - /// Reference to the map - map_ref: &'a IntervalMap, - /// Stack for iteration - stack: Option>>, -} - -impl Iter<'_, T, V, Ix> -where - Ix: IndexType, -{ - /// Initializes the stack - fn init_stack(&mut self) { - self.stack = Some(Self::left_link(self.map_ref, self.map_ref.root)); - } - - /// Pushes a link of nodes on the left to stack. - fn left_link(map_ref: &IntervalMap, mut x: NodeIndex) -> Vec> { - let mut nodes = vec![]; - while !map_ref.node_ref(x, Node::is_sentinel) { - nodes.push(x); - x = map_ref.node_ref(x, Node::left); - } - nodes - } -} - -impl<'a, T, V, Ix> Iterator for Iter<'a, T, V, Ix> -where - Ix: IndexType, -{ - type Item = (&'a Interval, &'a V); - - #[allow(clippy::unwrap_used, clippy::unwrap_in_result)] - #[inline] - fn next(&mut self) -> Option { - if self.stack.is_none() { - self.init_stack(); - } - let stack = self.stack.as_mut().unwrap(); - if stack.is_empty() { - return None; - } - let x = stack.pop().unwrap(); - stack.extend(Self::left_link( - self.map_ref, - self.map_ref.node_ref(x, Node::right), - )); - Some(self.map_ref.node_ref(x, |xn| (xn.interval(), xn.value()))) - } -} - -/// A view into a single entry in a map, which may either be vacant or occupied. -#[allow(clippy::exhaustive_enums)] // It is final -#[derive(Debug)] -pub enum Entry<'a, T, V, Ix> { - /// An occupied entry. - Occupied(OccupiedEntry<'a, T, V, Ix>), - /// A vacant entry. - Vacant(VacantEntry<'a, T, V, Ix>), -} - -/// A view into an occupied entry in a `IntervalMap`. -/// It is part of the [`Entry`] enum. -#[derive(Debug)] -pub struct OccupiedEntry<'a, T, V, Ix> { - /// Reference to the map - map_ref: &'a mut IntervalMap, - /// The entry node - node: NodeIndex, -} - -/// A view into a vacant entry in a `IntervalMap`. -/// It is part of the [`Entry`] enum. -#[derive(Debug)] -pub struct VacantEntry<'a, T, V, Ix> { - /// Mutable reference to the map - map_ref: &'a mut IntervalMap, - /// The interval of this entry - interval: Interval, -} - -impl<'a, T, V, Ix> Entry<'a, T, V, Ix> -where - T: Ord, - Ix: IndexType, -{ - /// Ensures a value is in the entry by inserting the default if empty, and returns - /// a mutable reference to the value in the entry. - #[inline] - pub fn or_insert(self, default: V) -> &'a mut V { - match self { - Entry::Occupied(entry) => entry.map_ref.node_mut(entry.node, Node::value_mut), - Entry::Vacant(entry) => { - let entry_idx = NodeIndex::new(entry.map_ref.nodes.len()); - let _ignore = entry.map_ref.insert(entry.interval, default); - entry.map_ref.node_mut(entry_idx, Node::value_mut) - } - } - } - - /// Provides in-place mutable access to an occupied entry before any - /// potential inserts into the map. - /// - /// # Panics - /// - /// This method panics when the node is a sentinel node - #[inline] - #[must_use] - pub fn and_modify(self, f: F) -> Self - where - F: FnOnce(&mut V), - { - match self { - Entry::Occupied(entry) => { - f(entry.map_ref.node_mut(entry.node, Node::value_mut)); - Self::Occupied(entry) - } - Entry::Vacant(entry) => Self::Vacant(entry), - } - } -} - -// TODO: better typed `Node` -/// Node of the interval tree -#[derive(Debug)] -pub struct Node { - /// Left children - left: Option>, - /// Right children - right: Option>, - /// Parent - parent: Option>, - /// Color of the node - color: Color, - - /// Interval of the node - interval: Option>, - /// The index that point to the node with the max value - max_index: Option>, - /// Value of the node - value: Option, -} - -// Convenient getter/setter methods -#[allow(clippy::missing_docs_in_private_items)] -#[allow(clippy::missing_docs_in_private_items)] // Trivial convenient methods -#[allow(clippy::unwrap_used)] // Won't panic since the conditions are checked in the implementation -impl Node -where - Ix: IndexType, -{ - fn color(&self) -> Color { - self.color - } - - fn interval(&self) -> &Interval { - self.interval.as_ref().unwrap() - } - - fn max_index(&self) -> NodeIndex { - self.max_index.unwrap() - } - - fn left(&self) -> NodeIndex { - self.left.unwrap() - } - - fn right(&self) -> NodeIndex { - self.right.unwrap() - } - - fn parent(&self) -> NodeIndex { - self.parent.unwrap() - } - - fn is_sentinel(&self) -> bool { - self.interval.is_none() - } - - fn sentinel(&self) -> Option<&Self> { - self.interval.is_some().then_some(self) - } - - fn is_black(&self) -> bool { - matches!(self.color, Color::Black) - } - - fn is_red(&self) -> bool { - matches!(self.color, Color::Red) - } - - fn value(&self) -> &V { - self.value.as_ref().unwrap() - } - - fn value_mut(&mut self) -> &mut V { - self.value.as_mut().unwrap() - } - - fn take_value(&mut self) -> V { - self.value.take().unwrap() - } - - fn set_value(value: V) -> impl FnOnce(&mut Node) -> V { - move |node: &mut Node| node.value.replace(value).unwrap() - } - - fn set_color(color: Color) -> impl FnOnce(&mut Node) { - move |node: &mut Node| { - node.color = color; - } - } - - fn set_max_index(max_index: NodeIndex) -> impl FnOnce(&mut Node) { - move |node: &mut Node| { - let _ignore = node.max_index.replace(max_index); - } - } - - fn set_left(left: NodeIndex) -> impl FnOnce(&mut Node) { - move |node: &mut Node| { - let _ignore = node.left.replace(left); - } - } - - fn set_right(right: NodeIndex) -> impl FnOnce(&mut Node) { - move |node: &mut Node| { - let _ignore = node.right.replace(right); - } - } - - fn set_parent(parent: NodeIndex) -> impl FnOnce(&mut Node) { - move |node: &mut Node| { - let _ignore = node.parent.replace(parent); - } - } -} - -/// The Interval stored in `IntervalMap` -/// Represents the interval [low, high) -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[non_exhaustive] -pub struct Interval { - /// Low value - pub low: T, - /// high value - pub high: T, -} - -impl Interval { - /// Creates a new `Interval` - /// - /// # Panics - /// - /// This method panics when low is greater than high - #[inline] - pub fn new(low: T, high: T) -> Self { - assert!(low < high, "invalid range"); - Self { low, high } - } - - /// Checks if self overlaps with other interval - #[inline] - pub fn overlap(&self, other: &Self) -> bool { - self.high > other.low && other.high > self.low - } -} - -/// Reference type of `Interval` -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -struct IntervalRef<'a, T> { - /// Low value - low: &'a T, - /// high value - high: &'a T, -} - -impl<'a, T: Ord> IntervalRef<'a, T> { - /// Creates a new `IntervalRef` - /// - /// # Panics - /// - /// This method panics when low is greater than high - #[inline] - fn new(low: &'a T, high: &'a T) -> Self { - assert!(low < high, "invalid range"); - Self { low, high } - } - - /// Checks if self overlaps with a `Interval` - fn overlap(&self, other: &Interval) -> bool { - self.high > &other.low && &other.high > self.low - } -} - -/// The color of the node -#[derive(Debug, Clone, Copy)] -enum Color { - /// Red node - Red, - /// Black node - Black, -} diff --git a/crates/utils/src/interval_map/tests.rs b/crates/utils/src/interval_map/tests.rs deleted file mode 100644 index ca63a5c51..000000000 --- a/crates/utils/src/interval_map/tests.rs +++ /dev/null @@ -1,322 +0,0 @@ -use std::collections::HashSet; - -use rand::{rngs::StdRng, Rng, SeedableRng}; - -use super::*; - -struct IntervalGenerator { - rng: StdRng, - unique: HashSet>, - limit: i32, -} - -impl IntervalGenerator { - fn new(seed: [u8; 32]) -> Self { - const LIMIT: i32 = 1000; - Self { - rng: SeedableRng::from_seed(seed), - unique: HashSet::new(), - limit: LIMIT, - } - } - - fn next(&mut self) -> Interval { - let low = self.rng.gen_range(0..self.limit - 1); - let high = self.rng.gen_range((low + 1)..self.limit); - Interval::new(low, high) - } - - fn next_unique(&mut self) -> Interval { - let mut interval = self.next(); - while self.unique.contains(&interval) { - interval = self.next(); - } - self.unique.insert(interval.clone()); - interval - } - - fn next_with_range(&mut self, range: i32) -> Interval { - let low = self.rng.gen_range(0..self.limit - 1); - let high = self - .rng - .gen_range((low + 1)..self.limit.min(low + 1 + range)); - Interval::new(low, high) - } -} - -impl IntervalMap { - fn check_max(&self) { - let _ignore = self.check_max_inner(self.root); - } - - fn check_max_inner(&self, x: NodeIndex) -> i32 { - if self.node_ref(x, Node::is_sentinel) { - return 0; - } - let l_max = self.check_max_inner(self.node_ref(x, Node::left)); - let r_max = self.check_max_inner(self.node_ref(x, Node::right)); - let max = self.node_ref(x, |x| x.interval().high.max(l_max).max(r_max)); - assert_eq!(self.max(x), Some(&max)); - max - } - - /// 1. Every node is either red or black. - /// 2. The root is black. - /// 3. Every leaf (NIL) is black. - /// 4. If a node is red, then both its children are black. - /// 5. For each node, all simple paths from the node to descendant leaves contain the - /// same number of black nodes. - fn check_rb_properties(&self) { - assert!(matches!( - self.node_ref(self.root, Node::color), - Color::Black - )); - self.check_children_color(self.root); - self.check_black_height(self.root); - } - - fn check_children_color(&self, x: NodeIndex) { - if self.node_ref(x, Node::is_sentinel) { - return; - } - self.check_children_color(self.node_ref(x, Node::left)); - self.check_children_color(self.node_ref(x, Node::right)); - if self.node_ref(x, Node::is_red) { - assert!(matches!(self.left_ref(x, Node::color), Color::Black)); - assert!(matches!(self.right_ref(x, Node::color), Color::Black)); - } - } - - fn check_black_height(&self, x: NodeIndex) -> usize { - if self.node_ref(x, Node::is_sentinel) { - return 0; - } - let lefth = self.check_black_height(self.node_ref(x, Node::left)); - let righth = self.check_black_height(self.node_ref(x, Node::right)); - assert_eq!(lefth, righth); - if self.node_ref(x, Node::is_black) { - return lefth + 1; - } - lefth - } -} - -fn with_map_and_generator(test_fn: impl Fn(IntervalMap, IntervalGenerator)) { - let seeds = vec![[0; 32], [1; 32], [2; 32]]; - for seed in seeds { - let gen = IntervalGenerator::new(seed); - let map = IntervalMap::new(); - test_fn(map, gen); - } -} - -#[test] -fn red_black_tree_properties_is_satisfied() { - with_map_and_generator(|mut map, mut gen| { - let intervals: Vec<_> = std::iter::repeat_with(|| gen.next_unique()) - .take(1000) - .collect(); - for i in intervals.clone() { - let _ignore = map.insert(i, ()); - } - map.check_rb_properties(); - }); -} - -#[test] -#[should_panic(expected = "invalid range")] -fn invalid_range_should_panic() { - let _interval = Interval::new(3, 1); -} - -#[test] -fn insert_equal_interval_returns_previous_value() { - let mut map = IntervalMap::new(); - map.insert(Interval::new(1, 3), 1); - assert_eq!(map.insert(Interval::new(1, 3), 2), Some(1)); - assert_eq!(map.insert(Interval::new(1, 3), 3), Some(2)); -} - -#[test] -fn map_len_will_update() { - with_map_and_generator(|mut map, mut gen| { - let intervals: Vec<_> = std::iter::repeat_with(|| gen.next_unique()) - .take(100) - .collect(); - for i in intervals.clone() { - let _ignore = map.insert(i, ()); - } - assert_eq!(map.len(), 100); - for i in intervals { - let _ignore = map.remove(&i); - } - assert_eq!(map.len(), 0); - }); -} - -#[test] -fn check_overlap_is_ok_simple() { - let mut map = IntervalMap::new(); - map.insert(Interval::new(1, 3), ()); - map.insert(Interval::new(6, 7), ()); - map.insert(Interval::new(9, 11), ()); - assert!(map.overlap(&Interval::new(2, 5))); - assert!(map.overlap(&Interval::new(1, 17))); - assert!(!map.overlap(&Interval::new(4, 5))); - assert!(!map.overlap(&Interval::new(20, 23))); -} - -#[test] -fn check_overlap_is_ok() { - with_map_and_generator(|mut map, mut gen| { - let intervals: Vec<_> = std::iter::repeat_with(|| gen.next_with_range(10)) - .take(100) - .collect(); - for i in intervals.clone() { - let _ignore = map.insert(i, ()); - } - let to_check: Vec<_> = std::iter::repeat_with(|| gen.next_with_range(10)) - .take(1000) - .collect(); - let expects: Vec<_> = to_check - .iter() - .map(|ci| intervals.iter().any(|i| ci.overlap(i))) - .collect(); - - for (ci, expect) in to_check.into_iter().zip(expects.into_iter()) { - assert_eq!(map.overlap(&ci), expect); - } - }); -} - -#[test] -fn check_max_is_ok() { - with_map_and_generator(|mut map, mut gen| { - let intervals: Vec<_> = std::iter::repeat_with(|| gen.next_unique()) - .take(1000) - .collect(); - for i in intervals.clone() { - let _ignore = map.insert(i, ()); - map.check_max(); - } - assert_eq!(map.len(), 1000); - for i in intervals { - let _ignore = map.remove(&i); - map.check_max(); - } - }); -} - -#[test] -fn remove_non_exist_interval_will_do_nothing() { - with_map_and_generator(|mut map, mut gen| { - let intervals: Vec<_> = std::iter::repeat_with(|| gen.next_unique()) - .take(1000) - .collect(); - for i in intervals { - let _ignore = map.insert(i, ()); - } - assert_eq!(map.len(), 1000); - let to_remove: Vec<_> = std::iter::repeat_with(|| gen.next_unique()) - .take(1000) - .collect(); - for i in to_remove { - let _ignore = map.remove(&i); - } - assert_eq!(map.len(), 1000); - }); -} - -#[test] -fn find_all_overlap_is_ok_simple() { - let mut map = IntervalMap::new(); - map.insert(Interval::new(1, 3), ()); - map.insert(Interval::new(2, 4), ()); - map.insert(Interval::new(6, 7), ()); - map.insert(Interval::new(7, 11), ()); - assert_eq!(map.find_all_overlap(&Interval::new(2, 7)).len(), 3); - map.remove(&Interval::new(1, 3)); - assert_eq!(map.find_all_overlap(&Interval::new(2, 7)).len(), 2); -} - -#[test] -fn find_all_overlap_is_ok() { - with_map_and_generator(|mut map, mut gen| { - let intervals: Vec<_> = std::iter::repeat_with(|| gen.next_unique()) - .take(1000) - .collect(); - for i in intervals.clone() { - let _ignore = map.insert(i, ()); - } - let to_find: Vec<_> = std::iter::repeat_with(|| gen.next()).take(1000).collect(); - - let expects: Vec> = to_find - .iter() - .map(|ti| intervals.iter().filter(|i| ti.overlap(i)).collect()) - .collect(); - - for (ti, mut expect) in to_find.into_iter().zip(expects.into_iter()) { - let mut result = map.find_all_overlap(&ti); - expect.sort_unstable(); - result.sort_unstable(); - assert_eq!(expect.len(), result.len()); - for (e, r) in expect.into_iter().zip(result.into_iter()) { - assert_eq!(e, r.0); - } - } - }); -} - -#[test] -fn entry_modify_is_ok() { - let mut map = IntervalMap::new(); - map.insert(Interval::new(1, 3), 1); - map.insert(Interval::new(2, 4), 2); - map.insert(Interval::new(6, 7), 3); - map.insert(Interval::new(7, 11), 4); - let _ignore = map.entry(Interval::new(6, 7)).and_modify(|v| *v += 1); - assert_eq!(map.get(&Interval::new(1, 3)), Some(&1)); - assert_eq!(map.get(&Interval::new(2, 4)), Some(&2)); - assert_eq!(map.get(&Interval::new(6, 7)), Some(&4)); - assert_eq!(map.get(&Interval::new(7, 11)), Some(&4)); - assert_eq!(map.get(&Interval::new(5, 17)), None); - map.entry(Interval::new(3, 5)) - .and_modify(|v| *v += 1) - .or_insert(0); - let _ignore = map.get_mut(&Interval::new(3, 5)).map(|v| *v += 1); - assert_eq!(map.get(&Interval::new(3, 5)), Some(&1)); -} - -#[test] -fn iterate_through_map_is_sorted() { - with_map_and_generator(|mut map, mut gen| { - let mut intervals: Vec<_> = std::iter::repeat_with(|| gen.next_unique()) - .enumerate() - .take(1000) - .collect(); - for (v, i) in intervals.clone() { - let _ignore = map.insert(i, v); - } - intervals.sort_unstable_by(|a, b| a.1.cmp(&b.1)); - - #[allow(clippy::pattern_type_mismatch)] - for ((ei, ev), (v, i)) in map.iter().zip(intervals.iter()) { - assert_eq!(ei, i); - assert_eq!(ev, v); - } - }); -} - -#[test] -fn interval_map_clear_is_ok() { - let mut map = IntervalMap::new(); - map.insert(Interval::new(1, 3), 1); - map.insert(Interval::new(2, 4), 2); - map.insert(Interval::new(6, 7), 3); - assert_eq!(map.len(), 3); - map.clear(); - assert_eq!(map.len(), 0); - assert!(map.is_empty()); - assert_eq!(map.nodes.len(), 1); - assert!(map.nodes[0].is_sentinel()); -} diff --git a/crates/utils/src/lca_tree.rs b/crates/utils/src/lca_tree.rs new file mode 100644 index 000000000..9e76ad135 --- /dev/null +++ b/crates/utils/src/lca_tree.rs @@ -0,0 +1,175 @@ +use std::ops::{Add, Sub as _}; + +/// A LCA tree to accelerate Txns' key overlap validation +#[non_exhaustive] +#[derive(Debug)] +pub struct LCATree { + /// + nodes: Vec, +} + +/// +#[non_exhaustive] +#[derive(Debug)] +pub struct LCANode { + /// + pub parent: Vec, + /// + pub depth: usize, +} + +#[allow(clippy::indexing_slicing)] +impl LCATree { + /// build a `LCATree` with a sentinel node + #[inline] + #[must_use] + pub fn new() -> Self { + Self { + nodes: vec![LCANode { + parent: vec![0], + depth: 0, + }], + } + } + /// get a node by index + /// + /// # Panics + /// + /// The function panics if given `i` > max index + #[inline] + #[must_use] + pub fn get_node(&self, i: usize) -> &LCANode { + assert!(i < self.nodes.len(), "Node {i} doesn't exist"); + &self.nodes[i] + } + /// insert a node and return its index + /// + /// # Panics + /// + /// The function panics if given `parent` doesn't exist + #[inline] + #[must_use] + #[allow(clippy::as_conversions)] + pub fn insert_node(&mut self, parent: usize) -> usize { + let depth = if parent == 0 { + 0 + } else { + self.get_node(parent).depth.add(1) + }; + let mut node = LCANode { + parent: vec![], + depth, + }; + node.parent.push(parent); + let parent_num = if depth == 0 { 0 } else { depth.ilog2() } as usize; + for i in 0..parent_num { + node.parent.push(self.get_node(node.parent[i]).parent[i]); + } + self.nodes.push(node); + self.nodes.len().sub(1) + } + /// Use Binary Lifting to find the LCA of `node_a` and `node_b` + /// + /// # Panics + /// + /// The function panics if given `node_a` or `node_b` doesn't exist + #[inline] + #[must_use] + pub fn find_lca(&self, node_a: usize, node_b: usize) -> usize { + let (mut x, mut y) = if self.get_node(node_a).depth < self.get_node(node_b).depth { + (node_a, node_b) + } else { + (node_b, node_a) + }; + while self.get_node(x).depth < self.get_node(y).depth { + for ancestor in self.get_node(y).parent.iter().rev() { + if self.get_node(x).depth <= self.get_node(*ancestor).depth { + y = *ancestor; + } + } + } + while x != y { + let node_x = self.get_node(x); + let node_y = self.get_node(y); + if node_x.parent[0] == node_y.parent[0] { + x = node_x.parent[0]; + break; + } + for i in (0..node_x.parent.len()).rev() { + if node_x.parent[i] != node_y.parent[i] { + x = node_x.parent[i]; + y = node_y.parent[i]; + break; + } + } + } + x + } +} + +impl Default for LCATree { + #[inline] + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod test { + use crate::lca_tree::LCATree; + + #[test] + fn test_ilog2() { + assert_eq!(3_i32.ilog2(), 1); + assert_eq!(5_i32.ilog2(), 2); + assert_eq!(7_i32.ilog2(), 2); + assert_eq!(10_i32.ilog2(), 3); + } + + #[test] + // root + // / | \ + // / | \ + // / | \ + // node1 node2 node3 + // | \ | | + // | \ | | + // node4 node5 node6 node7 + // | \ \ + // | \ node10 + // node8 node9 + // + // + fn test_lca() { + let mut tree = LCATree::new(); + let root = 0; + let node1 = tree.insert_node(root); + let node2 = tree.insert_node(root); + let node3 = tree.insert_node(root); + + let node4 = tree.insert_node(node1); + let node5 = tree.insert_node(node1); + + let node6 = tree.insert_node(node2); + + let node7 = tree.insert_node(node3); + + let node8 = tree.insert_node(node4); + let node9 = tree.insert_node(node4); + + let node10 = tree.insert_node(node5); + + assert_eq!(tree.find_lca(node1, node2), root); + assert_eq!(tree.find_lca(node1, node3), root); + assert_eq!(tree.find_lca(node1, node4), node1); + assert_eq!(tree.find_lca(node4, node5), node1); + assert_eq!(tree.find_lca(node5, node7), root); + assert_eq!(tree.find_lca(node6, node7), root); + assert_eq!(tree.find_lca(node8, node9), node4); + assert_eq!(tree.find_lca(node8, node10), node1); + assert_eq!(tree.find_lca(node6, node10), root); + assert_eq!(tree.find_lca(node8, node5), node1); + assert_eq!(tree.find_lca(node9, node3), root); + assert_eq!(tree.find_lca(node10, node2), root); + } +} diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index 44fbdbf2e..88aca819a 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -188,8 +188,8 @@ pub struct ServerTlsConfig; pub mod barrier; /// configuration pub mod config; -/// Interval tree implementation -pub mod interval_map; +/// LCA tree implementation +pub mod lca_tree; /// utils for metrics pub mod metrics; /// utils of `parking_lot` lock @@ -211,6 +211,8 @@ pub mod tokio_lock; pub mod tracing; use ::tracing::debug; +/// Interval tree implementation +pub use interval_map; pub use parser::*; use pbkdf2::{ password_hash::{rand_core::OsRng, PasswordHasher, SaltString}, diff --git a/crates/utils/src/parser.rs b/crates/utils/src/parser.rs index e7e4e8520..15c0d7182 100644 --- a/crates/utils/src/parser.rs +++ b/crates/utils/src/parser.rs @@ -40,7 +40,9 @@ pub enum ConfigFileError { } /// parse members from string like "node1=addr1,addr2,node2=add3,addr4,addr5,node3=addr6" +/// /// # Errors +/// /// Return error when pass wrong args #[inline] pub fn parse_members(s: &str) -> Result>, ConfigParseError> { @@ -69,7 +71,9 @@ pub fn parse_members(s: &str) -> Result>, ConfigPars } /// Parse `ClusterRange` from the given string +/// /// # Errors +/// /// Return error when parsing the given string to `ClusterRange` failed #[inline] pub fn parse_range(s: &str) -> Result { @@ -86,7 +90,9 @@ pub fn parse_range(s: &str) -> Result { } /// Parse `Duration` from string +/// /// # Errors +/// /// Return error when parsing the given string to `Duration` failed #[inline] pub fn parse_duration(s: &str) -> Result { @@ -150,7 +156,9 @@ pub fn parse_duration(s: &str) -> Result { } /// Parse `InitialClusterState` from string +/// /// # Errors +/// /// Return error when parsing the given string to `InitialClusterState` failed #[inline] pub fn parse_state(s: &str) -> Result { @@ -164,7 +172,9 @@ pub fn parse_state(s: &str) -> Result { } /// Parse `LOG_PATH` from string +/// /// # Errors +/// /// Return error when parsing the given string to `PathBuf` failed #[inline] pub fn parse_log_file(s: &str) -> Result { @@ -195,7 +205,9 @@ pub fn parse_log_file(s: &str) -> Result { } /// Parse `LevelConfig` from string +/// /// # Errors +/// /// Return error when parsing the given string to `LevelConfig` failed #[inline] pub fn parse_log_level(s: &str) -> Result { @@ -212,7 +224,9 @@ pub fn parse_log_level(s: &str) -> Result { } /// Parse `RotationConfig` from string +/// /// # Errors +/// /// Return error when parsing the given string to `RotationConfig` failed #[inline] pub fn parse_rotation(s: &str) -> Result { @@ -227,7 +241,9 @@ pub fn parse_rotation(s: &str) -> Result { } /// Parse bytes from string +/// /// # Errors +/// /// Return error when parsing the given string to usize failed #[inline] #[allow(clippy::arithmetic_side_effects)] @@ -264,7 +280,9 @@ pub fn parse_batch_bytes(s: &str) -> Result { } /// Get the metrics push protocol +/// /// # Errors +/// /// Return error when parsing the given string to `MetricsPushProtocol` failed #[inline] pub fn parse_metrics_push_protocol(s: &str) -> Result { diff --git a/crates/utils/src/task_manager/mod.rs b/crates/utils/src/task_manager/mod.rs index 894b70170..587613cb7 100644 --- a/crates/utils/src/task_manager/mod.rs +++ b/crates/utils/src/task_manager/mod.rs @@ -10,7 +10,7 @@ use std::{ use clippy_utilities::OverflowArithmetic; use dashmap::DashMap; use tokio::{sync::Notify, task::JoinHandle}; -use tracing::{debug, info}; +use tracing::{debug, info, warn}; use self::tasks::{TaskName, ALL_EDGES}; @@ -33,8 +33,6 @@ pub struct TaskManager { pub struct ClusterShutdownTracker { /// Cluster shutdown notify notify: Notify, - /// State of mpsc channel. - mpmc_channel_shutdown: AtomicBool, /// Count of sync follower tasks. sync_follower_task_count: AtomicU8, /// Shutdown Applied @@ -48,20 +46,11 @@ impl ClusterShutdownTracker { pub fn new() -> Self { Self { notify: Notify::new(), - mpmc_channel_shutdown: AtomicBool::new(false), sync_follower_task_count: AtomicU8::new(0), leader_notified: AtomicBool::new(false), } } - /// Mark mpmc channel shutdown - #[inline] - pub fn mark_mpmc_channel_shutdown(&self) { - self.mpmc_channel_shutdown.store(true, Ordering::Relaxed); - self.notify.notify_one(); - debug!("mark mpmc channel shutdown"); - } - /// Sync follower task count inc #[inline] pub fn sync_follower_task_count_inc(&self) { @@ -93,10 +82,9 @@ impl ClusterShutdownTracker { /// Check if the cluster shutdown condition is met fn check(&self) -> bool { - let mpmc_channel_shutdown = self.mpmc_channel_shutdown.load(Ordering::Relaxed); let sync_follower_task_count = self.sync_follower_task_count.load(Ordering::Relaxed); let leader_notified = self.leader_notified.load(Ordering::Relaxed); - mpmc_channel_shutdown && sync_follower_task_count == 0 && leader_notified + sync_follower_task_count == 0 && leader_notified } } @@ -132,19 +120,32 @@ impl TaskManager { self.state.load(Ordering::Acquire) != 0 } + /// Check if the cluster is shutdown + #[must_use] + #[inline] + pub fn is_node_shutdown(&self) -> bool { + self.state.load(Ordering::Acquire) == 1 + } + + /// Check if the cluster is shutdown + #[must_use] + #[inline] + pub fn is_cluster_shutdown(&self) -> bool { + self.state.load(Ordering::Acquire) == 2 + } + /// Get shutdown listener + /// + /// Returns `None` if the cluster has been shutdowned #[must_use] #[inline] - pub fn get_shutdown_listener(&self, name: TaskName) -> Listener { - let task = self - .tasks - .get(&name) - .unwrap_or_else(|| unreachable!("task {:?} should exist", name)); - Listener::new( + pub fn get_shutdown_listener(&self, name: TaskName) -> Option { + let task = self.tasks.get(&name)?; + Some(Listener::new( Arc::clone(&self.state), Arc::clone(&task.notifier), Arc::clone(&self.cluster_shutdown_tracker), - ) + )) } /// Spawn a task @@ -180,18 +181,25 @@ impl TaskManager { } /// Inner shutdown task - async fn inner_shutdown(tasks: Arc>, state: Arc) { + async fn inner_shutdown(tasks: Arc>) { let mut queue = Self::root_tasks_queue(&tasks); - state.store(1, Ordering::Release); while let Some(v) = queue.pop_front() { let Some((_name, mut task)) = tasks.remove(&v) else { continue; }; task.notifier.notify_waiters(); for handle in task.handle.drain(..) { - handle - .await - .unwrap_or_else(|e| unreachable!("background task should not panic: {e}")); + // Directly abort the task if it's cancel safe + if task.name.cancel_safe() { + handle.abort(); + if let Err(e) = handle.await { + assert!(e.is_cancelled(), "background task should not panic: {e}"); + } + } else { + handle + .await + .unwrap_or_else(|e| unreachable!("background task should not panic: {e}")); + } } for child in task.depend_by.drain(..) { let Some(mut child_task) = tasks.get_mut(&child) else { @@ -210,8 +218,8 @@ impl TaskManager { #[inline] pub async fn shutdown(&self, wait: bool) { let tasks = Arc::clone(&self.tasks); - let state = Arc::clone(&self.state); - let h = tokio::spawn(Self::inner_shutdown(tasks, state)); + self.state.store(1, Ordering::Release); + let h = tokio::spawn(Self::inner_shutdown(tasks)); if wait { h.await .unwrap_or_else(|e| unreachable!("shutdown task should not panic: {e}")); @@ -222,14 +230,13 @@ impl TaskManager { #[inline] pub fn cluster_shutdown(&self) { let tasks = Arc::clone(&self.tasks); - let state = Arc::clone(&self.state); let tracker = Arc::clone(&self.cluster_shutdown_tracker); + self.state.store(2, Ordering::Release); let _ig = tokio::spawn(async move { info!("cluster shutdown start"); - state.store(2, Ordering::Release); - for name in [TaskName::SyncFollower, TaskName::ConflictCheckedMpmc] { - _ = tasks.get(&name).map(|n| n.notifier.notify_waiters()); - } + _ = tasks + .get(&TaskName::SyncFollower) + .map(|n| n.notifier.notify_waiters()); loop { if tracker.check() { break; @@ -237,7 +244,7 @@ impl TaskManager { tracker.notify.notified().await; } info!("cluster shutdown check passed, start shutdown"); - Self::inner_shutdown(tasks, state).await; + Self::inner_shutdown(tasks).await; }); } @@ -254,6 +261,7 @@ impl TaskManager { for t in self.tasks.iter() { for h in &t.handle { if !h.is_finished() { + warn!("task: {:?} not finished", t.name); return false; } } @@ -374,6 +382,14 @@ impl Listener { self.state() } + /// Checks whether self has shutdown. + #[inline] + #[must_use] + pub fn is_shutdown(&self) -> bool { + let state = self.state(); + matches!(state, State::Shutdown) + } + /// Get a sync follower guard #[must_use] #[inline] @@ -383,12 +399,6 @@ impl Listener { tracker: Arc::clone(&self.cluster_shutdown_tracker), } } - - /// Mark mpmc channel shutdown - #[inline] - pub fn mark_mpmc_channel_shutdown(&self) { - self.cluster_shutdown_tracker.mark_mpmc_channel_shutdown(); - } } /// Sync follower guard, used to track sync follower task count @@ -421,13 +431,18 @@ mod test { for name in TaskName::iter() { let record_tx = record_tx.clone(); tm.spawn(name, move |listener| async move { - listener.wait().await; - record_tx.send(name).unwrap(); + if name.cancel_safe() { + record_tx.send(name).unwrap(); + listener.wait().await; + } else { + listener.wait().await; + record_tx.send(name).unwrap(); + } }); } drop(record_tx); tokio::time::sleep(Duration::from_secs(1)).await; - TaskManager::inner_shutdown(Arc::clone(&tm.tasks), Arc::clone(&tm.state)).await; + TaskManager::inner_shutdown(Arc::clone(&tm.tasks)).await; let mut shutdown_order = vec![]; while let Some(name) = record_rx.recv().await { shutdown_order.push(name); diff --git a/crates/utils/src/task_manager/tasks.rs b/crates/utils/src/task_manager/tasks.rs index b4e29f2ec..e32606b00 100644 --- a/crates/utils/src/task_manager/tasks.rs +++ b/crates/utils/src/task_manager/tasks.rs @@ -1,12 +1,13 @@ -// CONFLICT_CHECKED_MPMC -// | -// CMD_WORKER LEASE_KEEP_ALIVE -// / \ | -// COMPACT_BG KV_UPDATES TONIC_SERVER ELECTION -// \ / | \ / -// WATCH_TASK CONF_CHANGE LOG_PERSIST +// AFTER_SYNC LEASE_KEEP_ALIVE +// | | +// KV_UPDATES TONIC_SERVER +// \ / | +// WATCH_TASK CONF_CHANGE +// +// Other tasks like `CompactBg`, `GcSpecPool`, `GcCmdBoard`, `RevokeExpiredLeases`, `SyncVictims`, +// `Election`, and `AutoCompactor` do not have dependent tasks. -// NOTE: In integration tests, we use bottom tasks, like `WatchTask`, `ConfChange`, and `LogPersist`, +// NOTE: In integration tests, we use bottom tasks, like `WatchTask` and `ConfChange`, // which are not dependent on other tasks to detect the curp group is closed or not. If you want // to refactor the task group, don't forget to modify the `BOTTOM_TASKS` in `crates/curp/tests/it/common/curp_group.rs` // to prevent the integration tests from failing. @@ -35,33 +36,48 @@ macro_rules! enum_with_iter { } } enum_with_iter! { - ConflictCheckedMpmc, - CmdWorker, CompactBg, KvUpdates, WatchTask, LeaseKeepAlive, TonicServer, - LogPersist, Election, SyncFollower, ConfChange, - GcSpecPool, - GcCmdBoard, + GcClientLease, RevokeExpiredLeases, SyncVictims, AutoCompactor, + AfterSync, + HandlePropose, +} + +impl TaskName { + /// Returns `true` if the task is cancel safe + pub(super) fn cancel_safe(self) -> bool { + match self { + TaskName::HandlePropose | TaskName::AfterSync => true, + TaskName::CompactBg + | TaskName::KvUpdates + | TaskName::WatchTask + | TaskName::LeaseKeepAlive + | TaskName::TonicServer + | TaskName::Election + | TaskName::SyncFollower + | TaskName::ConfChange + | TaskName::GcClientLease + | TaskName::RevokeExpiredLeases + | TaskName::SyncVictims + | TaskName::AutoCompactor => false, + } + } } /// All edges of task graph, the first item in each pair must be shut down before the second item -pub const ALL_EDGES: [(TaskName, TaskName); 9] = [ - (TaskName::ConflictCheckedMpmc, TaskName::CmdWorker), - (TaskName::CmdWorker, TaskName::CompactBg), - (TaskName::CmdWorker, TaskName::KvUpdates), +pub const ALL_EDGES: [(TaskName, TaskName); 5] = [ + (TaskName::AfterSync, TaskName::KvUpdates), (TaskName::KvUpdates, TaskName::WatchTask), (TaskName::LeaseKeepAlive, TaskName::TonicServer), (TaskName::TonicServer, TaskName::WatchTask), (TaskName::TonicServer, TaskName::ConfChange), - (TaskName::TonicServer, TaskName::LogPersist), - (TaskName::Election, TaskName::LogPersist), ]; diff --git a/crates/utils/src/tracing.rs b/crates/utils/src/tracing.rs index 163fc895a..36f2c7d28 100644 --- a/crates/utils/src/tracing.rs +++ b/crates/utils/src/tracing.rs @@ -81,6 +81,7 @@ impl Inject for tonic::metadata::MetadataMap { #[cfg(test)] mod test { + use opentelemetry::trace::TracerProvider as _; use opentelemetry::trace::{TraceContextExt, TraceId}; use opentelemetry_sdk::propagation::TraceContextPropagator; use tracing::info_span; @@ -89,7 +90,7 @@ mod test { }; use super::*; - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn test_inject_and_extract() -> Result<(), Box> { init()?; global::set_text_map_propagator(TraceContextPropagator::new()); @@ -113,11 +114,13 @@ mod test { /// init tracing subscriber fn init() -> Result<(), Box> { let otlp_exporter = opentelemetry_otlp::new_exporter().tonic(); - let jaeger_online_layer = opentelemetry_otlp::new_pipeline() + let provider = opentelemetry_otlp::new_pipeline() .tracing() .with_exporter(otlp_exporter) - .install_simple() - .map(|tracer| tracing_opentelemetry::layer().with_tracer(tracer))?; + .install_simple()?; + global::set_tracer_provider(provider.clone()); + let tracer = provider.tracer("xline"); + let jaeger_online_layer = tracing_opentelemetry::layer().with_tracer(tracer); tracing_subscriber::registry() .with(jaeger_online_layer) .init(); diff --git a/crates/xline-client/Cargo.toml b/crates/xline-client/Cargo.toml index 72c591457..2554b6aac 100644 --- a/crates/xline-client/Cargo.toml +++ b/crates/xline-client/Cargo.toml @@ -13,15 +13,16 @@ keywords = ["Client", "Xline", "RPC"] [dependencies] anyhow = "1.0.83" async-dropper = { version = "0.3.1", features = ["tokio", "simple"] } -async-trait = "0.1.80" +async-trait = "0.1.81" clippy-utilities = "0.2.0" curp = { path = "../curp" } futures = "0.3.25" getrandom = "0.2" -http = "0.2.9" +http = "1.0" +prost = "0.13" thiserror = "1.0.61" tokio = { version = "0.2.25", package = "madsim-tokio", features = ["sync"] } -tonic = { version = "0.4.2", package = "madsim-tonic" } +tonic = { version = "0.5.0", package = "madsim-tonic" } tower = { version = "0.4", features = ["discover"] } utils = { path = "../utils", features = ["parking_lot"] } workspace-hack = { version = "0.1", path = "../../workspace-hack" } @@ -31,3 +32,6 @@ xlineapi = { path = "../xlineapi" } rand = "0.8.5" test-macros = { path = "../test-macros" } xline-test-utils = { path = "../xline-test-utils" } + +[build-dependencies] +tonic-build = { version = "0.5.0", package = "madsim-tonic-build" } diff --git a/crates/xline-client/README.md b/crates/xline-client/README.md index fb9521c2a..279b213bd 100644 --- a/crates/xline-client/README.md +++ b/crates/xline-client/README.md @@ -83,7 +83,7 @@ To create a xline client: ```rust, no_run use xline_client::{ - types::kv::{PutRequest, RangeRequest}, + types::kv::{PutOptions, RangeOptions}, Client, ClientOptions, }; use anyhow::Result; @@ -97,9 +97,10 @@ async fn main() -> Result<()> { .await? .kv_client(); - client.put(PutRequest::new("key", "value")).await?; + client.put("key", "value", None).await?; - let resp = client.range(RangeRequest::new("key")).await?; + let resp = client.range("key", None).await?; + // let resp = client.range("key2", Some(RangeOptions::default().with_limit(6))).await?; if let Some(kv) = resp.kvs.first() { println!( diff --git a/crates/xline-client/examples/auth_role.rs b/crates/xline-client/examples/auth_role.rs index 2319dd8ff..fe09d34ac 100644 --- a/crates/xline-client/examples/auth_role.rs +++ b/crates/xline-client/examples/auth_role.rs @@ -1,12 +1,5 @@ use anyhow::Result; -use xline_client::{ - types::auth::{ - AuthRoleAddRequest, AuthRoleDeleteRequest, AuthRoleGetRequest, - AuthRoleGrantPermissionRequest, AuthRoleRevokePermissionRequest, Permission, - PermissionType, - }, - Client, ClientOptions, -}; +use xline_client::{types::auth::PermissionType, Client, ClientOptions}; #[tokio::main] async fn main() -> Result<()> { @@ -18,21 +11,15 @@ async fn main() -> Result<()> { .auth_client(); // add roles - client.role_add(AuthRoleAddRequest::new("role1")).await?; - client.role_add(AuthRoleAddRequest::new("role2")).await?; + client.role_add("role1").await?; + client.role_add("role2").await?; // grant permissions to roles client - .role_grant_permission(AuthRoleGrantPermissionRequest::new( - "role1", - Permission::new(PermissionType::Read, "key1"), - )) + .role_grant_permission("role1", PermissionType::Read, "key1", None) .await?; client - .role_grant_permission(AuthRoleGrantPermissionRequest::new( - "role2", - Permission::new(PermissionType::Readwrite, "key2"), - )) + .role_grant_permission("role2", PermissionType::Readwrite, "key2", None) .await?; // list all roles and their permissions @@ -40,7 +27,7 @@ async fn main() -> Result<()> { println!("roles:"); for role in resp.roles { println!("{}", role); - let get_resp = client.role_get(AuthRoleGetRequest::new(role)).await?; + let get_resp = client.role_get(role).await?; println!("permmisions:"); for perm in get_resp.perm { println!("{} {}", perm.perm_type, String::from_utf8_lossy(&perm.key)); @@ -48,20 +35,12 @@ async fn main() -> Result<()> { } // revoke permissions from roles - client - .role_revoke_permission(AuthRoleRevokePermissionRequest::new("role1", "key1")) - .await?; - client - .role_revoke_permission(AuthRoleRevokePermissionRequest::new("role2", "key2")) - .await?; + client.role_revoke_permission("role1", "key1", None).await?; + client.role_revoke_permission("role2", "key2", None).await?; // delete roles - client - .role_delete(AuthRoleDeleteRequest::new("role1")) - .await?; - client - .role_delete(AuthRoleDeleteRequest::new("role2")) - .await?; + client.role_delete("role1").await?; + client.role_delete("role2").await?; Ok(()) } diff --git a/crates/xline-client/examples/auth_user.rs b/crates/xline-client/examples/auth_user.rs index 416135834..dc881f9ed 100644 --- a/crates/xline-client/examples/auth_user.rs +++ b/crates/xline-client/examples/auth_user.rs @@ -1,11 +1,5 @@ use anyhow::Result; -use xline_client::{ - types::auth::{ - AuthUserAddRequest, AuthUserChangePasswordRequest, AuthUserDeleteRequest, - AuthUserGetRequest, AuthUserGrantRoleRequest, AuthUserRevokeRoleRequest, - }, - Client, ClientOptions, -}; +use xline_client::{Client, ClientOptions}; #[tokio::main] async fn main() -> Result<()> { @@ -17,27 +11,21 @@ async fn main() -> Result<()> { .auth_client(); // add user - client.user_add(AuthUserAddRequest::new("user1")).await?; - client.user_add(AuthUserAddRequest::new("user2")).await?; + client.user_add("user1", "", true).await?; + client.user_add("user2", "", true).await?; // change user1's password to "123" - client - .user_change_password(AuthUserChangePasswordRequest::new("user1", "123")) - .await?; + client.user_change_password("user1", "123").await?; // grant roles - client - .user_grant_role(AuthUserGrantRoleRequest::new("user1", "role1")) - .await?; - client - .user_grant_role(AuthUserGrantRoleRequest::new("user2", "role2")) - .await?; + client.user_grant_role("user1", "role1").await?; + client.user_grant_role("user2", "role2").await?; // list all users and their roles let resp = client.user_list().await?; for user in resp.users { println!("user: {}", user); - let get_resp = client.user_get(AuthUserGetRequest::new(user)).await?; + let get_resp = client.user_get(user).await?; println!("roles:"); for role in get_resp.roles.iter() { print!("{} ", role); @@ -46,20 +34,12 @@ async fn main() -> Result<()> { } // revoke role from user - client - .user_revoke_role(AuthUserRevokeRoleRequest::new("user1", "role1")) - .await?; - client - .user_revoke_role(AuthUserRevokeRoleRequest::new("user2", "role2")) - .await?; + client.user_revoke_role("user1", "role1").await?; + client.user_revoke_role("user2", "role2").await?; // delete users - client - .user_delete(AuthUserDeleteRequest::new("user1")) - .await?; - client - .user_delete(AuthUserDeleteRequest::new("user2")) - .await?; + client.user_delete("user1").await?; + client.user_delete("user2").await?; Ok(()) } diff --git a/crates/xline-client/examples/cluster.rs b/crates/xline-client/examples/cluster.rs index ce52558c8..859afdf6b 100644 --- a/crates/xline-client/examples/cluster.rs +++ b/crates/xline-client/examples/cluster.rs @@ -1,11 +1,5 @@ use anyhow::Result; -use xline_client::{ - types::cluster::{ - MemberAddRequest, MemberListRequest, MemberPromoteRequest, MemberRemoveRequest, - MemberUpdateRequest, - }, - Client, ClientOptions, -}; +use xline_client::{Client, ClientOptions}; #[tokio::main] async fn main() -> Result<()> { @@ -17,7 +11,7 @@ async fn main() -> Result<()> { .cluster_client(); // send a linearizable member list request - let resp = client.member_list(MemberListRequest::new(true)).await?; + let resp = client.member_list(true).await?; println!("members: {:?}", resp.members); // whether the added member is a learner. @@ -25,36 +19,24 @@ async fn main() -> Result<()> { let is_learner = true; // add a normal node into the cluster - let resp = client - .member_add(MemberAddRequest::new( - vec!["127.0.0.1:2379".to_owned()], - is_learner, - )) - .await?; + let resp = client.member_add(["127.0.0.1:2379"], is_learner).await?; let added_member = resp.member.unwrap(); println!("members: {:?}, added: {}", resp.members, added_member.id); if is_learner { // promote the learner to a normal node - let resp = client - .member_promote(MemberPromoteRequest::new(added_member.id)) - .await?; + let resp = client.member_promote(added_member.id).await?; println!("members: {:?}", resp.members); } // update the peer_ur_ls of the added member if the network topology has changed. let resp = client - .member_update(MemberUpdateRequest::new( - added_member.id, - vec!["127.0.0.2:2379".to_owned()], - )) + .member_update(added_member.id, ["127.0.0.2:2379"]) .await?; println!("members: {:?}", resp.members); // remove the member from the cluster if it is no longer needed. - let resp = client - .member_remove(MemberRemoveRequest::new(added_member.id)) - .await?; + let resp = client.member_remove(added_member.id).await?; println!("members: {:?}", resp.members); Ok(()) diff --git a/crates/xline-client/examples/error_handling.rs b/crates/xline-client/examples/error_handling.rs index 278004bb0..a9b23d881 100644 --- a/crates/xline-client/examples/error_handling.rs +++ b/crates/xline-client/examples/error_handling.rs @@ -1,6 +1,6 @@ //! An example to show how the errors are organized in `xline-client` use anyhow::Result; -use xline_client::{error::XlineClientError, types::kv::PutRequest, Client, ClientOptions}; +use xline_client::{error::XlineClientError, types::kv::PutOptions, Client, ClientOptions}; use xlineapi::execute_error::ExecuteError; #[tokio::main] @@ -16,7 +16,11 @@ async fn main() -> Result<()> { // It should return an error and it should be `key not found` // as we did not add it before. let resp = client - .put(PutRequest::new("key", "").with_ignore_value(true)) + .put( + "key", + "", + Some(PutOptions::default().with_ignore_value(true)), + ) .await; let err = resp.unwrap_err(); diff --git a/crates/xline-client/examples/kv.rs b/crates/xline-client/examples/kv.rs index fe4b786b6..0373f74e2 100644 --- a/crates/xline-client/examples/kv.rs +++ b/crates/xline-client/examples/kv.rs @@ -1,9 +1,6 @@ use anyhow::Result; use xline_client::{ - types::kv::{ - CompactionRequest, Compare, CompareResult, DeleteRangeRequest, PutRequest, RangeRequest, - TxnOp, TxnRequest, - }, + types::kv::{Compare, CompareResult, DeleteRangeOptions, PutOptions, TxnOp, TxnRequest}, Client, ClientOptions, }; @@ -17,11 +14,11 @@ async fn main() -> Result<()> { .kv_client(); // put - client.put(PutRequest::new("key1", "value1")).await?; - client.put(PutRequest::new("key2", "value2")).await?; + client.put("key1", "value1", None).await?; + client.put("key2", "value2", None).await?; // range - let resp = client.range(RangeRequest::new("key1")).await?; + let resp = client.range("key1", None).await?; if let Some(kv) = resp.kvs.first() { println!( @@ -33,7 +30,10 @@ async fn main() -> Result<()> { // delete let resp = client - .delete(DeleteRangeRequest::new("key1").with_prev_kv(true)) + .delete( + "key1", + Some(DeleteRangeOptions::default().with_prev_kv(true)), + ) .await?; for kv in resp.prev_kvs { @@ -49,13 +49,15 @@ async fn main() -> Result<()> { .when(&[Compare::value("key2", CompareResult::Equal, "value2")][..]) .and_then( &[TxnOp::put( - PutRequest::new("key2", "value3").with_prev_kv(true), + "key2", + "value3", + Some(PutOptions::default().with_prev_kv(true)), )][..], ) - .or_else(&[TxnOp::range(RangeRequest::new("key2"))][..]); + .or_else(&[TxnOp::range("key2", None)][..]); let _resp = client.txn(txn_req).await?; - let resp = client.range(RangeRequest::new("key2")).await?; + let resp = client.range("key2", None).await?; // should print "value3" if let Some(kv) = resp.kvs.first() { println!( @@ -67,7 +69,7 @@ async fn main() -> Result<()> { // compact let rev = resp.header.unwrap().revision; - let _resp = client.compact(CompactionRequest::new(rev)).await?; + let _resp = client.compact(rev, false).await?; Ok(()) } diff --git a/crates/xline-client/examples/lease.rs b/crates/xline-client/examples/lease.rs index 24f1babe5..56e5dd012 100644 --- a/crates/xline-client/examples/lease.rs +++ b/crates/xline-client/examples/lease.rs @@ -1,10 +1,5 @@ use anyhow::Result; -use xline_client::{ - types::lease::{ - LeaseGrantRequest, LeaseKeepAliveRequest, LeaseRevokeRequest, LeaseTimeToLiveRequest, - }, - Client, ClientOptions, -}; +use xline_client::{Client, ClientOptions}; #[tokio::main] async fn main() -> Result<()> { @@ -16,24 +11,20 @@ async fn main() -> Result<()> { .lease_client(); // grant new lease - let resp1 = client.grant(LeaseGrantRequest::new(60)).await?; - let resp2 = client.grant(LeaseGrantRequest::new(60)).await?; + let resp1 = client.grant(60, None).await?; + let resp2 = client.grant(60, None).await?; let lease_id1 = resp1.id; let lease_id2 = resp2.id; println!("lease id 1: {}", lease_id1); println!("lease id 2: {}", lease_id2); // get the ttl of lease1 - let resp = client - .time_to_live(LeaseTimeToLiveRequest::new(lease_id1)) - .await?; + let resp = client.time_to_live(lease_id1, false).await?; println!("remaining ttl: {}", resp.ttl); // keep alive lease2 - let (mut keeper, mut stream) = client - .keep_alive(LeaseKeepAliveRequest::new(lease_id2)) - .await?; + let (mut keeper, mut stream) = client.keep_alive(lease_id2).await?; if let Some(resp) = stream.message().await? { println!("new ttl: {}", resp.ttl); @@ -48,8 +39,8 @@ async fn main() -> Result<()> { } // revoke the leases - let _resp = client.revoke(LeaseRevokeRequest::new(lease_id1)).await?; - let _resp = client.revoke(LeaseRevokeRequest::new(lease_id2)).await?; + let _resp = client.revoke(lease_id1).await?; + let _resp = client.revoke(lease_id2).await?; Ok(()) } diff --git a/crates/xline-client/examples/lock.rs b/crates/xline-client/examples/lock.rs index a0bb84f79..94907d991 100644 --- a/crates/xline-client/examples/lock.rs +++ b/crates/xline-client/examples/lock.rs @@ -1,7 +1,7 @@ use anyhow::Result; use xline_client::{ clients::Xutex, - types::kv::{Compare, CompareResult, PutRequest, TxnOp}, + types::kv::{Compare, CompareResult, PutOptions, TxnOp}, Client, ClientOptions, }; @@ -23,7 +23,9 @@ async fn main() -> Result<()> { .txn_check_locked_key() .when([Compare::value("key2", CompareResult::Equal, "value2")]) .and_then([TxnOp::put( - PutRequest::new("key2", "value3").with_prev_kv(true), + "key2", + "value3", + Some(PutOptions::default().with_prev_kv(true)), )]) .or_else(&[]); diff --git a/crates/xline-client/examples/watch.rs b/crates/xline-client/examples/watch.rs index 55e0a3d27..00792f192 100644 --- a/crates/xline-client/examples/watch.rs +++ b/crates/xline-client/examples/watch.rs @@ -1,8 +1,5 @@ use anyhow::Result; -use xline_client::{ - types::{kv::PutRequest, watch::WatchRequest}, - Client, ClientOptions, -}; +use xline_client::{Client, ClientOptions}; #[tokio::main] async fn main() -> Result<()> { @@ -14,8 +11,8 @@ async fn main() -> Result<()> { let kv_client = client.kv_client(); // watch - let (mut watcher, mut stream) = watch_client.watch(WatchRequest::new("key1")).await?; - kv_client.put(PutRequest::new("key1", "value1")).await?; + let (mut watcher, mut stream) = watch_client.watch("key1", None).await?; + kv_client.put("key1", "value1", None).await?; let resp = stream.message().await?.unwrap(); let kv = resp.events[0].kv.as_ref().unwrap(); diff --git a/crates/xline-client/src/clients/auth.rs b/crates/xline-client/src/clients/auth.rs index 0973c0772..e786f4cd6 100644 --- a/crates/xline-client/src/clients/auth.rs +++ b/crates/xline-client/src/clients/auth.rs @@ -9,16 +9,12 @@ use xlineapi::{ AuthUserAddResponse, AuthUserChangePasswordResponse, AuthUserDeleteResponse, AuthUserGetResponse, AuthUserGrantRoleResponse, AuthUserListResponse, AuthUserRevokeRoleResponse, AuthenticateResponse, RequestWrapper, ResponseWrapper, + Type as PermissionType, }; use crate::{ error::{Result, XlineClientError}, - types::auth::{ - AuthRoleAddRequest, AuthRoleDeleteRequest, AuthRoleGetRequest, - AuthRoleGrantPermissionRequest, AuthRoleRevokePermissionRequest, AuthUserAddRequest, - AuthUserChangePasswordRequest, AuthUserDeleteRequest, AuthUserGetRequest, - AuthUserGrantRoleRequest, AuthUserRevokeRoleRequest, AuthenticateRequest, - }, + types::{auth::Permission, range_end::RangeOption}, AuthService, CurpClient, }; @@ -170,7 +166,7 @@ impl AuthClient { /// # Examples /// /// ```no_run - /// use xline_client::{types::auth::AuthenticateRequest, Client, ClientOptions}; + /// use xline_client::{Client, ClientOptions}; /// use anyhow::Result; /// /// #[tokio::main] @@ -182,7 +178,7 @@ impl AuthClient { /// .auth_client(); /// /// let resp = client - /// .authenticate(AuthenticateRequest::new("root", "root pass word")) + /// .authenticate("root", "root pass word") /// .await?; /// /// println!("auth token: {}", resp.token); @@ -193,25 +189,33 @@ impl AuthClient { #[inline] pub async fn authenticate( &mut self, - request: AuthenticateRequest, + name: impl Into, + password: impl Into, ) -> Result { Ok(self .auth_client - .authenticate(xlineapi::AuthenticateRequest::from(request)) + .authenticate(xlineapi::AuthenticateRequest { + name: name.into(), + password: password.into(), + }) .await? .into_inner()) } /// Add an user. + /// Set password to empty String if you want to create a user without password. /// /// # Errors /// - /// This function will return an error if the inner CURP client encountered a propose failure + /// This function will return an error if the inner CURP client encountered a propose failure; + /// + /// Returns `XlineClientError::InvalidArgs` if the user name is empty, + /// or the password is empty when `allow_no_password` is false. /// /// # Examples /// /// ```no_run - /// use xline_client::{types::auth::AuthUserAddRequest, Client, ClientOptions}; + /// use xline_client::{Client, ClientOptions}; /// use anyhow::Result; /// /// #[tokio::main] @@ -222,33 +226,43 @@ impl AuthClient { /// .await? /// .auth_client(); /// - /// client.user_add(AuthUserAddRequest::new("user1")).await?; + /// client.user_add("user1", "", true).await?; /// Ok(()) /// } ///``` #[inline] - pub async fn user_add(&self, mut request: AuthUserAddRequest) -> Result { - if request.inner.name.is_empty() { + pub async fn user_add( + &self, + name: impl Into, + password: impl AsRef, + allow_no_password: bool, + ) -> Result { + let name = name.into(); + let password: &str = password.as_ref(); + if name.is_empty() { return Err(XlineClientError::InvalidArgs(String::from( "user name is empty", ))); } - let need_password = request - .inner - .options - .as_ref() - .map_or(true, |o| !o.no_password); - if need_password && request.inner.password.is_empty() { + if !allow_no_password && password.is_empty() { return Err(XlineClientError::InvalidArgs(String::from( "password is required but not provided", ))); } - let hashed_password = hash_password(request.inner.password.as_bytes()).map_err(|err| { + let hashed_password = hash_password(password.as_bytes()).map_err(|err| { XlineClientError::InternalError(format!("Failed to hash password: {err}")) })?; - request.inner.hashed_password = hashed_password; - request.inner.password = String::new(); - self.handle_req(request.inner, false).await + let options = allow_no_password.then_some(xlineapi::UserAddOptions { no_password: true }); + self.handle_req( + xlineapi::AuthUserAddRequest { + name, + password: String::new(), + hashed_password, + options, + }, + false, + ) + .await } /// Gets the user info by the user name. @@ -260,7 +274,7 @@ impl AuthClient { /// # Examples /// /// ```no_run - /// use xline_client::{types::auth::AuthUserGetRequest, Client, ClientOptions}; + /// use xline_client::{Client, ClientOptions}; /// use anyhow::Result; /// /// #[tokio::main] @@ -271,7 +285,7 @@ impl AuthClient { /// .await? /// .auth_client(); /// - /// let resp = client.user_get(AuthUserGetRequest::new("user")).await?; + /// let resp = client.user_get("user").await?; /// /// for role in resp.roles { /// print!("{} ", role); @@ -281,8 +295,9 @@ impl AuthClient { /// } ///``` #[inline] - pub async fn user_get(&self, request: AuthUserGetRequest) -> Result { - self.handle_req(request.inner, true).await + pub async fn user_get(&self, name: impl Into) -> Result { + self.handle_req(xlineapi::AuthUserGetRequest { name: name.into() }, true) + .await } /// Lists all users. @@ -340,23 +355,15 @@ impl AuthClient { /// .await? /// .auth_client(); /// - /// // add the user - /// - /// let resp = client.user_list().await?; - /// - /// for user in resp.users { - /// println!("user: {}", user); - /// } + /// let resp = client.user_delete("user").await?; /// /// Ok(()) /// } ///``` #[inline] - pub async fn user_delete( - &self, - request: AuthUserDeleteRequest, - ) -> Result { - self.handle_req(request.inner, false).await + pub async fn user_delete(&self, name: impl Into) -> Result { + self.handle_req(xlineapi::AuthUserDeleteRequest { name: name.into() }, false) + .await } /// Change password for an user. @@ -368,9 +375,7 @@ impl AuthClient { /// # Examples /// /// ```no_run - /// use xline_client::{ - /// types::auth::AuthUserChangePasswordRequest, Client, ClientOptions, - /// }; + /// use xline_client::{Client, ClientOptions}; /// use anyhow::Result; /// /// #[tokio::main] @@ -384,7 +389,7 @@ impl AuthClient { /// // add the user /// /// client - /// .user_change_password(AuthUserChangePasswordRequest::new("user", "123")) + /// .user_change_password("user", "123") /// .await?; /// /// Ok(()) @@ -393,19 +398,27 @@ impl AuthClient { #[inline] pub async fn user_change_password( &self, - mut request: AuthUserChangePasswordRequest, + name: impl Into, + password: impl AsRef, ) -> Result { - if request.inner.password.is_empty() { + let password: &str = password.as_ref(); + if password.is_empty() { return Err(XlineClientError::InvalidArgs(String::from( "role name is empty", ))); } - let hashed_password = hash_password(request.inner.password.as_bytes()).map_err(|err| { + let hashed_password = hash_password(password.as_bytes()).map_err(|err| { XlineClientError::InternalError(format!("Failed to hash password: {err}")) })?; - request.inner.hashed_password = hashed_password; - request.inner.password = String::new(); - self.handle_req(request.inner, false).await + self.handle_req( + xlineapi::AuthUserChangePasswordRequest { + name: name.into(), + hashed_password, + password: String::new(), + }, + false, + ) + .await } /// Grant role for an user. @@ -417,7 +430,7 @@ impl AuthClient { /// # Examples /// /// ```no_run - /// use xline_client::{types::auth::AuthUserGrantRoleRequest, Client, ClientOptions}; + /// use xline_client::{Client, ClientOptions}; /// use anyhow::Result; /// /// #[tokio::main] @@ -430,9 +443,7 @@ impl AuthClient { /// /// // add user and role /// - /// client - /// .user_grant_role(AuthUserGrantRoleRequest::new("user", "role")) - /// .await?; + /// client.user_grant_role("user", "role").await?; /// /// Ok(()) /// } @@ -440,9 +451,17 @@ impl AuthClient { #[inline] pub async fn user_grant_role( &self, - request: AuthUserGrantRoleRequest, + name: impl Into, + role: impl Into, ) -> Result { - self.handle_req(request.inner, false).await + self.handle_req( + xlineapi::AuthUserGrantRoleRequest { + user: name.into(), + role: role.into(), + }, + false, + ) + .await } /// Revoke role for an user. @@ -454,7 +473,7 @@ impl AuthClient { /// # Examples /// /// ```no_run - /// use xline_client::{types::auth::AuthUserRevokeRoleRequest, Client, ClientOptions}; + /// use xline_client::{Client, ClientOptions}; /// use anyhow::Result; /// /// #[tokio::main] @@ -467,9 +486,7 @@ impl AuthClient { /// /// // grant role /// - /// client - /// .user_revoke_role(AuthUserRevokeRoleRequest::new("user", "role")) - /// .await?; + /// client.user_revoke_role("user", "role").await?; /// /// Ok(()) /// } @@ -477,9 +494,17 @@ impl AuthClient { #[inline] pub async fn user_revoke_role( &self, - request: AuthUserRevokeRoleRequest, + name: impl Into, + role: impl Into, ) -> Result { - self.handle_req(request.inner, false).await + self.handle_req( + xlineapi::AuthUserRevokeRoleRequest { + name: name.into(), + role: role.into(), + }, + false, + ) + .await } /// Adds role. @@ -491,7 +516,6 @@ impl AuthClient { /// # Examples /// /// ```no_run - /// use xline_client::types::auth::AuthRoleAddRequest; /// use xline_client::{Client, ClientOptions}; /// use anyhow::Result; /// @@ -503,19 +527,21 @@ impl AuthClient { /// .await? /// .auth_client(); /// - /// client.role_add(AuthRoleAddRequest::new("role")).await?; + /// client.role_add("role").await?; /// /// Ok(()) /// } ///``` #[inline] - pub async fn role_add(&self, request: AuthRoleAddRequest) -> Result { - if request.inner.name.is_empty() { + pub async fn role_add(&self, name: impl Into) -> Result { + let name = name.into(); + if name.is_empty() { return Err(XlineClientError::InvalidArgs(String::from( "role name is empty", ))); } - self.handle_req(request.inner, false).await + self.handle_req(xlineapi::AuthRoleAddRequest { name }, false) + .await } /// Gets role. @@ -527,7 +553,6 @@ impl AuthClient { /// # Examples /// /// ```no_run - /// use xline_client::types::auth::AuthRoleGetRequest; /// use xline_client::{Client, ClientOptions}; /// use anyhow::Result; /// @@ -539,7 +564,7 @@ impl AuthClient { /// .await? /// .auth_client(); /// - /// let resp = client.role_get(AuthRoleGetRequest::new("role")).await?; + /// let resp = client.role_get("role").await?; /// /// println!("permissions:"); /// for perm in resp.perm { @@ -550,8 +575,9 @@ impl AuthClient { /// } ///``` #[inline] - pub async fn role_get(&self, request: AuthRoleGetRequest) -> Result { - self.handle_req(request.inner, true).await + pub async fn role_get(&self, name: impl Into) -> Result { + self.handle_req(xlineapi::AuthRoleGetRequest { role: name.into() }, true) + .await } /// Lists role. @@ -599,7 +625,7 @@ impl AuthClient { /// # Examples /// /// ```no_run - /// use xline_client::{types::auth::AuthRoleDeleteRequest, Client, ClientOptions}; + /// use xline_client::{Client, ClientOptions}; /// use anyhow::Result; /// /// #[tokio::main] @@ -613,18 +639,16 @@ impl AuthClient { /// // add the role /// /// client - /// .role_delete(AuthRoleDeleteRequest::new("role")) + /// .role_delete("role") /// .await?; /// /// Ok(()) /// } ///``` #[inline] - pub async fn role_delete( - &self, - request: AuthRoleDeleteRequest, - ) -> Result { - self.handle_req(request.inner, false).await + pub async fn role_delete(&self, name: impl Into) -> Result { + self.handle_req(xlineapi::AuthRoleDeleteRequest { role: name.into() }, false) + .await } /// Grants role permission. @@ -637,7 +661,7 @@ impl AuthClient { /// /// ```no_run /// use xline_client::{ - /// types::auth::{AuthRoleGrantPermissionRequest, Permission, PermissionType}, + /// types::auth::{Permission, PermissionType}, /// Client, ClientOptions, /// }; /// use anyhow::Result; @@ -653,10 +677,12 @@ impl AuthClient { /// // add the role and key /// /// client - /// .role_grant_permission(AuthRoleGrantPermissionRequest::new( + /// .role_grant_permission( /// "role", - /// Permission::new(PermissionType::Read, "key"), - /// )) + /// PermissionType::Read, + /// "key", + /// None + /// ) /// .await?; /// /// Ok(()) @@ -665,14 +691,19 @@ impl AuthClient { #[inline] pub async fn role_grant_permission( &self, - request: AuthRoleGrantPermissionRequest, + name: impl Into, + perm_type: PermissionType, + perm_key: impl Into>, + range_option: Option, ) -> Result { - if request.inner.perm.is_none() { - return Err(XlineClientError::InvalidArgs(String::from( - "Permission not given", - ))); - } - self.handle_req(request.inner, false).await + self.handle_req( + xlineapi::AuthRoleGrantPermissionRequest { + name: name.into(), + perm: Some(Permission::new(perm_type, perm_key.into(), range_option).into()), + }, + false, + ) + .await } /// Revokes role permission. @@ -684,9 +715,7 @@ impl AuthClient { /// # Examples /// /// ```no_run - /// use xline_client::{ - /// types::auth::AuthRoleRevokePermissionRequest, Client, ClientOptions, - /// }; + /// use xline_client::{Client, ClientOptions, types::range_end::RangeOption}; /// use anyhow::Result; /// /// #[tokio::main] @@ -699,8 +728,13 @@ impl AuthClient { /// /// // grant the role /// + /// client.role_revoke_permission("role", "key", None).await?; /// client - /// .role_revoke_permission(AuthRoleRevokePermissionRequest::new("role", "key")) + /// .role_revoke_permission( + /// "role2", + /// "hi", + /// Some(RangeOption::RangeEnd("hjj".into())), + /// ) /// .await?; /// /// Ok(()) @@ -709,9 +743,21 @@ impl AuthClient { #[inline] pub async fn role_revoke_permission( &self, - request: AuthRoleRevokePermissionRequest, + name: impl Into, + key: impl Into>, + range_option: Option, ) -> Result { - self.handle_req(request.inner, false).await + let mut key = key.into(); + let range_end = range_option.unwrap_or_default().get_range_end(&mut key); + self.handle_req( + xlineapi::AuthRoleRevokePermissionRequest { + role: name.into(), + key, + range_end, + }, + false, + ) + .await } /// Send request using fast path diff --git a/crates/xline-client/src/clients/cluster.rs b/crates/xline-client/src/clients/cluster.rs index a4ec0cc0c..545d28510 100644 --- a/crates/xline-client/src/clients/cluster.rs +++ b/crates/xline-client/src/clients/cluster.rs @@ -2,14 +2,10 @@ use std::sync::Arc; use tonic::transport::Channel; -use crate::{ - error::Result, - types::cluster::{ - MemberAddRequest, MemberAddResponse, MemberListRequest, MemberListResponse, - MemberPromoteRequest, MemberPromoteResponse, MemberRemoveRequest, MemberRemoveResponse, - MemberUpdateRequest, MemberUpdateResponse, - }, - AuthService, +use crate::{error::Result, AuthService}; +use xlineapi::{ + MemberAddResponse, MemberListResponse, MemberPromoteResponse, MemberRemoveResponse, + MemberUpdateResponse, }; /// Client for Cluster operations. @@ -47,7 +43,6 @@ impl ClusterClient { /// /// ```no_run /// use xline_client::{Client, ClientOptions}; - /// use xline_client::types::cluster::*; /// use anyhow::Result; /// /// #[tokio::main] @@ -58,7 +53,7 @@ impl ClusterClient { /// .await? /// .cluster_client(); /// - /// let resp = client.member_add(MemberAddRequest::new(vec!["127.0.0.1:2380".to_owned()], true)).await?; + /// let resp = client.member_add(["127.0.0.1:2380"], true).await?; /// /// println!( /// "members: {:?}, added: {:?}", @@ -69,10 +64,17 @@ impl ClusterClient { /// } /// ``` #[inline] - pub async fn member_add(&mut self, request: MemberAddRequest) -> Result { + pub async fn member_add>( + &mut self, + peer_urls: impl Into>, + is_learner: bool, + ) -> Result { Ok(self .inner - .member_add(xlineapi::MemberAddRequest::from(request)) + .member_add(xlineapi::MemberAddRequest { + peer_ur_ls: peer_urls.into().into_iter().map(Into::into).collect(), + is_learner, + }) .await? .into_inner()) } @@ -87,7 +89,6 @@ impl ClusterClient { /// /// ```no_run /// use xline_client::{Client, ClientOptions}; - /// use xline_client::types::cluster::*; /// use anyhow::Result; /// /// #[tokio::main] @@ -97,7 +98,7 @@ impl ClusterClient { /// let mut client = Client::connect(curp_members, ClientOptions::default()) /// .await? /// .cluster_client(); - /// let resp = client.member_remove(MemberRemoveRequest::new(1)).await?; + /// let resp = client.member_remove(1).await?; /// /// println!("members: {:?}", resp.members); /// @@ -105,13 +106,10 @@ impl ClusterClient { /// } /// #[inline] - pub async fn member_remove( - &mut self, - request: MemberRemoveRequest, - ) -> Result { + pub async fn member_remove(&mut self, id: u64) -> Result { Ok(self .inner - .member_remove(xlineapi::MemberRemoveRequest::from(request)) + .member_remove(xlineapi::MemberRemoveRequest { id }) .await? .into_inner()) } @@ -126,7 +124,6 @@ impl ClusterClient { /// /// ```no_run /// use xline_client::{Client, ClientOptions}; - /// use xline_client::types::cluster::*; /// use anyhow::Result; /// /// #[tokio::main] @@ -136,7 +133,7 @@ impl ClusterClient { /// let mut client = Client::connect(curp_members, ClientOptions::default()) /// .await? /// .cluster_client(); - /// let resp = client.member_promote(MemberPromoteRequest::new(1)).await?; + /// let resp = client.member_promote(1).await?; /// /// println!("members: {:?}", resp.members); /// @@ -144,13 +141,10 @@ impl ClusterClient { /// } /// #[inline] - pub async fn member_promote( - &mut self, - request: MemberPromoteRequest, - ) -> Result { + pub async fn member_promote(&mut self, id: u64) -> Result { Ok(self .inner - .member_promote(xlineapi::MemberPromoteRequest::from(request)) + .member_promote(xlineapi::MemberPromoteRequest { id }) .await? .into_inner()) } @@ -165,7 +159,6 @@ impl ClusterClient { /// /// ```no_run /// use xline_client::{Client, ClientOptions}; - /// use xline_client::types::cluster::*; /// use anyhow::Result; /// /// #[tokio::main] @@ -175,7 +168,7 @@ impl ClusterClient { /// let mut client = Client::connect(curp_members, ClientOptions::default()) /// .await? /// .cluster_client(); - /// let resp = client.member_update(MemberUpdateRequest::new(1, vec!["127.0.0.1:2379".to_owned()])).await?; + /// let resp = client.member_update(1, ["127.0.0.1:2379"]).await?; /// /// println!("members: {:?}", resp.members); /// @@ -183,13 +176,17 @@ impl ClusterClient { /// } /// #[inline] - pub async fn member_update( + pub async fn member_update>( &mut self, - request: MemberUpdateRequest, + id: u64, + peer_urls: impl Into>, ) -> Result { Ok(self .inner - .member_update(xlineapi::MemberUpdateRequest::from(request)) + .member_update(xlineapi::MemberUpdateRequest { + id, + peer_ur_ls: peer_urls.into().into_iter().map(Into::into).collect(), + }) .await? .into_inner()) } @@ -204,7 +201,6 @@ impl ClusterClient { /// /// ```no_run /// use xline_client::{Client, ClientOptions}; - /// use xline_client::types::cluster::*; /// use anyhow::Result; /// /// #[tokio::main] @@ -214,17 +210,17 @@ impl ClusterClient { /// let mut client = Client::connect(curp_members, ClientOptions::default()) /// .await? /// .cluster_client(); - /// let resp = client.member_list(MemberListRequest::new(false)).await?; + /// let resp = client.member_list(false).await?; /// /// println!("members: {:?}", resp.members); /// /// Ok(()) /// } #[inline] - pub async fn member_list(&mut self, request: MemberListRequest) -> Result { + pub async fn member_list(&mut self, linearizable: bool) -> Result { Ok(self .inner - .member_list(xlineapi::MemberListRequest::from(request)) + .member_list(xlineapi::MemberListRequest { linearizable }) .await? .into_inner()) } diff --git a/crates/xline-client/src/clients/kv.rs b/crates/xline-client/src/clients/kv.rs index d3819410d..c78e49cba 100644 --- a/crates/xline-client/src/clients/kv.rs +++ b/crates/xline-client/src/clients/kv.rs @@ -8,7 +8,7 @@ use xlineapi::{ use crate::{ error::Result, - types::kv::{CompactionRequest, DeleteRangeRequest, PutRequest, RangeRequest, TxnRequest}, + types::kv::{DeleteRangeOptions, PutOptions, RangeOptions, TxnRequest}, AuthService, CurpClient, }; @@ -65,7 +65,7 @@ impl KvClient { /// # Examples /// /// ```no_run - /// use xline_client::{types::kv::PutRequest, Client, ClientOptions}; + /// use xline_client::{types::kv::PutOptions, Client, ClientOptions}; /// use anyhow::Result; /// /// #[tokio::main] @@ -76,14 +76,22 @@ impl KvClient { /// .await? /// .kv_client(); /// - /// client.put(PutRequest::new("key1", "value1")).await?; + /// client.put("key1", "value1", None).await?; + /// client.put("key2", "value2", Some(PutOptions::default().with_prev_kv(true))).await?; /// /// Ok(()) /// } /// ``` #[inline] - pub async fn put(&self, request: PutRequest) -> Result { - let request = RequestWrapper::from(xlineapi::PutRequest::from(request)); + pub async fn put( + &self, + key: impl Into>, + value: impl Into>, + option: Option, + ) -> Result { + let request = RequestWrapper::from(xlineapi::PutRequest::from( + option.unwrap_or_default().with_kv(key.into(), value.into()), + )); let cmd = Command::new(request); let (cmd_res, _sync_res) = self .curp_client @@ -101,7 +109,7 @@ impl KvClient { /// # Examples /// /// ```no_run - /// use xline_client::{types::kv::RangeRequest, Client, ClientOptions}; + /// use xline_client::{types::kv::RangeOptions, Client, ClientOptions}; /// use anyhow::Result; /// /// #[tokio::main] @@ -112,7 +120,8 @@ impl KvClient { /// .await? /// .kv_client(); /// - /// let resp = client.range(RangeRequest::new("key1")).await?; + /// let resp = client.range("key1", None).await?; + /// let resp = client.range("key2", Some(RangeOptions::default().with_limit(6))).await?; /// /// if let Some(kv) = resp.kvs.first() { /// println!( @@ -126,8 +135,14 @@ impl KvClient { /// } /// ``` #[inline] - pub async fn range(&self, request: RangeRequest) -> Result { - let request = RequestWrapper::from(xlineapi::RangeRequest::from(request)); + pub async fn range( + &self, + key: impl Into>, + options: Option, + ) -> Result { + let request = RequestWrapper::from(xlineapi::RangeRequest::from( + options.unwrap_or_default().with_key(key), + )); let cmd = Command::new(request); let (cmd_res, _sync_res) = self .curp_client @@ -144,7 +159,7 @@ impl KvClient { /// /// # Examples /// ```no_run - /// use xline_client::{types::kv::DeleteRangeRequest, Client, ClientOptions}; + /// use xline_client::{types::kv::DeleteRangeOptions, Client, ClientOptions}; /// use anyhow::Result; /// /// #[tokio::main] @@ -156,15 +171,21 @@ impl KvClient { /// .kv_client(); /// /// client - /// .delete(DeleteRangeRequest::new("key1").with_prev_kv(true)) + /// .delete("key1", Some(DeleteRangeOptions::default().with_prev_kv(true))) /// .await?; /// /// Ok(()) /// } /// ``` #[inline] - pub async fn delete(&self, request: DeleteRangeRequest) -> Result { - let request = RequestWrapper::from(xlineapi::DeleteRangeRequest::from(request)); + pub async fn delete( + &self, + key: impl Into>, + options: Option, + ) -> Result { + let request = RequestWrapper::from(xlineapi::DeleteRangeRequest::from( + options.unwrap_or_default().with_key(key), + )); let cmd = Command::new(request); let (cmd_res, _sync_res) = self .curp_client @@ -183,7 +204,7 @@ impl KvClient { /// /// ```no_run /// use xline_client::{ - /// types::kv::{Compare, PutRequest, RangeRequest, TxnOp, TxnRequest, CompareResult}, + /// types::kv::{Compare, PutOptions, TxnOp, TxnRequest, CompareResult}, /// Client, ClientOptions, /// }; /// use anyhow::Result; @@ -199,11 +220,9 @@ impl KvClient { /// let txn_req = TxnRequest::new() /// .when(&[Compare::value("key2", CompareResult::Equal, "value2")][..]) /// .and_then( - /// &[TxnOp::put( - /// PutRequest::new("key2", "value3").with_prev_kv(true), - /// )][..], + /// &[TxnOp::put("key2", "value3", Some(PutOptions::default().with_prev_kv(true)))][..], /// ) - /// .or_else(&[TxnOp::range(RangeRequest::new("key2"))][..]); + /// .or_else(&[TxnOp::range("key2", None)][..]); /// /// let _resp = client.txn(txn_req).await?; /// @@ -233,6 +252,11 @@ impl KvClient { /// We compact at revision 3. After the compaction, the revision list will become [(A, 3), (A, 4), (A, 5)]. /// All revisions less than 3 are deleted. The latest revision, 3, will be kept. /// + /// `Revision` is the key-value store revision for the compaction operation. + /// `Physical` is set so the RPC will wait until the compaction is physically + /// applied to the local database such that compacted entries are totally + /// removed from the backend database. + /// /// # Errors /// /// This function will return an error if the inner CURP client encountered a propose failure @@ -241,8 +265,7 @@ impl KvClient { /// ///```no_run /// use xline_client::{ - /// types::kv::{CompactionRequest, PutRequest}, - /// Client, ClientOptions, + /// Client, ClientOptions /// }; /// use anyhow::Result; /// @@ -254,26 +277,26 @@ impl KvClient { /// .await? /// .kv_client(); /// - /// let resp_put = client.put(PutRequest::new("key", "val")).await?; + /// let resp_put = client.put("key", "val", None).await?; /// let rev = resp_put.header.unwrap().revision; /// - /// let _resp = client.compact(CompactionRequest::new(rev)).await?; + /// let _resp = client.compact(rev, false).await?; /// /// Ok(()) /// } /// ``` #[inline] - pub async fn compact(&self, request: CompactionRequest) -> Result { - if request.physical() { + pub async fn compact(&self, revision: i64, physical: bool) -> Result { + let request = xlineapi::CompactionRequest { revision, physical }; + if physical { let mut kv_client = self.kv_client.clone(); return kv_client - .compact(xlineapi::CompactionRequest::from(request)) + .compact(request) .await .map(tonic::Response::into_inner) .map_err(Into::into); } - let request = RequestWrapper::from(xlineapi::CompactionRequest::from(request)); - let cmd = Command::new(request); + let cmd = Command::new(RequestWrapper::from(request)); let (cmd_res, _sync_res) = self .curp_client .propose(&cmd, self.token.as_ref(), true) diff --git a/crates/xline-client/src/clients/lease.rs b/crates/xline-client/src/clients/lease.rs index b09577744..42b7a1e18 100644 --- a/crates/xline-client/src/clients/lease.rs +++ b/crates/xline-client/src/clients/lease.rs @@ -10,10 +10,7 @@ use xlineapi::{ use crate::{ error::{Result, XlineClientError}, lease_gen::LeaseIdGenerator, - types::lease::{ - LeaseGrantRequest, LeaseKeepAliveRequest, LeaseKeeper, LeaseRevokeRequest, - LeaseTimeToLiveRequest, - }, + types::lease::LeaseKeeper, AuthService, CurpClient, }; @@ -70,6 +67,9 @@ impl LeaseClient { /// within a given time to live period. All keys attached to the lease will be expired and /// deleted if the lease expires. Each expired key generates a delete event in the event history. /// + /// `ttl` is the advisory time-to-live in seconds. Expired lease will return -1. + /// `id` is the requested ID for the lease. If ID is set to `None` or 0, the lessor chooses an ID. + /// /// # Errors /// /// This function will return an error if the inner CURP client encountered a propose failure @@ -77,7 +77,7 @@ impl LeaseClient { /// # Examples /// /// ```no_run - /// use xline_client::{types::lease::LeaseGrantRequest, Client, ClientOptions}; + /// use xline_client::{Client, ClientOptions}; /// use anyhow::Result; /// /// #[tokio::main] @@ -88,19 +88,22 @@ impl LeaseClient { /// .await? /// .lease_client(); /// - /// let resp = client.grant(LeaseGrantRequest::new(60)).await?; + /// let resp = client.grant(60, None).await?; /// println!("lease id: {}", resp.id); /// /// Ok(()) /// } /// ``` #[inline] - pub async fn grant(&self, mut request: LeaseGrantRequest) -> Result { - if request.inner.id == 0 { - request.inner.id = self.id_gen.next(); + pub async fn grant(&self, ttl: i64, id: Option) -> Result { + let mut id = id.unwrap_or_default(); + if id == 0 { + id = self.id_gen.next(); } - let request = RequestWrapper::from(xlineapi::LeaseGrantRequest::from(request)); - let cmd = Command::new(request); + let cmd = Command::new(RequestWrapper::from(xlineapi::LeaseGrantRequest { + ttl, + id, + })); let (cmd_res, _sync_res) = self .curp_client .propose(&cmd, self.token.as_ref(), true) @@ -110,6 +113,8 @@ impl LeaseClient { /// Revokes a lease. All keys attached to the lease will expire and be deleted. /// + /// `id` is the lease ID to revoke. When the ID is revoked, all associated keys will be deleted. + /// /// # Errors /// /// This function will return an error if the inner RPC client encountered a propose failure @@ -117,7 +122,7 @@ impl LeaseClient { /// # Examples /// /// ```no_run - /// use xline_client::{types::lease::LeaseRevokeRequest, Client, ClientOptions}; + /// use xline_client::{Client, ClientOptions}; /// use anyhow::Result; /// /// #[tokio::main] @@ -130,20 +135,25 @@ impl LeaseClient { /// /// // granted a lease id 1 /// - /// let _resp = client.revoke(LeaseRevokeRequest::new(1)).await?; + /// let _resp = client.revoke(1).await?; /// /// Ok(()) /// } /// ``` #[inline] - pub async fn revoke(&mut self, request: LeaseRevokeRequest) -> Result { - let res = self.lease_client.lease_revoke(request.inner).await?; + pub async fn revoke(&mut self, id: i64) -> Result { + let res = self + .lease_client + .lease_revoke(xlineapi::LeaseRevokeRequest { id }) + .await?; Ok(res.into_inner()) } /// Keeps the lease alive by streaming keep alive requests from the client /// to the server and streaming keep alive responses from the server to the client. /// + /// `id` is the lease ID for the lease to keep alive. + /// /// # Errors /// /// This function will return an error if the inner RPC client encountered a propose failure @@ -151,7 +161,7 @@ impl LeaseClient { /// # Examples /// /// ```no_run - /// use xline_client::{types::lease::LeaseKeepAliveRequest, Client, ClientOptions}; + /// use xline_client::{Client, ClientOptions}; /// use anyhow::Result; /// /// #[tokio::main] @@ -164,7 +174,7 @@ impl LeaseClient { /// /// // granted a lease id 1 /// - /// let (mut keeper, mut stream) = client.keep_alive(LeaseKeepAliveRequest::new(1)).await?; + /// let (mut keeper, mut stream) = client.keep_alive(1).await?; /// /// if let Some(resp) = stream.message().await? { /// println!("new ttl: {}", resp.ttl); @@ -178,12 +188,12 @@ impl LeaseClient { #[inline] pub async fn keep_alive( &mut self, - request: LeaseKeepAliveRequest, + id: i64, ) -> Result<(LeaseKeeper, Streaming)> { let (mut sender, receiver) = channel::(100); sender - .try_send(request.into()) + .try_send(xlineapi::LeaseKeepAliveRequest { id }) .map_err(|e| XlineClientError::LeaseError(e.to_string()))?; let mut stream = self @@ -192,7 +202,7 @@ impl LeaseClient { .await? .into_inner(); - let id = match stream.message().await? { + let resp_id = match stream.message().await? { Some(resp) => resp.id, None => { return Err(XlineClientError::LeaseError(String::from( @@ -201,11 +211,14 @@ impl LeaseClient { } }; - Ok((LeaseKeeper::new(id, sender), stream)) + Ok((LeaseKeeper::new(resp_id, sender), stream)) } /// Retrieves lease information. /// + /// `id` is the lease ID for the lease, + /// `keys` is true to query all the keys attached to this lease. + /// /// # Errors /// /// This function will return an error if the inner RPC client encountered a propose failure @@ -213,7 +226,7 @@ impl LeaseClient { /// # Examples /// /// ```no_run - /// use xline_client::{types::lease::LeaseTimeToLiveRequest, Client, ClientOptions}; + /// use xline_client::{Client, ClientOptions}; /// use anyhow::Result; /// /// #[tokio::main] @@ -226,7 +239,7 @@ impl LeaseClient { /// /// // granted a lease id 1 /// - /// let resp = client.time_to_live(LeaseTimeToLiveRequest::new(1)).await?; + /// let resp = client.time_to_live(1, false).await?; /// /// println!("remaining ttl: {}", resp.ttl); /// @@ -234,13 +247,10 @@ impl LeaseClient { /// } /// ``` #[inline] - pub async fn time_to_live( - &mut self, - request: LeaseTimeToLiveRequest, - ) -> Result { + pub async fn time_to_live(&mut self, id: i64, keys: bool) -> Result { Ok(self .lease_client - .lease_time_to_live(xlineapi::LeaseTimeToLiveRequest::from(request)) + .lease_time_to_live(xlineapi::LeaseTimeToLiveRequest { id, keys }) .await? .into_inner()) } diff --git a/crates/xline-client/src/clients/lock.rs b/crates/xline-client/src/clients/lock.rs index 9a7b4d624..58af9764a 100644 --- a/crates/xline-client/src/clients/lock.rs +++ b/crates/xline-client/src/clients/lock.rs @@ -16,11 +16,7 @@ use crate::{ clients::{lease::LeaseClient, watch::WatchClient, DEFAULT_SESSION_TTL}, error::{Result, XlineClientError}, lease_gen::LeaseIdGenerator, - types::{ - kv::TxnRequest as KvTxnRequest, - lease::{LeaseGrantRequest, LeaseKeepAliveRequest}, - watch::WatchRequest, - }, + types::kv::TxnRequest as KvTxnRequest, CurpClient, }; @@ -130,19 +126,14 @@ impl Xutex { let lease_id = if let Some(id) = lease_id { id } else { - let lease_response = client - .lease_client - .grant(LeaseGrantRequest::new(ttl)) - .await?; + let lease_response = client.lease_client.grant(ttl, None).await?; lease_response.id }; let mut lease_client = client.lease_client.clone(); let keep_alive = Some(tokio::spawn(async move { /// The renew interval factor of which value equals 60% of one second. const RENEW_INTERVAL_FACTOR: u64 = 600; - let (mut keeper, mut stream) = lease_client - .keep_alive(LeaseKeepAliveRequest::new(lease_id)) - .await?; + let (mut keeper, mut stream) = lease_client.keep_alive(lease_id).await?; loop { keeper.keep_alive()?; if let Some(resp) = stream.message().await? { @@ -201,7 +192,7 @@ impl Xutex { ..Default::default() })), }; - let range_end = KeyRange::get_prefix(prefix.as_bytes()); + let range_end = KeyRange::get_prefix(prefix); #[allow(clippy::as_conversions)] // this cast is always safe let get_owner = RequestOp { request: Some(Request::RequestRange(RangeRequest { @@ -260,37 +251,33 @@ impl Xutex { /// # Examples /// /// ```no_run - /// use xline_client:: - /// clients::{Session, Xutex}, - /// types::lock::{LockRequest, UnlockRequest}, + /// use anyhow::Result; + /// use xline_client::{ + /// clients::Xutex, + /// types::kv::{Compare, CompareResult, PutOptions, TxnOp}, /// Client, ClientOptions, /// }; - /// use anyhow::Result; /// /// #[tokio::main] /// async fn main() -> Result<()> { /// // the name and address of all curp members /// let curp_members = ["10.0.0.1:2379", "10.0.0.2:2379", "10.0.0.3:2379"]; /// - /// let mut client = Client::connect(curp_members, ClientOptions::default()) - /// .await? - /// .lock_client(); + /// let client = Client::connect(curp_members, ClientOptions::default()).await?; /// - /// // acquire a lock session - /// let session = Session::new(client.lock_client()).build().await?; - /// let mut xutex = Xutex::new(session, "lock-test"); - /// let lock = xutex.lock().await?; + /// let lock_client = client.lock_client(); + /// let kv_client = client.kv_client(); /// + /// let mut xutex = Xutex::new(lock_client, "lock-test", None, None).await?; + /// // when the `xutex_guard` drop, the lock will be unlocked. + /// let xutex_guard = xutex.lock_unsafe().await?; /// let txn_req = xutex_guard /// .txn_check_locked_key() /// .when([Compare::value("key2", CompareResult::Equal, "value2")]) - /// .and_then([TxnOp::put( - /// PutRequest::new("key2", "value3").with_prev_kv(true), - /// )]) + /// .and_then([TxnOp::put("key2", "value3", Some(PutOptions::default().with_prev_kv(true)))]) /// .or_else(&[]); - + /// /// let _resp = kv_client.txn(txn_req).await?; - /// println!("lock key: {:?}", String::from_utf8_lossy(xutex.key().as_bytes(); /// // the lock will be released when the lock session is dropped. /// Ok(()) /// } @@ -419,7 +406,7 @@ impl LockClient { let rev = my_rev.overflow_sub(1); let mut watch_client = self.watch_client.clone(); loop { - let range_end = KeyRange::get_prefix(pfx.as_bytes()); + let range_end = KeyRange::get_prefix(&pfx); #[allow(clippy::as_conversions)] // this cast is always safe let get_req = RangeRequest { key: pfx.as_bytes().to_vec(), @@ -437,7 +424,7 @@ impl LockClient { Some(kv) => kv.key.clone(), None => return Ok(()), }; - let (_, mut response_stream) = watch_client.watch(WatchRequest::new(last_key)).await?; + let (_, mut response_stream) = watch_client.watch(last_key, None).await?; while let Some(watch_res) = response_stream.message().await? { #[allow(clippy::as_conversions)] // this cast is always safe if watch_res diff --git a/crates/xline-client/src/clients/maintenance.rs b/crates/xline-client/src/clients/maintenance.rs index 8d2ae1718..c2b7e1bd5 100644 --- a/crates/xline-client/src/clients/maintenance.rs +++ b/crates/xline-client/src/clients/maintenance.rs @@ -2,7 +2,8 @@ use std::{fmt::Debug, sync::Arc}; use tonic::{transport::Channel, Streaming}; use xlineapi::{ - AlarmRequest, AlarmResponse, SnapshotRequest, SnapshotResponse, StatusRequest, StatusResponse, + AlarmAction, AlarmRequest, AlarmResponse, AlarmType, SnapshotRequest, SnapshotResponse, + StatusRequest, StatusResponse, }; use crate::{error::Result, AuthService}; @@ -95,14 +96,27 @@ impl MaintenanceClient { /// .await? /// .maintenance_client(); /// - /// client.alarm(AlarmRequest::new(AlarmAction::Get, 0, AlarmType::None)).await?; + /// client.alarm(AlarmAction::Get, 0, AlarmType::None).await?; /// /// Ok(()) /// } /// ``` #[inline] - pub async fn alarm(&mut self, request: AlarmRequest) -> Result { - Ok(self.inner.alarm(request).await?.into_inner()) + pub async fn alarm( + &mut self, + action: AlarmAction, + member_id: u64, + alarm_type: AlarmType, + ) -> Result { + Ok(self + .inner + .alarm(AlarmRequest { + action: action.into(), + member_id, + alarm: alarm_type.into(), + }) + .await? + .into_inner()) } /// Sends a status request diff --git a/crates/xline-client/src/clients/watch.rs b/crates/xline-client/src/clients/watch.rs index 98f399adb..947cb21fc 100644 --- a/crates/xline-client/src/clients/watch.rs +++ b/crates/xline-client/src/clients/watch.rs @@ -6,7 +6,7 @@ use xlineapi::{self, RequestUnion}; use crate::{ error::{Result, XlineClientError}, - types::watch::{WatchRequest, WatchStreaming, Watcher}, + types::watch::{WatchOptions, WatchStreaming, Watcher}, AuthService, }; @@ -53,10 +53,7 @@ impl WatchClient { /// # Examples /// /// ```no_run - /// use xline_client::{ - /// types::{kv::PutRequest, watch::WatchRequest}, - /// Client, ClientOptions, - /// }; + /// use xline_client::{Client, ClientOptions}; /// use anyhow::Result; /// /// #[tokio::main] @@ -67,8 +64,8 @@ impl WatchClient { /// let mut watch_client = client.watch_client(); /// let mut kv_client = client.kv_client(); /// - /// let (mut watcher, mut stream) = watch_client.watch(WatchRequest::new("key1")).await?; - /// kv_client.put(PutRequest::new("key1", "value1")).await?; + /// let (mut watcher, mut stream) = watch_client.watch("key1", None).await?; + /// kv_client.put("key1", "value1", None).await?; /// /// let resp = stream.message().await?.unwrap(); /// let kv = resp.events[0].kv.as_ref().unwrap(); @@ -86,12 +83,18 @@ impl WatchClient { /// } /// ``` #[inline] - pub async fn watch(&mut self, request: WatchRequest) -> Result<(Watcher, WatchStreaming)> { + pub async fn watch( + &mut self, + key: impl Into>, + options: Option, + ) -> Result<(Watcher, WatchStreaming)> { let (mut request_sender, request_receiver) = channel::(CHANNEL_SIZE); let request = xlineapi::WatchRequest { - request_union: Some(RequestUnion::CreateRequest(request.into())), + request_union: Some(RequestUnion::CreateRequest( + options.unwrap_or_default().with_key(key.into()).into(), + )), }; request_sender diff --git a/crates/xline-client/src/lib.rs b/crates/xline-client/src/lib.rs index 11f780cdc..3bc638ba2 100644 --- a/crates/xline-client/src/lib.rs +++ b/crates/xline-client/src/lib.rs @@ -253,7 +253,7 @@ impl Client { Some((username, password)) => { let mut tmp_auth = AuthClient::new(Arc::clone(&curp_client), channel.clone(), None); let resp = tmp_auth - .authenticate(types::auth::AuthenticateRequest::new(username, password)) + .authenticate(username, password) .await .map_err(|err| XlineClientBuildError::AuthError(err.to_string()))?; diff --git a/crates/xline-client/src/types/auth.rs b/crates/xline-client/src/types/auth.rs index 89a13b222..a025d7323 100644 --- a/crates/xline-client/src/types/auth.rs +++ b/crates/xline-client/src/types/auth.rs @@ -1,4 +1,3 @@ -use xlineapi::command::KeyRange; pub use xlineapi::{ AuthDisableResponse, AuthEnableResponse, AuthRoleAddResponse, AuthRoleDeleteResponse, AuthRoleGetResponse, AuthRoleGrantPermissionResponse, AuthRoleListResponse, @@ -8,387 +7,15 @@ pub use xlineapi::{ AuthenticateResponse, Type as PermissionType, }; -/// Request for `Authenticate` -#[derive(Debug)] -pub struct AuthenticateRequest { - /// Inner request - pub(crate) inner: xlineapi::AuthenticateRequest, -} - -impl AuthenticateRequest { - /// Creates a new `AuthenticateRequest`. - #[inline] - pub fn new(user_name: impl Into, user_password: impl Into) -> Self { - Self { - inner: xlineapi::AuthenticateRequest { - name: user_name.into(), - password: user_password.into(), - }, - } - } -} - -impl From for xlineapi::AuthenticateRequest { - #[inline] - fn from(req: AuthenticateRequest) -> Self { - req.inner - } -} - -/// Request for `Authenticate` -#[derive(Debug, PartialEq)] -pub struct AuthUserAddRequest { - /// Inner request - pub(crate) inner: xlineapi::AuthUserAddRequest, -} - -impl AuthUserAddRequest { - /// Creates a new `AuthUserAddRequest`. - #[inline] - pub fn new(user_name: impl Into) -> Self { - Self { - inner: xlineapi::AuthUserAddRequest { - name: user_name.into(), - options: Some(xlineapi::UserAddOptions { no_password: true }), - ..Default::default() - }, - } - } - - /// Sets the password. - #[inline] - #[must_use] - pub fn with_pwd(mut self, password: impl Into) -> Self { - self.inner.password = password.into(); - self.inner.options = Some(xlineapi::UserAddOptions { no_password: false }); - self - } -} - -impl From for xlineapi::AuthUserAddRequest { - #[inline] - fn from(req: AuthUserAddRequest) -> Self { - req.inner - } -} - -/// Request for `AuthUserGet` -#[derive(Debug, PartialEq)] -pub struct AuthUserGetRequest { - /// Inner request - pub(crate) inner: xlineapi::AuthUserGetRequest, -} - -impl AuthUserGetRequest { - /// Creates a new `AuthUserGetRequest`. - #[inline] - pub fn new(user_name: impl Into) -> Self { - Self { - inner: xlineapi::AuthUserGetRequest { - name: user_name.into(), - }, - } - } -} - -impl From for xlineapi::AuthUserGetRequest { - #[inline] - fn from(req: AuthUserGetRequest) -> Self { - req.inner - } -} - -/// Request for `AuthUserDelete` -#[derive(Debug, PartialEq)] -pub struct AuthUserDeleteRequest { - /// Inner request - pub(crate) inner: xlineapi::AuthUserDeleteRequest, -} - -impl AuthUserDeleteRequest { - /// Creates a new `AuthUserDeleteRequest`. - #[inline] - pub fn new(user_name: impl Into) -> Self { - Self { - inner: xlineapi::AuthUserDeleteRequest { - name: user_name.into(), - }, - } - } -} - -impl From for xlineapi::AuthUserDeleteRequest { - #[inline] - fn from(req: AuthUserDeleteRequest) -> Self { - req.inner - } -} - -/// Request for `AuthUserChangePassword` -#[derive(Debug, PartialEq)] -pub struct AuthUserChangePasswordRequest { - /// Inner request - pub(crate) inner: xlineapi::AuthUserChangePasswordRequest, -} - -impl AuthUserChangePasswordRequest { - /// Creates a new `AuthUserChangePasswordRequest`. - #[inline] - pub fn new(user_name: impl Into, new_password: impl Into) -> Self { - Self { - inner: xlineapi::AuthUserChangePasswordRequest { - name: user_name.into(), - password: new_password.into(), - hashed_password: String::new(), - }, - } - } -} - -impl From for xlineapi::AuthUserChangePasswordRequest { - #[inline] - fn from(req: AuthUserChangePasswordRequest) -> Self { - req.inner - } -} - -/// Request for `AuthUserGrantRole` -#[derive(Debug, PartialEq)] -pub struct AuthUserGrantRoleRequest { - /// Inner request - pub(crate) inner: xlineapi::AuthUserGrantRoleRequest, -} - -impl AuthUserGrantRoleRequest { - /// Creates a new `AuthUserGrantRoleRequest` - /// - /// `user_name` is the name of the user to grant role, - /// `role` is the role name to grant. - #[inline] - pub fn new(user_name: impl Into, role: impl Into) -> Self { - Self { - inner: xlineapi::AuthUserGrantRoleRequest { - user: user_name.into(), - role: role.into(), - }, - } - } -} - -impl From for xlineapi::AuthUserGrantRoleRequest { - #[inline] - fn from(req: AuthUserGrantRoleRequest) -> Self { - req.inner - } -} - -/// Request for `AuthUserRevokeRole` -#[derive(Debug, PartialEq)] -pub struct AuthUserRevokeRoleRequest { - /// Inner request - pub(crate) inner: xlineapi::AuthUserRevokeRoleRequest, -} - -impl AuthUserRevokeRoleRequest { - /// Creates a new `AuthUserRevokeRoleRequest` - /// - /// `user_name` is the name of the user to revoke role, - /// `role` is the role name to revoke. - #[inline] - pub fn new(user_name: impl Into, role: impl Into) -> Self { - Self { - inner: xlineapi::AuthUserRevokeRoleRequest { - name: user_name.into(), - role: role.into(), - }, - } - } -} - -impl From for xlineapi::AuthUserRevokeRoleRequest { - #[inline] - fn from(req: AuthUserRevokeRoleRequest) -> Self { - req.inner - } -} - -/// Request for `AuthRoleAdd` -#[derive(Debug, PartialEq)] -pub struct AuthRoleAddRequest { - /// Inner request - pub(crate) inner: xlineapi::AuthRoleAddRequest, -} - -impl AuthRoleAddRequest { - /// Creates a new `AuthRoleAddRequest` - /// - /// `role` is the name of the role to add. - #[inline] - pub fn new(role: impl Into) -> Self { - Self { - inner: xlineapi::AuthRoleAddRequest { name: role.into() }, - } - } -} - -impl From for xlineapi::AuthRoleAddRequest { - #[inline] - fn from(req: AuthRoleAddRequest) -> Self { - req.inner - } -} - -/// Request for `AuthRoleGet` -#[derive(Debug, PartialEq)] -pub struct AuthRoleGetRequest { - /// Inner request - pub(crate) inner: xlineapi::AuthRoleGetRequest, -} - -impl AuthRoleGetRequest { - /// Creates a new `AuthRoleGetRequest` - /// - /// `role` is the name of the role to get. - #[inline] - pub fn new(role: impl Into) -> Self { - Self { - inner: xlineapi::AuthRoleGetRequest { role: role.into() }, - } - } -} - -impl From for xlineapi::AuthRoleGetRequest { - #[inline] - fn from(req: AuthRoleGetRequest) -> Self { - req.inner - } -} - -/// Request for `AuthRoleDelete` -#[derive(Debug, PartialEq)] -pub struct AuthRoleDeleteRequest { - /// Inner request - pub(crate) inner: xlineapi::AuthRoleDeleteRequest, -} - -impl AuthRoleDeleteRequest { - /// Creates a new `AuthRoleDeleteRequest` - /// - /// `role` is the name of the role to delete. - #[inline] - pub fn new(role: impl Into) -> Self { - Self { - inner: xlineapi::AuthRoleDeleteRequest { role: role.into() }, - } - } -} - -impl From for xlineapi::AuthRoleDeleteRequest { - #[inline] - fn from(req: AuthRoleDeleteRequest) -> Self { - req.inner - } -} - -/// Request for `AuthRoleGrantPermission` -#[derive(Debug, PartialEq)] -pub struct AuthRoleGrantPermissionRequest { - /// Inner request - pub(crate) inner: xlineapi::AuthRoleGrantPermissionRequest, -} - -impl AuthRoleGrantPermissionRequest { - /// Creates a new `AuthRoleGrantPermissionRequest` - /// - /// `role` is the name of the role to grant permission, - /// `perm` is the permission name to grant. - #[inline] - pub fn new(role: impl Into, perm: Permission) -> Self { - Self { - inner: xlineapi::AuthRoleGrantPermissionRequest { - name: role.into(), - perm: Some(perm.into()), - }, - } - } -} - -impl From for xlineapi::AuthRoleGrantPermissionRequest { - #[inline] - fn from(req: AuthRoleGrantPermissionRequest) -> Self { - req.inner - } -} - -/// Request for `AuthRoleRevokePermission` -#[derive(Debug, PartialEq)] -pub struct AuthRoleRevokePermissionRequest { - /// Inner request - pub(crate) inner: xlineapi::AuthRoleRevokePermissionRequest, -} - -impl AuthRoleRevokePermissionRequest { - /// Creates a new `RoleRevokePermissionOption` from pb role revoke permission. - /// - /// `role` is the name of the role to revoke permission, - /// `key` is the key to revoke from the role. - #[inline] - pub fn new(role: impl Into, key: impl Into>) -> Self { - Self { - inner: xlineapi::AuthRoleRevokePermissionRequest { - role: role.into(), - key: key.into(), - ..Default::default() - }, - } - } - - /// If set, Xline will return all keys with the matching prefix - #[inline] - #[must_use] - pub fn with_prefix(mut self) -> Self { - if self.inner.key.is_empty() { - self.inner.key = vec![0]; - self.inner.range_end = vec![0]; - } else { - self.inner.range_end = KeyRange::get_prefix(&self.inner.key); - } - self - } - - /// If set, Xline will return all keys that are equal or greater than the given key - #[inline] - #[must_use] - pub fn with_from_key(mut self) -> Self { - if self.inner.key.is_empty() { - self.inner.key = vec![0]; - } - self.inner.range_end = vec![0]; - self - } - - /// `range_end` is the upper bound on the requested range \[key,` range_en`d). - /// If `range_end` is '\0', the range is all keys >= key. - #[inline] - #[must_use] - pub fn with_range_end(mut self, range_end: impl Into>) -> Self { - self.inner.range_end = range_end.into(); - self - } -} - -impl From for xlineapi::AuthRoleRevokePermissionRequest { - #[inline] - fn from(req: AuthRoleRevokePermissionRequest) -> Self { - req.inner - } -} +use super::range_end::RangeOption; /// Role access permission. #[derive(Debug, Clone)] pub struct Permission { /// The inner Permission inner: xlineapi::Permission, + /// The range option + range_option: Option, } impl Permission { @@ -396,55 +23,57 @@ impl Permission { /// /// `perm_type` is the permission type, /// `key` is the key to grant with the permission. + /// `range_option` is the range option of how to get `range_end` from key. #[inline] #[must_use] - pub fn new(perm_type: PermissionType, key: impl Into>) -> Self { - Self { - inner: xlineapi::Permission { - perm_type: perm_type.into(), - key: key.into(), - ..Default::default() - }, - } + pub fn new( + perm_type: PermissionType, + key: impl Into>, + range_option: Option, + ) -> Self { + Self::from((perm_type, key.into(), range_option)) } +} - /// If set, Xline will return all keys with the matching prefix +impl From for xlineapi::Permission { #[inline] - #[must_use] - pub fn with_prefix(mut self) -> Self { - if self.inner.key.is_empty() { - self.inner.key = vec![0]; - self.inner.range_end = vec![0]; - } else { - self.inner.range_end = KeyRange::get_prefix(&self.inner.key); - } - self + fn from(mut perm: Permission) -> Self { + perm.inner.range_end = perm + .range_option + .unwrap_or_default() + .get_range_end(&mut perm.inner.key); + perm.inner } +} - /// If set, Xline will return all keys that are equal or greater than the given key +impl PartialEq for Permission { #[inline] - #[must_use] - pub fn with_from_key(mut self) -> Self { - if self.inner.key.is_empty() { - self.inner.key = vec![0]; - } - self.inner.range_end = vec![0]; - self + fn eq(&self, other: &Self) -> bool { + self.inner == other.inner && self.range_option == other.range_option } +} + +impl Eq for Permission {} - /// `range_end` is the upper bound on the requested range \[key,` range_en`d). - /// If `range_end` is '\0', the range is all keys >= key. +impl From<(PermissionType, Vec, Option)> for Permission { #[inline] - #[must_use] - pub fn with_range_end(mut self, range_end: impl Into>) -> Self { - self.inner.range_end = range_end.into(); - self + fn from( + (perm_type, key, range_option): (PermissionType, Vec, Option), + ) -> Self { + Permission { + inner: xlineapi::Permission { + perm_type: perm_type.into(), + key, + ..Default::default() + }, + range_option, + } } } -impl From for xlineapi::Permission { +impl From<(PermissionType, &str, Option)> for Permission { #[inline] - fn from(perm: Permission) -> Self { - perm.inner + fn from(value: (PermissionType, &str, Option)) -> Self { + Self::from((value.0, value.1.as_bytes().to_vec(), value.2)) } } diff --git a/crates/xline-client/src/types/cluster.rs b/crates/xline-client/src/types/cluster.rs deleted file mode 100644 index 3803e8d49..000000000 --- a/crates/xline-client/src/types/cluster.rs +++ /dev/null @@ -1,133 +0,0 @@ -pub use xlineapi::{ - Cluster, Member, MemberAddResponse, MemberListResponse, MemberPromoteResponse, - MemberRemoveResponse, MemberUpdateResponse, -}; - -/// Request for `MemberAdd` -#[derive(Debug, PartialEq)] -pub struct MemberAddRequest { - /// The inner request - inner: xlineapi::MemberAddRequest, -} - -impl MemberAddRequest { - /// Creates a new `MemberAddRequest` - #[inline] - pub fn new(peer_ur_ls: impl Into>, is_learner: bool) -> Self { - Self { - inner: xlineapi::MemberAddRequest { - peer_ur_ls: peer_ur_ls.into(), - is_learner, - }, - } - } -} - -impl From for xlineapi::MemberAddRequest { - #[inline] - fn from(req: MemberAddRequest) -> Self { - req.inner - } -} - -/// Request for `MemberList` -#[derive(Debug, PartialEq)] -pub struct MemberListRequest { - /// The inner request - inner: xlineapi::MemberListRequest, -} - -impl MemberListRequest { - /// Creates a new `MemberListRequest` - #[inline] - #[must_use] - pub fn new(linearizable: bool) -> Self { - Self { - inner: xlineapi::MemberListRequest { linearizable }, - } - } -} - -impl From for xlineapi::MemberListRequest { - #[inline] - fn from(req: MemberListRequest) -> Self { - req.inner - } -} - -/// Request for `MemberPromote` -#[derive(Debug, PartialEq)] -pub struct MemberPromoteRequest { - /// The inner request - inner: xlineapi::MemberPromoteRequest, -} - -impl MemberPromoteRequest { - /// Creates a new `MemberPromoteRequest` - #[inline] - #[must_use] - pub fn new(id: u64) -> Self { - Self { - inner: xlineapi::MemberPromoteRequest { id }, - } - } -} - -impl From for xlineapi::MemberPromoteRequest { - #[inline] - fn from(req: MemberPromoteRequest) -> Self { - req.inner - } -} - -/// Request for `MemberRemove` -#[derive(Debug, PartialEq)] -pub struct MemberRemoveRequest { - /// The inner request - inner: xlineapi::MemberRemoveRequest, -} - -impl MemberRemoveRequest { - /// Creates a new `MemberRemoveRequest` - #[inline] - #[must_use] - pub fn new(id: u64) -> Self { - Self { - inner: xlineapi::MemberRemoveRequest { id }, - } - } -} - -impl From for xlineapi::MemberRemoveRequest { - #[inline] - fn from(req: MemberRemoveRequest) -> Self { - req.inner - } -} - -/// Request for `MemberUpdate` -#[derive(Debug, PartialEq)] -pub struct MemberUpdateRequest { - /// The inner request - inner: xlineapi::MemberUpdateRequest, -} - -impl MemberUpdateRequest { - /// Creates a new `MemberUpdateRequest` - #[inline] - pub fn new(id: u64, peer_ur_ls: impl Into>) -> Self { - Self { - inner: xlineapi::MemberUpdateRequest { - id, - peer_ur_ls: peer_ur_ls.into(), - }, - } - } -} - -impl From for xlineapi::MemberUpdateRequest { - #[inline] - fn from(req: MemberUpdateRequest) -> Self { - req.inner - } -} diff --git a/crates/xline-client/src/types/kv.rs b/crates/xline-client/src/types/kv.rs index 195c1942d..fe23e0ebb 100644 --- a/crates/xline-client/src/types/kv.rs +++ b/crates/xline-client/src/types/kv.rs @@ -4,27 +4,24 @@ pub use xlineapi::{ RangeResponse, Response, ResponseOp, SortOrder, SortTarget, TargetUnion, TxnResponse, }; -/// Request type for `Put` -#[derive(Debug, PartialEq)] -pub struct PutRequest { +use super::range_end::RangeOption; + +/// Options for `Put`, as same as the `PutRequest` for `Put`. +#[derive(Debug, PartialEq, Default)] +pub struct PutOptions { /// Inner request inner: xlineapi::PutRequest, } -impl PutRequest { - /// Creates a new `PutRequest` - /// +impl PutOptions { + #[inline] + #[must_use] /// `key` is the key, in bytes, to put into the key-value store. /// `value` is the value, in bytes, to associate with the key in the key-value store. - #[inline] - pub fn new(key: impl Into>, value: impl Into>) -> Self { - Self { - inner: xlineapi::PutRequest { - key: key.into(), - value: value.into(), - ..Default::default() - }, - } + pub fn with_kv(mut self, key: Vec, value: Vec) -> Self { + self.inner.key = key; + self.inner.value = value; + self } /// lease is the lease ID to associate with the key in the key-value store. @@ -106,44 +103,36 @@ impl PutRequest { } } -impl From for xlineapi::PutRequest { +impl From for xlineapi::PutRequest { #[inline] - fn from(req: PutRequest) -> Self { + fn from(req: PutOptions) -> Self { req.inner } } -/// Request type for `Range` -#[derive(Debug, PartialEq)] -pub struct RangeRequest { - /// Inner request +/// Options for `range` function. +#[derive(Debug, PartialEq, Default)] +pub struct RangeOptions { + /// Inner request, RangeRequest = inner + key + range_end inner: xlineapi::RangeRequest, + /// Range end options, indicates how to generate `range_end` from key. + range_end_options: RangeOption, } -impl RangeRequest { - /// Creates a new `RangeRequest` - /// +impl RangeOptions { /// `key` is the first key for the range. If `range_end` is not given, the request only looks up key. #[inline] - pub fn new(key: impl Into>) -> Self { - Self { - inner: xlineapi::RangeRequest { - key: key.into(), - ..Default::default() - }, - } + #[must_use] + pub fn with_key(mut self, key: impl Into>) -> Self { + self.inner.key = key.into(); + self } /// If set, Xline will return all keys with the matching prefix #[inline] #[must_use] pub fn with_prefix(mut self) -> Self { - if self.inner.key.is_empty() { - self.inner.key = vec![0]; - self.inner.range_end = vec![0]; - } else { - self.inner.range_end = KeyRange::get_prefix(&self.inner.key); - } + self.range_end_options = RangeOption::Prefix; self } @@ -151,10 +140,7 @@ impl RangeRequest { #[inline] #[must_use] pub fn with_from_key(mut self) -> Self { - if self.inner.key.is_empty() { - self.inner.key = vec![0]; - } - self.inner.range_end = vec![0]; + self.range_end_options = RangeOption::FromKey; self } @@ -163,7 +149,7 @@ impl RangeRequest { #[inline] #[must_use] pub fn with_range_end(mut self, range_end: impl Into>) -> Self { - self.inner.range_end = range_end.into(); + self.range_end_options = RangeOption::RangeEnd(range_end.into()); self } @@ -268,18 +254,11 @@ impl RangeRequest { self } - /// Get `key` + /// Get `range_end_options` #[inline] #[must_use] - pub fn key(&self) -> &[u8] { - &self.inner.key - } - - /// Get `range_end` - #[inline] - #[must_use] - pub fn range_end(&self) -> &[u8] { - &self.inner.range_end + pub fn range_end_options(&self) -> &RangeOption { + &self.range_end_options } /// Get `limit` @@ -360,44 +339,37 @@ impl RangeRequest { } } -impl From for xlineapi::RangeRequest { +impl From for xlineapi::RangeRequest { #[inline] - fn from(req: RangeRequest) -> Self { + fn from(mut req: RangeOptions) -> Self { + req.inner.range_end = req.range_end_options.get_range_end(&mut req.inner.key); req.inner } } /// Request type for `DeleteRange` -#[derive(Debug, PartialEq)] -pub struct DeleteRangeRequest { +#[derive(Debug, PartialEq, Default)] +pub struct DeleteRangeOptions { /// Inner request inner: xlineapi::DeleteRangeRequest, + /// Range end options + range_end_options: RangeOption, } -impl DeleteRangeRequest { - /// Creates a new `DeleteRangeRequest` - /// - /// `key` is the first key to delete in the range. +impl DeleteRangeOptions { + /// `key` is the first key for the range. If `range_end` is not given, the request only looks up key. #[inline] - pub fn new(key: impl Into>) -> Self { - Self { - inner: xlineapi::DeleteRangeRequest { - key: key.into(), - ..Default::default() - }, - } + #[must_use] + pub fn with_key(mut self, key: impl Into>) -> Self { + self.inner.key = key.into(); + self } /// If set, Xline will delete all keys with the matching prefix #[inline] #[must_use] pub fn with_prefix(mut self) -> Self { - if self.inner.key.is_empty() { - self.inner.key = vec![0]; - self.inner.range_end = vec![0]; - } else { - self.inner.range_end = KeyRange::get_prefix(&self.inner.key); - } + self.range_end_options = RangeOption::Prefix; self } @@ -405,22 +377,15 @@ impl DeleteRangeRequest { #[inline] #[must_use] pub fn with_from_key(mut self) -> Self { - if self.inner.key.is_empty() { - self.inner.key = vec![0]; - } - self.inner.range_end = vec![0]; + self.range_end_options = RangeOption::FromKey; self } - /// `range_end` is the key following the last key to delete for the range \[key,` range_en`d). - /// If `range_end` is not given, the range is defined to contain only the key argument. - /// If `range_end` is one bit larger than the given key, then the range is all the keys - /// with the prefix (the given key). - /// If `range_end` is '\0', the range is all keys greater than or equal to the key argument. + /// If set, Xline will delete all keys in range `[key, range_end)`. #[inline] #[must_use] pub fn with_range_end(mut self, range_end: impl Into>) -> Self { - self.inner.range_end = range_end.into(); + self.range_end_options = RangeOption::RangeEnd(range_end.into()); self } @@ -433,18 +398,11 @@ impl DeleteRangeRequest { self } - /// Get `key` + /// Get `range_end_options` #[inline] #[must_use] - pub fn key(&self) -> &[u8] { - &self.inner.key - } - - /// Get `range_end` - #[inline] - #[must_use] - pub fn range_end(&self) -> &[u8] { - &self.inner.range_end + pub fn range_end_options(&self) -> &RangeOption { + &self.range_end_options } /// Get `prev_kv` @@ -455,9 +413,10 @@ impl DeleteRangeRequest { } } -impl From for xlineapi::DeleteRangeRequest { +impl From for xlineapi::DeleteRangeRequest { #[inline] - fn from(req: DeleteRangeRequest) -> Self { + fn from(mut req: DeleteRangeOptions) -> Self { + req.inner.range_end = req.range_end_options.get_range_end(&mut req.inner.key); req.inner } } @@ -567,27 +526,38 @@ impl TxnOp { /// Creates a `Put` operation. #[inline] #[must_use] - pub fn put(request: PutRequest) -> Self { + pub fn put( + key: impl Into>, + value: impl Into>, + option: Option, + ) -> Self { TxnOp { - inner: xlineapi::Request::RequestPut(request.into()), + inner: xlineapi::Request::RequestPut( + option + .unwrap_or_default() + .with_kv(key.into(), value.into()) + .into(), + ), } } /// Creates a `Range` operation. #[inline] #[must_use] - pub fn range(request: RangeRequest) -> Self { + pub fn range(key: impl Into>, option: Option) -> Self { TxnOp { - inner: xlineapi::Request::RequestRange(request.into()), + inner: xlineapi::Request::RequestRange(option.unwrap_or_default().with_key(key).into()), } } /// Creates a `DeleteRange` operation. #[inline] #[must_use] - pub fn delete(request: DeleteRangeRequest) -> Self { + pub fn delete(key: impl Into>, option: Option) -> Self { TxnOp { - inner: xlineapi::Request::RequestDeleteRange(request.into()), + inner: xlineapi::Request::RequestDeleteRange( + option.unwrap_or_default().with_key(key).into(), + ), } } @@ -710,55 +680,3 @@ impl From for xlineapi::TxnRequest { txn.inner } } - -/// Compaction Request compacts the key-value store up to a given revision. -/// All keys with revisions less than the given revision will be compacted. -/// The compaction process will remove all historical versions of these keys, except for the most recent one. -/// For example, here is a revision list: [(A, 1), (A, 2), (A, 3), (A, 4), (A, 5)]. -/// We compact at revision 3. After the compaction, the revision list will become [(A, 3), (A, 4), (A, 5)]. -/// All revisions less than 3 are deleted. The latest revision, 3, will be kept. -#[derive(Debug, PartialEq)] -pub struct CompactionRequest { - /// The inner request - inner: xlineapi::CompactionRequest, -} - -impl CompactionRequest { - /// Creates a new `CompactionRequest` - /// - /// `Revision` is the key-value store revision for the compaction operation. - #[inline] - #[must_use] - pub fn new(revision: i64) -> Self { - Self { - inner: xlineapi::CompactionRequest { - revision, - ..Default::default() - }, - } - } - - /// Physical is set so the RPC will wait until the compaction is physically - /// applied to the local database such that compacted entries are totally - /// removed from the backend database. - #[inline] - #[must_use] - pub fn with_physical(mut self) -> Self { - self.inner.physical = true; - self - } - - /// Get `physical` - #[inline] - #[must_use] - pub fn physical(&self) -> bool { - self.inner.physical - } -} - -impl From for xlineapi::CompactionRequest { - #[inline] - fn from(req: CompactionRequest) -> Self { - req.inner - } -} diff --git a/crates/xline-client/src/types/lease.rs b/crates/xline-client/src/types/lease.rs index fbf39fad6..03fa80cc2 100644 --- a/crates/xline-client/src/types/lease.rs +++ b/crates/xline-client/src/types/lease.rs @@ -38,137 +38,7 @@ impl LeaseKeeper { #[inline] pub fn keep_alive(&mut self) -> Result<()> { self.sender - .try_send(LeaseKeepAliveRequest::new(self.id).into()) + .try_send(xlineapi::LeaseKeepAliveRequest { id: self.id }) .map_err(|e| XlineClientError::LeaseError(e.to_string())) } } - -/// Request for `LeaseGrant` -#[derive(Debug, PartialEq)] -pub struct LeaseGrantRequest { - /// Inner request - pub(crate) inner: xlineapi::LeaseGrantRequest, -} - -impl LeaseGrantRequest { - /// Creates a new `LeaseGrantRequest` - /// - /// `ttl` is the advisory time-to-live in seconds. Expired lease will return -1. - #[inline] - #[must_use] - pub fn new(ttl: i64) -> Self { - Self { - inner: xlineapi::LeaseGrantRequest { - ttl, - ..Default::default() - }, - } - } - - /// `id` is the requested ID for the lease. If ID is set to 0, the lessor chooses an ID. - #[inline] - #[must_use] - pub fn with_id(mut self, id: i64) -> Self { - self.inner.id = id; - self - } -} - -impl From for xlineapi::LeaseGrantRequest { - #[inline] - fn from(req: LeaseGrantRequest) -> Self { - req.inner - } -} - -/// Request for `LeaseRevoke` -#[derive(Debug, PartialEq)] -pub struct LeaseRevokeRequest { - /// Inner request - pub(crate) inner: xlineapi::LeaseRevokeRequest, -} - -impl LeaseRevokeRequest { - /// Creates a new `LeaseRevokeRequest` - /// - /// `id` is the lease ID to revoke. When the ID is revoked, all associated keys will be deleted. - #[inline] - #[must_use] - pub fn new(id: i64) -> Self { - Self { - inner: xlineapi::LeaseRevokeRequest { id }, - } - } -} - -impl From for xlineapi::LeaseRevokeRequest { - #[inline] - fn from(req: LeaseRevokeRequest) -> Self { - req.inner - } -} - -/// Request for `LeaseKeepAlive` -#[derive(Debug, PartialEq)] -pub struct LeaseKeepAliveRequest { - /// Inner request - pub(crate) inner: xlineapi::LeaseKeepAliveRequest, -} - -impl LeaseKeepAliveRequest { - /// Creates a new `LeaseKeepAliveRequest` - /// - /// `id` is the lease ID for the lease to keep alive. - #[inline] - #[must_use] - pub fn new(id: i64) -> Self { - Self { - inner: xlineapi::LeaseKeepAliveRequest { id }, - } - } -} - -impl From for xlineapi::LeaseKeepAliveRequest { - #[inline] - fn from(req: LeaseKeepAliveRequest) -> Self { - req.inner - } -} - -/// Request for `LeaseTimeToLive` -#[derive(Debug, PartialEq)] -pub struct LeaseTimeToLiveRequest { - /// Inner request - pub(crate) inner: xlineapi::LeaseTimeToLiveRequest, -} - -impl LeaseTimeToLiveRequest { - /// Creates a new `LeaseTimeToLiveRequest` - /// - /// `id` is the lease ID for the lease. - #[inline] - #[must_use] - pub fn new(id: i64) -> Self { - Self { - inner: xlineapi::LeaseTimeToLiveRequest { - id, - ..Default::default() - }, - } - } - - /// `keys` is true to query all the keys attached to this lease. - #[inline] - #[must_use] - pub fn with_keys(mut self, keys: bool) -> Self { - self.inner.keys = keys; - self - } -} - -impl From for xlineapi::LeaseTimeToLiveRequest { - #[inline] - fn from(req: LeaseTimeToLiveRequest) -> Self { - req.inner - } -} diff --git a/crates/xline-client/src/types/maintenance.rs b/crates/xline-client/src/types/maintenance.rs deleted file mode 100644 index 44dead5f0..000000000 --- a/crates/xline-client/src/types/maintenance.rs +++ /dev/null @@ -1 +0,0 @@ -pub use xlineapi::SnapshotResponse; diff --git a/crates/xline-client/src/types/mod.rs b/crates/xline-client/src/types/mod.rs index a3abb3b5f..b894ebc82 100644 --- a/crates/xline-client/src/types/mod.rs +++ b/crates/xline-client/src/types/mod.rs @@ -1,12 +1,10 @@ /// Auth type definitions. pub mod auth; -/// Cluster type definitions. -pub mod cluster; /// Kv type definitions. pub mod kv; /// Lease type definitions pub mod lease; -/// Maintenance type definitions. -pub mod maintenance; +/// Range Option definitions, to build a `range_end` from key. +pub mod range_end; /// Watch type definitions. pub mod watch; diff --git a/crates/xline-client/src/types/range_end.rs b/crates/xline-client/src/types/range_end.rs new file mode 100644 index 000000000..d4c3d5f70 --- /dev/null +++ b/crates/xline-client/src/types/range_end.rs @@ -0,0 +1,63 @@ +use xlineapi::command::KeyRange; + +/// Range end options, indicates how to set `range_end` from a key. +#[derive(Clone, Debug, PartialEq, Eq, Default)] +#[non_exhaustive] +pub enum RangeOption { + /// Only lookup the given single key. Use empty Vec as `range_end` + #[default] + SingleKey, + /// If set, Xline will lookup all keys match the given prefix + Prefix, + /// If set, Xline will lookup all keys that are equal to or greater than the given key + FromKey, + /// Set `range_end` directly + RangeEnd(Vec), +} + +impl RangeOption { + /// Get the `range_end` for request, and modify key if necessary. + #[inline] + pub fn get_range_end(self, key: &mut Vec) -> Vec { + match self { + RangeOption::SingleKey => vec![], + RangeOption::Prefix => { + if key.is_empty() { + key.push(0); + vec![0] + } else { + KeyRange::get_prefix(key) + } + } + RangeOption::FromKey => { + if key.is_empty() { + key.push(0); + } + vec![0] + } + RangeOption::RangeEnd(range_end) => range_end, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_range_end() { + let mut key = vec![]; + assert!(RangeOption::SingleKey.get_range_end(&mut key).is_empty()); + assert!(key.is_empty()); + assert!(RangeOption::FromKey.get_range_end(&mut key).first() == Some(&0)); + assert!(key.first() == Some(&0)); + assert_eq!( + RangeOption::Prefix.get_range_end(&mut key), + KeyRange::get_prefix(&key) + ); + assert_eq!( + RangeOption::RangeEnd(vec![1, 2, 3]).get_range_end(&mut key), + vec![1, 2, 3] + ); + } +} diff --git a/crates/xline-client/src/types/watch.rs b/crates/xline-client/src/types/watch.rs index 874253d58..7c7be55aa 100644 --- a/crates/xline-client/src/types/watch.rs +++ b/crates/xline-client/src/types/watch.rs @@ -3,11 +3,11 @@ use std::{ ops::{Deref, DerefMut}, }; +use super::range_end::RangeOption; +use crate::error::{Result, XlineClientError}; use futures::channel::mpsc::Sender; -use xlineapi::{command::KeyRange, RequestUnion, WatchCancelRequest, WatchProgressRequest}; pub use xlineapi::{Event, EventType, KeyValue, WatchResponse}; - -use crate::error::{Result, XlineClientError}; +use xlineapi::{RequestUnion, WatchCancelRequest, WatchProgressRequest}; /// The watching handle. #[derive(Debug)] @@ -39,7 +39,7 @@ impl Watcher { /// /// If sender fails to send to channel #[inline] - pub fn watch(&mut self, request: WatchRequest) -> Result<()> { + pub fn watch(&mut self, request: WatchOptions) -> Result<()> { let request = xlineapi::WatchRequest { request_union: Some(RequestUnion::CreateRequest(request.into())), }; @@ -102,37 +102,28 @@ impl Watcher { } /// Watch Request -#[derive(Clone, Debug, PartialEq)] -pub struct WatchRequest { +#[derive(Clone, Debug, PartialEq, Default)] +pub struct WatchOptions { /// Inner watch create request inner: xlineapi::WatchCreateRequest, + /// Watch range end options + range_end_options: RangeOption, } -impl WatchRequest { - /// Creates a New `WatchRequest` - /// +impl WatchOptions { /// `key` is the key to register for watching. #[inline] #[must_use] - pub fn new(key: impl Into>) -> Self { - Self { - inner: xlineapi::WatchCreateRequest { - key: key.into(), - ..Default::default() - }, - } + pub fn with_key(mut self, key: impl Into>) -> Self { + self.inner.key = key.into(); + self } /// If set, Xline will watch all keys with the matching prefix #[inline] #[must_use] pub fn with_prefix(mut self) -> Self { - if self.inner.key.is_empty() { - self.inner.key = vec![0]; - self.inner.range_end = vec![0]; - } else { - self.inner.range_end = KeyRange::get_prefix(&self.inner.key); - } + self.range_end_options = RangeOption::Prefix; self } @@ -140,10 +131,7 @@ impl WatchRequest { #[inline] #[must_use] pub fn with_from_key(mut self) -> Self { - if self.inner.key.is_empty() { - self.inner.key = vec![0]; - } - self.inner.range_end = vec![0]; + self.range_end_options = RangeOption::FromKey; self } @@ -155,7 +143,7 @@ impl WatchRequest { #[inline] #[must_use] pub fn with_range_end(mut self, range_end: impl Into>) -> Self { - self.inner.range_end = range_end.into(); + self.range_end_options = RangeOption::RangeEnd(range_end.into()); self } @@ -212,9 +200,12 @@ impl WatchRequest { } } -impl From for xlineapi::WatchCreateRequest { +impl From for xlineapi::WatchCreateRequest { #[inline] - fn from(request: WatchRequest) -> Self { + fn from(mut request: WatchOptions) -> Self { + request.inner.range_end = request + .range_end_options + .get_range_end(&mut request.inner.key); request.inner } } @@ -278,3 +269,22 @@ impl DerefMut for WatchStreaming { &mut self.inner } } + +#[cfg(test)] +mod tests { + use xlineapi::command::KeyRange; + + use super::*; + + #[test] + fn test_watch_request_build_from_watch_options() { + let options = WatchOptions::default().with_prev_kv().with_key("key"); + let request = xlineapi::WatchCreateRequest::from(options.clone()); + assert!(request.prev_kv); + assert!(request.range_end.is_empty()); + + let options2 = options.clone().with_prefix(); + let request = xlineapi::WatchCreateRequest::from(options2.clone()); + assert_eq!(request.range_end, KeyRange::get_prefix("key")); + } +} diff --git a/crates/xline-client/tests/it/auth.rs b/crates/xline-client/tests/it/auth.rs index 83a191691..da32304c2 100644 --- a/crates/xline-client/tests/it/auth.rs +++ b/crates/xline-client/tests/it/auth.rs @@ -1,11 +1,9 @@ //! The following tests are originally from `etcd-client` use xline_client::{ error::Result, - types::auth::{ - AuthRoleAddRequest, AuthRoleDeleteRequest, AuthRoleGetRequest, - AuthRoleGrantPermissionRequest, AuthRoleRevokePermissionRequest, AuthUserAddRequest, - AuthUserChangePasswordRequest, AuthUserDeleteRequest, AuthUserGetRequest, - AuthUserGrantRoleRequest, AuthUserRevokeRoleRequest, Permission, PermissionType, + types::{ + auth::{Permission, PermissionType}, + range_end::RangeOption, }, }; @@ -18,11 +16,11 @@ async fn role_operations_should_success_in_normal_path() -> Result<()> { let role1 = "role1"; let role2 = "role2"; - client.role_add(AuthRoleAddRequest::new(role1)).await?; - client.role_add(AuthRoleAddRequest::new(role2)).await?; + client.role_add(role1).await?; + client.role_add(role2).await?; - client.role_get(AuthRoleGetRequest::new(role1)).await?; - client.role_get(AuthRoleGetRequest::new(role2)).await?; + client.role_get(role1).await?; + client.role_get(role2).await?; let role_list_resp = client.role_list().await?; assert_eq!( @@ -30,21 +28,11 @@ async fn role_operations_should_success_in_normal_path() -> Result<()> { vec![role1.to_owned(), role2.to_owned()] ); - client - .role_delete(AuthRoleDeleteRequest::new(role1)) - .await?; - client - .role_delete(AuthRoleDeleteRequest::new(role2)) - .await?; + client.role_delete(role1).await?; + client.role_delete(role2).await?; - client - .role_get(AuthRoleGetRequest::new(role1)) - .await - .unwrap_err(); - client - .role_get(AuthRoleGetRequest::new(role2)) - .await - .unwrap_err(); + client.role_get(role1).await.unwrap_err(); + client.role_get(role2).await.unwrap_err(); Ok(()) } @@ -55,67 +43,60 @@ async fn permission_operations_should_success_in_normal_path() -> Result<()> { let client = client.auth_client(); let role1 = "role1"; - let perm1 = Permission::new(PermissionType::Read, "123"); - let perm2 = Permission::new(PermissionType::Write, "abc").with_from_key(); - let perm3 = Permission::new(PermissionType::Readwrite, "hi").with_range_end("hjj"); - let perm4 = Permission::new(PermissionType::Write, "pp").with_prefix(); - let perm5 = Permission::new(PermissionType::Read, vec![0]).with_from_key(); - - client.role_add(AuthRoleAddRequest::new(role1)).await?; - - client - .role_grant_permission(AuthRoleGrantPermissionRequest::new(role1, perm1.clone())) - .await?; - client - .role_grant_permission(AuthRoleGrantPermissionRequest::new(role1, perm2.clone())) - .await?; - client - .role_grant_permission(AuthRoleGrantPermissionRequest::new(role1, perm3.clone())) - .await?; - client - .role_grant_permission(AuthRoleGrantPermissionRequest::new(role1, perm4.clone())) - .await?; - client - .role_grant_permission(AuthRoleGrantPermissionRequest::new(role1, perm5.clone())) - .await?; + let perm1 = (PermissionType::Read, "123", None); + let perm2 = (PermissionType::Write, "abc", Some(RangeOption::FromKey)); + let perm3 = ( + PermissionType::Readwrite, + "hi", + Some(RangeOption::RangeEnd("hjj".into())), + ); + let perm4 = (PermissionType::Write, "pp", Some(RangeOption::Prefix)); + let perm5 = (PermissionType::Read, vec![0], Some(RangeOption::FromKey)); + + client.role_add(role1).await?; + + let (p1, p2, p3) = perm1.clone(); + client.role_grant_permission(role1, p1, p2, p3).await?; + let (p1, p2, p3) = perm2.clone(); + client.role_grant_permission(role1, p1, p2, p3).await?; + let (p1, p2, p3) = perm3.clone(); + client.role_grant_permission(role1, p1, p2, p3).await?; + let (p1, p2, p3) = perm4.clone(); + client.role_grant_permission(role1, p1, p2, p3).await?; + let (p1, p2, p3) = perm5.clone(); + client.role_grant_permission(role1, p1, p2, p3).await?; { - let resp = client.role_get(AuthRoleGetRequest::new(role1)).await?; + // get permissions for role1, and validate the result + let resp = client.role_get(role1).await?; let permissions = resp.perm; - assert!(permissions.contains(&perm1.into())); - assert!(permissions.contains(&perm2.into())); - assert!(permissions.contains(&perm3.into())); - assert!(permissions.contains(&perm4.into())); - assert!(permissions.contains(&perm5.into())); + + assert!(permissions.contains(&Permission::from(perm1).into())); + assert!(permissions.contains(&Permission::from(perm2).into())); + assert!(permissions.contains(&Permission::from(perm3).into())); + assert!(permissions.contains(&Permission::from(perm4).into())); + assert!(permissions.contains(&Permission::from(perm5).into())); } // revoke all permission + client.role_revoke_permission(role1, "123", None).await?; client - .role_revoke_permission(AuthRoleRevokePermissionRequest::new(role1, "123")) + .role_revoke_permission(role1, "abc", Some(RangeOption::FromKey)) .await?; client - .role_revoke_permission(AuthRoleRevokePermissionRequest::new(role1, "abc").with_from_key()) + .role_revoke_permission(role1, "hi", Some(RangeOption::RangeEnd("hjj".into()))) .await?; client - .role_revoke_permission( - AuthRoleRevokePermissionRequest::new(role1, "hi").with_range_end("hjj"), - ) + .role_revoke_permission(role1, "pp", Some(RangeOption::Prefix)) .await?; client - .role_revoke_permission(AuthRoleRevokePermissionRequest::new(role1, "pp").with_prefix()) - .await?; - client - .role_revoke_permission( - AuthRoleRevokePermissionRequest::new(role1, vec![0]).with_from_key(), - ) + .role_revoke_permission(role1, vec![0], Some(RangeOption::FromKey)) .await?; - let role_get_resp = client.role_get(AuthRoleGetRequest::new(role1)).await?; + let role_get_resp = client.role_get(role1).await?; assert!(role_get_resp.perm.is_empty()); - client - .role_delete(AuthRoleDeleteRequest::new(role1)) - .await?; + client.role_delete(role1).await?; Ok(()) } @@ -128,25 +109,16 @@ async fn user_operations_should_success_in_normal_path() -> Result<()> { let password1 = "pwd1"; let password2 = "pwd2"; - client - .user_add(AuthUserAddRequest::new(name1).with_pwd(password1)) - .await?; - client.user_get(AuthUserGetRequest::new(name1)).await?; + client.user_add(name1, password1, false).await?; + client.user_get(name1).await?; let user_list_resp = client.user_list().await?; assert!(user_list_resp.users.contains(&name1.to_string())); - client - .user_change_password(AuthUserChangePasswordRequest::new(name1, password2)) - .await?; + client.user_change_password(name1, password2).await?; - client - .user_delete(AuthUserDeleteRequest::new(name1)) - .await?; - client - .user_get(AuthUserGetRequest::new(name1)) - .await - .unwrap_err(); + client.user_delete(name1).await?; + client.user_get(name1).await.unwrap_err(); Ok(()) } @@ -160,29 +132,21 @@ async fn user_role_operations_should_success_in_normal_path() -> Result<()> { let role1 = "role1"; let role2 = "role2"; - client.user_add(AuthUserAddRequest::new(name1)).await?; - client.role_add(AuthRoleAddRequest::new(role1)).await?; - client.role_add(AuthRoleAddRequest::new(role2)).await?; + client.user_add(name1, "", true).await?; + client.role_add(role1).await?; + client.role_add(role2).await?; - client - .user_grant_role(AuthUserGrantRoleRequest::new(name1, role1)) - .await?; - client - .user_grant_role(AuthUserGrantRoleRequest::new(name1, role2)) - .await?; + client.user_grant_role(name1, role1).await?; + client.user_grant_role(name1, role2).await?; - let user_get_resp = client.user_get(AuthUserGetRequest::new(name1)).await?; + let user_get_resp = client.user_get(name1).await?; assert_eq!( user_get_resp.roles, vec![role1.to_owned(), role2.to_owned()] ); - client - .user_revoke_role(AuthUserRevokeRoleRequest::new(name1, role1)) - .await?; - client - .user_revoke_role(AuthUserRevokeRoleRequest::new(name1, role2)) - .await?; + client.user_revoke_role(name1, role1).await?; + client.user_revoke_role(name1, role2).await?; Ok(()) } diff --git a/crates/xline-client/tests/it/kv.rs b/crates/xline-client/tests/it/kv.rs index dd36e4e96..e254adfd4 100644 --- a/crates/xline-client/tests/it/kv.rs +++ b/crates/xline-client/tests/it/kv.rs @@ -1,10 +1,10 @@ //! The following tests are originally from `etcd-client` + use test_macros::abort_on_panic; use xline_client::{ error::Result, types::kv::{ - CompactionRequest, Compare, CompareResult, DeleteRangeRequest, PutRequest, RangeRequest, - TxnOp, TxnRequest, + Compare, CompareResult, DeleteRangeOptions, PutOptions, RangeOptions, TxnOp, TxnRequest, }, }; @@ -16,13 +16,13 @@ async fn put_should_success_in_normal_path() -> Result<()> { let (_cluster, client) = get_cluster_client().await.unwrap(); let client = client.kv_client(); - let request = PutRequest::new("put", "123"); - client.put(request).await?; + client.put("put", "123", None).await?; // overwrite with prev key { - let request = PutRequest::new("put", "456").with_prev_kv(true); - let resp = client.put(request).await?; + let resp = client + .put("put", "456", Some(PutOptions::default().with_prev_kv(true))) + .await?; let prev_kv = resp.prev_kv; assert!(prev_kv.is_some()); let prev_kv = prev_kv.unwrap(); @@ -32,8 +32,9 @@ async fn put_should_success_in_normal_path() -> Result<()> { // overwrite again with prev key { - let request = PutRequest::new("put", "456").with_prev_kv(true); - let resp = client.put(request).await?; + let resp = client + .put("put", "456", Some(PutOptions::default().with_prev_kv(true))) + .await?; let prev_kv = resp.prev_kv; assert!(prev_kv.is_some()); let prev_kv = prev_kv.unwrap(); @@ -50,14 +51,14 @@ async fn range_should_fetches_previously_put_keys() -> Result<()> { let (_cluster, client) = get_cluster_client().await.unwrap(); let client = client.kv_client(); - client.put(PutRequest::new("get10", "10")).await?; - client.put(PutRequest::new("get11", "11")).await?; - client.put(PutRequest::new("get20", "20")).await?; - client.put(PutRequest::new("get21", "21")).await?; + client.put("get10", "10", None).await?; + client.put("get11", "11", None).await?; + client.put("get20", "20", None).await?; + client.put("get21", "21", None).await?; // get key { - let resp = client.range(RangeRequest::new("get11")).await?; + let resp = client.range("get11", None).await?; assert_eq!(resp.count, 1); assert!(!resp.more); assert_eq!(resp.kvs.len(), 1); @@ -68,7 +69,10 @@ async fn range_should_fetches_previously_put_keys() -> Result<()> { // get from key { let resp = client - .range(RangeRequest::new("get11").with_from_key().with_limit(2)) + .range( + "get11", + Some(RangeOptions::default().with_from_key().with_limit(2)), + ) .await?; assert!(resp.more); assert_eq!(resp.kvs.len(), 2); @@ -81,7 +85,7 @@ async fn range_should_fetches_previously_put_keys() -> Result<()> { // get prefix keys { let resp = client - .range(RangeRequest::new("get1").with_prefix()) + .range("get1", Some(RangeOptions::default().with_prefix())) .await?; assert_eq!(resp.count, 2); assert!(!resp.more); @@ -101,23 +105,26 @@ async fn delete_should_remove_previously_put_kvs() -> Result<()> { let (_cluster, client) = get_cluster_client().await.unwrap(); let client = client.kv_client(); - client.put(PutRequest::new("del10", "10")).await?; - client.put(PutRequest::new("del11", "11")).await?; - client.put(PutRequest::new("del20", "20")).await?; - client.put(PutRequest::new("del21", "21")).await?; - client.put(PutRequest::new("del31", "31")).await?; - client.put(PutRequest::new("del32", "32")).await?; + client.put("del10", "10", None).await?; + client.put("del11", "11", None).await?; + client.put("del20", "20", None).await?; + client.put("del21", "21", None).await?; + client.put("del31", "31", None).await?; + client.put("del32", "32", None).await?; // delete key { let resp = client - .delete(DeleteRangeRequest::new("del11").with_prev_kv(true)) + .delete( + "del11", + Some(DeleteRangeOptions::default().with_prev_kv(true)), + ) .await?; assert_eq!(resp.deleted, 1); assert_eq!(&resp.prev_kvs[0].key, "del11".as_bytes()); assert_eq!(&resp.prev_kvs[0].value, "11".as_bytes()); let resp = client - .range(RangeRequest::new("del11").with_count_only(true)) + .range("del11", Some(RangeOptions::default().with_count_only(true))) .await?; assert_eq!(resp.count, 0); } @@ -126,9 +133,12 @@ async fn delete_should_remove_previously_put_kvs() -> Result<()> { { let resp = client .delete( - DeleteRangeRequest::new("del11") - .with_range_end("del22") - .with_prev_kv(true), + "del11", + Some( + DeleteRangeOptions::default() + .with_range_end("del22") + .with_prev_kv(true), + ), ) .await?; assert_eq!(resp.deleted, 2); @@ -138,9 +148,12 @@ async fn delete_should_remove_previously_put_kvs() -> Result<()> { assert_eq!(&resp.prev_kvs[1].value, "21".as_bytes()); let resp = client .range( - RangeRequest::new("del11") - .with_range_end("del22") - .with_count_only(true), + "del11", + Some( + RangeOptions::default() + .with_range_end("del22") + .with_count_only(true), + ), ) .await?; assert_eq!(resp.count, 0); @@ -150,9 +163,12 @@ async fn delete_should_remove_previously_put_kvs() -> Result<()> { { let resp = client .delete( - DeleteRangeRequest::new("del3") - .with_prefix() - .with_prev_kv(true), + "del3", + Some( + DeleteRangeOptions::default() + .with_prefix() + .with_prev_kv(true), + ), ) .await?; assert_eq!(resp.deleted, 2); @@ -161,7 +177,7 @@ async fn delete_should_remove_previously_put_kvs() -> Result<()> { assert_eq!(&resp.prev_kvs[1].key, "del32".as_bytes()); assert_eq!(&resp.prev_kvs[1].value, "32".as_bytes()); let resp = client - .range(RangeRequest::new("del32").with_count_only(true)) + .range("del32", Some(RangeOptions::default().with_count_only(true))) .await?; assert_eq!(resp.count, 0); } @@ -175,7 +191,7 @@ async fn txn_should_execute_as_expected() -> Result<()> { let (_cluster, client) = get_cluster_client().await.unwrap(); let client = client.kv_client(); - client.put(PutRequest::new("txn01", "01")).await?; + client.put("txn01", "01", None).await?; // transaction 1 { @@ -185,10 +201,12 @@ async fn txn_should_execute_as_expected() -> Result<()> { .when(&[Compare::value("txn01", CompareResult::Equal, "01")][..]) .and_then( &[TxnOp::put( - PutRequest::new("txn01", "02").with_prev_kv(true), + "txn01", + "02", + Some(PutOptions::default().with_prev_kv(true)), )][..], ) - .or_else(&[TxnOp::range(RangeRequest::new("txn01"))][..]), + .or_else(&[TxnOp::range("txn01", None)][..]), ) .await?; @@ -203,7 +221,7 @@ async fn txn_should_execute_as_expected() -> Result<()> { _ => panic!("expect put response)"), } - let resp = client.range(RangeRequest::new("txn01")).await?; + let resp = client.range("txn01", None).await?; assert_eq!(resp.kvs[0].key, b"txn01"); assert_eq!(resp.kvs[0].value, b"02"); } @@ -214,8 +232,8 @@ async fn txn_should_execute_as_expected() -> Result<()> { .txn( TxnRequest::new() .when(&[Compare::value("txn01", CompareResult::Equal, "01")][..]) - .and_then(&[TxnOp::put(PutRequest::new("txn01", "02"))][..]) - .or_else(&[TxnOp::range(RangeRequest::new("txn01"))][..]), + .and_then(&[TxnOp::put("txn01", "02", None)][..]) + .or_else(&[TxnOp::range("txn01", None)][..]), ) .await?; @@ -240,31 +258,31 @@ async fn compact_should_remove_previous_revision() -> Result<()> { let (_cluster, client) = get_cluster_client().await.unwrap(); let client = client.kv_client(); - client.put(PutRequest::new("compact", "0")).await?; - client.put(PutRequest::new("compact", "1")).await?; + client.put("compact", "0", None).await?; + client.put("compact", "1", None).await?; // before compacting let rev0_resp = client - .range(RangeRequest::new("compact").with_revision(2)) + .range("compact", Some(RangeOptions::default().with_revision(2))) .await?; assert_eq!(rev0_resp.kvs[0].value, b"0"); let rev1_resp = client - .range(RangeRequest::new("compact").with_revision(3)) + .range("compact", Some(RangeOptions::default().with_revision(3))) .await?; assert_eq!(rev1_resp.kvs[0].value, b"1"); - client.compact(CompactionRequest::new(3)).await?; + client.compact(3, false).await?; // after compacting let rev0_resp = client - .range(RangeRequest::new("compact").with_revision(2)) + .range("compact", Some(RangeOptions::default().with_revision(2))) .await; assert!( rev0_resp.is_err(), "client.range should receive an err after compaction, but it receives: {rev0_resp:?}" ); let rev1_resp = client - .range(RangeRequest::new("compact").with_revision(3)) + .range("compact", Some(RangeOptions::default().with_revision(3))) .await?; assert_eq!(rev1_resp.kvs[0].value, b"1"); diff --git a/crates/xline-client/tests/it/lease.rs b/crates/xline-client/tests/it/lease.rs index 4bab8caba..445162eb3 100644 --- a/crates/xline-client/tests/it/lease.rs +++ b/crates/xline-client/tests/it/lease.rs @@ -1,9 +1,4 @@ -use xline_client::{ - error::Result, - types::lease::{ - LeaseGrantRequest, LeaseKeepAliveRequest, LeaseRevokeRequest, LeaseTimeToLiveRequest, - }, -}; +use xline_client::error::Result; use super::common::get_cluster_client; @@ -12,10 +7,10 @@ async fn grant_revoke_should_success_in_normal_path() -> Result<()> { let (_cluster, client) = get_cluster_client().await.unwrap(); let mut client = client.lease_client(); - let resp = client.grant(LeaseGrantRequest::new(123)).await?; + let resp = client.grant(123, None).await?; assert_eq!(resp.ttl, 123); let id = resp.id; - client.revoke(LeaseRevokeRequest::new(id)).await?; + client.revoke(id).await?; Ok(()) } @@ -25,18 +20,18 @@ async fn keep_alive_should_success_in_normal_path() -> Result<()> { let (_cluster, client) = get_cluster_client().await.unwrap(); let mut client = client.lease_client(); - let resp = client.grant(LeaseGrantRequest::new(60)).await?; + let resp = client.grant(60, None).await?; assert_eq!(resp.ttl, 60); let id = resp.id; - let (mut keeper, mut stream) = client.keep_alive(LeaseKeepAliveRequest::new(id)).await?; + let (mut keeper, mut stream) = client.keep_alive(id).await?; keeper.keep_alive()?; let resp = stream.message().await?.unwrap(); assert_eq!(resp.id, keeper.id()); assert_eq!(resp.ttl, 60); - client.revoke(LeaseRevokeRequest::new(id)).await?; + client.revoke(id).await?; Ok(()) } @@ -47,19 +42,15 @@ async fn time_to_live_ttl_is_consistent_in_normal_path() -> Result<()> { let mut client = client.lease_client(); let lease_id = 200; - let resp = client - .grant(LeaseGrantRequest::new(60).with_id(lease_id)) - .await?; + let resp = client.grant(60, Some(lease_id)).await?; assert_eq!(resp.ttl, 60); assert_eq!(resp.id, lease_id); - let resp = client - .time_to_live(LeaseTimeToLiveRequest::new(lease_id)) - .await?; + let resp = client.time_to_live(lease_id, false).await?; assert_eq!(resp.id, lease_id); assert_eq!(resp.granted_ttl, 60); - client.revoke(LeaseRevokeRequest::new(lease_id)).await?; + client.revoke(lease_id).await?; Ok(()) } @@ -73,21 +64,15 @@ async fn leases_should_include_granted_in_normal_path() -> Result<()> { let (_cluster, client) = get_cluster_client().await.unwrap(); let mut client = client.lease_client(); - let resp = client - .grant(LeaseGrantRequest::new(60).with_id(lease1)) - .await?; + let resp = client.grant(60, Some(lease1)).await?; assert_eq!(resp.ttl, 60); assert_eq!(resp.id, lease1); - let resp = client - .grant(LeaseGrantRequest::new(60).with_id(lease2)) - .await?; + let resp = client.grant(60, Some(lease2)).await?; assert_eq!(resp.ttl, 60); assert_eq!(resp.id, lease2); - let resp = client - .grant(LeaseGrantRequest::new(60).with_id(lease3)) - .await?; + let resp = client.grant(60, Some(lease3)).await?; assert_eq!(resp.ttl, 60); assert_eq!(resp.id, lease3); @@ -97,9 +82,9 @@ async fn leases_should_include_granted_in_normal_path() -> Result<()> { assert!(leases.contains(&lease2)); assert!(leases.contains(&lease3)); - client.revoke(LeaseRevokeRequest::new(lease1)).await?; - client.revoke(LeaseRevokeRequest::new(lease2)).await?; - client.revoke(LeaseRevokeRequest::new(lease3)).await?; + client.revoke(lease1).await?; + client.revoke(lease2).await?; + client.revoke(lease3).await?; Ok(()) } diff --git a/crates/xline-client/tests/it/watch.rs b/crates/xline-client/tests/it/watch.rs index eca6a33e1..f6c573088 100644 --- a/crates/xline-client/tests/it/watch.rs +++ b/crates/xline-client/tests/it/watch.rs @@ -1,11 +1,5 @@ //! The following tests are originally from `etcd-client` -use xline_client::{ - error::Result, - types::{ - kv::PutRequest, - watch::{EventType, WatchRequest}, - }, -}; +use xline_client::{error::Result, types::watch::EventType}; use super::common::get_cluster_client; @@ -15,9 +9,9 @@ async fn watch_should_receive_consistent_events() -> Result<()> { let mut watch_client = client.watch_client(); let kv_client = client.kv_client(); - let (mut watcher, mut stream) = watch_client.watch(WatchRequest::new("watch01")).await?; + let (mut watcher, mut stream) = watch_client.watch("watch01", None).await?; - kv_client.put(PutRequest::new("watch01", "01")).await?; + kv_client.put("watch01", "01", None).await?; let resp = stream.message().await?.unwrap(); assert_eq!(resp.watch_id, watcher.watch_id()); @@ -44,9 +38,9 @@ async fn watch_stream_should_work_after_watcher_dropped() -> Result<()> { let mut watch_client = client.watch_client(); let kv_client = client.kv_client(); - let (_, mut stream) = watch_client.watch(WatchRequest::new("watch01")).await?; + let (_, mut stream) = watch_client.watch("watch01", None).await?; - kv_client.put(PutRequest::new("watch01", "01")).await?; + kv_client.put("watch01", "01", None).await?; let resp = stream.message().await?.unwrap(); assert_eq!(resp.events.len(), 1); diff --git a/crates/xline-test-utils/Cargo.toml b/crates/xline-test-utils/Cargo.toml index 8c8c40e5d..e8a6ee5cf 100644 --- a/crates/xline-test-utils/Cargo.toml +++ b/crates/xline-test-utils/Cargo.toml @@ -21,8 +21,7 @@ tokio = { version = "0.2.25", package = "madsim-tokio", features = [ "net", "signal", ] } -# tonic = "0.11.0" -tonic = { version = "0.4.2", package = "madsim-tonic" } +tonic = { version = "0.5.0", package = "madsim-tonic" } utils = { path = "../utils", features = ["parking_lot"] } workspace-hack = { version = "0.1", path = "../../workspace-hack" } xline = { path = "../xline" } diff --git a/crates/xline-test-utils/src/lib.rs b/crates/xline-test-utils/src/lib.rs index 624b7f32b..b3135bf24 100644 --- a/crates/xline-test-utils/src/lib.rs +++ b/crates/xline-test-utils/src/lib.rs @@ -14,10 +14,7 @@ use utils::config::{ LogConfig, MetricsConfig, StorageConfig, TlsConfig, TraceConfig, XlineServerConfig, }; use xline::server::XlineServer; -use xline_client::types::auth::{ - AuthRoleAddRequest, AuthRoleGrantPermissionRequest, AuthUserAddRequest, - AuthUserGrantRoleRequest, Permission, PermissionType, -}; +use xline_client::types::{auth::PermissionType, range_end::RangeOption}; pub use xline_client::{clients, types, Client, ClientOptions}; /// Cluster @@ -348,19 +345,17 @@ pub async fn set_user( range_end: &[u8], ) -> Result<(), Box> { let client = client.auth_client(); - client - .user_add(AuthUserAddRequest::new(name).with_pwd(password)) - .await?; - client.role_add(AuthRoleAddRequest::new(role)).await?; - client - .user_grant_role(AuthUserGrantRoleRequest::new(name, role)) - .await?; + client.user_add(name, password, false).await?; + client.role_add(role).await?; + client.user_grant_role(name, role).await?; if !key.is_empty() { client - .role_grant_permission(AuthRoleGrantPermissionRequest::new( + .role_grant_permission( role, - Permission::new(PermissionType::Readwrite, key).with_range_end(range_end), - )) + PermissionType::Readwrite, + key, + Some(RangeOption::RangeEnd(range_end.to_vec())), + ) .await?; } Ok(()) diff --git a/crates/xline/Cargo.toml b/crates/xline/Cargo.toml index 3d16ec07f..a47d6c8eb 100644 --- a/crates/xline/Cargo.toml +++ b/crates/xline/Cargo.toml @@ -14,42 +14,45 @@ categories = ["KV"] [dependencies] anyhow = "1.0.83" async-stream = "0.3.5" -async-trait = "0.1.80" -axum = "0.6.20" -bytes = "1.4.0" +async-trait = "0.1.81" +axum = "0.7.0" +bytes = "1.7.1" clap = { version = "4", features = ["derive"] } clippy-utilities = "0.2.0" -crc32fast = "1.4.0" +crc32fast = "1.4.2" crossbeam-skiplist = "0.1.1" curp = { path = "../curp", version = "0.1.0", features = ["client-metrics"] } curp-external-api = { path = "../curp-external-api" } -dashmap = "5.5.3" +dashmap = "6.0.1" engine = { path = "../engine" } -event-listener = "5.3.0" +event-listener = "5.3.1" +flume = "0.11.0" futures = "0.3.25" -hyper = "0.14.27" +hyper = "1.0.0" itertools = "0.13" jsonwebtoken = "9.3.0" log = "0.4.21" merged_range = "0.1.0" nix = "0.28.0" -opentelemetry = { version = "0.22.0", features = ["metrics"] } -opentelemetry-contrib = { version = "0.14.0", features = [ +opentelemetry = { version = "0.24.0", features = ["metrics"] } +opentelemetry-contrib = { version = "0.16.0", features = [ "jaeger_json_exporter", "rt-tokio", ] } -opentelemetry-otlp = { version = "0.15.0", features = [ +opentelemetry-otlp = { version = "0.17.0", features = [ + "grpc-tonic", "metrics", "http-proto", "reqwest-client", ] } -opentelemetry-prometheus = { version = "0.15.0" } -opentelemetry_sdk = { version = "0.22.1", features = ["metrics", "rt-tokio"] } +opentelemetry-prometheus = { version = "0.17.0" } +opentelemetry_sdk = { version = "0.24.1", features = ["metrics", "rt-tokio"] } parking_lot = "0.12.3" pbkdf2 = { version = "0.12.2", features = ["simple"] } priority-queue = "2.0.2" prometheus = "0.13.4" -prost = "0.12.3" +prost = "0.13.0" +real_tokio = { version = "1", package = "tokio" } serde = { version = "1.0.204", features = ["derive"] } sha2 = "0.10.6" tokio = { version = "0.2.25", package = "madsim-tokio", features = [ @@ -62,28 +65,27 @@ tokio = { version = "0.2.25", package = "madsim-tokio", features = [ tokio-stream = { git = "https://github.com/madsim-rs/tokio.git", rev = "ab251ad" } tokio-util = { version = "0.7.11", features = ["io"] } toml = "0.8.14" -# tonic = "0.11.0" -tonic = { version = "0.4.2", package = "madsim-tonic" } -tonic-health = "0.11.0" +tonic = { version = "0.5.0", package = "madsim-tonic" } +tonic-health = "0.12.0" tracing = "0.1.37" tracing-appender = "0.2" -tracing-opentelemetry = "0.23.0" +tracing-opentelemetry = "0.25.0" tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } utils = { path = "../utils", features = ["parking_lot"] } -uuid = { version = "1.9.0", features = ["v4"] } +uuid = { version = "1.10.0", features = ["v4"] } workspace-hack = { version = "0.1", path = "../../workspace-hack" } x509-certificate = "0.23.1" xlineapi = { path = "../xlineapi" } [build-dependencies] -tonic-build = { version = "0.4.3", package = "madsim-tonic-build" } +tonic-build = { version = "0.5.0", package = "madsim-tonic-build" } [dev-dependencies] -etcd-client = { version = "0.13.0", features = ["tls"] } +etcd-client = { version = "0.14.0", features = ["tls"] } mockall = "0.12.1" rand = "0.8.5" strum = "0.26" -strum_macros = "0.26.2" +strum_macros = "0.26.4" test-macros = { path = "../test-macros" } xline-client = { path = "../xline-client" } xline-test-utils = { path = "../xline-test-utils" } diff --git a/crates/xline/src/conflict/mod.rs b/crates/xline/src/conflict/mod.rs index 28c652cd3..279c2f90e 100644 --- a/crates/xline/src/conflict/mod.rs +++ b/crates/xline/src/conflict/mod.rs @@ -1,15 +1,24 @@ +use std::sync::Arc; + use curp::{ cmd::Command as CurpCommand, + rpc::PoolEntry, server::{SpObject, UcpObject}, }; use utils::interval_map::Interval; -use xlineapi::{command::Command, interval::BytesAffine, RequestBackend, RequestWrapper}; +use xlineapi::{ + command::{Command, KeyRange}, + interval::BytesAffine, + RequestWrapper, +}; + +use crate::storage::lease_store::{Lease, LeaseCollection}; use self::{ spec_pool::{ExclusiveSpecPool, KvSpecPool, LeaseSpecPool}, uncommitted_pool::{ExclusiveUncomPool, KvUncomPool, LeaseUncomPool}, }; - +// TODO: Refine code to improve reusability for different conflict pool types /// Speculative pool implementations pub(crate) mod spec_pool; /// Uncommitted pool implementations @@ -20,25 +29,46 @@ pub(crate) mod uncommitted_pool; mod tests; /// Returns command intervals -fn intervals(entry: &C) -> Vec> +fn intervals(lease_collection: &LeaseCollection, entry: &C) -> Vec> where C: AsRef, { - entry - .as_ref() - .keys() - .iter() - .cloned() - .map(Into::into) + intervals_kv(entry) + .into_iter() + .chain(intervals_lease(lease_collection, entry)) .collect() } -/// Filter kv commands -fn filter_kv(entry: C) -> Option +/// Gets KV intervals of a kv request +fn intervals_kv(entry: &C) -> impl IntoIterator> +where + C: AsRef, +{ + entry.as_ref().keys().into_iter().map(Into::into) +} + +/// Gets KV intervals of a lease request +/// +/// We also needs to handle `LeaseRevokeRequest` in KV conflict pools, +/// as a revoke may delete keys associated with the lease id. Therefore, +/// we should insert these keys into the KV conflict pool as well. +fn intervals_lease( + lease_collection: &LeaseCollection, + entry: &C, +) -> impl IntoIterator> where C: AsRef, { - matches!(entry.as_ref().request().backend(), RequestBackend::Kv).then_some(entry) + let id = if let RequestWrapper::LeaseRevokeRequest(ref req) = *entry.as_ref().request() { + lease_collection.look_up(req.id) + } else { + None + }; + + id.into_iter() + .flat_map(Lease::into_keys) + .map(KeyRange::new_one_key) + .map(Into::into) } /// Returns `true` if this command conflicts with all other commands @@ -62,6 +92,29 @@ fn is_exclusive_cmd(cmd: &Command) -> bool { ) } +/// Gets all lease id +/// * lease ids in the requests field +/// * lease ids associated with the keys +pub(super) fn all_leases(lease_collection: &LeaseCollection, req: &PoolEntry) -> Vec { + req.leases() + .into_iter() + .chain(lookup_lease(lease_collection, req)) + .collect() +} + +/// Lookups lease ids from lease collection +/// +/// We also needs to handle `PutRequest` and `DeleteRangeRequest` in +/// lease conflict pools, as they may conflict with a `LeaseRevokeRequest`. +/// Therefore, we should lookup the lease ids from lease collection. +fn lookup_lease(lease_collection: &LeaseCollection, req: &PoolEntry) -> Vec { + req.request() + .keys() + .into_iter() + .flat_map(|key| lease_collection.get_lease_by_range(key)) + .collect() +} + /// Xline speculative pools wrapper pub(crate) struct XlineSpeculativePools(Vec>); @@ -72,10 +125,11 @@ impl XlineSpeculativePools { } } -impl Default for XlineSpeculativePools { - fn default() -> Self { - let kv_sp = Box::::default(); - let lease_sp = Box::::default(); +impl XlineSpeculativePools { + /// Creates a new [`XlineSpeculativePools`]. + pub(crate) fn new(lease_collection: Arc) -> Self { + let kv_sp = Box::new(KvSpecPool::new(Arc::clone(&lease_collection))); + let lease_sp = Box::new(LeaseSpecPool::new(lease_collection)); let exclusive_sp = Box::::default(); Self(vec![kv_sp, lease_sp, exclusive_sp]) } @@ -91,10 +145,11 @@ impl XlineUncommittedPools { } } -impl Default for XlineUncommittedPools { - fn default() -> Self { - let kv_ucp = Box::::default(); - let lease_ucp = Box::::default(); +impl XlineUncommittedPools { + /// Creates a new [`XlineUncommittedPools`]. + pub(crate) fn new(lease_collection: Arc) -> Self { + let kv_ucp = Box::new(KvUncomPool::new(Arc::clone(&lease_collection))); + let lease_ucp = Box::new(LeaseUncomPool::new(lease_collection)); let exclusive_ucp = Box::::default(); Self(vec![kv_ucp, lease_ucp, exclusive_ucp]) } diff --git a/crates/xline/src/conflict/spec_pool.rs b/crates/xline/src/conflict/spec_pool.rs index 5ac387c2c..f015804c1 100644 --- a/crates/xline/src/conflict/spec_pool.rs +++ b/crates/xline/src/conflict/spec_pool.rs @@ -1,35 +1,52 @@ -//! A speculative pool(witness) is used to store commands that are speculatively executed. -//! CURP requires that a witness only accepts and saves an operation if it is commutative -//! with every other operation currently stored by that witness +//! A speculative pool(witness) is used to store commands that are speculatively +//! executed. CURP requires that a witness only accepts and saves an operation +//! if it is commutative with every other operation currently stored by that +//! witness -use std::collections::HashMap; +use std::{collections::HashMap, sync::Arc}; -use curp::server::conflict::CommandEntry; -use curp_external_api::conflict::{ConflictPoolOp, SpeculativePoolOp}; -use utils::interval_map::IntervalMap; -use xlineapi::{ - command::{get_lease_ids, Command}, - interval::BytesAffine, -}; +use curp::rpc::PoolEntry; +use curp_external_api::conflict::{ConflictPoolOp, EntryId, SpeculativePoolOp}; +use utils::interval_map::{Interval, IntervalMap}; +use xlineapi::{command::Command, interval::BytesAffine}; -use super::{filter_kv, intervals, is_exclusive_cmd}; +use crate::storage::lease_store::LeaseCollection; + +use super::{all_leases, intervals, is_exclusive_cmd}; /// Speculative pool for KV commands. -#[derive(Debug, Default)] +#[derive(Debug)] +#[cfg_attr(test, derive(Default))] pub(crate) struct KvSpecPool { /// Interval map for keys overlap detection - map: IntervalMap>, + map: IntervalMap>, + /// Lease collection + lease_collection: Arc, + /// Id to intervals map + /// + /// NOTE: To avoid potential side-effects from the `LeaseCollection`, we + /// store The lookup results from `LeaseCollection` during entry insert + /// and use these result in entry remove. + intervals: + HashMap<<::Entry as EntryId>::Id, Vec>>, } -impl ConflictPoolOp for KvSpecPool { - type Entry = CommandEntry; +impl KvSpecPool { + /// Creates a new [`KvSpecPool`]. + pub(crate) fn new(lease_collection: Arc) -> Self { + Self { + map: IntervalMap::new(), + lease_collection, + intervals: HashMap::new(), + } + } +} - fn remove(&mut self, entry: Self::Entry) { - let Some(entry) = filter_kv(entry) else { - return; - }; +impl ConflictPoolOp for KvSpecPool { + type Entry = PoolEntry; - for interval in intervals(&entry) { + fn remove(&mut self, entry: &Self::Entry) { + for interval in self.intervals.remove(&entry.id()).into_iter().flatten() { let _ignore = self.map.remove(&interval); } } @@ -53,12 +70,16 @@ impl ConflictPoolOp for KvSpecPool { impl SpeculativePoolOp for KvSpecPool { fn insert_if_not_conflict(&mut self, entry: Self::Entry) -> Option { - let entry = filter_kv(entry)?; - - let intervals = intervals(&entry); - if intervals.iter().any(|i| self.map.overlap(i)) { + let intervals = intervals(&self.lease_collection, &entry); + if intervals.iter().any(|i| self.map.overlaps(i)) { return Some(entry); } + assert!( + self.intervals + .insert(entry.id(), intervals.clone()) + .is_none(), + "duplicate entry id" + ); for interval in intervals { let _ignore = self.map.insert(interval, entry.clone()); } @@ -67,22 +88,41 @@ impl SpeculativePoolOp for KvSpecPool { } /// Speculative pool for Lease commands. -#[derive(Debug, Default)] +#[derive(Debug)] +#[cfg_attr(test, derive(Default))] pub(crate) struct LeaseSpecPool { /// Stores leases in the pool - leases: HashMap>, + leases: HashMap>, + /// Lease collection + lease_collection: Arc, + /// Id to lease ids map + /// + /// NOTE: To avoid potential side-effects from the `LeaseCollection`, we + /// store The lookup results from `LeaseCollection` during entry insert + /// and use these result in entry remove. + ids: HashMap<<::Entry as EntryId>::Id, Vec>, +} + +impl LeaseSpecPool { + /// Creates a new [`LeaseSpecPool`]. + pub(crate) fn new(lease_collection: Arc) -> Self { + Self { + leases: HashMap::new(), + lease_collection, + ids: HashMap::new(), + } + } } impl ConflictPoolOp for LeaseSpecPool { - type Entry = CommandEntry; + type Entry = PoolEntry; fn is_empty(&self) -> bool { self.leases.is_empty() } - fn remove(&mut self, entry: Self::Entry) { - let ids = get_lease_ids(entry.request()); - for id in ids { + fn remove(&mut self, entry: &Self::Entry) { + for id in self.ids.remove(&entry.id()).into_iter().flatten() { let _ignore = self.leases.remove(&id); } } @@ -102,12 +142,16 @@ impl ConflictPoolOp for LeaseSpecPool { impl SpeculativePoolOp for LeaseSpecPool { fn insert_if_not_conflict(&mut self, entry: Self::Entry) -> Option { - let ids = get_lease_ids(entry.request()); - for id in ids.clone() { - if self.leases.contains_key(&id) { + let ids = all_leases(&self.lease_collection, &entry); + for id in &ids { + if self.leases.contains_key(id) { return Some(entry); } } + assert!( + self.ids.insert(entry.id(), ids.clone()).is_none(), + "duplicate entry id" + ); for id in ids { let _ignore = self.leases.insert(id, entry.clone()); } @@ -119,18 +163,18 @@ impl SpeculativePoolOp for LeaseSpecPool { #[derive(Debug, Default)] pub(crate) struct ExclusiveSpecPool { /// Stores the command - conflict: Option>, + conflict: Option>, } impl ConflictPoolOp for ExclusiveSpecPool { - type Entry = CommandEntry; + type Entry = PoolEntry; fn is_empty(&self) -> bool { self.conflict.is_none() } - fn remove(&mut self, entry: Self::Entry) { - if is_exclusive_cmd(&entry) { + fn remove(&mut self, entry: &Self::Entry) { + if is_exclusive_cmd(entry) { self.conflict = None; } } diff --git a/crates/xline/src/conflict/tests.rs b/crates/xline/src/conflict/tests.rs index e93677be4..44954f24f 100644 --- a/crates/xline/src/conflict/tests.rs +++ b/crates/xline/src/conflict/tests.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use curp::{rpc::ProposeId, server::conflict::CommandEntry}; +use curp::rpc::{PoolEntry, ProposeId}; use curp_external_api::conflict::{ConflictPoolOp, SpeculativePoolOp, UncommittedPoolOp}; use xlineapi::{ command::Command, AuthEnableRequest, AuthRoleAddRequest, DeleteRangeRequest, LeaseGrantRequest, @@ -8,9 +8,12 @@ use xlineapi::{ }; use super::spec_pool::{KvSpecPool, LeaseSpecPool}; -use crate::conflict::{ - spec_pool::ExclusiveSpecPool, - uncommitted_pool::{ExclusiveUncomPool, KvUncomPool, LeaseUncomPool}, +use crate::{ + conflict::{ + spec_pool::ExclusiveSpecPool, + uncommitted_pool::{ExclusiveUncomPool, KvUncomPool, LeaseUncomPool}, + }, + storage::lease_store::LeaseCollection, }; #[test] @@ -31,11 +34,11 @@ fn kv_sp_operations_are_ok() { vec![entry1.clone(), entry2.clone(), entry4.clone()], ); assert_eq!(sp.len(), 3); - sp.remove(entry1.clone()); + sp.remove(&entry1.clone()); assert!(sp.insert_if_not_conflict(entry3.clone()).is_some()); - sp.remove(entry2.clone()); + sp.remove(&entry2.clone()); assert!(sp.insert_if_not_conflict(entry3.clone()).is_some()); - sp.remove(entry4.clone()); + sp.remove(&entry4.clone()); assert!(sp.insert_if_not_conflict(entry3.clone()).is_none()); sp.clear(); assert!(sp.is_empty()); @@ -76,8 +79,8 @@ fn kv_ucp_operations_are_ok() { ucp.all_conflict(&entry6), vec![entry4.clone(), entry5.clone()], ); - ucp.remove(entry4.clone()); - ucp.remove(entry5.clone()); + ucp.remove(&entry4.clone()); + ucp.remove(&entry5.clone()); assert!(!ucp.insert(entry6.clone())); ucp.clear(); assert!(ucp.is_empty()); @@ -99,8 +102,8 @@ fn lease_sp_operations_are_ok() { assert!(sp.insert_if_not_conflict(entry4.clone()).is_some()); compare_commands(sp.all(), vec![entry1.clone(), entry2.clone()]); assert_eq!(sp.len(), 2); - sp.remove(entry1); - sp.remove(entry2); + sp.remove(&entry1); + sp.remove(&entry2); assert!(sp.insert_if_not_conflict(entry3).is_none()); assert!(sp.insert_if_not_conflict(entry4).is_none()); sp.clear(); @@ -141,8 +144,8 @@ fn lease_ucp_operations_are_ok() { ucp.all_conflict(&entry3), vec![entry3.clone(), entry5.clone()], ); - ucp.remove(entry3.clone()); - ucp.remove(entry5.clone()); + ucp.remove(&entry3.clone()); + ucp.remove(&entry5.clone()); assert!(!ucp.insert(entry5.clone())); ucp.clear(); assert!(ucp.is_empty()); @@ -160,7 +163,7 @@ fn exclusive_sp_operations_are_ok() { assert!(sp.insert_if_not_conflict(entry2.clone()).is_some()); compare_commands(sp.all(), vec![entry1.clone()]); assert_eq!(sp.len(), 1); - sp.remove(entry1); + sp.remove(&entry1); assert!(sp.insert_if_not_conflict(entry2).is_some()); sp.clear(); assert!(sp.is_empty()); @@ -181,15 +184,233 @@ fn exclusive_ucp_operations_are_ok() { vec![entry1.clone(), entry2.clone()], ); assert_eq!(ucp.len(), 2); - ucp.remove(entry1.clone()); - ucp.remove(entry2.clone()); + ucp.remove(&entry1.clone()); + ucp.remove(&entry2.clone()); assert!(ucp.insert(entry1.clone())); ucp.clear(); assert!(ucp.is_empty()); assert_eq!(ucp.len(), 0); } -fn compare_commands(mut a: Vec>, mut b: Vec>) { +#[test] +fn sp_kv_then_revoke_conflict_ok() { + let lease_collection = Arc::new(LeaseCollection::new(60)); + let mut sp = KvSpecPool::new(Arc::clone(&lease_collection)); + + let mut gen = EntryGenerator::default(); + // suppose initially we have a key "foo" associated with lease id 1 + lease_collection.grant(1, 60, true); + lease_collection + .attach(1, "foo".as_bytes().to_vec()) + .unwrap(); + + let kv_put = gen.gen_put("foo"); + let kv_delete = gen.gen_delete_range("foo", "foz"); + let lease_revoke = gen.gen_lease_revoke(1); + + // put conflicts with lease revoke + assert!(sp.insert_if_not_conflict(kv_put.clone()).is_none()); + assert!(sp.insert_if_not_conflict(lease_revoke.clone()).is_some()); + sp.remove(&kv_put); + assert!(sp.insert_if_not_conflict(lease_revoke.clone()).is_none()); + sp.remove(&lease_revoke.clone()); + + // delete range conflicts with lease revoke + assert!(sp.insert_if_not_conflict(kv_delete.clone()).is_none()); + assert!(sp.insert_if_not_conflict(lease_revoke.clone()).is_some()); + sp.remove(&kv_delete); + assert!(sp.insert_if_not_conflict(lease_revoke.clone()).is_none()); + sp.remove(&lease_revoke); +} + +#[test] +fn sp_revoke_then_kv_conflict_ok() { + let lease_collection = Arc::new(LeaseCollection::new(60)); + let mut sp = LeaseSpecPool::new(Arc::clone(&lease_collection)); + + let mut gen = EntryGenerator::default(); + // suppose initially we have a key "foo" associated with lease id 1 + lease_collection.grant(1, 60, true); + lease_collection + .attach(1, "foo".as_bytes().to_vec()) + .unwrap(); + + let kv_put = gen.gen_put("foo"); + let kv_delete = gen.gen_delete_range("foo", "foz"); + let lease_revoke = gen.gen_lease_revoke(1); + + // lease revoke conflicts with put + assert!(sp.insert_if_not_conflict(lease_revoke.clone()).is_none()); + assert!(sp.insert_if_not_conflict(kv_put.clone()).is_some()); + sp.remove(&lease_revoke.clone()); + assert!(sp.insert_if_not_conflict(kv_put.clone()).is_none()); + sp.remove(&kv_put.clone()); + + // lease revoke conflicts with delete range + assert!(sp.insert_if_not_conflict(lease_revoke.clone()).is_none()); + assert!(sp.insert_if_not_conflict(kv_delete.clone()).is_some()); + sp.remove(&lease_revoke.clone()); + assert!(sp.insert_if_not_conflict(kv_delete.clone()).is_none()); + sp.remove(&kv_delete.clone()); +} + +#[test] +fn ucp_kv_then_revoke_conflict_ok() { + let lease_collection = Arc::new(LeaseCollection::new(60)); + let mut ucp = KvUncomPool::new(Arc::clone(&lease_collection)); + + let mut gen = EntryGenerator::default(); + // suppose initially we have a key "foo" associated with lease id 1 + lease_collection.grant(1, 60, true); + lease_collection + .attach(1, "foo".as_bytes().to_vec()) + .unwrap(); + + let kv_put = gen.gen_put("foo"); + let kv_delete = gen.gen_delete_range("foo", "foz"); + let lease_revoke = gen.gen_lease_revoke(1); + + // put conflicts with lease revoke + assert!(!ucp.insert(kv_put.clone())); + assert!(ucp.insert(lease_revoke.clone())); + ucp.remove(&kv_put); + ucp.remove(&lease_revoke.clone()); + assert!(!ucp.insert(lease_revoke.clone())); + ucp.remove(&lease_revoke.clone()); + + // delete range conflicts with lease revoke + assert!(!ucp.insert(kv_delete.clone())); + assert!(ucp.insert(lease_revoke.clone())); + ucp.remove(&kv_delete); + ucp.remove(&lease_revoke.clone()); + assert!(!ucp.insert(lease_revoke.clone())); +} + +#[test] +fn ucp_revoke_then_kv_conflict_ok() { + let lease_collection = Arc::new(LeaseCollection::new(60)); + let mut ucp = LeaseUncomPool::new(Arc::clone(&lease_collection)); + + let mut gen = EntryGenerator::default(); + // suppose initially we have a key "foo" associated with lease id 1 + lease_collection.grant(1, 60, true); + lease_collection + .attach(1, "foo".as_bytes().to_vec()) + .unwrap(); + + let kv_put = gen.gen_put("foo"); + let kv_delete = gen.gen_delete_range("foo", "foz"); + let lease_revoke = gen.gen_lease_revoke(1); + + // lease revoke conflicts with put + assert!(!ucp.insert(lease_revoke.clone())); + assert!(ucp.insert(kv_put.clone())); + ucp.remove(&lease_revoke.clone()); + ucp.remove(&kv_put.clone()); + assert!(!ucp.insert(kv_put.clone())); + ucp.remove(&kv_put.clone()); + + // lease revoke conflicts with delete range + assert!(!ucp.insert(lease_revoke.clone())); + assert!(ucp.insert(kv_delete.clone())); + ucp.remove(&lease_revoke.clone()); + ucp.remove(&kv_delete.clone()); + assert!(!ucp.insert(kv_delete.clone())); +} + +/* +The following test cases verify that insert and remove operations in a +conflict pool are independent of any external state. + +Specifically, we need to query `LeaseCollection` for kv and lease requests +and the `LeaseCollection` might be mutated sometime between a insert and +a remove, potentially leading to an inconsist state in our conflict pool. +*/ + +#[test] +fn kv_sp_mutation_no_side_effect() { + let lease_collection = Arc::new(LeaseCollection::new(60)); + let mut sp = KvSpecPool::new(Arc::clone(&lease_collection)); + let mut gen = EntryGenerator::default(); + + lease_collection.grant(1, 60, true); + lease_collection + .attach(1, "foo".as_bytes().to_vec()) + .unwrap(); + let kv_put = gen.gen_put("foo"); + let lease_revoke = gen.gen_lease_revoke(1); + + sp.insert_if_not_conflict(lease_revoke.clone()); + assert!(sp.insert_if_not_conflict(kv_put.clone()).is_some()); + // Here we detach the lease from the lease collection, + // ensuring that the mutation of `LeaseCollection` + // won't affect the behavior of our conflict pool. + lease_collection.detach(1, "foo".as_bytes()).unwrap(); + sp.remove(&lease_revoke); + assert!(sp.insert_if_not_conflict(kv_put).is_none()); +} + +#[test] +fn lease_sp_mutation_no_side_effect() { + let lease_collection = Arc::new(LeaseCollection::new(60)); + let mut sp = LeaseSpecPool::new(Arc::clone(&lease_collection)); + let mut gen = EntryGenerator::default(); + + lease_collection.grant(1, 60, true); + lease_collection + .attach(1, "foo".as_bytes().to_vec()) + .unwrap(); + let kv_put = gen.gen_put("foo"); + let lease_revoke = gen.gen_lease_revoke(1); + + sp.insert_if_not_conflict(kv_put.clone()); + assert!(sp.insert_if_not_conflict(lease_revoke.clone()).is_some()); + lease_collection.detach(1, "foo".as_bytes()).unwrap(); + sp.remove(&kv_put); + assert!(sp.insert_if_not_conflict(kv_put).is_none()); +} + +#[test] +fn kv_ucp_mutation_no_side_effect() { + let lease_collection = Arc::new(LeaseCollection::new(60)); + let mut ucp = KvUncomPool::new(Arc::clone(&lease_collection)); + let mut gen = EntryGenerator::default(); + + lease_collection.grant(1, 60, true); + lease_collection + .attach(1, "foo".as_bytes().to_vec()) + .unwrap(); + let kv_put = gen.gen_put("foo"); + let lease_revoke = gen.gen_lease_revoke(1); + + ucp.insert(lease_revoke.clone()); + assert!(!ucp.all_conflict(&kv_put).is_empty()); + lease_collection.detach(1, "foo".as_bytes()).unwrap(); + ucp.remove(&lease_revoke); + assert!(ucp.all_conflict(&kv_put).is_empty()); +} + +#[test] +fn lease_ucp_mutation_no_side_effect() { + let lease_collection = Arc::new(LeaseCollection::new(60)); + let mut ucp = LeaseUncomPool::new(Arc::clone(&lease_collection)); + let mut gen = EntryGenerator::default(); + + lease_collection.grant(1, 60, true); + lease_collection + .attach(1, "foo".as_bytes().to_vec()) + .unwrap(); + let kv_put = gen.gen_put("foo"); + let lease_revoke = gen.gen_lease_revoke(1); + + ucp.insert(kv_put.clone()); + assert!(!ucp.all_conflict(&lease_revoke).is_empty()); + lease_collection.detach(1, "foo".as_bytes()).unwrap(); + ucp.remove(&kv_put); + assert!(ucp.all_conflict(&lease_revoke).is_empty()); +} + +fn compare_commands(mut a: Vec>, mut b: Vec>) { a.sort_unstable(); b.sort_unstable(); assert_eq!(a, b); @@ -201,14 +422,14 @@ struct EntryGenerator { } impl EntryGenerator { - fn gen_put(&mut self, key: &str) -> CommandEntry { + fn gen_put(&mut self, key: &str) -> PoolEntry { self.gen_entry(RequestWrapper::PutRequest(PutRequest { key: key.as_bytes().to_vec(), ..Default::default() })) } - fn gen_delete_range(&mut self, key: &str, range_end: &str) -> CommandEntry { + fn gen_delete_range(&mut self, key: &str, range_end: &str) -> PoolEntry { self.gen_entry(RequestWrapper::DeleteRangeRequest(DeleteRangeRequest { key: key.as_bytes().to_vec(), range_end: range_end.as_bytes().to_vec(), @@ -216,32 +437,32 @@ impl EntryGenerator { })) } - fn gen_lease_grant(&mut self, id: i64) -> CommandEntry { + fn gen_lease_grant(&mut self, id: i64) -> PoolEntry { self.gen_entry(RequestWrapper::LeaseGrantRequest(LeaseGrantRequest { id, ..Default::default() })) } - fn gen_lease_revoke(&mut self, id: i64) -> CommandEntry { + fn gen_lease_revoke(&mut self, id: i64) -> PoolEntry { self.gen_entry(RequestWrapper::LeaseRevokeRequest(LeaseRevokeRequest { id, })) } - fn gen_auth_enable(&mut self) -> CommandEntry { + fn gen_auth_enable(&mut self) -> PoolEntry { self.gen_entry(RequestWrapper::AuthEnableRequest(AuthEnableRequest {})) } - fn gen_role_add(&mut self) -> CommandEntry { + fn gen_role_add(&mut self) -> PoolEntry { self.gen_entry(RequestWrapper::AuthRoleAddRequest( AuthRoleAddRequest::default(), )) } - fn gen_entry(&mut self, req: RequestWrapper) -> CommandEntry { + fn gen_entry(&mut self, req: RequestWrapper) -> PoolEntry { self.id += 1; let cmd = Command::new(req); - CommandEntry::new(ProposeId(0, self.id), Arc::new(cmd)) + PoolEntry::new(ProposeId(0, self.id), Arc::new(cmd)) } } diff --git a/crates/xline/src/conflict/uncommitted_pool.rs b/crates/xline/src/conflict/uncommitted_pool.rs index f1058afce..6a0d38830 100644 --- a/crates/xline/src/conflict/uncommitted_pool.rs +++ b/crates/xline/src/conflict/uncommitted_pool.rs @@ -1,40 +1,60 @@ //! An uncommitted pool is used to store unsynced commands. -//! CURP requires that a master will only execute client operations speculatively, -//! if that operation is commutative with every other unsynced operation. +//! CURP requires that a master will only execute client operations +//! speculatively, if that operation is commutative with every other unsynced +//! operation. -use std::collections::{hash_map, HashMap}; +use std::{ + collections::{hash_map, HashMap}, + sync::Arc, +}; -use curp::server::conflict::CommandEntry; -use curp_external_api::conflict::{ConflictPoolOp, UncommittedPoolOp}; +use curp::rpc::PoolEntry; +use curp_external_api::conflict::{ConflictPoolOp, EntryId, UncommittedPoolOp}; use itertools::Itertools; -use utils::interval_map::IntervalMap; -use xlineapi::{ - command::{get_lease_ids, Command}, - interval::BytesAffine, -}; +use utils::interval_map::{Interval, IntervalMap}; +use xlineapi::{command::Command, interval::BytesAffine}; + +use crate::storage::lease_store::LeaseCollection; -use super::{filter_kv, intervals, is_exclusive_cmd}; +use super::{all_leases, intervals, is_exclusive_cmd}; /// Uncommitted pool for KV commands. -#[derive(Debug, Default)] +#[derive(Debug)] +#[cfg_attr(test, derive(Default))] pub(crate) struct KvUncomPool { /// Interval map for keys overlap detection map: IntervalMap, + /// Lease collection + lease_collection: Arc, + /// Id to intervals map + /// + /// NOTE: To avoid potential side-effects from the `LeaseCollection`, we + /// store The lookup results from `LeaseCollection` during entry insert + /// and use these result in entry remove. + intervals: + HashMap<<::Entry as EntryId>::Id, Vec>>, +} + +impl KvUncomPool { + /// Creates a new [`KvUncomPool`]. + pub(crate) fn new(lease_collection: Arc) -> Self { + Self { + map: IntervalMap::new(), + lease_collection, + intervals: HashMap::new(), + } + } } impl ConflictPoolOp for KvUncomPool { - type Entry = CommandEntry; + type Entry = PoolEntry; - fn remove(&mut self, entry: Self::Entry) { - let Some(entry) = filter_kv(entry) else { - return; - }; - let intervals = intervals(&entry); - for interval in intervals { + fn remove(&mut self, entry: &Self::Entry) { + for interval in self.intervals.remove(&entry.id()).into_iter().flatten() { if self .map .get_mut(&interval) - .map_or(false, |m| m.remove_cmd(&entry)) + .map_or(false, |m| m.remove_cmd(entry)) { let _ignore = self.map.remove(&interval); } @@ -64,12 +84,9 @@ impl ConflictPoolOp for KvUncomPool { impl UncommittedPoolOp for KvUncomPool { fn insert(&mut self, entry: Self::Entry) -> bool { - let Some(entry) = filter_kv(entry) else { - return false; - }; - - let intervals = intervals(&entry); - let conflict = intervals.iter().any(|i| self.map.overlap(i)); + let intervals = intervals(&self.lease_collection, &entry); + let _ignore = self.intervals.insert(entry.id(), intervals.clone()); + let conflict = intervals.iter().any(|i| self.map.overlaps(i)); for interval in intervals { let e = self.map.entry(interval).or_insert(Commands::default()); e.push_cmd(entry.clone()); @@ -78,10 +95,7 @@ impl UncommittedPoolOp for KvUncomPool { } fn all_conflict(&self, entry: &Self::Entry) -> Vec { - let Some(entry) = filter_kv(entry) else { - return vec![]; - }; - let intervals = intervals(entry); + let intervals = intervals(&self.lease_collection, entry); intervals .into_iter() .flat_map(|i| self.map.find_all_overlap(&i)) @@ -92,20 +106,39 @@ impl UncommittedPoolOp for KvUncomPool { } /// Lease uncommitted pool -#[derive(Debug, Default)] +#[derive(Debug)] +#[cfg_attr(test, derive(Default))] pub(crate) struct LeaseUncomPool { /// Stores leases in the pool leases: HashMap, + /// Lease collection + lease_collection: Arc, + /// Id to lease ids map + /// + /// NOTE: To avoid potential side-effects from the `LeaseCollection`, we + /// store The lookup results from `LeaseCollection` during entry insert + /// and use these result in entry remove. + ids: HashMap<<::Entry as EntryId>::Id, Vec>, +} + +impl LeaseUncomPool { + /// Creates a new [`LeaseUncomPool`]. + pub(crate) fn new(lease_collection: Arc) -> Self { + Self { + leases: HashMap::new(), + lease_collection, + ids: HashMap::new(), + } + } } impl ConflictPoolOp for LeaseUncomPool { - type Entry = CommandEntry; + type Entry = PoolEntry; - fn remove(&mut self, entry: Self::Entry) { - let ids = get_lease_ids(entry.request()); - for id in ids { + fn remove(&mut self, entry: &Self::Entry) { + for id in self.ids.remove(&entry.id()).into_iter().flatten() { if let hash_map::Entry::Occupied(mut e) = self.leases.entry(id) { - if e.get_mut().remove_cmd(&entry) { + if e.get_mut().remove_cmd(entry) { let _ignore = e.remove_entry(); } } @@ -140,7 +173,8 @@ impl ConflictPoolOp for LeaseUncomPool { impl UncommittedPoolOp for LeaseUncomPool { fn insert(&mut self, entry: Self::Entry) -> bool { let mut conflict = false; - let ids = get_lease_ids(entry.request()); + let ids = all_leases(&self.lease_collection, &entry); + let _ignore = self.ids.insert(entry.id(), ids.clone()); for id in ids { match self.leases.entry(id) { hash_map::Entry::Occupied(mut e) => { @@ -157,7 +191,7 @@ impl UncommittedPoolOp for LeaseUncomPool { } fn all_conflict(&self, entry: &Self::Entry) -> Vec { - let ids = get_lease_ids(entry.request()); + let ids = all_leases(&self.lease_collection, entry); ids.into_iter() .flat_map(|id| self.leases.get(&id).map(Commands::all).unwrap_or_default()) .collect() @@ -172,7 +206,7 @@ pub(crate) struct ExclusiveUncomPool { } impl ConflictPoolOp for ExclusiveUncomPool { - type Entry = CommandEntry; + type Entry = PoolEntry; fn all(&self) -> Vec { self.conflicts.all() @@ -182,9 +216,9 @@ impl ConflictPoolOp for ExclusiveUncomPool { self.conflicts.is_empty() } - fn remove(&mut self, entry: Self::Entry) { - if is_exclusive_cmd(&entry) { - let _ignore = self.conflicts.remove_cmd(&entry); + fn remove(&mut self, entry: &Self::Entry) { + if is_exclusive_cmd(entry) { + let _ignore = self.conflicts.remove_cmd(entry); } } @@ -220,19 +254,19 @@ struct Commands { /// /// As we may need to insert multiple commands with the same /// set of keys, we store a vector of commands as the value. - cmds: Vec>, + cmds: Vec>, } impl Commands { /// Appends a cmd to the value - fn push_cmd(&mut self, cmd: CommandEntry) { + fn push_cmd(&mut self, cmd: PoolEntry) { self.cmds.push(cmd); } /// Removes a cmd from the value /// /// Returns `true` if the value is empty - fn remove_cmd(&mut self, cmd: &CommandEntry) -> bool { + fn remove_cmd(&mut self, cmd: &PoolEntry) -> bool { let Some(idx) = self.cmds.iter().position(|c| c == cmd) else { return self.is_empty(); }; @@ -246,7 +280,7 @@ impl Commands { } /// Gets all commands - fn all(&self) -> Vec> { + fn all(&self) -> Vec> { self.cmds.clone() } diff --git a/crates/xline/src/id_gen.rs b/crates/xline/src/id_gen.rs index a84addc48..980be97e4 100644 --- a/crates/xline/src/id_gen.rs +++ b/crates/xline/src/id_gen.rs @@ -8,6 +8,7 @@ use clippy_utilities::{NumericCast, OverflowArithmetic}; use curp::members::ServerId; /// Generator of unique id +/// /// id format: /// | prefix | suffix | /// | 2 bytes | 5 bytes | 1 byte | diff --git a/crates/xline/src/restore.rs b/crates/xline/src/restore.rs index 2801d5867..dcb6f1818 100644 --- a/crates/xline/src/restore.rs +++ b/crates/xline/src/restore.rs @@ -10,9 +10,11 @@ use utils::table_names::XLINE_TABLES; use crate::server::MAINTENANCE_SNAPSHOT_CHUNK_SIZE; /// Restore snapshot to data dir +/// /// # Errors -/// return `ClientError::IoError` if meet io errors -/// return `ClientError::EngineError` if meet engine errors +/// +/// - return `ClientError::IoError` if meet io errors +/// - return `ClientError::EngineError` if meet engine errors #[inline] #[allow(clippy::indexing_slicing)] // safe operation pub async fn restore, D: Into>( diff --git a/crates/xline/src/revision_number.rs b/crates/xline/src/revision_number.rs index bf6043cd8..fb5e4287f 100644 --- a/crates/xline/src/revision_number.rs +++ b/crates/xline/src/revision_number.rs @@ -2,27 +2,35 @@ use std::sync::atomic::{AtomicI64, Ordering}; /// Revision number #[derive(Debug)] -pub(crate) struct RevisionNumberGenerator(AtomicI64); +pub(crate) struct RevisionNumberGenerator { + /// The current revision number + current: AtomicI64, +} impl RevisionNumberGenerator { /// Create a new revision pub(crate) fn new(rev: i64) -> Self { - Self(AtomicI64::new(rev)) + Self { + current: AtomicI64::new(rev), + } } - /// Get the revision number + /// Get the current revision number pub(crate) fn get(&self) -> i64 { - self.0.load(Ordering::Relaxed) - } - - /// Get the next revision number - pub(crate) fn next(&self) -> i64 { - self.0.fetch_add(1, Ordering::Relaxed).wrapping_add(1) + self.current.load(Ordering::Relaxed) } /// Set the revision number pub(crate) fn set(&self, rev: i64) { - self.0.store(rev, Ordering::Relaxed); + self.current.store(rev, Ordering::Relaxed); + } + + /// Gets a temporary state + pub(crate) fn state(&self) -> RevisionNumberGeneratorState { + RevisionNumberGeneratorState { + current: &self.current, + next: AtomicI64::new(self.get()), + } } } @@ -32,3 +40,29 @@ impl Default for RevisionNumberGenerator { RevisionNumberGenerator::new(1) } } + +/// Revision generator with temporary state +pub(crate) struct RevisionNumberGeneratorState<'a> { + /// The current revision number + current: &'a AtomicI64, + /// Next revision number + next: AtomicI64, +} + +impl RevisionNumberGeneratorState<'_> { + /// Get the current revision number + pub(crate) fn get(&self) -> i64 { + self.next.load(Ordering::Relaxed) + } + + /// Increases the next revision number + pub(crate) fn next(&self) -> i64 { + self.next.fetch_add(1, Ordering::Relaxed).wrapping_add(1) + } + + /// Commit the revision number + pub(crate) fn commit(&self) { + self.current + .store(self.next.load(Ordering::Relaxed), Ordering::Relaxed); + } +} diff --git a/crates/xline/src/server/auth_server.rs b/crates/xline/src/server/auth_server.rs index 33a0949ef..bd285d926 100644 --- a/crates/xline/src/server/auth_server.rs +++ b/crates/xline/src/server/auth_server.rs @@ -51,7 +51,6 @@ impl AuthServer { async fn propose( &self, request: tonic::Request, - use_fast_path: bool, ) -> Result<(CommandResponse, Option), tonic::Status> where T: Into, @@ -59,7 +58,7 @@ impl AuthServer { let auth_info = self.auth_store.try_get_auth_info_from_request(&request)?; let request = request.into_inner().into(); let cmd = Command::new_with_auth_info(request, auth_info); - let res = self.client.propose(&cmd, None, use_fast_path).await??; + let res = self.client.propose(&cmd, None, false).await??; Ok(res) } @@ -67,13 +66,12 @@ impl AuthServer { async fn handle_req( &self, request: tonic::Request, - use_fast_path: bool, ) -> Result, tonic::Status> where Req: Into, Res: From, { - let (cmd_res, sync_res) = self.propose(request, use_fast_path).await?; + let (cmd_res, sync_res) = self.propose(request).await?; let mut res_wrapper = cmd_res.into_inner(); if let Some(sync_res) = sync_res { res_wrapper.update_revision(sync_res.revision()); @@ -89,7 +87,7 @@ impl Auth for AuthServer { request: tonic::Request, ) -> Result, tonic::Status> { debug!("Receive AuthEnableRequest {:?}", request); - self.handle_req(request, false).await + self.handle_req(request).await } async fn auth_disable( @@ -97,7 +95,7 @@ impl Auth for AuthServer { request: tonic::Request, ) -> Result, tonic::Status> { debug!("Receive AuthDisableRequest {:?}", request); - self.handle_req(request, false).await + self.handle_req(request).await } async fn auth_status( @@ -105,8 +103,7 @@ impl Auth for AuthServer { request: tonic::Request, ) -> Result, tonic::Status> { debug!("Receive AuthStatusRequest {:?}", request); - let is_fast_path = true; - self.handle_req(request, is_fast_path).await + self.handle_req(request).await } async fn authenticate( @@ -114,7 +111,7 @@ impl Auth for AuthServer { request: tonic::Request, ) -> Result, tonic::Status> { debug!("Receive AuthenticateRequest {:?}", request); - self.handle_req(request, false).await + self.handle_req(request).await } async fn user_add( @@ -128,7 +125,7 @@ impl Auth for AuthServer { .map_err(|err| tonic::Status::internal(format!("Failed to hash password: {err}")))?; user_add_req.hashed_password = hashed_password; user_add_req.password = String::new(); - self.handle_req(request, false).await + self.handle_req(request).await } async fn user_get( @@ -136,8 +133,7 @@ impl Auth for AuthServer { request: tonic::Request, ) -> Result, tonic::Status> { debug!("Receive AuthUserGetRequest {:?}", request); - let is_fast_path = true; - self.handle_req(request, is_fast_path).await + self.handle_req(request).await } async fn user_list( @@ -145,8 +141,7 @@ impl Auth for AuthServer { request: tonic::Request, ) -> Result, tonic::Status> { debug!("Receive AuthUserListRequest {:?}", request); - let is_fast_path = true; - self.handle_req(request, is_fast_path).await + self.handle_req(request).await } async fn user_delete( @@ -154,7 +149,7 @@ impl Auth for AuthServer { request: tonic::Request, ) -> Result, tonic::Status> { debug!("Receive AuthUserDeleteRequest {:?}", request); - self.handle_req(request, false).await + self.handle_req(request).await } async fn user_change_password( @@ -167,7 +162,7 @@ impl Auth for AuthServer { .map_err(|err| tonic::Status::internal(format!("Failed to hash password: {err}")))?; user_change_password_req.hashed_password = hashed_password; user_change_password_req.password = String::new(); - self.handle_req(request, false).await + self.handle_req(request).await } async fn user_grant_role( @@ -175,7 +170,7 @@ impl Auth for AuthServer { request: tonic::Request, ) -> Result, tonic::Status> { debug!("Receive AuthUserGrantRoleRequest {:?}", request); - self.handle_req(request, false).await + self.handle_req(request).await } async fn user_revoke_role( @@ -183,7 +178,7 @@ impl Auth for AuthServer { request: tonic::Request, ) -> Result, tonic::Status> { debug!("Receive AuthUserRevokeRoleRequest {:?}", request); - self.handle_req(request, false).await + self.handle_req(request).await } async fn role_add( @@ -192,7 +187,7 @@ impl Auth for AuthServer { ) -> Result, tonic::Status> { debug!("Receive AuthRoleAddRequest {:?}", request); request.get_ref().validation()?; - self.handle_req(request, false).await + self.handle_req(request).await } async fn role_get( @@ -200,8 +195,7 @@ impl Auth for AuthServer { request: tonic::Request, ) -> Result, tonic::Status> { debug!("Receive AuthRoleGetRequest {:?}", request); - let is_fast_path = true; - self.handle_req(request, is_fast_path).await + self.handle_req(request).await } async fn role_list( @@ -209,8 +203,7 @@ impl Auth for AuthServer { request: tonic::Request, ) -> Result, tonic::Status> { debug!("Receive AuthRoleListRequest {:?}", request); - let is_fast_path = true; - self.handle_req(request, is_fast_path).await + self.handle_req(request).await } async fn role_delete( @@ -218,7 +211,7 @@ impl Auth for AuthServer { request: tonic::Request, ) -> Result, tonic::Status> { debug!("Receive AuthRoleDeleteRequest {:?}", request); - self.handle_req(request, false).await + self.handle_req(request).await } async fn role_grant_permission( @@ -230,7 +223,7 @@ impl Auth for AuthServer { request.get_ref() ); request.get_ref().validation()?; - self.handle_req(request, false).await + self.handle_req(request).await } async fn role_revoke_permission( @@ -241,6 +234,6 @@ impl Auth for AuthServer { "Receive AuthRoleRevokePermissionRequest {}", request.get_ref() ); - self.handle_req(request, false).await + self.handle_req(request).await } } diff --git a/crates/xline/src/server/auth_wrapper.rs b/crates/xline/src/server/auth_wrapper.rs index 509d57b16..1df9d65d0 100644 --- a/crates/xline/src/server/auth_wrapper.rs +++ b/crates/xline/src/server/auth_wrapper.rs @@ -4,11 +4,13 @@ use curp::{ cmd::PbCodec, rpc::{ FetchClusterRequest, FetchClusterResponse, FetchReadStateRequest, FetchReadStateResponse, - LeaseKeepAliveMsg, MoveLeaderRequest, MoveLeaderResponse, ProposeConfChangeRequest, - ProposeConfChangeResponse, ProposeRequest, ProposeResponse, Protocol, PublishRequest, - PublishResponse, ShutdownRequest, ShutdownResponse, WaitSyncedRequest, WaitSyncedResponse, + LeaseKeepAliveMsg, MoveLeaderRequest, MoveLeaderResponse, OpResponse, + ProposeConfChangeRequest, ProposeConfChangeResponse, ProposeRequest, Protocol, + PublishRequest, PublishResponse, ReadIndexRequest, ReadIndexResponse, RecordRequest, + RecordResponse, ShutdownRequest, ShutdownResponse, }, }; +use flume::r#async::RecvStream; use tracing::debug; use xlineapi::command::Command; @@ -35,10 +37,12 @@ impl AuthWrapper { #[tonic::async_trait] impl Protocol for AuthWrapper { - async fn propose( + type ProposeStreamStream = RecvStream<'static, Result>; + + async fn propose_stream( &self, mut request: tonic::Request, - ) -> Result, tonic::Status> { + ) -> Result, tonic::Status> { debug!( "AuthWrapper received propose request: {}", request.get_ref().propose_id() @@ -51,7 +55,21 @@ impl Protocol for AuthWrapper { command.set_auth_info(auth_info); request.get_mut().command = command.encode(); }; - self.curp_server.propose(request).await + self.curp_server.propose_stream(request).await + } + + async fn record( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + self.curp_server.record(request).await + } + + async fn read_index( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + self.curp_server.read_index(request).await } async fn shutdown( @@ -75,13 +93,6 @@ impl Protocol for AuthWrapper { self.curp_server.publish(request).await } - async fn wait_synced( - &self, - request: tonic::Request, - ) -> Result, tonic::Status> { - self.curp_server.wait_synced(request).await - } - async fn fetch_cluster( &self, request: tonic::Request, diff --git a/crates/xline/src/server/barriers.rs b/crates/xline/src/server/barriers.rs deleted file mode 100644 index 934964e34..000000000 --- a/crates/xline/src/server/barriers.rs +++ /dev/null @@ -1,113 +0,0 @@ -use std::collections::BTreeMap; - -use clippy_utilities::OverflowArithmetic; -use curp::LogIndex; -use event_listener::Event; -use parking_lot::Mutex; - -/// Waiter for index -#[derive(Debug)] -pub(crate) struct IndexBarrier { - /// Inner - inner: Mutex, -} - -impl IndexBarrier { - /// Create a new index barrier - pub(crate) fn new() -> Self { - IndexBarrier { - inner: Mutex::new(IndexBarrierInner { - last_trigger_index: 0, - barriers: BTreeMap::new(), - }), - } - } - - /// Wait for the index until it is triggered. - pub(crate) async fn wait(&self, index: LogIndex) { - let listener = { - let mut inner_l = self.inner.lock(); - if inner_l.last_trigger_index >= index { - return; - } - inner_l.barriers.entry(index).or_default().listen() - }; - listener.await; - } - - /// Trigger all barriers whose index is less than or equal to the given index. - pub(crate) fn trigger(&self, index: LogIndex) { - let mut inner_l = self.inner.lock(); - if inner_l.last_trigger_index < index { - inner_l.last_trigger_index = index; - } - let mut split_barriers = inner_l.barriers.split_off(&(index.overflow_add(1))); - std::mem::swap(&mut inner_l.barriers, &mut split_barriers); - for (_, barrier) in split_barriers { - let _ignore = barrier.notify(usize::MAX); - } - } -} - -/// Inner of index barrier. -#[derive(Debug)] -struct IndexBarrierInner { - /// The last index that the barrier has triggered. - last_trigger_index: LogIndex, - /// Barrier of index. - barriers: BTreeMap, -} - -#[cfg(test)] -mod test { - use std::{sync::Arc, time::Duration}; - - use futures::future::join_all; - use test_macros::abort_on_panic; - use tokio::time::{sleep, timeout}; - use utils::barrier::IdBarrier; - - use super::*; - - #[tokio::test] - #[abort_on_panic] - async fn test_id_barrier() { - let id_barrier = Arc::new(IdBarrier::new()); - let barriers = (0..5) - .map(|i| { - let id_barrier = Arc::clone(&id_barrier); - tokio::spawn(async move { - id_barrier.wait(i).await; - }) - }) - .collect::>(); - sleep(Duration::from_millis(10)).await; - for i in 0..5 { - id_barrier.trigger(&i); - } - timeout(Duration::from_millis(100), join_all(barriers)) - .await - .unwrap(); - } - - #[tokio::test] - #[abort_on_panic] - async fn test_index_barrier() { - let index_barrier = Arc::new(IndexBarrier::new()); - let barriers = (0..5).map(|i| { - let id_barrier = Arc::clone(&index_barrier); - tokio::spawn(async move { - id_barrier.wait(i).await; - }) - }); - index_barrier.trigger(5); - - timeout(Duration::from_millis(100), index_barrier.wait(3)) - .await - .unwrap(); - - timeout(Duration::from_millis(100), join_all(barriers)) - .await - .unwrap(); - } -} diff --git a/crates/xline/src/server/command.rs b/crates/xline/src/server/command.rs index 3bf05ae9e..cd564729d 100644 --- a/crates/xline/src/server/command.rs +++ b/crates/xline/src/server/command.rs @@ -1,29 +1,32 @@ -use std::{fmt::Debug, sync::Arc}; +use std::{fmt::Debug, iter, sync::Arc}; use clippy_utilities::OverflowArithmetic; use curp::{ - cmd::{Command as CurpCommand, CommandExecutor as CurpCommandExecutor}, + cmd::{ + AfterSyncCmd, AfterSyncOk, Command as CurpCommand, CommandExecutor as CurpCommandExecutor, + }, members::ServerId, InflightId, LogIndex, }; use dashmap::DashMap; -use engine::Snapshot; +use engine::{Snapshot, TransactionApi}; use event_listener::Event; use parking_lot::RwLock; use tracing::warn; use utils::{barrier::IdBarrier, table_names::META_TABLE}; use xlineapi::{ - command::{Command, CurpClient}, + command::{Command, CurpClient, SyncResponse}, execute_error::ExecuteError, AlarmAction, AlarmRequest, AlarmType, }; -use super::barriers::IndexBarrier; use crate::{ - revision_number::RevisionNumberGenerator, + revision_number::RevisionNumberGeneratorState, rpc::{RequestBackend, RequestWrapper}, storage::{ db::{WriteOp, DB}, + index::IndexOperate, + storage_api::XlineStorageOps, AlarmStore, AuthStore, KvStore, LeaseStore, }, }; @@ -73,14 +76,8 @@ pub(crate) struct CommandExecutor { alarm_storage: Arc, /// persistent storage db: Arc, - /// Barrier for applied index - index_barrier: Arc, /// Barrier for propose id id_barrier: Arc>, - /// Revision Number generator for KV request and Lease request - general_rev: Arc, - /// Revision Number generator for Auth request - auth_rev: Arc, /// Compact events compact_events: Arc>>, /// Quota checker @@ -225,10 +222,7 @@ impl CommandExecutor { lease_storage: Arc, alarm_storage: Arc, db: Arc, - index_barrier: Arc, id_barrier: Arc>, - general_rev: Arc, - auth_rev: Arc, compact_events: Arc>>, quota: u64, ) -> Self { @@ -240,10 +234,7 @@ impl CommandExecutor { lease_storage, alarm_storage, db, - index_barrier, id_barrier, - general_rev, - auth_rev, compact_events, quota_checker, alarmer, @@ -278,86 +269,260 @@ impl CommandExecutor { _ => Ok(()), } } -} -#[async_trait::async_trait] -impl CurpCommandExecutor for CommandExecutor { - fn prepare( + /// After sync KV commands + fn after_sync_kv( &self, - cmd: &Command, - ) -> Result<::PR, ::Error> { - self.check_alarm(cmd)?; - let wrapper = cmd.request(); - let auth_info = cmd.auth_info(); - self.auth_storage.check_permission(wrapper, auth_info)?; - let revision = match wrapper.backend() { - RequestBackend::Auth => { - if wrapper.skip_auth_revision() { - -1 - } else { - self.auth_rev.next() - } - } - RequestBackend::Kv | RequestBackend::Lease => { - if wrapper.skip_general_revision() { - -1 - } else { - self.general_rev.next() - } + wrapper: &RequestWrapper, + txn_db: &T, + index: &(dyn IndexOperate + Send + Sync), + revision_gen: &RevisionNumberGeneratorState<'_>, + to_execute: bool, + ) -> Result< + ( + ::ASR, + Option<::ER>, + ), + ExecuteError, + > + where + T: XlineStorageOps + TransactionApi, + { + let (asr, er) = + self.kv_storage + .after_sync(wrapper, txn_db, index, revision_gen, to_execute)?; + Ok((asr, er)) + } + + /// After sync other type of commands + fn after_sync_others( + &self, + wrapper: &RequestWrapper, + txn_db: &T, + index: &I, + general_revision: &RevisionNumberGeneratorState<'_>, + auth_revision: &RevisionNumberGeneratorState<'_>, + to_execute: bool, + ) -> Result< + ( + ::ASR, + Option<::ER>, + ), + ExecuteError, + > + where + T: XlineStorageOps + TransactionApi, + I: IndexOperate, + { + let er = to_execute + .then(|| match wrapper.backend() { + RequestBackend::Auth => self.auth_storage.execute(wrapper), + RequestBackend::Lease => self.lease_storage.execute(wrapper), + RequestBackend::Alarm => Ok(self.alarm_storage.execute(wrapper)), + RequestBackend::Kv => unreachable!("Should not execute kv commands"), + }) + .transpose()?; + + let (asr, wr_ops) = match wrapper.backend() { + RequestBackend::Auth => self.auth_storage.after_sync(wrapper, auth_revision)?, + RequestBackend::Lease => { + self.lease_storage + .after_sync(wrapper, general_revision, txn_db, index)? } - RequestBackend::Alarm => -1, + RequestBackend::Alarm => self.alarm_storage.after_sync(wrapper, general_revision), + RequestBackend::Kv => unreachable!("Should not sync kv commands"), }; - Ok(revision) + + txn_db.write_ops(wr_ops)?; + + Ok((asr, er)) + } +} + +/// After Sync Result +type AfterSyncResult = Result, ::Error>; + +/// Collection of after sync results +struct ASResults<'a> { + /// After sync cmds and there execution results + cmd_results: Vec<(AfterSyncCmd<'a, Command>, Option)>, +} + +impl<'a> ASResults<'a> { + /// Creates a new [`ASResultStates`]. + fn new(cmds: Vec>) -> Self { + Self { + // Initially all commands have no results + cmd_results: cmds.into_iter().map(|cmd| (cmd, None)).collect(), + } + } + + #[allow(clippy::pattern_type_mismatch)] // can't be fixed + /// Updates the results of commands that have errors by applying a given + /// operation. + fn update_err(&mut self, op: F) + where + F: Fn(&AfterSyncCmd<'_, Command>) -> Result<(), ExecuteError>, + { + self.for_each_none_result(|(cmd, result_opt)| { + if let Err(e) = op(cmd) { + let _ignore = result_opt.replace(Err(e)); + } + }); + } + + /// Updates the results of commands by applying a given operation. + #[allow(clippy::pattern_type_mismatch)] // can't be fixed + fn update_result(&mut self, op: F) + where + F: Fn(&AfterSyncCmd<'_, Command>) -> AfterSyncResult, + { + self.for_each_none_result(|(cmd, result_opt)| { + let _ignore = result_opt.replace(op(cmd)); + }); } - async fn execute( + /// Applies the provided operation to each command-result pair in `cmd_results` where the result is `None`. + #[allow(clippy::pattern_type_mismatch)] // can't be fixed + fn for_each_none_result(&mut self, op: F) + where + F: FnMut(&mut (AfterSyncCmd<'_, Command>, Option)), + { + self.cmd_results + .iter_mut() + .filter(|(_cmd, res)| res.is_none()) + .for_each(op); + } + + /// Converts into errors. + fn into_errors(self, err: ::Error) -> Vec { + iter::repeat(err) + .map(Err) + .take(self.cmd_results.len()) + .collect() + } + + /// Converts into results. + fn into_results(self) -> Vec { + self.cmd_results + .into_iter() + .filter_map(|(_cmd, res)| res) + .collect() + } +} + +#[async_trait::async_trait] +impl CurpCommandExecutor for CommandExecutor { + fn execute( &self, cmd: &Command, ) -> Result<::ER, ::Error> { + self.check_alarm(cmd)?; + let auth_info = cmd.auth_info(); let wrapper = cmd.request(); + self.auth_storage.check_permission(wrapper, auth_info)?; match wrapper.backend() { - RequestBackend::Kv => self.kv_storage.execute(wrapper), + RequestBackend::Kv => self.kv_storage.execute(wrapper, None), RequestBackend::Auth => self.auth_storage.execute(wrapper), RequestBackend::Lease => self.lease_storage.execute(wrapper), RequestBackend::Alarm => Ok(self.alarm_storage.execute(wrapper)), } } - async fn after_sync( + fn execute_ro( &self, cmd: &Command, - index: LogIndex, - revision: i64, - ) -> Result<::ASR, ::Error> { - let quota_enough = self.quota_checker.check(cmd); - let mut ops = vec![WriteOp::PutAppliedIndex(index)]; + ) -> Result< + (::ER, ::ASR), + ::Error, + > { + let er = self.execute(cmd)?; let wrapper = cmd.request(); - let (res, mut wr_ops) = match wrapper.backend() { - RequestBackend::Kv => self.kv_storage.after_sync(wrapper, revision).await?, - RequestBackend::Auth => self.auth_storage.after_sync(wrapper, revision)?, - RequestBackend::Lease => self.lease_storage.after_sync(wrapper, revision).await?, - RequestBackend::Alarm => self.alarm_storage.after_sync(wrapper, revision), - }; - if let RequestWrapper::CompactionRequest(ref compact_req) = *wrapper { - if compact_req.physical { - if let Some(n) = self.compact_events.get(&cmd.compact_id()) { - let _ignore = n.notify(usize::MAX); - } + let rev = match wrapper.backend() { + RequestBackend::Kv | RequestBackend::Lease | RequestBackend::Alarm => { + self.kv_storage.revision_gen().get() } + RequestBackend::Auth => self.auth_storage.revision_gen().get(), }; - if let RequestWrapper::CompactionRequest(ref compact_req) = *wrapper { - if compact_req.physical { - if let Some(n) = self.compact_events.get(&cmd.compact_id()) { - let _ignore = n.notify(usize::MAX); - } + Ok((er, SyncResponse::new(rev))) + } + + fn after_sync( + &self, + cmds: Vec>, + highest_index: Option, + ) -> Vec { + if cmds.is_empty() { + return Vec::new(); + } + let quota_enough = cmds + .iter() + .map(AfterSyncCmd::cmd) + .all(|c| self.quota_checker.check(c)); + + let mut states = ASResults::new(cmds); + states.update_err(|c| self.check_alarm(c.cmd())); + states.update_err(|c| { + self.auth_storage + .check_permission(c.cmd().request(), c.cmd().auth_info()) + }); + + let index = self.kv_storage.index(); + let index_state = index.state(); + let general_revision_gen = self.kv_storage.revision_gen(); + let auth_revision_gen = self.auth_storage.revision_gen(); + let general_revision_state = general_revision_gen.state(); + let auth_revision_state = auth_revision_gen.state(); + + let txn_db = self.db.transaction(); + if let Some(i) = highest_index { + if let Err(e) = txn_db.write_op(WriteOp::PutAppliedIndex(i)) { + return states.into_errors(e); } - }; - ops.append(&mut wr_ops); - let key_revisions = self.db.flush_ops(ops)?; - if !key_revisions.is_empty() { - self.kv_storage.insert_index(key_revisions); } - self.lease_storage.mark_lease_synced(wrapper); + + states.update_result(|c| { + let (cmd, to_execute) = c.into_parts(); + let wrapper = cmd.request(); + let (asr, er) = match wrapper.backend() { + RequestBackend::Kv => self.after_sync_kv( + wrapper, + &txn_db, + &index_state, + &general_revision_state, + to_execute, + ), + RequestBackend::Auth | RequestBackend::Lease | RequestBackend::Alarm => self + .after_sync_others( + wrapper, + &txn_db, + &index_state, + &general_revision_state, + &auth_revision_state, + to_execute, + ), + }?; + + if let RequestWrapper::CompactionRequest(ref compact_req) = *wrapper { + if compact_req.physical { + if let Some(n) = self.compact_events.get(&cmd.compact_id()) { + let _ignore = n.notify(usize::MAX); + } + } + }; + + self.lease_storage.mark_lease_synced(wrapper); + + Ok(AfterSyncOk::new(asr, er)) + }); + + if let Err(e) = txn_db.commit() { + return states.into_errors(ExecuteError::DbError(e.to_string())); + } + index_state.commit(); + general_revision_state.commit(); + auth_revision_state.commit(); + if !quota_enough { if let Some(alarmer) = self.alarmer.read().clone() { let _ig = tokio::spawn(async move { @@ -370,7 +535,8 @@ impl CurpCommandExecutor for CommandExecutor { }); } } - Ok(res) + + states.into_results() } async fn reset( @@ -378,12 +544,13 @@ impl CurpCommandExecutor for CommandExecutor { snapshot: Option<(Snapshot, LogIndex)>, ) -> Result<(), ::Error> { let s = if let Some((snapshot, index)) = snapshot { - _ = self.db.flush_ops(vec![WriteOp::PutAppliedIndex(index)])?; + self.db.write_ops(vec![WriteOp::PutAppliedIndex(index)])?; Some(snapshot) } else { None }; - self.db.reset(s).await + self.db.reset(s).await?; + self.kv_storage.recover().await } async fn snapshot(&self) -> Result::Error> { @@ -392,7 +559,7 @@ impl CurpCommandExecutor for CommandExecutor { } fn set_last_applied(&self, index: LogIndex) -> Result<(), ::Error> { - _ = self.db.flush_ops(vec![WriteOp::PutAppliedIndex(index)])?; + self.db.write_ops(vec![WriteOp::PutAppliedIndex(index)])?; Ok(()) } @@ -406,9 +573,8 @@ impl CurpCommandExecutor for CommandExecutor { Ok(u64::from_le_bytes(buf)) } - fn trigger(&self, id: InflightId, index: LogIndex) { + fn trigger(&self, id: InflightId) { self.id_barrier.trigger(&id); - self.index_barrier.trigger(index); } } diff --git a/crates/xline/src/server/kv_server.rs b/crates/xline/src/server/kv_server.rs index 0c05da1c3..7e87064f3 100644 --- a/crates/xline/src/server/kv_server.rs +++ b/crates/xline/src/server/kv_server.rs @@ -6,23 +6,18 @@ use std::{ time::Duration, }; -use curp::{rpc::ReadState, InflightId}; use dashmap::DashMap; use event_listener::Event; -use futures::future::{join_all, Either}; +use futures::future::Either; use tokio::time::timeout; use tracing::{debug, instrument}; -use utils::barrier::IdBarrier; use xlineapi::{ - command::{Command, CommandResponse, CurpClient, SyncResponse}, - execute_error::ExecuteError, + command::{Command, CurpClient}, request_validation::RequestValidator, AuthInfo, ResponseWrapper, }; -use super::barriers::IndexBarrier; use crate::{ - metrics, revision_check::RevisionCheck, rpc::{ CompactionRequest, CompactionResponse, DeleteRangeRequest, DeleteRangeResponse, Kv, @@ -38,12 +33,6 @@ pub(crate) struct KvServer { kv_storage: Arc, /// Auth storage auth_storage: Arc, - /// Barrier for applied index - index_barrier: Arc, - /// Barrier for propose id - id_barrier: Arc>, - /// Range request retry timeout - range_retry_timeout: Duration, /// Compact timeout compact_timeout: Duration, /// Consensus client @@ -60,9 +49,6 @@ impl KvServer { pub(crate) fn new( kv_storage: Arc, auth_storage: Arc, - index_barrier: Arc, - id_barrier: Arc>, - range_retry_timeout: Duration, compact_timeout: Duration, client: Arc, compact_events: Arc>>, @@ -70,9 +56,6 @@ impl KvServer { Self { kv_storage, auth_storage, - index_barrier, - id_barrier, - range_retry_timeout, compact_timeout, client, compact_events, @@ -93,7 +76,7 @@ impl KvServer { fn do_serializable(&self, command: &Command) -> Result { self.auth_storage .check_permission(command.request(), command.auth_info())?; - let cmd_res = self.kv_storage.execute(command.request())?; + let cmd_res = self.kv_storage.execute(command.request(), None)?; Ok(Self::parse_response_op(cmd_res.into_inner().into())) } @@ -102,14 +85,19 @@ impl KvServer { &self, request: T, auth_info: Option, - use_fast_path: bool, - ) -> Result<(CommandResponse, Option), tonic::Status> + ) -> Result where T: Into, { let request = request.into(); let cmd = Command::new_with_auth_info(request, auth_info); - let res = self.client.propose(&cmd, None, use_fast_path).await??; + let (cmd_res, sync_res) = self.client.propose(&cmd, None, false).await??; + let revision = sync_res + .unwrap_or_else(|| unreachable!("sync response should always exist in slow path")) + .revision(); + let mut res = Self::parse_response_op(cmd_res.into_inner().into()); + debug!("Get revision {:?}", revision); + Self::update_header_revision(&mut res, revision); Ok(res) } @@ -143,48 +131,6 @@ impl KvServer { } }; } - - /// check whether the required revision is compacted or not - fn check_range_compacted( - range_revision: i64, - compacted_revision: i64, - ) -> Result<(), tonic::Status> { - (range_revision <= 0 || range_revision >= compacted_revision) - .then_some(()) - .ok_or(ExecuteError::RevisionCompacted(range_revision, compacted_revision).into()) - } - - /// Wait current node's state machine apply the conflict commands - async fn wait_read_state(&self, cmd: &Command) -> Result<(), tonic::Status> { - loop { - let rd_state = self.client.fetch_read_state(cmd).await.map_err(|e| { - metrics::get().read_indexes_failed_total.add(1, &[]); - e - })?; - let wait_future = async move { - match rd_state { - ReadState::Ids(id_set) => { - debug!(?id_set, "Range wait for command ids"); - let fus = id_set - .inflight_ids - .into_iter() - .map(|id| self.id_barrier.wait(id)) - .collect::>(); - let _ignore = join_all(fus).await; - } - ReadState::CommitIndex(index) => { - debug!(?index, "Range wait for commit index"); - self.index_barrier.wait(index).await; - } - } - }; - if timeout(self.range_retry_timeout, wait_future).await.is_ok() { - break; - } - metrics::get().slow_read_indexes_total.add(1, &[]); - } - Ok(()) - } } #[tonic::async_trait] @@ -203,22 +149,14 @@ impl Kv for KvServer { self.kv_storage.revision(), )?; let auth_info = self.auth_storage.try_get_auth_info_from_request(&request)?; - let range_required_revision = range_req.revision; let is_serializable = range_req.serializable; - let request = RequestWrapper::from(request.into_inner()); - let cmd = Command::new_with_auth_info(request, auth_info); - if !is_serializable { - self.wait_read_state(&cmd).await?; - // Double check whether the range request is compacted or not since the compaction request - // may be executed during the process of `wait_read_state` which results in the result of - // previous `check_range_request` outdated. - Self::check_range_compacted( - range_required_revision, - self.kv_storage.compacted_revision(), - )?; - } + let res = if is_serializable { + let cmd = Command::new_with_auth_info(request.into_inner().into(), auth_info); + self.do_serializable(&cmd)? + } else { + self.propose(request.into_inner(), auth_info).await? + }; - let res = self.do_serializable(&cmd)?; if let Response::ResponseRange(response) = res { Ok(tonic::Response::new(response)) } else { @@ -227,6 +165,7 @@ impl Kv for KvServer { } /// Put puts the given key into the key-value store. + /// /// A put request increments the revision of the key-value store /// and generates one event in the event history. #[instrument(skip_all)] @@ -236,18 +175,9 @@ impl Kv for KvServer { ) -> Result, tonic::Status> { let put_req: &PutRequest = request.get_ref(); put_req.validation()?; - debug!("Receive grpc request: {}", put_req); + debug!("Receive grpc request: {:?}", put_req); let auth_info = self.auth_storage.try_get_auth_info_from_request(&request)?; - let is_fast_path = true; - let (cmd_res, sync_res) = self - .propose(request.into_inner(), auth_info, is_fast_path) - .await?; - let mut res = Self::parse_response_op(cmd_res.into_inner().into()); - if let Some(sync_res) = sync_res { - let revision = sync_res.revision(); - debug!("Get revision {} for PutRequest", revision); - Self::update_header_revision(&mut res, revision); - } + let res = self.propose(request.into_inner(), auth_info).await?; if let Response::ResponsePut(response) = res { Ok(tonic::Response::new(response)) } else { @@ -256,6 +186,7 @@ impl Kv for KvServer { } /// DeleteRange deletes the given range from the key-value store. + /// /// A delete request increments the revision of the key-value store /// and generates a delete event in the event history for every deleted key. #[instrument(skip_all)] @@ -265,18 +196,9 @@ impl Kv for KvServer { ) -> Result, tonic::Status> { let delete_range_req = request.get_ref(); delete_range_req.validation()?; - debug!("Receive grpc request: {}", delete_range_req); + debug!("Receive grpc request: {:?}", delete_range_req); let auth_info = self.auth_storage.try_get_auth_info_from_request(&request)?; - let is_fast_path = true; - let (cmd_res, sync_res) = self - .propose(request.into_inner(), auth_info, is_fast_path) - .await?; - let mut res = Self::parse_response_op(cmd_res.into_inner().into()); - if let Some(sync_res) = sync_res { - let revision = sync_res.revision(); - debug!("Get revision {} for DeleteRangeRequest", revision); - Self::update_header_revision(&mut res, revision); - } + let res = self.propose(request.into_inner(), auth_info).await?; if let Response::ResponseDeleteRange(response) = res { Ok(tonic::Response::new(response)) } else { @@ -285,6 +207,7 @@ impl Kv for KvServer { } /// Txn processes multiple requests in a single transaction. + /// /// A txn request increments the revision of the key-value store /// and generates events with the same revision for every completed request. /// It is not allowed to modify the same key several times within one txn. @@ -301,28 +224,7 @@ impl Kv for KvServer { self.kv_storage.revision(), )?; let auth_info = self.auth_storage.try_get_auth_info_from_request(&request)?; - let res = if txn_req.is_read_only() { - debug!("TxnRequest is read only"); - let is_serializable = txn_req.is_serializable(); - let request = RequestWrapper::from(request.into_inner()); - let cmd = Command::new_with_auth_info(request, auth_info); - if !is_serializable { - self.wait_read_state(&cmd).await?; - } - self.do_serializable(&cmd)? - } else { - let is_fast_path = true; - let (cmd_res, sync_res) = self - .propose(request.into_inner(), auth_info, is_fast_path) - .await?; - let mut res = Self::parse_response_op(cmd_res.into_inner().into()); - if let Some(sync_res) = sync_res { - let revision = sync_res.revision(); - debug!("Get revision {} for TxnRequest", revision); - Self::update_header_revision(&mut res, revision); - } - res - }; + let res = self.propose(request.into_inner(), auth_info).await?; if let Response::ResponseTxn(response) = res { Ok(tonic::Response::new(response)) } else { @@ -330,9 +232,9 @@ impl Kv for KvServer { } } - /// Compact compacts the event history in the etcd key-value store. The key-value - /// store should be periodically compacted or the event history will continue to grow - /// indefinitely. + /// Compact compacts the event history in the etcd key-value store. The + /// key-value store should be periodically compacted or the event + /// history will continue to grow indefinitely. #[instrument(skip_all)] async fn compact( &self, @@ -356,7 +258,7 @@ impl Kv for KvServer { } else { Either::Right(async {}) }; - let (cmd_res, _sync_res) = self.client.propose(&cmd, None, !physical).await??; + let (cmd_res, _sync_res) = self.client.propose(&cmd, None, false).await??; let resp = cmd_res.into_inner(); if timeout(self.compact_timeout, compact_physical_fut) .await diff --git a/crates/xline/src/server/lease_server.rs b/crates/xline/src/server/lease_server.rs index 03e3d33c8..1dca749f7 100644 --- a/crates/xline/src/server/lease_server.rs +++ b/crates/xline/src/server/lease_server.rs @@ -52,6 +52,10 @@ pub(crate) struct LeaseServer { task_manager: Arc, } +/// A lease keep alive stream +type KeepAliveStream = + Pin> + Send>>; + impl LeaseServer { /// New `LeaseServer` pub(crate) fn new( @@ -119,16 +123,14 @@ impl LeaseServer { async fn propose( &self, request: tonic::Request, - use_fast_path: bool, ) -> Result<(CommandResponse, Option), tonic::Status> where T: Into, { let auth_info = self.auth_storage.try_get_auth_info_from_request(&request)?; let request = request.into_inner().into(); - // FIXME: get the keys in the conflict pools let cmd = Command::new_with_auth_info(request, auth_info); - let res = self.client.propose(&cmd, None, use_fast_path).await??; + let res = self.client.propose(&cmd, None, false).await??; Ok(res) } @@ -137,10 +139,11 @@ impl LeaseServer { fn leader_keep_alive( &self, mut request_stream: tonic::Streaming, - ) -> Pin> + Send>> { + ) -> Result { let shutdown_listener = self .task_manager - .get_shutdown_listener(TaskName::LeaseKeepAlive); + .get_shutdown_listener(TaskName::LeaseKeepAlive) + .ok_or(tonic::Status::cancelled("The cluster is shutting down"))?; let lease_storage = Arc::clone(&self.lease_storage); let stream = try_stream! { loop { @@ -178,7 +181,7 @@ impl LeaseServer { }; } }; - Box::pin(stream) + Ok(Box::pin(stream)) } /// Handle keep alive at follower @@ -187,13 +190,11 @@ impl LeaseServer { &self, mut request_stream: tonic::Streaming, leader_addrs: &[String], - ) -> Result< - Pin> + Send>>, - tonic::Status, - > { + ) -> Result { let shutdown_listener = self .task_manager - .get_shutdown_listener(TaskName::LeaseKeepAlive); + .get_shutdown_listener(TaskName::LeaseKeepAlive) + .ok_or(tonic::Status::cancelled("The cluster is shutting down"))?; let endpoints = build_endpoints(leader_addrs, self.client_tls_config.as_ref())?; let channel = tonic::transport::Channel::balance_list(endpoints.into_iter()); let mut lease_client = LeaseClient::new(channel); @@ -256,8 +257,7 @@ impl Lease for LeaseServer { lease_grant_req.id = self.id_gen.next(); } - let is_fast_path = true; - let (res, sync_res) = self.propose(request, is_fast_path).await?; + let (res, sync_res) = self.propose(request).await?; let mut res: LeaseGrantResponse = res.into_inner().into(); if let Some(sync_res) = sync_res { @@ -277,8 +277,7 @@ impl Lease for LeaseServer { ) -> Result, tonic::Status> { debug!("Receive LeaseRevokeRequest {:?}", request); - let is_fast_path = true; - let (res, sync_res) = self.propose(request, is_fast_path).await?; + let (res, sync_res) = self.propose(request).await?; let mut res: LeaseRevokeResponse = res.into_inner().into(); if let Some(sync_res) = sync_res { @@ -306,7 +305,7 @@ impl Lease for LeaseServer { let request_stream = request.into_inner(); let stream = loop { if self.lease_storage.is_primary() { - break self.leader_keep_alive(request_stream); + break self.leader_keep_alive(request_stream)?; } let leader_id = self.client.fetch_leader_id(false).await?; // Given that a candidate server may become a leader when it won the election or @@ -379,8 +378,7 @@ impl Lease for LeaseServer { ) -> Result, tonic::Status> { debug!("Receive LeaseLeasesRequest {:?}", request); - let is_fast_path = true; - let (res, sync_res) = self.propose(request, is_fast_path).await?; + let (res, sync_res) = self.propose(request).await?; let mut res: LeaseLeasesResponse = res.into_inner().into(); if let Some(sync_res) = sync_res { diff --git a/crates/xline/src/server/lock_server.rs b/crates/xline/src/server/lock_server.rs index f5649cb8c..ac0b39aa2 100644 --- a/crates/xline/src/server/lock_server.rs +++ b/crates/xline/src/server/lock_server.rs @@ -71,14 +71,13 @@ impl LockServer { &self, request: T, auth_info: Option, - use_fast_path: bool, ) -> Result<(CommandResponse, Option), tonic::Status> where T: Into, { let request = request.into(); let cmd = Command::new_with_auth_info(request, auth_info); - let res = self.client.propose(&cmd, None, use_fast_path).await??; + let res = self.client.propose(&cmd, None, false).await??; Ok(res) } @@ -107,7 +106,7 @@ impl LockServer { ..Default::default() })), }; - let range_end = KeyRange::get_prefix(prefix.as_bytes()); + let range_end = KeyRange::get_prefix(prefix); #[allow(clippy::as_conversions)] // this cast is always safe let get_owner = RequestOp { request: Some(Request::RequestRange(RangeRequest { @@ -137,7 +136,7 @@ impl LockServer { let mut watch_client = WatchClient::new(Channel::balance_list(self.addrs.clone().into_iter())); loop { - let range_end = KeyRange::get_prefix(pfx.as_bytes()); + let range_end = KeyRange::get_prefix(&pfx); #[allow(clippy::as_conversions)] // this cast is always safe let get_req = RangeRequest { key: pfx.as_bytes().to_vec(), @@ -148,7 +147,7 @@ impl LockServer { max_create_revision: rev, ..Default::default() }; - let (cmd_res, _sync_res) = self.propose(get_req, auth_info.cloned(), false).await?; + let (cmd_res, _sync_res) = self.propose(get_req, auth_info.cloned()).await?; let response = Into::::into(cmd_res.into_inner()); let last_key = match response.kvs.first() { Some(kv) => kv.key.clone(), @@ -186,7 +185,7 @@ impl LockServer { key: key.into(), ..Default::default() }; - let (cmd_res, _) = self.propose(del_req, auth_info, true).await?; + let (cmd_res, _) = self.propose(del_req, auth_info).await?; let res = Into::::into(cmd_res.into_inner()); Ok(res.header) } @@ -198,7 +197,7 @@ impl LockServer { ttl: DEFAULT_SESSION_TTL, id: lease_id, }; - let (cmd_res, _) = self.propose(lease_grant_req, auth_info, true).await?; + let (cmd_res, _) = self.propose(lease_grant_req, auth_info).await?; let res = Into::::into(cmd_res.into_inner()); Ok(res.id) } @@ -229,7 +228,7 @@ impl Lock for LockServer { let key = format!("{prefix}{lease_id:x}"); let txn = Self::create_acquire_txn(&prefix, lease_id); - let (cmd_res, sync_res) = self.propose(txn, auth_info.clone(), false).await?; + let (cmd_res, sync_res) = self.propose(txn, auth_info.clone()).await?; let mut txn_res = Into::::into(cmd_res.into_inner()); #[allow(clippy::unwrap_used)] // sync_res always has value when use slow path let my_rev = sync_res.unwrap().revision(); @@ -261,7 +260,7 @@ impl Lock for LockServer { key: key.as_bytes().to_vec(), ..Default::default() }; - let result = self.propose(range_req, auth_info.clone(), true).await; + let result = self.propose(range_req, auth_info.clone()).await; match result { Ok(res) => { let res = Into::::into(res.0.into_inner()); diff --git a/crates/xline/src/server/maintenance.rs b/crates/xline/src/server/maintenance.rs index e8bc522c1..9ecf80209 100644 --- a/crates/xline/src/server/maintenance.rs +++ b/crates/xline/src/server/maintenance.rs @@ -84,7 +84,6 @@ impl MaintenanceServer { async fn propose( &self, request: tonic::Request, - use_fast_path: bool, ) -> Result<(CommandResponse, Option), tonic::Status> where T: Into + Debug, @@ -92,7 +91,7 @@ impl MaintenanceServer { let auth_info = self.auth_store.try_get_auth_info_from_request(&request)?; let request = request.into_inner().into(); let cmd = Command::new_with_auth_info(request, auth_info); - let res = self.client.propose(&cmd, None, use_fast_path).await??; + let res = self.client.propose(&cmd, None, false).await??; Ok(res) } } @@ -103,8 +102,7 @@ impl Maintenance for MaintenanceServer { &self, request: tonic::Request, ) -> Result, tonic::Status> { - let is_fast_path = true; - let (res, sync_res) = self.propose(request, is_fast_path).await?; + let (res, sync_res) = self.propose(request).await?; let mut res: AlarmResponse = res.into_inner().into(); if let Some(sync_res) = sync_res { let revision = sync_res.revision(); @@ -254,7 +252,7 @@ fn snapshot_stream( } checksum_gen.update(&buf); yield SnapshotResponse { - header: Some(header.clone()), + header: Some(header), remaining_bytes: remain_size, blob: Vec::from(buf) }; diff --git a/crates/xline/src/server/mod.rs b/crates/xline/src/server/mod.rs index f6c88947c..b2d0feefa 100644 --- a/crates/xline/src/server/mod.rs +++ b/crates/xline/src/server/mod.rs @@ -2,8 +2,6 @@ mod auth_server; /// Auth Wrapper mod auth_wrapper; -/// Barriers for range requests -mod barriers; /// Cluster server mod cluster_server; /// Command to be executed diff --git a/crates/xline/src/server/watch_server.rs b/crates/xline/src/server/watch_server.rs index 6b047ce97..29f67cf74 100644 --- a/crates/xline/src/server/watch_server.rs +++ b/crates/xline/src/server/watch_server.rs @@ -417,6 +417,7 @@ mod test { time::Duration, }; + use engine::TransactionApi; use parking_lot::Mutex; use test_macros::abort_on_panic; use tokio::{ @@ -443,21 +444,24 @@ mod test { && wr.header.as_ref().map_or(false, |h| h.revision != 0) } - async fn put( - store: &KvStore, - db: &DB, - key: impl Into>, - value: impl Into>, - revision: i64, - ) { + fn put(store: &KvStore, key: impl Into>, value: impl Into>) { let req = RequestWrapper::from(PutRequest { key: key.into(), value: value.into(), ..Default::default() }); - let (_sync_res, ops) = store.after_sync(&req, revision).await.unwrap(); - let key_revisions = db.flush_ops(ops).unwrap(); - store.insert_index(key_revisions); + + let rev_gen = store.revision_gen(); + let index = store.index(); + let txn = store.db().transaction(); + let rev_state = rev_gen.state(); + let index_state = index.state(); + store + .after_sync(&req, &txn, &index_state, &rev_state, false) + .unwrap(); + txn.commit().unwrap(); + index_state.commit(); + rev_state.commit(); } #[tokio::test] @@ -477,7 +481,9 @@ mod test { .return_const(-1_i64); let watcher = Arc::new(mock_watcher); let next_id = Arc::new(WatchIdGenerator::new(1)); - let n = task_manager.get_shutdown_listener(TaskName::WatchTask); + let n = task_manager + .get_shutdown_listener(TaskName::WatchTask) + .unwrap(); let handle = tokio::spawn(WatchServer::task( next_id, Arc::clone(&watcher), @@ -581,13 +587,13 @@ mod test { #[abort_on_panic] async fn test_watch_prev_kv() { let task_manager = Arc::new(TaskManager::new()); - let (compact_tx, _compact_rx) = mpsc::channel(COMPACT_CHANNEL_SIZE); + let (compact_tx, _compact_rx) = flume::bounded(COMPACT_CHANNEL_SIZE); let index = Arc::new(Index::new()); let db = DB::open(&EngineConfig::Memory).unwrap(); let header_gen = Arc::new(HeaderGenerator::new(0, 0)); let lease_collection = Arc::new(LeaseCollection::new(0)); let next_id_gen = Arc::new(WatchIdGenerator::new(1)); - let (kv_update_tx, kv_update_rx) = mpsc::channel(CHANNEL_SIZE); + let (kv_update_tx, kv_update_rx) = flume::bounded(CHANNEL_SIZE); let kv_store_inner = Arc::new(KvStoreInner::new(index, Arc::clone(&db))); let kv_store = Arc::new(KvStore::new( Arc::clone(&kv_store_inner), @@ -602,8 +608,8 @@ mod test { Duration::from_millis(10), &task_manager, ); - put(&kv_store, &db, "foo", "old_bar", 2).await; - put(&kv_store, &db, "foo", "bar", 3).await; + put(&kv_store, "foo", "old_bar"); + put(&kv_store, "foo", "bar"); let (req_tx, req_rx) = mpsc::channel(CHANNEL_SIZE); let req_stream = ReceiverStream::new(req_rx); @@ -729,7 +735,9 @@ mod test { .return_const(-1_i64); let watcher = Arc::new(mock_watcher); let next_id = Arc::new(WatchIdGenerator::new(1)); - let n = task_manager.get_shutdown_listener(TaskName::WatchTask); + let n = task_manager + .get_shutdown_listener(TaskName::WatchTask) + .unwrap(); let handle = tokio::spawn(WatchServer::task( next_id, Arc::clone(&watcher), @@ -767,13 +775,13 @@ mod test { #[tokio::test] async fn watch_compacted_revision_should_fail() { let task_manager = Arc::new(TaskManager::new()); - let (compact_tx, _compact_rx) = mpsc::channel(COMPACT_CHANNEL_SIZE); + let (compact_tx, _compact_rx) = flume::bounded(COMPACT_CHANNEL_SIZE); let index = Arc::new(Index::new()); let db = DB::open(&EngineConfig::Memory).unwrap(); let header_gen = Arc::new(HeaderGenerator::new(0, 0)); let lease_collection = Arc::new(LeaseCollection::new(0)); let next_id_gen = Arc::new(WatchIdGenerator::new(1)); - let (kv_update_tx, kv_update_rx) = mpsc::channel(CHANNEL_SIZE); + let (kv_update_tx, kv_update_rx) = flume::bounded(CHANNEL_SIZE); let kv_store_inner = Arc::new(KvStoreInner::new(index, Arc::clone(&db))); let kv_store = Arc::new(KvStore::new( Arc::clone(&kv_store_inner), @@ -788,9 +796,9 @@ mod test { Duration::from_millis(10), &task_manager, ); - put(&kv_store, &db, "foo", "old_bar", 2).await; - put(&kv_store, &db, "foo", "bar", 3).await; - put(&kv_store, &db, "foo", "new_bar", 4).await; + put(&kv_store, "foo", "old_bar"); + put(&kv_store, "foo", "bar"); + put(&kv_store, "foo", "new_bar"); kv_store.update_compacted_revision(3); diff --git a/crates/xline/src/server/xline_server.rs b/crates/xline/src/server/xline_server.rs index 08a68d157..73a8a4ac6 100644 --- a/crates/xline/src/server/xline_server.rs +++ b/crates/xline/src/server/xline_server.rs @@ -13,9 +13,9 @@ use engine::{MemorySnapshotAllocator, RocksSnapshotAllocator, SnapshotAllocator} #[cfg(not(madsim))] use futures::Stream; use jsonwebtoken::{DecodingKey, EncodingKey}; +use tokio::fs; #[cfg(not(madsim))] use tokio::io::{AsyncRead, AsyncWrite}; -use tokio::{fs, sync::mpsc::channel}; #[cfg(not(madsim))] use tonic::transport::{ server::Connected, Certificate, ClientTlsConfig, Identity, ServerTlsConfig, @@ -37,7 +37,6 @@ use xlineapi::command::{Command, CurpClient}; use super::{ auth_server::AuthServer, auth_wrapper::AuthWrapper, - barriers::IndexBarrier, cluster_server::ClusterServer, command::{Alarmer, CommandExecutor}, kv_server::KvServer, @@ -69,7 +68,7 @@ use crate::{ }; /// Rpc Server of curp protocol -pub(crate) type CurpServer = Rpc>>; +pub(crate) type CurpServer = Rpc>>; /// Xline server #[derive(Debug)] @@ -97,7 +96,9 @@ pub struct XlineServer { impl XlineServer { /// New `XlineServer` + /// /// # Errors + /// /// Return error if init cluster info failed #[inline] pub async fn new( @@ -196,7 +197,8 @@ impl XlineServer { Arc::new(LeaseCollection::new(min_ttl_secs.numeric_cast())) } - /// Construct underlying storages, including `KvStore`, `LeaseStore`, `AuthStore` + /// Construct underlying storages, including `KvStore`, `LeaseStore`, + /// `AuthStore` #[allow(clippy::type_complexity)] // it is easy to read #[inline] async fn construct_underlying_storages( @@ -212,9 +214,9 @@ impl XlineServer { Arc, Arc, )> { - let (compact_task_tx, compact_task_rx) = channel(COMPACT_CHANNEL_SIZE); + let (compact_task_tx, compact_task_rx) = flume::bounded(COMPACT_CHANNEL_SIZE); let index = Arc::new(Index::new()); - let (kv_update_tx, kv_update_rx) = channel(CHANNEL_SIZE); + let (kv_update_tx, kv_update_rx) = flume::bounded(CHANNEL_SIZE); let kv_store_inner = Arc::new(KvStoreInner::new(Arc::clone(&index), Arc::clone(&db))); let kv_storage = Arc::new(KvStore::new( Arc::clone(&kv_store_inner), @@ -226,7 +228,7 @@ impl XlineServer { self.task_manager.spawn(TaskName::CompactBg, |n| { compact_bg_task( Arc::clone(&kv_storage), - Arc::clone(&index), + index, *self.compact_config.compact_batch_size(), *self.compact_config.compact_sleep_interval(), compact_task_rx, @@ -237,7 +239,6 @@ impl XlineServer { Arc::clone(&lease_collection), Arc::clone(&header_gen), Arc::clone(&db), - index, kv_update_tx, *self.cluster_config.is_leader(), )); @@ -346,7 +347,8 @@ impl XlineServer { ) -> Result>> { let n1 = self .task_manager - .get_shutdown_listener(TaskName::TonicServer); + .get_shutdown_listener(TaskName::TonicServer) + .unwrap_or_else(|| unreachable!("cluster should never shutdown before start")); let n2 = n1.clone(); let db = DB::open(&self.storage_config.engine)?; let key_pair = Self::read_key_pair(&self.auth_config).await?; @@ -425,8 +427,8 @@ impl XlineServer { self.start_inner(xline_incoming, curp_incoming).await } - /// Init `KvServer`, `LockServer`, `LeaseServer`, `WatchServer` and `CurpServer` - /// for the Xline Server. + /// Init `KvServer`, `LockServer`, `LeaseServer`, `WatchServer` and + /// `CurpServer` for the Xline Server. #[allow( clippy::type_complexity, // it is easy to read clippy::too_many_lines, // TODO: split this into multiple functions @@ -458,13 +460,12 @@ impl XlineServer { let (kv_storage, lease_storage, auth_storage, alarm_storage, watcher) = self .construct_underlying_storages( Arc::clone(&db), - lease_collection, + Arc::clone(&lease_collection), Arc::clone(&header_gen), key_pair, ) .await?; - let index_barrier = Arc::new(IndexBarrier::new()); let id_barrier = Arc::new(IdBarrier::new()); let compact_events = Arc::new(DashMap::new()); let ce = Arc::new(CommandExecutor::new( @@ -473,10 +474,7 @@ impl XlineServer { Arc::clone(&lease_storage), Arc::clone(&alarm_storage), Arc::clone(&db), - Arc::clone(&index_barrier), Arc::clone(&id_barrier), - header_gen.general_revision_arc(), - header_gen.auth_revision_arc(), Arc::clone(&compact_events), self.storage_config.quota, )); @@ -518,8 +516,8 @@ impl XlineServer { Arc::clone(&self.curp_storage), Arc::clone(&self.task_manager), self.client_tls_config.clone(), - XlineSpeculativePools::default().into_inner(), - XlineUncommittedPools::default().into_inner(), + XlineSpeculativePools::new(Arc::clone(&lease_collection)).into_inner(), + XlineUncommittedPools::new(lease_collection).into_inner(), ) .await; @@ -549,9 +547,6 @@ impl XlineServer { KvServer::new( Arc::clone(&kv_storage), Arc::clone(&auth_storage), - index_barrier, - id_barrier, - *server_timeout.range_retry_timeout(), *server_timeout.compact_timeout(), Arc::clone(&client), compact_events, @@ -696,7 +691,7 @@ impl XlineServer { #[cfg(not(madsim))] fn bind_addrs( addrs: &[String], -) -> Result>> { +) -> Result>> { use std::net::ToSocketAddrs; if addrs.is_empty() { return Err(anyhow!("No address to bind")); diff --git a/crates/xline/src/storage/alarm_store.rs b/crates/xline/src/storage/alarm_store.rs index 98cdc6ad7..7483f249b 100644 --- a/crates/xline/src/storage/alarm_store.rs +++ b/crates/xline/src/storage/alarm_store.rs @@ -19,7 +19,7 @@ use xlineapi::{ }; use super::db::{WriteOp, DB}; -use crate::header_gen::HeaderGenerator; +use crate::{header_gen::HeaderGenerator, revision_number::RevisionNumberGeneratorState}; /// Alarm store #[derive(Debug)] @@ -64,7 +64,7 @@ impl AlarmStore { pub(crate) fn after_sync( &self, request: &RequestWrapper, - revision: i64, + revision_gen: &RevisionNumberGeneratorState<'_>, ) -> (SyncResponse, Vec) { #[allow(clippy::wildcard_enum_match_arm)] let ops = match *request { @@ -77,7 +77,7 @@ impl AlarmStore { unreachable!("Other request should not be sent to this store"); } }; - (SyncResponse::new(revision), ops) + (SyncResponse::new(revision_gen.get()), ops) } /// Recover data form persistent storage @@ -160,10 +160,10 @@ impl AlarmStore { fn handle_alarm_get(&self, alarm: AlarmType) -> Vec { let types = self.types.read(); match alarm { - AlarmType::None => types.values().flat_map(HashMap::values).cloned().collect(), + AlarmType::None => types.values().flat_map(HashMap::values).copied().collect(), a @ (AlarmType::Nospace | AlarmType::Corrupt) => types .get(&a) - .map(|s| s.values().cloned().collect()) + .map(|s| s.values().copied().collect()) .unwrap_or_default(), } } @@ -175,7 +175,7 @@ impl AlarmStore { .read() .get(&alarm) .and_then(|e| e.get(&member_id)) - .map_or_else(|| vec![new_alarm], |m| vec![m.clone()]) + .map_or_else(|| vec![new_alarm], |m| vec![*m]) } /// Handle alarm deactivate request @@ -184,7 +184,7 @@ impl AlarmStore { .read() .get(&alarm) .and_then(|e| e.get(&member_id)) - .map(|m| vec![m.clone()]) + .map(|m| vec![*m]) .unwrap_or_default() } @@ -195,7 +195,7 @@ impl AlarmStore { let e = types_w.entry(alarm).or_default(); let mut ops = vec![]; if e.get(&member_id).is_none() { - _ = e.insert(new_alarm.member_id, new_alarm.clone()); + _ = e.insert(new_alarm.member_id, new_alarm); ops.push(WriteOp::PutAlarm(new_alarm)); } self.refresh_current_alarm(&types_w); diff --git a/crates/xline/src/storage/auth_store/backend.rs b/crates/xline/src/storage/auth_store/backend.rs index 1cd3810d9..8d9ef26a2 100644 --- a/crates/xline/src/storage/auth_store/backend.rs +++ b/crates/xline/src/storage/auth_store/backend.rs @@ -6,7 +6,7 @@ use xlineapi::execute_error::ExecuteError; use crate::{ rpc::{Role, User}, - storage::db::DB, + storage::{db::DB, storage_api::XlineStorageOps}, }; /// Key of `AuthEnable` @@ -117,7 +117,7 @@ impl AuthStoreBackend { &self, ops: Vec, ) -> Result<(), ExecuteError> { - _ = self.db.flush_ops(ops)?; + self.db.write_ops(ops)?; Ok(()) } } diff --git a/crates/xline/src/storage/auth_store/store.rs b/crates/xline/src/storage/auth_store/store.rs index 771b7c8b6..66fd776ce 100644 --- a/crates/xline/src/storage/auth_store/store.rs +++ b/crates/xline/src/storage/auth_store/store.rs @@ -29,7 +29,7 @@ use super::{ }; use crate::{ header_gen::HeaderGenerator, - revision_number::RevisionNumberGenerator, + revision_number::{RevisionNumberGenerator, RevisionNumberGeneratorState}, rpc::{ AuthDisableRequest, AuthDisableResponse, AuthEnableRequest, AuthEnableResponse, AuthRoleAddRequest, AuthRoleAddResponse, AuthRoleDeleteRequest, AuthRoleDeleteResponse, @@ -193,13 +193,13 @@ impl AuthStore { ) -> Result { #[allow(clippy::wildcard_enum_match_arm)] let res = match *request { - RequestWrapper::AuthEnableRequest(ref req) => { + RequestWrapper::AuthEnableRequest(req) => { self.handle_auth_enable_request(req).map(Into::into) } - RequestWrapper::AuthDisableRequest(ref req) => { + RequestWrapper::AuthDisableRequest(req) => { Ok(self.handle_auth_disable_request(req).into()) } - RequestWrapper::AuthStatusRequest(ref req) => { + RequestWrapper::AuthStatusRequest(req) => { Ok(self.handle_auth_status_request(req).into()) } RequestWrapper::AuthUserAddRequest(ref req) => { @@ -208,7 +208,7 @@ impl AuthStore { RequestWrapper::AuthUserGetRequest(ref req) => { self.handle_user_get_request(req).map(Into::into) } - RequestWrapper::AuthUserListRequest(ref req) => { + RequestWrapper::AuthUserListRequest(req) => { self.handle_user_list_request(req).map(Into::into) } RequestWrapper::AuthUserGrantRoleRequest(ref req) => { @@ -238,7 +238,7 @@ impl AuthStore { RequestWrapper::AuthRoleDeleteRequest(ref req) => { self.handle_role_delete_request(req).map(Into::into) } - RequestWrapper::AuthRoleListRequest(ref req) => { + RequestWrapper::AuthRoleListRequest(req) => { self.handle_role_list_request(req).map(Into::into) } RequestWrapper::AuthenticateRequest(ref req) => { @@ -254,7 +254,7 @@ impl AuthStore { /// Handle `AuthEnableRequest` fn handle_auth_enable_request( &self, - _req: &AuthEnableRequest, + _req: AuthEnableRequest, ) -> Result { debug!("handle_auth_enable"); let res = Ok(AuthEnableResponse { @@ -272,7 +272,7 @@ impl AuthStore { } /// Handle `AuthDisableRequest` - fn handle_auth_disable_request(&self, _req: &AuthDisableRequest) -> AuthDisableResponse { + fn handle_auth_disable_request(&self, _req: AuthDisableRequest) -> AuthDisableResponse { debug!("handle_auth_disable"); if !self.is_enabled() { debug!("auth is already disabled"); @@ -283,7 +283,7 @@ impl AuthStore { } /// Handle `AuthStatusRequest` - fn handle_auth_status_request(&self, _req: &AuthStatusRequest) -> AuthStatusResponse { + fn handle_auth_status_request(&self, _req: AuthStatusRequest) -> AuthStatusResponse { debug!("handle_auth_status"); AuthStatusResponse { header: Some(self.header_gen.gen_auth_header()), @@ -339,7 +339,7 @@ impl AuthStore { /// Handle `AuthUserListRequest` fn handle_user_list_request( &self, - _req: &AuthUserListRequest, + _req: AuthUserListRequest, ) -> Result { debug!("handle_user_list_request"); let users = self @@ -458,7 +458,7 @@ impl AuthStore { /// Handle `AuthRoleListRequest` fn handle_role_list_request( &self, - _req: &AuthRoleListRequest, + _req: AuthRoleListRequest, ) -> Result { debug!("handle_role_list_request"); let roles = self @@ -527,8 +527,13 @@ impl AuthStore { pub(crate) fn after_sync<'a>( &self, request: &'a RequestWrapper, - revision: i64, + revision_gen: &RevisionNumberGeneratorState, ) -> Result<(SyncResponse, Vec>), ExecuteError> { + let revision = if request.skip_auth_revision() { + revision_gen.get() + } else { + revision_gen.next() + }; #[allow(clippy::wildcard_enum_match_arm)] let ops = match *request { RequestWrapper::AuthEnableRequest(ref req) => { @@ -641,7 +646,7 @@ impl AuthStore { let user = User { name: req.name.as_str().into(), password: req.hashed_password.as_str().into(), - options: req.options.clone(), + options: req.options, roles: Vec::new(), }; ops.push(WriteOp::PutAuthRevision(revision)); @@ -969,7 +974,7 @@ impl AuthStore { self.check_txn_permission(username, txn_req)?; } RequestWrapper::LeaseRevokeRequest(ref lease_revoke_req) => { - self.check_lease_revoke_permission(username, lease_revoke_req)?; + self.check_lease_revoke_permission(username, *lease_revoke_req)?; } RequestWrapper::AuthUserGetRequest(ref user_get_req) => { self.check_admin_permission(username).map_or_else( @@ -1073,7 +1078,7 @@ impl AuthStore { fn check_lease_revoke_permission( &self, username: &str, - req: &LeaseRevokeRequest, + req: LeaseRevokeRequest, ) -> Result<(), ExecuteError> { self.check_lease(username, req.id) } @@ -1151,6 +1156,11 @@ impl AuthStore { self.create_permission_cache()?; Ok(()) } + + /// Gets the auth revision generator + pub(crate) fn revision_gen(&self) -> Arc { + Arc::clone(&self.revision) + } } /// Get common name from tonic request @@ -1205,7 +1215,7 @@ mod test { range_end: "foz".into(), }), }); - assert!(exe_and_sync(&store, &req, 6).is_ok()); + assert!(exe_and_sync(&store, &req).is_ok()); assert_eq!( store.permission_cache(), PermissionCache { @@ -1234,7 +1244,7 @@ mod test { key: "foo".into(), range_end: "".into(), }); - assert!(exe_and_sync(&store, &req, 6).is_ok()); + assert!(exe_and_sync(&store, &req).is_ok()); assert_eq!( store.permission_cache(), PermissionCache { @@ -1252,7 +1262,7 @@ mod test { let req = RequestWrapper::from(AuthRoleDeleteRequest { role: "r".to_owned(), }); - assert!(exe_and_sync(&store, &req, 6).is_ok()); + assert!(exe_and_sync(&store, &req).is_ok()); assert_eq!( store.permission_cache(), PermissionCache { @@ -1270,7 +1280,7 @@ mod test { let req = RequestWrapper::from(AuthUserDeleteRequest { name: "u".to_owned(), }); - assert!(exe_and_sync(&store, &req, 6).is_ok()); + assert!(exe_and_sync(&store, &req).is_ok()); assert_eq!( store.permission_cache(), PermissionCache { @@ -1286,39 +1296,39 @@ mod test { let db = DB::open(&EngineConfig::Memory).unwrap(); let store = init_auth_store(db); let revision = store.revision(); - let rev_gen = Arc::clone(&store.revision); assert!(!store.is_enabled()); let enable_req = RequestWrapper::from(AuthEnableRequest {}); // AuthEnableRequest won't increase the auth revision, but AuthDisableRequest will - assert!(exe_and_sync(&store, &enable_req, store.revision()).is_err()); + assert!(exe_and_sync(&store, &enable_req).is_err()); let req_1 = RequestWrapper::from(AuthUserAddRequest { name: "root".to_owned(), password: String::new(), hashed_password: "123".to_owned(), options: None, }); - assert!(exe_and_sync(&store, &req_1, rev_gen.next()).is_ok()); + assert!(exe_and_sync(&store, &req_1).is_ok()); let req_2 = RequestWrapper::from(AuthRoleAddRequest { name: "root".to_owned(), }); - assert!(exe_and_sync(&store, &req_2, rev_gen.next()).is_ok()); + assert!(exe_and_sync(&store, &req_2).is_ok()); let req_3 = RequestWrapper::from(AuthUserGrantRoleRequest { user: "root".to_owned(), role: "root".to_owned(), }); - assert!(exe_and_sync(&store, &req_3, rev_gen.next()).is_ok()); + assert!(exe_and_sync(&store, &req_3).is_ok()); + assert_eq!(store.revision(), revision + 3); - assert!(exe_and_sync(&store, &enable_req, -1).is_ok()); + assert!(exe_and_sync(&store, &enable_req).is_ok()); assert_eq!(store.revision(), 8); assert!(store.is_enabled()); let disable_req = RequestWrapper::from(AuthDisableRequest {}); - assert!(exe_and_sync(&store, &disable_req, rev_gen.next()).is_ok()); + assert!(exe_and_sync(&store, &disable_req).is_ok()); assert_eq!(store.revision(), revision + 4); assert!(!store.is_enabled()); } @@ -1339,33 +1349,33 @@ mod test { fn init_auth_store(db: Arc) -> AuthStore { let store = init_empty_store(db); - let rev = Arc::clone(&store.revision); let req1 = RequestWrapper::from(AuthRoleAddRequest { name: "r".to_owned(), }); - assert!(exe_and_sync(&store, &req1, rev.next()).is_ok()); + assert!(exe_and_sync(&store, &req1).is_ok()); let req2 = RequestWrapper::from(AuthUserAddRequest { name: "u".to_owned(), password: String::new(), hashed_password: "123".to_owned(), options: None, }); - assert!(exe_and_sync(&store, &req2, rev.next()).is_ok()); + assert!(exe_and_sync(&store, &req2).is_ok()); let req3 = RequestWrapper::from(AuthUserGrantRoleRequest { user: "u".to_owned(), role: "r".to_owned(), }); - assert!(exe_and_sync(&store, &req3, rev.next()).is_ok()); + assert!(exe_and_sync(&store, &req3).is_ok()); let req4 = RequestWrapper::from(AuthRoleGrantPermissionRequest { name: "r".to_owned(), perm: Some(Permission { + #[allow(clippy::as_conversions)] // This cast is always valid perm_type: Type::Readwrite as i32, key: b"foo".to_vec(), range_end: vec![], }), }); - assert!(exe_and_sync(&store, &req4, rev.next()).is_ok()); + assert!(exe_and_sync(&store, &req4).is_ok()); assert_eq!( store.permission_cache(), PermissionCache { @@ -1392,10 +1402,12 @@ mod test { fn exe_and_sync( store: &AuthStore, req: &RequestWrapper, - revision: i64, ) -> Result<(CommandResponse, SyncResponse), ExecuteError> { let cmd_res = store.execute(req)?; - let (sync_res, ops) = store.after_sync(req, revision)?; + let rev_gen = store.revision_gen(); + let rev_gen_state = rev_gen.state(); + let (sync_res, ops) = store.after_sync(req, &rev_gen_state)?; + rev_gen_state.commit(); store.backend.flush_ops(ops)?; Ok((cmd_res, sync_res)) } diff --git a/crates/xline/src/storage/compact/mod.rs b/crates/xline/src/storage/compact/mod.rs index ec0b063a8..7768667e5 100644 --- a/crates/xline/src/storage/compact/mod.rs +++ b/crates/xline/src/storage/compact/mod.rs @@ -5,17 +5,14 @@ use curp::client::ClientApi; use event_listener::Event; use periodic_compactor::PeriodicCompactor; use revision_compactor::RevisionCompactor; -use tokio::{sync::mpsc::Receiver, time::sleep}; +use tokio::time::sleep; use utils::{ config::AutoCompactConfig, task_manager::{tasks::TaskName, Listener, TaskManager}, }; use xlineapi::{command::Command, execute_error::ExecuteError, RequestWrapper}; -use super::{ - index::{Index, IndexOperate}, - KvStore, -}; +use super::{index::Index, KvStore}; use crate::{revision_number::RevisionNumberGenerator, rpc::CompactionRequest}; /// mod revision compactor; @@ -101,13 +98,13 @@ pub(crate) async fn compact_bg_task( index: Arc, batch_limit: usize, interval: Duration, - mut compact_task_rx: Receiver<(i64, Option>)>, + compact_task_rx: flume::Receiver<(i64, Option>)>, shutdown_listener: Listener, ) { loop { let (revision, listener) = tokio::select! { - recv = compact_task_rx.recv() => { - let Some((revision, listener)) = recv else { + recv = compact_task_rx.recv_async() => { + let Ok((revision, listener)) = recv else { return; }; (revision, listener) diff --git a/crates/xline/src/storage/compact/revision_compactor.rs b/crates/xline/src/storage/compact/revision_compactor.rs index 149830a39..cd6619a3b 100644 --- a/crates/xline/src/storage/compact/revision_compactor.rs +++ b/crates/xline/src/storage/compact/revision_compactor.rs @@ -129,16 +129,20 @@ mod test { let mut compactable = MockCompactable::new(); compactable.expect_compact().times(3).returning(Ok); let revision_gen = Arc::new(RevisionNumberGenerator::new(110)); + let revision_gen_state = revision_gen.state(); let revision_compactor = RevisionCompactor::new_arc(true, Arc::clone(&revision_gen), 100); revision_compactor.set_compactable(compactable).await; // auto_compactor works successfully assert_eq!(revision_compactor.do_compact(None).await, Some(10)); - revision_gen.next(); // current revision: 111 + revision_gen_state.next(); // current revision: 111 + revision_gen_state.commit(); assert_eq!(revision_compactor.do_compact(Some(10)).await, Some(11)); revision_compactor.pause(); - revision_gen.next(); // current revision 112 + revision_gen_state.next(); // current revision 112 + revision_gen_state.commit(); assert!(revision_compactor.do_compact(Some(11)).await.is_none()); - revision_gen.next(); // current revision 113 + revision_gen_state.next(); // current revision 113 + revision_gen_state.commit(); assert!(revision_compactor.do_compact(Some(11)).await.is_none()); revision_compactor.resume(); assert_eq!(revision_compactor.do_compact(Some(11)).await, Some(13)); diff --git a/crates/xline/src/storage/db.rs b/crates/xline/src/storage/db.rs index 8d442edf9..d6389d389 100644 --- a/crates/xline/src/storage/db.rs +++ b/crates/xline/src/storage/db.rs @@ -1,6 +1,10 @@ +#![allow(clippy::multiple_inherent_impl)] + use std::{collections::HashMap, path::Path, sync::Arc}; -use engine::{Engine, EngineType, Snapshot, StorageEngine, StorageOps, WriteOperation}; +use engine::{ + Engine, EngineType, Snapshot, StorageEngine, StorageOps, Transaction, WriteOperation, +}; use prost::Message; use utils::{ config::EngineConfig, @@ -13,7 +17,7 @@ use xlineapi::{execute_error::ExecuteError, AlarmMember}; use super::{ auth_store::{AUTH_ENABLE_KEY, AUTH_REVISION_KEY}, - revision::KeyRevision, + storage_api::XlineStorageOps, }; use crate::{ rpc::{KeyValue, PbLease, Role, User}, @@ -28,8 +32,6 @@ pub(crate) const SCHEDULED_COMPACT_REVISION: &str = "scheduled_compact_revision" /// Key and value pair type KeyValuePair = (Vec, Vec); -/// Key and revision pair -type KeyRevisionPair = (Vec, KeyRevision); /// Database to store revision to kv mapping #[derive(Debug)] @@ -42,6 +44,7 @@ impl DB { /// Create a new `DB` /// /// # Errors + /// /// Return `ExecuteError::DbError` when open db failed #[inline] pub fn open(config: &EngineConfig) -> Result, ExecuteError> { @@ -56,76 +59,46 @@ impl DB { engine: Arc::new(engine), })) } +} - /// Get del lease key buffer +impl StorageOps for DB { #[inline] - fn get_del_lease_key_buffer(ops: &[WriteOp]) -> HashMap> { - ops.iter() - .filter_map(|op| { - if let WriteOp::DeleteLease(lease_id) = *op { - Some((lease_id, lease_id.encode_to_vec())) - } else { - None - } - }) - .collect::>() + fn write(&self, op: WriteOperation<'_>, sync: bool) -> Result<(), engine::EngineError> { + self.engine.write(op, sync) } - /// get del alarm buffer #[inline] - fn get_del_alarm_buffer(ops: &[WriteOp]) -> Vec { - ops.iter() - .find_map(|op| { - if let WriteOp::DeleteAlarm(ref key) = *op { - Some(key.encode_to_vec()) - } else { - None - } - }) - .unwrap_or_default() - } - - /// Get values by keys from storage - /// - /// # Errors - /// - /// if error occurs in storage, return `Err(error)` - pub(crate) fn get_values( - &self, - table: &'static str, - keys: &[K], - ) -> Result>>, ExecuteError> + fn write_multi<'a, Ops>(&self, ops: Ops, sync: bool) -> Result<(), engine::EngineError> where - K: AsRef<[u8]> + std::fmt::Debug + Sized, + Ops: IntoIterator>, { - let values = self - .engine - .get_multi(table, keys) - .map_err(|e| ExecuteError::DbError(format!("Failed to get keys {keys:?}: {e}")))? - .into_iter() - .collect::>(); - - assert_eq!(values.len(), keys.len(), "Index doesn't match with DB"); + self.engine.write_multi(ops, sync) + } - Ok(values) + #[inline] + fn get( + &self, + table: &str, + key: impl AsRef<[u8]>, + ) -> Result>, engine::EngineError> { + self.engine.get(table, key) } - /// Get values by keys from storage - /// - /// # Errors - /// - /// if error occurs in storage, return `Err(error)` - pub(crate) fn get_value( + #[inline] + fn get_multi( &self, - table: &'static str, - key: K, - ) -> Result>, ExecuteError> - where - K: AsRef<[u8]> + std::fmt::Debug, - { - self.engine - .get(table, key.as_ref()) - .map_err(|e| ExecuteError::DbError(format!("Failed to get key {key:?}: {e}"))) + table: &str, + keys: &[impl AsRef<[u8]>], + ) -> Result>>, engine::EngineError> { + self.engine.get_multi(table, keys) + } +} + +impl DB { + /// Creates a transaction + #[allow(unused)] // TODO: use this in the following refactor + pub(crate) fn transaction(&self) -> Transaction { + self.engine.transaction() } /// Get all values of the given table from storage @@ -172,27 +145,50 @@ impl DB { } } - /// Flush the operations to storage - pub(crate) fn flush_ops( - &self, - ops: Vec, - ) -> Result, ExecuteError> { + /// Calculate the hash of the storage + pub(crate) fn hash(&self) -> Result { + let mut hasher = crc32fast::Hasher::new(); + for table in XLINE_TABLES { + hasher.update(table.as_bytes()); + let kv_pairs = self.engine.get_all(table).map_err(|e| { + ExecuteError::DbError(format!("Failed to get all keys from {table:?}: {e}")) + })?; + for (k, v) in kv_pairs { + hasher.update(&k); + hasher.update(&v); + } + } + Ok(hasher.finalize()) + } + + /// Get the cached size of the engine + pub(crate) fn estimated_file_size(&self) -> u64 { + self.engine.estimated_file_size() + } + + /// Get the file size of the engine + pub(crate) fn file_size(&self) -> Result { + self.engine + .file_size() + .map_err(|e| ExecuteError::DbError(format!("Failed to get file size, error: {e}"))) + } +} + +impl XlineStorageOps for T +where + T: StorageOps, +{ + fn write_op(&self, op: WriteOp) -> Result<(), ExecuteError> { + self.write_ops(vec![op]) + } + + fn write_ops(&self, ops: Vec) -> Result<(), ExecuteError> { let mut wr_ops = Vec::new(); - let mut revs = Vec::new(); - let del_lease_key_buffer = Self::get_del_lease_key_buffer(&ops); - let del_alarm_buffer = Self::get_del_alarm_buffer(&ops); + let del_lease_key_buffer = get_del_lease_key_buffer(&ops); + let del_alarm_buffer = get_del_alarm_buffer(&ops); for op in ops { let wop = match op { WriteOp::PutKeyValue(rev, value) => { - revs.push(( - value.key.clone(), - KeyRevision::new( - value.create_revision, - value.version, - rev.revision(), - rev.sub_revision(), - ), - )); let key = rev.encode_to_vec(); WriteOperation::new_put(KV_TABLE, key, value.encode_to_vec()) } @@ -257,41 +253,66 @@ impl DB { }; wr_ops.push(wop); } - self.engine - .write_multi(wr_ops, false) - .map_err(|e| ExecuteError::DbError(format!("Failed to flush ops, error: {e}")))?; - Ok(revs) + self.write_multi(wr_ops, false) + .map_err(|e| ExecuteError::DbError(format!("Failed to flush ops, error: {e}"))) } - /// Calculate the hash of the storage - pub(crate) fn hash(&self) -> Result { - let mut hasher = crc32fast::Hasher::new(); - for table in XLINE_TABLES { - hasher.update(table.as_bytes()); - let kv_pairs = self.engine.get_all(table).map_err(|e| { - ExecuteError::DbError(format!("Failed to get all keys from {table:?}: {e}")) - })?; - for (k, v) in kv_pairs { - hasher.update(&k); - hasher.update(&v); - } - } - Ok(hasher.finalize()) + fn get_value(&self, table: &'static str, key: K) -> Result>, ExecuteError> + where + K: AsRef<[u8]> + std::fmt::Debug, + { + self.get(table, key.as_ref()) + .map_err(|e| ExecuteError::DbError(format!("Failed to get key {key:?}: {e}"))) } - /// Get the cached size of the engine - pub(crate) fn estimated_file_size(&self) -> u64 { - self.engine.estimated_file_size() - } + fn get_values( + &self, + table: &'static str, + keys: &[K], + ) -> Result>>, ExecuteError> + where + K: AsRef<[u8]> + std::fmt::Debug, + { + let values = self + .get_multi(table, keys) + .map_err(|e| ExecuteError::DbError(format!("Failed to get keys {keys:?}: {e}")))? + .into_iter() + .collect::>(); - /// Get the file size of the engine - pub(crate) fn file_size(&self) -> Result { - self.engine - .file_size() - .map_err(|e| ExecuteError::DbError(format!("Failed to get file size, error: {e}"))) + assert_eq!(values.len(), keys.len(), "Index doesn't match with DB"); + + Ok(values) } } +/// Get del lease key buffer +#[inline] +fn get_del_lease_key_buffer(ops: &[WriteOp]) -> HashMap> { + ops.iter() + .filter_map(|op| { + if let WriteOp::DeleteLease(lease_id) = *op { + Some((lease_id, lease_id.encode_to_vec())) + } else { + None + } + }) + .collect::>() +} + +/// get del alarm buffer +#[inline] +fn get_del_alarm_buffer(ops: &[WriteOp]) -> Vec { + ops.iter() + .find_map(|op| { + if let WriteOp::DeleteAlarm(ref key) = *op { + Some(key.encode_to_vec()) + } else { + None + } + }) + .unwrap_or_default() +} + /// Buffered Write Operation #[derive(Debug, Clone)] #[non_exhaustive] @@ -350,7 +371,7 @@ mod test { ..Default::default() }; let ops = vec![WriteOp::PutKeyValue(revision, kv.clone())]; - _ = db.flush_ops(ops)?; + db.write_ops(ops)?; let res = db.get_value(KV_TABLE, &key)?; assert_eq!(res, Some(kv.encode_to_vec())); @@ -381,7 +402,7 @@ mod test { ..Default::default() }; let ops = vec![WriteOp::PutKeyValue(revision, kv.clone())]; - _ = origin_db.flush_ops(ops)?; + origin_db.write_ops(ops)?; let snapshot = origin_db.get_snapshot(snapshot_path)?; @@ -462,7 +483,7 @@ mod test { WriteOp::PutUser(user), WriteOp::PutRole(role), ]; - _ = db.flush_ops(write_ops).unwrap(); + db.write_ops(write_ops).unwrap(); assert_eq!( db.get_value(KV_TABLE, Revision::new(1, 2).encode_to_vec()) .unwrap(), @@ -492,7 +513,7 @@ mod test { WriteOp::DeleteUser("user"), WriteOp::DeleteRole("role"), ]; - _ = db.flush_ops(del_ops).unwrap(); + db.write_ops(del_ops).unwrap(); assert_eq!( db.get_value(LEASE_TABLE, 1i64.encode_to_vec()).unwrap(), None diff --git a/crates/xline/src/storage/index.rs b/crates/xline/src/storage/index.rs index e6d903abd..27bc8138e 100644 --- a/crates/xline/src/storage/index.rs +++ b/crates/xline/src/storage/index.rs @@ -1,15 +1,50 @@ -use std::collections::HashSet; +#![allow(clippy::multiple_inherent_impl)] +#![allow(unused)] // Remove this when `IndexState` is used in xline + +use std::collections::{btree_map, BTreeMap, HashSet}; use clippy_utilities::OverflowArithmetic; -use crossbeam_skiplist::SkipMap; +use crossbeam_skiplist::{map::Entry, SkipMap}; use itertools::Itertools; -use parking_lot::RwLock; +use parking_lot::{Mutex, RwLock}; use utils::parking_lot_lock::RwLockMap; use xlineapi::command::KeyRange; use super::revision::{KeyRevision, Revision}; use crate::server::command::RangeType; +/// Operations for `Index` +pub(crate) trait IndexOperate { + /// Get `Revision` of keys, get the latest `Revision` when revision <= 0 + fn get(&self, key: &[u8], range_end: &[u8], revision: i64) -> Vec; + + /// Register a new `KeyRevision` of the given key + /// + /// Returns a new `KeyRevision` and previous `KeyRevision` of the key + fn register_revision( + &self, + key: Vec, + revision: i64, + sub_revision: i64, + ) -> (KeyRevision, Option); + + /// Gets the latest revision of the key + fn current_rev(&self, key: &[u8]) -> Option; + + /// Insert or update `KeyRevision` + fn insert(&self, key_revisions: Vec<(Vec, KeyRevision)>); + + /// Mark keys as deleted and return latest revision before deletion and deletion revision + /// return all revision pairs and all keys in range + fn delete( + &self, + key: &[u8], + range_end: &[u8], + revision: i64, + sub_revision: i64, + ) -> (Vec<(Revision, Revision)>, Vec>); +} + /// Keys to revisions mapping #[derive(Debug)] pub(crate) struct Index { @@ -25,7 +60,15 @@ impl Index { } } - /// Filter out `KeyRevision` that is less than one revision and convert to `Revision` + /// Creates a `IndexState` + pub(crate) fn state(&self) -> IndexState<'_> { + IndexState { + index_ref: self, + state: Mutex::default(), + } + } + + /// Filter out `KeyRevision` that is greater than or equal to the given revision and convert to `Revision` fn filter_revision(revs: &[KeyRevision], revision: i64) -> Vec { revs.iter() .filter(|rev| rev.mod_revision >= revision) @@ -53,18 +96,6 @@ impl Index { .map(KeyRevision::as_revision) } - /// Insert `KeyRevision` of deleted and generate `Revision` pair of deleted - fn gen_del_revision( - revs: &mut Vec, - revision: i64, - sub_revision: i64, - ) -> Option<(Revision, Revision)> { - let last_available_rev = Self::get_revision(revs, 0)?; - let del_rev = KeyRevision::new_deletion(revision, sub_revision); - revs.push(del_rev); - Some((last_available_rev, del_rev.as_revision())) - } - /// Get all revisions that need to be kept after compact at the given revision pub(crate) fn keep(&self, at_rev: i64) -> HashSet { let mut revs = HashSet::new(); @@ -88,116 +119,224 @@ impl Index { }); revs } -} -/// Operations of Index -pub(super) trait IndexOperate { - /// Get `Revision` of keys, get the latest `Revision` when revision <= 0 - fn get(&self, key: &[u8], range_end: &[u8], revision: i64) -> Vec; + /// Insert `KeyRevision` of deleted and generate `Revision` pair of deleted + fn gen_del_revision( + revs: &mut Vec, + revision: i64, + sub_revision: i64, + ) -> Option<(Revision, Revision)> { + let last_available_rev = Self::get_revision(revs, 0)?; + let del_rev = KeyRevision::new_deletion(revision, sub_revision); + revs.push(del_rev); + Some((last_available_rev, del_rev.as_revision())) + } +} +impl Index { /// Get `Revision` of keys from one revision - fn get_from_rev(&self, key: &[u8], range_end: &[u8], revision: i64) -> Vec; - - /// Mark keys as deleted and return latest revision before deletion and deletion revision - /// return all revision pairs and all keys in range - fn delete( + pub(super) fn get_from_rev( &self, key: &[u8], range_end: &[u8], revision: i64, - sub_revision: i64, - ) -> (Vec<(Revision, Revision)>, Vec>); - - /// Insert or update `KeyRevision` - fn insert(&self, key_revisions: Vec<(Vec, KeyRevision)>); - - /// Register a new `KeyRevision` of the given key - fn register_revision(&self, key: &[u8], revision: i64, sub_revision: i64) -> KeyRevision; - - /// Restore `KeyRevision` of a key - fn restore( - &self, - key: Vec, - revision: i64, - sub_revision: i64, - create_revision: i64, - version: i64, - ); - - /// Compact a `KeyRevision` by removing the versions with smaller or equal - /// revision than the given atRev except the largest one (If the largest one is - /// a tombstone, it will not be kept). - fn compact(&self, at_rev: i64) -> Vec; -} - -impl IndexOperate for Index { - fn get(&self, key: &[u8], range_end: &[u8], revision: i64) -> Vec { + ) -> Vec { match RangeType::get_range_type(key, range_end) { RangeType::OneKey => self .inner .get(key) - .and_then(|entry| { + .map(|entry| { entry .value() - .map_read(|revs| Self::get_revision(revs.as_ref(), revision)) + .map_read(|revs| Self::filter_revision(revs.as_ref(), revision)) }) - .map(|rev| vec![rev]) .unwrap_or_default(), RangeType::AllKeys => self .inner .iter() - .filter_map(|entry| { + .flat_map(|entry| { entry .value() - .map_read(|revs| Self::get_revision(revs.as_ref(), revision)) + .map_read(|revs| Self::filter_revision(revs.as_ref(), revision)) }) + .sorted() .collect(), RangeType::Range => self .inner .range(KeyRange::new(key, range_end)) - .filter_map(|entry| { + .flat_map(|entry| { entry .value() - .map_read(|revs| Self::get_revision(revs.as_ref(), revision)) + .map_read(|revs| Self::filter_revision(revs.as_ref(), revision)) }) + .sorted() .collect(), } } - fn get_from_rev(&self, key: &[u8], range_end: &[u8], revision: i64) -> Vec { + /// Restore `KeyRevision` of a key + pub(super) fn restore( + &self, + key: Vec, + revision: i64, + sub_revision: i64, + create_revision: i64, + version: i64, + ) { + self.inner + .get_or_insert(key, RwLock::new(Vec::new())) + .value() + .map_write(|mut revisions| { + revisions.push(KeyRevision::new( + create_revision, + version, + revision, + sub_revision, + )); + }); + } + /// Compact a `KeyRevision` by removing all versions smaller than or equal to the + /// given atRev, except for the largest one. Note that if the largest version + /// is a tombstone, it will also be removed. + pub(super) fn compact(&self, at_rev: i64) -> Vec { + let mut revs = Vec::new(); + let mut del_keys = Vec::new(); + + self.inner.iter().for_each(|entry| { + entry.value().map_write(|mut revisions| { + if let Some(revision) = revisions.first() { + if revision.mod_revision < at_rev { + let pivot = revisions.partition_point(|rev| rev.mod_revision <= at_rev); + let compacted_last_idx = pivot.overflow_sub(1); + // There is at least 1 element in the first partition, so the key revision at `compacted_last_idx` + // must exist. + let key_rev = revisions.get(compacted_last_idx).unwrap_or_else(|| { + unreachable!( + "Oops, the key revision at {compacted_last_idx} should not be None", + ) + }); + let compact_revs = if key_rev.is_deleted() { + revisions.drain(..=compacted_last_idx) + } else { + revisions.drain(..compacted_last_idx) + }; + revs.extend(compact_revs); + + if revisions.is_empty() { + del_keys.push(entry.key().clone()); + } + } + } + }); + }); + for key in del_keys { + let _ignore = self.inner.remove(&key); + } + revs + } +} + +/// Maps a closure to an entry +fn fmap_entry(mut op: F) -> impl FnMut(Entry, RwLock>>) -> R +where + F: FnMut((&[u8], &[KeyRevision])) -> R, +{ + move |entry: Entry, RwLock>>| { + entry.value().map_read(|revs| op((entry.key(), &revs))) + } +} + +/// Maps a closure to an entry value +fn fmap_value(mut op: F) -> impl FnMut(Entry, RwLock>>) -> R +where + F: FnMut(&[KeyRevision]) -> R, +{ + move |entry: Entry, RwLock>>| entry.value().map_read(|revs| op(&revs)) +} + +/// Mutably maps a closure to an entry value +fn fmap_value_mut(mut op: F) -> impl FnMut(Entry, RwLock>>) -> R +where + F: FnMut(&mut Vec) -> R, +{ + move |entry: Entry, RwLock>>| { + entry.value().map_write(|mut revs| op(&mut revs)) + } +} + +impl IndexOperate for Index { + fn get(&self, key: &[u8], range_end: &[u8], revision: i64) -> Vec { match RangeType::get_range_type(key, range_end) { RangeType::OneKey => self .inner .get(key) - .map(|entry| { - entry - .value() - .map_read(|revs| Self::filter_revision(revs.as_ref(), revision)) - }) + .and_then(fmap_value(|revs| Index::get_revision(revs, revision))) + .map(|rev| vec![rev]) .unwrap_or_default(), RangeType::AllKeys => self .inner .iter() - .flat_map(|entry| { - entry - .value() - .map_read(|revs| Self::filter_revision(revs.as_ref(), revision)) - }) - .sorted() + .filter_map(fmap_value(|revs| Index::get_revision(revs, revision))) .collect(), RangeType::Range => self .inner .range(KeyRange::new(key, range_end)) - .flat_map(|entry| { - entry - .value() - .map_read(|revs| Self::filter_revision(revs.as_ref(), revision)) - }) - .sorted() + .filter_map(fmap_value(|revs| Index::get_revision(revs, revision))) .collect(), } } + fn register_revision( + &self, + key: Vec, + revision: i64, + sub_revision: i64, + ) -> (KeyRevision, Option) { + self.inner.get(&key).map_or_else( + || { + let new_rev = KeyRevision::new(revision, 1, revision, sub_revision); + let _ignore = self.inner.insert(key, RwLock::new(vec![new_rev])); + (new_rev, None) + }, + fmap_value_mut(|revisions| { + let last = *revisions + .last() + .unwrap_or_else(|| unreachable!("empty revision list")); + let new_rev = if last.is_deleted() { + KeyRevision::new(revision, 1, revision, sub_revision) + } else { + KeyRevision::new( + last.create_revision, + last.version.overflow_add(1), + revision, + sub_revision, + ) + }; + revisions.push(new_rev); + (new_rev, Some(last)) + }), + ) + } + + fn current_rev(&self, key: &[u8]) -> Option { + self.inner + .get(key) + .and_then(fmap_value(|revs| revs.last().copied())) + } + + fn insert(&self, key_revisions: Vec<(Vec, KeyRevision)>) { + for (key, revision) in key_revisions { + self.inner.get(&key).map_or_else( + || { + let _ignore = self.inner.insert(key, RwLock::new(vec![revision])); + }, + fmap_value_mut(|revs| { + revs.push(revision); + }), + ); + } + } + fn delete( &self, key: &[u8], @@ -211,11 +350,9 @@ impl IndexOperate for Index { .inner .get(key) .into_iter() - .filter_map(|entry| { - entry.value().map_write(|mut revs| { - Self::gen_del_revision(revs.as_mut(), revision, sub_revision) - }) - }) + .filter_map(fmap_value_mut(|revs| { + Self::gen_del_revision(revs, revision, sub_revision) + })) .collect(); let keys = if pairs.is_empty() { vec![] @@ -230,12 +367,8 @@ impl IndexOperate for Index { .zip(0..) .filter_map(|(entry, i)| { entry.value().map_write(|mut revs| { - Self::gen_del_revision( - revs.as_mut(), - revision, - sub_revision.overflow_add(i), - ) - .map(|pair| (pair, entry.key().clone())) + Self::gen_del_revision(&mut revs, revision, sub_revision.overflow_add(i)) + .map(|pair| (pair, entry.key().clone())) }) }) .unzip(), @@ -245,108 +378,250 @@ impl IndexOperate for Index { .zip(0..) .filter_map(|(entry, i)| { entry.value().map_write(|mut revs| { - Self::gen_del_revision( - revs.as_mut(), - revision, - sub_revision.overflow_add(i), - ) - .map(|pair| (pair, entry.key().clone())) + Self::gen_del_revision(&mut revs, revision, sub_revision.overflow_add(i)) + .map(|pair| (pair, entry.key().clone())) }) }) .unzip(), }; (pairs, keys) } +} - fn insert(&self, key_revisions: Vec<(Vec, KeyRevision)>) { - for (key, revision) in key_revisions { - if let Some(entry) = self.inner.get::<[u8]>(key.as_ref()) { - entry.value().map_write(|mut revs| revs.push(revision)); - } else { - _ = self.inner.insert(key, RwLock::new(vec![revision])); - } +/// A index with extra state, it won't mutate the index directly before commit +#[derive(Debug)] +pub(crate) struct IndexState<'a> { + /// Inner struct of `Index` + index_ref: &'a Index, + /// State current modification + state: Mutex, Vec>>, +} + +impl IndexState<'_> { + /// Commits all changes + pub(crate) fn commit(self) { + let index = &self.index_ref.inner; + while let Some((key, state_revs)) = self.state.lock().pop_first() { + let entry = index.get_or_insert(key, RwLock::default()); + fmap_value_mut(|revs| { + revs.extend_from_slice(&state_revs); + })(entry); } } - fn register_revision(&self, key: &[u8], revision: i64, sub_revision: i64) -> KeyRevision { - if let Some(entry) = self.inner.get(key) { - entry.value().map_read(|revisions| { - if let Some(rev) = revisions.last() { - if rev.is_deleted() { - KeyRevision::new(revision, 1, revision, sub_revision) - } else { - KeyRevision::new( - rev.create_revision, - rev.version.overflow_add(1), - revision, - sub_revision, - ) - } - } else { - panic!("Get empty revision list for key {key:?}"); - } - }) - } else { - KeyRevision::new(revision, 1, revision, sub_revision) + /// Discards all changes + pub(crate) fn discard(&self) { + self.state.lock().clear(); + } + + /// Gets the revisions for a single key + fn one_key_revisions( + &self, + key: &[u8], + state: &BTreeMap, Vec>, + ) -> Vec { + let index = &self.index_ref.inner; + let mut result = index + .get(key) + .map(fmap_value(<[KeyRevision]>::to_vec)) + .unwrap_or_default(); + if let Some(revs) = state.get(key) { + result.extend_from_slice(revs); } + result } - fn restore( + /// Gets the revisions for a range of keys + fn range_key_revisions(&self, range: KeyRange) -> BTreeMap, Vec> { + let mut map: BTreeMap, Vec> = BTreeMap::new(); + let index = &self.index_ref.inner; + let state = self.state.lock(); + for (key, value) in index + .range(range.clone()) + .map(fmap_entry(|(k, v)| (k.to_vec(), v.to_vec()))) + .chain(state.range(range).map(|(k, v)| (k.clone(), v.clone()))) + { + let entry = map.entry(key.clone()).or_default(); + entry.extend(value); + } + map + } + + /// Gets the revisions for all keys + fn all_key_revisions(&self) -> BTreeMap, Vec> { + let mut map: BTreeMap, Vec> = BTreeMap::new(); + let index = &self.index_ref.inner; + let state = self.state.lock(); + for (key, value) in index + .iter() + .map(fmap_entry(|(k, v)| (k.to_vec(), v.to_vec()))) + .chain(state.clone().into_iter()) + { + let entry = map.entry(key.clone()).or_default(); + entry.extend(value.clone()); + } + map + } + + /// Deletes one key + fn delete_one( + &self, + key: &[u8], + revision: i64, + sub_revision: i64, + ) -> Option<((Revision, Revision), Vec)> { + let mut state = self.state.lock(); + let revs = self.one_key_revisions(key, &state); + let last_available_rev = Index::get_revision(&revs, 0)?; + let entry = state.entry(key.to_vec()).or_default(); + let del_rev = KeyRevision::new_deletion(revision, sub_revision); + entry.push(del_rev); + let pair = (last_available_rev, del_rev.as_revision()); + + Some((pair, key.to_vec())) + } + + /// Deletes a range of keys + fn delete_range( + &self, + range: KeyRange, + revision: i64, + sub_revision: i64, + ) -> (Vec<(Revision, Revision)>, Vec>) { + self.range_key_revisions(range) + .into_keys() + .zip(0..) + .filter_map(|(key, i)| self.delete_one(&key, revision, sub_revision.overflow_add(i))) + .unzip() + } + + /// Deletes all keys + fn delete_all( + &self, + revision: i64, + sub_revision: i64, + ) -> (Vec<(Revision, Revision)>, Vec>) { + self.all_key_revisions() + .into_keys() + .zip(0..) + .filter_map(|(key, i)| self.delete_one(&key, revision, sub_revision.overflow_add(i))) + .unzip() + } + + /// Reads an entry + #[allow(clippy::needless_pass_by_value)] // it's intended to consume the entry + fn entry_read(entry: Entry, RwLock>>) -> (Vec, Vec) { + (entry.key().clone(), entry.value().read().clone()) + } +} + +impl IndexOperate for IndexState<'_> { + fn get(&self, key: &[u8], range_end: &[u8], revision: i64) -> Vec { + match RangeType::get_range_type(key, range_end) { + RangeType::OneKey => { + Index::get_revision(&self.one_key_revisions(key, &self.state.lock()), revision) + .map(|rev| vec![rev]) + .unwrap_or_default() + } + RangeType::AllKeys => self + .all_key_revisions() + .into_iter() + .filter_map(|(_, revs)| Index::get_revision(revs.as_ref(), revision)) + .collect(), + RangeType::Range => self + .range_key_revisions(KeyRange::new(key, range_end)) + .into_iter() + .filter_map(|(_, revs)| Index::get_revision(revs.as_ref(), revision)) + .collect(), + } + } + + fn register_revision( &self, key: Vec, revision: i64, sub_revision: i64, - create_revision: i64, - version: i64, - ) { - self.inner - .get_or_insert(key, RwLock::new(Vec::new())) - .value() - .map_write(|mut revisions| { - revisions.push(KeyRevision::new( - create_revision, - version, + ) -> (KeyRevision, Option) { + let index = &self.index_ref.inner; + let mut state = self.state.lock(); + + let next_rev = |revisions: &[KeyRevision]| { + let last = *revisions + .last() + .unwrap_or_else(|| unreachable!("empty revision list")); + let new_rev = if last.is_deleted() { + KeyRevision::new(revision, 1, revision, sub_revision) + } else { + KeyRevision::new( + last.create_revision, + last.version.overflow_add(1), revision, sub_revision, - )); - }); + ) + }; + (new_rev, Some(last)) + }; + + match (index.get(&key), state.entry(key)) { + (None, btree_map::Entry::Vacant(e)) => { + let new_rev = KeyRevision::new(revision, 1, revision, sub_revision); + let _ignore = e.insert(vec![new_rev]); + (new_rev, None) + } + (None | Some(_), btree_map::Entry::Occupied(mut e)) => { + let (new_rev, last) = next_rev(e.get_mut()); + e.get_mut().push(new_rev); + (new_rev, last) + } + (Some(e), btree_map::Entry::Vacant(se)) => { + let (new_rev, last) = fmap_value(next_rev)(e); + let _ignore = se.insert(vec![new_rev]); + (new_rev, last) + } + } } - fn compact(&self, at_rev: i64) -> Vec { - let mut revs = Vec::new(); - let mut del_keys = Vec::new(); + fn current_rev(&self, key: &[u8]) -> Option { + let index = &self.index_ref.inner; + let state = self.state.lock(); - self.inner.iter().for_each(|entry| { - entry.value().map_write(|mut revisions| { - if let Some(revision) = revisions.first() { - if revision.mod_revision < at_rev { - let pivot = revisions.partition_point(|rev| rev.mod_revision <= at_rev); - let compacted_last_idx = pivot.overflow_sub(1); - // There is at least 1 element in the first partition, so the key revision at `compacted_last_idx` - // must exist. - let key_rev = revisions.get(compacted_last_idx).unwrap_or_else(|| { - unreachable!( - "Oops, the key revision at {compacted_last_idx} should not be None", - ) - }); - let compact_revs = if key_rev.is_deleted() { - revisions.drain(..=compacted_last_idx) - } else { - revisions.drain(..compacted_last_idx) - }; - revs.extend(compact_revs); + match (index.get(key), state.get(key)) { + (None, None) => None, + (None | Some(_), Some(revs)) => revs.last().copied(), + (Some(e), None) => fmap_value(|revs| revs.last().copied())(e), + } + } - if revisions.is_empty() { - del_keys.push(entry.key().clone()); - } - } - } - }); - }); - for key in del_keys { - let _ignore = self.inner.remove(&key); + fn insert(&self, key_revisions: Vec<(Vec, KeyRevision)>) { + let mut state = self.state.lock(); + for (key, revision) in key_revisions { + if let Some(revs) = state.get_mut::<[u8]>(key.as_ref()) { + revs.push(revision); + } else { + _ = state.insert(key, vec![revision]); + } } - revs + } + + fn delete( + &self, + key: &[u8], + range_end: &[u8], + revision: i64, + sub_revision: i64, + ) -> (Vec<(Revision, Revision)>, Vec>) { + let (pairs, keys) = match RangeType::get_range_type(key, range_end) { + RangeType::OneKey => self + .delete_one(key, revision, sub_revision) + .into_iter() + .unzip(), + RangeType::AllKeys => self.delete_all(revision, sub_revision), + RangeType::Range => { + self.delete_range(KeyRange::new(key, range_end), revision, sub_revision) + } + }; + + (pairs, keys) } } @@ -358,30 +633,23 @@ mod test { index .inner .get(key.as_ref()) - .expect("index entry should not be None") - .value() - .map_read(|revs| assert_eq!(*revs, expected_values)); + .map(fmap_value(|revs| assert_eq!(revs, expected_values))) + .expect("index entry should not be None"); } fn init_and_test_insert() -> Index { let index = Index::new(); - - index.insert(vec![ - (b"key".to_vec(), index.register_revision(b"key", 1, 3)), - (b"foo".to_vec(), index.register_revision(b"foo", 4, 5)), - (b"bar".to_vec(), index.register_revision(b"bar", 5, 4)), - ]); - - index.insert(vec![ - (b"key".to_vec(), index.register_revision(b"key", 2, 2)), - (b"foo".to_vec(), index.register_revision(b"foo", 6, 6)), - (b"bar".to_vec(), index.register_revision(b"bar", 7, 7)), - ]); - index.insert(vec![ - (b"key".to_vec(), index.register_revision(b"key", 3, 1)), - (b"foo".to_vec(), index.register_revision(b"foo", 8, 8)), - (b"bar".to_vec(), index.register_revision(b"bar", 9, 9)), - ]); + let mut txn = index.state(); + txn.register_revision(b"key".to_vec(), 1, 3); + txn.register_revision(b"foo".to_vec(), 4, 5); + txn.register_revision(b"bar".to_vec(), 5, 4); + txn.register_revision(b"key".to_vec(), 2, 2); + txn.register_revision(b"foo".to_vec(), 6, 6); + txn.register_revision(b"bar".to_vec(), 7, 7); + txn.register_revision(b"key".to_vec(), 3, 1); + txn.register_revision(b"foo".to_vec(), 8, 8); + txn.register_revision(b"bar".to_vec(), 9, 9); + txn.commit(); match_values( &index, @@ -419,8 +687,10 @@ mod test { #[test] fn test_get() { let index = init_and_test_insert(); - assert_eq!(index.get(b"key", b"", 0), vec![Revision::new(3, 1)]); - assert_eq!(index.get(b"key", b"", 1), vec![Revision::new(1, 3)]); + let txn = index.state(); + assert_eq!(txn.get(b"key", b"", 0), vec![Revision::new(3, 1)]); + assert_eq!(txn.get(b"key", b"", 1), vec![Revision::new(1, 3)]); + txn.commit(); assert_eq!( index.get_from_rev(b"key", b"", 2), vec![Revision::new(2, 2), Revision::new(3, 1)] @@ -453,9 +723,10 @@ mod test { #[test] fn test_delete() { let index = init_and_test_insert(); + let mut txn = index.state(); assert_eq!( - index.delete(b"key", b"", 10, 0), + txn.delete(b"key", b"", 10, 0), ( vec![(Revision::new(3, 1), Revision::new(10, 0))], vec![b"key".to_vec()] @@ -463,7 +734,7 @@ mod test { ); assert_eq!( - index.delete(b"a", b"g", 11, 0), + txn.delete(b"a", b"g", 11, 0), ( vec![ (Revision::new(9, 9), Revision::new(11, 0)), @@ -473,7 +744,10 @@ mod test { ) ); - assert_eq!(index.delete(b"\0", b"\0", 12, 0), (vec![], vec![])); + assert_eq!(txn.delete(b"\0", b"\0", 12, 0), (vec![], vec![])); + + txn.commit(); + match_values( &index, b"key", @@ -552,11 +826,11 @@ mod test { #[test] fn test_compact_with_deletion() { let index = init_and_test_insert(); - index.delete(b"a", b"g", 10, 0); - index.insert(vec![( - b"bar".to_vec(), - index.register_revision(b"bar", 11, 0), - )]); + let mut txn = index.state(); + + txn.delete(b"a", b"g", 10, 0); + txn.register_revision(b"bar".to_vec(), 11, 0); + txn.commit(); let res = index.compact(10); diff --git a/crates/xline/src/storage/kv_store.rs b/crates/xline/src/storage/kv_store.rs index b07a2276d..19b8fb20a 100644 --- a/crates/xline/src/storage/kv_store.rs +++ b/crates/xline/src/storage/kv_store.rs @@ -2,7 +2,7 @@ use std::{ cmp::Ordering, - collections::{HashMap, VecDeque}, + collections::HashMap, sync::{ atomic::{AtomicI64, Ordering::Relaxed}, Arc, @@ -10,8 +10,8 @@ use std::{ }; use clippy_utilities::{NumericCast, OverflowArithmetic}; +use engine::{Transaction, TransactionApi}; use prost::Message; -use tokio::sync::mpsc; use tracing::{debug, warn}; use utils::table_names::{KV_TABLE, META_TABLE}; use xlineapi::{ @@ -28,14 +28,17 @@ use super::{ use crate::{ header_gen::HeaderGenerator, revision_check::RevisionCheck, - revision_number::RevisionNumberGenerator, + revision_number::{RevisionNumberGenerator, RevisionNumberGeneratorState}, rpc::{ CompactionRequest, CompactionResponse, Compare, CompareResult, CompareTarget, DeleteRangeRequest, DeleteRangeResponse, Event, EventType, KeyValue, PutRequest, PutResponse, RangeRequest, RangeResponse, Request, RequestWrapper, ResponseWrapper, SortOrder, SortTarget, TargetUnion, TxnRequest, TxnResponse, }, - storage::db::{WriteOp, FINISHED_COMPACT_REVISION}, + storage::{ + db::{WriteOp, FINISHED_COMPACT_REVISION}, + storage_api::XlineStorageOps, + }, }; /// KV store @@ -48,9 +51,9 @@ pub(crate) struct KvStore { /// Header generator header_gen: Arc, /// KV update sender - kv_update_tx: mpsc::Sender<(i64, Vec)>, + kv_update_tx: flume::Sender<(i64, Vec)>, /// Compact task submit sender - compact_task_tx: mpsc::Sender<(i64, Option>)>, + compact_task_tx: flume::Sender<(i64, Option>)>, /// Lease collection lease_collection: Arc, } @@ -76,13 +79,16 @@ impl KvStoreInner { } } - /// Get `KeyValue` from the `KvStoreInner` - fn get_values(&self, revisions: &[Revision]) -> Result, ExecuteError> { + /// Get `KeyValue` from the `KvStore` + fn get_values(txn: &T, revisions: &[Revision]) -> Result, ExecuteError> + where + T: XlineStorageOps, + { let revisions = revisions .iter() .map(Revision::encode_to_vec) .collect::>>(); - let values = self.db.get_values(KV_TABLE, &revisions)?; + let values = txn.get_values(KV_TABLE, &revisions)?; let kvs: Vec = values .into_iter() .flatten() @@ -97,15 +103,59 @@ impl KvStoreInner { /// Get `KeyValue` of a range /// - /// If `range_end` is `&[]`, this function will return one or zero `KeyValue`. - fn get_range( - &self, + /// If `range_end` is `&[]`, this function will return one or zero + /// `KeyValue`. + fn get_range( + txn_db: &T, + index: &dyn IndexOperate, + key: &[u8], + range_end: &[u8], + revision: i64, + ) -> Result, ExecuteError> + where + T: XlineStorageOps, + { + let revisions = index.get(key, range_end, revision); + Self::get_values(txn_db, &revisions) + } + + /// Get `KeyValue` of a range with limit and count only, return kvs and + /// total count + fn get_range_with_opts( + txn_db: &T, + index: &dyn IndexOperate, key: &[u8], range_end: &[u8], revision: i64, - ) -> Result, ExecuteError> { - let revisions = self.index.get(key, range_end, revision); - self.get_values(&revisions) + limit: usize, + count_only: bool, + ) -> Result<(Vec, usize), ExecuteError> + where + T: XlineStorageOps, + { + let mut revisions = index.get(key, range_end, revision); + let total = revisions.len(); + if count_only || total == 0 { + return Ok((vec![], total)); + } + if limit != 0 { + revisions.truncate(limit); + } + let kvs = Self::get_values(txn_db, &revisions)?; + Ok((kvs, total)) + } + + /// Get previous `KeyValue` of a `KeyValue` + pub(crate) fn get_prev_kv(&self, kv: &KeyValue) -> Option { + Self::get_range( + self.db.as_ref(), + self.index.as_ref(), + &kv.key, + &[], + kv.mod_revision.overflow_sub(1), + ) + .ok()? + .pop() } /// Get `KeyValue` start from a revision and convert to `Event` @@ -117,8 +167,7 @@ impl KvStoreInner { let revisions = self.index .get_from_rev(key_range.range_start(), key_range.range_end(), revision); - let events: Vec = self - .get_values(&revisions)? + let events = Self::get_values(self.db.as_ref(), &revisions)? .into_iter() .map(|kv| { // Delete @@ -140,58 +189,44 @@ impl KvStoreInner { Ok(events) } - /// Get previous `KeyValue` of a `KeyValue` - pub(crate) fn get_prev_kv(&self, kv: &KeyValue) -> Option { - self.get_range(&kv.key, &[], kv.mod_revision.overflow_sub(1)) - .ok()? - .pop() - } - /// Get compacted revision of KV store pub(crate) fn compacted_revision(&self) -> i64 { self.compacted_rev.load(Relaxed) } - - /// Get `KeyValue` of a range with limit and count only, return kvs and total count - fn get_range_with_opts( - &self, - key: &[u8], - range_end: &[u8], - revision: i64, - limit: usize, - count_only: bool, - ) -> Result<(Vec, usize), ExecuteError> { - let mut revisions = self.index.get(key, range_end, revision); - let total = revisions.len(); - if count_only || total == 0 { - return Ok((vec![], total)); - } - if limit != 0 { - revisions.truncate(limit); - } - let kvs = self.get_values(&revisions)?; - Ok((kvs, total)) - } } impl KvStore { - /// execute a kv request + /// Executes a request pub(crate) fn execute( &self, request: &RequestWrapper, + as_ctx: Option<(&Transaction, &mut dyn IndexOperate)>, ) -> Result { - self.handle_kv_requests(request).map(CommandResponse::new) + if let Some((db, index)) = as_ctx { + self.execute_request(request, db, index) + } else { + self.execute_request( + request, + &self.inner.db.transaction(), + &mut self.inner.index.state(), + ) + } + .map(CommandResponse::new) } - /// sync a kv request - pub(crate) async fn after_sync( + /// After-Syncs a request + pub(crate) fn after_sync( &self, request: &RequestWrapper, - revision: i64, - ) -> Result<(SyncResponse, Vec), ExecuteError> { - self.sync_request(request, revision) - .await - .map(|(rev, ops)| (SyncResponse::new(rev), ops)) + txn_db: &T, + index: &(dyn IndexOperate + Send + Sync), + revision_gen: &RevisionNumberGeneratorState<'_>, + to_execute: bool, + ) -> Result<(SyncResponse, Option), ExecuteError> + where + T: XlineStorageOps + TransactionApi, + { + self.sync_request(request, txn_db, index, revision_gen, to_execute) } /// Recover data from persistent storage @@ -238,11 +273,7 @@ impl KvStore { if scheduled_rev > self.compacted_revision() { let event = Arc::new(event_listener::Event::new()); let listener = event.listen(); - if let Err(e) = self - .compact_task_tx - .send((scheduled_rev, Some(event))) - .await - { + if let Err(e) = self.compact_task_tx.send((scheduled_rev, Some(event))) { panic!("the compactor exited unexpectedly: {e:?}"); } listener.await; @@ -270,8 +301,8 @@ impl KvStore { pub(crate) fn new( inner: Arc, header_gen: Arc, - kv_update_tx: mpsc::Sender<(i64, Vec)>, - compact_task_tx: mpsc::Sender<(i64, Option>)>, + kv_update_tx: flume::Sender<(i64, Vec)>, + compact_task_tx: flume::Sender<(i64, Option>)>, lease_collection: Arc, ) -> Self { Self { @@ -300,9 +331,9 @@ impl KvStore { } /// Notify KV changes to KV watcher - async fn notify_updates(&self, revision: i64, updates: Vec) { + fn notify_updates(&self, revision: i64, updates: Vec) { assert!( - self.kv_update_tx.send((revision, updates)).await.is_ok(), + self.kv_update_tx.send((revision, updates)).is_ok(), "Failed to send updates to KV watcher" ); } @@ -439,11 +470,12 @@ impl KvStore { } /// Check result of a `Compare` - fn check_compare(&self, cmp: &Compare) -> bool { - let kvs = self - .inner - .get_range(&cmp.key, &cmp.range_end, 0) - .unwrap_or_default(); + fn check_compare(txn_db: &T, index: &dyn IndexOperate, cmp: &Compare) -> bool + where + T: XlineStorageOps, + { + let kvs = + KvStoreInner::get_range(txn_db, index, &cmp.key, &cmp.range_end, 0).unwrap_or_default(); if kvs.is_empty() { if let Some(TargetUnion::Value(_)) = cmp.target_union { false @@ -476,14 +508,14 @@ impl KvStore { revisions .iter() .for_each(|rev| ops.push(WriteOp::DeleteKeyValue(rev.as_ref()))); - _ = self.inner.db.flush_ops(ops)?; + self.inner.db.write_ops(ops)?; Ok(()) } /// Compact kv storage pub(crate) fn compact_finished(&self, revision: i64) -> Result<(), ExecuteError> { let ops = vec![WriteOp::PutFinishedCompactRevision(revision)]; - _ = self.inner.db.flush_ops(ops)?; + self.inner.db.write_ops(ops)?; self.update_compacted_revision(revision); Ok(()) } @@ -522,32 +554,63 @@ impl KvStore { } } -/// handle and sync kv requests +#[cfg(test)] +/// Test uitls +impl KvStore { + pub(crate) fn db(&self) -> &DB { + self.inner.db.as_ref() + } +} + +// Speculatively execute requests impl KvStore { - /// Handle kv requests - fn handle_kv_requests( + /// execute requests + fn execute_request( &self, wrapper: &RequestWrapper, + txn_db: &Transaction, + index: &mut dyn IndexOperate, ) -> Result { debug!("Execute {:?}", wrapper); + #[allow(clippy::wildcard_enum_match_arm)] - let res = match *wrapper { - RequestWrapper::RangeRequest(ref req) => self.handle_range_request(req).map(Into::into), - RequestWrapper::PutRequest(ref req) => self.handle_put_request(req).map(Into::into), - RequestWrapper::DeleteRangeRequest(ref req) => { - self.handle_delete_range_request(req).map(Into::into) + let res: ResponseWrapper = match *wrapper { + RequestWrapper::RangeRequest(ref req) => { + self.execute_range(txn_db, index, req).map(Into::into)? + } + RequestWrapper::PutRequest(ref req) => { + self.execute_put(txn_db, index, req).map(Into::into)? + } + RequestWrapper::DeleteRangeRequest(ref req) => self + .execute_delete_range(txn_db, index, req) + .map(Into::into)?, + RequestWrapper::TxnRequest(ref req) => { + // As we store use revision as key in the DB storage, + // a fake revision needs to be used during speculative execution + let fake_revision = i64::MAX; + self.execute_txn(txn_db, index, req, fake_revision, &mut 0) + .map(Into::into)? } - RequestWrapper::TxnRequest(ref req) => self.handle_txn_request(req).map(Into::into), RequestWrapper::CompactionRequest(ref req) => { - self.handle_compaction_request(req).map(Into::into) + debug!("Receive CompactionRequest {:?}", req); + self.execute_compaction(req).map(Into::into)? } _ => unreachable!("Other request should not be sent to this store"), }; - res + + Ok(res) } - /// Handle `RangeRequest` - fn handle_range_request(&self, req: &RangeRequest) -> Result { + /// Execute `RangeRequest` + fn execute_range( + &self, + tnx_db: &T, + index: &dyn IndexOperate, + req: &RangeRequest, + ) -> Result + where + T: XlineStorageOps, + { req.check_revision(self.compacted_revision(), self.revision())?; let storage_fetch_limit = if (req.sort_order() != SortOrder::None) @@ -561,7 +624,9 @@ impl KvStore { } else { req.limit.overflow_add(1) // get one extra for "more" flag }; - let (mut kvs, total) = self.inner.get_range_with_opts( + let (mut kvs, total) = KvStoreInner::get_range_with_opts( + tnx_db, + index, &req.key, &req.range_end, req.revision, @@ -594,11 +659,20 @@ impl KvStore { kvs.iter_mut().for_each(|kv| kv.value.clear()); } response.kvs = kvs; + Ok(response) } - /// Handle `PutRequest` - fn handle_put_request(&self, req: &PutRequest) -> Result { + /// Generates `PutResponse` + fn generate_put_resp( + &self, + req: &PutRequest, + txn_db: &T, + prev_rev: Option, + ) -> Result<(PutResponse, Option), ExecuteError> + where + T: XlineStorageOps, + { let mut response = PutResponse { header: Some(self.header_gen.gen_header()), ..Default::default() @@ -606,24 +680,91 @@ impl KvStore { if req.lease != 0 && self.lease_collection.look_up(req.lease).is_none() { return Err(ExecuteError::LeaseNotFound(req.lease)); }; + if req.prev_kv || req.ignore_lease || req.ignore_value { - let prev_kv = self.inner.get_range(&req.key, &[], 0)?.pop(); + let prev_kv = + KvStoreInner::get_values(txn_db, &prev_rev.into_iter().collect::>())?.pop(); if prev_kv.is_none() && (req.ignore_lease || req.ignore_value) { return Err(ExecuteError::KeyNotFound); } if req.prev_kv { - response.prev_kv = prev_kv; + response.prev_kv = prev_kv.clone(); } + return Ok((response, prev_kv)); + } + + Ok((response, None)) + } + + /// Execute `PutRequest` + fn execute_put( + &self, + txn_db: &Transaction, + index: &dyn IndexOperate, + req: &PutRequest, + ) -> Result { + let prev_rev = (req.prev_kv || req.ignore_lease || req.ignore_value) + .then(|| index.current_rev(&req.key)) + .flatten(); + let (response, _prev_kv) = + self.generate_put_resp(req, txn_db, prev_rev.map(|key_rev| key_rev.as_revision()))?; + Ok(response) + } + + /// Execute `PutRequest` in Txn + fn execute_txn_put( + &self, + txn_db: &Transaction, + index: &dyn IndexOperate, + req: &PutRequest, + revision: i64, + sub_revision: &mut i64, + ) -> Result { + let (new_rev, prev_rev) = index.register_revision(req.key.clone(), revision, *sub_revision); + let (response, prev_kv) = + self.generate_put_resp(req, txn_db, prev_rev.map(|key_rev| key_rev.as_revision()))?; + let mut kv = KeyValue { + key: req.key.clone(), + value: req.value.clone(), + create_revision: new_rev.create_revision, + mod_revision: new_rev.mod_revision, + version: new_rev.version, + lease: req.lease, }; + if req.ignore_lease { + kv.lease = prev_kv + .as_ref() + .unwrap_or_else(|| { + unreachable!("Should returns an error when prev kv does not exist") + }) + .lease; + } + if req.ignore_value { + kv.value = prev_kv + .as_ref() + .unwrap_or_else(|| { + unreachable!("Should returns an error when prev kv does not exist") + }) + .value + .clone(); + } + txn_db.write_op(WriteOp::PutKeyValue(new_rev.as_revision(), kv.clone()))?; + *sub_revision = sub_revision.overflow_add(1); + Ok(response) } - /// Handle `DeleteRangeRequest` - fn handle_delete_range_request( + /// Generates `DeleteRangeResponse` + fn generate_delete_range_resp( &self, req: &DeleteRangeRequest, - ) -> Result { - let prev_kvs = self.inner.get_range(&req.key, &req.range_end, 0)?; + txn_db: &T, + index: &dyn IndexOperate, + ) -> Result + where + T: XlineStorageOps, + { + let prev_kvs = KvStoreInner::get_range(txn_db, index, &req.key, &req.range_end, 0)?; let mut response = DeleteRangeResponse { header: Some(self.header_gen.gen_header()), ..DeleteRangeResponse::default() @@ -635,33 +776,91 @@ impl KvStore { Ok(response) } - /// Handle `TxnRequest` - fn handle_txn_request(&self, req: &TxnRequest) -> Result { - req.check_revision(self.compacted_revision(), self.revision())?; + /// Execute `DeleteRangeRequest` + fn execute_delete_range( + &self, + txn_db: &T, + index: &dyn IndexOperate, + req: &DeleteRangeRequest, + ) -> Result + where + T: XlineStorageOps, + { + self.generate_delete_range_resp(req, txn_db, index) + } + + /// Execute `DeleteRangeRequest` in Txn + fn execute_txn_delete_range( + &self, + txn_db: &T, + index: &dyn IndexOperate, + req: &DeleteRangeRequest, + revision: i64, + sub_revision: &mut i64, + ) -> Result + where + T: XlineStorageOps, + { + let response = self.generate_delete_range_resp(req, txn_db, index)?; + let _keys = Self::delete_keys( + txn_db, + index, + &req.key, + &req.range_end, + revision, + sub_revision, + )?; + + Ok(response) + } - let success = req + /// Execute `TxnRequest` + fn execute_txn( + &self, + txn_db: &Transaction, + index: &mut dyn IndexOperate, + request: &TxnRequest, + revision: i64, + sub_revision: &mut i64, + ) -> Result { + let success = request .compare .iter() - .all(|compare| self.check_compare(compare)); + .all(|compare| Self::check_compare(txn_db, index, compare)); + tracing::warn!("txn success in execute: {success}"); let requests = if success { - req.success.iter() + request.success.iter() } else { - req.failure.iter() + request.failure.iter() }; - let mut responses = Vec::with_capacity(requests.len()); - for request_op in requests { - let response = self.handle_kv_requests(&request_op.clone().into())?; - responses.push(response.into()); - } + + let responses = requests + .filter_map(|op| op.request.as_ref()) + .map(|req| match *req { + Request::RequestRange(ref r) => { + self.execute_range(txn_db, index, r).map(Into::into) + } + Request::RequestTxn(ref r) => self + .execute_txn(txn_db, index, r, revision, sub_revision) + .map(Into::into), + Request::RequestPut(ref r) => self + .execute_txn_put(txn_db, index, r, revision, sub_revision) + .map(Into::into), + Request::RequestDeleteRange(ref r) => self + .execute_txn_delete_range(txn_db, index, r, revision, sub_revision) + .map(Into::into), + }) + .collect::, _>>()?; + Ok(TxnResponse { header: Some(self.header_gen.gen_header()), succeeded: success, - responses, + responses: responses.into_iter().map(Into::into).collect(), }) } - /// Handle `CompactionRequest` - fn handle_compaction_request( + /// Execute `CompactionRequest` + fn execute_compaction( &self, req: &CompactionRequest, ) -> Result { @@ -677,110 +876,102 @@ impl KvStore { header: Some(self.header_gen.gen_header()), }) } +} - /// Sync requests in kv store - async fn sync_request( +/// Sync requests +impl KvStore { + /// Sync kv requests + fn sync_request( &self, wrapper: &RequestWrapper, - revision: i64, - ) -> Result<(i64, Vec), ExecuteError> { - debug!("After Sync {:?} with revision {}", wrapper, revision); - #[allow(clippy::wildcard_enum_match_arm)] // only kv requests can be sent to kv store - let (ops, events) = match *wrapper { - RequestWrapper::RangeRequest(_) => (Vec::new(), Vec::new()), - RequestWrapper::PutRequest(ref req) => self.sync_put_request(req, revision, 0)?, - RequestWrapper::DeleteRangeRequest(ref req) => { - self.sync_delete_range_request(req, revision, 0) + txn_db: &T, + index: &(dyn IndexOperate + Send + Sync), + revision_gen: &RevisionNumberGeneratorState<'_>, + to_execute: bool, + ) -> Result<(SyncResponse, Option), ExecuteError> + where + T: XlineStorageOps + TransactionApi, + { + debug!("Execute {:?}", wrapper); + warn!("after sync: {wrapper:?}"); + + let next_revision = revision_gen.get().overflow_add(1); + + #[allow(clippy::wildcard_enum_match_arm)] + let (events, execute_response): (_, Option) = match *wrapper { + RequestWrapper::RangeRequest(ref req) => { + self.sync_range(txn_db, index, req, to_execute) } - RequestWrapper::TxnRequest(ref req) => self.sync_txn_request(req, revision)?, - RequestWrapper::CompactionRequest(ref req) => { - self.sync_compaction_request(req, revision).await? + RequestWrapper::PutRequest(ref req) => { + self.sync_put(txn_db, index, req, next_revision, &mut 0, to_execute) + } + RequestWrapper::DeleteRangeRequest(ref req) => { + self.sync_delete_range(txn_db, index, req, next_revision, &mut 0, to_execute) } - _ => { - unreachable!("only kv requests can be sent to kv store"); + RequestWrapper::TxnRequest(ref req) => { + self.sync_txn(txn_db, index, req, next_revision, &mut 0, to_execute) } - }; - self.notify_updates(revision, events).await; - Ok((revision, ops)) - } + RequestWrapper::CompactionRequest(ref req) => self.sync_compaction(req, to_execute), + _ => unreachable!("Other request should not be sent to this store"), + }?; - /// Sync `CompactionRequest` and return if kvstore is changed - async fn sync_compaction_request( - &self, - req: &CompactionRequest, - _revision: i64, - ) -> Result<(Vec, Vec), ExecuteError> { - let revision = req.revision; - let ops = vec![WriteOp::PutScheduledCompactRevision(revision)]; - // TODO: Remove the physical process logic here. It's better to move into the KvServer - let (event, listener) = if req.physical { - let event = Arc::new(event_listener::Event::new()); - let listener = event.listen(); - (Some(event), Some(listener)) + let sync_response = if events.is_empty() { + SyncResponse::new(revision_gen.get()) } else { - (None, None) + self.notify_updates(next_revision, events); + SyncResponse::new(revision_gen.next()) }; - if let Err(e) = self.compact_task_tx.send((revision, event)).await { - panic!("the compactor exited unexpectedly: {e:?}"); - } - if let Some(listener) = listener { - listener.await; - } - Ok((ops, Vec::new())) + + tracing::warn!("sync response: {sync_response:?}"); + + Ok((sync_response, execute_response.map(CommandResponse::new))) } - /// Sync `TxnRequest` and return if kvstore is changed - fn sync_txn_request( + /// Sync `RangeRequest` + fn sync_range( &self, - req: &TxnRequest, - revision: i64, - ) -> Result<(Vec, Vec), ExecuteError> { - let mut sub_revision = 0; - let mut origin_reqs = VecDeque::from([Request::RequestTxn(req.clone())]); - let mut all_events = Vec::new(); - let mut all_ops = Vec::new(); - while let Some(request) = origin_reqs.pop_front() { - let (mut ops, mut events) = match request { - Request::RequestRange(_) => (Vec::new(), Vec::new()), - Request::RequestPut(ref put_req) => { - self.sync_put_request(put_req, revision, sub_revision)? - } - Request::RequestDeleteRange(del_req) => { - self.sync_delete_range_request(&del_req, revision, sub_revision) - } - Request::RequestTxn(txn_req) => { - let success = txn_req - .compare - .iter() - .all(|compare| self.check_compare(compare)); - let reqs_iter = if success { - txn_req.success.into_iter() - } else { - txn_req.failure.into_iter() - }; - origin_reqs.extend(reqs_iter.filter_map(|req_op| req_op.request)); - continue; - } - }; - sub_revision = sub_revision.overflow_add(events.len().numeric_cast()); - all_events.append(&mut events); - all_ops.append(&mut ops); - } - Ok((all_ops, all_events)) + txn_db: &T, + index: &dyn IndexOperate, + req: &RangeRequest, + to_execute: bool, + ) -> Result<(Vec, Option), ExecuteError> + where + T: XlineStorageOps, + { + Ok(( + vec![], + to_execute + .then(|| self.execute_range(txn_db, index, req).map(Into::into)) + .transpose()?, + )) } - /// Sync `PutRequest` and return if kvstore is changed - fn sync_put_request( + /// Sync `PutRequest` + fn sync_put( &self, + txn_db: &T, + index: &dyn IndexOperate, req: &PutRequest, revision: i64, - sub_revision: i64, - ) -> Result<(Vec, Vec), ExecuteError> { - let mut ops = Vec::new(); - let new_rev = self - .inner - .index - .register_revision(&req.key, revision, sub_revision); + sub_revision: &mut i64, + to_execute: bool, + ) -> Result<(Vec, Option), ExecuteError> + where + T: XlineStorageOps, + { + let (new_rev, prev_rev_opt) = + index.register_revision(req.key.clone(), revision, *sub_revision); + let execute_resp = to_execute + .then(|| { + self.generate_put_resp( + req, + txn_db, + prev_rev_opt.map(|key_rev| key_rev.as_revision()), + ) + .map(|(resp, _)| resp.into()) + }) + .transpose()?; + let mut kv = KeyValue { key: req.key.clone(), value: req.value.clone(), @@ -789,8 +980,12 @@ impl KvStore { version: new_rev.version, lease: req.lease, }; + if req.ignore_lease || req.ignore_value { - let prev_kv = self.inner.get_range(&req.key, &[], 0)?.pop(); + let prev_rev = prev_rev_opt + .map(|key_rev| key_rev.as_revision()) + .ok_or(ExecuteError::KeyNotFound)?; + let prev_kv = KvStoreInner::get_values(txn_db, &[prev_rev])?.pop(); let prev = prev_kv.as_ref().ok_or(ExecuteError::KeyNotFound)?; if req.ignore_lease { kv.lease = prev.lease; @@ -798,7 +993,7 @@ impl KvStore { if req.ignore_value { kv.value = prev.value.clone(); } - } + }; let old_lease = self.get_lease(&kv.key); if old_lease != 0 { @@ -807,20 +1002,155 @@ impl KvStore { } if req.lease != 0 { self.attach(req.lease, kv.key.as_slice()) - .unwrap_or_else(|e| panic!("unexpected error from lease Attach: {e}")); + .unwrap_or_else(|e| warn!("unexpected error from lease Attach: {e}")); } - ops.push(WriteOp::PutKeyValue(new_rev.as_revision(), kv.clone())); - let event = Event { + + txn_db.write_op(WriteOp::PutKeyValue(new_rev.as_revision(), kv.clone()))?; + *sub_revision = sub_revision.overflow_add(1); + + let events = vec![Event { #[allow(clippy::as_conversions)] // This cast is always valid r#type: EventType::Put as i32, kv: Some(kv), prev_kv: None, + }]; + + Ok((events, execute_resp)) + } + + /// Sync `DeleteRangeRequest` + fn sync_delete_range( + &self, + txn_db: &T, + index: &dyn IndexOperate, + req: &DeleteRangeRequest, + revision: i64, + sub_revision: &mut i64, + to_execute: bool, + ) -> Result<(Vec, Option), ExecuteError> + where + T: XlineStorageOps, + { + let execute_resp = to_execute + .then(|| self.generate_delete_range_resp(req, txn_db, index)) + .transpose()? + .map(Into::into); + + let keys = Self::delete_keys( + txn_db, + index, + &req.key, + &req.range_end, + revision, + sub_revision, + )?; + + Self::detach_leases(&keys, &self.lease_collection); + + Ok((Self::new_deletion_events(revision, keys), execute_resp)) + } + + /// Sync `TxnRequest` + fn sync_txn( + &self, + txn_db: &T, + index: &dyn IndexOperate, + request: &TxnRequest, + revision: i64, + sub_revision: &mut i64, + to_execute: bool, + ) -> Result<(Vec, Option), ExecuteError> + where + T: XlineStorageOps, + { + request.check_revision(self.compacted_revision(), self.revision())?; + let success = request + .compare + .iter() + .all(|compare| Self::check_compare(txn_db, index, compare)); + tracing::warn!("txn success: {success}"); + let requests = if success { + request.success.iter() + } else { + request.failure.iter() }; - Ok((ops, vec![event])) + + let (events, resps): (Vec<_>, Vec<_>) = requests + .filter_map(|op| op.request.as_ref()) + .map(|req| match *req { + Request::RequestRange(ref r) => self.sync_range(txn_db, index, r, to_execute), + Request::RequestTxn(ref r) => { + self.sync_txn(txn_db, index, r, revision, sub_revision, to_execute) + } + Request::RequestPut(ref r) => { + self.sync_put(txn_db, index, r, revision, sub_revision, to_execute) + } + Request::RequestDeleteRange(ref r) => { + self.sync_delete_range(txn_db, index, r, revision, sub_revision, to_execute) + } + }) + .collect::, _>>()? + .into_iter() + .unzip(); + + let resp = to_execute.then(|| { + TxnResponse { + header: Some(self.header_gen.gen_header()), + succeeded: success, + responses: resps + .into_iter() + .flat_map(Option::into_iter) + .map(Into::into) + .collect(), + } + .into() + }); + + Ok((events.into_iter().flatten().collect(), resp)) + } + + /// Sync `CompactionRequest` and return if kvstore is changed + fn sync_compaction( + &self, + req: &CompactionRequest, + to_execute: bool, + ) -> Result<(Vec, Option), ExecuteError> { + let revision = req.revision; + let ops = vec![WriteOp::PutScheduledCompactRevision(revision)]; + // TODO: Remove the physical process logic here. It's better to move into the + // KvServer + // FIXME: madsim is single threaded, we cannot use synchronous wait here + let index = self.index(); + let target_revisions = index + .compact(revision) + .into_iter() + .map(|key_rev| key_rev.as_revision().encode_to_vec()) + .collect::>>(); + // Given that the Xline uses a lim-tree database with smaller write amplification as the storage backend , does using progressive compaction really good at improving performance? + for revision_chunk in target_revisions.chunks(1000) { + if let Err(e) = self.compact(revision_chunk) { + panic!("failed to compact revision chunk {revision_chunk:?} due to {e}"); + } + } + if let Err(e) = self.compact_finished(revision) { + panic!("failed to set finished compact revision {revision:?} due to {e}"); + } + + self.inner.db.write_ops(ops)?; + + let resp = to_execute + .then(|| CompactionResponse { + header: Some(self.header_gen.gen_header()), + }) + .map(Into::into); + + Ok((vec![], resp)) } +} +impl KvStore { /// create events for a deletion - fn new_deletion_events(revision: i64, keys: Vec>) -> Vec { + pub(crate) fn new_deletion_events(revision: i64, keys: Vec>) -> Vec { keys.into_iter() .map(|key| { let kv = KeyValue { @@ -842,7 +1172,7 @@ impl KvStore { fn mark_deletions<'a>( revisions: &[(Revision, Revision)], keys: &[Vec], - ) -> Vec> { + ) -> (Vec>, Vec<(Vec, KeyRevision)>) { assert_eq!(keys.len(), revisions.len(), "Index doesn't match with DB"); keys.iter() .zip(revisions.iter()) @@ -852,55 +1182,67 @@ impl KvStore { mod_revision: new_rev.revision(), ..KeyValue::default() }; - WriteOp::PutKeyValue(new_rev, del_kv) - }) - .collect() - } - /// Sync `DeleteRangeRequest` and return if kvstore is changed - fn sync_delete_range_request( - &self, - req: &DeleteRangeRequest, - revision: i64, - sub_revision: i64, - ) -> (Vec, Vec) { - Self::delete_keys( - &self.inner.index, - &self.lease_collection, - &req.key, - &req.range_end, - revision, - sub_revision, - ) + let key_revision = ( + del_kv.key.clone(), + KeyRevision::new( + del_kv.create_revision, + del_kv.version, + new_rev.revision(), + new_rev.sub_revision(), + ), + ); + (WriteOp::PutKeyValue(new_rev, del_kv), key_revision) + }) + .unzip() } - /// Delete keys from index and detach them in lease collection, return all the write operations and events - pub(crate) fn delete_keys<'a>( - index: &Index, - lease_collection: &LeaseCollection, + /// Delete keys from index and detach them in lease collection, return all + /// the write operations and events + pub(crate) fn delete_keys( + txn_db: &T, + index: &dyn IndexOperate, key: &[u8], range_end: &[u8], revision: i64, - sub_revision: i64, - ) -> (Vec>, Vec) { - let mut ops = Vec::new(); - let (revisions, keys) = index.delete(key, range_end, revision, sub_revision); - let mut del_ops = Self::mark_deletions(&revisions, &keys); - ops.append(&mut del_ops); - for k in &keys { + sub_revision: &mut i64, + ) -> Result>, ExecuteError> + where + T: XlineStorageOps, + { + let (revisions, keys) = index.delete(key, range_end, revision, *sub_revision); + let (del_ops, key_revisions) = Self::mark_deletions(&revisions, &keys); + + index.insert(key_revisions); + + *sub_revision = sub_revision.overflow_add(del_ops.len().numeric_cast()); + for op in del_ops { + txn_db.write_op(op)?; + } + + Ok(keys) + } + + /// Detaches the leases + pub(crate) fn detach_leases(keys: &[Vec], lease_collection: &LeaseCollection) { + for k in keys { let lease_id = lease_collection.get_lease(k); lease_collection .detach(lease_id, k) .unwrap_or_else(|e| warn!("Failed to detach lease from a key, error: {:?}", e)); } - let events = Self::new_deletion_events(revision, keys); - (ops, events) + } +} + +impl KvStore { + /// Gets the index + pub(crate) fn index(&self) -> Arc { + Arc::clone(&self.inner.index) } - /// Insert the given pairs (key, `KeyRevision`) into the index - #[inline] - pub(crate) fn insert_index(&self, key_revisions: Vec<(Vec, KeyRevision)>) { - self.inner.index.insert(key_revisions); + /// Gets the general revision generator + pub(crate) fn revision_gen(&self) -> Arc { + Arc::clone(&self.revision) } } @@ -965,9 +1307,7 @@ mod test { } } - async fn init_store( - db: Arc, - ) -> Result<(StoreWrapper, RevisionNumberGenerator), ExecuteError> { + fn init_store(db: Arc) -> Result<(StoreWrapper, RevisionNumberGenerator), ExecuteError> { let store = init_empty_store(db); let keys = vec!["a", "b", "c", "d", "e", "z", "z", "z"]; let vals = vec!["a", "b", "c", "d", "e", "z1", "z2", "z3"]; @@ -978,15 +1318,15 @@ mod test { value: val.into(), ..Default::default() }); - exe_as_and_flush(&store, &req, revision.next()).await?; + exe_as_and_flush(&store, &req)?; } Ok((store, revision)) } fn init_empty_store(db: Arc) -> StoreWrapper { let task_manager = Arc::new(TaskManager::new()); - let (compact_tx, compact_rx) = mpsc::channel(COMPACT_CHANNEL_SIZE); - let (kv_update_tx, kv_update_rx) = mpsc::channel(CHANNEL_SIZE); + let (compact_tx, compact_rx) = flume::bounded(COMPACT_CHANNEL_SIZE); + let (kv_update_tx, kv_update_rx) = flume::bounded(CHANNEL_SIZE); let lease_collection = Arc::new(LeaseCollection::new(0)); let header_gen = Arc::new(HeaderGenerator::new(0, 0)); let index = Arc::new(Index::new()); @@ -1017,14 +1357,18 @@ mod test { StoreWrapper(Some(storage), task_manager) } - async fn exe_as_and_flush( + fn exe_as_and_flush( store: &Arc, request: &RequestWrapper, - revision: i64, ) -> Result<(), ExecuteError> { - let (_sync_res, ops) = store.after_sync(request, revision).await?; - let key_revs = store.inner.db.flush_ops(ops)?; - store.insert_index(key_revs); + let txn_db = store.db().transaction(); + let index = store.index(); + let index_state = index.state(); + let rev_gen_state = store.revision.state(); + let _res = store.after_sync(request, &txn_db, &index_state, &rev_gen_state, false)?; + txn_db.commit().unwrap(); + index_state.commit(); + rev_gen_state.commit(); Ok(()) } @@ -1042,14 +1386,16 @@ mod test { #[abort_on_panic] async fn test_keys_only() -> Result<(), ExecuteError> { let db = DB::open(&EngineConfig::Memory)?; - let (store, _rev) = init_store(db).await?; + let (store, _rev) = init_store(db)?; let request = RangeRequest { key: vec![0], range_end: vec![0], keys_only: true, ..Default::default() }; - let response = store.handle_range_request(&request)?; + let txn_db = store.inner.db.transaction(); + let index = store.inner.index.state(); + let response = store.execute_range(&txn_db, &index, &request)?; assert_eq!(response.kvs.len(), 6); for kv in response.kvs { assert!(kv.value.is_empty()); @@ -1061,7 +1407,7 @@ mod test { #[abort_on_panic] async fn test_range_empty() -> Result<(), ExecuteError> { let db = DB::open(&EngineConfig::Memory)?; - let (store, _rev) = init_store(db).await?; + let (store, _rev) = init_store(db)?; let request = RangeRequest { key: "x".into(), @@ -1069,7 +1415,9 @@ mod test { keys_only: true, ..Default::default() }; - let response = store.handle_range_request(&request)?; + let txn_db = store.inner.db.transaction(); + let index = store.inner.index.state(); + let response = store.execute_range(&txn_db, &index, &request)?; assert_eq!(response.kvs.len(), 0); assert_eq!(response.count, 0); Ok(()) @@ -1079,7 +1427,7 @@ mod test { #[abort_on_panic] async fn test_range_filter() -> Result<(), ExecuteError> { let db = DB::open(&EngineConfig::Memory)?; - let (store, _rev) = init_store(db).await?; + let (store, _rev) = init_store(db)?; let request = RangeRequest { key: vec![0], @@ -1090,7 +1438,9 @@ mod test { min_mod_revision: 2, ..Default::default() }; - let response = store.handle_range_request(&request)?; + let txn_db = store.inner.db.transaction(); + let index = store.inner.index.state(); + let response = store.execute_range(&txn_db, &index, &request)?; assert_eq!(response.count, 6); assert_eq!(response.kvs.len(), 2); assert_eq!(response.kvs[0].create_revision, 2); @@ -1102,7 +1452,7 @@ mod test { #[abort_on_panic] async fn test_range_sort() -> Result<(), ExecuteError> { let db = DB::open(&EngineConfig::Memory)?; - let (store, _rev) = init_store(db).await?; + let (store, _rev) = init_store(db)?; let keys = ["a", "b", "c", "d", "e", "z"]; let reversed_keys = ["z", "e", "d", "c", "b", "a"]; let version_keys = ["z", "a", "b", "c", "d", "e"]; @@ -1114,7 +1464,9 @@ mod test { SortTarget::Mod, SortTarget::Value, ] { - let response = store.handle_range_request(&sort_req(order, target))?; + let txn_db = store.inner.db.transaction(); + let index = store.inner.index.state(); + let response = store.execute_range(&txn_db, &index, &sort_req(order, target))?; assert_eq!(response.count, 6); assert_eq!(response.kvs.len(), 6); let expected: [&str; 6] = match order { @@ -1135,7 +1487,10 @@ mod test { } } for order in [SortOrder::Ascend, SortOrder::Descend, SortOrder::None] { - let response = store.handle_range_request(&sort_req(order, SortTarget::Version))?; + let txn_db = store.inner.db.transaction(); + let index = store.inner.index.state(); + let response = + store.execute_range(&txn_db, &index, &sort_req(order, SortTarget::Version))?; assert_eq!(response.count, 6); assert_eq!(response.kvs.len(), 6); let expected = match order { @@ -1161,8 +1516,8 @@ mod test { async fn test_recover() -> Result<(), ExecuteError> { let db = DB::open(&EngineConfig::Memory)?; let ops = vec![WriteOp::PutScheduledCompactRevision(8)]; - db.flush_ops(ops)?; - let (store, _rev_gen) = init_store(Arc::clone(&db)).await?; + db.write_ops(ops)?; + let (store, _rev_gen) = init_store(Arc::clone(&db))?; assert_eq!(store.inner.index.get_from_rev(b"z", b"", 5).len(), 3); let new_store = init_empty_store(db); @@ -1172,13 +1527,18 @@ mod test { range_end: vec![], ..Default::default() }; - let res = new_store.handle_range_request(&range_req)?; + + let txn_db = new_store.inner.db.transaction(); + let index = new_store.inner.index.state(); + let res = new_store.execute_range(&txn_db, &index, &range_req)?; assert_eq!(res.kvs.len(), 0); assert_eq!(new_store.compacted_revision(), -1); new_store.recover().await?; - let res = new_store.handle_range_request(&range_req)?; + let txn_db_recovered = new_store.inner.db.transaction(); + let index_recovered = new_store.inner.index.state(); + let res = store.execute_range(&txn_db_recovered, &index_recovered, &range_req)?; assert_eq!(res.kvs.len(), 1); assert_eq!(res.kvs[0].key, b"a"); assert_eq!(new_store.compacted_revision(), 8); @@ -1227,14 +1587,17 @@ mod test { }], }); let db = DB::open(&EngineConfig::Memory)?; - let (store, rev) = init_store(db).await?; - exe_as_and_flush(&store, &txn_req, rev.next()).await?; + let (store, _rev) = init_store(db)?; + exe_as_and_flush(&store, &txn_req)?; let request = RangeRequest { key: "success".into(), range_end: vec![], ..Default::default() }; - let response = store.handle_range_request(&request)?; + + let txn_db = store.inner.db.transaction(); + let index = store.inner.index.state(); + let response = store.execute_range(&txn_db, &index, &request)?; assert_eq!(response.count, 1); assert_eq!(response.kvs.len(), 1); assert_eq!(response.kvs[0].value, "1".as_bytes()); @@ -1246,7 +1609,7 @@ mod test { #[abort_on_panic] async fn test_kv_store_index_available() { let db = DB::open(&EngineConfig::Memory).unwrap(); - let (store, revision) = init_store(Arc::clone(&db)).await.unwrap(); + let (store, _revision) = init_store(Arc::clone(&db)).unwrap(); let handle = tokio::spawn({ let store = Arc::clone(&store); async move { @@ -1256,15 +1619,13 @@ mod test { value: vec![i], ..Default::default() }); - exe_as_and_flush(&store, &req, revision.next()) - .await - .unwrap(); + exe_as_and_flush(&store, &req).unwrap(); } } }); tokio::time::sleep(std::time::Duration::from_micros(50)).await; let revs = store.inner.index.get_from_rev(b"foo", b"", 1); - let kvs = store.inner.get_values(&revs).unwrap(); + let kvs = KvStoreInner::get_values(&db.transaction(), &revs).unwrap(); assert_eq!( kvs.len(), revs.len(), @@ -1274,10 +1635,10 @@ mod test { } #[tokio::test(flavor = "multi_thread")] + #[allow(clippy::too_many_lines)] // TODO: splits this test async fn test_compaction() -> Result<(), ExecuteError> { let db = DB::open(&EngineConfig::Memory)?; let store = init_empty_store(db); - let revision = RevisionNumberGenerator::default(); // sample requests: (a, 1) (b, 2) (a, 3) (del a) // their revisions: 2 3 4 5 let requests = vec![ @@ -1303,20 +1664,25 @@ mod test { ]; for req in requests { - exe_as_and_flush(&store, &req, revision.next()) - .await - .unwrap(); + exe_as_and_flush(&store, &req).unwrap(); } let target_revisions = index_compact(&store, 3); store.compact(target_revisions.as_ref())?; + + let txn_db = store.inner.db.transaction(); + let index = store.inner.index.state(); assert_eq!( - store.inner.get_range(b"a", b"", 2).unwrap().len(), + KvStoreInner::get_range(&txn_db, &index, b"a", b"", 2) + .unwrap() + .len(), 1, "(a, 1) should not be removed" ); assert_eq!( - store.inner.get_range(b"b", b"", 3).unwrap().len(), + KvStoreInner::get_range(&txn_db, &index, b"b", b"", 3) + .unwrap() + .len(), 1, "(b, 2) should not be removed" ); @@ -1324,16 +1690,22 @@ mod test { let target_revisions = index_compact(&store, 4); store.compact(target_revisions.as_ref())?; assert!( - store.inner.get_range(b"a", b"", 2).unwrap().is_empty(), + KvStoreInner::get_range(&txn_db, &index, b"a", b"", 2) + .unwrap() + .is_empty(), "(a, 1) should be removed" ); assert_eq!( - store.inner.get_range(b"b", b"", 3).unwrap().len(), + KvStoreInner::get_range(&txn_db, &index, b"b", b"", 3) + .unwrap() + .len(), 1, "(b, 2) should not be removed" ); assert_eq!( - store.inner.get_range(b"a", b"", 4).unwrap().len(), + KvStoreInner::get_range(&txn_db, &index, b"a", b"", 4) + .unwrap() + .len(), 1, "(a, 3) should not be removed" ); @@ -1341,20 +1713,28 @@ mod test { let target_revisions = index_compact(&store, 5); store.compact(target_revisions.as_ref())?; assert!( - store.inner.get_range(b"a", b"", 2).unwrap().is_empty(), + KvStoreInner::get_range(&txn_db, &index, b"a", b"", 2) + .unwrap() + .is_empty(), "(a, 1) should be removed" ); assert_eq!( - store.inner.get_range(b"b", b"", 3).unwrap().len(), + KvStoreInner::get_range(&txn_db, &index, b"b", b"", 3) + .unwrap() + .len(), 1, "(b, 2) should not be removed" ); assert!( - store.inner.get_range(b"a", b"", 4).unwrap().is_empty(), + KvStoreInner::get_range(&txn_db, &index, b"a", b"", 4) + .unwrap() + .is_empty(), "(a, 3) should be removed" ); assert!( - store.inner.get_range(b"a", b"", 5).unwrap().is_empty(), + KvStoreInner::get_range(&txn_db, &index, b"a", b"", 5) + .unwrap() + .is_empty(), "(a, 4) should be removed" ); diff --git a/crates/xline/src/storage/kvwatcher.rs b/crates/xline/src/storage/kvwatcher.rs index 496abfd1a..6b8524b56 100644 --- a/crates/xline/src/storage/kvwatcher.rs +++ b/crates/xline/src/storage/kvwatcher.rs @@ -383,7 +383,7 @@ impl KvWatcher { /// Create a new `Arc` pub(crate) fn new_arc( kv_store_inner: Arc, - kv_update_rx: mpsc::Receiver<(i64, Vec)>, + kv_update_rx: flume::Receiver<(i64, Vec)>, sync_victims_interval: Duration, task_manager: &TaskManager, ) -> Arc { @@ -405,13 +405,13 @@ impl KvWatcher { #[allow(clippy::arithmetic_side_effects, clippy::ignored_unit_patterns)] // Introduced by tokio::select! async fn kv_updates_task( kv_watcher: Arc, - mut kv_update_rx: mpsc::Receiver<(i64, Vec)>, + kv_update_rx: flume::Receiver<(i64, Vec)>, shutdown_listener: Listener, ) { loop { tokio::select! { - updates = kv_update_rx.recv() => { - let Some(updates) = updates else { + updates = kv_update_rx.recv_async() => { + let Ok(updates) = updates else { return; }; kv_watcher.handle_kv_updates(updates); @@ -592,7 +592,7 @@ mod test { use std::{collections::BTreeMap, time::Duration}; - use clippy_utilities::{NumericCast, OverflowArithmetic}; + use engine::TransactionApi; use test_macros::abort_on_panic; use tokio::time::{sleep, timeout}; use utils::config::EngineConfig; @@ -608,14 +608,14 @@ mod test { }, }; - fn init_empty_store(task_manager: &TaskManager) -> (Arc, Arc, Arc) { - let (compact_tx, _compact_rx) = mpsc::channel(COMPACT_CHANNEL_SIZE); + fn init_empty_store(task_manager: &TaskManager) -> (Arc, Arc) { + let (compact_tx, _compact_rx) = flume::bounded(COMPACT_CHANNEL_SIZE); let db = DB::open(&EngineConfig::Memory).unwrap(); let header_gen = Arc::new(HeaderGenerator::new(0, 0)); let index = Arc::new(Index::new()); let lease_collection = Arc::new(LeaseCollection::new(0)); - let (kv_update_tx, kv_update_rx) = mpsc::channel(128); - let kv_store_inner = Arc::new(KvStoreInner::new(index, Arc::clone(&db))); + let (kv_update_tx, kv_update_rx) = flume::bounded(128); + let kv_store_inner = Arc::new(KvStoreInner::new(index, db)); let store = Arc::new(KvStore::new( Arc::clone(&kv_store_inner), header_gen, @@ -630,14 +630,14 @@ mod test { sync_victims_interval, task_manager, ); - (store, db, kv_watcher) + (store, kv_watcher) } #[tokio::test(flavor = "multi_thread")] #[abort_on_panic] async fn watch_should_not_lost_events() { let task_manager = Arc::new(TaskManager::new()); - let (store, db, kv_watcher) = init_empty_store(&task_manager); + let (store, kv_watcher) = init_empty_store(&task_manager); let mut map = BTreeMap::new(); let (event_tx, mut event_rx) = mpsc::channel(128); let stop_notify = Arc::new(event_listener::Event::new()); @@ -654,14 +654,7 @@ mod test { let store = Arc::clone(&store); async move { for i in 0..100_u8 { - put( - store.as_ref(), - db.as_ref(), - "foo", - vec![i], - i.overflow_add(2).numeric_cast(), - ) - .await; + put(store.as_ref(), "foo", vec![i]); } } }); @@ -694,7 +687,7 @@ mod test { #[abort_on_panic] async fn test_victim() { let task_manager = Arc::new(TaskManager::new()); - let (store, db, kv_watcher) = init_empty_store(&task_manager); + let (store, kv_watcher) = init_empty_store(&task_manager); // response channel with capacity 1, so it will be full easily, then we can trigger victim let (event_tx, mut event_rx) = mpsc::channel(1); let stop_notify = Arc::new(event_listener::Event::new()); @@ -723,14 +716,7 @@ mod test { }); for i in 0..100_u8 { - put( - store.as_ref(), - db.as_ref(), - "foo", - vec![i], - i.numeric_cast(), - ) - .await; + put(store.as_ref(), "foo", vec![i]); } handle.await.unwrap(); drop(store); @@ -741,7 +727,7 @@ mod test { #[abort_on_panic] async fn test_cancel_watcher() { let task_manager = Arc::new(TaskManager::new()); - let (store, _db, kv_watcher) = init_empty_store(&task_manager); + let (store, kv_watcher) = init_empty_store(&task_manager); let (event_tx, _event_rx) = mpsc::channel(1); let stop_notify = Arc::new(event_listener::Event::new()); kv_watcher.watch( @@ -761,20 +747,22 @@ mod test { task_manager.shutdown(true).await; } - async fn put( - store: &KvStore, - db: &DB, - key: impl Into>, - value: impl Into>, - revision: i64, - ) { + fn put(store: &KvStore, key: impl Into>, value: impl Into>) { let req = RequestWrapper::from(PutRequest { key: key.into(), value: value.into(), ..Default::default() }); - let (_sync_res, ops) = store.after_sync(&req, revision).await.unwrap(); - let key_revisions = db.flush_ops(ops).unwrap(); - store.insert_index(key_revisions); + let txn = store.db().transaction(); + let index = store.index(); + let index_state = index.state(); + let rev_gen = store.revision_gen(); + let rev_gen_state = rev_gen.state(); + store + .after_sync(&req, &txn, &index_state, &rev_gen_state, false) + .unwrap(); + txn.commit().unwrap(); + index_state.commit(); + rev_gen_state.commit(); } } diff --git a/crates/xline/src/storage/lease_store/lease.rs b/crates/xline/src/storage/lease_store/lease.rs index 9274d85c9..36cb38743 100644 --- a/crates/xline/src/storage/lease_store/lease.rs +++ b/crates/xline/src/storage/lease_store/lease.rs @@ -36,6 +36,11 @@ impl Lease { self.keys_set.iter().cloned().collect() } + /// Convert into keys + pub(crate) fn into_keys(mut self) -> Vec> { + self.keys_set.drain().collect() + } + /// Lease id pub(crate) fn id(&self) -> i64 { self.id diff --git a/crates/xline/src/storage/lease_store/lease_collection.rs b/crates/xline/src/storage/lease_store/lease_collection.rs index 7a64698af..f2301d5d0 100644 --- a/crates/xline/src/storage/lease_store/lease_collection.rs +++ b/crates/xline/src/storage/lease_store/lease_collection.rs @@ -1,5 +1,6 @@ use std::{ - collections::HashMap, + collections::{BTreeMap, HashMap}, + ops::RangeBounds, time::{Duration, Instant}, }; @@ -14,6 +15,7 @@ use crate::rpc::PbLease; /// Collection of lease related data #[derive(Debug)] +#[cfg_attr(test, derive(Default))] pub(crate) struct LeaseCollection { /// Inner data of `LeaseCollection` inner: RwLock, @@ -22,12 +24,13 @@ pub(crate) struct LeaseCollection { } #[derive(Debug)] +#[cfg_attr(test, derive(Default))] /// Inner data of `LeaseCollection` struct LeaseCollectionInner { /// lease id to lease lease_map: HashMap, /// key to lease id - item_map: HashMap, i64>, + item_map: BTreeMap, i64>, /// lease queue expired_queue: LeaseQueue, } @@ -38,7 +41,7 @@ impl LeaseCollection { Self { inner: RwLock::new(LeaseCollectionInner { lease_map: HashMap::new(), - item_map: HashMap::new(), + item_map: BTreeMap::new(), expired_queue: LeaseQueue::new(), }), min_ttl, @@ -108,6 +111,19 @@ impl LeaseCollection { self.inner.read().item_map.get(key).copied().unwrap_or(0) } + /// Get lease id by given key + pub(crate) fn get_lease_by_range(&self, range: R) -> Vec + where + R: RangeBounds>, + { + self.inner + .read() + .item_map + .range(range) + .map(|(_, lease)| *lease) + .collect() + } + /// Get Lease by lease id pub(crate) fn look_up(&self, lease_id: i64) -> Option { self.inner.read().lease_map.get(&lease_id).cloned() diff --git a/crates/xline/src/storage/lease_store/lease_queue.rs b/crates/xline/src/storage/lease_store/lease_queue.rs index 95fcc267f..cfe2f0f38 100644 --- a/crates/xline/src/storage/lease_store/lease_queue.rs +++ b/crates/xline/src/storage/lease_store/lease_queue.rs @@ -4,6 +4,7 @@ use priority_queue::PriorityQueue; /// Priority queue of lease #[derive(Debug)] +#[cfg_attr(test, derive(Default))] pub(super) struct LeaseQueue { /// Inner queue of lease queue inner: PriorityQueue>, diff --git a/crates/xline/src/storage/lease_store/mod.rs b/crates/xline/src/storage/lease_store/mod.rs index f6adc2f0b..b9fd0f52e 100644 --- a/crates/xline/src/storage/lease_store/mod.rs +++ b/crates/xline/src/storage/lease_store/mod.rs @@ -16,10 +16,11 @@ use std::{ time::Duration, }; +use clippy_utilities::OverflowArithmetic; +use engine::TransactionApi; use log::debug; use parking_lot::RwLock; use prost::Message; -use tokio::sync::mpsc; use utils::table_names::LEASE_TABLE; use xlineapi::{ command::{CommandResponse, SyncResponse}, @@ -29,10 +30,12 @@ use xlineapi::{ pub(crate) use self::{lease::Lease, lease_collection::LeaseCollection}; use super::{ db::{WriteOp, DB}, - index::Index, + index::IndexOperate, + storage_api::XlineStorageOps, }; use crate::{ header_gen::HeaderGenerator, + revision_number::RevisionNumberGeneratorState, rpc::{ Event, LeaseGrantRequest, LeaseGrantResponse, LeaseLeasesRequest, LeaseLeasesResponse, LeaseRevokeRequest, LeaseRevokeResponse, LeaseStatus, PbLease, RequestWrapper, @@ -51,12 +54,10 @@ pub(crate) struct LeaseStore { lease_collection: Arc, /// Db to store lease db: Arc, - /// Key to revision index - index: Arc, /// Header generator header_gen: Arc, /// KV update sender - kv_update_tx: mpsc::Sender<(i64, Vec)>, + kv_update_tx: flume::Sender<(i64, Vec)>, /// Primary flag is_primary: AtomicBool, /// cache unsynced lease id @@ -71,14 +72,12 @@ impl LeaseStore { lease_collection: Arc, header_gen: Arc, db: Arc, - index: Arc, - kv_update_tx: mpsc::Sender<(i64, Vec)>, + kv_update_tx: flume::Sender<(i64, Vec)>, is_leader: bool, ) -> Self { Self { lease_collection, db, - index, header_gen, kv_update_tx, is_primary: AtomicBool::new(is_leader), @@ -97,14 +96,26 @@ impl LeaseStore { } /// sync a lease request - pub(crate) async fn after_sync( + pub(crate) fn after_sync( &self, request: &RequestWrapper, - revision: i64, - ) -> Result<(SyncResponse, Vec), ExecuteError> { - self.sync_request(request, revision) - .await - .map(|(rev, ops)| (SyncResponse::new(rev), ops)) + revision_gen: &RevisionNumberGeneratorState<'_>, + txn_db: &T, + index: &I, + ) -> Result<(SyncResponse, Vec), ExecuteError> + where + T: XlineStorageOps + TransactionApi, + I: IndexOperate, + { + let next_revision = revision_gen.get().overflow_add(1); + let updated = self.sync_request(request, next_revision, txn_db, index)?; + let rev = if updated { + revision_gen.next() + } else { + revision_gen.get() + }; + // TODO: return only a `SyncResponse` + Ok((SyncResponse::new(rev), vec![])) } /// Get lease by id @@ -122,16 +133,6 @@ impl LeaseStore { self.lease_collection.find_expired_leases() } - /// Get keys attached to a lease - /// FIXME: use this in conflict pools - #[allow(unused)] - pub(crate) fn get_keys(&self, lease_id: i64) -> Vec> { - self.lease_collection - .look_up(lease_id) - .map(|l| l.keys()) - .unwrap_or_default() - } - /// Keep alive a lease pub(crate) fn keep_alive(&self, lease_id: i64) -> Result { self.lease_collection.renew(lease_id) @@ -209,11 +210,11 @@ impl LeaseStore { debug!("Receive LeaseGrantRequest {:?}", req); self.handle_lease_grant_request(req).map(Into::into) } - RequestWrapper::LeaseRevokeRequest(ref req) => { + RequestWrapper::LeaseRevokeRequest(req) => { debug!("Receive LeaseRevokeRequest {:?}", req); self.handle_lease_revoke_request(req).map(Into::into) } - RequestWrapper::LeaseLeasesRequest(ref req) => { + RequestWrapper::LeaseLeasesRequest(req) => { debug!("Receive LeaseLeasesRequest {:?}", req); Ok(self.handle_lease_leases_request(req).into()) } @@ -250,7 +251,7 @@ impl LeaseStore { /// Handle `LeaseRevokeRequest` fn handle_lease_revoke_request( &self, - req: &LeaseRevokeRequest, + req: LeaseRevokeRequest, ) -> Result { if self.lease_collection.contains_lease(req.id) { _ = self.unsynced_cache.write().insert(req.id); @@ -264,7 +265,7 @@ impl LeaseStore { } /// Handle `LeaseRevokeRequest` - fn handle_lease_leases_request(&self, _req: &LeaseLeasesRequest) -> LeaseLeasesResponse { + fn handle_lease_leases_request(&self, _req: LeaseLeasesRequest) -> LeaseLeasesResponse { let leases = self .leases() .into_iter() @@ -278,36 +279,47 @@ impl LeaseStore { } /// Sync `RequestWithToken` - async fn sync_request( + fn sync_request( &self, wrapper: &RequestWrapper, revision: i64, - ) -> Result<(i64, Vec), ExecuteError> { + txn_db: &T, + index: &I, + ) -> Result + where + T: XlineStorageOps + TransactionApi, + I: IndexOperate, + { #[allow(clippy::wildcard_enum_match_arm)] - let ops = match *wrapper { + let updated = match *wrapper { RequestWrapper::LeaseGrantRequest(ref req) => { debug!("Sync LeaseGrantRequest {:?}", req); - self.sync_lease_grant_request(req) + self.sync_lease_grant_request(req, txn_db)?; + false } RequestWrapper::LeaseRevokeRequest(ref req) => { debug!("Sync LeaseRevokeRequest {:?}", req); - self.sync_lease_revoke_request(req, revision).await? + self.sync_lease_revoke_request(req, revision, txn_db, index)? } RequestWrapper::LeaseLeasesRequest(ref req) => { debug!("Sync LeaseLeasesRequest {:?}", req); - vec![] + false } _ => unreachable!("Other request should not be sent to this store"), }; - Ok((revision, ops)) + Ok(updated) } /// Sync `LeaseGrantRequest` - fn sync_lease_grant_request(&self, req: &LeaseGrantRequest) -> Vec { + fn sync_lease_grant_request( + &self, + req: &LeaseGrantRequest, + txn_db: &T, + ) -> Result<(), ExecuteError> { let lease = self .lease_collection .grant(req.id, req.ttl, self.is_primary()); - vec![WriteOp::PutLease(lease)] + txn_db.write_op(WriteOp::PutLease(lease)) } /// Get all `PbLease` @@ -325,14 +337,20 @@ impl LeaseStore { } /// Sync `LeaseRevokeRequest` - async fn sync_lease_revoke_request( + #[allow(clippy::trivially_copy_pass_by_ref)] // we can only get a reference in the caller + fn sync_lease_revoke_request( &self, req: &LeaseRevokeRequest, revision: i64, - ) -> Result, ExecuteError> { - let mut ops = Vec::new(); + txn_db: &T, + index: &I, + ) -> Result + where + T: XlineStorageOps + TransactionApi, + I: IndexOperate, + { let mut updates = Vec::new(); - ops.push(WriteOp::DeleteLease(req.id)); + txn_db.write_op(WriteOp::DeleteLease(req.id))?; let del_keys = match self.lease_collection.look_up(req.id) { Some(l) => l.keys(), @@ -341,28 +359,24 @@ impl LeaseStore { if del_keys.is_empty() { let _ignore = self.lease_collection.revoke(req.id); - return Ok(Vec::new()); + return Ok(false); } - for (key, sub_revision) in del_keys.iter().zip(0..) { - let (mut del_ops, mut del_event) = KvStore::delete_keys( - &self.index, - &self.lease_collection, - key, - &[], - revision, - sub_revision, - ); - ops.append(&mut del_ops); + for (key, mut sub_revision) in del_keys.iter().zip(0..) { + let deleted = + KvStore::delete_keys(txn_db, index, key, &[], revision, &mut sub_revision)?; + KvStore::detach_leases(&deleted, &self.lease_collection); + let mut del_event = KvStore::new_deletion_events(revision, deleted); updates.append(&mut del_event); } let _ignore = self.lease_collection.revoke(req.id); assert!( - self.kv_update_tx.send((revision, updates)).await.is_ok(), + self.kv_update_tx.send((revision, updates)).is_ok(), "Failed to send updates to KV watcher" ); - Ok(ops) + + Ok(true) } } @@ -374,17 +388,25 @@ mod test { use utils::config::EngineConfig; use super::*; - use crate::storage::db::DB; + use crate::{ + revision_number::RevisionNumberGenerator, + storage::{ + db::DB, + index::{Index, IndexState}, + storage_api::XlineStorageOps, + }, + }; #[tokio::test(flavor = "multi_thread")] #[abort_on_panic] async fn test_lease_storage() -> Result<(), Box> { let db = DB::open(&EngineConfig::Memory)?; - let lease_store = init_store(db); - let revision_gen = lease_store.header_gen.general_revision_arc(); + let index = Index::new(); + let (lease_store, rev_gen) = init_store(db); + let rev_gen_state = rev_gen.state(); let req1 = RequestWrapper::from(LeaseGrantRequest { ttl: 10, id: 1 }); - let _ignore1 = exe_and_sync_req(&lease_store, &req1, -1).await?; + let _ignore1 = exe_and_sync_req(&lease_store, index.state(), &req1, &rev_gen_state)?; let lo = lease_store.look_up(1).unwrap(); assert_eq!(lo.id(), 1); @@ -398,7 +420,7 @@ mod test { lease_store.lease_collection.detach(1, "key".as_bytes())?; let req2 = RequestWrapper::from(LeaseRevokeRequest { id: 1 }); - let _ignore2 = exe_and_sync_req(&lease_store, &req2, revision_gen.next()).await?; + let _ignore2 = exe_and_sync_req(&lease_store, index.state(), &req2, &rev_gen_state)?; assert!(lease_store.look_up(1).is_none()); assert!(lease_store.leases().is_empty()); @@ -406,9 +428,9 @@ mod test { let req4 = RequestWrapper::from(LeaseGrantRequest { ttl: 10, id: 4 }); let req5 = RequestWrapper::from(LeaseRevokeRequest { id: 3 }); let req6 = RequestWrapper::from(LeaseLeasesRequest {}); - let _ignore3 = exe_and_sync_req(&lease_store, &req3, -1).await?; - let _ignore4 = exe_and_sync_req(&lease_store, &req4, -1).await?; - let resp_1 = exe_and_sync_req(&lease_store, &req6, -1).await?; + let _ignore3 = exe_and_sync_req(&lease_store, index.state(), &req3, &rev_gen_state)?; + let _ignore4 = exe_and_sync_req(&lease_store, index.state(), &req4, &rev_gen_state)?; + let resp_1 = exe_and_sync_req(&lease_store, index.state(), &req6, &rev_gen_state)?; let ResponseWrapper::LeaseLeasesResponse(leases_1) = resp_1 else { panic!("wrong response type: {resp_1:?}"); @@ -416,8 +438,8 @@ mod test { assert_eq!(leases_1.leases[0].id, 3); assert_eq!(leases_1.leases[1].id, 4); - let _ignore5 = exe_and_sync_req(&lease_store, &req5, -1).await?; - let resp_2 = exe_and_sync_req(&lease_store, &req6, -1).await?; + let _ignore5 = exe_and_sync_req(&lease_store, index.state(), &req5, &rev_gen_state)?; + let resp_2 = exe_and_sync_req(&lease_store, index.state(), &req6, &rev_gen_state)?; let ResponseWrapper::LeaseLeasesResponse(leases_2) = resp_2 else { panic!("wrong response type: {resp_2:?}"); }; @@ -429,7 +451,10 @@ mod test { #[tokio::test(flavor = "multi_thread")] async fn test_lease_sync() -> Result<(), Box> { let db = DB::open(&EngineConfig::Memory)?; - let lease_store = init_store(db); + let txn = db.transaction(); + let index = Index::new(); + let (lease_store, rev_gen) = init_store(Arc::clone(&db)); + let rev_gen_state = rev_gen.state(); let wait_duration = Duration::from_millis(1); let req1 = RequestWrapper::from(LeaseGrantRequest { ttl: 10, id: 1 }); @@ -442,8 +467,8 @@ mod test { "the future should block until the lease is synced" ); - let (_ignore, ops) = lease_store.after_sync(&req1, -1).await?; - _ = lease_store.db.flush_ops(ops)?; + let (_ignore, ops) = lease_store.after_sync(&req1, &rev_gen_state, &txn, &index)?; + lease_store.db.write_ops(ops)?; lease_store.mark_lease_synced(&req1); assert!( @@ -463,8 +488,8 @@ mod test { "the future should block until the lease is synced" ); - let (_ignore, ops) = lease_store.after_sync(&req2, -1).await?; - _ = lease_store.db.flush_ops(ops)?; + let (_ignore, ops) = lease_store.after_sync(&req2, &rev_gen_state, &txn, &index)?; + lease_store.db.write_ops(ops)?; lease_store.mark_lease_synced(&req2); assert!( @@ -481,13 +506,15 @@ mod test { #[abort_on_panic] async fn test_recover() -> Result<(), ExecuteError> { let db = DB::open(&EngineConfig::Memory)?; - let store = init_store(Arc::clone(&db)); + let index = Index::new(); + let (store, rev_gen) = init_store(Arc::clone(&db)); + let rev_gen_state = rev_gen.state(); let req1 = RequestWrapper::from(LeaseGrantRequest { ttl: 10, id: 1 }); - let _ignore1 = exe_and_sync_req(&store, &req1, -1).await?; + let _ignore1 = exe_and_sync_req(&store, index.state(), &req1, &rev_gen_state)?; store.lease_collection.attach(1, "key".into())?; - let new_store = init_store(db); + let (new_store, _) = init_store(db); assert!(new_store.look_up(1).is_none()); new_store.recover()?; @@ -502,22 +529,29 @@ mod test { Ok(()) } - fn init_store(db: Arc) -> LeaseStore { + fn init_store(db: Arc) -> (LeaseStore, RevisionNumberGenerator) { let lease_collection = Arc::new(LeaseCollection::new(0)); - let (kv_update_tx, _) = mpsc::channel(1); + let (kv_update_tx, _) = flume::bounded(1); let header_gen = Arc::new(HeaderGenerator::new(0, 0)); - let index = Arc::new(Index::new()); - LeaseStore::new(lease_collection, header_gen, db, index, kv_update_tx, true) + ( + LeaseStore::new(lease_collection, header_gen, db, kv_update_tx, true), + RevisionNumberGenerator::new(1), + ) } - async fn exe_and_sync_req( + fn exe_and_sync_req( ls: &LeaseStore, + index: IndexState, req: &RequestWrapper, - revision: i64, + rev_gen: &RevisionNumberGeneratorState<'_>, ) -> Result { let cmd_res = ls.execute(req)?; - let (_ignore, ops) = ls.after_sync(req, revision).await?; - _ = ls.db.flush_ops(ops)?; + let txn = ls.db.transaction(); + let (_ignore, _ops) = ls.after_sync(req, rev_gen, &txn, &index)?; + txn.commit() + .map_err(|e| ExecuteError::DbError(e.to_string()))?; + index.commit(); + rev_gen.commit(); Ok(cmd_res.into_inner()) } } diff --git a/crates/xline/src/storage/mod.rs b/crates/xline/src/storage/mod.rs index a586c4bc7..a41cbe621 100644 --- a/crates/xline/src/storage/mod.rs +++ b/crates/xline/src/storage/mod.rs @@ -16,6 +16,8 @@ pub(crate) mod kvwatcher; pub(crate) mod lease_store; /// Revision module pub(crate) mod revision; +/// Storage API +pub(crate) mod storage_api; pub use self::revision::Revision; pub(crate) use self::{ diff --git a/crates/xline/src/storage/revision.rs b/crates/xline/src/storage/revision.rs index b1e466b23..ebe9c5766 100644 --- a/crates/xline/src/storage/revision.rs +++ b/crates/xline/src/storage/revision.rs @@ -68,7 +68,9 @@ impl Revision { } /// Decode `Revision` from `&[u8]` + /// /// # Panics + /// /// This function panics if there is not enough remaining data in `buf`. #[must_use] #[inline] diff --git a/crates/xline/src/storage/storage_api.rs b/crates/xline/src/storage/storage_api.rs index 216fee213..ec2b1c99a 100644 --- a/crates/xline/src/storage/storage_api.rs +++ b/crates/xline/src/storage/storage_api.rs @@ -1,23 +1,21 @@ -use std::path::Path; - -use engine::Snapshot; use xlineapi::execute_error::ExecuteError; -use super::{db::WriteOp, revision::KeyRevision}; +use super::db::WriteOp; + +/// Storage operations in xline +pub(crate) trait XlineStorageOps { + /// Write an operation to the transaction + fn write_op(&self, op: WriteOp) -> Result<(), ExecuteError>; + + /// Write a batch of operations to the transaction + fn write_ops(&self, ops: Vec) -> Result<(), ExecuteError>; -/// The Stable Storage Api -#[async_trait::async_trait] -pub(crate) trait StorageApi: Send + Sync + 'static + std::fmt::Debug { /// Get values by keys from storage /// /// # Errors /// /// if error occurs in storage, return `Err(error)` - fn get_values( - &self, - table: &'static str, - keys: &[K], - ) -> Result>>, ExecuteError> + fn get_value(&self, table: &'static str, key: K) -> Result>, ExecuteError> where K: AsRef<[u8]> + std::fmt::Debug; @@ -26,37 +24,11 @@ pub(crate) trait StorageApi: Send + Sync + 'static + std::fmt::Debug { /// # Errors /// /// if error occurs in storage, return `Err(error)` - fn get_value(&self, table: &'static str, key: K) -> Result>, ExecuteError> + fn get_values( + &self, + table: &'static str, + keys: &[K], + ) -> Result>>, ExecuteError> where K: AsRef<[u8]> + std::fmt::Debug; - - /// Get all values of the given table from storage - /// - /// # Errors - /// - /// if error occurs in storage, return `Err(error)` - #[allow(clippy::type_complexity)] // it's clear that (Vec, Vec) is a key-value pair - fn get_all(&self, table: &'static str) -> Result, Vec)>, ExecuteError>; - - /// Reset the storage by given snapshot - /// - /// # Errors - /// - /// if error occurs in storage, return `Err(error)` - async fn reset(&self, snapshot: Option) -> Result<(), ExecuteError>; - - /// Get the snapshot of the storage - fn get_snapshot(&self, snap_path: impl AsRef) -> Result; - - /// Flush the operations to storage - fn flush_ops(&self, ops: Vec) -> Result, KeyRevision)>, ExecuteError>; - - /// Calculate the hash of the storage - fn hash(&self) -> Result; - - /// Get the cached size of the engine - fn estimated_file_size(&self) -> u64; - - /// Get the file size of the engine - fn file_size(&self) -> Result; } diff --git a/crates/xline/src/utils/args.rs b/crates/xline/src/utils/args.rs index f16c55002..f8b6d44c8 100644 --- a/crates/xline/src/utils/args.rs +++ b/crates/xline/src/utils/args.rs @@ -348,7 +348,9 @@ impl From for XlineServerConfig { } /// Parse config from command line arguments or config file +/// /// # Errors +/// /// Return error if parse failed #[inline] pub async fn parse_config() -> Result { diff --git a/crates/xline/src/utils/metrics.rs b/crates/xline/src/utils/metrics.rs index 97a22896d..3621936b6 100644 --- a/crates/xline/src/utils/metrics.rs +++ b/crates/xline/src/utils/metrics.rs @@ -1,3 +1,5 @@ +use std::net::SocketAddr; + use opentelemetry::global; use opentelemetry_otlp::WithExportConfig; use opentelemetry_sdk::{metrics::SdkMeterProvider, runtime::Tokio}; @@ -5,7 +7,9 @@ use tracing::info; use utils::config::{MetricsConfig, MetricsPushProtocol}; /// Start metrics server +/// /// # Errors +/// /// Return error if init failed #[inline] pub fn init_metrics(config: &MetricsConfig) -> anyhow::Result<()> { @@ -47,7 +51,7 @@ pub fn init_metrics(config: &MetricsConfig) -> anyhow::Result<()> { let provider = SdkMeterProvider::builder().with_reader(exporter).build(); global::set_meter_provider(provider); - let addr = format!("0.0.0.0:{}", config.port()) + let addr: SocketAddr = format!("0.0.0.0:{}", config.port()) .parse() .unwrap_or_else(|_| { unreachable!("local address 0.0.0.0:{} should be parsed", config.port()) @@ -55,9 +59,8 @@ pub fn init_metrics(config: &MetricsConfig) -> anyhow::Result<()> { info!("metrics server start on {addr:?}"); let app = axum::Router::new().route(config.path(), axum::routing::any(metrics)); let _ig = tokio::spawn(async move { - axum::Server::bind(&addr) - .serve(app.into_make_service()) - .await + let listener = real_tokio::net::TcpListener::bind(addr).await?; + axum::serve(listener, app).await }); Ok(()) diff --git a/crates/xline/src/utils/trace.rs b/crates/xline/src/utils/trace.rs index 9384626a1..cbfff13d3 100644 --- a/crates/xline/src/utils/trace.rs +++ b/crates/xline/src/utils/trace.rs @@ -1,9 +1,12 @@ use anyhow::{Ok, Result}; +use opentelemetry::global; +use opentelemetry::trace::TracerProvider; use opentelemetry_contrib::trace::exporter::jaeger_json::JaegerJsonExporter; use opentelemetry_sdk::runtime::Tokio; use tracing::warn; use tracing_appender::non_blocking::WorkerGuard; -use tracing_subscriber::{fmt::format, layer::SubscriberExt, util::SubscriberInitExt, Layer}; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::{fmt::format, util::SubscriberInitExt, Layer}; use utils::config::{file_appender, LogConfig, RotationConfig, TraceConfig}; /// Return a Box trait from the config @@ -36,6 +39,10 @@ pub fn init_subscriber( .tracing() .with_exporter(otlp_exporter) .install_batch(Tokio) + .map(|provider| { + let _prev = global::set_tracer_provider(provider.clone()); + provider.tracer("xline") + }) .ok() }) .flatten() diff --git a/crates/xline/tests/it/auth_test.rs b/crates/xline/tests/it/auth_test.rs index 2692ffaa4..935fcc0c5 100644 --- a/crates/xline/tests/it/auth_test.rs +++ b/crates/xline/tests/it/auth_test.rs @@ -6,12 +6,7 @@ use utils::config::{ TraceConfig, XlineServerConfig, }; use xline_test_utils::{ - enable_auth, set_user, - types::{ - auth::{AuthRoleDeleteRequest, AuthUserAddRequest, AuthUserGetRequest}, - kv::{PutRequest, RangeRequest}, - }, - Client, ClientOptions, Cluster, + enable_auth, set_user, types::kv::RangeOptions, Client, ClientOptions, Cluster, }; #[tokio::test(flavor = "multi_thread")] @@ -22,7 +17,7 @@ async fn test_auth_empty_user_get() -> Result<(), Box> { let client = cluster.client().await; enable_auth(client).await?; - let res = client.kv_client().range(RangeRequest::new("foo")).await; + let res = client.kv_client().range("foo", None).await; assert!(res.is_err()); Ok(()) @@ -36,7 +31,7 @@ async fn test_auth_empty_user_put() -> Result<(), Box> { let client = cluster.client().await; enable_auth(client).await?; - let res = client.kv_client().put(PutRequest::new("foo", "bar")).await; + let res = client.kv_client().put("foo", "bar", None).await; assert!(res.is_err()); Ok(()) @@ -56,9 +51,9 @@ async fn test_auth_token_with_disable() -> Result<(), Box> { ) .await?; let kv_client = authed_client.kv_client(); - kv_client.put(PutRequest::new("foo", "bar")).await?; + kv_client.put("foo", "bar", None).await?; authed_client.auth_client().auth_disable().await?; - kv_client.put(PutRequest::new("foo", "bar")).await?; + kv_client.put("foo", "bar", None).await?; Ok(()) } @@ -71,14 +66,9 @@ async fn test_auth_revision() -> Result<(), Box> { let client = cluster.client().await; let auth_client = client.auth_client(); - client - .kv_client() - .put(PutRequest::new("foo", "bar")) - .await?; + client.kv_client().put("foo", "bar", None).await?; - let user_add_resp = auth_client - .user_add(AuthUserAddRequest::new("root").with_pwd("123")) - .await?; + let user_add_resp = auth_client.user_add("root", "123", false).await?; let auth_rev = user_add_resp.header.unwrap().revision; assert_eq!(auth_rev, 2); @@ -93,10 +83,10 @@ async fn test_auth_non_authorized_rpcs() -> Result<(), Box> { let client = cluster.client().await; let kv_client = client.kv_client(); - let result = kv_client.put(PutRequest::new("foo", "bar")).await; + let result = kv_client.put("foo", "bar", None).await; assert!(result.is_ok()); enable_auth(client).await?; - let result = kv_client.put(PutRequest::new("foo", "bar")).await; + let result = kv_client.put("foo", "bar", None).await; assert!(result.is_err()); Ok(()) @@ -126,17 +116,17 @@ async fn test_kv_authorization() -> Result<(), Box> { .await? .kv_client(); - let result = u1_client.put(PutRequest::new("foo", "bar")).await; + let result = u1_client.put("foo", "bar", None).await; assert!(result.is_ok()); - let result = u1_client.put(PutRequest::new("fop", "bar")).await; + let result = u1_client.put("fop", "bar", None).await; assert!(result.is_err()); let result = u2_client - .range(RangeRequest::new("foo").with_range_end("fox")) + .range("foo", Some(RangeOptions::default().with_range_end("fox"))) .await; assert!(result.is_ok()); let result = u2_client - .range(RangeRequest::new("foo").with_range_end("foz")) + .range("foo", Some(RangeOptions::default().with_range_end("foz"))) .await; assert!(result.is_err()); @@ -151,12 +141,10 @@ async fn test_role_delete() -> Result<(), Box> { let client = cluster.client().await; let auth_client = client.auth_client(); set_user(client, "u", "123", "r", b"foo", &[]).await?; - let user = auth_client.user_get(AuthUserGetRequest::new("u")).await?; + let user = auth_client.user_get("u").await?; assert_eq!(user.roles.len(), 1); - auth_client - .role_delete(AuthRoleDeleteRequest::new("r")) - .await?; - let user = auth_client.user_get(AuthUserGetRequest::new("u")).await?; + auth_client.role_delete("r").await?; + let user = auth_client.user_get("u").await?; assert_eq!(user.roles.len(), 0); Ok(()) @@ -184,16 +172,12 @@ async fn test_no_root_user_do_admin_ops() -> Result<(), Box> { .await? .auth_client(); - let result = user_client - .user_add(AuthUserAddRequest::new("u2").with_pwd("123")) - .await; + let result = user_client.user_add("u2", "123", false).await; assert!( result.is_err(), "normal user should not allow to add user when auth is enabled: {result:?}" ); - let result = root_client - .user_add(AuthUserAddRequest::new("u2").with_pwd("123")) - .await; + let result = root_client.user_add("u2", "123", false).await; assert!(result.is_ok(), "root user failed to add user: {result:?}"); Ok(()) diff --git a/crates/xline/tests/it/cluster_test.rs b/crates/xline/tests/it/cluster_test.rs index e10458233..9c435859e 100644 --- a/crates/xline/tests/it/cluster_test.rs +++ b/crates/xline/tests/it/cluster_test.rs @@ -2,13 +2,7 @@ use std::{error::Error, time::Duration}; use test_macros::abort_on_panic; use tokio::{net::TcpListener, time::sleep}; -use xline_client::{ - types::{ - cluster::{MemberAddRequest, MemberListRequest, MemberRemoveRequest, MemberUpdateRequest}, - kv::PutRequest, - }, - Client, ClientOptions, -}; +use xline_client::{Client, ClientOptions}; use xline_test_utils::Cluster; #[tokio::test(flavor = "multi_thread")] @@ -19,13 +13,10 @@ async fn xline_remove_node() -> Result<(), Box> { let mut cluster_client = Client::connect(cluster.all_client_addrs(), ClientOptions::default()) .await? .cluster_client(); - let list_res = cluster_client - .member_list(MemberListRequest::new(false)) - .await?; + let list_res = cluster_client.member_list(false).await?; assert_eq!(list_res.members.len(), 5); let remove_id = list_res.members[0].id; - let remove_req = MemberRemoveRequest::new(remove_id); - let remove_res = cluster_client.member_remove(remove_req).await?; + let remove_res = cluster_client.member_remove(remove_id).await?; assert_eq!(remove_res.members.len(), 4); assert!(remove_res.members.iter().all(|m| m.id != remove_id)); Ok(()) @@ -39,13 +30,12 @@ async fn xline_add_node() -> Result<(), Box> { let client = Client::connect(cluster.all_client_addrs(), ClientOptions::default()).await?; let mut cluster_client = client.cluster_client(); let kv_client = client.kv_client(); - _ = kv_client.put(PutRequest::new("key", "value")).await?; + _ = kv_client.put("key", "value", None).await?; let new_node_peer_listener = TcpListener::bind("0.0.0.0:0").await?; let new_node_peer_urls = vec![format!("http://{}", new_node_peer_listener.local_addr()?)]; let new_node_client_listener = TcpListener::bind("0.0.0.0:0").await?; let new_node_client_urls = vec![format!("http://{}", new_node_client_listener.local_addr()?)]; - let add_req = MemberAddRequest::new(new_node_peer_urls.clone(), false); - let add_res = cluster_client.member_add(add_req).await?; + let add_res = cluster_client.member_add(new_node_peer_urls, false).await?; assert_eq!(add_res.members.len(), 4); cluster .run_node(new_node_client_listener, new_node_peer_listener) @@ -62,9 +52,7 @@ async fn xline_update_node() -> Result<(), Box> { let mut cluster = Cluster::new(3).await; cluster.start().await; let mut cluster_client = cluster.client().await.cluster_client(); - let old_list_res = cluster_client - .member_list(MemberListRequest::new(false)) - .await?; + let old_list_res = cluster_client.member_list(false).await?; assert_eq!(old_list_res.members.len(), 3); let update_id = old_list_res.members[0].id; let port = old_list_res.members[0] @@ -76,14 +64,12 @@ async fn xline_update_node() -> Result<(), Box> { .unwrap() .parse::() .unwrap(); - let update_req = - MemberUpdateRequest::new(update_id, vec![format!("http://localhost:{}", port)]); - let update_res = cluster_client.member_update(update_req).await?; + let update_res = cluster_client + .member_update(update_id, [format!("http://localhost:{}", port)]) + .await?; assert_eq!(update_res.members.len(), 3); sleep(Duration::from_secs(3)).await; - let new_list_res = cluster_client - .member_list(MemberListRequest::new(false)) - .await?; + let new_list_res = cluster_client.member_list(false).await?; assert_eq!(new_list_res.members.len(), 3); let old_addr = &old_list_res .members diff --git a/crates/xline/tests/it/kv_test.rs b/crates/xline/tests/it/kv_test.rs index 7f4751081..4188fb91d 100644 --- a/crates/xline/tests/it/kv_test.rs +++ b/crates/xline/tests/it/kv_test.rs @@ -3,7 +3,7 @@ use std::{error::Error, time::Duration}; use test_macros::abort_on_panic; use xline_test_utils::{ types::kv::{ - Compare, CompareResult, DeleteRangeRequest, PutRequest, RangeRequest, Response, SortOrder, + Compare, CompareResult, DeleteRangeOptions, PutOptions, RangeOptions, Response, SortOrder, SortTarget, TxnOp, TxnRequest, }, Client, ClientOptions, Cluster, @@ -13,25 +13,35 @@ use xline_test_utils::{ #[abort_on_panic] async fn test_kv_put() -> Result<(), Box> { struct TestCase { - req: PutRequest, + key: &'static str, + value: &'static str, + option: Option, want_err: bool, } let tests = [ TestCase { - req: PutRequest::new("foo", "").with_ignore_value(true), + key: "foo", + value: "", + option: Some(PutOptions::default().with_ignore_value(true)), want_err: true, }, TestCase { - req: PutRequest::new("foo", "bar"), + key: "foo", + value: "bar", + option: None, want_err: false, }, TestCase { - req: PutRequest::new("foo", "").with_ignore_value(true), + key: "foo", + value: "", + option: Some(PutOptions::default().with_ignore_value(true)), want_err: false, }, TestCase { - req: PutRequest::new("foo", "").with_lease(12345), + key: "foo", + value: "", + option: Some(PutOptions::default().with_lease(12345)), want_err: true, }, ]; @@ -41,7 +51,7 @@ async fn test_kv_put() -> Result<(), Box> { let client = cluster.client().await.kv_client(); for test in tests { - let res = client.put(test.req).await; + let res = client.put(test.key, test.value, test.option).await; assert_eq!(res.is_err(), test.want_err); } @@ -52,7 +62,8 @@ async fn test_kv_put() -> Result<(), Box> { #[abort_on_panic] async fn test_kv_get() -> Result<(), Box> { struct TestCase<'a> { - req: RangeRequest, + key: Vec, + opt: Option, want_kvs: &'a [&'a str], } @@ -67,92 +78,119 @@ async fn test_kv_get() -> Result<(), Box> { let tests = [ TestCase { - req: RangeRequest::new("a"), + key: "a".into(), + opt: None, want_kvs: &want_kvs[..1], }, TestCase { - req: RangeRequest::new("a").with_serializable(true), + key: "a".into(), + opt: Some(RangeOptions::default().with_serializable(true)), want_kvs: &want_kvs[..1], }, TestCase { - req: RangeRequest::new("a").with_range_end("c"), + key: "a".into(), + opt: Some(RangeOptions::default().with_range_end("c")), want_kvs: &want_kvs[..2], }, TestCase { - req: RangeRequest::new("").with_prefix(), + key: "".into(), + opt: Some(RangeOptions::default().with_prefix()), want_kvs: &want_kvs[..], }, TestCase { - req: RangeRequest::new("").with_from_key(), + key: "".into(), + opt: Some(RangeOptions::default().with_from_key()), want_kvs: &want_kvs[..], }, TestCase { - req: RangeRequest::new("a").with_range_end("x"), + key: "a".into(), + opt: Some(RangeOptions::default().with_range_end("x")), want_kvs: &want_kvs[..], }, TestCase { - req: RangeRequest::new("").with_prefix().with_revision(4), + key: "".into(), + opt: Some(RangeOptions::default().with_prefix().with_revision(4)), want_kvs: &want_kvs[..3], }, TestCase { - req: RangeRequest::new("a").with_count_only(true), + key: "a".into(), + opt: Some(RangeOptions::default().with_count_only(true)), want_kvs: &[], }, TestCase { - req: RangeRequest::new("foo").with_prefix(), + key: "foo".into(), + opt: Some(RangeOptions::default().with_prefix()), want_kvs: &["foo", "foo/abc"], }, TestCase { - req: RangeRequest::new("foo").with_from_key(), + key: "foo".into(), + opt: Some(RangeOptions::default().with_from_key()), want_kvs: &["foo", "foo/abc", "fop"], }, TestCase { - req: RangeRequest::new("").with_prefix().with_limit(2), + key: "".into(), + opt: Some(RangeOptions::default().with_prefix().with_limit(2)), want_kvs: &want_kvs[..2], }, TestCase { - req: RangeRequest::new("") - .with_prefix() - .with_sort_target(SortTarget::Mod) - .with_sort_order(SortOrder::Ascend), + key: "".into(), + opt: Some( + RangeOptions::default() + .with_prefix() + .with_sort_order(SortOrder::Descend) + .with_sort_order(SortOrder::Ascend), + ), want_kvs: &want_kvs[..], }, TestCase { - req: RangeRequest::new("") - .with_prefix() - .with_sort_target(SortTarget::Version) - .with_sort_order(SortOrder::Ascend), + key: "".into(), + opt: Some( + RangeOptions::default() + .with_prefix() + .with_sort_target(SortTarget::Version) + .with_sort_order(SortOrder::Ascend), + ), + want_kvs: &kvs_by_version[..], }, TestCase { - req: RangeRequest::new("") - .with_prefix() - .with_sort_target(SortTarget::Create) - .with_sort_order(SortOrder::None), + key: "".into(), + opt: Some( + RangeOptions::default() + .with_prefix() + .with_sort_target(SortTarget::Create) + .with_sort_order(SortOrder::None), + ), want_kvs: &want_kvs[..], }, TestCase { - req: RangeRequest::new("") - .with_prefix() - .with_sort_target(SortTarget::Create) - .with_sort_order(SortOrder::Descend), + key: "".into(), + opt: Some( + RangeOptions::default() + .with_prefix() + .with_sort_target(SortTarget::Create) + .with_sort_order(SortOrder::Descend), + ), want_kvs: &reversed_kvs[..], }, TestCase { - req: RangeRequest::new("") - .with_prefix() - .with_sort_target(SortTarget::Key) - .with_sort_order(SortOrder::Descend), + key: "".into(), + opt: Some( + RangeOptions::default() + .with_prefix() + .with_sort_target(SortTarget::Key) + .with_sort_order(SortOrder::Descend), + ), want_kvs: &reversed_kvs[..], }, ]; for key in kvs { - client.put(PutRequest::new(key, "bar")).await?; + client.put(key, "bar", None).await?; } for test in tests { - let res = client.range(test.req).await?; + let res = client.range(test.key, test.opt).await?; assert_eq!(res.kvs.len(), test.want_kvs.len()); let is_identical = res .kvs @@ -175,9 +213,9 @@ async fn test_range_redirect() -> Result<(), Box> { let kv_client = Client::connect([addr], ClientOptions::default()) .await? .kv_client(); - let _ignore = kv_client.put(PutRequest::new("foo", "bar")).await?; + let _ignore = kv_client.put("foo", "bar", None).await?; tokio::time::sleep(Duration::from_millis(300)).await; - let res = kv_client.range(RangeRequest::new("foo")).await?; + let res = kv_client.range("foo", None).await?; assert_eq!(res.kvs.len(), 1); assert_eq!(res.kvs[0].value, b"bar"); @@ -188,7 +226,8 @@ async fn test_range_redirect() -> Result<(), Box> { #[abort_on_panic] async fn test_kv_delete() -> Result<(), Box> { struct TestCase<'a> { - req: DeleteRangeRequest, + key: Vec, + opt: Option, want_deleted: i64, want_keys: &'a [&'a str], } @@ -201,37 +240,44 @@ async fn test_kv_delete() -> Result<(), Box> { let tests = [ TestCase { - req: DeleteRangeRequest::new("").with_prefix(), + key: "".into(), + opt: Some(DeleteRangeOptions::default().with_prefix()), want_deleted: 5, want_keys: &[], }, TestCase { - req: DeleteRangeRequest::new("").with_from_key(), + key: "".into(), + opt: Some(DeleteRangeOptions::default().with_from_key()), want_deleted: 5, want_keys: &[], }, TestCase { - req: DeleteRangeRequest::new("a").with_range_end("c"), + key: "a".into(), + opt: Some(DeleteRangeOptions::default().with_range_end("c")), want_deleted: 2, want_keys: &["c", "c/abc", "d"], }, TestCase { - req: DeleteRangeRequest::new("c"), + key: "c".into(), + opt: None, want_deleted: 1, want_keys: &["a", "b", "c/abc", "d"], }, TestCase { - req: DeleteRangeRequest::new("c").with_prefix(), + key: "c".into(), + opt: Some(DeleteRangeOptions::default().with_prefix()), want_deleted: 2, want_keys: &["a", "b", "d"], }, TestCase { - req: DeleteRangeRequest::new("c").with_from_key(), + key: "c".into(), + opt: Some(DeleteRangeOptions::default().with_from_key()), want_deleted: 3, want_keys: &["a", "b"], }, TestCase { - req: DeleteRangeRequest::new("e"), + key: "e".into(), + opt: None, want_deleted: 0, want_keys: &keys, }, @@ -239,13 +285,15 @@ async fn test_kv_delete() -> Result<(), Box> { for test in tests { for key in keys { - client.put(PutRequest::new(key, "bar")).await?; + client.put(key, "bar", None).await?; } - let res = client.delete(test.req).await?; + let res = client.delete(test.key, test.opt).await?; assert_eq!(res.deleted, test.want_deleted); - let res = client.range(RangeRequest::new("").with_prefix()).await?; + let res = client + .range("", Some(RangeOptions::default().with_prefix())) + .await?; let is_identical = res .kvs .iter() @@ -266,13 +314,13 @@ async fn test_txn() -> Result<(), Box> { let kvs = ["a", "b", "c", "d", "e"]; for key in kvs { - client.put(PutRequest::new(key, "bar")).await?; + client.put(key, "bar", None).await?; } let read_write_txn_req = TxnRequest::new() .when(&[Compare::value("b", CompareResult::Equal, "bar")][..]) - .and_then(&[TxnOp::put(PutRequest::new("f", "foo"))][..]) - .or_else(&[TxnOp::range(RangeRequest::new("a"))][..]); + .and_then(&[TxnOp::put("f", "foo", None)][..]) + .or_else(&[TxnOp::range("a", None)][..]); let res = client.txn(read_write_txn_req).await?; assert!(res.succeeded); @@ -284,8 +332,8 @@ async fn test_txn() -> Result<(), Box> { let read_only_txn = TxnRequest::new() .when(&[Compare::version("b", CompareResult::Greater, 10)][..]) - .and_then(&[TxnOp::range(RangeRequest::new("a"))][..]) - .or_else(&[TxnOp::range(RangeRequest::new("b"))][..]); + .and_then(&[TxnOp::range("a", None)][..]) + .or_else(&[TxnOp::range("b", None)][..]); let mut res = client.txn(read_only_txn).await?; assert!(!res.succeeded); assert_eq!(res.responses.len(), 1); @@ -307,8 +355,18 @@ async fn test_txn() -> Result<(), Box> { let serializable_txn = TxnRequest::new() .when([]) - .and_then(&[TxnOp::range(RangeRequest::new("c").with_serializable(true))][..]) - .or_else(&[TxnOp::range(RangeRequest::new("d").with_serializable(true))][..]); + .and_then( + &[TxnOp::range( + "c", + Some(RangeOptions::default().with_serializable(true)), + )][..], + ) + .or_else( + &[TxnOp::range( + "d", + Some(RangeOptions::default().with_serializable(true)), + )][..], + ); let mut res = client.txn(serializable_txn).await?; assert!(res.succeeded); assert_eq!(res.responses.len(), 1); diff --git a/crates/xline/tests/it/lease_test.rs b/crates/xline/tests/it/lease_test.rs index 2eb20274b..036235913 100644 --- a/crates/xline/tests/it/lease_test.rs +++ b/crates/xline/tests/it/lease_test.rs @@ -2,13 +2,7 @@ use std::{error::Error, time::Duration}; use test_macros::abort_on_panic; use tracing::info; -use xline_test_utils::{ - types::{ - kv::{PutRequest, RangeRequest}, - lease::{LeaseGrantRequest, LeaseKeepAliveRequest}, - }, - Client, ClientOptions, Cluster, -}; +use xline_test_utils::{types::kv::PutOptions, Client, ClientOptions, Cluster}; #[tokio::test(flavor = "multi_thread")] #[abort_on_panic] @@ -17,24 +11,25 @@ async fn test_lease_expired() -> Result<(), Box> { cluster.start().await; let client = cluster.client().await; - let res = client - .lease_client() - .grant(LeaseGrantRequest::new(1)) - .await?; + let res = client.lease_client().grant(1, None).await?; let lease_id = res.id; assert!(lease_id > 0); let _ = client .kv_client() - .put(PutRequest::new("foo", "bar").with_lease(lease_id)) + .put( + "foo", + "bar", + Some(PutOptions::default().with_lease(lease_id)), + ) .await?; - let res = client.kv_client().range(RangeRequest::new("foo")).await?; + let res = client.kv_client().range("foo", None).await?; assert_eq!(res.kvs.len(), 1); assert_eq!(res.kvs[0].value, b"bar"); tokio::time::sleep(Duration::from_secs(3)).await; - let res = client.kv_client().range(RangeRequest::new("foo")).await?; + let res = client.kv_client().range("foo", None).await?; assert_eq!(res.kvs.len(), 0); Ok(()) @@ -48,28 +43,29 @@ async fn test_lease_keep_alive() -> Result<(), Box> { let non_leader_ep = cluster.get_client_url(1); let client = cluster.client().await; - let res = client - .lease_client() - .grant(LeaseGrantRequest::new(1)) - .await?; + let res = client.lease_client().grant(3, None).await?; let lease_id = res.id; assert!(lease_id > 0); let _ = client .kv_client() - .put(PutRequest::new("foo", "bar").with_lease(lease_id)) + .put( + "foo", + "bar", + Some(PutOptions::default().with_lease(lease_id)), + ) .await?; - let res = client.kv_client().range(RangeRequest::new("foo")).await?; + let res = client.kv_client().range("foo", None).await?; assert_eq!(res.kvs.len(), 1); assert_eq!(res.kvs[0].value, b"bar"); let mut c = Client::connect(vec![non_leader_ep], ClientOptions::default()) .await? .lease_client(); - let (mut keeper, mut stream) = c.keep_alive(LeaseKeepAliveRequest::new(lease_id)).await?; + let (mut keeper, mut stream) = c.keep_alive(lease_id).await?; let handle = tokio::spawn(async move { loop { - tokio::time::sleep(Duration::from_millis(500)).await; + tokio::time::sleep(Duration::from_millis(1500)).await; let _ = keeper.keep_alive(); if let Ok(Some(r)) = stream.message().await { info!("keep alive response: {:?}", r); @@ -78,13 +74,13 @@ async fn test_lease_keep_alive() -> Result<(), Box> { }); tokio::time::sleep(Duration::from_secs(3)).await; - let res = client.kv_client().range(RangeRequest::new("foo")).await?; + let res = client.kv_client().range("foo", None).await?; assert_eq!(res.kvs.len(), 1); assert_eq!(res.kvs[0].value, b"bar"); handle.abort(); - tokio::time::sleep(Duration::from_secs(2)).await; - let res = client.kv_client().range(RangeRequest::new("foo")).await?; + tokio::time::sleep(Duration::from_secs(6)).await; + let res = client.kv_client().range("foo", None).await?; assert_eq!(res.kvs.len(), 0); Ok(()) diff --git a/crates/xline/tests/it/maintenance_test.rs b/crates/xline/tests/it/maintenance_test.rs index c99cc0495..77cde73cd 100644 --- a/crates/xline/tests/it/maintenance_test.rs +++ b/crates/xline/tests/it/maintenance_test.rs @@ -5,11 +5,8 @@ use tokio::io::AsyncWriteExt; #[cfg(test)] use xline::restore::restore; use xline_client::error::XlineClientError; -use xline_test_utils::{ - types::kv::{PutRequest, RangeRequest}, - Client, ClientOptions, Cluster, -}; -use xlineapi::{execute_error::ExecuteError, AlarmAction, AlarmRequest, AlarmType}; +use xline_test_utils::{Client, ClientOptions, Cluster}; +use xlineapi::{execute_error::ExecuteError, AlarmAction, AlarmType}; #[tokio::test(flavor = "multi_thread")] #[abort_on_panic] @@ -27,7 +24,7 @@ async fn test_snapshot_and_restore() -> Result<(), Box> { let mut cluster = Cluster::new_rocks(3).await; cluster.start().await; let client = cluster.client().await.kv_client(); - let _ignore = client.put(PutRequest::new("key", "value")).await?; + let _ignore = client.put("key", "value", None).await?; tokio::time::sleep(Duration::from_millis(100)).await; // TODO: use `propose_index` and remove this sleep after we finished our client. let mut maintenance_client = Client::connect(vec![cluster.get_client_url(0)], ClientOptions::default()) @@ -45,7 +42,7 @@ async fn test_snapshot_and_restore() -> Result<(), Box> { let mut new_cluster = Cluster::new_with_configs(restore_cluster_configs).await; new_cluster.start().await; let client = new_cluster.client().await.kv_client(); - let res = client.range(RangeRequest::new("key")).await?; + let res = client.range("key", None).await?; assert_eq!(res.kvs.len(), 1); assert_eq!(res.kvs[0].key, b"key"); assert_eq!(res.kvs[0].value, b"value"); @@ -85,8 +82,7 @@ async fn test_alarm(idx: usize) { for i in 1..100u8 { let key: Vec = vec![i]; let value: Vec = vec![i]; - let req = PutRequest::new(key, value); - if let Err(err) = k_client.put(req).await { + if let Err(err) = k_client.put(key, value, None).await { assert!(matches!( err, XlineClientError::ExecuteError(ExecuteError::Nospace) @@ -96,7 +92,7 @@ async fn test_alarm(idx: usize) { } tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; let res = m_client - .alarm(AlarmRequest::new(AlarmAction::Get, 0, AlarmType::None)) + .alarm(AlarmAction::Get, 0, AlarmType::None) .await .unwrap(); assert!(!res.alarms.is_empty()); diff --git a/crates/xline/tests/it/tls_test.rs b/crates/xline/tests/it/tls_test.rs index 527b305c1..00b42c84b 100644 --- a/crates/xline/tests/it/tls_test.rs +++ b/crates/xline/tests/it/tls_test.rs @@ -7,7 +7,6 @@ use utils::config::{ AuthConfig, ClusterConfig, CompactConfig, LogConfig, MetricsConfig, StorageConfig, TlsConfig, TraceConfig, XlineServerConfig, }; -use xline_client::types::kv::PutRequest; use xline_test_utils::{enable_auth, set_user, Cluster}; #[tokio::test(flavor = "multi_thread")] @@ -19,7 +18,7 @@ async fn test_basic_tls() { let client = cluster .client_with_tls_config(basic_tls_client_config()) .await; - let res = client.kv_client().put(PutRequest::new("foo", "bar")).await; + let res = client.kv_client().put("foo", "bar", None).await; assert!(res.is_ok()); } @@ -32,7 +31,7 @@ async fn test_mtls() { let client = cluster .client_with_tls_config(mtls_client_config("root")) .await; - let res = client.kv_client().put(PutRequest::new("foo", "bar")).await; + let res = client.kv_client().put("foo", "bar", None).await; assert!(res.is_ok()); } @@ -59,10 +58,7 @@ async fn test_certificate_authenticate() { let u1_client = cluster .client_with_tls_config(mtls_client_config("u1")) .await; - let res = u1_client - .kv_client() - .put(PutRequest::new("foo", "bar")) - .await; + let res = u1_client.kv_client().put("foo", "bar", None).await; assert!(res.is_err()); set_user(&root_client, "u1", "123", "r1", b"foo", &[]) @@ -74,10 +70,7 @@ async fn test_certificate_authenticate() { let res = etcd_u2_client.put("foa", "bar", None).await; assert!(res.is_ok()); - let res = u1_client - .kv_client() - .put(PutRequest::new("foo", "bar")) - .await; + let res = u1_client.kv_client().put("foo", "bar", None).await; assert!(res.is_ok()); } diff --git a/crates/xline/tests/it/watch_test.rs b/crates/xline/tests/it/watch_test.rs index 52069c61f..43d0a67cc 100644 --- a/crates/xline/tests/it/watch_test.rs +++ b/crates/xline/tests/it/watch_test.rs @@ -1,13 +1,7 @@ use std::error::Error; use test_macros::abort_on_panic; -use xline_test_utils::{ - types::{ - kv::{DeleteRangeRequest, PutRequest}, - watch::WatchRequest, - }, - Cluster, -}; +use xline_test_utils::Cluster; use xlineapi::EventType; fn event_type(event_type: i32) -> EventType { @@ -27,7 +21,7 @@ async fn test_watch() -> Result<(), Box> { let mut watch_client = client.watch_client(); let kv_client = client.kv_client(); - let (_watcher, mut stream) = watch_client.watch(WatchRequest::new("foo")).await?; + let (_watcher, mut stream) = watch_client.watch("foo", None).await?; let handle = tokio::spawn(async move { if let Ok(Some(res)) = stream.message().await { let event = res.events.get(0).unwrap(); @@ -45,8 +39,8 @@ async fn test_watch() -> Result<(), Box> { } }); - kv_client.put(PutRequest::new("foo", "bar")).await?; - kv_client.delete(DeleteRangeRequest::new("foo")).await?; + kv_client.put("foo", "bar", None).await?; + kv_client.delete("foo", None).await?; handle.await?; diff --git a/crates/xlineapi/Cargo.toml b/crates/xlineapi/Cargo.toml index bab2a98b0..0574402ab 100644 --- a/crates/xlineapi/Cargo.toml +++ b/crates/xlineapi/Cargo.toml @@ -11,19 +11,19 @@ categories = ["RPC"] keywords = ["RPC", "Interfaces"] [dependencies] -async-trait = "0.1.80" +async-trait = "0.1.81" curp = { path = "../curp" } curp-external-api = { path = "../curp-external-api" } itertools = "0.13" -prost = "0.12.3" +prost = "0.13" serde = { version = "1.0.204", features = ["derive"] } thiserror = "1.0.61" -tonic = { version = "0.4.2", package = "madsim-tonic" } +tonic = { version = "0.5.0", package = "madsim-tonic" } utils = { path = "../utils", features = ["parking_lot"] } workspace-hack = { version = "0.1", path = "../../workspace-hack" } [build-dependencies] -tonic-build = { version = "0.4.3", package = "madsim-tonic-build" } +tonic-build = { version = "0.5.0", package = "madsim-tonic-build" } [dev-dependencies] strum = "0.26" diff --git a/crates/xlineapi/src/command.rs b/crates/xlineapi/src/command.rs index ab28daa17..ecbd37231 100644 --- a/crates/xlineapi/src/command.rs +++ b/crates/xlineapi/src/command.rs @@ -1,5 +1,5 @@ use std::{ - collections::{HashSet, VecDeque}, + collections::HashSet, ops::{Bound, RangeBounds}, }; @@ -11,10 +11,11 @@ use serde::{Deserialize, Serialize}; use crate::{ execute_error::ExecuteError, AuthInfo, PbCommand, PbCommandResponse, PbKeyRange, - PbSyncResponse, Request, RequestWrapper, ResponseWrapper, + PbSyncResponse, RequestWrapper, ResponseWrapper, }; /// The curp client trait object on the command of xline +/// /// TODO: use `type CurpClient = impl ClientApi<...>` when `type_alias_impl_trait` stabilized pub type CurpClient = dyn ClientApi + Sync + Send + 'static; @@ -125,11 +126,13 @@ impl KeyRange { } /// Get end of range with prefix + /// /// User will provide a start key when prefix is true, we need calculate the end key of `KeyRange` #[allow(clippy::indexing_slicing)] // end[i] is always valid #[must_use] #[inline] - pub fn get_prefix(key: &[u8]) -> Vec { + pub fn get_prefix(key: impl AsRef<[u8]>) -> Vec { + let key = key.as_ref(); let mut end = key.to_vec(); for i in (0..key.len()).rev() { if key[i] < 0xFF { @@ -219,67 +222,6 @@ pub struct Command { auth_info: Option, } -/// get all lease ids in the request wrapper -pub fn get_lease_ids(wrapper: &RequestWrapper) -> HashSet { - match *wrapper { - RequestWrapper::LeaseGrantRequest(ref req) => HashSet::from_iter(vec![req.id]), - RequestWrapper::LeaseRevokeRequest(ref req) => HashSet::from_iter(vec![req.id]), - RequestWrapper::PutRequest(ref req) if req.lease != 0 => { - HashSet::from_iter(vec![req.lease]) - } - RequestWrapper::TxnRequest(ref txn_req) => { - let mut lease_ids = HashSet::new(); - let mut reqs = txn_req - .success - .iter() - .chain(txn_req.failure.iter()) - .filter_map(|op| op.request.as_ref()) - .collect::>(); - while let Some(req) = reqs.pop_front() { - match *req { - Request::RequestPut(ref req) => { - if req.lease != 0 { - let _ignore = lease_ids.insert(req.lease); - } - } - Request::RequestTxn(ref req) => reqs.extend( - &mut req - .success - .iter() - .chain(req.failure.iter()) - .filter_map(|op| op.request.as_ref()), - ), - Request::RequestRange(_) | Request::RequestDeleteRange(_) => {} - } - } - lease_ids - } - RequestWrapper::PutRequest(_) - | RequestWrapper::RangeRequest(_) - | RequestWrapper::DeleteRangeRequest(_) - | RequestWrapper::CompactionRequest(_) - | RequestWrapper::AuthEnableRequest(_) - | RequestWrapper::AuthDisableRequest(_) - | RequestWrapper::AuthStatusRequest(_) - | RequestWrapper::AuthRoleAddRequest(_) - | RequestWrapper::AuthRoleDeleteRequest(_) - | RequestWrapper::AuthRoleGetRequest(_) - | RequestWrapper::AuthRoleGrantPermissionRequest(_) - | RequestWrapper::AuthRoleListRequest(_) - | RequestWrapper::AuthRoleRevokePermissionRequest(_) - | RequestWrapper::AuthUserAddRequest(_) - | RequestWrapper::AuthUserChangePasswordRequest(_) - | RequestWrapper::AuthUserDeleteRequest(_) - | RequestWrapper::AuthUserGetRequest(_) - | RequestWrapper::AuthUserGrantRoleRequest(_) - | RequestWrapper::AuthUserListRequest(_) - | RequestWrapper::AuthUserRevokeRoleRequest(_) - | RequestWrapper::AuthenticateRequest(_) - | RequestWrapper::LeaseLeasesRequest(_) - | RequestWrapper::AlarmRequest(_) => HashSet::new(), - } -} - impl ConflictCheck for Command { #[inline] fn is_conflict(&self, other: &Self) -> bool { @@ -332,8 +274,8 @@ impl ConflictCheck for Command { } } - let this_lease_ids = get_lease_ids(this_req); - let other_lease_ids = get_lease_ids(other_req); + let this_lease_ids = this_req.leases().into_iter().collect::>(); + let other_lease_ids = other_req.leases().into_iter().collect::>(); let lease_conflict = !this_lease_ids.is_disjoint(&other_lease_ids); let key_conflict = self .keys() @@ -539,6 +481,13 @@ impl CurpCommand for Command { } } +impl Command { + /// Get leases of the command + pub fn leases(&self) -> Vec { + self.request().leases() + } +} + impl PbCodec for Command { #[inline] fn encode(&self) -> Vec { @@ -567,9 +516,9 @@ impl PbCodec for Command { mod test { use super::*; use crate::{ - AuthEnableRequest, AuthStatusRequest, CommandKeys, CompactionRequest, Compare, + AuthEnableRequest, AuthStatusRequest, CommandAttr, CompactionRequest, Compare, DeleteRangeRequest, LeaseGrantRequest, LeaseLeasesRequest, LeaseRevokeRequest, PutRequest, - PutResponse, RangeRequest, RequestOp, TxnRequest, + PutResponse, RangeRequest, Request, RequestOp, TxnRequest, }; #[test] diff --git a/crates/xlineapi/src/lib.rs b/crates/xlineapi/src/lib.rs index 87b78cd28..1b88bb8e6 100644 --- a/crates/xlineapi/src/lib.rs +++ b/crates/xlineapi/src/lib.rs @@ -330,37 +330,52 @@ pub enum RequestBackend { Alarm, } -/// Get command keys from a Request for conflict check -pub trait CommandKeys { +/// Command attributes +pub trait CommandAttr { /// Key ranges fn keys(&self) -> Vec; + + /// Lease ids + fn leases(&self) -> Vec; } -impl CommandKeys for RangeRequest { +impl CommandAttr for RangeRequest { fn keys(&self) -> Vec { vec![KeyRange::new( self.key.as_slice(), self.range_end.as_slice(), )] } + + fn leases(&self) -> Vec { + vec![] + } } -impl CommandKeys for PutRequest { +impl CommandAttr for PutRequest { fn keys(&self) -> Vec { vec![KeyRange::new_one_key(self.key.as_slice())] } + + fn leases(&self) -> Vec { + vec![self.lease] + } } -impl CommandKeys for DeleteRangeRequest { +impl CommandAttr for DeleteRangeRequest { fn keys(&self) -> Vec { vec![KeyRange::new( self.key.as_slice(), self.range_end.as_slice(), )] } + + fn leases(&self) -> Vec { + vec![] + } } -impl CommandKeys for TxnRequest { +impl CommandAttr for TxnRequest { fn keys(&self) -> Vec { let mut keys: Vec<_> = self .compare @@ -391,6 +406,26 @@ impl CommandKeys for TxnRequest { keys } + + fn leases(&self) -> Vec { + let mut leases = Vec::new(); + for op in self + .success + .iter() + .chain(self.failure.iter()) + .map(|op| &op.request) + .flatten() + { + match *op { + Request::RequestPut(ref req) => { + leases.push(req.lease); + } + Request::RequestTxn(ref req) => leases.append(&mut req.leases()), + Request::RequestDeleteRange(_) | Request::RequestRange(_) => {} + } + } + leases + } } impl RequestWrapper { @@ -406,6 +441,19 @@ impl RequestWrapper { } } + /// Get leases of the request + pub fn leases(&self) -> Vec { + match *self { + RequestWrapper::RangeRequest(ref req) => req.leases(), + RequestWrapper::PutRequest(ref req) => req.leases(), + RequestWrapper::DeleteRangeRequest(ref req) => req.leases(), + RequestWrapper::TxnRequest(ref req) => req.leases(), + RequestWrapper::LeaseGrantRequest(ref req) => vec![req.id], + RequestWrapper::LeaseRevokeRequest(ref req) => vec![req.id], + _ => vec![], + } + } + /// Get the backend of the request pub fn backend(&self) -> RequestBackend { match *self { @@ -495,6 +543,11 @@ impl RequestWrapper { ) } + /// Check whether the kv request or lease request should skip the revision or not + pub fn skip_lease_revision(&self) -> bool { + matches!(self, RequestWrapper::LeaseGrantRequest(_)) + } + /// Check whether the kv request or lease request should skip the revision or not pub fn skip_general_revision(&self) -> bool { match self { diff --git a/crates/xlineapi/src/request_validation.rs b/crates/xlineapi/src/request_validation.rs index ff6ff9a86..a85ce07be 100644 --- a/crates/xlineapi/src/request_validation.rs +++ b/crates/xlineapi/src/request_validation.rs @@ -1,10 +1,12 @@ -use std::collections::HashSet; +use std::collections::{hash_map::Entry, HashMap}; use serde::{Deserialize, Serialize}; use thiserror::Error; +use utils::interval_map::{Interval, IntervalMap}; +use utils::lca_tree::LCATree; use crate::{ - command::KeyRange, AuthRoleAddRequest, AuthRoleGrantPermissionRequest, AuthUserAddRequest, + interval::BytesAffine, AuthRoleAddRequest, AuthRoleGrantPermissionRequest, AuthUserAddRequest, DeleteRangeRequest, PutRequest, RangeRequest, Request, RequestOp, SortOrder, SortTarget, TxnRequest, }; @@ -85,61 +87,133 @@ impl RequestValidator for TxnRequest { } } - let _ignore_success = check_intervals(&self.success)?; - let _ignore_failure = check_intervals(&self.failure)?; + check_intervals(&self.success)?; + check_intervals(&self.failure)?; Ok(()) } } -/// Check if puts and deletes overlap -fn check_intervals(ops: &[RequestOp]) -> Result<(HashSet<&[u8]>, Vec), ValidationError> { - // TODO: use interval tree is better? +type DelsIntervalMap<'a> = IntervalMap>; - let mut dels = Vec::new(); +fn new_bytes_affine_interval(start: &[u8], key_end: &[u8]) -> Interval { + let high = match key_end { + &[] => { + let mut end = start.to_vec(); + end.push(0); + BytesAffine::Bytes(end) + } + &[0] => BytesAffine::Unbounded, + bytes => BytesAffine::Bytes(bytes.to_vec()), + }; + Interval::new(BytesAffine::new_key(start), high) +} - for op in ops { - if let Some(Request::RequestDeleteRange(ref req)) = op.request { - // collect dels - let del = KeyRange::new(req.key.as_slice(), req.range_end.as_slice()); - dels.push(del); +/// Check if puts and deletes overlap +fn check_intervals(ops: &[RequestOp]) -> Result<(), ValidationError> { + let mut lca_tree = LCATree::new(); + // Because `dels` stores Vec corresponding to the interval, merging two `dels` is slightly cumbersome. + // Here, `dels` are directly passed into the build function + let mut dels = DelsIntervalMap::new(); + // This function will traverse all RequestOp and collect all the parent nodes corresponding to `put` and `del` operations. + // During this process, the atomicity of the put operation can be guaranteed. + let puts = build_interval_tree(ops, &mut dels, &mut lca_tree, 0)?; + + // Now we have `dels` and `puts` which contain all node index corresponding to `del` and `put` ops, + // we only need to iterate through the puts to find out whether each put overlaps with the del operation in the dels, + // and even if it overlaps, whether it satisfies lca.depth % 2 == 0. + for (put_key, put_vec) in puts { + let put_interval = new_bytes_affine_interval(put_key, &[]); + let overlaps = dels.find_all_overlap(&put_interval); + for put_node_idx in put_vec { + for (_, del_vec) in overlaps.iter() { + for del_node_idx in del_vec.iter() { + let lca_node_idx = lca_tree.find_lca(put_node_idx, *del_node_idx); + // lca.depth % 2 == 0 means this lca is on a success or failure branch, + // and two nodes on the same branch are prohibited from overlapping. + if lca_tree.get_node(lca_node_idx).depth % 2 == 0 { + return Err(ValidationError::DuplicateKey); + } + } + } } } - let mut puts: HashSet<&[u8]> = HashSet::new(); + Ok(()) +} +fn build_interval_tree<'a>( + ops: &'a [RequestOp], + dels_map: &mut DelsIntervalMap<'a>, + lca_tree: &mut LCATree, + parent: usize, +) -> Result>, ValidationError> { + let mut puts_map: HashMap<&[u8], Vec> = HashMap::new(); for op in ops { - if let Some(Request::RequestTxn(ref req)) = op.request { - // handle child txn request - let (success_puts, mut success_dels) = check_intervals(&req.success)?; - let (failure_puts, mut failure_dels) = check_intervals(&req.failure)?; - - for k in success_puts.union(&failure_puts) { - if !puts.insert(k) { - return Err(ValidationError::DuplicateKey); + match op.request { + Some(Request::RequestDeleteRange(ref req)) => { + // collect dels + let cur_node_idx = lca_tree.insert_node(parent); + let del = new_bytes_affine_interval(req.key.as_slice(), req.range_end.as_slice()); + dels_map.entry(del).or_insert(vec![]).push(cur_node_idx); + } + Some(Request::RequestTxn(ref req)) => { + // RequestTxn is absolutely a node + let cur_node_idx = lca_tree.insert_node(parent); + let success_puts_map = if !req.success.is_empty() { + // success branch is also a node + let success_node_idx = lca_tree.insert_node(cur_node_idx); + build_interval_tree(&req.success, dels_map, lca_tree, success_node_idx)? + } else { + HashMap::new() + }; + let failure_puts_map = if !req.failure.is_empty() { + // failure branch is also a node + let failure_node_idx = lca_tree.insert_node(cur_node_idx); + build_interval_tree(&req.failure, dels_map, lca_tree, failure_node_idx)? + } else { + HashMap::new() + }; + // success_puts_map and failure_puts_map cannot overlap with other op's puts_map. + for (sub_put_key, sub_put_node_idx) in success_puts_map.iter() { + if puts_map.contains_key(sub_put_key) { + return Err(ValidationError::DuplicateKey); + } + puts_map.insert(&sub_put_key, sub_put_node_idx.to_vec()); } - if dels.iter().any(|del| del.contains_key(k)) { - return Err(ValidationError::DuplicateKey); + // but they can overlap with each other + for (sub_put_key, mut sub_put_node_idx) in failure_puts_map.into_iter() { + match puts_map.entry(&sub_put_key) { + Entry::Vacant(_) => { + puts_map.insert(&sub_put_key, sub_put_node_idx); + } + Entry::Occupied(mut put_entry) => { + if !success_puts_map.contains_key(sub_put_key) { + return Err(ValidationError::DuplicateKey); + } + let put_vec = put_entry.get_mut(); + put_vec.append(&mut sub_put_node_idx); + } + }; } } - - dels.append(&mut success_dels); - dels.append(&mut failure_dels); + _ => {} } } - + // put in RequestPut cannot overlap with all puts in RequestTxn for op in ops { - if let Some(Request::RequestPut(ref req)) = op.request { - // check puts in this level - if !puts.insert(&req.key) { - return Err(ValidationError::DuplicateKey); - } - if dels.iter().any(|del| del.contains_key(&req.key)) { - return Err(ValidationError::DuplicateKey); + match op.request { + Some(Request::RequestPut(ref req)) => { + if puts_map.contains_key(&req.key.as_slice()) { + return Err(ValidationError::DuplicateKey); + } + let cur_node_idx = lca_tree.insert_node(parent); + puts_map.insert(&req.key, vec![cur_node_idx]); } + _ => {} } } - Ok((puts, dels)) + Ok(puts_map) } impl RequestValidator for AuthUserAddRequest { @@ -583,9 +657,6 @@ mod test { run_test(testcases); } - // FIXME: This test will fail in the current implementation. - // See https://github.com/xline-kv/Xline/issues/410 for more details - #[ignore] #[test] fn check_intervals_txn_nested_overlap_should_return_error() { let put_op = RequestOp { diff --git a/crates/xlinectl/Cargo.toml b/crates/xlinectl/Cargo.toml index c88b94dce..c79c5cb1a 100644 --- a/crates/xlinectl/Cargo.toml +++ b/crates/xlinectl/Cargo.toml @@ -15,10 +15,10 @@ anyhow = "1.0" clap = "4" regex = "1.10.5" serde = { version = "1.0.204", features = ["derive"] } -serde_json = "1.0.117" +serde_json = "1.0.125" shlex = "1.3.0" tokio = "1" -tonic = { version = "0.4.2", package = "madsim-tonic" } +tonic = { version = "0.5.0", package = "madsim-tonic" } utils = { path = "../utils" } workspace-hack = { version = "0.1", path = "../../workspace-hack" } xline-client = { path = "../xline-client" } diff --git a/crates/xlinectl/README.md b/crates/xlinectl/README.md index 6f09ee19d..f1cd9241b 100644 --- a/crates/xlinectl/README.md +++ b/crates/xlinectl/README.md @@ -692,7 +692,7 @@ Acquire a lock, which will return a unique key that exists so long as the lock i #### Usage ```bash -lock +lock [command arg1 arg2 ...] ``` #### Examples @@ -700,6 +700,15 @@ lock ```bash # Hold a lock named foo until `SIGINT` is received ./xlinectl lock foo +foo/df123ef45ef678f + +# Acquire lock and execute `echo lock acquired` +./xlinectl lock foo echo lock acquired +lock acquired + +# Acquire lock and execute `xlinectl put` command +./xlinectl lock foo ./xlinectl put foo bar +OK ``` ## Authentication commands diff --git a/crates/xlinectl/src/command/compaction.rs b/crates/xlinectl/src/command/compaction.rs index 64a201973..274b92d3e 100644 --- a/crates/xlinectl/src/command/compaction.rs +++ b/crates/xlinectl/src/command/compaction.rs @@ -1,9 +1,12 @@ use anyhow::Result; use clap::{arg, value_parser, ArgMatches, Command}; -use xline_client::{types::kv::CompactionRequest, Client}; +use xline_client::Client; use crate::utils::printer::Printer; +/// Temp type for build a compaction request, indicates `(revision, physical)` +type CompactionRequest = (i64, bool); + /// Definition of `compaction` command pub(crate) fn command() -> Command { Command::new("compaction") @@ -17,19 +20,13 @@ pub(crate) fn build_request(matches: &ArgMatches) -> CompactionRequest { let revision = matches.get_one::("revision").expect("required"); let physical = matches.get_flag("physical"); - let mut request = CompactionRequest::new(*revision); - - if physical { - request = request.with_physical(); - } - - request + (*revision, physical) } /// Execute the command pub(crate) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result<()> { let req = build_request(matches); - let resp = client.kv_client().compact(req).await?; + let resp = client.kv_client().compact(req.0, req.1).await?; resp.print(); Ok(()) @@ -45,11 +42,8 @@ mod tests { #[test] fn command_parse_should_be_valid() { let test_cases = vec![ - TestCase::new(vec!["compaction", "123"], Some(CompactionRequest::new(123))), - TestCase::new( - vec!["compaction", "123", "--physical"], - Some(CompactionRequest::new(123).with_physical()), - ), + TestCase::new(vec!["compaction", "123"], Some((123, false))), + TestCase::new(vec!["compaction", "123", "--physical"], Some((123, true))), ]; for case in test_cases { diff --git a/crates/xlinectl/src/command/delete.rs b/crates/xlinectl/src/command/delete.rs index 2f7577229..689454023 100644 --- a/crates/xlinectl/src/command/delete.rs +++ b/crates/xlinectl/src/command/delete.rs @@ -1,9 +1,12 @@ use anyhow::Result; use clap::{arg, ArgMatches, Command}; -use xline_client::{types::kv::DeleteRangeRequest, Client}; +use xline_client::{types::kv::DeleteRangeOptions, Client}; use crate::utils::printer::Printer; +/// temp type to pass `(key, delete range options)` +type DeleteRangeRequest = (String, DeleteRangeOptions); + /// Definition of `delete` command pub(crate) fn command() -> Command { Command::new("delete") @@ -32,25 +35,25 @@ pub(crate) fn build_request(matches: &ArgMatches) -> DeleteRangeRequest { let prev_kv = matches.get_flag("prev_kv"); let from_key = matches.get_flag("from_key"); - let mut request = DeleteRangeRequest::new(key.as_bytes()); + let mut options = DeleteRangeOptions::default(); if let Some(range_end) = range_end { - request = request.with_range_end(range_end.as_bytes()); + options = options.with_range_end(range_end.as_bytes()); } if prefix { - request = request.with_prefix(); + options = options.with_prefix(); } - request = request.with_prev_kv(prev_kv); + options = options.with_prev_kv(prev_kv); if from_key { - request = request.with_from_key(); + options = options.with_from_key(); } - request + (key.to_owned(), options) } /// Execute the command pub(crate) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result<()> { let req = build_request(matches); - let resp = client.kv_client().delete(req).await?; + let resp = client.kv_client().delete(req.0, Some(req.1)).await?; resp.print(); Ok(()) @@ -68,23 +71,29 @@ mod tests { let test_cases = vec![ TestCase::new( vec!["delete", "key1"], - Some(DeleteRangeRequest::new("key1".as_bytes())), + Some(("key1".into(), DeleteRangeOptions::default())), ), TestCase::new( vec!["delete", "key2", "end2"], - Some(DeleteRangeRequest::new("key2".as_bytes()).with_range_end("end2".as_bytes())), + Some(( + "key2".into(), + DeleteRangeOptions::default().with_range_end("end2".as_bytes()), + )), ), TestCase::new( vec!["delete", "key3", "--prefix"], - Some(DeleteRangeRequest::new("key3".as_bytes()).with_prefix()), + Some(("key3".into(), DeleteRangeOptions::default().with_prefix())), ), TestCase::new( vec!["delete", "key4", "--prev_kv"], - Some(DeleteRangeRequest::new("key4".as_bytes()).with_prev_kv(true)), + Some(( + "key4".into(), + DeleteRangeOptions::default().with_prev_kv(true), + )), ), TestCase::new( vec!["delete", "key5", "--from_key"], - Some(DeleteRangeRequest::new("key5".as_bytes()).with_from_key()), + Some(("key5".into(), DeleteRangeOptions::default().with_from_key())), ), ]; diff --git a/crates/xlinectl/src/command/get.rs b/crates/xlinectl/src/command/get.rs index 0feaad007..d7ed32ec7 100644 --- a/crates/xlinectl/src/command/get.rs +++ b/crates/xlinectl/src/command/get.rs @@ -1,10 +1,13 @@ use anyhow::Result; use clap::{arg, value_parser, ArgMatches, Command}; -use xline_client::{types::kv::RangeRequest, Client}; +use xline_client::{types::kv::RangeOptions, Client}; use xlineapi::{SortOrder, SortTarget}; use crate::utils::printer::Printer; +/// Temp struct for building command, indicates `(key, rangeoptions)` +type RangeRequest = (Vec, RangeOptions); + /// Definition of `get` command pub(crate) fn command() -> Command { Command::new("get") @@ -66,24 +69,24 @@ pub(crate) fn build_request(matches: &ArgMatches) -> RangeRequest { let keys_only = matches.get_flag("keys_only"); let count_only = matches.get_flag("count_only"); - let mut request = RangeRequest::new(key.as_bytes()); + let mut options = RangeOptions::default(); if let Some(range_end) = range_end { - request = request.with_range_end(range_end.as_bytes()); + options = options.with_range_end(range_end.as_bytes()); } - request = match consistency.as_str() { - "L" => request.with_serializable(false), - "S" => request.with_serializable(true), + options = match consistency.as_str() { + "L" => options.with_serializable(false), + "S" => options.with_serializable(true), _ => unreachable!("The format should be checked by Clap."), }; if let Some(order) = order { - request = request.with_sort_order(match order.as_str() { + options = options.with_sort_order(match order.as_str() { "ASCEND" => SortOrder::Ascend, "DESCEND" => SortOrder::Descend, _ => unreachable!("The format should be checked by Clap."), }); } if let Some(sort_by) = sort_by { - request = request.with_sort_target(match sort_by.as_str() { + options = options.with_sort_target(match sort_by.as_str() { "CREATE" => SortTarget::Create, "KEY" => SortTarget::Key, "MODIFY" => SortTarget::Mod, @@ -92,24 +95,24 @@ pub(crate) fn build_request(matches: &ArgMatches) -> RangeRequest { _ => unreachable!("The format should be checked by Clap."), }); } - request = request.with_limit(*limit); + options = options.with_limit(*limit); if prefix { - request = request.with_prefix(); + options = options.with_prefix(); } if from_key { - request = request.with_from_key(); + options = options.with_from_key(); } - request = request.with_revision(*rev); - request = request.with_keys_only(keys_only); - request = request.with_count_only(count_only); + options = options.with_revision(*rev); + options = options.with_keys_only(keys_only); + options = options.with_count_only(count_only); - request + (key.as_bytes().to_vec(), options) } /// Execute the command pub(crate) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result<()> { - let req = build_request(matches); - let resp = client.kv_client().range(req).await?; + let (key, options) = build_request(matches); + let resp = client.kv_client().range(key, Some(options)).await?; resp.print(); Ok(()) @@ -127,47 +130,59 @@ mod tests { let test_cases = vec![ TestCase::new( vec!["get", "key"], - Some(RangeRequest::new("key".as_bytes())), + Some(("key".into(), RangeOptions::default())), ), TestCase::new( vec!["get", "key", "key2"], - Some(RangeRequest::new("key".as_bytes()).with_range_end("key2".as_bytes())), + Some(( + "key".into(), + RangeOptions::default().with_range_end("key2".as_bytes()), + )), ), TestCase::new( vec!["get", "key", "--consistency", "L"], - Some(RangeRequest::new("key".as_bytes()).with_serializable(false)), + Some(( + "key".into(), + RangeOptions::default().with_serializable(false), + )), ), TestCase::new( vec!["get", "key", "--order", "DESCEND"], - Some(RangeRequest::new("key".as_bytes()).with_sort_order(SortOrder::Descend)), + Some(( + "key".into(), + RangeOptions::default().with_sort_order(SortOrder::Descend), + )), ), TestCase::new( vec!["get", "key", "--sort_by", "MODIFY"], - Some(RangeRequest::new("key".as_bytes()).with_sort_target(SortTarget::Mod)), + Some(( + "key".into(), + RangeOptions::default().with_sort_target(SortTarget::Mod), + )), ), TestCase::new( vec!["get", "key", "--limit", "10"], - Some(RangeRequest::new("key".as_bytes()).with_limit(10)), + Some(("key".into(), RangeOptions::default().with_limit(10))), ), TestCase::new( vec!["get", "key", "--prefix"], - Some(RangeRequest::new("key".as_bytes()).with_prefix()), + Some(("key".into(), RangeOptions::default().with_prefix())), ), TestCase::new( vec!["get", "key", "--from_key"], - Some(RangeRequest::new("key".as_bytes()).with_from_key()), + Some(("key".into(), RangeOptions::default().with_from_key())), ), TestCase::new( vec!["get", "key", "--rev", "5"], - Some(RangeRequest::new("key".as_bytes()).with_revision(5)), + Some(("key".into(), RangeOptions::default().with_revision(5))), ), TestCase::new( vec!["get", "key", "--keys_only"], - Some(RangeRequest::new("key".as_bytes()).with_keys_only(true)), + Some(("key".into(), RangeOptions::default().with_keys_only(true))), ), TestCase::new( vec!["get", "key", "--count_only"], - Some(RangeRequest::new("key".as_bytes()).with_count_only(true)), + Some(("key".into(), RangeOptions::default().with_count_only(true))), ), ]; diff --git a/crates/xlinectl/src/command/lease/grant.rs b/crates/xlinectl/src/command/lease/grant.rs index 3b3107434..fe452e775 100644 --- a/crates/xlinectl/src/command/lease/grant.rs +++ b/crates/xlinectl/src/command/lease/grant.rs @@ -1,5 +1,5 @@ use clap::{arg, value_parser, ArgMatches, Command}; -use xline_client::{error::Result, types::lease::LeaseGrantRequest, Client}; +use xline_client::{error::Result, Client}; use crate::utils::printer::Printer; @@ -11,15 +11,15 @@ pub(super) fn command() -> Command { } /// Build request from matches -pub(super) fn build_request(matches: &ArgMatches) -> LeaseGrantRequest { +pub(super) fn build_request(matches: &ArgMatches) -> i64 { let ttl = matches.get_one::("ttl").expect("required"); - LeaseGrantRequest::new(*ttl) + *ttl } /// Execute the command pub(super) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result<()> { - let request = build_request(matches); - let resp = client.lease_client().grant(request).await?; + let ttl = build_request(matches); + let resp = client.lease_client().grant(ttl, None).await?; resp.print(); Ok(()) @@ -30,14 +30,11 @@ mod tests { use super::*; use crate::test_case_struct; - test_case_struct!(LeaseGrantRequest); + test_case_struct!(i64); #[test] fn command_parse_should_be_valid() { - let test_cases = vec![TestCase::new( - vec!["grant", "100"], - Some(LeaseGrantRequest::new(100)), - )]; + let test_cases = vec![TestCase::new(vec!["grant", "100"], Some(100))]; for case in test_cases { case.run_test(); diff --git a/crates/xlinectl/src/command/lease/keep_alive.rs b/crates/xlinectl/src/command/lease/keep_alive.rs index 67a208b21..fddfbab8a 100644 --- a/crates/xlinectl/src/command/lease/keep_alive.rs +++ b/crates/xlinectl/src/command/lease/keep_alive.rs @@ -5,7 +5,7 @@ use tokio::signal::ctrl_c; use tonic::Streaming; use xline_client::{ error::{Result, XlineClientError}, - types::lease::{LeaseKeepAliveRequest, LeaseKeeper}, + types::lease::LeaseKeeper, Client, }; use xlineapi::LeaseKeepAliveResponse; @@ -21,9 +21,9 @@ pub(super) fn command() -> Command { } /// Build request from matches -pub(super) fn build_request(matches: &ArgMatches) -> LeaseKeepAliveRequest { +pub(super) fn build_request(matches: &ArgMatches) -> i64 { let lease_id = matches.get_one::("leaseId").expect("required"); - LeaseKeepAliveRequest::new(*lease_id) + *lease_id } /// Execute the command @@ -80,19 +80,13 @@ mod tests { use super::*; use crate::test_case_struct; - test_case_struct!(LeaseKeepAliveRequest); + test_case_struct!(i64); #[test] fn command_parse_should_be_valid() { let test_cases = vec![ - TestCase::new( - vec!["keep_alive", "123"], - Some(LeaseKeepAliveRequest::new(123)), - ), - TestCase::new( - vec!["keep_alive", "456", "--once"], - Some(LeaseKeepAliveRequest::new(456)), - ), + TestCase::new(vec!["keep_alive", "123"], Some(123)), + TestCase::new(vec!["keep_alive", "456", "--once"], Some(456)), ]; for case in test_cases { diff --git a/crates/xlinectl/src/command/lease/revoke.rs b/crates/xlinectl/src/command/lease/revoke.rs index 1ccbdaf4a..12c9b6cce 100644 --- a/crates/xlinectl/src/command/lease/revoke.rs +++ b/crates/xlinectl/src/command/lease/revoke.rs @@ -1,5 +1,5 @@ use clap::{arg, value_parser, ArgMatches, Command}; -use xline_client::{error::Result, types::lease::LeaseRevokeRequest, Client}; +use xline_client::{error::Result, Client}; use crate::utils::printer::Printer; @@ -11,9 +11,9 @@ pub(super) fn command() -> Command { } /// Build request from matches -pub(super) fn build_request(matches: &ArgMatches) -> LeaseRevokeRequest { +pub(super) fn build_request(matches: &ArgMatches) -> i64 { let lease_id = matches.get_one::("leaseId").expect("required"); - LeaseRevokeRequest::new(*lease_id) + *lease_id } /// Execute the command @@ -30,14 +30,11 @@ mod tests { use super::*; use crate::test_case_struct; - test_case_struct!(LeaseRevokeRequest); + test_case_struct!(i64); #[test] fn command_parse_should_be_valid() { - let test_cases = vec![TestCase::new( - vec!["revoke", "123"], - Some(LeaseRevokeRequest::new(123)), - )]; + let test_cases = vec![TestCase::new(vec!["revoke", "123"], Some(123))]; for case in test_cases { case.run_test(); diff --git a/crates/xlinectl/src/command/lease/timetolive.rs b/crates/xlinectl/src/command/lease/timetolive.rs index b9bad3262..2860285ff 100644 --- a/crates/xlinectl/src/command/lease/timetolive.rs +++ b/crates/xlinectl/src/command/lease/timetolive.rs @@ -1,5 +1,5 @@ use clap::{arg, value_parser, ArgMatches, Command}; -use xline_client::{error::Result, types::lease::LeaseTimeToLiveRequest, Client}; +use xline_client::{error::Result, Client}; use crate::utils::printer::Printer; @@ -11,15 +11,15 @@ pub(super) fn command() -> Command { } /// Build request from matches -pub(super) fn build_request(matches: &ArgMatches) -> LeaseTimeToLiveRequest { +pub(super) fn build_request(matches: &ArgMatches) -> i64 { let lease_id = matches.get_one::("leaseId").expect("required"); - LeaseTimeToLiveRequest::new(*lease_id) + *lease_id } /// Execute the command pub(super) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result<()> { let req = build_request(matches); - let resp = client.lease_client().time_to_live(req).await?; + let resp = client.lease_client().time_to_live(req, false).await?; resp.print(); Ok(()) @@ -30,14 +30,11 @@ mod tests { use super::*; use crate::test_case_struct; - test_case_struct!(LeaseTimeToLiveRequest); + test_case_struct!(i64); #[test] fn command_parse_should_be_valid() { - let test_cases = vec![TestCase::new( - vec!["timetolive", "123"], - Some(LeaseTimeToLiveRequest::new(123)), - )]; + let test_cases = vec![TestCase::new(vec!["timetolive", "123"], Some(123))]; for case in test_cases { case.run_test(); diff --git a/crates/xlinectl/src/command/lock.rs b/crates/xlinectl/src/command/lock.rs index 09214b1af..0b00559d2 100644 --- a/crates/xlinectl/src/command/lock.rs +++ b/crates/xlinectl/src/command/lock.rs @@ -1,4 +1,5 @@ use clap::{arg, ArgMatches, Command}; +use std::process::Command as StdCommand; use tokio::signal; use xline_client::{clients::Xutex, error::Result, Client}; @@ -7,6 +8,7 @@ pub(crate) fn command() -> Command { Command::new("lock") .about("Acquire a lock, which will return a unique key that exists so long as the lock is held") .arg(arg!( "name of the lock")) + .arg(arg!( "command to execute").num_args(1..).required(false)) } /// Execute the command @@ -15,9 +17,84 @@ pub(crate) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result let mut xutex = Xutex::new(client.lock_client(), prefix, None, None).await?; let xutex_guard = xutex.lock_unsafe().await?; - println!("{}", xutex_guard.key()); - signal::ctrl_c().await.expect("failed to listen for event"); + + let exec_command = matches.get_many::("exec_command"); + let mut should_wait_for_ctrl_c = true; + + if let Some(exec_command) = exec_command { + let exec_command_vec: Vec<&String> = exec_command.collect(); + if !exec_command_vec.is_empty() { + let output = execute_exec_command(&exec_command_vec); + println!("{output}"); + should_wait_for_ctrl_c = false; + } + } + if should_wait_for_ctrl_c { + println!("{}", xutex_guard.key()); + signal::ctrl_c().await.expect("failed to listen for event"); + println!("releasing the lock, "); + } // let res = lock_resp.unlock().await; - println!("releasing the lock, "); Ok(()) } + +/// Execute an exec command +fn execute_exec_command(command_and_args: &[&String]) -> String { + let (command, args) = command_and_args + .split_first() + .expect("Expected at least one exec command"); + let output = StdCommand::new(command) + .args(args) + .output() + .expect("failed to execute command"); + String::from_utf8_lossy(&output.stdout).trim().to_owned() +} + +#[cfg(test)] +mod tests { + use super::*; + + struct TestCase { + exec_command: Vec<&'static str>, + expected_output: String, + } + + impl TestCase { + fn new(exec_command: Vec<&'static str>, expected_output: String) -> TestCase { + TestCase { + exec_command, + expected_output, + } + } + + fn run_test(&self) { + let exec_command_owned: Vec = + self.exec_command.iter().map(ToString::to_string).collect(); + let exec_command_refs: Vec<&String> = exec_command_owned.iter().collect(); + let output = execute_exec_command(&exec_command_refs); + + assert_eq!( + output.trim(), + self.expected_output.trim(), + "Failed executing {:?}. Expected output: {}, got: {}", + self.exec_command, + self.expected_output, + output + ); + } + } + + #[test] + fn test_execute_exec_command() { + let test_cases = vec![ + TestCase::new(vec!["echo", "success"], "success".to_owned()), + TestCase::new(vec!["echo", "fail"], "fail".to_owned()), + TestCase::new(vec!["echo", "lock acquired"], "lock acquired".to_owned()), + TestCase::new(vec!["echo", "-n"], String::new()), + ]; + + for test_case in test_cases { + test_case.run_test(); + } + } +} diff --git a/crates/xlinectl/src/command/member/add.rs b/crates/xlinectl/src/command/member/add.rs index e0771f66b..e16e04d97 100644 --- a/crates/xlinectl/src/command/member/add.rs +++ b/crates/xlinectl/src/command/member/add.rs @@ -1,9 +1,12 @@ use clap::{arg, ArgMatches, Command}; -use xline_client::{error::Result, types::cluster::MemberAddRequest, Client}; +use xline_client::{error::Result, Client}; use super::parse_peer_urls; use crate::utils::printer::Printer; +/// Temp type for cluster member `add` command, indicates `(peer_urls, is_learner)` +type MemberAddRequest = (Vec, bool); + /// Definition of `add` command pub(super) fn command() -> Command { Command::new("add") @@ -22,13 +25,16 @@ pub(super) fn build_request(matches: &ArgMatches) -> MemberAddRequest { .expect("required"); let is_learner = matches.get_flag("is_learner"); - MemberAddRequest::new(peer_urls.clone(), is_learner) + (peer_urls.clone(), is_learner) } /// Execute the command pub(super) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result<()> { let request = build_request(matches); - let resp = client.cluster_client().member_add(request).await?; + let resp = client + .cluster_client() + .member_add(request.0, request.1) + .await?; resp.print(); Ok(()) @@ -46,12 +52,12 @@ mod tests { let test_cases = vec![ TestCase::new( vec!["add", "127.0.0.1:2379", "--is_learner"], - Some(MemberAddRequest::new(["127.0.0.1:2379".to_owned()], true)), + Some((["127.0.0.1:2379".to_owned()].into(), true)), ), TestCase::new( vec!["add", "127.0.0.1:2379,127.0.0.1:2380"], - Some(MemberAddRequest::new( - ["127.0.0.1:2379".to_owned(), "127.0.0.1:2380".to_owned()], + Some(( + ["127.0.0.1:2379".to_owned(), "127.0.0.1:2380".to_owned()].into(), false, )), ), diff --git a/crates/xlinectl/src/command/member/list.rs b/crates/xlinectl/src/command/member/list.rs index 7612783f9..269a7365d 100644 --- a/crates/xlinectl/src/command/member/list.rs +++ b/crates/xlinectl/src/command/member/list.rs @@ -1,5 +1,5 @@ use clap::{arg, ArgMatches, Command}; -use xline_client::{error::Result, types::cluster::MemberListRequest, Client}; +use xline_client::{error::Result, Client}; use crate::utils::printer::Printer; @@ -11,10 +11,8 @@ pub(super) fn command() -> Command { } /// Build request from matches -pub(super) fn build_request(matches: &ArgMatches) -> MemberListRequest { - let linearizable = matches.get_flag("linearizable"); - - MemberListRequest::new(linearizable) +pub(super) fn build_request(matches: &ArgMatches) -> bool { + matches.get_flag("linearizable") } /// Execute the command @@ -31,14 +29,14 @@ mod tests { use super::*; use crate::test_case_struct; - test_case_struct!(MemberListRequest); + test_case_struct!(bool); #[test] fn command_parse_should_be_valid() { - let test_cases = vec![TestCase::new( - vec!["list", "--linearizable"], - Some(MemberListRequest::new(true)), - )]; + let test_cases = vec![ + TestCase::new(vec!["list", "--linearizable"], Some(true)), + TestCase::new(vec!["list"], Some(false)), + ]; for case in test_cases { case.run_test(); diff --git a/crates/xlinectl/src/command/member/promote.rs b/crates/xlinectl/src/command/member/promote.rs index 4d5e9de53..3e4be7da1 100644 --- a/crates/xlinectl/src/command/member/promote.rs +++ b/crates/xlinectl/src/command/member/promote.rs @@ -1,5 +1,5 @@ use clap::{arg, value_parser, ArgMatches, Command}; -use xline_client::{error::Result, types::cluster::MemberPromoteRequest, Client}; +use xline_client::{error::Result, Client}; use crate::utils::printer::Printer; @@ -11,10 +11,8 @@ pub(super) fn command() -> Command { } /// Build request from matches -pub(super) fn build_request(matches: &ArgMatches) -> MemberPromoteRequest { - let member_id = matches.get_one::("ID").expect("required"); - - MemberPromoteRequest::new(*member_id) +pub(super) fn build_request(matches: &ArgMatches) -> u64 { + *matches.get_one::("ID").expect("required") } /// Execute the command @@ -31,14 +29,11 @@ mod tests { use super::*; use crate::test_case_struct; - test_case_struct!(MemberPromoteRequest); + test_case_struct!(u64); #[test] fn command_parse_should_be_valid() { - let test_cases = vec![TestCase::new( - vec!["remove", "1"], - Some(MemberPromoteRequest::new(1)), - )]; + let test_cases = vec![TestCase::new(vec!["remove", "1"], Some(1))]; for case in test_cases { case.run_test(); diff --git a/crates/xlinectl/src/command/member/remove.rs b/crates/xlinectl/src/command/member/remove.rs index 667e762cd..b13a49015 100644 --- a/crates/xlinectl/src/command/member/remove.rs +++ b/crates/xlinectl/src/command/member/remove.rs @@ -1,5 +1,5 @@ use clap::{arg, value_parser, ArgMatches, Command}; -use xline_client::{error::Result, types::cluster::MemberRemoveRequest, Client}; +use xline_client::{error::Result, Client}; use crate::utils::printer::Printer; @@ -11,10 +11,8 @@ pub(super) fn command() -> Command { } /// Build request from matches -pub(super) fn build_request(matches: &ArgMatches) -> MemberRemoveRequest { - let member_id = matches.get_one::("ID").expect("required"); - - MemberRemoveRequest::new(*member_id) +pub(super) fn build_request(matches: &ArgMatches) -> u64 { + *matches.get_one::("ID").expect("required") } /// Execute the command @@ -31,14 +29,11 @@ mod tests { use super::*; use crate::test_case_struct; - test_case_struct!(MemberRemoveRequest); + test_case_struct!(u64); #[test] fn command_parse_should_be_valid() { - let test_cases = vec![TestCase::new( - vec!["remove", "1"], - Some(MemberRemoveRequest::new(1)), - )]; + let test_cases = vec![TestCase::new(vec!["remove", "1"], Some(1))]; for case in test_cases { case.run_test(); diff --git a/crates/xlinectl/src/command/member/update.rs b/crates/xlinectl/src/command/member/update.rs index 59fc9f310..17db4566a 100644 --- a/crates/xlinectl/src/command/member/update.rs +++ b/crates/xlinectl/src/command/member/update.rs @@ -1,9 +1,12 @@ use clap::{arg, value_parser, ArgMatches, Command}; -use xline_client::{error::Result, types::cluster::MemberUpdateRequest, Client}; +use xline_client::{error::Result, Client}; use super::parse_peer_urls; use crate::utils::printer::Printer; +/// Temp type for request and testing, indicates `(id, peer_urls)` +type MemberUpdateRequest = (u64, Vec); + /// Definition of `update` command pub(super) fn command() -> Command { Command::new("update") @@ -22,13 +25,16 @@ pub(super) fn build_request(matches: &ArgMatches) -> MemberUpdateRequest { .get_one::>("peer_urls") .expect("required"); - MemberUpdateRequest::new(*member_id, peer_urls.clone()) + (*member_id, peer_urls.clone()) } /// Execute the command pub(super) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result<()> { let request = build_request(matches); - let resp = client.cluster_client().member_update(request).await?; + let resp = client + .cluster_client() + .member_update(request.0, request.1) + .await?; resp.print(); Ok(()) @@ -46,13 +52,13 @@ mod tests { let test_cases = vec![ TestCase::new( vec!["update", "1", "127.0.0.1:2379"], - Some(MemberUpdateRequest::new(1, ["127.0.0.1:2379".to_owned()])), + Some((1, ["127.0.0.1:2379".to_owned()].into())), ), TestCase::new( vec!["update", "2", "127.0.0.1:2379,127.0.0.1:2380"], - Some(MemberUpdateRequest::new( + Some(( 2, - ["127.0.0.1:2379".to_owned(), "127.0.0.1:2380".to_owned()], + ["127.0.0.1:2379".to_owned(), "127.0.0.1:2380".to_owned()].into(), )), ), ]; diff --git a/crates/xlinectl/src/command/put.rs b/crates/xlinectl/src/command/put.rs index 9b788a839..9e4d406ff 100644 --- a/crates/xlinectl/src/command/put.rs +++ b/crates/xlinectl/src/command/put.rs @@ -1,9 +1,14 @@ use anyhow::Result; use clap::{arg, value_parser, ArgMatches, Command}; -use xline_client::{types::kv::PutRequest, Client}; +use xline_client::{types::kv::PutOptions, Client}; use crate::utils::printer::Printer; +/// Indicates the type of the request builted. +/// The first `Vec` is the key, the second `Vec` is the value, +/// and the third is the options. +type PutRequest = (Vec, Vec, PutOptions); + /// Definition of `get` command pub(crate) fn command() -> Command { Command::new("put") @@ -30,19 +35,19 @@ pub(crate) fn build_request(matches: &ArgMatches) -> PutRequest { let ignore_value = matches.get_flag("ignore_value"); let ignore_lease = matches.get_flag("ignore_lease"); - let mut request = PutRequest::new(key.as_bytes(), value.as_bytes()); - request = request.with_lease(*lease); - request = request.with_prev_kv(prev_kv); - request = request.with_ignore_value(ignore_value); - request = request.with_ignore_lease(ignore_lease); + let request = PutOptions::default() + .with_lease(*lease) + .with_prev_kv(prev_kv) + .with_ignore_value(ignore_value) + .with_ignore_lease(ignore_lease); - request + (key.as_bytes().to_vec(), value.as_bytes().to_vec(), request) } /// Execute the command pub(crate) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result<()> { let req = build_request(matches); - let resp = client.kv_client().put(req).await?; + let resp = client.kv_client().put(req.0, req.1, Some(req.2)).await?; resp.print(); Ok(()) @@ -60,27 +65,39 @@ mod tests { let test_cases = vec![ TestCase::new( vec!["put", "key1", "value1"], - Some(PutRequest::new("key1".as_bytes(), "value1".as_bytes())), + Some(("key1".into(), "value1".into(), PutOptions::default())), ), TestCase::new( vec!["put", "key2", "value2", "--lease", "1"], - Some(PutRequest::new("key2".as_bytes(), "value2".as_bytes()).with_lease(1)), + Some(( + "key2".into(), + "value2".into(), + PutOptions::default().with_lease(1), + )), ), TestCase::new( vec!["put", "key3", "value3", "--prev_kv"], - Some(PutRequest::new("key3".as_bytes(), "value3".as_bytes()).with_prev_kv(true)), + Some(( + "key3".into(), + "value3".into(), + PutOptions::default().with_prev_kv(true), + )), ), TestCase::new( vec!["put", "key4", "value4", "--ignore_value"], - Some( - PutRequest::new("key4".as_bytes(), "value4".as_bytes()).with_ignore_value(true), - ), + Some(( + "key4".into(), + "value4".into(), + PutOptions::default().with_ignore_value(true), + )), ), TestCase::new( vec!["put", "key5", "value5", "--ignore_lease"], - Some( - PutRequest::new("key5".as_bytes(), "value5".as_bytes()).with_ignore_lease(true), - ), + Some(( + "key5".into(), + "value5".into(), + PutOptions::default().with_ignore_lease(true), + )), ), ]; diff --git a/crates/xlinectl/src/command/role/add.rs b/crates/xlinectl/src/command/role/add.rs index 19dc4a791..50201b54e 100644 --- a/crates/xlinectl/src/command/role/add.rs +++ b/crates/xlinectl/src/command/role/add.rs @@ -1,5 +1,5 @@ use clap::{arg, ArgMatches, Command}; -use xline_client::{error::Result, types::auth::AuthRoleAddRequest, Client}; +use xline_client::{error::Result, Client}; use crate::utils::printer::Printer; @@ -11,17 +11,20 @@ pub(super) fn command() -> Command { } /// Build request from matches -pub(super) fn build_request(matches: &ArgMatches) -> AuthRoleAddRequest { +/// +/// # Returns +/// +/// name of the role +pub(super) fn build_request(matches: &ArgMatches) -> String { let name = matches.get_one::("name").expect("required"); - AuthRoleAddRequest::new(name) + name.into() } /// Execute the command pub(super) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result<()> { - let req = build_request(matches); - let resp = client.auth_client().role_add(req).await?; + let name = build_request(matches); + let resp = client.auth_client().role_add(name).await?; resp.print(); - Ok(()) } @@ -30,14 +33,11 @@ mod tests { use super::*; use crate::test_case_struct; - test_case_struct!(AuthRoleAddRequest); + test_case_struct!(String); #[test] fn command_parse_should_be_valid() { - let test_cases = vec![TestCase::new( - vec!["add", "Admin"], - Some(AuthRoleAddRequest::new("Admin")), - )]; + let test_cases = vec![TestCase::new(vec!["add", "Admin"], Some("Admin".into()))]; for case in test_cases { case.run_test(); diff --git a/crates/xlinectl/src/command/role/delete.rs b/crates/xlinectl/src/command/role/delete.rs index 40b2f533f..de705bc89 100644 --- a/crates/xlinectl/src/command/role/delete.rs +++ b/crates/xlinectl/src/command/role/delete.rs @@ -1,5 +1,5 @@ use clap::{arg, ArgMatches, Command}; -use xline_client::{error::Result, types::auth::AuthRoleDeleteRequest, Client}; +use xline_client::{error::Result, Client}; use crate::utils::printer::Printer; @@ -11,9 +11,11 @@ pub(super) fn command() -> Command { } /// Build request from matches -pub(super) fn build_request(matches: &ArgMatches) -> AuthRoleDeleteRequest { +/// +/// Returns the name of the role to be deleted +pub(super) fn build_request(matches: &ArgMatches) -> String { let name = matches.get_one::("name").expect("required"); - AuthRoleDeleteRequest::new(name) + name.to_owned() } /// Execute the command @@ -30,14 +32,11 @@ mod tests { use super::*; use crate::test_case_struct; - test_case_struct!(AuthRoleDeleteRequest); + test_case_struct!(String); #[test] fn command_parse_should_be_valid() { - let test_cases = vec![TestCase::new( - vec!["delete", "Admin"], - Some(AuthRoleDeleteRequest::new("Admin")), - )]; + let test_cases = vec![TestCase::new(vec!["delete", "Admin"], Some("Admin".into()))]; for case in test_cases { case.run_test(); diff --git a/crates/xlinectl/src/command/role/get.rs b/crates/xlinectl/src/command/role/get.rs index 46c786fab..3fe7236e6 100644 --- a/crates/xlinectl/src/command/role/get.rs +++ b/crates/xlinectl/src/command/role/get.rs @@ -1,5 +1,5 @@ use clap::{arg, ArgMatches, Command}; -use xline_client::{error::Result, types::auth::AuthRoleGetRequest, Client}; +use xline_client::{error::Result, Client}; use crate::utils::printer::Printer; @@ -11,9 +11,9 @@ pub(super) fn command() -> Command { } /// Build request from matches -pub(super) fn build_request(matches: &ArgMatches) -> AuthRoleGetRequest { +pub(super) fn build_request(matches: &ArgMatches) -> String { let name = matches.get_one::("name").expect("required"); - AuthRoleGetRequest::new(name) + name.to_owned() } /// Execute the command @@ -30,14 +30,11 @@ mod tests { use super::*; use crate::test_case_struct; - test_case_struct!(AuthRoleGetRequest); + test_case_struct!(String); #[test] fn command_parse_should_be_valid() { - let test_cases = vec![TestCase::new( - vec!["get", "Admin"], - Some(AuthRoleGetRequest::new("Admin")), - )]; + let test_cases = vec![TestCase::new(vec!["get", "Admin"], Some("Admin".into()))]; for case in test_cases { case.run_test(); diff --git a/crates/xlinectl/src/command/role/grant_perm.rs b/crates/xlinectl/src/command/role/grant_perm.rs index c4c0ac91d..d81b0f41d 100644 --- a/crates/xlinectl/src/command/role/grant_perm.rs +++ b/crates/xlinectl/src/command/role/grant_perm.rs @@ -1,13 +1,12 @@ use clap::{arg, ArgMatches, Command}; -use xline_client::{ - error::Result, - types::auth::{AuthRoleGrantPermissionRequest, Permission}, - Client, -}; +use xline_client::{error::Result, types::range_end::RangeOption, Client}; use xlineapi::Type; use crate::utils::printer::Printer; +/// Temp return type for `grant_perm` command, indicates `(name, PermissionType, key, RangeOption)` +type AuthRoleGrantPermissionRequest = (String, Type, Vec, Option); + /// Definition of `grant_perm` command pub(super) fn command() -> Command { Command::new("grant_perm") @@ -32,34 +31,36 @@ pub(super) fn build_request(matches: &ArgMatches) -> AuthRoleGrantPermissionRequ let prefix = matches.get_flag("prefix"); let from_key = matches.get_flag("from_key"); - let perm_type = match perm_type_local.as_str() { - "Read" => Type::Read, - "Write" => Type::Write, - "ReadWrite" => Type::Readwrite, + let perm_type = match perm_type_local.to_lowercase().as_str() { + "read" => Type::Read, + "write" => Type::Write, + "readwrite" => Type::Readwrite, _ => unreachable!("should be checked by clap"), }; - let mut perm = Permission::new(perm_type, key.as_bytes()); - - if let Some(range_end) = range_end { - perm = perm.with_range_end(range_end.as_bytes()); + let range_option = if prefix { + Some(RangeOption::Prefix) + } else if from_key { + Some(RangeOption::FromKey) + } else { + range_end.map(|inner| RangeOption::RangeEnd(inner.as_bytes().to_vec())) }; - if prefix { - perm = perm.with_prefix(); - } - - if from_key { - perm = perm.with_from_key(); - } - - AuthRoleGrantPermissionRequest::new(name, perm) + ( + name.to_owned(), + perm_type, + key.as_bytes().to_vec(), + range_option, + ) } /// Execute the command pub(super) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result<()> { let req = build_request(matches); - let resp = client.auth_client().role_grant_permission(req).await?; + let resp = client + .auth_client() + .role_grant_permission(req.0, req.1, req.2, req.3) + .await?; resp.print(); Ok(()) @@ -77,16 +78,20 @@ mod tests { let test_cases = vec![ TestCase::new( vec!["grant_perm", "Admin", "Read", "key1", "key2"], - Some(AuthRoleGrantPermissionRequest::new( - "Admin", - Permission::new(Type::Read, "key1").with_range_end("key2"), + Some(( + "Admin".into(), + Type::Read, + "key1".into(), + Some(RangeOption::RangeEnd("key2".into())), )), ), TestCase::new( vec!["grant_perm", "Admin", "Write", "key3", "--from_key"], - Some(AuthRoleGrantPermissionRequest::new( - "Admin", - Permission::new(Type::Write, "key3").with_from_key(), + Some(( + "Admin".into(), + Type::Write, + "key3".into(), + Some(RangeOption::FromKey), )), ), ]; diff --git a/crates/xlinectl/src/command/role/revoke_perm.rs b/crates/xlinectl/src/command/role/revoke_perm.rs index 8ba5c2071..8973c605b 100644 --- a/crates/xlinectl/src/command/role/revoke_perm.rs +++ b/crates/xlinectl/src/command/role/revoke_perm.rs @@ -1,8 +1,11 @@ use clap::{arg, ArgMatches, Command}; -use xline_client::{error::Result, types::auth::AuthRoleRevokePermissionRequest, Client}; +use xline_client::{error::Result, types::range_end::RangeOption, Client}; use crate::utils::printer::Printer; +/// Temp request type for `revoke_perm` command +type AuthRoleRevokePermissionRequest = (String, Vec, Option); + /// Definition of `revoke_perm` command pub(super) fn command() -> Command { Command::new("revoke_perm") @@ -18,19 +21,23 @@ pub(super) fn build_request(matches: &ArgMatches) -> AuthRoleRevokePermissionReq let key = matches.get_one::("key").expect("required"); let range_end = matches.get_one::("range_end"); - let mut request = AuthRoleRevokePermissionRequest::new(name, key.as_bytes()); + let key = key.as_bytes().to_vec(); + let mut option = None; if let Some(range_end) = range_end { - request = request.with_range_end(range_end.as_bytes()); + option = Some(RangeOption::RangeEnd(range_end.as_bytes().to_vec())); }; - request + (name.into(), key, option) } /// Execute the command pub(super) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result<()> { let req = build_request(matches); - let resp = client.auth_client().role_revoke_permission(req).await?; + let resp = client + .auth_client() + .role_revoke_permission(req.0, req.1, req.2) + .await?; resp.print(); Ok(()) @@ -48,11 +55,15 @@ mod tests { let test_cases = vec![ TestCase::new( vec!["revoke_perm", "Admin", "key1", "key2"], - Some(AuthRoleRevokePermissionRequest::new("Admin", "key1").with_range_end("key2")), + Some(( + "Admin".into(), + "key1".into(), + Some(RangeOption::RangeEnd("key2".into())), + )), ), TestCase::new( vec!["revoke_perm", "Admin", "key3"], - Some(AuthRoleRevokePermissionRequest::new("Admin", "key3")), + Some(("Admin".into(), "key3".into(), None)), ), ]; diff --git a/crates/xlinectl/src/command/txn.rs b/crates/xlinectl/src/command/txn.rs index d96798baa..664260832 100644 --- a/crates/xlinectl/src/command/txn.rs +++ b/crates/xlinectl/src/command/txn.rs @@ -140,17 +140,17 @@ fn parse_op_line(line: &str) -> Result { "put" => { let matches = put_cmd.try_get_matches_from(args.clone())?; let req = put::build_request(&matches); - Ok(TxnOp::put(req)) + Ok(TxnOp::put(req.0, req.1, Some(req.2))) } "get" => { let matches = get_cmd.try_get_matches_from(args.clone())?; let req = get::build_request(&matches); - Ok(TxnOp::range(req)) + Ok(TxnOp::range(req.0, Some(req.1))) } "delete" => { let matches = delete_cmd.try_get_matches_from(args.clone())?; let req = delete::build_request(&matches); - Ok(TxnOp::delete(req)) + Ok(TxnOp::delete(req.0, Some(req.1))) } _ => Err(anyhow!(format!("parse op failed in: `{line}`"))), } @@ -167,7 +167,7 @@ pub(crate) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result #[cfg(test)] mod tests { - use xline_client::types::kv::{PutRequest, RangeRequest}; + use xline_client::types::kv::RangeOptions; use super::*; @@ -187,15 +187,18 @@ mod tests { fn parse_op() { assert_eq!( parse_op_line(r#"put key1 "created-key1""#).unwrap(), - TxnOp::put(PutRequest::new("key1", "created-key1")) + TxnOp::put("key1", "created-key1", None) ); assert_eq!( parse_op_line(r"get key1 key11").unwrap(), - TxnOp::range(RangeRequest::new("key1").with_range_end("key11")) + TxnOp::range( + "key1", + Some(RangeOptions::default().with_range_end("key11")) + ) ); assert_eq!( parse_op_line(r"get key1 --from_key").unwrap(), - TxnOp::range(RangeRequest::new("key1").with_from_key()) + TxnOp::range("key1", Some(RangeOptions::default().with_from_key())) ); } } diff --git a/crates/xlinectl/src/command/user/add.rs b/crates/xlinectl/src/command/user/add.rs index 5c7071972..e133b9430 100644 --- a/crates/xlinectl/src/command/user/add.rs +++ b/crates/xlinectl/src/command/user/add.rs @@ -1,7 +1,14 @@ +use crate::utils::printer::Printer; use clap::{arg, ArgMatches, Command}; -use xline_client::{error::Result, types::auth::AuthUserAddRequest, Client}; +use xline_client::{error::Result, Client}; -use crate::utils::printer::Printer; +/// Parameters of `AuthClient::user_add`. +/// +/// The first parameter is the name of the user. +/// The second parameter is the password of the user. If the user has no password, set it to empty string. +/// The third parameter is whether the user could has no password. +/// If set, the user is allowed to have no password. +type AuthUserAddRequest = (String, String, bool); /// Definition of `add` command pub(super) fn command() -> Command { @@ -9,7 +16,7 @@ pub(super) fn command() -> Command { .about("Add a new user") .arg(arg!( "The name of the user")) .arg( - arg!([password] "Password of the user") + arg!([password] "Password of the user, set to empty string if the user has no password") .required_if_eq("no_password", "false") .required_unless_present("no_password"), ) @@ -18,20 +25,30 @@ pub(super) fn command() -> Command { /// Build request from matches pub(super) fn build_request(matches: &ArgMatches) -> AuthUserAddRequest { - let name = matches.get_one::("name").expect("required"); + let name = matches + .get_one::("name") + .expect("required") + .to_owned(); let no_password = matches.get_flag("no_password"); - if no_password { - AuthUserAddRequest::new(name) - } else { - let password = matches.get_one::("password").expect("required"); - AuthUserAddRequest::new(name).with_pwd(password) - } + + ( + name, + if no_password { + String::new() + } else { + matches + .get_one::("password") + .expect("required") + .to_owned() + }, + no_password, + ) } /// Execute the command pub(super) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result<()> { let req = build_request(matches); - let resp = client.auth_client().user_add(req).await?; + let resp = client.auth_client().user_add(req.0, req.1, req.2).await?; resp.print(); Ok(()) @@ -49,11 +66,11 @@ mod tests { let test_cases = vec![ TestCase::new( vec!["add", "JaneSmith", "password123"], - Some(AuthUserAddRequest::new("JaneSmith").with_pwd("password123")), + Some(("JaneSmith".into(), "password123".into(), false)), ), TestCase::new( vec!["add", "--no_password", "BobJohnson"], - Some(AuthUserAddRequest::new("BobJohnson")), + Some(("BobJohnson".into(), String::new(), true)), ), ]; diff --git a/crates/xlinectl/src/command/user/delete.rs b/crates/xlinectl/src/command/user/delete.rs index 1f170c833..f848702dc 100644 --- a/crates/xlinectl/src/command/user/delete.rs +++ b/crates/xlinectl/src/command/user/delete.rs @@ -1,5 +1,5 @@ use clap::{arg, ArgMatches, Command}; -use xline_client::{error::Result, types::auth::AuthUserDeleteRequest, Client}; +use xline_client::{error::Result, Client}; use crate::utils::printer::Printer; @@ -11,9 +11,9 @@ pub(super) fn command() -> Command { } /// Build request from matches -pub(super) fn build_request(matches: &ArgMatches) -> AuthUserDeleteRequest { +pub(super) fn build_request(matches: &ArgMatches) -> String { let name = matches.get_one::("name").expect("required"); - AuthUserDeleteRequest::new(name) + name.to_owned() } /// Execute the command @@ -30,13 +30,13 @@ mod tests { use super::*; use crate::test_case_struct; - test_case_struct!(AuthUserDeleteRequest); + test_case_struct!(String); #[test] fn command_parse_should_be_valid() { let test_cases = vec![TestCase::new( vec!["delete", "JohnDoe"], - Some(AuthUserDeleteRequest::new("JohnDoe")), + Some("JohnDoe".into()), )]; for case in test_cases { diff --git a/crates/xlinectl/src/command/user/get.rs b/crates/xlinectl/src/command/user/get.rs index d9247741b..c7f12f7d8 100644 --- a/crates/xlinectl/src/command/user/get.rs +++ b/crates/xlinectl/src/command/user/get.rs @@ -1,9 +1,5 @@ use clap::{arg, ArgMatches, Command}; -use xline_client::{ - error::Result, - types::auth::{AuthRoleGetRequest, AuthUserGetRequest}, - Client, -}; +use xline_client::{error::Result, Client}; use crate::utils::printer::Printer; @@ -16,9 +12,9 @@ pub(super) fn command() -> Command { } /// Build request from matches -pub(super) fn build_request(matches: &ArgMatches) -> AuthUserGetRequest { +pub(super) fn build_request(matches: &ArgMatches) -> String { let name = matches.get_one::("name").expect("required"); - AuthUserGetRequest::new(name.as_str()) + name.to_owned() } /// Execute the command @@ -32,10 +28,7 @@ pub(super) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result if detail { for role in resp.roles { println!("{role}"); - let resp_role_get = client - .auth_client() - .role_get(AuthRoleGetRequest::new(&role)) - .await?; + let resp_role_get = client.auth_client().role_get(role).await?; resp_role_get.print(); } } else { @@ -50,18 +43,15 @@ mod tests { use super::*; use crate::test_case_struct; - test_case_struct!(AuthUserGetRequest); + test_case_struct!(String); #[test] fn command_parse_should_be_valid() { let test_cases = vec![ - TestCase::new( - vec!["get", "JohnDoe"], - Some(AuthUserGetRequest::new("JohnDoe")), - ), + TestCase::new(vec!["get", "JohnDoe"], Some("JohnDoe".into())), TestCase::new( vec!["get", "--detail", "JaneSmith"], - Some(AuthUserGetRequest::new("JaneSmith")), + Some("JaneSmith".into()), ), ]; diff --git a/crates/xlinectl/src/command/user/grant_role.rs b/crates/xlinectl/src/command/user/grant_role.rs index 23b76408e..3646ec9fa 100644 --- a/crates/xlinectl/src/command/user/grant_role.rs +++ b/crates/xlinectl/src/command/user/grant_role.rs @@ -1,8 +1,11 @@ use clap::{arg, ArgMatches, Command}; -use xline_client::{error::Result, types::auth::AuthUserGrantRoleRequest, Client}; +use xline_client::{error::Result, Client}; use crate::utils::printer::Printer; +/// Temporary struct for testing, indicates `(user_name, role)` +type AuthUserGrantRoleRequest = (String, String); + /// Definition of `grant_role` command pub(super) fn command() -> Command { Command::new("grant_role") @@ -15,13 +18,13 @@ pub(super) fn command() -> Command { pub(super) fn build_request(matches: &ArgMatches) -> AuthUserGrantRoleRequest { let name = matches.get_one::("name").expect("required"); let role = matches.get_one::("role").expect("required"); - AuthUserGrantRoleRequest::new(name, role) + (name.into(), role.into()) } /// Execute the command pub(super) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result<()> { let req = build_request(matches); - let resp = client.auth_client().user_grant_role(req).await?; + let resp = client.auth_client().user_grant_role(req.0, req.1).await?; resp.print(); Ok(()) @@ -38,7 +41,7 @@ mod tests { fn command_parse_should_be_valid() { let test_cases = vec![TestCase::new( vec!["grant_role", "JohnDoe", "Admin"], - Some(AuthUserGrantRoleRequest::new("JohnDoe", "Admin")), + Some(("JohnDoe".into(), "Admin".into())), )]; for case in test_cases { diff --git a/crates/xlinectl/src/command/user/passwd.rs b/crates/xlinectl/src/command/user/passwd.rs index 4dbd45f77..976766d42 100644 --- a/crates/xlinectl/src/command/user/passwd.rs +++ b/crates/xlinectl/src/command/user/passwd.rs @@ -1,8 +1,11 @@ use clap::{arg, ArgMatches, Command}; -use xline_client::{error::Result, types::auth::AuthUserChangePasswordRequest, Client}; +use xline_client::{error::Result, Client}; use crate::utils::printer::Printer; +/// Temporary request for changing password. 0 is name, 1 is password +type AuthUserChangePasswordRequest = (String, String); + /// Definition of `passwd` command // TODO: interactive mode pub(super) fn command() -> Command { @@ -16,13 +19,16 @@ pub(super) fn command() -> Command { pub(super) fn build_request(matches: &ArgMatches) -> AuthUserChangePasswordRequest { let name = matches.get_one::("name").expect("required"); let password = matches.get_one::("password").expect("required"); - AuthUserChangePasswordRequest::new(name, password) + (name.to_owned(), password.to_owned()) } /// Execute the command pub(super) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result<()> { let req = build_request(matches); - let resp = client.auth_client().user_change_password(req).await?; + let resp = client + .auth_client() + .user_change_password(req.0, req.1) + .await?; resp.print(); Ok(()) @@ -39,10 +45,7 @@ mod tests { fn command_parse_should_be_valid() { let test_cases = vec![TestCase::new( vec!["passwd", "JohnDoe", "new_password"], - Some(AuthUserChangePasswordRequest::new( - "JohnDoe", - "new_password", - )), + Some(("JohnDoe".into(), "new_password".into())), )]; for case in test_cases { diff --git a/crates/xlinectl/src/command/user/revoke_role.rs b/crates/xlinectl/src/command/user/revoke_role.rs index 0b34c1dbb..f35f38a10 100644 --- a/crates/xlinectl/src/command/user/revoke_role.rs +++ b/crates/xlinectl/src/command/user/revoke_role.rs @@ -1,8 +1,11 @@ use clap::{arg, ArgMatches, Command}; -use xline_client::{error::Result, types::auth::AuthUserRevokeRoleRequest, Client}; +use xline_client::{error::Result, Client}; use crate::utils::printer::Printer; +/// Temporary struct for testing, indicates `(user_name, role)` +type AuthUserRevokeRoleRequest = (String, String); + /// Definition of `revoke_role` command pub(super) fn command() -> Command { Command::new("revoke_role") @@ -15,13 +18,13 @@ pub(super) fn command() -> Command { pub(super) fn build_request(matches: &ArgMatches) -> AuthUserRevokeRoleRequest { let name = matches.get_one::("name").expect("required"); let role = matches.get_one::("role").expect("required"); - AuthUserRevokeRoleRequest::new(name, role) + (name.to_owned(), role.to_owned()) } /// Execute the command pub(super) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result<()> { let req = build_request(matches); - let resp = client.auth_client().user_revoke_role(req).await?; + let resp = client.auth_client().user_revoke_role(req.0, req.1).await?; resp.print(); Ok(()) @@ -38,7 +41,7 @@ mod tests { fn command_parse_should_be_valid() { let test_cases = vec![TestCase::new( vec!["revoke_role", "JohnDoe", "Admin"], - Some(AuthUserRevokeRoleRequest::new("JohnDoe", "Admin")), + Some(("JohnDoe".to_owned(), "Admin".to_owned())), )]; for case in test_cases { diff --git a/crates/xlinectl/src/command/watch.rs b/crates/xlinectl/src/command/watch.rs index af2c86f8f..16e1a2f76 100644 --- a/crates/xlinectl/src/command/watch.rs +++ b/crates/xlinectl/src/command/watch.rs @@ -1,17 +1,21 @@ use std::io; +use std::{collections::HashMap, ffi::OsString}; use anyhow::{anyhow, Result}; use clap::{arg, value_parser, ArgMatches, Command}; +use std::process::Command as StdCommand; use xline_client::{ error::XlineClientError, - types::watch::{WatchRequest, Watcher}, + types::watch::{WatchOptions, Watcher}, Client, }; use xlineapi::command::Command as XlineCommand; +use xlineapi::WatchResponse; use crate::utils::printer::Printer; -/// Definition of `watch` command +/// Definition of `watch` command : +/// `WATCH [options] [key or prefix] [range_end] [--] [exec-command arg1 arg2 ...]` pub(crate) fn command() -> Command { Command::new("watch") .about("Watches events stream on keys or prefixes") @@ -25,10 +29,16 @@ pub(crate) fn command() -> Command { .arg(arg!(--pre_kv "Get the previous key-value pair before the event happens")) .arg(arg!(--progress_notify "Get periodic watch progress notification from server")) .arg(arg!(--interactive "Interactive mode")) + .arg( + arg!( "command to execute after -- ") + .num_args(1..) + .required(false) + .last(true), + ) } /// a function that builds a watch request with existing fields -type BuildRequestFn = dyn Fn(&str, Option<&str>) -> WatchRequest; +type BuildRequestFn = dyn Fn(Option<&str>) -> WatchOptions; /// Build request from matches pub(crate) fn build_request(matches: &ArgMatches) -> Box { @@ -37,8 +47,8 @@ pub(crate) fn build_request(matches: &ArgMatches) -> Box { let pre_kv = matches.get_flag("pre_kv"); let progress_notify = matches.get_flag("progress_notify"); - Box::new(move |key: &str, range_end: Option<&str>| -> WatchRequest { - let mut request = WatchRequest::new(key.as_bytes()); + Box::new(move |range_end: Option<&str>| -> WatchOptions { + let mut request = WatchOptions::default(); if prefix { request = request.with_prefix(); @@ -67,23 +77,116 @@ pub(crate) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result if interactive { exec_interactive(client, matches).await?; } else { - let key = matches.get_one::("key").expect("required"); - let range_end = matches.get_one::("range_end"); - let request = build_request(matches)(key, range_end.map(String::as_str)); - - let (_watcher, mut stream) = client.watch_client().watch(request).await?; - while let Some(resp) = stream - .message() - .await - .map_err(|e| XlineClientError::::WatchError(e.to_string()))? - { - resp.print(); + exec_non_interactive(client, matches).await?; + } + + Ok(()) +} + +/// Execute the command in non-interactive mode +async fn exec_non_interactive(client: &mut Client, matches: &ArgMatches) -> Result<()> { + let key = matches.get_one::("key").expect("required"); + let range_end = matches.get_one::("range_end"); + let watch_options = build_request(matches)(range_end.map(String::as_str)); + + // extract the command provided by user + let command_to_execute: Vec = matches + .get_many::("exec_command") + .unwrap_or_default() + .map(OsString::from) + .collect(); + + let (_watcher, mut stream) = client + .watch_client() + .watch(key.as_bytes(), Some(watch_options)) + .await?; + while let Some(resp) = stream + .message() + .await + .map_err(|e| XlineClientError::::WatchError(e.to_string()))? + { + resp.print(); + if !command_to_execute.is_empty() { + execute_command_on_events(&command_to_execute, &resp)?; } } Ok(()) } +/// Execute user command for each event. We wanna support things like: +/// +/// ```shell +/// ./xlinectl watch foo -- sh -c "env | grep ETCD_WATCH_" +/// ``` +/// +/// Expected output format: +/// ```plain +/// PUT +/// foo +/// bar +/// XLINE_WATCH_REVISION=11 +/// XLINE_WATCH_KEY="foo" +/// XLINE_WATCH_EVENT_TYPE="PUT" +/// XLINE_WATCH_VALUE="bar" +/// ``` +/// +fn execute_command_on_events(command_to_execute: &[OsString], resp: &WatchResponse) -> Result<()> { + let watch_revision = resp + .header + .as_ref() + .map(|header| header.revision) + .unwrap_or_default(); + + for event in &resp.events { + let kv = event.kv.as_ref().expect("expected key-value pair"); + let watch_key = String::from_utf8(kv.key.clone()).expect("expected valid UTF-8"); + let watch_value = String::from_utf8(kv.value.clone()).expect("expected valid UTF-8"); + let event_type = match event.r#type { + 0 => "PUT", + 1 => "DELETE", + _ => "UNKNOWN", + }; + + let envs = HashMap::from([ + ("XLINE_WATCH_REVISION", watch_revision.to_string()), + ("XLINE_WATCH_KEY", watch_key), + ("XLINE_WATCH_EVENT_TYPE", event_type.to_owned()), + ("XLINE_WATCH_VALUE", watch_value), + ]); + + execute_inner(command_to_execute, envs)?; + } + + Ok(()) +} + +/// Actual executor responsible for the user command, +/// command format: [exec-command arg1 arg2 ...] +#[allow(clippy::indexing_slicing)] // this is safe 'cause we always check non-empty. +#[allow(clippy::let_underscore_untyped)] // skip type annotation to make code clean, it's safe. +fn execute_inner(command: &[OsString], envs: HashMap<&str, String>) -> Result<()> { + let mut cmd = StdCommand::new(&command[0]); + + // adding environment variables + for (key, value) in envs { + let _ = cmd.env(key, value); + } + + // collecting the args + if command.len() > 1 { + let _ = cmd.args(&command[1..]); + } + // executing the command + let output = cmd.output()?; + if !output.status.success() { + eprintln!("Command failed with status: {}", output.status); + eprintln!("Error details: {}", String::from_utf8_lossy(&output.stderr)); + } + println!("{}", String::from_utf8_lossy(&output.stdout)); + Ok(()) +} + /// Execute the command in interactive mode async fn exec_interactive(client: &mut Client, matches: &ArgMatches) -> Result<()> { let req_builder = build_request(matches); @@ -117,8 +220,11 @@ async fn exec_interactive(client: &mut Client, matches: &ArgMatches) -> Result<( let Some(key) = args.next() else { failed!(line); }; - let request = req_builder(key, args.next()); - let (new_watcher, mut stream) = client.watch_client().watch(request).await?; + let watch_options = req_builder(args.next()); + let (new_watcher, mut stream) = client + .watch_client() + .watch(key.as_bytes(), Some(watch_options)) + .await?; watcher = Some(new_watcher); let _handle = tokio::spawn(async move { while let Some(resp) = stream.message().await? { @@ -159,12 +265,21 @@ mod tests { struct TestCase { arg: Vec<&'static str>, - req: Option, + key: String, + req: Option, } impl TestCase { - fn new(arg: Vec<&'static str>, req: Option) -> TestCase { - TestCase { arg, req } + fn new( + arg: Vec<&'static str>, + key: impl Into, + req: Option, + ) -> TestCase { + TestCase { + arg, + key: key.into(), + req, + } } fn run_test(&self) { @@ -182,8 +297,31 @@ mod tests { }; let key = matches.get_one::("key").expect("required"); let range_end = matches.get_one::("range_end"); - let req = build_request(&matches)(key, range_end.map(String::as_str)); + let req = build_request(&matches)(range_end.map(String::as_str)); + assert_eq!(key.to_owned(), self.key); assert_eq!(Some(req), self.req); + // Extract the command to execute from the matches + let command_to_execute: Vec = matches + .get_many::("exec_command") + .unwrap_or_default() + .map(OsString::from) + .collect(); + // Execute the user command upon receiving an event + if !command_to_execute.is_empty() { + // Mock environment variables to be passed to the command + let mock_envs = HashMap::from([ + ("XLINE_WATCH_REVISION", "11".to_owned()), + ("XLINE_WATCH_KEY", "mock_key".to_owned()), + ("XLINE_WATCH_EVENT_TYPE", "PUT".to_owned()), + ("XLINE_WATCH_VALUE", "mock_value".to_owned()), + ]); + // Here we ideally call a function to execute and capture the command output + // Since we are testing, we can check the command formation INSTEAD OF actual execution. + assert!( + execute_inner(&command_to_execute, mock_envs).is_ok(), + "Command execution failed" + ); + } } } @@ -192,12 +330,14 @@ mod tests { let test_cases = vec![ TestCase::new( vec!["watch", "key1", "key11"], - Some(WatchRequest::new("key1").with_range_end("key11")), + "key1", + Some(WatchOptions::default().with_range_end("key11")), ), TestCase::new( vec!["watch", "key1", "key11", "--rev", "100", "--pre_kv"], + "key1", Some( - WatchRequest::new("key1") + WatchOptions::default() .with_range_end("key11") .with_start_revision(100) .with_prev_kv(), @@ -205,11 +345,55 @@ mod tests { ), TestCase::new( vec!["watch", "key1", "--prefix", "--progress_notify"], - Some( - WatchRequest::new("key1") - .with_prefix() - .with_progress_notify(), - ), + "key1", + Some(WatchOptions::default().with_prefix().with_progress_notify()), + ), + // newly added test case: + // testing command `-- echo watch event received` + TestCase::new( + vec![ + "watch", + "key1", + "--prefix", + "--progress_notify", + "--", + "echo", + "watch event received", + ], + "key1", + Some(WatchOptions::default().with_prefix().with_progress_notify()), + ), + // newly added test case: + // testing command `-- sh -c ls` + TestCase::new( + vec![ + "watch", + "key1", + "--prefix", + "--progress_notify", + "--", + "sh", + "-c", + "ls", + ], + "key1", + Some(WatchOptions::default().with_prefix().with_progress_notify()), + ), + // newly added test case: + // testing command `-- sh -c "env | grep XLINE_WATCH_"` + TestCase::new( + vec![ + "watch", + "key1", + "--prefix", + "--progress_notify", + "--", + "sh", + "-c", + "env | grep XLINE_WATCH_", + ], + "key1", + Some(WatchOptions::default().with_prefix().with_progress_notify()), ), ]; diff --git a/crates/xlineutl/Cargo.toml b/crates/xlineutl/Cargo.toml index 891040207..5f11475d1 100644 --- a/crates/xlineutl/Cargo.toml +++ b/crates/xlineutl/Cargo.toml @@ -14,10 +14,10 @@ keywords = ["Client", "CommandLine"] [dependencies] anyhow = "1.0" clap = "4" -crc32fast = "1.4.0" +crc32fast = "1.4.2" engine = { path = "../engine" } serde = { version = "1.0.204", features = ["derive"] } -serde_json = "1.0.117" +serde_json = "1.0.125" tempfile = "3.10.1" tokio = "1" utils = { path = "../utils" } diff --git a/scripts/validation_test.sh b/scripts/validation_test.sh index 1fda4cf67..8868e5226 100755 --- a/scripts/validation_test.sh +++ b/scripts/validation_test.sh @@ -159,6 +159,32 @@ watch_validation() { log::info "watch validation test passed" } +# validate watch requests with commands +watch_with_command_validation() { + log::info "watch validation test running..." + + command="${ETCDCTL} watch watch_key -- echo watch event received" + log::info "running: ${command}" + want=("PUT" "watch_key" "value" "watch event received" "DELETE" "watch_key" "watch event received") + ${command} | while read line; do + log::debug ${line} + if [ "${line}" == "${want[0]}" ]; then + unset want[0] + want=("${want[@]}") + else + log::fatal "result not match pattern\n\tpattern: ${want[0]}\n\tresult: ${line}" + fi + done & + sleep 0.1 # wait watch + run "${ETCDCTL} put watch_key value" + check_positive "OK" + run "${ETCDCTL} del watch_key" + check_positive "1" + watch_progress_validation + + log::info "watch validation test passed" +} + # validate lease requests lease_validation() { log::info "lease validation test running..." @@ -272,6 +298,9 @@ lock_validation() { run "${ETCDCTL} lock mutex echo success" check_positive "success" + run "${ETCDCTL} lock mutex -- etcdctl --endpoints=http://172.20.0.3:2379,http://172.20.0.4:2379 put foo bar" + check_positive "OK" + log::info "lock validation test passed" } @@ -324,6 +353,7 @@ cluster_validation() { compact_validation kv_validation watch_validation +watch_with_command_validation lease_validation auth_validation lock_validation diff --git a/workspace-hack/Cargo.toml b/workspace-hack/Cargo.toml index 6b4d31d24..92723c697 100644 --- a/workspace-hack/Cargo.toml +++ b/workspace-hack/Cargo.toml @@ -13,7 +13,8 @@ publish = false ### BEGIN HAKARI SECTION [dependencies] -axum = { version = "0.6" } +axum = { version = "0.7" } +axum-core = { version = "0.4", default-features = false, features = ["tracing"] } bytes = { version = "1" } clap = { version = "4", features = ["derive"] } crypto-common = { version = "0.1", default-features = false, features = ["std"] } @@ -22,23 +23,25 @@ either = { version = "1", default-features = false, features = ["use_std"] } futures-channel = { version = "0.3", features = ["sink"] } futures-util = { version = "0.3", features = ["channel", "io", "sink"] } getrandom = { version = "0.2", default-features = false, features = ["js", "rdrand", "std"] } +itertools = { version = "0.13" } libc = { version = "0.2", features = ["extra_traits"] } log = { version = "0.4", default-features = false, features = ["std"] } -madsim-tokio = { git = "https://github.com/Phoenix500526/madsim.git", branch = "update-tonic", default-features = false, features = ["fs", "io-util", "macros", "net", "rt", "rt-multi-thread", "signal", "sync", "time"] } -madsim-tonic = { git = "https://github.com/Phoenix500526/madsim.git", branch = "update-tonic", default-features = false, features = ["tls"] } +madsim-tokio = { git = "https://github.com/LucienY01/madsim.git", branch = "bz/tonic-0-12", default-features = false, features = ["fs", "io-util", "macros", "net", "rt", "rt-multi-thread", "signal", "sync", "time"] } +madsim-tonic = { git = "https://github.com/LucienY01/madsim.git", branch = "bz/tonic-0-12", default-features = false, features = ["tls"] } memchr = { version = "2" } -num-traits = { version = "0.2", default-features = false, features = ["i128", "std"] } -opentelemetry_sdk = { version = "0.22", features = ["metrics", "rt-tokio"] } -petgraph = { version = "0.6" } +opentelemetry_sdk = { version = "0.24", features = ["rt-tokio"] } predicates = { version = "3", default-features = false, features = ["diff"] } +rand = { version = "0.8", features = ["small_rng"] } serde = { version = "1", features = ["derive", "rc"] } serde_json = { version = "1", features = ["raw_value"] } sha2 = { version = "0.10" } +smallvec = { version = "1", default-features = false, features = ["const_new"] } time = { version = "0.3", features = ["formatting", "macros", "parsing"] } tokio = { version = "1", features = ["fs", "io-std", "io-util", "macros", "net", "rt-multi-thread", "signal", "sync", "time"] } +tokio-stream = { version = "0.1", features = ["net"] } tokio-util = { version = "0.7", features = ["codec", "io"] } -tonic = { version = "0.11", features = ["tls"] } -tower = { version = "0.4", features = ["balance", "buffer", "filter", "limit", "timeout", "util"] } +tonic = { version = "0.12", features = ["tls"] } +tower = { version = "0.4", features = ["balance", "buffer", "filter", "limit", "util"] } tracing = { version = "0.1", features = ["log"] } tracing-log = { version = "0.2", default-features = false, features = ["log-tracer", "std"] } tracing-subscriber = { version = "0.3", features = ["env-filter", "time"] } @@ -48,11 +51,10 @@ zeroize = { version = "1", features = ["derive"] } bytes = { version = "1" } cc = { version = "1", default-features = false, features = ["parallel"] } either = { version = "1", default-features = false, features = ["use_std"] } -itertools = { version = "0.12", default-features = false, features = ["use_alloc"] } +itertools = { version = "0.13" } libc = { version = "0.2", features = ["extra_traits"] } log = { version = "0.4", default-features = false, features = ["std"] } memchr = { version = "2" } -petgraph = { version = "0.6" } predicates = { version = "3", default-features = false, features = ["diff"] } syn-dff4ba8e3ae991db = { package = "syn", version = "1", features = ["extra-traits", "full"] } syn-f595c2ba2a3f28df = { package = "syn", version = "2", features = ["extra-traits", "full", "visit", "visit-mut"] }