kafka-python 3.0.0__py3-none-any.whl

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 (373) hide show
  1. kafka/__init__.py +34 -0
  2. kafka/__main__.py +5 -0
  3. kafka/admin/__init__.py +29 -0
  4. kafka/admin/__main__.py +5 -0
  5. kafka/admin/_acls.py +355 -0
  6. kafka/admin/_cluster.py +359 -0
  7. kafka/admin/_configs.py +479 -0
  8. kafka/admin/_groups.py +754 -0
  9. kafka/admin/_partitions.py +595 -0
  10. kafka/admin/_topics.py +281 -0
  11. kafka/admin/_transactions.py +450 -0
  12. kafka/admin/_users.py +194 -0
  13. kafka/admin/client.py +373 -0
  14. kafka/benchmarks/__init__.py +0 -0
  15. kafka/benchmarks/consumer_performance.py +138 -0
  16. kafka/benchmarks/load_example.py +109 -0
  17. kafka/benchmarks/producer_encode_path.py +201 -0
  18. kafka/benchmarks/producer_performance.py +161 -0
  19. kafka/benchmarks/profile_protocol.py +138 -0
  20. kafka/benchmarks/protocol_old_vs_new.py +447 -0
  21. kafka/benchmarks/record_batch_compose.py +77 -0
  22. kafka/benchmarks/record_batch_read.py +82 -0
  23. kafka/benchmarks/varint_speed.py +426 -0
  24. kafka/cli/__init__.py +36 -0
  25. kafka/cli/admin/__init__.py +117 -0
  26. kafka/cli/admin/acls/__init__.py +9 -0
  27. kafka/cli/admin/acls/common.py +76 -0
  28. kafka/cli/admin/acls/create.py +19 -0
  29. kafka/cli/admin/acls/delete.py +23 -0
  30. kafka/cli/admin/acls/describe.py +16 -0
  31. kafka/cli/admin/cluster/__init__.py +14 -0
  32. kafka/cli/admin/cluster/describe.py +11 -0
  33. kafka/cli/admin/cluster/describe_quorum.py +11 -0
  34. kafka/cli/admin/cluster/features.py +52 -0
  35. kafka/cli/admin/cluster/log_dirs.py +43 -0
  36. kafka/cli/admin/cluster/versions.py +33 -0
  37. kafka/cli/admin/configs/__init__.py +10 -0
  38. kafka/cli/admin/configs/alter.py +43 -0
  39. kafka/cli/admin/configs/common.py +17 -0
  40. kafka/cli/admin/configs/describe.py +30 -0
  41. kafka/cli/admin/configs/list.py +16 -0
  42. kafka/cli/admin/configs/reset.py +20 -0
  43. kafka/cli/admin/groups/__init__.py +16 -0
  44. kafka/cli/admin/groups/alter_offsets.py +30 -0
  45. kafka/cli/admin/groups/delete.py +11 -0
  46. kafka/cli/admin/groups/delete_offsets.py +29 -0
  47. kafka/cli/admin/groups/describe.py +11 -0
  48. kafka/cli/admin/groups/list.py +28 -0
  49. kafka/cli/admin/groups/list_offsets.py +29 -0
  50. kafka/cli/admin/groups/remove_members.py +40 -0
  51. kafka/cli/admin/groups/reset_offsets.py +139 -0
  52. kafka/cli/admin/partitions/__init__.py +21 -0
  53. kafka/cli/admin/partitions/alter_reassignments.py +37 -0
  54. kafka/cli/admin/partitions/create.py +27 -0
  55. kafka/cli/admin/partitions/delete_records.py +31 -0
  56. kafka/cli/admin/partitions/describe.py +36 -0
  57. kafka/cli/admin/partitions/elect_leaders.py +53 -0
  58. kafka/cli/admin/partitions/list_offsets.py +88 -0
  59. kafka/cli/admin/partitions/list_reassignments.py +35 -0
  60. kafka/cli/admin/topics/__init__.py +10 -0
  61. kafka/cli/admin/topics/create.py +13 -0
  62. kafka/cli/admin/topics/delete.py +19 -0
  63. kafka/cli/admin/topics/describe.py +18 -0
  64. kafka/cli/admin/topics/list.py +11 -0
  65. kafka/cli/admin/transactions/__init__.py +17 -0
  66. kafka/cli/admin/transactions/abort.py +38 -0
  67. kafka/cli/admin/transactions/describe.py +24 -0
  68. kafka/cli/admin/transactions/describe_producers.py +29 -0
  69. kafka/cli/admin/transactions/find_hanging.py +26 -0
  70. kafka/cli/admin/transactions/list.py +37 -0
  71. kafka/cli/admin/users/__init__.py +8 -0
  72. kafka/cli/admin/users/alter_user_scram_credentials.py +34 -0
  73. kafka/cli/admin/users/describe_user_scram_credentials.py +15 -0
  74. kafka/cli/common.py +95 -0
  75. kafka/cli/consumer/__init__.py +63 -0
  76. kafka/cli/producer/__init__.py +57 -0
  77. kafka/cluster.py +824 -0
  78. kafka/codec.py +325 -0
  79. kafka/consumer/__init__.py +5 -0
  80. kafka/consumer/__main__.py +5 -0
  81. kafka/consumer/fetcher.py +2012 -0
  82. kafka/consumer/group.py +1347 -0
  83. kafka/consumer/subscription_state.py +897 -0
  84. kafka/coordinator/__init__.py +0 -0
  85. kafka/coordinator/assignors/__init__.py +0 -0
  86. kafka/coordinator/assignors/abstract.py +90 -0
  87. kafka/coordinator/assignors/cooperative_sticky.py +167 -0
  88. kafka/coordinator/assignors/range.py +81 -0
  89. kafka/coordinator/assignors/roundrobin.py +101 -0
  90. kafka/coordinator/assignors/sticky/StickyAssignorUserData.json +37 -0
  91. kafka/coordinator/assignors/sticky/__init__.py +0 -0
  92. kafka/coordinator/assignors/sticky/partition_movements.py +149 -0
  93. kafka/coordinator/assignors/sticky/sorted_set.py +63 -0
  94. kafka/coordinator/assignors/sticky/sticky_assignor.py +665 -0
  95. kafka/coordinator/assignors/sticky/user_data.py +8 -0
  96. kafka/coordinator/base.py +1215 -0
  97. kafka/coordinator/consumer.py +1224 -0
  98. kafka/coordinator/heartbeat.py +82 -0
  99. kafka/coordinator/subscription.py +34 -0
  100. kafka/errors.py +1004 -0
  101. kafka/future.py +166 -0
  102. kafka/metrics/__init__.py +13 -0
  103. kafka/metrics/compound_stat.py +33 -0
  104. kafka/metrics/dict_reporter.py +81 -0
  105. kafka/metrics/kafka_metric.py +36 -0
  106. kafka/metrics/measurable.py +27 -0
  107. kafka/metrics/measurable_stat.py +13 -0
  108. kafka/metrics/metric_config.py +33 -0
  109. kafka/metrics/metric_name.py +105 -0
  110. kafka/metrics/metrics.py +261 -0
  111. kafka/metrics/metrics_reporter.py +53 -0
  112. kafka/metrics/quota.py +41 -0
  113. kafka/metrics/stat.py +19 -0
  114. kafka/metrics/stats/__init__.py +15 -0
  115. kafka/metrics/stats/avg.py +24 -0
  116. kafka/metrics/stats/count.py +17 -0
  117. kafka/metrics/stats/histogram.py +99 -0
  118. kafka/metrics/stats/max_stat.py +17 -0
  119. kafka/metrics/stats/min_stat.py +19 -0
  120. kafka/metrics/stats/percentile.py +14 -0
  121. kafka/metrics/stats/percentiles.py +75 -0
  122. kafka/metrics/stats/rate.py +118 -0
  123. kafka/metrics/stats/sampled_stat.py +99 -0
  124. kafka/metrics/stats/sensor.py +136 -0
  125. kafka/metrics/stats/total.py +15 -0
  126. kafka/net/__init__.py +19 -0
  127. kafka/net/compat.py +165 -0
  128. kafka/net/connection.py +593 -0
  129. kafka/net/http_connect.py +144 -0
  130. kafka/net/inet.py +122 -0
  131. kafka/net/manager.py +451 -0
  132. kafka/net/metrics.py +149 -0
  133. kafka/net/sasl/__init__.py +32 -0
  134. kafka/net/sasl/abc.py +28 -0
  135. kafka/net/sasl/gssapi.py +95 -0
  136. kafka/net/sasl/msk.py +245 -0
  137. kafka/net/sasl/oauth.py +98 -0
  138. kafka/net/sasl/plain.py +42 -0
  139. kafka/net/sasl/scram.py +135 -0
  140. kafka/net/sasl/sspi.py +111 -0
  141. kafka/net/selector.py +644 -0
  142. kafka/net/socks5.py +262 -0
  143. kafka/net/transport.py +415 -0
  144. kafka/net/wakeup_notifier.py +72 -0
  145. kafka/partitioner/__init__.py +8 -0
  146. kafka/partitioner/abc.py +8 -0
  147. kafka/partitioner/default.py +89 -0
  148. kafka/partitioner/sticky.py +109 -0
  149. kafka/producer/__init__.py +5 -0
  150. kafka/producer/__main__.py +5 -0
  151. kafka/producer/future.py +101 -0
  152. kafka/producer/kafka.py +1123 -0
  153. kafka/producer/producer_batch.py +192 -0
  154. kafka/producer/record_accumulator.py +647 -0
  155. kafka/producer/sender.py +884 -0
  156. kafka/producer/transaction_manager.py +1326 -0
  157. kafka/protocol/__init__.py +0 -0
  158. kafka/protocol/admin/__init__.py +29 -0
  159. kafka/protocol/admin/acl.py +83 -0
  160. kafka/protocol/admin/acl.pyi +375 -0
  161. kafka/protocol/admin/client_quotas.py +14 -0
  162. kafka/protocol/admin/client_quotas.pyi +265 -0
  163. kafka/protocol/admin/cluster.py +31 -0
  164. kafka/protocol/admin/cluster.pyi +620 -0
  165. kafka/protocol/admin/configs.py +22 -0
  166. kafka/protocol/admin/configs.pyi +437 -0
  167. kafka/protocol/admin/groups.py +24 -0
  168. kafka/protocol/admin/groups.pyi +261 -0
  169. kafka/protocol/admin/topics.py +53 -0
  170. kafka/protocol/admin/topics.pyi +982 -0
  171. kafka/protocol/admin/transactions.py +18 -0
  172. kafka/protocol/admin/transactions.pyi +311 -0
  173. kafka/protocol/admin/users.py +14 -0
  174. kafka/protocol/admin/users.pyi +223 -0
  175. kafka/protocol/api_data.py +125 -0
  176. kafka/protocol/api_header.py +55 -0
  177. kafka/protocol/api_key.py +97 -0
  178. kafka/protocol/api_message.py +277 -0
  179. kafka/protocol/broker_version_data.py +246 -0
  180. kafka/protocol/consumer/__init__.py +13 -0
  181. kafka/protocol/consumer/fetch.py +16 -0
  182. kafka/protocol/consumer/fetch.pyi +298 -0
  183. kafka/protocol/consumer/group.py +38 -0
  184. kafka/protocol/consumer/group.pyi +824 -0
  185. kafka/protocol/consumer/metadata.py +30 -0
  186. kafka/protocol/consumer/metadata.pyi +89 -0
  187. kafka/protocol/consumer/offsets.py +75 -0
  188. kafka/protocol/consumer/offsets.pyi +288 -0
  189. kafka/protocol/data_container.py +166 -0
  190. kafka/protocol/frame.py +30 -0
  191. kafka/protocol/generate_stubs.py +468 -0
  192. kafka/protocol/metadata/__init__.py +10 -0
  193. kafka/protocol/metadata/api_versions.py +41 -0
  194. kafka/protocol/metadata/api_versions.pyi +128 -0
  195. kafka/protocol/metadata/find_coordinator.py +19 -0
  196. kafka/protocol/metadata/find_coordinator.pyi +105 -0
  197. kafka/protocol/metadata/metadata.py +34 -0
  198. kafka/protocol/metadata/metadata.pyi +160 -0
  199. kafka/protocol/old/__init__.py +0 -0
  200. kafka/protocol/old/abstract.py +17 -0
  201. kafka/protocol/old/add_offsets_to_txn.py +54 -0
  202. kafka/protocol/old/add_partitions_to_txn.py +71 -0
  203. kafka/protocol/old/admin.py +1086 -0
  204. kafka/protocol/old/api.py +205 -0
  205. kafka/protocol/old/api_versions.py +133 -0
  206. kafka/protocol/old/commit.py +355 -0
  207. kafka/protocol/old/consumer_protocol.py +36 -0
  208. kafka/protocol/old/end_txn.py +53 -0
  209. kafka/protocol/old/fetch.py +408 -0
  210. kafka/protocol/old/find_coordinator.py +72 -0
  211. kafka/protocol/old/group.py +451 -0
  212. kafka/protocol/old/init_producer_id.py +42 -0
  213. kafka/protocol/old/list_offsets.py +186 -0
  214. kafka/protocol/old/metadata.py +290 -0
  215. kafka/protocol/old/offset_for_leader_epoch.py +133 -0
  216. kafka/protocol/old/produce.py +247 -0
  217. kafka/protocol/old/sasl_authenticate.py +38 -0
  218. kafka/protocol/old/sasl_handshake.py +39 -0
  219. kafka/protocol/old/struct.py +87 -0
  220. kafka/protocol/old/txn_offset_commit.py +73 -0
  221. kafka/protocol/old/types.py +440 -0
  222. kafka/protocol/parser.py +191 -0
  223. kafka/protocol/producer/__init__.py +7 -0
  224. kafka/protocol/producer/produce.py +17 -0
  225. kafka/protocol/producer/produce.pyi +197 -0
  226. kafka/protocol/producer/transaction.py +30 -0
  227. kafka/protocol/producer/transaction.pyi +663 -0
  228. kafka/protocol/sasl.py +52 -0
  229. kafka/protocol/sasl.pyi +126 -0
  230. kafka/protocol/schemas/__init__.py +7 -0
  231. kafka/protocol/schemas/fields/__init__.py +7 -0
  232. kafka/protocol/schemas/fields/array.py +127 -0
  233. kafka/protocol/schemas/fields/base.py +156 -0
  234. kafka/protocol/schemas/fields/codecs/__init__.py +12 -0
  235. kafka/protocol/schemas/fields/codecs/encode_buffer.py +82 -0
  236. kafka/protocol/schemas/fields/codecs/tagged_fields.py +109 -0
  237. kafka/protocol/schemas/fields/codecs/types.py +505 -0
  238. kafka/protocol/schemas/fields/codegen.py +40 -0
  239. kafka/protocol/schemas/fields/simple.py +127 -0
  240. kafka/protocol/schemas/fields/struct.py +357 -0
  241. kafka/protocol/schemas/fields/struct_array.py +142 -0
  242. kafka/protocol/schemas/load_json.py +42 -0
  243. kafka/protocol/schemas/resources/AddOffsetsToTxnRequest.json +40 -0
  244. kafka/protocol/schemas/resources/AddOffsetsToTxnResponse.json +35 -0
  245. kafka/protocol/schemas/resources/AddPartitionsToTxnRequest.json +65 -0
  246. kafka/protocol/schemas/resources/AddPartitionsToTxnResponse.json +60 -0
  247. kafka/protocol/schemas/resources/AlterClientQuotasRequest.json +47 -0
  248. kafka/protocol/schemas/resources/AlterClientQuotasResponse.json +41 -0
  249. kafka/protocol/schemas/resources/AlterConfigsRequest.json +43 -0
  250. kafka/protocol/schemas/resources/AlterConfigsResponse.json +39 -0
  251. kafka/protocol/schemas/resources/AlterPartitionReassignmentsRequest.json +42 -0
  252. kafka/protocol/schemas/resources/AlterPartitionReassignmentsResponse.json +47 -0
  253. kafka/protocol/schemas/resources/AlterReplicaLogDirsRequest.json +41 -0
  254. kafka/protocol/schemas/resources/AlterReplicaLogDirsResponse.json +41 -0
  255. kafka/protocol/schemas/resources/AlterUserScramCredentialsRequest.json +45 -0
  256. kafka/protocol/schemas/resources/AlterUserScramCredentialsResponse.json +35 -0
  257. kafka/protocol/schemas/resources/ApiVersionsRequest.json +34 -0
  258. kafka/protocol/schemas/resources/ApiVersionsResponse.json +79 -0
  259. kafka/protocol/schemas/resources/ConsumerProtocolAssignment.json +42 -0
  260. kafka/protocol/schemas/resources/ConsumerProtocolSubscription.json +49 -0
  261. kafka/protocol/schemas/resources/CreateAclsRequest.json +46 -0
  262. kafka/protocol/schemas/resources/CreateAclsResponse.json +37 -0
  263. kafka/protocol/schemas/resources/CreatePartitionsRequest.json +47 -0
  264. kafka/protocol/schemas/resources/CreatePartitionsResponse.json +41 -0
  265. kafka/protocol/schemas/resources/CreateTopicsRequest.json +65 -0
  266. kafka/protocol/schemas/resources/CreateTopicsResponse.json +72 -0
  267. kafka/protocol/schemas/resources/DeleteAclsRequest.json +46 -0
  268. kafka/protocol/schemas/resources/DeleteAclsResponse.json +59 -0
  269. kafka/protocol/schemas/resources/DeleteGroupsRequest.json +30 -0
  270. kafka/protocol/schemas/resources/DeleteGroupsResponse.json +36 -0
  271. kafka/protocol/schemas/resources/DeleteRecordsRequest.json +42 -0
  272. kafka/protocol/schemas/resources/DeleteRecordsResponse.json +43 -0
  273. kafka/protocol/schemas/resources/DeleteTopicsRequest.json +43 -0
  274. kafka/protocol/schemas/resources/DeleteTopicsResponse.json +52 -0
  275. kafka/protocol/schemas/resources/DescribeAclsRequest.json +43 -0
  276. kafka/protocol/schemas/resources/DescribeAclsResponse.json +55 -0
  277. kafka/protocol/schemas/resources/DescribeClientQuotasRequest.json +37 -0
  278. kafka/protocol/schemas/resources/DescribeClientQuotasResponse.json +47 -0
  279. kafka/protocol/schemas/resources/DescribeClusterRequest.json +35 -0
  280. kafka/protocol/schemas/resources/DescribeClusterResponse.json +56 -0
  281. kafka/protocol/schemas/resources/DescribeConfigsRequest.json +42 -0
  282. kafka/protocol/schemas/resources/DescribeConfigsResponse.json +69 -0
  283. kafka/protocol/schemas/resources/DescribeGroupsRequest.json +38 -0
  284. kafka/protocol/schemas/resources/DescribeGroupsResponse.json +74 -0
  285. kafka/protocol/schemas/resources/DescribeLogDirsRequest.json +38 -0
  286. kafka/protocol/schemas/resources/DescribeLogDirsResponse.json +65 -0
  287. kafka/protocol/schemas/resources/DescribeProducersRequest.json +32 -0
  288. kafka/protocol/schemas/resources/DescribeProducersResponse.json +55 -0
  289. kafka/protocol/schemas/resources/DescribeQuorumRequest.json +39 -0
  290. kafka/protocol/schemas/resources/DescribeQuorumResponse.json +82 -0
  291. kafka/protocol/schemas/resources/DescribeTopicPartitionsRequest.json +40 -0
  292. kafka/protocol/schemas/resources/DescribeTopicPartitionsResponse.json +66 -0
  293. kafka/protocol/schemas/resources/DescribeTransactionsRequest.json +27 -0
  294. kafka/protocol/schemas/resources/DescribeTransactionsResponse.json +52 -0
  295. kafka/protocol/schemas/resources/DescribeUserScramCredentialsRequest.json +30 -0
  296. kafka/protocol/schemas/resources/DescribeUserScramCredentialsResponse.json +45 -0
  297. kafka/protocol/schemas/resources/ElectLeadersRequest.json +41 -0
  298. kafka/protocol/schemas/resources/ElectLeadersResponse.json +45 -0
  299. kafka/protocol/schemas/resources/EndTxnRequest.json +43 -0
  300. kafka/protocol/schemas/resources/EndTxnResponse.json +41 -0
  301. kafka/protocol/schemas/resources/FetchRequest.json +125 -0
  302. kafka/protocol/schemas/resources/FetchResponse.json +124 -0
  303. kafka/protocol/schemas/resources/FindCoordinatorRequest.json +43 -0
  304. kafka/protocol/schemas/resources/FindCoordinatorResponse.json +58 -0
  305. kafka/protocol/schemas/resources/HeartbeatRequest.json +39 -0
  306. kafka/protocol/schemas/resources/HeartbeatResponse.json +35 -0
  307. kafka/protocol/schemas/resources/IncrementalAlterConfigsRequest.json +44 -0
  308. kafka/protocol/schemas/resources/IncrementalAlterConfigsResponse.json +38 -0
  309. kafka/protocol/schemas/resources/InitProducerIdRequest.json +50 -0
  310. kafka/protocol/schemas/resources/InitProducerIdResponse.json +47 -0
  311. kafka/protocol/schemas/resources/JoinGroupRequest.json +63 -0
  312. kafka/protocol/schemas/resources/JoinGroupResponse.json +69 -0
  313. kafka/protocol/schemas/resources/LeaveGroupRequest.json +47 -0
  314. kafka/protocol/schemas/resources/LeaveGroupResponse.json +47 -0
  315. kafka/protocol/schemas/resources/ListConfigResourcesRequest.json +31 -0
  316. kafka/protocol/schemas/resources/ListConfigResourcesResponse.json +37 -0
  317. kafka/protocol/schemas/resources/ListGroupsRequest.json +36 -0
  318. kafka/protocol/schemas/resources/ListGroupsResponse.json +49 -0
  319. kafka/protocol/schemas/resources/ListOffsetsRequest.json +72 -0
  320. kafka/protocol/schemas/resources/ListOffsetsResponse.json +71 -0
  321. kafka/protocol/schemas/resources/ListPartitionReassignmentsRequest.json +34 -0
  322. kafka/protocol/schemas/resources/ListPartitionReassignmentsResponse.json +46 -0
  323. kafka/protocol/schemas/resources/ListTransactionsRequest.json +40 -0
  324. kafka/protocol/schemas/resources/ListTransactionsResponse.json +42 -0
  325. kafka/protocol/schemas/resources/MetadataRequest.json +56 -0
  326. kafka/protocol/schemas/resources/MetadataResponse.json +101 -0
  327. kafka/protocol/schemas/resources/OffsetCommitRequest.json +76 -0
  328. kafka/protocol/schemas/resources/OffsetCommitResponse.json +71 -0
  329. kafka/protocol/schemas/resources/OffsetDeleteRequest.json +39 -0
  330. kafka/protocol/schemas/resources/OffsetDeleteResponse.json +42 -0
  331. kafka/protocol/schemas/resources/OffsetFetchRequest.json +76 -0
  332. kafka/protocol/schemas/resources/OffsetFetchResponse.json +107 -0
  333. kafka/protocol/schemas/resources/OffsetForLeaderEpochRequest.json +52 -0
  334. kafka/protocol/schemas/resources/OffsetForLeaderEpochResponse.json +51 -0
  335. kafka/protocol/schemas/resources/ProduceRequest.json +73 -0
  336. kafka/protocol/schemas/resources/ProduceResponse.json +96 -0
  337. kafka/protocol/schemas/resources/RequestHeader.json +44 -0
  338. kafka/protocol/schemas/resources/ResponseHeader.json +26 -0
  339. kafka/protocol/schemas/resources/SaslAuthenticateRequest.json +29 -0
  340. kafka/protocol/schemas/resources/SaslAuthenticateResponse.json +34 -0
  341. kafka/protocol/schemas/resources/SaslHandshakeRequest.json +31 -0
  342. kafka/protocol/schemas/resources/SaslHandshakeResponse.json +32 -0
  343. kafka/protocol/schemas/resources/SyncGroupRequest.json +56 -0
  344. kafka/protocol/schemas/resources/SyncGroupResponse.json +46 -0
  345. kafka/protocol/schemas/resources/TxnOffsetCommitRequest.json +68 -0
  346. kafka/protocol/schemas/resources/TxnOffsetCommitResponse.json +47 -0
  347. kafka/protocol/schemas/resources/UpdateFeaturesRequest.json +43 -0
  348. kafka/protocol/schemas/resources/UpdateFeaturesResponse.json +39 -0
  349. kafka/protocol/schemas/resources/WriteTxnMarkersRequest.json +49 -0
  350. kafka/protocol/schemas/resources/WriteTxnMarkersResponse.json +45 -0
  351. kafka/protocol/schemas/resources/__init__.py +0 -0
  352. kafka/record/__init__.py +3 -0
  353. kafka/record/_crc32c.py +161 -0
  354. kafka/record/abc.py +144 -0
  355. kafka/record/default_records.py +782 -0
  356. kafka/record/legacy_records.py +587 -0
  357. kafka/record/memory_records.py +255 -0
  358. kafka/record/util.py +135 -0
  359. kafka/serializer/__init__.py +4 -0
  360. kafka/serializer/abstract.py +20 -0
  361. kafka/serializer/default.py +16 -0
  362. kafka/serializer/json.py +17 -0
  363. kafka/serializer/wrapper.py +21 -0
  364. kafka/structs.py +69 -0
  365. kafka/util.py +159 -0
  366. kafka/vendor/__init__.py +0 -0
  367. kafka/version.py +1 -0
  368. kafka_python-3.0.0.dist-info/METADATA +319 -0
  369. kafka_python-3.0.0.dist-info/RECORD +373 -0
  370. kafka_python-3.0.0.dist-info/WHEEL +5 -0
  371. kafka_python-3.0.0.dist-info/entry_points.txt +2 -0
  372. kafka_python-3.0.0.dist-info/licenses/LICENSE +202 -0
  373. kafka_python-3.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1326 @@
