kafka-python 2.2.3__py2.py3-none-any.whl → 2.2.4__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
kafka/client_async.py CHANGED
@@ -27,7 +27,7 @@ from kafka.metrics.stats import Avg, Count, Rate
27
27
  from kafka.metrics.stats.rate import TimeUnit
28
28
  from kafka.protocol.broker_api_versions import BROKER_API_VERSIONS
29
29
  from kafka.protocol.metadata import MetadataRequest
30
- from kafka.util import Dict, WeakMethod, ensure_valid_topic_name, timeout_ms_fn
30
+ from kafka.util import Dict, Timer, WeakMethod, ensure_valid_topic_name
31
31
  # Although this looks unused, it actually monkey-patches socket.socketpair()
32
32
  # and should be left in as long as we're using socket.socketpair() in this file
33
33
  from kafka.vendor import socketpair # noqa: F401
@@ -645,12 +645,8 @@ class KafkaClient(object):
645
645
  """
646
646
  if not isinstance(timeout_ms, (int, float, type(None))):
647
647
  raise TypeError('Invalid type for timeout: %s' % type(timeout_ms))
648
+ timer = Timer(timeout_ms)
648
649
 
649
- begin = time.time()
650
- if timeout_ms is not None:
651
- timeout_at = begin + (timeout_ms / 1000)
652
- else:
653
- timeout_at = begin + (self.config['request_timeout_ms'] / 1000)
654
650
  # Loop for futures, break after first loop if None
655
651
  responses = []
656
652
  while True:
@@ -675,7 +671,7 @@ class KafkaClient(object):
675
671
  if future is not None and future.is_done:
676
672
  timeout = 0
677
673
  else:
678
- user_timeout_ms = 1000 * max(0, timeout_at - time.time())
674
+ user_timeout_ms = timer.timeout_ms if timeout_ms is not None else self.config['request_timeout_ms']
679
675
  idle_connection_timeout_ms = self._idle_expiry_manager.next_check_ms()
680
676
  request_timeout_ms = self._next_ifr_request_timeout_ms()
681
677
  log.debug("Timeouts: user %f, metadata %f, idle connection %f, request %f", user_timeout_ms, metadata_timeout_ms, idle_connection_timeout_ms, request_timeout_ms)
@@ -698,7 +694,7 @@ class KafkaClient(object):
698
694
  break
699
695
  elif future.is_done:
700
696
  break
701
- elif timeout_ms is not None and time.time() >= timeout_at:
697
+ elif timeout_ms is not None and timer.expired:
702
698
  break
703
699
 
704
700
  return responses
@@ -1175,16 +1171,16 @@ class KafkaClient(object):
1175
1171
  This method is useful for implementing blocking behaviour on top of the non-blocking `NetworkClient`, use it with
1176
1172
  care.
1177
1173
  """
1178
- inner_timeout_ms = timeout_ms_fn(timeout_ms, None)
1174
+ timer = Timer(timeout_ms)
1179
1175
  self.poll(timeout_ms=0)
1180
1176
  if self.is_ready(node_id):
1181
1177
  return True
1182
1178
 
1183
- while not self.is_ready(node_id) and inner_timeout_ms() > 0:
1179
+ while not self.is_ready(node_id) and not timer.expired:
1184
1180
  if self.connection_failed(node_id):
1185
1181
  raise Errors.KafkaConnectionError("Connection to %s failed." % (node_id,))
1186
1182
  self.maybe_connect(node_id)
1187
- self.poll(timeout_ms=inner_timeout_ms())
1183
+ self.poll(timeout_ms=timer.timeout_ms)
1188
1184
  return self.is_ready(node_id)
1189
1185
 
1190
1186
  def send_and_receive(self, node_id, request):
