Repository: tgockel/zookeeper-cpp Branch: master Commit: 425dfe0a5f50 Files: 96 Total size: 367.6 KB Directory structure: gitextract_87o4bcld/ ├── .github/ │ └── ISSUE_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── AUTHORS ├── CMakeLists.txt ├── CONTRIBUTING.rst ├── COPYING ├── README.md ├── cmake/ │ └── modules/ │ ├── BuildFunctions.cmake │ ├── CodeCoverage.cmake │ ├── ConfigurationSetting.cmake │ ├── ListSplit.cmake │ └── ZooKeeper.cmake ├── config/ │ ├── dev-env │ ├── docker/ │ │ ├── debian-10/ │ │ │ └── Dockerfile │ │ ├── opensuse-15/ │ │ │ └── Dockerfile │ │ ├── ubuntu-16.04/ │ │ │ └── Dockerfile │ │ ├── ubuntu-18.04/ │ │ │ └── Dockerfile │ │ └── ubuntu-20.04/ │ │ └── Dockerfile │ ├── make-doxygen │ ├── make-package │ ├── make-packages │ ├── publish-doxygen │ ├── run-tests │ ├── travisci_rsa.enc │ └── upload-coverage ├── doc/ │ └── Doxyfile ├── install/ │ └── deb/ │ ├── libzkpp/ │ │ ├── control.in │ │ ├── postinst.in │ │ └── shlibs.in │ ├── libzkpp-dev/ │ │ └── control.in │ ├── libzkpp-server/ │ │ ├── control.in │ │ └── postinst.in │ └── libzkpp-server-dev/ │ └── control.in └── src/ └── zk/ ├── acl.cpp ├── acl.hpp ├── acl_tests.cpp ├── buffer.hpp ├── client.cpp ├── client.hpp ├── client_tests.cpp ├── config.hpp ├── connection.cpp ├── connection.hpp ├── connection_tests.cpp ├── connection_zk.cpp ├── connection_zk.hpp ├── error.cpp ├── error.hpp ├── error_tests.cpp ├── exceptions.cpp ├── exceptions.hpp ├── forwards.hpp ├── future.hpp ├── multi.cpp ├── multi.hpp ├── multi_tests.cpp ├── optional.hpp ├── optional_tests.cpp ├── results.cpp ├── results.hpp ├── server/ │ ├── classpath.cpp │ ├── classpath.hpp │ ├── classpath_registration_template.cpp.in │ ├── classpath_tests.cpp │ ├── configuration.cpp │ ├── configuration.hpp │ ├── configuration_tests.cpp │ ├── detail/ │ │ ├── close.cpp │ │ ├── close.hpp │ │ ├── event_handle.cpp │ │ ├── event_handle.hpp │ │ ├── pipe.cpp │ │ ├── pipe.hpp │ │ ├── pipe_tests.cpp │ │ ├── subprocess.cpp │ │ ├── subprocess.hpp │ │ └── subprocess_tests.cpp │ ├── package_registry.cpp │ ├── package_registry.hpp │ ├── package_registry_tests.cpp │ ├── package_registry_tests.hpp │ ├── server.cpp │ ├── server.hpp │ ├── server_group.cpp │ ├── server_group.hpp │ ├── server_group_tests.cpp │ ├── server_tests.cpp │ └── server_tests.hpp ├── string_view.hpp ├── tests/ │ ├── main.cpp │ ├── test.cpp │ └── test.hpp ├── types.cpp ├── types.hpp └── types_tests.cpp ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ Don't forget the label! - If you have a question, use the "question" tag. ================================================ FILE: .gitignore ================================================ *~ *.swp *.orig *.kdev4 build* .kdev* *.kdev4 packages/ .vs x64/ *sdf *.pc # Compiled source # ################### *.com *.class *.dll *.exe *.o *.so # Packages # ############ # it's better to unpack these files and commit the raw source # git has its own built in compression methods *.7z *.dmg *.gz *.iso *.jar *.rar *.tar *.zip # Logs and databases # ###################### *.log *.sql *.sqlite # OS generated files # ###################### .DS_Store .DS_Store? ._* .Spotlight-V100 .Trashes ehthumbs.db Thumbs.db ## my files and cmake out out.* # IDE files # ############# nbproject .~lock.* .buildpath .idea .project .settings composer.lock #Cmake CMakeCache.txt CMakeFiles CMakeScripts Makefile cmake_install.cmake install_manifest.txt ================================================ FILE: .travis.yml ================================================ language: cpp sudo: required dist: trusty env: matrix: - DISTRO=ubuntu-16.04 CONFIG=Debug - DISTRO=ubuntu-16.04 CONFIG=Release - DISTRO=ubuntu-18.04 CONFIG=Debug - DISTRO=ubuntu-18.04 CONFIG=Release - DISTRO=ubuntu-20.04 CONFIG=Debug - DISTRO=ubuntu-20.04 CONFIG=Release - DISTRO=debian-10 CONFIG=Debug - DISTRO=debian-10 CONFIG=Release - DISTRO=opensuse-15 CONFIG=Debug services: - docker before_install: - docker build config/docker/${DISTRO} -t dev/zookeeper-cpp/${DISTRO} script: - echo ${COVERALLS_REPO_TOKEN} > ${TRAVIS_BUILD_DIR}/coveralls-repo-token - if [[ ${CONFIG} == "Debug" ]]; then ./config/dev-env ${DISTRO} -- ./config/run-tests; fi - if [[ ${CONFIG} == "Release" ]]; then ./config/make-package --dockerize ${DISTRO}; fi after_success: - if [[ ${DISTRO} != "ubuntu-20.04" ]]; then echo "Skipping documentation publishing due to non-main build environment"; exit 0; fi - if [[ ${CONFIG} != "Debug" ]]; then echo "Skipp documentation publishing due to non-debug build"; exit 0; fi - GIT_CURRENT_HASH=$(git rev-parse HEAD) - GIT_MASTER_HASH=$(git rev-parse master) - GIT_REMOTE_NAME=$(git remote) - GIT_REMOTE_FETCH_PATH=$(git remote --verbose | grep -P '^'${GIT_REMOTE_NAME}'.*\(fetch\)$' | awk '{print $2}') - GIT_EXPECTED_PATH=https://github.com/tgockel/zookeeper-cpp.git - echo "GIT_CURRENT_HASH=${GIT_CURRENT_HASH} GIT_REMOTE_NAME=${GIT_REMOTE_NAME} GIT_REMOTE_FETCH_PATH=${GIT_REMOTE_FETCH_PATH}" - if [[ ${GIT_CURRENT_HASH} != ${GIT_MASTER_HASH} ]]; then echo "Skipping documentation publishing due to non-master ${GIT_CURRENT_HASH} (master=${GIT_MASTER_HASH})"; exit 0; fi - if [[ ${GIT_REMOTE_FETCH_PATH} != ${GIT_EXPECTED_PATH} ]]; then echo "Skipping documentation publishing due to non-mainline remote ${GIT_REMOTE_FETCH_PATH}"; exit 0; fi - sudo add-apt-repository --yes ppa:libreoffice/ppa - sudo apt-get update - sudo apt-get install --yes doxygen graphviz texlive-full - openssl aes-256-cbc -K $encrypted_513b1ad04072_key -iv $encrypted_513b1ad04072_iv -in config/travisci_rsa.enc -out config/travisci_rsa -d - chmod 0600 config/travisci_rsa - cp config/travisci_rsa ~/.ssh/id_rsa - "./config/publish-doxygen" ================================================ FILE: AUTHORS ================================================ Contributors ============ Travis Gockel ================================================ FILE: CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.5) file(READ src/zk/config.hpp CONFIG_HPP_STR) string(REGEX REPLACE ".*# *define +ZKPP_VERSION_MAJOR +([0-9]+).*" "\\1" ZKPP_VERSION_MAJOR "${CONFIG_HPP_STR}") string(REGEX REPLACE ".*# *define +ZKPP_VERSION_MINOR +([0-9]+).*" "\\1" ZKPP_VERSION_MINOR "${CONFIG_HPP_STR}") string(REGEX REPLACE ".*# *define +ZKPP_VERSION_PATCH +([0-9]+).*" "\\1" ZKPP_VERSION_PATCH "${CONFIG_HPP_STR}") set(ZKPP_VERSION "${ZKPP_VERSION_MAJOR}.${ZKPP_VERSION_MINOR}.${ZKPP_VERSION_PATCH}") project(zookeeper-cpp LANGUAGES CXX VERSION "${ZKPP_VERSION}" ) set(PROJECT_SO_VERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}") message(STATUS "Software Version: ${ZKPP_VERSION}") ################################################################################ # CMake # ################################################################################ cmake_policy(VERSION 3.5) cmake_policy(SET CMP0037 OLD) # allow generation of "test" target set(CMAKE_REQUIRED_QUIET YES) # tell check_include_file_cxx to keep quiet list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules/") include(BuildFunctions) include(CheckIncludeFileCXX) include(ConfigurationSetting) include(ListSplit) include(ZooKeeper) ################################################################################ # Build Configuration # ################################################################################ if (NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE "Debug") message(STATUS "No build type selected, default to ${CMAKE_BUILD_TYPE}") endif() set(VALID_BUILD_TYPES Debug Release) if(NOT ${CMAKE_BUILD_TYPE} IN_LIST VALID_BUILD_TYPES) message(FATAL_ERROR "Invalid CMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}\nValid build types are: ${VALID_BUILD_TYPES}") endif() message(STATUS "Configuration: ${CMAKE_BUILD_TYPE}") set(ZKPP_SERVER_VERSIONS "3.5.5;3.4.14" CACHE STRING "The ZooKeeper server versions to run tests against. The first in the list is the default." ) message(STATUS "Features:") build_option(NAME CODE_COVERAGE DOC "Enable code coverage (turns on the test-coverage target)" DEFAULT OFF CONFIGS_ON Debug ) configuration_setting(NAME BUFFER DOC "Type to use for zk::buffer" DEFAULT STD_VECTOR OPTIONS STD_VECTOR STD_STRING CUSTOM ) configuration_setting(NAME FUTURE DOC "Type to use for zk::future and zk::promise" DEFAULT STD OPTIONS STD STD_EXPERIMENTAL BOOST CUSTOM ) set(CXX_STANDARD c++17 CACHE STRING "The language standard to target for C++." ) set(CXX_WARNINGS "-Wall -Wextra -Wconversion -Werror") set(CXX_EXTRA_FLAGS "-Wl,--no-as-needed") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --std=${CXX_STANDARD} ${CXX_WARNINGS} -ggdb3 ${CXX_EXTRA_FLAGS}") set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DZKPP_DEBUG=1") set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3") ################################################################################ # External Libraries # ################################################################################ find_library(zookeeper_LIBRARIES zookeeper_mt) set(ZKPP_LIB_DEPENDENCIES ${ZKPP_LIB_DEPENDENCIES} ${zookeeper_LIBRARIES}) include_directories("${PROJECT_SOURCE_DIR}/src") if (ZKPP_BUILD_SETTING_FUTURE STREQUAL "BOOST") find_package(Boost 1.52.0 REQUIRED thread) set(ZKPP_LIB_DEPENDENCIES ${ZKPP_LIB_DEPENDENCIES} ${Boost_LIBRARIES}) endif() ################################################################################ # GTest # ################################################################################ find_package(GTest) if(GTest_FOUND) # Make it look like find_library set(gtest_LIBRARIES GTest::GTest) elseif(EXISTS "/usr/src/googletest/googletest/src" OR EXISTS "/usr/src/gtest/src/") # GTest's packaging on Ubuntu (googletest or libgtest-dev, depending on which version) contains all the source files # instead of a library and headers. CMake has a package discovery for GTest, but it does not pick up on this for you, # so we'll build it manually here. message(STATUS "GTest is not installed, but the sources were found...adding them to the build") if(EXISTS "/usr/src/googletest/googletest/src") # Prefer the "googletest" version, as sometimes the "gtest" one is an empty directory (this seems to be the result # of a packaging issue) set(GTEST_SRC_ROOT "/usr/src/googletest/googletest/src") else() set(GTEST_SRC_ROOT "/usr/src/gtest/src") endif() if(EXISTS "${GTEST_SRC_ROOT}/gtest-all.cc") set(gtest_lib_cpps "${GTEST_SRC_ROOT}/gtest-all.cc") message(STATUS "Building with ${GTEST_SRC_ROOT}/gtest-all.cc") else() file(GLOB gtest_lib_cpps "${GTEST_SRC_ROOT}/gtest-*.cc") message(STATUS "Building with gtest_lib_cpps=${gtest_lib_cpps}") endif() add_library(gtest SHARED ${gtest_lib_cpps}) # GTest uses relative imports incorrectly, so make sure to add it to the include path. target_include_directories(gtest PRIVATE "${GTEST_SRC_ROOT}/..") # Also disable -Werror target_compile_options(gtest PRIVATE "-Wno-error") set(gtest_LIBRARIES gtest) else() message(SEND_ERROR "GTest was not found") endif() ################################################################################ # Building # ################################################################################ build_module(NAME zkpp-tests PATH src/zk/tests LINK_LIBRARIES ${gtest_LIBRARIES} ) build_module(NAME zkpp PATH src/zk NO_RECURSE LINK_LIBRARIES ${ZKPP_LIB_DEPENDENCIES} ) build_module(NAME zkpp-server PATH src/zk/server LINK_LIBRARIES zkpp ) target_link_libraries(zkpp_tests zkpp-server zkpp-server_tests) ################################################################################ # ZooKeeper Server Testing # ################################################################################ foreach(server_version IN LISTS ZKPP_SERVER_VERSIONS) find_zookeeper_server(VERSION "${server_version}" OUTPUT_CLASSPATH server_classpath ) set(generated_cpp "${CMAKE_CURRENT_BINARY_DIR}/generated/src/zk/server/classpath_registration_${server_version}.cpp") configure_file(src/zk/server/classpath_registration_template.cpp.in "${generated_cpp}" @ONLY) target_sources(zkpp-server_tests PRIVATE "${generated_cpp}") endforeach() ################################################################################ # Targets # ################################################################################ add_custom_target(test COMMAND $ "--gtest_output=xml:test-results.xml" "--gtest_death_test_style=threadsafe" DEPENDS zkpp-tests_prog BYPRODUCTS test-results.xml USES_TERMINAL ) # Similar to test, but run it inside of GDB with GTest options one would want when running in GDB. add_custom_target(gdbtest COMMAND "gdb" "-args" $ "--gtest_output=xml:test-results-gdb.xml" "--gtest_death_test_style=threadsafe" "--gtest_break_on_failure=1" "--gtest_catch_exceptions=0" DEPENDS zkpp-tests_prog BYPRODUCTS test-results-gdb.xml USES_TERMINAL ) if(ZKPP_BUILD_OPTION_CODE_COVERAGE) include(CodeCoverage) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fprofile-arcs -ftest-coverage") setup_target_for_coverage(test-coverage zkpp-tests_prog coverage "--gtest_death_test_style=threadsafe") endif() ################################################################################ # Packaging # ################################################################################ set(ZKPP_PACKAGE_SYSTEM "" CACHE STRING "The packaging system to generate a package build file for (leave blank to not build a package)" ) if(ZKPP_PACKAGE_SYSTEM) message(STATUS "Packaging system ${ZKPP_PACKAGE_SYSTEM}:") set(PROJECT_PACKAGE_VERSION "${PROJECT_VERSION}-1") # TODO: Allow arbitrary tags here. if(CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64") set(PROJECT_BUILD_ARCHITECTURE "amd64") elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL "x86") set(PROJECT_BUILD_ARCHITECTURE "i386") else() set(PROJECT_BUILD_ARCHITECTURE ${CMAKE_SYSTEM_PROCESSOR}) endif() set(all_packages) foreach(module zkpp zkpp-server) message(STATUS " ${module}:") set(package_name ${module}) string(REGEX REPLACE "-" "/" header_path "${module}") string(REGEX REPLACE "pp" "" header_path "${header_path}") if(ZKPP_PACKAGE_SYSTEM STREQUAL "DEB") if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/install/deb/lib${package_name}") message(STATUS " lib${package_name}") configure_file("${CMAKE_CURRENT_SOURCE_DIR}/install/deb/lib${package_name}/control.in" "${CMAKE_CURRENT_BINARY_DIR}/install/lib${package_name}${PROJECT_SO_VERSION}/DEBIAN/control" @ONLY IMMEDIATE ) if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/install/deb/lib${package_name}/postinst.in") configure_file("${CMAKE_CURRENT_SOURCE_DIR}/install/deb/lib${package_name}/postinst.in" "${CMAKE_CURRENT_BINARY_DIR}/intermediate/lib${package_name}${PROJECT_SO_VERSION}/DEBIAN/postinst" @ONLY IMMEDIATE ) file(COPY "${CMAKE_CURRENT_BINARY_DIR}/intermediate/lib${package_name}${PROJECT_SO_VERSION}/DEBIAN/postinst" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/install/lib${package_name}${PROJECT_SO_VERSION}/DEBIAN" FILE_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE ) endif() if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/install/deb/lib${package_name}/shlibs.in") configure_file("${CMAKE_CURRENT_SOURCE_DIR}/install/deb/lib${package_name}/shlibs.in" "${CMAKE_CURRENT_BINARY_DIR}/install/lib${package_name}${PROJECT_SO_VERSION}/DEBIAN/shlibs" @ONLY IMMEDIATE ) endif() add_custom_target("lib${package_name}.deb" BYPRODUCTS "install/lib${package_name}${PROJECT_SO_VERSION}.deb" COMMAND rm -rf "install/lib${package_name}${PROJECT_SO_VERSION}${CMAKE_INSTALL_PREFIX}/lib" COMMAND mkdir -p "install/lib${package_name}${PROJECT_SO_VERSION}${CMAKE_INSTALL_PREFIX}/lib" COMMAND cp "$" "install/lib${package_name}${PROJECT_SO_VERSION}${CMAKE_INSTALL_PREFIX}/lib" COMMAND dpkg --build "install/lib${package_name}${PROJECT_SO_VERSION}" DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/install/lib${package_name}${PROJECT_SO_VERSION}/DEBIAN/control" ${module} ) list(APPEND all_packages "lib${package_name}.deb") endif() if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/install/deb/lib${package_name}-dev") message(STATUS " lib${package_name}-dev") set(staging_dir "install/lib${package_name}${PROJECT_SO_VERSION}-dev${CMAKE_INSTALL_PREFIX}/include/${header_path}") configure_file("${CMAKE_CURRENT_SOURCE_DIR}/install/deb/lib${package_name}-dev/control.in" "${CMAKE_CURRENT_BINARY_DIR}/install/lib${package_name}${PROJECT_SO_VERSION}-dev/DEBIAN/control" @ONLY IMMEDIATE ) add_custom_target("lib${package_name}-dev.deb" BYPRODUCTS "install/lib${package_name}${PROJECT_SO_VERSION}-dev.deb" COMMAND rm -rf "${staging_dir}" COMMAND mkdir -p "${staging_dir}" COMMAND cp -t "${staging_dir}" ${${module}_LIBRARY_HEADERS} COMMAND dpkg --build "install/lib${package_name}${PROJECT_SO_VERSION}-dev" DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/install/lib${package_name}${PROJECT_SO_VERSION}-dev/DEBIAN/control" ${${module}_LIBRARY_HEADERS} ) list(APPEND all_packages "lib${package_name}-dev.deb") endif() else() message(FATAL_ERROR "Unknown ZKPP_PACKAGE_SYSTEM=${ZKPP_PACKAGE_SYSTEM}") endif() endforeach() add_custom_target(package DEPENDS ${all_packages} COMMENT "Built all packages" ) endif() ================================================ FILE: CONTRIBUTING.rst ================================================ Contributing Guide ================== So you want to contribute to the ZooKeeper C++ library? I'd love your contribution! Please help me. Building -------- Building the system only requires `CMake `_ and the standard-issue C++ compilation tools. Docker ^^^^^^ Docker is the official mechanism for supporting multiple Linux distributions (see the `TravisCI `_ build). If you would like to do this at home, simply use the ``dev-env`` script:: $> cd /path/to/zookeeper-cpp $> ./config/dev-env ubuntu-18.04 This will create a Docker image named something like ``dev/zookeeper-cpp/ubuntu-18.04`` and run that image with the project's working directory mapped to ``~/zookeeper-cpp`` with you in control of a shell. Inside Docker, you can now build:: root@0ae2f54b152b:~/zookeeper-cpp# mkdir build-debug root@0ae2f54b152b:~/zookeeper-cpp# cd build-debug root@0ae2f54b152b:~/zookeeper-cpp/build-debug# cmake -GNinja .. ... output ... root@0ae2f54b152b:~/zookeeper-cpp/build-debug# ninja test ... output ... This experience is pretty decent. The biggest annoyance is editing within the Docker image makes files you touch owned by *root* (I suspect there is a way to prevent this, but I am far from competent at Docker). If you use `KDevelop `_, you can use the IDE to build and debug inside of these images with `KDevelop Runtimes `_. Process ------- This library follows the `GitHub Fork + Pull Model `_. Below are the more project-specific steps. Issue Tracker ^^^^^^^^^^^^^ All work *must* be tracked in the `Issue Tracker `_, otherwise the maintainer will have no idea what is going on. Try to find an existing bug in the list of issues -- if you can't find it, open a new issue with a descriptive title and descriptive description. If you are unclear on if it should be a bug or not, mark it with a *Question* tag or just send me an `email `_. Assign the issue to yourself so I don't forget who is working on it. For more granular tracking, the issue should move across the `GitHub Project board `_. The columns of the project should be somewhat intuitive: :Backlog: Things we are planning on doing soon. :Design: System is being designed. What this usually means is the API is being written. *Please* write your API first -- it can save a lot of time in the long run. :Implementation: The component is currently being implemented. :Pull Request: There is an open pull request. :Done: Work is complete! Developing ^^^^^^^^^^ 1. Fork the repository. 2. Branch in your fork (not actually required, but generally considered a Good Idea). 3. Write your code. 4. If this is your first contribution, add yourself to ``AUTHORS`` (alphabetically). 5. Commit your code (somewhere in the commit message, be sure to mention "Issue #NN", where "NN" is the issue number you were working on). 6. Watch your tests pass for all environments in TravisCI. 7. Issue a pull request from your branch to the master branch of the main repository. 8. Close the branch in your repository (not actually required, but clean repos are nice). Sign Your Commits """"""""""""""""" When committing code, please `sign commits with GPG `_. This lets me know that work submitted by you was really created by you (security or something like that). If you always want to sign commits instead of specifying ``-S`` on the command line every time, add it to your global configuration:: $> git config --global user.signingkey ${YOUR_KEY_ID} $> git config --global commit.gpgsign true ================================================ FILE: COPYING ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # ZooKeeper C++ A ZooKeeper client for C++. It is [hosted on GitHub](https://github.com/tgockel/zookeeper-cpp), [documented](https://tgockel.github.io/zookeeper-cpp/) (including [previous versions](https://tgockel.github.io/zookeeper-cpp/version/) of the software), and [tested](https://travis-ci.org/tgockel/zookeeper-cpp). Features include (but are not necessarily limited to): - Simple - Connect with just a connection string - Clients should not require factories - Does not require knowledge of the Java or C APIs - Configurable - Use the parts you need - Change parts to fit in your application - Safe - In the best case, illegal code should fail to compile - An illegal action should throw an exception - Utility functions have a [strong exception guarantee](http://www.gotw.ca/gotw/082.htm>) - Stable - Worry less about upgrading -- the API and ABI will not change out from under you **NOTE**: This library is a work-in-progress. All documentation you see here is subject to change and non-existence. [![Build Status](https://travis-ci.org/tgockel/zookeeper-cpp.svg?branch=master)](https://travis-ci.org/tgockel/zookeeper-cpp) ## Usage Ultimately, the usage looks like this (assuming you have a ZooKeeper server running on your local host): #include #include #include #include #include #include /** All result types are printable for debugging purposes. **/ template void print_thing(const zk::future& result) { try { // Unwrap the future value, which will not block (based on usage), but could throw. T value(result.get()); std::cerr << value << std::endl; } catch (const std::exception& ex) { // Error "handling" std::cerr << "Exception: " << ex.what() << std::endl; } } int main() { // Start a ZK server running on localhost (not needed if you just want a client, but great for testing and // demonstration purposes). zk::server::server server(zk::server::configuration::make_minimal("zk-data", 2181)); // zk::client::connect returns a future, which is delivered when the connection is established. auto client = zk::client::connect("zk://127.0.0.1:2181") .get(); // get_result has a zk::buffer and zk::stat. client.get("/foo/bar") .then(print_thing); // get_children_result has a std::vector for the path names and zk::stat for the parent stat. client.get_children("/foo") .then(print_thing); // set_result has a zk::stat for the modified ZNode. client.set("/foo/bar", "some data") .then(print_thing); // More explicit: client.create("/foo/baz", "more data", zk::acls::open_unsafe(), zk::create_mode::normal); client.create("/foo/baz", "more data") .then(print_thing); client.get("/foo/bar") .then([client] (const auto& get_res) { zk::version foo_bar_version = get_res.get().stat().data_version; zk::multi_op txn = { zk::op::check("/foo", zk::version::any()), zk::op::check("/foo/baz", foo_bar_version), zk::op::create("/foo/bap", "hi", nullopt, zk::create_mode::sequential), zk::op::erase("/foo/bzr"), }; // multi_res's type is zk::future client.commit(txn).then(print_thing); }); // This is not strictly needed -- a client falling out of scope will auto-trigger close client.close(); } ## Value-Added Features The core library of `libzkpp` provides the primitives for connecting to and manipulating a ZooKeeper database. This library also bundles a number of other features that are commonly required when working with a ZooKeeper cluster. ### `zk/curator` Things in `zk/curator` have features found in the [Apache Curator](http://curator.apache.org/) project. * Elections * [Leader Latch](https://github.com/tgockel/zookeeper-cpp/issues/1) * [Leader Election](https://github.com/tgockel/zookeeper-cpp/issues/2) * Locks * [Shared Reentrant Lock](https://github.com/tgockel/zookeeper-cpp/issues/3) * [Shared Lock](https://github.com/tgockel/zookeeper-cpp/issues/4) * [Shared Reentrant Read Write Lock](https://github.com/tgockel/zookeeper-cpp/issues/5) * [Shared Semaphore](https://github.com/tgockel/zookeeper-cpp/issues/6) * [Multi Shared Lock](https://github.com/tgockel/zookeeper-cpp/issues/7) * Barriers * [Barrier](https://github.com/tgockel/zookeeper-cpp/issues/8) * [Double Barrier](https://github.com/tgockel/zookeeper-cpp/issues/9) * Counters * [Shared Counter](https://github.com/tgockel/zookeeper-cpp/issues/10) * [Distributed Atomic Long](https://github.com/tgockel/zookeeper-cpp/issues/11) * Caches * [Path Cache](https://github.com/tgockel/zookeeper-cpp/issues/12) * [Node Cache](https://github.com/tgockel/zookeeper-cpp/issues/13) * [Tree Cache](https://github.com/tgockel/zookeeper-cpp/issues/14) * Nodes * [Persistent Node](https://github.com/tgockel/zookeeper-cpp/issues/15) * [Persistent TTL Node](https://github.com/tgockel/zookeeper-cpp/issues/16) * [Group Member](https://github.com/tgockel/zookeeper-cpp/issues/17) None of the queue types are planned to be implemented. The [Curator Documentation (TN4)](https://cwiki.apache.org/confluence/display/CURATOR/TN4) advises against their use, claiming "it is a bad idea to use ZooKeeper as a Queue." The authors of this library agree with this claim. ### `zk/fake` This library also provides a fake version of ZooKeeper which operates in-memory. It is meant to be used in your unit testing, when fine-grained control of behavior of ZooKeeper is needed. This allows for the injection of arbitrary behavior into ZK, allowing you to simulate some of the hard-to-reproduce issues like `zk::event_type::not_watching`, `zk::marshalling_error`, or timing bugs. It also allows for fast creation and teardown of entire databases, which is commonly done in unit testing. It is connected to through using a connection string of the form: fake://{name} To use this in unit tests link to `libzkpp_fake` and use `zk::fake::server`: TEST(my_test) { // The default constructor uses a randomly-generated unique name zk::fake::server server; // Fetch that name through the connection_string zk::client client(server.connection_string()); // use client normally } ### `zk/server` This library controls a ZooKeeper Java process on this machine. It is meant to be used in applications that manage a ZooKeeper cluster from native code. ## Unsupported Functionality If you are used to using ZooKeeper via the Java or C APIs, there are a few things that are explicitly not supported in this library. ### Global Watches There are two main ways to receive watch notifications: the global watch or through use a watcher objects. In the Java API, the `ZooKeeper` client allows for a global [Watcher](https://zookeeper.apache.org/doc/r3.4.10/api/org/apache/zookeeper/Watcher.html). In the C API, `zookeeper_init` can be provided with a global function with the signature `void (*)(zhandle_t* zh, int type, int state, const char* path, void* watcherCtx)` to achieve this same result. Global watches are somewhat of a "legacy" feature -- the dual interface of global and callbacks is somewhat confusing. As such, global watches are *not* supported by this library. ### Synchronous API The C library offers both a synchronous and an asynchronous API. This library offers only an asynchronous version. If you prefer a synchronous API, call `get()` on the returned `future` to block until you receive the response. ### Non-Linux Can you get this library working on platforms that are not Linux? Maybe. But Linux is the primary development, testing, and deployment platform of people writing distributed applications, so this library is targetted at Linux. ## License Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0). Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ## F.A.Q. ### Why `erase` instead of `delete`? In the Java and C APIs, the act of removing a ZNode is called `delete` and `zoo_delete`, respectively. However, `delete` is a C++ keyword and cannot be used as a member function. So, this library uses `erase`, which falls in line with standard C++ containers. Alternatives such as calling the operation `delete_` look a bit worse (in the author's opinion). ### Why are watch calls separate? In the Java and C APIs, adding a watch to a ZNode is an additional parameter to the `get`, `get_children`, or `exists` calls while this library uses separate `watch`, `watch_children`, and `watch_exists` calls. This is done because the return types are different between a simple fetch and setting a watch. While `get` returns a `future`, `watch` returns the slightly more complicated `future`. The `future` in `watch_result::next()` would be disabled in cases where a flag is not set, and it would be ignored with the majority of use cases. This leads to an awkward API for simple calls. An alternative used by other libraries is to provide a `std::function`, implying to not watch when the function is not passed in. This has a number of disadvantages: - There is no good way to cancel a watch without giving an extra parameter. With a `future`, you simply let it fall out of scope. - Watches are delivered only once, which is obvious from a `future`-like API, but not obvious from a `function`-like API. - It is not obvious what the behavior should be if the original call returns in error. With a `future`, the behavior is obvious, since you never receive the mechanisms to perform the watch. In Java, the method of choice is to use the [Watcher](https://zookeeper.apache.org/doc/r3.4.10/api/org/apache/zookeeper/Watcher.html) interface, but this feels extremely out of place in C++ code. ### Where are all the `KeeperException`s? This library uses an exception hierarchy with fewer exception codes than what are available in [`KeeperException`](https://zookeeper.apache.org/doc/r3.4.10/api/org/apache/zookeeper/KeeperException.html). ![Exception hierarchy](https://tgockel.github.io/zookeeper-cpp/classzk_1_1error.png) Some exceptions are not present in this library because they are no longer used in the server implementation and will not be used again; an example of this is `DataInconsistencyException`, which has not been used in ZooKeeper for a while. In other cases, the error code would never be thrown by this library; examples of this are `NoWatcherException` (watch removal happens implicitly in destructors) and `RuntimeInconsistencyException` (failed multi-ops throw a `transaction_failed` containing only the index of the failed operation instead). In other cases, the error codes have been merged into a single exception type, as there was much logical overlap. Another distinction that was dropped is the difference between "system errors" (`Code.SYSTEMERROR`/`ZSYSTEMERROR`) and "API errors" (`Code.APIERROR`/`ZAPIERROR`). The general distinction is the origin of the error -- system errors are client-side (`invalid_arguments` -- other APIs: `Code.BADARGUMENTS`/`ZBADARGUMENTS`), while API errors are server-size (`no_entry` -- other APIs: `Code.NoNode`/`ZNONODE`). This was dropped because this is not entirely meaningful from user's point of view. As an example, `authentication_failed` is a subclass of `invalid_arguments`, even though the contents of the arguments happen to be validated by the server instead of by the client. ### How can I contribute? Pick an [open issue](https://github.com/tgockel/zookeeper-cpp/issues) and start working on it! For more details, read the [CONTRIBUTING](https://github.com/tgockel/zookeeper-cpp/blob/master/CONTRIBUTING.rst) guide. ================================================ FILE: cmake/modules/BuildFunctions.cmake ================================================ include(CMakeParseArguments) include(ListSplit) # build_option # Creates a build option, which is configurable via a CMake option. If the option is set to anything non-default, a # macro with the name `ZKPP_ENABLE_${NAME}` is exported with the value of `0` or `1`. # # - NAME: The name of the build option. # - DOC: Documentation to place in the configuration GUI. # - DEFAULT: The default value of the configuration (ON or OFF). # - CONFIGS_ON[]: List of build configurations this option should be ON for. # - CONFIGS_OFF[]: List of build configurations this option should be OFF for. function(build_option) set(options) set(oneValueArgs NAME DOC DEFAULT) set(multiValueArgs CONFIGS_ON CONFIGS_OFF) cmake_parse_arguments(OPT "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) if(NOT DEFINED VALID_BUILD_TYPES) message(SEND_ERROR "VALID_BUILD_TYPES not set -- future build_option errors will be inaccurrate") endif() foreach(bt IN LISTS OPT_CONFIGS_ON OPT_CONFIGS_OFF) if(NOT ${bt} IN_LIST VALID_BUILD_TYPES) message(WARNING "Specified configuration for invalid CMAKE_BUILD_TYPE=${bt}") endif() endforeach() if(${CMAKE_BUILD_TYPE} IN_LIST OPT_CONFIGS_ON) set(ENABLED ON) elseif(${CMAKE_BUILD_TYPE} IN_LIST OPT_CONFIGS_OFF) set(ENABLED OFF) else() set(ENABLED ${OPT_DEFAULT}) endif() set(ZKPP_BUILD_OPTION_${OPT_NAME} ${ENABLED} CACHE BOOL "${OPT_DOC}" ) if(ZKPP_BUILD_OPTION_${OPT_NAME}) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DZKPP_ENABLE_${OPT_NAME}=1" PARENT_SCOPE) else() set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DZKPP_ENABLE_${OPT_NAME}=0" PARENT_SCOPE) endif() message(STATUS " ${OPT_NAME}: ${ENABLED}") endfunction() # build_module # Adds a module to build. # # NAME: The name of this module. # PATH: The path to find the source files for this module. It is legal to specify more than one PATH in this list. # LINK_LIBRARIES: A list of libraries to link to # PROTOTYPE: If set, the module should be considered a "prototype." It will not be built by default and does not # consider warnings as errors. # NO_RECURSE: Do not search recursively. function(build_module) set(options PROTOTYPE NO_RECURSE) set(oneValueArgs NAME) set(multiValueArgs LINK_LIBRARIES PATH) cmake_parse_arguments(MODULE "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) message(STATUS "${MODULE_NAME} : ${MODULE_PATH}") if(MODULE_PROTOTYPE) set(BUILD_PROTOTYPE_${MODULE_NAME} OFF CACHE BOOL "Build the '${MODULE_NAME}' program?" ) message(STATUS " ! prototype ${BUILD_PROTOTYPE_${MODULE_NAME}}") if(NOT BUILD_PROTOTYPE_${MODULE_NAME}) return() endif() endif() if(MODULE_NO_RECURSE) set(CPP_SEARCH GLOB) else() set(CPP_SEARCH GLOB_RECURSE) endif() set(all_library_cpps) set(all_library_hpps) set(main_name) foreach(subpath ${MODULE_PATH}) file(${CPP_SEARCH} local_library_cpps RELATIVE_PATH "." "${subpath}/*.cpp") file(${CPP_SEARCH} local_library_hpps RELATIVE_PATH "." "${subpath}/*.hpp") file(GLOB local_main_name RELATIVE_PATH "." "${subpath}/main.cpp") if(local_main_name) if(main_name) message(SEND_ERROR "Found main.cpp in different paths for ${MODULE_NAME} (${main_name} and ${local_main_name})") endif() set(main_name ${local_main_name}) list(REMOVE_ITEM local_library_cpps ${local_main_name}) endif() list(APPEND all_library_cpps ${local_library_cpps}) list(APPEND all_library_hpps ${local_library_hpps}) endforeach() list_split(library_test_cpps library_cpps "${all_library_cpps}" "_tests.cpp") list_split(library_test_hpps library_notest_hpps "${all_library_hpps}" "_tests.hpp") list_split(library_detail_hpps library_hpps "${library_notest_hpps}" "detail") list(APPEND library_cpps ${library_detail_hpps}) set(MODULE_TARGETS) if(main_name) message(STATUS " + executable") list(APPEND MODULE_TARGETS ${MODULE_NAME}_prog) add_executable(${MODULE_NAME}_prog ${main_name}) target_link_libraries(${MODULE_NAME}_prog ${MODULE_LINK_LIBRARIES}) set_target_properties(${MODULE_NAME}_prog PROPERTIES OUTPUT_NAME ${MODULE_NAME} ) set(${MODULE_NAME}_MAIN_SOURCES main_name PARENT_SCOPE) endif() if(library_cpps) list(LENGTH library_cpps library_cpps_length) message(STATUS " + library (${library_cpps_length})") list(APPEND MODULE_TARGETS ${MODULE_NAME}) add_library(${MODULE_NAME} SHARED ${library_cpps}) set_target_properties(${MODULE_NAME} PROPERTIES SOVERSION ${PROJECT_SO_VERSION} VERSION ${PROJECT_SO_VERSION} ) target_link_libraries(${MODULE_NAME} ${MODULE_LINK_LIBRARIES}) if(main_name) target_link_libraries(${MODULE_NAME}_prog ${MODULE_NAME}) endif() set(${MODULE_NAME}_LIBRARY_SOURCES ${library_cpps} PARENT_SCOPE) set(${MODULE_NAME}_LIBRARY_HEADERS ${library_hpps} PARENT_SCOPE) endif() if(library_test_cpps) list(LENGTH library_test_cpps library_test_cpps_length) message(STATUS " + test library (${library_test_cpps_length})") list(APPEND MODULE_TARGETS ${MODULE_NAME}_tests) add_library(${MODULE_NAME}_tests SHARED ${library_test_cpps}) set_target_properties(${MODULE_NAME}_tests PROPERTIES SOVERSION ${PROJECT_SO_VERSION} VERSION ${PROJECT_SO_VERSION} ) target_link_libraries(${MODULE_NAME}_tests zkpp-tests) if(library_cpps) target_link_libraries(${MODULE_NAME}_tests ${MODULE_NAME}) endif() target_link_libraries(zkpp-tests_prog ${MODULE_NAME}_tests) endif() if(MODULE_PROTOTYPE) foreach(target ${MODULE_TARGETS}) set_target_properties(${target} PROPERTIES COMPILE_FLAGS "${CMAKE_CXX_FLAGS} -Wno-error" ) endforeach() endif() endfunction() ================================================ FILE: cmake/modules/CodeCoverage.cmake ================================================ # Copyright (c) 2012 - 2015, Lars Bilke # All rights reserved. # # Redistribution and use in source and binary forms, with or without modification, # are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # 3. Neither the name of the copyright holder nor the names of its contributors # may be used to endorse or promote products derived from this software without # specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # # # 2012-01-31, Lars Bilke # - Enable Code Coverage # # 2013-09-17, Joakim Söderberg # - Added support for Clang. # - Some additional usage instructions. # # USAGE: # 0. (Mac only) If you use Xcode 5.1 make sure to patch geninfo as described here: # http://stackoverflow.com/a/22404544/80480 # # 1. Copy this file into your cmake modules path. # # 2. Add the following line to your CMakeLists.txt: # INCLUDE(CodeCoverage) # # 3. Set compiler flags to turn off optimization and enable coverage: # SET(CMAKE_CXX_FLAGS "-g -O0 -fprofile-arcs -ftest-coverage") # SET(CMAKE_C_FLAGS "-g -O0 -fprofile-arcs -ftest-coverage") # # 3. Use the function SETUP_TARGET_FOR_COVERAGE to create a custom make target # which runs your test executable and produces a lcov code coverage report: # Example: # SETUP_TARGET_FOR_COVERAGE( # my_coverage_target # Name for custom target. # test_driver # Name of the test driver executable that runs the tests. # # NOTE! This should always have a ZERO as exit code # # otherwise the coverage generation will not complete. # coverage # Name of output directory. # ) # # 4. Build a Debug build: # cmake -DCMAKE_BUILD_TYPE=Debug .. # make # make my_coverage_target # # # Check prereqs FIND_PROGRAM( GCOV_PATH gcov ) FIND_PROGRAM( LCOV_PATH lcov ) FIND_PROGRAM( GENHTML_PATH genhtml ) FIND_PROGRAM( GCOVR_PATH gcovr PATHS ${CMAKE_SOURCE_DIR}/tests) IF(NOT GCOV_PATH) MESSAGE(FATAL_ERROR "gcov not found! Aborting...") ENDIF() # NOT GCOV_PATH IF(NOT CMAKE_COMPILER_IS_GNUCXX) # Clang version 3.0.0 and greater now supports gcov as well. MESSAGE(WARNING "Compiler is not GNU gcc! Clang Version 3.0.0 and greater supports gcov as well, but older versions don't.") IF(NOT "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") MESSAGE(FATAL_ERROR "Compiler is not GNU gcc! Aborting...") ENDIF() ENDIF() # NOT CMAKE_COMPILER_IS_GNUCXX SET(CMAKE_CXX_FLAGS_COVERAGE "-g -O0 --coverage -fprofile-arcs -ftest-coverage" CACHE STRING "Flags used by the C++ compiler during coverage builds." FORCE ) SET(CMAKE_C_FLAGS_COVERAGE "-g -O0 --coverage -fprofile-arcs -ftest-coverage" CACHE STRING "Flags used by the C compiler during coverage builds." FORCE ) SET(CMAKE_EXE_LINKER_FLAGS_COVERAGE "" CACHE STRING "Flags used for linking binaries during coverage builds." FORCE ) SET(CMAKE_SHARED_LINKER_FLAGS_COVERAGE "" CACHE STRING "Flags used by the shared libraries linker during coverage builds." FORCE ) MARK_AS_ADVANCED( CMAKE_CXX_FLAGS_COVERAGE CMAKE_C_FLAGS_COVERAGE CMAKE_EXE_LINKER_FLAGS_COVERAGE CMAKE_SHARED_LINKER_FLAGS_COVERAGE ) IF ( NOT (CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "Coverage")) MESSAGE( WARNING "Code coverage results with an optimized (non-Debug) build may be misleading" ) ENDIF() # NOT CMAKE_BUILD_TYPE STREQUAL "Debug" # Param _targetname The name of new the custom make target # Param _testrunner The name of the target which runs the tests. # MUST return ZERO always, even on errors. # If not, no coverage report will be created! # Param _outputname lcov output is generated as _outputname.info # HTML report is generated in _outputname/index.html # Optional fourth parameter is passed as arguments to _testrunner # Pass them in list form, e.g.: "-j;2" for -j 2 FUNCTION(SETUP_TARGET_FOR_COVERAGE _targetname _testrunner _outputname) IF(NOT LCOV_PATH) MESSAGE(FATAL_ERROR "lcov not found! Aborting...") ENDIF() # NOT LCOV_PATH IF(NOT GENHTML_PATH) MESSAGE(FATAL_ERROR "genhtml not found! Aborting...") ENDIF() # NOT GENHTML_PATH # Setup target ADD_CUSTOM_TARGET(${_targetname} # Cleanup lcov ${LCOV_PATH} --directory . --zerocounters # Run tests COMMAND ${_testrunner} ${ARGV3} # Capturing lcov counters and generating report COMMAND ${LCOV_PATH} --directory . --capture --output-file ${_outputname}.info COMMAND ${LCOV_PATH} --remove ${_outputname}.info '*_tests.cpp' '*generated*' '/usr/*' --output-file ${_outputname}.info.cleaned COMMAND ${GENHTML_PATH} -o ${_outputname} ${_outputname}.info.cleaned COMMAND ${CMAKE_COMMAND} -E remove ${_outputname}.info ${_outputname}.info.cleaned WORKING_DIRECTORY ${CMAKE_BINARY_DIR} COMMENT "Resetting code coverage counters to zero.\nProcessing code coverage counters and generating report." ) # Show info where to find the report ADD_CUSTOM_COMMAND(TARGET ${_targetname} POST_BUILD COMMAND ; COMMENT "Open ./${_outputname}/index.html in your browser to view the coverage report." ) ENDFUNCTION() # SETUP_TARGET_FOR_COVERAGE ================================================ FILE: cmake/modules/ConfigurationSetting.cmake ================================================ include(CMakeParseArguments) # configuration_setting # Creates a configuration setting, which is configurable via a CMake option. If the option is set to anything # non-default, a macro with the name `ZKPP_${NAME}_USE_${OPTION_VALUE}` is exported as `1`. # # - NAME: The name of the configuration setting. # - DOC: Documentation to place in the CMake configuration GUI. # - DEFUALT: The default value of the configuration setting (some value from OPTIONS). This value must be synced with # the C++ code or the behavior will be nonsense. # - SET: Set the value to this. If unspecified, this will simply be the same as DEFUALT. However, this can be useful in # cases where you wish to specify a non-default based on system information. # - OPTIONS[]: List of valid options to set. function(configuration_setting) set(options) set(oneValueArgs NAME DOC DEFAULT SET) set(multiValueArgs OPTIONS) cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) if(NOT ARG_SET) set(ARG_SET "${ARG_DEFAULT}") endif() set(ZKPP_BUILD_SETTING_${ARG_NAME} "${ARG_SET}" CACHE STRING "${ARG_DOC}" ) if(NOT ${ZKPP_BUILD_SETTING_${ARG_NAME}} IN_LIST ARG_OPTIONS) message(SEND_ERROR "Invalid setting for ${ARG_NAME}: ${ZKPP_BUILD_SETTING_${ARG_NAME}}") endif() if(NOT ${ZKPP_BUILD_SETTING_${ARG_NAME}} STREQUAL ${ARG_DEFAULT}) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DZKPP_${ARG_NAME}_USE_${ZKPP_BUILD_SETTING_${ARG_NAME}}=1" PARENT_SCOPE) endif() message(STATUS " ${ARG_NAME}: ${ZKPP_BUILD_SETTING_${ARG_NAME}}") endfunction() ================================================ FILE: cmake/modules/ListSplit.cmake ================================================ # list_split # Split a list into values matching or not matching a given expression. macro(list_split matching non_matching orig expression) foreach(val ${orig}) if(${val} MATCHES ${expression}) list(APPEND ${matching} ${val}) else() list(APPEND ${non_matching} ${val}) endif() endforeach() endmacro() ================================================ FILE: cmake/modules/ZooKeeper.cmake ================================================ # Utilities for configuring and running a ZooKeeper Server. cmake_minimum_required(VERSION 3.5) include(CMakeParseArguments) find_package(Java COMPONENTS Runtime) if(NOT Java_Runtime_FOUND) message(FATAL_ERROR "Could not find Java Runtime") endif() # execute_jar # Similar to execute_process, but drops "java -jar" in front for you so you can execute a JAR file in the same way you # would a process. macro(execute_jar) execute_process(COMMAND "${Java_JAVA_EXECUTABLE}" "-jar" ${ARGN}) endmacro() # execute_java_cp # Similar to execute_process, but drops "java -cp" in front for you so you can execute a collection of Java locations in # the same way you would a process (albeit more annoyingly). macro(execute_java_cp) execute_process(COMMAND "${Java_JAVA_EXECUTABLE}" "-cp" ${ARGN}) endmacro() find_program(IVY_JAR NAMES ivy.jar PATHS "/usr/share/java" ) if(NOT IVY_JAR) message(FATAL_ERROR "Could not find Apache Ivy") endif() # find_zookeeper_server # Get the ZooKeeper server JARs from Ivy. # # VERSION: ZooKeeper version to fetch. This can be any Ivy pattern (for example, "3.5+"). # OUTPUT_CLASSPATH: A variable to output a classpath that can be used to run the server. # # Example: # # find_zookeeper_server(VERSION "3.5+" OUTPUT_CLASSPATH ZOOKEEPER_SERVER_CLASSPATH) # execute_java_cp("${ZOOKEEPER_SERVER_CLASSPATH}" "org.apache.zookeeper.server.quorum.QuorumPeerMain" 2181 zk-data) function(find_zookeeper_server) set(options) set(oneValueArgs VERSION OUTPUT_CLASSPATH) set(multiValueArgs) cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) if(NOT ARG_VERSION) message(FATAL_ERROR "You must specify a VERSION to fetch") endif() if(NOT ARG_OUTPUT_CLASSPATH) message(FATAL_ERROR "You must specify an OUTPUT_CLASSPATH") endif() file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/ivy-cache") set(TEMP_CLASSPATH_FILE "${CMAKE_CURRENT_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/ivy-cache/zookeeper-${ARG_VERSION}.txt") # There does not appear to be a good way to tell Ivy to be reasonable about progress reporting, so we'll silence it # completely and hope the ellipsis in the status message will prevent people from thinking the system hung. message(STATUS "Fetching ZooKeeper Server ${ARG_VERSION}...") execute_jar(${IVY_JAR} "-dependency" "org.apache.zookeeper" "zookeeper" "${ARG_VERSION}" "-cachepath" "${TEMP_CLASSPATH_FILE}" OUTPUT_VARIABLE IVY_FETCH_OUTPUT ERROR_VARIABLE IVY_FETCH_ERROR ) if(EXISTS "${TEMP_CLASSPATH_FILE}") file(READ "${TEMP_CLASSPATH_FILE}" CLASSPATH) string(STRIP "${CLASSPATH}" CLASSPATH) message(STATUS " > SUCCESS!") set(${ARG_OUTPUT_CLASSPATH} "${CLASSPATH}" PARENT_SCOPE) else() message(SEND_ERROR "Could not fetch ZooKeeper Server ${ARG_VERSION}\n" "Ivy fetch output:\n${IVY_FETCH_OUTPUT}\n" "Ivy fetch errors:\n${IVY_FETCH_ERROR}" ) endif() endfunction() ================================================ FILE: config/dev-env ================================================ #!/bin/bash -e # Create a build environment. PROJECT_ROOT=$(readlink -f $(dirname $0)/..) COMMAND=/bin/bash DISTRO= function show_usage { echo "$0: Run a program in a Docker environment." echo "" echo "Options:" echo "" echo " --distro DISTRO:" echo " The Linux distro to create the base container." echo "" echo " Available options:" printf " " printf " %s" $(ls $(dirname $0)/docker) echo "" echo " --:" echo " Stop processing arguments and pass the remaining arguments to the" echo " container." echo "" echo "Example:" echo "" echo " Enter into a bash shell:" echo " $> $0 $(ls $(dirname $0)/docker | sort --random-sort | head -1)" echo "" echo " Build the package for a given distro:" echo " $> $0 --distro=$(ls $(dirname $0)/docker | sort --random-sort | head -1) -- ./config/make-package" } if [[ $# -lt 1 ]]; then show_usage exit 1 else UNRECOGNIZED=0 while [[ $# -gt 0 ]]; do key="$1" case $key in --distro) DISTRO="$2" shift 2 ;; --distro=*) DISTRO="${key/--distro=/}" shift ;; --help) show_usage exit 1 ;; --) shift COMMAND=$@ break ;; *) if [[ -z "${DISTRO}" ]]; then DISTRO="$key" else echo "Unrecognized option: $key" UNRECOGNIZED=1 fi shift ;; esac done fi if [[ -z "${DISTRO}" ]]; then echo "Must set distro." show_usage exit 1 fi IMAGE_NAME=dev/zookeeper-cpp/${DISTRO} DOCKER_DIR=${PROJECT_ROOT}/config/docker/${DISTRO} if ! [[ -e ${DOCKER_DIR}/Dockerfile ]]; then echo "Specified distro \"${DISTRO}\" does not have a Dockerfile" echo "" show_usage exit 1 fi docker build ${DOCKER_DIR} -t ${IMAGE_NAME} exec docker run \ --rm \ -v ${PROJECT_ROOT}:/root/zookeeper-cpp \ -w /root/zookeeper-cpp \ --security-opt seccomp=unconfined \ -it ${IMAGE_NAME} \ ${COMMAND} ================================================ FILE: config/docker/debian-10/Dockerfile ================================================ FROM debian:10 LABEL maintainer="Travis Gockel " RUN apt-get update \ && apt-get install --yes \ cmake \ g++ \ grep \ googletest \ ivy \ lcov \ libgtest-dev \ libzookeeper-mt-dev \ ninja-build CMD ["/root/zookeeper-cpp/config/run-tests"] ================================================ FILE: config/docker/opensuse-15/Dockerfile ================================================ FROM opensuse/leap:15 LABEL maintainer="Travis Gockel " RUN zypper addrepo -f "https://download.opensuse.org/repositories/server:/database/openSUSE_Leap_15.2/" "serverdatabase" \ && zypper --no-gpg-checks \ install -y \ apache-ivy \ cmake \ grep \ gcc-c++ \ git \ googletest-devel \ java-1_8_0-openjdk \ lcov \ libzookeeper2-devel \ ninja CMD ["/root/zookeeper-cpp/config/run-tests"] ================================================ FILE: config/docker/ubuntu-16.04/Dockerfile ================================================ FROM ubuntu:16.04 LABEL maintainer="Travis Gockel " RUN apt-get update RUN apt-get install --yes software-properties-common RUN add-apt-repository --yes ppa:ubuntu-toolchain-r/test # You might ask why g++-6 is installed, even when we intend to build with GCC 7. It turns out that Coveralls won't work # if it isn't installed due to...something. Instead of root causing the issue, we're going to ignore it and hope it gets # fixed in a future release. RUN apt-get update \ && apt-get install --yes \ cmake \ grep \ g++-6 \ g++-7 \ ivy \ libgtest-dev \ libzookeeper-mt-dev \ ninja-build # Code Coverage RUN apt-get install --yes \ git \ lcov \ python-pip RUN pip install --upgrade pip RUN pip install \ cpp-coveralls \ pyyaml RUN update-alternatives --install /usr/bin/c++ c++ /usr/bin/g++-7 99 CMD ["/root/zookeeper-cpp/config/run-tests"] ================================================ FILE: config/docker/ubuntu-18.04/Dockerfile ================================================ FROM ubuntu:18.04 LABEL maintainer="Travis Gockel " RUN apt-get update \ && apt-get install --yes \ cmake \ grep \ googletest \ g++-7 \ ivy \ lcov \ libgtest-dev \ libzookeeper-mt-dev \ ninja-build RUN update-alternatives --install /usr/bin/c++ c++ /usr/bin/g++-7 99 CMD ["/root/zookeeper-cpp/config/run-tests"] ================================================ FILE: config/docker/ubuntu-20.04/Dockerfile ================================================ FROM ubuntu:20.04 LABEL maintainer="Travis Gockel " RUN apt-get update \ && DEBIAN_FRONTEND=noninteractive \ apt-get install --yes \ cmake \ g++ \ grep \ googletest \ ivy \ lcov \ libgtest-dev \ libzookeeper-mt-dev \ ninja-build CMD ["/root/zookeeper-cpp/config/run-tests"] ================================================ FILE: config/make-doxygen ================================================ #!/bin/bash -e mkdir -p build/doc doxygen doc/Doxyfile ================================================ FILE: config/make-package ================================================ #!/bin/bash -e PROJECT_ROOT=$(readlink -f $(dirname $0)/..) BUILD_DIR="$BUILD_DIR" COPY_TO="" DISTRO="$DISTRO" PACKAGE_PREFIX="" function show_usage { echo "$0: Create a package for this operating system." echo "" echo "Usage: $0 [OPTION]..." echo "" echo "Options:" echo "" echo " --build-dir BUILD_DIR:" echo " Set the build output directory. By default, this is build-release or" echo " build-release-\${DISTRO} if --distro was set." echo " --copy-to COPY_TO:" echo " Copy the generated packages to the specified folder." echo " --distro DISTRO:" echo " Set the distro name." echo " --dockerize DISTRO:" echo " Perform the build inside of a dev-env-created Docker container. If used," echo " this must be the first option. The --distro flag is automatically set to" echo " the same value." echo " --package-prefix PREFIX:" echo " Generated packages will be renamed with this prefix. By default, this" echo " will be \"\${DISTRO}-\" if --distro was set." echo "" echo "Examples:" echo "" echo " Create a package inside an Ubuntu 18.04 container, copying the DEBs to a" echo " packages folder." echo " $> $0 --dockerize=ubuntu-18.04 --copy-to=packages" } UNRECOGNIZED=0 DOCKERIZE=0 FIRST=1 while [[ $# -gt 0 ]]; do key="$1" case $key in --build-dir) BUILD_DIR="$2" shift 2 ;; --build-dir=*) BUILD_DIR="${key/--build-dir=/}" shift ;; --copy-to) COPY_TO="$2" shift 2 ;; --copy-to=*) COPY_TO="${key/--copy-to=/}" shift ;; --distro) DISTRO="$2" shift 2 ;; --distro=*) DISTRO="${key/--distro=/}" shift ;; --dockerize) DISTRO="$2" DOCKERIZE=1 shift 2 break ;; --dockerize=*) DISTRO="${key/--dockerize=/}" DOCKERIZE=1 shift break ;; --help) show_usage exit 1 ;; --package-prefix) PACKAGE_PREFIX="$2" shift 2 ;; --package-prefix=*) PACKAGE_PREFIX="${key/--package-prefix=/}" shift ;; *) echo "Unrecognized option: $key" UNRECOGNIZED=1 shift ;; esac FIRST=0 done if [[ ${UNRECOGNIZED} -ne 0 ]]; then show_usage exit 1 fi if [[ ${DOCKERIZE} -eq 1 ]]; then if [[ ${FIRST} -eq 0 ]]; then echo "If using --dockerize, it must be the first argument." exit 1 fi exec ${PROJECT_ROOT}/config/dev-env "${DISTRO}" -- \ ./config/make-package \ --distro="${DISTRO}" \ $@ fi if [[ -z "${BUILD_DIR}" ]]; then if [[ -z "${DISTRO}" ]]; then BUILD_DIR="build-release" else BUILD_DIR="build-release-${DISTRO}" fi fi if [[ -z "${PACKAGE_PREFIX}" ]]; then if [[ -n "${DISTRO}" ]]; then PACKAGE_PREFIX="${DISTRO}-" fi fi if $(hash rpm 2>/dev/null); then PACKAGE_SYSTEM=RPM PACKAGE_SUFFIX=rpm function show_contents { rpm -qlp "$1" } elif $(hash dpkg 2>/dev/null); then PACKAGE_SYSTEM=DEB PACKAGE_SUFFIX=deb function show_contents { dpkg --contents "${pkg_file}" } else echo "Unknown packaging system for this operating system" exit 2 fi if [[ -e ${BUILD_DIR} ]]; then echo "Build directory (${BUILD_DIR}) unclean -- deleting it" rm -rf ${BUILD_DIR} fi mkdir ${BUILD_DIR} cd ${BUILD_DIR} cmake .. -DZKPP_PACKAGE_SYSTEM=${PACKAGE_SYSTEM} -DCMAKE_BUILD_TYPE=Release -GNinja ninja package cd - for pkg_file in $(find "${BUILD_DIR}/install" -maxdepth 1 -name "*.${PACKAGE_SUFFIX}"); do if [[ -n "${PACKAGE_PREFIX}" ]]; then new_pkg_file="$(dirname -- ${pkg_file})/${PACKAGE_PREFIX}$(basename -- ${pkg_file})" mv "${pkg_file}" "${new_pkg_file}" pkg_file="${new_pkg_file}" fi echo "PACKAGE ${pkg_file}" show_contents "${pkg_file}" if [[ -n "${COPY_TO}" ]]; then cp "${pkg_file}" "${COPY_TO}" fi done ================================================ FILE: config/make-packages ================================================ #!/bin/bash -e PROJECT_ROOT=$(readlink -f $(dirname $0)/..) function show_usage { echo "$0: Create multiple packages." echo "" echo "Usage: $0 [OPTION]... [DISTRO]..." echo "" echo "Options:" echo "" echo " --all" echo " Create packages for all known distributions. If this is specified, you" echo " are not allowed to specify individual distributions." echo " --copy-to COPY_TO:" echo " Copy the generated packages to the specified folder." echo " --:" echo " Stop processing and pass the remaining arguments to make-package." } COPY_TO= DISTROS=() ALL=0 while [[ $# -gt 0 ]]; do key="$1" case $key in --all) ALL=1 shift ;; --copy-to) COPY_TO="$2" shift 2 ;; --copy-to=*) COPY_TO="${key/--copy-to=/}" shift ;; --help) show_usage exit 1 ;; --) # No shift: want to use the -- in the call break ;; *) DISTROS+=(${key}) shift ;; esac done if [[ ${ALL} -eq 1 ]]; then if [[ ${#DISTROS[@]} -ne 0 ]]; then echo "Specified --all, but also distributions: ${DISTROS[@]}" echo "" show_usage exit 1 fi # All distros with working package systems. DISTROS=(ubuntu-16.04 ubuntu-17.10 ubuntu-18.04 ubuntu-18.10 debian-9) fi if [[ -n "${COPY_TO}" ]]; then mkdir -p "${COPY_TO}" COPY_TO="--copy-to=${COPY_TO}" fi for distro in ${DISTROS[@]}; do echo "BUILDING PACKAGE FOR $distro" ${PROJECT_ROOT}/config/make-package --dockerize=${distro} ${COPY_TO} $@ done ================================================ FILE: config/publish-doxygen ================================================ #!/bin/bash -e # Settings REPO_PATH=git@github.com:tgockel/zookeeper-cpp.git HTML_PATH=build/doc/html COMMIT_USER="Documentation Builder" COMMIT_EMAIL="travis@gockelhut.com" CHANGESET=$(git rev-parse --verify HEAD) # Get a clean version of the HTML documentation repo. rm -rf ${HTML_PATH} mkdir -p ${HTML_PATH} git clone -b gh-pages "${REPO_PATH}" --single-branch ${HTML_PATH} # rm all the files through git to prevent stale files. cd ${HTML_PATH} git rm -rf *.html *.js *.png *.css search cd - # Generate the HTML documentation. ./config/make-doxygen # Create and commit the documentation repo. cd ${HTML_PATH} git add . git config user.name "${COMMIT_USER}" git config user.email "${COMMIT_EMAIL}" git commit -m "Automated documentation build for changeset ${CHANGESET}." git push origin gh-pages cd - ================================================ FILE: config/run-tests ================================================ #!/bin/bash -e PROJECT_ROOT=$(readlink -f $(dirname $0)/..) BUILD_DIR=${PROJECT_ROOT}/build-ci echo "${DISTRO} ${PROJECT_ROOT}" c++ --version if [[ -e ${BUILD_DIR} ]]; then echo "Build directory (${BUILD_DIR}) unclean -- deleting it" rm -rf ${BUILD_DIR} fi mkdir ${BUILD_DIR} cd ${BUILD_DIR} if $(hash lcov 2>/dev/null); then # TODO(https://github.com/tgockel/zookeeper-cpp/issues/42): Disabling Coveralls coverage for now. COVERAGE=0 else COVERAGE=0 fi cmake .. -DZKPP_BUILD_OPTION_CODE_COVERAGE=${COVERAGE} make VERBOSE=1 make test if [[ ${COVERAGE} -eq 1 ]]; then ${PROJECT_ROOT}/config/upload-coverage ${BUILD_DIR} fi ================================================ FILE: config/upload-coverage ================================================ #!/bin/bash -e # TODO: 99% sure there is a better way to do this GCC_VERSION_MAJOR=$(c++ --version | grep -Po '\d\.\d\.\d' | grep -Po '^\d+' | head -1) ls -l $1/../coveralls-repo-token COVERALLS_REPO_TOKEN=$(cat $1/../coveralls-repo-token) TRAVIS=true CI=true coveralls \ --repo-token="${COVERALLS_REPO_TOKEN}" \ --build-root $1 \ --exclude-pattern '_test\.cpp$' \ --exclude-pattern bits \ --gcov-options '\-lp' --gcov "gcov-${GCC_VERSION_MAJOR}" ================================================ FILE: doc/Doxyfile ================================================ #--------------------------------------------------------------------------- # Project related configuration options #--------------------------------------------------------------------------- DOXYFILE_ENCODING = UTF-8 PROJECT_NAME = "zookeeper-cpp" PROJECT_NUMBER = PROJECT_BRIEF = "ZooKeeper Client for C++" PROJECT_LOGO = OUTPUT_DIRECTORY = build/doc CREATE_SUBDIRS = NO OUTPUT_LANGUAGE = English BRIEF_MEMBER_DESC = YES REPEAT_BRIEF = YES ABBREVIATE_BRIEF = ALWAYS_DETAILED_SEC = YES INLINE_INHERITED_MEMB = NO FULL_PATH_NAMES = YES STRIP_FROM_PATH = src STRIP_FROM_INC_PATH = src SHORT_NAMES = NO JAVADOC_AUTOBRIEF = YES QT_AUTOBRIEF = NO MULTILINE_CPP_IS_BRIEF = NO INHERIT_DOCS = YES SEPARATE_MEMBER_PAGES = NO TAB_SIZE = 4 ALIASES = TCL_SUBST = OPTIMIZE_OUTPUT_FOR_C = NO OPTIMIZE_OUTPUT_JAVA = NO OPTIMIZE_FOR_FORTRAN = NO OPTIMIZE_OUTPUT_VHDL = NO EXTENSION_MAPPING = MARKDOWN_SUPPORT = YES AUTOLINK_SUPPORT = YES BUILTIN_STL_SUPPORT = YES CPP_CLI_SUPPORT = NO SIP_SUPPORT = NO IDL_PROPERTY_SUPPORT = YES DISTRIBUTE_GROUP_DOC = NO SUBGROUPING = YES INLINE_GROUPED_CLASSES = NO INLINE_SIMPLE_STRUCTS = NO TYPEDEF_HIDES_STRUCT = NO LOOKUP_CACHE_SIZE = 0 #--------------------------------------------------------------------------- # Build related configuration options #--------------------------------------------------------------------------- EXTRACT_ALL = NO EXTRACT_PRIVATE = NO EXTRACT_STATIC = NO EXTRACT_LOCAL_CLASSES = YES EXTRACT_LOCAL_METHODS = NO EXTRACT_ANON_NSPACES = NO HIDE_UNDOC_MEMBERS = NO HIDE_UNDOC_CLASSES = NO HIDE_FRIEND_COMPOUNDS = NO HIDE_IN_BODY_DOCS = NO INTERNAL_DOCS = NO CASE_SENSE_NAMES = YES HIDE_SCOPE_NAMES = NO SHOW_INCLUDE_FILES = YES FORCE_LOCAL_INCLUDES = NO INLINE_INFO = YES SORT_MEMBER_DOCS = YES SORT_BRIEF_DOCS = NO SORT_MEMBERS_CTORS_1ST = YES SORT_GROUP_NAMES = YES SORT_BY_SCOPE_NAME = NO STRICT_PROTO_MATCHING = NO GENERATE_TODOLIST = YES GENERATE_TESTLIST = YES GENERATE_BUGLIST = YES GENERATE_DEPRECATEDLIST= YES ENABLED_SECTIONS = MAX_INITIALIZER_LINES = 30 SHOW_USED_FILES = YES SHOW_DIRECTORIES = NO SHOW_FILES = YES SHOW_NAMESPACES = YES FILE_VERSION_FILTER = LAYOUT_FILE = CITE_BIB_FILES = #--------------------------------------------------------------------------- # Configuration options related to warning and progress messages #--------------------------------------------------------------------------- QUIET = NO WARNINGS = YES WARN_IF_UNDOCUMENTED = YES WARN_IF_DOC_ERROR = YES WARN_NO_PARAMDOC = NO WARN_FORMAT = "$file:$line: $text" WARN_LOGFILE = #--------------------------------------------------------------------------- # Configuration options related to the input files #--------------------------------------------------------------------------- INPUT = src README.md INPUT_ENCODING = UTF-8 FILE_PATTERNS = RECURSIVE = YES EXCLUDE = EXCLUDE_SYMLINKS = NO EXCLUDE_PATTERNS = *_tests.cpp *_tests.hpp tests detail EXCLUDE_SYMBOLS = EXAMPLE_PATH = EXAMPLE_PATTERNS = EXAMPLE_RECURSIVE = NO IMAGE_PATH = INPUT_FILTER = FILTER_PATTERNS = FILTER_SOURCE_FILES = NO FILTER_SOURCE_PATTERNS = USE_MDFILE_AS_MAINPAGE = README.md #--------------------------------------------------------------------------- # Configuration options related to source browsing #--------------------------------------------------------------------------- SOURCE_BROWSER = YES INLINE_SOURCES = NO STRIP_CODE_COMMENTS = YES REFERENCED_BY_RELATION = NO REFERENCES_RELATION = NO REFERENCES_LINK_SOURCE = YES USE_HTAGS = NO VERBATIM_HEADERS = YES #--------------------------------------------------------------------------- # Configuration options related to the alphabetical class index #--------------------------------------------------------------------------- ALPHABETICAL_INDEX = YES COLS_IN_ALPHA_INDEX = 5 IGNORE_PREFIX = #--------------------------------------------------------------------------- # Configuration options related to the HTML output #--------------------------------------------------------------------------- GENERATE_HTML = YES HTML_OUTPUT = html HTML_FILE_EXTENSION = .html HTML_HEADER = HTML_FOOTER = HTML_STYLESHEET = HTML_EXTRA_STYLESHEET = HTML_EXTRA_FILES = HTML_COLORSTYLE_HUE = 220 HTML_COLORSTYLE_SAT = 100 HTML_COLORSTYLE_GAMMA = 80 HTML_TIMESTAMP = NO HTML_DYNAMIC_SECTIONS = YES HTML_INDEX_NUM_ENTRIES = 100 GENERATE_DOCSET = NO DOCSET_FEEDNAME = "Doxygen generated docs" DOCSET_BUNDLE_ID = com.gockelhut.zookeeper-cpp DOCSET_PUBLISHER_ID = com.gockelhut DOCSET_PUBLISHER_NAME = Gockel GENERATE_HTMLHELP = NO CHM_FILE = HHC_LOCATION = GENERATE_CHI = NO CHM_INDEX_ENCODING = BINARY_TOC = NO TOC_EXPAND = NO GENERATE_QHP = NO QCH_FILE = QHP_NAMESPACE = org.doxygen.Project QHP_VIRTUAL_FOLDER = doc QHP_CUST_FILTER_NAME = QHP_CUST_FILTER_ATTRS = QHP_SECT_FILTER_ATTRS = QHG_LOCATION = GENERATE_ECLIPSEHELP = NO ECLIPSE_DOC_ID = com.gockelhut.zookeeper-cpp DISABLE_INDEX = NO GENERATE_TREEVIEW = YES ENUM_VALUES_PER_LINE = 4 USE_INLINE_TREES = NO TREEVIEW_WIDTH = 250 EXT_LINKS_IN_WINDOW = NO FORMULA_FONTSIZE = 10 FORMULA_TRANSPARENT = YES USE_MATHJAX = NO MATHJAX_RELPATH = http://www.mathjax.org/mathjax MATHJAX_EXTENSIONS = SEARCHENGINE = YES SERVER_BASED_SEARCH = NO EXTERNAL_SEARCH = NO SEARCHENGINE_URL = SEARCHDATA_FILE = searchdata.xml EXTERNAL_SEARCH_ID = EXTRA_SEARCH_MAPPINGS = #--------------------------------------------------------------------------- # configuration options related to the LaTeX output #--------------------------------------------------------------------------- GENERATE_LATEX = NO LATEX_OUTPUT = latex LATEX_CMD_NAME = latex MAKEINDEX_CMD_NAME = makeindex COMPACT_LATEX = NO PAPER_TYPE = a4 EXTRA_PACKAGES = LATEX_HEADER = LATEX_FOOTER = PDF_HYPERLINKS = YES USE_PDFLATEX = YES LATEX_BATCHMODE = NO LATEX_HIDE_INDICES = NO LATEX_SOURCE_CODE = NO LATEX_BIB_STYLE = plain #--------------------------------------------------------------------------- # configuration options related to the RTF output #--------------------------------------------------------------------------- GENERATE_RTF = NO RTF_OUTPUT = rtf COMPACT_RTF = NO RTF_HYPERLINKS = NO RTF_STYLESHEET_FILE = RTF_EXTENSIONS_FILE = #--------------------------------------------------------------------------- # configuration options related to the man page output #--------------------------------------------------------------------------- GENERATE_MAN = NO MAN_OUTPUT = man MAN_EXTENSION = .3 MAN_LINKS = NO #--------------------------------------------------------------------------- # configuration options related to the XML output #--------------------------------------------------------------------------- GENERATE_XML = NO XML_OUTPUT = xml XML_SCHEMA = XML_DTD = XML_PROGRAMLISTING = YES #--------------------------------------------------------------------------- # configuration options for the AutoGen Definitions output #--------------------------------------------------------------------------- GENERATE_AUTOGEN_DEF = NO #--------------------------------------------------------------------------- # configuration options related to the Perl module output #--------------------------------------------------------------------------- GENERATE_PERLMOD = NO PERLMOD_LATEX = NO PERLMOD_PRETTY = YES PERLMOD_MAKEVAR_PREFIX = #--------------------------------------------------------------------------- # Configuration options related to the preprocessor #--------------------------------------------------------------------------- ENABLE_PREPROCESSING = YES MACRO_EXPANSION = NO EXPAND_ONLY_PREDEF = NO SEARCH_INCLUDES = YES INCLUDE_PATH = INCLUDE_FILE_PATTERNS = PREDEFINED = EXPAND_AS_DEFINED = SKIP_FUNCTION_MACROS = YES #--------------------------------------------------------------------------- # Configuration::additions related to external references #--------------------------------------------------------------------------- TAGFILES = GENERATE_TAGFILE = ALLEXTERNALS = NO EXTERNAL_GROUPS = YES PERL_PATH = /usr/bin/perl #--------------------------------------------------------------------------- # Configuration options related to the dot tool #--------------------------------------------------------------------------- CLASS_DIAGRAMS = YES MSCGEN_PATH = HIDE_UNDOC_RELATIONS = YES HAVE_DOT = NO DOT_NUM_THREADS = 0 DOT_FONTNAME = Helvetica DOT_FONTSIZE = 10 DOT_FONTPATH = CLASS_GRAPH = YES COLLABORATION_GRAPH = YES GROUP_GRAPHS = YES UML_LOOK = NO TEMPLATE_RELATIONS = NO INCLUDE_GRAPH = YES INCLUDED_BY_GRAPH = YES CALL_GRAPH = YES CALLER_GRAPH = NO GRAPHICAL_HIERARCHY = YES DIRECTORY_GRAPH = YES DOT_IMAGE_FORMAT = svg INTERACTIVE_SVG = YES DOT_PATH = DOTFILE_DIRS = . MSCFILE_DIRS = DOT_GRAPH_MAX_NODES = 50 MAX_DOT_GRAPH_DEPTH = 0 DOT_TRANSPARENT = NO DOT_MULTI_TARGETS = YES GENERATE_LEGEND = YES DOT_CLEANUP = YES ================================================ FILE: install/deb/libzkpp/control.in ================================================ Package: libzkpp@PROJECT_SO_VERSION@ Version: @PROJECT_PACKAGE_VERSION@ Priority: optional Maintainer: Travis Gockel Architecture: @PROJECT_BUILD_ARCHITECTURE@ Section: libs Depends: libzookeeper-mt2 (>= 3.4), libc6 (>= 2.18), libstdc++6 (>= 7-20170407) Build-Depends: debhelper (>= 9), cmake (>= 3.5), libzookeeper-mt-dev (>= 3.4) Homepage: https://tgockel.github.io/zookeeper-cpp/ Description: A ZooKeeper client for C++. ================================================ FILE: install/deb/libzkpp/postinst.in ================================================ #!/bin/sh -e update-alternatives --install @CMAKE_INSTALL_PREFIX@/lib/libzkpp.so libzkpp @CMAKE_INSTALL_PREFIX@/lib/libzkpp.so.@PROJECT_SO_VERSION@ 10 ================================================ FILE: install/deb/libzkpp/shlibs.in ================================================ libzkpp 0.2 libzkpp0.2 (>= 0.2.3) ================================================ FILE: install/deb/libzkpp-dev/control.in ================================================ Package: libzkpp@PROJECT_SO_VERSION@-dev Version: @PROJECT_PACKAGE_VERSION@ Priority: optional Maintainer: Travis Gockel Architecture: all Section: libdevel Depends: libzkpp@PROJECT_SO_VERSION@ (= @PROJECT_PACKAGE_VERSION@) Description: development files for zkpp A ZooKeeper client for C++. ================================================ FILE: install/deb/libzkpp-server/control.in ================================================ Package: libzkpp-server@PROJECT_SO_VERSION@ Version: @PROJECT_PACKAGE_VERSION@ Priority: optional Maintainer: Travis Gockel Architecture: @PROJECT_BUILD_ARCHITECTURE@ Section: libs Depends: libzkpp@PROJECT_SO_VERSION@ (= @PROJECT_PACKAGE_VERSION@) Build-Depends: debhelper (>= 9), cmake (>= 3.5) Homepage: https://tgockel.github.io/zookeeper-cpp/ Description: Control a ZooKeeper Java process on a single machine. ================================================ FILE: install/deb/libzkpp-server/postinst.in ================================================ #!/bin/sh -e update-alternatives --install @CMAKE_INSTALL_PREFIX@/lib/libzkpp-server.so libzkpp-server @CMAKE_INSTALL_PREFIX@/lib/libzkpp-server.so.@PROJECT_SO_VERSION@ 10 ================================================ FILE: install/deb/libzkpp-server-dev/control.in ================================================ Package: libzkpp-server@PROJECT_SO_VERSION@-dev Version: @PROJECT_PACKAGE_VERSION@ Priority: optional Maintainer: Travis Gockel Architecture: all Section: libdevel Depends: libzkpp@PROJECT_SO_VERSION@-dev (= @PROJECT_PACKAGE_VERSION@), libzkpp-server@PROJECT_SO_VERSION@ (= @PROJECT_PACKAGE_VERSION@) Description: development files for zkpp_server@PROJECT_SO_VERSION@ Control a ZooKeeper Java process on a single machine. ================================================ FILE: src/zk/acl.cpp ================================================ #include "acl.hpp" #include #include #include #include namespace zk { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // permission // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// std::ostream& operator<<(std::ostream& os, const permission& self) { if (self == permission::none) return os << "none"; else if (self == permission::all) return os << "all"; bool first = true; auto tick = [&] { return std::exchange(first, false) ? "" : "|"; }; if (allows(self, permission::read)) os << tick() << "read"; if (allows(self, permission::write)) os << tick() << "write"; if (allows(self, permission::create)) os << tick() << "create"; if (allows(self, permission::erase)) os << tick() << "erase"; if (allows(self, permission::admin)) os << tick() << "admin"; return os; } std::string to_string(const permission& self) { std::ostringstream os; os << self; return os.str(); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // acl_rule // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// acl_rule::acl_rule(std::string scheme, std::string id, permission permissions) : _scheme(std::move(scheme)), _id(std::move(id)), _permissions(permissions) { } acl_rule::~acl_rule() noexcept { } std::size_t hash(const acl_rule& self) { return std::hash{}(self.scheme()) ^ std::hash{}(self.id()) ^ std::hash{}(static_cast(self.permissions())); } bool operator==(const acl_rule& lhs, const acl_rule& rhs) { return lhs.scheme() == rhs.scheme() && lhs.id() == rhs.id() && lhs.permissions() == rhs.permissions(); } bool operator!=(const acl_rule& lhs, const acl_rule& rhs) { return !(lhs == rhs); } bool operator< (const acl_rule& lhs, const acl_rule& rhs) { return std::tie(lhs.scheme(), lhs.id(), lhs.permissions()) < std::tie(rhs.scheme(), rhs.id(), rhs.permissions()); } bool operator<=(const acl_rule& lhs, const acl_rule& rhs) { return !(rhs < lhs); } bool operator> (const acl_rule& lhs, const acl_rule& rhs) { return rhs < lhs; } bool operator>=(const acl_rule& lhs, const acl_rule& rhs) { return !(lhs < rhs); } std::ostream& operator<<(std::ostream& os, const acl_rule& self) { os << '(' << self.scheme(); if (!self.id().empty()) os << ':' << self.id(); os << ", " << self.permissions() << ')'; return os; } std::string to_string(const acl_rule& self) { std::ostringstream os; os << self; return os.str(); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // acl // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// acl::acl(std::vector rules) noexcept : _impl(std::move(rules)) { } acl::~acl() noexcept { } bool operator==(const acl& lhs, const acl& rhs) { return std::equal(lhs.cbegin(), lhs.cend(), rhs.cbegin(), rhs.cend()); } bool operator!=(const acl& lhs, const acl& rhs) { return !(lhs == rhs); } std::ostream& operator<<(std::ostream& os, const acl& self) { os << '['; bool first = true; for (const auto& x : self) { if (first) first = false; else os << ", "; os << x; } return os << ']'; } std::string to_string(const acl& self) { std::ostringstream os; os << self; return os.str(); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // acls // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// const acl& acls::creator_all() { static acl instance = { { "auth", "", permission::all } }; return instance; } const acl& acls::open_unsafe() { static acl instance = { { "world", "anyone", permission::all } }; return instance; } const acl& acls::read_unsafe() { static acl instance = { { "world", "anyone", permission::read } }; return instance; } } ================================================ FILE: src/zk/acl.hpp ================================================ #pragma once #include #include #include #include #include #include #include "forwards.hpp" namespace zk { /// \addtogroup Client /// \{ /// Describes the ability of a user to perform a certain action. Permissions can be mixed together like integers with /// \c | and \c &. enum class permission : unsigned int { none = 0b00000, //!< No permissions are set (server could have been configured without ACL support). read = 0b00001, //!< You can access the data of a node and can list its children. write = 0b00010, //!< You can set the data of a node. create = 0b00100, //!< You can create a child node. erase = 0b01000, //!< You can erase a child node (but not necessarily this one). admin = 0b10000, //!< You can alter permissions on this node. all = 0b11111, //!< You can do anything. }; /// \{ /// Set union operation of \ref permission. inline constexpr permission operator|(permission a, permission b) { return permission(static_cast(a) | static_cast(b)); } inline constexpr permission& operator|=(permission& self, permission mask) { return self = self | mask; } /// \} /// \{ /// Set intersection operation of \ref permission. inline constexpr permission operator&(permission a, permission b) { return permission(static_cast(a) & static_cast(b)); } inline constexpr permission& operator&=(permission& self, permission mask) { return self = self & mask; } /// \} /// Set inverse operation of \ref permission. This is not exactly the bitwise complement of \a a, as the returned value /// will only include bits set that are valid in \ref permission discriminants. inline constexpr permission operator~(permission a) { return permission(~static_cast(a)) & permission::all; } /// Check that \a self allows it \a perform all operations. For example, /// `allows(permission::read | permission::write, permission::read)` will be \c true, as `read|write` is allowed to /// \c read. inline constexpr bool allows(permission self, permission perform) { return (self & perform) == perform; } std::ostream& operator<<(std::ostream&, const permission&); std::string to_string(const permission&); /// An individual rule in an \ref acl. It consists of a \ref scheme and \ref id pair to identify the who and a /// \ref permission set to determine what they are allowed to do. /// /// See "Builtin ACL /// Schemes" in the ZooKeeper Programmer's Guide for more information. class acl_rule final { public: /// Create an ACL under the given \a scheme and \a id with the given \a permissions. acl_rule(std::string scheme, std::string id, permission permissions); acl_rule(const acl_rule&) = default; acl_rule& operator=(const acl_rule&) = default; acl_rule(acl_rule&&) = default; acl_rule& operator=(acl_rule&&) = default; ~acl_rule() noexcept; /// The authentication scheme this list is used for. The most common scheme is `"auth"`, which allows any /// authenticated user to perform actions (see \ref acls::creator_all). /// /// ZooKeeper's authentication system is extensible, but the majority of use cases are covered by the built-in /// schemes: /// /// - \c "world" -- This has a single ID \c "anyone" that represents any user of the system. The ACLs /// \ref acls::open_unsafe and \ref acls::read_unsafe use the \c "world" scheme. /// - \c "auth" -- This represents any authenticated user. The \c id field is unused. The ACL \ref acls::creator_all /// uses the \c "auth" scheme. /// - \c "digest" -- This uses a \c "${username}:${password}" string to generate MD5 hash which is then used as an /// identity. Authentication is done by sending the string in clear text. When used in the ACL, the expression /// will be the \c "${username}:${digest}", where \c digest is the base 64 encoded SHA1 digest of \c password. /// - \c "ip" -- This uses the client host IP as an identity. The \c id expression is an IP address or CIDR netmask, /// which will be matched against the client identity. const std::string& scheme() const { return _scheme; } /// The ID of the user under the \ref scheme. For example, with the \c "ip" \c scheme, this is an IP address or CIDR /// netmask. const std::string& id() const { return _id; } /// The permissions associated with this ACL. const permission& permissions() const { return _permissions; } private: std::string _scheme; std::string _id; permission _permissions; }; /// Compute a hash for the given \a rule. std::size_t hash(const acl_rule& rule); [[gnu::pure]] bool operator==(const acl_rule& lhs, const acl_rule& rhs); [[gnu::pure]] bool operator!=(const acl_rule& lhs, const acl_rule& rhs); [[gnu::pure]] bool operator< (const acl_rule& lhs, const acl_rule& rhs); [[gnu::pure]] bool operator<=(const acl_rule& lhs, const acl_rule& rhs); [[gnu::pure]] bool operator> (const acl_rule& lhs, const acl_rule& rhs); [[gnu::pure]] bool operator>=(const acl_rule& lhs, const acl_rule& rhs); std::ostream& operator<<(std::ostream&, const acl_rule&); std::string to_string(const acl_rule&); /// An access control list is a wrapper around \ref acl_rule instances. In general, the ACL system is similar to UNIX /// file access permissions, where znodes act as files. Unlike UNIX, each znode can have any number of ACLs to /// correspond with the potentially limitless (and pluggable) authentication schemes. A more surprising difference is /// that ACLs are not recursive: If \c /path is only readable by a single user, but \c /path/sub is world-readable, then /// anyone will be able to read \c /path/sub. /// /// See ZooKeeper /// Programmer's Guide for more information. /// /// \see acls class acl final { public: using iterator = std::vector::iterator; using const_iterator = std::vector::const_iterator; using size_type = std::size_t; public: /// Create an empty ACL. Keep in mind that an empty ACL is an illegal ACL. acl() = default; /// Create an instance with the provided \a rules. acl(std::vector rules) noexcept; /// Create an instance with the provided \a rules. acl(std::initializer_list rules) : acl(std::vector(rules)) { } acl(const acl&) = default; acl& operator=(const acl&) = default; acl(acl&&) = default; acl& operator=(acl&&) = default; ~acl() noexcept; /// The number of rules in this ACL. size_type size() const { return _impl.size(); } /// \{ /// Get the rule at the given \a idx. const acl_rule& operator[](size_type idx) const { return _impl[idx]; } acl_rule& operator[](size_type idx) { return _impl[idx]; } /// \} /// \{ /// Get the rule at the given \a idx. /// /// \throws std::out_of_range if the \a idx is larger than \ref size. const acl_rule& at(size_type idx) const { return _impl.at(idx); } acl_rule& at(size_type idx) { return _impl.at(idx); } /// \} /// \{ /// Get an iterator to the beginning of the rule list. iterator begin() { return _impl.begin(); } const_iterator begin() const { return _impl.begin(); } const_iterator cbegin() const { return _impl.begin(); } /// \} /// \{ /// Get an iterator to the end of the rule list. iterator end() { return _impl.end(); } const_iterator end() const { return _impl.end(); } const_iterator cend() const { return _impl.end(); } /// \} /// Increase the reserved memory block so it can store at least \a capacity rules without reallocating. void reserve(size_type capacity) { _impl.reserve(capacity); } /// Construct a rule emplace on the end of the list using \a args. /// /// \see push_back template void emplace_back(TArgs&&... args) { _impl.emplace_back(std::forward(args)...); } /// \{ /// Add the rule \a x to the end of this list. void push_back(acl_rule&& x) { emplace_back(std::move(x)); } void push_back(const acl_rule& x) { emplace_back(x); } /// \} private: std::vector _impl; }; [[gnu::pure]] bool operator==(const acl& lhs, const acl& rhs); [[gnu::pure]] bool operator!=(const acl& lhs, const acl& rhs); std::ostream& operator<<(std::ostream&, const acl&); std::string to_string(const acl& self); /// Commonly-used ACLs. class acls { public: /// This ACL gives the creators authentication id's all permissions. static const acl& creator_all(); /// This is a completely open ACL. It is also the ACL used in operations like \ref client::create if no ACL is /// specified. static const acl& open_unsafe(); /// This ACL gives the world the ability to read. static const acl& read_unsafe(); }; /// \} } namespace std { template <> class hash { public: using argument_type = zk::acl_rule; using result_type = std::size_t; result_type operator()(const argument_type& x) const { return zk::hash(x); } }; } ================================================ FILE: src/zk/acl_tests.cpp ================================================ #include #include "acl.hpp" namespace zk { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // permission // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// GTEST_TEST(permission_tests, all) { auto all = permission::read | permission::write | permission::create | permission::erase | permission::admin; CHECK_EQ(permission::all, all); CHECK_EQ("all", to_string(all)); } GTEST_TEST(permission_tests, stringification) { CHECK_EQ("none", to_string(permission::none)); CHECK_EQ("read|create", to_string(permission::read | permission::create)); CHECK_EQ("write|admin", to_string(permission::write | permission::admin)); CHECK_EQ("read|create|erase", to_string(permission::read | permission::create | permission::erase)); CHECK_EQ("admin", to_string(permission::admin)); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // acl_rule // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// GTEST_TEST(acl_rule_tests, stringification) { CHECK_EQ("(ip:80.23.0.0/16, read|write)", to_string(acl_rule("ip", "80.23.0.0/16", permission::read | permission::write)) ); } GTEST_TEST(acl_rule_tests, comparisons) { acl_rule creator_all = { "auth", "", permission::all }; acl_rule world_open = { "world", "anyone", permission::all }; CHECK_EQ(creator_all, creator_all); CHECK_NE(creator_all, world_open); CHECK_LT(creator_all, world_open); CHECK_LE(creator_all, world_open); CHECK_GT(world_open, creator_all); CHECK_GE(world_open, creator_all); } GTEST_TEST(acl_rule_tests, hashing) { acl_rule creator_all = { "auth", "", permission::all }; acl_rule world_open = { "world", "anyone", permission::all }; CHECK_EQ(hash(creator_all), hash(creator_all)); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // acl // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// GTEST_TEST(acl_tests, stringification) { CHECK_EQ("[(auth, all)]", to_string(acls::creator_all())); CHECK_EQ("[(world:anyone, all)]", to_string(acls::open_unsafe())); CHECK_EQ("[(world:anyone, read)]", to_string(acls::read_unsafe())); CHECK_EQ("[(auth, read), (ip:50.40.30.0/24, all)]", to_string(acl({ { "auth", "", permission::read }, { "ip", "50.40.30.0/24", permission::all } })) ); } } ================================================ FILE: src/zk/buffer.hpp ================================================ /// \file /// Controls the \c buffer type. #pragma once #include #include /// \addtogroup Client /// \{ // \def ZKPP_BUFFER_USE_STD_STRING // Set this to 1 to use \c std::string as the backing type for zk::buffer. // #ifndef ZKPP_BUFFER_USE_STD_STRING # define ZKPP_BUFFER_USE_STD_STRING 0 #endif /// \def ZKPP_BUFFER_USE_CUSTOM /// Set this to 1 to use a custom definitions for \c buffer. If this is set, you must also set /// \ref ZKPP_BUFFER_INCLUDE and \ref ZKPP_BUFFER_TYPE. /// /// \def ZKPP_BUFFER_INCLUDE /// The header file to use to find the \c buffer type. This value is set automatically if using built-in configuration /// options (such as \ref ZKPP_BUFFER_USE_STD_VECTOR) and must be set manually if using \ref ZKPP_BUFFER_USE_CUSTOM. /// /// \def ZKPP_BUFFER_TYPE /// The type name to use for the \c buffer type. This value is set automatically if using built-in configuration /// options (such as \ref ZKPP_BUFFER_USE_STD_VECTOR) and must be set manually if using \ref ZKPP_BUFFER_USE_CUSTOM. #ifndef ZKPP_BUFFER_USE_CUSTOM # define ZKPP_BUFFER_USE_CUSTOM 0 #endif /// \def ZKPP_BUFFER_USE_STD_VECTOR /// Set this to 1 to use \c std::vector for the implementation of \ref zk::buffer. This is the default behavior. #ifndef ZKPP_BUFFER_USE_STD_VECTOR # if ZKPP_BUFFER_USE_STD_STRING || ZKPP_BUFFER_USE_CUSTOM # define ZKPP_BUFFER_USE_STD_VECTOR 0 # else # define ZKPP_BUFFER_USE_STD_VECTOR 1 # endif #endif #if ZKPP_BUFFER_USE_STD_VECTOR # define ZKPP_BUFFER_INCLUDE # define ZKPP_BUFFER_TYPE std::vector #elif ZKPP_BUFFER_USE_STD_STRING # define ZKPP_BUFFER_INCLUDE # define ZKPP_BUFFER_TYPE std::string #elif ZKPP_BUFFER_USE_CUSTOM # if !defined ZKPP_BUFFER_INCLUDE || !defined ZKPP_BUFFER_TYPE # error "When ZKPP_BUFFER_USE_CUSTOM is set, you must also define ZKPP_BUFFER_INCLUDE and ZKPP_BUFFER_TYPE" # endif #else # error "Unknown type to use for zk::buffer" #endif /// \} #include ZKPP_BUFFER_INCLUDE namespace zk { /// \addtogroup Client /// \{ /// The \c buffer type. By default, this is an \c std::vector, but this can be altered by compile-time flags such /// as \ref ZKPP_BUFFER_USE_CUSTOM. The requirements for a custom buffer are minimal -- the type must fit this criteria: /// /// | expression | type | description | /// |:----------------------|:--------------------------|:-------------------------------------------------------------| /// | `buffer::value_type` | `char` | Buffers must be made of single-byte elements | /// | `buffer::size_type` | `std::size_t` | | /// | `buffer(ib, ie)` | `buffer` | Constructs a buffer with the range [`ib`, `ie`) | /// | `buffer(buffer&&)` | `buffer` | Move constructible (must be `noexcept`). | /// | `operator=(buffer&&)` | `buffer&` | Move assignable (must be `noexcept`). | /// | `size()` | `size_type` | Get the length of the buffer | /// | `data()` | `const value_type*` | Get a pointer to the beginning of the contents | using buffer = ZKPP_BUFFER_TYPE; // Check through static_assert: static_assert(sizeof(buffer::value_type) == 1U, "buffer::value_type must be single-byte elements"); static_assert(std::is_same::value, "buffer::size_type must be std::size_t"); static_assert(std::is_constructible, ptr>::value, "buffer must be constructible with two pointers" ); static_assert(std::is_move_constructible::value, "buffer must be move-constructible"); static_assert(std::is_nothrow_move_constructible::value, "buffer must be nothrow move-constructible"); static_assert(std::is_nothrow_move_assignable::value, "buffer must be nothrow move-assignable"); static_assert(std::is_same().size()), buffer::size_type>::value, "buffer::size() must return buffer::size_type" ); static_assert(std::is_constructible, decltype(std::declval().data()) >::value, "buffer::data() must return ptr" ); /// \} } ================================================ FILE: src/zk/client.cpp ================================================ #include "client.hpp" #include "acl.hpp" #include "connection.hpp" #include "multi.hpp" #include "exceptions.hpp" #include #include namespace zk { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // client // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// client::client(const connection_params& params) : client(connection::connect(params)) { } client::client(string_view conn_string) : client(connection::connect(conn_string)) { } client::client(std::shared_ptr conn) noexcept : _conn(std::move(conn)) { } future client::connect(string_view conn_string) { return connect(connection_params::parse(conn_string)); } future client::connect(connection_params conn_params) { try { auto conn = connection::connect(conn_params); auto state_change_fut = conn->watch_state(); if (conn->state() == state::connected) { promise p; p.set_value(client(std::move(conn))); return p.get_future(); } else { // TODO: Test if future::then can be relied on and use that instead of std::async return zk::async ( zk::launch::async, [state_change_fut = std::move(state_change_fut), conn = std::move(conn)] () mutable -> client { state s(state_change_fut.get()); if (s == state::connected) return client(conn); else zk::throw_exception(std::runtime_error(std::string("Unexpected state: ") + to_string(s))); } ); } } catch (...) { promise p; p.set_exception(zk::current_exception()); return p.get_future(); } } client::~client() noexcept = default; void client::close() { _conn->close(); } future client::get(string_view path) const { return _conn->get(path); } future client::watch(string_view path) const { return _conn->watch(path); } future client::get_children(string_view path) const { return _conn->get_children(path); } future client::watch_children(string_view path) const { return _conn->watch_children(path); } future client::exists(string_view path) const { return _conn->exists(path); } future client::watch_exists(string_view path) const { return _conn->watch_exists(path); } future client::create(string_view path, const buffer& data, const acl& rules, create_mode mode ) { return _conn->create(path, data, rules, mode); } future client::create(string_view path, const buffer& data, create_mode mode ) { return create(path, data, acls::open_unsafe(), mode); } future client::set(string_view path, const buffer& data, version check) { return _conn->set(path, data, check); } future client::get_acl(string_view path) const { return _conn->get_acl(path); } future client::set_acl(string_view path, const acl& rules, acl_version check) { return _conn->set_acl(path, rules, check); } future client::erase(string_view path, version check) { return _conn->erase(path, check); } future client::load_fence() const { return _conn->load_fence(); } future client::commit(multi_op txn) { return _conn->commit(std::move(txn)); } } ================================================ FILE: src/zk/client.hpp ================================================ #pragma once #include #include #include #include "buffer.hpp" #include "forwards.hpp" #include "future.hpp" #include "optional.hpp" #include "string_view.hpp" #include "results.hpp" #include "types.hpp" namespace zk { /// \defgroup Client /// Interacting with ZooKeeper as a \ref client. /// \{ /// A ZooKeeper client connection. This is the primary class for interacting with the ZooKeeper cluster. The best way to /// create a client is with the static \ref connect function. class client final { public: /// Create a client connected to the cluster specified by \a params. explicit client(const connection_params& params); /// Create a client connected to the cluster specified by \a conn_string. /// /// \param conn_string A ZooKeeper \ref ConnectionStrings "connection string". explicit client(string_view conn_string); /// Create a client connected with \a conn. explicit client(std::shared_ptr conn) noexcept; /// \{ /// Create a client connected to the cluster specified by \c conn_string. /// /// \param conn_string A ZooKeeper \ref ConnectionStrings "connection string". /// \returns A future which will be filled when the conneciton is established. The future will be filled in error if /// the client will never be able to connect to the cluster (for example: a bad connection string). static future connect(string_view conn_string); static future connect(connection_params conn_params); /// \} client(const client&) noexcept = default; client(client&&) noexcept = default; client& operator=(const client&) noexcept = default; client& operator=(client&&) noexcept = default; ~client() noexcept; /// Close the underlying \ref connection. All outstanding operations will be cancelled and all watches will be /// delivered with \ref closed. You usually do not need to call this operation, as the destructor handles this /// automatically. void close(); /// Return the data and the \ref stat of the entry of the given \a path. /// /// \throws no_entry If no entry exists at the given \a path, the future will be delievered with \ref no_entry. future get(string_view path) const; /// Similar to \ref get, but if the call is successful (no error is returned), a watch will be left on the entry /// with the given \a path. The watch will be triggered by a successful operation that sets data or erases the /// entry. /// /// \throws no_entry If no entry exists at the given \a path, the future will be delievered with \ref no_entry. To /// watch for the creation of an entry, use \ref watch_exists. future watch(string_view path) const; /// Return the list of the children of the entry of the given \a path. The returned values are not prefixed with the /// provided \a path; i.e. if the database contains \c "/path/a" and \c "/path/b", the result of \c get_children for /// \c "/path" will be `["a", "b"]`. The list of children returned is not sorted and no guarantee is provided as to /// its natural or lexical order. /// /// \throws no_entry If no entry exists at the given \a path, the future will be delievered with \ref no_entry. future get_children(string_view path) const; /// Similar to \ref get_children, but if the call is successful (no error is returned), a watch will be left on the /// entry with the given \a path. The watch will be triggered by a successful operation that erases the entry at the /// given \a path or creates or erases a child immediately under the path (it is not recursive). future watch_children(string_view path) const; /// Return the \ref stat of the entry of the given \a path or \c nullopt if it does not exist. future exists(string_view path) const; /// Similar to \ref watch, but if the call is successful (no error is returned), a watch will be left on the entry /// with the given \a path. The watch will be triggered by a successful operation that creates the entry, erases the /// entry, or sets the data on the entry. future watch_exists(string_view path) const; /// \{ /// Create an entry at the given \a path. /// /// This operation, if successful, will trigger all the watches left on the entry of the given path by \ref watch /// API calls, and the watches left on the parent entry by \ref watch_children API calls. /// /// \param path The path or path pattern (if using \ref create_mode::sequential) to create. /// \param data The data to create for the entry. /// \param mode Specifies the behavior of the created entry (see \ref create_mode for more information). /// \param rules The ACL for the created entry. If unspecified, it is equivalent to providing /// \ref acls::open_unsafe. /// \returns A future which will be filled with the name of the created entry and its \ref stat. /// /// \throws entry_exists If an entry with the same actual \a path already exists in the ZooKeeper, the future will /// be delivered with \ref entry_exists. Note that since a different actual path is used for each invocation of /// creating sequential entry with the same \a path argument, the call should never error in this manner. /// \throws no_entry If the parent of the given \a path does not exist, the future will be delievered with /// \ref no_entry. /// \throws no_children_for_ephemerals An ephemeral entry cannot have children. If the parent entry of the given /// \a path is ephemeral, the future will be delivered with \c no_children_for_ephemerals. /// \throws invalid_acl If the \a acl is invalid or empty, the future will be delivered with \ref invalid_acl. /// \throws invalid_arguments The maximum allowable size of the data array is 1 MiB (1,048,576 bytes). If \a data /// is larger than this the future will be delivered with \ref invalid_arguments. future create(string_view path, const buffer& data, const acl& rules, create_mode mode = create_mode::normal ); future create(string_view path, const buffer& data, create_mode mode = create_mode::normal ); /// \} /// Set the data for the entry of the given \a path if such an entry exists and the given version matches the /// version of the entry (if the given version is \ref version::any, there is no version check). This operation, if /// successful, will trigger all the watches on the entry of the given \c path left by \ref watch calls. /// /// \throws no_entry If no entry exists at the given \a path, the future will be delievered with \ref no_entry. /// \throws version_mismatch If the given version \a check does not match the entry's version, the future will be /// delivered with \ref version_mismatch. /// \throws invalid_arguments The maximum allowable size of the data array is 1 MiB (1,048,576 bytes). If \a data /// is larger than this the future will be delivered with \ref invalid_arguments. future set(string_view path, const buffer& data, version check = version::any()); /// Return the ACL and \ref stat of the entry of the given path. /// /// \throws no_entry If no entry exists at the given \a path, the future will be delievered with \ref no_entry. future get_acl(string_view path) const; /// Set the ACL for the entry of the given \a path if such an entry exists and the given version \a check matches /// the version of the entry. /// /// \param check If specified, check that the ACL version matches. Keep in mind this is the \c acl_version, not the /// data \ref version -- there is no way to have this operation fail on changes to \ref stat::data_version without /// the use of a \ref multi_op. /// /// \throws no_entry If no entry exists at the given \a path, the future will be delievered with \ref no_entry. /// \throws version_mismatch If the given version \a check does not match the entry's version, the future will be /// delivered with \ref version_mismatch. future set_acl(string_view path, const acl& rules, acl_version check = acl_version::any()); /// Erase the entry at the given \a path. The call will succeed if such an entry exists, and the given version /// \a check matches the entry's version (if the given version is \ref version::any, it matches any entry's /// versions). This operation, if successful, will trigger all the watches on the entry of the given \a path left by /// \ref watch API calls, watches left by \ref watch_exists API calls, and the watches on the parent entry left by /// \ref watch_children API calls. /// /// \throws no_entry If no entry exists at the given \a path, the future will be delievered with \ref no_entry. /// \throws version_mismatch If the given version \a check does not match the entry's version, the future will be /// delivered with \ref version_mismatch. /// \throws not_empty You are only allowed to erase entries with no children. If the entry has children, the future /// will be delievered with \ref not_empty. future erase(string_view path, version check = version::any()); /// Ensure that all subsequent reads observe the data at the transaction on the server at or past real-time \e now. /// If your application communicates only through reads and writes of ZooKeeper, this operation is never needed. /// However, if your application communicates a change in ZooKeeper state through means outside of ZooKeeper (called /// a "hidden channel" in ZooKeeper vernacular), then it is possible for a receiver to attempt to react to a change /// before it can observe it through ZooKeeper state. /// /// \warning /// The internal pipeline for this operation is not the same as modifying operations (\ref set, \ref create, etc.). /// In cases of leader failure, there is a chance that the leader does not have support from the quorum, as it has /// switched to a new leader. While this is rare, it is still \e possible that not all updates have been processed. /// /// \note /// Other APIs call this operation \c sync and allow you to provide a \c path parameter. There are a few issues with /// this. First: that name conflicts with the commonly-used POSIX \c sync command, leading to confusion that data in /// ZooKeeper does not have integrity. Secondly, this operation has more in common with \c std::atomic_thread_fence /// or the x86 \c lfence instruction than \c sync (on the server, "flush" is an appropriate term -- just like fence /// implementations in CPUs). Finally, the \c path parameter is ignored by the server -- all future fetches are /// fenced, no matter what path is specified. In the future, ZooKeeper might support partitioning, in which case the /// \c path parameter might become relevant. /// /// It is often not necessary to wait for the fence future to be returned, as future reads will be synced without /// waiting. However, there is no guarantee on the ordering of the read if the future returned from \c load_fence /// is completed in error. /// /// \code /// auto fence_future = client.load_fence(); /// // data_future will be completed with the load_fence, even though we haven't waited to complete /// auto data_future = client.get("/some/path"); /// /// // Useful to use when_all to concat and error check (C++ Extensions for Concurrency, ISO/IEC TS 19571:2016) /// auto guaranteed_future = std::when_all(std::move(fence_future), std::move(data_future)); /// \endcode future load_fence() const; /// Commit the transaction specified by \a txn. The operations are performed atomically: They will either all /// succeed or all fail. /// /// \throws transaction_failed If the transaction does not complete with an error in the transaction itself (any /// \ref error_code that fits \ref is_api_error), the future will be delivered with \ref transaction_failed. Check /// the thrown \ref transaction_failed::underlying_cause for more information. /// \throws system_error For the same reasons any other operation might fail, the future will be delivered with a /// specific \ref system_error. future commit(multi_op txn); private: std::shared_ptr _conn; }; /// \} } ================================================ FILE: src/zk/client_tests.cpp ================================================ #include #include #include #include #include #include #include "client.hpp" #include "error.hpp" #include "multi.hpp" #include "string_view.hpp" namespace zk { static buffer buffer_from(string_view str) { return buffer(str.data(), str.data() + str.size()); } class client_tests : public server::single_server_fixture { }; GTEST_TEST_F(client_tests, get_root) { client c = get_connected_client(); auto res = c.get("/").get(); std::cerr << res << std::endl; c.close(); } GTEST_TEST_F(client_tests, exists) { client c = get_connected_client(); CHECK_TRUE(c.exists("/").get()); CHECK_FALSE(c.exists("/some/bogus/path").get()); } GTEST_TEST_F(client_tests, create) { client c = get_connected_client(); const char local_buf[10] = { 0 }; auto f_create = c.create("/test-node", buffer(local_buf, local_buf + sizeof local_buf)); auto name = f_create.get().name(); CHECK_EQ("/test-node", name); } GTEST_TEST_F(client_tests, create_seq_and_set) { client c = get_connected_client(); auto f_create = c.create("/test-node-", buffer_from("Hello!"), create_mode::sequential); auto name = f_create.get().name(); auto orig_stat = c.get(name).get().stat(); auto expected_version = orig_stat.data_version; ++expected_version; c.set(name, buffer_from("WORLD")).get(); auto contents = c.get(name).get(); CHECK_EQ(contents.stat().data_version, expected_version); CHECK_TRUE(contents.data() == buffer_from("WORLD")); } GTEST_TEST_F(client_tests, create_seq_and_erase) { client c = get_connected_client(); auto f_create = c.create("/test-node-", buffer_from("Hello!"), create_mode::sequential); auto name = f_create.get().name(); auto orig_stat = c.get(name).get().stat(); c.erase(name, orig_stat.data_version).get(); CHECK_THROWS(no_entry) { c.get(name).get(); }; } GTEST_TEST_F(client_tests, create_seq_and_get_children) { client c = get_connected_client(); auto f_create = c.create("/test-node-", buffer_from("Hello!"), create_mode::sequential); auto name = f_create.get().name(); auto orig_stat = c.get(name).get().stat(); c.create(name + "/a", buffer()).get(); c.create(name + "/b", buffer()).get(); c.create(name + "/c", buffer()).get(); auto result = c.get_children(name).get(); CHECK_EQ(orig_stat.data_version, result.parent_stat().data_version); CHECK_LT(orig_stat.child_version, result.parent_stat().child_version); std::vector expected_children = { "a", "b", "c" }; std::sort(result.children().begin(), result.children().end()); CHECK_TRUE(expected_children == result.children()); } GTEST_TEST_F(client_tests, acl) { client c = get_connected_client(); auto name = c.create("/test-node-", buffer_from("Hello!"), create_mode::sequential).get().name(); // set the data of the node a few times to make sure the data_version is different value from acl_version for (std::size_t changes = 0; changes < 5; ++changes) c.set(name, buffer_from("data change")).get(); auto orig_result = c.get_acl(name).get(); CHECK_EQ(acls::open_unsafe(), orig_result.acl()); std::cerr << "HEY: " << orig_result << std::endl; c.set_acl(name, acls::read_unsafe(), orig_result.stat().acl_version).get(); auto new_result = c.get_acl(name).get(); CHECK_EQ(acls::read_unsafe(), new_result.acl()); } GTEST_TEST_F(client_tests, watch_change) { client c = get_connected_client(); auto name = c.create("/test-node-", buffer_from("Hello!"), create_mode::sequential).get().name(); auto watch = c.watch(name).get(); CHECK_TRUE(watch.initial().data() == buffer_from("Hello!")); c.set(name, buffer_from("world")); // don't wait -- the watch won't trigger until the operation completes auto ev = watch.next().get(); CHECK_EQ(ev.type(), event_type::changed); CHECK_EQ(ev.state(), state::connected); } GTEST_TEST_F(client_tests, watch_children) { client c = get_connected_client(); auto root_name = c.create("/test-node-", buffer_from("Hello!"), create_mode::sequential).get().name(); c.commit({ op::create(root_name + "/a", buffer_from("a")), op::create(root_name + "/b", buffer_from("b")), op::create(root_name + "/c", buffer_from("c")), op::create(root_name + "/d", buffer_from("d")), }).get(); auto watch_child_creation = c.watch_children(root_name).get(); CHECK_EQ(4U, watch_child_creation.initial().children().size()); c.create(root_name + "/e", buffer_from("e")); auto ev = watch_child_creation.next().get(); CHECK_EQ(ev.type(), event_type::child); CHECK_EQ(ev.state(), state::connected); auto watch_child_erase = c.watch_children(root_name).get(); CHECK_EQ(5U, watch_child_erase.initial().children().size()); c.erase(root_name + "/a"); auto ev2 = watch_child_erase.next().get(); CHECK_EQ(ev2.type(), event_type::child); CHECK_EQ(ev2.state(), state::connected); } GTEST_TEST_F(client_tests, watch_exists) { client c = get_connected_client(); auto root_name = c.create("/test-node-", buffer_from("Hello!"), create_mode::sequential).get().name(); auto watch_creation = c.watch_exists(root_name + "/sub").get(); CHECK_FALSE(watch_creation.initial()); c.create(root_name + "/sub", buffer_from("Blah")); auto ev = watch_creation.next().get(); CHECK_EQ(ev.type(), event_type::created); CHECK_EQ(ev.state(), state::connected); auto watch_erase = c.watch_exists(root_name + "/sub").get(); CHECK_TRUE(watch_erase.initial()); c.erase(root_name + "/sub"); auto ev2 = watch_erase.next().get(); CHECK_EQ(ev2.type(), event_type::erased); CHECK_EQ(ev2.state(), state::connected); } GTEST_TEST_F(client_tests, load_fence) { client c = get_connected_client(); // There does not appear to be a good way to actually test this -- so just make sure we don't segfault c.load_fence().get(); } GTEST_TEST_F(client_tests, watch_close) { client c = get_connected_client(); auto watch = c.watch("/").get(); c.close(); // watch should be triggered with session closed auto ev = watch.next().get(); CHECK_EQ(ev.type(), event_type::session); CHECK_EQ(ev.state(), state::closed); } class stopping_client_tests : public server::server_fixture { }; GTEST_TEST_F(stopping_client_tests, watch_server_stop) { client c = get_connected_client(); auto watch = c.watch("/").get(); this->stop_server(true); auto ev = watch.next().get(); CHECK_EQ(ev.type(), event_type::session); } } ================================================ FILE: src/zk/config.hpp ================================================ #pragma once /// \addtogroup Client /// \{ /// \def ZKPP_USER_CONFIG /// A user-defined configuration file to be included before all other ZooKeeper C++ content. #ifdef ZKPP_USER_CONFIG # include ZKPP_USER_CONFIG #endif #define ZKPP_VERSION_MAJOR 0 #define ZKPP_VERSION_MINOR 2 #define ZKPP_VERSION_PATCH 3 /// \def ZKPP_DEBUG /// Was ZooKeeper C++ compiled in debug mode? This value must be the same between when the SO was built and when you are /// compiling. In general, this is not useful outside of library maintainers. /// /// \warning /// Keep in mind this value is \e always defined. Use `#if ZKPP_DEBUG`, \e not `#ifdef ZKPP_DEBUG`. #ifndef ZKPP_DEBUG # define ZKPP_DEBUG 0 #endif /// \} namespace zk { /// \addtogroup Client /// \{ /// A simple, unowned pointer. It operates exactly like using \c *, but removes the question of \c * associativity and /// is easier to read when \c const qualifiers are involved. template using ptr = T*; /// \} } ================================================ FILE: src/zk/connection.cpp ================================================ #include "connection.hpp" #include "connection_zk.hpp" #include "error.hpp" #include "types.hpp" #include "exceptions.hpp" #include #include #include #include #include #include #include namespace zk { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // connection // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// connection::~connection() noexcept { } std::shared_ptr connection::connect(const connection_params& params) { return std::make_shared(params); } std::shared_ptr connection::connect(string_view conn_string) { return connect(connection_params::parse(conn_string)); } future connection::watch_state() { std::unique_lock ax(_state_change_promises_protect); _state_change_promises.emplace_back(); return _state_change_promises.rbegin()->get_future(); } void connection::on_session_event(zk::state new_state) { std::unique_lock ax(_state_change_promises_protect); auto l_state_change_promises = std::move(_state_change_promises); ax.unlock(); auto ex = new_state == zk::state::expired_session ? get_exception_ptr_of(error_code::session_expired) : new_state == zk::state::authentication_failed ? get_exception_ptr_of(error_code::authentication_failed) : zk::exception_ptr(); for (auto& p : l_state_change_promises) { if (ex) p.set_exception(ex); else p.set_value(new_state); } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // connection_params // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// connection_params::connection_params() noexcept : _connection_schema("zk"), _hosts({}), _chroot("/"), _randomize_hosts(true), _read_only(false), _timeout(default_timeout) { } connection_params::~connection_params() noexcept { } template static string_view sv_from_match(const TMatch& src) { return string_view(src.first, std::distance(src.first, src.second)); } template void split_each_substr(string_view src, char delim, const FAction& action) { while (!src.empty()) { // if next_div is src.end, the logic still works auto next_div = std::find(src.begin(), src.end(), delim); action(string_view(src.data(), std::distance(src.begin(), next_div))); src.remove_prefix(std::distance(src.begin(), next_div)); if (!src.empty()) src.remove_prefix(1U); } } static connection_params::host_list extract_host_list(string_view src) { connection_params::host_list out; out.reserve(std::count(src.begin(), src.end(), ',')); split_each_substr(src, ',', [&] (string_view sub) { out.emplace_back(std::string(sub)); }); return out; } static bool extract_bool(string_view key, string_view val) { if (val.empty()) zk::throw_exception(std::invalid_argument(std::string("Key ") + std::string(key) + " has blank value")); switch (val[0]) { case '1': case 't': case 'T': return true; case '0': case 'f': case 'F': return false; default: zk::throw_exception(std::invalid_argument(std::string("Invalid value for ") + std::string(key) + std::string(" \"") + std::string(val) + "\" -- expected a boolean" )); } } static std::chrono::milliseconds extract_millis(string_view key, string_view val) { if (val.empty()) zk::throw_exception(std::invalid_argument(std::string("Key ") + std::string(key) + " has blank value")); if (val[0] == 'P') { zk::throw_exception(std::invalid_argument("ISO 8601 duration is not supported (yet).")); } else { double seconds = std::stod(std::string(val)); return std::chrono::duration_cast(std::chrono::duration(seconds)); } } static void extract_advanced_options(string_view src, connection_params& out) { if (src.empty() || src.size() == 1U) return; else src.remove_prefix(1U); std::string invalid_keys_msg; auto invalid_key = [&] (string_view key) { if (invalid_keys_msg.empty()) invalid_keys_msg = "Invalid key in querystring: "; else invalid_keys_msg += ", "; invalid_keys_msg += std::string(key); }; split_each_substr(src, '&', [&] (string_view qp_part) { auto eq_it = std::find(qp_part.begin(), qp_part.end(), '='); if (eq_it == qp_part.end()) zk::throw_exception(std::invalid_argument("Invalid connection string -- query string must be specified as " "\"key1=value1&key2=value2...\"" )); auto key = qp_part.substr(0, std::distance(qp_part.begin(), eq_it)); auto val = qp_part.substr(std::distance(qp_part.begin(), eq_it) + 1); if (key == "randomize_hosts") out.randomize_hosts() = extract_bool(key, val); else if (key == "read_only") out.read_only() = extract_bool(key, val); else if (key == "timeout") out.timeout() = extract_millis(key, val); else invalid_key(key); }); if (!invalid_keys_msg.empty()) zk::throw_exception(std::invalid_argument(std::move(invalid_keys_msg))); } connection_params connection_params::parse(string_view conn_string) { static const std::regex expr(R"(([^:]+)://([^/]+)((/[^\?]*)(\?.*)?)?)", std::regex_constants::ECMAScript | std::regex_constants::optimize ); constexpr auto schema_idx = 1U; constexpr auto hostaddr_idx = 2U; constexpr auto path_idx = 4U; constexpr auto querystring_idx = 5U; std::cmatch match; if (!std::regex_match(conn_string.begin(), conn_string.end(), match, expr)) zk::throw_exception(std::invalid_argument(std::string("Invalid connection string (") + std::string(conn_string) + " -- format is \"schema://[auth@]${host_addrs}/[path][?options]\"" )); connection_params out; out.connection_schema() = match[schema_idx].str(); out.hosts() = extract_host_list(sv_from_match(match[hostaddr_idx])); out.chroot() = match[path_idx].str(); if (out.chroot().empty()) out.chroot() = "/"; extract_advanced_options(sv_from_match(match[querystring_idx]), out); return out; } bool operator==(const connection_params& lhs, const connection_params& rhs) { return lhs.connection_schema() == rhs.connection_schema() && lhs.hosts() == rhs.hosts() && lhs.chroot() == rhs.chroot() && lhs.randomize_hosts() == rhs.randomize_hosts() && lhs.read_only() == rhs.read_only() && lhs.timeout() == rhs.timeout(); } bool operator!=(const connection_params& lhs, const connection_params& rhs) { return !(lhs == rhs); } std::ostream& operator<<(std::ostream& os, const connection_params& x) { os << x.connection_schema() << "://"; bool first = true; for (const auto& host : x.hosts()) { if (first) first = false; else os << ','; os << host; } os << x.chroot(); first = true; auto query_string = [&] (ptr key, const auto& val) { if (first) { first = false; os << '?'; } else { os << '&'; } os << key << '=' << val; }; if (!x.randomize_hosts()) query_string("randomize_hosts", "false"); if (x.read_only()) query_string("read_only", "true"); if (x.timeout() != connection_params::default_timeout) query_string("timeout", std::chrono::duration(x.timeout()).count()); return os; } std::string to_string(const connection_params& x) { std::ostringstream os; os << x; return os.str(); } } ================================================ FILE: src/zk/connection.hpp ================================================ #pragma once #include #include #include #include #include #include #include #include "buffer.hpp" #include "forwards.hpp" #include "future.hpp" #include "string_view.hpp" namespace zk { /// \addtogroup Client /// \{ /// An actual connection to the server. The majority of methods have the same signature and meaning as \ref client. /// /// \see connection_zk class connection { public: static std::shared_ptr connect(const connection_params&); static std::shared_ptr connect(string_view conn_string); virtual ~connection() noexcept; virtual void close() = 0; virtual future get(string_view path) = 0; virtual future watch(string_view path) = 0; virtual future get_children(string_view path) = 0; virtual future watch_children(string_view path) = 0; virtual future exists(string_view path) = 0; virtual future watch_exists(string_view path) = 0; virtual future create(string_view path, const buffer& data, const acl& rules, create_mode mode ) = 0; virtual future set(string_view path, const buffer& data, version check) = 0; virtual future erase(string_view path, version check) = 0; virtual future get_acl(string_view path) const = 0; virtual future set_acl(string_view path, const acl& rules, acl_version check) = 0; virtual future commit(multi_op&& txn) = 0; virtual future load_fence() = 0; virtual zk::state state() const = 0; /// Watch for a state change. virtual future watch_state(); protected: /// Call this from derived classes when a session event happens. This triggers the delivery of all promises of state /// changes (issued through \ref watch_state). virtual void on_session_event(zk::state new_state); private: mutable std::mutex _state_change_promises_protect; std::vector> _state_change_promises; }; /// Used to specify parameters for a \c connection. This can either be created manually or through a /// \ref ConnectionStrings "connection string". class connection_params final { public: using host_list = std::vector; public: static constexpr std::chrono::milliseconds default_timeout = std::chrono::seconds(10); public: /// Create an instance with default values. connection_params() noexcept; ~connection_params() noexcept; /// Create an instance from a connection string. /// /// \anchor ConnectionStrings /// \par Connection Strings /// Connection strings follow the standard format for URLs (`schema://host_address/path?querystring`). The `schema` /// is the type of connection (usually `"zk"`), `auth` is potential authentication, the `host_address` is the list /// of servers, the `path` is the chroot to use for this connection and the `querystring` allows for configuring /// advanced options. For example: `"zk://server-a:2181,server-b:2181,server-c:2181/?timeout=5"`. /// /// \par /// - \e schema: \ref connection_params::connection_schema /// - \e host_address: comma-separated list of \ref connection_params::hosts /// - \e path: \ref connection_params::chroot /// - \e querystring: Allows for an arbitrary amount of optional parameters to be specified. These are specified /// with the conventional HTTP-style (e.g. `"zk://localhost/?timeout=10&read_only=true"` specifies a timeout of 10 /// seconds and sets a read-only client. Boolean values can be specified with \c true, \c t, or \c 1 for \c true /// or \c false, \c f, or \c 0 for \c false. It is important to note that, unlike regular HTTP URLs, query /// parameters which are not understood will result in an error. /// - `randomize_hosts`: \ref connection_params::randomize_hosts /// - `read_only`: \ref connection_params::read_only /// - `timeout`: \ref connection_params::timeout /// /// \throws std::invalid_argument if the string is malformed in some way. static connection_params parse(string_view conn_string); /// \{ /// Determines the underlying \ref zk::connection implementation to use. The valid values are \c "zk" and /// \c "fakezk". /// /// - `zk`: The standard-issue ZooKeeper connection to a real ZooKeeper cluster. This schema uses /// \ref zk::connection_zk as the underlying connection. /// - `fakezk`: Create a client connected to a fake ZooKeeper server (\ref zk::fake::server). Here, the /// `host_address` refers to the name of the in-memory DB created when the server instance was. This schema uses /// \ref zk::fake::connection_fake as the underlying connection. const std::string& connection_schema() const { return _connection_schema; } std::string& connection_schema() { return _connection_schema; } /// \} /// \{ /// Addresses for the ensemble to connect to. This can be IP addresses (IPv4 or IPv6) or hostnames, but IP /// addresses are the recommended method of specification. If the port is left unspecified, \c 2181 is assumed (as /// it is the de facto standard for ZooKeeper server). For IPv6 addresses, use the boxed format (e.g. `[::1]:2181`); /// this is required even when the port is \c 2181 to disambiguate between a host named in hexadecimal and an IPv6 /// address (e.g. `"[fd2d:8413:d6c6::73b]"` or `"[::1]:2181"`). const host_list& hosts() const { return _hosts; } host_list& hosts() { return _hosts; } /// \} /// \{ /// Specifying a value for \c chroot as something aside from \c "" or \c "/" will run the client commands while /// interpreting all paths relative to the specified path. For example, specifying `"/app/a"` will make requests for /// `"/foo/bar"` sent to `"/app/a/foo/bar"` (from the server's perspective). If unspecified, the path will be /// treated as `"/"`. const std::string& chroot() const { return _chroot; } std::string& chroot() { return _chroot; } /// \} /// \{ /// Connect to a host at random (as opposed to attempting connections in order)? The default is to randomize (the /// use cases for sequential connections are usually limited to testing purposes). bool randomize_hosts() const { return _randomize_hosts; } bool& randomize_hosts() { return _randomize_hosts; } /// \} /// \{ /// Allow connections to read-only servers? The default (\c false) is to disallow. **/ bool read_only() const { return _read_only; } bool& read_only() { return _read_only; } /// \} /// \{ /// The session timeout between this client and the server. The server will attempt to respect this value, but will /// automatically use a lower timeout value if this value is too large (see the ZooKeeper Programmer's Guide for /// more information on maximum values). The default is 10 seconds. /// /// When specified in a query string, this value is specified either with floating-point seconds or as an ISO 8601 /// duration (`PT8.93S` for \c 8.930 seconds). std::chrono::milliseconds timeout() const { return _timeout; } std::chrono::milliseconds& timeout() { return _timeout; } /// \} private: std::string _connection_schema; host_list _hosts; std::string _chroot; bool _randomize_hosts; bool _read_only; std::chrono::milliseconds _timeout; }; bool operator==(const connection_params& lhs, const connection_params& rhs); bool operator!=(const connection_params& lhs, const connection_params& rhs); std::string to_string(const connection_params&); std::ostream& operator<<(std::ostream&, const connection_params&); /// \} } ================================================ FILE: src/zk/connection_tests.cpp ================================================ #include #include "connection.hpp" namespace zk { // This test is mostly to check that we still use the defaults. GTEST_TEST(connection_params_tests, defaults) { const auto res = connection_params::parse("zk://localhost/"); CHECK_EQ("zk", res.connection_schema()); CHECK_EQ(1U, res.hosts().size()); CHECK_EQ("localhost", res.hosts()[0]); CHECK_EQ("/", res.chroot()); CHECK_TRUE(res.randomize_hosts()); CHECK_FALSE(res.read_only()); CHECK_TRUE(connection_params::default_timeout == res.timeout()); connection_params manual; manual.hosts() = { "localhost" }; CHECK_EQ(manual, res); } GTEST_TEST(connection_params_tests, multi_hosts) { const auto res = connection_params::parse("zk://server-a,server-b,server-c/"); connection_params manual; manual.hosts() = { "server-a", "server-b", "server-c" }; CHECK_EQ(manual, res); } GTEST_TEST(connection_params_tests, chroot) { const auto res = connection_params::parse("zk://localhost/some/sub/path"); connection_params manual; manual.hosts() = { "localhost" }; manual.chroot() = "/some/sub/path"; CHECK_EQ(manual, res); } GTEST_TEST(connection_params_tests, randomize_hosts) { const auto res = connection_params::parse("zk://localhost/?randomize_hosts=false"); connection_params manual; manual.hosts() = { "localhost" }; manual.randomize_hosts() = false; CHECK_EQ(manual, res); } GTEST_TEST(connection_params_tests, read_only) { const auto res = connection_params::parse("zk://localhost/?read_only=true"); connection_params manual; manual.hosts() = { "localhost" }; manual.read_only() = true; CHECK_EQ(manual, res); } GTEST_TEST(connection_params_tests, randomize_and_read_only) { const auto res = connection_params::parse("zk://localhost/?randomize_hosts=false&read_only=1"); connection_params manual; manual.hosts() = { "localhost" }; manual.randomize_hosts() = false; manual.read_only() = true; CHECK_EQ(manual, res); } GTEST_TEST(connection_params_tests, timeout) { const auto res = connection_params::parse("zk://localhost/?timeout=0.5"); connection_params manual; manual.hosts() = { "localhost" }; manual.timeout() = std::chrono::milliseconds(500); CHECK_EQ(manual, res); } } ================================================ FILE: src/zk/connection_zk.cpp ================================================ #include "connection_zk.hpp" #include "exceptions.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "acl.hpp" #include "error.hpp" #include "multi.hpp" #include "results.hpp" #include "types.hpp" namespace zk { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Utility Functions // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// template auto with_str(string_view src, FAction&& action) noexcept(noexcept(std::forward(action)(ptr()))) -> decltype(std::forward(action)(ptr())) { char buffer[src.size() + 1]; buffer[src.size()] = '\0'; std::memcpy(buffer, src.data(), src.size()); return std::forward(action)(buffer); } static ACL encode_acl_part(const acl_rule& src) { ACL out; out.perms = static_cast(src.permissions()); out.id.scheme = const_cast>(src.scheme().c_str()); out.id.id = const_cast>(src.id().c_str()); return out; } template auto with_acl(const acl& rules, FAction&& action) noexcept(noexcept(std::forward(action)(ptr()))) -> decltype(std::forward(action)(ptr())) { ACL parts[rules.size()]; for (std::size_t idx = 0; idx < rules.size(); ++idx) parts[idx] = encode_acl_part(rules[idx]); ACL_vector vec; vec.count = int(rules.size()); vec.data = parts; return std::forward(action)(&vec); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Native Adaptors // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// static error_code error_code_from_raw(int raw) { switch (raw) { case ZOPERATIONTIMEOUT: raw = ZCONNECTIONLOSS; break; case ZINVALIDCALLBACK: case ZINVALIDACL: raw = ZBADARGUMENTS; break; case ZSESSIONMOVED: raw = ZCONNECTIONLOSS; break; default: break; } return static_cast(raw); } static event_type event_from_raw(int raw) { return static_cast(raw); } // ZooKeeper does not have this concept pre-3.5 #if ZOO_MAJOR_VERSION <= 3 && ZOO_MINOR_VERSION <= 4 static const int ZOO_NOTCONNECTED_STATE = 999; #endif static state state_from_raw(int raw) { // The C client will put us into `ZOO_NOTCONNECTED_STATE` for two reasons: // // 1. This is the state of the initial connection (zookeeper_init_internal), which is then replaced when the adaptor // threads first call the interest function. // 2. During a reconfiguration, the client disconnects and transitions to this state (update_addrs), which is then // updated the next time the I/O thread touches interest. // // In both cases, the state is still "connecting" from the point of view of a client. if (raw == ZOO_NOTCONNECTED_STATE) { raw = ZOO_CONNECTING_STATE; } // `ZOO_ASSOCIATING_STATE` means we have connected to a server, but have not yet authenticated and created the // session. We still can't perform any operations, so treat it as connecting -- the client does not care about the // difference between establishing a TCP connection and negotiating credentials. else if (raw == ZOO_ASSOCIATING_STATE) { raw = ZOO_CONNECTING_STATE; } return static_cast(raw); } static stat stat_from_raw(const struct Stat& raw) { stat out; out.acl_version = acl_version(raw.aversion); out.child_modified_transaction = transaction_id(raw.pzxid); out.child_version = child_version(raw.cversion); out.children_count = raw.numChildren; out.create_time = stat::time_point() + std::chrono::milliseconds(raw.ctime); out.create_transaction = transaction_id(raw.czxid); out.data_size = raw.dataLength; out.data_version = version(raw.version); out.ephemeral_owner = raw.ephemeralOwner; out.modified_time = stat::time_point() + std::chrono::milliseconds(raw.mtime); out.modified_transaction = transaction_id(raw.mzxid); return out; } static std::vector string_vector_from_raw(const struct String_vector& raw) { std::vector out; out.reserve(raw.count); for (std::int32_t idx = 0; idx < raw.count; ++idx) out.emplace_back(raw.data[idx]); return out; } static acl acl_from_raw(const struct ACL_vector& raw) { auto sz = std::size_t(raw.count); acl out; out.reserve(sz); for (std::size_t idx = 0; idx < sz; ++idx) { const auto& item = raw.data[idx]; out.emplace_back(item.id.scheme, item.id.id, static_cast(item.perms)); } return out; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // connection_zk // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// connection_zk::connection_zk(const connection_params& params) : _handle(nullptr) { if (params.connection_schema() != "zk") zk::throw_exception(std::invalid_argument(std::string("Invalid connection string \"") + to_string(params) + "\"")); auto conn_string = [&] () { std::ostringstream os; bool first = true; for (const auto& host : params.hosts()) { if (first) first = false; else os << ','; os << host; } return os.str(); }(); _handle = ::zookeeper_init(conn_string.c_str(), on_session_event_raw, static_cast(params.timeout().count()), nullptr, this, 0 ); if (!_handle) std::system_error(errno, std::system_category(), "Failed to create ZooKeeper client"); } connection_zk::~connection_zk() noexcept { close(); } class connection_zk::watcher { public: watcher() : _event_delivered(false) { } virtual ~watcher() noexcept {} virtual void deliver_event(event ev) { if (!_event_delivered.exchange(true, std::memory_order_relaxed)) { _event_promise.set_value(std::move(ev)); } } future get_event_future() { return _event_promise.get_future(); } protected: std::atomic _event_delivered; promise _event_promise; }; template class connection_zk::basic_watcher : public connection_zk::watcher { public: basic_watcher() : _data_delivered(false) { } future get_data_future() { return _data_promise.get_future(); } virtual void deliver_event(event ev) override { if (!_data_delivered.load(std::memory_order_relaxed)) { deliver_data(nullopt, get_exception_ptr_of(error_code::closed)); } watcher::deliver_event(std::move(ev)); } void deliver_data(optional data, zk::exception_ptr ex_ptr) { if (!_data_delivered.exchange(true, std::memory_order_relaxed)) { if (ex_ptr) { _data_promise.set_exception(std::move(ex_ptr)); } else { _data_promise.set_value(std::move(*data)); } } } private: std::atomic _data_delivered; promise _data_promise; }; std::shared_ptr connection_zk::try_extract_watch(ptr addr) { std::unique_lock ax(_watches_protect); auto iter = _watches.find(addr); if (iter != _watches.end()) return _watches.extract(iter).mapped(); else return nullptr; } static ptr connection_from_context(ptr zh) { return (ptr) zoo_get_context(zh); } void connection_zk::deliver_watch(ptr zh, int type_in, int state_in, ptr path [[gnu::unused]], ptr proms_in ) { auto& self = *connection_from_context(zh); if (auto watcher = self.try_extract_watch(proms_in)) watcher->deliver_event(event(event_from_raw(type_in), state_from_raw(state_in))); } void connection_zk::close() { if (_handle) { auto err = error_code_from_raw(::zookeeper_close(_handle)); if (err != error_code::ok) throw_error(err); _handle = nullptr; // Deliver a session event as if there was a close. std::unique_lock ax(_watches_protect); auto l_watches = std::move(_watches); ax.unlock(); for (const auto& pair : l_watches) pair.second->deliver_event(event(event_type::session, zk::state::closed)); } } zk::state connection_zk::state() const { if (_handle) return state_from_raw(::zoo_state(_handle)); else return zk::state::closed; } future connection_zk::get(string_view path) { ::data_completion_t callback = [] (int rc_in, ptr data, int data_sz, ptr pstat, ptr prom_in) noexcept { std::unique_ptr> prom((ptr>) prom_in); auto rc = error_code_from_raw(rc_in); if (rc == error_code::ok) prom->set_value(get_result(buffer(data, data + data_sz), stat_from_raw(*pstat))); else prom->set_exception(get_exception_ptr_of(rc)); }; return with_str(path, [&] (ptr path) noexcept { auto ppromise = std::make_unique>(); auto fut = ppromise->get_future(); auto rc = error_code_from_raw(::zoo_aget(_handle, path, 0, callback, ppromise.get())); if (rc == error_code::ok) { ppromise.release(); return fut; } else { ppromise->set_exception(get_exception_ptr_of(rc)); return fut; } }); } class connection_zk::data_watcher : public connection_zk::basic_watcher { public: static void deliver_raw(int rc_in, ptr data, int data_sz, ptr pstat, ptr self_in ) noexcept { auto& self = *static_cast>(const_cast>(self_in)); auto rc = error_code_from_raw(rc_in); if (rc == error_code::ok) { self.deliver_data(watch_result(get_result(buffer(data, data + data_sz), stat_from_raw(*pstat)), self.get_event_future() ), zk::exception_ptr() ); } else { self.deliver_data(nullopt, get_exception_ptr_of(rc)); } } }; future connection_zk::watch(string_view path) { return with_str(path, [&] (ptr path) noexcept { std::unique_lock ax(_watches_protect); auto watcher = std::make_shared(); auto rc = error_code_from_raw(::zoo_awget(_handle, path, deliver_watch, watcher.get(), data_watcher::deliver_raw, watcher.get() ) ); if (rc == error_code::ok) _watches.emplace(watcher.get(), watcher); else watcher->deliver_data(nullopt, get_exception_ptr_of(rc)); return watcher->get_data_future(); }); } future connection_zk::get_children(string_view path) { ::strings_stat_completion_t callback = [] (int rc_in, ptr strings_in, ptr stat_in, ptr prom_in ) { std::unique_ptr> prom((ptr>) prom_in); auto rc = error_code_from_raw(rc_in); try { if (rc != error_code::ok) throw_error(rc); prom->set_value(get_children_result(string_vector_from_raw(*strings_in), stat_from_raw(*stat_in))); } catch (...) { prom->set_exception(zk::current_exception()); } }; return with_str(path, [&] (ptr path) noexcept { auto ppromise = std::make_unique>(); auto fut = ppromise->get_future(); auto rc = error_code_from_raw(::zoo_aget_children2(_handle, path, 0, callback, ppromise.get() ) ); if (rc == error_code::ok) { ppromise.release(); return fut; } else { ppromise->set_exception(get_exception_ptr_of(rc)); return fut; } }); } class connection_zk::child_watcher : public connection_zk::basic_watcher { public: static void deliver_raw(int rc_in, ptr strings_in, ptr stat_in, ptr prom_in ) noexcept { auto& self = *static_cast>(const_cast>(prom_in)); auto rc = error_code_from_raw(rc_in); try { if (rc != error_code::ok) throw_error(rc); self.deliver_data(watch_children_result(get_children_result(string_vector_from_raw(*strings_in), stat_from_raw(*stat_in) ), self.get_event_future() ), zk::exception_ptr() ); } catch (...) { self.deliver_data(nullopt, zk::current_exception()); } } }; future connection_zk::watch_children(string_view path) { return with_str(path, [&] (ptr path) noexcept { std::unique_lock ax(_watches_protect); auto watcher = std::make_shared(); auto rc = error_code_from_raw(::zoo_awget_children2(_handle, path, deliver_watch, watcher.get(), child_watcher::deliver_raw, watcher.get() ) ); if (rc == error_code::ok) _watches.emplace(watcher.get(), watcher); else watcher->deliver_data(nullopt, get_exception_ptr_of(rc)); return watcher->get_data_future(); }); } future connection_zk::exists(string_view path) { ::stat_completion_t callback = [] (int rc_in, ptr stat_in, ptr prom_in) { std::unique_ptr> prom((ptr>) prom_in); auto rc = error_code_from_raw(rc_in); if (rc == error_code::ok) prom->set_value(exists_result(stat_from_raw(*stat_in))); else if (rc == error_code::no_entry) prom->set_value(exists_result(nullopt)); else prom->set_exception(get_exception_ptr_of(rc)); }; return with_str(path, [&] (ptr path) noexcept { auto ppromise = std::make_unique>(); auto fut = ppromise->get_future(); auto rc = error_code_from_raw(::zoo_aexists(_handle, path, 0, callback, ppromise.get())); if (rc == error_code::ok) { ppromise.release(); return fut; } else { ppromise->set_exception(get_exception_ptr_of(rc)); return fut; } }); } class connection_zk::exists_watcher : public connection_zk::basic_watcher { public: static void deliver_raw(int rc_in, ptr stat_in, ptr self_in) { auto& self = *static_cast>(const_cast>(self_in)); auto rc = error_code_from_raw(rc_in); if (rc == error_code::ok) { self.deliver_data(watch_exists_result(exists_result(stat_from_raw(*stat_in)), self.get_event_future()), zk::exception_ptr() ); } else if (rc == error_code::no_entry) { self.deliver_data(watch_exists_result(exists_result(nullopt), self.get_event_future()), zk::exception_ptr() ); } else { self.deliver_data(nullopt, get_exception_ptr_of(rc)); } } }; future connection_zk::watch_exists(string_view path) { return with_str(path, [&] (ptr path) noexcept { std::unique_lock ax(_watches_protect); auto watcher = std::make_shared(); auto rc = error_code_from_raw(::zoo_awexists(_handle, path, deliver_watch, watcher.get(), exists_watcher::deliver_raw, watcher.get() ) ); if (rc == error_code::ok) _watches.emplace(watcher.get(), watcher); else watcher->deliver_data(nullopt, get_exception_ptr_of(rc)); return watcher->get_data_future(); }); } future connection_zk::create(string_view path, const buffer& data, const acl& rules, create_mode mode ) { ::string_completion_t callback = [] (int rc_in, ptr name_in, ptr prom_in) { std::unique_ptr> prom((ptr>) prom_in); auto rc = error_code_from_raw(rc_in); if (rc == error_code::ok) prom->set_value(create_result(std::string(name_in))); else prom->set_exception(get_exception_ptr_of(rc)); }; return with_str(path, [&] (ptr path) noexcept { auto ppromise = std::make_unique>(); auto fut = ppromise->get_future(); auto rc = with_acl(rules, [&] (ptr rules) noexcept { return error_code_from_raw(::zoo_acreate(_handle, path, data.data(), int(data.size()), rules, static_cast(mode), callback, ppromise.get() ) ); }); if (rc == error_code::ok) { ppromise.release(); return fut; } else { ppromise->set_exception(get_exception_ptr_of(rc)); return fut; } }); } future connection_zk::set(string_view path, const buffer& data, version check) { ::stat_completion_t callback = [] (int rc_in, ptr stat_raw, ptr prom_in) { std::unique_ptr> prom((ptr>) prom_in); auto rc = error_code_from_raw(rc_in); if (rc == error_code::ok) prom->set_value(set_result(stat_from_raw(*stat_raw))); else prom->set_exception(get_exception_ptr_of(rc)); }; return with_str(path, [&] (ptr path) noexcept { auto ppromise = std::make_unique>(); auto fut = ppromise->get_future(); auto rc = error_code_from_raw(::zoo_aset(_handle, path, data.data(), int(data.size()), check.value, callback, ppromise.get() )); if (rc == error_code::ok) { ppromise.release(); return fut; } else { ppromise->set_exception(get_exception_ptr_of(rc)); return fut; } }); } future connection_zk::erase(string_view path, version check) { ::void_completion_t callback = [] (int rc_in, ptr prom_in) { std::unique_ptr> prom((ptr>) prom_in); auto rc = error_code_from_raw(rc_in); if (rc == error_code::ok) prom->set_value(); else prom->set_exception(get_exception_ptr_of(rc)); }; return with_str(path, [&] (ptr path) noexcept { auto ppromise = std::make_unique>(); auto fut = ppromise->get_future(); auto rc = error_code_from_raw(::zoo_adelete(_handle, path, check.value, callback, ppromise.get())); if (rc == error_code::ok) { ppromise.release(); return fut; } else { ppromise->set_exception(get_exception_ptr_of(rc)); return fut; } }); } future connection_zk::get_acl(string_view path) const { ::acl_completion_t callback = [] (int rc_in, ptr acl_raw, ptr stat_raw, ptr prom_in) noexcept { std::unique_ptr> prom((ptr>) prom_in); auto rc = error_code_from_raw(rc_in); if (rc == error_code::ok) prom->set_value(get_acl_result(acl_from_raw(*acl_raw), stat_from_raw(*stat_raw))); else prom->set_exception(get_exception_ptr_of(rc)); }; return with_str(path, [&] (ptr path) noexcept { auto ppromise = std::make_unique>(); auto fut = ppromise->get_future(); auto rc = error_code_from_raw(::zoo_aget_acl(_handle, path, callback, ppromise.get())); if (rc == error_code::ok) { ppromise.release(); return fut; } else { ppromise->set_exception(get_exception_ptr_of(rc)); return fut; } }); } future connection_zk::set_acl(string_view path, const acl& rules, acl_version check) { ::void_completion_t callback = [] (int rc_in, ptr prom_in) { std::unique_ptr> prom((ptr>) prom_in); auto rc = error_code_from_raw(rc_in); if (rc == error_code::ok) prom->set_value(); else prom->set_exception(get_exception_ptr_of(rc)); }; return with_str(path, [&] (ptr path) noexcept { return with_acl(rules, [&] (ptr rules) noexcept { auto ppromise = std::make_unique>(); auto fut = ppromise->get_future(); auto rc = error_code_from_raw(::zoo_aset_acl(_handle, path, check.value, rules, callback, ppromise.get() ) ); if (rc == error_code::ok) { ppromise.release(); return fut; } else { ppromise->set_exception(get_exception_ptr_of(rc)); return fut; } }); }); } struct connection_zk_commit_completer { multi_op source_txn; promise prom; std::vector raw_results; std::map raw_stats; std::map> path_buffers; explicit connection_zk_commit_completer(multi_op&& src) : source_txn(std::move(src)), raw_results(source_txn.size()) { for (zoo_op_result_t& x : raw_results) x.err = -42; } ptr raw_stat_at(std::size_t idx) { return &raw_stats[idx]; } ptr> path_buffer_for(std::size_t idx, const std::string& path, create_mode mode) { // If the creation is sequential, append 12 extra characters to store the digits auto sz = path.size() + (is_set(mode, create_mode::sequential) ? 12 : 1); path_buffers[idx] = std::vector(sz); return &path_buffers[idx]; } void deliver(error_code rc) { try { if (rc == error_code::ok) { multi_result out; out.reserve(raw_results.size()); for (std::size_t idx = 0; idx < source_txn.size(); ++idx) { const auto& raw_res = raw_results[idx]; switch (source_txn[idx].type()) { case op_type::create: out.emplace_back(create_result(std::string(raw_res.value))); break; case op_type::set: out.emplace_back(set_result(stat_from_raw(*raw_res.stat))); break; default: out.emplace_back(source_txn[idx].type(), nullptr); break; } } prom.set_value(std::move(out)); } else { // All results until the failure are 0, equal to rc where we care, and runtime_inconsistency after that. auto iter = std::partition_point(raw_results.begin(), raw_results.end(), [] (auto res) { return res.err == 0; } ); zk::throw_exception(transaction_failed(rc, std::size_t(std::distance(raw_results.begin(), iter)))); } } catch (...) { prom.set_exception(zk::current_exception()); } } }; future connection_zk::commit(multi_op&& txn_in) { ::void_completion_t callback = [] (int rc_in, ptr completer_in) { std::unique_ptr completer((ptr) completer_in); completer->deliver(error_code_from_raw(rc_in)); }; auto pcompleter = std::make_unique(std::move(txn_in)); auto& txn = pcompleter->source_txn; try { ::zoo_op raw_ops[txn.size()]; std::size_t create_op_count = 0; std::size_t acl_piece_count = 0; for (const auto& tx : txn) { if (tx.type() == op_type::create) { ++create_op_count; acl_piece_count += tx.as_create().rules.size(); } } ACL_vector encoded_acls[create_op_count]; ACL acl_pieces[acl_piece_count]; ptr encoded_acl_iter = encoded_acls; ptr acl_piece_iter = acl_pieces; for (std::size_t idx = 0; idx < txn.size(); ++idx) { auto& raw_op = raw_ops[idx]; auto& src_op = txn[idx]; switch (src_op.type()) { case op_type::check: zoo_check_op_init(&raw_op, src_op.as_check().path.c_str(), src_op.as_check().check.value); break; case op_type::create: { const auto& cdata = src_op.as_create(); encoded_acl_iter->count = int(cdata.rules.size()); encoded_acl_iter->data = acl_piece_iter; for (const auto& acl : cdata.rules) { *acl_piece_iter = encode_acl_part(acl); ++acl_piece_iter; } auto path_buf_ref = pcompleter->path_buffer_for(idx, cdata.path, cdata.mode); zoo_create_op_init(&raw_op, cdata.path.c_str(), cdata.data.data(), int(cdata.data.size()), encoded_acl_iter, static_cast(cdata.mode), path_buf_ref->data(), int(path_buf_ref->size()) ); ++encoded_acl_iter; break; } case op_type::erase: zoo_delete_op_init(&raw_op, src_op.as_erase().path.c_str(), src_op.as_erase().check.value); break; case op_type::set: { const auto& setting = src_op.as_set(); zoo_set_op_init(&raw_op, setting.path.c_str(), setting.data.data(), int(setting.data.size()), setting.check.value, pcompleter->raw_stat_at(idx) ); break; } default: { using std::to_string; zk::throw_exception(std::invalid_argument("Invalid op_type at index=" + to_string(idx) + ": " + to_string(src_op.type()) )); } } } auto fut = pcompleter->prom.get_future(); auto rc = error_code_from_raw(::zoo_amulti(_handle, int(txn.size()), raw_ops, pcompleter->raw_results.data(), callback, pcompleter.get() ) ); if (rc == error_code::ok) { pcompleter.release(); return fut; } else { pcompleter->prom.set_exception(get_exception_ptr_of(rc)); return pcompleter->prom.get_future(); } } catch (...) { pcompleter->prom.set_exception(zk::current_exception()); return pcompleter->prom.get_future(); } } future connection_zk::load_fence() { ::string_completion_t callback = [] (int rc_in, ptr, ptr prom_in) { std::unique_ptr> prom((ptr>) prom_in); auto rc = error_code_from_raw(rc_in); if (rc == error_code::ok) prom->set_value(); else prom->set_exception(get_exception_ptr_of(rc)); }; auto ppromise = std::make_unique>(); auto rc = error_code_from_raw(::zoo_async(_handle, "/", callback, ppromise.get())); if (rc == error_code::ok) { auto f = ppromise->get_future(); ppromise.release(); return f; } else { ppromise->set_exception(get_exception_ptr_of(rc)); return ppromise->get_future(); } } void connection_zk::on_session_event_raw(ptr handle [[gnu::unused]], int ev_type, int state, ptr path_ptr, ptr watcher_ctx ) noexcept { auto self = static_cast>(watcher_ctx); // Most of the time, self's _handle will be the same thing that ZK provides to us. However, if we connect very // quickly, a session event will happen trigger *before* we set the _handle. This isn't a problem, just something to // be aware of. assert(self->_handle == nullptr || self->_handle == handle); auto ev = event_from_raw(ev_type); auto st = state_from_raw(state); auto path = string_view(path_ptr); if (ev != event_type::session) { // TODO: Remove this usage of std::cerr std::cerr << "WARNING: Got unexpected event " << ev << " in state=" << st << " with path=" << path << std::endl; return; } self->on_session_event(st); } } ================================================ FILE: src/zk/connection_zk.hpp ================================================ #pragma once #include #include #include #include #include #include "connection.hpp" #include "string_view.hpp" typedef struct _zhandle zhandle_t; namespace zk { /// \addtogroup Client /// \{ class connection_zk final : public connection { public: explicit connection_zk(const connection_params& params); virtual ~connection_zk() noexcept; virtual void close() override; virtual zk::state state() const override; virtual future get(string_view path) override; virtual future watch(string_view path) override; virtual future get_children(string_view path) override; virtual future watch_children(string_view path) override; virtual future exists(string_view path) override; virtual future watch_exists(string_view path) override; virtual future create(string_view path, const buffer& data, const acl& rules, create_mode mode ) override; virtual future set(string_view path, const buffer& data, version check) override; virtual future erase(string_view path, version check) override; virtual future get_acl(string_view path) const override; virtual future set_acl(string_view path, const acl& rules, acl_version check) override; virtual future commit(multi_op&& txn) override; virtual future load_fence() override; private: static void on_session_event_raw(ptr handle, int ev_type, int state, ptr path, ptr watcher_ctx ) noexcept; using watch_function = void (*)(ptr, int type_in, int state_in, ptr, ptr); class watcher; template class basic_watcher; class data_watcher; class child_watcher; class exists_watcher; /** Erase the watch tracker for the watch with the value \a p. * * \returns \c true if it was deleted (the watch should be delivered); \c false if \a p was not in the list. **/ std::shared_ptr try_extract_watch(ptr p); static void deliver_watch(ptr zh, int type_in, int state_in, ptr, ptr proms_in); private: ptr _handle; std::unordered_map, std::shared_ptr> _watches; mutable std::mutex _watches_protect; }; /// \} } ================================================ FILE: src/zk/error.cpp ================================================ #include "error.hpp" #include "exceptions.hpp" #include #include #include namespace zk { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // error_code // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// std::ostream& operator<<(std::ostream& os, const error_code& code) { switch (code) { case error_code::ok: return os << "ok"; default: return os << "error_code(" << static_cast(code) << ')'; } } std::string to_string(const error_code& code) { std::ostringstream os; os << code; return os.str(); } void throw_error(error_code code) { switch (code) { case error_code::connection_loss: zk::throw_exception( connection_loss() ); case error_code::marshalling_error: zk::throw_exception( marshalling_error() ); case error_code::not_implemented: zk::throw_exception( not_implemented("unspecified") ); case error_code::invalid_arguments: zk::throw_exception( invalid_arguments() ); case error_code::new_configuration_no_quorum: zk::throw_exception( new_configuration_no_quorum() ); case error_code::reconfiguration_in_progress: zk::throw_exception( reconfiguration_in_progress() ); case error_code::no_entry: zk::throw_exception( no_entry() ); case error_code::not_authorized: zk::throw_exception( not_authorized() ); case error_code::version_mismatch: zk::throw_exception( version_mismatch() ); case error_code::no_children_for_ephemerals: zk::throw_exception( no_children_for_ephemerals() ); case error_code::entry_exists: zk::throw_exception( entry_exists() ); case error_code::not_empty: zk::throw_exception( not_empty() ); case error_code::session_expired: zk::throw_exception( session_expired() ); case error_code::authentication_failed: zk::throw_exception( authentication_failed() ); case error_code::closed: zk::throw_exception( closed() ); case error_code::read_only_connection: zk::throw_exception( read_only_connection() ); case error_code::ephemeral_on_local_session: zk::throw_exception( ephemeral_on_local_session() ); case error_code::reconfiguration_disabled: zk::throw_exception( reconfiguration_disabled() ); case error_code::transaction_failed: zk::throw_exception( transaction_failed(error_code::transaction_failed, 0U) ); default: zk::throw_exception( error(code, "unknown") ); } } zk::exception_ptr get_exception_ptr_of(error_code code) { try { throw_error(code); } catch (...) { return zk::current_exception(); } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // error_category // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// class error_category_impl final : public std::error_category { public: virtual ptr name() const noexcept override { return "zookeeper"; } virtual std::string message(int condition) const override; }; std::string error_category_impl::message(int condition) const { return to_string(static_cast(condition)); } const std::error_category& error_category() { static error_category_impl instance; return instance; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // errors // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// static std::string format_error(error_code code, const std::string& description) { if (description.empty()) return to_string(code); else return to_string(code) + ": " + description; } error::error(error_code code, const std::string& description) : std::system_error(static_cast(code), error_category(), format_error(code, description)), _code(code) { } error::~error() noexcept = default; transport_error::transport_error(error_code code, const std::string& description) : error(code, std::move(description)) { } transport_error::~transport_error() noexcept = default; connection_loss::connection_loss() : transport_error(error_code::connection_loss, "") { } connection_loss::~connection_loss() noexcept = default; marshalling_error::marshalling_error() : transport_error(error_code::marshalling_error, "") { } marshalling_error::~marshalling_error() noexcept = default; not_implemented::not_implemented(ptr op_name) : error(error_code::not_implemented, std::string("Operation not implemented: ") + op_name) { } not_implemented::~not_implemented() noexcept = default; invalid_arguments::invalid_arguments(error_code code, const std::string& description) : error(code, description) { } invalid_arguments::invalid_arguments() : invalid_arguments(error_code::invalid_arguments, "") { } invalid_arguments::~invalid_arguments() noexcept = default; authentication_failed::authentication_failed() : invalid_arguments(error_code::authentication_failed, "") { } authentication_failed::~authentication_failed() noexcept = default; invalid_ensemble_state::invalid_ensemble_state(error_code code, const std::string& description) : error(code, description) { } invalid_ensemble_state::~invalid_ensemble_state() noexcept = default; new_configuration_no_quorum::new_configuration_no_quorum() : invalid_ensemble_state(error_code::new_configuration_no_quorum, "") { } new_configuration_no_quorum::~new_configuration_no_quorum() noexcept = default; reconfiguration_in_progress::reconfiguration_in_progress() : invalid_ensemble_state(error_code::reconfiguration_in_progress, "") { } reconfiguration_in_progress::~reconfiguration_in_progress() noexcept = default; reconfiguration_disabled::reconfiguration_disabled() : invalid_ensemble_state(error_code::reconfiguration_disabled, "") { } reconfiguration_disabled::~reconfiguration_disabled() noexcept = default; invalid_connection_state::invalid_connection_state(error_code code, const std::string& description) : error(code, description) { } invalid_connection_state::~invalid_connection_state() noexcept = default; session_expired::session_expired() : invalid_connection_state(error_code::session_expired, "") { } session_expired::~session_expired() noexcept = default; not_authorized::not_authorized() : invalid_connection_state(error_code::not_authorized, "") { } not_authorized::~not_authorized() noexcept = default; closed::closed() : invalid_connection_state(error_code::closed, "") { } closed::~closed() noexcept = default; ephemeral_on_local_session::ephemeral_on_local_session() : invalid_connection_state(error_code::ephemeral_on_local_session, "") { } ephemeral_on_local_session::~ephemeral_on_local_session() noexcept = default; read_only_connection::read_only_connection() : invalid_connection_state(error_code::read_only_connection, "") { } read_only_connection::~read_only_connection() noexcept = default; check_failed::check_failed(error_code code, const std::string& description) : error(code, description) { } check_failed::~check_failed() noexcept = default; no_entry::no_entry() : check_failed(error_code::no_entry, "") { } no_entry::~no_entry() noexcept = default; entry_exists::entry_exists() : check_failed(error_code::entry_exists, "") { } entry_exists::~entry_exists() noexcept = default; not_empty::not_empty() : check_failed(error_code::not_empty, "") { } not_empty::~not_empty() noexcept = default; version_mismatch::version_mismatch() : check_failed(error_code::version_mismatch, "") { } version_mismatch::~version_mismatch() noexcept = default; no_children_for_ephemerals::no_children_for_ephemerals() : check_failed(error_code::no_children_for_ephemerals, "") { } no_children_for_ephemerals::~no_children_for_ephemerals() noexcept = default; transaction_failed::transaction_failed(error_code underlying_cause, std::size_t op_index) : check_failed(error_code::transaction_failed, std::string("Could not commit transaction due to ") + to_string(underlying_cause) + " on operation " + std::to_string(op_index) ), _underlying_cause(underlying_cause), _op_index(op_index) { } transaction_failed::~transaction_failed() noexcept = default; } ================================================ FILE: src/zk/error.hpp ================================================ #pragma once #include #include "exceptions.hpp" #include #include #include namespace zk { /// \addtogroup Client /// \{ /// Code for all \ref error types thrown by the client library. /// /// \see error enum class error_code : int { ok = 0, //!< Never thrown. connection_loss = -4, //!< Code for \ref connection_loss. marshalling_error = -5, //!< Code for \ref marshalling_error. not_implemented = -6, //!< Code for \ref not_implemented. invalid_arguments = -8, //!< Code for \ref invalid_arguments. new_configuration_no_quorum = -13, //!< Code for \ref new_configuration_no_quorum. reconfiguration_in_progress = -14, //!< Code for \ref reconfiguration_in_progress. no_entry = -101, //!< Code for \ref no_entry. not_authorized = -102, //!< Code for \ref not_authorized. version_mismatch = -103, //!< Code for \ref version_mismatch. no_children_for_ephemerals = -108, //!< Code for \ref no_children_for_ephemerals. entry_exists = -110, //!< Code for \ref entry_exists. not_empty = -111, //!< Code for \ref not_empty. session_expired = -112, //!< Code for \ref session_expired. authentication_failed = -115, //!< Code for \ref authentication_failed. closed = -116, //!< Code for \ref closed. read_only_connection = -119, //!< Code for \ref read_only_connection. ephemeral_on_local_session = -120, //!< Code for \ref ephemeral_on_local_session. reconfiguration_disabled = -123, //!< Code for \ref reconfiguration_disabled. transaction_failed = -199, //!< Code for \ref transaction_failed. }; /// Check if the provided \a code is an exception code for a \ref transport_error type of exception. inline constexpr bool is_transport_error(error_code code) { return code == error_code::connection_loss || code == error_code::marshalling_error; } /// Check if the provided \a code is an exception code for a \ref invalid_arguments type of exception. inline constexpr bool is_invalid_arguments(error_code code) { return code == error_code::invalid_arguments || code == error_code::authentication_failed; } /// Check if the provided \a code is an exception code for a \ref invalid_ensemble_state type of exception. inline constexpr bool is_invalid_ensemble_state(error_code code) { return code == error_code::new_configuration_no_quorum || code == error_code::reconfiguration_disabled || code == error_code::reconfiguration_in_progress; } /// Check if the provided \a code is an exception code for a \ref invalid_connection_state type of exception. inline constexpr bool is_invalid_connection_state(error_code code) { return code == error_code::closed || code == error_code::ephemeral_on_local_session || code == error_code::not_authorized || code == error_code::read_only_connection || code == error_code::session_expired; } /// Check if the provided \a code is an exception code for a \ref check_failed type of exception. inline constexpr bool is_check_failed(error_code code) { return code == error_code::no_children_for_ephemerals || code == error_code::no_entry || code == error_code::entry_exists || code == error_code::not_empty || code == error_code::transaction_failed || code == error_code::version_mismatch; } std::ostream& operator<<(std::ostream&, const error_code&); std::string to_string(const error_code&); /// Throw an exception for the given \a code. This will use the proper refined exception type (such as \ref no_entry) if /// one exists. [[noreturn]] void throw_error(error_code code); /// Get an \c zk::exception_ptr containing an exception with the proper type for the given \a code. /// /// \see throw_error zk::exception_ptr get_exception_ptr_of(error_code code); /// Get the \c std::error_category capable of describing ZooKeeper-provided error codes. /// /// \see error const std::error_category& error_category(); /// Base error type for all errors raised by this library. /// /// \see error_code class error : public std::system_error { public: explicit error(error_code code, const std::string& description); virtual ~error() noexcept; /// The code representation of this error. error_code code() const { return _code; } private: error_code _code; }; /// Base types for errors that occurred while transporting data across a network. /// /// \see is_transport_error class transport_error : public error { public: explicit transport_error(error_code code, const std::string& description); virtual ~transport_error() noexcept; }; /// Connection to the server has been lost before the attempted operation was verified as completed. /// /// When thrown on an attempt to perform a modification, it is important to remember that it is possible to see this /// error and have the operation be a success. For example, a \ref client::set operation can complete on the server, but /// the client can experience a \ref connection_loss before the server replies with OK. /// /// \see session_expired class connection_loss: public transport_error { public: explicit connection_loss(); virtual ~connection_loss() noexcept; }; /// An error occurred while marshalling data. The most common cause of this is exceeding the Jute buffer size -- meaning /// the transaction was too large (check the server logs for messages containing `"Unreasonable length"`). If that is /// the case, the solution is to change `jute.maxbuffer` on all servers (see the /// ZooKeeper Administrator's Guide for more /// information and a stern warning). Another possible cause is the system running out of memory, but due to overcommit, /// OOM issues rarely manifest so cleanly. class marshalling_error: public transport_error { public: explicit marshalling_error(); virtual ~marshalling_error() noexcept; }; /// Operation was attempted that was not implemented. If you happen to be writing a \ref connection implementation, you /// are encouraged to raise this error in cases where you have not implemented an operation. class not_implemented: public error { public: /// \param op_name the name of the attempted operation. explicit not_implemented(ptr op_name); virtual ~not_implemented() noexcept; }; /// Arguments to an operation were invalid. class invalid_arguments : public error { public: explicit invalid_arguments(error_code code, const std::string& description); explicit invalid_arguments(); virtual ~invalid_arguments() noexcept; }; /// The server rejected the connection due to invalid authentication information. Depending on the authentication /// schemes enabled on the server, the authentication information sent might be explicit (in the case of the \c "digest" /// scheme) or implicit (in the case of the \c "ip" scheme). The connection must be recreated with the proper /// credentials to function. /// /// \see acl_rule class authentication_failed: public invalid_arguments { public: explicit authentication_failed(); virtual ~authentication_failed() noexcept; }; /// Base exception for cases where the ensemble is in an invalid state to perform a given action. While errors such as /// \ref connection_loss might also imply a bad ensemble (no quorum means no connection is possible), these errors are /// explicit rejections from the server. class invalid_ensemble_state : public error { public: explicit invalid_ensemble_state(error_code code, const std::string& description); virtual ~invalid_ensemble_state() noexcept; }; /// Raised when attempting an ensemble reconfiguration, but the proposed new ensemble would not be able to form quorum. /// This happens when not enough time has passed for potential new servers to sync with the leader. If the proposed new /// ensemble is up and running, the solution is usually to simply wait longer and attempt reconfiguration later. class new_configuration_no_quorum: public invalid_ensemble_state { public: explicit new_configuration_no_quorum(); virtual ~new_configuration_no_quorum() noexcept; }; /// An attempt was made to reconfigure the ensemble, but there is already a reconfiguration in progress. Concurrent /// reconfiguration is not supported. class reconfiguration_in_progress: public invalid_ensemble_state { public: explicit reconfiguration_in_progress(); virtual ~reconfiguration_in_progress() noexcept; }; /// The ensemble does not support reconfiguration. class reconfiguration_disabled: public invalid_ensemble_state { public: explicit reconfiguration_disabled(); virtual ~reconfiguration_disabled() noexcept; }; /// Base type for errors generated because the connection is misconfigured. class invalid_connection_state : public error { public: explicit invalid_connection_state(error_code code, const std::string& description); virtual ~invalid_connection_state() noexcept; }; /// The client session has been ended by the server. When this occurs, all ephemerals associated with the session are /// deleted and standing watches are cancelled. /// /// This error is somewhat easy to confuse with \ref connection_loss, as they commonly happen around the same time. The /// key difference is a \ref session_expired is an explicit error delivered from the server, whereas /// \ref connection_loss is a client-related notification. A \ref connection_loss is \e usually followed by /// \ref session_expired, but this is not guaranteed. If the client reconnects to a different server before the quorum /// removes the session, the connection can move back to \ref state::connected without losing the session. The mechanism /// of resuming a session can happen even in cases of quorum loss, as session expiration requires a leader in order to /// proceed, so a client reconnecting soon enough after the ensemble forms quorum and elects a leader will resume the /// session, even if the quorum has been lost for days. class session_expired: public invalid_connection_state { public: explicit session_expired(); virtual ~session_expired() noexcept; }; /// An attempt was made to read or write to a ZNode when the connection does not have permission to do. class not_authorized: public invalid_connection_state { public: explicit not_authorized(); virtual ~not_authorized() noexcept; }; /// The connection is closed. This exception is delivered for all unfilled operations and watches when the connection is /// closing. class closed: public invalid_connection_state { public: explicit closed(); virtual ~closed() noexcept; }; /// An attempt was made to create an ephemeral entry, but the connection has a local session. /// /// \see connection_params::local class ephemeral_on_local_session: public invalid_connection_state { public: explicit ephemeral_on_local_session(); virtual ~ephemeral_on_local_session() noexcept; }; /// A write operation was attempted on a read-only connection. /// /// \see connection_params::read_only class read_only_connection : public invalid_connection_state { public: explicit read_only_connection(); virtual ~read_only_connection() noexcept; }; /// Base exception for cases where a write operation was rolled back due to a failed check. There are more details in /// derived types such as \ref no_entry or \ref bad_version. class check_failed : public error { public: explicit check_failed(error_code code, const std::string& description); virtual ~check_failed() noexcept; }; /// Thrown from read operations when attempting to read a ZNode that does not exist. class no_entry : public check_failed { public: explicit no_entry(); virtual ~no_entry() noexcept; }; /// Thrown when attempting to create a ZNode, but one already exists at the specified path. class entry_exists : public check_failed { public: explicit entry_exists(); virtual ~entry_exists() noexcept; }; /// Thrown when attempting to erase a ZNode that has children. class not_empty : public check_failed { public: explicit not_empty(); virtual ~not_empty() noexcept; }; /// Thrown from modification operations when a version check is specified and the value in the database does not match /// the expected. class version_mismatch : public check_failed { public: explicit version_mismatch(); virtual ~version_mismatch() noexcept; }; /// Ephemeral ZNodes cannot have children. class no_children_for_ephemerals : public check_failed { public: explicit no_children_for_ephemerals(); virtual ~no_children_for_ephemerals() noexcept; }; /// Thrown from \ref client::commit when a transaction cannot be committed to the system. Check the /// \ref underlying_cause to see the specific error and \ref failed_op_index to see what operation failed. class transaction_failed : public check_failed { public: explicit transaction_failed(error_code code, std::size_t op_index); virtual ~transaction_failed() noexcept; /// The underlying cause that caused this transaction to be aborted. For example, if a \ref op::set operation is /// attempted on a node that does not exist, this will be \ref error_code::no_entry. error_code underlying_cause() const { return _underlying_cause; } /// The transaction index which caused the error (0 indexed). If the 3rd operation in the \ref multi_op could not be /// committed, this will be 2. std::size_t failed_op_index() const { return _op_index; } private: error_code _underlying_cause; std::size_t _op_index; }; /// \} } namespace std { template <> struct is_error_code_enum : true_type { }; } ================================================ FILE: src/zk/error_tests.cpp ================================================ #include #include "error.hpp" namespace zk { static error_code all_error_codes[] = { error_code::connection_loss, error_code::marshalling_error, error_code::not_implemented, error_code::invalid_arguments, error_code::new_configuration_no_quorum, error_code::reconfiguration_in_progress, error_code::no_entry, error_code::not_authorized, error_code::version_mismatch, error_code::no_children_for_ephemerals, error_code::entry_exists, error_code::not_empty, error_code::session_expired, error_code::authentication_failed, error_code::closed, error_code::read_only_connection, error_code::ephemeral_on_local_session, error_code::reconfiguration_disabled, error_code::transaction_failed, }; GTEST_TEST(error_code_tests, throwing) { for (error_code code : all_error_codes) { try { try { throw_error(code); } catch (const check_failed& ex) { CHECK_EQ(code, ex.code()); CHECK_TRUE(is_check_failed(code)) << code; throw; } catch (const invalid_arguments& ex) { CHECK_EQ(code, ex.code()); CHECK_TRUE(is_invalid_arguments(code)) << code; throw; } catch (const invalid_connection_state& ex) { CHECK_EQ(code, ex.code()); CHECK_TRUE(is_invalid_connection_state(code)) << code; throw; } catch (const invalid_ensemble_state& ex) { CHECK_EQ(code, ex.code()); CHECK_TRUE(is_invalid_ensemble_state(code)) << code; throw; } catch (const not_implemented& ex) { CHECK_EQ(code, ex.code()); CHECK_EQ(error_code::not_implemented, ex.code()); throw; } catch (const transport_error& ex) { CHECK_EQ(code, ex.code()); throw; } catch (const error& ex) { // not a real error, so it will come across as unknown CHECK_EQ(error_code::ok, ex.code()); throw; } CHECK_FAIL() << "Should not have reached this point"; } catch (const error& ex) { CHECK_EQ(code, ex.code()); } } } GTEST_TEST(error_code_tests, to_string_bogus_code) { CHECK_EQ("error_code(19)", to_string(static_cast(19))); } } ================================================ FILE: src/zk/exceptions.cpp ================================================ #include "exceptions.hpp" namespace zk { exception_ptr current_exception() noexcept { #if ZKPP_FUTURE_USE_BOOST return boost::current_exception(); #else return std::current_exception(); #endif } } ================================================ FILE: src/zk/exceptions.hpp ================================================ /** \file * Controls the throwing of exceptions and import of \c exception_ptr types and \c current_exception implementation. * These are probably \c std::exception_ptr and \c std::current_exception(), but if boost future is used it will be * boost's implementation. **/ #pragma once #include #if ZKPP_FUTURE_USE_BOOST #include #else #include #endif namespace zk { /// \addtogroup Client /// \{ #if ZKPP_FUTURE_USE_BOOST using exception_ptr = boost::exception_ptr; exception_ptr current_exception() noexcept; template [[noreturn]] inline void throw_exception(T const & e) { throw boost::enable_current_exception(e); } #else using exception_ptr = std::exception_ptr; exception_ptr current_exception() noexcept; template [[noreturn]] inline void throw_exception(T const & e) { throw e; } #endif /// \} } ================================================ FILE: src/zk/forwards.hpp ================================================ #pragma once #include namespace zk { class acl; class acl_rule; struct acl_version; struct child_version; class client; class connection; class connection_params; enum class create_mode : unsigned int; class create_result; class error; class event; class exists_result; enum class error_code : int; enum class event_type : int; class get_acl_result; class get_children_result; class get_result; class multi_result; class multi_op; class op; enum class op_type : int; enum class permission : unsigned int; class set_result; enum class state : int; struct transaction_id; struct version; class watch_children_result; class watch_exists_result; class watch_result; } ================================================ FILE: src/zk/future.hpp ================================================ /** \file * Controls the import of \c future and \c promise types. These are probably \c std::future and \c std::promise, but * can be your custom types (as long as they behave in a manner similar enough to \c std::future and \c std::promise). **/ #pragma once #include /** \addtogroup Client * \{ **/ /** \def ZKPP_FUTURE_USE_STD_EXPERIMENTAL * Set this to 1 to use \c std::experimental::future and \c std::experimental::promise as the backing types for * \c zk::future and \c zk::promise. **/ #ifndef ZKPP_FUTURE_USE_STD_EXPERIMENTAL # define ZKPP_FUTURE_USE_STD_EXPERIMENTAL 0 #endif /** \def ZKPP_FUTURE_USE_BOOST * Set this to 1 to use \c boost::future and \c boost::promise as the backing types for * \c zk::future and \c zk::promise. **/ #ifndef ZKPP_FUTURE_USE_BOOST # define ZKPP_FUTURE_USE_BOOST 0 #endif /** \def ZKPP_FUTURE_USE_CUSTOM * Set this to 1 to use custom definitions of \c zk::future and \c zk::promise. If this is set, you must also set * \c ZKPP_FUTURE_TEMPLATE, \c ZKPP_PROMISE_TEMPLATE, and \c ZKPP_FUTURE_INCLUDE. * * \def ZKPP_FUTURE_TEMPLATE * The template to use for \c zk::future. By default, this is \c std::future. * * \def ZKPP_PROMISE_TEMPLATE * The template to use for \c zk::promise. This should be highly related to \c ZKPP_FUTURE_TEMPLATE * * \def ZKPP_ASYNC_TEMPLATE * The template to use for \c zk::async. This should be highly related to \c ZKPP_FUTURE_TEMPLATE and usually mapped * to std::async or boost::async. * * \def ZKPP_LAUNCH_ENUM * The enum to use for \c zk::async launch policy. This should be highly related to \c ZKPP_FUTURE_TEMPLATE and usually mapped * to std::launch::async or boost::launch::async. * * \def ZKPP_FUTURE_INCLUDE * The file to include to get the implementation for \c future and \c promise. If you define \c ZKPP_FUTURE_TEMPLATE * and \c ZKPP_PROMISE_TEMPLATE, you must also define this. **/ #ifndef ZKPP_FUTURE_USE_CUSTOM # define ZKPP_FUTURE_USE_CUSTOM 0 #endif /** \def ZKPP_FUTURE_USE_STD * Set this to 1 to use \c std::future and \c std::promise as the backing types for \c zk::future and \c zk::promise. * This is the default behavior. **/ #ifndef ZKPP_FUTURE_USE_STD # if ZKPP_FUTURE_USE_BOOST || ZKPP_FUTURE_USE_STD_EXPERIMENTAL || ZKPP_FUTURE_USE_CUSTOM # define ZKPP_FUTURE_USE_STD 0 # else # define ZKPP_FUTURE_USE_STD 1 # endif #endif #if ZKPP_FUTURE_USE_STD # define ZKPP_FUTURE_INCLUDE # define ZKPP_FUTURE_TEMPLATE std::future # define ZKPP_PROMISE_TEMPLATE std::promise # define ZKPP_ASYNC_TEMPLATE std::async # define ZKPP_LAUNCH_ENUM std::launch #elif ZKPP_FUTURE_USE_STD_EXPERIMENTAL # define ZKPP_FUTURE_INCLUDE # define ZKPP_FUTURE_TEMPLATE std::experimental::future # define ZKPP_PROMISE_TEMPLATE std::experimental::promise # define ZKPP_ASYNC_TEMPLATE std::async # define ZKPP_LAUNCH_ENUM std::launch #elif ZKPP_FUTURE_USE_BOOST # define BOOST_THREAD_PROVIDES_FUTURE # define BOOST_THREAD_PROVIDES_FUTURE_CONTINUATION # define BOOST_THREAD_PROVIDES_FUTURE_WHEN_ALL_WHEN_ANY # define ZKPP_FUTURE_INCLUDE # define ZKPP_FUTURE_TEMPLATE boost::future # define ZKPP_PROMISE_TEMPLATE boost::promise # define ZKPP_ASYNC_TEMPLATE boost::async # define ZKPP_LAUNCH_ENUM boost::launch #elif ZKPP_FUTURE_USE_CUSTOM # if !defined ZKPP_FUTURE_TEMPLATE || !defined ZKPP_PROMISE_TEMPLATE || !defined ZKPP_FUTURE_INCLUDE # error "When ZKPP_FUTURE_USE_CUSTOM is set, you must also define ZKPP_FUTURE_TEMPLATE, ZKPP_PROMISE_TEMPLATE," # error "and ZKPP_FUTURE_INCLUDE." # endif #else # error "Unknown type to use for zk::future and zk::promise" #endif #include ZKPP_FUTURE_INCLUDE /** \} **/ namespace zk { /** \addtogroup Client * \{ **/ template using future = ZKPP_FUTURE_TEMPLATE; template using promise = ZKPP_PROMISE_TEMPLATE; using ZKPP_LAUNCH_ENUM; using ZKPP_ASYNC_TEMPLATE; /** \} **/ } ================================================ FILE: src/zk/multi.cpp ================================================ #include "multi.hpp" #include "exceptions.hpp" #include #include #include #include namespace zk { template static std::string to_string_generic(const T& self) { std::ostringstream os; os << self; return os.str(); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // op_type // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// std::ostream& operator<<(std::ostream& os, const op_type& self) { switch (self) { case op_type::check: return os << "check"; case op_type::create: return os << "create"; case op_type::erase: return os << "erase"; case op_type::set: return os << "set"; default: return os << "op_type(" << static_cast(self) << ')'; } } std::string to_string(const op_type& self) { return to_string_generic(self); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // op // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// op::op(any_data&& src) noexcept : _storage(std::move(src)) { } op::op(const op& src) = default; op::op(op&& src) noexcept : _storage(std::move(src._storage)) { } op::~op() noexcept = default; op_type op::type() const { return std::visit([] (const auto& x) { return x.type(); }, _storage); } template const T& op::as(ptr operation) const { try { return std::get(_storage); } catch (const std::bad_variant_access&) { zk::throw_exception(std::logic_error( std::string("Invalid op type for op::") + std::string(operation) + std::string(": ") + to_string(type()) )); } } // check op::check_data::check_data(std::string path, version check_) : path(std::move(path)), check(check_) { } std::ostream& operator<<(std::ostream& os, const op::check_data& self) { os << '{' << self.path; os << ' ' << self.check; return os << '}'; } op op::check(std::string path, version check_) { return op(check_data(std::move(path), check_)); } const op::check_data& op::as_check() const { return as("as_check"); } // create op::create_data::create_data(std::string path, buffer data, acl rules, create_mode mode) : path(std::move(path)), data(std::move(data)), rules(std::move(rules)), mode(mode) { } std::ostream& operator<<(std::ostream& os, const op::create_data& self) { os << '{' << self.path; os << ' ' << self.mode; os << ' ' << self.rules; return os << '}'; } op op::create(std::string path, buffer data, acl rules, create_mode mode) { return op(create_data(std::move(path), std::move(data), std::move(rules), mode)); } op op::create(std::string path, buffer data, create_mode mode) { return create(std::move(path), std::move(data), acls::open_unsafe(), mode); } const op::create_data& op::as_create() const { return as("as_create"); } // erase op::erase_data::erase_data(std::string path, version check) : path(std::move(path)), check(check) { } std::ostream& operator<<(std::ostream& os, const op::erase_data& self) { os << '{' << self.path; os << ' ' << self.check; return os << '}'; } op op::erase(std::string path, version check) { return op(erase_data(std::move(path), check)); } const op::erase_data& op::as_erase() const { return as("as_erase"); } // set op::set_data::set_data(std::string path, buffer data, version check) : path(std::move(path)), data(std::move(data)), check(check) { } std::ostream& operator<<(std::ostream& os, const op::set_data& self) { os << '{' << self.path; os << ' ' << self.check; return os << '}'; } op op::set(std::string path, buffer data, version check) { return op(set_data(std::move(path), std::move(data), check)); } const op::set_data& op::as_set() const { return as("as_set"); } // generic std::ostream& operator<<(std::ostream& os, const op& self) { os << self.type(); std::visit([&] (const auto& x) { os << x; }, self._storage); return os; } std::string to_string(const op& self) { std::ostringstream os; os << self; return os.str(); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // multi_op // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// multi_op::multi_op(std::vector ops) noexcept : _ops(std::move(ops)) { } multi_op::~multi_op() noexcept { } std::ostream& operator<<(std::ostream& os, const multi_op& self) { os << '['; bool first = true; for (const auto& x : self) { if (first) first = false; else os << ", "; os << x; } return os << ']'; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // multi_result // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// multi_result::part::part(op_type type, std::nullptr_t) noexcept : _type(type), _storage(std::monostate()) { } multi_result::part::part(create_result res) noexcept : _type(op_type::create), _storage(std::move(res)) { } multi_result::part::part(set_result res) noexcept : _type(op_type::set), _storage(std::move(res)) { } multi_result::part::part(const part& src) = default; multi_result::part::part(part&& src) noexcept : _type(src._type), _storage(std::move(src._storage)) { } multi_result::part::~part() noexcept = default; template const T& multi_result::part::as(ptr operation) const { try { return std::get(_storage); } catch (const std::bad_variant_access&) { zk::throw_exception(std::logic_error( std::string("Invalid op type for multi_result::") + std::string(operation) + std::string(": ") + to_string(type()) )); } } const create_result& multi_result::part::as_create() const { return as("as_create"); } const set_result& multi_result::part::as_set() const { return as("as_set"); } multi_result::multi_result(std::vector parts) noexcept : _parts(std::move(parts)) { } multi_result::~multi_result() noexcept { } std::ostream& operator<<(std::ostream& os, const multi_result::part& self) { switch (self.type()) { case op_type::create: return os << self.as_create(); case op_type::set: return os << self.as_set(); default: return os << self.type() << "_result{}"; } } std::ostream& operator<<(std::ostream& os, const multi_result& self) { os << '['; bool first = true; for (const auto& x : self) { if (first) first = false; else os << ", "; os << x; } return os << ']'; } std::string to_string(const multi_result::part& self) { return to_string_generic(self); } std::string to_string(const multi_result& self) { return to_string_generic(self); } } ================================================ FILE: src/zk/multi.hpp ================================================ #pragma once #include #include #include #include #include #include #include "acl.hpp" #include "buffer.hpp" #include "forwards.hpp" #include "results.hpp" #include "types.hpp" namespace zk { /// \addtogroup Client /// \{ /// Describes the type of an \ref op. enum class op_type : int { check, //!< \ref op::check create, //!< \ref op::create erase, //!< \ref op::erase set, //!< \ref op::set }; std::ostream& operator<<(std::ostream&, const op_type&); std::string to_string(const op_type&); /// Represents a single operation of a \ref multi_op. class op final { public: /// Data for a \ref op::check operation. struct check_data { std::string path; version check; explicit check_data(std::string path, version check); op_type type() const { return op_type::check; } }; /// Check that the given \a path exists with the provided version \a check (which can be \c version::any). static op check(std::string path, version check = version::any()); /// Data for a \ref op::create operation. struct create_data { std::string path; buffer data; acl rules; create_mode mode; explicit create_data(std::string path, buffer data, acl rules, create_mode mode); op_type type() const { return op_type::create; } }; /// \{ /// Create a new entry at the given \a path with the \a data. /// /// \see client::create static op create(std::string path, buffer data, acl rules, create_mode mode = create_mode::normal); static op create(std::string path, buffer data, create_mode mode = create_mode::normal); /// \} /// Data for a \ref op::erase operation. struct erase_data { std::string path; version check; explicit erase_data(std::string path, version check); op_type type() const { return op_type::erase; } }; /// Delete the entry at the given \a path if it matches the version \a check. /// /// \see client::erase static op erase(std::string path, version check = version::any()); /// Data for a \ref op::set operation. struct set_data { std::string path; buffer data; version check; explicit set_data(std::string path, buffer data, version check); op_type type() const { return op_type::set; } }; /// Set the \a data for the entry at \a path if it matches the version \a check. /// /// \see client::set static op set(std::string path, buffer data, version check = version::any()); public: op(const op&); op(op&&) noexcept; op& operator=(const op&) = delete; op& operator=(op&&) = delete; ~op() noexcept; /// Get the underlying type of this operation. op_type type() const; /// Get the check-specific data. /// /// \throws std::logic_error if the \ref type is not \ref op_type::check. const check_data& as_check() const; /// Get the create-specific data. /// /// \throws std::logic_error if the \ref type is not \ref op_type::create. const create_data& as_create() const; /// Get the erase-specific data. /// /// \throws std::logic_error if the \ref type is not \ref op_type::erase. const erase_data& as_erase() const; /// Get the set-specific data. /// /// \throws std::logic_error if the \ref type is not \ref op_type::set. const set_data& as_set() const; private: using any_data = std::variant; explicit op(any_data&&) noexcept; template const T& as(ptr operation) const; friend std::ostream& operator<<(std::ostream&, const op&); private: any_data _storage; }; std::ostream& operator<<(std::ostream&, const op&); std::string to_string(const op&); /// A collection of operations that will be performed atomically. /// /// \see client::commit /// \see op class multi_op final { public: using iterator = std::vector::iterator; using const_iterator = std::vector::const_iterator; using size_type = std::vector::size_type; public: /// Create an empty operation set. multi_op() noexcept { } /// Create an instance from the provided \a ops. multi_op(std::vector ops) noexcept; /// Create an instance from the provided \a ops. multi_op(std::initializer_list ops) : multi_op(std::vector(ops)) { } ~multi_op() noexcept; /// The number of operations in this transaction bundle. size_type size() const { return _ops.size(); } /// \{ /// Get the operation at the given \a idx. const op& operator[](size_type idx) const { return _ops[idx]; } op& operator[](size_type idx) { return _ops[idx]; } /// \} /// \{ /// Get the operation at the given \a idx. /// /// \throws std::out_of_range if \a idx is larger than \ref size. const op& at(size_type idx) const { return _ops.at(idx); } op& at(size_type idx) { return _ops.at(idx); } /// \} /// \{ /// Get an iterator to the beginning of the operation list. iterator begin() { return _ops.begin(); } const_iterator begin() const { return _ops.begin(); } const_iterator cbegin() const { return _ops.begin(); } /// \} /// \{ /// Get an iterator to the end of the operation list. iterator end() { return _ops.end(); } const_iterator end() const { return _ops.end(); } const_iterator cend() const { return _ops.end(); } /// \} /// Increase the reserved memory block so it can store at least \a capacity operations without reallocating. void reserve(size_type capacity) { _ops.reserve(capacity); } /// Construct an operation emplace on the end of the list using \a args. /// /// \see push_back template void emplace_back(TArgs&&... args) { _ops.emplace_back(std::forward(args)...); } /// \{ /// Add the operation \a x to the end of this list. void push_back(op&& x) { emplace_back(std::move(x)); } void push_back(const op& x) { emplace_back(x); } /// \} private: std::vector _ops; }; std::ostream& operator<<(std::ostream&, const multi_op&); std::string to_string(const multi_op&); /// The result of a successful \ref client::commit operation. class multi_result final { public: /// A part of a result. The behavior depends on the type of \ref op provided to the original transaction. class part final { public: explicit part(op_type, std::nullptr_t) noexcept; explicit part(create_result) noexcept; explicit part(set_result) noexcept; part(const part&); part(part&&) noexcept; part& operator=(const part&) = delete; part& operator=(part&&) = delete; ~part() noexcept; /// The \ref op_type of the \ref op that caused this result. op_type type() const { return _type; } /// Get the create-specific result data. /// /// \throws std::logic_error if the \ref type is not \ref op_type::set. const create_result& as_create() const; /// Get the set-specific result data. /// /// \throws std::logic_error if the \ref type is not \ref op_type::set. const set_result& as_set() const; private: using any_result = std::variant; template const T& as(ptr operation) const; private: op_type _type; any_result _storage; }; using iterator = std::vector::iterator; using const_iterator = std::vector::const_iterator; using size_type = std::vector::size_type; public: multi_result() noexcept { } multi_result(std::vector parts) noexcept; multi_result(multi_result&&) = default; multi_result & operator=(multi_result&&) = default; ~multi_result() noexcept; /// The number of results in this transaction bundle. size_type size() const { return _parts.size(); } /// \{ /// Get the result at the given \a idx. /// /// \throws std::out_of_range if \a idx is larger than \a size. part& operator[](size_type idx) { return _parts[idx]; } const part& operator[](size_type idx) const { return _parts[idx]; } /// \} /// \{ /// Get the result at the given \a idx. /// /// \throws std::out_of_range if \a idx is larger than \ref size. const part& at(size_type idx) const { return _parts.at(idx); } part& at(size_type idx) { return _parts.at(idx); } /// \} /// \{ /// Get an iterator to the beginning of the result list. iterator begin() { return _parts.begin(); } const_iterator begin() const { return _parts.begin(); } const_iterator cbegin() const { return _parts.begin(); } /// \} /// \{ /// Get an iterator to the end of the result list. iterator end() { return _parts.end(); } const_iterator end() const { return _parts.end(); } const_iterator cend() const { return _parts.end(); } /// \} /// Increase the reserved memory block so it can store at least \a capacity results without reallocating. void reserve(size_type capacity) { _parts.reserve(capacity); } /// Construct a result emplace on the end of the list using \a args. /// /// \see push_back template void emplace_back(TArgs&&... args) { _parts.emplace_back(std::forward(args)...); } /// \{ /// Add the operation \a x to the end of this list. void push_back(part&& x) { emplace_back(std::move(x)); } void push_back(const part& x) { emplace_back(x); } /// \} private: std::vector _parts; }; std::ostream& operator<<(std::ostream&, const multi_result::part&); std::ostream& operator<<(std::ostream&, const multi_result&); std::string to_string(const multi_result::part&); std::string to_string(const multi_result&); /** \} **/ } ================================================ FILE: src/zk/multi_tests.cpp ================================================ #include #include #include #include #include #include "client.hpp" #include "error.hpp" #include "multi.hpp" #include "string_view.hpp" namespace zk { class multi_tests : public server::server_fixture { }; static buffer buffer_from(string_view str) { return buffer(str.data(), str.data() + str.size()); } GTEST_TEST_F(multi_tests, commit_no_fail) { client c = get_connected_client(); auto node1 = c.create("/test-", buffer_from("Going to delete"), create_mode::sequential).get().name(); auto node2 = c.create("/test-", buffer_from("First data"), create_mode::sequential).get().name(); multi_op txn = { op::check("/"), op::create("/test-", buffer_from("1"), create_mode::sequential), op::erase(node1), op::set(node2, buffer_from("Second data")), }; multi_result res = c.commit(txn).get(); CHECK_EQ(4U, res.size()); // node1 should be erased CHECK_FALSE(c.exists(node1).get()); auto node2_contents = c.get(node2).get().data(); CHECK_TRUE(node2_contents == buffer_from("Second data")); // Create a child of the node created in the multi auto name_to_make = res[1].as_create().name() + "/some-subnode"; c.create(name_to_make, buffer_from("sub")).get(); } GTEST_TEST_F(multi_tests, commit_fail_check) { client c = get_connected_client(); auto base = c.create("/test-", buffer_from("Base"), create_mode::sequential).get().name(); multi_op txn = { op::create(base + "/a", buffer_from("a")), op::check(base + "/does-not-exist"), op::create(base + "/b", buffer_from("b")), }; try { c.commit(txn).get(); } catch (const transaction_failed& ex) { CHECK_EQ(error_code::no_entry, ex.underlying_cause()); CHECK_EQ(1U, ex.failed_op_index()); } } } ================================================ FILE: src/zk/optional.hpp ================================================ /// \file /// Imports of \c optional and \c nullopt_t types, as well as the \c nullopt \c constexpr. These are \c std::optional, /// \c std::nullopt_t, and \c std::nullopt, respectively. #pragma once #include #include namespace zk { /// \addtogroup Client /// \{ template using optional = std::optional; using nullopt_t = std::nullopt_t; using std::nullopt; /// Apply \a transform with the arguments in \a x iff all of them have a value. Otherwise, \c nullopt will be returned. template auto map(FUnary&& transform, const optional&... x) -> optional { if ((x && ...)) return transform(x.value()...); else return nullopt; } template optional some(T x) { return optional(std::move(x)); } /// \} } ================================================ FILE: src/zk/optional_tests.cpp ================================================ #include #include "optional.hpp" namespace zk { GTEST_TEST(optional_test, integer) { optional x = nullopt; CHECK_FALSE(x); x = 1; CHECK_TRUE(x); CHECK_EQ(1, x.value()); } } ================================================ FILE: src/zk/results.cpp ================================================ #include "results.hpp" #include #include #include namespace zk { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Utilities // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // These tags are used during print_buffer overload resolution. If TBuffer type is not // applicable (ill-formed) for one of the overloads, then SFINAE selects the appropriate overload. // But there are cases (e.g. when TBuffer is std::string) when both of the print_buffer // are well-formed and therefore the call to print_buffer would be ambiguous. These tags // allows to select one of the overloads by using more derived tag (print_buffer_content_tag). struct print_buffer_length_tag {}; struct print_buffer_content_tag : public print_buffer_length_tag {}; template auto print_buffer(std::ostream& os, const TBuffer& buf, struct print_buffer_content_tag) -> decltype((os << buf), void()) { os << buf; } template void print_buffer(std::ostream& os, const TBuffer& buf, struct print_buffer_length_tag) { os << "size=" << buf.size(); } template void print_range(std::ostream& os, const TRange& range) { os << '['; bool first = true; for (const auto& x : range) { if (first) first = false; else os << ", "; os << x; } os << ']'; } template std::string to_string_generic(const T& x) { std::ostringstream os; os << x; return os.str(); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // get_result // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// get_result::get_result(buffer data, const zk::stat& stat) noexcept : _data(std::move(data)), _stat(stat) { } get_result::~get_result() noexcept { } std::ostream& operator<<(std::ostream& os, const get_result& self) { os << "get_result{"; print_buffer(os, self.data(), print_buffer_content_tag {}); os << ' ' << self.stat(); return os << '}'; } std::string to_string(const get_result& self) { return to_string_generic(self); } static_assert(std::is_copy_constructible_v); static_assert(std::is_copy_assignable_v); static_assert(std::is_nothrow_move_constructible_v); static_assert(std::is_nothrow_move_assignable_v); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // get_children_result // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// get_children_result::get_children_result(children_list_type children, const stat& parent_stat) noexcept : _children(std::move(children)), _parent_stat(parent_stat) { } get_children_result::~get_children_result() noexcept { } std::ostream& operator<<(std::ostream& os, const get_children_result& self) { os << "get_children_result{"; print_range(os, self.children()); os << " parent=" << self.parent_stat(); return os << '}'; } std::string to_string(const get_children_result& self) { return to_string_generic(self); } static_assert(std::is_copy_constructible_v); static_assert(std::is_copy_assignable_v); static_assert(std::is_nothrow_move_constructible_v); static_assert(std::is_nothrow_move_assignable_v); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // exists_result // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// exists_result::exists_result(const optional& stat) noexcept : _stat(stat) { } exists_result::~exists_result() noexcept { } std::ostream& operator<<(std::ostream& os, const exists_result& self) { os << "exists_result{"; if (self) os << *self.stat(); else os << "(no)"; return os << '}'; } std::string to_string(const exists_result& self) { return to_string_generic(self); } static_assert(std::is_copy_constructible_v); static_assert(std::is_copy_assignable_v); static_assert(std::is_nothrow_move_constructible_v); static_assert(std::is_nothrow_move_assignable_v); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // create_result // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// create_result::create_result(std::string name) noexcept : _name(std::move(name)) { } create_result::~create_result() noexcept { } std::ostream& operator<<(std::ostream& os, const create_result& self) { return os << "create_result{name=" << self.name() << '}'; } std::string to_string(const create_result& self) { return to_string_generic(self); } static_assert(std::is_copy_constructible_v); static_assert(std::is_copy_assignable_v); static_assert(std::is_nothrow_move_constructible_v); static_assert(std::is_nothrow_move_assignable_v); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // set_result // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// set_result::set_result(const zk::stat& stat) noexcept : _stat(stat) { } set_result::~set_result() noexcept { } std::ostream& operator<<(std::ostream& os, const set_result& self) { return os << "set_result{" << self.stat() << '}'; } std::string to_string(const set_result& self) { return to_string_generic(self); } static_assert(std::is_copy_constructible_v); static_assert(std::is_copy_assignable_v); static_assert(std::is_nothrow_move_constructible_v); static_assert(std::is_nothrow_move_assignable_v); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // get_acl_result // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// get_acl_result::get_acl_result(zk::acl acl, const zk::stat& stat) noexcept : _acl(std::move(acl)), _stat(stat) { } get_acl_result::~get_acl_result() noexcept { } std::ostream& operator<<(std::ostream& os, const get_acl_result& self) { return os << "get_acl_result{" << self.acl() << ' ' << self.stat() << '}'; } std::string to_string(const get_acl_result& self) { return to_string_generic(self); } static_assert(std::is_copy_constructible_v); static_assert(std::is_copy_assignable_v); static_assert(std::is_nothrow_move_constructible_v); static_assert(std::is_nothrow_move_assignable_v); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // event // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// event::event(event_type type, zk::state state) noexcept : _type(type), _state(state) { } std::ostream& operator<<(std::ostream& os, const event& self) { return os << "event{" << self.type() << " | " << self.state() << '}'; } std::string to_string(const event& self) { return to_string_generic(self); } static_assert(std::is_copy_constructible_v); static_assert(std::is_copy_assignable_v); static_assert(std::is_nothrow_move_constructible_v); static_assert(std::is_nothrow_move_assignable_v); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // watch_result // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// watch_result::watch_result(get_result initial, future next) noexcept : _initial(std::move(initial)), _next(std::move(next)) { } watch_result::~watch_result() noexcept { } std::ostream& operator<<(std::ostream& os, const watch_result& self) { return os << "watch_result{initial=" << self.initial() << '}'; } std::string to_string(const watch_result& self) { return to_string_generic(self); } static_assert(std::is_nothrow_move_constructible_v); static_assert(std::is_nothrow_move_assignable_v); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // watch_children_result // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// watch_children_result::watch_children_result(get_children_result initial, future next) noexcept : _initial(std::move(initial)), _next(std::move(next)) { } watch_children_result::~watch_children_result() noexcept { } std::ostream& operator<<(std::ostream& os, const watch_children_result& self) { return os << "watch_children_result{initial=" << self.initial() << '}'; } std::string to_string(const watch_children_result& self) { return to_string_generic(self); } static_assert(std::is_nothrow_move_constructible_v); static_assert(std::is_nothrow_move_assignable_v); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // watch_exists_result // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// watch_exists_result::watch_exists_result(exists_result initial, future next) noexcept : _initial(std::move(initial)), _next(std::move(next)) { } watch_exists_result::~watch_exists_result() noexcept { } std::ostream& operator<<(std::ostream& os, const watch_exists_result& self) { return os << "watch_exists_result{initial=" << self.initial() << '}'; } std::string to_string(const watch_exists_result& self) { return to_string_generic(self); } static_assert(std::is_nothrow_move_constructible_v); static_assert(std::is_nothrow_move_assignable_v); } ================================================ FILE: src/zk/results.hpp ================================================ /// \file /// Describes the various result types of \c client operations. #pragma once #include #include #include #include #include "acl.hpp" #include "buffer.hpp" #include "future.hpp" #include "optional.hpp" #include "types.hpp" namespace zk { /// \addtogroup Client /// \{ /// The result type of \c client::get. class get_result final { public: explicit get_result(buffer data, const zk::stat& stat) noexcept; get_result(const get_result&) = default; get_result& operator=(const get_result&) = default; get_result(get_result&&) = default; get_result& operator=(get_result&&) = default; ~get_result() noexcept; /// \{ /// The data read from the entry. const buffer& data() const & { return _data; } buffer& data() & { return _data; } buffer data() && { return std::move(_data); } /// \} /// \{ /// The \ref zk::stat of the entry at the time it was read. The most useful value of the returned value is /// \ref stat::data_version. const zk::stat& stat() const { return _stat; } zk::stat& stat() { return _stat; } /// \} private: buffer _data; zk::stat _stat; }; std::ostream& operator<<(std::ostream&, const get_result&); std::string to_string(const get_result&); /// The result type of \c client::get_children. class get_children_result final { public: using children_list_type = std::vector; public: explicit get_children_result(children_list_type children, const stat& parent_stat) noexcept; get_children_result(const get_children_result&) = default; get_children_result& operator=(const get_children_result&) = default; get_children_result(get_children_result&&) = default; get_children_result& operator=(get_children_result&&) = default; ~get_children_result() noexcept; /// \{ /// The list of children of the originally-queried node. Note that there is no guarantee on ordering of this list. const children_list_type& children() const & { return _children; } children_list_type& children() & { return _children; } children_list_type children() && { return std::move(_children); } /// \} /// \{ /// The \ref zk::stat of the entry queried (the parent of the \ref children). const stat& parent_stat() const { return _parent_stat; } stat& parent_stat() { return _parent_stat; } /// \} private: children_list_type _children; stat _parent_stat; }; std::ostream& operator<<(std::ostream&, const get_children_result&); std::string to_string(const get_children_result&); /// The result type of \ref client::exists. class exists_result final { public: explicit exists_result(const optional& stat) noexcept; exists_result(const exists_result&) = default; exists_result& operator=(const exists_result&) = default; exists_result(exists_result&&) = default; exists_result& operator=(exists_result&&) = default; ~exists_result() noexcept; /// \{ /// An \c exists_result is \c true if the entry exists. explicit operator bool() const { return bool(_stat); } bool operator!() const { return !_stat; } /// \} /// \{ /// The \ref zk::stat of the entry if it exists. If it does not exist, this value will be \c nullopt. const optional& stat() const { return _stat; } optional& stat() { return _stat; } /// \} private: optional _stat; }; std::ostream& operator<<(std::ostream&, const exists_result&); std::string to_string(const exists_result&); /// The result type of \ref client::create. class create_result final { public: explicit create_result(std::string name) noexcept; create_result(const create_result&) = default; create_result& operator=(const create_result&) = default; create_result(create_result&&) = default; create_result& operator=(create_result&&) = default; ~create_result() noexcept; /// \{ /// The name of the created entry. How useful this is depends on the value of \ref create_mode passed to the create /// operation. If \ref create_mode::sequential was set, this value must be used to see what was created. In all /// other cases, the name is the same as the path which was passed in. const std::string& name() const & { return _name; } std::string& name() & { return _name; } std::string name() && { return std::move(_name); } /// \} private: std::string _name; }; std::ostream& operator<<(std::ostream&, const create_result&); std::string to_string(const create_result&); /// The result type of \ref client::set. class set_result final { public: explicit set_result(const zk::stat& stat) noexcept; set_result(const set_result&) = default; set_result& operator=(const set_result&) = default; set_result(set_result&&) = default; set_result& operator=(set_result&&) = default; ~set_result() noexcept; /// \{ /// The \ref zk::stat of the entry after the set operation. const zk::stat& stat() const { return _stat; } zk::stat& stat() { return _stat; } /// \} private: zk::stat _stat; }; std::ostream& operator<<(std::ostream&, const set_result&); std::string to_string(const set_result&); /// The result type of \ref client::get_acl. class get_acl_result final { public: explicit get_acl_result(zk::acl acl, const zk::stat& stat) noexcept; get_acl_result(const get_acl_result&) = default; get_acl_result& operator=(const get_acl_result&) = default; get_acl_result(get_acl_result&&) = default; get_acl_result& operator=(get_acl_result&&) = default; ~get_acl_result() noexcept; /// \{ /// The \ref zk::acl of the entry. const zk::acl& acl() const & { return _acl; } zk::acl& acl() & { return _acl; } zk::acl acl() && { return std::move(_acl); } /// \} /// \{ /// The \ref zk::stat of the entry at the time it was read. The most useful value of the returned value is /// \ref stat::acl_version. const zk::stat& stat() const { return _stat; } zk::stat& stat() { return _stat; } /// \} private: zk::acl _acl; zk::stat _stat; }; std::ostream& operator<<(std::ostream&, const get_acl_result&); std::string to_string(const get_acl_result&); /// Data delivered when a watched event triggers. /// /// \note /// If you are familiar with the ZooKeeper C API, the limited information delivered might seem very simplistic. Since /// this API only supports non-global watches, the extra parameters are not helpful and generally unsafe. As an example, /// the \c path parameter is not included. It is not helpful to include, since you already know the path you specified /// when you set the watch in the first place. Furthermore, it is unsafe, as the contents addressed by the pointer are /// only safe in the callback thread. While we could copy the path into an \c std::string, this would require an /// allocation on every delivery, which is very intrusive. class event final { public: explicit event(event_type type, zk::state state) noexcept; event(const event&) = default; event& operator=(const event&) = default; event(event&&) = default; event& operator=(event&&) = default; /// The type of event that occurred. const event_type& type() const { return _type; } /// The state of the connection when the event occurred. Keep in mind that it might be different when the value is /// delivered. const zk::state& state() const { return _state; } private: event_type _type; zk::state _state; }; std::ostream& operator<<(std::ostream&, const event&); std::string to_string(const event&); /// The result type of \ref client::watch. class watch_result final { public: explicit watch_result(get_result initial, future next) noexcept; watch_result(const watch_result&) = delete; watch_result& operator=(const watch_result&) = delete; watch_result(watch_result&&) = default; watch_result& operator=(watch_result&&) = default; ~watch_result() noexcept; /// \{ /// The initial result of the fetch. const get_result& initial() const & { return _initial; } get_result& initial() & { return _initial; } get_result initial() && { return std::move(_initial); } /// \} /// \{ /// Future to be delivered when the watch is triggered. const future& next() const & { return _next; } future& next() & { return _next; } future next() && { return std::move(_next); } /// \} private: get_result _initial; future _next; }; std::ostream& operator<<(std::ostream&, const watch_result&); std::string to_string(const watch_result&); /// The result type of \ref client::watch_children. class watch_children_result final { public: explicit watch_children_result(get_children_result initial, future next) noexcept; watch_children_result(const watch_children_result&) = delete; watch_children_result& operator=(const watch_children_result&) = delete; watch_children_result(watch_children_result&&) = default; watch_children_result& operator=(watch_children_result&&) = default; ~watch_children_result() noexcept; /// \{ /// The initial result of the fetch. const get_children_result& initial() const & { return _initial; } get_children_result& initial() & { return _initial; } get_children_result initial() && { return std::move(_initial); } /// \} /// \{ /// Future to be delivered when the watch is triggered. const future& next() const & { return _next; } future& next() & { return _next; } future next() && { return std::move(_next); } /// \} private: get_children_result _initial; future _next; }; std::ostream& operator<<(std::ostream&, const watch_children_result&); std::string to_string(const watch_children_result&); /// The result type of \ref client::watch_exists. class watch_exists_result final { public: explicit watch_exists_result(exists_result initial, future next) noexcept; watch_exists_result(const watch_exists_result&) = delete; watch_exists_result& operator=(const watch_exists_result&) = delete; watch_exists_result(watch_exists_result&&) = default; watch_exists_result& operator=(watch_exists_result&&) = default; ~watch_exists_result() noexcept; /// \{ /// The initial result of the fetch. const exists_result& initial() const & { return _initial; } exists_result& initial() & { return _initial; } exists_result initial() && { return std::move(_initial); } /// \} /// \{ /// Future to be delivered when the watch is triggered. const future& next() const & { return _next; } future& next() & { return _next; } future next() && { return std::move(_next); } /// \} private: exists_result _initial; future _next; }; std::ostream& operator<<(std::ostream&, const watch_exists_result&); std::string to_string(const watch_exists_result&); /// \} } ================================================ FILE: src/zk/server/classpath.cpp ================================================ #include "classpath.hpp" #include #include #include #include #include #include #include #include #include #include #include namespace zk::server { template void join(std::ostream& os, const TContainer& src, TSep sep) { bool first = true; for (const auto& x : src) { if (!std::exchange(first, false)) os << sep; os << x; } } static bool file_exists(ptr path) { struct ::stat s; if (::stat(path, &s)) { if (errno == ENOENT) return false; else throw std::system_error(errno, std::system_category()); } else { return true; } } static classpath find_system_default() { string_view locations[] = { "/usr/share/java", "/usr/local/share/java", }; string_view requirements[] = { "zookeeper.jar", "slf4j-api.jar", "slf4j-simple.jar", }; std::vector components; std::vector unfound; for (auto jar : requirements) { bool found = false; for (auto base_loc : locations) { auto potential_sz = base_loc.size() + jar.size() + 4; char potential[potential_sz]; std::snprintf(potential, potential_sz, "%s/%s", base_loc.data(), jar.data()); if (file_exists(potential)) { found = true; components.emplace_back(std::string(potential)); break; } } if (!found) unfound.emplace_back(jar); } if (unfound.empty()) { return classpath(std::move(components)); } else { std::ostringstream os; os << "Could not find requirement" << ((unfound.size() == 1U) ? "" : "s") << ": "; join(os, unfound, ", "); os << ". Searched paths: "; join(os, locations, ", "); throw std::runtime_error(os.str()); } } classpath classpath::system_default() { static const auto instance = find_system_default(); return instance; } classpath::classpath(std::vector components) noexcept : _components(std::move(components)) { } std::string classpath::command_line() const { std::ostringstream os; os << *this; return os.str(); } std::ostream& operator<<(std::ostream& os, const classpath& self) { join(os, self._components, ':'); return os; } } ================================================ FILE: src/zk/server/classpath.hpp ================================================ #pragma once #include #include #include #include namespace zk::server { /// \addtogroup Server /// \{ /// Represents a collection of JARs or other Java entities that should be provided as the `--classpath` to the JVM. This /// is used to fetch both the ZooKeeper package (`zookeeper.jar`), but the required SLF JAR. If you do not know or care /// about what that second part means, just call \ref system_default. class classpath final { public: /// Create a classpath specification from the provided \a components. explicit classpath(std::vector components) noexcept; /// Load the system-default classpath for ZooKeeper. This searches for `zookeeper.jar` nad `slf4j-simple.jar` in /// various standard locations. static classpath system_default(); /// Get the command-line representation of this classpath. This puts \c ':' characters between the components. std::string command_line() const; friend std::ostream& operator<<(std::ostream&, const classpath&); private: std::vector _components; }; /// \} } ================================================ FILE: src/zk/server/classpath_registration_template.cpp.in ================================================ #include #include #include #include namespace zk::server { namespace { auto registration = test_package_registry::instance() .register_classpath_server ( R"(@server_version@)", classpath({R"(@server_classpath@)"}) ); } } ================================================ FILE: src/zk/server/classpath_tests.cpp ================================================ #include #include #include #include "classpath.hpp" namespace zk::server { GTEST_TEST(classpath_tests, system_default) { try { auto cp = classpath::system_default(); std::cout << "Found system-default ZooKeeper: " << cp.command_line() << std::endl; } catch (const std::runtime_error& ex) { std::cout << "Could not find system-default ZooKeeper: " << ex.what() << std::endl; } } } ================================================ FILE: src/zk/server/configuration.cpp ================================================ #include "configuration.hpp" #include #include #include #include #include #include #include #include namespace zk::server { void server_id::ensure_valid() const { if (0U < value && value < 256U) return; std::ostringstream os; os << "Server ID value " << value << " is not in the valid range [1 .. 255]"; throw std::out_of_range(os.str()); } std::ostream& operator<<(std::ostream& os, const server_id& self) { return os << self.value; } namespace { constexpr std::size_t not_a_line = ~0UL; class zero_copy_streambuf final : public std::streambuf { public: zero_copy_streambuf(string_view input) { // We are just going to read from it, so this const_cast is okay ptr p = const_cast>(input.data()); setg(p, p, p + input.size()); } }; } const std::uint16_t configuration::default_client_port = std::uint16_t(2181); const std::uint16_t configuration::default_peer_port = std::uint16_t(2888); const std::uint16_t configuration::default_leader_port = std::uint16_t(3888); const configuration::duration_type configuration::default_tick_time = std::chrono::milliseconds(2000); const std::size_t configuration::default_init_limit = 10U; const std::size_t configuration::default_sync_limit = 5U; const std::set configuration::default_four_letter_word_whitelist = { "srvr" }; const std::set configuration::all_four_letter_word_whitelist = { "*" }; template configuration::setting::setting() noexcept : value(nullopt), line(not_a_line) { } template configuration::setting::setting(T value, std::size_t line) noexcept : value(std::move(value)), line(line) { } configuration::configuration() = default; configuration::~configuration() noexcept = default; configuration configuration::make_minimal(std::string data_directory, std::uint16_t client_port) { configuration out; out.data_directory(std::move(data_directory)) .client_port(client_port) .init_limit(default_init_limit) .sync_limit(default_sync_limit) ; return out; } static std::set parse_whitelist(string_view source) { std::set out; while (!source.empty()) { auto idx = source.find_first_of(','); if (idx == string_view::npos) idx = source.size(); if (idx == 0) { source.remove_prefix(1); continue; } auto sub = source.substr(0, idx); source.remove_prefix(idx); while (!sub.empty() && std::isspace(sub.front())) sub.remove_prefix(1); while (!sub.empty() && std::isspace(sub.back())) sub.remove_suffix(1); out.insert(std::string(sub)); } return out; } configuration configuration::from_lines(std::vector lines) { static const std::regex line_expr(R"(^([^=]+)=([^ #]+)[ #]*$)", std::regex_constants::ECMAScript | std::regex_constants::optimize ); constexpr auto name_idx = 1U; constexpr auto data_idx = 2U; configuration out; for (std::size_t line_no = 0U; line_no < lines.size(); ++line_no) { const auto& line = lines[line_no]; if (line.empty() || line[0] == '#') continue; std::smatch match; if (std::regex_match(line, match, line_expr)) { auto name = match[name_idx].str(); auto data = match[data_idx].str(); if (name == "clientPort") { out._client_port = { std::uint16_t(std::atoi(data.c_str())), line_no }; } else if (name == "dataDir") { out._data_directory = { std::move(data), line_no }; } else if (name == "tickTime") { out._tick_time = { std::chrono::milliseconds(std::atol(data.c_str())), line_no }; } else if (name == "initLimit") { out._init_limit = { std::size_t(std::atol(data.c_str())), line_no }; } else if (name == "syncLimit") { out._sync_limit = { std::size_t(std::atol(data.c_str())), line_no }; } else if (name == "leaderServes") { out._leader_serves = { (data == "yes"), line_no }; } else if (name == "4lw.commands.whitelist") { out._four_letter_word_whitelist = { parse_whitelist(data), line_no }; } else if (name.find("server.") == 0U) { auto id = std::size_t(std::atol(name.c_str() + 7)); out._server_paths.insert({ server_id(id), { std::move(data), line_no } }); } else { out._unknown_settings.insert({ std::move(name), { std::move(data), line_no } }); } } } out._lines = std::move(lines); return out; } configuration configuration::from_stream(std::istream& stream) { std::vector lines; std::string line; while(std::getline(stream, line)) { lines.emplace_back(std::move(line)); } if (stream.eof()) return from_lines(std::move(lines)); else throw std::runtime_error("Loading configuration did not reach EOF"); } configuration configuration::from_file(std::string filename) { std::ifstream inf(filename.c_str()); auto out = from_stream(inf); out._source_file = std::move(filename); return out; } configuration configuration::from_string(string_view value) { zero_copy_streambuf buff(value); std::istream stream(&buff); return from_stream(stream); } bool configuration::is_minimal() const { return _data_directory.value && _client_port.value && _init_limit.value && init_limit() == default_init_limit && _sync_limit.value && sync_limit() == default_sync_limit && _lines.size() == 4U; } template void configuration::set(setting& target, optional value, string_view key, const FEncode& encode) { std::string target_line; if (value) { std::ostringstream os; os << key << '=' << encode(*value); target_line = os.str(); } if (target.line == not_a_line && value) { target.value = std::move(value); target.line = _lines.size(); _lines.emplace_back(std::move(target_line)); } else if (target.line == not_a_line && !value) { // do nothing -- no line means no value } else { target.value = std::move(value); _lines[target.line] = std::move(target_line); } } template void configuration::set(setting& target, optional value, string_view key) { return set(target, std::move(value), key, [] (const T& x) -> const T& { return x; }); } std::uint16_t configuration::client_port() const { return _client_port.value.value_or(default_client_port); } configuration& configuration::client_port(optional port) { set(_client_port, port, "clientPort"); return *this; } const optional& configuration::data_directory() const { return _data_directory.value; } configuration& configuration::data_directory(optional path) { set(_data_directory, std::move(path), "dataDir"); return *this; } configuration::duration_type configuration::tick_time() const { return _tick_time.value.value_or(default_tick_time); } configuration& configuration::tick_time(optional tick_time) { set(_tick_time, tick_time, "tickTime", [] (duration_type x) { return x.count(); }); return *this; } std::size_t configuration::init_limit() const { return _init_limit.value.value_or(default_init_limit); } configuration& configuration::init_limit(optional limit) { set(_init_limit, limit, "initLimit"); return *this; } std::size_t configuration::sync_limit() const { return _sync_limit.value.value_or(default_sync_limit); } configuration& configuration::sync_limit(optional limit) { set(_sync_limit, limit, "syncLimit"); return *this; } bool configuration::leader_serves() const { return _leader_serves.value.value_or(true); } configuration& configuration::leader_serves(optional serve) { set(_leader_serves, serve, "leaderServes", [] (bool x) { return x ? "yes" : "no"; }); return *this; } const std::set& configuration::four_letter_word_whitelist() const { if (_four_letter_word_whitelist.value) return *_four_letter_word_whitelist.value; else return default_four_letter_word_whitelist; } configuration& configuration::four_letter_word_whitelist(optional> words) { if (words && words->size() > 1U && words->count("*")) throw std::invalid_argument(""); set(_four_letter_word_whitelist, std::move(words), "4lw.commands.whitelist", [] (const std::set& words) { bool first = true; std::ostringstream os; for (const auto& word : words) { if (!std::exchange(first, false)) os << ','; os << word; } return os.str(); } ); return *this; } std::map configuration::servers() const { std::map out; for (const auto& entry : _server_paths) out.insert({ entry.first, *entry.second.value }); return out; } configuration& configuration::add_server(server_id id, std::string hostname, std::uint16_t peer_port, std::uint16_t leader_port ) { id.ensure_valid(); if (_server_paths.count(id)) throw std::runtime_error(std::string("Already a server with ID ") + std::to_string(id.value)); hostname += ":"; hostname += std::to_string(peer_port); hostname += ":"; hostname += std::to_string(leader_port); auto iter = _server_paths.emplace(id, setting()).first; set(iter->second, some(std::move(hostname)), std::string("server.") + std::to_string(iter->first.value)); return *this; } std::map configuration::unknown_settings() const { std::map out; for (const auto& entry : _unknown_settings) out.insert({ entry.first, *entry.second.value }); return out; } configuration& configuration::add_setting(std::string key, std::string value) { // This process is really inefficient, but people should not be using this very often. This is done this way because // it is possible to specify a key that has a known setting (such as "dataDir"), which needs to be picked up // correctly. `from_lines` has this logic, so just use it. Taking that matching logic out would be the best approach // to take, but since this shouldn't be used, I haven't bothered. auto source_file = _source_file; auto lines = _lines; lines.emplace_back(key + "=" + value); *this = configuration::from_lines(std::move(lines)); _source_file = std::move(source_file); return *this; } void configuration::save(std::ostream& os) const { for (const auto& line : _lines) os << line << std::endl; os.flush(); } void configuration::save_file(std::string filename) { std::ofstream ofs(filename.c_str()); save(ofs); if (ofs) _source_file = std::move(filename); else throw std::runtime_error("Error saving file"); } bool operator==(const configuration& lhs, const configuration& rhs) { if (&lhs == &rhs) return true; auto same_items = [] (const auto& a, const auto& b) { return a.first == b.first && a.second.value == b.second.value; }; return lhs.client_port() == rhs.client_port() && lhs.data_directory() == rhs.data_directory() && lhs.tick_time() == rhs.tick_time() && lhs.init_limit() == rhs.init_limit() && lhs.sync_limit() == rhs.sync_limit() && lhs.leader_serves() == rhs.leader_serves() && lhs.four_letter_word_whitelist() == rhs.four_letter_word_whitelist() && lhs._server_paths.size() == rhs._server_paths.size() && lhs._server_paths.end() == std::mismatch(lhs._server_paths.begin(), lhs._server_paths.end(), rhs._server_paths.begin(), rhs._server_paths.end(), same_items ).first && lhs._unknown_settings.size() == rhs._unknown_settings.size() && lhs._unknown_settings.end() == std::mismatch(lhs._unknown_settings.begin(), lhs._unknown_settings.end(), rhs._unknown_settings.begin(), rhs._unknown_settings.end(), same_items ).first ; } bool operator!=(const configuration& lhs, const configuration& rhs) { return !(lhs == rhs); } } ================================================ FILE: src/zk/server/configuration.hpp ================================================ #pragma once #include #include #include #include #include #include #include #include #include #include #include namespace zk::server { /// \addtogroup Server /// \{ /// Represents the ID of a server in the ensemble. /// /// \note /// The backing type for this ID is a \c std::size_t, when a \c std::uint8_t would cover all valid values of the ID. /// However, printing `unsigned char` types is somewhat odd, as the character value is usually printed. Beyond that, /// using a \c std::uint8_t requires explicit casting when converting from a \c std::size_t when \c -Wconversion is /// enabled. struct server_id : strong_id { /// Create an instance from the given \a value. /// /// \throws std::out_of_range if \a value is not in 1-255. server_id(std::size_t value) : strong_id(value) { ensure_valid(); } /// Check that this ID is a valid one. /// /// \throws std::out_of_range if \a value is not in 1-255. void ensure_valid() const; /// Debug print for this instance. friend std::ostream& operator<<(std::ostream&, const server_id&); }; /// Represents a configuration which should be run by \ref server instance. This can also be used to modify an existing /// ZooKeeper server configuration file in a safer manner than the unfortunate operating practice of \c sed, \c awk, and /// \c perl. /// /// This can be used to quickly create a quorum peer, ready to connect to 3 servers. /// \code /// auto config = server::configuration::make_minimal("zk-data", 2181) /// .add_server(1, "192.168.1.101") /// .add_server(2, "192.168.1.102") /// .add_server(3, "192.168.1.103"); /// config.save_file("settings.cfg"); /// { /// std::ofstream of("zk-data/myid"); /// // Assuming this is server 1 /// of << 1 << std::endl; /// } /// server::server svr(config); /// \endcode /// /// \see server For where this is used. /// \see server_group An example of quickly creating multiple ZooKeeper servers on a single machine (for testing). class configuration final { public: using duration_type = std::chrono::milliseconds; public: /// The default value for \ref client_port. static const std::uint16_t default_client_port; /// The default value for \ref peer_port. static const std::uint16_t default_peer_port; /// The default value for \ref leader_port. static const std::uint16_t default_leader_port; /// The default value for \ref tick_time. static const duration_type default_tick_time; /// The default value for \ref init_limit. static const std::size_t default_init_limit; /// The default value for \ref sync_limit. static const std::size_t default_sync_limit; /// The default value for \ref four_letter_word_whitelist. static const std::set default_four_letter_word_whitelist; /// A value for \ref four_letter_word_whitelist that enables all commands. Note that this is not a list of all /// allowed words, but simply the string \c "*". static const std::set all_four_letter_word_whitelist; /// All known values allowed in \ref four_letter_word_whitelist. This set comes from what ZooKeeper server 3.5.3 /// supported, so it is possible the version of ZooKeeper you are running supports a different set. static const std::set known_four_letter_word_whitelist; public: /// Creates a minimal configuration, setting the four needed values. The resulting \ref configuration can be run /// through a file with \c save or it can run directly from the command line. static configuration make_minimal(std::string data_directory, std::uint16_t client_port = default_client_port); /// Load the configuration from a file. static configuration from_file(std::string filename); /// Load configuration from the provided \a stream. static configuration from_stream(std::istream& stream); /// Load configuration from the provided \a lines. static configuration from_lines(std::vector lines); /// Load configuration directly from the in-memory \a value. static configuration from_string(string_view value); ~configuration() noexcept; /// Get the source file. This will only have a value if this was created by \ref from_file. const optional& source_file() const { return _source_file; } /// Check if this is a "minimal" configuration -- meaning it only has a \c data_directory and \c client_port set. /// Configurations which are minimal can be started directly from the command line. bool is_minimal() const; /// \{ /// The port a client should use to connect to this server. std::uint16_t client_port() const; configuration& client_port(optional port); /// \} /// \{ /// The directory for "myid" file and "version-2" directory (containing the log, snapshot, and epoch files). const optional& data_directory() const; configuration& data_directory(optional path); /// \} /// \{ /// The time between server "ticks." This value is used to translate \ref init_limit and \ref sync_limit into clock /// times. duration_type tick_time() const; configuration& tick_time(optional time); /// \} /// \{ /// The number of ticks that the initial synchronization phase can take. This limits the length of time the /// ZooKeeper servers in quorum have to connect to a leader. std::size_t init_limit() const; configuration& init_limit(optional limit); /// \} /// \{ /// Limits how far out of date a server can be from a leader. std::size_t sync_limit() const; configuration& sync_limit(optional limit); /// \} /// \{ /// Should an elected leader accepts client connections? For higher update throughput at the slight expense of read /// latency, the leader can be configured to not accept clients and focus on coordination. The default to this value /// is \c true, which means that a leader will accept client connections. bool leader_serves() const; configuration& leader_serves(optional serve); /// \} /// \{ /// A list of comma separated four letter words commands that user wants to use. A valid four letter words command /// must be put in this list or the ZooKeeper server will not enable the command. If unspecified, the whitelist only /// contains "srvr" command (\ref default_four_letter_word_whitelist). /// /// \note /// It is planned that the ZooKeeper server will deprecate this whitelist in preference of using a JSON REST API for /// health checks. It is unlikely to be deprecated any time in the near future and will likely remain in the product /// for a very long time. /// /// \param words is the list of four letter words to allow or \c nullopt to clear the setting. If specified as an /// empty set, this explicitly disables all words, which is \e different than setting this value to \c nullopt. /// /// \throws std::invalid_argument if \a words contains the all value (\c "*") but it is not the only value in the /// set. /// /// \ref default_four_letter_word_whitelist /// \ref all_four_letter_word_whitelist /// \ref known_four_letter_word_whitelist const std::set& four_letter_word_whitelist() const; configuration& four_letter_word_whitelist(optional> words); /// \} /// Get the servers which are part of the ZooKeeper ensemble. std::map servers() const; /// Add a new server to the configuration. /// /// \param id The cluster unique ID of this server. /// \param hostname The address of the server to connect to. /// \param peer_port The port used to move ZooKeeper data on. /// \param leader_port The port used for leader election. /// \throws std::out_of_range if the \a id is not in the valid range of IDs (see \ref server_id). /// \throws std::runtime_error if there is already a server with the given \a id. configuration& add_server(server_id id, std::string hostname, std::uint16_t peer_port = default_peer_port, std::uint16_t leader_port = default_leader_port ); /// Get settings that were in the configuration file (or manually added with \ref add_setting) but unknown to this /// library. std::map unknown_settings() const; /// Add an arbitrary setting with the \a key and \a value. /// /// \note /// You should not use this frequently -- prefer the named settings. configuration& add_setting(std::string key, std::string value); /// Write this configuration to the provided \a stream. /// /// \see save_file void save(std::ostream& stream) const; /// Save this configuration to \a filename. On successful save, \c source_file will but updated to reflect the new /// file. void save_file(std::string filename); /// \{ /// Check for equality of configuration. This does not check the specification in lines, but the values of the /// settings. In other words, two configurations with \c tick_time set to 1 second are equal, even if the source /// files are different and \c "tickTime=1000" was set on different lines. friend bool operator==(const configuration& lhs, const configuration& rhs); friend bool operator!=(const configuration& lhs, const configuration& rhs); /// \} private: using line_list = std::vector; template struct setting { setting() noexcept; setting(T value, std::size_t line) noexcept; optional value; std::size_t line; }; template void set(setting& target, optional value, string_view key, const FEncode& encode); template void set(setting& target, optional value, string_view key); private: explicit configuration(); private: optional _source_file; line_list _lines; setting _client_port; setting _data_directory; setting _tick_time; setting _init_limit; setting _sync_limit; setting _leader_serves; setting> _four_letter_word_whitelist; std::map> _server_paths; std::map> _unknown_settings; }; /// \} } namespace std { template <> struct hash { using argument_type = zk::server::server_id; using result_type = std::size_t; result_type operator()(const argument_type& x) const { return zk::hash(x); } }; } ================================================ FILE: src/zk/server/configuration_tests.cpp ================================================ #include #include #include #include #include "configuration.hpp" namespace zk::server { static string_view configuration_source_file_example = R"(# http://hadoop.apache.org/zookeeper/docs/current/zookeeperAdmin.html tickTime=2500 initLimit=10 syncLimit=5 dataDir=/var/lib/zookeeper # the port at which the clients will connect clientPort=2181 # specify all zookeeper servers server.1=zookeeper1:2888:3888 server.2=zookeeper2:2888:3888 server.3=zookeeper3:2888:3888 #preAllocSize=65536 #snapCount=1000 leaderServes=yes # A server key that we do not understand randomExtra=Value )"; GTEST_TEST(configuration_tests, from_example) { auto parsed = configuration::from_string(configuration_source_file_example); CHECK_EQ(2500U, parsed.tick_time().count()); CHECK_EQ(10U, parsed.init_limit()); CHECK_EQ(5U, parsed.sync_limit()); CHECK_EQ(2181U, parsed.client_port()); CHECK_TRUE(parsed.leader_serves()); auto servers = parsed.servers(); CHECK_EQ(3U, servers.size()); CHECK_EQ("zookeeper1:2888:3888", servers.at(server_id(1))); CHECK_EQ("zookeeper2:2888:3888", servers.at(server_id(2))); CHECK_EQ("zookeeper3:2888:3888", servers.at(server_id(3))); auto unrecognized = parsed.unknown_settings(); CHECK_EQ(1U, unrecognized.size()); CHECK_EQ("Value", unrecognized.at("randomExtra")); auto configured = configuration::make_minimal("/var/lib/zookeeper") .tick_time(std::chrono::milliseconds(2500)) .init_limit(10) .sync_limit(5) .client_port(2181) .leader_serves(true) .add_server(server_id(1), "zookeeper1") .add_server(server_id(2), "zookeeper2") .add_server(server_id(3), "zookeeper3") .add_setting("randomExtra", "Value") ; CHECK_EQ(configured, parsed); std::ostringstream os; parsed.save(os); auto reloaded = configuration::from_string(os.str()); CHECK_EQ(parsed, reloaded); } GTEST_TEST(configuration_tests, minimal) { auto minimal = configuration::make_minimal("/some/path", 2345); CHECK_EQ("/some/path", minimal.data_directory().value()); CHECK_EQ(2345, minimal.client_port()); CHECK_TRUE(minimal.is_minimal()); } static string_view configuration_source_with_four_letter_words_example = R"(# http://hadoop.apache.org/zookeeper/docs/current/zookeeperAdmin.html tickTime=2500 initLimit=10 syncLimit=5 dataDir=/var/lib/zookeeper 4lw.commands.whitelist=stat,mntr,srvr,ruok )"; GTEST_TEST(configuration_tests, four_letter_words) { auto parsed = configuration::from_string(configuration_source_with_four_letter_words_example); CHECK_EQ(4U, parsed.four_letter_word_whitelist().size()); std::set expected = { "stat", "mntr", "srvr", "ruok" }; CHECK_TRUE(expected == parsed.four_letter_word_whitelist()); } } ================================================ FILE: src/zk/server/detail/close.cpp ================================================ #include "close.hpp" #include #include #include #include namespace zk::server::detail { void close(int fd) { if (::close(fd) == -1) { // LCOV_EXCL_START: Close will always work -- push failures to the consumer switch (errno) { case EINTR: return; // POSIX guarantees we're still closed in EINTR cases case EIO: throw std::system_error(errno, std::system_category(), "I/O error on close()"); default: throw std::system_error(errno, std::system_category(), "System error on close()"); } // LCOV_EXCL_STOP } } } ================================================ FILE: src/zk/server/detail/close.hpp ================================================ #pragma once #include namespace zk::server::detail { /** A safe wrapper around the Linux \c close call. In particular, it transforms certain error codes that matter into * exceptions and happily ignores error codes that still close the file descriptor (\c EINTR). * * \throws system_error if an I/O error occurs (this can happen if the kernel defers writes -- although in general, you * have made a grave error by not performing a \c sync before \c close). * \throws system_error if \a fd is not a file descriptor. **/ void close(int fd); } ================================================ FILE: src/zk/server/detail/event_handle.cpp ================================================ #include "event_handle.hpp" #include "close.hpp" #include #include #include #include #include namespace zk::server::detail { event_handle::event_handle() : _fd(::eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK)) { } event_handle::~event_handle() noexcept { close(); } void event_handle::close() noexcept { if (_fd != -1) { detail::close(_fd); _fd = -1; } } void event_handle::notify_one() { std::uint64_t x = 1; if (::write(_fd, &x, sizeof x) == -1 && errno != EAGAIN) throw std::system_error(errno, std::system_category(), "event_handle::notify_one()"); } bool event_handle::try_wait() { std::uint64_t burn; if (::read(_fd, &burn, sizeof burn) == -1) return errno == EAGAIN ? false : throw std::system_error(errno, std::system_category(), "event_handle::try_wait()"); else return true; } } ================================================ FILE: src/zk/server/detail/event_handle.hpp ================================================ #pragma once #include namespace zk::server::detail { class event_handle final { public: using native_handle_type = int; public: explicit event_handle(); event_handle(const event_handle&) = delete; event_handle& operator=(const event_handle&) = delete; ~event_handle() noexcept; /// Close this event for future signalling. This is automatically called from the destructor. void close() noexcept; /// Signal this handle that something has happened. void notify_one(); /// Attempt to wait for this handle to be signalled, but do not block. /// /// \returns \c true if we successfully waited for a signal (and consumed it); \c false if this handle was not /// signalled. bool try_wait(); /// Get the file descriptor backing this handle. This is generally only used when interacting with the kernel and /// should be avoided in regular use. native_handle_type native_handle() { return _fd; } private: native_handle_type _fd; }; } ================================================ FILE: src/zk/server/detail/pipe.cpp ================================================ #include "close.hpp" #include "pipe.hpp" #include #include #include #include namespace zk::server::detail { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // pipe_closed // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// pipe_closed::pipe_closed() : std::runtime_error("I/O operation on closed pipe") { } pipe_closed::~pipe_closed() noexcept { } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // pipe // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// static int on_exec_flags(on_exec exec) { switch (exec) { case on_exec::close: return O_CLOEXEC; case on_exec::keep_open: default: return 0; } } pipe::pipe(on_exec exec) : _read_fd(-1), _write_fd(-1) { int fds[2]; if (::pipe2(fds, O_NONBLOCK | on_exec_flags(exec))) throw std::system_error(errno, std::system_category(), "Could not create pipe"); _read_fd = fds[0]; _write_fd = fds[1]; } pipe::~pipe() noexcept { close(); } static void close_if_open(int& fd) { if (fd != -1) { close(fd); fd = -1; } } void pipe::close() { close_write(); close_read(); } void pipe::close_read() { close_if_open(_read_fd); } void pipe::close_write() { close_if_open(_write_fd); } std::string pipe::read(optional max) { if (_read_fd == -1) throw pipe_closed(); std::string out; if (max) out.reserve(max.value()); while (true) { char read_buf[4096]; ssize_t rc = ::read(_read_fd, read_buf, sizeof read_buf); if (rc < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) rc = 0; if (rc < 0 && errno == EINTR) { continue; } else if (rc < 0 && errno != EINTR) { if (out.empty()) throw std::system_error(errno, std::system_category(), "Failed to read from pipe"); else return out; } else if (rc == 0) { return out; } else { out.append(read_buf, rc); } } } void pipe::write(const std::string& contents) { if (_write_fd == -1) throw pipe_closed(); ptr write_ptr = contents.data(); while (write_ptr < contents.data() + contents.size()) { ssize_t goal_dist = std::distance(write_ptr, contents.data() + contents.size()); ssize_t rc = ::write(_write_fd, write_ptr, goal_dist); if (rc < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) rc = 0; if (rc < 0 && errno == EINTR) { continue; } else if (rc < 0 && errno == EPIPE) { throw pipe_closed(); } else if (rc < 0) { throw std::system_error(errno, std::system_category(), "Failed to write to pipe"); } else { write_ptr += rc; } } } static int dup3(int src_fd, int redir_fd, on_exec exec) { while (true) { int rc = ::dup3(src_fd, redir_fd, on_exec_flags(exec)); if (rc == -1 && errno == EINTR) continue; else if (rc == -1 && errno != EINTR) throw std::system_error(errno, std::system_category()); else return rc; } } void pipe::subsume_read(handle fd, on_exec exec) { dup3(_read_fd, fd, exec); // close now unused read side of this pipe close_read(); } void pipe::subsume_write(handle fd, on_exec exec) { dup3(_write_fd, fd, exec); close_write(); } pipe::handle pipe::native_read_handle() { return _read_fd; } pipe::handle pipe::native_write_handle() { return _write_fd; } } ================================================ FILE: src/zk/server/detail/pipe.hpp ================================================ #pragma once #include #include #include #include namespace zk::server::detail { /** Used to specify behavior of POSIX resources when \c exec is called. **/ enum class on_exec { /** Enable the close-on-exec flag for an opened handle. This means that any subprocess created with \c exec will not * be able to access the resource of the parent process. **/ close, /** Keep the file descriptor open for any opened subprocesses. You probably do not want to use this flag unless you * intend to share a pipe with the subprocess. In general, it is safer for the subprocess to open the same file * itself. **/ keep_open, }; /** Thrown when a read or write operation is attempted on a \c pipe which is closed. **/ class pipe_closed : public std::runtime_error { public: pipe_closed(); virtual ~pipe_closed() noexcept; }; /** A unidirectional data channel that can be used for interprocess communication or as a signal-safe mechanism for * in-process communication. **/ class pipe final { public: using handle = int; public: /** Create a pipe. * * \param exec Should this pipe stay open when \c exec is called? See \c on_exec for more details. * \throws std::system_error if the pipe could not be created. **/ explicit pipe(on_exec exec = on_exec::close); pipe(const pipe&) = delete; pipe& operator=(const pipe&) = delete; ~pipe() noexcept; /** Close the read and write ends of this pipe. It is safe to call this multiple times (subsequent calls to \c close * will have no effect). **/ void close(); /** Similar to \c close, but only close the read end of the pipe. **/ void close_read(); /** Similar to \c close, but only close the write end of the pipe. **/ void close_write(); /** Read from the pipe (up to \a max). If there is nothing to read, an empty string is returned. * * \param max The maximum amount of bytes to read in a single pass. If unspecified, this will read until the pipe * appears to be empty. * \throws pipe_closed if the pipe is already closed. **/ std::string read(optional max = nullopt); /** Write the \a contents into the pipe. * * \throws pipe_closed if the pipe is already closed (this typically happens when communicating with a subprocess * which has been terminated). **/ void write(const std::string& contents); /** Redirect the provided \a fd to read from this pipe instead of what it was originally reading from. * * \example * This redirects standard input to be controlled by a pipe. See \c subprocess for the use case. * \code * pipe p; * p.subsume_read(STDIN_FILENO); * // Now p.write(...) calls will be picked up by reads to standard input * \endcode **/ void subsume_read(handle fd, on_exec exec = on_exec::close); /** Redirect the provided \a fd to write to this pipe instead of what it was originally writing to. **/ void subsume_write(handle fd, on_exec exec = on_exec::close); /** Get the native handle for the read end of this pipe. **/ handle native_read_handle(); /** Get the native handle for the write end of this pipe. **/ handle native_write_handle(); private: handle _read_fd; handle _write_fd; }; } ================================================ FILE: src/zk/server/detail/pipe_tests.cpp ================================================ #include #include "pipe.hpp" #include namespace zk::server::detail { GTEST_TEST(pipe_tests, read_write) { std::string buff(8000, 'a'); pipe p; p.write(buff); std::string out; out.reserve(buff.size()); while (out.size() < buff.size()) out += p.read(); CHECK_EQ(buff, out); } } ================================================ FILE: src/zk/server/detail/subprocess.cpp ================================================ #include "subprocess.hpp" #include #include #include #include #include #include #include #include #include #include namespace zk::server::detail { static pid_t create_subproc(pipe& stdin_pipe, pipe& stdout_pipe, pipe& stderr_pipe, std::string program_name, subprocess::argument_list args ) { std::vector arg_ptrs; arg_ptrs.reserve(args.size() + 2); arg_ptrs.emplace_back(program_name.c_str()); std::transform(begin(args), end(args), std::back_inserter(arg_ptrs), [] (const std::string& s) { return s.c_str(); } ); arg_ptrs.emplace_back(nullptr); pid_t pid = ::fork(); if (pid == 0) // child process { // LCOV_EXCL_START: No way to detect success at this point stdin_pipe.subsume_read(STDIN_FILENO, on_exec::keep_open); stdin_pipe.close(); stdout_pipe.subsume_write(STDOUT_FILENO, on_exec::keep_open); stdout_pipe.close(); stderr_pipe.subsume_write(STDERR_FILENO, on_exec::keep_open); stderr_pipe.close(); ::execvp(program_name.c_str(), const_cast(arg_ptrs.data()) ); // if we get here, exec failed std::exit(1); // LCOV_EXCL_STOP } else // parent process { stdin_pipe.close_read(); stdout_pipe.close_write(); stderr_pipe.close_write(); return pid; } } static void dont_leak(pipe& p) noexcept { auto fds = { p.native_read_handle(), p.native_write_handle() }; for (auto fd : fds) { if (fd == -1) continue; // ignore the return code -- the inability to set FD_CLOEXEC isn't fatal, just inconvenient ::fcntl(fd, F_SETFD, FD_CLOEXEC); } } subprocess::subprocess(std::string program_name, argument_list args) : _program_name(std::move(program_name)), _proc_id(-1), _stdin(on_exec::keep_open), _stdout(on_exec::keep_open), _stderr(on_exec::keep_open) { _proc_id = create_subproc(_stdin, _stdout, _stderr, _program_name, std::move(args)); // Set the on_exec for the pipes to no longer keep_open for future subprocesses -- there is a race condition here if // another thread creates a subprocess before we set FD_CLOEXEC. The worst thing that happens is we leak a couple of // (unused) file descriptors to the subprocess. There is no easy way to prevent this. dont_leak(_stdin); dont_leak(_stdout); dont_leak(_stderr); } subprocess::~subprocess() noexcept { terminate(); } void subprocess::terminate(duration_type time_to_abort) noexcept { auto alarm_time = [&] () -> unsigned int { if (time_to_abort.count() <= 0) return 1U; else if (time_to_abort.count() > 300) return 300U; else return static_cast(time_to_abort.count()); }(); for (unsigned attempt = 1U; _proc_id != -1; ++attempt) { auto old_sig_handler = ::signal(SIGALRM, [](int) { }); signal(attempt == 1U ? SIGTERM : SIGABRT); int rc; ::alarm(alarm_time); if (::waitpid(_proc_id, &rc, 0) > 0) { _proc_id = -1; } ::alarm(0); ::signal(SIGALRM, old_sig_handler); } } bool subprocess::signal(int sig_val) { if (_proc_id == -1) return false; pid_t pid = _proc_id; int rc = ::kill(pid, sig_val); if (rc == -1 && errno == ESRCH) return false; else if (rc == -1) throw std::system_error(errno, std::system_category()); else return true; } } ================================================ FILE: src/zk/server/detail/subprocess.hpp ================================================ #pragma once #include #include #include #include #include #include #include "pipe.hpp" namespace zk::server::detail { /// Represents an owned subprocess. class subprocess { public: using handle = int; using argument_list = std::vector; using duration_type = std::chrono::seconds; public: explicit subprocess(std::string program_name, argument_list args = argument_list()); subprocess(const subprocess&) = delete; subprocess& operator=(const subprocess&) = delete; ~subprocess() noexcept; /// Send a signal to this subprocess. /// /// \returns \c true if the signal likely reached the subprocess; \c false if it might not have (this can happen if /// the subprocess has already terminated). bool signal(int sig_val); pipe& stdin() noexcept { return _stdin; } pipe& stdout() noexcept { return _stdout; } pipe& stderr() noexcept { return _stderr; } /// Terminate the process if it is still running. In the first attempt to terminate, \c SIGTERM is used. If the /// process has not terminated before \a time_to_abort has passed, the process is signalled again with \c SIGABRT. void terminate(duration_type time_to_abort = std::chrono::seconds(1U)) noexcept; private: std::string _program_name; handle _proc_id; pipe _stdin; pipe _stdout; pipe _stderr; }; } ================================================ FILE: src/zk/server/detail/subprocess_tests.cpp ================================================ #include #include #include #include "subprocess.hpp" namespace zk::server::detail { GTEST_TEST(subprocess_tests, echo) { subprocess proc("echo", { "Hello, world!" }); std::string read_str; while (read_str.size() < 14) read_str += proc.stdout().read(4096); CHECK_EQ("Hello, world!\n", read_str); } // `cat` hangs on shutdown due to never getting EOF from stdin -- however, we should still be able to kill the process // with a signal on scope exit. GTEST_TEST(subprocess_tests, cat_kill) { std::chrono::steady_clock::time_point scope_exit_time; { subprocess proc("cat"); scope_exit_time = std::chrono::steady_clock::now(); } auto elapsed = std::chrono::steady_clock::now() - scope_exit_time; CHECK(elapsed < std::chrono::milliseconds(100)); } } ================================================ FILE: src/zk/server/package_registry.cpp ================================================ #include "package_registry.hpp" #include namespace zk::server { struct package_registry::registration_info final { std::weak_ptr owner; std::string name; registration_info(std::shared_ptr owner, std::string name) : owner(std::move(owner)), name(std::move(name)) { } ~registration_info() noexcept { if (auto strong_owner = owner.lock()) strong_owner->unregister_server(*this); } }; package_registry::package_registry() : _lifetime(std::make_shared()) { } package_registry::~package_registry() noexcept { std::unique_lock ax(_protect); _lifetime.reset(); } package_registry::registration package_registry::register_classpath_server(std::string version, classpath packages) { std::unique_lock ax(_protect); auto ret = _registrations.insert({ std::move(version), std::move(packages) }); if (!ret.second) throw std::invalid_argument(version + " is already registered"); return std::make_shared(std::shared_ptr(_lifetime, this), ret.first->first); } bool package_registry::unregister_server(const registration_info& reg) { std::unique_lock ax(_protect); return _registrations.erase(reg.name) > 0U; } bool package_registry::unregister_server(registration reg) { if (reg) return unregister_server(*reg); else return false; } package_registry::size_type package_registry::size() const { std::unique_lock ax(_protect); return _registrations.size(); } optional package_registry::find_newest_classpath() const { std::unique_lock ax(_protect); if (_registrations.empty()) return nullopt; else return _registrations.rbegin()->second; } } ================================================ FILE: src/zk/server/package_registry.hpp ================================================ #pragma once #include #include #include #include #include #include #include #include "classpath.hpp" namespace zk::server { /// \addtogroup Server /// \{ /// The package registry tracks configuration of classpaths and JARs needed to run various ZooKeeper versions. /// /// \note{Thread Safety} /// Registering and unregistering configurations is thread-safe. However, it is \e not safe when a \c package_registry /// is being destroyed. class package_registry final { public: using size_type = std::size_t; struct registration_info; using registration = std::shared_ptr; public: /// Create an empty registry. package_registry(); ~package_registry() noexcept; /// Register a server that can be created via the specified Java \a classpath. /// /// \param version A version string used to look up the server when creating them. While this can be a lie, it /// should not be. /// \param packages The Java classpath used to run the server. This will be the \c cp argument to Java. /// \returns a registration that can be used to \ref unregister_server. /// \throws std::invalid_argument if \a version is already registered. registration register_classpath_server(std::string version, classpath packages); /// \{ /// Attempt to unregister the server associated with the provided registration. Unregistering will prevent future /// servers from being created with the particular setup, but will not teardown servers which might be running with /// it. /// /// \returns \c true if this call removed anything; \c false if otherwise. bool unregister_server(registration reg); bool unregister_server(const registration_info& reg); /// \} /// How many registrations have been registered? size_type size() const; /// Is this registry empty? bool empty() const { return size() == size_type(0); } /// Get the classpath for running the newest registered server version. optional find_newest_classpath() const; private: mutable std::mutex _protect; std::shared_ptr _lifetime; std::map _registrations; }; /// \} } ================================================ FILE: src/zk/server/package_registry_tests.cpp ================================================ #include #include "classpath.hpp" #include "package_registry.hpp" #include "package_registry_tests.hpp" #include #include namespace zk::server { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // test_package_registry // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// package_registry& test_package_registry::instance() { static auto instance_ptr = std::make_shared(); return *instance_ptr; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Unit Tests // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// GTEST_TEST(package_registry_tests, test_package_registry_global_instance) { CHECK_LT(0U, test_package_registry::instance().size()); } GTEST_TEST(package_registry_tests, registration) { package_registry registry; CHECK_TRUE(registry.empty()); auto registration1 = registry.register_classpath_server("1.0", classpath({ "RANDOM" })); CHECK_EQ(1U, registry.size()); CHECK_THROWS(std::invalid_argument) { registry.register_classpath_server("1.0", classpath({ "RANDOM" })); }; auto registration2 = registry.register_classpath_server("2.0", classpath({ "RANDOM" })); CHECK_EQ(2U, registry.size()); registration2.reset(); CHECK_EQ(1U, registry.size()); CHECK_FALSE(registry.unregister_server(registration2)); CHECK_TRUE(registry.unregister_server(std::move(registration1))); CHECK_TRUE(registry.empty()); } } ================================================ FILE: src/zk/server/package_registry_tests.hpp ================================================ #pragma once #include namespace zk::server { class package_registry; /** Global package registry for server unit tests. **/ class test_package_registry final { public: static package_registry& instance(); }; } ================================================ FILE: src/zk/server/server.cpp ================================================ #include "server.hpp" #include #include #include #include #include #include #include #include "classpath.hpp" #include "configuration.hpp" #include "detail/event_handle.hpp" #include "detail/subprocess.hpp" namespace zk::server { static void validate_settings(const configuration& settings) { if (settings.is_minimal()) { return; } else if (!settings.source_file()) { throw std::invalid_argument("Configuration has not been saved to a file"); } } server::server(classpath packages, configuration settings) : _running(true), _shutdown_event(std::make_unique()) { validate_settings(settings); _worker = std::thread([this, packages = std::move(packages), settings = std::move(settings)] () { this->run_process(packages, settings); } ); } server::server(configuration settings) : server(classpath::system_default(), std::move(settings)) { } server::~server() noexcept { shutdown(true); } void server::shutdown(bool wait_for_stop) { _running.store(false, std::memory_order_release); _shutdown_event->notify_one(); if (wait_for_stop && _worker.joinable()) _worker.join(); } static void wait_for_event(int fd1, int fd2, int fd3) { // This could be implemented with epoll instead of select, but since N=3, it doesn't really matter ::fd_set read_fds; FD_ZERO(&read_fds); FD_SET(fd1, &read_fds); FD_SET(fd2, &read_fds); FD_SET(fd3, &read_fds); int nfds = std::max(std::max(fd1, fd2), fd3) + 1; int rc = ::select(nfds, &read_fds, nullptr, nullptr, nullptr); if (rc < 0) { if (errno == EINTR) return; else throw std::system_error(errno, std::system_category(), "select"); } } void server::run_process(const classpath& packages, const configuration& settings) { detail::subprocess::argument_list args = { "-cp", packages.command_line(), "org.apache.zookeeper.server.quorum.QuorumPeerMain", }; if (settings.is_minimal()) { args.emplace_back(std::to_string(settings.client_port())); args.emplace_back(settings.data_directory().value()); } else { args.emplace_back(settings.source_file().value()); } detail::subprocess proc("java", std::move(args)); auto drain_pipes = [&] () { bool read_anything = true; while (read_anything) { read_anything = false; auto out = proc.stdout().read(); if (!out.empty()) { read_anything = true; std::cout << out; } auto err = proc.stderr().read(); if (!err.empty()) { read_anything = true; std::cerr << out; } } }; while (_running.load(std::memory_order_acquire)) { wait_for_event(proc.stdout().native_read_handle(), proc.stderr().native_read_handle(), _shutdown_event->native_handle() ); drain_pipes(); } proc.terminate(); drain_pipes(); } } ================================================ FILE: src/zk/server/server.hpp ================================================ #pragma once #include #include #include #include #include #include namespace zk::server { namespace detail { class event_handle; } /// \defgroup Server /// Control a ZooKeeper \ref server process. /// \{ class classpath; class configuration; /// Controls a ZooKeeper server process on this local machine. class server final { public: /// Create a running server process with the specified \a packages and \a settings. /// /// \param packages The classpath to use to find ZooKeeper's \c QuorumPeerMain class. /// \param settings The server settings to run with. /// \throws std::invalid_argument If `settings.is_minimal()` is \c false and `settings.source_file()` is \c nullopt. /// This is because non-minimal configurations require ZooKeeper to be launched with a file. explicit server(classpath packages, configuration settings); /// Create a running server with the specified \a settings using the system-provided default packages for ZooKeeper /// (see \ref classpath::system_default). /// /// \param settings The server settings to run with. /// \throws std::invalid_argument If `settings.is_minimal()` is \c false and `settings.source_file()` is \c nullopt. /// This is because non-minimal configurations require ZooKeeper to be launched with a file. explicit server(configuration settings); server(const server&) = delete; ~server() noexcept; /// Initiate shutting down the server process. For most usage, this is not needed, as it is called automatically /// from the destructor. /// /// \param wait_for_stop If \c true, wait for the process to run until termination instead of simply initiating the /// termination. void shutdown(bool wait_for_stop = false); private: void run_process(const classpath&, const configuration&); private: std::atomic _running; std::unique_ptr _shutdown_event; std::thread _worker; // NOTE: The configuration is NOT stored in the server object. This is because configuration can be changed by the // ZK process in cases like ensemble reconfiguration. It is the job of run_process to deal with this. }; /// \} } ================================================ FILE: src/zk/server/server_group.cpp ================================================ #include "server_group.hpp" #include #include #include #include #include #include #include #include #include "classpath.hpp" #include "configuration.hpp" #include "server.hpp" namespace zk::server { struct server_group::info { configuration settings; //!< Settings for this server. std::string name; //!< Name of this server (string version of its ID) std::string path; //!< settings.data_directory std::uint16_t peer_port; //!< settings.peer_port std::uint16_t leader_port; //!< settings.leader_port std::shared_ptr instance; //!< Instance(if this is running) info(const configuration& base) : settings(base) { } }; server_group::server_group() noexcept { } server_group::server_group(server_group&&) noexcept = default; server_group& server_group::operator=(server_group&& src) noexcept { _servers = std::move(src._servers); _conn_string = std::move(src._conn_string); return *this; } static void create_directory(const std::string& path) { if (::mkdir(path.c_str(), 0755)) { throw std::system_error(errno, std::system_category()); } } static void save_id_file(const std::string& path, const server_id& id) { std::ofstream ofs(path.c_str()); if (!ofs) throw std::runtime_error("IO failure"); ofs << id << '\n'; ofs.flush(); } server_group server_group::make_ensemble(std::size_t size, const configuration& base_settings_in) { auto base_settings = base_settings_in; if (!base_settings.data_directory()) throw std::invalid_argument("Settings must specify a base directory"); auto base_directory = base_settings.data_directory().value(); auto base_port = base_settings.client_port() == configuration::default_client_port ? std::uint16_t(18500) : base_settings.client_port(); server_group out; for (std::size_t idx = 0U; idx < size; ++idx) { auto id = server_id(idx + 1); auto px = std::make_shared(base_settings); auto& x = *px; x.name = std::to_string(id.value); x.path = base_directory + "/" + x.name; x.settings .client_port(base_port++) .data_directory(x.path + "/data") ; x.peer_port = base_port++; x.leader_port = base_port++; out._servers.emplace(id, px); } std::ostringstream conn_str_os; conn_str_os << "zk://"; bool first = true; create_directory(base_directory); for (auto& [id, srvr] : out._servers) { for (const auto& [id2, srvr2] : out._servers) { srvr->settings.add_server(id2, "127.0.0.1", srvr2->peer_port, srvr2->leader_port); } create_directory(srvr->path); create_directory(srvr->path + "/data"); save_id_file(srvr->path + "/data/myid", id); srvr->settings.save_file(srvr->path + "/settings.cfg"); if (!std::exchange(first, false)) conn_str_os << ','; conn_str_os << "127.0.0.1:" << srvr->settings.client_port(); } conn_str_os << '/'; out._conn_string = conn_str_os.str(); return out; } const std::string& server_group::get_connection_string() { return _conn_string; } void server_group::start_all_servers(const classpath& packages) { for (auto& [name, srvr] : _servers) { static_cast(name); if (!srvr->instance) { srvr->instance = std::make_shared(packages, srvr->settings); } } } } ================================================ FILE: src/zk/server/server_group.hpp ================================================ #pragma once #include #include #include #include #include #include #include "configuration.hpp" namespace zk::server { /** \addtogroup Server * \{ **/ class classpath; class server; /// Create and manage a group of \ref server instances on this local machine (most likely in a single ensemble). This is /// exclusively for testing, as running multiple peers on a single machine is a very bad idea in production. /// /// \code /// auto servers = zk::server::server_group::make_ensemble(3U, /// zk::server::configuration::make_minimal("test-data") /// ); /// servers.start_all_servers(); /// auto client = zk::client::connect(servers.get_connection_string()).get(); /// // do things with client... /// \endcode class server_group final { public: /// Create an empty server group. server_group() noexcept; /// Move-construct a server group. server_group(server_group&&) noexcept; /// Move-assign a server group. server_group& operator=(server_group&&) noexcept; /// Create an ensemble of the given \a size. None of the servers will be started. /// /// \param size The size of the ensemble. This should be an odd number. /// \param base_settings The basic settings to use for every server. static server_group make_ensemble(std::size_t size, const configuration& base_settings); /// Get a connection string which can connect to any the servers in the group. const std::string& get_connection_string(); /// Start all servers in the group. Note that this does not wait for the servers to be up-and-running. Use a /// \ref client instance to check for connectability. void start_all_servers(const classpath& packages); /// How many servers are in this group? std::size_t size() const { return _servers.size(); } private: struct info; using server_map_type = std::map>; private: server_map_type _servers; std::string _conn_string; }; /// \} } ================================================ FILE: src/zk/server/server_group_tests.cpp ================================================ #include #include #include #include #include "configuration.hpp" #include "package_registry.hpp" #include "package_registry_tests.hpp" #include "server_group.hpp" namespace zk::server { void delete_directory(std::string path); GTEST_TEST(server_group_tests, ensemble) { delete_directory("ensemble"); auto group = server_group::make_ensemble(5U, configuration::make_minimal("ensemble")); group.start_all_servers(test_package_registry::instance().find_newest_classpath().value()); // connect and get data from the ensemble auto c = client::connect(group.get_connection_string()).get(); CHECK_TRUE(c.exists("/").get()); } } ================================================ FILE: src/zk/server/server_tests.cpp ================================================ #include #include #include #include #include #include #include #include #include #include "classpath.hpp" #include "configuration.hpp" #include "package_registry.hpp" #include "package_registry_tests.hpp" #include "server.hpp" #include "server_tests.hpp" namespace zk::server { void delete_directory(std::string path) { auto unlink_cb = [] (ptr fpath, ptr, int, ptr) -> int { return std::remove(fpath); }; if (nftw(path.c_str(), unlink_cb, 64, FTW_DEPTH | FTW_PHYS)) { if (errno == ENOENT) return; else throw std::system_error(errno, std::system_category()); } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // server_fixture // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void server_fixture::SetUp() { delete_directory("zk-data"); _server = std::make_shared(test_package_registry::instance().find_newest_classpath().value(), configuration::make_minimal("zk-data") ); _conn_string = "zk://127.0.0.1:2181"; } void server_fixture::TearDown() { _server->shutdown(); _server.reset(); _conn_string.clear(); } const std::string& server_fixture::get_connection_string() const { return _conn_string; } client server_fixture::get_connected_client() const { return client(client::connect(get_connection_string()).get()); } void server_fixture::stop_server(bool wait_for_stop) { _server->shutdown(wait_for_stop); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // single_server_fixture // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// static std::shared_ptr single_server_server; static std::string single_server_conn_string; void single_server_fixture::SetUpTestCase() { delete_directory("zk-data"); single_server_server = std::make_shared(test_package_registry::instance().find_newest_classpath().value(), configuration::make_minimal("zk-data") ); single_server_conn_string = "zk://127.0.0.1:2181"; } void single_server_fixture::TearDownTestCase() { single_server_server->shutdown(); single_server_server.reset(); single_server_conn_string.clear(); } const std::string& single_server_fixture::get_connection_string() { return single_server_conn_string; } client single_server_fixture::get_connected_client() { return client(client::connect(get_connection_string()).get()); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Unit Tests // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// GTEST_TEST(server_tests, start_stop) { server svr(test_package_registry::instance().find_newest_classpath().value(), configuration::make_minimal("zk-data") ); std::this_thread::sleep_for(std::chrono::seconds(1)); svr.shutdown(); } GTEST_TEST(server_tests, shutdown_and_wait) { server svr(test_package_registry::instance().find_newest_classpath().value(), configuration::make_minimal("zk-data") ); svr.shutdown(true); } } ================================================ FILE: src/zk/server/server_tests.hpp ================================================ #pragma once #include #include #include #include namespace zk::server { class server; class server_fixture : public test::test_fixture { public: virtual void SetUp() override; virtual void TearDown() override; protected: const std::string& get_connection_string() const; client get_connected_client() const; void stop_server(bool wait_for_stop = true); private: std::shared_ptr _server; std::string _conn_string; }; /// Similar to \ref server_fixture, but do not start up and tear down the server with each test. Instead, setup is run /// once at the start of a suite and torn down at the end of it. class single_server_fixture : public test::test_fixture { public: static void SetUpTestCase(); static void TearDownTestCase(); protected: static const std::string& get_connection_string(); static client get_connected_client(); }; } ================================================ FILE: src/zk/string_view.hpp ================================================ /// \file /// Imports the \c string_view type as \c std::string_view. #pragma once #include #include namespace zk { /// \addtogroup Client /// \{ using string_view = std::string_view; /// \} } ================================================ FILE: src/zk/tests/main.cpp ================================================ #include "test.hpp" int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); return ::RUN_ALL_TESTS(); } ================================================ FILE: src/zk/tests/test.cpp ================================================ #include "test.hpp" ================================================ FILE: src/zk/tests/test.hpp ================================================ #pragma once #include #ifdef GTEST_API_ # error "GTest was included externally -- you MUST include first" #endif // Before including GTest, tell it not to define the various generically-named macros (which clash with other common // names). #define GTEST_DONT_DEFINE_ASSERT_EQ 1 #define GTEST_DONT_DEFINE_ASSERT_NE 1 #define GTEST_DONT_DEFINE_ASSERT_LE 1 #define GTEST_DONT_DEFINE_ASSERT_LT 1 #define GTEST_DONT_DEFINE_ASSERT_GE 1 #define GTEST_DONT_DEFINE_ASSERT_GT 1 #define GTEST_DONT_DEFINE_TEST 1 #define GTEST_DONT_DEFINE_FAIL 1 #define GTEST_DONT_DEFINE_SUCCEED 1 #include // Maybe some day this will be defined #ifndef GTEST_TEST_F # define GTEST_TEST_F TEST_F #endif namespace zk::test { /** \def CHECK * Check a condition and abort the test in failure if it does not hold. When performing binary comparisons, prefer to * use the check macros which accept two arguments (\c CHECK_EQ, \c CHECK_LT, etc), as these will print out the values * in failure cases. * * \example * \code * CHECK(something) << "Something isn't right!"; * \endcode **/ #define CHECK(cond) GTEST_TEST_BOOLEAN_(cond, #cond, false, true, GTEST_FATAL_FAILURE_) #define CHECK_TRUE(cond) GTEST_TEST_BOOLEAN_(!!(cond), #cond, false, true, GTEST_FATAL_FAILURE_) #define CHECK_FALSE(cond) GTEST_TEST_BOOLEAN_(!(cond), #cond, true, false, GTEST_FATAL_FAILURE_) #define CHECK_EQ GTEST_ASSERT_EQ #define CHECK_NE GTEST_ASSERT_NE #define CHECK_LT GTEST_ASSERT_LT #define CHECK_LE GTEST_ASSERT_LE #define CHECK_GT GTEST_ASSERT_GT #define CHECK_GE GTEST_ASSERT_GE #define CHECK_SUCCESS GTEST_SUCCEED #define CHECK_FAIL GTEST_FAIL using test_fixture = ::testing::Test; namespace detail { template struct check_throws_info { ptr filename; std::size_t line_no; explicit check_throws_info(ptr filename, std::size_t line_no) : filename(filename), line_no(line_no) { } friend std::ostream& operator<<(std::ostream& os, const check_throws_info& info) { return os << info.filename << ':' << info.line_no; } }; template void operator+(const check_throws_info& info, FAction&& action) { try { std::forward(action)(); CHECK_FAIL() << "At " << info << ": Action was supposed to throw, but it did not"; // LCOV_EXCL_LINE } catch (const TException&) { CHECK_SUCCESS() << "Successfully threw expected error"; } } /** \def CHECK_THROWS * Ensure that a block of code throws an exception of type \a ex. * * \code * CHECK_THROWS(std::logic_error) * { * throw std::logic_error("Some issue"); * }; // <- note the ';' * \endcode **/ #define CHECK_THROWS(ex) \ ::zk::test::detail::check_throws_info(__FILE__, __LINE__) + [&] () } } ================================================ FILE: src/zk/types.cpp ================================================ #include "types.hpp" #include #include #include namespace zk { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // event_type // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// std::ostream& operator<<(std::ostream& os, const event_type& self) { switch (self) { case event_type::error: return os << "error"; case event_type::created: return os << "created"; case event_type::erased: return os << "erased"; case event_type::changed: return os << "changed"; case event_type::child: return os << "child"; case event_type::session: return os << "session"; case event_type::not_watching: return os << "not_watching"; default: return os << "event_type(" << static_cast(self) << ')'; }; } std::string to_string(const event_type& self) { std::ostringstream os; os << self; return os.str(); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // version // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// std::ostream& operator<<(std::ostream& os, const version& self) { os << "version("; if (self == version::any()) os << "any"; else if (self == version::invalid()) os << "invalid"; else os << self.value; return os << ')'; } std::string to_string(const version& self) { std::ostringstream os; os << self; return os.str(); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // acl_version // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// std::ostream& operator<<(std::ostream& os, const acl_version& self) { os << "acl_version("; if (self == acl_version::any()) os << "any"; else if (self == acl_version::invalid()) os << "invalid"; else os << self.value; return os << ')'; } std::string to_string(const acl_version& self) { std::ostringstream os; os << self; return os.str(); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // child_version // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// std::ostream& operator<<(std::ostream& os, const child_version& self) { os << "child_version("; if (self == child_version::any()) os << "any"; else if (self == child_version::invalid()) os << "invalid"; else os << self.value; return os << ')'; } std::string to_string(const child_version& self) { std::ostringstream os; os << self; return os.str(); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // transaction_id // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// std::ostream& operator<<(std::ostream& os, const transaction_id& self) { return os << "transaction_id(" << self.value << ')'; } std::string to_string(const transaction_id& self) { std::ostringstream os; os << self; return os.str(); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // stat // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// std::ostream& operator<<(std::ostream& os, const stat& self) { os << "{data_version=" << self.data_version.value; os << " child_version=" << self.child_version.value; os << " acl_version=" << self.acl_version.value; os << " data_size=" << self.data_size; os << " children_count=" << self.children_count; os << " ephemeral=" << (self.is_ephemeral() ? "true" : "false"); return os << '}'; } std::string to_string(const stat& self) { std::ostringstream os; os << self; return os.str(); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // state // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// std::ostream& operator<<(std::ostream& os, const state& self) { switch (self) { case state::closed: return os << "closed"; case state::connecting: return os << "connecting"; case state::connected: return os << "connected"; case state::read_only: return os << "read_only"; case state::expired_session: return os << "expired_session"; case state::authentication_failed: return os << "authentication_failed"; default: return os << "state(" << static_cast(self) << ')'; } } std::string to_string(const state& self) { std::ostringstream os; os << self; return os.str(); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // create_mode // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// std::ostream& operator<<(std::ostream& os, const create_mode& mode) { if (mode == create_mode::normal) return os << "normal"; bool first = true; auto tick = [&] { return std::exchange(first, false) ? "" : "|"; }; if (is_set(mode, create_mode::ephemeral)) os << tick() << "ephemeral"; if (is_set(mode, create_mode::sequential)) os << tick() << "sequential"; if (is_set(mode, create_mode::container)) os << tick() << "container"; return os; } std::string to_string(const create_mode& self) { std::ostringstream os; os << self; return os.str(); } } ================================================ FILE: src/zk/types.hpp ================================================ #pragma once #include #include #include #include namespace zk { /// \addtogroup Client /// \{ /// Base type for creating strong ID types. These behave similar to a \c typedef, but do not allow conversion between /// different \c TReal types, even if they share the same \c TId. This makes attempting to use a \c version instead of /// an \c acl_version in \ref client::set_acl a compile-time failure instead of throwing a \c bad_version at run-time. /// /// \tparam TReal The "real" leaf type of this ID. /// \tparam TId The representation type of this ID. This must be some form of integer and is usually \c std::int32_t (as /// in \ref basic_version). /// /// \see version /// \see child_version /// \see acl_version /// \see transaction_id template struct strong_id { /// The representation type of this ID. using value_type = TId; /// Underlying value of this ID. value_type value; /// Default construct the ID. This probably leaves \c value uninitialized (depending on the constructor of /// \ref value_type). strong_id() noexcept = default; /// Construct this instance with the given \a value. constexpr explicit strong_id(value_type value) noexcept : value(value) { } /// Get the \ref value of this ID. constexpr value_type get() const noexcept { return value; } /// Get the \ref value of this ID. /// /// \see get explicit constexpr operator value_type() noexcept { return value; } /// \{ /// Increment the \ref value of this ID by 1. This can be useful for maintaining caches after updates without /// getting the result. For example, a transaction with a version-checked set operation knows the next version of /// the entry will be only one more. TReal& operator++() { ++this->value; return *static_cast(this); } TReal operator++(int) { TReal copy(*this); operator++(); return copy; } /// \} /// \{ /// Decrement the \ref value of this ID by 1. This operation is probably not useful, but is included for /// completeness. TReal& operator--() { --this->value; return *static_cast(this); } TReal operator--(int) { TReal copy(*this); operator--(); return copy; } /// \} }; template constexpr bool operator==(const strong_id& a, const strong_id& b) { return a.value == b.value; } template constexpr bool operator!=(const strong_id& a, const strong_id& b) { return a.value != b.value; } template constexpr bool operator<(const strong_id& a, const strong_id& b) { return a.value < b.value; } template constexpr bool operator<=(const strong_id& a, const strong_id& b) { return a.value <= b.value; } template constexpr bool operator>(const strong_id& a, const strong_id& b) { return a.value > b.value; } template constexpr bool operator>=(const strong_id& a, const strong_id& b) { return a.value >= b.value; } /// Compute the \c std::hash of the given \a x. template inline std::size_t hash(const strong_id& x) { return std::hash()(x.value); } /// Base type for version types. These are distinct so we can emit a compilation error on attempts to use the incorrect /// version type (for example, \ref client::set_acl does a check on \ref acl_version instead of the standard /// \ref version). /// /// \see version /// \see acl_version /// \see child_version template struct basic_version : public strong_id { /// An invalid version specifier. This will never be returned by the database and will always be rejected in commit /// operations. This is a good value to use as a placeholder when you are searching for the proper \ref version. static constexpr TReal invalid() { return TReal(-42); }; /** When specified in an operation, this version specifier will always pass. It is the equivalent to not performing * a version check. **/ static constexpr TReal any() { return TReal(-1); }; using strong_id::strong_id; }; /// Represents a version of the data. /// /// \see stat::data_version struct version final : public basic_version { using basic_version::basic_version; }; std::ostream& operator<<(std::ostream&, const version&); std::string to_string(const version&); /// Represents a version of the ACL of an entry. /// /// \see stat::acl_version struct acl_version final : public basic_version { using basic_version::basic_version; }; std::ostream& operator<<(std::ostream&, const acl_version&); std::string to_string(const acl_version&); /// Represents a version of the children of an entry. /// /// \see stat::child_version struct child_version final : public basic_version { using basic_version::basic_version; }; std::ostream& operator<<(std::ostream&, const child_version&); std::string to_string(const child_version&); /// Represents the ZooKeeper transaction ID in which an event happened to an entry. /// /// \see stat::create_transaction /// \see stat::modified_transaction /// \see stat::child_modified_transaction struct transaction_id final : public strong_id { using strong_id::strong_id; }; std::ostream& operator<<(std::ostream&, const transaction_id&); std::string to_string(const transaction_id&); /// Statistics about a ZooKeeper entry, similar to the UNIX `stat` structure. /// /// \note{Time in ZooKeeper} /// The concept of time is tricky in distributed systems. ZooKeeper keeps track of time in a number of ways. /// /// - **zxid**: Every change to a ZooKeeper cluster receives a stamp in the form of a *zxid* (ZooKeeper Transaction ID). /// This exposes the total ordering of all changes to ZooKeeper. Each change will have a unique *zxid* -- if *zxid:a* /// is smaller than *zxid:b*, then the associated change to *zxid:a* happened before *zxid:b*. /// - **Version Numbers**: Every change to an entry will cause an increase to one of the version numbers of that entry. /// - **Clock Time**: ZooKeeper does not use clock time to make decisions, but it uses it to put timestamps into the /// \c stat structure. struct stat final { public: using time_point = std::chrono::system_clock::time_point; public: /// The transaction ID that created the entry. transaction_id create_transaction; /// The last transaction that modified the entry. transaction_id modified_transaction; /// The transaction ID that last modified the children of the entry. transaction_id child_modified_transaction; /// Time the entry was created. /// /// \warning /// This should \e not be relied on for any logic. ZooKeeper sets this time based on the system clock of the master /// server at the time the entry is created and performs no validity checking or synchronization with other servers /// in the cluster. As such, there is no guarantee that this value is accurrate. There are many situations where a /// entry with a higher \c create_transaction (created after) will have a lower \c create_time (appear to have been /// created before). time_point create_time; /// Last time the entry was last modified. Like \ref create_time, this is not a reliable source. time_point modified_time; /// The number of changes to the data of the entry. This value is used in operations like \c client::set that take a /// version check before modifying data. zk::version data_version; /// The number of changes to the children of the entry. zk::child_version child_version; /// The number of changes to the ACL of the entry. zk::acl_version acl_version; /// The session ID of the owner of this entry, if it is an ephemeral entry. In general, this is not useful beyond a /// check for being \c 0. /// /// \see is_ephemeral std::uint64_t ephemeral_owner; /// The size of the data field of the entry. std::size_t data_size; /// The number of children this entry has. std::size_t children_count; /// Is the entry an ephemeral entry? bool is_ephemeral() const { return ephemeral_owner == 0U; } }; std::ostream& operator<<(std::ostream&, const stat&); std::string to_string(const stat&); /// When used in \c client::set, this value determines how the entry is created on the server. These values can be ORed /// together to create combinations. enum class create_mode : unsigned int { /// Standard behavior of an entry -- the opposite of doing any of the options. normal = 0b0000, /// The entry will be deleted when the client session expires. ephemeral = 0b0001, /// The name of the entry will be appended with a monotonically increasing number. The actual path name of a /// sequential entry will be the given path plus a suffix \c "i" where \c i is the current sequential number of the /// entry. The sequence number is always fixed length of 10 digits, 0 padded. Once such a entry is created, the /// sequential number will be incremented by one. sequential = 0b0010, /// Container entries are special purpose entries useful for recipes such as leader, lock, etc. When the last child /// of a container is deleted, the container becomes a candidate to be deleted by the server at some point in the /// future. Given this property, you should be prepared to get \ref no_entry when creating children inside of this /// container entry. container = 0b0100, }; /// Set union operation of \ref create_mode. constexpr create_mode operator|(create_mode a, create_mode b) { return create_mode(static_cast(a) | static_cast(b)); } /// Set intersection operation of \ref create_mode. constexpr create_mode operator&(create_mode a, create_mode b) { return create_mode(static_cast(a) & static_cast(b)); } /// Set inverse operation of \ref create_mode. This is not exactly the bitwise complement of \a a, as the returned value /// will only include bits set that are valid in \ref create_mode discriminants. constexpr create_mode operator~(create_mode a) { return create_mode(~static_cast(a) & 0b0111); } /// Check that \a self has \a flags set. constexpr bool is_set(create_mode self, create_mode flags) { return (self & flags) == flags; } std::ostream& operator<<(std::ostream&, const create_mode&); std::string to_string(const create_mode&); /// Enumeration of types of events that may occur. enum class event_type : int { error = 0, //!< Invalid event (this should never be issued). created = 1, //!< Issued when an entry for a given path is created. erased = 2, //!< Issued when an entry at a given path is erased. changed = 3, //!< Issued when the data of a watched entry is altered. This event value is issued whenever //!< a \e set operation occurs without an actual contents check, so there is no guarantee the //!< data actually changed. child = 4, //!< Issued when the children of a watched entry are created or deleted. This event is not //!< issued when the data within children is altered. session = -1, //!< This value is issued as part of an event when the \c state changes. not_watching = -2, //!< Watch has been forcefully removed. This is generated when the server for some reason //!< (probably a resource constraint), will no longer watch an entry for a client. }; std::ostream& operator<<(std::ostream&, const event_type&); std::string to_string(const event_type&); /// Enumeration of states the client may be at when a watch triggers. It represents the state of the connection at the /// time the event was generated. /// /// \dot /// digraph G { /// rankdir = LR /// /// connecting /// connected /// read_only /// authentication_failed /// expired_session /// closed /// /// connecting -> connected [label="Successful connection"] /// connecting -> read_only [label="Connection to read-only peer"] /// connecting -> authentication_failed [label="Authentication failure"] /// connecting -> expired_session [label="Session lost"] /// connecting -> closed [label="close()"] /// connected -> connecting [label="Connection lost" color="red"] /// connected -> closed [label="close()"] /// read_only -> connecting [label="Connection lost" color="red"] /// read_only -> closed [label="close()"] /// authentication_failed -> closed [label="close()"] /// expired_session -> closed [label="close()"] /// } /// \enddot /// /// \note /// If you are familiar with the C API, notably missing from this list is a \c ZOO_NOTCONNECTED_STATE equivalent. This /// state happens in cases where the client disconnects on purpose (either on initial connection or just after ensemble /// reconfiguration). However, the ability to see this state is limited to times when you call \c zk_state at just the /// right moment. This state leads to a bit of confusion with \c closed and \c expired_session, so it is not in the /// list. Instead, these cases are presented as just \c connecting, as the client is attempting to reconnect to the /// cluster. enum class state : int { closed = 0, //!< The client is not connected to any server in the ensemble. connecting = 1, //!< The client is connecting. connected = 3, //!< The client is in the connected state -- it is connected to a server in the //!< ensemble (one of the servers specified in the host connection parameter during //!< ZooKeeper client creation). read_only = 5, //!< The client is connected to a read-only server, that is the server which is not //!< currently connected to the majority. The only operations allowed after receiving //!< this state is read operations. This state is generated for read-only clients only //!< since read/write clients aren't allowed to connect to read-only servers. expired_session = -112, //!< The serving cluster has expired this session. The ZooKeeper client connection //!< (the session) is no longer valid. You must create a new client \c connection if //!< you wish to access the ensemble. authentication_failed = -113, //!< Authentication has failed -- connection requires a new \c connection instance //!< with different credentials. }; std::ostream& operator<<(std::ostream&, const state&); /// Get the string representation of the provided \a state. std::string to_string(const state& state); /// \} } namespace std { template <> struct hash { using argument_type = zk::version; using result_type = std::size_t; result_type operator()(const argument_type& x) const { return zk::hash(x); } }; template <> struct hash { using argument_type = zk::acl_version; using result_type = std::size_t; result_type operator()(const argument_type& x) const { return zk::hash(x); } }; template <> struct hash { using argument_type = zk::child_version; using result_type = std::size_t; result_type operator()(const argument_type& x) const { return zk::hash(x); } }; template <> struct hash { using argument_type = zk::transaction_id; using result_type = std::size_t; result_type operator()(const argument_type& x) const { return zk::hash(x); } }; } ================================================ FILE: src/zk/types_tests.cpp ================================================ #include #include "types.hpp" namespace zk { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // event_type // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// GTEST_TEST(event_type_tests, stringification) { CHECK_EQ("error", to_string(event_type::error)); CHECK_EQ("created", to_string(event_type::created)); CHECK_EQ("erased", to_string(event_type::erased)); CHECK_EQ("changed", to_string(event_type::changed)); CHECK_EQ("child", to_string(event_type::child)); CHECK_EQ("session", to_string(event_type::session)); CHECK_EQ("not_watching", to_string(event_type::not_watching)); CHECK_EQ("event_type(764532)", to_string(static_cast(764532))); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // state // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// GTEST_TEST(state_tests, stringification) { CHECK_EQ("closed", to_string(state::closed)); CHECK_EQ("connecting", to_string(state::connecting)); CHECK_EQ("connected", to_string(state::connected)); CHECK_EQ("read_only", to_string(state::read_only)); CHECK_EQ("expired_session", to_string(state::expired_session)); CHECK_EQ("authentication_failed", to_string(state::authentication_failed)); CHECK_EQ("state(605983)", to_string(static_cast(605983))); } }