kafka-python 2.0.1__py2.py3-none-any.whl → 2.0.3__py2.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 +1 -1
- kafka/admin/client.py +167 -50
- kafka/client_async.py +6 -4
- kafka/codec.py +26 -1
- kafka/conn.py +15 -4
- kafka/consumer/fetcher.py +16 -5
- kafka/consumer/group.py +9 -4
- 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 +685 -0
- kafka/coordinator/base.py +17 -9
- kafka/coordinator/consumer.py +4 -1
- kafka/errors.py +12 -0
- kafka/producer/future.py +3 -3
- kafka/producer/kafka.py +23 -8
- kafka/producer/record_accumulator.py +4 -4
- kafka/producer/sender.py +23 -6
- kafka/protocol/__init__.py +3 -0
- kafka/protocol/admin.py +234 -2
- kafka/protocol/api.py +42 -1
- kafka/protocol/fetch.py +180 -2
- kafka/protocol/message.py +7 -3
- kafka/protocol/offset.py +87 -2
- kafka/protocol/parser.py +7 -14
- kafka/protocol/produce.py +77 -2
- kafka/protocol/types.py +169 -2
- kafka/record/_crc32c.py +1 -1
- kafka/record/abc.py +1 -1
- kafka/record/default_records.py +9 -2
- kafka/record/legacy_records.py +1 -1
- kafka/record/memory_records.py +1 -1
- kafka/record/util.py +1 -1
- kafka/scram.py +0 -1
- kafka/structs.py +63 -3
- kafka/vendor/selectors34.py +5 -1
- kafka/vendor/six.py +128 -21
- kafka/vendor/socketpair.py +17 -0
- kafka/version.py +1 -1
- kafka_python-2.0.3.dist-info/METADATA +250 -0
- {kafka_python-2.0.1.dist-info → kafka_python-2.0.3.dist-info}/RECORD +44 -40
- {kafka_python-2.0.1.dist-info → kafka_python-2.0.3.dist-info}/WHEEL +1 -1
- kafka_python-2.0.1.dist-info/METADATA +0 -187
- {kafka_python-2.0.1.dist-info → kafka_python-2.0.3.dist-info}/LICENSE +0 -0
- {kafka_python-2.0.1.dist-info → kafka_python-2.0.3.dist-info}/top_level.txt +0 -0
kafka/__init__.py
CHANGED
|
@@ -4,7 +4,7 @@ __title__ = 'kafka'
|
|
|
4
4
|
from kafka.version import __version__
|
|
5
5
|
__author__ = 'Dana Powers'
|
|
6
6
|
__license__ = 'Apache License 2.0'
|
|
7
|
-
__copyright__ = 'Copyright
|
|
7
|
+
__copyright__ = 'Copyright 2025 Dana Powers, David Arthur, and Contributors'
|
|
8
8
|
|
|
9
9
|
# Set default logging handler to avoid "No handler found" warnings.
|
|
10
10
|
import logging
|
kafka/admin/client.py
CHANGED
|
@@ -8,7 +8,10 @@ import socket
|
|
|
8
8
|
from . import ConfigResourceType
|
|
9
9
|
from kafka.vendor import six
|
|
10
10
|
|
|
11
|
+
from kafka.admin.acl_resource import ACLOperation, ACLPermissionType, ACLFilter, ACL, ResourcePattern, ResourceType, \
|
|
12
|
+
ACLResourcePatternType
|
|
11
13
|
from kafka.client_async import KafkaClient, selectors
|
|
14
|
+
from kafka.coordinator.protocol import ConsumerProtocolMemberMetadata, ConsumerProtocolMemberAssignment, ConsumerProtocol
|
|
12
15
|
import kafka.errors as Errors
|
|
13
16
|
from kafka.errors import (
|
|
14
17
|
IncompatibleBrokerVersion, KafkaConfigurationError, NotControllerError,
|
|
@@ -16,12 +19,13 @@ from kafka.errors import (
|
|
|
16
19
|
from kafka.metrics import MetricConfig, Metrics
|
|
17
20
|
from kafka.protocol.admin import (
|
|
18
21
|
CreateTopicsRequest, DeleteTopicsRequest, DescribeConfigsRequest, AlterConfigsRequest, CreatePartitionsRequest,
|
|
19
|
-
ListGroupsRequest, DescribeGroupsRequest, DescribeAclsRequest, CreateAclsRequest, DeleteAclsRequest
|
|
22
|
+
ListGroupsRequest, DescribeGroupsRequest, DescribeAclsRequest, CreateAclsRequest, DeleteAclsRequest,
|
|
23
|
+
DeleteGroupsRequest, DescribeLogDirsRequest
|
|
24
|
+
)
|
|
20
25
|
from kafka.protocol.commit import GroupCoordinatorRequest, OffsetFetchRequest
|
|
21
26
|
from kafka.protocol.metadata import MetadataRequest
|
|
22
|
-
from kafka.
|
|
23
|
-
from kafka.
|
|
24
|
-
ACLResourcePatternType
|
|
27
|
+
from kafka.protocol.types import Array
|
|
28
|
+
from kafka.structs import TopicPartition, OffsetAndMetadata, MemberInformation, GroupInformation
|
|
25
29
|
from kafka.version import __version__
|
|
26
30
|
|
|
27
31
|
|
|
@@ -142,6 +146,7 @@ class KafkaAdminClient(object):
|
|
|
142
146
|
sasl mechanism handshake. Default: one of bootstrap servers
|
|
143
147
|
sasl_oauth_token_provider (AbstractTokenProvider): OAuthBearer token provider
|
|
144
148
|
instance. (See kafka.oauth.abstract). Default: None
|
|
149
|
+
kafka_client (callable): Custom class / callable for creating KafkaClient instances
|
|
145
150
|
|
|
146
151
|
"""
|
|
147
152
|
DEFAULT_CONFIG = {
|
|
@@ -182,6 +187,7 @@ class KafkaAdminClient(object):
|
|
|
182
187
|
'metric_reporters': [],
|
|
183
188
|
'metrics_num_samples': 2,
|
|
184
189
|
'metrics_sample_window_ms': 30000,
|
|
190
|
+
'kafka_client': KafkaClient,
|
|
185
191
|
}
|
|
186
192
|
|
|
187
193
|
def __init__(self, **configs):
|
|
@@ -201,10 +207,12 @@ class KafkaAdminClient(object):
|
|
|
201
207
|
reporters = [reporter() for reporter in self.config['metric_reporters']]
|
|
202
208
|
self._metrics = Metrics(metric_config, reporters)
|
|
203
209
|
|
|
204
|
-
self._client =
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
210
|
+
self._client = self.config['kafka_client'](
|
|
211
|
+
metrics=self._metrics,
|
|
212
|
+
metric_group_prefix='admin',
|
|
213
|
+
**self.config
|
|
214
|
+
)
|
|
215
|
+
self._client.check_version(timeout=(self.config['api_version_auto_timeout_ms'] / 1000))
|
|
208
216
|
|
|
209
217
|
# Get auto-discovered version from client if necessary
|
|
210
218
|
if self.config['api_version'] is None:
|
|
@@ -271,7 +279,7 @@ class KafkaAdminClient(object):
|
|
|
271
279
|
response = future.value
|
|
272
280
|
controller_id = response.controller_id
|
|
273
281
|
# verify the controller is new enough to support our requests
|
|
274
|
-
controller_version = self._client.check_version(controller_id)
|
|
282
|
+
controller_version = self._client.check_version(controller_id, timeout=(self.config['api_version_auto_timeout_ms'] / 1000))
|
|
275
283
|
if controller_version < (0, 10, 0):
|
|
276
284
|
raise IncompatibleBrokerVersion(
|
|
277
285
|
"The controller appears to be running Kafka {}. KafkaAdminClient requires brokers >= 0.10.0.0."
|
|
@@ -324,30 +332,37 @@ class KafkaAdminClient(object):
|
|
|
324
332
|
.format(response.API_VERSION))
|
|
325
333
|
return response.coordinator_id
|
|
326
334
|
|
|
327
|
-
def
|
|
328
|
-
"""Find the broker
|
|
335
|
+
def _find_coordinator_ids(self, group_ids):
|
|
336
|
+
"""Find the broker node_ids of the coordinators of the given groups.
|
|
329
337
|
|
|
330
|
-
Sends a FindCoordinatorRequest message to the cluster
|
|
331
|
-
the FindCoordinatorResponse is received
|
|
332
|
-
raised.
|
|
338
|
+
Sends a FindCoordinatorRequest message to the cluster for each group_id.
|
|
339
|
+
Will block until the FindCoordinatorResponse is received for all groups.
|
|
340
|
+
Any errors are immediately raised.
|
|
333
341
|
|
|
334
|
-
:param
|
|
342
|
+
:param group_ids: A list of consumer group IDs. This is typically the group
|
|
335
343
|
name as a string.
|
|
336
|
-
:return:
|
|
344
|
+
:return: A dict of {group_id: node_id} where node_id is the id of the
|
|
345
|
+
broker that is the coordinator for the corresponding group.
|
|
337
346
|
"""
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
347
|
+
groups_futures = {
|
|
348
|
+
group_id: self._find_coordinator_id_send_request(group_id)
|
|
349
|
+
for group_id in group_ids
|
|
350
|
+
}
|
|
351
|
+
self._wait_for_futures(groups_futures.values())
|
|
352
|
+
groups_coordinators = {
|
|
353
|
+
group_id: self._find_coordinator_id_process_response(future.value)
|
|
354
|
+
for group_id, future in groups_futures.items()
|
|
355
|
+
}
|
|
356
|
+
return groups_coordinators
|
|
357
|
+
|
|
358
|
+
def _send_request_to_node(self, node_id, request, wakeup=True):
|
|
345
359
|
"""Send a Kafka protocol message to a specific broker.
|
|
346
360
|
|
|
347
361
|
Returns a future that may be polled for status and results.
|
|
348
362
|
|
|
349
363
|
:param node_id: The broker id to which to send the message.
|
|
350
364
|
:param request: The message to send.
|
|
365
|
+
:param wakeup: Optional flag to disable thread-wakeup.
|
|
351
366
|
:return: A future object that may be polled for status and results.
|
|
352
367
|
:exception: The exception if the message could not be sent.
|
|
353
368
|
"""
|
|
@@ -355,7 +370,7 @@ class KafkaAdminClient(object):
|
|
|
355
370
|
# poll until the connection to broker is ready, otherwise send()
|
|
356
371
|
# will fail with NodeNotReadyError
|
|
357
372
|
self._client.poll()
|
|
358
|
-
return self._client.send(node_id, request)
|
|
373
|
+
return self._client.send(node_id, request, wakeup)
|
|
359
374
|
|
|
360
375
|
def _send_request_to_controller(self, request):
|
|
361
376
|
"""Send a Kafka protocol message to the cluster controller.
|
|
@@ -682,7 +697,6 @@ class KafkaAdminClient(object):
|
|
|
682
697
|
self._wait_for_futures([future])
|
|
683
698
|
response = future.value
|
|
684
699
|
|
|
685
|
-
|
|
686
700
|
return self._convert_create_acls_response_to_acls(acls, response)
|
|
687
701
|
|
|
688
702
|
@staticmethod
|
|
@@ -1000,22 +1014,47 @@ class KafkaAdminClient(object):
|
|
|
1000
1014
|
"""Process a DescribeGroupsResponse into a group description."""
|
|
1001
1015
|
if response.API_VERSION <= 3:
|
|
1002
1016
|
assert len(response.groups) == 1
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1017
|
+
for response_field, response_name in zip(response.SCHEMA.fields, response.SCHEMA.names):
|
|
1018
|
+
if isinstance(response_field, Array):
|
|
1019
|
+
described_groups_field_schema = response_field.array_of
|
|
1020
|
+
described_group = response.__dict__[response_name][0]
|
|
1021
|
+
described_group_information_list = []
|
|
1022
|
+
protocol_type_is_consumer = False
|
|
1023
|
+
for (described_group_information, group_information_name, group_information_field) in zip(described_group, described_groups_field_schema.names, described_groups_field_schema.fields):
|
|
1024
|
+
if group_information_name == 'protocol_type':
|
|
1025
|
+
protocol_type = described_group_information
|
|
1026
|
+
protocol_type_is_consumer = (protocol_type == ConsumerProtocol.PROTOCOL_TYPE or not protocol_type)
|
|
1027
|
+
if isinstance(group_information_field, Array):
|
|
1028
|
+
member_information_list = []
|
|
1029
|
+
member_schema = group_information_field.array_of
|
|
1030
|
+
for members in described_group_information:
|
|
1031
|
+
member_information = []
|
|
1032
|
+
for (member, member_field, member_name) in zip(members, member_schema.fields, member_schema.names):
|
|
1033
|
+
if protocol_type_is_consumer:
|
|
1034
|
+
if member_name == 'member_metadata' and member:
|
|
1035
|
+
member_information.append(ConsumerProtocolMemberMetadata.decode(member))
|
|
1036
|
+
elif member_name == 'member_assignment' and member:
|
|
1037
|
+
member_information.append(ConsumerProtocolMemberAssignment.decode(member))
|
|
1038
|
+
else:
|
|
1039
|
+
member_information.append(member)
|
|
1040
|
+
member_info_tuple = MemberInformation._make(member_information)
|
|
1041
|
+
member_information_list.append(member_info_tuple)
|
|
1042
|
+
described_group_information_list.append(member_information_list)
|
|
1043
|
+
else:
|
|
1044
|
+
described_group_information_list.append(described_group_information)
|
|
1045
|
+
# Version 3 of the DescribeGroups API introduced the "authorized_operations" field.
|
|
1046
|
+
# This will cause the namedtuple to fail.
|
|
1047
|
+
# Therefore, appending a placeholder of None in it.
|
|
1048
|
+
if response.API_VERSION <=2:
|
|
1049
|
+
described_group_information_list.append(None)
|
|
1050
|
+
group_description = GroupInformation._make(described_group_information_list)
|
|
1051
|
+
error_code = group_description.error_code
|
|
1009
1052
|
error_type = Errors.for_code(error_code)
|
|
1010
1053
|
# Java has the note: KAFKA-6789, we can retry based on the error code
|
|
1011
1054
|
if error_type is not Errors.NoError:
|
|
1012
1055
|
raise error_type(
|
|
1013
1056
|
"DescribeGroupsResponse failed with response '{}'."
|
|
1014
1057
|
.format(response))
|
|
1015
|
-
# TODO Java checks the group protocol type, and if consumer
|
|
1016
|
-
# (ConsumerProtocol.PROTOCOL_TYPE) or empty string, it decodes
|
|
1017
|
-
# the members' partition assignments... that hasn't yet been
|
|
1018
|
-
# implemented here so just return the raw struct results
|
|
1019
1058
|
else:
|
|
1020
1059
|
raise NotImplementedError(
|
|
1021
1060
|
"Support for DescribeGroupsResponse_v{} has not yet been added to KafkaAdminClient."
|
|
@@ -1044,18 +1083,19 @@ class KafkaAdminClient(object):
|
|
|
1044
1083
|
partition assignments.
|
|
1045
1084
|
"""
|
|
1046
1085
|
group_descriptions = []
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1086
|
+
|
|
1087
|
+
if group_coordinator_id is not None:
|
|
1088
|
+
groups_coordinators = {group_id: group_coordinator_id for group_id in group_ids}
|
|
1089
|
+
else:
|
|
1090
|
+
groups_coordinators = self._find_coordinator_ids(group_ids)
|
|
1091
|
+
|
|
1092
|
+
futures = [
|
|
1093
|
+
self._describe_consumer_groups_send_request(
|
|
1054
1094
|
group_id,
|
|
1055
|
-
|
|
1095
|
+
coordinator_id,
|
|
1056
1096
|
include_authorized_operations)
|
|
1057
|
-
|
|
1058
|
-
|
|
1097
|
+
for group_id, coordinator_id in groups_coordinators.items()
|
|
1098
|
+
]
|
|
1059
1099
|
self._wait_for_futures(futures)
|
|
1060
1100
|
|
|
1061
1101
|
for future in futures:
|
|
@@ -1170,7 +1210,7 @@ class KafkaAdminClient(object):
|
|
|
1170
1210
|
|
|
1171
1211
|
:param response: an OffsetFetchResponse.
|
|
1172
1212
|
:return: A dictionary composed of TopicPartition keys and
|
|
1173
|
-
|
|
1213
|
+
OffsetAndMetadata values.
|
|
1174
1214
|
"""
|
|
1175
1215
|
if response.API_VERSION <= 3:
|
|
1176
1216
|
|
|
@@ -1184,7 +1224,7 @@ class KafkaAdminClient(object):
|
|
|
1184
1224
|
.format(response))
|
|
1185
1225
|
|
|
1186
1226
|
# transform response into a dictionary with TopicPartition keys and
|
|
1187
|
-
#
|
|
1227
|
+
# OffsetAndMetadata values--this is what the Java AdminClient returns
|
|
1188
1228
|
offsets = {}
|
|
1189
1229
|
for topic, partitions in response.topics:
|
|
1190
1230
|
for partition, offset, metadata, error_code in partitions:
|
|
@@ -1227,15 +1267,76 @@ class KafkaAdminClient(object):
|
|
|
1227
1267
|
explicitly specified.
|
|
1228
1268
|
"""
|
|
1229
1269
|
if group_coordinator_id is None:
|
|
1230
|
-
group_coordinator_id = self.
|
|
1270
|
+
group_coordinator_id = self._find_coordinator_ids([group_id])[group_id]
|
|
1231
1271
|
future = self._list_consumer_group_offsets_send_request(
|
|
1232
1272
|
group_id, group_coordinator_id, partitions)
|
|
1233
1273
|
self._wait_for_futures([future])
|
|
1234
1274
|
response = future.value
|
|
1235
1275
|
return self._list_consumer_group_offsets_process_response(response)
|
|
1236
1276
|
|
|
1237
|
-
|
|
1238
|
-
|
|
1277
|
+
def delete_consumer_groups(self, group_ids, group_coordinator_id=None):
|
|
1278
|
+
"""Delete Consumer Group Offsets for given consumer groups.
|
|
1279
|
+
|
|
1280
|
+
Note:
|
|
1281
|
+
This does not verify that the group ids actually exist and
|
|
1282
|
+
group_coordinator_id is the correct coordinator for all these groups.
|
|
1283
|
+
|
|
1284
|
+
The result needs checking for potential errors.
|
|
1285
|
+
|
|
1286
|
+
:param group_ids: The consumer group ids of the groups which are to be deleted.
|
|
1287
|
+
:param group_coordinator_id: The node_id of the broker which is the coordinator for
|
|
1288
|
+
all the groups. Use only if all groups are coordinated by the same broker.
|
|
1289
|
+
If set to None, will query the cluster to find the coordinator for every single group.
|
|
1290
|
+
Explicitly specifying this can be useful to prevent
|
|
1291
|
+
that extra network round trips if you already know the group
|
|
1292
|
+
coordinator. Default: None.
|
|
1293
|
+
:return: A list of tuples (group_id, KafkaError)
|
|
1294
|
+
"""
|
|
1295
|
+
if group_coordinator_id is not None:
|
|
1296
|
+
futures = [self._delete_consumer_groups_send_request(group_ids, group_coordinator_id)]
|
|
1297
|
+
else:
|
|
1298
|
+
coordinators_groups = defaultdict(list)
|
|
1299
|
+
for group_id, coordinator_id in self._find_coordinator_ids(group_ids).items():
|
|
1300
|
+
coordinators_groups[coordinator_id].append(group_id)
|
|
1301
|
+
futures = [
|
|
1302
|
+
self._delete_consumer_groups_send_request(group_ids, coordinator_id)
|
|
1303
|
+
for coordinator_id, group_ids in coordinators_groups.items()
|
|
1304
|
+
]
|
|
1305
|
+
|
|
1306
|
+
self._wait_for_futures(futures)
|
|
1307
|
+
|
|
1308
|
+
results = []
|
|
1309
|
+
for f in futures:
|
|
1310
|
+
results.extend(self._convert_delete_groups_response(f.value))
|
|
1311
|
+
return results
|
|
1312
|
+
|
|
1313
|
+
def _convert_delete_groups_response(self, response):
|
|
1314
|
+
if response.API_VERSION <= 1:
|
|
1315
|
+
results = []
|
|
1316
|
+
for group_id, error_code in response.results:
|
|
1317
|
+
results.append((group_id, Errors.for_code(error_code)))
|
|
1318
|
+
return results
|
|
1319
|
+
else:
|
|
1320
|
+
raise NotImplementedError(
|
|
1321
|
+
"Support for DeleteGroupsResponse_v{} has not yet been added to KafkaAdminClient."
|
|
1322
|
+
.format(response.API_VERSION))
|
|
1323
|
+
|
|
1324
|
+
def _delete_consumer_groups_send_request(self, group_ids, group_coordinator_id):
|
|
1325
|
+
"""Send a DeleteGroups request to a broker.
|
|
1326
|
+
|
|
1327
|
+
:param group_ids: The consumer group ids of the groups which are to be deleted.
|
|
1328
|
+
:param group_coordinator_id: The node_id of the broker which is the coordinator for
|
|
1329
|
+
all the groups.
|
|
1330
|
+
:return: A message future
|
|
1331
|
+
"""
|
|
1332
|
+
version = self._matching_api_version(DeleteGroupsRequest)
|
|
1333
|
+
if version <= 1:
|
|
1334
|
+
request = DeleteGroupsRequest[version](group_ids)
|
|
1335
|
+
else:
|
|
1336
|
+
raise NotImplementedError(
|
|
1337
|
+
"Support for DeleteGroupsRequest_v{} has not yet been added to KafkaAdminClient."
|
|
1338
|
+
.format(version))
|
|
1339
|
+
return self._send_request_to_node(group_coordinator_id, request)
|
|
1239
1340
|
|
|
1240
1341
|
def _wait_for_futures(self, futures):
|
|
1241
1342
|
while not all(future.succeeded() for future in futures):
|
|
@@ -1244,3 +1345,19 @@ class KafkaAdminClient(object):
|
|
|
1244
1345
|
|
|
1245
1346
|
if future.failed():
|
|
1246
1347
|
raise future.exception # pylint: disable-msg=raising-bad-type
|
|
1348
|
+
|
|
1349
|
+
def describe_log_dirs(self):
|
|
1350
|
+
"""Send a DescribeLogDirsRequest request to a broker.
|
|
1351
|
+
|
|
1352
|
+
:return: A message future
|
|
1353
|
+
"""
|
|
1354
|
+
version = self._matching_api_version(DescribeLogDirsRequest)
|
|
1355
|
+
if version <= 0:
|
|
1356
|
+
request = DescribeLogDirsRequest[version]()
|
|
1357
|
+
future = self._send_request_to_node(self._client.least_loaded_node(), request)
|
|
1358
|
+
self._wait_for_futures([future])
|
|
1359
|
+
else:
|
|
1360
|
+
raise NotImplementedError(
|
|
1361
|
+
"Support for DescribeLogDirsRequest_v{} has not yet been added to KafkaAdminClient."
|
|
1362
|
+
.format(version))
|
|
1363
|
+
return future.value
|
kafka/client_async.py
CHANGED
|
@@ -2,7 +2,6 @@ from __future__ import absolute_import, division
|
|
|
2
2
|
|
|
3
3
|
import collections
|
|
4
4
|
import copy
|
|
5
|
-
import functools
|
|
6
5
|
import logging
|
|
7
6
|
import random
|
|
8
7
|
import socket
|
|
@@ -202,10 +201,15 @@ class KafkaClient(object):
|
|
|
202
201
|
if key in configs:
|
|
203
202
|
self.config[key] = configs[key]
|
|
204
203
|
|
|
204
|
+
# these properties need to be set on top of the initialization pipeline
|
|
205
|
+
# because they are used when __del__ method is called
|
|
206
|
+
self._closed = False
|
|
207
|
+
self._wake_r, self._wake_w = socket.socketpair()
|
|
208
|
+
self._selector = self.config['selector']()
|
|
209
|
+
|
|
205
210
|
self.cluster = ClusterMetadata(**self.config)
|
|
206
211
|
self._topics = set() # empty set will fetch all topic metadata
|
|
207
212
|
self._metadata_refresh_in_progress = False
|
|
208
|
-
self._selector = self.config['selector']()
|
|
209
213
|
self._conns = Dict() # object to support weakrefs
|
|
210
214
|
self._api_versions = None
|
|
211
215
|
self._connecting = set()
|
|
@@ -213,7 +217,6 @@ class KafkaClient(object):
|
|
|
213
217
|
self._refresh_on_disconnects = True
|
|
214
218
|
self._last_bootstrap = 0
|
|
215
219
|
self._bootstrap_fails = 0
|
|
216
|
-
self._wake_r, self._wake_w = socket.socketpair()
|
|
217
220
|
self._wake_r.setblocking(False)
|
|
218
221
|
self._wake_w.settimeout(self.config['wakeup_timeout_ms'] / 1000.0)
|
|
219
222
|
self._wake_lock = threading.Lock()
|
|
@@ -227,7 +230,6 @@ class KafkaClient(object):
|
|
|
227
230
|
|
|
228
231
|
self._selector.register(self._wake_r, selectors.EVENT_READ)
|
|
229
232
|
self._idle_expiry_manager = IdleConnectionManager(self.config['connections_max_idle_ms'])
|
|
230
|
-
self._closed = False
|
|
231
233
|
self._sensors = None
|
|
232
234
|
if self.config['metrics']:
|
|
233
235
|
self._sensors = KafkaClientMetrics(self.config['metrics'],
|
kafka/codec.py
CHANGED
|
@@ -10,12 +10,18 @@ from kafka.vendor.six.moves import range
|
|
|
10
10
|
|
|
11
11
|
_XERIAL_V1_HEADER = (-126, b'S', b'N', b'A', b'P', b'P', b'Y', 0, 1, 1)
|
|
12
12
|
_XERIAL_V1_FORMAT = 'bccccccBii'
|
|
13
|
+
ZSTD_MAX_OUTPUT_SIZE = 1024 * 1024
|
|
13
14
|
|
|
14
15
|
try:
|
|
15
16
|
import snappy
|
|
16
17
|
except ImportError:
|
|
17
18
|
snappy = None
|
|
18
19
|
|
|
20
|
+
try:
|
|
21
|
+
import zstandard as zstd
|
|
22
|
+
except ImportError:
|
|
23
|
+
zstd = None
|
|
24
|
+
|
|
19
25
|
try:
|
|
20
26
|
import lz4.frame as lz4
|
|
21
27
|
|
|
@@ -58,6 +64,10 @@ def has_snappy():
|
|
|
58
64
|
return snappy is not None
|
|
59
65
|
|
|
60
66
|
|
|
67
|
+
def has_zstd():
|
|
68
|
+
return zstd is not None
|
|
69
|
+
|
|
70
|
+
|
|
61
71
|
def has_lz4():
|
|
62
72
|
if lz4 is not None:
|
|
63
73
|
return True
|
|
@@ -177,7 +187,7 @@ def _detect_xerial_stream(payload):
|
|
|
177
187
|
The version is the version of this format as written by xerial,
|
|
178
188
|
in the wild this is currently 1 as such we only support v1.
|
|
179
189
|
|
|
180
|
-
Compat is there to claim the
|
|
190
|
+
Compat is there to claim the minimum supported version that
|
|
181
191
|
can read a xerial block stream, presently in the wild this is
|
|
182
192
|
1.
|
|
183
193
|
"""
|
|
@@ -299,3 +309,18 @@ def lz4_decode_old_kafka(payload):
|
|
|
299
309
|
payload[header_size:]
|
|
300
310
|
])
|
|
301
311
|
return lz4_decode(munged_payload)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def zstd_encode(payload):
|
|
315
|
+
if not zstd:
|
|
316
|
+
raise NotImplementedError("Zstd codec is not available")
|
|
317
|
+
return zstd.ZstdCompressor().compress(payload)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def zstd_decode(payload):
|
|
321
|
+
if not zstd:
|
|
322
|
+
raise NotImplementedError("Zstd codec is not available")
|
|
323
|
+
try:
|
|
324
|
+
return zstd.ZstdDecompressor().decompress(payload)
|
|
325
|
+
except zstd.ZstdError:
|
|
326
|
+
return zstd.ZstdDecompressor().decompress(payload, max_output_size=ZSTD_MAX_OUTPUT_SIZE)
|
kafka/conn.py
CHANGED
|
@@ -24,9 +24,12 @@ import kafka.errors as Errors
|
|
|
24
24
|
from kafka.future import Future
|
|
25
25
|
from kafka.metrics.stats import Avg, Count, Max, Rate
|
|
26
26
|
from kafka.oauth.abstract import AbstractTokenProvider
|
|
27
|
-
from kafka.protocol.admin import SaslHandShakeRequest
|
|
27
|
+
from kafka.protocol.admin import SaslHandShakeRequest, DescribeAclsRequest_v2, DescribeClientQuotasRequest
|
|
28
28
|
from kafka.protocol.commit import OffsetFetchRequest
|
|
29
|
+
from kafka.protocol.offset import OffsetRequest
|
|
30
|
+
from kafka.protocol.produce import ProduceRequest
|
|
29
31
|
from kafka.protocol.metadata import MetadataRequest
|
|
32
|
+
from kafka.protocol.fetch import FetchRequest
|
|
30
33
|
from kafka.protocol.parser import KafkaProtocol
|
|
31
34
|
from kafka.protocol.types import Int32, Int8
|
|
32
35
|
from kafka.scram import ScramClient
|
|
@@ -75,7 +78,7 @@ except ImportError:
|
|
|
75
78
|
try:
|
|
76
79
|
import gssapi
|
|
77
80
|
from gssapi.raw.misc import GSSError
|
|
78
|
-
except ImportError:
|
|
81
|
+
except (ImportError, OSError):
|
|
79
82
|
#no gssapi available, will disable gssapi mechanism
|
|
80
83
|
gssapi = None
|
|
81
84
|
GSSError = None
|
|
@@ -493,7 +496,7 @@ class BrokerConnection(object):
|
|
|
493
496
|
try:
|
|
494
497
|
self._sock = self._ssl_context.wrap_socket(
|
|
495
498
|
self._sock,
|
|
496
|
-
server_hostname=self.host,
|
|
499
|
+
server_hostname=self.host.rstrip("."),
|
|
497
500
|
do_handshake_on_connect=False)
|
|
498
501
|
except ssl.SSLError as e:
|
|
499
502
|
log.exception('%s: Failed to wrap socket in SSLContext!', self)
|
|
@@ -913,7 +916,7 @@ class BrokerConnection(object):
|
|
|
913
916
|
with self._lock:
|
|
914
917
|
if self.state is ConnectionStates.DISCONNECTED:
|
|
915
918
|
return
|
|
916
|
-
log.
|
|
919
|
+
log.log(logging.ERROR if error else logging.INFO, '%s: Closing connection. %s', self, error or '')
|
|
917
920
|
self._update_reconnect_backoff()
|
|
918
921
|
self._sasl_auth_future = None
|
|
919
922
|
self._protocol = KafkaProtocol(
|
|
@@ -1166,6 +1169,14 @@ class BrokerConnection(object):
|
|
|
1166
1169
|
# in reverse order. As soon as we find one that works, return it
|
|
1167
1170
|
test_cases = [
|
|
1168
1171
|
# format (<broker version>, <needed struct>)
|
|
1172
|
+
((2, 6, 0), DescribeClientQuotasRequest[0]),
|
|
1173
|
+
((2, 5, 0), DescribeAclsRequest_v2),
|
|
1174
|
+
((2, 4, 0), ProduceRequest[8]),
|
|
1175
|
+
((2, 3, 0), FetchRequest[11]),
|
|
1176
|
+
((2, 2, 0), OffsetRequest[5]),
|
|
1177
|
+
((2, 1, 0), FetchRequest[10]),
|
|
1178
|
+
((2, 0, 0), FetchRequest[8]),
|
|
1179
|
+
((1, 1, 0), FetchRequest[7]),
|
|
1169
1180
|
((1, 0, 0), MetadataRequest[5]),
|
|
1170
1181
|
((0, 11, 0), MetadataRequest[4]),
|
|
1171
1182
|
((0, 10, 2), OffsetFetchRequest[2]),
|
kafka/consumer/fetcher.py
CHANGED
|
@@ -125,7 +125,7 @@ class Fetcher(six.Iterator):
|
|
|
125
125
|
log.debug("Sending FetchRequest to node %s", node_id)
|
|
126
126
|
future = self._client.send(node_id, request, wakeup=False)
|
|
127
127
|
future.add_callback(self._handle_fetch_response, request, time.time())
|
|
128
|
-
future.add_errback(
|
|
128
|
+
future.add_errback(self._handle_fetch_error, node_id)
|
|
129
129
|
futures.append(future)
|
|
130
130
|
self._fetch_futures.extend(futures)
|
|
131
131
|
self._clean_done_fetch_futures()
|
|
@@ -293,7 +293,7 @@ class Fetcher(six.Iterator):
|
|
|
293
293
|
# Issue #1780
|
|
294
294
|
# Recheck partition existence after after a successful metadata refresh
|
|
295
295
|
if refresh_future.succeeded() and isinstance(future.exception, Errors.StaleMetadata):
|
|
296
|
-
log.debug("Stale metadata was raised, and we now have an updated metadata. Rechecking partition
|
|
296
|
+
log.debug("Stale metadata was raised, and we now have an updated metadata. Rechecking partition existence")
|
|
297
297
|
unknown_partition = future.exception.args[0] # TopicPartition from StaleMetadata
|
|
298
298
|
if self._client.cluster.leader_for_partition(unknown_partition) is None:
|
|
299
299
|
log.debug("Removed partition %s from offsets retrieval" % (unknown_partition, ))
|
|
@@ -474,7 +474,9 @@ class Fetcher(six.Iterator):
|
|
|
474
474
|
self.config['value_deserializer'],
|
|
475
475
|
tp.topic, record.value)
|
|
476
476
|
headers = record.headers
|
|
477
|
-
header_size = sum(
|
|
477
|
+
header_size = sum(
|
|
478
|
+
len(h_key.encode("utf-8")) + (len(h_val) if h_val is not None else 0) for h_key, h_val in
|
|
479
|
+
headers) if headers else -1
|
|
478
480
|
yield ConsumerRecord(
|
|
479
481
|
tp.topic, tp.partition, record.offset, record.timestamp,
|
|
480
482
|
record.timestamp_type, key, value, headers, record.checksum,
|
|
@@ -776,6 +778,14 @@ class Fetcher(six.Iterator):
|
|
|
776
778
|
self._sensors.fetch_throttle_time_sensor.record(response.throttle_time_ms)
|
|
777
779
|
self._sensors.fetch_latency.record((time.time() - send_time) * 1000)
|
|
778
780
|
|
|
781
|
+
def _handle_fetch_error(self, node_id, exception):
|
|
782
|
+
log.log(
|
|
783
|
+
logging.INFO if isinstance(exception, Errors.Cancelled) else logging.ERROR,
|
|
784
|
+
'Fetch to node %s failed: %s',
|
|
785
|
+
node_id,
|
|
786
|
+
exception
|
|
787
|
+
)
|
|
788
|
+
|
|
779
789
|
def _parse_fetched_data(self, completed_fetch):
|
|
780
790
|
tp = completed_fetch.topic_partition
|
|
781
791
|
fetch_offset = completed_fetch.fetched_offset
|
|
@@ -815,8 +825,9 @@ class Fetcher(six.Iterator):
|
|
|
815
825
|
position)
|
|
816
826
|
unpacked = list(self._unpack_message_set(tp, records))
|
|
817
827
|
parsed_records = self.PartitionRecords(fetch_offset, tp, unpacked)
|
|
818
|
-
|
|
819
|
-
|
|
828
|
+
if unpacked:
|
|
829
|
+
last_offset = unpacked[-1].offset
|
|
830
|
+
self._sensors.records_fetch_lag.record(highwater - last_offset)
|
|
820
831
|
num_bytes = records.valid_bytes()
|
|
821
832
|
records_count = len(unpacked)
|
|
822
833
|
elif records.size_in_bytes() > 0:
|
kafka/consumer/group.py
CHANGED
|
@@ -167,7 +167,8 @@ class KafkaConsumer(six.Iterator):
|
|
|
167
167
|
message iteration before raising StopIteration (i.e., ending the
|
|
168
168
|
iterator). Default block forever [float('inf')].
|
|
169
169
|
security_protocol (str): Protocol used to communicate with brokers.
|
|
170
|
-
Valid values are: PLAINTEXT, SSL
|
|
170
|
+
Valid values are: PLAINTEXT, SSL, SASL_PLAINTEXT, SASL_SSL.
|
|
171
|
+
Default: PLAINTEXT.
|
|
171
172
|
ssl_context (ssl.SSLContext): Pre-configured SSLContext for wrapping
|
|
172
173
|
socket connections. If provided, all other ssl_* configurations
|
|
173
174
|
will be ignored. Default: None.
|
|
@@ -243,6 +244,7 @@ class KafkaConsumer(six.Iterator):
|
|
|
243
244
|
sasl mechanism handshake. Default: one of bootstrap servers
|
|
244
245
|
sasl_oauth_token_provider (AbstractTokenProvider): OAuthBearer token provider
|
|
245
246
|
instance. (See kafka.oauth.abstract). Default: None
|
|
247
|
+
kafka_client (callable): Custom class / callable for creating KafkaClient instances
|
|
246
248
|
|
|
247
249
|
Note:
|
|
248
250
|
Configuration parameters are described in more detail at
|
|
@@ -305,6 +307,7 @@ class KafkaConsumer(six.Iterator):
|
|
|
305
307
|
'sasl_kerberos_domain_name': None,
|
|
306
308
|
'sasl_oauth_token_provider': None,
|
|
307
309
|
'legacy_iterator': False, # enable to revert to < 1.4.7 iterator
|
|
310
|
+
'kafka_client': KafkaClient,
|
|
308
311
|
}
|
|
309
312
|
DEFAULT_SESSION_TIMEOUT_MS_0_9 = 30000
|
|
310
313
|
|
|
@@ -352,7 +355,7 @@ class KafkaConsumer(six.Iterator):
|
|
|
352
355
|
log.warning('use api_version=%s [tuple] -- "%s" as str is deprecated',
|
|
353
356
|
str(self.config['api_version']), str_version)
|
|
354
357
|
|
|
355
|
-
self._client =
|
|
358
|
+
self._client = self.config['kafka_client'](metrics=self._metrics, **self.config)
|
|
356
359
|
|
|
357
360
|
# Get auto-discovered version from client if necessary
|
|
358
361
|
if self.config['api_version'] is None:
|
|
@@ -650,7 +653,7 @@ class KafkaConsumer(six.Iterator):
|
|
|
650
653
|
# Poll for new data until the timeout expires
|
|
651
654
|
start = time.time()
|
|
652
655
|
remaining = timeout_ms
|
|
653
|
-
while
|
|
656
|
+
while not self._closed:
|
|
654
657
|
records = self._poll_once(remaining, max_records, update_offsets=update_offsets)
|
|
655
658
|
if records:
|
|
656
659
|
return records
|
|
@@ -659,7 +662,9 @@ class KafkaConsumer(six.Iterator):
|
|
|
659
662
|
remaining = timeout_ms - elapsed_ms
|
|
660
663
|
|
|
661
664
|
if remaining <= 0:
|
|
662
|
-
|
|
665
|
+
break
|
|
666
|
+
|
|
667
|
+
return {}
|
|
663
668
|
|
|
664
669
|
def _poll_once(self, timeout_ms, max_records, update_offsets=True):
|
|
665
670
|
"""Do one round of polling. In addition to checking for new data, this does
|
|
File without changes
|