kafka-python 2.2.7__tar.gz → 2.2.8__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 (168) hide show
  1. {kafka_python-2.2.7 → kafka_python-2.2.8}/CHANGES.md +12 -0
  2. {kafka_python-2.2.7 → kafka_python-2.2.8}/PKG-INFO +1 -1
  3. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/coordinator/base.py +96 -47
  4. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/coordinator/consumer.py +21 -5
  5. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/errors.py +1 -8
  6. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/producer/kafka.py +2 -2
  7. kafka_python-2.2.8/kafka/version.py +1 -0
  8. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka_python.egg-info/PKG-INFO +1 -1
  9. {kafka_python-2.2.7 → kafka_python-2.2.8}/test/integration/test_consumer_group.py +26 -2
  10. {kafka_python-2.2.7 → kafka_python-2.2.8}/test/test_coordinator.py +1 -0
  11. kafka_python-2.2.7/kafka/version.py +0 -1
  12. {kafka_python-2.2.7 → kafka_python-2.2.8}/AUTHORS.md +0 -0
  13. {kafka_python-2.2.7 → kafka_python-2.2.8}/LICENSE +0 -0
  14. {kafka_python-2.2.7 → kafka_python-2.2.8}/MANIFEST.in +0 -0
  15. {kafka_python-2.2.7 → kafka_python-2.2.8}/README.rst +0 -0
  16. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/__init__.py +0 -0
  17. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/admin/__init__.py +0 -0
  18. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/admin/acl_resource.py +0 -0
  19. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/admin/client.py +0 -0
  20. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/admin/config_resource.py +0 -0
  21. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/admin/new_partitions.py +0 -0
  22. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/admin/new_topic.py +0 -0
  23. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/benchmarks/__init__.py +0 -0
  24. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/benchmarks/consumer_performance.py +0 -0
  25. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/benchmarks/load_example.py +0 -0
  26. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/benchmarks/producer_performance.py +0 -0
  27. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/benchmarks/record_batch_compose.py +0 -0
  28. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/benchmarks/record_batch_read.py +0 -0
  29. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/benchmarks/varint_speed.py +0 -0
  30. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/client_async.py +0 -0
  31. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/cluster.py +0 -0
  32. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/codec.py +0 -0
  33. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/conn.py +0 -0
  34. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/consumer/__init__.py +0 -0
  35. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/consumer/fetcher.py +0 -0
  36. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/consumer/group.py +0 -0
  37. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/consumer/subscription_state.py +0 -0
  38. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/coordinator/__init__.py +0 -0
  39. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/coordinator/assignors/__init__.py +0 -0
  40. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/coordinator/assignors/abstract.py +0 -0
  41. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/coordinator/assignors/range.py +0 -0
  42. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/coordinator/assignors/roundrobin.py +0 -0
  43. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/coordinator/assignors/sticky/__init__.py +0 -0
  44. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/coordinator/assignors/sticky/partition_movements.py +0 -0
  45. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/coordinator/assignors/sticky/sorted_set.py +0 -0
  46. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/coordinator/assignors/sticky/sticky_assignor.py +0 -0
  47. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/coordinator/heartbeat.py +0 -0
  48. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/coordinator/protocol.py +0 -0
  49. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/future.py +0 -0
  50. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/metrics/__init__.py +0 -0
  51. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/metrics/compound_stat.py +0 -0
  52. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/metrics/dict_reporter.py +0 -0
  53. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/metrics/kafka_metric.py +0 -0
  54. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/metrics/measurable.py +0 -0
  55. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/metrics/measurable_stat.py +0 -0
  56. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/metrics/metric_config.py +0 -0
  57. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/metrics/metric_name.py +0 -0
  58. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/metrics/metrics.py +0 -0
  59. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/metrics/metrics_reporter.py +0 -0
  60. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/metrics/quota.py +0 -0
  61. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/metrics/stat.py +0 -0
  62. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/metrics/stats/__init__.py +0 -0
  63. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/metrics/stats/avg.py +0 -0
  64. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/metrics/stats/count.py +0 -0
  65. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/metrics/stats/histogram.py +0 -0
  66. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/metrics/stats/max_stat.py +0 -0
  67. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/metrics/stats/min_stat.py +0 -0
  68. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/metrics/stats/percentile.py +0 -0
  69. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/metrics/stats/percentiles.py +0 -0
  70. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/metrics/stats/rate.py +0 -0
  71. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/metrics/stats/sampled_stat.py +0 -0
  72. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/metrics/stats/sensor.py +0 -0
  73. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/metrics/stats/total.py +0 -0
  74. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/partitioner/__init__.py +0 -0
  75. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/partitioner/default.py +0 -0
  76. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/producer/__init__.py +0 -0
  77. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/producer/future.py +0 -0
  78. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/producer/record_accumulator.py +0 -0
  79. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/producer/sender.py +0 -0
  80. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/producer/transaction_manager.py +0 -0
  81. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/protocol/__init__.py +0 -0
  82. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/protocol/abstract.py +0 -0
  83. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/protocol/add_offsets_to_txn.py +0 -0
  84. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/protocol/add_partitions_to_txn.py +0 -0
  85. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/protocol/admin.py +0 -0
  86. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/protocol/api.py +0 -0
  87. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/protocol/api_versions.py +0 -0
  88. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/protocol/broker_api_versions.py +0 -0
  89. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/protocol/commit.py +0 -0
  90. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/protocol/end_txn.py +0 -0
  91. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/protocol/fetch.py +0 -0
  92. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/protocol/find_coordinator.py +0 -0
  93. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/protocol/frame.py +0 -0
  94. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/protocol/group.py +0 -0
  95. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/protocol/init_producer_id.py +0 -0
  96. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/protocol/list_offsets.py +0 -0
  97. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/protocol/message.py +0 -0
  98. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/protocol/metadata.py +0 -0
  99. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/protocol/offset_for_leader_epoch.py +0 -0
  100. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/protocol/parser.py +0 -0
  101. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/protocol/pickle.py +0 -0
  102. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/protocol/produce.py +0 -0
  103. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/protocol/sasl_authenticate.py +0 -0
  104. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/protocol/sasl_handshake.py +0 -0
  105. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/protocol/struct.py +0 -0
  106. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/protocol/txn_offset_commit.py +0 -0
  107. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/protocol/types.py +0 -0
  108. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/record/__init__.py +0 -0
  109. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/record/_crc32c.py +0 -0
  110. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/record/abc.py +0 -0
  111. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/record/default_records.py +0 -0
  112. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/record/legacy_records.py +0 -0
  113. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/record/memory_records.py +0 -0
  114. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/record/util.py +0 -0
  115. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/sasl/__init__.py +0 -0
  116. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/sasl/abc.py +0 -0
  117. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/sasl/gssapi.py +0 -0
  118. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/sasl/msk.py +0 -0
  119. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/sasl/oauth.py +0 -0
  120. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/sasl/plain.py +0 -0
  121. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/sasl/scram.py +0 -0
  122. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/sasl/sspi.py +0 -0
  123. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/serializer/__init__.py +0 -0
  124. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/serializer/abstract.py +0 -0
  125. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/socks5_wrapper.py +0 -0
  126. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/structs.py +0 -0
  127. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/util.py +0 -0
  128. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/vendor/__init__.py +0 -0
  129. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/vendor/enum34.py +0 -0
  130. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/vendor/selectors34.py +0 -0
  131. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/vendor/six.py +0 -0
  132. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka/vendor/socketpair.py +0 -0
  133. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka_python.egg-info/SOURCES.txt +0 -0
  134. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka_python.egg-info/dependency_links.txt +0 -0
  135. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka_python.egg-info/requires.txt +0 -0
  136. {kafka_python-2.2.7 → kafka_python-2.2.8}/kafka_python.egg-info/top_level.txt +0 -0
  137. {kafka_python-2.2.7 → kafka_python-2.2.8}/pyproject.toml +0 -0
  138. {kafka_python-2.2.7 → kafka_python-2.2.8}/setup.cfg +0 -0
  139. {kafka_python-2.2.7 → kafka_python-2.2.8}/setup.py +0 -0
  140. {kafka_python-2.2.7 → kafka_python-2.2.8}/test/integration/__init__.py +0 -0
  141. {kafka_python-2.2.7 → kafka_python-2.2.8}/test/integration/conftest.py +0 -0
  142. {kafka_python-2.2.7 → kafka_python-2.2.8}/test/integration/fixtures.py +0 -0
  143. {kafka_python-2.2.7 → kafka_python-2.2.8}/test/integration/test_admin_integration.py +0 -0
  144. {kafka_python-2.2.7 → kafka_python-2.2.8}/test/integration/test_consumer_integration.py +0 -0
  145. {kafka_python-2.2.7 → kafka_python-2.2.8}/test/integration/test_producer_integration.py +0 -0
  146. {kafka_python-2.2.7 → kafka_python-2.2.8}/test/integration/test_sasl_integration.py +0 -0
  147. {kafka_python-2.2.7 → kafka_python-2.2.8}/test/test_acl_comparisons.py +0 -0
  148. {kafka_python-2.2.7 → kafka_python-2.2.8}/test/test_admin.py +0 -0
  149. {kafka_python-2.2.7 → kafka_python-2.2.8}/test/test_api_object_implementation.py +0 -0
  150. {kafka_python-2.2.7 → kafka_python-2.2.8}/test/test_assignors.py +0 -0
  151. {kafka_python-2.2.7 → kafka_python-2.2.8}/test/test_client_async.py +0 -0
  152. {kafka_python-2.2.7 → kafka_python-2.2.8}/test/test_cluster.py +0 -0
  153. {kafka_python-2.2.7 → kafka_python-2.2.8}/test/test_codec.py +0 -0
  154. {kafka_python-2.2.7 → kafka_python-2.2.8}/test/test_conn.py +0 -0
  155. {kafka_python-2.2.7 → kafka_python-2.2.8}/test/test_consumer.py +0 -0
  156. {kafka_python-2.2.7 → kafka_python-2.2.8}/test/test_fetcher.py +0 -0
  157. {kafka_python-2.2.7 → kafka_python-2.2.8}/test/test_metrics.py +0 -0
  158. {kafka_python-2.2.7 → kafka_python-2.2.8}/test/test_object_conversion.py +0 -0
  159. {kafka_python-2.2.7 → kafka_python-2.2.8}/test/test_package.py +0 -0
  160. {kafka_python-2.2.7 → kafka_python-2.2.8}/test/test_partition_movements.py +0 -0
  161. {kafka_python-2.2.7 → kafka_python-2.2.8}/test/test_partitioner.py +0 -0
  162. {kafka_python-2.2.7 → kafka_python-2.2.8}/test/test_producer.py +0 -0
  163. {kafka_python-2.2.7 → kafka_python-2.2.8}/test/test_protocol.py +0 -0
  164. {kafka_python-2.2.7 → kafka_python-2.2.8}/test/test_record_accumulator.py +0 -0
  165. {kafka_python-2.2.7 → kafka_python-2.2.8}/test/test_sender.py +0 -0
  166. {kafka_python-2.2.7 → kafka_python-2.2.8}/test/test_subscription_state.py +0 -0
  167. {kafka_python-2.2.7 → kafka_python-2.2.8}/test/test_util.py +0 -0
  168. {kafka_python-2.2.7 → kafka_python-2.2.8}/test/testutil.py +0 -0
