kafka-python 2.1.2__py2.py3-none-any.whl → 2.1.4__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/admin/client.py CHANGED
@@ -1496,7 +1496,7 @@ class KafkaAdminClient(object):
1496
1496
  A message future
1497
1497
  """
1498
1498
  version = self._client.api_version(OffsetFetchRequest, max_version=5)
1499
- if version <= 3:
1499
+ if version <= 5:
1500
1500
  if partitions is None:
1501
1501
  if version <= 1:
1502
1502
  raise ValueError(
kafka/client_async.py CHANGED
@@ -27,7 +27,7 @@ from kafka.metrics.stats import Avg, Count, Rate
27
27
  from kafka.metrics.stats.rate import TimeUnit
28
28
  from kafka.protocol.broker_api_versions import BROKER_API_VERSIONS
29
29
  from kafka.protocol.metadata import MetadataRequest
30
- from kafka.util import Dict, WeakMethod
30
+ from kafka.util import Dict, WeakMethod, ensure_valid_topic_name
31
31
  # Although this looks unused, it actually monkey-patches socket.socketpair()
32
32
  # and should be left in as long as we're using socket.socketpair() in this file
33
33
  from kafka.vendor import socketpair # noqa: F401
@@ -276,6 +276,7 @@ class KafkaClient(object):
276
276
  if compatible_version:
277
277
  log.warning('Configured api_version %s not supported; using %s',
278
278
  self.config['api_version'], compatible_version)
279
+ self.config['api_version'] = compatible_version
279
280
  self._api_versions = BROKER_API_VERSIONS[compatible_version]
280
281
  else:
281
282
  raise Errors.UnrecognizedBrokerVersion(self.config['api_version'])
@@ -364,7 +365,7 @@ class KafkaClient(object):
364
365
  self._connecting.remove(node_id)
365
366
  try:
366
367
  self._selector.unregister(sock)
367
- except KeyError:
368
+ except (KeyError, ValueError):
368
369
  pass
369
370
 
370
371
  if self._sensors:
@@ -909,7 +910,13 @@ class KafkaClient(object):
909
910
 
910
911
  Returns:
911
912
  Future: resolves after metadata request/response
913
+
914
+ Raises:
915
+ TypeError: if topic is not a string
916
+ ValueError: if topic is invalid: must be chars (a-zA-Z0-9._-), and less than 250 length
912
917
  """
918
+ ensure_valid_topic_name(topic)
919
+
913
920
  if topic in self._topics:
914
921
  return Future().success(set(self._topics))
915
922
 
@@ -971,8 +978,10 @@ class KafkaClient(object):
971
978
  topics = list(self.config['bootstrap_topics_filter'])
972
979
 
973
980
  api_version = self.api_version(MetadataRequest, max_version=7)
974
- if self.cluster.need_all_topic_metadata or not topics:
981
+ if self.cluster.need_all_topic_metadata:
975
982
  topics = MetadataRequest[api_version].ALL_TOPICS
983
+ elif not topics:
984
+ topics = MetadataRequest[api_version].NO_TOPICS
976
985
  if api_version >= 4:
977
986
  request = MetadataRequest[api_version](topics, self.config['allow_auto_create_topics'])
978
987
  else:
kafka/cluster.py CHANGED
@@ -112,6 +112,7 @@ class ClusterMetadata(object):
112
112
 
113
113
  Returns:
114
114
  set: {partition (int), ...}