1
+ from abc import ABC, abstractmethod, abstractproperty
2
+ import collections
3
+ from enum import IntEnum
4
+ import heapq
5
+ import logging
6
+ import threading
7
+
8
+ import kafka.errors as Errors
9
+ from kafka.protocol.metadata import FindCoordinatorRequest, CoordinatorType
10
+ from kafka.protocol.producer import (
11
+ AddOffsetsToTxnRequest, AddPartitionsToTxnRequest,
12
+ EndTxnRequest, InitProducerIdRequest, TxnOffsetCommitRequest,
13
+ )
14
+ from kafka.structs import ConsumerGroupMetadata, TopicPartition
15
+
16
+
17
+ log = logging.getLogger(__name__)
18
+
19
+
20
+ NO_PRODUCER_ID = -1
21
+ NO_PRODUCER_EPOCH = -1
22
+ NO_SEQUENCE = -1
23
+
24
+
25
+ class ProducerIdAndEpoch:
26
+ __slots__ = ('producer_id', 'epoch')
27
+
28
+ def __init__(self, producer_id, epoch):
29
+ self.producer_id = producer_id
30
+ self.epoch = epoch
31
+
32
+ @property
33
+ def is_valid(self):
34
+ return NO_PRODUCER_ID < self.producer_id
35
+
36
+ def match(self, batch):
37
+ return self.producer_id == batch.producer_id and self.epoch == batch.producer_epoch
38
+
39
+ def __eq__(self, other):
40
+ return isinstance(other, ProducerIdAndEpoch) and self.producer_id == other.producer_id and self.epoch == other.epoch
41
+
42
+ def __str__(self):
43
+ return "ProducerIdAndEpoch(producer_id={}, epoch={})".format(self.producer_id, self.epoch)
44
+
45
+
46
+ class TransactionState(IntEnum):
47
+ UNINITIALIZED = 0
48
+ INITIALIZING = 1
49
+ READY = 2
50
+ IN_TRANSACTION = 3
51
+ COMMITTING_TRANSACTION = 4
52
+ ABORTING_TRANSACTION = 5
53
+ ABORTABLE_ERROR = 6
54
+ FATAL_ERROR = 7
55
+ # KIP-360: intermediate state entered when a recoverable sequence-related
56
+ # error is encountered. The producer sends an InitProducerIdRequest v3+
57
+ # with its current producer_id/epoch to bump the epoch, then transitions
58
+ # back to READY on success. Records in the accumulator will be sent under
59
+ # the bumped epoch with fresh sequence numbers. In-flight batches at the
60
+ # moment of the bump are lost (their futures fail). (TODO re KAFKA-5793)
61
+ BUMPING_PRODUCER_EPOCH = 8
62
+
63
+ @classmethod
64
+ def is_transition_valid(cls, source, target):
65
+ if target == cls.INITIALIZING:
66
+ return source in (cls.UNINITIALIZED, cls.BUMPING_PRODUCER_EPOCH)
67
+ elif target == cls.READY:
68
+ return source in (cls.INITIALIZING, cls.COMMITTING_TRANSACTION,
69
+ cls.ABORTING_TRANSACTION, cls.BUMPING_PRODUCER_EPOCH)
70
+ elif target == cls.IN_TRANSACTION:
71
+ return source == cls.READY
72
+ elif target == cls.COMMITTING_TRANSACTION:
73
+ return source == cls.IN_TRANSACTION
74
+ elif target == cls.ABORTING_TRANSACTION:
75
+ return source in (cls.IN_TRANSACTION, cls.ABORTABLE_ERROR)
76
+ elif target == cls.ABORTABLE_ERROR:
77
+ return source in (cls.IN_TRANSACTION, cls.COMMITTING_TRANSACTION,
78
+ cls.ABORTABLE_ERROR, cls.BUMPING_PRODUCER_EPOCH)
79
+ elif target == cls.BUMPING_PRODUCER_EPOCH:
80
+ # A recoverable sequence-related error can arrive at any point in
81
+ # the producer's lifetime; the bump is a unilateral recovery
82
+ # action. Disallow only from UNINITIALIZED (no producer_id yet
83
+ # to bump) and the terminal error states.
84
+ return source in (cls.READY, cls.IN_TRANSACTION,
85
+ cls.COMMITTING_TRANSACTION, cls.ABORTING_TRANSACTION,
86
+ cls.ABORTABLE_ERROR)
87
+ elif target == cls.UNINITIALIZED:
88
+ # Disallow transitions to UNITIALIZED
89
+ return False
90
+ elif target == cls.FATAL_ERROR:
91
+ # We can transition to FATAL_ERROR unconditionally.
92
+ # FATAL_ERROR is never a valid starting state for any transition. So the only option is to close the
93
+ # producer or do purely non transactional requests.
94
+ return True
95
+
96
+
97
+ class Priority(IntEnum):
98
+ # We use the priority to determine the order in which requests need to be sent out. For instance, if we have
99
+ # a pending FindCoordinator request, that must always go first. Next, If we need a producer id, that must go second.
100
+ # The endTxn request must always go last.
101
+ FIND_COORDINATOR = 0
102
+ INIT_PRODUCER_ID = 1
103
+ ADD_PARTITIONS_OR_OFFSETS = 2
104
+ END_TXN = 3
105
+
106
+
107
+ class TransactionManager:
108
+ """
109
+ A class which maintains state for transactions. Also keeps the state necessary to ensure idempotent production.
110
+ """
111
+ NO_INFLIGHT_REQUEST_CORRELATION_ID = -1
112
+ # The retry_backoff_ms is overridden to the following value if the first AddPartitions receives a
113
+ # CONCURRENT_TRANSACTIONS error.
114
+ ADD_PARTITIONS_RETRY_BACKOFF_MS = 20
115
+
116
+ def __init__(self, transactional_id=None, transaction_timeout_ms=0, retry_backoff_ms=100, api_version=(0, 11), metadata=None):
117
+ self._api_version = api_version
118
+ self._metadata = metadata
119
+
120
+ self._sequence_numbers = collections.defaultdict(lambda: 0)
121
+ # The offset of the last ack'd record for each partition. Used to
122
+ # distinguish retention-based UnknownProducerIdError (broker's
123
+ # log_start_offset > last_acked_offset -> safe to reset and retry)
124
+ # from actual data loss. See KAFKA-5793.
125
+ self._last_acked_offset = {}
126
+
127
+ self.transactional_id = transactional_id
128
+ self.transaction_timeout_ms = transaction_timeout_ms
129
+ self._transaction_coordinator = None
130
+ self._consumer_group_coordinator = None
131
+ self._new_partitions_in_transaction = set()
132
+ self._pending_partitions_in_transaction = set()
133
+ self._partitions_in_transaction = set()
134
+ self._pending_txn_offset_commits = dict()
135
+
136
+ self._current_state = TransactionState.UNINITIALIZED
137
+ self._last_error = None
138
+ self.producer_id_and_epoch = ProducerIdAndEpoch(NO_PRODUCER_ID, NO_PRODUCER_EPOCH)
139
+
140
+ self._transaction_started = False
141
+
142
+ self._pending_requests = [] # priority queue via heapq
143
+ self._pending_requests_sort_id = 0
144
+ self._in_flight_request_correlation_id = self.NO_INFLIGHT_REQUEST_CORRELATION_ID
145
+
146
+ # This is used by the TxnRequestHandlers to control how long to back off before a given request is retried.
147
+ # For instance, this value is lowered by the AddPartitionsToTxnHandler when it receives a CONCURRENT_TRANSACTIONS
148
+ # error for the first AddPartitionsRequest in a transaction.
149
+ self.retry_backoff_ms = retry_backoff_ms
150
+ self._lock = threading.Condition()
151
+
152
+ def initialize_transactions(self):
153
+ with self._lock:
154
+ self._ensure_transactional()
155
+ self._transition_to(TransactionState.INITIALIZING)
156
+ self.set_producer_id_and_epoch(ProducerIdAndEpoch(NO_PRODUCER_ID, NO_PRODUCER_EPOCH))
157
+ self._sequence_numbers.clear()
158
+ handler = InitProducerIdHandler(self, self.transaction_timeout_ms)
159
+ self._enqueue_request(handler)
160
+ return handler.result
161
+
162
+ def init_producer_id(self):
163
+ """Idempotent (non-transactional) producer: enqueue an InitProducerIdHandler.
164
+
165
+ Drives UNINITIALIZED -> INITIALIZING; the handler completes the
166
+ transition to READY on success. No-op outside UNINITIALIZED so
167
+ repeated calls from the sender's run loop are safe.
168
+ """
169
+ with self._lock:
170
+ if self.is_transactional():
171
+ raise Errors.IllegalStateError(
172
+ "init_producer_id is for idempotent (non-transactional) producers;"
173
+ " use initialize_transactions for transactional producers")
174
+ if self._current_state != TransactionState.UNINITIALIZED:
175
+ return
176
+ self._transition_to(TransactionState.INITIALIZING)
177
+ self.set_producer_id_and_epoch(ProducerIdAndEpoch(NO_PRODUCER_ID, NO_PRODUCER_EPOCH))
178
+ self._sequence_numbers.clear()
179
+ handler = InitProducerIdHandler(self, 0)
180
+ self._enqueue_request(handler)
181
+
182
+ def begin_transaction(self):
183
+ with self._lock:
184
+ self._ensure_transactional()
185
+ self._maybe_fail_with_error()
186
+ self._transition_to(TransactionState.IN_TRANSACTION)
187
+
188
+ def begin_commit(self):
189
+ with self._lock:
190
+ self._ensure_transactional()
191
+ self._maybe_fail_with_error()
192
+ self._transition_to(TransactionState.COMMITTING_TRANSACTION)
193
+ return self._begin_completing_transaction(True)
194
+
195
+ def begin_abort(self):
196
+ with self._lock:
197
+ self._ensure_transactional()
198
+ if self._current_state != TransactionState.ABORTABLE_ERROR:
199
+ self._maybe_fail_with_error()
200
+ self._transition_to(TransactionState.ABORTING_TRANSACTION)
201
+
202
+ # We're aborting the transaction, so there should be no need to add new partitions
203
+ self._new_partitions_in_transaction.clear()
204
+ return self._begin_completing_transaction(False)
205
+
206
+ def _begin_completing_transaction(self, committed):
207
+ if self._new_partitions_in_transaction:
208
+ self._enqueue_request(self._add_partitions_to_transaction_handler())
209
+ handler = EndTxnHandler(self, committed)
210
+ self._enqueue_request(handler)
211
+ return handler.result
212
+
213
+ def send_offsets_to_transaction(self, offsets, group_metadata):
214
+ """Send consumer-group offsets as part of the current transaction.
215
+
216
+ Arguments:
217
+ offsets ({TopicPartition: OffsetAndMetadata}): offsets to commit.
218
+ group_metadata (ConsumerGroupMetadata or str): full group metadata
219
+ from KafkaConsumer.group_metadata() (preferred - enables
220
+ broker-side fencing per KIP-447), or a bare group_id string
221
+ for backwards compatibility (broker treats it as v0-v2).
222
+
223
+ Returns:
224
+ FutureRecordMetadata-style Future that completes once the offsets
225
+ are durably committed (or fails fatally).
226
+ """
227
+ if isinstance(group_metadata, str):
228
+ group_metadata = ConsumerGroupMetadata(group_id=group_metadata)
229
+ elif not isinstance(group_metadata, ConsumerGroupMetadata):
230
+ raise TypeError(
231
+ "send_offsets_to_transaction expects group_metadata to be a "
232
+ "ConsumerGroupMetadata or a group_id str, got %r" % (type(group_metadata),))
233
+
234
+ if group_metadata.generation_id > 0 and not group_metadata.member_id:
235
+ raise ValueError(
236
+ "Invalid ConsumerGroupMetadata: generation_id=%s implies a"
237
+ " joined group but member_id is empty" % (group_metadata.generation_id,))
238
+
239
+ with self._lock:
240
+ self._ensure_transactional()
241
+ self._maybe_fail_with_error()
242
+ if self._current_state != TransactionState.IN_TRANSACTION:
243
+ raise Errors.KafkaError("Cannot send offsets to transaction because the producer is not in an active transaction")
244
+
245
+ log.debug("Begin adding offsets %s for consumer group %s to transaction", offsets, group_metadata.group_id)
246
+ handler = AddOffsetsToTxnHandler(self, group_metadata, offsets)
247
+ self._enqueue_request(handler)
248
+ return handler.result
249
+
250
+ def maybe_add_partition_to_transaction(self, topic_partition):
251
+ with self._lock:
252
+ self._fail_if_not_ready_for_send()
253
+
254
+ if self.is_partition_added(topic_partition) or self.is_partition_pending_add(topic_partition):
255
+ return
256
+
257
+ log.debug("Begin adding new partition %s to transaction", topic_partition)
258
+ self._new_partitions_in_transaction.add(topic_partition)
259
+
260
+ def _fail_if_not_ready_for_send(self):
261
+ with self._lock:
262
+ if self.has_error():
263
+ raise Errors.KafkaError(
264
+ "Cannot perform send because at least one previous transactional or"
265
+ " idempotent request has failed with errors.", self._last_error)
266
+
267
+ if self.is_transactional():
268
+ if not self.has_producer_id():
269
+ raise Errors.IllegalStateError(
270
+ "Cannot perform a 'send' before completing a call to init_transactions"
271
+ " when transactions are enabled.")
272
+
273
+ if self._current_state != TransactionState.IN_TRANSACTION:
274
+ raise Errors.IllegalStateError("Cannot call send in state %s" % (self._current_state.name,))
275
+
276
+ def is_send_to_partition_allowed(self, tp):
277
+ with self._lock:
278
+ if self.has_fatal_error():
279
+ return False
280
+ return not self.is_transactional() or tp in self._partitions_in_transaction
281
+
282
+ def has_producer_id(self, producer_id=None):
283
+ if producer_id is None:
284
+ return self.producer_id_and_epoch.is_valid
285
+ else:
286
+ return self.producer_id_and_epoch.producer_id == producer_id
287
+
288
+ def is_transactional(self):
289
+ return self.transactional_id is not None
290
+
291
+ def has_partitions_to_add(self):
292
+ with self._lock:
293
+ return bool(self._new_partitions_in_transaction) or bool(self._pending_partitions_in_transaction)
294
+
295
+ def is_completing(self):
296
+ with self._lock:
297
+ return self._current_state in (
298
+ TransactionState.COMMITTING_TRANSACTION,
299
+ TransactionState.ABORTING_TRANSACTION)
300
+
301
+ @property
302
+ def last_error(self):
303
+ return self._last_error
304
+
305
+ def has_error(self):
306
+ with self._lock:
307
+ return self._current_state in (
308
+ TransactionState.ABORTABLE_ERROR,
309
+ TransactionState.FATAL_ERROR)
310
+
311
+ def is_bumping_epoch(self):
312
+ with self._lock:
313
+ return self._current_state == TransactionState.BUMPING_PRODUCER_EPOCH
314
+
315
+ # KIP-360 error classification
316
+ #
317
+ # Errors whose correct recovery is to bump the producer epoch via
318
+ # InitProducerIdRequest v3+. On brokers that do not support the bump
319
+ # (api_version < 2.5) these degrade to FATAL for transactional producers
320
+ # and NEEDS_PRODUCER_ID_RESET for non-transactional idempotent producers,
321
+ # matching the pre-KIP-360 behavior.
322
+ _NEEDS_EPOCH_BUMP_ERRORS = frozenset({
323
+ Errors.OutOfOrderSequenceNumberError,
324
+ Errors.UnknownProducerIdError,
325
+ Errors.InvalidProducerEpochError,
326
+ })
327
+
328
+ # Errors that are always fatal regardless of broker version: auth
329
+ # failures, fencing, or structural state corruption where no recovery
330
+ # is possible without operator action.
331
+ _FATAL_ERRORS = frozenset({
332
+ Errors.ClusterAuthorizationFailedError,
333
+ Errors.TransactionalIdAuthorizationFailedError,
334
+ Errors.ProducerFencedError,
335
+ Errors.InvalidTxnStateError,
336
+ })
337
+
338
+ # Classification outcomes returned by classify_batch_error().
339
+ ERROR_CLASS_RETRIABLE = 'RETRIABLE'
340
+ ERROR_CLASS_ABORTABLE = 'ABORTABLE'
341
+ ERROR_CLASS_FATAL = 'FATAL'
342
+ ERROR_CLASS_NEEDS_EPOCH_BUMP = 'NEEDS_EPOCH_BUMP'
343
+ ERROR_CLASS_NEEDS_PRODUCER_ID_RESET = 'NEEDS_PRODUCER_ID_RESET'
344
+
345
+ def _supports_epoch_bump(self):
346
+ """Return True if the broker supports InitProducerIdRequest v3+ (KIP-360).
347
+
348
+ KIP-360 landed in Kafka 2.5. On older brokers we fall back to the
349
+ pre-KIP-360 recovery: reset producer id for idempotent producers,
350
+ fatal state for transactional producers.
351
+ """
352
+ return self._api_version >= (2, 5)
353
+
354
+ def classify_batch_error(self, error, batch, log_start_offset=-1):
355
+ """Categorize a batch-completion error into a recovery outcome.
356
+
357
+ Used by the Sender to decide what to do with a failed batch. This
358
+ method does not mutate any state - it is a pure classification
359
+ helper. The caller is responsible for dispatching to the
360
+ appropriate recovery path.
361
+
362
+ Arguments:
363
+ error (type or BaseException): The error class or instance.
364
+ batch (ProducerBatch): The batch that failed.
365
+ log_start_offset (int): log_start_offset from the broker's
366
+ PartitionProduceResponse, or -1 if unknown / client-side
367
+ failure. Used for KAFKA-5793 retention detection.
368
+
369
+ Returns one of:
370
+ ERROR_CLASS_RETRIABLE - caller should retry the batch
371
+ ERROR_CLASS_ABORTABLE - transactional producer only;
372
+ abort the transaction
373
+ ERROR_CLASS_FATAL - unrecoverable; transition to
374
+ fatal error and fail the batch
375
+ ERROR_CLASS_NEEDS_EPOCH_BUMP - recoverable via KIP-360 epoch
376
+ bump (only when broker supports
377
+ InitProducerIdRequest v3+)
378
+ ERROR_CLASS_NEEDS_PRODUCER_ID_RESET - non-transactional pre-KIP-360
379
+ fallback: reset the
380
+ producer id entirely
381
+
382
+ Note: this classification is for transactional/idempotent producers
383
+ only. Non-idempotent producers don't call this; the Sender uses
384
+ simpler retry/fail logic for them.
385
+ """
386
+ error_type = error if isinstance(error, type) else type(error)
387
+
388
+ if error_type in self._FATAL_ERRORS:
389
+ return self.ERROR_CLASS_FATAL
390
+
391
+ # KAFKA-5793: a retention-based UnknownProducerIdError is recoverable
392
+ # by resetting the partition's sequence (not a full epoch bump). The
393
+ # Sender checks this condition separately before consulting this
394
+ # classifier, but we mirror the logic here so the classifier alone
395
+ # gives the correct answer for callers that pass log_start_offset.
396
+ if error_type is Errors.UnknownProducerIdError and log_start_offset is not None and log_start_offset >= 0:
397
+ last_acked = self.last_acked_offset(batch.topic_partition)
398
+ if log_start_offset > last_acked:
399
+ return self.ERROR_CLASS_RETRIABLE
400
+
401
+ if error_type in self._NEEDS_EPOCH_BUMP_ERRORS:
402
+ if self._supports_epoch_bump():
403
+ return self.ERROR_CLASS_NEEDS_EPOCH_BUMP
404
+ # Pre-KIP-360 brokers: fall back to the older (lossier) recovery.
405
+ if self.is_transactional():
406
+ return self.ERROR_CLASS_FATAL
407
+ return self.ERROR_CLASS_NEEDS_PRODUCER_ID_RESET
408
+
409
+ # Retriable errors (broker-retriable or client connection errors)
410
+ # become ABORTABLE for transactional producers only if they're
411
+ # non-retriable AND we're in a transaction. The Sender's existing
412
+ # can_retry/can_split logic handles the actual retry decision; this
413
+ # classifier is only consulted for the FAIL branch.
414
+ if issubclass(error_type, Errors.RetriableError):
415
+ return self.ERROR_CLASS_RETRIABLE
416
+
417
+ # Non-retriable, not in the bump or fatal sets: transactional
418
+ # producers should abort the current transaction; non-transactional
419
+ # idempotent producers just fail the batch without any state reset.
420
+ if self.is_transactional():
421
+ return self.ERROR_CLASS_ABORTABLE
422
+ return self.ERROR_CLASS_FATAL
423
+
424
+ def is_aborting(self):
425
+ with self._lock:
426
+ return self._current_state == TransactionState.ABORTING_TRANSACTION
427
+
428
+ def transition_to_abortable_error(self, exc):
429
+ with self._lock:
430
+ if self._current_state == TransactionState.ABORTING_TRANSACTION:
431
+ log.debug("Skipping transition to abortable error state since the transaction is already being "
432
+ " aborted. Underlying exception: %s", exc)
433
+ return
434
+ self._transition_to(TransactionState.ABORTABLE_ERROR, error=exc)
435
+
436
+ def transition_to_fatal_error(self, exc):
437
+ with self._lock:
438
+ self._transition_to(TransactionState.FATAL_ERROR, error=exc)
439
+
440
+ def is_partition_added(self, partition):
441
+ with self._lock:
442
+ return partition in self._partitions_in_transaction
443
+
444
+ def is_partition_pending_add(self, partition):
445
+ return partition in self._new_partitions_in_transaction or partition in self._pending_partitions_in_transaction
446
+
447
+ def has_producer_id_and_epoch(self, producer_id, producer_epoch):
448
+ return (
449
+ self.producer_id_and_epoch.producer_id == producer_id and
450
+ self.producer_id_and_epoch.epoch == producer_epoch
451
+ )
452
+
453
+ def set_producer_id_and_epoch(self, producer_id_and_epoch):
454
+ if not isinstance(producer_id_and_epoch, ProducerIdAndEpoch):
455
+ raise TypeError("ProducerAndIdEpoch type required")
456
+ log.info("ProducerId set to %s with epoch %s",
457
+ producer_id_and_epoch.producer_id, producer_id_and_epoch.epoch)
458
+ self.producer_id_and_epoch = producer_id_and_epoch
459
+
460
+ def reset_producer_id(self):
461
+ """
462
+ This method is used when the producer needs to reset its internal state because of an irrecoverable exception
463
+ from the broker.
464
+
465
+ We need to reset the producer id and associated state when we have sent a batch to the broker, but we either get
466
+ a non-retriable exception or we run out of retries, or the batch expired in the producer queue after it was already
467
+ sent to the broker.
468
+
469
+ In all of these cases, we don't know whether batch was actually committed on the broker, and hence whether the
470
+ sequence number was actually updated. If we don't reset the producer state, we risk the chance that all future
471
+ messages will return an OutOfOrderSequenceNumberError.
472
+
473
+ Note that we can't reset the producer state for the transactional producer as this would mean bumping the epoch
474
+ for the same producer id. This might involve aborting the ongoing transaction during the initProducerIdRequest,
475
+ and the user would not have any way of knowing this happened. So for the transactional producer,
476
+ it's best to return the produce error to the user and let them abort the transaction and close the producer explicitly.
477
+ """
478
+ with self._lock:
479
+ if self.is_transactional():
480
+ raise Errors.IllegalStateError(
481
+ "Cannot reset producer state for a transactional producer."
482
+ " You must either abort the ongoing transaction or"
483
+ " reinitialize the transactional producer instead")
484
+ self.set_producer_id_and_epoch(ProducerIdAndEpoch(NO_PRODUCER_ID, NO_PRODUCER_EPOCH))
485
+ self._sequence_numbers.clear()
486
+ self._last_acked_offset.clear()
487
+
488
+ def bump_producer_id_and_epoch(self):
489
+ """KIP-360: recover from a transient producer-state error by bumping
490
+ the epoch.
491
+
492
+ Transitions to BUMPING_PRODUCER_EPOCH and enqueues an
493
+ InitProducerIdRequest v3+ carrying the current producer_id/epoch.
494
+ When the broker responds with the bumped epoch, _complete_epoch_bump
495
+ transitions back to READY and the sender resumes producing under
496
+ the new epoch. Records in the accumulator that haven't been drained
497
+ yet will be stamped with the new epoch on the next drain.
498
+
499
+ TODO (KAFKA-5793 full): in-flight batches at the moment of the bump
500
+ are lost--their futures fail. Adding in-place rewrite of the
501
+ closed batch buffer (producer_id/epoch/base_sequence fields + CRC
502
+ recompute) would let us retry them under the new epoch without
503
+ losing records.
504
+
505
+ Requires broker >= 2.5 (InitProducerIdRequest v3+). On older
506
+ brokers, Sender falls back to reset_producer_id / fatal instead
507
+ via classify_batch_error.
508
+
509
+ Idempotent: if we're already in BUMPING_PRODUCER_EPOCH, this is a
510
+ no-op. This matters because with max_in_flight > 1, multiple
511
+ in-flight batches may all fail with the same epoch-bump-triggering
512
+ error in quick succession; only the first should drive the bump.
513
+ """
514
+ with self._lock:
515
+ if self._current_state == TransactionState.BUMPING_PRODUCER_EPOCH:
516
+ return
517
+ if self._current_state == TransactionState.FATAL_ERROR:
518
+ return
519
+ if not self._supports_epoch_bump():
520
+ raise Errors.IllegalStateError(
521
+ "Cannot bump producer epoch: broker version %s does not support KIP-360 "
522
+ "(InitProducerIdRequest v3+ requires Kafka 2.5+)" % (self._api_version,))
523
+ log.warning("Bumping producer epoch for %s after recoverable error",
524
+ self.producer_id_and_epoch)
525
+ self._transition_to(TransactionState.BUMPING_PRODUCER_EPOCH)
526
+ # Drop all per-partition sequence state. The bumped epoch starts
527
+ # each partition at sequence 0. last_acked_offset is also cleared
528
+ # since it's tied to the pre-bump producer_id/epoch range.
529
+ self._sequence_numbers.clear()
530
+ self._last_acked_offset.clear()
531
+ # Transactional state: the broker aborts any in-flight
532
+ # transaction as part of processing InitProducerIdRequest v3+
533
+ # with a matching producer_id/epoch, so we clear our local
534
+ # view of which partitions are in the transaction. The user's
535
+ # ongoing begin/commit/abort coroutine (if any) will see the
536
+ # bump via the _result and can react accordingly.
537
+ self._transaction_started = False
538
+ self._partitions_in_transaction.clear()
539
+ self._new_partitions_in_transaction.clear()
540
+ self._pending_partitions_in_transaction.clear()
541
+ handler = InitProducerIdHandler(self, self.transaction_timeout_ms, is_epoch_bump=True)
542
+ self._enqueue_request(handler)
543
+
544
+ def _complete_epoch_bump(self):
545
+ """Called from InitProducerIdHandler on successful bump response.
546
+
547
+ Transitions BUMPING_PRODUCER_EPOCH -> READY so the sender resumes
548
+ producing under the new epoch.
549
+ """
550
+ # Caller (handle_response) already holds _lock.
551
+ self._transition_to(TransactionState.READY)
552
+ self._last_error = None
553
+
554
+ def _restart_epoch_bump_without_producer_id(self, transaction_timeout_ms, result):
555
+ """Called from InitProducerIdHandler when the broker rejects the bump
556
+ with INVALID_PRODUCER_EPOCH (our producer_id/epoch are stale).
557
+
558
+ Falls back to requesting a fresh producer_id by enqueuing a new
559
+ InitProducerIdRequest without the producer_id/epoch fields. The
560
+ original TransactionalRequestResult is re-used so the caller waits
561
+ on the overall bump-then-init sequence.
562
+ """
563
+ # Caller (handle_response) already holds _lock.
564
+ self.set_producer_id_and_epoch(ProducerIdAndEpoch(NO_PRODUCER_ID, NO_PRODUCER_EPOCH))
565
+ # Stay in BUMPING_PRODUCER_EPOCH; the follow-up init will transition
566
+ # to READY on success via the regular (non-bump) code path.
567
+ handler = InitProducerIdHandler(self, transaction_timeout_ms, is_epoch_bump=False)
568
+ handler._result = result # thread the caller's result through
569
+ self._enqueue_request(handler)
570
+
571
+ def sequence_number(self, tp):
572
+ with self._lock:
573
+ return self._sequence_numbers[tp]
574
+
575
+ def increment_sequence_number(self, tp, increment):
576
+ with self._lock:
577
+ if tp not in self._sequence_numbers:
578
+ raise Errors.IllegalStateError("Attempt to increment sequence number for a partition with no current sequence.")
579
+ # Sequence number wraps at java max int
580
+ base = self._sequence_numbers[tp]
581
+ if base > (2147483647 - increment):
582
+ self._sequence_numbers[tp] = increment - (2147483647 - base) - 1
583
+ else:
584
+ self._sequence_numbers[tp] += increment
585
+
586
+ def set_sequence_number(self, tp, sequence):
587
+ with self._lock:
588
+ self._sequence_numbers[tp] = sequence
589
+
590
+ def reset_sequence_for_partition(self, tp):
591
+ with self._lock:
592
+ self._sequence_numbers.pop(tp, None)
593
+ self._last_acked_offset.pop(tp, None)
594
+
595
+ def update_last_acked_offset(self, tp, base_offset, record_count):
596
+ """Record the offset of the last successfully-produced record for tp.
597
+
598
+ Called from the sender on each successful batch completion. The
599
+ last acked offset is used to detect whether a subsequent
600
+ UnknownProducerIdError reflects retention (safe to retry) vs. real
601
+ data loss (fatal). See KAFKA-5793.
602
+ """
603
+ if base_offset < 0:
604
+ return
605
+ last_offset = base_offset + record_count - 1
606
+ with self._lock:
607
+ if last_offset > self._last_acked_offset.get(tp, -1):
608
+ self._last_acked_offset[tp] = last_offset
609
+
610
+ def last_acked_offset(self, tp):
611
+ with self._lock:
612
+ return self._last_acked_offset.get(tp, -1)
613
+
614
+ def next_request_handler(self, has_incomplete_batches):
615
+ with self._lock:
616
+ if self._new_partitions_in_transaction:
617
+ self._enqueue_request(self._add_partitions_to_transaction_handler())
618
+
619
+ if not self._pending_requests:
620
+ return None
621
+
622
+ _, _, next_request_handler = self._pending_requests[0]
623
+ # Do not send the EndTxn until all batches have been flushed
624
+ if isinstance(next_request_handler, EndTxnHandler) and has_incomplete_batches:
625
+ return None
626
+
627
+ heapq.heappop(self._pending_requests)
628
+ if self._maybe_terminate_request_with_error(next_request_handler):
629
+ log.debug("Not sending transactional request %s because we are in an error state",
630
+ next_request_handler.request)
631
+ return None
632
+
633
+ if isinstance(next_request_handler, EndTxnHandler) and not self._transaction_started:
634
+ next_request_handler.result.done()
635
+ if self._current_state != TransactionState.FATAL_ERROR:
636
+ log.debug("Not sending EndTxn for completed transaction since no partitions"
637
+ " or offsets were successfully added")
638
+ self._complete_transaction()
639
+ try:
640
+ _, _, next_request_handler = heapq.heappop(self._pending_requests)
641
+ except IndexError:
642
+ next_request_handler = None
643
+
644
+ if next_request_handler:
645
+ log.debug("Request %s dequeued for sending", next_request_handler.request)
646
+
647
+ return next_request_handler
648
+
649
+ def retry(self, request):
650
+ with self._lock:
651
+ request.set_retry()
652
+ self._enqueue_request(request)
653
+
654
+ def authentication_failed(self, exc):
655
+ with self._lock:
656
+ for _, _, request in self._pending_requests:
657
+ request.fatal_error(exc)
658
+
659
+ def coordinator(self, coord_type):
660
+ if coord_type == CoordinatorType.GROUP:
661
+ return self._consumer_group_coordinator
662
+ elif coord_type == CoordinatorType.TRANSACTION:
663
+ return self._transaction_coordinator
664
+ else:
665
+ raise Errors.IllegalStateError("Received an invalid coordinator type: %s" % (coord_type,))
666
+
667
+ def lookup_coordinator_for_request(self, request):
668
+ self._lookup_coordinator(request.coordinator_type, request.coordinator_key)
669
+
670
+ def next_in_flight_request_correlation_id(self):
671
+ self._in_flight_request_correlation_id += 1
672
+ return self._in_flight_request_correlation_id
673
+
674
+ def clear_in_flight_transactional_request_correlation_id(self):
675
+ self._in_flight_request_correlation_id = self.NO_INFLIGHT_REQUEST_CORRELATION_ID
676
+
677
+ def has_in_flight_transactional_request(self):
678
+ return self._in_flight_request_correlation_id != self.NO_INFLIGHT_REQUEST_CORRELATION_ID
679
+
680
+ def has_fatal_error(self):
681
+ return self._current_state == TransactionState.FATAL_ERROR
682
+
683
+ def has_abortable_error(self):
684
+ return self._current_state == TransactionState.ABORTABLE_ERROR
685
+
686
+ # visible for testing
687
+ def _test_transaction_contains_partition(self, tp):
688
+ with self._lock:
689
+ return tp in self._partitions_in_transaction
690
+
691
+ # visible for testing
692
+ def _test_has_pending_offset_commits(self):
693
+ return bool(self._pending_txn_offset_commits)
694
+
695
+ # visible for testing
696
+ def _test_has_ongoing_transaction(self):
697
+ with self._lock:
698
+ # transactions are considered ongoing once started until completion or a fatal error
699
+ return self._current_state == TransactionState.IN_TRANSACTION or self.is_completing() or self.has_abortable_error()
700
+
701
+ # visible for testing
702
+ def _test_is_ready(self):
703
+ with self._lock:
704
+ return self.is_transactional() and self._current_state == TransactionState.READY
705
+
706
+ def _transition_to(self, target, error=None):
707
+ with self._lock:
708
+ if not self._current_state.is_transition_valid(self._current_state, target):
709
+ raise Errors.KafkaError("TransactionalId %s: Invalid transition attempted from state %s to state %s" % (
710
+ self.transactional_id, self._current_state.name, target.name))
711
+
712
+ if target in (TransactionState.FATAL_ERROR, TransactionState.ABORTABLE_ERROR):
713
+ if error is None:
714
+ raise Errors.IllegalArgumentError("Cannot transition to %s with an None exception" % (target.name,))
715
+ self._last_error = error
716
+ else:
717
+ self._last_error = None
718
+
719
+ if self._last_error is not None:
720
+ log.debug("Transition from state %s to error state %s (%s)", self._current_state.name, target.name, self._last_error)
721
+ else:
722
+ log.debug("Transition from state %s to %s", self._current_state, target)
723
+ self._current_state = target
724
+
725
+ def _ensure_transactional(self):
726
+ if not self.is_transactional():
727
+ raise Errors.IllegalStateError("Transactional method invoked on a non-transactional producer.")
728
+
729
+ def _maybe_fail_with_error(self):
730
+ if self.has_error():
731
+ raise Errors.KafkaError("Cannot execute transactional method because we are in an error state: %s" % (self._last_error,))
732
+
733
+ def _maybe_terminate_request_with_error(self, request_handler):
734
+ if self.has_error():
735
+ if self.has_abortable_error() and isinstance(request_handler, FindCoordinatorHandler):
736
+ # No harm letting the FindCoordinator request go through if we're expecting to abort
737
+ return False
738
+ request_handler.fail(self._last_error)
739
+ return True
740
+ return False
741
+
742
+ def _next_pending_requests_sort_id(self):
743
+ self._pending_requests_sort_id += 1
744
+ return self._pending_requests_sort_id
745
+
746
+ def _enqueue_request(self, request_handler):
747
+ log.debug("Enqueuing transactional request %s", request_handler.request)
748
+ heapq.heappush(
749
+ self._pending_requests,
750
+ (
751
+ request_handler.priority, # keep lowest priority at head of queue
752
+ self._next_pending_requests_sort_id(), # break ties
753
+ request_handler
754
+ )
755
+ )
756
+
757
+ def _lookup_coordinator(self, coord_type, coord_key):
758
+ with self._lock:
759
+ if coord_type == CoordinatorType.GROUP:
760
+ self._consumer_group_coordinator = None
761
+ elif coord_type == CoordinatorType.TRANSACTION:
762
+ self._transaction_coordinator = None
763
+ else:
764
+ raise Errors.IllegalStateError("Invalid coordinator type: %s" % (coord_type,))
765
+ self._enqueue_request(FindCoordinatorHandler(self, coord_type, coord_key))
766
+
767
+ def _complete_transaction(self):
768
+ with self._lock:
769
+ self._transition_to(TransactionState.READY)
770
+ self._transaction_started = False
771
+ self._new_partitions_in_transaction.clear()
772
+ self._pending_partitions_in_transaction.clear()
773
+ self._partitions_in_transaction.clear()
774
+
775
+ def _add_partitions_to_transaction_handler(self):
776
+ with self._lock:
777
+ self._pending_partitions_in_transaction.update(self._new_partitions_in_transaction)
778
+ self._new_partitions_in_transaction.clear()
779
+ return AddPartitionsToTxnHandler(self, self._pending_partitions_in_transaction)
780
+
781
+
782
+ class TransactionalRequestResult:
783
+ def __init__(self):
784
+ self._latch = threading.Event()
785
+ self._error = None
786
+
787
+ def done(self, error=None):
788
+ self._error = error
789
+ self._latch.set()
790
+
791
+ def wait(self, timeout_ms=None):
792
+ timeout = timeout_ms / 1000 if timeout_ms is not None else None
793
+ success = self._latch.wait(timeout)
794
+ if self._error:
795
+ raise self._error
796
+ return success
797
+
798
+ @property
799
+ def is_done(self):
800
+ return self._latch.is_set()
801
+
802
+ @property
803
+ def succeeded(self):
804
+ return self._latch.is_set() and self._error is None
805
+
806
+ @property
807
+ def failed(self):
808
+ return self._latch.is_set() and self._error is not None
809
+
810
+ @property
811
+ def exception(self):
812
+ return self._error
813
+
814
+
815
+ class TxnRequestHandler(ABC):
816
+ def __init__(self, transaction_manager, result=None):
817
+ self.transaction_manager = transaction_manager
818
+ self.retry_backoff_ms = transaction_manager.retry_backoff_ms
819
+ self.request = None
820
+ self._result = result or TransactionalRequestResult()
821
+ self._is_retry = False
822
+
823
+ @property
824
+ def transactional_id(self):
825
+ return self.transaction_manager.transactional_id
826
+
827
+ @property
828
+ def producer_id(self):
829
+ return self.transaction_manager.producer_id_and_epoch.producer_id
830
+
831
+ @property
832
+ def producer_epoch(self):
833
+ return self.transaction_manager.producer_id_and_epoch.epoch
834
+
835
+ def fatal_error(self, exc):
836
+ log.error(f'Fatal Error handling request {self.request.name if self.request else "none"}: {exc}')
837
+ self.transaction_manager.transition_to_fatal_error(exc)
838
+ self._result.done(error=exc)
839
+
840
+ def abortable_error(self, exc):
841
+ self.transaction_manager.transition_to_abortable_error(exc)
842
+ self._result.done(error=exc)
843
+
844
+ def fail(self, exc):
845
+ self._result.done(error=exc)
846
+
847
+ def reenqueue(self):
848
+ with self.transaction_manager._lock:
849
+ self._is_retry = True
850
+ self.transaction_manager._enqueue_request(self)
851
+
852
+ def on_complete(self, correlation_id, response_or_exc):
853
+ if correlation_id != self.transaction_manager._in_flight_request_correlation_id:
854
+ self.fatal_error(RuntimeError("Detected more than one in-flight transactional request."))
855
+ else:
856
+ self.transaction_manager.clear_in_flight_transactional_request_correlation_id()
857
+ if isinstance(response_or_exc, Errors.KafkaConnectionError):
858
+ log.debug("Disconnected from node. Will retry.")
859
+ if self.needs_coordinator():
860
+ self.transaction_manager._lookup_coordinator(self.coordinator_type, self.coordinator_key)
861
+ self.reenqueue()
862
+ elif isinstance(response_or_exc, Errors.UnsupportedVersionError):
863
+ self.fatal_error(response_or_exc)
864
+ elif not isinstance(response_or_exc, (Exception, type(None))):
865
+ log.debug("Received transactional response %s for request %s", response_or_exc, self.request)
866
+ with self.transaction_manager._lock:
867
+ self.handle_response(response_or_exc)
868
+ else:
869
+ self.fatal_error(Errors.KafkaError("Could not execute transactional request for unknown reasons: %s" % response_or_exc))
870
+
871
+ def needs_coordinator(self):
872
+ return self.coordinator_type is not None
873
+
874
+ @property
875
+ def result(self):
876
+ return self._result
877
+
878
+ @property
879
+ def coordinator_type(self):
880
+ return CoordinatorType.TRANSACTION
881
+
882
+ @property
883
+ def coordinator_key(self):
884
+ return self.transaction_manager.transactional_id
885
+
886
+ def set_retry(self):
887
+ self._is_retry = True
888
+
889
+ @property
890
+ def is_retry(self):
891
+ return self._is_retry
892
+
893
+ @abstractmethod
894
+ def handle_response(self, response):
895
+ pass
896
+
897
+ @abstractproperty
898
+ def priority(self):
899
+ pass
900
+
901
+
902
+ class InitProducerIdHandler(TxnRequestHandler):
903
+ def __init__(self, transaction_manager, transaction_timeout_ms, is_epoch_bump=False):
904
+ super().__init__(transaction_manager)
905
+ self._is_epoch_bump = is_epoch_bump
906
+ max_version = 4
907
+ min_version = 0
908
+
909
+ if is_epoch_bump:
910
+ # KIP-360 / InitProducerIdRequest v3+ (Kafka 2.5+) lets us resume
911
+ # an existing producer_id by bumping its epoch rather than allocating
912
+ # a fresh one. v3+ takes producer_id + epoch fields; on broker match,
913
+ # the broker returns (same producer_id, epoch+1).
914
+ min_version = 3
915
+ producer_id = transaction_manager.producer_id_and_epoch.producer_id
916
+ producer_epoch = transaction_manager.producer_id_and_epoch.epoch
917
+ else:
918
+ producer_id = NO_PRODUCER_ID
919
+ producer_epoch = NO_PRODUCER_EPOCH
920
+
921
+ self.request = InitProducerIdRequest(
922
+ transactional_id=self.transactional_id,
923
+ transaction_timeout_ms=transaction_timeout_ms,
924
+ producer_id=producer_id,
925
+ producer_epoch=producer_epoch,
926
+ max_version=max_version,
927
+ min_version=min_version)
928
+
929
+ @property
930
+ def priority(self):
931
+ return Priority.INIT_PRODUCER_ID
932
+
933
+ @property
934
+ def coordinator_type(self):
935
+ # Idempotent (non-transactional) producers don't have a transaction
936
+ # coordinator -- InitProducerIdRequest can be sent to any broker.
937
+ if self.transaction_manager.transactional_id is None:
938
+ return None
939
+ return CoordinatorType.TRANSACTION
940
+
941
+ def handle_response(self, response):
942
+ error_type = Errors.for_code(response.error_code)
943
+
944
+ if error_type is Errors.NoError:
945
+ self.transaction_manager.set_producer_id_and_epoch(ProducerIdAndEpoch(response.producer_id, response.producer_epoch))
946
+ if self._is_epoch_bump:
947
+ self.transaction_manager._complete_epoch_bump()
948
+ else:
949
+ self.transaction_manager._transition_to(TransactionState.READY)
950
+ self._result.done()
951
+ elif issubclass(error_type, Errors.RetriableError):
952
+ if error_type in (Errors.NotCoordinatorError, Errors.CoordinatorNotAvailableError):
953
+ self.transaction_manager._lookup_coordinator(CoordinatorType.TRANSACTION, self.transactional_id)
954
+ self.reenqueue()
955
+ elif error_type is Errors.InvalidProducerEpochError and self._is_epoch_bump:
956
+ # KIP-360: our (producer_id, epoch) are stale--the broker no
957
+ # longer recognizes them. Fall back to allocating a fresh
958
+ # producer_id by reissuing InitProducerIdRequest without
959
+ # producer_id/epoch fields.
960
+ log.info("InitProducerId bump rejected with INVALID_PRODUCER_EPOCH; "
961
+ "falling back to a fresh producer id")
962
+ self.transaction_manager._restart_epoch_bump_without_producer_id(
963
+ self.request.transaction_timeout_ms, self._result)
964
+ elif error_type in (Errors.ProducerFencedError, Errors.InvalidProducerEpochError):
965
+ # Another producer instance has taken over this transactional_id.
966
+ # Fatal--the application must rebuild the producer. Mirrors the
967
+ # Java client, which normalizes INVALID_PRODUCER_EPOCH to
968
+ # PRODUCER_FENCED on the non-bump InitProducerId path.
969
+ self.fatal_error(Errors.ProducerFencedError())
970
+ elif error_type is Errors.TransactionalIdAuthorizationFailedError:
971
+ self.fatal_error(error_type())
972
+ else:
973
+ self.fatal_error(Errors.KafkaError("Unexpected error in InitProducerIdResponse: %s" % (error_type())))
974
+
975
+ class AddPartitionsToTxnHandler(TxnRequestHandler):
976
+ def __init__(self, transaction_manager, topic_partitions):
977
+ super().__init__(transaction_manager)
978
+
979
+ topic_data = collections.defaultdict(list)
980
+ for tp in topic_partitions:
981
+ topic_data[tp.topic].append(tp.partition)
982
+
983
+ Topic = AddPartitionsToTxnRequest.AddPartitionsToTxnTopic
984
+ self.request = AddPartitionsToTxnRequest(
985
+ v3_and_below_transactional_id=self.transactional_id,
986
+ v3_and_below_producer_id=self.producer_id,
987
+ v3_and_below_producer_epoch=self.producer_epoch,
988
+ v3_and_below_topics=[Topic(name=topic, partitions=partitions)
989
+ for topic, partitions in topic_data.items()],
990
+ max_version=3)
991
+
992
+ @property
993
+ def priority(self):
994
+ return Priority.ADD_PARTITIONS_OR_OFFSETS
995
+
996
+ def handle_response(self, response):
997
+ has_partition_errors = False
998
+ unauthorized_topics = set()
999
+ self.retry_backoff_ms = self.transaction_manager.retry_backoff_ms
1000
+
1001
+ results = {TopicPartition(topic, partition): Errors.for_code(error_code)
1002
+ for topic, partition_data in response.results_by_topic_v3_and_below
1003
+ for partition, error_code in partition_data}
1004
+
1005
+ for tp, error_type in results.items():
1006
+ if error_type is Errors.NoError:
1007
+ continue
1008
+ elif issubclass(error_type, Errors.RetriableError):
1009
+ if error_type in (Errors.CoordinatorNotAvailableError, Errors.NotCoordinatorError):
1010
+ self.transaction_manager._lookup_coordinator(CoordinatorType.TRANSACTION, self.transactional_id)
1011
+ elif error_type is Errors.ConcurrentTransactionsError:
1012
+ self.maybe_override_retry_backoff_ms()
1013
+ self.reenqueue()
1014
+ return
1015
+ elif error_type in (Errors.InvalidProducerEpochError, Errors.ProducerFencedError):
1016
+ # Java client normalizes INVALID_PRODUCER_EPOCH to PRODUCER_FENCED
1017
+ # on the txn-coordinator RPC paths (KIP-360).
1018
+ self.fatal_error(Errors.ProducerFencedError())
1019
+ return
1020
+ elif error_type is Errors.TransactionalIdAuthorizationFailedError:
1021
+ self.fatal_error(error_type())
1022
+ return
1023
+ elif error_type in (Errors.InvalidProducerIdMappingError, Errors.InvalidTxnStateError):
1024
+ self.fatal_error(Errors.KafkaError(error_type()))
1025
+ return
1026
+ elif error_type is Errors.TopicAuthorizationFailedError:
1027
+ unauthorized_topics.add(tp.topic)
1028
+ elif error_type is Errors.OperationNotAttemptedError:
1029
+ log.debug("Did not attempt to add partition %s to transaction because other partitions in the"
1030
+ " batch had errors.", tp)
1031
+ has_partition_errors = True
1032
+ else:
1033
+ log.error("Could not add partition %s due to unexpected error %s", tp, error_type())
1034
+ has_partition_errors = True
1035
+
1036
+ partitions = set(results)
1037
+
1038
+ # Remove the partitions from the pending set regardless of the result. We use the presence
1039
+ # of partitions in the pending set to know when it is not safe to send batches. However, if
1040
+ # the partitions failed to be added and we enter an error state, we expect the batches to be
1041
+ # aborted anyway. In this case, we must be able to continue sending the batches which are in
1042
+ # retry for partitions that were successfully added.
1043
+ self.transaction_manager._pending_partitions_in_transaction -= partitions
1044
+
1045
+ if unauthorized_topics:
1046
+ self.abortable_error(Errors.TopicAuthorizationFailedError(unauthorized_topics))
1047
+ elif has_partition_errors:
1048
+ self.abortable_error(Errors.KafkaError("Could not add partitions to transaction due to errors: %s" % (results)))
1049
+ else:
1050
+ log.debug("Successfully added partitions %s to transaction", partitions)
1051
+ self.transaction_manager._partitions_in_transaction.update(partitions)
1052
+ self.transaction_manager._transaction_started = True
1053
+ self._result.done()
1054
+
1055
+ def maybe_override_retry_backoff_ms(self):
1056
+ # We only want to reduce the backoff when retrying the first AddPartition which errored out due to a
1057
+ # CONCURRENT_TRANSACTIONS error since this means that the previous transaction is still completing and
1058
+ # we don't want to wait too long before trying to start the new one.
1059
+ #
1060
+ # This is only a temporary fix, the long term solution is being tracked in
1061
+ # https://issues.apache.org/jira/browse/KAFKA-5482
1062
+ if not self.transaction_manager._partitions_in_transaction:
1063
+ self.retry_backoff_ms = min(self.transaction_manager.ADD_PARTITIONS_RETRY_BACKOFF_MS, self.retry_backoff_ms)
1064
+
1065
+
1066
+ class FindCoordinatorHandler(TxnRequestHandler):
1067
+ def __init__(self, transaction_manager, coord_type, coord_key):
1068
+ super().__init__(transaction_manager)
1069
+
1070
+ self._coord_type = CoordinatorType.build_from(coord_type)
1071
+ self._coord_key = coord_key
1072
+ # Setting key, key_type, and coordinator_keys all at once lets the
1073
+ # connection layer negotiate any version: v0-v3 emit `key`/`key_type`,
1074
+ # v4+ (KIP-699) emit `key_type`/`coordinator_keys`.
1075
+ self.request = FindCoordinatorRequest(
1076
+ key=self._coord_key,
1077
+ key_type=self._coord_type.value,
1078
+ coordinator_keys=[self._coord_key],
1079
+ )
1080
+
1081
+ @property
1082
+ def priority(self):
1083
+ return Priority.FIND_COORDINATOR
1084
+
1085
+ @property
1086
+ def coordinator_type(self):
1087
+ return None
1088
+
1089
+ @property
1090
+ def coordinator_key(self):
1091
+ return None
1092
+
1093
+ def handle_response(self, response):
1094
+ # v4+ returns results in a Coordinators array; we always send a single
1095
+ # key, so the first entry is ours. v0-v3 returns top-level fields.
1096
+ result = response.coordinators[0] if response.coordinators else response
1097
+ error_type = Errors.for_code(result.error_code)
1098
+
1099
+ if error_type is Errors.NoError:
1100
+ coordinator_id = self.transaction_manager._metadata.add_coordinator(
1101
+ result, self._coord_type, self._coord_key)
1102
+ if self._coord_type == CoordinatorType.GROUP:
1103
+ self.transaction_manager._consumer_group_coordinator = coordinator_id
1104
+ elif self._coord_type == CoordinatorType.TRANSACTION:
1105
+ self.transaction_manager._transaction_coordinator = coordinator_id
1106
+ self._result.done()
1107
+ elif issubclass(error_type, Errors.RetriableError):
1108
+ self.reenqueue()
1109
+ elif error_type is Errors.TransactionalIdAuthorizationFailedError:
1110
+ self.fatal_error(error_type())
1111
+ elif error_type is Errors.GroupAuthorizationFailedError:
1112
+ self.abortable_error(error_type(self._coord_key))
1113
+ else:
1114
+ self.fatal_error(Errors.KafkaError(
1115
+ "Could not find a coordinator with type %s with key %s due to"
1116
+ " unexpected error: %s" % (self._coord_type, self._coord_key, error_type())))
1117
+
1118
+
1119
+ class EndTxnHandler(TxnRequestHandler):
1120
+ def __init__(self, transaction_manager, committed):
1121
+ super().__init__(transaction_manager)
1122
+ self.request = EndTxnRequest(
1123
+ transactional_id=self.transactional_id,
1124
+ producer_id=self.producer_id,
1125
+ producer_epoch=self.producer_epoch,
1126
+ committed=committed,
1127
+ max_version=3)
1128
+
1129
+ @property
1130
+ def priority(self):
1131
+ return Priority.END_TXN
1132
+
1133
+ def handle_response(self, response):
1134
+ error_type = Errors.for_code(response.error_code)
1135
+
1136
+ if error_type is Errors.NoError:
1137
+ self.transaction_manager._complete_transaction()
1138
+ self._result.done()
1139
+ elif issubclass(error_type, Errors.RetriableError):
1140
+ if error_type in (Errors.CoordinatorNotAvailableError, Errors.NotCoordinatorError):
1141
+ self.transaction_manager._lookup_coordinator(CoordinatorType.TRANSACTION, self.transactional_id)
1142
+ self.reenqueue()
1143
+ elif error_type in (Errors.InvalidProducerEpochError, Errors.ProducerFencedError):
1144
+ # Java client normalizes INVALID_PRODUCER_EPOCH to PRODUCER_FENCED
1145
+ # on the txn-coordinator RPC paths (KIP-360).
1146
+ self.fatal_error(Errors.ProducerFencedError())
1147
+ elif error_type is Errors.TransactionalIdAuthorizationFailedError:
1148
+ self.fatal_error(error_type())
1149
+ elif error_type is Errors.InvalidTxnStateError:
1150
+ self.fatal_error(error_type())
1151
+ else:
1152
+ self.fatal_error(Errors.KafkaError("Unhandled error in EndTxnResponse: %s" % (error_type())))
1153
+
1154
+
1155
+ class AddOffsetsToTxnHandler(TxnRequestHandler):
1156
+ def __init__(self, transaction_manager, group_metadata, offsets):
1157
+ super().__init__(transaction_manager)
1158
+
1159
+ self.group_metadata = group_metadata
1160
+ self.consumer_group_id = group_metadata.group_id
1161
+ self.offsets = offsets
1162
+ # max_version=3 is the highest we know how to drive (v4 is KIP-890).
1163
+ # The connection negotiates the actual wire version against the broker.
1164
+ self.request = AddOffsetsToTxnRequest(
1165
+ transactional_id=self.transactional_id,
1166
+ producer_id=self.producer_id,
1167
+ producer_epoch=self.producer_epoch,
1168
+ group_id=self.consumer_group_id,
1169
+ max_version=3,
1170
+ )
1171
+
1172
+ @property
1173
+ def priority(self):
1174
+ return Priority.ADD_PARTITIONS_OR_OFFSETS
1175
+
1176
+ def handle_response(self, response):
1177
+ error_type = Errors.for_code(response.error_code)
1178
+
1179
+ if error_type is Errors.NoError:
1180
+ log.debug("Successfully added partition for consumer group %s to transaction", self.consumer_group_id)
1181
+
1182
+ # note the result is not completed until the TxnOffsetCommit returns
1183
+ for tp, offset in self.offsets.items():
1184
+ self.transaction_manager._pending_txn_offset_commits[tp] = offset
1185
+ handler = TxnOffsetCommitHandler(self.transaction_manager, self.group_metadata,
1186
+ self.transaction_manager._pending_txn_offset_commits, self._result)
1187
+ self.transaction_manager._enqueue_request(handler)
1188
+ self.transaction_manager._transaction_started = True
1189
+ elif issubclass(error_type, Errors.RetriableError):
1190
+ if error_type in (Errors.CoordinatorNotAvailableError, Errors.NotCoordinatorError):
1191
+ self.transaction_manager._lookup_coordinator(CoordinatorType.TRANSACTION, self.transactional_id)
1192
+ self.reenqueue()
1193
+ elif error_type in (Errors.CoordinatorLoadInProgressError, Errors.ConcurrentTransactionsError):
1194
+ self.reenqueue()
1195
+ elif error_type in (Errors.InvalidProducerEpochError, Errors.ProducerFencedError):
1196
+ # Java client normalizes INVALID_PRODUCER_EPOCH to PRODUCER_FENCED
1197
+ # on the txn-coordinator RPC paths (KIP-360).
1198
+ self.fatal_error(Errors.ProducerFencedError())
1199
+ elif error_type in (Errors.UnknownProducerIdError, Errors.InvalidProducerIdMappingError):
1200
+ if self.transaction_manager._supports_epoch_bump():
1201
+ self.abortable_error(error_type())
1202
+ else:
1203
+ self.fatal_error(error_type())
1204
+ elif error_type is Errors.TransactionalIdAuthorizationFailedError:
1205
+ self.fatal_error(error_type())
1206
+ elif error_type is Errors.GroupAuthorizationFailedError:
1207
+ self.abortable_error(error_type(self.consumer_group_id))
1208
+ else:
1209
+ self.fatal_error(Errors.KafkaError("Unexpected error in AddOffsetsToTxnResponse: %s" % (error_type())))
1210
+
1211
+
1212
+ class TxnOffsetCommitHandler(TxnRequestHandler):
1213
+ def __init__(self, transaction_manager, group_metadata, offsets, result):
1214
+ super().__init__(transaction_manager, result=result)
1215
+
1216
+ self.group_metadata = group_metadata
1217
+ self.consumer_group_id = group_metadata.group_id
1218
+ self.offsets = offsets
1219
+ self.request = self._build_request()
1220
+
1221
+ def _build_request(self):
1222
+ # KIP-447: v3+ carries member_id / generation_id / group_instance_id
1223
+ # so the broker can fence stale consumer instances. We always set them
1224
+ # - the protocol drops them when the connection negotiates v0-v2
1225
+ # against an older broker. max_version is the highest version this
1226
+ # client knows how to drive: v4/v5 belong to KIP-890.
1227
+ Topic = TxnOffsetCommitRequest.TxnOffsetCommitRequestTopic
1228
+ Partition = Topic.TxnOffsetCommitRequestPartition
1229
+
1230
+ topic_data = collections.defaultdict(list)
1231
+ for tp, offset in self.offsets.items():
1232
+ topic_data[tp.topic].append(Partition(
1233
+ partition_index=tp.partition,
1234
+ committed_offset=offset.offset,
1235
+ committed_leader_epoch=offset.leader_epoch,
1236
+ committed_metadata=offset.metadata))
1237
+
1238
+ return TxnOffsetCommitRequest(
1239
+ transactional_id=self.transactional_id,
1240
+ group_id=self.consumer_group_id,
1241
+ producer_id=self.producer_id,
1242
+ producer_epoch=self.producer_epoch,
1243
+ generation_id=self.group_metadata.generation_id,
1244
+ member_id=self.group_metadata.member_id,
1245
+ group_instance_id=self.group_metadata.group_instance_id,
1246
+ topics=[Topic(name=topic, partitions=partitions)
1247
+ for topic, partitions in topic_data.items()],
1248
+ max_version=3,
1249
+ )
1250
+
1251
+ @property
1252
+ def priority(self):
1253
+ return Priority.ADD_PARTITIONS_OR_OFFSETS
1254
+
1255
+ @property
1256
+ def coordinator_type(self):
1257
+ return CoordinatorType.GROUP
1258
+
1259
+ @property
1260
+ def coordinator_key(self):
1261
+ return self.consumer_group_id
1262
+
1263
+ def handle_response(self, response):
1264
+ lookup_coordinator = False
1265
+ retriable_failure = False
1266
+
1267
+ errors = {TopicPartition(topic, partition): Errors.for_code(error_code)
1268
+ for topic, partition_data in response.topics
1269
+ for partition, error_code in partition_data}
1270
+
1271
+ for tp, error_type in errors.items():
1272
+ if error_type is Errors.NoError:
1273
+ log.debug("Successfully added offsets for %s from consumer group %s to transaction.",
1274
+ tp, self.consumer_group_id)
1275
+ del self.transaction_manager._pending_txn_offset_commits[tp]
1276
+ elif issubclass(error_type, Errors.RetriableError):
1277
+ retriable_failure = True
1278
+ if error_type in (Errors.CoordinatorNotAvailableError, Errors.NotCoordinatorError, Errors.RequestTimedOutError):
1279
+ lookup_coordinator = True
1280
+ elif error_type is Errors.GroupAuthorizationFailedError:
1281
+ self.abortable_error(error_type(self.consumer_group_id))
1282
+ return
1283
+ elif error_type in (Errors.InvalidProducerEpochError, Errors.ProducerFencedError):
1284
+ # Java client normalizes INVALID_PRODUCER_EPOCH to PRODUCER_FENCED
1285
+ # on the txn-coordinator RPC paths (KIP-360).
1286
+ self.fatal_error(Errors.ProducerFencedError())
1287
+ return
1288
+ elif error_type is Errors.FencedInstanceIdError:
1289
+ # KIP-447: static-membership fencing - another consumer
1290
+ # instance with this group_instance_id displaced ours. The
1291
+ # transaction must be aborted, but the producer can be
1292
+ # reused for a fresh transaction.
1293
+ self.abortable_error(error_type())
1294
+ return
1295
+ elif error_type in (Errors.IllegalGenerationError,
1296
+ Errors.UnknownMemberIdError):
1297
+ # KIP-447: the consumer generation / member_id we passed
1298
+ # in are stale (the consumer rebalanced between when we
1299
+ # snapshotted group_metadata and when the broker checked
1300
+ # it). Abort the txn so the application can re-snapshot
1301
+ # and retry.
1302
+ self.abortable_error(Errors.CommitFailedError(
1303
+ "Transaction offset commit failed due to consumer group"
1304
+ " metadata mismatch: %s" % (error_type.__name__,)))
1305
+ return
1306
+ elif error_type in (Errors.TransactionalIdAuthorizationFailedError,
1307
+ Errors.UnsupportedForMessageFormatError):
1308
+ self.fatal_error(error_type())
1309
+ return
1310
+ else:
1311
+ self.fatal_error(Errors.KafkaError("Unexpected error in TxnOffsetCommitResponse: %s" % (error_type())))
1312
+ return
1313
+
1314
+ if lookup_coordinator:
1315
+ self.transaction_manager._lookup_coordinator(CoordinatorType.GROUP, self.consumer_group_id)
1316
+
1317
+ if not retriable_failure:
1318
+ # all attempted partitions were either successful, or there was a fatal failure.
1319
+ # either way, we are not retrying, so complete the request.
1320
+ self.result.done()
1321
+
1322
+ # retry the commits which failed with a retriable error.
1323
+ elif self.transaction_manager._pending_txn_offset_commits:
1324
+ self.offsets = self.transaction_manager._pending_txn_offset_commits
1325
+ self.request = self._build_request()
1326
+ self.reenqueue()