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 b810f07479 [Test][E2E] Stabilize Kafka restore start-mode tests
(#11426)
b810f07479 is described below
commit b810f07479d8a8c46b68c1b4dedb6c39124a0eeb
Author: Daniel <[email protected]>
AuthorDate: Thu Jul 30 23:48:53 2026 +0800
[Test][E2E] Stabilize Kafka restore start-mode tests (#11426)
Co-authored-by: DanielLeens <[email protected]>
---
.../seatunnel/e2e/connector/kafka/KafkaIT.java | 459 +++++++++++++++------
.../kafkasource_restore_with_earliest_mode.conf | 6 +-
.../kafkasource_restore_with_latest_mode.conf | 6 +-
...asource_restore_with_specific_offsets_mode.conf | 6 +-
.../kafkasource_restore_with_timestamp_mode.conf | 6 +-
5 files changed, 339 insertions(+), 144 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 f906bab4c6..ea1fc779d9 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
@@ -105,14 +105,17 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
+import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@@ -808,10 +811,19 @@ public class KafkaIT extends TestSuiteBase implements
TestResource {
public void testSourceKafkaRestoreWithEarliestMode(TestContainer container)
throws IOException, InterruptedException {
- final String sourceTopic = "test_topic_restore_earliest";
- final String sinkTopic = "test_topic_restore_earliest_output";
+ String resourceSuffix = startModeTopic(container, "restore-earliest");
+ final String sourceTopic = resourceSuffix + "_source";
+ final String sinkTopic = resourceSuffix + "_sink";
+ final String consumerGroup = resourceSuffix + "_group";
final String payload = "Seatunnel Restore Test Data";
final String jobId = "18696753645408";
+ List<String> restoreVariables =
+ buildKafkaRestoreVariables(sourceTopic, sinkTopic,
consumerGroup);
+
+ // The Kafka broker is shared across all engine variants in this
class, so restore tests
+ // need dedicated topics and groups to avoid inheriting offsets from
earlier template runs.
+ createKafkaTopic(sourceTopic);
+ createKafkaTopic(sinkTopic);
// Write 20 initial records with unique keys (avoid any potential
dedup logic
// elsewhere).
@@ -825,18 +837,22 @@ public class KafkaIT extends TestSuiteBase implements
TestResource {
long srcEndBeforeStart = endOffsetOnP0(sourceTopic);
// Start the first streaming job asynchronously.
- CompletableFuture.runAsync(
- () -> {
- try {
- container.executeJob(
-
"/kafka/kafkasource_restore_with_earliest_mode.conf", jobId);
- } catch (Exception e) {
- log.error("First job execution exception", e);
- throw new RuntimeException(e);
- }
- });
+ CompletableFuture<Container.ExecResult> firstJobFuture =
+ CompletableFuture.supplyAsync(
+ () -> {
+ try {
+ return container.executeJob(
+
"/kafka/kafkasource_restore_with_earliest_mode.conf",
+ jobId,
+ restoreVariables.toArray(new
String[0]));
+ } catch (Exception e) {
+ log.error("First job execution exception", e);
+ throw new RuntimeException(e);
+ }
+ });
awaitJobRunning(container, jobId);
+ waitForStreamingJobStartup(firstJobFuture);
// Produce 10 additional records after the job starts.
for (int i = 0; i < 10; i++) {
@@ -857,7 +873,8 @@ public class KafkaIT extends TestSuiteBase implements
TestResource {
.until(() -> visibleCountOnP0(sinkTopic) ==
expectedSinkAfterFirstRun);
// Savepoint the running job (so restore should continue from this
position).
- container.savepointJob(jobId);
+ assertCommandSucceeded("Savepoint", container.savepointJob(jobId));
+ assertCommandSucceeded("Initial job",
waitForJobResult(firstJobFuture));
// Append 15 records after savepoint, used to validate restore
progress.
for (int i = 0; i < 15; i++) {
@@ -879,18 +896,24 @@ public class KafkaIT extends TestSuiteBase implements
TestResource {
// Deliberately move the consumer-group offset past the savepoint
position.
// Restore must still use the checkpointed split offsets instead of
the external group
// offset, otherwise the 15 post-savepoint records will be skipped.
- commitOffset(sourceTopic, "test_restore_group", srcEndAfterAll);
+ commitOffset(sourceTopic, consumerGroup, srcEndAfterAll);
// Restore the job from the savepoint asynchronously.
- CompletableFuture.runAsync(
- () -> {
- try {
- container.restoreJob(
-
"/kafka/kafkasource_restore_with_earliest_mode.conf", jobId);
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- });
+ CompletableFuture<Container.ExecResult> restoredJobFuture =
+ CompletableFuture.supplyAsync(
+ () -> {
+ try {
+ return container.restoreJob(
+
"/kafka/kafkasource_restore_with_earliest_mode.conf",
+ jobId,
+ restoreVariables.toArray(new
String[0]));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ });
+
+ awaitJobRunning(container, jobId);
+ waitForStreamingJobStartup(restoredJobFuture);
// After restore, sink should advance by the 15 newly produced records
at
// minimum.
@@ -898,6 +921,8 @@ public class KafkaIT extends TestSuiteBase implements
TestResource {
.pollInterval(2, SECONDS)
.atMost(5, MINUTES)
.until(() -> visibleCountOnP0(sinkTopic) ==
expectedSinkAfterFirstRun + 15);
+ assertCommandSucceeded("Restored job cancellation",
container.cancelJob(jobId));
+ assertCommandSucceeded("Restored job",
waitForJobResult(restoredJobFuture));
}
// ------------------------------ LATEST MODE
------------------------------
@@ -909,12 +934,19 @@ public class KafkaIT extends TestSuiteBase implements
TestResource {
public void testSourceKafkaRestoreWithLatestMode(TestContainer container)
throws IOException, InterruptedException {
- final String sourceTopic = "test_topic_restore_latest";
- final String sinkTopic = "test_topic_restore_latest_output";
+ String resourceSuffix = startModeTopic(container, "restore-latest");
+ final String sourceTopic = resourceSuffix + "_source";
+ final String sinkTopic = resourceSuffix + "_sink";
+ final String consumerGroup = resourceSuffix + "_group";
final String payload = "Seatunnel Restore Test Data Latest";
final String firstRunPayload = payload + "_additional";
final String restorePayload = payload + "_restore";
final String jobId = "18696753645410";
+ List<String> restoreVariables =
+ buildKafkaRestoreVariables(sourceTopic, sinkTopic,
consumerGroup);
+
+ createKafkaTopic(sourceTopic);
+ createKafkaTopic(sinkTopic);
// Write 20 initial records before starting the job.
for (int i = 0; i < 20; i++) {
@@ -925,18 +957,22 @@ public class KafkaIT extends TestSuiteBase implements
TestResource {
long srcEndBeforeStart = endOffsetOnP0(sourceTopic);
- CompletableFuture.runAsync(
- () -> {
- try {
- container.executeJob(
-
"/kafka/kafkasource_restore_with_latest_mode.conf", jobId);
- } catch (Exception e) {
- log.error("First job execution exception", e);
- throw new RuntimeException(e);
- }
- });
+ CompletableFuture<Container.ExecResult> firstJobFuture =
+ CompletableFuture.supplyAsync(
+ () -> {
+ try {
+ return container.executeJob(
+
"/kafka/kafkasource_restore_with_latest_mode.conf",
+ jobId,
+ restoreVariables.toArray(new
String[0]));
+ } catch (Exception e) {
+ log.error("First job execution exception", e);
+ throw new RuntimeException(e);
+ }
+ });
awaitJobRunning(container, jobId);
+ waitForStreamingJobStartup(firstJobFuture);
AtomicInteger latestModeProbeId = new AtomicInteger();
Awaitility.await()
@@ -978,7 +1014,8 @@ public class KafkaIT extends TestSuiteBase implements
TestResource {
Assertions.assertEquals(
10, countKafkaMessages(sinkTopic,
firstRunPayload)));
- container.savepointJob(jobId);
+ assertCommandSucceeded("Savepoint", container.savepointJob(jobId));
+ assertCommandSucceeded("Initial job",
waitForJobResult(firstJobFuture));
// Append 15 more records after savepoint.
for (int i = 0; i < 15; i++) {
@@ -995,15 +1032,20 @@ public class KafkaIT extends TestSuiteBase implements
TestResource {
srcEndAfterAll >= srcEndBeforeStart + 25,
"Final end offset should advance by at least 25");
- CompletableFuture.runAsync(
- () -> {
- try {
- container.restoreJob(
-
"/kafka/kafkasource_restore_with_latest_mode.conf", jobId);
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- });
+ CompletableFuture<Container.ExecResult> restoredJobFuture =
+ CompletableFuture.supplyAsync(
+ () -> {
+ try {
+ return container.restoreJob(
+
"/kafka/kafkasource_restore_with_latest_mode.conf",
+ jobId,
+ restoreVariables.toArray(new
String[0]));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ });
+
+ awaitJobRunning(container, jobId);
Awaitility.await()
.pollInterval(2, SECONDS)
@@ -1012,6 +1054,8 @@ public class KafkaIT extends TestSuiteBase implements
TestResource {
() ->
Assertions.assertEquals(
15, countKafkaMessages(sinkTopic,
restorePayload)));
+ assertCommandSucceeded("Restored job cancellation",
container.cancelJob(jobId));
+ assertCommandSucceeded("Restored job",
waitForJobResult(restoredJobFuture));
}
// ---------------------------- TIMESTAMP MODE
-----------------------------
@@ -1023,10 +1067,17 @@ public class KafkaIT extends TestSuiteBase implements
TestResource {
public void testSourceKafkaRestoreWithTimestampMode(TestContainer
container)
throws IOException, InterruptedException {
- final String sourceTopic = "test_topic_restore_timestamp";
- final String sinkTopic = "test_topic_restore_timestamp_output";
+ String resourceSuffix = startModeTopic(container, "restore-timestamp");
+ final String sourceTopic = resourceSuffix + "_source";
+ final String sinkTopic = resourceSuffix + "_sink";
+ final String consumerGroup = resourceSuffix + "_group";
final String payload = "Seatunnel Restore Test Data Timestamp";
final String jobId = "18696753645411";
+ List<String> restoreVariables =
+ buildKafkaRestoreVariables(sourceTopic, sinkTopic,
consumerGroup);
+
+ createKafkaTopic(sourceTopic);
+ createKafkaTopic(sinkTopic);
for (int i = 0; i < 20; i++) {
producer.send(
@@ -1036,16 +1087,19 @@ public class KafkaIT extends TestSuiteBase implements
TestResource {
long srcEndBeforeStart = endOffsetOnP0(sourceTopic);
- CompletableFuture.runAsync(
- () -> {
- try {
- container.executeJob(
-
"/kafka/kafkasource_restore_with_timestamp_mode.conf", jobId);
- } catch (Exception e) {
- log.error("First job execution exception", e);
- throw new RuntimeException(e);
- }
- });
+ CompletableFuture<Container.ExecResult> firstJobFuture =
+ CompletableFuture.supplyAsync(
+ () -> {
+ try {
+ return container.executeJob(
+
"/kafka/kafkasource_restore_with_timestamp_mode.conf",
+ jobId,
+ restoreVariables.toArray(new
String[0]));
+ } catch (Exception e) {
+ log.error("First job execution exception", e);
+ throw new RuntimeException(e);
+ }
+ });
awaitJobRunning(container, jobId);
@@ -1067,7 +1121,8 @@ public class KafkaIT extends TestSuiteBase implements
TestResource {
.atMost(2, MINUTES)
.until(() -> visibleCountOnP0(sinkTopic) ==
expectedSinkAfterFirstRun);
- container.savepointJob(jobId);
+ assertCommandSucceeded("Savepoint", container.savepointJob(jobId));
+ assertCommandSucceeded("Initial job",
waitForJobResult(firstJobFuture));
// Append 15 more records after savepoint.
for (int i = 0; i < 15; i++) {
@@ -1084,20 +1139,27 @@ public class KafkaIT extends TestSuiteBase implements
TestResource {
srcEndAfterAll == srcEndBeforeStart + 25,
"Final end offset should advance by at least 25");
- CompletableFuture.runAsync(
- () -> {
- try {
- container.restoreJob(
-
"/kafka/kafkasource_restore_with_timestamp_mode.conf", jobId);
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- });
+ CompletableFuture<Container.ExecResult> restoredJobFuture =
+ CompletableFuture.supplyAsync(
+ () -> {
+ try {
+ return container.restoreJob(
+
"/kafka/kafkasource_restore_with_timestamp_mode.conf",
+ jobId,
+ restoreVariables.toArray(new
String[0]));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ });
+
+ awaitJobRunning(container, jobId);
Awaitility.await()
.pollInterval(2, SECONDS)
.atMost(5, MINUTES)
.until(() -> visibleCountOnP0(sinkTopic) ==
expectedSinkAfterFirstRun + 15);
+ assertCommandSucceeded("Restored job cancellation",
container.cancelJob(jobId));
+ assertCommandSucceeded("Restored job",
waitForJobResult(restoredJobFuture));
}
// ------------------------- SPECIFIC OFFSETS MODE
-------------------------
@@ -1110,9 +1172,19 @@ public class KafkaIT extends TestSuiteBase implements
TestResource {
throws IOException, InterruptedException {
final String sourceTopic = "test_topic_restore_specific_offsets";
- final String sinkTopic = "test_topic_restore_specific_offsets_output";
+ String resourceSuffix = startModeTopic(container,
"restore-specific-offsets");
+ final String sinkTopic = resourceSuffix + "_sink";
+ final String consumerGroup = resourceSuffix + "_group";
final String payload = "Seatunnel Restore Test Data Specific Offsets";
final String jobId = "18696753645412";
+ List<String> restoreVariables =
+ buildKafkaRestoreVariables(sourceTopic, sinkTopic,
consumerGroup);
+
+ // The specific_offsets config stores the source topic inside a map
key, and current
+ // variable replacement does not rewrite config keys. Recreate the
fixed source topic
+ // before each run so the test still gets isolated offsets without
changing parser rules.
+ recreateKafkaTopic(sourceTopic);
+ createKafkaTopic(sinkTopic);
for (int i = 0; i < 20; i++) {
producer.send(
@@ -1122,17 +1194,19 @@ public class KafkaIT extends TestSuiteBase implements
TestResource {
long srcEndBeforeStart = endOffsetOnP0(sourceTopic);
- CompletableFuture.runAsync(
- () -> {
- try {
- container.executeJob(
-
"/kafka/kafkasource_restore_with_specific_offsets_mode.conf",
- jobId);
- } catch (Exception e) {
- log.error("First job execution exception", e);
- throw new RuntimeException(e);
- }
- });
+ CompletableFuture<Container.ExecResult> firstJobFuture =
+ CompletableFuture.supplyAsync(
+ () -> {
+ try {
+ return container.executeJob(
+
"/kafka/kafkasource_restore_with_specific_offsets_mode.conf",
+ jobId,
+ restoreVariables.toArray(new
String[0]));
+ } catch (Exception e) {
+ log.error("First job execution exception", e);
+ throw new RuntimeException(e);
+ }
+ });
awaitJobRunning(container, jobId);
@@ -1154,7 +1228,8 @@ public class KafkaIT extends TestSuiteBase implements
TestResource {
.atMost(2, MINUTES)
.until(() -> visibleCountOnP0(sinkTopic) ==
expectedSinkAfterFirstRun - 11);
- container.savepointJob(jobId);
+ assertCommandSucceeded("Savepoint", container.savepointJob(jobId));
+ assertCommandSucceeded("Initial job",
waitForJobResult(firstJobFuture));
// Append 15 more records after savepoint.
for (int i = 0; i < 15; i++) {
@@ -1171,21 +1246,27 @@ public class KafkaIT extends TestSuiteBase implements
TestResource {
srcEndAfterAll == srcEndBeforeStart + 25,
"Final end offset should advance by at least 25");
- CompletableFuture.runAsync(
- () -> {
- try {
- container.restoreJob(
-
"/kafka/kafkasource_restore_with_specific_offsets_mode.conf",
- jobId);
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- });
+ CompletableFuture<Container.ExecResult> restoredJobFuture =
+ CompletableFuture.supplyAsync(
+ () -> {
+ try {
+ return container.restoreJob(
+
"/kafka/kafkasource_restore_with_specific_offsets_mode.conf",
+ jobId,
+ restoreVariables.toArray(new
String[0]));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ });
+
+ awaitJobRunning(container, jobId);
Awaitility.await()
.pollInterval(2, SECONDS)
.atMost(5, MINUTES)
.until(() -> visibleCountOnP0(sinkTopic) ==
expectedSinkAfterFirstRun + 15 - 11);
+ assertCommandSucceeded("Restored job cancellation",
container.cancelJob(jobId));
+ assertCommandSucceeded("Restored job",
waitForJobResult(restoredJobFuture));
}
private void awaitJobRunning(TestContainer container, String jobId) {
@@ -1196,6 +1277,69 @@ public class KafkaIT extends TestSuiteBase implements
TestResource {
() -> Assertions.assertEquals("RUNNING",
container.getJobStatus(jobId)));
}
+ /**
+ * Wait for a streaming submit command to exit after savepoint or
cancellation.
+ *
+ * <p>A bounded wait prevents a failed lifecycle transition from leaving
the test thread blocked
+ * indefinitely.
+ */
+ private Container.ExecResult waitForJobResult(
+ CompletableFuture<Container.ExecResult> jobFuture) {
+ try {
+ return jobFuture.get(2, MINUTES);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new AssertionError("Interrupted while waiting for streaming
job completion", e);
+ } catch (ExecutionException | TimeoutException e) {
+ throw new AssertionError("Streaming job command did not complete
successfully", e);
+ }
+ }
+
+ /**
+ * Give a streaming submit command a short startup window before the test
publishes data that
+ * must only be observed after initialization.
+ */
+ private void
waitForStreamingJobStartup(CompletableFuture<Container.ExecResult> jobFuture) {
+ try {
+ jobFuture.get(15, SECONDS);
+ throw new AssertionError("Streaming job finished before startup
grace window elapsed");
+ } catch (TimeoutException expected) {
+ // Expected path: the job is still running and had time to
initialize.
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new AssertionError("Interrupted while waiting for streaming
job startup", e);
+ } catch (ExecutionException e) {
+ throw new AssertionError("Streaming job failed before startup
grace window elapsed", e);
+ }
+ }
+
+ /**
+ * Publish a probe record and wait until it becomes visible on the sink
topic so later
+ * assertions only count business records emitted after the Kafka source
is actively consuming.
+ */
+ private long awaitStreamingPipelineReady(
+ String producerTopic, String consumerTopic, long sinkStartOffset,
String probeData) {
+ sendTextRecordAndWait(producerTopic, probeData);
+ given().pollDelay(5, SECONDS)
+ .pollInterval(5, SECONDS)
+ .await()
+ .atMost(2, MINUTES)
+ .untilAsserted(
+ () ->
+ Assertions.assertTrue(
+ checkData(consumerTopic,
sinkStartOffset, 1, probeData)));
+ return endOffsetOnP0(consumerTopic);
+ }
+
+ /**
+ * Assert that a lifecycle command completed successfully.
+ *
+ * <p>The command stderr is included so CI failures retain the
container-side error context.
+ */
+ private void assertCommandSucceeded(String action, Container.ExecResult
result) {
+ Assertions.assertEquals(0, result.getExitCode(), action + " failed: "
+ result.getStderr());
+ }
+
/**
* Get visible record count on partition-0: endOffset - beginningOffset
(exclusive upper bound).
*/
@@ -1926,51 +2070,38 @@ public class KafkaIT extends TestSuiteBase implements
TestResource {
createKafkaTopic(producerTopic);
createKafkaTopic(consumerTopic);
String sourceData = "Seatunnel Exactly Once Example";
- String keepAliveData = sourceData + "-keepalive-" + resourceSuffix;
+ String readinessProbeData = sourceData + "-probe-" + resourceSuffix;
long sinkStartOffset = endOffsetOnP0(consumerTopic);
- for (int i = 0; i < 10; i++) {
- ProducerRecord<byte[], byte[]> record =
- new ProducerRecord<>(producerTopic, null,
sourceData.getBytes());
- producer.send(record);
- producer.flush();
- }
// async execute
- CompletableFuture.supplyAsync(
- () -> {
- try {
- container.executeJob(
-
"/kafka/kafka_to_kafka_exactly_once_streaming.conf",
- exactlyOnceVariables);
- } catch (Exception e) {
- log.error("Commit task exception :" + e.getMessage());
- throw new RuntimeException(e);
- }
- return null;
- });
+ CompletableFuture<Container.ExecResult> jobFuture =
+ CompletableFuture.supplyAsync(
+ () -> {
+ try {
+ return container.executeJob(
+
"/kafka/kafka_to_kafka_exactly_once_streaming.conf",
+ exactlyOnceVariables);
+ } catch (Exception e) {
+ log.error("Commit task exception :" +
e.getMessage());
+ throw new RuntimeException(e);
+ }
+ });
+ waitForStreamingJobStartup(jobFuture);
+ long sinkDataStartOffset =
+ awaitStreamingPipelineReady(
+ producerTopic, consumerTopic, sinkStartOffset,
readinessProbeData);
+ for (int i = 0; i < 10; i++) {
+ sendTextRecordAndWait(producerTopic, sourceData);
+ }
// wait for data written to kafka
- given().pollDelay(120, SECONDS)
+ given().pollDelay(10, SECONDS)
.pollInterval(5, SECONDS)
.await()
.atMost(5, MINUTES)
.untilAsserted(
() -> {
- // 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)));
+ checkData(consumerTopic,
sinkDataStartOffset, 10, sourceData));
});
}
@@ -2010,6 +2141,23 @@ public class KafkaIT extends TestSuiteBase implements
TestResource {
return checkData(topicName, startOffset, expectedCount, data,
Collections.emptyList());
}
+ /**
+ * Sends one text record and waits for the broker acknowledgement so
producer-side delivery
+ * issues surface immediately instead of appearing later as assertion
flakes.
+ */
+ private void sendTextRecordAndWait(String topicName, String data) {
+ ProducerRecord<byte[], byte[]> record =
+ new ProducerRecord<>(topicName, null,
data.getBytes(StandardCharsets.UTF_8));
+ try {
+ producer.send(record).get();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new RuntimeException("Interrupted while sending Kafka test
record", e);
+ } catch (ExecutionException e) {
+ throw new RuntimeException("Failed to send Kafka test record to
topic " + topicName, e);
+ }
+ }
+
private boolean checkData(
String topicName,
long startOffset,
@@ -2318,15 +2466,18 @@ public class KafkaIT extends TestSuiteBase implements
TestResource {
}
/**
- * Create a dedicated Kafka topic for the exactly-once tests so each
method reads its own data
- * and never reuses offsets from earlier runs in the same class.
+ * Create a dedicated Kafka topic for streaming tests that require
isolated records and offsets.
+ *
+ * <p>Each registered topic is removed during class cleanup.
*/
private void createKafkaTopic(String topicName) {
NewTopic topic = new NewTopic(topicName, 1, (short) 1);
topic.configs(Collections.singletonMap("retention.ms", "-1"));
try (AdminClient adminClient = createKafkaAdmin()) {
adminClient.createTopics(Collections.singletonList(topic)).all().get();
- dynamicTopics.add(topicName);
+ if (!dynamicTopics.contains(topicName)) {
+ dynamicTopics.add(topicName);
+ }
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException(
@@ -2336,6 +2487,36 @@ public class KafkaIT extends TestSuiteBase implements
TestResource {
}
}
+ /**
+ * Recreate a fixed topic so restore tests can start from a clean
partition state without
+ * inheriting offsets or retained records from earlier runs.
+ */
+ private void recreateKafkaTopic(String topicName) {
+ try (AdminClient adminClient = createKafkaAdmin()) {
+
adminClient.deleteTopics(Collections.singletonList(topicName)).all().get();
+ Awaitility.await()
+ .atMost(Duration.ofSeconds(30))
+ .untilAsserted(
+ () ->
+ Assertions.assertFalse(
+ adminClient
+ .listTopics()
+ .names()
+ .get()
+ .contains(topicName)));
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException(
+ "Interrupted while recreating Kafka topic " + topicName,
e);
+ } catch (ExecutionException e) {
+ log.info(
+ "Kafka topic {} did not exist before recreation or could
not be deleted cleanly: {}",
+ topicName,
+ e.getMessage());
+ }
+ createKafkaTopic(topicName);
+ }
+
/** Build the dynamic `-i key=value` variables for the exactly-once
streaming template. */
private List<String> buildExactlyOnceStreamingVariables(
String sourceTopic, String sinkTopic, String consumerGroup) {
@@ -2345,6 +2526,19 @@ public class KafkaIT extends TestSuiteBase implements
TestResource {
EXACTLY_ONCE_CONSUMER_GROUP_VARIABLE + "=" + consumerGroup);
}
+ /**
+ * Build the dynamic {@code -i key=value} variables for Kafka restore-mode
templates.
+ *
+ * <p>Each test invocation uses isolated Kafka topics and a consumer group.
+ */
+ private List<String> buildKafkaRestoreVariables(
+ String sourceTopic, String sinkTopic, String consumerGroup) {
+ return Arrays.asList(
+ "sourceTopic=" + sourceTopic,
+ "sinkTopic=" + sinkTopic,
+ "consumerGroup=" + consumerGroup);
+ }
+
private void initKafkaProducer() {
Properties props = new Properties();
String bootstrapServers = kafkaContainer.getBootstrapServers();
@@ -2581,6 +2775,7 @@ public class KafkaIT extends TestSuiteBase implements
TestResource {
KafkaConsumer<String, String> consumer = null;
try {
List<String> data = new ArrayList<>();
+ Set<Long> seenOffsets = new HashSet<>();
consumer = new KafkaConsumer<>(kafkaManualConsumerConfig());
TopicPartition topicPartition = new TopicPartition(topicName, 0);
consumer.assign(Collections.singletonList(topicPartition));
@@ -2590,7 +2785,7 @@ public class KafkaIT extends TestSuiteBase implements
TestResource {
long visibleEndOffsetExclusive =
consumer.endOffsets(Collections.singletonList(topicPartition))
.get(topicPartition);
- Long lastProcessedOffset = startOffset - 1;
+ long lastObservedOffset = startOffset - 1;
int consecutiveEmptyPolls = 0;
do {
ConsumerRecords<String, String> records =
consumer.poll(Duration.ofMillis(100));
@@ -2600,12 +2795,12 @@ public class KafkaIT extends TestSuiteBase implements
TestResource {
}
consecutiveEmptyPolls = 0;
for (ConsumerRecord<String, String> record :
records.records(topicPartition)) {
- if (record.offset() >= startOffset && record.offset() >
lastProcessedOffset) {
+ if (record.offset() >= startOffset &&
seenOffsets.add(record.offset())) {
data.add(record.value());
}
- lastProcessedOffset = record.offset();
+ lastObservedOffset = Math.max(lastObservedOffset,
record.offset());
}
- } while (lastProcessedOffset < visibleEndOffsetExclusive - 1
+ } while (lastObservedOffset < visibleEndOffsetExclusive - 1
&& consecutiveEmptyPolls < 20);
return data;
} finally {
diff --git
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafkasource_restore_with_earliest_mode.conf
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafkasource_restore_with_earliest_mode.conf
index ac4c7d9117..ed34c090d8 100644
---
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafkasource_restore_with_earliest_mode.conf
+++
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafkasource_restore_with_earliest_mode.conf
@@ -25,10 +25,10 @@ env {
source {
Kafka {
bootstrap.servers = "kafkaCluster:9092"
- topic = "test_topic_restore_earliest"
+ topic = "${sourceTopic}"
format = text
start_mode = earliest
- consumer.group = "test_restore_group"
+ consumer.group = "${consumerGroup}"
discovery.interval.millis = 10000
}
}
@@ -39,7 +39,7 @@ transform {
sink {
Kafka {
bootstrap.servers = "kafkaCluster:9092"
- topic = "test_topic_restore_earliest_output"
+ topic = "${sinkTopic}"
format = text
}
}
diff --git
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafkasource_restore_with_latest_mode.conf
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafkasource_restore_with_latest_mode.conf
index 00b7b38c0a..f2c295dd8d 100644
---
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafkasource_restore_with_latest_mode.conf
+++
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafkasource_restore_with_latest_mode.conf
@@ -25,10 +25,10 @@ env {
source {
Kafka {
bootstrap.servers = "kafkaCluster:9092"
- topic = "test_topic_restore_latest"
+ topic = "${sourceTopic}"
format = text
start_mode = latest
- consumer.group = "test_restore_latest"
+ consumer.group = "${consumerGroup}"
discovery.interval.millis = 10000
}
}
@@ -39,7 +39,7 @@ transform {
sink {
Kafka {
bootstrap.servers = "kafkaCluster:9092"
- topic = "test_topic_restore_latest_output"
+ topic = "${sinkTopic}"
format = text
}
}
diff --git
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafkasource_restore_with_specific_offsets_mode.conf
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafkasource_restore_with_specific_offsets_mode.conf
index bc929bb24b..b285240349 100644
---
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafkasource_restore_with_specific_offsets_mode.conf
+++
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafkasource_restore_with_specific_offsets_mode.conf
@@ -25,13 +25,13 @@ env {
source {
Kafka {
bootstrap.servers = "kafkaCluster:9092"
- topic = "test_topic_restore_specific_offsets"
+ topic = "${sourceTopic}"
format = text
start_mode = specific_offsets
start_mode.offsets = {
test_topic_restore_specific_offsets-0 = 11
}
- consumer.group = "test_restore_specific_offsets"
+ consumer.group = "${consumerGroup}"
discovery.interval.millis = 10000
}
}
@@ -42,7 +42,7 @@ transform {
sink {
Kafka {
bootstrap.servers = "kafkaCluster:9092"
- topic = "test_topic_restore_specific_offsets_output"
+ topic = "${sinkTopic}"
format = text
}
}
diff --git
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafkasource_restore_with_timestamp_mode.conf
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafkasource_restore_with_timestamp_mode.conf
index f42dae81f5..5fd230317f 100644
---
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafkasource_restore_with_timestamp_mode.conf
+++
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafkasource_restore_with_timestamp_mode.conf
@@ -25,11 +25,11 @@ env {
source {
Kafka {
bootstrap.servers = "kafkaCluster:9092"
- topic = "test_topic_restore_timestamp"
+ topic = "${sourceTopic}"
format = text
start_mode = timestamp
start_mode.timestamp = 1738395840000
- consumer.group = "test_restore_timestamp"
+ consumer.group = "${consumerGroup}"
discovery.interval.millis = 10000
}
}
@@ -40,7 +40,7 @@ transform {
sink {
Kafka {
bootstrap.servers = "kafkaCluster:9092"
- topic = "test_topic_restore_timestamp_output"
+ topic = "${sinkTopic}"
format = text
}
}