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
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
"""Hanging-transaction tooling mixin for KafkaAdminClient (KIP-664).
|
|
2
|
+
|
|
3
|
+
Exposes four wire APIs (ListTransactions, DescribeTransactions,
|
|
4
|
+
DescribeProducers, WriteTxnMarkers in admin abort mode) plus the
|
|
5
|
+
``find_hanging_transactions`` convenience that ties them together,
|
|
6
|
+
mirroring the Java tool's ``kafka-transactions.sh --find-hanging``.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections import defaultdict
|
|
11
|
+
from enum import Enum
|
|
12
|
+
import logging
|
|
13
|
+
from typing import TYPE_CHECKING, Dict, List, NamedTuple, Optional, Set
|
|
14
|
+
|
|
15
|
+
import kafka.errors as Errors
|
|
16
|
+
from kafka.protocol.admin.transactions import (
|
|
17
|
+
DescribeProducersRequest, DescribeTransactionsRequest,
|
|
18
|
+
ListTransactionsRequest,
|
|
19
|
+
)
|
|
20
|
+
from kafka.protocol.metadata import CoordinatorType
|
|
21
|
+
from kafka.protocol.producer.transaction import WriteTxnMarkersRequest
|
|
22
|
+
from kafka.structs import TopicPartition
|
|
23
|
+
from kafka.util import EnumHelper
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from kafka.net.manager import KafkaConnectionManager
|
|
27
|
+
|
|
28
|
+
log = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TransactionState(EnumHelper, str, Enum):
|
|
32
|
+
"""Broker-reported transaction states (DescribeTransactions /
|
|
33
|
+
ListTransactions wire values)."""
|
|
34
|
+
EMPTY = 'Empty'
|
|
35
|
+
ONGOING = 'Ongoing'
|
|
36
|
+
PREPARE_COMMIT = 'PrepareCommit'
|
|
37
|
+
PREPARE_ABORT = 'PrepareAbort'
|
|
38
|
+
COMPLETE_COMMIT = 'CompleteCommit'
|
|
39
|
+
COMPLETE_ABORT = 'CompleteAbort'
|
|
40
|
+
DEAD = 'Dead'
|
|
41
|
+
PREPARE_EPOCH_FENCE = 'PrepareEpochFence'
|
|
42
|
+
UNKNOWN = 'Unknown'
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class TransactionListing(NamedTuple):
|
|
46
|
+
"""One row from a ListTransactions response."""
|
|
47
|
+
transactional_id: str
|
|
48
|
+
producer_id: int
|
|
49
|
+
state: TransactionState
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class TransactionDescription(NamedTuple):
|
|
53
|
+
"""One transactional id's state as returned by DescribeTransactions,
|
|
54
|
+
plus the coordinator that owns it."""
|
|
55
|
+
coordinator_id: int
|
|
56
|
+
state: TransactionState
|
|
57
|
+
producer_id: int
|
|
58
|
+
producer_epoch: int
|
|
59
|
+
transaction_timeout_ms: int
|
|
60
|
+
transaction_start_time_ms: int
|
|
61
|
+
topic_partitions: Set[TopicPartition]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ProducerState(NamedTuple):
|
|
65
|
+
"""One ActiveProducer row from DescribeProducers."""
|
|
66
|
+
producer_id: int
|
|
67
|
+
producer_epoch: int
|
|
68
|
+
last_sequence: int
|
|
69
|
+
last_timestamp: int
|
|
70
|
+
coordinator_epoch: int
|
|
71
|
+
current_transaction_start_offset: int
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class PartitionProducerState(NamedTuple):
|
|
75
|
+
active_producers: List[ProducerState]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class AbortTransactionSpec(NamedTuple):
|
|
79
|
+
"""Inputs for ``abort_transaction``. ``coordinator_epoch=-1`` is the
|
|
80
|
+
sentinel used by the Java admin tool to bypass the epoch check; the
|
|
81
|
+
partition leader still validates ``producer_id``/``producer_epoch``
|
|
82
|
+
against current state."""
|
|
83
|
+
topic_partition: TopicPartition
|
|
84
|
+
producer_id: int
|
|
85
|
+
producer_epoch: int
|
|
86
|
+
coordinator_epoch: int = -1
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class TransactionsAdminMixin:
|
|
90
|
+
"""Mixin providing KIP-664 hanging-transaction tooling."""
|
|
91
|
+
_manager: "KafkaConnectionManager"
|
|
92
|
+
config: dict
|
|
93
|
+
|
|
94
|
+
# -- ListTransactions ---------------------------------------------------
|
|
95
|
+
|
|
96
|
+
@staticmethod
|
|
97
|
+
def _list_transactions_request(state_filters=None, producer_id_filters=None,
|
|
98
|
+
duration_filter_ms=None,
|
|
99
|
+
transactional_id_pattern=None):
|
|
100
|
+
kwargs = {'min_version': 0}
|
|
101
|
+
kwargs['state_filters'] = list(state_filters) if state_filters else []
|
|
102
|
+
kwargs['producer_id_filters'] = list(producer_id_filters) if producer_id_filters else []
|
|
103
|
+
if duration_filter_ms is not None:
|
|
104
|
+
kwargs['duration_filter'] = int(duration_filter_ms)
|
|
105
|
+
kwargs['min_version'] = max(kwargs['min_version'], 1)
|
|
106
|
+
if transactional_id_pattern is not None:
|
|
107
|
+
kwargs['transactional_id_pattern'] = transactional_id_pattern
|
|
108
|
+
kwargs['min_version'] = max(kwargs['min_version'], 2)
|
|
109
|
+
return ListTransactionsRequest(**kwargs)
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def _list_transactions_process_response(response):
|
|
113
|
+
error_type = Errors.for_code(response.error_code)
|
|
114
|
+
if error_type is not Errors.NoError:
|
|
115
|
+
raise error_type(
|
|
116
|
+
"ListTransactionsRequest failed with response '{}'.".format(response))
|
|
117
|
+
listings = []
|
|
118
|
+
for txn in response.transaction_states:
|
|
119
|
+
listings.append(TransactionListing(
|
|
120
|
+
transactional_id=txn.transactional_id,
|
|
121
|
+
producer_id=txn.producer_id,
|
|
122
|
+
state=TransactionState(txn.transaction_state),
|
|
123
|
+
))
|
|
124
|
+
return listings
|
|
125
|
+
|
|
126
|
+
async def _async_list_transactions(self, broker_ids=None, producer_id_filters=None,
|
|
127
|
+
state_filters=None, duration_filter_ms=None,
|
|
128
|
+
transactional_id_pattern=None):
|
|
129
|
+
# Semantic version pre-checks: the user-visible flag silently
|
|
130
|
+
# disappears at the wire if we don't enforce it.
|
|
131
|
+
if duration_filter_ms is not None:
|
|
132
|
+
if self._manager.broker_version_data.api_version(ListTransactionsRequest) < 1:
|
|
133
|
+
raise Errors.UnsupportedVersionError(
|
|
134
|
+
'duration_filter_ms requires broker support for '
|
|
135
|
+
'ListTransactions v1+ (Apache Kafka 3.8+).')
|
|
136
|
+
if transactional_id_pattern is not None:
|
|
137
|
+
if self._manager.broker_version_data.api_version(ListTransactionsRequest) < 2:
|
|
138
|
+
raise Errors.UnsupportedVersionError(
|
|
139
|
+
'transactional_id_pattern requires broker support for '
|
|
140
|
+
'ListTransactions v2+ (Apache Kafka 4.1+, KIP-1152).')
|
|
141
|
+
|
|
142
|
+
if broker_ids is None:
|
|
143
|
+
broker_ids = [broker.node_id for broker in self._manager.cluster.brokers()]
|
|
144
|
+
results = {}
|
|
145
|
+
for broker_id in broker_ids:
|
|
146
|
+
request = self._list_transactions_request(
|
|
147
|
+
state_filters=state_filters,
|
|
148
|
+
producer_id_filters=producer_id_filters,
|
|
149
|
+
duration_filter_ms=duration_filter_ms,
|
|
150
|
+
transactional_id_pattern=transactional_id_pattern,
|
|
151
|
+
)
|
|
152
|
+
response = await self._manager.send(request, node_id=broker_id)
|
|
153
|
+
results[broker_id] = self._list_transactions_process_response(response)
|
|
154
|
+
return results
|
|
155
|
+
|
|
156
|
+
def list_transactions(self, broker_ids=None, producer_id_filters=None,
|
|
157
|
+
state_filters=None, duration_filter_ms=None,
|
|
158
|
+
transactional_id_pattern=None):
|
|
159
|
+
"""List active transactions across all brokers (or a subset).
|
|
160
|
+
|
|
161
|
+
Each broker hosts a slice of the ``__transaction_state`` topic,
|
|
162
|
+
so a full listing requires sharding the request to every broker
|
|
163
|
+
and concatenating the results.
|
|
164
|
+
|
|
165
|
+
Keyword Arguments:
|
|
166
|
+
broker_ids ([int], optional): Brokers to query. Default: every
|
|
167
|
+
broker in the cluster metadata.
|
|
168
|
+
producer_id_filters ([int], optional): Only return transactions
|
|
169
|
+
whose ``producer_id`` is in this list.
|
|
170
|
+
state_filters ([str], optional): Only return transactions whose
|
|
171
|
+
broker-reported state matches. Accepts :class:`TransactionState`
|
|
172
|
+
members or their string wire values.
|
|
173
|
+
duration_filter_ms (int, optional): Only return transactions
|
|
174
|
+
running longer than this. Requires broker >= 3.8
|
|
175
|
+
(ListTransactions v1+).
|
|
176
|
+
transactional_id_pattern (str, optional): Only return
|
|
177
|
+
transactions whose transactional id matches this regex.
|
|
178
|
+
Requires broker >= 4.1 (ListTransactions v2+, KIP-1152).
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
dict: A dict mapping broker ``node_id`` to a list of
|
|
182
|
+
:class:`TransactionListing`.
|
|
183
|
+
"""
|
|
184
|
+
return self._manager.run(
|
|
185
|
+
self._async_list_transactions, broker_ids, producer_id_filters,
|
|
186
|
+
state_filters, duration_filter_ms, transactional_id_pattern)
|
|
187
|
+
|
|
188
|
+
# -- DescribeTransactions ----------------------------------------------
|
|
189
|
+
|
|
190
|
+
@staticmethod
|
|
191
|
+
def _describe_transactions_request(transactional_ids):
|
|
192
|
+
return DescribeTransactionsRequest(transactional_ids=list(transactional_ids))
|
|
193
|
+
|
|
194
|
+
@staticmethod
|
|
195
|
+
def _describe_transactions_process_response(response, coordinator_id):
|
|
196
|
+
results = {}
|
|
197
|
+
for txn in response.transaction_states:
|
|
198
|
+
error_type = Errors.for_code(txn.error_code)
|
|
199
|
+
if error_type is not Errors.NoError:
|
|
200
|
+
raise error_type(
|
|
201
|
+
"DescribeTransactionsRequest failed for transactional id '{}'."
|
|
202
|
+
.format(txn.transactional_id))
|
|
203
|
+
topic_partitions = set()
|
|
204
|
+
for topic in txn.topics:
|
|
205
|
+
for partition in topic.partitions:
|
|
206
|
+
topic_partitions.add(TopicPartition(topic.topic, partition))
|
|
207
|
+
results[txn.transactional_id] = TransactionDescription(
|
|
208
|
+
coordinator_id=coordinator_id,
|
|
209
|
+
state=TransactionState(txn.transaction_state),
|
|
210
|
+
producer_id=txn.producer_id,
|
|
211
|
+
producer_epoch=txn.producer_epoch,
|
|
212
|
+
transaction_timeout_ms=txn.transaction_timeout_ms,
|
|
213
|
+
transaction_start_time_ms=txn.transaction_start_time_ms,
|
|
214
|
+
topic_partitions=topic_partitions,
|
|
215
|
+
)
|
|
216
|
+
return results
|
|
217
|
+
|
|
218
|
+
async def _async_describe_transactions(self, transactional_ids):
|
|
219
|
+
transactional_ids = list(transactional_ids)
|
|
220
|
+
if not transactional_ids:
|
|
221
|
+
return {}
|
|
222
|
+
coordinator_ids = await self._find_coordinator_ids(
|
|
223
|
+
transactional_ids, key_type=CoordinatorType.TRANSACTION)
|
|
224
|
+
coordinator_to_txn_ids = defaultdict(list)
|
|
225
|
+
for txn_id, coord_id in coordinator_ids.items():
|
|
226
|
+
coordinator_to_txn_ids[coord_id].append(txn_id)
|
|
227
|
+
results = {}
|
|
228
|
+
for coord_id, txn_ids in coordinator_to_txn_ids.items():
|
|
229
|
+
request = self._describe_transactions_request(txn_ids)
|
|
230
|
+
response = await self._manager.send(request, node_id=coord_id)
|
|
231
|
+
results.update(self._describe_transactions_process_response(response, coord_id))
|
|
232
|
+
return results
|
|
233
|
+
|
|
234
|
+
def describe_transactions(self, transactional_ids):
|
|
235
|
+
"""Describe one or more transactions by transactional id.
|
|
236
|
+
|
|
237
|
+
Each request is routed to the transaction coordinator that owns
|
|
238
|
+
the transactional id (discovered via FindCoordinator with
|
|
239
|
+
``CoordinatorType.TRANSACTION``).
|
|
240
|
+
|
|
241
|
+
Arguments:
|
|
242
|
+
transactional_ids: Iterable of transactional id strings.
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
dict: A dict mapping ``transactional_id`` (str) to
|
|
246
|
+
:class:`TransactionDescription`.
|
|
247
|
+
|
|
248
|
+
Raises:
|
|
249
|
+
TransactionalIdNotFoundError: If a transactional id is unknown
|
|
250
|
+
to its coordinator.
|
|
251
|
+
BrokerResponseError: For any other per-id error.
|
|
252
|
+
"""
|
|
253
|
+
return self._manager.run(self._async_describe_transactions, transactional_ids)
|
|
254
|
+
|
|
255
|
+
# -- DescribeProducers --------------------------------------------------
|
|
256
|
+
|
|
257
|
+
@staticmethod
|
|
258
|
+
def _describe_producers_request(partitions_by_topic):
|
|
259
|
+
Topic = DescribeProducersRequest.TopicRequest
|
|
260
|
+
topics = [
|
|
261
|
+
Topic(name=name, partition_indexes=list(parts))
|
|
262
|
+
for name, parts in partitions_by_topic.items()
|
|
263
|
+
]
|
|
264
|
+
return DescribeProducersRequest(topics=topics)
|
|
265
|
+
|
|
266
|
+
@staticmethod
|
|
267
|
+
def _describe_producers_process_response(response):
|
|
268
|
+
results = {}
|
|
269
|
+
for topic in response.topics:
|
|
270
|
+
for partition in topic.partitions:
|
|
271
|
+
tp = TopicPartition(topic.name, partition.partition_index)
|
|
272
|
+
error_type = Errors.for_code(partition.error_code)
|
|
273
|
+
if error_type is not Errors.NoError:
|
|
274
|
+
raise error_type(
|
|
275
|
+
"DescribeProducersRequest failed for {}: {}"
|
|
276
|
+
.format(tp, partition.error_message))
|
|
277
|
+
producers = [
|
|
278
|
+
ProducerState(
|
|
279
|
+
producer_id=p.producer_id,
|
|
280
|
+
producer_epoch=p.producer_epoch,
|
|
281
|
+
last_sequence=p.last_sequence,
|
|
282
|
+
last_timestamp=p.last_timestamp,
|
|
283
|
+
coordinator_epoch=p.coordinator_epoch,
|
|
284
|
+
current_transaction_start_offset=p.current_txn_start_offset,
|
|
285
|
+
) for p in partition.active_producers
|
|
286
|
+
]
|
|
287
|
+
results[tp] = PartitionProducerState(active_producers=producers)
|
|
288
|
+
return results
|
|
289
|
+
|
|
290
|
+
async def _async_describe_producers(self, partitions, broker_id=None):
|
|
291
|
+
partitions = list(partitions)
|
|
292
|
+
if not partitions:
|
|
293
|
+
return {}
|
|
294
|
+
|
|
295
|
+
if broker_id is not None:
|
|
296
|
+
# Send a single request to the specified replica.
|
|
297
|
+
partitions_by_topic = defaultdict(list)
|
|
298
|
+
for tp in partitions:
|
|
299
|
+
partitions_by_topic[tp.topic].append(tp.partition)
|
|
300
|
+
request = self._describe_producers_request(partitions_by_topic)
|
|
301
|
+
response = await self._manager.send(request, node_id=broker_id)
|
|
302
|
+
return self._describe_producers_process_response(response)
|
|
303
|
+
|
|
304
|
+
# Route per-partition to the current leader. Shares the metadata
|
|
305
|
+
# round-trip helper used by partition-level operations.
|
|
306
|
+
leader2partitions = await self._async_get_leader_for_partitions(partitions)
|
|
307
|
+
results = {}
|
|
308
|
+
for leader, leader_tps in leader2partitions.items():
|
|
309
|
+
partitions_by_topic = defaultdict(list)
|
|
310
|
+
for tp in leader_tps:
|
|
311
|
+
partitions_by_topic[tp.topic].append(tp.partition)
|
|
312
|
+
request = self._describe_producers_request(partitions_by_topic)
|
|
313
|
+
response = await self._manager.send(request, node_id=leader)
|
|
314
|
+
results.update(self._describe_producers_process_response(response))
|
|
315
|
+
return results
|
|
316
|
+
|
|
317
|
+
def describe_producers(self, partitions, broker_id=None):
|
|
318
|
+
"""Describe active producer state on a set of topic partitions.
|
|
319
|
+
|
|
320
|
+
Arguments:
|
|
321
|
+
partitions: Iterable of :class:`~kafka.TopicPartition`.
|
|
322
|
+
|
|
323
|
+
Keyword Arguments:
|
|
324
|
+
broker_id (int, optional): Replica to query. Default: the
|
|
325
|
+
partition leader (discovered from cluster metadata).
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
dict: A dict mapping :class:`~kafka.TopicPartition` to
|
|
329
|
+
:class:`PartitionProducerState`.
|
|
330
|
+
|
|
331
|
+
Raises:
|
|
332
|
+
BrokerResponseError: For any per-partition error (e.g.
|
|
333
|
+
``NotLeaderOrFollowerError`` if the chosen broker is not
|
|
334
|
+
a replica).
|
|
335
|
+
"""
|
|
336
|
+
return self._manager.run(self._async_describe_producers, partitions, broker_id)
|
|
337
|
+
|
|
338
|
+
# -- AbortTransaction (WriteTxnMarkers) --------------------------------
|
|
339
|
+
|
|
340
|
+
@staticmethod
|
|
341
|
+
def _abort_transaction_request(spec):
|
|
342
|
+
Marker = WriteTxnMarkersRequest.WritableTxnMarker
|
|
343
|
+
Topic = Marker.WritableTxnMarkerTopic
|
|
344
|
+
marker = Marker(
|
|
345
|
+
producer_id=spec.producer_id,
|
|
346
|
+
producer_epoch=spec.producer_epoch,
|
|
347
|
+
transaction_result=False, # False -> ABORT
|
|
348
|
+
topics=[Topic(name=spec.topic_partition.topic,
|
|
349
|
+
partition_indexes=[spec.topic_partition.partition])],
|
|
350
|
+
coordinator_epoch=spec.coordinator_epoch,
|
|
351
|
+
)
|
|
352
|
+
return WriteTxnMarkersRequest(markers=[marker])
|
|
353
|
+
|
|
354
|
+
@staticmethod
|
|
355
|
+
def _abort_transaction_process_response(response, spec):
|
|
356
|
+
for result in response.markers:
|
|
357
|
+
for topic in result.topics:
|
|
358
|
+
for partition in topic.partitions:
|
|
359
|
+
error_type = Errors.for_code(partition.error_code)
|
|
360
|
+
if error_type is not Errors.NoError:
|
|
361
|
+
raise error_type(
|
|
362
|
+
"WriteTxnMarkers (abort) failed for {}".format(spec.topic_partition))
|
|
363
|
+
|
|
364
|
+
async def _async_abort_transaction(self, spec):
|
|
365
|
+
leader2partitions = await self._async_get_leader_for_partitions([spec.topic_partition])
|
|
366
|
+
leader = next(iter(leader2partitions))
|
|
367
|
+
request = self._abort_transaction_request(spec)
|
|
368
|
+
response = await self._manager.send(request, node_id=leader)
|
|
369
|
+
self._abort_transaction_process_response(response, spec)
|
|
370
|
+
|
|
371
|
+
def abort_transaction(self, spec):
|
|
372
|
+
"""Administratively abort an open transaction on a partition.
|
|
373
|
+
|
|
374
|
+
Sends a WriteTxnMarkers request (with ``transaction_result=False``)
|
|
375
|
+
to the partition leader. The leader validates ``producer_id`` /
|
|
376
|
+
``producer_epoch`` against current state before writing the
|
|
377
|
+
abort marker. Pass ``coordinator_epoch=-1`` (the default) to
|
|
378
|
+
signal an admin abort that bypasses the coordinator-epoch
|
|
379
|
+
guard, matching the Java AdminClient behaviour.
|
|
380
|
+
|
|
381
|
+
Arguments:
|
|
382
|
+
spec (:class:`AbortTransactionSpec`): Target partition,
|
|
383
|
+
producer id/epoch, and optional coordinator epoch.
|
|
384
|
+
"""
|
|
385
|
+
return self._manager.run(self._async_abort_transaction, spec)
|
|
386
|
+
|
|
387
|
+
# -- find_hanging convenience ------------------------------------------
|
|
388
|
+
|
|
389
|
+
async def _async_find_hanging_transactions(self, broker_ids=None,
|
|
390
|
+
max_transaction_timeout_ms=900000):
|
|
391
|
+
# Padding matches the Java tool: a transaction is only flagged
|
|
392
|
+
# "hanging" if it has been alive longer than the broker-side
|
|
393
|
+
# max-transaction-timeout plus the 5-minute slack the tool uses.
|
|
394
|
+
threshold_ms = max_transaction_timeout_ms + 5 * 60 * 1000
|
|
395
|
+
listings_by_broker = await self._async_list_transactions(broker_ids=broker_ids)
|
|
396
|
+
txn_ids = sorted({t.transactional_id for txns in listings_by_broker.values() for t in txns})
|
|
397
|
+
if not txn_ids:
|
|
398
|
+
return []
|
|
399
|
+
descriptions = await self._async_describe_transactions(txn_ids)
|
|
400
|
+
# Resolve "now" via the latest start-time we observed; we don't
|
|
401
|
+
# have a reliable broker clock otherwise. Fall back to local time
|
|
402
|
+
# for an empty result.
|
|
403
|
+
import time
|
|
404
|
+
now_ms = int(time.time() * 1000)
|
|
405
|
+
hanging = []
|
|
406
|
+
for txn_id, desc in descriptions.items():
|
|
407
|
+
if desc.state in (TransactionState.EMPTY, TransactionState.COMPLETE_COMMIT,
|
|
408
|
+
TransactionState.COMPLETE_ABORT, TransactionState.DEAD):
|
|
409
|
+
continue
|
|
410
|
+
if desc.transaction_start_time_ms < 0:
|
|
411
|
+
continue
|
|
412
|
+
age_ms = now_ms - desc.transaction_start_time_ms
|
|
413
|
+
if age_ms < threshold_ms:
|
|
414
|
+
continue
|
|
415
|
+
hanging.append({
|
|
416
|
+
'transactional_id': txn_id,
|
|
417
|
+
'producer_id': desc.producer_id,
|
|
418
|
+
'producer_epoch': desc.producer_epoch,
|
|
419
|
+
'state': desc.state.value,
|
|
420
|
+
'age_ms': age_ms,
|
|
421
|
+
'coordinator_id': desc.coordinator_id,
|
|
422
|
+
'topic_partitions': sorted(desc.topic_partitions),
|
|
423
|
+
})
|
|
424
|
+
return hanging
|
|
425
|
+
|
|
426
|
+
def find_hanging_transactions(self, broker_ids=None,
|
|
427
|
+
max_transaction_timeout_ms=900000):
|
|
428
|
+
"""Detect transactions whose age exceeds the broker timeout + 5min.
|
|
429
|
+
|
|
430
|
+
Convenience wrapper that runs :meth:`list_transactions` against
|
|
431
|
+
each broker, then :meth:`describe_transactions` to read
|
|
432
|
+
``transaction_start_time_ms``, and filters to transactions in an
|
|
433
|
+
active state whose age exceeds the threshold. Mirrors
|
|
434
|
+
``kafka-transactions.sh --find-hanging``.
|
|
435
|
+
|
|
436
|
+
Keyword Arguments:
|
|
437
|
+
broker_ids ([int], optional): Brokers to query. Default: all.
|
|
438
|
+
max_transaction_timeout_ms (int): Suspected-hang threshold.
|
|
439
|
+
Default: 900000 (15 minutes -- Kafka's default
|
|
440
|
+
``transaction.max.timeout.ms``).
|
|
441
|
+
|
|
442
|
+
Returns:
|
|
443
|
+
list: One dict per suspected hanging transaction with keys
|
|
444
|
+
``transactional_id``, ``producer_id``, ``producer_epoch``,
|
|
445
|
+
``state``, ``age_ms``, ``coordinator_id``,
|
|
446
|
+
``topic_partitions``.
|
|
447
|
+
"""
|
|
448
|
+
return self._manager.run(
|
|
449
|
+
self._async_find_hanging_transactions, broker_ids, max_transaction_timeout_ms)
|
|
450
|
+
|
kafka/admin/_users.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""User management mixin for KafkaAdminClient.
|
|
2
|
+
|
|
3
|
+
Also defines ScramMechanism, UserCredentialDeletion,
|
|
4
|
+
and UserCredentialUpsertion data classes.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from enum import IntEnum
|
|
10
|
+
import hashlib
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
import kafka.errors as Errors
|
|
16
|
+
from kafka.errors import IllegalArgumentError
|
|
17
|
+
from kafka.protocol.admin import (
|
|
18
|
+
AlterUserScramCredentialsRequest,
|
|
19
|
+
DescribeUserScramCredentialsRequest,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from kafka.net.manager import KafkaConnectionManager
|
|
24
|
+
|
|
25
|
+
log = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class UserAdminMixin:
|
|
29
|
+
"""Mixin providing user management methods for KafkaAdminClient."""
|
|
30
|
+
_manager: KafkaConnectionManager
|
|
31
|
+
|
|
32
|
+
async def _async_alter_user_scram_credentials(self, alterations):
|
|
33
|
+
deletions = []
|
|
34
|
+
upsertions = []
|
|
35
|
+
for alt in alterations:
|
|
36
|
+
if isinstance(alt, UserScramCredentialDeletion):
|
|
37
|
+
deletions.append((alt.user, int(alt.mechanism)))
|
|
38
|
+
elif isinstance(alt, UserScramCredentialUpsertion):
|
|
39
|
+
upsertions.append((
|
|
40
|
+
alt.user,
|
|
41
|
+
int(alt.mechanism),
|
|
42
|
+
alt.iterations,
|
|
43
|
+
alt.salt,
|
|
44
|
+
alt.salted_password,
|
|
45
|
+
))
|
|
46
|
+
else:
|
|
47
|
+
raise IllegalArgumentError(
|
|
48
|
+
"alterations must be UserScramCredentialDeletion or "
|
|
49
|
+
"UserScramCredentialUpsertion, got %s" % type(alt).__name__)
|
|
50
|
+
|
|
51
|
+
request = AlterUserScramCredentialsRequest(
|
|
52
|
+
deletions=deletions,
|
|
53
|
+
upsertions=upsertions,
|
|
54
|
+
)
|
|
55
|
+
response = await self._manager.send(request)
|
|
56
|
+
|
|
57
|
+
ret = {}
|
|
58
|
+
for result in response.results:
|
|
59
|
+
ret[result.user] = result.error_message if result.error_code else None
|
|
60
|
+
return ret
|
|
61
|
+
|
|
62
|
+
def alter_user_scram_credentials(self, alterations):
|
|
63
|
+
"""Alter SCRAM credentials for one or more users.
|
|
64
|
+
|
|
65
|
+
Arguments:
|
|
66
|
+
alterations: A list of UserScramCredentialDeletion and/or
|
|
67
|
+
UserScramCredentialUpsertion objects describing the
|
|
68
|
+
credentials to delete and/or insert/update.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
A dict mapping user name -> error message (or None on success).
|
|
72
|
+
"""
|
|
73
|
+
return self._manager.run(self._async_alter_user_scram_credentials, alterations)
|
|
74
|
+
|
|
75
|
+
async def _async_describe_user_scram_credentials(self, users=None):
|
|
76
|
+
if users is None:
|
|
77
|
+
users_field = None
|
|
78
|
+
else:
|
|
79
|
+
users_field = [(user,) for user in users]
|
|
80
|
+
request = DescribeUserScramCredentialsRequest(users=users_field)
|
|
81
|
+
response = await self._manager.send(request)
|
|
82
|
+
|
|
83
|
+
error_type = Errors.for_code(response.error_code)
|
|
84
|
+
if error_type is not Errors.NoError:
|
|
85
|
+
raise error_type(
|
|
86
|
+
"DescribeUserScramCredentialsRequest failed: %s"
|
|
87
|
+
% (response.error_message,))
|
|
88
|
+
|
|
89
|
+
ret = {}
|
|
90
|
+
for result in response.results:
|
|
91
|
+
if result.error_code:
|
|
92
|
+
ret[result.user] = {
|
|
93
|
+
'error': result.error_message,
|
|
94
|
+
'credential_infos': [],
|
|
95
|
+
}
|
|
96
|
+
else:
|
|
97
|
+
ret[result.user] = {
|
|
98
|
+
'error': None,
|
|
99
|
+
'credential_infos': [
|
|
100
|
+
{
|
|
101
|
+
'mechanism': ScramMechanism(ci.mechanism),
|
|
102
|
+
'iterations': ci.iterations,
|
|
103
|
+
}
|
|
104
|
+
for ci in result.credential_infos
|
|
105
|
+
],
|
|
106
|
+
}
|
|
107
|
+
return ret
|
|
108
|
+
|
|
109
|
+
def describe_user_scram_credentials(self, users=None):
|
|
110
|
+
"""Describe SCRAM credentials for one or more users.
|
|
111
|
+
|
|
112
|
+
Arguments:
|
|
113
|
+
users (list of str, optional): User names to describe. If None,
|
|
114
|
+
describe all users with SCRAM credentials.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
A dict mapping user name to a dict with keys
|
|
118
|
+
``'error'`` (None or error message) and ``'credential_infos'``
|
|
119
|
+
(list of {'mechanism': ScramMechanism, 'iterations': int}).
|
|
120
|
+
"""
|
|
121
|
+
return self._manager.run(self._async_describe_user_scram_credentials, users)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class ScramMechanism(IntEnum):
|
|
125
|
+
UNKNOWN = 0
|
|
126
|
+
SCRAM_SHA_256 = 1
|
|
127
|
+
SCRAM_SHA_512 = 2
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def hash_name(self):
|
|
131
|
+
return {
|
|
132
|
+
ScramMechanism.SCRAM_SHA_256: 'sha256',
|
|
133
|
+
ScramMechanism.SCRAM_SHA_512: 'sha512',
|
|
134
|
+
}[self]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class UserScramCredentialDeletion:
|
|
138
|
+
"""Specifies that a SCRAM credential should be deleted.
|
|
139
|
+
|
|
140
|
+
Arguments:
|
|
141
|
+
user (str): The user name.
|
|
142
|
+
mechanism (ScramMechanism or int or str): The SCRAM mechanism to
|
|
143
|
+
delete for this user.
|
|
144
|
+
"""
|
|
145
|
+
def __init__(self, user, mechanism):
|
|
146
|
+
if not isinstance(mechanism, ScramMechanism):
|
|
147
|
+
if isinstance(mechanism, str):
|
|
148
|
+
mechanism = ScramMechanism[mechanism.upper().replace('-', '_')]
|
|
149
|
+
else:
|
|
150
|
+
mechanism = ScramMechanism(mechanism)
|
|
151
|
+
self.user = user
|
|
152
|
+
self.mechanism = mechanism
|
|
153
|
+
|
|
154
|
+
def __repr__(self):
|
|
155
|
+
return f"UserScramCredentialDeletion({self.user}, {self.mechanism.name})"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class UserScramCredentialUpsertion:
|
|
159
|
+
"""Specifies that a SCRAM credential should be inserted or updated.
|
|
160
|
+
|
|
161
|
+
Arguments:
|
|
162
|
+
user (str): The user name.
|
|
163
|
+
mechanism (ScramMechanism or int or str): The SCRAM mechanism.
|
|
164
|
+
password (bytes or str): The plaintext password. The salted
|
|
165
|
+
password sent to the broker is derived via PBKDF2-HMAC using
|
|
166
|
+
the given salt and iteration count.
|
|
167
|
+
|
|
168
|
+
Keyword Arguments:
|
|
169
|
+
iterations (int, optional): PBKDF2 iteration count. Default: 4096.
|
|
170
|
+
salt (bytes, optional): Salt to use. If omitted, a random 24-byte
|
|
171
|
+
salt is generated.
|
|
172
|
+
"""
|
|
173
|
+
DEFAULT_ITERATIONS = 4096
|
|
174
|
+
|
|
175
|
+
def __init__(self, user, mechanism, password, iterations=None, salt=None):
|
|
176
|
+
if not isinstance(mechanism, ScramMechanism):
|
|
177
|
+
if isinstance(mechanism, str):
|
|
178
|
+
mechanism = ScramMechanism[mechanism.upper().replace('-', '_')]
|
|
179
|
+
else:
|
|
180
|
+
mechanism = ScramMechanism(mechanism)
|
|
181
|
+
if mechanism == ScramMechanism.UNKNOWN:
|
|
182
|
+
raise IllegalArgumentError("SCRAM mechanism must not be UNKNOWN")
|
|
183
|
+
self.user = user
|
|
184
|
+
self.mechanism = mechanism
|
|
185
|
+
self.iterations = iterations if iterations is not None else self.DEFAULT_ITERATIONS
|
|
186
|
+
self.salt = salt if salt is not None else os.urandom(24)
|
|
187
|
+
if isinstance(password, str):
|
|
188
|
+
password = password.encode('utf-8')
|
|
189
|
+
self.salted_password = hashlib.pbkdf2_hmac(
|
|
190
|
+
mechanism.hash_name, password, self.salt, self.iterations)
|
|
191
|
+
|
|
192
|
+
def __repr__(self):
|
|
193
|
+
return (f"UserScramCredentialUpsertion({self.user}, "
|
|
194
|
+
f"{self.mechanism.name}, iterations={self.iterations})")
|