@ohos-ports/confluentinc-kafka-javascript 1.10.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +457 -0
- package/LICENSE.kafkajs +31 -0
- package/LICENSE.node-rdkafka +25 -0
- package/LICENSE.txt +20 -0
- package/README.md +133 -0
- package/binding.gyp +184 -0
- package/build/Release/confluent-kafka-javascript.node +0 -0
- package/build/deps/librdkafka++.so.1 +0 -0
- package/build/deps/librdkafka.Makefile +6 -0
- package/build/deps/librdkafka.so.1 +0 -0
- package/build/deps/librdkafka.target.mk +55 -0
- package/deps/librdkafka/.clang-format +136 -0
- package/deps/librdkafka/.clang-format-cpp +103 -0
- package/deps/librdkafka/.dir-locals.el +10 -0
- package/deps/librdkafka/.formatignore +35 -0
- package/deps/librdkafka/.gdbmacros +19 -0
- package/deps/librdkafka/CHANGELOG.md +2477 -0
- package/deps/librdkafka/CMakeLists.txt +291 -0
- package/deps/librdkafka/CODE_OF_CONDUCT.md +46 -0
- package/deps/librdkafka/CONFIGURATION.md +212 -0
- package/deps/librdkafka/CONTRIBUTING.md +431 -0
- package/deps/librdkafka/Doxyfile +2375 -0
- package/deps/librdkafka/INTRODUCTION.md +2752 -0
- package/deps/librdkafka/LICENSE +26 -0
- package/deps/librdkafka/LICENSE.cjson +22 -0
- package/deps/librdkafka/LICENSE.crc32c +28 -0
- package/deps/librdkafka/LICENSE.fnv1a +18 -0
- package/deps/librdkafka/LICENSE.hdrhistogram +27 -0
- package/deps/librdkafka/LICENSE.lz4 +26 -0
- package/deps/librdkafka/LICENSE.murmur2 +25 -0
- package/deps/librdkafka/LICENSE.nanopb +22 -0
- package/deps/librdkafka/LICENSE.opentelemetry +203 -0
- package/deps/librdkafka/LICENSE.pycrc +23 -0
- package/deps/librdkafka/LICENSE.queue +31 -0
- package/deps/librdkafka/LICENSE.regexp +5 -0
- package/deps/librdkafka/LICENSE.snappy +36 -0
- package/deps/librdkafka/LICENSE.tinycthread +26 -0
- package/deps/librdkafka/LICENSE.wingetopt +49 -0
- package/deps/librdkafka/LICENSES.txt +625 -0
- package/deps/librdkafka/Makefile +125 -0
- package/deps/librdkafka/README.md +201 -0
- package/deps/librdkafka/README.win32 +26 -0
- package/deps/librdkafka/STATISTICS.md +624 -0
- package/deps/librdkafka/configure +214 -0
- package/deps/librdkafka/configure.self +341 -0
- package/deps/librdkafka/dev-conf.sh +123 -0
- package/deps/librdkafka/lds-gen.py +73 -0
- package/deps/librdkafka/mainpage.doxy +40 -0
- package/deps/librdkafka/mklove/Makefile.base +329 -0
- package/deps/librdkafka/mklove/modules/configure.atomics +144 -0
- package/deps/librdkafka/mklove/modules/configure.base +2484 -0
- package/deps/librdkafka/mklove/modules/configure.builtin +70 -0
- package/deps/librdkafka/mklove/modules/configure.cc +186 -0
- package/deps/librdkafka/mklove/modules/configure.cxx +8 -0
- package/deps/librdkafka/mklove/modules/configure.fileversion +65 -0
- package/deps/librdkafka/mklove/modules/configure.gitversion +29 -0
- package/deps/librdkafka/mklove/modules/configure.good_cflags +18 -0
- package/deps/librdkafka/mklove/modules/configure.host +132 -0
- package/deps/librdkafka/mklove/modules/configure.lib +49 -0
- package/deps/librdkafka/mklove/modules/configure.libcurl +99 -0
- package/deps/librdkafka/mklove/modules/configure.libsasl2 +36 -0
- package/deps/librdkafka/mklove/modules/configure.libssl +147 -0
- package/deps/librdkafka/mklove/modules/configure.libzstd +58 -0
- package/deps/librdkafka/mklove/modules/configure.parseversion +95 -0
- package/deps/librdkafka/mklove/modules/configure.pic +16 -0
- package/deps/librdkafka/mklove/modules/configure.socket +20 -0
- package/deps/librdkafka/mklove/modules/configure.zlib +61 -0
- package/deps/librdkafka/mklove/modules/patches/README.md +8 -0
- package/deps/librdkafka/mklove/modules/patches/libcurl.0000-no-runtime-linking-check.patch +11 -0
- package/deps/librdkafka/mklove/modules/patches/libssl.0000-osx-rand-include-fix-OpenSSL-PR16409.patch +56 -0
- package/deps/librdkafka/service.yml +182 -0
- package/deps/librdkafka/src/CMakeLists.txt +379 -0
- package/deps/librdkafka/src/Makefile +105 -0
- package/deps/librdkafka/src/README.lz4.md +30 -0
- package/deps/librdkafka/src/cJSON.c +3191 -0
- package/deps/librdkafka/src/cJSON.h +306 -0
- package/deps/librdkafka/src/crc32c.c +430 -0
- package/deps/librdkafka/src/crc32c.h +38 -0
- package/deps/librdkafka/src/generate_proto.sh +66 -0
- package/deps/librdkafka/src/librdkafka_cgrp_synch.png +0 -0
- package/deps/librdkafka/src/lz4.c +2727 -0
- package/deps/librdkafka/src/lz4.h +842 -0
- package/deps/librdkafka/src/lz4frame.c +2078 -0
- package/deps/librdkafka/src/lz4frame.h +692 -0
- package/deps/librdkafka/src/lz4frame_static.h +47 -0
- package/deps/librdkafka/src/lz4hc.c +1631 -0
- package/deps/librdkafka/src/lz4hc.h +413 -0
- package/deps/librdkafka/src/nanopb/pb.h +917 -0
- package/deps/librdkafka/src/nanopb/pb_common.c +388 -0
- package/deps/librdkafka/src/nanopb/pb_common.h +49 -0
- package/deps/librdkafka/src/nanopb/pb_decode.c +1727 -0
- package/deps/librdkafka/src/nanopb/pb_decode.h +193 -0
- package/deps/librdkafka/src/nanopb/pb_encode.c +1000 -0
- package/deps/librdkafka/src/nanopb/pb_encode.h +185 -0
- package/deps/librdkafka/src/opentelemetry/common.pb.c +32 -0
- package/deps/librdkafka/src/opentelemetry/common.pb.h +170 -0
- package/deps/librdkafka/src/opentelemetry/metrics.options +2 -0
- package/deps/librdkafka/src/opentelemetry/metrics.pb.c +67 -0
- package/deps/librdkafka/src/opentelemetry/metrics.pb.h +966 -0
- package/deps/librdkafka/src/opentelemetry/resource.pb.c +12 -0
- package/deps/librdkafka/src/opentelemetry/resource.pb.h +58 -0
- package/deps/librdkafka/src/queue.h +850 -0
- package/deps/librdkafka/src/rd.h +592 -0
- package/deps/librdkafka/src/rdaddr.c +255 -0
- package/deps/librdkafka/src/rdaddr.h +202 -0
- package/deps/librdkafka/src/rdatomic.h +262 -0
- package/deps/librdkafka/src/rdavg.h +260 -0
- package/deps/librdkafka/src/rdavl.c +210 -0
- package/deps/librdkafka/src/rdavl.h +250 -0
- package/deps/librdkafka/src/rdbase64.c +200 -0
- package/deps/librdkafka/src/rdbase64.h +43 -0
- package/deps/librdkafka/src/rdbuf.c +1884 -0
- package/deps/librdkafka/src/rdbuf.h +375 -0
- package/deps/librdkafka/src/rdcrc32.c +114 -0
- package/deps/librdkafka/src/rdcrc32.h +170 -0
- package/deps/librdkafka/src/rddl.c +179 -0
- package/deps/librdkafka/src/rddl.h +43 -0
- package/deps/librdkafka/src/rdendian.h +175 -0
- package/deps/librdkafka/src/rdfloat.h +67 -0
- package/deps/librdkafka/src/rdfnv1a.c +113 -0
- package/deps/librdkafka/src/rdfnv1a.h +35 -0
- package/deps/librdkafka/src/rdgz.c +120 -0
- package/deps/librdkafka/src/rdgz.h +46 -0
- package/deps/librdkafka/src/rdhdrhistogram.c +721 -0
- package/deps/librdkafka/src/rdhdrhistogram.h +87 -0
- package/deps/librdkafka/src/rdhttp.c +831 -0
- package/deps/librdkafka/src/rdhttp.h +101 -0
- package/deps/librdkafka/src/rdinterval.h +177 -0
- package/deps/librdkafka/src/rdkafka.c +7287 -0
- package/deps/librdkafka/src/rdkafka.h +11795 -0
- package/deps/librdkafka/src/rdkafka_admin.c +10165 -0
- package/deps/librdkafka/src/rdkafka_admin.h +658 -0
- package/deps/librdkafka/src/rdkafka_assignment.c +1282 -0
- package/deps/librdkafka/src/rdkafka_assignment.h +73 -0
- package/deps/librdkafka/src/rdkafka_assignor.c +1800 -0
- package/deps/librdkafka/src/rdkafka_assignor.h +402 -0
- package/deps/librdkafka/src/rdkafka_aux.c +409 -0
- package/deps/librdkafka/src/rdkafka_aux.h +174 -0
- package/deps/librdkafka/src/rdkafka_background.c +222 -0
- package/deps/librdkafka/src/rdkafka_broker.c +6736 -0
- package/deps/librdkafka/src/rdkafka_broker.h +851 -0
- package/deps/librdkafka/src/rdkafka_buf.c +543 -0
- package/deps/librdkafka/src/rdkafka_buf.h +1525 -0
- package/deps/librdkafka/src/rdkafka_cert.c +576 -0
- package/deps/librdkafka/src/rdkafka_cert.h +62 -0
- package/deps/librdkafka/src/rdkafka_cgrp.c +8310 -0
- package/deps/librdkafka/src/rdkafka_cgrp.h +589 -0
- package/deps/librdkafka/src/rdkafka_conf.c +5177 -0
- package/deps/librdkafka/src/rdkafka_conf.h +746 -0
- package/deps/librdkafka/src/rdkafka_confval.h +97 -0
- package/deps/librdkafka/src/rdkafka_coord.c +623 -0
- package/deps/librdkafka/src/rdkafka_coord.h +132 -0
- package/deps/librdkafka/src/rdkafka_error.c +228 -0
- package/deps/librdkafka/src/rdkafka_error.h +80 -0
- package/deps/librdkafka/src/rdkafka_event.c +502 -0
- package/deps/librdkafka/src/rdkafka_event.h +126 -0
- package/deps/librdkafka/src/rdkafka_feature.c +898 -0
- package/deps/librdkafka/src/rdkafka_feature.h +104 -0
- package/deps/librdkafka/src/rdkafka_fetcher.c +3820 -0
- package/deps/librdkafka/src/rdkafka_fetcher.h +62 -0
- package/deps/librdkafka/src/rdkafka_header.c +220 -0
- package/deps/librdkafka/src/rdkafka_header.h +76 -0
- package/deps/librdkafka/src/rdkafka_idempotence.c +807 -0
- package/deps/librdkafka/src/rdkafka_idempotence.h +144 -0
- package/deps/librdkafka/src/rdkafka_int.h +1505 -0
- package/deps/librdkafka/src/rdkafka_interceptor.c +842 -0
- package/deps/librdkafka/src/rdkafka_interceptor.h +104 -0
- package/deps/librdkafka/src/rdkafka_lz4.c +450 -0
- package/deps/librdkafka/src/rdkafka_lz4.h +49 -0
- package/deps/librdkafka/src/rdkafka_metadata.c +2347 -0
- package/deps/librdkafka/src/rdkafka_metadata.h +350 -0
- package/deps/librdkafka/src/rdkafka_metadata_cache.c +1183 -0
- package/deps/librdkafka/src/rdkafka_mock.c +4514 -0
- package/deps/librdkafka/src/rdkafka_mock.h +867 -0
- package/deps/librdkafka/src/rdkafka_mock_cgrp.c +1877 -0
- package/deps/librdkafka/src/rdkafka_mock_handlers.c +5255 -0
- package/deps/librdkafka/src/rdkafka_mock_int.h +1005 -0
- package/deps/librdkafka/src/rdkafka_mock_sharegrp.c +1207 -0
- package/deps/librdkafka/src/rdkafka_msg.c +2619 -0
- package/deps/librdkafka/src/rdkafka_msg.h +629 -0
- package/deps/librdkafka/src/rdkafka_msgbatch.h +62 -0
- package/deps/librdkafka/src/rdkafka_msgset.h +105 -0
- package/deps/librdkafka/src/rdkafka_msgset_reader.c +2099 -0
- package/deps/librdkafka/src/rdkafka_msgset_writer.c +1474 -0
- package/deps/librdkafka/src/rdkafka_offset.c +1565 -0
- package/deps/librdkafka/src/rdkafka_offset.h +150 -0
- package/deps/librdkafka/src/rdkafka_op.c +1219 -0
- package/deps/librdkafka/src/rdkafka_op.h +1065 -0
- package/deps/librdkafka/src/rdkafka_partition.c +5109 -0
- package/deps/librdkafka/src/rdkafka_partition.h +1215 -0
- package/deps/librdkafka/src/rdkafka_pattern.c +228 -0
- package/deps/librdkafka/src/rdkafka_pattern.h +70 -0
- package/deps/librdkafka/src/rdkafka_plugin.c +213 -0
- package/deps/librdkafka/src/rdkafka_plugin.h +41 -0
- package/deps/librdkafka/src/rdkafka_proto.h +739 -0
- package/deps/librdkafka/src/rdkafka_protocol.h +131 -0
- package/deps/librdkafka/src/rdkafka_queue.c +1351 -0
- package/deps/librdkafka/src/rdkafka_queue.h +1235 -0
- package/deps/librdkafka/src/rdkafka_range_assignor.c +1748 -0
- package/deps/librdkafka/src/rdkafka_request.c +7298 -0
- package/deps/librdkafka/src/rdkafka_request.h +761 -0
- package/deps/librdkafka/src/rdkafka_roundrobin_assignor.c +123 -0
- package/deps/librdkafka/src/rdkafka_sasl.c +589 -0
- package/deps/librdkafka/src/rdkafka_sasl.h +63 -0
- package/deps/librdkafka/src/rdkafka_sasl_cyrus.c +722 -0
- package/deps/librdkafka/src/rdkafka_sasl_int.h +89 -0
- package/deps/librdkafka/src/rdkafka_sasl_oauthbearer.c +1833 -0
- package/deps/librdkafka/src/rdkafka_sasl_oauthbearer.h +52 -0
- package/deps/librdkafka/src/rdkafka_sasl_oauthbearer_oidc.c +1827 -0
- package/deps/librdkafka/src/rdkafka_sasl_oauthbearer_oidc.h +52 -0
- package/deps/librdkafka/src/rdkafka_sasl_plain.c +142 -0
- package/deps/librdkafka/src/rdkafka_sasl_scram.c +858 -0
- package/deps/librdkafka/src/rdkafka_sasl_win32.c +550 -0
- package/deps/librdkafka/src/rdkafka_share_acknowledgement.c +1445 -0
- package/deps/librdkafka/src/rdkafka_share_acknowledgement.h +373 -0
- package/deps/librdkafka/src/rdkafka_ssl.c +2235 -0
- package/deps/librdkafka/src/rdkafka_ssl.h +86 -0
- package/deps/librdkafka/src/rdkafka_sticky_assignor.c +4785 -0
- package/deps/librdkafka/src/rdkafka_subscription.c +411 -0
- package/deps/librdkafka/src/rdkafka_telemetry.c +761 -0
- package/deps/librdkafka/src/rdkafka_telemetry.h +52 -0
- package/deps/librdkafka/src/rdkafka_telemetry_decode.c +1307 -0
- package/deps/librdkafka/src/rdkafka_telemetry_decode.h +59 -0
- package/deps/librdkafka/src/rdkafka_telemetry_encode.c +1309 -0
- package/deps/librdkafka/src/rdkafka_telemetry_encode.h +459 -0
- package/deps/librdkafka/src/rdkafka_timer.c +407 -0
- package/deps/librdkafka/src/rdkafka_timer.h +117 -0
- package/deps/librdkafka/src/rdkafka_topic.c +2382 -0
- package/deps/librdkafka/src/rdkafka_topic.h +369 -0
- package/deps/librdkafka/src/rdkafka_transport.c +1309 -0
- package/deps/librdkafka/src/rdkafka_transport.h +99 -0
- package/deps/librdkafka/src/rdkafka_transport_int.h +100 -0
- package/deps/librdkafka/src/rdkafka_txnmgr.c +3256 -0
- package/deps/librdkafka/src/rdkafka_txnmgr.h +171 -0
- package/deps/librdkafka/src/rdkafka_zstd.c +226 -0
- package/deps/librdkafka/src/rdkafka_zstd.h +57 -0
- package/deps/librdkafka/src/rdlist.c +593 -0
- package/deps/librdkafka/src/rdlist.h +444 -0
- package/deps/librdkafka/src/rdlog.c +89 -0
- package/deps/librdkafka/src/rdlog.h +41 -0
- package/deps/librdkafka/src/rdmap.c +508 -0
- package/deps/librdkafka/src/rdmap.h +492 -0
- package/deps/librdkafka/src/rdmurmur2.c +167 -0
- package/deps/librdkafka/src/rdmurmur2.h +35 -0
- package/deps/librdkafka/src/rdports.c +61 -0
- package/deps/librdkafka/src/rdports.h +38 -0
- package/deps/librdkafka/src/rdposix.h +250 -0
- package/deps/librdkafka/src/rdrand.c +151 -0
- package/deps/librdkafka/src/rdrand.h +72 -0
- package/deps/librdkafka/src/rdregex.c +156 -0
- package/deps/librdkafka/src/rdregex.h +43 -0
- package/deps/librdkafka/src/rdsignal.h +57 -0
- package/deps/librdkafka/src/rdstring.c +645 -0
- package/deps/librdkafka/src/rdstring.h +98 -0
- package/deps/librdkafka/src/rdsysqueue.h +404 -0
- package/deps/librdkafka/src/rdtime.h +356 -0
- package/deps/librdkafka/src/rdtypes.h +86 -0
- package/deps/librdkafka/src/rdunittest.c +564 -0
- package/deps/librdkafka/src/rdunittest.h +232 -0
- package/deps/librdkafka/src/rdunittest_acknowledge.c +1177 -0
- package/deps/librdkafka/src/rdunittest_fetcher.c +1211 -0
- package/deps/librdkafka/src/rdunittest_msgset_errors.c +1521 -0
- package/deps/librdkafka/src/rdvarint.c +134 -0
- package/deps/librdkafka/src/rdvarint.h +165 -0
- package/deps/librdkafka/src/rdwin32.h +382 -0
- package/deps/librdkafka/src/rdxxhash.c +1030 -0
- package/deps/librdkafka/src/rdxxhash.h +328 -0
- package/deps/librdkafka/src/regexp.c +1352 -0
- package/deps/librdkafka/src/regexp.h +41 -0
- package/deps/librdkafka/src/snappy.c +1866 -0
- package/deps/librdkafka/src/snappy.h +62 -0
- package/deps/librdkafka/src/snappy_compat.h +138 -0
- package/deps/librdkafka/src/statistics_schema.json +444 -0
- package/deps/librdkafka/src/tinycthread.c +932 -0
- package/deps/librdkafka/src/tinycthread.h +503 -0
- package/deps/librdkafka/src/tinycthread_extra.c +210 -0
- package/deps/librdkafka/src/tinycthread_extra.h +218 -0
- package/deps/librdkafka/src/win32_config.h +58 -0
- package/deps/librdkafka/src-cpp/CMakeLists.txt +90 -0
- package/deps/librdkafka/src-cpp/ConfImpl.cpp +84 -0
- package/deps/librdkafka/src-cpp/ConsumerImpl.cpp +244 -0
- package/deps/librdkafka/src-cpp/HandleImpl.cpp +436 -0
- package/deps/librdkafka/src-cpp/HeadersImpl.cpp +48 -0
- package/deps/librdkafka/src-cpp/KafkaConsumerImpl.cpp +296 -0
- package/deps/librdkafka/src-cpp/Makefile +55 -0
- package/deps/librdkafka/src-cpp/MessageImpl.cpp +38 -0
- package/deps/librdkafka/src-cpp/MetadataImpl.cpp +170 -0
- package/deps/librdkafka/src-cpp/ProducerImpl.cpp +197 -0
- package/deps/librdkafka/src-cpp/QueueImpl.cpp +70 -0
- package/deps/librdkafka/src-cpp/README.md +16 -0
- package/deps/librdkafka/src-cpp/RdKafka.cpp +59 -0
- package/deps/librdkafka/src-cpp/TopicImpl.cpp +124 -0
- package/deps/librdkafka/src-cpp/TopicPartitionImpl.cpp +57 -0
- package/deps/librdkafka/src-cpp/rdkafkacpp.h +3797 -0
- package/deps/librdkafka/src-cpp/rdkafkacpp_int.h +1641 -0
- package/deps/librdkafka/tests/0000-unittests.c +72 -0
- package/deps/librdkafka/tests/0001-multiobj.c +102 -0
- package/deps/librdkafka/tests/0002-unkpart.c +244 -0
- package/deps/librdkafka/tests/0003-msgmaxsize.c +173 -0
- package/deps/librdkafka/tests/0004-conf.c +934 -0
- package/deps/librdkafka/tests/0005-order.c +133 -0
- package/deps/librdkafka/tests/0006-symbols.c +163 -0
- package/deps/librdkafka/tests/0007-autotopic.c +136 -0
- package/deps/librdkafka/tests/0008-reqacks.c +179 -0
- package/deps/librdkafka/tests/0009-mock_cluster.c +272 -0
- package/deps/librdkafka/tests/0011-produce_batch.c +753 -0
- package/deps/librdkafka/tests/0012-produce_consume.c +537 -0
- package/deps/librdkafka/tests/0013-null-msgs.c +473 -0
- package/deps/librdkafka/tests/0014-reconsume-191.c +512 -0
- package/deps/librdkafka/tests/0015-offset_seeks.c +172 -0
- package/deps/librdkafka/tests/0016-client_swname.c +181 -0
- package/deps/librdkafka/tests/0017-compression.c +140 -0
- package/deps/librdkafka/tests/0018-cgrp_term.c +444 -0
- package/deps/librdkafka/tests/0019-list_groups.c +289 -0
- package/deps/librdkafka/tests/0020-destroy_hang.c +162 -0
- package/deps/librdkafka/tests/0021-rkt_destroy.c +72 -0
- package/deps/librdkafka/tests/0022-consume_batch.c +331 -0
- package/deps/librdkafka/tests/0025-timers.c +147 -0
- package/deps/librdkafka/tests/0026-consume_pause.c +547 -0
- package/deps/librdkafka/tests/0028-long_topicnames.c +79 -0
- package/deps/librdkafka/tests/0029-assign_offset.c +202 -0
- package/deps/librdkafka/tests/0030-offset_commit.c +589 -0
- package/deps/librdkafka/tests/0031-get_offsets.c +235 -0
- package/deps/librdkafka/tests/0033-regex_subscribe.c +536 -0
- package/deps/librdkafka/tests/0034-offset_reset.c +398 -0
- package/deps/librdkafka/tests/0035-api_version.c +73 -0
- package/deps/librdkafka/tests/0036-partial_fetch.c +87 -0
- package/deps/librdkafka/tests/0037-destroy_hang_local.c +85 -0
- package/deps/librdkafka/tests/0038-performance.c +121 -0
- package/deps/librdkafka/tests/0039-event.c +284 -0
- package/deps/librdkafka/tests/0040-io_event.c +257 -0
- package/deps/librdkafka/tests/0041-fetch_max_bytes.c +97 -0
- package/deps/librdkafka/tests/0042-many_topics.c +252 -0
- package/deps/librdkafka/tests/0043-no_connection.c +77 -0
- package/deps/librdkafka/tests/0044-partition_cnt.c +94 -0
- package/deps/librdkafka/tests/0045-subscribe_update.c +1010 -0
- package/deps/librdkafka/tests/0046-rkt_cache.c +65 -0
- package/deps/librdkafka/tests/0047-partial_buf_tmout.c +98 -0
- package/deps/librdkafka/tests/0048-partitioner.c +283 -0
- package/deps/librdkafka/tests/0049-consume_conn_close.c +162 -0
- package/deps/librdkafka/tests/0050-subscribe_adds.c +145 -0
- package/deps/librdkafka/tests/0051-assign_adds.c +126 -0
- package/deps/librdkafka/tests/0052-msg_timestamps.c +238 -0
- package/deps/librdkafka/tests/0053-stats_cb.cpp +527 -0
- package/deps/librdkafka/tests/0054-offset_time.cpp +236 -0
- package/deps/librdkafka/tests/0055-producer_latency.c +539 -0
- package/deps/librdkafka/tests/0056-balanced_group_mt.c +315 -0
- package/deps/librdkafka/tests/0057-invalid_topic.cpp +112 -0
- package/deps/librdkafka/tests/0058-log.cpp +123 -0
- package/deps/librdkafka/tests/0059-bsearch.cpp +241 -0
- package/deps/librdkafka/tests/0060-op_prio.cpp +163 -0
- package/deps/librdkafka/tests/0061-consumer_lag.cpp +295 -0
- package/deps/librdkafka/tests/0062-stats_event.c +126 -0
- package/deps/librdkafka/tests/0063-clusterid.cpp +180 -0
- package/deps/librdkafka/tests/0064-interceptors.c +529 -0
- package/deps/librdkafka/tests/0065-yield.cpp +140 -0
- package/deps/librdkafka/tests/0066-plugins.cpp +143 -0
- package/deps/librdkafka/tests/0067-empty_topic.cpp +151 -0
- package/deps/librdkafka/tests/0068-produce_timeout.c +136 -0
- package/deps/librdkafka/tests/0069-consumer_add_parts.c +119 -0
- package/deps/librdkafka/tests/0070-null_empty.cpp +197 -0
- package/deps/librdkafka/tests/0072-headers_ut.c +448 -0
- package/deps/librdkafka/tests/0073-headers.c +381 -0
- package/deps/librdkafka/tests/0074-producev.c +87 -0
- package/deps/librdkafka/tests/0075-retry.c +290 -0
- package/deps/librdkafka/tests/0076-produce_retry.c +452 -0
- package/deps/librdkafka/tests/0077-compaction.c +366 -0
- package/deps/librdkafka/tests/0078-c_from_cpp.cpp +96 -0
- package/deps/librdkafka/tests/0079-fork.c +93 -0
- package/deps/librdkafka/tests/0080-admin_ut.c +3095 -0
- package/deps/librdkafka/tests/0081-admin.c +5633 -0
- package/deps/librdkafka/tests/0082-fetch_max_bytes.cpp +137 -0
- package/deps/librdkafka/tests/0083-cb_event.c +233 -0
- package/deps/librdkafka/tests/0084-destroy_flags.c +208 -0
- package/deps/librdkafka/tests/0085-headers.cpp +392 -0
- package/deps/librdkafka/tests/0086-purge.c +368 -0
- package/deps/librdkafka/tests/0088-produce_metadata_timeout.c +162 -0
- package/deps/librdkafka/tests/0089-max_poll_interval.c +511 -0
- package/deps/librdkafka/tests/0090-idempotence.c +171 -0
- package/deps/librdkafka/tests/0091-max_poll_interval_timeout.c +295 -0
- package/deps/librdkafka/tests/0092-mixed_msgver.c +103 -0
- package/deps/librdkafka/tests/0093-holb.c +200 -0
- package/deps/librdkafka/tests/0094-idempotence_msg_timeout.c +231 -0
- package/deps/librdkafka/tests/0095-all_brokers_down.cpp +122 -0
- package/deps/librdkafka/tests/0097-ssl_verify.cpp +674 -0
- package/deps/librdkafka/tests/0098-consumer-txn.cpp +1218 -0
- package/deps/librdkafka/tests/0099-commit_metadata.c +194 -0
- package/deps/librdkafka/tests/0100-thread_interceptors.cpp +195 -0
- package/deps/librdkafka/tests/0101-fetch-from-follower.cpp +446 -0
- package/deps/librdkafka/tests/0102-static_group_rebalance.c +836 -0
- package/deps/librdkafka/tests/0103-transactions.c +1383 -0
- package/deps/librdkafka/tests/0104-fetch_from_follower_mock.c +625 -0
- package/deps/librdkafka/tests/0105-transactions_mock.c +3930 -0
- package/deps/librdkafka/tests/0106-cgrp_sess_timeout.c +318 -0
- package/deps/librdkafka/tests/0107-topic_recreate.c +259 -0
- package/deps/librdkafka/tests/0109-auto_create_topics.cpp +278 -0
- package/deps/librdkafka/tests/0110-batch_size.cpp +182 -0
- package/deps/librdkafka/tests/0111-delay_create_topics.cpp +127 -0
- package/deps/librdkafka/tests/0112-assign_unknown_part.c +87 -0
- package/deps/librdkafka/tests/0113-cooperative_rebalance.cpp +3473 -0
- package/deps/librdkafka/tests/0114-sticky_partitioning.cpp +176 -0
- package/deps/librdkafka/tests/0115-producer_auth.cpp +182 -0
- package/deps/librdkafka/tests/0116-kafkaconsumer_close.cpp +216 -0
- package/deps/librdkafka/tests/0117-mock_errors.c +331 -0
- package/deps/librdkafka/tests/0118-commit_rebalance.c +154 -0
- package/deps/librdkafka/tests/0119-consumer_auth.cpp +167 -0
- package/deps/librdkafka/tests/0120-asymmetric_subscription.c +185 -0
- package/deps/librdkafka/tests/0121-clusterid.c +115 -0
- package/deps/librdkafka/tests/0122-buffer_cleaning_after_rebalance.c +227 -0
- package/deps/librdkafka/tests/0123-connections_max_idle.c +98 -0
- package/deps/librdkafka/tests/0124-openssl_invalid_engine.c +69 -0
- package/deps/librdkafka/tests/0125-immediate_flush.c +144 -0
- package/deps/librdkafka/tests/0126-oauthbearer_oidc.c +1164 -0
- package/deps/librdkafka/tests/0127-fetch_queue_backoff.cpp +165 -0
- package/deps/librdkafka/tests/0128-sasl_callback_queue.cpp +217 -0
- package/deps/librdkafka/tests/0129-fetch_aborted_msgs.c +79 -0
- package/deps/librdkafka/tests/0130-store_offsets.c +178 -0
- package/deps/librdkafka/tests/0131-connect_timeout.c +81 -0
- package/deps/librdkafka/tests/0132-strategy_ordering.c +179 -0
- package/deps/librdkafka/tests/0133-ssl_keys.c +150 -0
- package/deps/librdkafka/tests/0134-ssl_provider.c +92 -0
- package/deps/librdkafka/tests/0135-sasl_credentials.cpp +279 -0
- package/deps/librdkafka/tests/0136-resolve_cb.c +181 -0
- package/deps/librdkafka/tests/0137-barrier_batch_consume.c +752 -0
- package/deps/librdkafka/tests/0138-admin_mock.c +281 -0
- package/deps/librdkafka/tests/0139-offset_validation_mock.c +950 -0
- package/deps/librdkafka/tests/0140-commit_metadata.cpp +108 -0
- package/deps/librdkafka/tests/0142-reauthentication.c +931 -0
- package/deps/librdkafka/tests/0143-exponential_backoff_mock.c +552 -0
- package/deps/librdkafka/tests/0144-idempotence_mock.c +373 -0
- package/deps/librdkafka/tests/0145-pause_resume_mock.c +119 -0
- package/deps/librdkafka/tests/0146-metadata_mock.c +505 -0
- package/deps/librdkafka/tests/0147-consumer_group_consumer_mock.c +1210 -0
- package/deps/librdkafka/tests/0148-offset_fetch_commit_error_mock.c +569 -0
- package/deps/librdkafka/tests/0149-broker-same-host-port.c +140 -0
- package/deps/librdkafka/tests/0150-telemetry_mock.c +651 -0
- package/deps/librdkafka/tests/0151-purge-brokers.c +566 -0
- package/deps/librdkafka/tests/0152-rebootstrap.c +59 -0
- package/deps/librdkafka/tests/0153-memberid.c +128 -0
- package/deps/librdkafka/tests/0155-share_group_heartbeat_mock.c +2695 -0
- package/deps/librdkafka/tests/0156-share_consumer_fetch_mock.c +1551 -0
- package/deps/librdkafka/tests/0157-share_consumer_ack_mock.c +1788 -0
- package/deps/librdkafka/tests/0158-share_consumer_transactions_mock.c +467 -0
- package/deps/librdkafka/tests/0170-share_consumer_subscription.c +2192 -0
- package/deps/librdkafka/tests/0171-share_consumer_consume.c +2150 -0
- package/deps/librdkafka/tests/0172-share_consumer_acknowledge.c +1919 -0
- package/deps/librdkafka/tests/0173-share_consumer_commit_async.c +2735 -0
- package/deps/librdkafka/tests/0174-share_consumer_concurrency.c +1434 -0
- package/deps/librdkafka/tests/0175-share_consumer_groups.c +574 -0
- package/deps/librdkafka/tests/0176-share_consumer_commit_sync.c +2986 -0
- package/deps/librdkafka/tests/0177-share_consumer_transactions.c +1815 -0
- package/deps/librdkafka/tests/0178-share_consumer_close.c +2102 -0
- package/deps/librdkafka/tests/0179-share_consumer_destroy.c +2482 -0
- package/deps/librdkafka/tests/0180-share_consumer_config.c +1677 -0
- package/deps/librdkafka/tests/0181-share_consumer_topic_delete.c +354 -0
- package/deps/librdkafka/tests/0182-share_consumer_error_handling_mock.c +3358 -0
- package/deps/librdkafka/tests/0183-share_consumer_leader_change_mock.c +1234 -0
- package/deps/librdkafka/tests/0184-share_consumer_topic_recreate.c +1356 -0
- package/deps/librdkafka/tests/0185-share_consumer_max_poll_interval.c +557 -0
- package/deps/librdkafka/tests/0186-share_consumer_fatal_error.c +360 -0
- package/deps/librdkafka/tests/0190-share_consumer_telemetry.c +387 -0
- package/deps/librdkafka/tests/1000-unktopic.c +164 -0
- package/deps/librdkafka/tests/8000-idle.cpp +60 -0
- package/deps/librdkafka/tests/8001-fetch_from_follower_mock_manual.c +113 -0
- package/deps/librdkafka/tests/CMakeLists.txt +193 -0
- package/deps/librdkafka/tests/LibrdkafkaTestApp.py +291 -0
- package/deps/librdkafka/tests/Makefile +182 -0
- package/deps/librdkafka/tests/README.md +514 -0
- package/deps/librdkafka/tests/autotest.sh +33 -0
- package/deps/librdkafka/tests/backtrace.gdb +30 -0
- package/deps/librdkafka/tests/broker_version_tests.py +315 -0
- package/deps/librdkafka/tests/buildbox.sh +17 -0
- package/deps/librdkafka/tests/cleanup-checker-tests.sh +20 -0
- package/deps/librdkafka/tests/cluster_testing.py +203 -0
- package/deps/librdkafka/tests/delete-test-topics.sh +56 -0
- package/deps/librdkafka/tests/fixtures/oauthbearer/jwt_assertion_template.json +10 -0
- package/deps/librdkafka/tests/fixtures/ssl/Makefile +8 -0
- package/deps/librdkafka/tests/fixtures/ssl/README.md +13 -0
- package/deps/librdkafka/tests/fixtures/ssl/client.keystore.intermediate.p12 +0 -0
- package/deps/librdkafka/tests/fixtures/ssl/client.keystore.p12 +0 -0
- package/deps/librdkafka/tests/fixtures/ssl/client2.certificate.intermediate.pem +72 -0
- package/deps/librdkafka/tests/fixtures/ssl/client2.certificate.pem +50 -0
- package/deps/librdkafka/tests/fixtures/ssl/client2.intermediate.key +46 -0
- package/deps/librdkafka/tests/fixtures/ssl/client2.key +46 -0
- package/deps/librdkafka/tests/fixtures/ssl/create_keys.sh +168 -0
- package/deps/librdkafka/tests/fuzzers/Makefile +12 -0
- package/deps/librdkafka/tests/fuzzers/README.md +31 -0
- package/deps/librdkafka/tests/fuzzers/fuzz_regex.c +74 -0
- package/deps/librdkafka/tests/fuzzers/helpers.h +90 -0
- package/deps/librdkafka/tests/gen-ssl-certs.sh +165 -0
- package/deps/librdkafka/tests/interactive_broker_version.py +170 -0
- package/deps/librdkafka/tests/interceptor_test/CMakeLists.txt +16 -0
- package/deps/librdkafka/tests/interceptor_test/Makefile +22 -0
- package/deps/librdkafka/tests/interceptor_test/interceptor_test.c +314 -0
- package/deps/librdkafka/tests/interceptor_test/interceptor_test.h +54 -0
- package/deps/librdkafka/tests/java/IncrementalRebalanceCli.java +97 -0
- package/deps/librdkafka/tests/java/Makefile +13 -0
- package/deps/librdkafka/tests/java/Murmur2Cli.java +46 -0
- package/deps/librdkafka/tests/java/README.md +14 -0
- package/deps/librdkafka/tests/java/TransactionProducerCli.java +162 -0
- package/deps/librdkafka/tests/java/run-class.sh +11 -0
- package/deps/librdkafka/tests/librdkafka.suppressions +483 -0
- package/deps/librdkafka/tests/lz4_manual_test.sh +59 -0
- package/deps/librdkafka/tests/multi-broker-version-test.sh +50 -0
- package/deps/librdkafka/tests/parse-refcnt.sh +43 -0
- package/deps/librdkafka/tests/performance_plot.py +115 -0
- package/deps/librdkafka/tests/plugin_test/Makefile +19 -0
- package/deps/librdkafka/tests/plugin_test/plugin_test.c +58 -0
- package/deps/librdkafka/tests/requirements.txt +2 -0
- package/deps/librdkafka/tests/run-all-tests.sh +81 -0
- package/deps/librdkafka/tests/run-consumer-tests.sh +16 -0
- package/deps/librdkafka/tests/run-producer-tests.sh +16 -0
- package/deps/librdkafka/tests/run-test-batches.py +157 -0
- package/deps/librdkafka/tests/run-test.sh +141 -0
- package/deps/librdkafka/tests/rusage.c +249 -0
- package/deps/librdkafka/tests/sasl_test.py +289 -0
- package/deps/librdkafka/tests/scenarios/README.md +6 -0
- package/deps/librdkafka/tests/scenarios/ak23.json +6 -0
- package/deps/librdkafka/tests/scenarios/default.json +5 -0
- package/deps/librdkafka/tests/scenarios/noautocreate.json +5 -0
- package/deps/librdkafka/tests/share_telemetry_verify/share-otel-collector-config.yaml +28 -0
- package/deps/librdkafka/tests/sockem.c +801 -0
- package/deps/librdkafka/tests/sockem.h +85 -0
- package/deps/librdkafka/tests/sockem_ctrl.c +145 -0
- package/deps/librdkafka/tests/sockem_ctrl.h +61 -0
- package/deps/librdkafka/tests/test.c +8376 -0
- package/deps/librdkafka/tests/test.conf.example +27 -0
- package/deps/librdkafka/tests/test.h +1150 -0
- package/deps/librdkafka/tests/testcpp.cpp +131 -0
- package/deps/librdkafka/tests/testcpp.h +388 -0
- package/deps/librdkafka/tests/testshared.h +416 -0
- package/deps/librdkafka/tests/tools/README.md +4 -0
- package/deps/librdkafka/tests/tools/stats/README.md +21 -0
- package/deps/librdkafka/tests/tools/stats/filter.jq +42 -0
- package/deps/librdkafka/tests/tools/stats/graph.py +150 -0
- package/deps/librdkafka/tests/tools/stats/requirements.txt +3 -0
- package/deps/librdkafka/tests/tools/stats/to_csv.py +124 -0
- package/deps/librdkafka/tests/trivup/trivup-0.15.0.tar.gz +0 -0
- package/deps/librdkafka/tests/until-fail.sh +87 -0
- package/deps/librdkafka/tests/xxxx-assign_partition.c +122 -0
- package/deps/librdkafka/tests/xxxx-metadata.cpp +159 -0
- package/deps/librdkafka/vcpkg.json +23 -0
- package/deps/librdkafka/win32/README.md +5 -0
- package/deps/librdkafka/win32/build-package.bat +3 -0
- package/deps/librdkafka/win32/build.bat +19 -0
- package/deps/librdkafka/win32/common.vcxproj +84 -0
- package/deps/librdkafka/win32/interceptor_test/interceptor_test.vcxproj +87 -0
- package/deps/librdkafka/win32/librdkafka.autopkg.template +54 -0
- package/deps/librdkafka/win32/librdkafka.master.testing.targets +13 -0
- package/deps/librdkafka/win32/librdkafka.sln +226 -0
- package/deps/librdkafka/win32/librdkafka.vcxproj +282 -0
- package/deps/librdkafka/win32/librdkafkacpp/librdkafkacpp.vcxproj +104 -0
- package/deps/librdkafka/win32/msbuild.ps1 +58 -0
- package/deps/librdkafka/win32/openssl_engine_example/openssl_engine_example.vcxproj +132 -0
- package/deps/librdkafka/win32/package-zip.ps1 +46 -0
- package/deps/librdkafka/win32/packages/repositories.config +4 -0
- package/deps/librdkafka/win32/push-package.bat +4 -0
- package/deps/librdkafka/win32/rdkafka_complex_consumer_example_cpp/rdkafka_complex_consumer_example_cpp.vcxproj +67 -0
- package/deps/librdkafka/win32/rdkafka_example/rdkafka_example.vcxproj +97 -0
- package/deps/librdkafka/win32/rdkafka_performance/rdkafka_performance.vcxproj +97 -0
- package/deps/librdkafka/win32/setup-msys2.ps1 +47 -0
- package/deps/librdkafka/win32/setup-vcpkg.ps1 +34 -0
- package/deps/librdkafka/win32/tests/test.conf.example +25 -0
- package/deps/librdkafka/win32/tests/tests.vcxproj +278 -0
- package/deps/librdkafka/win32/win_ssl_cert_store/win_ssl_cert_store.vcxproj +132 -0
- package/deps/librdkafka/win32/wingetopt.c +564 -0
- package/deps/librdkafka/win32/wingetopt.h +101 -0
- package/deps/librdkafka/win32/wintime.h +33 -0
- package/deps/librdkafka.gyp +83 -0
- package/deps/windows-install.py +80 -0
- package/index.d.ts +3 -0
- package/lib/admin.js +732 -0
- package/lib/client.js +679 -0
- package/lib/error.js +501 -0
- package/lib/index.js +8 -0
- package/lib/kafka-consumer-stream.js +399 -0
- package/lib/kafka-consumer.js +798 -0
- package/lib/kafkajs/_admin.js +1052 -0
- package/lib/kafkajs/_common.js +887 -0
- package/lib/kafkajs/_consumer.js +2156 -0
- package/lib/kafkajs/_consumer_cache.js +310 -0
- package/lib/kafkajs/_error.js +298 -0
- package/lib/kafkajs/_kafka.js +125 -0
- package/lib/kafkajs/_linked-list.js +220 -0
- package/lib/kafkajs/_producer.js +863 -0
- package/lib/kafkajs/index.js +5 -0
- package/lib/producer/high-level-producer.js +400 -0
- package/lib/producer-stream.js +337 -0
- package/lib/producer.js +405 -0
- package/lib/rdkafka.js +44 -0
- package/lib/tools/ref-counter.js +53 -0
- package/lib/topic-partition.js +91 -0
- package/lib/topic.js +43 -0
- package/lib/util.js +55 -0
- package/librdkafka.js +12 -0
- package/package.json +85 -0
- package/shim/source_location +54 -0
- package/src/admin.cc +1597 -0
- package/src/admin.h +112 -0
- package/src/binding.cc +76 -0
- package/src/binding.h +25 -0
- package/src/callbacks.cc +676 -0
- package/src/callbacks.h +302 -0
- package/src/common.cc +1628 -0
- package/src/common.h +192 -0
- package/src/config.cc +219 -0
- package/src/config.h +61 -0
- package/src/connection.cc +703 -0
- package/src/connection.h +117 -0
- package/src/errors.cc +119 -0
- package/src/errors.h +60 -0
- package/src/kafka-consumer.cc +1539 -0
- package/src/kafka-consumer.h +147 -0
- package/src/producer.cc +917 -0
- package/src/producer.h +124 -0
- package/src/topic.cc +173 -0
- package/src/topic.h +54 -0
- package/src/workers.cc +1714 -0
- package/src/workers.h +690 -0
- package/types/config.d.ts +1269 -0
- package/types/errors.d.ts +373 -0
- package/types/kafkajs.d.ts +568 -0
- package/types/rdkafka.d.ts +571 -0
- package/util/configure.js +32 -0
- package/util/generate-docs.sh +18 -0
- package/util/get-env.js +6 -0
- package/util/test-compile.js +11 -0
- package/util/test-producer-delivery.js +100 -0
|
@@ -0,0 +1,3358 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* librdkafka - Apache Kafka C library
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2026, Confluent Inc.
|
|
5
|
+
* All rights reserved.
|
|
6
|
+
*
|
|
7
|
+
* Redistribution and use in source and binary forms, with or without
|
|
8
|
+
* modification, are permitted provided that the following conditions are met:
|
|
9
|
+
*
|
|
10
|
+
* 1. Redistributions of source code must retain the above copyright notice,
|
|
11
|
+
* this list of conditions and the following disclaimer.
|
|
12
|
+
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
* this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
* and/or other materials provided with the distribution.
|
|
15
|
+
*
|
|
16
|
+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
17
|
+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
18
|
+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
19
|
+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
|
20
|
+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
21
|
+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
22
|
+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
23
|
+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
24
|
+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
25
|
+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
26
|
+
* POSSIBILITY OF SUCH DAMAGE.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
#include "test.h"
|
|
30
|
+
|
|
31
|
+
#include "../src/rdkafka_proto.h"
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @brief Share consumer top-level error propagation through ShareAcknowledge.
|
|
35
|
+
*
|
|
36
|
+
* Verifies that a top-level err on a ShareAcknowledge response reaches
|
|
37
|
+
* the per-partition rktpar->err that commit_sync reads, with no
|
|
38
|
+
* _IN_PROGRESS sentinel leaking to the caller. The broker-thread helper
|
|
39
|
+
* (rd_kafka_share_fetch_op_reply_with_err) sets err on each batch in
|
|
40
|
+
* ack_details; the main reply handler copies batch->rktpar->err into
|
|
41
|
+
* rkcg_commit_sync_request.results.
|
|
42
|
+
*
|
|
43
|
+
* Out of scope here (need mock enhancements not yet available):
|
|
44
|
+
* - Per-partition AcknowledgementErrorCode injection (mock only supports
|
|
45
|
+
* top-level err push)
|
|
46
|
+
* - Partition missing from ShareAcknowledge response (no mock API to
|
|
47
|
+
* drop a partition from response)
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
#define CONSUME_ARRAY 1024
|
|
51
|
+
|
|
52
|
+
/* ===================================================================
|
|
53
|
+
* Mock broker infrastructure (same pattern as 0176).
|
|
54
|
+
* =================================================================== */
|
|
55
|
+
typedef struct test_ctx_s {
|
|
56
|
+
rd_kafka_t *producer;
|
|
57
|
+
rd_kafka_mock_cluster_t *mcluster;
|
|
58
|
+
const char *bootstraps;
|
|
59
|
+
} test_ctx_t;
|
|
60
|
+
|
|
61
|
+
static test_ctx_t test_ctx_new_n(int nbrok) {
|
|
62
|
+
test_ctx_t ctx;
|
|
63
|
+
rd_kafka_conf_t *conf;
|
|
64
|
+
char errstr[512];
|
|
65
|
+
|
|
66
|
+
memset(&ctx, 0, sizeof(ctx));
|
|
67
|
+
|
|
68
|
+
ctx.mcluster = test_mock_cluster_new(nbrok, &ctx.bootstraps);
|
|
69
|
+
|
|
70
|
+
TEST_ASSERT(rd_kafka_mock_set_apiversion(
|
|
71
|
+
ctx.mcluster, RD_KAFKAP_ShareGroupHeartbeat, 1, 1) ==
|
|
72
|
+
RD_KAFKA_RESP_ERR_NO_ERROR,
|
|
73
|
+
"Failed to enable ShareGroupHeartbeat");
|
|
74
|
+
TEST_ASSERT(rd_kafka_mock_set_apiversion(ctx.mcluster,
|
|
75
|
+
RD_KAFKAP_ShareFetch, 1, 1) ==
|
|
76
|
+
RD_KAFKA_RESP_ERR_NO_ERROR,
|
|
77
|
+
"Failed to enable ShareFetch");
|
|
78
|
+
|
|
79
|
+
rd_kafka_mock_sharegroup_set_auto_offset_reset(ctx.mcluster, 1);
|
|
80
|
+
|
|
81
|
+
test_conf_init(&conf, NULL, 0);
|
|
82
|
+
test_conf_set(conf, "bootstrap.servers", ctx.bootstraps);
|
|
83
|
+
rd_kafka_conf_set_dr_msg_cb(conf, test_dr_msg_cb);
|
|
84
|
+
|
|
85
|
+
ctx.producer =
|
|
86
|
+
rd_kafka_new(RD_KAFKA_PRODUCER, conf, errstr, sizeof(errstr));
|
|
87
|
+
TEST_ASSERT(ctx.producer != NULL, "Failed to create producer: %s",
|
|
88
|
+
errstr);
|
|
89
|
+
|
|
90
|
+
return ctx;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
static test_ctx_t test_ctx_new(void) {
|
|
94
|
+
return test_ctx_new_n(1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
static void test_ctx_destroy(test_ctx_t *ctx) {
|
|
98
|
+
if (ctx->producer)
|
|
99
|
+
rd_kafka_destroy(ctx->producer);
|
|
100
|
+
if (ctx->mcluster)
|
|
101
|
+
test_mock_cluster_destroy(ctx->mcluster);
|
|
102
|
+
memset(ctx, 0, sizeof(*ctx));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
static rd_kafka_share_t *
|
|
106
|
+
create_mock_share_consumer(const char *bootstraps,
|
|
107
|
+
const char *group_id,
|
|
108
|
+
const char *ack_mode,
|
|
109
|
+
test_ack_cb_state_t *cb_state,
|
|
110
|
+
void (*cb)(rd_kafka_share_t *,
|
|
111
|
+
rd_kafka_share_partition_offsets_list_t *,
|
|
112
|
+
rd_kafka_resp_err_t,
|
|
113
|
+
void *)) {
|
|
114
|
+
rd_kafka_conf_t *conf;
|
|
115
|
+
rd_kafka_share_t *rkshare;
|
|
116
|
+
|
|
117
|
+
test_conf_init(&conf, NULL, 0);
|
|
118
|
+
test_conf_set(conf, "bootstrap.servers", bootstraps);
|
|
119
|
+
test_conf_set(conf, "group.id", group_id);
|
|
120
|
+
test_conf_set(conf, "share.acknowledgement.mode", ack_mode);
|
|
121
|
+
|
|
122
|
+
rkshare = rd_kafka_share_consumer_new(conf, NULL, 0);
|
|
123
|
+
TEST_ASSERT(rkshare != NULL, "Failed to create share consumer");
|
|
124
|
+
|
|
125
|
+
/* Register acknowledgement callback at runtime */
|
|
126
|
+
if (cb && cb_state) {
|
|
127
|
+
rd_kafka_error_t *error =
|
|
128
|
+
rd_kafka_share_set_acknowledgement_commit_cb(rkshare, cb,
|
|
129
|
+
cb_state);
|
|
130
|
+
TEST_ASSERT(error == NULL,
|
|
131
|
+
"Failed to set acknowledgement commit callback: "
|
|
132
|
+
"%s",
|
|
133
|
+
rd_kafka_error_string(error));
|
|
134
|
+
}
|
|
135
|
+
return rkshare;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
static void mock_produce(rd_kafka_t *producer, const char *topic, int msgcnt) {
|
|
139
|
+
int i;
|
|
140
|
+
for (i = 0; i < msgcnt; i++) {
|
|
141
|
+
char payload[64];
|
|
142
|
+
snprintf(payload, sizeof(payload), "%s-%d", topic, i);
|
|
143
|
+
TEST_ASSERT(rd_kafka_producev(
|
|
144
|
+
producer, RD_KAFKA_V_TOPIC(topic),
|
|
145
|
+
RD_KAFKA_V_VALUE(payload, strlen(payload)),
|
|
146
|
+
RD_KAFKA_V_MSGFLAGS(RD_KAFKA_MSG_F_COPY),
|
|
147
|
+
RD_KAFKA_V_END) == RD_KAFKA_RESP_ERR_NO_ERROR,
|
|
148
|
+
"Produce failed");
|
|
149
|
+
}
|
|
150
|
+
rd_kafka_flush(producer, 5000);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
static void mock_produce_partition(rd_kafka_t *producer,
|
|
154
|
+
const char *topic,
|
|
155
|
+
int32_t partition,
|
|
156
|
+
int msgcnt) {
|
|
157
|
+
int i;
|
|
158
|
+
for (i = 0; i < msgcnt; i++) {
|
|
159
|
+
char payload[64];
|
|
160
|
+
snprintf(payload, sizeof(payload), "%s-p%d-%d", topic,
|
|
161
|
+
(int)partition, i);
|
|
162
|
+
TEST_ASSERT(rd_kafka_producev(
|
|
163
|
+
producer, RD_KAFKA_V_TOPIC(topic),
|
|
164
|
+
RD_KAFKA_V_PARTITION(partition),
|
|
165
|
+
RD_KAFKA_V_VALUE(payload, strlen(payload)),
|
|
166
|
+
RD_KAFKA_V_MSGFLAGS(RD_KAFKA_MSG_F_COPY),
|
|
167
|
+
RD_KAFKA_V_END) == RD_KAFKA_RESP_ERR_NO_ERROR,
|
|
168
|
+
"Produce to partition %d failed", (int)partition);
|
|
169
|
+
}
|
|
170
|
+
rd_kafka_flush(producer, 5000);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @brief Consume up to msgcnt records and ACCEPT each, returning count
|
|
175
|
+
* actually acknowledged.
|
|
176
|
+
*/
|
|
177
|
+
static int consume_and_ack_all(rd_kafka_share_t *rkshare, int msgcnt) {
|
|
178
|
+
rd_kafka_messages_t *batch = NULL;
|
|
179
|
+
int acked = 0;
|
|
180
|
+
int attempts = 0;
|
|
181
|
+
|
|
182
|
+
while (acked < msgcnt && attempts++ < 30) {
|
|
183
|
+
size_t rcvd = 0;
|
|
184
|
+
size_t j;
|
|
185
|
+
rd_kafka_error_t *error =
|
|
186
|
+
rd_kafka_share_poll(rkshare, 3000, &batch);
|
|
187
|
+
if (error) {
|
|
188
|
+
rd_kafka_error_destroy(error);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
rcvd = rd_kafka_messages_count(batch);
|
|
192
|
+
for (j = 0; j < rcvd; j++) {
|
|
193
|
+
rd_kafka_message_t *rkm =
|
|
194
|
+
rd_kafka_messages_get(batch, j);
|
|
195
|
+
if (!rkm->err) {
|
|
196
|
+
rd_kafka_share_acknowledge(rkshare, rkm);
|
|
197
|
+
acked++;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
rd_kafka_messages_destroy(batch);
|
|
201
|
+
batch = NULL;
|
|
202
|
+
}
|
|
203
|
+
return acked;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/* ===================================================================
|
|
207
|
+
* Parameterized helper: inject one top-level err on next
|
|
208
|
+
* ShareAcknowledge response and verify commit_sync result carries
|
|
209
|
+
* that err on every partition.
|
|
210
|
+
*
|
|
211
|
+
* This exercises the broker-thread helper
|
|
212
|
+
* (rd_kafka_share_fetch_op_reply_with_err) + the main reply handler
|
|
213
|
+
* defensive _IN_PROGRESS sentinel together: regardless of which
|
|
214
|
+
* layer writes batch->rktpar->err, the commit_sync caller must see
|
|
215
|
+
* the top-level err for every partition that was sent to the broker.
|
|
216
|
+
* =================================================================== */
|
|
217
|
+
static void
|
|
218
|
+
do_test_commit_sync_top_level_err(const char *test_name,
|
|
219
|
+
rd_kafka_resp_err_t injected_err) {
|
|
220
|
+
test_ctx_t ctx;
|
|
221
|
+
rd_kafka_share_t *rkshare;
|
|
222
|
+
rd_kafka_topic_partition_list_t *partitions = NULL;
|
|
223
|
+
rd_kafka_error_t *error;
|
|
224
|
+
char topic[64];
|
|
225
|
+
char group[64];
|
|
226
|
+
const int msgcnt = 10;
|
|
227
|
+
int acked;
|
|
228
|
+
int i;
|
|
229
|
+
test_ack_cb_state_t cb_state = {0};
|
|
230
|
+
|
|
231
|
+
SUB_TEST_QUICK("%s -> %s", test_name, rd_kafka_err2name(injected_err));
|
|
232
|
+
|
|
233
|
+
ctx = test_ctx_new();
|
|
234
|
+
|
|
235
|
+
rd_snprintf(topic, sizeof(topic), "0182-%s", test_name);
|
|
236
|
+
rd_snprintf(group, sizeof(group), "sg-0182-%s", test_name);
|
|
237
|
+
|
|
238
|
+
TEST_ASSERT(rd_kafka_mock_topic_create(ctx.mcluster, topic, 1, 1) ==
|
|
239
|
+
RD_KAFKA_RESP_ERR_NO_ERROR,
|
|
240
|
+
"create topic");
|
|
241
|
+
|
|
242
|
+
mock_produce(ctx.producer, topic, msgcnt);
|
|
243
|
+
|
|
244
|
+
rkshare = create_mock_share_consumer(ctx.bootstraps, group, "explicit",
|
|
245
|
+
&cb_state, test_share_ack_cb);
|
|
246
|
+
test_share_consumer_subscribe_multi(rkshare, 1, topic);
|
|
247
|
+
|
|
248
|
+
acked = consume_and_ack_all(rkshare, msgcnt);
|
|
249
|
+
TEST_ASSERT(acked == msgcnt, "expected %d acked, got %d", msgcnt,
|
|
250
|
+
acked);
|
|
251
|
+
|
|
252
|
+
/* Inject the top-level err on the next ShareAcknowledge
|
|
253
|
+
* response from broker 1 (only broker in cluster). */
|
|
254
|
+
TEST_ASSERT(rd_kafka_mock_broker_push_request_error_rtts(
|
|
255
|
+
ctx.mcluster, 1, RD_KAFKAP_ShareAcknowledge, 1,
|
|
256
|
+
injected_err, 0) == RD_KAFKA_RESP_ERR_NO_ERROR,
|
|
257
|
+
"push error");
|
|
258
|
+
|
|
259
|
+
partitions = NULL;
|
|
260
|
+
error = rd_kafka_share_commit_sync(rkshare, 30000, &partitions);
|
|
261
|
+
|
|
262
|
+
if (error)
|
|
263
|
+
rd_kafka_error_destroy(error);
|
|
264
|
+
|
|
265
|
+
TEST_ASSERT(partitions != NULL,
|
|
266
|
+
"expected non-NULL partition results "
|
|
267
|
+
"(top-level err must surface per-partition, "
|
|
268
|
+
"not as _IN_PROGRESS leak)");
|
|
269
|
+
|
|
270
|
+
for (i = 0; i < partitions->cnt; i++) {
|
|
271
|
+
rd_kafka_topic_partition_t *rktpar = &partitions->elems[i];
|
|
272
|
+
TEST_SAY("%s [%" PRId32 "]: %s\n", rktpar->topic,
|
|
273
|
+
rktpar->partition, rd_kafka_err2name(rktpar->err));
|
|
274
|
+
TEST_ASSERT(rktpar->err == injected_err, "expected %s, got %s",
|
|
275
|
+
rd_kafka_err2name(injected_err),
|
|
276
|
+
rd_kafka_err2name(rktpar->err));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
rd_kafka_topic_partition_list_destroy(partitions);
|
|
280
|
+
|
|
281
|
+
/* Verify callback was invoked with the error */
|
|
282
|
+
TEST_ASSERT(cb_state.callback_cnt == 1, "expected 1 callback, got %d",
|
|
283
|
+
cb_state.callback_cnt);
|
|
284
|
+
TEST_ASSERT(test_ack_cb_state_first_err(&cb_state) == injected_err,
|
|
285
|
+
"expected callback err %s, got %s",
|
|
286
|
+
rd_kafka_err2name(injected_err),
|
|
287
|
+
rd_kafka_err2name(test_ack_cb_state_first_err(&cb_state)));
|
|
288
|
+
TEST_ASSERT(cb_state.total_offsets == msgcnt,
|
|
289
|
+
"expected callback total_offsets %d, got %zu", msgcnt,
|
|
290
|
+
cb_state.total_offsets);
|
|
291
|
+
|
|
292
|
+
test_share_consumer_close(rkshare);
|
|
293
|
+
test_share_destroy(rkshare);
|
|
294
|
+
test_ack_cb_state_destroy(&cb_state);
|
|
295
|
+
test_ctx_destroy(&ctx);
|
|
296
|
+
|
|
297
|
+
SUB_TEST_PASS();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/* Top-level session error: SHARE_SESSION_NOT_FOUND on ShareAcknowledge
|
|
301
|
+
* is propagated to every partition in commit_sync results (no
|
|
302
|
+
* _IN_PROGRESS leak). */
|
|
303
|
+
static void test_commit_sync_share_session_not_found(void) {
|
|
304
|
+
do_test_commit_sync_top_level_err(
|
|
305
|
+
"session-not-found", RD_KAFKA_RESP_ERR_SHARE_SESSION_NOT_FOUND);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/* Same path with a different session error code — confirms
|
|
309
|
+
* propagation isn't tied to a specific err. */
|
|
310
|
+
static void test_commit_sync_invalid_share_session_epoch(void) {
|
|
311
|
+
do_test_commit_sync_top_level_err(
|
|
312
|
+
"invalid-session-epoch",
|
|
313
|
+
RD_KAFKA_RESP_ERR_INVALID_SHARE_SESSION_EPOCH);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/* commit_sync surfaces a top-level SHARE_SESSION_LIMIT_REACHED from
|
|
317
|
+
* ShareAcknowledge without any client-side special handling. */
|
|
318
|
+
static void test_commit_sync_share_session_limit_reached(void) {
|
|
319
|
+
do_test_commit_sync_top_level_err(
|
|
320
|
+
"session-limit-reached",
|
|
321
|
+
RD_KAFKA_RESP_ERR_SHARE_SESSION_LIMIT_REACHED);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/* GROUP_AUTHORIZATION_FAILED on ShareAcknowledge propagates through
|
|
325
|
+
* the default-case path (previously hit `default: break` with no ack
|
|
326
|
+
* propagation). */
|
|
327
|
+
static void test_commit_sync_group_authorization_failed(void) {
|
|
328
|
+
do_test_commit_sync_top_level_err(
|
|
329
|
+
"group-auth-failed", RD_KAFKA_RESP_ERR_GROUP_AUTHORIZATION_FAILED);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/* Same default-case path as group-auth-failed with a different err. */
|
|
333
|
+
static void test_commit_sync_topic_authorization_failed(void) {
|
|
334
|
+
do_test_commit_sync_top_level_err(
|
|
335
|
+
"topic-auth-failed", RD_KAFKA_RESP_ERR_TOPIC_AUTHORIZATION_FAILED);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/* Generic protocol error to confirm unknown / fatal codes also
|
|
339
|
+
* propagate through the default-case path. */
|
|
340
|
+
static void test_commit_sync_invalid_request(void) {
|
|
341
|
+
do_test_commit_sync_top_level_err("invalid-request",
|
|
342
|
+
RD_KAFKA_RESP_ERR_INVALID_REQUEST);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/* ===================================================================
|
|
346
|
+
* Test — top-level error is propagated to all partitions in
|
|
347
|
+
* multi-partition acknowledgement.
|
|
348
|
+
*
|
|
349
|
+
* Creates a topic with multiple partitions, consumes and acknowledges
|
|
350
|
+
* records from all partitions, injects a top-level error on
|
|
351
|
+
* ShareAcknowledge, and verifies that commit_sync returns the error
|
|
352
|
+
* for EVERY partition that was part of the acknowledgement request.
|
|
353
|
+
*
|
|
354
|
+
* This ensures the error propagation logic correctly applies the
|
|
355
|
+
* top-level error to all partitions in the ack_details list, not
|
|
356
|
+
* just the first one.
|
|
357
|
+
* =================================================================== */
|
|
358
|
+
static void test_commit_sync_multi_partition_top_level_error(void) {
|
|
359
|
+
test_ctx_t ctx;
|
|
360
|
+
rd_kafka_share_t *rkshare;
|
|
361
|
+
rd_kafka_topic_partition_list_t *partitions = NULL;
|
|
362
|
+
rd_kafka_error_t *error;
|
|
363
|
+
const char *topic = "0182-multi-partition-error";
|
|
364
|
+
const char *group = "sg-0182-multi-partition-error";
|
|
365
|
+
const int partition_cnt = 3;
|
|
366
|
+
const int msgs_per_partition = 5;
|
|
367
|
+
const int total_msgs = partition_cnt * msgs_per_partition;
|
|
368
|
+
rd_kafka_resp_err_t injected_err =
|
|
369
|
+
RD_KAFKA_RESP_ERR_INVALID_SHARE_SESSION_EPOCH;
|
|
370
|
+
rd_kafka_messages_t *batches[CONSUME_ARRAY] = {0};
|
|
371
|
+
int batch_cnt = 0;
|
|
372
|
+
int total_consumed = 0;
|
|
373
|
+
int acked = 0;
|
|
374
|
+
int attempts = 0;
|
|
375
|
+
int i;
|
|
376
|
+
test_ack_cb_state_t cb_state = {0};
|
|
377
|
+
|
|
378
|
+
SUB_TEST_QUICK();
|
|
379
|
+
|
|
380
|
+
ctx = test_ctx_new();
|
|
381
|
+
|
|
382
|
+
/* Create topic with multiple partitions */
|
|
383
|
+
TEST_ASSERT(rd_kafka_mock_topic_create(ctx.mcluster, topic,
|
|
384
|
+
partition_cnt,
|
|
385
|
+
1) == RD_KAFKA_RESP_ERR_NO_ERROR,
|
|
386
|
+
"create topic with %d partitions", partition_cnt);
|
|
387
|
+
|
|
388
|
+
/* Produce messages to all partitions explicitly */
|
|
389
|
+
for (i = 0; i < partition_cnt; i++)
|
|
390
|
+
mock_produce_partition(ctx.producer, topic, i,
|
|
391
|
+
msgs_per_partition);
|
|
392
|
+
|
|
393
|
+
rkshare = create_mock_share_consumer(ctx.bootstraps, group, "explicit",
|
|
394
|
+
&cb_state, test_share_ack_cb);
|
|
395
|
+
test_share_consumer_subscribe_multi(rkshare, 1, topic);
|
|
396
|
+
|
|
397
|
+
/* Consume messages from all partitions */
|
|
398
|
+
while (total_consumed < total_msgs && attempts++ < 50) {
|
|
399
|
+
rd_kafka_messages_t *batch = NULL;
|
|
400
|
+
size_t rcvd = 0;
|
|
401
|
+
error = rd_kafka_share_poll(rkshare, 3000, &batch);
|
|
402
|
+
if (error) {
|
|
403
|
+
rd_kafka_error_destroy(error);
|
|
404
|
+
rd_kafka_messages_destroy(batch);
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
rcvd = rd_kafka_messages_count(batch);
|
|
408
|
+
if (rcvd == 0) {
|
|
409
|
+
rd_kafka_messages_destroy(batch);
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
TEST_ASSERT(batch_cnt < CONSUME_ARRAY, "batch buffer overflow");
|
|
413
|
+
batches[batch_cnt++] = batch;
|
|
414
|
+
total_consumed += (int)rcvd;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
TEST_ASSERT(total_consumed == total_msgs,
|
|
418
|
+
"expected to consume %d messages from %d partitions, "
|
|
419
|
+
"got %d",
|
|
420
|
+
total_msgs, partition_cnt, total_consumed);
|
|
421
|
+
|
|
422
|
+
/* Acknowledge all messages */
|
|
423
|
+
for (i = 0; i < batch_cnt; i++) {
|
|
424
|
+
size_t j;
|
|
425
|
+
size_t rcvd = rd_kafka_messages_count(batches[i]);
|
|
426
|
+
for (j = 0; j < rcvd; j++) {
|
|
427
|
+
rd_kafka_message_t *rkm =
|
|
428
|
+
rd_kafka_messages_get(batches[i], j);
|
|
429
|
+
if (!rkm->err) {
|
|
430
|
+
rd_kafka_share_acknowledge(rkshare, rkm);
|
|
431
|
+
acked++;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
TEST_ASSERT(acked == total_msgs, "expected to ack %d messages, got %d",
|
|
437
|
+
total_msgs, acked);
|
|
438
|
+
|
|
439
|
+
/* Inject top-level error on next ShareAcknowledge */
|
|
440
|
+
TEST_ASSERT(rd_kafka_mock_broker_push_request_error_rtts(
|
|
441
|
+
ctx.mcluster, 1, RD_KAFKAP_ShareAcknowledge, 1,
|
|
442
|
+
injected_err, 0) == RD_KAFKA_RESP_ERR_NO_ERROR,
|
|
443
|
+
"push error");
|
|
444
|
+
|
|
445
|
+
partitions = NULL;
|
|
446
|
+
error = rd_kafka_share_commit_sync(rkshare, 30000, &partitions);
|
|
447
|
+
|
|
448
|
+
if (error)
|
|
449
|
+
rd_kafka_error_destroy(error);
|
|
450
|
+
|
|
451
|
+
TEST_ASSERT(partitions != NULL, "expected non-NULL partition results");
|
|
452
|
+
|
|
453
|
+
/* Verify ALL partitions have the injected error */
|
|
454
|
+
TEST_ASSERT(partitions->cnt == partition_cnt,
|
|
455
|
+
"expected results for %d partitions, got %d", partition_cnt,
|
|
456
|
+
partitions->cnt);
|
|
457
|
+
|
|
458
|
+
for (i = 0; i < partitions->cnt; i++) {
|
|
459
|
+
rd_kafka_topic_partition_t *rktpar = &partitions->elems[i];
|
|
460
|
+
TEST_SAY("%s [%" PRId32 "]: %s\n", rktpar->topic,
|
|
461
|
+
rktpar->partition, rd_kafka_err2name(rktpar->err));
|
|
462
|
+
TEST_ASSERT(rktpar->err == injected_err,
|
|
463
|
+
"partition [%" PRId32 "]: expected %s, got %s",
|
|
464
|
+
rktpar->partition, rd_kafka_err2name(injected_err),
|
|
465
|
+
rd_kafka_err2name(rktpar->err));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
rd_kafka_topic_partition_list_destroy(partitions);
|
|
469
|
+
|
|
470
|
+
/* Verify callback was invoked with the error.
|
|
471
|
+
* Callback is invoked once per partition, so we expect
|
|
472
|
+
* partition_cnt callbacks. */
|
|
473
|
+
TEST_ASSERT(cb_state.callback_cnt == partition_cnt,
|
|
474
|
+
"expected %d callbacks (one per partition), got %d",
|
|
475
|
+
partition_cnt, cb_state.callback_cnt);
|
|
476
|
+
TEST_ASSERT(test_ack_cb_state_first_err(&cb_state) == injected_err,
|
|
477
|
+
"expected callback err %s, got %s",
|
|
478
|
+
rd_kafka_err2name(injected_err),
|
|
479
|
+
rd_kafka_err2name(test_ack_cb_state_first_err(&cb_state)));
|
|
480
|
+
TEST_ASSERT(cb_state.total_offsets == total_msgs,
|
|
481
|
+
"expected callback total_offsets %d, got %zu", total_msgs,
|
|
482
|
+
cb_state.total_offsets);
|
|
483
|
+
|
|
484
|
+
for (i = 0; i < batch_cnt; i++)
|
|
485
|
+
rd_kafka_messages_destroy(batches[i]);
|
|
486
|
+
|
|
487
|
+
test_share_consumer_close(rkshare);
|
|
488
|
+
test_share_destroy(rkshare);
|
|
489
|
+
test_ack_cb_state_destroy(&cb_state);
|
|
490
|
+
test_ctx_destroy(&ctx);
|
|
491
|
+
|
|
492
|
+
SUB_TEST_PASS();
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/* ===================================================================
|
|
496
|
+
* Test — top-level error on ShareFetch with piggybacked acks is
|
|
497
|
+
* propagated to callback for all partitions.
|
|
498
|
+
*
|
|
499
|
+
* Uses implicit mode where acks are piggybacked on ShareFetch.
|
|
500
|
+
* Creates a topic with multiple partitions, consumes messages in
|
|
501
|
+
* implicit mode (auto-acknowledge), then injects a top-level error
|
|
502
|
+
* on the next ShareFetch request (which carries piggybacked acks).
|
|
503
|
+
*
|
|
504
|
+
* Verifies that the acknowledgement callback is invoked with the
|
|
505
|
+
* error for all partitions that had piggybacked acks.
|
|
506
|
+
* =================================================================== */
|
|
507
|
+
static void test_consume_batch_multi_partition_top_level_error(void) {
|
|
508
|
+
test_ctx_t ctx;
|
|
509
|
+
rd_kafka_share_t *rkshare;
|
|
510
|
+
rd_kafka_error_t *error;
|
|
511
|
+
const char *topic = "0182-consume-multi-partition-error";
|
|
512
|
+
const char *group = "sg-0182-consume-multi-partition-error";
|
|
513
|
+
const int partition_cnt = 3;
|
|
514
|
+
const int msgs_per_partition = 5;
|
|
515
|
+
const int total_msgs = partition_cnt * msgs_per_partition;
|
|
516
|
+
rd_kafka_resp_err_t injected_err =
|
|
517
|
+
RD_KAFKA_RESP_ERR_SHARE_SESSION_NOT_FOUND;
|
|
518
|
+
rd_kafka_messages_t *batch = NULL;
|
|
519
|
+
int total_consumed = 0;
|
|
520
|
+
int attempts = 0;
|
|
521
|
+
int i;
|
|
522
|
+
test_ack_cb_state_t cb_state = {0};
|
|
523
|
+
|
|
524
|
+
SUB_TEST_QUICK();
|
|
525
|
+
|
|
526
|
+
ctx = test_ctx_new();
|
|
527
|
+
|
|
528
|
+
/* Create topic with multiple partitions */
|
|
529
|
+
TEST_ASSERT(rd_kafka_mock_topic_create(ctx.mcluster, topic,
|
|
530
|
+
partition_cnt,
|
|
531
|
+
1) == RD_KAFKA_RESP_ERR_NO_ERROR,
|
|
532
|
+
"create topic with %d partitions", partition_cnt);
|
|
533
|
+
|
|
534
|
+
/* Produce messages to all partitions explicitly */
|
|
535
|
+
for (i = 0; i < partition_cnt; i++)
|
|
536
|
+
mock_produce_partition(ctx.producer, topic, i,
|
|
537
|
+
msgs_per_partition);
|
|
538
|
+
|
|
539
|
+
/* Use implicit mode - acks are piggybacked on ShareFetch */
|
|
540
|
+
rkshare = create_mock_share_consumer(ctx.bootstraps, group, "implicit",
|
|
541
|
+
&cb_state, test_share_ack_cb);
|
|
542
|
+
test_share_consumer_subscribe_multi(rkshare, 1, topic);
|
|
543
|
+
|
|
544
|
+
/* First consume batch - establishes session, consumes messages */
|
|
545
|
+
while (total_consumed < total_msgs && attempts++ < 50) {
|
|
546
|
+
size_t rcvd = 0;
|
|
547
|
+
error = rd_kafka_share_poll(rkshare, 3000, &batch);
|
|
548
|
+
if (error) {
|
|
549
|
+
rd_kafka_error_destroy(error);
|
|
550
|
+
rd_kafka_messages_destroy(batch);
|
|
551
|
+
batch = NULL;
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
rcvd = rd_kafka_messages_count(batch);
|
|
555
|
+
total_consumed += (int)rcvd;
|
|
556
|
+
/* Destroy messages - in implicit mode they're
|
|
557
|
+
* auto-acknowledged */
|
|
558
|
+
rd_kafka_messages_destroy(batch);
|
|
559
|
+
batch = NULL;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
TEST_ASSERT(total_consumed == total_msgs,
|
|
563
|
+
"expected to consume %d messages from %d partitions, "
|
|
564
|
+
"got %d",
|
|
565
|
+
total_msgs, partition_cnt, total_consumed);
|
|
566
|
+
|
|
567
|
+
/* Produce more messages to trigger another ShareFetch with
|
|
568
|
+
* piggybacked acks from the previous consume */
|
|
569
|
+
for (i = 0; i < partition_cnt; i++)
|
|
570
|
+
mock_produce_partition(ctx.producer, topic, i,
|
|
571
|
+
msgs_per_partition);
|
|
572
|
+
|
|
573
|
+
/* Inject top-level error on next ShareFetch (which will carry
|
|
574
|
+
* piggybacked acks from the previous consume_batch) */
|
|
575
|
+
TEST_ASSERT(rd_kafka_mock_broker_push_request_error_rtts(
|
|
576
|
+
ctx.mcluster, 1, RD_KAFKAP_ShareFetch, 1, injected_err,
|
|
577
|
+
0) == RD_KAFKA_RESP_ERR_NO_ERROR,
|
|
578
|
+
"push error on ShareFetch");
|
|
579
|
+
|
|
580
|
+
/* Next consume_batch triggers ShareFetch with piggybacked acks.
|
|
581
|
+
* The error should trigger the callback for the piggybacked acks. */
|
|
582
|
+
total_consumed = 0;
|
|
583
|
+
attempts = 0;
|
|
584
|
+
while (total_consumed < total_msgs && attempts++ < 50) {
|
|
585
|
+
size_t rcvd = 0;
|
|
586
|
+
size_t j;
|
|
587
|
+
error = rd_kafka_share_poll(rkshare, 3000, &batch);
|
|
588
|
+
if (error) {
|
|
589
|
+
rd_kafka_error_destroy(error);
|
|
590
|
+
/* May get errors due to injected error */
|
|
591
|
+
}
|
|
592
|
+
rcvd = rd_kafka_messages_count(batch);
|
|
593
|
+
for (j = 0; j < rcvd; j++) {
|
|
594
|
+
rd_kafka_message_t *rkm =
|
|
595
|
+
rd_kafka_messages_get(batch, j);
|
|
596
|
+
if (!rkm->err)
|
|
597
|
+
total_consumed++;
|
|
598
|
+
}
|
|
599
|
+
rd_kafka_messages_destroy(batch);
|
|
600
|
+
batch = NULL;
|
|
601
|
+
/* Break after we've given the callback a chance to fire */
|
|
602
|
+
if (cb_state.callback_cnt > 0)
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/* Verify callback was invoked with the error for piggybacked acks.
|
|
607
|
+
* The callback should have been invoked for the acks that were
|
|
608
|
+
* piggybacked on the ShareFetch that got the error. */
|
|
609
|
+
TEST_ASSERT(cb_state.callback_cnt >= 1,
|
|
610
|
+
"expected at least 1 callback for piggybacked acks, got %d",
|
|
611
|
+
cb_state.callback_cnt);
|
|
612
|
+
TEST_ASSERT(test_ack_cb_state_first_err(&cb_state) == injected_err,
|
|
613
|
+
"expected callback err %s, got %s",
|
|
614
|
+
rd_kafka_err2name(injected_err),
|
|
615
|
+
rd_kafka_err2name(test_ack_cb_state_first_err(&cb_state)));
|
|
616
|
+
/* In implicit mode, we expect the callback to be invoked for the
|
|
617
|
+
* first batch of messages that were piggybacked */
|
|
618
|
+
TEST_ASSERT(cb_state.total_offsets > 0,
|
|
619
|
+
"expected callback total_offsets > 0, got %zu",
|
|
620
|
+
cb_state.total_offsets);
|
|
621
|
+
|
|
622
|
+
TEST_SAY(
|
|
623
|
+
"Callback invoked %d times with %zu total offsets, first_err=%s\n",
|
|
624
|
+
cb_state.callback_cnt, cb_state.total_offsets,
|
|
625
|
+
rd_kafka_err2name(test_ack_cb_state_first_err(&cb_state)));
|
|
626
|
+
|
|
627
|
+
test_share_consumer_close(rkshare);
|
|
628
|
+
test_share_destroy(rkshare);
|
|
629
|
+
test_ack_cb_state_destroy(&cb_state);
|
|
630
|
+
test_ctx_destroy(&ctx);
|
|
631
|
+
|
|
632
|
+
SUB_TEST_PASS();
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/* ===================================================================
|
|
636
|
+
* Test — commit_sync at session epoch 0 returns
|
|
637
|
+
* INVALID_SHARE_SESSION_EPOCH without sending a
|
|
638
|
+
* ShareAcknowledge request.
|
|
639
|
+
*
|
|
640
|
+
* When the broker session epoch is 0 (new consumer or post-reset)
|
|
641
|
+
* there is no session state to acknowledge against, so the client
|
|
642
|
+
* must fail acks locally.
|
|
643
|
+
*
|
|
644
|
+
* Two-phase test:
|
|
645
|
+
* Phase 1: Trigger session reset by injecting
|
|
646
|
+
* SHARE_SESSION_NOT_FOUND on the first ShareAcknowledge.
|
|
647
|
+
* commit_sync surfaces SHARE_SESSION_NOT_FOUND for the
|
|
648
|
+
* partition; broker thread resets epoch to 0.
|
|
649
|
+
* Phase 2: Acknowledge remaining records and call commit_sync
|
|
650
|
+
* again. With epoch 0 the client fails acks locally: no
|
|
651
|
+
* ShareAcknowledge is sent, commit_sync returns
|
|
652
|
+
* INVALID_SHARE_SESSION_EPOCH for the partition.
|
|
653
|
+
* =================================================================== */
|
|
654
|
+
static rd_bool_t is_share_ack_request(rd_kafka_mock_request_t *request,
|
|
655
|
+
void *opaque) {
|
|
656
|
+
return rd_kafka_mock_request_api_key(request) ==
|
|
657
|
+
RD_KAFKAP_ShareAcknowledge;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
static void
|
|
661
|
+
test_commit_sync_at_epoch_zero_returns_invalid_session_epoch_error(void) {
|
|
662
|
+
test_ctx_t ctx;
|
|
663
|
+
rd_kafka_share_t *rkshare;
|
|
664
|
+
rd_kafka_topic_partition_list_t *partitions = NULL;
|
|
665
|
+
rd_kafka_error_t *error;
|
|
666
|
+
const char *topic = "0182-epoch-zero-ack";
|
|
667
|
+
const char *group = "sg-0182-epoch-zero-ack";
|
|
668
|
+
const int msgcnt = 10;
|
|
669
|
+
rd_kafka_message_t *rkmessages[CONSUME_ARRAY];
|
|
670
|
+
rd_kafka_messages_t *batches[CONSUME_ARRAY] = {0};
|
|
671
|
+
int batch_cnt = 0;
|
|
672
|
+
int total_consumed = 0;
|
|
673
|
+
int attempts = 0;
|
|
674
|
+
size_t share_ack_cnt;
|
|
675
|
+
int i;
|
|
676
|
+
test_ack_cb_state_t cb_state = {0};
|
|
677
|
+
|
|
678
|
+
SUB_TEST_QUICK();
|
|
679
|
+
|
|
680
|
+
ctx = test_ctx_new();
|
|
681
|
+
|
|
682
|
+
TEST_ASSERT(rd_kafka_mock_topic_create(ctx.mcluster, topic, 1, 1) ==
|
|
683
|
+
RD_KAFKA_RESP_ERR_NO_ERROR,
|
|
684
|
+
"create topic");
|
|
685
|
+
|
|
686
|
+
mock_produce(ctx.producer, topic, msgcnt);
|
|
687
|
+
|
|
688
|
+
rkshare = create_mock_share_consumer(ctx.bootstraps, group, "explicit",
|
|
689
|
+
&cb_state, test_share_ack_cb);
|
|
690
|
+
test_share_consumer_subscribe_multi(rkshare, 1, topic);
|
|
691
|
+
|
|
692
|
+
/* Phase 0: consume all 10 records. Hold message handles for
|
|
693
|
+
* acknowledge in phase 1 and phase 2. */
|
|
694
|
+
while (total_consumed < msgcnt && attempts++ < 30) {
|
|
695
|
+
rd_kafka_messages_t *batch = NULL;
|
|
696
|
+
size_t rcvd = 0;
|
|
697
|
+
size_t j;
|
|
698
|
+
error = rd_kafka_share_poll(rkshare, 3000, &batch);
|
|
699
|
+
if (error) {
|
|
700
|
+
rd_kafka_error_destroy(error);
|
|
701
|
+
rd_kafka_messages_destroy(batch);
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
rcvd = rd_kafka_messages_count(batch);
|
|
705
|
+
if (rcvd == 0) {
|
|
706
|
+
rd_kafka_messages_destroy(batch);
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
/* Flatten message handles into rkmessages for the
|
|
710
|
+
* phase-1/phase-2 partial-ack indexing pattern. */
|
|
711
|
+
for (j = 0; j < rcvd && total_consumed < CONSUME_ARRAY; j++)
|
|
712
|
+
rkmessages[total_consumed++] =
|
|
713
|
+
rd_kafka_messages_get(batch, j);
|
|
714
|
+
TEST_ASSERT(batch_cnt < CONSUME_ARRAY, "batch buffer overflow");
|
|
715
|
+
batches[batch_cnt++] = batch;
|
|
716
|
+
}
|
|
717
|
+
TEST_ASSERT(total_consumed == msgcnt,
|
|
718
|
+
"Phase 0: expected %d records, got %d", msgcnt,
|
|
719
|
+
total_consumed);
|
|
720
|
+
|
|
721
|
+
/* Phase 1: ACCEPT first 5 records, inject SHARE_SESSION_NOT_FOUND
|
|
722
|
+
* on next ShareAcknowledge, call commit_sync and verify the err
|
|
723
|
+
* propagates. The buf reply handler resets the broker session
|
|
724
|
+
* epoch to 0 on this error. */
|
|
725
|
+
for (i = 0; i < 5; i++)
|
|
726
|
+
rd_kafka_share_acknowledge(rkshare, rkmessages[i]);
|
|
727
|
+
|
|
728
|
+
rd_kafka_mock_start_request_tracking(ctx.mcluster);
|
|
729
|
+
rd_kafka_mock_clear_requests(ctx.mcluster);
|
|
730
|
+
|
|
731
|
+
TEST_ASSERT(rd_kafka_mock_broker_push_request_error_rtts(
|
|
732
|
+
ctx.mcluster, 1, RD_KAFKAP_ShareAcknowledge, 1,
|
|
733
|
+
RD_KAFKA_RESP_ERR_SHARE_SESSION_NOT_FOUND,
|
|
734
|
+
0) == RD_KAFKA_RESP_ERR_NO_ERROR,
|
|
735
|
+
"push SHARE_SESSION_NOT_FOUND");
|
|
736
|
+
|
|
737
|
+
partitions = NULL;
|
|
738
|
+
error = rd_kafka_share_commit_sync(rkshare, 1000, &partitions);
|
|
739
|
+
if (error)
|
|
740
|
+
rd_kafka_error_destroy(error);
|
|
741
|
+
|
|
742
|
+
TEST_ASSERT(partitions != NULL,
|
|
743
|
+
"Phase 1: expected non-NULL partition results");
|
|
744
|
+
for (i = 0; i < partitions->cnt; i++) {
|
|
745
|
+
rd_kafka_topic_partition_t *p = &partitions->elems[i];
|
|
746
|
+
TEST_SAY("Phase 1 %s [%" PRId32 "]: %s\n", p->topic,
|
|
747
|
+
p->partition, rd_kafka_err2name(p->err));
|
|
748
|
+
TEST_ASSERT(p->err == RD_KAFKA_RESP_ERR_SHARE_SESSION_NOT_FOUND,
|
|
749
|
+
"Phase 1: expected SHARE_SESSION_NOT_FOUND, "
|
|
750
|
+
"got %s",
|
|
751
|
+
rd_kafka_err2name(p->err));
|
|
752
|
+
}
|
|
753
|
+
rd_kafka_topic_partition_list_destroy(partitions);
|
|
754
|
+
|
|
755
|
+
share_ack_cnt = test_mock_get_matching_request_cnt(
|
|
756
|
+
ctx.mcluster, is_share_ack_request, NULL);
|
|
757
|
+
TEST_ASSERT(share_ack_cnt == 1,
|
|
758
|
+
"Phase 1: expected 1 ShareAck request, got %" PRIusz,
|
|
759
|
+
share_ack_cnt);
|
|
760
|
+
|
|
761
|
+
/* Verify Phase 1 callback was invoked with SHARE_SESSION_NOT_FOUND */
|
|
762
|
+
TEST_ASSERT(cb_state.callback_cnt == 1,
|
|
763
|
+
"Phase 1: expected 1 callback, got %d",
|
|
764
|
+
cb_state.callback_cnt);
|
|
765
|
+
TEST_ASSERT(
|
|
766
|
+
test_ack_cb_state_first_err(&cb_state) ==
|
|
767
|
+
RD_KAFKA_RESP_ERR_SHARE_SESSION_NOT_FOUND,
|
|
768
|
+
"Phase 1: expected callback err SHARE_SESSION_NOT_FOUND, got %s",
|
|
769
|
+
rd_kafka_err2name(test_ack_cb_state_first_err(&cb_state)));
|
|
770
|
+
TEST_ASSERT(cb_state.total_offsets == 5,
|
|
771
|
+
"Phase 1: expected 5 offsets in callback, got %zu",
|
|
772
|
+
cb_state.total_offsets);
|
|
773
|
+
|
|
774
|
+
/* Reset callback state for Phase 2 */
|
|
775
|
+
test_ack_cb_state_destroy(&cb_state);
|
|
776
|
+
|
|
777
|
+
/* Phase 2: ACCEPT remaining 5 records and call commit_sync
|
|
778
|
+
* again. Broker epoch is 0 (session reset by phase 1). B4a
|
|
779
|
+
* must fire: no ShareAck request sent, commit_sync returns
|
|
780
|
+
* INVALID_SHARE_SESSION_EPOCH per partition. */
|
|
781
|
+
rd_kafka_mock_clear_requests(ctx.mcluster);
|
|
782
|
+
|
|
783
|
+
for (i = 5; i < msgcnt; i++)
|
|
784
|
+
rd_kafka_share_acknowledge(rkshare, rkmessages[i]);
|
|
785
|
+
|
|
786
|
+
partitions = NULL;
|
|
787
|
+
error = rd_kafka_share_commit_sync(rkshare, 1000, &partitions);
|
|
788
|
+
if (error)
|
|
789
|
+
rd_kafka_error_destroy(error);
|
|
790
|
+
|
|
791
|
+
TEST_ASSERT(partitions != NULL,
|
|
792
|
+
"Phase 2: expected non-NULL partition results");
|
|
793
|
+
for (i = 0; i < partitions->cnt; i++) {
|
|
794
|
+
rd_kafka_topic_partition_t *p = &partitions->elems[i];
|
|
795
|
+
TEST_SAY("Phase 2 %s [%" PRId32 "]: %s\n", p->topic,
|
|
796
|
+
p->partition, rd_kafka_err2name(p->err));
|
|
797
|
+
TEST_ASSERT(p->err ==
|
|
798
|
+
RD_KAFKA_RESP_ERR_INVALID_SHARE_SESSION_EPOCH,
|
|
799
|
+
"Phase 2: expected INVALID_SHARE_SESSION_EPOCH, "
|
|
800
|
+
"got %s",
|
|
801
|
+
rd_kafka_err2name(p->err));
|
|
802
|
+
}
|
|
803
|
+
rd_kafka_topic_partition_list_destroy(partitions);
|
|
804
|
+
|
|
805
|
+
share_ack_cnt = test_mock_get_matching_request_cnt(
|
|
806
|
+
ctx.mcluster, is_share_ack_request, NULL);
|
|
807
|
+
TEST_ASSERT(share_ack_cnt == 0,
|
|
808
|
+
"Phase 2: expected 0 ShareAck requests "
|
|
809
|
+
"(local epoch-0 fail should have prevented send), "
|
|
810
|
+
"got %" PRIusz,
|
|
811
|
+
share_ack_cnt);
|
|
812
|
+
|
|
813
|
+
/* Verify Phase 2 callback was invoked with INVALID_SHARE_SESSION_EPOCH
|
|
814
|
+
* (local fail because session epoch is 0) */
|
|
815
|
+
TEST_ASSERT(cb_state.callback_cnt == 1,
|
|
816
|
+
"Phase 2: expected 1 callback, got %d",
|
|
817
|
+
cb_state.callback_cnt);
|
|
818
|
+
TEST_ASSERT(test_ack_cb_state_first_err(&cb_state) ==
|
|
819
|
+
RD_KAFKA_RESP_ERR_INVALID_SHARE_SESSION_EPOCH,
|
|
820
|
+
"Phase 2: expected callback err "
|
|
821
|
+
"INVALID_SHARE_SESSION_EPOCH, got %s",
|
|
822
|
+
rd_kafka_err2name(test_ack_cb_state_first_err(&cb_state)));
|
|
823
|
+
TEST_ASSERT(cb_state.total_offsets == 5,
|
|
824
|
+
"Phase 2: expected 5 offsets in callback, got %zu",
|
|
825
|
+
cb_state.total_offsets);
|
|
826
|
+
|
|
827
|
+
rd_kafka_mock_stop_request_tracking(ctx.mcluster);
|
|
828
|
+
|
|
829
|
+
for (i = 0; i < batch_cnt; i++)
|
|
830
|
+
rd_kafka_messages_destroy(batches[i]);
|
|
831
|
+
|
|
832
|
+
test_share_consumer_close(rkshare);
|
|
833
|
+
test_share_destroy(rkshare);
|
|
834
|
+
test_ack_cb_state_destroy(&cb_state);
|
|
835
|
+
test_ctx_destroy(&ctx);
|
|
836
|
+
|
|
837
|
+
SUB_TEST_PASS();
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/* ===================================================================
|
|
841
|
+
* Test — consume_batch with session epoch 0 strips piggybacked
|
|
842
|
+
* acks from the ShareFetch wire request.
|
|
843
|
+
*
|
|
844
|
+
* Same shape as test_commit_sync_at_epoch_zero_..._error but Phase
|
|
845
|
+
* 2 calls consume_batch (which triggers a FANOUT -> ShareFetch
|
|
846
|
+
* with should_fetch=true) instead of commit_sync. With session
|
|
847
|
+
* epoch == 0 and any cached piggyback acks attached, the strip
|
|
848
|
+
* path in broker_share_rpc fires: acks are pre-set with
|
|
849
|
+
* INVALID_SHARE_SESSION_EPOCH and detached from rko before
|
|
850
|
+
* ShareFetch is built, so the wire request goes out with no ack
|
|
851
|
+
* data section.
|
|
852
|
+
*
|
|
853
|
+
* Asserts (per-partition):
|
|
854
|
+
* - Wire-level: ShareFetch is sent (>= 1), no extra ShareAck
|
|
855
|
+
* requests fire.
|
|
856
|
+
* - Acknowledgement callback fires with
|
|
857
|
+
* INVALID_SHARE_SESSION_EPOCH for each stripped batch (the
|
|
858
|
+
* session-establish ShareFetch's response would normally cause
|
|
859
|
+
* the parser to write the broker's per-partition
|
|
860
|
+
* AcknowledgementErrorCode, but the parser conditional preserves
|
|
861
|
+
* our pre-set).
|
|
862
|
+
*
|
|
863
|
+
* To verify the strip path manually, run with:
|
|
864
|
+
* TEST_DEBUG=fetch,broker,cgrp TESTS=0182 \
|
|
865
|
+
* SUBTESTS=test_consume_batch_at_epoch_zero make
|
|
866
|
+
* and look for the "Stripping N piggybacked ack batches" SHAREFETCH
|
|
867
|
+
* log line in stderr.
|
|
868
|
+
* =================================================================== */
|
|
869
|
+
static rd_bool_t is_share_fetch_request(rd_kafka_mock_request_t *request,
|
|
870
|
+
void *opaque) {
|
|
871
|
+
return rd_kafka_mock_request_api_key(request) == RD_KAFKAP_ShareFetch;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
static void test_consume_batch_at_epoch_zero_strips_piggyback_acks(void) {
|
|
875
|
+
test_ctx_t ctx;
|
|
876
|
+
rd_kafka_share_t *rkshare;
|
|
877
|
+
rd_kafka_topic_partition_list_t *partitions = NULL;
|
|
878
|
+
rd_kafka_error_t *error;
|
|
879
|
+
const char *topic = "0182-epoch-zero-piggyback";
|
|
880
|
+
const char *group = "sg-0182-epoch-zero-piggyback";
|
|
881
|
+
const int msgcnt = 10;
|
|
882
|
+
rd_kafka_message_t *rkmessages[CONSUME_ARRAY];
|
|
883
|
+
rd_kafka_messages_t *phase0_batch = NULL;
|
|
884
|
+
rd_kafka_messages_t *phase2_batch = NULL;
|
|
885
|
+
int attempts = 0;
|
|
886
|
+
size_t rcvd = 0;
|
|
887
|
+
size_t share_ack_cnt;
|
|
888
|
+
size_t share_fetch_cnt;
|
|
889
|
+
int i;
|
|
890
|
+
test_ack_cb_state_t cb_state = {0};
|
|
891
|
+
|
|
892
|
+
SUB_TEST_QUICK();
|
|
893
|
+
|
|
894
|
+
ctx = test_ctx_new();
|
|
895
|
+
|
|
896
|
+
TEST_ASSERT(rd_kafka_mock_topic_create(ctx.mcluster, topic, 1, 1) ==
|
|
897
|
+
RD_KAFKA_RESP_ERR_NO_ERROR,
|
|
898
|
+
"create topic");
|
|
899
|
+
|
|
900
|
+
mock_produce(ctx.producer, topic, msgcnt);
|
|
901
|
+
|
|
902
|
+
rkshare = create_mock_share_consumer(ctx.bootstraps, group, "explicit",
|
|
903
|
+
&cb_state, test_share_ack_cb);
|
|
904
|
+
test_share_consumer_subscribe_multi(rkshare, 1, topic);
|
|
905
|
+
|
|
906
|
+
/* Phase 0: consume all msgcnt records in a single consume_batch
|
|
907
|
+
* call (with retry-on-empty for transient cases). Explicit-mode
|
|
908
|
+
* contract requires every record from a previous consume_batch
|
|
909
|
+
* to be acknowledged before the next call, so we must not loop
|
|
910
|
+
* and accumulate without acking each batch first. msgcnt is far
|
|
911
|
+
* below max.poll.records (default 500), so a non-empty call
|
|
912
|
+
* returns the full set. */
|
|
913
|
+
while (rcvd == 0 && attempts++ < 30) {
|
|
914
|
+
rd_kafka_messages_destroy(phase0_batch);
|
|
915
|
+
phase0_batch = NULL;
|
|
916
|
+
error = rd_kafka_share_poll(rkshare, 3000, &phase0_batch);
|
|
917
|
+
if (error)
|
|
918
|
+
rd_kafka_error_destroy(error);
|
|
919
|
+
rcvd = rd_kafka_messages_count(phase0_batch);
|
|
920
|
+
}
|
|
921
|
+
TEST_ASSERT(rcvd == (size_t)msgcnt,
|
|
922
|
+
"Phase 0: expected %d records in single batch, "
|
|
923
|
+
"got %" PRIusz,
|
|
924
|
+
msgcnt, rcvd);
|
|
925
|
+
for (i = 0; i < msgcnt; i++)
|
|
926
|
+
rkmessages[i] = rd_kafka_messages_get(phase0_batch, i);
|
|
927
|
+
|
|
928
|
+
/* Phase 1: ACCEPT first 5 records, inject SHARE_SESSION_NOT_FOUND
|
|
929
|
+
* on next ShareAcknowledge, call commit_sync to trigger session
|
|
930
|
+
* reset on the broker (epoch -> 0). */
|
|
931
|
+
for (i = 0; i < 5; i++)
|
|
932
|
+
rd_kafka_share_acknowledge(rkshare, rkmessages[i]);
|
|
933
|
+
|
|
934
|
+
rd_kafka_mock_start_request_tracking(ctx.mcluster);
|
|
935
|
+
rd_kafka_mock_clear_requests(ctx.mcluster);
|
|
936
|
+
|
|
937
|
+
TEST_ASSERT(rd_kafka_mock_broker_push_request_error_rtts(
|
|
938
|
+
ctx.mcluster, 1, RD_KAFKAP_ShareAcknowledge, 1,
|
|
939
|
+
RD_KAFKA_RESP_ERR_SHARE_SESSION_NOT_FOUND,
|
|
940
|
+
0) == RD_KAFKA_RESP_ERR_NO_ERROR,
|
|
941
|
+
"push SHARE_SESSION_NOT_FOUND");
|
|
942
|
+
|
|
943
|
+
partitions = NULL;
|
|
944
|
+
error = rd_kafka_share_commit_sync(rkshare, 1000, &partitions);
|
|
945
|
+
if (error)
|
|
946
|
+
rd_kafka_error_destroy(error);
|
|
947
|
+
|
|
948
|
+
TEST_ASSERT(partitions != NULL,
|
|
949
|
+
"Phase 1: expected non-NULL partition results");
|
|
950
|
+
for (i = 0; i < partitions->cnt; i++) {
|
|
951
|
+
rd_kafka_topic_partition_t *p = &partitions->elems[i];
|
|
952
|
+
TEST_SAY("Phase 1 %s [%" PRId32 "]: %s\n", p->topic,
|
|
953
|
+
p->partition, rd_kafka_err2name(p->err));
|
|
954
|
+
TEST_ASSERT(p->err == RD_KAFKA_RESP_ERR_SHARE_SESSION_NOT_FOUND,
|
|
955
|
+
"Phase 1: expected SHARE_SESSION_NOT_FOUND, "
|
|
956
|
+
"got %s",
|
|
957
|
+
rd_kafka_err2name(p->err));
|
|
958
|
+
}
|
|
959
|
+
rd_kafka_topic_partition_list_destroy(partitions);
|
|
960
|
+
|
|
961
|
+
share_ack_cnt = test_mock_get_matching_request_cnt(
|
|
962
|
+
ctx.mcluster, is_share_ack_request, NULL);
|
|
963
|
+
TEST_ASSERT(share_ack_cnt == 1,
|
|
964
|
+
"Phase 1: expected 1 ShareAck request, got %" PRIusz,
|
|
965
|
+
share_ack_cnt);
|
|
966
|
+
|
|
967
|
+
/* Verify Phase 1 callback was invoked with SHARE_SESSION_NOT_FOUND */
|
|
968
|
+
TEST_ASSERT(cb_state.callback_cnt == 1,
|
|
969
|
+
"Phase 1: expected 1 callback, got %d",
|
|
970
|
+
cb_state.callback_cnt);
|
|
971
|
+
TEST_ASSERT(
|
|
972
|
+
test_ack_cb_state_first_err(&cb_state) ==
|
|
973
|
+
RD_KAFKA_RESP_ERR_SHARE_SESSION_NOT_FOUND,
|
|
974
|
+
"Phase 1: expected callback err SHARE_SESSION_NOT_FOUND, got %s",
|
|
975
|
+
rd_kafka_err2name(test_ack_cb_state_first_err(&cb_state)));
|
|
976
|
+
|
|
977
|
+
/* Reset callback state for Phase 2 */
|
|
978
|
+
test_ack_cb_state_destroy(&cb_state);
|
|
979
|
+
|
|
980
|
+
/* Phase 2: ACCEPT remaining 5 records and call consume_batch
|
|
981
|
+
* (NOT commit_sync). This triggers a FANOUT -> ShareFetch with
|
|
982
|
+
* should_fetch=true. Broker epoch is 0 (reset in phase 1). If
|
|
983
|
+
* any piggyback acks are attached when broker_share_rpc runs,
|
|
984
|
+
* the strip path fires and pre-sets each batch's err to
|
|
985
|
+
* INVALID_SHARE_SESSION_EPOCH. ShareFetch goes out with no ack
|
|
986
|
+
* data section.
|
|
987
|
+
*
|
|
988
|
+
* Wire-level assertions:
|
|
989
|
+
* - At least 1 new ShareFetch request was sent (session
|
|
990
|
+
* re-establishment).
|
|
991
|
+
* - 0 new ShareAck requests (strip prevented send if any
|
|
992
|
+
* piggyback acks were present; ack-only path also doesn't
|
|
993
|
+
* fire because we did not call commit_sync). */
|
|
994
|
+
rd_kafka_mock_clear_requests(ctx.mcluster);
|
|
995
|
+
|
|
996
|
+
for (i = 5; i < msgcnt; i++)
|
|
997
|
+
rd_kafka_share_acknowledge(rkshare, rkmessages[i]);
|
|
998
|
+
|
|
999
|
+
/* Single consume_batch triggers the strip-mode FANOUT.
|
|
1000
|
+
* Records here may be re-deliveries after lock expiry — we
|
|
1001
|
+
* don't assert on contents, only on wire-level counts.
|
|
1002
|
+
* test_wait_for_cb_with_poll below tolerates the explicit-mode
|
|
1003
|
+
* __STATE the next consume_batch may return for these
|
|
1004
|
+
* un-acknowledged records (rcvd stays 0). */
|
|
1005
|
+
error = rd_kafka_share_poll(rkshare, 1000, &phase2_batch);
|
|
1006
|
+
if (error)
|
|
1007
|
+
rd_kafka_error_destroy(error);
|
|
1008
|
+
rd_kafka_messages_destroy(phase2_batch);
|
|
1009
|
+
phase2_batch = NULL;
|
|
1010
|
+
|
|
1011
|
+
share_fetch_cnt = test_mock_get_matching_request_cnt(
|
|
1012
|
+
ctx.mcluster, is_share_fetch_request, NULL);
|
|
1013
|
+
share_ack_cnt = test_mock_get_matching_request_cnt(
|
|
1014
|
+
ctx.mcluster, is_share_ack_request, NULL);
|
|
1015
|
+
|
|
1016
|
+
TEST_SAY("Phase 2 wire counts: ShareFetch=%" PRIusz
|
|
1017
|
+
", ShareAck=%" PRIusz "\n",
|
|
1018
|
+
share_fetch_cnt, share_ack_cnt);
|
|
1019
|
+
|
|
1020
|
+
TEST_ASSERT(share_fetch_cnt >= 1,
|
|
1021
|
+
"Phase 2: expected >= 1 ShareFetch (session "
|
|
1022
|
+
"re-establish), got %" PRIusz,
|
|
1023
|
+
share_fetch_cnt);
|
|
1024
|
+
TEST_ASSERT(share_ack_cnt == 0,
|
|
1025
|
+
"Phase 2: expected 0 ShareAck "
|
|
1026
|
+
"(strip should have prevented any piggyback ack "
|
|
1027
|
+
"send and no ack-only path fired), got %" PRIusz,
|
|
1028
|
+
share_ack_cnt);
|
|
1029
|
+
|
|
1030
|
+
/* Wait for the per-partition acknowledgement callback to fire
|
|
1031
|
+
* (dispatched on rk_rep when the SHARE_FETCH op reply reaches
|
|
1032
|
+
* the main thread). */
|
|
1033
|
+
TEST_ASSERT(test_wait_for_cb_with_poll(&cb_state, rkshare, 1, 5000),
|
|
1034
|
+
"Phase 2: timed out waiting for ack callback");
|
|
1035
|
+
|
|
1036
|
+
/* The strip path pre-set INVALID_SHARE_SESSION_EPOCH on each
|
|
1037
|
+
* batch in ack_details. The session-establish ShareFetch
|
|
1038
|
+
* succeeds and the broker echoes the added partition in its
|
|
1039
|
+
* response, so the parser would normally write the broker's
|
|
1040
|
+
* AcknowledgementErrorCode (NO_ERROR) onto the batch — the
|
|
1041
|
+
* parser conditional (override only _IN_PROGRESS) preserves
|
|
1042
|
+
* the pre-set instead. The callback fires with the pre-set
|
|
1043
|
+
* err. */
|
|
1044
|
+
TEST_ASSERT(cb_state.callback_cnt == 1,
|
|
1045
|
+
"Phase 2: expected 1 callback, got %d",
|
|
1046
|
+
cb_state.callback_cnt);
|
|
1047
|
+
TEST_ASSERT(test_ack_cb_state_first_err(&cb_state) ==
|
|
1048
|
+
RD_KAFKA_RESP_ERR_INVALID_SHARE_SESSION_EPOCH,
|
|
1049
|
+
"Phase 2: expected callback err "
|
|
1050
|
+
"INVALID_SHARE_SESSION_EPOCH, got %s",
|
|
1051
|
+
rd_kafka_err2name(test_ack_cb_state_first_err(&cb_state)));
|
|
1052
|
+
TEST_ASSERT(cb_state.total_offsets == 5,
|
|
1053
|
+
"Phase 2: expected 5 acked offsets in callback, "
|
|
1054
|
+
"got %" PRIusz,
|
|
1055
|
+
cb_state.total_offsets);
|
|
1056
|
+
|
|
1057
|
+
rd_kafka_mock_stop_request_tracking(ctx.mcluster);
|
|
1058
|
+
|
|
1059
|
+
rd_kafka_messages_destroy(phase0_batch);
|
|
1060
|
+
|
|
1061
|
+
test_share_consumer_close(rkshare);
|
|
1062
|
+
test_share_destroy(rkshare);
|
|
1063
|
+
test_ack_cb_state_destroy(&cb_state);
|
|
1064
|
+
test_ctx_destroy(&ctx);
|
|
1065
|
+
|
|
1066
|
+
SUB_TEST_PASS();
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/* ===================================================================
|
|
1070
|
+
* Test — strip + ShareFetch response top-level err: callback err
|
|
1071
|
+
* remains INVALID_SHARE_SESSION_EPOCH (not the response err).
|
|
1072
|
+
*
|
|
1073
|
+
* Same flow as test_consume_batch_at_epoch_zero_strips_piggyback_acks
|
|
1074
|
+
* but in Phase 2 we ALSO inject a top-level err on the
|
|
1075
|
+
* session-establish ShareFetch response. The buf-callback's helper
|
|
1076
|
+
* (rd_kafka_share_fetch_op_reply_with_err) will be called with that
|
|
1077
|
+
* err — its conditional override (only _IN_PROGRESS) must NOT
|
|
1078
|
+
* clobber the pre-set INVALID_SHARE_SESSION_EPOCH on the stripped
|
|
1079
|
+
* batches.
|
|
1080
|
+
*
|
|
1081
|
+
* Asserts (per-partition via callback):
|
|
1082
|
+
* - Wire-level: ShareFetch is sent, no ShareAck.
|
|
1083
|
+
* - Acknowledgement callback fires with
|
|
1084
|
+
* INVALID_SHARE_SESSION_EPOCH (NOT the injected response err)
|
|
1085
|
+
* for each stripped batch.
|
|
1086
|
+
* =================================================================== */
|
|
1087
|
+
static void test_strip_pre_set_survives_sharefetch_err(void) {
|
|
1088
|
+
test_ctx_t ctx;
|
|
1089
|
+
rd_kafka_share_t *rkshare;
|
|
1090
|
+
rd_kafka_topic_partition_list_t *partitions = NULL;
|
|
1091
|
+
rd_kafka_error_t *error;
|
|
1092
|
+
const char *topic = "0182-epoch-zero-piggyback-fetch-err";
|
|
1093
|
+
const char *group = "sg-0182-epoch-zero-piggyback-fetch-err";
|
|
1094
|
+
const int msgcnt = 10;
|
|
1095
|
+
rd_kafka_message_t *rkmessages[CONSUME_ARRAY];
|
|
1096
|
+
rd_kafka_messages_t *phase0_batch = NULL;
|
|
1097
|
+
rd_kafka_messages_t *phase2_batch = NULL;
|
|
1098
|
+
int attempts = 0;
|
|
1099
|
+
size_t rcvd = 0;
|
|
1100
|
+
size_t share_ack_cnt;
|
|
1101
|
+
size_t share_fetch_cnt;
|
|
1102
|
+
int i;
|
|
1103
|
+
test_ack_cb_state_t cb_state = {0};
|
|
1104
|
+
|
|
1105
|
+
SUB_TEST_QUICK();
|
|
1106
|
+
|
|
1107
|
+
ctx = test_ctx_new();
|
|
1108
|
+
|
|
1109
|
+
TEST_ASSERT(rd_kafka_mock_topic_create(ctx.mcluster, topic, 1, 1) ==
|
|
1110
|
+
RD_KAFKA_RESP_ERR_NO_ERROR,
|
|
1111
|
+
"create topic");
|
|
1112
|
+
|
|
1113
|
+
mock_produce(ctx.producer, topic, msgcnt);
|
|
1114
|
+
|
|
1115
|
+
rkshare = create_mock_share_consumer(ctx.bootstraps, group, "explicit",
|
|
1116
|
+
&cb_state, test_share_ack_cb);
|
|
1117
|
+
test_share_consumer_subscribe_multi(rkshare, 1, topic);
|
|
1118
|
+
|
|
1119
|
+
/* Phase 0: consume all msgcnt records in a single consume_batch
|
|
1120
|
+
* call (with retry-on-empty for transient cases). Explicit-mode
|
|
1121
|
+
* contract requires every record from a previous consume_batch
|
|
1122
|
+
* to be acknowledged before the next call, so we must not loop
|
|
1123
|
+
* and accumulate without acking each batch first. msgcnt is far
|
|
1124
|
+
* below max.poll.records (default 500), so a non-empty call
|
|
1125
|
+
* returns the full set. */
|
|
1126
|
+
while (rcvd == 0 && attempts++ < 30) {
|
|
1127
|
+
rd_kafka_messages_destroy(phase0_batch);
|
|
1128
|
+
phase0_batch = NULL;
|
|
1129
|
+
error = rd_kafka_share_poll(rkshare, 3000, &phase0_batch);
|
|
1130
|
+
if (error)
|
|
1131
|
+
rd_kafka_error_destroy(error);
|
|
1132
|
+
rcvd = rd_kafka_messages_count(phase0_batch);
|
|
1133
|
+
}
|
|
1134
|
+
TEST_ASSERT(rcvd == (size_t)msgcnt,
|
|
1135
|
+
"Phase 0: expected %d records in single batch, "
|
|
1136
|
+
"got %" PRIusz,
|
|
1137
|
+
msgcnt, rcvd);
|
|
1138
|
+
for (i = 0; i < msgcnt; i++)
|
|
1139
|
+
rkmessages[i] = rd_kafka_messages_get(phase0_batch, i);
|
|
1140
|
+
|
|
1141
|
+
/* Phase 1: ACCEPT first 5 records, inject SHARE_SESSION_NOT_FOUND
|
|
1142
|
+
* on next ShareAcknowledge, call commit_sync to trigger session
|
|
1143
|
+
* reset on the broker (epoch -> 0). */
|
|
1144
|
+
for (i = 0; i < 5; i++)
|
|
1145
|
+
rd_kafka_share_acknowledge(rkshare, rkmessages[i]);
|
|
1146
|
+
|
|
1147
|
+
rd_kafka_mock_start_request_tracking(ctx.mcluster);
|
|
1148
|
+
rd_kafka_mock_clear_requests(ctx.mcluster);
|
|
1149
|
+
|
|
1150
|
+
TEST_ASSERT(rd_kafka_mock_broker_push_request_error_rtts(
|
|
1151
|
+
ctx.mcluster, 1, RD_KAFKAP_ShareAcknowledge, 1,
|
|
1152
|
+
RD_KAFKA_RESP_ERR_SHARE_SESSION_NOT_FOUND,
|
|
1153
|
+
0) == RD_KAFKA_RESP_ERR_NO_ERROR,
|
|
1154
|
+
"push SHARE_SESSION_NOT_FOUND");
|
|
1155
|
+
|
|
1156
|
+
partitions = NULL;
|
|
1157
|
+
error = rd_kafka_share_commit_sync(rkshare, 1000, &partitions);
|
|
1158
|
+
if (error)
|
|
1159
|
+
rd_kafka_error_destroy(error);
|
|
1160
|
+
|
|
1161
|
+
TEST_ASSERT(partitions != NULL,
|
|
1162
|
+
"Phase 1: expected non-NULL partition results");
|
|
1163
|
+
for (i = 0; i < partitions->cnt; i++) {
|
|
1164
|
+
rd_kafka_topic_partition_t *p = &partitions->elems[i];
|
|
1165
|
+
TEST_ASSERT(p->err == RD_KAFKA_RESP_ERR_SHARE_SESSION_NOT_FOUND,
|
|
1166
|
+
"Phase 1: expected SHARE_SESSION_NOT_FOUND, "
|
|
1167
|
+
"got %s",
|
|
1168
|
+
rd_kafka_err2name(p->err));
|
|
1169
|
+
}
|
|
1170
|
+
rd_kafka_topic_partition_list_destroy(partitions);
|
|
1171
|
+
|
|
1172
|
+
/* Reset callback state for Phase 2 */
|
|
1173
|
+
test_ack_cb_state_destroy(&cb_state);
|
|
1174
|
+
|
|
1175
|
+
/* Phase 2: ACCEPT remaining 5 records. Inject
|
|
1176
|
+
* SHARE_SESSION_LIMIT_REACHED on the next ShareFetch — this is
|
|
1177
|
+
* the strip-mode ShareFetch (epoch 0, with stripped piggyback
|
|
1178
|
+
* acks) that goes out from consume_batch. The buf-callback's
|
|
1179
|
+
* helper is then called with SHARE_SESSION_LIMIT_REACHED on a
|
|
1180
|
+
* batch already pre-set to INVALID_SHARE_SESSION_EPOCH; the
|
|
1181
|
+
* helper conditional (only override _IN_PROGRESS) must
|
|
1182
|
+
* preserve the pre-set. */
|
|
1183
|
+
rd_kafka_mock_clear_requests(ctx.mcluster);
|
|
1184
|
+
|
|
1185
|
+
for (i = 5; i < msgcnt; i++)
|
|
1186
|
+
rd_kafka_share_acknowledge(rkshare, rkmessages[i]);
|
|
1187
|
+
|
|
1188
|
+
TEST_ASSERT(rd_kafka_mock_broker_push_request_error_rtts(
|
|
1189
|
+
ctx.mcluster, 1, RD_KAFKAP_ShareFetch, 1,
|
|
1190
|
+
RD_KAFKA_RESP_ERR_SHARE_SESSION_LIMIT_REACHED,
|
|
1191
|
+
0) == RD_KAFKA_RESP_ERR_NO_ERROR,
|
|
1192
|
+
"push SHARE_SESSION_LIMIT_REACHED on next ShareFetch");
|
|
1193
|
+
|
|
1194
|
+
/* Single consume_batch triggers the strip-mode FANOUT.
|
|
1195
|
+
* Records here may be re-deliveries after lock expiry — we
|
|
1196
|
+
* don't assert on contents, only on wire-level counts.
|
|
1197
|
+
* test_wait_for_cb_with_poll below tolerates the explicit-mode
|
|
1198
|
+
* __STATE the next consume_batch may return for these
|
|
1199
|
+
* un-acknowledged records (rcvd stays 0). */
|
|
1200
|
+
error = rd_kafka_share_poll(rkshare, 1000, &phase2_batch);
|
|
1201
|
+
if (error)
|
|
1202
|
+
rd_kafka_error_destroy(error);
|
|
1203
|
+
rd_kafka_messages_destroy(phase2_batch);
|
|
1204
|
+
phase2_batch = NULL;
|
|
1205
|
+
|
|
1206
|
+
share_fetch_cnt = test_mock_get_matching_request_cnt(
|
|
1207
|
+
ctx.mcluster, is_share_fetch_request, NULL);
|
|
1208
|
+
share_ack_cnt = test_mock_get_matching_request_cnt(
|
|
1209
|
+
ctx.mcluster, is_share_ack_request, NULL);
|
|
1210
|
+
|
|
1211
|
+
TEST_SAY("Phase 2 wire counts: ShareFetch=%" PRIusz
|
|
1212
|
+
", ShareAck=%" PRIusz "\n",
|
|
1213
|
+
share_fetch_cnt, share_ack_cnt);
|
|
1214
|
+
|
|
1215
|
+
TEST_ASSERT(share_fetch_cnt >= 1,
|
|
1216
|
+
"Phase 2: expected >= 1 ShareFetch, got %" PRIusz,
|
|
1217
|
+
share_fetch_cnt);
|
|
1218
|
+
TEST_ASSERT(share_ack_cnt == 0,
|
|
1219
|
+
"Phase 2: expected 0 ShareAck (strip), got %" PRIusz,
|
|
1220
|
+
share_ack_cnt);
|
|
1221
|
+
|
|
1222
|
+
/* Wait for the per-partition acknowledgement callback. */
|
|
1223
|
+
TEST_ASSERT(test_wait_for_cb_with_poll(&cb_state, rkshare, 1, 5000),
|
|
1224
|
+
"Phase 2: timed out waiting for ack callback");
|
|
1225
|
+
|
|
1226
|
+
/* The injected ShareFetch top-level err
|
|
1227
|
+
* (SHARE_SESSION_LIMIT_REACHED) reached the
|
|
1228
|
+
* rd_kafka_share_fetch_op_reply_with_err helper. The helper's
|
|
1229
|
+
* conditional override (only _IN_PROGRESS) must NOT clobber
|
|
1230
|
+
* the pre-set INVALID_SHARE_SESSION_EPOCH. */
|
|
1231
|
+
TEST_ASSERT(cb_state.callback_cnt == 1,
|
|
1232
|
+
"Phase 2: expected 1 callback, got %d",
|
|
1233
|
+
cb_state.callback_cnt);
|
|
1234
|
+
TEST_ASSERT(test_ack_cb_state_first_err(&cb_state) ==
|
|
1235
|
+
RD_KAFKA_RESP_ERR_INVALID_SHARE_SESSION_EPOCH,
|
|
1236
|
+
"Phase 2: expected callback err "
|
|
1237
|
+
"INVALID_SHARE_SESSION_EPOCH (pre-set preserved), "
|
|
1238
|
+
"got %s",
|
|
1239
|
+
rd_kafka_err2name(test_ack_cb_state_first_err(&cb_state)));
|
|
1240
|
+
TEST_ASSERT(cb_state.total_offsets == 5,
|
|
1241
|
+
"Phase 2: expected 5 acked offsets in callback, "
|
|
1242
|
+
"got %" PRIusz,
|
|
1243
|
+
cb_state.total_offsets);
|
|
1244
|
+
|
|
1245
|
+
rd_kafka_mock_stop_request_tracking(ctx.mcluster);
|
|
1246
|
+
|
|
1247
|
+
rd_kafka_messages_destroy(phase0_batch);
|
|
1248
|
+
|
|
1249
|
+
test_share_consumer_close(rkshare);
|
|
1250
|
+
test_share_destroy(rkshare);
|
|
1251
|
+
test_ack_cb_state_destroy(&cb_state);
|
|
1252
|
+
test_ctx_destroy(&ctx);
|
|
1253
|
+
|
|
1254
|
+
SUB_TEST_PASS();
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
/* ===================================================================
|
|
1258
|
+
* Wire-level socket timeout matrix.
|
|
1259
|
+
*
|
|
1260
|
+
* Exercises commit_sync under a socket-timeout-induced connection
|
|
1261
|
+
* teardown. The two timer layers in the share-acknowledge path are
|
|
1262
|
+
* intentionally decoupled:
|
|
1263
|
+
*
|
|
1264
|
+
* - The app-visible commit_sync(timeout_ms) deadline drives the
|
|
1265
|
+
* timeout cb that stamps REQUEST_TIMED_OUT on the per-partition
|
|
1266
|
+
* results.
|
|
1267
|
+
* - The wire RPC carries the broker connection's socket.timeout.ms
|
|
1268
|
+
* and tears the connection down (default socket.max.fails = 1)
|
|
1269
|
+
* when it fires.
|
|
1270
|
+
*
|
|
1271
|
+
* Three (api_timeout_ms, socket_timeout_ms, rtt_ms) triples cover
|
|
1272
|
+
* the cases where api > socket, api == socket, and api < socket. In
|
|
1273
|
+
* all three, rtt_ms > socket_timeout_ms so the wire eventually times
|
|
1274
|
+
* out and the connection is torn down (broker drops the session as
|
|
1275
|
+
* a result of the TCP close).
|
|
1276
|
+
*
|
|
1277
|
+
* Two flow variants build on the same setup:
|
|
1278
|
+
*
|
|
1279
|
+
* full_ack_then_more — Phase 1 commits all consumed records.
|
|
1280
|
+
* Phase 2 produces and consumes new records on a fresh session
|
|
1281
|
+
* and expects NO_ERROR on the second commit_sync.
|
|
1282
|
+
*
|
|
1283
|
+
* partial_ack_then_remaining — Phase 1 commits only half. The
|
|
1284
|
+
* records left in the local inflight map were ACQUIRED on the
|
|
1285
|
+
* now-dropped session, so Phase 2's commit_sync surfaces an
|
|
1286
|
+
* INVALID_RECORD_STATE / SHARE_SESSION_NOT_FOUND /
|
|
1287
|
+
* INVALID_SHARE_SESSION_EPOCH-class error (when session-err
|
|
1288
|
+
* translation lands at the app-facing boundary this narrows
|
|
1289
|
+
* to INVALID_RECORD_STATE).
|
|
1290
|
+
*
|
|
1291
|
+
* TODO KIP-932: add multi-broker variants once the single-broker
|
|
1292
|
+
* matrix is stable. Multi-broker exercises the case where only one
|
|
1293
|
+
* broker is slow — partitions on other brokers should commit
|
|
1294
|
+
* immediately without waiting for the slow broker's socket timer.
|
|
1295
|
+
* =================================================================== */
|
|
1296
|
+
|
|
1297
|
+
#define SOCKET_TIMEOUT_MATRIX_RECORD_LOCK_MS 600000
|
|
1298
|
+
#define SOCKET_TIMEOUT_MATRIX_PARTITIONS 3
|
|
1299
|
+
#define SOCKET_TIMEOUT_MATRIX_MSGS_PER_PARTITION 10
|
|
1300
|
+
|
|
1301
|
+
/**
|
|
1302
|
+
* @brief Create a share consumer with a custom socket.timeout.ms.
|
|
1303
|
+
*/
|
|
1304
|
+
static rd_kafka_share_t *create_share_consumer_socket_timeout(
|
|
1305
|
+
const char *bootstraps,
|
|
1306
|
+
const char *group_id,
|
|
1307
|
+
const char *ack_mode,
|
|
1308
|
+
int socket_timeout_ms,
|
|
1309
|
+
test_ack_cb_state_t *cb_state,
|
|
1310
|
+
void (*cb)(rd_kafka_share_t *,
|
|
1311
|
+
rd_kafka_share_partition_offsets_list_t *,
|
|
1312
|
+
rd_kafka_resp_err_t,
|
|
1313
|
+
void *)) {
|
|
1314
|
+
rd_kafka_conf_t *conf;
|
|
1315
|
+
rd_kafka_share_t *rkshare;
|
|
1316
|
+
char buf[32];
|
|
1317
|
+
|
|
1318
|
+
test_conf_init(&conf, NULL, 0);
|
|
1319
|
+
test_conf_set(conf, "bootstrap.servers", bootstraps);
|
|
1320
|
+
test_conf_set(conf, "group.id", group_id);
|
|
1321
|
+
test_conf_set(conf, "share.acknowledgement.mode", ack_mode);
|
|
1322
|
+
|
|
1323
|
+
rd_snprintf(buf, sizeof(buf), "%d", socket_timeout_ms);
|
|
1324
|
+
test_conf_set(conf, "socket.timeout.ms", buf);
|
|
1325
|
+
|
|
1326
|
+
rkshare = rd_kafka_share_consumer_new(conf, NULL, 0);
|
|
1327
|
+
TEST_ASSERT(rkshare != NULL, "Failed to create share consumer");
|
|
1328
|
+
|
|
1329
|
+
if (cb && cb_state) {
|
|
1330
|
+
rd_kafka_error_t *error =
|
|
1331
|
+
rd_kafka_share_set_acknowledgement_commit_cb(rkshare, cb,
|
|
1332
|
+
cb_state);
|
|
1333
|
+
TEST_ASSERT(error == NULL,
|
|
1334
|
+
"Failed to set acknowledgement commit callback: "
|
|
1335
|
+
"%s",
|
|
1336
|
+
rd_kafka_error_string(error));
|
|
1337
|
+
}
|
|
1338
|
+
return rkshare;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
/**
|
|
1342
|
+
* @brief Consume up to \p expected records (across one or more
|
|
1343
|
+
* share_poll calls), acknowledging the first \p ack_first.
|
|
1344
|
+
*
|
|
1345
|
+
* Caller owns the returned batch via \p *out_batch and must
|
|
1346
|
+
* rd_kafka_messages_destroy() it. The flattened \p rkmessages array
|
|
1347
|
+
* is populated with pointers borrowed from the batch (lifetime tied
|
|
1348
|
+
* to the batch). Asserts that exactly \p expected records were
|
|
1349
|
+
* received within the polling budget. Used both for the pre-stage
|
|
1350
|
+
* Phase 1 consume (broker has all records ready, typically arrives
|
|
1351
|
+
* in a single batch) and for the post-teardown Phase 2 consume
|
|
1352
|
+
* (records may trickle in across multiple batches as the new
|
|
1353
|
+
* session warms up).
|
|
1354
|
+
*/
|
|
1355
|
+
static void consume_first_batch(rd_kafka_share_t *rkshare,
|
|
1356
|
+
rd_kafka_message_t **rkmessages,
|
|
1357
|
+
int expected,
|
|
1358
|
+
rd_kafka_messages_t **out_batch) {
|
|
1359
|
+
size_t rcvd = 0;
|
|
1360
|
+
int attempts = 0;
|
|
1361
|
+
rd_kafka_error_t *error;
|
|
1362
|
+
rd_kafka_messages_t *batch = NULL;
|
|
1363
|
+
int j;
|
|
1364
|
+
|
|
1365
|
+
/* Spin until the first non-empty batch arrives. The test
|
|
1366
|
+
* pre-stages the broker with all \p expected records so the
|
|
1367
|
+
* batch we receive will contain exactly that many. */
|
|
1368
|
+
while (rcvd == 0 && attempts++ < 30) {
|
|
1369
|
+
rd_kafka_messages_destroy(batch);
|
|
1370
|
+
batch = NULL;
|
|
1371
|
+
error = rd_kafka_share_poll(rkshare, 3000, &batch);
|
|
1372
|
+
if (error) {
|
|
1373
|
+
TEST_SAY("consume_first_batch: err=%s\n",
|
|
1374
|
+
rd_kafka_err2name(rd_kafka_error_code(error)));
|
|
1375
|
+
rd_kafka_error_destroy(error);
|
|
1376
|
+
}
|
|
1377
|
+
rcvd = rd_kafka_messages_count(batch);
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
TEST_ASSERT((int)rcvd == expected,
|
|
1381
|
+
"Expected %d records in first batch, got %zu", expected,
|
|
1382
|
+
rcvd);
|
|
1383
|
+
|
|
1384
|
+
for (j = 0; j < (int)rcvd; j++) {
|
|
1385
|
+
rkmessages[j] = rd_kafka_messages_get(batch, j);
|
|
1386
|
+
TEST_ASSERT(!rkmessages[j]->err,
|
|
1387
|
+
"Unexpected per-record err: %s",
|
|
1388
|
+
rd_kafka_err2str(rkmessages[j]->err));
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
*out_batch = batch;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
/**
|
|
1395
|
+
* @brief Drain pending ack callbacks via commit_async until at least
|
|
1396
|
+
* \p min_callbacks have been observed, or \p timeout_ms elapses.
|
|
1397
|
+
*
|
|
1398
|
+
* commit_async's first step drains rk_rep for callbacks. With no
|
|
1399
|
+
* pending acks to commit, the remainder is a no-op.
|
|
1400
|
+
*
|
|
1401
|
+
* @returns rd_true if min_callbacks observed within timeout.
|
|
1402
|
+
*/
|
|
1403
|
+
static rd_bool_t drain_callbacks_via_commit_async(rd_kafka_share_t *rkshare,
|
|
1404
|
+
test_ack_cb_state_t *cb_state,
|
|
1405
|
+
int min_callbacks,
|
|
1406
|
+
int timeout_ms) {
|
|
1407
|
+
int elapsed_ms = 0;
|
|
1408
|
+
|
|
1409
|
+
while (elapsed_ms < timeout_ms) {
|
|
1410
|
+
if (cb_state->callback_cnt >= min_callbacks)
|
|
1411
|
+
return rd_true;
|
|
1412
|
+
rd_kafka_error_t *err = rd_kafka_share_commit_async(rkshare);
|
|
1413
|
+
if (err)
|
|
1414
|
+
rd_kafka_error_destroy(err);
|
|
1415
|
+
rd_usleep(50 * 1000, NULL);
|
|
1416
|
+
elapsed_ms += 50;
|
|
1417
|
+
}
|
|
1418
|
+
return cb_state->callback_cnt >= min_callbacks;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
/**
|
|
1422
|
+
* @brief Test commit_sync under a wire-level socket timeout, then
|
|
1423
|
+
* consume + commit a fresh batch.
|
|
1424
|
+
*
|
|
1425
|
+
* The three timer layers (api timeout A, RTT R, socket.timeout.ms S)
|
|
1426
|
+
* determine which event wins:
|
|
1427
|
+
* - A smallest: commit_sync timeout cb stamps REQUEST_TIMED_OUT
|
|
1428
|
+
* on results and returns at t=A. Wire request continues in
|
|
1429
|
+
* flight until either R completes or S fires; late callback
|
|
1430
|
+
* reflects the actual wire outcome (NO_ERROR if R < S,
|
|
1431
|
+
* __TIMED_OUT if S < R).
|
|
1432
|
+
* - S smallest: wire torn down at t=S. Reply handler stamps
|
|
1433
|
+
* __TIMED_OUT on batches and results; commit_sync returns at
|
|
1434
|
+
* ~t=S with __TIMED_OUT and callbacks fire with __TIMED_OUT.
|
|
1435
|
+
* - R smallest: broker reply arrives normally; commit_sync
|
|
1436
|
+
* returns NO_ERROR. Not exercised by this matrix.
|
|
1437
|
+
*
|
|
1438
|
+
* Phase 1 measures the wait between commit_sync return and all ack
|
|
1439
|
+
* callbacks landing. That wait equals max(0, min(R, S) -
|
|
1440
|
+
* min(A, R, S)) and is asserted with tolerance.
|
|
1441
|
+
*
|
|
1442
|
+
* Phase 2 produces N new records, consumes + acks + commits on the
|
|
1443
|
+
* (possibly reconnected) consumer. Expects NO_ERROR for every
|
|
1444
|
+
* partition and a fresh set of NO_ERROR callbacks.
|
|
1445
|
+
*
|
|
1446
|
+
* TODO KIP-932: add multi-broker variant once single-broker matrix
|
|
1447
|
+
* is stable. Multi-broker exercises the case where only one broker
|
|
1448
|
+
* is slow.
|
|
1449
|
+
*
|
|
1450
|
+
* TODO KIP-932: add partial-ack variant — Phase 1 acks only half
|
|
1451
|
+
* the records, Phase 2 acks the remaining. With session drop on
|
|
1452
|
+
* connection tear-down, the remaining records reference a dropped
|
|
1453
|
+
* session and Phase 2's commit_sync surfaces INVALID_RECORD_STATE.
|
|
1454
|
+
*/
|
|
1455
|
+
static void do_test_socket_timeout_full_ack_then_more(int api_timeout_ms,
|
|
1456
|
+
int socket_timeout_ms,
|
|
1457
|
+
int rtt_ms) {
|
|
1458
|
+
test_ctx_t ctx;
|
|
1459
|
+
rd_kafka_share_t *rkshare;
|
|
1460
|
+
rd_kafka_topic_partition_list_t *partitions = NULL;
|
|
1461
|
+
rd_kafka_error_t *error;
|
|
1462
|
+
char topic[128];
|
|
1463
|
+
char group[128];
|
|
1464
|
+
const int partitions_total = SOCKET_TIMEOUT_MATRIX_PARTITIONS;
|
|
1465
|
+
const int msgcnt =
|
|
1466
|
+
partitions_total * SOCKET_TIMEOUT_MATRIX_MSGS_PER_PARTITION;
|
|
1467
|
+
rd_kafka_message_t **rkmessages;
|
|
1468
|
+
rd_kafka_messages_t *phase1_batch = NULL;
|
|
1469
|
+
rd_kafka_messages_t *phase2_batch = NULL;
|
|
1470
|
+
test_ack_cb_state_t cb_state = {0};
|
|
1471
|
+
rd_ts_t t_p1_end_us, t_callbacks_done_us;
|
|
1472
|
+
int actual_wait_ms, expected_wait_ms;
|
|
1473
|
+
int min_between_rtt_ms_socket_timeout_ms;
|
|
1474
|
+
int min_between_api_timeout_ms_rtt_ms_socket_timeout_ms;
|
|
1475
|
+
rd_kafka_resp_err_t expected_phase1_commit_err;
|
|
1476
|
+
rd_kafka_resp_err_t expected_phase1_callback_err;
|
|
1477
|
+
int prev_callback_cnt;
|
|
1478
|
+
int i;
|
|
1479
|
+
/* Valgrind serializes threads and slows wall-clock-bound paths;
|
|
1480
|
+
* widen the tolerance to avoid flaky over-budget failures. */
|
|
1481
|
+
const int wait_tolerance_ms =
|
|
1482
|
+
!strcmp(test_mode, "valgrind") ? 2000 : 500;
|
|
1483
|
+
|
|
1484
|
+
SUB_TEST_QUICK("api_timeout_ms=%d socket_timeout_ms=%d rtt_ms=%d",
|
|
1485
|
+
api_timeout_ms, socket_timeout_ms, rtt_ms);
|
|
1486
|
+
|
|
1487
|
+
/* Derive expected outcomes from the ordering of three timers:
|
|
1488
|
+
*
|
|
1489
|
+
* api_timeout_ms -> commit_sync API's deadline parameter;
|
|
1490
|
+
* upper bound on how long commit_sync
|
|
1491
|
+
* can block before returning
|
|
1492
|
+
* REQUEST_TIMED_OUT.
|
|
1493
|
+
* rtt_ms -> mock-injected broker round-trip-time;
|
|
1494
|
+
* time after which the broker's
|
|
1495
|
+
* ShareAcknowledge response is allowed
|
|
1496
|
+
* to leave the broker side (records are
|
|
1497
|
+
* applied broker-side immediately on
|
|
1498
|
+
* request receive — see
|
|
1499
|
+
* rdkafka_mock_handlers.c:3811-3840).
|
|
1500
|
+
* socket_timeout_ms -> wire-level socket.timeout.ms; after
|
|
1501
|
+
* this the connection is torn down and
|
|
1502
|
+
* any in-flight request fails with
|
|
1503
|
+
* __TIMED_OUT (default socket.max.fails
|
|
1504
|
+
* of 1 makes a single failure terminal).
|
|
1505
|
+
*
|
|
1506
|
+
* The expected wait between commit_sync returning and the last
|
|
1507
|
+
* ack callback landing equals
|
|
1508
|
+
* max(0, min_between(rtt_ms, socket_timeout_ms)
|
|
1509
|
+
* - min_between(api_timeout_ms, rtt_ms,
|
|
1510
|
+
* socket_timeout_ms))
|
|
1511
|
+
*
|
|
1512
|
+
* The reasoning:
|
|
1513
|
+
*
|
|
1514
|
+
* - commit_sync returns at
|
|
1515
|
+
* t_p1_end = min_between(api_timeout_ms, rtt_ms,
|
|
1516
|
+
* socket_timeout_ms):
|
|
1517
|
+
* api_timeout_ms smallest: api timer cb fires, stamps
|
|
1518
|
+
* REQUEST_TIMED_OUT on the per-partition results;
|
|
1519
|
+
* send_response wakes commit_sync at ~t=api_timeout_ms.
|
|
1520
|
+
* socket_timeout_ms smallest: wire socket.timeout.ms
|
|
1521
|
+
* fires, broker thread reply path runs with
|
|
1522
|
+
* rko_err=__TIMED_OUT, callbacks dispatched, results
|
|
1523
|
+
* stamped via apply_result, commit_sync returns at
|
|
1524
|
+
* ~t=socket_timeout_ms.
|
|
1525
|
+
* rtt_ms smallest: broker reply arrives normally,
|
|
1526
|
+
* callbacks dispatched with broker's per-partition err,
|
|
1527
|
+
* results stamped, commit_sync returns at ~t=rtt_ms
|
|
1528
|
+
* (NO_ERROR for this happy path — not in our matrix).
|
|
1529
|
+
*
|
|
1530
|
+
* - All Phase 1 callbacks have landed by
|
|
1531
|
+
* t_callbacks_done = min_between(rtt_ms, socket_timeout_ms):
|
|
1532
|
+
* When the wire request resolves the broker-thread reply
|
|
1533
|
+
* handler dispatches the per-partition callbacks. That
|
|
1534
|
+
* happens when broker actually replies (t=rtt_ms) OR
|
|
1535
|
+
* when socket.timeout.ms fires (t=socket_timeout_ms),
|
|
1536
|
+
* whichever comes first. For
|
|
1537
|
+
* api_timeout_ms < (rtt_ms, socket_timeout_ms) the
|
|
1538
|
+
* callbacks fire AFTER commit_sync returned; for
|
|
1539
|
+
* (rtt_ms or socket_timeout_ms) < api_timeout_ms the
|
|
1540
|
+
* callbacks already fired BEFORE commit_sync returned
|
|
1541
|
+
* and the wait is 0.
|
|
1542
|
+
*
|
|
1543
|
+
* - Wait = t_callbacks_done - t_p1_end
|
|
1544
|
+
* = min_between(rtt_ms, socket_timeout_ms)
|
|
1545
|
+
* - min_between(api_timeout_ms, rtt_ms,
|
|
1546
|
+
* socket_timeout_ms).
|
|
1547
|
+
*
|
|
1548
|
+
* The expected callback err depends on which layer resolved
|
|
1549
|
+
* the wire:
|
|
1550
|
+
* - rtt_ms < socket_timeout_ms: broker actually replied —
|
|
1551
|
+
* callback gets broker's per-partition result (NO_ERROR,
|
|
1552
|
+
* the records were applied).
|
|
1553
|
+
* - socket_timeout_ms < rtt_ms: wire torn down before broker
|
|
1554
|
+
* could reply — helper stamps __TIMED_OUT on batches; the
|
|
1555
|
+
* app-facing funnel translates it to REQUEST_TIMED_OUT
|
|
1556
|
+
* before the callback fires.
|
|
1557
|
+
*
|
|
1558
|
+
* The expected commit_sync result err comes from which layer
|
|
1559
|
+
* stamped results first:
|
|
1560
|
+
* - api_timeout_ms < min_between(rtt_ms, socket_timeout_ms):
|
|
1561
|
+
* api timer cb wrote REQUEST_TIMED_OUT into results.
|
|
1562
|
+
* - socket_timeout_ms <= api_timeout_ms: broker-thread reply
|
|
1563
|
+
* path wrote __TIMED_OUT into results via apply_result;
|
|
1564
|
+
* the funnel translates it to REQUEST_TIMED_OUT. */
|
|
1565
|
+
min_between_rtt_ms_socket_timeout_ms =
|
|
1566
|
+
socket_timeout_ms < rtt_ms ? socket_timeout_ms : rtt_ms;
|
|
1567
|
+
min_between_api_timeout_ms_rtt_ms_socket_timeout_ms = api_timeout_ms;
|
|
1568
|
+
if (rtt_ms < min_between_api_timeout_ms_rtt_ms_socket_timeout_ms)
|
|
1569
|
+
min_between_api_timeout_ms_rtt_ms_socket_timeout_ms = rtt_ms;
|
|
1570
|
+
if (socket_timeout_ms <
|
|
1571
|
+
min_between_api_timeout_ms_rtt_ms_socket_timeout_ms)
|
|
1572
|
+
min_between_api_timeout_ms_rtt_ms_socket_timeout_ms =
|
|
1573
|
+
socket_timeout_ms;
|
|
1574
|
+
expected_wait_ms = min_between_rtt_ms_socket_timeout_ms -
|
|
1575
|
+
min_between_api_timeout_ms_rtt_ms_socket_timeout_ms;
|
|
1576
|
+
|
|
1577
|
+
if (rtt_ms < api_timeout_ms && rtt_ms < socket_timeout_ms) {
|
|
1578
|
+
/* rtt_ms smaller than both other timers: broker reply
|
|
1579
|
+
* lands before either timer fires. commit_sync sees
|
|
1580
|
+
* the broker's per-partition success and callbacks
|
|
1581
|
+
* fire with NO_ERROR. Happy path — included for
|
|
1582
|
+
* matrix completeness, not exercising any timeout
|
|
1583
|
+
* layer. */
|
|
1584
|
+
expected_phase1_commit_err = RD_KAFKA_RESP_ERR_NO_ERROR;
|
|
1585
|
+
expected_phase1_callback_err = RD_KAFKA_RESP_ERR_NO_ERROR;
|
|
1586
|
+
} else if (socket_timeout_ms < api_timeout_ms) {
|
|
1587
|
+
/* socket_timeout_ms < api_timeout_ms (and rtt_ms is
|
|
1588
|
+
* NOT strictly smallest by the first branch): socket
|
|
1589
|
+
* fires before api can. (At api == socket the race
|
|
1590
|
+
* outcome is non-deterministic across runs; that
|
|
1591
|
+
* boundary case is not in the matrix.)
|
|
1592
|
+
*
|
|
1593
|
+
* __TIMED_OUT from the broker-thread socket timer is
|
|
1594
|
+
* translated to REQUEST_TIMED_OUT at the app-facing
|
|
1595
|
+
* funnel. */
|
|
1596
|
+
expected_phase1_commit_err =
|
|
1597
|
+
RD_KAFKA_RESP_ERR_REQUEST_TIMED_OUT;
|
|
1598
|
+
expected_phase1_callback_err =
|
|
1599
|
+
RD_KAFKA_RESP_ERR_REQUEST_TIMED_OUT;
|
|
1600
|
+
} else if (rtt_ms < socket_timeout_ms) {
|
|
1601
|
+
/* api wins (api <= socket AND rtt < socket; api
|
|
1602
|
+
* fires no later than broker reply at rtt). Late
|
|
1603
|
+
* broker reply at rtt brings actual per-partition
|
|
1604
|
+
* outcome (NO_ERROR — broker did apply the ack) to
|
|
1605
|
+
* the callback. */
|
|
1606
|
+
expected_phase1_commit_err =
|
|
1607
|
+
RD_KAFKA_RESP_ERR_REQUEST_TIMED_OUT;
|
|
1608
|
+
expected_phase1_callback_err = RD_KAFKA_RESP_ERR_NO_ERROR;
|
|
1609
|
+
} else {
|
|
1610
|
+
/* api wins (api <= socket AND rtt >= socket). Socket
|
|
1611
|
+
* fires before broker can reply, wire torn down; the
|
|
1612
|
+
* late wire-failure err is __TIMED_OUT, translated to
|
|
1613
|
+
* REQUEST_TIMED_OUT at the app-facing funnel. */
|
|
1614
|
+
expected_phase1_commit_err =
|
|
1615
|
+
RD_KAFKA_RESP_ERR_REQUEST_TIMED_OUT;
|
|
1616
|
+
expected_phase1_callback_err =
|
|
1617
|
+
RD_KAFKA_RESP_ERR_REQUEST_TIMED_OUT;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
ctx = test_ctx_new();
|
|
1621
|
+
rd_kafka_mock_sharegroup_set_record_lock_duration(
|
|
1622
|
+
ctx.mcluster, SOCKET_TIMEOUT_MATRIX_RECORD_LOCK_MS);
|
|
1623
|
+
|
|
1624
|
+
rd_snprintf(topic, sizeof(topic), "0182-fullack-a%d-s%d",
|
|
1625
|
+
api_timeout_ms, socket_timeout_ms);
|
|
1626
|
+
rd_snprintf(group, sizeof(group), "sg-0182-fullack-a%d-s%d",
|
|
1627
|
+
api_timeout_ms, socket_timeout_ms);
|
|
1628
|
+
|
|
1629
|
+
TEST_ASSERT(rd_kafka_mock_topic_create(ctx.mcluster, topic,
|
|
1630
|
+
partitions_total,
|
|
1631
|
+
1) == RD_KAFKA_RESP_ERR_NO_ERROR,
|
|
1632
|
+
"create topic");
|
|
1633
|
+
|
|
1634
|
+
for (i = 0; i < partitions_total; i++)
|
|
1635
|
+
mock_produce_partition(
|
|
1636
|
+
ctx.producer, topic, i,
|
|
1637
|
+
SOCKET_TIMEOUT_MATRIX_MSGS_PER_PARTITION);
|
|
1638
|
+
|
|
1639
|
+
rkshare = create_share_consumer_socket_timeout(
|
|
1640
|
+
ctx.bootstraps, group, "explicit", socket_timeout_ms, &cb_state,
|
|
1641
|
+
test_share_ack_cb);
|
|
1642
|
+
test_share_consumer_subscribe_multi(rkshare, 1, topic);
|
|
1643
|
+
|
|
1644
|
+
rkmessages = rd_calloc(msgcnt, sizeof(*rkmessages));
|
|
1645
|
+
|
|
1646
|
+
/* Phase 1: consume the first batch (all msgcnt records
|
|
1647
|
+
* given the small partition count and broker readiness),
|
|
1648
|
+
* acknowledge everyone, then inject RTT and commit_sync. */
|
|
1649
|
+
consume_first_batch(rkshare, rkmessages, msgcnt, &phase1_batch);
|
|
1650
|
+
for (i = 0; i < msgcnt; i++)
|
|
1651
|
+
rd_kafka_share_acknowledge(rkshare, rkmessages[i]);
|
|
1652
|
+
|
|
1653
|
+
rd_kafka_mock_broker_set_rtt(ctx.mcluster, -1, rtt_ms);
|
|
1654
|
+
|
|
1655
|
+
partitions = NULL;
|
|
1656
|
+
error =
|
|
1657
|
+
rd_kafka_share_commit_sync(rkshare, api_timeout_ms, &partitions);
|
|
1658
|
+
t_p1_end_us = test_clock();
|
|
1659
|
+
|
|
1660
|
+
if (error)
|
|
1661
|
+
rd_kafka_error_destroy(error);
|
|
1662
|
+
|
|
1663
|
+
TEST_ASSERT(partitions != NULL,
|
|
1664
|
+
"Phase 1: expected non-NULL partition results");
|
|
1665
|
+
TEST_ASSERT(partitions->cnt == partitions_total,
|
|
1666
|
+
"Phase 1: expected %d partition results, got %d",
|
|
1667
|
+
partitions_total, partitions->cnt);
|
|
1668
|
+
|
|
1669
|
+
for (i = 0; i < partitions->cnt; i++) {
|
|
1670
|
+
rd_kafka_topic_partition_t *rktpar = &partitions->elems[i];
|
|
1671
|
+
TEST_SAY("Phase 1 commit_sync: %s [%" PRId32 "]: %s\n",
|
|
1672
|
+
rktpar->topic, rktpar->partition,
|
|
1673
|
+
rd_kafka_err2name(rktpar->err));
|
|
1674
|
+
TEST_ASSERT(rktpar->err == expected_phase1_commit_err,
|
|
1675
|
+
"Phase 1: expected %s, got %s",
|
|
1676
|
+
rd_kafka_err2name(expected_phase1_commit_err),
|
|
1677
|
+
rd_kafka_err2name(rktpar->err));
|
|
1678
|
+
}
|
|
1679
|
+
rd_kafka_topic_partition_list_destroy(partitions);
|
|
1680
|
+
|
|
1681
|
+
/* Drain ack callbacks via commit_async until we observe one
|
|
1682
|
+
* per partition. For socket_timeout_ms-smallest orderings the
|
|
1683
|
+
* callbacks fired before commit_sync returned so this returns
|
|
1684
|
+
* immediately. For api_timeout_ms-smallest orderings the
|
|
1685
|
+
* callbacks fire when the wire resolves (broker reply at
|
|
1686
|
+
* t=rtt_ms OR socket timer at t=socket_timeout_ms). */
|
|
1687
|
+
TEST_ASSERT(drain_callbacks_via_commit_async(rkshare, &cb_state,
|
|
1688
|
+
partitions_total, 30000),
|
|
1689
|
+
"Phase 1: expected %d ack callbacks within 30s, got %d",
|
|
1690
|
+
partitions_total, cb_state.callback_cnt);
|
|
1691
|
+
t_callbacks_done_us = test_clock();
|
|
1692
|
+
|
|
1693
|
+
TEST_ASSERT(test_ack_cb_state_first_err(&cb_state) ==
|
|
1694
|
+
expected_phase1_callback_err,
|
|
1695
|
+
"Phase 1: expected callback err %s, got %s",
|
|
1696
|
+
rd_kafka_err2name(expected_phase1_callback_err),
|
|
1697
|
+
rd_kafka_err2name(test_ack_cb_state_first_err(&cb_state)));
|
|
1698
|
+
|
|
1699
|
+
actual_wait_ms = (int)((t_callbacks_done_us - t_p1_end_us) / 1000);
|
|
1700
|
+
TEST_SAY(
|
|
1701
|
+
"Phase 1 wait t_callbacks_done - t_p1_end = %dms "
|
|
1702
|
+
"(expected ~%dms, tolerance %dms)\n",
|
|
1703
|
+
actual_wait_ms, expected_wait_ms, wait_tolerance_ms);
|
|
1704
|
+
TEST_ASSERT(actual_wait_ms >= expected_wait_ms - wait_tolerance_ms &&
|
|
1705
|
+
actual_wait_ms <= expected_wait_ms + wait_tolerance_ms,
|
|
1706
|
+
"Phase 1 wait %dms outside expected %dms +/- %dms",
|
|
1707
|
+
actual_wait_ms, expected_wait_ms, wait_tolerance_ms);
|
|
1708
|
+
|
|
1709
|
+
prev_callback_cnt = cb_state.callback_cnt;
|
|
1710
|
+
|
|
1711
|
+
/* Phase 2: clear RTT (so Phase 2's broker response isn't
|
|
1712
|
+
* delayed), produce more, consume + ack + commit. */
|
|
1713
|
+
rd_kafka_mock_broker_set_rtt(ctx.mcluster, -1, 0);
|
|
1714
|
+
|
|
1715
|
+
for (i = 0; i < partitions_total; i++)
|
|
1716
|
+
mock_produce_partition(
|
|
1717
|
+
ctx.producer, topic, i,
|
|
1718
|
+
SOCKET_TIMEOUT_MATRIX_MSGS_PER_PARTITION);
|
|
1719
|
+
|
|
1720
|
+
/* Free phase-1 messages. */
|
|
1721
|
+
rd_kafka_messages_destroy(phase1_batch);
|
|
1722
|
+
phase1_batch = NULL;
|
|
1723
|
+
memset(rkmessages, 0, msgcnt * sizeof(*rkmessages));
|
|
1724
|
+
|
|
1725
|
+
consume_first_batch(rkshare, rkmessages, msgcnt, &phase2_batch);
|
|
1726
|
+
for (i = 0; i < msgcnt; i++)
|
|
1727
|
+
rd_kafka_share_acknowledge(rkshare, rkmessages[i]);
|
|
1728
|
+
|
|
1729
|
+
partitions = NULL;
|
|
1730
|
+
error = rd_kafka_share_commit_sync(rkshare, 5000, &partitions);
|
|
1731
|
+
if (error)
|
|
1732
|
+
rd_kafka_error_destroy(error);
|
|
1733
|
+
|
|
1734
|
+
TEST_ASSERT(partitions != NULL,
|
|
1735
|
+
"Phase 2: expected non-NULL partition results");
|
|
1736
|
+
|
|
1737
|
+
for (i = 0; i < partitions->cnt; i++) {
|
|
1738
|
+
rd_kafka_topic_partition_t *rktpar = &partitions->elems[i];
|
|
1739
|
+
TEST_SAY("Phase 2: %s [%" PRId32 "]: %s\n", rktpar->topic,
|
|
1740
|
+
rktpar->partition, rd_kafka_err2name(rktpar->err));
|
|
1741
|
+
TEST_ASSERT(rktpar->err == RD_KAFKA_RESP_ERR_NO_ERROR,
|
|
1742
|
+
"Phase 2: expected NO_ERROR on a fresh "
|
|
1743
|
+
"session, got %s",
|
|
1744
|
+
rd_kafka_err2name(rktpar->err));
|
|
1745
|
+
}
|
|
1746
|
+
TEST_ASSERT(partitions->cnt == partitions_total,
|
|
1747
|
+
"Phase 2: expected %d partition results, got %d",
|
|
1748
|
+
partitions_total, partitions->cnt);
|
|
1749
|
+
rd_kafka_topic_partition_list_destroy(partitions);
|
|
1750
|
+
|
|
1751
|
+
TEST_ASSERT(cb_state.callback_cnt > prev_callback_cnt,
|
|
1752
|
+
"Phase 2: expected new ack callbacks; before=%d "
|
|
1753
|
+
"after=%d",
|
|
1754
|
+
prev_callback_cnt, cb_state.callback_cnt);
|
|
1755
|
+
|
|
1756
|
+
rd_kafka_messages_destroy(phase2_batch);
|
|
1757
|
+
rd_free(rkmessages);
|
|
1758
|
+
|
|
1759
|
+
test_share_consumer_close(rkshare);
|
|
1760
|
+
test_share_destroy(rkshare);
|
|
1761
|
+
test_ctx_destroy(&ctx);
|
|
1762
|
+
test_ack_cb_state_destroy(&cb_state);
|
|
1763
|
+
|
|
1764
|
+
SUB_TEST_PASS();
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
/**
|
|
1768
|
+
* @brief Same Phase 1 as do_test_socket_timeout_full_ack_then_more, but
|
|
1769
|
+
* Phase 1 acks only half the consumed records. Phase 2 then
|
|
1770
|
+
* acks the remaining half (still in the client's local inflight
|
|
1771
|
+
* map) and runs commit_sync on those.
|
|
1772
|
+
*
|
|
1773
|
+
* After the Phase 1 drain-callbacks step we clear RTT, so Phase 2's
|
|
1774
|
+
* commit_sync gets a prompt broker response (no second wire-level
|
|
1775
|
+
* timer to race).
|
|
1776
|
+
*
|
|
1777
|
+
* Phase 2 outcome depends on whether the connection was torn down
|
|
1778
|
+
* during Phase 1, which is equivalent to socket_timeout_ms < rtt_ms:
|
|
1779
|
+
*
|
|
1780
|
+
* Connection alive (rtt_ms < socket_timeout_ms): broker session
|
|
1781
|
+
* preserved; the remaining 15 records are still ACQUIRED for
|
|
1782
|
+
* this member on the same session. Phase 2's ShareAck advances
|
|
1783
|
+
* the session epoch normally; broker returns NO_ERROR per
|
|
1784
|
+
* partition.
|
|
1785
|
+
*
|
|
1786
|
+
* Connection killed (socket_timeout_ms < rtt_ms): broker dropped
|
|
1787
|
+
* the session on TCP close. The remaining 15 records' ACQUIRED
|
|
1788
|
+
* state was released to AVAILABLE by `release_member_locks`.
|
|
1789
|
+
* Phase 2's broker-thread reconnects and sends ShareAck with
|
|
1790
|
+
* session epoch reset to 0 client-side. Broker rejects with
|
|
1791
|
+
* INVALID_SHARE_SESSION_EPOCH at top-level (no active session
|
|
1792
|
+
* for this member at epoch 0 outside of a ShareFetch). The
|
|
1793
|
+
* top-level err propagates to all partitions via the existing
|
|
1794
|
+
* `rd_kafka_share_fetch_op_reply_and_update_ack_details_with_err`
|
|
1795
|
+
* helper.
|
|
1796
|
+
*
|
|
1797
|
+
* TODO KIP-932: when SHARE_SESSION_NOT_FOUND /
|
|
1798
|
+
* INVALID_SHARE_SESSION_EPOCH are translated to
|
|
1799
|
+
* INVALID_RECORD_STATE at the app-facing boundary,
|
|
1800
|
+
* expected_phase2_commit_err for the killed branch becomes
|
|
1801
|
+
* INVALID_RECORD_STATE.
|
|
1802
|
+
*/
|
|
1803
|
+
static void
|
|
1804
|
+
do_test_socket_timeout_partial_ack_then_remaining(int api_timeout_ms,
|
|
1805
|
+
int socket_timeout_ms,
|
|
1806
|
+
int rtt_ms) {
|
|
1807
|
+
test_ctx_t ctx;
|
|
1808
|
+
rd_kafka_share_t *rkshare;
|
|
1809
|
+
rd_kafka_topic_partition_list_t *partitions = NULL;
|
|
1810
|
+
rd_kafka_error_t *error;
|
|
1811
|
+
char topic[128];
|
|
1812
|
+
char group[128];
|
|
1813
|
+
const int partitions_total = SOCKET_TIMEOUT_MATRIX_PARTITIONS;
|
|
1814
|
+
const int msgcnt =
|
|
1815
|
+
partitions_total * SOCKET_TIMEOUT_MATRIX_MSGS_PER_PARTITION;
|
|
1816
|
+
rd_kafka_message_t **rkmessages;
|
|
1817
|
+
rd_kafka_messages_t *phase1_batch = NULL;
|
|
1818
|
+
test_ack_cb_state_t cb_state = {0};
|
|
1819
|
+
rd_ts_t t_p1_end_us, t_callbacks_done_us;
|
|
1820
|
+
int actual_wait_ms, expected_wait_ms;
|
|
1821
|
+
int min_between_rtt_ms_socket_timeout_ms;
|
|
1822
|
+
int min_between_api_timeout_ms_rtt_ms_socket_timeout_ms;
|
|
1823
|
+
rd_kafka_resp_err_t expected_phase1_commit_err;
|
|
1824
|
+
rd_kafka_resp_err_t expected_phase1_callback_err;
|
|
1825
|
+
rd_kafka_resp_err_t expected_phase2_commit_err;
|
|
1826
|
+
rd_bool_t connection_killed;
|
|
1827
|
+
int prev_callback_cnt;
|
|
1828
|
+
int phase1_partition_cnt;
|
|
1829
|
+
int i;
|
|
1830
|
+
/* See do_test_socket_timeout_full_ack_then_more for the Valgrind
|
|
1831
|
+
* tolerance rationale. */
|
|
1832
|
+
const int wait_tolerance_ms =
|
|
1833
|
+
!strcmp(test_mode, "valgrind") ? 2000 : 500;
|
|
1834
|
+
|
|
1835
|
+
SUB_TEST_QUICK("api_timeout_ms=%d socket_timeout_ms=%d rtt_ms=%d",
|
|
1836
|
+
api_timeout_ms, socket_timeout_ms, rtt_ms);
|
|
1837
|
+
|
|
1838
|
+
/* Phase 1 expected outcomes — same derivation as
|
|
1839
|
+
* do_test_socket_timeout_full_ack_then_more. See the comment
|
|
1840
|
+
* there for the full reasoning. */
|
|
1841
|
+
min_between_rtt_ms_socket_timeout_ms =
|
|
1842
|
+
socket_timeout_ms < rtt_ms ? socket_timeout_ms : rtt_ms;
|
|
1843
|
+
min_between_api_timeout_ms_rtt_ms_socket_timeout_ms = api_timeout_ms;
|
|
1844
|
+
if (rtt_ms < min_between_api_timeout_ms_rtt_ms_socket_timeout_ms)
|
|
1845
|
+
min_between_api_timeout_ms_rtt_ms_socket_timeout_ms = rtt_ms;
|
|
1846
|
+
if (socket_timeout_ms <
|
|
1847
|
+
min_between_api_timeout_ms_rtt_ms_socket_timeout_ms)
|
|
1848
|
+
min_between_api_timeout_ms_rtt_ms_socket_timeout_ms =
|
|
1849
|
+
socket_timeout_ms;
|
|
1850
|
+
expected_wait_ms = min_between_rtt_ms_socket_timeout_ms -
|
|
1851
|
+
min_between_api_timeout_ms_rtt_ms_socket_timeout_ms;
|
|
1852
|
+
|
|
1853
|
+
if (rtt_ms < api_timeout_ms && rtt_ms < socket_timeout_ms) {
|
|
1854
|
+
expected_phase1_commit_err = RD_KAFKA_RESP_ERR_NO_ERROR;
|
|
1855
|
+
expected_phase1_callback_err = RD_KAFKA_RESP_ERR_NO_ERROR;
|
|
1856
|
+
} else if (socket_timeout_ms < api_timeout_ms) {
|
|
1857
|
+
/* __TIMED_OUT from the broker-thread socket timer is
|
|
1858
|
+
* translated to REQUEST_TIMED_OUT at the app-facing
|
|
1859
|
+
* funnel. */
|
|
1860
|
+
expected_phase1_commit_err =
|
|
1861
|
+
RD_KAFKA_RESP_ERR_REQUEST_TIMED_OUT;
|
|
1862
|
+
expected_phase1_callback_err =
|
|
1863
|
+
RD_KAFKA_RESP_ERR_REQUEST_TIMED_OUT;
|
|
1864
|
+
} else if (rtt_ms < socket_timeout_ms) {
|
|
1865
|
+
expected_phase1_commit_err =
|
|
1866
|
+
RD_KAFKA_RESP_ERR_REQUEST_TIMED_OUT;
|
|
1867
|
+
expected_phase1_callback_err = RD_KAFKA_RESP_ERR_NO_ERROR;
|
|
1868
|
+
} else {
|
|
1869
|
+
/* Late wire-failure callback err __TIMED_OUT is
|
|
1870
|
+
* translated to REQUEST_TIMED_OUT at the app-facing
|
|
1871
|
+
* funnel. */
|
|
1872
|
+
expected_phase1_commit_err =
|
|
1873
|
+
RD_KAFKA_RESP_ERR_REQUEST_TIMED_OUT;
|
|
1874
|
+
expected_phase1_callback_err =
|
|
1875
|
+
RD_KAFKA_RESP_ERR_REQUEST_TIMED_OUT;
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
/* Connection torn down iff socket fires before broker reply.
|
|
1879
|
+
*
|
|
1880
|
+
* TODO KIP-932: when SHARE_SESSION_NOT_FOUND /
|
|
1881
|
+
* INVALID_SHARE_SESSION_EPOCH are translated to
|
|
1882
|
+
* INVALID_RECORD_STATE at the app-facing boundary, the
|
|
1883
|
+
* killed-branch expected err becomes INVALID_RECORD_STATE. */
|
|
1884
|
+
connection_killed = socket_timeout_ms < rtt_ms;
|
|
1885
|
+
expected_phase2_commit_err =
|
|
1886
|
+
connection_killed ? RD_KAFKA_RESP_ERR_INVALID_SHARE_SESSION_EPOCH
|
|
1887
|
+
: RD_KAFKA_RESP_ERR_NO_ERROR;
|
|
1888
|
+
|
|
1889
|
+
ctx = test_ctx_new();
|
|
1890
|
+
rd_kafka_mock_sharegroup_set_record_lock_duration(
|
|
1891
|
+
ctx.mcluster, SOCKET_TIMEOUT_MATRIX_RECORD_LOCK_MS);
|
|
1892
|
+
|
|
1893
|
+
rd_snprintf(topic, sizeof(topic), "0182-partial-a%d-s%d-r%d",
|
|
1894
|
+
api_timeout_ms, socket_timeout_ms, rtt_ms);
|
|
1895
|
+
rd_snprintf(group, sizeof(group), "sg-0182-partial-a%d-s%d-r%d",
|
|
1896
|
+
api_timeout_ms, socket_timeout_ms, rtt_ms);
|
|
1897
|
+
|
|
1898
|
+
TEST_ASSERT(rd_kafka_mock_topic_create(ctx.mcluster, topic,
|
|
1899
|
+
partitions_total,
|
|
1900
|
+
1) == RD_KAFKA_RESP_ERR_NO_ERROR,
|
|
1901
|
+
"create topic");
|
|
1902
|
+
|
|
1903
|
+
for (i = 0; i < partitions_total; i++)
|
|
1904
|
+
mock_produce_partition(
|
|
1905
|
+
ctx.producer, topic, i,
|
|
1906
|
+
SOCKET_TIMEOUT_MATRIX_MSGS_PER_PARTITION);
|
|
1907
|
+
|
|
1908
|
+
rkshare = create_share_consumer_socket_timeout(
|
|
1909
|
+
ctx.bootstraps, group, "explicit", socket_timeout_ms, &cb_state,
|
|
1910
|
+
test_share_ack_cb);
|
|
1911
|
+
test_share_consumer_subscribe_multi(rkshare, 1, topic);
|
|
1912
|
+
|
|
1913
|
+
rkmessages = rd_calloc(msgcnt, sizeof(*rkmessages));
|
|
1914
|
+
|
|
1915
|
+
/* Phase 1: consume the first batch (all msgcnt records),
|
|
1916
|
+
* then ack the first half of the batch in receive order.
|
|
1917
|
+
* The exact set of partitions covered depends on broker-side
|
|
1918
|
+
* record ordering; the assertions below adapt to whatever
|
|
1919
|
+
* partitions appear in commit_sync's results. */
|
|
1920
|
+
consume_first_batch(rkshare, rkmessages, msgcnt, &phase1_batch);
|
|
1921
|
+
for (i = 0; i < msgcnt / 2; i++)
|
|
1922
|
+
rd_kafka_share_acknowledge(rkshare, rkmessages[i]);
|
|
1923
|
+
|
|
1924
|
+
rd_kafka_mock_broker_set_rtt(ctx.mcluster, -1, rtt_ms);
|
|
1925
|
+
|
|
1926
|
+
partitions = NULL;
|
|
1927
|
+
error =
|
|
1928
|
+
rd_kafka_share_commit_sync(rkshare, api_timeout_ms, &partitions);
|
|
1929
|
+
t_p1_end_us = test_clock();
|
|
1930
|
+
|
|
1931
|
+
if (error)
|
|
1932
|
+
rd_kafka_error_destroy(error);
|
|
1933
|
+
|
|
1934
|
+
TEST_ASSERT(partitions != NULL,
|
|
1935
|
+
"Phase 1: expected non-NULL partition results");
|
|
1936
|
+
|
|
1937
|
+
/* The number of partitions reflects whichever ones had records
|
|
1938
|
+
* in the first half of the batch — depends on broker-side
|
|
1939
|
+
* record ordering. We assert each partition's err matches the
|
|
1940
|
+
* expected code, and drain that many callbacks. */
|
|
1941
|
+
phase1_partition_cnt = partitions->cnt;
|
|
1942
|
+
TEST_ASSERT(phase1_partition_cnt > 0,
|
|
1943
|
+
"Phase 1: expected at least one partition result");
|
|
1944
|
+
|
|
1945
|
+
for (i = 0; i < partitions->cnt; i++) {
|
|
1946
|
+
rd_kafka_topic_partition_t *rktpar = &partitions->elems[i];
|
|
1947
|
+
TEST_SAY("Phase 1 commit_sync: %s [%" PRId32 "]: %s\n",
|
|
1948
|
+
rktpar->topic, rktpar->partition,
|
|
1949
|
+
rd_kafka_err2name(rktpar->err));
|
|
1950
|
+
TEST_ASSERT(rktpar->err == expected_phase1_commit_err,
|
|
1951
|
+
"Phase 1: expected %s, got %s",
|
|
1952
|
+
rd_kafka_err2name(expected_phase1_commit_err),
|
|
1953
|
+
rd_kafka_err2name(rktpar->err));
|
|
1954
|
+
}
|
|
1955
|
+
rd_kafka_topic_partition_list_destroy(partitions);
|
|
1956
|
+
|
|
1957
|
+
TEST_ASSERT(drain_callbacks_via_commit_async(
|
|
1958
|
+
rkshare, &cb_state, phase1_partition_cnt, 30000),
|
|
1959
|
+
"Phase 1: expected %d ack callbacks within 30s, got %d",
|
|
1960
|
+
phase1_partition_cnt, cb_state.callback_cnt);
|
|
1961
|
+
t_callbacks_done_us = test_clock();
|
|
1962
|
+
|
|
1963
|
+
TEST_ASSERT(test_ack_cb_state_first_err(&cb_state) ==
|
|
1964
|
+
expected_phase1_callback_err,
|
|
1965
|
+
"Phase 1: expected callback err %s, got %s",
|
|
1966
|
+
rd_kafka_err2name(expected_phase1_callback_err),
|
|
1967
|
+
rd_kafka_err2name(test_ack_cb_state_first_err(&cb_state)));
|
|
1968
|
+
|
|
1969
|
+
actual_wait_ms = (int)((t_callbacks_done_us - t_p1_end_us) / 1000);
|
|
1970
|
+
TEST_SAY(
|
|
1971
|
+
"Phase 1 wait t_callbacks_done - t_p1_end = %dms "
|
|
1972
|
+
"(expected ~%dms, tolerance %dms)\n",
|
|
1973
|
+
actual_wait_ms, expected_wait_ms, wait_tolerance_ms);
|
|
1974
|
+
TEST_ASSERT(actual_wait_ms >= expected_wait_ms - wait_tolerance_ms &&
|
|
1975
|
+
actual_wait_ms <= expected_wait_ms + wait_tolerance_ms,
|
|
1976
|
+
"Phase 1 wait %dms outside expected %dms +/- %dms",
|
|
1977
|
+
actual_wait_ms, expected_wait_ms, wait_tolerance_ms);
|
|
1978
|
+
|
|
1979
|
+
prev_callback_cnt = cb_state.callback_cnt;
|
|
1980
|
+
|
|
1981
|
+
/* Phase 2: clear RTT and ack the remaining records (still in
|
|
1982
|
+
* the local inflight map from Phase 1's consume — the second
|
|
1983
|
+
* half per partition). No new consume_batch in this variant.
|
|
1984
|
+
*
|
|
1985
|
+
* Small settle delay so the consumer's broker thread can
|
|
1986
|
+
* finish post-teardown bookkeeping (reconnect, session
|
|
1987
|
+
* reset, broker decommission cleanup) before Phase 2's
|
|
1988
|
+
* commit_sync probes the state. Without this perm 5/6/boundary
|
|
1989
|
+
* cases racing the post-teardown handling can return
|
|
1990
|
+
* __STATE from the FANOUT op instead of the expected
|
|
1991
|
+
* INVALID_SHARE_SESSION_EPOCH from the local epoch-0 check. */
|
|
1992
|
+
rd_kafka_mock_broker_set_rtt(ctx.mcluster, -1, 0);
|
|
1993
|
+
rd_sleep(1);
|
|
1994
|
+
|
|
1995
|
+
/* Ack the remaining half of the batch in receive order. */
|
|
1996
|
+
for (i = msgcnt / 2; i < msgcnt; i++)
|
|
1997
|
+
rd_kafka_share_acknowledge(rkshare, rkmessages[i]);
|
|
1998
|
+
|
|
1999
|
+
partitions = NULL;
|
|
2000
|
+
error = rd_kafka_share_commit_sync(rkshare, 5000, &partitions);
|
|
2001
|
+
if (error)
|
|
2002
|
+
rd_kafka_error_destroy(error);
|
|
2003
|
+
|
|
2004
|
+
TEST_ASSERT(partitions != NULL,
|
|
2005
|
+
"Phase 2: expected non-NULL partition results");
|
|
2006
|
+
|
|
2007
|
+
/* As in Phase 1, the partition count reflects whichever
|
|
2008
|
+
* partitions had records in the second half. Some partitions
|
|
2009
|
+
* may appear in both phases (e.g., partition that had its
|
|
2010
|
+
* first 5 records acked in Phase 1 and last 5 in Phase 2). */
|
|
2011
|
+
TEST_ASSERT(partitions->cnt > 0,
|
|
2012
|
+
"Phase 2: expected at least one partition result");
|
|
2013
|
+
|
|
2014
|
+
for (i = 0; i < partitions->cnt; i++) {
|
|
2015
|
+
rd_kafka_topic_partition_t *rktpar = &partitions->elems[i];
|
|
2016
|
+
TEST_SAY("Phase 2 commit_sync: %s [%" PRId32 "]: %s\n",
|
|
2017
|
+
rktpar->topic, rktpar->partition,
|
|
2018
|
+
rd_kafka_err2name(rktpar->err));
|
|
2019
|
+
TEST_ASSERT(rktpar->err == expected_phase2_commit_err,
|
|
2020
|
+
"Phase 2: expected %s, got %s",
|
|
2021
|
+
rd_kafka_err2name(expected_phase2_commit_err),
|
|
2022
|
+
rd_kafka_err2name(rktpar->err));
|
|
2023
|
+
}
|
|
2024
|
+
TEST_ASSERT(
|
|
2025
|
+
drain_callbacks_via_commit_async(
|
|
2026
|
+
rkshare, &cb_state, prev_callback_cnt + partitions->cnt, 10000),
|
|
2027
|
+
"Phase 2: expected %d new ack callbacks; before=%d "
|
|
2028
|
+
"after=%d",
|
|
2029
|
+
partitions->cnt, prev_callback_cnt, cb_state.callback_cnt);
|
|
2030
|
+
rd_kafka_topic_partition_list_destroy(partitions);
|
|
2031
|
+
|
|
2032
|
+
TEST_ASSERT(
|
|
2033
|
+
cb_state.errs[cb_state.callback_cnt - 1] ==
|
|
2034
|
+
expected_phase2_commit_err,
|
|
2035
|
+
"Phase 2: expected last callback err %s, got %s",
|
|
2036
|
+
rd_kafka_err2name(expected_phase2_commit_err),
|
|
2037
|
+
rd_kafka_err2name(cb_state.errs[cb_state.callback_cnt - 1]));
|
|
2038
|
+
|
|
2039
|
+
rd_kafka_messages_destroy(phase1_batch);
|
|
2040
|
+
rd_free(rkmessages);
|
|
2041
|
+
|
|
2042
|
+
test_share_consumer_close(rkshare);
|
|
2043
|
+
test_share_destroy(rkshare);
|
|
2044
|
+
test_ctx_destroy(&ctx);
|
|
2045
|
+
test_ack_cb_state_destroy(&cb_state);
|
|
2046
|
+
|
|
2047
|
+
SUB_TEST_PASS();
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
/* ===================================================================
|
|
2051
|
+
* Topic-level metadata error tests.
|
|
2052
|
+
*
|
|
2053
|
+
* Verify that a share consumer surfaces topic-level errors from
|
|
2054
|
+
* metadata responses to the application as rd_kafka_error_t via
|
|
2055
|
+
* consume_batch. Only TOPIC_EXCEPTION and TOPIC_AUTHORIZATION_FAILED
|
|
2056
|
+
* reach the app; transient codes (UNKNOWN_TOPIC, UNKNOWN_TOPIC_OR_PART,
|
|
2057
|
+
* UNKNOWN_TOPIC_ID, UNKNOWN_PARTITION) are not delivered. Repeats of
|
|
2058
|
+
* the same (topic, err) are deduped; recovery, unsubscribe, and
|
|
2059
|
+
* re-subscribe each have well-defined behaviour exercised below.
|
|
2060
|
+
*
|
|
2061
|
+
* These scenarios are hard to reproduce on a real broker — the mock
|
|
2062
|
+
* cluster lets us inject the exact per-topic error byte in a metadata
|
|
2063
|
+
* response on demand.
|
|
2064
|
+
* =================================================================== */
|
|
2065
|
+
|
|
2066
|
+
/* Drive at least one successful consume_batch so the share assignment
|
|
2067
|
+
* is fully materialised before the test injects an error. Records are
|
|
2068
|
+
* ACKed inline because the consumer is in explicit-ack mode. */
|
|
2069
|
+
static void share_topic_err_prime_assignment(rd_kafka_share_t *rkshare) {
|
|
2070
|
+
rd_kafka_messages_t *batch = NULL;
|
|
2071
|
+
rd_kafka_error_t *error;
|
|
2072
|
+
size_t rcvd;
|
|
2073
|
+
size_t j;
|
|
2074
|
+
int attempts;
|
|
2075
|
+
rd_bool_t got_any = rd_false;
|
|
2076
|
+
|
|
2077
|
+
for (attempts = 0; attempts < 20; attempts++) {
|
|
2078
|
+
error = rd_kafka_share_poll(rkshare, 1000, &batch);
|
|
2079
|
+
if (error) {
|
|
2080
|
+
rd_kafka_error_destroy(error);
|
|
2081
|
+
rd_kafka_messages_destroy(batch);
|
|
2082
|
+
batch = NULL;
|
|
2083
|
+
continue;
|
|
2084
|
+
}
|
|
2085
|
+
rcvd = rd_kafka_messages_count(batch);
|
|
2086
|
+
for (j = 0; j < rcvd; j++) {
|
|
2087
|
+
rd_kafka_message_t *rkm =
|
|
2088
|
+
rd_kafka_messages_get(batch, j);
|
|
2089
|
+
if (!rkm->err) {
|
|
2090
|
+
rd_kafka_share_acknowledge(rkshare, rkm);
|
|
2091
|
+
got_any = rd_true;
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
rd_kafka_messages_destroy(batch);
|
|
2095
|
+
batch = NULL;
|
|
2096
|
+
if (got_any)
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
2099
|
+
TEST_FAIL(
|
|
2100
|
+
"Pre-condition: expected to consume a batch before "
|
|
2101
|
+
"injecting the metadata error");
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
/* Force a metadata refresh on the share consumer's underlying rk so
|
|
2105
|
+
* the injected per-topic err is observed. */
|
|
2106
|
+
static void share_topic_err_force_metadata(rd_kafka_share_t *rkshare) {
|
|
2107
|
+
const rd_kafka_metadata_t *md = NULL;
|
|
2108
|
+
rd_kafka_t *rk;
|
|
2109
|
+
|
|
2110
|
+
rk = test_share_consumer_get_rk(rkshare);
|
|
2111
|
+
(void)rd_kafka_metadata(rk, 1 /*all_topics*/, NULL, &md, 5000);
|
|
2112
|
+
if (md)
|
|
2113
|
+
rd_kafka_metadata_destroy(md);
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
/* Drain consume_batch until either the expected err code surfaces (then
|
|
2117
|
+
* return rd_true) or `max_attempts` calls go by without it (return
|
|
2118
|
+
* rd_false). Records that arrive are destroyed. */
|
|
2119
|
+
static rd_bool_t share_topic_err_wait_for_err(rd_kafka_share_t *rkshare,
|
|
2120
|
+
rd_kafka_resp_err_t expected,
|
|
2121
|
+
int max_attempts) {
|
|
2122
|
+
rd_kafka_messages_t *batch = NULL;
|
|
2123
|
+
rd_kafka_error_t *error;
|
|
2124
|
+
size_t rcvd;
|
|
2125
|
+
size_t j;
|
|
2126
|
+
int attempts;
|
|
2127
|
+
|
|
2128
|
+
for (attempts = 0; attempts < max_attempts; attempts++) {
|
|
2129
|
+
error = rd_kafka_share_poll(rkshare, 500, &batch);
|
|
2130
|
+
if (error) {
|
|
2131
|
+
rd_kafka_resp_err_t code = rd_kafka_error_code(error);
|
|
2132
|
+
TEST_SAY("share_poll returned %s: %s\n",
|
|
2133
|
+
rd_kafka_err2name(code),
|
|
2134
|
+
rd_kafka_error_string(error));
|
|
2135
|
+
rd_kafka_error_destroy(error);
|
|
2136
|
+
rd_kafka_messages_destroy(batch);
|
|
2137
|
+
batch = NULL;
|
|
2138
|
+
if (code == expected)
|
|
2139
|
+
return rd_true;
|
|
2140
|
+
continue;
|
|
2141
|
+
}
|
|
2142
|
+
rcvd = rd_kafka_messages_count(batch);
|
|
2143
|
+
/* Ack received records so the next share_poll can
|
|
2144
|
+
* proceed past the explicit-mode "previous poll
|
|
2145
|
+
* unacked" gate. */
|
|
2146
|
+
for (j = 0; j < rcvd; j++) {
|
|
2147
|
+
rd_kafka_message_t *rkm =
|
|
2148
|
+
rd_kafka_messages_get(batch, j);
|
|
2149
|
+
if (!rkm->err)
|
|
2150
|
+
rd_kafka_share_acknowledge(rkshare, rkm);
|
|
2151
|
+
}
|
|
2152
|
+
rd_kafka_messages_destroy(batch);
|
|
2153
|
+
batch = NULL;
|
|
2154
|
+
}
|
|
2155
|
+
return rd_false;
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
/* Run `n_attempts` consume_batch calls and fail the test if any of them
|
|
2159
|
+
* returns an rd_kafka_error_t — used for the negative-assertion tests
|
|
2160
|
+
* (transient-code log-only paths must not surface). */
|
|
2161
|
+
static void share_topic_err_assert_no_err(rd_kafka_share_t *rkshare,
|
|
2162
|
+
int n_attempts,
|
|
2163
|
+
const char *context) {
|
|
2164
|
+
rd_kafka_messages_t *batch = NULL;
|
|
2165
|
+
rd_kafka_error_t *error;
|
|
2166
|
+
size_t rcvd;
|
|
2167
|
+
size_t j;
|
|
2168
|
+
int attempts;
|
|
2169
|
+
|
|
2170
|
+
for (attempts = 0; attempts < n_attempts; attempts++) {
|
|
2171
|
+
error = rd_kafka_share_poll(rkshare, 200, &batch);
|
|
2172
|
+
if (error) {
|
|
2173
|
+
rd_kafka_resp_err_t code = rd_kafka_error_code(error);
|
|
2174
|
+
rd_kafka_error_destroy(error);
|
|
2175
|
+
rd_kafka_messages_destroy(batch);
|
|
2176
|
+
TEST_FAIL(
|
|
2177
|
+
"[%s] unexpected error from share_poll: "
|
|
2178
|
+
"%s",
|
|
2179
|
+
context, rd_kafka_err2name(code));
|
|
2180
|
+
}
|
|
2181
|
+
rcvd = rd_kafka_messages_count(batch);
|
|
2182
|
+
/* Ack received records so the next share_poll can
|
|
2183
|
+
* proceed past the explicit-mode "previous poll
|
|
2184
|
+
* unacked" gate. */
|
|
2185
|
+
for (j = 0; j < rcvd; j++) {
|
|
2186
|
+
rd_kafka_message_t *rkm =
|
|
2187
|
+
rd_kafka_messages_get(batch, j);
|
|
2188
|
+
if (!rkm->err)
|
|
2189
|
+
rd_kafka_share_acknowledge(rkshare, rkm);
|
|
2190
|
+
}
|
|
2191
|
+
rd_kafka_messages_destroy(batch);
|
|
2192
|
+
batch = NULL;
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
/* Run one share-topic-err scenario: assign, inject `inject_err`,
|
|
2197
|
+
* verify consume_batch surfaces `expect_err` (or fails). */
|
|
2198
|
+
static void do_test_share_topic_err_surfaces(const char *topic_suffix,
|
|
2199
|
+
rd_kafka_resp_err_t inject_err,
|
|
2200
|
+
rd_kafka_resp_err_t expect_err) {
|
|
2201
|
+
test_ctx_t ctx;
|
|
2202
|
+
rd_kafka_share_t *rkshare;
|
|
2203
|
+
char topic[64];
|
|
2204
|
+
char group[64];
|
|
2205
|
+
|
|
2206
|
+
SUB_TEST_QUICK("inject=%s expect=%s", rd_kafka_err2name(inject_err),
|
|
2207
|
+
rd_kafka_err2name(expect_err));
|
|
2208
|
+
|
|
2209
|
+
ctx = test_ctx_new();
|
|
2210
|
+
rd_snprintf(topic, sizeof(topic), "0182-%s", topic_suffix);
|
|
2211
|
+
rd_snprintf(group, sizeof(group), "sg-0182-%s", topic_suffix);
|
|
2212
|
+
|
|
2213
|
+
TEST_ASSERT(rd_kafka_mock_topic_create(ctx.mcluster, topic, 1, 1) ==
|
|
2214
|
+
RD_KAFKA_RESP_ERR_NO_ERROR,
|
|
2215
|
+
"create topic");
|
|
2216
|
+
mock_produce(ctx.producer, topic, 5);
|
|
2217
|
+
|
|
2218
|
+
rkshare = create_mock_share_consumer(ctx.bootstraps, group, "explicit",
|
|
2219
|
+
NULL, NULL);
|
|
2220
|
+
test_share_consumer_subscribe_multi(rkshare, 1, topic);
|
|
2221
|
+
share_topic_err_prime_assignment(rkshare);
|
|
2222
|
+
|
|
2223
|
+
rd_kafka_mock_topic_set_error(ctx.mcluster, topic, inject_err);
|
|
2224
|
+
share_topic_err_force_metadata(rkshare);
|
|
2225
|
+
|
|
2226
|
+
TEST_ASSERT(share_topic_err_wait_for_err(rkshare, expect_err, 30),
|
|
2227
|
+
"Expected consume_batch to surface %s within 30 attempts",
|
|
2228
|
+
rd_kafka_err2name(expect_err));
|
|
2229
|
+
|
|
2230
|
+
test_share_consumer_close(rkshare);
|
|
2231
|
+
test_share_destroy(rkshare);
|
|
2232
|
+
test_ctx_destroy(&ctx);
|
|
2233
|
+
|
|
2234
|
+
SUB_TEST_PASS();
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
|
|
2238
|
+
static void test_share_consumer_surfaces_topic_exception(void) {
|
|
2239
|
+
do_test_share_topic_err_surfaces("surfaces-topic-exception",
|
|
2240
|
+
RD_KAFKA_RESP_ERR_TOPIC_EXCEPTION,
|
|
2241
|
+
RD_KAFKA_RESP_ERR_TOPIC_EXCEPTION);
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
|
|
2245
|
+
static void test_share_consumer_surfaces_topic_authorization_failed(void) {
|
|
2246
|
+
do_test_share_topic_err_surfaces(
|
|
2247
|
+
"surfaces-topic-auth-failed",
|
|
2248
|
+
RD_KAFKA_RESP_ERR_TOPIC_AUTHORIZATION_FAILED,
|
|
2249
|
+
RD_KAFKA_RESP_ERR_TOPIC_AUTHORIZATION_FAILED);
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
|
|
2253
|
+
/* A topic with N partitions failing in a single metadata cycle must
|
|
2254
|
+
* surface exactly one op for that topic, not one per partition.
|
|
2255
|
+
*
|
|
2256
|
+
* share_toppar_enq_error keys its accumulator on topic_id and
|
|
2257
|
+
* dedups-on-add, so the per-rktp calls from partition_cnt_update and
|
|
2258
|
+
* propagate_notexists (2N total) collapse to a single entry in
|
|
2259
|
+
* rkcg_errored_topics — and hence a single rd_kafka_consumer_err. */
|
|
2260
|
+
static void test_share_consumer_multi_partition_single_op_per_cycle(void) {
|
|
2261
|
+
test_ctx_t ctx;
|
|
2262
|
+
rd_kafka_share_t *rkshare;
|
|
2263
|
+
const char *topic = "0182-multipart-single-op";
|
|
2264
|
+
const char *group = "sg-0182-multipart-single-op";
|
|
2265
|
+
const int partition_cnt = 5;
|
|
2266
|
+
rd_kafka_messages_t *batch = NULL;
|
|
2267
|
+
rd_kafka_error_t *error;
|
|
2268
|
+
size_t rcvd, j;
|
|
2269
|
+
int err_count = 0;
|
|
2270
|
+
int attempts;
|
|
2271
|
+
|
|
2272
|
+
SUB_TEST();
|
|
2273
|
+
|
|
2274
|
+
ctx = test_ctx_new();
|
|
2275
|
+
TEST_ASSERT(rd_kafka_mock_topic_create(ctx.mcluster, topic,
|
|
2276
|
+
partition_cnt,
|
|
2277
|
+
1) == RD_KAFKA_RESP_ERR_NO_ERROR,
|
|
2278
|
+
"create topic with %d partitions", partition_cnt);
|
|
2279
|
+
mock_produce(ctx.producer, topic, partition_cnt * 3);
|
|
2280
|
+
|
|
2281
|
+
rkshare = create_mock_share_consumer(ctx.bootstraps, group, "explicit",
|
|
2282
|
+
NULL, NULL);
|
|
2283
|
+
test_share_consumer_subscribe_multi(rkshare, 1, topic);
|
|
2284
|
+
share_topic_err_prime_assignment(rkshare);
|
|
2285
|
+
|
|
2286
|
+
/* Inject AUTH_FAILED on the topic; partition_cnt_update +
|
|
2287
|
+
* propagate_notexists will each fire enq_error per rktp. */
|
|
2288
|
+
rd_kafka_mock_topic_set_error(
|
|
2289
|
+
ctx.mcluster, topic, RD_KAFKA_RESP_ERR_TOPIC_AUTHORIZATION_FAILED);
|
|
2290
|
+
share_topic_err_force_metadata(rkshare);
|
|
2291
|
+
|
|
2292
|
+
/* Drain a short window immediately after the synchronous
|
|
2293
|
+
* force_metadata. By the time it returns, propagate has emitted
|
|
2294
|
+
* the (single) op for this cycle. The window is intentionally
|
|
2295
|
+
* short to keep it within one heartbeat interval and count just
|
|
2296
|
+
* what this cycle produced. */
|
|
2297
|
+
for (attempts = 0; attempts < 3; attempts++) {
|
|
2298
|
+
error = rd_kafka_share_poll(rkshare, 200, &batch);
|
|
2299
|
+
if (error) {
|
|
2300
|
+
rd_kafka_resp_err_t code = rd_kafka_error_code(error);
|
|
2301
|
+
const char *errstr = rd_kafka_error_string(error);
|
|
2302
|
+
if (code ==
|
|
2303
|
+
RD_KAFKA_RESP_ERR_TOPIC_AUTHORIZATION_FAILED &&
|
|
2304
|
+
errstr && strstr(errstr, topic))
|
|
2305
|
+
err_count++;
|
|
2306
|
+
rd_kafka_error_destroy(error);
|
|
2307
|
+
rd_kafka_messages_destroy(batch);
|
|
2308
|
+
batch = NULL;
|
|
2309
|
+
continue;
|
|
2310
|
+
}
|
|
2311
|
+
rcvd = rd_kafka_messages_count(batch);
|
|
2312
|
+
for (j = 0; j < rcvd; j++) {
|
|
2313
|
+
rd_kafka_message_t *rkm =
|
|
2314
|
+
rd_kafka_messages_get(batch, j);
|
|
2315
|
+
if (!rkm->err)
|
|
2316
|
+
rd_kafka_share_acknowledge(rkshare, rkm);
|
|
2317
|
+
}
|
|
2318
|
+
rd_kafka_messages_destroy(batch);
|
|
2319
|
+
batch = NULL;
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
TEST_ASSERT(err_count == 1,
|
|
2323
|
+
"expected exactly 1 AUTH_FAILED op for a %d-partition "
|
|
2324
|
+
"topic in a single metadata cycle, got %d",
|
|
2325
|
+
partition_cnt, err_count);
|
|
2326
|
+
|
|
2327
|
+
test_share_consumer_close(rkshare);
|
|
2328
|
+
test_share_destroy(rkshare);
|
|
2329
|
+
test_ctx_destroy(&ctx);
|
|
2330
|
+
|
|
2331
|
+
SUB_TEST_PASS();
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
|
|
2335
|
+
/* The same topic failing with a different error code must surface a
|
|
2336
|
+
* fresh op for the new code rather than being deduped against the
|
|
2337
|
+
* previous one. */
|
|
2338
|
+
static void test_share_consumer_re_emits_when_err_code_changes(void) {
|
|
2339
|
+
test_ctx_t ctx;
|
|
2340
|
+
rd_kafka_share_t *rkshare;
|
|
2341
|
+
const char *topic = "0182-err-code-change";
|
|
2342
|
+
const char *group = "sg-0182-err-code-change";
|
|
2343
|
+
|
|
2344
|
+
SUB_TEST();
|
|
2345
|
+
|
|
2346
|
+
ctx = test_ctx_new();
|
|
2347
|
+
TEST_ASSERT(rd_kafka_mock_topic_create(ctx.mcluster, topic, 1, 1) ==
|
|
2348
|
+
RD_KAFKA_RESP_ERR_NO_ERROR,
|
|
2349
|
+
"create topic");
|
|
2350
|
+
mock_produce(ctx.producer, topic, 5);
|
|
2351
|
+
|
|
2352
|
+
rkshare = create_mock_share_consumer(ctx.bootstraps, group, "explicit",
|
|
2353
|
+
NULL, NULL);
|
|
2354
|
+
test_share_consumer_subscribe_multi(rkshare, 1, topic);
|
|
2355
|
+
share_topic_err_prime_assignment(rkshare);
|
|
2356
|
+
|
|
2357
|
+
/* First err: AUTH_FAILED. */
|
|
2358
|
+
rd_kafka_mock_topic_set_error(
|
|
2359
|
+
ctx.mcluster, topic, RD_KAFKA_RESP_ERR_TOPIC_AUTHORIZATION_FAILED);
|
|
2360
|
+
share_topic_err_force_metadata(rkshare);
|
|
2361
|
+
TEST_ASSERT(
|
|
2362
|
+
share_topic_err_wait_for_err(
|
|
2363
|
+
rkshare, RD_KAFKA_RESP_ERR_TOPIC_AUTHORIZATION_FAILED, 30),
|
|
2364
|
+
"First AUTH_FAILED must surface");
|
|
2365
|
+
|
|
2366
|
+
/* Second err for the same topic: TOPIC_EXCEPTION (different
|
|
2367
|
+
* code) — must surface, dedup must NOT swallow it. */
|
|
2368
|
+
rd_kafka_mock_topic_set_error(ctx.mcluster, topic,
|
|
2369
|
+
RD_KAFKA_RESP_ERR_TOPIC_EXCEPTION);
|
|
2370
|
+
share_topic_err_force_metadata(rkshare);
|
|
2371
|
+
TEST_ASSERT(share_topic_err_wait_for_err(
|
|
2372
|
+
rkshare, RD_KAFKA_RESP_ERR_TOPIC_EXCEPTION, 30),
|
|
2373
|
+
"TOPIC_EXCEPTION must surface after AUTH_FAILED "
|
|
2374
|
+
"(err code change must bypass dedup)");
|
|
2375
|
+
|
|
2376
|
+
test_share_consumer_close(rkshare);
|
|
2377
|
+
test_share_destroy(rkshare);
|
|
2378
|
+
test_ctx_destroy(&ctx);
|
|
2379
|
+
|
|
2380
|
+
SUB_TEST_PASS();
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
|
|
2384
|
+
/* A topic that surfaces AUTH_FAILED, then recovers, then fails again
|
|
2385
|
+
* with the same code must surface the error a second time. */
|
|
2386
|
+
static void test_share_consumer_re_surfaces_after_recovery(void) {
|
|
2387
|
+
test_ctx_t ctx;
|
|
2388
|
+
rd_kafka_share_t *rkshare;
|
|
2389
|
+
const char *topic = "0182-re-surface-after-recovery";
|
|
2390
|
+
const char *group = "sg-0182-re-surface-after-recovery";
|
|
2391
|
+
|
|
2392
|
+
SUB_TEST();
|
|
2393
|
+
|
|
2394
|
+
ctx = test_ctx_new();
|
|
2395
|
+
TEST_ASSERT(rd_kafka_mock_topic_create(ctx.mcluster, topic, 1, 1) ==
|
|
2396
|
+
RD_KAFKA_RESP_ERR_NO_ERROR,
|
|
2397
|
+
"create topic");
|
|
2398
|
+
mock_produce(ctx.producer, topic, 5);
|
|
2399
|
+
|
|
2400
|
+
rkshare = create_mock_share_consumer(ctx.bootstraps, group, "explicit",
|
|
2401
|
+
NULL, NULL);
|
|
2402
|
+
test_share_consumer_subscribe_multi(rkshare, 1, topic);
|
|
2403
|
+
share_topic_err_prime_assignment(rkshare);
|
|
2404
|
+
|
|
2405
|
+
/* Phase 1: fail — first surface. */
|
|
2406
|
+
rd_kafka_mock_topic_set_error(
|
|
2407
|
+
ctx.mcluster, topic, RD_KAFKA_RESP_ERR_TOPIC_AUTHORIZATION_FAILED);
|
|
2408
|
+
share_topic_err_force_metadata(rkshare);
|
|
2409
|
+
TEST_ASSERT(
|
|
2410
|
+
share_topic_err_wait_for_err(
|
|
2411
|
+
rkshare, RD_KAFKA_RESP_ERR_TOPIC_AUTHORIZATION_FAILED, 30),
|
|
2412
|
+
"First AUTH_FAILED must surface");
|
|
2413
|
+
|
|
2414
|
+
/* Phase 2: recover — no error must reach the app. */
|
|
2415
|
+
rd_kafka_mock_topic_set_error(ctx.mcluster, topic,
|
|
2416
|
+
RD_KAFKA_RESP_ERR_NO_ERROR);
|
|
2417
|
+
share_topic_err_force_metadata(rkshare);
|
|
2418
|
+
share_topic_err_assert_no_err(rkshare, 5,
|
|
2419
|
+
"no error must surface while recovered");
|
|
2420
|
+
|
|
2421
|
+
/* Phase 3: fail again with the same code — must surface a
|
|
2422
|
+
* second time. */
|
|
2423
|
+
rd_kafka_mock_topic_set_error(
|
|
2424
|
+
ctx.mcluster, topic, RD_KAFKA_RESP_ERR_TOPIC_AUTHORIZATION_FAILED);
|
|
2425
|
+
share_topic_err_force_metadata(rkshare);
|
|
2426
|
+
TEST_ASSERT(
|
|
2427
|
+
share_topic_err_wait_for_err(
|
|
2428
|
+
rkshare, RD_KAFKA_RESP_ERR_TOPIC_AUTHORIZATION_FAILED, 30),
|
|
2429
|
+
"AUTH_FAILED must surface a second time after recovery");
|
|
2430
|
+
|
|
2431
|
+
test_share_consumer_close(rkshare);
|
|
2432
|
+
test_share_destroy(rkshare);
|
|
2433
|
+
test_ctx_destroy(&ctx);
|
|
2434
|
+
|
|
2435
|
+
SUB_TEST_PASS();
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
|
|
2439
|
+
/* A topic that surfaces TOPIC_EXCEPTION, then recovers, then fails
|
|
2440
|
+
* again with the same code must surface the error a second time. */
|
|
2441
|
+
static void
|
|
2442
|
+
test_share_consumer_re_surfaces_after_recovery_topic_exception(void) {
|
|
2443
|
+
test_ctx_t ctx;
|
|
2444
|
+
rd_kafka_share_t *rkshare;
|
|
2445
|
+
const char *topic = "0182-re-surface-topic-exception";
|
|
2446
|
+
const char *group = "sg-0182-re-surface-topic-exception";
|
|
2447
|
+
|
|
2448
|
+
SUB_TEST();
|
|
2449
|
+
|
|
2450
|
+
ctx = test_ctx_new();
|
|
2451
|
+
TEST_ASSERT(rd_kafka_mock_topic_create(ctx.mcluster, topic, 1, 1) ==
|
|
2452
|
+
RD_KAFKA_RESP_ERR_NO_ERROR,
|
|
2453
|
+
"create topic");
|
|
2454
|
+
mock_produce(ctx.producer, topic, 5);
|
|
2455
|
+
|
|
2456
|
+
rkshare = create_mock_share_consumer(ctx.bootstraps, group, "explicit",
|
|
2457
|
+
NULL, NULL);
|
|
2458
|
+
test_share_consumer_subscribe_multi(rkshare, 1, topic);
|
|
2459
|
+
share_topic_err_prime_assignment(rkshare);
|
|
2460
|
+
|
|
2461
|
+
/* Phase 1: fail. */
|
|
2462
|
+
rd_kafka_mock_topic_set_error(ctx.mcluster, topic,
|
|
2463
|
+
RD_KAFKA_RESP_ERR_TOPIC_EXCEPTION);
|
|
2464
|
+
share_topic_err_force_metadata(rkshare);
|
|
2465
|
+
TEST_ASSERT(share_topic_err_wait_for_err(
|
|
2466
|
+
rkshare, RD_KAFKA_RESP_ERR_TOPIC_EXCEPTION, 30),
|
|
2467
|
+
"first TOPIC_EXCEPTION must surface");
|
|
2468
|
+
|
|
2469
|
+
/* Phase 2: recover. */
|
|
2470
|
+
rd_kafka_mock_topic_set_error(ctx.mcluster, topic,
|
|
2471
|
+
RD_KAFKA_RESP_ERR_NO_ERROR);
|
|
2472
|
+
share_topic_err_force_metadata(rkshare);
|
|
2473
|
+
share_topic_err_assert_no_err(
|
|
2474
|
+
rkshare, 5, "no error must surface while topic is recovered");
|
|
2475
|
+
|
|
2476
|
+
/* Phase 3: re-fail with the same code — must surface again. */
|
|
2477
|
+
rd_kafka_mock_topic_set_error(ctx.mcluster, topic,
|
|
2478
|
+
RD_KAFKA_RESP_ERR_TOPIC_EXCEPTION);
|
|
2479
|
+
share_topic_err_force_metadata(rkshare);
|
|
2480
|
+
TEST_ASSERT(share_topic_err_wait_for_err(
|
|
2481
|
+
rkshare, RD_KAFKA_RESP_ERR_TOPIC_EXCEPTION, 30),
|
|
2482
|
+
"TOPIC_EXCEPTION must surface a second time after "
|
|
2483
|
+
"recovery");
|
|
2484
|
+
|
|
2485
|
+
test_share_consumer_close(rkshare);
|
|
2486
|
+
test_share_destroy(rkshare);
|
|
2487
|
+
test_ctx_destroy(&ctx);
|
|
2488
|
+
|
|
2489
|
+
SUB_TEST_PASS();
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
|
|
2493
|
+
/* A topic surfaces TOPIC_EXCEPTION, gets unsubscribed (no surface),
|
|
2494
|
+
* and is then re-subscribed while still failing. The error must
|
|
2495
|
+
* surface again on re-subscribe — not be permanently suppressed. */
|
|
2496
|
+
static void test_share_consumer_resubscribe_re_emits_persistent_failure(void) {
|
|
2497
|
+
test_ctx_t ctx;
|
|
2498
|
+
rd_kafka_share_t *rkshare;
|
|
2499
|
+
const char *topic = "0182-resubscribe-re-emit";
|
|
2500
|
+
const char *group = "sg-0182-resubscribe-re-emit";
|
|
2501
|
+
|
|
2502
|
+
SUB_TEST();
|
|
2503
|
+
|
|
2504
|
+
ctx = test_ctx_new();
|
|
2505
|
+
TEST_ASSERT(rd_kafka_mock_topic_create(ctx.mcluster, topic, 1, 1) ==
|
|
2506
|
+
RD_KAFKA_RESP_ERR_NO_ERROR,
|
|
2507
|
+
"create topic");
|
|
2508
|
+
mock_produce(ctx.producer, topic, 5);
|
|
2509
|
+
|
|
2510
|
+
rkshare = create_mock_share_consumer(ctx.bootstraps, group, "explicit",
|
|
2511
|
+
NULL, NULL);
|
|
2512
|
+
test_share_consumer_subscribe_multi(rkshare, 1, topic);
|
|
2513
|
+
share_topic_err_prime_assignment(rkshare);
|
|
2514
|
+
|
|
2515
|
+
/* Phase 1: subscribe + fail + surface. */
|
|
2516
|
+
rd_kafka_mock_topic_set_error(ctx.mcluster, topic,
|
|
2517
|
+
RD_KAFKA_RESP_ERR_TOPIC_EXCEPTION);
|
|
2518
|
+
share_topic_err_force_metadata(rkshare);
|
|
2519
|
+
TEST_ASSERT(share_topic_err_wait_for_err(
|
|
2520
|
+
rkshare, RD_KAFKA_RESP_ERR_TOPIC_EXCEPTION, 30),
|
|
2521
|
+
"first TOPIC_EXCEPTION must surface");
|
|
2522
|
+
|
|
2523
|
+
/* Phase 2: unsubscribe. */
|
|
2524
|
+
TEST_ASSERT(rd_kafka_share_unsubscribe(rkshare) ==
|
|
2525
|
+
RD_KAFKA_RESP_ERR_NO_ERROR,
|
|
2526
|
+
"unsubscribe");
|
|
2527
|
+
|
|
2528
|
+
/* Phase 3: re-subscribe to the same still-failing topic; the
|
|
2529
|
+
* error must surface again. Re-force metadata across the wait
|
|
2530
|
+
* loop so the request happens after the share-assignment
|
|
2531
|
+
* heartbeat re-populates the partition list. */
|
|
2532
|
+
test_share_consumer_subscribe_multi(rkshare, 1, topic);
|
|
2533
|
+
rd_bool_t saw_err = rd_false;
|
|
2534
|
+
int outer;
|
|
2535
|
+
for (outer = 0; outer < 10 && !saw_err; outer++) {
|
|
2536
|
+
share_topic_err_force_metadata(rkshare);
|
|
2537
|
+
if (share_topic_err_wait_for_err(
|
|
2538
|
+
rkshare, RD_KAFKA_RESP_ERR_TOPIC_EXCEPTION, 3))
|
|
2539
|
+
saw_err = rd_true;
|
|
2540
|
+
}
|
|
2541
|
+
TEST_ASSERT(saw_err,
|
|
2542
|
+
"TOPIC_EXCEPTION must surface again after "
|
|
2543
|
+
"re-subscribing to a still-failing topic");
|
|
2544
|
+
|
|
2545
|
+
test_share_consumer_close(rkshare);
|
|
2546
|
+
test_share_destroy(rkshare);
|
|
2547
|
+
test_ctx_destroy(&ctx);
|
|
2548
|
+
|
|
2549
|
+
SUB_TEST_PASS();
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
|
|
2553
|
+
/* UNKNOWN_TOPIC_OR_PART is debug-logged only and must not reach the
|
|
2554
|
+
* app via consume_batch. */
|
|
2555
|
+
static void test_share_consumer_does_not_surface_unknown_topic_or_part(void) {
|
|
2556
|
+
test_ctx_t ctx;
|
|
2557
|
+
rd_kafka_conf_t *conf;
|
|
2558
|
+
rd_kafka_share_t *rkshare;
|
|
2559
|
+
const char *topic = "0182-no-surface-unknown-tp";
|
|
2560
|
+
const char *group = "sg-0182-no-surface-unknown-tp";
|
|
2561
|
+
|
|
2562
|
+
SUB_TEST();
|
|
2563
|
+
|
|
2564
|
+
ctx = test_ctx_new();
|
|
2565
|
+
TEST_ASSERT(rd_kafka_mock_topic_create(ctx.mcluster, topic, 1, 1) ==
|
|
2566
|
+
RD_KAFKA_RESP_ERR_NO_ERROR,
|
|
2567
|
+
"create topic");
|
|
2568
|
+
mock_produce(ctx.producer, topic, 5);
|
|
2569
|
+
|
|
2570
|
+
/* Custom consumer with the metadata propagation defer window
|
|
2571
|
+
* disabled so the no-surface path is exercised on the first
|
|
2572
|
+
* metadata refresh rather than 30 s later. */
|
|
2573
|
+
test_conf_init(&conf, NULL, 0);
|
|
2574
|
+
test_conf_set(conf, "bootstrap.servers", ctx.bootstraps);
|
|
2575
|
+
test_conf_set(conf, "group.id", group);
|
|
2576
|
+
test_conf_set(conf, "share.acknowledgement.mode", "explicit");
|
|
2577
|
+
test_conf_set(conf, "topic.metadata.propagation.max.ms", "0");
|
|
2578
|
+
rkshare = rd_kafka_share_consumer_new(conf, NULL, 0);
|
|
2579
|
+
TEST_ASSERT(rkshare != NULL, "Failed to create share consumer");
|
|
2580
|
+
|
|
2581
|
+
test_share_consumer_subscribe_multi(rkshare, 1, topic);
|
|
2582
|
+
share_topic_err_prime_assignment(rkshare);
|
|
2583
|
+
|
|
2584
|
+
rd_kafka_mock_topic_set_error(ctx.mcluster, topic,
|
|
2585
|
+
RD_KAFKA_RESP_ERR_UNKNOWN_TOPIC_OR_PART);
|
|
2586
|
+
share_topic_err_force_metadata(rkshare);
|
|
2587
|
+
|
|
2588
|
+
share_topic_err_assert_no_err(
|
|
2589
|
+
rkshare, 20,
|
|
2590
|
+
"UNKNOWN_TOPIC_OR_PART must be logged only, not surfaced");
|
|
2591
|
+
|
|
2592
|
+
test_share_consumer_close(rkshare);
|
|
2593
|
+
test_share_destroy(rkshare);
|
|
2594
|
+
test_ctx_destroy(&ctx);
|
|
2595
|
+
|
|
2596
|
+
SUB_TEST_PASS();
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
|
|
2600
|
+
/* ===================================================================
|
|
2601
|
+
* Log callback shared by the two select_broker STATE_UP guard tests
|
|
2602
|
+
* below. Counts the broker-thread short-circuit log emitted at
|
|
2603
|
+
* src/rdkafka_broker.c when a SHARE_FETCH op is served against a
|
|
2604
|
+
* broker whose rkb_state is not STATE_UP.
|
|
2605
|
+
* =================================================================== */
|
|
2606
|
+
static void no_bounce_loop_log_cb(const rd_kafka_t *rk,
|
|
2607
|
+
int level,
|
|
2608
|
+
const char *fac,
|
|
2609
|
+
const char *buf) {
|
|
2610
|
+
rd_atomic32_t *cnt = rd_kafka_opaque(rk);
|
|
2611
|
+
if (cnt && !strcmp(fac, "SHAREFETCH") && strstr(buf, "broker not up"))
|
|
2612
|
+
rd_atomic32_add(cnt, 1);
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
/* ===================================================================
|
|
2616
|
+
* do_test_no_bounce_loop_on_down_broker
|
|
2617
|
+
*
|
|
2618
|
+
* Steady-state DOWN: the consumer is idle when the broker is taken
|
|
2619
|
+
* down, sleeps long enough for the broker thread to settle rkb_state
|
|
2620
|
+
* to !UP, then drives consume_batch for ~1s. Every FANOUT must skip
|
|
2621
|
+
* the DOWN leader via the select_broker STATE_UP guard, so no
|
|
2622
|
+
* "broker not up" log line should fire.
|
|
2623
|
+
*
|
|
2624
|
+
* Wire never sees a ShareFetch on the DOWN broker (broker thread
|
|
2625
|
+
* rejects the internal op before any RPC is built), so we assert on
|
|
2626
|
+
* the count of the broker-thread short-circuit debug log instead of
|
|
2627
|
+
* on mock_get_requests.
|
|
2628
|
+
* =================================================================== */
|
|
2629
|
+
static void do_test_no_bounce_loop_on_down_broker(void) {
|
|
2630
|
+
test_ctx_t ctx;
|
|
2631
|
+
rd_kafka_share_t *rkshare;
|
|
2632
|
+
rd_kafka_conf_t *conf;
|
|
2633
|
+
rd_atomic32_t broker_not_up_cnt;
|
|
2634
|
+
rd_kafka_error_t *error;
|
|
2635
|
+
rd_kafka_messages_t *batch = NULL;
|
|
2636
|
+
const char *topic = "0182-no_bounce_loop";
|
|
2637
|
+
const char *group = "sg-0182-no-bounce-loop";
|
|
2638
|
+
const int msgcnt_phase1 = 5;
|
|
2639
|
+
const int msgcnt_phase2 = 5;
|
|
2640
|
+
int acked, cnt;
|
|
2641
|
+
size_t rcvd;
|
|
2642
|
+
size_t share_fetch_cnt_before_drain;
|
|
2643
|
+
size_t share_fetch_cnt_after_drain;
|
|
2644
|
+
rd_ts_t end_ts;
|
|
2645
|
+
|
|
2646
|
+
SUB_TEST_QUICK();
|
|
2647
|
+
|
|
2648
|
+
/* Taking the only broker down legitimately raises
|
|
2649
|
+
* __ALL_BROKERS_DOWN and __TRANSPORT on producer + consumer
|
|
2650
|
+
* error callbacks. None of these should fail the test. */
|
|
2651
|
+
test_curr->is_fatal_cb = test_error_is_not_fatal_cb;
|
|
2652
|
+
|
|
2653
|
+
ctx = test_ctx_new();
|
|
2654
|
+
rd_kafka_mock_start_request_tracking(ctx.mcluster);
|
|
2655
|
+
|
|
2656
|
+
TEST_ASSERT(rd_kafka_mock_topic_create(ctx.mcluster, topic, 1, 1) ==
|
|
2657
|
+
RD_KAFKA_RESP_ERR_NO_ERROR,
|
|
2658
|
+
"create topic");
|
|
2659
|
+
|
|
2660
|
+
mock_produce(ctx.producer, topic, msgcnt_phase1);
|
|
2661
|
+
|
|
2662
|
+
rd_atomic32_init(&broker_not_up_cnt, 0);
|
|
2663
|
+
test_conf_init(&conf, NULL, 0);
|
|
2664
|
+
test_conf_set(conf, "bootstrap.servers", ctx.bootstraps);
|
|
2665
|
+
test_conf_set(conf, "group.id", group);
|
|
2666
|
+
test_conf_set(conf, "share.acknowledgement.mode", "explicit");
|
|
2667
|
+
test_conf_set(conf, "debug", "broker");
|
|
2668
|
+
/* Cap reconnect backoff so the consumer recovers quickly after
|
|
2669
|
+
* set_up. The default max (10s) extends past the recovery
|
|
2670
|
+
* sleep below, causing the post-recovery ShareFetch not to
|
|
2671
|
+
* land in time. */
|
|
2672
|
+
test_conf_set(conf, "reconnect.backoff.ms", "100");
|
|
2673
|
+
test_conf_set(conf, "reconnect.backoff.max.ms", "500");
|
|
2674
|
+
rd_kafka_conf_set_log_cb(conf, no_bounce_loop_log_cb);
|
|
2675
|
+
rd_kafka_conf_set_opaque(conf, &broker_not_up_cnt);
|
|
2676
|
+
|
|
2677
|
+
rkshare = rd_kafka_share_consumer_new(conf, NULL, 0);
|
|
2678
|
+
TEST_ASSERT(rkshare != NULL, "Failed to create share consumer");
|
|
2679
|
+
|
|
2680
|
+
test_share_consumer_subscribe_multi(rkshare, 1, topic);
|
|
2681
|
+
|
|
2682
|
+
acked = consume_and_ack_all(rkshare, msgcnt_phase1);
|
|
2683
|
+
TEST_ASSERT(acked == msgcnt_phase1, "phase1: expected %d acked, got %d",
|
|
2684
|
+
msgcnt_phase1, acked);
|
|
2685
|
+
|
|
2686
|
+
/* Flush any acks still cached in rkb_share_async_ack_details
|
|
2687
|
+
* before taking the broker down. Otherwise the next FANOUT
|
|
2688
|
+
* after set_down would dispatch an ack-only op to the DOWN
|
|
2689
|
+
* broker (the FANOUT iteration must always deliver cached
|
|
2690
|
+
* acks so the broker thread can surface the error), and the
|
|
2691
|
+
* broker thread would legitimately log "broker not up" once.
|
|
2692
|
+
* That log is unrelated to the select_broker guard. */
|
|
2693
|
+
error = rd_kafka_share_commit_async(rkshare);
|
|
2694
|
+
TEST_ASSERT(!error, "commit_async error: %s",
|
|
2695
|
+
error ? rd_kafka_error_string(error) : "NULL");
|
|
2696
|
+
|
|
2697
|
+
/* Phase-2 records sit in the partition log; the post-recovery
|
|
2698
|
+
* consume below drains them. */
|
|
2699
|
+
mock_produce(ctx.producer, topic, msgcnt_phase2);
|
|
2700
|
+
|
|
2701
|
+
TEST_SAY("Taking broker 1 down\n");
|
|
2702
|
+
rd_kafka_mock_broker_set_down(ctx.mcluster, 1);
|
|
2703
|
+
|
|
2704
|
+
/* Settle: let the client's broker thread detect TCP close and
|
|
2705
|
+
* transition rkb_state to !UP before we start counting. After
|
|
2706
|
+
* this point every select_broker reads DOWN — no race window. */
|
|
2707
|
+
rd_sleep(1);
|
|
2708
|
+
rd_atomic32_set(&broker_not_up_cnt, 0);
|
|
2709
|
+
|
|
2710
|
+
end_ts = test_clock() + 1000 * 1000;
|
|
2711
|
+
while (test_clock() < end_ts) {
|
|
2712
|
+
error = rd_kafka_share_poll(rkshare, 100, &batch);
|
|
2713
|
+
TEST_ASSERT(!error,
|
|
2714
|
+
"unexpected error from share_poll while "
|
|
2715
|
+
"broker is down: %s",
|
|
2716
|
+
error ? rd_kafka_error_string(error) : "NULL");
|
|
2717
|
+
rcvd = rd_kafka_messages_count(batch);
|
|
2718
|
+
TEST_ASSERT(rcvd == 0,
|
|
2719
|
+
"expected 0 records while broker is down, "
|
|
2720
|
+
"got %zu",
|
|
2721
|
+
rcvd);
|
|
2722
|
+
rd_kafka_messages_destroy(batch);
|
|
2723
|
+
batch = NULL;
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
cnt = rd_atomic32_get(&broker_not_up_cnt);
|
|
2727
|
+
TEST_SAY("\"broker not up\" log count: %d (expected 0)\n", cnt);
|
|
2728
|
+
TEST_ASSERT(cnt == 0,
|
|
2729
|
+
"select_broker should skip the DOWN leader on every "
|
|
2730
|
+
"FANOUT once rkb_state has settled; got %d "
|
|
2731
|
+
"\"broker not up\" log lines",
|
|
2732
|
+
cnt);
|
|
2733
|
+
|
|
2734
|
+
TEST_SAY("Bringing broker 1 back up\n");
|
|
2735
|
+
rd_kafka_mock_broker_set_up(ctx.mcluster, 1);
|
|
2736
|
+
|
|
2737
|
+
/* The main-thread retrigger keeps calling select_broker. As
|
|
2738
|
+
* soon as broker 1 reaches STATE_UP the next select_broker
|
|
2739
|
+
* returns it and a ShareFetch fires automatically — records
|
|
2740
|
+
* land on the consumer queue without any consume_batch call
|
|
2741
|
+
* driving the fetch. */
|
|
2742
|
+
rd_sleep(3);
|
|
2743
|
+
|
|
2744
|
+
share_fetch_cnt_before_drain = test_mock_get_matching_request_cnt(
|
|
2745
|
+
ctx.mcluster, is_share_fetch_request, NULL);
|
|
2746
|
+
TEST_SAY("ShareFetch count after recovery (pre-drain): %" PRIusz "\n",
|
|
2747
|
+
share_fetch_cnt_before_drain);
|
|
2748
|
+
TEST_ASSERT(share_fetch_cnt_before_drain >= 1,
|
|
2749
|
+
"expected >= 1 ShareFetch via internal retry after "
|
|
2750
|
+
"set_up, got %" PRIusz,
|
|
2751
|
+
share_fetch_cnt_before_drain);
|
|
2752
|
+
|
|
2753
|
+
/* Drain the pre-fetched records. They're already on the
|
|
2754
|
+
* consumer queue, so share_poll returns them directly
|
|
2755
|
+
* without enqueueing a FANOUT and no new ShareFetch fires. */
|
|
2756
|
+
error = rd_kafka_share_poll(rkshare, 100, &batch);
|
|
2757
|
+
rcvd = rd_kafka_messages_count(batch);
|
|
2758
|
+
share_fetch_cnt_after_drain = test_mock_get_matching_request_cnt(
|
|
2759
|
+
ctx.mcluster, is_share_fetch_request, NULL);
|
|
2760
|
+
/* Destroy the batch before asserting so a failed assert can't
|
|
2761
|
+
* leak it (TEST_FAIL longjmps past any later destroy). */
|
|
2762
|
+
rd_kafka_messages_destroy(batch);
|
|
2763
|
+
batch = NULL;
|
|
2764
|
+
|
|
2765
|
+
TEST_ASSERT(!error, "post-recovery share_poll error: %s",
|
|
2766
|
+
error ? rd_kafka_error_string(error) : "NULL");
|
|
2767
|
+
TEST_ASSERT(rcvd == (size_t)msgcnt_phase2,
|
|
2768
|
+
"expected %d records from queue, got %" PRIusz,
|
|
2769
|
+
msgcnt_phase2, rcvd);
|
|
2770
|
+
TEST_ASSERT(share_fetch_cnt_after_drain == share_fetch_cnt_before_drain,
|
|
2771
|
+
"share_poll should drain the queue without firing "
|
|
2772
|
+
"a new ShareFetch; pre=%" PRIusz " post=%" PRIusz,
|
|
2773
|
+
share_fetch_cnt_before_drain, share_fetch_cnt_after_drain);
|
|
2774
|
+
|
|
2775
|
+
rd_kafka_mock_clear_requests(ctx.mcluster);
|
|
2776
|
+
|
|
2777
|
+
test_share_consumer_close(rkshare);
|
|
2778
|
+
test_share_destroy(rkshare);
|
|
2779
|
+
test_ctx_destroy(&ctx);
|
|
2780
|
+
|
|
2781
|
+
test_curr->is_fatal_cb = NULL;
|
|
2782
|
+
|
|
2783
|
+
SUB_TEST_PASS();
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2786
|
+
/* ===================================================================
|
|
2787
|
+
* do_test_one_log_on_broker_down_during_active_empty_poll
|
|
2788
|
+
*
|
|
2789
|
+
* Race-window companion to do_test_no_bounce_loop_on_down_broker.
|
|
2790
|
+
*
|
|
2791
|
+
* With an empty topic the consumer's FANOUT->ShareFetch->Step 6
|
|
2792
|
+
* empty-poll loop fires continuously. We rapidly flip the broker
|
|
2793
|
+
* up/down across N cycles while that loop is hot. Each set_down
|
|
2794
|
+
* has a narrow window between the mock TCP close and the client
|
|
2795
|
+
* broker thread updating rkb_state, in which select_broker can
|
|
2796
|
+
* read stale STATE_UP and enqueue one more op — the broker thread
|
|
2797
|
+
* (rkb_state has caught up by then) logs "broker not up" at most
|
|
2798
|
+
* once and replies ERR__STATE. The next Step 6 re-select reads
|
|
2799
|
+
* DOWN and skips, closing the loop until the next set_down.
|
|
2800
|
+
*
|
|
2801
|
+
* Total log count is therefore bounded by N (one slip per
|
|
2802
|
+
* set_down). Without the select_broker guard, each set_down would
|
|
2803
|
+
* produce hundreds of logs per second for the duration of the
|
|
2804
|
+
* down window — unbounded across cycles.
|
|
2805
|
+
* =================================================================== */
|
|
2806
|
+
static void do_test_one_log_on_broker_down_during_active_empty_poll(void) {
|
|
2807
|
+
test_ctx_t ctx;
|
|
2808
|
+
rd_kafka_share_t *rkshare;
|
|
2809
|
+
rd_kafka_conf_t *conf;
|
|
2810
|
+
rd_atomic32_t broker_not_up_cnt;
|
|
2811
|
+
const char *topic = "0182-race_bounce";
|
|
2812
|
+
const char *group = "sg-0182-race-bounce";
|
|
2813
|
+
const int msgcnt_recovery = 5;
|
|
2814
|
+
const int n_cycles = 10;
|
|
2815
|
+
const int max_allowed_log_cnt = n_cycles;
|
|
2816
|
+
rd_kafka_messages_t *batch = NULL;
|
|
2817
|
+
size_t rcvd;
|
|
2818
|
+
rd_kafka_error_t *error;
|
|
2819
|
+
int i, cnt, acked;
|
|
2820
|
+
|
|
2821
|
+
SUB_TEST_QUICK();
|
|
2822
|
+
|
|
2823
|
+
/* Taking the only broker down legitimately raises
|
|
2824
|
+
* __ALL_BROKERS_DOWN and __TRANSPORT on producer + consumer
|
|
2825
|
+
* error callbacks. None of these should fail the test. */
|
|
2826
|
+
test_curr->is_fatal_cb = test_error_is_not_fatal_cb;
|
|
2827
|
+
|
|
2828
|
+
ctx = test_ctx_new();
|
|
2829
|
+
|
|
2830
|
+
TEST_ASSERT(rd_kafka_mock_topic_create(ctx.mcluster, topic, 1, 1) ==
|
|
2831
|
+
RD_KAFKA_RESP_ERR_NO_ERROR,
|
|
2832
|
+
"create topic");
|
|
2833
|
+
|
|
2834
|
+
rd_atomic32_init(&broker_not_up_cnt, 0);
|
|
2835
|
+
test_conf_init(&conf, NULL, 0);
|
|
2836
|
+
test_conf_set(conf, "bootstrap.servers", ctx.bootstraps);
|
|
2837
|
+
test_conf_set(conf, "group.id", group);
|
|
2838
|
+
test_conf_set(conf, "share.acknowledgement.mode", "explicit");
|
|
2839
|
+
test_conf_set(conf, "debug", "broker");
|
|
2840
|
+
/* Cap reconnect backoff so each set_up reconnects quickly
|
|
2841
|
+
* across the chaos cycles. */
|
|
2842
|
+
test_conf_set(conf, "reconnect.backoff.ms", "100");
|
|
2843
|
+
test_conf_set(conf, "reconnect.backoff.max.ms", "500");
|
|
2844
|
+
rd_kafka_conf_set_log_cb(conf, no_bounce_loop_log_cb);
|
|
2845
|
+
rd_kafka_conf_set_opaque(conf, &broker_not_up_cnt);
|
|
2846
|
+
|
|
2847
|
+
rkshare = rd_kafka_share_consumer_new(conf, NULL, 0);
|
|
2848
|
+
TEST_ASSERT(rkshare != NULL, "Failed to create share consumer");
|
|
2849
|
+
|
|
2850
|
+
test_share_consumer_subscribe_multi(rkshare, 1, topic);
|
|
2851
|
+
|
|
2852
|
+
/* Prime with one record so the share group has joined and the
|
|
2853
|
+
* partition is assigned by the time we take the broker down.
|
|
2854
|
+
* Without this guarantee, select_broker would return NULL
|
|
2855
|
+
* (no toppars) and the bounce loop wouldn't fire at all,
|
|
2856
|
+
* making the test pass for the wrong reason. */
|
|
2857
|
+
mock_produce(ctx.producer, topic, 1);
|
|
2858
|
+
acked = consume_and_ack_all(rkshare, 1);
|
|
2859
|
+
TEST_ASSERT(acked == 1, "prime: expected 1 acked, got %d", acked);
|
|
2860
|
+
|
|
2861
|
+
/* Flush the prime ack still cached in rkb_share_async_ack_details
|
|
2862
|
+
* before taking the broker down. Otherwise the next FANOUT
|
|
2863
|
+
* iteration would dispatch an ack-only op to the DOWN broker
|
|
2864
|
+
* and produce a "broker not up" log unrelated to the
|
|
2865
|
+
* select_broker race we're measuring. */
|
|
2866
|
+
error = rd_kafka_share_commit_async(rkshare);
|
|
2867
|
+
TEST_ASSERT(!error, "commit_async error: %s",
|
|
2868
|
+
error ? rd_kafka_error_string(error) : "NULL");
|
|
2869
|
+
|
|
2870
|
+
/* Reset to drop any noise from cgrp/connection bring-up. */
|
|
2871
|
+
rd_atomic32_set(&broker_not_up_cnt, 0);
|
|
2872
|
+
|
|
2873
|
+
/* Kickstart the empty-poll loop: one share_poll on the now
|
|
2874
|
+
* empty topic starts the FANOUT->Step 6 cycle which keeps
|
|
2875
|
+
* firing on the main thread until share_poll returns. */
|
|
2876
|
+
error = rd_kafka_share_poll(rkshare, 500, &batch);
|
|
2877
|
+
TEST_ASSERT(!error, "kickstart share_poll error: %s",
|
|
2878
|
+
error ? rd_kafka_error_string(error) : "NULL");
|
|
2879
|
+
rcvd = rd_kafka_messages_count(batch);
|
|
2880
|
+
TEST_ASSERT(rcvd == 0, "expected 0 records, got %zu", rcvd);
|
|
2881
|
+
rd_kafka_messages_destroy(batch);
|
|
2882
|
+
batch = NULL;
|
|
2883
|
+
|
|
2884
|
+
/* Chaos: rapidly flip the broker up/down across n_cycles
|
|
2885
|
+
* while the consumer's empty-poll loop is hot. Each set_down
|
|
2886
|
+
* may admit one race-window slip; total log count must stay
|
|
2887
|
+
* bounded by n_cycles. */
|
|
2888
|
+
for (i = 0; i < n_cycles; i++) {
|
|
2889
|
+
TEST_SAY("Cycle %d/%d: taking broker 1 down\n", i + 1,
|
|
2890
|
+
n_cycles);
|
|
2891
|
+
rd_kafka_mock_broker_set_down(ctx.mcluster, 1);
|
|
2892
|
+
error = rd_kafka_share_poll(rkshare, 200, &batch);
|
|
2893
|
+
TEST_ASSERT(!error, "cycle %d down: share_poll error: %s",
|
|
2894
|
+
i + 1,
|
|
2895
|
+
error ? rd_kafka_error_string(error) : "NULL");
|
|
2896
|
+
rcvd = rd_kafka_messages_count(batch);
|
|
2897
|
+
TEST_ASSERT(rcvd == 0,
|
|
2898
|
+
"cycle %d down: expected 0 records, got %zu", i + 1,
|
|
2899
|
+
rcvd);
|
|
2900
|
+
rd_kafka_messages_destroy(batch);
|
|
2901
|
+
batch = NULL;
|
|
2902
|
+
|
|
2903
|
+
TEST_SAY("Cycle %d/%d: bringing broker 1 back up\n", i + 1,
|
|
2904
|
+
n_cycles);
|
|
2905
|
+
rd_kafka_mock_broker_set_up(ctx.mcluster, 1);
|
|
2906
|
+
error = rd_kafka_share_poll(rkshare, 200, &batch);
|
|
2907
|
+
TEST_ASSERT(!error, "cycle %d up: share_poll error: %s", i + 1,
|
|
2908
|
+
error ? rd_kafka_error_string(error) : "NULL");
|
|
2909
|
+
rcvd = rd_kafka_messages_count(batch);
|
|
2910
|
+
TEST_ASSERT(rcvd == 0,
|
|
2911
|
+
"cycle %d up: expected 0 records, got %zu", i + 1,
|
|
2912
|
+
rcvd);
|
|
2913
|
+
rd_kafka_messages_destroy(batch);
|
|
2914
|
+
batch = NULL;
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
cnt = rd_atomic32_get(&broker_not_up_cnt);
|
|
2918
|
+
TEST_SAY("\"broker not up\" log count after %d cycles: %d (max %d)\n",
|
|
2919
|
+
n_cycles, cnt, max_allowed_log_cnt);
|
|
2920
|
+
TEST_ASSERT(cnt <= max_allowed_log_cnt,
|
|
2921
|
+
"race-window slips must be bounded to %d (one per "
|
|
2922
|
+
"set_down across %d cycles); got %d log lines",
|
|
2923
|
+
max_allowed_log_cnt, n_cycles, cnt);
|
|
2924
|
+
|
|
2925
|
+
/* Recovery: broker is left UP at the end of the chaos loop.
|
|
2926
|
+
* Produce records and verify the consumer drains them. */
|
|
2927
|
+
mock_produce(ctx.producer, topic, msgcnt_recovery);
|
|
2928
|
+
|
|
2929
|
+
acked = consume_and_ack_all(rkshare, msgcnt_recovery);
|
|
2930
|
+
TEST_ASSERT(acked == msgcnt_recovery,
|
|
2931
|
+
"post-recovery: expected %d acked, got %d", msgcnt_recovery,
|
|
2932
|
+
acked);
|
|
2933
|
+
|
|
2934
|
+
test_share_consumer_close(rkshare);
|
|
2935
|
+
test_share_destroy(rkshare);
|
|
2936
|
+
test_ctx_destroy(&ctx);
|
|
2937
|
+
|
|
2938
|
+
test_curr->is_fatal_cb = NULL;
|
|
2939
|
+
|
|
2940
|
+
SUB_TEST_PASS();
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2943
|
+
|
|
2944
|
+
/* ===================================================================
|
|
2945
|
+
* Log callback for do_test_fetch_stall_logged_on_no_broker. Counts
|
|
2946
|
+
* the main-thread "Fetch stalled: no eligible broker" debug log.
|
|
2947
|
+
* =================================================================== */
|
|
2948
|
+
static void fetch_stall_log_cb(const rd_kafka_t *rk,
|
|
2949
|
+
int level,
|
|
2950
|
+
const char *fac,
|
|
2951
|
+
const char *buf) {
|
|
2952
|
+
rd_atomic32_t *cnt = rd_kafka_opaque(rk);
|
|
2953
|
+
if (cnt && !strcmp(fac, "FETCHMORE") &&
|
|
2954
|
+
strstr(buf, "Fetch stalled: no eligible broker"))
|
|
2955
|
+
rd_atomic32_add(cnt, 1);
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2958
|
+
/* ===================================================================
|
|
2959
|
+
* do_test_fetch_stall_logged_on_no_broker
|
|
2960
|
+
*
|
|
2961
|
+
* With a partition assigned but the only broker down, the main-thread
|
|
2962
|
+
* re-trigger loop wants to fetch (share_fetch_more_records set, no op
|
|
2963
|
+
* in-flight) yet select_broker returns NULL. Verify it emits the
|
|
2964
|
+
* rate-limited "Fetch stalled: no eligible broker" log: at least once
|
|
2965
|
+
* (the stall is reported) and bounded (the throttle prevents a flood
|
|
2966
|
+
* while the empty-poll loop is hot).
|
|
2967
|
+
* =================================================================== */
|
|
2968
|
+
static void do_test_fetch_stall_logged_on_no_broker(void) {
|
|
2969
|
+
test_ctx_t ctx;
|
|
2970
|
+
rd_kafka_share_t *rkshare;
|
|
2971
|
+
rd_kafka_conf_t *conf;
|
|
2972
|
+
rd_atomic32_t stall_cnt;
|
|
2973
|
+
const char *topic = "0182-fetch-stall";
|
|
2974
|
+
const char *group = "sg-0182-fetch-stall";
|
|
2975
|
+
const int msgcnt_recovery = 5;
|
|
2976
|
+
rd_kafka_messages_t *batch = NULL;
|
|
2977
|
+
rd_kafka_error_t *error;
|
|
2978
|
+
size_t rcvd;
|
|
2979
|
+
int acked, cnt;
|
|
2980
|
+
|
|
2981
|
+
SUB_TEST_QUICK();
|
|
2982
|
+
|
|
2983
|
+
/* Taking the only broker down raises __ALL_BROKERS_DOWN /
|
|
2984
|
+
* __TRANSPORT on the callbacks; none should fail the test. */
|
|
2985
|
+
test_curr->is_fatal_cb = test_error_is_not_fatal_cb;
|
|
2986
|
+
|
|
2987
|
+
ctx = test_ctx_new();
|
|
2988
|
+
|
|
2989
|
+
TEST_ASSERT(rd_kafka_mock_topic_create(ctx.mcluster, topic, 1, 1) ==
|
|
2990
|
+
RD_KAFKA_RESP_ERR_NO_ERROR,
|
|
2991
|
+
"create topic");
|
|
2992
|
+
|
|
2993
|
+
rd_atomic32_init(&stall_cnt, 0);
|
|
2994
|
+
test_conf_init(&conf, NULL, 0);
|
|
2995
|
+
test_conf_set(conf, "bootstrap.servers", ctx.bootstraps);
|
|
2996
|
+
test_conf_set(conf, "group.id", group);
|
|
2997
|
+
test_conf_set(conf, "share.acknowledgement.mode", "explicit");
|
|
2998
|
+
/* The stall log is emitted on the CONSUMER debug context. */
|
|
2999
|
+
test_conf_set(conf, "debug", "consumer");
|
|
3000
|
+
test_conf_set(conf, "reconnect.backoff.ms", "100");
|
|
3001
|
+
test_conf_set(conf, "reconnect.backoff.max.ms", "500");
|
|
3002
|
+
rd_kafka_conf_set_log_cb(conf, fetch_stall_log_cb);
|
|
3003
|
+
rd_kafka_conf_set_opaque(conf, &stall_cnt);
|
|
3004
|
+
|
|
3005
|
+
rkshare = rd_kafka_share_consumer_new(conf, NULL, 0);
|
|
3006
|
+
TEST_ASSERT(rkshare != NULL, "Failed to create share consumer");
|
|
3007
|
+
|
|
3008
|
+
test_share_consumer_subscribe_multi(rkshare, 1, topic);
|
|
3009
|
+
|
|
3010
|
+
/* Prime so the share group joins and the partition is assigned —
|
|
3011
|
+
* otherwise the stall reason would be "no partitions assigned". */
|
|
3012
|
+
mock_produce(ctx.producer, topic, 1);
|
|
3013
|
+
acked = consume_and_ack_all(rkshare, 1);
|
|
3014
|
+
TEST_ASSERT(acked == 1, "prime: expected 1 acked, got %d", acked);
|
|
3015
|
+
|
|
3016
|
+
/* Flush the primed ack so the down broker doesn't get an
|
|
3017
|
+
* ack-only op unrelated to the stall we're measuring. */
|
|
3018
|
+
error = rd_kafka_share_commit_async(rkshare);
|
|
3019
|
+
TEST_ASSERT(!error, "commit_async error: %s",
|
|
3020
|
+
error ? rd_kafka_error_string(error) : "NULL");
|
|
3021
|
+
|
|
3022
|
+
/* Kickstart a poll while the broker is still up so the primed ack
|
|
3023
|
+
* is flushed to the broker — otherwise it is redelivered after
|
|
3024
|
+
* recovery and inflates the post-recovery count. */
|
|
3025
|
+
error = rd_kafka_share_poll(rkshare, 500, &batch);
|
|
3026
|
+
TEST_ASSERT(!error, "kickstart share_poll error: %s",
|
|
3027
|
+
error ? rd_kafka_error_string(error) : "NULL");
|
|
3028
|
+
rd_kafka_messages_destroy(batch);
|
|
3029
|
+
batch = NULL;
|
|
3030
|
+
|
|
3031
|
+
/* Reset to drop any bring-up noise. */
|
|
3032
|
+
rd_atomic32_set(&stall_cnt, 0);
|
|
3033
|
+
|
|
3034
|
+
/* Take the only broker down, then poll. With the partition still
|
|
3035
|
+
* assigned and no broker reachable, the main-thread loop keeps
|
|
3036
|
+
* wanting to fetch but select_broker returns NULL. The 2s poll
|
|
3037
|
+
* stays inside the throttle window. */
|
|
3038
|
+
rd_kafka_mock_broker_set_down(ctx.mcluster, 1);
|
|
3039
|
+
|
|
3040
|
+
error = rd_kafka_share_poll(rkshare, 2000, &batch);
|
|
3041
|
+
TEST_ASSERT(!error, "down share_poll error: %s",
|
|
3042
|
+
error ? rd_kafka_error_string(error) : "NULL");
|
|
3043
|
+
rcvd = rd_kafka_messages_count(batch);
|
|
3044
|
+
TEST_ASSERT(rcvd == 0, "expected 0 records, got %zu", rcvd);
|
|
3045
|
+
rd_kafka_messages_destroy(batch);
|
|
3046
|
+
batch = NULL;
|
|
3047
|
+
|
|
3048
|
+
cnt = rd_atomic32_get(&stall_cnt);
|
|
3049
|
+
TEST_SAY("\"Fetch stalled: no eligible broker\" log count: %d\n", cnt);
|
|
3050
|
+
TEST_ASSERT(cnt >= 1,
|
|
3051
|
+
"expected the stall to be logged at least once, got %d",
|
|
3052
|
+
cnt);
|
|
3053
|
+
/* Throttled: a 2s hot loop must not flood. Allow a small margin
|
|
3054
|
+
* for a race-window reset + re-log around set_down. */
|
|
3055
|
+
TEST_ASSERT(cnt <= 3,
|
|
3056
|
+
"stall log must be throttled (<=3 in a 2s window), got %d",
|
|
3057
|
+
cnt);
|
|
3058
|
+
|
|
3059
|
+
/* Recovery: bring the broker back and confirm the consumer
|
|
3060
|
+
* drains. */
|
|
3061
|
+
rd_kafka_mock_broker_set_up(ctx.mcluster, 1);
|
|
3062
|
+
mock_produce(ctx.producer, topic, msgcnt_recovery);
|
|
3063
|
+
acked = consume_and_ack_all(rkshare, msgcnt_recovery);
|
|
3064
|
+
TEST_ASSERT(acked == msgcnt_recovery,
|
|
3065
|
+
"post-recovery: expected %d acked, got %d", msgcnt_recovery,
|
|
3066
|
+
acked);
|
|
3067
|
+
|
|
3068
|
+
test_share_consumer_close(rkshare);
|
|
3069
|
+
test_share_destroy(rkshare);
|
|
3070
|
+
test_ctx_destroy(&ctx);
|
|
3071
|
+
|
|
3072
|
+
test_curr->is_fatal_cb = NULL;
|
|
3073
|
+
|
|
3074
|
+
SUB_TEST_PASS();
|
|
3075
|
+
}
|
|
3076
|
+
|
|
3077
|
+
|
|
3078
|
+
/* ===================================================================
|
|
3079
|
+
* Partition-level error injection: API basics.
|
|
3080
|
+
* =================================================================== */
|
|
3081
|
+
static void test_partition_error_injection_general(void) {
|
|
3082
|
+
test_ctx_t ctx;
|
|
3083
|
+
rd_kafka_share_t *rkshare;
|
|
3084
|
+
char topic[64];
|
|
3085
|
+
char group[64];
|
|
3086
|
+
const int msgcnt = 3;
|
|
3087
|
+
int acked;
|
|
3088
|
+
|
|
3089
|
+
SUB_TEST_QUICK();
|
|
3090
|
+
|
|
3091
|
+
ctx = test_ctx_new();
|
|
3092
|
+
|
|
3093
|
+
rd_snprintf(topic, sizeof(topic), "0182-part_err_general");
|
|
3094
|
+
rd_snprintf(group, sizeof(group), "sg-0182-part_err_general");
|
|
3095
|
+
|
|
3096
|
+
TEST_ASSERT(rd_kafka_mock_topic_create(ctx.mcluster, topic, 1, 1) ==
|
|
3097
|
+
RD_KAFKA_RESP_ERR_NO_ERROR,
|
|
3098
|
+
"create topic");
|
|
3099
|
+
|
|
3100
|
+
/* Unknown topics are auto-created. */
|
|
3101
|
+
TEST_ASSERT(rd_kafka_mock_partition_push_request_errors(
|
|
3102
|
+
ctx.mcluster, "0182-no-such-topic", 0,
|
|
3103
|
+
RD_KAFKAP_ShareFetch, 1,
|
|
3104
|
+
RD_KAFKA_RESP_ERR_KAFKA_STORAGE_ERROR) ==
|
|
3105
|
+
RD_KAFKA_RESP_ERR_NO_ERROR,
|
|
3106
|
+
"push to unknown topic");
|
|
3107
|
+
|
|
3108
|
+
/* Out-of-range partition is rejected. */
|
|
3109
|
+
TEST_ASSERT(rd_kafka_mock_partition_push_request_errors(
|
|
3110
|
+
ctx.mcluster, topic, 99, RD_KAFKAP_ShareFetch, 1,
|
|
3111
|
+
RD_KAFKA_RESP_ERR_KAFKA_STORAGE_ERROR) ==
|
|
3112
|
+
RD_KAFKA_RESP_ERR_UNKNOWN_TOPIC_OR_PART,
|
|
3113
|
+
"push to unknown partition");
|
|
3114
|
+
|
|
3115
|
+
TEST_ASSERT(rd_kafka_mock_partition_push_request_errors(
|
|
3116
|
+
ctx.mcluster, topic, 0, RD_KAFKAP_ShareFetch, 1,
|
|
3117
|
+
RD_KAFKA_RESP_ERR_KAFKA_STORAGE_ERROR) ==
|
|
3118
|
+
RD_KAFKA_RESP_ERR_NO_ERROR,
|
|
3119
|
+
"push partition error");
|
|
3120
|
+
|
|
3121
|
+
mock_produce_partition(ctx.producer, topic, 0, msgcnt);
|
|
3122
|
+
|
|
3123
|
+
rkshare = create_mock_share_consumer(ctx.bootstraps, group, "explicit",
|
|
3124
|
+
NULL, NULL);
|
|
3125
|
+
test_share_consumer_subscribe_multi(rkshare, 1, topic);
|
|
3126
|
+
|
|
3127
|
+
/* The injected error is transient: all records are delivered
|
|
3128
|
+
* once it has been consumed off the stack. */
|
|
3129
|
+
acked = consume_and_ack_all(rkshare, msgcnt);
|
|
3130
|
+
TEST_ASSERT(acked == msgcnt, "expected %d acked, got %d", msgcnt,
|
|
3131
|
+
acked);
|
|
3132
|
+
|
|
3133
|
+
test_share_consumer_close(rkshare);
|
|
3134
|
+
test_share_destroy(rkshare);
|
|
3135
|
+
test_ctx_destroy(&ctx);
|
|
3136
|
+
|
|
3137
|
+
SUB_TEST_PASS();
|
|
3138
|
+
}
|
|
3139
|
+
|
|
3140
|
+
|
|
3141
|
+
/* ===================================================================
|
|
3142
|
+
* ShareFetch partition error injection: errors on one partition
|
|
3143
|
+
* must not affect the other, and the partition recovers once the
|
|
3144
|
+
* error stack drains.
|
|
3145
|
+
* =================================================================== */
|
|
3146
|
+
static void test_partition_error_injection_share_fetch(void) {
|
|
3147
|
+
test_ctx_t ctx;
|
|
3148
|
+
rd_kafka_share_t *rkshare;
|
|
3149
|
+
char topic[64];
|
|
3150
|
+
char group[64];
|
|
3151
|
+
const int msgs_per_part = 5;
|
|
3152
|
+
int acked;
|
|
3153
|
+
|
|
3154
|
+
SUB_TEST_QUICK();
|
|
3155
|
+
|
|
3156
|
+
ctx = test_ctx_new();
|
|
3157
|
+
|
|
3158
|
+
rd_snprintf(topic, sizeof(topic), "0182-part_err_sharefetch");
|
|
3159
|
+
rd_snprintf(group, sizeof(group), "sg-0182-part_err_sharefetch");
|
|
3160
|
+
|
|
3161
|
+
TEST_ASSERT(rd_kafka_mock_topic_create(ctx.mcluster, topic, 2, 1) ==
|
|
3162
|
+
RD_KAFKA_RESP_ERR_NO_ERROR,
|
|
3163
|
+
"create topic");
|
|
3164
|
+
|
|
3165
|
+
mock_produce_partition(ctx.producer, topic, 0, msgs_per_part);
|
|
3166
|
+
mock_produce_partition(ctx.producer, topic, 1, msgs_per_part);
|
|
3167
|
+
|
|
3168
|
+
TEST_ASSERT(rd_kafka_mock_partition_push_request_errors(
|
|
3169
|
+
ctx.mcluster, topic, 1, RD_KAFKAP_ShareFetch, 2,
|
|
3170
|
+
RD_KAFKA_RESP_ERR_NOT_LEADER_OR_FOLLOWER,
|
|
3171
|
+
RD_KAFKA_RESP_ERR_KAFKA_STORAGE_ERROR) ==
|
|
3172
|
+
RD_KAFKA_RESP_ERR_NO_ERROR,
|
|
3173
|
+
"push partition errors");
|
|
3174
|
+
|
|
3175
|
+
rkshare = create_mock_share_consumer(ctx.bootstraps, group, "explicit",
|
|
3176
|
+
NULL, NULL);
|
|
3177
|
+
test_share_consumer_subscribe_multi(rkshare, 1, topic);
|
|
3178
|
+
|
|
3179
|
+
/* Partition 0 is unaffected and partition 1 recovers after
|
|
3180
|
+
* the two errored fetches: nothing is lost. */
|
|
3181
|
+
acked = consume_and_ack_all(rkshare, 2 * msgs_per_part);
|
|
3182
|
+
TEST_ASSERT(acked == 2 * msgs_per_part, "expected %d acked, got %d",
|
|
3183
|
+
2 * msgs_per_part, acked);
|
|
3184
|
+
|
|
3185
|
+
test_share_consumer_close(rkshare);
|
|
3186
|
+
test_share_destroy(rkshare);
|
|
3187
|
+
test_ctx_destroy(&ctx);
|
|
3188
|
+
|
|
3189
|
+
SUB_TEST_PASS();
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
|
|
3193
|
+
/* ===================================================================
|
|
3194
|
+
* ShareAcknowledge partition error injection: the injected error
|
|
3195
|
+
* surfaces in commit_sync results for that partition only.
|
|
3196
|
+
* =================================================================== */
|
|
3197
|
+
static void test_partition_error_injection_share_ack(void) {
|
|
3198
|
+
test_ctx_t ctx;
|
|
3199
|
+
rd_kafka_share_t *rkshare;
|
|
3200
|
+
rd_kafka_topic_partition_list_t *partitions = NULL;
|
|
3201
|
+
rd_kafka_error_t *error;
|
|
3202
|
+
char topic[64];
|
|
3203
|
+
char group[64];
|
|
3204
|
+
const int msgs_per_part = 5;
|
|
3205
|
+
const rd_kafka_resp_err_t injected_err =
|
|
3206
|
+
RD_KAFKA_RESP_ERR_KAFKA_STORAGE_ERROR;
|
|
3207
|
+
int acked;
|
|
3208
|
+
int i;
|
|
3209
|
+
|
|
3210
|
+
SUB_TEST_QUICK();
|
|
3211
|
+
|
|
3212
|
+
ctx = test_ctx_new();
|
|
3213
|
+
|
|
3214
|
+
rd_snprintf(topic, sizeof(topic), "0182-part_err_shareack");
|
|
3215
|
+
rd_snprintf(group, sizeof(group), "sg-0182-part_err_shareack");
|
|
3216
|
+
|
|
3217
|
+
TEST_ASSERT(rd_kafka_mock_topic_create(ctx.mcluster, topic, 2, 1) ==
|
|
3218
|
+
RD_KAFKA_RESP_ERR_NO_ERROR,
|
|
3219
|
+
"create topic");
|
|
3220
|
+
|
|
3221
|
+
mock_produce_partition(ctx.producer, topic, 0, msgs_per_part);
|
|
3222
|
+
mock_produce_partition(ctx.producer, topic, 1, msgs_per_part);
|
|
3223
|
+
|
|
3224
|
+
rkshare = create_mock_share_consumer(ctx.bootstraps, group, "explicit",
|
|
3225
|
+
NULL, NULL);
|
|
3226
|
+
test_share_consumer_subscribe_multi(rkshare, 1, topic);
|
|
3227
|
+
|
|
3228
|
+
acked = consume_and_ack_all(rkshare, 2 * msgs_per_part);
|
|
3229
|
+
TEST_ASSERT(acked == 2 * msgs_per_part, "expected %d acked, got %d",
|
|
3230
|
+
2 * msgs_per_part, acked);
|
|
3231
|
+
|
|
3232
|
+
TEST_ASSERT(rd_kafka_mock_partition_push_request_errors(
|
|
3233
|
+
ctx.mcluster, topic, 0, RD_KAFKAP_ShareAcknowledge, 1,
|
|
3234
|
+
injected_err) == RD_KAFKA_RESP_ERR_NO_ERROR,
|
|
3235
|
+
"push partition error");
|
|
3236
|
+
|
|
3237
|
+
partitions = NULL;
|
|
3238
|
+
error = rd_kafka_share_commit_sync(rkshare, 30000, &partitions);
|
|
3239
|
+
if (error)
|
|
3240
|
+
rd_kafka_error_destroy(error);
|
|
3241
|
+
|
|
3242
|
+
TEST_ASSERT(partitions != NULL, "expected non-NULL partition results");
|
|
3243
|
+
TEST_ASSERT(partitions->cnt == 2,
|
|
3244
|
+
"expected results for 2 partitions, got %d",
|
|
3245
|
+
partitions->cnt);
|
|
3246
|
+
|
|
3247
|
+
/* Partition 0 carries the injected error, partition 1 is
|
|
3248
|
+
* unaffected. */
|
|
3249
|
+
for (i = 0; i < partitions->cnt; i++) {
|
|
3250
|
+
rd_kafka_topic_partition_t *rktpar = &partitions->elems[i];
|
|
3251
|
+
rd_kafka_resp_err_t exp_err = rktpar->partition == 0
|
|
3252
|
+
? injected_err
|
|
3253
|
+
: RD_KAFKA_RESP_ERR_NO_ERROR;
|
|
3254
|
+
|
|
3255
|
+
TEST_SAY("%s [%" PRId32 "]: %s\n", rktpar->topic,
|
|
3256
|
+
rktpar->partition, rd_kafka_err2name(rktpar->err));
|
|
3257
|
+
TEST_ASSERT(rktpar->err == exp_err,
|
|
3258
|
+
"partition [%" PRId32 "]: expected %s, got %s",
|
|
3259
|
+
rktpar->partition, rd_kafka_err2name(exp_err),
|
|
3260
|
+
rd_kafka_err2name(rktpar->err));
|
|
3261
|
+
}
|
|
3262
|
+
|
|
3263
|
+
rd_kafka_topic_partition_list_destroy(partitions);
|
|
3264
|
+
|
|
3265
|
+
test_share_consumer_close(rkshare);
|
|
3266
|
+
test_share_destroy(rkshare);
|
|
3267
|
+
test_ctx_destroy(&ctx);
|
|
3268
|
+
|
|
3269
|
+
SUB_TEST_PASS();
|
|
3270
|
+
}
|
|
3271
|
+
|
|
3272
|
+
|
|
3273
|
+
int main_0182_share_consumer_error_handling_mock(int argc, char **argv) {
|
|
3274
|
+
TEST_SKIP_MOCK_CLUSTER(0);
|
|
3275
|
+
|
|
3276
|
+
test_timeout_set(120);
|
|
3277
|
+
|
|
3278
|
+
test_commit_sync_share_session_not_found();
|
|
3279
|
+
test_commit_sync_invalid_share_session_epoch();
|
|
3280
|
+
test_commit_sync_share_session_limit_reached();
|
|
3281
|
+
test_commit_sync_group_authorization_failed();
|
|
3282
|
+
test_commit_sync_topic_authorization_failed();
|
|
3283
|
+
test_commit_sync_invalid_request();
|
|
3284
|
+
test_commit_sync_multi_partition_top_level_error();
|
|
3285
|
+
test_consume_batch_multi_partition_top_level_error();
|
|
3286
|
+
test_commit_sync_at_epoch_zero_returns_invalid_session_epoch_error();
|
|
3287
|
+
test_consume_batch_at_epoch_zero_strips_piggyback_acks();
|
|
3288
|
+
test_strip_pre_set_survives_sharefetch_err();
|
|
3289
|
+
|
|
3290
|
+
/* Topic-level metadata err surface */
|
|
3291
|
+
test_share_consumer_surfaces_topic_exception();
|
|
3292
|
+
test_share_consumer_surfaces_topic_authorization_failed();
|
|
3293
|
+
test_share_consumer_multi_partition_single_op_per_cycle();
|
|
3294
|
+
test_share_consumer_re_emits_when_err_code_changes();
|
|
3295
|
+
test_share_consumer_re_surfaces_after_recovery();
|
|
3296
|
+
test_share_consumer_re_surfaces_after_recovery_topic_exception();
|
|
3297
|
+
test_share_consumer_resubscribe_re_emits_persistent_failure();
|
|
3298
|
+
test_share_consumer_does_not_surface_unknown_topic_or_part();
|
|
3299
|
+
|
|
3300
|
+
/* Socket timeout matrix (single broker).
|
|
3301
|
+
*
|
|
3302
|
+
* Each call corresponds to a different ordering of the three
|
|
3303
|
+
* timer layers (api_timeout_ms, socket_timeout_ms, rtt_ms).
|
|
3304
|
+
* See the comment in do_test_socket_timeout_full_ack_then_more
|
|
3305
|
+
* for the full analysis of what each ordering exercises.
|
|
3306
|
+
* Arguments are (api_timeout_ms, socket_timeout_ms, rtt_ms). */
|
|
3307
|
+
|
|
3308
|
+
/* All 6 strict-inequality permutations. */
|
|
3309
|
+
do_test_socket_timeout_full_ack_then_more(
|
|
3310
|
+
1000, 5000, 3000); /* api < rtt < socket */
|
|
3311
|
+
do_test_socket_timeout_full_ack_then_more(
|
|
3312
|
+
1000, 3000, 5000); /* api < socket < rtt */
|
|
3313
|
+
do_test_socket_timeout_full_ack_then_more(
|
|
3314
|
+
3000, 5000, 1000); /* rtt < api < socket */
|
|
3315
|
+
do_test_socket_timeout_full_ack_then_more(
|
|
3316
|
+
5000, 3000, 1000); /* rtt < socket < api */
|
|
3317
|
+
do_test_socket_timeout_full_ack_then_more(
|
|
3318
|
+
3000, 1000, 5000); /* socket < api < rtt */
|
|
3319
|
+
do_test_socket_timeout_full_ack_then_more(
|
|
3320
|
+
5000, 1000, 3000); /* socket < rtt < api */
|
|
3321
|
+
/* Boundary api == socket is intentionally skipped: the race
|
|
3322
|
+
* between the api timer cb and the wire socket timer is
|
|
3323
|
+
* non-deterministic in practice — both outcomes are
|
|
3324
|
+
* observed across runs. */
|
|
3325
|
+
|
|
3326
|
+
/* Partial-ack variant of the matrix. Phase 1 acks only half
|
|
3327
|
+
* the consumed records; Phase 2 acks the remaining half (still
|
|
3328
|
+
* in client's inflight map). Phase 2 commit_sync surfaces
|
|
3329
|
+
* INVALID_SHARE_SESSION_EPOCH when the session was dropped
|
|
3330
|
+
* by the Phase 1 socket teardown (socket < rtt), NO_ERROR
|
|
3331
|
+
* otherwise. */
|
|
3332
|
+
do_test_socket_timeout_partial_ack_then_remaining(
|
|
3333
|
+
1000, 5000, 3000); /* api < rtt < socket */
|
|
3334
|
+
do_test_socket_timeout_partial_ack_then_remaining(
|
|
3335
|
+
1000, 3000, 5000); /* api < socket < rtt */
|
|
3336
|
+
do_test_socket_timeout_partial_ack_then_remaining(
|
|
3337
|
+
3000, 5000, 1000); /* rtt < api < socket */
|
|
3338
|
+
do_test_socket_timeout_partial_ack_then_remaining(
|
|
3339
|
+
5000, 3000, 1000); /* rtt < socket < api */
|
|
3340
|
+
do_test_socket_timeout_partial_ack_then_remaining(
|
|
3341
|
+
3000, 1000, 5000); /* socket < api < rtt */
|
|
3342
|
+
do_test_socket_timeout_partial_ack_then_remaining(
|
|
3343
|
+
5000, 1000, 3000); /* socket < rtt < api */
|
|
3344
|
+
/* Boundary api == socket skipped — see comment above. */
|
|
3345
|
+
|
|
3346
|
+
/* select_broker STATE_UP guard: steady-state DOWN and
|
|
3347
|
+
* race-window flavours. */
|
|
3348
|
+
do_test_no_bounce_loop_on_down_broker();
|
|
3349
|
+
do_test_one_log_on_broker_down_during_active_empty_poll();
|
|
3350
|
+
do_test_fetch_stall_logged_on_no_broker();
|
|
3351
|
+
|
|
3352
|
+
|
|
3353
|
+
/* Partition-level error injection. */
|
|
3354
|
+
test_partition_error_injection_general();
|
|
3355
|
+
test_partition_error_injection_share_fetch();
|
|
3356
|
+
test_partition_error_injection_share_ack();
|
|
3357
|
+
return 0;
|
|
3358
|
+
}
|