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,1326 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod, abstractproperty
|
|
2
|
+
import collections
|
|
3
|
+
from enum import IntEnum
|
|
4
|
+
import heapq
|
|
5
|
+
import logging
|
|
6
|
+
import threading
|
|
7
|
+
|
|
8
|
+
import kafka.errors as Errors
|
|
9
|
+
from kafka.protocol.metadata import FindCoordinatorRequest, CoordinatorType
|
|
10
|
+
from kafka.protocol.producer import (
|
|
11
|
+
AddOffsetsToTxnRequest, AddPartitionsToTxnRequest,
|
|
12
|
+
EndTxnRequest, InitProducerIdRequest, TxnOffsetCommitRequest,
|
|
13
|
+
)
|
|
14
|
+
from kafka.structs import ConsumerGroupMetadata, TopicPartition
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
log = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
NO_PRODUCER_ID = -1
|
|
21
|
+
NO_PRODUCER_EPOCH = -1
|
|
22
|
+
NO_SEQUENCE = -1
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ProducerIdAndEpoch:
|
|
26
|
+
__slots__ = ('producer_id', 'epoch')
|
|
27
|
+
|
|
28
|
+
def __init__(self, producer_id, epoch):
|
|
29
|
+
self.producer_id = producer_id
|
|
30
|
+
self.epoch = epoch
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def is_valid(self):
|
|
34
|
+
return NO_PRODUCER_ID < self.producer_id
|
|
35
|
+
|
|
36
|
+
def match(self, batch):
|
|
37
|
+
return self.producer_id == batch.producer_id and self.epoch == batch.producer_epoch
|
|
38
|
+
|
|
39
|
+
def __eq__(self, other):
|
|
40
|
+
return isinstance(other, ProducerIdAndEpoch) and self.producer_id == other.producer_id and self.epoch == other.epoch
|
|
41
|
+
|
|
42
|
+
def __str__(self):
|
|
43
|
+
return "ProducerIdAndEpoch(producer_id={}, epoch={})".format(self.producer_id, self.epoch)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TransactionState(IntEnum):
|
|
47
|
+
UNINITIALIZED = 0
|
|
48
|
+
INITIALIZING = 1
|
|
49
|
+
READY = 2
|
|
50
|
+
IN_TRANSACTION = 3
|
|
51
|
+
COMMITTING_TRANSACTION = 4
|
|
52
|
+
ABORTING_TRANSACTION = 5
|
|
53
|
+
ABORTABLE_ERROR = 6
|
|
54
|
+
FATAL_ERROR = 7
|
|
55
|
+
# KIP-360: intermediate state entered when a recoverable sequence-related
|
|
56
|
+
# error is encountered. The producer sends an InitProducerIdRequest v3+
|
|
57
|
+
# with its current producer_id/epoch to bump the epoch, then transitions
|
|
58
|
+
# back to READY on success. Records in the accumulator will be sent under
|
|
59
|
+
# the bumped epoch with fresh sequence numbers. In-flight batches at the
|
|
60
|
+
# moment of the bump are lost (their futures fail). (TODO re KAFKA-5793)
|
|
61
|
+
BUMPING_PRODUCER_EPOCH = 8
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def is_transition_valid(cls, source, target):
|
|
65
|
+
if target == cls.INITIALIZING:
|
|
66
|
+
return source in (cls.UNINITIALIZED, cls.BUMPING_PRODUCER_EPOCH)
|
|
67
|
+
elif target == cls.READY:
|
|
68
|
+
return source in (cls.INITIALIZING, cls.COMMITTING_TRANSACTION,
|
|
69
|
+
cls.ABORTING_TRANSACTION, cls.BUMPING_PRODUCER_EPOCH)
|
|
70
|
+
elif target == cls.IN_TRANSACTION:
|
|
71
|
+
return source == cls.READY
|
|
72
|
+
elif target == cls.COMMITTING_TRANSACTION:
|
|
73
|
+
return source == cls.IN_TRANSACTION
|
|
74
|
+
elif target == cls.ABORTING_TRANSACTION:
|
|
75
|
+
return source in (cls.IN_TRANSACTION, cls.ABORTABLE_ERROR)
|
|
76
|
+
elif target == cls.ABORTABLE_ERROR:
|
|
77
|
+
return source in (cls.IN_TRANSACTION, cls.COMMITTING_TRANSACTION,
|
|
78
|
+
cls.ABORTABLE_ERROR, cls.BUMPING_PRODUCER_EPOCH)
|
|
79
|
+
elif target == cls.BUMPING_PRODUCER_EPOCH:
|
|
80
|
+
# A recoverable sequence-related error can arrive at any point in
|
|
81
|
+
# the producer's lifetime; the bump is a unilateral recovery
|
|
82
|
+
# action. Disallow only from UNINITIALIZED (no producer_id yet
|
|
83
|
+
# to bump) and the terminal error states.
|
|
84
|
+
return source in (cls.READY, cls.IN_TRANSACTION,
|
|
85
|
+
cls.COMMITTING_TRANSACTION, cls.ABORTING_TRANSACTION,
|
|
86
|
+
cls.ABORTABLE_ERROR)
|
|
87
|
+
elif target == cls.UNINITIALIZED:
|
|
88
|
+
# Disallow transitions to UNITIALIZED
|
|
89
|
+
return False
|
|
90
|
+
elif target == cls.FATAL_ERROR:
|
|
91
|
+
# We can transition to FATAL_ERROR unconditionally.
|
|
92
|
+
# FATAL_ERROR is never a valid starting state for any transition. So the only option is to close the
|
|
93
|
+
# producer or do purely non transactional requests.
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class Priority(IntEnum):
|
|
98
|
+
# We use the priority to determine the order in which requests need to be sent out. For instance, if we have
|
|
99
|
+
# a pending FindCoordinator request, that must always go first. Next, If we need a producer id, that must go second.
|
|
100
|
+
# The endTxn request must always go last.
|
|
101
|
+
FIND_COORDINATOR = 0
|
|
102
|
+
INIT_PRODUCER_ID = 1
|
|
103
|
+
ADD_PARTITIONS_OR_OFFSETS = 2
|
|
104
|
+
END_TXN = 3
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class TransactionManager:
|
|
108
|
+
"""
|
|
109
|
+
A class which maintains state for transactions. Also keeps the state necessary to ensure idempotent production.
|
|
110
|
+
"""
|
|
111
|
+
NO_INFLIGHT_REQUEST_CORRELATION_ID = -1
|
|
112
|
+
# The retry_backoff_ms is overridden to the following value if the first AddPartitions receives a
|
|
113
|
+
# CONCURRENT_TRANSACTIONS error.
|
|
114
|
+
ADD_PARTITIONS_RETRY_BACKOFF_MS = 20
|
|
115
|
+
|
|
116
|
+
def __init__(self, transactional_id=None, transaction_timeout_ms=0, retry_backoff_ms=100, api_version=(0, 11), metadata=None):
|
|
117
|
+
self._api_version = api_version
|
|
118
|
+
self._metadata = metadata
|
|
119
|
+
|
|
120
|
+
self._sequence_numbers = collections.defaultdict(lambda: 0)
|
|
121
|
+
# The offset of the last ack'd record for each partition. Used to
|
|
122
|
+
# distinguish retention-based UnknownProducerIdError (broker's
|
|
123
|
+
# log_start_offset > last_acked_offset -> safe to reset and retry)
|
|
124
|
+
# from actual data loss. See KAFKA-5793.
|
|
125
|
+
self._last_acked_offset = {}
|
|
126
|
+
|
|
127
|
+
self.transactional_id = transactional_id
|
|
128
|
+
self.transaction_timeout_ms = transaction_timeout_ms
|
|
129
|
+
self._transaction_coordinator = None
|
|
130
|
+
self._consumer_group_coordinator = None
|
|
131
|
+
self._new_partitions_in_transaction = set()
|
|
132
|
+
self._pending_partitions_in_transaction = set()
|
|
133
|
+
self._partitions_in_transaction = set()
|
|
134
|
+
self._pending_txn_offset_commits = dict()
|
|
135
|
+
|
|
136
|
+
self._current_state = TransactionState.UNINITIALIZED
|
|
137
|
+
self._last_error = None
|
|
138
|
+
self.producer_id_and_epoch = ProducerIdAndEpoch(NO_PRODUCER_ID, NO_PRODUCER_EPOCH)
|
|
139
|
+
|
|
140
|
+
self._transaction_started = False
|
|
141
|
+
|
|
142
|
+
self._pending_requests = [] # priority queue via heapq
|
|
143
|
+
self._pending_requests_sort_id = 0
|
|
144
|
+
self._in_flight_request_correlation_id = self.NO_INFLIGHT_REQUEST_CORRELATION_ID
|
|
145
|
+
|
|
146
|
+
# This is used by the TxnRequestHandlers to control how long to back off before a given request is retried.
|
|
147
|
+
# For instance, this value is lowered by the AddPartitionsToTxnHandler when it receives a CONCURRENT_TRANSACTIONS
|
|
148
|
+
# error for the first AddPartitionsRequest in a transaction.
|
|
149
|
+
self.retry_backoff_ms = retry_backoff_ms
|
|
150
|
+
self._lock = threading.Condition()
|
|
151
|
+
|
|
152
|
+
def initialize_transactions(self):
|
|
153
|
+
with self._lock:
|
|
154
|
+
self._ensure_transactional()
|
|
155
|
+
self._transition_to(TransactionState.INITIALIZING)
|
|
156
|
+
self.set_producer_id_and_epoch(ProducerIdAndEpoch(NO_PRODUCER_ID, NO_PRODUCER_EPOCH))
|
|
157
|
+
self._sequence_numbers.clear()
|
|
158
|
+
handler = InitProducerIdHandler(self, self.transaction_timeout_ms)
|
|
159
|
+
self._enqueue_request(handler)
|
|
160
|
+
return handler.result
|
|
161
|
+
|
|
162
|
+
def init_producer_id(self):
|
|
163
|
+
"""Idempotent (non-transactional) producer: enqueue an InitProducerIdHandler.
|
|
164
|
+
|
|
165
|
+
Drives UNINITIALIZED -> INITIALIZING; the handler completes the
|
|
166
|
+
transition to READY on success. No-op outside UNINITIALIZED so
|
|
167
|
+
repeated calls from the sender's run loop are safe.
|
|
168
|
+
"""
|
|
169
|
+
with self._lock:
|
|
170
|
+
if self.is_transactional():
|
|
171
|
+
raise Errors.IllegalStateError(
|
|
172
|
+
"init_producer_id is for idempotent (non-transactional) producers;"
|
|
173
|
+
" use initialize_transactions for transactional producers")
|
|
174
|
+
if self._current_state != TransactionState.UNINITIALIZED:
|
|
175
|
+
return
|
|
176
|
+
self._transition_to(TransactionState.INITIALIZING)
|
|
177
|
+
self.set_producer_id_and_epoch(ProducerIdAndEpoch(NO_PRODUCER_ID, NO_PRODUCER_EPOCH))
|
|
178
|
+
self._sequence_numbers.clear()
|
|
179
|
+
handler = InitProducerIdHandler(self, 0)
|
|
180
|
+
self._enqueue_request(handler)
|
|
181
|
+
|
|
182
|
+
def begin_transaction(self):
|
|
183
|
+
with self._lock:
|
|
184
|
+
self._ensure_transactional()
|
|
185
|
+
self._maybe_fail_with_error()
|
|
186
|
+
self._transition_to(TransactionState.IN_TRANSACTION)
|
|
187
|
+
|
|
188
|
+
def begin_commit(self):
|
|
189
|
+
with self._lock:
|
|
190
|
+
self._ensure_transactional()
|
|
191
|
+
self._maybe_fail_with_error()
|
|
192
|
+
self._transition_to(TransactionState.COMMITTING_TRANSACTION)
|
|
193
|
+
return self._begin_completing_transaction(True)
|
|
194
|
+
|
|
195
|
+
def begin_abort(self):
|
|
196
|
+
with self._lock:
|
|
197
|
+
self._ensure_transactional()
|
|
198
|
+
if self._current_state != TransactionState.ABORTABLE_ERROR:
|
|
199
|
+
self._maybe_fail_with_error()
|
|
200
|
+
self._transition_to(TransactionState.ABORTING_TRANSACTION)
|
|
201
|
+
|
|
202
|
+
# We're aborting the transaction, so there should be no need to add new partitions
|
|
203
|
+
self._new_partitions_in_transaction.clear()
|
|
204
|
+
return self._begin_completing_transaction(False)
|
|
205
|
+
|
|
206
|
+
def _begin_completing_transaction(self, committed):
|
|
207
|
+
if self._new_partitions_in_transaction:
|
|
208
|
+
self._enqueue_request(self._add_partitions_to_transaction_handler())
|
|
209
|
+
handler = EndTxnHandler(self, committed)
|
|
210
|
+
self._enqueue_request(handler)
|
|
211
|
+
return handler.result
|
|
212
|
+
|
|
213
|
+
def send_offsets_to_transaction(self, offsets, group_metadata):
|
|
214
|
+
"""Send consumer-group offsets as part of the current transaction.
|
|
215
|
+
|
|
216
|
+
Arguments:
|
|
217
|
+
offsets ({TopicPartition: OffsetAndMetadata}): offsets to commit.
|
|
218
|
+
group_metadata (ConsumerGroupMetadata or str): full group metadata
|
|
219
|
+
from KafkaConsumer.group_metadata() (preferred - enables
|
|
220
|
+
broker-side fencing per KIP-447), or a bare group_id string
|
|
221
|
+
for backwards compatibility (broker treats it as v0-v2).
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
FutureRecordMetadata-style Future that completes once the offsets
|
|
225
|
+
are durably committed (or fails fatally).
|
|
226
|
+
"""
|
|
227
|
+
if isinstance(group_metadata, str):
|
|
228
|
+
group_metadata = ConsumerGroupMetadata(group_id=group_metadata)
|
|
229
|
+
elif not isinstance(group_metadata, ConsumerGroupMetadata):
|
|
230
|
+
raise TypeError(
|
|
231
|
+
"send_offsets_to_transaction expects group_metadata to be a "
|
|
232
|
+
"ConsumerGroupMetadata or a group_id str, got %r" % (type(group_metadata),))
|
|
233
|
+
|
|
234
|
+
if group_metadata.generation_id > 0 and not group_metadata.member_id:
|
|
235
|
+
raise ValueError(
|
|
236
|
+
"Invalid ConsumerGroupMetadata: generation_id=%s implies a"
|
|
237
|
+
" joined group but member_id is empty" % (group_metadata.generation_id,))
|
|
238
|
+
|
|
239
|
+
with self._lock:
|
|
240
|
+
self._ensure_transactional()
|
|
241
|
+
self._maybe_fail_with_error()
|
|
242
|
+
if self._current_state != TransactionState.IN_TRANSACTION:
|
|
243
|
+
raise Errors.KafkaError("Cannot send offsets to transaction because the producer is not in an active transaction")
|
|
244
|
+
|
|
245
|
+
log.debug("Begin adding offsets %s for consumer group %s to transaction", offsets, group_metadata.group_id)
|
|
246
|
+
handler = AddOffsetsToTxnHandler(self, group_metadata, offsets)
|
|
247
|
+
self._enqueue_request(handler)
|
|
248
|
+
return handler.result
|
|
249
|
+
|
|
250
|
+
def maybe_add_partition_to_transaction(self, topic_partition):
|
|
251
|
+
with self._lock:
|
|
252
|
+
self._fail_if_not_ready_for_send()
|
|
253
|
+
|
|
254
|
+
if self.is_partition_added(topic_partition) or self.is_partition_pending_add(topic_partition):
|
|
255
|
+
return
|
|
256
|
+
|
|
257
|
+
log.debug("Begin adding new partition %s to transaction", topic_partition)
|
|
258
|
+
self._new_partitions_in_transaction.add(topic_partition)
|
|
259
|
+
|
|
260
|
+
def _fail_if_not_ready_for_send(self):
|
|
261
|
+
with self._lock:
|
|
262
|
+
if self.has_error():
|
|
263
|
+
raise Errors.KafkaError(
|
|
264
|
+
"Cannot perform send because at least one previous transactional or"
|
|
265
|
+
" idempotent request has failed with errors.", self._last_error)
|
|
266
|
+
|
|
267
|
+
if self.is_transactional():
|
|
268
|
+
if not self.has_producer_id():
|
|
269
|
+
raise Errors.IllegalStateError(
|
|
270
|
+
"Cannot perform a 'send' before completing a call to init_transactions"
|
|
271
|
+
" when transactions are enabled.")
|
|
272
|
+
|
|
273
|
+
if self._current_state != TransactionState.IN_TRANSACTION:
|
|
274
|
+
raise Errors.IllegalStateError("Cannot call send in state %s" % (self._current_state.name,))
|
|
275
|
+
|
|
276
|
+
def is_send_to_partition_allowed(self, tp):
|
|
277
|
+
with self._lock:
|
|
278
|
+
if self.has_fatal_error():
|
|
279
|
+
return False
|
|
280
|
+
return not self.is_transactional() or tp in self._partitions_in_transaction
|
|
281
|
+
|
|
282
|
+
def has_producer_id(self, producer_id=None):
|
|
283
|
+
if producer_id is None:
|
|
284
|
+
return self.producer_id_and_epoch.is_valid
|
|
285
|
+
else:
|
|
286
|
+
return self.producer_id_and_epoch.producer_id == producer_id
|
|
287
|
+
|
|
288
|
+
def is_transactional(self):
|
|
289
|
+
return self.transactional_id is not None
|
|
290
|
+
|
|
291
|
+
def has_partitions_to_add(self):
|
|
292
|
+
with self._lock:
|
|
293
|
+
return bool(self._new_partitions_in_transaction) or bool(self._pending_partitions_in_transaction)
|
|
294
|
+
|
|
295
|
+
def is_completing(self):
|
|
296
|
+
with self._lock:
|
|
297
|
+
return self._current_state in (
|
|
298
|
+
TransactionState.COMMITTING_TRANSACTION,
|
|
299
|
+
TransactionState.ABORTING_TRANSACTION)
|
|
300
|
+
|
|
301
|
+
@property
|
|
302
|
+
def last_error(self):
|
|
303
|
+
return self._last_error
|
|
304
|
+
|
|
305
|
+
def has_error(self):
|
|
306
|
+
with self._lock:
|
|
307
|
+
return self._current_state in (
|
|
308
|
+
TransactionState.ABORTABLE_ERROR,
|
|
309
|
+
TransactionState.FATAL_ERROR)
|
|
310
|
+
|
|
311
|
+
def is_bumping_epoch(self):
|
|
312
|
+
with self._lock:
|
|
313
|
+
return self._current_state == TransactionState.BUMPING_PRODUCER_EPOCH
|
|
314
|
+
|
|
315
|
+
# KIP-360 error classification
|
|
316
|
+
#
|
|
317
|
+
# Errors whose correct recovery is to bump the producer epoch via
|
|
318
|
+
# InitProducerIdRequest v3+. On brokers that do not support the bump
|
|
319
|
+
# (api_version < 2.5) these degrade to FATAL for transactional producers
|
|
320
|
+
# and NEEDS_PRODUCER_ID_RESET for non-transactional idempotent producers,
|
|
321
|
+
# matching the pre-KIP-360 behavior.
|
|
322
|
+
_NEEDS_EPOCH_BUMP_ERRORS = frozenset({
|
|
323
|
+
Errors.OutOfOrderSequenceNumberError,
|
|
324
|
+
Errors.UnknownProducerIdError,
|
|
325
|
+
Errors.InvalidProducerEpochError,
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
# Errors that are always fatal regardless of broker version: auth
|
|
329
|
+
# failures, fencing, or structural state corruption where no recovery
|
|
330
|
+
# is possible without operator action.
|
|
331
|
+
_FATAL_ERRORS = frozenset({
|
|
332
|
+
Errors.ClusterAuthorizationFailedError,
|
|
333
|
+
Errors.TransactionalIdAuthorizationFailedError,
|
|
334
|
+
Errors.ProducerFencedError,
|
|
335
|
+
Errors.InvalidTxnStateError,
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
# Classification outcomes returned by classify_batch_error().
|
|
339
|
+
ERROR_CLASS_RETRIABLE = 'RETRIABLE'
|
|
340
|
+
ERROR_CLASS_ABORTABLE = 'ABORTABLE'
|
|
341
|
+
ERROR_CLASS_FATAL = 'FATAL'
|
|
342
|
+
ERROR_CLASS_NEEDS_EPOCH_BUMP = 'NEEDS_EPOCH_BUMP'
|
|
343
|
+
ERROR_CLASS_NEEDS_PRODUCER_ID_RESET = 'NEEDS_PRODUCER_ID_RESET'
|
|
344
|
+
|
|
345
|
+
def _supports_epoch_bump(self):
|
|
346
|
+
"""Return True if the broker supports InitProducerIdRequest v3+ (KIP-360).
|
|
347
|
+
|
|
348
|
+
KIP-360 landed in Kafka 2.5. On older brokers we fall back to the
|
|
349
|
+
pre-KIP-360 recovery: reset producer id for idempotent producers,
|
|
350
|
+
fatal state for transactional producers.
|
|
351
|
+
"""
|
|
352
|
+
return self._api_version >= (2, 5)
|
|
353
|
+
|
|
354
|
+
def classify_batch_error(self, error, batch, log_start_offset=-1):
|
|
355
|
+
"""Categorize a batch-completion error into a recovery outcome.
|
|
356
|
+
|
|
357
|
+
Used by the Sender to decide what to do with a failed batch. This
|
|
358
|
+
method does not mutate any state - it is a pure classification
|
|
359
|
+
helper. The caller is responsible for dispatching to the
|
|
360
|
+
appropriate recovery path.
|
|
361
|
+
|
|
362
|
+
Arguments:
|
|
363
|
+
error (type or BaseException): The error class or instance.
|
|
364
|
+
batch (ProducerBatch): The batch that failed.
|
|
365
|
+
log_start_offset (int): log_start_offset from the broker's
|
|
366
|
+
PartitionProduceResponse, or -1 if unknown / client-side
|
|
367
|
+
failure. Used for KAFKA-5793 retention detection.
|
|
368
|
+
|
|
369
|
+
Returns one of:
|
|
370
|
+
ERROR_CLASS_RETRIABLE - caller should retry the batch
|
|
371
|
+
ERROR_CLASS_ABORTABLE - transactional producer only;
|
|
372
|
+
abort the transaction
|
|
373
|
+
ERROR_CLASS_FATAL - unrecoverable; transition to
|
|
374
|
+
fatal error and fail the batch
|
|
375
|
+
ERROR_CLASS_NEEDS_EPOCH_BUMP - recoverable via KIP-360 epoch
|
|
376
|
+
bump (only when broker supports
|
|
377
|
+
InitProducerIdRequest v3+)
|
|
378
|
+
ERROR_CLASS_NEEDS_PRODUCER_ID_RESET - non-transactional pre-KIP-360
|
|
379
|
+
fallback: reset the
|
|
380
|
+
producer id entirely
|
|
381
|
+
|
|
382
|
+
Note: this classification is for transactional/idempotent producers
|
|
383
|
+
only. Non-idempotent producers don't call this; the Sender uses
|
|
384
|
+
simpler retry/fail logic for them.
|
|
385
|
+
"""
|
|
386
|
+
error_type = error if isinstance(error, type) else type(error)
|
|
387
|
+
|
|
388
|
+
if error_type in self._FATAL_ERRORS:
|
|
389
|
+
return self.ERROR_CLASS_FATAL
|
|
390
|
+
|
|
391
|
+
# KAFKA-5793: a retention-based UnknownProducerIdError is recoverable
|
|
392
|
+
# by resetting the partition's sequence (not a full epoch bump). The
|
|
393
|
+
# Sender checks this condition separately before consulting this
|
|
394
|
+
# classifier, but we mirror the logic here so the classifier alone
|
|
395
|
+
# gives the correct answer for callers that pass log_start_offset.
|
|
396
|
+
if error_type is Errors.UnknownProducerIdError and log_start_offset is not None and log_start_offset >= 0:
|
|
397
|
+
last_acked = self.last_acked_offset(batch.topic_partition)
|
|
398
|
+
if log_start_offset > last_acked:
|
|
399
|
+
return self.ERROR_CLASS_RETRIABLE
|
|
400
|
+
|
|
401
|
+
if error_type in self._NEEDS_EPOCH_BUMP_ERRORS:
|
|
402
|
+
if self._supports_epoch_bump():
|
|
403
|
+
return self.ERROR_CLASS_NEEDS_EPOCH_BUMP
|
|
404
|
+
# Pre-KIP-360 brokers: fall back to the older (lossier) recovery.
|
|
405
|
+
if self.is_transactional():
|
|
406
|
+
return self.ERROR_CLASS_FATAL
|
|
407
|
+
return self.ERROR_CLASS_NEEDS_PRODUCER_ID_RESET
|
|
408
|
+
|
|
409
|
+
# Retriable errors (broker-retriable or client connection errors)
|
|
410
|
+
# become ABORTABLE for transactional producers only if they're
|
|
411
|
+
# non-retriable AND we're in a transaction. The Sender's existing
|
|
412
|
+
# can_retry/can_split logic handles the actual retry decision; this
|
|
413
|
+
# classifier is only consulted for the FAIL branch.
|
|
414
|
+
if issubclass(error_type, Errors.RetriableError):
|
|
415
|
+
return self.ERROR_CLASS_RETRIABLE
|
|
416
|
+
|
|
417
|
+
# Non-retriable, not in the bump or fatal sets: transactional
|
|
418
|
+
# producers should abort the current transaction; non-transactional
|
|
419
|
+
# idempotent producers just fail the batch without any state reset.
|
|
420
|
+
if self.is_transactional():
|
|
421
|
+
return self.ERROR_CLASS_ABORTABLE
|
|
422
|
+
return self.ERROR_CLASS_FATAL
|
|
423
|
+
|
|
424
|
+
def is_aborting(self):
|
|
425
|
+
with self._lock:
|
|
426
|
+
return self._current_state == TransactionState.ABORTING_TRANSACTION
|
|
427
|
+
|
|
428
|
+
def transition_to_abortable_error(self, exc):
|
|
429
|
+
with self._lock:
|
|
430
|
+
if self._current_state == TransactionState.ABORTING_TRANSACTION:
|
|
431
|
+
log.debug("Skipping transition to abortable error state since the transaction is already being "
|
|
432
|
+
" aborted. Underlying exception: %s", exc)
|
|
433
|
+
return
|
|
434
|
+
self._transition_to(TransactionState.ABORTABLE_ERROR, error=exc)
|
|
435
|
+
|
|
436
|
+
def transition_to_fatal_error(self, exc):
|
|
437
|
+
with self._lock:
|
|
438
|
+
self._transition_to(TransactionState.FATAL_ERROR, error=exc)
|
|
439
|
+
|
|
440
|
+
def is_partition_added(self, partition):
|
|
441
|
+
with self._lock:
|
|
442
|
+
return partition in self._partitions_in_transaction
|
|
443
|
+
|
|
444
|
+
def is_partition_pending_add(self, partition):
|
|
445
|
+
return partition in self._new_partitions_in_transaction or partition in self._pending_partitions_in_transaction
|
|
446
|
+
|
|
447
|
+
def has_producer_id_and_epoch(self, producer_id, producer_epoch):
|
|
448
|
+
return (
|
|
449
|
+
self.producer_id_and_epoch.producer_id == producer_id and
|
|
450
|
+
self.producer_id_and_epoch.epoch == producer_epoch
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
def set_producer_id_and_epoch(self, producer_id_and_epoch):
|
|
454
|
+
if not isinstance(producer_id_and_epoch, ProducerIdAndEpoch):
|
|
455
|
+
raise TypeError("ProducerAndIdEpoch type required")
|
|
456
|
+
log.info("ProducerId set to %s with epoch %s",
|
|
457
|
+
producer_id_and_epoch.producer_id, producer_id_and_epoch.epoch)
|
|
458
|
+
self.producer_id_and_epoch = producer_id_and_epoch
|
|
459
|
+
|
|
460
|
+
def reset_producer_id(self):
|
|
461
|
+
"""
|
|
462
|
+
This method is used when the producer needs to reset its internal state because of an irrecoverable exception
|
|
463
|
+
from the broker.
|
|
464
|
+
|
|
465
|
+
We need to reset the producer id and associated state when we have sent a batch to the broker, but we either get
|
|
466
|
+
a non-retriable exception or we run out of retries, or the batch expired in the producer queue after it was already
|
|
467
|
+
sent to the broker.
|
|
468
|
+
|
|
469
|
+
In all of these cases, we don't know whether batch was actually committed on the broker, and hence whether the
|
|
470
|
+
sequence number was actually updated. If we don't reset the producer state, we risk the chance that all future
|
|
471
|
+
messages will return an OutOfOrderSequenceNumberError.
|
|
472
|
+
|
|
473
|
+
Note that we can't reset the producer state for the transactional producer as this would mean bumping the epoch
|
|
474
|
+
for the same producer id. This might involve aborting the ongoing transaction during the initProducerIdRequest,
|
|
475
|
+
and the user would not have any way of knowing this happened. So for the transactional producer,
|
|
476
|
+
it's best to return the produce error to the user and let them abort the transaction and close the producer explicitly.
|
|
477
|
+
"""
|
|
478
|
+
with self._lock:
|
|
479
|
+
if self.is_transactional():
|
|
480
|
+
raise Errors.IllegalStateError(
|
|
481
|
+
"Cannot reset producer state for a transactional producer."
|
|
482
|
+
" You must either abort the ongoing transaction or"
|
|
483
|
+
" reinitialize the transactional producer instead")
|
|
484
|
+
self.set_producer_id_and_epoch(ProducerIdAndEpoch(NO_PRODUCER_ID, NO_PRODUCER_EPOCH))
|
|
485
|
+
self._sequence_numbers.clear()
|
|
486
|
+
self._last_acked_offset.clear()
|
|
487
|
+
|
|
488
|
+
def bump_producer_id_and_epoch(self):
|
|
489
|
+
"""KIP-360: recover from a transient producer-state error by bumping
|
|
490
|
+
the epoch.
|
|
491
|
+
|
|
492
|
+
Transitions to BUMPING_PRODUCER_EPOCH and enqueues an
|
|
493
|
+
InitProducerIdRequest v3+ carrying the current producer_id/epoch.
|
|
494
|
+
When the broker responds with the bumped epoch, _complete_epoch_bump
|
|
495
|
+
transitions back to READY and the sender resumes producing under
|
|
496
|
+
the new epoch. Records in the accumulator that haven't been drained
|
|
497
|
+
yet will be stamped with the new epoch on the next drain.
|
|
498
|
+
|
|
499
|
+
TODO (KAFKA-5793 full): in-flight batches at the moment of the bump
|
|
500
|
+
are lost--their futures fail. Adding in-place rewrite of the
|
|
501
|
+
closed batch buffer (producer_id/epoch/base_sequence fields + CRC
|
|
502
|
+
recompute) would let us retry them under the new epoch without
|
|
503
|
+
losing records.
|
|
504
|
+
|
|
505
|
+
Requires broker >= 2.5 (InitProducerIdRequest v3+). On older
|
|
506
|
+
brokers, Sender falls back to reset_producer_id / fatal instead
|
|
507
|
+
via classify_batch_error.
|
|
508
|
+
|
|
509
|
+
Idempotent: if we're already in BUMPING_PRODUCER_EPOCH, this is a
|
|
510
|
+
no-op. This matters because with max_in_flight > 1, multiple
|
|
511
|
+
in-flight batches may all fail with the same epoch-bump-triggering
|
|
512
|
+
error in quick succession; only the first should drive the bump.
|
|
513
|
+
"""
|
|
514
|
+
with self._lock:
|
|
515
|
+
if self._current_state == TransactionState.BUMPING_PRODUCER_EPOCH:
|
|
516
|
+
return
|
|
517
|
+
if self._current_state == TransactionState.FATAL_ERROR:
|
|
518
|
+
return
|
|
519
|
+
if not self._supports_epoch_bump():
|
|
520
|
+
raise Errors.IllegalStateError(
|
|
521
|
+
"Cannot bump producer epoch: broker version %s does not support KIP-360 "
|
|
522
|
+
"(InitProducerIdRequest v3+ requires Kafka 2.5+)" % (self._api_version,))
|
|
523
|
+
log.warning("Bumping producer epoch for %s after recoverable error",
|
|
524
|
+
self.producer_id_and_epoch)
|
|
525
|
+
self._transition_to(TransactionState.BUMPING_PRODUCER_EPOCH)
|
|
526
|
+
# Drop all per-partition sequence state. The bumped epoch starts
|
|
527
|
+
# each partition at sequence 0. last_acked_offset is also cleared
|
|
528
|
+
# since it's tied to the pre-bump producer_id/epoch range.
|
|
529
|
+
self._sequence_numbers.clear()
|
|
530
|
+
self._last_acked_offset.clear()
|
|
531
|
+
# Transactional state: the broker aborts any in-flight
|
|
532
|
+
# transaction as part of processing InitProducerIdRequest v3+
|
|
533
|
+
# with a matching producer_id/epoch, so we clear our local
|
|
534
|
+
# view of which partitions are in the transaction. The user's
|
|
535
|
+
# ongoing begin/commit/abort coroutine (if any) will see the
|
|
536
|
+
# bump via the _result and can react accordingly.
|
|
537
|
+
self._transaction_started = False
|
|
538
|
+
self._partitions_in_transaction.clear()
|
|
539
|
+
self._new_partitions_in_transaction.clear()
|
|
540
|
+
self._pending_partitions_in_transaction.clear()
|
|
541
|
+
handler = InitProducerIdHandler(self, self.transaction_timeout_ms, is_epoch_bump=True)
|
|
542
|
+
self._enqueue_request(handler)
|
|
543
|
+
|
|
544
|
+
def _complete_epoch_bump(self):
|
|
545
|
+
"""Called from InitProducerIdHandler on successful bump response.
|
|
546
|
+
|
|
547
|
+
Transitions BUMPING_PRODUCER_EPOCH -> READY so the sender resumes
|
|
548
|
+
producing under the new epoch.
|
|
549
|
+
"""
|
|
550
|
+
# Caller (handle_response) already holds _lock.
|
|
551
|
+
self._transition_to(TransactionState.READY)
|
|
552
|
+
self._last_error = None
|
|
553
|
+
|
|
554
|
+
def _restart_epoch_bump_without_producer_id(self, transaction_timeout_ms, result):
|
|
555
|
+
"""Called from InitProducerIdHandler when the broker rejects the bump
|
|
556
|
+
with INVALID_PRODUCER_EPOCH (our producer_id/epoch are stale).
|
|
557
|
+
|
|
558
|
+
Falls back to requesting a fresh producer_id by enqueuing a new
|
|
559
|
+
InitProducerIdRequest without the producer_id/epoch fields. The
|
|
560
|
+
original TransactionalRequestResult is re-used so the caller waits
|
|
561
|
+
on the overall bump-then-init sequence.
|
|
562
|
+
"""
|
|
563
|
+
# Caller (handle_response) already holds _lock.
|
|
564
|
+
self.set_producer_id_and_epoch(ProducerIdAndEpoch(NO_PRODUCER_ID, NO_PRODUCER_EPOCH))
|
|
565
|
+
# Stay in BUMPING_PRODUCER_EPOCH; the follow-up init will transition
|
|
566
|
+
# to READY on success via the regular (non-bump) code path.
|
|
567
|
+
handler = InitProducerIdHandler(self, transaction_timeout_ms, is_epoch_bump=False)
|
|
568
|
+
handler._result = result # thread the caller's result through
|
|
569
|
+
self._enqueue_request(handler)
|
|
570
|
+
|
|
571
|
+
def sequence_number(self, tp):
|
|
572
|
+
with self._lock:
|
|
573
|
+
return self._sequence_numbers[tp]
|
|
574
|
+
|
|
575
|
+
def increment_sequence_number(self, tp, increment):
|
|
576
|
+
with self._lock:
|
|
577
|
+
if tp not in self._sequence_numbers:
|
|
578
|
+
raise Errors.IllegalStateError("Attempt to increment sequence number for a partition with no current sequence.")
|
|
579
|
+
# Sequence number wraps at java max int
|
|
580
|
+
base = self._sequence_numbers[tp]
|
|
581
|
+
if base > (2147483647 - increment):
|
|
582
|
+
self._sequence_numbers[tp] = increment - (2147483647 - base) - 1
|
|
583
|
+
else:
|
|
584
|
+
self._sequence_numbers[tp] += increment
|
|
585
|
+
|
|
586
|
+
def set_sequence_number(self, tp, sequence):
|
|
587
|
+
with self._lock:
|
|
588
|
+
self._sequence_numbers[tp] = sequence
|
|
589
|
+
|
|
590
|
+
def reset_sequence_for_partition(self, tp):
|
|
591
|
+
with self._lock:
|
|
592
|
+
self._sequence_numbers.pop(tp, None)
|
|
593
|
+
self._last_acked_offset.pop(tp, None)
|
|
594
|
+
|
|
595
|
+
def update_last_acked_offset(self, tp, base_offset, record_count):
|
|
596
|
+
"""Record the offset of the last successfully-produced record for tp.
|
|
597
|
+
|
|
598
|
+
Called from the sender on each successful batch completion. The
|
|
599
|
+
last acked offset is used to detect whether a subsequent
|
|
600
|
+
UnknownProducerIdError reflects retention (safe to retry) vs. real
|
|
601
|
+
data loss (fatal). See KAFKA-5793.
|
|
602
|
+
"""
|
|
603
|
+
if base_offset < 0:
|
|
604
|
+
return
|
|
605
|
+
last_offset = base_offset + record_count - 1
|
|
606
|
+
with self._lock:
|
|
607
|
+
if last_offset > self._last_acked_offset.get(tp, -1):
|
|
608
|
+
self._last_acked_offset[tp] = last_offset
|
|
609
|
+
|
|
610
|
+
def last_acked_offset(self, tp):
|
|
611
|
+
with self._lock:
|
|
612
|
+
return self._last_acked_offset.get(tp, -1)
|
|
613
|
+
|
|
614
|
+
def next_request_handler(self, has_incomplete_batches):
|
|
615
|
+
with self._lock:
|
|
616
|
+
if self._new_partitions_in_transaction:
|
|
617
|
+
self._enqueue_request(self._add_partitions_to_transaction_handler())
|
|
618
|
+
|
|
619
|
+
if not self._pending_requests:
|
|
620
|
+
return None
|
|
621
|
+
|
|
622
|
+
_, _, next_request_handler = self._pending_requests[0]
|
|
623
|
+
# Do not send the EndTxn until all batches have been flushed
|
|
624
|
+
if isinstance(next_request_handler, EndTxnHandler) and has_incomplete_batches:
|
|
625
|
+
return None
|
|
626
|
+
|
|
627
|
+
heapq.heappop(self._pending_requests)
|
|
628
|
+
if self._maybe_terminate_request_with_error(next_request_handler):
|
|
629
|
+
log.debug("Not sending transactional request %s because we are in an error state",
|
|
630
|
+
next_request_handler.request)
|
|
631
|
+
return None
|
|
632
|
+
|
|
633
|
+
if isinstance(next_request_handler, EndTxnHandler) and not self._transaction_started:
|
|
634
|
+
next_request_handler.result.done()
|
|
635
|
+
if self._current_state != TransactionState.FATAL_ERROR:
|
|
636
|
+
log.debug("Not sending EndTxn for completed transaction since no partitions"
|
|
637
|
+
" or offsets were successfully added")
|
|
638
|
+
self._complete_transaction()
|
|
639
|
+
try:
|
|
640
|
+
_, _, next_request_handler = heapq.heappop(self._pending_requests)
|
|
641
|
+
except IndexError:
|
|
642
|
+
next_request_handler = None
|
|
643
|
+
|
|
644
|
+
if next_request_handler:
|
|
645
|
+
log.debug("Request %s dequeued for sending", next_request_handler.request)
|
|
646
|
+
|
|
647
|
+
return next_request_handler
|
|
648
|
+
|
|
649
|
+
def retry(self, request):
|
|
650
|
+
with self._lock:
|
|
651
|
+
request.set_retry()
|
|
652
|
+
self._enqueue_request(request)
|
|
653
|
+
|
|
654
|
+
def authentication_failed(self, exc):
|
|
655
|
+
with self._lock:
|
|
656
|
+
for _, _, request in self._pending_requests:
|
|
657
|
+
request.fatal_error(exc)
|
|
658
|
+
|
|
659
|
+
def coordinator(self, coord_type):
|
|
660
|
+
if coord_type == CoordinatorType.GROUP:
|
|
661
|
+
return self._consumer_group_coordinator
|
|
662
|
+
elif coord_type == CoordinatorType.TRANSACTION:
|
|
663
|
+
return self._transaction_coordinator
|
|
664
|
+
else:
|
|
665
|
+
raise Errors.IllegalStateError("Received an invalid coordinator type: %s" % (coord_type,))
|
|
666
|
+
|
|
667
|
+
def lookup_coordinator_for_request(self, request):
|
|
668
|
+
self._lookup_coordinator(request.coordinator_type, request.coordinator_key)
|
|
669
|
+
|
|
670
|
+
def next_in_flight_request_correlation_id(self):
|
|
671
|
+
self._in_flight_request_correlation_id += 1
|
|
672
|
+
return self._in_flight_request_correlation_id
|
|
673
|
+
|
|
674
|
+
def clear_in_flight_transactional_request_correlation_id(self):
|
|
675
|
+
self._in_flight_request_correlation_id = self.NO_INFLIGHT_REQUEST_CORRELATION_ID
|
|
676
|
+
|
|
677
|
+
def has_in_flight_transactional_request(self):
|
|
678
|
+
return self._in_flight_request_correlation_id != self.NO_INFLIGHT_REQUEST_CORRELATION_ID
|
|
679
|
+
|
|
680
|
+
def has_fatal_error(self):
|
|
681
|
+
return self._current_state == TransactionState.FATAL_ERROR
|
|
682
|
+
|
|
683
|
+
def has_abortable_error(self):
|
|
684
|
+
return self._current_state == TransactionState.ABORTABLE_ERROR
|
|
685
|
+
|
|
686
|
+
# visible for testing
|
|
687
|
+
def _test_transaction_contains_partition(self, tp):
|
|
688
|
+
with self._lock:
|
|
689
|
+
return tp in self._partitions_in_transaction
|
|
690
|
+
|
|
691
|
+
# visible for testing
|
|
692
|
+
def _test_has_pending_offset_commits(self):
|
|
693
|
+
return bool(self._pending_txn_offset_commits)
|
|
694
|
+
|
|
695
|
+
# visible for testing
|
|
696
|
+
def _test_has_ongoing_transaction(self):
|
|
697
|
+
with self._lock:
|
|
698
|
+
# transactions are considered ongoing once started until completion or a fatal error
|
|
699
|
+
return self._current_state == TransactionState.IN_TRANSACTION or self.is_completing() or self.has_abortable_error()
|
|
700
|
+
|
|
701
|
+
# visible for testing
|
|
702
|
+
def _test_is_ready(self):
|
|
703
|
+
with self._lock:
|
|
704
|
+
return self.is_transactional() and self._current_state == TransactionState.READY
|
|
705
|
+
|
|
706
|
+
def _transition_to(self, target, error=None):
|
|
707
|
+
with self._lock:
|
|
708
|
+
if not self._current_state.is_transition_valid(self._current_state, target):
|
|
709
|
+
raise Errors.KafkaError("TransactionalId %s: Invalid transition attempted from state %s to state %s" % (
|
|
710
|
+
self.transactional_id, self._current_state.name, target.name))
|
|
711
|
+
|
|
712
|
+
if target in (TransactionState.FATAL_ERROR, TransactionState.ABORTABLE_ERROR):
|
|
713
|
+
if error is None:
|
|
714
|
+
raise Errors.IllegalArgumentError("Cannot transition to %s with an None exception" % (target.name,))
|
|
715
|
+
self._last_error = error
|
|
716
|
+
else:
|
|
717
|
+
self._last_error = None
|
|
718
|
+
|
|
719
|
+
if self._last_error is not None:
|
|
720
|
+
log.debug("Transition from state %s to error state %s (%s)", self._current_state.name, target.name, self._last_error)
|
|
721
|
+
else:
|
|
722
|
+
log.debug("Transition from state %s to %s", self._current_state, target)
|
|
723
|
+
self._current_state = target
|
|
724
|
+
|
|
725
|
+
def _ensure_transactional(self):
|
|
726
|
+
if not self.is_transactional():
|
|
727
|
+
raise Errors.IllegalStateError("Transactional method invoked on a non-transactional producer.")
|
|
728
|
+
|
|
729
|
+
def _maybe_fail_with_error(self):
|
|
730
|
+
if self.has_error():
|
|
731
|
+
raise Errors.KafkaError("Cannot execute transactional method because we are in an error state: %s" % (self._last_error,))
|
|
732
|
+
|
|
733
|
+
def _maybe_terminate_request_with_error(self, request_handler):
|
|
734
|
+
if self.has_error():
|
|
735
|
+
if self.has_abortable_error() and isinstance(request_handler, FindCoordinatorHandler):
|
|
736
|
+
# No harm letting the FindCoordinator request go through if we're expecting to abort
|
|
737
|
+
return False
|
|
738
|
+
request_handler.fail(self._last_error)
|
|
739
|
+
return True
|
|
740
|
+
return False
|
|
741
|
+
|
|
742
|
+
def _next_pending_requests_sort_id(self):
|
|
743
|
+
self._pending_requests_sort_id += 1
|
|
744
|
+
return self._pending_requests_sort_id
|
|
745
|
+
|
|
746
|
+
def _enqueue_request(self, request_handler):
|
|
747
|
+
log.debug("Enqueuing transactional request %s", request_handler.request)
|
|
748
|
+
heapq.heappush(
|
|
749
|
+
self._pending_requests,
|
|
750
|
+
(
|
|
751
|
+
request_handler.priority, # keep lowest priority at head of queue
|
|
752
|
+
self._next_pending_requests_sort_id(), # break ties
|
|
753
|
+
request_handler
|
|
754
|
+
)
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
def _lookup_coordinator(self, coord_type, coord_key):
|
|
758
|
+
with self._lock:
|
|
759
|
+
if coord_type == CoordinatorType.GROUP:
|
|
760
|
+
self._consumer_group_coordinator = None
|
|
761
|
+
elif coord_type == CoordinatorType.TRANSACTION:
|
|
762
|
+
self._transaction_coordinator = None
|
|
763
|
+
else:
|
|
764
|
+
raise Errors.IllegalStateError("Invalid coordinator type: %s" % (coord_type,))
|
|
765
|
+
self._enqueue_request(FindCoordinatorHandler(self, coord_type, coord_key))
|
|
766
|
+
|
|
767
|
+
def _complete_transaction(self):
|
|
768
|
+
with self._lock:
|
|
769
|
+
self._transition_to(TransactionState.READY)
|
|
770
|
+
self._transaction_started = False
|
|
771
|
+
self._new_partitions_in_transaction.clear()
|
|
772
|
+
self._pending_partitions_in_transaction.clear()
|
|
773
|
+
self._partitions_in_transaction.clear()
|
|
774
|
+
|
|
775
|
+
def _add_partitions_to_transaction_handler(self):
|
|
776
|
+
with self._lock:
|
|
777
|
+
self._pending_partitions_in_transaction.update(self._new_partitions_in_transaction)
|
|
778
|
+
self._new_partitions_in_transaction.clear()
|
|
779
|
+
return AddPartitionsToTxnHandler(self, self._pending_partitions_in_transaction)
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
class TransactionalRequestResult:
|
|
783
|
+
def __init__(self):
|
|
784
|
+
self._latch = threading.Event()
|
|
785
|
+
self._error = None
|
|
786
|
+
|
|
787
|
+
def done(self, error=None):
|
|
788
|
+
self._error = error
|
|
789
|
+
self._latch.set()
|
|
790
|
+
|
|
791
|
+
def wait(self, timeout_ms=None):
|
|
792
|
+
timeout = timeout_ms / 1000 if timeout_ms is not None else None
|
|
793
|
+
success = self._latch.wait(timeout)
|
|
794
|
+
if self._error:
|
|
795
|
+
raise self._error
|
|
796
|
+
return success
|
|
797
|
+
|
|
798
|
+
@property
|
|
799
|
+
def is_done(self):
|
|
800
|
+
return self._latch.is_set()
|
|
801
|
+
|
|
802
|
+
@property
|
|
803
|
+
def succeeded(self):
|
|
804
|
+
return self._latch.is_set() and self._error is None
|
|
805
|
+
|
|
806
|
+
@property
|
|
807
|
+
def failed(self):
|
|
808
|
+
return self._latch.is_set() and self._error is not None
|
|
809
|
+
|
|
810
|
+
@property
|
|
811
|
+
def exception(self):
|
|
812
|
+
return self._error
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
class TxnRequestHandler(ABC):
|
|
816
|
+
def __init__(self, transaction_manager, result=None):
|
|
817
|
+
self.transaction_manager = transaction_manager
|
|
818
|
+
self.retry_backoff_ms = transaction_manager.retry_backoff_ms
|
|
819
|
+
self.request = None
|
|
820
|
+
self._result = result or TransactionalRequestResult()
|
|
821
|
+
self._is_retry = False
|
|
822
|
+
|
|
823
|
+
@property
|
|
824
|
+
def transactional_id(self):
|
|
825
|
+
return self.transaction_manager.transactional_id
|
|
826
|
+
|
|
827
|
+
@property
|
|
828
|
+
def producer_id(self):
|
|
829
|
+
return self.transaction_manager.producer_id_and_epoch.producer_id
|
|
830
|
+
|
|
831
|
+
@property
|
|
832
|
+
def producer_epoch(self):
|
|
833
|
+
return self.transaction_manager.producer_id_and_epoch.epoch
|
|
834
|
+
|
|
835
|
+
def fatal_error(self, exc):
|
|
836
|
+
log.error(f'Fatal Error handling request {self.request.name if self.request else "none"}: {exc}')
|
|
837
|
+
self.transaction_manager.transition_to_fatal_error(exc)
|
|
838
|
+
self._result.done(error=exc)
|
|
839
|
+
|
|
840
|
+
def abortable_error(self, exc):
|
|
841
|
+
self.transaction_manager.transition_to_abortable_error(exc)
|
|
842
|
+
self._result.done(error=exc)
|
|
843
|
+
|
|
844
|
+
def fail(self, exc):
|
|
845
|
+
self._result.done(error=exc)
|
|
846
|
+
|
|
847
|
+
def reenqueue(self):
|
|
848
|
+
with self.transaction_manager._lock:
|
|
849
|
+
self._is_retry = True
|
|
850
|
+
self.transaction_manager._enqueue_request(self)
|
|
851
|
+
|
|
852
|
+
def on_complete(self, correlation_id, response_or_exc):
|
|
853
|
+
if correlation_id != self.transaction_manager._in_flight_request_correlation_id:
|
|
854
|
+
self.fatal_error(RuntimeError("Detected more than one in-flight transactional request."))
|
|
855
|
+
else:
|
|
856
|
+
self.transaction_manager.clear_in_flight_transactional_request_correlation_id()
|
|
857
|
+
if isinstance(response_or_exc, Errors.KafkaConnectionError):
|
|
858
|
+
log.debug("Disconnected from node. Will retry.")
|
|
859
|
+
if self.needs_coordinator():
|
|
860
|
+
self.transaction_manager._lookup_coordinator(self.coordinator_type, self.coordinator_key)
|
|
861
|
+
self.reenqueue()
|
|
862
|
+
elif isinstance(response_or_exc, Errors.UnsupportedVersionError):
|
|
863
|
+
self.fatal_error(response_or_exc)
|
|
864
|
+
elif not isinstance(response_or_exc, (Exception, type(None))):
|
|
865
|
+
log.debug("Received transactional response %s for request %s", response_or_exc, self.request)
|
|
866
|
+
with self.transaction_manager._lock:
|
|
867
|
+
self.handle_response(response_or_exc)
|
|
868
|
+
else:
|
|
869
|
+
self.fatal_error(Errors.KafkaError("Could not execute transactional request for unknown reasons: %s" % response_or_exc))
|
|
870
|
+
|
|
871
|
+
def needs_coordinator(self):
|
|
872
|
+
return self.coordinator_type is not None
|
|
873
|
+
|
|
874
|
+
@property
|
|
875
|
+
def result(self):
|
|
876
|
+
return self._result
|
|
877
|
+
|
|
878
|
+
@property
|
|
879
|
+
def coordinator_type(self):
|
|
880
|
+
return CoordinatorType.TRANSACTION
|
|
881
|
+
|
|
882
|
+
@property
|
|
883
|
+
def coordinator_key(self):
|
|
884
|
+
return self.transaction_manager.transactional_id
|
|
885
|
+
|
|
886
|
+
def set_retry(self):
|
|
887
|
+
self._is_retry = True
|
|
888
|
+
|
|
889
|
+
@property
|
|
890
|
+
def is_retry(self):
|
|
891
|
+
return self._is_retry
|
|
892
|
+
|
|
893
|
+
@abstractmethod
|
|
894
|
+
def handle_response(self, response):
|
|
895
|
+
pass
|
|
896
|
+
|
|
897
|
+
@abstractproperty
|
|
898
|
+
def priority(self):
|
|
899
|
+
pass
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
class InitProducerIdHandler(TxnRequestHandler):
|
|
903
|
+
def __init__(self, transaction_manager, transaction_timeout_ms, is_epoch_bump=False):
|
|
904
|
+
super().__init__(transaction_manager)
|
|
905
|
+
self._is_epoch_bump = is_epoch_bump
|
|
906
|
+
max_version = 4
|
|
907
|
+
min_version = 0
|
|
908
|
+
|
|
909
|
+
if is_epoch_bump:
|
|
910
|
+
# KIP-360 / InitProducerIdRequest v3+ (Kafka 2.5+) lets us resume
|
|
911
|
+
# an existing producer_id by bumping its epoch rather than allocating
|
|
912
|
+
# a fresh one. v3+ takes producer_id + epoch fields; on broker match,
|
|
913
|
+
# the broker returns (same producer_id, epoch+1).
|
|
914
|
+
min_version = 3
|
|
915
|
+
producer_id = transaction_manager.producer_id_and_epoch.producer_id
|
|
916
|
+
producer_epoch = transaction_manager.producer_id_and_epoch.epoch
|
|
917
|
+
else:
|
|
918
|
+
producer_id = NO_PRODUCER_ID
|
|
919
|
+
producer_epoch = NO_PRODUCER_EPOCH
|
|
920
|
+
|
|
921
|
+
self.request = InitProducerIdRequest(
|
|
922
|
+
transactional_id=self.transactional_id,
|
|
923
|
+
transaction_timeout_ms=transaction_timeout_ms,
|
|
924
|
+
producer_id=producer_id,
|
|
925
|
+
producer_epoch=producer_epoch,
|
|
926
|
+
max_version=max_version,
|
|
927
|
+
min_version=min_version)
|
|
928
|
+
|
|
929
|
+
@property
|
|
930
|
+
def priority(self):
|
|
931
|
+
return Priority.INIT_PRODUCER_ID
|
|
932
|
+
|
|
933
|
+
@property
|
|
934
|
+
def coordinator_type(self):
|
|
935
|
+
# Idempotent (non-transactional) producers don't have a transaction
|
|
936
|
+
# coordinator -- InitProducerIdRequest can be sent to any broker.
|
|
937
|
+
if self.transaction_manager.transactional_id is None:
|
|
938
|
+
return None
|
|
939
|
+
return CoordinatorType.TRANSACTION
|
|
940
|
+
|
|
941
|
+
def handle_response(self, response):
|
|
942
|
+
error_type = Errors.for_code(response.error_code)
|
|
943
|
+
|
|
944
|
+
if error_type is Errors.NoError:
|
|
945
|
+
self.transaction_manager.set_producer_id_and_epoch(ProducerIdAndEpoch(response.producer_id, response.producer_epoch))
|
|
946
|
+
if self._is_epoch_bump:
|
|
947
|
+
self.transaction_manager._complete_epoch_bump()
|
|
948
|
+
else:
|
|
949
|
+
self.transaction_manager._transition_to(TransactionState.READY)
|
|
950
|
+
self._result.done()
|
|
951
|
+
elif issubclass(error_type, Errors.RetriableError):
|
|
952
|
+
if error_type in (Errors.NotCoordinatorError, Errors.CoordinatorNotAvailableError):
|
|
953
|
+
self.transaction_manager._lookup_coordinator(CoordinatorType.TRANSACTION, self.transactional_id)
|
|
954
|
+
self.reenqueue()
|
|
955
|
+
elif error_type is Errors.InvalidProducerEpochError and self._is_epoch_bump:
|
|
956
|
+
# KIP-360: our (producer_id, epoch) are stale--the broker no
|
|
957
|
+
# longer recognizes them. Fall back to allocating a fresh
|
|
958
|
+
# producer_id by reissuing InitProducerIdRequest without
|
|
959
|
+
# producer_id/epoch fields.
|
|
960
|
+
log.info("InitProducerId bump rejected with INVALID_PRODUCER_EPOCH; "
|
|
961
|
+
"falling back to a fresh producer id")
|
|
962
|
+
self.transaction_manager._restart_epoch_bump_without_producer_id(
|
|
963
|
+
self.request.transaction_timeout_ms, self._result)
|
|
964
|
+
elif error_type in (Errors.ProducerFencedError, Errors.InvalidProducerEpochError):
|
|
965
|
+
# Another producer instance has taken over this transactional_id.
|
|
966
|
+
# Fatal--the application must rebuild the producer. Mirrors the
|
|
967
|
+
# Java client, which normalizes INVALID_PRODUCER_EPOCH to
|
|
968
|
+
# PRODUCER_FENCED on the non-bump InitProducerId path.
|
|
969
|
+
self.fatal_error(Errors.ProducerFencedError())
|
|
970
|
+
elif error_type is Errors.TransactionalIdAuthorizationFailedError:
|
|
971
|
+
self.fatal_error(error_type())
|
|
972
|
+
else:
|
|
973
|
+
self.fatal_error(Errors.KafkaError("Unexpected error in InitProducerIdResponse: %s" % (error_type())))
|
|
974
|
+
|
|
975
|
+
class AddPartitionsToTxnHandler(TxnRequestHandler):
|
|
976
|
+
def __init__(self, transaction_manager, topic_partitions):
|
|
977
|
+
super().__init__(transaction_manager)
|
|
978
|
+
|
|
979
|
+
topic_data = collections.defaultdict(list)
|
|
980
|
+
for tp in topic_partitions:
|
|
981
|
+
topic_data[tp.topic].append(tp.partition)
|
|
982
|
+
|
|
983
|
+
Topic = AddPartitionsToTxnRequest.AddPartitionsToTxnTopic
|
|
984
|
+
self.request = AddPartitionsToTxnRequest(
|
|
985
|
+
v3_and_below_transactional_id=self.transactional_id,
|
|
986
|
+
v3_and_below_producer_id=self.producer_id,
|
|
987
|
+
v3_and_below_producer_epoch=self.producer_epoch,
|
|
988
|
+
v3_and_below_topics=[Topic(name=topic, partitions=partitions)
|
|
989
|
+
for topic, partitions in topic_data.items()],
|
|
990
|
+
max_version=3)
|
|
991
|
+
|
|
992
|
+
@property
|
|
993
|
+
def priority(self):
|
|
994
|
+
return Priority.ADD_PARTITIONS_OR_OFFSETS
|
|
995
|
+
|
|
996
|
+
def handle_response(self, response):
|
|
997
|
+
has_partition_errors = False
|
|
998
|
+
unauthorized_topics = set()
|
|
999
|
+
self.retry_backoff_ms = self.transaction_manager.retry_backoff_ms
|
|
1000
|
+
|
|
1001
|
+
results = {TopicPartition(topic, partition): Errors.for_code(error_code)
|
|
1002
|
+
for topic, partition_data in response.results_by_topic_v3_and_below
|
|
1003
|
+
for partition, error_code in partition_data}
|
|
1004
|
+
|
|
1005
|
+
for tp, error_type in results.items():
|
|
1006
|
+
if error_type is Errors.NoError:
|
|
1007
|
+
continue
|
|
1008
|
+
elif issubclass(error_type, Errors.RetriableError):
|
|
1009
|
+
if error_type in (Errors.CoordinatorNotAvailableError, Errors.NotCoordinatorError):
|
|
1010
|
+
self.transaction_manager._lookup_coordinator(CoordinatorType.TRANSACTION, self.transactional_id)
|
|
1011
|
+
elif error_type is Errors.ConcurrentTransactionsError:
|
|
1012
|
+
self.maybe_override_retry_backoff_ms()
|
|
1013
|
+
self.reenqueue()
|
|
1014
|
+
return
|
|
1015
|
+
elif error_type in (Errors.InvalidProducerEpochError, Errors.ProducerFencedError):
|
|
1016
|
+
# Java client normalizes INVALID_PRODUCER_EPOCH to PRODUCER_FENCED
|
|
1017
|
+
# on the txn-coordinator RPC paths (KIP-360).
|
|
1018
|
+
self.fatal_error(Errors.ProducerFencedError())
|
|
1019
|
+
return
|
|
1020
|
+
elif error_type is Errors.TransactionalIdAuthorizationFailedError:
|
|
1021
|
+
self.fatal_error(error_type())
|
|
1022
|
+
return
|
|
1023
|
+
elif error_type in (Errors.InvalidProducerIdMappingError, Errors.InvalidTxnStateError):
|
|
1024
|
+
self.fatal_error(Errors.KafkaError(error_type()))
|
|
1025
|
+
return
|
|
1026
|
+
elif error_type is Errors.TopicAuthorizationFailedError:
|
|
1027
|
+
unauthorized_topics.add(tp.topic)
|
|
1028
|
+
elif error_type is Errors.OperationNotAttemptedError:
|
|
1029
|
+
log.debug("Did not attempt to add partition %s to transaction because other partitions in the"
|
|
1030
|
+
" batch had errors.", tp)
|
|
1031
|
+
has_partition_errors = True
|
|
1032
|
+
else:
|
|
1033
|
+
log.error("Could not add partition %s due to unexpected error %s", tp, error_type())
|
|
1034
|
+
has_partition_errors = True
|
|
1035
|
+
|
|
1036
|
+
partitions = set(results)
|
|
1037
|
+
|
|
1038
|
+
# Remove the partitions from the pending set regardless of the result. We use the presence
|
|
1039
|
+
# of partitions in the pending set to know when it is not safe to send batches. However, if
|
|
1040
|
+
# the partitions failed to be added and we enter an error state, we expect the batches to be
|
|
1041
|
+
# aborted anyway. In this case, we must be able to continue sending the batches which are in
|
|
1042
|
+
# retry for partitions that were successfully added.
|
|
1043
|
+
self.transaction_manager._pending_partitions_in_transaction -= partitions
|
|
1044
|
+
|
|
1045
|
+
if unauthorized_topics:
|
|
1046
|
+
self.abortable_error(Errors.TopicAuthorizationFailedError(unauthorized_topics))
|
|
1047
|
+
elif has_partition_errors:
|
|
1048
|
+
self.abortable_error(Errors.KafkaError("Could not add partitions to transaction due to errors: %s" % (results)))
|
|
1049
|
+
else:
|
|
1050
|
+
log.debug("Successfully added partitions %s to transaction", partitions)
|
|
1051
|
+
self.transaction_manager._partitions_in_transaction.update(partitions)
|
|
1052
|
+
self.transaction_manager._transaction_started = True
|
|
1053
|
+
self._result.done()
|
|
1054
|
+
|
|
1055
|
+
def maybe_override_retry_backoff_ms(self):
|
|
1056
|
+
# We only want to reduce the backoff when retrying the first AddPartition which errored out due to a
|
|
1057
|
+
# CONCURRENT_TRANSACTIONS error since this means that the previous transaction is still completing and
|
|
1058
|
+
# we don't want to wait too long before trying to start the new one.
|
|
1059
|
+
#
|
|
1060
|
+
# This is only a temporary fix, the long term solution is being tracked in
|
|
1061
|
+
# https://issues.apache.org/jira/browse/KAFKA-5482
|
|
1062
|
+
if not self.transaction_manager._partitions_in_transaction:
|
|
1063
|
+
self.retry_backoff_ms = min(self.transaction_manager.ADD_PARTITIONS_RETRY_BACKOFF_MS, self.retry_backoff_ms)
|
|
1064
|
+
|
|
1065
|
+
|
|
1066
|
+
class FindCoordinatorHandler(TxnRequestHandler):
|
|
1067
|
+
def __init__(self, transaction_manager, coord_type, coord_key):
|
|
1068
|
+
super().__init__(transaction_manager)
|
|
1069
|
+
|
|
1070
|
+
self._coord_type = CoordinatorType.build_from(coord_type)
|
|
1071
|
+
self._coord_key = coord_key
|
|
1072
|
+
# Setting key, key_type, and coordinator_keys all at once lets the
|
|
1073
|
+
# connection layer negotiate any version: v0-v3 emit `key`/`key_type`,
|
|
1074
|
+
# v4+ (KIP-699) emit `key_type`/`coordinator_keys`.
|
|
1075
|
+
self.request = FindCoordinatorRequest(
|
|
1076
|
+
key=self._coord_key,
|
|
1077
|
+
key_type=self._coord_type.value,
|
|
1078
|
+
coordinator_keys=[self._coord_key],
|
|
1079
|
+
)
|
|
1080
|
+
|
|
1081
|
+
@property
|
|
1082
|
+
def priority(self):
|
|
1083
|
+
return Priority.FIND_COORDINATOR
|
|
1084
|
+
|
|
1085
|
+
@property
|
|
1086
|
+
def coordinator_type(self):
|
|
1087
|
+
return None
|
|
1088
|
+
|
|
1089
|
+
@property
|
|
1090
|
+
def coordinator_key(self):
|
|
1091
|
+
return None
|
|
1092
|
+
|
|
1093
|
+
def handle_response(self, response):
|
|
1094
|
+
# v4+ returns results in a Coordinators array; we always send a single
|
|
1095
|
+
# key, so the first entry is ours. v0-v3 returns top-level fields.
|
|
1096
|
+
result = response.coordinators[0] if response.coordinators else response
|
|
1097
|
+
error_type = Errors.for_code(result.error_code)
|
|
1098
|
+
|
|
1099
|
+
if error_type is Errors.NoError:
|
|
1100
|
+
coordinator_id = self.transaction_manager._metadata.add_coordinator(
|
|
1101
|
+
result, self._coord_type, self._coord_key)
|
|
1102
|
+
if self._coord_type == CoordinatorType.GROUP:
|
|
1103
|
+
self.transaction_manager._consumer_group_coordinator = coordinator_id
|
|
1104
|
+
elif self._coord_type == CoordinatorType.TRANSACTION:
|
|
1105
|
+
self.transaction_manager._transaction_coordinator = coordinator_id
|
|
1106
|
+
self._result.done()
|
|
1107
|
+
elif issubclass(error_type, Errors.RetriableError):
|
|
1108
|
+
self.reenqueue()
|
|
1109
|
+
elif error_type is Errors.TransactionalIdAuthorizationFailedError:
|
|
1110
|
+
self.fatal_error(error_type())
|
|
1111
|
+
elif error_type is Errors.GroupAuthorizationFailedError:
|
|
1112
|
+
self.abortable_error(error_type(self._coord_key))
|
|
1113
|
+
else:
|
|
1114
|
+
self.fatal_error(Errors.KafkaError(
|
|
1115
|
+
"Could not find a coordinator with type %s with key %s due to"
|
|
1116
|
+
" unexpected error: %s" % (self._coord_type, self._coord_key, error_type())))
|
|
1117
|
+
|
|
1118
|
+
|
|
1119
|
+
class EndTxnHandler(TxnRequestHandler):
|
|
1120
|
+
def __init__(self, transaction_manager, committed):
|
|
1121
|
+
super().__init__(transaction_manager)
|
|
1122
|
+
self.request = EndTxnRequest(
|
|
1123
|
+
transactional_id=self.transactional_id,
|
|
1124
|
+
producer_id=self.producer_id,
|
|
1125
|
+
producer_epoch=self.producer_epoch,
|
|
1126
|
+
committed=committed,
|
|
1127
|
+
max_version=3)
|
|
1128
|
+
|
|
1129
|
+
@property
|
|
1130
|
+
def priority(self):
|
|
1131
|
+
return Priority.END_TXN
|
|
1132
|
+
|
|
1133
|
+
def handle_response(self, response):
|
|
1134
|
+
error_type = Errors.for_code(response.error_code)
|
|
1135
|
+
|
|
1136
|
+
if error_type is Errors.NoError:
|
|
1137
|
+
self.transaction_manager._complete_transaction()
|
|
1138
|
+
self._result.done()
|
|
1139
|
+
elif issubclass(error_type, Errors.RetriableError):
|
|
1140
|
+
if error_type in (Errors.CoordinatorNotAvailableError, Errors.NotCoordinatorError):
|
|
1141
|
+
self.transaction_manager._lookup_coordinator(CoordinatorType.TRANSACTION, self.transactional_id)
|
|
1142
|
+
self.reenqueue()
|
|
1143
|
+
elif error_type in (Errors.InvalidProducerEpochError, Errors.ProducerFencedError):
|
|
1144
|
+
# Java client normalizes INVALID_PRODUCER_EPOCH to PRODUCER_FENCED
|
|
1145
|
+
# on the txn-coordinator RPC paths (KIP-360).
|
|
1146
|
+
self.fatal_error(Errors.ProducerFencedError())
|
|
1147
|
+
elif error_type is Errors.TransactionalIdAuthorizationFailedError:
|
|
1148
|
+
self.fatal_error(error_type())
|
|
1149
|
+
elif error_type is Errors.InvalidTxnStateError:
|
|
1150
|
+
self.fatal_error(error_type())
|
|
1151
|
+
else:
|
|
1152
|
+
self.fatal_error(Errors.KafkaError("Unhandled error in EndTxnResponse: %s" % (error_type())))
|
|
1153
|
+
|
|
1154
|
+
|
|
1155
|
+
class AddOffsetsToTxnHandler(TxnRequestHandler):
|
|
1156
|
+
def __init__(self, transaction_manager, group_metadata, offsets):
|
|
1157
|
+
super().__init__(transaction_manager)
|
|
1158
|
+
|
|
1159
|
+
self.group_metadata = group_metadata
|
|
1160
|
+
self.consumer_group_id = group_metadata.group_id
|
|
1161
|
+
self.offsets = offsets
|
|
1162
|
+
# max_version=3 is the highest we know how to drive (v4 is KIP-890).
|
|
1163
|
+
# The connection negotiates the actual wire version against the broker.
|
|
1164
|
+
self.request = AddOffsetsToTxnRequest(
|
|
1165
|
+
transactional_id=self.transactional_id,
|
|
1166
|
+
producer_id=self.producer_id,
|
|
1167
|
+
producer_epoch=self.producer_epoch,
|
|
1168
|
+
group_id=self.consumer_group_id,
|
|
1169
|
+
max_version=3,
|
|
1170
|
+
)
|
|
1171
|
+
|
|
1172
|
+
@property
|
|
1173
|
+
def priority(self):
|
|
1174
|
+
return Priority.ADD_PARTITIONS_OR_OFFSETS
|
|
1175
|
+
|
|
1176
|
+
def handle_response(self, response):
|
|
1177
|
+
error_type = Errors.for_code(response.error_code)
|
|
1178
|
+
|
|
1179
|
+
if error_type is Errors.NoError:
|
|
1180
|
+
log.debug("Successfully added partition for consumer group %s to transaction", self.consumer_group_id)
|
|
1181
|
+
|
|
1182
|
+
# note the result is not completed until the TxnOffsetCommit returns
|
|
1183
|
+
for tp, offset in self.offsets.items():
|
|
1184
|
+
self.transaction_manager._pending_txn_offset_commits[tp] = offset
|
|
1185
|
+
handler = TxnOffsetCommitHandler(self.transaction_manager, self.group_metadata,
|
|
1186
|
+
self.transaction_manager._pending_txn_offset_commits, self._result)
|
|
1187
|
+
self.transaction_manager._enqueue_request(handler)
|
|
1188
|
+
self.transaction_manager._transaction_started = True
|
|
1189
|
+
elif issubclass(error_type, Errors.RetriableError):
|
|
1190
|
+
if error_type in (Errors.CoordinatorNotAvailableError, Errors.NotCoordinatorError):
|
|
1191
|
+
self.transaction_manager._lookup_coordinator(CoordinatorType.TRANSACTION, self.transactional_id)
|
|
1192
|
+
self.reenqueue()
|
|
1193
|
+
elif error_type in (Errors.CoordinatorLoadInProgressError, Errors.ConcurrentTransactionsError):
|
|
1194
|
+
self.reenqueue()
|
|
1195
|
+
elif error_type in (Errors.InvalidProducerEpochError, Errors.ProducerFencedError):
|
|
1196
|
+
# Java client normalizes INVALID_PRODUCER_EPOCH to PRODUCER_FENCED
|
|
1197
|
+
# on the txn-coordinator RPC paths (KIP-360).
|
|
1198
|
+
self.fatal_error(Errors.ProducerFencedError())
|
|
1199
|
+
elif error_type in (Errors.UnknownProducerIdError, Errors.InvalidProducerIdMappingError):
|
|
1200
|
+
if self.transaction_manager._supports_epoch_bump():
|
|
1201
|
+
self.abortable_error(error_type())
|
|
1202
|
+
else:
|
|
1203
|
+
self.fatal_error(error_type())
|
|
1204
|
+
elif error_type is Errors.TransactionalIdAuthorizationFailedError:
|
|
1205
|
+
self.fatal_error(error_type())
|
|
1206
|
+
elif error_type is Errors.GroupAuthorizationFailedError:
|
|
1207
|
+
self.abortable_error(error_type(self.consumer_group_id))
|
|
1208
|
+
else:
|
|
1209
|
+
self.fatal_error(Errors.KafkaError("Unexpected error in AddOffsetsToTxnResponse: %s" % (error_type())))
|
|
1210
|
+
|
|
1211
|
+
|
|
1212
|
+
class TxnOffsetCommitHandler(TxnRequestHandler):
|
|
1213
|
+
def __init__(self, transaction_manager, group_metadata, offsets, result):
|
|
1214
|
+
super().__init__(transaction_manager, result=result)
|
|
1215
|
+
|
|
1216
|
+
self.group_metadata = group_metadata
|
|
1217
|
+
self.consumer_group_id = group_metadata.group_id
|
|
1218
|
+
self.offsets = offsets
|
|
1219
|
+
self.request = self._build_request()
|
|
1220
|
+
|
|
1221
|
+
def _build_request(self):
|
|
1222
|
+
# KIP-447: v3+ carries member_id / generation_id / group_instance_id
|
|
1223
|
+
# so the broker can fence stale consumer instances. We always set them
|
|
1224
|
+
# - the protocol drops them when the connection negotiates v0-v2
|
|
1225
|
+
# against an older broker. max_version is the highest version this
|
|
1226
|
+
# client knows how to drive: v4/v5 belong to KIP-890.
|
|
1227
|
+
Topic = TxnOffsetCommitRequest.TxnOffsetCommitRequestTopic
|
|
1228
|
+
Partition = Topic.TxnOffsetCommitRequestPartition
|
|
1229
|
+
|
|
1230
|
+
topic_data = collections.defaultdict(list)
|
|
1231
|
+
for tp, offset in self.offsets.items():
|
|
1232
|
+
topic_data[tp.topic].append(Partition(
|
|
1233
|
+
partition_index=tp.partition,
|
|
1234
|
+
committed_offset=offset.offset,
|
|
1235
|
+
committed_leader_epoch=offset.leader_epoch,
|
|
1236
|
+
committed_metadata=offset.metadata))
|
|
1237
|
+
|
|
1238
|
+
return TxnOffsetCommitRequest(
|
|
1239
|
+
transactional_id=self.transactional_id,
|
|
1240
|
+
group_id=self.consumer_group_id,
|
|
1241
|
+
producer_id=self.producer_id,
|
|
1242
|
+
producer_epoch=self.producer_epoch,
|
|
1243
|
+
generation_id=self.group_metadata.generation_id,
|
|
1244
|
+
member_id=self.group_metadata.member_id,
|
|
1245
|
+
group_instance_id=self.group_metadata.group_instance_id,
|
|
1246
|
+
topics=[Topic(name=topic, partitions=partitions)
|
|
1247
|
+
for topic, partitions in topic_data.items()],
|
|
1248
|
+
max_version=3,
|
|
1249
|
+
)
|
|
1250
|
+
|
|
1251
|
+
@property
|
|
1252
|
+
def priority(self):
|
|
1253
|
+
return Priority.ADD_PARTITIONS_OR_OFFSETS
|
|
1254
|
+
|
|
1255
|
+
@property
|
|
1256
|
+
def coordinator_type(self):
|
|
1257
|
+
return CoordinatorType.GROUP
|
|
1258
|
+
|
|
1259
|
+
@property
|
|
1260
|
+
def coordinator_key(self):
|
|
1261
|
+
return self.consumer_group_id
|
|
1262
|
+
|
|
1263
|
+
def handle_response(self, response):
|
|
1264
|
+
lookup_coordinator = False
|
|
1265
|
+
retriable_failure = False
|
|
1266
|
+
|
|
1267
|
+
errors = {TopicPartition(topic, partition): Errors.for_code(error_code)
|
|
1268
|
+
for topic, partition_data in response.topics
|
|
1269
|
+
for partition, error_code in partition_data}
|
|
1270
|
+
|
|
1271
|
+
for tp, error_type in errors.items():
|
|
1272
|
+
if error_type is Errors.NoError:
|
|
1273
|
+
log.debug("Successfully added offsets for %s from consumer group %s to transaction.",
|
|
1274
|
+
tp, self.consumer_group_id)
|
|
1275
|
+
del self.transaction_manager._pending_txn_offset_commits[tp]
|
|
1276
|
+
elif issubclass(error_type, Errors.RetriableError):
|
|
1277
|
+
retriable_failure = True
|
|
1278
|
+
if error_type in (Errors.CoordinatorNotAvailableError, Errors.NotCoordinatorError, Errors.RequestTimedOutError):
|
|
1279
|
+
lookup_coordinator = True
|
|
1280
|
+
elif error_type is Errors.GroupAuthorizationFailedError:
|
|
1281
|
+
self.abortable_error(error_type(self.consumer_group_id))
|
|
1282
|
+
return
|
|
1283
|
+
elif error_type in (Errors.InvalidProducerEpochError, Errors.ProducerFencedError):
|
|
1284
|
+
# Java client normalizes INVALID_PRODUCER_EPOCH to PRODUCER_FENCED
|
|
1285
|
+
# on the txn-coordinator RPC paths (KIP-360).
|
|
1286
|
+
self.fatal_error(Errors.ProducerFencedError())
|
|
1287
|
+
return
|
|
1288
|
+
elif error_type is Errors.FencedInstanceIdError:
|
|
1289
|
+
# KIP-447: static-membership fencing - another consumer
|
|
1290
|
+
# instance with this group_instance_id displaced ours. The
|
|
1291
|
+
# transaction must be aborted, but the producer can be
|
|
1292
|
+
# reused for a fresh transaction.
|
|
1293
|
+
self.abortable_error(error_type())
|
|
1294
|
+
return
|
|
1295
|
+
elif error_type in (Errors.IllegalGenerationError,
|
|
1296
|
+
Errors.UnknownMemberIdError):
|
|
1297
|
+
# KIP-447: the consumer generation / member_id we passed
|
|
1298
|
+
# in are stale (the consumer rebalanced between when we
|
|
1299
|
+
# snapshotted group_metadata and when the broker checked
|
|
1300
|
+
# it). Abort the txn so the application can re-snapshot
|
|
1301
|
+
# and retry.
|
|
1302
|
+
self.abortable_error(Errors.CommitFailedError(
|
|
1303
|
+
"Transaction offset commit failed due to consumer group"
|
|
1304
|
+
" metadata mismatch: %s" % (error_type.__name__,)))
|
|
1305
|
+
return
|
|
1306
|
+
elif error_type in (Errors.TransactionalIdAuthorizationFailedError,
|
|
1307
|
+
Errors.UnsupportedForMessageFormatError):
|
|
1308
|
+
self.fatal_error(error_type())
|
|
1309
|
+
return
|
|
1310
|
+
else:
|
|
1311
|
+
self.fatal_error(Errors.KafkaError("Unexpected error in TxnOffsetCommitResponse: %s" % (error_type())))
|
|
1312
|
+
return
|
|
1313
|
+
|
|
1314
|
+
if lookup_coordinator:
|
|
1315
|
+
self.transaction_manager._lookup_coordinator(CoordinatorType.GROUP, self.consumer_group_id)
|
|
1316
|
+
|
|
1317
|
+
if not retriable_failure:
|
|
1318
|
+
# all attempted partitions were either successful, or there was a fatal failure.
|
|
1319
|
+
# either way, we are not retrying, so complete the request.
|
|
1320
|
+
self.result.done()
|
|
1321
|
+
|
|
1322
|
+
# retry the commits which failed with a retriable error.
|
|
1323
|
+
elif self.transaction_manager._pending_txn_offset_commits:
|
|
1324
|
+
self.offsets = self.transaction_manager._pending_txn_offset_commits
|
|
1325
|
+
self.request = self._build_request()
|
|
1326
|
+
self.reenqueue()
|