kafka-python 2.0.5__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.5 → kafka_python-2.0.6}/CHANGES.md +18 -0
  2. {kafka_python-2.0.5 → kafka_python-2.0.6}/PKG-INFO +1 -1
  3. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/admin/client.py +1 -1
  4. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/client_async.py +70 -37
  5. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/consumer/fetcher.py +12 -2
  6. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/coordinator/consumer.py +4 -1
  7. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/record/default_records.py +45 -2
  8. kafka_python-2.0.6/kafka/version.py +1 -0
  9. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka_python.egg-info/PKG-INFO +1 -1
  10. {kafka_python-2.0.5 → kafka_python-2.0.6}/test/test_client_async.py +13 -19
  11. {kafka_python-2.0.5 → kafka_python-2.0.6}/test/test_conn.py +1 -1
  12. kafka_python-2.0.5/kafka/version.py +0 -1
  13. {kafka_python-2.0.5 → kafka_python-2.0.6}/AUTHORS.md +0 -0
  14. {kafka_python-2.0.5 → kafka_python-2.0.6}/LICENSE +0 -0
  15. {kafka_python-2.0.5 → kafka_python-2.0.6}/MANIFEST.in +0 -0
  16. {kafka_python-2.0.5 → kafka_python-2.0.6}/README.rst +0 -0
  17. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/__init__.py +0 -0
  18. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/admin/__init__.py +0 -0
  19. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/admin/acl_resource.py +0 -0
  20. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/admin/config_resource.py +0 -0
  21. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/admin/new_partitions.py +0 -0
  22. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/admin/new_topic.py +0 -0
  23. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/cluster.py +0 -0
  24. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/codec.py +0 -0
  25. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/conn.py +0 -0
  26. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/consumer/__init__.py +0 -0
  27. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/consumer/group.py +0 -0
  28. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/consumer/subscription_state.py +0 -0
  29. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/coordinator/__init__.py +0 -0
  30. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/coordinator/assignors/__init__.py +0 -0
  31. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/coordinator/assignors/abstract.py +0 -0
  32. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/coordinator/assignors/range.py +0 -0
  33. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/coordinator/assignors/roundrobin.py +0 -0
  34. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/coordinator/assignors/sticky/__init__.py +0 -0
  35. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/coordinator/assignors/sticky/partition_movements.py +0 -0
  36. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/coordinator/assignors/sticky/sorted_set.py +0 -0
  37. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/coordinator/assignors/sticky/sticky_assignor.py +0 -0
  38. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/coordinator/base.py +0 -0
  39. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/coordinator/heartbeat.py +0 -0
  40. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/coordinator/protocol.py +0 -0
  41. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/errors.py +0 -0
  42. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/future.py +0 -0
  43. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/metrics/__init__.py +0 -0
  44. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/metrics/compound_stat.py +0 -0
  45. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/metrics/dict_reporter.py +0 -0
  46. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/metrics/kafka_metric.py +0 -0
  47. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/metrics/measurable.py +0 -0
  48. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/metrics/measurable_stat.py +0 -0
  49. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/metrics/metric_config.py +0 -0
  50. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/metrics/metric_name.py +0 -0
  51. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/metrics/metrics.py +0 -0
  52. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/metrics/metrics_reporter.py +0 -0
  53. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/metrics/quota.py +0 -0
  54. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/metrics/stat.py +0 -0
  55. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/metrics/stats/__init__.py +0 -0
  56. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/metrics/stats/avg.py +0 -0
  57. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/metrics/stats/count.py +0 -0
  58. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/metrics/stats/histogram.py +0 -0
  59. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/metrics/stats/max_stat.py +0 -0
  60. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/metrics/stats/min_stat.py +0 -0
  61. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/metrics/stats/percentile.py +0 -0
  62. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/metrics/stats/percentiles.py +0 -0
  63. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/metrics/stats/rate.py +0 -0
  64. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/metrics/stats/sampled_stat.py +0 -0
  65. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/metrics/stats/sensor.py +0 -0
  66. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/metrics/stats/total.py +0 -0
  67. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/oauth/__init__.py +0 -0
  68. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/oauth/abstract.py +0 -0
  69. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/partitioner/__init__.py +0 -0
  70. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/partitioner/default.py +0 -0
  71. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/producer/__init__.py +0 -0
  72. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/producer/buffer.py +0 -0
  73. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/producer/future.py +0 -0
  74. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/producer/kafka.py +0 -0
  75. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/producer/record_accumulator.py +0 -0
  76. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/producer/sender.py +0 -0
  77. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/protocol/__init__.py +0 -0
  78. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/protocol/abstract.py +0 -0
  79. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/protocol/admin.py +0 -0
  80. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/protocol/api.py +0 -0
  81. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/protocol/commit.py +0 -0
  82. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/protocol/fetch.py +0 -0
  83. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/protocol/frame.py +0 -0
  84. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/protocol/group.py +0 -0
  85. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/protocol/message.py +0 -0
  86. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/protocol/metadata.py +0 -0
  87. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/protocol/offset.py +0 -0
  88. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/protocol/parser.py +0 -0
  89. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/protocol/pickle.py +0 -0
  90. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/protocol/produce.py +0 -0
  91. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/protocol/struct.py +0 -0
  92. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/protocol/types.py +0 -0
  93. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/record/__init__.py +0 -0
  94. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/record/_crc32c.py +0 -0
  95. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/record/abc.py +0 -0
  96. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/record/legacy_records.py +0 -0
  97. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/record/memory_records.py +0 -0
  98. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/record/util.py +0 -0
  99. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/scram.py +0 -0
  100. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/serializer/__init__.py +0 -0
  101. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/serializer/abstract.py +0 -0
  102. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/structs.py +0 -0
  103. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/util.py +0 -0
  104. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/vendor/__init__.py +0 -0
  105. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/vendor/enum34.py +0 -0
  106. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/vendor/selectors34.py +0 -0
  107. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/vendor/six.py +0 -0
  108. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka/vendor/socketpair.py +0 -0
  109. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka_python.egg-info/SOURCES.txt +0 -0
  110. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka_python.egg-info/dependency_links.txt +0 -0
  111. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka_python.egg-info/requires.txt +0 -0
  112. {kafka_python-2.0.5 → kafka_python-2.0.6}/kafka_python.egg-info/top_level.txt +0 -0
  113. {kafka_python-2.0.5 → kafka_python-2.0.6}/pyproject.toml +0 -0
  114. {kafka_python-2.0.5 → kafka_python-2.0.6}/setup.cfg +0 -0
  115. {kafka_python-2.0.5 → kafka_python-2.0.6}/setup.py +0 -0
  116. {kafka_python-2.0.5 → kafka_python-2.0.6}/test/test_acl_comparisons.py +0 -0
  117. {kafka_python-2.0.5 → kafka_python-2.0.6}/test/test_admin.py +0 -0
  118. {kafka_python-2.0.5 → kafka_python-2.0.6}/test/test_admin_integration.py +0 -0
  119. {kafka_python-2.0.5 → kafka_python-2.0.6}/test/test_api_object_implementation.py +0 -0
  120. {kafka_python-2.0.5 → kafka_python-2.0.6}/test/test_assignors.py +0 -0
  121. {kafka_python-2.0.5 → kafka_python-2.0.6}/test/test_cluster.py +0 -0
  122. {kafka_python-2.0.5 → kafka_python-2.0.6}/test/test_codec.py +0 -0
  123. {kafka_python-2.0.5 → kafka_python-2.0.6}/test/test_consumer.py +0 -0
  124. {kafka_python-2.0.5 → kafka_python-2.0.6}/test/test_consumer_group.py +0 -0
  125. {kafka_python-2.0.5 → kafka_python-2.0.6}/test/test_consumer_integration.py +0 -0
  126. {kafka_python-2.0.5 → kafka_python-2.0.6}/test/test_coordinator.py +0 -0
  127. {kafka_python-2.0.5 → kafka_python-2.0.6}/test/test_fetcher.py +0 -0
  128. {kafka_python-2.0.5 → kafka_python-2.0.6}/test/test_metrics.py +0 -0
  129. {kafka_python-2.0.5 → kafka_python-2.0.6}/test/test_object_conversion.py +0 -0
  130. {kafka_python-2.0.5 → kafka_python-2.0.6}/test/test_package.py +0 -0
  131. {kafka_python-2.0.5 → kafka_python-2.0.6}/test/test_partition_movements.py +0 -0
  132. {kafka_python-2.0.5 → kafka_python-2.0.6}/test/test_partitioner.py +0 -0
  133. {kafka_python-2.0.5 → kafka_python-2.0.6}/test/test_producer.py +0 -0
  134. {kafka_python-2.0.5 → kafka_python-2.0.6}/test/test_protocol.py +0 -0
  135. {kafka_python-2.0.5 → kafka_python-2.0.6}/test/test_sasl_integration.py +0 -0
  136. {kafka_python-2.0.5 → kafka_python-2.0.6}/test/test_sender.py +0 -0
  137. {kafka_python-2.0.5 → kafka_python-2.0.6}/test/test_subscription_state.py +0 -0
  138. {kafka_python-2.0.5 → kafka_python-2.0.6}/test/testutil.py +0 -0