115
+ None if topic not found.
115
116
  """
116
117
  if topic not in self._partitions:
117
118
  return None
kafka/conn.py CHANGED
@@ -101,6 +101,10 @@ class BrokerConnection(object):
101
101
  server-side log entries that correspond to this client. Also
102
102
  submitted to GroupCoordinator for logging with respect to
103
103
  consumer group administration. Default: 'kafka-python-{version}'
104
+ client_software_name (str): Sent to kafka broker for KIP-511.
105
+ Default: 'kafka-python'
106
+ client_software_version (str): Sent to kafka broker for KIP-511.
107
+ Default: The kafka-python version (via kafka.version).
104
108
  reconnect_backoff_ms (int): The amount of time in milliseconds to
105
109
  wait before attempting to reconnect to a given host.
106
110
  Default: 50.
@@ -191,6 +195,8 @@ class BrokerConnection(object):
191
195
 
192
196
  DEFAULT_CONFIG = {
193
197
  'client_id': 'kafka-python-' + __version__,
198
+ 'client_software_name': 'kafka-python',
199
+ 'client_software_version': __version__,
194
200
  'node_id': 0,
195
201
  'request_timeout_ms': 30000,
196
202
  'reconnect_backoff_ms': 50,
@@ -242,7 +248,7 @@ class BrokerConnection(object):
242
248
  self._api_versions = None
243
249
  self._api_version = None
244
250
  self._check_version_idx = None
245
- self._api_versions_idx = 2
251
+ self._api_versions_idx = 4 # version of ApiVersionsRequest to try on first connect
246
252
  self._throttle_time = None
247
253
  self._socks5_proxy = None
248
254
 
@@ -265,12 +271,10 @@ class BrokerConnection(object):
265
271
  assert self.config['security_protocol'] in self.SECURITY_PROTOCOLS, (
266
272
  'security_protocol must be in ' + ', '.join(self.SECURITY_PROTOCOLS))
267
273
 
268
- self._sasl_mechanism = None
269
274
  if self.config['security_protocol'] in ('SSL', 'SASL_SSL'):
270
275
  assert ssl_available, "Python wasn't built with SSL support"
271
276
 
272
- if self.config['security_protocol'] in ('SASL_PLAINTEXT', 'SASL_SSL'):
273
- self._sasl_mechanism = get_sasl_mechanism(self.config['sasl_mechanism'])(**self.config)
277
+ self._init_sasl_mechanism()
274
278
 
275
279
  # This is not a general lock / this class is not generally thread-safe yet
276
280
  # However, to avoid pushing responsibility for maintaining
@@ -306,11 +310,17 @@ class BrokerConnection(object):
306
310
  self.config['metric_group_prefix'],
307
311
  self.node_id)
308
312
 
313
+ def _init_sasl_mechanism(self):
314
+ if self.config['security_protocol'] in ('SASL_PLAINTEXT', 'SASL_SSL'):
315
+ self._sasl_mechanism = get_sasl_mechanism(self.config['sasl_mechanism'])(**self.config)
316
+ else:
317
+ self._sasl_mechanism = None
318
+
309
319
  def _dns_lookup(self):
310
320
  self._gai = dns_lookup(self.host, self.port, self.afi)
311
321
  if not self._gai:
312
- log.error('DNS lookup failed for %s:%i (%s)',
313
- self.host, self.port, self.afi)
322
+ log.error('%s: DNS lookup failed for %s:%i (%s)',
323
+ self, self.host, self.port, self.afi)
314
324
  return False
315
325
  return True
316
326
 
@@ -356,6 +366,7 @@ class BrokerConnection(object):
356
366
  def connect(self):
357
367
  """Attempt to connect and return ConnectionState"""
358
368
  if self.state is ConnectionStates.DISCONNECTED and not self.blacked_out():
369
+ self.state = ConnectionStates.CONNECTING
359
370
  self.last_attempt = time.time()
360
371
  next_lookup = self._next_afi_sockaddr()
361
372
  if not next_lookup:
@@ -380,7 +391,6 @@ class BrokerConnection(object):
380
391
  self._sock.setsockopt(*option)
381
392
 
382
393
  self._sock.setblocking(False)
383
- self.state = ConnectionStates.CONNECTING
384
394
  self.config['state_change_callback'](self.node_id, self._sock, self)
385
395
  log.info('%s: connecting to %s:%d [%s %s]', self, self.host,
386
396
  self.port, self._sock_addr, AFI_NAMES[self._sock_afi])
@@ -402,20 +412,20 @@ class BrokerConnection(object):
402
412
  log.debug('%s: established TCP connection', self)
403
413
 
404
414
  if self.config['security_protocol'] in ('SSL', 'SASL_SSL'):
405
- log.debug('%s: initiating SSL handshake', self)
406
415
  self.state = ConnectionStates.HANDSHAKE
416
+ log.debug('%s: initiating SSL handshake', self)
407
417
  self.config['state_change_callback'](self.node_id, self._sock, self)
408
418
  # _wrap_ssl can alter the connection state -- disconnects on failure
409
419
  self._wrap_ssl()
410
420
  else:
411
- log.debug('%s: checking broker Api Versions', self)
412
421
  self.state = ConnectionStates.API_VERSIONS_SEND
422
+ log.debug('%s: checking broker Api Versions', self)
413
423
  self.config['state_change_callback'](self.node_id, self._sock, self)
414
424
 
415
425
  # Connection failed
416
426
  # WSAEINVAL == 10022, but errno.WSAEINVAL is not available on non-win systems
417
427
  elif ret not in (errno.EINPROGRESS, errno.EALREADY, errno.EWOULDBLOCK, 10022):
418
- log.error('Connect attempt to %s returned error %s.'
428
+ log.error('%s: Connect attempt returned error %s.'
419
429
  ' Disconnecting.', self, ret)
420
430
  errstr = errno.errorcode.get(ret, 'UNKNOWN')
421
431
  self.close(Errors.KafkaConnectionError('{} {}'.format(ret, errstr)))
@@ -428,8 +438,8 @@ class BrokerConnection(object):
428
438
  if self.state is ConnectionStates.HANDSHAKE:
429
439
  if self._try_handshake():
430
440
  log.debug('%s: completed SSL handshake.', self)
431
- log.debug('%s: checking broker Api Versions', self)
432
441
  self.state = ConnectionStates.API_VERSIONS_SEND
442
+ log.debug('%s: checking broker Api Versions', self)
433
443
  self.config['state_change_callback'](self.node_id, self._sock, self)
434
444
 
435
445
  if self.state in (ConnectionStates.API_VERSIONS_SEND, ConnectionStates.API_VERSIONS_RECV):
@@ -437,13 +447,13 @@ class BrokerConnection(object):
437
447
  # _try_api_versions_check has side-effects: possibly disconnected on socket errors
438
448
  if self.state in (ConnectionStates.API_VERSIONS_SEND, ConnectionStates.API_VERSIONS_RECV):
439
449
  if self.config['security_protocol'] in ('SASL_PLAINTEXT', 'SASL_SSL'):
440
- log.debug('%s: initiating SASL authentication', self)
441
450
  self.state = ConnectionStates.AUTHENTICATING
451
+ log.debug('%s: initiating SASL authentication', self)
442
452
  self.config['state_change_callback'](self.node_id, self._sock, self)
443
453
  else:
444
454
  # security_protocol PLAINTEXT
445
- log.info('%s: Connection complete.', self)
446
455
  self.state = ConnectionStates.CONNECTED
456
+ log.info('%s: Connection complete.', self)
447
457
  self._reset_reconnect_backoff()
448
458
  self.config['state_change_callback'](self.node_id, self._sock, self)
449
459
 
@@ -452,8 +462,8 @@ class BrokerConnection(object):
452
462
  if self._try_authenticate():
453
463
  # _try_authenticate has side-effects: possibly disconnected on socket errors
454
464
  if self.state is ConnectionStates.AUTHENTICATING:
455
- log.info('%s: Connection complete.', self)
456
465
  self.state = ConnectionStates.CONNECTED
466
+ log.info('%s: Connection complete.', self)
457
467
  self._reset_reconnect_backoff()
458
468
  self.config['state_change_callback'](self.node_id, self._sock, self)
459
469
 
@@ -462,7 +472,7 @@ class BrokerConnection(object):
462
472
  # Connection timed out
463
473
  request_timeout = self.config['request_timeout_ms'] / 1000.0
464
474
  if time.time() > request_timeout + self.last_attempt:
465
- log.error('Connection attempt to %s timed out', self)
475
+ log.error('%s: Connection attempt timed out', self)
466
476
  self.close(Errors.KafkaConnectionError('timeout'))
467
477
  return self.state
468
478
 
@@ -521,7 +531,7 @@ class BrokerConnection(object):
521
531
  except (SSLWantReadError, SSLWantWriteError):
522
532
  pass
523
533
  except (SSLZeroReturnError, ConnectionError, TimeoutError, SSLEOFError):
524
- log.warning('SSL connection closed by server during handshake.')
534
+ log.warning('%s: SSL connection closed by server during handshake.', self)
525
535
  self.close(Errors.KafkaConnectionError('SSL connection closed by server during handshake'))
526
536
  # Other SSLErrors will be raised to user
527
537
 
@@ -538,7 +548,14 @@ class BrokerConnection(object):
538
548
  log.debug('%s: Using pre-configured api_version %s for ApiVersions', self, self._api_version)
539
549
  return True
540
550
  elif self._check_version_idx is None:
541
- request = ApiVersionsRequest[self._api_versions_idx]()
551
+ version = self._api_versions_idx
552
+ if version >= 3:
553
+ request = ApiVersionsRequest[version](
554
+ client_software_name=self.config['client_software_name'],
555
+ client_software_version=self.config['client_software_version'],
556
+ _tagged_fields={})
557
+ else:
558
+ request = ApiVersionsRequest[version]()
542
559
  future = Future()
543
560
  response = self._send(request, blocking=True, request_timeout_ms=(self.config['api_version_auto_timeout_ms'] * 0.8))
544
561
  response.add_callback(self._handle_api_versions_response, future)
@@ -573,11 +590,15 @@ class BrokerConnection(object):
573
590
 
574
591
  def _handle_api_versions_response(self, future, response):
575
592
  error_type = Errors.for_code(response.error_code)
576
- # if error_type i UNSUPPORTED_VERSION: retry w/ latest version from response
577
593
  if error_type is not Errors.NoError:
578
594
  future.failure(error_type())
579
595
  if error_type is Errors.UnsupportedVersionError:
580
596
  self._api_versions_idx -= 1
597
+ for api_key, min_version, max_version, *rest in response.api_versions:
598
+ # If broker provides a lower max_version, skip to that
599
+ if api_key == response.API_KEY:
600
+ self._api_versions_idx = min(self._api_versions_idx, max_version)
601
+ break
581
602
  if self._api_versions_idx >= 0:
582
603
  self._api_versions_future = None
583
604
  self.state = ConnectionStates.API_VERSIONS_SEND
@@ -587,10 +608,10 @@ class BrokerConnection(object):
587
608
  return
588
609
  self._api_versions = dict([
589
610
  (api_key, (min_version, max_version))
590
- for api_key, min_version, max_version in response.api_versions
611
+ for api_key, min_version, max_version, *rest in response.api_versions
591
612
  ])
592
613
  self._api_version = self._infer_broker_version_from_api_versions(self._api_versions)
593
- log.info('Broker version identified as %s', '.'.join(map(str, self._api_version)))
614
+ log.info('%s: Broker version identified as %s', self, '.'.join(map(str, self._api_version)))
594
615
  future.success(self._api_version)
595
616
  self.connect()
596
617
 
@@ -600,7 +621,7 @@ class BrokerConnection(object):
600
621
  # after failure connection is closed, so state should already be DISCONNECTED
601
622
 
602
623
  def _handle_check_version_response(self, future, version, _response):
603
- log.info('Broker version identified as %s', '.'.join(map(str, version)))
624
+ log.info('%s: Broker version identified as %s', self, '.'.join(map(str, version)))
604
625
  log.info('Set configuration api_version=%s to skip auto'
605
626
  ' check_version requests on startup', version)
606
627
  self._api_versions = BROKER_API_VERSIONS[version]
@@ -730,6 +751,7 @@ class BrokerConnection(object):
730
751
  request = SaslAuthenticateRequest[0](sasl_auth_bytes)
731
752
  self._send(request, blocking=True)
732
753
  else:
754
+ log.debug('%s: Sending %d raw sasl auth bytes to server', self, len(sasl_auth_bytes))
733
755
  try:
734
756
  self._send_bytes_blocking(Int32.encode(len(sasl_auth_bytes)) + sasl_auth_bytes)
735
757
  except (ConnectionError, TimeoutError) as e:
@@ -759,7 +781,7 @@ class BrokerConnection(object):
759
781
  latency_ms = (time.time() - timestamp) * 1000
760
782
  if self._sensors:
761
783
  self._sensors.request_time.record(latency_ms)
762
- log.debug('%s Response %d (%s ms): %s', self, correlation_id, latency_ms, response)
784
+ log.debug('%s: Response %d (%s ms): %s', self, correlation_id, latency_ms, response)
763
785
 
764
786
  error_type = Errors.for_code(response.error_code)
765
787
  if error_type is not Errors.NoError:
@@ -770,6 +792,7 @@ class BrokerConnection(object):
770
792
  return response.auth_bytes
771
793
  else:
772
794
  # unframed bytes w/ SaslHandhake v0
795
+ log.debug('%s: Received %d raw sasl auth bytes from server', self, nbytes)
773
796
  return data[4:]
774
797
 
775
798
  def _sasl_authenticate(self, future):
@@ -913,6 +936,7 @@ class BrokerConnection(object):
913
936
  self._update_reconnect_backoff()
914
937
  self._api_versions_future = None
915
938
  self._sasl_auth_future = None
939
+ self._init_sasl_mechanism()
916
940
  self._protocol = KafkaProtocol(
917
941
  client_id=self.config['client_id'],
918
942
  api_version=self.config['api_version'])
@@ -932,7 +956,8 @@ class BrokerConnection(object):
932
956
 
933
957
  # drop lock before state change callback and processing futures
934
958
  self.config['state_change_callback'](self.node_id, sock, self)
935
- sock.close()
959
+ if sock:
960
+ sock.close()
936
961
  for (_correlation_id, (future, _timestamp, _timeout)) in ifrs:
937
962
  future.failure(error)
938
963
 
@@ -978,7 +1003,7 @@ class BrokerConnection(object):
978
1003
 
979
1004
  correlation_id = self._protocol.send_request(request)
980
1005
 
981
- log.debug('%s Request %d (timeout_ms %s): %s', self, correlation_id, request_timeout_ms, request)
1006
+ log.debug('%s: Request %d (timeout_ms %s): %s', self, correlation_id, request_timeout_ms, request)
982
1007
  if request.expect_response():
983
1008
  assert correlation_id not in self.in_flight_requests, 'Correlation ID already in-flight!'
984
1009
  sent_time = time.time()
@@ -1012,7 +1037,7 @@ class BrokerConnection(object):
1012
1037
  return True
1013
1038
 
1014
1039
  except (ConnectionError, TimeoutError) as e:
1015
- log.exception("Error sending request data to %s", self)
1040
+ log.exception("%s: Error sending request data", self)
1016
1041
  error = Errors.KafkaConnectionError("%s: %s" % (self, e))
1017
1042
  self.close(error=error)
1018
1043
  return False
@@ -1045,7 +1070,7 @@ class BrokerConnection(object):
1045
1070
  return len(self._send_buffer) == 0
1046
1071
 
1047
1072
  except (ConnectionError, TimeoutError, Exception) as e:
1048
- log.exception("Error sending request data to %s", self)
1073
+ log.exception("%s: Error sending request data", self)
1049
1074
  error = Errors.KafkaConnectionError("%s: %s" % (self, e))
1050
1075
  self.close(error=error)
1051
1076
  return False
@@ -1082,7 +1107,7 @@ class BrokerConnection(object):
1082
1107
  if not responses and self.requests_timed_out():
1083
1108
  timed_out = self.timed_out_ifrs()
1084
1109
  timeout_ms = (timed_out[0][2] - timed_out[0][1]) * 1000
1085
- log.warning('%s timed out after %s ms. Closing connection.',
1110
+ log.warning('%s: timed out after %s ms. Closing connection.',
1086
1111
  self, timeout_ms)
1087
1112
  self.close(error=Errors.RequestTimedOutError(
1088
1113
  'Request timed out after %s ms' %
@@ -1101,7 +1126,7 @@ class BrokerConnection(object):
1101
1126
  if self._sensors:
1102
1127
  self._sensors.request_time.record(latency_ms)
1103
1128
 
1104
- log.debug('%s Response %d (%s ms): %s', self, correlation_id, latency_ms, response)
1129
+ log.debug('%s: Response %d (%s ms): %s', self, correlation_id, latency_ms, response)
1105
1130
  self._maybe_throttle(response)
1106
1131
  responses[i] = (response, future)
1107
1132
 
@@ -1113,7 +1138,7 @@ class BrokerConnection(object):
1113
1138
  err = None
1114
1139
  with self._lock:
1115
1140
  if not self._can_send_recv():
1116
- log.warning('%s cannot recv: socket not connected', self)
1141
+ log.warning('%s: cannot recv: socket not connected', self)
1117
1142
  return ()
1118
1143
 
1119
1144
  while len(recvd) < self.config['sock_chunk_buffer_count']:
kafka/consumer/fetcher.py CHANGED
@@ -4,7 +4,6 @@ import collections
4
4
  import copy
5
5
  import itertools
6
6
  import logging
7
- import random
8
7
  import sys
9
8
  import time
10
9
 
@@ -57,7 +56,6 @@ class Fetcher(six.Iterator):
57
56
  'max_partition_fetch_bytes': 1048576,
58
57
  'max_poll_records': sys.maxsize,
59
58
  'check_crcs': True,
60
- 'iterator_refetch_records': 1, # undocumented -- interface may change
61
59
  'metric_group_prefix': 'consumer',
62
60
  'retry_backoff_ms': 100,
63
61
  'enable_incremental_fetch_sessions': True,
@@ -116,6 +114,7 @@ class Fetcher(six.Iterator):
116
114
  self._sensors = FetchManagerMetrics(metrics, self.config['metric_group_prefix'])
117
115
  self._isolation_level = READ_UNCOMMITTED
118
116
  self._session_handlers = {}
117
+ self._nodes_with_pending_fetch_requests = set()
119
118
 
120
119
  def send_fetches(self):
121
120
  """Send FetchRequests for all assigned partitions that do not already have
