kafka-python 2.0.4__tar.gz → 2.0.6__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.
Files changed (138) hide show
  1. {kafka_python-2.0.4 → kafka_python-2.0.6}/CHANGES.md +44 -0
  2. {kafka_python-2.0.4 → kafka_python-2.0.6}/PKG-INFO +2 -2
  3. {kafka_python-2.0.4 → kafka_python-2.0.6}/README.rst +1 -1
  4. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/admin/client.py +30 -18
  5. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/client_async.py +72 -53
  6. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/conn.py +2 -2
  7. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/consumer/fetcher.py +12 -2
  8. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/coordinator/base.py +4 -3
  9. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/coordinator/consumer.py +4 -1
  10. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/record/default_records.py +45 -2
  11. kafka_python-2.0.6/kafka/version.py +1 -0
  12. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka_python.egg-info/PKG-INFO +2 -2
  13. {kafka_python-2.0.4 → kafka_python-2.0.6}/test/test_admin_integration.py +6 -3
  14. {kafka_python-2.0.4 → kafka_python-2.0.6}/test/test_client_async.py +13 -19
  15. {kafka_python-2.0.4 → kafka_python-2.0.6}/test/test_conn.py +1 -1
  16. {kafka_python-2.0.4 → kafka_python-2.0.6}/test/test_consumer_integration.py +1 -1
  17. {kafka_python-2.0.4 → kafka_python-2.0.6}/test/test_sasl_integration.py +13 -7
  18. {kafka_python-2.0.4 → kafka_python-2.0.6}/test/testutil.py +2 -2
  19. kafka_python-2.0.4/kafka/version.py +0 -1
  20. {kafka_python-2.0.4 → kafka_python-2.0.6}/AUTHORS.md +0 -0
  21. {kafka_python-2.0.4 → kafka_python-2.0.6}/LICENSE +0 -0
  22. {kafka_python-2.0.4 → kafka_python-2.0.6}/MANIFEST.in +0 -0
  23. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/__init__.py +0 -0
  24. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/admin/__init__.py +0 -0
  25. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/admin/acl_resource.py +0 -0
  26. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/admin/config_resource.py +0 -0
  27. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/admin/new_partitions.py +0 -0
  28. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/admin/new_topic.py +0 -0
  29. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/cluster.py +0 -0
  30. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/codec.py +0 -0
  31. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/consumer/__init__.py +0 -0
  32. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/consumer/group.py +0 -0
  33. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/consumer/subscription_state.py +0 -0
  34. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/coordinator/__init__.py +0 -0
  35. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/coordinator/assignors/__init__.py +0 -0
  36. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/coordinator/assignors/abstract.py +0 -0
  37. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/coordinator/assignors/range.py +0 -0
  38. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/coordinator/assignors/roundrobin.py +0 -0
  39. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/coordinator/assignors/sticky/__init__.py +0 -0
  40. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/coordinator/assignors/sticky/partition_movements.py +0 -0
  41. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/coordinator/assignors/sticky/sorted_set.py +0 -0
  42. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/coordinator/assignors/sticky/sticky_assignor.py +0 -0
  43. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/coordinator/heartbeat.py +0 -0
  44. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/coordinator/protocol.py +0 -0
  45. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/errors.py +0 -0
  46. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/future.py +0 -0
  47. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/metrics/__init__.py +0 -0
  48. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/metrics/compound_stat.py +0 -0
  49. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/metrics/dict_reporter.py +0 -0
  50. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/metrics/kafka_metric.py +0 -0
  51. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/metrics/measurable.py +0 -0
  52. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/metrics/measurable_stat.py +0 -0
  53. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/metrics/metric_config.py +0 -0
  54. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/metrics/metric_name.py +0 -0
  55. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/metrics/metrics.py +0 -0
  56. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/metrics/metrics_reporter.py +0 -0
  57. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/metrics/quota.py +0 -0
  58. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/metrics/stat.py +0 -0
  59. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/metrics/stats/__init__.py +0 -0
  60. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/metrics/stats/avg.py +0 -0
  61. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/metrics/stats/count.py +0 -0
  62. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/metrics/stats/histogram.py +0 -0
  63. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/metrics/stats/max_stat.py +0 -0
  64. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/metrics/stats/min_stat.py +0 -0
  65. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/metrics/stats/percentile.py +0 -0
  66. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/metrics/stats/percentiles.py +0 -0
  67. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/metrics/stats/rate.py +0 -0
  68. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/metrics/stats/sampled_stat.py +0 -0
  69. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/metrics/stats/sensor.py +0 -0
  70. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/metrics/stats/total.py +0 -0
  71. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/oauth/__init__.py +0 -0
  72. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/oauth/abstract.py +0 -0
  73. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/partitioner/__init__.py +0 -0
  74. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/partitioner/default.py +0 -0
  75. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/producer/__init__.py +0 -0
  76. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/producer/buffer.py +0 -0
  77. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/producer/future.py +0 -0
  78. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/producer/kafka.py +0 -0
  79. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/producer/record_accumulator.py +0 -0
  80. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/producer/sender.py +0 -0
  81. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/protocol/__init__.py +0 -0
  82. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/protocol/abstract.py +0 -0
  83. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/protocol/admin.py +0 -0
  84. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/protocol/api.py +0 -0
  85. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/protocol/commit.py +0 -0
  86. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/protocol/fetch.py +0 -0
  87. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/protocol/frame.py +0 -0
  88. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/protocol/group.py +0 -0
  89. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/protocol/message.py +0 -0
  90. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/protocol/metadata.py +0 -0
  91. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/protocol/offset.py +0 -0
  92. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/protocol/parser.py +0 -0
  93. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/protocol/pickle.py +0 -0
  94. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/protocol/produce.py +0 -0
  95. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/protocol/struct.py +0 -0
  96. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/protocol/types.py +0 -0
  97. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/record/__init__.py +0 -0
  98. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/record/_crc32c.py +0 -0
  99. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/record/abc.py +0 -0
  100. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/record/legacy_records.py +0 -0
  101. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/record/memory_records.py +0 -0
  102. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/record/util.py +0 -0
  103. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/scram.py +0 -0
  104. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/serializer/__init__.py +0 -0
  105. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/serializer/abstract.py +0 -0
  106. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/structs.py +0 -0
  107. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/util.py +0 -0
  108. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/vendor/__init__.py +0 -0
  109. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/vendor/enum34.py +0 -0
  110. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/vendor/selectors34.py +0 -0
  111. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/vendor/six.py +0 -0
  112. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka/vendor/socketpair.py +0 -0
  113. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka_python.egg-info/SOURCES.txt +0 -0
  114. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka_python.egg-info/dependency_links.txt +0 -0
  115. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka_python.egg-info/requires.txt +0 -0
  116. {kafka_python-2.0.4 → kafka_python-2.0.6}/kafka_python.egg-info/top_level.txt +0 -0
  117. {kafka_python-2.0.4 → kafka_python-2.0.6}/pyproject.toml +0 -0
  118. {kafka_python-2.0.4 → kafka_python-2.0.6}/setup.cfg +0 -0
  119. {kafka_python-2.0.4 → kafka_python-2.0.6}/setup.py +0 -0
  120. {kafka_python-2.0.4 → kafka_python-2.0.6}/test/test_acl_comparisons.py +0 -0
  121. {kafka_python-2.0.4 → kafka_python-2.0.6}/test/test_admin.py +0 -0
  122. {kafka_python-2.0.4 → kafka_python-2.0.6}/test/test_api_object_implementation.py +0 -0
  123. {kafka_python-2.0.4 → kafka_python-2.0.6}/test/test_assignors.py +0 -0
  124. {kafka_python-2.0.4 → kafka_python-2.0.6}/test/test_cluster.py +0 -0
  125. {kafka_python-2.0.4 → kafka_python-2.0.6}/test/test_codec.py +0 -0
  126. {kafka_python-2.0.4 → kafka_python-2.0.6}/test/test_consumer.py +0 -0
  127. {kafka_python-2.0.4 → kafka_python-2.0.6}/test/test_consumer_group.py +0 -0
  128. {kafka_python-2.0.4 → kafka_python-2.0.6}/test/test_coordinator.py +0 -0
  129. {kafka_python-2.0.4 → kafka_python-2.0.6}/test/test_fetcher.py +0 -0
  130. {kafka_python-2.0.4 → kafka_python-2.0.6}/test/test_metrics.py +0 -0
  131. {kafka_python-2.0.4 → kafka_python-2.0.6}/test/test_object_conversion.py +0 -0
  132. {kafka_python-2.0.4 → kafka_python-2.0.6}/test/test_package.py +0 -0
  133. {kafka_python-2.0.4 → kafka_python-2.0.6}/test/test_partition_movements.py +0 -0
  134. {kafka_python-2.0.4 → kafka_python-2.0.6}/test/test_partitioner.py +0 -0
  135. {kafka_python-2.0.4 → kafka_python-2.0.6}/test/test_producer.py +0 -0
  136. {kafka_python-2.0.4 → kafka_python-2.0.6}/test/test_protocol.py +0 -0
  137. {kafka_python-2.0.4 → kafka_python-2.0.6}/test/test_sender.py +0 -0
  138. {kafka_python-2.0.4 → kafka_python-2.0.6}/test/test_subscription_state.py +0 -0