@@ -1,3 +1,21 @@
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
+
1
19
  # 2.0.5 (Feb 25, 2025)
2
20
 
3
21
  Networking
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: kafka-python
3
- Version: 2.0.5
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
@@ -294,7 +294,7 @@ class KafkaAdminClient(object):
294
294
  time.sleep(1)
295
295
  continue
296
296
  # verify the controller is new enough to support our requests
297
- controller_version = self._client.check_version(node_id=controller_id, timeout=(self.config['api_version_auto_timeout_ms'] / 1000))
297
+ controller_version = self._client.check_version(node_id=controller_id)
298
298
  if controller_version < (0, 10, 0):
299
299
  raise IncompatibleBrokerVersion(
300
300
  "The controller appears to be running Kafka {}. KafkaAdminClient requires brokers >= 0.10.0.0."
@@ -237,8 +237,7 @@ class KafkaClient(object):
237
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()
@@ -364,14 +363,24 @@ class KafkaClient(object):
364
363
 
365
364
  return False
366
365
 
367
- def _maybe_connect(self, node_id):
368
- """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
+ """
369
371
  with self._lock:
370
372
  conn = self._conns.get(node_id)
371
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
+
372
379
  if conn is None:
373
380
  broker = self.cluster.broker_metadata(node_id)
374
- 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
375
384
 
376
385
  log.debug("Initiating connection to node %s at %s:%s",
377
386
  node_id, broker.host, broker.port)
@@ -383,16 +392,9 @@ class KafkaClient(object):
383
392
  **self.config)
