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,2012 @@
1
+ import collections
2
+ import copy
3
+ import itertools
4
+ import logging
5
+ import sys
6
+ import time
7
+ import warnings
8
+
9
+ import kafka.errors as Errors
10
+ from kafka.future import Future
11
+ from kafka.metrics.stats import Avg, Count, Max, Rate
12
+ from kafka.protocol.consumer import FetchRequest
13
+ from kafka.protocol.consumer import (
14
+ ListOffsetsRequest, OffsetForLeaderEpochRequest,
15
+ OffsetSpec, UNKNOWN_OFFSET, IsolationLevel,
16
+ )
17
+ from kafka.record import MemoryRecords
18
+ from kafka.serializer import Deserializer, DeserializeWrapper
19
+ from kafka.structs import TopicPartition, OffsetAndMetadata, OffsetAndTimestamp
20
+ from kafka.util import Timer
21
+
22
+ log = logging.getLogger(__name__)
23
+
24
+ _LOGGED_DESERIALIZE_WARNING = False
25
+
26
+ ConsumerRecord = collections.namedtuple("ConsumerRecord",
27
+ ["topic", "partition", "leader_epoch", "offset", "timestamp", "timestamp_type",
28
+ "key", "value", "headers", "checksum", "serialized_key_size", "serialized_value_size", "serialized_header_size"])
29
+
30
+
31
+ CompletedFetch = collections.namedtuple("CompletedFetch",
32
+ ["topic_partition", "fetched_offset", "response_version",
33
+ "partition_data", "metric_aggregator"])
34
+
35
+
36
+ ExceptionMetadata = collections.namedtuple("ExceptionMetadata",
37
+ ["partition", "fetched_offset", "exception"])
38
+
39
+
40
+ _FetchTopic = FetchRequest.FetchTopic
41
+ _FetchPartition = _FetchTopic.FetchPartition
42
+ _ForgottenTopic = FetchRequest.ForgottenTopic
43
+ _ListOffsetsTopic = ListOffsetsRequest.ListOffsetsTopic
44
+ _ListOffsetsPartition = _ListOffsetsTopic.ListOffsetsPartition
45
+ _OffsetForLeaderTopic = OffsetForLeaderEpochRequest.OffsetForLeaderTopic
46
+ _OffsetForLeaderPartition = _OffsetForLeaderTopic.OffsetForLeaderPartition
47
+
48
+
49
+ class RecordTooLargeError(Errors.KafkaError):
50
+ pass
51
+
52
+
53
+ class Fetcher:
54
+ DEFAULT_CONFIG = {
55
+ 'key_deserializer': None,
56
+ 'value_deserializer': None,
57
+ 'fetch_min_bytes': 1,
58
+ 'fetch_max_wait_ms': 500,
59
+ 'fetch_max_bytes': 52428800,
60
+ 'max_partition_fetch_bytes': 1048576,
61
+ 'max_poll_records': sys.maxsize,
62
+ 'check_crcs': True,
63
+ 'metrics': None,
64
+ 'metric_group_prefix': 'consumer',
65
+ 'request_timeout_ms': 30000,
66
+ 'retry_backoff_ms': 100,
67
+ 'enable_incremental_fetch_sessions': True,
68
+ 'isolation_level': 'read_uncommitted',
69
+ 'client_rack': '',
70
+ 'metadata_max_age_ms': 5 * 60 * 1000,
71
+ }
72
+
73
+ def __init__(self, client, subscriptions, **configs):
74
+ """Initialize a Kafka Message Fetcher.
75
+
76
+ Keyword Arguments:
77
+ key_deserializer (kafka.serializer.Deserializer): Takes a
78
+ raw message key and returns a deserialized key.
79
+ Default: None.
80
+ value_deserializer (kafka.serializer.Deserializer): Takes a
81
+ raw message value and returns a deserialized value.
82
+ Default: None.
83
+ enable_incremental_fetch_sessions: (bool): Use incremental fetch sessions
84
+ when available / supported by kafka broker. See KIP-227. Default: True.
85
+ fetch_min_bytes (int): Minimum amount of data the server should
86
+ return for a fetch request, otherwise wait up to
87
+ fetch_max_wait_ms for more data to accumulate. Default: 1.
88
+ fetch_max_wait_ms (int): The maximum amount of time in milliseconds
89
+ the server will block before answering the fetch request if
90
+ there isn't sufficient data to immediately satisfy the
91
+ requirement given by fetch_min_bytes. Default: 500.
92
+ fetch_max_bytes (int): The maximum amount of data the server should
93
+ return for a fetch request. This is not an absolute maximum, if
94
+ the first message in the first non-empty partition of the fetch
95
+ is larger than this value, the message will still be returned
96
+ to ensure that the consumer can make progress. NOTE: consumer
97
+ performs fetches to multiple brokers in parallel so memory
98
+ usage will depend on the number of brokers containing
99
+ partitions for the topic.
100
+ Supported Kafka version >= 0.10.1.0. Default: 52428800 (50 MB).
101
+ max_partition_fetch_bytes (int): The maximum amount of data
102
+ per-partition the server will return. The maximum total memory
103
+ used for a request = #partitions * max_partition_fetch_bytes.
104
+ This size must be at least as large as the maximum message size
105
+ the server allows or else it is possible for the producer to
106
+ send messages larger than the consumer can fetch. If that
107
+ happens, the consumer can get stuck trying to fetch a large
108
+ message on a certain partition. Default: 1048576.
109
+ check_crcs (bool): Automatically check the CRC32 of the records
110
+ consumed. This ensures no on-the-wire or on-disk corruption to
111
+ the messages occurred. This check adds some overhead, so it may
112
+ be disabled in cases seeking extreme performance. Default: True
113
+ isolation_level (str): Configure KIP-98 transactional consumer by
114
+ setting to 'read_committed'. This will cause the consumer to
115
+ skip records from aborted tranactions. Default: 'read_uncommitted'
116
+ """
117
+ self.config = copy.copy(self.DEFAULT_CONFIG)
118
+ for key in self.config:
119
+ if key in configs:
120
+ self.config[key] = configs[key]
121
+
122
+ for key in ('key_deserializer', 'value_deserializer'):
123
+ if self.config[key] is not None and not isinstance(self.config[key], Deserializer):
124
+ warnings.warn('%s does not implement kafka.serializer.Deserializer' % (key,), category=DeprecationWarning, stacklevel=3)
125
+ self.config[key] = DeserializeWrapper(self.config[key])
126
+
127
+ try:
128
+ self._isolation_level = IsolationLevel.build_from(self.config['isolation_level'])
129
+ except ValueError:
130
+ raise Errors.KafkaConfigurationError('Unrecognized isolation_level') from None
131
+
132
+ self._client = client
133
+ self._manager = client._manager
134
+ self._net = self._manager._net
135
+ self._subscriptions = subscriptions
136
+ self._completed_fetches = collections.deque() # Unparsed responses
137
+ self._next_partition_records = None # Holds a single PartitionRecords until fully consumed
138
+ self._paused_completed_fetches = {} # tp -> CompletedFetch (raw)
139
+ self._paused_partition_records = {} # tp -> PartitionRecords (parsed)
140
+ self._iterator = None
141
+ self._fetch_futures = collections.deque()
142
+ if self.config['metrics']:
143
+ self._sensors = FetchManagerMetrics(self.config['metrics'], self.config['metric_group_prefix'])
144
+ else:
145
+ self._sensors = None
146
+ self._session_handlers = {}
147
+ self._nodes_with_pending_fetch_requests = set()
148
+ self._cached_list_offsets_exception = None
149
+ self._next_in_line_exception_metadata = None
150
+ # In-flight offset-reset Task, cached across reset_offsets_if_needed
151
+ # calls so concurrent callers (consumer.poll fire-and-forget,
152
+ # consumer.position blocking-await) share one fan-out instead of
153
+ # racing duplicate ListOffsets requests.
154
+ self._reset_task = None
155
+ # KIP-320 offset validation: same caching pattern, separate from
156
+ # reset (a partition can be awaiting-reset OR awaiting-validation,
157
+ # never both - awaiting-validation requires a valid position).
158
+ self._validation_task = None
159
+ self._cached_log_truncation = None
160
+
161
+ @property
162
+ def _enable_incremental_fetch_sessions(self):
163
+ if self._manager.broker_version is None or self._manager.broker_version < (1, 1):
164
+ return False
165
+ return self.config['enable_incremental_fetch_sessions']
166
+
167
+ def fetch_records(self, max_records=None, update_offsets=True, timeout_ms=None):
168
+ """Drain buffered records, pipeline next fetches, and wait briefly
169
+ for in-flight responses if no records are immediately available.
170
+
171
+ Single-call replacement for the legacy
172
+ ``fetched_records -> send_fetches -> client.poll -> fetched_records``
173
+ loop in :meth:`KafkaConsumer._poll_once`. The caller no longer
174
+ drives the event loop; the wait happens inside this method via a
175
+ wakeup Future fired by any in-flight fetch's completion callback.
176
+
177
+ Arguments:
178
+ max_records (int, optional): cap on returned records.
179
+ update_offsets (bool): advance subscription positions for
180
+ consumed records.
181
+ timeout_ms (int, optional): wall-clock cap on the wait phase.
182
+ Only applies when no records are immediately available.
183
+
184
+ Returns:
185
+ tuple[dict[TopicPartition, list[ConsumerRecord]], bool]:
186
+ ``(records, idle)``. ``idle`` is True when there were no
187
+ buffered records, no in-flight fetches, and no pending
188
+ offset-reset task -- i.e. nothing this fetcher could wait
189
+ on. Callers in that state should sleep before retrying
190
+ instead of busy-looping.
191
+ """
192
+ # Drain whatever's already buffered from prior fetch responses.
193
+ records, partial = self.fetched_records(
194
+ max_records, update_offsets=update_offsets)
195
+ if not partial:
196
+ # No buffered records remaining; send next batch of fetch requests.
197
+ self.send_fetches()
198
+
199
+ if records:
200
+ return records, False
201
+
202
+ # No records yet. Block until either an in-flight fetch
203
+ # completes (records may have arrived) or a pending offset-reset
204
+ # task completes (positions become available, enabling a fetch
205
+ # on the next caller iteration). add_both fires synchronously on
206
+ # already-done futures, closing the race where a future resolves
207
+ # between scheduling and the wait setup.
208
+ waited_on = list(self._fetch_futures)
209
+ if self._reset_task is not None and not self._reset_task.is_done:
210
+ waited_on.append(self._reset_task)
211
+ if not waited_on:
212
+ return records, True # nothing pending; caller should sleep
213
+
214
+ wakeup = Future()
215
+ def _wake(_):
216
+ if not wakeup.is_done:
217
+ wakeup.success(None)
218
+ for fut in waited_on:
219
+ fut.add_both(_wake)
220
+
221
+ try:
222
+ self._net.run(self._manager.wait_for, wakeup, timeout_ms)
223
+ except Errors.KafkaTimeoutError:
224
+ pass
225
+
226
+ records, _ = self.fetched_records(
227
+ max_records, update_offsets=update_offsets)
228
+ return records, False
229
+
230
+ def send_fetches(self):
231
+ """Send FetchRequests for all assigned partitions that do not already have
232
+ an in-flight fetch or pending fetch data.
233
+
234
+ Returns:
235
+ List of Futures: each future resolves to a FetchResponse
236
+ """
237
+ return self._manager.run(self._send_fetches_async)
238
+
239
+ async def _send_fetches_async(self):
240
+ futures = []
241
+ for node_id, (request, fetch_offsets) in self._create_fetch_requests().items():
242
+ log.debug("Sending FetchRequest to node %s", node_id)
243
+ self._nodes_with_pending_fetch_requests.add(node_id)
244
+ future = self._manager.send(request, node_id=node_id)
245
+ future.add_callback(self._handle_fetch_response, node_id, fetch_offsets, time.monotonic())
246
+ future.add_errback(self._handle_fetch_error, node_id)
247
+ future.add_both(self._clear_pending_fetch_request, node_id)
248
+ futures.append(future)
249
+ self._fetch_futures.extend(futures)
250
+ self._clean_done_fetch_futures()
251
+ return futures
252
+
253
+ def _clean_done_fetch_futures(self):
254
+ while True:
255
+ if not self._fetch_futures:
256
+ break
257
+ if not self._fetch_futures[0].is_done:
258
+ break
259
+ self._fetch_futures.popleft()
260
+
261
+ def in_flight_fetches(self):
262
+ """Return True if there are any unprocessed FetchRequests in flight."""
263
+ self._clean_done_fetch_futures()
264
+ return bool(self._fetch_futures)
265
+
266
+ def reset_offsets_if_needed(self, timeout_ms=None):
267
+ """Schedule pending offset resets and return the in-flight Task.
268
+
269
+ Returns the cached Future for the in-flight reset task (shared
270
+ across concurrent callers) or None if no reset is needed. Callers
271
+ may discard the Future (fire-and-forget, e.g. consumer.poll) or
272
+ await it via ``manager.wait_for(future, timeout_ms)`` to block
273
+ until resets complete (e.g. consumer.position).
274
+
275
+ Arguments:
276
+ timeout_ms (int, optional): Maximum wall-clock the reset task
277
+ should run, including time spent awaiting metadata refresh
278
+ for unknown leaders. If None, uses ``request_timeout_ms``
279
+ as a default upper bound so a permanently-unresolvable
280
+ partition (deleted topic, etc.) doesn't spin forever. The
281
+ first caller's timeout wins for the cached task; later
282
+ callers' bounds are enforced via their own ``wait_for`` on
283
+ the returned Future.
284
+
285
+ Raises:
286
+ NoOffsetForPartitionError: if a previous reset attempt left a
287
+ cached non-retriable exception.
288
+ """
289
+ # Raise exception from previous offset fetch if there is one
290
+ exc, self._cached_list_offsets_exception = self._cached_list_offsets_exception, None
291
+ if exc:
292
+ raise exc
293
+
294
+ if self._reset_task is not None and not self._reset_task.is_done:
295
+ return self._reset_task
296
+
297
+ if not self._subscriptions.partitions_needing_reset():
298
+ return None
299
+
300
+ self._reset_task = self._manager.call_soon(
301
+ self._reset_offsets_async, timeout_ms)
302
+ return self._reset_task
303
+
304
+ def offsets_by_times(self, timestamps, timeout_ms=None):
305
+ """Fetch offset for each partition passed in ``timestamps`` map.
306
+
307
+ Blocks until offsets are obtained, a non-retriable exception is raised
308
+ or ``timeout_ms`` passed.
309
+
310
+ Arguments:
311
+ timestamps: {TopicPartition: int} dict with timestamps to fetch
312
+ offsets by. -1 for the latest available, -2 for the earliest
313
+ available. Otherwise timestamp is treated as epoch milliseconds.
314
+ timeout_ms (int, optional): The maximum time in milliseconds to block.
315
+
316
+ Returns:
317
+ {TopicPartition: OffsetAndTimestamp or None}: Mapping of partition to
318
+ retrieved offset, timestamp, and leader_epoch. If offset does not
319
+ exist for the provided timestamp, the value for the TopicPartition
320
+ will be None.
321
+
322
+ Raises:
323
+ KafkaTimeoutError if timeout_ms provided
324
+ """
325
+ offsets = self._net.run(self._fetch_offsets_by_times_async, timestamps, timeout_ms)
326
+ for tp in timestamps:
327
+ if tp not in offsets:
328
+ offsets[tp] = None
329
+ return offsets
330
+
331
+ async def _fetch_offsets_by_times_async(self, timestamps, timeout_ms=None):
332
+ """Fetch offsets for each partition in timestamps dict. This may send
333
+ request to multiple nodes, based on who is Leader for partition.
334
+
335
+ Per-node requests are dispatched concurrently; if any fails, the first
336
+ exception encountered propagates and the remaining results are dropped.
337
+
338
+ Arguments:
339
+ timestamps (dict): {TopicPartition: int} mapping of partitions to
340
+ timestamps or OffsetSpec sentinels.
341
+
342
+ Returns:
343
+ (fetched_offsets, partitions_to_retry):
344
+ dict[TopicPartition, OffsetAndTimestamp],
345
+ set[TopicPartition]
346
+
347
+ Raises:
348
+ KafkaTimeoutError: if offsets cannot be fully fetched before timeout_ms
349
+ """
350
+ if not timestamps:
351
+ return {}
352
+
353
+ timer = Timer(timeout_ms, "Failed to get offsets by timestamps in %s ms" % (timeout_ms,))
354
+ timestamps = copy.copy(timestamps)
355
+ fetched_offsets = dict()
356
+ while True:
357
+ if not timestamps:
358
+ return {}
359
+
360
+ future = self._manager.call_soon(self._send_list_offsets_requests, timestamps)
361
+ try:
362
+ refresh_future = None
363
+ backoff = False
364
+ offsets, retry = await self._manager.wait_for(future, timer.timeout_ms)
365
+ except Errors.InvalidMetadataError:
366
+ refresh_future = self._manager.cluster.request_update()
367
+ except Errors.RetriableError:
368
+ if self._manager.cluster.need_update:
369
+ refresh_future = self._manager.cluster.request_update()
370
+ else:
371
+ backoff = True
372
+ else:
373
+ fetched_offsets.update(offsets)
374
+ if not retry:
375
+ return fetched_offsets
376
+ timestamps = {tp: timestamps[tp] for tp in retry}
377
+
378
+ if refresh_future:
379
+ try:
380
+ await self._manager.wait_for(refresh_future, timer.timeout_ms)
381
+ except Errors.RetriableError:
382
+ backoff = True
383
+
384
+ if backoff:
385
+ delay = self.config['retry_backoff_ms'] / 1000
386
+ if timer.timeout_ms is not None:
387
+ delay = min(delay, timer.timeout_ms / 1000)
388
+ await self._manager._net.sleep(delay)
389
+
390
+ timer.maybe_raise()
391
+
392
+ def beginning_offsets(self, partitions, timeout_ms=None):
393
+ """Fetch earliest (oldest) offset for each partition.
394
+
395
+ Blocks until offsets are obtained, a non-retriable exception is raised
396
+ or ``timeout_ms`` passed.
397
+
398
+ Arguments:
399
+ partitions ([TopicPartition]): List of partitions for list offsets.
400
+ timeout_ms (int, optional): The maximum time in milliseconds to block.
401
+
402
+ Returns:
403
+ {TopicPartition: int}: Mapping of partition to retrieved offset.
404
+
405
+ Raises:
406
+ KafkaTimeoutError if timeout_ms provided.
407
+ """
408
+ return self.beginning_or_end_offset(
409
+ partitions, OffsetSpec.EARLIEST, timeout_ms)
410
+
411
+ def end_offsets(self, partitions, timeout_ms=None):
412
+ """Fetch latest (most recent) offset for each partition.
413
+
414
+ Blocks until offsets are obtained, a non-retriable exception is raised
415
+ or ``timeout_ms`` passed.
416
+
417
+ Arguments:
418
+ partitions ([TopicPartition]): List of partitions for list offsets.
419
+ timeout_ms (int, optional): The maximum time in milliseconds to block.
420
+
421
+ Returns:
422
+ {TopicPartition: int}: Mapping of partition to retrieved offset.
423
+
424
+ Raises:
425
+ KafkaTimeoutError if timeout_ms provided.
426
+ """
427
+ return self.beginning_or_end_offset(
428
+ partitions, OffsetSpec.LATEST, timeout_ms)
429
+
430
+ def beginning_or_end_offset(self, partitions, timestamp, timeout_ms=None):
431
+ """Fetch offset for each partition using ``timestamp``.
432
+
433
+ Blocks until offsets are obtained, a non-retriable exception is raised
434
+ or ``timeout_ms`` passed.
435
+
436
+ Arguments:
437
+ partitions ([TopicPartition]): List of partitions for list offsets.
438
+ timestamp (int or OffsetSpec): OffsetSpec.LATEST (-1) for the latest
439
+ available, OffsetSpec.EARLIEST (-2) for the earliest available.
440
+ Otherwise timestamp is treated as epoch milliseconds.
441
+ timeout_ms (int, optional): The maximum time in milliseconds to block.
442
+
443
+ Returns:
444
+ {TopicPartition: int}: Mapping of partition to retrieved offset.
445
+
446
+ Raises:
447
+ UnsupportedVersionError if broker does not support any compatible
448
+ ListOffsetsRequest api version.
449
+ KafkaTimeoutError if timeout_ms provided.
450
+ """
451
+ timestamps = dict([(tp, timestamp) for tp in partitions])
452
+ offsets = self._net.run(self._fetch_offsets_by_times_async, timestamps, timeout_ms)
453
+ for tp in timestamps:
454
+ offsets[tp] = offsets[tp].offset
455
+ return offsets
456
+
457
+ def fetched_records(self, max_records=None, update_offsets=True):
458
+ """Returns previously fetched records and updates consumed offsets.
459
+
460
+ Arguments:
461
+ max_records (int): Maximum number of records returned. Defaults
462
+ to max_poll_records configuration.
463
+
464
+ Raises:
465
+ OffsetOutOfRangeError: if no subscription offset_reset_strategy
466
+ CorruptRecordError: if message crc validation fails (check_crcs
467
+ must be set to True)
468
+ RecordTooLargeError: if a message is larger than the currently
469
+ configured max_partition_fetch_bytes
470
+ TopicAuthorizationError: if consumer is not authorized to fetch
471
+ messages from the topic
472
+ ValueError: if max_records is <= 0
473
+
474
+ Returns: (records (dict), partial (bool))
475
+ records: {TopicPartition: [messages]}
476
+ partial: True if records returned did not fully drain any pending
477
+ partition requests. This may be useful for choosing when to
478
+ pipeline additional fetch requests.
479
+ """
480
+ if max_records is None:
481
+ max_records = self.config['max_poll_records']
482
+ if max_records <= 0:
483
+ raise ValueError('max_records must be > 0')
484
+
485
+ if self._next_in_line_exception_metadata is not None:
486
+ exc_meta = self._next_in_line_exception_metadata
487
+ self._next_in_line_exception_metadata = None
488
+ tp = exc_meta.partition
489
+ if self._subscriptions.is_fetchable(tp) and self._subscriptions.position(tp).offset == exc_meta.fetched_offset:
490
+ raise exc_meta.exception
491
+
492
+ drained = collections.defaultdict(list)
493
+ records_remaining = max_records
494
+ # Needed to construct ExceptionMetadata if any exception is found when processing completed_fetch
495
+ fetched_partition = None
496
+ fetched_offset = -1
497
+
498
+ # KAFKA-7548: restore parked data for any partition that the user
499
+ # has since resumed. Raw completions go back into the fetch queue;
500
+ # parsed records take the in-line slot when free, otherwise stay
501
+ # parked and get picked up on a subsequent call.
502
+ for tp in list(self._paused_completed_fetches):
503
+ if not self._subscriptions.is_paused(tp):
504
+ self._completed_fetches.append(self._paused_completed_fetches.pop(tp))
505
+ if self._next_partition_records is None:
506
+ for tp in list(self._paused_partition_records):
507
+ if not self._subscriptions.is_paused(tp):
508
+ self._next_partition_records = self._paused_partition_records.pop(tp)
509
+ break
510
+
511
+ try:
512
+ while records_remaining > 0:
513
+ if not self._next_partition_records:
514
+ if not self._completed_fetches:
515
+ break
516
+ completion = self._completed_fetches.popleft()
517
+ if self._subscriptions.is_paused(completion.topic_partition):
518
+ self._paused_completed_fetches[completion.topic_partition] = completion
519
+ continue
520
+ fetched_partition = completion.topic_partition
521
+ fetched_offset = completion.fetched_offset
522
+ self._next_partition_records = self._parse_fetched_data(completion)
523
+ else:
524
+ tp = self._next_partition_records.topic_partition
525
+ if self._subscriptions.is_paused(tp):
526
+ self._paused_partition_records[tp] = self._next_partition_records
527
+ self._next_partition_records = None
528
+ continue
529
+ fetched_partition = tp
530
+ fetched_offset = self._next_partition_records.next_fetch_offset
531
+ records_remaining -= self._append(drained,
532
+ self._next_partition_records,
533
+ records_remaining,
534
+ update_offsets)
535
+ except Exception as e:
536
+ if not drained:
537
+ raise e
538
+ # To be thrown in the next call of this method
539
+ self._next_in_line_exception_metadata = ExceptionMetadata(fetched_partition, fetched_offset, e)
540
+ return dict(drained), bool(self._completed_fetches)
541
+
542
+ def _append(self, drained, part, max_records, update_offsets):
543
+ if not part:
544
+ return 0
545
+
546
+ tp = part.topic_partition
547
+ if not self._subscriptions.is_assigned(tp):
548
+ # this can happen when a rebalance happened before
549
+ # fetched records are returned to the consumer's poll call
550
+ log.debug("Not returning fetched records for partition %s"
551
+ " since it is no longer assigned", tp)
552
+ elif not self._subscriptions.is_fetchable(tp):
553
+ # this can happen when a partition is paused before
554
+ # fetched records are returned to the consumer's poll call
555
+ log.debug("Not returning fetched records for assigned partition"
556
+ " %s since it is no longer fetchable", tp)
557
+
558
+ else:
559
+ # note that the position should always be available
560
+ # as long as the partition is still assigned
561
+ position = self._subscriptions.assignment[tp].position
562
+ if part.next_fetch_offset == position.offset:
563
+ log.debug("Returning fetched records at offset %d for assigned"
564
+ " partition %s", position.offset, tp)
565
+ part_records = part.take(max_records)
566
+ # list.extend([]) is a noop, but because drained is a defaultdict
567
+ # we should avoid initializing the default list unless there are records
568
+ if part_records:
569
+ drained[tp].extend(part_records)
570
+ # We want to increment subscription position if (1) we're using consumer.poll(),
571
+ # or (2) we didn't return any records (consumer iterator will update position
572
+ # when each message is yielded). There may be edge cases where we re-fetch records
573
+ # that we'll end up skipping, but for now we'll live with that.
574
+ highwater = self._subscriptions.assignment[tp].highwater
575
+ if highwater is not None and self._sensors:
576
+ self._sensors.records_fetch_lag.record(highwater - part.next_fetch_offset)
577
+ if update_offsets or not part_records:
578
+ log.debug("Updating fetch position for assigned partition %s to %s (leader epoch %s)",
579
+ tp, part.next_fetch_offset, part.leader_epoch)
580
+ self._subscriptions.assignment[tp].position = OffsetAndMetadata(
581
+ part.next_fetch_offset, '', part.leader_epoch)
582
+ return len(part_records)
583
+
584
+ else:
585
+ # these records aren't next in line based on the last consumed
586
+ # position, ignore them they must be from an obsolete request
587
+ log.debug("Ignoring fetched records for %s at offset %s since"
588
+ " the current position is %d", tp, part.next_fetch_offset,
589
+ position.offset)
590
+
591
+ part.drain()
592
+ return 0
593
+
594
+ def _reset_offset_if_needed(self, partition, timestamp, offset):
595
+ # we might lose the assignment while fetching the offset, or the user might seek to a different offset,
596
+ # so verify it is still assigned and still in need of the requested reset
597
+ if not self._subscriptions.is_assigned(partition):
598
+ log.debug("Skipping reset of partition %s since it is no longer assigned", partition)
599
+ elif not self._subscriptions.is_offset_reset_needed(partition):
600
+ log.debug("Skipping reset of partition %s since reset is no longer needed", partition)
601
+ elif timestamp and not timestamp == self._subscriptions.assignment[partition].reset_strategy:
602
+ log.debug("Skipping reset of partition %s since an alternative reset has been requested", partition)
603
+ else:
604
+ log.info("Resetting offset for partition %s to offset %s.", partition, offset)
605
+ self._subscriptions.seek(partition, offset)
606
+
607
+ async def _reset_offsets_async(self, timeout_ms=None):
608
+ """Drive resets to completion or until the timer expires.
609
+
610
+ Each iteration fans out per-node ListOffsets requests concurrently
611
+ and awaits all of them. After a retriable failure (NotLeader, etc.)
612
+ a partition's next_allowed_retry_time is set ``retry_backoff_ms`` in
613
+ the future; the loop sleeps until that time and retries rather than
614
+ relying on an external caller to redrive. If all partitions have
615
+ unknown leaders, awaits a metadata refresh and retries within the
616
+ remaining budget.
617
+
618
+ Arguments:
619
+ timeout_ms (int, optional): Hard upper bound on the loop's
620
+ wall-clock. None falls back to ``request_timeout_ms`` so a
621
+ deleted-topic / permanently-unknown-leader partition can't
622
+ spin the loop forever. The metadata-refresh wait inside
623
+ the loop is capped by ``min(remaining_timer, request_timeout_ms)``.
624
+
625
+ Per-node failures are caught inside _reset_offsets_for_node and
626
+ stuffed into self._cached_list_offsets_exception; the next call to
627
+ reset_offsets_if_needed surfaces them.
628
+ """
629
+ if timeout_ms is None:
630
+ timeout_ms = self.config['request_timeout_ms']
631
+ timer = Timer(timeout_ms)
632
+ while not timer.expired:
633
+ if self._cached_list_offsets_exception is not None:
634
+ return
635
+ partitions = self._subscriptions.partitions_needing_reset()
636
+ if not partitions:
637
+ next_retry = self._subscriptions.next_offset_reset_retry_time()
638
+ if next_retry is None:
639
+ return
640
+ delay = max(0.0, next_retry - time.monotonic())
641
+ if timer.timeout_ms is not None:
642
+ delay = min(delay, timer.timeout_ms / 1000)
643
+ if delay > 0:
644
+ await self._manager._net.sleep(delay)
645
+ continue
646
+
647
+ offset_resets = {}
648
+ for tp in partitions:
649
+ ts = self._subscriptions.assignment[tp].reset_strategy
650
+ if ts:
651
+ offset_resets[tp] = ts
652
+ if not offset_resets:
653
+ return
654
+
655
+ timestamps_by_node = self._group_list_offset_requests(offset_resets)
656
+ if not timestamps_by_node:
657
+ # All requested partitions have unknown / unavailable leaders.
658
+ # _group_list_offset_requests has already requested a metadata
659
+ # refresh; await it within the remaining budget (capped at
660
+ # request_timeout_ms for any single broker round-trip).
661
+ metadata_update = self._manager.cluster.request_update()
662
+ wait_ms = self.config['request_timeout_ms']
663
+ if timer.timeout_ms is not None:
664
+ wait_ms = min(wait_ms, timer.timeout_ms)
665
+ try:
666
+ await self._manager.wait_for(metadata_update, wait_ms)
667
+ except Errors.KafkaTimeoutError:
668
+ pass
669
+ continue
670
+
671
+ log.debug('Resetting offsets for %s', set(offset_resets.keys()))
672
+ # Gather: schedule all per-node tasks concurrently, then await.
673
+ node_tasks = []
674
+ for node_id, t_and_e in timestamps_by_node.items():
675
+ node_partitions = set(t_and_e.keys())
676
+ expire_at = time.monotonic() + self.config['request_timeout_ms'] / 1000
677
+ self._subscriptions.set_reset_pending(node_partitions, expire_at)
678
+ node_tasks.append(self._manager.call_soon(
679
+ self._reset_offsets_for_node, node_id, t_and_e, node_partitions))
680
+ for task in node_tasks:
681
+ await task
682
+
683
+ async def _reset_offsets_for_node(self, node_id, timestamps_and_epochs, partitions):
684
+ try:
685
+ fetched_offsets, partitions_to_retry = await self._send_list_offsets_request(node_id, timestamps_and_epochs)
686
+ except Exception as error:
687
+ self._subscriptions.reset_failed(partitions, time.monotonic() + self.config['retry_backoff_ms'] / 1000)
688
+ self._manager.cluster.request_update()
689
+ if not isinstance(error, Errors.RetriableError):
690
+ if not self._cached_list_offsets_exception:
691
+ self._cached_list_offsets_exception = error
692
+ else:
693
+ log.error("Discarding error in ListOffsetResponse because another error is pending: %s", error)
694
+ return
695
+
696
+ if partitions_to_retry:
697
+ self._subscriptions.reset_failed(partitions_to_retry, time.monotonic() + self.config['retry_backoff_ms'] / 1000)
698
+ self._manager.cluster.request_update()
699
+ for partition, offset in fetched_offsets.items():
700
+ ts, _epoch = timestamps_and_epochs[partition]
701
+ self._reset_offset_if_needed(partition, ts, offset.offset)
702
+
703
+ async def _send_list_offsets_requests(self, timestamps):
704
+ """Fetch offsets for each partition in timestamps dict. This may send
705
+ request to multiple nodes, based on who is Leader for partition.
706
+
707
+ Per-node requests are dispatched concurrently; if any fails, the first
708
+ exception encountered propagates and the remaining results are dropped.
709
+
710
+ Arguments:
711
+ timestamps (dict): {TopicPartition: int} mapping of fetching
712
+ timestamps.
713
+
714
+ Returns:
715
+ (fetched_offsets, partitions_to_retry):
716
+ dict[TopicPartition, OffsetAndTimestamp],
717
+ set[TopicPartition]
718
+
719
+ Raises:
720
+ StaleMetadata: if no node has known leader for any partition.
721
+ """
722
+ timestamps_by_node = self._group_list_offset_requests(timestamps)
723
+ if not timestamps_by_node:
724
+ raise Errors.StaleMetadata()
725
+
726
+ futures = [
727
+ self._manager.call_soon(self._send_list_offsets_request, node_id, ts)
728
+ for node_id, ts in timestamps_by_node.items()
729
+ ]
730
+
731
+ fetched_offsets = dict()
732
+ partitions_to_retry = set()
733
+ for f in futures:
734
+ offs, retry = await f
735
+ fetched_offsets.update(offs)
736
+ partitions_to_retry.update(retry)
737
+ return fetched_offsets, partitions_to_retry
738
+
739
+ def _group_list_offset_requests(self, timestamps):
740
+ timestamps_by_node = collections.defaultdict(dict)
741
+ for partition, timestamp in timestamps.items():
742
+ node_id = self._manager.cluster.leader_for_partition(partition)
743
+ if node_id is None:
744
+ self._manager.cluster.add_topic(partition.topic)
745
+ log.debug("Partition %s is unknown for fetching offset", partition)
746
+ self._manager.cluster.request_update()
747
+ elif node_id == -1:
748
+ log.debug("Leader for partition %s unavailable for fetching "
749
+ "offset, wait for metadata refresh", partition)
750
+ self._manager.cluster.request_update()
751
+ else:
752
+ leader_epoch = -1
753
+ timestamps_by_node[node_id][partition] = (timestamp, leader_epoch)
754
+ return dict(timestamps_by_node)
755
+
756
+ async def _send_list_offsets_request(self, node_id, timestamps_and_epochs):
757
+ """Send single ListOffsetsResponse to node_id
758
+
759
+ Returns:
760
+ (fetched_offsets, partitions_to_retry):
761
+ dict[TopicPartition, OffsetAndTimestamp],
762
+ set[TopicPartition]
763
+
764
+ Raises:
765
+ TopicAuthorizationFailedError: if any topic returned an auth error
766
+ """
767
+ min_version = 1 if any(res[0] >= 0 for res in timestamps_and_epochs.values()) else 0
768
+ min_version = max(min_version, ListOffsetsRequest.min_version_for_isolation_level(self._isolation_level))
769
+ by_topic = collections.defaultdict(list)
770
+ for tp, (timestamp, leader_epoch) in timestamps_and_epochs.items():
771
+ data = _ListOffsetsPartition(
772
+ partition_index=tp.partition,
773
+ current_leader_epoch=leader_epoch,
774
+ timestamp=timestamp)
775
+ by_topic[tp.topic].append(data)
776
+
777
+ request = ListOffsetsRequest(
778
+ isolation_level=self._isolation_level,
779
+ topics=list(by_topic.items()),
780
+ min_version=min_version,
781
+ )
782
+
783
+ log.debug("Sending ListOffsetRequest %s to broker %s", request, node_id)
784
+ response = await self._manager.send(request, node_id=node_id)
785
+ return self._handle_list_offsets_response(response)
786
+
787
+ def _handle_list_offsets_response(self, response):
788
+ """Parse a ListOffsets response.
789
+
790
+ Returns:
791
+ (fetched_offsets, partitions_to_retry):
792
+ dict[TopicPartition, OffsetAndTimestamp],
793
+ set[TopicPartition]
794
+
795
+ Raises:
796
+ TopicAuthorizationFailedError: if any topic returned an auth error
797
+ ValueError: if ListOffsetsResponse v0 and > 1 offset returned
798
+ """
799
+ fetched_offsets = dict()
800
+ partitions_to_retry = set()
801
+ unauthorized_topics = set()
802
+ for topic_data in response.topics:
803
+ for partition_info in topic_data.partitions:
804
+ tp = TopicPartition(topic_data.name, partition_info.partition_index)
805
+ error_code = partition_info.error_code
806
+ error_type = Errors.for_code(error_code)
807
+ if error_type is Errors.NoError:
808
+ if response.API_VERSION == 0:
809
+ offsets = partition_info.old_style_offsets
810
+ if len(offsets) > 1:
811
+ raise ValueError('Expected ListOffsetsResponse with one offset')
812
+ offset = offsets[0] if offsets else UNKNOWN_OFFSET
813
+ else:
814
+ offset = partition_info.offset
815
+ timestamp = partition_info.timestamp
816
+ leader_epoch = partition_info.leader_epoch
817
+ # DataContainer currently does not set default for
818
+ # out-of-version fields; so we need to handle explicitly
819
+ if timestamp is None:
820
+ timestamp = -1
821
+ if leader_epoch is None:
822
+ leader_epoch = -1
823
+ log.debug("Handling ListOffsetsResponse response for %s. "
824
+ "Fetched offset %s, timestamp %s, leader_epoch %s",
825
+ tp, offset, timestamp, leader_epoch)
826
+ if offset != UNKNOWN_OFFSET:
827
+ fetched_offsets[tp] = OffsetAndTimestamp(offset, timestamp, leader_epoch)
828
+ elif error_type is Errors.UnsupportedForMessageFormatError:
829
+ # The message format on the broker side is before 0.10.0, which means it does not
830
+ # support timestamps. We treat this case the same as if we weren't able to find an
831
+ # offset corresponding to the requested timestamp and leave it out of the result.
832
+ log.debug("Cannot search by timestamp for partition %s because the"
833
+ " message format version is before 0.10.0", tp)
834
+ elif error_type in (Errors.NotLeaderForPartitionError,
835
+ Errors.ReplicaNotAvailableError,
836
+ Errors.KafkaStorageError,
837
+ Errors.OffsetNotAvailableError,
838
+ Errors.LeaderNotAvailableError):
839
+ log.debug("Attempt to fetch offsets for partition %s failed due"
840
+ " to %s, retrying.", error_type.__name__, tp)
841
+ partitions_to_retry.add(tp)
842
+ elif error_type is Errors.UnknownTopicOrPartitionError:
843
+ log.warning("Received unknown topic or partition error in ListOffsets "
844
+ "request for partition %s. The topic/partition " +
845
+ "may not exist or the user may not have Describe access "
846
+ "to it.", tp)
847
+ partitions_to_retry.add(tp)
848
+ elif error_type is Errors.TopicAuthorizationFailedError:
849
+ unauthorized_topics.add(tp.topic)
850
+ else:
851
+ log.warning("Attempt to fetch offsets for partition %s failed due to:"
852
+ " %s", tp, error_type.__name__)
853
+ partitions_to_retry.add(tp)
854
+ if unauthorized_topics:
855
+ raise Errors.TopicAuthorizationFailedError(unauthorized_topics)
856
+ return fetched_offsets, partitions_to_retry
857
+
858
+ # ------------------------------------------------------------------
859
+ # KIP-320: offset validation via OffsetForLeaderEpoch
860
+ # ------------------------------------------------------------------
861
+
862
+ def maybe_validate_positions(self):
863
+ """Walk assigned partitions; mark any whose cluster leader epoch has
864
+ advanced beyond the position's epoch as awaiting validation.
865
+
866
+ Cheap fire-and-forget marker; the actual RPC fan-out runs in
867
+ ``validate_offsets_if_needed`` -> ``_validate_offsets_async``.
868
+ Idempotent: partitions already awaiting validation, awaiting
869
+ reset, or with no recorded epoch are skipped inside
870
+ ``maybe_validate_position``.
871
+ """
872
+ for tp in self._subscriptions.assigned_partitions():
873
+ current_epoch = self._manager.cluster.leader_epoch_for_partition(tp)
874
+ self._subscriptions.maybe_validate_position_for_current_leader(tp, current_epoch)
875
+
876
+ def validate_offsets_if_needed(self, timeout_ms=None):
877
+ """Schedule any pending position validations and return the in-flight Task.
878
+
879
+ Mirrors :meth:`reset_offsets_if_needed`: returns a cached Future
880
+ shared across callers so concurrent ``consumer.poll`` and
881
+ ``consumer.position`` callers don't race the same partition into
882
+ duplicate OffsetForLeaderEpoch requests.
883
+
884
+ Raises:
885
+ LogTruncationError: if a previous validation detected truncation
886
+ on one or more partitions. The exception is cleared after
887
+ being raised so subsequent calls will re-attempt validation.
888
+ """
889
+ exc, self._cached_log_truncation = self._cached_log_truncation, None
890
+ if exc:
891
+ raise exc
892
+
893
+ if self._validation_task is not None and not self._validation_task.is_done:
894
+ return self._validation_task
895
+
896
+ if not self._subscriptions.partitions_needing_validation():
897
+ return None
898
+
899
+ self._validation_task = self._manager.call_soon(
900
+ self._validate_offsets_async, timeout_ms)
901
+ return self._validation_task
902
+
903
+ async def _validate_offsets_async(self, timeout_ms=None):
904
+ """Drive offset validations to completion or until the timer expires.
905
+
906
+ Same overall shape as ``_reset_offsets_async``: per-node fan-out.
907
+ After a retriable failure (FencedLeaderEpoch, etc.) a partition's
908
+ next_allowed_retry_time is set ``retry_backoff_ms`` in the future;
909
+ the loop sleeps until that time and retries rather than relying on
910
+ an external caller to redrive. Stops on first ``LogTruncationError``
911
+ accumulation; the next caller surfaces it.
912
+ """
913
+ if timeout_ms is None:
914
+ timeout_ms = self.config['request_timeout_ms']
915
+ timer = Timer(timeout_ms)
916
+ while not timer.expired:
917
+ if self._cached_log_truncation is not None:
918
+ return
919
+ partitions = self._subscriptions.partitions_needing_validation()
920
+ if not partitions:
921
+ next_retry = self._subscriptions.next_offset_validation_retry_time()
922
+ if next_retry is None:
923
+ return
924
+ delay = max(0.0, next_retry - time.monotonic())
925
+ if timer.timeout_ms is not None:
926
+ delay = min(delay, timer.timeout_ms / 1000)
927
+ if delay > 0:
928
+ await self._manager._net.sleep(delay)
929
+ continue
930
+
931
+ positions = {}
932
+ for tp in partitions:
933
+ state = self._subscriptions.assignment[tp]
934
+ if state.position is not None and state.position.leader_epoch >= 0:
935
+ positions[tp] = state.position
936
+ if not positions:
937
+ return
938
+
939
+ requests_by_node = self._group_offset_for_leader_epoch_requests(positions)
940
+ if not requests_by_node:
941
+ metadata_update = self._manager.cluster.request_update()
942
+ wait_ms = self.config['request_timeout_ms']
943
+ if timer.timeout_ms is not None:
944
+ wait_ms = min(wait_ms, timer.timeout_ms)
945
+ try:
946
+ await self._manager.wait_for(metadata_update, wait_ms)
947
+ except Errors.KafkaTimeoutError:
948
+ pass
949
+ continue
950
+
951
+ log.debug('Validating offsets for %s', set(positions.keys()))
952
+ node_tasks = []
953
+ for node_id, payload in requests_by_node.items():
954
+ node_partitions = set(payload.keys())
955
+ expire_at = time.monotonic() + self.config['request_timeout_ms'] / 1000
956
+ self._subscriptions.set_validation_pending(node_partitions, expire_at)
957
+ node_tasks.append(self._manager.call_soon(
958
+ self._validate_offsets_for_node, node_id, payload))
959
+ for task in node_tasks:
960
+ await task
961
+
962
+ async def _validate_offsets_for_node(self, node_id, partitions_to_positions):
963
+ try:
964
+ truncations = await self._send_offset_for_leader_epoch_request(
965
+ node_id, partitions_to_positions)
966
+ except Exception as error:
967
+ self._subscriptions.validation_failed(
968
+ set(partitions_to_positions),
969
+ time.monotonic() + self.config['retry_backoff_ms'] / 1000)
970
+ self._manager.cluster.request_update()
971
+ if not isinstance(error, Errors.RetriableError):
972
+ log.error("Non-retriable error from OffsetForLeaderEpoch on node %s: %s",
973
+ node_id, error)
974
+ return
975
+
976
+ if truncations:
977
+ if self._cached_log_truncation is None:
978
+ self._cached_log_truncation = Errors.LogTruncationError(truncations)
979
+ else:
980
+ self._cached_log_truncation.divergent_offsets.update(truncations)
981
+
982
+ def _group_offset_for_leader_epoch_requests(self, positions):
983
+ """Group {TopicPartition: OffsetAndMetadata} by leader node.
984
+
985
+ Partitions whose leader is unknown trigger a metadata refresh and
986
+ are dropped from this round. Partitions whose position lacks an
987
+ epoch are also dropped - they can't be validated.
988
+ """
989
+ by_node = collections.defaultdict(dict)
990
+ for tp, position in positions.items():
991
+ if position.leader_epoch < 0:
992
+ continue
993
+ node_id = self._manager.cluster.leader_for_partition(tp)
994
+ if node_id is None:
995
+ self._manager.cluster.add_topic(tp.topic)
996
+ self._manager.cluster.request_update()
997
+ elif node_id == -1:
998
+ self._manager.cluster.request_update()
999
+ else:
1000
+ by_node[node_id][tp] = position
1001
+ return dict(by_node)
1002
+
1003
+ async def _send_offset_for_leader_epoch_request(self, node_id, partitions_to_positions):
1004
+ """Send one OffsetForLeaderEpoch request and return any truncations.
1005
+
1006
+ Returns:
1007
+ dict[TopicPartition, OffsetAndMetadata]: partitions whose log
1008
+ was truncated past their position. Successful validations
1009
+ update :class:`SubscriptionState` directly via
1010
+ ``complete_validation``; retriable per-partition errors leave
1011
+ ``next_allowed_retry_time`` set so the outer loop will retry.
1012
+
1013
+ Raises:
1014
+ TopicAuthorizationFailedError: if any topic returned an auth error.
1015
+ """
1016
+ by_topic = collections.defaultdict(list)
1017
+ for tp, position in partitions_to_positions.items():
1018
+ current_leader_epoch = self._manager.cluster.leader_epoch_for_partition(tp)
1019
+ if current_leader_epoch is None or current_leader_epoch < 0:
1020
+ current_leader_epoch = -1
1021
+ by_topic[tp.topic].append(_OffsetForLeaderPartition(
1022
+ partition=tp.partition,
1023
+ current_leader_epoch=current_leader_epoch,
1024
+ leader_epoch=position.leader_epoch,
1025
+ ))
1026
+
1027
+ request = OffsetForLeaderEpochRequest(
1028
+ replica_id=-1,
1029
+ topics=list(by_topic.items()),
1030
+ )
1031
+
1032
+ log.debug("Sending OffsetForLeaderEpochRequest %s to broker %s", request, node_id)
1033
+ response = await self._manager.send(request, node_id=node_id)
1034
+ return self._handle_offset_for_leader_epoch_response(response, partitions_to_positions)
1035
+
1036
+ def _handle_offset_for_leader_epoch_response(self, response, requested_positions):
1037
+ """Parse an OffsetForLeaderEpoch response.
1038
+
1039
+ Side effects: calls ``complete_validation`` / ``validation_failed``
1040
+ / ``request_position_validation`` on the subscription state as
1041
+ appropriate for each partition's response code.
1042
+
1043
+ Returns:
1044
+ dict[TopicPartition, OffsetAndMetadata]: subset of requested
1045
+ partitions where end_offset < requested position (truncation).
1046
+ """
1047
+ truncations = {}
1048
+ unauthorized_topics = set()
1049
+ retry_at = time.monotonic() + self.config['retry_backoff_ms'] / 1000
1050
+ retry = set()
1051
+
1052
+ for topic_data in response.topics:
1053
+ for partition_info in topic_data.partitions:
1054
+ tp = TopicPartition(topic_data.topic, partition_info.partition)
1055
+ requested = requested_positions.get(tp)
1056
+ if requested is None:
1057
+ continue
1058
+ error_type = Errors.for_code(partition_info.error_code)
1059
+
1060
+ if error_type is Errors.NoError:
1061
+ end_offset = partition_info.end_offset
1062
+ end_epoch = partition_info.leader_epoch
1063
+ if end_epoch is None:
1064
+ end_epoch = -1
1065
+ current = self._subscriptions.assignment[tp].position if \
1066
+ self._subscriptions.is_assigned(tp) else None
1067
+ # Position may have changed (seek, rebalance) since request
1068
+ # was sent; skip stale completions.
1069
+ if current is None or current != requested:
1070
+ log.debug("Skipping validation completion for %s: position "
1071
+ "changed since request was sent", tp)
1072
+ continue
1073
+
1074
+ has_reset_policy = self._subscriptions.has_default_offset_reset_policy()
1075
+
1076
+ if end_offset < 0 or end_epoch < 0:
1077
+ # UNDEFINED_EPOCH / UNDEFINED_EPOCH_OFFSET: broker has
1078
+ # no record of our requested epoch on this partition.
1079
+ # Mirror Java SubscriptionState.maybeCompleteValidation:
1080
+ # this is truncation with no known diverging offset.
1081
+ if has_reset_policy:
1082
+ log.info("Truncation detected for %s at position %s "
1083
+ "(broker returned UNDEFINED end_offset/leader_epoch); "
1084
+ "resetting offset per auto_offset_reset policy",
1085
+ tp, current.offset)
1086
+ self._subscriptions.request_offset_reset(tp)
1087
+ else:
1088
+ log.warning("Truncation detected for %s at position %s "
1089
+ "(broker returned UNDEFINED end_offset/leader_epoch), "
1090
+ "but no reset policy is set", tp, current.offset)
1091
+ truncations[tp] = None
1092
+ self._subscriptions.complete_validation(tp)
1093
+ elif end_offset < current.offset:
1094
+ # Broker confirms the diverging point. Seek there
1095
+ # directly instead of resetting via policy, so the
1096
+ # consumer only re-reads records past the divergence
1097
+ # (Java: state.seekValidated(newPosition)).
1098
+ divergent = OffsetAndMetadata(end_offset, '', end_epoch)
1099
+ if has_reset_policy:
1100
+ log.info("Truncation detected for %s at position %s; "
1101
+ "seeking to first diverging offset %s",
1102
+ tp, current.offset, divergent)
1103
+ self._subscriptions.seek(tp, divergent)
1104
+ else:
1105
+ log.warning("Truncation detected for %s at position %s "
1106
+ "(first diverging offset is %s), but no reset "
1107
+ "policy is set", tp, current.offset, divergent)
1108
+ truncations[tp] = divergent
1109
+ self._subscriptions.complete_validation(tp)
1110
+ else:
1111
+ validated = OffsetAndMetadata(
1112
+ current.offset, current.metadata, end_epoch)
1113
+ self._subscriptions.complete_validation(tp, validated)
1114
+
1115
+ elif error_type in (Errors.FencedLeaderEpochError,
1116
+ Errors.UnknownLeaderEpochError,
1117
+ Errors.NotLeaderForPartitionError,
1118
+ Errors.ReplicaNotAvailableError,
1119
+ Errors.KafkaStorageError,
1120
+ Errors.LeaderNotAvailableError):
1121
+ log.debug("OffsetForLeaderEpoch for %s returned retriable %s; "
1122
+ "will retry after backoff", tp, error_type.__name__)
1123
+ self._manager.cluster.request_update()
1124
+ retry.add(tp)
1125
+ elif error_type is Errors.UnknownTopicOrPartitionError:
1126
+ log.warning("OffsetForLeaderEpoch for %s: unknown topic/partition", tp)
1127
+ retry.add(tp)
1128
+ elif error_type is Errors.TopicAuthorizationFailedError:
1129
+ unauthorized_topics.add(tp.topic)
1130
+ else:
1131
+ log.warning("OffsetForLeaderEpoch for %s failed with %s",
1132
+ tp, error_type.__name__)
1133
+ retry.add(tp)
1134
+
1135
+ if retry:
1136
+ self._subscriptions.validation_failed(retry, retry_at)
1137
+ if unauthorized_topics:
1138
+ raise Errors.TopicAuthorizationFailedError(unauthorized_topics)
1139
+ return truncations
1140
+
1141
+ def _fetchable_partitions(self):
1142
+ fetchable = self._subscriptions.fetchable_partitions()
1143
+ # do not fetch a partition if we have a pending fetch response to process
1144
+ # use copy to avoid runtimeerror on mutation from different thread
1145
+ discard = {fetch.topic_partition for fetch in self._completed_fetches.copy()}
1146
+ current = self._next_partition_records
1147
+ if current:
1148
+ discard.add(current.topic_partition)
1149
+ discard.update(self._paused_completed_fetches)
1150
+ discard.update(self._paused_partition_records)
1151
+ return [tp for tp in fetchable if tp not in discard]
1152
+
1153
+ def _select_read_replica(self, tp):
1154
+ """Pick the node to fetch from for ``tp``: a cached preferred read
1155
+ replica (KIP-392) when valid and *still listed as a replica of
1156
+ ``tp``*, otherwise the partition leader. A preferred replica that
1157
+ has been demoted out of the partition's replica set (or fell out
1158
+ of cluster metadata entirely) is cleared so the next fetch goes
1159
+ to the leader.
1160
+ """
1161
+ preferred = self._subscriptions.assignment[tp].preferred_read_replica()
1162
+ if preferred is None:
1163
+ return self._manager.cluster.leader_for_partition(tp)
1164
+ if not self._manager.cluster.is_replica_node(tp, preferred):
1165
+ self._subscriptions.assignment[tp].clear_preferred_read_replica()
1166
+ leader = self._manager.cluster.leader_for_partition(tp)
1167
+ log.debug("Preferred read replica %s for partition %s no longer"
1168
+ " online or no longer a replica; falling back to leader %s",
1169
+ preferred, tp, leader)
1170
+ return leader
1171
+ return preferred
1172
+
1173
+ def _create_fetch_requests(self):
1174
+ """Create fetch requests for all assigned partitions, grouped by node.
1175
+
1176
+ FetchRequests skipped if no leader, or node has requests in flight
1177
+
1178
+ Returns:
1179
+ dict: {node_id: (FetchRequest, {TopicPartition: fetch_offset}), ...}
1180
+ """
1181
+ # TODO:
1182
+ # v13 topic ids (KIP-516)
1183
+ # v14 tiered storage (KIP-405)
1184
+ # v15 replica state (KIP-903)
1185
+ # v16 node endpoints (KIP-951)
1186
+ # v17 directory id (KIP-853)
1187
+ max_version = 12
1188
+ fetchable = collections.defaultdict(collections.OrderedDict)
1189
+ for tp in self._fetchable_partitions():
1190
+ node_id = self._select_read_replica(tp)
1191
+
1192
+ position = self._subscriptions.assignment[tp].position
1193
+
1194
+ # fetch if there is a leader and no in-flight requests
1195
+ if node_id is None or node_id == -1:
1196
+ log.debug("No leader found for partition %s."
1197
+ " Requesting metadata update", tp)
1198
+ self._manager.cluster.request_update()
1199
+
1200
+ elif self._manager.connection_delay(node_id) > 0:
1201
+ # If we try to send during the reconnect backoff window, then the request is just
1202
+ # going to be failed anyway before being sent, so skip the send for now
1203
+ log.debug("Skipping fetch for partition %s because node %s is awaiting reconnect backoff",
1204
+ tp, node_id)
1205
+
1206
+ # TODO: handle throttle_delay in kafka.net
1207
+ elif self._client.throttle_delay(node_id) > 0:
1208
+ # If we try to send while throttled, then the request is just
1209
+ # going to be failed anyway before being sent, so skip the send for now
1210
+ log.debug("Skipping fetch for partition %s because node %s is throttled",
1211
+ tp, node_id)
1212
+
1213
+ elif node_id in self._nodes_with_pending_fetch_requests:
1214
+ log.debug("Skipping fetch for partition %s because there is a pending fetch request to node %s",
1215
+ tp, node_id)
1216
+
1217
+ else:
1218
+ # Leader is connected and does not have a pending fetch request.
1219
+ # current_leader_epoch (v9+) = metadata view (broker fencing);
1220
+ # last_fetched_epoch (v12+) = record view (broker divergence
1221
+ # detection). They differ once leadership advances past the
1222
+ # record at the fetch offset.
1223
+ current_leader_epoch = self._manager.cluster.leader_epoch_for_partition(tp)
1224
+ if current_leader_epoch is None:
1225
+ current_leader_epoch = -1
1226
+ partition_info = _FetchPartition(
1227
+ partition=tp.partition,
1228
+ current_leader_epoch=current_leader_epoch,
1229
+ fetch_offset=position.offset,
1230
+ last_fetched_epoch=position.leader_epoch,
1231
+ partition_max_bytes=self.config['max_partition_fetch_bytes']
1232
+ )
1233
+ fetchable[node_id][tp] = partition_info
1234
+ log.debug("Adding fetch request for partition %s at offset %d",
1235
+ tp, position.offset)
1236
+
1237
+ requests = {}
1238
+ for node_id, next_partitions in fetchable.items():
1239
+ if self._enable_incremental_fetch_sessions:
1240
+ if node_id not in self._session_handlers:
1241
+ self._session_handlers[node_id] = FetchSessionHandler(node_id)
1242
+ session = self._session_handlers[node_id].build_next(next_partitions)
1243
+ else:
1244
+ # No incremental fetch support
1245
+ session = FetchRequestData(next_partitions, None, FetchMetadata.LEGACY)
1246
+
1247
+ min_version = FetchRequest.min_version_for_isolation_level(self._isolation_level)
1248
+ request = FetchRequest(
1249
+ max_wait_ms=self.config['fetch_max_wait_ms'],
1250
+ min_bytes=self.config['fetch_min_bytes'],
1251
+ max_bytes=self.config['fetch_max_bytes'],
1252
+ isolation_level=self._isolation_level,
1253
+ session_id=session.id,
1254
+ session_epoch=session.epoch,
1255
+ topics=session.to_send,
1256
+ forgotten_topics_data=session.to_forget,
1257
+ rack_id=self.config['client_rack'],
1258
+ min_version=min_version,
1259
+ max_version=max_version,
1260
+ )
1261
+
1262
+ fetch_offsets = {tp: next_partitions[tp].fetch_offset for tp in next_partitions}
1263
+ requests[node_id] = (request, fetch_offsets)
1264
+
1265
+ return requests
1266
+
1267
+ def _handle_fetch_response(self, node_id, fetch_offsets, send_time, response):
1268
+ """The callback for fetch completion"""
1269
+ if response.API_VERSION >= 7 and self._enable_incremental_fetch_sessions:
1270
+ if node_id not in self._session_handlers:
1271
+ log.error("Unable to find fetch session handler for node %s. Ignoring fetch response", node_id)
1272
+ return
1273
+ if not self._session_handlers[node_id].handle_response(response):
1274
+ return
1275
+
1276
+ partitions = set([
1277
+ TopicPartition(
1278
+ topic_data.topic,
1279
+ partition_data.partition_index)
1280
+ for topic_data in response.responses
1281
+ for partition_data in topic_data.partitions
1282
+ ])
1283
+ if self._sensors:
1284
+ metric_aggregator = FetchResponseMetricAggregator(self._sensors, partitions)
1285
+ else:
1286
+ metric_aggregator = None
1287
+
1288
+ for topic_data in response.responses:
1289
+ for partition_data in topic_data.partitions:
1290
+ tp = TopicPartition(
1291
+ topic_data.topic,
1292
+ partition_data.partition_index
1293
+ )
1294
+ fetch_offset = fetch_offsets[tp]
1295
+ completed_fetch = CompletedFetch(
1296
+ tp, fetch_offset,
1297
+ response.API_VERSION,
1298
+ partition_data,
1299
+ metric_aggregator
1300
+ )
1301
+ self._completed_fetches.append(completed_fetch)
1302
+
1303
+ if self._sensors:
1304
+ self._sensors.fetch_latency.record((time.monotonic() - send_time) * 1000)
1305
+
1306
+ def _handle_fetch_error(self, node_id, exception):
1307
+ level = logging.INFO if isinstance(exception, Errors.Cancelled) else logging.ERROR
1308
+ log.log(level, 'Fetch to node %s failed: %s', node_id, exception)
1309
+ if node_id in self._session_handlers:
1310
+ self._session_handlers[node_id].handle_error(exception)
1311
+
1312
+ def _clear_pending_fetch_request(self, node_id, _):
1313
+ try:
1314
+ self._nodes_with_pending_fetch_requests.remove(node_id)
1315
+ except KeyError:
1316
+ pass
1317
+
1318
+ def _maybe_update_current_leader(self, tp, partition_data):
1319
+ """Apply a KIP-951 ``current_leader`` hint from a Fetch v12+ response.
1320
+
1321
+ Updates the cluster's cached leader id/epoch when the broker advertises
1322
+ a newer leader. If the new leader id is not yet a known broker (v12 has
1323
+ no ``node_endpoints``), requests a metadata refresh so the consumer
1324
+ learns its address.
1325
+ """
1326
+ leader = partition_data.current_leader
1327
+ if leader is None or leader.leader_epoch < 0:
1328
+ return
1329
+ if self._manager.cluster.update_partition_leader(
1330
+ tp, leader.leader_id, leader.leader_epoch):
1331
+ log.debug("Fetch response advertised new leader for %s: node %s epoch %s",
1332
+ tp, leader.leader_id, leader.leader_epoch)
1333
+ if self._manager.cluster.broker_metadata(leader.leader_id) is None:
1334
+ self._manager.cluster.request_update()
1335
+
1336
+ def _parse_fetched_data(self, completed_fetch):
1337
+ tp = completed_fetch.topic_partition
1338
+ fetch_offset = completed_fetch.fetched_offset
1339
+ error_code = completed_fetch.partition_data.error_code
1340
+ highwater = completed_fetch.partition_data.high_watermark
1341
+ error_type = Errors.for_code(error_code)
1342
+ parsed_records = None
1343
+
1344
+ try:
1345
+ if not self._subscriptions.is_fetchable(tp):
1346
+ # this can happen when a rebalance happened or a partition
1347
+ # consumption paused while fetch is still in-flight
1348
+ log.debug("Ignoring fetched records for partition %s"
1349
+ " since it is no longer fetchable", tp)
1350
+
1351
+ elif error_type is Errors.NoError:
1352
+ # we are interested in this fetch only if the beginning
1353
+ # offset (of the *request*) matches the current consumed position
1354
+ # Note that the *response* may return a messageset that starts
1355
+ # earlier (e.g., compressed messages) or later (e.g., compacted topic)
1356
+ position = self._subscriptions.assignment[tp].position
1357
+ if position is None or position.offset != fetch_offset:
1358
+ log.debug("Discarding fetch response for partition %s"
1359
+ " since its offset %d does not match the"
1360
+ " expected offset %d", tp, fetch_offset,
1361
+ position.offset)
1362
+ return None
1363
+
1364
+ # KIP-320 / Fetch v12+: the broker can tell us our last_fetched_epoch
1365
+ # diverges from its log. Route into the existing OffsetForLeaderEpoch
1366
+ # validation flow rather than truncating directly here; the
1367
+ # validation path surfaces LogTruncationError uniformly.
1368
+ diverging_epoch = completed_fetch.partition_data.diverging_epoch
1369
+ if diverging_epoch is not None and diverging_epoch.end_offset >= 0:
1370
+ log.info("Fetch for %s diverged at epoch %s offset %s;"
1371
+ " marking position for validation",
1372
+ tp, diverging_epoch.epoch, diverging_epoch.end_offset)
1373
+ self._subscriptions.request_position_validation(tp)
1374
+ self._manager.cluster.request_update()
1375
+ return None
1376
+
1377
+ records = MemoryRecords(completed_fetch.partition_data.records)
1378
+ aborted_transactions = completed_fetch.partition_data.aborted_transactions
1379
+ log.debug("Preparing to read %s bytes of data for partition %s with offset %d",
1380
+ records.size_in_bytes(), tp, fetch_offset)
1381
+ parsed_records = self.PartitionRecords(fetch_offset, tp, records,
1382
+ key_deserializer=self.config['key_deserializer'],
1383
+ value_deserializer=self.config['value_deserializer'],
1384
+ check_crcs=self.config['check_crcs'],
1385
+ isolation_level=self._isolation_level,
1386
+ aborted_transactions=aborted_transactions,
1387
+ metric_aggregator=completed_fetch.metric_aggregator,
1388
+ on_drain=self._on_partition_records_drain)
1389
+ if not records.has_next() and records.size_in_bytes() > 0:
1390
+ if completed_fetch.response_version < 3:
1391
+ # Implement the pre KIP-74 behavior of throwing a RecordTooLargeException.
1392
+ record_too_large_partitions = {tp: fetch_offset}
1393
+ raise RecordTooLargeError(
1394
+ "There are some messages at [Partition=Offset]: %s "
1395
+ " whose size is larger than the fetch size %s"
1396
+ " and hence cannot be ever returned. Please condier upgrading your broker to 0.10.1.0 or"
1397
+ " newer to avoid this issue. Alternatively, increase the fetch size on the client (using"
1398
+ " max_partition_fetch_bytes)" % (
1399
+ record_too_large_partitions,
1400
+ self.config['max_partition_fetch_bytes']),
1401
+ record_too_large_partitions)
1402
+ else:
1403
+ # This should not happen with brokers that support FetchRequest/Response V3 or higher (i.e. KIP-74)
1404
+ raise Errors.KafkaError("Failed to make progress reading messages at %s=%s."
1405
+ " Received a non-empty fetch response from the server, but no"
1406
+ " complete records were found." % (tp, fetch_offset))
1407
+
1408
+ if highwater >= 0:
1409
+ self._subscriptions.assignment[tp].highwater = highwater
1410
+
1411
+ preferred_read_replica = completed_fetch.partition_data.preferred_read_replica
1412
+ if self._subscriptions.assignment[tp].update_preferred_read_replica(
1413
+ preferred_read_replica,
1414
+ time.monotonic() + self.config['metadata_max_age_ms'] / 1000.0):
1415
+ if preferred_read_replica is None or preferred_read_replica < 0:
1416
+ log.debug("Cleared preferred read replica for partition %s", tp)
1417
+ else:
1418
+ log.debug("Updating preferred read replica for partition %s to %s",
1419
+ tp, preferred_read_replica)
1420
+
1421
+ elif error_type in (Errors.NotLeaderForPartitionError,
1422
+ Errors.ReplicaNotAvailableError,
1423
+ Errors.UnknownTopicOrPartitionError,
1424
+ Errors.KafkaStorageError):
1425
+ log.debug("Error fetching partition %s: %s", tp, error_type.__name__)
1426
+ self._maybe_update_current_leader(tp, completed_fetch.partition_data)
1427
+ self._manager.cluster.request_update()
1428
+ elif error_type in (Errors.FencedLeaderEpochError,
1429
+ Errors.UnknownLeaderEpochError):
1430
+ # KIP-320: the broker has a different view of the leader epoch
1431
+ # than we do; ask for metadata refresh and queue position
1432
+ # validation so we detect any truncation before continuing.
1433
+ # The cache is cleared by maybe_validate_position once the
1434
+ # cluster cache catches up with the new epoch.
1435
+ log.debug("Fetch for %s returned %s; marking position for validation",
1436
+ tp, error_type.__name__)
1437
+ self._maybe_update_current_leader(tp, completed_fetch.partition_data)
1438
+ self._subscriptions.request_position_validation(tp)
1439
+ self._manager.cluster.request_update()
1440
+ elif error_type is Errors.OffsetOutOfRangeError:
1441
+ position = self._subscriptions.assignment[tp].position
1442
+ if position is None or position.offset != fetch_offset:
1443
+ log.debug("Discarding stale fetch response for partition %s"
1444
+ " since the fetched offset %d does not match the"
1445
+ " current offset %d", tp, fetch_offset, position.offset)
1446
+ else:
1447
+ # KIP-392: a follower may be lagging behind the leader's
1448
+ # high watermark such that our leader-side position is
1449
+ # legitimately out of *its* range. If we'd been fetching
1450
+ # from a follower, drop the cache and retry against the
1451
+ # leader BEFORE concluding the offset is really out of
1452
+ # range. Only when there was no cached follower do we
1453
+ # proceed to reset / raise. Matches Java's behavior.
1454
+ cleared = self._subscriptions.assignment[tp].clear_preferred_read_replica()
1455
+ if cleared is not None:
1456
+ log.debug("Fetch offset %s out of range for %s on follower %s;"
1457
+ " retrying from leader", fetch_offset, tp, cleared)
1458
+ elif self._subscriptions.has_default_offset_reset_policy():
1459
+ log.info("Fetch offset %s is out of range for topic-partition %s",
1460
+ fetch_offset, tp)
1461
+ self._subscriptions.request_offset_reset(tp)
1462
+ else:
1463
+ raise Errors.OffsetOutOfRangeError({tp: fetch_offset})
1464
+
1465
+ elif error_type is Errors.TopicAuthorizationFailedError:
1466
+ log.warning("Not authorized to read from topic %s.", tp.topic)
1467
+ raise Errors.TopicAuthorizationFailedError(set([tp.topic]))
1468
+ elif issubclass(error_type, Errors.RetriableError):
1469
+ log.debug("Retriable error fetching partition %s: %s", tp, error_type())
1470
+ if issubclass(error_type, Errors.InvalidMetadataError):
1471
+ self._manager.cluster.request_update()
1472
+ else:
1473
+ raise error_type('Unexpected error while fetching data')
1474
+
1475
+ finally:
1476
+ if parsed_records is None and completed_fetch.metric_aggregator:
1477
+ completed_fetch.metric_aggregator.record(tp, 0, 0)
1478
+
1479
+ if error_type is not Errors.NoError:
1480
+ # Rotate this partition to the back of the iteration
1481
+ # order so we don't keep slamming the broken partition
1482
+ # first on the next poll - healthier partitions get
1483
+ # processed while this one's backoff / metadata
1484
+ # refresh runs. Cheap LRU-style fairness across the
1485
+ # assignment.
1486
+ self._subscriptions.move_partition_to_end(tp)
1487
+
1488
+ return parsed_records
1489
+
1490
+ def _on_partition_records_drain(self, partition_records):
1491
+ # Rotate this partition to the back of the iteration order so
1492
+ # the next poll prioritizes partitions we haven't drained from
1493
+ # recently. (Topic-grouping in the outgoing FetchRequest is
1494
+ # done unconditionally by FetchRequestData.to_send via
1495
+ # defaultdict, so this is purely round-robin fairness across
1496
+ # partitions, not a serialization-efficiency thing.)
1497
+ if partition_records.bytes_read > 0:
1498
+ self._subscriptions.move_partition_to_end(partition_records.topic_partition)
1499
+
1500
+ def close(self):
1501
+ if self._next_partition_records is not None:
1502
+ self._next_partition_records.drain()
1503
+ for parked in self._paused_partition_records.values():
1504
+ parked.drain()
1505
+ self._paused_partition_records.clear()
1506
+ self._paused_completed_fetches.clear()
1507
+ self._next_in_line_exception_metadata = None
1508
+
1509
+ class PartitionRecords:
1510
+ def __init__(self, fetch_offset, tp, records,
1511
+ key_deserializer=None, value_deserializer=None,
1512
+ check_crcs=True,
1513
+ isolation_level=IsolationLevel.READ_UNCOMMITTED,
1514
+ aborted_transactions=None, # AbortedTransaction data from FetchResponse
1515
+ metric_aggregator=None, on_drain=lambda x: None):
1516
+ self.fetch_offset = fetch_offset
1517
+ self.topic_partition = tp
1518
+ self.leader_epoch = -1
1519
+ self.next_fetch_offset = fetch_offset
1520
+ self.bytes_read = 0
1521
+ self.records_read = 0
1522
+ self.isolation_level = isolation_level
1523
+ self.aborted_producer_ids = set()
1524
+ self.aborted_transactions = collections.deque(
1525
+ sorted(aborted_transactions or [], key=lambda txn: txn.first_offset)
1526
+ )
1527
+ self.metric_aggregator = metric_aggregator
1528
+ self.check_crcs = check_crcs
1529
+ self.record_iterator = itertools.dropwhile(
1530
+ self._maybe_skip_record,
1531
+ self._unpack_records(tp, records, key_deserializer, value_deserializer))
1532
+ self.on_drain = on_drain
1533
+ self._next_inline_exception = None
1534
+
1535
+ def _maybe_skip_record(self, record):
1536
+ # When fetching an offset that is in the middle of a
1537
+ # compressed batch, we will get all messages in the batch.
1538
+ # But we want to start 'take' at the fetch_offset
1539
+ # (or the next highest offset in case the message was compacted)
1540
+ if record.offset < self.fetch_offset:
1541
+ log.debug("Skipping message offset: %s (expecting %s)",
1542
+ record.offset, self.fetch_offset)
1543
+ return True
1544
+ else:
1545
+ return False
1546
+
1547
+ # For truthiness evaluation
1548
+ def __bool__(self):
1549
+ return self.record_iterator is not None
1550
+
1551
+ def drain(self):
1552
+ if self.record_iterator is not None:
1553
+ self.record_iterator = None
1554
+ self._next_inline_exception = None
1555
+ if self.metric_aggregator:
1556
+ self.metric_aggregator.record(self.topic_partition, self.bytes_read, self.records_read)
1557
+ self.on_drain(self)
1558
+
1559
+ def _maybe_raise_next_inline_exception(self):
1560
+ if self._next_inline_exception:
1561
+ exc, self._next_inline_exception = self._next_inline_exception, None
1562
+ raise exc
1563
+
1564
+ def take(self, n=None):
1565
+ self._maybe_raise_next_inline_exception()
1566
+ records = []
1567
+ try:
1568
+ # Note that records.extend(iter) will extend partially when exception raised mid-stream
1569
+ records.extend(itertools.islice(self.record_iterator, 0, n))
1570
+ except Exception as e:
1571
+ if not records:
1572
+ raise e
1573
+ # To be thrown in the next call of this method
1574
+ self._next_inline_exception = e
1575
+ return records
1576
+
1577
+ def _unpack_records(self, tp, records, key_deserializer, value_deserializer):
1578
+ try:
1579
+ batch = records.next_batch()
1580
+ last_batch = None
1581
+ while batch is not None:
1582
+ last_batch = batch
1583
+
1584
+ if self.check_crcs and not batch.validate_crc():
1585
+ raise Errors.CorruptRecordError(
1586
+ "Record batch for partition %s at offset %s failed crc check" % (
1587
+ self.topic_partition, batch.base_offset))
1588
+
1589
+
1590
+ # Try DefaultsRecordBatch / message log format v2
1591
+ # base_offset, last_offset_delta, aborted transactions, and control batches
1592
+ if batch.magic == 2:
1593
+ self.leader_epoch = batch.leader_epoch
1594
+ if self.isolation_level == IsolationLevel.READ_COMMITTED and batch.has_producer_id():
1595
+ # remove from the aborted transaction queue all aborted transactions which have begun
1596
+ # before the current batch's last offset and add the associated producerIds to the
1597
+ # aborted producer set
1598
+ self._consume_aborted_transactions_up_to(batch.last_offset)
1599
+
1600
+ producer_id = batch.producer_id
1601
+ if self._contains_abort_marker(batch):
1602
+ try:
1603
+ self.aborted_producer_ids.remove(producer_id)
1604
+ except KeyError:
1605
+ pass
1606
+ elif self._is_batch_aborted(batch):
1607
+ log.debug("Skipping aborted record batch from partition %s with producer_id %s and"
1608
+ " offsets %s to %s",
1609
+ self.topic_partition, producer_id, batch.base_offset, batch.last_offset)
1610
+ self.next_fetch_offset = batch.next_offset
1611
+ batch = records.next_batch()
1612
+ continue
1613
+
1614
+ # Control batches have a single record indicating whether a transaction
1615
+ # was aborted or committed. These are not returned to the consumer.
1616
+ if batch.is_control_batch:
1617
+ self.next_fetch_offset = batch.next_offset
1618
+ batch = records.next_batch()
1619
+ continue
1620
+
1621
+ for record in batch:
1622
+ if self.check_crcs and not record.validate_crc():
1623
+ raise Errors.CorruptRecordError(
1624
+ "Record for partition %s at offset %s failed crc check" % (
1625
+ self.topic_partition, record.offset))
1626
+ key_size = len(record.key) if record.key is not None else -1
1627
+ value_size = len(record.value) if record.value is not None else -1
1628
+ key = self._deserialize(key_deserializer, tp.topic, record.headers, record.key)
1629
+ value = self._deserialize(value_deserializer, tp.topic, record.headers, record.value)
1630
+ headers = record.headers
1631
+ header_size = sum(
1632
+ len(h_key.encode("utf-8")) + (len(h_val) if h_val is not None else 0) for h_key, h_val in
1633
+ headers) if headers else -1
1634
+ self.records_read += 1
1635
+ self.bytes_read += record.size_in_bytes
1636
+ self.next_fetch_offset = record.offset + 1
1637
+ yield ConsumerRecord(
1638
+ tp.topic, tp.partition, self.leader_epoch, record.offset, record.timestamp,
1639
+ record.timestamp_type, key, value, record.headers, record.checksum,
1640
+ key_size, value_size, header_size)
1641
+
1642
+ batch = records.next_batch()
1643
+ else:
1644
+ # Message format v2 preserves the last offset in a batch even if the last record is removed
1645
+ # through compaction. By using the next offset computed from the last offset in the batch,
1646
+ # we ensure that the offset of the next fetch will point to the next batch, which avoids
1647
+ # unnecessary re-fetching of the same batch (in the worst case, the consumer could get stuck
1648
+ # fetching the same batch repeatedly).
1649
+ if last_batch and last_batch.magic == 2:
1650
+ self.next_fetch_offset = last_batch.next_offset
1651
+ self.drain()
1652
+
1653
+ # If unpacking raises StopIteration, it is erroneously
1654
+ # caught by the generator. We want all exceptions to be raised
1655
+ # back to the user. See Issue 545
1656
+ except StopIteration:
1657
+ log.exception('StopIteration raised unpacking messageset')
1658
+ raise RuntimeError('StopIteration raised unpacking messageset')
1659
+
1660
+ def _deserialize(self, deserializer, topic, headers, data):
1661
+ if deserializer is None:
1662
+ return data
1663
+ try:
1664
+ return deserializer.deserialize(topic, headers, data)
1665
+ except TypeError:
1666
+ global _LOGGED_DESERIALIZE_WARNING
1667
+ if not _LOGGED_DESERIALIZE_WARNING:
1668
+ warnings.warn('deserializer does not implement deserialize(topic, headers, data)', category=DeprecationWarning)
1669
+ LOGGED_DESERIALIZE_WARNING = True
1670
+ return deserializer.deserialize(topic, data)
1671
+
1672
+ def _consume_aborted_transactions_up_to(self, offset):
1673
+ if not self.aborted_transactions:
1674
+ return
1675
+
1676
+ while self.aborted_transactions and self.aborted_transactions[0].first_offset <= offset:
1677
+ self.aborted_producer_ids.add(self.aborted_transactions.popleft().producer_id)
1678
+
1679
+ def _is_batch_aborted(self, batch):
1680
+ return batch.is_transactional and batch.producer_id in self.aborted_producer_ids
1681
+
1682
+ def _contains_abort_marker(self, batch):
1683
+ if not batch.is_control_batch:
1684
+ return False
1685
+ record = next(batch)
1686
+ if not record:
1687
+ return False
1688
+ return record.abort
1689
+
1690
+
1691
+ class FetchSessionHandler:
1692
+ """
1693
+ FetchSessionHandler maintains the fetch session state for connecting to a broker.
1694
+
1695
+ Using the protocol outlined by KIP-227, clients can create incremental fetch sessions.
1696
+ These sessions allow the client to fetch information about a set of partition over
1697
+ and over, without explicitly enumerating all the partitions in the request and the
1698
+ response.
1699
+
1700
+ FetchSessionHandler tracks the partitions which are in the session. It also
1701
+ determines which partitions need to be included in each fetch request, and what
1702
+ the attached fetch session metadata should be for each request.
1703
+ """
1704
+
1705
+ def __init__(self, node_id):
1706
+ self.node_id = node_id
1707
+ self.next_metadata = FetchMetadata.INITIAL
1708
+ self.session_partitions = {}
1709
+
1710
+ def build_next(self, next_partitions):
1711
+ """
1712
+ Arguments:
1713
+ next_partitions (dict): TopicPartition -> TopicPartitionState
1714
+
1715
+ Returns:
1716
+ FetchRequestData
1717
+ """
1718
+ if self.next_metadata.is_full:
1719
+ log.debug("Built full fetch %s for node %s with %s partition(s).",
1720
+ self.next_metadata, self.node_id, len(next_partitions))
1721
+ self.session_partitions = next_partitions
1722
+ return FetchRequestData(next_partitions, None, self.next_metadata)
1723
+
1724
+ prev_tps = set(self.session_partitions.keys())
1725
+ next_tps = set(next_partitions.keys())
1726
+ log.debug("Building incremental partitions from next: %s, previous: %s", next_tps, prev_tps)
1727
+ added = next_tps - prev_tps
1728
+ for tp in added:
1729
+ self.session_partitions[tp] = next_partitions[tp]
1730
+ removed = prev_tps - next_tps
1731
+ for tp in removed:
1732
+ self.session_partitions.pop(tp)
1733
+ altered = set()
1734
+ for tp in next_tps & prev_tps:
1735
+ if next_partitions[tp] != self.session_partitions[tp]:
1736
+ self.session_partitions[tp] = next_partitions[tp]
1737
+ altered.add(tp)
1738
+
1739
+ log.debug("Built incremental fetch %s for node %s. Added %s, altered %s, removed %s out of %s",
1740
+ self.next_metadata, self.node_id, added, altered, removed, self.session_partitions.keys())
1741
+ to_send = collections.OrderedDict({tp: next_partitions[tp] for tp in next_partitions if tp in (added | altered)})
1742
+ return FetchRequestData(to_send, removed, self.next_metadata)
1743
+
1744
+ def handle_response(self, response):
1745
+ if response.error_code != Errors.NoError.errno:
1746
+ error_type = Errors.for_code(response.error_code)
1747
+ log.info("Node %s was unable to process the fetch request with %s: %s.",
1748
+ self.node_id, self.next_metadata, error_type())
1749
+ if error_type is Errors.FetchSessionIdNotFoundError:
1750
+ self.next_metadata = FetchMetadata.INITIAL
1751
+ else:
1752
+ self.next_metadata = self.next_metadata.next_close_existing()
1753
+ return False
1754
+
1755
+ response_tps = self._response_partitions(response)
1756
+ session_tps = set(self.session_partitions.keys())
1757
+ if self.next_metadata.is_full:
1758
+ if response_tps != session_tps:
1759
+ log.info("Node %s sent an invalid full fetch response with extra %s / omitted %s",
1760
+ self.node_id, response_tps - session_tps, session_tps - response_tps)
1761
+ self.next_metadata = FetchMetadata.INITIAL
1762
+ return False
1763
+ elif response.session_id == FetchMetadata.INVALID_SESSION_ID:
1764
+ log.debug("Node %s sent a full fetch response with %s partitions",
1765
+ self.node_id, len(response_tps))
1766
+ self.next_metadata = FetchMetadata.INITIAL
1767
+ return True
1768
+ elif response.session_id == FetchMetadata.THROTTLED_SESSION_ID:
1769
+ log.debug("Node %s sent a empty full fetch response due to a quota violation (%s partitions)",
1770
+ self.node_id, len(response_tps))
1771
+ # Keep current metadata
1772
+ return True
1773
+ else:
1774
+ # The server created a new incremental fetch session.
1775
+ log.debug("Node %s sent a full fetch response that created a new incremental fetch session %s"
1776
+ " with %s response partitions",
1777
+ self.node_id, response.session_id,
1778
+ len(response_tps))
1779
+ self.next_metadata = FetchMetadata.new_incremental(response.session_id)
1780
+ return True
1781
+ else:
1782
+ if response_tps - session_tps:
1783
+ log.info("Node %s sent an invalid incremental fetch response with extra partitions %s",
1784
+ self.node_id, response_tps - session_tps)
1785
+ self.next_metadata = self.next_metadata.next_close_existing()
1786
+ return False
1787
+ elif response.session_id == FetchMetadata.INVALID_SESSION_ID:
1788
+ # The incremental fetch session was closed by the server.
1789
+ log.debug("Node %s sent an incremental fetch response closing session %s"
1790
+ " with %s response partitions (%s implied)",
1791
+ self.node_id, self.next_metadata.session_id,
1792
+ len(response_tps), len(self.session_partitions) - len(response_tps))
1793
+ self.next_metadata = FetchMetadata.INITIAL
1794
+ return True
1795
+ elif response.session_id == FetchMetadata.THROTTLED_SESSION_ID:
1796
+ log.debug("Node %s sent a empty incremental fetch response due to a quota violation (%s partitions)",
1797
+ self.node_id, len(response_tps))
1798
+ # Keep current metadata
1799
+ return True
1800
+ else:
1801
+ # The incremental fetch session was continued by the server.
1802
+ log.debug("Node %s sent an incremental fetch response for session %s"
1803
+ " with %s response partitions (%s implied)",
1804
+ self.node_id, response.session_id,
1805
+ len(response_tps), len(self.session_partitions) - len(response_tps))
1806
+ self.next_metadata = self.next_metadata.next_incremental()
1807
+ return True
1808
+
1809
+ def handle_error(self, _exception):
1810
+ self.next_metadata = self.next_metadata.next_close_existing()
1811
+
1812
+ def _response_partitions(self, response):
1813
+ return {TopicPartition(topic_data.topic, partition_data.partition_index)
1814
+ for topic_data in response.responses
1815
+ for partition_data in topic_data.partitions}
1816
+
1817
+
1818
+ class FetchMetadata:
1819
+ __slots__ = ('session_id', 'epoch')
1820
+
1821
+ MAX_EPOCH = 2147483647
1822
+ INVALID_SESSION_ID = 0 # used by clients with no session.
1823
+ THROTTLED_SESSION_ID = -1 # returned with empty response on quota violation
1824
+ INITIAL_EPOCH = 0 # client wants to create or recreate a session.
1825
+ FINAL_EPOCH = -1 # client wants to close any existing session, and not create a new one.
1826
+
1827
+ def __init__(self, session_id, epoch):
1828
+ self.session_id = session_id
1829
+ self.epoch = epoch
1830
+
1831
+ @property
1832
+ def is_full(self):
1833
+ return self.epoch == self.INITIAL_EPOCH or self.epoch == self.FINAL_EPOCH
1834
+
1835
+ @classmethod
1836
+ def next_epoch(cls, prev_epoch):
1837
+ if prev_epoch < 0:
1838
+ return cls.FINAL_EPOCH
1839
+ elif prev_epoch == cls.MAX_EPOCH:
1840
+ return 1
1841
+ else:
1842
+ return prev_epoch + 1
1843
+
1844
+ def next_close_existing(self):
1845
+ return self.__class__(self.session_id, self.INITIAL_EPOCH)
1846
+
1847
+ @classmethod
1848
+ def new_incremental(cls, session_id):
1849
+ return cls(session_id, cls.next_epoch(cls.INITIAL_EPOCH))
1850
+
1851
+ def next_incremental(self):
1852
+ return self.__class__(self.session_id, self.next_epoch(self.epoch))
1853
+
1854
+ FetchMetadata.INITIAL = FetchMetadata(FetchMetadata.INVALID_SESSION_ID, FetchMetadata.INITIAL_EPOCH)
1855
+ FetchMetadata.LEGACY = FetchMetadata(FetchMetadata.INVALID_SESSION_ID, FetchMetadata.FINAL_EPOCH)
1856
+
1857
+
1858
+ class FetchRequestData:
1859
+ __slots__ = ('_to_send', '_to_forget', '_metadata')
1860
+
1861
+ def __init__(self, to_send, to_forget, metadata):
1862
+ self._to_send = to_send or dict() # {TopicPartition: (partition, ...)}
1863
+ self._to_forget = to_forget or set() # {TopicPartition}
1864
+ self._metadata = metadata
1865
+
1866
+ @property
1867
+ def metadata(self):
1868
+ return self._metadata
1869
+
1870
+ @property
1871
+ def id(self):
1872
+ return self._metadata.session_id
1873
+
1874
+ @property
1875
+ def epoch(self):
1876
+ return self._metadata.epoch
1877
+
1878
+ @property
1879
+ def to_send(self):
1880
+ # Return as list of _FetchTopic data objects
1881
+ # so it can be passed directly to encoder
1882
+ partition_data = collections.defaultdict(list)
1883
+ for tp, partition_info in self._to_send.items():
1884
+ partition_data[tp.topic].append(partition_info)
1885
+ return [
1886
+ _FetchTopic(topic=topic, partitions=partitions)
1887
+ for topic, partitions in partition_data.items()
1888
+ ]
1889
+
1890
+ @property
1891
+ def to_forget(self):
1892
+ # Return as list of _ForgottenTopic data objects
1893
+ # so it can be passed directly to encoder
1894
+ partition_data = collections.defaultdict(list)
1895
+ for tp in self._to_forget:
1896
+ partition_data[tp.topic].append(tp.partition)
1897
+ return [
1898
+ _ForgottenTopic(topic=topic, partitions=partitions)
1899
+ for topic, partitions in partition_data.items()
1900
+ ]
1901
+
1902
+
1903
+ class FetchMetrics:
1904
+ __slots__ = ('total_bytes', 'total_records')
1905
+
1906
+ def __init__(self):
1907
+ self.total_bytes = 0
1908
+ self.total_records = 0
1909
+
1910
+
1911
+ class FetchResponseMetricAggregator:
1912
+ """
1913
+ Since we parse the message data for each partition from each fetch
1914
+ response lazily, fetch-level metrics need to be aggregated as the messages
1915
+ from each partition are parsed. This class is used to facilitate this
1916
+ incremental aggregation.
1917
+ """
1918
+ def __init__(self, sensors, partitions):
1919
+ self.sensors = sensors
1920
+ self.unrecorded_partitions = partitions
1921
+ self.fetch_metrics = FetchMetrics()
1922
+ self.topic_fetch_metrics = collections.defaultdict(FetchMetrics)
1923
+
1924
+ def record(self, partition, num_bytes, num_records):
1925
+ """
1926
+ After each partition is parsed, we update the current metric totals
1927
+ with the total bytes and number of records parsed. After all partitions
1928
+ have reported, we write the metric.
1929
+ """
1930
+ self.unrecorded_partitions.remove(partition)
1931
+ self.fetch_metrics.total_bytes += num_bytes
1932
+ self.fetch_metrics.total_records += num_records
1933
+ self.topic_fetch_metrics[partition.topic].total_bytes += num_bytes
1934
+ self.topic_fetch_metrics[partition.topic].total_records += num_records
1935
+
1936
+ # once all expected partitions from the fetch have reported in, record the metrics
1937
+ if not self.unrecorded_partitions:
1938
+ self.sensors.bytes_fetched.record(self.fetch_metrics.total_bytes)
1939
+ self.sensors.records_fetched.record(self.fetch_metrics.total_records)
1940
+ for topic, metrics in self.topic_fetch_metrics.items():
1941
+ self.sensors.record_topic_fetch_metrics(topic, metrics.total_bytes, metrics.total_records)
1942
+
1943
+
1944
+ class FetchManagerMetrics:
1945
+ def __init__(self, metrics, prefix):
1946
+ self.metrics = metrics
1947
+ self.group_name = '%s-fetch-manager-metrics' % (prefix,)
1948
+
1949
+ self.bytes_fetched = metrics.sensor('bytes-fetched')
1950
+ self.bytes_fetched.add(metrics.metric_name('fetch-size-avg', self.group_name,
1951
+ 'The average number of bytes fetched per request'), Avg())
1952
+ self.bytes_fetched.add(metrics.metric_name('fetch-size-max', self.group_name,
1953
+ 'The maximum number of bytes fetched per request'), Max())
1954
+ self.bytes_fetched.add(metrics.metric_name('bytes-consumed-rate', self.group_name,
1955
+ 'The average number of bytes consumed per second'), Rate())
1956
+
1957
+ self.records_fetched = self.metrics.sensor('records-fetched')
1958
+ self.records_fetched.add(metrics.metric_name('records-per-request-avg', self.group_name,
1959
+ 'The average number of records in each request'), Avg())
1960
+ self.records_fetched.add(metrics.metric_name('records-consumed-rate', self.group_name,
1961
+ 'The average number of records consumed per second'), Rate())
1962
+
1963
+ self.fetch_latency = metrics.sensor('fetch-latency')
1964
+ self.fetch_latency.add(metrics.metric_name('fetch-latency-avg', self.group_name,
1965
+ 'The average time taken for a fetch request.'), Avg())
1966
+ self.fetch_latency.add(metrics.metric_name('fetch-latency-max', self.group_name,
1967
+ 'The max time taken for any fetch request.'), Max())
1968
+ self.fetch_latency.add(metrics.metric_name('fetch-rate', self.group_name,
1969
+ 'The number of fetch requests per second.'), Rate(sampled_stat=Count()))
1970
+
1971
+ self.records_fetch_lag = metrics.sensor('records-lag')
1972
+ self.records_fetch_lag.add(metrics.metric_name('records-lag-max', self.group_name,
1973
+ 'The maximum lag in terms of number of records for any partition in self window'), Max())
1974
+
1975
+ def record_topic_fetch_metrics(self, topic, num_bytes, num_records):
1976
+ # record bytes fetched
1977
+ name = '.'.join(['topic', topic, 'bytes-fetched'])
1978
+ bytes_fetched = self.metrics.get_sensor(name)
1979
+ if not bytes_fetched:
1980
+ metric_tags = {'topic': topic.replace('.', '_')}
1981
+
1982
+ bytes_fetched = self.metrics.sensor(name)
1983
+ bytes_fetched.add(self.metrics.metric_name('fetch-size-avg',
1984
+ self.group_name,
1985
+ 'The average number of bytes fetched per request for topic %s' % (topic,),
1986
+ metric_tags), Avg())
1987
+ bytes_fetched.add(self.metrics.metric_name('fetch-size-max',
1988
+ self.group_name,
1989
+ 'The maximum number of bytes fetched per request for topic %s' % (topic,),
1990
+ metric_tags), Max())
1991
+ bytes_fetched.add(self.metrics.metric_name('bytes-consumed-rate',
1992
+ self.group_name,
1993
+ 'The average number of bytes consumed per second for topic %s' % (topic,),
1994
+ metric_tags), Rate())
1995
+ bytes_fetched.record(num_bytes)
1996
+
1997
+ # record records fetched
1998
+ name = '.'.join(['topic', topic, 'records-fetched'])
1999
+ records_fetched = self.metrics.get_sensor(name)
2000
+ if not records_fetched:
2001
+ metric_tags = {'topic': topic.replace('.', '_')}
2002
+
2003
+ records_fetched = self.metrics.sensor(name)
2004
+ records_fetched.add(self.metrics.metric_name('records-per-request-avg',
2005
+ self.group_name,
2006
+ 'The average number of records in each request for topic %s' % (topic,),
2007
+ metric_tags), Avg())
2008
+ records_fetched.add(self.metrics.metric_name('records-consumed-rate',
2009
+ self.group_name,
2010
+ 'The average number of records consumed per second for topic %s' % (topic,),
2011
+ metric_tags), Rate())
2012
+ records_fetched.record(num_records)