├── .config ├── hakari.toml └── nextest.toml ├── .dockerignore ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yaml │ └── feature_request.yaml ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── benchmark.yml │ ├── build_env.yml │ ├── build_xline.yml │ ├── docker_image.yml │ ├── gen_release_note.yml │ ├── issue-cmds.yml │ ├── issue-welcome.yml │ ├── merge_image.yml │ ├── pull_request.yml │ ├── release.yml │ ├── stale.yml │ └── validation.yml ├── .gitignore ├── .gitmodules ├── .mergify.yml ├── .pre-commit-config.yaml ├── .typos.toml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── Cross.toml ├── LICENSE ├── MAINTAINERS ├── README.md ├── VALIDATION_REPORT.md ├── ci ├── Dockerfile ├── artifacts │ ├── kind-node-xline.Dockerfile │ ├── kind.yaml │ └── xline-pod.yaml ├── build-env.sh ├── cross │ ├── Dockerfile.aarch64-unknown-linux-gnu │ └── Dockerfile.x86_64-unknown-linux-gnu ├── rust-toolchain.toml └── scripts │ ├── check-trailing-spaces.sh │ ├── install_macos_deps.sh │ └── kind.sh ├── cliff.toml ├── clippy.toml ├── crates ├── benchmark │ ├── Cargo.toml │ └── src │ │ ├── args.rs │ │ ├── bench_client.rs │ │ ├── lib.rs │ │ ├── main.rs │ │ └── runner.rs ├── curp-external-api │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── cmd.rs │ │ ├── conflict.rs │ │ ├── lib.rs │ │ └── role_change.rs ├── curp-test-utils │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── lib.rs │ │ └── test_cmd.rs ├── curp │ ├── Cargo.toml │ ├── README.md │ ├── build.rs │ ├── proto │ │ └── inner_message.proto │ ├── src │ │ ├── client │ │ │ ├── metrics.rs │ │ │ ├── mod.rs │ │ │ ├── retry.rs │ │ │ ├── state.rs │ │ │ ├── stream.rs │ │ │ ├── tests.rs │ │ │ └── unary │ │ │ │ ├── mod.rs │ │ │ │ └── propose_impl.rs │ │ ├── error.rs │ │ ├── lib.rs │ │ ├── log_entry.rs │ │ ├── members.rs │ │ ├── response.rs │ │ ├── rpc │ │ │ ├── connect.rs │ │ │ ├── metrics.rs │ │ │ ├── mod.rs │ │ │ └── reconnect.rs │ │ ├── server │ │ │ ├── cmd_board.rs │ │ │ ├── cmd_worker │ │ │ │ └── mod.rs │ │ │ ├── conflict │ │ │ │ ├── mod.rs │ │ │ │ ├── spec_pool_new.rs │ │ │ │ ├── test_pools.rs │ │ │ │ ├── tests.rs │ │ │ │ └── uncommitted_pool.rs │ │ │ ├── curp_node.rs │ │ │ ├── gc.rs │ │ │ ├── lease_manager.rs │ │ │ ├── metrics.rs │ │ │ ├── mod.rs │ │ │ ├── raw_curp │ │ │ │ ├── log.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── state.rs │ │ │ │ └── tests.rs │ │ │ └── storage │ │ │ │ ├── db.rs │ │ │ │ ├── mod.rs │ │ │ │ └── wal │ │ │ │ ├── codec.rs │ │ │ │ ├── config.rs │ │ │ │ ├── error.rs │ │ │ │ ├── framed.rs │ │ │ │ ├── mock │ │ │ │ └── mod.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── pipeline.rs │ │ │ │ ├── remover.rs │ │ │ │ ├── segment.rs │ │ │ │ ├── storage.rs │ │ │ │ ├── test_util.rs │ │ │ │ ├── tests.rs │ │ │ │ └── util.rs │ │ ├── snapshot.rs │ │ └── tracker.rs │ ├── tests │ │ └── it │ │ │ ├── common │ │ │ ├── curp_group.rs │ │ │ └── mod.rs │ │ │ ├── main.rs │ │ │ └── server.rs │ └── tla+ │ │ ├── README.md │ │ ├── curp.pdf │ │ └── curp.tla ├── engine │ ├── Cargo.toml │ └── src │ │ ├── api │ │ ├── engine_api.rs │ │ ├── mod.rs │ │ ├── operation.rs │ │ ├── snapshot_api.rs │ │ └── transaction_api.rs │ │ ├── error.rs │ │ ├── lib.rs │ │ ├── memory_engine │ │ ├── mod.rs │ │ └── transaction.rs │ │ ├── metrics.rs │ │ ├── mock_rocksdb_engine.rs │ │ ├── proxy.rs │ │ ├── rocksdb_engine │ │ ├── mod.rs │ │ └── transaction.rs │ │ └── snapshot_allocator.rs ├── simulation │ ├── Cargo.toml │ ├── README.md │ ├── src │ │ ├── curp_group.rs │ │ ├── lib.rs │ │ └── xline_group.rs │ └── tests │ │ └── it │ │ ├── curp │ │ ├── mod.rs │ │ ├── server_election.rs │ │ └── server_recovery.rs │ │ ├── main.rs │ │ └── xline.rs ├── test-macros │ ├── Cargo.toml │ ├── README.md │ ├── src │ │ ├── bin │ │ │ ├── no_abort.rs │ │ │ └── with_abort.rs │ │ └── lib.rs │ └── tests │ │ └── test_abort.rs ├── utils │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── barrier.rs │ │ ├── config.rs │ │ ├── lca_tree.rs │ │ ├── lib.rs │ │ ├── metrics.rs │ │ ├── parking_lot_lock.rs │ │ ├── parser.rs │ │ ├── std_lock.rs │ │ ├── table_names.rs │ │ ├── task_manager │ │ ├── mod.rs │ │ └── tasks.rs │ │ ├── tokio_lock.rs │ │ └── tracing.rs ├── xline-client │ ├── Cargo.toml │ ├── README.md │ ├── examples │ │ ├── auth.rs │ │ ├── auth_role.rs │ │ ├── auth_user.rs │ │ ├── cluster.rs │ │ ├── error_handling.rs │ │ ├── kv.rs │ │ ├── lease.rs │ │ ├── lock.rs │ │ ├── maintenance.rs │ │ └── watch.rs │ ├── src │ │ ├── clients │ │ │ ├── auth.rs │ │ │ ├── cluster.rs │ │ │ ├── election.rs │ │ │ ├── kv.rs │ │ │ ├── lease.rs │ │ │ ├── lock.rs │ │ │ ├── maintenance.rs │ │ │ ├── mod.rs │ │ │ └── watch.rs │ │ ├── error.rs │ │ ├── lease_gen.rs │ │ ├── lib.rs │ │ └── types │ │ │ ├── auth.rs │ │ │ ├── kv.rs │ │ │ ├── lease.rs │ │ │ ├── mod.rs │ │ │ ├── range_end.rs │ │ │ └── watch.rs │ └── tests │ │ └── it │ │ ├── auth.rs │ │ ├── common.rs │ │ ├── kv.rs │ │ ├── lease.rs │ │ ├── lock.rs │ │ ├── main.rs │ │ ├── maintenance.rs │ │ └── watch.rs ├── xline-test-utils │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── bin │ │ └── test_cluster.rs │ │ └── lib.rs ├── xline │ ├── Cargo.toml │ ├── src │ │ ├── conflict │ │ │ ├── mod.rs │ │ │ ├── spec_pool.rs │ │ │ ├── tests.rs │ │ │ └── uncommitted_pool.rs │ │ ├── header_gen.rs │ │ ├── id_gen.rs │ │ ├── lib.rs │ │ ├── main.rs │ │ ├── metrics.rs │ │ ├── restore.rs │ │ ├── revision_check.rs │ │ ├── revision_number.rs │ │ ├── server │ │ │ ├── auth_server.rs │ │ │ ├── auth_wrapper.rs │ │ │ ├── cluster_server.rs │ │ │ ├── command.rs │ │ │ ├── kv_server.rs │ │ │ ├── lease_server.rs │ │ │ ├── lock_server.rs │ │ │ ├── maintenance.rs │ │ │ ├── mod.rs │ │ │ ├── watch_server.rs │ │ │ └── xline_server.rs │ │ ├── state.rs │ │ ├── storage │ │ │ ├── alarm_store.rs │ │ │ ├── auth_store │ │ │ │ ├── backend.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── perms.rs │ │ │ │ └── store.rs │ │ │ ├── compact │ │ │ │ ├── mod.rs │ │ │ │ ├── periodic_compactor.rs │ │ │ │ └── revision_compactor.rs │ │ │ ├── db.rs │ │ │ ├── index.rs │ │ │ ├── kv_store.rs │ │ │ ├── kvwatcher.rs │ │ │ ├── lease_store │ │ │ │ ├── lease.rs │ │ │ │ ├── lease_collection.rs │ │ │ │ ├── lease_queue.rs │ │ │ │ └── mod.rs │ │ │ ├── mod.rs │ │ │ ├── revision.rs │ │ │ └── storage_api.rs │ │ └── utils │ │ │ ├── args.rs │ │ │ ├── metrics.rs │ │ │ ├── mod.rs │ │ │ └── trace.rs │ └── tests │ │ └── it │ │ ├── auth_test.rs │ │ ├── cluster_test.rs │ │ ├── kv_test.rs │ │ ├── lease_test.rs │ │ ├── lock_test.rs │ │ ├── main.rs │ │ ├── maintenance_test.rs │ │ ├── tls_test.rs │ │ └── watch_test.rs ├── xlineapi │ ├── Cargo.toml │ ├── README.md │ ├── build.rs │ └── src │ │ ├── classifier.rs │ │ ├── command.rs │ │ ├── execute_error.rs │ │ ├── interval.rs │ │ ├── lib.rs │ │ └── request_validation.rs ├── xlinectl │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── command │ │ ├── auth │ │ │ ├── disable.rs │ │ │ ├── enable.rs │ │ │ ├── mod.rs │ │ │ └── status.rs │ │ ├── compaction.rs │ │ ├── delete.rs │ │ ├── get.rs │ │ ├── lease │ │ │ ├── grant.rs │ │ │ ├── keep_alive.rs │ │ │ ├── list.rs │ │ │ ├── mod.rs │ │ │ ├── revoke.rs │ │ │ └── timetolive.rs │ │ ├── lock.rs │ │ ├── member │ │ │ ├── add.rs │ │ │ ├── list.rs │ │ │ ├── mod.rs │ │ │ ├── promote.rs │ │ │ ├── remove.rs │ │ │ └── update.rs │ │ ├── mod.rs │ │ ├── put.rs │ │ ├── role │ │ │ ├── add.rs │ │ │ ├── delete.rs │ │ │ ├── get.rs │ │ │ ├── grant_perm.rs │ │ │ ├── list.rs │ │ │ ├── mod.rs │ │ │ └── revoke_perm.rs │ │ ├── snapshot.rs │ │ ├── txn.rs │ │ ├── user │ │ │ ├── add.rs │ │ │ ├── delete.rs │ │ │ ├── get.rs │ │ │ ├── grant_role.rs │ │ │ ├── list.rs │ │ │ ├── mod.rs │ │ │ ├── passwd.rs │ │ │ └── revoke_role.rs │ │ └── watch.rs │ │ ├── main.rs │ │ └── utils │ │ ├── macros.rs │ │ ├── mod.rs │ │ ├── parser.rs │ │ └── printer.rs └── xlineutl │ ├── Cargo.toml │ ├── README.md │ └── src │ ├── command │ ├── mod.rs │ └── snapshot.rs │ ├── main.rs │ └── printer.rs ├── doc ├── QUICK_START.md ├── img │ ├── cncf-logo.png │ ├── prom_demo.png │ ├── xline-horizontal-black.png │ ├── xline-key-perf.png │ └── xline_test_deployment.jpg └── metrics.md ├── fixtures ├── ca.crt ├── private.pem ├── public.pem ├── root_client.crt ├── root_client.key ├── server.crt ├── server.key ├── u1_client.crt ├── u1_client.key ├── u2_client.crt └── u2_client.key ├── rust-toolchain.toml ├── scripts ├── Dockerfile ├── benchmark.sh ├── certgen.sh ├── common.sh ├── log.sh ├── prometheus.yml ├── quick_start.sh └── validation_test.sh ├── workspace-hack ├── .gitattributes ├── Cargo.toml ├── build.rs └── src │ └── lib.rs └── xline_server.conf /.config/hakari.toml: -------------------------------------------------------------------------------- 1 | # This file contains settings for `cargo hakari`. 2 | # See https://docs.rs/cargo-hakari/latest/cargo_hakari/config for a full list of options. 3 | 4 | hakari-package = "workspace-hack" 5 | 6 | # Format version for hakari's output. Version 4 requires cargo-hakari 0.9.22 or above. 7 | dep-format-version = "4" 8 | 9 | # Setting workspace.resolver = "2" in the root Cargo.toml is HIGHLY recommended. 10 | # Hakari works much better with the new feature resolver. 11 | # For more about the new feature resolver, see: 12 | # https://blog.rust-lang.org/2021/03/25/Rust-1.51.0.html#cargos-new-feature-resolver 13 | resolver = "2" 14 | 15 | # Add triples corresponding to platforms commonly used by developers here. 16 | # https://doc.rust-lang.org/rustc/platform-support.html 17 | platforms = [ 18 | # "x86_64-unknown-linux-gnu", 19 | # "x86_64-apple-darwin", 20 | # "x86_64-pc-windows-msvc", 21 | ] 22 | 23 | # Write out exact versions rather than a semver range. (Defaults to false.) 24 | # exact-versions = true 25 | 26 | unify-target-host = "unify-if-both" 27 | 28 | [traversal-excludes] 29 | third-party = [ 30 | # Why regex and prost might be compile multiply times? 31 | { name = "regex" }, 32 | { name = "prost" }, 33 | ] 34 | -------------------------------------------------------------------------------- /.config/nextest.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | retries = 0 3 | slow-timeout = { period = "10s", terminate-after = 3 } 4 | status-level = "all" 5 | final-status-level = "slow" 6 | fail-fast = true 7 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | debug/ 2 | target/ 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yaml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | title: "[Bug]: " 4 | labels: ["bug"] 5 | 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Thanks for taking the time to fill out this bug report! 11 | - type: textarea 12 | id: Description 13 | attributes: 14 | label: Description about the bug 15 | description: Briefly answer these questions 16 | placeholder: | 17 | 1. One line summary of the issue 18 | 2. What did you expect and what actually happened? 19 | 3. What steps can we take to reproduce the behaviour you saw? 20 | validations: 21 | required: true 22 | - type: dropdown 23 | id: version 24 | attributes: 25 | label: Version 26 | description: What version of xline are you running? 27 | options: 28 | - 0.1.0 29 | - 0.2.0 30 | - 0.3.0 31 | - 0.4.0 32 | - 0.4.1 33 | - 0.5.0 34 | - 0.6.0 35 | - 0.6.1 36 | - 0.7.0 (Default) 37 | validations: 38 | required: true 39 | - type: textarea 40 | id: logs 41 | attributes: 42 | label: Relevant log output 43 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 44 | render: shell 45 | - type: checkboxes 46 | id: terms 47 | attributes: 48 | label: Code of Conduct 49 | description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/xline-kv/Xline/blob/master/CODE_OF_CONDUCT.md) 50 | options: 51 | - label: I agree to follow this project's Code of Conduct 52 | required: true 53 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yaml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Request a new feature 3 | title: "[Feature]: " 4 | labels: ["enhancement"] 5 | 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Thanks for taking the time to request this feature! 11 | - type: textarea 12 | id: Description 13 | attributes: 14 | label: Description about the feature 15 | description: Briefly answer these questions 16 | placeholder: | 17 | 1. Is your feature request related to a problem? 18 | 2. Describe the solution you'd like 19 | 3. Describe any alternative solutions or features you've considered 20 | validations: 21 | required: true 22 | - type: checkboxes 23 | id: terms 24 | attributes: 25 | label: Code of Conduct 26 | description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/xline-kv/Xline/blob/master/CODE_OF_CONDUCT.md) 27 | options: 28 | - label: I agree to follow this project's Code of Conduct 29 | required: true 30 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Please briefly answer these questions: 2 | 3 | * what problem are you trying to solve? (or if there's no problem, what's the motivation for this change?) 4 | 5 | * what changes does this pull request make? 6 | 7 | * are there any non-obvious implications of these changes? (does it break compatibility with previous versions, etc) 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | # Check for updates to GitHub Actions every day 7 | interval: "daily" 8 | # Allow up to 10 open pull requests for update github-actions 9 | # 5 by default 10 | # see https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#open-pull-requests-limit 11 | open-pull-requests-limit: 10 12 | ignore: 13 | - dependency-name: 'actions/upload-artifact' 14 | #ignore all updates greater than or equal to version 4 15 | versions: '>= 4' 16 | - dependency-name: 'actions/download-artifact' 17 | versions: '>= 4' 18 | - package-ecosystem: "cargo" 19 | directory: "/" 20 | schedule: 21 | interval: "weekly" 22 | labels: 23 | - "dependencies" 24 | open-pull-requests-limit: 3 25 | -------------------------------------------------------------------------------- /.github/workflows/benchmark.yml: -------------------------------------------------------------------------------- 1 | name: Benchmark 2 | 3 | on: 4 | workflow_dispatch: {} 5 | schedule: 6 | - cron: "00 00 * * 1" 7 | 8 | env: 9 | CI_RUST_TOOLCHAIN: 1.74.0 10 | 11 | jobs: 12 | benchmark: 13 | name: Build and Upload Artifacts 14 | uses: ./.github/workflows/build_xline.yml 15 | with: 16 | docker_xline_image: "ghcr.io/xline-kv/build-env:latest" 17 | binaries: "xline,benchmark" 18 | additional_setup_commands: | 19 | docker build . -t ghcr.io/xline-kv/xline:latest 20 | docker pull datenlord/etcd:v3.5.5 21 | script_name: "benchmark.sh" 22 | uploadLogs: false 23 | uploadBenchmark: true 24 | -------------------------------------------------------------------------------- /.github/workflows/build_env.yml: -------------------------------------------------------------------------------- 1 | name: Build CI Env Image 2 | 3 | on: 4 | push: 5 | paths: 6 | - "ci/build-env.sh" 7 | - "ci/Dockerfile" 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build_env: 12 | name: Build 13 | permissions: 14 | packages: write 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Run build scripts 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | run: ci/build-env.sh 22 | -------------------------------------------------------------------------------- /.github/workflows/gen_release_note.yml: -------------------------------------------------------------------------------- 1 | name: Milestone Closure 2 | 3 | # Trigger the workflow on milestone events 4 | on: 5 | milestone: 6 | types: [closed] 7 | 8 | jobs: 9 | create-release-notes: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@master 13 | - name: Create Release Notes 14 | uses: docker://decathlon/release-notes-generator-action:2.0.1 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | OUTPUT_FOLDER: temp_release_notes 18 | USE_MILESTONE_TITLE: "true" 19 | -------------------------------------------------------------------------------- /.github/workflows/issue-cmds.yml: -------------------------------------------------------------------------------- 1 | on: 2 | issue_comment: 3 | types: 4 | - created 5 | 6 | jobs: 7 | assignme: 8 | name: /assignme 9 | runs-on: ubuntu-latest 10 | if: startsWith(github.event.comment.body, '/assignme') 11 | 12 | steps: 13 | - uses: xt0rted/slash-command-action@v2 14 | with: 15 | repo-token: ${{ secrets.GITHUB_TOKEN }} 16 | command: assignme 17 | reaction: "true" 18 | reaction-type: "rocket" 19 | permission-level: read 20 | 21 | - uses: actions-ecosystem/action-add-assignees@v1 22 | with: 23 | github_token: ${{ secrets.github_token }} 24 | assignees: ${{ github.actor }} 25 | 26 | contributing-agreement: 27 | name: /contributing-agreement 28 | runs-on: ubuntu-latest 29 | if: startsWith(github.event.comment.body, '/contributing-agreement') 30 | 31 | steps: 32 | - uses: xt0rted/slash-command-action@v2 33 | with: 34 | repo-token: ${{ secrets.GITHUB_TOKEN }} 35 | command: contributing-agreement 36 | reaction: "true" 37 | reaction-type: "rocket" 38 | permission-level: read 39 | 40 | - uses: peter-evans/create-or-update-comment@v4 41 | with: 42 | issue-number: ${{ github.event.issue.number }} 43 | body: | 44 | Contributing Agreements: 45 | 46 | - [pull request](https://github.com/xline-kv/Xline/blob/master/CONTRIBUTING.md#pull-requests) 47 | - [merge policy](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges#rebase-and-merge-your-commits) 48 | -------------------------------------------------------------------------------- /.github/workflows/issue-welcome.yml: -------------------------------------------------------------------------------- 1 | on: 2 | issues: 3 | types: 4 | - opened 5 | 6 | # https://github.com/marketplace/actions/create-or-update-comment 7 | 8 | jobs: 9 | welcome: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: peter-evans/create-or-update-comment@v4 13 | with: 14 | issue-number: ${{ github.event.issue.number }} 15 | body: | 16 | 👋 Thanks for opening this issue! 17 | 18 | Reply with the following command on its own line to get help or engage: 19 | 20 | - `/contributing-agreement` : to print Contributing Agreements. 21 | - `/assignme` : to assign this issue to you. 22 | -------------------------------------------------------------------------------- /.github/workflows/merge_image.yml: -------------------------------------------------------------------------------- 1 | name: Merge Multi-platform Image Manifests 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | app_version: 7 | required: true 8 | type: string 9 | 10 | env: 11 | IMAGE_ID: ghcr.io/xline-kv/xline 12 | 13 | jobs: 14 | merge_image: 15 | runs-on: ubuntu-latest 16 | name: Merge Docker Image 17 | steps: 18 | - name: Download Digests 19 | uses: actions/download-artifact@v3 20 | with: 21 | name: digests 22 | path: /tmp/digests 23 | 24 | - name: Set up Docker Buildx 25 | uses: docker/setup-buildx-action@v3 26 | 27 | - name: Login to GHCR 28 | uses: docker/login-action@v3 29 | with: 30 | registry: ghcr.io 31 | username: xline-kv 32 | password: ${{ secrets.GITHUB_TOKEN }} 33 | 34 | - name: Push Manifest 35 | working-directory: /tmp/digests 36 | run: | 37 | docker buildx imagetools create \ 38 | -t ${{ env.IMAGE_ID }}:latest \ 39 | -t ${{ env.IMAGE_ID }}:${{ inputs.app_version }} \ 40 | $(printf '${{ env.IMAGE_ID }}@sha256:%s ' *) 41 | 42 | - name: Inspect Manifest 43 | run: | 44 | docker buildx imagetools inspect ${{ env.IMAGE_ID }}:${{ inputs.app_version }} 45 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale pull requests 2 | on: 3 | schedule: 4 | - cron: "0 0 * * *" 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/stale@v9 12 | with: 13 | repo-token: ${{ secrets.GITHUB_TOKEN }} 14 | start-date: '2024-04-19T00:00:00Z' # ISO 8601 or RFC 2822 15 | stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 14 days.' 16 | stale-pr-message: 'This PR is stale because it has been open for 30 days with no activity. Remove stale label or comment or this will be closed in 14 days' 17 | close-issue-message: 'This issue was closed because it has been stalled for 14 days with no activity.' 18 | close-pr-message: 'This PR was closed because it has been stalled for 14 days with no activity.' 19 | stale-pr-label: 'stale' 20 | exempt-issue-labels: 'good-first-issue' 21 | exempt-all-milestones: true 22 | days-before-stale: 30 23 | days-before-close: 14 24 | 25 | unassign: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: Auto unassign stale assignments 29 | uses: JasonEtco/slash-assign-action@v0.0.3 30 | with: 31 | required_label: 'good-first-issue' 32 | -------------------------------------------------------------------------------- /.github/workflows/validation.yml: -------------------------------------------------------------------------------- 1 | name: Validation 2 | 3 | on: 4 | workflow_dispatch: {} 5 | pull_request: 6 | types: [opened, synchronize, reopened] 7 | schedule: 8 | - cron: "00 00 * * 1" 9 | 10 | jobs: 11 | validation: 12 | name: Build and Upload Artifacts 13 | uses: ./.github/workflows/build_xline.yml 14 | with: 15 | docker_xline_image: "ghcr.io/xline-kv/build-env:latest" 16 | additional_setup_commands: | 17 | sudo apt-get install -y --force-yes expect 18 | ldd ./xline 19 | ldd ./benchmark 20 | cp ../fixtures/{private,public}.pem . 21 | docker build . -t ghcr.io/xline-kv/xline:latest 22 | docker pull gcr.io/etcd-development/etcd:v3.5.5 23 | binaries: "xline,benchmark" 24 | script_name: "validation_test.sh" 25 | uploadLogs: true 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | 7 | # These are backup files generated by rustfmt 8 | **/*.rs.bk 9 | 10 | # MSVC Windows builds of rustc generate these, which store debugging information 11 | *.pdb 12 | 13 | .DS_Store 14 | 15 | scripts/* 16 | !scripts/Dockerfile 17 | !scripts/benchmark.sh 18 | !scripts/quick_start.sh 19 | !scripts/coverage.sh 20 | !scripts/validation_test.sh 21 | !scripts/certgen.sh 22 | !scripts/log.sh 23 | !scripts/prometheus.yml 24 | 25 | coverage 26 | *.profraw 27 | 28 | .vscode 29 | .idea 30 | out/ 31 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "xlineapi/proto"] 2 | path = crates/xlineapi/proto 3 | url = https://github.com/xline-kv/xline-proto.git 4 | [submodule "curp/proto/common"] 5 | path = crates/curp/proto/common 6 | url = https://github.com/xline-kv/curp-proto.git 7 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | fail_fast: false 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v4.4.0 5 | hooks: 6 | - id: no-commit-to-branch 7 | - id: check-byte-order-marker 8 | - id: check-case-conflict 9 | - id: check-merge-conflict 10 | - id: check-symlinks 11 | - id: check-yaml 12 | - id: check-toml 13 | - id: end-of-file-fixer 14 | - id: mixed-line-ending 15 | - id: trailing-whitespace 16 | - repo: https://github.com/psf/black 17 | rev: 23.3.0 18 | hooks: 19 | - id: black 20 | - repo: https://github.com/crate-ci/typos 21 | rev: v1.20.9 22 | hooks: 23 | - id: typos 24 | - repo: local 25 | hooks: 26 | - id: cargo-fmt 27 | name: cargo fmt 28 | description: Format files with rustfmt. 29 | entry: bash -c 'cargo fmt -- --check' 30 | language: rust 31 | files: ^(?!abi)\.rs$ 32 | args: [] 33 | - id: conventional-commit-msg-validation 34 | name: commit message conventional validation 35 | language: pygrep 36 | entry: '^(breaking|build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test|squash|fixup){1}(\([\w\-\.]+\))?(!)?: ([\w `])+([\s\S]*)' 37 | args: [--multiline, --negate] 38 | stages: [commit-msg] 39 | - id: commit-msg-needs-to-be-signed-off 40 | name: commit message needs to be signed off 41 | language: pygrep 42 | entry: "^Signed-off-by:" 43 | args: [--multiline, --negate] 44 | stages: [commit-msg] 45 | - id: cargo-sort 46 | name: Check Cargo.toml is sorted 47 | description: Ensure Cargo.toml is sorted 48 | entry: bash -c 'cargo sort --workspace' 49 | language: rust 50 | files: Cargo\.toml 51 | pass_filenames: false 52 | - id: cargo-hakari 53 | name: Check if workspace-hack works correctly 54 | description: Ensure workspace-hack works correctly 55 | entry: bash -c 'cargo hakari generate --diff && cargo hakari manage-deps --dry-run && cargo hakari verify' 56 | language: rust 57 | files: Cargo\.(toml|lock) 58 | pass_filenames: false 59 | - id: cargo machete 60 | name: Check if all dependencies are used 61 | description: Ensure no unused dependencies in /crates increase compiling time 62 | entry: bash -c 'cargo machete --fix crates/' 63 | language: rust 64 | files: Cargo\.toml 65 | pass_filenames: false 66 | -------------------------------------------------------------------------------- /.typos.toml: -------------------------------------------------------------------------------- 1 | [files] 2 | # ignore the generated workspace-hack/Cargo.toml file 3 | extend-exclude = ["workspace-hack/Cargo.toml"] 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "crates/benchmark", 4 | "crates/curp", 5 | "crates/curp-external-api", 6 | "crates/curp-test-utils", 7 | "crates/engine", 8 | "crates/simulation", 9 | "crates/test-macros", 10 | "crates/utils", 11 | "crates/xline", 12 | "crates/xline-client", 13 | "crates/xline-test-utils", 14 | "crates/xlineapi", 15 | "crates/xlinectl", 16 | "crates/xlineutl", 17 | "workspace-hack", 18 | ] 19 | resolver = "2" 20 | 21 | [workspace.metadata.cargo-machete] 22 | ignored = ["prost", "workspace-hack"] 23 | 24 | [patch.crates-io] 25 | # This branch update the tonic version for madsim. We should switch to the original etcd-client crate when new version release. 26 | madsim = { git = "https://github.com/LucienY01/madsim.git", branch = "bz/tonic-0-12" } 27 | madsim-tonic = { git = "https://github.com/LucienY01/madsim.git", branch = "bz/tonic-0-12" } 28 | madsim-tonic-build = { git = "https://github.com/LucienY01/madsim.git", branch = "bz/tonic-0-12" } 29 | madsim-tokio = { git = "https://github.com/LucienY01/madsim.git", branch = "bz/tonic-0-12" } 30 | 31 | -------------------------------------------------------------------------------- /Cross.toml: -------------------------------------------------------------------------------- 1 | [target.aarch64-unknown-linux-gnu] 2 | image = "aarch64-linux-gnu" 3 | 4 | [target.x86_64-unknown-linux-gnu] 5 | image = "x86_64-linux-gnu" 6 | -------------------------------------------------------------------------------- /MAINTAINERS: -------------------------------------------------------------------------------- 1 | List of maintainers and the parts they're maintaining 2 | ============================================ 3 | 4 | We group the maintainers list by the parts they're maintaining, and only show the github names. 5 | ---------------- 6 | 7 | # Maintainers 8 | 9 | Roger Shi (@rogercloud) curp:* xline:* 10 | Phoenix Zhao (@Phoenix500526) curp:* xline:* 11 | Yu Guan (@themanforfree) curp:* xline:* 12 | Zhenghao Yin (@bsbds) xline:* 13 | Wenhui Tang (@iGxnon) xline:* 14 | -------------------------------------------------------------------------------- /ci/artifacts/kind-node-xline.Dockerfile: -------------------------------------------------------------------------------- 1 | ARG K8S_VERSION 2 | 3 | FROM kindest/node:${K8S_VERSION} 4 | 5 | RUN mkdir /tmp/kind 6 | COPY xline.tar /tmp/kind/ 7 | RUN ( containerd -l warning & ) && ctr -n k8s.io images import --no-unpack /tmp/kind/*.tar 8 | RUN rm /tmp/kind/*.tar 9 | -------------------------------------------------------------------------------- /ci/artifacts/kind.yaml: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: kind.x-k8s.io/v1alpha4 3 | nodes: 4 | - role: control-plane 5 | image: ghcr.io/xline-kv/kind-node-xline:${K8S_VERSION}-master 6 | extraMounts: 7 | - hostPath: ${WORKSPACE}/ci/artifacts/xline-pod.yaml 8 | containerPath: /etc/kubernetes/manifests/xline.yaml 9 | kubeadmConfigPatches: 10 | - | 11 | kind: ClusterConfiguration 12 | etcd: 13 | external: 14 | endpoints: 15 | - http://kind-control-plane:2379 16 | - role: worker 17 | image: ghcr.io/xline-kv/kind-node-xline:${K8S_VERSION}-master 18 | - role: worker 19 | image: ghcr.io/xline-kv/kind-node-xline:${K8S_VERSION}-master 20 | -------------------------------------------------------------------------------- /ci/artifacts/xline-pod.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | creationTimestamp: null 5 | name: xline 6 | namespace: kube-system 7 | spec: 8 | containers: 9 | - name: xline 10 | image: ghcr.io/xline-kv/xline:master 11 | imagePullPolicy: IfNotPresent 12 | command: 13 | - /bin/sh 14 | - -c 15 | - xline --name node1 --members node1=kind-control-plane:2379 --data-dir /tmp/xline --storage-engine rocksdb --client-listen-urls=http://kind-control-plane:2379 --peer-listen-urls=http://kind-control-plane:2380,http://kind-control-plane:2381 --client-advertise-urls=http://kind-control-plane:2379 --peer-advertise-urls=http://kind-control-plane:2380,http://kind-control-plane:2381 16 | hostNetwork: true 17 | status: {} 18 | -------------------------------------------------------------------------------- /ci/build-env.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" 6 | cd "$DIR" 7 | 8 | echo "=== Toolchain ===" 9 | cat rust-toolchain.toml 10 | 11 | export DOCKER_BUILDKIT=1 12 | export GHCR_ORG=xline-kv 13 | export BUILD_TAG="ghcr.io/${GHCR_ORG}/build-env:latest" 14 | 15 | # Change this version if rust-rocksdb updates in `engine` 16 | export LIB_ROCKS_SYS_VER="0.16.0+8.10.0" 17 | 18 | echo "=== Arch ===" 19 | arch 20 | 21 | echo "=== Docker build ===" 22 | set -x 23 | 24 | docker build -t ${BUILD_TAG} --progress=plain --no-cache --build-arg LIB_ROCKS_SYS_VER=$LIB_ROCKS_SYS_VER . 25 | 26 | set +x 27 | 28 | echo "=== Docker login ===" 29 | echo -n $GITHUB_TOKEN | docker login --username $GHCR_ORG --password-stdin "ghcr.io/${GHCR_ORG}" 30 | 31 | echo "=== Docker push ===" 32 | docker push ${BUILD_TAG} 33 | -------------------------------------------------------------------------------- /ci/cross/Dockerfile.aarch64-unknown-linux-gnu: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/cross-rs/aarch64-unknown-linux-gnu:main@sha256:b4f5bf74812f9bb6516140d4b83d1f173c2d5ce0523f3e1c2253d99d851c734f 2 | 3 | ENV PKG_CONFIG_ALLOW_CROSS="true" 4 | 5 | RUN set-eux; dpkg --add-architecture arm64 && \ 6 | apt-get update && \ 7 | apt-get install --assume-yes clang-8 libclang-8-dev binutils-aarch64-linux-gnu zlib1g-dev:arm64 unzip wget && \ 8 | wget https://github.com/protocolbuffers/protobuf/releases/download/v21.10/protoc-21.10-linux-x86_64.zip && \ 9 | unzip protoc-21.10-linux-x86_64.zip -d /usr/local 10 | -------------------------------------------------------------------------------- /ci/cross/Dockerfile.x86_64-unknown-linux-gnu: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/cross-rs/x86_64-unknown-linux-gnu:main@sha256:bf0cd3027befe882feb5a2b4040dc6dbdcb799b25c5338342a03163cea43da1b 2 | 3 | RUN set-eux; apt-get update && \ 4 | apt-get install --assume-yes clang libclang-dev unzip wget && \ 5 | wget https://github.com/protocolbuffers/protobuf/releases/download/v21.10/protoc-21.10-linux-x86_64.zip && \ 6 | unzip protoc-21.10-linux-x86_64.zip -d /usr/local 7 | -------------------------------------------------------------------------------- /ci/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.74.0" 3 | components = ["rustfmt", "clippy", "rust-src"] 4 | -------------------------------------------------------------------------------- /ci/scripts/check-trailing-spaces.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Script Author: https://github.com/stdrc 4 | # Repo: https://github.com/risingwavelabs/risingwave/blame/main/scripts/check/check-trailing-spaces.sh 5 | 6 | # Exits as soon as any line fails. 7 | set -euo pipefail 8 | 9 | self=$0 10 | 11 | # Shell colors 12 | RED='\033[0;31m' 13 | BLUE='\033[0;34m' 14 | GREEN='\033[0;32m' 15 | ORANGE='\033[0;33m' 16 | BOLD='\033[1m' 17 | NONE='\033[0m' 18 | 19 | print_help() { 20 | echo "Usage: $self [-f|--fix]" 21 | echo 22 | echo "Options:" 23 | echo " -f, --fix Fix trailing spaces." 24 | echo " -h, --help Show this help message and exit." 25 | } 26 | 27 | fix=false 28 | while [ $# -gt 0 ]; do 29 | case $1 in 30 | -f | --fix) 31 | fix=true 32 | ;; 33 | -h | --help) 34 | print_help 35 | exit 0 36 | ;; 37 | *) 38 | echo -e "${RED}${BOLD}$self: invalid option \`$1\`\n${NONE}" 39 | print_help 40 | exit 1 41 | ;; 42 | esac 43 | shift 44 | done 45 | 46 | temp_file=$(mktemp) 47 | 48 | echo -ne "${BLUE}" 49 | git config --global --add safe.directory '*' 50 | git grep -nIP --untracked '[[:space:]]+$' | tee $temp_file || true 51 | echo -ne "${NONE}" 52 | 53 | bad_files=$(cat $temp_file | cut -f1 -d ':' | sort -u) 54 | rm $temp_file 55 | 56 | if [ ! -z "$bad_files" ]; then 57 | if [[ $fix == true ]]; then 58 | for file in $bad_files; do 59 | sed -i '' -e's/[[:space:]]*$//' "$file" 60 | done 61 | 62 | echo 63 | echo -e "${GREEN}${BOLD}All trailing spaces listed above have been cleaned.${NONE}" 64 | exit 0 65 | else 66 | echo 67 | echo -e "${RED}${BOLD}Please clean all the trailing spaces listed above.${NONE}" 68 | echo -e "${BOLD}You can run '$self --fix' for convenience.${NONE}" 69 | exit 1 70 | fi 71 | else 72 | echo -e "${GREEN}${BOLD}No trailing spaces found.${NONE}" 73 | exit 0 74 | fi 75 | -------------------------------------------------------------------------------- /ci/scripts/install_macos_deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | brew install llvm 4 | brew install protobuf 5 | -------------------------------------------------------------------------------- /ci/scripts/kind.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | K8S_VERSION=${K8S_VERSION:-"v1.27.3"} 8 | KIND_VERSION=${KIND_VERSION:-"0.22.0"} 9 | 10 | wget -q https://github.com/kubernetes-sigs/kind/releases/download/v$KIND_VERSION/kind-linux-amd64 11 | chmod +x kind-linux-amd64 && mv kind-linux-amd64 /usr/local/bin/kind 12 | 13 | # print the config file 14 | WORKSPACE=$PWD envsubst 15 | 16 | WORKSPACE=$PWD envsubst < ci/artifacts/kind.yaml | kind create cluster -v7 --retain --wait 4m --config - 17 | kubectl wait node --all --for condition=ready 18 | -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | # git-cliff ~ default configuration file 2 | # https://git-cliff.org/docs/configuration 3 | # 4 | # Lines starting with "#" are comments. 5 | # Configuration options are organized into tables and keys. 6 | # See documentation for more information on available options. 7 | 8 | [changelog] 9 | # changelog header 10 | header = """ 11 | # Changelog\n 12 | All notable changes to this project will be documented in this file.\n 13 | """ 14 | # template for the changelog body 15 | # https://tera.netlify.app/docs 16 | body = """ 17 | {% if version %}\ 18 | ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} 19 | {% else %}\ 20 | ## [unreleased] 21 | {% endif %}\ 22 | {% for group, commits in commits | group_by(attribute="group") %} 23 | ### {{ group | upper_first }} 24 | {% for commit in commits %} 25 | - {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\ 26 | {% endfor %} 27 | {% endfor %}\n 28 | """ 29 | # remove the leading and trailing whitespace from the template 30 | trim = true 31 | # changelog footer 32 | footer = """ 33 | 34 | """ 35 | 36 | [git] 37 | # parse the commits based on https://www.conventionalcommits.org 38 | conventional_commits = true 39 | # filter out the commits that are not conventional 40 | filter_unconventional = true 41 | # process each line of a commit as an individual commit 42 | split_commits = false 43 | # regex for parsing and grouping commits 44 | commit_parsers = [ 45 | { message = "^feat", group = "Features" }, 46 | { message = "^fix", group = "Bug Fixes" }, 47 | { message = "^perf", group = "Performance" }, 48 | { message = "^refactor", group = "Refactor" }, 49 | { message = "^chore\\(release\\): prepare for", skip = true }, 50 | ] 51 | # protect breaking changes from being skipped due to matching a skipping commit_parser 52 | protect_breaking_commits = true 53 | # filter out the commits that are not matched by commit parsers 54 | filter_commits = true 55 | # glob pattern for matching git tags 56 | tag_pattern = "v[0-9]*" 57 | # sort the commits inside sections by oldest/newest order 58 | sort_commits = "oldest" 59 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | disallowed-methods = [ 2 | { path = "event_listener::EventListener::wait", reason = "no blocking single-thread runtime, please use .await instead" }, 3 | { path = "event_listener::EventListener::wait_timeout", reason = "no blocking single-thread runtime, please use .await instead" }, 4 | { path = "event_listener::EventListener::wait_deadline", reason = "no blocking single-thread runtime, please use .await instead" }, 5 | ] 6 | -------------------------------------------------------------------------------- /crates/benchmark/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "benchmark" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | authors = ["DatenLord "] 7 | categories = ["tools"] 8 | description = "Benchmark tool for Xline" 9 | keywords = ["benchmark"] 10 | license = "Apache-2.0" 11 | readme = "README.md" 12 | repository = "https://github.com/xline-kv/Xline/tree/master/benchmark" 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | anyhow = "1.0.90" 17 | clap = { version = "4", features = ["derive"] } 18 | clippy-utilities = "0.2.0" 19 | etcd-client = { version = "0.14.0", features = ["tls"] } 20 | futures = "0.3.30" 21 | indicatif = "0.17.8" 22 | rand = "0.8.5" 23 | thiserror = "1.0.61" 24 | tokio = "1.21.2" 25 | tracing = "0.1.37" 26 | tracing-subscriber = "0.3.1" 27 | utils = { path = "../utils", version = "0.1.0" } 28 | workspace-hack = { version = "0.1", path = "../../workspace-hack" } 29 | xline-client = { path = "../xline-client" } 30 | xline-test-utils = { path = "../xline-test-utils" } 31 | xlineapi = { path = "../xlineapi" } 32 | -------------------------------------------------------------------------------- /crates/benchmark/src/args.rs: -------------------------------------------------------------------------------- 1 | use clap::{Parser, Subcommand}; 2 | 3 | #[derive(Parser, Debug)] 4 | #[non_exhaustive] 5 | #[clap(author, version, about, long_about = None)] 6 | /// Args of Benchmark 7 | pub struct Benchmark { 8 | /// The address of the server 9 | #[clap(long, value_delimiter = ',', default_value = "127.0.0.1:2379")] 10 | pub endpoints: Vec, 11 | /// Clients number 12 | #[clap(long, default_value_t = 1)] 13 | pub clients: usize, 14 | /// Use curp or not 15 | #[clap(long)] 16 | pub use_curp: bool, 17 | /// Output to stdout 18 | #[clap(long)] 19 | pub stdout: bool, 20 | /// Sub command 21 | #[clap(subcommand)] 22 | pub command: Commands, 23 | } 24 | 25 | /// Types of sub command 26 | #[derive(Subcommand, Debug, Clone, Copy)] 27 | pub enum Commands { 28 | /// Put args 29 | Put { 30 | /// Key size 31 | #[clap(long, default_value_t = 8)] 32 | key_size: usize, 33 | /// Value size 34 | #[clap(long, default_value_t = 8)] 35 | val_size: usize, 36 | /// Total number of requests 37 | #[clap(long, default_value_t = 10000)] 38 | total: usize, 39 | /// Key space size 40 | #[clap(long, default_value_t = 1)] 41 | key_space_size: usize, 42 | /// sequential keys or not 43 | #[clap(long, default_value_t = false)] 44 | sequential_keys: bool, 45 | }, 46 | } 47 | -------------------------------------------------------------------------------- /crates/benchmark/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use anyhow::Result; 4 | use benchmark::{Benchmark, CommandRunner}; 5 | use clap::Parser; 6 | use tracing::info; 7 | use tracing_subscriber::{ 8 | filter::{LevelFilter, Targets}, 9 | prelude::*, 10 | Layer, 11 | }; 12 | 13 | fn tracing_init(stdout: bool) { 14 | let option_layer = if stdout { 15 | Some( 16 | tracing_subscriber::fmt::layer() 17 | .with_ansi(false) 18 | .with_writer(io::stdout) 19 | .with_filter(Targets::new().with_target("benchmark", LevelFilter::INFO)), 20 | ) 21 | } else { 22 | None 23 | }; 24 | let layer = tracing_subscriber::fmt::layer() 25 | .with_writer(io::stderr) 26 | .with_filter(Targets::new().with_target("benchmark", LevelFilter::DEBUG)); 27 | tracing_subscriber::registry() 28 | .with(layer) 29 | .with(option_layer) 30 | .init(); 31 | } 32 | 33 | #[tokio::main] 34 | async fn main() -> Result<()> { 35 | let args: Benchmark = Benchmark::parse(); 36 | tracing_init(args.stdout); 37 | 38 | let mut runner = CommandRunner::new(args); 39 | let stats = runner.run().await?; 40 | 41 | let summary = stats.summary(); 42 | let histogram = stats.histogram(); 43 | 44 | info!("{}", summary); 45 | info!("{}", histogram); 46 | 47 | Ok(()) 48 | } 49 | -------------------------------------------------------------------------------- /crates/curp-external-api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "curp-external-api" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["DatenLord "] 6 | repository = "https://github.com/xline-kv/Xline/tree/master/curp-external-api" 7 | description = "Curp external API" 8 | license = "Apache-2.0" 9 | readme = "README.md" 10 | categories = ["API"] 11 | keywords = ["API", "Curp"] 12 | 13 | [dependencies] 14 | async-trait = "0.1.81" 15 | engine = { path = "../engine" } 16 | mockall = "0.12.1" 17 | prost = "0.13" 18 | serde = { version = "1.0.204", features = ["derive", "rc"] } 19 | thiserror = "1.0.61" 20 | workspace-hack = { version = "0.1", path = "../../workspace-hack" } 21 | -------------------------------------------------------------------------------- /crates/curp-external-api/README.md: -------------------------------------------------------------------------------- 1 | # Curp Extend Interface 2 | 3 | This crate provides curp public trait interfaces for external applications. 4 | -------------------------------------------------------------------------------- /crates/curp-external-api/src/conflict.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::module_name_repetitions)] 2 | 3 | use std::hash::Hash; 4 | 5 | /// Entry with an identifier 6 | pub trait EntryId { 7 | /// The type of the id 8 | type Id: Copy + Hash; 9 | 10 | /// Gets the id of the entry 11 | fn id(&self) -> Self::Id; 12 | } 13 | 14 | /// Common operations for conflict pools 15 | pub trait ConflictPoolOp { 16 | /// Entry of the pool 17 | type Entry: EntryId; 18 | 19 | /// Removes a command from the pool 20 | fn remove(&mut self, entry: &Self::Entry); 21 | 22 | /// Returns all commands in the pool 23 | fn all(&self) -> Vec; 24 | 25 | /// Clears all entries in the pool 26 | fn clear(&mut self); 27 | 28 | /// Returns the number of commands in the pool 29 | fn len(&self) -> usize; 30 | 31 | /// Checks if the pool contains some commands that will conflict with all other commands 32 | fn is_empty(&self) -> bool; 33 | } 34 | 35 | /// Speculative pool operations 36 | pub trait SpeculativePoolOp: ConflictPoolOp { 37 | /// Inserts a command in to the pool 38 | /// 39 | /// Returns the entry if a conflict is detected 40 | fn insert_if_not_conflict(&mut self, entry: Self::Entry) -> Option; 41 | } 42 | 43 | /// Uncommitted pool operations 44 | pub trait UncommittedPoolOp: ConflictPoolOp { 45 | /// Inserts a command in to the pool 46 | /// 47 | /// Returns `true` if a conflict is detected 48 | fn insert(&mut self, entry: Self::Entry) -> bool; 49 | 50 | /// Returns all commands in the pool that conflicts with the given command 51 | fn all_conflict(&self, entry: &Self::Entry) -> Vec; 52 | } 53 | -------------------------------------------------------------------------------- /crates/curp-external-api/src/role_change.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::arithmetic_side_effects, clippy::indexing_slicing)] // introduced by mock! 2 | 3 | use mockall::mock; 4 | 5 | /// Callback when the leadership changes 6 | pub trait RoleChange: Clone + Send + Sync + 'static { 7 | /// The `on_election_win` will be invoked when the current server win the election. 8 | /// It means that the current server's role will change from Candidate to Leader. 9 | fn on_election_win(&self); 10 | 11 | /// The `on_calibrate` will be invoked when the current server has been calibrated. 12 | /// It means that the current server's role will change from Leader to Follower. 13 | fn on_calibrate(&self); 14 | } 15 | 16 | mock! { 17 | RoleChange {} 18 | 19 | impl Clone for RoleChange { 20 | fn clone(&self) -> Self; 21 | } 22 | 23 | impl RoleChange for RoleChange { 24 | fn on_election_win(&self); 25 | fn on_calibrate(&self); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /crates/curp-test-utils/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "curp-test-utils" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["DatenLord "] 6 | repository = "https://github.com/xline-kv/Xline/tree/master/curp-test-utils" 7 | description = "Test utils for curp" 8 | categories = ["Test"] 9 | keywords = ["Test", "Utils"] 10 | license = "Apache-2.0" 11 | readme = "README.md" 12 | 13 | [dependencies] 14 | async-trait = "0.1.81" 15 | bincode = "1.3.3" 16 | curp-external-api = { path = "../curp-external-api" } 17 | engine = { path = "../engine" } 18 | itertools = "0.13" 19 | prost = "0.13" 20 | serde = { version = "1.0.204", features = ["derive", "rc"] } 21 | thiserror = "1.0.61" 22 | tokio = { version = "0.2.25", package = "madsim-tokio", features = [ 23 | "rt-multi-thread", 24 | ] } 25 | tracing = { version = "0.1.37", features = ["std", "log", "attributes"] } 26 | tracing-subscriber = { version = "0.3.16", features = ["env-filter", "time"] } 27 | utils = { path = "../utils", version = "0.1.0", features = ["parking_lot"] } 28 | workspace-hack = { version = "0.1", path = "../../workspace-hack" } 29 | -------------------------------------------------------------------------------- /crates/curp-test-utils/README.md: -------------------------------------------------------------------------------- 1 | # Curp test utilities 2 | 3 | This crate provides utilities for curp tests. 4 | -------------------------------------------------------------------------------- /crates/curp-test-utils/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env, 3 | sync::{ 4 | atomic::{AtomicBool, Ordering}, 5 | Arc, 6 | }, 7 | time::Duration, 8 | }; 9 | 10 | use curp_external_api::role_change::RoleChange; 11 | use tracing_subscriber::fmt::time::uptime; 12 | 13 | pub mod test_cmd; 14 | 15 | pub const TEST_TABLE: &str = "test"; 16 | pub const TEST_CLIENT_ID: u64 = 12345; 17 | pub const REVISION_TABLE: &str = "revision"; 18 | pub const META_TABLE: &str = "meta"; 19 | 20 | #[derive(Default, Debug, Clone)] 21 | pub struct TestRoleChange { 22 | pub inner: Arc, 23 | } 24 | 25 | #[derive(Default, Debug)] 26 | pub struct TestRoleChangeInner { 27 | is_leader: AtomicBool, 28 | } 29 | 30 | impl TestRoleChange { 31 | pub fn get_inner_arc(&self) -> Arc { 32 | Arc::clone(&self.inner) 33 | } 34 | } 35 | 36 | impl RoleChange for TestRoleChange { 37 | fn on_calibrate(&self) { 38 | self.inner.is_leader.store(false, Ordering::Relaxed); 39 | } 40 | 41 | fn on_election_win(&self) { 42 | self.inner.is_leader.store(true, Ordering::Relaxed); 43 | } 44 | } 45 | 46 | impl TestRoleChangeInner { 47 | pub fn get_is_leader(&self) -> bool { 48 | self.is_leader.load(Ordering::Relaxed) 49 | } 50 | } 51 | 52 | pub fn init_logger() { 53 | if env::var("RUST_LOG").is_err() { 54 | env::set_var("RUST_LOG", "curp=debug,xline=debug"); 55 | } 56 | _ = tracing_subscriber::fmt() 57 | .with_timer(uptime()) 58 | .compact() 59 | .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) 60 | .try_init(); 61 | } 62 | 63 | pub fn mock_role_change() -> TestRoleChange { 64 | TestRoleChange::default() 65 | } 66 | 67 | pub async fn sleep_millis(n: u64) { 68 | tokio::time::sleep(Duration::from_millis(n)).await; 69 | } 70 | 71 | pub async fn sleep_secs(n: u64) { 72 | tokio::time::sleep(Duration::from_secs(n)).await; 73 | } 74 | -------------------------------------------------------------------------------- /crates/curp/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["DatenLord "] 3 | categories = ["Algorithms"] 4 | description = "Curp consensus protocol" 5 | edition = "2021" 6 | keywords = ["consensus", "distributed"] 7 | license = "Apache-2.0" 8 | name = "curp" 9 | readme = "README.md" 10 | repository = "https://github.com/xline-kv/Xline/tree/master/curp" 11 | version = "0.1.0" 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [dependencies] 15 | async-stream = "0.3.4" 16 | async-trait = "0.1.81" 17 | bincode = "1.3.3" 18 | bytes = "1.7.1" 19 | clippy-utilities = "0.2.0" 20 | curp-external-api = { path = "../curp-external-api" } 21 | curp-test-utils = { path = "../curp-test-utils" } 22 | dashmap = "6.1.0" 23 | derive_builder = "0.20.0" 24 | engine = { path = "../engine" } 25 | event-listener = "5.3.1" 26 | flume = "0.11.0" 27 | fs2 = "0.4.3" 28 | futures = "0.3.21" 29 | indexmap = "2.2.6" 30 | itertools = "0.13" 31 | madsim = { version = "0.2.27", features = ["rpc", "macros"] } 32 | opentelemetry = { version = "0.24.0", features = ["metrics"] } 33 | parking_lot = "0.12.3" 34 | priority-queue = "2.0.2" 35 | prost = "0.13" 36 | rand = "0.8.5" 37 | serde = { version = "1.0.204", features = ["derive", "rc"] } 38 | sha2 = "0.10.8" 39 | thiserror = "1.0.61" 40 | tokio = { version = "0.2.25", package = "madsim-tokio", features = [ 41 | "rt-multi-thread", 42 | ] } 43 | tokio-stream = { git = "https://github.com/madsim-rs/tokio.git", rev = "ab251ad", features = [ 44 | "net", 45 | ] } 46 | tokio-util = "0.7.11" 47 | tonic = { version = "0.5.0", package = "madsim-tonic", features = ["tls"] } 48 | tower = { version = "0.4.13", features = ["filter"] } 49 | tracing = { version = "0.1.37", features = ["std", "log", "attributes"] } 50 | utils = { path = "../utils", version = "0.1.0", features = ["parking_lot"] } 51 | workspace-hack = { version = "0.1", path = "../../workspace-hack" } 52 | 53 | [dev-dependencies] 54 | anyhow = "1.0.90" 55 | curp-test-utils = { path = "../curp-test-utils" } 56 | itertools = "0.13" 57 | mockall = "0.12.1" 58 | once_cell = "1.17.0" 59 | tempfile = "3" 60 | test-macros = { path = "../test-macros" } 61 | tracing-subscriber = { version = "0.3.16", features = ["env-filter", "time"] } 62 | tracing-test = "0.2.4" 63 | 64 | [build-dependencies] 65 | prost-build = "0.13.0" 66 | tonic-build = { version = "0.5.0", package = "madsim-tonic-build" } 67 | 68 | [features] 69 | client-metrics = [] 70 | -------------------------------------------------------------------------------- /crates/curp/README.md: -------------------------------------------------------------------------------- 1 | # Curp 2 | 3 | This crate provides basic implementation of the `Curp` protocol. 4 | -------------------------------------------------------------------------------- /crates/curp/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tonic_build::configure() 3 | .type_attribute( 4 | "ProposeConfChangeRequest.ConfChange", 5 | "#[derive(serde::Deserialize, serde::Serialize)]", 6 | ) 7 | .compile( 8 | &["./proto/common/src/curp-command.proto"], 9 | &["./proto/common/src"], 10 | ) 11 | .unwrap_or_else(|e| panic!("Failed to compile proto, error is {:?}", e)); 12 | 13 | let mut prost_config = prost_build::Config::new(); 14 | prost_config.bytes([".inner_messagepb.InstallSnapshotRequest"]); 15 | tonic_build::configure() 16 | .compile_with_config( 17 | prost_config, 18 | &["./proto/inner_message.proto"], 19 | &["./proto/"], 20 | ) 21 | .unwrap_or_else(|e| panic!("Failed to compile proto, error is {:?}", e)); 22 | } 23 | -------------------------------------------------------------------------------- /crates/curp/proto/inner_message.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package inner_messagepb; 4 | 5 | message AppendEntriesRequest { 6 | uint64 term = 1; 7 | uint64 leader_id = 2; 8 | uint64 prev_log_index = 3; 9 | uint64 prev_log_term = 4; 10 | repeated bytes entries = 5; 11 | uint64 leader_commit = 6; 12 | } 13 | 14 | message AppendEntriesResponse { 15 | uint64 term = 1; 16 | bool success = 2; 17 | uint64 hint_index = 3; 18 | } 19 | 20 | message VoteRequest { 21 | uint64 term = 1; 22 | uint64 candidate_id = 2; 23 | uint64 last_log_index = 3; 24 | uint64 last_log_term = 4; 25 | bool is_pre_vote = 5; 26 | } 27 | 28 | message VoteResponse { 29 | uint64 term = 1; 30 | bool vote_granted = 2; 31 | repeated bytes spec_pool = 3; 32 | bool shutdown_candidate = 4; 33 | } 34 | 35 | message InstallSnapshotRequest { 36 | uint64 term = 1; 37 | uint64 leader_id = 2; 38 | uint64 last_included_index = 3; 39 | uint64 last_included_term = 4; 40 | uint64 offset = 5; 41 | bytes data = 6; 42 | bool done = 7; 43 | } 44 | 45 | message InstallSnapshotResponse { 46 | uint64 term = 1; 47 | } 48 | 49 | message TriggerShutdownRequest {} 50 | 51 | message TriggerShutdownResponse {} 52 | 53 | message TryBecomeLeaderNowRequest {} 54 | 55 | message TryBecomeLeaderNowResponse {} 56 | 57 | service InnerProtocol { 58 | rpc AppendEntries(AppendEntriesRequest) returns (AppendEntriesResponse); 59 | rpc Vote(VoteRequest) returns (VoteResponse); 60 | rpc InstallSnapshot(stream InstallSnapshotRequest) 61 | returns (InstallSnapshotResponse); 62 | rpc TriggerShutdown(TriggerShutdownRequest) 63 | returns (TriggerShutdownResponse); 64 | rpc TryBecomeLeaderNow(TryBecomeLeaderNowRequest) returns (TryBecomeLeaderNowResponse); 65 | } 66 | -------------------------------------------------------------------------------- /crates/curp/src/client/metrics.rs: -------------------------------------------------------------------------------- 1 | use opentelemetry::metrics::Counter; 2 | use utils::define_metrics; 3 | 4 | define_metrics! { 5 | "curp_client", 6 | client_retry_count: Counter = meter() 7 | .u64_counter("client_retry_count") 8 | .with_description("The total number of retries when the client propose to the cluster.") 9 | .init(), 10 | client_fast_path_count: Counter = meter() 11 | .u64_counter("client_fast_path_count") 12 | .with_description("The total number of fast path when the client propose to the cluster.") 13 | .init(), 14 | client_slow_path_count: Counter = meter() 15 | .u64_counter("client_slow_path_count") 16 | .with_description("The total number of slow path when the client propose to the cluster.") 17 | .init(), 18 | client_fast_path_fallback_slow_path_count: Counter = meter() 19 | .u64_counter("client_fast_path_fallback_slow_path_count") 20 | .with_description("The total number of fast path fallbacks into slow path when the client propose to the cluster.") 21 | .init() 22 | } 23 | -------------------------------------------------------------------------------- /crates/curp/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use thiserror::Error; 4 | 5 | use crate::server::StorageError; 6 | 7 | /// Server bootstrap error 8 | #[allow(clippy::module_name_repetitions)] // this-error generate code false-positive 9 | #[non_exhaustive] 10 | #[derive(Error, Debug)] 11 | pub enum ServerError { 12 | /// Met I/O error during rpc communication 13 | #[error("meet io related error")] 14 | IoError(#[from] io::Error), 15 | 16 | /// Parsing error 17 | #[error("parsing error: {0}")] 18 | ParsingError(String), 19 | 20 | /// Rpc Error 21 | #[error("rpc error: {0}")] 22 | RpcError(#[from] tonic::transport::Error), 23 | 24 | /// Storage Error 25 | #[error("storage error: {0}")] 26 | StorageError(#[from] StorageError), 27 | } 28 | -------------------------------------------------------------------------------- /crates/curp/src/response.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::{AtomicBool, Ordering}; 2 | 3 | use curp_external_api::cmd::Command; 4 | use tonic::Status; 5 | 6 | use crate::rpc::{OpResponse, ProposeResponse, ResponseOp, SyncedResponse}; 7 | 8 | /// The response sender 9 | #[derive(Debug)] 10 | pub(super) struct ResponseSender { 11 | /// The stream sender 12 | tx: flume::Sender>, 13 | /// Whether the command will be speculatively executed 14 | conflict: AtomicBool, 15 | } 16 | 17 | impl ResponseSender { 18 | /// Creates a new `ResponseSender` 19 | pub(super) fn new(tx: flume::Sender>) -> ResponseSender { 20 | ResponseSender { 21 | tx, 22 | conflict: AtomicBool::new(false), 23 | } 24 | } 25 | 26 | /// Gets whether the command associated with this sender will be 27 | /// speculatively executed 28 | pub(super) fn is_conflict(&self) -> bool { 29 | self.conflict.load(Ordering::SeqCst) 30 | } 31 | 32 | /// Sets the the command associated with this sender will be 33 | /// speculatively executed 34 | pub(super) fn set_conflict(&self, conflict: bool) { 35 | let _ignore = self.conflict.fetch_or(conflict, Ordering::SeqCst); 36 | } 37 | 38 | /// Sends propose result 39 | pub(super) fn send_propose(&self, resp: ProposeResponse) { 40 | let resp = OpResponse { 41 | op: Some(ResponseOp::Propose(resp)), 42 | }; 43 | // Ignore the result because the client might close the receiving stream 44 | let _ignore = self.tx.try_send(Ok(resp)); 45 | } 46 | 47 | /// Sends after sync result 48 | pub(super) fn send_synced(&self, resp: SyncedResponse) { 49 | let resp = OpResponse { 50 | op: Some(ResponseOp::Synced(resp)), 51 | }; 52 | // Ignore the result because the client might close the receiving stream 53 | let _ignore = self.tx.try_send(Ok(resp)); 54 | } 55 | 56 | /// Sends the error result 57 | pub(super) fn send_err(&self, err: C::Error) { 58 | let er = ProposeResponse::new_result::(&Err(err.clone()), false); 59 | let asr = SyncedResponse::new_result::(&Err(err)); 60 | self.send_propose(er); 61 | self.send_synced(asr); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /crates/curp/src/rpc/metrics.rs: -------------------------------------------------------------------------------- 1 | use opentelemetry::metrics::{Counter, Histogram}; 2 | use utils::define_metrics; 3 | 4 | define_metrics! { 5 | "curp_p2p", 6 | peer_sent_bytes_total: Counter = meter() 7 | .u64_counter("peer_sent_bytes") 8 | .with_description("The total number of bytes send to peers.") 9 | .init(), 10 | peer_sent_failures_total: Counter = meter() 11 | .u64_counter("peer_sent_failures") 12 | .with_description("The total number of send failures to peers.") 13 | .init(), 14 | peer_round_trip_time_seconds: Histogram = meter() 15 | .u64_histogram("peer_round_trip_time_seconds") 16 | .with_description("The round-trip-time histogram between peers.") 17 | .init() 18 | } 19 | -------------------------------------------------------------------------------- /crates/curp/src/server/conflict/mod.rs: -------------------------------------------------------------------------------- 1 | /// Speculative pool 2 | pub(crate) mod spec_pool_new; 3 | 4 | /// Uncommitted pool 5 | pub(crate) mod uncommitted_pool; 6 | 7 | #[cfg(test)] 8 | mod tests; 9 | 10 | /// Conflict pool used in tests 11 | #[doc(hidden)] 12 | pub mod test_pools; 13 | -------------------------------------------------------------------------------- /crates/curp/src/server/conflict/spec_pool_new.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, sync::Arc}; 2 | 3 | use curp_external_api::conflict::SpeculativePoolOp; 4 | use parking_lot::Mutex; 5 | 6 | use crate::rpc::{PoolEntry, ProposeId}; 7 | 8 | /// Ref to `SpeculativePool` 9 | pub(crate) type SpeculativePoolRef = Arc>>; 10 | 11 | /// A speculative pool object 12 | pub type SpObject = Box> + Send + 'static>; 13 | 14 | /// Union type of `SpeculativePool` objects 15 | pub(crate) struct SpeculativePool { 16 | /// Command speculative pools 17 | command_sps: Vec>, 18 | /// propose id to entry mapping 19 | entries: HashMap>, 20 | } 21 | 22 | impl SpeculativePool { 23 | /// Creates a new pool 24 | pub(crate) fn new(command_sps: Vec>) -> Self { 25 | Self { 26 | command_sps, 27 | entries: HashMap::new(), 28 | } 29 | } 30 | 31 | /// Inserts an entry into the pool 32 | #[allow(clippy::needless_pass_by_value)] // we need to consume the entry 33 | pub(crate) fn insert(&mut self, entry: PoolEntry) -> Option> { 34 | for csp in &mut self.command_sps { 35 | if let Some(e) = csp.insert_if_not_conflict(entry.clone()) { 36 | return Some(e); 37 | } 38 | } 39 | 40 | let _ignore = self.entries.insert(entry.id, entry); 41 | 42 | None 43 | } 44 | 45 | /// Removes an entry from the pool 46 | pub(crate) fn remove(&mut self, entry: &PoolEntry) { 47 | for csp in &mut self.command_sps { 48 | csp.remove(entry); 49 | } 50 | 51 | let _ignore = self.entries.remove(&entry.id); 52 | } 53 | 54 | /// Removes an entry from the pool by it's propose id 55 | pub(crate) fn remove_by_id(&mut self, id: &ProposeId) { 56 | if let Some(entry) = self.entries.remove(id) { 57 | for csp in &mut self.command_sps { 58 | csp.remove(&entry); 59 | } 60 | } 61 | } 62 | 63 | /// Returns all entries in the pool 64 | pub(crate) fn all(&self) -> Vec> { 65 | let mut entries = Vec::new(); 66 | for csp in &self.command_sps { 67 | entries.extend(csp.all().into_iter().map(Into::into)); 68 | } 69 | entries 70 | } 71 | 72 | /// Returns the number of entries in the pool 73 | #[allow(clippy::arithmetic_side_effects)] // Pool sizes can't overflow a `usize` 74 | pub(crate) fn len(&self) -> usize { 75 | self.command_sps 76 | .iter() 77 | .fold(0, |sum, pool| sum + pool.len()) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /crates/curp/src/server/conflict/uncommitted_pool.rs: -------------------------------------------------------------------------------- 1 | use curp_external_api::conflict::UncommittedPoolOp; 2 | 3 | use crate::rpc::PoolEntry; 4 | 5 | /// An uncommitted pool object 6 | pub type UcpObject = Box> + Send + 'static>; 7 | 8 | /// Union type of `UncommittedPool` objects 9 | pub(crate) struct UncommittedPool { 10 | /// Command uncommitted pools 11 | command_ucps: Vec>, 12 | } 13 | 14 | impl UncommittedPool { 15 | /// Creates a new `UncomPool` 16 | pub(crate) fn new(command_ucps: Vec>) -> Self { 17 | Self { command_ucps } 18 | } 19 | 20 | /// Insert an entry into the pool 21 | pub(crate) fn insert(&mut self, entry: &PoolEntry) -> bool { 22 | let mut conflict = false; 23 | 24 | for cucp in &mut self.command_ucps { 25 | conflict |= cucp.insert(entry.clone()); 26 | } 27 | 28 | conflict 29 | } 30 | 31 | /// Removes an entry from the pool 32 | pub(crate) fn remove(&mut self, entry: &PoolEntry) { 33 | for cucp in &mut self.command_ucps { 34 | cucp.remove(entry); 35 | } 36 | } 37 | 38 | /// Returns all entries in the pool that conflict with the given entry 39 | pub(crate) fn all_conflict(&self, entry: &PoolEntry) -> Vec> { 40 | self.command_ucps 41 | .iter() 42 | .flat_map(|p| p.all_conflict(entry)) 43 | .collect() 44 | } 45 | 46 | #[cfg(test)] 47 | /// Gets all entries in the pool 48 | pub(crate) fn all(&self) -> Vec> { 49 | let mut entries = Vec::new(); 50 | for csp in &self.command_ucps { 51 | entries.extend(csp.all().into_iter()); 52 | } 53 | entries 54 | } 55 | 56 | #[cfg(test)] 57 | /// Returns `true` if the pool is empty 58 | pub(crate) fn is_empty(&self) -> bool { 59 | self.command_ucps.iter().all(|ucp| ucp.is_empty()) 60 | } 61 | 62 | /// Clears all entries in the pool 63 | pub(crate) fn clear(&mut self) { 64 | for ucp in &mut self.command_ucps { 65 | ucp.clear(); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /crates/curp/src/server/storage/wal/config.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | /// Size in bytes per segment, default is 64MiB 4 | const DEFAULT_SEGMENT_SIZE: u64 = 64 * 1024 * 1024; 5 | 6 | /// The config for WAL 7 | #[derive(Debug, Clone)] 8 | pub(crate) enum WALConfig { 9 | /// Persistent implementation 10 | Persistent(PersistentConfig), 11 | /// Mock memory implementation 12 | Memory, 13 | } 14 | 15 | /// The config for persistent WAL 16 | #[derive(Debug, Clone)] 17 | pub(crate) struct PersistentConfig { 18 | /// The path of this config 19 | pub(super) dir: PathBuf, 20 | /// The maximum size of this segment 21 | /// 22 | /// NOTE: This is a soft limit, the actual size may larger than this 23 | pub(super) max_segment_size: u64, 24 | } 25 | 26 | impl WALConfig { 27 | /// Creates a new `WALConfig` 28 | pub(crate) fn new(dir: impl AsRef) -> Self { 29 | Self::Persistent(PersistentConfig { 30 | dir: dir.as_ref().into(), 31 | max_segment_size: DEFAULT_SEGMENT_SIZE, 32 | }) 33 | } 34 | 35 | /// Creates a new memory `WALConfig` 36 | pub(crate) fn new_memory() -> Self { 37 | Self::Memory 38 | } 39 | 40 | /// Sets the `max_segment_size` 41 | pub(crate) fn with_max_segment_size(self, size: u64) -> Self { 42 | match self { 43 | Self::Persistent(PersistentConfig { 44 | dir, 45 | max_segment_size, 46 | }) => Self::Persistent(PersistentConfig { 47 | dir, 48 | max_segment_size: size, 49 | }), 50 | Self::Memory => Self::Memory, 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /crates/curp/src/server/storage/wal/error.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use thiserror::Error; 4 | 5 | /// Errors of the `WALStorage` 6 | #[derive(Debug, Error)] 7 | pub(crate) enum WALError { 8 | /// The WAL segment might reach on end 9 | /// 10 | /// NOTE: This exists because we cannot tell the difference between a corrupted WAL 11 | /// and a normally ended WAL, as the segment files are all preallocated with zeros 12 | #[error("WAL ended")] 13 | MaybeEnded, 14 | /// The WAL corrupt error 15 | #[error("WAL corrupted: {0}")] 16 | Corrupted(CorruptType), 17 | /// The IO error 18 | #[error("IO error: {0}")] 19 | IO(#[from] io::Error), 20 | } 21 | 22 | /// The type of the `Corrupted` error 23 | #[derive(Debug, Error)] 24 | pub(crate) enum CorruptType { 25 | /// Corrupt because of decode failure 26 | #[error("Error occurred when decoding WAL: {0}")] 27 | Codec(String), 28 | /// Corrupt because of checksum failure 29 | #[error("Checksumming for the file has failed")] 30 | Checksum, 31 | /// Corrupt because of some logs is missing 32 | #[error("The recovered logs are not continue")] 33 | LogNotContinue, 34 | } 35 | 36 | impl WALError { 37 | /// Converts `WALError` to `io::Result` 38 | pub(super) fn io_or_corrupt(self) -> io::Result { 39 | match self { 40 | WALError::Corrupted(e) => Ok(e), 41 | WALError::IO(e) => Err(e), 42 | WALError::MaybeEnded => unreachable!("Should not call on WALError::MaybeEnded"), 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /crates/curp/src/server/storage/wal/framed.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | /// Decoding of frames via buffers. 4 | pub(super) trait Decoder { 5 | /// The type of decoded frames. 6 | type Item; 7 | 8 | /// The type of unrecoverable frame decoding errors. 9 | type Error: From; 10 | 11 | /// Attempts to decode a frame from the provided buffer of bytes. 12 | fn decode(&mut self, src: &[u8]) -> Result<(Self::Item, usize), Self::Error>; 13 | } 14 | 15 | /// Trait of helper objects to write out messages as bytes 16 | pub(super) trait Encoder { 17 | /// The type of encoding errors. 18 | type Error: From; 19 | 20 | /// Encodes a frame 21 | fn encode(&mut self, item: Item) -> Result, Self::Error>; 22 | } 23 | -------------------------------------------------------------------------------- /crates/curp/src/server/storage/wal/mock/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::VecDeque, io, marker::PhantomData}; 2 | 3 | use curp_external_api::LogIndex; 4 | use serde::{de::DeserializeOwned, Serialize}; 5 | 6 | use crate::log_entry::LogEntry; 7 | 8 | use super::{codec::DataFrame, config::WALConfig, WALStorageOps}; 9 | 10 | /// The mock WAL storage 11 | #[derive(Debug)] 12 | pub(crate) struct WALStorage { 13 | /// Storage 14 | entries: VecDeque>, 15 | } 16 | 17 | impl WALStorage { 18 | /// Creates a new mock `WALStorage` 19 | pub(super) fn new() -> WALStorage { 20 | Self { 21 | entries: VecDeque::new(), 22 | } 23 | } 24 | } 25 | 26 | impl WALStorageOps for WALStorage 27 | where 28 | C: Clone, 29 | { 30 | fn recover(&mut self) -> io::Result>> { 31 | Ok(self.entries.clone().into_iter().collect()) 32 | } 33 | 34 | fn send_sync(&mut self, item: Vec>) -> io::Result<()> { 35 | for frame in item { 36 | if let DataFrame::Entry(entry) = frame { 37 | self.entries.push_back(entry.clone()); 38 | } 39 | } 40 | 41 | Ok(()) 42 | } 43 | 44 | fn truncate_head(&mut self, compact_index: LogIndex) -> io::Result<()> { 45 | while self 46 | .entries 47 | .front() 48 | .is_some_and(|e| e.index <= compact_index) 49 | { 50 | let _ignore = self.entries.pop_front(); 51 | } 52 | Ok(()) 53 | } 54 | 55 | fn truncate_tail(&mut self, max_index: LogIndex) -> io::Result<()> { 56 | while self.entries.back().is_some_and(|e| e.index > max_index) { 57 | let _ignore = self.entries.pop_back(); 58 | } 59 | Ok(()) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /crates/curp/src/snapshot.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | 3 | use engine::Snapshot as EngineSnapshot; 4 | 5 | /// Snapshot 6 | #[derive(Debug)] 7 | pub(crate) struct Snapshot { 8 | /// Snapshot metadata 9 | pub(crate) meta: SnapshotMeta, 10 | /// Snapshot 11 | inner: EngineSnapshot, 12 | } 13 | 14 | impl Snapshot { 15 | /// Create a new snapshot 16 | pub(crate) fn new(meta: SnapshotMeta, inner: EngineSnapshot) -> Self { 17 | Self { meta, inner } 18 | } 19 | 20 | /// Into inner snapshot 21 | pub(crate) fn into_inner(self) -> EngineSnapshot { 22 | self.inner 23 | } 24 | 25 | /// Get the inner snapshot ref 26 | #[cfg(feature = "client-metrics")] 27 | pub(crate) fn inner(&self) -> &EngineSnapshot { 28 | &self.inner 29 | } 30 | } 31 | 32 | /// Metadata for snapshot 33 | #[derive(Debug, Clone, Copy)] 34 | pub(crate) struct SnapshotMeta { 35 | /// Last included index 36 | pub(crate) last_included_index: u64, 37 | /// Last included term 38 | pub(crate) last_included_term: u64, 39 | } 40 | -------------------------------------------------------------------------------- /crates/curp/tests/it/common/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code, unused)] 2 | 3 | pub(crate) mod curp_group; 4 | -------------------------------------------------------------------------------- /crates/curp/tests/it/main.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | mod server; 4 | -------------------------------------------------------------------------------- /crates/curp/tla+/README.md: -------------------------------------------------------------------------------- 1 | # TLA+ specification of the CURP consensus algorithm 2 | 3 | This directory contains the TLA+ specification of the modified CURP consensus algorithm used in Xline. 4 | 5 | The original CURP Replication Protocol is described in the paper [Exploiting Commutativity For 6 | Practical Fast Replication](https://www.usenix.org/system/files/nsdi19-park.pdf). We extended CURP into a consensus protocol front-end and paired it with the [Raft Consensus Algorithm](https://raft.github.io/). 7 | 8 | Since Raft is already proven to be correct, we only need to prove the correctness of the CURP consensus protocol we implemented. The details of syncing commands and electing leaders are omitted in the specification. 9 | 10 | The specification is written in the TLA+ language and can be verified using the TLC model checker. 11 | -------------------------------------------------------------------------------- /crates/curp/tla+/curp.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xline-kv/Xline/71479a4fabdd12fe67eec0bee95e652976937541/crates/curp/tla+/curp.pdf -------------------------------------------------------------------------------- /crates/engine/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["DatenLord "] 3 | description = "Xline Persistent Layer " 4 | categories = ["Storage Engine"] 5 | keywords = ["storage engine"] 6 | license = "Apache-2.0" 7 | repository = "https://github.com/xline-kv/Xline/tree/master/storage" 8 | readme = "../README.md" 9 | name = "engine" 10 | version = "0.1.0" 11 | edition = "2021" 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [dependencies] 15 | async-trait = "0.1.81" 16 | bincode = "1.3.3" 17 | bytes = "1.7.1" 18 | clippy-utilities = "0.2.0" 19 | opentelemetry = { version = "0.24.0", features = ["metrics"] } 20 | parking_lot = "0.12.3" 21 | rocksdb = { version = "0.22.0", features = ["multi-threaded-cf"] } 22 | serde = { version = "1.0.204", features = ["derive"] } 23 | thiserror = "1.0.61" 24 | tokio = { version = "0.2.25", package = "madsim-tokio", features = [ 25 | "fs", 26 | "macros", 27 | "rt-multi-thread", 28 | "io-util", 29 | ] } 30 | tokio-util = { version = "0.7.11", features = ["io"] } 31 | tracing = "0.1.40" 32 | utils = { path = "../utils" } 33 | uuid = { version = "1", features = ["v4"] } 34 | workspace-hack = { version = "0.1", path = "../../workspace-hack" } 35 | 36 | [dev-dependencies] 37 | tempfile = "3" 38 | test-macros = { path = "../test-macros" } 39 | -------------------------------------------------------------------------------- /crates/engine/src/api/engine_api.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use crate::{api::snapshot_api::SnapshotApi, error::EngineError, TransactionApi}; 4 | 5 | /// The `StorageEngine` trait 6 | #[async_trait::async_trait] 7 | pub trait StorageEngine: Send + Sync + 'static + std::fmt::Debug { 8 | /// The snapshot type 9 | type Snapshot: SnapshotApi; 10 | /// The transaction type 11 | type Transaction<'db>: TransactionApi; 12 | 13 | /// Creates a transaction 14 | fn transaction(&self) -> Self::Transaction<'_>; 15 | 16 | /// Get all the values of the given table 17 | /// 18 | /// # Errors 19 | /// 20 | /// Return `EngineError::TableNotFound` if the given table does not exist 21 | /// Return `EngineError` if met some errors 22 | #[allow(clippy::type_complexity)] // it's clear that (Vec, Vec) is a key-value pair 23 | fn get_all(&self, table: &str) -> Result, Vec)>, EngineError>; 24 | 25 | /// Get a snapshot of the current state of the database 26 | /// 27 | /// # Errors 28 | /// 29 | /// Return `EngineError` if met some errors when creating the snapshot 30 | fn get_snapshot( 31 | &self, 32 | path: impl AsRef, 33 | tables: &[&'static str], 34 | ) -> Result; 35 | 36 | /// Apply a snapshot to the database 37 | /// 38 | /// # Errors 39 | /// 40 | /// Return `EngineError` if met some errors when applying the snapshot 41 | async fn apply_snapshot( 42 | &self, 43 | snapshot: Self::Snapshot, 44 | tables: &[&'static str], 45 | ) -> Result<(), EngineError>; 46 | 47 | /// Get the cached size of the engine (Measured in bytes) 48 | fn estimated_file_size(&self) -> u64; 49 | 50 | /// Get the file size of the engine (Measured in bytes) 51 | /// 52 | /// # Errors 53 | /// 54 | /// Return `EngineError` if met some errors when get file size 55 | fn file_size(&self) -> Result; 56 | } 57 | -------------------------------------------------------------------------------- /crates/engine/src/api/mod.rs: -------------------------------------------------------------------------------- 1 | /// engine trait definition 2 | pub(crate) mod engine_api; 3 | /// Storage operations trait definition; 4 | pub(crate) mod operation; 5 | /// Snapshot trait definition 6 | pub(crate) mod snapshot_api; 7 | /// Transaction trait definition; 8 | pub(crate) mod transaction_api; 9 | -------------------------------------------------------------------------------- /crates/engine/src/api/snapshot_api.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, io}; 2 | 3 | use bytes::{Bytes, BytesMut}; 4 | 5 | use crate::Snapshot; 6 | 7 | /// This trait is a abstraction of the snapshot, We can Read/Write the snapshot like a file. 8 | #[async_trait::async_trait] 9 | pub trait SnapshotApi: Send + Sync + std::fmt::Debug { 10 | /// Get the size of the snapshot 11 | fn size(&self) -> u64; 12 | 13 | /// Rewind the snapshot to the beginning 14 | /// 15 | /// # Errors 16 | /// Return `IO::Error` when `rewind` went wrong 17 | fn rewind(&mut self) -> io::Result<()>; 18 | 19 | /// Pull some bytes of the snapshot to the given uninitialized buffer 20 | async fn read_buf(&mut self, buf: &mut BytesMut) -> io::Result<()>; 21 | 22 | /// Read the exact capacity of the given uninitialized buffer 23 | #[inline] 24 | async fn read_buf_exact(&mut self, buf: &mut BytesMut) -> io::Result<()> { 25 | while buf.len() < buf.capacity() { 26 | let prev_len = buf.len(); 27 | match self.read_buf(buf).await { 28 | Ok(()) => {} 29 | Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue, 30 | Err(e) => return Err(e), 31 | } 32 | 33 | if buf.len() == prev_len { 34 | return Err(io::Error::new( 35 | io::ErrorKind::UnexpectedEof, 36 | "failed to fill whole buffer", 37 | )); 38 | } 39 | } 40 | 41 | Ok(()) 42 | } 43 | 44 | /// Write the given buffer to the snapshot 45 | async fn write_all(&mut self, buf: Bytes) -> io::Result<()>; 46 | 47 | /// Clean files of current snapshot 48 | async fn clean(&mut self) -> io::Result<()>; 49 | } 50 | 51 | /// The snapshot allocation is handled by the upper-level application 52 | #[async_trait::async_trait] 53 | pub trait SnapshotAllocator: Send + Sync { 54 | /// Allocate a new snapshot 55 | async fn allocate_new_snapshot(&self) -> Result>; 56 | } 57 | -------------------------------------------------------------------------------- /crates/engine/src/api/transaction_api.rs: -------------------------------------------------------------------------------- 1 | use crate::EngineError; 2 | 3 | /// Api for database transactions 4 | pub trait TransactionApi { 5 | /// Commits the changes 6 | /// 7 | /// # Errors 8 | /// 9 | /// if error occurs in storage, return `Err(error)` 10 | fn commit(self) -> Result<(), EngineError>; 11 | 12 | /// Rollbacks the changes 13 | /// 14 | /// # Errors 15 | /// 16 | /// if error occurs in storage, return `Err(error)` 17 | fn rollback(&self) -> Result<(), EngineError>; 18 | } 19 | -------------------------------------------------------------------------------- /crates/engine/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | /// The `EngineError` 4 | #[allow(clippy::module_name_repetitions)] 5 | #[non_exhaustive] 6 | #[derive(Error, Debug)] 7 | pub enum EngineError { 8 | /// Met I/O Error during persisting data 9 | #[error("I/O Error: {0}")] 10 | IoError(#[from] std::io::Error), 11 | /// Table Not Found 12 | #[error("Table {0} Not Found")] 13 | TableNotFound(String), 14 | /// DB File Corrupted 15 | #[error("DB File {0} Corrupted")] 16 | Corruption(String), 17 | /// Invalid Argument Error 18 | #[error("Invalid Argument: {0}")] 19 | InvalidArgument(String), 20 | /// The Underlying Database Error 21 | #[error("The Underlying Database Error: {0}")] 22 | UnderlyingError(String), 23 | /// The Snapshot is invalid 24 | #[error("The Snapshot is invalid")] 25 | InvalidSnapshot, 26 | } 27 | -------------------------------------------------------------------------------- /crates/engine/src/snapshot_allocator.rs: -------------------------------------------------------------------------------- 1 | use std::{env::temp_dir, error::Error}; 2 | 3 | use crate::{api::snapshot_api::SnapshotAllocator, EngineType, Snapshot}; 4 | 5 | /// Rocks snapshot allocator 6 | #[derive(Debug, Copy, Clone, Default)] 7 | #[non_exhaustive] 8 | #[allow(clippy::module_name_repetitions)] 9 | pub struct RocksSnapshotAllocator; 10 | 11 | #[async_trait::async_trait] 12 | impl SnapshotAllocator for RocksSnapshotAllocator { 13 | #[inline] 14 | async fn allocate_new_snapshot(&self) -> Result> { 15 | let tmp_path = temp_dir().join(format!("snapshot-{}", uuid::Uuid::new_v4())); 16 | Ok(Snapshot::new_for_receiving(EngineType::Rocks(tmp_path))?) 17 | } 18 | } 19 | 20 | /// Memory snapshot allocator 21 | #[derive(Debug, Copy, Clone, Default)] 22 | #[non_exhaustive] 23 | #[allow(clippy::module_name_repetitions)] 24 | pub struct MemorySnapshotAllocator; 25 | 26 | #[async_trait::async_trait] 27 | impl SnapshotAllocator for MemorySnapshotAllocator { 28 | #[inline] 29 | async fn allocate_new_snapshot(&self) -> Result> { 30 | Ok(Snapshot::new_for_receiving(EngineType::Memory)?) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /crates/simulation/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "simulation" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["DatenLord "] 6 | repository = "https://github.com/xline-kv/Xline/tree/master/tests/simulation" 7 | description = "Madsim simulation for Xline" 8 | license = "Apache-2.0" 9 | readme = "README.md" 10 | categories = ["Test"] 11 | keywords = ["Test", "Deterministic Simulation"] 12 | 13 | [dependencies] 14 | async-trait = "0.1.81" 15 | bincode = "1.3.3" 16 | curp = { path = "../curp" } 17 | curp-test-utils = { path = "../curp-test-utils" } 18 | engine = { path = "../engine" } 19 | futures = "0.3.29" 20 | itertools = "0.13" 21 | madsim = "0.2.27" 22 | parking_lot = "0.12.3" 23 | prost = "0.13" 24 | tempfile = "3" 25 | tokio = { version = "0.2.25", package = "madsim-tokio", features = [ 26 | "rt", 27 | "rt-multi-thread", 28 | "fs", 29 | "sync", 30 | "macros", 31 | "time", 32 | "signal", 33 | ] } 34 | tonic = { version = "0.5.0", package = "madsim-tonic" } 35 | tracing = { version = "0.1.34", features = ["std", "log", "attributes"] } 36 | utils = { path = "../utils", version = "0.1.0", features = ["parking_lot"] } 37 | workspace-hack = { version = "0.1", path = "../../workspace-hack" } 38 | xline = { path = "../xline" } 39 | xline-client = { path = "../xline-client" } 40 | xlineapi = { path = "../xlineapi" } 41 | 42 | [build-dependencies] 43 | tonic-build = { version = "0.5.0", package = "madsim-tonic-build" } 44 | -------------------------------------------------------------------------------- /crates/simulation/README.md: -------------------------------------------------------------------------------- 1 | # Xline simulation 2 | 3 | This crate provides madsim simulation for Xline. 4 | -------------------------------------------------------------------------------- /crates/simulation/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg(madsim)] 2 | 3 | pub mod curp_group; 4 | pub mod xline_group; 5 | -------------------------------------------------------------------------------- /crates/simulation/tests/it/curp/mod.rs: -------------------------------------------------------------------------------- 1 | mod server_election; 2 | mod server_recovery; 3 | -------------------------------------------------------------------------------- /crates/simulation/tests/it/main.rs: -------------------------------------------------------------------------------- 1 | #![cfg(madsim)] 2 | 3 | mod curp; 4 | mod xline; 5 | -------------------------------------------------------------------------------- /crates/simulation/tests/it/xline.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use curp_test_utils::init_logger; 4 | use madsim::time::sleep; 5 | use simulation::xline_group::{SimEtcdClient, XlineGroup}; 6 | use xline_client::types::watch::WatchOptions; 7 | 8 | // TODO: Add more tests if needed 9 | 10 | #[madsim::test] 11 | async fn basic_put() { 12 | init_logger(); 13 | let group = XlineGroup::new(3).await; 14 | let client = group.client().await; 15 | let res = client.put("key", "value", None).await; 16 | assert!(res.is_ok()); 17 | } 18 | 19 | #[madsim::test] 20 | async fn watch_compacted_revision_should_receive_canceled_response() { 21 | init_logger(); 22 | let group = XlineGroup::new(3).await; 23 | let watch_addr = group.get_node("S2").client_url.clone(); 24 | 25 | let client = SimEtcdClient::new(watch_addr, group.client_handle.clone()).await; 26 | 27 | for i in 1..=6 { 28 | let result = client.put("key", format!("value{}", i), None).await; 29 | assert!(result.is_ok()); 30 | } 31 | 32 | let result = client.compact(5, true).await; 33 | assert!(result.is_ok()); 34 | 35 | let (_, mut watch_stream) = client 36 | .watch("key", Some(WatchOptions::default().with_start_revision(4))) 37 | .await 38 | .unwrap(); 39 | let r = watch_stream.message().await.unwrap().unwrap(); 40 | assert!(r.canceled); 41 | } 42 | 43 | #[madsim::test] 44 | async fn xline_members_restore() { 45 | init_logger(); 46 | let mut group = XlineGroup::new(3).await; 47 | let node = group.get_node("S1"); 48 | let addr = node.client_url.clone(); 49 | let mut client = SimEtcdClient::new(addr, group.client_handle.clone()).await; 50 | 51 | let res = client 52 | .member_add(["http://192.168.1.4:12345"], true) 53 | .await 54 | .unwrap(); 55 | assert_eq!(res.members.len(), 4); 56 | let members = client.member_list(false).await.unwrap(); 57 | assert_eq!(members.members.len(), 4); 58 | group.crash("S1").await; 59 | sleep(Duration::from_secs(10)).await; 60 | 61 | group.restart("S1").await; 62 | sleep(Duration::from_secs(10)).await; 63 | let members = client.member_list(false).await.unwrap(); 64 | assert_eq!(members.members.len(), 4); 65 | } 66 | -------------------------------------------------------------------------------- /crates/test-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test-macros" 3 | authors = ["DatenLord "] 4 | categories = ["Macros"] 5 | description = "Macros for test configurations" 6 | keywords = ["Test", "Macros"] 7 | license = "Apache-2.0" 8 | readme = "README.md" 9 | repository = "https://github.com/xline-kv/Xline/tree/master/test-macros" 10 | version = "0.1.0" 11 | edition = "2021" 12 | 13 | [lib] 14 | proc-macro = true 15 | 16 | [dependencies] 17 | quote = "1.0" 18 | syn = { version = "2.0", features = ["full"] } 19 | tokio = { version = "1", features = ["rt-multi-thread", "macros"] } 20 | workspace-hack = { version = "0.1", path = "../../workspace-hack" } 21 | 22 | [dev-dependencies] 23 | assert_cmd = "2.0.15" 24 | -------------------------------------------------------------------------------- /crates/test-macros/README.md: -------------------------------------------------------------------------------- 1 | # Test Macros 2 | 3 | This crate provide macros for test configurations. 4 | -------------------------------------------------------------------------------- /crates/test-macros/src/bin/no_abort.rs: -------------------------------------------------------------------------------- 1 | //! This binary is only used for this crate's tests 2 | #[tokio::main] 3 | async fn main() { 4 | _ = tokio::spawn(async { 5 | panic!("here is a panic"); 6 | }) 7 | .await; 8 | } 9 | -------------------------------------------------------------------------------- /crates/test-macros/src/bin/with_abort.rs: -------------------------------------------------------------------------------- 1 | //! This binary is only used for this crate's tests 2 | #[tokio::main] 3 | #[test_macros::abort_on_panic] 4 | async fn main() { 5 | _ = tokio::spawn(async { 6 | panic!("here is a panic"); 7 | }) 8 | .await; 9 | } 10 | -------------------------------------------------------------------------------- /crates/test-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::quote; 3 | use syn::{parse_macro_input, ItemFn, Stmt}; 4 | 5 | #[proc_macro_attribute] 6 | pub fn abort_on_panic(_attr: TokenStream, item: TokenStream) -> TokenStream { 7 | let mut input: syn::ItemFn = parse_macro_input!(item as ItemFn); 8 | 9 | let panic_hook: Stmt = syn::parse_quote! { 10 | std::panic::set_hook(Box::new(|info| { 11 | let stacktrace = std::backtrace::Backtrace::force_capture(); 12 | println!("test panic! \n@info:\n{}\n@stackTrace:\n{}", info, stacktrace); 13 | std::process::abort(); 14 | })); 15 | }; 16 | 17 | input.block.stmts.insert(0, panic_hook); 18 | 19 | TokenStream::from(quote! { 20 | #input 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /crates/test-macros/tests/test_abort.rs: -------------------------------------------------------------------------------- 1 | use assert_cmd::Command; 2 | 3 | #[test] 4 | fn should_fail() { 5 | let mut cmd = Command::cargo_bin("with_abort").unwrap(); 6 | cmd.assert().failure(); 7 | } 8 | 9 | #[test] 10 | fn should_success() { 11 | let mut cmd = Command::cargo_bin("no_abort").unwrap(); 12 | cmd.assert().success(); 13 | } 14 | -------------------------------------------------------------------------------- /crates/utils/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "utils" 3 | version = "0.1.0" 4 | authors = ["DatenLord Developers "] 5 | edition = "2021" 6 | description = "Utilities functions for Xline" 7 | repository = "https://github.com/xline-kv/Xline/tree/master/utils" 8 | readme = "README.md" 9 | license = "MIT" 10 | keywords = ["utilities"] 11 | categories = ["utilities"] 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [features] 15 | full = ["std", "tokio", "parking_lot"] 16 | std = [] 17 | tokio = ["dep:async-trait"] 18 | parking_lot = ["dep:parking_lot"] 19 | 20 | [dependencies] 21 | async-trait = { version = "0.1.81", optional = true } 22 | clippy-utilities = "0.2.0" 23 | dashmap = "6.1.0" 24 | derive_builder = "0.20.0" 25 | event-listener = "5.3.1" 26 | futures = "0.3.30" 27 | getset = "0.1" 28 | interval_map = { version = "0.1", package = "rb-interval-map" } 29 | opentelemetry = { version = "0.24.0", features = ["trace"] } 30 | opentelemetry_sdk = { version = "0.24.1", features = ["trace"] } 31 | parking_lot = { version = "0.12.3", optional = true } 32 | pbkdf2 = { version = "0.12.2", features = ["simple"] } 33 | regex = "1.10.5" 34 | serde = { version = "1.0.204", features = ["derive"] } 35 | thiserror = "1.0.61" 36 | tokio = { version = "0.2.25", package = "madsim-tokio", features = [ 37 | "sync", 38 | "macros", 39 | "rt-multi-thread", 40 | ] } 41 | toml = "0.8.14" 42 | tonic = { version = "0.5.0", package = "madsim-tonic" } 43 | tracing = "0.1.37" 44 | tracing-appender = "0.2" 45 | tracing-opentelemetry = "0.25.0" 46 | workspace-hack = { version = "0.1", path = "../../workspace-hack" } 47 | 48 | [dev-dependencies] 49 | opentelemetry = { version = "0.24.0", features = ["trace"] } 50 | opentelemetry-jaeger-propagator = "0.3.0" 51 | opentelemetry-otlp = { version = "0.17.0", features = [ 52 | "metrics", 53 | "http-proto", 54 | "reqwest-client", 55 | ] } 56 | test-macros = { path = "../test-macros" } 57 | tracing-subscriber = "0.3.18" 58 | -------------------------------------------------------------------------------- /crates/utils/README.md: -------------------------------------------------------------------------------- 1 | # utils 2 | 3 | This crate provides a set of utilities for locks. 4 | 5 | ## Usage 6 | 7 | Add this to your Cargo.toml : 8 | 9 | ```Toml 10 | [dependencies] 11 | utils = { path = "../utils", features = ["std"] } 12 | ``` 13 | 14 | Write your code like this: 15 | 16 | ```Rust 17 | use utils::std_lock::{MutexMap, RwLockMap}; 18 | use std::sync::{Mutex, RwLock}; 19 | 20 | let mu = Mutex::new(1); 21 | mu.map_lock(|mut g| { 22 | *g = 3; 23 | })?; 24 | let val = mu.map_lock(|g| *g)?; 25 | assert_eq!(val, 3); 26 | 27 | let rwlock = RwLock::new(1); 28 | rwlock.map_write(|mut g| { 29 | *g = 3; 30 | })?; 31 | let val = rwlock.map_read(|g| *g)?; 32 | assert_eq!(val, 3); 33 | ``` 34 | 35 | ## Features 36 | 37 | - `std`: utils for `std::sync::Mutex` and `std::sync::RwLock` 38 | - `parking_lot`: utils for` parking_lot::Mutex` and `parking_lot::RwLock` 39 | - `tokio`: utils for `tokio::sync::Mutex` and `tokio::sync::RwLock` 40 | -------------------------------------------------------------------------------- /crates/utils/src/barrier.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, hash::Hash}; 2 | 3 | use event_listener::Event; 4 | use futures::{stream::FuturesOrdered, Future, FutureExt, StreamExt}; 5 | use parking_lot::Mutex; 6 | 7 | /// Barrier for id 8 | #[allow(clippy::module_name_repetitions)] 9 | #[derive(Debug, Default)] 10 | pub struct IdBarrier { 11 | /// Barriers of id 12 | barriers: Mutex>, 13 | } 14 | 15 | impl IdBarrier { 16 | /// Create a new id barrier 17 | #[inline] 18 | #[must_use] 19 | pub fn new() -> Self { 20 | Self { 21 | barriers: Mutex::new(HashMap::new()), 22 | } 23 | } 24 | } 25 | 26 | impl IdBarrier 27 | where 28 | Id: Eq + Hash, 29 | { 30 | /// Wait for the id until it is triggered. 31 | #[inline] 32 | pub async fn wait(&self, id: Id) { 33 | let listener = self.barriers.lock().entry(id).or_default().listen(); 34 | listener.await; 35 | } 36 | 37 | /// Wait for a collection of ids. 38 | #[inline] 39 | pub fn wait_all(&self, ids: Vec) -> impl Future + Send { 40 | let mut barriers_l = self.barriers.lock(); 41 | let listeners: FuturesOrdered<_> = ids 42 | .into_iter() 43 | .map(|id| barriers_l.entry(id).or_default().listen()) 44 | .collect(); 45 | listeners.collect::>().map(|_| ()) 46 | } 47 | 48 | /// Trigger the barrier of the given inflight id. 49 | #[inline] 50 | pub fn trigger(&self, id: &Id) { 51 | if let Some(event) = self.barriers.lock().remove(id) { 52 | let _ignore = event.notify(usize::MAX); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /crates/utils/src/metrics.rs: -------------------------------------------------------------------------------- 1 | /// Define some metrics 2 | #[macro_export] 3 | #[allow(clippy::module_name_repetitions)] // exported macros 4 | macro_rules! define_metrics { 5 | ($com:expr, $($field:ident : $ki:ty = $init:expr),*) => { 6 | 7 | pub(crate) struct Metrics { 8 | $(pub(crate) $field: $ki),*, 9 | } 10 | 11 | impl Metrics { 12 | fn new() -> Self { 13 | Self { 14 | $($field: $init),* 15 | } 16 | } 17 | } 18 | 19 | /// Global metrics for curp server 20 | static METRICS: std::sync::OnceLock = std::sync::OnceLock::new(); 21 | 22 | /// Meter for metrics 23 | static METRICS_METER: std::sync::OnceLock = std::sync::OnceLock::new(); 24 | 25 | /// Get the curp metrics 26 | pub(crate) fn get() -> &'static Metrics { 27 | METRICS.get_or_init(|| Metrics::new()) 28 | } 29 | 30 | /// Get the curp metrics meter 31 | fn meter() -> &'static opentelemetry::metrics::Meter { 32 | METRICS_METER.get_or_init(|| { 33 | opentelemetry::global::meter_with_version( 34 | env!("CARGO_PKG_NAME"), 35 | Some(env!("CARGO_PKG_VERSION")), 36 | Some(env!("CARGO_PKG_REPOSITORY")), 37 | Some(vec![opentelemetry::KeyValue::new("component", $com)]), 38 | ) 39 | }) 40 | } 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /crates/utils/src/parking_lot_lock.rs: -------------------------------------------------------------------------------- 1 | use parking_lot::{Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard}; 2 | 3 | /// Apply a closure on a mutex after getting the guard 4 | pub trait MutexMap { 5 | /// Map a closure to a mutex 6 | fn map_lock(&self, f: F) -> R 7 | where 8 | F: FnOnce(MutexGuard<'_, T>) -> R; 9 | } 10 | 11 | impl MutexMap for Mutex { 12 | #[inline] 13 | fn map_lock(&self, f: F) -> R 14 | where 15 | F: FnOnce(MutexGuard<'_, T>) -> R, 16 | { 17 | let lock = self.lock(); 18 | f(lock) 19 | } 20 | } 21 | 22 | /// Apply a closure on a rwlock after getting the guard 23 | pub trait RwLockMap { 24 | /// Map a closure to a read mutex 25 | fn map_read(&self, f: READ) -> R 26 | where 27 | READ: FnOnce(RwLockReadGuard<'_, T>) -> R; 28 | 29 | /// Map a closure to a write mutex 30 | fn map_write(&self, f: WRITE) -> R 31 | where 32 | WRITE: FnOnce(RwLockWriteGuard<'_, T>) -> R; 33 | } 34 | 35 | impl RwLockMap for RwLock { 36 | #[inline] 37 | fn map_read(&self, f: READ) -> R 38 | where 39 | READ: FnOnce(RwLockReadGuard<'_, T>) -> R, 40 | { 41 | let read_guard = self.read(); 42 | f(read_guard) 43 | } 44 | 45 | #[inline] 46 | fn map_write(&self, f: WRITE) -> R 47 | where 48 | WRITE: FnOnce(RwLockWriteGuard<'_, T>) -> R, 49 | { 50 | let write_guard = self.write(); 51 | f(write_guard) 52 | } 53 | } 54 | 55 | #[cfg(test)] 56 | mod test { 57 | use super::*; 58 | 59 | #[test] 60 | fn mutex_map_works() { 61 | let mu = Mutex::new(1); 62 | mu.map_lock(|mut g| { 63 | *g = 3; 64 | }); 65 | let val = mu.map_lock(|g| *g); 66 | assert_eq!(val, 3); 67 | } 68 | 69 | #[test] 70 | fn rwlock_map_works() { 71 | let mu = RwLock::new(1); 72 | mu.map_write(|mut g| { 73 | *g = 3; 74 | }); 75 | let val = mu.map_read(|g| *g); 76 | assert_eq!(val, 3); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /crates/utils/src/table_names.rs: -------------------------------------------------------------------------------- 1 | /// Meta table name 2 | pub const META_TABLE: &str = "meta"; 3 | /// Kv table name 4 | pub const KV_TABLE: &str = "kv"; 5 | /// Lease table name 6 | pub const LEASE_TABLE: &str = "lease"; 7 | /// Auth table name 8 | pub const AUTH_TABLE: &str = "auth"; 9 | /// User table name 10 | pub const USER_TABLE: &str = "user"; 11 | /// Role table name 12 | pub const ROLE_TABLE: &str = "role"; 13 | /// Alarm table name 14 | pub const ALARM_TABLE: &str = "alarm"; 15 | 16 | /// Xline Server Storage Table 17 | pub const XLINE_TABLES: [&str; 7] = [ 18 | META_TABLE, 19 | KV_TABLE, 20 | LEASE_TABLE, 21 | AUTH_TABLE, 22 | USER_TABLE, 23 | ROLE_TABLE, 24 | ALARM_TABLE, 25 | ]; 26 | -------------------------------------------------------------------------------- /crates/utils/src/tokio_lock.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use tokio::sync::{Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard}; 3 | 4 | /// Apply a closure on a mutex after getting the guard 5 | #[async_trait] 6 | pub trait MutexMap { 7 | /// Map a closure to a mutex in an async context 8 | async fn map_lock(&self, f: F) -> R 9 | where 10 | F: FnOnce(MutexGuard<'_, T>) -> R + Send; 11 | } 12 | 13 | #[async_trait] 14 | impl MutexMap for Mutex 15 | where 16 | T: Send, 17 | { 18 | #[inline] 19 | async fn map_lock(&self, f: F) -> R 20 | where 21 | F: FnOnce(MutexGuard<'_, T>) -> R + Send, 22 | { 23 | let lock = self.lock().await; 24 | f(lock) 25 | } 26 | } 27 | 28 | /// Apply a closure on a rwlock after getting the guard 29 | #[async_trait] 30 | pub trait RwLockMap { 31 | /// Map a closure to a read mutex 32 | async fn map_read(&self, f: READ) -> R 33 | where 34 | READ: FnOnce(RwLockReadGuard<'_, T>) -> R + Send; 35 | 36 | /// Map a closure to a write mutex 37 | async fn map_write(&self, f: WRITE) -> R 38 | where 39 | WRITE: FnOnce(RwLockWriteGuard<'_, T>) -> R + Send; 40 | } 41 | 42 | #[async_trait] 43 | impl RwLockMap for RwLock 44 | where 45 | T: Send + Sync, 46 | { 47 | #[inline] 48 | async fn map_read(&self, f: READ) -> R 49 | where 50 | READ: FnOnce(RwLockReadGuard<'_, T>) -> R + Send, 51 | { 52 | let read_guard = self.read().await; 53 | f(read_guard) 54 | } 55 | 56 | #[inline] 57 | async fn map_write(&self, f: WRITE) -> R 58 | where 59 | WRITE: FnOnce(RwLockWriteGuard<'_, T>) -> R + Send, 60 | { 61 | let write_guard = self.write().await; 62 | f(write_guard) 63 | } 64 | } 65 | 66 | #[cfg(test)] 67 | mod test { 68 | use test_macros::abort_on_panic; 69 | 70 | use super::*; 71 | 72 | #[tokio::test] 73 | #[abort_on_panic] 74 | async fn mutex_map_works() { 75 | let mu = Mutex::new(1); 76 | mu.map_lock(|mut g| { 77 | *g = 3; 78 | }) 79 | .await; 80 | let val = mu.map_lock(|g| *g).await; 81 | assert_eq!(val, 3); 82 | } 83 | 84 | #[tokio::test] 85 | #[abort_on_panic] 86 | async fn rwlock_map_works() { 87 | let mu = RwLock::new(1); 88 | mu.map_write(|mut g| { 89 | *g = 3; 90 | }) 91 | .await; 92 | let val = mu.map_read(|g| *g).await; 93 | assert_eq!(val, 3); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /crates/xline-client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xline-client" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["DatenLord "] 6 | description = "Client for Xline" 7 | license = "Apache-2.0" 8 | readme = "README.md" 9 | repository = "https://github.com/xline-kv/Xline/tree/master/xline-client" 10 | categories = ["Client"] 11 | keywords = ["Client", "Xline", "RPC"] 12 | 13 | [dependencies] 14 | anyhow = "1.0.90" 15 | async-dropper = { version = "0.3.1", features = ["tokio", "simple"] } 16 | async-trait = "0.1.81" 17 | clippy-utilities = "0.2.0" 18 | curp = { path = "../curp" } 19 | futures = "0.3.25" 20 | getrandom = "0.2" 21 | http = "1.0" 22 | prost = "0.13" 23 | thiserror = "1.0.61" 24 | tokio = { version = "0.2.25", package = "madsim-tokio", features = ["sync"] } 25 | tonic = { version = "0.5.0", package = "madsim-tonic" } 26 | tower = { version = "0.4", features = ["discover"] } 27 | utils = { path = "../utils", features = ["parking_lot"] } 28 | workspace-hack = { version = "0.1", path = "../../workspace-hack" } 29 | xlineapi = { path = "../xlineapi" } 30 | 31 | [dev-dependencies] 32 | rand = "0.8.5" 33 | test-macros = { path = "../test-macros" } 34 | xline-test-utils = { path = "../xline-test-utils" } 35 | 36 | [build-dependencies] 37 | tonic-build = { version = "0.5.0", package = "madsim-tonic-build" } 38 | -------------------------------------------------------------------------------- /crates/xline-client/examples/auth.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use xline_client::{Client, ClientOptions}; 3 | 4 | #[tokio::main] 5 | async fn main() -> Result<()> { 6 | // the name and address of all curp members 7 | let curp_members = ["10.0.0.1:2379", "10.0.0.2:2379", "10.0.0.3:2379"]; 8 | 9 | let client = Client::connect(curp_members, ClientOptions::default()) 10 | .await? 11 | .auth_client(); 12 | 13 | // enable auth 14 | let _resp = client.auth_enable().await?; 15 | 16 | // connect using the root user 17 | let options = ClientOptions::default().with_user("root", "rootpwd"); 18 | let client = Client::connect(curp_members, options).await?.auth_client(); 19 | 20 | // disable auth 21 | let _resp = client.auth_disable(); 22 | 23 | // get auth status 24 | let resp = client.auth_status().await?; 25 | println!("auth status:"); 26 | println!( 27 | "enabled: {}, revision: {}", 28 | resp.enabled, resp.auth_revision 29 | ); 30 | 31 | Ok(()) 32 | } 33 | -------------------------------------------------------------------------------- /crates/xline-client/examples/auth_role.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use xline_client::{types::auth::PermissionType, Client, ClientOptions}; 3 | 4 | #[tokio::main] 5 | async fn main() -> Result<()> { 6 | // the name and address of all curp members 7 | let curp_members = ["10.0.0.1:2379", "10.0.0.2:2379", "10.0.0.3:2379"]; 8 | 9 | let client = Client::connect(curp_members, ClientOptions::default()) 10 | .await? 11 | .auth_client(); 12 | 13 | // add roles 14 | client.role_add("role1").await?; 15 | client.role_add("role2").await?; 16 | 17 | // grant permissions to roles 18 | client 19 | .role_grant_permission("role1", PermissionType::Read, "key1", None) 20 | .await?; 21 | client 22 | .role_grant_permission("role2", PermissionType::Readwrite, "key2", None) 23 | .await?; 24 | 25 | // list all roles and their permissions 26 | let resp = client.role_list().await?; 27 | println!("roles:"); 28 | for role in resp.roles { 29 | println!("{}", role); 30 | let get_resp = client.role_get(role).await?; 31 | println!("permmisions:"); 32 | for perm in get_resp.perm { 33 | println!("{} {}", perm.perm_type, String::from_utf8_lossy(&perm.key)); 34 | } 35 | } 36 | 37 | // revoke permissions from roles 38 | client.role_revoke_permission("role1", "key1", None).await?; 39 | client.role_revoke_permission("role2", "key2", None).await?; 40 | 41 | // delete roles 42 | client.role_delete("role1").await?; 43 | client.role_delete("role2").await?; 44 | 45 | Ok(()) 46 | } 47 | -------------------------------------------------------------------------------- /crates/xline-client/examples/auth_user.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use xline_client::{Client, ClientOptions}; 3 | 4 | #[tokio::main] 5 | async fn main() -> Result<()> { 6 | // the name and address of all curp members 7 | let curp_members = ["10.0.0.1:2379", "10.0.0.2:2379", "10.0.0.3:2379"]; 8 | 9 | let client = Client::connect(curp_members, ClientOptions::default()) 10 | .await? 11 | .auth_client(); 12 | 13 | // add user 14 | client.user_add("user1", "", true).await?; 15 | client.user_add("user2", "", true).await?; 16 | 17 | // change user1's password to "123" 18 | client.user_change_password("user1", "123").await?; 19 | 20 | // grant roles 21 | client.user_grant_role("user1", "role1").await?; 22 | client.user_grant_role("user2", "role2").await?; 23 | 24 | // list all users and their roles 25 | let resp = client.user_list().await?; 26 | for user in resp.users { 27 | println!("user: {}", user); 28 | let get_resp = client.user_get(user).await?; 29 | println!("roles:"); 30 | for role in get_resp.roles.iter() { 31 | print!("{} ", role); 32 | } 33 | println!(); 34 | } 35 | 36 | // revoke role from user 37 | client.user_revoke_role("user1", "role1").await?; 38 | client.user_revoke_role("user2", "role2").await?; 39 | 40 | // delete users 41 | client.user_delete("user1").await?; 42 | client.user_delete("user2").await?; 43 | 44 | Ok(()) 45 | } 46 | -------------------------------------------------------------------------------- /crates/xline-client/examples/cluster.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use xline_client::{Client, ClientOptions}; 3 | 4 | #[tokio::main] 5 | async fn main() -> Result<()> { 6 | // the name and address of all curp members 7 | let curp_members = ["10.0.0.1:2379", "10.0.0.2:2379", "10.0.0.3:2379"]; 8 | 9 | let mut client = Client::connect(curp_members, ClientOptions::default()) 10 | .await? 11 | .cluster_client(); 12 | 13 | // send a linearizable member list request 14 | let resp = client.member_list(true).await?; 15 | println!("members: {:?}", resp.members); 16 | 17 | // whether the added member is a learner. 18 | // the learner does not participate in voting and will only catch up with the progress of the leader. 19 | let is_learner = true; 20 | 21 | // add a normal node into the cluster 22 | let resp = client.member_add(["127.0.0.1:2379"], is_learner).await?; 23 | let added_member = resp.member.unwrap(); 24 | println!("members: {:?}, added: {}", resp.members, added_member.id); 25 | 26 | if is_learner { 27 | // promote the learner to a normal node 28 | let resp = client.member_promote(added_member.id).await?; 29 | println!("members: {:?}", resp.members); 30 | } 31 | 32 | // update the peer_ur_ls of the added member if the network topology has changed. 33 | let resp = client 34 | .member_update(added_member.id, ["127.0.0.2:2379"]) 35 | .await?; 36 | println!("members: {:?}", resp.members); 37 | 38 | // remove the member from the cluster if it is no longer needed. 39 | let resp = client.member_remove(added_member.id).await?; 40 | println!("members: {:?}", resp.members); 41 | 42 | Ok(()) 43 | } 44 | -------------------------------------------------------------------------------- /crates/xline-client/examples/error_handling.rs: -------------------------------------------------------------------------------- 1 | //! An example to show how the errors are organized in `xline-client` 2 | use anyhow::Result; 3 | use xline_client::{error::XlineClientError, types::kv::PutOptions, Client, ClientOptions}; 4 | use xlineapi::execute_error::ExecuteError; 5 | 6 | #[tokio::main] 7 | async fn main() -> Result<()> { 8 | // the name and address of all curp members 9 | let curp_members = ["10.0.0.1:2379", "10.0.0.2:2379", "10.0.0.3:2379"]; 10 | 11 | let client = Client::connect(curp_members, ClientOptions::default()) 12 | .await? 13 | .kv_client(); 14 | 15 | // We try to update the key using its previous value. 16 | // It should return an error and it should be `key not found` 17 | // as we did not add it before. 18 | let resp = client 19 | .put( 20 | "key", 21 | "", 22 | Some(PutOptions::default().with_ignore_value(true)), 23 | ) 24 | .await; 25 | let err = resp.unwrap_err(); 26 | 27 | // We match the inner error returned by the Curp server. 28 | // The command should failed at execution stage. 29 | let XlineClientError::CommandError(ee) = err else { 30 | unreachable!("the propose error should be an Execute error, but it is {err:?}") 31 | }; 32 | 33 | assert!( 34 | matches!(ee, ExecuteError::KeyNotFound), 35 | "the return result should be `KeyNotFound`" 36 | ); 37 | println!("got error: {ee}"); 38 | 39 | Ok(()) 40 | } 41 | -------------------------------------------------------------------------------- /crates/xline-client/examples/kv.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use xline_client::{ 3 | types::kv::{Compare, CompareResult, DeleteRangeOptions, PutOptions, TxnOp, TxnRequest}, 4 | Client, ClientOptions, 5 | }; 6 | 7 | #[tokio::main] 8 | async fn main() -> Result<()> { 9 | // the name and address of all curp members 10 | let curp_members = ["10.0.0.1:2379", "10.0.0.2:2379", "10.0.0.3:2379"]; 11 | 12 | let client = Client::connect(curp_members, ClientOptions::default()) 13 | .await? 14 | .kv_client(); 15 | 16 | // put 17 | client.put("key1", "value1", None).await?; 18 | client.put("key2", "value2", None).await?; 19 | 20 | // range 21 | let resp = client.range("key1", None).await?; 22 | 23 | if let Some(kv) = resp.kvs.first() { 24 | println!( 25 | "got key: {}, value: {}", 26 | String::from_utf8_lossy(&kv.key), 27 | String::from_utf8_lossy(&kv.value) 28 | ); 29 | } 30 | 31 | // delete 32 | let resp = client 33 | .delete( 34 | "key1", 35 | Some(DeleteRangeOptions::default().with_prev_kv(true)), 36 | ) 37 | .await?; 38 | 39 | for kv in resp.prev_kvs { 40 | println!( 41 | "deleted key: {}, value: {}", 42 | String::from_utf8_lossy(&kv.key), 43 | String::from_utf8_lossy(&kv.value) 44 | ); 45 | } 46 | 47 | // txn 48 | let txn_req = TxnRequest::new() 49 | .when(&[Compare::value("key2", CompareResult::Equal, "value2")][..]) 50 | .and_then( 51 | &[TxnOp::put( 52 | "key2", 53 | "value3", 54 | Some(PutOptions::default().with_prev_kv(true)), 55 | )][..], 56 | ) 57 | .or_else(&[TxnOp::range("key2", None)][..]); 58 | 59 | let _resp = client.txn(txn_req).await?; 60 | let resp = client.range("key2", None).await?; 61 | // should print "value3" 62 | if let Some(kv) = resp.kvs.first() { 63 | println!( 64 | "got key: {}, value: {}", 65 | String::from_utf8_lossy(&kv.key), 66 | String::from_utf8_lossy(&kv.value) 67 | ); 68 | } 69 | 70 | // compact 71 | let rev = resp.header.unwrap().revision; 72 | let _resp = client.compact(rev, false).await?; 73 | 74 | Ok(()) 75 | } 76 | -------------------------------------------------------------------------------- /crates/xline-client/examples/lease.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use xline_client::{Client, ClientOptions}; 3 | 4 | #[tokio::main] 5 | async fn main() -> Result<()> { 6 | // the name and address of all curp members 7 | let curp_members = ["10.0.0.1:2379", "10.0.0.2:2379", "10.0.0.3:2379"]; 8 | 9 | let mut client = Client::connect(curp_members, ClientOptions::default()) 10 | .await? 11 | .lease_client(); 12 | 13 | // grant new lease 14 | let resp1 = client.grant(60, None).await?; 15 | let resp2 = client.grant(60, None).await?; 16 | let lease_id1 = resp1.id; 17 | let lease_id2 = resp2.id; 18 | println!("lease id 1: {}", lease_id1); 19 | println!("lease id 2: {}", lease_id2); 20 | 21 | // get the ttl of lease1 22 | let resp = client.time_to_live(lease_id1, false).await?; 23 | 24 | println!("remaining ttl: {}", resp.ttl); 25 | 26 | // keep alive lease2 27 | let (mut keeper, mut stream) = client.keep_alive(lease_id2).await?; 28 | 29 | if let Some(resp) = stream.message().await? { 30 | println!("new ttl: {}", resp.ttl); 31 | } 32 | 33 | // keep alive lease2 again using the keeper 34 | keeper.keep_alive()?; 35 | 36 | // list all leases 37 | for lease in client.leases().await?.leases { 38 | println!("lease: {}", lease.id); 39 | } 40 | 41 | // revoke the leases 42 | let _resp = client.revoke(lease_id1).await?; 43 | let _resp = client.revoke(lease_id2).await?; 44 | 45 | Ok(()) 46 | } 47 | -------------------------------------------------------------------------------- /crates/xline-client/examples/lock.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use xline_client::{ 3 | clients::Xutex, 4 | types::kv::{Compare, CompareResult, PutOptions, TxnOp}, 5 | Client, ClientOptions, 6 | }; 7 | 8 | #[tokio::main] 9 | async fn main() -> Result<()> { 10 | // the name and address of all curp members 11 | let curp_members = ["10.0.0.1:2379", "10.0.0.2:2379", "10.0.0.3:2379"]; 12 | 13 | let client = Client::connect(curp_members, ClientOptions::default()).await?; 14 | 15 | let lock_client = client.lock_client(); 16 | let kv_client = client.kv_client(); 17 | 18 | let mut xutex = Xutex::new(lock_client, "lock-test", None, None).await?; 19 | // when the `xutex_guard` drop, the lock will be unlocked. 20 | let xutex_guard = xutex.lock_unsafe().await?; 21 | 22 | let txn_req = xutex_guard 23 | .txn_check_locked_key() 24 | .when([Compare::value("key2", CompareResult::Equal, "value2")]) 25 | .and_then([TxnOp::put( 26 | "key2", 27 | "value3", 28 | Some(PutOptions::default().with_prev_kv(true)), 29 | )]) 30 | .or_else(&[]); 31 | 32 | let _resp = kv_client.txn(txn_req).await?; 33 | 34 | Ok(()) 35 | } 36 | -------------------------------------------------------------------------------- /crates/xline-client/examples/maintenance.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use xline_client::{Client, ClientOptions}; 3 | 4 | #[tokio::main] 5 | async fn main() -> Result<()> { 6 | // the name and address of all curp members 7 | let curp_members = ["10.0.0.1:2379", "10.0.0.2:2379", "10.0.0.3:2379"]; 8 | 9 | let mut client = Client::connect(curp_members, ClientOptions::default()) 10 | .await? 11 | .maintenance_client(); 12 | 13 | // snapshot 14 | let mut msg = client.snapshot().await?; 15 | let mut snapshot = vec![]; 16 | loop { 17 | if let Some(resp) = msg.message().await? { 18 | snapshot.extend_from_slice(&resp.blob); 19 | if resp.remaining_bytes == 0 { 20 | break; 21 | } 22 | } 23 | } 24 | println!("snapshot size: {}", snapshot.len()); 25 | 26 | Ok(()) 27 | } 28 | -------------------------------------------------------------------------------- /crates/xline-client/examples/watch.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use xline_client::{Client, ClientOptions}; 3 | 4 | #[tokio::main] 5 | async fn main() -> Result<()> { 6 | // the name and address of all curp members 7 | let curp_members = ["10.0.0.1:2379", "10.0.0.2:2379", "10.0.0.3:2379"]; 8 | 9 | let client = Client::connect(curp_members, ClientOptions::default()).await?; 10 | let mut watch_client = client.watch_client(); 11 | let kv_client = client.kv_client(); 12 | 13 | // watch 14 | let (mut watcher, mut stream) = watch_client.watch("key1", None).await?; 15 | kv_client.put("key1", "value1", None).await?; 16 | 17 | let resp = stream.message().await?.unwrap(); 18 | let kv = resp.events[0].kv.as_ref().unwrap(); 19 | 20 | println!( 21 | "got key: {}, value: {}", 22 | String::from_utf8_lossy(&kv.key), 23 | String::from_utf8_lossy(&kv.value) 24 | ); 25 | 26 | // cancel the watch 27 | watcher.cancel()?; 28 | 29 | Ok(()) 30 | } 31 | -------------------------------------------------------------------------------- /crates/xline-client/src/clients/election.rs: -------------------------------------------------------------------------------- 1 | // TODO: Remove these when the placeholder is implemented. 2 | #![allow(missing_copy_implementations)] 3 | #![allow(clippy::new_without_default)] 4 | 5 | /// Client for Election operations. 6 | #[derive(Clone, Debug)] 7 | #[non_exhaustive] 8 | pub struct ElectionClient; 9 | 10 | impl ElectionClient { 11 | /// Create a new election client 12 | #[inline] 13 | #[must_use] 14 | pub fn new() -> Self { 15 | Self 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /crates/xline-client/src/clients/mod.rs: -------------------------------------------------------------------------------- 1 | pub use auth::AuthClient; 2 | pub use cluster::ClusterClient; 3 | pub use election::ElectionClient; 4 | pub use kv::KvClient; 5 | pub use lease::LeaseClient; 6 | pub use lock::{LockClient, Session, Xutex}; 7 | pub use maintenance::MaintenanceClient; 8 | pub use watch::WatchClient; 9 | 10 | /// Auth client. 11 | mod auth; 12 | /// Cluster client 13 | mod cluster; 14 | /// Election client. 15 | mod election; 16 | /// Kv client. 17 | mod kv; 18 | /// Lease client. 19 | mod lease; 20 | /// Lock client. 21 | pub mod lock; 22 | /// Maintenance client. 23 | mod maintenance; 24 | /// Watch client. 25 | mod watch; 26 | 27 | /// Default session ttl 28 | pub const DEFAULT_SESSION_TTL: i64 = 60; 29 | -------------------------------------------------------------------------------- /crates/xline-client/src/lease_gen.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::{AtomicU64, Ordering}; 2 | 3 | use clippy_utilities::NumericCast; 4 | 5 | /// Generator of unique lease id 6 | /// Note that this Lease Id generation method may cause collisions, 7 | /// the client should retry after informed by the server. 8 | #[derive(Debug, Default)] 9 | pub struct LeaseIdGenerator { 10 | /// the current lease id 11 | id: AtomicU64, 12 | } 13 | 14 | impl LeaseIdGenerator { 15 | /// New `IdGenerator` 16 | /// 17 | /// # Panics 18 | /// 19 | /// panic if failed to generate random bytes 20 | #[inline] 21 | #[must_use] 22 | pub fn new() -> Self { 23 | let mut buf = [0u8; 8]; 24 | getrandom::getrandom(&mut buf).unwrap_or_else(|err| { 25 | panic!("Failed to generate random bytes for lease id generator: {err}"); 26 | }); 27 | let id = AtomicU64::new(u64::from_le_bytes(buf)); 28 | Self { id } 29 | } 30 | 31 | /// Generate next id 32 | #[inline] 33 | pub fn next(&self) -> i64 { 34 | let id = self.id.fetch_add(1, Ordering::Relaxed); 35 | // to ensure the id is positive 36 | if id == 0 { 37 | return self.next(); 38 | } 39 | // set the highest bit to 0 as we need only need positive values 40 | (id & 0x7fff_ffff_ffff_ffff).numeric_cast() 41 | } 42 | } 43 | 44 | #[cfg(test)] 45 | mod test { 46 | use std::collections::HashSet; 47 | 48 | use super::*; 49 | 50 | #[test] 51 | fn no_duplicates_in_one() { 52 | let mut exist = HashSet::new(); 53 | let id_gen = LeaseIdGenerator::new(); 54 | 55 | for _ in 0..1_000_000 { 56 | let id = id_gen.next(); 57 | assert!(exist.insert(id), "id {id} is duplicated"); 58 | } 59 | } 60 | 61 | #[test] 62 | fn no_duplicates_in_multi() { 63 | let mut exist = HashSet::new(); 64 | 65 | for _ in 0..1000 { 66 | let id_gen = LeaseIdGenerator::new(); 67 | for _ in 0..1000 { 68 | let id = id_gen.next(); 69 | assert!(exist.insert(id), "id {id} is duplicated"); 70 | } 71 | } 72 | } 73 | 74 | #[test] 75 | fn leases_are_valid() { 76 | for _ in 0..1000 { 77 | let id_gen = LeaseIdGenerator::new(); 78 | for _ in 0..1000 { 79 | let id = id_gen.next(); 80 | assert!(id > 0, "id {id} is not positive"); 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /crates/xline-client/src/types/auth.rs: -------------------------------------------------------------------------------- 1 | pub use xlineapi::{ 2 | AuthDisableResponse, AuthEnableResponse, AuthRoleAddResponse, AuthRoleDeleteResponse, 3 | AuthRoleGetResponse, AuthRoleGrantPermissionResponse, AuthRoleListResponse, 4 | AuthRoleRevokePermissionResponse, AuthStatusResponse, AuthUserAddResponse, 5 | AuthUserChangePasswordResponse, AuthUserDeleteResponse, AuthUserGetResponse, 6 | AuthUserGrantRoleResponse, AuthUserListResponse, AuthUserRevokeRoleResponse, 7 | AuthenticateResponse, Type as PermissionType, 8 | }; 9 | 10 | use super::range_end::RangeOption; 11 | 12 | /// Role access permission. 13 | #[derive(Debug, Clone)] 14 | pub struct Permission { 15 | /// The inner Permission 16 | inner: xlineapi::Permission, 17 | /// The range option 18 | range_option: Option, 19 | } 20 | 21 | impl Permission { 22 | /// Creates a permission with operation type and key 23 | /// 24 | /// `perm_type` is the permission type, 25 | /// `key` is the key to grant with the permission. 26 | /// `range_option` is the range option of how to get `range_end` from key. 27 | #[inline] 28 | #[must_use] 29 | pub fn new( 30 | perm_type: PermissionType, 31 | key: impl Into>, 32 | range_option: Option, 33 | ) -> Self { 34 | Self::from((perm_type, key.into(), range_option)) 35 | } 36 | } 37 | 38 | impl From for xlineapi::Permission { 39 | #[inline] 40 | fn from(mut perm: Permission) -> Self { 41 | perm.inner.range_end = perm 42 | .range_option 43 | .unwrap_or_default() 44 | .get_range_end(&mut perm.inner.key); 45 | perm.inner 46 | } 47 | } 48 | 49 | impl PartialEq for Permission { 50 | #[inline] 51 | fn eq(&self, other: &Self) -> bool { 52 | self.inner == other.inner && self.range_option == other.range_option 53 | } 54 | } 55 | 56 | impl Eq for Permission {} 57 | 58 | impl From<(PermissionType, Vec, Option)> for Permission { 59 | #[inline] 60 | fn from( 61 | (perm_type, key, range_option): (PermissionType, Vec, Option), 62 | ) -> Self { 63 | Permission { 64 | inner: xlineapi::Permission { 65 | perm_type: perm_type.into(), 66 | key, 67 | ..Default::default() 68 | }, 69 | range_option, 70 | } 71 | } 72 | } 73 | 74 | impl From<(PermissionType, &str, Option)> for Permission { 75 | #[inline] 76 | fn from(value: (PermissionType, &str, Option)) -> Self { 77 | Self::from((value.0, value.1.as_bytes().to_vec(), value.2)) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /crates/xline-client/src/types/lease.rs: -------------------------------------------------------------------------------- 1 | use futures::channel::mpsc::Sender; 2 | pub use xlineapi::{ 3 | LeaseGrantResponse, LeaseKeepAliveResponse, LeaseLeasesResponse, LeaseRevokeResponse, 4 | LeaseStatus, LeaseTimeToLiveResponse, 5 | }; 6 | 7 | use crate::error::{Result, XlineClientError}; 8 | 9 | /// The lease keep alive handle. 10 | #[derive(Debug)] 11 | pub struct LeaseKeeper { 12 | /// lease id 13 | id: i64, 14 | /// sender to send keep alive request 15 | sender: Sender, 16 | } 17 | 18 | impl LeaseKeeper { 19 | /// Creates a new `LeaseKeeper`. 20 | #[inline] 21 | #[must_use] 22 | pub fn new(id: i64, sender: Sender) -> Self { 23 | Self { id, sender } 24 | } 25 | 26 | /// The lease id which user want to keep alive. 27 | #[inline] 28 | #[must_use] 29 | pub const fn id(&self) -> i64 { 30 | self.id 31 | } 32 | 33 | /// Sends a keep alive request and receive response 34 | /// 35 | /// # Errors 36 | /// 37 | /// This function will return an error if the inner channel is closed 38 | #[inline] 39 | pub fn keep_alive(&mut self) -> Result<()> { 40 | self.sender 41 | .try_send(xlineapi::LeaseKeepAliveRequest { id: self.id }) 42 | .map_err(|e| XlineClientError::LeaseError(e.to_string())) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /crates/xline-client/src/types/mod.rs: -------------------------------------------------------------------------------- 1 | /// Auth type definitions. 2 | pub mod auth; 3 | /// Kv type definitions. 4 | pub mod kv; 5 | /// Lease type definitions 6 | pub mod lease; 7 | /// Range Option definitions, to build a `range_end` from key. 8 | pub mod range_end; 9 | /// Watch type definitions. 10 | pub mod watch; 11 | -------------------------------------------------------------------------------- /crates/xline-client/src/types/range_end.rs: -------------------------------------------------------------------------------- 1 | use xlineapi::command::KeyRange; 2 | 3 | /// Range end options, indicates how to set `range_end` from a key. 4 | #[derive(Clone, Debug, PartialEq, Eq, Default)] 5 | #[non_exhaustive] 6 | pub enum RangeOption { 7 | /// Only lookup the given single key. Use empty Vec as `range_end` 8 | #[default] 9 | SingleKey, 10 | /// If set, Xline will lookup all keys match the given prefix 11 | Prefix, 12 | /// If set, Xline will lookup all keys that are equal to or greater than the given key 13 | FromKey, 14 | /// Set `range_end` directly 15 | RangeEnd(Vec), 16 | } 17 | 18 | impl RangeOption { 19 | /// Get the `range_end` for request, and modify key if necessary. 20 | #[inline] 21 | pub fn get_range_end(self, key: &mut Vec) -> Vec { 22 | match self { 23 | RangeOption::SingleKey => vec![], 24 | RangeOption::Prefix => { 25 | if key.is_empty() { 26 | key.push(0); 27 | vec![0] 28 | } else { 29 | KeyRange::get_prefix(key) 30 | } 31 | } 32 | RangeOption::FromKey => { 33 | if key.is_empty() { 34 | key.push(0); 35 | } 36 | vec![0] 37 | } 38 | RangeOption::RangeEnd(range_end) => range_end, 39 | } 40 | } 41 | } 42 | 43 | #[cfg(test)] 44 | mod tests { 45 | use super::*; 46 | 47 | #[test] 48 | fn test_get_range_end() { 49 | let mut key = vec![]; 50 | assert!(RangeOption::SingleKey.get_range_end(&mut key).is_empty()); 51 | assert!(key.is_empty()); 52 | assert!(RangeOption::FromKey.get_range_end(&mut key).first() == Some(&0)); 53 | assert!(key.first() == Some(&0)); 54 | assert_eq!( 55 | RangeOption::Prefix.get_range_end(&mut key), 56 | KeyRange::get_prefix(&key) 57 | ); 58 | assert_eq!( 59 | RangeOption::RangeEnd(vec![1, 2, 3]).get_range_end(&mut key), 60 | vec![1, 2, 3] 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /crates/xline-client/tests/it/common.rs: -------------------------------------------------------------------------------- 1 | use xline_client::{error::XlineClientBuildError, Client, ClientOptions}; 2 | use xline_test_utils::Cluster; 3 | 4 | pub async fn get_cluster_client() -> Result<(Cluster, Client), XlineClientBuildError> { 5 | let mut cluster = Cluster::new(3).await; 6 | cluster.start().await; 7 | let client = Client::connect(cluster.all_client_addrs(), ClientOptions::default()).await?; 8 | Ok((cluster, client)) 9 | } 10 | -------------------------------------------------------------------------------- /crates/xline-client/tests/it/main.rs: -------------------------------------------------------------------------------- 1 | mod auth; 2 | mod common; 3 | mod kv; 4 | mod lease; 5 | mod lock; 6 | mod maintenance; 7 | mod watch; 8 | -------------------------------------------------------------------------------- /crates/xline-client/tests/it/maintenance.rs: -------------------------------------------------------------------------------- 1 | use xline_client::error::Result; 2 | 3 | use super::common::get_cluster_client; 4 | 5 | #[tokio::test(flavor = "multi_thread")] 6 | async fn snapshot_should_get_valid_data() -> Result<()> { 7 | let (_cluster, client) = get_cluster_client().await.unwrap(); 8 | let mut client = client.maintenance_client(); 9 | 10 | let mut msg = client.snapshot().await?; 11 | loop { 12 | if let Some(resp) = msg.message().await? { 13 | assert!(!resp.blob.is_empty()); 14 | if resp.remaining_bytes == 0 { 15 | break; 16 | } 17 | } 18 | } 19 | 20 | Ok(()) 21 | } 22 | -------------------------------------------------------------------------------- /crates/xline-client/tests/it/watch.rs: -------------------------------------------------------------------------------- 1 | //! The following tests are originally from `etcd-client` 2 | use xline_client::{error::Result, types::watch::EventType}; 3 | 4 | use super::common::get_cluster_client; 5 | 6 | #[tokio::test(flavor = "multi_thread")] 7 | async fn watch_should_receive_consistent_events() -> Result<()> { 8 | let (_cluster, client) = get_cluster_client().await.unwrap(); 9 | let mut watch_client = client.watch_client(); 10 | let kv_client = client.kv_client(); 11 | 12 | let (mut watcher, mut stream) = watch_client.watch("watch01", None).await?; 13 | 14 | kv_client.put("watch01", "01", None).await?; 15 | 16 | let resp = stream.message().await?.unwrap(); 17 | assert_eq!(resp.watch_id, watcher.watch_id()); 18 | assert_eq!(resp.events.len(), 1); 19 | 20 | let kv = resp.events[0].kv.as_ref().unwrap(); 21 | assert_eq!(kv.key, b"watch01"); 22 | assert_eq!(kv.value, b"01"); 23 | assert_eq!(resp.events[0].r#type(), EventType::Put); 24 | 25 | watcher.cancel()?; 26 | 27 | let resp = stream.message().await?.unwrap(); 28 | assert_eq!(resp.watch_id, watcher.watch_id()); 29 | assert!(resp.canceled); 30 | 31 | Ok(()) 32 | } 33 | 34 | /// To ensure #505 is fixed 35 | #[tokio::test(flavor = "multi_thread")] 36 | async fn watch_stream_should_work_after_watcher_dropped() -> Result<()> { 37 | let (_cluster, client) = get_cluster_client().await.unwrap(); 38 | let mut watch_client = client.watch_client(); 39 | let kv_client = client.kv_client(); 40 | 41 | let (_, mut stream) = watch_client.watch("watch01", None).await?; 42 | 43 | kv_client.put("watch01", "01", None).await?; 44 | 45 | let resp = stream.message().await?.unwrap(); 46 | assert_eq!(resp.events.len(), 1); 47 | 48 | let kv = resp.events[0].kv.as_ref().unwrap(); 49 | assert_eq!(kv.key, b"watch01"); 50 | assert_eq!(kv.value, b"01"); 51 | assert_eq!(resp.events[0].r#type(), EventType::Put); 52 | 53 | Ok(()) 54 | } 55 | -------------------------------------------------------------------------------- /crates/xline-test-utils/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xline-test-utils" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["DatenLord "] 6 | repository = "https://github.com/xline-kv/Xline/tree/master/xline-test-utils" 7 | description = "Test utils for Xline" 8 | categories = ["Test"] 9 | keywords = ["Test", "Utils"] 10 | license = "Apache-2.0" 11 | readme = "README.md" 12 | 13 | [dependencies] 14 | futures = "0.3.30" 15 | rand = "0.8.5" 16 | tokio = { version = "0.2.25", package = "madsim-tokio", features = [ 17 | "rt-multi-thread", 18 | "time", 19 | "fs", 20 | "macros", 21 | "net", 22 | "signal", 23 | ] } 24 | tonic = { version = "0.5.0", package = "madsim-tonic" } 25 | utils = { path = "../utils", features = ["parking_lot"] } 26 | workspace-hack = { version = "0.1", path = "../../workspace-hack" } 27 | xline = { path = "../xline" } 28 | xline-client = { path = "../xline-client" } 29 | -------------------------------------------------------------------------------- /crates/xline-test-utils/README.md: -------------------------------------------------------------------------------- 1 | # Xline test utilities 2 | 3 | This crate provides utilities for Xline tests. 4 | 5 | The main reason for the existence of this crate is to share the utilities between Xline's and Xline client's tests. 6 | -------------------------------------------------------------------------------- /crates/xline-test-utils/src/bin/test_cluster.rs: -------------------------------------------------------------------------------- 1 | //! The binary is to quickly setup a cluster for convenient testing 2 | use tokio::signal; 3 | use xline_test_utils::Cluster; 4 | 5 | #[tokio::main] 6 | async fn main() { 7 | let mut cluster = Cluster::new(3).await; 8 | cluster.start().await; 9 | 10 | println!("cluster running"); 11 | 12 | for (id, addr) in cluster.all_members_client_urls_map() { 13 | println!("server id: {} addr: {}", id, addr); 14 | } 15 | 16 | if let Err(e) = signal::ctrl_c().await { 17 | eprintln!("Unable to listen for shutdown signal: {e}"); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /crates/xline/src/header_gen.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use curp::members::ServerId; 4 | use parking_lot::Mutex; 5 | 6 | use crate::{revision_number::RevisionNumberGenerator, rpc::ResponseHeader}; 7 | 8 | /// Generator of `ResponseHeader` 9 | #[derive(Debug)] 10 | pub(crate) struct HeaderGenerator { 11 | /// Id of the cluster 12 | cluster_id: u64, 13 | /// Id of the member 14 | member_id: u64, 15 | /// Term of curp 16 | term: Arc>, 17 | /// Revision of kv store 18 | general_revision: Arc, 19 | /// Revision of auth store 20 | auth_revision: Arc, 21 | } 22 | 23 | impl HeaderGenerator { 24 | /// New `HeaderGenerator` 25 | pub(crate) fn new(cluster_id: u64, member_id: ServerId) -> Self { 26 | Self { 27 | cluster_id, 28 | member_id, 29 | term: Arc::new(Mutex::new(0)), 30 | general_revision: Arc::new(RevisionNumberGenerator::default()), 31 | auth_revision: Arc::new(RevisionNumberGenerator::default()), 32 | } 33 | } 34 | 35 | /// Generate `ResponseHeader` 36 | pub(crate) fn gen_header(&self) -> ResponseHeader { 37 | ResponseHeader { 38 | cluster_id: self.cluster_id, 39 | member_id: self.member_id, 40 | raft_term: *self.term.lock(), 41 | revision: self.general_revision(), 42 | } 43 | } 44 | 45 | /// Generate `ResponseHeader` for auth request 46 | pub(crate) fn gen_auth_header(&self) -> ResponseHeader { 47 | ResponseHeader { 48 | cluster_id: self.cluster_id, 49 | member_id: self.member_id, 50 | raft_term: *self.term.lock(), 51 | revision: self.auth_revision(), 52 | } 53 | } 54 | 55 | /// Set term 56 | #[allow(dead_code)] // Will be used in the future 57 | pub(crate) fn set_term(&self, term: u64) { 58 | *self.term.lock() = term; 59 | } 60 | 61 | /// Get general revision 62 | pub(crate) fn general_revision(&self) -> i64 { 63 | self.general_revision.get() 64 | } 65 | 66 | /// Return Arc of general revision 67 | pub(crate) fn general_revision_arc(&self) -> Arc { 68 | Arc::clone(&self.general_revision) 69 | } 70 | 71 | /// Get auth revision 72 | pub(crate) fn auth_revision(&self) -> i64 { 73 | self.auth_revision.get() 74 | } 75 | 76 | /// Return Arc of auth revision 77 | pub(crate) fn auth_revision_arc(&self) -> Arc { 78 | Arc::clone(&self.auth_revision) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /crates/xline/src/id_gen.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | use std::{ 3 | sync::atomic::{AtomicU64, Ordering}, 4 | time::{SystemTime, UNIX_EPOCH}, 5 | }; 6 | 7 | use clippy_utilities::{NumericCast, OverflowArithmetic}; 8 | use curp::members::ServerId; 9 | 10 | /// Generator of unique id 11 | /// 12 | /// id format: 13 | /// | prefix | suffix | 14 | /// | 2 bytes | 5 bytes | 1 byte | 15 | /// | member id | timestamp | cnt | 16 | #[derive(Debug)] 17 | pub(crate) struct IdGenerator { 18 | /// prefix of id 19 | prefix: u64, 20 | /// suffix of id 21 | suffix: AtomicU64, 22 | } 23 | 24 | impl IdGenerator { 25 | /// New `IdGenerator` 26 | pub(crate) fn new(member_id: ServerId) -> Self { 27 | let prefix = member_id.overflowing_shl(48).0; 28 | let mut ts = SystemTime::now() 29 | .duration_since(UNIX_EPOCH) 30 | .unwrap_or_else(|e| panic!("SystemTime before UNIX EPOCH! {e}")) 31 | .as_millis(); 32 | ts &= (u128::MAX.overflowing_shr(88).0); // lower 40 bits (128 - 40) 33 | ts = ts.overflowing_shl(8).0; // shift left 8 bits 34 | let suffix = AtomicU64::new(ts.numeric_cast()); 35 | Self { prefix, suffix } 36 | } 37 | 38 | /// Generate next id 39 | pub(crate) fn next(&self) -> i64 { 40 | let suffix = self.suffix.fetch_add(1, Ordering::Relaxed); 41 | let id = self.prefix | suffix; 42 | (id & 0x7fff_ffff_ffff_ffff).numeric_cast() 43 | } 44 | } 45 | 46 | #[cfg(test)] 47 | mod test { 48 | use super::*; 49 | #[test] 50 | fn test_id_generator() { 51 | let id_gen = IdGenerator::new(0); 52 | assert_ne!(id_gen.next(), id_gen.next()); 53 | assert_ne!(id_gen.next(), id_gen.next()); 54 | assert_ne!(id_gen.next(), id_gen.next()); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /crates/xline/src/restore.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use anyhow::Result; 4 | use bytes::BytesMut; 5 | use clippy_utilities::NumericCast; 6 | use engine::{Engine, EngineType, Snapshot, SnapshotApi, StorageEngine}; 7 | use tokio_util::io::read_buf; 8 | use utils::table_names::XLINE_TABLES; 9 | 10 | use crate::server::MAINTENANCE_SNAPSHOT_CHUNK_SIZE; 11 | 12 | /// Restore snapshot to data dir 13 | /// 14 | /// # Errors 15 | /// 16 | /// - return `ClientError::IoError` if meet io errors 17 | /// - return `ClientError::EngineError` if meet engine errors 18 | #[inline] 19 | #[allow(clippy::indexing_slicing)] // safe operation 20 | pub async fn restore, D: Into>( 21 | snapshot_path: P, 22 | data_dir: D, 23 | ) -> Result<()> { 24 | let mut snapshot_f = tokio::fs::File::open(snapshot_path).await?; 25 | let tmp_path = format!("/tmp/snapshot-{}", uuid::Uuid::new_v4()); 26 | let mut rocks_snapshot = Snapshot::new_for_receiving(EngineType::Rocks((&tmp_path).into()))?; 27 | let mut buf = BytesMut::with_capacity(MAINTENANCE_SNAPSHOT_CHUNK_SIZE.numeric_cast()); 28 | while let Ok(n) = read_buf(&mut snapshot_f, &mut buf).await { 29 | if n == 0 { 30 | break; 31 | } 32 | rocks_snapshot.write_all(buf.split().freeze()).await?; 33 | } 34 | 35 | let restore_rocks_engine = Engine::new(EngineType::Rocks(data_dir.into()), &XLINE_TABLES)?; 36 | restore_rocks_engine 37 | .apply_snapshot(rocks_snapshot, &XLINE_TABLES) 38 | .await?; 39 | Ok(()) 40 | } 41 | -------------------------------------------------------------------------------- /crates/xline/src/revision_number.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::{AtomicI64, Ordering}; 2 | 3 | /// Revision number 4 | #[derive(Debug)] 5 | pub(crate) struct RevisionNumberGenerator { 6 | /// The current revision number 7 | current: AtomicI64, 8 | } 9 | 10 | impl RevisionNumberGenerator { 11 | /// Create a new revision 12 | pub(crate) fn new(rev: i64) -> Self { 13 | Self { 14 | current: AtomicI64::new(rev), 15 | } 16 | } 17 | 18 | /// Get the current revision number 19 | pub(crate) fn get(&self) -> i64 { 20 | self.current.load(Ordering::Relaxed) 21 | } 22 | 23 | /// Set the revision number 24 | pub(crate) fn set(&self, rev: i64) { 25 | self.current.store(rev, Ordering::Relaxed); 26 | } 27 | 28 | /// Gets a temporary state 29 | pub(crate) fn state(&self) -> RevisionNumberGeneratorState { 30 | RevisionNumberGeneratorState { 31 | current: &self.current, 32 | next: AtomicI64::new(self.get()), 33 | } 34 | } 35 | } 36 | 37 | impl Default for RevisionNumberGenerator { 38 | #[inline] 39 | fn default() -> Self { 40 | RevisionNumberGenerator::new(1) 41 | } 42 | } 43 | 44 | /// Revision generator with temporary state 45 | pub(crate) struct RevisionNumberGeneratorState<'a> { 46 | /// The current revision number 47 | current: &'a AtomicI64, 48 | /// Next revision number 49 | next: AtomicI64, 50 | } 51 | 52 | impl RevisionNumberGeneratorState<'_> { 53 | /// Get the current revision number 54 | pub(crate) fn get(&self) -> i64 { 55 | self.next.load(Ordering::Relaxed) 56 | } 57 | 58 | /// Increases the next revision number 59 | pub(crate) fn next(&self) -> i64 { 60 | self.next.fetch_add(1, Ordering::Relaxed).wrapping_add(1) 61 | } 62 | 63 | /// Commit the revision number 64 | pub(crate) fn commit(&self) { 65 | self.current 66 | .store(self.next.load(Ordering::Relaxed), Ordering::Relaxed); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /crates/xline/src/server/mod.rs: -------------------------------------------------------------------------------- 1 | /// Xline auth server 2 | mod auth_server; 3 | /// Auth Wrapper 4 | mod auth_wrapper; 5 | /// Cluster server 6 | mod cluster_server; 7 | /// Command to be executed 8 | pub(crate) mod command; 9 | /// Xline kv server 10 | mod kv_server; 11 | /// Xline lease server 12 | mod lease_server; 13 | /// Xline lock server 14 | mod lock_server; 15 | /// Xline maintenance client 16 | mod maintenance; 17 | /// Xline watch server 18 | mod watch_server; 19 | /// Xline server 20 | mod xline_server; 21 | 22 | pub use self::xline_server::XlineServer; 23 | pub(crate) use self::{auth_server::get_token, maintenance::MAINTENANCE_SNAPSHOT_CHUNK_SIZE}; 24 | -------------------------------------------------------------------------------- /crates/xline/src/state.rs: -------------------------------------------------------------------------------- 1 | use std::{sync::Arc, time::Duration}; 2 | 3 | use curp::role_change::RoleChange; 4 | 5 | use crate::storage::{ 6 | compact::{Compactable, Compactor}, 7 | LeaseStore, 8 | }; 9 | 10 | /// State of current node 11 | pub(crate) struct State { 12 | /// lease storage 13 | lease_storage: Arc, 14 | /// auto compactor 15 | auto_compactor: Option>>, 16 | } 17 | 18 | impl Clone for State { 19 | fn clone(&self) -> Self { 20 | Self { 21 | lease_storage: Arc::clone(&self.lease_storage), 22 | auto_compactor: self.auto_compactor.clone(), 23 | } 24 | } 25 | } 26 | 27 | impl RoleChange for State { 28 | fn on_election_win(&self) { 29 | self.lease_storage.promote(Duration::from_secs(1)); // TODO: extend should be election timeout 30 | if let Some(auto_compactor) = self.auto_compactor.as_ref() { 31 | auto_compactor.resume(); 32 | } 33 | } 34 | 35 | fn on_calibrate(&self) { 36 | self.lease_storage.demote(); 37 | if let Some(auto_compactor) = self.auto_compactor.as_ref() { 38 | auto_compactor.pause(); 39 | } 40 | } 41 | } 42 | 43 | impl State { 44 | /// Create a new State 45 | pub(super) fn new( 46 | lease_storage: Arc, 47 | auto_compactor: Option>>, 48 | ) -> Self { 49 | Self { 50 | lease_storage, 51 | auto_compactor, 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /crates/xline/src/storage/auth_store/mod.rs: -------------------------------------------------------------------------------- 1 | /// Storage backend for auth 2 | mod backend; 3 | /// Structs for permission 4 | mod perms; 5 | /// Storage for auth 6 | mod store; 7 | 8 | pub(crate) use backend::{AUTH_ENABLE_KEY, AUTH_REVISION_KEY}; 9 | pub(crate) use store::AuthStore; 10 | -------------------------------------------------------------------------------- /crates/xline/src/storage/lease_store/lease_queue.rs: -------------------------------------------------------------------------------- 1 | use std::{cmp::Reverse, time::Instant}; 2 | 3 | use priority_queue::PriorityQueue; 4 | 5 | /// Priority queue of lease 6 | #[derive(Debug)] 7 | #[cfg_attr(test, derive(Default))] 8 | pub(super) struct LeaseQueue { 9 | /// Inner queue of lease queue 10 | inner: PriorityQueue>, 11 | } 12 | 13 | impl LeaseQueue { 14 | /// New `LeaseQueue` 15 | pub(super) fn new() -> Self { 16 | Self { 17 | inner: PriorityQueue::new(), 18 | } 19 | } 20 | 21 | /// Insert lease 22 | pub(super) fn insert(&mut self, lease_id: i64, expiry: Instant) -> Option { 23 | self.inner.push(lease_id, Reverse(expiry)).map(|v| v.0) 24 | } 25 | 26 | /// Update lease 27 | pub(super) fn update(&mut self, lease_id: i64, expiry: Instant) -> Option { 28 | self.inner 29 | .change_priority(&lease_id, Reverse(expiry)) 30 | .map(|v| v.0) 31 | } 32 | 33 | /// Return the smallest expiry in the queue, or None if it is empty. 34 | pub(super) fn peek(&self) -> Option<&Instant> { 35 | self.inner.peek().map(|(_, v)| &v.0) 36 | } 37 | 38 | /// Remove the lease id with the smallest expiry time 39 | pub(super) fn pop(&mut self) -> Option { 40 | self.inner.pop().map(|(k, _)| k) 41 | } 42 | 43 | /// Clear the lease heap 44 | pub(super) fn clear(&mut self) { 45 | self.inner.clear(); 46 | } 47 | } 48 | 49 | #[cfg(test)] 50 | mod test { 51 | use std::time::Duration; 52 | 53 | use super::*; 54 | #[test] 55 | fn test_lease_heap() { 56 | let mut hp = LeaseQueue::new(); 57 | 58 | let expiry1 = Instant::now() + Duration::from_secs(8); 59 | assert!(hp.insert(1, expiry1).is_none()); 60 | assert_eq!(hp.inner.len(), 1); 61 | assert_eq!(hp.peek(), Some(&expiry1)); 62 | 63 | let expiry2 = Instant::now() + Duration::from_secs(10); 64 | assert!(hp.insert(2, expiry2).is_none()); 65 | assert_eq!(hp.inner.len(), 2); 66 | assert_eq!(hp.peek(), Some(&expiry1)); 67 | 68 | let expiry3 = Instant::now() + Duration::from_secs(1); 69 | assert_eq!(hp.update(1, expiry3), Some(expiry1)); 70 | assert_eq!(hp.inner.len(), 2); 71 | assert_eq!(hp.peek(), Some(&expiry3)); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /crates/xline/src/storage/mod.rs: -------------------------------------------------------------------------------- 1 | /// Storage for alarm 2 | pub(crate) mod alarm_store; 3 | /// Storage for Auth 4 | pub(crate) mod auth_store; 5 | /// Compact module 6 | pub(super) mod compact; 7 | /// Database module 8 | pub mod db; 9 | /// Index module 10 | pub(crate) mod index; 11 | /// Storage for KV 12 | pub(crate) mod kv_store; 13 | /// KV watcher module 14 | pub(crate) mod kvwatcher; 15 | /// Storage for lease 16 | pub(crate) mod lease_store; 17 | /// Revision module 18 | pub(crate) mod revision; 19 | /// Storage API 20 | pub(crate) mod storage_api; 21 | 22 | pub use self::revision::Revision; 23 | pub(crate) use self::{ 24 | alarm_store::AlarmStore, auth_store::AuthStore, kv_store::KvStore, lease_store::LeaseStore, 25 | }; 26 | -------------------------------------------------------------------------------- /crates/xline/src/storage/storage_api.rs: -------------------------------------------------------------------------------- 1 | use xlineapi::execute_error::ExecuteError; 2 | 3 | use super::db::WriteOp; 4 | 5 | /// Storage operations in xline 6 | pub(crate) trait XlineStorageOps { 7 | /// Write an operation to the transaction 8 | fn write_op(&self, op: WriteOp) -> Result<(), ExecuteError>; 9 | 10 | /// Write a batch of operations to the transaction 11 | fn write_ops(&self, ops: Vec) -> Result<(), ExecuteError>; 12 | 13 | /// Get values by keys from storage 14 | /// 15 | /// # Errors 16 | /// 17 | /// if error occurs in storage, return `Err(error)` 18 | fn get_value(&self, table: &'static str, key: K) -> Result>, ExecuteError> 19 | where 20 | K: AsRef<[u8]> + std::fmt::Debug; 21 | 22 | /// Get values by keys from storage 23 | /// 24 | /// # Errors 25 | /// 26 | /// if error occurs in storage, return `Err(error)` 27 | fn get_values( 28 | &self, 29 | table: &'static str, 30 | keys: &[K], 31 | ) -> Result>>, ExecuteError> 32 | where 33 | K: AsRef<[u8]> + std::fmt::Debug; 34 | } 35 | -------------------------------------------------------------------------------- /crates/xline/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | /// Xline command line arguments 2 | mod args; 3 | /// Xline tracing init 4 | mod trace; 5 | 6 | /// Xline metrics init 7 | mod metrics; 8 | 9 | pub use args::{parse_config, ServerArgs}; 10 | pub use metrics::init_metrics; 11 | pub use trace::init_subscriber; 12 | -------------------------------------------------------------------------------- /crates/xline/tests/it/lock_test.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, sync::Arc, time::Duration}; 2 | 3 | use test_macros::abort_on_panic; 4 | use tokio::time::{sleep, Instant}; 5 | use xline_test_utils::{clients::Xutex, Cluster}; 6 | 7 | #[tokio::test(flavor = "multi_thread")] 8 | #[abort_on_panic] 9 | async fn test_lock() -> Result<(), Box> { 10 | let mut cluster = Cluster::new(3).await; 11 | cluster.start().await; 12 | let client = cluster.client().await; 13 | let lock_client = client.lock_client(); 14 | let event = Arc::new(event_listener::Event::new()); 15 | 16 | let lock_handle = tokio::spawn({ 17 | let c = lock_client.clone(); 18 | let event = Arc::clone(&event); 19 | async move { 20 | let mut xutex = Xutex::new(c, "test", None, None).await.unwrap(); 21 | let _lock = xutex.lock_unsafe().await.unwrap(); 22 | let _notified = event.notify(1); 23 | sleep(Duration::from_secs(2)).await; 24 | } 25 | }); 26 | 27 | event.listen().await; 28 | let now = Instant::now(); 29 | 30 | let mut xutex = Xutex::new(lock_client, "test", None, None).await?; 31 | let _lock = xutex.lock_unsafe().await?; 32 | let elapsed = now.elapsed(); 33 | assert!(elapsed >= Duration::from_secs(1)); 34 | let _ignore = lock_handle.await; 35 | 36 | Ok(()) 37 | } 38 | -------------------------------------------------------------------------------- /crates/xline/tests/it/main.rs: -------------------------------------------------------------------------------- 1 | mod auth_test; 2 | mod cluster_test; 3 | mod kv_test; 4 | mod lease_test; 5 | mod lock_test; 6 | mod maintenance_test; 7 | mod tls_test; 8 | mod watch_test; 9 | -------------------------------------------------------------------------------- /crates/xline/tests/it/watch_test.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | 3 | use test_macros::abort_on_panic; 4 | use xline_test_utils::Cluster; 5 | use xlineapi::EventType; 6 | 7 | fn event_type(event_type: i32) -> EventType { 8 | match event_type { 9 | 0 => EventType::Put, 10 | 1 => EventType::Delete, 11 | _ => unreachable!("un"), 12 | } 13 | } 14 | 15 | #[tokio::test(flavor = "multi_thread")] 16 | #[abort_on_panic] 17 | async fn test_watch() -> Result<(), Box> { 18 | let mut cluster = Cluster::new(3).await; 19 | cluster.start().await; 20 | let client = cluster.client().await; 21 | let mut watch_client = client.watch_client(); 22 | let kv_client = client.kv_client(); 23 | 24 | let (_watcher, mut stream) = watch_client.watch("foo", None).await?; 25 | let handle = tokio::spawn(async move { 26 | if let Ok(Some(res)) = stream.message().await { 27 | let event = res.events.get(0).unwrap(); 28 | assert_eq!(event_type(event.r#type), EventType::Put); 29 | let kv = event.kv.clone().unwrap(); 30 | assert_eq!(kv.key, b"foo"); 31 | assert_eq!(kv.value, b"bar"); 32 | } 33 | if let Ok(Some(res)) = stream.message().await { 34 | let event = res.events.get(0).unwrap(); 35 | let kv = event.kv.clone().unwrap(); 36 | assert_eq!(event_type(event.r#type), EventType::Delete); 37 | assert_eq!(kv.key, b"foo"); 38 | assert_eq!(kv.value, b""); 39 | } 40 | }); 41 | 42 | kv_client.put("foo", "bar", None).await?; 43 | kv_client.delete("foo", None).await?; 44 | 45 | handle.await?; 46 | 47 | Ok(()) 48 | } 49 | -------------------------------------------------------------------------------- /crates/xlineapi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xlineapi" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["DatenLord "] 6 | description = "Xline RPC API" 7 | license = "Apache-2.0" 8 | readme = "README.md" 9 | repository = "https://github.com/xline-kv/Xline/tree/master/xlineapi" 10 | categories = ["RPC"] 11 | keywords = ["RPC", "Interfaces"] 12 | 13 | [dependencies] 14 | async-trait = "0.1.81" 15 | curp = { path = "../curp" } 16 | curp-external-api = { path = "../curp-external-api" } 17 | itertools = "0.13" 18 | prost = "0.13" 19 | serde = { version = "1.0.204", features = ["derive"] } 20 | thiserror = "1.0.61" 21 | tonic = { version = "0.5.0", package = "madsim-tonic" } 22 | utils = { path = "../utils", features = ["parking_lot"] } 23 | workspace-hack = { version = "0.1", path = "../../workspace-hack" } 24 | 25 | [build-dependencies] 26 | tonic-build = { version = "0.5.0", package = "madsim-tonic-build" } 27 | 28 | [dev-dependencies] 29 | strum = "0.26" 30 | strum_macros = "0.26" 31 | -------------------------------------------------------------------------------- /crates/xlineapi/README.md: -------------------------------------------------------------------------------- 1 | # Xline API 2 | 3 | This crate provides the RPC interfaces for the communication between the Xline server and client. 4 | -------------------------------------------------------------------------------- /crates/xlineapi/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tonic_build::configure() 3 | .type_attribute(".", "#[derive(serde::Deserialize, serde::Serialize)]") 4 | .compile( 5 | &[ 6 | "proto/src/kv.proto", 7 | "proto/src/rpc.proto", 8 | "proto/src/auth.proto", 9 | "proto/src/v3lock.proto", 10 | "proto/src/lease.proto", 11 | "proto/src/xline-command.proto", 12 | "proto/src/xline-error.proto", 13 | ], 14 | &["./proto/src"], 15 | ) 16 | .unwrap_or_else(|e| panic!("Failed to compile proto, error is {:?}", e)); 17 | } 18 | -------------------------------------------------------------------------------- /crates/xlinectl/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xlinectl" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["DatenLord "] 6 | repository = "https://github.com/xline-kv/Xline/tree/master/xlinectl" 7 | description = "Command line client for Xline" 8 | license = "Apache-2.0" 9 | readme = "README.md" 10 | categories = ["Client"] 11 | keywords = ["Client", "CommandLine"] 12 | 13 | [dependencies] 14 | anyhow = "1.0" 15 | clap = "4" 16 | regex = "1.10.5" 17 | serde = { version = "1.0.204", features = ["derive"] } 18 | serde_json = "1.0.125" 19 | shlex = "1.3.0" 20 | tokio = "1" 21 | tonic = { version = "0.5.0", package = "madsim-tonic" } 22 | utils = { path = "../utils" } 23 | workspace-hack = { version = "0.1", path = "../../workspace-hack" } 24 | xline-client = { path = "../xline-client" } 25 | xlineapi = { path = "../xlineapi" } 26 | -------------------------------------------------------------------------------- /crates/xlinectl/src/command/auth/disable.rs: -------------------------------------------------------------------------------- 1 | use clap::{ArgMatches, Command}; 2 | use xline_client::{error::Result, Client}; 3 | 4 | use crate::utils::printer::Printer; 5 | 6 | /// Definition of `disable` command 7 | pub(super) fn command() -> Command { 8 | Command::new("disable").about("disable authentication") 9 | } 10 | 11 | /// Execute the command 12 | pub(super) async fn execute(client: &mut Client, _matches: &ArgMatches) -> Result<()> { 13 | let resp = client.auth_client().auth_disable().await?; 14 | resp.print(); 15 | 16 | Ok(()) 17 | } 18 | -------------------------------------------------------------------------------- /crates/xlinectl/src/command/auth/enable.rs: -------------------------------------------------------------------------------- 1 | use clap::{ArgMatches, Command}; 2 | use xline_client::{error::Result, Client}; 3 | 4 | use crate::utils::printer::Printer; 5 | 6 | /// Definition of `enable` command 7 | pub(super) fn command() -> Command { 8 | Command::new("enable").about("Enable authentication") 9 | } 10 | 11 | /// Execute the command 12 | pub(super) async fn execute(client: &mut Client, _matches: &ArgMatches) -> Result<()> { 13 | let resp = client.auth_client().auth_enable().await?; 14 | resp.print(); 15 | 16 | Ok(()) 17 | } 18 | -------------------------------------------------------------------------------- /crates/xlinectl/src/command/auth/mod.rs: -------------------------------------------------------------------------------- 1 | use clap::{ArgMatches, Command}; 2 | use xline_client::{error::Result, Client}; 3 | 4 | use crate::handle_matches; 5 | 6 | /// Auth disable command 7 | mod disable; 8 | /// Auth enable command 9 | mod enable; 10 | /// Auth status command 11 | mod status; 12 | 13 | /// Definition of `auth` command 14 | pub(crate) fn command() -> Command { 15 | Command::new("auth") 16 | .about("Manage authentication") 17 | .subcommand(enable::command()) 18 | .subcommand(disable::command()) 19 | .subcommand(status::command()) 20 | } 21 | 22 | /// Execute the command 23 | pub(crate) async fn execute(mut client: &mut Client, matches: &ArgMatches) -> Result<()> { 24 | handle_matches!(matches, client, {enable, disable, status}); 25 | 26 | Ok(()) 27 | } 28 | -------------------------------------------------------------------------------- /crates/xlinectl/src/command/auth/status.rs: -------------------------------------------------------------------------------- 1 | use clap::{ArgMatches, Command}; 2 | use xline_client::{error::Result, Client}; 3 | 4 | use crate::utils::printer::Printer; 5 | 6 | /// Definition of `status` command 7 | pub(super) fn command() -> Command { 8 | Command::new("status").about("Status of authentication") 9 | } 10 | 11 | /// Execute the command 12 | pub(super) async fn execute(client: &mut Client, _matches: &ArgMatches) -> Result<()> { 13 | let resp = client.auth_client().auth_status().await?; 14 | resp.print(); 15 | 16 | Ok(()) 17 | } 18 | -------------------------------------------------------------------------------- /crates/xlinectl/src/command/compaction.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use clap::{arg, value_parser, ArgMatches, Command}; 3 | use xline_client::Client; 4 | 5 | use crate::utils::printer::Printer; 6 | 7 | /// Temp type for build a compaction request, indicates `(revision, physical)` 8 | type CompactionRequest = (i64, bool); 9 | 10 | /// Definition of `compaction` command 11 | pub(crate) fn command() -> Command { 12 | Command::new("compaction") 13 | .about("Discards all Xline event history prior to a given revision") 14 | .arg(arg!( "The revision to compact").value_parser(value_parser!(i64))) 15 | .arg(arg!(--physical "To wait for compaction to physically remove all old revisions")) 16 | } 17 | 18 | /// Build request from matches 19 | pub(crate) fn build_request(matches: &ArgMatches) -> CompactionRequest { 20 | let revision = matches.get_one::("revision").expect("required"); 21 | let physical = matches.get_flag("physical"); 22 | 23 | (*revision, physical) 24 | } 25 | 26 | /// Execute the command 27 | pub(crate) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result<()> { 28 | let req = build_request(matches); 29 | let resp = client.kv_client().compact(req.0, req.1).await?; 30 | resp.print(); 31 | 32 | Ok(()) 33 | } 34 | 35 | #[cfg(test)] 36 | mod tests { 37 | use super::*; 38 | use crate::test_case_struct; 39 | 40 | test_case_struct!(CompactionRequest); 41 | 42 | #[test] 43 | fn command_parse_should_be_valid() { 44 | let test_cases = vec![ 45 | TestCase::new(vec!["compaction", "123"], Some((123, false))), 46 | TestCase::new(vec!["compaction", "123", "--physical"], Some((123, true))), 47 | ]; 48 | 49 | for case in test_cases { 50 | case.run_test(); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /crates/xlinectl/src/command/lease/grant.rs: -------------------------------------------------------------------------------- 1 | use clap::{arg, value_parser, ArgMatches, Command}; 2 | use xline_client::{error::Result, Client}; 3 | 4 | use crate::utils::printer::Printer; 5 | 6 | /// Definition of `grant` command 7 | pub(super) fn command() -> Command { 8 | Command::new("grant") 9 | .about("Create a lease with a given TTL") 10 | .arg(arg!( "time to live of the lease").value_parser(value_parser!(i64))) 11 | } 12 | 13 | /// Build request from matches 14 | pub(super) fn build_request(matches: &ArgMatches) -> i64 { 15 | let ttl = matches.get_one::("ttl").expect("required"); 16 | *ttl 17 | } 18 | 19 | /// Execute the command 20 | pub(super) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result<()> { 21 | let ttl = build_request(matches); 22 | let resp = client.lease_client().grant(ttl, None).await?; 23 | resp.print(); 24 | 25 | Ok(()) 26 | } 27 | 28 | #[cfg(test)] 29 | mod tests { 30 | use super::*; 31 | use crate::test_case_struct; 32 | 33 | test_case_struct!(i64); 34 | 35 | #[test] 36 | fn command_parse_should_be_valid() { 37 | let test_cases = vec![TestCase::new(vec!["grant", "100"], Some(100))]; 38 | 39 | for case in test_cases { 40 | case.run_test(); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /crates/xlinectl/src/command/lease/list.rs: -------------------------------------------------------------------------------- 1 | use clap::{ArgMatches, Command}; 2 | use xline_client::{error::Result, Client}; 3 | 4 | use crate::utils::printer::Printer; 5 | 6 | /// Definition of `List` command 7 | pub(super) fn command() -> Command { 8 | Command::new("list").about("List all active leases") 9 | } 10 | 11 | /// Execute the command 12 | pub(super) async fn execute(client: &mut Client, _matches: &ArgMatches) -> Result<()> { 13 | let resp = client.lease_client().leases().await?; 14 | resp.print(); 15 | 16 | Ok(()) 17 | } 18 | -------------------------------------------------------------------------------- /crates/xlinectl/src/command/lease/mod.rs: -------------------------------------------------------------------------------- 1 | use clap::{ArgMatches, Command}; 2 | use xline_client::{error::Result, Client}; 3 | 4 | use crate::handle_matches; 5 | 6 | /// `grant` command 7 | mod grant; 8 | /// `keep_alive` command 9 | pub(crate) mod keep_alive; 10 | /// `list` command 11 | mod list; 12 | /// `revoke` command 13 | mod revoke; 14 | /// `timetolive` command 15 | mod timetolive; 16 | 17 | /// Definition of `lease` command 18 | pub(crate) fn command() -> Command { 19 | Command::new("lease") 20 | .about("Lease related commands") 21 | .subcommand(grant::command()) 22 | .subcommand(keep_alive::command()) 23 | .subcommand(list::command()) 24 | .subcommand(revoke::command()) 25 | .subcommand(timetolive::command()) 26 | } 27 | 28 | /// Get matches and generate request 29 | pub(crate) async fn execute(mut client: &mut Client, matches: &ArgMatches) -> Result<()> { 30 | handle_matches!(matches, client, { grant, keep_alive, list, revoke, timetolive }); 31 | Ok(()) 32 | } 33 | -------------------------------------------------------------------------------- /crates/xlinectl/src/command/lease/revoke.rs: -------------------------------------------------------------------------------- 1 | use clap::{arg, value_parser, ArgMatches, Command}; 2 | use xline_client::{error::Result, Client}; 3 | 4 | use crate::utils::printer::Printer; 5 | 6 | /// Definition of `revoke` command 7 | pub(super) fn command() -> Command { 8 | Command::new("revoke") 9 | .about("Revoke a lease") 10 | .arg(arg!( "Lease Id to revoke").value_parser(value_parser!(i64))) 11 | } 12 | 13 | /// Build request from matches 14 | pub(super) fn build_request(matches: &ArgMatches) -> i64 { 15 | let lease_id = matches.get_one::("leaseId").expect("required"); 16 | *lease_id 17 | } 18 | 19 | /// Execute the command 20 | pub(super) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result<()> { 21 | let req = build_request(matches); 22 | let resp = client.lease_client().revoke(req).await?; 23 | resp.print(); 24 | 25 | Ok(()) 26 | } 27 | 28 | #[cfg(test)] 29 | mod tests { 30 | use super::*; 31 | use crate::test_case_struct; 32 | 33 | test_case_struct!(i64); 34 | 35 | #[test] 36 | fn command_parse_should_be_valid() { 37 | let test_cases = vec![TestCase::new(vec!["revoke", "123"], Some(123))]; 38 | 39 | for case in test_cases { 40 | case.run_test(); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /crates/xlinectl/src/command/lease/timetolive.rs: -------------------------------------------------------------------------------- 1 | use clap::{arg, value_parser, ArgMatches, Command}; 2 | use xline_client::{error::Result, Client}; 3 | 4 | use crate::utils::printer::Printer; 5 | 6 | /// Definition of `timetolive` command 7 | pub(super) fn command() -> Command { 8 | Command::new("timetolive") 9 | .about("Get lease ttl information") 10 | .arg(arg!( "Lease id to get").value_parser(value_parser!(i64))) 11 | } 12 | 13 | /// Build request from matches 14 | pub(super) fn build_request(matches: &ArgMatches) -> i64 { 15 | let lease_id = matches.get_one::("leaseId").expect("required"); 16 | *lease_id 17 | } 18 | 19 | /// Execute the command 20 | pub(super) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result<()> { 21 | let req = build_request(matches); 22 | let resp = client.lease_client().time_to_live(req, false).await?; 23 | resp.print(); 24 | 25 | Ok(()) 26 | } 27 | 28 | #[cfg(test)] 29 | mod tests { 30 | use super::*; 31 | use crate::test_case_struct; 32 | 33 | test_case_struct!(i64); 34 | 35 | #[test] 36 | fn command_parse_should_be_valid() { 37 | let test_cases = vec![TestCase::new(vec!["timetolive", "123"], Some(123))]; 38 | 39 | for case in test_cases { 40 | case.run_test(); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /crates/xlinectl/src/command/member/add.rs: -------------------------------------------------------------------------------- 1 | use clap::{arg, ArgMatches, Command}; 2 | use xline_client::{error::Result, Client}; 3 | 4 | use super::parse_peer_urls; 5 | use crate::utils::printer::Printer; 6 | 7 | /// Temp type for cluster member `add` command, indicates `(peer_urls, is_learner)` 8 | type MemberAddRequest = (Vec, bool); 9 | 10 | /// Definition of `add` command 11 | pub(super) fn command() -> Command { 12 | Command::new("add") 13 | .about("Adds a member into the cluster") 14 | .arg( 15 | arg!( "Comma separated peer URLs for the new member.") 16 | .value_parser(parse_peer_urls), 17 | ) 18 | .arg(arg!(--is_learner "Add as learner")) 19 | } 20 | 21 | /// Build request from matches 22 | pub(super) fn build_request(matches: &ArgMatches) -> MemberAddRequest { 23 | let peer_urls = matches 24 | .get_one::>("peer_urls") 25 | .expect("required"); 26 | let is_learner = matches.get_flag("is_learner"); 27 | 28 | (peer_urls.clone(), is_learner) 29 | } 30 | 31 | /// Execute the command 32 | pub(super) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result<()> { 33 | let request = build_request(matches); 34 | let resp = client 35 | .cluster_client() 36 | .member_add(request.0, request.1) 37 | .await?; 38 | resp.print(); 39 | 40 | Ok(()) 41 | } 42 | 43 | #[cfg(test)] 44 | mod tests { 45 | use super::*; 46 | use crate::test_case_struct; 47 | 48 | test_case_struct!(MemberAddRequest); 49 | 50 | #[test] 51 | fn command_parse_should_be_valid() { 52 | let test_cases = vec![ 53 | TestCase::new( 54 | vec!["add", "127.0.0.1:2379", "--is_learner"], 55 | Some((["127.0.0.1:2379".to_owned()].into(), true)), 56 | ), 57 | TestCase::new( 58 | vec!["add", "127.0.0.1:2379,127.0.0.1:2380"], 59 | Some(( 60 | ["127.0.0.1:2379".to_owned(), "127.0.0.1:2380".to_owned()].into(), 61 | false, 62 | )), 63 | ), 64 | ]; 65 | 66 | for case in test_cases { 67 | case.run_test(); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /crates/xlinectl/src/command/member/list.rs: -------------------------------------------------------------------------------- 1 | use clap::{arg, ArgMatches, Command}; 2 | use xline_client::{error::Result, Client}; 3 | 4 | use crate::utils::printer::Printer; 5 | 6 | /// Definition of `list` command 7 | pub(super) fn command() -> Command { 8 | Command::new("list") 9 | .about("Lists all members in the cluster") 10 | .arg(arg!(--linearizable "To use linearizable fetch")) 11 | } 12 | 13 | /// Build request from matches 14 | pub(super) fn build_request(matches: &ArgMatches) -> bool { 15 | matches.get_flag("linearizable") 16 | } 17 | 18 | /// Execute the command 19 | pub(super) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result<()> { 20 | let request = build_request(matches); 21 | let resp = client.cluster_client().member_list(request).await?; 22 | resp.print(); 23 | 24 | Ok(()) 25 | } 26 | 27 | #[cfg(test)] 28 | mod tests { 29 | use super::*; 30 | use crate::test_case_struct; 31 | 32 | test_case_struct!(bool); 33 | 34 | #[test] 35 | fn command_parse_should_be_valid() { 36 | let test_cases = vec![ 37 | TestCase::new(vec!["list", "--linearizable"], Some(true)), 38 | TestCase::new(vec!["list"], Some(false)), 39 | ]; 40 | 41 | for case in test_cases { 42 | case.run_test(); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /crates/xlinectl/src/command/member/mod.rs: -------------------------------------------------------------------------------- 1 | use clap::{ArgMatches, Command}; 2 | use xline_client::{error::Result, Client}; 3 | 4 | use crate::handle_matches; 5 | 6 | /// `add` command 7 | mod add; 8 | /// `list` command 9 | mod list; 10 | /// `promote` command 11 | mod promote; 12 | /// `remove` command 13 | mod remove; 14 | /// `update` command 15 | mod update; 16 | 17 | /// Definition of `lease` command 18 | pub(crate) fn command() -> Command { 19 | Command::new("member") 20 | .about("Member related commands") 21 | .subcommand(add::command()) 22 | .subcommand(update::command()) 23 | .subcommand(list::command()) 24 | .subcommand(remove::command()) 25 | .subcommand(promote::command()) 26 | } 27 | 28 | /// Get matches and generate request 29 | pub(crate) async fn execute(mut client: &mut Client, matches: &ArgMatches) -> Result<()> { 30 | handle_matches!(matches, client, { add, update, list, remove, promote }); 31 | Ok(()) 32 | } 33 | 34 | #[allow(clippy::unnecessary_wraps)] // The Result is required by clap 35 | /// Parse comma separated peer urls 36 | pub(super) fn parse_peer_urls(arg: &str) -> Result> { 37 | Ok(arg.split(',').map(ToOwned::to_owned).collect()) 38 | } 39 | -------------------------------------------------------------------------------- /crates/xlinectl/src/command/member/promote.rs: -------------------------------------------------------------------------------- 1 | use clap::{arg, value_parser, ArgMatches, Command}; 2 | use xline_client::{error::Result, Client}; 3 | 4 | use crate::utils::printer::Printer; 5 | 6 | /// Definition of `promote` command 7 | pub(super) fn command() -> Command { 8 | Command::new("promote") 9 | .about("Promotes a member in the cluster") 10 | .arg(arg!( "The member ID").value_parser(value_parser!(u64))) 11 | } 12 | 13 | /// Build request from matches 14 | pub(super) fn build_request(matches: &ArgMatches) -> u64 { 15 | *matches.get_one::("ID").expect("required") 16 | } 17 | 18 | /// Execute the command 19 | pub(super) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result<()> { 20 | let request = build_request(matches); 21 | let resp = client.cluster_client().member_promote(request).await?; 22 | resp.print(); 23 | 24 | Ok(()) 25 | } 26 | 27 | #[cfg(test)] 28 | mod tests { 29 | use super::*; 30 | use crate::test_case_struct; 31 | 32 | test_case_struct!(u64); 33 | 34 | #[test] 35 | fn command_parse_should_be_valid() { 36 | let test_cases = vec![TestCase::new(vec!["remove", "1"], Some(1))]; 37 | 38 | for case in test_cases { 39 | case.run_test(); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /crates/xlinectl/src/command/member/remove.rs: -------------------------------------------------------------------------------- 1 | use clap::{arg, value_parser, ArgMatches, Command}; 2 | use xline_client::{error::Result, Client}; 3 | 4 | use crate::utils::printer::Printer; 5 | 6 | /// Definition of `remove` command 7 | pub(super) fn command() -> Command { 8 | Command::new("remove") 9 | .about("Removes a member from the cluster") 10 | .arg(arg!( "The member ID").value_parser(value_parser!(u64))) 11 | } 12 | 13 | /// Build request from matches 14 | pub(super) fn build_request(matches: &ArgMatches) -> u64 { 15 | *matches.get_one::("ID").expect("required") 16 | } 17 | 18 | /// Execute the command 19 | pub(super) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result<()> { 20 | let request = build_request(matches); 21 | let resp = client.cluster_client().member_remove(request).await?; 22 | resp.print(); 23 | 24 | Ok(()) 25 | } 26 | 27 | #[cfg(test)] 28 | mod tests { 29 | use super::*; 30 | use crate::test_case_struct; 31 | 32 | test_case_struct!(u64); 33 | 34 | #[test] 35 | fn command_parse_should_be_valid() { 36 | let test_cases = vec![TestCase::new(vec!["remove", "1"], Some(1))]; 37 | 38 | for case in test_cases { 39 | case.run_test(); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /crates/xlinectl/src/command/member/update.rs: -------------------------------------------------------------------------------- 1 | use clap::{arg, value_parser, ArgMatches, Command}; 2 | use xline_client::{error::Result, Client}; 3 | 4 | use super::parse_peer_urls; 5 | use crate::utils::printer::Printer; 6 | 7 | /// Temp type for request and testing, indicates `(id, peer_urls)` 8 | type MemberUpdateRequest = (u64, Vec); 9 | 10 | /// Definition of `update` command 11 | pub(super) fn command() -> Command { 12 | Command::new("update") 13 | .about("Updates a member in the cluster") 14 | .arg(arg!( "The member ID").value_parser(value_parser!(u64))) 15 | .arg( 16 | arg!( "Comma separated peer URLs for the new member.") 17 | .value_parser(parse_peer_urls), 18 | ) 19 | } 20 | 21 | /// Build request from matches 22 | pub(super) fn build_request(matches: &ArgMatches) -> MemberUpdateRequest { 23 | let member_id = matches.get_one::("ID").expect("required"); 24 | let peer_urls = matches 25 | .get_one::>("peer_urls") 26 | .expect("required"); 27 | 28 | (*member_id, peer_urls.clone()) 29 | } 30 | 31 | /// Execute the command 32 | pub(super) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result<()> { 33 | let request = build_request(matches); 34 | let resp = client 35 | .cluster_client() 36 | .member_update(request.0, request.1) 37 | .await?; 38 | resp.print(); 39 | 40 | Ok(()) 41 | } 42 | 43 | #[cfg(test)] 44 | mod tests { 45 | use super::*; 46 | use crate::test_case_struct; 47 | 48 | test_case_struct!(MemberUpdateRequest); 49 | 50 | #[test] 51 | fn command_parse_should_be_valid() { 52 | let test_cases = vec![ 53 | TestCase::new( 54 | vec!["update", "1", "127.0.0.1:2379"], 55 | Some((1, ["127.0.0.1:2379".to_owned()].into())), 56 | ), 57 | TestCase::new( 58 | vec!["update", "2", "127.0.0.1:2379,127.0.0.1:2380"], 59 | Some(( 60 | 2, 61 | ["127.0.0.1:2379".to_owned(), "127.0.0.1:2380".to_owned()].into(), 62 | )), 63 | ), 64 | ]; 65 | 66 | for case in test_cases { 67 | case.run_test(); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /crates/xlinectl/src/command/mod.rs: -------------------------------------------------------------------------------- 1 | /// Auth command 2 | pub(crate) mod auth; 3 | /// Compaction command 4 | pub(crate) mod compaction; 5 | /// Delete command 6 | pub(crate) mod delete; 7 | /// Get command 8 | pub(crate) mod get; 9 | /// Lease command 10 | pub(crate) mod lease; 11 | /// Lock command 12 | pub(crate) mod lock; 13 | /// Member command 14 | pub(crate) mod member; 15 | /// Put command 16 | pub(crate) mod put; 17 | /// Role command 18 | pub(crate) mod role; 19 | /// Snapshot command 20 | pub(crate) mod snapshot; 21 | /// Txn command 22 | pub(crate) mod txn; 23 | /// User command 24 | pub(crate) mod user; 25 | /// Watch command 26 | pub(crate) mod watch; 27 | -------------------------------------------------------------------------------- /crates/xlinectl/src/command/role/add.rs: -------------------------------------------------------------------------------- 1 | use clap::{arg, ArgMatches, Command}; 2 | use xline_client::{error::Result, Client}; 3 | 4 | use crate::utils::printer::Printer; 5 | 6 | /// Definition of `add` command 7 | pub(super) fn command() -> Command { 8 | Command::new("add") 9 | .about("Create a new role") 10 | .arg(arg!( "The name of the role")) 11 | } 12 | 13 | /// Build request from matches 14 | /// 15 | /// # Returns 16 | /// 17 | /// name of the role 18 | pub(super) fn build_request(matches: &ArgMatches) -> String { 19 | let name = matches.get_one::("name").expect("required"); 20 | name.into() 21 | } 22 | 23 | /// Execute the command 24 | pub(super) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result<()> { 25 | let name = build_request(matches); 26 | let resp = client.auth_client().role_add(name).await?; 27 | resp.print(); 28 | Ok(()) 29 | } 30 | 31 | #[cfg(test)] 32 | mod tests { 33 | use super::*; 34 | use crate::test_case_struct; 35 | 36 | test_case_struct!(String); 37 | 38 | #[test] 39 | fn command_parse_should_be_valid() { 40 | let test_cases = vec![TestCase::new(vec!["add", "Admin"], Some("Admin".into()))]; 41 | 42 | for case in test_cases { 43 | case.run_test(); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /crates/xlinectl/src/command/role/delete.rs: -------------------------------------------------------------------------------- 1 | use clap::{arg, ArgMatches, Command}; 2 | use xline_client::{error::Result, Client}; 3 | 4 | use crate::utils::printer::Printer; 5 | 6 | /// Definition of `delete` command 7 | pub(super) fn command() -> Command { 8 | Command::new("delete") 9 | .about("delete a role") 10 | .arg(arg!( "The name of the role")) 11 | } 12 | 13 | /// Build request from matches 14 | /// 15 | /// Returns the name of the role to be deleted 16 | pub(super) fn build_request(matches: &ArgMatches) -> String { 17 | let name = matches.get_one::("name").expect("required"); 18 | name.to_owned() 19 | } 20 | 21 | /// Execute the command 22 | pub(super) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result<()> { 23 | let req = build_request(matches); 24 | let resp = client.auth_client().role_delete(req).await?; 25 | resp.print(); 26 | 27 | Ok(()) 28 | } 29 | 30 | #[cfg(test)] 31 | mod tests { 32 | use super::*; 33 | use crate::test_case_struct; 34 | 35 | test_case_struct!(String); 36 | 37 | #[test] 38 | fn command_parse_should_be_valid() { 39 | let test_cases = vec![TestCase::new(vec!["delete", "Admin"], Some("Admin".into()))]; 40 | 41 | for case in test_cases { 42 | case.run_test(); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /crates/xlinectl/src/command/role/get.rs: -------------------------------------------------------------------------------- 1 | use clap::{arg, ArgMatches, Command}; 2 | use xline_client::{error::Result, Client}; 3 | 4 | use crate::utils::printer::Printer; 5 | 6 | /// Definition of `get` command 7 | pub(super) fn command() -> Command { 8 | Command::new("get") 9 | .about("Get a new role") 10 | .arg(arg!( "The name of the role")) 11 | } 12 | 13 | /// Build request from matches 14 | pub(super) fn build_request(matches: &ArgMatches) -> String { 15 | let name = matches.get_one::("name").expect("required"); 16 | name.to_owned() 17 | } 18 | 19 | /// Execute the command 20 | pub(super) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result<()> { 21 | let req = build_request(matches); 22 | let resp = client.auth_client().role_get(req).await?; 23 | resp.print(); 24 | 25 | Ok(()) 26 | } 27 | 28 | #[cfg(test)] 29 | mod tests { 30 | use super::*; 31 | use crate::test_case_struct; 32 | 33 | test_case_struct!(String); 34 | 35 | #[test] 36 | fn command_parse_should_be_valid() { 37 | let test_cases = vec![TestCase::new(vec!["get", "Admin"], Some("Admin".into()))]; 38 | 39 | for case in test_cases { 40 | case.run_test(); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /crates/xlinectl/src/command/role/list.rs: -------------------------------------------------------------------------------- 1 | use clap::{ArgMatches, Command}; 2 | use xline_client::{error::Result, Client}; 3 | 4 | use crate::utils::printer::Printer; 5 | 6 | /// Definition of `list` command 7 | pub(super) fn command() -> Command { 8 | Command::new("list").about("List all roles") 9 | } 10 | 11 | /// Execute the command 12 | pub(super) async fn execute(client: &mut Client, _matches: &ArgMatches) -> Result<()> { 13 | let resp = client.auth_client().role_list().await?; 14 | resp.print(); 15 | 16 | Ok(()) 17 | } 18 | -------------------------------------------------------------------------------- /crates/xlinectl/src/command/role/mod.rs: -------------------------------------------------------------------------------- 1 | use clap::{ArgMatches, Command}; 2 | use xline_client::{error::Result, Client}; 3 | 4 | use crate::handle_matches; 5 | 6 | /// Role add command 7 | pub(super) mod add; 8 | /// Role delete command 9 | pub(super) mod delete; 10 | /// Role get command 11 | pub(super) mod get; 12 | /// Role grant permission command 13 | pub(super) mod grant_perm; 14 | /// Role list command 15 | pub(super) mod list; 16 | /// Role revoke permission command 17 | pub(super) mod revoke_perm; 18 | 19 | /// Definition of `auth` command 20 | pub(crate) fn command() -> Command { 21 | Command::new("role") 22 | .about("Role related commands") 23 | .subcommand(add::command()) 24 | .subcommand(delete::command()) 25 | .subcommand(get::command()) 26 | .subcommand(grant_perm::command()) 27 | .subcommand(list::command()) 28 | .subcommand(revoke_perm::command()) 29 | } 30 | 31 | /// Execute the command 32 | pub(crate) async fn execute(mut client: &mut Client, matches: &ArgMatches) -> Result<()> { 33 | handle_matches!(matches, client, { add, delete, get, grant_perm, list, revoke_perm }); 34 | 35 | Ok(()) 36 | } 37 | -------------------------------------------------------------------------------- /crates/xlinectl/src/command/role/revoke_perm.rs: -------------------------------------------------------------------------------- 1 | use clap::{arg, ArgMatches, Command}; 2 | use xline_client::{error::Result, types::range_end::RangeOption, Client}; 3 | 4 | use crate::utils::printer::Printer; 5 | 6 | /// Temp request type for `revoke_perm` command 7 | type AuthRoleRevokePermissionRequest = (String, Vec, Option); 8 | 9 | /// Definition of `revoke_perm` command 10 | pub(super) fn command() -> Command { 11 | Command::new("revoke_perm") 12 | .about("Revoke permission from a role") 13 | .arg(arg!( "The name of the role")) 14 | .arg(arg!( "The Key")) 15 | .arg(arg!([range_end] "Range end of the key")) 16 | } 17 | 18 | /// Build request from matches 19 | pub(super) fn build_request(matches: &ArgMatches) -> AuthRoleRevokePermissionRequest { 20 | let name = matches.get_one::("name").expect("required"); 21 | let key = matches.get_one::("key").expect("required"); 22 | let range_end = matches.get_one::("range_end"); 23 | 24 | let key = key.as_bytes().to_vec(); 25 | let mut option = None; 26 | 27 | if let Some(range_end) = range_end { 28 | option = Some(RangeOption::RangeEnd(range_end.as_bytes().to_vec())); 29 | }; 30 | 31 | (name.into(), key, option) 32 | } 33 | 34 | /// Execute the command 35 | pub(super) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result<()> { 36 | let req = build_request(matches); 37 | let resp = client 38 | .auth_client() 39 | .role_revoke_permission(req.0, req.1, req.2) 40 | .await?; 41 | resp.print(); 42 | 43 | Ok(()) 44 | } 45 | 46 | #[cfg(test)] 47 | mod tests { 48 | use super::*; 49 | use crate::test_case_struct; 50 | 51 | test_case_struct!(AuthRoleRevokePermissionRequest); 52 | 53 | #[test] 54 | fn command_parse_should_be_valid() { 55 | let test_cases = vec![ 56 | TestCase::new( 57 | vec!["revoke_perm", "Admin", "key1", "key2"], 58 | Some(( 59 | "Admin".into(), 60 | "key1".into(), 61 | Some(RangeOption::RangeEnd("key2".into())), 62 | )), 63 | ), 64 | TestCase::new( 65 | vec!["revoke_perm", "Admin", "key3"], 66 | Some(("Admin".into(), "key3".into(), None)), 67 | ), 68 | ]; 69 | 70 | for case in test_cases { 71 | case.run_test(); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /crates/xlinectl/src/command/snapshot.rs: -------------------------------------------------------------------------------- 1 | use std::{fs::File, io::Write, path::PathBuf}; 2 | 3 | use clap::{arg, ArgMatches, Command}; 4 | use xline_client::{ 5 | error::{Result, XlineClientError}, 6 | Client, 7 | }; 8 | 9 | /// Definition of `snapshot` command 10 | pub(crate) fn command() -> Command { 11 | Command::new("snapshot") 12 | .about("get snapshots of xline nodes") 13 | .subcommand( 14 | Command::new("save") 15 | .about("save snapshot") 16 | .arg(arg!( "save snapshot to the give filename")), 17 | ) 18 | } 19 | 20 | /// Execute the command 21 | pub(crate) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result<()> { 22 | if let Some(("save", sub_matches)) = matches.subcommand() { 23 | let filename = sub_matches.get_one::("filename").expect("required"); 24 | let path = PathBuf::from(filename); 25 | let mut resp = client.maintenance_client().snapshot().await?; 26 | 27 | if path.exists() || path.is_dir() { 28 | eprintln!("file exist: {filename}"); 29 | return Ok(()); 30 | } 31 | 32 | let mut file = 33 | File::create(path).map_err(|err| XlineClientError::IoError(err.to_string()))?; 34 | 35 | let mut all = Vec::new(); 36 | while let Some(data) = resp.message().await? { 37 | all.extend_from_slice(&data.blob); 38 | } 39 | 40 | file.write_all(&all) 41 | .map_err(|err| XlineClientError::IoError(err.to_string()))?; 42 | 43 | println!("snapshot saved to: {filename}"); 44 | } 45 | 46 | Ok(()) 47 | } 48 | -------------------------------------------------------------------------------- /crates/xlinectl/src/command/user/delete.rs: -------------------------------------------------------------------------------- 1 | use clap::{arg, ArgMatches, Command}; 2 | use xline_client::{error::Result, Client}; 3 | 4 | use crate::utils::printer::Printer; 5 | 6 | /// Definition of `delete` command 7 | pub(super) fn command() -> Command { 8 | Command::new("delete") 9 | .about("Delete a user") 10 | .arg(arg!( "The name of the user")) 11 | } 12 | 13 | /// Build request from matches 14 | pub(super) fn build_request(matches: &ArgMatches) -> String { 15 | let name = matches.get_one::("name").expect("required"); 16 | name.to_owned() 17 | } 18 | 19 | /// Execute the command 20 | pub(super) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result<()> { 21 | let req = build_request(matches); 22 | let resp = client.auth_client().user_delete(req).await?; 23 | resp.print(); 24 | 25 | Ok(()) 26 | } 27 | 28 | #[cfg(test)] 29 | mod tests { 30 | use super::*; 31 | use crate::test_case_struct; 32 | 33 | test_case_struct!(String); 34 | 35 | #[test] 36 | fn command_parse_should_be_valid() { 37 | let test_cases = vec![TestCase::new( 38 | vec!["delete", "JohnDoe"], 39 | Some("JohnDoe".into()), 40 | )]; 41 | 42 | for case in test_cases { 43 | case.run_test(); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /crates/xlinectl/src/command/user/get.rs: -------------------------------------------------------------------------------- 1 | use clap::{arg, ArgMatches, Command}; 2 | use xline_client::{error::Result, Client}; 3 | 4 | use crate::utils::printer::Printer; 5 | 6 | /// Definition of `get` command 7 | pub(super) fn command() -> Command { 8 | Command::new("get") 9 | .about("Get a new user") 10 | .arg(arg!( "The name of the user")) 11 | .arg(arg!(--detail "Show permissions of roles granted to the user")) 12 | } 13 | 14 | /// Build request from matches 15 | pub(super) fn build_request(matches: &ArgMatches) -> String { 16 | let name = matches.get_one::("name").expect("required"); 17 | name.to_owned() 18 | } 19 | 20 | /// Execute the command 21 | pub(super) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result<()> { 22 | let detail = matches.get_flag("detail"); 23 | 24 | let req = build_request(matches); 25 | 26 | let resp = client.auth_client().user_get(req).await?; 27 | 28 | if detail { 29 | for role in resp.roles { 30 | println!("{role}"); 31 | let resp_role_get = client.auth_client().role_get(role).await?; 32 | resp_role_get.print(); 33 | } 34 | } else { 35 | resp.print(); 36 | } 37 | 38 | Ok(()) 39 | } 40 | 41 | #[cfg(test)] 42 | mod tests { 43 | use super::*; 44 | use crate::test_case_struct; 45 | 46 | test_case_struct!(String); 47 | 48 | #[test] 49 | fn command_parse_should_be_valid() { 50 | let test_cases = vec![ 51 | TestCase::new(vec!["get", "JohnDoe"], Some("JohnDoe".into())), 52 | TestCase::new( 53 | vec!["get", "--detail", "JaneSmith"], 54 | Some("JaneSmith".into()), 55 | ), 56 | ]; 57 | 58 | for case in test_cases { 59 | case.run_test(); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /crates/xlinectl/src/command/user/grant_role.rs: -------------------------------------------------------------------------------- 1 | use clap::{arg, ArgMatches, Command}; 2 | use xline_client::{error::Result, Client}; 3 | 4 | use crate::utils::printer::Printer; 5 | 6 | /// Temporary struct for testing, indicates `(user_name, role)` 7 | type AuthUserGrantRoleRequest = (String, String); 8 | 9 | /// Definition of `grant_role` command 10 | pub(super) fn command() -> Command { 11 | Command::new("grant_role") 12 | .about("Grant role to a user") 13 | .arg(arg!( "The name of the user")) 14 | .arg(arg!( "The name of the role")) 15 | } 16 | 17 | /// Build request from matches 18 | pub(super) fn build_request(matches: &ArgMatches) -> AuthUserGrantRoleRequest { 19 | let name = matches.get_one::("name").expect("required"); 20 | let role = matches.get_one::("role").expect("required"); 21 | (name.into(), role.into()) 22 | } 23 | 24 | /// Execute the command 25 | pub(super) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result<()> { 26 | let req = build_request(matches); 27 | let resp = client.auth_client().user_grant_role(req.0, req.1).await?; 28 | resp.print(); 29 | 30 | Ok(()) 31 | } 32 | 33 | #[cfg(test)] 34 | mod tests { 35 | use super::*; 36 | use crate::test_case_struct; 37 | 38 | test_case_struct!(AuthUserGrantRoleRequest); 39 | 40 | #[test] 41 | fn command_parse_should_be_valid() { 42 | let test_cases = vec![TestCase::new( 43 | vec!["grant_role", "JohnDoe", "Admin"], 44 | Some(("JohnDoe".into(), "Admin".into())), 45 | )]; 46 | 47 | for case in test_cases { 48 | case.run_test(); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /crates/xlinectl/src/command/user/list.rs: -------------------------------------------------------------------------------- 1 | use clap::{ArgMatches, Command}; 2 | use xline_client::{error::Result, Client}; 3 | 4 | use crate::utils::printer::Printer; 5 | 6 | /// Definition of `list` command 7 | pub(super) fn command() -> Command { 8 | Command::new("list").about("List all users") 9 | } 10 | 11 | /// Execute the command 12 | pub(super) async fn execute(client: &mut Client, _matches: &ArgMatches) -> Result<()> { 13 | let resp = client.auth_client().user_list().await?; 14 | resp.print(); 15 | 16 | Ok(()) 17 | } 18 | -------------------------------------------------------------------------------- /crates/xlinectl/src/command/user/mod.rs: -------------------------------------------------------------------------------- 1 | use clap::{ArgMatches, Command}; 2 | use xline_client::{error::Result, Client}; 3 | 4 | use crate::handle_matches; 5 | 6 | /// User add command 7 | mod add; 8 | /// User delete command 9 | mod delete; 10 | /// User get command 11 | mod get; 12 | /// User grant role command 13 | mod grant_role; 14 | /// User list command 15 | mod list; 16 | /// User change password command 17 | mod passwd; 18 | /// User revoke role command 19 | mod revoke_role; 20 | 21 | /// Definition of `auth` command 22 | pub(crate) fn command() -> Command { 23 | Command::new("user") 24 | .about("User related commands") 25 | .subcommand(add::command()) 26 | .subcommand(delete::command()) 27 | .subcommand(get::command()) 28 | .subcommand(grant_role::command()) 29 | .subcommand(list::command()) 30 | .subcommand(passwd::command()) 31 | .subcommand(revoke_role::command()) 32 | } 33 | 34 | /// Execute the command 35 | pub(crate) async fn execute(mut client: &mut Client, matches: &ArgMatches) -> Result<()> { 36 | handle_matches!(matches, client, { add, delete, get, grant_role, list, passwd, revoke_role }); 37 | 38 | Ok(()) 39 | } 40 | -------------------------------------------------------------------------------- /crates/xlinectl/src/command/user/passwd.rs: -------------------------------------------------------------------------------- 1 | use clap::{arg, ArgMatches, Command}; 2 | use xline_client::{error::Result, Client}; 3 | 4 | use crate::utils::printer::Printer; 5 | 6 | /// Temporary request for changing password. 0 is name, 1 is password 7 | type AuthUserChangePasswordRequest = (String, String); 8 | 9 | /// Definition of `passwd` command 10 | // TODO: interactive mode 11 | pub(super) fn command() -> Command { 12 | Command::new("passwd") 13 | .about("Change the password of a user") 14 | .arg(arg!( "The name of the user")) 15 | .arg(arg!( "Password to change")) 16 | } 17 | 18 | /// Build request from matches 19 | pub(super) fn build_request(matches: &ArgMatches) -> AuthUserChangePasswordRequest { 20 | let name = matches.get_one::("name").expect("required"); 21 | let password = matches.get_one::("password").expect("required"); 22 | (name.to_owned(), password.to_owned()) 23 | } 24 | 25 | /// Execute the command 26 | pub(super) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result<()> { 27 | let req = build_request(matches); 28 | let resp = client 29 | .auth_client() 30 | .user_change_password(req.0, req.1) 31 | .await?; 32 | resp.print(); 33 | 34 | Ok(()) 35 | } 36 | 37 | #[cfg(test)] 38 | mod tests { 39 | use super::*; 40 | use crate::test_case_struct; 41 | 42 | test_case_struct!(AuthUserChangePasswordRequest); 43 | 44 | #[test] 45 | fn command_parse_should_be_valid() { 46 | let test_cases = vec![TestCase::new( 47 | vec!["passwd", "JohnDoe", "new_password"], 48 | Some(("JohnDoe".into(), "new_password".into())), 49 | )]; 50 | 51 | for case in test_cases { 52 | case.run_test(); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /crates/xlinectl/src/command/user/revoke_role.rs: -------------------------------------------------------------------------------- 1 | use clap::{arg, ArgMatches, Command}; 2 | use xline_client::{error::Result, Client}; 3 | 4 | use crate::utils::printer::Printer; 5 | 6 | /// Temporary struct for testing, indicates `(user_name, role)` 7 | type AuthUserRevokeRoleRequest = (String, String); 8 | 9 | /// Definition of `revoke_role` command 10 | pub(super) fn command() -> Command { 11 | Command::new("revoke_role") 12 | .about("Revoke role from a user") 13 | .arg(arg!( "The name of the user")) 14 | .arg(arg!( "The name of the role")) 15 | } 16 | 17 | /// Build request from matches 18 | pub(super) fn build_request(matches: &ArgMatches) -> AuthUserRevokeRoleRequest { 19 | let name = matches.get_one::("name").expect("required"); 20 | let role = matches.get_one::("role").expect("required"); 21 | (name.to_owned(), role.to_owned()) 22 | } 23 | 24 | /// Execute the command 25 | pub(super) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result<()> { 26 | let req = build_request(matches); 27 | let resp = client.auth_client().user_revoke_role(req.0, req.1).await?; 28 | resp.print(); 29 | 30 | Ok(()) 31 | } 32 | 33 | #[cfg(test)] 34 | mod tests { 35 | use super::*; 36 | use crate::test_case_struct; 37 | 38 | test_case_struct!(AuthUserRevokeRoleRequest); 39 | 40 | #[test] 41 | fn command_parse_should_be_valid() { 42 | let test_cases = vec![TestCase::new( 43 | vec!["revoke_role", "JohnDoe", "Admin"], 44 | Some(("JohnDoe".to_owned(), "Admin".to_owned())), 45 | )]; 46 | 47 | for case in test_cases { 48 | case.run_test(); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /crates/xlinectl/src/utils/macros.rs: -------------------------------------------------------------------------------- 1 | /// Generate match handler for each command 2 | #[macro_export] 3 | macro_rules! handle_matches { 4 | ($matches:ident, $client:ident, { $($cmd:ident),* }) => { 5 | match $matches.subcommand() { 6 | $(Some((stringify!($cmd), sub_matches)) => { 7 | $cmd::execute(&mut $client, sub_matches).await?; 8 | })* 9 | _ => {}, 10 | } 11 | }; 12 | } 13 | 14 | /// Generate `TestCase` struct 15 | #[macro_export] 16 | macro_rules! test_case_struct { 17 | ($req:ident) => { 18 | struct TestCase { 19 | arg: Vec<&'static str>, 20 | req: Option<$req>, 21 | } 22 | 23 | impl TestCase { 24 | fn new(arg: Vec<&'static str>, req: Option<$req>) -> TestCase { 25 | TestCase { arg, req } 26 | } 27 | 28 | fn run_test(&self) { 29 | let matches = match command().try_get_matches_from(self.arg.clone()) { 30 | Ok(matches) => matches, 31 | Err(e) => { 32 | assert!( 33 | self.req.is_none(), 34 | "the arg {:?} is invalid, err: {}", 35 | self.arg, 36 | e 37 | ); 38 | return; 39 | } 40 | }; 41 | let req = build_request(&matches); 42 | assert_eq!(Some(req), self.req); 43 | } 44 | } 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /crates/xlinectl/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | /// Macro utils 2 | pub(crate) mod macros; 3 | /// Parser util 4 | pub(crate) mod parser; 5 | /// Printer of common response types 6 | pub(crate) mod printer; 7 | -------------------------------------------------------------------------------- /crates/xlinectl/src/utils/parser.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{bail, Result}; 2 | use clap::ArgMatches; 3 | 4 | /// Parser user name and password 5 | pub(crate) fn parse_user(matches: &ArgMatches) -> Result> { 6 | let user_pw = matches.get_one::("user"); 7 | if let Some(user_pw) = user_pw { 8 | // try get password from `--user` first 9 | let mut split = user_pw.split(':'); 10 | let user = split.next().map_or_else( 11 | || unreachable!("the string should exist"), 12 | ToOwned::to_owned, 13 | ); 14 | let passwd = split.next().map(ToOwned::to_owned); 15 | let password_opt = matches.get_one::("password"); 16 | 17 | if let Some(passwd) = passwd { 18 | if password_opt.is_some() { 19 | bail!( 20 | "Password already set in `--user`, please remove it from `--password`" 21 | .to_owned(), 22 | ); 23 | } 24 | Ok(Some((user, passwd))) 25 | } else { 26 | let Some(password) = password_opt else { 27 | bail!("Password not set in `--user`, please set it in `--password`".to_owned()); 28 | }; 29 | Ok(Some((user, password.clone()))) 30 | } 31 | } else { 32 | Ok(None) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /crates/xlineutl/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xlineutl" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["DatenLord "] 6 | repository = "https://github.com/xline-kv/Xline/tree/master/xlinectl" 7 | description = "Command line client for Xline" 8 | license = "Apache-2.0" 9 | readme = "README.md" 10 | categories = ["Client"] 11 | keywords = ["Client", "CommandLine"] 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [dependencies] 15 | anyhow = "1.0" 16 | clap = "4" 17 | crc32fast = "1.4.2" 18 | engine = { path = "../engine" } 19 | serde = { version = "1.0.204", features = ["derive"] } 20 | serde_json = "1.0.125" 21 | tempfile = "3.12.0" 22 | tokio = "1" 23 | utils = { path = "../utils" } 24 | workspace-hack = { version = "0.1", path = "../../workspace-hack" } 25 | xline = { path = "../xline" } 26 | -------------------------------------------------------------------------------- /crates/xlineutl/README.md: -------------------------------------------------------------------------------- 1 | # xlineutl 2 | 3 | This crate provides a command line utility for Xline. 4 | 5 | ## Snapshot command 6 | 7 | ### Restore 8 | 9 | Restore xline snapshot from a snapshot file 10 | 11 | #### Usage 12 | 13 | ```bash 14 | restore [options] 15 | ``` 16 | 17 | #### Options 18 | 19 | - `--data-dir` -- path to the output data directory 20 | 21 | #### Examples 22 | 23 | ```bash 24 | # restore snapshot to data dir 25 | ./xlineutl snapshot restore /path/to/snapshot --data-dir /path/to/target/dir 26 | ``` 27 | -------------------------------------------------------------------------------- /crates/xlineutl/src/command/mod.rs: -------------------------------------------------------------------------------- 1 | /// Snapshot command 2 | pub(super) mod snapshot; 3 | -------------------------------------------------------------------------------- /crates/xlineutl/src/printer.rs: -------------------------------------------------------------------------------- 1 | use std::sync::OnceLock; 2 | 3 | use serde::Serialize; 4 | 5 | /// The global printer type config 6 | static PRINTER_TYPE: OnceLock = OnceLock::new(); 7 | 8 | /// The type of the Printer 9 | pub(crate) enum PrinterType { 10 | /// Simple printer, which print simplified result 11 | Simple, 12 | /// Filed printer, which print every fields of the result 13 | Field, 14 | /// JSON printer, which print in JSON format 15 | Json, 16 | } 17 | 18 | /// Set the type of the printer 19 | pub(crate) fn set_printer_type(printer_type: PrinterType) { 20 | let _ignore = PRINTER_TYPE.get_or_init(|| printer_type); 21 | } 22 | 23 | /// The printer implementation trait 24 | pub(crate) trait Printer: Serialize { 25 | /// Print the simplified result 26 | fn simple(&self); 27 | /// Print every fields of the result 28 | fn field(&self); 29 | /// Print the result in JSON format 30 | fn json(&self) { 31 | println!( 32 | "{}", 33 | serde_json::to_string_pretty(self).expect("failed to serialize result") 34 | ); 35 | } 36 | /// Print according to the config set 37 | fn print(&self) { 38 | match *PRINTER_TYPE 39 | .get() 40 | .unwrap_or_else(|| unreachable!("the printer type should be initialized")) 41 | { 42 | PrinterType::Simple => self.simple(), 43 | PrinterType::Field => self.field(), 44 | PrinterType::Json => self.json(), 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /doc/img/cncf-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xline-kv/Xline/71479a4fabdd12fe67eec0bee95e652976937541/doc/img/cncf-logo.png -------------------------------------------------------------------------------- /doc/img/prom_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xline-kv/Xline/71479a4fabdd12fe67eec0bee95e652976937541/doc/img/prom_demo.png -------------------------------------------------------------------------------- /doc/img/xline-horizontal-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xline-kv/Xline/71479a4fabdd12fe67eec0bee95e652976937541/doc/img/xline-horizontal-black.png -------------------------------------------------------------------------------- /doc/img/xline-key-perf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xline-kv/Xline/71479a4fabdd12fe67eec0bee95e652976937541/doc/img/xline-key-perf.png -------------------------------------------------------------------------------- /doc/img/xline_test_deployment.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xline-kv/Xline/71479a4fabdd12fe67eec0bee95e652976937541/doc/img/xline_test_deployment.jpg -------------------------------------------------------------------------------- /fixtures/ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC+zCCAeOgAwIBAgIUdq2avyzBS6mkNz2hiuKGWt93OwwwDQYJKoZIhvcNAQEL 3 | BQAwDTELMAkGA1UEAwwCY2EwHhcNMjQwMTA3MTMxMjU5WhcNMzQwMTA0MTMxMjU5 4 | WjANMQswCQYDVQQDDAJjYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB 5 | AJBMPjtDhQZ8M89GBXVxX4FIHYEJ9ncy64Fc3OZm/uSo4kZf2PMYOxQgGlxvXYeq 6 | pQC7yX1O+gshxozc8899cAEM6hs2HY50JdAgONshkdDD8vJs7iRlshmdL+MIJx4e 7 | SAtNrEvmyeGUX1YNMiaCKZUSRAry5oaGul7k7fessa1y1ClsTPVPWnR+4S1UvtKC 8 | EG8eV5rJrFrgTLLHHNZmh+blSsiHm5o19nFIB9RZiBDwts/Lkzy5bRHk2HKE+fI5 9 | R82fujNL2Z1oLogfLCXyEw2cLgCe1j1WMiMx6t7w47nr24/yN2aZtk821qe/SmwS 10 | EQv1Hv+8cswzhBkHYjflHGsCAwEAAaNTMFEwHQYDVR0OBBYEFPzHZyv/ULvDt5jw 11 | K7NmgPRHsD0YMB8GA1UdIwQYMBaAFPzHZyv/ULvDt5jwK7NmgPRHsD0YMA8GA1Ud 12 | EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAE0BLsOfsISfclclA2x1zbku 13 | FLxffZxbyDhlXzE4W8xB2QaCMbF3IWrYI3ZoDMhEYGOH8QZSSs2TgcXu9kr9CdIp 14 | gYjtAsZG2s3lpp32ikreecxaf3bVRa+aMTykKewkoHdhSSj79JV9MwIAFrFoBTCS 15 | CYngCijzzW2rJYx09q/AIOfWGxPmrcIvVC30zRsVSiZfGk77gqC73sqFup+VWylS 16 | jEhRlLqpSzEq2goJpjOwf6T36TnBxAI/K9aEKXl1tU5tjuJ1qgEMvj1AaXpLXPpv 17 | lhQzoPZJz0nvOWcbLW7nmocDeViSOPlQP8HyD7rTV/OcVp5fNLNr2LABIMBCE20= 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /fixtures/private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCnAxxSXJYWZCKr 3 | 6f6j0HRUwkhX/0+GXjEclWoLA5+KZAuWMSu8bz6X+IScv4vNwORlGSWOnrz+8mb2 4 | I0F6teVZWfWFqsnyWk7IxM+h9yTg7aY/8685YfWTL7fpWq1/3Fniz4QbsYFuzB1V 5 | gaZ5fD2CSYIKzSD+qVSlXF25JDFHV7b2OdHrX0UKZOTWY/VE//STt+PJKdX9R3pl 6 | kGwAzJIkkcAZy0vhvqT3ASTgXchNeN8wGYYb3YirkqIsQB5Xcs1R1W+yz+IrVa6/ 7 | 0WMcyE6qtJPZ0lviyT0nHV/pZjXuD4B0aja/1fk/HmXDPMjpK1BuCBTStM/KlcrA 8 | oAxo+YDhAgMBAAECggEAIyJhY+Y8YMuCC753JkklH+ubQn/gX/kSxduc6mJBvuBb 9 | G6aOd97DQT8zzrHxHEDXC3ml0AIO6mdeR6uVC9aWQBzPrOYIA+cBqfTVZVJTvMnh 10 | 7pQ6KY01F1izjPDZjQtzEWbseNL30rI3/ZP/zJDZc745EEKlDU3cE8mBogA+Ka6w 11 | GLozT9qQf8knBrtzxH6SvrZpfaRlP95is82b4IuPhqYdG7dVYFTALE1MyVrCbS4Y 12 | KytjNLgwp1bIQtWrzMebBGoiU+DvDcRY8zvOfFupDwpYCt3p1aU5wyYYdr74esV7 13 | jjqHj89Ua65JHJ3XnMAaMc4dHM2FsGqMsOv/DDKInQKBgQDawckQEekx0QuP3eJP 14 | GWdZ87oc+FVjDe3bYhAnCf/yXRJoqcs5vr1m1yCXFfsjbQFYHWXR9AUtNn5HCwOZ 15 | zoT1Mv96fXBVGQORgzvlUWS43uKpfIPDVv2I6ZcKSIQAGOgcWYvmBDhYqPHgmx3o 16 | VSrNGWtLdyw3rD1J6O+1RwtbiwKBgQDDchmY59EXBiTvlyT3Qjl0vZFMHa+TElbh 17 | ikNtYltbUHtamOXZzpdk/KA7X2dYi0QpVfbbpfP/ly5lYvgZwl8h90Obopru+ACM 18 | ndlKBfNQYArmWY6bJ2CwF7j1aTCCHZuVuX6/pzFVStRcssn15uoVaIyKd/MhJzLF 19 | S3ertQkSwwKBgAniMYRhWsjeaghQ/RWXzzyYL3N5oNn92h5MWvB4mjDIFbnW2hC8 20 | 1m/cDmPlIVijZyklAuGuhcFaMfBhxgLf+s/dQv+0xSuDGs8rP7yHpeZYY6NGtelQ 21 | d9oEu8dCKXybo3kMbq6wyB7xWyRLvdkuZ+WmXVumgb/uL0K0nIfzMscrAoGAeA1e 22 | K845YSslBQaSbk7/e/X1iguyDWT2eRO01zvTYgPNwZipl2CPHjkPM2km0fy5oaps 23 | N/94IUd7+EsSmsAKL5LytGbtRFyR+c376rw8+OIFz/iy4BsQCRqJQjWa1lHZf96x 24 | PIg2hW2xhD9OTv3IS94sdeG4NmUdipMQryhEqoECgYEAkvXOg66IAVTrO6qgoyl5 25 | 42oufa/QE+qOAYoQEpmx3SZx6tMkycfAQqUHYcXhW1HNjyGbbg/sl13yddnPQqig 26 | +ObtQNSIqGZWCc/HIqM//pPI3MHPhWARMOmAbk0I1mT0QKhuFfSugV2xb1Dj/Rvf 27 | 0VdB8txY+5Wz6zP1F2g46gM= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /fixtures/public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApwMcUlyWFmQiq+n+o9B0 3 | VMJIV/9Phl4xHJVqCwOfimQLljErvG8+l/iEnL+LzcDkZRkljp68/vJm9iNBerXl 4 | WVn1harJ8lpOyMTPofck4O2mP/OvOWH1ky+36Vqtf9xZ4s+EG7GBbswdVYGmeXw9 5 | gkmCCs0g/qlUpVxduSQxR1e29jnR619FCmTk1mP1RP/0k7fjySnV/Ud6ZZBsAMyS 6 | JJHAGctL4b6k9wEk4F3ITXjfMBmGG92Iq5KiLEAeV3LNUdVvss/iK1Wuv9FjHMhO 7 | qrST2dJb4sk9Jx1f6WY17g+AdGo2v9X5Px5lwzzI6StQbggU0rTPypXKwKAMaPmA 8 | 4QIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /fixtures/root_client.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDPzCCAiegAwIBAgIUSfB3FqdIGUJJcelUrsRG0ctY9AcwDQYJKoZIhvcNAQEL 3 | BQAwDTELMAkGA1UEAwwCY2EwHhcNMjQwMjA4MDE1MDQ2WhcNMzQwMjA1MDE1MDQ2 4 | WjAPMQ0wCwYDVQQDDARyb290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC 5 | AQEAsJvXvzxxWqL9k5lDiKsZExegcwZssXbtKs/9IpphIdnJtKxL0sYZMVLDgFxG 6 | yvGbB7W2u4z/DEfPwT2yH6D67Yq2igZDDSB6TfD5EkZ0K0IFI3YBvMd55K+1+Ofe 7 | rL5OXJ0p8UYUAjVnhUvq12RrQN4w4pywb1cv1+XATLO1pYRjxkxy+csk8BxudMyb 8 | Vq4CiCYTnhPjCu4FOW9ecZYWMHRYkZ92Z85f5QeEd29xGz9/zgEw3NEYAyjJt0HZ 9 | Ye3cS9j5PR+WOjAAEorKleD/Yr0sqdFJa5LSwjzJhqexgGTAe43UV+jpvk8PN8sH 10 | ZTcNmKrR4KQ4mqV+QWjaiNwgDQIDAQABo4GUMIGRMA4GA1UdDwEB/wQEAwIFoDAd 11 | BgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwIAYDVR0RBBkwF4IJbG9jYWxo 12 | b3N0hwR/AAABhwQAAAAAMB0GA1UdDgQWBBS17ALmZXD0x059Sxa4/nR5mq43/jAf 13 | BgNVHSMEGDAWgBT8x2cr/1C7w7eY8CuzZoD0R7A9GDANBgkqhkiG9w0BAQsFAAOC 14 | AQEAgV0SvYUgtPbykIJnT2YH8bQUabEwX37/nJbIRTK/K+vknI6QXGfVf7tZlYrN 15 | 8wSMy6thEhlJnoU8CMsN+dp9iyt13kZypE+4R05AdU6QLeD2L0mKFrUvSxRFhoVo 16 | Zsc0AhBdVFpDPylMiWohVLrglgKrsdpQVLzePs107ep3GOYlX7Tm1q4OPCvbwewi 17 | +EP5wwFpEakiOg3eK/zbJX3tqDBVEARfskuSVyxAqN+xLO9nSehfGbkER20PWQyR 18 | fgGoJOR098J033ogylo0z6HbAXMqD+sS8Q4ndURBu4+Q8pjd/mTjDWzyhU0vbTHn 19 | Hz2s9I5v9wsrYlyQr84YKZBRQw== 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /fixtures/root_client.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCwm9e/PHFaov2T 3 | mUOIqxkTF6BzBmyxdu0qz/0immEh2cm0rEvSxhkxUsOAXEbK8ZsHtba7jP8MR8/B 4 | PbIfoPrtiraKBkMNIHpN8PkSRnQrQgUjdgG8x3nkr7X4596svk5cnSnxRhQCNWeF 5 | S+rXZGtA3jDinLBvVy/X5cBMs7WlhGPGTHL5yyTwHG50zJtWrgKIJhOeE+MK7gU5 6 | b15xlhYwdFiRn3Znzl/lB4R3b3EbP3/OATDc0RgDKMm3Qdlh7dxL2Pk9H5Y6MAAS 7 | isqV4P9ivSyp0UlrktLCPMmGp7GAZMB7jdRX6Om+Tw83ywdlNw2YqtHgpDiapX5B 8 | aNqI3CANAgMBAAECggEACXPh7ZCq4YbJS0HgCTvdLPYuBpIQtcPviEPXIOfB7Kmn 9 | P+Yb7dVgClGLeL6XpGufmF9Lv0iZ6xqZ3lYyxzoazCWJRSR2KMj0+uB7uFRwidMw 10 | CriZebT0i36zHvYsJzNNZuwbj1gooICiwJHyaSJtEi0CUVLBJ8hzHVtk+kYBIjF3 11 | ydwBVP8QNXlUsXc+0zfvEvlzcEiT8QbdOUNs/eQZVbBjXlcyp0w3gH7RglKbS4zh 12 | JRvbko5ndYeBoDnl3uVH7Gy+YbByEXyiZ09r4vOMmp3SoDVzb62uJvz2nNfo1tn7 13 | 2PADFBJ870tGDS9czidlHF1Os+X2cX3UetqUUBcKQQKBgQDJ6H/GF5rI0Ai6GTkr 14 | 0DqpCu7CNGiHNYXpPid2/Kys1xiceZMCOwv2FTbkTZCPU5jETvTWOQbmIec1eqQt 15 | RzwqkWf9pGO3kSdXZlPYIx1MVRjdozml/0wlo8CVe+J3VUbAiTpoSj09xyKgJoE+ 16 | bcsj/gRkBXfAxz6pe/DhIwaxzQKBgQDf7Dp2Lxm9K14wHL5Ye+d1qqUklc23at5E 17 | E1E6I7tTXFWU9ZQSWAKiGfDGB9EBxU2bqLhpkhygRNdz9kv2c2RK5qM1QbdhgJsM 18 | jTXbMd7c11lgMTndeFiRqnRsZdAbOTvVqOFn7H7qXLY3Krp8rn+nIr908EV+c8IP 19 | TqHxy53nQQKBgE9x/0y3nvzi9nwbTqaRsXMwTQ/3RSXmhoFnJmooM91yaUmwgIrB 20 | Jwy1/jpI1te+gf7EPoxINhG5R1uAnTb/r4nkWvGvjFj/cWZvahBCiNWvKjCTeCx7 21 | zr+EtlNbQpnH4SYDGQtOIti7EUHIxNQGqYbI/XtJt3wQKfTQQtCjUpAFAoGAD/dn 22 | H8AyBKQP/jw+ck67bU9yoQ4xce+j05TRCOU9WS9PRuTP3xL5dReGbIhoJcksxHme 23 | VKC/e8oM1s7sEbGeqByT7Js3+TLTW2zCN1PyASs1yz8XUixfnFtcG9KSqS5GjvCQ 24 | yfk5/3oG4B4i1/sVTRSUNEFhoyeb4b2InJYZN0ECgYEAgodPz7ub5ZlTHonwM7yc 25 | PVRUShRevI2xe9XvpyzYhy2KGv/IxGr8XAxyBmH7cl8VbZW98nA0jKDLk4LjvEg3 26 | ki0R83ve4xVxISDHXnXkWyYy2Zn3F2ZDeSGx1rjXwTYOnEkRWmk+U0+J7CtmL+uJ 27 | itBsAsAc/N6Ou9xamMO4ohA= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /fixtures/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDQTCCAimgAwIBAgIUGX6JVc3gilrrVve6oNwAd5CnCDgwDQYJKoZIhvcNAQEL 3 | BQAwDTELMAkGA1UEAwwCY2EwHhcNMjQwMTA3MTMxMjU5WhcNMzQwMTA0MTMxMjU5 4 | WjARMQ8wDQYDVQQDDAZzZXJ2ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK 5 | AoIBAQDBst9wJ19PNrBdeuTCsDP56H58hqOlUQ7JCK6Euycl2CLswM6DC6EdmmkI 6 | b0Sc3fWaYhTsYBOs/9pQ2amwcYmvcbrRS+QANlv5YqJFnNpaMxo1jBOOqbZaf5Xa 7 | YBv5bf7nT2CCxDhb+8deLxq4y0i0q6lVyTI4xMp/HffpiSE2xGKfT2fetVrpF0do 8 | K47eAHQO+hY2qTjAtd6UAfrRwpaZairGd3Yk209/SE8HiGiwz9iof5ID2LdqdMH2 9 | MtzPVB8ejeo+tQfzpAyqkECkZJegfSytDVw55QzD/uXiHtnSRBiSDnIvaG4xqFmY 10 | mWyxaW0Q2UqClm49S+XsHw3E7f1FAgMBAAGjgZQwgZEwDgYDVR0PAQH/BAQDAgWg 11 | MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAgBgNVHREEGTAXgglsb2Nh 12 | bGhvc3SHBH8AAAGHBAAAAAAwHQYDVR0OBBYEFNsdElONraOmD3c/mFq4s9Pn/nyc 13 | MB8GA1UdIwQYMBaAFPzHZyv/ULvDt5jwK7NmgPRHsD0YMA0GCSqGSIb3DQEBCwUA 14 | A4IBAQCHpC4XsX3EVEHGeA2S0jmgh85Z/VvTO6B5XGnjn1QbAxzOmJoDsLO32vFn 15 | FhhFRrerTS8iJEwFXdE6O848T0N9yC/NI6cBbBP7FRCUgQvWJWyU+8O/f7AjDAOF 16 | E8hHRIhA3KSAR7gGJSo9glTK2EW5EL0VEiPR+yw6H0lfE2vp+IKm428jL2yCc83O 17 | qkhSSOh0ChzYCIdp0qGKXGYzki1mKWMrXRApi7M3WrJrqzb8ybOt9AuJHyOUYP6d 18 | UTN44qT0RsCfBdm6Lkwh7sN0ot0sfwHs7hWIvbtn/wca3JE0Kf0AfrCcT0aIxfbN 19 | K5nSkFw1jDcG4OgdRQBpfumnz/hB 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /fixtures/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDBst9wJ19PNrBd 3 | euTCsDP56H58hqOlUQ7JCK6Euycl2CLswM6DC6EdmmkIb0Sc3fWaYhTsYBOs/9pQ 4 | 2amwcYmvcbrRS+QANlv5YqJFnNpaMxo1jBOOqbZaf5XaYBv5bf7nT2CCxDhb+8de 5 | Lxq4y0i0q6lVyTI4xMp/HffpiSE2xGKfT2fetVrpF0doK47eAHQO+hY2qTjAtd6U 6 | AfrRwpaZairGd3Yk209/SE8HiGiwz9iof5ID2LdqdMH2MtzPVB8ejeo+tQfzpAyq 7 | kECkZJegfSytDVw55QzD/uXiHtnSRBiSDnIvaG4xqFmYmWyxaW0Q2UqClm49S+Xs 8 | Hw3E7f1FAgMBAAECggEAAcnB8EocwiFyHVrOE9IGmM8Bo+qmRhdqeIag/c6wfNPy 9 | 9paNEX6g/SH4yEAmtQADjWpMYSY96EHwZQCtZViW+cyl+dqmfy4BSVsUvmnFRkHk 10 | gwwI/i8o5Mr6uipa98GBMed0r6BaGNdJLuBmccBGqoxPIuCZmxaN/ABt0nUWnmfL 11 | k12rA1gL8/4nT925anvZ+FKwtX3HQUGonee3AA7CwfdkJQq2ZWp3LO9ugF6X9htq 12 | h3Xc40FWpL5saR9Oh+tdlzTy9R7dmjjXdq2PYZCkfV2u8S/Vhozq7Dq3x474dVxF 13 | jPadetl0fbsql3WwVwF+LMcvZrtNcS4XaW8eensKnQKBgQD2Zd20vbv9ULE1s1Je 14 | 93sX22PDYTbNixox+59S5MMkanUTTUrpyrxxsil7H+eqyEP5m+7bggYMTZKfgzE+ 15 | 2420pgGWy9SNYInhvb442fxD5ChqHjSkJIgX5deYM1KCw0KRjYIvlAI4ll+yijre 16 | h0Zp7empeyiL6fYNQt7Vvo2ACwKBgQDJP0PXFzdxCwZPegQ6ZBSc3kGulBO+IVV8 17 | irRcs/mZpqSl43FVtmx5WOxKC4370ESDs/aK36oW+pSibgxXnX+JHjDbq6cMhV+y 18 | HDPAOhtK2Ju3nHtTou14Zvres/tNL7O1CmbWFiM1hx4zsm3fiWoF+9rRubJb/F2i 19 | sy575ZM57wKBgQCJO2EI6mfKhWe9hfuJowKu3eUpi3pvblsPnZo2G+1H2fcM4g6o 20 | Od9M37LUvYdVCic53YxiO3/M2GOibfo+jR/WjmLyLuJaP3tr/Db3CQnEck3EFMCy 21 | 7xnRXZT/Uw5QgBoSYSMyBphaFf6EvRJeoC8qjnSGjW6expbg+e6MU44R0wKBgQCn 22 | R1IRmLe53AsbxvV6v7OujMQV4/nhIX4YbobrQ6ImiLD1Pu/26hKSb0rBZYdYl85L 23 | lyxIVQ/h19nyhfz2WvEmvZSpE65gJwDbYLvXfbUv6orM/WI5rAUt/pNqyDoL96mt 24 | w36lV3Ney/hbymWv2F3rqWRCzdMmyMfgNIZf7/0HuwKBgQDip2idAAABStlaSmPT 25 | yi7dFgrJwMnxlDX3G0bs+4RMudYkP4P2GWxB3uoz610KWJlojrBsyMuRyJ7znQjL 26 | d1hYYK/dMJTonAF9VY2R+erQ6SLwyFoUaagU2fXbUQdZCVJ5wgdAMSA4BSOY1pw1 27 | RqHkBq+EvSB+PXZWvK3RTCn5lQ== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /fixtures/u1_client.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDPTCCAiWgAwIBAgIUPNdbG6JyVpNanAE4Jxz0Zc7uhdkwDQYJKoZIhvcNAQEL 3 | BQAwDTELMAkGA1UEAwwCY2EwHhcNMjQwMjA4MDE1MDQ3WhcNMzQwMjA1MDE1MDQ3 4 | WjANMQswCQYDVQQDDAJ1MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB 5 | AOrk6fzfKC56niK9aP0AgdWwfw7h0xEHS4IUY3qQvUglaG1QMjCItlXwXsdYVBa3 6 | Wasdavy+8H5KS77VTaX4fdQzFmzAmd5odGI34/S59Be72+uvFREiUH2DKC37ilAl 7 | eBHEHyeXPcLxZQujvLMt1i3S3HQ+NCKia+llURcEd7mE9omP1YiVRukBLCeT3LHe 8 | fkNwCdeZH/VbNh7GyoxN87kaaCPv9y224WYGw/Bpg+vu0QSTrrS2vx7lgwUdgXVs 9 | ktZD0dini8+gxiub6R2KKhdBJof3IlHw8O8PPCIHDSyADslHxoIrkW2rwlcmYBhe 10 | AEQKq3TnCJZQ6y/OXMI9vqMCAwEAAaOBlDCBkTAOBgNVHQ8BAf8EBAMCBaAwHQYD 11 | VR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMCAGA1UdEQQZMBeCCWxvY2FsaG9z 12 | dIcEfwAAAYcEAAAAADAdBgNVHQ4EFgQU2xBZdDwcNrOiQ12fpGvTKJmMX0wwHwYD 13 | VR0jBBgwFoAU/MdnK/9Qu8O3mPArs2aA9EewPRgwDQYJKoZIhvcNAQELBQADggEB 14 | AIfy9lqvQ6vV5Ja1p7Xa2j85cLrHwqRu9PMYCQTRCKS/A48UggwtZ0wgkMNhDAUf 15 | 4iiwPGe3EUY6ax3SPazSavrsYgF944ZjshiRutaKawEi5F4w8KF01TsI5q6ckOQL 16 | m03mer14Mq4oZ5OZO5RFjQN4aN/8cIaz88tMXZyKAxYnU5o7jUtCUiWRdt7en+gl 17 | A2ULMIG27rDuDQDGYdu4T/mnjMu54pKoYbh4GKOoePll6OMHqpCOu3t0WT5FGzmR 18 | 37lFLFIjtzPunqxgtm4UixjRzPL8uxGbBdkMwRWtSrw43wPb2zzyvhKwoS8FyK8h 19 | RLUoOh38z/WdXYwvbaLIoPo= 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /fixtures/u1_client.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDq5On83yguep4i 3 | vWj9AIHVsH8O4dMRB0uCFGN6kL1IJWhtUDIwiLZV8F7HWFQWt1mrHWr8vvB+Sku+ 4 | 1U2l+H3UMxZswJneaHRiN+P0ufQXu9vrrxURIlB9gygt+4pQJXgRxB8nlz3C8WUL 5 | o7yzLdYt0tx0PjQiomvpZVEXBHe5hPaJj9WIlUbpASwnk9yx3n5DcAnXmR/1WzYe 6 | xsqMTfO5Gmgj7/cttuFmBsPwaYPr7tEEk660tr8e5YMFHYF1bJLWQ9HYp4vPoMYr 7 | m+kdiioXQSaH9yJR8PDvDzwiBw0sgA7JR8aCK5Ftq8JXJmAYXgBECqt05wiWUOsv 8 | zlzCPb6jAgMBAAECggEASetf90QC0R2lRil1MKlIMocodPZ8BY/WI0lnRNrnthyP 9 | kFb8dGmRd9n8+Z7CV6O7hC3tzTwJUrTuwU8+8EwSzQSGuhKiWWtltMMB6bHbLtzf 10 | iHvAlXPHALiVnGLcU/x2nKCrbTobJY3xzofoijqSfDkvRyWMIpoae4h2zFeLlcW3 11 | rTBWpPITz7JTl7/oxLaoXccLFQEIUUwxVjecV4ZbLQZaWwtzdCric6lxEc+IXq09 12 | jzHQMRU6mwqlTn6qM1qNGPDD+h5ze23X+RQ5YxZRaMJ5yjtyVcz74+Po02aqbeAR 13 | W4Los+4zxsjLwKgWESgpQlEIigX+8Wy0khEPnBWpAQKBgQDvVVdbTWzggHgaPaZb 14 | A0uEHrN5LqvDrJODveMmlmQgtUZkUFTqkPi+d1Cx+thtCU4vHkux74cqsMMZl7Sz 15 | rjqbMd97rLBL8WpKP6a5Ws9LQxjHW/JZJgHrfoz5ryADHOzZLs6hv8QVUm3MaOet 16 | I2DODQqUJ86vvVHJCeU6j3l3qwKBgQD7QG8+lZzfIyWHXK1Z52HCer0/4JWcZ4H0 17 | SPIbVQLQlvl1m+yxqBVwmsC8a4eg791A057I5hY2SexjKD79OCRCEz0//dwSEQn0 18 | Y12fFiWp0CMtKiqQKCstWRN6wvIwIuhretegZ46Dl2lsYTBQ7kIuT7KzEVVn2XKl 19 | cLbdtyt86QKBgQCS+i7ulCrT6DFZtBiQdgKPPadsOTwkq7vfwOJZlSwiZhC3lBnB 20 | /4uytGVrF6iHtZo9F3bW7Elu7ySxd9fyLIIzQrqDIfcWfEiRFmvWEq2RA8CY1Z+M 21 | Heo56/q5b1HSd5YAfl1JOhI6IefqC1aTnlFZ2OpxN80XYcVzF8+dWfT3hwKBgQDA 22 | 6uPACT7PcnwxaG7OlkRRAM9pSbd2xV9aQ9xqMiccJKBctqHSc34q3RhaVLJqV2Lq 23 | kNLd7RVnD/HK3S8oXUAx3/XqubCNyrl7BgTvzyCSN/eaiULvN3iXtEqpuyAc5+DQ 24 | Dh2c04bw8YSILEPeEGOOjbFnn0qVl7hY4af3q9pfaQKBgHv1gFa8myukZpN8YoxD 25 | rLMUBD1gpeTSjVAhqIUFjo4yRO/gfLz+c2ImLetZzSvIyExEUa07bH47Jy4U+CbV 26 | 1sZ3AKiJcoI/bAR9+G8ol7j/SXAHpcZl+v/8cAlQLg9njy8TV8J2iWd1vdMIwLa2 27 | 064UOsO11uIdTJ+TFbmJYvI2 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /fixtures/u2_client.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDPTCCAiWgAwIBAgIUHlpAGnA7JzeANDHScWspJJI34IUwDQYJKoZIhvcNAQEL 3 | BQAwDTELMAkGA1UEAwwCY2EwHhcNMjQwMjA4MDE1MDQ3WhcNMzQwMjA1MDE1MDQ3 4 | WjANMQswCQYDVQQDDAJ1MjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB 5 | AMZG9a04m4N4HAlsfi5DJC796rSrEmnXiXMZltoTjTtwCYihR2aUNiJui/vujlEA 6 | x/qyo1d5ryRmnRL5pAXwYtf+wjvUEVyx+6omaS3PbB+f0HdlQR/GvDzoSvhg3/dZ 7 | 7LW952RHSSTkC3HTb9LM2bYmbEkeAH4aUzLKtUCzQmth7Qzwu/SsALjWoIC7kCZN 8 | i4Ba3ONwtO03nF8Kr3Dzg6HGTxEK3X9aWf+qaVIgJJZfeX7FZFh21liDcbXahAux 9 | yhgPycxsQ/Fkip2JRPCw6K7k2SdesuvnuDE+tWJfDRlZUcXiXNEE1PxgUN9fr8d3 10 | DTyM1Vu0OMioCoSCwvZ6iBMCAwEAAaOBlDCBkTAOBgNVHQ8BAf8EBAMCBaAwHQYD 11 | VR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMCAGA1UdEQQZMBeCCWxvY2FsaG9z 12 | dIcEfwAAAYcEAAAAADAdBgNVHQ4EFgQU9dQyV9zQfB6LhFz3qarACKXk6GIwHwYD 13 | VR0jBBgwFoAU/MdnK/9Qu8O3mPArs2aA9EewPRgwDQYJKoZIhvcNAQELBQADggEB 14 | AEiENIrVfgkFSkWhVY7bYmv7GvV4I0/xkXvt3YkWqB1TRlT0w74c7C6uJHgqYuGo 15 | c21iu1h64PD0hjE9QryGV4Y9Ph9kwOSSrP4LIisRJfH84F3bBpJlglsJ3cp/6fb0 16 | 5yizQIomMey/W2SHdbAEsFccMbuJiD/LM/ei8BWzYsFcalMnzeNf4b7JvcB+yNjR 17 | DVAehsYorEkjOYsGBYfMNMud8N0PQqLHQcUHp+eWqBZBGunUXBEERs5RgZaVG6+q 18 | 5ZrKyqYES2PtvndcaQAIc97ChS3uQa02exaW9h5H0KuWmBwMvrbrNR4aS1dgRkWN 19 | UUiEGX3p+uQpKc3YYi+lMv4= 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /fixtures/u2_client.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDGRvWtOJuDeBwJ 3 | bH4uQyQu/eq0qxJp14lzGZbaE407cAmIoUdmlDYibov77o5RAMf6sqNXea8kZp0S 4 | +aQF8GLX/sI71BFcsfuqJmktz2wfn9B3ZUEfxrw86Er4YN/3Wey1vedkR0kk5Atx 5 | 02/SzNm2JmxJHgB+GlMyyrVAs0JrYe0M8Lv0rAC41qCAu5AmTYuAWtzjcLTtN5xf 6 | Cq9w84Ohxk8RCt1/Wln/qmlSICSWX3l+xWRYdtZYg3G12oQLscoYD8nMbEPxZIqd 7 | iUTwsOiu5NknXrLr57gxPrViXw0ZWVHF4lzRBNT8YFDfX6/Hdw08jNVbtDjIqAqE 8 | gsL2eogTAgMBAAECggEALyzo/UrvrWsheJiIuSceb9PYSR4+5lHzESCwhLiMLO0u 9 | R4TgAScYbj7RLaMH13wRGLPyKzj3k/iaPM6K2SLl0bB6tFNzwhNS5pv3AIBOoIOc 10 | zRuZUB8v4GoAlHIwN6D8sg06BeD0JUPg+TcubwBweGdR8l+iDF7lFrumPnuoPPYH 11 | g+txdPWdmFcd50eLLqvxjnBtr4v4CGk/DDQucBjfX4YjUfImRx1uwyV60fKdGrGs 12 | 24oGFzZLj/yGSn6tbszHc+ndag+ukE7KVroY9lCxwMKDcIco6SDcuuXBoGrm181m 13 | SlYGC1a0Sui2meLGDKSnm4HQD8E8hCPsvEq2lsUuyQKBgQDhm7G0B901izxcZSZZ 14 | +Ba9kUIybcrek3XV57UXXl76Mp6zelHazFiZxqk99Sj0AgKAxV76E9iHZBYSnnPs 15 | TegqC29eBkYB8/3gaqwNyQRN4gXn0ptE9RT15tXfiuCJLiO8USOk0FJRQ2hTOni4 16 | Mk+XteB13ay8z1bowr0tvEYB3QKBgQDg/LskWo7z7WoXvaZ5s+Jhmq1btGKdMdMf 17 | A/KRnapMhK+t+aGUNEzldvokqGGK0nmDRN6lvsaNPRcXoAuO5p1gI/vUNS4k9rJj 18 | gyfSfeOJdK/8KoLRqeTesnTYK5ObkKVN4SltVFUz7D2oqB0pqnxI6q6+hkx/4AbL 19 | fsOdYQUqrwKBgBBJSAPCCZMC8SGOX71g7mtS4B9504pLxNbjOixssJiJLPGx49TS 20 | qZa4Q6FoYEN1Ha8kEF4nLptfe0Ru0dl+KkNWvfxgoY8kiPA5YyA/oaLprRl2F6Vb 21 | t96kgk333YupnATNKrCMJVUec5qGfw50+0/tXj3D+eNLaG+3FycFD1y1AoGBAKcZ 22 | rlji1Ze/9nMpjVKI0xORET6yLCf6UIaRpQCX7FsmlWCOrn5nldE726+MS94SUuO3 23 | K/JITimqfNM0MEFzcOFt+GT1Fo+nlnioedQxYeS+gNK2NDFKkM0CGBxRyTDabpv4 24 | Jo+n+hw1UtpH+lju4Z46h4zELF8xYXqUbO8flvnLAoGBAJR8Ydn53dnUvo85pbWt 25 | AOd/75/vmim8a09J6aXkb8r+AT2cXIR8IImssxh7qaiICDCGeM1tMfEWVvL2bxYh 26 | Q5nYDWf/gloZuST1DvRdGd1ZDouQsZfHZWRdWoBBmKFNFNB3qWYCAMenXZompdsS 27 | hHmdRI1y9mCB+77usYvx23xo 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | ci/rust-toolchain.toml -------------------------------------------------------------------------------- /scripts/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:latest 2 | 3 | RUN apt-get update && apt-get install -y iproute2 iputils-ping 4 | 5 | COPY xline /usr/local/bin 6 | COPY benchmark /usr/local/bin 7 | 8 | CMD ["/usr/local/bin/xline"] 9 | -------------------------------------------------------------------------------- /scripts/log.sh: -------------------------------------------------------------------------------- 1 | ${__LOG__:=false} && return 0 || __LOG__=true 2 | 3 | function log::debug() { 4 | echo -e "\033[00;34m" "[DEBUG]" "$@" "\033[0m" 5 | } 6 | 7 | function log::info() { 8 | echo -e "\033[00;32m" "[INFO]" "$@" "\033[0m" 9 | } 10 | 11 | function log::warn() { 12 | echo -e "\033[00;33m" "[WARN]" "$@" "\033[0m" 13 | } 14 | 15 | function log::error() { 16 | echo -e "\033[00;31m" "[ERROR]" "$@" "\033[0m" 17 | } 18 | 19 | function log::fatal() { 20 | echo -e "\033[00;31m" "[FATAL]" "$@" "\033[0m" 21 | exit 1 22 | } 23 | -------------------------------------------------------------------------------- /scripts/prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 15s 3 | 4 | scrape_configs: 5 | - job_name: "xline" 6 | static_configs: 7 | - targets: 8 | [ 9 | "node1:9100", 10 | "node2:9100", 11 | "node3:9100", 12 | ] 13 | metrics_path: /metrics 14 | scheme: http 15 | -------------------------------------------------------------------------------- /scripts/quick_start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DIR=$( 4 | cd "$(dirname "$0")" 5 | pwd 6 | ) 7 | source $DIR/common.sh 8 | source $DIR/log.sh 9 | 10 | # stop all containers 11 | stop_all() { 12 | log::info stopping 13 | for name in "node1" "node2" "node3" "client"; do 14 | common::stop_container ${name} 15 | done 16 | docker network rm xline_net >/dev/null 2>&1 17 | docker stop "prometheus" > /dev/null 2>&1 18 | sleep 1 19 | log::info stopped 20 | } 21 | 22 | # run cluster of xline/etcd in container 23 | run_cluster() { 24 | log::info cluster starting 25 | common::run_xline 1 ${MEMBERS} new 26 | common::run_xline 2 ${MEMBERS} new 27 | common::run_xline 3 ${MEMBERS} new 28 | common::run_etcd_client 29 | wait 30 | log::info cluster started 31 | } 32 | 33 | # run prometheus 34 | run_prometheus() { 35 | docker run -d -it --rm --name=prometheus --net=xline_net -p 9090:9090 \ 36 | --ip=${1} --cap-add=NET_ADMIN -v ${DIR}/prometheus.yml:/etc/prometheus/prometheus.yml \ 37 | prom/prometheus 38 | } 39 | 40 | if [ -z "$1" ]; then 41 | stop_all 42 | docker network create --subnet=172.20.0.0/24 xline_net >/dev/null 2>&1 43 | log::warn "A Docker network named 'xline_net' is created for communication among various xline nodes. You can use the command 'docker network rm xline_net' to remove it after use." 44 | run_cluster 45 | run_prometheus "172.20.0.6" 46 | echo "Prometheus starts on http://172.20.0.6:9090/graph and http://127.0.0.1:9090/graph (if you are using Docker Desktop)." 47 | exit 0 48 | elif [ "$1" == "stop" ]; then 49 | stop_all 50 | exit 0 51 | else 52 | echo "Unexpected argument: $1" 53 | exit 1 54 | fi 55 | -------------------------------------------------------------------------------- /workspace-hack/.gitattributes: -------------------------------------------------------------------------------- 1 | # Avoid putting conflict markers in the generated Cargo.toml file, since their presence breaks 2 | # Cargo. 3 | # Also do not check out the file as CRLF on Windows, as that's what hakari needs. 4 | Cargo.toml merge=binary -crlf 5 | -------------------------------------------------------------------------------- /workspace-hack/build.rs: -------------------------------------------------------------------------------- 1 | // A build script is required for cargo to consider build dependencies. 2 | fn main() {} 3 | -------------------------------------------------------------------------------- /workspace-hack/src/lib.rs: -------------------------------------------------------------------------------- 1 | // This is a stub lib.rs. 2 | -------------------------------------------------------------------------------- /xline_server.conf: -------------------------------------------------------------------------------- 1 | [cluster] 2 | name = 'node1' 3 | is_leader = true 4 | 5 | [cluster.members] 6 | node1 = ['127.0.0.1:2379'] 7 | node2 = ['127.0.0.1:2380'] 8 | node3 = ['127.0.0.1:2381'] 9 | 10 | # curp server timeout settings 11 | [cluster.curp_config] 12 | # The heartbeat(tick) interval between curp server nodes, default value is 150ms 13 | # heartbeat_interval = '150ms' 14 | 15 | # The wait synced timeout between curp server nodes, default value is 5s 16 | # wait_synced_timeout = '5s' 17 | 18 | # The propose retry timeout between curp server nodes, default value is 800ms 19 | # retry_timeout = '800ms' 20 | 21 | # The rpc timeout between curp server nodes, default value is 50ms 22 | # rpc_timeout = '50ms' 23 | 24 | # How many ticks a follower is allowed to miss before it starts a new round of election, default value is 5 25 | # The actual timeout will be randomized and in between heartbeat_interval * [follower_timeout_ticks, 2 * follower_timeout_ticks) 26 | # follower_timeout_ticks = 5 27 | 28 | # How many ticks a candidate needs to wait before it starts a new round of election, default value is 2 29 | # It should be smaller than `follower_timeout_ticks` 30 | # The actual timeout will be randomized and in between heartbeat_interval * [candidate_timeout_ticks, 2 * candidate_timeout_ticks) 31 | # candidate_timeout_ticks = 2 32 | 33 | # How often should the gc task run, default Value is 20s. 34 | # gc_interval = '20s' 35 | 36 | # curp client timeout settings 37 | [cluster.client_config] 38 | # The curp client timeout, default value is 1s 39 | # timeout = '1s' 40 | 41 | # The client wait synced timeout, default value is 2s 42 | # wait_synced_timeout = '2s' 43 | 44 | # The curp client propose request timeout 45 | # propose_timeout = '1s' 46 | 47 | # Storage Engine Settings. Required 48 | [storage] 49 | engine = 'rocksdb' 50 | data_dir = '/usr/local/xline/data-dir' 51 | 52 | [log] 53 | path = '/var/log/xline' 54 | rotation = 'daily' 55 | level = 'info' 56 | 57 | [trace] 58 | jaeger_online = false 59 | jaeger_offline = false 60 | jaeger_output_dir = './jaeger_jsons' 61 | jaeger_level = 'info' 62 | 63 | [auth] 64 | # auth_public_key = './public_key'.pem' 65 | # auth_private_key = './private_key.pem' 66 | --------------------------------------------------------------------------------