384
393
  self._conns[node_id] = conn
385
394
 
386
- # Check if existing connection should be recreated because host/port changed
387
- elif self._should_recycle_connection(conn):
388
- self._conns.pop(node_id)
389
- return False
390
-
391
- elif conn.connected():
392
- return True
393
-
394
- conn.connect()
395
- return conn.connected()
395
+ if conn.disconnected():
396
+ conn.connect()
397
+ return not conn.disconnected()
396
398
 
397
399
  def ready(self, node_id, metadata_priority=True):
398
400
  """Check whether a node is connected and ok to send more requests.
@@ -576,12 +578,18 @@ class KafkaClient(object):
576
578
  if self._closed:
577
579
  break
578
580
 
579
- # Send a metadata request if needed (or initiate new connection)
580
- metadata_timeout_ms = self._maybe_refresh_metadata()
581
-
582
581
  # Attempt to complete pending connections
583
582
  for node_id in list(self._connecting):
584
- 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()
585
593
 
586
594
  # If we got a future that is already done, don't block in _poll
587
595
  if future is not None and future.is_done:
@@ -623,6 +631,11 @@ class KafkaClient(object):
623
631
  self._selector.register(conn._sock, selectors.EVENT_WRITE, conn)
624
632
 
625
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
626
639
  # This needs to be locked, but since it is only called from within the
627
640
  # locked section of poll(), there is no additional lock acquisition here
628
641
  processed = set()
@@ -843,6 +856,26 @@ class KafkaClient(object):
843
856
  log.debug("Give up sending metadata request since no node is available. (reconnect delay %d ms)", next_connect_ms)
844
857
  return next_connect_ms
845
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
846
879
  if self._can_send_request(node_id):
847
880
  topics = list(self._topics)
848
881
  if not topics and self.cluster.is_bootstrap(node_id):
@@ -864,20 +897,11 @@ class KafkaClient(object):
864
897
  future.add_errback(refresh_done)
865
898
  return self.config['request_timeout_ms']
866
899
 
867
- # If there's any connection establishment underway, wait until it completes. This prevents
868
- # the client from unnecessarily connecting to additional nodes while a previous connection
869
- # attempt has not been completed.
900
+ # Should only get here if still connecting
870
901
  if self._connecting:
871
902
  return float('inf')
872
-
873
- if self.maybe_connect(node_id, wakeup=wakeup):
874
- log.debug("Initializing connection to node %s for metadata request", node_id)
875
- return float('inf')
876
-
877
- # connected but can't send more, OR connecting
878
- # In either case we just need to wait for a network event
879
- # to let us know the selected connection might be usable again.
880
- return float('inf')
903
+ else:
904
+ return self.config['reconnect_backoff_ms']
881
905
 
882
906
  def get_api_versions(self):
883
907
  """Return the ApiVersions map, if available.
