This is an automated email from the ASF dual-hosted git repository.
davidzollo pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/seatunnel.git
The following commit(s) were added to refs/heads/dev by this push:
new 7be1b0e464 [Test][E2E] Stabilize Kafka exactly-once record reads
(#11205)
7be1b0e464 is described below
commit 7be1b0e4643451c448ca0a909c1ada8a118824d7
Author: Daniel <[email protected]>
AuthorDate: Mon Jun 29 22:35:37 2026 +0800
[Test][E2E] Stabilize Kafka exactly-once record reads (#11205)
---
.../seatunnel/e2e/connector/kafka/KafkaIT.java | 81 +++++++++++++++++-----
1 file changed, 63 insertions(+), 18 deletions(-)
diff --git
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/java/org/apache/seatunnel/e2e/connector/kafka/KafkaIT.java
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/java/org/apache/seatunnel/e2e/connector/kafka/KafkaIT.java
index f91c1827a1..72ac59dc22 100644
---
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/java/org/apache/seatunnel/e2e/connector/kafka/KafkaIT.java
+++
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/java/org/apache/seatunnel/e2e/connector/kafka/KafkaIT.java
@@ -1689,6 +1689,7 @@ public class KafkaIT extends TestSuiteBase implements
TestResource {
createKafkaTopic(producerTopic);
createKafkaTopic(consumerTopic);
String sourceData = "Seatunnel Exactly Once Example";
+ String keepAliveData = sourceData + "-keepalive-" + resourceSuffix;
long sinkStartOffset = endOffsetOnP0(consumerTopic);
for (int i = 0; i < 10; i++) {
ProducerRecord<byte[], byte[]> record =
@@ -1716,9 +1717,24 @@ public class KafkaIT extends TestSuiteBase implements
TestResource {
.await()
.atMost(5, MINUTES)
.untilAsserted(
- () ->
- Assertions.assertTrue(
- checkData(consumerTopic,
sinkStartOffset, 10, sourceData)));
+ () -> {
+ // Keep the streaming source active so the last
exactly-once transaction
+ // is forced through a later checkpoint on slow
Flink CI axes.
+ ProducerRecord<byte[], byte[]> keepAliveRecord =
+ new ProducerRecord<>(
+ producerTopic,
+ null,
+
keepAliveData.getBytes(StandardCharsets.UTF_8));
+ producer.send(keepAliveRecord);
+ producer.flush();
+ Assertions.assertTrue(
+ checkData(
+ consumerTopic,
+ sinkStartOffset,
+ 10,
+ sourceData,
+
Collections.singletonList(keepAliveData)));
+ });
}
@TestTemplate
@@ -1754,21 +1770,40 @@ public class KafkaIT extends TestSuiteBase implements
TestResource {
// Compare the values of data fields obtained from consumers
private boolean checkData(String topicName, long startOffset, long
expectedCount, String data) {
- List<String> listData = getKafkaConsumerListData(topicName,
startOffset, expectedCount);
- if (listData.isEmpty() || listData.size() != expectedCount) {
+ return checkData(topicName, startOffset, expectedCount, data,
Collections.emptyList());
+ }
+
+ private boolean checkData(
+ String topicName,
+ long startOffset,
+ long expectedCount,
+ String data,
+ List<String> ignoredValues) {
+ List<String> listData = getKafkaConsumerListData(topicName,
startOffset);
+ List<String> matchedData = new ArrayList<>();
+ for (String value : listData) {
+ if (data.equals(value)) {
+ matchedData.add(value);
+ continue;
+ }
+ if (ignoredValues.contains(value)) {
+ continue;
+ }
log.error(
- "testKafkaToKafkaExactlyOnce get data size is not
expect,get consumer data size {},start offset {},expected count {}",
+ "testKafkaToKafkaExactlyOnce get unexpected data value {},
start offset {}",
+ value,
+ startOffset);
+ return false;
+ }
+ if (matchedData.isEmpty() || matchedData.size() != expectedCount) {
+ log.error(
+ "testKafkaToKafkaExactlyOnce get data size is not
expect,get matched data size {},visible data size {},start offset {},expected
count {}",
+ matchedData.size(),
listData.size(),
startOffset,
expectedCount);
return false;
}
- for (String value : listData) {
- if (!data.equals(value)) {
- log.error("testKafkaToKafkaExactlyOnce get data value is not
expect");
- return false;
- }
- }
return true;
}
@@ -2305,8 +2340,7 @@ public class KafkaIT extends TestSuiteBase implements
TestResource {
}
}
- private List<String> getKafkaConsumerListData(
- String topicName, long startOffset, long expectedCount) {
+ private List<String> getKafkaConsumerListData(String topicName, long
startOffset) {
KafkaConsumer<String, String> consumer = null;
try {
List<String> data = new ArrayList<>();
@@ -2314,17 +2348,28 @@ public class KafkaIT extends TestSuiteBase implements
TestResource {
TopicPartition topicPartition = new TopicPartition(topicName, 0);
consumer.assign(Collections.singletonList(topicPartition));
consumer.seek(topicPartition, startOffset);
- long targetOffsetExclusive = startOffset + expectedCount;
+ // READ_COMMITTED consumers may skip aborted transactional
offsets, so N committed
+ // records are not guaranteed to occupy N contiguous offsets after
startOffset.
+ long visibleEndOffsetExclusive =
+
consumer.endOffsets(Collections.singletonList(topicPartition))
+ .get(topicPartition);
Long lastProcessedOffset = startOffset - 1;
+ int consecutiveEmptyPolls = 0;
do {
ConsumerRecords<String, String> records =
consumer.poll(Duration.ofMillis(100));
- for (ConsumerRecord<String, String> record : records) {
- if (record.offset() >= startOffset && record.offset() <
targetOffsetExclusive) {
+ if (records.isEmpty()) {
+ consecutiveEmptyPolls++;
+ continue;
+ }
+ consecutiveEmptyPolls = 0;
+ for (ConsumerRecord<String, String> record :
records.records(topicPartition)) {
+ if (record.offset() >= startOffset && record.offset() >
lastProcessedOffset) {
data.add(record.value());
}
lastProcessedOffset = record.offset();
}
- } while (lastProcessedOffset < targetOffsetExclusive - 1);
+ } while (lastProcessedOffset < visibleEndOffsetExclusive - 1
+ && consecutiveEmptyPolls < 20);
return data;
} finally {
closeKafkaConsumer(consumer);