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 +1 -1
- kafka/client_async.py +12 -3
- kafka/cluster.py +1 -0
- kafka/conn.py +54 -29
- kafka/consumer/fetcher.py +87 -60
- kafka/consumer/group.py +18 -6
- kafka/consumer/subscription_state.py +68 -56
- kafka/coordinator/base.py +11 -9
- kafka/coordinator/consumer.py +12 -5
- kafka/producer/kafka.py +5 -0
- kafka/protocol/admin.py +2 -1
- kafka/protocol/api.py +8 -6
- kafka/protocol/api_versions.py +45 -1
- kafka/protocol/broker_api_versions.py +2 -0
- kafka/protocol/metadata.py +1 -0
- kafka/protocol/parser.py +7 -6
- kafka/sasl/oauth.py +15 -2
- kafka/util.py +24 -0
- kafka/version.py +1 -1
- {kafka_python-2.1.2.dist-info → kafka_python-2.1.4.dist-info}/METADATA +3 -2
- {kafka_python-2.1.2.dist-info → kafka_python-2.1.4.dist-info}/RECORD +23 -23
- {kafka_python-2.1.2.dist-info → kafka_python-2.1.4.dist-info}/WHEEL +1 -1
- {kafka_python-2.1.2.dist-info → kafka_python-2.1.4.dist-info}/top_level.txt +0 -0
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 <=
|
|
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
|
|
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
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
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
|
-
|
|
569
|
-
for
|
|
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(
|
|
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.
|
|
598
|
-
|
|
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
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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):
|