|
2 | 2 |
|
3 | 3 | from kafka import ConsumerGroupMetadata, KafkaConsumer, TopicPartition |
4 | 4 | from kafka.errors import KafkaConfigurationError, IllegalStateError |
| 5 | +from kafka.util import Timer |
5 | 6 |
|
6 | 7 |
|
7 | 8 | def test_session_timeout_different_from_max_poll_timeout_raises(): |
@@ -92,3 +93,69 @@ def test_group_metadata_with_group_id_delegates_to_coordinator(): |
92 | 93 | assert gm.generation_id == -1 |
93 | 94 | assert gm.member_id == '' |
94 | 95 | consumer.close() |
| 96 | + |
| 97 | + |
| 98 | +def _stub_poll_path(consumer, mocker, fetch_return): |
| 99 | + """Patch out the bits of _poll_once we don't care about so the test can |
| 100 | + focus on the fetch -> sleep handoff.""" |
| 101 | + mocker.patch.object(consumer._coordinator, 'poll', return_value=True) |
| 102 | + mocker.patch.object(consumer, '_refresh_committed_offsets') |
| 103 | + mocker.patch.object(consumer._fetcher, 'reset_offsets_if_needed') |
| 104 | + mocker.patch.object(consumer._fetcher, 'maybe_validate_positions') |
| 105 | + mocker.patch.object(consumer._fetcher, 'validate_offsets_if_needed') |
| 106 | + mocker.patch.object(consumer._fetcher, 'fetch_records', |
| 107 | + return_value=fetch_return) |
| 108 | + |
| 109 | + |
| 110 | +def test_poll_once_sleeps_when_fetcher_idle(mocker): |
| 111 | + """If the fetcher reports it has no work pending, _poll_once sleeps up to |
| 112 | + poll_timeout_ms before returning - otherwise consumer.poll() busy-loops |
| 113 | + on no-fetchable-partition consumers.""" |
| 114 | + consumer = KafkaConsumer(api_version=(0, 10, 0)) |
| 115 | + try: |
| 116 | + _stub_poll_path(consumer, mocker, fetch_return=({}, True)) |
| 117 | + sleep = mocker.patch('kafka.consumer.group.time.sleep') |
| 118 | + |
| 119 | + records = consumer._poll_once(Timer(1000), max_records=100) |
| 120 | + |
| 121 | + assert records == {} |
| 122 | + sleep.assert_called_once() |
| 123 | + slept_secs = sleep.call_args[0][0] |
| 124 | + # poll_timeout_ms is uncapped for no-group consumers, so we sleep |
| 125 | + # roughly the full Timer budget (1.0s). |
| 126 | + assert 0 < slept_secs <= 1.0 |
| 127 | + finally: |
| 128 | + consumer.close() |
| 129 | + |
| 130 | + |
| 131 | +def test_poll_once_does_not_sleep_when_records_returned(mocker): |
| 132 | + """fetch_records returned records - no sleep, caller can return them.""" |
| 133 | + consumer = KafkaConsumer(api_version=(0, 10, 0)) |
| 134 | + try: |
| 135 | + tp_records = {TopicPartition('foo', 0): [object()]} |
| 136 | + _stub_poll_path(consumer, mocker, fetch_return=(tp_records, False)) |
| 137 | + sleep = mocker.patch('kafka.consumer.group.time.sleep') |
| 138 | + |
| 139 | + records = consumer._poll_once(Timer(1000), max_records=100) |
| 140 | + |
| 141 | + assert records is tp_records |
| 142 | + sleep.assert_not_called() |
| 143 | + finally: |
| 144 | + consumer.close() |
| 145 | + |
| 146 | + |
| 147 | +def test_poll_once_does_not_sleep_when_fetcher_waited_but_empty(mocker): |
| 148 | + """fetch_records returned ({}, False) - it waited on in-flight work and |
| 149 | + got nothing. Don't sleep; the next loop iteration will check the |
| 150 | + completed fetches without delay.""" |
| 151 | + consumer = KafkaConsumer(api_version=(0, 10, 0)) |
| 152 | + try: |
| 153 | + _stub_poll_path(consumer, mocker, fetch_return=({}, False)) |
| 154 | + sleep = mocker.patch('kafka.consumer.group.time.sleep') |
| 155 | + |
| 156 | + records = consumer._poll_once(Timer(1000), max_records=100) |
| 157 | + |
| 158 | + assert records == {} |
| 159 | + sleep.assert_not_called() |
| 160 | + finally: |
| 161 | + consumer.close() |
0 commit comments