This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch 
gh-readonly-queue/dev/pr-12032-5d7c81cdcb34867b4e963bc548d6d6eb16ed26a0
in repository https://gitbox.apache.org/repos/asf/seatunnel.git

commit 30170807f8d9d4608f03b294295129d17bb134f2
Author: David Zollo <[email protected]>
AuthorDate: Thu Sep 3 05:12:48 2026 +0000

    [Test][E2E] Add regression coverage for FAILED-pipeline metrics cleanup 
(#12032)
---
 .../seatunnel/engine/e2e/JobExecutionIT.java       | 105 +++++++++++++++++++++
 1 file changed, 105 insertions(+)

diff --git 
a/seatunnel-e2e/seatunnel-engine-e2e/connector-seatunnel-e2e-base/src/test/java/org/apache/seatunnel/engine/e2e/JobExecutionIT.java
 
b/seatunnel-e2e/seatunnel-engine-e2e/connector-seatunnel-e2e-base/src/test/java/org/apache/seatunnel/engine/e2e/JobExecutionIT.java
index c20aeb8522..54d8d122c6 100644
--- 
a/seatunnel-e2e/seatunnel-engine-e2e/connector-seatunnel-e2e-base/src/test/java/org/apache/seatunnel/engine/e2e/JobExecutionIT.java
+++ 
b/seatunnel-e2e/seatunnel-engine-e2e/connector-seatunnel-e2e-base/src/test/java/org/apache/seatunnel/engine/e2e/JobExecutionIT.java
@@ -27,7 +27,12 @@ import org.apache.seatunnel.engine.common.config.JobConfig;
 import org.apache.seatunnel.engine.common.config.SeaTunnelConfig;
 import org.apache.seatunnel.engine.common.job.JobResult;
 import org.apache.seatunnel.engine.common.job.JobStatus;
+import org.apache.seatunnel.engine.server.SeaTunnelServer;
 import org.apache.seatunnel.engine.server.SeaTunnelServerStarter;
+import org.apache.seatunnel.engine.server.dag.physical.PipelineLocation;
+import org.apache.seatunnel.engine.server.execution.TaskGroupLocation;
+import org.apache.seatunnel.engine.server.execution.TaskLocation;
+import org.apache.seatunnel.engine.server.metrics.SeaTunnelMetricsContext;
 
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Assertions;
@@ -38,6 +43,8 @@ import com.hazelcast.client.config.ClientConfig;
 import com.hazelcast.instance.impl.HazelcastInstanceImpl;
 import lombok.extern.slf4j.Slf4j;
 
+import java.util.HashMap;
+import java.util.Map;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
@@ -160,6 +167,104 @@ public class JobExecutionIT {
         }
     }
 
