|
116 | 116 | TransactionalContainerTests.topic3, TransactionalContainerTests.topic3DLT, TransactionalContainerTests.topic4, |
117 | 117 | TransactionalContainerTests.topic5, TransactionalContainerTests.topic6, TransactionalContainerTests.topic7, |
118 | 118 | TransactionalContainerTests.topic8, TransactionalContainerTests.topic8DLT, TransactionalContainerTests.topic9, |
119 | | - TransactionalContainerTests.topic10}, |
| 119 | + TransactionalContainerTests.topic10, TransactionalContainerTests.topic11, TransactionalContainerTests.topic12, |
| 120 | + TransactionalContainerTests.topic13, TransactionalContainerTests.topic14, TransactionalContainerTests.topic15 |
| 121 | +}, |
120 | 122 | brokerProperties = { "transaction.state.log.replication.factor=1", "transaction.state.log.min.isr=1" }) |
121 | 123 | public class TransactionalContainerTests { |
122 | 124 |
|
@@ -146,6 +148,15 @@ public class TransactionalContainerTests { |
146 | 148 |
|
147 | 149 | public static final String topic11 = "txTopic11"; |
148 | 150 |
|
| 151 | + public static final String topic12 = "txTopic12"; |
| 152 | + |
| 153 | + public static final String topic13 = "txTopic13"; |
| 154 | + |
| 155 | + public static final String topic14 = "txTopic14"; |
| 156 | + |
| 157 | + public static final String topic15 = "txTopic15"; |
| 158 | + |
| 159 | + |
149 | 160 | private static EmbeddedKafkaBroker embeddedKafka; |
150 | 161 |
|
151 | 162 | @BeforeAll |
@@ -1232,4 +1243,213 @@ public void afterRecord( |
1232 | 1243 | container.stop(); |
1233 | 1244 | pf.destroy(); |
1234 | 1245 | } |
| 1246 | + |
| 1247 | + @SuppressWarnings("unchecked") |
| 1248 | + @Test |
| 1249 | + void testFixTxOffsetsWithReadUncommitted() throws Exception { |
| 1250 | + Map<String, Object> props = KafkaTestUtils.consumerProps(embeddedKafka, "txReadUncommittedTest", false); |
| 1251 | + props.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG, "read_uncommitted"); |
| 1252 | + DefaultKafkaConsumerFactory<Integer, String> cf = new DefaultKafkaConsumerFactory<>(props); |
| 1253 | + ContainerProperties containerProps = new ContainerProperties(topic12); |
| 1254 | + containerProps.setGroupId("txReadUncommittedTest"); |
| 1255 | + containerProps.setPollTimeout(500L); |
| 1256 | + containerProps.setIdleEventInterval(500L); |
| 1257 | + containerProps.setFixTxOffsets(true); |
| 1258 | + |
| 1259 | + Map<String, Object> senderProps = KafkaTestUtils.producerProps(embeddedKafka); |
| 1260 | + DefaultKafkaProducerFactory<Integer, String> pf = new DefaultKafkaProducerFactory<>(senderProps); |
| 1261 | + pf.setTransactionIdPrefix("readUncommitted."); |
| 1262 | + |
| 1263 | + final KafkaTemplate<Integer, String> template = new KafkaTemplate<>(pf); |
| 1264 | + final AtomicInteger messageCount = new AtomicInteger(); |
| 1265 | + final CountDownLatch latch = new CountDownLatch(1); |
| 1266 | + containerProps.setMessageListener((MessageListener<Integer, String>) message -> { |
| 1267 | + messageCount.incrementAndGet(); |
| 1268 | + latch.countDown(); |
| 1269 | + }); |
| 1270 | + |
| 1271 | + @SuppressWarnings({ "rawtypes" }) |
| 1272 | + KafkaTransactionManager tm = new KafkaTransactionManager(pf); |
| 1273 | + containerProps.setKafkaAwareTransactionManager(tm); |
| 1274 | + KafkaMessageListenerContainer<Integer, String> container = |
| 1275 | + new KafkaMessageListenerContainer<>(cf, containerProps); |
| 1276 | + container.setBeanName("testFixTxOffsetsWithReadUncommitted"); |
| 1277 | + |
| 1278 | + AtomicReference<Map<TopicPartition, OffsetAndMetadata>> committed = new AtomicReference<>(); |
| 1279 | + CountDownLatch idleLatch = new CountDownLatch(1); |
| 1280 | + container.setApplicationEventPublisher(event -> { |
| 1281 | + if (event instanceof ListenerContainerIdleEvent) { |
| 1282 | + Consumer<?, ?> consumer = ((ListenerContainerIdleEvent) event).getConsumer(); |
| 1283 | + committed.set(consumer.committed( |
| 1284 | + Collections.singleton(new TopicPartition(topic12, 0)))); |
| 1285 | + idleLatch.countDown(); |
| 1286 | + } |
| 1287 | + }); |
| 1288 | + |
| 1289 | + container.start(); |
| 1290 | + |
| 1291 | + template.setDefaultTopic(topic12); |
| 1292 | + template.executeInTransaction(t -> { |
| 1293 | + template.sendDefault(0, 0, "msg1"); |
| 1294 | + template.sendDefault(0, 0, "msg2"); |
| 1295 | + template.sendDefault(0, 0, "msg3"); |
| 1296 | + return null; |
| 1297 | + }); |
| 1298 | + |
| 1299 | + assertThat(latch.await(60, TimeUnit.SECONDS)).isTrue(); |
| 1300 | + assertThat(idleLatch.await(60, TimeUnit.SECONDS)).isTrue(); |
| 1301 | + |
| 1302 | + assertThat(messageCount.get()).isGreaterThanOrEqualTo(3); |
| 1303 | + TopicPartition partition0 = new TopicPartition(topic12, 0); |
| 1304 | + assertThat(committed.get().get(partition0)).isNotNull(); |
| 1305 | + |
| 1306 | + // 0 1 2 3(tx marker) => next offset 4 |
| 1307 | + assertThat(committed.get().get(partition0).offset()).isGreaterThanOrEqualTo(4L); |
| 1308 | + |
| 1309 | + container.stop(); |
| 1310 | + pf.destroy(); |
| 1311 | + } |
| 1312 | + |
| 1313 | + @Test |
| 1314 | + void testFixTxOffsetsWithEmptyPollAdvance() throws Exception { |
| 1315 | + Map<String, Object> props = KafkaTestUtils.consumerProps(embeddedKafka, "txEmptyPoll", false); |
| 1316 | + props.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG, "read_committed"); |
| 1317 | + props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 1); |
| 1318 | + DefaultKafkaConsumerFactory<Integer, String> cf = new DefaultKafkaConsumerFactory<>(props); |
| 1319 | + |
| 1320 | + ContainerProperties containerProps = new ContainerProperties(topic13); |
| 1321 | + containerProps.setGroupId("txEmptyPoll"); |
| 1322 | + containerProps.setPollTimeout(500L); |
| 1323 | + containerProps.setFixTxOffsets(true); |
| 1324 | + containerProps.setIdleEventInterval(1000L); |
| 1325 | + |
| 1326 | + containerProps.setMessageListener((MessageListener<Integer, String>) rec -> {}); |
| 1327 | + |
| 1328 | + DefaultKafkaProducerFactory<Integer, String> pf = |
| 1329 | + new DefaultKafkaProducerFactory<>(KafkaTestUtils.producerProps(embeddedKafka)); |
| 1330 | + pf.setTransactionIdPrefix("tx.emptyPoll."); |
| 1331 | + KafkaTemplate<Integer, String> template = new KafkaTemplate<>(pf); |
| 1332 | + |
| 1333 | + KafkaMessageListenerContainer<Integer, String> container = |
| 1334 | + new KafkaMessageListenerContainer<>(cf, containerProps); |
| 1335 | + container.setBeanName("testFixEmptyPoll"); |
| 1336 | + |
| 1337 | + AtomicReference<Map<TopicPartition, OffsetAndMetadata>> committed = new AtomicReference<>(); |
| 1338 | + CountDownLatch latch = new CountDownLatch(1); |
| 1339 | + |
| 1340 | + container.setApplicationEventPublisher(event -> { |
| 1341 | + if (event instanceof ListenerContainerIdleEvent e) { |
| 1342 | + TopicPartition tp = new TopicPartition(topic13, 0); |
| 1343 | + committed.set(e.getConsumer().committed(Set.of(tp))); |
| 1344 | + latch.countDown(); |
| 1345 | + } |
| 1346 | + }); |
| 1347 | + |
| 1348 | + container.start(); |
| 1349 | + |
| 1350 | + template.setDefaultTopic(topic13); |
| 1351 | + template.executeInTransaction(t -> { |
| 1352 | + template.sendDefault(0, 0, "msg1"); |
| 1353 | + template.sendDefault(0, 0, "msg2"); |
| 1354 | + template.sendDefault(0, 0, "msg3"); |
| 1355 | + return null; |
| 1356 | + }); |
| 1357 | + |
| 1358 | + assertThat(latch.await(60, TimeUnit.SECONDS)).isTrue(); |
| 1359 | + assertThat(committed.get().get(new TopicPartition(topic13, 0)).offset()) |
| 1360 | + .isEqualTo(4L); |
| 1361 | + container.stop(); |
| 1362 | + } |
| 1363 | + |
| 1364 | + @Test |
| 1365 | + void testFixTxOffsetsRetainsLeaderEpoch() throws Exception { |
| 1366 | + Map<String, Object> props = KafkaTestUtils.consumerProps(embeddedKafka, "txLeaderEpoch", false); |
| 1367 | + props.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG, "read_committed"); |
| 1368 | + DefaultKafkaConsumerFactory<Integer, String> cf = new DefaultKafkaConsumerFactory<>(props); |
| 1369 | + |
| 1370 | + ContainerProperties containerProps = new ContainerProperties(topic14); |
| 1371 | + containerProps.setFixTxOffsets(true); |
| 1372 | + containerProps.setIdleEventInterval(1000L); |
| 1373 | + |
| 1374 | + containerProps.setMessageListener((MessageListener<Integer, String>) rec -> {}); |
| 1375 | + |
| 1376 | + DefaultKafkaProducerFactory<Integer, String> pf = |
| 1377 | + new DefaultKafkaProducerFactory<>(KafkaTestUtils.producerProps(embeddedKafka)); |
| 1378 | + pf.setTransactionIdPrefix("tx.leaderEpoch."); |
| 1379 | + KafkaTemplate<Integer, String> template = new KafkaTemplate<>(pf); |
| 1380 | + |
| 1381 | + KafkaMessageListenerContainer<Integer, String> container = |
| 1382 | + new KafkaMessageListenerContainer<>(cf, containerProps); |
| 1383 | + |
| 1384 | + AtomicReference<OffsetAndMetadata> committed = new AtomicReference<>(); |
| 1385 | + CountDownLatch latch = new CountDownLatch(1); |
| 1386 | + |
| 1387 | + container.setApplicationEventPublisher(event -> { |
| 1388 | + if (event instanceof ListenerContainerIdleEvent e) { |
| 1389 | + TopicPartition tp = new TopicPartition(topic14, 0); |
| 1390 | + committed.set(e.getConsumer().committed(Set.of(tp)).get(tp)); |
| 1391 | + latch.countDown(); |
| 1392 | + } |
| 1393 | + }); |
| 1394 | + |
| 1395 | + container.start(); |
| 1396 | + |
| 1397 | + template.setDefaultTopic(topic14); |
| 1398 | + template.executeInTransaction(t -> { |
| 1399 | + template.sendDefault(0, 0, "data"); |
| 1400 | + return null; |
| 1401 | + }); |
| 1402 | + |
| 1403 | + assertThat(latch.await(30, TimeUnit.SECONDS)).isTrue(); |
| 1404 | + assertThat(committed.get().leaderEpoch().isPresent()).isTrue(); |
| 1405 | + container.stop(); |
| 1406 | + } |
| 1407 | + |
| 1408 | + @Test |
| 1409 | + void testFixLagWhenMaxPollEqualsTxBatchSize() throws Exception { |
| 1410 | + Map<String, Object> props = KafkaTestUtils.consumerProps(embeddedKafka, "txTestPollLimit", false); |
| 1411 | + props.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG, "read_committed"); |
| 1412 | + props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 3); |
| 1413 | + DefaultKafkaConsumerFactory<Integer, String> cf = new DefaultKafkaConsumerFactory<>(props); |
| 1414 | + |
| 1415 | + ContainerProperties containerProps = new ContainerProperties(topic15); |
| 1416 | + containerProps.setGroupId("txTestPollLimit"); |
| 1417 | + containerProps.setPollTimeout(500L); |
| 1418 | + containerProps.setFixTxOffsets(true); |
| 1419 | + containerProps.setIdleEventInterval(1000L); |
| 1420 | + containerProps.setMessageListener((MessageListener<Integer, String>) rec -> {}); |
| 1421 | + |
| 1422 | + DefaultKafkaProducerFactory<Integer, String> pf = |
| 1423 | + new DefaultKafkaProducerFactory<>(KafkaTestUtils.producerProps(embeddedKafka)); |
| 1424 | + pf.setTransactionIdPrefix("tx.polllimit."); |
| 1425 | + |
| 1426 | + KafkaTemplate<Integer, String> template = new KafkaTemplate<>(pf); |
| 1427 | + KafkaMessageListenerContainer<Integer, String> container = new KafkaMessageListenerContainer<>(cf, containerProps); |
| 1428 | + container.setBeanName("testFixLagPollLimit"); |
| 1429 | + |
| 1430 | + AtomicReference<Map<TopicPartition, OffsetAndMetadata>> committed = new AtomicReference<>(); |
| 1431 | + CountDownLatch latch = new CountDownLatch(1); |
| 1432 | + |
| 1433 | + container.setApplicationEventPublisher(event -> { |
| 1434 | + if (event instanceof ListenerContainerIdleEvent e) { |
| 1435 | + committed.set(e.getConsumer().committed(Set.of(new TopicPartition(topic15, 0)))); |
| 1436 | + latch.countDown(); |
| 1437 | + } |
| 1438 | + }); |
| 1439 | + |
| 1440 | + container.start(); |
| 1441 | + |
| 1442 | + template.setDefaultTopic(topic15); |
| 1443 | + template.executeInTransaction(t -> { |
| 1444 | + template.sendDefault(0, 0, "msg1"); |
| 1445 | + template.sendDefault(0, 0, "msg2"); |
| 1446 | + template.sendDefault(0, 0, "msg3"); |
| 1447 | + return null; |
| 1448 | + }); |
| 1449 | + |
| 1450 | + assertThat(latch.await(60, TimeUnit.SECONDS)).isTrue(); |
| 1451 | + assertThat(committed.get().get(new TopicPartition(topic15, 0)).offset()).isEqualTo(4L); |
| 1452 | + container.stop(); |
| 1453 | + } |
| 1454 | + |
1235 | 1455 | } |
0 commit comments