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
File without changes
File without changes
@@ -0,0 +1,90 @@
1
+ from abc import ABC, abstractmethod, abstractproperty
2
+ from enum import IntEnum
3
+
4
+ from kafka.protocol.consumer.metadata import (
5
+ ConsumerProtocolSubscription, ConsumerProtocolAssignment,
6
+ )
7
+
8
+
9
+ class RebalanceProtocol(IntEnum):
10
+ """KIP-429: rebalance protocol mode for a partition assignor.
11
+
12
+ EAGER - pre-KIP-429 behaviour: every member revokes its full
13
+ assignment before JoinGroup, then receives a fresh assignment in
14
+ SyncGroup. Simple but causes a "stop the world" pause on every
15
+ rebalance.
16
+
17
+ COOPERATIVE - KIP-429 incremental rebalance: members keep their
18
+ existing assignment across JoinGroup; the leader's assignment
19
+ indicates the partitions that need to move; only revoked
20
+ partitions are released, and only newly-assigned partitions
21
+ invoke the listener. A second rebalance round picks up partitions
22
+ that were revoked in round 1.
23
+ """
24
+ EAGER = 0
25
+ COOPERATIVE = 1
26
+
27
+
28
+ class AbstractPartitionAssignor(ABC):
29
+ """
30
+ Abstract assignor implementation which does some common grunt work (in particular collecting
31
+ partition counts which are always needed in assignors).
32
+ """
33
+
34
+ @abstractproperty
35
+ def name(self):
36
+ """.name should be a string identifying the assignor"""
37
+ pass
38
+
39
+ def supported_protocols(self):
40
+ """Return the list of :class:`RebalanceProtocol` modes this
41
+ assignor supports, in order of preference.
42
+
43
+ Default is ``[EAGER]`` - every legacy assignor (Range,
44
+ RoundRobin, the original Sticky from KIP-54) behaves this
45
+ way. Override in subclasses that participate in KIP-429
46
+ incremental cooperative rebalancing (e.g.
47
+ ``CooperativeStickyAssignor``).
48
+ """
49
+ return [RebalanceProtocol.EAGER]
50
+
51
+ @abstractmethod
52
+ def assign(self, cluster, members):
53
+ """Perform group assignment given cluster metadata and member subscriptions
54
+
55
+ Arguments:
56
+ cluster (ClusterMetadata): metadata for use in assignment
57
+ members ([JoinGroupResponseMember]): member_id and metadata
58
+ for each member in the group, including group_instance_id
59
+ when available (v5+). metadata is a decoded instance of
60
+ ConsumerProtocolSubscription.
61
+
62
+ Returns:
63
+ dict: {member_id: ConsumerProtocolAssignment}
64
+ """
65
+ pass
66
+
67
+ @abstractmethod
68
+ def metadata(self, topics):
69
+ """Generate ProtocolMetadata to be submitted via JoinGroupRequest.
70
+
71
+ Arguments:
72
+ topics (set): a member's subscribed topics
73
+
74
+ Returns:
75
+ ConsumerProtocolSubscription
76
+ """
77
+ pass
78
+
79
+ @abstractmethod
80
+ def on_assignment(self, assignment, generation):
81
+ """Callback that runs on each assignment.
82
+
83
+ This method can be used to update internal state, if any, of the
84
+ partition assignor.
85
+
86
+ Arguments:
87
+ assignment (ConsumerProtocolAssignment): the member's assignment
88
+ generation (int): generation id of assignment
89
+ """
90
+ pass
@@ -0,0 +1,167 @@
1
+ """KIP-429 cooperative sticky partition assignor.
2
+
3
+ Wraps :class:`StickyPartitionAssignor` (KIP-54) with the two-phase
4
+ "incremental cooperative" rebalancing protocol:
5
+
6
+ * Members keep their assignment across JoinGroup - no global revoke.
7
+ * The leader runs the sticky algorithm to compute the *ideal* final
8
+ assignment, then identifies any partition that is moving from one
9
+ owner to another and *removes it from the new owner's first-round
10
+ assignment*. The current owner sees its assignment shrink, revokes
11
+ the lost partition, and the broker is signaled (via
12
+ ``request_rejoin``) that another rebalance is needed.
13
+ * Round two: the freshly-revoked partition is owned by nobody; the
14
+ sticky algorithm now gives it to its intended new owner.
15
+
16
+ This avoids the "stop the world" pause that EAGER mode imposes - each
17
+ member only pauses while it's processing the specific partitions
18
+ moving in or out of its own assignment.
19
+
20
+ References:
21
+ * KIP-429: https://cwiki.apache.org/confluence/x/vAclBg
22
+ * Java: org.apache.kafka.clients.consumer.CooperativeStickyAssignor
23
+ """
24
+
25
+ from collections import defaultdict
26
+
27
+ from kafka.coordinator.assignors.abstract import RebalanceProtocol
28
+ from kafka.coordinator.assignors.sticky.sticky_assignor import (
29
+ StickyAssignmentExecutor,
30
+ StickyAssignorMemberMetadataV1,
31
+ StickyPartitionAssignor,
32
+ )
33
+ from kafka.protocol.consumer.metadata import (
34
+ ConsumerProtocolAssignment, ConsumerProtocolSubscription,
35
+ )
36
+ from kafka.structs import TopicPartition
37
+
38
+
39
+ # Wire version 1 of ConsumerProtocolSubscription is what KIP-429 added.
40
+ # Members advertise their currently-owned partitions in the
41
+ # ``owned_partitions`` field so the leader can compute the diff.
42
+ _COOPERATIVE_SUBSCRIPTION_VERSION = 1
43
+
44
+
45
+ class CooperativeStickyAssignor(StickyPartitionAssignor):
46
+ """KIP-429 cooperative variant of the sticky assignor.
47
+
48
+ Behaviorally identical to :class:`StickyPartitionAssignor` for
49
+ final partition placement (it inherits the same algorithm) - the
50
+ only difference is that movements are staged across two rebalance
51
+ rounds so no member ever sees a partition assigned to it while
52
+ another member still owns it.
53
+ """
54
+
55
+ name = "cooperative-sticky"
56
+ # Bump the wire metadata to v1 so OwnedPartitions is encoded on
57
+ # JoinGroup. The leader reads .owned_partitions to compute the
58
+ # set of partitions that are moving.
59
+ version = _COOPERATIVE_SUBSCRIPTION_VERSION
60
+
61
+ def supported_protocols(self):
62
+ # COOPERATIVE only - mixing this assignor with eager assignors
63
+ # in the same consumer is rejected at consumer init time
64
+ # (see KafkaConsumer.__init__ validation).
65
+ return [RebalanceProtocol.COOPERATIVE]
66
+
67
+ def metadata(self, topics):
68
+ # Encode OwnedPartitions (v1+) so the leader can compute the
69
+ # cooperative diff. The base class uses StickyAssignorUserData
70
+ # for the same purpose in v0 - under cooperative we surface
71
+ # the owned set via the dedicated schema field instead.
72
+ SubTP = ConsumerProtocolSubscription.TopicPartition
73
+ owned_partitions = []
74
+ if self.member_assignment is not None:
75
+ by_topic = defaultdict(list)
76
+ for tp in self.member_assignment:
77
+ by_topic[tp.topic].append(tp.partition)
78
+ owned_partitions = [
79
+ SubTP(topic=t, partitions=sorted(parts))
80
+ for t, parts in by_topic.items()
81
+ ]
82
+ return ConsumerProtocolSubscription(
83
+ version=self.version,
84
+ topics=sorted(topics),
85
+ user_data=b'',
86
+ owned_partitions=owned_partitions,
87
+ )
88
+
89
+ @classmethod
90
+ def parse_member_metadata(cls, metadata):
91
+ """Decode a member's ``ConsumerProtocolSubscription``.
92
+
93
+ Cooperative members carry owned partitions in the
94
+ ``owned_partitions`` schema field (v1+) rather than the
95
+ ``user_data`` blob the legacy sticky assignor uses. Returns
96
+ the same ``StickyAssignorMemberMetadataV1`` shape so the
97
+ underlying sticky algorithm can consume it unchanged.
98
+ """
99
+ member_partitions = []
100
+ # owned_partitions is a list of TopicPartition data containers
101
+ # (v1+); on v0 metadata the field is absent - treat as empty.
102
+ for tp in getattr(metadata, 'owned_partitions', None) or ():
103
+ for partition in tp.partitions:
104
+ member_partitions.append(TopicPartition(tp.topic, partition))
105
+
106
+ generation = metadata.generation_id
107
+ return StickyAssignorMemberMetadataV1(
108
+ partitions=member_partitions,
109
+ generation=metadata.generation_id, # requires schema v2, defaults to -1
110
+ subscription=list(metadata.topics),
111
+ )
112
+
113
+ def assign(self, cluster, members):
114
+ """Cooperative two-phase assignment.
115
+
116
+ 1. Compute the ideal final sticky assignment.
117
+ 2. Build a map of currently-owned partitions across all
118
+ members from their ``OwnedPartitions``.
119
+ 3. For any partition whose final owner differs from its
120
+ current owner, remove it from the new owner's first-round
121
+ assignment. The current owner sees its assignment shrink,
122
+ revokes the partition, and re-joins; on round two the
123
+ partition is unowned and the algorithm assigns it.
124
+ """
125
+ members_metadata = {
126
+ member.member_id: self.parse_member_metadata(member.metadata)
127
+ for member in members
128
+ }
129
+ executor = StickyAssignmentExecutor(cluster, members_metadata)
130
+ executor.perform_initial_assignment()
131
+ executor.balance()
132
+ # Expose for diagnostic tests (matches parent behaviour).
133
+ self._latest_partition_movements = executor.partition_movements
134
+
135
+ # Map: partition -> current_owner_member_id (None if unowned).
136
+ currently_owned = {}
137
+ for member_id, parsed in members_metadata.items():
138
+ for tp in parsed.partitions:
139
+ currently_owned[tp] = member_id
140
+
141
+ # Build the round-1 assignment: drop any partition that's
142
+ # moving (final owner != current owner). The current owner
143
+ # will revoke it in _on_join_complete; the broker will see
144
+ # the consumer re-join and round 2 will land the partition
145
+ # on the intended new owner.
146
+ #
147
+ # ``executor.get_final_assignment`` returns the canonical wire
148
+ # shape: ``[(topic, [partition, ...]), ...]``. We rebuild the
149
+ # same shape after filtering so the encoder is happy.
150
+ cooperative = {}
151
+ for member in members:
152
+ member_id = member.member_id
153
+ kept_by_topic = defaultdict(list)
154
+ for topic, parts in executor.get_final_assignment(member_id):
155
+ for p in parts:
156
+ tp = TopicPartition(topic, p)
157
+ current_owner = currently_owned.get(tp)
158
+ if current_owner is None or current_owner == member_id:
159
+ # Either nobody owned it, or this member
160
+ # already owns it. Safe to assign now.
161
+ kept_by_topic[topic].append(p)
162
+ # else: partition is moving; defer to round 2.
163
+ assigned_partitions = sorted(
164
+ (t, sorted(ps)) for t, ps in kept_by_topic.items())
165
+ cooperative[member_id] = ConsumerProtocolAssignment(
166
+ self.version, assigned_partitions, b'')
167
+ return cooperative
@@ -0,0 +1,81 @@
1
+ import collections
2
+ import itertools
3
+ import logging
4
+
5
+ from kafka.coordinator.assignors.abstract import (
6
+ AbstractPartitionAssignor,
7
+ ConsumerProtocolSubscription,
8
+ ConsumerProtocolAssignment,
9
+ )
10
+
11
+ log = logging.getLogger(__name__)
12
+
13
+
14
+ class RangePartitionAssignor(AbstractPartitionAssignor):
15
+ """
16
+ The range assignor works on a per-topic basis. For each topic, we lay out
17
+ the available partitions in numeric order and the consumers in
18
+ lexicographic order. We then divide the number of partitions by the total
19
+ number of consumers to determine the number of partitions to assign to each
20
+ consumer. If it does not evenly divide, then the first few consumers will
21
+ have one extra partition.
22
+
23
+ For example, suppose there are two consumers C0 and C1, two topics t0 and
24
+ t1, and each topic has 3 partitions, resulting in partitions t0p0, t0p1,
25
+ t0p2, t1p0, t1p1, and t1p2.
26
+
27
+ The assignment will be:
28
+ C0: [t0p0, t0p1, t1p0, t1p1]
29
+ C1: [t0p2, t1p2]
30
+ """
31
+ name = 'range'
32
+ version = 0
33
+
34
+ @classmethod
35
+ def assign(cls, cluster, members):
36
+ consumers_per_topic = collections.defaultdict(list)
37
+ for member in members:
38
+ for topic in member.metadata.topics:
39
+ consumers_per_topic[topic].append((member.group_instance_id, member.member_id))
40
+
41
+ # construct {member_id: {topic: [partition, ...]}}
42
+ assignment = collections.defaultdict(dict)
43
+
44
+ for topic in consumers_per_topic:
45
+ # group by static members (True) v dynamic members (False)
46
+ grouped = {k: list(g) for k, g in itertools.groupby(consumers_per_topic[topic], key=lambda ids: ids[0] is not None)}
47
+ consumers_per_topic[topic] = sorted(grouped.get(True, [])) + sorted(grouped.get(False, [])) # sorted static members first, then sorted dynamic
48
+
49
+ for topic, consumers_for_topic in consumers_per_topic.items():
50
+ partitions = cluster.partitions_for_topic(topic)
51
+ if partitions is None:
52
+ log.warning('No partition metadata for topic %s', topic)
53
+ continue
54
+ partitions = sorted(partitions)
55
+
56
+ partitions_per_consumer = len(partitions) // len(consumers_for_topic)
57
+ consumers_with_extra = len(partitions) % len(consumers_for_topic)
58
+
59
+ for i, (_group_instance_id, member_id) in enumerate(consumers_for_topic):
60
+ start = partitions_per_consumer * i
61
+ start += min(i, consumers_with_extra)
62
+ length = partitions_per_consumer
63
+ if not i + 1 > consumers_with_extra:
64
+ length += 1
65
+ assignment[member_id][topic] = partitions[start:start+length]
66
+
67
+ protocol_assignment = {}
68
+ for member in members:
69
+ protocol_assignment[member.member_id] = ConsumerProtocolAssignment(
70
+ cls.version,
71
+ sorted(assignment[member.member_id].items()),
72
+ b'')
73
+ return protocol_assignment
74
+
75
+ @classmethod
76
+ def metadata(cls, topics):
77
+ return ConsumerProtocolSubscription(cls.version, list(topics), b'')
78
+
79
+ @classmethod
80
+ def on_assignment(cls, assignment, generation):
81
+ pass
@@ -0,0 +1,101 @@
1
+ import collections
2
+ import itertools
3
+ import logging
4
+
5
+ from kafka.coordinator.assignors.abstract import (
6
+ AbstractPartitionAssignor,
7
+ ConsumerProtocolSubscription,
8
+ ConsumerProtocolAssignment,
9
+ )
10
+ from kafka.structs import TopicPartition
11
+
12
+ log = logging.getLogger(__name__)
13
+
14
+
15
+ class RoundRobinPartitionAssignor(AbstractPartitionAssignor):
16
+ """
17
+ The roundrobin assignor lays out all the available partitions and all the
18
+ available consumers. It then proceeds to do a roundrobin assignment from
19
+ partition to consumer. If the subscriptions of all consumer instances are
20
+ identical, then the partitions will be uniformly distributed. (i.e., the
21
+ partition ownership counts will be within a delta of exactly one across all
22
+ consumers.)
23
+
24
+ For example, suppose there are two consumers C0 and C1, two topics t0 and
25
+ t1, and each topic has 3 partitions, resulting in partitions t0p0, t0p1,
26
+ t0p2, t1p0, t1p1, and t1p2.
27
+
28
+ The assignment will be:
29
+ C0: [t0p0, t0p2, t1p1]
30
+ C1: [t0p1, t1p0, t1p2]
31
+
32
+ When subscriptions differ across consumer instances, the assignment process
33
+ still considers each consumer instance in round robin fashion but skips
34
+ over an instance if it is not subscribed to the topic. Unlike the case when
35
+ subscriptions are identical, this can result in imbalanced assignments.
36
+
37
+ For example, suppose we have three consumers C0, C1, C2, and three topics
38
+ t0, t1, t2, with unbalanced partitions t0p0, t1p0, t1p1, t2p0, t2p1, t2p2,
39
+ where C0 is subscribed to t0; C1 is subscribed to t0, t1; and C2 is
40
+ subscribed to t0, t1, t2.
41
+
42
+ The assignment will be:
43
+ C0: [t0p0]
44
+ C1: [t1p0]
45
+ C2: [t1p1, t2p0, t2p1, t2p2]
46
+ """
47
+ name = 'roundrobin'
48
+ version = 0
49
+
50
+ @classmethod
51
+ def assign(cls, cluster, members):
52
+ all_topics = set()
53
+ for member in members:
54
+ all_topics.update(member.metadata.topics)
55
+
56
+ all_topic_partitions = []
57
+ for topic in all_topics:
58
+ partitions = cluster.partitions_for_topic(topic)
59
+ if partitions is None:
60
+ log.warning('No partition metadata for topic %s', topic)
61
+ continue
62
+ for partition in partitions:
63
+ all_topic_partitions.append(TopicPartition(topic, partition))
64
+ all_topic_partitions.sort()
65
+
66
+ # construct {member_id: {topic: [partition, ...]}}
67
+ assignment = collections.defaultdict(lambda: collections.defaultdict(list))
68
+
69
+ # Sort static and dynamic members separately to maintain stable static assignments
70
+ ungrouped = [(member.group_instance_id, member.member_id) for member in members]
71
+ grouped = {k: list(g) for k, g in itertools.groupby(ungrouped, key=lambda ids: ids[0] is not None)}
72
+ member_list = sorted(grouped.get(True, [])) + sorted(grouped.get(False, [])) # sorted static members first, then sorted dynamic
73
+ member_iter = itertools.cycle(member_list)
74
+ member_topics = {member.member_id: member.metadata.topics for member in members}
75
+
76
+ for partition in all_topic_partitions:
77
+ _group_instance_id, member_id = next(member_iter)
78
+
79
+ # Because we constructed all_topic_partitions from the set of
80
+ # member subscribed topics, we should be safe assuming that
81
+ # each topic in all_topic_partitions is in at least one member
82
+ # subscription; otherwise this could yield an infinite loop
83
+ while partition.topic not in member_topics[member_id]:
84
+ member_id = next(member_iter)
85
+ assignment[member_id][partition.topic].append(partition.partition)
86
+
87
+ protocol_assignment = {}
88
+ for member in members:
89
+ protocol_assignment[member.member_id] = ConsumerProtocolAssignment(
90
+ cls.version,
91
+ sorted(assignment[member.member_id].items()),
92
+ b'')
93
+ return protocol_assignment
94
+
95
+ @classmethod
96
+ def metadata(cls, topics):
97
+ return ConsumerProtocolSubscription(cls.version, list(topics), b'')
98
+
99
+ @classmethod
100
+ def on_assignment(cls, assignment, generation):
101
+ pass
@@ -0,0 +1,37 @@
1
+ // Licensed to the Apache Software Foundation (ASF) under one or more
2
+ // contributor license agreements. See the NOTICE file distributed with
3
+ // this work for additional information regarding copyright ownership.
4
+ // The ASF licenses this file to You under the Apache License, Version 2.0
5
+ // (the "License"); you may not use this file except in compliance with
6
+ // the License. You may obtain a copy of the License at
7
+ //
8
+ // http://www.apache.org/licenses/LICENSE-2.0
9
+ //
10
+ // Unless required by applicable law or agreed to in writing, software
11
+ // distributed under the License is distributed on an "AS IS" BASIS,
12
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ // See the License for the specific language governing permissions and
14
+ // limitations under the License.
15
+
16
+ {
17
+ "type": "data",
18
+ "name": "StickyAssignorUserData",
19
+ // StickyAssignor currently always encodes with version 1.
20
+ // To decode, versions are attempted in reverse order until one succeeds.
21
+ // If no decoding is possible, the assignor ignores the previous user data.
22
+
23
+ // Version 1 added the "generation" field
24
+ "validVersions": "0-1",
25
+ "flexibleVersions": "none",
26
+ "fields": [
27
+ { "name": "PreviousAssignment", "type": "[]TopicPartition", "versions": "0+", "fields": [
28
+ { "name": "Topic", "type": "string", "mapKey": true, "versions": "0+", "entityType": "topicName",
29
+ "about": "The topic name."},
30
+ { "name": "Partitions", "type": "[]int32", "versions": "0+",
31
+ "about": "The partition ids."}
32
+ ]
33
+ },
34
+ { "name": "Generation", "type": "int32", "versions": "1+", "default": "-1", "ignorable": true,
35
+ "about": "The generation id of the previous assignment."}
36
+ ]
37
+ }
File without changes
@@ -0,0 +1,149 @@
1
+ import logging
2
+ from collections import defaultdict, namedtuple
3
+ from copy import deepcopy
4
+
5
+ log = logging.getLogger(__name__)
6
+
7
+
8
+ ConsumerPair = namedtuple("ConsumerPair", ["src_member_id", "dst_member_id"])
9
+ """
10
+ Represents a pair of Kafka consumer ids involved in a partition reassignment.
11
+ Each ConsumerPair corresponds to a particular partition or topic, indicates that the particular partition or some
12
+ partition of the particular topic was moved from the source consumer to the destination consumer
13
+ during the rebalance. This class helps in determining whether a partition reassignment results in cycles among
14
+ the generated graph of consumer pairs.
15
+ """
16
+
17
+
18
+ def is_sublist(source, target):
19
+ """Checks if one list is a sublist of another.
20
+
21
+ Arguments:
22
+ source: the list in which to search for the occurrence of target.
23
+ target: the list to search for as a sublist of source
24
+
25
+ Returns:
26
+ true if target is in source; false otherwise
27
+ """
28
+ for index in (i for i, e in enumerate(source) if e == target[0]):
29
+ if tuple(source[index: index + len(target)]) == target:
30
+ return True
31
+ return False
32
+
33
+
34
+ class PartitionMovements:
35
+ """
36
+ This class maintains some data structures to simplify lookup of partition movements among consumers.
37
+ At each point of time during a partition rebalance it keeps track of partition movements
38
+ corresponding to each topic, and also possible movement (in form a ConsumerPair object) for each partition.
39
+ """
40
+
41
+ def __init__(self):
42
+ self.partition_movements_by_topic = defaultdict(
43
+ lambda: defaultdict(set)
44
+ )
45
+ self.partition_movements = {}
46
+
47
+ def move_partition(self, partition, old_consumer, new_consumer):
48
+ pair = ConsumerPair(src_member_id=old_consumer, dst_member_id=new_consumer)
49
+ if partition in self.partition_movements:
50
+ # this partition has previously moved
51
+ existing_pair = self._remove_movement_record_of_partition(partition)
52
+ if existing_pair.dst_member_id != old_consumer:
53
+ raise ValueError()
54
+ if existing_pair.src_member_id != new_consumer:
55
+ # the partition is not moving back to its previous consumer
56
+ self._add_partition_movement_record(
57
+ partition, ConsumerPair(src_member_id=existing_pair.src_member_id, dst_member_id=new_consumer)
58
+ )
59
+ else:
60
+ self._add_partition_movement_record(partition, pair)
61
+
62
+ def get_partition_to_be_moved(self, partition, old_consumer, new_consumer):
63
+ if partition.topic not in self.partition_movements_by_topic:
64
+ return partition
65
+ if partition in self.partition_movements:
66
+ # this partition has previously moved
67
+ if old_consumer != self.partition_movements[partition].dst_member_id:
68
+ raise ValueError()
69
+ old_consumer = self.partition_movements[partition].src_member_id
70
+ reverse_pair = ConsumerPair(src_member_id=new_consumer, dst_member_id=old_consumer)
71
+ if reverse_pair not in self.partition_movements_by_topic[partition.topic]:
72
+ return partition
73
+
74
+ return next(iter(self.partition_movements_by_topic[partition.topic][reverse_pair]))
75
+
76
+ def are_sticky(self):
77
+ for topic, movements in self.partition_movements_by_topic.items():
78
+ movement_pairs = set(movements.keys())
79
+ if self._has_cycles(movement_pairs):
80
+ log.error(
81
+ "Stickiness is violated for topic {}\n"
82
+ "Partition movements for this topic occurred among the following consumer pairs:\n"
83
+ "{}".format(topic, movement_pairs)
84
+ )
85
+ return False
86
+ return True
87
+
88
+ def _remove_movement_record_of_partition(self, partition):
89
+ pair = self.partition_movements[partition]
90
+ del self.partition_movements[partition]
91
+
92
+ self.partition_movements_by_topic[partition.topic][pair].remove(partition)
93
+ if not self.partition_movements_by_topic[partition.topic][pair]:
94
+ del self.partition_movements_by_topic[partition.topic][pair]
95
+ if not self.partition_movements_by_topic[partition.topic]:
96
+ del self.partition_movements_by_topic[partition.topic]
97
+
98
+ return pair
99
+
100
+ def _add_partition_movement_record(self, partition, pair):
101
+ self.partition_movements[partition] = pair
102
+ self.partition_movements_by_topic[partition.topic][pair].add(partition)
103
+
104
+ def _has_cycles(self, consumer_pairs):
105
+ cycles = set()
106
+ for pair in consumer_pairs:
107
+ reduced_pairs = deepcopy(consumer_pairs)
108
+ reduced_pairs.remove(pair)
109
+ path = [pair.src_member_id]
110
+ if self._is_linked(pair.dst_member_id, pair.src_member_id, reduced_pairs, path) and not self._is_subcycle(
111
+ path, cycles
112
+ ):
113
+ cycles.add(tuple(path))
114
+ log.error("A cycle of length {} was found: {}".format(len(path) - 1, path))
115
+
116
+ # for now we want to make sure there is no partition movements of the same topic between a pair of consumers.
117
+ # the odds of finding a cycle among more than two consumers seem to be very low (according to various randomized
118
+ # tests with the given sticky algorithm) that it should not worth the added complexity of handling those cases.
119
+ for cycle in cycles:
120
+ if len(cycle) == 3: # indicates a cycle of length 2
121
+ return True
122
+ return False
123
+
124
+ @staticmethod
125
+ def _is_subcycle(cycle, cycles):
126
+ super_cycle = deepcopy(cycle)
127
+ super_cycle = super_cycle[:-1]
128
+ super_cycle.extend(cycle)
129
+ for found_cycle in cycles:
130
+ if len(found_cycle) == len(cycle) and is_sublist(super_cycle, found_cycle):
131
+ return True
132
+ return False
133
+
134
+ def _is_linked(self, src, dst, pairs, current_path):
135
+ if src == dst:
136
+ return False
137
+ if not pairs:
138
+ return False
139
+ if ConsumerPair(src, dst) in pairs:
140
+ current_path.append(src)
141
+ current_path.append(dst)
142
+ return True
143
+ for pair in pairs:
144
+ if pair.src_member_id == src:
145
+ reduced_set = deepcopy(pairs)
146
+ reduced_set.remove(pair)
147
+ current_path.append(pair.src_member_id)
148
+ return self._is_linked(pair.dst_member_id, dst, reduced_set, current_path)
149
+ return False