@@ -126,12 +125,12 @@ class Fetcher(six.Iterator):
126
125
  """
127
126
  futures = []
128
127
  for node_id, (request, fetch_offsets) in six.iteritems(self._create_fetch_requests()):
129
- if self._client.ready(node_id):
130
- log.debug("Sending FetchRequest to node %s", node_id)
131
- future = self._client.send(node_id, request, wakeup=False)
132
- future.add_callback(self._handle_fetch_response, node_id, fetch_offsets, time.time())
133
- future.add_errback(self._handle_fetch_error, node_id)
134
- futures.append(future)
128
+ log.debug("Sending FetchRequest to node %s", node_id)
129
+ self._nodes_with_pending_fetch_requests.add(node_id)
130
+ future = self._client.send(node_id, request, wakeup=False)
131
+ future.add_callback(self._handle_fetch_response, node_id, fetch_offsets, time.time())
132
+ future.add_errback(self._handle_fetch_error, node_id)
133
+ futures.append(future)
135
134
  self._fetch_futures.extend(futures)
136
135
  self._clean_done_fetch_futures()
137
136
  return futures
@@ -380,10 +379,13 @@ class Fetcher(six.Iterator):
380
379
  # as long as the partition is still assigned
381
380
  position = self._subscriptions.assignment[tp].position
382
381
  if part.next_fetch_offset == position.offset:
383
- part_records = part.take(max_records)
384
382
  log.debug("Returning fetched records at offset %d for assigned"
385
383
  " partition %s", position.offset, tp)
386
- drained[tp].extend(part_records)
384
+ part_records = part.take(max_records)
385
+ # list.extend([]) is a noop, but because drained is a defaultdict
386
+ # we should avoid initializing the default list unless there are records
387
+ if part_records:
388
+ drained[tp].extend(part_records)
387
389
  # We want to increment subscription position if (1) we're using consumer.poll(),
388
390
  # or (2) we didn't return any records (consumer iterator will update position
389
391
  # when each message is yielded). There may be edge cases where we re-fetch records
@@ -562,13 +564,11 @@ class Fetcher(six.Iterator):
562
564
  def _fetchable_partitions(self):
563
565
  fetchable = self._subscriptions.fetchable_partitions()
564
566
  # do not fetch a partition if we have a pending fetch response to process
567
+ discard = {fetch.topic_partition for fetch in self._completed_fetches}
565
568
  current = self._next_partition_records
566
- pending = copy.copy(self._completed_fetches)
567
569
  if current:
568
- fetchable.discard(current.topic_partition)
569
- for fetch in pending:
570
- fetchable.discard(fetch.topic_partition)
571
- return fetchable
570
+ discard.add(current.topic_partition)
571
+ return [tp for tp in fetchable if tp not in discard]
572
572
 
573
573
  def _create_fetch_requests(self):
574
574
  """Create fetch requests for all assigned partitions, grouped by node.