kafka/consumer/fetcher.py CHANGED
@@ -19,7 +19,7 @@ from kafka.protocol.list_offsets import (
19
19
  from kafka.record import MemoryRecords
20
20
  from kafka.serializer import Deserializer
21
21
  from kafka.structs import TopicPartition, OffsetAndMetadata, OffsetAndTimestamp
22
- from kafka.util import timeout_ms_fn
22
+ from kafka.util import Timer
23
23
 
24
24
  log = logging.getLogger(__name__)
25
25
 
@@ -230,7 +230,7 @@ class Fetcher(six.Iterator):
230
230
  if not timestamps:
231
231
  return {}
232
232
 
233
- inner_timeout_ms = timeout_ms_fn(timeout_ms, 'Timeout fetching offsets')
233
+ timer = Timer(timeout_ms, "Failed to get offsets by timestamps in %s ms" % (timeout_ms,))
234
234
  timestamps = copy.copy(timestamps)
235
235
  fetched_offsets = dict()
236
236
  while True:
@@ -238,7 +238,7 @@ class Fetcher(six.Iterator):
238
238
  return {}
239
239
 
240
240
  future = self._send_list_offsets_requests(timestamps)
241
- self._client.poll(future=future, timeout_ms=inner_timeout_ms())
241
+ self._client.poll(future=future, timeout_ms=timer.timeout_ms)
242
242
 
243
243
  # Timeout w/o future completion
244
244
  if not future.is_done:
@@ -256,12 +256,17 @@ class Fetcher(six.Iterator):
256
256
 
257
257
  if future.exception.invalid_metadata or self._client.cluster.need_update:
258
258
  refresh_future = self._client.cluster.request_update()
259
- self._client.poll(future=refresh_future, timeout_ms=inner_timeout_ms())
259
+ self._client.poll(future=refresh_future, timeout_ms=timer.timeout_ms)
260
260
 
261
261
  if not future.is_done:
262
262
  break
263
263
  else:
264
- time.sleep(inner_timeout_ms(self.config['retry_backoff_ms']) / 1000)
264
+ if timer.timeout_ms is None or timer.timeout_ms > self.config['retry_backoff_ms']:
265
+ time.sleep(self.config['retry_backoff_ms'] / 1000)
266
+ else:
267
+ time.sleep(timer.timeout_ms / 1000)
268
+
269
+ timer.maybe_raise()
265
270
 
266
271
  raise Errors.KafkaTimeoutError(
267
272
  "Failed to get offsets by timestamps in %s ms" % (timeout_ms,))
@@ -418,7 +423,7 @@ class Fetcher(six.Iterator):
418
423
  expire_at = time.time() + self.config['request_timeout_ms'] / 1000
419
424
  self._subscriptions.set_reset_pending(partitions, expire_at)
420
425
 
421
- def on_success(result):
426
+ def on_success(timestamps_and_epochs, result):
422
427
  fetched_offsets, partitions_to_retry = result
423
428
  if partitions_to_retry:
424
429
  self._subscriptions.reset_failed(partitions_to_retry, time.time() + self.config['retry_backoff_ms'] / 1000)
@@ -428,7 +433,7 @@ class Fetcher(six.Iterator):
428
433
  ts, _epoch = timestamps_and_epochs[partition]
429
434
  self._reset_offset_if_needed(partition, ts, offset.offset)
430
435
 
431
- def on_failure(error):
436
+ def on_failure(partitions, error):
432
437
  self._subscriptions.reset_failed(partitions, time.time() + self.config['retry_backoff_ms'] / 1000)
433
438
  self._client.cluster.request_update()
434
439
 
@@ -439,8 +444,8 @@ class Fetcher(six.Iterator):
439
444
  log.error("Discarding error in ListOffsetResponse because another error is pending: %s", error)
440
445
 
441
446
  future = self._send_list_offsets_request(node_id, timestamps_and_epochs)
442
- future.add_callback(on_success)
443
- future.add_errback(on_failure)
447
+ future.add_callback(on_success, timestamps_and_epochs)
448
+ future.add_errback(on_failure, partitions)
444
449
 
445
450
  def _send_list_offsets_requests(self, timestamps):
446
451
  """Fetch offsets for each partition in timestamps dict. This may send
kafka/consumer/group.py CHANGED
@@ -18,7 +18,7 @@ from kafka.coordinator.assignors.roundrobin import RoundRobinPartitionAssignor
18
18
  from kafka.metrics import MetricConfig, Metrics
19
19
  from kafka.protocol.list_offsets import OffsetResetStrategy
20
20
  from kafka.structs import OffsetAndMetadata, TopicPartition
21
- from kafka.util import timeout_ms_fn
21
+ from kafka.util import Timer
22
22
  from kafka.version import __version__
23
23
 
24
24
  log = logging.getLogger(__name__)
@@ -679,41 +679,40 @@ class KafkaConsumer(six.Iterator):
679
679
  assert not self._closed, 'KafkaConsumer is closed'
680
680
 
681
681
  # Poll for new data until the timeout expires
682
- inner_timeout_ms = timeout_ms_fn(timeout_ms, None)
682
+ timer = Timer(timeout_ms)
683
683
  while not self._closed:
684
- records = self._poll_once(inner_timeout_ms(), max_records, update_offsets=update_offsets)
684
+ records = self._poll_once(timer, max_records, update_offsets=update_offsets)
685
685
  if records:
686
686
  return records
687
-
688
- if inner_timeout_ms() <= 0:
687
+ elif timer.expired:
689
688
  break
690
-
691
689
  return {}
692
690
 
693
- def _poll_once(self, timeout_ms, max_records, update_offsets=True):
691
+ def _poll_once(self, timer, max_records, update_offsets=True):
694
692
  """Do one round of polling. In addition to checking for new data, this does
695
693
  any needed heart-beating, auto-commits, and offset updates.
696
694
 
697
695
  Arguments:
698
- timeout_ms (int): The maximum time in milliseconds to block.
696
+ timer (Timer): The maximum time in milliseconds to block.
699
697
 
700
698
  Returns:
701
699
  dict: Map of topic to list of records (may be empty).
702
700
  """
703
- inner_timeout_ms = timeout_ms_fn(timeout_ms, None)
704
- if not self._coordinator.poll(timeout_ms=inner_timeout_ms()):
701
+ if not self._coordinator.poll(timeout_ms=timer.timeout_ms):
705
702
  return {}
706
703
 
707
- has_all_fetch_positions = self._update_fetch_positions(timeout_ms=inner_timeout_ms())
704
+ has_all_fetch_positions = self._update_fetch_positions(timeout_ms=timer.timeout_ms)
708
705
 
709
706
  # If data is available already, e.g. from a previous network client
710
707
  # poll() call to commit, then just return it immediately
711
708
  records, partial = self._fetcher.fetched_records(max_records, update_offsets=update_offsets)
709
+ log.debug('Fetched records: %s, %s', records, partial)
712
710
  # Before returning the fetched records, we can send off the
713
711
  # next round of fetches and avoid block waiting for their
714
712
  # responses to enable pipelining while the user is handling the
715
713
  # fetched records.
716
714
  if not partial:
715
+ log.debug("Sending fetches")
717
716
  futures = self._fetcher.send_fetches()
718
717
  if len(futures):
719
718
  self._client.poll(timeout_ms=0)
@@ -723,7 +722,7 @@ class KafkaConsumer(six.Iterator):
723
722
 
724
723
  # We do not want to be stuck blocking in poll if we are missing some positions
725
724
  # since the offset lookup may be backing off after a failure
726
- poll_timeout_ms = inner_timeout_ms(self._coordinator.time_to_next_poll() * 1000)
725
+ poll_timeout_ms = min(timer.timeout_ms, self._coordinator.time_to_next_poll() * 1000)
727
726
  if not has_all_fetch_positions:
728
727
  poll_timeout_ms = min(poll_timeout_ms, self.config['retry_backoff_ms'])
729
728
 
@@ -749,15 +748,14 @@ class KafkaConsumer(six.Iterator):
749
748
  raise TypeError('partition must be a TopicPartition namedtuple')
750
749
  assert self._subscription.is_assigned(partition), 'Partition is not assigned'
751
750
 
752
- inner_timeout_ms = timeout_ms_fn(timeout_ms, 'Timeout retrieving partition position')
751
+ timer = Timer(timeout_ms)
753
752
  position = self._subscription.assignment[partition].position
754
- try:
755
- while position is None:
756
- # batch update fetch positions for any partitions without a valid position
757
- self._update_fetch_positions(timeout_ms=inner_timeout_ms())
753
+ while position is None:
754
+ # batch update fetch positions for any partitions without a valid position
755
+ if self._update_fetch_positions(timeout_ms=timer.timeout_ms):
758
756
  position = self._subscription.assignment[partition].position
759
- except KafkaTimeoutError:
760
- return None
757
+ elif timer.expired:
758
+ return None
761
759
  else:
762
760
  return position.offset
763
761
 
kafka/coordinator/base.py CHANGED
@@ -16,7 +16,7 @@ from kafka.metrics import AnonMeasurable
16
16
  from kafka.metrics.stats import Avg, Count, Max, Rate
17
17
  from kafka.protocol.find_coordinator import FindCoordinatorRequest
18
18
  from kafka.protocol.group import HeartbeatRequest, JoinGroupRequest, LeaveGroupRequest, SyncGroupRequest, DEFAULT_GENERATION_ID, UNKNOWN_MEMBER_ID
19
- from kafka.util import timeout_ms_fn
19
+ from kafka.util import Timer
20
20
 
21
21
  log = logging.getLogger('kafka.coordinator')
22
22
 
@@ -256,9 +256,9 @@ class BaseCoordinator(object):
256
256
  timeout_ms (numeric, optional): Maximum number of milliseconds to
257
257
  block waiting to find coordinator. Default: None.
258
258
 
259
- Raises: KafkaTimeoutError if timeout_ms is not None
259
+ Returns: True is coordinator found before timeout_ms, else False
260
260
  """
261
- inner_timeout_ms = timeout_ms_fn(timeout_ms, 'Timeout attempting to find group coordinator')
261
+ timer = Timer(timeout_ms)
262
262
  with self._client._lock, self._lock:
263
263
  while self.coordinator_unknown():
264
264
 
@@ -272,27 +272,37 @@ class BaseCoordinator(object):
272
272
  else:
273
273
  self.coordinator_id = maybe_coordinator_id
274
274
  self._client.maybe_connect(self.coordinator_id)
275
- continue
275
+ if timer.expired:
276
+ return False
277
+ else:
278
+ continue
276
279
  else:
277
280
  future = self.lookup_coordinator()
278
281
 
279
- self._client.poll(future=future, timeout_ms=inner_timeout_ms())
282
+ self._client.poll(future=future, timeout_ms=timer.timeout_ms)
280
283
 
281
284
  if not future.is_done:
282
- raise Errors.KafkaTimeoutError()
285
+ return False
283
286
 
284
287
  if future.failed():
285
288
  if future.retriable():
286
289
  if getattr(future.exception, 'invalid_metadata', False):
287
290
  log.debug('Requesting metadata for group coordinator request: %s', future.exception)
288
291
  metadata_update = self._client.cluster.request_update()
289
- self._client.poll(future=metadata_update, timeout_ms=inner_timeout_ms())
292
+ self._client.poll(future=metadata_update, timeout_ms=timer.timeout_ms)
290
293
  if not metadata_update.is_done:
291
- raise Errors.KafkaTimeoutError()
294
+ return False
292
295
  else:
293
- time.sleep(inner_timeout_ms(self.config['retry_backoff_ms']) / 1000)
296
+ if timeout_ms is None or timer.timeout_ms > self.config['retry_backoff_ms']:
297
+ time.sleep(self.config['retry_backoff_ms'] / 1000)
298
+ else:
299
+ time.sleep(timer.timeout_ms / 1000)
294
300
  else:
295
301
  raise future.exception # pylint: disable-msg=raising-bad-type
302
+ if timer.expired:
303
+ return False
304
+ else:
305
+ return True
296
306
 
297
307
  def _reset_find_coordinator_future(self, result):
298
308
  self._find_coordinator_future = None
@@ -407,21 +417,23 @@ class BaseCoordinator(object):
407
417
  timeout_ms (numeric, optional): Maximum number of milliseconds to
408
418
  block waiting to join group. Default: None.
409
419
 
410
- Raises: KafkaTimeoutError if timeout_ms is not None
420
+ Returns: True if group initialized before timeout_ms, else False
411
421
  """
412
422
  if self.config['api_version'] < (0, 9):
413
423
  raise Errors.UnsupportedVersionError('Group Coordinator APIs require 0.9+ broker')
414
- inner_timeout_ms = timeout_ms_fn(timeout_ms, 'Timeout attempting to join consumer group')
415
- self.ensure_coordinator_ready(timeout_ms=inner_timeout_ms())
424
+ timer = Timer(timeout_ms)
425
+ if not self.ensure_coordinator_ready(timeout_ms=timer.timeout_ms):
426
+ return False
416
427
  self._start_heartbeat_thread()
417
- self.join_group(timeout_ms=inner_timeout_ms())
428
+ return self.join_group(timeout_ms=timer.timeout_ms)
418
429
 
419
430
  def join_group(self, timeout_ms=None):
420
431
  if self.config['api_version'] < (0, 9):
421
432
  raise Errors.UnsupportedVersionError('Group Coordinator APIs require 0.9+ broker')
422
- inner_timeout_ms = timeout_ms_fn(timeout_ms, 'Timeout attempting to join consumer group')
433
+ timer = Timer(timeout_ms)
423
434
  while self.need_rejoin():
424
- self.ensure_coordinator_ready(timeout_ms=inner_timeout_ms())
435
+ if not self.ensure_coordinator_ready(timeout_ms=timer.timeout_ms):
436
+ return False
425
437
 
426
438
  # call on_join_prepare if needed. We set a flag
427
439
  # to make sure that we do not call it a second
@@ -434,7 +446,7 @@ class BaseCoordinator(object):
434
446
  if not self.rejoining:
435
447
  self._on_join_prepare(self._generation.generation_id,
436
448
  self._generation.member_id,
437
- timeout_ms=inner_timeout_ms())
449
+ timeout_ms=timer.timeout_ms)
438
450
  self.rejoining = True
439
451
 
440
452
  # fence off the heartbeat thread explicitly so that it cannot
@@ -449,16 +461,19 @@ class BaseCoordinator(object):
449
461
  while not self.coordinator_unknown():
450
462
  if not self._client.in_flight_request_count(self.coordinator_id):
451
463
  break
452
- self._client.poll(timeout_ms=inner_timeout_ms(200))
464
+ poll_timeout_ms = 200 if timer.timeout_ms is None or timer.timeout_ms > 200 else timer.timeout_ms
465
+ self._client.poll(timeout_ms=poll_timeout_ms)
466
+ if timer.expired:
467
+ return False
453
468
  else:
454
469
  continue
455
470
 
456
471
  future = self._initiate_join_group()
457
- self._client.poll(future=future, timeout_ms=inner_timeout_ms())
472
+ self._client.poll(future=future, timeout_ms=timer.timeout_ms)
458
473
  if future.is_done:
459
474
  self._reset_join_group_future()
460
475
  else:
461
- raise Errors.KafkaTimeoutError()
476
+ return False
462
477
 
463
478
  if future.succeeded():
464
479
  self.rejoining = False
@@ -467,6 +482,7 @@ class BaseCoordinator(object):
467
482
  self._generation.member_id,
468
483
  self._generation.protocol,
469
484
  future.value)
485
+ return True
470
486
  else:
471
487
  exception = future.exception
472
488
  if isinstance(exception, (Errors.UnknownMemberIdError,
@@ -476,7 +492,13 @@ class BaseCoordinator(object):
476
492
  continue
477
493
  elif not future.retriable():
478
494
  raise exception # pylint: disable-msg=raising-bad-type
479
- time.sleep(inner_timeout_ms(self.config['retry_backoff_ms']) / 1000)
495
+ elif timer.expired:
496
+ return False
497
+ else:
498
+ if timer.timeout_ms is None or timer.timeout_ms > self.config['retry_backoff_ms']:
499
+ time.sleep(self.config['retry_backoff_ms'] / 1000)
500
+ else:
501
+ time.sleep(timer.timeout_ms / 1000)
480
502
 
481
503
  def _send_join_group_request(self):
482
504
  """Join the group and return the assignment for the next generation.
@@ -19,7 +19,7 @@ from kafka.metrics import AnonMeasurable
19
19
  from kafka.metrics.stats import Avg, Count, Max, Rate
20
20
  from kafka.protocol.commit import OffsetCommitRequest, OffsetFetchRequest
21
21
  from kafka.structs import OffsetAndMetadata, TopicPartition
22
- from kafka.util import timeout_ms_fn, WeakMethod
22
+ from kafka.util import Timer, WeakMethod
23
23
 
24
24
 
25
25
  log = logging.getLogger(__name__)
@@ -95,6 +95,7 @@ class ConsumerCoordinator(BaseCoordinator):
95
95
  self.auto_commit_interval = self.config['auto_commit_interval_ms'] / 1000
96
96
  self.next_auto_commit_deadline = None
97
97
  self.completed_offset_commits = collections.deque()
98
+ self._offset_fetch_futures = dict()
98
99
 
99
100
  if self.config['default_offset_commit_callback'] is None:
100
101
  self.config['default_offset_commit_callback'] = self._default_offset_commit_callback
@@ -269,10 +270,11 @@ class ConsumerCoordinator(BaseCoordinator):
269
270
  if self.group_id is None:
270
271
  return True
271
272
 
272
- inner_timeout_ms = timeout_ms_fn(timeout_ms, 'Timeout in coordinator.poll')
273
+ timer = Timer(timeout_ms)
273
274
  try:
274
275
  self._invoke_completed_offset_commit_callbacks()
275
- self.ensure_coordinator_ready(timeout_ms=inner_timeout_ms())
276
+ if not self.ensure_coordinator_ready(timeout_ms=timer.timeout_ms):
277
+ return False
276
278
 
277
279
  if self.config['api_version'] >= (0, 9) and self._subscription.partitions_auto_assigned():
278
280
  if self.need_rejoin():
@@ -289,9 +291,12 @@ class ConsumerCoordinator(BaseCoordinator):
289
291
  # description of the problem.
290
292
  if self._subscription.subscribed_pattern:
291
293
  metadata_update = self._client.cluster.request_update()
292
- self._client.poll(future=metadata_update, timeout_ms=inner_timeout_ms())
294
+ self._client.poll(future=metadata_update, timeout_ms=timer.timeout_ms)
295
+ if not metadata_update.is_done:
296
+ return False
293
297
 
294
- self.ensure_active_group(timeout_ms=inner_timeout_ms())
298
+ if not self.ensure_active_group(timeout_ms=timer.timeout_ms):
299
+ return False
295
300
 
296
301
  self.poll_heartbeat()
297
302
 
@@ -395,10 +400,14 @@ class ConsumerCoordinator(BaseCoordinator):
395
400
  def refresh_committed_offsets_if_needed(self, timeout_ms=None):
396
401
  """Fetch committed offsets for assigned partitions."""
397
402
  missing_fetch_positions = set(self._subscription.missing_fetch_positions())
398
- offsets = self.fetch_committed_offsets(missing_fetch_positions, timeout_ms=timeout_ms)
403
+ try:
404
+ offsets = self.fetch_committed_offsets(missing_fetch_positions, timeout_ms=timeout_ms)
405
+ except Errors.KafkaTimeoutError:
406
+ return False
399
407
  for partition, offset in six.iteritems(offsets):
400
- log.debug("Setting offset for partition %s to the committed offset %s", partition, offset.offset);
408
+ log.debug("Setting offset for partition %s to the committed offset %s", partition, offset.offset)
401
409
  self._subscription.seek(partition, offset.offset)
410
+ return True
402
411
 
403
412
  def fetch_committed_offsets(self, partitions, timeout_ms=None):
404
413
  """Fetch the current committed offsets for specified partitions
@@ -415,24 +424,35 @@ class ConsumerCoordinator(BaseCoordinator):
415
424
  if not partitions:
416
425
  return {}
417
426
 
418
- inner_timeout_ms = timeout_ms_fn(timeout_ms, 'Timeout in coordinator.fetch_committed_offsets')
427
+ future_key = frozenset(partitions)
428
+ timer = Timer(timeout_ms)
419
429
  while True:
420
- self.ensure_coordinator_ready(timeout_ms=inner_timeout_ms())
430
+ self.ensure_coordinator_ready(timeout_ms=timer.timeout_ms)
421
431
 
422
432
  # contact coordinator to fetch committed offsets
423
- future = self._send_offset_fetch_request(partitions)
424
- self._client.poll(future=future, timeout_ms=inner_timeout_ms())
433
+ if future_key in self._offset_fetch_futures:
434
+ future = self._offset_fetch_futures[future_key]
435
+ else:
436
+ future = self._send_offset_fetch_request(partitions)
437
+ self._offset_fetch_futures[future_key] = future
425
438
 
426
- if not future.is_done:
427
- raise Errors.KafkaTimeoutError()
439
+ self._client.poll(future=future, timeout_ms=timer.timeout_ms)
428
440
 
429
- if future.succeeded():
430
- return future.value
441
+ if future.is_done:
442
+ del self._offset_fetch_futures[future_key]
431
443
 
432
- if not future.retriable():
433
- raise future.exception # pylint: disable-msg=raising-bad-type
444
+ if future.succeeded():
445
+ return future.value
434
446
 
435
- time.sleep(inner_timeout_ms(self.config['retry_backoff_ms']) / 1000)
447
+ elif not future.retriable():
448
+ raise future.exception # pylint: disable-msg=raising-bad-type
449
+
450
+ # future failed but is retriable, or is not done yet
451
+ if timer.timeout_ms is None or timer.timeout_ms > self.config['retry_backoff_ms']:
452
+ time.sleep(self.config['retry_backoff_ms'] / 1000)
453
+ else:
454
+ time.sleep(timer.timeout_ms / 1000)
455
+ timer.maybe_raise()
436
456
 
437
457
  def close(self, autocommit=True, timeout_ms=None):
438
458
  """Close the coordinator, leave the current group,
@@ -523,23 +543,26 @@ class ConsumerCoordinator(BaseCoordinator):
523
543
  if not offsets:
524
544
  return
525
545
 
526
- inner_timeout_ms = timeout_ms_fn(timeout_ms, 'Timeout in coordinator.poll')
546
+ timer = Timer(timeout_ms)
527
547
  while True:
528
- self.ensure_coordinator_ready(timeout_ms=inner_timeout_ms())
548
+ self.ensure_coordinator_ready(timeout_ms=timer.timeout_ms)
529
549
 
530
550
  future = self._send_offset_commit_request(offsets)
531
- self._client.poll(future=future, timeout_ms=inner_timeout_ms())
532
-
533
- if not future.is_done:
534
- raise Errors.KafkaTimeoutError()
551
+ self._client.poll(future=future, timeout_ms=timer.timeout_ms)
535
552
 
536
- if future.succeeded():
537
- return future.value
553
+ if future.is_done:
554
+ if future.succeeded():
555
+ return future.value
538
556
 
539
- if not future.retriable():
540
- raise future.exception # pylint: disable-msg=raising-bad-type
557
+ elif not future.retriable():
558
+ raise future.exception # pylint: disable-msg=raising-bad-type
541
559
 
542
- time.sleep(inner_timeout_ms(self.config['retry_backoff_ms']) / 1000)
560
+ # future failed but is retriable, or it is still pending
561
+ if timer.timeout_ms is None or timer.timeout_ms > self.config['retry_backoff_ms']:
562
+ time.sleep(self.config['retry_backoff_ms'] / 1000)
563
+ else:
564
+ time.sleep(timer.timeout_ms / 1000)
565
+ timer.maybe_raise()
543
566
 
544
567
  def _maybe_auto_commit_offsets_sync(self, timeout_ms=None):
545
568
  if self.config['enable_auto_commit']:
@@ -591,18 +614,19 @@ class ConsumerCoordinator(BaseCoordinator):
591
614
  for tp, offset in six.iteritems(offsets):
592
615
  offset_data[tp.topic][tp.partition] = offset
593
616
 
594
- if self._subscription.partitions_auto_assigned():
595
- generation = self.generation() or Generation.NO_GENERATION
617
+ version = self._client.api_version(OffsetCommitRequest, max_version=6)
618
+ if version > 1 and self._subscription.partitions_auto_assigned():
619
+ generation = self.generation()
596
620
  else:
597
621
  generation = Generation.NO_GENERATION
598
622
 
599
623
  # if the generation is None, we are not part of an active group
600
624
  # (and we expect to be). The only thing we can do is fail the commit
601
625
  # and let the user rejoin the group in poll()
602
- if self.config['api_version'] >= (0, 9) and generation is None:
603
- return Future().failure(Errors.CommitFailedError())
626
+ if generation is None:
627
+ 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'))
604
629
 
605
- version = self._client.api_version(OffsetCommitRequest, max_version=6)
606
630
  if version == 0:
607
631
  request = OffsetCommitRequest[version](
608
632
  self.group_id,
@@ -724,13 +748,22 @@ class ConsumerCoordinator(BaseCoordinator):
724
748
  self.coordinator_dead(error_type())
725
749
  future.failure(error_type(self.group_id))
726
750
  return
751
+ elif error_type is Errors.RebalanceInProgressError:
752
+ # Consumer never tries to commit offset in between join-group and sync-group,
753
+ # and hence on broker-side it is not expected to see a commit offset request
754
+ # during CompletingRebalance phase; if it ever happens then broker would return
755
+ # this error. In this case we should just treat as a fatal CommitFailed exception.
756
+ # However, we do not need to reset generations and just request re-join, such that
757
+ # if the caller decides to proceed and poll, it would still try to proceed and re-join normally.
758
+ self.request_rejoin()
759
+ future.failure(Errors.CommitFailedError('Group rebalance in progress'))
760
+ return
727
761
  elif error_type in (Errors.UnknownMemberIdError,
728
- Errors.IllegalGenerationError,
729
- Errors.RebalanceInProgressError):
730
- # need to re-join group
762
+ Errors.IllegalGenerationError):
763
+ # need reset generation and re-join group
731
764
  error = error_type(self.group_id)
732
- log.debug("OffsetCommit for group %s failed: %s",
733
- self.group_id, error)
765
+ log.warning("OffsetCommit for group %s failed: %s",
766
+ self.group_id, error)
734
767
  self.reset_generation()
735
768
  future.failure(Errors.CommitFailedError())
736
769
  return
kafka/errors.py CHANGED
@@ -21,18 +21,18 @@ class Cancelled(KafkaError):
21
21
 
22
22
 
23
23
  class CommitFailedError(KafkaError):
24
- def __init__(self, *args, **kwargs):
25
- super(CommitFailedError, self).__init__(
26
- """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.
35
- """, *args, **kwargs)
24
+ def __init__(self, *args):
25
+ if not args:
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.",)
35
+ super(CommitFailedError, self).__init__(*args)
36
36
 
37
37
 
38
38
  class IllegalArgumentError(KafkaError):
kafka/producer/kafka.py CHANGED
@@ -5,7 +5,6 @@ import copy
5
5
  import logging
6
6
  import socket
7
7
  import threading
8
- import time
9
8
  import warnings
10
9
  import weakref
11
10
 
@@ -24,7 +23,7 @@ from kafka.record.default_records import DefaultRecordBatchBuilder
24
23
  from kafka.record.legacy_records import LegacyRecordBatchBuilder
25
24
  from kafka.serializer import Serializer
26
25
  from kafka.structs import TopicPartition
27
- from kafka.util import ensure_valid_topic_name
26
+ from kafka.util import Timer, ensure_valid_topic_name
28
27
 
29
28
 
30
29
  log = logging.getLogger(__name__)
@@ -664,8 +663,7 @@ class KafkaProducer(object):
664
663
 
665
664
  def partitions_for(self, topic):
666
665
  """Returns set of all known partitions for the topic."""
667
- max_wait = self.config['max_block_ms'] / 1000
668
- return self._wait_on_metadata(topic, max_wait)
666
+ return self._wait_on_metadata(topic, self.config['max_block_ms'])
669
667
 
670
668
  @classmethod
671
669
  def max_usable_produce_magic(cls, api_version):
@@ -835,14 +833,11 @@ class KafkaProducer(object):
835
833
  assert not (value is None and key is None), 'Need at least one: key or value'
836
834
  ensure_valid_topic_name(topic)
837
835
  key_bytes = value_bytes = None
836
+ timer = Timer(self.config['max_block_ms'], "Failed to assign partition for message in max_block_ms.")
838
837
  try:
839
838
  assigned_partition = None
840
- elapsed = 0.0
841
- begin = time.time()
842
- timeout = self.config['max_block_ms'] / 1000
843
- while assigned_partition is None and elapsed < timeout:
844
- elapsed = time.time() - begin
845
- self._wait_on_metadata(topic, timeout - elapsed)
839
+ while assigned_partition is None and not timer.expired:
840
+ self._wait_on_metadata(topic, timer.timeout_ms)
846
841
 
847
842
  key_bytes = self._serialize(
848
843
  self.config['key_serializer'],
@@ -856,7 +851,7 @@ class KafkaProducer(object):
856
851
  assigned_partition = self._partition(topic, partition, key, value,
857
852
  key_bytes, value_bytes)
858
853
  if assigned_partition is None:
859
- raise Errors.KafkaTimeoutError("Failed to assign partition for message after %s secs." % timeout)
854
+ raise Errors.KafkaTimeoutError("Failed to assign partition for message after %s secs." % timer.elapsed_ms / 1000)
860
855
  else:
861
856
  partition = assigned_partition
862
857
 
@@ -931,7 +926,7 @@ class KafkaProducer(object):
931
926
  " the maximum request size you have configured with the"
932
927
  " max_request_size configuration" % (size,))
933
928
 
934
- def _wait_on_metadata(self, topic, max_wait):
929
+ def _wait_on_metadata(self, topic, max_wait_ms):
935
930
  """
936
931
  Wait for cluster metadata including partitions for the given topic to
937
932
  be available.
@@ -949,36 +944,29 @@ class KafkaProducer(object):
949
944
  """
950
945
  # add topic to metadata topic list if it is not there already.
951
946
  self._sender.add_topic(topic)
952
- begin = time.time()
953
- elapsed = 0.0
947
+ timer = Timer(max_wait_ms, "Failed to update metadata after %.1f secs." % (max_wait_ms * 1000,))
954
948
  metadata_event = None
955
949
  while True:
956
950
  partitions = self._metadata.partitions_for_topic(topic)
957
951
  if partitions is not None:
958
952
  return partitions
959
-
960
- if elapsed >= max_wait:
961
- raise Errors.KafkaTimeoutError(
962
- "Failed to update metadata after %.1f secs." % (max_wait,))
963
-
953
+ timer.maybe_raise()
964
954
  if not metadata_event:
965
955
  metadata_event = threading.Event()
966
956
 
967
957
  log.debug("%s: Requesting metadata update for topic %s", str(self), topic)
968
-
969
958
  metadata_event.clear()
970
959
  future = self._metadata.request_update()
971
960
  future.add_both(lambda e, *args: e.set(), metadata_event)
972
961
  self._sender.wakeup()
973
- metadata_event.wait(max_wait - elapsed)
962
+ metadata_event.wait(timer.timeout_ms / 1000)
974
963
  if not metadata_event.is_set():
975
964
  raise Errors.KafkaTimeoutError(
976
- "Failed to update metadata after %.1f secs." % (max_wait,))
965
+ "Failed to update metadata after %.1f secs." % (max_wait_ms * 1000,))
977
966
  elif topic in self._metadata.unauthorized_topics:
