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,897 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from collections import OrderedDict
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from enum import IntEnum
|
|
5
|
+
import logging
|
|
6
|
+
import random
|
|
7
|
+
import re
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
|
|
11
|
+
import kafka.errors as Errors
|
|
12
|
+
from kafka.protocol.consumer import OffsetResetStrategy
|
|
13
|
+
from kafka.structs import OffsetAndMetadata
|
|
14
|
+
from kafka.util import ensure_valid_topic_name, synchronized
|
|
15
|
+
|
|
16
|
+
log = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SubscriptionType(IntEnum):
|
|
20
|
+
NONE = 0
|
|
21
|
+
AUTO_TOPICS = 1
|
|
22
|
+
AUTO_PATTERN = 2
|
|
23
|
+
USER_ASSIGNED = 3
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SubscriptionState:
|
|
27
|
+
"""
|
|
28
|
+
A class for tracking the topics, partitions, and offsets for the consumer.
|
|
29
|
+
A partition is "assigned" either directly with assign_from_user() (manual
|
|
30
|
+
assignment) or with assign_from_subscribed() (automatic assignment from
|
|
31
|
+
subscription).
|
|
32
|
+
|
|
33
|
+
Once assigned, the partition is not considered "fetchable" until its initial
|
|
34
|
+
position has been set with seek(). Fetchable partitions track a fetch
|
|
35
|
+
position which is used to set the offset of the next fetch, and a consumed
|
|
36
|
+
position which is the last offset that has been returned to the user. You
|
|
37
|
+
can suspend fetching from a partition through pause() without affecting the
|
|
38
|
+
fetched/consumed offsets. The partition will remain unfetchable until the
|
|
39
|
+
resume() is used. You can also query the pause state independently with
|
|
40
|
+
is_paused().
|
|
41
|
+
|
|
42
|
+
Note that pause state as well as fetch/consumed positions are not preserved
|
|
43
|
+
when partition assignment is changed whether directly by the user or
|
|
44
|
+
through a group rebalance.
|
|
45
|
+
"""
|
|
46
|
+
_SUBSCRIPTION_EXCEPTION_MESSAGE = (
|
|
47
|
+
"You must choose only one way to configure your consumer:"
|
|
48
|
+
" (1) subscribe to specific topics by name,"
|
|
49
|
+
" (2) subscribe to topics matching a regex pattern,"
|
|
50
|
+
" (3) assign itself specific topic-partitions.")
|
|
51
|
+
|
|
52
|
+
def __init__(self, offset_reset_strategy='earliest'):
|
|
53
|
+
"""Initialize a SubscriptionState instance
|
|
54
|
+
|
|
55
|
+
Keyword Arguments:
|
|
56
|
+
offset_reset_strategy: 'earliest' or 'latest', otherwise
|
|
57
|
+
exception will be raised when fetching an offset that is no
|
|
58
|
+
longer available. Default: 'earliest'
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
offset_reset_strategy = getattr(OffsetResetStrategy,
|
|
62
|
+
offset_reset_strategy.upper())
|
|
63
|
+
except AttributeError:
|
|
64
|
+
log.warning('Unrecognized offset_reset_strategy, using NONE')
|
|
65
|
+
offset_reset_strategy = OffsetResetStrategy.NONE
|
|
66
|
+
self._default_offset_reset_strategy = offset_reset_strategy
|
|
67
|
+
|
|
68
|
+
self.subscription = None # set() or None
|
|
69
|
+
self.subscription_type = SubscriptionType.NONE
|
|
70
|
+
self.subscribed_pattern = None # regex str or None
|
|
71
|
+
self._group_subscription = set()
|
|
72
|
+
self._user_assignment = set()
|
|
73
|
+
self.assignment = OrderedDict()
|
|
74
|
+
self.rebalance_listener = None
|
|
75
|
+
self.listeners = []
|
|
76
|
+
self._lock = threading.RLock()
|
|
77
|
+
|
|
78
|
+
def _set_subscription_type(self, subscription_type):
|
|
79
|
+
if not isinstance(subscription_type, SubscriptionType):
|
|
80
|
+
raise ValueError('SubscriptionType enum required')
|
|
81
|
+
if self.subscription_type == SubscriptionType.NONE:
|
|
82
|
+
self.subscription_type = subscription_type
|
|
83
|
+
elif self.subscription_type != subscription_type:
|
|
84
|
+
raise Errors.IllegalStateError(self._SUBSCRIPTION_EXCEPTION_MESSAGE)
|
|
85
|
+
|
|
86
|
+
@synchronized
|
|
87
|
+
def subscribe(self, topics=(), pattern=None, listener=None):
|
|
88
|
+
"""Subscribe to a list of topics, or a topic regex pattern.
|
|
89
|
+
|
|
90
|
+
Partitions will be dynamically assigned via a group coordinator.
|
|
91
|
+
Topic subscriptions are not incremental: this list will replace the
|
|
92
|
+
current assignment (if there is one).
|
|
93
|
+
|
|
94
|
+
This method is incompatible with assign_from_user()
|
|
95
|
+
|
|
96
|
+
Arguments:
|
|
97
|
+
topics (list): List of topics for subscription.
|
|
98
|
+
pattern (str): Pattern to match available topics. You must provide
|
|
99
|
+
either topics or pattern, but not both.
|
|
100
|
+
listener (ConsumerRebalanceListener): Optionally include listener
|
|
101
|
+
callback, which will be called before and after each rebalance
|
|
102
|
+
operation.
|
|
103
|
+
|
|
104
|
+
As part of group management, the consumer will keep track of the
|
|
105
|
+
list of consumers that belong to a particular group and will
|
|
106
|
+
trigger a rebalance operation if one of the following events
|
|
107
|
+
trigger:
|
|
108
|
+
|
|
109
|
+
* Number of partitions change for any of the subscribed topics
|
|
110
|
+
* Topic is created or deleted
|
|
111
|
+
* An existing member of the consumer group dies
|
|
112
|
+
* A new member is added to the consumer group
|
|
113
|
+
|
|
114
|
+
When any of these events are triggered, the provided listener
|
|
115
|
+
will be invoked first to indicate that the consumer's assignment
|
|
116
|
+
has been revoked, and then again when the new assignment has
|
|
117
|
+
been received. Note that this listener will immediately override
|
|
118
|
+
any listener set in a previous call to subscribe. It is
|
|
119
|
+
guaranteed, however, that the partitions revoked/assigned
|
|
120
|
+
through this interface are from topics subscribed in this call.
|
|
121
|
+
|
|
122
|
+
Raises:
|
|
123
|
+
ValueError: if neither topics nor pattern provided.
|
|
124
|
+
IllegalStateError: if both topics and pattern provided.
|
|
125
|
+
TypeError: if topics is not a list/sequence, or listener is not
|
|
126
|
+
a AsyncConsumerRebalanceListener or ConsumerRebalanceListener.
|
|
127
|
+
"""
|
|
128
|
+
if not topics and not pattern:
|
|
129
|
+
raise ValueError('Must provide topics or pattern')
|
|
130
|
+
if (topics and pattern):
|
|
131
|
+
raise Errors.IllegalStateError(self._SUBSCRIPTION_EXCEPTION_MESSAGE)
|
|
132
|
+
|
|
133
|
+
elif pattern:
|
|
134
|
+
self._set_subscription_type(SubscriptionType.AUTO_PATTERN)
|
|
135
|
+
log.info('Subscribing to pattern: /%s/', pattern)
|
|
136
|
+
self.subscription = set()
|
|
137
|
+
self.subscribed_pattern = re.compile(pattern)
|
|
138
|
+
else:
|
|
139
|
+
if isinstance(topics, str) or not isinstance(topics, Sequence):
|
|
140
|
+
raise TypeError('Topics must be a list (or non-str sequence)')
|
|
141
|
+
self._set_subscription_type(SubscriptionType.AUTO_TOPICS)
|
|
142
|
+
self.change_subscription(topics)
|
|
143
|
+
|
|
144
|
+
if listener and not isinstance(
|
|
145
|
+
listener, (ConsumerRebalanceListener, AsyncConsumerRebalanceListener)):
|
|
146
|
+
raise TypeError(
|
|
147
|
+
'listener must be a ConsumerRebalanceListener or AsyncConsumerRebalanceListener')
|
|
148
|
+
self.rebalance_listener = listener
|
|
149
|
+
|
|
150
|
+
@synchronized
|
|
151
|
+
def change_subscription(self, topics):
|
|
152
|
+
"""Change the topic subscription.
|
|
153
|
+
|
|
154
|
+
Arguments:
|
|
155
|
+
topics (list of str): topics for subscription
|
|
156
|
+
|
|
157
|
+
Raises:
|
|
158
|
+
IllegalStateError: if assign_from_user has been used already
|
|
159
|
+
TypeError: if a topic is None or a non-str
|
|
160
|
+
ValueError: if a topic is an empty string or
|
|
161
|
+
- a topic name is '.' or '..' or
|
|
162
|
+
- a topic name does not consist of ASCII-characters/'-'/'_'/'.'
|
|
163
|
+
"""
|
|
164
|
+
if not self.partitions_auto_assigned():
|
|
165
|
+
raise Errors.IllegalStateError(self._SUBSCRIPTION_EXCEPTION_MESSAGE)
|
|
166
|
+
|
|
167
|
+
if isinstance(topics, str):
|
|
168
|
+
topics = [topics]
|
|
169
|
+
|
|
170
|
+
if self.subscription == set(topics):
|
|
171
|
+
log.warning("subscription unchanged by change_subscription(%s)",
|
|
172
|
+
topics)
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
for t in topics:
|
|
176
|
+
ensure_valid_topic_name(t)
|
|
177
|
+
|
|
178
|
+
log.info('Updating subscribed topics to: %s', topics)
|
|
179
|
+
self.subscription = set(topics)
|
|
180
|
+
self._group_subscription.update(topics)
|
|
181
|
+
|
|
182
|
+
@synchronized
|
|
183
|
+
def group_subscribe(self, topics):
|
|
184
|
+
"""Add topics to the current group subscription.
|
|
185
|
+
|
|
186
|
+
This is used by the group leader to ensure that it receives metadata
|
|
187
|
+
updates for all topics that any member of the group is subscribed to.
|
|
188
|
+
|
|
189
|
+
Arguments:
|
|
190
|
+
topics (list of str): topics to add to the group subscription
|
|
191
|
+
"""
|
|
192
|
+
if not self.partitions_auto_assigned():
|
|
193
|
+
raise Errors.IllegalStateError(self._SUBSCRIPTION_EXCEPTION_MESSAGE)
|
|
194
|
+
self._group_subscription.update(topics)
|
|
195
|
+
|
|
196
|
+
@synchronized
|
|
197
|
+
def reset_group_subscription(self):
|
|
198
|
+
"""Reset the group's subscription to only contain topics subscribed by this consumer."""
|
|
199
|
+
if not self.partitions_auto_assigned():
|
|
200
|
+
raise Errors.IllegalStateError(self._SUBSCRIPTION_EXCEPTION_MESSAGE)
|
|
201
|
+
if self.subscription is None:
|
|
202
|
+
raise Errors.IllegalStateError('Subscription required')
|
|
203
|
+
self._group_subscription.intersection_update(self.subscription)
|
|
204
|
+
|
|
205
|
+
@synchronized
|
|
206
|
+
def assign_from_user(self, partitions):
|
|
207
|
+
"""Manually assign a list of TopicPartitions to this consumer.
|
|
208
|
+
|
|
209
|
+
The new assignment replaces the previous one (this is not an
|
|
210
|
+
incremental-add API), but ``TopicPartitionState`` is preserved
|
|
211
|
+
for any partition that's present in both the old and new
|
|
212
|
+
assignment. Manual topic assignment through this method does
|
|
213
|
+
not use the consumer's group management functionality. As
|
|
214
|
+
such, there will be no rebalance operation triggered when
|
|
215
|
+
group membership or cluster and topic metadata change. Note
|
|
216
|
+
that it is not possible to use both manual partition
|
|
217
|
+
assignment with assign() and group assignment with subscribe().
|
|
218
|
+
|
|
219
|
+
Arguments:
|
|
220
|
+
partitions (list of TopicPartition): assignment for this instance.
|
|
221
|
+
|
|
222
|
+
Raises:
|
|
223
|
+
IllegalStateError: if consumer has already called subscribe()
|
|
224
|
+
"""
|
|
225
|
+
self._set_subscription_type(SubscriptionType.USER_ASSIGNED)
|
|
226
|
+
if self._user_assignment == set(partitions):
|
|
227
|
+
return
|
|
228
|
+
self._user_assignment = set(partitions)
|
|
229
|
+
self._apply_assignment(partitions)
|
|
230
|
+
|
|
231
|
+
@synchronized
|
|
232
|
+
def assign_from_subscribed(self, assignments):
|
|
233
|
+
"""Update the assignment to the specified partitions.
|
|
234
|
+
|
|
235
|
+
This method is called by the coordinator to dynamically assign
|
|
236
|
+
partitions based on the consumer's topic subscription. Differs
|
|
237
|
+
from :meth:`assign_from_user` which directly sets the assignment
|
|
238
|
+
from a user-supplied TopicPartition list.
|
|
239
|
+
|
|
240
|
+
Preserves ``TopicPartitionState`` (position, paused flag,
|
|
241
|
+
preferred read replica, fetch buffers tied to the partition)
|
|
242
|
+
for any partition present in both the prior and new assignments.
|
|
243
|
+
|
|
244
|
+
Validation raises ``ValueError`` BEFORE any mutation if a
|
|
245
|
+
partition's topic isn't subscribed.
|
|
246
|
+
|
|
247
|
+
Arguments:
|
|
248
|
+
assignments (list of TopicPartition): the *full* new
|
|
249
|
+
assignment (not a diff). Partitions present in both
|
|
250
|
+
the old and new assignment retain their state;
|
|
251
|
+
revoked partitions are dropped; new partitions get
|
|
252
|
+
fresh state.
|
|
253
|
+
"""
|
|
254
|
+
if not self.partitions_auto_assigned():
|
|
255
|
+
raise Errors.IllegalStateError(self._SUBSCRIPTION_EXCEPTION_MESSAGE)
|
|
256
|
+
|
|
257
|
+
for tp in assignments:
|
|
258
|
+
if tp.topic not in self.subscription:
|
|
259
|
+
raise ValueError("Assigned partition %s for non-subscribed topic." % (tp,))
|
|
260
|
+
|
|
261
|
+
# randomize new-partition insertion order so short-lived
|
|
262
|
+
# consumers (CLI tools, one-shot jobs) that re-run from scratch
|
|
263
|
+
# don't keep starting on the same partition. The Fetcher
|
|
264
|
+
# iterates fetchable_partitions() in self.assignment insertion
|
|
265
|
+
# order; without the shuffle, partition 0 of the
|
|
266
|
+
# alphabetically-first topic always wins the first fetch and
|
|
267
|
+
# short-lived runs that bail before exhausting it never see
|
|
268
|
+
# later partitions.
|
|
269
|
+
self._apply_assignment(assignments, randomize=True)
|
|
270
|
+
log.info("Updated partition assignment: %s", assignments)
|
|
271
|
+
|
|
272
|
+
def _apply_assignment(self, partitions, randomize=False):
|
|
273
|
+
"""Mutate ``self.assignment`` in place to contain exactly
|
|
274
|
+
``partitions``, preserving the existing ``TopicPartitionState``
|
|
275
|
+
for any partition present in both the old and new sets.
|
|
276
|
+
|
|
277
|
+
Shared between :meth:`assign_from_user` and
|
|
278
|
+
:meth:`assign_from_subscribed` - the algorithm is identical;
|
|
279
|
+
only the validation around it differs.
|
|
280
|
+
|
|
281
|
+
When ``randomize=True``, the *newly-added* partitions are
|
|
282
|
+
inserted in random order. Kept partitions retain their
|
|
283
|
+
existing position regardless. This is intended for the
|
|
284
|
+
coordinator-driven path (assign_from_subscribed) - see the
|
|
285
|
+
comment at that call site for rationale.
|
|
286
|
+
"""
|
|
287
|
+
new_set = set(partitions)
|
|
288
|
+
# Drop revoked partitions (we mutate self.assignment, so list()
|
|
289
|
+
# the keys first to avoid "dict changed size during iteration").
|
|
290
|
+
for tp in list(self.assignment.keys()):
|
|
291
|
+
if tp not in new_set:
|
|
292
|
+
del self.assignment[tp]
|
|
293
|
+
# Add new partitions; kept partitions retain their existing
|
|
294
|
+
# TopicPartitionState (positions, paused flag, KIP-392 cache,
|
|
295
|
+
# etc.).
|
|
296
|
+
new_partitions = [tp for tp in partitions if tp not in self.assignment]
|
|
297
|
+
if randomize:
|
|
298
|
+
random.shuffle(new_partitions)
|
|
299
|
+
for tp in new_partitions:
|
|
300
|
+
self.assignment[tp] = TopicPartitionState()
|
|
301
|
+
|
|
302
|
+
@synchronized
|
|
303
|
+
def unsubscribe(self):
|
|
304
|
+
"""Clear all topic subscriptions and partition assignments"""
|
|
305
|
+
self.subscription = None
|
|
306
|
+
self._user_assignment.clear()
|
|
307
|
+
self.assignment.clear()
|
|
308
|
+
self.subscribed_pattern = None
|
|
309
|
+
self.subscription_type = SubscriptionType.NONE
|
|
310
|
+
|
|
311
|
+
@synchronized
|
|
312
|
+
def group_subscription(self):
|
|
313
|
+
"""Get the topic subscription for the group.
|
|
314
|
+
|
|
315
|
+
For the leader, this will include the union of all member subscriptions.
|
|
316
|
+
For followers, it is the member's subscription only.
|
|
317
|
+
|
|
318
|
+
This is used when querying topic metadata to detect metadata changes
|
|
319
|
+
that would require rebalancing (the leader fetches metadata for all
|
|
320
|
+
topics in the group so that it can do partition assignment).
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
set: topics
|
|
324
|
+
"""
|
|
325
|
+
return self._group_subscription
|
|
326
|
+
|
|
327
|
+
@synchronized
|
|
328
|
+
def seek(self, partition, offset):
|
|
329
|
+
"""Manually specify the fetch offset for a TopicPartition.
|
|
330
|
+
|
|
331
|
+
Overrides the fetch offsets that the consumer will use on the next
|
|
332
|
+
poll(). If this API is invoked for the same partition more than once,
|
|
333
|
+
the latest offset will be used on the next poll(). Note that you may
|
|
334
|
+
lose data if this API is arbitrarily used in the middle of consumption,
|
|
335
|
+
to reset the fetch offsets.
|
|
336
|
+
|
|
337
|
+
Arguments:
|
|
338
|
+
partition (TopicPartition): partition for seek operation
|
|
339
|
+
offset (int or OffsetAndMetadata): message offset in partition
|
|
340
|
+
"""
|
|
341
|
+
if not isinstance(offset, (int, OffsetAndMetadata)):
|
|
342
|
+
raise TypeError("offset must be type in or OffsetAndMetadata")
|
|
343
|
+
self.assignment[partition].seek(offset)
|
|
344
|
+
|
|
345
|
+
@synchronized
|
|
346
|
+
def assigned_partitions(self):
|
|
347
|
+
"""Return set of TopicPartitions in current assignment."""
|
|
348
|
+
return set(self.assignment.keys())
|
|
349
|
+
|
|
350
|
+
@synchronized
|
|
351
|
+
def paused_partitions(self):
|
|
352
|
+
"""Return current set of paused TopicPartitions."""
|
|
353
|
+
return set(partition for partition in self.assignment
|
|
354
|
+
if self.is_paused(partition))
|
|
355
|
+
|
|
356
|
+
@synchronized
|
|
357
|
+
def fetchable_partitions(self):
|
|
358
|
+
"""Return ordered list of TopicPartitions that should be Fetched."""
|
|
359
|
+
fetchable = list()
|
|
360
|
+
for partition, state in self.assignment.items():
|
|
361
|
+
if state.is_fetchable():
|
|
362
|
+
fetchable.append(partition)
|
|
363
|
+
return fetchable
|
|
364
|
+
|
|
365
|
+
@synchronized
|
|
366
|
+
def partitions_auto_assigned(self):
|
|
367
|
+
"""Return True unless user supplied partitions manually."""
|
|
368
|
+
return self.subscription_type in (SubscriptionType.AUTO_TOPICS, SubscriptionType.AUTO_PATTERN)
|
|
369
|
+
|
|
370
|
+
@synchronized
|
|
371
|
+
def all_consumed_offsets(self):
|
|
372
|
+
"""Returns consumed offsets as {TopicPartition: OffsetAndMetadata}"""
|
|
373
|
+
all_consumed = {}
|
|
374
|
+
for partition, state in self.assignment.items():
|
|
375
|
+
if state.has_valid_position:
|
|
376
|
+
all_consumed[partition] = state.position
|
|
377
|
+
return all_consumed
|
|
378
|
+
|
|
379
|
+
@synchronized
|
|
380
|
+
def request_offset_reset(self, partition, offset_reset_strategy=None):
|
|
381
|
+
"""Mark partition for offset reset using specified or default strategy.
|
|
382
|
+
|
|
383
|
+
Arguments:
|
|
384
|
+
partition (TopicPartition): partition to mark
|
|
385
|
+
offset_reset_strategy (OffsetResetStrategy, optional)
|
|
386
|
+
"""
|
|
387
|
+
if offset_reset_strategy is None:
|
|
388
|
+
offset_reset_strategy = self._default_offset_reset_strategy
|
|
389
|
+
self.assignment[partition].reset(offset_reset_strategy)
|
|
390
|
+
|
|
391
|
+
@synchronized
|
|
392
|
+
def set_reset_pending(self, partitions, next_allowed_reset_time):
|
|
393
|
+
for partition in partitions:
|
|
394
|
+
self.assignment[partition].set_reset_pending(next_allowed_reset_time)
|
|
395
|
+
|
|
396
|
+
@synchronized
|
|
397
|
+
def has_default_offset_reset_policy(self):
|
|
398
|
+
"""Return True if default offset reset policy is Earliest or Latest"""
|
|
399
|
+
return self._default_offset_reset_strategy != OffsetResetStrategy.NONE
|
|
400
|
+
|
|
401
|
+
@synchronized
|
|
402
|
+
def is_offset_reset_needed(self, partition):
|
|
403
|
+
return self.assignment[partition].awaiting_reset
|
|
404
|
+
|
|
405
|
+
@synchronized
|
|
406
|
+
def has_all_fetch_positions(self):
|
|
407
|
+
for state in self.assignment.values():
|
|
408
|
+
if not state.has_valid_position:
|
|
409
|
+
return False
|
|
410
|
+
return True
|
|
411
|
+
|
|
412
|
+
@synchronized
|
|
413
|
+
def missing_fetch_positions(self):
|
|
414
|
+
missing = set()
|
|
415
|
+
for partition, state in self.assignment.items():
|
|
416
|
+
if state.is_missing_position():
|
|
417
|
+
missing.add(partition)
|
|
418
|
+
return missing
|
|
419
|
+
|
|
420
|
+
@synchronized
|
|
421
|
+
def has_valid_position(self, partition):
|
|
422
|
+
return partition in self.assignment and self.assignment[partition].has_valid_position
|
|
423
|
+
|
|
424
|
+
@synchronized
|
|
425
|
+
def reset_missing_positions(self):
|
|
426
|
+
partitions_with_no_offsets = set()
|
|
427
|
+
for tp, state in self.assignment.items():
|
|
428
|
+
if state.is_missing_position():
|
|
429
|
+
if self._default_offset_reset_strategy == OffsetResetStrategy.NONE:
|
|
430
|
+
partitions_with_no_offsets.add(tp)
|
|
431
|
+
else:
|
|
432
|
+
state.reset(self._default_offset_reset_strategy)
|
|
433
|
+
|
|
434
|
+
if partitions_with_no_offsets:
|
|
435
|
+
raise Errors.NoOffsetForPartitionError(partitions_with_no_offsets)
|
|
436
|
+
|
|
437
|
+
@synchronized
|
|
438
|
+
def partitions_needing_reset(self):
|
|
439
|
+
partitions = set()
|
|
440
|
+
for tp, state in self.assignment.items():
|
|
441
|
+
if state.awaiting_reset and state.is_reset_allowed():
|
|
442
|
+
partitions.add(tp)
|
|
443
|
+
return partitions
|
|
444
|
+
|
|
445
|
+
@synchronized
|
|
446
|
+
def next_offset_reset_retry_time(self):
|
|
447
|
+
times = [state.next_allowed_retry_time
|
|
448
|
+
for state in self.assignment.values()
|
|
449
|
+
if state.awaiting_reset and state.next_allowed_retry_time is not None]
|
|
450
|
+
return min(times) if times else None
|
|
451
|
+
|
|
452
|
+
@synchronized
|
|
453
|
+
def maybe_validate_position_for_current_leader(self, partition, current_leader_epoch):
|
|
454
|
+
if partition not in self.assignment:
|
|
455
|
+
return False
|
|
456
|
+
return self.assignment[partition].maybe_validate_position(current_leader_epoch)
|
|
457
|
+
|
|
458
|
+
@synchronized
|
|
459
|
+
def request_position_validation(self, partition):
|
|
460
|
+
if partition not in self.assignment:
|
|
461
|
+
return False
|
|
462
|
+
return self.assignment[partition].request_position_validation()
|
|
463
|
+
|
|
464
|
+
@synchronized
|
|
465
|
+
def partitions_needing_validation(self):
|
|
466
|
+
partitions = set()
|
|
467
|
+
for tp, state in self.assignment.items():
|
|
468
|
+
if state.awaiting_validation and state.is_validation_allowed():
|
|
469
|
+
partitions.add(tp)
|
|
470
|
+
return partitions
|
|
471
|
+
|
|
472
|
+
@synchronized
|
|
473
|
+
def next_offset_validation_retry_time(self):
|
|
474
|
+
times = [state.next_allowed_retry_time
|
|
475
|
+
for state in self.assignment.values()
|
|
476
|
+
if state.awaiting_validation and state.next_allowed_retry_time is not None]
|
|
477
|
+
return min(times) if times else None
|
|
478
|
+
|
|
479
|
+
@synchronized
|
|
480
|
+
def set_validation_pending(self, partitions, next_allowed_retry_time):
|
|
481
|
+
for partition in partitions:
|
|
482
|
+
self.assignment[partition].set_validation_pending(next_allowed_retry_time)
|
|
483
|
+
|
|
484
|
+
@synchronized
|
|
485
|
+
def validation_failed(self, partitions, next_allowed_retry_time):
|
|
486
|
+
for partition in partitions:
|
|
487
|
+
self.assignment[partition].validation_failed(next_allowed_retry_time)
|
|
488
|
+
|
|
489
|
+
@synchronized
|
|
490
|
+
def complete_validation(self, partition, validated_position=None):
|
|
491
|
+
if partition in self.assignment:
|
|
492
|
+
self.assignment[partition].complete_validation(validated_position)
|
|
493
|
+
|
|
494
|
+
@synchronized
|
|
495
|
+
def is_offset_validation_needed(self, partition):
|
|
496
|
+
return partition in self.assignment and self.assignment[partition].awaiting_validation
|
|
497
|
+
|
|
498
|
+
@synchronized
|
|
499
|
+
def is_assigned(self, partition):
|
|
500
|
+
return partition in self.assignment
|
|
501
|
+
|
|
502
|
+
@synchronized
|
|
503
|
+
def is_paused(self, partition):
|
|
504
|
+
return partition in self.assignment and self.assignment[partition].paused
|
|
505
|
+
|
|
506
|
+
@synchronized
|
|
507
|
+
def is_fetchable(self, partition):
|
|
508
|
+
return partition in self.assignment and self.assignment[partition].is_fetchable()
|
|
509
|
+
|
|
510
|
+
@synchronized
|
|
511
|
+
def pause(self, partition):
|
|
512
|
+
self.assignment[partition].pause()
|
|
513
|
+
|
|
514
|
+
@synchronized
|
|
515
|
+
def resume(self, partition):
|
|
516
|
+
self.assignment[partition].resume()
|
|
517
|
+
|
|
518
|
+
@synchronized
|
|
519
|
+
def mark_pending_revocation(self, partitions):
|
|
520
|
+
"""KIP-429: gate ``is_fetchable()`` for each partition's state
|
|
521
|
+
so the fetcher would skip them while an on_partitions_revoked /
|
|
522
|
+
on_partitions_lost listener runs. Called immediately before
|
|
523
|
+
invoking the listener. The flag is single-shot - the
|
|
524
|
+
surrounding ``assign_from_subscribed`` drops the
|
|
525
|
+
``TopicPartitionState`` for revoked partitions when the
|
|
526
|
+
listener returns.
|
|
527
|
+
|
|
528
|
+
Currently a no-op while the user thread is blocked in ``_net.run``
|
|
529
|
+
during rebalance and so the only path that calls ``send_fetches``
|
|
530
|
+
cannot fire. Kept as a defensive gate in case this changes in
|
|
531
|
+
the future."""
|
|
532
|
+
for tp in partitions:
|
|
533
|
+
if tp in self.assignment:
|
|
534
|
+
self.assignment[tp].mark_pending_revocation()
|
|
535
|
+
|
|
536
|
+
@synchronized
|
|
537
|
+
def reset_failed(self, partitions, next_retry_time):
|
|
538
|
+
for partition in partitions:
|
|
539
|
+
self.assignment[partition].reset_failed(next_retry_time)
|
|
540
|
+
|
|
541
|
+
@synchronized
|
|
542
|
+
def move_partition_to_end(self, partition):
|
|
543
|
+
if partition in self.assignment:
|
|
544
|
+
try:
|
|
545
|
+
self.assignment.move_to_end(partition)
|
|
546
|
+
except AttributeError:
|
|
547
|
+
state = self.assignment.pop(partition)
|
|
548
|
+
self.assignment[partition] = state
|
|
549
|
+
|
|
550
|
+
@synchronized
|
|
551
|
+
def position(self, partition):
|
|
552
|
+
return self.assignment[partition].position
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
class TopicPartitionState:
|
|
556
|
+
def __init__(self):
|
|
557
|
+
self.paused = False # whether this partition has been paused by the user
|
|
558
|
+
self.reset_strategy = None # the reset strategy if awaiting_reset is set
|
|
559
|
+
self._position = None # OffsetAndMetadata exposed to the user
|
|
560
|
+
self.highwater = None
|
|
561
|
+
self.drop_pending_record_batch = False
|
|
562
|
+
self.next_allowed_retry_time = None
|
|
563
|
+
# KIP-320: offset validation state. _awaiting_validation gates fetches
|
|
564
|
+
# until OffsetForLeaderEpoch confirms the position is consistent with
|
|
565
|
+
# the current leader's log; mutually exclusive with awaiting_reset.
|
|
566
|
+
self._awaiting_validation = False
|
|
567
|
+
# KIP-392: preferred read replica chosen by the broker (rack-aware).
|
|
568
|
+
# ``_preferred_read_replica_expiration`` is a monotonic deadline; after
|
|
569
|
+
# it passes we fall back to the leader and re-learn. Cleared on
|
|
570
|
+
# replica-related errors so the next fetch goes to the leader.
|
|
571
|
+
self._preferred_read_replica = None
|
|
572
|
+
self._preferred_read_replica_expiration = None
|
|
573
|
+
# KIP-429 (Java parity): set while an on_partitions_revoked /
|
|
574
|
+
# on_partitions_lost listener is running for this partition.
|
|
575
|
+
# Gates fetches so records aren't pulled for a partition the
|
|
576
|
+
# user is in the middle of releasing. The TopicPartitionState
|
|
577
|
+
# is dropped from the assignment when the listener returns,
|
|
578
|
+
# so the flag only matters for the listener-call window.
|
|
579
|
+
self._pending_revocation = False
|
|
580
|
+
|
|
581
|
+
def _set_position(self, offset):
|
|
582
|
+
if not self.has_valid_position:
|
|
583
|
+
raise Errors.IllegalStateError('Valid position required')
|
|
584
|
+
if not isinstance(offset, OffsetAndMetadata):
|
|
585
|
+
raise TypeError('offset must be OffsetAndMetadata')
|
|
586
|
+
self._position = offset
|
|
587
|
+
|
|
588
|
+
def _get_position(self):
|
|
589
|
+
return self._position
|
|
590
|
+
|
|
591
|
+
position = property(_get_position, _set_position, None, "last position")
|
|
592
|
+
|
|
593
|
+
def reset(self, strategy):
|
|
594
|
+
if strategy is None:
|
|
595
|
+
raise ValueError('strategy cannot be None')
|
|
596
|
+
self.reset_strategy = strategy
|
|
597
|
+
self._position = None
|
|
598
|
+
self.next_allowed_retry_time = None
|
|
599
|
+
self._awaiting_validation = False
|
|
600
|
+
self.clear_preferred_read_replica()
|
|
601
|
+
|
|
602
|
+
def is_reset_allowed(self):
|
|
603
|
+
return self.next_allowed_retry_time is None or self.next_allowed_retry_time < time.monotonic()
|
|
604
|
+
|
|
605
|
+
@property
|
|
606
|
+
def awaiting_reset(self):
|
|
607
|
+
return self.reset_strategy is not None
|
|
608
|
+
|
|
609
|
+
def set_reset_pending(self, next_allowed_retry_time):
|
|
610
|
+
self.next_allowed_retry_time = next_allowed_retry_time
|
|
611
|
+
|
|
612
|
+
def reset_failed(self, next_allowed_retry_time):
|
|
613
|
+
self.next_allowed_retry_time = next_allowed_retry_time
|
|
614
|
+
|
|
615
|
+
@property
|
|
616
|
+
def has_valid_position(self):
|
|
617
|
+
return self._position is not None
|
|
618
|
+
|
|
619
|
+
def is_missing_position(self):
|
|
620
|
+
return not self.has_valid_position and not self.awaiting_reset
|
|
621
|
+
|
|
622
|
+
def seek(self, offset):
|
|
623
|
+
self._position = offset if isinstance(offset, OffsetAndMetadata) else OffsetAndMetadata(offset, '', -1)
|
|
624
|
+
self.reset_strategy = None
|
|
625
|
+
self.drop_pending_record_batch = True
|
|
626
|
+
self.next_allowed_retry_time = None
|
|
627
|
+
self._awaiting_validation = False
|
|
628
|
+
self.clear_preferred_read_replica()
|
|
629
|
+
|
|
630
|
+
def pause(self):
|
|
631
|
+
self.paused = True
|
|
632
|
+
|
|
633
|
+
def resume(self):
|
|
634
|
+
self.paused = False
|
|
635
|
+
|
|
636
|
+
def mark_pending_revocation(self):
|
|
637
|
+
"""KIP-429: gate fetches while an on_partitions_revoked /
|
|
638
|
+
on_partitions_lost listener is in progress for this partition.
|
|
639
|
+
Single-shot: the surrounding ``assign_from_subscribed`` drops
|
|
640
|
+
the state object once the listener returns."""
|
|
641
|
+
self._pending_revocation = True
|
|
642
|
+
|
|
643
|
+
def is_fetchable(self):
|
|
644
|
+
return (not self.paused
|
|
645
|
+
and not self._pending_revocation
|
|
646
|
+
and self.has_valid_position
|
|
647
|
+
and not self._awaiting_validation)
|
|
648
|
+
|
|
649
|
+
def preferred_read_replica(self):
|
|
650
|
+
"""Return the currently-cached preferred read replica (KIP-392),
|
|
651
|
+
or None if unset/expired. Lazily clears the cache on expiry."""
|
|
652
|
+
if self._preferred_read_replica is None:
|
|
653
|
+
return None
|
|
654
|
+
if (self._preferred_read_replica_expiration is not None
|
|
655
|
+
and time.monotonic() >= self._preferred_read_replica_expiration):
|
|
656
|
+
self.clear_preferred_read_replica()
|
|
657
|
+
return None
|
|
658
|
+
return self._preferred_read_replica
|
|
659
|
+
|
|
660
|
+
def update_preferred_read_replica(self, node_id, expiration_time):
|
|
661
|
+
"""Cache the broker's chosen preferred read replica until ``expiration_time``
|
|
662
|
+
(monotonic). ``node_id == -1`` (or None) clears the cache.
|
|
663
|
+
|
|
664
|
+
Returns True if the cached replica actually changed (caller can log).
|
|
665
|
+
"""
|
|
666
|
+
if node_id is None or node_id < 0:
|
|
667
|
+
changed = self._preferred_read_replica is not None
|
|
668
|
+
self.clear_preferred_read_replica()
|
|
669
|
+
return changed
|
|
670
|
+
if node_id == self._preferred_read_replica:
|
|
671
|
+
return False
|
|
672
|
+
self._preferred_read_replica = node_id
|
|
673
|
+
self._preferred_read_replica_expiration = expiration_time
|
|
674
|
+
return True
|
|
675
|
+
|
|
676
|
+
def clear_preferred_read_replica(self):
|
|
677
|
+
"""Clear the cached preferred read replica. Returns the previously-
|
|
678
|
+
cached node_id (or None) so the caller can log the eviction."""
|
|
679
|
+
previous = self._preferred_read_replica
|
|
680
|
+
self._preferred_read_replica = None
|
|
681
|
+
self._preferred_read_replica_expiration = None
|
|
682
|
+
return previous
|
|
683
|
+
|
|
684
|
+
@property
|
|
685
|
+
def awaiting_validation(self):
|
|
686
|
+
return self._awaiting_validation
|
|
687
|
+
|
|
688
|
+
def maybe_validate_position(self, current_leader_epoch):
|
|
689
|
+
"""Mark for validation if current leader has advanced beyond our position's epoch.
|
|
690
|
+
|
|
691
|
+
Returns True if the partition is now awaiting validation.
|
|
692
|
+
"""
|
|
693
|
+
if self.awaiting_reset:
|
|
694
|
+
return False
|
|
695
|
+
if self._position is None:
|
|
696
|
+
return False
|
|
697
|
+
if current_leader_epoch is None or current_leader_epoch < 0:
|
|
698
|
+
return False
|
|
699
|
+
# Positions without a known epoch (legacy data, post-seek to bare offset)
|
|
700
|
+
# can't be validated; treat as already-fetchable.
|
|
701
|
+
if self._position.leader_epoch < 0:
|
|
702
|
+
return False
|
|
703
|
+
if self._position.leader_epoch >= current_leader_epoch:
|
|
704
|
+
return False
|
|
705
|
+
self.clear_preferred_read_replica()
|
|
706
|
+
self._awaiting_validation = True
|
|
707
|
+
self.next_allowed_retry_time = None
|
|
708
|
+
return True
|
|
709
|
+
|
|
710
|
+
def request_position_validation(self):
|
|
711
|
+
"""Force validation (e.g., after FENCED/UNKNOWN epoch errors from the broker)."""
|
|
712
|
+
if self._position is None or self._position.leader_epoch < 0:
|
|
713
|
+
return False
|
|
714
|
+
self._awaiting_validation = True
|
|
715
|
+
self.next_allowed_retry_time = None
|
|
716
|
+
return True
|
|
717
|
+
|
|
718
|
+
def is_validation_allowed(self):
|
|
719
|
+
return self.next_allowed_retry_time is None or self.next_allowed_retry_time < time.monotonic()
|
|
720
|
+
|
|
721
|
+
def set_validation_pending(self, next_allowed_retry_time):
|
|
722
|
+
self.next_allowed_retry_time = next_allowed_retry_time
|
|
723
|
+
|
|
724
|
+
def validation_failed(self, next_allowed_retry_time):
|
|
725
|
+
self.next_allowed_retry_time = next_allowed_retry_time
|
|
726
|
+
|
|
727
|
+
def complete_validation(self, validated_position=None):
|
|
728
|
+
self._awaiting_validation = False
|
|
729
|
+
self.next_allowed_retry_time = None
|
|
730
|
+
if validated_position is not None:
|
|
731
|
+
self._position = validated_position
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
class ConsumerRebalanceListener(ABC):
|
|
735
|
+
"""
|
|
736
|
+
A callback interface that the user can implement to trigger custom actions
|
|
737
|
+
when the set of partitions assigned to the consumer changes.
|
|
738
|
+
|
|
739
|
+
This is applicable when the consumer is having Kafka auto-manage group
|
|
740
|
+
membership. If the consumer's directly assign partitions, those
|
|
741
|
+
partitions will never be reassigned and this callback is not applicable.
|
|
742
|
+
|
|
743
|
+
When Kafka is managing the group membership, a partition re-assignment will
|
|
744
|
+
be triggered any time the members of the group changes or the subscription
|
|
745
|
+
of the members changes. This can occur when processes die, new process
|
|
746
|
+
instances are added or old instances come back to life after failure.
|
|
747
|
+
Rebalances can also be triggered by changes affecting the subscribed
|
|
748
|
+
topics (e.g. when then number of partitions is administratively adjusted).
|
|
749
|
+
|
|
750
|
+
There are many uses for this functionality. One common use is saving offsets
|
|
751
|
+
in a custom store. By saving offsets in the on_partitions_revoked(), call we
|
|
752
|
+
can ensure that any time partition assignment changes the offset gets saved.
|
|
753
|
+
|
|
754
|
+
Another use is flushing out any kind of cache of intermediate results the
|
|
755
|
+
consumer may be keeping. For example, consider a case where the consumer is
|
|
756
|
+
subscribed to a topic containing user page views, and the goal is to count
|
|
757
|
+
the number of page views per users for each five minute window. Let's say
|
|
758
|
+
the topic is partitioned by the user id so that all events for a particular
|
|
759
|
+
user will go to a single consumer instance. The consumer can keep in memory
|
|
760
|
+
a running tally of actions per user and only flush these out to a remote
|
|
761
|
+
data store when its cache gets too big. However if a partition is reassigned
|
|
762
|
+
it may want to automatically trigger a flush of this cache, before the new
|
|
763
|
+
owner takes over consumption.
|
|
764
|
+
|
|
765
|
+
Threading: callbacks run on the consumer's IO event loop, the same loop
|
|
766
|
+
that drives heartbeats. Sync listener methods must return promptly --
|
|
767
|
+
blocking IO inside a sync listener will block heartbeats for the duration
|
|
768
|
+
and can cause the consumer to be kicked from the group if the listener
|
|
769
|
+
runs longer than ``session_timeout_ms``. For listeners that need to do
|
|
770
|
+
blocking work (e.g. flushing state to a database), prefer
|
|
771
|
+
:class:`AsyncConsumerRebalanceListener`, which lets you ``await`` while
|
|
772
|
+
keeping the loop responsive, or wrap the blocking call in your own
|
|
773
|
+
worker thread.
|
|
774
|
+
|
|
775
|
+
It is guaranteed that all consumer processes will invoke
|
|
776
|
+
on_partitions_revoked() prior to any process invoking
|
|
777
|
+
on_partitions_assigned(). So if offsets or other state is saved in the
|
|
778
|
+
on_partitions_revoked() call, it should be saved by the time the process
|
|
779
|
+
taking over that partition has their on_partitions_assigned() callback
|
|
780
|
+
called to load the state.
|
|
781
|
+
"""
|
|
782
|
+
@abstractmethod
|
|
783
|
+
def on_partitions_revoked(self, revoked):
|
|
784
|
+
"""
|
|
785
|
+
A callback method the user can implement to provide handling of offset
|
|
786
|
+
commits to a customized store on the start of a rebalance operation.
|
|
787
|
+
This method will be called before a rebalance operation starts and
|
|
788
|
+
after the consumer stops fetching data. It is recommended that offsets
|
|
789
|
+
should be committed in this callback to either Kafka or a custom offset
|
|
790
|
+
store to prevent duplicate data.
|
|
791
|
+
|
|
792
|
+
NOTE: This method is called before each rebalance and also when the
|
|
793
|
+
consumer is closing (KafkaConsumer.close()), so that offsets / state
|
|
794
|
+
can be committed before the partitions are given up. If the group
|
|
795
|
+
membership has already been lost (forced eviction),
|
|
796
|
+
on_partitions_lost() is called instead.
|
|
797
|
+
|
|
798
|
+
Arguments:
|
|
799
|
+
revoked (list of TopicPartition): the partitions that were assigned
|
|
800
|
+
to the consumer on the last rebalance
|
|
801
|
+
"""
|
|
802
|
+
pass
|
|
803
|
+
|
|
804
|
+
@abstractmethod
|
|
805
|
+
def on_partitions_assigned(self, assigned):
|
|
806
|
+
"""
|
|
807
|
+
A callback method the user can implement to provide handling of
|
|
808
|
+
customized offsets on completion of a successful partition
|
|
809
|
+
re-assignment. This method will be called after an offset re-assignment
|
|
810
|
+
completes and before the consumer starts fetching data.
|
|
811
|
+
|
|
812
|
+
It is guaranteed that all the processes in a consumer group will execute
|
|
813
|
+
their on_partitions_revoked() callback before any instance executes its
|
|
814
|
+
on_partitions_assigned() callback.
|
|
815
|
+
|
|
816
|
+
Arguments:
|
|
817
|
+
assigned (list of TopicPartition): the partitions assigned to the
|
|
818
|
+
consumer (may include partitions that were previously assigned)
|
|
819
|
+
"""
|
|
820
|
+
pass
|
|
821
|
+
|
|
822
|
+
def on_partitions_lost(self, lost):
|
|
823
|
+
"""KIP-429: called when the consumer has been forcibly removed
|
|
824
|
+
from the group (heartbeat session expiry, ``UnknownMemberIdError``,
|
|
825
|
+
``IllegalGenerationError``, ``FencedInstanceIdError``) and the
|
|
826
|
+
partitions cannot be cleanly committed. ``on_partitions_revoked``
|
|
827
|
+
implies the user *can* still commit; ``on_partitions_lost`` makes
|
|
828
|
+
explicit that the member has been booted and any in-flight state
|
|
829
|
+
for these partitions should be discarded.
|
|
830
|
+
|
|
831
|
+
Default behaviour is to delegate to ``on_partitions_revoked`` so
|
|
832
|
+
listeners written before KIP-429 keep working unchanged. Override
|
|
833
|
+
for cleanup that is specific to the forced-eviction case (e.g.
|
|
834
|
+
skipping a commit attempt that will fail anyway).
|
|
835
|
+
|
|
836
|
+
Arguments:
|
|
837
|
+
lost (set of TopicPartition): the partitions that were
|
|
838
|
+
assigned but have been lost due to forced eviction.
|
|
839
|
+
"""
|
|
840
|
+
return self.on_partitions_revoked(lost)
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
class AsyncConsumerRebalanceListener(ABC):
|
|
844
|
+
"""
|
|
845
|
+
Async variant of :class:`ConsumerRebalanceListener`.
|
|
846
|
+
|
|
847
|
+
Implement this when your rebalance hooks need to perform IO that would
|
|
848
|
+
otherwise block the consumer's event loop -- e.g. flushing state to a
|
|
849
|
+
database, calling an external service, or coordinating with other async
|
|
850
|
+
code. The coordinator detects coroutine functions and ``await`` s them
|
|
851
|
+
instead of calling inline, so other tasks on the loop (notably the
|
|
852
|
+
heartbeat coroutine) continue to run while your listener is suspended.
|
|
853
|
+
|
|
854
|
+
Same lifecycle and ordering guarantees as the sync listener: all
|
|
855
|
+
consumers in the group invoke ``on_partitions_revoked`` before any
|
|
856
|
+
invokes ``on_partitions_assigned``. Both methods must be defined as
|
|
857
|
+
``async def``; otherwise use :class:`ConsumerRebalanceListener`.
|
|
858
|
+
"""
|
|
859
|
+
@abstractmethod
|
|
860
|
+
async def on_partitions_revoked(self, revoked):
|
|
861
|
+
"""Async-callback for the start of a rebalance operation.
|
|
862
|
+
|
|
863
|
+
See :meth:`ConsumerRebalanceListener.on_partitions_revoked` for
|
|
864
|
+
semantics. The coordinator awaits this method, so non-blocking IO
|
|
865
|
+
via ``await`` keeps the heartbeat loop responsive during the call.
|
|
866
|
+
|
|
867
|
+
Arguments:
|
|
868
|
+
revoked (set of TopicPartition): the partitions that were
|
|
869
|
+
assigned to the consumer on the last rebalance.
|
|
870
|
+
"""
|
|
871
|
+
pass
|
|
872
|
+
|
|
873
|
+
@abstractmethod
|
|
874
|
+
async def on_partitions_assigned(self, assigned):
|
|
875
|
+
"""Async-callback for the completion of a partition re-assignment.
|
|
876
|
+
|
|
877
|
+
See :meth:`ConsumerRebalanceListener.on_partitions_assigned` for
|
|
878
|
+
semantics.
|
|
879
|
+
|
|
880
|
+
Arguments:
|
|
881
|
+
assigned (set of TopicPartition): the partitions assigned to
|
|
882
|
+
the consumer (may include partitions that were previously
|
|
883
|
+
assigned).
|
|
884
|
+
"""
|
|
885
|
+
pass
|
|
886
|
+
|
|
887
|
+
async def on_partitions_lost(self, lost):
|
|
888
|
+
"""Async variant of
|
|
889
|
+
:meth:`ConsumerRebalanceListener.on_partitions_lost`. Default
|
|
890
|
+
implementation awaits ``on_partitions_revoked`` for backward
|
|
891
|
+
compatibility with listeners written before KIP-429.
|
|
892
|
+
|
|
893
|
+
Arguments:
|
|
894
|
+
lost (set of TopicPartition): the partitions that were
|
|
895
|
+
assigned but have been lost due to forced eviction.
|
|
896
|
+
"""
|
|
897
|
+
await self.on_partitions_revoked(lost)
|