@@ -581,7 +581,7 @@ class Fetcher(six.Iterator):
581
581
  # create the fetch info as a dict of lists of partition info tuples
582
582
  # which can be passed to FetchRequest() via .items()
583
583
  version = self._client.api_version(FetchRequest, max_version=10)
584
- fetchable = collections.defaultdict(dict)
584
+ fetchable = collections.defaultdict(collections.OrderedDict)
585
585
 
586
586
  for partition in self._fetchable_partitions():
587
587
  node_id = self._client.cluster.leader_for_partition(partition)
@@ -594,8 +594,20 @@ class Fetcher(six.Iterator):
594
594
  " Requesting metadata update", partition)
595
595
  self._client.cluster.request_update()
596
596
 
597
- elif self._client.in_flight_request_count(node_id) > 0:
598
- log.log(0, "Skipping fetch for partition %s because there is an inflight request to node %s",
597
+ elif not self._client.connected(node_id) and self._client.connection_delay(node_id) > 0:
598
+ # If we try to send during the reconnect backoff window, then the request is just
599
+ # going to be failed anyway before being sent, so skip the send for now
600
+ log.log(0, "Skipping fetch for partition %s because node %s is awaiting reconnect backoff",
601
+ partition, node_id)
602
+
603
+ elif self._client.throttle_delay(node_id) > 0:
604
+ # If we try to send while throttled, then the request is just
605
+ # going to be failed anyway before being sent, so skip the send for now
606
+ log.log(0, "Skipping fetch for partition %s because node %s is throttled",
607
+ partition, node_id)
608
+
609
+ elif node_id in self._nodes_with_pending_fetch_requests:
610
+ log.log(0, "Skipping fetch for partition %s because there is a pending fetch request to node %s",
599
611
  partition, node_id)
600
612
  continue
601
613
 
@@ -695,10 +707,7 @@ class Fetcher(six.Iterator):
695
707
  for partition_data in partitions])
