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
kafka/cluster.py
ADDED
|
@@ -0,0 +1,824 @@
|
|
|
1
|
+
import collections
|
|
2
|
+
import copy
|
|
3
|
+
import logging
|
|
4
|
+
import random
|
|
5
|
+
import re
|
|
6
|
+
import socket
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
import uuid
|
|
10
|
+
import weakref
|
|
11
|
+
|
|
12
|
+
from kafka import errors as Errors
|
|
13
|
+
from kafka.future import Future
|
|
14
|
+
from kafka.net.wakeup_notifier import WakeupNotifier
|
|
15
|
+
from kafka.protocol.metadata import MetadataRequest, MetadataResponse, CoordinatorType
|
|
16
|
+
from kafka.structs import TopicPartition
|
|
17
|
+
from kafka.util import ensure_valid_topic_name
|
|
18
|
+
|
|
19
|
+
log = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ClusterMetadata:
|
|
23
|
+
"""
|
|
24
|
+
A class to manage kafka cluster metadata.
|
|
25
|
+
|
|
26
|
+
Keyword Arguments:
|
|
27
|
+
retry_backoff_ms (int): Milliseconds to backoff when retrying on
|
|
28
|
+
errors. Default: 100.
|
|
29
|
+
metadata_max_age_ms (int): The period of time in milliseconds after
|
|
30
|
+
which we force a refresh of metadata even if we haven't seen any
|
|
31
|
+
partition leadership changes to proactively discover any new
|
|
32
|
+
brokers or partitions. Default: 300000
|
|
33
|
+
bootstrap_servers: 'host[:port]' string (or list of 'host[:port]'
|
|
34
|
+
strings) that the client should contact to bootstrap initial
|
|
35
|
+
cluster metadata. This does not have to be the full node list.
|
|
36
|
+
It just needs to have at least one broker that will respond to a
|
|
37
|
+
Metadata API Request. Default port is 9092. If no servers are
|
|
38
|
+
specified, will default to localhost:9092.
|
|
39
|
+
allow_auto_create_topics (bool): Enable/disable auto topic creation
|
|
40
|
+
on metadata request. Only available with api_version >= (0, 11).
|
|
41
|
+
Default: True
|
|
42
|
+
"""
|
|
43
|
+
DEFAULT_CONFIG = {
|
|
44
|
+
'retry_backoff_ms': 100,
|
|
45
|
+
'metadata_max_age_ms': 300000,
|
|
46
|
+
'bootstrap_servers': [],
|
|
47
|
+
'allow_auto_create_topics': True,
|
|
48
|
+
'client_dns_lookup': 'use_all_dns_ips',
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
def __init__(self, **configs):
|
|
52
|
+
self._manager = None
|
|
53
|
+
self._topics = set()
|
|
54
|
+
self._brokers = {} # node_id -> MetadataResponseBroker
|
|
55
|
+
self._partitions = {} # topic -> partition -> PartitionMetadata
|
|
56
|
+
self._broker_partitions = collections.defaultdict(set) # node_id -> {TopicPartition...}
|
|
57
|
+
self._topic_ids = {} # topic name -> uuid.UUID
|
|
58
|
+
self._topic_names_by_id = {} # uuid.UUID -> topic name
|
|
59
|
+
self._coordinators = {} # (key_type, key) -> node_id
|
|
60
|
+
self._last_refresh_ms = 0
|
|
61
|
+
self._last_successful_refresh_ms = 0
|
|
62
|
+
self._need_update = True
|
|
63
|
+
self._future = None
|
|
64
|
+
self._listeners = set()
|
|
65
|
+
self._lock = threading.Lock()
|
|
66
|
+
self.need_all_topic_metadata = False
|
|
67
|
+
self.unauthorized_topics = set()
|
|
68
|
+
self.internal_topics = set()
|
|
69
|
+
self.controller = None
|
|
70
|
+
self.cluster_id = None
|
|
71
|
+
|
|
72
|
+
self._refresh_loop_future = None
|
|
73
|
+
self._refresh_future = None
|
|
74
|
+
self._wakeup = None
|
|
75
|
+
self.closed = False
|
|
76
|
+
|
|
77
|
+
self.config = copy.copy(self.DEFAULT_CONFIG)
|
|
78
|
+
for key in self.config:
|
|
79
|
+
if key in configs:
|
|
80
|
+
self.config[key] = configs[key]
|
|
81
|
+
|
|
82
|
+
self._bootstrap_brokers = self._generate_bootstrap_brokers()
|
|
83
|
+
self._coordinator_brokers = {}
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def metadata_refresh_in_progress(self):
|
|
87
|
+
"""True if a refresh is mid-flight."""
|
|
88
|
+
return self._refresh_future is not None and not self._refresh_future.is_done
|
|
89
|
+
|
|
90
|
+
def attach(self, manager):
|
|
91
|
+
"""Wire this cluster to its connection manager.
|
|
92
|
+
|
|
93
|
+
Construction is split from attach so ClusterMetadata can be built
|
|
94
|
+
standalone (tests, snapshots) without a live manager. The reference is
|
|
95
|
+
held via weakref.proxy so that manager <-> cluster does not form a GC
|
|
96
|
+
cycle; manager.close() still calls cluster.close() to clear eagerly.
|
|
97
|
+
"""
|
|
98
|
+
self._manager = weakref.proxy(manager)
|
|
99
|
+
self._wakeup = WakeupNotifier(self._manager._net)
|
|
100
|
+
|
|
101
|
+
def close(self):
|
|
102
|
+
self.closed = True
|
|
103
|
+
self._wakeup.notify()
|
|
104
|
+
|
|
105
|
+
def start_refresh_loop(self):
|
|
106
|
+
"""Spawn the periodic refresh coroutine. Idempotent. Triggers bootstrap if needed."""
|
|
107
|
+
if self._manager is None:
|
|
108
|
+
raise RuntimeError('start_refresh_loop requires prior attach()')
|
|
109
|
+
if self._refresh_loop_future is not None:
|
|
110
|
+
return
|
|
111
|
+
self._refresh_loop_future = self._manager.call_soon(self._refresh_loop)
|
|
112
|
+
|
|
113
|
+
async def _refresh_loop(self):
|
|
114
|
+
"""Awaits ttl() then triggers refresh_metadata(); request_update() wakes early."""
|
|
115
|
+
if self._manager is None:
|
|
116
|
+
raise RuntimeError('start_refresh_loop requires prior attach()')
|
|
117
|
+
if not self._manager.bootstrapped:
|
|
118
|
+
log.debug('Metadata refresh loop needs bootstrap...')
|
|
119
|
+
await self._manager.bootstrap_async()
|
|
120
|
+
log.info('Starting metadata refresh loop')
|
|
121
|
+
while not self.closed:
|
|
122
|
+
if self.metadata_refresh_in_progress:
|
|
123
|
+
await self._refresh_future
|
|
124
|
+
ttl_ms = self.ttl()
|
|
125
|
+
if ttl_ms == 0:
|
|
126
|
+
try:
|
|
127
|
+
await self.refresh_metadata()
|
|
128
|
+
except Errors.KafkaError as exc:
|
|
129
|
+
log.debug('Metadata refresh failed: %s', exc)
|
|
130
|
+
log.exception(exc)
|
|
131
|
+
continue
|
|
132
|
+
try:
|
|
133
|
+
log.debug('Sleeping %s for next Metadata refresh', ttl_ms / 1000)
|
|
134
|
+
await self._wakeup(ttl_ms / 1000)
|
|
135
|
+
except Exception as exc:
|
|
136
|
+
log.error('Metadata refresh loop error: %s', exc)
|
|
137
|
+
log.info('Stopping metadata refresh loop')
|
|
138
|
+
|
|
139
|
+
async def refresh_metadata(self, node_id=None):
|
|
140
|
+
"""Send one MetadataRequest and apply the response.
|
|
141
|
+
|
|
142
|
+
Concurrent callers share a single in-flight request: if a refresh is
|
|
143
|
+
already underway, additional callers await the same Future and see the
|
|
144
|
+
same outcome (success or exception). This avoids duplicate broker
|
|
145
|
+
requests when bootstrap and the refresh loop race, or when external
|
|
146
|
+
callers invoke refresh while the loop is mid-flight.
|
|
147
|
+
"""
|
|
148
|
+
if self._manager is None:
|
|
149
|
+
raise RuntimeError('refresh_metadata requires prior attach()')
|
|
150
|
+
if self.metadata_refresh_in_progress:
|
|
151
|
+
log.debug('Metadata refresh already in flight; awaiting existing')
|
|
152
|
+
await self._refresh_future
|
|
153
|
+
return
|
|
154
|
+
self._refresh_future = Future()
|
|
155
|
+
try:
|
|
156
|
+
await self._do_refresh_metadata(node_id)
|
|
157
|
+
except Exception as exc:
|
|
158
|
+
self._refresh_future.failure(exc)
|
|
159
|
+
raise
|
|
160
|
+
else:
|
|
161
|
+
self._refresh_future.success(None)
|
|
162
|
+
|
|
163
|
+
async def _do_refresh_metadata(self, node_id):
|
|
164
|
+
node_id = self._manager.least_loaded_node() if node_id is None else node_id
|
|
165
|
+
if node_id is None:
|
|
166
|
+
log.warning('No node available for metadata refresh - backoff/retry')
|
|
167
|
+
self._manager.update_backoff('metadata')
|
|
168
|
+
raise Errors.NodeNotReadyError('metadata')
|
|
169
|
+
else:
|
|
170
|
+
self._manager.reset_backoff('metadata')
|
|
171
|
+
log.info(f'Metadata refresh (node_id={node_id})')
|
|
172
|
+
try:
|
|
173
|
+
request = self.metadata_request()
|
|
174
|
+
log.debug("Sending metadata request %s to node %s", request, node_id)
|
|
175
|
+
response = await self._manager.send(request, node_id)
|
|
176
|
+
except Exception as exc:
|
|
177
|
+
log.error('Metadata refresh: failed %s', exc)
|
|
178
|
+
self.failed_update(exc)
|
|
179
|
+
raise
|
|
180
|
+
log.debug('Metadata refresh: success')
|
|
181
|
+
self.update_metadata(response)
|
|
182
|
+
|
|
183
|
+
def _generate_bootstrap_brokers(self):
|
|
184
|
+
# collect_hosts does not perform DNS, so we should be fine to re-use
|
|
185
|
+
bootstrap_hosts = collect_hosts(self.config['bootstrap_servers'])
|
|
186
|
+
|
|
187
|
+
if self.config['client_dns_lookup'] == 'resolve_canonical_bootstrap_servers_only':
|
|
188
|
+
bootstrap_hosts = expand_to_canonical_bootstrap_hosts(bootstrap_hosts)
|
|
189
|
+
|
|
190
|
+
brokers = {}
|
|
191
|
+
for i, (host, port, _) in enumerate(bootstrap_hosts):
|
|
192
|
+
node_id = 'bootstrap-%s' % i
|
|
193
|
+
brokers[node_id] = MetadataResponse.MetadataResponseBroker(node_id, host, port, None)
|
|
194
|
+
return brokers
|
|
195
|
+
|
|
196
|
+
def is_bootstrap(self, node_id):
|
|
197
|
+
return node_id in self._bootstrap_brokers
|
|
198
|
+
|
|
199
|
+
def set_topics(self, topics):
|
|
200
|
+
"""Set specific topics to track for metadata.
|
|
201
|
+
|
|
202
|
+
Arguments:
|
|
203
|
+
topics (list of str): topics to check for metadata
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
Future: resolves after metadata request/response
|
|
207
|
+
"""
|
|
208
|
+
for topic in topics:
|
|
209
|
+
ensure_valid_topic_name(topic)
|
|
210
|
+
if not set(topics).difference(self._topics):
|
|
211
|
+
return Future().success(self)
|
|
212
|
+
# TODO: handle future when old metadata request is currently in-flight
|
|
213
|
+
# TODO: handle future when set_topics called multiple times before new request
|
|
214
|
+
self._topics = set(topics)
|
|
215
|
+
return self.request_update()
|
|
216
|
+
|
|
217
|
+
def add_topic(self, topic):
|
|
218
|
+
"""Add a topic to the list of topics tracked via metadata.
|
|
219
|
+
|
|
220
|
+
Arguments:
|
|
221
|
+
topic (str): topic to track
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
Future: resolves after metadata request/response
|
|
225
|
+
|
|
226
|
+
Raises:
|
|
227
|
+
TypeError: if topic is not a string
|
|
228
|
+
ValueError: if topic is invalid: must be chars (a-zA-Z0-9._-), and less than 250 length
|
|
229
|
+
"""
|
|
230
|
+
ensure_valid_topic_name(topic)
|
|
231
|
+
if topic in self._topics:
|
|
232
|
+
return Future().success(self)
|
|
233
|
+
# TODO: handle future when old metadata request is currently in-flight
|
|
234
|
+
self._topics.add(topic)
|
|
235
|
+
return self.request_update()
|
|
236
|
+
|
|
237
|
+
def brokers(self):
|
|
238
|
+
"""Get all MetadataResponseBroker
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
list: [MetadataResponseBroker, ...]
|
|
242
|
+
"""
|
|
243
|
+
return list(self._brokers.values())
|
|
244
|
+
|
|
245
|
+
def bootstrap_brokers(self):
|
|
246
|
+
"""Get bootstrap brokers only, extracted from the
|
|
247
|
+
bootstrap_servers config option. Node ids are synthesized
|
|
248
|
+
as 'bootstrap-0' etc.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
list: [MetadataResponseBroker, ...]
|
|
252
|
+
"""
|
|
253
|
+
return list(self._bootstrap_brokers.values())
|
|
254
|
+
|
|
255
|
+
def broker_metadata(self, broker_id):
|
|
256
|
+
"""Get MetadataResponseBroker
|
|
257
|
+
|
|
258
|
+
Arguments:
|
|
259
|
+
broker_id (int or str): node_id for a broker to check
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
MetadataResponseBroker or None if not found
|
|
263
|
+
"""
|
|
264
|
+
return (
|
|
265
|
+
self._brokers.get(broker_id) or
|
|
266
|
+
self._bootstrap_brokers.get(broker_id) or
|
|
267
|
+
self._coordinator_brokers.get(broker_id)
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
def partitions_for_topic(self, topic):
|
|
271
|
+
"""Return set of all partitions for topic (whether available or not)
|
|
272
|
+
|
|
273
|
+
Arguments:
|
|
274
|
+
topic (str): topic to check for partitions
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
set: {partition (int), ...}
|
|
278
|
+
None if topic not found.
|
|
279
|
+
"""
|
|
280
|
+
if topic not in self._partitions:
|
|
281
|
+
return None
|
|
282
|
+
return set(self._partitions[topic].keys())
|
|
283
|
+
|
|
284
|
+
def available_partitions_for_topic(self, topic):
|
|
285
|
+
"""Return set of partitions with known leaders
|
|
286
|
+
|
|
287
|
+
Arguments:
|
|
288
|
+
topic (str): topic to check for partitions
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
set: {partition (int), ...}
|
|
292
|
+
None if topic not found.
|
|
293
|
+
"""
|
|
294
|
+
if topic not in self._partitions:
|
|
295
|
+
return None
|
|
296
|
+
return set([partition for partition, metadata
|
|
297
|
+
in self._partitions[topic].items()
|
|
298
|
+
if metadata.leader_id != -1])
|
|
299
|
+
|
|
300
|
+
def leader_for_partition(self, partition):
|
|
301
|
+
"""Return node_id of leader, -1 unavailable, None if unknown."""
|
|
302
|
+
if partition.topic not in self._partitions:
|
|
303
|
+
return None
|
|
304
|
+
elif partition.partition not in self._partitions[partition.topic]:
|
|
305
|
+
return None
|
|
306
|
+
return self._partitions[partition.topic][partition.partition].leader_id
|
|
307
|
+
|
|
308
|
+
def is_replica_node(self, partition, node_id):
|
|
309
|
+
"""Return MetadataResponseBroker for ``node_id`` only when it is
|
|
310
|
+
known AND still listed as a replica of ``partition`` (KIP-392).
|
|
311
|
+
|
|
312
|
+
Used by the consumer's preferred-read-replica routing to avoid
|
|
313
|
+
sending fetches to a broker that has been demoted out of the
|
|
314
|
+
partition's replica set even though it still exists as a node.
|
|
315
|
+
|
|
316
|
+
Arguments:
|
|
317
|
+
partition (TopicPartition): topic / partition to look up.
|
|
318
|
+
node_id (int): broker id to validate.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
MetadataResponseBroker if the node exists in cluster metadata
|
|
322
|
+
and is currently listed as a replica of ``partition``;
|
|
323
|
+
otherwise None.
|
|
324
|
+
"""
|
|
325
|
+
broker = self.broker_metadata(node_id)
|
|
326
|
+
if broker is None:
|
|
327
|
+
return None
|
|
328
|
+
if partition.topic not in self._partitions:
|
|
329
|
+
return None
|
|
330
|
+
partition_data = self._partitions[partition.topic].get(partition.partition)
|
|
331
|
+
if partition_data is None:
|
|
332
|
+
return None
|
|
333
|
+
if node_id not in partition_data.replica_nodes:
|
|
334
|
+
return None
|
|
335
|
+
return broker
|
|
336
|
+
|
|
337
|
+
def leader_epoch_for_partition(self, partition):
|
|
338
|
+
"""Return leader_epoch for partition, or None if topic/partition is unknown."""
|
|
339
|
+
if partition.topic not in self._partitions:
|
|
340
|
+
return None
|
|
341
|
+
elif partition.partition not in self._partitions[partition.topic]:
|
|
342
|
+
return None
|
|
343
|
+
return self._partitions[partition.topic][partition.partition].leader_epoch
|
|
344
|
+
|
|
345
|
+
def update_partition_leader(self, partition, leader_id, leader_epoch):
|
|
346
|
+
"""Apply a KIP-951 current-leader hint from a Fetch/Produce response.
|
|
347
|
+
|
|
348
|
+
The cached leader id and epoch for ``partition`` are replaced only when
|
|
349
|
+
``leader_epoch`` is strictly newer than the cached value (and
|
|
350
|
+
non-negative). When the leader id moves, ``_broker_partitions`` is
|
|
351
|
+
rewired so leader-based routing follows immediately.
|
|
352
|
+
|
|
353
|
+
Arguments:
|
|
354
|
+
partition (TopicPartition): topic / partition the hint is about.
|
|
355
|
+
leader_id (int): broker id named as the new leader.
|
|
356
|
+
leader_epoch (int): epoch of that new leader.
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
bool: True iff cached state was changed.
|
|
360
|
+
"""
|
|
361
|
+
with self._lock:
|
|
362
|
+
p_data = self._partitions.get(partition.topic, {}).get(partition.partition)
|
|
363
|
+
if p_data is None:
|
|
364
|
+
return False
|
|
365
|
+
if leader_epoch < 0 or leader_epoch <= p_data.leader_epoch:
|
|
366
|
+
return False
|
|
367
|
+
old_leader = p_data.leader_id
|
|
368
|
+
p_data.leader_id = leader_id
|
|
369
|
+
p_data.leader_epoch = leader_epoch
|
|
370
|
+
if old_leader != leader_id:
|
|
371
|
+
if old_leader in self._broker_partitions:
|
|
372
|
+
self._broker_partitions[old_leader].discard(partition)
|
|
373
|
+
if leader_id != -1:
|
|
374
|
+
self._broker_partitions[leader_id].add(partition)
|
|
375
|
+
return True
|
|
376
|
+
|
|
377
|
+
def partitions_for_broker(self, broker_id):
|
|
378
|
+
"""Return TopicPartitions for which the broker is a leader.
|
|
379
|
+
|
|
380
|
+
Arguments:
|
|
381
|
+
broker_id (int or str): node id for a broker
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
set: {TopicPartition, ...}
|
|
385
|
+
None if the broker either has no partitions or does not exist.
|
|
386
|
+
"""
|
|
387
|
+
return self._broker_partitions.get(broker_id)
|
|
388
|
+
|
|
389
|
+
def get_coordinator(self, key, key_type=CoordinatorType.GROUP):
|
|
390
|
+
"""Return node_id of group coordinator from cache.
|
|
391
|
+
|
|
392
|
+
Arguments:
|
|
393
|
+
key (str): name of consumer group or transaction_id
|
|
394
|
+
key_type (CoordinatorType, optional): Default GROUP
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
node_id (int or str) for coordinator, -1 if coordinator unknown
|
|
398
|
+
None if the group does not exist.
|
|
399
|
+
"""
|
|
400
|
+
return self._coordinators.get((key_type, key))
|
|
401
|
+
|
|
402
|
+
def ttl(self):
|
|
403
|
+
"""Milliseconds until metadata should be refreshed"""
|
|
404
|
+
now = time.monotonic() * 1000
|
|
405
|
+
if self._manager is not None and self._manager.connection_delay('metadata'):
|
|
406
|
+
# Exponential backoff - KIP-580
|
|
407
|
+
return self._manager.connection_delay('metadata') * 1000
|
|
408
|
+
elif self._need_update:
|
|
409
|
+
ttl = 0
|
|
410
|
+
else:
|
|
411
|
+
metadata_age = now - self._last_successful_refresh_ms
|
|
412
|
+
ttl = self.config['metadata_max_age_ms'] - metadata_age
|
|
413
|
+
|
|
414
|
+
retry_age = now - self._last_refresh_ms
|
|
415
|
+
next_retry = self.config['retry_backoff_ms'] - retry_age
|
|
416
|
+
|
|
417
|
+
return max(ttl, next_retry, 0)
|
|
418
|
+
|
|
419
|
+
def refresh_backoff(self):
|
|
420
|
+
"""Return milliseconds to wait before attempting to retry after failure"""
|
|
421
|
+
return self.config['retry_backoff_ms']
|
|
422
|
+
|
|
423
|
+
def request_update(self):
|
|
424
|
+
"""Flags metadata for update, return Future()
|
|
425
|
+
|
|
426
|
+
Actual update must be handled separately. This method will only
|
|
427
|
+
change the reported ttl()
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
kafka.future.Future (value will be the cluster object after update)
|
|
431
|
+
"""
|
|
432
|
+
with self._lock:
|
|
433
|
+
self._need_update = True
|
|
434
|
+
if not self._future or self._future.is_done:
|
|
435
|
+
self._future = Future()
|
|
436
|
+
ret = self._future
|
|
437
|
+
if self._manager:
|
|
438
|
+
self.start_refresh_loop()
|
|
439
|
+
self._wakeup.notify()
|
|
440
|
+
return ret
|
|
441
|
+
|
|
442
|
+
@property
|
|
443
|
+
def need_update(self):
|
|
444
|
+
return self._need_update
|
|
445
|
+
|
|
446
|
+
def topics(self, exclude_internal_topics=True):
|
|
447
|
+
"""Get set of known topics.
|
|
448
|
+
|
|
449
|
+
Arguments:
|
|
450
|
+
exclude_internal_topics (bool): Whether records from internal topics
|
|
451
|
+
(such as offsets) should be exposed to the consumer. If set to
|
|
452
|
+
True the only way to receive records from an internal topic is
|
|
453
|
+
subscribing to it. Default True
|
|
454
|
+
|
|
455
|
+
Returns:
|
|
456
|
+
set: {topic (str), ...}
|
|
457
|
+
"""
|
|
458
|
+
topics = set(self._partitions.keys())
|
|
459
|
+
if exclude_internal_topics:
|
|
460
|
+
return topics - self.internal_topics
|
|
461
|
+
else:
|
|
462
|
+
return topics
|
|
463
|
+
|
|
464
|
+
def metadata_request(self):
|
|
465
|
+
if self.need_all_topic_metadata:
|
|
466
|
+
topics = MetadataRequest.ALL_TOPICS
|
|
467
|
+
elif not self._topics:
|
|
468
|
+
topics = MetadataRequest.NO_TOPICS
|
|
469
|
+
else:
|
|
470
|
+
topics = [MetadataRequest.MetadataRequestTopic(name=topic)
|
|
471
|
+
for topic in self._topics]
|
|
472
|
+
return MetadataRequest(
|
|
473
|
+
topics=topics,
|
|
474
|
+
allow_auto_topic_creation=self.config['allow_auto_create_topics'],
|
|
475
|
+
include_cluster_authorized_operations=False,
|
|
476
|
+
include_topic_authorized_operations=False,
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
def topic_id(self, topic_name):
|
|
480
|
+
"""Return the topic UUID for ``topic_name``, or None if unknown.
|
|
481
|
+
|
|
482
|
+
Populated from MetadataResponse v10+ (Kafka 2.8+, KIP-516). Older
|
|
483
|
+
responses leave this empty.
|
|
484
|
+
"""
|
|
485
|
+
return self._topic_ids.get(topic_name)
|
|
486
|
+
|
|
487
|
+
def topic_name_for_id(self, topic_id):
|
|
488
|
+
"""Return the topic name for ``topic_id`` (uuid.UUID), or None.
|
|
489
|
+
|
|
490
|
+
Reverse lookup of :meth:`topic_id`. Populated from MetadataResponse
|
|
491
|
+
v10+ (KIP-516).
|
|
492
|
+
"""
|
|
493
|
+
return self._topic_names_by_id.get(topic_id)
|
|
494
|
+
|
|
495
|
+
def failed_update(self, exception):
|
|
496
|
+
"""Update cluster state given a failed MetadataRequest."""
|
|
497
|
+
f = None
|
|
498
|
+
with self._lock:
|
|
499
|
+
if self._future:
|
|
500
|
+
f = self._future
|
|
501
|
+
self._future = None
|
|
502
|
+
self._last_refresh_ms = time.monotonic() * 1000
|
|
503
|
+
if f:
|
|
504
|
+
f.failure(exception)
|
|
505
|
+
|
|
506
|
+
def update_metadata(self, metadata):
|
|
507
|
+
"""Update cluster state given a MetadataResponse.
|
|
508
|
+
|
|
509
|
+
Arguments:
|
|
510
|
+
metadata (MetadataResponse): broker response to a metadata request
|
|
511
|
+
|
|
512
|
+
Returns: None
|
|
513
|
+
"""
|
|
514
|
+
if not metadata.brokers:
|
|
515
|
+
log.warning("No broker metadata found in MetadataResponse -- ignoring.")
|
|
516
|
+
return self.failed_update(Errors.MetadataEmptyBrokerList(metadata))
|
|
517
|
+
|
|
518
|
+
_new_brokers = {}
|
|
519
|
+
for broker in metadata.brokers:
|
|
520
|
+
if metadata.API_VERSION == 0:
|
|
521
|
+
node_id, host, port = broker
|
|
522
|
+
rack = None
|
|
523
|
+
else:
|
|
524
|
+
node_id, host, port, rack = broker
|
|
525
|
+
_new_brokers.update({
|
|
526
|
+
node_id: MetadataResponse.MetadataResponseBroker(node_id, host, port, rack)
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
if metadata.API_VERSION == 0:
|
|
530
|
+
_new_controller = None
|
|
531
|
+
else:
|
|
532
|
+
_new_controller = _new_brokers.get(metadata.controller_id)
|
|
533
|
+
|
|
534
|
+
if metadata.API_VERSION < 2:
|
|
535
|
+
_new_cluster_id = None
|
|
536
|
+
else:
|
|
537
|
+
_new_cluster_id = metadata.cluster_id
|
|
538
|
+
|
|
539
|
+
_new_partitions = {}
|
|
540
|
+
_new_broker_partitions = collections.defaultdict(set)
|
|
541
|
+
_new_unauthorized_topics = set()
|
|
542
|
+
_new_internal_topics = set()
|
|
543
|
+
_new_topic_ids = {}
|
|
544
|
+
_new_topic_names_by_id = {}
|
|
545
|
+
_retry_topics = set()
|
|
546
|
+
|
|
547
|
+
# KAFKA-9212: pre-2.4 brokers may emit stale leader_epoch values
|
|
548
|
+
# during partition reassignment (the controller failed to update
|
|
549
|
+
# its own cached epoch before sending UpdateMetadata, so the
|
|
550
|
+
# propagated epoch lags behind the actual leader's). Caching that
|
|
551
|
+
# stale value would loop us on FENCED_LEADER_EPOCH for every
|
|
552
|
+
# subsequent ListOffsets / Fetch / OffsetCommit. The Java client's
|
|
553
|
+
# fix gates on response version >= 9 (Kafka 2.4+, where the
|
|
554
|
+
# controller bug is fixed); we do the same.
|
|
555
|
+
epoch_reliable = metadata.API_VERSION >= 9
|
|
556
|
+
|
|
557
|
+
for t in metadata.topics:
|
|
558
|
+
topic = t.name
|
|
559
|
+
if t.is_internal:
|
|
560
|
+
_new_internal_topics.add(topic)
|
|
561
|
+
error_type = Errors.for_code(t.error_code)
|
|
562
|
+
new_topic_id = t.topic_id
|
|
563
|
+
recreated = False
|
|
564
|
+
if new_topic_id is not None and topic is not None:
|
|
565
|
+
prior = self._topic_ids.get(topic)
|
|
566
|
+
if prior is not None and prior != new_topic_id:
|
|
567
|
+
log.warning(
|
|
568
|
+
"Topic %s topic_id changed from %s to %s -- likely"
|
|
569
|
+
" recreated; resetting cached leader epochs.",
|
|
570
|
+
topic, prior, new_topic_id)
|
|
571
|
+
recreated = True
|
|
572
|
+
_new_topic_ids[topic] = new_topic_id
|
|
573
|
+
_new_topic_names_by_id[new_topic_id] = topic
|
|
574
|
+
if error_type is Errors.NoError:
|
|
575
|
+
_new_partitions[topic] = {}
|
|
576
|
+
for p_data in t.partitions:
|
|
577
|
+
partition = p_data.partition_index
|
|
578
|
+
if not epoch_reliable or recreated:
|
|
579
|
+
p_data.leader_epoch = -1
|
|
580
|
+
_new_partitions[topic][partition] = p_data
|
|
581
|
+
if p_data.leader_id != -1:
|
|
582
|
+
_new_broker_partitions[p_data.leader_id].add(
|
|
583
|
+
TopicPartition(topic, partition))
|
|
584
|
+
|
|
585
|
+
# Only log errors for topics we are specifically tracking
|
|
586
|
+
elif topic in self._topics:
|
|
587
|
+
if issubclass(error_type, Errors.RetriableError):
|
|
588
|
+
_retry_topics.add(topic)
|
|
589
|
+
if error_type is Errors.LeaderNotAvailableError:
|
|
590
|
+
log.warning("Topic %s is not available during auto-create"
|
|
591
|
+
" initialization", topic)
|
|
592
|
+
elif error_type is Errors.UnknownTopicOrPartitionError:
|
|
593
|
+
log.error("Topic %s not found in cluster metadata", topic)
|
|
594
|
+
elif error_type is Errors.TopicAuthorizationFailedError:
|
|
595
|
+
log.error("Topic %s is not authorized for this client", topic)
|
|
596
|
+
_new_unauthorized_topics.add(topic)
|
|
597
|
+
elif error_type is Errors.InvalidTopicError:
|
|
598
|
+
log.error("'%s' is not a valid topic name", topic)
|
|
599
|
+
if topic in self._topics:
|
|
600
|
+
self._topics.remove(topic)
|
|
601
|
+
else:
|
|
602
|
+
log.error("Error fetching metadata for topic %s: %s",
|
|
603
|
+
topic, error_type)
|
|
604
|
+
|
|
605
|
+
with self._lock:
|
|
606
|
+
self._brokers = _new_brokers
|
|
607
|
+
self.controller = _new_controller
|
|
608
|
+
self.cluster_id = _new_cluster_id
|
|
609
|
+
self._partitions = _new_partitions
|
|
610
|
+
self._broker_partitions = _new_broker_partitions
|
|
611
|
+
self.unauthorized_topics = _new_unauthorized_topics
|
|
612
|
+
self.internal_topics = _new_internal_topics
|
|
613
|
+
# Pre-v10 responses don't carry topic_id, so the wholesale swap
|
|
614
|
+
# would clobber known ids during a rolling downgrade (or any
|
|
615
|
+
# cross-broker version skew). Only replace the index when the
|
|
616
|
+
# response actually had a chance to populate it.
|
|
617
|
+
if metadata.API_VERSION >= 10:
|
|
618
|
+
self._topic_ids = _new_topic_ids
|
|
619
|
+
self._topic_names_by_id = _new_topic_names_by_id
|
|
620
|
+
self._need_update = len(_retry_topics) > 0
|
|
621
|
+
f = None
|
|
622
|
+
if self._future:
|
|
623
|
+
f = self._future
|
|
624
|
+
self._future = None
|
|
625
|
+
|
|
626
|
+
now = time.monotonic() * 1000
|
|
627
|
+
self._last_refresh_ms = now
|
|
628
|
+
self._last_successful_refresh_ms = now
|
|
629
|
+
|
|
630
|
+
if f:
|
|
631
|
+
# In the common case where we ask for a single topic and get back an
|
|
632
|
+
# error, we should fail the future
|
|
633
|
+
if len(metadata.topics) == 1 and metadata.topics[0][0] != Errors.NoError.errno:
|
|
634
|
+
error_code, topic = metadata.topics[0][:2]
|
|
635
|
+
error = Errors.for_code(error_code)(topic)
|
|
636
|
+
f.failure(error)
|
|
637
|
+
else:
|
|
638
|
+
f.success(self)
|
|
639
|
+
|
|
640
|
+
log.info("Updated metadata: %s", self)
|
|
641
|
+
|
|
642
|
+
for listener in self._listeners:
|
|
643
|
+
listener(self)
|
|
644
|
+
|
|
645
|
+
def add_listener(self, listener):
|
|
646
|
+
"""Add a callback function to be called on each metadata update"""
|
|
647
|
+
self._listeners.add(listener)
|
|
648
|
+
|
|
649
|
+
def remove_listener(self, listener):
|
|
650
|
+
"""Remove a previously added listener callback."""
|
|
651
|
+
try:
|
|
652
|
+
self._listeners.remove(listener)
|
|
653
|
+
except KeyError:
|
|
654
|
+
pass
|
|
655
|
+
|
|
656
|
+
def add_coordinator(self, response, key_type, key, synthesize_node_id=True):
|
|
657
|
+
"""Update with metadata for a group or txn coordinator
|
|
658
|
+
|
|
659
|
+
Arguments:
|
|
660
|
+
response (FindCoordinatorResponse): broker response
|
|
661
|
+
key_type (CoordinatorType): GROUP / TRANSACTION / SHARE
|
|
662
|
+
key (str): consumer_group or transactional_id
|
|
663
|
+
synthesize_node_id (bool): If True synthesizes a unique
|
|
664
|
+
node_id to generate a dedicated network connection for
|
|
665
|
+
coordinator requests. Default: True.
|
|
666
|
+
|
|
667
|
+
Returns:
|
|
668
|
+
string: coordinator node_id.
|
|
669
|
+
|
|
670
|
+
Raises:
|
|
671
|
+
BrokerResponseError: if ``response.error_code`` is non-zero.
|
|
672
|
+
"""
|
|
673
|
+
key_type = CoordinatorType.build_from(key_type)
|
|
674
|
+
log.debug("Updating coordinator for %s/%s: %s", key_type.name, key, response)
|
|
675
|
+
error_type = Errors.for_code(response.error_code)
|
|
676
|
+
if error_type is not Errors.NoError:
|
|
677
|
+
raise error_type(
|
|
678
|
+
"FindCoordinatorResponse error for %s/%s: %s"
|
|
679
|
+
% (key_type.name, key, getattr(response, 'error_message', '')))
|
|
680
|
+
|
|
681
|
+
# Use a coordinator-specific node id so that requests
|
|
682
|
+
# get a dedicated connection
|
|
683
|
+
if synthesize_node_id:
|
|
684
|
+
node_id = 'coordinator-{}'.format(response.node_id)
|
|
685
|
+
else:
|
|
686
|
+
node_id = response.node_id
|
|
687
|
+
coordinator = MetadataResponse.MetadataResponseBroker(
|
|
688
|
+
node_id,
|
|
689
|
+
response.host,
|
|
690
|
+
response.port,
|
|
691
|
+
None)
|
|
692
|
+
|
|
693
|
+
log.info("Coordinator for %s/%s is %s", key_type.name, key, coordinator)
|
|
694
|
+
self._coordinator_brokers[node_id] = coordinator
|
|
695
|
+
self._coordinators[(key_type, key)] = node_id
|
|
696
|
+
return node_id
|
|
697
|
+
|
|
698
|
+
def __str__(self):
|
|
699
|
+
return 'ClusterMetadata(brokers: %d, topics: %d, coordinators: %d)' % \
|
|
700
|
+
(len(self._brokers), len(self._partitions), len(self._coordinators))
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
def collect_hosts(hosts, randomize=True):
|
|
704
|
+
"""
|
|
705
|
+
Processes a list (or comma-separated string) of hosts strings (host:port)
|
|
706
|
+
and returns a list of (host, port, family) tuples.
|
|
707
|
+
Optionally randomizes the returned list.
|
|
708
|
+
"""
|
|
709
|
+
|
|
710
|
+
if isinstance(hosts, str):
|
|
711
|
+
hosts = hosts.strip().split(',')
|
|
712
|
+
|
|
713
|
+
result = []
|
|
714
|
+
for host_port in hosts:
|
|
715
|
+
# ignore leading SECURITY_PROTOCOL:// to mimic java client
|
|
716
|
+
host_port = re.sub('^.*://', '', host_port)
|
|
717
|
+
host, port, afi = get_ip_port_afi(host_port)
|
|
718
|
+
result.append((host, port, afi))
|
|
719
|
+
|
|
720
|
+
if randomize:
|
|
721
|
+
random.shuffle(result)
|
|
722
|
+
return result
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def expand_to_canonical_bootstrap_hosts(hosts):
|
|
726
|
+
"""Expand each bootstrap entry to one entry per canonical FQDN.
|
|
727
|
+
|
|
728
|
+
Mirrors Java's ``client.dns.lookup=resolve_canonical_bootstrap_servers_only``:
|
|
729
|
+
forward-resolve each host, take the ``canonname`` reported by the resolver,
|
|
730
|
+
and emit one bootstrap entry per unique canonical name. Useful for
|
|
731
|
+
Kerberos round-robin DNS deployments where the principal must match each
|
|
732
|
+
individual broker FQDN.
|
|
733
|
+
|
|
734
|
+
If a host fails to resolve, the original entry is preserved verbatim --
|
|
735
|
+
matching Java's best-effort behaviour so bootstrap doesn't fail outright.
|
|
736
|
+
"""
|
|
737
|
+
expanded = []
|
|
738
|
+
for host, port, afi in hosts:
|
|
739
|
+
try:
|
|
740
|
+
addrinfos = socket.getaddrinfo(
|
|
741
|
+
host, port, afi, socket.SOCK_STREAM, 0, socket.AI_CANONNAME)
|
|
742
|
+
except socket.gaierror as exc:
|
|
743
|
+
log.warning('Canonical bootstrap resolution failed for %s:%s: %s; '
|
|
744
|
+
'keeping original entry', host, port, exc)
|
|
745
|
+
expanded.append((host, port, afi))
|
|
746
|
+
continue
|
|
747
|
+
seen = set()
|
|
748
|
+
for family, _socktype, _proto, canonname, _sockaddr in addrinfos:
|
|
749
|
+
name = canonname or host
|
|
750
|
+
if name in seen:
|
|
751
|
+
continue
|
|
752
|
+
seen.add(name)
|
|
753
|
+
expanded.append((name, port, family))
|
|
754
|
+
return expanded
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def _address_family(address):
|
|
758
|
+
"""
|
|
759
|
+
Attempt to determine the family of an address (or hostname)
|
|
760
|
+
|
|
761
|
+
:return: either socket.AF_INET or socket.AF_INET6 or socket.AF_UNSPEC if the address family
|
|
762
|
+
could not be determined
|
|
763
|
+
"""
|
|
764
|
+
if address.startswith('[') and address.endswith(']'):
|
|
765
|
+
return socket.AF_INET6
|
|
766
|
+
for af in (socket.AF_INET, socket.AF_INET6):
|
|
767
|
+
try:
|
|
768
|
+
socket.inet_pton(af, address)
|
|
769
|
+
return af
|
|
770
|
+
except (ValueError, AttributeError, socket.error):
|
|
771
|
+
continue
|
|
772
|
+
return socket.AF_UNSPEC
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
DEFAULT_KAFKA_PORT = 9092
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
def get_ip_port_afi(host_and_port_str):
|
|
779
|
+
"""
|
|
780
|
+
Parse the IP and port from a string in the format of:
|
|
781
|
+
|
|
782
|
+
* host_or_ip <- Can be either IPv4 address literal or hostname/fqdn
|
|
783
|
+
* host_or_ipv4:port <- Can be either IPv4 address literal or hostname/fqdn
|
|
784
|
+
* [host_or_ip] <- IPv6 address literal
|
|
785
|
+
* [host_or_ip]:port. <- IPv6 address literal
|
|
786
|
+
|
|
787
|
+
.. note:: IPv6 address literals with ports *must* be enclosed in brackets
|
|
788
|
+
|
|
789
|
+
.. note:: If the port is not specified, default will be returned.
|
|
790
|
+
|
|
791
|
+
:return: tuple (host, port, afi), afi will be socket.AF_INET or socket.AF_INET6 or socket.AF_UNSPEC
|
|
792
|
+
"""
|
|
793
|
+
host_and_port_str = host_and_port_str.strip()
|
|
794
|
+
if host_and_port_str.startswith('['):
|
|
795
|
+
af = socket.AF_INET6
|
|
796
|
+
host, rest = host_and_port_str[1:].split(']')
|
|
797
|
+
if rest:
|
|
798
|
+
port = int(rest[1:])
|
|
799
|
+
else:
|
|
800
|
+
port = DEFAULT_KAFKA_PORT
|
|
801
|
+
return host, port, af
|
|
802
|
+
else:
|
|
803
|
+
if ':' not in host_and_port_str:
|
|
804
|
+
af = _address_family(host_and_port_str)
|
|
805
|
+
return host_and_port_str, DEFAULT_KAFKA_PORT, af
|
|
806
|
+
else:
|
|
807
|
+
# now we have something with a colon in it and no square brackets. It could be
|
|
808
|
+
# either an IPv6 address literal (e.g., "::1") or an IP:port pair or a host:port pair
|
|
809
|
+
try:
|
|
810
|
+
# if it decodes as an IPv6 address, use that
|
|
811
|
+
socket.inet_pton(socket.AF_INET6, host_and_port_str)
|
|
812
|
+
return host_and_port_str, DEFAULT_KAFKA_PORT, socket.AF_INET6
|
|
813
|
+
except AttributeError:
|
|
814
|
+
log.warning('socket.inet_pton not available on this platform.'
|
|
815
|
+
' consider `pip install win_inet_pton`')
|
|
816
|
+
pass
|
|
817
|
+
except (ValueError, socket.error):
|
|
818
|
+
# it's a host:port pair
|
|
819
|
+
pass
|
|
820
|
+
host, port = host_and_port_str.rsplit(':', 1)
|
|
821
|
+
port = int(port)
|
|
822
|
+
|
|
823
|
+
af = _address_family(host)
|
|
824
|
+
return host, port, af
|