978
967
  raise Errors.TopicAuthorizationFailedError(set([topic]))
979
968
  else:
980
- elapsed = time.time() - begin
981
- log.debug("%s: _wait_on_metadata woke after %s secs.", str(self), elapsed)
969
+ log.debug("%s: _wait_on_metadata woke after %s secs.", str(self), timer.elapsed_ms / 1000)
982
970
 
983
971
  def _serialize(self, f, topic, data):
984
972
  if not f:
kafka/util.py CHANGED
@@ -1,4 +1,4 @@
1
- from __future__ import absolute_import
1
+ from __future__ import absolute_import, division
2
2
 
3
3
  import binascii
4
4
  import re
@@ -25,24 +25,44 @@ else:
25
25
  from binascii import crc32 # noqa: F401
26
26
 
27
27
 
28
- def timeout_ms_fn(timeout_ms, error_message):
29
- elapsed = 0.0 # noqa: F841
30
- begin = time.time()
31
- def inner_timeout_ms(fallback=None):
32
- if timeout_ms is None:
33
- return fallback
34
- elapsed = (time.time() - begin) * 1000
35
- if elapsed >= timeout_ms:
36
- if error_message is not None:
37
- raise KafkaTimeoutError(error_message)
38
- else:
39
- return 0
40
- ret = max(0, timeout_ms - elapsed)
41
- if fallback is not None:
42
- return min(ret, fallback)
43
- return ret
44
- return inner_timeout_ms
45
-
28
+ class Timer:
29
+ __slots__ = ('_start_at', '_expire_at', '_timeout_ms', '_error_message')
30
+
31
+ def __init__(self, timeout_ms, error_message=None, start_at=None):
32
+ self._timeout_ms = timeout_ms
33
+ self._start_at = start_at or time.time()
34
+ if timeout_ms is not None:
35
+ self._expire_at = self._start_at + timeout_ms / 1000
36
+ else:
37
+ self._expire_at = float('inf')
38
+ self._error_message = error_message
39
+
40
+ @property
41
+ def expired(self):
42
+ return time.time() >= self._expire_at
43
+
44
+ @property
45
+ def timeout_ms(self):
46
+ if self._timeout_ms is None:
47
+ return None
48
+ elif self._expire_at == float('inf'):
49
+ return float('inf')
50
+ remaining = self._expire_at - time.time()
51
+ if remaining < 0:
52
+ return 0
53
+ else:
54
+ return int(remaining * 1000)
55
+
56
+ @property
57
+ def elapsed_ms(self):
58
+ return int(1000 * (time.time() - self._start_at))
59
+
60
+ def maybe_raise(self):
61
+ if self.expired:
62
+ raise KafkaTimeoutError(self._error_message)
63
+
64
+ def __str__(self):
65
+ return "Timer(%s ms remaining)" % (self.timeout_ms)
46
66
 