@@ -1,3 +1,15 @@
1
+ # 2.2.8 (May 20, 2025)
2
+
3
+ Fixes
4
+ * Wait for next heartbeat in thread loop; check for connected coordinator (#2622)
5
+ * Acquire client lock in heartbeat thread before sending requests (#2620)
6
+
7
+ Logging / Error Messages
8
+ * Log all SyncGroupResponse errors as info+
9
+ * More coordinator / heartbeat logging (#2621)
10
+ * Fix timeout seconds error message in KafkaProducer (#2627)
11
+ * Update offset commit error handling; use RebalanceInProgressError if applicable (#2623)
12
+
1
13
  # 2.2.7 (May 13, 2025)
2
14
 
3
15
  Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kafka-python
3
- Version: 2.2.7
3
+ Version: 2.2.8
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
@@ -5,6 +5,7 @@ import copy
5
5
  import logging
6
6
  import threading
7
7
  import time
8
+ import warnings
8
9
  import weakref
9
10
 
10
11
  from kafka.vendor import six
@@ -43,6 +44,9 @@ class Generation(object):
43
44
  self.member_id == other.member_id and
44
45
  self.protocol == other.protocol)
45
46
 
47
+ def __str__(self):
48
+ return "<Generation %s (member_id: %s, protocol: %s)>" % (self.generation_id, self.member_id, self.protocol)
49
+
46
50
 
47
51
  Generation.NO_GENERATION = Generation(DEFAULT_GENERATION_ID, UNKNOWN_MEMBER_ID, None)
48
52
 
@@ -250,6 +254,11 @@ class BaseCoordinator(object):
250
254
  else:
251
255
  return self.coordinator_id
252
256
 
257
+ def connected(self):
258
+ """Return True iff the coordinator node is connected"""
259
+ with self._lock:
260
+ return self.coordinator_id is not None and self._client.connected(self.coordinator_id)
261
+
253
262
  def ensure_coordinator_ready(self, timeout_ms=None):
254
263
  """Block until the coordinator for this group is known.
255
264
 
@@ -309,7 +318,7 @@ class BaseCoordinator(object):
309
318
  self._find_coordinator_future = None
310
319
 
311
320
  def lookup_coordinator(self):
312
- with self._lock:
321
+ with self._client._lock, self._lock:
313
322
  if self._find_coordinator_future is not None:
314
323
  return self._find_coordinator_future
315
324
 
@@ -398,17 +407,16 @@ class BaseCoordinator(object):
398
407
  # will be invoked even if the consumer is woken up before
399
408
  # finishing the rebalance
400
409
  with self._lock:
401
- log.info("Successfully joined group %s with generation %s",
402
- self.group_id, self._generation.generation_id)
403
410
  self.state = MemberState.STABLE
404
411
  if self._heartbeat_thread:
405
412
  self._heartbeat_thread.enable()
406
413
 
407
- def _handle_join_failure(self, _):
414
+ def _handle_join_failure(self, exception):
408
415
  # we handle failures below after the request finishes.
409
416
  # if the join completes after having been woken up,
410
417
  # the exception is ignored and we will rejoin
411
418
  with self._lock:
419
+ log.info("Failed to join group %s: %s", self.group_id, exception)
412
420
  self.state = MemberState.UNJOINED
413
421
 
414
422
  def ensure_active_group(self, timeout_ms=None):
@@ -554,8 +562,9 @@ class BaseCoordinator(object):
554
562
 
555
563
  def _failed_request(self, node_id, request, future, error):
556
564
  # Marking coordinator dead
557
- # unless the error is caused by internal client pipelining
565
+ # unless the error is caused by internal client pipelining or throttling
558
566
  if not isinstance(error, (Errors.NodeNotReadyError,
567
+ Errors.ThrottlingQuotaExceededError,
559
568
  Errors.TooManyInFlightRequests)):
560
569
  log.error('Error sending %s to node %s [%s]',
561
570
  request.__class__.__name__, node_id, error)
@@ -566,10 +575,9 @@ class BaseCoordinator(object):
566
575
  future.failure(error)
567
576
 
568
577
  def _handle_join_group_response(self, future, send_time, response):
578
+ log.debug("Received JoinGroup response: %s", response)
569
579
  error_type = Errors.for_code(response.error_code)
570
580
  if error_type is Errors.NoError:
571
- log.debug("Received successful JoinGroup response for group %s: %s",
572
- self.group_id, response)
573
581
  if self._sensors:
574
582
  self._sensors.join_latency.record((time.time() - send_time) * 1000)
575
583
  with self._lock:
@@ -583,6 +591,7 @@ class BaseCoordinator(object):
583
591
  response.member_id,
584
592
  response.group_protocol)
585
593
 
594
+ log.info("Successfully joined group %s %s", self.group_id, self._generation)
586
595
  if response.leader_id == response.member_id:
587
596
  log.info("Elected group leader -- performing partition"
588
597
  " assignments using %s", self._generation.protocol)
@@ -591,24 +600,24 @@ class BaseCoordinator(object):
591
600
  self._on_join_follower().chain(future)
592
601
 
593
602
  elif error_type is Errors.CoordinatorLoadInProgressError:
594
- log.debug("Attempt to join group %s rejected since coordinator %s"
595
- " is loading the group.", self.group_id, self.coordinator_id)
603
+ log.info("Attempt to join group %s rejected since coordinator %s"
604
+ " is loading the group.", self.group_id, self.coordinator_id)
596
605
  # backoff and retry
597
606
  future.failure(error_type(response))
598
607
  elif error_type is Errors.UnknownMemberIdError:
599
608
  # reset the member id and retry immediately
600
609
  error = error_type(self._generation.member_id)
601
610
  self.reset_generation()
602
- log.debug("Attempt to join group %s failed due to unknown member id",
603
- self.group_id)
611
+ log.info("Attempt to join group %s failed due to unknown member id",
612
+ self.group_id)
604
613
  future.failure(error)
605
614
  elif error_type in (Errors.CoordinatorNotAvailableError,
606
615
  Errors.NotCoordinatorError):
607
616
  # re-discover the coordinator and retry with backoff
608
617
  self.coordinator_dead(error_type())
609
- log.debug("Attempt to join group %s failed due to obsolete "
610
- "coordinator information: %s", self.group_id,
611
- error_type.__name__)
618
+ log.info("Attempt to join group %s failed due to obsolete "
619
+ "coordinator information: %s", self.group_id,
620
+ error_type.__name__)
612
621
  future.failure(error_type())
613
622
  elif error_type in (Errors.InconsistentGroupProtocolError,
614
623
  Errors.InvalidSessionTimeoutError,
@@ -619,12 +628,21 @@ class BaseCoordinator(object):
619
628
  self.group_id, error)
620
629
  future.failure(error)
621
630
  elif error_type is Errors.GroupAuthorizationFailedError:
631
+ log.error("Attempt to join group %s failed due to group authorization error",
632
+ self.group_id)
622
633
  future.failure(error_type(self.group_id))
623
634
  elif error_type is Errors.MemberIdRequiredError:
624
635
  # Broker requires a concrete member id to be allowed to join the group. Update member id
625
636
  # and send another join group request in next cycle.
637
+ log.info("Received member id %s for group %s; will retry join-group",
638
+ response.member_id, self.group_id)
626
639
  self.reset_generation(response.member_id)
627
640
  future.failure(error_type())
641
+ elif error_type is Errors.RebalanceInProgressError:
642
+ log.info("Attempt to join group %s failed due to RebalanceInProgressError,"
643
+ " which could indicate a replication timeout on the broker. Will retry.",
644
+ self.group_id)
645
+ future.failure(error_type())
628
646
  else:
629
647
  # unexpected error, throw the exception
630
648
  error = error_type()
@@ -693,6 +711,7 @@ class BaseCoordinator(object):
693
711
  return future
694
712
 
695
713
  def _handle_sync_group_response(self, future, send_time, response):
714
+ log.debug("Received SyncGroup response: %s", response)
696
715
  error_type = Errors.for_code(response.error_code)
697
716
  if error_type is Errors.NoError:
698
717
  if self._sensors:
@@ -705,19 +724,19 @@ class BaseCoordinator(object):
705
724
  if error_type is Errors.GroupAuthorizationFailedError:
706
725
  future.failure(error_type(self.group_id))
707
726
  elif error_type is Errors.RebalanceInProgressError:
708
- log.debug("SyncGroup for group %s failed due to coordinator"
709
- " rebalance", self.group_id)
727
+ log.info("SyncGroup for group %s failed due to coordinator"
728
+ " rebalance", self.group_id)
710
729
  future.failure(error_type(self.group_id))
711
730
  elif error_type in (Errors.UnknownMemberIdError,
712
731
  Errors.IllegalGenerationError):
713
732
  error = error_type()
714
- log.debug("SyncGroup for group %s failed due to %s", self.group_id, error)
733
+ log.info("SyncGroup for group %s failed due to %s", self.group_id, error)
715
734
  self.reset_generation()
716
735
  future.failure(error)
717
736
  elif error_type in (Errors.CoordinatorNotAvailableError,
718
737
  Errors.NotCoordinatorError):
719
738
  error = error_type()
720
- log.debug("SyncGroup for group %s failed due to %s", self.group_id, error)
739
+ log.info("SyncGroup for group %s failed due to %s", self.group_id, error)
721
740
  self.coordinator_dead(error)
722
741
  future.failure(error)
723
742
  else:
@@ -739,13 +758,13 @@ class BaseCoordinator(object):
739
758
  e = Errors.NodeNotReadyError(node_id)
740
759
  return Future().failure(e)
741
760
 
742
- log.debug("Sending group coordinator request for group %s to broker %s",
743
- self.group_id, node_id)
744
761
  version = self._client.api_version(FindCoordinatorRequest, max_version=2)
745
762
  if version == 0:
746
763
  request = FindCoordinatorRequest[version](self.group_id)
747
764
  else:
748
765
  request = FindCoordinatorRequest[version](self.group_id, 0)
766
+ log.debug("Sending group coordinator request for group %s to broker %s: %s",
767
+ self.group_id, node_id, request)
749
768
  future = Future()
750
769
  _f = self._client.send(node_id, request)
751
770
  _f.add_callback(self._handle_group_coordinator_response, future)
@@ -792,7 +811,7 @@ class BaseCoordinator(object):
792
811
  self.coordinator_id, self.group_id, error)
793
812
  self.coordinator_id = None
794
813
 
795
- def generation(self):
814
+ def generation_if_stable(self):
796
815
  """Get the current generation state if the group is stable.
797
816
 
798
817
  Returns: the current generation or None if the group is unjoined/rebalancing
@@ -802,6 +821,15 @@ class BaseCoordinator(object):
802
821
  return None
803
822
  return self._generation
804
823
 
824
+ # deprecated
825
+ def generation(self):
826
+ warnings.warn("Function coordinator.generation() has been renamed to generation_if_stable()",
827
+ DeprecationWarning, stacklevel=2)
828
+ return self.generation_if_stable()
829
+
830
+ def rebalance_in_progress(self):
831
+ return self.state is MemberState.REBALANCING
832
+
805
833
  def reset_generation(self, member_id=UNKNOWN_MEMBER_ID):
806
834
  """Reset the generation and member_id because we have fallen out of the group."""
807
835
  with self._lock:
@@ -865,6 +893,7 @@ class BaseCoordinator(object):
865
893
  log.info('Leaving consumer group (%s).', self.group_id)
866
894
  version = self._client.api_version(LeaveGroupRequest, max_version=2)
867
895
  request = LeaveGroupRequest[version](self.group_id, self._generation.member_id)
896
+ log.debug('Sending LeaveGroupRequest to %s: %s', self.coordinator_id, request)
868
897
  future = self._client.send(self.coordinator_id, request)
869
898
  future.add_callback(self._handle_leave_group_response)
870
899
  future.add_errback(log.error, "LeaveGroup request failed: %s")
@@ -873,16 +902,18 @@ class BaseCoordinator(object):
873
902
  self.reset_generation()
874
903
 
875
904
  def _handle_leave_group_response(self, response):
905
+ log.debug("Received LeaveGroupResponse: %s", response)
876
906
  error_type = Errors.for_code(response.error_code)
877
907
  if error_type is Errors.NoError:
878
- log.debug("LeaveGroup request for group %s returned successfully",
879
- self.group_id)
908
+ log.info("LeaveGroup request for group %s returned successfully",
909
+ self.group_id)
880
910
  else:
881
911
  log.error("LeaveGroup request for group %s failed with error: %s",
882
912
  self.group_id, error_type())
883
913
 
884
914
  def _send_heartbeat_request(self):
885
915
  """Send a heartbeat request"""
916
+ # Note: acquire both client + coordinator lock before calling
886
917
  if self.coordinator_unknown():
887
918
  e = Errors.CoordinatorNotAvailableError(self.coordinator_id)
888
919
  return Future().failure(e)
@@ -895,7 +926,7 @@ class BaseCoordinator(object):
895
926
  request = HeartbeatRequest[version](self.group_id,
896
927
  self._generation.generation_id,
897
928
  self._generation.member_id)
898
- heartbeat_log.debug("Heartbeat: %s[%s] %s", request.group, request.generation_id, request.member_id) # pylint: disable-msg=no-member
929
+ heartbeat_log.debug("Sending HeartbeatRequest to %s: %s", self.coordinator_id, request)
899
930
  future = Future()
900
931
  _f = self._client.send(self.coordinator_id, request)
901
932
  _f.add_callback(self._handle_heartbeat_response, future, time.time())
@@ -906,10 +937,10 @@ class BaseCoordinator(object):
906
937
  def _handle_heartbeat_response(self, future, send_time, response):
907
938
  if self._sensors:
908
939
  self._sensors.heartbeat_latency.record((time.time() - send_time) * 1000)
940
+ heartbeat_log.debug("Received heartbeat response for group %s: %s",
941
+ self.group_id, response)
909
942
  error_type = Errors.for_code(response.error_code)
910
943
  if error_type is Errors.NoError:
911
- heartbeat_log.debug("Received successful heartbeat response for group %s",
912
- self.group_id)
913
944
  future.success(None)
914
945
  elif error_type in (Errors.CoordinatorNotAvailableError,
915
946
  Errors.NotCoordinatorError):
@@ -1054,20 +1085,15 @@ class HeartbeatThread(threading.Thread):
1054
1085
  heartbeat_log.debug('Heartbeat thread closed')
1055
1086
 
1056
1087
  def _run_once(self):
1057
- with self.coordinator._client._lock, self.coordinator._lock:
1058
- if self.enabled and self.coordinator.state is MemberState.STABLE:
1059
- # TODO: When consumer.wakeup() is implemented, we need to
1060
- # disable here to prevent propagating an exception to this
1061
- # heartbeat thread
1062
- # must get client._lock, or maybe deadlock at heartbeat
1063
- # failure callback in consumer poll
1064
- self.coordinator._client.poll(timeout_ms=0)
1065
-
1066
- with self.coordinator._lock:
1088
+ self.coordinator._client._lock.acquire()
1089
+ self.coordinator._lock.acquire()
1090
+ try:
1067
1091
  if not self.enabled:
1068
1092
  heartbeat_log.debug('Heartbeat disabled. Waiting')
1093
+ self.coordinator._client._lock.release()
1069
1094
  self.coordinator._lock.wait()
1070
- heartbeat_log.debug('Heartbeat re-enabled.')
1095
+ if self.enabled:
1096
+ heartbeat_log.debug('Heartbeat re-enabled.')
1071
1097
  return
1072
1098
 
1073
1099
  if self.coordinator.state is not MemberState.STABLE:
@@ -1078,14 +1104,24 @@ class HeartbeatThread(threading.Thread):
1078
1104
  self.disable()
1079
1105
  return
1080
1106
 
1107
+ # TODO: When consumer.wakeup() is implemented, we need to
1108
+ # disable here to prevent propagating an exception to this
1109
+ # heartbeat thread
1110
+ self.coordinator._client.poll(timeout_ms=0)
1111
+
1081
1112
  if self.coordinator.coordinator_unknown():
1082
1113
  future = self.coordinator.lookup_coordinator()
1083
1114
  if not future.is_done or future.failed():
1084
1115
  # the immediate future check ensures that we backoff
1085
1116
  # properly in the case that no brokers are available
1086
1117
  # to connect to (and the future is automatically failed).
1118
+ self.coordinator._client._lock.release()
1087
1119
  self.coordinator._lock.wait(self.coordinator.config['retry_backoff_ms'] / 1000)
1088
1120
 
1121
+ elif not self.coordinator.connected():
1122
+ self.coordinator._client._lock.release()
1123
+ self.coordinator._lock.wait(self.coordinator.config['retry_backoff_ms'] / 1000)
1124
+
1089
1125
  elif self.coordinator.heartbeat.session_timeout_expired():
1090
1126
  # the session timeout has expired without seeing a
1091
1127
  # successful heartbeat, so we should probably make sure
@@ -1097,28 +1133,39 @@ class HeartbeatThread(threading.Thread):
1097
1133
  # the poll timeout has expired, which means that the
1098
1134
  # foreground thread has stalled in between calls to
1099
1135
  # poll(), so we explicitly leave the group.
1100
- heartbeat_log.warning('Heartbeat poll expired, leaving group')
1101
- ### XXX
1102
- # maybe_leave_group acquires client + coordinator lock;
1103
- # if we hold coordinator lock before calling, we risk deadlock
1104
- # release() is safe here because this is the last code in the current context
1105
- self.coordinator._lock.release()
1136
+ heartbeat_log.warning(
1137
+ "Consumer poll timeout has expired. This means the time between subsequent calls to poll()"
1138
+ " was longer than the configured max_poll_interval_ms, which typically implies that"
1139
+ " the poll loop is spending too much time processing messages. You can address this"
1140
+ " either by increasing max_poll_interval_ms or by reducing the maximum size of batches"
1141
+ " returned in poll() with max_poll_records."
1142
+ )
1106
1143
  self.coordinator.maybe_leave_group()
1107
1144
 
1108
1145
  elif not self.coordinator.heartbeat.should_heartbeat():
1109
- # poll again after waiting for the retry backoff in case
1110
- # the heartbeat failed or the coordinator disconnected
1111
- heartbeat_log.log(0, 'Not ready to heartbeat, waiting')
1112
- self.coordinator._lock.wait(self.coordinator.config['retry_backoff_ms'] / 1000)
1146
+ next_hb = self.coordinator.heartbeat.time_to_next_heartbeat()
1147
+ heartbeat_log.debug('Waiting %0.1f secs to send next heartbeat', next_hb)
1148
+ self.coordinator._client._lock.release()
1149
+ self.coordinator._lock.wait(next_hb)
1113
1150
 
1114
1151
  else:
1152
+ heartbeat_log.debug('Sending heartbeat for group %s %s', self.coordinator.group_id, self.coordinator._generation)
1115
1153
  self.coordinator.heartbeat.sent_heartbeat()
1116
1154
  future = self.coordinator._send_heartbeat_request()
1117
1155
  future.add_callback(self._handle_heartbeat_success)
1118
1156
  future.add_errback(self._handle_heartbeat_failure)
1119
1157
 
1158
+ finally:
1159
+ self.coordinator._lock.release()
1160
+ try:
1161
+ # Possibly released in block above to allow coordinator lock wait()
1162
+ self.coordinator._client._lock.release()
1163
+ except RuntimeError:
1164
+ pass
1165
+
1120
1166
  def _handle_heartbeat_success(self, result):
1121
1167
  with self.coordinator._lock:
1168
+ heartbeat_log.debug('Heartbeat success')
1122
1169
  self.coordinator.heartbeat.received_heartbeat()
1123
1170
 
1124
1171
  def _handle_heartbeat_failure(self, exception):
@@ -1129,8 +1176,10 @@ class HeartbeatThread(threading.Thread):
1129
1176
  # member in the group for as long as the duration of the
1130
1177
  # rebalance timeout. If we stop sending heartbeats, however,
1131
1178
  # then the session timeout may expire before we can rejoin.
1179
+ heartbeat_log.debug('Treating RebalanceInProgressError as successful heartbeat')
1132
1180
  self.coordinator.heartbeat.received_heartbeat()
1133
1181
  else:
1182
+ heartbeat_log.debug('Heartbeat failure: %s', exception)
1134
1183
  self.coordinator.heartbeat.fail_heartbeat()
1135
1184
  # wake up the thread if it's sleeping to reschedule the heartbeat
1136
1185
  self.coordinator._lock.notify()
@@ -608,6 +608,11 @@ class ConsumerCoordinator(BaseCoordinator):
608
608
  if node_id is None:
609
609
  return Future().failure(Errors.CoordinatorNotAvailableError)
610
610
 
611
+ # Verify node is ready
612
+ if not self._client.ready(node_id, metadata_priority=False):
613
+ log.debug("Node %s not ready -- failing offset commit request",
614
+ node_id)
615
+ return Future().failure(Errors.NodeNotReadyError)
611
616
 
612
617
  # create the offset commit request
613
618
  offset_data = collections.defaultdict(dict)
@@ -616,7 +621,7 @@ class ConsumerCoordinator(BaseCoordinator):
616
621
 
617
622
  version = self._client.api_version(OffsetCommitRequest, max_version=6)
618
623
  if version > 1 and self._subscription.partitions_auto_assigned():
619
- generation = self.generation()
624
+ generation = self.generation_if_stable()
620
625
  else:
621
626
  generation = Generation.NO_GENERATION
622
627
 
@@ -625,7 +630,18 @@ class ConsumerCoordinator(BaseCoordinator):
625
630
  # and let the user rejoin the group in poll()
626
631
  if generation is None:
627
632
  log.info("Failing OffsetCommit request since the consumer is not part of an active group")
628
- return Future().failure(Errors.CommitFailedError('Group rebalance in progress'))
633
+ if self.rebalance_in_progress():
634
+ # if the client knows it is already rebalancing, we can use RebalanceInProgressError instead of
635
+ # CommitFailedError to indicate this is not a fatal error
636
+ return Future().failure(Errors.RebalanceInProgressError(
637
+ "Offset commit cannot be completed since the"
638
+ " consumer is undergoing a rebalance for auto partition assignment. You can try completing the rebalance"
639
+ " by calling poll() and then retry the operation."))
640
+ else:
641
+ return Future().failure(Errors.CommitFailedError(
642
+ "Offset commit cannot be completed since the"
643
+ " consumer is not part of an active group for auto partition assignment; it is likely that the consumer"
644
+ " was kicked out of the group."))
629
645
 
630
646
  if version == 0:
631
647
  request = OffsetCommitRequest[version](
@@ -756,7 +772,7 @@ class ConsumerCoordinator(BaseCoordinator):
756
772
  # However, we do not need to reset generations and just request re-join, such that
757
773
  # if the caller decides to proceed and poll, it would still try to proceed and re-join normally.
758
774
  self.request_rejoin()
759
- future.failure(Errors.CommitFailedError('Group rebalance in progress'))
775
+ future.failure(Errors.CommitFailedError(error_type()))
760
776
  return
761
777
  elif error_type in (Errors.UnknownMemberIdError,
762
778
  Errors.IllegalGenerationError):
@@ -765,7 +781,7 @@ class ConsumerCoordinator(BaseCoordinator):
765
781
  log.warning("OffsetCommit for group %s failed: %s",
766
782
  self.group_id, error)
767
783
  self.reset_generation()
768
- future.failure(Errors.CommitFailedError())
784
+ future.failure(Errors.CommitFailedError(error_type()))
769
785
  return
770
786
  else:
771
787
  log.error("Group %s failed to commit partition %s at offset"
@@ -804,7 +820,7 @@ class ConsumerCoordinator(BaseCoordinator):
804
820
  return Future().failure(Errors.CoordinatorNotAvailableError)
805
821
 
806
822
  # Verify node is ready
807
- if not self._client.ready(node_id):
823
+ if not self._client.ready(node_id, metadata_priority=False):
808
824
  log.debug("Node %s not ready -- failing offset fetch request",
809
825
  node_id)
810
826
  return Future().failure(Errors.NodeNotReadyError)
@@ -24,14 +24,7 @@ class CommitFailedError(KafkaError):
24
24
  def __init__(self, *args):
25
25
  if not args:
26
26
  args = ("Commit cannot be completed since the group has already"
27
- " rebalanced and assigned the partitions to another member."
28
- " This means that the time between subsequent calls to poll()"
29
- " was longer than the configured max_poll_interval_ms, which"
30
- " typically implies that the poll loop is spending too much"
31
- " time message processing. You can address this either by"
32
- " increasing the rebalance timeout with max_poll_interval_ms,"
33
- " or by reducing the maximum size of batches returned in poll()"
34
- " with max_poll_records.",)
27
+ " rebalanced and assigned the partitions to another member.",)
35
28
  super(CommitFailedError, self).__init__(*args)
36
29
 
37
30
 
@@ -944,7 +944,7 @@ class KafkaProducer(object):
944
944
  """
945
945
  # add topic to metadata topic list if it is not there already.
946
946
  self._sender.add_topic(topic)
947
- timer = Timer(max_wait_ms, "Failed to update metadata after %.1f secs." % (max_wait_ms * 1000,))
947
+ timer = Timer(max_wait_ms, "Failed to update metadata after %.1f secs." % (max_wait_ms / 1000,))
948
948
  metadata_event = None
949
949
  while True:
950
950
  partitions = self._metadata.partitions_for_topic(topic)
@@ -962,7 +962,7 @@ class KafkaProducer(object):
962
962
  metadata_event.wait(timer.timeout_ms / 1000)
963
963
  if not metadata_event.is_set():
964
964
  raise Errors.KafkaTimeoutError(
965
- "Failed to update metadata after %.1f secs." % (max_wait_ms * 1000,))
965
+ "Failed to update metadata after %.1f secs." % (max_wait_ms / 1000,))
966
966
  elif topic in self._metadata.unauthorized_topics:
967
967
  raise Errors.TopicAuthorizationFailedError(set([topic]))
968
968
  else:
@@ -0,0 +1 @@
1
+ __version__ = '2.2.8'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kafka-python
3
- Version: 2.2.7
3
+ Version: 2.2.8
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
@@ -125,6 +125,20 @@ def test_group(kafka_broker, topic):
125
125
  for partition in range(num_partitions)])
126
126
  logging.info('Assignment looks good!')
127
127
 
128
+ logging.info('Verifying heartbeats')
129
+ while True:
130
+ for c in range(num_consumers):
131
+ heartbeat = consumers[c]._coordinator.heartbeat
132
+ last_hb = time.time() - 0.5
133
+ if (heartbeat.heartbeat_failed or
134
+ heartbeat.last_receive < last_hb or
135
+ heartbeat.last_reset > last_hb):
136
+ time.sleep(0.1)
137
+ continue
138
+ else:
139
+ break
140
+ logging.info('Heartbeats look good')
141
+
128
142
  finally:
129
143
  logging.info('Shutting down %s consumers', num_consumers)
130
144
  for c in range(num_consumers):
@@ -163,18 +177,28 @@ def test_heartbeat_thread(kafka_broker, topic):
163
177
  heartbeat_interval_ms=500)
164
178
 
165
179
  # poll until we have joined group / have assignment
180
+ start = time.time()
166
181
  while not consumer.assignment():
167
182
  consumer.poll(timeout_ms=100)
168
183
 
169
184
  assert consumer._coordinator.state is MemberState.STABLE
170
185
  last_poll = consumer._coordinator.heartbeat.last_poll
171
- last_beat = consumer._coordinator.heartbeat.last_send
186
+
187
+ # wait until we receive first heartbeat
188
+ while consumer._coordinator.heartbeat.last_receive < start:
189
+ time.sleep(0.1)
190
+
191
+ last_send = consumer._coordinator.heartbeat.last_send
192
+ last_recv = consumer._coordinator.heartbeat.last_receive
193
+ assert last_poll > start
194
+ assert last_send > start
195
+ assert last_recv > start
172
196
 
173
197
  timeout = time.time() + 30
174
198
  while True:
175
199
  if time.time() > timeout:
176
200
  raise RuntimeError('timeout waiting for heartbeat')
177
- if consumer._coordinator.heartbeat.last_send > last_beat:
201
+ if consumer._coordinator.heartbeat.last_receive > last_recv:
178
202
  break
179
203
  time.sleep(0.5)
180
204
 
@@ -658,6 +658,7 @@ def test_heartbeat(mocker, patched_coord):
658
658
  heartbeat.enable()
659
659
  patched_coord.state = MemberState.STABLE
660
660
  mocker.spy(patched_coord, '_send_heartbeat_request')
661
+ mocker.patch.object(patched_coord, 'connected', return_value=True)
661
662
  mocker.patch.object(patched_coord.heartbeat, 'should_heartbeat', return_value=True)
662
663
  heartbeat._run_once()
663
664
  assert patched_coord._send_heartbeat_request.call_count == 1
@@ -1 +0,0 @@
1
- __version__ = '2.2.7'
File without changes
File without changes
File without changes
File without changes
File without changes