Skip to content

Commit 3a9434b

Browse files
committed
Propagate timeout to _retrieve_offsets
The default timeout of _retrieve_offsets is infinite, this makes the Consumer block indefinitely even if poll was called with a timeout. Propagate the timeout from the Consumer to the Fetcher operation, removing some of the timeout as more and more sub-operation consume the total allowed timeout.
1 parent e0ab864 commit 3a9434b

File tree

3 files changed

+26
-18
lines changed

3 files changed

+26
-18
lines changed

kafka/consumer/fetcher.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -131,17 +131,18 @@ def send_fetches(self):
131131
self._clean_done_fetch_futures()
132132
return futures
133133

134-
def reset_offsets_if_needed(self, partitions):
134+
def reset_offsets_if_needed(self, partitions, timeout_ms=float("inf")):
135135
"""Lookup and set offsets for any partitions which are awaiting an
136136
explicit reset.
137137
138138
Arguments:
139139
partitions (set of TopicPartitions): the partitions to reset
140140
"""
141+
end_time = time.time() + timeout_ms / 1000
141142
for tp in partitions:
142143
# TODO: If there are several offsets to reset, we could submit offset requests in parallel
143144
if self._subscriptions.is_assigned(tp) and self._subscriptions.is_offset_reset_needed(tp):
144-
self._reset_offset(tp)
145+
self._reset_offset(tp, timeout_ms=1000 * (end_time - time.time()))
145146

146147
def _clean_done_fetch_futures(self):
147148
while True:
@@ -156,7 +157,7 @@ def in_flight_fetches(self):
156157
self._clean_done_fetch_futures()
157158
return bool(self._fetch_futures)
158159