47
67
  # Taken from: https://github.com/apache/kafka/blob/39eb31feaeebfb184d98cc5d94da9148c2319d81/clients/src/main/java/org/apache/kafka/common/internals/Topic.java#L29
48
68
  TOPIC_MAX_LENGTH = 249
kafka/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = '2.2.3'
1
+ __version__ = '2.2.4'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kafka-python
3
- Version: 2.2.3
3
+ Version: 2.2.4
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
@@ -1,14 +1,14 @@
1
1
  kafka/__init__.py,sha256=4dvHKZAxmD_4tfJ5wGcRV2X78vPcm8vsUoqceULevjA,1077
2
- kafka/client_async.py,sha256=joZB3AnL1mLwvV5fv61Pqn8mkP90FVvzcZ2tZsTGmvM,57060
2
+ kafka/client_async.py,sha256=R8q_rRpG3RrYrRmcZo7XgO2oSdpLJATNcq8w-1vIJ_8,56878
3
3
  kafka/cluster.py,sha256=N3_Al4We4ZhWzz6lVHy6SfqwDZfQy73iV7Qg4g4nxRs,16745
4
4
  kafka/codec.py,sha256=8NZpnehzNrhSBIjzbPVSvyFbSeLAqEntE7BfVHu-_9I,10036