696
708
  metric_aggregator = FetchResponseMetricAggregator(self._sensors, partitions)
697
709
 
698
- # randomized ordering should improve balance for short-lived consumers
699
- random.shuffle(response.topics)
700
710
  for topic, partitions in response.topics:
701
- random.shuffle(partitions)
702
711
  for partition_data in partitions:
703
712
  tp = TopicPartition(topic, partition_data[0])
704
713
  fetch_offset = fetch_offsets[tp]
@@ -711,12 +720,14 @@ class Fetcher(six.Iterator):
711
720
  self._completed_fetches.append(completed_fetch)
712
721
 
713
722
  self._sensors.fetch_latency.record((time.time() - send_time) * 1000)
723
+ self._nodes_with_pending_fetch_requests.remove(node_id)
714
724
 
715
725
  def _handle_fetch_error(self, node_id, exception):
716
726
  level = logging.INFO if isinstance(exception, Errors.Cancelled) else logging.ERROR
717
727
  log.log(level, 'Fetch to node %s failed: %s', node_id, exception)
718
728
  if node_id in self._session_handlers:
719
729
  self._session_handlers[node_id].handle_error(exception)
730
+ self._nodes_with_pending_fetch_requests.remove(node_id)
720
731
 
721
732
  def _parse_fetched_data(self, completed_fetch):