+    /**
+     * Regression test for the FAILED-pipeline metrics leak fixed by <a
+     * href="https://github.com/apache/seatunnel/pull/10757";>#10757</a> 
("[Fix][Zeta] Clean failed
+     * pipeline metrics without full-map cleanup scan"). Before that fix, 
{@code
+     * CoordinatorService#shouldCleanup} and {@code 
JobMaster#removeMetricsContext} only matched
+     * {@code PipelineStatus.CANCELED}/{@code FINISHED}, so a pipeline that 
ended in {@code FAILED}
+     * never had its entry removed from the coordinator's metrics {@code 
IMap}: it leaked there for
+     * the remaining life of the cluster, once per failed pipeline.
+     *
+     * <p>This reuses the same deterministic {@code NumberFormatException} 
failure as {@link
+     * #testGetErrorInfo()} (a SQL transform casting a random string to {@code 
int}), which fails
+     * the pipeline almost immediately after task deployment. That speed makes 
it unsafe to rely on
+     * the worker's real metrics-report cycle ({@code 
job-metrics-backup-interval}, 10 seconds by
+     * default) to populate the coordinator's metrics {@code IMap} before the 
pipeline ends: the
+     * pipeline can fail and have its task group context torn down before the 
next scheduled report
+     * ever fires, which would make "no metrics after FAILED" trivially true 
and prove nothing about
+     * the fix. Instead, this test seeds one metrics entry for the pipeline 
directly through {@link
+     * SeaTunnelServer#updateMetrics}, the exact entry point the worker's 
periodic reporter itself
+     * calls (via {@code ReportMetricsOperation}). From the coordinator 
cleanup logic's point of
+     * view, a seeded entry is indistinguishable from a worker-reported one: 
{@code
+     * CoordinatorService#shouldCleanup} and {@code 
#processPendingPipelineCleanup} match purely on
+     * {@link PipelineLocation} and the pipeline's terminal status recorded by 
the real job, not on
+     * how the metrics entry was populated.
+     *
+     * <p>Cleanup can complete either through the immediate best-effort call 
in {@code
+     * SubPlan#subPlanDone}, or, if that racing attempt lands before this test 
seeds the entry,
+     * through the 60-second scheduled {@code 
CoordinatorService#cleanupPendingPipelines} safety net
+     * introduced by <a 
href="https://github.com/apache/seatunnel/pull/10418";>#10418</a>. This test
+     * does not need to know which one fires; it only asserts the end-to-end 
guarantee that the
+     * entry does not survive forever.
+     */
+    @Test
+    public void testFailedPipelineMetricsAreCleanedUp()
+            throws ExecutionException, InterruptedException {
+        Common.setDeployMode(DeployMode.CLIENT);
+        String filePath = 
TestUtils.getResource("batch_fakesource_to_console_error.conf");
+        JobConfig jobConfig = new JobConfig();
+        jobConfig.setName("fake_to_console_error_cleanup");
+        ClientConfig clientConfig = ConfigProvider.locateAndGetClientConfig();
+        
clientConfig.setClusterName(TestUtils.getClusterName("JobExecutionIT"));
+        try (SeaTunnelClient engineClient = new SeaTunnelClient(clientConfig)) 
{
+            ClientJobExecutionEnvironment jobExecutionEnv =
+                    engineClient.createExecutionContext(filePath, jobConfig, 
SEATUNNEL_CONFIG);
+            final ClientJobProxy clientJobProxy = jobExecutionEnv.execute();
+
+            // A config with no shuffle/split boundary always compiles down to 
exactly one
+            // pipeline, whose id is always 1 (see 
ExecutionPlanGenerator#normalizePipeline), so
+            // the location can be constructed directly instead of racing the 
failing pipeline to
+            // read it off the JobMaster before its bookkeeping is torn down.
+            PipelineLocation pipelineLocation = new 
PipelineLocation(clientJobProxy.getJobId(), 1);
+
+            SeaTunnelServer seaTunnelServer =
+                    
hazelcastInstance.node.getNodeEngine().getService(SeaTunnelServer.SERVICE_NAME);
+
+            // Sentinel task group id 999: this entry stands in for whatever a 
worker would have
+            // reported for this pipeline. It is deliberately not tied to any 
of the job's real
+            // task groups so seeding it cannot race, or depend on, the job's 
own execution.
+            TaskGroupLocation seededTaskGroupLocation =
+                    new TaskGroupLocation(
+                            pipelineLocation.getJobId(), 
pipelineLocation.getPipelineId(), 999L);
+            TaskLocation seededTaskLocation = new 
TaskLocation(seededTaskGroupLocation, 0, 0);
+            Map<TaskLocation, SeaTunnelMetricsContext> seededMetrics = new 
HashMap<>();
+            seededMetrics.put(seededTaskLocation, new 
SeaTunnelMetricsContext());
+            seaTunnelServer.updateMetrics(seededMetrics);
+            Assertions.assertTrue(
+                    seaTunnelServer
+                            .getEngineContext()
+                            .getStateStores()
+                            .metricsSnapshotStore()
+                            .containsPipeline(pipelineLocation),
+                    "Sanity check: seeded metrics entry must be visible before 
asserting cleanup");
+
+            CompletableFuture<JobStatus> completableFuture =
+                    
CompletableFuture.supplyAsync(clientJobProxy::waitForJobComplete);
+            await().atMost(300000, TimeUnit.MILLISECONDS)
+                    .untilAsserted(() -> 
Assertions.assertTrue(completableFuture.isDone()));
+
+            JobResult result = clientJobProxy.getJobResultCache();
+            Assertions.assertEquals(JobStatus.FAILED, result.getStatus());
+
+            // Bound generously above 
CoordinatorService#PIPELINE_CLEANUP_INTERVAL_SECONDS (60s):
+            // the scheduled safety net's first run can land anywhere in that 
window relative to
+            // when this pipeline actually failed.
+            await().atMost(120, TimeUnit.SECONDS)
+                    .untilAsserted(
+                            () ->
+                                    Assertions.assertFalse(
+                                            seaTunnelServer
+                                                    .getEngineContext()
+                                                    .getStateStores()
+                                                    .metricsSnapshotStore()
+                                                    
.containsPipeline(pipelineLocation),
+                                            "Metrics for a FAILED pipeline 
must eventually be "
+                                                    + "removed from the 
coordinator's metrics "
+                                                    + "IMap, not retained 
forever"));
+        }
+    }
+
     @Test
     public void testValidJobNameInJobConfig() throws ExecutionException, 
InterruptedException {
         Common.setDeployMode(DeployMode.CLIENT);

Reply via email to