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,2012 @@
|
|
|
1
|
+
import collections
|
|
2
|
+
import copy
|
|
3
|
+
import itertools
|
|
4
|
+
import logging
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
import warnings
|
|
8
|
+
|
|
9
|
+
import kafka.errors as Errors
|
|
10
|
+
from kafka.future import Future
|
|
11
|
+
from kafka.metrics.stats import Avg, Count, Max, Rate
|
|
12
|
+
from kafka.protocol.consumer import FetchRequest
|
|
13
|
+
from kafka.protocol.consumer import (
|
|
14
|
+
ListOffsetsRequest, OffsetForLeaderEpochRequest,
|
|
15
|
+
OffsetSpec, UNKNOWN_OFFSET, IsolationLevel,
|
|
16
|
+
)
|
|
17
|
+
from kafka.record import MemoryRecords
|
|
18
|
+
from kafka.serializer import Deserializer, DeserializeWrapper
|
|
19
|
+
from kafka.structs import TopicPartition, OffsetAndMetadata, OffsetAndTimestamp
|
|
20
|
+
from kafka.util import Timer
|
|
21
|
+
|
|
22
|
+
log = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
_LOGGED_DESERIALIZE_WARNING = False
|
|
25
|
+
|
|
26
|
+
ConsumerRecord = collections.namedtuple("ConsumerRecord",
|
|
27
|
+
["topic", "partition", "leader_epoch", "offset", "timestamp", "timestamp_type",
|
|
28
|
+
"key", "value", "headers", "checksum", "serialized_key_size", "serialized_value_size", "serialized_header_size"])
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
CompletedFetch = collections.namedtuple("CompletedFetch",
|
|
32
|
+
["topic_partition", "fetched_offset", "response_version",
|
|
33
|
+
"partition_data", "metric_aggregator"])
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
ExceptionMetadata = collections.namedtuple("ExceptionMetadata",
|
|
37
|
+
["partition", "fetched_offset", "exception"])
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
_FetchTopic = FetchRequest.FetchTopic
|
|
41
|
+
_FetchPartition = _FetchTopic.FetchPartition
|
|
42
|
+
_ForgottenTopic = FetchRequest.ForgottenTopic
|
|
43
|
+
_ListOffsetsTopic = ListOffsetsRequest.ListOffsetsTopic
|
|
44
|
+
_ListOffsetsPartition = _ListOffsetsTopic.ListOffsetsPartition
|
|
45
|
+
_OffsetForLeaderTopic = OffsetForLeaderEpochRequest.OffsetForLeaderTopic
|
|
46
|
+
_OffsetForLeaderPartition = _OffsetForLeaderTopic.OffsetForLeaderPartition
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class RecordTooLargeError(Errors.KafkaError):
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Fetcher:
|
|
54
|
+
DEFAULT_CONFIG = {
|
|
55
|
+
'key_deserializer': None,
|
|
56
|
+
'value_deserializer': None,
|
|
57
|
+
'fetch_min_bytes': 1,
|
|
58
|
+
'fetch_max_wait_ms': 500,
|
|
59
|
+
'fetch_max_bytes': 52428800,
|
|
60
|
+
'max_partition_fetch_bytes': 1048576,
|
|
61
|
+
'max_poll_records': sys.maxsize,
|
|
62
|
+
'check_crcs': True,
|
|
63
|
+
'metrics': None,
|
|
64
|
+
'metric_group_prefix': 'consumer',
|
|
65
|
+
'request_timeout_ms': 30000,
|
|
66
|
+
'retry_backoff_ms': 100,
|
|
67
|
+
'enable_incremental_fetch_sessions': True,
|
|
68
|
+
'isolation_level': 'read_uncommitted',
|
|
69
|
+
'client_rack': '',
|
|
70
|
+
'metadata_max_age_ms': 5 * 60 * 1000,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
def __init__(self, client, subscriptions, **configs):
|
|
74
|
+
"""Initialize a Kafka Message Fetcher.
|
|
75
|
+
|
|
76
|
+
Keyword Arguments:
|
|
77
|
+
key_deserializer (kafka.serializer.Deserializer): Takes a
|
|
78
|
+
raw message key and returns a deserialized key.
|
|
79
|
+
Default: None.
|
|
80
|
+
value_deserializer (kafka.serializer.Deserializer): Takes a
|
|
81
|
+
raw message value and returns a deserialized value.
|
|
82
|
+
Default: None.
|
|
83
|
+
enable_incremental_fetch_sessions: (bool): Use incremental fetch sessions
|
|
84
|
+
when available / supported by kafka broker. See KIP-227. Default: True.
|
|
85
|
+
fetch_min_bytes (int): Minimum amount of data the server should
|
|
86
|
+
return for a fetch request, otherwise wait up to
|
|
87
|
+
fetch_max_wait_ms for more data to accumulate. Default: 1.
|
|
88
|
+
fetch_max_wait_ms (int): The maximum amount of time in milliseconds
|
|
89
|
+
the server will block before answering the fetch request if
|
|
90
|
+
there isn't sufficient data to immediately satisfy the
|
|
91
|
+
requirement given by fetch_min_bytes. Default: 500.
|
|
92
|
+
fetch_max_bytes (int): The maximum amount of data the server should
|
|
93
|
+
return for a fetch request. This is not an absolute maximum, if
|
|
94
|
+
the first message in the first non-empty partition of the fetch
|
|
95
|
+
is larger than this value, the message will still be returned
|
|
96
|
+
to ensure that the consumer can make progress. NOTE: consumer
|
|
97
|
+
performs fetches to multiple brokers in parallel so memory
|
|
98
|
+
usage will depend on the number of brokers containing
|
|
99
|
+
partitions for the topic.
|
|
100
|
+
Supported Kafka version >= 0.10.1.0. Default: 52428800 (50 MB).
|
|
101
|
+
max_partition_fetch_bytes (int): The maximum amount of data
|
|
102
|
+
per-partition the server will return. The maximum total memory
|
|
103
|
+
used for a request = #partitions * max_partition_fetch_bytes.
|
|
104
|
+
This size must be at least as large as the maximum message size
|
|
105
|
+
the server allows or else it is possible for the producer to
|
|
106
|
+
send messages larger than the consumer can fetch. If that
|
|
107
|
+
happens, the consumer can get stuck trying to fetch a large
|
|
108
|
+
message on a certain partition. Default: 1048576.
|
|
109
|
+
check_crcs (bool): Automatically check the CRC32 of the records
|
|
110
|
+
consumed. This ensures no on-the-wire or on-disk corruption to
|
|
111
|
+
the messages occurred. This check adds some overhead, so it may
|
|
112
|
+
be disabled in cases seeking extreme performance. Default: True
|
|
113
|
+
isolation_level (str): Configure KIP-98 transactional consumer by
|
|
114
|
+
setting to 'read_committed'. This will cause the consumer to
|
|
115
|
+
skip records from aborted tranactions. Default: 'read_uncommitted'
|
|
116
|
+
"""
|
|
117
|
+
self.config = copy.copy(self.DEFAULT_CONFIG)
|
|
118
|
+
for key in self.config:
|
|
119
|
+
if key in configs:
|
|
120
|
+
self.config[key] = configs[key]
|
|
121
|
+
|
|
122
|
+
for key in ('key_deserializer', 'value_deserializer'):
|
|
123
|
+
if self.config[key] is not None and not isinstance(self.config[key], Deserializer):
|
|
124
|
+
warnings.warn('%s does not implement kafka.serializer.Deserializer' % (key,), category=DeprecationWarning, stacklevel=3)
|
|
125
|
+
self.config[key] = DeserializeWrapper(self.config[key])
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
self._isolation_level = IsolationLevel.build_from(self.config['isolation_level'])
|
|
129
|
+
except ValueError:
|
|
130
|
+
raise Errors.KafkaConfigurationError('Unrecognized isolation_level') from None
|
|
131
|
+
|
|
132
|
+
self._client = client
|
|
133
|
+
self._manager = client._manager
|
|
134
|
+
self._net = self._manager._net
|
|
135
|
+
self._subscriptions = subscriptions
|
|
136
|
+
self._completed_fetches = collections.deque() # Unparsed responses
|
|
137
|
+
self._next_partition_records = None # Holds a single PartitionRecords until fully consumed
|
|
138
|
+
self._paused_completed_fetches = {} # tp -> CompletedFetch (raw)
|
|
139
|
+
self._paused_partition_records = {} # tp -> PartitionRecords (parsed)
|
|
140
|
+
self._iterator = None
|
|
141
|
+
self._fetch_futures = collections.deque()
|
|
142
|
+
if self.config['metrics']:
|
|
143
|
+
self._sensors = FetchManagerMetrics(self.config['metrics'], self.config['metric_group_prefix'])
|
|
144
|
+
else:
|
|
145
|
+
self._sensors = None
|
|
146
|
+
self._session_handlers = {}
|
|
147
|
+
self._nodes_with_pending_fetch_requests = set()
|
|
148
|
+
self._cached_list_offsets_exception = None
|
|
149
|
+
self._next_in_line_exception_metadata = None
|
|
150
|
+
# In-flight offset-reset Task, cached across reset_offsets_if_needed
|
|
151
|
+
# calls so concurrent callers (consumer.poll fire-and-forget,
|
|
152
|
+
# consumer.position blocking-await) share one fan-out instead of
|
|
153
|
+
# racing duplicate ListOffsets requests.
|
|
154
|
+
self._reset_task = None
|
|
155
|
+
# KIP-320 offset validation: same caching pattern, separate from
|
|
156
|
+
# reset (a partition can be awaiting-reset OR awaiting-validation,
|
|
157
|
+
# never both - awaiting-validation requires a valid position).
|
|
158
|
+
self._validation_task = None
|
|
159
|
+
self._cached_log_truncation = None
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def _enable_incremental_fetch_sessions(self):
|
|
163
|
+
if self._manager.broker_version is None or self._manager.broker_version < (1, 1):
|
|
164
|
+
return False
|
|
165
|
+
return self.config['enable_incremental_fetch_sessions']
|
|
166
|
+
|
|
167
|
+
def fetch_records(self, max_records=None, update_offsets=True, timeout_ms=None):
|
|
168
|
+
"""Drain buffered records, pipeline next fetches, and wait briefly
|
|
169
|
+
for in-flight responses if no records are immediately available.
|
|
170
|
+
|
|
171
|
+
Single-call replacement for the legacy
|
|
172
|
+
``fetched_records -> send_fetches -> client.poll -> fetched_records``
|
|
173
|
+
loop in :meth:`KafkaConsumer._poll_once`. The caller no longer
|
|
174
|
+
drives the event loop; the wait happens inside this method via a
|
|
175
|
+
wakeup Future fired by any in-flight fetch's completion callback.
|
|
176
|
+
|
|
177
|
+
Arguments:
|
|
178
|
+
max_records (int, optional): cap on returned records.
|
|
179
|
+
update_offsets (bool): advance subscription positions for
|
|
180
|
+
consumed records.
|
|
181
|
+
timeout_ms (int, optional): wall-clock cap on the wait phase.
|
|
182
|
+
Only applies when no records are immediately available.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
tuple[dict[TopicPartition, list[ConsumerRecord]], bool]:
|
|
186
|
+
``(records, idle)``. ``idle`` is True when there were no
|
|
187
|
+
buffered records, no in-flight fetches, and no pending
|
|
188
|
+
offset-reset task -- i.e. nothing this fetcher could wait
|
|
189
|
+
on. Callers in that state should sleep before retrying
|
|
190
|
+
instead of busy-looping.
|
|
191
|
+
"""
|
|
192
|
+
# Drain whatever's already buffered from prior fetch responses.
|
|
193
|
+
records, partial = self.fetched_records(
|
|
194
|
+
max_records, update_offsets=update_offsets)
|
|
195
|
+
if not partial:
|
|
196
|
+
# No buffered records remaining; send next batch of fetch requests.
|
|
197
|
+
self.send_fetches()
|
|
198
|
+
|
|
199
|
+
if records:
|
|
200
|
+
return records, False
|
|
201
|
+
|
|
202
|
+
# No records yet. Block until either an in-flight fetch
|
|
203
|
+
# completes (records may have arrived) or a pending offset-reset
|
|
204
|
+
# task completes (positions become available, enabling a fetch
|
|
205
|
+
# on the next caller iteration). add_both fires synchronously on
|
|
206
|
+
# already-done futures, closing the race where a future resolves
|
|
207
|
+
# between scheduling and the wait setup.
|
|
208
|
+
waited_on = list(self._fetch_futures)
|
|
209
|
+
if self._reset_task is not None and not self._reset_task.is_done:
|
|
210
|
+
waited_on.append(self._reset_task)
|
|
211
|
+
if not waited_on:
|
|
212
|
+
return records, True # nothing pending; caller should sleep
|
|
213
|
+
|
|
214
|
+
wakeup = Future()
|
|
215
|
+
def _wake(_):
|
|
216
|
+
if not wakeup.is_done:
|
|
217
|
+
wakeup.success(None)
|
|
218
|
+
for fut in waited_on:
|
|
219
|
+
fut.add_both(_wake)
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
self._net.run(self._manager.wait_for, wakeup, timeout_ms)
|
|
223
|
+
except Errors.KafkaTimeoutError:
|
|
224
|
+
pass
|
|
225
|
+
|
|
226
|
+
records, _ = self.fetched_records(
|
|
227
|
+
max_records, update_offsets=update_offsets)
|
|
228
|
+
return records, False
|
|
229
|
+
|
|
230
|
+
def send_fetches(self):
|
|
231
|
+
"""Send FetchRequests for all assigned partitions that do not already have
|
|
232
|
+
an in-flight fetch or pending fetch data.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
List of Futures: each future resolves to a FetchResponse
|
|
236
|
+
"""
|
|
237
|
+
return self._manager.run(self._send_fetches_async)
|
|
238
|
+
|
|
239
|
+
async def _send_fetches_async(self):
|
|
240
|
+
futures = []
|
|
241
|
+
for node_id, (request, fetch_offsets) in self._create_fetch_requests().items():
|
|
242
|
+
log.debug("Sending FetchRequest to node %s", node_id)
|
|
243
|
+
self._nodes_with_pending_fetch_requests.add(node_id)
|
|
244
|
+
future = self._manager.send(request, node_id=node_id)
|
|
245
|
+
future.add_callback(self._handle_fetch_response, node_id, fetch_offsets, time.monotonic())
|
|
246
|
+
future.add_errback(self._handle_fetch_error, node_id)
|
|
247
|
+
future.add_both(self._clear_pending_fetch_request, node_id)
|
|
248
|
+
futures.append(future)
|
|
249
|
+
self._fetch_futures.extend(futures)
|
|
250
|
+
self._clean_done_fetch_futures()
|
|
251
|
+
return futures
|
|
252
|
+
|
|
253
|
+
def _clean_done_fetch_futures(self):
|
|
254
|
+
while True:
|
|
255
|
+
if not self._fetch_futures:
|
|
256
|
+
break
|
|
257
|
+
if not self._fetch_futures[0].is_done:
|
|
258
|
+
break
|
|
259
|
+
self._fetch_futures.popleft()
|
|
260
|
+
|
|
261
|
+
def in_flight_fetches(self):
|
|
262
|
+
"""Return True if there are any unprocessed FetchRequests in flight."""
|
|
263
|
+
self._clean_done_fetch_futures()
|
|
264
|
+
return bool(self._fetch_futures)
|
|
265
|
+
|
|
266
|
+
def reset_offsets_if_needed(self, timeout_ms=None):
|
|
267
|
+
"""Schedule pending offset resets and return the in-flight Task.
|
|
268
|
+
|
|
269
|
+
Returns the cached Future for the in-flight reset task (shared
|
|
270
|
+
across concurrent callers) or None if no reset is needed. Callers
|
|
271
|
+
may discard the Future (fire-and-forget, e.g. consumer.poll) or
|
|
272
|
+
await it via ``manager.wait_for(future, timeout_ms)`` to block
|
|
273
|
+
until resets complete (e.g. consumer.position).
|
|
274
|
+
|
|
275
|
+
Arguments:
|
|
276
|
+
timeout_ms (int, optional): Maximum wall-clock the reset task
|
|
277
|
+
should run, including time spent awaiting metadata refresh
|
|
278
|
+
for unknown leaders. If None, uses ``request_timeout_ms``
|
|
279
|
+
as a default upper bound so a permanently-unresolvable
|
|
280
|
+
partition (deleted topic, etc.) doesn't spin forever. The
|
|
281
|
+
first caller's timeout wins for the cached task; later
|
|
282
|
+
callers' bounds are enforced via their own ``wait_for`` on
|
|
283
|
+
the returned Future.
|
|
284
|
+
|
|
285
|
+
Raises:
|
|
286
|
+
NoOffsetForPartitionError: if a previous reset attempt left a
|
|
287
|
+
cached non-retriable exception.
|
|
288
|
+
"""
|
|
289
|
+
# Raise exception from previous offset fetch if there is one
|
|
290
|
+
exc, self._cached_list_offsets_exception = self._cached_list_offsets_exception, None
|
|
291
|
+
if exc:
|
|
292
|
+
raise exc
|
|
293
|
+
|
|
294
|
+
if self._reset_task is not None and not self._reset_task.is_done:
|
|
295
|
+
return self._reset_task
|
|
296
|
+
|
|
297
|
+
if not self._subscriptions.partitions_needing_reset():
|
|
298
|
+
return None
|
|
299
|
+
|
|
300
|
+
self._reset_task = self._manager.call_soon(
|
|
301
|
+
self._reset_offsets_async, timeout_ms)
|
|
302
|
+
return self._reset_task
|
|
303
|
+
|
|
304
|
+
def offsets_by_times(self, timestamps, timeout_ms=None):
|
|
305
|
+
"""Fetch offset for each partition passed in ``timestamps`` map.
|
|
306
|
+
|
|
307
|
+
Blocks until offsets are obtained, a non-retriable exception is raised
|
|
308
|
+
or ``timeout_ms`` passed.
|
|
309
|
+
|
|
310
|
+
Arguments:
|
|
311
|
+
timestamps: {TopicPartition: int} dict with timestamps to fetch
|
|
312
|
+
offsets by. -1 for the latest available, -2 for the earliest
|
|
313
|
+
available. Otherwise timestamp is treated as epoch milliseconds.
|
|
314
|
+
timeout_ms (int, optional): The maximum time in milliseconds to block.
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
{TopicPartition: OffsetAndTimestamp or None}: Mapping of partition to
|
|
318
|
+
retrieved offset, timestamp, and leader_epoch. If offset does not
|
|
319
|
+
exist for the provided timestamp, the value for the TopicPartition
|
|
320
|
+
will be None.
|
|
321
|
+
|
|
322
|
+
Raises:
|
|
323
|
+
KafkaTimeoutError if timeout_ms provided
|
|
324
|
+
"""
|
|
325
|
+
offsets = self._net.run(self._fetch_offsets_by_times_async, timestamps, timeout_ms)
|
|
326
|
+
for tp in timestamps:
|
|
327
|
+
if tp not in offsets:
|
|
328
|
+
offsets[tp] = None
|
|
329
|
+
return offsets
|
|
330
|
+
|
|
331
|
+
async def _fetch_offsets_by_times_async(self, timestamps, timeout_ms=None):
|
|
332
|
+
"""Fetch offsets for each partition in timestamps dict. This may send
|
|
333
|
+
request to multiple nodes, based on who is Leader for partition.
|
|
334
|
+
|
|
335
|
+
Per-node requests are dispatched concurrently; if any fails, the first
|
|
336
|
+
exception encountered propagates and the remaining results are dropped.
|
|
337
|
+
|
|
338
|
+
Arguments:
|
|
339
|
+
timestamps (dict): {TopicPartition: int} mapping of partitions to
|
|
340
|
+
timestamps or OffsetSpec sentinels.
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
(fetched_offsets, partitions_to_retry):
|
|
344
|
+
dict[TopicPartition, OffsetAndTimestamp],
|
|
345
|
+
set[TopicPartition]
|
|
346
|
+
|
|
347
|
+
Raises:
|
|
348
|
+
KafkaTimeoutError: if offsets cannot be fully fetched before timeout_ms
|
|
349
|
+
"""
|
|
350
|
+
if not timestamps:
|
|
351
|
+
return {}
|
|
352
|
+
|
|
353
|
+
timer = Timer(timeout_ms, "Failed to get offsets by timestamps in %s ms" % (timeout_ms,))
|
|
354
|
+
timestamps = copy.copy(timestamps)
|
|
355
|
+
fetched_offsets = dict()
|
|
356
|
+
while True:
|
|
357
|
+
if not timestamps:
|
|
358
|
+
return {}
|
|
359
|
+
|
|
360
|
+
future = self._manager.call_soon(self._send_list_offsets_requests, timestamps)
|
|
361
|
+
try:
|
|
362
|
+
refresh_future = None
|
|
363
|
+
backoff = False
|
|
364
|
+
offsets, retry = await self._manager.wait_for(future, timer.timeout_ms)
|
|
365
|
+
except Errors.InvalidMetadataError:
|
|
366
|
+
refresh_future = self._manager.cluster.request_update()
|
|
367
|
+
except Errors.RetriableError:
|
|
368
|
+
if self._manager.cluster.need_update:
|
|
369
|
+
refresh_future = self._manager.cluster.request_update()
|
|
370
|
+
else:
|
|
371
|
+
backoff = True
|
|
372
|
+
else:
|
|
373
|
+
fetched_offsets.update(offsets)
|
|
374
|
+
if not retry:
|
|
375
|
+
return fetched_offsets
|
|
376
|
+
timestamps = {tp: timestamps[tp] for tp in retry}
|
|
377
|
+
|
|
378
|
+
if refresh_future:
|
|
379
|
+
try:
|
|
380
|
+
await self._manager.wait_for(refresh_future, timer.timeout_ms)
|
|
381
|
+
except Errors.RetriableError:
|
|
382
|
+
backoff = True
|
|
383
|
+
|
|
384
|
+
if backoff:
|
|
385
|
+
delay = self.config['retry_backoff_ms'] / 1000
|
|
386
|
+
if timer.timeout_ms is not None:
|
|
387
|
+
delay = min(delay, timer.timeout_ms / 1000)
|
|
388
|
+
await self._manager._net.sleep(delay)
|
|
389
|
+
|
|
390
|
+
timer.maybe_raise()
|
|
391
|
+
|
|
392
|
+
def beginning_offsets(self, partitions, timeout_ms=None):
|
|
393
|
+
"""Fetch earliest (oldest) offset for each partition.
|
|
394
|
+
|
|
395
|
+
Blocks until offsets are obtained, a non-retriable exception is raised
|
|
396
|
+
or ``timeout_ms`` passed.
|
|
397
|
+
|
|
398
|
+
Arguments:
|
|
399
|
+
partitions ([TopicPartition]): List of partitions for list offsets.
|
|
400
|
+
timeout_ms (int, optional): The maximum time in milliseconds to block.
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
{TopicPartition: int}: Mapping of partition to retrieved offset.
|
|
404
|
+
|
|
405
|
+
Raises:
|
|
406
|
+
KafkaTimeoutError if timeout_ms provided.
|
|
407
|
+
"""
|
|
408
|
+
return self.beginning_or_end_offset(
|
|
409
|
+
partitions, OffsetSpec.EARLIEST, timeout_ms)
|
|
410
|
+
|
|
411
|
+
def end_offsets(self, partitions, timeout_ms=None):
|
|
412
|
+
"""Fetch latest (most recent) offset for each partition.
|
|
413
|
+
|
|
414
|
+
Blocks until offsets are obtained, a non-retriable exception is raised
|
|
415
|
+
or ``timeout_ms`` passed.
|
|
416
|
+
|
|
417
|
+
Arguments:
|
|
418
|
+
partitions ([TopicPartition]): List of partitions for list offsets.
|
|
419
|
+
timeout_ms (int, optional): The maximum time in milliseconds to block.
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
{TopicPartition: int}: Mapping of partition to retrieved offset.
|
|
423
|
+
|
|
424
|
+
Raises:
|
|
425
|
+
KafkaTimeoutError if timeout_ms provided.
|
|
426
|
+
"""
|
|
427
|
+
return self.beginning_or_end_offset(
|
|
428
|
+
partitions, OffsetSpec.LATEST, timeout_ms)
|
|
429
|
+
|
|
430
|
+
def beginning_or_end_offset(self, partitions, timestamp, timeout_ms=None):
|
|
431
|
+
"""Fetch offset for each partition using ``timestamp``.
|
|
432
|
+
|
|
433
|
+
Blocks until offsets are obtained, a non-retriable exception is raised
|
|
434
|
+
or ``timeout_ms`` passed.
|
|
435
|
+
|
|
436
|
+
Arguments:
|
|
437
|
+
partitions ([TopicPartition]): List of partitions for list offsets.
|
|
438
|
+
timestamp (int or OffsetSpec): OffsetSpec.LATEST (-1) for the latest
|
|
439
|
+
available, OffsetSpec.EARLIEST (-2) for the earliest available.
|
|
440
|
+
Otherwise timestamp is treated as epoch milliseconds.
|
|
441
|
+
timeout_ms (int, optional): The maximum time in milliseconds to block.
|
|
442
|
+
|
|
443
|
+
Returns:
|
|
444
|
+
{TopicPartition: int}: Mapping of partition to retrieved offset.
|
|
445
|
+
|
|
446
|
+
Raises:
|
|
447
|
+
UnsupportedVersionError if broker does not support any compatible
|
|
448
|
+
ListOffsetsRequest api version.
|
|
449
|
+
KafkaTimeoutError if timeout_ms provided.
|
|
450
|
+
"""
|
|
451
|
+
timestamps = dict([(tp, timestamp) for tp in partitions])
|
|
452
|
+
offsets = self._net.run(self._fetch_offsets_by_times_async, timestamps, timeout_ms)
|
|
453
|
+
for tp in timestamps:
|
|
454
|
+
offsets[tp] = offsets[tp].offset
|
|
455
|
+
return offsets
|
|
456
|
+
|
|
457
|
+
def fetched_records(self, max_records=None, update_offsets=True):
|
|
458
|
+
"""Returns previously fetched records and updates consumed offsets.
|
|
459
|
+
|
|
460
|
+
Arguments:
|
|
461
|
+
max_records (int): Maximum number of records returned. Defaults
|
|
462
|
+
to max_poll_records configuration.
|
|
463
|
+
|
|
464
|
+
Raises:
|
|
465
|
+
OffsetOutOfRangeError: if no subscription offset_reset_strategy
|
|
466
|
+
CorruptRecordError: if message crc validation fails (check_crcs
|
|
467
|
+
must be set to True)
|
|
468
|
+
RecordTooLargeError: if a message is larger than the currently
|
|
469
|
+
configured max_partition_fetch_bytes
|
|
470
|
+
TopicAuthorizationError: if consumer is not authorized to fetch
|
|
471
|
+
messages from the topic
|
|
472
|
+
ValueError: if max_records is <= 0
|
|
473
|
+
|
|
474
|
+
Returns: (records (dict), partial (bool))
|
|
475
|
+
records: {TopicPartition: [messages]}
|
|
476
|
+
partial: True if records returned did not fully drain any pending
|
|
477
|
+
partition requests. This may be useful for choosing when to
|
|
478
|
+
pipeline additional fetch requests.
|
|
479
|
+
"""
|
|
480
|
+
if max_records is None:
|
|
481
|
+
max_records = self.config['max_poll_records']
|
|
482
|
+
if max_records <= 0:
|
|
483
|
+
raise ValueError('max_records must be > 0')
|
|
484
|
+
|
|
485
|
+
if self._next_in_line_exception_metadata is not None:
|
|
486
|
+
exc_meta = self._next_in_line_exception_metadata
|
|
487
|
+
self._next_in_line_exception_metadata = None
|
|
488
|
+
tp = exc_meta.partition
|
|
489
|
+
if self._subscriptions.is_fetchable(tp) and self._subscriptions.position(tp).offset == exc_meta.fetched_offset:
|
|
490
|
+
raise exc_meta.exception
|
|
491
|
+
|
|
492
|
+
drained = collections.defaultdict(list)
|
|
493
|
+
records_remaining = max_records
|
|
494
|
+
# Needed to construct ExceptionMetadata if any exception is found when processing completed_fetch
|
|
495
|
+
fetched_partition = None
|
|
496
|
+
fetched_offset = -1
|
|
497
|
+
|
|
498
|
+
# KAFKA-7548: restore parked data for any partition that the user
|
|
499
|
+
# has since resumed. Raw completions go back into the fetch queue;
|
|
500
|
+
# parsed records take the in-line slot when free, otherwise stay
|
|
501
|
+
# parked and get picked up on a subsequent call.
|
|
502
|
+
for tp in list(self._paused_completed_fetches):
|
|
503
|
+
if not self._subscriptions.is_paused(tp):
|
|
504
|
+
self._completed_fetches.append(self._paused_completed_fetches.pop(tp))
|
|
505
|
+
if self._next_partition_records is None:
|
|
506
|
+
for tp in list(self._paused_partition_records):
|
|
507
|
+
if not self._subscriptions.is_paused(tp):
|
|
508
|
+
self._next_partition_records = self._paused_partition_records.pop(tp)
|
|
509
|
+
break
|
|
510
|
+
|
|
511
|
+
try:
|
|
512
|
+
while records_remaining > 0:
|
|
513
|
+
if not self._next_partition_records:
|
|
514
|
+
if not self._completed_fetches:
|
|
515
|
+
break
|
|
516
|
+
completion = self._completed_fetches.popleft()
|
|
517
|
+
if self._subscriptions.is_paused(completion.topic_partition):
|
|
518
|
+
self._paused_completed_fetches[completion.topic_partition] = completion
|
|
519
|
+
continue
|
|
520
|
+
fetched_partition = completion.topic_partition
|
|
521
|
+
fetched_offset = completion.fetched_offset
|
|
522
|
+
self._next_partition_records = self._parse_fetched_data(completion)
|
|
523
|
+
else:
|
|
524
|
+
tp = self._next_partition_records.topic_partition
|
|
525
|
+
if self._subscriptions.is_paused(tp):
|
|
526
|
+
self._paused_partition_records[tp] = self._next_partition_records
|
|
527
|
+
self._next_partition_records = None
|
|
528
|
+
continue
|
|
529
|
+
fetched_partition = tp
|
|
530
|
+
fetched_offset = self._next_partition_records.next_fetch_offset
|
|
531
|
+
records_remaining -= self._append(drained,
|
|
532
|
+
self._next_partition_records,
|
|
533
|
+
records_remaining,
|
|
534
|
+
update_offsets)
|
|
535
|
+
except Exception as e:
|
|
536
|
+
if not drained:
|
|
537
|
+
raise e
|
|
538
|
+
# To be thrown in the next call of this method
|
|
539
|
+
self._next_in_line_exception_metadata = ExceptionMetadata(fetched_partition, fetched_offset, e)
|
|
540
|
+
return dict(drained), bool(self._completed_fetches)
|
|
541
|
+
|
|
542
|
+
def _append(self, drained, part, max_records, update_offsets):
|
|
543
|
+
if not part:
|
|
544
|
+
return 0
|
|
545
|
+
|
|
546
|
+
tp = part.topic_partition
|
|
547
|
+
if not self._subscriptions.is_assigned(tp):
|
|
548
|
+
# this can happen when a rebalance happened before
|
|
549
|
+
# fetched records are returned to the consumer's poll call
|
|
550
|
+
log.debug("Not returning fetched records for partition %s"
|
|
551
|
+
" since it is no longer assigned", tp)
|
|
552
|
+
elif not self._subscriptions.is_fetchable(tp):
|
|
553
|
+
# this can happen when a partition is paused before
|
|
554
|
+
# fetched records are returned to the consumer's poll call
|
|
555
|
+
log.debug("Not returning fetched records for assigned partition"
|
|
556
|
+
" %s since it is no longer fetchable", tp)
|
|
557
|
+
|
|
558
|
+
else:
|
|
559
|
+
# note that the position should always be available
|
|
560
|
+
# as long as the partition is still assigned
|
|
561
|
+
position = self._subscriptions.assignment[tp].position
|
|
562
|
+
if part.next_fetch_offset == position.offset:
|
|
563
|
+
log.debug("Returning fetched records at offset %d for assigned"
|
|
564
|
+
" partition %s", position.offset, tp)
|
|
565
|
+
part_records = part.take(max_records)
|
|
566
|
+
# list.extend([]) is a noop, but because drained is a defaultdict
|
|
567
|
+
# we should avoid initializing the default list unless there are records
|
|
568
|
+
if part_records:
|
|
569
|
+
drained[tp].extend(part_records)
|
|
570
|
+
# We want to increment subscription position if (1) we're using consumer.poll(),
|
|
571
|
+
# or (2) we didn't return any records (consumer iterator will update position
|
|
572
|
+
# when each message is yielded). There may be edge cases where we re-fetch records
|
|
573
|
+
# that we'll end up skipping, but for now we'll live with that.
|
|
574
|
+
highwater = self._subscriptions.assignment[tp].highwater
|
|
575
|
+
if highwater is not None and self._sensors:
|
|
576
|
+
self._sensors.records_fetch_lag.record(highwater - part.next_fetch_offset)
|
|
577
|
+
if update_offsets or not part_records:
|
|
578
|
+
log.debug("Updating fetch position for assigned partition %s to %s (leader epoch %s)",
|
|
579
|
+
tp, part.next_fetch_offset, part.leader_epoch)
|
|
580
|
+
self._subscriptions.assignment[tp].position = OffsetAndMetadata(
|
|
581
|
+
part.next_fetch_offset, '', part.leader_epoch)
|
|
582
|
+
return len(part_records)
|
|
583
|
+
|
|
584
|
+
else:
|
|
585
|
+
# these records aren't next in line based on the last consumed
|
|
586
|
+
# position, ignore them they must be from an obsolete request
|
|
587
|
+
log.debug("Ignoring fetched records for %s at offset %s since"
|
|
588
|
+
" the current position is %d", tp, part.next_fetch_offset,
|
|
589
|
+
position.offset)
|
|
590
|
+
|
|
591
|
+
part.drain()
|
|
592
|
+
return 0
|
|
593
|
+
|
|
594
|
+
def _reset_offset_if_needed(self, partition, timestamp, offset):
|
|
595
|
+
# we might lose the assignment while fetching the offset, or the user might seek to a different offset,
|
|
596
|
+
# so verify it is still assigned and still in need of the requested reset
|
|
597
|
+
if not self._subscriptions.is_assigned(partition):
|
|
598
|
+
log.debug("Skipping reset of partition %s since it is no longer assigned", partition)
|
|
599
|
+
elif not self._subscriptions.is_offset_reset_needed(partition):
|
|
600
|
+
log.debug("Skipping reset of partition %s since reset is no longer needed", partition)
|
|
601
|
+
elif timestamp and not timestamp == self._subscriptions.assignment[partition].reset_strategy:
|
|
602
|
+
log.debug("Skipping reset of partition %s since an alternative reset has been requested", partition)
|
|
603
|
+
else:
|
|
604
|
+
log.info("Resetting offset for partition %s to offset %s.", partition, offset)
|
|
605
|
+
self._subscriptions.seek(partition, offset)
|
|
606
|
+
|
|
607
|
+
async def _reset_offsets_async(self, timeout_ms=None):
|
|
608
|
+
"""Drive resets to completion or until the timer expires.
|
|
609
|
+
|
|
610
|
+
Each iteration fans out per-node ListOffsets requests concurrently
|
|
611
|
+
and awaits all of them. After a retriable failure (NotLeader, etc.)
|
|
612
|
+
a partition's next_allowed_retry_time is set ``retry_backoff_ms`` in
|
|
613
|
+
the future; the loop sleeps until that time and retries rather than
|
|
614
|
+
relying on an external caller to redrive. If all partitions have
|
|
615
|
+
unknown leaders, awaits a metadata refresh and retries within the
|
|
616
|
+
remaining budget.
|
|
617
|
+
|
|
618
|
+
Arguments:
|
|
619
|
+
timeout_ms (int, optional): Hard upper bound on the loop's
|
|
620
|
+
wall-clock. None falls back to ``request_timeout_ms`` so a
|
|
621
|
+
deleted-topic / permanently-unknown-leader partition can't
|
|
622
|
+
spin the loop forever. The metadata-refresh wait inside
|
|
623
|
+
the loop is capped by ``min(remaining_timer, request_timeout_ms)``.
|
|
624
|
+
|
|
625
|
+
Per-node failures are caught inside _reset_offsets_for_node and
|
|
626
|
+
stuffed into self._cached_list_offsets_exception; the next call to
|
|
627
|
+
reset_offsets_if_needed surfaces them.
|
|
628
|
+
"""
|
|
629
|
+
if timeout_ms is None:
|
|
630
|
+
timeout_ms = self.config['request_timeout_ms']
|
|
631
|
+
timer = Timer(timeout_ms)
|
|
632
|
+
while not timer.expired:
|
|
633
|
+
if self._cached_list_offsets_exception is not None:
|
|
634
|
+
return
|
|
635
|
+
partitions = self._subscriptions.partitions_needing_reset()
|
|
636
|
+
if not partitions:
|
|
637
|
+
next_retry = self._subscriptions.next_offset_reset_retry_time()
|
|
638
|
+
if next_retry is None:
|
|
639
|
+
return
|
|
640
|
+
delay = max(0.0, next_retry - time.monotonic())
|
|
641
|
+
if timer.timeout_ms is not None:
|
|
642
|
+
delay = min(delay, timer.timeout_ms / 1000)
|
|
643
|
+
if delay > 0:
|
|
644
|
+
await self._manager._net.sleep(delay)
|
|
645
|
+
continue
|
|
646
|
+
|
|
647
|
+
offset_resets = {}
|
|
648
|
+
for tp in partitions:
|
|
649
|
+
ts = self._subscriptions.assignment[tp].reset_strategy
|
|
650
|
+
if ts:
|
|
651
|
+
offset_resets[tp] = ts
|
|
652
|
+
if not offset_resets:
|
|
653
|
+
return
|
|
654
|
+
|
|
655
|
+
timestamps_by_node = self._group_list_offset_requests(offset_resets)
|
|
656
|
+
if not timestamps_by_node:
|
|
657
|
+
# All requested partitions have unknown / unavailable leaders.
|
|
658
|
+
# _group_list_offset_requests has already requested a metadata
|
|
659
|
+
# refresh; await it within the remaining budget (capped at
|
|
660
|
+
# request_timeout_ms for any single broker round-trip).
|
|
661
|
+
metadata_update = self._manager.cluster.request_update()
|
|
662
|
+
wait_ms = self.config['request_timeout_ms']
|
|
663
|
+
if timer.timeout_ms is not None:
|
|
664
|
+
wait_ms = min(wait_ms, timer.timeout_ms)
|
|
665
|
+
try:
|
|
666
|
+
await self._manager.wait_for(metadata_update, wait_ms)
|
|
667
|
+
except Errors.KafkaTimeoutError:
|
|
668
|
+
pass
|
|
669
|
+
continue
|
|
670
|
+
|
|
671
|
+
log.debug('Resetting offsets for %s', set(offset_resets.keys()))
|
|
672
|
+
# Gather: schedule all per-node tasks concurrently, then await.
|
|
673
|
+
node_tasks = []
|
|
674
|
+
for node_id, t_and_e in timestamps_by_node.items():
|
|
675
|
+
node_partitions = set(t_and_e.keys())
|
|
676
|
+
expire_at = time.monotonic() + self.config['request_timeout_ms'] / 1000
|
|
677
|
+
self._subscriptions.set_reset_pending(node_partitions, expire_at)
|
|
678
|
+
node_tasks.append(self._manager.call_soon(
|
|
679
|
+
self._reset_offsets_for_node, node_id, t_and_e, node_partitions))
|
|
680
|
+
for task in node_tasks:
|
|
681
|
+
await task
|
|
682
|
+
|
|
683
|
+
async def _reset_offsets_for_node(self, node_id, timestamps_and_epochs, partitions):
|
|
684
|
+
try:
|
|
685
|
+
fetched_offsets, partitions_to_retry = await self._send_list_offsets_request(node_id, timestamps_and_epochs)
|
|
686
|
+
except Exception as error:
|
|
687
|
+
self._subscriptions.reset_failed(partitions, time.monotonic() + self.config['retry_backoff_ms'] / 1000)
|
|
688
|
+
self._manager.cluster.request_update()
|
|
689
|
+
if not isinstance(error, Errors.RetriableError):
|
|
690
|
+
if not self._cached_list_offsets_exception:
|
|
691
|
+
self._cached_list_offsets_exception = error
|
|
692
|
+
else:
|
|
693
|
+
log.error("Discarding error in ListOffsetResponse because another error is pending: %s", error)
|
|
694
|
+
return
|
|
695
|
+
|
|
696
|
+
if partitions_to_retry:
|
|
697
|
+
self._subscriptions.reset_failed(partitions_to_retry, time.monotonic() + self.config['retry_backoff_ms'] / 1000)
|
|
698
|
+
self._manager.cluster.request_update()
|
|
699
|
+
for partition, offset in fetched_offsets.items():
|
|
700
|
+
ts, _epoch = timestamps_and_epochs[partition]
|
|
701
|
+
self._reset_offset_if_needed(partition, ts, offset.offset)
|
|
702
|
+
|
|
703
|
+
async def _send_list_offsets_requests(self, timestamps):
|
|
704
|
+
"""Fetch offsets for each partition in timestamps dict. This may send
|
|
705
|
+
request to multiple nodes, based on who is Leader for partition.
|
|
706
|
+
|
|
707
|
+
Per-node requests are dispatched concurrently; if any fails, the first
|
|
708
|
+
exception encountered propagates and the remaining results are dropped.
|
|
709
|
+
|
|
710
|
+
Arguments:
|
|
711
|
+
timestamps (dict): {TopicPartition: int} mapping of fetching
|
|
712
|
+
timestamps.
|
|
713
|
+
|
|
714
|
+
Returns:
|
|
715
|
+
(fetched_offsets, partitions_to_retry):
|
|
716
|
+
dict[TopicPartition, OffsetAndTimestamp],
|
|
717
|
+
set[TopicPartition]
|
|
718
|
+
|
|
719
|
+
Raises:
|
|
720
|
+
StaleMetadata: if no node has known leader for any partition.
|
|
721
|
+
"""
|
|
722
|
+
timestamps_by_node = self._group_list_offset_requests(timestamps)
|
|
723
|
+
if not timestamps_by_node:
|
|
724
|
+
raise Errors.StaleMetadata()
|
|
725
|
+
|
|
726
|
+
futures = [
|
|
727
|
+
self._manager.call_soon(self._send_list_offsets_request, node_id, ts)
|
|
728
|
+
for node_id, ts in timestamps_by_node.items()
|
|
729
|
+
]
|
|
730
|
+
|
|
731
|
+
fetched_offsets = dict()
|
|
732
|
+
partitions_to_retry = set()
|
|
733
|
+
for f in futures:
|
|
734
|
+
offs, retry = await f
|
|
735
|
+
fetched_offsets.update(offs)
|
|
736
|
+
partitions_to_retry.update(retry)
|
|
737
|
+
return fetched_offsets, partitions_to_retry
|
|
738
|
+
|
|
739
|
+
def _group_list_offset_requests(self, timestamps):
|
|
740
|
+
timestamps_by_node = collections.defaultdict(dict)
|
|
741
|
+
for partition, timestamp in timestamps.items():
|
|
742
|
+
node_id = self._manager.cluster.leader_for_partition(partition)
|
|
743
|
+
if node_id is None:
|
|
744
|
+
self._manager.cluster.add_topic(partition.topic)
|
|
745
|
+
log.debug("Partition %s is unknown for fetching offset", partition)
|
|
746
|
+
self._manager.cluster.request_update()
|
|
747
|
+
elif node_id == -1:
|
|
748
|
+
log.debug("Leader for partition %s unavailable for fetching "
|
|
749
|
+
"offset, wait for metadata refresh", partition)
|
|
750
|
+
self._manager.cluster.request_update()
|
|
751
|
+
else:
|
|
752
|
+
leader_epoch = -1
|
|
753
|
+
timestamps_by_node[node_id][partition] = (timestamp, leader_epoch)
|
|
754
|
+
return dict(timestamps_by_node)
|
|
755
|
+
|
|
756
|
+
async def _send_list_offsets_request(self, node_id, timestamps_and_epochs):
|
|
757
|
+
"""Send single ListOffsetsResponse to node_id
|
|
758
|
+
|
|
759
|
+
Returns:
|
|
760
|
+
(fetched_offsets, partitions_to_retry):
|
|
761
|
+
dict[TopicPartition, OffsetAndTimestamp],
|
|
762
|
+
set[TopicPartition]
|
|
763
|
+
|
|
764
|
+
Raises:
|
|
765
|
+
TopicAuthorizationFailedError: if any topic returned an auth error
|
|
766
|
+
"""
|
|
767
|
+
min_version = 1 if any(res[0] >= 0 for res in timestamps_and_epochs.values()) else 0
|
|
768
|
+
min_version = max(min_version, ListOffsetsRequest.min_version_for_isolation_level(self._isolation_level))
|
|
769
|
+
by_topic = collections.defaultdict(list)
|
|
770
|
+
for tp, (timestamp, leader_epoch) in timestamps_and_epochs.items():
|
|
771
|
+
data = _ListOffsetsPartition(
|
|
772
|
+
partition_index=tp.partition,
|
|
773
|
+
current_leader_epoch=leader_epoch,
|
|
774
|
+
timestamp=timestamp)
|
|
775
|
+
by_topic[tp.topic].append(data)
|
|
776
|
+
|
|
777
|
+
request = ListOffsetsRequest(
|
|
778
|
+
isolation_level=self._isolation_level,
|
|
779
|
+
topics=list(by_topic.items()),
|
|
780
|
+
min_version=min_version,
|
|
781
|
+
)
|
|
782
|
+
|
|
783
|
+
log.debug("Sending ListOffsetRequest %s to broker %s", request, node_id)
|
|
784
|
+
response = await self._manager.send(request, node_id=node_id)
|
|
785
|
+
return self._handle_list_offsets_response(response)
|
|
786
|
+
|
|
787
|
+
def _handle_list_offsets_response(self, response):
|
|
788
|
+
"""Parse a ListOffsets response.
|
|
789
|
+
|
|
790
|
+
Returns:
|
|
791
|
+
(fetched_offsets, partitions_to_retry):
|
|
792
|
+
dict[TopicPartition, OffsetAndTimestamp],
|
|
793
|
+
set[TopicPartition]
|
|
794
|
+
|
|
795
|
+
Raises:
|
|
796
|
+
TopicAuthorizationFailedError: if any topic returned an auth error
|
|
797
|
+
ValueError: if ListOffsetsResponse v0 and > 1 offset returned
|
|
798
|
+
"""
|
|
799
|
+
fetched_offsets = dict()
|
|
800
|
+
partitions_to_retry = set()
|
|
801
|
+
unauthorized_topics = set()
|
|
802
|
+
for topic_data in response.topics:
|
|
803
|
+
for partition_info in topic_data.partitions:
|
|
804
|
+
tp = TopicPartition(topic_data.name, partition_info.partition_index)
|
|
805
|
+
error_code = partition_info.error_code
|
|
806
|
+
error_type = Errors.for_code(error_code)
|
|
807
|
+
if error_type is Errors.NoError:
|
|
808
|
+
if response.API_VERSION == 0:
|
|
809
|
+
offsets = partition_info.old_style_offsets
|
|
810
|
+
if len(offsets) > 1:
|
|
811
|
+
raise ValueError('Expected ListOffsetsResponse with one offset')
|
|
812
|
+
offset = offsets[0] if offsets else UNKNOWN_OFFSET
|
|
813
|
+
else:
|
|
814
|
+
offset = partition_info.offset
|
|
815
|
+
timestamp = partition_info.timestamp
|
|
816
|
+
leader_epoch = partition_info.leader_epoch
|
|
817
|
+
# DataContainer currently does not set default for
|
|
818
|
+
# out-of-version fields; so we need to handle explicitly
|
|
819
|
+
if timestamp is None:
|
|
820
|
+
timestamp = -1
|
|
821
|
+
if leader_epoch is None:
|
|
822
|
+
leader_epoch = -1
|
|
823
|
+
log.debug("Handling ListOffsetsResponse response for %s. "
|
|
824
|
+
"Fetched offset %s, timestamp %s, leader_epoch %s",
|
|
825
|
+
tp, offset, timestamp, leader_epoch)
|
|
826
|
+
if offset != UNKNOWN_OFFSET:
|
|
827
|
+
fetched_offsets[tp] = OffsetAndTimestamp(offset, timestamp, leader_epoch)
|
|
828
|
+
elif error_type is Errors.UnsupportedForMessageFormatError:
|
|
829
|
+
# The message format on the broker side is before 0.10.0, which means it does not
|
|
830
|
+
# support timestamps. We treat this case the same as if we weren't able to find an
|
|
831
|
+
# offset corresponding to the requested timestamp and leave it out of the result.
|
|
832
|
+
log.debug("Cannot search by timestamp for partition %s because the"
|
|
833
|
+
" message format version is before 0.10.0", tp)
|
|
834
|
+
elif error_type in (Errors.NotLeaderForPartitionError,
|
|
835
|
+
Errors.ReplicaNotAvailableError,
|
|
836
|
+
Errors.KafkaStorageError,
|
|
837
|
+
Errors.OffsetNotAvailableError,
|
|
838
|
+
Errors.LeaderNotAvailableError):
|
|
839
|
+
log.debug("Attempt to fetch offsets for partition %s failed due"
|
|
840
|
+
" to %s, retrying.", error_type.__name__, tp)
|
|
841
|
+
partitions_to_retry.add(tp)
|
|
842
|
+
elif error_type is Errors.UnknownTopicOrPartitionError:
|
|
843
|
+
log.warning("Received unknown topic or partition error in ListOffsets "
|
|
844
|
+
"request for partition %s. The topic/partition " +
|
|
845
|
+
"may not exist or the user may not have Describe access "
|
|
846
|
+
"to it.", tp)
|
|
847
|
+
partitions_to_retry.add(tp)
|
|
848
|
+
elif error_type is Errors.TopicAuthorizationFailedError:
|
|
849
|
+
unauthorized_topics.add(tp.topic)
|
|
850
|
+
else:
|
|
851
|
+
log.warning("Attempt to fetch offsets for partition %s failed due to:"
|
|
852
|
+
" %s", tp, error_type.__name__)
|
|
853
|
+
partitions_to_retry.add(tp)
|
|
854
|
+
if unauthorized_topics:
|
|
855
|
+
raise Errors.TopicAuthorizationFailedError(unauthorized_topics)
|
|
856
|
+
return fetched_offsets, partitions_to_retry
|
|
857
|
+
|
|
858
|
+
# ------------------------------------------------------------------
|
|
859
|
+
# KIP-320: offset validation via OffsetForLeaderEpoch
|
|
860
|
+
# ------------------------------------------------------------------
|
|
861
|
+
|
|
862
|
+
def maybe_validate_positions(self):
|
|
863
|
+
"""Walk assigned partitions; mark any whose cluster leader epoch has
|
|
864
|
+
advanced beyond the position's epoch as awaiting validation.
|
|
865
|
+
|
|
866
|
+
Cheap fire-and-forget marker; the actual RPC fan-out runs in
|
|
867
|
+
``validate_offsets_if_needed`` -> ``_validate_offsets_async``.
|
|
868
|
+
Idempotent: partitions already awaiting validation, awaiting
|
|
869
|
+
reset, or with no recorded epoch are skipped inside
|
|
870
|
+
``maybe_validate_position``.
|
|
871
|
+
"""
|
|
872
|
+
for tp in self._subscriptions.assigned_partitions():
|
|
873
|
+
current_epoch = self._manager.cluster.leader_epoch_for_partition(tp)
|
|
874
|
+
self._subscriptions.maybe_validate_position_for_current_leader(tp, current_epoch)
|
|
875
|
+
|
|
876
|
+
def validate_offsets_if_needed(self, timeout_ms=None):
|
|
877
|
+
"""Schedule any pending position validations and return the in-flight Task.
|
|
878
|
+
|
|
879
|
+
Mirrors :meth:`reset_offsets_if_needed`: returns a cached Future
|
|
880
|
+
shared across callers so concurrent ``consumer.poll`` and
|
|
881
|
+
``consumer.position`` callers don't race the same partition into
|
|
882
|
+
duplicate OffsetForLeaderEpoch requests.
|
|
883
|
+
|
|
884
|
+
Raises:
|
|
885
|
+
LogTruncationError: if a previous validation detected truncation
|
|
886
|
+
on one or more partitions. The exception is cleared after
|
|
887
|
+
being raised so subsequent calls will re-attempt validation.
|
|
888
|
+
"""
|
|
889
|
+
exc, self._cached_log_truncation = self._cached_log_truncation, None
|
|
890
|
+
if exc:
|
|
891
|
+
raise exc
|
|
892
|
+
|
|
893
|
+
if self._validation_task is not None and not self._validation_task.is_done:
|
|
894
|
+
return self._validation_task
|
|
895
|
+
|
|
896
|
+
if not self._subscriptions.partitions_needing_validation():
|
|
897
|
+
return None
|
|
898
|
+
|
|
899
|
+
self._validation_task = self._manager.call_soon(
|
|
900
|
+
self._validate_offsets_async, timeout_ms)
|
|
901
|
+
return self._validation_task
|
|
902
|
+
|
|
903
|
+
async def _validate_offsets_async(self, timeout_ms=None):
|
|
904
|
+
"""Drive offset validations to completion or until the timer expires.
|
|
905
|
+
|
|
906
|
+
Same overall shape as ``_reset_offsets_async``: per-node fan-out.
|
|
907
|
+
After a retriable failure (FencedLeaderEpoch, etc.) a partition's
|
|
908
|
+
next_allowed_retry_time is set ``retry_backoff_ms`` in the future;
|
|
909
|
+
the loop sleeps until that time and retries rather than relying on
|
|
910
|
+
an external caller to redrive. Stops on first ``LogTruncationError``
|
|
911
|
+
accumulation; the next caller surfaces it.
|
|
912
|
+
"""
|
|
913
|
+
if timeout_ms is None:
|
|
914
|
+
timeout_ms = self.config['request_timeout_ms']
|
|
915
|
+
timer = Timer(timeout_ms)
|
|
916
|
+
while not timer.expired:
|
|
917
|
+
if self._cached_log_truncation is not None:
|
|
918
|
+
return
|
|
919
|
+
partitions = self._subscriptions.partitions_needing_validation()
|
|
920
|
+
if not partitions:
|
|
921
|
+
next_retry = self._subscriptions.next_offset_validation_retry_time()
|
|
922
|
+
if next_retry is None:
|
|
923
|
+
return
|
|
924
|
+
delay = max(0.0, next_retry - time.monotonic())
|
|
925
|
+
if timer.timeout_ms is not None:
|
|
926
|
+
delay = min(delay, timer.timeout_ms / 1000)
|
|
927
|
+
if delay > 0:
|
|
928
|
+
await self._manager._net.sleep(delay)
|
|
929
|
+
continue
|
|
930
|
+
|
|
931
|
+
positions = {}
|
|
932
|
+
for tp in partitions:
|
|
933
|
+
state = self._subscriptions.assignment[tp]
|
|
934
|
+
if state.position is not None and state.position.leader_epoch >= 0:
|
|
935
|
+
positions[tp] = state.position
|
|
936
|
+
if not positions:
|
|
937
|
+
return
|
|
938
|
+
|
|
939
|
+
requests_by_node = self._group_offset_for_leader_epoch_requests(positions)
|
|
940
|
+
if not requests_by_node:
|
|
941
|
+
metadata_update = self._manager.cluster.request_update()
|
|
942
|
+
wait_ms = self.config['request_timeout_ms']
|
|
943
|
+
if timer.timeout_ms is not None:
|
|
944
|
+
wait_ms = min(wait_ms, timer.timeout_ms)
|
|
945
|
+
try:
|
|
946
|
+
await self._manager.wait_for(metadata_update, wait_ms)
|
|
947
|
+
except Errors.KafkaTimeoutError:
|
|
948
|
+
pass
|
|
949
|
+
continue
|
|
950
|
+
|
|
951
|
+
log.debug('Validating offsets for %s', set(positions.keys()))
|
|
952
|
+
node_tasks = []
|
|
953
|
+
for node_id, payload in requests_by_node.items():
|
|
954
|
+
node_partitions = set(payload.keys())
|
|
955
|
+
expire_at = time.monotonic() + self.config['request_timeout_ms'] / 1000
|
|
956
|
+
self._subscriptions.set_validation_pending(node_partitions, expire_at)
|
|
957
|
+
node_tasks.append(self._manager.call_soon(
|
|
958
|
+
self._validate_offsets_for_node, node_id, payload))
|
|
959
|
+
for task in node_tasks:
|
|
960
|
+
await task
|
|
961
|
+
|
|
962
|
+
async def _validate_offsets_for_node(self, node_id, partitions_to_positions):
|
|
963
|
+
try:
|
|
964
|
+
truncations = await self._send_offset_for_leader_epoch_request(
|
|
965
|
+
node_id, partitions_to_positions)
|
|
966
|
+
except Exception as error:
|
|
967
|
+
self._subscriptions.validation_failed(
|
|
968
|
+
set(partitions_to_positions),
|
|
969
|
+
time.monotonic() + self.config['retry_backoff_ms'] / 1000)
|
|
970
|
+
self._manager.cluster.request_update()
|
|
971
|
+
if not isinstance(error, Errors.RetriableError):
|
|
972
|
+
log.error("Non-retriable error from OffsetForLeaderEpoch on node %s: %s",
|
|
973
|
+
node_id, error)
|
|
974
|
+
return
|
|
975
|
+
|
|
976
|
+
if truncations:
|
|
977
|
+
if self._cached_log_truncation is None:
|
|
978
|
+
self._cached_log_truncation = Errors.LogTruncationError(truncations)
|
|
979
|
+
else:
|
|
980
|
+
self._cached_log_truncation.divergent_offsets.update(truncations)
|
|
981
|
+
|
|
982
|
+
def _group_offset_for_leader_epoch_requests(self, positions):
|
|
983
|
+
"""Group {TopicPartition: OffsetAndMetadata} by leader node.
|
|
984
|
+
|
|
985
|
+
Partitions whose leader is unknown trigger a metadata refresh and
|
|
986
|
+
are dropped from this round. Partitions whose position lacks an
|
|
987
|
+
epoch are also dropped - they can't be validated.
|
|
988
|
+
"""
|
|
989
|
+
by_node = collections.defaultdict(dict)
|
|
990
|
+
for tp, position in positions.items():
|
|
991
|
+
if position.leader_epoch < 0:
|
|
992
|
+
continue
|
|
993
|
+
node_id = self._manager.cluster.leader_for_partition(tp)
|
|
994
|
+
if node_id is None:
|
|
995
|
+
self._manager.cluster.add_topic(tp.topic)
|
|
996
|
+
self._manager.cluster.request_update()
|
|
997
|
+
elif node_id == -1:
|
|
998
|
+
self._manager.cluster.request_update()
|
|
999
|
+
else:
|
|
1000
|
+
by_node[node_id][tp] = position
|
|
1001
|
+
return dict(by_node)
|
|
1002
|
+
|
|
1003
|
+
async def _send_offset_for_leader_epoch_request(self, node_id, partitions_to_positions):
|
|
1004
|
+
"""Send one OffsetForLeaderEpoch request and return any truncations.
|
|
1005
|
+
|
|
1006
|
+
Returns:
|
|
1007
|
+
dict[TopicPartition, OffsetAndMetadata]: partitions whose log
|
|
1008
|
+
was truncated past their position. Successful validations
|
|
1009
|
+
update :class:`SubscriptionState` directly via
|
|
1010
|
+
``complete_validation``; retriable per-partition errors leave
|
|
1011
|
+
``next_allowed_retry_time`` set so the outer loop will retry.
|
|
1012
|
+
|
|
1013
|
+
Raises:
|
|
1014
|
+
TopicAuthorizationFailedError: if any topic returned an auth error.
|
|
1015
|
+
"""
|
|
1016
|
+
by_topic = collections.defaultdict(list)
|
|
1017
|
+
for tp, position in partitions_to_positions.items():
|
|
1018
|
+
current_leader_epoch = self._manager.cluster.leader_epoch_for_partition(tp)
|
|
1019
|
+
if current_leader_epoch is None or current_leader_epoch < 0:
|
|
1020
|
+
current_leader_epoch = -1
|
|
1021
|
+
by_topic[tp.topic].append(_OffsetForLeaderPartition(
|
|
1022
|
+
partition=tp.partition,
|
|
1023
|
+
current_leader_epoch=current_leader_epoch,
|
|
1024
|
+
leader_epoch=position.leader_epoch,
|
|
1025
|
+
))
|
|
1026
|
+
|
|
1027
|
+
request = OffsetForLeaderEpochRequest(
|
|
1028
|
+
replica_id=-1,
|
|
1029
|
+
topics=list(by_topic.items()),
|
|
1030
|
+
)
|
|
1031
|
+
|
|
1032
|
+
log.debug("Sending OffsetForLeaderEpochRequest %s to broker %s", request, node_id)
|
|
1033
|
+
response = await self._manager.send(request, node_id=node_id)
|
|
1034
|
+
return self._handle_offset_for_leader_epoch_response(response, partitions_to_positions)
|
|
1035
|
+
|
|
1036
|
+
def _handle_offset_for_leader_epoch_response(self, response, requested_positions):
|
|
1037
|
+
"""Parse an OffsetForLeaderEpoch response.
|
|
1038
|
+
|
|
1039
|
+
Side effects: calls ``complete_validation`` / ``validation_failed``
|
|
1040
|
+
/ ``request_position_validation`` on the subscription state as
|
|
1041
|
+
appropriate for each partition's response code.
|
|
1042
|
+
|
|
1043
|
+
Returns:
|
|
1044
|
+
dict[TopicPartition, OffsetAndMetadata]: subset of requested
|
|
1045
|
+
partitions where end_offset < requested position (truncation).
|
|
1046
|
+
"""
|
|
1047
|
+
truncations = {}
|
|
1048
|
+
unauthorized_topics = set()
|
|
1049
|
+
retry_at = time.monotonic() + self.config['retry_backoff_ms'] / 1000
|
|
1050
|
+
retry = set()
|
|
1051
|
+
|
|
1052
|
+
for topic_data in response.topics:
|
|
1053
|
+
for partition_info in topic_data.partitions:
|
|
1054
|
+
tp = TopicPartition(topic_data.topic, partition_info.partition)
|
|
1055
|
+
requested = requested_positions.get(tp)
|
|
1056
|
+
if requested is None:
|
|
1057
|
+
continue
|
|
1058
|
+
error_type = Errors.for_code(partition_info.error_code)
|
|
1059
|
+
|
|
1060
|
+
if error_type is Errors.NoError:
|
|
1061
|
+
end_offset = partition_info.end_offset
|
|
1062
|
+
end_epoch = partition_info.leader_epoch
|
|
1063
|
+
if end_epoch is None:
|
|
1064
|
+
end_epoch = -1
|
|
1065
|
+
current = self._subscriptions.assignment[tp].position if \
|
|
1066
|
+
self._subscriptions.is_assigned(tp) else None
|
|
1067
|
+
# Position may have changed (seek, rebalance) since request
|
|
1068
|
+
# was sent; skip stale completions.
|
|
1069
|
+
if current is None or current != requested:
|
|
1070
|
+
log.debug("Skipping validation completion for %s: position "
|
|
1071
|
+
"changed since request was sent", tp)
|
|
1072
|
+
continue
|
|
1073
|
+
|
|
1074
|
+
has_reset_policy = self._subscriptions.has_default_offset_reset_policy()
|
|
1075
|
+
|
|
1076
|
+
if end_offset < 0 or end_epoch < 0:
|
|
1077
|
+
# UNDEFINED_EPOCH / UNDEFINED_EPOCH_OFFSET: broker has
|
|
1078
|
+
# no record of our requested epoch on this partition.
|
|
1079
|
+
# Mirror Java SubscriptionState.maybeCompleteValidation:
|
|
1080
|
+
# this is truncation with no known diverging offset.
|
|
1081
|
+
if has_reset_policy:
|
|
1082
|
+
log.info("Truncation detected for %s at position %s "
|
|
1083
|
+
"(broker returned UNDEFINED end_offset/leader_epoch); "
|
|
1084
|
+
"resetting offset per auto_offset_reset policy",
|
|
1085
|
+
tp, current.offset)
|
|
1086
|
+
self._subscriptions.request_offset_reset(tp)
|
|
1087
|
+
else:
|
|
1088
|
+
log.warning("Truncation detected for %s at position %s "
|
|
1089
|
+
"(broker returned UNDEFINED end_offset/leader_epoch), "
|
|
1090
|
+
"but no reset policy is set", tp, current.offset)
|
|
1091
|
+
truncations[tp] = None
|
|
1092
|
+
self._subscriptions.complete_validation(tp)
|
|
1093
|
+
elif end_offset < current.offset:
|
|
1094
|
+
# Broker confirms the diverging point. Seek there
|
|
1095
|
+
# directly instead of resetting via policy, so the
|
|
1096
|
+
# consumer only re-reads records past the divergence
|
|
1097
|
+
# (Java: state.seekValidated(newPosition)).
|
|
1098
|
+
divergent = OffsetAndMetadata(end_offset, '', end_epoch)
|
|
1099
|
+
if has_reset_policy:
|
|
1100
|
+
log.info("Truncation detected for %s at position %s; "
|
|
1101
|
+
"seeking to first diverging offset %s",
|
|
1102
|
+
tp, current.offset, divergent)
|
|
1103
|
+
self._subscriptions.seek(tp, divergent)
|
|
1104
|
+
else:
|
|
1105
|
+
log.warning("Truncation detected for %s at position %s "
|
|
1106
|
+
"(first diverging offset is %s), but no reset "
|
|
1107
|
+
"policy is set", tp, current.offset, divergent)
|
|
1108
|
+
truncations[tp] = divergent
|
|
1109
|
+
self._subscriptions.complete_validation(tp)
|
|
1110
|
+
else:
|
|
1111
|
+
validated = OffsetAndMetadata(
|
|
1112
|
+
current.offset, current.metadata, end_epoch)
|
|
1113
|
+
self._subscriptions.complete_validation(tp, validated)
|
|
1114
|
+
|
|
1115
|
+
elif error_type in (Errors.FencedLeaderEpochError,
|
|
1116
|
+
Errors.UnknownLeaderEpochError,
|
|
1117
|
+
Errors.NotLeaderForPartitionError,
|
|
1118
|
+
Errors.ReplicaNotAvailableError,
|
|
1119
|
+
Errors.KafkaStorageError,
|
|
1120
|
+
Errors.LeaderNotAvailableError):
|
|
1121
|
+
log.debug("OffsetForLeaderEpoch for %s returned retriable %s; "
|
|
1122
|
+
"will retry after backoff", tp, error_type.__name__)
|
|
1123
|
+
self._manager.cluster.request_update()
|
|
1124
|
+
retry.add(tp)
|
|
1125
|
+
elif error_type is Errors.UnknownTopicOrPartitionError:
|
|
1126
|
+
log.warning("OffsetForLeaderEpoch for %s: unknown topic/partition", tp)
|
|
1127
|
+
retry.add(tp)
|
|
1128
|
+
elif error_type is Errors.TopicAuthorizationFailedError:
|
|
1129
|
+
unauthorized_topics.add(tp.topic)
|
|
1130
|
+
else:
|
|
1131
|
+
log.warning("OffsetForLeaderEpoch for %s failed with %s",
|
|
1132
|
+
tp, error_type.__name__)
|
|
1133
|
+
retry.add(tp)
|
|
1134
|
+
|
|
1135
|
+
if retry:
|
|
1136
|
+
self._subscriptions.validation_failed(retry, retry_at)
|
|
1137
|
+
if unauthorized_topics:
|
|
1138
|
+
raise Errors.TopicAuthorizationFailedError(unauthorized_topics)
|
|
1139
|
+
return truncations
|
|
1140
|
+
|
|
1141
|
+
def _fetchable_partitions(self):
|
|
1142
|
+
fetchable = self._subscriptions.fetchable_partitions()
|
|
1143
|
+
# do not fetch a partition if we have a pending fetch response to process
|
|
1144
|
+
# use copy to avoid runtimeerror on mutation from different thread
|
|
1145
|
+
discard = {fetch.topic_partition for fetch in self._completed_fetches.copy()}
|
|
1146
|
+
current = self._next_partition_records
|
|
1147
|
+
if current:
|
|
1148
|
+
discard.add(current.topic_partition)
|
|
1149
|
+
discard.update(self._paused_completed_fetches)
|
|
1150
|
+
discard.update(self._paused_partition_records)
|
|
1151
|
+
return [tp for tp in fetchable if tp not in discard]
|
|
1152
|
+
|
|
1153
|
+
def _select_read_replica(self, tp):
|
|
1154
|
+
"""Pick the node to fetch from for ``tp``: a cached preferred read
|
|
1155
|
+
replica (KIP-392) when valid and *still listed as a replica of
|
|
1156
|
+
``tp``*, otherwise the partition leader. A preferred replica that
|
|
1157
|
+
has been demoted out of the partition's replica set (or fell out
|
|
1158
|
+
of cluster metadata entirely) is cleared so the next fetch goes
|
|
1159
|
+
to the leader.
|
|
1160
|
+
"""
|
|
1161
|
+
preferred = self._subscriptions.assignment[tp].preferred_read_replica()
|
|
1162
|
+
if preferred is None:
|
|
1163
|
+
return self._manager.cluster.leader_for_partition(tp)
|
|
1164
|
+
if not self._manager.cluster.is_replica_node(tp, preferred):
|
|
1165
|
+
self._subscriptions.assignment[tp].clear_preferred_read_replica()
|
|
1166
|
+
leader = self._manager.cluster.leader_for_partition(tp)
|
|
1167
|
+
log.debug("Preferred read replica %s for partition %s no longer"
|
|
1168
|
+
" online or no longer a replica; falling back to leader %s",
|
|
1169
|
+
preferred, tp, leader)
|
|
1170
|
+
return leader
|
|
1171
|
+
return preferred
|
|
1172
|
+
|
|
1173
|
+
def _create_fetch_requests(self):
|
|
1174
|
+
"""Create fetch requests for all assigned partitions, grouped by node.
|
|
1175
|
+
|
|
1176
|
+
FetchRequests skipped if no leader, or node has requests in flight
|
|
1177
|
+
|
|
1178
|
+
Returns:
|
|
1179
|
+
dict: {node_id: (FetchRequest, {TopicPartition: fetch_offset}), ...}
|
|
1180
|
+
"""
|
|
1181
|
+
# TODO:
|
|
1182
|
+
# v13 topic ids (KIP-516)
|
|
1183
|
+
# v14 tiered storage (KIP-405)
|
|
1184
|
+
# v15 replica state (KIP-903)
|
|
1185
|
+
# v16 node endpoints (KIP-951)
|
|
1186
|
+
# v17 directory id (KIP-853)
|
|
1187
|
+
max_version = 12
|
|
1188
|
+
fetchable = collections.defaultdict(collections.OrderedDict)
|
|
1189
|
+
for tp in self._fetchable_partitions():
|
|
1190
|
+
node_id = self._select_read_replica(tp)
|
|
1191
|
+
|
|
1192
|
+
position = self._subscriptions.assignment[tp].position
|
|
1193
|
+
|
|
1194
|
+
# fetch if there is a leader and no in-flight requests
|
|
1195
|
+
if node_id is None or node_id == -1:
|
|
1196
|
+
log.debug("No leader found for partition %s."
|
|
1197
|
+
" Requesting metadata update", tp)
|
|
1198
|
+
self._manager.cluster.request_update()
|
|
1199
|
+
|
|
1200
|
+
elif self._manager.connection_delay(node_id) > 0:
|
|
1201
|
+
# If we try to send during the reconnect backoff window, then the request is just
|
|
1202
|
+
# going to be failed anyway before being sent, so skip the send for now
|
|
1203
|
+
log.debug("Skipping fetch for partition %s because node %s is awaiting reconnect backoff",
|
|
1204
|
+
tp, node_id)
|
|
1205
|
+
|
|
1206
|
+
# TODO: handle throttle_delay in kafka.net
|
|
1207
|
+
elif self._client.throttle_delay(node_id) > 0:
|
|
1208
|
+
# If we try to send while throttled, then the request is just
|
|
1209
|
+
# going to be failed anyway before being sent, so skip the send for now
|
|
1210
|
+
log.debug("Skipping fetch for partition %s because node %s is throttled",
|
|
1211
|
+
tp, node_id)
|
|
1212
|
+
|
|
1213
|
+
elif node_id in self._nodes_with_pending_fetch_requests:
|
|
1214
|
+
log.debug("Skipping fetch for partition %s because there is a pending fetch request to node %s",
|
|
1215
|
+
tp, node_id)
|
|
1216
|
+
|
|
1217
|
+
else:
|
|
1218
|
+
# Leader is connected and does not have a pending fetch request.
|
|
1219
|
+
# current_leader_epoch (v9+) = metadata view (broker fencing);
|
|
1220
|
+
# last_fetched_epoch (v12+) = record view (broker divergence
|
|
1221
|
+
# detection). They differ once leadership advances past the
|
|
1222
|
+
# record at the fetch offset.
|
|
1223
|
+
current_leader_epoch = self._manager.cluster.leader_epoch_for_partition(tp)
|
|
1224
|
+
if current_leader_epoch is None:
|
|
1225
|
+
current_leader_epoch = -1
|
|
1226
|
+
partition_info = _FetchPartition(
|
|
1227
|
+
partition=tp.partition,
|
|
1228
|
+
current_leader_epoch=current_leader_epoch,
|
|
1229
|
+
fetch_offset=position.offset,
|
|
1230
|
+
last_fetched_epoch=position.leader_epoch,
|
|
1231
|
+
partition_max_bytes=self.config['max_partition_fetch_bytes']
|
|
1232
|
+
)
|
|
1233
|
+
fetchable[node_id][tp] = partition_info
|
|
1234
|
+
log.debug("Adding fetch request for partition %s at offset %d",
|
|
1235
|
+
tp, position.offset)
|
|
1236
|
+
|
|
1237
|
+
requests = {}
|
|
1238
|
+
for node_id, next_partitions in fetchable.items():
|
|
1239
|
+
if self._enable_incremental_fetch_sessions:
|
|
1240
|
+
if node_id not in self._session_handlers:
|
|
1241
|
+
self._session_handlers[node_id] = FetchSessionHandler(node_id)
|
|
1242
|
+
session = self._session_handlers[node_id].build_next(next_partitions)
|
|
1243
|
+
else:
|
|
1244
|
+
# No incremental fetch support
|
|
1245
|
+
session = FetchRequestData(next_partitions, None, FetchMetadata.LEGACY)
|
|
1246
|
+
|
|
1247
|
+
min_version = FetchRequest.min_version_for_isolation_level(self._isolation_level)
|
|
1248
|
+
request = FetchRequest(
|
|
1249
|
+
max_wait_ms=self.config['fetch_max_wait_ms'],
|
|
1250
|
+
min_bytes=self.config['fetch_min_bytes'],
|
|
1251
|
+
max_bytes=self.config['fetch_max_bytes'],
|
|
1252
|
+
isolation_level=self._isolation_level,
|
|
1253
|
+
session_id=session.id,
|
|
1254
|
+
session_epoch=session.epoch,
|
|
1255
|
+
topics=session.to_send,
|
|
1256
|
+
forgotten_topics_data=session.to_forget,
|
|
1257
|
+
rack_id=self.config['client_rack'],
|
|
1258
|
+
min_version=min_version,
|
|
1259
|
+
max_version=max_version,
|
|
1260
|
+
)
|
|
1261
|
+
|
|
1262
|
+
fetch_offsets = {tp: next_partitions[tp].fetch_offset for tp in next_partitions}
|
|
1263
|
+
requests[node_id] = (request, fetch_offsets)
|
|
1264
|
+
|
|
1265
|
+
return requests
|
|
1266
|
+
|
|
1267
|
+
def _handle_fetch_response(self, node_id, fetch_offsets, send_time, response):
|
|
1268
|
+
"""The callback for fetch completion"""
|
|
1269
|
+
if response.API_VERSION >= 7 and self._enable_incremental_fetch_sessions:
|
|
1270
|
+
if node_id not in self._session_handlers:
|
|
1271
|
+
log.error("Unable to find fetch session handler for node %s. Ignoring fetch response", node_id)
|
|
1272
|
+
return
|
|
1273
|
+
if not self._session_handlers[node_id].handle_response(response):
|
|
1274
|
+
return
|
|
1275
|
+
|
|
1276
|
+
partitions = set([
|
|
1277
|
+
TopicPartition(
|
|
1278
|
+
topic_data.topic,
|
|
1279
|
+
partition_data.partition_index)
|
|
1280
|
+
for topic_data in response.responses
|
|
1281
|
+
for partition_data in topic_data.partitions
|
|
1282
|
+
])
|
|
1283
|
+
if self._sensors:
|
|
1284
|
+
metric_aggregator = FetchResponseMetricAggregator(self._sensors, partitions)
|
|
1285
|
+
else:
|
|
1286
|
+
metric_aggregator = None
|
|
1287
|
+
|
|
1288
|
+
for topic_data in response.responses:
|
|
1289
|
+
for partition_data in topic_data.partitions:
|
|
1290
|
+
tp = TopicPartition(
|
|
1291
|
+
topic_data.topic,
|
|
1292
|
+
partition_data.partition_index
|
|
1293
|
+
)
|
|
1294
|
+
fetch_offset = fetch_offsets[tp]
|
|
1295
|
+
completed_fetch = CompletedFetch(
|
|
1296
|
+
tp, fetch_offset,
|
|
1297
|
+
response.API_VERSION,
|
|
1298
|
+
partition_data,
|
|
1299
|
+
metric_aggregator
|
|
1300
|
+
)
|
|
1301
|
+
self._completed_fetches.append(completed_fetch)
|
|
1302
|
+
|
|
1303
|
+
if self._sensors:
|
|
1304
|
+
self._sensors.fetch_latency.record((time.monotonic() - send_time) * 1000)
|
|
1305
|
+
|
|
1306
|
+
def _handle_fetch_error(self, node_id, exception):
|
|
1307
|
+
level = logging.INFO if isinstance(exception, Errors.Cancelled) else logging.ERROR
|
|
1308
|
+
log.log(level, 'Fetch to node %s failed: %s', node_id, exception)
|
|
1309
|
+
if node_id in self._session_handlers:
|
|
1310
|
+
self._session_handlers[node_id].handle_error(exception)
|
|
1311
|
+
|
|
1312
|
+
def _clear_pending_fetch_request(self, node_id, _):
|
|
1313
|
+
try:
|
|
1314
|
+
self._nodes_with_pending_fetch_requests.remove(node_id)
|
|
1315
|
+
except KeyError:
|
|
1316
|
+
pass
|
|
1317
|
+
|
|
1318
|
+
def _maybe_update_current_leader(self, tp, partition_data):
|
|
1319
|
+
"""Apply a KIP-951 ``current_leader`` hint from a Fetch v12+ response.
|
|
1320
|
+
|
|
1321
|
+
Updates the cluster's cached leader id/epoch when the broker advertises
|
|
1322
|
+
a newer leader. If the new leader id is not yet a known broker (v12 has
|
|
1323
|
+
no ``node_endpoints``), requests a metadata refresh so the consumer
|
|
1324
|
+
learns its address.
|
|
1325
|
+
"""
|
|
1326
|
+
leader = partition_data.current_leader
|
|
1327
|
+
if leader is None or leader.leader_epoch < 0:
|
|
1328
|
+
return
|
|
1329
|
+
if self._manager.cluster.update_partition_leader(
|
|
1330
|
+
tp, leader.leader_id, leader.leader_epoch):
|
|
1331
|
+
log.debug("Fetch response advertised new leader for %s: node %s epoch %s",
|
|
1332
|
+
tp, leader.leader_id, leader.leader_epoch)
|
|
1333
|
+
if self._manager.cluster.broker_metadata(leader.leader_id) is None:
|
|
1334
|
+
self._manager.cluster.request_update()
|
|
1335
|
+
|
|
1336
|
+
def _parse_fetched_data(self, completed_fetch):
|
|
1337
|
+
tp = completed_fetch.topic_partition
|
|
1338
|
+
fetch_offset = completed_fetch.fetched_offset
|
|
1339
|
+
error_code = completed_fetch.partition_data.error_code
|
|
1340
|
+
highwater = completed_fetch.partition_data.high_watermark
|
|
1341
|
+
error_type = Errors.for_code(error_code)
|
|
1342
|
+
parsed_records = None
|
|
1343
|
+
|
|
1344
|
+
try:
|
|
1345
|
+
if not self._subscriptions.is_fetchable(tp):
|
|
1346
|
+
# this can happen when a rebalance happened or a partition
|
|
1347
|
+
# consumption paused while fetch is still in-flight
|
|
1348
|
+
log.debug("Ignoring fetched records for partition %s"
|
|
1349
|
+
" since it is no longer fetchable", tp)
|
|
1350
|
+
|
|
1351
|
+
elif error_type is Errors.NoError:
|
|
1352
|
+
# we are interested in this fetch only if the beginning
|
|
1353
|
+
# offset (of the *request*) matches the current consumed position
|
|
1354
|
+
# Note that the *response* may return a messageset that starts
|
|
1355
|
+
# earlier (e.g., compressed messages) or later (e.g., compacted topic)
|
|
1356
|
+
position = self._subscriptions.assignment[tp].position
|
|
1357
|
+
if position is None or position.offset != fetch_offset:
|
|
1358
|
+
log.debug("Discarding fetch response for partition %s"
|
|
1359
|
+
" since its offset %d does not match the"
|
|
1360
|
+
" expected offset %d", tp, fetch_offset,
|
|
1361
|
+
position.offset)
|
|
1362
|
+
return None
|
|
1363
|
+
|
|
1364
|
+
# KIP-320 / Fetch v12+: the broker can tell us our last_fetched_epoch
|
|
1365
|
+
# diverges from its log. Route into the existing OffsetForLeaderEpoch
|
|
1366
|
+
# validation flow rather than truncating directly here; the
|
|
1367
|
+
# validation path surfaces LogTruncationError uniformly.
|
|
1368
|
+
diverging_epoch = completed_fetch.partition_data.diverging_epoch
|
|
1369
|
+
if diverging_epoch is not None and diverging_epoch.end_offset >= 0:
|
|
1370
|
+
log.info("Fetch for %s diverged at epoch %s offset %s;"
|
|
1371
|
+
" marking position for validation",
|
|
1372
|
+
tp, diverging_epoch.epoch, diverging_epoch.end_offset)
|
|
1373
|
+
self._subscriptions.request_position_validation(tp)
|
|
1374
|
+
self._manager.cluster.request_update()
|
|
1375
|
+
return None
|
|
1376
|
+
|
|
1377
|
+
records = MemoryRecords(completed_fetch.partition_data.records)
|
|
1378
|
+
aborted_transactions = completed_fetch.partition_data.aborted_transactions
|
|
1379
|
+
log.debug("Preparing to read %s bytes of data for partition %s with offset %d",
|
|
1380
|
+
records.size_in_bytes(), tp, fetch_offset)
|
|
1381
|
+
parsed_records = self.PartitionRecords(fetch_offset, tp, records,
|
|
1382
|
+
key_deserializer=self.config['key_deserializer'],
|
|
1383
|
+
value_deserializer=self.config['value_deserializer'],
|
|
1384
|
+
check_crcs=self.config['check_crcs'],
|
|
1385
|
+
isolation_level=self._isolation_level,
|
|
1386
|
+
aborted_transactions=aborted_transactions,
|
|
1387
|
+
metric_aggregator=completed_fetch.metric_aggregator,
|
|
1388
|
+
on_drain=self._on_partition_records_drain)
|
|
1389
|
+
if not records.has_next() and records.size_in_bytes() > 0:
|
|
1390
|
+
if completed_fetch.response_version < 3:
|
|
1391
|
+
# Implement the pre KIP-74 behavior of throwing a RecordTooLargeException.
|
|
1392
|
+
record_too_large_partitions = {tp: fetch_offset}
|
|
1393
|
+
raise RecordTooLargeError(
|
|
1394
|
+
"There are some messages at [Partition=Offset]: %s "
|
|
1395
|
+
" whose size is larger than the fetch size %s"
|
|
1396
|
+
" and hence cannot be ever returned. Please condier upgrading your broker to 0.10.1.0 or"
|
|
1397
|
+
" newer to avoid this issue. Alternatively, increase the fetch size on the client (using"
|
|
1398
|
+
" max_partition_fetch_bytes)" % (
|
|
1399
|
+
record_too_large_partitions,
|
|
1400
|
+
self.config['max_partition_fetch_bytes']),
|
|
1401
|
+
record_too_large_partitions)
|
|
1402
|
+
else:
|
|
1403
|
+
# This should not happen with brokers that support FetchRequest/Response V3 or higher (i.e. KIP-74)
|
|
1404
|
+
raise Errors.KafkaError("Failed to make progress reading messages at %s=%s."
|
|
1405
|
+
" Received a non-empty fetch response from the server, but no"
|
|
1406
|
+
" complete records were found." % (tp, fetch_offset))
|
|
1407
|
+
|
|
1408
|
+
if highwater >= 0:
|
|
1409
|
+
self._subscriptions.assignment[tp].highwater = highwater
|
|
1410
|
+
|
|
1411
|
+
preferred_read_replica = completed_fetch.partition_data.preferred_read_replica
|
|
1412
|
+
if self._subscriptions.assignment[tp].update_preferred_read_replica(
|
|
1413
|
+
preferred_read_replica,
|
|
1414
|
+
time.monotonic() + self.config['metadata_max_age_ms'] / 1000.0):
|
|
1415
|
+
if preferred_read_replica is None or preferred_read_replica < 0:
|
|
1416
|
+
log.debug("Cleared preferred read replica for partition %s", tp)
|
|
1417
|
+
else:
|
|
1418
|
+
log.debug("Updating preferred read replica for partition %s to %s",
|
|
1419
|
+
tp, preferred_read_replica)
|
|
1420
|
+
|
|
1421
|
+
elif error_type in (Errors.NotLeaderForPartitionError,
|
|
1422
|
+
Errors.ReplicaNotAvailableError,
|
|
1423
|
+
Errors.UnknownTopicOrPartitionError,
|
|
1424
|
+
Errors.KafkaStorageError):
|
|
1425
|
+
log.debug("Error fetching partition %s: %s", tp, error_type.__name__)
|
|
1426
|
+
self._maybe_update_current_leader(tp, completed_fetch.partition_data)
|
|
1427
|
+
self._manager.cluster.request_update()
|
|
1428
|
+
elif error_type in (Errors.FencedLeaderEpochError,
|
|
1429
|
+
Errors.UnknownLeaderEpochError):
|
|
1430
|
+
# KIP-320: the broker has a different view of the leader epoch
|
|
1431
|
+
# than we do; ask for metadata refresh and queue position
|
|
1432
|
+
# validation so we detect any truncation before continuing.
|
|
1433
|
+
# The cache is cleared by maybe_validate_position once the
|
|
1434
|
+
# cluster cache catches up with the new epoch.
|
|
1435
|
+
log.debug("Fetch for %s returned %s; marking position for validation",
|
|
1436
|
+
tp, error_type.__name__)
|
|
1437
|
+
self._maybe_update_current_leader(tp, completed_fetch.partition_data)
|
|
1438
|
+
self._subscriptions.request_position_validation(tp)
|
|
1439
|
+
self._manager.cluster.request_update()
|
|
1440
|
+
elif error_type is Errors.OffsetOutOfRangeError:
|
|
1441
|
+
position = self._subscriptions.assignment[tp].position
|
|
1442
|
+
if position is None or position.offset != fetch_offset:
|
|
1443
|
+
log.debug("Discarding stale fetch response for partition %s"
|
|
1444
|
+
" since the fetched offset %d does not match the"
|
|
1445
|
+
" current offset %d", tp, fetch_offset, position.offset)
|
|
1446
|
+
else:
|
|
1447
|
+
# KIP-392: a follower may be lagging behind the leader's
|
|
1448
|
+
# high watermark such that our leader-side position is
|
|
1449
|
+
# legitimately out of *its* range. If we'd been fetching
|
|
1450
|
+
# from a follower, drop the cache and retry against the
|
|
1451
|
+
# leader BEFORE concluding the offset is really out of
|
|
1452
|
+
# range. Only when there was no cached follower do we
|
|
1453
|
+
# proceed to reset / raise. Matches Java's behavior.
|
|
1454
|
+
cleared = self._subscriptions.assignment[tp].clear_preferred_read_replica()
|
|
1455
|
+
if cleared is not None:
|
|
1456
|
+
log.debug("Fetch offset %s out of range for %s on follower %s;"
|
|
1457
|
+
" retrying from leader", fetch_offset, tp, cleared)
|
|
1458
|
+
elif self._subscriptions.has_default_offset_reset_policy():
|
|
1459
|
+
log.info("Fetch offset %s is out of range for topic-partition %s",
|
|
1460
|
+
fetch_offset, tp)
|
|
1461
|
+
self._subscriptions.request_offset_reset(tp)
|
|
1462
|
+
else:
|
|
1463
|
+
raise Errors.OffsetOutOfRangeError({tp: fetch_offset})
|
|
1464
|
+
|
|
1465
|
+
elif error_type is Errors.TopicAuthorizationFailedError:
|
|
1466
|
+
log.warning("Not authorized to read from topic %s.", tp.topic)
|
|
1467
|
+
raise Errors.TopicAuthorizationFailedError(set([tp.topic]))
|
|
1468
|
+
elif issubclass(error_type, Errors.RetriableError):
|
|
1469
|
+
log.debug("Retriable error fetching partition %s: %s", tp, error_type())
|
|
1470
|
+
if issubclass(error_type, Errors.InvalidMetadataError):
|
|
1471
|
+
self._manager.cluster.request_update()
|
|
1472
|
+
else:
|
|
1473
|
+
raise error_type('Unexpected error while fetching data')
|
|
1474
|
+
|
|
1475
|
+
finally:
|
|
1476
|
+
if parsed_records is None and completed_fetch.metric_aggregator:
|
|
1477
|
+
completed_fetch.metric_aggregator.record(tp, 0, 0)
|
|
1478
|
+
|
|
1479
|
+
if error_type is not Errors.NoError:
|
|
1480
|
+
# Rotate this partition to the back of the iteration
|
|
1481
|
+
# order so we don't keep slamming the broken partition
|
|
1482
|
+
# first on the next poll - healthier partitions get
|
|
1483
|
+
# processed while this one's backoff / metadata
|
|
1484
|
+
# refresh runs. Cheap LRU-style fairness across the
|
|
1485
|
+
# assignment.
|
|
1486
|
+
self._subscriptions.move_partition_to_end(tp)
|
|
1487
|
+
|
|
1488
|
+
return parsed_records
|
|
1489
|
+
|
|
1490
|
+
def _on_partition_records_drain(self, partition_records):
|
|
1491
|
+
# Rotate this partition to the back of the iteration order so
|
|
1492
|
+
# the next poll prioritizes partitions we haven't drained from
|
|
1493
|
+
# recently. (Topic-grouping in the outgoing FetchRequest is
|
|
1494
|
+
# done unconditionally by FetchRequestData.to_send via
|
|
1495
|
+
# defaultdict, so this is purely round-robin fairness across
|
|
1496
|
+
# partitions, not a serialization-efficiency thing.)
|
|
1497
|
+
if partition_records.bytes_read > 0:
|
|
1498
|
+
self._subscriptions.move_partition_to_end(partition_records.topic_partition)
|
|
1499
|
+
|
|
1500
|
+
def close(self):
|
|
1501
|
+
if self._next_partition_records is not None:
|
|
1502
|
+
self._next_partition_records.drain()
|
|
1503
|
+
for parked in self._paused_partition_records.values():
|
|
1504
|
+
parked.drain()
|
|
1505
|
+
self._paused_partition_records.clear()
|
|
1506
|
+
self._paused_completed_fetches.clear()
|
|
1507
|
+
self._next_in_line_exception_metadata = None
|
|
1508
|
+
|
|
1509
|
+
class PartitionRecords:
|
|
1510
|
+
def __init__(self, fetch_offset, tp, records,
|
|
1511
|
+
key_deserializer=None, value_deserializer=None,
|
|
1512
|
+
check_crcs=True,
|
|
1513
|
+
isolation_level=IsolationLevel.READ_UNCOMMITTED,
|
|
1514
|
+
aborted_transactions=None, # AbortedTransaction data from FetchResponse
|
|
1515
|
+
metric_aggregator=None, on_drain=lambda x: None):
|
|
1516
|
+
self.fetch_offset = fetch_offset
|
|
1517
|
+
self.topic_partition = tp
|
|
1518
|
+
self.leader_epoch = -1
|
|
1519
|
+
self.next_fetch_offset = fetch_offset
|
|
1520
|
+
self.bytes_read = 0
|
|
1521
|
+
self.records_read = 0
|
|
1522
|
+
self.isolation_level = isolation_level
|
|
1523
|
+
self.aborted_producer_ids = set()
|
|
1524
|
+
self.aborted_transactions = collections.deque(
|
|
1525
|
+
sorted(aborted_transactions or [], key=lambda txn: txn.first_offset)
|
|
1526
|
+
)
|
|
1527
|
+
self.metric_aggregator = metric_aggregator
|
|
1528
|
+
self.check_crcs = check_crcs
|
|
1529
|
+
self.record_iterator = itertools.dropwhile(
|
|
1530
|
+
self._maybe_skip_record,
|
|
1531
|
+
self._unpack_records(tp, records, key_deserializer, value_deserializer))
|
|
1532
|
+
self.on_drain = on_drain
|
|
1533
|
+
self._next_inline_exception = None
|
|
1534
|
+
|
|
1535
|
+
def _maybe_skip_record(self, record):
|
|
1536
|
+
# When fetching an offset that is in the middle of a
|
|
1537
|
+
# compressed batch, we will get all messages in the batch.
|
|
1538
|
+
# But we want to start 'take' at the fetch_offset
|
|
1539
|
+
# (or the next highest offset in case the message was compacted)
|
|
1540
|
+
if record.offset < self.fetch_offset:
|
|
1541
|
+
log.debug("Skipping message offset: %s (expecting %s)",
|
|
1542
|
+
record.offset, self.fetch_offset)
|
|
1543
|
+
return True
|
|
1544
|
+
else:
|
|
1545
|
+
return False
|
|
1546
|
+
|
|
1547
|
+
# For truthiness evaluation
|
|
1548
|
+
def __bool__(self):
|
|
1549
|
+
return self.record_iterator is not None
|
|
1550
|
+
|
|
1551
|
+
def drain(self):
|
|
1552
|
+
if self.record_iterator is not None:
|
|
1553
|
+
self.record_iterator = None
|
|
1554
|
+
self._next_inline_exception = None
|
|
1555
|
+
if self.metric_aggregator:
|
|
1556
|
+
self.metric_aggregator.record(self.topic_partition, self.bytes_read, self.records_read)
|
|
1557
|
+
self.on_drain(self)
|
|
1558
|
+
|
|
1559
|
+
def _maybe_raise_next_inline_exception(self):
|
|
1560
|
+
if self._next_inline_exception:
|
|
1561
|
+
exc, self._next_inline_exception = self._next_inline_exception, None
|
|
1562
|
+
raise exc
|
|
1563
|
+
|
|
1564
|
+
def take(self, n=None):
|
|
1565
|
+
self._maybe_raise_next_inline_exception()
|
|
1566
|
+
records = []
|
|
1567
|
+
try:
|
|
1568
|
+
# Note that records.extend(iter) will extend partially when exception raised mid-stream
|
|
1569
|
+
records.extend(itertools.islice(self.record_iterator, 0, n))
|
|
1570
|
+
except Exception as e:
|
|
1571
|
+
if not records:
|
|
1572
|
+
raise e
|
|
1573
|
+
# To be thrown in the next call of this method
|
|
1574
|
+
self._next_inline_exception = e
|
|
1575
|
+
return records
|
|
1576
|
+
|
|
1577
|
+
def _unpack_records(self, tp, records, key_deserializer, value_deserializer):
|
|
1578
|
+
try:
|
|
1579
|
+
batch = records.next_batch()
|
|
1580
|
+
last_batch = None
|
|
1581
|
+
while batch is not None:
|
|
1582
|
+
last_batch = batch
|
|
1583
|
+
|
|
1584
|
+
if self.check_crcs and not batch.validate_crc():
|
|
1585
|
+
raise Errors.CorruptRecordError(
|
|
1586
|
+
"Record batch for partition %s at offset %s failed crc check" % (
|
|
1587
|
+
self.topic_partition, batch.base_offset))
|
|
1588
|
+
|
|
1589
|
+
|
|
1590
|
+
# Try DefaultsRecordBatch / message log format v2
|
|
1591
|
+
# base_offset, last_offset_delta, aborted transactions, and control batches
|
|
1592
|
+
if batch.magic == 2:
|
|
1593
|
+
self.leader_epoch = batch.leader_epoch
|
|
1594
|
+
if self.isolation_level == IsolationLevel.READ_COMMITTED and batch.has_producer_id():
|
|
1595
|
+
# remove from the aborted transaction queue all aborted transactions which have begun
|
|
1596
|
+
# before the current batch's last offset and add the associated producerIds to the
|
|
1597
|
+
# aborted producer set
|
|
1598
|
+
self._consume_aborted_transactions_up_to(batch.last_offset)
|
|
1599
|
+
|
|
1600
|
+
producer_id = batch.producer_id
|
|
1601
|
+
if self._contains_abort_marker(batch):
|
|
1602
|
+
try:
|
|
1603
|
+
self.aborted_producer_ids.remove(producer_id)
|
|
1604
|
+
except KeyError:
|
|
1605
|
+
pass
|
|
1606
|
+
elif self._is_batch_aborted(batch):
|
|
1607
|
+
log.debug("Skipping aborted record batch from partition %s with producer_id %s and"
|
|
1608
|
+
" offsets %s to %s",
|
|
1609
|
+
self.topic_partition, producer_id, batch.base_offset, batch.last_offset)
|
|
1610
|
+
self.next_fetch_offset = batch.next_offset
|
|
1611
|
+
batch = records.next_batch()
|
|
1612
|
+
continue
|
|
1613
|
+
|
|
1614
|
+
# Control batches have a single record indicating whether a transaction
|
|
1615
|
+
# was aborted or committed. These are not returned to the consumer.
|
|
1616
|
+
if batch.is_control_batch:
|
|
1617
|
+
self.next_fetch_offset = batch.next_offset
|
|
1618
|
+
batch = records.next_batch()
|
|
1619
|
+
continue
|
|
1620
|
+
|
|
1621
|
+
for record in batch:
|
|
1622
|
+
if self.check_crcs and not record.validate_crc():
|
|
1623
|
+
raise Errors.CorruptRecordError(
|
|
1624
|
+
"Record for partition %s at offset %s failed crc check" % (
|
|
1625
|
+
self.topic_partition, record.offset))
|
|
1626
|
+
key_size = len(record.key) if record.key is not None else -1
|
|
1627
|
+
value_size = len(record.value) if record.value is not None else -1
|
|
1628
|
+
key = self._deserialize(key_deserializer, tp.topic, record.headers, record.key)
|
|
1629
|
+
value = self._deserialize(value_deserializer, tp.topic, record.headers, record.value)
|
|
1630
|
+
headers = record.headers
|
|
1631
|
+
header_size = sum(
|
|
1632
|
+
len(h_key.encode("utf-8")) + (len(h_val) if h_val is not None else 0) for h_key, h_val in
|
|
1633
|
+
headers) if headers else -1
|
|
1634
|
+
self.records_read += 1
|
|
1635
|
+
self.bytes_read += record.size_in_bytes
|
|
1636
|
+
self.next_fetch_offset = record.offset + 1
|
|
1637
|
+
yield ConsumerRecord(
|
|
1638
|
+
tp.topic, tp.partition, self.leader_epoch, record.offset, record.timestamp,
|
|
1639
|
+
record.timestamp_type, key, value, record.headers, record.checksum,
|
|
1640
|
+
key_size, value_size, header_size)
|
|
1641
|
+
|
|
1642
|
+
batch = records.next_batch()
|
|
1643
|
+
else:
|
|
1644
|
+
# Message format v2 preserves the last offset in a batch even if the last record is removed
|
|
1645
|
+
# through compaction. By using the next offset computed from the last offset in the batch,
|
|
1646
|
+
# we ensure that the offset of the next fetch will point to the next batch, which avoids
|
|
1647
|
+
# unnecessary re-fetching of the same batch (in the worst case, the consumer could get stuck
|
|
1648
|
+
# fetching the same batch repeatedly).
|
|
1649
|
+
if last_batch and last_batch.magic == 2:
|
|
1650
|
+
self.next_fetch_offset = last_batch.next_offset
|
|
1651
|
+
self.drain()
|
|
1652
|
+
|
|
1653
|
+
# If unpacking raises StopIteration, it is erroneously
|
|
1654
|
+
# caught by the generator. We want all exceptions to be raised
|
|
1655
|
+
# back to the user. See Issue 545
|
|
1656
|
+
except StopIteration:
|
|
1657
|
+
log.exception('StopIteration raised unpacking messageset')
|
|
1658
|
+
raise RuntimeError('StopIteration raised unpacking messageset')
|
|
1659
|
+
|
|
1660
|
+
def _deserialize(self, deserializer, topic, headers, data):
|
|
1661
|
+
if deserializer is None:
|
|
1662
|
+
return data
|
|
1663
|
+
try:
|
|
1664
|
+
return deserializer.deserialize(topic, headers, data)
|
|
1665
|
+
except TypeError:
|
|
1666
|
+
global _LOGGED_DESERIALIZE_WARNING
|
|
1667
|
+
if not _LOGGED_DESERIALIZE_WARNING:
|
|
1668
|
+
warnings.warn('deserializer does not implement deserialize(topic, headers, data)', category=DeprecationWarning)
|
|
1669
|
+
LOGGED_DESERIALIZE_WARNING = True
|
|
1670
|
+
return deserializer.deserialize(topic, data)
|
|
1671
|
+
|
|
1672
|
+
def _consume_aborted_transactions_up_to(self, offset):
|
|
1673
|
+
if not self.aborted_transactions:
|
|
1674
|
+
return
|
|
1675
|
+
|
|
1676
|
+
while self.aborted_transactions and self.aborted_transactions[0].first_offset <= offset:
|
|
1677
|
+
self.aborted_producer_ids.add(self.aborted_transactions.popleft().producer_id)
|
|
1678
|
+
|
|
1679
|
+
def _is_batch_aborted(self, batch):
|
|
1680
|
+
return batch.is_transactional and batch.producer_id in self.aborted_producer_ids
|
|
1681
|
+
|
|
1682
|
+
def _contains_abort_marker(self, batch):
|
|
1683
|
+
if not batch.is_control_batch:
|
|
1684
|
+
return False
|
|
1685
|
+
record = next(batch)
|
|
1686
|
+
if not record:
|
|
1687
|
+
return False
|
|
1688
|
+
return record.abort
|
|
1689
|
+
|
|
1690
|
+
|
|
1691
|
+
class FetchSessionHandler:
|
|
1692
|
+
"""
|
|
1693
|
+
FetchSessionHandler maintains the fetch session state for connecting to a broker.
|
|
1694
|
+
|
|
1695
|
+
Using the protocol outlined by KIP-227, clients can create incremental fetch sessions.
|
|
1696
|
+
These sessions allow the client to fetch information about a set of partition over
|
|
1697
|
+
and over, without explicitly enumerating all the partitions in the request and the
|
|
1698
|
+
response.
|
|
1699
|
+
|
|
1700
|
+
FetchSessionHandler tracks the partitions which are in the session. It also
|
|
1701
|
+
determines which partitions need to be included in each fetch request, and what
|
|
1702
|
+
the attached fetch session metadata should be for each request.
|
|
1703
|
+
"""
|
|
1704
|
+
|
|
1705
|
+
def __init__(self, node_id):
|
|
1706
|
+
self.node_id = node_id
|
|
1707
|
+
self.next_metadata = FetchMetadata.INITIAL
|
|
1708
|
+
self.session_partitions = {}
|
|
1709
|
+
|
|
1710
|
+
def build_next(self, next_partitions):
|
|
1711
|
+
"""
|
|
1712
|
+
Arguments:
|
|
1713
|
+
next_partitions (dict): TopicPartition -> TopicPartitionState
|
|
1714
|
+
|
|
1715
|
+
Returns:
|
|
1716
|
+
FetchRequestData
|
|
1717
|
+
"""
|
|
1718
|
+
if self.next_metadata.is_full:
|
|
1719
|
+
log.debug("Built full fetch %s for node %s with %s partition(s).",
|
|
1720
|
+
self.next_metadata, self.node_id, len(next_partitions))
|
|
1721
|
+
self.session_partitions = next_partitions
|
|
1722
|
+
return FetchRequestData(next_partitions, None, self.next_metadata)
|
|
1723
|
+
|
|
1724
|
+
prev_tps = set(self.session_partitions.keys())
|
|
1725
|
+
next_tps = set(next_partitions.keys())
|
|
1726
|
+
log.debug("Building incremental partitions from next: %s, previous: %s", next_tps, prev_tps)
|
|
1727
|
+
added = next_tps - prev_tps
|
|
1728
|
+
for tp in added:
|
|
1729
|
+
self.session_partitions[tp] = next_partitions[tp]
|
|
1730
|
+
removed = prev_tps - next_tps
|
|
1731
|
+
for tp in removed:
|
|
1732
|
+
self.session_partitions.pop(tp)
|
|
1733
|
+
altered = set()
|
|
1734
|
+
for tp in next_tps & prev_tps:
|
|
1735
|
+
if next_partitions[tp] != self.session_partitions[tp]:
|
|
1736
|
+
self.session_partitions[tp] = next_partitions[tp]
|
|
1737
|
+
altered.add(tp)
|
|
1738
|
+
|
|
1739
|
+
log.debug("Built incremental fetch %s for node %s. Added %s, altered %s, removed %s out of %s",
|
|
1740
|
+
self.next_metadata, self.node_id, added, altered, removed, self.session_partitions.keys())
|
|
1741
|
+
to_send = collections.OrderedDict({tp: next_partitions[tp] for tp in next_partitions if tp in (added | altered)})
|
|
1742
|
+
return FetchRequestData(to_send, removed, self.next_metadata)
|
|
1743
|
+
|
|
1744
|
+
def handle_response(self, response):
|
|
1745
|
+
if response.error_code != Errors.NoError.errno:
|
|
1746
|
+
error_type = Errors.for_code(response.error_code)
|
|
1747
|
+
log.info("Node %s was unable to process the fetch request with %s: %s.",
|
|
1748
|
+
self.node_id, self.next_metadata, error_type())
|
|
1749
|
+
if error_type is Errors.FetchSessionIdNotFoundError:
|
|
1750
|
+
self.next_metadata = FetchMetadata.INITIAL
|
|
1751
|
+
else:
|
|
1752
|
+
self.next_metadata = self.next_metadata.next_close_existing()
|
|
1753
|
+
return False
|
|
1754
|
+
|
|
1755
|
+
response_tps = self._response_partitions(response)
|
|
1756
|
+
session_tps = set(self.session_partitions.keys())
|
|
1757
|
+
if self.next_metadata.is_full:
|
|
1758
|
+
if response_tps != session_tps:
|
|
1759
|
+
log.info("Node %s sent an invalid full fetch response with extra %s / omitted %s",
|
|
1760
|
+
self.node_id, response_tps - session_tps, session_tps - response_tps)
|
|
1761
|
+
self.next_metadata = FetchMetadata.INITIAL
|
|
1762
|
+
return False
|
|
1763
|
+
elif response.session_id == FetchMetadata.INVALID_SESSION_ID:
|
|
1764
|
+
log.debug("Node %s sent a full fetch response with %s partitions",
|
|
1765
|
+
self.node_id, len(response_tps))
|
|
1766
|
+
self.next_metadata = FetchMetadata.INITIAL
|
|
1767
|
+
return True
|
|
1768
|
+
elif response.session_id == FetchMetadata.THROTTLED_SESSION_ID:
|
|
1769
|
+
log.debug("Node %s sent a empty full fetch response due to a quota violation (%s partitions)",
|
|
1770
|
+
self.node_id, len(response_tps))
|
|
1771
|
+
# Keep current metadata
|
|
1772
|
+
return True
|
|
1773
|
+
else:
|
|
1774
|
+
# The server created a new incremental fetch session.
|
|
1775
|
+
log.debug("Node %s sent a full fetch response that created a new incremental fetch session %s"
|
|
1776
|
+
" with %s response partitions",
|
|
1777
|
+
self.node_id, response.session_id,
|
|
1778
|
+
len(response_tps))
|
|
1779
|
+
self.next_metadata = FetchMetadata.new_incremental(response.session_id)
|
|
1780
|
+
return True
|
|
1781
|
+
else:
|
|
1782
|
+
if response_tps - session_tps:
|
|
1783
|
+
log.info("Node %s sent an invalid incremental fetch response with extra partitions %s",
|
|
1784
|
+
self.node_id, response_tps - session_tps)
|
|
1785
|
+
self.next_metadata = self.next_metadata.next_close_existing()
|
|
1786
|
+
return False
|
|
1787
|
+
elif response.session_id == FetchMetadata.INVALID_SESSION_ID:
|
|
1788
|
+
# The incremental fetch session was closed by the server.
|
|
1789
|
+
log.debug("Node %s sent an incremental fetch response closing session %s"
|
|
1790
|
+
" with %s response partitions (%s implied)",
|
|
1791
|
+
self.node_id, self.next_metadata.session_id,
|
|
1792
|
+
len(response_tps), len(self.session_partitions) - len(response_tps))
|
|
1793
|
+
self.next_metadata = FetchMetadata.INITIAL
|
|
1794
|
+
return True
|
|
1795
|
+
elif response.session_id == FetchMetadata.THROTTLED_SESSION_ID:
|
|
1796
|
+
log.debug("Node %s sent a empty incremental fetch response due to a quota violation (%s partitions)",
|
|
1797
|
+
self.node_id, len(response_tps))
|
|
1798
|
+
# Keep current metadata
|
|
1799
|
+
return True
|
|
1800
|
+
else:
|
|
1801
|
+
# The incremental fetch session was continued by the server.
|
|
1802
|
+
log.debug("Node %s sent an incremental fetch response for session %s"
|
|
1803
|
+
" with %s response partitions (%s implied)",
|
|
1804
|
+
self.node_id, response.session_id,
|
|
1805
|
+
len(response_tps), len(self.session_partitions) - len(response_tps))
|
|
1806
|
+
self.next_metadata = self.next_metadata.next_incremental()
|
|
1807
|
+
return True
|
|
1808
|
+
|
|
1809
|
+
def handle_error(self, _exception):
|
|
1810
|
+
self.next_metadata = self.next_metadata.next_close_existing()
|
|
1811
|
+
|
|
1812
|
+
def _response_partitions(self, response):
|
|
1813
|
+
return {TopicPartition(topic_data.topic, partition_data.partition_index)
|
|
1814
|
+
for topic_data in response.responses
|
|
1815
|
+
for partition_data in topic_data.partitions}
|
|
1816
|
+
|
|
1817
|
+
|
|
1818
|
+
class FetchMetadata:
|
|
1819
|
+
__slots__ = ('session_id', 'epoch')
|
|
1820
|
+
|
|
1821
|
+
MAX_EPOCH = 2147483647
|
|
1822
|
+
INVALID_SESSION_ID = 0 # used by clients with no session.
|
|
1823
|
+
THROTTLED_SESSION_ID = -1 # returned with empty response on quota violation
|
|
1824
|
+
INITIAL_EPOCH = 0 # client wants to create or recreate a session.
|
|
1825
|
+
FINAL_EPOCH = -1 # client wants to close any existing session, and not create a new one.
|
|
1826
|
+
|
|
1827
|
+
def __init__(self, session_id, epoch):
|
|
1828
|
+
self.session_id = session_id
|
|
1829
|
+
self.epoch = epoch
|
|
1830
|
+
|
|
1831
|
+
@property
|
|
1832
|
+
def is_full(self):
|
|
1833
|
+
return self.epoch == self.INITIAL_EPOCH or self.epoch == self.FINAL_EPOCH
|
|
1834
|
+
|
|
1835
|
+
@classmethod
|
|
1836
|
+
def next_epoch(cls, prev_epoch):
|
|
1837
|
+
if prev_epoch < 0:
|
|
1838
|
+
return cls.FINAL_EPOCH
|
|
1839
|
+
elif prev_epoch == cls.MAX_EPOCH:
|
|
1840
|
+
return 1
|
|
1841
|
+
else:
|
|
1842
|
+
return prev_epoch + 1
|
|
1843
|
+
|
|
1844
|
+
def next_close_existing(self):
|
|
1845
|
+
return self.__class__(self.session_id, self.INITIAL_EPOCH)
|
|
1846
|
+
|
|
1847
|
+
@classmethod
|
|
1848
|
+
def new_incremental(cls, session_id):
|
|
1849
|
+
return cls(session_id, cls.next_epoch(cls.INITIAL_EPOCH))
|
|
1850
|
+
|
|
1851
|
+
def next_incremental(self):
|
|
1852
|
+
return self.__class__(self.session_id, self.next_epoch(self.epoch))
|
|
1853
|
+
|
|
1854
|
+
FetchMetadata.INITIAL = FetchMetadata(FetchMetadata.INVALID_SESSION_ID, FetchMetadata.INITIAL_EPOCH)
|
|
1855
|
+
FetchMetadata.LEGACY = FetchMetadata(FetchMetadata.INVALID_SESSION_ID, FetchMetadata.FINAL_EPOCH)
|
|
1856
|
+
|
|
1857
|
+
|
|
1858
|
+
class FetchRequestData:
|
|
1859
|
+
__slots__ = ('_to_send', '_to_forget', '_metadata')
|
|
1860
|
+
|
|
1861
|
+
def __init__(self, to_send, to_forget, metadata):
|
|
1862
|
+
self._to_send = to_send or dict() # {TopicPartition: (partition, ...)}
|
|
1863
|
+
self._to_forget = to_forget or set() # {TopicPartition}
|
|
1864
|
+
self._metadata = metadata
|
|
1865
|
+
|
|
1866
|
+
@property
|
|
1867
|
+
def metadata(self):
|
|
1868
|
+
return self._metadata
|
|
1869
|
+
|
|
1870
|
+
@property
|
|
1871
|
+
def id(self):
|
|
1872
|
+
return self._metadata.session_id
|
|
1873
|
+
|
|
1874
|
+
@property
|
|
1875
|
+
def epoch(self):
|
|
1876
|
+
return self._metadata.epoch
|
|
1877
|
+
|
|
1878
|
+
@property
|
|
1879
|
+
def to_send(self):
|
|
1880
|
+
# Return as list of _FetchTopic data objects
|
|
1881
|
+
# so it can be passed directly to encoder
|
|
1882
|
+
partition_data = collections.defaultdict(list)
|
|
1883
|
+
for tp, partition_info in self._to_send.items():
|
|
1884
|
+
partition_data[tp.topic].append(partition_info)
|
|
1885
|
+
return [
|
|
1886
|
+
_FetchTopic(topic=topic, partitions=partitions)
|
|
1887
|
+
for topic, partitions in partition_data.items()
|
|
1888
|
+
]
|
|
1889
|
+
|
|
1890
|
+
@property
|
|
1891
|
+
def to_forget(self):
|
|
1892
|
+
# Return as list of _ForgottenTopic data objects
|
|
1893
|
+
# so it can be passed directly to encoder
|
|
1894
|
+
partition_data = collections.defaultdict(list)
|
|
1895
|
+
for tp in self._to_forget:
|
|
1896
|
+
partition_data[tp.topic].append(tp.partition)
|
|
1897
|
+
return [
|
|
1898
|
+
_ForgottenTopic(topic=topic, partitions=partitions)
|
|
1899
|
+
for topic, partitions in partition_data.items()
|
|
1900
|
+
]
|
|
1901
|
+
|
|
1902
|
+
|
|
1903
|
+
class FetchMetrics:
|
|
1904
|
+
__slots__ = ('total_bytes', 'total_records')
|
|
1905
|
+
|
|
1906
|
+
def __init__(self):
|
|
1907
|
+
self.total_bytes = 0
|
|
1908
|
+
self.total_records = 0
|
|
1909
|
+
|
|
1910
|
+
|
|
1911
|
+
class FetchResponseMetricAggregator:
|
|
1912
|
+
"""
|
|
1913
|
+
Since we parse the message data for each partition from each fetch
|
|
1914
|
+
response lazily, fetch-level metrics need to be aggregated as the messages
|
|
1915
|
+
from each partition are parsed. This class is used to facilitate this
|
|
1916
|
+
incremental aggregation.
|
|
1917
|
+
"""
|
|
1918
|
+
def __init__(self, sensors, partitions):
|
|
1919
|
+
self.sensors = sensors
|
|
1920
|
+
self.unrecorded_partitions = partitions
|
|
1921
|
+
self.fetch_metrics = FetchMetrics()
|
|
1922
|
+
self.topic_fetch_metrics = collections.defaultdict(FetchMetrics)
|
|
1923
|
+
|
|
1924
|
+
def record(self, partition, num_bytes, num_records):
|
|
1925
|
+
"""
|
|
1926
|
+
After each partition is parsed, we update the current metric totals
|
|
1927
|
+
with the total bytes and number of records parsed. After all partitions
|
|
1928
|
+
have reported, we write the metric.
|
|
1929
|
+
"""
|
|
1930
|
+
self.unrecorded_partitions.remove(partition)
|
|
1931
|
+
self.fetch_metrics.total_bytes += num_bytes
|
|
1932
|
+
self.fetch_metrics.total_records += num_records
|
|
1933
|
+
self.topic_fetch_metrics[partition.topic].total_bytes += num_bytes
|
|
1934
|
+
self.topic_fetch_metrics[partition.topic].total_records += num_records
|
|
1935
|
+
|
|
1936
|
+
# once all expected partitions from the fetch have reported in, record the metrics
|
|
1937
|
+
if not self.unrecorded_partitions:
|
|
1938
|
+
self.sensors.bytes_fetched.record(self.fetch_metrics.total_bytes)
|
|
1939
|
+
self.sensors.records_fetched.record(self.fetch_metrics.total_records)
|
|
1940
|
+
for topic, metrics in self.topic_fetch_metrics.items():
|
|
1941
|
+
self.sensors.record_topic_fetch_metrics(topic, metrics.total_bytes, metrics.total_records)
|
|
1942
|
+
|
|
1943
|
+
|
|
1944
|
+
class FetchManagerMetrics:
|
|
1945
|
+
def __init__(self, metrics, prefix):
|
|
1946
|
+
self.metrics = metrics
|
|
1947
|
+
self.group_name = '%s-fetch-manager-metrics' % (prefix,)
|
|
1948
|
+
|
|
1949
|
+
self.bytes_fetched = metrics.sensor('bytes-fetched')
|
|
1950
|
+
self.bytes_fetched.add(metrics.metric_name('fetch-size-avg', self.group_name,
|
|
1951
|
+
'The average number of bytes fetched per request'), Avg())
|
|
1952
|
+
self.bytes_fetched.add(metrics.metric_name('fetch-size-max', self.group_name,
|
|
1953
|
+
'The maximum number of bytes fetched per request'), Max())
|
|
1954
|
+
self.bytes_fetched.add(metrics.metric_name('bytes-consumed-rate', self.group_name,
|
|
1955
|
+
'The average number of bytes consumed per second'), Rate())
|
|
1956
|
+
|
|
1957
|
+
self.records_fetched = self.metrics.sensor('records-fetched')
|
|
1958
|
+
self.records_fetched.add(metrics.metric_name('records-per-request-avg', self.group_name,
|
|
1959
|
+
'The average number of records in each request'), Avg())
|
|
1960
|
+
self.records_fetched.add(metrics.metric_name('records-consumed-rate', self.group_name,
|
|
1961
|
+
'The average number of records consumed per second'), Rate())
|
|
1962
|
+
|
|
1963
|
+
self.fetch_latency = metrics.sensor('fetch-latency')
|
|
1964
|
+
self.fetch_latency.add(metrics.metric_name('fetch-latency-avg', self.group_name,
|
|
1965
|
+
'The average time taken for a fetch request.'), Avg())
|
|
1966
|
+
self.fetch_latency.add(metrics.metric_name('fetch-latency-max', self.group_name,
|
|
1967
|
+
'The max time taken for any fetch request.'), Max())
|
|
1968
|
+
self.fetch_latency.add(metrics.metric_name('fetch-rate', self.group_name,
|
|
1969
|
+
'The number of fetch requests per second.'), Rate(sampled_stat=Count()))
|
|
1970
|
+
|
|
1971
|
+
self.records_fetch_lag = metrics.sensor('records-lag')
|
|
1972
|
+
self.records_fetch_lag.add(metrics.metric_name('records-lag-max', self.group_name,
|
|
1973
|
+
'The maximum lag in terms of number of records for any partition in self window'), Max())
|
|
1974
|
+
|
|
1975
|
+
def record_topic_fetch_metrics(self, topic, num_bytes, num_records):
|
|
1976
|
+
# record bytes fetched
|
|
1977
|
+
name = '.'.join(['topic', topic, 'bytes-fetched'])
|
|
1978
|
+
bytes_fetched = self.metrics.get_sensor(name)
|
|
1979
|
+
if not bytes_fetched:
|
|
1980
|
+
metric_tags = {'topic': topic.replace('.', '_')}
|
|
1981
|
+
|
|
1982
|
+
bytes_fetched = self.metrics.sensor(name)
|
|
1983
|
+
bytes_fetched.add(self.metrics.metric_name('fetch-size-avg',
|
|
1984
|
+
self.group_name,
|
|
1985
|
+
'The average number of bytes fetched per request for topic %s' % (topic,),
|
|
1986
|
+
metric_tags), Avg())
|
|
1987
|
+
bytes_fetched.add(self.metrics.metric_name('fetch-size-max',
|
|
1988
|
+
self.group_name,
|
|
1989
|
+
'The maximum number of bytes fetched per request for topic %s' % (topic,),
|
|
1990
|
+
metric_tags), Max())
|
|
1991
|
+
bytes_fetched.add(self.metrics.metric_name('bytes-consumed-rate',
|
|
1992
|
+
self.group_name,
|
|
1993
|
+
'The average number of bytes consumed per second for topic %s' % (topic,),
|
|
1994
|
+
metric_tags), Rate())
|
|
1995
|
+
bytes_fetched.record(num_bytes)
|
|
1996
|
+
|
|
1997
|
+
# record records fetched
|
|
1998
|
+
name = '.'.join(['topic', topic, 'records-fetched'])
|
|
1999
|
+
records_fetched = self.metrics.get_sensor(name)
|
|
2000
|
+
if not records_fetched:
|
|
2001
|
+
metric_tags = {'topic': topic.replace('.', '_')}
|
|
2002
|
+
|
|
2003
|
+
records_fetched = self.metrics.sensor(name)
|
|
2004
|
+
records_fetched.add(self.metrics.metric_name('records-per-request-avg',
|
|
2005
|
+
self.group_name,
|
|
2006
|
+
'The average number of records in each request for topic %s' % (topic,),
|
|
2007
|
+
metric_tags), Avg())
|
|
2008
|
+
records_fetched.add(self.metrics.metric_name('records-consumed-rate',
|
|
2009
|
+
self.group_name,
|
|
2010
|
+
'The average number of records consumed per second for topic %s' % (topic,),
|
|
2011
|
+
metric_tags), Rate())
|
|
2012
|
+
records_fetched.record(num_records)
|