159-
def update_fetch_positions(self, partitions):
160+
def update_fetch_positions(self, partitions, timeout_ms=float("inf")):
160161
"""Update the fetch positions for the provided partitions.
161162
162163
Arguments:
@@ -167,6 +168,7 @@ def update_fetch_positions(self, partitions):
167168
partition and no reset policy is available
168169
"""
169170
# reset the fetch position to the committed position
171+
end_time = time.time() + timeout_ms / 1000
170172
for tp in partitions:
171173
if not self._subscriptions.is_assigned(tp):
172174
log.warning("partition %s is not assigned - skipping offset"
@@ -178,12 +180,12 @@ def update_fetch_positions(self, partitions):
178180
continue
179181

180182
if self._subscriptions.is_offset_reset_needed(tp):
181-
self._reset_offset(tp)
183+
self._reset_offset(tp, timeout_ms=1000 * (end_time - time.time()))
182184
elif self._subscriptions.assignment[tp].committed is None:
183185
# there's no committed position, so we need to reset with the
184186
# default strategy
185187
self._subscriptions.need_offset_reset(tp)
186-
self._reset_offset(tp)
188+
self._reset_offset(tp, timeout_ms=1000 * (end_time - time.time()))
187189
else:
188190
committed = self._subscriptions.assignment[tp].committed.offset
189191
log.debug("Resetting offset for partition %s to the committed"
@@ -215,7 +217,7 @@ def beginning_or_end_offset(self, partitions, timestamp, timeout_ms):
215217
offsets[tp] = offsets[tp][0]
216218
return offsets
217219

218-
def _reset_offset(self, partition):
220+
def _reset_offset(self, partition, timeout_ms):
219221
"""Reset offsets for the given partition using the offset reset strategy.
220222
221223
Arguments:
@@ -234,7 +236,7 @@ def _reset_offset(self, partition):
234236

235237
log.debug("Resetting offset for partition %s to %s offset.",
236238
partition, strategy)
237-
offsets = self._retrieve_offsets({partition: timestamp})
239+
offsets = self._retrieve_offsets({partition: timestamp}, timeout_ms)
238240

239241
if partition in offsets:
240242
offset = offsets[partition][0]

kafka/consumer/group.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -678,12 +678,14 @@ def _poll_once(self, timeout_ms, max_records, update_offsets=True):
678678
Returns:
679679
dict: Map of topic to list of records (may be empty).
680680
"""
681+
end_time = time.time() + timeout_ms / 1000
681682
self._coordinator.poll()
682683

683684
# Fetch positions if we have partitions we're subscribed to that we
684685
# don't know the offset for
685686
if not self._subscription.has_all_fetch_positions():
686-
self._update_fetch_positions(self._subscription.missing_fetch_positions())
687+
update_timeout_ms = 1000 * (end_time - time.time())
688+
self._update_fetch_positions(self._subscription.missing_fetch_positions(), update_timeout_ms)
687689

688690
# If data is available already, e.g. from a previous network client
689691
# poll() call to commit, then just return it immediately
@@ -714,7 +716,7 @@ def _poll_once(self, timeout_ms, max_records, update_offsets=True):
714716
records, _ = self._fetcher.fetched_records(max_records, update_offsets=update_offsets)
715717
return records
716718

717-
def position(self, partition):
719+
def position(self, partition, timeout_ms=float("inf")):
718720
"""Get the offset of the next record that will be fetched
719721
720722
Arguments:
@@ -728,7 +730,7 @@ def position(self, partition):
728730
assert self._subscription.is_assigned(partition), 'Partition is not assigned'
729731
offset = self._subscription.assignment[partition].position
730732
if offset is None:
731-
self._update_fetch_positions([partition])
733+
self._update_fetch_positions([partition], timeout_ms)
732734
offset = self._subscription.assignment[partition].position
733735
return offset
734736

@@ -1087,7 +1089,7 @@ def _use_consumer_group(self):
10871089
return False
10881090
return True
10891091

1090-
def _update_fetch_positions(self, partitions):
1092+
def _update_fetch_positions(self, partitions, timeout_ms):
10911093
"""Set the fetch position to the committed position (if there is one)
10921094
or reset it using the offset reset policy the user has configured.
10931095
@@ -1099,12 +1101,13 @@ def _update_fetch_positions(self, partitions):
10991101
NoOffsetForPartitionError: If no offset is stored for a given
11001102
partition and no offset reset policy is defined.
11011103
"""
1104+
end_time = time.time() + timeout_ms / 1000
11021105
# Lookup any positions for partitions which are awaiting reset (which may be the
11031106
# case if the user called :meth:`seek_to_beginning` or :meth:`seek_to_end`. We do
11041107
# this check first to avoid an unnecessary lookup of committed offsets (which
11051108
# typically occurs when the user is manually assigning partitions and managing
11061109
# their own offsets).
1107-
self._fetcher.reset_offsets_if_needed(partitions)
1110+
self._fetcher.reset_offsets_if_needed(partitions, timeout_ms)
11081111

11091112
if not self._subscription.has_all_fetch_positions():
11101113
# if we still don't have offsets for all partitions, then we should either seek
@@ -1115,7 +1118,8 @@ def _update_fetch_positions(self, partitions):
11151118
self._coordinator.refresh_committed_offsets_if_needed()
11161119

11171120
# Then, do any offset lookups in case some positions are not known
1118-
self._fetcher.update_fetch_positions(partitions)
1121+
update_timeout_ms = 1000 * (end_time - time.time())
1122+
self._fetcher.update_fetch_positions(partitions, update_timeout_ms)
11191123

11201124
def _message_generator_v2(self):
11211125
timeout_ms = 1000 * (self._consumer_timeout - time.time())
@@ -1145,7 +1149,8 @@ def _message_generator(self):
11451149
# Fetch offsets for any subscribed partitions that we arent tracking yet
11461150
if not self._subscription.has_all_fetch_positions():
11471151
partitions = self._subscription.missing_fetch_positions()
1148-
self._update_fetch_positions(partitions)
1152+
update_timeout_ms = 1000 * (self._consumer_timeout - time.time())
1153+
self._update_fetch_positions(partitions, update_timeout_ms)
11491154

11501155
poll_ms = min((1000 * (self._consumer_timeout - time.time())), self.config['retry_backoff_ms'])
11511156
self._client.poll(timeout_ms=poll_ms)

test/test_fetcher.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import pytest
55

66
from collections import OrderedDict
7+
from unittest.mock import ANY
78
import itertools
89
import time
910

@@ -114,11 +115,11 @@ def test_update_fetch_positions(fetcher, topic, mocker):
114115
# partition needs reset, no committed offset
115116
fetcher._subscriptions.need_offset_reset(partition)
116117
fetcher._subscriptions.assignment[partition].awaiting_reset = False
117-
fetcher.update_fetch_positions([partition])
118-
fetcher._reset_offset.assert_called_with(partition)
118+
fetcher.update_fetch_positions([partition], timeout_ms=1234)
119+
fetcher._reset_offset.assert_called_with(partition, timeout_ms=ANY)
119120
assert fetcher._subscriptions.assignment[partition].awaiting_reset is True
120121
fetcher.update_fetch_positions([partition])
121-
fetcher._reset_offset.assert_called_with(partition)
122+
fetcher._reset_offset.assert_called_with(partition, timeout_ms=ANY)
122123

123124
# partition needs reset, has committed offset
124125
fetcher._reset_offset.reset_mock()
@@ -139,7 +140,7 @@ def test__reset_offset(fetcher, mocker):
139140
mocked = mocker.patch.object(fetcher, '_retrieve_offsets')
140141

141142
mocked.return_value = {tp: (1001, None)}
142-
fetcher._reset_offset(tp)
143+
fetcher._reset_offset(tp, timeout_ms=1234)
143144
assert not fetcher._subscriptions.assignment[tp].awaiting_reset
144145
assert fetcher._subscriptions.assignment[tp].position == 1001
145146

0 commit comments

Comments
 (0)