722
733
  tp = completed_fetch.topic_partition
@@ -733,8 +744,6 @@ class Fetcher(six.Iterator):
733
744
  " since it is no longer fetchable", tp)
734
745
 
735
746
  elif error_type is Errors.NoError:
736
- self._subscriptions.assignment[tp].highwater = highwater
737
-
738
747
  # we are interested in this fetch only if the beginning
739
748
  # offset (of the *request*) matches the current consumed position
740
749
  # Note that the *response* may return a messageset that starts
@@ -748,30 +757,35 @@ class Fetcher(six.Iterator):
748
757
  return None
749
758
 
750
759
  records = MemoryRecords(completed_fetch.partition_data[-1])
751
- if records.has_next():
752
- log.debug("Adding fetched record for partition %s with"
753
- " offset %d to buffered record list", tp,
754
- position.offset)
755
- parsed_records = self.PartitionRecords(fetch_offset, tp, records,
756
- self.config['key_deserializer'],
757
- self.config['value_deserializer'],
758
- self.config['check_crcs'],
759
- completed_fetch.metric_aggregator)
760
- return parsed_records
761
- elif records.size_in_bytes() > 0:
762
- # we did not read a single message from a non-empty
763
- # buffer because that message's size is larger than
764
- # fetch size, in this case record this exception
765
- record_too_large_partitions = {tp: fetch_offset}
766
- raise RecordTooLargeError(
767
- "There are some messages at [Partition=Offset]: %s "
768
- " whose size is larger than the fetch size %s"
769
- " and hence cannot be ever returned."
770
- " Increase the fetch size, or decrease the maximum message"
771
- " size the broker will allow." % (
772
- record_too_large_partitions,
773
- self.config['max_partition_fetch_bytes']),
774
- record_too_large_partitions)
760
+ log.debug("Preparing to read %s bytes of data for partition %s with offset %d",
761
+ records.size_in_bytes(), tp, fetch_offset)
762
+ parsed_records = self.PartitionRecords(fetch_offset, tp, records,
763
+ self.config['key_deserializer'],
764
+ self.config['value_deserializer'],
765
+ self.config['check_crcs'],
766
+ completed_fetch.metric_aggregator,
767
+ self._on_partition_records_drain)
768
+ if not records.has_next() and records.size_in_bytes() > 0:
769
+ if completed_fetch.response_version < 3:
770
+ # Implement the pre KIP-74 behavior of throwing a RecordTooLargeException.
771
+ record_too_large_partitions = {tp: fetch_offset}
772
+ raise RecordTooLargeError(
773
+ "There are some messages at [Partition=Offset]: %s "
774
+ " whose size is larger than the fetch size %s"
775
+ " and hence cannot be ever returned. Please condier upgrading your broker to 0.10.1.0 or"
776
+ " newer to avoid this issue. Alternatively, increase the fetch size on the client (using"
777
+ " max_partition_fetch_bytes)" % (
778
+ record_too_large_partitions,
779
+ self.config['max_partition_fetch_bytes']),
780
+ record_too_large_partitions)
781
+ else:
782
+ # This should not happen with brokers that support FetchRequest/Response V3 or higher (i.e. KIP-74)
783
+ raise Errors.KafkaError("Failed to make progress reading messages at %s=%s."
784
+ " Received a non-empty fetch response from the server, but no"
785
+ " complete records were found." % (tp, fetch_offset))
786
+
787
+ if highwater >= 0:
788
+ self._subscriptions.assignment[tp].highwater = highwater
775
789
 