5
5
  kafka/conn.py,sha256=pDmzcn-m8oiFdvYh-97qbRLEBXh0sSl9nT74VIIRuEE,69472
6
- kafka/errors.py,sha256=J3R7z2hkbWA1hsD-bGHdRjcz6BYjP6RNVSQswA2UMmE,33749
6
+ kafka/errors.py,sha256=VygO7AYZvbb52wVgjxuXz-6S2W3vNzzDstF5FNP8Bvk,33829
7
7
  kafka/future.py,sha256=ZQStbfUYIPJRrgMfAWxxjrIRVxsw4WCtSR0J0bkyGno,2847
8
8
  kafka/socks5_wrapper.py,sha256=6woOaCTJXJ5e89_zdyW5BjOpyE4rCbYFH-kd-FeuPuk,9827
9
9
  kafka/structs.py,sha256=SJGzmLdV21jZyQ7247k0WFy16UiusgTHK3I-e4qzI-E,3058
10
- kafka/util.py,sha256=LV6BlELC8-889FpWM1RECX25sccoVrY2U0r5dRZjLNo,3781
11
- kafka/version.py,sha256=imyOcBgptJng0fWUAVwWSHYVE3csDgLCIYFSbnvEA-U,22
10
+ kafka/util.py,sha256=EnzCJuRkQ6Kh2lIdNwFKvT4PddkZ5bzop4ooGGIhe5g,4366
11
+ kafka/version.py,sha256=fHmc2ETGBC5aQYYEdjwwhCQpc1iDi4YoXAxIb24VljI,22
12
12
  kafka/admin/__init__.py,sha256=S_XxqyyV480_yXhttK79XZqNAmZyXRjspd3SoqYykE8,720