@@ -1,3 +1,47 @@
1
+ # 2.0.6 (Mar 4, 2025)
2
+
3
+ Networking
4
+ * Improve error handling in `client._maybe_connect` (#2504)
5
+ * Client connection / `maybe_refresh_metadata` changes (#2507)
6
+ * Improve too-large timeout handling in client poll
7
+ * Default `client.check_version` timeout to `api_version_auto_timeout_ms` (#2496)
8
+
9
+ Fixes
10
+ * Decode and skip transactional control records in consumer (#2499)
11
+ * try / except in consumer coordinator `__del__`
12
+
13
+ Testing
14
+ * test_conn fixup for py2
15
+
16
+ Project Maintenance
17
+ * Add 2.0 branch for backports
18
+
19
+ # 2.0.5 (Feb 25, 2025)
20
+
21
+ Networking
22
+ * Remove unused client bootstrap backoff code
23
+ * 200ms timeout for client.poll in ensure_active_group and admin client
24
+
25
+ Fixes
26
+ * Admin client: check_version only if needed, use node_id kwarg for controller
27
+ * Check for -1 controller_id in admin client
28
+ * Only acquire coordinator lock in heartbeat thread close if not self thread
29
+
30
+ Testing
31
+ * Also sleep when waiting for consumers in test_describe_consumer_group_exists
32
+ * Refactor sasl_integration test_client - wait for node ready; use send future
33
+ * Add timeout to test_kafka_consumer
34
+ * Add error str to assert_message_count checks
35
+ * Retry on error in test fixture create_topic_via_metadata
36
+ * Fixup variable interpolation in test fixture error
37
+
38
+ Documentation
39
+ * Update compatibility docs
40
+ * Include client_id in BrokerConnection __str__ output
41
+
42
+ Project Maintenance
43
+ * Add make targets `servers/*/api_versions` and `servers/*/messages`
44
+
1
45
  # 2.0.4 (Feb 21, 2025)
2
46
 
3
47
  Networking
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: kafka-python
3
- Version: 2.0.4
3
+ Version: 2.0.6
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
@@ -41,7 +41,7 @@ Requires-Dist: pytest-mock; extra == "testing"
41
41
  Kafka Python client
42
42
  ------------------------
43
43
 
44
- .. image:: https://img.shields.io/badge/kafka-2.6%2C%202.5%2C%202.4%2C%202.3%2C%202.2%2C%202.1%2C%202.0%2C%201.1%2C%201.0%2C%200.11%2C%200.10%2C%200.9%2C%200.8-brightgreen.svg
44
+ .. image:: https://img.shields.io/badge/kafka-3.9--0.8-brightgreen.svg
45
45
  :target: https://kafka-python.readthedocs.io/en/master/compatibility.html
46
46
  .. image:: https://img.shields.io/pypi/pyversions/kafka-python.svg
47
47
  :target: https://pypi.python.org/pypi/kafka-python
@@ -1,7 +1,7 @@
1
1
  Kafka Python client
2
2
  ------------------------
3
3
 
4
- .. image:: https://img.shields.io/badge/kafka-2.6%2C%202.5%2C%202.4%2C%202.3%2C%202.2%2C%202.1%2C%202.0%2C%201.1%2C%201.0%2C%200.11%2C%200.10%2C%200.9%2C%200.8-brightgreen.svg
4
+ .. image:: https://img.shields.io/badge/kafka-3.9--0.8-brightgreen.svg
5
5
  :target: https://kafka-python.readthedocs.io/en/master/compatibility.html
6
6
  .. image:: https://img.shields.io/pypi/pyversions/kafka-python.svg
7
7
  :target: https://pypi.python.org/pypi/kafka-python
@@ -1,9 +1,10 @@
1
- from __future__ import absolute_import
1
+ from __future__ import absolute_import, division
2
2
 
3
3
  from collections import defaultdict
4
4
  import copy
5
5
  import logging
6
6
  import socket
7
+ import time
7
8
 
8
9
  from . import ConfigResourceType
9
10
  from kafka.vendor import six
@@ -212,11 +213,13 @@ class KafkaAdminClient(object):
212
213
  metric_group_prefix='admin',
213
214
  **self.config
214
215
  )
215
- self._client.check_version(timeout=(self.config['api_version_auto_timeout_ms'] / 1000))
216
216
 
217
217
  # Get auto-discovered version from client if necessary
218
218
  if self.config['api_version'] is None:
219
219
  self.config['api_version'] = self._client.config['api_version']
220
+ else:
221
+ # need to run check_version for get_api_versions()
222
+ self._client.check_version(timeout=(self.config['api_version_auto_timeout_ms'] / 1000))
220
223
 
221
224
  self._closed = False
222
225
  self._refresh_controller_id()
@@ -273,24 +276,33 @@ class KafkaAdminClient(object):
273
276
  """
274
277
  return timeout_ms or self.config['request_timeout_ms']
275
278
 
276
- def _refresh_controller_id(self):
279
+ def _refresh_controller_id(self, timeout_ms=30000):
277
280
  """Determine the Kafka cluster controller."""
278
281
  version = self._matching_api_version(MetadataRequest)
279
282
  if 1 <= version <= 6:
280
- request = MetadataRequest[version]()
281
- future = self._send_request_to_node(self._client.least_loaded_node(), request)
282
-
283
- self._wait_for_futures([future])
284
-
285
- response = future.value
286
- controller_id = response.controller_id
287
- # verify the controller is new enough to support our requests
288
- controller_version = self._client.check_version(controller_id, timeout=(self.config['api_version_auto_timeout_ms'] / 1000))
289
- if controller_version < (0, 10, 0):
290
- raise IncompatibleBrokerVersion(
291
- "The controller appears to be running Kafka {}. KafkaAdminClient requires brokers >= 0.10.0.0."
292
- .format(controller_version))
293
- self._controller_id = controller_id
283
+ timeout_at = time.time() + timeout_ms / 1000
284
+ while time.time() < timeout_at:
285
+ request = MetadataRequest[version]()
286
+ future = self._send_request_to_node(self._client.least_loaded_node(), request)
287
+
288
+ self._wait_for_futures([future])
289
+
290
+ response = future.value
291
+ controller_id = response.controller_id
292
+ if controller_id == -1:
293
+ log.warning("Controller ID not available, got -1")
294
+ time.sleep(1)
295
+ continue
296
+ # verify the controller is new enough to support our requests
297
+ controller_version = self._client.check_version(node_id=controller_id)
298
+ if controller_version < (0, 10, 0):
299
+ raise IncompatibleBrokerVersion(
300
+ "The controller appears to be running Kafka {}. KafkaAdminClient requires brokers >= 0.10.0.0."
301
+ .format(controller_version))
302
+ self._controller_id = controller_id
303
+ return
304
+ else:
305
+ raise Errors.NodeNotAvailableError('controller')
294
306
  else:
295
307
  raise UnrecognizedBrokerVersion(
296
308
  "Kafka Admin interface cannot determine the controller using MetadataRequest_v{}."
@@ -390,7 +402,7 @@ class KafkaAdminClient(object):
390
402
  while not self._client.ready(node_id):
391
403
  # poll until the connection to broker is ready, otherwise send()
392
404
  # will fail with NodeNotReadyError
393
- self._client.poll()
405
+ self._client.poll(timeout_ms=200)
394
406
  return self._client.send(node_id, request, wakeup)
395
407
 
396
408
  def _send_request_to_controller(self, request):
@@ -216,6 +216,8 @@ class KafkaClient(object):
216
216
  self._connecting = set()
217
217
  self._sending = set()
218
218
  self._refresh_on_disconnects = True
219
+
220
+ # Not currently used, but data is collected internally
219
221
  self._last_bootstrap = 0
220
222
  self._bootstrap_fails = 0
221
223
 
@@ -233,12 +235,9 @@ class KafkaClient(object):
233
235
  self.config['metric_group_prefix'],
234
236
  weakref.proxy(self._conns))
235
237
 
236
- self._num_bootstrap_hosts = len(collect_hosts(self.config['bootstrap_servers']))
237
-
238
238
  # Check Broker Version if not set explicitly
239
239
  if self.config['api_version'] is None:
240
- check_timeout = self.config['api_version_auto_timeout_ms'] / 1000
241
- self.config['api_version'] = self.check_version(timeout=check_timeout)
240
+ self.config['api_version'] = self.check_version()
242
241
 
243
242
  def _init_wakeup_socketpair(self):
244
243
  self._wake_r, self._wake_w = socket.socketpair()
@@ -259,20 +258,6 @@ class KafkaClient(object):
259
258
  self._wake_r = None
260
259
  self._wake_w = None
261
260
 
262
- def _can_bootstrap(self):
263
- effective_failures = self._bootstrap_fails // self._num_bootstrap_hosts
264
- backoff_factor = 2 ** effective_failures
265
- backoff_ms = min(self.config['reconnect_backoff_ms'] * backoff_factor,
266
- self.config['reconnect_backoff_max_ms'])
267
-
268
- backoff_ms *= random.uniform(0.8, 1.2)
269
-
270
- next_at = self._last_bootstrap + backoff_ms / 1000.0
271
- now = time.time()
272
- if next_at > now:
273
- return False
274
- return True
275
-
276
261
  def _can_connect(self, node_id):
277
262
  if node_id not in self._conns:
278
263
  if self.cluster.broker_metadata(node_id):
@@ -378,14 +363,24 @@ class KafkaClient(object):
378
363
 
379
364
  return False
380
365
 
381
- def _maybe_connect(self, node_id):
382
- """Idempotent non-blocking connection attempt to the given node id."""
366
+ def _init_connect(self, node_id):
367
+ """Idempotent non-blocking connection attempt to the given node id.
368
+
369
+ Returns True if connection object exists and is connected / connecting
370
+ """
383
371
  with self._lock:
384
372
  conn = self._conns.get(node_id)
385
373
 
374
+ # Check if existing connection should be recreated because host/port changed
375
+ if conn is not None and self._should_recycle_connection(conn):
376
+ self._conns.pop(node_id).close()
377
+ conn = None
378
+
386
379
  if conn is None:
387
380
  broker = self.cluster.broker_metadata(node_id)
388
- assert broker, 'Broker id %s not in current metadata' % (node_id,)
381
+ if broker is None:
382
+ log.debug('Broker id %s not in current metadata', node_id)
383
+ return False
389
384
 
390
385
  log.debug("Initiating connection to node %s at %s:%s",
391
386
  node_id, broker.host, broker.port)
@@ -397,16 +392,9 @@ class KafkaClient(object):
397
392
  **self.config)
398
393
  self._conns[node_id] = conn
399
394
 
400
- # Check if existing connection should be recreated because host/port changed
401
- elif self._should_recycle_connection(conn):
402
- self._conns.pop(node_id)
403
- return False
404
-
405
- elif conn.connected():
406
- return True
407
-
408
- conn.connect()
409
- return conn.connected()
395
+ if conn.disconnected():
396
+ conn.connect()
397
+ return not conn.disconnected()
410
398
 
411
399
  def ready(self, node_id, metadata_priority=True):
412
400
  """Check whether a node is connected and ok to send more requests.
@@ -590,12 +578,18 @@ class KafkaClient(object):
590
578
  if self._closed:
591
579
  break
592
580
 
593
- # Send a metadata request if needed (or initiate new connection)
594
- metadata_timeout_ms = self._maybe_refresh_metadata()
595
-
596
581
  # Attempt to complete pending connections
597
582
  for node_id in list(self._connecting):
598
- self._maybe_connect(node_id)
583
+ # False return means no more connection progress is possible
584
+ # Connected nodes will update _connecting via state_change callback
585
+ if not self._init_connect(node_id):
586
+ # It's possible that the connection attempt triggered a state change
587
+ # but if not, make sure to remove from _connecting list
588
+ if node_id in self._connecting:
589
+ self._connecting.remove(node_id)
590
+
591
+ # Send a metadata request if needed (or initiate new connection)
592
+ metadata_timeout_ms = self._maybe_refresh_metadata()
599
593
 
600
594
  # If we got a future that is already done, don't block in _poll
601
595
  if future is not None and future.is_done:
@@ -637,6 +631,11 @@ class KafkaClient(object):
637
631
  self._selector.register(conn._sock, selectors.EVENT_WRITE, conn)
638
632
 
639
633
  def _poll(self, timeout):
634
+ # Python throws OverflowError if timeout is > 2147483647 milliseconds
635
+ # (though the param to selector.select is in seconds)
636
+ # so convert any too-large timeout to blocking
637
+ if timeout > 2147483:
638
+ timeout = None
640
639
  # This needs to be locked, but since it is only called from within the
641
640
  # locked section of poll(), there is no additional lock acquisition here
642
641
  processed = set()
@@ -857,6 +856,26 @@ class KafkaClient(object):
857
856
  log.debug("Give up sending metadata request since no node is available. (reconnect delay %d ms)", next_connect_ms)
858
857
  return next_connect_ms
859
858
 
859
+ if not self._can_send_request(node_id):
860
+ # If there's any connection establishment underway, wait until it completes. This prevents
861
+ # the client from unnecessarily connecting to additional nodes while a previous connection
862
+ # attempt has not been completed.
863
+ if self._connecting:
864
+ return float('inf')
865
+
866
+ elif self._can_connect(node_id):
867
+ log.debug("Initializing connection to node %s for metadata request", node_id)
868
+ self._connecting.add(node_id)
869
+ if not self._init_connect(node_id):
870
+ if node_id in self._connecting:
871
+ self._connecting.remove(node_id)
872
+ # Connection attempt failed immediately, need to retry with a different node
873
+ return self.config['reconnect_backoff_ms']
874
+ else:
875
+ # Existing connection with max in flight requests. Wait for request to complete.
876
+ return self.config['request_timeout_ms']
877
+
878
+ # Recheck node_id in case we were able to connect immediately above
860
879
  if self._can_send_request(node_id):
861
880
  topics = list(self._topics)
862
881
  if not topics and self.cluster.is_bootstrap(node_id):
@@ -878,20 +897,11 @@ class KafkaClient(object):
878
897
  future.add_errback(refresh_done)
879
898
  return self.config['request_timeout_ms']
880
899
 
881
- # If there's any connection establishment underway, wait until it completes. This prevents
882
- # the client from unnecessarily connecting to additional nodes while a previous connection
883
- # attempt has not been completed.
900
+ # Should only get here if still connecting
884
901
  if self._connecting:
885
902
  return float('inf')
886
-
887
- if self.maybe_connect(node_id, wakeup=wakeup):
888
- log.debug("Initializing connection to node %s for metadata request", node_id)
889
- return float('inf')
890
-
891
- # connected but can't send more, OR connecting
892
- # In either case we just need to wait for a network event
893
- # to let us know the selected connection might be usable again.
894
- return float('inf')
903
+ else:
904
+ return self.config['reconnect_backoff_ms']
895
905
 
896
906
  def get_api_versions(self):
897
907
  """Return the ApiVersions map, if available.
@@ -904,13 +914,16 @@ class KafkaClient(object):
904
914
  """
905
915
  return self._api_versions
906
916
 
907
- def check_version(self, node_id=None, timeout=2, strict=False):
917
+ def check_version(self, node_id=None, timeout=None, strict=False):
908
918
  """Attempt to guess the version of a Kafka broker.
909
919
 
910
- Note: It is possible that this method blocks longer than the
911
- specified timeout. This can happen if the entire cluster
912
- is down and the client enters a bootstrap backoff sleep.
913
- This is only possible if node_id is None.
920
+ Keyword Arguments:
921
+ node_id (str, optional): Broker node id from cluster metadata. If None, attempts
922
+ to connect to any available broker until version is identified.
923
+ Default: None
924
+ timeout (num, optional): Maximum time in seconds to try to check broker version.
925
+ If unable to identify version before timeout, raise error (see below).
926
+ Default: api_version_auto_timeout_ms / 1000
914
927
 
915
928
  Returns: version tuple, i.e. (0, 10), (0, 9), (0, 8, 2), ...
916
929
 
@@ -920,6 +933,7 @@ class KafkaClient(object):
920
933
  UnrecognizedBrokerVersion: please file bug if seen!
921
934
  AssertionError (if strict=True): please file bug if seen!
922
935
  """
936
+ timeout = timeout or (self.config['api_version_auto_timeout_ms'] / 1000)
923
937
  self._lock.acquire()
924
938
  end = time.time() + timeout
925
939
  while time.time() < end:
@@ -930,7 +944,12 @@ class KafkaClient(object):
930
944
  if try_node is None:
931
945
  self._lock.release()
932
946
  raise Errors.NoBrokersAvailable()
933
- self._maybe_connect(try_node)
947
+ if not self._init_connect(try_node):
948
+ if try_node == node_id:
949
+ raise Errors.NodeNotReadyError("Connection failed to %s" % node_id)
950
+ else:
951
+ continue
952
+
934
953
  conn = self._conns[try_node]
935
954
 
936
955
  # We will intentionally cause socket failures
@@ -1315,8 +1315,8 @@ class BrokerConnection(object):
1315
1315
  return version
1316
1316
 
1317
1317
  def __str__(self):
1318
- return "<BrokerConnection node_id=%s host=%s:%d %s [%s %s]>" % (
1319
- self.node_id, self.host, self.port, self.state,
1318
+ return "<BrokerConnection client_id=%s, node_id=%s host=%s:%d %s [%s %s]>" % (
1319
+ self.config['client_id'], self.node_id, self.host, self.port, self.state,
1320
1320
  AFI_NAMES[self._sock_afi], self._sock_addr)
1321
1321
 
1322
1322
 
@@ -457,10 +457,20 @@ class Fetcher(six.Iterator):
457
457
  batch = records.next_batch()
458
458
  while batch is not None:
459
459
 
460
- # LegacyRecordBatch cannot access either base_offset or last_offset_delta
460
+ # Try DefaultsRecordBatch / message log format v2
461
+ # base_offset, last_offset_delta, and control batches
461
462
  try:
462
463
  self._subscriptions.assignment[tp].last_offset_from_message_batch = batch.base_offset + \
463
464
  batch.last_offset_delta
465
+ # Control batches have a single record indicating whether a transaction
466
+ # was aborted or committed.
467
+ # When isolation_level is READ_COMMITTED (currently unsupported)
468
+ # we should also skip all messages from aborted transactions
469
+ # For now we only support READ_UNCOMMITTED and so we ignore the
470
+ # abort/commit signal.
471
+ if batch.is_control_batch:
472
+ batch = records.next_batch()
473
+ continue
464
474
  except AttributeError:
465
475
  pass
466
476
 
@@ -677,7 +687,7 @@ class Fetcher(six.Iterator):
677
687
  if next_offset_from_batch_header > self._subscriptions.assignment[partition].position:
678
688
  log.debug(
679
689
  "Advance position for partition %s from %s to %s (last message batch location plus one)"
680
- " to correct for deleted compacted messages",
690
+ " to correct for deleted compacted messages and/or transactional control records",
681
691
  partition, self._subscriptions.assignment[partition].position, next_offset_from_batch_header)
682
692
  self._subscriptions.assignment[partition].position = next_offset_from_batch_header
683
693
 
@@ -371,7 +371,7 @@ class BaseCoordinator(object):
371
371
  while not self.coordinator_unknown():
372
372
  if not self._client.in_flight_request_count(self.coordinator_id):
373
373
  break
374
- self._client.poll()
374
+ self._client.poll(timeout_ms=200)
375
375
  else:
376
376
  continue
377
377
 
@@ -923,8 +923,6 @@ class HeartbeatThread(threading.Thread):
923
923
  if self.closed:
924
924
  return
925
925
  self.closed = True
926
- with self.coordinator._lock:
927
- self.coordinator._lock.notify()
928
926
 
929
927
  # Generally this should not happen - close() is triggered
930
928
  # by the coordinator. But in some cases GC may close the coordinator
@@ -932,6 +930,9 @@ class HeartbeatThread(threading.Thread):
932
930
  if threading.current_thread() == self:
933
931
  return
934
932
 
933
+ with self.coordinator._lock:
934
+ self.coordinator._lock.notify()
935
+
935
936
  if self.is_alive():
936
937
  self.join(self.coordinator.config['heartbeat_interval_ms'] / 1000)
937
938
  if self.is_alive():
@@ -128,7 +128,10 @@ class ConsumerCoordinator(BaseCoordinator):
128
128
 
129
129
  def __del__(self):
130
130
  if hasattr(self, '_cluster') and self._cluster:
131
- self._cluster.remove_listener(WeakMethod(self._handle_metadata_update))
131
+ try:
132
+ self._cluster.remove_listener(WeakMethod(self._handle_metadata_update))
133
+ except TypeError:
134
+ pass
132
135
  super(ConsumerCoordinator, self).__del__()
133
136
 
134
137
  def protocol_type(self):
@@ -269,8 +269,12 @@ class DefaultRecordBatch(DefaultRecordBase, ABCRecordBatch):
269
269
  "payload, but instead read {}".format(length, pos - start_pos))
270
270
  self._pos = pos
271
271
 
272
- return DefaultRecord(
273
- offset, timestamp, self.timestamp_type, key, value, headers)
272
+ if self.is_control_batch:
273
+ return ControlRecord(
274
+ offset, timestamp, self.timestamp_type, key, value, headers)
275
+ else:
276
+ return DefaultRecord(
277
+ offset, timestamp, self.timestamp_type, key, value, headers)
274
278
 
275
279
  def __iter__(self):
276
280
  self._maybe_uncompress()
@@ -362,6 +366,45 @@ class DefaultRecord(ABCRecord):
362
366
  )
363
367
 
364
368
 
369
+ class ControlRecord(DefaultRecord):
370
+ __slots__ = ("_offset", "_timestamp", "_timestamp_type", "_key", "_value",
371
+ "_headers", "_version", "_type")
372
+
373
+ KEY_STRUCT = struct.Struct(
374
+ ">h" # Current Version => Int16
375
+ "h" # Type => Int16 (0 indicates an abort marker, 1 indicates a commit)
376
+ )
377
+
378
+ def __init__(self, offset, timestamp, timestamp_type, key, value, headers):
379
+ super(ControlRecord, self).__init__(offset, timestamp, timestamp_type, key, value, headers)
380
+ (self._version, self._type) = self.KEY_STRUCT.unpack(self._key)
381
+
382
+ # see https://kafka.apache.org/documentation/#controlbatch
383
+ @property
384
+ def version(self):
385
+ return self._version
386
+
387
+ @property
388
+ def type(self):
389
+ return self._type
390
+
391
+ @property
392
+ def abort(self):
393
+ return self._type == 0
394
+
395
+ @property
396
+ def commit(self):
397
+ return self._type == 1
398
+
399
+ def __repr__(self):
400
+ return (
401
+ "ControlRecord(offset={!r}, timestamp={!r}, timestamp_type={!r},"
402
+ " version={!r}, type={!r} <{!s}>)".format(
403
+ self._offset, self._timestamp, self._timestamp_type,
404
+ self._version, self._type, "abort" if self.abort else "commit")
405
+ )
406
+
407
+
365
408
  class DefaultRecordBatchBuilder(DefaultRecordBase, ABCRecordBatchBuilder):
366
409
 
367
410
  # excluding key, value and headers:
@@ -0,0 +1 @@
1
+ __version__ = '2.0.6'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: kafka-python
3
- Version: 2.0.4
3
+ Version: 2.0.6
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
@@ -41,7 +41,7 @@ Requires-Dist: pytest-mock; extra == "testing"
41
41
  Kafka Python client
42
42
  ------------------------
43
43
 
44
- .. image:: https://img.shields.io/badge/kafka-2.6%2C%202.5%2C%202.4%2C%202.3%2C%202.2%2C%202.1%2C%202.0%2C%201.1%2C%201.0%2C%200.11%2C%200.10%2C%200.9%2C%200.8-brightgreen.svg
44
+ .. image:: https://img.shields.io/badge/kafka-3.9--0.8-brightgreen.svg
45
45
  :target: https://kafka-python.readthedocs.io/en/master/compatibility.html
46
46
  .. image:: https://img.shields.io/pypi/pyversions/kafka-python.svg
47
47
  :target: https://pypi.python.org/pypi/kafka-python
@@ -168,7 +168,7 @@ def test_describe_consumer_group_exists(kafka_admin_client, kafka_consumer_facto
168
168
  stop[i] = Event()
169
169
  consumers[i] = kafka_consumer_factory(group_id=group_id)
170
170
  while not stop[i].is_set():
171
- consumers[i].poll(20)
171
+ consumers[i].poll(timeout_ms=200)
172
172
  consumers[i].close()
173
173
  consumers[i] = None
174
174
  stop[i] = None
@@ -183,6 +183,7 @@ def test_describe_consumer_group_exists(kafka_admin_client, kafka_consumer_facto
183
183
  try:
184
184
  timeout = time() + 35
185
185
  while True:
186
+ info('Checking consumers...')
186
187
  for c in range(num_consumers):
187
188
 
188
189
  # Verify all consumers have been created
@@ -212,9 +213,9 @@ def test_describe_consumer_group_exists(kafka_admin_client, kafka_consumer_facto
212
213
 
213
214
  if not rejoining and is_same_generation:
214
215
  break
215
- else:
216
- sleep(1)
217
216
  assert time() < timeout, "timeout waiting for assignments"
217
+ info('sleeping...')
218
+ sleep(1)
218
219
 
219
220
  info('Group stabilized; verifying assignment')
220
221
  output = kafka_admin_client.describe_consumer_groups(group_id_list)
@@ -236,6 +237,8 @@ def test_describe_consumer_group_exists(kafka_admin_client, kafka_consumer_facto
236
237
  for c in range(num_consumers):
237
238
  info('Stopping consumer %s', c)
238
239
  stop[c].set()
240
+ for c in range(num_consumers):
241
+ info('Waiting for consumer thread %s', c)
239
242
  threads[c].join()
240
243
  threads[c] = None
241
244