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.
Files changed (45) hide show
  1. kafka/__init__.py +1 -1
  2. kafka/admin/client.py +167 -50
  3. kafka/client_async.py +6 -4
  4. kafka/codec.py +26 -1
  5. kafka/conn.py +15 -4
  6. kafka/consumer/fetcher.py +16 -5
  7. kafka/consumer/group.py +9 -4
  8. kafka/coordinator/assignors/sticky/__init__.py +0 -0
  9. kafka/coordinator/assignors/sticky/partition_movements.py +149 -0
  10. kafka/coordinator/assignors/sticky/sorted_set.py +63 -0
  11. kafka/coordinator/assignors/sticky/sticky_assignor.py +685 -0
  12. kafka/coordinator/base.py +17 -9
  13. kafka/coordinator/consumer.py +4 -1
  14. kafka/errors.py +12 -0
  15. kafka/producer/future.py +3 -3
  16. kafka/producer/kafka.py +23 -8
  17. kafka/producer/record_accumulator.py +4 -4
  18. kafka/producer/sender.py +23 -6
  19. kafka/protocol/__init__.py +3 -0
  20. kafka/protocol/admin.py +234 -2
  21. kafka/protocol/api.py +42 -1
  22. kafka/protocol/fetch.py +180 -2
  23. kafka/protocol/message.py +7 -3
  24. kafka/protocol/offset.py +87 -2
  25. kafka/protocol/parser.py +7 -14
  26. kafka/protocol/produce.py +77 -2
  27. kafka/protocol/types.py +169 -2
  28. kafka/record/_crc32c.py +1 -1
  29. kafka/record/abc.py +1 -1
  30. kafka/record/default_records.py +9 -2
  31. kafka/record/legacy_records.py +1 -1
  32. kafka/record/memory_records.py +1 -1
  33. kafka/record/util.py +1 -1
  34. kafka/scram.py +0 -1
  35. kafka/structs.py +63 -3
  36. kafka/vendor/selectors34.py +5 -1
  37. kafka/vendor/six.py +128 -21
  38. kafka/vendor/socketpair.py +17 -0
  39. kafka/version.py +1 -1
  40. kafka_python-2.0.3.dist-info/METADATA +250 -0
  41. {kafka_python-2.0.1.dist-info → kafka_python-2.0.3.dist-info}/RECORD +44 -40
  42. {kafka_python-2.0.1.dist-info → kafka_python-2.0.3.dist-info}/WHEEL +1 -1
  43. kafka_python-2.0.1.dist-info/METADATA +0 -187
  44. {kafka_python-2.0.1.dist-info → kafka_python-2.0.3.dist-info}/LICENSE +0 -0
  45. {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 2016 Dana Powers, David Arthur, and Contributors'
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.structs import TopicPartition, OffsetAndMetadata
23
- from kafka.admin.acl_resource import ACLOperation, ACLPermissionType, ACLFilter, ACL, ResourcePattern, ResourceType, \
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 = KafkaClient(metrics=self._metrics,
205
- metric_group_prefix='admin',
206
- **self.config)
207
- self._client.check_version()
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 _find_coordinator_id(self, group_id):
328
- """Find the broker node_id of the coordinator of the given group.
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. Will block until
331
- the FindCoordinatorResponse is received. Any errors are immediately
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 group_id: The consumer group ID. This is typically the group
342
+ :param group_ids: A list of consumer group IDs. This is typically the group
335
343
  name as a string.
336
- :return: The node_id of the broker that is the coordinator.
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
- # Note: Java may change how this is implemented in KAFKA-6791.
339
- future = self._find_coordinator_id_send_request(group_id)
340
- self._wait_for_futures([future])
341
- response = future.value
342
- return self._find_coordinator_id_process_response(response)
343
-
344
- def _send_request_to_node(self, node_id, request):
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
- # TODO need to implement converting the response tuple into
1004
- # a more accessible interface like a namedtuple and then stop
1005
- # hardcoding tuple indices here. Several Java examples,
1006
- # including KafkaAdminClient.java
1007
- group_description = response.groups[0]
1008
- error_code = group_description[0]
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
- futures = []
1048
- for group_id in group_ids:
1049
- if group_coordinator_id is not None:
1050
- this_groups_coordinator_id = group_coordinator_id
1051
- else:
1052
- this_groups_coordinator_id = self._find_coordinator_id(group_id)
1053
- f = self._describe_consumer_groups_send_request(
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
- this_groups_coordinator_id,
1095
+ coordinator_id,
1056
1096
  include_authorized_operations)
1057
- futures.append(f)
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
- OffsetAndMetada values.
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
- # OffsetAndMetada values--this is what the Java AdminClient returns
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._find_coordinator_id(group_id)
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
- # delete groups protocol not yet implemented
1238
- # Note: send the request to the group's coordinator.
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 miniumum supported version that
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.info('%s: Closing connection. %s', self, error or '')
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(log.error, 'Fetch to node %s failed: %s', node_id)
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 existance")
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(len(h_key.encode("utf-8")) + len(h_val) for h_key, h_val in headers) if headers else -1
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
- last_offset = unpacked[-1].offset
819
- self._sensors.records_fetch_lag.record(highwater - last_offset)
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. Default: PLAINTEXT.
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 = KafkaClient(metrics=self._metrics, **self.config)
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 True:
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
- return {}
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