776
790
  elif error_type in (Errors.NotLeaderForPartitionError,
777
791
  Errors.ReplicaNotAvailableError,
@@ -805,14 +819,25 @@ class Fetcher(six.Iterator):
805
819
  if parsed_records is None:
806
820
  completed_fetch.metric_aggregator.record(tp, 0, 0)
807
821
 
808
- return None
822
+ if error_type is not Errors.NoError:
823
+ # we move the partition to the end if there was an error. This way, it's more likely that partitions for
824
+ # the same topic can remain together (allowing for more efficient serialization).
825
+ self._subscriptions.move_partition_to_end(tp)
826
+
827
+ return parsed_records
828
+
829
+ def _on_partition_records_drain(self, partition_records):
830
+ # we move the partition to the end if we received some bytes. This way, it's more likely that partitions
831
+ # for the same topic can remain together (allowing for more efficient serialization).
832
+ if partition_records.bytes_read > 0:
833
+ self._subscriptions.move_partition_to_end(partition_records.topic_partition)
809
834
 
810
835
  def close(self):
811
836
  if self._next_partition_records is not None:
812
837
  self._next_partition_records.drain()
813
838
 
814
839
  class PartitionRecords(object):
815
- def __init__(self, fetch_offset, tp, records, key_deserializer, value_deserializer, check_crcs, metric_aggregator):
840
+ def __init__(self, fetch_offset, tp, records, key_deserializer, value_deserializer, check_crcs, metric_aggregator, on_drain):
816
841
  self.fetch_offset = fetch_offset
817
842
  self.topic_partition = tp
818
843
  self.leader_epoch = -1
@@ -824,6 +849,7 @@ class Fetcher(six.Iterator):
824
849
  self.record_iterator = itertools.dropwhile(
825
850
  self._maybe_skip_record,
826
851
  self._unpack_records(tp, records, key_deserializer, value_deserializer))
852
+ self.on_drain = on_drain
827
853
 
828
854
  def _maybe_skip_record(self, record):
829
855
  # When fetching an offset that is in the middle of a
@@ -845,6 +871,7 @@ class Fetcher(six.Iterator):
845
871
  if self.record_iterator is not None:
846
872
  self.record_iterator = None
847
873
  self.metric_aggregator.record(self.topic_partition, self.bytes_read, self.records_read)
874
+ self.on_drain(self)
848
875
 
849
876
  def take(self, n=None):
850
877
  return list(itertools.islice(self.record_iterator, 0, n))
@@ -943,6 +970,13 @@ class FetchSessionHandler(object):
943
970
  self.session_partitions = {}
944
971
 
945
972
  def build_next(self, next_partitions):
973
+ """
974
+ Arguments:
975
+ next_partitions (dict): TopicPartition -> TopicPartitionState
976
+
977
+ Returns:
978
+ FetchRequestData
979
+ """
946
980
  if self.next_metadata.is_full:
947
981
  log.debug("Built full fetch %s for node %s with %s partition(s).",
948
982
  self.next_metadata, self.node_id, len(next_partitions))
@@ -965,8 +999,8 @@ class FetchSessionHandler(object):
965
999
  altered.add(tp)
966
1000
 
967
1001
  log.debug("Built incremental fetch %s for node %s. Added %s, altered %s, removed %s out of %s",
968
- self.next_metadata, self.node_id, added, altered, removed, self.session_partitions.keys())
969
- to_send = {tp: next_partitions[tp] for tp in (added | altered)}
1002
+ self.next_metadata, self.node_id, added, altered, removed, self.session_partitions.keys())
1003
+ to_send = collections.OrderedDict({tp: next_partitions[tp] for tp in next_partitions if tp in (added | altered)})
970
1004
  return FetchRequestData(to_send, removed, self.next_metadata)
971
1005
 
972
1006
  def handle_response(self, response):
@@ -1106,18 +1140,11 @@ class FetchRequestData(object):
1106
1140
  @property
1107
1141
  def to_send(self):
1108
1142
  # Return as list of [(topic, [(partition, ...), ...]), ...]
1109
- # so it an be passed directly to encoder
1143
+ # so it can be passed directly to encoder
1110
1144
  partition_data = collections.defaultdict(list)
1111
1145
  for tp, partition_info in six.iteritems(self._to_send):
1112
1146
  partition_data[tp.topic].append(partition_info)
1113
- # As of version == 3 partitions will be returned in order as
1114
- # they are requested, so to avoid starvation with
1115
- # `fetch_max_bytes` option we need this shuffle
1116
- # NOTE: we do have partition_data in random order due to usage
1117
- # of unordered structures like dicts, but that does not
1118
- # guarantee equal distribution, and starting in Python3.6
1119
- # dicts retain insert order.
1120
- return random.sample(list(partition_data.items()), k=len(partition_data))
1147
+ return list(partition_data.items())
1121
1148
 
1122
1149
  @property
1123
1150
  def to_forget(self):