13
13
  kafka/admin/acl_resource.py,sha256=ak_dUsSni4SyP0ORbSKenZpwTy0Ykxq3FSt_9XgLR8k,8265
14
14
  kafka/admin/client.py,sha256=RabA8l8Im3iBEXgPVkiofNW6QyeatQHaymBWFZ8Sxkw,78929
@@ -23,12 +23,12 @@ kafka/benchmarks/record_batch_compose.py,sha256=CnUreNg1lUT0Qx9enmSr-THmBl9PjVMf
23
23
  kafka/benchmarks/record_batch_read.py,sha256=vlFaWU2YWI379n_2M8qieb_S2uHUWKV0NquEYy5b-Ho,2184
24
24
  kafka/benchmarks/varint_speed.py,sha256=s4CuvKgDZL-_zna5E3vM8RgHjhXuW6pcaO1z1WYZ_0Y,12585
25
25
  kafka/consumer/__init__.py,sha256=NDdvtyuJgFyQZahqL9i5sYXGP6rOMIXWwHQEaZ1fCcs,122
26
- kafka/consumer/fetcher.py,sha256=EP7SHDS35BaIa3TqAu8GbI1HG8An15twGc9zia6LZ9M,68584
27
- kafka/consumer/group.py,sha256=Jvoal4SdOniweXeUhhYR_HxDUJmmUiKf4WrI_tuJfCQ,58857
26
+ kafka/consumer/fetcher.py,sha256=iwYhWotaEQ55oXTzGKPUOYxvC_6FcoIks_ZqL-gu3DE,68855
27
+ kafka/consumer/group.py,sha256=xmEpVMPJbCAk9__pdAOMswh8I-Ujj5hBax_hPZHZb_s,58758
28
28
  kafka/consumer/subscription_state.py,sha256=f_qJQMhTWQnUd_7lPj43gsagWSKGEmP4jpnEwA6s1Ec,23661
