├── .bazelrc ├── .clang-format ├── .github └── workflows │ └── c-cpp.yml ├── .gitignore ├── BUILD.bazel ├── README.md ├── WORKSPACE ├── bazel ├── BUILD ├── raftcpp_deps_build_all.bzl ├── raftcpp_deps_setup.bzl └── thirdparty │ ├── BUILD │ ├── asio.BUILD │ └── spdlog.BUILD ├── build.sh ├── check-git-clang-format-output.sh ├── docs └── .gitkeep ├── examples ├── BUILD.bazel ├── counter │ ├── counter_client_main.cc │ ├── counter_server.cc │ ├── counter_server.h │ ├── counter_server_main.cc │ ├── counter_state_machine.h │ └── run_example_servers.sh └── proto │ └── counter.proto ├── git-clang-format ├── proto ├── BUILD ├── protoc.sh └── raft.proto ├── scripts ├── code-format.sh └── install-bazel.sh ├── src ├── common │ ├── config.cc │ ├── config.h │ ├── constants.h │ ├── endpoint.h │ ├── file.cc │ ├── file.h │ ├── id.h │ ├── logging.cc │ ├── logging.h │ ├── randomer.h │ ├── status.h │ ├── timer.cc │ ├── timer.h │ ├── timer_manager.cc │ ├── timer_manager.h │ ├── type_def.h │ └── util.h ├── log_manager │ ├── blocking_queue_interface.h │ ├── blocking_queue_mutex_impl.h │ ├── leader_log_manager.cc │ ├── leader_log_manager.h │ ├── non_leader_log_manager.cc │ └── non_leader_log_manager.h ├── node │ ├── node.cc │ └── node.h └── statemachine │ └── state_machine.h └── tests └── unit_tests ├── blocking_queue_test.cc ├── config_test.cc ├── file_test.cc ├── log_manager_test.cc ├── logging_check_test.cc ├── logging_test.cc ├── nodeid_test.cc ├── paper_test.cc ├── timer_test.cc ├── util.cc └── util.h /.bazelrc: -------------------------------------------------------------------------------- 1 | # Bazel doesn't need more than 200MB of memory for local build based on memory profiling: 2 | # https://docs.bazel.build/versions/master/skylark/performance.html#memory-profiling 3 | # The default JVM max heapsize is 1/4 of physical memory up to 32GB which could be large 4 | # enough to consume all memory constrained by cgroup in large host. 5 | # Limiting JVM heapsize here to let it do GC more when approaching the limit to 6 | # leave room for compiler/linker. 7 | # The number 2G is chosen heuristically to both support large VM and small VM with RBE. 8 | # Startup options cannot be selected via config. 9 | 10 | #startup --host_jvm_args=-Xmx2g 11 | 12 | run --color=yes 13 | 14 | build --color=yes 15 | 16 | # build --workspace_status_command="bash bazel/get_workspace_status" 17 | 18 | build --incompatible_strict_action_env 19 | build --host_force_python=PY3 20 | #build --java_runtime_version=remotejdk_11 21 | #build --tool_java_runtime_version=remotejdk_11 22 | 23 | build --enable_platform_specific_config 24 | 25 | # Allow tags to influence execution requirements 26 | common --experimental_allow_tags_propagation 27 | 28 | # Enable position independent code (this is the default on macOS and Windows) 29 | # (Workaround for https://github.com/bazelbuild/rules_foreign_cc/issues/421) 30 | build:linux --copt=-fPIC 31 | build:linux --copt=-Wno-deprecated-declarations 32 | build:linux --cxxopt=-std=c++2a 33 | build:linux --conlyopt=-fexceptions 34 | build:linux --fission=dbg,opt 35 | build:linux --features=per_object_debug_info 36 | build:linux --action_env=BAZEL_LINKLIBS=-l%:libstdc++.a 37 | build:linux --action_env=BAZEL_LINKOPTS=-lm 38 | 39 | # We already have absl in the build, define absl=1 to tell googletest to use absl for backtrace. 40 | build --define absl=1 41 | 42 | # Pass PATH, CC, CXX and LLVM_CONFIG variables from the environment. 43 | build --action_env=CC 44 | build --action_env=CXX 45 | build --action_env=LLVM_CONFIG 46 | build --action_env=PATH 47 | 48 | # Common flags for sanitizers 49 | build:sanitizer --define tcmalloc=disabled 50 | build:sanitizer --linkopt -ldl 51 | build:sanitizer --build_tag_filters=-no_san 52 | build:sanitizer --test_tag_filters=-no_san 53 | 54 | # Common flags for Clang 55 | build:clang --action_env=BAZEL_COMPILER=clang 56 | build:clang --action_env=CC=clang --action_env=CXX=clang++ 57 | build:clang --linkopt=-fuse-ld=lld 58 | 59 | # Flags for Clang + PCH 60 | build:clang-pch --spawn_strategy=local 61 | 62 | # Basic ASAN/UBSAN that works for gcc 63 | build:asan --config=sanitizer 64 | # ASAN install its signal handler, disable ours so the stacktrace will be printed by ASAN 65 | build:asan --define signal_trace=disabled 66 | build:asan --copt -fsanitize=address,undefined 67 | build:asan --linkopt -fsanitize=address,undefined 68 | # vptr and function sanitizer are enabled in clang-asan if it is set up via bazel/setup_clang.sh. 69 | build:asan --copt -fno-sanitize=vptr,function 70 | build:asan --linkopt -fno-sanitize=vptr,function 71 | build:asan --copt -DADDRESS_SANITIZER=1 72 | build:asan --copt -D__SANITIZE_ADDRESS__ 73 | build:asan --test_env=ASAN_OPTIONS=handle_abort=1:allow_addr2line=true:check_initialization_order=true:strict_init_order=true:detect_odr_violation=1 74 | build:asan --test_env=UBSAN_OPTIONS=halt_on_error=true:print_stacktrace=1 75 | build:asan --test_env=ASAN_SYMBOLIZER_PATH 76 | # ASAN needs -O1 to get reasonable performance. 77 | build:asan --copt -O1 78 | build:asan --copt -fno-optimize-sibling-calls 79 | 80 | # Clang ASAN/UBSAN 81 | build:clang-asan --config=clang 82 | build:clang-asan --config=asan 83 | build:clang-asan --linkopt -fuse-ld=lld 84 | build:clang-asan --linkopt --rtlib=compiler-rt 85 | # build:clang-asan --linkopt --unwindlib=libgcc 86 | 87 | # macOS 88 | build:macos --cxxopt=-std=c++2a 89 | 90 | # macOS ASAN/UBSAN 91 | build:macos-asan --config=asan 92 | # Workaround, see https://github.com/bazelbuild/bazel/issues/6932 93 | build:macos-asan --copt -Wno-macro-redefined 94 | build:macos-asan --copt -D_FORTIFY_SOURCE=0 95 | # Workaround, see https://github.com/bazelbuild/bazel/issues/4341 96 | # build:macos-asan --copt -DGRPC_BAZEL_BUILD 97 | # Dynamic link cause issues like: `dyld: malformed mach-o: load commands size (59272) > 32768` 98 | build:macos-asan --dynamic_mode=off 99 | 100 | # Clang TSAN 101 | build:clang-tsan --config=sanitizer 102 | build:clang-tsan --copt -fsanitize=thread 103 | build:clang-tsan --linkopt -fsanitize=thread 104 | build:clang-tsan --linkopt -fuse-ld=lld 105 | build:clang-tsan --build_tag_filters=-no_san,-no_tsan 106 | build:clang-tsan --test_tag_filters=-no_san,-no_tsan 107 | # Needed due to https://github.com/libevent/libevent/issues/777 108 | build:clang-tsan --copt -DEVENT__DISABLE_DEBUG_MODE 109 | # https://github.com/abseil/abseil-cpp/issues/760 110 | # https://github.com/google/sanitizers/issues/953 111 | build:clang-tsan --test_env="TSAN_OPTIONS=report_atomic_races=0" 112 | 113 | # Clang MSAN - this is the base config for remote-msan and docker-msan. To run this config without 114 | # our build image, follow https://github.com/google/sanitizers/wiki/MemorySanitizerLibcxxHowTo 115 | # with libc++ instruction and provide corresponding `--copt` and `--linkopt` as well. 116 | build:clang-msan --config=sanitizer 117 | build:clang-msan --copt -fsanitize=memory 118 | build:clang-msan --linkopt -fsanitize=memory 119 | build:clang-msan --linkopt -fuse-ld=lld 120 | build:clang-msan --copt -fsanitize-memory-track-origins=2 121 | build:clang-msan --test_env=MSAN_SYMBOLIZER_PATH 122 | # MSAN needs -O1 to get reasonable performance. 123 | build:clang-msan --copt -O1 124 | build:clang-msan --copt -fno-optimize-sibling-calls 125 | 126 | # Clang with libc++ 127 | build:libc++ --config=clang 128 | build:libc++ --action_env=CXXFLAGS=-stdlib=libc++ 129 | build:libc++ --action_env=LDFLAGS=-stdlib=libc++ 130 | build:libc++ --action_env=BAZEL_CXXOPTS=-stdlib=libc++ 131 | build:libc++ --action_env=BAZEL_LINKLIBS=-l%:libc++.a:-l%:libc++abi.a 132 | build:libc++ --action_env=BAZEL_LINKOPTS=-lm:-pthread 133 | build:libc++ --define force_libcpp=enabled 134 | 135 | # Optimize build for binary size reduction. 136 | build:sizeopt -c opt --copt -Os 137 | 138 | # Test options 139 | build --test_env=HEAPCHECK=normal --test_env=PPROF_PATH 140 | 141 | # Coverage options 142 | coverage --config=coverage 143 | coverage --build_tests_only 144 | build:coverage --action_env=BAZEL_USE_LLVM_NATIVE_COVERAGE=1 145 | build:coverage --action_env=GCOV=llvm-profdata 146 | build:coverage --copt=-DNDEBUG 147 | # 1.5x original timeout + 300s for trace merger in all categories 148 | build:coverage --test_timeout=390,750,1500,5700 149 | build:coverage --define=dynamic_link_tests=true 150 | build:coverage --test_env=HEAPCHECK= 151 | build:coverage --combined_report=lcov 152 | build:coverage --strategy=TestRunner=sandboxed,local 153 | build:coverage --strategy=CoverageReport=sandboxed,local 154 | build:coverage --experimental_use_llvm_covmap 155 | build:coverage --collect_code_coverage 156 | build:coverage --test_tag_filters=-nocoverage 157 | build:coverage --instrumentation_filter="//src[/:],//include[/:]" 158 | build:test-coverage --test_arg="-l trace" 159 | build:fuzz-coverage --config=plain-fuzzer 160 | 161 | test --enable_runfiles=True 162 | 163 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: Google 2 | ColumnLimit: 90 3 | DerivePointerAlignment: false 4 | IndentCaseLabels: false 5 | PointerAlignment: Right 6 | IndentWidth: 4 7 | AccessModifierOffset: -4 8 | -------------------------------------------------------------------------------- /.github/workflows/c-cpp.yml: -------------------------------------------------------------------------------- 1 | name: CPP CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | lint: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | os: [ubuntu-latest] 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | with: 19 | fetch-depth: 0 20 | - name: cpplint 21 | env: 22 | GITHUB_CONTEXT: ${{ toJson(github) }} 23 | run: ./check-git-clang-format-output.sh 24 | 25 | build-with-bazel: 26 | timeout-minutes: 60 27 | runs-on: ${{ matrix.os }} 28 | strategy: 29 | matrix: 30 | os: [macos-latest, ubuntu-latest] 31 | 32 | steps: 33 | - uses: actions/checkout@v2 34 | - name: BUILD 35 | run: ./build.sh 36 | - name: TEST 37 | run: bazel test //:all --jobs=4 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Generated by Cargo 3 | # will have compiled files and executables 4 | /target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | 14 | # Added by cargo 15 | 16 | /target 17 | .idea 18 | .vscode 19 | /build 20 | 21 | //bazel related 22 | bazel-bin 23 | bazel-raftcpp 24 | bazel-out 25 | bazel-testlogs 26 | 27 | // logs 28 | log 29 | 30 | # MacOS 31 | .DS_Store 32 | 33 | # protobuf 34 | **/*.pb.cc 35 | **/*.pb.h 36 | -------------------------------------------------------------------------------- /BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library", "cc_proto_library", "cc_test") 2 | load("@rules_proto//proto:defs.bzl", "proto_library") 3 | load("@com_github_grpc_grpc//bazel:cc_grpc_library.bzl", "cc_grpc_library") 4 | 5 | cc_library( 6 | name = "common_lib", 7 | srcs = glob(["src/common/*.cc"]), 8 | hdrs = glob(["src/common/*.h"]), 9 | strip_include_prefix = "//src", 10 | visibility = ["//visibility:public"], 11 | deps = [ 12 | "@asio", 13 | "@com_github_spdlog//:spdlog", 14 | ], 15 | ) 16 | 17 | cc_grpc_library( 18 | name = "raft_cc_grpc", 19 | srcs = ["//proto:raft_proto"], 20 | grpc_only = True, 21 | deps = [ 22 | "//proto:raft_cc_proto", 23 | ], 24 | ) 25 | 26 | cc_library( 27 | name = "node_lib", 28 | srcs = glob([ 29 | "src/node/*.cc", 30 | "src/log_manager/*.cc", 31 | ]), 32 | hdrs = glob([ 33 | "src/node/*.h", 34 | "src/log_manager/*.h", 35 | "src/statemachine/*.h", 36 | ]), 37 | strip_include_prefix = "//src", 38 | visibility = ["//visibility:public"], 39 | deps = [ 40 | ":common_lib", 41 | ":raft_cc_grpc", 42 | "@asio", 43 | "@com_github_grpc_grpc//:grpc++", 44 | "@com_github_spdlog//:spdlog", 45 | ], 46 | ) 47 | 48 | # TODO: (luhuanbing) rebuild examples. 49 | #cc_test( 50 | # name = "example_counter_client_main", 51 | # srcs = glob([ 52 | # "examples/counter/counter_client_main.cc", 53 | # ]), 54 | # deps = [ 55 | # ":common_lib", 56 | # "@com_github_gflags_gflags//:gflags", 57 | # "@com_google_googletest//:gtest_main", 58 | # ], 59 | #) 60 | # 61 | #cc_test( 62 | # name = "example_counter_server_main", 63 | # srcs = glob([ 64 | # "examples/counter/counter_server_main.cc", 65 | # ]), 66 | # deps = [ 67 | # ":node_lib", 68 | # "@com_github_gflags_gflags//:gflags", 69 | # "@com_google_googletest//:gtest_main", 70 | # ], 71 | #) 72 | 73 | cc_test( 74 | name = "timer_test", 75 | srcs = glob([ 76 | "tests/unit_tests/timer_test.cc", 77 | ]), 78 | deps = [ 79 | ":common_lib", 80 | "@com_google_googletest//:gtest_main", 81 | ], 82 | ) 83 | 84 | cc_test( 85 | name = "config_test", 86 | srcs = glob([ 87 | "tests/unit_tests/config_test.cc", 88 | ]), 89 | deps = [ 90 | ":common_lib", 91 | "@com_google_googletest//:gtest_main", 92 | ], 93 | ) 94 | 95 | cc_test( 96 | name = "logging_test", 97 | srcs = glob([ 98 | "tests/unit_tests/logging_test.cc", 99 | ]), 100 | deps = [ 101 | ":common_lib", 102 | "@com_google_googletest//:gtest_main", 103 | ], 104 | ) 105 | 106 | cc_test( 107 | name = "file_test", 108 | srcs = glob([ 109 | "tests/unit_tests/file_test.cc", 110 | ]), 111 | deps = [ 112 | ":common_lib", 113 | "@com_google_googletest//:gtest_main", 114 | ], 115 | ) 116 | 117 | cc_test( 118 | name = "log_manager_test", 119 | srcs = glob([ 120 | "tests/unit_tests/blocking_queue_test.cc", 121 | ]), 122 | deps = [ 123 | ":common_lib", 124 | ":node_lib", 125 | "@com_google_googletest//:gtest_main", 126 | ], 127 | ) 128 | 129 | cc_test( 130 | name = "node_id_test", 131 | srcs = glob([ 132 | "tests/unit_tests/nodeid_test.cc", 133 | ]), 134 | deps = [ 135 | ":common_lib", 136 | "@com_google_googletest//:gtest_main", 137 | ], 138 | ) 139 | 140 | cc_test( 141 | name = "logging_check_test", 142 | srcs = glob([ 143 | "tests/unit_tests/logging_check_test.cc", 144 | ]), 145 | deps = [ 146 | ":common_lib", 147 | ":node_lib", 148 | "@com_google_googletest//:gtest_main", 149 | ], 150 | ) 151 | # TODO: (luhuanbing) repair the log mannager test 152 | #cc_test( 153 | # name = "logger_manager_test", 154 | # srcs = glob([ 155 | # "tests/unit_tests/log_manager_test.cc", 156 | # "tests/unit_tests/util.cc", 157 | # "tests/unit_tests/util.h", 158 | # ]), 159 | # deps = [ 160 | # ":common_lib", 161 | # ":node_lib", 162 | # "@com_github_grpc_grpc//:grpc++", 163 | # "@com_google_googletest//:gtest_main", 164 | # ], 165 | #) 166 | 167 | cc_test( 168 | name = "select_test", 169 | srcs = glob([ 170 | "tests/unit_tests/select_test.cc", 171 | "tests/unit_tests/util.cc", 172 | "tests/unit_tests/util.h", 173 | ]), 174 | deps = [ 175 | ":common_lib", 176 | ":node_lib", 177 | ":raft_cc_grpc", 178 | "@com_github_grpc_grpc//:grpc++", 179 | "@com_google_googletest//:gtest_main", 180 | ], 181 | ) 182 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # raftcpp 2 | A RAFT implementation to help build your RAFT service in 1 minute. 3 | [Note that this project is now WORKING IN PROGRESS. We are going to release 0.1.0 soon.] 4 | 5 | ## Main dependencies 6 | - bazel: for building tool 7 | - asio: for asynchronous eventloop and IO 8 | - grpc: for RPC framework 9 | - protobuf: for serialization framework 10 | - gtest: for test framework 11 | - spdlog: for logging tool 12 | - gflags: for command line tool 13 | 14 | ## Quick Start 15 | ### Install Building Tool 16 | We are now using bazel 5.1.1 for building `raftcpp` project, please make sure you have installed bazel. If you don't have installed it, you could install it via the following command: 17 | ```shell script 18 | ./scripts/install-bazel.sh 19 | ``` 20 | 21 | ### Build 22 | ```shell script 23 | bazel build //:all 24 | ``` 25 | ### Test 26 | ```shell script 27 | bazel test //:all 28 | ``` 29 | or test one specific test case in following command: 30 | ```shell script 31 | bazel test //:xxxx_test 32 | ``` 33 | 34 | ## Get Involved 35 | Because this project is working in progress now, we are very welcome you if 36 | you have the willing to contribute any thing. 37 | 38 | 1. Open issue to feedback issues or create pull request for your code changes. 39 | 2. After your code changed, maybe a unit test is necessary. 40 | 3. Run `./scripts/code-format.sh` to format your code style. 41 | 4. Review, then merge. 42 | -------------------------------------------------------------------------------- /WORKSPACE: -------------------------------------------------------------------------------- 1 | workspace(name = "com_github_purecpp_raftcpp") 2 | 3 | load("@com_github_purecpp_raftcpp//bazel:raftcpp_deps_setup.bzl", "raftcpp_deps_setup") 4 | 5 | raftcpp_deps_setup() 6 | 7 | load("@com_github_purecpp_raftcpp//bazel:raftcpp_deps_build_all.bzl", "raftcpp_deps_build_all") 8 | 9 | raftcpp_deps_build_all() 10 | 11 | # This needs to be run after grpc_deps() in metable_deps_build_all() to make 12 | # sure all the packages loaded by grpc_deps() are available. However a 13 | # load() statement cannot be in a function so we put it here. 14 | load("@com_github_grpc_grpc//bazel:grpc_extra_deps.bzl", "grpc_extra_deps") 15 | 16 | grpc_extra_deps() 17 | -------------------------------------------------------------------------------- /bazel/BUILD: -------------------------------------------------------------------------------- 1 | licenses(["notice"]) 2 | package(default_visibility = ["//:__subpackages__"]) 3 | -------------------------------------------------------------------------------- /bazel/raftcpp_deps_build_all.bzl: -------------------------------------------------------------------------------- 1 | load("@com_github_grpc_grpc//bazel:grpc_deps.bzl", "grpc_deps") 2 | load("@rules_proto_grpc//:repositories.bzl", "rules_proto_grpc_toolchains") 3 | 4 | def raftcpp_deps_build_all(): 5 | grpc_deps() 6 | rules_proto_grpc_toolchains() 7 | -------------------------------------------------------------------------------- /bazel/raftcpp_deps_setup.bzl: -------------------------------------------------------------------------------- 1 | load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository", "new_git_repository") 2 | load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_file") 3 | load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe") 4 | 5 | def raftcpp_deps_setup(): 6 | maybe( 7 | http_archive, 8 | name = "com_github_spdlog", 9 | sha256 = "1e68e9b40cf63bb022a4b18cdc1c9d88eb5d97e4fd64fa981950a9cacf57a4bf", 10 | urls = [ 11 | "https://github.com/gabime/spdlog/archive/v1.8.0.tar.gz", 12 | ], 13 | strip_prefix = "spdlog-1.8.0", 14 | build_file = "@com_github_purecpp_raftcpp//bazel/thirdparty:spdlog.BUILD", 15 | ) 16 | 17 | maybe( 18 | http_archive, 19 | name = "com_github_grpc_grpc", 20 | url = "https://github.com/grpc/grpc/archive/refs/tags/v1.47.0.tar.gz", 21 | strip_prefix = "grpc-1.47.0", 22 | ) 23 | 24 | maybe( 25 | git_repository, 26 | name = "rules_proto_grpc", 27 | branch = "dev", 28 | remote = "https://github.com/rules-proto-grpc/rules_proto_grpc.git", 29 | ) 30 | 31 | maybe( 32 | http_archive, 33 | name = "com_google_googletest", 34 | url = "https://github.com/google/googletest/archive/refs/tags/release-1.11.0.tar.gz", 35 | sha256 = "b4870bf121ff7795ba20d20bcdd8627b8e088f2d1dab299a031c1034eddc93d5", 36 | strip_prefix = "googletest-release-1.11.0", 37 | ) 38 | 39 | maybe( 40 | http_archive, 41 | name = "com_github_gflags_gflags", 42 | url = "https://github.com/gflags/gflags/archive/refs/tags/v2.2.2.tar.gz", 43 | sha256 = "34af2f15cf7367513b352bdcd2493ab14ce43692d2dcd9dfc499492966c64dcf", 44 | strip_prefix = "gflags-2.2.2", 45 | ) 46 | 47 | maybe( 48 | http_archive, 49 | name = "asio", 50 | urls = [ 51 | "https://udomain.dl.sourceforge.net/project/asio/asio/1.22.1%20%28Stable%29/asio-1.22.1.tar.gz", 52 | ], 53 | strip_prefix = "asio-1.22.1", 54 | build_file = "@com_github_purecpp_raftcpp//bazel/thirdparty:asio.BUILD", 55 | ) 56 | -------------------------------------------------------------------------------- /bazel/thirdparty/BUILD: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purecpp-org/raftcpp/8f0c0324d98b9b935b66b65fe08f31363cd38156/bazel/thirdparty/BUILD -------------------------------------------------------------------------------- /bazel/thirdparty/asio.BUILD: -------------------------------------------------------------------------------- 1 | load("@rules_cc//cc:defs.bzl", "cc_library") 2 | 3 | cc_library( 4 | name = "asio", 5 | hdrs = glob(["include/**/*.hpp"]) + glob(["include/**/*.ipp"]), 6 | defines = ["ASIO_STANDALONE"], 7 | visibility = ["//visibility:public"], 8 | strip_include_prefix = "//include", 9 | ) -------------------------------------------------------------------------------- /bazel/thirdparty/spdlog.BUILD: -------------------------------------------------------------------------------- 1 | COPTS = ["-DSPDLOG_COMPILED_LIB"] 2 | 3 | cc_library( 4 | name = "spdlog", 5 | srcs = glob([ 6 | "src/*.cpp", 7 | ]), 8 | hdrs = glob([ 9 | "include/spdlog/*.h", 10 | "include/spdlog/cfg/*.h", 11 | "include/spdlog/details/*.h", 12 | "include/spdlog/fmt/*.h", 13 | "include/spdlog/fmt/bundled/*.h", 14 | "include/spdlog/sinks/*.h", 15 | ]), 16 | includes = [ 17 | ".", 18 | "include/", 19 | ], 20 | strip_include_prefix = 'include', 21 | copts = COPTS, 22 | deps = [ 23 | ], 24 | visibility = ["//visibility:public"], 25 | ) -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -x 4 | # Cause the script to exit if a single command fails. 5 | set -e 6 | 7 | bazel --version 8 | bazel build --jobs=4 //:all 9 | -------------------------------------------------------------------------------- /check-git-clang-format-output.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | pull_request=$(echo "$GITHUB_CONTEXT" | jq -r '.event.pull_request') 6 | if [ "$pull_request" != null ]; then 7 | echo "this is pull request, run git-clang-format" 8 | origin_commit=$(echo "$GITHUB_CONTEXT" | jq -r '.event.pull_request.base.sha') 9 | current_commit=$(echo "$GITHUB_CONTEXT" | jq -r '.event.pull_request.head.sha') 10 | echo "Running clang-format against parent commit $origin_commit, and $current_commit" 11 | else 12 | echo "this is not pull request, it is already run git-clang-format" 13 | exit 0 14 | fi 15 | 16 | output="$(./git-clang-format --binary clang-format --commit "$current_commit" "$origin_commit" --diff)" 17 | if [ "$output" = "no modified files to format" ] || [ "$output" = "clang-format did not modify any files" ]; then 18 | echo "clang-format passed." 19 | exit 0 20 | else 21 | echo "clang-format failed:" 22 | echo "$output" 23 | exit 1 24 | fi 25 | -------------------------------------------------------------------------------- /docs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purecpp-org/raftcpp/8f0c0324d98b9b935b66b65fe08f31363cd38156/docs/.gitkeep -------------------------------------------------------------------------------- /examples/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@com_github_grpc_grpc//bazel:cc_grpc_library.bzl", "cc_grpc_library") 2 | load("@rules_proto//proto:defs.bzl", "proto_library") 3 | load("@rules_proto_grpc//cpp:defs.bzl", "cpp_proto_library") 4 | load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library", "cc_proto_library", "cc_test") 5 | load("@rules_proto_grpc//python:defs.bzl", "python_grpc_compile") 6 | 7 | proto_library( 8 | name = "counter_proto", 9 | srcs = ["proto/counter.proto"], 10 | visibility = ["//visibility:public"], 11 | deps = [ 12 | "@com_google_protobuf//:empty_proto", 13 | ], 14 | ) 15 | 16 | cc_proto_library( 17 | name = "counter_cc_proto", 18 | deps = [":counter_proto"], 19 | ) 20 | 21 | cc_grpc_library( 22 | name = "counter_cc_grpc", 23 | srcs = [":counter_proto"], 24 | grpc_only = True, 25 | deps = [ 26 | ":counter_cc_proto", 27 | ], 28 | ) 29 | 30 | 31 | cc_library( 32 | name = "example_counter_server_lib", 33 | srcs = glob([ 34 | "counter/counter_server.cc", 35 | ]), 36 | hdrs = glob(["counter/*.h"]), 37 | strip_include_prefix = "//examples", 38 | visibility = ["//visibility:public"], 39 | deps = [ 40 | "@com_github_purecpp_raftcpp//:node_lib", 41 | ":counter_cc_grpc", 42 | "@asio", 43 | "@com_github_grpc_grpc//:grpc++", 44 | "@com_github_gflags_gflags//:gflags", 45 | "@com_google_googletest//:gtest_main", 46 | ], 47 | ) 48 | 49 | cc_binary( 50 | name = "counter_server_main", 51 | srcs = glob([ 52 | "counter/counter_server_main.cc", 53 | ]), 54 | deps = [ 55 | ":example_counter_server_lib", 56 | ], 57 | ) 58 | 59 | # cc_library( 60 | # name = "example_counter_client_lib", 61 | # srcs = glob([ 62 | # "examples/counter/counter_client.cc", 63 | # ]), 64 | # hdrs = glob(["counter/*.h"]), 65 | # strip_include_prefix = "//examples", 66 | # visibility = ["//visibility:public"], 67 | # deps = [ 68 | # "@com_github_purecpp_raftcpp//:node_lib", 69 | # ":counter_cc_grpc", 70 | # "@asio", 71 | # "@com_github_grpc_grpc//:grpc++", 72 | # "@com_github_gflags_gflags//:gflags", 73 | # "@com_google_googletest//:gtest_main", 74 | # ], 75 | # ) 76 | 77 | # cc_binary( 78 | # name = "counter_client_main", 79 | # srcs = glob([ 80 | # "counter/counter_client_main.cc", 81 | # ]), 82 | # deps = [ 83 | # ":example_counter_client_lib", 84 | # ], 85 | # ) 86 | -------------------------------------------------------------------------------- /examples/counter/counter_client_main.cc: -------------------------------------------------------------------------------- 1 | 2 | #include "common/logging.h" 3 | //#include "rpc/client.h" 4 | 5 | int main(int argc, char *argv[]) { 6 | auto rpc_client = std::make_shared("127.0.0.1:10001"); 7 | rpc_client->enable_auto_heartbeat(); 8 | RAFTCPP_LOG(RLL_DEBUG) << "Counter client connected to the server."; 9 | 10 | auto r = rpc_client->call("incr", 98); 11 | RAFTCPP_LOG(RLL_INFO) << "==========r = " << r; 12 | return 0; 13 | } 14 | -------------------------------------------------------------------------------- /examples/counter/counter_server.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include "common/config.h" 6 | #include "counter_state_machine.h" 7 | #include "node/node.h" 8 | 9 | 10 | #include "examples/proto/counter.pb.h" 11 | #include "examples/proto/counter.grpc.pb.h" 12 | 13 | #include 14 | #include 15 | 16 | using grpc::Server; 17 | using grpc::ServerBuilder; 18 | using grpc::ServerContext; 19 | using grpc::Status; 20 | 21 | using namespace examples; 22 | using namespace examples::counter; 23 | -------------------------------------------------------------------------------- /examples/counter/counter_server.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | #include 5 | 6 | #include "common/config.h" 7 | #include "counter_state_machine.h" 8 | #include "node/node.h" 9 | 10 | 11 | #include "examples/proto/counter.pb.h" 12 | #include "examples/proto/counter.grpc.pb.h" 13 | 14 | #include 15 | #include 16 | 17 | using grpc::Server; 18 | using grpc::ServerBuilder; 19 | using grpc::ServerContext; 20 | using grpc::Status; 21 | 22 | using namespace examples; 23 | using namespace examples::counter; 24 | namespace examples { 25 | namespace counter { 26 | 27 | class CounterServiceImpl: public examples::counter::CounterService::Service, public std::enable_shared_from_this { 28 | public: 29 | // TODO(qwang): Are node and fsm uncopyable? 30 | CounterServiceImpl(std::shared_ptr node, 31 | std::shared_ptr &fsm) 32 | : node_(std::move(node)), fsm_(std::move(fsm)) {} 33 | 34 | 35 | 36 | grpc::Status Incr(::grpc::ServerContext *context, 37 | const ::examples::counter::IncrRequest *request, 38 | ::examples::counter::IncrResponse *response) { 39 | 40 | } 41 | 42 | grpc::Status Get(::grpc::ServerContext *context, 43 | const ::examples::counter::GetRequest *request, 44 | ::examples::counter::GetResponse *response) { 45 | 46 | } 47 | 48 | // void Incr(rpc_conn conn, int delta) { 49 | // // CHECK is leader. 50 | // RAFTCPP_LOG(RLL_INFO) << "=============Incring: " << delta; 51 | // // Does this should be enabled from this? 52 | // std::shared_ptr request = std::make_shared(delta); 53 | // if (!node_->IsLeader()) { 54 | // //// RETURN redirect. 55 | // } 56 | // node_->PushRequest(request); 57 | // } 58 | 59 | // int64_t Get(rpc_conn conn) { 60 | // // There is no need to gurantee the write-read consistency, 61 | // // so we can get the value directly from this fsm instead of 62 | // // apply it to all nodes. 63 | // return fsm_->GetValue(); 64 | // } 65 | 66 | private: 67 | std::shared_ptr node_; 68 | std::shared_ptr fsm_; 69 | }; 70 | 71 | 72 | } 73 | 74 | } // namespace examples 75 | -------------------------------------------------------------------------------- /examples/counter/counter_server_main.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include "common/config.h" 6 | #include "counter_state_machine.h" 7 | #include "node/node.h" 8 | #include "counter_server.h" 9 | 10 | #include "examples/proto/counter.pb.h" 11 | #include "examples/proto/counter.grpc.pb.h" 12 | 13 | #include 14 | #include 15 | 16 | using grpc::Server; 17 | using grpc::ServerBuilder; 18 | using grpc::ServerContext; 19 | using grpc::Status; 20 | 21 | using namespace examples; 22 | using namespace examples::counter; 23 | 24 | 25 | DEFINE_string(conf, "", "The configurations of this raft group."); 26 | // DEFINE_string(this_addr, "", "This address of this instance listening on."); 27 | 28 | int main(int argc, char *argv[]) { 29 | std::string conf_str; 30 | { 31 | gflags::ParseCommandLineFlags(&argc, &argv, false); 32 | conf_str = FLAGS_conf; 33 | gflags::ShutDownCommandLineFlags(); 34 | } 35 | // RAFTCPP_CHECK(!conf_str.empty()) << "Failed to start counter server with empty 36 | // config string."; 37 | if (conf_str.empty()) { 38 | RAFTCPP_LOG(RLL_INFO) 39 | << "Failed to start counter server with empty config string."; 40 | return -1; 41 | } 42 | const auto config = raftcpp::common::Config::From(conf_str); 43 | auto fsm = std::make_shared(); 44 | auto node = std::make_shared(fsm, config); 45 | node->Init(); 46 | 47 | ////////grpc server 48 | std::string server_address("0.0.0.0:50051"); 49 | CounterServiceImpl service(node, fsm); 50 | 51 | grpc::EnableDefaultHealthCheckService(true); 52 | ServerBuilder builder; 53 | // Listen on the given address without any authentication mechanism. 54 | builder.AddListeningPort(server_address, grpc::InsecureServerCredentials()); 55 | // Register "service" as the instance through which we'll communicate with 56 | // clients. In this case it corresponds to an *synchronous* service. 57 | builder.RegisterService(&service); 58 | // Finally assemble the server. 59 | std::unique_ptr server(builder.BuildAndStart()); 60 | std::cout << "Server listening on " << server_address << std::endl; 61 | 62 | // Wait for the server to shutdown. Note that some other thread must be 63 | // responsible for shutting down the server for this call to ever return. 64 | server->Wait(); 65 | 66 | return 0; 67 | } 68 | -------------------------------------------------------------------------------- /examples/counter/counter_state_machine.h: -------------------------------------------------------------------------------- 1 | #ifndef _RAFTCPP_EXAMPLE_COUNTER_COUNTER_STATE_MACHINE_H 2 | #define _RAFTCPP_EXAMPLE_COUNTER_COUNTER_STATE_MACHINE_H 3 | 4 | #include 5 | 6 | #include "common/file.h" 7 | #include "common/status.h" 8 | #include "statemachine/state_machine.h" 9 | 10 | namespace examples { 11 | namespace counter { 12 | 13 | using Status = raftcpp::Status; 14 | /** 15 | * The CounterStateMachine extends from raftcpp::StateMachine. It overrides 16 | * the snapshot interfaces that user can decide when and how do snapshot. 17 | * 18 | * The customs state machine must override the OnApply, OnApply interface defines 19 | * how to apply the request from clients. 20 | * 21 | * Now in this state machine, we do the snapshot for every 3 requests. 22 | */ 23 | class CounterStateMachine : public raftcpp::StateMachine { 24 | public: 25 | CounterStateMachine() {} 26 | 27 | 28 | // We should do snapshot for every 3 requests. 29 | bool ShouldDoSnapshot() override { return received_requests_num_.load() % 3; } 30 | 31 | void SaveSnapshot() override { 32 | raftcpp::File snapshot_file = 33 | raftcpp::File::Open("/tmp/raftcpp/counter/snapshot.txt"); 34 | snapshot_file.CleanAndWrite(std::to_string(atomic_value_.load())); 35 | } 36 | 37 | void LoadSnapshot() override { 38 | raftcpp::File snapshot_file = 39 | raftcpp::File::Open("/tmp/raftcpp/counter/snapshot.txt"); 40 | std::string value = snapshot_file.ReadAll(); 41 | atomic_value_.store(static_cast(std::stoi(value))); 42 | } 43 | 44 | bool OnApply(const std::string &serialized) override { 45 | received_requests_num_.fetch_add(1); 46 | 47 | // /// serialized to Request. 48 | // auto counter_request = CounterRequest::Deserialize1(serialized); 49 | // if (counter_request->GetType() == CounterRequestType::INCR) { 50 | // counter_request.get(); 51 | // auto *incr_request = dynamic_cast(counter_request.get()); 52 | // atomic_value_.fetch_add(incr_request->GetDelta()); 53 | // return IncrResponse(Status::OK); 54 | // } else if (counter_request->GetType() == CounterRequestType::GET) { 55 | // auto *get_request = dynamic_cast(counter_request.get()); 56 | // return GetResponse(atomic_value_.load()); 57 | // } 58 | 59 | // return CounterResponse(Status::UNKNOWN_REQUEST); 60 | return false; 61 | } 62 | 63 | int64_t GetValue() const { return atomic_value_.load(); } 64 | 65 | private: 66 | std::atomic atomic_value_; 67 | 68 | // the number of received requests to decided when we do snapshot. 69 | std::atomic received_requests_num_; 70 | }; 71 | 72 | } // namespace counter 73 | } // namespace examples 74 | 75 | #endif 76 | -------------------------------------------------------------------------------- /examples/counter/run_example_servers.sh: -------------------------------------------------------------------------------- 1 | 2 | nohup./ example_counter_server_main-- conf = 3 | "127.0.0.1:10001,127.0.0.1:10002,127.0.0.1:10003" >> 4 | ./ nohup_10001_output.log 2 > 5 | & 1 &nohup 6 | ./ example_counter_server_main-- conf = 7 | "127.0.0.1:10002,127.0.0.1:10001,127.0.0.1:10003" >> 8 | ./ nohup_10002_output 9 | .log 2 > 10 | & 1 &nohup 11 | ./ example_counter_server_main-- conf = 12 | "127.0.0.1:10003,127.0.0.1:10002,127.0.0.1:10001" >>./ nohup_10003_output 13 | .log 2 > 14 | &1 & 15 | -------------------------------------------------------------------------------- /examples/proto/counter.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package examples.counter; 3 | 4 | import "google/protobuf/empty.proto"; 5 | 6 | message IncrRequest { 7 | int32 detla = 1; 8 | } 9 | 10 | message IncrResponse { 11 | bool ok = 1; 12 | } 13 | 14 | message GetRequest { } 15 | 16 | message GetResponse { 17 | bool ok = 1; 18 | int32 value = 2; 19 | } 20 | 21 | service CounterService { 22 | rpc Incr(IncrRequest) returns (IncrResponse) {} 23 | rpc Get(GetRequest) returns (GetResponse) {} 24 | } 25 | -------------------------------------------------------------------------------- /git-clang-format: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | #===- git-clang-format - ClangFormat Git Integration ---------*- python -*--===# 4 | # 5 | # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 6 | # See https://llvm.org/LICENSE.txt for license information. 7 | # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 8 | # 9 | #===------------------------------------------------------------------------===# 10 | 11 | r""" 12 | clang-format git integration 13 | ============================ 14 | 15 | This file provides a clang-format integration for git. Put it somewhere in your 16 | path and ensure that it is executable. Then, "git clang-format" will invoke 17 | clang-format on the changes in current files or a specific commit. 18 | 19 | For further details, run: 20 | git clang-format -h 21 | 22 | Requires Python 2.7 or Python 3 23 | """ 24 | 25 | from __future__ import absolute_import, division, print_function 26 | import argparse 27 | import collections 28 | import contextlib 29 | import errno 30 | import os 31 | import re 32 | import subprocess 33 | import sys 34 | 35 | usage = 'git clang-format [OPTIONS] [] [] [--] [...]' 36 | 37 | desc = ''' 38 | If zero or one commits are given, run clang-format on all lines that differ 39 | between the working directory and , which defaults to HEAD. Changes are 40 | only applied to the working directory. 41 | 42 | If two commits are given (requires --diff), run clang-format on all lines in the 43 | second that differ from the first . 44 | 45 | The following git-config settings set the default of the corresponding option: 46 | clangFormat.binary 47 | clangFormat.commit 48 | clangFormat.extension 49 | clangFormat.style 50 | ''' 51 | 52 | # Name of the temporary index file in which save the output of clang-format. 53 | # This file is created within the .git directory. 54 | temp_index_basename = 'clang-format-index' 55 | 56 | 57 | Range = collections.namedtuple('Range', 'start, count') 58 | 59 | 60 | def main(): 61 | config = load_git_config() 62 | 63 | # In order to keep '--' yet allow options after positionals, we need to 64 | # check for '--' ourselves. (Setting nargs='*' throws away the '--', while 65 | # nargs=argparse.REMAINDER disallows options after positionals.) 66 | argv = sys.argv[1:] 67 | try: 68 | idx = argv.index('--') 69 | except ValueError: 70 | dash_dash = [] 71 | else: 72 | dash_dash = argv[idx:] 73 | argv = argv[:idx] 74 | 75 | default_extensions = ','.join([ 76 | # From clang/lib/Frontend/FrontendOptions.cpp, all lower case 77 | 'c', 'h', # C 78 | 'm', # ObjC 79 | 'mm', # ObjC++ 80 | 'cc', 'cp', 'cpp', 'c++', 'cxx', 'hpp', 'hxx', # C++ 81 | 'cu', # CUDA 82 | # Other languages that clang-format supports 83 | 'proto', 'protodevel', # Protocol Buffers 84 | 'java', # Java 85 | 'js', # JavaScript 86 | 'ts', # TypeScript 87 | ]) 88 | 89 | p = argparse.ArgumentParser( 90 | usage=usage, formatter_class=argparse.RawDescriptionHelpFormatter, 91 | description=desc) 92 | p.add_argument('--binary', 93 | default=config.get('clangformat.binary', 'clang-format'), 94 | help='path to clang-format'), 95 | p.add_argument('--commit', 96 | default=config.get('clangformat.commit', 'HEAD'), 97 | help='default commit to use if none is specified'), 98 | p.add_argument('--diff', action='store_true', 99 | help='print a diff instead of applying the changes') 100 | p.add_argument('--extensions', 101 | default=config.get('clangformat.extensions', 102 | default_extensions), 103 | help=('comma-separated list of file extensions to format, ' 104 | 'excluding the period and case-insensitive')), 105 | p.add_argument('-f', '--force', action='store_true', 106 | help='allow changes to unstaged files') 107 | p.add_argument('-p', '--patch', action='store_true', 108 | help='select hunks interactively') 109 | p.add_argument('-q', '--quiet', action='count', default=0, 110 | help='print less information') 111 | p.add_argument('--style', 112 | default=config.get('clangformat.style', None), 113 | help='passed to clang-format'), 114 | p.add_argument('-v', '--verbose', action='count', default=0, 115 | help='print extra information') 116 | # We gather all the remaining positional arguments into 'args' since we need 117 | # to use some heuristics to determine whether or not was present. 118 | # However, to print pretty messages, we make use of metavar and help. 119 | p.add_argument('args', nargs='*', metavar='', 120 | help='revision from which to compute the diff') 121 | p.add_argument('ignored', nargs='*', metavar='...', 122 | help='if specified, only consider differences in these files') 123 | opts = p.parse_args(argv) 124 | 125 | opts.verbose -= opts.quiet 126 | del opts.quiet 127 | 128 | commits, files = interpret_args(opts.args, dash_dash, opts.commit) 129 | if len(commits) > 1: 130 | if not opts.diff: 131 | die('--diff is required when two commits are given') 132 | else: 133 | if len(commits) > 2: 134 | die('at most two commits allowed; %d given' % len(commits)) 135 | changed_lines = compute_diff_and_extract_lines(commits, files) 136 | if opts.verbose >= 1: 137 | ignored_files = set(changed_lines) 138 | filter_by_extension(changed_lines, opts.extensions.lower().split(',')) 139 | if opts.verbose >= 1: 140 | ignored_files.difference_update(changed_lines) 141 | if ignored_files: 142 | print('Ignoring changes in the following files (wrong extension):') 143 | for filename in ignored_files: 144 | print(' %s' % filename) 145 | if changed_lines: 146 | print('Running clang-format on the following files:') 147 | for filename in changed_lines: 148 | print(' %s' % filename) 149 | if not changed_lines: 150 | print('no modified files to format') 151 | return 152 | # The computed diff outputs absolute paths, so we must cd before accessing 153 | # those files. 154 | cd_to_toplevel() 155 | if len(commits) > 1: 156 | old_tree = commits[1] 157 | new_tree = run_clang_format_and_save_to_tree(changed_lines, 158 | revision=commits[1], 159 | binary=opts.binary, 160 | style=opts.style) 161 | else: 162 | old_tree = create_tree_from_workdir(changed_lines) 163 | new_tree = run_clang_format_and_save_to_tree(changed_lines, 164 | binary=opts.binary, 165 | style=opts.style) 166 | if opts.verbose >= 1: 167 | print('old tree: %s' % old_tree) 168 | print('new tree: %s' % new_tree) 169 | if old_tree == new_tree: 170 | if opts.verbose >= 0: 171 | print('clang-format did not modify any files') 172 | elif opts.diff: 173 | print_diff(old_tree, new_tree) 174 | else: 175 | changed_files = apply_changes(old_tree, new_tree, force=opts.force, 176 | patch_mode=opts.patch) 177 | if (opts.verbose >= 0 and not opts.patch) or opts.verbose >= 1: 178 | print('changed files:') 179 | for filename in changed_files: 180 | print(' %s' % filename) 181 | 182 | 183 | def load_git_config(non_string_options=None): 184 | """Return the git configuration as a dictionary. 185 | 186 | All options are assumed to be strings unless in `non_string_options`, in which 187 | is a dictionary mapping option name (in lower case) to either "--bool" or 188 | "--int".""" 189 | if non_string_options is None: 190 | non_string_options = {} 191 | out = {} 192 | for entry in run('git', 'config', '--list', '--null').split('\0'): 193 | if entry: 194 | name, value = entry.split('\n', 1) 195 | if name in non_string_options: 196 | value = run('git', 'config', non_string_options[name], name) 197 | out[name] = value 198 | return out 199 | 200 | 201 | def interpret_args(args, dash_dash, default_commit): 202 | """Interpret `args` as "[commits] [--] [files]" and return (commits, files). 203 | 204 | It is assumed that "--" and everything that follows has been removed from 205 | args and placed in `dash_dash`. 206 | 207 | If "--" is present (i.e., `dash_dash` is non-empty), the arguments to its 208 | left (if present) are taken as commits. Otherwise, the arguments are checked 209 | from left to right if they are commits or files. If commits are not given, 210 | a list with `default_commit` is used.""" 211 | if dash_dash: 212 | if len(args) == 0: 213 | commits = [default_commit] 214 | else: 215 | commits = args 216 | for commit in commits: 217 | object_type = get_object_type(commit) 218 | if object_type not in ('commit', 'tag'): 219 | if object_type is None: 220 | die("'%s' is not a commit" % commit) 221 | else: 222 | die("'%s' is a %s, but a commit was expected" % (commit, object_type)) 223 | files = dash_dash[1:] 224 | elif args: 225 | commits = [] 226 | while args: 227 | if not disambiguate_revision(args[0]): 228 | break 229 | commits.append(args.pop(0)) 230 | if not commits: 231 | commits = [default_commit] 232 | files = args 233 | else: 234 | commits = [default_commit] 235 | files = [] 236 | return commits, files 237 | 238 | 239 | def disambiguate_revision(value): 240 | """Returns True if `value` is a revision, False if it is a file, or dies.""" 241 | # If `value` is ambiguous (neither a commit nor a file), the following 242 | # command will die with an appropriate error message. 243 | run('git', 'rev-parse', value, verbose=False) 244 | object_type = get_object_type(value) 245 | if object_type is None: 246 | return False 247 | if object_type in ('commit', 'tag'): 248 | return True 249 | die('`%s` is a %s, but a commit or filename was expected' % 250 | (value, object_type)) 251 | 252 | 253 | def get_object_type(value): 254 | """Returns a string description of an object's type, or None if it is not 255 | a valid git object.""" 256 | cmd = ['git', 'cat-file', '-t', value] 257 | p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 258 | stdout, stderr = p.communicate() 259 | if p.returncode != 0: 260 | return None 261 | return convert_string(stdout.strip()) 262 | 263 | 264 | def compute_diff_and_extract_lines(commits, files): 265 | """Calls compute_diff() followed by extract_lines().""" 266 | diff_process = compute_diff(commits, files) 267 | changed_lines = extract_lines(diff_process.stdout) 268 | diff_process.stdout.close() 269 | diff_process.wait() 270 | if diff_process.returncode != 0: 271 | # Assume error was already printed to stderr. 272 | sys.exit(2) 273 | return changed_lines 274 | 275 | 276 | def compute_diff(commits, files): 277 | """Return a subprocess object producing the diff from `commits`. 278 | 279 | The return value's `stdin` file object will produce a patch with the 280 | differences between the working directory and the first commit if a single 281 | one was specified, or the difference between both specified commits, filtered 282 | on `files` (if non-empty). Zero context lines are used in the patch.""" 283 | git_tool = 'diff-index' 284 | if len(commits) > 1: 285 | git_tool = 'diff-tree' 286 | cmd = ['git', git_tool, '-p', '-U0'] + commits + ['--'] 287 | cmd.extend(files) 288 | p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) 289 | p.stdin.close() 290 | return p 291 | 292 | 293 | def extract_lines(patch_file): 294 | """Extract the changed lines in `patch_file`. 295 | 296 | The return value is a dictionary mapping filename to a list of (start_line, 297 | line_count) pairs. 298 | 299 | The input must have been produced with ``-U0``, meaning unidiff format with 300 | zero lines of context. The return value is a dict mapping filename to a 301 | list of line `Range`s.""" 302 | matches = {} 303 | for line in patch_file: 304 | line = convert_string(line) 305 | match = re.search(r'^\+\+\+\ [^/]+/(.*)', line) 306 | if match: 307 | filename = match.group(1).rstrip('\r\n') 308 | match = re.search(r'^@@ -[0-9,]+ \+(\d+)(,(\d+))?', line) 309 | if match: 310 | start_line = int(match.group(1)) 311 | line_count = 1 312 | if match.group(3): 313 | line_count = int(match.group(3)) 314 | if line_count > 0: 315 | matches.setdefault(filename, []).append(Range(start_line, line_count)) 316 | return matches 317 | 318 | 319 | def filter_by_extension(dictionary, allowed_extensions): 320 | """Delete every key in `dictionary` that doesn't have an allowed extension. 321 | 322 | `allowed_extensions` must be a collection of lowercase file extensions, 323 | excluding the period.""" 324 | allowed_extensions = frozenset(allowed_extensions) 325 | for filename in list(dictionary.keys()): 326 | base_ext = filename.rsplit('.', 1) 327 | if len(base_ext) == 1 and '' in allowed_extensions: 328 | continue 329 | if len(base_ext) == 1 or base_ext[1].lower() not in allowed_extensions: 330 | del dictionary[filename] 331 | 332 | 333 | def cd_to_toplevel(): 334 | """Change to the top level of the git repository.""" 335 | toplevel = run('git', 'rev-parse', '--show-toplevel') 336 | os.chdir(toplevel) 337 | 338 | 339 | def create_tree_from_workdir(filenames): 340 | """Create a new git tree with the given files from the working directory. 341 | 342 | Returns the object ID (SHA-1) of the created tree.""" 343 | return create_tree(filenames, '--stdin') 344 | 345 | 346 | def run_clang_format_and_save_to_tree(changed_lines, revision=None, 347 | binary='clang-format', style=None): 348 | """Run clang-format on each file and save the result to a git tree. 349 | 350 | Returns the object ID (SHA-1) of the created tree.""" 351 | def iteritems(container): 352 | try: 353 | return container.iteritems() # Python 2 354 | except AttributeError: 355 | return container.items() # Python 3 356 | def index_info_generator(): 357 | for filename, line_ranges in iteritems(changed_lines): 358 | if revision: 359 | git_metadata_cmd = ['git', 'ls-tree', 360 | '%s:%s' % (revision, os.path.dirname(filename)), 361 | os.path.basename(filename)] 362 | git_metadata = subprocess.Popen(git_metadata_cmd, stdin=subprocess.PIPE, 363 | stdout=subprocess.PIPE) 364 | stdout = git_metadata.communicate()[0] 365 | mode = oct(int(stdout.split()[0], 8)) 366 | else: 367 | mode = oct(os.stat(filename).st_mode) 368 | # Adjust python3 octal format so that it matches what git expects 369 | if mode.startswith('0o'): 370 | mode = '0' + mode[2:] 371 | blob_id = clang_format_to_blob(filename, line_ranges, 372 | revision=revision, 373 | binary=binary, 374 | style=style) 375 | yield '%s %s\t%s' % (mode, blob_id, filename) 376 | return create_tree(index_info_generator(), '--index-info') 377 | 378 | 379 | def create_tree(input_lines, mode): 380 | """Create a tree object from the given input. 381 | 382 | If mode is '--stdin', it must be a list of filenames. If mode is 383 | '--index-info' is must be a list of values suitable for "git update-index 384 | --index-info", such as " ". Any other mode 385 | is invalid.""" 386 | assert mode in ('--stdin', '--index-info') 387 | cmd = ['git', 'update-index', '--add', '-z', mode] 388 | with temporary_index_file(): 389 | p = subprocess.Popen(cmd, stdin=subprocess.PIPE) 390 | for line in input_lines: 391 | p.stdin.write(to_bytes('%s\0' % line)) 392 | p.stdin.close() 393 | if p.wait() != 0: 394 | die('`%s` failed' % ' '.join(cmd)) 395 | tree_id = run('git', 'write-tree') 396 | return tree_id 397 | 398 | 399 | def clang_format_to_blob(filename, line_ranges, revision=None, 400 | binary='clang-format', style=None): 401 | """Run clang-format on the given file and save the result to a git blob. 402 | 403 | Runs on the file in `revision` if not None, or on the file in the working 404 | directory if `revision` is None. 405 | 406 | Returns the object ID (SHA-1) of the created blob.""" 407 | clang_format_cmd = [binary] 408 | if style: 409 | clang_format_cmd.extend(['-style='+style]) 410 | clang_format_cmd.extend([ 411 | '-lines=%s:%s' % (start_line, start_line+line_count-1) 412 | for start_line, line_count in line_ranges]) 413 | if revision: 414 | clang_format_cmd.extend(['-assume-filename='+filename]) 415 | git_show_cmd = ['git', 'cat-file', 'blob', '%s:%s' % (revision, filename)] 416 | git_show = subprocess.Popen(git_show_cmd, stdin=subprocess.PIPE, 417 | stdout=subprocess.PIPE) 418 | git_show.stdin.close() 419 | clang_format_stdin = git_show.stdout 420 | else: 421 | clang_format_cmd.extend([filename]) 422 | git_show = None 423 | clang_format_stdin = subprocess.PIPE 424 | try: 425 | clang_format = subprocess.Popen(clang_format_cmd, stdin=clang_format_stdin, 426 | stdout=subprocess.PIPE) 427 | if clang_format_stdin == subprocess.PIPE: 428 | clang_format_stdin = clang_format.stdin 429 | except OSError as e: 430 | if e.errno == errno.ENOENT: 431 | die('cannot find executable "%s"' % binary) 432 | else: 433 | raise 434 | clang_format_stdin.close() 435 | hash_object_cmd = ['git', 'hash-object', '-w', '--path='+filename, '--stdin'] 436 | hash_object = subprocess.Popen(hash_object_cmd, stdin=clang_format.stdout, 437 | stdout=subprocess.PIPE) 438 | clang_format.stdout.close() 439 | stdout = hash_object.communicate()[0] 440 | if hash_object.returncode != 0: 441 | die('`%s` failed' % ' '.join(hash_object_cmd)) 442 | if clang_format.wait() != 0: 443 | die('`%s` failed' % ' '.join(clang_format_cmd)) 444 | if git_show and git_show.wait() != 0: 445 | die('`%s` failed' % ' '.join(git_show_cmd)) 446 | return convert_string(stdout).rstrip('\r\n') 447 | 448 | 449 | @contextlib.contextmanager 450 | def temporary_index_file(tree=None): 451 | """Context manager for setting GIT_INDEX_FILE to a temporary file and deleting 452 | the file afterward.""" 453 | index_path = create_temporary_index(tree) 454 | old_index_path = os.environ.get('GIT_INDEX_FILE') 455 | os.environ['GIT_INDEX_FILE'] = index_path 456 | try: 457 | yield 458 | finally: 459 | if old_index_path is None: 460 | del os.environ['GIT_INDEX_FILE'] 461 | else: 462 | os.environ['GIT_INDEX_FILE'] = old_index_path 463 | os.remove(index_path) 464 | 465 | 466 | def create_temporary_index(tree=None): 467 | """Create a temporary index file and return the created file's path. 468 | 469 | If `tree` is not None, use that as the tree to read in. Otherwise, an 470 | empty index is created.""" 471 | gitdir = run('git', 'rev-parse', '--git-dir') 472 | path = os.path.join(gitdir, temp_index_basename) 473 | if tree is None: 474 | tree = '--empty' 475 | run('git', 'read-tree', '--index-output='+path, tree) 476 | return path 477 | 478 | 479 | def print_diff(old_tree, new_tree): 480 | """Print the diff between the two trees to stdout.""" 481 | # We use the porcelain 'diff' and not plumbing 'diff-tree' because the output 482 | # is expected to be viewed by the user, and only the former does nice things 483 | # like color and pagination. 484 | # 485 | # We also only print modified files since `new_tree` only contains the files 486 | # that were modified, so unmodified files would show as deleted without the 487 | # filter. 488 | subprocess.check_call(['git', 'diff', '--diff-filter=M', old_tree, new_tree, 489 | '--']) 490 | 491 | 492 | def apply_changes(old_tree, new_tree, force=False, patch_mode=False): 493 | """Apply the changes in `new_tree` to the working directory. 494 | 495 | Bails if there are local changes in those files and not `force`. If 496 | `patch_mode`, runs `git checkout --patch` to select hunks interactively.""" 497 | changed_files = run('git', 'diff-tree', '--diff-filter=M', '-r', '-z', 498 | '--name-only', old_tree, 499 | new_tree).rstrip('\0').split('\0') 500 | if not force: 501 | unstaged_files = run('git', 'diff-files', '--name-status', *changed_files) 502 | if unstaged_files: 503 | print('The following files would be modified but ' 504 | 'have unstaged changes:', file=sys.stderr) 505 | print(unstaged_files, file=sys.stderr) 506 | print('Please commit, stage, or stash them first.', file=sys.stderr) 507 | sys.exit(2) 508 | if patch_mode: 509 | # In patch mode, we could just as well create an index from the new tree 510 | # and checkout from that, but then the user will be presented with a 511 | # message saying "Discard ... from worktree". Instead, we use the old 512 | # tree as the index and checkout from new_tree, which gives the slightly 513 | # better message, "Apply ... to index and worktree". This is not quite 514 | # right, since it won't be applied to the user's index, but oh well. 515 | with temporary_index_file(old_tree): 516 | subprocess.check_call(['git', 'checkout', '--patch', new_tree]) 517 | index_tree = old_tree 518 | else: 519 | with temporary_index_file(new_tree): 520 | run('git', 'checkout-index', '-a', '-f') 521 | return changed_files 522 | 523 | 524 | def run(*args, **kwargs): 525 | stdin = kwargs.pop('stdin', '') 526 | verbose = kwargs.pop('verbose', True) 527 | strip = kwargs.pop('strip', True) 528 | for name in kwargs: 529 | raise TypeError("run() got an unexpected keyword argument '%s'" % name) 530 | p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 531 | stdin=subprocess.PIPE) 532 | stdout, stderr = p.communicate(input=stdin) 533 | 534 | stdout = convert_string(stdout) 535 | stderr = convert_string(stderr) 536 | 537 | if p.returncode == 0: 538 | if stderr: 539 | if verbose: 540 | print('`%s` printed to stderr:' % ' '.join(args), file=sys.stderr) 541 | print(stderr.rstrip(), file=sys.stderr) 542 | if strip: 543 | stdout = stdout.rstrip('\r\n') 544 | return stdout 545 | if verbose: 546 | print('`%s` returned %s' % (' '.join(args), p.returncode), file=sys.stderr) 547 | if stderr: 548 | print(stderr.rstrip(), file=sys.stderr) 549 | sys.exit(2) 550 | 551 | 552 | def die(message): 553 | print('error:', message, file=sys.stderr) 554 | sys.exit(2) 555 | 556 | 557 | def to_bytes(str_input): 558 | # Encode to UTF-8 to get binary data. 559 | if isinstance(str_input, bytes): 560 | return str_input 561 | return str_input.encode('utf-8') 562 | 563 | 564 | def to_string(bytes_input): 565 | if isinstance(bytes_input, str): 566 | return bytes_input 567 | return bytes_input.encode('utf-8') 568 | 569 | 570 | def convert_string(bytes_input): 571 | try: 572 | return to_string(bytes_input.decode('utf-8')) 573 | except AttributeError: # 'str' object has no attribute 'decode'. 574 | return str(bytes_input) 575 | except UnicodeError: 576 | return str(bytes_input) 577 | 578 | if __name__ == '__main__': 579 | main() 580 | -------------------------------------------------------------------------------- /proto/BUILD: -------------------------------------------------------------------------------- 1 | package(default_visibility = ["//visibility:public"]) 2 | 3 | load("@rules_proto//proto:defs.bzl", "proto_library") 4 | load("@rules_proto_grpc//cpp:defs.bzl", "cpp_proto_library") 5 | load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library", "cc_proto_library", "cc_test") 6 | load("@rules_proto_grpc//python:defs.bzl", "python_grpc_compile") 7 | 8 | proto_library( 9 | name = "raft_proto", 10 | srcs = ["raft.proto"], 11 | visibility = ["//visibility:public"], 12 | deps = [ 13 | "@com_google_protobuf//:empty_proto", 14 | ], 15 | ) 16 | 17 | cc_proto_library( 18 | name = "raft_cc_proto", 19 | deps = [":raft_proto"], 20 | ) 21 | -------------------------------------------------------------------------------- /proto/protoc.sh: -------------------------------------------------------------------------------- 1 | protoc --cpp_out=. raft.proto && 2 | protoc --grpc_out=. --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` raft.proto -------------------------------------------------------------------------------- /proto/raft.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package raftcpp; 3 | 4 | service raftrpc { 5 | rpc HandleRequestPreVote(PreVoteRequest) returns (PreVoteResponse) {} 6 | rpc HandleRequestVote(VoteRequest) returns (VoteResponse) {} 7 | rpc HandleRequestAppendEntries(AppendEntriesRequest) returns (AppendEntriesResponse) { 8 | } 9 | } 10 | 11 | message PreVoteRequest { 12 | int64 candidate_id = 1; // the ID of the candidate who requested the ballot 13 | int64 term = 2; // candidate's term 14 | int64 last_log_index = 3; // index of candidate’s last log entry 15 | int64 last_log_term = 4; // term of candidate’s last log entry 16 | } 17 | 18 | message PreVoteResponse { 19 | bool vote_granted = 1; // true means candidate received vote 20 | int64 term = 2; // current Term, for candidate to update itself 21 | int64 leader_id = 3; // current Leader id, for candidate to update itself 22 | } 23 | 24 | message VoteRequest { 25 | int64 candidate_id = 1; // the ID of the candidate who requested the ballot 26 | int64 term = 2; // candidate's term 27 | int64 last_log_index = 3; // index of candidate’s last log entry 28 | int64 last_log_term = 4; // term of candidate’s last log entry 29 | } 30 | 31 | message VoteResponse { 32 | bool vote_granted = 1; // true means candidate received vote 33 | int64 term = 2; // current Term, for candidate to update itself 34 | int64 leader_id = 3; // current Leader id, for candidate to update itself 35 | } 36 | 37 | message LogEntry { 38 | int64 term = 1; // log term 39 | int64 index = 2; // log index 40 | string data = 3; // log data 41 | } 42 | 43 | message AppendEntriesRequest { 44 | int64 leader_id = 1; // so follower can redirect clients 45 | int64 term = 2; // leader’s term 46 | int64 leader_commit = 3; // leader’s commitIndex 47 | int64 prev_log_index = 4; // index of log entry immediately preceding new ones 48 | int64 prev_log_term = 5; // term of prev_log_lndex entry 49 | repeated LogEntry entries = 6; // log entries to store (empty for heartbeat; may send 50 | // more than one for efficiency) 51 | } 52 | 53 | message AppendEntriesResponse { 54 | bool success = 55 | 1; // true if follower contained entry matching prev_log_index and prev_log_term 56 | int64 term = 2; // current Term, for leader to update itself 57 | int64 leader_id = 3; // current Leader id, for leader to update itself 58 | int64 conflict_index = 59 | 4; // if the append fails, this field indicates the index of the log conflict 60 | int64 conflict_term = 5; // the term of the conflict index 61 | } -------------------------------------------------------------------------------- /scripts/code-format.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | CURR_DIR=$(cd `dirname $0`; pwd) 4 | 5 | clang-format -i $CURR_DIR/../src/*/* 6 | clang-format -i $CURR_DIR/../tests/*/* 7 | # clang-format -i $CURR_DIR/../proto/* 8 | 9 | echo "============ Code formatted! =============" 10 | -------------------------------------------------------------------------------- /scripts/install-bazel.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -x 3 | set -euo pipefail 4 | 5 | ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE:-$0}")"; pwd) 6 | 7 | arg1="${1-}" 8 | 9 | achitecture="${HOSTTYPE}" 10 | platform="unknown" 11 | case "${OSTYPE}" in 12 | msys) 13 | echo "Platform is Windows." 14 | platform="windows" 15 | # No installer for Windows 16 | ;; 17 | darwin*) 18 | echo "Platform is Mac OS X." 19 | platform="darwin" 20 | ;; 21 | linux*) 22 | echo "Platform is Linux (or WSL)." 23 | platform="linux" 24 | ;; 25 | *) 26 | echo "Unrecognized platform." 27 | exit 1 28 | esac 29 | 30 | # Sanity check: Verify we have symlinks where we expect them, or Bazel can produce weird "missing input file" errors. 31 | # This is most likely to occur on Windows, where symlinks are sometimes disabled by default. 32 | { git ls-files -s 2>/dev/null || true; } | ( 33 | set +x 34 | missing_symlinks=() 35 | while read -r mode _ _ path; do 36 | if [ "${mode}" = 120000 ]; then 37 | test -L "${path}" || missing_symlinks+=("${path}") 38 | fi 39 | done 40 | if [ ! 0 -eq "${#missing_symlinks[@]}" ]; then 41 | echo "error: expected symlink: ${missing_symlinks[*]}" 1>&2 42 | echo "For a correct build, please run 'git config --local core.symlinks true' and re-run git checkout." 1>&2 43 | false 44 | fi 45 | ) 46 | 47 | version="5.3.0" 48 | if [ "${OSTYPE}" = "msys" ]; then 49 | target="${MINGW_DIR-/usr}/bin/bazel.exe" 50 | mkdir -p "${target%/*}" 51 | curl -f -s -L -R -o "${target}" "https://github.com/bazelbuild/bazel/releases/download/${version}/bazel-${version}-${platform}-${achitecture}.exe" 52 | else 53 | target="./install.sh" 54 | curl -f -s -L -R -o "${target}" "https://github.com/bazelbuild/bazel/releases/download/${version}/bazel-${version}-installer-${platform}-${achitecture}.sh" 55 | chmod +x "${target}" 56 | if [[ -n "${BUILDKITE-}" ]] && [ "${platform}" = "darwin" ]; then 57 | "${target}" --user 58 | # Add bazel to the path. 59 | # shellcheck disable=SC2016 60 | printf '\nexport PATH="$HOME/bin:$PATH"\n' >> ~/.zshrc 61 | # shellcheck disable=SC1090 62 | source ~/.zshrc 63 | elif [ "${CI-}" = true ] || [ "${arg1-}" = "--system" ]; then 64 | "$(command -v sudo || echo command)" "${target}" > /dev/null # system-wide install for CI 65 | else 66 | "${target}" --user > /dev/null 67 | export PATH=$PATH:"$HOME/bin" 68 | fi 69 | which bazel 70 | rm -f "${target}" 71 | fi 72 | 73 | for bazel_cfg in ${BAZEL_CONFIG-}; do 74 | echo "build --config=${bazel_cfg}" >> ~/.bazelrc 75 | done 76 | if [ "${TRAVIS-}" = true ]; then 77 | echo "build --config=ci-travis" >> ~/.bazelrc 78 | 79 | # If we are in Travis, most of the compilation result will be cached. 80 | # This means we are I/O bounded. By default, Bazel set the number of concurrent 81 | # jobs to the the number cores on the machine, which are not efficient for 82 | # network bounded cache downloading workload. Therefore we increase the number 83 | # of jobs to 50 84 | # NOTE: Normally --jobs should be under 'build:ci-travis' in .bazelrc, but we put 85 | # it under 'build' here avoid conflicts with other --config options. 86 | echo "build --jobs=50" >> ~/.bazelrc 87 | fi 88 | if [ "${GITHUB_ACTIONS-}" = true ]; then 89 | #echo "build --config=ci-github" >> ~/.bazelrc 90 | echo "build --jobs="$(($(nproc)+2)) >> ~/.bazelrc 91 | fi 92 | if [ "${CI-}" = true ]; then 93 | #echo "build --config=ci" >> ~/.bazelrc 94 | 95 | # In Windows CI we want to use this to avoid long path issue 96 | # https://docs.bazel.build/versions/main/windows.html#avoid-long-path-issues 97 | if [ "${OSTYPE}" = msys ]; then 98 | echo "startup --output_user_root=c:/tmp" >> ~/.bazelrc 99 | fi 100 | 101 | # If we are in master build, we can write to the cache as well. 102 | upload=0 103 | if [ "${TRAVIS_PULL_REQUEST-false}" = false ]; then 104 | # shellcheck disable=SC2154 105 | if [ -n "${BAZEL_CACHE_CREDENTIAL_B64:+x}" ]; then 106 | { 107 | printf "%s" "${BAZEL_CACHE_CREDENTIAL_B64}" | base64 -d - >> "${HOME}/bazel_cache_credential.json" 108 | } 2>&- # avoid printing secrets 109 | upload=1 110 | elif [ -n "${encrypted_1c30b31fe1ee_key:+x}" ]; then 111 | { 112 | # shellcheck disable=SC2154 113 | openssl aes-256-cbc -K "${encrypted_1c30b31fe1ee_key}" \ 114 | -iv "${encrypted_1c30b31fe1ee_iv}" \ 115 | -in "${ROOT_DIR}/bazel_cache_credential.json.enc" \ 116 | -out "${HOME}/bazel_cache_credential.json" -d 117 | } 2>&- # avoid printing secrets 118 | # shellcheck disable=SC2181 119 | if [ 0 -eq $? ]; then 120 | upload=1 121 | fi 122 | fi 123 | fi 124 | if [ 0 -ne "${upload}" ]; then 125 | translated_path=~/bazel_cache_credential.json 126 | if [ "${OSTYPE}" = msys ]; then # On Windows, we need path translation 127 | translated_path="$(cygpath -m -- "${translated_path}")" 128 | fi 129 | cat <> ~/.bazelrc 130 | build --google_credentials="${translated_path}" 131 | EOF 132 | elif [ -n "${BUILDKITE-}" ]; then 133 | 134 | if [ "${platform}" = "darwin" ]; then 135 | echo "Using local disk cache on mac" 136 | cat <> ~/.bazelrc 137 | build --disk_cache=/tmp/bazel-cache 138 | build --repository_cache=/tmp/bazel-repo-cache 139 | EOF 140 | else 141 | echo "Using buildkite secret store to communicate with cache address" 142 | cat <> ~/.bazelrc 143 | build --remote_cache=${BUILDKITE_BAZEL_CACHE_URL} 144 | EOF 145 | if [ "${BUILDKITE_PULL_REQUEST}" != "false" ]; then 146 | echo "build --remote_upload_local_results=false" >> ~/.bazelrc 147 | fi 148 | fi 149 | 150 | else 151 | echo "Using remote build cache in read-only mode." 1>&2 152 | cat <> ~/.bazelrc 153 | build --remote_upload_local_results=false 154 | EOF 155 | fi 156 | fi 157 | -------------------------------------------------------------------------------- /src/common/config.cc: -------------------------------------------------------------------------------- 1 | #include "common/config.h" 2 | 3 | #include 4 | 5 | namespace raftcpp { 6 | 7 | namespace common { 8 | 9 | Config Config::From(const std::string &config_str) { 10 | Config config; 11 | config.other_endpoints_.clear(); 12 | const static std::regex reg(R"((\d{1,3}(\.\d{1,3}){3}:\d+))"); 13 | std::sregex_iterator pos(config_str.begin(), config_str.end(), reg); 14 | decltype(pos) end; 15 | std::set end_point_set; 16 | for (; pos != end; ++pos) { 17 | end_point_set.emplace(pos->str()); 18 | } 19 | 20 | int64_t curr_id = 1; 21 | for (auto &end_point : end_point_set) { 22 | if (curr_id == 1) { 23 | config.this_endpoint_ = std::make_pair(curr_id++, end_point); 24 | } else { 25 | config.PushBack(curr_id++, end_point); 26 | } 27 | } 28 | return config; 29 | } 30 | 31 | } // namespace common 32 | } // namespace raftcpp 33 | -------------------------------------------------------------------------------- /src/common/config.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "endpoint.h" 8 | 9 | namespace raftcpp { 10 | namespace common { 11 | 12 | class Config final { 13 | public: 14 | Config(Config &&c) 15 | : other_endpoints_(std::move(c.other_endpoints_)), 16 | this_endpoint_(std::move(c.this_endpoint_)) {} 17 | 18 | static Config From(const std::string &config_str); 19 | 20 | const std::map &GetOtherEndpoints() const { 21 | return other_endpoints_; 22 | } 23 | 24 | Endpoint GetThisEndpoint() const { return this_endpoint_.second; } 25 | 26 | int64_t GetThisId() const { return this_endpoint_.first; } 27 | 28 | Config(const Config &c) = default; 29 | 30 | size_t GetNodesNum() const { return 1 + other_endpoints_.size(); } 31 | 32 | bool GreaterThanHalfNodesNum(size_t num) const { return num > GetNodesNum() / 2; } 33 | 34 | std::string ToString() const { 35 | std::string s = this_endpoint_.second.ToString(); 36 | for (const auto &e : other_endpoints_) { 37 | s.append("," + e.second.ToString()); 38 | } 39 | return s; 40 | } 41 | 42 | Config &operator=(const Config &c) { 43 | if (&c == this) { 44 | return *this; 45 | } 46 | this_endpoint_ = c.this_endpoint_; 47 | other_endpoints_ = c.other_endpoints_; 48 | return *this; 49 | } 50 | 51 | bool operator==(const Config &c) const { 52 | if (&c == this) { 53 | return true; 54 | } 55 | return this_endpoint_ == c.this_endpoint_ && 56 | other_endpoints_ == c.other_endpoints_; 57 | }; 58 | 59 | bool operator!=(const Config &c) const { return !(*this == c); } 60 | 61 | Config() = default; 62 | 63 | private: 64 | // Push to other endpoints. 65 | void PushBack(int64_t id, Endpoint endpoint) { 66 | other_endpoints_.emplace(id, endpoint); 67 | } 68 | 69 | std::map other_endpoints_; 70 | 71 | std::pair this_endpoint_; 72 | }; 73 | 74 | } // namespace common 75 | } // namespace raftcpp 76 | -------------------------------------------------------------------------------- /src/common/constants.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace raftcpp { 6 | 7 | class RaftcppConstants { 8 | public: 9 | /// Election timeout, randomly selected from the range of base ~ top 10 | constexpr static uint64_t DEFAULT_ELECTION_TIMER_TIMEOUT_BASE_MS = 1500; 11 | 12 | constexpr static uint64_t DEFAULT_ELECTION_TIMER_TIMEOUT_TOP_MS = 3000; 13 | 14 | /// Note that the heartbeat interval must be smaller than election timeout. 15 | /// Otherwise followers will always request pre vote. 16 | constexpr static uint64_t DEFAULT_HEARTBEAT_INTERVAL_MS = 2000; 17 | 18 | /// RPC method names. 19 | constexpr static const char *REQUEST_PRE_VOTE_RPC_NAME = "request_pre_vote"; 20 | 21 | constexpr static const char *REQUEST_VOTE_RPC_NAME = "request_vote"; 22 | 23 | constexpr static const char *REQUEST_HEARTBEAT = "request_heartbeat"; 24 | 25 | constexpr static const char *REQUEST_PUSH_LOGS = "request_push_logs"; 26 | 27 | /// timer keys 28 | constexpr static const char *TIMER_PUSH_LOGS = "push_logs_timer"; 29 | 30 | constexpr static const char *TIMER_HEARTBEAT = "heartbeat_timer"; 31 | 32 | constexpr static const char *TIMER_ELECTION = "election_timer"; 33 | }; 34 | 35 | } // namespace raftcpp 36 | -------------------------------------------------------------------------------- /src/common/endpoint.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | namespace raftcpp { 8 | 9 | class Endpoint { 10 | public: 11 | Endpoint() = default; 12 | 13 | explicit Endpoint(const std::string &address) { 14 | const auto index = address.find(':'); 15 | host_ = address.substr(0, index); 16 | // Note that stoi may throw an exception once the format is incorrect. 17 | port_ = std::stoi(address.substr(index + 1, address.size())); 18 | } 19 | 20 | Endpoint(std::string host, const uint16_t port) 21 | : host_(std::move(host)), port_(port) {} 22 | 23 | std::string ToString() const { return host_ + ":" + std::to_string(port_); } 24 | 25 | std::string GetHost() const { return host_; } 26 | 27 | uint16_t GetPort() const { return port_; } 28 | 29 | bool operator==(const Endpoint &e) const { 30 | return host_ == e.host_ && port_ == e.port_; 31 | } 32 | 33 | bool operator!=(const Endpoint &e) const { return !(*this == e); } 34 | 35 | bool operator()(const Endpoint &lhs, const Endpoint &rhs) const { 36 | char dot; 37 | std::vector v1(4, 0); 38 | std::vector v2(4, 0); 39 | std::istringstream s1(lhs.host_); 40 | std::istringstream s2(rhs.host_); 41 | s1 >> v1[0] >> dot >> v1[1] >> dot >> v1[2] >> dot >> v1[3]; 42 | s2 >> v2[0] >> dot >> v2[1] >> dot >> v2[2] >> dot >> v2[3]; 43 | 44 | uint64_t result1 = (((v1[0] * 255 + v1[1]) * 255 + v1[2]) * 255 + v1[3]); 45 | uint64_t result2 = (((v2[0] * 255 + v2[1]) * 255 + v2[2]) * 255 + v2[3]); 46 | if (result1 < result2) { 47 | return true; 48 | } else if (result1 == result2) { 49 | return lhs.port_ < rhs.port_; 50 | } else { 51 | return false; 52 | } 53 | } 54 | 55 | friend std::ostream &operator<<(std::ostream &out, const Endpoint &e) { 56 | out << e.host_ + ":" + std::to_string(e.port_); 57 | return out; 58 | } 59 | 60 | friend std::istream &operator>>(std::istream &in, Endpoint &e) { 61 | in >> e.host_ >> e.port_; 62 | return in; 63 | } 64 | 65 | private: 66 | std::string host_; 67 | uint16_t port_; 68 | }; 69 | 70 | } // namespace raftcpp 71 | 72 | namespace std { 73 | template <> 74 | struct hash { 75 | std::size_t operator()(const raftcpp::Endpoint &e) const noexcept { 76 | return std::hash()(e.GetHost()) ^ 77 | (std::hash()(e.GetPort()) << 1u); 78 | } 79 | }; 80 | } // namespace std 81 | -------------------------------------------------------------------------------- /src/common/file.cc: -------------------------------------------------------------------------------- 1 | #include "common/file.h" 2 | 3 | namespace raftcpp { 4 | 5 | File File::Open(const std::string &file_name) { 6 | fstream file_id; 7 | file_id.open(file_name.c_str(), ios::in | ios::out | ios::binary | ios::trunc); 8 | File tmp(std::move(file_id), file_name); 9 | return tmp; 10 | } 11 | 12 | void File::CleanAndWrite(const std::string &context) { 13 | if (file_id_.is_open()) { 14 | file_id_.close(); 15 | } 16 | file_id_.open(file_name_.c_str(), ios::in | ios::out | ios::binary | ios::trunc); 17 | file_id_.write(context.c_str(), context.size()); 18 | } 19 | 20 | std::string File::ReadAll() { 21 | size_t file_len = 0; 22 | if (file_id_) { 23 | file_id_.seekg(0, ios::end); 24 | file_len = file_id_.tellg(); 25 | file_id_.seekg(0, ios::beg); 26 | std::string res; 27 | res.resize(file_len); 28 | file_id_.read(const_cast(res.c_str()), file_len); 29 | file_id_.close(); 30 | return res; 31 | } 32 | return std::string(); 33 | } 34 | 35 | } // namespace raftcpp 36 | -------------------------------------------------------------------------------- /src/common/file.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | using namespace std; 6 | 7 | namespace raftcpp { 8 | 9 | class File { 10 | public: 11 | File(fstream file_id, std::string file_name) 12 | : file_id_(std::move(file_id)), file_name_(file_name) {} 13 | 14 | static File Open(const std::string &file_name); 15 | 16 | void CleanAndWrite(const std::string &context); 17 | 18 | std::string ReadAll(); 19 | 20 | private: 21 | fstream file_id_; 22 | std::string file_name_; 23 | }; 24 | 25 | } // namespace raftcpp 26 | -------------------------------------------------------------------------------- /src/common/id.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "endpoint.h" 8 | 9 | namespace raftcpp { 10 | 11 | class BaseID { 12 | public: 13 | BaseID() { data_ = ""; } 14 | 15 | bool operator==(const BaseID &rhs) const { return (this->ToHex() == rhs.ToHex()); } 16 | 17 | bool operator!=(const BaseID &rhs) const { return (this->ToHex() != rhs.ToHex()); } 18 | 19 | std::string ToBinary() const { return data_; } 20 | 21 | // TODO: better way ? 22 | std::string ToHex() const { 23 | std::string result; 24 | result.resize(data_.length() * 2); 25 | for (size_t i = 0; i < data_.size(); i++) { 26 | uint8_t cTemp = data_[i]; 27 | for (size_t j = 0; j < 2; j++) { 28 | uint8_t cCur = (cTemp & 0x0f); 29 | if (cCur < 10) { 30 | cCur += '0'; 31 | } else { 32 | cCur += ('a' - 10); 33 | } 34 | result[2 * i + 1 - j] = cCur; 35 | cTemp >>= 4; 36 | } 37 | } 38 | return result; 39 | } 40 | 41 | protected: 42 | std::string data_; 43 | }; 44 | 45 | class NodeID : public BaseID { 46 | public: 47 | NodeID() {} 48 | 49 | explicit NodeID(const Endpoint &endpoint_id) { 50 | data_ = ""; 51 | data_.resize(sizeof(uint32_t) + sizeof(uint16_t)); 52 | uint32_t inet = ip2uint(endpoint_id.GetHost()); 53 | uint16_t port = endpoint_id.GetPort(); 54 | std::memcpy((void *)data_.data(), &inet, sizeof(uint32_t)); 55 | std::memcpy((void *)(data_.data() + sizeof(uint32_t)), &port, sizeof(uint16_t)); 56 | } 57 | 58 | NodeID(const NodeID &nid) : BaseID(nid) { data_ = nid.data_; } 59 | 60 | NodeID &operator=(const NodeID &o) { 61 | if (this == &o) return *this; 62 | data_ = o.data_; 63 | return *this; 64 | } 65 | 66 | static NodeID FromBinary(const std::string &binary) { 67 | NodeID ret; 68 | ret.data_ = binary; 69 | return ret; 70 | } 71 | 72 | std::ostream &operator<<(std::ostream &os) { 73 | os << "{\n" 74 | << " nodeId:" << ToHex() << "\n}"; 75 | return os; 76 | } 77 | 78 | std::stringstream &operator<<(std::stringstream &ss) { 79 | ss << "{\n" 80 | << " nodeId:" << ToHex() << "\n}"; 81 | return ss; 82 | } 83 | 84 | private: 85 | static std::vector explode(const std::string &s, const char &c) { 86 | std::string buff; 87 | std::vector v; 88 | 89 | for (auto n : s) { 90 | if (n != c) 91 | buff += n; 92 | else if (n == c && !buff.empty()) { 93 | v.push_back(buff); 94 | buff = ""; 95 | } 96 | } 97 | if (buff != "") v.push_back(buff); 98 | 99 | return v; 100 | } 101 | static uint32_t ip2uint(const std::string &ip) { 102 | std::vector v{explode(ip, '.')}; 103 | uint32_t result = 0; 104 | for (auto i = 1; i <= v.size(); i++) 105 | if (i < 4) 106 | result += (stoi(v[i - 1])) << (8 * (4 - i)); 107 | else 108 | result += stoi(v[i - 1]); 109 | 110 | return result; 111 | } 112 | }; 113 | 114 | using term_t = int32_t; 115 | 116 | class TermID : public BaseID { 117 | public: 118 | TermID() { term_ = 0; } 119 | 120 | explicit TermID(term_t term) : term_(term) { 121 | data_ = ""; 122 | data_.resize(sizeof(term_t)); 123 | std::memcpy((void *)data_.data(), &term_, sizeof(term_t)); 124 | } 125 | 126 | TermID(const TermID &tid) : BaseID(tid) { 127 | term_ = tid.term_; 128 | data_ = tid.data_; 129 | } 130 | 131 | TermID &operator=(const TermID &o) { 132 | if (this == &o) return *this; 133 | term_ = o.term_; 134 | data_ = o.data_; 135 | return *this; 136 | } 137 | 138 | term_t getTerm() const { return term_; } 139 | 140 | void setTerm(term_t term) { 141 | term_ = term; 142 | data_ = ""; 143 | data_.resize(sizeof(term_t)); 144 | std::memcpy((void *)data_.data(), &term_, sizeof(term_t)); 145 | } 146 | 147 | // MSGPACK_DEFINE(term_); 148 | 149 | std::ostream &operator<<(std::ostream &os) { 150 | os << "{\n" 151 | << " termId:" << term_ << "\n}"; 152 | return os; 153 | } 154 | 155 | private: 156 | term_t term_; 157 | }; 158 | 159 | } // namespace raftcpp 160 | 161 | namespace std { 162 | template <> 163 | struct hash { 164 | std::size_t operator()(const raftcpp::NodeID &n) const noexcept { 165 | return std::hash()(n.ToBinary()); 166 | } 167 | }; 168 | 169 | template <> 170 | struct hash { 171 | std::size_t operator()(const raftcpp::TermID &t) const noexcept { 172 | return std::hash()(t.getTerm()); 173 | } 174 | }; 175 | } // namespace std 176 | -------------------------------------------------------------------------------- /src/common/logging.cc: -------------------------------------------------------------------------------- 1 | #include "logging.h" 2 | 3 | namespace raftcpp { 4 | static spdlog::level::level_enum GetMappedSeverity(RaftcppLogLevel severity) { 5 | switch (severity) { 6 | case RaftcppLogLevel::RLL_DEBUG: 7 | return spdlog::level::debug; 8 | case RaftcppLogLevel::RLL_INFO: 9 | return spdlog::level::info; 10 | case RaftcppLogLevel::RLL_WARNING: 11 | return spdlog::level::warn; 12 | case RaftcppLogLevel::RLL_ERROR: 13 | return spdlog::level::err; 14 | case RaftcppLogLevel::RLL_FATAL: 15 | return spdlog::level::critical; 16 | default: 17 | return spdlog::level::critical; 18 | } 19 | } 20 | 21 | RaftcppLog::RaftcppLog(const char *file_name, int line_number, RaftcppLogLevel severity) 22 | : filename_(file_name), 23 | line_number_(line_number), 24 | log_level_(std::move(severity)), 25 | is_enabled_(severity >= severity_threshold_) {} 26 | 27 | RaftcppLog::~RaftcppLog() { 28 | try { 29 | if (is_enabled_) { 30 | logging_provider->log(GetMappedSeverity(log_level_), "in {} line:{} {}", 31 | filename_, line_number_, ss_.str()); 32 | } 33 | } catch (const spdlog::spdlog_ex &ex) { 34 | std::cout << "logging_provider->log failed: " << ex.what() << std::endl; 35 | } 36 | } 37 | 38 | void RaftcppLog::StartRaftcppLog(const std::string &log_file_name, 39 | RaftcppLogLevel severity, uint32_t log_file_roll_size_mb, 40 | uint32_t log_file_roll_cout) { 41 | severity_threshold_ = severity; 42 | if (logging_provider == nullptr) { 43 | try { 44 | logging_provider = ::spdlog::rotating_logger_mt( 45 | "raftcpp_log", log_file_name, 1024 * 1024 * 1024 * log_file_roll_size_mb, 46 | log_file_roll_cout); 47 | spdlog::set_level(GetMappedSeverity(severity)); 48 | logging_provider->flush_on(spdlog::level::debug); 49 | } catch (const spdlog::spdlog_ex &ex) { 50 | std::cout << "RaftcppLog failed: " << ex.what() << std::endl; 51 | } 52 | } 53 | } 54 | 55 | bool RaftcppLog::IsEnabled() const { return is_enabled_; } 56 | 57 | bool RaftcppLog::IsLevelEnabled(RaftcppLogLevel log_level) { 58 | return log_level >= severity_threshold_; 59 | } 60 | 61 | void RaftcppLog::ShutDownRaftcppLog() { spdlog::shutdown(); } 62 | 63 | std::shared_ptr RaftcppLog::logging_provider = nullptr; 64 | RaftcppLogLevel RaftcppLog::severity_threshold_ = RaftcppLogLevel::RLL_NOLEVEL; 65 | 66 | } // namespace raftcpp 67 | -------------------------------------------------------------------------------- /src/common/logging.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "common/id.h" 8 | #include "spdlog/sinks/rotating_file_sink.h" 9 | #include "spdlog/spdlog.h" 10 | 11 | namespace raftcpp { 12 | enum class RaftcppLogLevel { 13 | RLL_DEBUG, 14 | RLL_INFO, 15 | RLL_WARNING, 16 | RLL_ERROR, 17 | RLL_FATAL, 18 | RLL_NOLEVEL 19 | }; 20 | class RaftcppLogBase { 21 | public: 22 | virtual ~RaftcppLogBase(){}; 23 | 24 | virtual bool IsEnabled() const { return false; }; 25 | 26 | template 27 | RaftcppLogBase &operator<<(const T &t) { 28 | if (IsEnabled()) { 29 | ss_ << t; 30 | } 31 | return *this; 32 | } 33 | 34 | RaftcppLogBase &operator<<(NodeID &id) { 35 | if (IsEnabled()) { 36 | id << ss_; 37 | } 38 | return *this; 39 | } 40 | 41 | protected: 42 | std::stringstream ss_; 43 | }; 44 | 45 | class RaftcppLog : public RaftcppLogBase { 46 | public: 47 | RaftcppLog(const char *file_name, int line_number, RaftcppLogLevel severity); 48 | 49 | ~RaftcppLog(); 50 | 51 | static void StartRaftcppLog(const std::string &log_file_name, 52 | RaftcppLogLevel severity, uint32_t log_file_roll_size_mb, 53 | uint32_t log_file_roll_cout); 54 | 55 | bool IsEnabled() const; 56 | 57 | static bool IsLevelEnabled(RaftcppLogLevel log_level); 58 | 59 | static void ShutDownRaftcppLog(); 60 | 61 | private: 62 | bool is_enabled_; 63 | RaftcppLogLevel log_level_; 64 | std::string filename_; 65 | int line_number_; 66 | static std::shared_ptr logging_provider; 67 | static RaftcppLogLevel severity_threshold_; 68 | 69 | protected: 70 | }; 71 | 72 | class Voidify { 73 | public: 74 | Voidify() { std::abort(); } 75 | 76 | void operator&(RaftcppLogBase &) {} 77 | }; 78 | 79 | #ifdef _WIN32 80 | #define spdlogfilename(x) strrchr(x, '\\') ? strrchr(x, '\\') + 1 : x 81 | #else 82 | #define spdlogfilename(x) strrchr(x, '/') ? strrchr(x, '/') + 1 : x 83 | #endif 84 | 85 | #define RAFTCPP_LOG_INTERNAL(level) \ 86 | ::raftcpp::RaftcppLog(spdlogfilename(__FILE__), __LINE__, level) 87 | #define RAFTCPP_LOG(level) \ 88 | if (raftcpp::RaftcppLog::IsLevelEnabled(raftcpp::RaftcppLogLevel::level)) \ 89 | RAFTCPP_LOG_INTERNAL(raftcpp::RaftcppLogLevel::level) 90 | 91 | #define RAFTCPP_LOG_ENABLED(level) \ 92 | raftcpp::RaftcppLog::IsLevelEnabled(raftcpp::RaftcppLogLevel::level) 93 | #define RAFTCPP_IGNORE_EXPR(expr) ((void)(expr)) 94 | #define RAFTCPP_CHECK(condition) \ 95 | (condition) ? RAFTCPP_IGNORE_EXPR(0) \ 96 | : ::raftcpp::Voidify() & \ 97 | ::raftcpp::RaftcppLog(__FILE__, __LINE__, \ 98 | raftcpp::RaftcppLogLevel::RLL_FATAL) \ 99 | << " Check failed: " #condition " " 100 | } // namespace raftcpp 101 | -------------------------------------------------------------------------------- /src/common/randomer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "util.h" 6 | namespace raftcpp { 7 | class Randomer { 8 | public: 9 | Randomer() { e.seed(common::CurrentTimeUs()); } 10 | 11 | Randomer(const Randomer &rd) = delete; 12 | 13 | Randomer &operator=(const Randomer &rd) = delete; 14 | 15 | uint64_t TakeOne(const uint64_t begin, const uint64_t end) { 16 | u.param(std::uniform_int_distribution::param_type{begin, end}); 17 | return u(e); 18 | } 19 | 20 | private: 21 | std::default_random_engine e; 22 | std::uniform_int_distribution u; 23 | }; 24 | } // namespace raftcpp -------------------------------------------------------------------------------- /src/common/status.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace raftcpp { 4 | 5 | enum class Status { 6 | OK = 0, 7 | 8 | UNKNOWN_REQUEST = 100, 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /src/common/timer.cc: -------------------------------------------------------------------------------- 1 | #include "src/common/timer.h" 2 | 3 | #include 4 | 5 | namespace raftcpp { 6 | namespace common { 7 | 8 | void RepeatedTimer::Start(const uint64_t timeout_ms) { Reset(timeout_ms); } 9 | 10 | void RepeatedTimer::Stop() { is_running_.store(false); } 11 | 12 | void RepeatedTimer::Reset(const uint64_t timeout_ms) { 13 | is_running_.store(true); 14 | DoSetExpired(timeout_ms); 15 | } 16 | 17 | bool RepeatedTimer::IsRunning() const { return is_running_.load(); } 18 | 19 | void RepeatedTimer::DoSetExpired(const uint64_t timeout_ms) { 20 | if (!is_running_.load()) { 21 | return; 22 | } 23 | 24 | timer_.expires_from_now(std::chrono::milliseconds(timeout_ms)); 25 | timer_.async_wait([this, timeout_ms](const asio::error_code &e) { 26 | if (e.value() == asio::error::operation_aborted || !is_running_.load()) { 27 | // The timer was canceled. 28 | return; 29 | } 30 | timeout_handler_(e); 31 | this->DoSetExpired(timeout_ms); 32 | }); 33 | } 34 | 35 | } // namespace common 36 | } // namespace raftcpp 37 | -------------------------------------------------------------------------------- /src/common/timer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "common/util.h" 13 | 14 | namespace raftcpp { 15 | 16 | namespace common { 17 | 18 | /** 19 | * A class that encapsulates a boost timer as a repeated timer with a fixed 20 | * expired time. This class is thread safe to use. 21 | * 22 | * Note that the io_service must be ran after we create this timer, otherwise 23 | * it may cause that the io_service has no event and it will exit at once. 24 | */ 25 | class RepeatedTimer final { 26 | public: 27 | RepeatedTimer(asio::io_service &io_service, 28 | std::function timeout_handler) 29 | : io_service_(io_service), 30 | timer_(io_service_), 31 | timeout_handler_(std::move(timeout_handler)) {} 32 | 33 | ~RepeatedTimer() { Stop(); } 34 | 35 | void Start(uint64_t timeout_ms); 36 | 37 | void Stop(); 38 | 39 | void Reset(uint64_t timeout_ms); 40 | 41 | bool IsRunning() const; 42 | 43 | private: 44 | void DoSetExpired(uint64_t timeout_ms); 45 | 46 | private: 47 | // The io service that runs this timer. 48 | asio::io_service &io_service_; 49 | 50 | // The actual boost timer. 51 | asio::steady_timer timer_; 52 | 53 | std::atomic is_running_{false}; 54 | 55 | // The handler that will be triggered once the time's up. 56 | std::function timeout_handler_; 57 | }; 58 | 59 | class ContinuousTimer final { 60 | public: 61 | explicit ContinuousTimer(asio::io_service &ios, size_t timeout_ms, 62 | std::function handler) 63 | : timer_(ios), timeout_ms_(timeout_ms), timeout_handler_(std::move(handler)) {} 64 | 65 | void Start() { RunTimer(); } 66 | 67 | void Cancel() { 68 | if (timeout_ms_ == 0) { 69 | return; 70 | } 71 | 72 | asio::error_code ec; 73 | timer_.cancel(ec); 74 | timeout_ms_ = 0; 75 | } 76 | 77 | private: 78 | void RunTimer() { 79 | if (timeout_ms_ == 0) { 80 | return; 81 | } 82 | 83 | timer_.expires_from_now(std::chrono::milliseconds(timeout_ms_)); 84 | timer_.async_wait([this](const asio::error_code &e) { 85 | timeout_handler_(e); 86 | RunTimer(); 87 | }); 88 | } 89 | 90 | private: 91 | // The actual boost timer. 92 | asio::steady_timer timer_; 93 | 94 | // The timeout time: ms. 95 | size_t timeout_ms_; 96 | 97 | // The handler that will be triggered once the time's up. 98 | std::function timeout_handler_; 99 | }; 100 | 101 | } // namespace common 102 | } // namespace raftcpp 103 | -------------------------------------------------------------------------------- /src/common/timer_manager.cc: -------------------------------------------------------------------------------- 1 | #include "timer_manager.h" 2 | 3 | #include "src/common/logging.h" 4 | 5 | namespace raftcpp { 6 | namespace common { 7 | 8 | TimerManager::TimerManager() { 9 | io_service_ = std::make_unique(); 10 | work_ = std::make_unique(*io_service_); 11 | } 12 | 13 | TimerManager::~TimerManager() { 14 | for (auto &iter : timers_) { 15 | iter.second->Stop(); 16 | } 17 | 18 | io_service_->stop(); 19 | thread_->join(); 20 | } 21 | 22 | void TimerManager::Run() { 23 | // Note that the `Start()` should be invoked before `io_service->run()`. 24 | thread_ = std::make_unique([this]() { io_service_->run(); }); 25 | } 26 | 27 | void TimerManager::StartTimer(const std::string &timer_key, uint64_t timeout_ms) { 28 | RAFTCPP_CHECK(timers_.end() != timers_.find(timer_key)); 29 | timers_[timer_key]->Start(timeout_ms); 30 | } 31 | 32 | void TimerManager::ResetTimer(const std::string &timer_key, uint64_t timeout_ms) { 33 | RAFTCPP_CHECK(timers_.end() != timers_.find(timer_key)); 34 | timers_[timer_key]->Reset(timeout_ms); 35 | } 36 | 37 | void TimerManager::StopTimer(const std::string &timer_key) { 38 | RAFTCPP_CHECK(timers_.end() != timers_.find(timer_key)); 39 | timers_[timer_key]->Stop(); 40 | } 41 | 42 | bool TimerManager::IsTimerRunning(const std::string &timer_key) const { 43 | RAFTCPP_CHECK(timers_.end() != timers_.find(timer_key)); 44 | return timers_.at(timer_key)->IsRunning(); 45 | } 46 | 47 | void TimerManager::RegisterTimer(const std::string &timer_key, 48 | const std::function &handler) { 49 | RAFTCPP_CHECK(handler != nullptr); 50 | RAFTCPP_CHECK(timers_.end() == timers_.find(timer_key)); 51 | timers_.insert(std::make_pair( 52 | timer_key, std::make_shared( 53 | *io_service_, [handler](const asio::error_code &e) { 54 | if (e.value() != asio::error::operation_aborted) { 55 | handler(); 56 | } 57 | }))); 58 | } 59 | 60 | } // namespace common 61 | } // namespace raftcpp 62 | -------------------------------------------------------------------------------- /src/common/timer_manager.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "common/randomer.h" 11 | #include "common/timer.h" 12 | 13 | namespace raftcpp { 14 | namespace common { 15 | 16 | /** 17 | * The manager class to manage all of the timers for node. 18 | */ 19 | class TimerManager final { 20 | public: 21 | explicit TimerManager(); 22 | 23 | ~TimerManager(); 24 | 25 | // run the manager, as start run timers that be registered 26 | void Run(); 27 | 28 | /// Register timer by timer handler func, and return timer id that be assigned. 29 | void RegisterTimer(const std::string &timer_key, 30 | const std::function &handler); 31 | 32 | // enclosure timer's operation by timer id 33 | void StartTimer(const std::string &timer_key, uint64_t timeout_ms); 34 | void ResetTimer(const std::string &timer_key, uint64_t timeout_ms); 35 | void StopTimer(const std::string &timer_key); 36 | bool IsTimerRunning(const std::string &timer_key) const; 37 | 38 | private: 39 | // A separated service that runs for all timers. 40 | std::unique_ptr io_service_ = nullptr; 41 | 42 | std::unique_ptr work_ = nullptr; 43 | 44 | // The thread that runs all timers. 45 | std::unique_ptr thread_ = nullptr; 46 | 47 | std::map> timers_; 48 | }; 49 | 50 | } // namespace common 51 | } // namespace raftcpp 52 | -------------------------------------------------------------------------------- /src/common/type_def.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace raftcpp { 4 | 5 | /** 6 | * The RaftState that represents the current state of a node. 7 | */ 8 | enum class RaftState { 9 | LEADER = 0, 10 | PRECANDIDATE = 1, 11 | CANDIDATE = 2, 12 | FOLLOWER = 3, 13 | }; 14 | 15 | } // namespace raftcpp 16 | -------------------------------------------------------------------------------- /src/common/util.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace raftcpp { 7 | namespace common { 8 | 9 | /** 10 | * Return the current timestamp in milliseconds in a steady clock. 11 | */ 12 | inline int64_t CurrentTimeMs() { 13 | std::chrono::milliseconds ms_since_epoch = 14 | std::chrono::duration_cast( 15 | std::chrono::steady_clock::now().time_since_epoch()); 16 | return ms_since_epoch.count(); 17 | } 18 | 19 | inline int64_t CurrentTimeUs() { 20 | std::chrono::microseconds us_since_epoch = 21 | std::chrono::duration_cast( 22 | std::chrono::steady_clock::now().time_since_epoch()); 23 | return us_since_epoch.count(); 24 | } 25 | 26 | inline uint64_t RandomNumber(const uint64_t begin, const uint64_t end) { 27 | srand(CurrentTimeMs()); 28 | return (rand() % (end - begin)) + begin; 29 | } 30 | 31 | } // namespace common 32 | } // namespace raftcpp 33 | -------------------------------------------------------------------------------- /src/log_manager/blocking_queue_interface.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | namespace raftcpp { 5 | 6 | /** 7 | * The interface that represents a blocking queue. 8 | */ 9 | template 10 | class BlockingQueueInterface { 11 | public: 12 | virtual ~BlockingQueueInterface() = default; 13 | 14 | /** 15 | * Pop the front element from blocking queue. Note that 16 | * this will be blocked if there is no log in the queue. 17 | */ 18 | virtual LogEntryType Pop() = 0; 19 | 20 | /** 21 | * Pop the front element from blocking queue, if the queue is 22 | * empty, it will return false. 23 | */ 24 | virtual bool Pop(LogEntryType &log_entry) = 0; 25 | 26 | /** 27 | * Push the log to this log manager. 28 | */ 29 | virtual void Push(const LogEntryType &log_entry) = 0; 30 | 31 | /** 32 | * Get the most front items from the queue. 33 | */ 34 | virtual std::vector MostFront(int mostFrontNumber) = 0; 35 | }; 36 | 37 | } // namespace raftcpp 38 | -------------------------------------------------------------------------------- /src/log_manager/blocking_queue_mutex_impl.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "blocking_queue_interface.h" 8 | 9 | namespace raftcpp { 10 | 11 | template 12 | class BlockingQueueMutexImpl : public BlockingQueueInterface { 13 | public: 14 | BlockingQueueMutexImpl() = default; 15 | 16 | ~BlockingQueueMutexImpl() = default; 17 | 18 | virtual LogEntryType Pop() override; 19 | 20 | virtual bool Pop(LogEntryType &log_entry) override; 21 | 22 | virtual void Push(const LogEntryType &log_entry) override; 23 | 24 | virtual std::vector MostFront(int mostFrontNumber) override; 25 | 26 | private: 27 | std::mutex queue_mutex_; 28 | std::condition_variable queue_cv_; 29 | std::queue queue_; 30 | }; 31 | 32 | template 33 | LogEntryType BlockingQueueMutexImpl::Pop() { 34 | std::unique_lock lock(queue_mutex_); 35 | queue_cv_.wait(lock, [this] { return !queue_.empty(); }); 36 | LogEntryType log_entry_type = queue_.front(); 37 | queue_.pop(); 38 | return log_entry_type; 39 | } 40 | 41 | template 42 | bool BlockingQueueMutexImpl::Pop(LogEntryType &log_entry) { 43 | std::unique_lock lock(queue_mutex_); 44 | if (!queue_.empty()) { 45 | log_entry = queue_.front(); 46 | queue_.pop(); 47 | return true; 48 | } 49 | return false; 50 | } 51 | 52 | template 53 | void BlockingQueueMutexImpl::Push(const LogEntryType &log_entry) { 54 | std::unique_lock lock(queue_mutex_); 55 | queue_.push(log_entry); 56 | queue_cv_.notify_all(); 57 | } 58 | 59 | template 60 | std::vector BlockingQueueMutexImpl::MostFront( 61 | int mostFrontNumber) { 62 | std::vector v; 63 | std::unique_lock lock(queue_mutex_); 64 | if (mostFrontNumber <= 0) { 65 | return v; 66 | } 67 | if (queue_.size() < mostFrontNumber) { 68 | mostFrontNumber = queue_.size(); 69 | } 70 | // TODO: Just copy a vector form the queue. 71 | while (!queue_.empty()) { 72 | v.push_back(queue_.front()); 73 | queue_.pop(); 74 | } 75 | 76 | for (int i = 0; i < v.size(); i++) { 77 | queue_.push(v[i]); 78 | } 79 | 80 | typename std::vector::iterator itor1 = v.begin() + mostFrontNumber; 81 | typename std::vector::iterator itor2 = v.end(); 82 | v.erase(itor1, itor2); 83 | 84 | return v; 85 | } 86 | 87 | } // namespace raftcpp 88 | -------------------------------------------------------------------------------- /src/log_manager/leader_log_manager.cc: -------------------------------------------------------------------------------- 1 | #include "src/log_manager/leader_log_manager.h" 2 | 3 | #include 4 | #include 5 | 6 | #include "proto/raft.pb.h" 7 | 8 | namespace raftcpp { 9 | 10 | LeaderLogManager::LeaderLogManager( 11 | int64_t this_node_id, std::function get_all_rpc_clients_func, 12 | const std::shared_ptr &timer_manager) 13 | : io_service_(), 14 | this_node_id_(this_node_id), 15 | get_all_rpc_clients_func_(get_all_rpc_clients_func), 16 | all_log_entries_(), 17 | timer_manager_(timer_manager) { 18 | timer_manager->RegisterTimer(RaftcppConstants::TIMER_PUSH_LOGS, 19 | std::bind(&LeaderLogManager::DoPushLogs, this)); 20 | } 21 | 22 | void LeaderLogManager::Push(int64_t term_id, LogEntry &entry) { 23 | std::lock_guard lock(mutex_); 24 | ++curr_log_index_; 25 | entry.set_term(term_id); 26 | entry.set_index(curr_log_index_); 27 | all_log_entries_[curr_log_index_] = entry; 28 | } 29 | 30 | void LeaderLogManager::Run(std::unordered_map &logs, 31 | int64_t committedIndex) { 32 | std::lock_guard lock(mutex_); 33 | auto size = static_cast(logs.size()); 34 | curr_log_index_ = size - 1; 35 | committed_log_index_ = committedIndex; 36 | all_log_entries_.swap(logs); 37 | 38 | timer_manager_->StartTimer(RaftcppConstants::TIMER_PUSH_LOGS, 1000); 39 | } 40 | 41 | void LeaderLogManager::Stop() { 42 | timer_manager_->StopTimer(RaftcppConstants::TIMER_PUSH_LOGS); 43 | } 44 | 45 | void LeaderLogManager::TryAsyncCommitLogs( 46 | int64_t node_id, size_t next_log_index, 47 | std::function committed_callback) { 48 | /// TODO(qwang): Trigger commit. 49 | 50 | std::vector index_nums; 51 | for (const auto &it : match_log_indexes_) { 52 | index_nums.emplace_back(it.second); 53 | } 54 | index_nums.emplace_back(curr_log_index_); 55 | 56 | std::sort(index_nums.begin(), index_nums.end()); 57 | auto size = index_nums.size(); 58 | auto i = (size % 2 == 0) ? size / 2 : size / 2 + 1; 59 | int media_index = index_nums.at(i); 60 | if (media_index > committed_log_index_) { 61 | committed_log_index_ = media_index; 62 | } 63 | } 64 | 65 | void LeaderLogManager::DoPushLogs() { 66 | std::lock_guard lock(mutex_); 67 | auto all_rpc_clients = get_all_rpc_clients_func_(); 68 | for (const auto &follower : all_rpc_clients) { 69 | const auto &follower_node_id = follower.first; 70 | // Filter this node self. 71 | if (follower_node_id == this_node_id_) { 72 | continue; 73 | } 74 | 75 | if (next_log_indexes_.find(follower_node_id) == next_log_indexes_.end()) { 76 | continue; 77 | } 78 | auto next_log_index_to_be_sent = next_log_indexes_[follower_node_id]; 79 | 80 | // get log entry 81 | auto it = all_log_entries_.find(next_log_index_to_be_sent); 82 | if (it == all_log_entries_.end()) { 83 | continue; 84 | } 85 | 86 | // get pre_log_term 87 | int32_t pre_log_term_num = -1; 88 | if (next_log_index_to_be_sent > 0) { 89 | auto it = all_log_entries_.find(next_log_index_to_be_sent - 1); 90 | if (it != all_log_entries_.end()) { 91 | // pre_log_term_num = it->second.term_id.getTerm(); 92 | pre_log_term_num = it->second.term(); 93 | } 94 | } 95 | 96 | // do request push log 97 | auto &follower_rpc_client = follower.second; 98 | // TODO: (luhuanbing) repair push logic 99 | // follower_rpc_client->async_call( 100 | // RaftcppConstants::REQUEST_PUSH_LOGS, 101 | // [this, next_log_index_to_be_sent, follower_node_id]( 102 | // boost::system::error_code ec, string_view data) { 103 | // /** 104 | // * The return parameters are success and lastLogIdx 105 | // * @param success if push logs successfully 106 | // * @param lastLogIdx response tells the leader the last log index it 107 | // has 108 | // */ 109 | // msgpack::type::tuple response; 110 | // msgpack::object_handle oh = msgpack::unpack(data.data(), data.size()); 111 | // msgpack::object obj = oh.get(); 112 | // obj.convert(response); 113 | 114 | // bool success = response.get<0>(); 115 | // int64_t resp_last_log_idx = response.get<1>(); 116 | 117 | // std::lock_guard lock(this->mutex_); 118 | 119 | // auto iter = this->next_log_indexes_.find(follower_node_id); 120 | // if (success) { 121 | // iter->second = next_log_index_to_be_sent; 122 | // } else { 123 | // iter->second = resp_last_log_idx; 124 | // } 125 | // }, 126 | // /*committed_log_index=*/committed_log_index_, 127 | // /*pre_log_term_num=*/pre_log_term_num, 128 | // /*log_entry=*/it->second); 129 | } 130 | } 131 | 132 | } // namespace raftcpp 133 | -------------------------------------------------------------------------------- /src/log_manager/leader_log_manager.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "blocking_queue_interface.h" 10 | #include "blocking_queue_mutex_impl.h" 11 | #include "proto/raft.grpc.pb.h" 12 | #include "proto/raft.pb.h" 13 | #include "src/common/constants.h" 14 | #include "src/common/id.h" 15 | #include "src/common/timer.h" 16 | #include "src/common/timer_manager.h" 17 | 18 | namespace raftcpp { 19 | 20 | using AllRpcClientType = std::unordered_map>; 21 | 22 | /// TODO(qwang): Should clean all inmemory data once this is Ran(). 23 | class LeaderLogManager final { 24 | public: 25 | explicit LeaderLogManager(int64_t this_node_id, 26 | std::function get_all_rpc_clients_func, 27 | const std::shared_ptr &timer_manager); 28 | 29 | ~LeaderLogManager() { timer_manager_->StopTimer(RaftcppConstants::TIMER_PUSH_LOGS); } 30 | 31 | void Push(int64_t term_id, LogEntry &entry); 32 | 33 | void Run(std::unordered_map &logs, int64_t committedIndex); 34 | 35 | void Stop(); 36 | 37 | int64_t CurrLogIndex() const { 38 | std::lock_guard lock(mutex_); 39 | return curr_log_index_; 40 | } 41 | 42 | // attention to all_log_entries_ may be large, so as far as possible no copy 43 | std::unordered_map &Logs() { 44 | std::lock_guard lock(mutex_); 45 | return all_log_entries_; 46 | } 47 | 48 | int64_t CommittedLogIndex() const { 49 | std::lock_guard lock(mutex_); 50 | return committed_log_index_; 51 | } 52 | 53 | private: 54 | /// Try to commit the logs asynchronously. If a log was replied 55 | /// by more than one half of followers, it will be async-commit, 56 | /// and apply the user state machine. otherwise we don't dump it. 57 | void TryAsyncCommitLogs(int64_t node_id, size_t next_log_index, 58 | std::function committed_callback); 59 | 60 | void DoPushLogs(); 61 | 62 | private: 63 | mutable std::mutex mutex_; 64 | 65 | /// The largest log index that not committed in this leader. 66 | int64_t curr_log_index_ = -1; 67 | 68 | int64_t committed_log_index_ = -1; 69 | 70 | std::unordered_map all_log_entries_; 71 | 72 | /// The map that contains the non-leader nodes to the next_log_index. 73 | std::unordered_map next_log_indexes_; 74 | 75 | /// The map that contains the non-leader nodes to follower already has max log index. 76 | std::unordered_map match_log_indexes_; 77 | 78 | /// The function to get all rpc clients to followers(Including this node self). 79 | std::function get_all_rpc_clients_func_; 80 | 81 | /// ID of this node. 82 | int64_t this_node_id_; 83 | 84 | asio::io_service io_service_; 85 | 86 | /// TODO(qwang): This shouldn't be hardcode. 87 | constexpr static size_t NODE_NUM = 3; 88 | 89 | constexpr static size_t MAX_LOG_INDEX = 1000000000; 90 | 91 | std::shared_ptr timer_manager_; 92 | }; 93 | 94 | } // namespace raftcpp 95 | -------------------------------------------------------------------------------- /src/log_manager/non_leader_log_manager.cc: -------------------------------------------------------------------------------- 1 | #include "non_leader_log_manager.h" 2 | 3 | #include 4 | #include 5 | 6 | #include "src/common/logging.h" 7 | #include "src/statemachine/state_machine.h" 8 | 9 | namespace raftcpp { 10 | 11 | NonLeaderLogManager::NonLeaderLogManager( 12 | int64_t this_node_id, std::shared_ptr fsm, 13 | std::function is_leader_func, 14 | std::function()> get_leader_rpc_client_func, 15 | const std::shared_ptr &timer_manager) 16 | : this_node_id_(this_node_id), 17 | is_leader_func_(std::move(is_leader_func)), 18 | is_running_(false), 19 | get_leader_rpc_client_func_(std::move(get_leader_rpc_client_func)), 20 | fsm_(std::move(fsm)), 21 | timer_manager_(timer_manager) {} 22 | 23 | void NonLeaderLogManager::Run(std::unordered_map &logs, 24 | int64_t committedIndex) { 25 | std::lock_guard lock(mutex_); 26 | committed_log_index_ = committedIndex; 27 | next_index_ = logs.size(); 28 | all_log_entries_.swap(logs); 29 | } 30 | 31 | void NonLeaderLogManager::Stop() { is_running_.store(false); } 32 | 33 | bool NonLeaderLogManager::IsRunning() const { return is_running_.load(); } 34 | 35 | void NonLeaderLogManager::Push(int64_t committed_log_index, int64_t pre_log_term, 36 | LogEntry log_entry) { 37 | std::lock_guard lock(mutex_); 38 | // RAFTCPP_CHECK(log_entry.log_index >= 0); 39 | RAFTCPP_CHECK(log_entry.index() >= 0); 40 | 41 | /// Ignore if duplicated log_index. 42 | if (all_log_entries_.count(log_entry.index()) > 0) { 43 | RAFTCPP_LOG(RLL_DEBUG) << "Duplicated log index = " << log_entry.index(); 44 | } 45 | 46 | auto pre_log_index = log_entry.index() - 1; 47 | if (log_entry.index() > 0) { 48 | auto it = all_log_entries_.find(pre_log_index); 49 | if (it == all_log_entries_.end() || it->second.term() != pre_log_term) { 50 | next_index_ = pre_log_index; 51 | push_log_result_ = false; 52 | 53 | RAFTCPP_LOG(RLL_DEBUG) << "lack of log index = " << pre_log_index; 54 | return; 55 | } 56 | } 57 | 58 | auto req_term = log_entry.term(); 59 | auto it = all_log_entries_.find(log_entry.index()); 60 | if (it != all_log_entries_.end() && it->second.term() != req_term) { 61 | auto index = log_entry.index(); 62 | while ((it = all_log_entries_.find(index)) != all_log_entries_.end()) { 63 | all_log_entries_.erase(it); 64 | index++; 65 | } 66 | 67 | next_index_ = log_entry.index(); 68 | RAFTCPP_LOG(RLL_DEBUG) << "conflict at log index = " << next_index_; 69 | } 70 | 71 | all_log_entries_[log_entry.index()] = log_entry; 72 | if (log_entry.index() >= next_index_) { 73 | next_index_ = log_entry.index() + 1; 74 | } 75 | 76 | push_log_result_ = true; 77 | CommitLogs(committed_log_index); 78 | } 79 | 80 | void NonLeaderLogManager::CommitLogs(int64_t committed_log_index) { 81 | if (committed_log_index <= committed_log_index_) { 82 | return; 83 | } 84 | const auto last_committed_log_index = committed_log_index; 85 | committed_log_index_ = committed_log_index; 86 | for (auto index = last_committed_log_index + 1; index <= committed_log_index_; 87 | ++index) { 88 | RAFTCPP_CHECK(all_log_entries_.count(index) == 1); 89 | fsm_->OnApply(all_log_entries_[index].data()); 90 | } 91 | } 92 | 93 | } // namespace raftcpp 94 | -------------------------------------------------------------------------------- /src/log_manager/non_leader_log_manager.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "common/constants.h" 10 | #include "common/id.h" 11 | #include "common/timer.h" 12 | #include "common/timer_manager.h" 13 | #include "log_manager/blocking_queue_interface.h" 14 | #include "log_manager/blocking_queue_mutex_impl.h" 15 | #include "proto/raft.grpc.pb.h" 16 | #include "proto/raft.pb.h" 17 | #include "statemachine/state_machine.h" 18 | 19 | namespace raftcpp { 20 | 21 | class NonLeaderLogManager final { 22 | public: 23 | NonLeaderLogManager( 24 | int64_t this_node_id, std::shared_ptr fsm, 25 | std::function is_leader_func, 26 | std::function()> get_leader_rpc_client_func, 27 | const std::shared_ptr &timer_manager); 28 | 29 | ~NonLeaderLogManager(){}; 30 | 31 | void Run(std::unordered_map &logs, int64_t committedIndex); 32 | void Stop(); 33 | bool IsRunning() const; 34 | 35 | void Push(int64_t committed_log_index, int64_t pre_log_term, LogEntry log_entry); 36 | 37 | int64_t CurrLogIndex() const { 38 | std::lock_guard lock(mutex_); 39 | return next_index_ - 1; 40 | } 41 | 42 | // attention to all_log_entries_ may be large, so as far as possible no copy 43 | std::unordered_map &Logs() { 44 | std::lock_guard lock(mutex_); 45 | return all_log_entries_; 46 | } 47 | 48 | int64_t CommittedLogIndex() const { 49 | std::lock_guard lock(mutex_); 50 | return committed_log_index_; 51 | } 52 | 53 | private: 54 | void CommitLogs(int64_t committed_log_index); 55 | 56 | private: 57 | mutable std::mutex mutex_; 58 | 59 | /// The index which the leader committed. 60 | int64_t committed_log_index_ = -1; 61 | 62 | /// Next index to be read from leader. 63 | int64_t next_index_ = 0; 64 | 65 | /// Handle push log result 66 | bool push_log_result_ = true; 67 | 68 | std::unordered_map all_log_entries_; 69 | 70 | asio::io_service io_service_; 71 | 72 | std::unique_ptr committing_thread_; 73 | 74 | /// The function to get leader rpc client. 75 | std::function()> get_leader_rpc_client_func_; 76 | 77 | std::function is_leader_func_ = nullptr; 78 | 79 | std::atomic_bool is_running_{false}; 80 | 81 | int64_t this_node_id_; 82 | 83 | std::shared_ptr fsm_; 84 | 85 | std::shared_ptr timer_manager_; 86 | 87 | /// The timer used to send pull log entries requests to the leader. 88 | /// Note that this is only used in non-leader node. 89 | }; 90 | 91 | } // namespace raftcpp 92 | -------------------------------------------------------------------------------- /src/node/node.cc: -------------------------------------------------------------------------------- 1 | #include "node.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | #include "src/common/constants.h" 13 | #include "src/common/logging.h" 14 | #include "src/common/type_def.h" 15 | 16 | namespace raftcpp::node { 17 | 18 | RaftNode::RaftNode(std::shared_ptr state_machine, 19 | // std::unique_ptr rpc_server, 20 | const common::Config &config, RaftcppLogLevel severity) 21 | : // rpc_server_(std::move(rpc_server)), 22 | raftrpc::Service(), 23 | config_(config), 24 | this_node_id_(config.GetThisId()), 25 | timer_manager_(std::make_shared()), 26 | leader_log_manager_(std::make_unique( 27 | this_node_id_, [this]() -> AllRpcClientType { return all_rpc_clients_; }, 28 | timer_manager_)), 29 | 30 | non_leader_log_manager_(std::make_unique( 31 | this_node_id_, state_machine, 32 | [this]() { 33 | std::lock_guard guard{mutex_}; 34 | return curr_state_ == RaftState::LEADER; 35 | }, 36 | [this]() -> std::shared_ptr { 37 | std::lock_guard guard{mutex_}; 38 | if (curr_state_ == RaftState::LEADER) { 39 | return nullptr; 40 | } 41 | RAFTCPP_CHECK(leader_node_id_ > 0); 42 | RAFTCPP_CHECK(all_rpc_clients_.count(leader_node_id_) == 1); 43 | return all_rpc_clients_[leader_node_id_]; 44 | }, 45 | timer_manager_)) { 46 | std::string log_name = "node-" + config_.GetThisEndpoint().ToString() + ".log"; 47 | replace(log_name.begin(), log_name.end(), '.', '-'); 48 | replace(log_name.begin(), log_name.end(), ':', '-'); 49 | raftcpp::RaftcppLog::StartRaftcppLog(log_name, severity, 10, 3); 50 | } 51 | 52 | void RaftNode::Init() { 53 | ConnectToOtherNodes(); 54 | InitTimers(); 55 | } 56 | 57 | bool RaftNode::IsLeader() const { 58 | std::lock_guard guard{mutex_}; 59 | return curr_state_ == RaftState::LEADER; 60 | } 61 | 62 | RaftNode::~RaftNode() { 63 | leader_log_manager_->Stop(); 64 | non_leader_log_manager_->Stop(); 65 | } 66 | 67 | void RaftNode::PushEntry(LogEntry &entry) { 68 | std::lock_guard guard{mutex_}; 69 | RAFTCPP_CHECK(curr_state_ == RaftState::LEADER); 70 | // This is leader code path. 71 | leader_log_manager_->Push(curr_term_, entry); 72 | // AsyncAppendLogsToFollowers(entry); 73 | // TODO(qwang) 74 | 75 | BroadcastHeartbeat(); 76 | } 77 | 78 | void RaftNode::RequestPreVote() { 79 | std::lock_guard guard{mutex_}; 80 | RAFTCPP_LOG(RLL_DEBUG) << "Node[" << this_node_id_ << "] request pre-vote"; 81 | PreVoteRequest request; 82 | request.set_candidate_id(config_.GetThisId()); 83 | request.set_term(curr_term_ + 1); 84 | // TODO implment last log 85 | request.set_last_log_index(0); 86 | request.set_last_log_term(0); 87 | // Count the results of the vote and have one vote at the beginning 88 | std::shared_ptr granted_votes = std::make_shared(1); 89 | auto context = std::make_shared(); 90 | auto response = std::make_shared(); 91 | for (auto &peer : config_.GetOtherEndpoints()) { 92 | all_rpc_clients_[peer.first]->async()->HandleRequestPreVote( 93 | context.get(), &request, response.get(), 94 | [context, request, response, granted_votes, this](grpc::Status s) { 95 | if (!s.ok()) { 96 | RAFTCPP_LOG(RLL_DEBUG) << s.error_code() << ": " << s.error_message(); 97 | return; 98 | } 99 | std::lock_guard guard{mutex_}; 100 | if (curr_term_ != request.term() || 101 | curr_state_ != RaftState::PRECANDIDATE) 102 | return; 103 | if (response->vote_granted()) { 104 | ++(*granted_votes); 105 | if (config_.GreaterThanHalfNodesNum(*granted_votes)) { 106 | RAFTCPP_LOG(RLL_DEBUG) 107 | << "Node[" << this_node_id_ 108 | << "] receives majority pre-votes in term " << curr_term_; 109 | BecomeCandidate(); 110 | RescheduleElection(); 111 | RequestVote(); 112 | } 113 | } else if (response->term() > curr_term_) { 114 | RAFTCPP_LOG(RLL_DEBUG) 115 | << "Node[" << this_node_id_ << "] finds a new leader Node[" 116 | << response->leader_id() << "] with term " << response->term() 117 | << " and steps down in term " << curr_term_; 118 | BecomeFollower(response->term(), response->leader_id()); 119 | RescheduleElection(); 120 | } 121 | }); 122 | } 123 | } 124 | 125 | grpc::Status RaftNode::HandleRequestPreVote(::grpc::ServerContext *context, 126 | const ::raftcpp::PreVoteRequest *request, 127 | ::raftcpp::PreVoteResponse *response) { 128 | std::lock_guard guard{mutex_}; 129 | RAFTCPP_LOG(RLL_DEBUG) << "Node[" << this_node_id_ 130 | << "] received a RequestPreVote from node[" 131 | << request->candidate_id() << "] at term " << curr_term_; 132 | // The current node is leader or in the leader's lease 133 | if (curr_state_ == RaftState::LEADER || 134 | curr_state_ == RaftState::FOLLOWER && leader_node_id_ != -1) { 135 | response->set_vote_granted(false); 136 | response->set_term(curr_term_); 137 | response->set_leader_id(leader_node_id_); 138 | return grpc::Status::OK; 139 | } 140 | // Reject to pre-vote for pre-candidates whose terms are shorter than or equal to this 141 | // node 142 | if (request->term() <= curr_term_) { 143 | response->set_vote_granted(false); 144 | response->set_term(curr_term_); 145 | response->set_leader_id(leader_node_id_); 146 | return grpc::Status::OK; 147 | } 148 | 149 | // TODO log match 150 | // Reject requests that entry older than this node 151 | // if (!logs.isUpToDate(request->last_log_index(), request->last_log_term())) { 152 | // response->set_vote_granted(false); 153 | // response->set_term(curr_term_); 154 | // response->set_leader_id(leader_node_id_); 155 | // return grpc::Status::OK; 156 | // } 157 | 158 | // Note: The election timer is reset after successful voting, which will help the 159 | // liveness problem of choosing the master under the condition of network instability 160 | RescheduleElection(); 161 | 162 | response->set_vote_granted(true); 163 | response->set_term(curr_term_); 164 | response->set_leader_id(leader_node_id_); 165 | 166 | return grpc::Status::OK; 167 | } 168 | 169 | void RaftNode::RequestVote() { 170 | std::lock_guard guard{mutex_}; 171 | RAFTCPP_LOG(RLL_DEBUG) << "Node[" << this_node_id_ << "] request vote"; 172 | VoteRequest request; 173 | request.set_candidate_id(config_.GetThisId()); 174 | request.set_term(curr_term_); 175 | // TODO implment last log 176 | request.set_last_log_index(0); 177 | request.set_last_log_term(0); 178 | vote_for_ = this_node_id_; 179 | // Count the results of the vote and have one vote at the beginning 180 | std::shared_ptr granted_votes = std::make_shared(1); 181 | auto context = std::make_shared(); 182 | auto response = std::make_shared(); 183 | for (auto &peer : config_.GetOtherEndpoints()) { 184 | all_rpc_clients_[peer.first]->async()->HandleRequestVote( 185 | context.get(), &request, response.get(), 186 | [context, request, response, granted_votes, this](grpc::Status s) { 187 | if (!s.ok()) { 188 | RAFTCPP_LOG(RLL_DEBUG) << s.error_code() << ": " << s.error_message(); 189 | return; 190 | } 191 | std::lock_guard guard{mutex_}; 192 | if (curr_term_ != request.term() || curr_state_ != RaftState::CANDIDATE) 193 | return; 194 | if (response->vote_granted()) { 195 | ++(*granted_votes); 196 | if (config_.GreaterThanHalfNodesNum(*granted_votes)) { 197 | RAFTCPP_LOG(RLL_DEBUG) 198 | << "Node[" << this_node_id_ 199 | << "] receives majority votes in term " << curr_term_; 200 | BecomeLeader(); 201 | } 202 | } else if (response->term() > curr_term_) { 203 | RAFTCPP_LOG(RLL_DEBUG) 204 | << "Node[" << this_node_id_ << "] finds a new leader Node[" 205 | << response->leader_id() << "] with term " << response->term() 206 | << " and steps down in term " << curr_term_; 207 | BecomeFollower(response->term(), response->leader_id()); 208 | RescheduleElection(); 209 | } 210 | }); 211 | } 212 | } 213 | 214 | grpc::Status RaftNode::HandleRequestVote(::grpc::ServerContext *context, 215 | const ::raftcpp::VoteRequest *request, 216 | ::raftcpp::VoteResponse *response) { 217 | std::lock_guard guard{mutex_}; 218 | RAFTCPP_LOG(RLL_DEBUG) << "Node[" << this_node_id_ 219 | << "] received a RequestVote from node[" 220 | << request->candidate_id() << "] at term " << curr_term_; 221 | // Reject to vote for candidates whose terms are shorter than this node 222 | if (request->term() < curr_term_ || 223 | // A situation in which more than one candidate request vote 224 | (request->term() == curr_term_ && vote_for_ != -1 && 225 | vote_for_ != request->candidate_id())) { 226 | response->set_vote_granted(false); 227 | response->set_term(curr_term_); 228 | response->set_leader_id(leader_node_id_); 229 | return grpc::Status::OK; 230 | } 231 | 232 | if (request->term() > curr_term_) { 233 | BecomeFollower(request->term()); 234 | } 235 | 236 | // TODO log match 237 | // Reject requests that entry older than this node 238 | // if (!logs.isUpToDate(request->last_log_index(), request->last_log_term())) { 239 | // response->set_vote_granted(false); 240 | // response->set_term(curr_term_); 241 | // response->set_leader_id(leader_node_id_); 242 | // return grpc::Status::OK; 243 | // } 244 | 245 | vote_for_ = request->candidate_id(); 246 | 247 | // Note: The election timer is reset after successful voting, which will help the 248 | // liveness problem of choosing the master under the condition of network instability 249 | RescheduleElection(); 250 | 251 | response->set_vote_granted(true); 252 | response->set_term(curr_term_); 253 | response->set_leader_id(leader_node_id_); 254 | 255 | return grpc::Status::OK; 256 | } 257 | 258 | grpc::Status RaftNode::HandleRequestAppendEntries( 259 | ::grpc::ServerContext *context, const ::raftcpp::AppendEntriesRequest *request, 260 | ::raftcpp::AppendEntriesResponse *response) { 261 | // RAFTCPP_LOG(RLL_INFO) << "HandleRequestPushLogs: log_entry.term_id=" 262 | // << request->log().termid() 263 | // << ", committed_log_index=" << request->commited_log_index() 264 | // << ", log_entry.log_index=" << request->log().log_index() 265 | // << ", log_entry.data=" << request->log().data(); 266 | 267 | std::lock_guard guard{mutex_}; 268 | 269 | // Reject log replication requests with a leader whose term is less than this node 270 | if (request->term() < curr_term_) { 271 | response->set_success(false); 272 | response->set_term(curr_term_); 273 | response->set_leader_id(leader_node_id_); 274 | return grpc::Status::OK; 275 | } 276 | 277 | // If the other node's term is greater than this node, 278 | // or this node are a candidate who lost the election in the same term, 279 | // then become a follower. 280 | if (request->term() > curr_term_ || 281 | (request->term() == curr_term_ && curr_state_ == RaftState::CANDIDATE)) { 282 | BecomeFollower(request->term(), request->leader_id()); 283 | } 284 | 285 | RescheduleElection(); 286 | 287 | // TODO Reject erroneous log append requests 288 | 289 | // TODO Fast fallback to speed up the resolution of log conflicts between nodes 290 | 291 | response->set_success(true); 292 | response->set_term(curr_term_); 293 | response->set_leader_id(leader_node_id_); 294 | return grpc::Status::OK; 295 | } 296 | 297 | void RaftNode::ConnectToOtherNodes() { 298 | // Initial the rpc clients connecting to other nodes. 299 | for (auto &[id, endpoint] : config_.GetOtherEndpoints()) { 300 | grpc::ChannelArguments args; 301 | auto channel = 302 | grpc::CreateChannel(endpoint.ToString(), grpc::InsecureChannelCredentials()); 303 | all_rpc_clients_[id] = std::make_shared(channel); 304 | RAFTCPP_LOG(RLL_INFO) << "This node " << config_.GetThisEndpoint().ToString() 305 | << " succeeded to connect to the node " 306 | << endpoint.ToString(); 307 | } 308 | } 309 | 310 | void RaftNode::InitTimers() { 311 | timer_manager_->RegisterTimer(RaftcppConstants::TIMER_ELECTION, [this] { 312 | std::lock_guard guard{mutex_}; 313 | if (curr_state_ == RaftState::FOLLOWER) { 314 | BecomePreCandidate(); 315 | RequestPreVote(); 316 | } else if (curr_state_ == RaftState::PRECANDIDATE) { 317 | BecomeFollower(curr_term_); 318 | } else if (curr_state_ == RaftState::CANDIDATE) { 319 | RequestVote(); 320 | } else if (curr_state_ == RaftState::LEADER) { 321 | if (QuorumActive()) { 322 | RAFTCPP_LOG(RLL_DEBUG) 323 | << "Node[" << this_node_id_ 324 | << "] stepped down to follower since quorum is not active"; 325 | } 326 | } 327 | RescheduleElection(); 328 | }); 329 | 330 | timer_manager_->RegisterTimer(RaftcppConstants::TIMER_HEARTBEAT, 331 | std::bind(&RaftNode::BroadcastHeartbeat, this)); 332 | 333 | timer_manager_->StartTimer(RaftcppConstants::TIMER_ELECTION, 334 | GetRandomizedElectionTimeout()); 335 | timer_manager_->Run(); 336 | } 337 | 338 | void RaftNode::BecomeFollower(int64_t term, int64_t leader_id) { 339 | // In order to better control the time of resetting the election timer, 340 | // do not RescheduleElection() here first 341 | // RescheduleElection(); 342 | timer_manager_->StopTimer(RaftcppConstants::TIMER_HEARTBEAT); 343 | curr_state_ = RaftState::FOLLOWER; 344 | curr_term_ = term; 345 | leader_node_id_ = leader_id; 346 | vote_for_ = -1; 347 | leader_log_manager_->Stop(); 348 | non_leader_log_manager_->Run(leader_log_manager_->Logs(), 349 | leader_log_manager_->CommittedLogIndex()); 350 | RAFTCPP_LOG(RLL_INFO) << "Node[" << this_node_id_ << "] became follower at term " 351 | << term; 352 | } 353 | 354 | void RaftNode::BecomePreCandidate() { 355 | // Becoming a pre-candidate does not increase curr_term_ or change vote_for_. 356 | curr_state_ = RaftState::PRECANDIDATE; 357 | leader_node_id_ = -1; 358 | RAFTCPP_LOG(RLL_INFO) << "Node[" << this_node_id_ << "] became PreCandidate at term " 359 | << curr_term_; 360 | } 361 | 362 | void RaftNode::BecomeCandidate() { 363 | curr_state_ = RaftState::CANDIDATE; 364 | ++curr_term_; 365 | vote_for_ = this_node_id_; 366 | RAFTCPP_LOG(RLL_INFO) << "Node[" << this_node_id_ << "] became Candidate at term " 367 | << curr_term_; 368 | } 369 | 370 | void RaftNode::BecomeLeader() { 371 | curr_state_ = RaftState::LEADER; 372 | leader_node_id_ = this_node_id_; 373 | 374 | // After becoming a leader, the leader does not know the logs of other nodes, 375 | // so he needs to synchronize the logs with other nodes. The leader does not know 376 | // the status of other nodes in the cluster, so he chooses to keep trying. 377 | // Nextindex and matchindex are used to save the next log index to be synchronized 378 | // and the matched log index of other nodes respectively. The initialization value 379 | // of nextindex is lastindex+1, that is, the leader's last log sequence number +1. 380 | // Therefore, in fact, this log sequence number does not exist. Obviously, the leader 381 | // does not expect to synchronize successfully at one time, but takes out a value 382 | // to test. The initialization value of matchindex is 0, which is easy to understand. 383 | // Because it has not been synchronized with any node successfully, it is directly 0. 384 | for ([[maybe_unused]] auto &[id, _] : config_.GetOtherEndpoints()) { 385 | // TODO 386 | // nextIndex[id] = lastIndex() + 1 387 | // matchIndex[id] = 0; 388 | } 389 | 390 | RAFTCPP_LOG(RLL_INFO) << "Node[" << this_node_id_ << "] became Leader at term " 391 | << curr_term_; 392 | 393 | leader_log_manager_->Run(non_leader_log_manager_->Logs(), 394 | non_leader_log_manager_->CommittedLogIndex()); 395 | // The leader cannot submit the entry of non current term, so submit an empty entry 396 | // to indirectly submit the entry of previous term 397 | LogEntry empty; 398 | leader_log_manager_->Push(curr_term_, empty); 399 | non_leader_log_manager_->Stop(); 400 | 401 | BroadcastHeartbeat(); 402 | 403 | timer_manager_->StartTimer(RaftcppConstants::TIMER_HEARTBEAT, 404 | RaftcppConstants::DEFAULT_HEARTBEAT_INTERVAL_MS); 405 | } 406 | 407 | uint64_t RaftNode::GetRandomizedElectionTimeout() { 408 | return randomer_.TakeOne(RaftcppConstants::DEFAULT_ELECTION_TIMER_TIMEOUT_BASE_MS, 409 | RaftcppConstants::DEFAULT_ELECTION_TIMER_TIMEOUT_TOP_MS); 410 | } 411 | 412 | void RaftNode::RescheduleElection() { 413 | timer_manager_->ResetTimer(RaftcppConstants::TIMER_ELECTION, 414 | GetRandomizedElectionTimeout()); 415 | } 416 | 417 | void RaftNode::ReplicateOneRound(int64_t node_id) { 418 | AppendEntriesRequest request; 419 | auto context = std::make_shared(); 420 | auto response = std::make_shared(); 421 | all_rpc_clients_[node_id]->async()->HandleRequestAppendEntries( 422 | context.get(), &request, response.get(), 423 | [context, response, node_id, this](grpc::Status s) { 424 | if (!s.ok()) { 425 | std::lock_guard guard{mutex_}; 426 | actives_[node_id] = false; 427 | return; 428 | } 429 | std::lock_guard guard{mutex_}; 430 | // TODO asynchronous replication 431 | 432 | actives_[node_id] = true; 433 | }); 434 | } 435 | 436 | // With the heartbeat, the follower's log will be replicated to the same location as the 437 | // leader 438 | void RaftNode::BroadcastHeartbeat() { 439 | std::lock_guard guard{mutex_}; 440 | if (curr_state_ != RaftState::LEADER) { 441 | return; 442 | } 443 | for ([[maybe_unused]] auto &[id, _] : config_.GetOtherEndpoints()) { 444 | // This is asynchronous replication 445 | ReplicateOneRound(id); 446 | } 447 | } 448 | 449 | bool RaftNode::QuorumActive() { 450 | size_t active = 1; 451 | for (auto &item : actives_) { 452 | if (item.second) { 453 | active++; 454 | item.second = false; 455 | } 456 | } 457 | return config_.GreaterThanHalfNodesNum(active); 458 | } 459 | 460 | } // namespace raftcpp::node 461 | -------------------------------------------------------------------------------- /src/node/node.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "proto/raft.grpc.pb.h" 11 | #include "proto/raft.pb.h" 12 | #include "src/common/config.h" 13 | #include "src/common/endpoint.h" 14 | #include "src/common/logging.h" 15 | #include "src/common/timer.h" 16 | #include "src/common/timer_manager.h" 17 | #include "src/common/type_def.h" 18 | #include "src/log_manager/blocking_queue_interface.h" 19 | #include "src/log_manager/blocking_queue_mutex_impl.h" 20 | #include "src/log_manager/leader_log_manager.h" 21 | #include "src/log_manager/non_leader_log_manager.h" 22 | #include "src/statemachine/state_machine.h" 23 | 24 | namespace raftcpp { 25 | namespace node { 26 | 27 | class RaftNode : public raftrpc::Service, public std::enable_shared_from_this { 28 | public: 29 | RaftNode( 30 | std::shared_ptr state_machine, 31 | // std::unique_ptr rpc_server, 32 | const common::Config &config, 33 | const raftcpp::RaftcppLogLevel severity = raftcpp::RaftcppLogLevel::RLL_DEBUG); 34 | 35 | // explicit RaftNode() = default; 36 | 37 | ~RaftNode(); 38 | 39 | void Init(); 40 | 41 | bool IsLeader() const; 42 | 43 | void PushEntry(LogEntry &entry); 44 | 45 | void RequestPreVote(); 46 | 47 | grpc::Status HandleRequestPreVote(::grpc::ServerContext *context, 48 | const ::raftcpp::PreVoteRequest *request, 49 | ::raftcpp::PreVoteResponse *response); 50 | 51 | void RequestVote(); 52 | 53 | grpc::Status HandleRequestVote(::grpc::ServerContext *context, 54 | const ::raftcpp::VoteRequest *request, 55 | ::raftcpp::VoteResponse *response); 56 | 57 | grpc::Status HandleRequestAppendEntries( 58 | ::grpc::ServerContext *context, const ::raftcpp::AppendEntriesRequest *request, 59 | ::raftcpp::AppendEntriesResponse *response); 60 | 61 | RaftState GetCurrState() { 62 | std::lock_guard guard{mutex_}; 63 | return curr_state_; 64 | } 65 | 66 | int64_t CurrLogIndex() const { 67 | std::lock_guard guard{mutex_}; 68 | if (curr_state_ == RaftState::LEADER) { 69 | return leader_log_manager_->CurrLogIndex(); 70 | } else { 71 | return non_leader_log_manager_->CurrLogIndex(); 72 | } 73 | } 74 | 75 | private: 76 | void ConnectToOtherNodes(); 77 | 78 | // void InitRpcHandlers(); 79 | 80 | void StepBack(int64_t term_id); 81 | 82 | void InitTimers(); 83 | 84 | void BecomeFollower(int64_t term, int64_t leader_id = -1); 85 | 86 | void BecomePreCandidate(); 87 | 88 | void BecomeCandidate(); 89 | 90 | void BecomeLeader(); 91 | 92 | uint64_t GetRandomizedElectionTimeout(); 93 | 94 | // Reset the election timer 95 | void RescheduleElection(); 96 | 97 | // Asynchronous replication to a raft node 98 | void ReplicateOneRound(int64_t node_id); 99 | 100 | // With the heartbeat, the follower's log will be replicated to the same location as 101 | // the leader 102 | void BroadcastHeartbeat(); 103 | 104 | // Check whether most slave nodes are active 105 | bool QuorumActive(); 106 | 107 | private: 108 | // Current state of this node. This initial value of this should be a FOLLOWER. 109 | RaftState curr_state_ = RaftState::FOLLOWER; 110 | 111 | // The ID of this node. 112 | int64_t this_node_id_; 113 | 114 | // The ID of this current term leader node, -1 means no leader has been elected. 115 | int64_t leader_node_id_ = -1; 116 | 117 | // Current term id in this node local view. 118 | int64_t curr_term_; 119 | 120 | // CandidateId voted for in the current term, or -1 if not voted for any candidate 121 | int64_t vote_for_; 122 | 123 | // The rpc clients to all other nodes. 124 | std::unordered_map> all_rpc_clients_; 125 | 126 | common::Config config_; 127 | 128 | // The recursive mutex that protects all of the node state. 129 | mutable std::recursive_mutex mutex_; 130 | 131 | // Accept the heartbeat reset election time to be random, 132 | // otherwise the all followers will be timed out at one time. 133 | Randomer randomer_; 134 | 135 | std::shared_ptr state_machine_; 136 | 137 | std::shared_ptr timer_manager_; 138 | 139 | // LogManager for this node. 140 | std::unique_ptr leader_log_manager_; 141 | 142 | std::unique_ptr non_leader_log_manager_; 143 | 144 | // Record the active status of nodes 145 | std::map actives_; 146 | }; 147 | 148 | } // namespace node 149 | } // namespace raftcpp 150 | -------------------------------------------------------------------------------- /src/statemachine/state_machine.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace raftcpp { 6 | 7 | class StateMachine { 8 | public: 9 | virtual ~StateMachine() = default; 10 | 11 | virtual bool ShouldDoSnapshot() { return true; } 12 | 13 | virtual void SaveSnapshot() = 0; 14 | 15 | virtual void LoadSnapshot() = 0; 16 | 17 | // virtual RaftcppResponse OnApply(RaftcppRequest &request) = 0; 18 | 19 | virtual bool OnApply(const std::string &serialized) = 0; 20 | 21 | private: 22 | }; 23 | 24 | } // namespace raftcpp 25 | -------------------------------------------------------------------------------- /tests/unit_tests/blocking_queue_test.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "gtest/gtest.h" 5 | #include "src/log_manager/blocking_queue_mutex_impl.h" 6 | 7 | using namespace raftcpp; 8 | 9 | struct LogEntryTest { 10 | LogEntryTest() : index_(1), term_(1) {} 11 | LogEntryTest(int64_t index, int64_t term) : index_(index), term_(term) {} 12 | LogEntryTest(const LogEntryTest &entry) { 13 | index_ = entry.index_; 14 | term_ = entry.term_; 15 | } 16 | 17 | int64_t index_; 18 | int64_t term_; 19 | }; 20 | 21 | bool g_flag = false; 22 | void thread_func(BlockingQueueMutexImpl &log_manager) { 23 | std::this_thread::sleep_for(std::chrono::milliseconds(500)); 24 | g_flag = true; 25 | LogEntryTest log_entry{123, 456}; 26 | log_manager.Push(log_entry); 27 | } 28 | 29 | TEST(LogManagerTest, TestLogManager) { 30 | using namespace raftcpp; 31 | 32 | BlockingQueueMutexImpl log_manager; 33 | LogEntryTest log_entry{123, 456}; 34 | log_manager.Push(log_entry); 35 | LogEntryTest res = log_manager.Pop(); 36 | ASSERT_EQ(log_entry.index_, res.index_); 37 | ASSERT_EQ(log_entry.term_, res.term_); 38 | } 39 | 40 | TEST(LogManagerTest, TestNonBlocking) { 41 | using namespace raftcpp; 42 | 43 | BlockingQueueMutexImpl log_manager; 44 | LogEntryTest log_entry; 45 | bool res = log_manager.Pop(log_entry); 46 | ASSERT_EQ(res, false); 47 | } 48 | 49 | TEST(LogManagerTest, TestBlocking) { 50 | using namespace raftcpp; 51 | 52 | BlockingQueueMutexImpl log_manager; 53 | ASSERT_EQ(g_flag, false); 54 | std::thread th(thread_func, std::ref(log_manager)); 55 | LogEntryTest res; 56 | res = log_manager.Pop(); 57 | ASSERT_EQ(g_flag, true); 58 | ASSERT_EQ(res.index_, 123); 59 | ASSERT_EQ(res.term_, 456); 60 | th.join(); 61 | } 62 | 63 | TEST(BlockingQueueTest, TestMostFront) { 64 | using namespace raftcpp; 65 | 66 | BlockingQueueMutexImpl log_manager; 67 | LogEntryTest log_entry0{1, 5}; 68 | LogEntryTest log_entry1{2, 6}; 69 | LogEntryTest log_entry2{3, 7}; 70 | LogEntryTest log_entry3{4, 8}; 71 | log_manager.Push(log_entry0); 72 | log_manager.Push(log_entry1); 73 | log_manager.Push(log_entry2); 74 | log_manager.Push(log_entry3); 75 | std::vector v = log_manager.MostFront(3); 76 | ASSERT_EQ(v.size(), 3); 77 | ASSERT_EQ(v[0].index_, log_entry0.index_); 78 | ASSERT_EQ(v[1].index_, log_entry1.index_); 79 | ASSERT_EQ(v[2].index_, log_entry2.index_); 80 | } 81 | 82 | int main(int argc, char **argv) { 83 | ::testing::InitGoogleTest(&argc, argv); 84 | return RUN_ALL_TESTS(); 85 | } 86 | -------------------------------------------------------------------------------- /tests/unit_tests/config_test.cc: -------------------------------------------------------------------------------- 1 | #include "src/common/config.h" 2 | 3 | #include 4 | #include 5 | 6 | #include "gtest/gtest.h" 7 | 8 | TEST(ConfigTest, TestBasicConfig) { 9 | using namespace raftcpp::common; 10 | 11 | Config config = Config::From(""); 12 | ASSERT_EQ(config.GetOtherEndpoints().size(), 0); 13 | 14 | config = Config::From("123"); 15 | ASSERT_EQ(config.GetOtherEndpoints().size(), 0); 16 | 17 | config = Config::From("127.0.0.1:10001"); 18 | ASSERT_EQ(config.GetOtherEndpoints().size(), 0); 19 | ASSERT_EQ(config.GetThisEndpoint().GetHost(), "127.0.0.1"); 20 | ASSERT_EQ(config.GetThisEndpoint().GetPort(), 10001); 21 | 22 | config = Config::From("127.0.0.1:10001,127.0.0.1:10002"); 23 | ASSERT_EQ(config.GetOtherEndpoints().size(), 1); 24 | 25 | config = Config::From("127.0.0.1:10001,127.0.0.1:10002,"); 26 | ASSERT_EQ(config.GetOtherEndpoints().size(), 1); 27 | 28 | config = Config::From("127.0.0.1:10001,127.0.0.1:10002,123"); 29 | ASSERT_EQ(config.GetOtherEndpoints().size(), 1); 30 | 31 | config = Config::From( 32 | "127.0.0.1:10001,127.0.0.1:10002,255.255.255.255:10003,0.0.0.0:10004"); 33 | ASSERT_EQ(config.GetOtherEndpoints().size(), 3); 34 | 35 | std::string input = "0.0.0.0:0,0.0.0.0:1,255.255.255.255:65534,255.255.255.255:65535"; 36 | config = Config::From(input); 37 | std::string output = config.ToString(); 38 | // ASSERT_EQ(input, output); 39 | ASSERT_EQ(config, config); 40 | 41 | input = "0.0.0.0:0,255.255.255.255:65534,255.255.255.255:65535,0.0.0.0:1"; 42 | Config config2 = Config::From(input); 43 | ASSERT_EQ(config, config2); 44 | output = config2.ToString(); 45 | // ASSERT_EQ(input, output); 46 | 47 | input = "0.0.0.0:0,0.0.0.0:1,255.255.255.255:65534,255.255.255.255:65533"; 48 | config2 = Config::From(input); 49 | ASSERT_NE(config, config2); 50 | 51 | input = "0.0.0.0:65535"; 52 | config2 = Config::From(input); 53 | ASSERT_NE(config, config2); 54 | 55 | // when input has duplicate endpoint 56 | input = "0.0.0.0:0,0.0.0.0:1,0.0.0.0:0,0.0.0.0:1"; 57 | config2 = Config::From(input); 58 | ASSERT_EQ(config2.GetOtherEndpoints().size(), 1); 59 | } 60 | 61 | TEST(ConfigTest, TestEndpoint) { 62 | using namespace raftcpp; 63 | 64 | Endpoint e0("127.0.0.1:10001"); 65 | Endpoint e1("127.0.0.1:10001"); 66 | Endpoint e2("0.0.0.0:0"); 67 | Endpoint e3("255.255.255.255:65535"); 68 | Endpoint e4("255.255.255.255:65534"); 69 | 70 | std::vector v{e0, e1, e2, e3, e4}; 71 | ASSERT_EQ(e0, e1); 72 | ASSERT_NE(e3, e4); 73 | 74 | std::sort(v.begin(), v.end(), Endpoint()); 75 | 76 | ASSERT_EQ(v[0], e2); 77 | ASSERT_EQ(v[1], e0); 78 | ASSERT_EQ(v[2], e1); 79 | ASSERT_EQ(v[3], e4); 80 | ASSERT_EQ(v[4], e3); 81 | 82 | std::unordered_map m = {{{"127.0.0.1", 1001}, "first"}, 83 | {{"127.0.0.1", 1002}, "second"}}; 84 | std::string key1 = "127.0.0.1:1001"; 85 | std::string key2 = "127.0.0.1:1002"; 86 | for (const auto &element : m) { 87 | std::stringstream buffer; 88 | buffer << element.first; 89 | if (element.second == "first") { 90 | ASSERT_EQ(buffer.str(), key1); 91 | } else if (element.second == "first") { 92 | ASSERT_EQ(buffer.str(), key2); 93 | } 94 | } 95 | } 96 | 97 | int main(int argc, char **argv) { 98 | ::testing::InitGoogleTest(&argc, argv); 99 | return RUN_ALL_TESTS(); 100 | } 101 | -------------------------------------------------------------------------------- /tests/unit_tests/file_test.cc: -------------------------------------------------------------------------------- 1 | #include "common/file.h" 2 | 3 | #include 4 | 5 | TEST(FileTest, TestWriteToFile) { 6 | using namespace raftcpp; 7 | 8 | File test_file = File::Open("./tmp.dat"); 9 | std::string str_test1 = "hello, world"; 10 | std::string str_test2 = "hello"; 11 | test_file.CleanAndWrite(str_test1); 12 | std::string str_res1 = test_file.ReadAll(); 13 | ASSERT_EQ(str_res1, str_test1); 14 | test_file.CleanAndWrite(str_test2); 15 | std::string str_res2 = test_file.ReadAll(); 16 | ASSERT_EQ(str_res2, str_test2); 17 | } 18 | 19 | int main(int argc, char **argv) { 20 | ::testing::InitGoogleTest(&argc, argv); 21 | return RUN_ALL_TESTS(); 22 | } 23 | -------------------------------------------------------------------------------- /tests/unit_tests/log_manager_test.cc: -------------------------------------------------------------------------------- 1 | //#include 2 | //#include 3 | // 4 | //#include "grpcpp/grpcpp.h" 5 | //#include "gtest/gtest.h" 6 | //#include "node/node.h" 7 | //#include "src/common/config.h" 8 | //#include "src/common/logging.h" 9 | //#include "src/statemachine/state_machine_impl.h" 10 | //#include "util.h" 11 | // 12 | // TEST(LogManagerTest, TestLeaderPushLog) { 13 | // int leaderFlag = 0; // mark the leader node 14 | // int nodeNum = 3; 15 | // int basePort = 10001; 16 | // std::string address("127.0.0.1"); 17 | // 18 | // std::vector nodeStateLeader; 19 | // std::vector nodeStateFollower; 20 | // 21 | // std::vector> nodes(nodeNum); 22 | // // std::vector> servers(nodeNum); 23 | // std::vector> servers(nodeNum); 24 | // std::vector threads(nodeNum); 25 | // 26 | // for (int i = 0; i < nodeNum; i++) { 27 | // std::string config_str = init_config(address, basePort, nodeNum, i); 28 | // const auto config = raftcpp::common::Config::From(config_str); 29 | // std::cout << "Raft node run on: " << config.GetThisEndpoint() << std::endl; 30 | // auto node = std::make_shared( 31 | // std::make_shared(), config, 32 | // raftcpp::RaftcppLogLevel::RLL_DEBUG); 33 | // auto server = BuildServer(config.GetThisEndpoint(), node); 34 | // node->Init(); 35 | // nodes[i] = node; 36 | // 37 | // threads[i] = std::thread([server = std::move(server)] { 38 | // // Wait for the server to shutdown. Note that some other thread must be 39 | // // responsible for shutting down the server for this call to ever return. 40 | // server->Wait(); 41 | // std::cout << "stop\n"; 42 | // }); 43 | // } 44 | // 45 | // for (auto &thread : threads) { 46 | // thread.detach(); 47 | // } 48 | // 49 | // // wait for their initialization 50 | // std::this_thread::sleep_for(std::chrono::seconds(4)); 51 | // 52 | // for (int i = 0; i < nodeNum; ++i) { 53 | // if (nodes[i]->GetCurrState() == raftcpp::RaftState::LEADER) { 54 | // leaderFlag = i; 55 | // } 56 | // 57 | // ASSERT_EQ(nodes[i]->CurrLogIndex(), 0); 58 | // } 59 | // ASSERT_NE(leaderFlag, -1); 60 | // 61 | // int delta = 1; 62 | // std::shared_ptr request = 63 | // std::make_shared(delta); 64 | // nodes[leaderFlag]->PushRequest(request); 65 | // 66 | // delta++; 67 | // request = std::make_shared(delta); 68 | // nodes[leaderFlag]->PushRequest(request); 69 | // 70 | // std::this_thread::sleep_for(std::chrono::seconds(4)); 71 | // for (int i = 0; i < nodeNum; ++i) { 72 | // ASSERT_EQ(nodes[i]->CurrLogIndex(), delta); 73 | // } 74 | // 75 | // // shutdown leader 76 | // servers[leaderFlag] = nullptr; 77 | // nodes[leaderFlag].reset(); 78 | // std::this_thread::sleep_for(std::chrono::seconds(4)); 79 | // 80 | // delta++; 81 | // for (int i = 0; i < nodeNum; ++i) { 82 | // if (servers[i] == nullptr) { 83 | // continue; 84 | // } 85 | // 86 | // if (nodes[i]->GetCurrState() == raftcpp::RaftState::LEADER) { 87 | // leaderFlag = i; 88 | // } 89 | // 90 | // ASSERT_EQ(nodes[i]->CurrLogIndex(), delta); 91 | // } 92 | // ASSERT_NE(leaderFlag, -1); 93 | // 94 | // // end 95 | // std::this_thread::sleep_for(std::chrono::seconds(1)); 96 | // servers.clear(); 97 | // for (int i = 0; i < nodeNum; i++) { 98 | // if (threads[i].joinable()) { 99 | // threads[i].join(); 100 | // } 101 | // } 102 | //} 103 | // 104 | // int main(int argc, char **argv) { 105 | // ::testing::InitGoogleTest(&argc, argv); 106 | // return RUN_ALL_TESTS(); 107 | //} -------------------------------------------------------------------------------- /tests/unit_tests/logging_check_test.cc: -------------------------------------------------------------------------------- 1 | #include "gtest/gtest.h" 2 | #include "src/common/logging.h" 3 | 4 | TEST(LogCheckTest, TestRaftcppCheck) { 5 | RAFTCPP_CHECK(true); 6 | // TODO(luhuanbing): Better solution? 7 | // RAFTCPP_CHECK(false); 8 | } 9 | 10 | int main(int argc, char **argv) { 11 | ::testing::InitGoogleTest(&argc, argv); 12 | return RUN_ALL_TESTS(); 13 | } 14 | -------------------------------------------------------------------------------- /tests/unit_tests/logging_test.cc: -------------------------------------------------------------------------------- 1 | #include "common/logging.h" 2 | 3 | #include 4 | 5 | #include "common/id.h" 6 | #include "gtest/gtest.h" 7 | 8 | TEST(LoggingTest, TestLogLevel) { 9 | raftcpp::RaftcppLog::StartRaftcppLog("log/test.log", 10 | raftcpp::RaftcppLogLevel::RLL_INFO, 10, 3); 11 | RAFTCPP_LOG(RLL_DEBUG) << "this debug message won't show up " << 456; 12 | RAFTCPP_LOG(RLL_WARNING) << "Hello " << 123; 13 | RAFTCPP_LOG(RLL_INFO) << "world " << 456 << " 789"; 14 | RAFTCPP_CHECK(true) << "This is a RAFTCPP_CHECK" 15 | << " message but it won't show up"; 16 | raftcpp::RaftcppLog::ShutDownRaftcppLog(); 17 | std::fstream file("log/test.log"); 18 | 19 | std::string line; 20 | uint8_t debug_count = 0; 21 | uint8_t info_count = 0; 22 | uint8_t warn_count = 0; 23 | while (getline(file, line)) { 24 | std::cout << "line:" << line << std::endl; 25 | if (line.find("debug") != std::string::npos) { 26 | debug_count++; 27 | } else if (line.find("info") != std::string::npos) { 28 | info_count++; 29 | } else if (line.find("warning") != std::string::npos) { 30 | warn_count++; 31 | } 32 | } 33 | 34 | ASSERT_EQ(debug_count, 0); 35 | ASSERT_EQ(info_count, 1); 36 | ASSERT_EQ(warn_count, 1); 37 | file.close(); 38 | std::remove("log/test.log"); 39 | } 40 | 41 | // Here tests trivial things for the logging 42 | TEST(LoggingTest, TestLogTrivial) { 43 | raftcpp::NodeID node_id; 44 | RAFTCPP_LOG(RLL_DEBUG) << node_id; 45 | } 46 | 47 | int main(int argc, char **argv) { 48 | ::testing::InitGoogleTest(&argc, argv); 49 | return RUN_ALL_TESTS(); 50 | } 51 | -------------------------------------------------------------------------------- /tests/unit_tests/nodeid_test.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "common/id.h" 4 | #include "gtest/gtest.h" 5 | 6 | TEST(IdTest, TestNodeid) { 7 | raftcpp::Endpoint ep("127.0.0.1:5000"); 8 | raftcpp::NodeID id(ep); 9 | 10 | raftcpp::Endpoint ep2("127.0.0.1:5000"); 11 | raftcpp::NodeID id2(ep2); 12 | ASSERT_EQ(id, id2); 13 | 14 | raftcpp::Endpoint ep3("192.168.0.1:8080"); 15 | raftcpp::NodeID id3(ep3); 16 | ASSERT_NE(id2, id3); 17 | 18 | raftcpp::Endpoint ep4("192.168.0.1:8081"); 19 | raftcpp::NodeID id4(ep4); 20 | 21 | std::unordered_map um; 22 | um.insert(std::pair{id, true}); 23 | um.insert(std::pair{id2, true}); 24 | um.insert(std::pair{id3, true}); 25 | 26 | ASSERT_EQ(2, um.size()); 27 | ASSERT_NE(um.cend(), um.find(id)); 28 | ASSERT_NE(um.cend(), um.find(id2)); 29 | ASSERT_NE(um.cend(), um.find(id3)); 30 | ASSERT_EQ(um.cend(), um.find(id4)); 31 | } 32 | 33 | TEST(IdTest, TestTermid) { 34 | raftcpp::TermID id(1); 35 | raftcpp::TermID id2(1); 36 | ASSERT_EQ(id, id2); 37 | 38 | raftcpp::TermID id3(3); 39 | ASSERT_NE(id2, id3); 40 | ASSERT_EQ(3, id3.getTerm()); 41 | 42 | id2.setTerm(3); 43 | ASSERT_EQ(id2, id3); 44 | 45 | raftcpp::TermID id4(4); 46 | 47 | std::unordered_map um; 48 | um.insert(std::pair{id, true}); 49 | um.insert(std::pair{id2, true}); 50 | um.insert(std::pair{id3, true}); 51 | 52 | ASSERT_EQ(2, um.size()); 53 | ASSERT_NE(um.cend(), um.find(id)); 54 | ASSERT_NE(um.cend(), um.find(id2)); 55 | ASSERT_NE(um.cend(), um.find(id3)); 56 | ASSERT_EQ(um.cend(), um.find(id4)); 57 | } 58 | 59 | int main(int argc, char **argv) { 60 | ::testing::InitGoogleTest(&argc, argv); 61 | return RUN_ALL_TESTS(); 62 | } 63 | -------------------------------------------------------------------------------- /tests/unit_tests/paper_test.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | 6 | #include "gtest/gtest.h" 7 | #include "src/common/endpoint.h" 8 | #include "src/common/type_def.h" 9 | #include "src/node/node.h" 10 | #include "src/statemachine/state_machine_impl.h" 11 | #include "util.h" 12 | 13 | TEST(raftPaperTests, selectTest) { 14 | int leaderFlag = 0; // mark the leader node 15 | int nodeNum = 3; 16 | int basePort = 10001; 17 | std::string address("127.0.0.1"); 18 | 19 | std::vector nodeStateLeader; 20 | std::vector nodeStateFollower; 21 | 22 | std::vector> nodes(nodeNum); 23 | // std::vector> servers(nodeNum); 24 | std::vector> servers(nodeNum); 25 | std::vector threads(nodeNum); 26 | 27 | for (int i = 0; i < nodeNum; i++) { 28 | std::string config_str = init_config(address, basePort, nodeNum, i); 29 | const auto config = raftcpp::common::Config::From(config_str); 30 | std::cout << "Raft node run on: " << config.GetThisEndpoint() << std::endl; 31 | auto node = std::make_shared( 32 | std::make_shared(), config, 33 | raftcpp::RaftcppLogLevel::RLL_DEBUG); 34 | auto server = BuildServer(config.GetThisEndpoint(), node); 35 | node->Init(); 36 | nodes[i] = node; 37 | 38 | threads[i] = std::thread([server = std::move(server)] { 39 | // Wait for the server to shutdown. Note that some other thread must be 40 | // responsible for shutting down the server for this call to ever return. 41 | server->Wait(); 42 | std::cout << "stop\n"; 43 | }); 44 | } 45 | 46 | for (auto &thread : threads) { 47 | thread.detach(); 48 | } 49 | std::this_thread::sleep_for(std::chrono::seconds(5)); 50 | 51 | nodeStateFollower.clear(); 52 | nodeStateLeader.clear(); 53 | for (int i = 0; i < nodeNum; ++i) { 54 | if (nodes[i]->GetCurrState() == raftcpp::RaftState::FOLLOWER) { 55 | nodeStateFollower.push_back(raftcpp::RaftState::FOLLOWER); 56 | } else if (nodes[i]->GetCurrState() == raftcpp::RaftState::LEADER) { 57 | leaderFlag = i; 58 | nodeStateLeader.push_back(raftcpp::RaftState::LEADER); 59 | } 60 | } 61 | ASSERT_EQ(nodeStateLeader.size(), 1); 62 | ASSERT_EQ(nodeStateFollower.size(), 2); 63 | } 64 | 65 | int main(int argc, char **argv) { 66 | ::testing::InitGoogleTest(&argc, argv); 67 | return RUN_ALL_TESTS(); 68 | } -------------------------------------------------------------------------------- /tests/unit_tests/timer_test.cc: -------------------------------------------------------------------------------- 1 | #include "common/timer.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "common/util.h" 8 | #include "gtest/gtest.h" 9 | 10 | TEST(RepeatedTimerTest, TestReset) { 11 | using namespace raftcpp::common; 12 | 13 | asio::io_service io_service; 14 | asio::io_service::work work{io_service}; 15 | 16 | uint64_t callback_invoked_time_ms = -1; 17 | RepeatedTimer repeated_timer{io_service, 18 | [&callback_invoked_time_ms](const asio::error_code &e) { 19 | callback_invoked_time_ms = CurrentTimeMs(); 20 | }}; 21 | std::thread th{[&io_service]() { io_service.run(); }}; 22 | 23 | repeated_timer.Start(3 * 1000); 24 | std::this_thread::sleep_for(std::chrono::milliseconds{500}); 25 | 26 | repeated_timer.Reset(6 * 1000); 27 | auto resetting_time_ms = CurrentTimeMs(); 28 | ASSERT_EQ(true, callback_invoked_time_ms > 0); 29 | ASSERT_EQ(true, (callback_invoked_time_ms - resetting_time_ms > 0)); 30 | 31 | repeated_timer.Stop(); 32 | io_service.stop(); 33 | th.join(); 34 | } 35 | 36 | TEST(ContinuousTimerTest, TestBasic) { 37 | using namespace raftcpp::common; 38 | 39 | uint64_t counter = 0; 40 | 41 | asio::io_service io_service; 42 | ContinuousTimer continuous_timer( 43 | io_service, 1000, [&counter](const asio::error_code &e) { ++counter; }); 44 | continuous_timer.Start(); 45 | std::thread th([&io_service]() { io_service.run(); }); 46 | 47 | std::this_thread::sleep_for(std::chrono::milliseconds(5 * 1000 + 600)); 48 | 49 | continuous_timer.Cancel(); 50 | // Note that stop a io_service is thread safe. 51 | io_service.stop(); 52 | th.join(); 53 | 54 | ASSERT_EQ(5, counter); 55 | } 56 | 57 | int main(int argc, char **argv) { 58 | ::testing::InitGoogleTest(&argc, argv); 59 | return RUN_ALL_TESTS(); 60 | } 61 | -------------------------------------------------------------------------------- /tests/unit_tests/util.cc: -------------------------------------------------------------------------------- 1 | #include "util.h" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | #include "src/node/node.h" 9 | 10 | std::string init_config(std::string address, int basePort, int nodeNum, int thisNode) { 11 | std::vector addr; 12 | addr.push_back(address + ":" + std::to_string(basePort + thisNode)); 13 | 14 | for (int i = 0; i < nodeNum; i++) { 15 | if (i == thisNode) { 16 | continue; 17 | } 18 | addr.push_back(address + ":" + std::to_string(basePort + i)); 19 | } 20 | 21 | std::string config; 22 | for (int i = 0; i < nodeNum; i++) { 23 | config += addr[i]; 24 | if (i < nodeNum - 1) { 25 | config += ","; 26 | } 27 | } 28 | 29 | return config; 30 | } 31 | 32 | std::unique_ptr BuildServer( 33 | const raftcpp::Endpoint &endpoint, std::shared_ptr service) { 34 | grpc::ServerBuilder builder; 35 | // Listen on the given address without any authentication mechanism. 36 | builder.AddListeningPort(endpoint.ToString(), grpc::InsecureServerCredentials()); 37 | // Register "service" as the instance through which we'll communicate with 38 | // clients. In this case, it corresponds to an *synchronous* service. 39 | builder.RegisterService(service.get()); 40 | auto server_ = builder.BuildAndStart(); 41 | return server_; 42 | } -------------------------------------------------------------------------------- /tests/unit_tests/util.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | 6 | #include "src/node/node.h" 7 | 8 | std::string init_config(std::string address, int basePort, int nodeNum, int thisNode); 9 | std::unique_ptr BuildServer( 10 | const raftcpp::Endpoint &endpoint, std::shared_ptr service); --------------------------------------------------------------------------------