|
14 | 14 | import os |
15 | 15 | import time |
16 | 16 | import unittest |
| 17 | +from unittest import mock |
17 | 18 |
|
18 | 19 | import pyarrow as pa |
19 | 20 | from alibabacloud_alikafka20190916 import models as alikafka_models |
20 | 21 | from alibabacloud_alikafka20190916.client import Client as AliKafkaClient |
21 | 22 | from alibabacloud_credentials.client import Client as CredClient |
22 | 23 | from alibabacloud_tea_openapi import models as openapi_models |
23 | 24 | from alibabacloud_tea_util import models as util_models |
24 | | -from confluent_kafka import Producer |
| 25 | +from confluent_kafka import KafkaException, Producer, TopicPartition |
25 | 26 | from parameterized import parameterized |
26 | 27 | from torch.utils.data import DataLoader |
27 | 28 |
|
28 | | -from tzrec.datasets.kafka_dataset import KafkaDataset, _parse_kafka_uri |
| 29 | +from tzrec.datasets.kafka_dataset import ( |
| 30 | + KafkaDataset, |
| 31 | + _offsets_for_times_batched, |
| 32 | + _parse_kafka_uri, |
| 33 | +) |
29 | 34 | from tzrec.features.feature import FgMode, create_features |
30 | 35 | from tzrec.protos import data_pb2, feature_pb2 |
31 | 36 | from tzrec.utils.checkpoint_util import update_dataloder_state |
@@ -69,6 +74,122 @@ def test_parse_with_non_integer_timestamp_raises(self): |
69 | 74 | _parse_kafka_uri(uri) |
70 | 75 |
|
71 | 76 |
|
| 77 | +def _resolved(partition, offset, topic="t"): |
| 78 | + """Build a TopicPartition as returned by offsets_for_times.""" |
| 79 | + tp = TopicPartition(topic, partition) |
| 80 | + tp.offset = offset |
| 81 | + return tp |
| 82 | + |
| 83 | + |
| 84 | +class OffsetsForTimesBatchedTest(unittest.TestCase): |
| 85 | + """Unit tests for _offsets_for_times_batched (broker-free, mocked).""" |
| 86 | + |
| 87 | + START_TS = 1711929600000 |
| 88 | + |
| 89 | + def test_empty_partitions_returns_empty(self): |
| 90 | + consumer = mock.MagicMock() |
| 91 | + result = _offsets_for_times_batched( |
| 92 | + consumer, [], self.START_TS, 30, 30.0, 3, sleep_fn=mock.MagicMock() |
| 93 | + ) |
| 94 | + self.assertEqual(result, {}) |
| 95 | + consumer.offsets_for_times.assert_not_called() |
| 96 | + |
| 97 | + def test_single_chunk_all_resolved(self): |
| 98 | + seen_offsets = [] |
| 99 | + |
| 100 | + def _fake(chunk, timeout): |
| 101 | + # offsets_for_times receives the target timestamp in tp.offset. |
| 102 | + seen_offsets.extend(tp.offset for tp in chunk) |
| 103 | + return [_resolved(tp.partition, tp.partition + 100) for tp in chunk] |
| 104 | + |
| 105 | + consumer = mock.MagicMock() |
| 106 | + consumer.offsets_for_times.side_effect = _fake |
| 107 | + parts = [TopicPartition("t", p) for p in range(5)] |
| 108 | + result = _offsets_for_times_batched( |
| 109 | + consumer, parts, self.START_TS, 30, 30.0, 3, sleep_fn=mock.MagicMock() |
| 110 | + ) |
| 111 | + self.assertEqual(consumer.offsets_for_times.call_count, 1) |
| 112 | + self.assertEqual(result, {("t", p): p + 100 for p in range(5)}) |
| 113 | + # target timestamp was set on every partition before the call. |
| 114 | + self.assertEqual(seen_offsets, [self.START_TS] * 5) |
| 115 | + |
| 116 | + def test_chunking_boundary(self): |
| 117 | + consumer = mock.MagicMock() |
| 118 | + consumer.offsets_for_times.side_effect = lambda chunk, timeout: [ |
| 119 | + _resolved(tp.partition, tp.partition + 100) for tp in chunk |
| 120 | + ] |
| 121 | + parts = [TopicPartition("t", p) for p in range(65)] |
| 122 | + result = _offsets_for_times_batched( |
| 123 | + consumer, parts, self.START_TS, 30, 30.0, 3, sleep_fn=mock.MagicMock() |
| 124 | + ) |
| 125 | + chunk_sizes = [ |
| 126 | + len(call.args[0]) for call in consumer.offsets_for_times.call_args_list |
| 127 | + ] |
| 128 | + self.assertEqual(chunk_sizes, [30, 30, 5]) |
| 129 | + self.assertEqual(len(result), 65) |
| 130 | + |
| 131 | + def test_partial_resolution_filters_negative(self): |
| 132 | + # broker returns -1 when the timestamp is past the last message. |
| 133 | + consumer = mock.MagicMock() |
| 134 | + consumer.offsets_for_times.return_value = [ |
| 135 | + _resolved(0, 100), |
| 136 | + _resolved(1, -1), |
| 137 | + _resolved(2, 102), |
| 138 | + ] |
| 139 | + parts = [TopicPartition("t", p) for p in range(3)] |
| 140 | + result = _offsets_for_times_batched( |
| 141 | + consumer, parts, self.START_TS, 30, 30.0, 3, sleep_fn=mock.MagicMock() |
| 142 | + ) |
| 143 | + self.assertEqual(result, {("t", 0): 100, ("t", 2): 102}) |
| 144 | + self.assertNotIn(("t", 1), result) |
| 145 | + |
| 146 | + def test_retry_then_success(self): |
| 147 | + sleep_fn = mock.MagicMock() |
| 148 | + consumer = mock.MagicMock() |
| 149 | + consumer.offsets_for_times.side_effect = [ |
| 150 | + KafkaException("transient timeout"), |
| 151 | + [_resolved(0, 100)], |
| 152 | + ] |
| 153 | + parts = [TopicPartition("t", 0)] |
| 154 | + result = _offsets_for_times_batched( |
| 155 | + consumer, parts, self.START_TS, 30, 30.0, 3, sleep_fn=sleep_fn |
| 156 | + ) |
| 157 | + self.assertEqual(result, {("t", 0): 100}) |
| 158 | + self.assertEqual(consumer.offsets_for_times.call_count, 2) |
| 159 | + self.assertEqual(sleep_fn.call_count, 1) |
| 160 | + |
| 161 | + def test_retry_exhausted_raises(self): |
| 162 | + sleep_fn = mock.MagicMock() |
| 163 | + consumer = mock.MagicMock() |
| 164 | + consumer.offsets_for_times.side_effect = KafkaException("persistent timeout") |
| 165 | + parts = [TopicPartition("t", 0)] |
| 166 | + with self.assertRaises(KafkaException): |
| 167 | + _offsets_for_times_batched( |
| 168 | + consumer, parts, self.START_TS, 30, 30.0, 3, sleep_fn=sleep_fn |
| 169 | + ) |
| 170 | + # 1 initial attempt + 3 retries = 4 calls; sleep before each retry = 3. |
| 171 | + self.assertEqual(consumer.offsets_for_times.call_count, 4) |
| 172 | + self.assertEqual(sleep_fn.call_count, 3) |
| 173 | + |
| 174 | + def test_retry_isolated_per_chunk(self): |
| 175 | + sleep_fn = mock.MagicMock() |
| 176 | + # chunk 0 fails once then succeeds; chunk 1 succeeds first try. |
| 177 | + outcomes = [ |
| 178 | + KafkaException("blip"), |
| 179 | + [_resolved(0, 100)], |
| 180 | + [_resolved(1, 101)], |
| 181 | + ] |
| 182 | + consumer = mock.MagicMock() |
| 183 | + consumer.offsets_for_times.side_effect = outcomes |
| 184 | + parts = [TopicPartition("t", 0), TopicPartition("t", 1)] |
| 185 | + result = _offsets_for_times_batched( |
| 186 | + consumer, parts, self.START_TS, 1, 30.0, 3, sleep_fn=sleep_fn |
| 187 | + ) |
| 188 | + self.assertEqual(result, {("t", 0): 100, ("t", 1): 101}) |
| 189 | + self.assertEqual(consumer.offsets_for_times.call_count, 3) |
| 190 | + self.assertEqual(sleep_fn.call_count, 1) |
| 191 | + |
| 192 | + |
72 | 193 | class KafkaDatasetTest(unittest.TestCase): |
73 | 194 | def setUp(self): |
74 | 195 | credential = CredClient() |
|
0 commit comments