29
29
  kafka/coordinator/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
- kafka/coordinator/base.py,sha256=a6GZKCz4kHKxHNhvwQXr2cTVKm3PH5A-ybDBIN_Dr3Y,50640
31
- kafka/coordinator/consumer.py,sha256=3MbVk4X1QeS-r9iUQJ7R1RkRRVsQSk5W7dvPSq1F6DU,43014
30
+ kafka/coordinator/base.py,sha256=1FxVQ5QR854Ysr8qLW8j2k4PrFvaB-GsMAdq1TKQVG4,51362
31
+ kafka/coordinator/consumer.py,sha256=IJWWt4E6E7JZZGKtGgPtud9V3eqs0js6EaosS3bxffE,44766
32
32
  kafka/coordinator/heartbeat.py,sha256=WJqZGnXHG7TTq1Is3D0mKDis-bBwWVZlSgQiUoZv1jU,2304
33
33
  kafka/coordinator/protocol.py,sha256=wTaIOnUVbj0CKXZ82FktZo-zMRvOCk3hdQAoHJ62e3I,1041
34
34
  kafka/coordinator/assignors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -67,7 +67,7 @@ kafka/partitioner/__init__.py,sha256=Fks3C5_kokVWYw1Ad5wv0sVVzaaBtOejL-2bIL1yRII
