|
15 | 15 | from kafka.coordinator.protocol import ConsumerProtocolMemberMetadata, ConsumerProtocolMemberAssignment, ConsumerProtocol
|
16 | 16 | import kafka.errors as Errors
|
17 | 17 | from kafka.errors import (
|
18 |
| - IncompatibleBrokerVersion, KafkaConfigurationError, NotControllerError, |
| 18 | + IncompatibleBrokerVersion, KafkaConfigurationError, NotControllerError, UnknownTopicOrPartitionError, |
19 | 19 | UnrecognizedBrokerVersion, IllegalArgumentError)
|
20 | 20 | from kafka.metrics import MetricConfig, Metrics
|
21 | 21 | from kafka.protocol.admin import (
|
22 | 22 | CreateTopicsRequest, DeleteTopicsRequest, DescribeConfigsRequest, AlterConfigsRequest, CreatePartitionsRequest,
|
23 | 23 | ListGroupsRequest, DescribeGroupsRequest, DescribeAclsRequest, CreateAclsRequest, DeleteAclsRequest,
|
24 |
| - DeleteGroupsRequest, DescribeLogDirsRequest |
25 |
| -) |
| 24 | + DeleteGroupsRequest, DeleteRecordsRequest, DescribeLogDirsRequest) |
26 | 25 | from kafka.protocol.commit import OffsetFetchRequest
|
27 | 26 | from kafka.protocol.find_coordinator import FindCoordinatorRequest
|
28 | 27 | from kafka.protocol.metadata import MetadataRequest
|
@@ -1116,8 +1115,118 @@ def create_partitions(self, topic_partitions, timeout_ms=None, validate_only=Fal
|
1116 | 1115 | .format(version))
|
1117 | 1116 | return self._send_request_to_controller(request)
|
1118 | 1117 |
|
1119 |
| - # delete records protocol not yet implemented |
1120 |
| - # Note: send the request to the partition leaders |
| 1118 | + def _get_leader_for_partitions(self, partitions, timeout_ms=None): |
| 1119 | + """Finds ID of the leader node for every given topic partition. |
| 1120 | +
|
| 1121 | + Will raise UnknownTopicOrPartitionError if for some partition no leader can be found. |
| 1122 | +
|
| 1123 | + :param partitions: ``[TopicPartition]``: partitions for which to find leaders. |
| 1124 | + :param timeout_ms: ``float``: Timeout in milliseconds, if None (default), will be read from |
| 1125 | + config. |
| 1126 | +
|
| 1127 | + :return: Dictionary with ``{leader_id -> {partitions}}`` |
| 1128 | + """ |
| 1129 | + timeout_ms = self._validate_timeout(timeout_ms) |
| 1130 | + |
| 1131 | + partitions = set(partitions) |
| 1132 | + topics = set(tp.topic for tp in partitions) |
| 1133 | + |
| 1134 | + response = self._get_cluster_metadata(topics=topics).to_object() |
| 1135 | + |
| 1136 | + leader2partitions = defaultdict(list) |
| 1137 | + valid_partitions = set() |
| 1138 | + for topic in response.get("topics", ()): |
| 1139 | + for partition in topic.get("partitions", ()): |
| 1140 | + t2p = TopicPartition(topic=topic["topic"], partition=partition["partition"]) |
| 1141 | + if t2p in partitions: |
| 1142 | + leader2partitions[partition["leader"]].append(t2p) |
| 1143 | + valid_partitions.add(t2p) |
| 1144 | + |
| 1145 | + if len(partitions) != len(valid_partitions): |
| 1146 | + unknown = set(partitions) - valid_partitions |
| 1147 | + raise UnknownTopicOrPartitionError( |
| 1148 | + "The following partitions are not known: %s" |
| 1149 | + % ", ".join(str(x) for x in unknown) |
| 1150 | + ) |
| 1151 | + |
| 1152 | + return leader2partitions |
| 1153 | + |
| 1154 | + def delete_records(self, records_to_delete, timeout_ms=None, partition_leader_id=None): |
| 1155 | + """Delete records whose offset is smaller than the given offset of the corresponding partition. |
| 1156 | +
|
| 1157 | + :param records_to_delete: ``{TopicPartition: int}``: The earliest available offsets for the |
| 1158 | + given partitions. |
| 1159 | + :param timeout_ms: ``float``: Timeout in milliseconds, if None (default), will be read from |
| 1160 | + config. |
| 1161 | + :param partition_leader_id: ``str``: If specified, all deletion requests will be sent to |
| 1162 | + this node. No check is performed verifying that this is indeed the leader for all |
| 1163 | + listed partitions: use with caution. |
| 1164 | +
|
| 1165 | + :return: Dictionary {topicPartition -> metadata}, where metadata is returned by the broker. |
| 1166 | + See DeleteRecordsResponse for possible fields. error_code for all partitions is |
| 1167 | + guaranteed to be zero, otherwise an exception is raised. |
| 1168 | + """ |
| 1169 | + timeout_ms = self._validate_timeout(timeout_ms) |
| 1170 | + responses = [] |
| 1171 | + version = self._client.api_version(DeleteRecordsRequest, max_version=0) |
| 1172 | + if version is None: |
| 1173 | + raise IncompatibleBrokerVersion("Broker does not support DeleteGroupsRequest") |
| 1174 | + |
| 1175 | + # We want to make as few requests as possible |
| 1176 | + # If a single node serves as a partition leader for multiple partitions (and/or |
| 1177 | + # topics), we can send all of those in a single request. |
| 1178 | + # For that we store {leader -> {partitions for leader}}, and do 1 request per leader |
| 1179 | + if partition_leader_id is None: |
| 1180 | + leader2partitions = self._get_leader_for_partitions( |
| 1181 | + set(records_to_delete), timeout_ms |
| 1182 | + ) |
| 1183 | + else: |
| 1184 | + leader2partitions = {partition_leader_id: set(records_to_delete)} |
| 1185 | + |
| 1186 | + for leader, partitions in leader2partitions.items(): |
| 1187 | + topic2partitions = defaultdict(list) |
| 1188 | + for partition in partitions: |
| 1189 | + topic2partitions[partition.topic].append(partition) |
| 1190 | + |
| 1191 | + request = DeleteRecordsRequest[version]( |
| 1192 | + topics=[ |
| 1193 | + (topic, [(tp.partition, records_to_delete[tp]) for tp in partitions]) |
| 1194 | + for topic, partitions in topic2partitions.items() |
| 1195 | + ], |
| 1196 | + timeout_ms=timeout_ms |
| 1197 | + ) |
| 1198 | + future = self._send_request_to_node(leader, request) |
| 1199 | + self._wait_for_futures([future]) |
| 1200 | + |
| 1201 | + responses.append(future.value.to_object()) |
| 1202 | + |
| 1203 | + partition2result = {} |
| 1204 | + partition2error = {} |
| 1205 | + for response in responses: |
| 1206 | + for topic in response["topics"]: |
| 1207 | + for partition in topic["partitions"]: |
| 1208 | + tp = TopicPartition(topic["name"], partition["partition_index"]) |
| 1209 | + partition2result[tp] = partition |
| 1210 | + if partition["error_code"] != 0: |
| 1211 | + partition2error[tp] = partition["error_code"] |
| 1212 | + |
| 1213 | + if partition2error: |
| 1214 | + if len(partition2error) == 1: |
| 1215 | + key, error = next(iter(partition2error.items())) |
| 1216 | + raise Errors.for_code(error)( |
| 1217 | + "Error deleting records from topic %s partition %s" % (key.topic, key.partition) |
| 1218 | + ) |
| 1219 | + else: |
| 1220 | + raise Errors.BrokerResponseError( |
| 1221 | + "The following errors occured when trying to delete records: " + |
| 1222 | + ", ".join( |
| 1223 | + "%s(partition=%d): %s" % |
| 1224 | + (partition.topic, partition.partition, Errors.for_code(error).__name__) |
| 1225 | + for partition, error in partition2error.items() |
| 1226 | + ) |
| 1227 | + ) |
| 1228 | + |
| 1229 | + return partition2result |
1121 | 1230 |
|
1122 | 1231 | # create delegation token protocol not yet implemented
|
1123 | 1232 | # Note: send the request to the least_loaded_node()
|
|
0 commit comments