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/net/connection.py
ADDED
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
import collections
|
|
2
|
+
import copy
|
|
3
|
+
import logging
|
|
4
|
+
import random
|
|
5
|
+
import struct
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
import kafka.errors as Errors
|
|
9
|
+
from kafka.future import Future
|
|
10
|
+
from kafka.net.metrics import KafkaConnectionMetrics
|
|
11
|
+
from kafka.net.sasl import get_sasl_mechanism
|
|
12
|
+
from kafka.protocol.metadata import ApiVersionsRequest
|
|
13
|
+
from kafka.protocol.sasl import SaslAuthenticateRequest, SaslHandshakeRequest, SaslBytesRequest
|
|
14
|
+
from kafka.protocol.broker_version_data import BrokerVersionData
|
|
15
|
+
from kafka.protocol.parser import KafkaProtocol
|
|
16
|
+
from kafka.version import __version__
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
log = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class KafkaConnection:
|
|
23
|
+
DEFAULT_CONFIG = {
|
|
24
|
+
'client_id': 'kafka-python-' + __version__,
|
|
25
|
+
'client_software_name': 'kafka-python',
|
|
26
|
+
'client_software_version': __version__,
|
|
27
|
+
'max_in_flight_requests_per_connection': 5,
|
|
28
|
+
'receive_message_max_bytes': 1000000,
|
|
29
|
+
'request_timeout_ms': 30000,
|
|
30
|
+
'security_protocol': 'PLAINTEXT',
|
|
31
|
+
'sasl_mechanism': None,
|
|
32
|
+
'sasl_plain_username': None,
|
|
33
|
+
'sasl_plain_password': None,
|
|
34
|
+
'sasl_kerberos_name': None,
|
|
35
|
+
'sasl_kerberos_service_name': 'kafka',
|
|
36
|
+
'sasl_kerberos_domain_name': None,
|
|
37
|
+
'sasl_oauth_token_provider': None,
|
|
38
|
+
'metrics': None,
|
|
39
|
+
'metric_group_prefix': '',
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
def __init__(self, net, node_id=None, broker_version_data=None, **configs):
|
|
43
|
+
self.config = copy.copy(self.DEFAULT_CONFIG)
|
|
44
|
+
for key in self.config:
|
|
45
|
+
if key in configs:
|
|
46
|
+
self.config[key] = configs[key]
|
|
47
|
+
|
|
48
|
+
self.node_id = node_id
|
|
49
|
+
self.net = net
|
|
50
|
+
self.transport = None
|
|
51
|
+
self.parser = None
|
|
52
|
+
self._request_buffer = collections.deque()
|
|
53
|
+
self.paused = set()
|
|
54
|
+
self.connected = False
|
|
55
|
+
self.initializing = True
|
|
56
|
+
self._init_future = Future()
|
|
57
|
+
self._close_future = Future()
|
|
58
|
+
self.in_flight_requests = collections.deque()
|
|
59
|
+
self.broker_version_data = broker_version_data
|
|
60
|
+
self._api_versions_idx = ApiVersionsRequest.max_version # version of ApiVersionsRequest to try on first connect
|
|
61
|
+
self._throttle_time = 0
|
|
62
|
+
self._reauth = SaslReauthenticator(self)
|
|
63
|
+
if self.config['metrics']:
|
|
64
|
+
self._sensors = KafkaConnectionMetrics(
|
|
65
|
+
self.config['metrics'], self.config['metric_group_prefix'], node_id)
|
|
66
|
+
else:
|
|
67
|
+
self._sensors = None
|
|
68
|
+
self._init_future.add_errback(self.fail_in_flight_requests)
|
|
69
|
+
self._close_future.add_both(self.fail_in_flight_requests)
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def broker_version(self):
|
|
73
|
+
if self.broker_version_data is None:
|
|
74
|
+
return None
|
|
75
|
+
return self.broker_version_data.broker_version
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def closed(self):
|
|
79
|
+
return not self.connected and not self.initializing
|
|
80
|
+
|
|
81
|
+
def __str__(self):
|
|
82
|
+
if self.initializing:
|
|
83
|
+
state = 'initializing'
|
|
84
|
+
elif not self.connected:
|
|
85
|
+
state = 'disconnected'
|
|
86
|
+
elif self.paused:
|
|
87
|
+
state = 'paused'
|
|
88
|
+
else:
|
|
89
|
+
state = 'connected'
|
|
90
|
+
host_port = ' host=[%s]' % self.transport.host_port() if self.transport else ''
|
|
91
|
+
broker_version = self.broker_version if self.broker_version is not None else 'unknown'
|
|
92
|
+
return f'<KafkaConnection node_id={self.node_id}{host_port} broker_version={broker_version} ({state})>'
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def init_future(self):
|
|
96
|
+
return self._init_future
|
|
97
|
+
|
|
98
|
+
def __await__(self):
|
|
99
|
+
yield self.init_future
|
|
100
|
+
return self
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def close_future(self):
|
|
104
|
+
return self._close_future
|
|
105
|
+
|
|
106
|
+
def _timeout_at(self, now=None, timeout_ms=None):
|
|
107
|
+
if now is None:
|
|
108
|
+
now = time.monotonic()
|
|
109
|
+
if timeout_ms is not None:
|
|
110
|
+
return now + timeout_ms / 1000
|
|
111
|
+
else:
|
|
112
|
+
try:
|
|
113
|
+
return now + self._timeout_secs
|
|
114
|
+
except AttributeError:
|
|
115
|
+
self._timeout_secs = self.config['request_timeout_ms'] / 1000
|
|
116
|
+
return now + self._timeout_secs
|
|
117
|
+
|
|
118
|
+
def send_request(self, request, request_timeout_ms=None):
|
|
119
|
+
future = Future()
|
|
120
|
+
timeout_at = self._timeout_at(timeout_ms=request_timeout_ms)
|
|
121
|
+
if self.initializing or self._reauth.is_reauthenticating:
|
|
122
|
+
self._request_buffer.append((request, future, timeout_at))
|
|
123
|
+
return future
|
|
124
|
+
elif self.paused:
|
|
125
|
+
return future.failure(Errors.NodeNotReadyError(f'Node paused: {self.paused}'))
|
|
126
|
+
elif not self.connected:
|
|
127
|
+
return future.failure(Errors.KafkaConnectionError('Node not connected'))
|
|
128
|
+
else:
|
|
129
|
+
self._send_request(request, future=future, timeout_at=timeout_at)
|
|
130
|
+
return future
|
|
131
|
+
|
|
132
|
+
def _send_request(self, request, future=None, timeout_at=None):
|
|
133
|
+
if future is None:
|
|
134
|
+
future = Future()
|
|
135
|
+
if self.closed:
|
|
136
|
+
return future.failure(Errors.KafkaConnectionError('closed'))
|
|
137
|
+
if request.API_VERSION is None:
|
|
138
|
+
try:
|
|
139
|
+
request.API_VERSION = self.broker_version_data.api_version(request)
|
|
140
|
+
except Errors.IncompatibleBrokerVersion as exc:
|
|
141
|
+
future.failure(exc)
|
|
142
|
+
return future
|
|
143
|
+
sent_time = time.monotonic()
|
|
144
|
+
if timeout_at is None:
|
|
145
|
+
timeout_at = self._timeout_at(now=sent_time)
|
|
146
|
+
if timeout_at <= sent_time:
|
|
147
|
+
future.failure(Errors.KafkaTimeoutError())
|
|
148
|
+
return future
|
|
149
|
+
correlation_id = self.parser.send_request(request)
|
|
150
|
+
log.debug('%s Request %d: %s', self, correlation_id, request)
|
|
151
|
+
if request.expect_response():
|
|
152
|
+
# Each in-flight request owns its own timer so heterogeneous
|
|
153
|
+
# per-request timeouts (e.g. JoinGroup with a rebalance-sized
|
|
154
|
+
# deadline interleaved with default-timeout MetadataRequests)
|
|
155
|
+
# don't require monotonic-deadline FIFO ordering.
|
|
156
|
+
timeout_task = self.net.call_at(
|
|
157
|
+
timeout_at,
|
|
158
|
+
lambda: self._request_timed_out(future, sent_time, timeout_at))
|
|
159
|
+
self.in_flight_requests.append(
|
|
160
|
+
(correlation_id, future, sent_time, timeout_at, timeout_task))
|
|
161
|
+
else:
|
|
162
|
+
future.success(None)
|
|
163
|
+
|
|
164
|
+
# Write the current request's bytes before checking max_in_flight.
|
|
165
|
+
# Otherwise with max_in_flight=1, the first request would be added to
|
|
166
|
+
# in_flight_requests (len==1), trip the >= check, pause, and never be
|
|
167
|
+
# written to the transport - hanging forever.
|
|
168
|
+
if not self.paused:
|
|
169
|
+
self.transport.write(self.parser.send_bytes())
|
|
170
|
+
if len(self.in_flight_requests) >= self.config['max_in_flight_requests_per_connection']:
|
|
171
|
+
self.pause('max_in_flight')
|
|
172
|
+
return future
|
|
173
|
+
|
|
174
|
+
def send_buffered(self):
|
|
175
|
+
while self._request_buffer:
|
|
176
|
+
request, future, timeout_at = self._request_buffer.popleft()
|
|
177
|
+
self._send_request(request, future=future, timeout_at=timeout_at)
|
|
178
|
+
|
|
179
|
+
def _request_timed_out(self, future, sent_at, timeout_at):
|
|
180
|
+
# Defensive: a response and its timer can both be dispatched within a
|
|
181
|
+
# single _poll_once iteration; if data_received resolved the future
|
|
182
|
+
# first, skip the connection-close.
|
|
183
|
+
if self.closed or future.is_done:
|
|
184
|
+
return
|
|
185
|
+
timeout_ms = (timeout_at - sent_at) * 1000
|
|
186
|
+
log.warning('%s: Request timed out after %d ms. Closing connection.', self, timeout_ms)
|
|
187
|
+
self.close(Errors.RequestTimedOutError('Request timed out after %d ms' % timeout_ms))
|
|
188
|
+
|
|
189
|
+
def data_received(self, data):
|
|
190
|
+
""" Called when some data is received."""
|
|
191
|
+
if self.closed:
|
|
192
|
+
log.debug('%s: Ignoring %d bytes received by closed connection', self, len(data))
|
|
193
|
+
return
|
|
194
|
+
responses = self.parser.receive_bytes(data)
|
|
195
|
+
|
|
196
|
+
# augment responses w/ correlation_id, future, and timestamp
|
|
197
|
+
for i, (resp_correlation_id, response) in enumerate(responses):
|
|
198
|
+
try:
|
|
199
|
+
(req_correlation_id, future, sent_time, _timeout_at, timeout_task) = self.in_flight_requests.popleft()
|
|
200
|
+
except IndexError:
|
|
201
|
+
return self.close(Errors.KafkaConnectionError('Received response with no in-flight-requests!'))
|
|
202
|
+
|
|
203
|
+
if req_correlation_id != resp_correlation_id:
|
|
204
|
+
return self.close(Errors.KafkaConnectionError('Received unrecognized correlation id'))
|
|
205
|
+
|
|
206
|
+
self.net.unschedule(timeout_task)
|
|
207
|
+
latency_ms = (time.monotonic() - sent_time) * 1000
|
|
208
|
+
if self._sensors:
|
|
209
|
+
self._sensors.request_time.record(latency_ms)
|
|
210
|
+
|
|
211
|
+
log.debug('%s: Response %d (%s ms): %s', self, resp_correlation_id, latency_ms, response)
|
|
212
|
+
self._maybe_throttle(response)
|
|
213
|
+
future.success(response)
|
|
214
|
+
if 'max_in_flight' in self.paused and len(self.in_flight_requests) < self.config['max_in_flight_requests_per_connection']:
|
|
215
|
+
self.unpause('max_in_flight')
|
|
216
|
+
self._reauth.on_response_processed()
|
|
217
|
+
|
|
218
|
+
def eof_received(self):
|
|
219
|
+
""" Called when the other end calls write_eof() or equivalent.
|
|
220
|
+
|
|
221
|
+
If this returns a false value (including None), the transport
|
|
222
|
+
will close itself. If it returns a true value, closing the
|
|
223
|
+
transport is up to the protocol.
|
|
224
|
+
"""
|
|
225
|
+
return False
|
|
226
|
+
|
|
227
|
+
def connection_lost(self, exc):
|
|
228
|
+
""" Called when the connection is lost or closed.
|
|
229
|
+
|
|
230
|
+
The argument is an exception object or None (the latter
|
|
231
|
+
meaning a regular EOF is received or the connection was
|
|
232
|
+
aborted or closed).
|
|
233
|
+
"""
|
|
234
|
+
self.connected = self.initializing = False
|
|
235
|
+
self.transport = None
|
|
236
|
+
self._reauth.cancel()
|
|
237
|
+
error = exc or Errors.KafkaConnectionError()
|
|
238
|
+
if not self._init_future.is_done:
|
|
239
|
+
self._init_future.failure(error)
|
|
240
|
+
if not self._close_future.is_done:
|
|
241
|
+
if exc is None:
|
|
242
|
+
self._close_future.success(None)
|
|
243
|
+
else:
|
|
244
|
+
self._close_future.failure(exc)
|
|
245
|
+
|
|
246
|
+
def fail_in_flight_requests(self, error):
|
|
247
|
+
if not self.closed:
|
|
248
|
+
raise RuntimeError('Connection must be closed to fail in flight requests')
|
|
249
|
+
error = error or Errors.Cancelled()
|
|
250
|
+
while self._request_buffer:
|
|
251
|
+
_, future, _ = self._request_buffer.popleft()
|
|
252
|
+
future.failure(error)
|
|
253
|
+
while self.in_flight_requests:
|
|
254
|
+
_, future, _, _, timeout_task = self.in_flight_requests.popleft()
|
|
255
|
+
self.net.unschedule(timeout_task)
|
|
256
|
+
future.failure(error)
|
|
257
|
+
|
|
258
|
+
def connection_made(self, transport):
|
|
259
|
+
""" Called when a connection is made.
|
|
260
|
+
|
|
261
|
+
The argument is the transport representing the pipe connection.
|
|
262
|
+
To receive data, wait for data_received() calls.
|
|
263
|
+
When the connection is closed, connection_lost() is called.
|
|
264
|
+
"""
|
|
265
|
+
self.transport = transport
|
|
266
|
+
if self.transport.get_protocol() != self:
|
|
267
|
+
self.transport.set_protocol(self)
|
|
268
|
+
self.initializing = True
|
|
269
|
+
self.transport.resume_reading()
|
|
270
|
+
try:
|
|
271
|
+
log_prefix = 'node=%s[%s:%s]' % (self.node_id, *self.transport.getPeer()[0:2])
|
|
272
|
+
except Exception:
|
|
273
|
+
log.exception('Failed to build connection log_prefix')
|
|
274
|
+
log_prefix = ''
|
|
275
|
+
self.parser = KafkaProtocol(
|
|
276
|
+
client_id=self.config['client_id'],
|
|
277
|
+
receive_message_max_bytes=self.config['receive_message_max_bytes'],
|
|
278
|
+
ident=log_prefix)
|
|
279
|
+
|
|
280
|
+
def pause(self, v):
|
|
281
|
+
self.paused.add(v)
|
|
282
|
+
|
|
283
|
+
def unpause(self, v):
|
|
284
|
+
try:
|
|
285
|
+
self.paused.remove(v)
|
|
286
|
+
except KeyError:
|
|
287
|
+
pass
|
|
288
|
+
else:
|
|
289
|
+
if not self.paused and self.parser and self.transport:
|
|
290
|
+
to_send = self.parser.send_bytes()
|
|
291
|
+
if to_send:
|
|
292
|
+
self.transport.write(to_send)
|
|
293
|
+
|
|
294
|
+
def pause_writing(self):
|
|
295
|
+
""" Called when the transport's buffer goes over the high-water mark.
|
|
296
|
+
|
|
297
|
+
Pause and resume calls are paired -- pause_writing() is called
|
|
298
|
+
once when the buffer goes strictly over the high-water mark
|
|
299
|
+
(even if subsequent writes increases the buffer size even
|
|
300
|
+
more), and eventually resume_writing() is called once when the
|
|
301
|
+
buffer size reaches the low-water mark.
|
|
302
|
+
|
|
303
|
+
Note that if the buffer size equals the high-water mark,
|
|
304
|
+
pause_writing() is not called -- it must go strictly over.
|
|
305
|
+
Conversely, resume_writing() is called when the buffer size is
|
|
306
|
+
equal or lower than the low-water mark. These end conditions
|
|
307
|
+
are important to ensure that things go as expected when either
|
|
308
|
+
mark is zero.
|
|
309
|
+
|
|
310
|
+
NOTE: This is the only Protocol callback that is not called
|
|
311
|
+
through EventLoop.call_soon() -- if it were, it would have no
|
|
312
|
+
effect when it's most needed (when the app keeps writing
|
|
313
|
+
without yielding until pause_writing() is called).
|
|
314
|
+
"""
|
|
315
|
+
self.pause('buffer')
|
|
316
|
+
|
|
317
|
+
def resume_writing(self):
|
|
318
|
+
""" Called when the transport's buffer drains below the low-water mark."""
|
|
319
|
+
self.unpause('buffer')
|
|
320
|
+
|
|
321
|
+
def close(self, error=None):
|
|
322
|
+
if error is None and not self._init_future.is_done:
|
|
323
|
+
error = Errors.KafkaConnectionError()
|
|
324
|
+
if not self.transport:
|
|
325
|
+
self.connection_lost(error)
|
|
326
|
+
return
|
|
327
|
+
if error:
|
|
328
|
+
self.transport.abort(error)
|
|
329
|
+
else:
|
|
330
|
+
self.transport.close()
|
|
331
|
+
|
|
332
|
+
def _maybe_throttle(self, response):
|
|
333
|
+
throttle_time_ms = getattr(response, 'throttle_time_ms', 0)
|
|
334
|
+
if self._sensors:
|
|
335
|
+
self._sensors.throttle_time.record(throttle_time_ms)
|
|
336
|
+
if not throttle_time_ms:
|
|
337
|
+
return
|
|
338
|
+
# Client side throttling enabled in v2.0 brokers
|
|
339
|
+
# prior to that throttling (if present) was managed broker-side
|
|
340
|
+
if self.broker_version is not None and self.broker_version >= (2, 0):
|
|
341
|
+
throttle_time = time.monotonic() + throttle_time_ms / 1000
|
|
342
|
+
if throttle_time > self._throttle_time:
|
|
343
|
+
self._throttle_time = throttle_time
|
|
344
|
+
self.net.call_at(throttle_time, self._maybe_unthrottle)
|
|
345
|
+
self.pause('throttle')
|
|
346
|
+
log.warning("%s: %s throttled by broker (%d ms)", self,
|
|
347
|
+
response.__class__.__name__, throttle_time_ms)
|
|
348
|
+
|
|
349
|
+
def _maybe_unthrottle(self):
|
|
350
|
+
if time.monotonic() >= self._throttle_time:
|
|
351
|
+
self._throttle_time = 0
|
|
352
|
+
self.unpause('throttle')
|
|
353
|
+
|
|
354
|
+
async def initialize(self, timeout_at=None):
|
|
355
|
+
if timeout_at is None:
|
|
356
|
+
timeout_at = self._timeout_at()
|
|
357
|
+
try:
|
|
358
|
+
await self._get_api_versions(timeout_at)
|
|
359
|
+
if self.sasl_enabled:
|
|
360
|
+
await self._sasl_authenticate(timeout_at)
|
|
361
|
+
except Exception as error:
|
|
362
|
+
self.close(error)
|
|
363
|
+
else:
|
|
364
|
+
self._init_complete()
|
|
365
|
+
|
|
366
|
+
async def _get_api_versions(self, timeout_at=None):
|
|
367
|
+
if timeout_at is None:
|
|
368
|
+
timeout_at = self._timeout_at()
|
|
369
|
+
if self.broker_version_data is not None:
|
|
370
|
+
try:
|
|
371
|
+
self._api_versions_idx = self.broker_version_data.api_version(ApiVersionsRequest)
|
|
372
|
+
except Errors.IncompatibleBrokerVersion:
|
|
373
|
+
log.debug('%s: Using pre-configured api_version %s for ApiVersions', self, self.broker_version)
|
|
374
|
+
return
|
|
375
|
+
|
|
376
|
+
while timeout_at > time.monotonic():
|
|
377
|
+
version = self._api_versions_idx
|
|
378
|
+
request = ApiVersionsRequest(
|
|
379
|
+
version=version,
|
|
380
|
+
client_software_name=self.config['client_software_name'],
|
|
381
|
+
client_software_version=self.config['client_software_version'],
|
|
382
|
+
)
|
|
383
|
+
response = await self._send_request(request, timeout_at=timeout_at)
|
|
384
|
+
error_type = Errors.for_code(response.error_code)
|
|
385
|
+
if error_type is Errors.NoError:
|
|
386
|
+
break
|
|
387
|
+
elif error_type is Errors.UnsupportedVersionError:
|
|
388
|
+
for api_version in response.api_keys:
|
|
389
|
+
if api_version.api_key == response.API_KEY:
|
|
390
|
+
self._api_versions_idx = min(self._api_versions_idx, api_version.max_version)
|
|
391
|
+
break
|
|
392
|
+
else:
|
|
393
|
+
self._api_versions_idx = 0
|
|
394
|
+
continue
|
|
395
|
+
else:
|
|
396
|
+
raise error_type()
|
|
397
|
+
else:
|
|
398
|
+
raise Errors.KafkaTimeoutError('Timeout during ApiVersions check')
|
|
399
|
+
|
|
400
|
+
api_versions = {api_version.api_key: (api_version.min_version, api_version.max_version)
|
|
401
|
+
for api_version in response.api_keys}
|
|
402
|
+
bvd = BrokerVersionData(api_versions=api_versions)
|
|
403
|
+
log.info('%s: Broker version identified as %s', self, '.'.join(map(str, bvd.broker_version)))
|
|
404
|
+
if self.broker_version_data is None or self.broker_version_data > bvd:
|
|
405
|
+
self.broker_version_data = bvd
|
|
406
|
+
else:
|
|
407
|
+
log.info('%s: Clamping client to user-supplied broker version %s', self, '.'.join(map(str, self.broker_version)))
|
|
408
|
+
|
|
409
|
+
@property
|
|
410
|
+
def sasl_enabled(self):
|
|
411
|
+
return self.config['security_protocol'] in ('SASL_PLAINTEXT', 'SASL_SSL')
|
|
412
|
+
|
|
413
|
+
async def _sasl_authenticate(self, timeout_at=None):
|
|
414
|
+
if timeout_at is None:
|
|
415
|
+
timeout_at = self._timeout_at()
|
|
416
|
+
# Step 1: SaslHandshake to negotiate mechanism
|
|
417
|
+
request = SaslHandshakeRequest(
|
|
418
|
+
mechanism=self.config['sasl_mechanism'],
|
|
419
|
+
max_version=1)
|
|
420
|
+
response = await self._send_request(request, timeout_at=timeout_at)
|
|
421
|
+
error_type = Errors.for_code(response.error_code)
|
|
422
|
+
if error_type is not Errors.NoError:
|
|
423
|
+
log.error('%s: SaslHandshake failed: %s', self, error_type.__name__)
|
|
424
|
+
raise error_type()
|
|
425
|
+
|
|
426
|
+
if self.config['sasl_mechanism'] not in response.mechanisms:
|
|
427
|
+
raise Errors.UnsupportedSaslMechanismError(
|
|
428
|
+
'Kafka broker does not support %s sasl mechanism. Enabled mechanisms: %s'
|
|
429
|
+
% (self.config['sasl_mechanism'], response.mechanisms))
|
|
430
|
+
|
|
431
|
+
# Step 2: SASL authentication exchange
|
|
432
|
+
version = response.API_VERSION
|
|
433
|
+
# Prefer the configured hostname (stored on the transport) so that
|
|
434
|
+
# mechanisms like GSSAPI construct service principals against the
|
|
435
|
+
# user-supplied name, not whichever IP getaddrinfo handed us.
|
|
436
|
+
sasl_host = self.transport.host if self.transport.host else self.transport.getPeer()[0]
|
|
437
|
+
mechanism = get_sasl_mechanism(self.config['sasl_mechanism'])(
|
|
438
|
+
host=sasl_host, **self.config)
|
|
439
|
+
|
|
440
|
+
auth_response = None
|
|
441
|
+
while not mechanism.is_done() and timeout_at > time.monotonic():
|
|
442
|
+
token = mechanism.auth_bytes()
|
|
443
|
+
if version == 1:
|
|
444
|
+
auth_request = SaslAuthenticateRequest(token)
|
|
445
|
+
else:
|
|
446
|
+
auth_request = SaslBytesRequest(token)
|
|
447
|
+
auth_response = await self._send_request(auth_request, timeout_at=timeout_at)
|
|
448
|
+
error_type = Errors.for_code(auth_response.error_code)
|
|
449
|
+
if error_type is not Errors.NoError:
|
|
450
|
+
raise Errors.SaslAuthenticationFailedError(
|
|
451
|
+
'%s: %s' % (error_type.__name__, auth_response.error_message))
|
|
452
|
+
|
|
453
|
+
# GSSAPI does not get a final recv in v0 unframed mode
|
|
454
|
+
if version == 0 and mechanism.is_done():
|
|
455
|
+
break
|
|
456
|
+
mechanism.receive(auth_response.auth_bytes)
|
|
457
|
+
|
|
458
|
+
if time.monotonic() > timeout_at:
|
|
459
|
+
raise Errors.KafkaTimeoutError('SASL Authentication timed out')
|
|
460
|
+
elif not mechanism.is_authenticated():
|
|
461
|
+
raise Errors.SaslAuthenticationFailedError(
|
|
462
|
+
'Failed to authenticate via SASL %s' % self.config['sasl_mechanism'])
|
|
463
|
+
|
|
464
|
+
# KIP-368: SessionLifetimeMs is only present on SaslAuthenticateResponse v1+.
|
|
465
|
+
if version == 1:
|
|
466
|
+
self._reauth.session_updated(auth_response.session_lifetime_ms)
|
|
467
|
+
log.info('%s: %s', self, mechanism.auth_details())
|
|
468
|
+
|
|
469
|
+
def _init_complete(self):
|
|
470
|
+
if self.initializing:
|
|
471
|
+
self.initializing = False
|
|
472
|
+
self.connected = True
|
|
473
|
+
self.send_buffered()
|
|
474
|
+
self._init_future.success(True)
|
|
475
|
+
self._reauth.schedule()
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
class SaslReauthenticator:
|
|
479
|
+
"""KIP-368 SASL re-authentication state and scheduling for a single
|
|
480
|
+
KafkaConnection. Owns the per-connection re-auth lifecycle so the
|
|
481
|
+
connection doesn't have to carry the related attributes and coroutines
|
|
482
|
+
inline. The connection plugs this in at five points:
|
|
483
|
+
|
|
484
|
+
- after each successful SASL auth -> session_updated()
|
|
485
|
+
- after init completes -> schedule()
|
|
486
|
+
- when send_request needs to gate the public API -> is_reauthenticating
|
|
487
|
+
- on every response popped from in_flight_requests -> on_response_processed()
|
|
488
|
+
- on connection_lost -> cancel()
|
|
489
|
+
"""
|
|
490
|
+
|
|
491
|
+
def __init__(self, conn):
|
|
492
|
+
self._conn = conn
|
|
493
|
+
self.session_lifetime_ms = 0
|
|
494
|
+
self.authenticated_at = None
|
|
495
|
+
self._task = None
|
|
496
|
+
self._reauthenticating = False
|
|
497
|
+
self._drain_future = None
|
|
498
|
+
|
|
499
|
+
@property
|
|
500
|
+
def is_reauthenticating(self):
|
|
501
|
+
return self._reauthenticating
|
|
502
|
+
|
|
503
|
+
@property
|
|
504
|
+
def task(self):
|
|
505
|
+
"""The scheduled re-auth task, or None. Exposed for tests/observability."""
|
|
506
|
+
return self._task
|
|
507
|
+
|
|
508
|
+
def session_updated(self, session_lifetime_ms):
|
|
509
|
+
"""Capture broker-advertised session lifetime after each successful
|
|
510
|
+
auth round (initial and subsequent re-auths). Clamp negative values to 0,
|
|
511
|
+
and require minimum non-zero lifetime of 1sec (1000)."""
|
|
512
|
+
self.session_lifetime_ms = session_lifetime_ms or 0
|
|
513
|
+
if self.session_lifetime_ms < 0:
|
|
514
|
+
self.session_lifetime_ms = 0
|
|
515
|
+
elif 0 < self.session_lifetime_ms <= 1000:
|
|
516
|
+
self.session_lifetime_ms = 1000
|
|
517
|
+
self.authenticated_at = time.monotonic()
|
|
518
|
+
|
|
519
|
+
def schedule(self):
|
|
520
|
+
"""Schedule the next re-auth before the lifetime elapses. Jittered to
|
|
521
|
+
85-95% of the lifetime to avoid synchronised re-auth storms across
|
|
522
|
+
many connections (Apache Java semantics). No-op when SASL is disabled
|
|
523
|
+
or the broker advertised lifetime=0.
|
|
524
|
+
"""
|
|
525
|
+
if not self._conn.sasl_enabled or not self.session_lifetime_ms:
|
|
526
|
+
return
|
|
527
|
+
pct = random.uniform(0.85, 0.95)
|
|
528
|
+
delay = (self.session_lifetime_ms * pct) / 1000
|
|
529
|
+
log.debug('%s: Scheduling SASL re-authentication in %.3fs (session_lifetime_ms=%d)',
|
|
530
|
+
self._conn, delay, self.session_lifetime_ms)
|
|
531
|
+
self._task = self._conn.net.call_later(delay, self._run)
|
|
532
|
+
|
|
533
|
+
def cancel(self):
|
|
534
|
+
"""Cancel any pending re-auth and fail the drain awaiter if present.
|
|
535
|
+
Called from KafkaConnection.connection_lost."""
|
|
536
|
+
if self._task is not None:
|
|
537
|
+
try:
|
|
538
|
+
self._conn.net.unschedule(self._task)
|
|
539
|
+
except (ValueError, KeyError):
|
|
540
|
+
pass
|
|
541
|
+
self._task = None
|
|
542
|
+
if self._drain_future is not None and not self._drain_future.is_done:
|
|
543
|
+
self._drain_future.failure(Errors.KafkaConnectionError())
|
|
544
|
+
self._drain_future = None
|
|
545
|
+
self._reauthenticating = False
|
|
546
|
+
|
|
547
|
+
def on_response_processed(self):
|
|
548
|
+
"""Wake the drain awaiter once in_flight_requests clears during reauth.
|
|
549
|
+
Called from KafkaConnection.data_received after each pop."""
|
|
550
|
+
if (self._reauthenticating
|
|
551
|
+
and self._drain_future is not None
|
|
552
|
+
and not self._conn.in_flight_requests
|
|
553
|
+
and not self._drain_future.is_done):
|
|
554
|
+
self._drain_future.success(None)
|
|
555
|
+
|
|
556
|
+
async def _run(self):
|
|
557
|
+
self._task = None
|
|
558
|
+
if self._conn.closed:
|
|
559
|
+
return
|
|
560
|
+
try:
|
|
561
|
+
await self._do_reauth()
|
|
562
|
+
except BaseException as exc: # pylint: disable=W0718
|
|
563
|
+
# Re-auth failure is transient (KIP-368: not cached like initial
|
|
564
|
+
# auth failure); close the connection so the manager reconnects on
|
|
565
|
+
# next demand.
|
|
566
|
+
log.warning('%s: SASL re-authentication failed: %s', self._conn, exc)
|
|
567
|
+
err = exc if isinstance(exc, Exception) else Errors.SaslAuthenticationFailedError(str(exc))
|
|
568
|
+
self._conn.close(err)
|
|
569
|
+
|
|
570
|
+
async def _do_reauth(self):
|
|
571
|
+
self._reauthenticating = True
|
|
572
|
+
try:
|
|
573
|
+
# Drain in-flight so the SaslHandshake/Authenticate frames are the
|
|
574
|
+
# next bytes on the wire (Apache Java does the same; avoids
|
|
575
|
+
# reasoning about FIFO interleaving with the broker's reauth
|
|
576
|
+
# validation).
|
|
577
|
+
while self._conn.in_flight_requests and not self._conn.closed:
|
|
578
|
+
self._drain_future = Future()
|
|
579
|
+
if not self._conn.in_flight_requests:
|
|
580
|
+
break
|
|
581
|
+
await self._drain_future
|
|
582
|
+
self._drain_future = None
|
|
583
|
+
if self._conn.closed:
|
|
584
|
+
return
|
|
585
|
+
log.debug('%s: Beginning SASL re-authentication', self._conn)
|
|
586
|
+
await self._conn._sasl_authenticate() # pylint: disable=W0212
|
|
587
|
+
finally:
|
|
588
|
+
self._reauthenticating = False
|
|
589
|
+
self._drain_future = None
|
|
590
|
+
if self._conn.closed:
|
|
591
|
+
return
|
|
592
|
+
self._conn.send_buffered()
|
|
593
|
+
self.schedule()
|