67
67
  kafka/partitioner/default.py,sha256=tW-RC1PWIPRDEbeEAaPTLn-00oiZnXoVouEk9AnYE4w,2879
68
68
  kafka/producer/__init__.py,sha256=i3Wxih0NHjmqCkRNE54ial8fBp9siqabUE6ZGyL6oX8,122
69
69
  kafka/producer/future.py,sha256=UC3-g9QlgVFmbitrtMXVpeP0Pbvr7xl2kcw6bAehKG8,2983
70
- kafka/producer/kafka.py,sha256=tDCw1qr--ij4T88h2bIGxH7JQaRukkTl_kgnU83rRio,53379
70
+ kafka/producer/kafka.py,sha256=rzsAoB4ser889nRCtILqGqzWI7jREGV9HPngimCWJPE,53211
71
71
  kafka/producer/record_accumulator.py,sha256=a_mdSATxl-3dVT2rVFh1gTwAv0wUzNbGwVXScwWJ5AE,28072
72
72
  kafka/producer/sender.py,sha256=2EeA3c7po89F2BLTPjex8-MFKzrCdbXAPvHHDa0SOec,37690
73
73
  kafka/producer/transaction_manager.py,sha256=HNfJNZwNfJtYdftn9SeaDfi7I5MKk0LD3sK64inuPt0,41537
@@ -120,7 +120,7 @@ kafka/vendor/enum34.py,sha256=-u-lxAiJMt6ru4Do7NUDY9OpeWkYJMksb2xengJawFE,31204
120
120
  kafka/vendor/selectors34.py,sha256=gxejLO4eXf8mRSGXaQiknPig3GdX1rtsZiYOQJVuAy8,20594
121
121
  kafka/vendor/six.py,sha256=lLBa9_HrANP5BMZ7twEzg1M3wofwPmXyptuWmHX0brY,34826
122
122
  kafka/vendor/socketpair.py,sha256=Fi3PoY1Okkppab720wFk1BhHXyjcw7hi5DwhqrYZH2Y,2737
123
- kafka_python-2.2.3.dist-info/METADATA,sha256=5rHeRnLYzvBgmUIntERI45fmOmLAcZtPttVu2DDDZTs,9951
124
- kafka_python-2.2.3.dist-info/WHEEL,sha256=_itY3bZllKbLk93i0gzNzdweAt5eocJdfN7atrjOnvQ,109
125
- kafka_python-2.2.3.dist-info/top_level.txt,sha256=IivJz7l5WHdLNDT6RIiVAlhjQzYRwGqBBmKHZ7WjPeM,6
126
- kafka_python-2.2.3.dist-info/RECORD,,
123
+ kafka_python-2.2.4.dist-info/METADATA,sha256=FZ5evD2mf0bnJwo3vWAnOFNhN8xQMLXs94J8wCNnm3A,9951
124
+ kafka_python-2.2.4.dist-info/WHEEL,sha256=KsLc7-ImW3kO10_MVVAJ6KE49o7_KqpEzIMxFX-6COY,109
125
+ kafka_python-2.2.4.dist-info/top_level.txt,sha256=IivJz7l5WHdLNDT6RIiVAlhjQzYRwGqBBmKHZ7WjPeM,6
126
+ kafka_python-2.2.4.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.1.0)
2
+ Generator: setuptools (80.3.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py2-none-any
5
5
  Tag: py3-none-any