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.
- kafka/__init__.py +34 -0
- kafka/__main__.py +5 -0
- kafka/admin/__init__.py +29 -0
- kafka/admin/__main__.py +5 -0
- kafka/admin/_acls.py +355 -0
- kafka/admin/_cluster.py +359 -0
- kafka/admin/_configs.py +479 -0
- kafka/admin/_groups.py +754 -0
- kafka/admin/_partitions.py +595 -0
- kafka/admin/_topics.py +281 -0
- kafka/admin/_transactions.py +450 -0
- kafka/admin/_users.py +194 -0
- kafka/admin/client.py +373 -0
- kafka/benchmarks/__init__.py +0 -0
- kafka/benchmarks/consumer_performance.py +138 -0
- kafka/benchmarks/load_example.py +109 -0
- kafka/benchmarks/producer_encode_path.py +201 -0
- kafka/benchmarks/producer_performance.py +161 -0
- kafka/benchmarks/profile_protocol.py +138 -0
- kafka/benchmarks/protocol_old_vs_new.py +447 -0
- kafka/benchmarks/record_batch_compose.py +77 -0
- kafka/benchmarks/record_batch_read.py +82 -0
- kafka/benchmarks/varint_speed.py +426 -0
- kafka/cli/__init__.py +36 -0
- kafka/cli/admin/__init__.py +117 -0
- kafka/cli/admin/acls/__init__.py +9 -0
- kafka/cli/admin/acls/common.py +76 -0
- kafka/cli/admin/acls/create.py +19 -0
- kafka/cli/admin/acls/delete.py +23 -0
- kafka/cli/admin/acls/describe.py +16 -0
- kafka/cli/admin/cluster/__init__.py +14 -0
- kafka/cli/admin/cluster/describe.py +11 -0
- kafka/cli/admin/cluster/describe_quorum.py +11 -0
- kafka/cli/admin/cluster/features.py +52 -0
- kafka/cli/admin/cluster/log_dirs.py +43 -0
- kafka/cli/admin/cluster/versions.py +33 -0
- kafka/cli/admin/configs/__init__.py +10 -0
- kafka/cli/admin/configs/alter.py +43 -0
- kafka/cli/admin/configs/common.py +17 -0
- kafka/cli/admin/configs/describe.py +30 -0
- kafka/cli/admin/configs/list.py +16 -0
- kafka/cli/admin/configs/reset.py +20 -0
- kafka/cli/admin/groups/__init__.py +16 -0
- kafka/cli/admin/groups/alter_offsets.py +30 -0
- kafka/cli/admin/groups/delete.py +11 -0
- kafka/cli/admin/groups/delete_offsets.py +29 -0
- kafka/cli/admin/groups/describe.py +11 -0
- kafka/cli/admin/groups/list.py +28 -0
- kafka/cli/admin/groups/list_offsets.py +29 -0
- kafka/cli/admin/groups/remove_members.py +40 -0
- kafka/cli/admin/groups/reset_offsets.py +139 -0
- kafka/cli/admin/partitions/__init__.py +21 -0
- kafka/cli/admin/partitions/alter_reassignments.py +37 -0
- kafka/cli/admin/partitions/create.py +27 -0
- kafka/cli/admin/partitions/delete_records.py +31 -0
- kafka/cli/admin/partitions/describe.py +36 -0
- kafka/cli/admin/partitions/elect_leaders.py +53 -0
- kafka/cli/admin/partitions/list_offsets.py +88 -0
- kafka/cli/admin/partitions/list_reassignments.py +35 -0
- kafka/cli/admin/topics/__init__.py +10 -0
- kafka/cli/admin/topics/create.py +13 -0
- kafka/cli/admin/topics/delete.py +19 -0
- kafka/cli/admin/topics/describe.py +18 -0
- kafka/cli/admin/topics/list.py +11 -0
- kafka/cli/admin/transactions/__init__.py +17 -0
- kafka/cli/admin/transactions/abort.py +38 -0
- kafka/cli/admin/transactions/describe.py +24 -0
- kafka/cli/admin/transactions/describe_producers.py +29 -0
- kafka/cli/admin/transactions/find_hanging.py +26 -0
- kafka/cli/admin/transactions/list.py +37 -0
- kafka/cli/admin/users/__init__.py +8 -0
- kafka/cli/admin/users/alter_user_scram_credentials.py +34 -0
- kafka/cli/admin/users/describe_user_scram_credentials.py +15 -0
- kafka/cli/common.py +95 -0
- kafka/cli/consumer/__init__.py +63 -0
- kafka/cli/producer/__init__.py +57 -0
- kafka/cluster.py +824 -0
- kafka/codec.py +325 -0
- kafka/consumer/__init__.py +5 -0
- kafka/consumer/__main__.py +5 -0
- kafka/consumer/fetcher.py +2012 -0
- kafka/consumer/group.py +1347 -0
- kafka/consumer/subscription_state.py +897 -0
- kafka/coordinator/__init__.py +0 -0
- kafka/coordinator/assignors/__init__.py +0 -0
- kafka/coordinator/assignors/abstract.py +90 -0
- kafka/coordinator/assignors/cooperative_sticky.py +167 -0
- kafka/coordinator/assignors/range.py +81 -0
- kafka/coordinator/assignors/roundrobin.py +101 -0
- kafka/coordinator/assignors/sticky/StickyAssignorUserData.json +37 -0
- kafka/coordinator/assignors/sticky/__init__.py +0 -0
- kafka/coordinator/assignors/sticky/partition_movements.py +149 -0
- kafka/coordinator/assignors/sticky/sorted_set.py +63 -0
- kafka/coordinator/assignors/sticky/sticky_assignor.py +665 -0
- kafka/coordinator/assignors/sticky/user_data.py +8 -0
- kafka/coordinator/base.py +1215 -0
- kafka/coordinator/consumer.py +1224 -0
- kafka/coordinator/heartbeat.py +82 -0
- kafka/coordinator/subscription.py +34 -0
- kafka/errors.py +1004 -0
- kafka/future.py +166 -0
- kafka/metrics/__init__.py +13 -0
- kafka/metrics/compound_stat.py +33 -0
- kafka/metrics/dict_reporter.py +81 -0
- kafka/metrics/kafka_metric.py +36 -0
- kafka/metrics/measurable.py +27 -0
- kafka/metrics/measurable_stat.py +13 -0
- kafka/metrics/metric_config.py +33 -0
- kafka/metrics/metric_name.py +105 -0
- kafka/metrics/metrics.py +261 -0
- kafka/metrics/metrics_reporter.py +53 -0
- kafka/metrics/quota.py +41 -0
- kafka/metrics/stat.py +19 -0
- kafka/metrics/stats/__init__.py +15 -0
- kafka/metrics/stats/avg.py +24 -0
- kafka/metrics/stats/count.py +17 -0
- kafka/metrics/stats/histogram.py +99 -0
- kafka/metrics/stats/max_stat.py +17 -0
- kafka/metrics/stats/min_stat.py +19 -0
- kafka/metrics/stats/percentile.py +14 -0
- kafka/metrics/stats/percentiles.py +75 -0
- kafka/metrics/stats/rate.py +118 -0
- kafka/metrics/stats/sampled_stat.py +99 -0
- kafka/metrics/stats/sensor.py +136 -0
- kafka/metrics/stats/total.py +15 -0
- kafka/net/__init__.py +19 -0
- kafka/net/compat.py +165 -0
- kafka/net/connection.py +593 -0
- kafka/net/http_connect.py +144 -0
- kafka/net/inet.py +122 -0
- kafka/net/manager.py +451 -0
- kafka/net/metrics.py +149 -0
- kafka/net/sasl/__init__.py +32 -0
- kafka/net/sasl/abc.py +28 -0
- kafka/net/sasl/gssapi.py +95 -0
- kafka/net/sasl/msk.py +245 -0
- kafka/net/sasl/oauth.py +98 -0
- kafka/net/sasl/plain.py +42 -0
- kafka/net/sasl/scram.py +135 -0
- kafka/net/sasl/sspi.py +111 -0
- kafka/net/selector.py +644 -0
- kafka/net/socks5.py +262 -0
- kafka/net/transport.py +415 -0
- kafka/net/wakeup_notifier.py +72 -0
- kafka/partitioner/__init__.py +8 -0
- kafka/partitioner/abc.py +8 -0
- kafka/partitioner/default.py +89 -0
- kafka/partitioner/sticky.py +109 -0
- kafka/producer/__init__.py +5 -0
- kafka/producer/__main__.py +5 -0
- kafka/producer/future.py +101 -0
- kafka/producer/kafka.py +1123 -0
- kafka/producer/producer_batch.py +192 -0
- kafka/producer/record_accumulator.py +647 -0
- kafka/producer/sender.py +884 -0
- kafka/producer/transaction_manager.py +1326 -0
- kafka/protocol/__init__.py +0 -0
- kafka/protocol/admin/__init__.py +29 -0
- kafka/protocol/admin/acl.py +83 -0
- kafka/protocol/admin/acl.pyi +375 -0
- kafka/protocol/admin/client_quotas.py +14 -0
- kafka/protocol/admin/client_quotas.pyi +265 -0
- kafka/protocol/admin/cluster.py +31 -0
- kafka/protocol/admin/cluster.pyi +620 -0
- kafka/protocol/admin/configs.py +22 -0
- kafka/protocol/admin/configs.pyi +437 -0
- kafka/protocol/admin/groups.py +24 -0
- kafka/protocol/admin/groups.pyi +261 -0
- kafka/protocol/admin/topics.py +53 -0
- kafka/protocol/admin/topics.pyi +982 -0
- kafka/protocol/admin/transactions.py +18 -0
- kafka/protocol/admin/transactions.pyi +311 -0
- kafka/protocol/admin/users.py +14 -0
- kafka/protocol/admin/users.pyi +223 -0
- kafka/protocol/api_data.py +125 -0
- kafka/protocol/api_header.py +55 -0
- kafka/protocol/api_key.py +97 -0
- kafka/protocol/api_message.py +277 -0
- kafka/protocol/broker_version_data.py +246 -0
- kafka/protocol/consumer/__init__.py +13 -0
- kafka/protocol/consumer/fetch.py +16 -0
- kafka/protocol/consumer/fetch.pyi +298 -0
- kafka/protocol/consumer/group.py +38 -0
- kafka/protocol/consumer/group.pyi +824 -0
- kafka/protocol/consumer/metadata.py +30 -0
- kafka/protocol/consumer/metadata.pyi +89 -0
- kafka/protocol/consumer/offsets.py +75 -0
- kafka/protocol/consumer/offsets.pyi +288 -0
- kafka/protocol/data_container.py +166 -0
- kafka/protocol/frame.py +30 -0
- kafka/protocol/generate_stubs.py +468 -0
- kafka/protocol/metadata/__init__.py +10 -0
- kafka/protocol/metadata/api_versions.py +41 -0
- kafka/protocol/metadata/api_versions.pyi +128 -0
- kafka/protocol/metadata/find_coordinator.py +19 -0
- kafka/protocol/metadata/find_coordinator.pyi +105 -0
- kafka/protocol/metadata/metadata.py +34 -0
- kafka/protocol/metadata/metadata.pyi +160 -0
- kafka/protocol/old/__init__.py +0 -0
- kafka/protocol/old/abstract.py +17 -0
- kafka/protocol/old/add_offsets_to_txn.py +54 -0
- kafka/protocol/old/add_partitions_to_txn.py +71 -0
- kafka/protocol/old/admin.py +1086 -0
- kafka/protocol/old/api.py +205 -0
- kafka/protocol/old/api_versions.py +133 -0
- kafka/protocol/old/commit.py +355 -0
- kafka/protocol/old/consumer_protocol.py +36 -0
- kafka/protocol/old/end_txn.py +53 -0
- kafka/protocol/old/fetch.py +408 -0
- kafka/protocol/old/find_coordinator.py +72 -0
- kafka/protocol/old/group.py +451 -0
- kafka/protocol/old/init_producer_id.py +42 -0
- kafka/protocol/old/list_offsets.py +186 -0
- kafka/protocol/old/metadata.py +290 -0
- kafka/protocol/old/offset_for_leader_epoch.py +133 -0
- kafka/protocol/old/produce.py +247 -0
- kafka/protocol/old/sasl_authenticate.py +38 -0
- kafka/protocol/old/sasl_handshake.py +39 -0
- kafka/protocol/old/struct.py +87 -0
- kafka/protocol/old/txn_offset_commit.py +73 -0
- kafka/protocol/old/types.py +440 -0
- kafka/protocol/parser.py +191 -0
- kafka/protocol/producer/__init__.py +7 -0
- kafka/protocol/producer/produce.py +17 -0
- kafka/protocol/producer/produce.pyi +197 -0
- kafka/protocol/producer/transaction.py +30 -0
- kafka/protocol/producer/transaction.pyi +663 -0
- kafka/protocol/sasl.py +52 -0
- kafka/protocol/sasl.pyi +126 -0
- kafka/protocol/schemas/__init__.py +7 -0
- kafka/protocol/schemas/fields/__init__.py +7 -0
- kafka/protocol/schemas/fields/array.py +127 -0
- kafka/protocol/schemas/fields/base.py +156 -0
- kafka/protocol/schemas/fields/codecs/__init__.py +12 -0
- kafka/protocol/schemas/fields/codecs/encode_buffer.py +82 -0
- kafka/protocol/schemas/fields/codecs/tagged_fields.py +109 -0
- kafka/protocol/schemas/fields/codecs/types.py +505 -0
- kafka/protocol/schemas/fields/codegen.py +40 -0
- kafka/protocol/schemas/fields/simple.py +127 -0
- kafka/protocol/schemas/fields/struct.py +357 -0
- kafka/protocol/schemas/fields/struct_array.py +142 -0
- kafka/protocol/schemas/load_json.py +42 -0
- kafka/protocol/schemas/resources/AddOffsetsToTxnRequest.json +40 -0
- kafka/protocol/schemas/resources/AddOffsetsToTxnResponse.json +35 -0
- kafka/protocol/schemas/resources/AddPartitionsToTxnRequest.json +65 -0
- kafka/protocol/schemas/resources/AddPartitionsToTxnResponse.json +60 -0
- kafka/protocol/schemas/resources/AlterClientQuotasRequest.json +47 -0
- kafka/protocol/schemas/resources/AlterClientQuotasResponse.json +41 -0
- kafka/protocol/schemas/resources/AlterConfigsRequest.json +43 -0
- kafka/protocol/schemas/resources/AlterConfigsResponse.json +39 -0
- kafka/protocol/schemas/resources/AlterPartitionReassignmentsRequest.json +42 -0
- kafka/protocol/schemas/resources/AlterPartitionReassignmentsResponse.json +47 -0
- kafka/protocol/schemas/resources/AlterReplicaLogDirsRequest.json +41 -0
- kafka/protocol/schemas/resources/AlterReplicaLogDirsResponse.json +41 -0
- kafka/protocol/schemas/resources/AlterUserScramCredentialsRequest.json +45 -0
- kafka/protocol/schemas/resources/AlterUserScramCredentialsResponse.json +35 -0
- kafka/protocol/schemas/resources/ApiVersionsRequest.json +34 -0
- kafka/protocol/schemas/resources/ApiVersionsResponse.json +79 -0
- kafka/protocol/schemas/resources/ConsumerProtocolAssignment.json +42 -0
- kafka/protocol/schemas/resources/ConsumerProtocolSubscription.json +49 -0
- kafka/protocol/schemas/resources/CreateAclsRequest.json +46 -0
- kafka/protocol/schemas/resources/CreateAclsResponse.json +37 -0
- kafka/protocol/schemas/resources/CreatePartitionsRequest.json +47 -0
- kafka/protocol/schemas/resources/CreatePartitionsResponse.json +41 -0
- kafka/protocol/schemas/resources/CreateTopicsRequest.json +65 -0
- kafka/protocol/schemas/resources/CreateTopicsResponse.json +72 -0
- kafka/protocol/schemas/resources/DeleteAclsRequest.json +46 -0
- kafka/protocol/schemas/resources/DeleteAclsResponse.json +59 -0
- kafka/protocol/schemas/resources/DeleteGroupsRequest.json +30 -0
- kafka/protocol/schemas/resources/DeleteGroupsResponse.json +36 -0
- kafka/protocol/schemas/resources/DeleteRecordsRequest.json +42 -0
- kafka/protocol/schemas/resources/DeleteRecordsResponse.json +43 -0
- kafka/protocol/schemas/resources/DeleteTopicsRequest.json +43 -0
- kafka/protocol/schemas/resources/DeleteTopicsResponse.json +52 -0
- kafka/protocol/schemas/resources/DescribeAclsRequest.json +43 -0
- kafka/protocol/schemas/resources/DescribeAclsResponse.json +55 -0
- kafka/protocol/schemas/resources/DescribeClientQuotasRequest.json +37 -0
- kafka/protocol/schemas/resources/DescribeClientQuotasResponse.json +47 -0
- kafka/protocol/schemas/resources/DescribeClusterRequest.json +35 -0
- kafka/protocol/schemas/resources/DescribeClusterResponse.json +56 -0
- kafka/protocol/schemas/resources/DescribeConfigsRequest.json +42 -0
- kafka/protocol/schemas/resources/DescribeConfigsResponse.json +69 -0
- kafka/protocol/schemas/resources/DescribeGroupsRequest.json +38 -0
- kafka/protocol/schemas/resources/DescribeGroupsResponse.json +74 -0
- kafka/protocol/schemas/resources/DescribeLogDirsRequest.json +38 -0
- kafka/protocol/schemas/resources/DescribeLogDirsResponse.json +65 -0
- kafka/protocol/schemas/resources/DescribeProducersRequest.json +32 -0
- kafka/protocol/schemas/resources/DescribeProducersResponse.json +55 -0
- kafka/protocol/schemas/resources/DescribeQuorumRequest.json +39 -0
- kafka/protocol/schemas/resources/DescribeQuorumResponse.json +82 -0
- kafka/protocol/schemas/resources/DescribeTopicPartitionsRequest.json +40 -0
- kafka/protocol/schemas/resources/DescribeTopicPartitionsResponse.json +66 -0
- kafka/protocol/schemas/resources/DescribeTransactionsRequest.json +27 -0
- kafka/protocol/schemas/resources/DescribeTransactionsResponse.json +52 -0
- kafka/protocol/schemas/resources/DescribeUserScramCredentialsRequest.json +30 -0
- kafka/protocol/schemas/resources/DescribeUserScramCredentialsResponse.json +45 -0
- kafka/protocol/schemas/resources/ElectLeadersRequest.json +41 -0
- kafka/protocol/schemas/resources/ElectLeadersResponse.json +45 -0
- kafka/protocol/schemas/resources/EndTxnRequest.json +43 -0
- kafka/protocol/schemas/resources/EndTxnResponse.json +41 -0
- kafka/protocol/schemas/resources/FetchRequest.json +125 -0
- kafka/protocol/schemas/resources/FetchResponse.json +124 -0
- kafka/protocol/schemas/resources/FindCoordinatorRequest.json +43 -0
- kafka/protocol/schemas/resources/FindCoordinatorResponse.json +58 -0
- kafka/protocol/schemas/resources/HeartbeatRequest.json +39 -0
- kafka/protocol/schemas/resources/HeartbeatResponse.json +35 -0
- kafka/protocol/schemas/resources/IncrementalAlterConfigsRequest.json +44 -0
- kafka/protocol/schemas/resources/IncrementalAlterConfigsResponse.json +38 -0
- kafka/protocol/schemas/resources/InitProducerIdRequest.json +50 -0
- kafka/protocol/schemas/resources/InitProducerIdResponse.json +47 -0
- kafka/protocol/schemas/resources/JoinGroupRequest.json +63 -0
- kafka/protocol/schemas/resources/JoinGroupResponse.json +69 -0
- kafka/protocol/schemas/resources/LeaveGroupRequest.json +47 -0
- kafka/protocol/schemas/resources/LeaveGroupResponse.json +47 -0
- kafka/protocol/schemas/resources/ListConfigResourcesRequest.json +31 -0
- kafka/protocol/schemas/resources/ListConfigResourcesResponse.json +37 -0
- kafka/protocol/schemas/resources/ListGroupsRequest.json +36 -0
- kafka/protocol/schemas/resources/ListGroupsResponse.json +49 -0
- kafka/protocol/schemas/resources/ListOffsetsRequest.json +72 -0
- kafka/protocol/schemas/resources/ListOffsetsResponse.json +71 -0
- kafka/protocol/schemas/resources/ListPartitionReassignmentsRequest.json +34 -0
- kafka/protocol/schemas/resources/ListPartitionReassignmentsResponse.json +46 -0
- kafka/protocol/schemas/resources/ListTransactionsRequest.json +40 -0
- kafka/protocol/schemas/resources/ListTransactionsResponse.json +42 -0
- kafka/protocol/schemas/resources/MetadataRequest.json +56 -0
- kafka/protocol/schemas/resources/MetadataResponse.json +101 -0
- kafka/protocol/schemas/resources/OffsetCommitRequest.json +76 -0
- kafka/protocol/schemas/resources/OffsetCommitResponse.json +71 -0
- kafka/protocol/schemas/resources/OffsetDeleteRequest.json +39 -0
- kafka/protocol/schemas/resources/OffsetDeleteResponse.json +42 -0
- kafka/protocol/schemas/resources/OffsetFetchRequest.json +76 -0
- kafka/protocol/schemas/resources/OffsetFetchResponse.json +107 -0
- kafka/protocol/schemas/resources/OffsetForLeaderEpochRequest.json +52 -0
- kafka/protocol/schemas/resources/OffsetForLeaderEpochResponse.json +51 -0
- kafka/protocol/schemas/resources/ProduceRequest.json +73 -0
- kafka/protocol/schemas/resources/ProduceResponse.json +96 -0
- kafka/protocol/schemas/resources/RequestHeader.json +44 -0
- kafka/protocol/schemas/resources/ResponseHeader.json +26 -0
- kafka/protocol/schemas/resources/SaslAuthenticateRequest.json +29 -0
- kafka/protocol/schemas/resources/SaslAuthenticateResponse.json +34 -0
- kafka/protocol/schemas/resources/SaslHandshakeRequest.json +31 -0
- kafka/protocol/schemas/resources/SaslHandshakeResponse.json +32 -0
- kafka/protocol/schemas/resources/SyncGroupRequest.json +56 -0
- kafka/protocol/schemas/resources/SyncGroupResponse.json +46 -0
- kafka/protocol/schemas/resources/TxnOffsetCommitRequest.json +68 -0
- kafka/protocol/schemas/resources/TxnOffsetCommitResponse.json +47 -0
- kafka/protocol/schemas/resources/UpdateFeaturesRequest.json +43 -0
- kafka/protocol/schemas/resources/UpdateFeaturesResponse.json +39 -0
- kafka/protocol/schemas/resources/WriteTxnMarkersRequest.json +49 -0
- kafka/protocol/schemas/resources/WriteTxnMarkersResponse.json +45 -0
- kafka/protocol/schemas/resources/__init__.py +0 -0
- kafka/record/__init__.py +3 -0
- kafka/record/_crc32c.py +161 -0
- kafka/record/abc.py +144 -0
- kafka/record/default_records.py +782 -0
- kafka/record/legacy_records.py +587 -0
- kafka/record/memory_records.py +255 -0
- kafka/record/util.py +135 -0
- kafka/serializer/__init__.py +4 -0
- kafka/serializer/abstract.py +20 -0
- kafka/serializer/default.py +16 -0
- kafka/serializer/json.py +17 -0
- kafka/serializer/wrapper.py +21 -0
- kafka/structs.py +69 -0
- kafka/util.py +159 -0
- kafka/vendor/__init__.py +0 -0
- kafka/version.py +1 -0
- kafka_python-3.0.0.dist-info/METADATA +319 -0
- kafka_python-3.0.0.dist-info/RECORD +373 -0
- kafka_python-3.0.0.dist-info/WHEEL +5 -0
- kafka_python-3.0.0.dist-info/entry_points.txt +2 -0
- kafka_python-3.0.0.dist-info/licenses/LICENSE +202 -0
- 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
|