From 7068d3e2f9c7216f94aef4d82b626257e73ab2f6 Mon Sep 17 00:00:00 2001 From: Kenneth Jia <48558845+kenneth-jia@users.noreply.github.com> Date: Fri, 18 Sep 2020 14:09:43 +0800 Subject: [PATCH] Initial Commit --- .clang-tidy | 43 + .gitignore | 6 + CMakeLists.txt | 186 ++ LICENSE | 202 ++ NOTICE | 3 + README.md | 120 + doc/CMakeLists.txt | 50 + doc/GoodPracticesToUseKafkaConsumer.md | 22 + doc/GoodPracticesToUseKafkaProducer.md | 76 + doc/HowToMakeKafkaProducerReliable.md | 221 ++ doc/KafkaBrokerConfiguration.md | 155 + doc/KafkaClientConfiguration.md | 51 + doc/KafkaConsumerQuickStart.md | 154 + doc/KafkaProducerQuickStart.md | 251 ++ include/CMakeLists.txt | 8 + include/kafka/AdminClient.h | 291 ++ include/kafka/AdminClientConfig.h | 44 + include/kafka/BrokerMetadata.h | 154 + include/kafka/ConsumerConfig.h | 96 + include/kafka/ConsumerRecord.h | 154 + include/kafka/Error.h | 66 + include/kafka/Header.h | 56 + include/kafka/KafkaClient.h | 545 ++++ include/kafka/KafkaConsumer.h | 960 +++++++ include/kafka/KafkaException.h | 52 + include/kafka/KafkaProducer.h | 665 +++++ include/kafka/Logger.h | 65 + include/kafka/ProducerConfig.h | 133 + include/kafka/ProducerRecord.h | 101 + include/kafka/Project.h | 7 + include/kafka/Properties.h | 84 + include/kafka/RdKafkaHelper.h | 114 + include/kafka/Timestamp.h | 82 + include/kafka/Types.h | 171 ++ include/kafka/Utility.h | 69 + scripts/doxyfile.cfg | 2511 +++++++++++++++++ scripts/markdown2html.py | 80 + tests/CMakeLists.txt | 10 + tests/integration/CMakeLists.txt | 23 + tests/integration/TestAdminClient.cc | 111 + tests/integration/TestKafkaConsumer.cc | 1267 +++++++++ tests/integration/TestKafkaEnv.cc | 18 + tests/integration/TestKafkaProducer.cc | 506 ++++ tests/unit/CMakeLists.txt | 23 + tests/unit/TestBrokerMetadata.cc | 90 + tests/unit/TestConsumerRecord.cc | 94 + tests/unit/TestError.cc | 45 + tests/unit/TestHeader.cc | 37 + .../unit/TestKafkaClientDefaultProperties.cc | 135 + tests/unit/TestKafkaException.cc | 92 + tests/unit/TestProducerRecord.cc | 44 + tests/unit/TestProperties.cc | 81 + tests/unit/TestTimestamp.cc | 55 + tests/unit/TestTypes.cc | 49 + tests/utils/TestUtility.h | 151 + tools/CMakeLists.txt | 70 + tools/KafkaConsoleConsumer.cc | 171 ++ tools/KafkaConsoleProducer.cc | 146 + tools/KafkaTopics.cc | 200 ++ 59 files changed, 11466 insertions(+) create mode 100644 .clang-tidy create mode 100755 .gitignore create mode 100644 CMakeLists.txt create mode 100644 LICENSE create mode 100644 NOTICE create mode 100644 README.md create mode 100644 doc/CMakeLists.txt create mode 100644 doc/GoodPracticesToUseKafkaConsumer.md create mode 100644 doc/GoodPracticesToUseKafkaProducer.md create mode 100644 doc/HowToMakeKafkaProducerReliable.md create mode 100644 doc/KafkaBrokerConfiguration.md create mode 100644 doc/KafkaClientConfiguration.md create mode 100644 doc/KafkaConsumerQuickStart.md create mode 100644 doc/KafkaProducerQuickStart.md create mode 100644 include/CMakeLists.txt create mode 100644 include/kafka/AdminClient.h create mode 100644 include/kafka/AdminClientConfig.h create mode 100644 include/kafka/BrokerMetadata.h create mode 100644 include/kafka/ConsumerConfig.h create mode 100644 include/kafka/ConsumerRecord.h create mode 100644 include/kafka/Error.h create mode 100644 include/kafka/Header.h create mode 100644 include/kafka/KafkaClient.h create mode 100644 include/kafka/KafkaConsumer.h create mode 100644 include/kafka/KafkaException.h create mode 100644 include/kafka/KafkaProducer.h create mode 100644 include/kafka/Logger.h create mode 100644 include/kafka/ProducerConfig.h create mode 100644 include/kafka/ProducerRecord.h create mode 100644 include/kafka/Project.h create mode 100644 include/kafka/Properties.h create mode 100644 include/kafka/RdKafkaHelper.h create mode 100644 include/kafka/Timestamp.h create mode 100644 include/kafka/Types.h create mode 100644 include/kafka/Utility.h create mode 100644 scripts/doxyfile.cfg create mode 100755 scripts/markdown2html.py create mode 100644 tests/CMakeLists.txt create mode 100644 tests/integration/CMakeLists.txt create mode 100644 tests/integration/TestAdminClient.cc create mode 100644 tests/integration/TestKafkaConsumer.cc create mode 100644 tests/integration/TestKafkaEnv.cc create mode 100644 tests/integration/TestKafkaProducer.cc create mode 100644 tests/unit/CMakeLists.txt create mode 100644 tests/unit/TestBrokerMetadata.cc create mode 100644 tests/unit/TestConsumerRecord.cc create mode 100644 tests/unit/TestError.cc create mode 100644 tests/unit/TestHeader.cc create mode 100644 tests/unit/TestKafkaClientDefaultProperties.cc create mode 100644 tests/unit/TestKafkaException.cc create mode 100644 tests/unit/TestProducerRecord.cc create mode 100644 tests/unit/TestProperties.cc create mode 100644 tests/unit/TestTimestamp.cc create mode 100644 tests/unit/TestTypes.cc create mode 100644 tests/utils/TestUtility.h create mode 100644 tools/CMakeLists.txt create mode 100644 tools/KafkaConsoleConsumer.cc create mode 100644 tools/KafkaConsoleProducer.cc create mode 100644 tools/KafkaTopics.cc diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 000000000..af679d54b --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,43 @@ +Checks: "*,\ + -llvm-header-guard,\ + -llvm-namespace-comment,\ + -fuchsia-default-arguments,\ + -fuchsia-statically-constructed-objects,\ + -fuchsia-overloaded-operator,\ + -google-default-arguments,\ + -google-readability-namespace-comments,\ + -google-build-using-namespace,\ + -google-runtime-references,\ + -modernize-use-nodiscard,\ + -modernize-deprecated-headers,\ + -modernize-avoid-c-arrays,\ + -hicpp-special-member-functions,\ + -hicpp-vararg,\ + -hicpp-no-malloc,\ + -hicpp-avoid-c-arrays,\ + -hicpp-no-array-decay,\ + -hicpp-deprecated-headers,\ + -hicpp-braces-around-statements,\ + -cppcoreguidelines-special-member-function,\ + -cppcoreguidelines-macro-usage,\ + -cppcoreguidelines-avoid-magic-numbers,\ + -cppcoreguidelines-pro-type-vararg,\ + -cppcoreguidelines-pro-type-cstyle-cast,\ + -cppcoreguidelines-pro-bounds-array-to-pointer-decay,\ + -cppcoreguidelines-pro-bounds-pointer-arithmetic,\ + -cppcoreguidelines-special-member-functions,\ + -cppcoreguidelines-owning-memory,\ + -cppcoreguidelines-avoid-c-arrays,\ + -cppcoreguidelines-no-malloc,\ + -cppcoreguidelines-non-private-member-variables-in-classes,\ + -cppcoreguidelines-pro-bounds-constant-array-index,\ + -misc-non-private-member-variables-in-classes,\ + -readability-magic-numbers,\ + -readability-implicit-bool-conversion,\ + -readability-braces-around-statements,\ + -readability-isolate-declaration,\ + -bugprone-macro-parentheses,\ + -bugprone-unused-return-value,\ + -cert-err58-cpp,\ + -cert-err60-cpp" + diff --git a/.gitignore b/.gitignore new file mode 100755 index 000000000..0727dffce --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*.swp +*.pyc +*.*~ + +build/ +install/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 000000000..ca2eca7f2 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,186 @@ +cmake_minimum_required(VERSION "3.8") + +project("Modern C++ based Kafka API" VERSION 1.0.0) + +include(CTest) +include(CheckCXXCompilerFlag) +include(CMakePushCheckState) + + +#--------------------------- +# Suggest to use C++17 +#--------------------------- +set(CMAKE_CXX_STARNDARD 17) +set(CMAKE_CXX_STARNDARD_REQUIRED False) + +add_compile_options("-Wall" "-Werror" "-Wextra" "-Wshadow") + +#--------------------------- +# Check librdkafka directory +#--------------------------- +if (DEFINED ENV{LIBRDKAFKA_ROOT}) + set(LIBRDKAFKA_ROOT $ENV{LIBRDKAFKA_ROOT}) + message(STATUS "librdkafka root directory: ${LIBRDKAFKA_ROOT}") +else () + message(FATAL_ERROR "No LIBRDKAFKA_ROOT specified!") +endif () + +include_directories(SYSTEM ${LIBRDKAFKA_ROOT}/include) +link_directories(SYSTEM ${LIBRDKAFKA_ROOT}/lib) + + +#--------------------------- +# Build Option: clang-tidy +#--------------------------- +option(BUILD_OPTION_CLANG_TIDY "Build with clang-tidy enabled" OFF) +if (BUILD_OPTION_CLANG_TIDY) + find_program(CLANG_TIDY_EXE NAMES "clang-tidy") + + if (CLANG_TIDY_EXE) + message(STATUS "Use clang-tidy: ${CLANG_TIDY_EXE}") + set(CMAKE_CXX_CLANG_TIDY clang-tidy -warnings-as-errors=* -header-filter=.*) + else () + message(FATAL_ERROR "The clang-tidy executable not found!") + endif () + +else () + message(STATUS "With NO clang-tidy build option") +endif () + + +#--------------------------- +# Build Option: ASAN +#--------------------------- +option(BUILD_OPTION_USE_ASAN "Build with Address Sanitizer (ASAN) enabled" OFF) + +if (BUILD_OPTION_USE_ASAN) + check_cxx_compiler_flag("-fsanitize=address" HAS_ASAN) + + CMAKE_PUSH_CHECK_STATE(RESET) + # Make check_cxx_compiler_flag pass required flags to linker as well: + set(CMAKE_REQUIRED_FLAGS "-fsanitize=address -static-libasan") + check_cxx_compiler_flag("-fsanitize=address -static-libasan" HAS_ASAN_NEEDS_LIB) + CMAKE_POP_CHECK_STATE() + + if (HAS_ASAN) + add_compile_options("-fsanitize=address") + add_link_options("-fsanitize=address") + elseif (HAS_ASAN_NEEDS_LIB) + add_compile_options("-fsanitize=address" "-static-libasan") + add_link_options("-fsanitize=address" "-static-libasan") + else () + message(FATAL_ERROR "Address Sanitizer requested by BUILD_OPTION_USE_ASAN, but appears to be not supported on this platform") + endif () + + set(MEMORYCHECK_TYPE AddressSanitizer) + + message(STATUS "Use Address Sanitizer") +endif () + + +#--------------------------- +# Build Option: TSAN +#--------------------------- +option(BUILD_OPTION_USE_TSAN "Build with Thread Sanitizer (TSAN) enabled" OFF) + +if (BUILD_OPTION_USE_TSAN) + check_cxx_compiler_flag("-fsanitize=thread" HAS_TSAN) + + CMAKE_PUSH_CHECK_STATE(RESET) + # Make check_cxx_compiler_flag pass required flags to linker as well: + set(CMAKE_REQUIRED_FLAGS "-fsanitize=thread -static-libtsan") + check_cxx_compiler_flag("-fsanitize=thread -static-libtsan" HAS_TSAN_NEEDS_LIB) + CMAKE_POP_CHECK_STATE() + + if (HAS_TSAN) + add_compile_options("-fsanitize=thread") + add_link_options("-fsanitize=thread") + elseif (HAS_TSAN_NEEDS_LIB) + add_compile_options("-fsanitize=thread" "-static-libtsan") + add_link_options("-fsanitize=thread" "-static-libtsan") + else () + message(FATAL_ERROR "Thread Sanitizer requested by BINLOG_USE_TSAN, but appears to be not supported on this platform") + endif () + + set(MEMORYCHECK_TYPE ThreadSanitizer) + + message(STATUS "Use Thread Sanitizer") +endif () + + +#--------------------------- +# Build Option: UBSAN +#--------------------------- +option(BUILD_OPTION_USE_UBSAN "Build with Undefined Behavior Sanitizer (UBSAN) enabled" OFF) + +if (BUILD_OPTION_USE_UBSAN) + check_cxx_compiler_flag("-fsanitize=undefined" HAS_UBSAN) + + CMAKE_PUSH_CHECK_STATE(RESET) + # Make check_cxx_compiler_flag pass required flags to linker as well: + set(CMAKE_REQUIRED_FLAGS "-fsanitize=undefined -static-libubsan") + check_cxx_compiler_flag("-fsanitize=undefined -static-libubsan" HAS_UBSAN_NEEDS_LIB) + CMAKE_POP_CHECK_STATE() + + if (HAS_UBSAN_NEEDS_LIB) + add_compile_options("-fsanitize=undefined" "-static-libubsan") + add_link_options("-fsanitize=undefined" "-static-libubsan") + elseif (HAS_UBSAN) + add_compile_options("-fsanitize=undefined") + add_link_options("-fsanitize=undefined") + else () + message(FATAL_ERROR "Undefined Behavior Sanitizer requested by BINLOG_USE_UBSAN, but appears to be not supported on this platform") + endif () + + message(STATUS "Use Undefined Behavior Sanitizer") +endif () + + +#--------------------------- +# Build Option: generate doc +#--------------------------- +option(BUILD_OPTION_GEN_DOC "Generate html files for doxygen/markdown doc" OFF) + + +#--------------------------- +# Build Option: generate coverage report +#--------------------------- +option(BUILD_OPTION_GEN_COVERAGE "Generate code coverage report" OFF) + +if (BUILD_OPTION_GEN_COVERAGE) + check_cxx_compiler_flag("-fprofile-instr-generate -fcoverage-mapping" HAS_CLANG_COV) + + if (HAS_CLANG_COV) + add_compile_options("-fprofile-instr-generate" "-fcoverage-mapping") + add_link_options("-fprofile-instr-generate" "-fcoverage-mapping") + + add_custom_target(coverage_init + COMMENT "Initialize coverage counters" + COMMAND "rm" "-f" "tests/unit/default.profraw" "tests/default.profdata") + + add_custom_target(coverage + COMMENT "Generate coverage report" + COMMAND "llvm-profdata" "merge" "-sparse" "tests/unit/default.profraw" + "-o" "tests/default.profdata" + COMMAND "llvm-cov" "show" "-format" "html" "-instr-profile" "tests/default.profdata" "tests/unit/kafka-unit-test" + ">" "coverage_report.html" + COMMAND "echo" "Coverage report generated: coverage_report.html" + ) + else () + message(FATAL_ERROR "Coverage report requrested by BUILD_OPTION_GEN_COVERAGE, but only supported with Clang build") + endif () + + message(STATUS "Enable code coverage data generation") +endif () + + +#--------------------------- +# Build Sub-directories +#--------------------------- +add_subdirectory("include") +add_subdirectory("tests") +add_subdirectory("tools") + +if (BUILD_OPTION_GEN_DOC) + add_subdirectory("doc") +endif () diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..fbf435137 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + 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 [2019] [Morgan Stanley] + + 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. \ No newline at end of file diff --git a/NOTICE b/NOTICE new file mode 100644 index 000000000..c333d92a8 --- /dev/null +++ b/NOTICE @@ -0,0 +1,3 @@ +Modern C++ based Kafka API +Copyright 2020 Morgan Stanley +This project includes software developed at Morgan Stanley. diff --git a/README.md b/README.md new file mode 100644 index 000000000..92832e18f --- /dev/null +++ b/README.md @@ -0,0 +1,120 @@ +# About the `Modern C++ based Kafka API` + +## Introduction + +The `Modern C++ based Kafka API` is a layer of C++ wrapper based on [librdkafka](https://github.com/edenhill/librdkafka) (the C part), with high quality, but more friendly to users. + +Here is the [doxygen documentation for developers](doxygen/annotated.html). + +- At present, the `Modern C++ based Kafka API` is compatible with `librdkafka` v1.4.2. + +## Why it's here + +The `librdkafka` is a robust high performance C/C++ library, -- widely used and well maintained. + +Unfortunately, its `C++ interface` (a thin wrapper of its C lib), -- compared with Java version APIs -- is not object-oriented, nor ease-of-use. + +Thus we worked out `Modern C++ based Kafka API`, which makes the development work much easier. + +## Features + +* Java-like APIs + + * Here're some reference links for Java's native kafka clients, -- much helpful for cross-reference + + [org.apache.kafka.clients.producer](https://kafka.apache.org/22/javadoc/org/apache/kafka/clients/producer/package-summary.html) + + [org.apache.kafka.clients.consumer](https://kafka.apache.org/22/javadoc/org/apache/kafka/clients/consumer/package-summary.html) + + [org.apache.kafka.clients.admin](https://kafka.apache.org/22/javadoc/org/apache/kafka/clients/admin/package-summary.html) + +* Robust + + * Verified with kinds of test cases, which cover many abnormal scenarios (edge cases) + + * Stability test with unstable brokers + + * Memory leak check for failed client with on-flight messages + + * Client failure and taking over, etc. + +* Efficient + + * No extra performance cost (No deep copy introduced internally) + + * Much better (2~4 times throughput) performance result than those native language (Java/Scala) implementation, in most commonly used cases (message size: 256 B ~ 2 KB) + +* Headers only + + * No extra library required to link + +## Build + +* To build its `tools`/`tests`, you should + + * Specify library locations with environment variables + + * `LIBRDKAFKA_ROOT` -- `librdkafka` headers and libraries + + * `GMOCK_ROOT` -- `googlemock` headers and libraries + + * `BOOST_ROOT` -- `boost` libraries + + * `SASL2_LIBRARYDIR` -- if SASL connection support is wanted + + * Create an empty directory for the build, and `cd` to it + + * Build commands + + * Type `cmake path-to-project-root` + + * Type `make` (could follow build options with `-D`) + + * `BUILD_OPTION_USE_ASAN=ON` -- Use Address Sanitizer + + * `BUILD_OPTION_USE_TSAN=ON` -- Use Thread Sanitizer + + * `BUILD_OPTION_USE_UBSAN=ON` -- Use Undefined Behavior Sanitizer + + * `BUILD_OPTION_CLANG_TIDY=ON` -- Enable clang-tidy checking + + * `BUILD_OPTION_GEN_DOC=ON` -- Generate documentation as well + + * `BUILD_OPTION_GEN_COVERAGE=ON` -- Generate test coverage, only support by clang currently + + * Type `make install` + +## Install + + * The APIs is headers only + + * Just need to include the `include/kafka` directory in your project + + * The compiler should support + + * Option 1: C++17 + + * Option 2: C++14, together with `boost` headers (would depend on `boost::optional` in the case) + +## Start-up + +* Prepare the servers (ZooKeeper/Kafka cluster) + + * [Start the servers](https://kafka.apache.org/documentation/#quickstart_startserver) + +* [KafkaProducer Quick Start](doc/KafkaProducerQuickStart.md) + +* [KafkaConsumer Quick Start](doc/KafkaConsumerQuickStart.md) + +* [KafkaClient Configuration](doc/KafkaClientConfiguration.md) + +## How to achieve good availability & performance + +* [Kafka Broker Configuration](doc/KafkaBrokerConfiguration.md) + +* [Good Practices to Use KafkaProducer](doc/GoodPracticesToUseKafkaProducer.md) + +* [Good Practices to Use KafkaConsumer](doc/GoodPracticesToUseKafkaConsumer.md) + +* [How to Make KafkaProducer Reliable](doc/HowToMakeKafkaProducerReliable.md) + diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt new file mode 100644 index 000000000..4655f3789 --- /dev/null +++ b/doc/CMakeLists.txt @@ -0,0 +1,50 @@ +find_program (DOXYGEN_EXECUTABLE NAMES doxygen) +if (NOT DOXYGEN_EXECUTABLE) + message(FATAL_ERROR "Could not find doxygen!") +else() + message("Doxygen build started... [${DOXYGEN_EXECUTABLE}]") +endif() + +set(DOXYGEN_INPUT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../include/kafka) + +set(doxyfile_in ${CMAKE_CURRENT_SOURCE_DIR}/../scripts/doxyfile.cfg) +set(doxyfile_out ${CMAKE_CURRENT_BINARY_DIR}/doxyfile.cfg) +configure_file(${doxyfile_in} ${doxyfile_out} @ONLY) + +add_custom_target( + doxygen-doc ALL + COMMAND ${DOXYGEN_EXECUTABLE} ${doxyfile_out} + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/../include + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + COMMENT "Generating doxygen documents" + VERBATIM +) + +install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/doxygen DESTINATION doc) + +message("Markdown build started...") +set (DOC_ENTRY_SRC ${CMAKE_CURRENT_SOURCE_DIR}/../README.md) +set (MARKDOWN_EXECUTABLE "${CMAKE_CURRENT_SOURCE_DIR}/../scripts/markdown2html.py") +set (MARKDOWN_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/markdown") +file(GLOB MARKDOWN_INPUT_FILES ${CMAKE_CURRENT_SOURCE_DIR}/*.md) + +add_custom_target( + doc-entry ALL + COMMAND ${MARKDOWN_EXECUTABLE} -i ${DOC_ENTRY_SRC} -o ${CMAKE_CURRENT_BINARY_DIR} + DEPENDS ${DOC_ENTRY_SRC} + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + COMMENT "Generating documents entry" + VERBATIM +) + +add_custom_target( + markdown-doc ALL + COMMAND ${MARKDOWN_EXECUTABLE} -i ${MARKDOWN_INPUT_FILES} -o ${MARKDOWN_OUTPUT_DIR} + DEPENDS ${MARKDOWN_INPUT_FILES} + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + COMMENT "Generating markdown documents" + VERBATIM +) + +install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/markdown DESTINATION doc) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/README.html DESTINATION doc) diff --git a/doc/GoodPracticesToUseKafkaConsumer.md b/doc/GoodPracticesToUseKafkaConsumer.md new file mode 100644 index 000000000..8d2f30774 --- /dev/null +++ b/doc/GoodPracticesToUseKafkaConsumer.md @@ -0,0 +1,22 @@ +# Good Practices to Use a KafkaConsumer + +If we want to achieve high performance/availability, here're some rules of thumb. + +## How to distribute the messages (for the same topics) to different KafkaConsumers + +* Use a consumer group for these KafkaConsumers, thus they will work together -- each one deals with different partitions. + +* Besides `subscribe` (topics), users could also choose to explicitly `assign` certain partitions to a `KafkaConsumer`. + +## How to enhance the throughput + +* Try with a larger `QUEUED_MIN_MESSAGES`, especially for small messages. + +* Use multiple KafkaConsumers to distribute the payload. + +## How to avoid polling duplicated messages + +* A `KafkaManualCommitConsumer` could help to commit the offsets more frequently (e.g, always do commit after finishing processing a message). + +* Don't use quite a large `MAX_POLL_RECORDS` for a `KafkaAutoCommitConsumer`, -- you might fail to commit all these messages before crash, thus more duplications with the next `poll`. + diff --git a/doc/GoodPracticesToUseKafkaProducer.md b/doc/GoodPracticesToUseKafkaProducer.md new file mode 100644 index 000000000..e0ad8f6c5 --- /dev/null +++ b/doc/GoodPracticesToUseKafkaProducer.md @@ -0,0 +1,76 @@ +# Good Practices to Use a KafkaProducer + +If we want to achieve high performance/availability, here're some rules of thumb. + +## Use a `KafkaAsyncProducer` for better throughput + +You should not use a `KafkaSyncProducer` if you want to get a high throughput. The `Sync` means the `send` operation is a synchronous operation, and would not go on until the `acks` are received. + +## The `message.max.bytes` must be consistent with Kafka servers' setting + +* Default value: 1000,000 + +* The default setting for brokers is `message.max.bytes = 1000012`, and do MAKE SURE the client side setting no larger than it. Otherwise, it might construct a MessageSet which would be rejected (error: INVALID_MESSAGE_SIZE) by brokers. + +## In most cases, the default value for `linger.ms` is good enough + +* It means the delay in milliseconds to wait for messages in the producer queue to accumulate before constructing MessageSets to transmit to brokers. + + 1. For a `KafkaSyncProducer`, it sends the messages one by one (after `acks` response received), thus it could use `linger.ms=0` (as default) to eliminate the unnecessary waiting time. + + 2. For a `KafkaAsyncProduer`, a larger `linger.ms` could be used to accumulate messages for MessageSets to improve the performance -- it should be the result of balancing between throughput and latency. + +## Calculate `batch.num.messages` with the average message size + +* Default value: 10,000 + +* It defines the maximum number of messages batched in one MessageSet. + + Normally, larger value, better performance. However, since the size of MessageSet is limited by `message.max.bytes`, a too large value would not help any more. + + E.g, with the default `message.max.bytes=1000000` and `batch.num.messages=10000` settings, you could get the best performance while the average message size is larger than 100 bytes. + + However, if the average message size is small, you have to enlarge it (to `message.max.bytes/average_message_size` at least). + +## Choose `acks` wisely + +* The acks parameter controls how many partition replicas must receive the record before the producer can consider the write successful. + + * `acks=0`, the producer will not wait for a reply from the broker before assuming the message was sent successfully. + + * `acks=1`, the producer will receive a success response from the broker the moment the leader replica received the message. + + * `acks=all`, the producer will receive a success response from the broker once all in-sync replicas received the message. + + * Note: if "ack=all", please make sure the topic's replication factor is larger than 1. + +* The `acks=all` setting will highly impact the throughput & latency, and it would be obvious if the traffic latency between kafka brokers is high. But it's mandatory if we want to achieve high availability. + +## Determine the default sending buffer (according to the latency) + +* Default queue.buffing setting, + + * `queue.buffering.max.messages=1000000` + + * `queue.buffering.max.kbytes=0x100000` (1GB) + +* Normally, the default settings should be good enough. + +## How could a message miss after send? + +* The message might even not have been received by the partition leader! (with `acks=0`) + +* Once the message received by the partition leader, the leader crashed just after responding to the producer, but has no chance to synchronize the message to other replicas. (with `acks=1`) + +* Once the message received by the partition leader, the leader crashed just after responding to the producer, but with no in-sync replica to synchronize for the message. (with `acks=all`, while brokers are with `min.insync.replicas=1`) + +## How does message disordering happen? How to avoid it? + +* Take an example, -- a `KafkaAsyncProducer` just sent many messages, and a few of these messages (in the middle) failed to be delivered successfully. While the producer got the sending error, it might retry sending these messages again, thus causing the disordering. + +* To avoid it. Two options, + + * Use a `KafkaSyncProducer`, but this would severely impact the throughput. + + * Embed some `sequence number` (e.g, record id, part of the `key`, etc) in the `ProducerRecord`, for de-duplication. + diff --git a/doc/HowToMakeKafkaProducerReliable.md b/doc/HowToMakeKafkaProducerReliable.md new file mode 100644 index 000000000..77c866ddd --- /dev/null +++ b/doc/HowToMakeKafkaProducerReliable.md @@ -0,0 +1,221 @@ +# How to Make KafkaProducer Reliable + +While using message dispatching systems, we always suffer from message lost, duplication and disordering. + +Since the application (using the `KafkaProducer`) might crash/restart, we might consider using certain mechanism to achieve `At most once`/`At least once`, and `Ordering`, -- such as locally persisting the messages until successful delivery, using embedded sequence number to de-duplicate, or responding data-source to acknowledgement the delivery result, etc. These are common topics, which are not quite specific to Kafka. + +Here we'd focus on `KafkaProducer`, together with the `idempotence` feature. Let's see, in which cases problems might happen, how to avoid them, and what's the best practise,-- to achieve `No Message Lost`, `Exactly Once` and `Ordering`. + + +## About `No Message Lost` + +### When might a message actually be lost + +* The producer gets a successful delivery response after sending the message, but the `partition leader` failed to sync it to other `replicas`. + +### How could a message be lost even with successful delivery + +* First, the `partition leader` doesn't sync-up the latest message to enough `in-sync replicas` before responding with the `ack` + + * The `partition leader` just don't need to wait for other `replica`s response + + - E.g, the producer is configured with `acks=1` + + * No available `in-sync replica` to wait for the response + + - E.g, all other replicas are not in-sync + +* Then, the `partition leader` crashes, and one `in-sync replica` becomes new `partition leader` + + * The new `partition leader` has no acknowledgement with the latest messages. Later, while new messages arrive, it would use conflicting record offsets (same with those records which the `partition leader` knows only). Then, even if the previous `partition leader` comes up again, these records have no chance to be recovered (just internally overwritten to be consistent with other replicas). + +### How to make sure `No Message Lost` + +* Make sure the leader would wait for responses from all in-sync replicas before the response + + * Configuration `acks=all` is a MUST for producer + +* Ensure enough `In-Sync partition replicas` + + * Configuration `min.insync.replicas >= 2` is a MUST for brokers + + - Take `min.insync.replicas = 2` for example, it means, + + 1. At most `replication.factor - min.insync.replicas` replicas are out-of-sync, -- the producer would still be able to send messages, otherwise, it could fail with 'no enough replica' error, and keeps retrying. + + 2. Occasionally no more than `min.insync.replicas` in-sync-replica failures. -- otherwise, messages might be missed. In this case, if just one in-sync replica crashes after sending back the ack to the producer, the message would not be lost; if two failed, it would! Since the new leader might be a replica which was not in-sync previously, and has no acknowledgement with these latest messages. + + * Please refer to [Kafka Broker Configuration](KafkaBrokerConfiguration.md) for more details. + + * Then, what would happen if replicas fail + + 1. Fails to send (`not enough in-sync replica failure`), -- while number of `in-sync replicas` could not meet `min.insync.replication` + + 2. Lost messages (after sending messages), -- with no `in-sync replica` survived from multi-failures + + 3. No message lost (while with all `in-sync replicas` acknowledged, and at least one `in-sync replica` available) + + +## About `Exactly Once` + +### How duplications happen + +* After brokers successfully persisted a message, it sent the `ack` to the producer. But for some abnormal reasons (such as network failure, etc), the producer might fail to receive the `ack`. The `librdkafka`'s internal queue would retry, thus another (duplicated) message would be persisted by brokers. + +### How to guarantee `Exactly Once` + +* The `enable.idempotence` configuration would resolve such a problem. And this configuration is RECOMMENDED for both `KafkaSyncProducer` and `KafkaAsyncProducer`, as long as it's possible. + + +## About `Ordering` + +### No ordering between partitions + +* Make sure these `ProducerRecord`s be with the same partition + + - Explicitly assigned with the same `topic-partition` + + - Use the same `key` for these records + +### How disordering happens within one partition + +* The `librdkafka` uses internal partition queues, and once a message fails to be sent successfully(e.g, brokers are down), it would be put back on the queue and retries again while `retry.backoff.ms` expires. However, before that (retry with the failed message), the brokers might recover and the messages behind (if with configuration `max.in.flight > 1`) happened to be sent successfully. In this case (with configuration `max.in.flight > 1` and `retries > 0`), disordering could happen, and the user would not even be aware of it. + +* Furthermore, while the last retry still failed, delivery callback would eventually be triggered. The user has to determine what to do for that (might want to re-send the message, etc). But there might be a case, -- some later messages had already been saved successfully by the server, thus no way to revert the disordering. + + +## More About `Idempotent producer` + +Please refer to the document from librdkafka, [Idempotent Producer](https://github.com/edenhill/librdkafka/blob/master/INTRODUCTION.md#idempotent-producer) for more details. + +### Extra fields to maintain the message sequence + +The `librdkafka` maintains the original produce() ordering per-partition for all messages produced, using an internal per-partition 64-bit counter called the `msgid` which starts at 1. This `msgid` allows messages to be re-inserted in the partition message queue in the original order in the case of retries. + +The Idempotent Producer functionality in the Kafka protocol also has a per-message `sequence number`, which is a signed 32-bit wrapping counter that is reset each time the `Producer's ID (PID)` or `Epoch` changes. + +The `msgid` is used, (along with a base `msgid` value stored at the time the `PID/Epoch` was bumped), to calculate the Kafka protocol's message `sequence number`. + +### Configuration conflicts + +* Since the following configuration properties are adjusted automatically (if not modified by the user). Producer instantiation will fail if user-supplied configuration is incompatible. + + - `acks = all` + + - `max.in.flight (i.e, `max.in.flight.requests.per.connection`) = 5` + + - `retries = INT32_MAX` + +### Error handling + +* Exception thrown during `send` + + * For these errors which could be detected locally (and could not be recovered with retrying), an exception would be thrown. E.g, invalid message, as RD_KAFKA_RESP_ERR_INVALID_MSG_SIZE (conflicting with local configuration `message.max.bytes`). + +* Permanent errors (respond from brokers) + + * Typical errors are: + + * Invalid message: RD_KAFKA_RESP_ERR_CORRUPT_MESSAGE, RD_KAFKA_RESP_ERR_MSG_SIZE_TOO_LARGE, RD_KAFKA_RESP_ERR_INVALID_REQUIRED_ACKS, RD_KAFKA_RESP_ERR_UNSUPPORTED_FOR_MESSAGE_FORMAT, RD_KAFKA_RESP_ERR_RECORD_LIST_TOO_LARGE. + + * Topic/Partition not exist: ERR_UNKNOWN_TOPIC_OR_PART, -- automatic topic creation is disabled on the broker or the application is specifying a partition that does not exist. + + * Authorization failure: ERR_TOPIC_AUTHORIZATION_FAILED, ERR_CLUSTER_AUTHORIZATION_FAILED + + * Normally, `Permanent error` means careless design, or wrong configuration, which should be avoided from the very beginning. + + * Unless with `enable.gapless.guarantee`(EXPERIMENTAL) configured, producer would keep going with the following messages; otherwise, it would purge all messages in-flight/in-queue (with RD_KAFKA_RESP_ERR__PURGE_INFLIGHT/RD_KAFKA_RESP_ERR__PURGE_QUEUE). + +* Temporary errors + + * Apart from those `permanent errors`, most of the left are temporary errors, which will be retried (if retry count permits); and while `message.timeout` expired, message delivery callback would be triggered with `RD_KAFKA_RESP_ERR__TIEMD_OUT`. + +* Be careful with the `RD_KAFKA_RESP_ERR__TIEMD_OUT` failure + + * There's some corner cases, such as a message that has been persisted by brokers but `KafkaProducer` failed to get the response. If `message.timeout.ms` has not expired, the producer could retry and eventually get the response. Otherwise, (i.e, `message.timeout.ms` expired before the producer receives the successful `ack`), it would be considered as a delivery failure by the producer (while the brokers wouldn't). Users might re-transmit the message thus causing duplications. + + * To avoid this tricky situation, a longer `message.timeout.ms` is RECOMMENDED, to make sure there's enough time for transmission retries / on-flight responses. + +### Performance impact + +* The main impact comes from `max.in.flight=5` limitation. Currently, `max.in.flight` means `max.in.flight.per.connection`, -- that's 5 message batches (with size of ~1MB at the most) in flight (not get the `ack` response yet) at the most, towards per broker. Within low-latency networks, it would not be a problem; while in other cases, it might be! Good news is, there might be a plan (in `librdkafka`) to improve that `per.connection` limit to `per.partition`, thus boost the performance a lot. + + +## The best practice for `KafkaProducer` + +* Enable `enable.idempotence` configuration + +* Use a long `message.timeout.ms`, which would let `librdkafka` keep retrying, before triggering the delivery failure callback. + + +## Some examples + +### `KafkaSyncProducer` demo + +```cpp + int ret = 0; + std::atomic running = true; + + KafkaSyncProducer producer( + Properties({ + { ProducerConfig::BOOTSTRAP_SERVERS, "192.168.0.1:9092,192.168.0.2:9092,192.168.0.3:9092" }, + { ProducerConfig::ENABLE_IDEMPOTENCE, "true" }, + { ProducerConfig::MESSAGE_TIMEOUT_MS, "60000"} + }) + ); + + while (running) { + const auto& msg = topMsgOfUpstream(); + try { + auto record = ProducerRecord(topic, msg.key, msg.value); + producer.send(record); + popMsgFromUpstream(); + } catch (const KafkaException& e) { + std::cerr << "Failed to send message! Reason: " << e.what() << std::endl; + ret = e.error(); + break; + } + } + + producer.close(); + + return ret; +``` + +* It's easy to use `KafkaSyncProducer`, since it sends messages one by one. + +* The throughput performance would not be that good, since there's only 1 message (embedded in 1 message batch) on the flight. + +### `KafkaAsyncProducer` demo + +```cpp + std::atomic running = true; + + KafkaAsyncProducer producer( + Properties({ + { ProducerConfig::BOOTSTRAP_SERVERS, "192.168.0.1:9092,192.168.0.2:9092,192.168.0.3:9092" }, + { ProducerConfig::ENABLE_IDEMPOTENCE, "true" }, + { ProducerConfig::MESSAGE_TIMEOUT_MS, "86400000"} // as long as 1 day + }) + ); + + while (running) { + auto msg = fetchMsgFromUpstream(); + auto record = ProducerRecord(topic, msg.key, msg.value, msg.id); + producer.send(record, + // Ack callback + [&msg](const Producer::RecordMetadata& metadata, std::error_code ec) { + // the message could be identified by `metadata.recordId()` + auto recordId = metadata.recordId(); + if (ec) { + std::cerr << "Cannot send out message with recordId: " << recordId << ", error:" << ec.message() << std::endl; + } else { + commitMsgToUpstream(recordId); + } + }); + } + + producer.close(); +``` + +* With a long `message.timeout.ms`, we're not likely to catch an error with delivery callback, --it would retry for temporary errors anyway. But be aware with permanent errors, it might be caused by careless design. diff --git a/doc/KafkaBrokerConfiguration.md b/doc/KafkaBrokerConfiguration.md new file mode 100644 index 000000000..914d90c71 --- /dev/null +++ b/doc/KafkaBrokerConfiguration.md @@ -0,0 +1,155 @@ +# Kafka Broker Configuration + +## Java Version + +* Recommend the latest released version of JDK 1.8, -- LinkedIn is currently running JDK 1.8 u5. + +## JVM Configuration + +* Here is a sample for `KAFKA_JVM_PERFORMANCE_OPTS` + + -Xmx8g -Xms8g -XX:MetaspaceSize=96m -XX:+UseG1GC -XX:MaxGCPauseMillis=20 -XX:InitiatingHeapOccupancyPercent=35 -XX:G1HeapRegionSize=16M -XX:MinMetaspaceFreeRatio=50 -XX:MaxMetaspaceFreeRatio=80 + +## Deployment + +* In at least three data centers with high bandwidth and low latency between them. (Commonly, using three availability zones inside one region of a cloud provider) + + * IMPORTANT: the `latency/bandwidth` between brokers could highly impact the `throughput/latency` of a producer client. + +* `rack.id` could be used to identify brokers from different data centers. + + +## Functionality + +* Controller + + * Maintains leaders/replicas info for partitions. + +* Partition Replicas + + * Leader replica + + * All produce/consume requests go through the leader. + + * In-Sync Replica + + * Replicas that are continuously asking for the latest messages; has caught up to the most recent message in 10 seconds (`replica.lag.time.max.ms`). + + * The preferred in-sync replica would be promoted to new leader while the previous one fails. + +## OS tuning + +(Use `sysctl`, or edit `/etc/sysctl.conf` for permanent change) + +* File descriptor limits + + * `fs.file-max` + + * Recommended 100000 or higher. + +* Maximum number of memory map areas for a process + + * `vm.max_map_count` + + * Each log segment uses 2 map areas. + +* Virtual memory + + * It's best to avoid swapping at all costs. + + * Set `vm.swappiness` to a very low value (e.g, 1). + +* Dirty page + + * `vm.dirty_background_ratio=5`, is appropriate in many situations. + + * `vm.dirty_ratio=60(~80)`, is a reasonable number. + +* Networking + + * `net.core.wmem_default` and `net.core.rmem_default`, reasonable setting: 131072 (128KB). + + * `net.core.wmem_max` and `net.core.rmem_max`, reasonable setting: 2097152 (2MB). + + * `net.ipv4.tcp_wmem` and `net.ipv4.tcp_rmem`, an example setting: 4096 (4KB minimum), 65536 (64KB default), 2048000 (2MB maximum). + + * `net.ipv4.tcp_window_scaling=1`. + + * `net.ipv4.tcp_max_syn_backlog`, should be above 1024 (default) to allow more simultaneous connections. + + * `net.core.netdev_max_backlog`, should be above 1000 (default) to allow more packets to be queued to process. + +## Disks and File-system + +* Throughput of the broker disks directly influence the performance of producer clients. + +* EXT4 and XFS are the most popular choices (XFS with better performance). Some companies are even trying with ZFS. + +* Do not use mounted shared drives and any network file systems. + +* Do not share the same drives used for Kafka data with other applications to ensure good latency. + +## Broker Settings + +* Auto-created Topics + + * With `auto.create.topics.enable=true`, a topic could be created while, + + 1. A producer starts writing messages to the topic. + + 2. A consumer starts reading messages from the topic. + + 3. Any client requests metadata for the topic. + + * The auto-created topics might not be what you want + + You might want to override some default configurations + + 1. `default.replication.factor=3` + + * We recommend a replication factor of 3 (at least) for any topic where availability is an issue. + + * The replication factor should be no more than the number of brokers. + + 2. `offsets.topic.replication.factor=3` + + * It's for the internal topic `__consumer_offsets`, -- auto-topic-creation will fail with a GROUP_COORDINATOR_NOT_AVAILABLE error if the cluster can't meet this replication factor requirement. + + 3. `num.partitions=5` (or whatever you want) + + * Unclean leader election + + * Set `unclean.leader.election.enable=false` to avoid out-of-sync replicas. + + * Minimal in-sync replicas + + * Set `min.insync.replicas=2` (at least) for fault-tolerant. + + * Log + + * `log.retention.bytes` and `log.retention.ms/hours`, -- the log segment will be cleared if it exceeds the limits. + + * `log.segment.bytes` and `log.segment.ms`, -- a new log segment will be created if any of the limits is reached. + + * Threads for recovery + + * `num.recovery.threads.per.data.dir` (default 1), could be a larger number to speed up opening/closing log segments, recovering from failure, etc. + + * Maximum message size supported + + * `message.max.bytes` (default 1000012). + + * `replica.fetch.max.bytes` MUST be larger than `message.max.bytes`. + + * MUST be coordinated with (lower than) the `fetch.message.max.bytes` configuration of consumer clients. + + * MUST be coordinated (same) with the `message.max.bytes` configuration of producer clients. + +# Performance tips + +* Factors: Memory, Disk, Partitions, and Ethernet bandwidth. + +* Partitions could be used to improve throughput, by using multiple Producers/Consumers. + +* Suggests that limiting the size of the partition on the disk to less than 6 GB per day of retention often gives satisfactory results. + diff --git a/doc/KafkaClientConfiguration.md b/doc/KafkaClientConfiguration.md new file mode 100644 index 000000000..846831536 --- /dev/null +++ b/doc/KafkaClientConfiguration.md @@ -0,0 +1,51 @@ +# Kafka Client Configuration + + +## KafkaConsumer Configuration + +Item | Description | Options | Default value +----------------------------|--------------------------------------------------------------------------------------|---------------------|---------------- +BOOTSTRAP_SERVERS | List of host:port pairs, comma(,) seperated string. E.g, "host1:port1,host2:port2" | | Mandatory (with no default) +GROUP_ID | Consumer's group ID string | | Randomly generated string +CLIENT_ID | Kafka Consumer's ID string | | Randomly generated string +AUTO_OFFSET_RESET | Where it starts to read while doesn't have a valid committed offset | latest, earliest | latest +MAX_POLL_RECORDS | The maximum number of records that a single call to poll() will return | Integer[1, ...] | 500 +ENABLE_PARTITION_EOF | Emit EOF event whenever the consumer reaches the end of a partition | true, false | false +QUEUED_MIN_MESSAGES | Minimum number of messages per topic/partition tries to maintain in the local consumer queue;
A Larger value means more frequently to send FetchRequest toward brokers | Integer[1, 10000000] | 100000 +SESSION_TIMEOUT_MS | Client group session and failure detection timeout;
If no heartbeat received by the broker, the consumer would be removed from the consumer group | Integer[1, 3600000] | 10000 +SOCKET_TIMEOUT_MS | Timeout for network requests (i.e, OffsetCommitRequest) | Integer[10, 300000] | 60000 +SECURITY_PROTOCOL | Protocol used to communicate with brokers | plaintext, ssl, sasl_palintest, sasl_ssl | plaintext +SASL_KERBEROS_KINIT_CMD | Shell command to refresh or acquire the client's Kerberos ticket | | +SASL_KERBEROS_SERVICE_NAME | The client's Kerberos principal name | | + + +## KafkaProducer Configuration + +Item | Description | Options | Default value +--------------------------------|------------------------------------------------------------------------------------------------------------|---------------------------|---------------- +BOOTSTRAP_SERVERS | List of host:port pairs, comma(,) seperated string. E.g, "host1:port1,host2:port2" | | Mandatory (with no default) +CLIENT_ID | Kafka Producer's ID string | | Randomly generated string +ACKS | How many partition replicas must receive the record before the producer can consider the write successful | 0, 1, all(-1) | all +QUEUE_BUFFERING_MAX_MESSAGES | Maximum number of messages allowed on the producer queue | Integer[1, 10000000] | 100000 +QUEUE_BUFFERING_MAX_KBYTES | Maximum total message size sum allowed on the producer queue | Integer[1, INT_MAX] | 0x100000 (1GB) +LINGER_MS | Delay in milliseconds to wait before constructing messages batches to transmit to brokers | Double[0, 900000.0] | 0 (KafkaSyncProducer);
0.5 (KafkaAsyncProducer) +BATCH_NUM_MESSAGES | Maximum number of messages batched in one messageSet | Integer[1, 1000000] | 10000 +MESSAGE_MAX_BYTES | Maximum Kafka protocol request message (or batch) size | Integer[1000, 1000000000] | 1000000 +MESSAGE_TIMEOUT_MS | This value is enforced locally and limits the time a produced message waits for successful delivery | Integer[0, INT32_MAX] | 300000 +REQUEST_TIMEOUT_MS | This value is only enforced by the brokers and relies on `ACKS` being non-zero;
It indicates how long the leading broker should wait for in-sync replicas to acknowledge the message. | Integer[1, 900000] | 5000 +PARTITIONER | The default partitioner for a ProducerRecord (with no partition assigned).
Note: partitioners with postfix `_random` mean `ProducerRecord`s with empty key are randomly partitioned. | random,
consistent, consistent_random,
murmur2, murmur2_random,
fnv1a, fnv1a_random | murmur2_random +MAX_IN_FLIGHT | Maximum number of in-flight requests per broker connection | Integer[1, 1000000] | 1000000 (while `enable.idempotence`=false);
5 (while `enable.idempotence`=true) +ENABLE_IDEMPOTENCE | This feature ensures that messages are successfully sent exactly once and in the original order | true, false | false +SECURITY_PROTOCOL | Protocol used to communicate with brokers | plaintext, ssl, sasl_palintest, sasl_ssl | plaintext +SASL_KERBEROS_KINIT_CMD | Shell command to refresh or acquire the client's Kerberos ticket | | +SASL_KERBEROS_SERVICE_NAME | The client's Kerberos principal name | | + + +## References + +* [librdkafka configuration](https://docs.confluent.io/current/clients/librdkafka/md_CONFIGURATION.html) + +* [Java's ConsumerConfig](https://kafka.apache.org/10/javadoc/org/apache/kafka/clients/consumer/ConsumerConfig.html) + +* [Java's ProducerConfig](https://kafka.apache.org/10/javadoc/org/apache/kafka/clients/producer/ProducerConfig.html) + diff --git a/doc/KafkaConsumerQuickStart.md b/doc/KafkaConsumerQuickStart.md new file mode 100644 index 000000000..2f7ac36da --- /dev/null +++ b/doc/KafkaConsumerQuickStart.md @@ -0,0 +1,154 @@ +# KafkaConsumer Quick Start + +Generally speaking, The `Modern C++ based Kafka API` is quite similar with [Kafka Java's API](https://kafka.apache.org/22/javadoc/org/apache/kafka/clients/consumer/KafkaConsumer.html) + +We'd recommend users to cross-reference them, --especially the examples. + +Unlike Java's KafkaConsumer, here we introduced two derived classes, --KafkaAutoCommitConsumer and KafkaManualCommitConsumer, --depending on whether users should call `commit` manually. + +## KafkaAutoCommitConsumer + +* Friendly for users, --would not care about when to commit the offsets for these received messages. + +* Internally, it would commit the offsets (for received records) within the next `poll` and the final `close`. +Note, each internal `commit` would "try its best", but "not guaranteed to succeed", -- it's supposed to be called periodically, thus occasional failure doesn't matter. + +### Example +```cpp + ConsumerConfig props; + props.put(ConsumerConfig::BOOTSTRAP_SERVERS, "127.0.0.1:1234,127.0.0.1:2345"); + + KafkaAutoCommitConsumer consumer(props); + + consumer.subscribe({"topic1", "topic2"}); + + while (true) { + auto records = consumer.poll(1000); + for (auto& record: records) { + // Process the message... + process(record); + } + } +``` + +* `ConsumerConfig::BOOTSTRAP_SERVERS` is mandatory for `ConsumerConfig`. + +* `subscribe` could take a topic list. And it's a blocking operation, -- would return after the rebalance event triggered callback was executed. + +* `poll` must be periodically called, and it would trigger kinds of callback handling internally. As in this example, just put it in a "while loop" would be OK. + +* At the end, the user could `close` the consumer manually, or just leave it to the destructor (which would `close` anyway). + +## KafkaManualCommitConsumer + +* Users must commit the offsets for received records manually. + +### Example +```cpp + ConsumerConfig props; + props.put(ConsumerConfig::BOOTSTRAP_SERVERS, "127.0.0.1:1234,127.0.0.1:2345"); + + KafkaManualCommitConsumer consumer(props); + + consumer.subscribe({"topic1", "topic2"}); + + while (true) { + auto records = consumer.poll(1000); + for (auto& record: records) { + // Process the message... + process(record); + + // Then, must commit the offset manually + consumer.commitSync(*record); + } + } +``` + +* The example is quite similar with the KafkaAutoCommitConsumer, with only 1 more line added for manual-commit. + +* `commitSync` and `commitAsync` are both available for a KafkaManualConsumer. Normally, use `commitSync` to guarantee the commitment, or use `commitAsync`(with `OffsetCommitCallback`) to get a better performance. + +## KafkaManualCommitConsumer with `KafkaClient::EventsPollingOption::MANUAL` + +While we construct a `KafkaManualCommitConsumer` with option `KafkaClient::EventsPollingOption::AUTO` (default), an internal thread would be created for `OffsetCommit` callbacks handling. + +This might not be what you want, since then you have to use 2 different threads to process the messages and handle the `OffsetCommit` responses. + +Here we have another choice, -- using `KafkaClient::EventsPollingOption::MANUAL`, thus the `OffsetCommit` callbacks would be called within member function `pollEvents()`. + +### Example +```cpp + ConsumerConfig props; + props.put(ConsumerConfig::BOOTSTRAP_SERVERS, "127.0.0.1:1234,127.0.0.1:2345"); + + KafkaManualCommitConsumer consumer(props, KafkaClient::EventsPollingOption::MANUAL); + + consumer.subscribe({"topic1", "topic2"}); + + while (true) { + auto records = consumer.poll(1000); + for (auto& record: records) { + // Process the message... + process(record); + + // Here we commit the offset manually + consumer.commitSync(*record); + } + + // Here we call the `OffsetCommit` callbacks + // Note, we can only do this while the consumer was constructed with `EventsPollingOption::MANUAL`. + consumer.pollEvents(); + } +``` + +## Error handling + +No exception would be thrown by `KafkaProducer::poll()`. + +Once an error occurs, the `ErrorCode` would be embedded in the `Consumer::ConsumerRecord`. + +There're 2 cases, + +1. Success + + - RD_KAFKA_RESP_ERR__NO_ERROR (0), -- got a message successfully + + - RD_KAFKA_RESP_ERR__PARTITION_EOF, -- reached the end of a partition (no message got) + +2. Failure + + - [Error Codes](https://cwiki.apache.org/confluence/display/KAFKA/A+Guide+To+The+Kafka+Protocol#AGuideToTheKafkaProtocol-ErrorCodes) + +## Frequently Asked Questions + +* What're the available configurations? + + - [KafkaProducerConfiguration](KafkaClientConfiguration.md#kafkaconsumer-configuration) + + - [Inline doxygen page](../doxygen/classKAFKA__CPP__APIS__NAMESPACE_1_1ConsumerConfig.html) + +* How to enhance the polling performance? + + `ConsumerConfig::QUEUED_MIN_MESSAGES` determines how frequently the consumer would send the FetchRequest towards brokers. + The default configuration (i.e, 100000) might not be good enough for small (less than 1KB) messages, and suggest using a larger value (e.g, 1000000) for it. + +* How many threads would be created by a KafkaConsumer? + + Excluding the user's main thread, `KafkaAutoCommitConsumer` would start another (N + 2) threads in the background, while `KafkaManualConsumer` would start (N + 3) background threads. (N means the number of BOOTSTRAP_SERVERS) + + 1. Each broker (in the list of BOOTSTRAP_SERVERS) would take a seperate thread to transmit messages towards a kafka cluster server. + + 2. Another 3 threads will handle internal operations, consumer group operations, and kinds of timers, etc. + + 3. KafkaManualConsumer has one more thread, which keeps polling the offset-commit callback event. + + E.g, if a KafkaAutoCommitConsumer was created with property of `BOOTSTRAP_SERVERS=127.0.0.1:8888,127.0.0.1:8889,127.0.0.1:8890`, it would take 6 threads in total (including the main thread). + +* Which one of these threads will handle the callbacks? + + There are 2 kinds of callbacks for a KafkaConsumer, + + 1. `RebalanceCallback` will be triggered internally by the user's thread, -- within the `poll` function. + + 2. `OffsetCommitCallback` (only available for `KafkaManualCommitConsumer`) will be triggered by a background thread, not by the user's thread. + diff --git a/doc/KafkaProducerQuickStart.md b/doc/KafkaProducerQuickStart.md new file mode 100644 index 000000000..433348e3d --- /dev/null +++ b/doc/KafkaProducerQuickStart.md @@ -0,0 +1,251 @@ +# KafkaProducer Quick Start + +Generally speaking, The `Modern C++ based Kafka API` is quite similar to the [Kafka Java's API](https://kafka.apache.org/10/javadoc/org/apache/kafka/clients/producer/KafkaProducer.html). + +We'd recommend users to cross-reference them, --especially the examples. + +Unlike Java's KafkaProducer, here we introduce two derived classes, -- `KafkaSyncProducer` and `KafkaAsyncProducer` --depending on different `send` behaviors (synchronous/asynchronous). + +## KafkaSyncProducer + +* The "Sync" (in the name) means `send` is a blocking operation, and it will immediately get the RecordMetadata while the function returns. If anything wrong occurs, an exception would be thrown. + +### Example +```cpp + ProducerConfig props; + props.put(ConsumerConfig::BOOTSTRAP_SERVERS, "127.0.0.1:1234,127.0.0.1:2345")); + + KafkaSyncProducer producer(props); + + // Prepare "msgsToBeSent", and an empty "msgsFailedToBeSent" as well + + for (const auto& msg : msgsToBeSent) { + auto record = ProducerRecord(topic, partition, msg.key, msg.value); + try { + producer.send(record); + } catch (const KafkaException& e) { + LOG_ERROR("Cannot send out message with err={0}", e.what()); + msgsFailedToBeSent.emplace(msg); // Push it back to another list to handle with them later + } + } + + producer.close(); // Not mandatory (destructor of the producer would call it anyway) +``` + +* `ProducerConfig::BOOTSTRAP_SERVERS` is mandatory for ProducerConfig. + +* `ProducerRecord` would not take any ownership for the `key` or `value`. Thus, the user must guarantee the memory block (pointed by `key` or `value`) is valid until being `send`. + +* Since `send` is a blocking operation, the throughput will be highly impacted, but it is easier to make sure of the message delivery and logically it is simpler. + +* At the end, the user could call `close` manually, or just leave it to the destructor (`close` would be called anyway). + +## KafkaAsyncProducer + +* The `Async` (in the name) means `send` is an unblocking operation, and the result (including errors) could only be got from the delivery callback. + +### Example +```cpp + ProducerConfig props; + props.put(ConsumerConfig::BOOTSTRAP_SERVERS, "127.0.0.1:1234,127.0.0.1:2345"); + + KafkaAsyncProducer producer(props); + + // Prepare "msgsToBeSent", and an empty "msgsFailedToBeSent" as well + + for (const auto& msg : msgsToBeSent) { + auto record = ProducerRecord(topic, partition, msg.key, msg.value); + producer.send(record, + // Ack callback + [&msg](const Producer::RecordMetadata& metadata, std::error_code ec) { + // the message could be identified by `metadata.recordId()` + if (ec) { + LOG_ERROR("Cannot send out message with recordId={0}", metadata.recordId()); + msgsFailedToBeSent.emplace(msg); // Push it back to another list to handle with them later + } + }); + } + + producer.close(); // Not mandatory (destructor of the producer would call it anyway) +``` + +* Same with KafkaSyncProducer, the user must guarantee the memory block for `ProducerRecord`'s `key` is valid until being `send`. + +* By default, the memory block for `ProducerRecord`'s `value` must be valid until the delivery callback is called; Otherwise, the `send` should be with option `KafkaProducer::SendOption::ToCopyRecordValue`. + +* It's guaranteed that the delivery callback would be triggered anyway after `send`, -- a producer would even be waiting for it before `close`. So, it's a good way to release these memory resources in the `Producer::Callback` function. + +## KafkaAsyncProducer with `KafkaClient::EventsPollingOption::MANUAL` + +While we construct a `KafkaAsyncProducer` with option `KafkaClient::EventsPollingOption::AUTO` (default), an internal thread would be created for `MessageDelivery` callbacks handling. + +This might not be what you want, since then you have to use 2 different threads to send the messages and handle the `MessageDelivery` responses. + +Here we have another choice, -- using `KafkaClient::EventsPollingOption::MANUAL`, thus the `MessageDelivery` callbacks would be called within member function `pollEvents()`. + +* Note, if you constructed the `KafkaAsyncProducer` with `EventsPollingOption::MANUAL`, the `send()` would be an `unblocked` operation. +I.e, once the `message buffering queue` becomes full, the `send()` operation would throw an exception (or return an `error code` with the input reference parameter), -- instead of blocking there. +This makes sense, since you might want to call `pollEvents()` later, thus delivery-callback could be called for some messages (which could then be removed from the `message buffering queue`). + +### Example +```cpp + ProducerConfig props; + props.put(ConsumerConfig::BOOTSTRAP_SERVERS, "127.0.0.1:1234,127.0.0.1:2345"); + + KafkaAsyncProducer producer(props, KafkaClient::EventsPollingOption::MANUAL); + + // Prepare "msgsToBeSent" + auto std::map> msgsToBeSent = ...; + + for (const auto& msg : msgsToBeSent) { + auto record = ProducerRecord(topic, partition, msg.second.first, msg.second.second, msg.first); + std::error_code error; + producer.send(error, + record, + // Ack callback + [&msg](const Producer::RecordMetadata& metadata, std::error_code ec) { + // the message could be identified by `metadata.recordId()` + if (ec) { + LOG_ERROR("Cannot send out message with recordId={0}", metadata.recordId()); + } else { + msgsToBeSend.erase(metadata.recordId()); // Quite safe here + } + }); + if (error) break; + } + + // Here we call the `MessageDelivery` callbacks + // Note, we can only do this while the producer was constructed with `EventsPollingOption::MANUAL`. + producer.pollEvents(); + + producer.close(); +``` + +## Idempotent Producer + +The way to make a `KafkaProducer` be `Idempotent` is really simple, just adding one single line of configuration -- `{ProducerConfig::ENABLE_IDEMPOTENCE, "true"}` would be enough. + +Note: The `ProducerConfig::ENABLE_IDEMPOTENCE` configuration would internally set some default values for related properties, such as `{ProducerConfig::ACKS, "all"}`, `{ProducerConfig::MAX_IN_FLIGHT, "5"}`, etc. Thus suggest not to set them explicitly to avoid configuration conflict. + +## Headers in ProducerRecord + +* A `ProducerRecord` could take extra information with `headers`. + + * Note, the `header` within `headers` contains the pointer of the memory block for its `value`. The memory block MUST be valid until the `ProducerRecord` is read by `producer.send()`. + +### Example +```cpp + KafkaAsyncProducer producer(props); + + auto record = ProducerRecord(topic, partition, Key(), Value()); + + for (const auto& msg : msgsToBeSent) { + // Prepare record headers + std::string session = msg.session; + std::uint32_t seqno = msg.seqno; + record.headers() = { + { "session", { session.c_str(), session.size()} }, + { "seqno", { &seqno, sizeof(seqno)} } + }; + + record.setKey(msg.key); + record.setValue(msg.value); + + producer.send(record, + // Ack callback + [&msg](const Producer::RecordMetadata& metadata, std::error_code ec) { + if (ec) { + LOG_ERROR("Cannot send out message: {0}, err: {1}", metadata.toString(), ec); + } + }); + } +``` + +## Error handling + +Once an error occurs during `send()`, `KafkaSyncProducer` and `KafkaAsyncProducer` behave differently. + +1. `KafkaSyncProducer` gets `std::error_code` by catching exceptions (with `error()` member function). + +2. `KafkaAsyncProducer` gets `std::error_code` with delivery-callback (with a parameter of the callback function). + +There are 2 kinds of possible errors, + +1. Local errors, + + - RD_KAFKA_RESP_ERR__UNKNOWN_TOPIC -- The topic doesn't exist + + - RD_KAFKA_RESP_ERR__UNKNOWN_PARTITION -- The partition doesn't exist + + - RD_KAFKA_RESP_ERR__INVALID_ARG -- Invalid topic (topic is null or the length is too long (>512)) + + - RD_KAFKA_RESP_ERR__MSG_TIMED_OUT -- No ack received within the time limit + +2. Broker errors, + + - [Error Codes](https://cwiki.apache.org/confluence/display/KAFKA/A+Guide+To+The+Kafka+Protocol#AGuideToTheKafkaProtocol-ErrorCodes) + +## Frequently Asked Questions + +### What are the available configurations? + +- [KafkaProducerConfiguration](KafkaClientConfiguration.md#kafkaproducer-configuration) + +- [Inline doxygen page](../doxygen/classKAFKA__CPP__APIS__NAMESPACE_1_1ProducerConfig.html) + +### About the automatic `topic creation` + +If the cluster's configuration is with `auto.create.topics.enable=true`, the producer/consumer could trigger the brokers to create a new topic (with `send`, `subscribe`, etc) + +Note, the default created topic may be not what you want (e.g, with `default.replication.factor=1` configuration as default, etc), thus causing other unexpected problems. + +### What will happen after `ack` timeout? + +If an ack failed to be received within `MESSAGE_TIMEOUT_MS`, an exception would be thrown for a KafkaSyncSend, or, an error code would be received by the delivery callback for a KafkaAsyncProducer. + +### How to enhance the sending performance? + +Enlarging the default `BATCH_NUM_MESSAGES` and `LINGER_MS` might improve message batching, thus enhancing the throughput. + +While, on the other hand, `LINGER_MS` would highly impact the latency. + +The `QUEUE_BUFFERING_MAX_MESSAGES` and `QUEUE_BUFFERING_MAX_KBYTES` would determine the `max in flight requests (some materials about Kafka would call it in this way)`. If the queue buffer is full, the `send` operation would be blocked. + +Larger `QUEUE_BUFFERING_MAX_MESSAGES`/`QUEUE_BUFFERING_MAX_KBYTES` might help to improve throughput as well, while also means more messages locally buffering. + +### How to achieve reliable delivery + +* Quick Answer, + + 1. The Kafka cluster should be configured with `min.insync.replicas = 2` at least + + 2. Use a `KafkaSyncProducer` (with configuration `{ProducerConfig::ACKS, "all"}`); or use a `KafkaAsyncProducer` (with configuration `{ProducerConfig::ENABLE_IDEMPOTENCE, "true"}`), together with proper error-handling within the delivery callbacks. + +* Complete Answer, + + * [How to Make KafkaProducer Reliable](HowToMakeKafkaProducerReliable.md) + +### How many threads would be created by a KafkaProducer? + +Excluding the user's main thread, KafkaSyncProducer would start another (N + 2) threads in the background, while `KafkaAsyncProducer` would start (N + 3) background threads. (N means the number of BOOTSTRAP_SERVERS) + +Most of these background threads are started internally by librdkafka. + +Here is a brief introduction what they're used for, + +1. Each broker (in the list of BOOTSTRAP_SERVERS) would take a separate thread to transmit messages towards a kafka cluster server. + +2. Another 2 background threads would handle internal operations and kinds of timers, etc. + +3. `KafkaAsyncProducer` has one more background thread to keep polling the delivery callback event. + +E.g, if a KafkaSyncProducer was created with property of `BOOTSTRAP_SERVERS=127.0.0.1:8888,127.0.0.1:8889,127.0.0.1:8890`, it would take 6 threads in total (including the main thread). + +### Which one of these threads will handle the callbacks + +The `Producer::Callback` is only available for a `KafkaAsyncProducer`. + +It will be handled by a background thread, not by the user's thread. + +Note, should be careful if both the `KafkaAsyncProducer::send()` and the `Producer::Callback` might access the same container at the same time. + diff --git a/include/CMakeLists.txt b/include/CMakeLists.txt new file mode 100644 index 000000000..7c1a1b4dd --- /dev/null +++ b/include/CMakeLists.txt @@ -0,0 +1,8 @@ +project(modern-cpp-kafka-api) + +add_library(${PROJECT_NAME} INTERFACE) + +target_include_directories(${PROJECT_NAME} INTERFACE "./") +target_link_libraries(${PROJECT_NAME} INTERFACE "rdkafka") + +install(DIRECTORY kafka DESTINATION include) diff --git a/include/kafka/AdminClient.h b/include/kafka/AdminClient.h new file mode 100644 index 000000000..1c268cfd8 --- /dev/null +++ b/include/kafka/AdminClient.h @@ -0,0 +1,291 @@ +#pragma once + +#include "kafka/Project.h" + +#include "kafka/AdminClientConfig.h" +#include "kafka/Error.h" +#include "kafka/KafkaClient.h" +#include "kafka/RdKafkaHelper.h" + +#include "librdkafka/rdkafka.h" + +#include +#include +#include +#include +#include + + +namespace KAFKA_API { + +namespace Admin +{ +/** + * The result of AdminClient::createTopics(). + */ +using CreateTopicsResult = ErrorWithDetail; + +/** + * The result of AdminClient::deleteTopics(). + */ +using DeleteTopicsResult = ErrorWithDetail; + +/** + * The result of AdminClient::listTopics(). + */ +struct ListTopicsResult: public ErrorWithDetail +{ + ListTopicsResult(rd_kafka_resp_err_t respErr, std::string detailedMsg): ErrorWithDetail(respErr, std::move(detailedMsg)) {} + explicit ListTopicsResult(Topics names): ErrorWithDetail(RD_KAFKA_RESP_ERR_NO_ERROR, "Success"), topics(std::move(names)) {} + + /** + * The topics fetched. + */ + Topics topics; +}; + +} // end of Admin + + +/** + * The administrative client for Kafka, which supports managing and inspecting topics, etc. + */ +class AdminClient: public KafkaClient +{ +public: + explicit AdminClient(const Properties& properties) + : KafkaClient(ClientType::AdminClient, KafkaClient::validateAndReformProperties(properties)) + { + } + + /** + * Create a batch of new topics. + */ + Admin::CreateTopicsResult createTopics(const Topics& topics, int numPartitions, int replicationFactor, + const Properties& topicConfig = Properties(), + std::chrono::milliseconds timeout = std::chrono::milliseconds(DEFAULT_COMMAND_TIMEOUT_MS)); + /** + * Delete a batch of topics. + */ + Admin::DeleteTopicsResult deleteTopics(const Topics& topics, + std::chrono::milliseconds timeout = std::chrono::milliseconds(DEFAULT_COMMAND_TIMEOUT_MS)); + /** + * List the topics available in the cluster. + */ + Admin::ListTopicsResult listTopics(std::chrono::milliseconds timeout = std::chrono::milliseconds(DEFAULT_COMMAND_TIMEOUT_MS)); + +private: + std::list getPerTopicResults(const rd_kafka_topic_result_t** topicResults, int topicCount) const; + ErrorWithDetail combineErrors(const std::list& errors) const; + + static constexpr int DEFAULT_COMMAND_TIMEOUT_MS = 30000; + static constexpr int EVENT_POLLING_INTERVAL_MS = 100; +}; + + +inline std::list +AdminClient::getPerTopicResults(const rd_kafka_topic_result_t** topicResults, int topicCount) const +{ + std::list errors; + + for (int i = 0; i < topicCount; ++i) + { + const rd_kafka_topic_result_t* topicResult = topicResults[i]; + if (rd_kafka_resp_err_t topicError = rd_kafka_topic_result_error(topicResult)) + { + std::string detailedMsg = "topic[" + std::string(rd_kafka_topic_result_name(topicResult)) + "] with error[" + rd_kafka_topic_result_error_string(topicResult) + "]"; + errors.emplace_back(topicError, detailedMsg); + } + } + return errors; +} + +inline ErrorWithDetail +AdminClient::combineErrors(const std::list& errors) const +{ + if (!errors.empty()) + { + std::string detailedMsg; + std::for_each(errors.cbegin(), errors.cend(), + [&detailedMsg](const auto& error) { + if (!detailedMsg.empty()) detailedMsg += "; "; + + detailedMsg += error.detail; + }); + + return ErrorWithDetail(errors.front().error, detailedMsg); + } + + return ErrorWithDetail(RD_KAFKA_RESP_ERR_NO_ERROR, "Success"); +} + +inline Admin::CreateTopicsResult +AdminClient::createTopics(const Topics& topics, int numPartitions, int replicationFactor, + const Properties& topicConfig, + std::chrono::milliseconds timeout) +{ + LogBuffer<500> errInfo; + + std::vector rkNewTopics; + + for (const auto& topic: topics) + { + rkNewTopics.emplace_back(rd_kafka_NewTopic_new(topic.c_str(), numPartitions, replicationFactor, errInfo.str(), errInfo.capacity())); + if (!rkNewTopics.back()) + { + return Admin::CreateTopicsResult(RD_KAFKA_RESP_ERR__INVALID_ARG, rd_kafka_err2str(RD_KAFKA_RESP_ERR__INVALID_ARG)); + } + + for (const auto& conf: topicConfig.map()) + { + rd_kafka_resp_err_t err = rd_kafka_NewTopic_set_config(rkNewTopics.back().get(), conf.first.c_str(), conf.second.c_str()); + if (err != RD_KAFKA_RESP_ERR_NO_ERROR) + { + std::string errMsg = "Invalid config[" + conf.first + "=" + conf.second + "]"; + KAFKA_API_DO_LOG(LOG_ERR, errMsg.c_str()); + return Admin::CreateTopicsResult(RD_KAFKA_RESP_ERR__INVALID_ARG, errMsg); + } + } + } + + std::vector rk_topics; + rk_topics.reserve(rkNewTopics.size()); + for (const auto& topic : rkNewTopics) { rk_topics.emplace_back(topic.get()); } + + auto rk_queue = rd_kafka_queue_unique_ptr(rd_kafka_queue_new(getClientHandle())); + + rd_kafka_CreateTopics(getClientHandle(), rk_topics.data(), rk_topics.size(), nullptr, rk_queue.get()); + + auto rk_ev = rd_kafka_event_unique_ptr(); + + const auto end = std::chrono::steady_clock::now() + timeout; + do + { + rk_ev.reset(rd_kafka_queue_poll(rk_queue.get(), EVENT_POLLING_INTERVAL_MS)); + + if (rd_kafka_event_type(rk_ev.get()) == RD_KAFKA_EVENT_CREATETOPICS_RESULT) break; + + if (rk_ev) + { + KAFKA_API_DO_LOG(LOG_INFO, "rd_kafka_queue_poll got event[%s], with error[%s]", rd_kafka_event_name(rk_ev.get()), rd_kafka_event_error_string(rk_ev.get())); + rk_ev.reset(); + } + } while (std::chrono::steady_clock::now() < end); + + if (!rk_ev) + { + return Admin::CreateTopicsResult(RD_KAFKA_RESP_ERR__TIMED_OUT, "No response within the time limit"); + } + + std::list errors; + + if (rd_kafka_resp_err_t respErr = rd_kafka_event_error(rk_ev.get())) + { + errors.emplace_back(respErr, rd_kafka_event_error_string(rk_ev.get())); + } + + // Fetch per-topic results + const rd_kafka_CreateTopics_result_t* res = rd_kafka_event_CreateTopics_result(rk_ev.get()); + std::size_t res_topic_cnt; + const rd_kafka_topic_result_t** res_topics = rd_kafka_CreateTopics_result_topics(res, &res_topic_cnt); + + errors.splice(errors.end(), getPerTopicResults(res_topics, res_topic_cnt)); + + // Return the error if any + if (!errors.empty()) + { + return combineErrors(errors); + } + + // Update metedata + do + { + auto listResult = listTopics(); + if (!listResult.error) + { + return Admin::CreateTopicsResult(RD_KAFKA_RESP_ERR_NO_ERROR, "Success"); + } + } while (std::chrono::steady_clock::now() < end); + + return Admin::CreateTopicsResult(RD_KAFKA_RESP_ERR__TIMED_OUT, "Updating metadata timed out"); +} + +inline Admin::DeleteTopicsResult +AdminClient::deleteTopics(const Topics& topics, std::chrono::milliseconds timeout) +{ + std::vector rkDeleteTopics; + + for (const auto& topic: topics) + { + rkDeleteTopics.emplace_back(rd_kafka_DeleteTopic_new(topic.c_str())); + assert(rkDeleteTopics.back()); + } + + std::vector rk_topics; + rk_topics.reserve(rkDeleteTopics.size()); + for (const auto& topic : rkDeleteTopics) { rk_topics.emplace_back(topic.get()); } + + auto rk_queue = rd_kafka_queue_unique_ptr(rd_kafka_queue_new(getClientHandle())); + + rd_kafka_DeleteTopics(getClientHandle(), rk_topics.data(), rk_topics.size(), nullptr, rk_queue.get()); + + auto rk_ev = rd_kafka_event_unique_ptr(); + + const auto end = std::chrono::steady_clock::now() + timeout; + do + { + rk_ev.reset(rd_kafka_queue_poll(rk_queue.get(), EVENT_POLLING_INTERVAL_MS)); + + if (rd_kafka_event_type(rk_ev.get()) == RD_KAFKA_EVENT_DELETETOPICS_RESULT) break; + + if (rk_ev) + { + KAFKA_API_DO_LOG(LOG_INFO, "rd_kafka_queue_poll got event[%s], with error[%s]", rd_kafka_event_name(rk_ev.get()), rd_kafka_event_error_string(rk_ev.get())); + rk_ev.reset(); + } + } while (std::chrono::steady_clock::now() < end); + + if (!rk_ev) + { + return Admin::DeleteTopicsResult(RD_KAFKA_RESP_ERR__TIMED_OUT, "No response within the time limit"); + } + + std::list errors; + + if (rd_kafka_resp_err_t respErr = rd_kafka_event_error(rk_ev.get())) + { + errors.emplace_back(respErr, rd_kafka_event_error_string(rk_ev.get())); + } + + // Fetch per-topic results + const rd_kafka_DeleteTopics_result_t* res = rd_kafka_event_DeleteTopics_result(rk_ev.get()); + std::size_t res_topic_cnt; + const rd_kafka_topic_result_t** res_topics = rd_kafka_DeleteTopics_result_topics(res, &res_topic_cnt); + + errors.splice(errors.end(), getPerTopicResults(res_topics, res_topic_cnt)); + + return combineErrors(errors); +} + +inline Admin::ListTopicsResult +AdminClient::listTopics(std::chrono::milliseconds timeout) +{ + const rd_kafka_metadata_t* rk_metadata = nullptr; + rd_kafka_resp_err_t err = rd_kafka_metadata(getClientHandle(), true, nullptr, &rk_metadata, convertMsDurationToInt(timeout)); + auto guard = rd_kafka_metadata_unique_ptr(rk_metadata); + + if (err != RD_KAFKA_RESP_ERR_NO_ERROR) + { + return Admin::ListTopicsResult(err, rd_kafka_err2str(err)); + } + + Topics names; + for (int i = 0; i < rk_metadata->topic_cnt; ++i) + { + names.insert(rk_metadata->topics[i].topic); + } + return Admin::ListTopicsResult(names); +} + +} // end of KAFKA_API + diff --git a/include/kafka/AdminClientConfig.h b/include/kafka/AdminClientConfig.h new file mode 100644 index 000000000..b4ca23b8a --- /dev/null +++ b/include/kafka/AdminClientConfig.h @@ -0,0 +1,44 @@ +#pragma once + +#include "kafka/Project.h" + +#include "kafka/Properties.h" + + +namespace KAFKA_API { + +/** + * Configuration for the Kafka Consumer. + */ +class AdminClientConfig: public Properties +{ +public: + AdminClientConfig() = default; + AdminClientConfig(const AdminClientConfig&) = default; + explicit AdminClientConfig(const PropertiesMap& kvMap): Properties(kvMap) {} + + /** + * The string contains host:port pairs of brokers (splitted by ",") that the administrative client will use to establish initial connection to the Kafka cluster. + * Note: It's mandatory. + */ + static const constexpr char* BOOTSTRAP_SERVERS = "bootstrap.servers"; + + /** + * Protocol used to communicate with brokers. + * Default value: plaintext + */ + static const constexpr char* SECURITY_PROTOCOL = "security.protocol"; + + /** + * Shell command to refresh or acquire the client's Kerberos ticket. + */ + static const constexpr char* SASL_KERBEROS_KINIT_CMD = "sasl.kerberos.kinit.cmd"; + + /** + * The client's Kerberos principal name. + */ + static const constexpr char* SASL_KERBEROS_SERVICE_NAME = "sasl.kerberos.service.name"; +}; + +} + diff --git a/include/kafka/BrokerMetadata.h b/include/kafka/BrokerMetadata.h new file mode 100644 index 000000000..b414a9387 --- /dev/null +++ b/include/kafka/BrokerMetadata.h @@ -0,0 +1,154 @@ +#pragma once + +#include "kafka/Project.h" + +#include "kafka/KafkaException.h" +#include "kafka/Types.h" + +#include "librdkafka/rdkafka.h" + +#include + + +namespace KAFKA_API { + +/** + * The metadata info for a topic. + */ +struct BrokerMetadata { + /** + * Information for a Kafka node. + */ + struct Node + { + public: + using Id = int; + using Host = std::string; + using Port = int; + + Node(Id i, Host h, Port p): id(i), host(std::move(h)), port(p) {} + + /** + * The node id. + */ + Node::Id id; + + /** + * The host name. + */ + Node::Host host; + + /** + * The port. + */ + Node::Port port; + + /** + * Obtains explanatory string. + */ + std::string toString() const { return host + ":" + std::to_string(port) + "/" + std::to_string(id); } + }; + + /** + * It is used to describe per-partition state in the MetadataResponse. + */ + struct PartitionInfo + { + void setLeader(const std::shared_ptr& ldr) { leader = ldr; } + void addReplica(const std::shared_ptr& replica) { replicas.emplace_back(replica); } + void addInSyncReplica(const std::shared_ptr& replica) { inSyncReplicas.emplace_back(replica); } + + /** + * The node currently acting as a leader for this partition or null if there is no leader. + */ + std::shared_ptr leader; + + /** + * The complete set of replicas for this partition regardless of whether they are alive or up-to-date. + */ + std::vector> replicas; + + /** + * The subset of the replicas that are in sync, that is caught-up to the leader and ready to take over as leader if the leader should fail. + */ + std::vector> inSyncReplicas; + + /** + * Obtains explanatory string. + */ + std::string toString() const; + }; + + /** + * The BrokerMetadata is per-topic constructed + */ + explicit BrokerMetadata(Topic topic): _topic(std::move(topic)) {} + + /** + * The topic name. + */ + std::string topic() const { return _topic; } + + /** + * The partitions' state in the MetadataResponse. + */ + const std::map& partitions() const { return _partitions; } + + /** + * Obtains explanatory string. + */ + std::string toString() const; + + void setOrigNodeName(const std::string& origNodeName) { _origNodeName = origNodeName; } + void addNode(Node::Id nodeId, const Node::Host& host, Node::Port port) { _nodes[nodeId] = std::make_shared(nodeId, host, port); } + std::shared_ptr getNode(Node::Id nodeId) { return _nodes[nodeId]; } + void addPartitionInfo(Partition partition, const PartitionInfo& partitionInfo) { _partitions.emplace(partition, partitionInfo); } + +private: + Topic _topic; + std::string _origNodeName; + std::map> _nodes; + std::map _partitions; +}; + +inline std::string +BrokerMetadata::PartitionInfo::toString() const +{ + std::ostringstream oss; + + auto streamNodes = [](std::ostringstream& ss, const std::vector>& nodes) -> std::ostringstream& { + bool isTheFirst = true; + std::for_each(nodes.cbegin(), nodes.cend(), + [&isTheFirst, &ss](const auto& node) { + ss << (isTheFirst ? (isTheFirst = false, "") : ", ") << node->toString(); + }); + return ss; + }; + + oss << "leader[" << leader->toString() << "], replicas["; + streamNodes(oss, replicas) << "], inSyncReplicas["; + streamNodes(oss, inSyncReplicas) << "]"; + + return oss.str(); +} + +inline std::string +BrokerMetadata::toString() const +{ + std::ostringstream oss; + + oss << "originatingNode[" << _origNodeName << "], topic[" << _topic << "], partitions{"; + bool isTheFirst = true; + for (const auto& partitionInfoPair: _partitions) + { + const Partition partition = partitionInfoPair.first; + const PartitionInfo& partitionInfo = partitionInfoPair.second; + oss << (isTheFirst ? (isTheFirst = false, "") : "; ") << partition << ": " << partitionInfo.toString(); + } + oss << "}"; + + return oss.str(); +} + +} + diff --git a/include/kafka/ConsumerConfig.h b/include/kafka/ConsumerConfig.h new file mode 100644 index 000000000..c91af98b1 --- /dev/null +++ b/include/kafka/ConsumerConfig.h @@ -0,0 +1,96 @@ +#pragma once + +#include "kafka/Project.h" + +#include "kafka/Properties.h" + + +namespace KAFKA_API { + +/** + * Configuration for the Kafka Consumer. + */ +class ConsumerConfig: public Properties +{ +public: + ConsumerConfig() = default; + ConsumerConfig(const ConsumerConfig&) = default; + explicit ConsumerConfig(const PropertiesMap& kvMap): Properties(kvMap) {} + + /** + * The string contains host:port pairs of brokers (splitted by ",") that the consumer will use to establish initial connection to the Kafka cluster. + * Note: It's mandatory. + */ + static const constexpr char* BOOTSTRAP_SERVERS = "bootstrap.servers"; + + /** + * Group identifier. + * Note: It's better to configure it manually, otherwise a random one would be used for it. + * + */ + static const constexpr char* GROUP_ID = "group.id"; + + /** + * Client identifier. + */ + static const constexpr char* CLIENT_ID = "client.id"; + + /** + * This property controls the behavior of the consumer when it starts reading a partition for which it doesn't have a valid committed offset. + * The "latest" means the consumer will begin reading the newest records written after the consumer started. While "earliest" means that the consumer will read from the very beginning. + * Available options: latest, earliest + * Default value: latest + */ + static const constexpr char* AUTO_OFFSET_RESET = "auto.offset.reset"; + + /** + * Emit RD_KAFKA_RESP_ERR_PARTITION_EOF event whenever the consumer reaches the end of a partition. + * Default value: false + */ + static const constexpr char* ENABLE_PARTITION_EOF = "enable.partition.eof"; + + /** + * This controls the maximum number of records that a single call to poll() will return. + * Default value: 500 + */ + static const constexpr char* MAX_POLL_RECORDS = "max.poll.records"; + + /** + * Minimum number of messages per topic/partition tries to maintain in the local consumer queue. + * Note: With a larger value configured, the consumer would send FetchRequest towards brokers more frequently. + * Defalut value: 100000 + */ + static const constexpr char* QUEUED_MIN_MESSAGES = "queued.min.messages"; + + /** + * Client group session and failure detection timeout. + * If no heartbeat received by the broker within this timeout, the broker will remove the consumer and trigger a rebalance. + * Default value: 10000 + */ + static const constexpr char* SESSION_TIMEOUT_MS = "session.timeout.ms"; + + /** + * Timeout for network requests. + * Default value: 60000 + */ + static const constexpr char* SOCKET_TIMEOUT_MS = "socket.timeout.ms"; + + /** + * Protocol used to communicate with brokers. + * Default value: plaintext + */ + static const constexpr char* SECURITY_PROTOCOL = "security.protocol"; + + /** + * Shell command to refresh or acquire the client's Kerberos ticket. + */ + static const constexpr char* SASL_KERBEROS_KINIT_CMD = "sasl.kerberos.kinit.cmd"; + + /** + * The client's Kerberos principal name. + */ + static const constexpr char* SASL_KERBEROS_SERVICE_NAME = "sasl.kerberos.service.name"; +}; + +} + diff --git a/include/kafka/ConsumerRecord.h b/include/kafka/ConsumerRecord.h new file mode 100644 index 000000000..879fa7e26 --- /dev/null +++ b/include/kafka/ConsumerRecord.h @@ -0,0 +1,154 @@ +#pragma once + +#include "kafka/Project.h" + +#include "kafka/Error.h" +#include "kafka/Header.h" +#include "kafka/Timestamp.h" +#include "kafka/Types.h" + +#include "librdkafka/rdkafka.h" + +#include + + +namespace KAFKA_API { + +/** + * A key/value pair to be received from Kafka. + * This also consists of a topic name and a partition number from which the record is being received, an offset that points to the record in a Kafka partition + */ +class ConsumerRecord +{ +public: + // ConsumerRecord will take the ownership of msg (rd_kafka_message_t*) + explicit ConsumerRecord(rd_kafka_message_t* msg): _rk_msg(msg, rd_kafka_message_destroy) {} + + /** + * The topic this record is received from. + */ + Topic topic() const { return _rk_msg->rkt ? rd_kafka_topic_name(_rk_msg->rkt): ""; } + + /** + * The partition from which this record is received. + */ + Partition partition() const { return _rk_msg->partition; } + + /** + * The position of this record in the corresponding Kafka partition. + */ + Offset offset() const { return _rk_msg->offset; } + + /** + * The key (or null if no key is specified). + */ + Key key() const { return Key(_rk_msg->key, _rk_msg->key_len); } + + /** + * The value. + */ + Value value() const { return Value(_rk_msg->payload, _rk_msg->len); } + + /** + * The timestamp of the record. + */ + Timestamp timestamp() const + { + rd_kafka_timestamp_type_t tstype; + Timestamp::Value tsValue = rd_kafka_message_timestamp(_rk_msg.get(), &tstype); + return {tsValue, tstype}; + } + + /** + * The headers of the record. + */ + Headers headers() const; + + /** + * Return just one (the very last) header's value for the given key. + */ + Header::Value lastHeaderValue(const Header::Key& key); + + /** + * The error. + * + * Possible cases: + * 1. Success + * - RD_KAFKA_RESP_ERR_NO_ERROR (0), -- got a message successfully + * - RD_KAFKA_RESP_ERR__PARTITION_EOF, -- reached the end of a partition (got no message) + * 2. Failure + * - [Error Codes] (https://cwiki.apache.org/confluence/display/KAFKA/A+Guide+To+The+Kafka+Protocol#AGuideToTheKafkaProtocol-ErrorCodes) + */ + std::error_code error() const { return ErrorCode(_rk_msg->err); } + + /** + * Obtains explanatory string. + */ + std::string toString() const; + +private: + using rd_kafka_message_unique_ptr = std::unique_ptr; + rd_kafka_message_unique_ptr _rk_msg; +}; + +inline Headers +ConsumerRecord::headers() const +{ + Headers headers; + + rd_kafka_headers_t* hdrs = nullptr; + if (rd_kafka_message_headers(_rk_msg.get(), &hdrs) != RD_KAFKA_RESP_ERR_NO_ERROR) + { + return headers; + } + + headers.reserve(rd_kafka_header_cnt(hdrs)); + + const char* name = nullptr; + const void* valuePtr = nullptr; + std::size_t valueSize = 0; + for (int i = 0; !rd_kafka_header_get_all(hdrs, i, &name, &valuePtr, &valueSize); i++) + { + headers.emplace_back(name, Header::Value(valuePtr, valueSize)); + } + + return headers; +} + +inline Header::Value +ConsumerRecord::lastHeaderValue(const Header::Key& key) +{ + rd_kafka_headers_t* hdrs = nullptr; + if (rd_kafka_message_headers(_rk_msg.get(), &hdrs) != RD_KAFKA_RESP_ERR_NO_ERROR) + { + return Header::Value(); + } + + const void* valuePtr = nullptr; + std::size_t valueSize = 0; + return (rd_kafka_header_get_last(hdrs, key.c_str(), &valuePtr, &valueSize) == RD_KAFKA_RESP_ERR_NO_ERROR) ? + Header::Value(valuePtr, valueSize) : Header::Value(); +} + +inline std::string +ConsumerRecord::toString() const +{ + std::ostringstream oss; + if (!error()) + { + oss << topic() << "-" << partition() << ":" << offset() << ", " << timestamp().toString() << ", " + << (key().size() ? (key().toString() + "/") : "") << value().toString(); + } + else if (error().value() == RD_KAFKA_RESP_ERR__PARTITION_EOF) + { + oss << "EOF[" << topic() << "-" << partition() << ":" << offset() << "]"; + } + else + { + oss << "ERROR[" << error().message() << ", " << topic() << "-" << partition() << ":" << offset() << "]"; + } + return oss.str(); +} + +} + diff --git a/include/kafka/Error.h b/include/kafka/Error.h new file mode 100644 index 000000000..3b7d07556 --- /dev/null +++ b/include/kafka/Error.h @@ -0,0 +1,66 @@ +#pragma once + +#include "kafka/Project.h" + +#include "librdkafka/rdkafka.h" + +#include +#include + + +namespace KAFKA_API { + +struct ErrorCategory: public std::error_category +{ + const char* name() const noexcept override { return "KafkaError"; } + std::string message(int ev) const override { return rd_kafka_err2str(static_cast(ev)); } + + template + struct Global { static ErrorCategory category; }; +}; + +template +ErrorCategory ErrorCategory::Global::category; + +/** + * A utility function which converts an error number to `std::error_code` (with KafkaError category) + */ +inline std::error_code ErrorCode(int errNo = 0) +{ + /** + * The error code for external interfaces. + * Actually, it's the same as 'rd_kafka_resp_err_t', which is defined by librdkafka. + * 1. The negative values are for internal errors. + * 2. Non-negative values are for external errors. See the defination at, + * - [Error Codes] (https://cwiki.apache.org/confluence/display/KAFKA/A+Guide+To+The+Kafka+Protocol#AGuideToTheKafkaProtocol-ErrorCodes) + */ + return {errNo, ErrorCategory::Global<>::category}; +} + +/** + * A utility fucntion which converts the `librdkafka`'s internal `rd_kafka_resp_err_t` to `std::error_code` (with KafkaError category) + */ +inline std::error_code ErrorCode(rd_kafka_resp_err_t respErr) +{ + return ErrorCode(static_cast(respErr)); +} + +/** + * It would contain detailed message. + */ +struct ErrorWithDetail +{ + ErrorWithDetail(std::error_code code, std::string detailedMsg) + : error(code), detail(std::move(detailedMsg)) {} + + ErrorWithDetail(rd_kafka_resp_err_t respErr, std::string detailedMsg) + : ErrorWithDetail(ErrorCode(respErr), std::move(detailedMsg)) {} + + explicit operator bool() const { return static_cast(error); } + + std::error_code error; + std::string detail; +}; + +} // end of KAFKA_API + diff --git a/include/kafka/Header.h b/include/kafka/Header.h new file mode 100644 index 000000000..fc56662f3 --- /dev/null +++ b/include/kafka/Header.h @@ -0,0 +1,56 @@ +#pragma once + +#include "kafka/Project.h" + +#include "kafka/Types.h" + +#include +#include +#include + + +namespace KAFKA_API { + +/** + * Message Header (with a key value pair) + */ +struct Header +{ + using Key = std::string; + using Value = ConstBuffer; + + Header() = default; + Header(Key k, Value v): key(std::move(k)), value(v) {} + + /** + * Obtains explanatory string. + */ + std::string toString() const + { + return key + ":" + value.toString(); + } + + Key key; + Value value; +}; + +/** + * Message Headers. + */ +using Headers = std::vector
; + +/** + * Obtains explanatory string for Headers. + */ +inline std::string toString(const Headers& headers) +{ + std::string ret; + std::for_each(headers.cbegin(), headers.cend(), + [&ret](const auto& header) { + ret.append(ret.empty() ? "" : ",").append(header.toString()); + }); + return ret; +} + +} // end of KAFKA_API + diff --git a/include/kafka/KafkaClient.h b/include/kafka/KafkaClient.h new file mode 100644 index 000000000..9489ea776 --- /dev/null +++ b/include/kafka/KafkaClient.h @@ -0,0 +1,545 @@ +#pragma once + +#include "kafka/Project.h" + +#include "kafka/BrokerMetadata.h" +#include "kafka/Error.h" +#include "kafka/KafkaException.h" +#include "kafka/Logger.h" +#include "kafka/Properties.h" +#include "kafka/RdKafkaHelper.h" +#include "kafka/Types.h" + +#include "librdkafka/rdkafka.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace KAFKA_API { + +/** + * The base class for Kafka clients (i.e, KafkaConsumer, KafkaProducer, and AdminClient). + */ +class KafkaClient +{ +protected: + using ConfigCallbacksRegister = std::function; + using StatsCallback = std::function; + + enum class ClientType { KafkaConsumer, KafkaProducer, AdminClient }; + static std::string getClientTypeString(ClientType type) + { + switch (type) + { + case ClientType::KafkaConsumer: return "KafkaConsumer"; + case ClientType::KafkaProducer: return "KafkaProducer"; + case ClientType::AdminClient: return "AdminClient"; + default: assert(false); return "Invalid Type"; + } + } + + static constexpr int TIMEOUT_INFINITE = -1; + + static int convertMsDurationToInt(std::chrono::milliseconds ms) + { + return ms > std::chrono::milliseconds(INT_MAX) ? TIMEOUT_INFINITE : static_cast(ms.count()); + } + +public: + enum class EventsPollingOption { Manual, Auto }; + + KafkaClient(ClientType clientType, + const Properties& properties, + const ConfigCallbacksRegister& registerCallbacks = ConfigCallbacksRegister(), + const std::set& privatePropertyKeys = {}); + + virtual ~KafkaClient() = default; + + const std::string& clientId() const { return _clientId; } + const std::string& name() const { return _clientName; } + + /** + * Set a log callback for kafka clients, which do not have a client specific logging callback configured (see `setLogger`). + */ + static void setGlobalLogger(Logger logger = NoneLogger) + { + std::call_once(Global<>::initOnce, [](){}); // Then no need to init within KafkaClient constructor + Global<>::logger = std::move(logger); + } + + /** + * Set the log callback for the kafka client (it's a per-client setting). + */ + void setLogger(Logger logger) { _logger = std::move(logger); } + + /** + * Set log level for the kafka client (the default value: LOG_NOTICE). + */ + void setLogLevel(int level); + + /** + * Set callback to receive the periodic statistics info. + * Note: 1) It only works while the "statistics.interval.ms" property is configured with a non-0 value. + * 2) The callback would be triggered periodically, receiving the internal statistics info (with JSON format) emited from librdkafka. + */ + void setStatsCallback(StatsCallback cb) { _statsCb = std::move(cb); } + + /** + * Return the properties which took effect. + */ + const Properties& properties() const { return _properties; } + + /** + * Fetch the effected property (including the property internally set by librdkafka). + */ + Optional getProperty(const std::string& name) const; + + /** + * Fetch matadata from a available broker. + */ + Optional fetchBrokerMetadata(const std::string& topic, + std::chrono::milliseconds timeout = std::chrono::milliseconds(DEFAULT_METADATA_TIMEOUT_MS), + bool disableErrorLogging = false); + + template + void doLog(int level, const char* filename, int lineno, const char* format, Args... args) const + { + const auto& logger = _logger ? _logger : Global<>::logger; + if (level >= LOG_EMERG && level <= _logLevel && logger) + { + LogBuffer logBuffer; + logBuffer.print("%s %s", name().c_str(), format, args...); + logger(level, filename, lineno, logBuffer.c_str()); + } + } + + void doLog(int level, const char* filename, int lineno, const char* msg) const + { + doLog(level, filename, lineno, "%s", msg); + } + +#define KAFKA_API_DO_LOG(lvl, ...) doLog(lvl, __FILE__, __LINE__, ##__VA_ARGS__) + + template + static void doGlobalLog(int level, const char* filename, int lineno, const char* format, Args... args) + { + if (!Global<>::logger) return; + + LogBuffer logBuffer; + logBuffer.print(format, args...); + Global<>::logger(level, filename, lineno, logBuffer.c_str()); + } + static void doGlobalLog(int level, const char* filename, int lineno, const char* msg) + { + doGlobalLog(level, filename, lineno, "%s", msg); + } + +/** + * Log for kafka clients, with the callback which `setGlobalLogger` assigned. + * + * E.g, + * KAFKA_API_LOG(LOG_ERR, "something wrong happened! %s", detailedInfo.c_str()); + */ +#define KAFKA_API_LOG(lvl, ...) KafkaClient::doGlobalLog(lvl, __FILE__, __LINE__, ##__VA_ARGS__) + +protected: + + rd_kafka_t* getClientHandle() const { return _rk.get(); } + + static const KafkaClient& kafkaClient(const rd_kafka_t* rk) { return *static_cast(rd_kafka_opaque(rk)); } + static KafkaClient& kafkaClient(rd_kafka_t* rk) { return *static_cast(rd_kafka_opaque(rk)); } + + static const constexpr int LOG_BUFFER_SIZE = 1024; + + template + struct Global + { + static Logger logger; + static std::once_flag initOnce; + }; + + // Log callback (for librdkafka) + static void logCallback(const rd_kafka_t* rk, int level, const char* fac, const char* buf); + + // Statistics callback (for librdkafka) + static int statsCallback(rd_kafka_t* rk, char* jsonStrBuf, size_t jsonStrLen, void* opaque); + + // Validate properties (and fix it if necesary) + static Properties validateAndReformProperties(const Properties& origProperties); + + // To avoid double-close + bool _opened = false; + +private: + std::string _clientId; + std::string _clientName; + std::atomic _logLevel = {LOG_INFO}; + Logger _logger; + Properties _properties; + StatsCallback _statsCb; + rd_kafka_unique_ptr _rk; + + // Log callback (for class instance) + void onLog(int level, const char* fac, const char* buf) const; + + // Stats callback (for class instance) + void onStats(std::string&& jsonString); + + static const constexpr char* BOOTSTRAP_SERVERS = "bootstrap.servers"; + static const constexpr char* CLIENT_ID = "client.id"; + static const constexpr char* LOG_LEVEL = "log_level"; + static const constexpr char* DEBUG = "debug"; + static const constexpr char* SECURITY_PROTOCOL = "security.protocol"; + static const constexpr char* SASL_KERBEROS_SERVICE_NAME = "sasl.kerberos.service.name"; + + static constexpr int DEFAULT_METADATA_TIMEOUT_MS = 10000; + +protected: + class Pollable + { + public: + virtual ~Pollable() = default; + virtual void poll(int timeoutMs) = 0; + }; + + template + class PollableCallback: public Pollable + { + public: + using Func = void(*)(T*, int); + PollableCallback(T* client, Func cb): _client(client), _cb(cb) {} + + void poll(int timeoutMs) override { _cb(_client, timeoutMs); } + + private: + T* _client; + Func _cb; + }; + + class PollThread + { + public: + explicit PollThread(Pollable& pollable) + : _running(true), _thread(keepPolling, std::ref(_running), std::ref(pollable)) + { + } + + ~PollThread() + { + _running = false; + + if (_thread.joinable()) + { + _thread.join(); + } + } + + private: + static void keepPolling(std::atomic_bool& running, Pollable& pollable) + { + while (running.load()) + { + pollable.poll(CALLBACK_POLLING_INTERVAL_MS); + } + } + + static constexpr int CALLBACK_POLLING_INTERVAL_MS = 10; + + std::atomic_bool _running; + std::thread _thread; + }; +}; + +template +Logger KafkaClient::Global::logger; + +template +std::once_flag KafkaClient::Global::initOnce; + +inline +KafkaClient::KafkaClient(ClientType clientType, + const Properties& properties, + const ConfigCallbacksRegister& registerCallbacks, + const std::set& privatePropertyKeys) +{ + // Save clientID + if (auto clientId = properties.getProperty(CLIENT_ID)) + { + _clientId = *clientId; + _clientName = getClientTypeString(clientType) + "[" + _clientId + "]"; + } + + // Init global logger + std::call_once(Global<>::initOnce, [](){ Global<>::logger = DefaultLogger; }); + + // Save LogLevel + if (auto logLevel = properties.getProperty(LOG_LEVEL)) + { + try + { + _logLevel = std::stoi(*logLevel); + } + catch (const std::exception& e) + { + KAFKA_THROW_WITH_MSG(RD_KAFKA_RESP_ERR__INVALID_ARG, std::string("Invalid log_level[").append(*logLevel).append("], which must be an number!").append(e.what())); + } + + if (_logLevel < LOG_EMERG || _logLevel > LOG_DEBUG) + { + KAFKA_THROW_WITH_MSG(RD_KAFKA_RESP_ERR__INVALID_ARG, std::string("Invalid log_level[").append(*logLevel).append("], which must be a value between 0 and 7!")); + } + } + + LogBuffer errInfo; + + auto rk_conf = rd_kafka_conf_unique_ptr(rd_kafka_conf_new()); + + for (const auto& prop: properties.map()) + { + // Those private properties are only available for `C++ wrapper`, not for librdkafka + if (privatePropertyKeys.count(prop.first)) + { + _properties.put(prop.first, prop.second); + continue; + } + + rd_kafka_conf_res_t result = rd_kafka_conf_set(rk_conf.get(), prop.first.c_str(), prop.second.c_str(), errInfo.str(), errInfo.capacity()); + if (result == RD_KAFKA_CONF_OK) + { + _properties.put(prop.first, prop.second); + } + else + { + KAFKA_API_DO_LOG(LOG_ERR, "failed to be initialized with property[%s:%s], result[%d]", prop.first.c_str(), prop.second.c_str(), result); + } + } + + // Save KafkaClient's raw pointer to the "opaque" field, thus we could fetch it later (for kinds of callbacks) + rd_kafka_conf_set_opaque(rk_conf.get(), this); + + // Log Callback + rd_kafka_conf_set_log_cb(rk_conf.get(), KafkaClient::logCallback); + + // Statistics Callback + rd_kafka_conf_set_stats_cb(rk_conf.get(), KafkaClient::statsCallback); + + // Other Callbacks + if (registerCallbacks) + { + registerCallbacks(rk_conf.get()); + } + + // Set client handler + _rk.reset(rd_kafka_new((clientType == ClientType::KafkaConsumer ? RD_KAFKA_CONSUMER : RD_KAFKA_PRODUCER), + rk_conf.release(), // rk_conf's ownship would be transferred to rk, after the "rd_kafka_new()" call + errInfo.clear().str(), + errInfo.capacity())); + KAFKA_THROW_IF_WITH_ERROR(rd_kafka_last_error()); + + // Add brokers + auto brokers = properties.getProperty(BOOTSTRAP_SERVERS); + if (rd_kafka_brokers_add(getClientHandle(), brokers->c_str()) == 0) + { + KAFKA_THROW_WITH_MSG(RD_KAFKA_RESP_ERR__INVALID_ARG,\ + "No broker could be added successfully, BOOTSTRAP_SERVERS=[" + *brokers + "]"); + } + + _opened = true; +} + +inline Properties +KafkaClient::validateAndReformProperties(const Properties& origProperties) +{ + Properties properties(origProperties); + + // BOOTSTRAP_SERVERS property is mandatory + if (!properties.getProperty(BOOTSTRAP_SERVERS)) + { + KAFKA_THROW_WITH_MSG(RD_KAFKA_RESP_ERR__INVALID_ARG,\ + "Validation failed! With no property [" + std::string(BOOTSTRAP_SERVERS) + "]"); + } + + // If no "client.id" configured, generate a random one for user + if (!properties.getProperty(CLIENT_ID)) + { + properties.put(CLIENT_ID, Utility::getRandomString()); + } + + // "sasl.kerberos.service.name" is mandatory for SASL connection + if (auto securityProtocol = properties.getProperty(SECURITY_PROTOCOL)) + { + if (securityProtocol->find("sasl") != std::string::npos) + { + if (!properties.getProperty(SASL_KERBEROS_SERVICE_NAME)) + { + KAFKA_THROW_WITH_MSG(RD_KAFKA_RESP_ERR__INVALID_ARG,\ + "The \"sasl.kerberos.service.name\" property is mandatory for SASL connection!"); + } + } + } + + // If no "log_level" configured, use LOG_NOTICE as default + if (!properties.getProperty(LOG_LEVEL)) + { + properties.put(LOG_LEVEL, std::to_string(LOG_NOTICE)); + } + // Enable debugging by default. Otherwise, no detail log could be got later + properties.put(DEBUG, "all"); + + return properties; +} + +inline Optional +KafkaClient::getProperty(const std::string& name) const +{ + constexpr int DEFAULT_BUF_SIZE = 512; + + const rd_kafka_conf_t* conf = rd_kafka_conf(getClientHandle()); + + std::vector valueBuf(DEFAULT_BUF_SIZE); + std::size_t valueSize = valueBuf.size(); + + // Firstly, try with a default buf size + if (rd_kafka_conf_get(conf, name.c_str(), valueBuf.data(), &valueSize) != RD_KAFKA_CONF_OK) + { + // If doesn't exist within librdkafka, might be from the C++ wrapper + return _properties.getProperty(name); + } + + // If the default buf size is not big enough, retry with a larger buf + if (valueSize > valueBuf.size()) + { + valueBuf.resize(valueSize); + [[maybe_unused]] rd_kafka_conf_res_t result = rd_kafka_conf_get(conf, name.c_str(), valueBuf.data(), &valueSize); + assert(result == RD_KAFKA_CONF_OK); + } + + return std::string(valueBuf.data()); +} + +inline void +KafkaClient::setLogLevel(int level) +{ + _logLevel = level < LOG_EMERG ? LOG_EMERG : (level > LOG_DEBUG ? LOG_DEBUG : level); + rd_kafka_set_log_level(getClientHandle(), _logLevel); +} + +inline void +KafkaClient::onLog(int level, const char* fac, const char* buf) const +{ + doLog(level, nullptr, 0, "%s | %s", fac, buf); // The `filename`/`lineno` here is NULL (just wouldn't help) +} + +inline void +KafkaClient::logCallback(const rd_kafka_t* rk, int level, const char* fac, const char* buf) +{ + kafkaClient(rk).onLog(level, fac, buf); +} + +inline void +KafkaClient::onStats(std::string&& jsonString) +{ + if (_statsCb) _statsCb(std::move(jsonString)); +} + +inline int +KafkaClient::statsCallback(rd_kafka_t* rk, char* jsonStrBuf, size_t jsonStrLen, void* /*opaque*/) +{ + kafkaClient(rk).onStats(std::string(jsonStrBuf, jsonStrBuf+jsonStrLen)); + return 0; +} + +inline Optional +KafkaClient::fetchBrokerMetadata(const std::string& topic, std::chrono::milliseconds timeout, bool disableErrorLogging) +{ + Optional ret; + auto rkt = rd_kafka_topic_unique_ptr(rd_kafka_topic_new(getClientHandle(), topic.c_str(), nullptr)); + + const rd_kafka_metadata_t* rk_metadata = nullptr; + rd_kafka_resp_err_t err = rd_kafka_metadata(getClientHandle(), false, rkt.get(), &rk_metadata, convertMsDurationToInt(timeout)); + auto guard = rd_kafka_metadata_unique_ptr(rk_metadata); + + if (err != RD_KAFKA_RESP_ERR_NO_ERROR) + { + if (!disableErrorLogging) + { + KAFKA_API_DO_LOG(LOG_ERR, "Failed to get BrokerMetadata! error[%s]", rd_kafka_err2str(err)); + } + return ret; + } + + if (rk_metadata->topic_cnt != 1) + { + if (!disableErrorLogging) + { + KAFKA_API_DO_LOG(LOG_ERR, "Failed to construct MetaData! topic_cnt[%d]", rk_metadata->topic_cnt); + } + return ret; + } + + const rd_kafka_metadata_topic& metadata_topic = rk_metadata->topics[0]; + if (metadata_topic.err != 0) + { + if (!disableErrorLogging) + { + KAFKA_API_DO_LOG(LOG_ERR, "Failed to construct MetaData! topic.err[%s]", rd_kafka_err2str(metadata_topic.err)); + } + return ret; + } + + // Construct the BrokerMetadata + BrokerMetadata metadata(metadata_topic.topic); + metadata.setOrigNodeName(rk_metadata->orig_broker_name ? std::string(rk_metadata->orig_broker_name) : ""); + + for (int i = 0; i < rk_metadata->broker_cnt; ++i) + { + metadata.addNode(rk_metadata->brokers[i].id, rk_metadata->brokers[i].host, rk_metadata->brokers[i].port); + } + + for (int i = 0; i < metadata_topic.partition_cnt; ++i) + { + const rd_kafka_metadata_partition& metadata_partition = metadata_topic.partitions[i]; + + Partition partition = metadata_partition.id; + + BrokerMetadata::PartitionInfo partitionInfo; + + if (metadata_partition.err != 0) + { + if (!disableErrorLogging) + { + KAFKA_API_DO_LOG(LOG_ERR, "Got error[%s] while constructing BrokerMetadata for topic[%s]-partition[%d]", rd_kafka_err2str(metadata_partition.err), topic.c_str(), partition); + } + + continue; + } + + partitionInfo.setLeader(metadata.getNode(metadata_partition.leader)); + + for (int j = 0; j < metadata_partition.replica_cnt; ++j) + { + partitionInfo.addReplica(metadata.getNode(metadata_partition.replicas[j])); + } + for (int j = 0; j < metadata_partition.isr_cnt; ++j) + { + partitionInfo.addInSyncReplica(metadata.getNode(metadata_partition.isrs[j])); + } + + metadata.addPartitionInfo(partition, partitionInfo); + } + + ret = metadata; + return ret; +} + + +} // end of KAFKA_API + diff --git a/include/kafka/KafkaConsumer.h b/include/kafka/KafkaConsumer.h new file mode 100644 index 000000000..6993e9440 --- /dev/null +++ b/include/kafka/KafkaConsumer.h @@ -0,0 +1,960 @@ +#pragma once + +#include "kafka/Project.h" + +#include "kafka/ConsumerConfig.h" +#include "kafka/ConsumerRecord.h" +#include "kafka/KafkaClient.h" + +#include "librdkafka/rdkafka.h" + +#include +#include +#include +#include +#include + + +namespace KAFKA_API { + +namespace Consumer +{ + /** + * To identify which kind of re-balance event is handling, when the set of partitions assigned to the consumer changes. + * It's guaranteed that rebalance callback will be called twice (first with PartitionsRevoked, and then with PartitionsAssigned). + */ + enum class RebalanceEventType { PartitionsAssigned, PartitionsRevoked }; + + /** + * A callback interface that the user can implement to trigger custom actions when the set of partitions assigned to the consumer changes. + */ + using RebalanceCallback = std::function; + /** + * A callback interface that the user can implement to trigger custom actions when a commit request completes. + */ + using OffsetCommitCallback = std::function; +} + + +/** + * The base class for KafkaAutoCommitProducer and KafkaManualCommitProducer. + */ +class KafkaConsumer: public KafkaClient +{ +protected: + using RebalanceEventType = Consumer::RebalanceEventType; + using RebalanceCallback = Consumer::RebalanceCallback; + using OffsetCommitCallback = Consumer::OffsetCommitCallback; + + // Default value for property "max.poll.records" (which is same with Java API) + static const constexpr char* DEFAULT_MAX_POLL_RECORDS_VALUE = "500"; + + enum class OffsetCommitOption { Auto, Manual }; + + // Constructor + KafkaConsumer(const Properties& properties, KafkaConsumer::OffsetCommitOption offsetCommitOption) + : KafkaClient(ClientType::KafkaConsumer, properties, registerConfigCallbacks, {ConsumerConfig::MAX_POLL_RECORDS}), + _offsetCommitOption(offsetCommitOption) + { + auto propStr = properties.toString(); + KAFKA_API_DO_LOG(LOG_INFO, "initializes with properties[%s]", propStr.c_str()); + + // Pick up the MAX_POLL_RECORDS configuration + auto maxPollRecords = properties.getProperty(ConsumerConfig::MAX_POLL_RECORDS); + assert(maxPollRecords); + _maxPollRecords = std::stoi(*maxPollRecords); + + // Fetch groupId from configuration + auto groupId = properties.getProperty(ConsumerConfig::GROUP_ID); + assert(groupId); + setGroupId(*groupId); + + // Redirect the reply queue (to the client group queue) + rd_kafka_resp_err_t err = rd_kafka_poll_set_consumer(getClientHandle()); + KAFKA_THROW_IF_WITH_ERROR(err); + + // Initialize message-fetching queue + _rk_queue.reset(rd_kafka_queue_get_consumer(getClientHandle())); + } + +public: + /** + * To get group ID. + */ + std::string getGroupId() const { return _groupId; } + + /** + * To set group ID. The group ID is mandatory for a Consumer. + */ + void setGroupId(const std::string& id) { _groupId = id; } + + /** + * Subscribe to the given list of topics to get dynamically assigned partitions. + * An exception would be thrown if assign is called previously (without a subsequent call to unsubscribe()) + */ + void subscribe(const Topics& topics, Consumer::RebalanceCallback cb = Consumer::RebalanceCallback()); + + /** + * Get the current subscription. + */ + Topics subscription() const; + + /** + * Unsubscribe from topics currently subscribed. + */ + void unsubscribe(); + + /** + * Manually assign a list of partitions to this consumer. + * An exception would be thrown if subscribe is called previously (without a subsequent call to unsubscribe()) + */ + void assign(const TopicPartitions& tps); + + /** + * Get the set of partitions currently assigned to this consumer. + */ + TopicPartitions assignment() const; + + // Seek & Position + /** + * Overrides the fetch offsets that the consumer will use on the next poll(timeout). + * If this API is invoked for the same partition more than once, the latest offset will be used on the next poll(). + * Throws KafkaException with errors: + * - RD_KAFKA_RESP_ERR__TIMED_OUT: Operation timed out + * - RD_KAFKA_RESP_ERR__UNKNOWN_PARTITION: Invalid partition + * - RD_KAFKA_RESP_ERR__STATE: Invalid broker state + */ + void seek(const TopicPartition& tp, Offset o, std::chrono::milliseconds timeout = std::chrono::milliseconds(DEFAULT_SEEK_TIMEOUT_MS)); + + /** + * Seek to the first offset for each of the given partitions. + * This function evaluates lazily, seeking to the first offset in all partitions only when poll(long) or position(TopicPartition) are called. + * If no partitions are provided, seek to the first offset for all of the currently assigned partitions. + * Throws KafkaException with errors: + * - RD_KAFKA_RESP_ERR__TIMED_OUT: Operation timed out + * - RD_KAFKA_RESP_ERR__UNKNOWN_PARTITION: Invalid partition + * - RD_KAFKA_RESP_ERR__STATE: Invalid broker state + */ + void seekToBeginning(const TopicPartitions& tps, + std::chrono::milliseconds timeout = std::chrono::milliseconds(DEFAULT_SEEK_TIMEOUT_MS)) { seekToBeginningOrEnd(tps, true, timeout); } + void seekToBeginning(std::chrono::milliseconds timeout = std::chrono::milliseconds(DEFAULT_SEEK_TIMEOUT_MS)) { seekToBeginningOrEnd(assignment(false), true, timeout); } + + /** + * Seek to the last offset for each of the given partitions. + * This function evaluates lazily, seeking to the final offset in all partitions only when poll(long) or position(TopicPartition) are called. + * If no partitions are provided, seek to the first offset for all of the currently assigned partitions. + * Throws KafkaException with errors: + * - RD_KAFKA_RESP_ERR__TIMED_OUT: Operation timed out + * - RD_KAFKA_RESP_ERR__UNKNOWN_PARTITION: Invalid partition + * - RD_KAFKA_RESP_ERR__STATE: Invalid broker state + */ + void seekToEnd(const TopicPartitions& tps, + std::chrono::milliseconds timeout = std::chrono::milliseconds(DEFAULT_SEEK_TIMEOUT_MS)) { seekToBeginningOrEnd(tps, false, timeout); } + void seekToEnd(std::chrono::milliseconds timeout = std::chrono::milliseconds(DEFAULT_SEEK_TIMEOUT_MS)) { seekToBeginningOrEnd(assignment(false), false, timeout); } + + /** + * Get the offset of the next record that will be fetched (if a record with that offset exists). + */ + Offset position(const TopicPartition& tp) const; + + /** + * Get the first offset for the given partitions. + * This method does not change the current consumer position of the partitions. + * Throws KafkaException with errors: + * - RD_KAFKA_RESP_ERR__FAIL: Generic failure + */ + std::map beginningOffsets(const TopicPartitions& tps) const { return getOffsets(tps, true); } + + /** + * Get the last offset for the given partitions. The last offset of a partition is the offset of the upcoming message, i.e. the offset of the last available message + 1. + * This method does not change the current consumer position of the partitions. + * Throws KafkaException with errors: + * - RD_KAFKA_RESP_ERR__FAIL: Generic failure + */ + std::map endOffsets(const TopicPartitions& tps) const { return getOffsets(tps, false); } + + /** + * Get the last committed offset for the given partition (whether the commit happened by this process or another).This offset will be used as the position for the consumer in the event of a failure. + * This call will block to do a remote call to get the latest committed offsets from the server. + * Throws KafkaException with errors: + * - RD_KAFKA_RESP_ERR__INVALID_ARG: Invalid partition + */ + Offset committed(const TopicPartition& tp); + + /** + * Fetch data for the topics or partitions specified using one of the subscribe/assign APIs. + * Returns the polled records. + * Note: 1) The result could be fetched through ConsumerRecord (with member function `error`). + * 2) Make sure the `ConsumerRecord` be destructed before the `KafkaConsumer.close()`. + */ + std::vector poll(std::chrono::milliseconds timeout); + + /** + * Fetch data for the topics or partitions specified using one of the subscribe/assign APIs. + * Returns the number of polled records (which have been saved into parameter `output`). + * Note: 1) The result could be fetched through ConsumerRecord (with member function `error`). + * 2) Make sure the `ConsumerRecord` be destructed before the `KafkaConsumer.close()`. + */ + std::size_t poll(std::chrono::milliseconds timeout, std::vector& output); + + /** + * Suspend fetching from the requested partitions. Future calls to poll() will not return any records from these partitions until they have been resumed using resume(). + * Note: 1) After pausing, the application still need to call `poll()` at regular intervals. + * 2) This method does not affect partition subscription/assignment (i.e, pause fetching from partitions would not trigger a rebalance, since the consumer is still alive). + * 3) If none of the provided partitions is assigned to this consumer, an exception would be thrown. + * Throws KafkaException with error: + * - RD_KAFKA_RESP_ERR__INVALID_ARG: Invalid arguments + */ + void pause(const TopicPartitions& tps); + + /** + * Suspend fetching from all assigned partitions. Future calls to poll() will not return any records until they have been resumed using resume(). + * Note: This method does not affect partition subscription/assignment. + */ + void pause(); + + /** + * Resume specified partitions which have been paused with pause(). New calls to poll() will return records from these partitions if there are any to be fetched. + * Note: If the partitions were not previously paused, this method is a no-op. + */ + void resume(const TopicPartitions& tps); + + /** + * Resume all partitions which have been paused with pause(). New calls to poll() will return records from these partitions if there are any to be fetched. + */ + void resume(); + +protected: + static const constexpr char* ENABLE_AUTO_OFFSET_STORE = "enable.auto.offset.store"; + static const constexpr char* ENABLE_AUTO_COMMIT = "enable.auto.commit"; + static const constexpr char* AUTO_COMMIT_INTERVAL_MS = "auto.commit.interval.ms"; + + static constexpr int DEFAULT_SEEK_TIMEOUT_MS = 10000; + static constexpr int SEEK_RETRY_INTERVAL_MS = 5000; + + const OffsetCommitOption _offsetCommitOption; + + enum class CommitType { Sync, Async }; + void commit(const TopicPartitionOffsets& tpos, CommitType type); + + void close(); + + // Offset Commit Callback (for librdkafka) + static void offsetCommitCallback(rd_kafka_t* rk, rd_kafka_resp_err_t err, rd_kafka_topic_partition_list_t* rk_tpos, void* opaque); + + // Validate properties (and fix it if necesary) + static Properties validateAndReformProperties(const Properties& origProperties); + +private: + void commitStoredOffsetsIfNecessary(CommitType type); + void storeOffsetsIfNecessary(const std::vector& records); + + void seekToBeginningOrEnd(const TopicPartitions& tps, bool toBeginning, std::chrono::milliseconds timeout); + std::map getOffsets(const TopicPartitions& tps, bool atBeginning) const; + + // Internal interface for "assign" + void _assign(const TopicPartitions& tps); + // Internal interface for "assignment" + TopicPartitions assignment(bool withQueryRequest) const; + + std::string _groupId; + + unsigned int _maxPollRecords = 500; // Default value for batch-poll + + rd_kafka_queue_unique_ptr _rk_queue; + + // Save assignment info (from "assign()" call or rebalance callback) locally, to accelerate seeking procedure + TopicPartitions _assignment; + // Assignment from user's input, -- by calling "assign()" + TopicPartitions _userAssignment; + + // The offsets to store (and commit later) + std::map _offsetsToStore; + + // Register Callbacks for rd_kafka_conf_t + static void registerConfigCallbacks(rd_kafka_conf_t* conf); + + void pollMessages(int timeoutMs, std::vector& output); + + enum class PauseOrResumeOperation { Pause, Resume }; + void pauseOrResumePartitions(const TopicPartitions& tps, PauseOrResumeOperation op); + + // Rebalance Callback (for librdkafka) + static void rebalanceCallback(rd_kafka_t* rk, rd_kafka_resp_err_t err, rd_kafka_topic_partition_list_t* partitions, void* opaque); + // Rebalance Callback (for class instance) + void onRebalance(rd_kafka_resp_err_t err, rd_kafka_topic_partition_list_t* rk_partitions); + + RebalanceCallback _rebalanceCb; +}; + + +// Validate properties (and fix it if necesary) +inline Properties +KafkaConsumer::validateAndReformProperties(const Properties& origProperties) +{ + // Let the base class validate first + Properties properties = KafkaClient::validateAndReformProperties(origProperties); + + // If no "group.id" configured, generate a random one for user + if (!properties.getProperty(ConsumerConfig::GROUP_ID)) + { + properties.put(ConsumerConfig::GROUP_ID, Utility::getRandomString()); + } + + // If no "max.poll.records" configured, use a default value + if (!properties.getProperty(ConsumerConfig::MAX_POLL_RECORDS)) + { + properties.put(ConsumerConfig::MAX_POLL_RECORDS, DEFAULT_MAX_POLL_RECORDS_VALUE); + } + + // We want to customize the auto-commit behavior, with librdkafka's configuration disabled + properties.put(ENABLE_AUTO_COMMIT, "false"); + properties.put(AUTO_COMMIT_INTERVAL_MS, "0"); + + return properties; +} + +// Register Callbacks for rd_kafka_conf_t +inline void +KafkaConsumer::registerConfigCallbacks(rd_kafka_conf_t* conf) +{ + // Rebalance Callback + // would turn off librdkafka's automatic partition assignment/revocation + rd_kafka_conf_set_rebalance_cb(conf, KafkaConsumer::rebalanceCallback); +} + +inline void +KafkaConsumer::close() +{ + _opened = false; + + try + { + // Commit the offsets for these messages which had been polled last time (for KafkaAutoCommitConsumer) + commitStoredOffsetsIfNecessary(CommitType::Sync); + } + catch(const KafkaException& e) + { + KAFKA_API_DO_LOG(LOG_ERR, "met error[%s] while closing", e.what()); + } + + rd_kafka_consumer_close(getClientHandle()); + + while (rd_kafka_outq_len(getClientHandle())) + { + rd_kafka_poll(getClientHandle(), KafkaClient::TIMEOUT_INFINITE); + } + + KAFKA_API_DO_LOG(LOG_INFO, "closed"); +} + + +// Subscription +inline void +KafkaConsumer::subscribe(const Topics& topics, RebalanceCallback cb) +{ + std::string topicsStr = toString(topics); + + if (!_userAssignment.empty()) + { + KAFKA_THROW_WITH_MSG(RD_KAFKA_RESP_ERR__FAIL, "Unexpected Operation! Once assign() was used, subscribe() should not be called any more!"); + } + + KAFKA_API_DO_LOG(LOG_INFO, "will subscribe, topics[%s]", topicsStr.c_str()); + + _rebalanceCb = std::move(cb); + + auto rk_topics = rd_kafka_topic_partition_list_unique_ptr(createRkTopicPartitionList(topics)); + + rd_kafka_resp_err_t err = rd_kafka_subscribe(getClientHandle(), rk_topics.get()); + KAFKA_THROW_IF_WITH_ERROR(err); + + // The rebalcance callback (e.g. "assign", etc) would be served during the time (within this thread) + rd_kafka_poll(getClientHandle(), TIMEOUT_INFINITE); + + KAFKA_API_DO_LOG(LOG_INFO, "subscribed, topics[%s]", topicsStr.c_str()); +} + +inline void +KafkaConsumer::unsubscribe() +{ + KAFKA_API_DO_LOG(LOG_INFO, "will unsubscribe"); + + rd_kafka_resp_err_t err = rd_kafka_unsubscribe(getClientHandle()); + KAFKA_THROW_IF_WITH_ERROR(err); + + // The rebalcance callback (e.g. "assign", etc) would be served during the time (within this thread) + rd_kafka_poll(getClientHandle(), TIMEOUT_INFINITE); + + KAFKA_API_DO_LOG(LOG_INFO, "unsubscribed"); +} + +inline Topics +KafkaConsumer::subscription() const +{ + rd_kafka_topic_partition_list_t* raw_topics = nullptr; + rd_kafka_resp_err_t err = rd_kafka_subscription(getClientHandle(), &raw_topics); + auto rk_topics = rd_kafka_topic_partition_list_unique_ptr(raw_topics); + + KAFKA_THROW_IF_WITH_ERROR(err); + + return getTopics(rk_topics.get()); +} + +// Assign for Topic/Partition level, -- internal interface +inline void +KafkaConsumer::_assign(const TopicPartitions& tps) +{ + std::string tpsStr = toString(tps); + KAFKA_API_DO_LOG(LOG_INFO, "will assign with TopicPartitions[%s]", tpsStr.c_str()); + + auto rk_tps = rd_kafka_topic_partition_list_unique_ptr(createRkTopicPartitionList(tps)); + + rd_kafka_resp_err_t err = rd_kafka_assign(getClientHandle(), (rk_tps->cnt > 0) ? rk_tps.get() : nullptr); + KAFKA_THROW_IF_WITH_ERROR(err); + + _assignment = tps; + + KAFKA_API_DO_LOG(LOG_INFO, "assigned with TopicPartitions[%s]", tpsStr.c_str()); +} + +// Assign for Topic/Partition level, -- external interface +inline void +KafkaConsumer::assign(const TopicPartitions& tps) +{ + if (!subscription().empty()) + { + KAFKA_THROW_WITH_MSG(RD_KAFKA_RESP_ERR__FAIL, "Unexpected Operation! Once subscribe() was used, assign() should not be called any more!"); + } + + _userAssignment = tps; + + _assign(tps); +} + +// Assignment, -- internal interface +inline TopicPartitions +KafkaConsumer::assignment(bool withQueryRequest) const +{ + if (withQueryRequest) + { + rd_kafka_topic_partition_list_t* raw_tps = nullptr; + rd_kafka_resp_err_t err = rd_kafka_assignment(getClientHandle(), &raw_tps); + + auto rk_tps = rd_kafka_topic_partition_list_unique_ptr(raw_tps); + + KAFKA_THROW_IF_WITH_ERROR(err); + + return getTopicPartitions(rk_tps.get()); + } + + return _assignment; +} + +// Assignment, -- external interface +inline TopicPartitions +KafkaConsumer::assignment() const +{ + return subscription().empty() ? assignment(false) : TopicPartitions(); +} + +// Seek & Position +inline void +KafkaConsumer::seek(const TopicPartition& tp, Offset o, std::chrono::milliseconds timeout) +{ + std::string tpStr = toString(tp); + KAFKA_API_DO_LOG(LOG_INFO, "will seek with topic-partition[%s], offset[%d]", tpStr.c_str(), o); + + auto rkt = rd_kafka_topic_unique_ptr(rd_kafka_topic_new(getClientHandle(), tp.first.c_str(), nullptr)); + if (!rkt) + { + KAFKA_THROW(rd_kafka_last_error()); + } + + const auto end = std::chrono::steady_clock::now() + timeout; + + rd_kafka_resp_err_t err; + do + { + err = rd_kafka_seek(rkt.get(), tp.second, o, SEEK_RETRY_INTERVAL_MS); + if (err != RD_KAFKA_RESP_ERR__STATE && err != RD_KAFKA_RESP_ERR__TIMED_OUT && err != RD_KAFKA_RESP_ERR__OUTDATED) + { + break; + } + + // If the "seek" was called just after "assign", there's a chance that the toppar's "fetch_state" (async setted) was not ready yes. + // If that's the case, we would retry again (normally, just after a very short while, the "seek" would succeed) + std::this_thread::yield(); + } while (std::chrono::steady_clock::now() < end); + + KAFKA_THROW_IF_WITH_ERROR(err); + + KAFKA_API_DO_LOG(LOG_INFO, "seeked with topic-partition[%s], offset[%d]", tpStr.c_str(), o); +} + +inline void +KafkaConsumer::seekToBeginningOrEnd(const TopicPartitions& tps, bool toBeginning, std::chrono::milliseconds timeout) +{ + for (const auto& tp: tps) + { + seek(tp, (toBeginning ? RD_KAFKA_OFFSET_BEGINNING : RD_KAFKA_OFFSET_END), timeout); + } +} + +inline Offset +KafkaConsumer::position(const TopicPartition& tp) const +{ + auto rk_tp = rd_kafka_topic_partition_list_unique_ptr(createRkTopicPartitionList({tp})); + + rd_kafka_resp_err_t err = rd_kafka_position(getClientHandle(), rk_tp.get()); + KAFKA_THROW_IF_WITH_ERROR(err); + + return rk_tp->elems[0].offset; +} + +inline std::map +KafkaConsumer::getOffsets(const TopicPartitions& tps, bool atBeginning) const +{ + std::map result; + + for (const auto& tp: tps) + { + Offset beginning, end; + rd_kafka_resp_err_t err = rd_kafka_query_watermark_offsets(getClientHandle(), tp.first.c_str(), tp.second, &beginning, &end, 0); + KAFKA_THROW_IF_WITH_ERROR(err); + + result[tp] = (atBeginning ? beginning : end); + } + + return result; +} + +// Commit +inline void +KafkaConsumer::commit(const TopicPartitionOffsets& tpos, CommitType type) +{ + auto rk_tpos = rd_kafka_topic_partition_list_unique_ptr(tpos.empty() ? nullptr : createRkTopicPartitionList(tpos)); + + rd_kafka_resp_err_t err = rd_kafka_commit(getClientHandle(), rk_tpos.get(), type == CommitType::Async ? 1 : 0); + // No stored offset to commit (it might happen and should not be treated as a mistake) + if (tpos.empty() && err == RD_KAFKA_RESP_ERR__NO_OFFSET) + { + err = RD_KAFKA_RESP_ERR_NO_ERROR; + } + + KAFKA_THROW_IF_WITH_ERROR(err); +} + +// Fetch committed offset +inline Offset +KafkaConsumer::committed(const TopicPartition& tp) +{ + auto rk_tps = rd_kafka_topic_partition_list_unique_ptr(createRkTopicPartitionList({tp})); + + rd_kafka_resp_err_t err = rd_kafka_committed(getClientHandle(), rk_tps.get(), TIMEOUT_INFINITE); + KAFKA_THROW_IF_WITH_ERROR(err); + + return rk_tps->elems[0].offset; +} + +// Commit stored offsets +inline void +KafkaConsumer::commitStoredOffsetsIfNecessary(CommitType type) +{ + if (_offsetCommitOption == OffsetCommitOption::Auto && !_offsetsToStore.empty()) + { + for (auto& o: _offsetsToStore) + { + ++o.second; + } + commit(_offsetsToStore, type); + _offsetsToStore.clear(); + } +} + +// Store offsets +inline void +KafkaConsumer::storeOffsetsIfNecessary(const std::vector& records) +{ + if (_offsetCommitOption == OffsetCommitOption::Auto) + { + for (const auto& record: records) + { + _offsetsToStore[TopicPartition(record.topic(), record.partition())] = record.offset(); + } + } +} + +// Fetch messages (internally used) +inline void +KafkaConsumer::pollMessages(int timeoutMs, std::vector& output) +{ + // Commit the offsets for these messages which had been polled last time (for KafkaAutoCommitConsumer) + commitStoredOffsetsIfNecessary(CommitType::Async); + + // Poll messages with librdkafka's API + rd_kafka_message_t *msgPtrArray[_maxPollRecords]; + std::size_t msgReceived = rd_kafka_consume_batch_queue(_rk_queue.get(), timeoutMs, msgPtrArray, _maxPollRecords); + + // Wrap messages with ConsumerRecord + output.clear(); + output.reserve(msgReceived); + std::for_each(msgPtrArray, msgPtrArray + msgReceived, [&output](rd_kafka_message_t* rkMsg) { output.emplace_back(rkMsg); }); + + // Store the offsets for all these polled messages (for KafkaAutoCommitConsumer) + storeOffsetsIfNecessary(output); +} + +// Fetch messages (return via return value) +inline std::vector +KafkaConsumer::poll(std::chrono::milliseconds timeout) +{ + std::vector result; + poll(timeout, result); + return result; +} + +// Fetch messages (return via input parameter) +inline std::size_t +KafkaConsumer::poll(std::chrono::milliseconds timeout, std::vector& output) +{ + pollMessages(convertMsDurationToInt(timeout), output); + return output.size(); +} + +inline void +KafkaConsumer::pauseOrResumePartitions(const TopicPartitions& tps, PauseOrResumeOperation op) +{ + auto rk_tpos = rd_kafka_topic_partition_list_unique_ptr(createRkTopicPartitionList(tps)); + + rd_kafka_resp_err_t err = (op == PauseOrResumeOperation::Pause) ? + rd_kafka_pause_partitions(getClientHandle(), rk_tpos.get()) : rd_kafka_resume_partitions(getClientHandle(), rk_tpos.get()); + KAFKA_THROW_IF_WITH_ERROR(err); + + const char* opString = (op == PauseOrResumeOperation::Pause) ? "pause" : "resume"; + int cnt = 0; + for (int i = 0; i < rk_tpos->cnt; ++i) + { + const rd_kafka_topic_partition_t& rk_tp = rk_tpos->elems[i]; + if (rk_tp.err != RD_KAFKA_RESP_ERR_NO_ERROR) + { + KAFKA_API_DO_LOG(LOG_ERR, "%s topic-partition[%s-%d] error[%s]", opString, rk_tp.topic, rk_tp.partition, rd_kafka_err2str(rk_tp.err)); + } + else + { + KAFKA_API_DO_LOG(LOG_INFO, "%sd topic-partition[%s-%d]", opString, rk_tp.topic, rk_tp.partition, rd_kafka_err2str(rk_tp.err)); + ++cnt; + } + } + + if (cnt == 0 && op == PauseOrResumeOperation::Pause) + { + std::string errMsg = std::string("No partition could be ") + opString + std::string("d among TopicPartitions[") + toString(tps) + std::string("]"); + KAFKA_THROW_WITH_MSG(RD_KAFKA_RESP_ERR__INVALID_ARG, errMsg); + } +} + +inline void +KafkaConsumer::pause(const TopicPartitions& tps) +{ + pauseOrResumePartitions(tps, PauseOrResumeOperation::Pause); +} + +inline void +KafkaConsumer::pause() +{ + pause(_assignment); +} + +inline void +KafkaConsumer::resume(const TopicPartitions& tps) +{ + pauseOrResumePartitions(tps, PauseOrResumeOperation::Resume); +} + +inline void +KafkaConsumer::resume() +{ + resume(_assignment); +} + +// Rebalance Callback (for class instance) +inline void +KafkaConsumer::onRebalance(rd_kafka_resp_err_t err, rd_kafka_topic_partition_list_t* rk_partitions) +{ + TopicPartitions tps = getTopicPartitions(rk_partitions); + std::string tpsStr = toString(tps); + + switch(err) + { + case RD_KAFKA_RESP_ERR__ASSIGN_PARTITIONS: + KAFKA_API_DO_LOG(LOG_INFO, "invoked re-balance callback for event[ASSIGN_PARTITIONS]. topic-partitions[%s]", tpsStr.c_str()); + + // Assign with a brand new full list + _assign(tps); + break; + + case RD_KAFKA_RESP_ERR__REVOKE_PARTITIONS: + KAFKA_API_DO_LOG(LOG_INFO, "invoked re-balance callback for event[REVOKE_PARTITIONS]. topic-partitions[%s]", tpsStr.c_str()); + + _offsetsToStore.clear(); + + // For "manual commit" cases, user must take all the responsibility to commit while necessary. + // -- thus, they must register a valid rebalance event listener and do the "commit things" properly. + + // Revoke all previously assigned partitions. + // Normally, another "ASSIGN_PARTITIONS" event would be received later. + _assign(TopicPartitions()); // with null + break; + + default: + KAFKA_API_DO_LOG(LOG_ERR, "invoked re-balance callback for event[unknown: %d]. topic-partitions[%s]", err, tpsStr.c_str()); + + _assign(TopicPartitions()); // with null + return; // would not call user's rebalance event listener + } + + if (_rebalanceCb) + { + RebalanceEventType et = + (err == RD_KAFKA_RESP_ERR__ASSIGN_PARTITIONS ? RebalanceEventType::PartitionsAssigned : RebalanceEventType::PartitionsRevoked); + _rebalanceCb(et, tps); + } +} + +// Rebalance Callback (for librdkafka) +inline void +KafkaConsumer::rebalanceCallback(rd_kafka_t* rk, rd_kafka_resp_err_t err, rd_kafka_topic_partition_list_t* partitions, void* /* opaque */) +{ + KafkaClient& client = kafkaClient(rk); + auto& consumer = dynamic_cast(client); + consumer.onRebalance(err, partitions); +} + +// Offset Commit Callback (for librdkafka) +inline void +KafkaConsumer::offsetCommitCallback(rd_kafka_t* rk, rd_kafka_resp_err_t err, rd_kafka_topic_partition_list_t* rk_tpos, void* opaque) +{ + TopicPartitionOffsets tpos = getTopicPartitionOffsets(rk_tpos); + + if (err != RD_KAFKA_RESP_ERR_NO_ERROR) + { + auto tposStr = toString(tpos); + kafkaClient(rk).KAFKA_API_DO_LOG(LOG_ERR, "invoked offset-commit callback. offsets[%s], result[%s]", tposStr.c_str(), rd_kafka_err2str(err)); + } + + auto* cb = static_cast(opaque); + if (cb && *cb) + { + (*cb)(tpos, ErrorCode(err)); + } + delete cb; +} + +/** + * Automatic-Commit consumer. + * Whenever you poll, the consumer checks if it is time to commit, and if it is, it will commit the offsets it returned in the last poll. + */ +class KafkaAutoCommitConsumer: public KafkaConsumer +{ +public: + /** + * The constructor for KafkaAutoCommitConsumer. + * Throws KafkaException with errors: + * - RD_KAFKA_RESP_ERR__INVALID_ARG: Invalid BOOTSTRAP_SERVERS property + * - RD_KAFKA_RESP_ERR__CRIT_SYS_RESOURCE: Fail to create internal threads + */ + explicit KafkaAutoCommitConsumer(const Properties& properties) + : KafkaConsumer(KafkaAutoCommitConsumer::validateAndReformProperties(properties), OffsetCommitOption::Auto) + { + } + + ~KafkaAutoCommitConsumer() override { if (_opened) close(); } + + /** + * Close the consumer, waiting for any needed cleanup. + */ + void close() + { + KafkaConsumer::close(); + } + +private: + // Validate properties (and fix it if necesary) + static Properties validateAndReformProperties(const Properties& origProperties) + { + // Let the base class validate first + Properties properties = KafkaConsumer::validateAndReformProperties(origProperties); + + // Don't "auto-store" offsets (librdkafka's configuration) + properties.put(ENABLE_AUTO_OFFSET_STORE, "false"); + + return properties; + } +}; + +/** + * Manual-Commit consumer. + * User must use commitSync/commitAsync to commit the offsets manually. + */ +class KafkaManualCommitConsumer: public KafkaConsumer +{ +public: + /** + * The constructor for KafkaManualCommitConsumer. + * + * Options: + * - EventsPollingOption::Auto (default) : An internal thread would be started for OffsetCommit callbacks handling. + * - EventsPollingOption::Maunal : User have to call the member function `pollEvents()` to trigger OffsetCommit callbacks. + * + * Throws KafkaException with errors: + * - RD_KAFKA_RESP_ERR__INVALID_ARG : Invalid BOOTSTRAP_SERVERS property + * - RD_KAFKA_RESP_ERR__CRIT_SYS_RESOURCE: Fail to create internal threads + */ + explicit KafkaManualCommitConsumer(const Properties& properties, + EventsPollingOption pollOption = EventsPollingOption::Auto) + : KafkaConsumer(KafkaManualCommitConsumer::validateAndReformProperties(properties), OffsetCommitOption::Manual) + { + _rk_commit_cb_queue.reset(rd_kafka_queue_new(getClientHandle())); + + _pollable = std::make_unique>(this, pollCallbacks); + if (pollOption == EventsPollingOption::Auto) + { + _pollThread = std::make_unique(*_pollable); + } + } + + ~KafkaManualCommitConsumer() override { if (_opened) close(); } + + /** + * Close the consumer, waiting for any needed cleanup. + */ + void close() + { + _pollThread.reset(); // Join the polling thread (in case it's running) + _pollable.reset(); + + KafkaConsumer::close(); + + rd_kafka_queue_t* queue = getCommitCbQueue(); + while (rd_kafka_queue_length(queue)) + { + rd_kafka_queue_poll_callback(queue, TIMEOUT_INFINITE); + } + } + + /** + * Commit offsets returned on the last poll() for all the subscribed list of topics and partitions. + */ + void commitSync(); + /** + * Commit the specified offsets for the specified records + */ + void commitSync(const ConsumerRecord& record); + /** + * Commit the specified offsets for the specified list of topics and partitions. + */ + void commitSync(const TopicPartitionOffsets& tpos); + /** + * Commit offsets returned on the last poll() for all the subscribed list of topics and partition. + * Note: If a callback is provided, it's guaranteed to be triggered (before closing the consumer). + */ + void commitAsync(const Consumer::OffsetCommitCallback& cb = OffsetCommitCallback()); + /** + * Commit the specified offsets for the specified records + * Note: If a callback is provided, it's guaranteed to be triggered (before closing the consumer). + */ + void commitAsync(const ConsumerRecord& record, const Consumer::OffsetCommitCallback& cb = OffsetCommitCallback()); + /** + * Commit the specified offsets for the specified list of topics and partitions to Kafka. + * Note: If a callback is provided, it's guaranteed to be triggered (before closing the consumer). + */ + void commitAsync(const TopicPartitionOffsets& tpos, const Consumer::OffsetCommitCallback& cb = OffsetCommitCallback()); + + /** + * Call the OffsetCommit callbacks (if any) + * Note: The KafkaManualCommitConsumer MUST be constructed with option `EventsPollingOption::Manual`. + */ + void pollEvents(std::chrono::milliseconds timeout) + { + assert(!_pollThread); + + _pollable->poll(convertMsDurationToInt(timeout)); + } + +private: + rd_kafka_queue_t* getCommitCbQueue() { return _rk_commit_cb_queue.get(); } + + rd_kafka_queue_unique_ptr _rk_commit_cb_queue; + + std::unique_ptr _pollable; + std::unique_ptr _pollThread; + + static void pollCallbacks(KafkaManualCommitConsumer* consumer, int timeoutMs) + { + rd_kafka_queue_t* queue = consumer->getCommitCbQueue(); + rd_kafka_queue_poll_callback(queue, timeoutMs); + } + +private: + // Validate properties (and fix it if necesary) + static Properties validateAndReformProperties(const Properties& origProperties) + { + // Let the base class validate first + Properties properties = KafkaConsumer::validateAndReformProperties(origProperties); + + // Automatically store offset of last message provided to application + properties.put(ENABLE_AUTO_OFFSET_STORE, "true"); + + return properties; + } +}; + +inline void +KafkaManualCommitConsumer::commitSync() +{ + commit(TopicPartitionOffsets(), CommitType::Sync); +} + +inline void +KafkaManualCommitConsumer::commitSync(const ConsumerRecord& record) +{ + TopicPartitionOffsets tpos; + // committed offset should be "current-received-offset + 1" + tpos[TopicPartition(record.topic(), record.partition())] = record.offset() + 1; + + commit(tpos, CommitType::Sync); +} + +inline void +KafkaManualCommitConsumer::commitSync(const TopicPartitionOffsets& tpos) +{ + commit(tpos, CommitType::Sync); +} + +inline void +KafkaManualCommitConsumer::commitAsync(const TopicPartitionOffsets& tpos, const OffsetCommitCallback& cb) +{ + auto rk_tpos = rd_kafka_topic_partition_list_unique_ptr(tpos.empty() ? nullptr : createRkTopicPartitionList(tpos)); + + rd_kafka_resp_err_t err = rd_kafka_commit_queue(getClientHandle(), rk_tpos.get(), getCommitCbQueue(), &KafkaConsumer::offsetCommitCallback, new OffsetCommitCallback(cb)); + KAFKA_THROW_IF_WITH_ERROR(err); +} + +inline void +KafkaManualCommitConsumer::commitAsync(const ConsumerRecord& record, const OffsetCommitCallback& cb) +{ + TopicPartitionOffsets tpos; + // committed offset should be "current received record's offset" + 1 + tpos[TopicPartition(record.topic(), record.partition())] = record.offset() + 1; + commitAsync(tpos, cb); +} + +inline void +KafkaManualCommitConsumer::commitAsync(const OffsetCommitCallback& cb) +{ + commitAsync(TopicPartitionOffsets(), cb); +} + +} // end of KAFKA_API + diff --git a/include/kafka/KafkaException.h b/include/kafka/KafkaException.h new file mode 100644 index 000000000..58840d8cc --- /dev/null +++ b/include/kafka/KafkaException.h @@ -0,0 +1,52 @@ +#pragma once + +#include "kafka/Project.h" + +#include "kafka/Error.h" +#include "kafka/Utility.h" + +#include "librdkafka/rdkafka.h" + +#include +#include + + +namespace KAFKA_API { + +/** + * Specific exception for Kafka clients. + */ +class KafkaException: public std::exception +{ +public: + KafkaException(const char* filename, int lineno, std::error_code ec, const std::string& errMsg) + : _errCode(ec) + { + _errMsg = Utility::getCurrentTime() + ": " + errMsg + " (" + std::string(filename) + ":" + std::to_string(lineno) + ")"; + } + + KafkaException(const char* filename, int lineno, std::error_code ec) + : KafkaException(filename, lineno, ec, ec.message()) + {} + + /** + * Obtains the underlying error code. + */ + std::error_code error() const { return _errCode; } + + /** + * Obtains explanatory string. + */ + const char* what() const noexcept override { return _errMsg.c_str(); } + +private: + std::string _errMsg; + std::error_code _errCode; +}; + +#define KAFKA_THROW(respErr) throw KafkaException(__FILE__, __LINE__, ErrorCode(respErr)) +#define KAFKA_THROW_WITH_MSG(respErr, ...) throw KafkaException(__FILE__, __LINE__, ErrorCode(respErr), __VA_ARGS__) +#define KAFKA_THROW_IF_WITH_ERROR(respErr) if (respErr != RD_KAFKA_RESP_ERR_NO_ERROR) KAFKA_THROW(respErr) + +} // end of KAFKA_API + diff --git a/include/kafka/KafkaProducer.h b/include/kafka/KafkaProducer.h new file mode 100644 index 000000000..d3be6f4eb --- /dev/null +++ b/include/kafka/KafkaProducer.h @@ -0,0 +1,665 @@ +#pragma once + +#include "kafka/Project.h" + +#include "kafka/KafkaClient.h" +#include "kafka/ProducerConfig.h" +#include "kafka/ProducerRecord.h" +#include "kafka/Timestamp.h" +#include "kafka/Types.h" + +#include "librdkafka/rdkafka.h" + +#include +#include +#include +#include +#include + +namespace KAFKA_API { + +/** + * Namespace for datatypes defined for KafkaProducer. + */ +namespace Producer +{ + /** + * The metadata for a record that has been acknowledged by the server. + */ + class RecordMetadata + { + public: + enum class PersistedStatus { Not, Possibly, Done }; + + // This is only called by the KafkaProducer::deliveryCallback (with a valid rkmsg pointer) + RecordMetadata(const rd_kafka_message_t* rkmsg, ProducerRecord::Id recordId) + : _cachedInfo(), + _rkmsg(rkmsg), + _recordId(recordId) + { + } + + RecordMetadata(const RecordMetadata& another) + : _cachedInfo(std::make_unique(another.topic(), + another.partition(), + another.offset() ? *another.offset() : RD_KAFKA_OFFSET_INVALID, + another.keySize(), + another.valueSize(), + another.timestamp(), + another.persistedStatus())), + _rkmsg(nullptr), + _recordId(another._recordId) + { + } + + /** + * The topic the record was appended to. + */ + std::string topic() const + { + return _rkmsg ? (_rkmsg->rkt ? rd_kafka_topic_name(_rkmsg->rkt) : "") : _cachedInfo->topic; + } + + /** + * The partition the record was sent to. + */ + Partition partition() const + { + return _rkmsg ? _rkmsg->partition : _cachedInfo->partition; + } + + /** + * The offset of the record in the topic/partition. + */ + Optional offset() const + { + auto offset = _rkmsg ? _rkmsg->offset : _cachedInfo->offset; + return (offset != RD_KAFKA_OFFSET_INVALID) ? Optional(offset) : Optional(); + } + + /** + * The recordId could be used to identify the acknowledged message. + */ + ProducerRecord::Id recordId() const + { + return _recordId; + } + + /** + * The size of the key in bytes. + */ + KeySize keySize() const + { + return _rkmsg ? _rkmsg->key_len : _cachedInfo->keySize; + } + + /** + * The size of the value in bytes. + */ + ValueSize valueSize() const + { + return _rkmsg ? _rkmsg->len : _cachedInfo->valueSize; + } + + /** + * The timestamp of the record in the topic/partition. + */ + Timestamp timestamp() const + { + return _rkmsg ? getMsgTimestamp(_rkmsg) : _cachedInfo->timestamp; + } + + /** + * The persisted status of the record. + */ + PersistedStatus persistedStatus() const + { + return _rkmsg ? getMsgPersistedStatus(_rkmsg) : _cachedInfo->persistedStatus; + } + + std::string persistedStatusString() const + { + return getPersistedStatusString(persistedStatus()); + } + + std::string toString() const + { + return topic() + "-" + std::to_string(partition()) + "@" + (offset() ? std::to_string(*offset()) : "NA") + + ":id[" + std::to_string(recordId()) + "]," + timestamp().toString() + "," + persistedStatusString(); + } + + private: + static Timestamp getMsgTimestamp(const rd_kafka_message_t* rkmsg) + { + rd_kafka_timestamp_type_t tstype; + Timestamp::Value tsValue = rd_kafka_message_timestamp(rkmsg, &tstype); + return {tsValue, tstype}; + } + + static PersistedStatus getMsgPersistedStatus(const rd_kafka_message_t* rkmsg) + { + rd_kafka_msg_status_t status = rd_kafka_message_status(rkmsg); + return status == RD_KAFKA_MSG_STATUS_NOT_PERSISTED ? PersistedStatus::Not : (status == RD_KAFKA_MSG_STATUS_PERSISTED ? PersistedStatus::Done : PersistedStatus::Possibly); + } + + static std::string getPersistedStatusString(PersistedStatus status) + { + return status == PersistedStatus::Not ? "NotPersisted" : + (status == PersistedStatus::Done ? "Persisted" : "PossiblyPersisted"); + } + + struct CachedInfo + { + CachedInfo(Topic t, Partition p, Offset o, KeySize ks, ValueSize vs, Timestamp ts, PersistedStatus pst) + : topic(std::move(t)), + partition(p), + offset(o), + keySize(ks), + valueSize(vs), + timestamp(ts), + persistedStatus(pst) + { + } + + CachedInfo(const CachedInfo&) = default; + + std::string topic; + Partition partition; + Offset offset; + KeySize keySize; + ValueSize valueSize; + Timestamp timestamp; + PersistedStatus persistedStatus; + }; + + const std::unique_ptr _cachedInfo; + const rd_kafka_message_t* _rkmsg; + const ProducerRecord::Id _recordId; + }; + + /** + * A callback method could be used to provide asynchronous handling of request completion. + * This method will be called when the record sent (by KafkaAsyncProducer) to the server has been acknowledged. + */ + using Callback = std::function; +} + + +/** + * The base class for KafkaAsyncProducer and KafkaSyncProducer. + */ +class KafkaProducer: public KafkaClient +{ +public: + /** + * Invoking this method makes all buffered records immediately available to send, and blocks on the completion of the requests associated with these records. + * + * Possible errors: + * - RD_KAFKA_RESP_ERR__TIMED_OUT: The `timeout` was reached before all outstanding requests were completed. + */ + std::error_code flush(std::chrono::milliseconds timeout = std::chrono::milliseconds::max()); + + enum class SendOption { NoCopyRecordValue, ToCopyRecordValue }; + +protected: + explicit KafkaProducer(const Properties& properties) + : KafkaClient(ClientType::KafkaProducer, properties, registerConfigCallbacks) + { + auto propStr = properties.toString(); + KAFKA_API_DO_LOG(LOG_INFO, "initializes with properties[%s]", propStr.c_str()); + } std::error_code close(std::chrono::milliseconds timeout); + + // Define datatypes for "opaque" (as a parameter of rd_kafka_produce), in order to implement the callback(async) or to return future(sync) + class MsgOpaque + { + public: + explicit MsgOpaque(ProducerRecord::Id id): _recordId(id) {} + virtual ~MsgOpaque() = default; + virtual void operator()(rd_kafka_t* rk, const rd_kafka_message_t* rkmsg) = 0; + protected: + ProducerRecord::Id _recordId; + }; + + class MsgCallbackOpaque: public MsgOpaque + { + public: + MsgCallbackOpaque(ProducerRecord::Id id, Producer::Callback cb): MsgOpaque(id), _drCb(std::move(cb)) {} + + void operator()(rd_kafka_t* /*rk*/, const rd_kafka_message_t* rkmsg) override + { + if (_drCb) + { + Producer::RecordMetadata metadata(rkmsg, _recordId); + _drCb(metadata, ErrorCode(rkmsg->err)); + } + } + + private: + Producer::Callback _drCb; + }; + + class MsgPromiseOpaque: public MsgOpaque + { + public: + using ResultType = std::pair; + + explicit MsgPromiseOpaque(ProducerRecord::Id id): MsgOpaque(id) {} + + void operator()(rd_kafka_t* /*rk*/, const rd_kafka_message_t* rkmsg) override + { + Producer::RecordMetadata metadata(rkmsg, _recordId); + _promMetadata.set_value(ResultType(ErrorCode(rkmsg->err), metadata)); + } + + std::future getFuture() { return _promMetadata.get_future(); } + + private: + std::promise _promMetadata; + }; + + enum class ActionWhileQueueIsFull { Block, NoBlock }; + + rd_kafka_resp_err_t sendMessage(const ProducerRecord& record, + std::unique_ptr opaque, + SendOption option, + ActionWhileQueueIsFull action); + + static constexpr int CALLBACK_POLLING_INTERVAL_MS = 10; + + // Validate properties (and fix it if necesary) + static Properties validateAndReformProperties(const Properties& origProperties); + + // Delivery Callback (for librdkafka) + static void deliveryCallback(rd_kafka_t* rk, const rd_kafka_message_t* rkmsg, void* opaque); + + // Register Callbacks for rd_kafka_conf_t + static void registerConfigCallbacks(rd_kafka_conf_t* conf); + +#ifdef KAFKA_API_ENABLE_UNIT_TEST_STUBS +public: + using HandleProduceResponseCb = std::function; + + /** + * Stub for ProduceResponse handing. + * Note: Only for internal unit tests + */ + void stubHandleProduceResponse(HandleProduceResponseCb cb = HandleProduceResponseCb()) { _handleProduceRespCb = cb; } + +private: + static rd_kafka_resp_err_t handleProduceResponse(rd_kafka_t* rk, int32_t brokerId, uint64_t msgSeq, rd_kafka_resp_err_t err) + { + KafkaClient* client = static_cast(rd_kafka_opaque(rk)); + KafkaProducer* producer = dynamic_cast(client); + auto respCb = producer->_handleProduceRespCb; + return respCb ? respCb(rk, brokerId, msgSeq, err) : err; + } + + HandleProduceResponseCb _handleProduceRespCb; +#endif +}; + +inline void +KafkaProducer::registerConfigCallbacks(rd_kafka_conf_t* conf) +{ + // Delivery Callback + rd_kafka_conf_set_dr_msg_cb(conf, deliveryCallback); + +#ifdef KAFKA_API_ENABLE_UNIT_TEST_STUBS + // UT stub for ProduceResponse + LogBuffer errInfo; + if (rd_kafka_conf_set(conf, "ut_handle_ProduceResponse", reinterpret_cast(&handleProduceResponse), errInfo.str(), errInfo.capacity())) + { + KafkaClient* client = nullptr; + size_t clientPtrSize = 0; + if (rd_kafka_conf_get(conf, "opaque", reinterpret_cast(&client), &clientPtrSize)) + { + LOG(LOG_CRIT, "failed to stub ut_handle_ProduceResponse! error[%s]. Meanwhile, failed to get the Kafka client!", errInfo.c_str()); + } + else + { + assert(clientPtrSize == sizeof(client)); + client->KAFKA_API_DO_LOG(LOG_ERR, "failed to stub ut_handle_ProduceResponse! error[%s]", errInfo.c_str()); + } + } +#endif +} + +inline Properties +KafkaProducer::validateAndReformProperties(const Properties& origProperties) +{ + // Let the base class validate first + Properties properties = KafkaClient::validateAndReformProperties(origProperties); + + // By default, we'd use an equvilent partitioner to Java Producer's. + const std::set availPartitioners = {"murmur2_random", "murmur2", "random", "consistent", "consistent_random", "fnv1a", "fnv1a_random"}; + auto partitioner = properties.getProperty(ProducerConfig::PARTITIONER); + if (!partitioner) + { + properties.put(ProducerConfig::PARTITIONER, "murmur2_random"); + } + else if (!availPartitioners.count(*partitioner)) + { + std::string errMsg = "Invalid partitioner [" + *partitioner + "]! Valid options: "; + bool isTheFirst = true; + for (const auto& availPartitioner: availPartitioners) + { + errMsg += (std::string(isTheFirst ? (isTheFirst = false, "") : ", ") + availPartitioner); + } + errMsg += "."; + + KAFKA_THROW_WITH_MSG(RD_KAFKA_RESP_ERR__INVALID_ARG, errMsg); + } + + // For "idempotence" feature + constexpr int KAFKA_IDEMP_MAX_INFLIGHT = 5; + const auto enableIdempotence = properties.getProperty(ProducerConfig::ENABLE_IDEMPOTENCE); + if (enableIdempotence && *enableIdempotence == "true") + { + if (const auto maxInFlight = properties.getProperty(ProducerConfig::MAX_IN_FLIGHT)) + { + if (std::stoi(*maxInFlight) > KAFKA_IDEMP_MAX_INFLIGHT) + { + KAFKA_THROW_WITH_MSG(RD_KAFKA_RESP_ERR__INVALID_ARG,\ + "`max.in.flight` must be set <= " + std::to_string(KAFKA_IDEMP_MAX_INFLIGHT) + " when `enable.idempotence` is `true`"); + } + } + + if (const auto acks = properties.getProperty(ProducerConfig::ACKS)) + { + if (*acks != "all" && *acks != "-1") + { + KAFKA_THROW_WITH_MSG(RD_KAFKA_RESP_ERR__INVALID_ARG,\ + "`acks` must be set to `all`/`-1` when `enable.idempotence` is `true`"); + } + } + } + + return properties; +} + +// Delivery Callback (for librdkafka) +inline void +KafkaProducer::deliveryCallback(rd_kafka_t* rk, const rd_kafka_message_t* rkmsg, void* /*opaque*/) +{ + if (auto* msgOpaque = static_cast(rkmsg->_private)) + { + (*msgOpaque)(rk, rkmsg); + delete msgOpaque; + } +} + +inline rd_kafka_resp_err_t +KafkaProducer::sendMessage(const ProducerRecord& record, + std::unique_ptr opaque, + SendOption option, + ActionWhileQueueIsFull action) +{ + auto rk = getClientHandle(); + auto topic = record.topic().c_str(); + auto partition = record.partition(); + auto msgFlags = (static_cast(option == SendOption::ToCopyRecordValue ? RD_KAFKA_MSG_F_COPY : 0) + | static_cast(action == ActionWhileQueueIsFull::Block ? RD_KAFKA_MSG_F_BLOCK : 0)); + auto keyPtr = record.key().data(); + auto keyLen = record.key().size(); + auto valuePtr = record.value().data(); + auto valueLen = record.value().size(); + auto opaquePtr = opaque.get(); + + rd_kafka_resp_err_t sendResult = RD_KAFKA_RESP_ERR_NO_ERROR; + + if (auto cntHeaders = record.headers().size()) + { + rd_kafka_headers_t* hdrs = rd_kafka_headers_new(cntHeaders); + for (const auto& header: record.headers()) + { + rd_kafka_header_add(hdrs, header.key.c_str(), header.key.size(), header.value.data(), header.value.size()); + } + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wold-style-cast" + sendResult = rd_kafka_producev(rk, + RD_KAFKA_V_TOPIC(topic), + RD_KAFKA_V_PARTITION(partition), + RD_KAFKA_V_MSGFLAGS(msgFlags), + RD_KAFKA_V_HEADERS(hdrs), + RD_KAFKA_V_VALUE(const_cast(valuePtr), valueLen), // NOLINT + RD_KAFKA_V_KEY(keyPtr, keyLen), + RD_KAFKA_V_OPAQUE(opaquePtr), + RD_KAFKA_V_END); +#pragma GCC diagnostic pop + if (sendResult != RD_KAFKA_RESP_ERR_NO_ERROR) + { + rd_kafka_headers_destroy(hdrs); + } + } + else + { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wold-style-cast" + sendResult = rd_kafka_producev(rk, + RD_KAFKA_V_TOPIC(topic), + RD_KAFKA_V_PARTITION(partition), + RD_KAFKA_V_MSGFLAGS(msgFlags), + RD_KAFKA_V_VALUE(const_cast(valuePtr), valueLen), // NOLINT + RD_KAFKA_V_KEY(keyPtr, keyLen), + RD_KAFKA_V_OPAQUE(opaquePtr), + RD_KAFKA_V_END); +#pragma GCC diagnostic pop + } + + if (sendResult == RD_KAFKA_RESP_ERR_NO_ERROR) + { + // KafkaProducer::deliveryCallback would delete the "opaque" + opaque.release(); + } + + return sendResult; // NOLINT: leak of memory pointed to by 'opaquePtr' [clang-analyzer-cplusplus.NewDeleteLeaks] +} + +inline std::error_code +KafkaProducer::flush(std::chrono::milliseconds timeout) +{ + return ErrorCode(rd_kafka_flush(getClientHandle(), convertMsDurationToInt(timeout))); +} + +inline std::error_code +KafkaProducer::close(std::chrono::milliseconds timeout) +{ + _opened = false; + + std::error_code ec = flush(timeout); + + std::string errMsg = ec.message(); + KAFKA_API_DO_LOG(LOG_INFO, "closed [%s]", errMsg.c_str()); + + return ec; +} + + +/** + * A Kafka client that publishes records to the Kafka cluster asynchronously. + */ +class KafkaAsyncProducer: public KafkaProducer +{ +public: + /** + * The constructor for KafkaAsyncProducer. + * + * Options: + * - EventsPollingOption::Auto (default) : An internal thread would be started for MessageDelivery callbacks handling. + * - EventsPollingOption::Manual : User have to call the member function `pollEvents()` to trigger MessageDelivery callbacks. + * + * Throws KafkaException with errors: + * - RD_KAFKA_RESP_ERR__INVALID_ARG : Invalid BOOTSTRAP_SERVERS property + * - RD_KAFKA_RESP_ERR__CRIT_SYS_RESOURCE: Fail to create internal threads + */ + explicit KafkaAsyncProducer(const Properties& properties, + EventsPollingOption pollOption = EventsPollingOption::Auto) + : KafkaProducer(KafkaProducer::validateAndReformProperties(properties)) + { + _pollable = std::make_unique>(this, pollCallbacks); + if (pollOption == EventsPollingOption::Auto) + { + _pollThread = std::make_unique(*_pollable); + } + } + + ~KafkaAsyncProducer() override { if (_opened) close(); } + + /** + * Close this producer. This method waits up to timeout for the producer to complete the sending of all incomplete requests. + */ + std::error_code close(std::chrono::milliseconds timeout = std::chrono::milliseconds::max()) + { + _pollThread.reset(); // Join the polling thread (in case it's running) + _pollable.reset(); + + return KafkaProducer::close(timeout); + } + + /** + * Asynchronously send a record to a topic. + * + * Note: + * - If a callback is provided, it's guaranteed to be triggered (before closing the producer). + * - If any error occured, an exception would be thrown. + * - Make sure the memory block (for ProducerRecord's value) is valid until the delivery callback finishes; Otherwise, should be with option `KafkaProducer::SendOption::ToCopyRecordValue`. + * + * Possible errors: + * Local errors, + * - RD_KAFKA_RESP_ERR__UNKNOWN_TOPIC: The topic doesn't exist + * - RD_KAFKA_RESP_ERR__UNKNOWN_PARTITION: The partition doesn't exist + * - RD_KAFKA_RESP_ERR__INVALID_ARG: Invalid topic(topic is null, or the length is too long (> 512) + * - RD_KAFKA_RESP_ERR__MSG_TIMED_OUT: No ack received within the time limit + * - RD_KAFKA_RESP_ERR__QUEUE_FULL: The message buffing queue is full + * Broker errors, + * - [Error Codes] (https://cwiki.apache.org/confluence/display/KAFKA/A+Guide+To+The+Kafka+Protocol#AGuideToTheKafkaProtocol-ErrorCodes) + */ + void send(const ProducerRecord& record, const Producer::Callback& cb, SendOption option = SendOption::NoCopyRecordValue) + { + rd_kafka_resp_err_t respErr = sendMessage(record, + std::make_unique(record.id(), cb), + option, + _pollThread ? ActionWhileQueueIsFull::Block : ActionWhileQueueIsFull::NoBlock); + KAFKA_THROW_IF_WITH_ERROR(respErr); + } + + /** + * Asynchronously send a record to a topic. + * + * Note: + * - If a callback is provided, it's guaranteed to be triggered (before closing the producer). + * - The input reference parameter `error` will be set if an error occurred. + * - Make sure the memory block (for ProducerRecord's value) is valid until the delivery callback finishes; Otherwise, should be with option `KafkaProducer::SendOption::ToCopyRecordValue`. + * + * Possible errors: + * Local errors, + * - RD_KAFKA_RESP_ERR__UNKNOWN_TOPIC: The topic doesn't exist + * - RD_KAFKA_RESP_ERR__UNKNOWN_PARTITION: The partition doesn't exist + * - RD_KAFKA_RESP_ERR__INVALID_ARG: Invalid topic(topic is null, or the length is too long (> 512) + * - RD_KAFKA_RESP_ERR__MSG_TIMED_OUT: No ack received within the time limit + * - RD_KAFKA_RESP_ERR__QUEUE_FULL: The message buffing queue is full + * Broker errors, + * - [Error Codes] (https://cwiki.apache.org/confluence/display/KAFKA/A+Guide+To+The+Kafka+Protocol#AGuideToTheKafkaProtocol-ErrorCodes) + */ + void send(const ProducerRecord& record, const Producer::Callback& cb, std::error_code& ec, SendOption option = SendOption::NoCopyRecordValue) + { + rd_kafka_resp_err_t respErr = sendMessage(record, + std::make_unique(record.id(), cb), + option, + _pollThread ? ActionWhileQueueIsFull::Block : ActionWhileQueueIsFull::NoBlock); + ec = ErrorCode(respErr); + } + + /** + * Call the MessageDelivery callbacks (if any) + * Note: The KafkaAsyncProducer MUST be constructed with option `EventsPollingOption::Manual`. + */ + void pollEvents(std::chrono::milliseconds timeout) + { + assert(!_pollThread); + + _pollable->poll(convertMsDurationToInt(timeout)); + } + +private: + std::unique_ptr _pollable; + std::unique_ptr _pollThread; + + static void pollCallbacks(KafkaAsyncProducer* producer, int timeoutMs) + { + rd_kafka_poll(producer->getClientHandle(), timeoutMs); + } +}; + +/** + * A Kafka client that publishes records to the Kafka cluster asynchronously. + */ +class KafkaSyncProducer: public KafkaProducer +{ +public: + /** + * The constructor for KafkaSyncProducer. + * Throws KafkaException with errors: + * - RD_KAFKA_RESP_ERR__INVALID_ARG: Invalid BOOTSTRAP_SERVERS property + * - RD_KAFKA_RESP_ERR__CRIT_SYS_RESOURCE: Fail to create internal threads + */ + explicit KafkaSyncProducer(const Properties& properties) + : KafkaProducer(KafkaSyncProducer::validateAndReformProperties(properties)) + { + } + + ~KafkaSyncProducer() override { if (_opened) close(); } + + /** + * Synchronously send a record to a topic. + * Throws KafkaException with errors: + * Local errors, + * - RD_KAFKA_RESP_ERR__UNKNOWN_TOPIC: The topic doesn't exist + * - RD_KAFKA_RESP_ERR__UNKNOWN_PARTITION: The partition doesn't exist + * - RD_KAFKA_RESP_ERR__INVALID_ARG: Invalid topic(topic is null, or the length is too long (> 512) + * - RD_KAFKA_RESP_ERR__MSG_TIMED_OUT: No ack received within the time limit + * Broker errors, + * - [Error Codes] (https://cwiki.apache.org/confluence/display/KAFKA/A+Guide+To+The+Kafka+Protocol#AGuideToTheKafkaProtocol-ErrorCodes) + */ + Producer::RecordMetadata send(const ProducerRecord& record) + { + auto opaque = std::make_unique(record.id()); + auto fut = opaque->getFuture(); + + rd_kafka_resp_err_t err = sendMessage(record, std::move(opaque), SendOption::ToCopyRecordValue, ActionWhileQueueIsFull::Block); + KAFKA_THROW_IF_WITH_ERROR(err); + + while (fut.wait_for(std::chrono::milliseconds(0)) != std::future_status::ready) + { + rd_kafka_poll(getClientHandle(), CALLBACK_POLLING_INTERVAL_MS); + } + + auto result = fut.get(); + KAFKA_THROW_IF_WITH_ERROR(static_cast(result.first.value())); + + return result.second; + } + + /** + * Close this producer. This method waits up to timeout for the producer to complete the sending of all incomplete requests. + */ + std::error_code close(std::chrono::milliseconds timeout = std::chrono::milliseconds::max()) + { + return KafkaProducer::close(timeout); + } +private: + static Properties validateAndReformProperties(const Properties& origProperties) + { + // Let the base class validate first + Properties properties = KafkaProducer::validateAndReformProperties(origProperties); + + // KafkaSyncProducer sends only one message each time, -- no need to wait for batching + properties.put(ProducerConfig::LINGER_MS, "0"); + + return properties; + } +}; + +} // end of KAFKA_API + diff --git a/include/kafka/Logger.h b/include/kafka/Logger.h new file mode 100644 index 000000000..8eb25e166 --- /dev/null +++ b/include/kafka/Logger.h @@ -0,0 +1,65 @@ +#pragma once + +#include "kafka/Project.h" + +#include "kafka/Utility.h" + +#include +#include +#include + +namespace KAFKA_API { + +template +class LogBuffer +{ +public: + LogBuffer():_wptr(_buf) { _buf[0] = 0; } // NOLINT + + LogBuffer& clear() + { + _wptr = _buf; + _buf[0] = 0; + return *this; + } + + template + LogBuffer& print(const char* format, Args... args) + { + assert(!(_buf[0] != 0 && _wptr == _buf)); // means it has already been used as a plain buffer (with `str()`) + + auto cnt = std::snprintf(_wptr, capacity(), format, args...); // returns number of characters written if successful (not including '\0') + if (cnt > 0) + { + _wptr = std::min(_wptr + cnt, _buf + MAX_CAPACITY - 1); + } + return *this; + } + LogBuffer& print(const char* format) { return print("%s", format); } + + int capacity() const { return _buf + MAX_CAPACITY - _wptr; } + char* str() { return _buf; } + const char* c_str() const { return _buf; } + +private: + char* _wptr; + char _buf[MAX_CAPACITY]; +}; + +using Logger = std::function; + +inline void DefaultLogger(int level, const char* /*filename*/, int /*lineno*/, const char* msg) +{ + const char levelNames[][10] = {"EMERG", "ALERT", "CRIT", "ERR", "WARNING", "NOTICE", "INFO", "DEBUG", "INVALID"}; + constexpr int INVALID_LEVEL = sizeof(levelNames)/sizeof(levelNames[0]) - 1; + const char* levelName = level < INVALID_LEVEL ? levelNames[level] : levelNames[INVALID_LEVEL]; + std::cout << "[" << Utility::getCurrentTime() << "]" << levelName << " " << msg; + std::cout << std::endl; +} + +inline void NoneLogger(int /*level*/, const char* /*filename*/, int /*lineno*/, const char* /*msg*/) +{ +} + +} // end of KAFKA_API + diff --git a/include/kafka/ProducerConfig.h b/include/kafka/ProducerConfig.h new file mode 100644 index 000000000..1eb675b58 --- /dev/null +++ b/include/kafka/ProducerConfig.h @@ -0,0 +1,133 @@ +#pragma once + +#include "kafka/Project.h" + +#include "kafka/Types.h" + + +namespace KAFKA_API { + +/** + * Configuration for the Kafka Producer. + */ +class ProducerConfig: public Properties +{ +public: + ProducerConfig() = default; + ProducerConfig(const ProducerConfig&) = default; + explicit ProducerConfig(const PropertiesMap& kvMap): Properties(kvMap) {} + + /** + * The string contains host:port pairs of brokers (splitted by ",") that the producer will use to establish initial connection to the Kafka cluster. + * Note: It's mandatory. + */ + static const constexpr char* BOOTSTRAP_SERVERS = "bootstrap.servers"; + + /** + * This can be any string, and will be used by the brokers to identify messages sent from the client. + */ + static const constexpr char* CLIENT_ID = "client.id"; + + /** + * The acks parameter controls how many partition replicas must receive the record before the producer can consider the write successful. + * 1) acks=0, the producer will not wait for a reply from the broker before assuming the message was sent successfully. + * 2) acks=1, the producer will receive a success response from the broker the moment the leader replica received the message. + * 3) acks=all, the producer will receive a success response from the broker once all in-sync replicas received the message. + * Note: if "ack=all", please make sure the topic's replication factor be larger than 1. + * That means, if the topic is automaticly created by producer's `send`, the `default.replication.factor` property for the kafka server should be larger than 1. + * The "ack=all" property is mandatory for reliability requirements, but would increase the ack latency and impact the throughput. + * Default value: all + */ + static const constexpr char* ACKS = "acks"; + + /** + * Maximum number of messages allowed on the producer queue. + * Default value: 100000 + */ + static const constexpr char* QUEUE_BUFFERING_MAX_MESSAGES = "queue.buffering.max.messages"; + + /** + * Maximum total message size sum allowed on the producer queue. + * Default value: 0x100000 (1GB) + */ + static const constexpr char* QUEUE_BUFFERING_MAX_KBYTES = "queue.buffering.max.kbytes"; + + /** + * Delay in milliseconds to wait for messages in the producer queue, to accumulate before constructing messages batches to transmit to brokers. + * Default value: 0 (KafkaSyncProducer); 0.5 (KafkaAsyncProducer) + */ + static const constexpr char* LINGER_MS = "linger.ms"; + + /** + * Maximum number of messages batched in one messageSet. The total MessageSet size is also limited by MESSAGE_MAX_BYTES. + * Default value: 10000 + */ + static const constexpr char* BATCH_NUM_MESSAGES = "batch.num.messages"; + + /** + * Maximum Kafka protocol request message size. + * Note: Should be coordinated with the bokers's configuration. Otherwise, any larger message would be rejected! + * Default value: 1000000 + */ + static const constexpr char* MESSAGE_MAX_BYTES = "message.max.bytes"; + + /** + * This value is enforced locally and limits the time a produced message waits for successful delivery. + * Note: If failed to get the ack within this limit, an exception would be thrown (in `SyncProducer.send()`), or an error code would be passed into the delivery callback (AsyncProducer). + * Default value: 300000 + */ + static const constexpr char* MESSAGE_TIMEOUT_MS = "message.timeout.ms"; + + /** + * This value is only enforced by the brokers and relies on `ACKS` being non-zero. + * Note: The leading broker waits for in-sync replicas to acknowledge the message, and will return an error if the time elapses without the necessary acks. + * Default value: 5000 + */ + static const constexpr char* REQUEST_TIMEOUT_MS = "request.timeout.ms"; + + /** + * The default partitioner for a ProducerRecord (with no partition assigned). + * Note: It's not the same with Java version's "partitioner.class" property + * Available options: + * 1) random -- random distribution + * 2) consistent -- CRC32 hash of key (`ProducerRecord`s with empty key are mapped to single partition) + * 3) consistent_random -- CRC32 hash of key (`ProducerRecord`s with empty key are randomly partitioned) + * 4) murmur2 -- Java Producer compatible Murmur2 hash of key (`ProducerRecord`s with empty key are mapped to single partition) + * 5) murmur2_random -- Java Producer compatible Murmur2 hash of key (`ProducerRecord`s with empty key are randomly partitioned. It's equivalent to the Java Producer's default partitioner) + * 6) fnv1a -- FNV-1a hash of key (`ProducerRecord`s with empty key are mapped to single partition) + * 7) fnv1a_random -- FNV-1a hash of key (`ProducerRecord`s with empty key are randomly partitioned) + * Default value: murmur2_random + */ + static const constexpr char* PARTITIONER = "partitioner"; + + /** + * Maximum number of in-flight requests per broker connection. + * Default value: 1000000 (while `enable.idempotence`=false); 5 (while `enable.idempotence`=true) + */ + static const constexpr char* MAX_IN_FLIGHT = "max.in.flight"; + + /** + * When set to `true`, the producer will ensure that messages are succefully sent exactly once and in the original order. + * Default value: false + */ + static const constexpr char* ENABLE_IDEMPOTENCE = "enable.idempotence"; + + /** + * Protocol used to communicate with brokers. + * Default value: plaintext + */ + static const constexpr char* SECURITY_PROTOCOL = "security.protocol"; + + /** + * Shell command to refresh or acquire the client's Kerberos ticket. + */ + static const constexpr char* SASL_KERBEROS_KINIT_CMD = "sasl.kerberos.kinit.cmd"; + + /** + * The client's Kerberos principal name. + */ + static const constexpr char* SASL_KERBEROS_SERVICE_NAME = "sasl.kerberos.service.name"; +}; + +} + diff --git a/include/kafka/ProducerRecord.h b/include/kafka/ProducerRecord.h new file mode 100644 index 000000000..b5b1b7c7f --- /dev/null +++ b/include/kafka/ProducerRecord.h @@ -0,0 +1,101 @@ +#pragma once + +#include "kafka/Project.h" + +#include "kafka/Header.h" +#include "kafka/Types.h" + +#include "librdkafka/rdkafka.h" + + +namespace KAFKA_API { + +/** + * A key/value pair to be sent to Kafka. + * This consists of a topic name to which the record is being sent, an optional partition number, and an optional key and value. + */ +class ProducerRecord +{ +public: + using Id = std::uint64_t; + + // Note: ProducerRecord would not take the ownership from these parameters, + ProducerRecord(Topic topic, Partition partition, const Key& key, const Value& value, Id id = 0) + : _topic(std::move(topic)), _partition(partition), _key(key), _value(value), _id(id) {} + ProducerRecord(const Topic& topic, const Key& key, const Value& value, Id id = 0) + : ProducerRecord(topic, RD_KAFKA_PARTITION_UA, key, value, id) {} + + /** + * The topic this record is being sent to. + */ + const Topic& topic() const { return _topic; } + + /** + * The partition to which the record will be sent (or UNKNOWN_PARTITION if no partition was specified). + */ + Partition partition() const { return _partition; } + + /** + * The key (or null if no key is specified). + */ + Key key() const { return _key; } + + /** + * The value. + */ + Value value() const { return _value; } + + /** + * The id to identify the message (consistent with `Producer::Metadata::recordId()`). + */ + Id id() const { return _id; } + + /** + * The headers. + */ + const Headers& headers() const { return _headers; } + + /** + * The headers. + * Note: Users could set headers with the reference. + */ + Headers& headers() { return _headers; } + + /** + * Set the partition. + */ + void setPartition(Partition partition) { _partition = partition; } + + /** + * Set the key. + */ + void setKey(const Key& key) { _key = key; } + + /** + * Set the value. + */ + void setValue(const Value& value) { _value = value; } + + /** + * Set the record id. + */ + void setId(Id id) { _id = id; } + + std::string toString() const + { + return _topic + "-" + (_partition == RD_KAFKA_PARTITION_UA ? "NA" : std::to_string(_partition)) + std::string(":") + std::to_string(_id) + + std::string(", ") + (_headers.empty() ? "" : ("headers[" + KAFKA_API::toString(_headers) + "], ")) + + _key.toString() + std::string("/") + _value.toString(); + } + +private: + Topic _topic; + Partition _partition; + Key _key; + Value _value; + Id _id; + Headers _headers; +}; + +} + diff --git a/include/kafka/Project.h b/include/kafka/Project.h new file mode 100644 index 000000000..51dcb4ec3 --- /dev/null +++ b/include/kafka/Project.h @@ -0,0 +1,7 @@ +#pragma once + +// Customize the namespace (default is `kafka`) if necessary +#ifndef KAFKA_API +#define KAFKA_API kafka +#endif + diff --git a/include/kafka/Properties.h b/include/kafka/Properties.h new file mode 100644 index 000000000..c63226d83 --- /dev/null +++ b/include/kafka/Properties.h @@ -0,0 +1,84 @@ +#pragma once + +#include "kafka/Project.h" + +#include "kafka/Types.h" + +#include +#include +#include + + +namespace KAFKA_API { + +/** + * The properties for Kafka clients. + */ +class Properties +{ +public: + // Just make sure key will printed in order + using PropertiesMap = std::map; + + Properties() = default; + Properties(const Properties&) = default; + explicit Properties(PropertiesMap kvMap): _kvMap(std::move(kvMap)) {} + + virtual ~Properties() = default; + + bool operator==(const Properties& rhs) const { return map() == rhs.map(); } + + /** + * Set a property. + * If the map previously contained a mapping for the key, the old value is replaced by the specified value. + */ + Properties& put(const std::string& key, const std::string& value) + { + _kvMap[key] = value; + return *this; + } + + /** + * Get a property. + * If the map previously contained a mapping for the key, the old value is replaced by the specified value. + */ + Optional getProperty(const std::string& key) const + { + Optional ret; + auto search = _kvMap.find(key); + if (search != _kvMap.end()) + { + ret = search->second; + } + return ret; + } + + /** + * Remove a property. + */ + void eraseProperty(const std::string& key) + { + _kvMap.erase(key); + } + + std::string toString() const + { + std::string ret; + std::for_each(_kvMap.cbegin(), _kvMap.cend(), + [&ret](const auto& kv) { + ret.append(ret.empty() ? "" : "|").append(kv.first).append("=").append(kv.second); + }); + return ret; + } + + /** + * Get all properties with a map. + */ + const PropertiesMap& map() const { return _kvMap; } + +private: + PropertiesMap _kvMap; +}; + +} // end of KAFKA_API + diff --git a/include/kafka/RdKafkaHelper.h b/include/kafka/RdKafkaHelper.h new file mode 100644 index 000000000..05ef22bb2 --- /dev/null +++ b/include/kafka/RdKafkaHelper.h @@ -0,0 +1,114 @@ +#pragma once + +#include "kafka/Project.h" + +#include "kafka/Types.h" + +#include "librdkafka/rdkafka.h" + +#include + +namespace KAFKA_API { + +// define smart pointers for rk_kafka_xxx datatypes + +struct RkQueueDeleter { void operator()(rd_kafka_queue_t* p) { rd_kafka_queue_destroy(p); } }; +using rd_kafka_queue_unique_ptr = std::unique_ptr; + +struct RkEventDeleter { void operator()(rd_kafka_event_t* p) { rd_kafka_event_destroy(p); } }; +using rd_kafka_event_unique_ptr = std::unique_ptr; + +struct RkTopicDeleter { void operator()(rd_kafka_topic_t* p) { rd_kafka_topic_destroy(p); } }; +using rd_kafka_topic_unique_ptr = std::unique_ptr; + +struct RkTopicPartitionListDeleter { void operator()(rd_kafka_topic_partition_list_t* p) { rd_kafka_topic_partition_list_destroy(p); } }; +using rd_kafka_topic_partition_list_unique_ptr = std::unique_ptr; + +struct RkConfDeleter { void operator()(rd_kafka_conf_t* p) { rd_kafka_conf_destroy(p); } }; +using rd_kafka_conf_unique_ptr = std::unique_ptr; + +struct RkMetadataDeleter { void operator()(const rd_kafka_metadata_t* p) { rd_kafka_metadata_destroy(p); } }; +using rd_kafka_metadata_unique_ptr = std::unique_ptr; + +struct RkDeleter { void operator()(rd_kafka_t* p) { rd_kafka_destroy(p); } }; +using rd_kafka_unique_ptr = std::unique_ptr; + +struct RkNewTopicDeleter { void operator()(rd_kafka_NewTopic_t* p) { rd_kafka_NewTopic_destroy(p); } }; +using rd_kafka_NewTopic_unique_ptr = std::unique_ptr; + +struct RkDeleteTopicDeleter { void operator()(rd_kafka_DeleteTopic_t* p) { rd_kafka_DeleteTopic_destroy(p); } }; +using rd_kafka_DeleteTopic_unique_ptr = std::unique_ptr; + + +// Convert from rd_kafka_xxx datatypes +inline TopicPartitionOffsets getTopicPartitionOffsets(const rd_kafka_topic_partition_list_t* rk_tpos) +{ + TopicPartitionOffsets ret; + int count = rk_tpos ? rk_tpos->cnt : 0; + for (int i = 0; i < count; ++i) + { + const Topic t = rk_tpos->elems[i].topic; + const Partition p = rk_tpos->elems[i].partition; + const Offset o = rk_tpos->elems[i].offset; + + ret[TopicPartition(t, p)] = o; + } + return ret; +} + +inline Topics getTopics(const rd_kafka_topic_partition_list_t* rk_topics) +{ + Topics result; + for (int i = 0; i < (rk_topics ? rk_topics->cnt : 0); ++i) + { + result.insert(rk_topics->elems[i].topic); + } + return result; +} + +inline TopicPartitions getTopicPartitions(const rd_kafka_topic_partition_list_t* rk_tpos) +{ + TopicPartitions result; + for (int i = 0; i < (rk_tpos ? rk_tpos->cnt : 0); ++i) + { + result.insert(TopicPartition{rk_tpos->elems[i].topic, rk_tpos->elems[i].partition}); + } + return result; +} + +// Convert to rd_kafka_xxx datatypes +inline rd_kafka_topic_partition_list_t* createRkTopicPartitionList(const TopicPartitionOffsets& tpos) +{ + rd_kafka_topic_partition_list_t* rk_tpos = rd_kafka_topic_partition_list_new(static_cast(tpos.size())); + for (const auto& tp_o: tpos) + { + const auto& tp = tp_o.first; + const auto& o = tp_o.second; + rd_kafka_topic_partition_t* rk_tp = rd_kafka_topic_partition_list_add(rk_tpos, tp.first.c_str(), tp.second); + rk_tp->offset = o; + } + return rk_tpos; +} + +inline rd_kafka_topic_partition_list_t* createRkTopicPartitionList(const TopicPartitions& tps) +{ + TopicPartitionOffsets tpos; + for (const auto& tp: tps) + { + tpos[TopicPartition(tp.first, tp.second)] = RD_KAFKA_OFFSET_INVALID; + } + return createRkTopicPartitionList(tpos); +} + +inline rd_kafka_topic_partition_list_t* createRkTopicPartitionList(const Topics& topics) +{ + TopicPartitionOffsets tpos; + for (const auto& topic: topics) + { + tpos[TopicPartition(topic, RD_KAFKA_PARTITION_UA)] = RD_KAFKA_OFFSET_INVALID; + } + return createRkTopicPartitionList(tpos); +} + +} // end of KAFKA_API + diff --git a/include/kafka/Timestamp.h b/include/kafka/Timestamp.h new file mode 100644 index 000000000..e47ec68e9 --- /dev/null +++ b/include/kafka/Timestamp.h @@ -0,0 +1,82 @@ +#pragma once + +#include "kafka/Project.h" + +#include "librdkafka/rdkafka.h" + +#include +#include +#include +#include +#include + + +namespace KAFKA_API { + +/** + * The time point together with the type. + */ +struct Timestamp +{ + using Value = std::int64_t; + + enum class Type { NotAvailable, CreateTime, LogAppendTime }; + + /** + * The milliseconds since epoch. + */ + Value msSinceEpoch; + + /** + * The type shows what the `msSinceEpoch` means (CreateTime or LogAppendTime). + */ + Type type; + + explicit Timestamp(Value v = 0, Type t = Type::NotAvailable): msSinceEpoch(v), type(t) {} + Timestamp(Value v, rd_kafka_timestamp_type_t t): Timestamp(v, convertType(t)) {} + + static Type convertType(rd_kafka_timestamp_type_t tstype) + { + return (tstype == RD_KAFKA_TIMESTAMP_CREATE_TIME) ? Type::CreateTime : + (tstype == RD_KAFKA_TIMESTAMP_LOG_APPEND_TIME ? Type::LogAppendTime : Type::NotAvailable); + } + + static std::string toString(Type t) + { + switch (t) + { + case Type::CreateTime: + return "CreateTime"; + case Type::LogAppendTime: + return "LogAppendTime"; + default: + assert(t == Type::NotAvailable); + return ""; + } + } + + static std::string toString(Value v) + { + using namespace std::chrono; + auto ms = std::chrono::milliseconds(v); + auto timepoint = std::chrono::time_point(ms); + std::time_t time = system_clock::to_time_t(timepoint); + std::ostringstream oss; + std::tm tmBuf = {}; + oss << std::put_time(localtime_r(&time, &tmBuf), "%F %T") << "." << std::setfill('0') << std::setw(3) << (v % 1000); + return oss.str(); + } + + /** + * Obtains explanatory string. + */ + std::string toString() const + { + auto typeString = toString(type); + auto timeString = toString(msSinceEpoch); + return typeString.empty() ? timeString : (typeString + "[" + timeString + "]"); + } +}; + +} // end of KAFKA_API + diff --git a/include/kafka/Types.h b/include/kafka/Types.h new file mode 100644 index 000000000..67ba29182 --- /dev/null +++ b/include/kafka/Types.h @@ -0,0 +1,171 @@ +#pragma once + +#include "kafka/Project.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Use `boost::optional` for C++14, which doesn't support `std::optional` +#if __cplusplus >= 201703L +#include +template +using Optional = std::optional; +#else +#include +template +using Optional = boost::optional; +#endif + +namespace KAFKA_API { + +class ConstBuffer; + +/** + * Topic name. + */ +using Topic = std::string; + +/** + * Partition number. + */ +using Partition = std::int32_t; + +/** + * Record offset. + */ +using Offset = std::int64_t; + +/** + * Record key. + */ +using Key = ConstBuffer; +using KeySize = std::size_t; + +/** + * Record value. + */ +using Value = ConstBuffer; +using ValueSize = std::size_t; + +/** + * Topic set. + */ +using Topics = std::set; + +/** + * Topic Partition pair. + */ +using TopicPartition = std::pair; + +/** + * TopicPartition set. + */ +using TopicPartitions = std::set; + +/** + * Topic/Partition/Offset tuple + */ +using TopicPartitionOffset = std::tuple; + +/** + * TopicPartition to Offset map. + */ +using TopicPartitionOffsets = std::map; + + +class ConstBuffer +{ +public: + explicit ConstBuffer(const void* data = nullptr, std::size_t size = 0): _data(data), _size(size) {} + const void* data() const { return _data; } + std::size_t size() const { return _size; } + std::string toString() const + { + std::ostringstream oss; + + auto printChar = [&oss](const unsigned char c) { + if (std::isprint(c)) { + oss << c; + } else { + oss << "[0x" << std::hex << std::setfill('0') << std::setw(2) << static_cast(c) << "]"; + } + }; + const auto* beg = static_cast(_data); + std::for_each(beg, beg + _size, printChar); + + return oss.str(); + } +private: + const void* _data; + std::size_t _size; +}; + + +/** + * Obtains explanatory string for Topics. + */ +inline std::string toString(const Topics& topics) +{ + std::string ret; + std::for_each(topics.cbegin(), topics.cend(), + [&ret](const auto& topic) { + ret.append(ret.empty() ? "" : ",").append(topic); + }); + return ret; +} + +/** + * Obtains explanatory string for TopicPartition. + */ +inline std::string toString(const TopicPartition& tp) +{ + return tp.first + std::string("-") + std::to_string(tp.second); +} + +/** + * Obtains explanatory string for TopicPartitions. + */ +inline std::string toString(const TopicPartitions& tps) +{ + std::string ret; + std::for_each(tps.cbegin(), tps.cend(), + [&ret](const auto& tp) { + ret.append((ret.empty() ? "" : ",") + tp.first + "-" + std::to_string(tp.second)); + }); + return ret; +} + +/** + * Obtains explanatory string for TopicPartitionOffset. + */ +inline std::string toString(const TopicPartitionOffset& tpo) +{ + return std::get<0>(tpo) + "-" + std::to_string(std::get<1>(tpo)) + ":" + std::to_string(std::get<2>(tpo)); +} + +/** + * Obtains explanatory string for TopicPartitionOffsets. + */ +inline std::string toString(const TopicPartitionOffsets& tpos) +{ + std::string ret; + std::for_each(tpos.cbegin(), tpos.cend(), + [&ret](const auto& tp_o) { + const TopicPartition& tp = tp_o.first; + const Offset& o = tp_o.second; + ret.append((ret.empty() ? "" : ",") + tp.first + "-" + std::to_string(tp.second) + ":" + std::to_string(o)); + }); + return ret; +} + +} // end of KAFKA_API + diff --git a/include/kafka/Utility.h b/include/kafka/Utility.h new file mode 100644 index 000000000..2f11d783a --- /dev/null +++ b/include/kafka/Utility.h @@ -0,0 +1,69 @@ +#pragma once + +#include "kafka/Project.h" + +#include +#include +#include +#include +#include +#include + +namespace KAFKA_API { + +namespace Utility { + +/** + * Get current local time as string. + */ +inline std::string getCurrentTime() +{ + using namespace std::chrono; + auto current = system_clock::now(); + auto micros = duration_cast(current.time_since_epoch()) % 1000000; + auto time = system_clock::to_time_t(current); + + std::ostringstream oss; + std::tm tmBuf = {}; + oss << std::put_time(localtime_r(&time, &tmBuf), "%F %T") << "." << std::setfill('0') << std::setw(6) << micros.count(); + return oss.str(); +} + +/** + * Get random string. + */ +inline std::string getRandomString() +{ + using namespace std::chrono; + std::uint32_t timestamp = static_cast(duration_cast(system_clock::now().time_since_epoch()).count()); + + std::random_device r; + std::default_random_engine e(r()); + std::uniform_int_distribution uniform_dist(0, 0xFFFFFFFF); + int rand = uniform_dist(e); + + std::ostringstream oss; + oss << std::setfill('0') << std::setw(sizeof(std::uint32_t) * 2) << std::hex << timestamp << "-" << rand; + return oss.str(); +} + +/** + * Get librdkafka version string. + */ +inline std::string getLibRdKafkaVersion() +{ + return rd_kafka_version_str(); +} + +/** + * Current number of threads created by rdkafka. + */ +inline int getLibRdKafkaThreadCount() +{ + return rd_kafka_thread_cnt(); +} + +} // end of Utility + +} // end of KAFKA_API + diff --git a/scripts/doxyfile.cfg b/scripts/doxyfile.cfg new file mode 100644 index 000000000..48de9d00c --- /dev/null +++ b/scripts/doxyfile.cfg @@ -0,0 +1,2511 @@ +# Doxyfile 1.8.15 + +# This file describes the settings to be used by the documentation system +# doxygen (www.doxygen.org) for a project. +# +# All text after a double hash (##) is considered a comment and is placed in +# front of the TAG it is preceding. +# +# All text after a single hash (#) is considered a comment and will be ignored. +# The format is: +# TAG = value [value, ...] +# For lists, items can also be appended using: +# TAG += value [value, ...] +# Values that contain spaces should be placed between quotes (\" \"). + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- + +# This tag specifies the encoding used for all characters in the configuration +# file that follow. The default is UTF-8 which is also the encoding used for all +# text before the first occurrence of this tag. Doxygen uses libiconv (or the +# iconv built into libc) for the transcoding. See +# https://www.gnu.org/software/libiconv/ for the list of possible encodings. +# The default value is: UTF-8. + +DOXYFILE_ENCODING = UTF-8 + +# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by +# double-quotes, unless you are using Doxywizard) that should identify the +# project for which the documentation is generated. This name is used in the +# title of most generated pages and in a few other places. +# The default value is: My Project. + +PROJECT_NAME = "Modern C++ based Kafka API" + +# The PROJECT_NUMBER tag can be used to enter a project or revision number. This +# could be handy for archiving the generated documentation or if some version +# control system is used. + +PROJECT_NUMBER = + +# Using the PROJECT_BRIEF tag one can provide an optional one line description +# for a project that appears at the top of each page and should give viewer a +# quick idea about the purpose of the project. Keep the description short. + +PROJECT_BRIEF = + +# With the PROJECT_LOGO tag one can specify a logo or an icon that is included +# in the documentation. The maximum height of the logo should not exceed 55 +# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy +# the logo to the output directory. + +PROJECT_LOGO = + +# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path +# into which the generated documentation will be written. If a relative path is +# entered, it will be relative to the location where doxygen was started. If +# left blank the current directory will be used. + +OUTPUT_DIRECTORY = + +# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub- +# directories (in 2 levels) under the output directory of each output format and +# will distribute the generated files over these directories. Enabling this +# option can be useful when feeding doxygen a huge amount of source files, where +# putting all generated files in the same directory would otherwise causes +# performance problems for the file system. +# The default value is: NO. + +CREATE_SUBDIRS = NO + +# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII +# characters to appear in the names of generated files. If set to NO, non-ASCII +# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode +# U+3044. +# The default value is: NO. + +ALLOW_UNICODE_NAMES = NO + +# The OUTPUT_LANGUAGE tag is used to specify the language in which all +# documentation generated by doxygen is written. Doxygen will use this +# information to generate all constant output in the proper language. +# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese, +# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States), +# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian, +# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages), +# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian, +# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian, +# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish, +# Ukrainian and Vietnamese. +# The default value is: English. + +OUTPUT_LANGUAGE = English + +# The OUTPUT_TEXT_DIRECTION tag is used to specify the direction in which all +# documentation generated by doxygen is written. Doxygen will use this +# information to generate all generated output in the proper direction. +# Possible values are: None, LTR, RTL and Context. +# The default value is: None. + +OUTPUT_TEXT_DIRECTION = None + +# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member +# descriptions after the members that are listed in the file and class +# documentation (similar to Javadoc). Set to NO to disable this. +# The default value is: YES. + +BRIEF_MEMBER_DESC = YES + +# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief +# description of a member or function before the detailed description +# +# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the +# brief descriptions will be completely suppressed. +# The default value is: YES. + +REPEAT_BRIEF = YES + +# This tag implements a quasi-intelligent brief description abbreviator that is +# used to form the text in various listings. Each string in this list, if found +# as the leading text of the brief description, will be stripped from the text +# and the result, after processing the whole list, is used as the annotated +# text. Otherwise, the brief description is used as-is. If left blank, the +# following values are used ($name is automatically replaced with the name of +# the entity):The $name class, The $name widget, The $name file, is, provides, +# specifies, contains, represents, a, an and the. + +ABBREVIATE_BRIEF = "The $name class" \ + "The $name widget" \ + "The $name file" \ + is \ + provides \ + specifies \ + contains \ + represents \ + a \ + an \ + the + +# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then +# doxygen will generate a detailed section even if there is only a brief +# description. +# The default value is: NO. + +ALWAYS_DETAILED_SEC = NO + +# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all +# inherited members of a class in the documentation of that class as if those +# members were ordinary class members. Constructors, destructors and assignment +# operators of the base classes will not be shown. +# The default value is: NO. + +INLINE_INHERITED_MEMB = NO + +# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path +# before files name in the file list and in the header files. If set to NO the +# shortest path that makes the file name unique will be used +# The default value is: YES. + +FULL_PATH_NAMES = YES + +# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path. +# Stripping is only done if one of the specified strings matches the left-hand +# part of the path. The tag can be used to show relative paths in the file list. +# If left blank the directory from which doxygen is run is used as the path to +# strip. +# +# Note that you can specify absolute paths here, but also relative paths, which +# will be relative from the directory where doxygen is started. +# This tag requires that the tag FULL_PATH_NAMES is set to YES. + +STRIP_FROM_PATH = + +# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the +# path mentioned in the documentation of a class, which tells the reader which +# header file to include in order to use a class. If left blank only the name of +# the header file containing the class definition is used. Otherwise one should +# specify the list of include paths that are normally passed to the compiler +# using the -I flag. + +STRIP_FROM_INC_PATH = + +# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but +# less readable) file names. This can be useful is your file systems doesn't +# support long names like on DOS, Mac, or CD-ROM. +# The default value is: NO. + +SHORT_NAMES = NO + +# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the +# first line (until the first dot) of a Javadoc-style comment as the brief +# description. If set to NO, the Javadoc-style will behave just like regular Qt- +# style comments (thus requiring an explicit @brief command for a brief +# description.) +# The default value is: NO. + +JAVADOC_AUTOBRIEF = YES + +# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first +# line (until the first dot) of a Qt-style comment as the brief description. If +# set to NO, the Qt-style will behave just like regular Qt-style comments (thus +# requiring an explicit \brief command for a brief description.) +# The default value is: NO. + +QT_AUTOBRIEF = NO + +# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a +# multi-line C++ special comment block (i.e. a block of //! or /// comments) as +# a brief description. This used to be the default behavior. The new default is +# to treat a multi-line C++ comment block as a detailed description. Set this +# tag to YES if you prefer the old behavior instead. +# +# Note that setting this tag to YES also means that rational rose comments are +# not recognized any more. +# The default value is: NO. + +MULTILINE_CPP_IS_BRIEF = NO + +# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the +# documentation from any documented member that it re-implements. +# The default value is: YES. + +INHERIT_DOCS = YES + +# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new +# page for each member. If set to NO, the documentation of a member will be part +# of the file/class/namespace that contains it. +# The default value is: NO. + +SEPARATE_MEMBER_PAGES = NO + +# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen +# uses this value to replace tabs by spaces in code fragments. +# Minimum value: 1, maximum value: 16, default value: 4. + +TAB_SIZE = 4 + +# This tag can be used to specify a number of aliases that act as commands in +# the documentation. An alias has the form: +# name=value +# For example adding +# "sideeffect=@par Side Effects:\n" +# will allow you to put the command \sideeffect (or @sideeffect) in the +# documentation, which will result in a user-defined paragraph with heading +# "Side Effects:". You can put \n's in the value part of an alias to insert +# newlines (in the resulting output). You can put ^^ in the value part of an +# alias to insert a newline as if a physical newline was in the original file. +# When you need a literal { or } or , in the value part of an alias you have to +# escape them by means of a backslash (\), this can lead to conflicts with the +# commands \{ and \} for these it is advised to use the version @{ and @} or use +# a double escape (\\{ and \\}) + +ALIASES = + +# This tag can be used to specify a number of word-keyword mappings (TCL only). +# A mapping has the form "name=value". For example adding "class=itcl::class" +# will allow you to use the command class in the itcl::class meaning. + +TCL_SUBST = + +# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources +# only. Doxygen will then generate output that is more tailored for C. For +# instance, some of the names that are used will be different. The list of all +# members will be omitted, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_FOR_C = NO + +# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or +# Python sources only. Doxygen will then generate output that is more tailored +# for that language. For instance, namespaces will be presented as packages, +# qualified scopes will look different, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_JAVA = NO + +# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran +# sources. Doxygen will then generate output that is tailored for Fortran. +# The default value is: NO. + +OPTIMIZE_FOR_FORTRAN = NO + +# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL +# sources. Doxygen will then generate output that is tailored for VHDL. +# The default value is: NO. + +OPTIMIZE_OUTPUT_VHDL = NO + +# Doxygen selects the parser to use depending on the extension of the files it +# parses. With this tag you can assign which parser to use for a given +# extension. Doxygen has a built-in mapping, but you can override or extend it +# using this tag. The format is ext=language, where ext is a file extension, and +# language is one of the parsers supported by doxygen: IDL, Java, Javascript, +# Csharp (C#), C, C++, D, PHP, md (Markdown), Objective-C, Python, Fortran +# (fixed format Fortran: FortranFixed, free formatted Fortran: FortranFree, +# unknown formatted Fortran: Fortran. In the later case the parser tries to +# guess whether the code is fixed or free formatted code, this is the default +# for Fortran type files), VHDL, tcl. For instance to make doxygen treat .inc +# files as Fortran files (default is PHP), and .f files as C (default is +# Fortran), use: inc=Fortran f=C. +# +# Note: For files without extension you can use no_extension as a placeholder. +# +# Note that for custom extensions you also need to set FILE_PATTERNS otherwise +# the files are not read by doxygen. + +EXTENSION_MAPPING = + +# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments +# according to the Markdown format, which allows for more readable +# documentation. See http://daringfireball.net/projects/markdown/ for details. +# The output of markdown processing is further processed by doxygen, so you can +# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in +# case of backward compatibilities issues. +# The default value is: YES. + +MARKDOWN_SUPPORT = YES + +# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up +# to that level are automatically included in the table of contents, even if +# they do not have an id attribute. +# Note: This feature currently applies only to Markdown headings. +# Minimum value: 0, maximum value: 99, default value: 0. +# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. + +TOC_INCLUDE_HEADINGS = 0 + +# When enabled doxygen tries to link words that correspond to documented +# classes, or namespaces to their corresponding documentation. Such a link can +# be prevented in individual cases by putting a % sign in front of the word or +# globally by setting AUTOLINK_SUPPORT to NO. +# The default value is: YES. + +AUTOLINK_SUPPORT = YES + +# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want +# to include (a tag file for) the STL sources as input, then you should set this +# tag to YES in order to let doxygen match functions declarations and +# definitions whose arguments contain STL classes (e.g. func(std::string); +# versus func(std::string) {}). This also make the inheritance and collaboration +# diagrams that involve STL classes more complete and accurate. +# The default value is: NO. + +BUILTIN_STL_SUPPORT = YES + +# If you use Microsoft's C++/CLI language, you should set this option to YES to +# enable parsing support. +# The default value is: NO. + +CPP_CLI_SUPPORT = NO + +# Set the SIP_SUPPORT tag to YES if your project consists of sip (see: +# https://www.riverbankcomputing.com/software/sip/intro) sources only. Doxygen +# will parse them like normal C++ but will assume all classes use public instead +# of private inheritance when no explicit protection keyword is present. +# The default value is: NO. + +SIP_SUPPORT = NO + +# For Microsoft's IDL there are propget and propput attributes to indicate +# getter and setter methods for a property. Setting this option to YES will make +# doxygen to replace the get and set methods by a property in the documentation. +# This will only work if the methods are indeed getting or setting a simple +# type. If this is not the case, or you want to show the methods anyway, you +# should set this option to NO. +# The default value is: YES. + +IDL_PROPERTY_SUPPORT = YES + +# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC +# tag is set to YES then doxygen will reuse the documentation of the first +# member in the group (if any) for the other members of the group. By default +# all members of a group must be documented explicitly. +# The default value is: NO. + +DISTRIBUTE_GROUP_DOC = NO + +# If one adds a struct or class to a group and this option is enabled, then also +# any nested class or struct is added to the same group. By default this option +# is disabled and one has to add nested compounds explicitly via \ingroup. +# The default value is: NO. + +GROUP_NESTED_COMPOUNDS = NO + +# Set the SUBGROUPING tag to YES to allow class member groups of the same type +# (for instance a group of public functions) to be put as a subgroup of that +# type (e.g. under the Public Functions section). Set it to NO to prevent +# subgrouping. Alternatively, this can be done per class using the +# \nosubgrouping command. +# The default value is: YES. + +SUBGROUPING = YES + +# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions +# are shown inside the group in which they are included (e.g. using \ingroup) +# instead of on a separate page (for HTML and Man pages) or section (for LaTeX +# and RTF). +# +# Note that this feature does not work in combination with +# SEPARATE_MEMBER_PAGES. +# The default value is: NO. + +INLINE_GROUPED_CLASSES = NO + +# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions +# with only public data fields or simple typedef fields will be shown inline in +# the documentation of the scope in which they are defined (i.e. file, +# namespace, or group documentation), provided this scope is documented. If set +# to NO, structs, classes, and unions are shown on a separate page (for HTML and +# Man pages) or section (for LaTeX and RTF). +# The default value is: NO. + +INLINE_SIMPLE_STRUCTS = NO + +# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or +# enum is documented as struct, union, or enum with the name of the typedef. So +# typedef struct TypeS {} TypeT, will appear in the documentation as a struct +# with name TypeT. When disabled the typedef will appear as a member of a file, +# namespace, or class. And the struct will be named TypeS. This can typically be +# useful for C code in case the coding convention dictates that all compound +# types are typedef'ed and only the typedef is referenced, never the tag name. +# The default value is: NO. + +TYPEDEF_HIDES_STRUCT = NO + +# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This +# cache is used to resolve symbols given their name and scope. Since this can be +# an expensive process and often the same symbol appears multiple times in the +# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small +# doxygen will become slower. If the cache is too large, memory is wasted. The +# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range +# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536 +# symbols. At the end of a run doxygen will report the cache usage and suggest +# the optimal cache size from a speed point of view. +# Minimum value: 0, maximum value: 9, default value: 0. + +LOOKUP_CACHE_SIZE = 0 + +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- + +# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in +# documentation are documented, even if no documentation was available. Private +# class members and static file members will be hidden unless the +# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES. +# Note: This will also disable the warnings about undocumented members that are +# normally produced when WARNINGS is set to YES. +# The default value is: NO. + +EXTRACT_ALL = NO + +# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will +# be included in the documentation. +# The default value is: NO. + +EXTRACT_PRIVATE = NO + +# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal +# scope will be included in the documentation. +# The default value is: NO. + +EXTRACT_PACKAGE = NO + +# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be +# included in the documentation. +# The default value is: NO. + +EXTRACT_STATIC = NO + +# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined +# locally in source files will be included in the documentation. If set to NO, +# only classes defined in header files are included. Does not have any effect +# for Java sources. +# The default value is: YES. + +EXTRACT_LOCAL_CLASSES = NO + +# This flag is only useful for Objective-C code. If set to YES, local methods, +# which are defined in the implementation section but not in the interface are +# included in the documentation. If set to NO, only methods in the interface are +# included. +# The default value is: NO. + +EXTRACT_LOCAL_METHODS = NO + +# If this flag is set to YES, the members of anonymous namespaces will be +# extracted and appear in the documentation as a namespace called +# 'anonymous_namespace{file}', where file will be replaced with the base name of +# the file that contains the anonymous namespace. By default anonymous namespace +# are hidden. +# The default value is: NO. + +EXTRACT_ANON_NSPACES = NO + +# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all +# undocumented members inside documented classes or files. If set to NO these +# members will be included in the various overviews, but no documentation +# section is generated. This option has no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_MEMBERS = NO + +# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all +# undocumented classes that are normally visible in the class hierarchy. If set +# to NO, these classes will be included in the various overviews. This option +# has no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_CLASSES = YES + +# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend +# (class|struct|union) declarations. If set to NO, these declarations will be +# included in the documentation. +# The default value is: NO. + +HIDE_FRIEND_COMPOUNDS = NO + +# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any +# documentation blocks found inside the body of a function. If set to NO, these +# blocks will be appended to the function's detailed documentation block. +# The default value is: NO. + +HIDE_IN_BODY_DOCS = NO + +# The INTERNAL_DOCS tag determines if documentation that is typed after a +# \internal command is included. If the tag is set to NO then the documentation +# will be excluded. Set it to YES to include the internal documentation. +# The default value is: NO. + +INTERNAL_DOCS = NO + +# If the CASE_SENSE_NAMES tag is set to NO then doxygen will only generate file +# names in lower-case letters. If set to YES, upper-case letters are also +# allowed. This is useful if you have classes or files whose names only differ +# in case and if your file system supports case sensitive file names. Windows +# and Mac users are advised to set this option to NO. +# The default value is: system dependent. + +CASE_SENSE_NAMES = YES + +# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with +# their full class and namespace scopes in the documentation. If set to YES, the +# scope will be hidden. +# The default value is: NO. + +HIDE_SCOPE_NAMES = NO + +# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will +# append additional text to a page's title, such as Class Reference. If set to +# YES the compound reference will be hidden. +# The default value is: NO. + +HIDE_COMPOUND_REFERENCE= NO + +# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of +# the files that are included by a file in the documentation of that file. +# The default value is: YES. + +SHOW_INCLUDE_FILES = YES + +# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each +# grouped member an include statement to the documentation, telling the reader +# which file to include in order to use the member. +# The default value is: NO. + +SHOW_GROUPED_MEMB_INC = NO + +# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include +# files with double quotes in the documentation rather than with sharp brackets. +# The default value is: NO. + +FORCE_LOCAL_INCLUDES = NO + +# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the +# documentation for inline members. +# The default value is: YES. + +INLINE_INFO = YES + +# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the +# (detailed) documentation of file and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. +# The default value is: YES. + +SORT_MEMBER_DOCS = YES + +# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief +# descriptions of file, namespace and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. Note that +# this will also influence the order of the classes in the class list. +# The default value is: NO. + +SORT_BRIEF_DOCS = NO + +# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the +# (brief and detailed) documentation of class members so that constructors and +# destructors are listed first. If set to NO the constructors will appear in the +# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS. +# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief +# member documentation. +# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting +# detailed member documentation. +# The default value is: NO. + +SORT_MEMBERS_CTORS_1ST = NO + +# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy +# of group names into alphabetical order. If set to NO the group names will +# appear in their defined order. +# The default value is: NO. + +SORT_GROUP_NAMES = NO + +# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by +# fully-qualified names, including namespaces. If set to NO, the class list will +# be sorted only by class name, not including the namespace part. +# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. +# Note: This option applies only to the class list, not to the alphabetical +# list. +# The default value is: NO. + +SORT_BY_SCOPE_NAME = NO + +# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper +# type resolution of all parameters of a function it will reject a match between +# the prototype and the implementation of a member function even if there is +# only one candidate or it is obvious which candidate to choose by doing a +# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still +# accept a match between prototype and implementation in such cases. +# The default value is: NO. + +STRICT_PROTO_MATCHING = NO + +# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo +# list. This list is created by putting \todo commands in the documentation. +# The default value is: YES. + +GENERATE_TODOLIST = YES + +# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test +# list. This list is created by putting \test commands in the documentation. +# The default value is: YES. + +GENERATE_TESTLIST = YES + +# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug +# list. This list is created by putting \bug commands in the documentation. +# The default value is: YES. + +GENERATE_BUGLIST = YES + +# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO) +# the deprecated list. This list is created by putting \deprecated commands in +# the documentation. +# The default value is: YES. + +GENERATE_DEPRECATEDLIST= YES + +# The ENABLED_SECTIONS tag can be used to enable conditional documentation +# sections, marked by \if ... \endif and \cond +# ... \endcond blocks. + +ENABLED_SECTIONS = + +# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the +# initial value of a variable or macro / define can have for it to appear in the +# documentation. If the initializer consists of more lines than specified here +# it will be hidden. Use a value of 0 to hide initializers completely. The +# appearance of the value of individual variables and macros / defines can be +# controlled using \showinitializer or \hideinitializer command in the +# documentation regardless of this setting. +# Minimum value: 0, maximum value: 10000, default value: 30. + +MAX_INITIALIZER_LINES = 30 + +# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at +# the bottom of the documentation of classes and structs. If set to YES, the +# list will mention the files that were used to generate the documentation. +# The default value is: YES. + +SHOW_USED_FILES = YES + +# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This +# will remove the Files entry from the Quick Index and from the Folder Tree View +# (if specified). +# The default value is: YES. + +SHOW_FILES = YES + +# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces +# page. This will remove the Namespaces entry from the Quick Index and from the +# Folder Tree View (if specified). +# The default value is: YES. + +SHOW_NAMESPACES = YES + +# The FILE_VERSION_FILTER tag can be used to specify a program or script that +# doxygen should invoke to get the current version for each file (typically from +# the version control system). Doxygen will invoke the program by executing (via +# popen()) the command command input-file, where command is the value of the +# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided +# by doxygen. Whatever the program writes to standard output is used as the file +# version. For an example see the documentation. + +FILE_VERSION_FILTER = + +# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed +# by doxygen. The layout file controls the global structure of the generated +# output files in an output format independent way. To create the layout file +# that represents doxygen's defaults, run doxygen with the -l option. You can +# optionally specify a file name after the option, if omitted DoxygenLayout.xml +# will be used as the name of the layout file. +# +# Note that if you run doxygen from a directory containing a file called +# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE +# tag is left empty. + +LAYOUT_FILE = + +# The CITE_BIB_FILES tag can be used to specify one or more bib files containing +# the reference definitions. This must be a list of .bib files. The .bib +# extension is automatically appended if omitted. This requires the bibtex tool +# to be installed. See also https://en.wikipedia.org/wiki/BibTeX for more info. +# For LaTeX the style of the bibliography can be controlled using +# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the +# search path. See also \cite for info how to create references. + +CITE_BIB_FILES = + +#--------------------------------------------------------------------------- +# Configuration options related to warning and progress messages +#--------------------------------------------------------------------------- + +# The QUIET tag can be used to turn on/off the messages that are generated to +# standard output by doxygen. If QUIET is set to YES this implies that the +# messages are off. +# The default value is: NO. + +QUIET = YES + +# The WARNINGS tag can be used to turn on/off the warning messages that are +# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES +# this implies that the warnings are on. +# +# Tip: Turn warnings on while writing the documentation. +# The default value is: YES. + +WARNINGS = YES + +# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate +# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag +# will automatically be disabled. +# The default value is: YES. + +WARN_IF_UNDOCUMENTED = NO + +# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for +# potential errors in the documentation, such as not documenting some parameters +# in a documented function, or documenting parameters that don't exist or using +# markup commands wrongly. +# The default value is: YES. + +WARN_IF_DOC_ERROR = YES + +# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that +# are documented, but have no documentation for their parameters or return +# value. If set to NO, doxygen will only warn about wrong or incomplete +# parameter documentation, but not about the absence of documentation. If +# EXTRACT_ALL is set to YES then this flag will automatically be disabled. +# The default value is: NO. + +WARN_NO_PARAMDOC = NO + +# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when +# a warning is encountered. +# The default value is: NO. + +WARN_AS_ERROR = NO + +# The WARN_FORMAT tag determines the format of the warning messages that doxygen +# can produce. The string should contain the $file, $line, and $text tags, which +# will be replaced by the file and line number from which the warning originated +# and the warning text. Optionally the format may contain $version, which will +# be replaced by the version of the file (if it could be obtained via +# FILE_VERSION_FILTER) +# The default value is: $file:$line: $text. + +WARN_FORMAT = "$file:$line: $text" + +# The WARN_LOGFILE tag can be used to specify a file to which warning and error +# messages should be written. If left blank the output is written to standard +# error (stderr). + +WARN_LOGFILE = + +#--------------------------------------------------------------------------- +# Configuration options related to the input files +#--------------------------------------------------------------------------- + +# The INPUT tag is used to specify the files and/or directories that contain +# documented source files. You may enter file names like myfile.cpp or +# directories like /usr/src/myproject. Separate the files or directories with +# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING +# Note: If this tag is empty the current directory is searched. + +INPUT = @DOXYGEN_INPUT_DIR@ + +# This tag can be used to specify the character encoding of the source files +# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses +# libiconv (or the iconv built into libc) for the transcoding. See the libiconv +# documentation (see: https://www.gnu.org/software/libiconv/) for the list of +# possible encodings. +# The default value is: UTF-8. + +INPUT_ENCODING = UTF-8 + +# If the value of the INPUT tag contains directories, you can use the +# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and +# *.h) to filter out the source-files in the directories. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# read by doxygen. +# +# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp, +# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, +# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc, +# *.m, *.markdown, *.md, *.mm, *.dox, *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, +# *.f, *.for, *.tcl, *.vhd, *.vhdl, *.ucf and *.qsf. + +FILE_PATTERNS = *.c \ + *.C \ + *.cc \ + *.cxx \ + *.cpp \ + *.c++ \ + *.java \ + *.ii \ + *.ixx \ + *.ipp \ + *.i++ \ + *.inl \ + *.idl \ + *.ddl \ + *.odl \ + *.h \ + *.H \ + *.hh \ + *.hxx \ + *.hpp \ + *.h++ \ + *.cs \ + *.d \ + *.php \ + *.php4 \ + *.php5 \ + *.phtml \ + *.inc \ + *.m \ + *.markdown \ + *.md \ + *.mm \ + *.dox \ + *.py \ + *.pyw \ + *.f90 \ + *.f95 \ + *.f03 \ + *.f08 \ + *.f \ + *.for \ + *.tcl \ + *.vhd \ + *.vhdl \ + *.ucf \ + *.qsf + +# The RECURSIVE tag can be used to specify whether or not subdirectories should +# be searched for input files as well. +# The default value is: NO. + +RECURSIVE = YES + +# The EXCLUDE tag can be used to specify files and/or directories that should be +# excluded from the INPUT source files. This way you can easily exclude a +# subdirectory from a directory tree whose root is specified with the INPUT tag. +# +# Note that relative paths are relative to the directory from which doxygen is +# run. + +EXCLUDE = + +# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or +# directories that are symbolic links (a Unix file system feature) are excluded +# from the input. +# The default value is: NO. + +EXCLUDE_SYMLINKS = NO + +# If the value of the INPUT tag contains directories, you can use the +# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude +# certain files from those directories. +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories for example use the pattern */test/* + +EXCLUDE_PATTERNS = + +# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names +# (namespaces, classes, functions, etc.) that should be excluded from the +# output. The symbol name can be a fully qualified name, a word, or if the +# wildcard * is used, a substring. Examples: ANamespace, AClass, +# AClass::ANamespace, ANamespace::*Test +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories use the pattern */test/* + +EXCLUDE_SYMBOLS = + +# The EXAMPLE_PATH tag can be used to specify one or more files or directories +# that contain example code fragments that are included (see the \include +# command). + +EXAMPLE_PATH = + +# If the value of the EXAMPLE_PATH tag contains directories, you can use the +# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and +# *.h) to filter out the source-files in the directories. If left blank all +# files are included. + +EXAMPLE_PATTERNS = * + +# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be +# searched for input files to be used with the \include or \dontinclude commands +# irrespective of the value of the RECURSIVE tag. +# The default value is: NO. + +EXAMPLE_RECURSIVE = NO + +# The IMAGE_PATH tag can be used to specify one or more files or directories +# that contain images that are to be included in the documentation (see the +# \image command). + +IMAGE_PATH = + +# The INPUT_FILTER tag can be used to specify a program that doxygen should +# invoke to filter for each input file. Doxygen will invoke the filter program +# by executing (via popen()) the command: +# +# +# +# where is the value of the INPUT_FILTER tag, and is the +# name of an input file. Doxygen will then use the output that the filter +# program writes to standard output. If FILTER_PATTERNS is specified, this tag +# will be ignored. +# +# Note that the filter must not add or remove lines; it is applied before the +# code is scanned, but not when the output code is generated. If lines are added +# or removed, the anchors will not be placed correctly. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# properly processed by doxygen. + +INPUT_FILTER = + +# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern +# basis. Doxygen will compare the file name with each pattern and apply the +# filter if there is a match. The filters are a list of the form: pattern=filter +# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how +# filters are used. If the FILTER_PATTERNS tag is empty or if none of the +# patterns match the file name, INPUT_FILTER is applied. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# properly processed by doxygen. + +FILTER_PATTERNS = + +# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using +# INPUT_FILTER) will also be used to filter the input files that are used for +# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES). +# The default value is: NO. + +FILTER_SOURCE_FILES = NO + +# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file +# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and +# it is also possible to disable source filtering for a specific pattern using +# *.ext= (so without naming a filter). +# This tag requires that the tag FILTER_SOURCE_FILES is set to YES. + +FILTER_SOURCE_PATTERNS = + +# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that +# is part of the input, its contents will be placed on the main page +# (index.html). This can be useful if you have a project on for instance GitHub +# and want to reuse the introduction page also for the doxygen output. + +USE_MDFILE_AS_MAINPAGE = + +#--------------------------------------------------------------------------- +# Configuration options related to source browsing +#--------------------------------------------------------------------------- + +# If the SOURCE_BROWSER tag is set to YES then a list of source files will be +# generated. Documented entities will be cross-referenced with these sources. +# +# Note: To get rid of all source code in the generated output, make sure that +# also VERBATIM_HEADERS is set to NO. +# The default value is: NO. + +SOURCE_BROWSER = NO + +# Setting the INLINE_SOURCES tag to YES will include the body of functions, +# classes and enums directly into the documentation. +# The default value is: NO. + +INLINE_SOURCES = NO + +# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any +# special comment blocks from generated source code fragments. Normal C, C++ and +# Fortran comments will always remain visible. +# The default value is: YES. + +STRIP_CODE_COMMENTS = YES + +# If the REFERENCED_BY_RELATION tag is set to YES then for each documented +# entity all documented functions referencing it will be listed. +# The default value is: NO. + +REFERENCED_BY_RELATION = NO + +# If the REFERENCES_RELATION tag is set to YES then for each documented function +# all documented entities called/used by that function will be listed. +# The default value is: NO. + +REFERENCES_RELATION = NO + +# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set +# to YES then the hyperlinks from functions in REFERENCES_RELATION and +# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will +# link to the documentation. +# The default value is: YES. + +REFERENCES_LINK_SOURCE = YES + +# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the +# source code will show a tooltip with additional information such as prototype, +# brief description and links to the definition and documentation. Since this +# will make the HTML file larger and loading of large files a bit slower, you +# can opt to disable this feature. +# The default value is: YES. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +SOURCE_TOOLTIPS = YES + +# If the USE_HTAGS tag is set to YES then the references to source code will +# point to the HTML generated by the htags(1) tool instead of doxygen built-in +# source browser. The htags tool is part of GNU's global source tagging system +# (see https://www.gnu.org/software/global/global.html). You will need version +# 4.8.6 or higher. +# +# To use it do the following: +# - Install the latest version of global +# - Enable SOURCE_BROWSER and USE_HTAGS in the configuration file +# - Make sure the INPUT points to the root of the source tree +# - Run doxygen as normal +# +# Doxygen will invoke htags (and that will in turn invoke gtags), so these +# tools must be available from the command line (i.e. in the search path). +# +# The result: instead of the source browser generated by doxygen, the links to +# source code will now point to the output of htags. +# The default value is: NO. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +USE_HTAGS = NO + +# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a +# verbatim copy of the header file for each class for which an include is +# specified. Set to NO to disable this. +# See also: Section \class. +# The default value is: YES. + +VERBATIM_HEADERS = YES + +#--------------------------------------------------------------------------- +# Configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- + +# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all +# compounds will be generated. Enable this if the project contains a lot of +# classes, structs, unions or interfaces. +# The default value is: YES. + +ALPHABETICAL_INDEX = YES + +# The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in +# which the alphabetical index list will be split. +# Minimum value: 1, maximum value: 20, default value: 5. +# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. + +COLS_IN_ALPHA_INDEX = 5 + +# In case all classes in a project start with a common prefix, all classes will +# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag +# can be used to specify a prefix (or a list of prefixes) that should be ignored +# while generating the index headers. +# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. + +IGNORE_PREFIX = + +#--------------------------------------------------------------------------- +# Configuration options related to the HTML output +#--------------------------------------------------------------------------- + +# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output +# The default value is: YES. + +GENERATE_HTML = YES + +# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a +# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of +# it. +# The default directory is: html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_OUTPUT = doxygen + +# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each +# generated HTML page (for example: .htm, .php, .asp). +# The default value is: .html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FILE_EXTENSION = .html + +# The HTML_HEADER tag can be used to specify a user-defined HTML header file for +# each generated HTML page. If the tag is left blank doxygen will generate a +# standard header. +# +# To get valid HTML the header file that includes any scripts and style sheets +# that doxygen needs, which is dependent on the configuration options used (e.g. +# the setting GENERATE_TREEVIEW). It is highly recommended to start with a +# default header using +# doxygen -w html new_header.html new_footer.html new_stylesheet.css +# YourConfigFile +# and then modify the file new_header.html. See also section "Doxygen usage" +# for information on how to generate the default header that doxygen normally +# uses. +# Note: The header is subject to change so you typically have to regenerate the +# default header when upgrading to a newer version of doxygen. For a description +# of the possible markers and block names see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_HEADER = + +# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each +# generated HTML page. If the tag is left blank doxygen will generate a standard +# footer. See HTML_HEADER for more information on how to generate a default +# footer and what special commands can be used inside the footer. See also +# section "Doxygen usage" for information on how to generate the default footer +# that doxygen normally uses. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FOOTER = + +# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style +# sheet that is used by each HTML page. It can be used to fine-tune the look of +# the HTML output. If left blank doxygen will generate a default style sheet. +# See also section "Doxygen usage" for information on how to generate the style +# sheet that doxygen normally uses. +# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as +# it is more robust and this tag (HTML_STYLESHEET) will in the future become +# obsolete. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_STYLESHEET = + +# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined +# cascading style sheets that are included after the standard style sheets +# created by doxygen. Using this option one can overrule certain style aspects. +# This is preferred over using HTML_STYLESHEET since it does not replace the +# standard style sheet and is therefore more robust against future updates. +# Doxygen will copy the style sheet files to the output directory. +# Note: The order of the extra style sheet files is of importance (e.g. the last +# style sheet in the list overrules the setting of the previous ones in the +# list). For an example see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_STYLESHEET = + +# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or +# other source files which should be copied to the HTML output directory. Note +# that these files will be copied to the base HTML output directory. Use the +# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these +# files. In the HTML_STYLESHEET file, use the file name only. Also note that the +# files will be copied as-is; there are no commands or markers available. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_FILES = + +# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen +# will adjust the colors in the style sheet and background images according to +# this color. Hue is specified as an angle on a colorwheel, see +# https://en.wikipedia.org/wiki/Hue for more information. For instance the value +# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300 +# purple, and 360 is red again. +# Minimum value: 0, maximum value: 359, default value: 220. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_HUE = 220 + +# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors +# in the HTML output. For a value of 0 the output will use grayscales only. A +# value of 255 will produce the most vivid colors. +# Minimum value: 0, maximum value: 255, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_SAT = 100 + +# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the +# luminance component of the colors in the HTML output. Values below 100 +# gradually make the output lighter, whereas values above 100 make the output +# darker. The value divided by 100 is the actual gamma applied, so 80 represents +# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not +# change the gamma. +# Minimum value: 40, maximum value: 240, default value: 80. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_GAMMA = 80 + +# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML +# page will contain the date and time when the page was generated. Setting this +# to YES can help to show when doxygen was last run and thus if the +# documentation is up to date. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_TIMESTAMP = NO + +# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML +# documentation will contain a main index with vertical navigation menus that +# are dynamically created via Javascript. If disabled, the navigation index will +# consists of multiple levels of tabs that are statically embedded in every HTML +# page. Disable this option to support browsers that do not have Javascript, +# like the Qt help browser. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_DYNAMIC_MENUS = YES + +# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML +# documentation will contain sections that can be hidden and shown after the +# page has loaded. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_DYNAMIC_SECTIONS = NO + +# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries +# shown in the various tree structured indices initially; the user can expand +# and collapse entries dynamically later on. Doxygen will expand the tree to +# such a level that at most the specified number of entries are visible (unless +# a fully collapsed tree already exceeds this amount). So setting the number of +# entries 1 will produce a full collapsed tree by default. 0 is a special value +# representing an infinite number of entries and will result in a full expanded +# tree by default. +# Minimum value: 0, maximum value: 9999, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_INDEX_NUM_ENTRIES = 100 + +# If the GENERATE_DOCSET tag is set to YES, additional index files will be +# generated that can be used as input for Apple's Xcode 3 integrated development +# environment (see: https://developer.apple.com/tools/xcode/), introduced with +# OSX 10.5 (Leopard). To create a documentation set, doxygen will generate a +# Makefile in the HTML output directory. Running make will produce the docset in +# that directory and running make install will install the docset in +# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at +# startup. See https://developer.apple.com/tools/creatingdocsetswithdoxygen.html +# for more information. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_DOCSET = NO + +# This tag determines the name of the docset feed. A documentation feed provides +# an umbrella under which multiple documentation sets from a single provider +# (such as a company or product suite) can be grouped. +# The default value is: Doxygen generated docs. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_FEEDNAME = "Doxygen generated docs" + +# This tag specifies a string that should uniquely identify the documentation +# set bundle. This should be a reverse domain-name style string, e.g. +# com.mycompany.MyDocSet. Doxygen will append .docset to the name. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_BUNDLE_ID = org.doxygen.Project + +# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify +# the documentation publisher. This should be a reverse domain-name style +# string, e.g. com.mycompany.MyDocSet.documentation. +# The default value is: org.doxygen.Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_ID = org.doxygen.Publisher + +# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher. +# The default value is: Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_NAME = Publisher + +# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three +# additional HTML index files: index.hhp, index.hhc, and index.hhk. The +# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop +# (see: http://www.microsoft.com/en-us/download/details.aspx?id=21138) on +# Windows. +# +# The HTML Help Workshop contains a compiler that can convert all HTML output +# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML +# files are now used as the Windows 98 help format, and will replace the old +# Windows help format (.hlp) on all Windows platforms in the future. Compressed +# HTML files also contain an index, a table of contents, and you can search for +# words in the documentation. The HTML workshop also contains a viewer for +# compressed HTML files. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_HTMLHELP = NO + +# The CHM_FILE tag can be used to specify the file name of the resulting .chm +# file. You can add a path in front of the file if the result should not be +# written to the html output directory. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_FILE = + +# The HHC_LOCATION tag can be used to specify the location (absolute path +# including file name) of the HTML help compiler (hhc.exe). If non-empty, +# doxygen will try to run the HTML help compiler on the generated index.hhp. +# The file has to be specified with full path. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +HHC_LOCATION = + +# The GENERATE_CHI flag controls if a separate .chi index file is generated +# (YES) or that it should be included in the master .chm file (NO). +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +GENERATE_CHI = NO + +# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc) +# and project file content. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_INDEX_ENCODING = + +# The BINARY_TOC flag controls whether a binary table of contents is generated +# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it +# enables the Previous and Next buttons. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +BINARY_TOC = NO + +# The TOC_EXPAND flag can be set to YES to add extra items for group members to +# the table of contents of the HTML help documentation and to the tree view. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +TOC_EXPAND = NO + +# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and +# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that +# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help +# (.qch) of the generated HTML documentation. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_QHP = NO + +# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify +# the file name of the resulting .qch file. The path specified is relative to +# the HTML output folder. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QCH_FILE = + +# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help +# Project output. For more information please see Qt Help Project / Namespace +# (see: http://doc.qt.io/qt-4.8/qthelpproject.html#namespace). +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_NAMESPACE = org.doxygen.Project + +# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt +# Help Project output. For more information please see Qt Help Project / Virtual +# Folders (see: http://doc.qt.io/qt-4.8/qthelpproject.html#virtual-folders). +# The default value is: doc. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_VIRTUAL_FOLDER = doc + +# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom +# filter to add. For more information please see Qt Help Project / Custom +# Filters (see: http://doc.qt.io/qt-4.8/qthelpproject.html#custom-filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_NAME = + +# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the +# custom filter to add. For more information please see Qt Help Project / Custom +# Filters (see: http://doc.qt.io/qt-4.8/qthelpproject.html#custom-filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_ATTRS = + +# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this +# project's filter section matches. Qt Help Project / Filter Attributes (see: +# http://doc.qt.io/qt-4.8/qthelpproject.html#filter-attributes). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_SECT_FILTER_ATTRS = + +# The QHG_LOCATION tag can be used to specify the location of Qt's +# qhelpgenerator. If non-empty doxygen will try to run qhelpgenerator on the +# generated .qhp file. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHG_LOCATION = + +# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be +# generated, together with the HTML files, they form an Eclipse help plugin. To +# install this plugin and make it available under the help contents menu in +# Eclipse, the contents of the directory containing the HTML and XML files needs +# to be copied into the plugins directory of eclipse. The name of the directory +# within the plugins directory should be the same as the ECLIPSE_DOC_ID value. +# After copying Eclipse needs to be restarted before the help appears. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_ECLIPSEHELP = NO + +# A unique identifier for the Eclipse help plugin. When installing the plugin +# the directory name containing the HTML and XML files should also have this +# name. Each documentation set should have its own identifier. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES. + +ECLIPSE_DOC_ID = org.doxygen.Project + +# If you want full control over the layout of the generated HTML pages it might +# be necessary to disable the index and replace it with your own. The +# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top +# of each HTML page. A value of NO enables the index and the value YES disables +# it. Since the tabs in the index contain the same information as the navigation +# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +DISABLE_INDEX = NO + +# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index +# structure should be generated to display hierarchical information. If the tag +# value is set to YES, a side panel will be generated containing a tree-like +# index structure (just like the one that is generated for HTML Help). For this +# to work a browser that supports JavaScript, DHTML, CSS and frames is required +# (i.e. any modern browser). Windows users are probably better off using the +# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can +# further fine-tune the look of the index. As an example, the default style +# sheet generated by doxygen has an example that shows how to put an image at +# the root of the tree instead of the PROJECT_NAME. Since the tree basically has +# the same information as the tab index, you could consider setting +# DISABLE_INDEX to YES when enabling this option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_TREEVIEW = NO + +# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that +# doxygen will group on one line in the generated HTML documentation. +# +# Note that a value of 0 will completely suppress the enum values from appearing +# in the overview section. +# Minimum value: 0, maximum value: 20, default value: 4. +# This tag requires that the tag GENERATE_HTML is set to YES. + +ENUM_VALUES_PER_LINE = 4 + +# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used +# to set the initial width (in pixels) of the frame in which the tree is shown. +# Minimum value: 0, maximum value: 1500, default value: 250. +# This tag requires that the tag GENERATE_HTML is set to YES. + +TREEVIEW_WIDTH = 250 + +# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to +# external symbols imported via tag files in a separate window. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +EXT_LINKS_IN_WINDOW = NO + +# Use this tag to change the font size of LaTeX formulas included as images in +# the HTML documentation. When you change the font size after a successful +# doxygen run you need to manually remove any form_*.png images from the HTML +# output directory to force them to be regenerated. +# Minimum value: 8, maximum value: 50, default value: 10. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_FONTSIZE = 10 + +# Use the FORMULA_TRANSPARENT tag to determine whether or not the images +# generated for formulas are transparent PNGs. Transparent PNGs are not +# supported properly for IE 6.0, but are supported on all modern browsers. +# +# Note that when changing this option you need to delete any form_*.png files in +# the HTML output directory before the changes have effect. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_TRANSPARENT = YES + +# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see +# https://www.mathjax.org) which uses client side Javascript for the rendering +# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX +# installed or if you want to formulas look prettier in the HTML output. When +# enabled you may also need to install MathJax separately and configure the path +# to it using the MATHJAX_RELPATH option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +USE_MATHJAX = NO + +# When MathJax is enabled you can set the default output format to be used for +# the MathJax output. See the MathJax site (see: +# http://docs.mathjax.org/en/latest/output.html) for more details. +# Possible values are: HTML-CSS (which is slower, but has the best +# compatibility), NativeMML (i.e. MathML) and SVG. +# The default value is: HTML-CSS. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_FORMAT = HTML-CSS + +# When MathJax is enabled you need to specify the location relative to the HTML +# output directory using the MATHJAX_RELPATH option. The destination directory +# should contain the MathJax.js script. For instance, if the mathjax directory +# is located at the same level as the HTML output directory, then +# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax +# Content Delivery Network so you can quickly see the result without installing +# MathJax. However, it is strongly recommended to install a local copy of +# MathJax from https://www.mathjax.org before deployment. +# The default value is: https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.2/. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_RELPATH = https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.2/ + +# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax +# extension names that should be enabled during MathJax rendering. For example +# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_EXTENSIONS = + +# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces +# of code that will be used on startup of the MathJax code. See the MathJax site +# (see: http://docs.mathjax.org/en/latest/output.html) for more details. For an +# example see the documentation. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_CODEFILE = + +# When the SEARCHENGINE tag is enabled doxygen will generate a search box for +# the HTML output. The underlying search engine uses javascript and DHTML and +# should work on any modern browser. Note that when using HTML help +# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET) +# there is already a search function so this one should typically be disabled. +# For large projects the javascript based search engine can be slow, then +# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to +# search using the keyboard; to jump to the search box use + S +# (what the is depends on the OS and browser, but it is typically +# , /