kafka-python 2.1.2__tar.gz → 2.1.3__tar.gz
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_python-2.1.2 → kafka_python-2.1.3}/CHANGES.md +22 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/PKG-INFO +3 -2
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/admin/client.py +1 -1
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/client_async.py +8 -1
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/cluster.py +1 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/conn.py +21 -4
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/consumer/fetcher.py +64 -52
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/consumer/group.py +18 -6
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/consumer/subscription_state.py +68 -56
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/coordinator/base.py +11 -9
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/coordinator/consumer.py +12 -5
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/producer/kafka.py +5 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/protocol/admin.py +2 -1
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/protocol/api.py +8 -6
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/protocol/api_versions.py +45 -1
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/protocol/broker_api_versions.py +2 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/protocol/parser.py +7 -6
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/util.py +24 -0
- kafka_python-2.1.3/kafka/version.py +1 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka_python.egg-info/PKG-INFO +3 -2
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka_python.egg-info/SOURCES.txt +1 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/pyproject.toml +1 -0
- kafka_python-2.1.3/test/test_consumer.py +52 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/test/test_consumer_group.py +7 -6
- {kafka_python-2.1.2 → kafka_python-2.1.3}/test/test_consumer_integration.py +2 -2
- {kafka_python-2.1.2 → kafka_python-2.1.3}/test/test_coordinator.py +1 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/test/test_fetcher.py +6 -6
- {kafka_python-2.1.2 → kafka_python-2.1.3}/test/test_producer.py +1 -1
- {kafka_python-2.1.2 → kafka_python-2.1.3}/test/test_sasl_integration.py +1 -1
- kafka_python-2.1.3/test/test_subscription_state.py +57 -0
- kafka_python-2.1.2/test/test_subscription_state.py → kafka_python-2.1.3/test/test_util.py +2 -3
- kafka_python-2.1.2/kafka/version.py +0 -1
- kafka_python-2.1.2/test/test_consumer.py +0 -26
- {kafka_python-2.1.2 → kafka_python-2.1.3}/AUTHORS.md +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/LICENSE +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/MANIFEST.in +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/README.rst +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/__init__.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/admin/__init__.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/admin/acl_resource.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/admin/config_resource.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/admin/new_partitions.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/admin/new_topic.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/codec.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/consumer/__init__.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/coordinator/__init__.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/coordinator/assignors/__init__.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/coordinator/assignors/abstract.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/coordinator/assignors/range.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/coordinator/assignors/roundrobin.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/coordinator/assignors/sticky/__init__.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/coordinator/assignors/sticky/partition_movements.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/coordinator/assignors/sticky/sorted_set.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/coordinator/assignors/sticky/sticky_assignor.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/coordinator/heartbeat.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/coordinator/protocol.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/errors.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/future.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/metrics/__init__.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/metrics/compound_stat.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/metrics/dict_reporter.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/metrics/kafka_metric.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/metrics/measurable.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/metrics/measurable_stat.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/metrics/metric_config.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/metrics/metric_name.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/metrics/metrics.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/metrics/metrics_reporter.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/metrics/quota.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/metrics/stat.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/metrics/stats/__init__.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/metrics/stats/avg.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/metrics/stats/count.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/metrics/stats/histogram.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/metrics/stats/max_stat.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/metrics/stats/min_stat.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/metrics/stats/percentile.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/metrics/stats/percentiles.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/metrics/stats/rate.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/metrics/stats/sampled_stat.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/metrics/stats/sensor.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/metrics/stats/total.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/partitioner/__init__.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/partitioner/default.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/producer/__init__.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/producer/buffer.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/producer/future.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/producer/record_accumulator.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/producer/sender.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/protocol/__init__.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/protocol/abstract.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/protocol/commit.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/protocol/fetch.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/protocol/find_coordinator.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/protocol/frame.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/protocol/group.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/protocol/list_offsets.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/protocol/message.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/protocol/metadata.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/protocol/offset_for_leader_epoch.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/protocol/pickle.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/protocol/produce.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/protocol/sasl_authenticate.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/protocol/sasl_handshake.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/protocol/struct.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/protocol/types.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/record/__init__.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/record/_crc32c.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/record/abc.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/record/default_records.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/record/legacy_records.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/record/memory_records.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/record/util.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/sasl/__init__.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/sasl/abc.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/sasl/gssapi.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/sasl/msk.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/sasl/oauth.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/sasl/plain.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/sasl/scram.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/sasl/sspi.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/serializer/__init__.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/serializer/abstract.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/socks5_wrapper.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/structs.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/vendor/__init__.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/vendor/enum34.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/vendor/selectors34.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/vendor/six.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka/vendor/socketpair.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka_python.egg-info/dependency_links.txt +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka_python.egg-info/requires.txt +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/kafka_python.egg-info/top_level.txt +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/setup.cfg +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/setup.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/test/test_acl_comparisons.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/test/test_admin.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/test/test_admin_integration.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/test/test_api_object_implementation.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/test/test_assignors.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/test/test_client_async.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/test/test_cluster.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/test/test_codec.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/test/test_conn.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/test/test_metrics.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/test/test_object_conversion.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/test/test_package.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/test/test_partition_movements.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/test/test_partitioner.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/test/test_protocol.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/test/test_sender.py +0 -0
- {kafka_python-2.1.2 → kafka_python-2.1.3}/test/testutil.py +0 -0
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
# 2.1.3 (Mar 25, 2025)
|
|
2
|
+
|
|
3
|
+
Fixes
|
|
4
|
+
* Fix crash when switching to closest compatible api_version in KafkaClient (#2567)
|
|
5
|
+
* Fix maximum version to send an OffsetFetchRequest in KafkaAdminClient (#2563)
|
|
6
|
+
* Return empty set from consumer.partitions_for_topic when topic not found (#2556)
|
|
7
|
+
|
|
8
|
+
Improvements
|
|
9
|
+
* KIP-511: Use ApiVersions v4 on initial connect w/ client_software_name + version (#2558)
|
|
10
|
+
* KIP-74: Manage assigned partition order in consumer (#2562)
|
|
11
|
+
* KIP-70: Auto-commit offsets on consumer.unsubscribe(), defer assignment changes to rejoin (#2560)
|
|
12
|
+
* Use SubscriptionType to track topics/pattern/user assignment (#2565)
|
|
13
|
+
* Add optional timeout_ms kwarg to consumer.close() (#2564)
|
|
14
|
+
* Move ensure_valid_topic_name to kafka.util; use in client and producer (#2561)
|
|
15
|
+
|
|
16
|
+
Testing
|
|
17
|
+
* Support KRaft / 4.0 brokers in tests (#2559)
|
|
18
|
+
* Test older pythons against 4.0 broker
|
|
19
|
+
|
|
20
|
+
Compatibility
|
|
21
|
+
* Add python 3.13 to compatibility list
|
|
22
|
+
|
|
1
23
|
# 2.1.2 (Mar 17, 2025)
|
|
2
24
|
|
|
3
25
|
Fixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: kafka-python
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.3
|
|
4
4
|
Summary: Pure Python client for Apache Kafka
|
|
5
5
|
Author-email: Dana Powers <dana.powers@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/dpkp/kafka-python
|
|
@@ -21,6 +21,7 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.10
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.11
|
|
23
23
|
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
25
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
25
26
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
26
27
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
@@ -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(
|
|
@@ -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'])
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -538,7 +544,14 @@ class BrokerConnection(object):
|
|
|
538
544
|
log.debug('%s: Using pre-configured api_version %s for ApiVersions', self, self._api_version)
|
|
539
545
|
return True
|
|
540
546
|
elif self._check_version_idx is None:
|
|
541
|
-
|
|
547
|
+
version = self._api_versions_idx
|
|
548
|
+
if version >= 3:
|
|
549
|
+
request = ApiVersionsRequest[version](
|
|
550
|
+
client_software_name=self.config['client_software_name'],
|
|
551
|
+
client_software_version=self.config['client_software_version'],
|
|
552
|
+
_tagged_fields={})
|
|
553
|
+
else:
|
|
554
|
+
request = ApiVersionsRequest[version]()
|
|
542
555
|
future = Future()
|
|
543
556
|
response = self._send(request, blocking=True, request_timeout_ms=(self.config['api_version_auto_timeout_ms'] * 0.8))
|
|
544
557
|
response.add_callback(self._handle_api_versions_response, future)
|
|
@@ -573,11 +586,15 @@ class BrokerConnection(object):
|
|
|
573
586
|
|
|
574
587
|
def _handle_api_versions_response(self, future, response):
|
|
575
588
|
error_type = Errors.for_code(response.error_code)
|
|
576
|
-
# if error_type i UNSUPPORTED_VERSION: retry w/ latest version from response
|
|
577
589
|
if error_type is not Errors.NoError:
|
|
578
590
|
future.failure(error_type())
|
|
579
591
|
if error_type is Errors.UnsupportedVersionError:
|
|
580
592
|
self._api_versions_idx -= 1
|
|
593
|
+
for api_key, min_version, max_version, *rest in response.api_versions:
|
|
594
|
+
# If broker provides a lower max_version, skip to that
|
|
595
|
+
if api_key == response.API_KEY:
|
|
596
|
+
self._api_versions_idx = min(self._api_versions_idx, max_version)
|
|
597
|
+
break
|
|
581
598
|
if self._api_versions_idx >= 0:
|
|
582
599
|
self._api_versions_future = None
|
|
583
600
|
self.state = ConnectionStates.API_VERSIONS_SEND
|
|
@@ -587,7 +604,7 @@ class BrokerConnection(object):
|
|
|
587
604
|
return
|
|
588
605
|
self._api_versions = dict([
|
|
589
606
|
(api_key, (min_version, max_version))
|
|
590
|
-
for api_key, min_version, max_version in response.api_versions
|
|
607
|
+
for api_key, min_version, max_version, *rest in response.api_versions
|
|
591
608
|
])
|
|
592
609
|
self._api_version = self._infer_broker_version_from_api_versions(self._api_versions)
|
|
593
610
|
log.info('Broker version identified as %s', '.'.join(map(str, self._api_version)))
|
|
@@ -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,
|
|
@@ -380,10 +378,13 @@ class Fetcher(six.Iterator):
|
|
|
380
378
|
# as long as the partition is still assigned
|
|
381
379
|
position = self._subscriptions.assignment[tp].position
|
|
382
380
|
if part.next_fetch_offset == position.offset:
|
|
383
|
-
part_records = part.take(max_records)
|
|
384
381
|
log.debug("Returning fetched records at offset %d for assigned"
|
|
385
382
|
" partition %s", position.offset, tp)
|
|
386
|
-
|
|
383
|
+
part_records = part.take(max_records)
|
|
384
|
+
# list.extend([]) is a noop, but because drained is a defaultdict
|
|
385
|
+
# we should avoid initializing the default list unless there are records
|
|
386
|
+
if part_records:
|
|
387
|
+
drained[tp].extend(part_records)
|
|
387
388
|
# We want to increment subscription position if (1) we're using consumer.poll(),
|
|
388
389
|
# or (2) we didn't return any records (consumer iterator will update position
|
|
389
390
|
# when each message is yielded). There may be edge cases where we re-fetch records
|
|
@@ -562,13 +563,11 @@ class Fetcher(six.Iterator):
|
|
|
562
563
|
def _fetchable_partitions(self):
|
|
563
564
|
fetchable = self._subscriptions.fetchable_partitions()
|
|
564
565
|
# do not fetch a partition if we have a pending fetch response to process
|
|
566
|
+
discard = {fetch.topic_partition for fetch in self._completed_fetches}
|
|
565
567
|
current = self._next_partition_records
|
|
566
|
-
pending = copy.copy(self._completed_fetches)
|
|
567
568
|
if current:
|
|
568
|
-
|
|
569
|
-
for
|
|
570
|
-
fetchable.discard(fetch.topic_partition)
|
|
571
|
-
return fetchable
|
|
569
|
+
discard.add(current.topic_partition)
|
|
570
|
+
return [tp for tp in fetchable if tp not in discard]
|
|
572
571
|
|
|
573
572
|
def _create_fetch_requests(self):
|
|
574
573
|
"""Create fetch requests for all assigned partitions, grouped by node.
|
|
@@ -581,7 +580,7 @@ class Fetcher(six.Iterator):
|
|
|
581
580
|
# create the fetch info as a dict of lists of partition info tuples
|
|
582
581
|
# which can be passed to FetchRequest() via .items()
|
|
583
582
|
version = self._client.api_version(FetchRequest, max_version=10)
|
|
584
|
-
fetchable = collections.defaultdict(
|
|
583
|
+
fetchable = collections.defaultdict(collections.OrderedDict)
|
|
585
584
|
|
|
586
585
|
for partition in self._fetchable_partitions():
|
|
587
586
|
node_id = self._client.cluster.leader_for_partition(partition)
|
|
@@ -695,10 +694,7 @@ class Fetcher(six.Iterator):
|
|
|
695
694
|
for partition_data in partitions])
|
|
696
695
|
metric_aggregator = FetchResponseMetricAggregator(self._sensors, partitions)
|
|
697
696
|
|
|
698
|
-
# randomized ordering should improve balance for short-lived consumers
|
|
699
|
-
random.shuffle(response.topics)
|
|
700
697
|
for topic, partitions in response.topics:
|
|
701
|
-
random.shuffle(partitions)
|
|
702
698
|
for partition_data in partitions:
|
|
703
699
|
tp = TopicPartition(topic, partition_data[0])
|
|
704
700
|
fetch_offset = fetch_offsets[tp]
|
|
@@ -733,8 +729,6 @@ class Fetcher(six.Iterator):
|
|
|
733
729
|
" since it is no longer fetchable", tp)
|
|
734
730
|
|
|
735
731
|
elif error_type is Errors.NoError:
|
|
736
|
-
self._subscriptions.assignment[tp].highwater = highwater
|
|
737
|
-
|
|
738
732
|
# we are interested in this fetch only if the beginning
|
|
739
733
|
# offset (of the *request*) matches the current consumed position
|
|
740
734
|
# Note that the *response* may return a messageset that starts
|
|
@@ -748,30 +742,35 @@ class Fetcher(six.Iterator):
|
|
|
748
742
|
return None
|
|
749
743
|
|
|
750
744
|
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
|
-
|
|
745
|
+
log.debug("Preparing to read %s bytes of data for partition %s with offset %d",
|
|
746
|
+
records.size_in_bytes(), tp, fetch_offset)
|
|
747
|
+
parsed_records = self.PartitionRecords(fetch_offset, tp, records,
|
|
748
|
+
self.config['key_deserializer'],
|
|
749
|
+
self.config['value_deserializer'],
|
|
750
|
+
self.config['check_crcs'],
|
|
751
|
+
completed_fetch.metric_aggregator,
|
|
752
|
+
self._on_partition_records_drain)
|
|
753
|
+
if not records.has_next() and records.size_in_bytes() > 0:
|
|
754
|
+
if completed_fetch.response_version < 3:
|
|
755
|
+
# Implement the pre KIP-74 behavior of throwing a RecordTooLargeException.
|
|
756
|
+
record_too_large_partitions = {tp: fetch_offset}
|
|
757
|
+
raise RecordTooLargeError(
|
|
758
|
+
"There are some messages at [Partition=Offset]: %s "
|
|
759
|
+
" whose size is larger than the fetch size %s"
|
|
760
|
+
" and hence cannot be ever returned. Please condier upgrading your broker to 0.10.1.0 or"
|
|
761
|
+
" newer to avoid this issue. Alternatively, increase the fetch size on the client (using"
|
|
762
|
+
" max_partition_fetch_bytes)" % (
|
|
763
|
+
record_too_large_partitions,
|
|
764
|
+
self.config['max_partition_fetch_bytes']),
|
|
765
|
+
record_too_large_partitions)
|
|
766
|
+
else:
|
|
767
|
+
# This should not happen with brokers that support FetchRequest/Response V3 or higher (i.e. KIP-74)
|
|
768
|
+
raise Errors.KafkaError("Failed to make progress reading messages at %s=%s."
|
|
769
|
+
" Received a non-empty fetch response from the server, but no"
|
|
770
|
+
" complete records were found." % (tp, fetch_offset))
|
|
771
|
+
|
|
772
|
+
if highwater >= 0:
|
|
773
|
+
self._subscriptions.assignment[tp].highwater = highwater
|
|
775
774
|
|
|
776
775
|
elif error_type in (Errors.NotLeaderForPartitionError,
|
|
777
776
|
Errors.ReplicaNotAvailableError,
|
|
@@ -805,14 +804,25 @@ class Fetcher(six.Iterator):
|
|
|
805
804
|
if parsed_records is None:
|
|
806
805
|
completed_fetch.metric_aggregator.record(tp, 0, 0)
|
|
807
806
|
|
|
808
|
-
|
|
807
|
+
if error_type is not Errors.NoError:
|
|
808
|
+
# we move the partition to the end if there was an error. This way, it's more likely that partitions for
|
|
809
|
+
# the same topic can remain together (allowing for more efficient serialization).
|
|
810
|
+
self._subscriptions.move_partition_to_end(tp)
|
|
811
|
+
|
|
812
|
+
return parsed_records
|
|
813
|
+
|
|
814
|
+
def _on_partition_records_drain(self, partition_records):
|
|
815
|
+
# we move the partition to the end if we received some bytes. This way, it's more likely that partitions
|
|
816
|
+
# for the same topic can remain together (allowing for more efficient serialization).
|
|
817
|
+
if partition_records.bytes_read > 0:
|
|
818
|
+
self._subscriptions.move_partition_to_end(partition_records.topic_partition)
|
|
809
819
|
|
|
810
820
|
def close(self):
|
|
811
821
|
if self._next_partition_records is not None:
|
|
812
822
|
self._next_partition_records.drain()
|
|
813
823
|
|
|
814
824
|
class PartitionRecords(object):
|
|
815
|
-
def __init__(self, fetch_offset, tp, records, key_deserializer, value_deserializer, check_crcs, metric_aggregator):
|
|
825
|
+
def __init__(self, fetch_offset, tp, records, key_deserializer, value_deserializer, check_crcs, metric_aggregator, on_drain):
|
|
816
826
|
self.fetch_offset = fetch_offset
|
|
817
827
|
self.topic_partition = tp
|
|
818
828
|
self.leader_epoch = -1
|
|
@@ -824,6 +834,7 @@ class Fetcher(six.Iterator):
|
|
|
824
834
|
self.record_iterator = itertools.dropwhile(
|
|
825
835
|
self._maybe_skip_record,
|
|
826
836
|
self._unpack_records(tp, records, key_deserializer, value_deserializer))
|
|
837
|
+
self.on_drain = on_drain
|
|
827
838
|
|
|
828
839
|
def _maybe_skip_record(self, record):
|
|
829
840
|
# When fetching an offset that is in the middle of a
|
|
@@ -845,6 +856,7 @@ class Fetcher(six.Iterator):
|
|
|
845
856
|
if self.record_iterator is not None:
|
|
846
857
|
self.record_iterator = None
|
|
847
858
|
self.metric_aggregator.record(self.topic_partition, self.bytes_read, self.records_read)
|
|
859
|
+
self.on_drain(self)
|
|
848
860
|
|
|
849
861
|
def take(self, n=None):
|
|
850
862
|
return list(itertools.islice(self.record_iterator, 0, n))
|
|
@@ -943,6 +955,13 @@ class FetchSessionHandler(object):
|
|
|
943
955
|
self.session_partitions = {}
|
|
944
956
|
|
|
945
957
|
def build_next(self, next_partitions):
|
|
958
|
+
"""
|
|
959
|
+
Arguments:
|
|
960
|
+
next_partitions (dict): TopicPartition -> TopicPartitionState
|
|
961
|
+
|
|
962
|
+
Returns:
|
|
963
|
+
FetchRequestData
|
|
964
|
+
"""
|
|
946
965
|
if self.next_metadata.is_full:
|
|
947
966
|
log.debug("Built full fetch %s for node %s with %s partition(s).",
|
|
948
967
|
self.next_metadata, self.node_id, len(next_partitions))
|
|
@@ -965,8 +984,8 @@ class FetchSessionHandler(object):
|
|
|
965
984
|
altered.add(tp)
|
|
966
985
|
|
|
967
986
|
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)}
|
|
987
|
+
self.next_metadata, self.node_id, added, altered, removed, self.session_partitions.keys())
|
|
988
|
+
to_send = collections.OrderedDict({tp: next_partitions[tp] for tp in next_partitions if tp in (added | altered)})
|
|
970
989
|
return FetchRequestData(to_send, removed, self.next_metadata)
|
|
971
990
|
|
|
972
991
|
def handle_response(self, response):
|
|
@@ -1106,18 +1125,11 @@ class FetchRequestData(object):
|
|
|
1106
1125
|
@property
|
|
1107
1126
|
def to_send(self):
|
|
1108
1127
|
# Return as list of [(topic, [(partition, ...), ...]), ...]
|
|
1109
|
-
# so it
|
|
1128
|
+
# so it can be passed directly to encoder
|
|
1110
1129
|
partition_data = collections.defaultdict(list)
|
|
1111
1130
|
for tp, partition_info in six.iteritems(self._to_send):
|
|
1112
1131
|
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))
|
|
1132
|
+
return list(partition_data.items())
|
|
1121
1133
|
|
|
1122
1134
|
@property
|
|
1123
1135
|
def to_forget(self):
|
|
@@ -444,8 +444,15 @@ class KafkaConsumer(six.Iterator):
|
|
|
444
444
|
no rebalance operation triggered when group membership or cluster
|
|
445
445
|
and topic metadata change.
|
|
446
446
|
"""
|
|
447
|
-
|
|
448
|
-
|
|
447
|
+
if not partitions:
|
|
448
|
+
self.unsubscribe()
|
|
449
|
+
else:
|
|
450
|
+
# make sure the offsets of topic partitions the consumer is unsubscribing from
|
|
451
|
+
# are committed since there will be no following rebalance
|
|
452
|
+
self._coordinator.maybe_auto_commit_offsets_now()
|
|
453
|
+
self._subscription.assign_from_user(partitions)
|
|
454
|
+
self._client.set_topics([tp.topic for tp in partitions])
|
|
455
|
+
log.debug("Subscribed to partition(s): %s", partitions)
|
|
449
456
|
|
|
450
457
|
def assignment(self):
|
|
451
458
|
"""Get the TopicPartitions currently assigned to this consumer.
|
|
@@ -463,19 +470,21 @@ class KafkaConsumer(six.Iterator):
|
|
|
463
470
|
"""
|
|
464
471
|
return self._subscription.assigned_partitions()
|
|
465
472
|
|
|
466
|
-
def close(self, autocommit=True):
|
|
473
|
+
def close(self, autocommit=True, timeout_ms=None):
|
|
467
474
|
"""Close the consumer, waiting indefinitely for any needed cleanup.
|
|
468
475
|
|
|
469
476
|
Keyword Arguments:
|
|
470
477
|
autocommit (bool): If auto-commit is configured for this consumer,
|
|
471
478
|
this optional flag causes the consumer to attempt to commit any
|
|
472
479
|
pending consumed offsets prior to close. Default: True
|
|
480
|
+
timeout_ms (num, optional): Milliseconds to wait for auto-commit.
|
|
481
|
+
Default: None
|
|
473
482
|
"""
|
|
474
483
|
if self._closed:
|
|
475
484
|
return
|
|
476
485
|
log.debug("Closing the KafkaConsumer.")
|
|
477
486
|
self._closed = True
|
|
478
|
-
self._coordinator.close(autocommit=autocommit)
|
|
487
|
+
self._coordinator.close(autocommit=autocommit, timeout_ms=timeout_ms)
|
|
479
488
|
self._metrics.close()
|
|
480
489
|
self._client.close()
|
|
481
490
|
try:
|
|
@@ -634,7 +643,7 @@ class KafkaConsumer(six.Iterator):
|
|
|
634
643
|
if partitions is None:
|
|
635
644
|
self._fetch_all_topic_metadata()
|
|
636
645
|
partitions = cluster.partitions_for_topic(topic)
|
|
637
|
-
return partitions
|
|
646
|
+
return partitions or set()
|
|
638
647
|
|
|
639
648
|
def poll(self, timeout_ms=0, max_records=None, update_offsets=True):
|
|
640
649
|
"""Fetch data from assigned topics / partitions.
|
|
@@ -959,8 +968,11 @@ class KafkaConsumer(six.Iterator):
|
|
|
959
968
|
|
|
960
969
|
def unsubscribe(self):
|
|
961
970
|
"""Unsubscribe from all topics and clear all assigned partitions."""
|
|
971
|
+
# make sure the offsets of topic partitions the consumer is unsubscribing from
|
|
972
|
+
# are committed since there will be no following rebalance
|
|
973
|
+
self._coordinator.maybe_auto_commit_offsets_now()
|
|
962
974
|
self._subscription.unsubscribe()
|
|
963
|
-
self._coordinator.
|
|
975
|
+
self._coordinator.maybe_leave_group()
|
|
964
976
|
self._client.cluster.need_all_topic_metadata = False
|
|
965
977
|
self._client.set_topics([])
|
|
966
978
|
log.debug("Unsubscribed all topics or patterns and assigned partitions")
|