@@ -890,13 +914,16 @@ class KafkaClient(object):
890
914
  """
891
915
  return self._api_versions
892
916
 
893
- def check_version(self, node_id=None, timeout=2, strict=False):
917
+ def check_version(self, node_id=None, timeout=None, strict=False):
894
918
  """Attempt to guess the version of a Kafka broker.
895
919
 
896
- Note: It is possible that this method blocks longer than the
897
- specified timeout. This can happen if the entire cluster
898
- is down and the client enters a bootstrap backoff sleep.
899
- 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
900
927
 
901
928
  Returns: version tuple, i.e. (0, 10), (0, 9), (0, 8, 2), ...
902
929
 
@@ -906,6 +933,7 @@ class KafkaClient(object):
906
933
  UnrecognizedBrokerVersion: please file bug if seen!
907
934
  AssertionError (if strict=True): please file bug if seen!
908
935
  """
936
+ timeout = timeout or (self.config['api_version_auto_timeout_ms'] / 1000)
909
937
  self._lock.acquire()
910
938
  end = time.time() + timeout
911
939
  while time.time() < end:
@@ -916,7 +944,12 @@ class KafkaClient(object):
916
944
  if try_node is None:
917
945
  self._lock.release()
918
946
  raise Errors.NoBrokersAvailable()
919
- 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
+
920
953
  conn = self._conns[try_node]
921
954
 
922
955
  # We will intentionally cause socket failures
@@ -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
 
@@ -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.5
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
@@ -58,7 +58,7 @@ def test_can_connect(cli, conn):
58
58
  assert cli._can_connect(0)
59
59
 
60
60
  # Node is connected, can't reconnect
61
- assert cli._maybe_connect(0) is True
61
+ assert cli._init_connect(0) is True
62
62
  assert not cli._can_connect(0)
63
63
 
64
64
  # Node is disconnected, can connect
@@ -70,20 +70,15 @@ def test_can_connect(cli, conn):
70
70
  assert not cli._can_connect(0)
71
71
 
72
72
 
73
- def test_maybe_connect(cli, conn):
74
- try:
75
- # Node not in metadata, raises AssertionError
76
- cli._maybe_connect(2)
77
- except AssertionError:
78
- pass
79
- else:
80
- assert False, 'Exception not raised'
73
+ def test_init_connect(cli, conn):
74
+ # Node not in metadata, return False
75
+ assert not cli._init_connect(2)
81
76
 
82
77
  # New node_id creates a conn object
83
78
  assert 0 not in cli._conns
84
79
  conn.state = ConnectionStates.DISCONNECTED
85
80
  conn.connect.side_effect = lambda: conn._set_conn_state(ConnectionStates.CONNECTING)
86
- assert cli._maybe_connect(0) is False
81
+ assert cli._init_connect(0) is True
87
82
  assert cli._conns[0] is conn
88
83
 
89
84
 
@@ -127,8 +122,8 @@ def test_ready(mocker, cli, conn):
127
122
 
128
123
 
129
124
  def test_is_ready(mocker, cli, conn):
130
- cli._maybe_connect(0)
131
- cli._maybe_connect(1)
125
+ cli._init_connect(0)
126
+ cli._init_connect(1)
132
127
 
133
128
  # metadata refresh blocks ready nodes
134
129
  assert cli.is_ready(0)
@@ -171,14 +166,14 @@ def test_close(mocker, cli, conn):
171
166
  assert conn.close.call_count == call_count
172
167
 
173
168
  # Single node close
174
- cli._maybe_connect(0)
169
+ cli._init_connect(0)
175
170
  assert conn.close.call_count == call_count
176
171
  cli.close(0)
177
172
  call_count += 1
178
173
  assert conn.close.call_count == call_count
179
174
 
180
175
  # All node close
181
- cli._maybe_connect(1)
176
+ cli._init_connect(1)
182
177
  cli.close()
183
178
  # +2 close: node 1, node bootstrap (node 0 already closed)
184
179
  call_count += 2
@@ -190,7 +185,7 @@ def test_is_disconnected(cli, conn):
190
185
  conn.state = ConnectionStates.DISCONNECTED
191
186
  assert not cli.is_disconnected(0)
192
187
 
193
- cli._maybe_connect(0)
188
+ cli._init_connect(0)
194
189
  assert cli.is_disconnected(0)
195
190
 
196
191
  conn.state = ConnectionStates.CONNECTING
@@ -215,7 +210,7 @@ def test_send(cli, conn):
215
210
  assert isinstance(f.exception, Errors.NodeNotReadyError)
216
211
 
217
212
  conn.state = ConnectionStates.CONNECTED
218
- cli._maybe_connect(0)
213
+ cli._init_connect(0)
219
214
  # ProduceRequest w/ 0 required_acks -> no response
220
215
  request = ProduceRequest[0](0, 0, [])
221
216
  assert request.expect_response() is False
@@ -344,8 +339,7 @@ def test_maybe_refresh_metadata_cant_send(mocker, client):
344
339
  mocker.patch.object(client, 'least_loaded_node', return_value='foobar')
345
340
  mocker.patch.object(client, '_can_send_request', return_value=False)
346
341
  mocker.patch.object(client, '_can_connect', return_value=True)
347
- mocker.patch.object(client, '_maybe_connect', return_value=True)
348
- mocker.patch.object(client, 'maybe_connect', return_value=True)
342
+ mocker.patch.object(client, '_init_connect', return_value=True)
349
343
 
350
344
  now = time.time()
351
345
  t = mocker.patch('time.time')
@@ -354,7 +348,7 @@ def test_maybe_refresh_metadata_cant_send(mocker, client):
354
348
  # first poll attempts connection
355
349
  client.poll(timeout_ms=12345678)
356
350
  client._poll.assert_called_with(12345.678)
357
- client.maybe_connect.assert_called_once_with('foobar', wakeup=False)
351
+ client._init_connect.assert_called_once_with('foobar')
358
352
 
359
353
  # poll while connecting should not attempt a new connection
360
354
  client._connecting.add('foobar')
@@ -90,7 +90,7 @@ def test_connection_delay(conn, mocker):
90
90
  conn.state = ConnectionStates.CONNECTED
91
91
  assert conn.connection_delay() == float('inf')
92
92
 
93
- conn._gai.clear()
93
+ del conn._gai[:]
94
94
  conn._update_reconnect_backoff()
95
95
  conn.state = ConnectionStates.DISCONNECTED
96
96
  assert conn.connection_delay() == 1.0 * conn.config['reconnect_backoff_ms']
@@ -1 +0,0 @@
1
- __version__ = '2.0.5'
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes