├── tests ├── .gitignore ├── CMakeLists.txt ├── test_client_fixture.hpp └── unit_tests.cpp ├── liboffkv ├── config.hpp.in ├── version.hpp.in ├── util.hpp ├── liboffkv.hpp ├── errors.hpp ├── key.hpp ├── ping_sender.hpp ├── client.hpp ├── clib.h ├── clib.cpp ├── zk_client.hpp ├── consul_client.hpp └── etcd_client.hpp ├── .gitignore ├── LICENSE-MIT ├── CMakeLists.txt ├── .travis.yml ├── LICENSE-CC0 ├── LICENSE-APACHE └── README.md /tests/.gitignore: -------------------------------------------------------------------------------- 1 | /test_consul 2 | /test_zk 3 | /test_etcd 4 | -------------------------------------------------------------------------------- /liboffkv/config.hpp.in: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "version.hpp" 4 | 5 | #cmakedefine ENABLE_ZK 6 | #cmakedefine ENABLE_ETCD 7 | #cmakedefine ENABLE_CONSUL 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files 2 | *.slo 3 | *.lo 4 | *.o 5 | *.obj 6 | 7 | # Compiled Dynamic libraries 8 | *.so 9 | *.dylib 10 | *.dll 11 | 12 | # Compiled Static libraries 13 | *.lai 14 | *.la 15 | *.a 16 | *.lib 17 | 18 | # IDEs 19 | .idea/ 20 | 21 | #Build 22 | /cmake-build-debug/ 23 | /cmake-build-release/ 24 | /build/ 25 | /generated/ 26 | -------------------------------------------------------------------------------- /liboffkv/version.hpp.in: -------------------------------------------------------------------------------- 1 | #cmakedefine PROJECT_VERSION_MAJOR @PROJECT_VERSION_MAJOR@ 2 | 3 | #ifndef PROJECT_VERSION_MAJOR 4 | # define PROJECT_VERSION_MAJOR 0 5 | #endif 6 | 7 | #cmakedefine PROJECT_VERSION_MINOR @PROJECT_VERSION_MINOR@ 8 | 9 | #ifndef PROJECT_VERSION_MINOR 10 | # define PROJECT_VERSION_MINOR 0 11 | #endif 12 | 13 | #cmakedefine PROJECT_VERSION_PATCH @PROJECT_VERSION_PATCH@ 14 | 15 | #ifndef PROJECT_VERSION_PATCH 16 | # define PROJECT_VERSION_PATCH 0 17 | #endif 18 | -------------------------------------------------------------------------------- /liboffkv/util.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace liboffkv::detail { 10 | 11 | template 12 | struct always_false : std::false_type {}; 13 | 14 | std::pair split_url(const std::string &url) 15 | { 16 | static const std::string DELIM = "://"; 17 | 18 | const auto pos = url.find(DELIM); 19 | 20 | if (pos == std::string::npos) 21 | throw InvalidAddress("URL must be of 'protocol://address' format"); 22 | 23 | return {url.substr(0, pos), url.substr(pos + DELIM.size())}; 24 | } 25 | 26 | template 27 | bool equal_as_unordered(const std::vector &a, const std::vector &b) 28 | { 29 | return std::multiset(a.begin(), a.end()) == std::multiset(b.begin(), b.end()); 30 | } 31 | 32 | } // namespace liboffkv::util 33 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright 2019–2020 Samuel Marks (for Offscale.io) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /liboffkv/liboffkv.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "client.hpp" 6 | #include "errors.hpp" 7 | #include "util.hpp" 8 | #include "key.hpp" 9 | 10 | #include 11 | 12 | #ifdef ENABLE_CONSUL 13 | # include "consul_client.hpp" 14 | #endif 15 | 16 | #ifdef ENABLE_ETCD 17 | # include "etcd_client.hpp" 18 | #endif 19 | 20 | #ifdef ENABLE_ZK 21 | # include "zk_client.hpp" 22 | #endif 23 | 24 | namespace liboffkv { 25 | 26 | std::unique_ptr open(std::string url, Path prefix = "") 27 | { 28 | auto [protocol, address] = detail::split_url(url); 29 | 30 | #ifdef ENABLE_ZK 31 | if (protocol == "zk") 32 | return std::make_unique(std::move(url), std::move(prefix)); 33 | #endif 34 | 35 | #ifdef ENABLE_CONSUL 36 | if (protocol == "consul") 37 | return std::make_unique(std::move(address), std::move(prefix)); 38 | #endif 39 | 40 | #ifdef ENABLE_ETCD 41 | if (protocol == "etcd") 42 | return std::make_unique(std::move(address), std::move(prefix)); 43 | #endif 44 | 45 | throw InvalidAddress("protocol not supported: " + protocol); 46 | } 47 | 48 | } // namespace liboffkv 49 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | find_package (GTest REQUIRED) 2 | 3 | function (create_test) 4 | set (ONE_VAL_ARGS NAME) 5 | set (MULTI_VAL_ARGS FILES COMPILE_DEFINITIONS LIBS) 6 | cmake_parse_arguments (ARG "" "${ONE_VAL_ARGS}" "${MULTI_VAL_ARGS}" ${ARGN}) 7 | 8 | add_executable (test_${ARG_NAME} ${ARG_FILES}) 9 | target_link_libraries (test_${ARG_NAME} liboffkv) 10 | target_link_libraries (test_${ARG_NAME} ${GTEST_BOTH_LIBRARIES}) 11 | target_link_libraries (test_${ARG_NAME} ${ARG_LIBS}) 12 | 13 | if (NOT "${ARG_COMPILE_DEFINITIONS}" STREQUAL "") 14 | target_compile_definitions (test_${ARG_NAME} PRIVATE ${ARG_COMPILE_DEFINITIONS}) 15 | endif () 16 | 17 | add_test (${ARG_NAME} test_${ARG_NAME}) 18 | endfunction () 19 | 20 | 21 | if (SANITIZE) 22 | message(STATUS "Use ${SANITIZE} sanitizer") 23 | set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-omit-frame-pointer -fsanitize=${SANITIZE}") 24 | set (CMAKE_LINKER_FLAGS "${CMAKE_LINKER_FLAGS} -fno-omit-frame-pointer -fsanitize=${SANITIZE}") 25 | endif() 26 | 27 | foreach (service_addr ${SERVICE_TEST_ADDRESSES}) 28 | string (REGEX MATCH "^[a-zA-Z0-9]+" service_name "${service_addr}") 29 | create_test( 30 | NAME ${service_name} 31 | FILES unit_tests.cpp 32 | COMPILE_DEFINITIONS "SERVICE_ADDRESS=\"${service_addr}\"") 33 | endforeach() 34 | -------------------------------------------------------------------------------- /liboffkv/errors.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace liboffkv { 9 | 10 | class Error : public std::exception {}; 11 | 12 | class InvalidAddress : public Error 13 | { 14 | std::string addr_; 15 | public: 16 | InvalidAddress(std::string addr) : addr_(std::move(addr)) {} 17 | 18 | const char *what() const noexcept override { return addr_.c_str(); } 19 | }; 20 | 21 | class InvalidKey : public Error 22 | { 23 | std::string key_; 24 | public: 25 | InvalidKey(std::string key) : key_(std::move(key)) {} 26 | 27 | const char *what() const noexcept override { return key_.c_str(); } 28 | }; 29 | 30 | class NoEntry : public Error 31 | { 32 | public: 33 | const char *what() const noexcept override { return "no entry"; } 34 | }; 35 | 36 | class EntryExists : public Error 37 | { 38 | public: 39 | const char *what() const noexcept override { return "entry exists"; } 40 | }; 41 | 42 | class NoChildrenForEphemeral : public Error 43 | { 44 | public: 45 | const char *what() const noexcept override 46 | { 47 | return "attempt to create a child of ephemeral node"; 48 | } 49 | }; 50 | 51 | class ConnectionLoss : public Error 52 | { 53 | public: 54 | const char *what() const noexcept override { return "connection loss"; } 55 | }; 56 | 57 | class TxnFailed : public Error 58 | { 59 | size_t failed_op_; 60 | std::string what_; 61 | 62 | public: 63 | TxnFailed(size_t failed_op) 64 | : failed_op_{failed_op} 65 | , what_(std::string("transaction failed on operation with index: ") + 66 | std::to_string(failed_op)) 67 | {} 68 | 69 | const char *what() const noexcept override { return what_.c_str(); } 70 | 71 | size_t failed_op() const { return failed_op_; } 72 | }; 73 | 74 | class ServiceError : public Error 75 | { 76 | std::string what_; 77 | public: 78 | ServiceError(std::string what) : what_(std::move(what)) {} 79 | 80 | const char *what() const noexcept override { return what_.c_str(); } 81 | }; 82 | 83 | } // namespace liboffkv 84 | -------------------------------------------------------------------------------- /liboffkv/key.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include "errors.hpp" 7 | 8 | namespace liboffkv { 9 | 10 | class Path 11 | { 12 | private: 13 | static bool validate_segment_(const std::string &segment) 14 | { 15 | for (unsigned char c : segment) 16 | if (c <= 0x1F || c >= 0x7F) 17 | return false; 18 | return !segment.empty() && 19 | segment != "." && 20 | segment != ".." && 21 | segment != "zookeeper"; 22 | } 23 | 24 | protected: 25 | std::string path_; 26 | 27 | public: 28 | std::vector segments() const 29 | { 30 | if (path_.empty()) 31 | return {}; 32 | if (path_[0] != '/') 33 | throw InvalidKey{path_}; 34 | std::vector result; 35 | auto it = path_.data(), end = it + path_.size(); 36 | while (true) { 37 | ++it; 38 | auto segment_end = std::find(it, end, '/'); 39 | result.emplace_back(it, segment_end); 40 | if (segment_end == end) 41 | break; 42 | it = segment_end; 43 | } 44 | return result; 45 | } 46 | 47 | template 48 | Path(T &&path) 49 | : path_(std::forward(path)) 50 | { 51 | for (const auto &segment : segments()) 52 | if (!validate_segment_(segment)) 53 | throw InvalidKey{path_}; 54 | } 55 | 56 | Path parent() const { return root() ? *this : Path{path_.substr(0, path_.rfind('/'))}; } 57 | 58 | bool root() const { return path_.empty(); } 59 | 60 | Path operator /(const Path &that) const { return Path{path_ + that.path_}; } 61 | 62 | explicit operator std::string() const { return path_; } 63 | 64 | size_t size() const { return path_.size(); } 65 | }; 66 | 67 | class Key : public Path 68 | { 69 | public: 70 | template 71 | Key(T &&key) 72 | : Path(std::forward(key)) 73 | { 74 | if (root()) 75 | throw InvalidKey{path_}; 76 | } 77 | }; 78 | 79 | } // namespace liboffkv 80 | -------------------------------------------------------------------------------- /tests/test_client_fixture.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | #include 12 | 13 | 14 | 15 | class ClientFixture : public ::testing::Test { 16 | public: 17 | static inline std::unique_ptr client; 18 | 19 | static void SetUpTestCase() 20 | { 21 | std::string server_addr = SERVICE_ADDRESS; 22 | std::cout << "\n\n ----------------------------------------------------- \n\n"; 23 | std::cout << " Using server address : " << server_addr << "\n"; 24 | std::cout << "\n ----------------------------------------------------- \n\n\n"; 25 | client = liboffkv::open(server_addr, "/unitTests"); 26 | } 27 | 28 | static void TearDownTestCase() 29 | {} 30 | 31 | void SetUp() 32 | {} 33 | 34 | void TearDown() 35 | {} 36 | 37 | 38 | class KeyHolder { 39 | private: 40 | std::string key_; 41 | 42 | void destroy_() 43 | { 44 | if (!key_.empty()) 45 | try { 46 | client->erase(key_); 47 | } catch (...) {} 48 | } 49 | 50 | public: 51 | explicit KeyHolder(std::string key) 52 | : key_(std::move(key)) 53 | { 54 | try { 55 | client->erase(key_); 56 | } catch (...) {} 57 | } 58 | 59 | KeyHolder(const KeyHolder&) = delete; 60 | 61 | KeyHolder(KeyHolder&& that) 62 | : key_(that.key_) 63 | { 64 | that.key_ = ""; 65 | } 66 | 67 | KeyHolder& operator=(const KeyHolder&) = delete; 68 | 69 | KeyHolder& operator=(KeyHolder&& that) 70 | { 71 | destroy_(); 72 | key_ = that.key_; 73 | that.key_ = ""; 74 | return *this; 75 | } 76 | 77 | ~KeyHolder() 78 | { destroy_(); } 79 | }; 80 | 81 | 82 | template 83 | static std::array hold_keys(Keys&& ... keys) 84 | { 85 | return {KeyHolder(std::move(keys))...}; 86 | } 87 | }; 88 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required (VERSION 3.0) 2 | 3 | project (liboffkv VERSION 0.0.1) 4 | 5 | set (CMAKE_CXX_STANDARD 17) 6 | 7 | option (ENABLE_ZK "Build with ZooKeeper support" ON) 8 | option (ENABLE_ETCD "Build with etcd support" ON) 9 | option (ENABLE_CONSUL "Build with Consul support" ON) 10 | option (BUILD_TESTS "Build library tests" ON) 11 | option (BUILD_CLIB "Build C library" OFF) 12 | set(SANITIZE "" CACHE STRING "Build tests with sanitizer") 13 | 14 | configure_file (liboffkv/version.hpp.in generated/liboffkv/version.hpp @ONLY) 15 | configure_file (liboffkv/config.hpp.in generated/liboffkv/config.hpp @ONLY) 16 | 17 | find_package (Threads REQUIRED) 18 | 19 | if ("${CMAKE_C_COMPILER_ID}" STREQUAL "GNU" OR "${CMAKE_C_COMPILER_ID}" STREQUAL "Clang") 20 | set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pedantic -Wpedantic -Wall -Wextra -Wno-unused-parameter -Wno-missing-field-initializers") 21 | endif () 22 | 23 | add_library (liboffkv INTERFACE) 24 | target_include_directories (liboffkv INTERFACE 25 | ${CMAKE_CURRENT_SOURCE_DIR} 26 | ${CMAKE_CURRENT_BINARY_DIR}/generated) 27 | target_link_libraries (liboffkv INTERFACE ${CMAKE_THREAD_LIBS_INIT}) 28 | 29 | if (BUILD_CLIB) 30 | add_library (liboffkv_c SHARED liboffkv/clib.cpp) 31 | target_link_libraries (liboffkv_c liboffkv) 32 | 33 | install(TARGETS liboffkv_c DESTINATION lib) 34 | install(FILES liboffkv/clib.h RENAME liboffkv.h DESTINATION include) 35 | endif () 36 | 37 | set (SERVICE_TEST_ADDRESSES) 38 | 39 | if (ENABLE_CONSUL) 40 | find_package (ppconsul REQUIRED) 41 | 42 | if (WIN32) 43 | find_package (ZLIB REQUIRED) # weird but works 44 | endif () 45 | target_link_libraries (liboffkv INTERFACE ppconsul) 46 | 47 | list (APPEND SERVICE_TEST_ADDRESSES "consul://localhost:8500") 48 | endif () 49 | 50 | if (ENABLE_ZK) 51 | find_package (zkpp REQUIRED) 52 | 53 | target_link_libraries (liboffkv INTERFACE zkpp) 54 | 55 | list (APPEND SERVICE_TEST_ADDRESSES "zk://localhost:2181") 56 | endif () 57 | 58 | if (ENABLE_ETCD) 59 | find_package (etcdcpp REQUIRED) 60 | find_package (gRPC CONFIG REQUIRED) 61 | 62 | target_link_libraries (liboffkv INTERFACE etcdcpp) 63 | target_link_libraries (liboffkv INTERFACE gRPC::gpr gRPC::grpc gRPC::grpc++ gRPC::grpc_cronet) 64 | 65 | list (APPEND SERVICE_TEST_ADDRESSES "etcd://localhost:2379") 66 | endif () 67 | 68 | if (BUILD_TESTS) 69 | # google tests 70 | enable_testing () 71 | add_subdirectory (tests) 72 | endif () 73 | -------------------------------------------------------------------------------- /liboffkv/ping_sender.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace liboffkv::detail { 9 | 10 | class PingControl 11 | { 12 | std::condition_variable cv_; 13 | std::mutex mtx_; 14 | bool closed_; 15 | 16 | public: 17 | PingControl() 18 | : cv_{} 19 | , mtx_{} 20 | , closed_{false} 21 | {} 22 | 23 | bool wait(std::chrono::seconds timeout) 24 | { 25 | std::unique_lock lock(mtx_); 26 | return cv_.wait_for(lock, timeout, [this]() { return closed_; }); 27 | } 28 | 29 | void close() 30 | { 31 | std::lock_guard lock(mtx_); 32 | closed_ = true; 33 | // https://en.cppreference.com/w/cpp/thread/condition_variable/notify_one 34 | // > Notifying while under the lock may nevertheless be necessary when precise scheduling of 35 | // > events is required, e.g. if the waiting thread would exit the program if the condition 36 | // > is satisfied, causing destruction of the notifying thread's condition_variable. A 37 | // > spurious wakeup after mutex unlock but before notify would result in notify called on a 38 | // > destroyed object. 39 | cv_.notify_one(); 40 | } 41 | }; 42 | 43 | class PingSender 44 | { 45 | PingControl *ctl_; // this belongs to a thread. 46 | 47 | void destroy_() { if (ctl_) ctl_->close(); } 48 | 49 | public: 50 | PingSender() : ctl_{nullptr} {} 51 | 52 | template 53 | PingSender(std::chrono::seconds first_timeout, Callback &&callback) 54 | : ctl_{new PingControl{}} 55 | { 56 | try { 57 | std::thread([ctl = ctl_, first_timeout, callback = std::forward(callback)]() { 58 | auto timeout = first_timeout; 59 | while (!ctl->wait(timeout)) 60 | timeout = callback(); 61 | delete ctl; 62 | }).detach(); 63 | } catch (...) { 64 | delete ctl_; 65 | throw; 66 | } 67 | } 68 | 69 | PingSender(const PingSender &) = delete; 70 | 71 | PingSender(PingSender &&that) 72 | : ctl_{that.ctl_} 73 | { 74 | that.ctl_ = nullptr; 75 | } 76 | 77 | PingSender& operator =(const PingSender &) = delete; 78 | 79 | PingSender& operator =(PingSender &&that) 80 | { 81 | destroy_(); 82 | ctl_ = that.ctl_; 83 | that.ctl_ = nullptr; 84 | return *this; 85 | } 86 | 87 | operator bool() const { return !!ctl_; } 88 | 89 | ~PingSender() { destroy_(); } 90 | }; 91 | 92 | } // namespace liboffkv 93 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: cpp 2 | 3 | git: 4 | depth: false 5 | 6 | cache: 7 | directories: 8 | - "$HOME/vcpkg" 9 | - "$HOME/Downloads" 10 | 11 | matrix: 12 | include: 13 | - os: linux 14 | sudo: true 15 | env: 16 | - VCPKG_TRIPLET="x64-linux" 17 | - MATRIX_EVAL="export CC=gcc-8 && export CXX=g++-8" 18 | - VCPKG_BOOT_EVAL="./bootstrap-vcpkg.sh -disableMetrics" 19 | - SYSTEM_TRIPLET="linux" 20 | - PREFERRED_ARCHIVE_EXTENSION="tar.gz" 21 | - LIBOFFKV_CMAKE_FLAGS="" 22 | - BADGE=linux 23 | addons: 24 | apt: 25 | sources: 26 | - ubuntu-toolchain-r-test 27 | packages: 28 | - g++-8 29 | - cmake 30 | - os: osx 31 | osx_image: xcode10.2 32 | env: 33 | - MATRIX_EVAL="brew install gcc@7" 34 | - OSX_ENABLE_GCC="CC=gcc-7 && CXX=g++-7" 35 | - OSX_ENABLE_CLANG="CC=clang && CXX=clang++" 36 | - VCPKG_BOOT_EVAL="./bootstrap-vcpkg.sh -disableMetrics" 37 | - SYSTEM_TRIPLET="darwin" 38 | - PREFERRED_ARCHIVE_EXTENSION="zip" 39 | - LIBOFFKV_CMAKE_FLAGS="" 40 | - BADGE=osx 41 | sudo: true 42 | # - os: windows 43 | # env: 44 | # - VCPKG_TRIPLET=x64-windows 45 | # - VCPKG_BOOT_EVAL="./bootstrap-vcpkg.bat" 46 | # - SYSTEM_TRIPLET=windows 47 | # - PREFERRED_ARCHIVE_EXTENSION=zip 48 | 49 | before_install: 50 | - eval "$MATRIX_EVAL" 51 | - eval "$OSX_ENABLE_GCC" 52 | 53 | before_script: 54 | - set +x 55 | - curl -sL https://github.com/offscale/kv-travis-scripts/archive/master.zip | jar xv 56 | - mv kv-travis-scripts-master "$HOME/scripts" 57 | - pushd "$HOME/scripts" 58 | - chmod +x *.bash 59 | - ./prepare_vcpkg.bash "$HOME/vcpkg/" "$VCPKG_BOOT_EVAL" 60 | - ./bootstrap_etcd.bash "$SYSTEM_TRIPLET" "$PREFERRED_ARCHIVE_EXTENSION" 61 | - ./bootstrap_consul.bash "$SYSTEM_TRIPLET" 62 | - ./bootstrap_zk.bash 63 | - pushd "$HOME/vcpkg" 64 | - travis_wait 30 ./vcpkg upgrade --no-dry-run 65 | - eval "$OSX_ENABLE_CLANG" 66 | - travis_wait 25 ./vcpkg install gtest || "$HOME/scripts/export_vcpkg_logs.bash" 67 | - travis_wait 34 ./vcpkg install ppconsul || "$HOME/scripts/export_vcpkg_logs.bash" 68 | - travis_wait 25 ./vcpkg install zkpp || "$HOME/scripts/export_vcpkg_logs.bash" 69 | - travis_wait 35 ./vcpkg install offscale-libetcd-cpp || "$HOME/scripts/export_vcpkg_logs.bash" 70 | - rm -rf buildtrees 71 | 72 | script: 73 | - mkdir "$TRAVIS_BUILD_DIR/cmake-build-debug" && pushd "$_" 74 | - cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE="$HOME/vcpkg/scripts/buildsystems/vcpkg.cmake" -DBUILD_TESTS=ON "${LIBOFFKV_CMAKE_FLAGS}" .. 75 | - cmake --build . 76 | - ctest --verbose 77 | 78 | after_failure: 79 | - "$HOME/scripts/send_status_message.bash" 80 | -------------------------------------------------------------------------------- /liboffkv/client.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "key.hpp" 10 | 11 | namespace liboffkv { 12 | 13 | class WatchHandle 14 | { 15 | public: 16 | virtual void wait() = 0; 17 | virtual ~WatchHandle() = default; 18 | }; 19 | 20 | struct ExistsResult 21 | { 22 | int64_t version; 23 | std::unique_ptr watch; 24 | 25 | operator bool() const { return version != 0; } 26 | }; 27 | 28 | struct ChildrenResult 29 | { 30 | std::vector children; 31 | std::unique_ptr watch; 32 | }; 33 | 34 | struct GetResult 35 | { 36 | int64_t version; 37 | std::string value; 38 | std::unique_ptr watch; 39 | }; 40 | 41 | struct CasResult 42 | { 43 | int64_t version; 44 | 45 | operator bool() const { return version != 0; } 46 | }; 47 | 48 | struct TxnCheck 49 | { 50 | Key key; 51 | int64_t version; 52 | 53 | TxnCheck(Key key_, int64_t version_) 54 | : key(std::move(key_)) 55 | , version{version_} 56 | {} 57 | }; 58 | 59 | struct TxnOpCreate 60 | { 61 | Key key; 62 | std::string value; 63 | bool lease; 64 | 65 | TxnOpCreate(Key key_, std::string value_, bool lease_ = false) 66 | : key(std::move(key_)) 67 | , value(std::move(value_)) 68 | , lease{lease_} 69 | {} 70 | }; 71 | 72 | struct TxnOpSet 73 | { 74 | Key key; 75 | std::string value; 76 | 77 | TxnOpSet(Key key_, std::string value_) 78 | : key(std::move(key_)) 79 | , value(std::move(value_)) 80 | {} 81 | }; 82 | 83 | struct TxnOpErase 84 | { 85 | Key key; 86 | 87 | explicit TxnOpErase(Key key_) 88 | : key(std::move(key_)) 89 | {} 90 | }; 91 | 92 | using TxnOp = std::variant; 93 | 94 | struct TxnOpResult 95 | { 96 | enum class Kind 97 | { 98 | CREATE, 99 | SET, 100 | }; 101 | 102 | Kind kind; 103 | int64_t version; 104 | }; 105 | 106 | 107 | using TransactionResult = std::vector; 108 | 109 | struct Transaction { 110 | std::vector checks; 111 | std::vector ops; 112 | }; 113 | 114 | 115 | class Client 116 | { 117 | protected: 118 | Path prefix_; 119 | 120 | public: 121 | explicit Client(Path prefix) 122 | : prefix_{std::move(prefix)} 123 | {} 124 | 125 | virtual int64_t create(const Key &key, const std::string &value, bool lease = false) = 0; 126 | 127 | virtual ExistsResult exists(const Key &key, bool watch = false) = 0; 128 | 129 | virtual ChildrenResult get_children(const Key &key, bool watch = false) = 0; 130 | 131 | virtual int64_t set(const Key &key, const std::string &value) = 0; 132 | 133 | virtual GetResult get(const Key &key, bool watch = false) = 0; 134 | 135 | virtual CasResult cas(const Key &key, const std::string &value, int64_t version = 0) = 0; 136 | 137 | virtual void erase(const Key &key, int64_t version = 0) = 0; 138 | 139 | virtual TransactionResult commit(const Transaction&) = 0; 140 | 141 | virtual ~Client() = default; 142 | }; 143 | 144 | } // namespace liboffkv 145 | -------------------------------------------------------------------------------- /liboffkv/clib.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | // flags 7 | enum { 8 | OFFKV_LEASE = 1 << 0, 9 | }; 10 | 11 | // errors 12 | enum { 13 | OFFKV_EADDR = -1, 14 | OFFKV_EKEY = -2, 15 | OFFKV_ENOENT = -3, 16 | OFFKV_EEXIST = -4, 17 | OFFKV_EEPHEM = -5, 18 | OFFKV_ECONN = -6, 19 | OFFKV_ETXN = -7, 20 | OFFKV_ESRV = -8, 21 | OFFKV_ENOMEM = -9, 22 | }; 23 | 24 | // transaction operations 25 | enum { 26 | OFFKV_OP_CREATE, 27 | OFFKV_OP_SET, 28 | OFFKV_OP_ERASE, 29 | }; 30 | 31 | typedef void *offkv_Handle; 32 | typedef void *offkv_Watch; 33 | 34 | // Filled and destroyed (with /offkv_get_result_free()/) by the library. 35 | typedef struct { 36 | char *value; 37 | size_t nvalue; 38 | int64_t version; 39 | } offkv_GetResult; 40 | 41 | // Filled and destroyed (with /offkv_children_free()/) by the library. 42 | typedef struct { 43 | char **keys; 44 | size_t nkeys; 45 | int errcode; 46 | } offkv_Children; 47 | 48 | // Filled and (possibly) destroyed by user. 49 | typedef struct { 50 | const char *key; 51 | int64_t version; 52 | } offkv_TxnCheck; 53 | 54 | // Filled and (possibly) destroyed by user. 55 | typedef struct { 56 | int op; 57 | int flags; 58 | const char *key; 59 | const char *value; 60 | size_t nvalue; 61 | } offkv_TxnOp; 62 | 63 | typedef struct { 64 | int op; 65 | int64_t version; 66 | } offkv_TxnOpResult; 67 | 68 | // Filled and destroyed (with /offkv_txn_result_free()/) by the library. 69 | typedef struct { 70 | offkv_TxnOpResult *results; 71 | size_t nresults; 72 | size_t failed_op; 73 | } offkv_TxnResult; 74 | 75 | // Returns a pointer to a static string. 76 | const char * 77 | offkv_error_descr(int /*errcode*/); 78 | 79 | // On error, returns /NULL/ and writes to /p_errcode/, unless it is /NULL/. 80 | offkv_Handle 81 | offkv_open(const char * /*url*/, const char * /*prefix*/, int * /*p_errcode*/); 82 | 83 | // On error, returns negative value. 84 | // On success, returns the version of the created node. 85 | int64_t 86 | offkv_create( 87 | offkv_Handle, 88 | const char * /*key*/, 89 | const char * /*value*/, 90 | size_t /*nvalue*/, 91 | int /*flags*/); 92 | 93 | // On error, returns negative value. 94 | // On success, returns the version of the created node. 95 | int64_t 96 | offkv_set( 97 | offkv_Handle, 98 | const char * /*key*/, 99 | const char * /*value*/, 100 | size_t /*nvalue*/); 101 | 102 | // If /p_watch/ is not NULL, a new watch is created and written into it. 103 | // 104 | // On error, returns negative value. 105 | // On success, returns the version of the existing node or 0. 106 | int64_t 107 | offkv_exists(offkv_Handle, const char * /*key*/, offkv_Watch * /*p_watch*/); 108 | 109 | // If /p_watch/ is not NULL, a new watch is created and written into it. 110 | // 111 | // On error, returns /offkv_GetResult/ with a negative /version/ field; it may not be freed in this 112 | // case. 113 | offkv_GetResult 114 | offkv_get(offkv_Handle, const char * /*key*/, offkv_Watch * /*p_watch*/); 115 | 116 | // If /p_watch/ is not NULL, a new watch is created and written into it. 117 | // 118 | // On error, returns /offkv_Chilren/ with a negative /errcode/ field; it may not be freed in this 119 | // case. 120 | offkv_Children 121 | offkv_children(offkv_Handle, const char * /*key*/, offkv_Watch * /*p_watch*/); 122 | 123 | // On error, returns negative value. 124 | // On success, returns 0. 125 | // 126 | // Never consumes the watch; you still have to call /offkv_watch_drop/ on it. 127 | int 128 | offkv_watch(offkv_Watch /*watch*/); 129 | 130 | // Frees a watch. 131 | void 132 | offkv_watch_drop(offkv_Watch /*watch*/); 133 | 134 | // On error, returns negative value. 135 | // On success, returns 0. 136 | int 137 | offkv_erase(offkv_Handle, const char * /*key*/, int64_t /*version*/); 138 | 139 | // On error, returns negative value. 140 | // If compare-and-swap succeeded, returns the new version of the node (positive). 141 | // If compare-and-swap failed, returns 0. 142 | int64_t 143 | offkv_cas( 144 | offkv_Handle, 145 | const char * /*key*/, 146 | const char * /*value*/, 147 | size_t /*nvalue*/, 148 | int64_t /*version*/); 149 | 150 | // On a non-transaction error (for example, if a key is invalid), its code is returned and 151 | // /p_results/ is not written to. 152 | // 153 | // On a transaction error, /OFFKV_ETXN/ is returned and, if /p_results/ is not NULL: 154 | // 1. /p_results->failed_op/ is filled with the index of the failed operation; 155 | // 2. /*p_results/ may not be freed. 156 | // 157 | // On success, 0 is returned and, if /p_results/ is not NULL, /*p_results/ is filled (and 158 | // /p_results->failed_op/ is set to /(size_t) -1/). 159 | int 160 | offkv_commit( 161 | offkv_Handle, 162 | const offkv_TxnCheck * /*checks*/, size_t /*nchecks*/, 163 | const offkv_TxnOp * /*ops*/, size_t /*nops*/, 164 | offkv_TxnResult * /*p_results*/); 165 | 166 | // Frees a value returned from /offkv_get()/. 167 | void 168 | offkv_get_result_free(offkv_GetResult); 169 | 170 | // Frees a value returned from /offkv_children()/. 171 | void 172 | offkv_children_free(offkv_Children); 173 | 174 | // Frees a value written to by /offkv_commit()/. 175 | void 176 | offkv_txn_result_free(offkv_TxnResult); 177 | 178 | // Closes a handle returned from /offkv_open()/. 179 | void 180 | offkv_close(offkv_Handle); 181 | -------------------------------------------------------------------------------- /LICENSE-CC0: -------------------------------------------------------------------------------- 1 | CC0 1.0 Universal 2 | 3 | Statement of Purpose 4 | 5 | The laws of most jurisdictions throughout the world automatically confer 6 | exclusive Copyright and Related Rights (defined below) upon the creator and 7 | subsequent owner(s) (each and all, an "owner") of an original work of 8 | authorship and/or a database (each, a "Work"). 9 | 10 | Certain owners wish to permanently relinquish those rights to a Work for the 11 | purpose of contributing to a commons of creative, cultural and scientific 12 | works ("Commons") that the public can reliably and without fear of later 13 | claims of infringement build upon, modify, incorporate in other works, reuse 14 | and redistribute as freely as possible in any form whatsoever and for any 15 | purposes, including without limitation commercial purposes. These owners may 16 | contribute to the Commons to promote the ideal of a free culture and the 17 | further production of creative, cultural and scientific works, or to gain 18 | reputation or greater distribution for their Work in part through the use and 19 | efforts of others. 20 | 21 | For these and/or other purposes and motivations, and without any expectation 22 | of additional consideration or compensation, the person associating CC0 with a 23 | Work (the "Affirmer"), to the extent that he or she is an owner of Copyright 24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work 25 | and publicly distribute the Work under its terms, with knowledge of his or her 26 | Copyright and Related Rights in the Work and the meaning and intended legal 27 | effect of CC0 on those rights. 28 | 29 | 1. Copyright and Related Rights. A Work made available under CC0 may be 30 | protected by copyright and related or neighboring rights ("Copyright and 31 | Related Rights"). Copyright and Related Rights include, but are not limited 32 | to, the following: 33 | 34 | i. the right to reproduce, adapt, distribute, perform, display, communicate, 35 | and translate a Work; 36 | 37 | ii. moral rights retained by the original author(s) and/or performer(s); 38 | 39 | iii. publicity and privacy rights pertaining to a person's image or likeness 40 | depicted in a Work; 41 | 42 | iv. rights protecting against unfair competition in regards to a Work, 43 | subject to the limitations in paragraph 4(a), below; 44 | 45 | v. rights protecting the extraction, dissemination, use and reuse of data in 46 | a Work; 47 | 48 | vi. database rights (such as those arising under Directive 96/9/EC of the 49 | European Parliament and of the Council of 11 March 1996 on the legal 50 | protection of databases, and under any national implementation thereof, 51 | including any amended or successor version of such directive); and 52 | 53 | vii. other similar, equivalent or corresponding rights throughout the world 54 | based on applicable law or treaty, and any national implementations thereof. 55 | 56 | 2. Waiver. To the greatest extent permitted by, but not in contravention of, 57 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and 58 | unconditionally waives, abandons, and surrenders all of Affirmer's Copyright 59 | and Related Rights and associated claims and causes of action, whether now 60 | known or unknown (including existing as well as future claims and causes of 61 | action), in the Work (i) in all territories worldwide, (ii) for the maximum 62 | duration provided by applicable law or treaty (including future time 63 | extensions), (iii) in any current or future medium and for any number of 64 | copies, and (iv) for any purpose whatsoever, including without limitation 65 | commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes 66 | the Waiver for the benefit of each member of the public at large and to the 67 | detriment of Affirmer's heirs and successors, fully intending that such Waiver 68 | shall not be subject to revocation, rescission, cancellation, termination, or 69 | any other legal or equitable action to disrupt the quiet enjoyment of the Work 70 | by the public as contemplated by Affirmer's express Statement of Purpose. 71 | 72 | 3. Public License Fallback. Should any part of the Waiver for any reason be 73 | judged legally invalid or ineffective under applicable law, then the Waiver 74 | shall be preserved to the maximum extent permitted taking into account 75 | Affirmer's express Statement of Purpose. In addition, to the extent the Waiver 76 | is so judged Affirmer hereby grants to each affected person a royalty-free, 77 | non transferable, non sublicensable, non exclusive, irrevocable and 78 | unconditional license to exercise Affirmer's Copyright and Related Rights in 79 | the Work (i) in all territories worldwide, (ii) for the maximum duration 80 | provided by applicable law or treaty (including future time extensions), (iii) 81 | in any current or future medium and for any number of copies, and (iv) for any 82 | purpose whatsoever, including without limitation commercial, advertising or 83 | promotional purposes (the "License"). The License shall be deemed effective as 84 | of the date CC0 was applied by Affirmer to the Work. Should any part of the 85 | License for any reason be judged legally invalid or ineffective under 86 | applicable law, such partial invalidity or ineffectiveness shall not 87 | invalidate the remainder of the License, and in such case Affirmer hereby 88 | affirms that he or she will not (i) exercise any of his or her remaining 89 | Copyright and Related Rights in the Work or (ii) assert any associated claims 90 | and causes of action with respect to the Work, in either case contrary to 91 | Affirmer's express Statement of Purpose. 92 | 93 | 4. Limitations and Disclaimers. 94 | 95 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 96 | surrendered, licensed or otherwise affected by this document. 97 | 98 | b. Affirmer offers the Work as-is and makes no representations or warranties 99 | of any kind concerning the Work, express, implied, statutory or otherwise, 100 | including without limitation warranties of title, merchantability, fitness 101 | for a particular purpose, non infringement, or the absence of latent or 102 | other defects, accuracy, or the present or absence of errors, whether or not 103 | discoverable, all to the greatest extent permissible under applicable law. 104 | 105 | c. Affirmer disclaims responsibility for clearing rights of other persons 106 | that may apply to the Work or any use thereof, including without limitation 107 | any person's Copyright and Related Rights in the Work. Further, Affirmer 108 | disclaims responsibility for obtaining any necessary consents, permissions 109 | or other rights required for any use of the Work. 110 | 111 | d. Affirmer understands and acknowledges that Creative Commons is not a 112 | party to this document and has no duty or obligation with respect to this 113 | CC0 or use of the Work. 114 | 115 | For more information, please see 116 | 117 | -------------------------------------------------------------------------------- /liboffkv/clib.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | #if __GNUC__ >= 2 11 | # define UNREACHABLE() __builtin_unreachable() 12 | #elif _MSC_VER >= 1100 13 | # define UNREACHABLE() __assume(false) 14 | #else 15 | # define UNREACHABLE() void() 16 | #endif 17 | 18 | extern "C" { 19 | #include "clib.h" 20 | } 21 | 22 | static inline offkv_Handle wrap_client(std::unique_ptr &&p) 23 | { 24 | return reinterpret_cast(p.release()); 25 | } 26 | 27 | static inline offkv_Watch wrap_watch(std::unique_ptr &&p) 28 | { 29 | return reinterpret_cast(p.release()); 30 | } 31 | 32 | static inline liboffkv::Client *unwrap_client(offkv_Handle h) 33 | { 34 | return reinterpret_cast(h); 35 | } 36 | 37 | static inline liboffkv::WatchHandle *unwrap_watch(offkv_Watch w) 38 | { 39 | return reinterpret_cast(w); 40 | } 41 | 42 | const char *offkv_error_descr(int errcode) 43 | { 44 | switch (errcode) { 45 | case OFFKV_EADDR: 46 | return "invalid address"; 47 | case OFFKV_EKEY: 48 | return "invalid key"; 49 | case OFFKV_ENOENT: 50 | return "no entry"; 51 | case OFFKV_EEXIST: 52 | return "entry exists"; 53 | case OFFKV_EEPHEM: 54 | return "attempt to create a child of ephemeral node"; 55 | case OFFKV_ECONN: 56 | return "connection loss"; 57 | case OFFKV_ETXN: 58 | return "transaction failed"; 59 | case OFFKV_ESRV: 60 | return "service error"; 61 | case OFFKV_ENOMEM: 62 | return "out of memory"; 63 | default: 64 | return nullptr; 65 | } 66 | } 67 | 68 | static int to_errcode(const std::exception &e) 69 | { 70 | if (dynamic_cast(&e)) 71 | return OFFKV_EADDR; 72 | else if (dynamic_cast(&e)) 73 | return OFFKV_EKEY; 74 | else if (dynamic_cast(&e)) 75 | return OFFKV_ENOENT; 76 | else if (dynamic_cast(&e)) 77 | return OFFKV_EEXIST; 78 | else if (dynamic_cast(&e)) 79 | return OFFKV_EEPHEM; 80 | else if (dynamic_cast(&e)) 81 | return OFFKV_ECONN; 82 | else if (dynamic_cast(&e)) 83 | return OFFKV_ETXN; 84 | else if (dynamic_cast(&e)) 85 | return OFFKV_ESRV; 86 | else if (dynamic_cast(&e)) 87 | return OFFKV_ENOMEM; 88 | throw e; 89 | } 90 | 91 | static char *dup_string(const std::string &s) 92 | { 93 | char *r = new char[s.size() + 1]; 94 | const auto data = s.data(); 95 | std::copy(data, data + s.size() + 1, r); 96 | return r; 97 | } 98 | 99 | static void free_string(char *p) 100 | { 101 | delete[] p; 102 | } 103 | 104 | static void free_strings(char **strings, size_t nstrings) 105 | { 106 | for (size_t i = 0; i < nstrings; ++i) 107 | free_string(strings[i]); 108 | delete[] strings; 109 | } 110 | 111 | static char **dup_strings(const std::vector &strings) 112 | { 113 | char **r = new char *[strings.size()]; 114 | for (size_t i = 0; i < strings.size(); ++i) 115 | try { 116 | r[i] = dup_string(strings[i]); 117 | } catch (...) { 118 | free_strings(r, i); 119 | throw; 120 | } 121 | return r; 122 | } 123 | 124 | offkv_Handle offkv_open(const char *url, const char *prefix, int *p_errcode) 125 | { 126 | try { 127 | return wrap_client(liboffkv::open(url, prefix)); 128 | } catch (const std::exception &e) { 129 | const int errcode = to_errcode(e); 130 | if (p_errcode) 131 | *p_errcode = errcode; 132 | return nullptr; 133 | } 134 | } 135 | 136 | int64_t offkv_create(offkv_Handle h, const char *key, const char *value, size_t nvalue, int flags) 137 | { 138 | try { 139 | return unwrap_client(h)->create(key, std::string(value, nvalue), flags & OFFKV_LEASE); 140 | } catch (const std::exception &e) { 141 | return to_errcode(e); 142 | } 143 | } 144 | 145 | int64_t offkv_set(offkv_Handle h, const char *key, const char *value, size_t nvalue) 146 | { 147 | try { 148 | return unwrap_client(h)->set(key, std::string(value, nvalue)); 149 | } catch (const std::exception &e) { 150 | return to_errcode(e); 151 | } 152 | } 153 | 154 | int64_t offkv_exists(offkv_Handle h, const char *key, offkv_Watch *p_watch) 155 | { 156 | try { 157 | auto r = unwrap_client(h)->exists(key, !!p_watch); 158 | if (p_watch) 159 | *p_watch = wrap_watch(std::move(r.watch)); 160 | return r.version; 161 | } catch (const std::exception &e) { 162 | return to_errcode(e); 163 | } 164 | } 165 | 166 | offkv_GetResult offkv_get(offkv_Handle h, const char *key, offkv_Watch *p_watch) 167 | { 168 | try { 169 | auto r = unwrap_client(h)->get(key, !!p_watch); 170 | if (p_watch) 171 | *p_watch = wrap_watch(std::move(r.watch)); 172 | return { 173 | dup_string(r.value), 174 | r.value.size(), 175 | r.version, 176 | }; 177 | } catch (const std::exception &e) { 178 | return { 179 | nullptr, 180 | 0, 181 | static_cast(to_errcode(e)), 182 | }; 183 | } 184 | } 185 | 186 | offkv_Children offkv_children(offkv_Handle h, const char *key, offkv_Watch *p_watch) 187 | { 188 | try { 189 | auto r = unwrap_client(h)->get_children(key, !!p_watch); 190 | if (p_watch) 191 | *p_watch = wrap_watch(std::move(r.watch)); 192 | return { 193 | dup_strings(r.children), 194 | r.children.size(), 195 | 0, 196 | }; 197 | } catch (const std::exception &e) { 198 | return { 199 | nullptr, 200 | 0, 201 | to_errcode(e), 202 | }; 203 | } 204 | } 205 | 206 | void offkv_children_free(offkv_Children c) 207 | { 208 | free_strings(c.keys, c.nkeys); 209 | } 210 | 211 | void offkv_get_result_free(offkv_GetResult r) 212 | { 213 | free_string(r.value); 214 | } 215 | 216 | int offkv_watch(offkv_Watch w) 217 | { 218 | try { 219 | unwrap_watch(w)->wait(); 220 | return 0; 221 | } catch (const std::exception &e) { 222 | return to_errcode(e); 223 | } 224 | } 225 | 226 | void offkv_watch_drop(offkv_Watch w) 227 | { 228 | delete unwrap_watch(w); 229 | } 230 | 231 | int offkv_erase(offkv_Handle h, const char *key, int64_t version) 232 | { 233 | try { 234 | unwrap_client(h)->erase(key, version); 235 | return 0; 236 | } catch (const std::exception &e) { 237 | return to_errcode(e); 238 | } 239 | } 240 | 241 | int64_t offkv_cas( 242 | offkv_Handle h, 243 | const char *key, 244 | const char *value, 245 | size_t nvalue, 246 | int64_t version) 247 | { 248 | try { 249 | auto r = unwrap_client(h)->cas(key, std::string(value, nvalue), version); 250 | return r ? r.version : 0; 251 | } catch (const std::exception &e) { 252 | return to_errcode(e); 253 | } 254 | } 255 | 256 | static offkv_TxnOpResult *dup_txn_results(const std::vector &data) 257 | { 258 | offkv_TxnOpResult *r = new offkv_TxnOpResult[data.size()]; 259 | std::transform(data.begin(), data.end(), r, [](liboffkv::TxnOpResult arg) -> offkv_TxnOpResult { 260 | switch (arg.kind) { 261 | case liboffkv::TxnOpResult::Kind::CREATE: 262 | return {OFFKV_OP_CREATE, arg.version}; 263 | case liboffkv::TxnOpResult::Kind::SET: 264 | return {OFFKV_OP_SET, arg.version}; 265 | } 266 | UNREACHABLE(); 267 | }); 268 | return r; 269 | } 270 | 271 | static void free_txn_results(offkv_TxnOpResult *p) 272 | { 273 | delete[] p; 274 | } 275 | 276 | int 277 | offkv_commit( 278 | offkv_Handle h, 279 | const offkv_TxnCheck *checks, size_t nchecks, 280 | const offkv_TxnOp *ops, size_t nops, 281 | offkv_TxnResult *p_result) 282 | { 283 | try { 284 | std::vector checks_vec; 285 | for (size_t i = 0; i < nchecks; ++i) 286 | checks_vec.emplace_back(checks[i].key, checks[i].version); 287 | 288 | std::vector ops_vec; 289 | for (size_t i = 0; i < nops; ++i) 290 | switch (ops[i].op) { 291 | case OFFKV_OP_CREATE: 292 | ops_vec.push_back(liboffkv::TxnOpCreate( 293 | ops[i].key, 294 | std::string(ops[i].value, ops[i].nvalue), 295 | ops[i].flags & OFFKV_LEASE 296 | )); 297 | break; 298 | case OFFKV_OP_SET: 299 | ops_vec.push_back(liboffkv::TxnOpSet( 300 | ops[i].key, 301 | std::string(ops[i].value, ops[i].nvalue) 302 | )); 303 | break; 304 | case OFFKV_OP_ERASE: 305 | ops_vec.push_back(liboffkv::TxnOpErase(ops[i].key)); 306 | break; 307 | default: 308 | UNREACHABLE(); 309 | } 310 | 311 | auto r = unwrap_client(h)->commit({checks_vec, ops_vec}); 312 | if (p_result) 313 | *p_result = {dup_txn_results(r), r.size(), static_cast(-1)}; 314 | return 0; 315 | 316 | } catch (const liboffkv::TxnFailed &e) { 317 | if (p_result) 318 | *p_result = {nullptr, 0, e.failed_op()}; 319 | return OFFKV_ETXN; 320 | } catch (const std::exception &e) { 321 | return to_errcode(e); 322 | } 323 | } 324 | 325 | void offkv_txn_result_free(offkv_TxnResult r) 326 | { 327 | free_txn_results(r.results); 328 | } 329 | 330 | void offkv_close(offkv_Handle h) 331 | { 332 | delete unwrap_client(h); 333 | } 334 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2019–2020 Samuel Marks (for Offscale.io) 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | liboffkv 2 | ======== 3 | [![License](https://img.shields.io/badge/license-Apache--2.0%20OR%20MIT-blue.svg)](https://opensource.org/licenses/Apache-2.0) 4 | [![Travis CI](http://badges.herokuapp.com/travis/offscale/liboffkv?branch=master&label=OSX&env=BADGE=osx&style=flat-square)](https://travis-ci.org/offscale/liboffkv) 5 | [![Travis CI](http://badges.herokuapp.com/travis/offscale/liboffkv?branch=master&label=Linux&env=BADGE=linux&style=flat-square)](https://travis-ci.org/offscale/liboffkv) 6 | 7 | #### The library is designed to provide a uniform interface for three distributed KV storages: etcd, ZooKeeper and Consul. 8 | 9 | The services have similar but different data models, so we outlined the common features. 10 | 11 | In our implementation, keys form a ZK-like hierarchy. Each key has a version that is int64 number greater than 0. Current version is returned with other data by the most of operations. All the operations supported are listed below. 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 29 | 34 | 35 | 36 | 37 | 40 | 45 | 46 | 47 | 48 | 52 | 60 | 61 | 62 | 63 | 66 | 72 | 73 | 74 | 75 | 78 | 83 | 84 | 85 | 86 | 89 | 95 | 96 | 97 | 98 | 106 | 107 | 108 | 109 | 110 | 113 | 114 | 115 |
MethodParametersDescription
create 25 | key: string
26 | value: char[]
27 | leased: bool (=false) -- makes the key to be deleted on client disconnect 28 |
Creates the key.
30 | Throws an exception if the key already exists or
31 | preceding entry does not exist.
32 | Returns: version of the newly created key. 33 |
setkey: string
38 | value: char[] 39 |
Assigns the value.
41 | Creates the key if it doesn’t exist.
42 | Throws an exception if preceding entry does not exist.
43 | Returns: new version of the key. 44 |
caskey: string
49 | value: char[]
50 | version: int64 (=0) -- expected version of the key 51 |
53 | Compare and set operation.
54 | If the key does not exist and the version passed equals 0, creates it.
55 | Throws an exception if preceding entry does not exist.
56 | If the key exists and its version equals to specified one updates the value. 57 | Otherwise does nothing and returns 0.
58 | Returns: new version of the key or 0. 59 |
getkey: string
64 | watch: bool (=false) -- start watching for change in value 65 |
67 | Returns the value currently assigned to the key.
68 | Throws an exception if the key does not exist.
69 | If watch is true, creates WatchHandler waiting for a change in value. (see usage example below).
70 | Returns: current value and WatchHandler. 71 |
existskey: string
76 | watch: bool (=false) -- start watching for removal or creation of the key 77 |
79 | Checks if the key exists.
80 | If watch is true, creates WatchHandler waiting for a change in state of existance (see usage example below).
81 | Returns: version of the key or 0 if it doesn't exist and WatchHandler. 82 |
get_childrenkey: string
87 | watch: bool (=false) 88 |
90 | Returns a list of the key's direct children.
91 | Throws an exception if the key does not exist.
92 | If watch is true, creates WatchHandler waiting for any changes among the children.
93 | Returns: list of direct children and WatchHandler. 94 |
erasekey: string
99 | version: int64 (=0) 100 |
101 | Erases the key and all its descendants if the version given equals to the current key's version.
102 | Does it unconditionally if version is 0.
103 | Throws an exception if the key does not exist.
104 | Returns: (void) 105 |
committransaction: TransactionCommits transaction (see transactions API below).
111 | If it was failed, throws TxnFailed with an index of the failed operation.
112 | Returns: list of new versions of keys affected by the transaction
116 | 117 | ### Transactions 118 | Transaction is a chain of operations of 4 types: create, set, erase, check, performing atomically. Their descriptions can be found below. 119 | N.b. at the moment set has different behavior in comparison to ordinary set: when used in transaction, it does not create the key if it does not exist. Besides you cannot assign watches. Leases are still available. 120 | Transaction body is separated into two blocks: firstly you should write all required checks and then the sequence of other operations (see an example below). As a result a list of new versions for all the keys involved in set operations is returned. 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 137 | 140 | 141 | 142 | 143 | 146 | 150 | 151 | 152 | 153 | 161 | 162 | 163 | 166 | 169 | 170 |
MethodParametersDescription
create 133 | key: string
134 | value: char[]
135 | leased: bool (=false) 136 |
Creates the key.
138 | Rolls back if the key already exists or preceding entry does not exist.
139 |
setkey: string
144 | value: char[] 145 |
Set the value.
147 | Rolls back if the key does not exist.
148 | n.b behaviors of transaction set and ordinary set differ 149 |
erasekey: string
154 | version: int64 (=0)
155 |
156 | Erases the key and all its descendants
157 | if the version passed equals to the current key's version.
158 | Does it unconditionally if the version is 0.
159 | Rolls back if the key does not exist.
160 |
checkkey: string
164 | version: int64 165 |
Checks if the given key has the specified version.
167 | Only checks if it exists if the version is 0 168 |
171 | 172 | ## Usage 173 | ```cpp 174 | #include 175 | #include 176 | 177 | #include 178 | 179 | using namespace liboffkv; 180 | 181 | 182 | int main() 183 | { 184 | // firstly specify protocol (zk | consul | etcd) and address 185 | // you can also specify a prefix all the keys will start with 186 | auto client = open("consul://127.0.0.1:8500", "/prefix"); 187 | 188 | 189 | // on failure methods throw exceptions (for more details see "liboffkv/client.hpp") 190 | try { 191 | int64_t initial_version = client->create("/key", "value"); 192 | std::cout << "Key \"/prefix/key\" was created successfully! " 193 | << "Its initial version is " << initial_version 194 | << std::endl; 195 | } catch (EntryExists&) { 196 | // other exception types can be found in liboffkv/errors.hpp 197 | std::cout << "Error: key \"/prefix/key\" already exists!" 198 | << std::endl; 199 | } 200 | 201 | 202 | // WATCH EXAMPLE 203 | auto result = client.exists("/key", true); 204 | 205 | // this thread erase the key after 10 seconds 206 | std::thread([&client]() mutable { 207 | std::this_thread::sleep_for(std::chrono::seconds(10)); 208 | client->erase("/key"); 209 | }).detach(); 210 | 211 | // now the key exists 212 | assert(result); 213 | 214 | // wait for changes 215 | result.watch->wait(); 216 | 217 | // if the waiting was completed, the existance state must be different 218 | assert(!client.exists("/key")); 219 | 220 | 221 | // TRANSACTION EXAMPLE 222 | // n.b. checks and other ops are separated from each other 223 | try { 224 | auto txn_result = client->commit( 225 | { 226 | // firstly list your checks 227 | { 228 | TxnCheck("/key", 42u), 229 | TxnCheck("/foo"), 230 | }, 231 | // then a chain of ops that are to be performed 232 | // in case all checks are satisfied 233 | { 234 | TxnErase("/key"), 235 | TxnSet("/foo", "new_value"), 236 | } 237 | } 238 | ); 239 | 240 | // only one set/create operation 241 | assert(txn_result.size() == 1 && 242 | txn_result[0].kind == TxnOpResult::Kind::SET); 243 | 244 | std::cout << "After the transaction the new version of \"/foo\" is " 245 | << txn_result[0].version << std::endl; 246 | } catch (TxnFailed& e) { 247 | // TxnFailed exception contains failed op index 248 | std::cout << "Transaction failed. Failed op index: " 249 | << e.failed_op() << std::endl; 250 | } 251 | } 252 | ``` 253 | 254 | ## Supported platforms 255 | 256 | The library is currently tested on 257 | 258 | - Ubuntu 18.04 259 | 260 | Full support. 261 | 262 | - MacOS 263 | 264 | Full support. 265 | 266 | - Windows 10 267 | 268 | Only Consul is supported. 269 | 270 | ## Dependencies 271 | 272 | - C++ compiler 273 | 274 | Currently tested compilers are 275 | 276 | - VS 2019 277 | - g++ 7.4.0 278 | - clang 279 | 280 | VS 2017 is known to fail. 281 | 282 | - [CMake](https://cmake.org) 283 | 284 | We suggest using cmake bundled with vcpkg. 285 | 286 | - [vcpkg](https://docs.microsoft.com/en-us/cpp/build/vcpkg) 287 | 288 | ## Developer workflow 289 | - Install dependencies 290 | 291 | ```sh 292 | # from vcpkg root 293 | vcpkg install ppconsul offscale-libetcd-cpp zkpp 294 | ``` 295 | 296 | Installing all three packages is not required. See control flags at the next step. 297 | 298 | - Build tests 299 | 300 | ```sh 301 | # from liboffkv directory 302 | mkdir cmake-build-debug && cd $_ 303 | cmake -DCMAKE_BUILD_TYPE=Debug \ 304 | -DCMAKE_TOOLCHAIN_FILE="" \ 305 | -DBUILD_TESTS=ON .. 306 | cmake --build . 307 | ``` 308 | 309 | You can control the set of supported services with the following flags 310 | 311 | - `-DENABLE_ZK=[ON|OFF]` 312 | - `-DENABLE_ETCD=[ON|OFF]` 313 | - `-DENABLE_CONSUL=[ON|OFF]` 314 | 315 | Sometimes you may also need to specify `VCPKG_TARGET_TRIPLET`. 316 | 317 | - Run tests 318 | 319 | ```sh 320 | # from liboffkv/cmake-build-debug directory 321 | make test 322 | ``` 323 | 324 | ## C interface 325 | We provide a pure C interface. It can be found in [liboffkv/clib.h](https://github.com/offscale/liboffkv/blob/master/liboffkv/clib.h). 326 | 327 | Set `-DBUILD_CLIB=ON` option to build the library. 328 | 329 | ## liboffkv is available in other languages!!! 330 | - Rust: [rsoffkv](https://github.com/offscale/rsoffkv) 331 | - Java: [liboffkv-java](https://github.com/offscale/liboffkv-java) 332 | - Go: [goffkv](https://github.com/offscale/goffkv), 333 | [goffkv-etcd](https://github.com/offscale/goffkv-etcd), 334 | [goffkv-zk](https://github.com/offscale/goffkv-zk), 335 | [goffkv-consul](https://github.com/offscale/goffkv-consul) 336 | 337 | ## License 338 | 339 | Licensed under any of: 340 | 341 | - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or ) 342 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or ) 343 | - CC0 license ([LICENSE-CC0](LICENSE-CC0) or ) 344 | 345 | at your option. 346 | 347 | ### Contribution 348 | 349 | Unless you explicitly state otherwise, any contribution intentionally submitted 350 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 351 | licensed as above, without any additional terms or conditions. 352 | -------------------------------------------------------------------------------- /liboffkv/zk_client.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | 9 | #include "client.hpp" 10 | #include "key.hpp" 11 | 12 | 13 | 14 | namespace liboffkv { 15 | 16 | class ZKClient : public Client { 17 | private: 18 | using buffer = zk::buffer; 19 | 20 | zk::client client_; 21 | 22 | static 23 | buffer from_string_(const std::string& str) 24 | { 25 | return {str.begin(), str.end()}; 26 | } 27 | 28 | static 29 | std::string to_string_(const buffer& buf) 30 | { 31 | return {buf.begin(), buf.end()}; 32 | } 33 | 34 | enum EraseQueryFailures { 35 | IGNORE_NO_ENTRY, 36 | THROW_NO_ENTRY, 37 | FAIL_TXN_ON_NO_ENTRY 38 | }; 39 | 40 | void make_recursive_erase_query_(zk::multi_op& query, const std::string& path, EraseQueryFailures no_entry_behavior) 41 | { 42 | zk::get_children_result::children_list_type children; 43 | bool entry_valid = true; 44 | try { 45 | children = client_.get_children(path).get().children(); 46 | } catch (zk::no_entry& err) { 47 | if (no_entry_behavior == THROW_NO_ENTRY) throw NoEntry{}; 48 | entry_valid = false; 49 | } 50 | 51 | if (entry_valid) { 52 | for (const auto& child : children) { 53 | make_recursive_erase_query_(query, path + '/' + child, IGNORE_NO_ENTRY); 54 | } 55 | } 56 | if (entry_valid || no_entry_behavior == FAIL_TXN_ON_NO_ENTRY) { 57 | query.push_back(zk::op::erase(path)); 58 | } 59 | } 60 | 61 | std::string as_path_string_(const Path &path) const 62 | { 63 | return static_cast(prefix_ / path); 64 | } 65 | 66 | [[noreturn]] static void rethrow_(zk::error& e) 67 | { 68 | switch (e.code()) { 69 | case zk::error_code::no_entry: 70 | throw NoEntry{}; 71 | case zk::error_code::entry_exists: 72 | throw EntryExists{}; 73 | case zk::error_code::connection_loss: 74 | throw ConnectionLoss{}; 75 | case zk::error_code::no_children_for_ephemerals: 76 | throw NoChildrenForEphemeral{}; 77 | case zk::error_code::version_mismatch: 78 | case zk::error_code::not_empty: 79 | default: 80 | throw ServiceError(e.what()); 81 | } 82 | } 83 | 84 | class ZKWatchHandle_ : public WatchHandle { 85 | private: 86 | std::future event_; 87 | 88 | public: 89 | ZKWatchHandle_(std::future&& event) 90 | : event_(std::move(event)) 91 | {} 92 | 93 | void wait() override 94 | { 95 | try { 96 | event_.get(); 97 | } catch (zk::error& e) { 98 | rethrow_(e); 99 | } 100 | } 101 | }; 102 | 103 | std::unique_ptr make_watch_handle_(std::future&& event) const 104 | { 105 | return std::make_unique(std::move(event)); 106 | } 107 | 108 | public: 109 | ZKClient(const std::string& address, Path prefix) try 110 | : Client(std::move(prefix)), client_(zk::client::connect(address).get()) 111 | { 112 | std::string entry; 113 | entry.reserve(prefix_.size()); 114 | for (const auto& segment : prefix_.segments()) { 115 | try { 116 | client_.create(entry.append("/").append(segment), buffer()).get(); 117 | } catch (zk::entry_exists&) { 118 | // do nothing 119 | } catch (zk::error& e) { 120 | rethrow_(e); 121 | } 122 | } 123 | } catch (zk::error& e) { 124 | rethrow_(e); 125 | } 126 | 127 | 128 | int64_t create(const Key& key, const std::string& value, bool lease = false) override 129 | { 130 | try { 131 | client_.create( 132 | as_path_string_(key), 133 | from_string_(value), 134 | !lease ? zk::create_mode::normal : zk::create_mode::ephemeral 135 | ).get(); 136 | } catch (zk::error& e) { 137 | rethrow_(e); 138 | } 139 | return 1; 140 | } 141 | 142 | 143 | ExistsResult exists(const Key& key, bool watch = false) override 144 | { 145 | std::unique_ptr watch_handle; 146 | std::optional stat; 147 | try { 148 | if (watch) { 149 | auto result = client_.watch_exists(as_path_string_(key)).get(); 150 | stat = std::move(result.initial().stat()); 151 | watch_handle = make_watch_handle_(std::move(result.next())); 152 | } else stat = client_.exists(as_path_string_(key)).get().stat(); 153 | } catch (zk::error& e) { 154 | rethrow_(e); 155 | } 156 | 157 | return { 158 | stat.has_value() ? static_cast(stat->data_version.value) + 1 : 0, 159 | std::move(watch_handle) 160 | }; 161 | } 162 | 163 | 164 | ChildrenResult get_children(const Key& key, bool watch) override 165 | { 166 | std::vector raw_children; 167 | std::unique_ptr watch_handle; 168 | try { 169 | if (watch) { 170 | auto result = client_.watch_children(as_path_string_(key)).get(); 171 | watch_handle = make_watch_handle_(std::move(result.next())); 172 | raw_children = std::move(result.initial().children()); 173 | } else { 174 | auto result = client_.get_children(as_path_string_(key)).get(); 175 | raw_children = std::move(result.children()); 176 | } 177 | } catch (zk::error& e) { 178 | rethrow_(e); 179 | } 180 | 181 | std::vector children; 182 | for (const auto& child : raw_children) { 183 | children.emplace_back(); 184 | children.back().reserve(key.size() + child.size() + 1); 185 | children.back().append(static_cast(key)).append("/").append(child); 186 | } 187 | return { std::move(children), std::move(watch_handle) }; 188 | } 189 | 190 | 191 | // No transactions. Atomicity is not necessary for linearizability here! 192 | // At least it seems to be so... 193 | // See also TLA+ spec: https://gist.github.com/raid-7/9ad7b88cd2ec2e83f56e3b69214b6762 194 | int64_t set(const Key& key, const std::string& value) override 195 | { 196 | auto path = as_path_string_(key); 197 | auto value_as_buffer = from_string_(value); 198 | 199 | try { 200 | client_.create(path, value_as_buffer).get(); 201 | return 1; 202 | } catch (zk::entry_exists&) { 203 | try { 204 | return static_cast(client_.set(path, value_as_buffer).get().stat().data_version.value) + 1; 205 | } catch (zk::no_entry&) { 206 | // concurrent remove happened, but set must not throw NoEntry 207 | // let's return some large number instead of real version 208 | return static_cast(1) << 62; 209 | } 210 | } catch (zk::error& e) { 211 | rethrow_(e); 212 | } 213 | } 214 | 215 | 216 | // Same as set: transactions aren't necessary 217 | CasResult cas(const Key& key, const std::string& value, int64_t version = 0) override 218 | { 219 | if (!version) { 220 | try { 221 | return {create(key, value)}; 222 | } catch (EntryExists&) { 223 | return {0}; 224 | } 225 | } 226 | 227 | try { 228 | return {static_cast( 229 | client_.set( 230 | as_path_string_(key), 231 | from_string_(value), 232 | zk::version(version - 1) 233 | ).get().stat().data_version.value) + 1}; 234 | } catch (zk::error& e) { 235 | switch (e.code()) { 236 | case zk::error_code::no_entry: 237 | throw NoEntry{}; 238 | case zk::error_code::version_mismatch: 239 | return {0}; 240 | default: 241 | rethrow_(e); 242 | } 243 | } 244 | } 245 | 246 | 247 | GetResult get(const Key& key, bool watch = false) override 248 | { 249 | std::optional result; 250 | std::unique_ptr watch_handle; 251 | 252 | try { 253 | if (watch) { 254 | auto watch_result = client_.watch(as_path_string_(key)).get(); 255 | result.emplace(std::move(watch_result.initial())); 256 | watch_handle = make_watch_handle_(std::move(watch_result.next())); 257 | } else result.emplace(client_.get(as_path_string_(key)).get()); 258 | } catch (zk::error& e) { 259 | rethrow_(e); 260 | } 261 | 262 | return { 263 | // zk::client::get returns future with zk::no_entry the key does not exist, so checking if result.stat() has value isn't needed 264 | static_cast(result->stat().data_version.value) + 1, 265 | to_string_(result->data()), 266 | std::move(watch_handle) 267 | }; 268 | } 269 | 270 | 271 | void erase(const Key& key, int64_t version = 0) override 272 | { 273 | auto path = as_path_string_(key); 274 | 275 | while (true) { 276 | zk::multi_op txn; 277 | txn.push_back(zk::op::check(path)); 278 | txn.push_back(zk::op::check(path, version ? zk::version(version - 1) : zk::version::any())); 279 | 280 | make_recursive_erase_query_(txn, path, THROW_NO_ENTRY); 281 | 282 | try { 283 | client_.commit(txn).get(); 284 | return; 285 | 286 | } catch (zk::transaction_failed& e) { 287 | // key does not exist 288 | if (e.failed_op_index() == 0) throw NoEntry{}; 289 | // version mismatch 290 | else if (e.failed_op_index() == 1) return; 291 | 292 | } catch (zk::error& e) { 293 | rethrow_(e); 294 | } 295 | } 296 | } 297 | 298 | 299 | TransactionResult commit(const Transaction& transaction) override 300 | { 301 | while (true) { 302 | std::vector boundaries; 303 | zk::multi_op txn; 304 | 305 | for (const auto& check : transaction.checks) { 306 | txn.push_back(zk::op::check(as_path_string_(check.key), 307 | check.version ? zk::version(check.version - 1) : zk::version::any())); 308 | boundaries.emplace_back(txn.size() - 1); 309 | } 310 | 311 | for (const auto& op : transaction.ops) { 312 | std::visit([&txn, this](auto&& arg) { 313 | using T = std::decay_t; 314 | if constexpr (std::is_same_v) { 315 | txn.push_back(zk::op::create( 316 | as_path_string_(arg.key), 317 | from_string_(arg.value), 318 | !arg.lease ? zk::create_mode::normal : zk::create_mode::ephemeral 319 | )); 320 | } else if constexpr (std::is_same_v) { 321 | txn.push_back(zk::op::set(as_path_string_(arg.key), 322 | from_string_(arg.value))); 323 | } else if constexpr (std::is_same_v) { 324 | make_recursive_erase_query_(txn, as_path_string_(arg.key), FAIL_TXN_ON_NO_ENTRY); 325 | } else static_assert(detail::always_false::value, "non-exhaustive visitor"); 326 | }, op); 327 | 328 | boundaries.push_back(txn.size() - 1); 329 | } 330 | 331 | std::optional raw_result; 332 | 333 | try { 334 | raw_result.emplace(client_.commit(txn).get()); 335 | } catch (zk::transaction_failed& e) { 336 | auto real_index = e.failed_op_index(); 337 | size_t user_index = std::distance(boundaries.begin(), 338 | std::lower_bound(boundaries.begin(), boundaries.end(), real_index)); 339 | 340 | // if the failed op is a part of a complex one, repeat 341 | if (boundaries[user_index] != real_index) continue; 342 | else throw TxnFailed{user_index}; 343 | } catch (zk::error& e) { 344 | rethrow_(e); 345 | } 346 | 347 | std::vector result; 348 | for (const auto& res : *raw_result) { 349 | switch (res.type()) { 350 | case zk::op_type::set: 351 | result.push_back(TxnOpResult{ 352 | TxnOpResult::Kind::SET, 353 | static_cast(res.as_set().stat().data_version.value) + 1 354 | }); 355 | break; 356 | case zk::op_type::create: 357 | result.push_back(TxnOpResult{ 358 | TxnOpResult::Kind::CREATE, 359 | 1 360 | }); 361 | break; 362 | case zk::op_type::check: 363 | case zk::op_type::erase: 364 | break; 365 | } 366 | } 367 | 368 | return result; 369 | } 370 | } 371 | }; 372 | 373 | } // namespace liboffkv 374 | -------------------------------------------------------------------------------- /tests/unit_tests.cpp: -------------------------------------------------------------------------------- 1 | #include "test_client_fixture.hpp" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | 9 | TEST_F(ClientFixture, key_validation_test) 10 | { 11 | const auto check_key = [](const std::string &path) { 12 | liboffkv::Key{path}; 13 | }; 14 | 15 | ASSERT_THROW(check_key(""), liboffkv::InvalidKey); 16 | ASSERT_THROW(check_key("/"), liboffkv::InvalidKey); 17 | ASSERT_THROW(check_key("mykey"), liboffkv::InvalidKey); 18 | 19 | ASSERT_NO_THROW(check_key("/mykey")); 20 | ASSERT_NO_THROW(check_key("/mykey/child")); 21 | 22 | ASSERT_THROW(check_key("/каша"), liboffkv::InvalidKey); 23 | ASSERT_THROW(check_key("/test\xFF"), liboffkv::InvalidKey); 24 | 25 | ASSERT_THROW(check_key(std::string("/test\0", 6)), liboffkv::InvalidKey); 26 | ASSERT_THROW(check_key(std::string("/test\x01", 6)), liboffkv::InvalidKey); 27 | ASSERT_THROW(check_key(std::string("/test\t", 6)), liboffkv::InvalidKey); 28 | ASSERT_THROW(check_key(std::string("/test\n", 6)), liboffkv::InvalidKey); 29 | ASSERT_THROW(check_key(std::string("/test\x1F", 6)), liboffkv::InvalidKey); 30 | ASSERT_THROW(check_key(std::string("/test\x7F", 6)), liboffkv::InvalidKey); 31 | 32 | ASSERT_THROW(check_key("/zookeeper"), liboffkv::InvalidKey); 33 | ASSERT_THROW(check_key("/zookeeper/child"), liboffkv::InvalidKey); 34 | ASSERT_THROW(check_key("/zookeeper/.."), liboffkv::InvalidKey); 35 | ASSERT_THROW(check_key("/one/two//three"), liboffkv::InvalidKey); 36 | ASSERT_THROW(check_key("/one/two/three/"), liboffkv::InvalidKey); 37 | ASSERT_THROW(check_key("/one/two/three/."), liboffkv::InvalidKey); 38 | ASSERT_THROW(check_key("/one/two/three/.."), liboffkv::InvalidKey); 39 | ASSERT_THROW(check_key("/one/./three"), liboffkv::InvalidKey); 40 | ASSERT_THROW(check_key("/one/../three"), liboffkv::InvalidKey); 41 | ASSERT_THROW(check_key("/one/zookeeper"), liboffkv::InvalidKey); 42 | 43 | ASSERT_NO_THROW(check_key("/.../.../zookeper")); 44 | } 45 | 46 | TEST_F(ClientFixture, create_large_test) 47 | { 48 | auto holder = hold_keys("/key"); 49 | 50 | std::string value(65'000, '\1'); 51 | 52 | ASSERT_NO_THROW(client->create("/key", value)); 53 | ASSERT_THROW(client->create("/key", value), liboffkv::EntryExists); 54 | 55 | ASSERT_THROW(client->create("/key/child/grandchild", value), liboffkv::NoEntry); 56 | ASSERT_NO_THROW(client->create("/key/child", value)); 57 | } 58 | 59 | TEST_F(ClientFixture, create_test) 60 | { 61 | auto holder = hold_keys("/key"); 62 | 63 | ASSERT_NO_THROW(client->create("/key", "value")); 64 | ASSERT_THROW(client->create("/key", "value"), liboffkv::EntryExists); 65 | 66 | ASSERT_THROW(client->create("/key/child/grandchild", "value"), liboffkv::NoEntry); 67 | ASSERT_NO_THROW(client->create("/key/child", "value")); 68 | } 69 | 70 | TEST_F(ClientFixture, exists_test) 71 | { 72 | auto holder = hold_keys("/key"); 73 | 74 | auto result = client->exists("/key"); 75 | ASSERT_FALSE(result); 76 | 77 | client->create("/key", "value"); 78 | 79 | result = client->exists("/key"); 80 | ASSERT_TRUE(result); 81 | } 82 | 83 | 84 | TEST_F(ClientFixture, erase_test) 85 | { 86 | auto holder = hold_keys("/key"); 87 | 88 | ASSERT_THROW(client->erase("/key"), liboffkv::NoEntry); 89 | 90 | client->create("/key", "value"); 91 | client->create("/key/child", "value"); 92 | 93 | ASSERT_NO_THROW(client->erase("/key")); 94 | ASSERT_FALSE(client->exists("/key")); 95 | ASSERT_FALSE(client->exists("/key/child")); 96 | } 97 | 98 | 99 | TEST_F(ClientFixture, versioned_erase_test) 100 | { 101 | auto holder = hold_keys("/key"); 102 | 103 | int64_t version = client->create("/key", "value"); 104 | client->create("/key/child", "value"); 105 | 106 | ASSERT_NO_THROW(client->erase("/key", version + 1)); 107 | ASSERT_TRUE(client->exists("/key")); 108 | ASSERT_TRUE(client->exists("/key/child")); 109 | 110 | ASSERT_NO_THROW(client->erase("/key", version)); 111 | ASSERT_FALSE(client->exists("/key")); 112 | ASSERT_FALSE(client->exists("/key/child")); 113 | } 114 | 115 | 116 | TEST_F(ClientFixture, exists_with_watch_test) 117 | { 118 | auto holder = hold_keys("/key"); 119 | 120 | client->create("/key", "value"); 121 | 122 | std::mutex my_lock; 123 | my_lock.lock(); 124 | 125 | auto thread = std::thread([&my_lock]() mutable { 126 | std::lock_guard lock_guard(my_lock); 127 | client->erase("/key"); 128 | }); 129 | 130 | auto result = client->exists("/key", true); 131 | my_lock.unlock(); 132 | 133 | ASSERT_TRUE(result); 134 | 135 | thread.join(); 136 | 137 | result.watch->wait(); 138 | ASSERT_FALSE(client->exists("/key")); 139 | } 140 | 141 | 142 | TEST_F(ClientFixture, create_with_lease_test) 143 | { 144 | auto holder = hold_keys("/key"); 145 | using namespace std::chrono_literals; 146 | 147 | { 148 | auto local_client = liboffkv::open(SERVICE_ADDRESS, "/unitTests"); 149 | ASSERT_NO_THROW(local_client->create("/key", "value", true)); 150 | 151 | std::this_thread::sleep_for(25s); 152 | 153 | ASSERT_TRUE(client->exists("/key")); 154 | } 155 | 156 | std::this_thread::sleep_for(25s); 157 | 158 | { 159 | auto local_client = liboffkv::open(SERVICE_ADDRESS, "/unitTests"); 160 | ASSERT_FALSE(client->exists("/key")); 161 | } 162 | } 163 | 164 | 165 | TEST_F(ClientFixture, get_test) 166 | { 167 | auto holder = hold_keys("/key"); 168 | 169 | ASSERT_THROW(client->get("/key"), liboffkv::NoEntry); 170 | 171 | int64_t initialVersion = client->create("/key", "value"); 172 | 173 | liboffkv::GetResult result; 174 | ASSERT_NO_THROW(result = client->get("/key")); 175 | 176 | ASSERT_EQ(result.value, "value"); 177 | ASSERT_EQ(result.version, initialVersion); 178 | } 179 | 180 | 181 | TEST_F(ClientFixture, set_test) 182 | { 183 | auto holder = hold_keys("/key"); 184 | 185 | int64_t initialVersion = client->create("/key", "value"); 186 | int64_t version = client->set("/key", "newValue"); 187 | 188 | client->set("/another", "anything"); 189 | 190 | auto result = client->get("/key"); 191 | 192 | ASSERT_GT(version, initialVersion); 193 | ASSERT_EQ(result.value, "newValue"); 194 | ASSERT_EQ(result.version, version); 195 | 196 | ASSERT_THROW(client->set("/key/child/grandchild", "value"), liboffkv::NoEntry); 197 | ASSERT_NO_THROW(client->set("/key/child", "value")); 198 | 199 | ASSERT_EQ(client->get("/key/child").value, "value"); 200 | } 201 | 202 | 203 | TEST_F(ClientFixture, get_with_watch_test) 204 | { 205 | auto holder = hold_keys("/key"); 206 | 207 | client->create("/key", "value"); 208 | 209 | std::mutex my_lock; 210 | my_lock.lock(); 211 | 212 | auto thread = std::thread([&my_lock]() mutable { 213 | std::lock_guard lock_guard(my_lock); 214 | client->set("/key", "newValue"); 215 | }); 216 | 217 | auto result = client->get("/key", true); 218 | my_lock.unlock(); 219 | 220 | ASSERT_EQ(result.value, "value"); 221 | 222 | result.watch->wait(); 223 | 224 | thread.join(); 225 | 226 | ASSERT_EQ(client->get("/key").value, "newValue"); 227 | } 228 | 229 | 230 | TEST_F(ClientFixture, cas_test) 231 | { 232 | auto holder = hold_keys("/key"); 233 | 234 | ASSERT_THROW(client->cas("/key", "value", 42), liboffkv::NoEntry); 235 | 236 | auto version = client->create("/key", "value"); 237 | 238 | liboffkv::CasResult cas_result; 239 | liboffkv::GetResult get_result; 240 | 241 | ASSERT_NO_THROW(cas_result = client->cas("/key", "new_value", version + 1)); 242 | 243 | ASSERT_FALSE(cas_result); 244 | 245 | get_result = client->get("/key"); 246 | ASSERT_EQ(get_result.version, version); 247 | ASSERT_EQ(get_result.value, "value"); 248 | 249 | ASSERT_NO_THROW(cas_result = client->cas("/key", "new_value", version)); 250 | 251 | ASSERT_TRUE(cas_result); 252 | 253 | get_result = client->get("/key"); 254 | ASSERT_EQ(get_result.version, cas_result.version); 255 | ASSERT_GT(cas_result.version, version); 256 | ASSERT_EQ(get_result.value, "new_value"); 257 | } 258 | 259 | 260 | TEST_F(ClientFixture, cas_zero_version_test) 261 | { 262 | auto holder = hold_keys("/key"); 263 | 264 | liboffkv::CasResult cas_result; 265 | liboffkv::GetResult get_result; 266 | 267 | ASSERT_NO_THROW(cas_result = client->cas("/key", "value")); 268 | 269 | ASSERT_TRUE(cas_result); 270 | 271 | ASSERT_NO_THROW(get_result = client->get("/key")); 272 | ASSERT_EQ(get_result.value, "value"); 273 | ASSERT_EQ(get_result.version, cas_result.version); 274 | int64_t version = cas_result.version; 275 | 276 | ASSERT_NO_THROW(cas_result = client->cas("/key", "new_value")); 277 | 278 | ASSERT_FALSE(cas_result); 279 | 280 | ASSERT_NO_THROW(get_result = client->get("/key")); 281 | ASSERT_EQ(get_result.value, "value"); 282 | ASSERT_EQ(get_result.version, version); 283 | } 284 | 285 | 286 | TEST_F(ClientFixture, get_children_test) 287 | { 288 | auto holder = hold_keys("/key"); 289 | 290 | ASSERT_THROW(client->get_children("/key"), liboffkv::NoEntry); 291 | 292 | client->create("/key", "/value"); 293 | client->create("/key/child", "/value"); 294 | client->create("/key/child/grandchild", "/value"); 295 | client->create("/key/hackerivan", "/value"); 296 | 297 | liboffkv::ChildrenResult result; 298 | 299 | ASSERT_NO_THROW(result = client->get_children("/key")); 300 | 301 | ASSERT_TRUE(liboffkv::detail::equal_as_unordered( 302 | result.children, 303 | {"/key/child", "/key/hackerivan"} 304 | )); 305 | 306 | ASSERT_NO_THROW(result = client->get_children("/key/child")); 307 | ASSERT_TRUE(liboffkv::detail::equal_as_unordered( 308 | result.children, 309 | {"/key/child/grandchild"} 310 | )); 311 | } 312 | 313 | 314 | TEST_F(ClientFixture, get_children_with_watch_test) 315 | { 316 | auto holder = hold_keys("/key"); 317 | 318 | client->create("/key", "value"); 319 | client->create("/key/child", "value"); 320 | client->create("/key/child/grandchild", "value"); 321 | client->create("/key/dimak24", "value"); 322 | 323 | std::mutex my_lock; 324 | my_lock.lock(); 325 | 326 | auto thread = std::thread([&my_lock]() mutable { 327 | std::lock_guard lock_guard(my_lock); 328 | client->erase("/key/dimak24"); 329 | }); 330 | 331 | auto result = client->get_children("/key", true); 332 | my_lock.unlock(); 333 | 334 | ASSERT_TRUE(liboffkv::detail::equal_as_unordered( 335 | result.children, 336 | {"/key/child", "/key/dimak24"} 337 | )); 338 | 339 | result.watch->wait(); 340 | 341 | thread.join(); 342 | 343 | ASSERT_TRUE(liboffkv::detail::equal_as_unordered( 344 | client->get_children("/key").children, 345 | {"/key/child"} 346 | )); 347 | } 348 | 349 | 350 | TEST_F(ClientFixture, commit_test) 351 | { 352 | auto holder = hold_keys("/key", "/foo"); 353 | 354 | auto key_version = client->create("/key", "value"); 355 | auto foo_version = client->create("/foo", "value"); 356 | auto bar_version = client->create("/foo/bar", "value"); 357 | client->create("/foo/bar/subbar", "value"); 358 | 359 | // check fails 360 | try { 361 | client->commit( 362 | { 363 | { 364 | liboffkv::TxnCheck("/key", key_version), 365 | liboffkv::TxnCheck("/foo", foo_version + 1), 366 | liboffkv::TxnCheck("/foo/bar", bar_version), 367 | }, 368 | { 369 | liboffkv::TxnOpCreate("/key/child", "value"), 370 | liboffkv::TxnOpSet("/key", "new_value"), 371 | liboffkv::TxnOpErase("/foo"), 372 | } 373 | } 374 | ); 375 | FAIL() << "Expected commit to throw TxnFailed, but it threw nothing"; 376 | } catch (liboffkv::TxnFailed &e) { 377 | ASSERT_EQ(e.failed_op(), 1); 378 | } catch (std::exception &e) { 379 | FAIL() << "Expected commit to throw TxnFailed, " 380 | "but it threw different exception:\n" << e.what(); 381 | } 382 | 383 | ASSERT_FALSE(client->exists("/key/child")); 384 | ASSERT_EQ(client->get("/key").value, "value"); 385 | ASSERT_TRUE(client->exists("/foo")); 386 | 387 | // op fails 388 | try { 389 | client->commit( 390 | { 391 | { 392 | liboffkv::TxnCheck("/key", key_version), 393 | liboffkv::TxnCheck("/foo", foo_version), 394 | liboffkv::TxnCheck("/foo/bar", bar_version), 395 | }, 396 | { 397 | liboffkv::TxnOpCreate("/key/child", "value"), 398 | liboffkv::TxnOpCreate("/key/hackerivan", "new_value"), 399 | liboffkv::TxnOpErase("/foo"), 400 | // this fails because /key/child/grandchild does not exist 401 | liboffkv::TxnOpSet("/key/child/grandchild", "new_value"), 402 | liboffkv::TxnOpErase("/asfdsfasdfa"), 403 | } 404 | } 405 | ); 406 | FAIL() << "Expected commit to throw TxnFailed, but it threw nothing"; 407 | } catch (liboffkv::TxnFailed &e) { 408 | ASSERT_EQ(e.failed_op(), 6); 409 | } catch (std::exception &e) { 410 | FAIL() << "Expected commit to throw TxnFailed, but it threw different exception:\n" << e.what(); 411 | } 412 | 413 | ASSERT_FALSE(client->exists("/key/child")); 414 | ASSERT_TRUE(client->exists("/foo")); 415 | 416 | // everything is OK 417 | liboffkv::TransactionResult result; 418 | 419 | ASSERT_NO_THROW(result = client->commit( 420 | { 421 | { 422 | liboffkv::TxnCheck("/key", key_version), 423 | liboffkv::TxnCheck("/foo", foo_version), 424 | liboffkv::TxnCheck("/foo/bar", bar_version), 425 | }, 426 | { 427 | liboffkv::TxnOpCreate("/key/child", "value"), 428 | liboffkv::TxnOpSet("/key", "new_value"), 429 | liboffkv::TxnOpErase("/foo"), 430 | } 431 | } 432 | )); 433 | 434 | ASSERT_TRUE(client->exists("/key/child")); 435 | ASSERT_EQ(client->get("/key").value, "new_value"); 436 | ASSERT_FALSE(client->exists("/foo")); 437 | ASSERT_FALSE(client->exists("/foo/bar")); 438 | ASSERT_FALSE(client->exists("/foo/bar/subbar")); 439 | 440 | ASSERT_GT(result.at(1).version, key_version); 441 | } 442 | 443 | TEST_F(ClientFixture, erase_prefix_test) 444 | { 445 | auto holder = hold_keys("/ichi", "/ichinichi"); 446 | 447 | ASSERT_NO_THROW(client->create("/ichi", "one")); 448 | ASSERT_NO_THROW(client->create("/ichinichi", "two")); 449 | 450 | ASSERT_NO_THROW(client->erase("/ichi")); 451 | 452 | ASSERT_TRUE(client->exists("/ichinichi")); 453 | } 454 | 455 | TEST_F(ClientFixture, get_prefix_test) 456 | { 457 | auto holder = hold_keys("/sore", "/sorewanan"); 458 | 459 | ASSERT_NO_THROW(client->create("/sore", "1")); 460 | ASSERT_NO_THROW(client->create("/sore/ga", "2")); 461 | ASSERT_NO_THROW(client->create("/sorewanan", "3")); 462 | 463 | liboffkv::ChildrenResult result; 464 | ASSERT_NO_THROW(result = client->get_children("/sore")); 465 | 466 | ASSERT_TRUE(liboffkv::detail::equal_as_unordered( 467 | client->get_children("/sore").children, 468 | {"/sore/ga"} 469 | )); 470 | } 471 | -------------------------------------------------------------------------------- /liboffkv/consul_client.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include "client.hpp" 15 | #include "key.hpp" 16 | #include "util.hpp" 17 | #include "ping_sender.hpp" 18 | 19 | namespace liboffkv { 20 | 21 | class ConsulClient : public Client 22 | { 23 | private: 24 | static constexpr auto WATCH_TIMEOUT = std::chrono::seconds(120); 25 | static constexpr auto TTL = std::chrono::seconds(10); 26 | static constexpr auto CONSISTENCY = ppconsul::Consistency::Consistent; 27 | 28 | ppconsul::Consul client_; 29 | ppconsul::kv::Kv kv_; 30 | std::string address_; 31 | std::string session_id_; 32 | detail::PingSender ping_sender_; 33 | 34 | [[noreturn]] static void rethrow_(const ppconsul::Error &e) 35 | { 36 | if (dynamic_cast(&e)) 37 | throw ServiceError(e.what()); 38 | throw e; 39 | } 40 | 41 | std::string as_path_string_(const Path &path) const 42 | { 43 | std::string result; 44 | for (const auto &segment : (prefix_ / path).segments()) { 45 | if (!result.empty()) 46 | result += '/'; 47 | result += segment; 48 | } 49 | return result; 50 | } 51 | 52 | class ConsulWatchHandle_ : public WatchHandle 53 | { 54 | ppconsul::Consul client_; 55 | ppconsul::kv::Kv kv_; 56 | std::string key_; 57 | uint64_t old_version_; 58 | bool all_with_prefix_; 59 | 60 | public: 61 | ConsulWatchHandle_( 62 | const std::string &address, 63 | std::string key, 64 | uint64_t old_version, 65 | bool all_with_prefix) 66 | : client_(address) 67 | , kv_(client_, ppconsul::kw::consistency = CONSISTENCY) 68 | , key_(std::move(key)) 69 | , old_version_{old_version} 70 | , all_with_prefix_{all_with_prefix} 71 | {} 72 | 73 | void wait() override 74 | { 75 | try { 76 | if (all_with_prefix_) 77 | kv_.keys(key_, ppconsul::kv::kw::block_for = {WATCH_TIMEOUT, old_version_}); 78 | else 79 | kv_.item(key_, ppconsul::kv::kw::block_for = {WATCH_TIMEOUT, old_version_}); 80 | } catch (const ppconsul::Error &e) { 81 | rethrow_(e); 82 | } 83 | } 84 | }; 85 | 86 | std::unique_ptr make_watch_handle_( 87 | const std::string &key, 88 | uint64_t old_version, 89 | bool all_with_prefix = false) const 90 | { 91 | return std::make_unique(address_, key, old_version, all_with_prefix); 92 | } 93 | 94 | void create_session_if_needed_() 95 | { 96 | if (!session_id_.empty()) 97 | return; 98 | auto client = std::make_unique(address_); 99 | auto sessions = std::make_unique(*client); 100 | 101 | session_id_ = sessions->create( 102 | ppconsul::sessions::kw::lock_delay = std::chrono::seconds{0}, 103 | ppconsul::sessions::kw::behavior = ppconsul::sessions::InvalidationBehavior::Delete, 104 | ppconsul::sessions::kw::ttl = TTL); 105 | 106 | ping_sender_ = detail::PingSender( 107 | (TTL + std::chrono::seconds(1)) / 2, 108 | [client = std::move(client), sessions = std::move(sessions), id = session_id_]() 109 | { 110 | try { 111 | sessions->renew(id); 112 | return (TTL + std::chrono::seconds(1)) / 2; 113 | } catch (... /* BadStatus& ? */) { 114 | return std::chrono::seconds::zero(); 115 | } 116 | } 117 | ); 118 | } 119 | 120 | public: 121 | ConsulClient(const std::string &address, Path prefix) 122 | : Client(std::move(prefix)) 123 | , client_(address) 124 | , kv_(client_, ppconsul::kw::consistency = CONSISTENCY) 125 | , address_{address} 126 | , session_id_{} 127 | , ping_sender_{} 128 | {} 129 | 130 | int64_t create(const Key &key, const std::string &value, bool lease = false) override 131 | { 132 | std::vector txn; 133 | 134 | const Path parent = key.parent(); 135 | const bool has_parent = !parent.root(); 136 | 137 | if (has_parent) 138 | txn.push_back(ppconsul::kv::txn_ops::Get{as_path_string_(parent)}); 139 | 140 | const std::string key_string = as_path_string_(key); 141 | txn.push_back(ppconsul::kv::txn_ops::CheckNotExists{key_string}); 142 | 143 | if (lease) { 144 | create_session_if_needed_(); 145 | txn.push_back(ppconsul::kv::txn_ops::Lock{key_string, value, session_id_}); 146 | } else { 147 | txn.push_back(ppconsul::kv::txn_ops::Set{key_string, value}); 148 | } 149 | 150 | try { 151 | return kv_.commit(txn).back().modifyIndex; 152 | 153 | } catch (const ppconsul::kv::TxnAborted &e) { 154 | const auto op_index = e.errors().front().opIndex; 155 | if (has_parent && op_index == 0) 156 | throw NoEntry{}; 157 | if (op_index == (has_parent ? 1 : 0)) 158 | throw EntryExists{}; 159 | throw; 160 | 161 | } catch (const ppconsul::Error &e) { 162 | rethrow_(e); 163 | } 164 | } 165 | 166 | ExistsResult exists(const Key &key, bool watch = false) override 167 | { 168 | const std::string key_string = as_path_string_(key); 169 | try { 170 | auto item = kv_.item(ppconsul::withHeaders, key_string); 171 | std::unique_ptr watch_handle; 172 | const int64_t version = 173 | item.data().valid() ? static_cast(item.headers().index()) : 0; 174 | if (watch) 175 | watch_handle = make_watch_handle_(key_string, version); 176 | return {version, std::move(watch_handle)}; 177 | 178 | } catch (const ppconsul::Error &e) { 179 | rethrow_(e); 180 | } 181 | } 182 | 183 | ChildrenResult get_children(const Key &key, bool watch = false) override 184 | { 185 | const std::string key_string = as_path_string_(key); 186 | const std::string child_prefix = key_string + "/"; 187 | try { 188 | auto result = kv_.commit({ 189 | ppconsul::kv::txn_ops::GetAll{child_prefix}, 190 | ppconsul::kv::txn_ops::Get{key_string}, 191 | }); 192 | 193 | std::vector children; 194 | uint64_t max_modify_index = result.back().modifyIndex; 195 | const auto nchild_prefix = child_prefix.size(); 196 | const auto nglobal_prefix = as_path_string_(Path{""}).size(); 197 | for (auto it = result.begin(), end = --result.end(); it != end; ++it) { 198 | max_modify_index = std::max(max_modify_index, it->modifyIndex); 199 | if (it->key.find('/', nchild_prefix) == std::string::npos) { 200 | if (nglobal_prefix) 201 | children.emplace_back(it->key.substr(nglobal_prefix)); 202 | else 203 | children.emplace_back("/" + it->key); 204 | } 205 | } 206 | 207 | std::unique_ptr watch_handle; 208 | if (watch) 209 | watch_handle = make_watch_handle_(child_prefix, max_modify_index, true); 210 | 211 | return {std::move(children), std::move(watch_handle)}; 212 | 213 | } catch (const ppconsul::kv::TxnAborted &) { 214 | throw NoEntry{}; 215 | 216 | } catch (const ppconsul::Error &e) { 217 | rethrow_(e); 218 | } 219 | } 220 | 221 | int64_t set(const Key &key, const std::string &value) override 222 | { 223 | const Path parent = key.parent(); 224 | 225 | std::vector txn; 226 | if (!parent.root()) 227 | txn.push_back(ppconsul::kv::txn_ops::Get{as_path_string_(parent)}); 228 | txn.push_back(ppconsul::kv::txn_ops::Set{as_path_string_(key), value}); 229 | 230 | try { 231 | auto result = kv_.commit(txn); 232 | return result.back().modifyIndex; 233 | 234 | } catch (const ppconsul::kv::TxnAborted &) { 235 | throw NoEntry{}; 236 | 237 | } catch (const ppconsul::Error &e) { 238 | rethrow_(e); 239 | } 240 | } 241 | 242 | GetResult get(const Key &key, bool watch = false) override 243 | { 244 | const std::string key_string = as_path_string_(key); 245 | 246 | try { 247 | auto item = kv_.item(ppconsul::withHeaders, key_string); 248 | if (!item.data().valid()) 249 | throw NoEntry{}; 250 | const int64_t version = static_cast(item.headers().index()); 251 | 252 | std::unique_ptr watch_handle; 253 | if (watch) 254 | watch_handle = make_watch_handle_(key_string, version); 255 | 256 | return {version, item.data().value, std::move(watch_handle)}; 257 | 258 | } catch (const ppconsul::Error &e) { 259 | rethrow_(e); 260 | } 261 | } 262 | 263 | CasResult cas(const Key &key, const std::string &value, int64_t version = 0) override 264 | { 265 | if (!version) 266 | try { 267 | return {create(key, value)}; 268 | } catch (const EntryExists &) { 269 | return {0}; 270 | } 271 | 272 | const std::string key_string = as_path_string_(key); 273 | try { 274 | auto result = kv_.commit({ 275 | ppconsul::kv::txn_ops::Get{key_string}, 276 | ppconsul::kv::txn_ops::CompareSet{key_string, static_cast(version), value}, 277 | }); 278 | return {static_cast(result.back().modifyIndex)}; 279 | 280 | } catch (const ppconsul::kv::TxnAborted &e) { 281 | const auto op_index = e.errors().front().opIndex; 282 | if (op_index == 0) 283 | throw NoEntry{}; 284 | return {0}; 285 | 286 | } catch (const ppconsul::Error &e) { 287 | rethrow_(e); 288 | } 289 | } 290 | 291 | void erase(const Key &key, int64_t version = 0) override 292 | { 293 | const std::string key_string = as_path_string_(key); 294 | 295 | std::vector txn{ 296 | ppconsul::kv::txn_ops::Get{key_string} 297 | }; 298 | 299 | if (version) { 300 | txn.push_back(ppconsul::kv::txn_ops::CompareErase{ 301 | key_string, 302 | static_cast(version), 303 | }); 304 | } else { 305 | txn.push_back(ppconsul::kv::txn_ops::Erase{key_string}); 306 | } 307 | txn.push_back(ppconsul::kv::txn_ops::EraseAll{key_string + "/"}); 308 | 309 | try { 310 | kv_.commit(txn); 311 | 312 | } catch (const ppconsul::kv::TxnAborted &e) { 313 | const auto op_index = e.errors().front().opIndex; 314 | if (op_index == 0) 315 | throw NoEntry{}; 316 | // else we don't need to throw anything 317 | 318 | } catch (const ppconsul::Error &e) { 319 | rethrow_(e); 320 | } 321 | } 322 | 323 | TransactionResult commit(const Transaction& transaction) override 324 | { 325 | enum class ResultKind 326 | { 327 | CREATE, 328 | SET, 329 | AUX, 330 | }; 331 | 332 | std::vector txn; 333 | std::vector boundaries; 334 | std::vector result_kinds; 335 | 336 | for (const auto &check : transaction.checks) { 337 | if (check.version) { 338 | txn.push_back(ppconsul::kv::txn_ops::CheckIndex{ 339 | as_path_string_(check.key), 340 | static_cast(check.version) 341 | }); 342 | } else { 343 | txn.push_back(ppconsul::kv::txn_ops::Get{ 344 | as_path_string_(check.key) 345 | }); 346 | } 347 | result_kinds.push_back(ResultKind::AUX); 348 | boundaries.push_back(txn.size() - 1); 349 | } 350 | 351 | for (const auto &op : transaction.ops) { 352 | std::visit([this, &txn, &result_kinds](auto &&arg) { 353 | using T = std::decay_t; 354 | const std::string key_string = as_path_string_(arg.key); 355 | 356 | if constexpr (std::is_same_v) { 357 | 358 | const Path parent = arg.key.parent(); 359 | if (!parent.root()) { 360 | txn.push_back(ppconsul::kv::txn_ops::Get{as_path_string_(parent)}); 361 | result_kinds.push_back(ResultKind::AUX); 362 | } 363 | 364 | txn.push_back(ppconsul::kv::txn_ops::CheckNotExists{key_string}); 365 | // CheckNotExists does not produce any results 366 | 367 | if (arg.lease) { 368 | create_session_if_needed_(); 369 | txn.push_back(ppconsul::kv::txn_ops::Lock{ 370 | key_string, 371 | arg.value, 372 | session_id_, 373 | }); 374 | } else { 375 | txn.push_back(ppconsul::kv::txn_ops::Set{key_string, arg.value}); 376 | } 377 | result_kinds.push_back(ResultKind::CREATE); 378 | 379 | } else if constexpr (std::is_same_v) { 380 | 381 | txn.push_back(ppconsul::kv::txn_ops::Get{key_string}); 382 | result_kinds.push_back(ResultKind::AUX); 383 | 384 | txn.push_back(ppconsul::kv::txn_ops::Set{key_string, arg.value}); 385 | result_kinds.push_back(ResultKind::SET); 386 | 387 | } else if constexpr (std::is_same_v) { 388 | 389 | txn.push_back(ppconsul::kv::txn_ops::Get{key_string}); 390 | result_kinds.push_back(ResultKind::AUX); 391 | 392 | txn.push_back(ppconsul::kv::txn_ops::Erase{key_string}); 393 | // Erase does not produce any results 394 | 395 | txn.push_back(ppconsul::kv::txn_ops::EraseAll{key_string + "/"}); 396 | // EraseAll does not produce any results 397 | 398 | } else static_assert(detail::always_false::value, "non-exhaustive visitor"); 399 | }, op); 400 | 401 | boundaries.push_back(txn.size() - 1); 402 | } 403 | 404 | try { 405 | auto results = kv_.commit(txn); 406 | std::vector answer; 407 | for (size_t i = 0; i < results.size(); ++i) { 408 | switch (result_kinds[i]) { 409 | case ResultKind::CREATE: 410 | answer.push_back(TxnOpResult{ 411 | TxnOpResult::Kind::CREATE, 412 | static_cast(results[i].modifyIndex) 413 | }); 414 | break; 415 | case ResultKind::SET: 416 | answer.push_back(TxnOpResult{ 417 | TxnOpResult::Kind::SET, 418 | static_cast(results[i].modifyIndex) 419 | }); 420 | break; 421 | case ResultKind::AUX: 422 | break; 423 | } 424 | } 425 | return answer; 426 | 427 | } catch (const ppconsul::kv::TxnAborted &e) { 428 | const auto op_index = e.errors().front().opIndex; 429 | const size_t user_op_index = 430 | std::lower_bound(boundaries.begin(), boundaries.end(), op_index) 431 | - boundaries.begin(); 432 | throw TxnFailed{user_op_index}; 433 | 434 | } catch (const ppconsul::Error &e) { 435 | rethrow_(e); 436 | } 437 | } 438 | }; 439 | 440 | } // namespace liboffkv 441 | -------------------------------------------------------------------------------- /liboffkv/etcd_client.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | 12 | 13 | #include "key.hpp" 14 | #include "ping_sender.hpp" 15 | 16 | 17 | namespace liboffkv { 18 | 19 | namespace detail { 20 | 21 | void ensure_succeeded_(grpc::Status status) 22 | { 23 | if (status.ok()) return; 24 | 25 | if (status.error_code() == grpc::StatusCode::UNAVAILABLE) { 26 | throw ConnectionLoss{}; 27 | } 28 | throw ServiceError{status.error_message()}; 29 | } 30 | 31 | } // namespace detail 32 | 33 | 34 | class ETCDWatchCreator { 35 | public: 36 | using WatchCreateRequest = etcdserverpb::WatchCreateRequest; 37 | using WatchResponse = etcdserverpb::WatchResponse; 38 | using Event = mvccpb::Event; 39 | using EventType = mvccpb::Event_EventType; 40 | 41 | struct WatchEventHandler { 42 | std::function process_event; 43 | std::function process_failure; 44 | }; 45 | 46 | private: 47 | static inline void* const tag_init_stream = reinterpret_cast(1); 48 | static inline void* const tag_write_finished = reinterpret_cast(2); 49 | static inline void* const tag_response_got = reinterpret_cast(3); 50 | 51 | using WatchRequest = etcdserverpb::WatchRequest; 52 | using WatchCancelRequest = etcdserverpb::WatchCancelRequest; 53 | using WatchEndpoint = etcdserverpb::Watch; 54 | 55 | std::mutex lock_; 56 | 57 | std::unique_ptr watch_stub_; 58 | grpc::ClientContext watch_context_; 59 | std::shared_ptr> watch_stream_; 60 | std::unique_ptr pending_watch_response_; 61 | std::map watch_handlers_; 62 | 63 | grpc::CompletionQueue cq_; 64 | 65 | bool watch_resolution_thread_running_ = false; 66 | std::thread watch_resolution_thread_; 67 | 68 | std::unique_ptr> current_watch_write_; 69 | std::unique_ptr pending_watch_handler_; 70 | 71 | std::condition_variable write_wait_cv_; 72 | std::condition_variable create_watch_wait_cv_; 73 | 74 | 75 | void fail_all_watches(const ServiceError& exc) 76 | { 77 | std::lock_guard lock(lock_); 78 | 79 | watch_stream_ = nullptr; 80 | pending_watch_response_ = nullptr; 81 | 82 | for (const auto& [_, watch_handler] : watch_handlers_) (void)_, watch_handler.process_failure(exc); 83 | 84 | watch_handlers_.clear(); 85 | 86 | if (current_watch_write_) { 87 | current_watch_write_->set_exception(std::make_exception_ptr(exc)); 88 | current_watch_write_ = nullptr; 89 | } 90 | 91 | if (pending_watch_handler_) { 92 | pending_watch_handler_->process_failure(exc); 93 | pending_watch_handler_ = nullptr; 94 | } 95 | 96 | write_wait_cv_.notify_all(); 97 | create_watch_wait_cv_.notify_all(); 98 | } 99 | 100 | void setup_watch_infrastructure_m() 101 | { 102 | if (watch_stream_) return; 103 | if (current_watch_write_) throw std::logic_error("Inconsistent internal state"); 104 | 105 | // just to slow down next write until stream init 106 | current_watch_write_ = std::make_unique>(); 107 | watch_stream_ = watch_stub_->AsyncWatch(&watch_context_, &cq_, tag_init_stream); 108 | 109 | if (!watch_resolution_thread_running_) { 110 | watch_resolution_thread_running_ = true; 111 | watch_resolution_thread_ = std::thread([this] { this->watch_resolution_loop_(); }); 112 | } 113 | } 114 | 115 | void resolve_write_m() 116 | { 117 | current_watch_write_->set_value(); 118 | current_watch_write_ = nullptr; 119 | write_wait_cv_.notify_one(); 120 | } 121 | 122 | void watch_resolution_loop_() 123 | { 124 | bool succeeded; 125 | void* tag; 126 | while (cq_.Next(&tag, &succeeded)) { 127 | if (!succeeded) { 128 | fail_all_watches(ServiceError{"Watch initialization failure"}); 129 | continue; 130 | } 131 | 132 | std::unique_lock lock(lock_); 133 | 134 | if (tag == tag_init_stream) { 135 | // stream initialized 136 | resolve_write_m(); 137 | request_read_next_watch_response_m(); 138 | continue; 139 | } 140 | 141 | if (tag == tag_write_finished) { 142 | // resolve write and continue 143 | resolve_write_m(); 144 | continue; 145 | } 146 | 147 | if (tag == tag_response_got) { 148 | // resolve response 149 | if (auto* response = pending_watch_response_.release()) { 150 | if (response->created()) { 151 | watch_handlers_[response->watch_id()] = *pending_watch_handler_; 152 | pending_watch_handler_ = nullptr; 153 | create_watch_wait_cv_.notify_one(); 154 | } 155 | 156 | if (!(response->created() || response->canceled()) && process_watch_response_m(*response)) { 157 | cancel_watch_m(response->watch_id(), lock); 158 | } 159 | 160 | delete response; 161 | } 162 | 163 | request_read_next_watch_response_m(); 164 | } 165 | } 166 | } 167 | 168 | void cancel_watch_m(int64_t watch_id, std::unique_lock& lock) 169 | { 170 | auto* cancel_req = new WatchCancelRequest(); 171 | cancel_req->set_watch_id(watch_id); 172 | WatchRequest req; 173 | req.set_allocated_cancel_request(cancel_req); 174 | write_to_watch_stream_m(req, lock); 175 | } 176 | 177 | bool process_watch_response_m(const WatchResponse& response) 178 | { 179 | if (!watch_handlers_.count(response.watch_id())) return true; 180 | WatchEventHandler& handler = watch_handlers_[response.watch_id()]; 181 | 182 | for (const Event& event : response.events()) { 183 | if (handler.process_event(event)) { 184 | watch_handlers_.erase(response.watch_id()); 185 | return true; 186 | } 187 | } 188 | return false; 189 | } 190 | 191 | void request_read_next_watch_response_m() 192 | { 193 | if (pending_watch_response_) throw std::logic_error("Inconsistent internal state"); 194 | 195 | pending_watch_response_ = std::make_unique(); 196 | watch_stream_->Read(pending_watch_response_.get(), tag_response_got); 197 | } 198 | 199 | std::future write_to_watch_stream_m(const WatchRequest& request, std::unique_lock& lock) 200 | { 201 | setup_watch_infrastructure_m(); 202 | 203 | while (current_watch_write_) write_wait_cv_.wait(lock); 204 | 205 | current_watch_write_ = std::make_unique>(); 206 | watch_stream_->Write(request, tag_write_finished); 207 | 208 | return current_watch_write_->get_future(); 209 | } 210 | 211 | 212 | public: 213 | ETCDWatchCreator(const std::shared_ptr& channel) 214 | : watch_stub_(WatchEndpoint::NewStub(channel)) 215 | {} 216 | 217 | void create_watch(const WatchCreateRequest& create_req, const WatchEventHandler& handler) 218 | { 219 | WatchRequest request; 220 | request.set_allocated_create_request(new WatchCreateRequest(create_req)); 221 | 222 | std::future watch_write_future; 223 | { 224 | std::unique_lock lock(lock_); 225 | while (pending_watch_handler_) create_watch_wait_cv_.wait(lock); 226 | 227 | pending_watch_handler_ = std::make_unique(handler); 228 | watch_write_future = write_to_watch_stream_m(request, lock); 229 | } 230 | 231 | watch_write_future.get(); 232 | } 233 | 234 | ~ETCDWatchCreator() 235 | { 236 | bool do_join; 237 | { 238 | std::unique_lock lock(lock_); 239 | if (watch_stream_) watch_context_.TryCancel(); 240 | cq_.Shutdown(); 241 | do_join = watch_resolution_thread_running_; 242 | } 243 | if (do_join) watch_resolution_thread_.join(); 244 | } 245 | }; 246 | 247 | 248 | class LeaseIssuer { 249 | private: 250 | static inline const size_t LEASE_TIMEOUT = 10; // seconds 251 | 252 | using LeaseEndpoint = etcdserverpb::Lease; 253 | 254 | int64_t lease_id_{0}; 255 | std::unique_ptr lease_stub_; 256 | detail::PingSender ping_sender_; 257 | 258 | 259 | void setup_lease_renewal_(int64_t lease_id, std::chrono::seconds ttl) 260 | { 261 | auto context_ptr = std::make_shared(); 262 | auto stream = lease_stub_->LeaseKeepAlive(context_ptr.get()); 263 | 264 | etcdserverpb::LeaseKeepAliveRequest req; 265 | req.set_id(lease_id); 266 | 267 | ping_sender_ = detail::PingSender( 268 | (ttl + std::chrono::seconds(1)) / 2, 269 | [req = std::move(req), context_ptr, stream = std::move(stream)]() { 270 | if (!stream->Write(req)) { 271 | return std::chrono::seconds::zero(); 272 | } 273 | etcdserverpb::LeaseKeepAliveResponse response; 274 | stream->Read(&response); 275 | return std::chrono::seconds((response.ttl() + 1) / 2); 276 | } 277 | ); 278 | } 279 | 280 | int64_t create_lease_() 281 | { 282 | etcdserverpb::LeaseGrantRequest req; 283 | req.set_id(0); 284 | req.set_ttl(LEASE_TIMEOUT); 285 | 286 | grpc::ClientContext context; 287 | etcdserverpb::LeaseGrantResponse response; 288 | grpc::Status status = lease_stub_->LeaseGrant(&context, req, &response); 289 | 290 | detail::ensure_succeeded_(status); 291 | 292 | setup_lease_renewal_(response.id(), std::chrono::seconds(response.ttl())); 293 | return response.id(); 294 | } 295 | 296 | 297 | public: 298 | LeaseIssuer(const std::shared_ptr& channel) 299 | : lease_stub_(LeaseEndpoint::NewStub(channel)) 300 | {} 301 | 302 | int64_t get_lease() 303 | { 304 | if (!lease_id_) lease_id_ = create_lease_(); 305 | return lease_id_; 306 | } 307 | }; 308 | 309 | 310 | class ETCDTransactionBuilder { 311 | private: 312 | using TxnRequest = etcdserverpb::TxnRequest; 313 | using Compare = etcdserverpb::Compare; 314 | using RequestOp = etcdserverpb::RequestOp; 315 | using PutRequest = etcdserverpb::PutRequest; 316 | using RangeRequest = etcdserverpb::RangeRequest; 317 | using DeleteRangeRequest = etcdserverpb::DeleteRangeRequest; 318 | 319 | TxnRequest txn_; 320 | 321 | Compare* make_compare_(const std::string& key, etcdserverpb::Compare_CompareTarget target, 322 | etcdserverpb::Compare_CompareResult result) 323 | { 324 | auto cmp = txn_.add_compare(); 325 | cmp->set_key(key); 326 | cmp->set_target(target); 327 | cmp->set_result(result); 328 | 329 | return cmp; 330 | } 331 | 332 | template 333 | static 334 | void set_key_range_(Request* request, const std::variant>& range) 335 | { 336 | std::visit([request](auto&& arg) { 337 | using T = std::decay_t; 338 | 339 | if constexpr (std::is_same_v) { 340 | if (arg.size()) request->set_key(arg); 341 | } else if constexpr (std::is_same_v>) { 342 | auto [begin, end] = arg; 343 | request->set_key(begin); 344 | request->set_range_end(end); 345 | } else static_assert(detail::always_false::value, "non-exhaustive visitor"); 346 | }, range); 347 | } 348 | 349 | enum class TBStatus { 350 | UNDEFINED, 351 | SUCCESS, 352 | FAILURE, 353 | } status_; 354 | 355 | public: 356 | TxnRequest& get_transaction() 357 | { 358 | return txn_; 359 | } 360 | 361 | ETCDTransactionBuilder& add_check_exists(const std::string& key) 362 | { 363 | auto cmp = make_compare_(key, Compare::CREATE, Compare::GREATER); 364 | cmp->set_create_revision(0); 365 | 366 | return *this; 367 | } 368 | 369 | ETCDTransactionBuilder& add_check_not_exists(const std::string& key) 370 | { 371 | auto cmp = make_compare_(key, Compare::CREATE, Compare::EQUAL); 372 | cmp->set_create_revision(0); 373 | 374 | return *this; 375 | } 376 | 377 | ETCDTransactionBuilder& add_version_compare(const std::string& key, int64_t version) 378 | { 379 | auto cmp = make_compare_(key, Compare::VERSION, Compare::EQUAL); 380 | cmp->set_version(version); 381 | return *this; 382 | } 383 | 384 | ETCDTransactionBuilder& add_lease_compare(const std::string& key, int64_t lease) 385 | { 386 | auto cmp = make_compare_(key, Compare::LEASE, Compare::EQUAL); 387 | cmp->set_lease(lease); 388 | return *this; 389 | } 390 | 391 | ETCDTransactionBuilder& on_success() 392 | { 393 | status_ = TBStatus::SUCCESS; 394 | return *this; 395 | } 396 | 397 | ETCDTransactionBuilder& on_failure() 398 | { 399 | status_ = TBStatus::FAILURE; 400 | return *this; 401 | } 402 | 403 | ETCDTransactionBuilder& add_put_request(const std::string& key, const std::string& value, 404 | int64_t lease = 0, bool ignore_lease = false) 405 | { 406 | assert(status_ != TBStatus::UNDEFINED); 407 | auto request = new PutRequest(); 408 | request->set_key(key); 409 | request->set_value(value); 410 | 411 | if (ignore_lease) request->set_ignore_lease(true); 412 | else request->set_lease(lease); 413 | 414 | auto requestOp = status_ == TBStatus::SUCCESS 415 | ? txn_.add_success() 416 | : txn_.add_failure(); 417 | requestOp->set_allocated_request_put(request); 418 | 419 | return *this; 420 | } 421 | 422 | ETCDTransactionBuilder& add_range_request( 423 | const std::variant>& range, 424 | bool keys_only = false, int64_t limit = 1) 425 | { 426 | assert(status_ != TBStatus::UNDEFINED); 427 | 428 | auto request = new RangeRequest(); 429 | set_key_range_(request, range); 430 | request->set_limit(limit); 431 | request->set_keys_only(keys_only); 432 | 433 | auto requestOp = status_ == TBStatus::SUCCESS 434 | ? txn_.add_success() 435 | : txn_.add_failure(); 436 | requestOp->set_allocated_request_range(request); 437 | 438 | return *this; 439 | } 440 | 441 | ETCDTransactionBuilder& add_delete_range_request( 442 | const std::variant>& range) 443 | { 444 | assert(status_ != TBStatus::UNDEFINED); 445 | 446 | auto request = new DeleteRangeRequest(); 447 | set_key_range_(request, range); 448 | 449 | auto requestOp = status_ == TBStatus::SUCCESS 450 | ? txn_.add_success() 451 | : txn_.add_failure(); 452 | requestOp->set_allocated_request_delete_range(request); 453 | 454 | return *this; 455 | } 456 | }; 457 | 458 | 459 | 460 | class ETCDClient : public Client { 461 | private: 462 | using KV = etcdserverpb::KV; 463 | 464 | using RangeRequest = etcdserverpb::RangeRequest; 465 | using RangeResponse = etcdserverpb::RangeResponse; 466 | using PutRequest = etcdserverpb::PutRequest; 467 | using PutResponse = etcdserverpb::PutResponse; 468 | using TxnRequest = etcdserverpb::TxnRequest; 469 | using TxnResponse = etcdserverpb::TxnResponse; 470 | 471 | std::shared_ptr channel_; 472 | std::unique_ptr stub_; 473 | 474 | ETCDWatchCreator watch_creator_; 475 | LeaseIssuer lease_issuer_; 476 | 477 | 478 | // to simplify further search of direct children and subtree range 479 | // each path will be transformed in the following way: 480 | // before the __last__ segment special symbol '\0' is inserted: 481 | // as_path_string_("/prefix/a/b") -> "/prefix/a/\0b" 482 | std::string as_path_string_(const Path &path) const 483 | { 484 | std::string result; 485 | result.reserve(path.size() + prefix_.size() + 2); 486 | result.append(static_cast(prefix_)); 487 | 488 | if (path.root()) return result; 489 | 490 | auto segments = path.segments(); 491 | for (size_t i = 0; i < segments.size() - 1; ++i) { 492 | result.append("/").append(segments[i]); 493 | } 494 | 495 | return result.append("/\0", 2).append(segments.back()); 496 | } 497 | 498 | // key lies in the subtree of iff it has form "{root}/something" 499 | auto make_subtree_range_(const Path& root) 500 | { 501 | auto path = static_cast(prefix_ / root); 502 | 503 | // any sequence beginning with '/' 504 | return std::make_pair( 505 | path + '/', 506 | path + static_cast('/' + 1) 507 | ); 508 | } 509 | 510 | // "{root}/child" is a direct child of iff the is no '/' in "{child}" 511 | // or in forestated terminology, {child} is the last segment 512 | // that means "{child}" begins with '\0' 513 | auto make_direct_children_range_(const Key& root) 514 | { 515 | auto path = static_cast(prefix_ / root); 516 | 517 | // any sequence beginning with "/\0" 518 | return std::make_pair( 519 | path + '/' + '\0', 520 | path + '/' + static_cast('\0' + 1) 521 | ); 522 | } 523 | 524 | // removes prefix and auxiliary \0 525 | std::string unwrap_key_(const std::string& full_path) const 526 | { 527 | size_t pos = full_path.rfind('/'); 528 | return full_path.substr(prefix_.size(), pos + 1 - prefix_.size()) + full_path.substr(pos + 2); 529 | } 530 | 531 | TxnResponse commit_(grpc::ClientContext& context, const TxnRequest& txn) 532 | { 533 | TxnResponse response; 534 | 535 | auto status = stub_->Txn(&context, txn, &response); 536 | detail::ensure_succeeded_(status); 537 | 538 | return response; 539 | } 540 | 541 | 542 | class ETCDWatchHandle_ : public WatchHandle { 543 | private: 544 | std::shared_future future_; 545 | 546 | public: 547 | ETCDWatchHandle_(std::shared_future&& future) 548 | : future_(std::move(future)) 549 | {} 550 | 551 | void wait() override { future_.get(); } 552 | }; 553 | 554 | 555 | template 556 | std::unique_ptr make_watch_handle_( 557 | const ETCDWatchCreator::WatchCreateRequest& request, 558 | EventChecker&& stop_waiting_condition 559 | ) 560 | { 561 | auto promise = std::make_shared>(); 562 | watch_creator_.create_watch( 563 | request, 564 | { 565 | [promise, foo = std::forward(stop_waiting_condition)] 566 | (const ETCDWatchCreator::Event& event) mutable 567 | { 568 | if (foo(event)) { 569 | promise->set_value(); 570 | return true; 571 | } 572 | return false; 573 | }, 574 | [promise](const ServiceError& exc) 575 | { 576 | promise->set_exception(std::make_exception_ptr(exc)); 577 | } 578 | }); 579 | return std::make_unique(promise->get_future().share()); 580 | } 581 | 582 | 583 | public: 584 | ETCDClient(const std::string& address, Path prefix) 585 | : Client(std::move(prefix)), 586 | channel_(grpc::CreateChannel(address, grpc::InsecureChannelCredentials())), 587 | stub_(KV::NewStub(channel_)), 588 | watch_creator_(channel_), 589 | lease_issuer_(channel_) 590 | {} 591 | 592 | 593 | int64_t create(const Key& key, const std::string& value, bool lease = false) override 594 | { 595 | grpc::ClientContext context; 596 | ETCDTransactionBuilder bldr; 597 | 598 | auto path = as_path_string_(key); 599 | 600 | if (auto parent = key.parent(); !parent.root()) bldr.add_check_exists(as_path_string_(parent)); 601 | 602 | bldr.add_check_not_exists(path) 603 | // on success put value 604 | .on_success().add_put_request(path, value, lease ? lease_issuer_.get_lease() : 0) 605 | // on failure perform get request to determine a kind of error 606 | .on_failure().add_range_request(path, true); 607 | 608 | TxnResponse response = commit_(context, bldr.get_transaction()); 609 | if (!response.succeeded()) { 610 | if (response.mutable_responses(0)->release_response_range()->kvs_size()) { 611 | throw EntryExists{}; 612 | } 613 | // if compare failed but key does not exist the parent has to not exist 614 | throw NoEntry{}; 615 | } 616 | 617 | return 1; 618 | } 619 | 620 | 621 | ExistsResult exists(const Key& key, bool watch = false) override 622 | { 623 | grpc::ClientContext context; 624 | auto path = as_path_string_(key); 625 | 626 | RangeRequest request; 627 | request.set_key(path); 628 | request.set_limit(1); 629 | request.set_keys_only(true); 630 | RangeResponse response; 631 | 632 | auto status = stub_->Range(&context, request, &response); 633 | detail::ensure_succeeded_(status); 634 | 635 | bool exists = response.kvs_size(); 636 | 637 | std::unique_ptr watch_handle; 638 | if (watch) { 639 | ETCDWatchCreator::WatchCreateRequest watch_request; 640 | watch_request.set_key(path); 641 | watch_request.set_start_revision(response.header().revision() + 1); 642 | watch_handle = make_watch_handle_(watch_request, [exists](const ETCDWatchCreator::Event& ev) { 643 | return (ev.type() == ETCDWatchCreator::EventType::Event_EventType_DELETE && exists) || 644 | (ev.type() == ETCDWatchCreator::EventType::Event_EventType_PUT && !exists); 645 | }); 646 | } 647 | 648 | return { 649 | !exists ? 0 : static_cast(response.kvs(0).version()), 650 | std::move(watch_handle) 651 | }; 652 | } 653 | 654 | 655 | ChildrenResult get_children(const Key& key, bool watch = false) override 656 | { 657 | grpc::ClientContext context; 658 | 659 | auto [key_begin, key_end] = make_direct_children_range_(key); 660 | 661 | ETCDTransactionBuilder bldr; 662 | bldr.add_check_exists(as_path_string_(key)) 663 | .on_success().add_range_request(std::make_pair(key_begin, key_end), true, 0); 664 | 665 | TxnResponse response = commit_(context, bldr.get_transaction()); 666 | if (!response.succeeded()) throw NoEntry{}; 667 | 668 | std::vector children; 669 | std::set raw_keys; 670 | for (const auto& kv : response.mutable_responses(0)->release_response_range()->kvs()) { 671 | children.emplace_back(unwrap_key_(kv.key())); 672 | raw_keys.emplace(kv.key()); 673 | } 674 | 675 | std::unique_ptr watch_handle; 676 | if (watch) { 677 | ETCDWatchCreator::WatchCreateRequest watch_request; 678 | watch_request.set_key(key_begin); 679 | watch_request.set_range_end(key_end); 680 | watch_request.set_start_revision(response.header().revision() + 1); 681 | watch_handle = make_watch_handle_(watch_request, [old_keys = std::move(raw_keys)](const ETCDWatchCreator::Event& ev) { 682 | bool old = old_keys.find(ev.kv().key()) != old_keys.end(); 683 | return (old && ev.type() == ETCDWatchCreator::EventType::Event_EventType_DELETE) || 684 | (!old && ev.type() == ETCDWatchCreator::EventType::Event_EventType_PUT); 685 | }); 686 | } 687 | return { std::move(children), std::move(watch_handle) }; 688 | } 689 | 690 | 691 | int64_t set(const Key& key, const std::string& value) override 692 | { 693 | // used to preserve lease(less)ness 694 | bool expect_lease = true; 695 | 696 | while (true) { 697 | expect_lease = !expect_lease; 698 | 699 | grpc::ClientContext context; 700 | ETCDTransactionBuilder bldr; 701 | 702 | if (auto parent = key.parent(); !parent.root()) { 703 | auto path = as_path_string_(parent); 704 | bldr.add_check_exists(path) 705 | .on_failure().add_range_request(path, true); 706 | } 707 | 708 | auto path = as_path_string_(key); 709 | 710 | if (!expect_lease) bldr.add_lease_compare(path, 0); 711 | 712 | bldr.on_success().add_put_request(path, value, 0, expect_lease) 713 | .add_range_request(path); 714 | 715 | TxnResponse response; 716 | auto status = stub_->Txn(&context, bldr.get_transaction(), &response); 717 | if (status.error_code() == grpc::StatusCode::INVALID_ARGUMENT) { 718 | continue; 719 | } 720 | detail::ensure_succeeded_(status); 721 | 722 | if (!response.succeeded()) { 723 | auto& responses = *response.mutable_responses(); 724 | if (!responses.empty() && !responses[0].release_response_range()->kvs_size()) 725 | throw NoEntry{}; 726 | 727 | continue; 728 | } 729 | 730 | return response.mutable_responses(1)->release_response_range()->kvs(0).version(); 731 | } 732 | } 733 | 734 | 735 | CasResult cas(const Key& key, const std::string& value, int64_t version = 0) override 736 | { 737 | if (!version) { 738 | try { 739 | return {create(key, value)}; 740 | } catch (EntryExists&) { 741 | return {0}; 742 | } 743 | } 744 | 745 | auto path = as_path_string_(key); 746 | grpc::ClientContext context; 747 | 748 | ETCDTransactionBuilder bldr; 749 | 750 | bldr.add_version_compare(path, version) 751 | .on_success().add_put_request(path, value, 0, true) 752 | .add_range_request(path, true) 753 | .on_failure().add_range_request(path, true); 754 | 755 | TxnResponse response = commit_(context, bldr.get_transaction()); 756 | if (!response.succeeded()) { 757 | auto failure_response = response.mutable_responses(0)->release_response_range(); 758 | if (failure_response->kvs_size()) return {0}; 759 | 760 | // ! throw NoEntry if version != 0 and key doesn't exist 761 | throw NoEntry{}; 762 | } 763 | 764 | return {response.mutable_responses(1)->release_response_range()->kvs(0).version()}; 765 | } 766 | 767 | 768 | GetResult get(const Key& key, bool watch = false) override 769 | { 770 | grpc::ClientContext context; 771 | auto path = as_path_string_(key); 772 | 773 | RangeRequest request; 774 | request.set_key(path); 775 | request.set_limit(1); 776 | 777 | RangeResponse response; 778 | auto status = stub_->Range(&context, request, &response); 779 | 780 | detail::ensure_succeeded_(status); 781 | 782 | if (!response.kvs_size()) throw NoEntry{}; 783 | 784 | std::unique_ptr watch_handle; 785 | if (watch) { 786 | ETCDWatchCreator::WatchCreateRequest watch_request; 787 | watch_request.set_key(path); 788 | watch_request.set_start_revision(response.header().revision() + 1); 789 | 790 | watch_handle = make_watch_handle_(watch_request, [](const ETCDWatchCreator::Event&) { return true; }); 791 | } 792 | 793 | auto kv = response.kvs(0); 794 | return { static_cast(kv.version()), kv.value(), std::move(watch_handle) }; 795 | } 796 | 797 | 798 | void erase(const Key& key, int64_t version = 0) override 799 | { 800 | auto path = as_path_string_(key); 801 | 802 | grpc::ClientContext context; 803 | ETCDTransactionBuilder bldr; 804 | 805 | if (version) bldr.add_version_compare(path, version); 806 | else bldr.add_check_exists(path); 807 | 808 | bldr.on_success().add_delete_range_request(path) 809 | .add_delete_range_request(make_subtree_range_(key)) 810 | .on_failure().add_range_request(path, true); 811 | 812 | TxnResponse response = commit_(context, bldr.get_transaction()); 813 | if (!response.succeeded() && !response.mutable_responses(0)->release_response_range()->count()) { 814 | throw NoEntry{}; 815 | } 816 | } 817 | 818 | 819 | TransactionResult commit(const Transaction& transaction) override 820 | { 821 | grpc::ClientContext context; 822 | 823 | std::vector set_indices, create_indices; 824 | std::vector> expected_existence; 825 | size_t total_op_number = 0; 826 | 827 | ETCDTransactionBuilder bldr; 828 | 829 | for (const auto& check : transaction.checks) { 830 | auto path = as_path_string_(check.key); 831 | bldr.add_version_compare(path, check.version) 832 | .on_failure().add_range_request(path); 833 | } 834 | 835 | for (const auto& op : transaction.ops) { 836 | std::visit([this, &expected_existence, 837 | &create_indices, &set_indices, 838 | &total_op_number, &bldr](auto&& arg) { 839 | auto path = as_path_string_(arg.key); 840 | using T = std::decay_t; 841 | if constexpr (std::is_same_v) { 842 | expected_existence.emplace_back(); 843 | bldr.add_check_not_exists(path) 844 | .on_success().add_put_request(path, arg.value, 845 | arg.lease ? lease_issuer_.get_lease() : 0) 846 | .on_failure().add_range_request(path, true); 847 | expected_existence.back().push_back(false); 848 | 849 | if (auto parent = arg.key.parent(); !parent.root()) { 850 | auto path = as_path_string_(parent); 851 | bldr.add_check_exists(path) 852 | .on_failure().add_range_request(path, true); 853 | expected_existence.back().push_back(true); 854 | } 855 | 856 | create_indices.push_back(total_op_number++); 857 | } else if constexpr (std::is_same_v) { 858 | expected_existence.emplace_back(); 859 | 860 | bldr.add_check_exists(path) 861 | .on_success().add_put_request(path, arg.value) 862 | .add_range_request(path) 863 | .on_failure().add_range_request(path, true); 864 | expected_existence.back().push_back(true); 865 | 866 | // skip put request 867 | ++total_op_number; 868 | 869 | // save range request index 870 | set_indices.push_back(total_op_number++); 871 | } else if constexpr (std::is_same_v) { 872 | expected_existence.emplace_back(); 873 | 874 | bldr.add_check_exists(path) 875 | .on_success().add_delete_range_request(path) 876 | .add_delete_range_request(make_subtree_range_(arg.key)) 877 | .on_failure().add_range_request(path, true); 878 | expected_existence.back().push_back(true); 879 | 880 | ++total_op_number; 881 | } else static_assert(detail::always_false::value, "non-exhaustive visitor"); 882 | }, op); 883 | } 884 | 885 | TxnResponse response = commit_(context, bldr.get_transaction()); 886 | 887 | if (!response.succeeded()) { 888 | auto& responses = *response.mutable_responses(); 889 | 890 | // check if any check failed 891 | for (size_t i = 0; i < transaction.checks.size(); ++i) { 892 | auto resp_i = responses[i].release_response_range(); 893 | if (resp_i->kvs_size() == 0 || transaction.checks[i].version != resp_i->kvs(0).version()) { 894 | throw TxnFailed{i}; 895 | } 896 | } 897 | 898 | size_t tr_checks_number = transaction.checks.size(), j = 0; 899 | // check if any op failed 900 | for (size_t i = 0; i < transaction.ops.size(); ++i) { 901 | for (bool should_exist : expected_existence[i]) { 902 | if (should_exist ^ static_cast(responses[j + tr_checks_number] 903 | .release_response_range() 904 | ->kvs_size())) 905 | throw TxnFailed{i + tr_checks_number}; 906 | ++j; 907 | } 908 | } 909 | 910 | throw ServiceError{"we are sorry for your transaction"}; 911 | } 912 | 913 | TransactionResult result; 914 | 915 | size_t i = 0, j = 0; 916 | while (i != create_indices.size() || j != set_indices.size()) { 917 | if (j == set_indices.size() 918 | || (i != create_indices.size() && create_indices[i] < set_indices[j])) { 919 | result.push_back(TxnOpResult{TxnOpResult::Kind::CREATE, 1}); 920 | ++i; 921 | } else { 922 | result.push_back(TxnOpResult{ 923 | TxnOpResult::Kind::SET, 924 | static_cast( 925 | response.mutable_responses(set_indices[j]) 926 | ->release_response_range() 927 | ->kvs(0) 928 | .version()) 929 | }); 930 | ++j; 931 | } 932 | } 933 | return result; 934 | } 935 | }; 936 | 937 | } // namespace liboffkv 938 | --------------------------------------------------------------------------------