@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.
Files changed (628) hide show
  1. package/CHANGELOG.md +457 -0
  2. package/LICENSE.kafkajs +31 -0
  3. package/LICENSE.node-rdkafka +25 -0
  4. package/LICENSE.txt +20 -0
  5. package/README.md +133 -0
  6. package/binding.gyp +184 -0
  7. package/build/Release/confluent-kafka-javascript.node +0 -0
  8. package/build/deps/librdkafka++.so.1 +0 -0
  9. package/build/deps/librdkafka.Makefile +6 -0
  10. package/build/deps/librdkafka.so.1 +0 -0
  11. package/build/deps/librdkafka.target.mk +55 -0
  12. package/deps/librdkafka/.clang-format +136 -0
  13. package/deps/librdkafka/.clang-format-cpp +103 -0
  14. package/deps/librdkafka/.dir-locals.el +10 -0
  15. package/deps/librdkafka/.formatignore +35 -0
  16. package/deps/librdkafka/.gdbmacros +19 -0
  17. package/deps/librdkafka/CHANGELOG.md +2477 -0
  18. package/deps/librdkafka/CMakeLists.txt +291 -0
  19. package/deps/librdkafka/CODE_OF_CONDUCT.md +46 -0
  20. package/deps/librdkafka/CONFIGURATION.md +212 -0
  21. package/deps/librdkafka/CONTRIBUTING.md +431 -0
  22. package/deps/librdkafka/Doxyfile +2375 -0
  23. package/deps/librdkafka/INTRODUCTION.md +2752 -0
  24. package/deps/librdkafka/LICENSE +26 -0
  25. package/deps/librdkafka/LICENSE.cjson +22 -0
  26. package/deps/librdkafka/LICENSE.crc32c +28 -0
  27. package/deps/librdkafka/LICENSE.fnv1a +18 -0
  28. package/deps/librdkafka/LICENSE.hdrhistogram +27 -0
  29. package/deps/librdkafka/LICENSE.lz4 +26 -0
  30. package/deps/librdkafka/LICENSE.murmur2 +25 -0
  31. package/deps/librdkafka/LICENSE.nanopb +22 -0
  32. package/deps/librdkafka/LICENSE.opentelemetry +203 -0
  33. package/deps/librdkafka/LICENSE.pycrc +23 -0
  34. package/deps/librdkafka/LICENSE.queue +31 -0
  35. package/deps/librdkafka/LICENSE.regexp +5 -0
  36. package/deps/librdkafka/LICENSE.snappy +36 -0
  37. package/deps/librdkafka/LICENSE.tinycthread +26 -0
  38. package/deps/librdkafka/LICENSE.wingetopt +49 -0
  39. package/deps/librdkafka/LICENSES.txt +625 -0
  40. package/deps/librdkafka/Makefile +125 -0
  41. package/deps/librdkafka/README.md +201 -0
  42. package/deps/librdkafka/README.win32 +26 -0
  43. package/deps/librdkafka/STATISTICS.md +624 -0
  44. package/deps/librdkafka/configure +214 -0
  45. package/deps/librdkafka/configure.self +341 -0
  46. package/deps/librdkafka/dev-conf.sh +123 -0
  47. package/deps/librdkafka/lds-gen.py +73 -0
  48. package/deps/librdkafka/mainpage.doxy +40 -0
  49. package/deps/librdkafka/mklove/Makefile.base +329 -0
  50. package/deps/librdkafka/mklove/modules/configure.atomics +144 -0
  51. package/deps/librdkafka/mklove/modules/configure.base +2484 -0
  52. package/deps/librdkafka/mklove/modules/configure.builtin +70 -0
  53. package/deps/librdkafka/mklove/modules/configure.cc +186 -0
  54. package/deps/librdkafka/mklove/modules/configure.cxx +8 -0
  55. package/deps/librdkafka/mklove/modules/configure.fileversion +65 -0
  56. package/deps/librdkafka/mklove/modules/configure.gitversion +29 -0
  57. package/deps/librdkafka/mklove/modules/configure.good_cflags +18 -0
  58. package/deps/librdkafka/mklove/modules/configure.host +132 -0
  59. package/deps/librdkafka/mklove/modules/configure.lib +49 -0
  60. package/deps/librdkafka/mklove/modules/configure.libcurl +99 -0
  61. package/deps/librdkafka/mklove/modules/configure.libsasl2 +36 -0
  62. package/deps/librdkafka/mklove/modules/configure.libssl +147 -0
  63. package/deps/librdkafka/mklove/modules/configure.libzstd +58 -0
  64. package/deps/librdkafka/mklove/modules/configure.parseversion +95 -0
  65. package/deps/librdkafka/mklove/modules/configure.pic +16 -0
  66. package/deps/librdkafka/mklove/modules/configure.socket +20 -0
  67. package/deps/librdkafka/mklove/modules/configure.zlib +61 -0
  68. package/deps/librdkafka/mklove/modules/patches/README.md +8 -0
  69. package/deps/librdkafka/mklove/modules/patches/libcurl.0000-no-runtime-linking-check.patch +11 -0
  70. package/deps/librdkafka/mklove/modules/patches/libssl.0000-osx-rand-include-fix-OpenSSL-PR16409.patch +56 -0
  71. package/deps/librdkafka/service.yml +182 -0
  72. package/deps/librdkafka/src/CMakeLists.txt +379 -0
  73. package/deps/librdkafka/src/Makefile +105 -0
  74. package/deps/librdkafka/src/README.lz4.md +30 -0
  75. package/deps/librdkafka/src/cJSON.c +3191 -0
  76. package/deps/librdkafka/src/cJSON.h +306 -0
  77. package/deps/librdkafka/src/crc32c.c +430 -0
  78. package/deps/librdkafka/src/crc32c.h +38 -0
  79. package/deps/librdkafka/src/generate_proto.sh +66 -0
  80. package/deps/librdkafka/src/librdkafka_cgrp_synch.png +0 -0
  81. package/deps/librdkafka/src/lz4.c +2727 -0
  82. package/deps/librdkafka/src/lz4.h +842 -0
  83. package/deps/librdkafka/src/lz4frame.c +2078 -0
  84. package/deps/librdkafka/src/lz4frame.h +692 -0
  85. package/deps/librdkafka/src/lz4frame_static.h +47 -0
  86. package/deps/librdkafka/src/lz4hc.c +1631 -0
  87. package/deps/librdkafka/src/lz4hc.h +413 -0
  88. package/deps/librdkafka/src/nanopb/pb.h +917 -0
  89. package/deps/librdkafka/src/nanopb/pb_common.c +388 -0
  90. package/deps/librdkafka/src/nanopb/pb_common.h +49 -0
  91. package/deps/librdkafka/src/nanopb/pb_decode.c +1727 -0
  92. package/deps/librdkafka/src/nanopb/pb_decode.h +193 -0
  93. package/deps/librdkafka/src/nanopb/pb_encode.c +1000 -0
  94. package/deps/librdkafka/src/nanopb/pb_encode.h +185 -0
  95. package/deps/librdkafka/src/opentelemetry/common.pb.c +32 -0
  96. package/deps/librdkafka/src/opentelemetry/common.pb.h +170 -0
  97. package/deps/librdkafka/src/opentelemetry/metrics.options +2 -0
  98. package/deps/librdkafka/src/opentelemetry/metrics.pb.c +67 -0
  99. package/deps/librdkafka/src/opentelemetry/metrics.pb.h +966 -0
  100. package/deps/librdkafka/src/opentelemetry/resource.pb.c +12 -0
  101. package/deps/librdkafka/src/opentelemetry/resource.pb.h +58 -0
  102. package/deps/librdkafka/src/queue.h +850 -0
  103. package/deps/librdkafka/src/rd.h +592 -0
  104. package/deps/librdkafka/src/rdaddr.c +255 -0
  105. package/deps/librdkafka/src/rdaddr.h +202 -0
  106. package/deps/librdkafka/src/rdatomic.h +262 -0
  107. package/deps/librdkafka/src/rdavg.h +260 -0
  108. package/deps/librdkafka/src/rdavl.c +210 -0
  109. package/deps/librdkafka/src/rdavl.h +250 -0
  110. package/deps/librdkafka/src/rdbase64.c +200 -0
  111. package/deps/librdkafka/src/rdbase64.h +43 -0
  112. package/deps/librdkafka/src/rdbuf.c +1884 -0
  113. package/deps/librdkafka/src/rdbuf.h +375 -0
  114. package/deps/librdkafka/src/rdcrc32.c +114 -0
  115. package/deps/librdkafka/src/rdcrc32.h +170 -0
  116. package/deps/librdkafka/src/rddl.c +179 -0
  117. package/deps/librdkafka/src/rddl.h +43 -0
  118. package/deps/librdkafka/src/rdendian.h +175 -0
  119. package/deps/librdkafka/src/rdfloat.h +67 -0
  120. package/deps/librdkafka/src/rdfnv1a.c +113 -0
  121. package/deps/librdkafka/src/rdfnv1a.h +35 -0
  122. package/deps/librdkafka/src/rdgz.c +120 -0
  123. package/deps/librdkafka/src/rdgz.h +46 -0
  124. package/deps/librdkafka/src/rdhdrhistogram.c +721 -0
  125. package/deps/librdkafka/src/rdhdrhistogram.h +87 -0
  126. package/deps/librdkafka/src/rdhttp.c +831 -0
  127. package/deps/librdkafka/src/rdhttp.h +101 -0
  128. package/deps/librdkafka/src/rdinterval.h +177 -0
  129. package/deps/librdkafka/src/rdkafka.c +7287 -0
  130. package/deps/librdkafka/src/rdkafka.h +11795 -0
  131. package/deps/librdkafka/src/rdkafka_admin.c +10165 -0
  132. package/deps/librdkafka/src/rdkafka_admin.h +658 -0
  133. package/deps/librdkafka/src/rdkafka_assignment.c +1282 -0
  134. package/deps/librdkafka/src/rdkafka_assignment.h +73 -0
  135. package/deps/librdkafka/src/rdkafka_assignor.c +1800 -0
  136. package/deps/librdkafka/src/rdkafka_assignor.h +402 -0
  137. package/deps/librdkafka/src/rdkafka_aux.c +409 -0
  138. package/deps/librdkafka/src/rdkafka_aux.h +174 -0
  139. package/deps/librdkafka/src/rdkafka_background.c +222 -0
  140. package/deps/librdkafka/src/rdkafka_broker.c +6736 -0
  141. package/deps/librdkafka/src/rdkafka_broker.h +851 -0
  142. package/deps/librdkafka/src/rdkafka_buf.c +543 -0
  143. package/deps/librdkafka/src/rdkafka_buf.h +1525 -0
  144. package/deps/librdkafka/src/rdkafka_cert.c +576 -0
  145. package/deps/librdkafka/src/rdkafka_cert.h +62 -0
  146. package/deps/librdkafka/src/rdkafka_cgrp.c +8310 -0
  147. package/deps/librdkafka/src/rdkafka_cgrp.h +589 -0
  148. package/deps/librdkafka/src/rdkafka_conf.c +5177 -0
  149. package/deps/librdkafka/src/rdkafka_conf.h +746 -0
  150. package/deps/librdkafka/src/rdkafka_confval.h +97 -0
  151. package/deps/librdkafka/src/rdkafka_coord.c +623 -0
  152. package/deps/librdkafka/src/rdkafka_coord.h +132 -0
  153. package/deps/librdkafka/src/rdkafka_error.c +228 -0
  154. package/deps/librdkafka/src/rdkafka_error.h +80 -0
  155. package/deps/librdkafka/src/rdkafka_event.c +502 -0
  156. package/deps/librdkafka/src/rdkafka_event.h +126 -0
  157. package/deps/librdkafka/src/rdkafka_feature.c +898 -0
  158. package/deps/librdkafka/src/rdkafka_feature.h +104 -0
  159. package/deps/librdkafka/src/rdkafka_fetcher.c +3820 -0
  160. package/deps/librdkafka/src/rdkafka_fetcher.h +62 -0
  161. package/deps/librdkafka/src/rdkafka_header.c +220 -0
  162. package/deps/librdkafka/src/rdkafka_header.h +76 -0
  163. package/deps/librdkafka/src/rdkafka_idempotence.c +807 -0
  164. package/deps/librdkafka/src/rdkafka_idempotence.h +144 -0
  165. package/deps/librdkafka/src/rdkafka_int.h +1505 -0
  166. package/deps/librdkafka/src/rdkafka_interceptor.c +842 -0
  167. package/deps/librdkafka/src/rdkafka_interceptor.h +104 -0
  168. package/deps/librdkafka/src/rdkafka_lz4.c +450 -0
  169. package/deps/librdkafka/src/rdkafka_lz4.h +49 -0
  170. package/deps/librdkafka/src/rdkafka_metadata.c +2347 -0
  171. package/deps/librdkafka/src/rdkafka_metadata.h +350 -0
  172. package/deps/librdkafka/src/rdkafka_metadata_cache.c +1183 -0
  173. package/deps/librdkafka/src/rdkafka_mock.c +4514 -0
  174. package/deps/librdkafka/src/rdkafka_mock.h +867 -0
  175. package/deps/librdkafka/src/rdkafka_mock_cgrp.c +1877 -0
  176. package/deps/librdkafka/src/rdkafka_mock_handlers.c +5255 -0
  177. package/deps/librdkafka/src/rdkafka_mock_int.h +1005 -0
  178. package/deps/librdkafka/src/rdkafka_mock_sharegrp.c +1207 -0
  179. package/deps/librdkafka/src/rdkafka_msg.c +2619 -0
  180. package/deps/librdkafka/src/rdkafka_msg.h +629 -0
  181. package/deps/librdkafka/src/rdkafka_msgbatch.h +62 -0
  182. package/deps/librdkafka/src/rdkafka_msgset.h +105 -0
  183. package/deps/librdkafka/src/rdkafka_msgset_reader.c +2099 -0
  184. package/deps/librdkafka/src/rdkafka_msgset_writer.c +1474 -0
  185. package/deps/librdkafka/src/rdkafka_offset.c +1565 -0
  186. package/deps/librdkafka/src/rdkafka_offset.h +150 -0
  187. package/deps/librdkafka/src/rdkafka_op.c +1219 -0
  188. package/deps/librdkafka/src/rdkafka_op.h +1065 -0
  189. package/deps/librdkafka/src/rdkafka_partition.c +5109 -0
  190. package/deps/librdkafka/src/rdkafka_partition.h +1215 -0
  191. package/deps/librdkafka/src/rdkafka_pattern.c +228 -0
  192. package/deps/librdkafka/src/rdkafka_pattern.h +70 -0
  193. package/deps/librdkafka/src/rdkafka_plugin.c +213 -0
  194. package/deps/librdkafka/src/rdkafka_plugin.h +41 -0
  195. package/deps/librdkafka/src/rdkafka_proto.h +739 -0
  196. package/deps/librdkafka/src/rdkafka_protocol.h +131 -0
  197. package/deps/librdkafka/src/rdkafka_queue.c +1351 -0
  198. package/deps/librdkafka/src/rdkafka_queue.h +1235 -0
  199. package/deps/librdkafka/src/rdkafka_range_assignor.c +1748 -0
  200. package/deps/librdkafka/src/rdkafka_request.c +7298 -0
  201. package/deps/librdkafka/src/rdkafka_request.h +761 -0
  202. package/deps/librdkafka/src/rdkafka_roundrobin_assignor.c +123 -0
  203. package/deps/librdkafka/src/rdkafka_sasl.c +589 -0
  204. package/deps/librdkafka/src/rdkafka_sasl.h +63 -0
  205. package/deps/librdkafka/src/rdkafka_sasl_cyrus.c +722 -0
  206. package/deps/librdkafka/src/rdkafka_sasl_int.h +89 -0
  207. package/deps/librdkafka/src/rdkafka_sasl_oauthbearer.c +1833 -0
  208. package/deps/librdkafka/src/rdkafka_sasl_oauthbearer.h +52 -0
  209. package/deps/librdkafka/src/rdkafka_sasl_oauthbearer_oidc.c +1827 -0
  210. package/deps/librdkafka/src/rdkafka_sasl_oauthbearer_oidc.h +52 -0
  211. package/deps/librdkafka/src/rdkafka_sasl_plain.c +142 -0
  212. package/deps/librdkafka/src/rdkafka_sasl_scram.c +858 -0
  213. package/deps/librdkafka/src/rdkafka_sasl_win32.c +550 -0
  214. package/deps/librdkafka/src/rdkafka_share_acknowledgement.c +1445 -0
  215. package/deps/librdkafka/src/rdkafka_share_acknowledgement.h +373 -0
  216. package/deps/librdkafka/src/rdkafka_ssl.c +2235 -0
  217. package/deps/librdkafka/src/rdkafka_ssl.h +86 -0
  218. package/deps/librdkafka/src/rdkafka_sticky_assignor.c +4785 -0
  219. package/deps/librdkafka/src/rdkafka_subscription.c +411 -0
  220. package/deps/librdkafka/src/rdkafka_telemetry.c +761 -0
  221. package/deps/librdkafka/src/rdkafka_telemetry.h +52 -0
  222. package/deps/librdkafka/src/rdkafka_telemetry_decode.c +1307 -0
  223. package/deps/librdkafka/src/rdkafka_telemetry_decode.h +59 -0
  224. package/deps/librdkafka/src/rdkafka_telemetry_encode.c +1309 -0
  225. package/deps/librdkafka/src/rdkafka_telemetry_encode.h +459 -0
  226. package/deps/librdkafka/src/rdkafka_timer.c +407 -0
  227. package/deps/librdkafka/src/rdkafka_timer.h +117 -0
  228. package/deps/librdkafka/src/rdkafka_topic.c +2382 -0
  229. package/deps/librdkafka/src/rdkafka_topic.h +369 -0
  230. package/deps/librdkafka/src/rdkafka_transport.c +1309 -0
  231. package/deps/librdkafka/src/rdkafka_transport.h +99 -0
  232. package/deps/librdkafka/src/rdkafka_transport_int.h +100 -0
  233. package/deps/librdkafka/src/rdkafka_txnmgr.c +3256 -0
  234. package/deps/librdkafka/src/rdkafka_txnmgr.h +171 -0
  235. package/deps/librdkafka/src/rdkafka_zstd.c +226 -0
  236. package/deps/librdkafka/src/rdkafka_zstd.h +57 -0
  237. package/deps/librdkafka/src/rdlist.c +593 -0
  238. package/deps/librdkafka/src/rdlist.h +444 -0
  239. package/deps/librdkafka/src/rdlog.c +89 -0
  240. package/deps/librdkafka/src/rdlog.h +41 -0
  241. package/deps/librdkafka/src/rdmap.c +508 -0
  242. package/deps/librdkafka/src/rdmap.h +492 -0
  243. package/deps/librdkafka/src/rdmurmur2.c +167 -0
  244. package/deps/librdkafka/src/rdmurmur2.h +35 -0
  245. package/deps/librdkafka/src/rdports.c +61 -0
  246. package/deps/librdkafka/src/rdports.h +38 -0
  247. package/deps/librdkafka/src/rdposix.h +250 -0
  248. package/deps/librdkafka/src/rdrand.c +151 -0
  249. package/deps/librdkafka/src/rdrand.h +72 -0
  250. package/deps/librdkafka/src/rdregex.c +156 -0
  251. package/deps/librdkafka/src/rdregex.h +43 -0
  252. package/deps/librdkafka/src/rdsignal.h +57 -0
  253. package/deps/librdkafka/src/rdstring.c +645 -0
  254. package/deps/librdkafka/src/rdstring.h +98 -0
  255. package/deps/librdkafka/src/rdsysqueue.h +404 -0
  256. package/deps/librdkafka/src/rdtime.h +356 -0
  257. package/deps/librdkafka/src/rdtypes.h +86 -0
  258. package/deps/librdkafka/src/rdunittest.c +564 -0
  259. package/deps/librdkafka/src/rdunittest.h +232 -0
  260. package/deps/librdkafka/src/rdunittest_acknowledge.c +1177 -0
  261. package/deps/librdkafka/src/rdunittest_fetcher.c +1211 -0
  262. package/deps/librdkafka/src/rdunittest_msgset_errors.c +1521 -0
  263. package/deps/librdkafka/src/rdvarint.c +134 -0
  264. package/deps/librdkafka/src/rdvarint.h +165 -0
  265. package/deps/librdkafka/src/rdwin32.h +382 -0
  266. package/deps/librdkafka/src/rdxxhash.c +1030 -0
  267. package/deps/librdkafka/src/rdxxhash.h +328 -0
  268. package/deps/librdkafka/src/regexp.c +1352 -0
  269. package/deps/librdkafka/src/regexp.h +41 -0
  270. package/deps/librdkafka/src/snappy.c +1866 -0
  271. package/deps/librdkafka/src/snappy.h +62 -0
  272. package/deps/librdkafka/src/snappy_compat.h +138 -0
  273. package/deps/librdkafka/src/statistics_schema.json +444 -0
  274. package/deps/librdkafka/src/tinycthread.c +932 -0
  275. package/deps/librdkafka/src/tinycthread.h +503 -0
  276. package/deps/librdkafka/src/tinycthread_extra.c +210 -0
  277. package/deps/librdkafka/src/tinycthread_extra.h +218 -0
  278. package/deps/librdkafka/src/win32_config.h +58 -0
  279. package/deps/librdkafka/src-cpp/CMakeLists.txt +90 -0
  280. package/deps/librdkafka/src-cpp/ConfImpl.cpp +84 -0
  281. package/deps/librdkafka/src-cpp/ConsumerImpl.cpp +244 -0
  282. package/deps/librdkafka/src-cpp/HandleImpl.cpp +436 -0
  283. package/deps/librdkafka/src-cpp/HeadersImpl.cpp +48 -0
  284. package/deps/librdkafka/src-cpp/KafkaConsumerImpl.cpp +296 -0
  285. package/deps/librdkafka/src-cpp/Makefile +55 -0
  286. package/deps/librdkafka/src-cpp/MessageImpl.cpp +38 -0
  287. package/deps/librdkafka/src-cpp/MetadataImpl.cpp +170 -0
  288. package/deps/librdkafka/src-cpp/ProducerImpl.cpp +197 -0
  289. package/deps/librdkafka/src-cpp/QueueImpl.cpp +70 -0
  290. package/deps/librdkafka/src-cpp/README.md +16 -0
  291. package/deps/librdkafka/src-cpp/RdKafka.cpp +59 -0
  292. package/deps/librdkafka/src-cpp/TopicImpl.cpp +124 -0
  293. package/deps/librdkafka/src-cpp/TopicPartitionImpl.cpp +57 -0
  294. package/deps/librdkafka/src-cpp/rdkafkacpp.h +3797 -0
  295. package/deps/librdkafka/src-cpp/rdkafkacpp_int.h +1641 -0
  296. package/deps/librdkafka/tests/0000-unittests.c +72 -0
  297. package/deps/librdkafka/tests/0001-multiobj.c +102 -0
  298. package/deps/librdkafka/tests/0002-unkpart.c +244 -0
  299. package/deps/librdkafka/tests/0003-msgmaxsize.c +173 -0
  300. package/deps/librdkafka/tests/0004-conf.c +934 -0
  301. package/deps/librdkafka/tests/0005-order.c +133 -0
  302. package/deps/librdkafka/tests/0006-symbols.c +163 -0
  303. package/deps/librdkafka/tests/0007-autotopic.c +136 -0
  304. package/deps/librdkafka/tests/0008-reqacks.c +179 -0
  305. package/deps/librdkafka/tests/0009-mock_cluster.c +272 -0
  306. package/deps/librdkafka/tests/0011-produce_batch.c +753 -0
  307. package/deps/librdkafka/tests/0012-produce_consume.c +537 -0
  308. package/deps/librdkafka/tests/0013-null-msgs.c +473 -0
  309. package/deps/librdkafka/tests/0014-reconsume-191.c +512 -0
  310. package/deps/librdkafka/tests/0015-offset_seeks.c +172 -0
  311. package/deps/librdkafka/tests/0016-client_swname.c +181 -0
  312. package/deps/librdkafka/tests/0017-compression.c +140 -0
  313. package/deps/librdkafka/tests/0018-cgrp_term.c +444 -0
  314. package/deps/librdkafka/tests/0019-list_groups.c +289 -0
  315. package/deps/librdkafka/tests/0020-destroy_hang.c +162 -0
  316. package/deps/librdkafka/tests/0021-rkt_destroy.c +72 -0
  317. package/deps/librdkafka/tests/0022-consume_batch.c +331 -0
  318. package/deps/librdkafka/tests/0025-timers.c +147 -0
  319. package/deps/librdkafka/tests/0026-consume_pause.c +547 -0
  320. package/deps/librdkafka/tests/0028-long_topicnames.c +79 -0
  321. package/deps/librdkafka/tests/0029-assign_offset.c +202 -0
  322. package/deps/librdkafka/tests/0030-offset_commit.c +589 -0
  323. package/deps/librdkafka/tests/0031-get_offsets.c +235 -0
  324. package/deps/librdkafka/tests/0033-regex_subscribe.c +536 -0
  325. package/deps/librdkafka/tests/0034-offset_reset.c +398 -0
  326. package/deps/librdkafka/tests/0035-api_version.c +73 -0
  327. package/deps/librdkafka/tests/0036-partial_fetch.c +87 -0
  328. package/deps/librdkafka/tests/0037-destroy_hang_local.c +85 -0
  329. package/deps/librdkafka/tests/0038-performance.c +121 -0
  330. package/deps/librdkafka/tests/0039-event.c +284 -0
  331. package/deps/librdkafka/tests/0040-io_event.c +257 -0
  332. package/deps/librdkafka/tests/0041-fetch_max_bytes.c +97 -0
  333. package/deps/librdkafka/tests/0042-many_topics.c +252 -0
  334. package/deps/librdkafka/tests/0043-no_connection.c +77 -0
  335. package/deps/librdkafka/tests/0044-partition_cnt.c +94 -0
  336. package/deps/librdkafka/tests/0045-subscribe_update.c +1010 -0
  337. package/deps/librdkafka/tests/0046-rkt_cache.c +65 -0
  338. package/deps/librdkafka/tests/0047-partial_buf_tmout.c +98 -0
  339. package/deps/librdkafka/tests/0048-partitioner.c +283 -0
  340. package/deps/librdkafka/tests/0049-consume_conn_close.c +162 -0
  341. package/deps/librdkafka/tests/0050-subscribe_adds.c +145 -0
  342. package/deps/librdkafka/tests/0051-assign_adds.c +126 -0
  343. package/deps/librdkafka/tests/0052-msg_timestamps.c +238 -0
  344. package/deps/librdkafka/tests/0053-stats_cb.cpp +527 -0
  345. package/deps/librdkafka/tests/0054-offset_time.cpp +236 -0
  346. package/deps/librdkafka/tests/0055-producer_latency.c +539 -0
  347. package/deps/librdkafka/tests/0056-balanced_group_mt.c +315 -0
  348. package/deps/librdkafka/tests/0057-invalid_topic.cpp +112 -0
  349. package/deps/librdkafka/tests/0058-log.cpp +123 -0
  350. package/deps/librdkafka/tests/0059-bsearch.cpp +241 -0
  351. package/deps/librdkafka/tests/0060-op_prio.cpp +163 -0
  352. package/deps/librdkafka/tests/0061-consumer_lag.cpp +295 -0
  353. package/deps/librdkafka/tests/0062-stats_event.c +126 -0
  354. package/deps/librdkafka/tests/0063-clusterid.cpp +180 -0
  355. package/deps/librdkafka/tests/0064-interceptors.c +529 -0
  356. package/deps/librdkafka/tests/0065-yield.cpp +140 -0
  357. package/deps/librdkafka/tests/0066-plugins.cpp +143 -0
  358. package/deps/librdkafka/tests/0067-empty_topic.cpp +151 -0
  359. package/deps/librdkafka/tests/0068-produce_timeout.c +136 -0
  360. package/deps/librdkafka/tests/0069-consumer_add_parts.c +119 -0
  361. package/deps/librdkafka/tests/0070-null_empty.cpp +197 -0
  362. package/deps/librdkafka/tests/0072-headers_ut.c +448 -0
  363. package/deps/librdkafka/tests/0073-headers.c +381 -0
  364. package/deps/librdkafka/tests/0074-producev.c +87 -0
  365. package/deps/librdkafka/tests/0075-retry.c +290 -0
  366. package/deps/librdkafka/tests/0076-produce_retry.c +452 -0
  367. package/deps/librdkafka/tests/0077-compaction.c +366 -0
  368. package/deps/librdkafka/tests/0078-c_from_cpp.cpp +96 -0
  369. package/deps/librdkafka/tests/0079-fork.c +93 -0
  370. package/deps/librdkafka/tests/0080-admin_ut.c +3095 -0
  371. package/deps/librdkafka/tests/0081-admin.c +5633 -0
  372. package/deps/librdkafka/tests/0082-fetch_max_bytes.cpp +137 -0
  373. package/deps/librdkafka/tests/0083-cb_event.c +233 -0
  374. package/deps/librdkafka/tests/0084-destroy_flags.c +208 -0
  375. package/deps/librdkafka/tests/0085-headers.cpp +392 -0
  376. package/deps/librdkafka/tests/0086-purge.c +368 -0
  377. package/deps/librdkafka/tests/0088-produce_metadata_timeout.c +162 -0
  378. package/deps/librdkafka/tests/0089-max_poll_interval.c +511 -0
  379. package/deps/librdkafka/tests/0090-idempotence.c +171 -0
  380. package/deps/librdkafka/tests/0091-max_poll_interval_timeout.c +295 -0
  381. package/deps/librdkafka/tests/0092-mixed_msgver.c +103 -0
  382. package/deps/librdkafka/tests/0093-holb.c +200 -0
  383. package/deps/librdkafka/tests/0094-idempotence_msg_timeout.c +231 -0
  384. package/deps/librdkafka/tests/0095-all_brokers_down.cpp +122 -0
  385. package/deps/librdkafka/tests/0097-ssl_verify.cpp +674 -0
  386. package/deps/librdkafka/tests/0098-consumer-txn.cpp +1218 -0
  387. package/deps/librdkafka/tests/0099-commit_metadata.c +194 -0
  388. package/deps/librdkafka/tests/0100-thread_interceptors.cpp +195 -0
  389. package/deps/librdkafka/tests/0101-fetch-from-follower.cpp +446 -0
  390. package/deps/librdkafka/tests/0102-static_group_rebalance.c +836 -0
  391. package/deps/librdkafka/tests/0103-transactions.c +1383 -0
  392. package/deps/librdkafka/tests/0104-fetch_from_follower_mock.c +625 -0
  393. package/deps/librdkafka/tests/0105-transactions_mock.c +3930 -0
  394. package/deps/librdkafka/tests/0106-cgrp_sess_timeout.c +318 -0
  395. package/deps/librdkafka/tests/0107-topic_recreate.c +259 -0
  396. package/deps/librdkafka/tests/0109-auto_create_topics.cpp +278 -0
  397. package/deps/librdkafka/tests/0110-batch_size.cpp +182 -0
  398. package/deps/librdkafka/tests/0111-delay_create_topics.cpp +127 -0
  399. package/deps/librdkafka/tests/0112-assign_unknown_part.c +87 -0
  400. package/deps/librdkafka/tests/0113-cooperative_rebalance.cpp +3473 -0
  401. package/deps/librdkafka/tests/0114-sticky_partitioning.cpp +176 -0
  402. package/deps/librdkafka/tests/0115-producer_auth.cpp +182 -0
  403. package/deps/librdkafka/tests/0116-kafkaconsumer_close.cpp +216 -0
  404. package/deps/librdkafka/tests/0117-mock_errors.c +331 -0
  405. package/deps/librdkafka/tests/0118-commit_rebalance.c +154 -0
  406. package/deps/librdkafka/tests/0119-consumer_auth.cpp +167 -0
  407. package/deps/librdkafka/tests/0120-asymmetric_subscription.c +185 -0
  408. package/deps/librdkafka/tests/0121-clusterid.c +115 -0
  409. package/deps/librdkafka/tests/0122-buffer_cleaning_after_rebalance.c +227 -0
  410. package/deps/librdkafka/tests/0123-connections_max_idle.c +98 -0
  411. package/deps/librdkafka/tests/0124-openssl_invalid_engine.c +69 -0
  412. package/deps/librdkafka/tests/0125-immediate_flush.c +144 -0
  413. package/deps/librdkafka/tests/0126-oauthbearer_oidc.c +1164 -0
  414. package/deps/librdkafka/tests/0127-fetch_queue_backoff.cpp +165 -0
  415. package/deps/librdkafka/tests/0128-sasl_callback_queue.cpp +217 -0
  416. package/deps/librdkafka/tests/0129-fetch_aborted_msgs.c +79 -0
  417. package/deps/librdkafka/tests/0130-store_offsets.c +178 -0
  418. package/deps/librdkafka/tests/0131-connect_timeout.c +81 -0
  419. package/deps/librdkafka/tests/0132-strategy_ordering.c +179 -0
  420. package/deps/librdkafka/tests/0133-ssl_keys.c +150 -0
  421. package/deps/librdkafka/tests/0134-ssl_provider.c +92 -0
  422. package/deps/librdkafka/tests/0135-sasl_credentials.cpp +279 -0
  423. package/deps/librdkafka/tests/0136-resolve_cb.c +181 -0
  424. package/deps/librdkafka/tests/0137-barrier_batch_consume.c +752 -0
  425. package/deps/librdkafka/tests/0138-admin_mock.c +281 -0
  426. package/deps/librdkafka/tests/0139-offset_validation_mock.c +950 -0
  427. package/deps/librdkafka/tests/0140-commit_metadata.cpp +108 -0
  428. package/deps/librdkafka/tests/0142-reauthentication.c +931 -0
  429. package/deps/librdkafka/tests/0143-exponential_backoff_mock.c +552 -0
  430. package/deps/librdkafka/tests/0144-idempotence_mock.c +373 -0
  431. package/deps/librdkafka/tests/0145-pause_resume_mock.c +119 -0
  432. package/deps/librdkafka/tests/0146-metadata_mock.c +505 -0
  433. package/deps/librdkafka/tests/0147-consumer_group_consumer_mock.c +1210 -0
  434. package/deps/librdkafka/tests/0148-offset_fetch_commit_error_mock.c +569 -0
  435. package/deps/librdkafka/tests/0149-broker-same-host-port.c +140 -0
  436. package/deps/librdkafka/tests/0150-telemetry_mock.c +651 -0
  437. package/deps/librdkafka/tests/0151-purge-brokers.c +566 -0
  438. package/deps/librdkafka/tests/0152-rebootstrap.c +59 -0
  439. package/deps/librdkafka/tests/0153-memberid.c +128 -0
  440. package/deps/librdkafka/tests/0155-share_group_heartbeat_mock.c +2695 -0
  441. package/deps/librdkafka/tests/0156-share_consumer_fetch_mock.c +1551 -0
  442. package/deps/librdkafka/tests/0157-share_consumer_ack_mock.c +1788 -0
  443. package/deps/librdkafka/tests/0158-share_consumer_transactions_mock.c +467 -0
  444. package/deps/librdkafka/tests/0170-share_consumer_subscription.c +2192 -0
  445. package/deps/librdkafka/tests/0171-share_consumer_consume.c +2150 -0
  446. package/deps/librdkafka/tests/0172-share_consumer_acknowledge.c +1919 -0
  447. package/deps/librdkafka/tests/0173-share_consumer_commit_async.c +2735 -0
  448. package/deps/librdkafka/tests/0174-share_consumer_concurrency.c +1434 -0
  449. package/deps/librdkafka/tests/0175-share_consumer_groups.c +574 -0
  450. package/deps/librdkafka/tests/0176-share_consumer_commit_sync.c +2986 -0
  451. package/deps/librdkafka/tests/0177-share_consumer_transactions.c +1815 -0
  452. package/deps/librdkafka/tests/0178-share_consumer_close.c +2102 -0
  453. package/deps/librdkafka/tests/0179-share_consumer_destroy.c +2482 -0
  454. package/deps/librdkafka/tests/0180-share_consumer_config.c +1677 -0
  455. package/deps/librdkafka/tests/0181-share_consumer_topic_delete.c +354 -0
  456. package/deps/librdkafka/tests/0182-share_consumer_error_handling_mock.c +3358 -0
  457. package/deps/librdkafka/tests/0183-share_consumer_leader_change_mock.c +1234 -0
  458. package/deps/librdkafka/tests/0184-share_consumer_topic_recreate.c +1356 -0
  459. package/deps/librdkafka/tests/0185-share_consumer_max_poll_interval.c +557 -0
  460. package/deps/librdkafka/tests/0186-share_consumer_fatal_error.c +360 -0
  461. package/deps/librdkafka/tests/0190-share_consumer_telemetry.c +387 -0
  462. package/deps/librdkafka/tests/1000-unktopic.c +164 -0
  463. package/deps/librdkafka/tests/8000-idle.cpp +60 -0
  464. package/deps/librdkafka/tests/8001-fetch_from_follower_mock_manual.c +113 -0
  465. package/deps/librdkafka/tests/CMakeLists.txt +193 -0
  466. package/deps/librdkafka/tests/LibrdkafkaTestApp.py +291 -0
  467. package/deps/librdkafka/tests/Makefile +182 -0
  468. package/deps/librdkafka/tests/README.md +514 -0
  469. package/deps/librdkafka/tests/autotest.sh +33 -0
  470. package/deps/librdkafka/tests/backtrace.gdb +30 -0
  471. package/deps/librdkafka/tests/broker_version_tests.py +315 -0
  472. package/deps/librdkafka/tests/buildbox.sh +17 -0
  473. package/deps/librdkafka/tests/cleanup-checker-tests.sh +20 -0
  474. package/deps/librdkafka/tests/cluster_testing.py +203 -0
  475. package/deps/librdkafka/tests/delete-test-topics.sh +56 -0
  476. package/deps/librdkafka/tests/fixtures/oauthbearer/jwt_assertion_template.json +10 -0
  477. package/deps/librdkafka/tests/fixtures/ssl/Makefile +8 -0
  478. package/deps/librdkafka/tests/fixtures/ssl/README.md +13 -0
  479. package/deps/librdkafka/tests/fixtures/ssl/client.keystore.intermediate.p12 +0 -0
  480. package/deps/librdkafka/tests/fixtures/ssl/client.keystore.p12 +0 -0
  481. package/deps/librdkafka/tests/fixtures/ssl/client2.certificate.intermediate.pem +72 -0
  482. package/deps/librdkafka/tests/fixtures/ssl/client2.certificate.pem +50 -0
  483. package/deps/librdkafka/tests/fixtures/ssl/client2.intermediate.key +46 -0
  484. package/deps/librdkafka/tests/fixtures/ssl/client2.key +46 -0
  485. package/deps/librdkafka/tests/fixtures/ssl/create_keys.sh +168 -0
  486. package/deps/librdkafka/tests/fuzzers/Makefile +12 -0
  487. package/deps/librdkafka/tests/fuzzers/README.md +31 -0
  488. package/deps/librdkafka/tests/fuzzers/fuzz_regex.c +74 -0
  489. package/deps/librdkafka/tests/fuzzers/helpers.h +90 -0
  490. package/deps/librdkafka/tests/gen-ssl-certs.sh +165 -0
  491. package/deps/librdkafka/tests/interactive_broker_version.py +170 -0
  492. package/deps/librdkafka/tests/interceptor_test/CMakeLists.txt +16 -0
  493. package/deps/librdkafka/tests/interceptor_test/Makefile +22 -0
  494. package/deps/librdkafka/tests/interceptor_test/interceptor_test.c +314 -0
  495. package/deps/librdkafka/tests/interceptor_test/interceptor_test.h +54 -0
  496. package/deps/librdkafka/tests/java/IncrementalRebalanceCli.java +97 -0
  497. package/deps/librdkafka/tests/java/Makefile +13 -0
  498. package/deps/librdkafka/tests/java/Murmur2Cli.java +46 -0
  499. package/deps/librdkafka/tests/java/README.md +14 -0
  500. package/deps/librdkafka/tests/java/TransactionProducerCli.java +162 -0
  501. package/deps/librdkafka/tests/java/run-class.sh +11 -0
  502. package/deps/librdkafka/tests/librdkafka.suppressions +483 -0
  503. package/deps/librdkafka/tests/lz4_manual_test.sh +59 -0
  504. package/deps/librdkafka/tests/multi-broker-version-test.sh +50 -0
  505. package/deps/librdkafka/tests/parse-refcnt.sh +43 -0
  506. package/deps/librdkafka/tests/performance_plot.py +115 -0
  507. package/deps/librdkafka/tests/plugin_test/Makefile +19 -0
  508. package/deps/librdkafka/tests/plugin_test/plugin_test.c +58 -0
  509. package/deps/librdkafka/tests/requirements.txt +2 -0
  510. package/deps/librdkafka/tests/run-all-tests.sh +81 -0
  511. package/deps/librdkafka/tests/run-consumer-tests.sh +16 -0
  512. package/deps/librdkafka/tests/run-producer-tests.sh +16 -0
  513. package/deps/librdkafka/tests/run-test-batches.py +157 -0
  514. package/deps/librdkafka/tests/run-test.sh +141 -0
  515. package/deps/librdkafka/tests/rusage.c +249 -0
  516. package/deps/librdkafka/tests/sasl_test.py +289 -0
  517. package/deps/librdkafka/tests/scenarios/README.md +6 -0
  518. package/deps/librdkafka/tests/scenarios/ak23.json +6 -0
  519. package/deps/librdkafka/tests/scenarios/default.json +5 -0
  520. package/deps/librdkafka/tests/scenarios/noautocreate.json +5 -0
  521. package/deps/librdkafka/tests/share_telemetry_verify/share-otel-collector-config.yaml +28 -0
  522. package/deps/librdkafka/tests/sockem.c +801 -0
  523. package/deps/librdkafka/tests/sockem.h +85 -0
  524. package/deps/librdkafka/tests/sockem_ctrl.c +145 -0
  525. package/deps/librdkafka/tests/sockem_ctrl.h +61 -0
  526. package/deps/librdkafka/tests/test.c +8376 -0
  527. package/deps/librdkafka/tests/test.conf.example +27 -0
  528. package/deps/librdkafka/tests/test.h +1150 -0
  529. package/deps/librdkafka/tests/testcpp.cpp +131 -0
  530. package/deps/librdkafka/tests/testcpp.h +388 -0
  531. package/deps/librdkafka/tests/testshared.h +416 -0
  532. package/deps/librdkafka/tests/tools/README.md +4 -0
  533. package/deps/librdkafka/tests/tools/stats/README.md +21 -0
  534. package/deps/librdkafka/tests/tools/stats/filter.jq +42 -0
  535. package/deps/librdkafka/tests/tools/stats/graph.py +150 -0
  536. package/deps/librdkafka/tests/tools/stats/requirements.txt +3 -0
  537. package/deps/librdkafka/tests/tools/stats/to_csv.py +124 -0
  538. package/deps/librdkafka/tests/trivup/trivup-0.15.0.tar.gz +0 -0
  539. package/deps/librdkafka/tests/until-fail.sh +87 -0
  540. package/deps/librdkafka/tests/xxxx-assign_partition.c +122 -0
  541. package/deps/librdkafka/tests/xxxx-metadata.cpp +159 -0
  542. package/deps/librdkafka/vcpkg.json +23 -0
  543. package/deps/librdkafka/win32/README.md +5 -0
  544. package/deps/librdkafka/win32/build-package.bat +3 -0
  545. package/deps/librdkafka/win32/build.bat +19 -0
  546. package/deps/librdkafka/win32/common.vcxproj +84 -0
  547. package/deps/librdkafka/win32/interceptor_test/interceptor_test.vcxproj +87 -0
  548. package/deps/librdkafka/win32/librdkafka.autopkg.template +54 -0
  549. package/deps/librdkafka/win32/librdkafka.master.testing.targets +13 -0
  550. package/deps/librdkafka/win32/librdkafka.sln +226 -0
  551. package/deps/librdkafka/win32/librdkafka.vcxproj +282 -0
  552. package/deps/librdkafka/win32/librdkafkacpp/librdkafkacpp.vcxproj +104 -0
  553. package/deps/librdkafka/win32/msbuild.ps1 +58 -0
  554. package/deps/librdkafka/win32/openssl_engine_example/openssl_engine_example.vcxproj +132 -0
  555. package/deps/librdkafka/win32/package-zip.ps1 +46 -0
  556. package/deps/librdkafka/win32/packages/repositories.config +4 -0
  557. package/deps/librdkafka/win32/push-package.bat +4 -0
  558. package/deps/librdkafka/win32/rdkafka_complex_consumer_example_cpp/rdkafka_complex_consumer_example_cpp.vcxproj +67 -0
  559. package/deps/librdkafka/win32/rdkafka_example/rdkafka_example.vcxproj +97 -0
  560. package/deps/librdkafka/win32/rdkafka_performance/rdkafka_performance.vcxproj +97 -0
  561. package/deps/librdkafka/win32/setup-msys2.ps1 +47 -0
  562. package/deps/librdkafka/win32/setup-vcpkg.ps1 +34 -0
  563. package/deps/librdkafka/win32/tests/test.conf.example +25 -0
  564. package/deps/librdkafka/win32/tests/tests.vcxproj +278 -0
  565. package/deps/librdkafka/win32/win_ssl_cert_store/win_ssl_cert_store.vcxproj +132 -0
  566. package/deps/librdkafka/win32/wingetopt.c +564 -0
  567. package/deps/librdkafka/win32/wingetopt.h +101 -0
  568. package/deps/librdkafka/win32/wintime.h +33 -0
  569. package/deps/librdkafka.gyp +83 -0
  570. package/deps/windows-install.py +80 -0
  571. package/index.d.ts +3 -0
  572. package/lib/admin.js +732 -0
  573. package/lib/client.js +679 -0
  574. package/lib/error.js +501 -0
  575. package/lib/index.js +8 -0
  576. package/lib/kafka-consumer-stream.js +399 -0
  577. package/lib/kafka-consumer.js +798 -0
  578. package/lib/kafkajs/_admin.js +1052 -0
  579. package/lib/kafkajs/_common.js +887 -0
  580. package/lib/kafkajs/_consumer.js +2156 -0
  581. package/lib/kafkajs/_consumer_cache.js +310 -0
  582. package/lib/kafkajs/_error.js +298 -0
  583. package/lib/kafkajs/_kafka.js +125 -0
  584. package/lib/kafkajs/_linked-list.js +220 -0
  585. package/lib/kafkajs/_producer.js +863 -0
  586. package/lib/kafkajs/index.js +5 -0
  587. package/lib/producer/high-level-producer.js +400 -0
  588. package/lib/producer-stream.js +337 -0
  589. package/lib/producer.js +405 -0
  590. package/lib/rdkafka.js +44 -0
  591. package/lib/tools/ref-counter.js +53 -0
  592. package/lib/topic-partition.js +91 -0
  593. package/lib/topic.js +43 -0
  594. package/lib/util.js +55 -0
  595. package/librdkafka.js +12 -0
  596. package/package.json +85 -0
  597. package/shim/source_location +54 -0
  598. package/src/admin.cc +1597 -0
  599. package/src/admin.h +112 -0
  600. package/src/binding.cc +76 -0
  601. package/src/binding.h +25 -0
  602. package/src/callbacks.cc +676 -0
  603. package/src/callbacks.h +302 -0
  604. package/src/common.cc +1628 -0
  605. package/src/common.h +192 -0
  606. package/src/config.cc +219 -0
  607. package/src/config.h +61 -0
  608. package/src/connection.cc +703 -0
  609. package/src/connection.h +117 -0
  610. package/src/errors.cc +119 -0
  611. package/src/errors.h +60 -0
  612. package/src/kafka-consumer.cc +1539 -0
  613. package/src/kafka-consumer.h +147 -0
  614. package/src/producer.cc +917 -0
  615. package/src/producer.h +124 -0
  616. package/src/topic.cc +173 -0
  617. package/src/topic.h +54 -0
  618. package/src/workers.cc +1714 -0
  619. package/src/workers.h +690 -0
  620. package/types/config.d.ts +1269 -0
  621. package/types/errors.d.ts +373 -0
  622. package/types/kafkajs.d.ts +568 -0
  623. package/types/rdkafka.d.ts +571 -0
  624. package/util/configure.js +32 -0
  625. package/util/generate-docs.sh +18 -0
  626. package/util/get-env.js +6 -0
  627. package/util/test-compile.js +11 -0
  628. 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
+ }