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

Jackie-Jiang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git


The following commit(s) were added to refs/heads/master by this push:
     new fe6511365d0 Simplify re-ingestion tracking and skip obsolete queued 
jobs (#19893)
fe6511365d0 is described below

commit fe6511365d0e067b15edf96e13b529c57fd14d30
Author: Xiaotian (Jackie) Jiang <[email protected]>
AuthorDate: Fri Oct 16 12:22:12 2026 -0700

    Simplify re-ingestion tracking and skip obsolete queued jobs (#19893)
---
 .../server/api/resources/ReingestionResource.java  |  71 +++++++------
 .../ReingestionConsumptionTimeoutTest.java         |  13 +--
 .../api/resources/ReingestionResourceApiTest.java  | 112 ++++++++++++++++++---
 3 files changed, 140 insertions(+), 56 deletions(-)

diff --git 
a/pinot-server/src/main/java/org/apache/pinot/server/api/resources/ReingestionResource.java
 
b/pinot-server/src/main/java/org/apache/pinot/server/api/resources/ReingestionResource.java
index 6264f4c4156..e6256156c80 100644
--- 
a/pinot-server/src/main/java/org/apache/pinot/server/api/resources/ReingestionResource.java
+++ 
b/pinot-server/src/main/java/org/apache/pinot/server/api/resources/ReingestionResource.java
@@ -40,10 +40,7 @@ import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.RejectedExecutionException;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.BooleanSupplier;
-import javax.annotation.Nullable;
 import javax.annotation.PreDestroy;
 import javax.inject.Inject;
 import javax.inject.Singleton;
@@ -66,6 +63,7 @@ import 
org.apache.pinot.segment.local.realtime.writer.StatelessRealtimeSegmentWr
 import org.apache.pinot.segment.local.segment.index.loader.IndexLoadingConfig;
 import org.apache.pinot.server.realtime.ServerSegmentCompletionProtocolHandler;
 import org.apache.pinot.server.starter.ServerInstance;
+import org.apache.pinot.spi.utils.CommonConstants.Segment.Realtime.Status;
 import org.apache.pinot.spi.utils.builder.TableNameBuilder;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -93,14 +91,11 @@ public class ReingestionResource {
   static final int MAX_PARALLEL_REINGESTIONS = 
Math.max(Runtime.getRuntime().availableProcessors() / 2, 1);
   public static final long CHECK_INTERVAL_MS = 
Duration.ofSeconds(5).toMillis();
 
-  // Tracks if a particular segment is currently being re-ingested
-  private final ConcurrentHashMap<String, AtomicBoolean> _reingestingSegments 
= new ConcurrentHashMap<>();
-
   // Executor for asynchronous re-ingestion
   private final ExecutorService _reingestionExecutor = 
Executors.newFixedThreadPool(MAX_PARALLEL_REINGESTIONS,
       new 
ThreadFactoryBuilder().setNameFormat("reingestion-worker-%d").setDaemon(true).build());
 
-  // Keep track of jobs by jobId => job info
+  // Tracks queued and running jobs by segment name, also preventing duplicate 
repairs
   private final ConcurrentHashMap<String, ReingestionJob> _runningJobs = new 
ConcurrentHashMap<>();
 
   @Inject
@@ -142,7 +137,6 @@ public class ReingestionResource {
   @ApiOperation("Get all running re-ingestion jobs, including the ones waiting 
for a re-ingestion thread, along with "
       + "job IDs")
   public Response getAllRunningReingestionJobs() {
-    // Filter only the jobs still marked as running, including the ones 
waiting for a re-ingestion thread
     List<ReingestionJob> runningJobs = new ArrayList<>(_runningJobs.values());
     return Response.ok(runningJobs).build();
   }
@@ -198,42 +192,31 @@ public class ReingestionResource {
           Response.Status.INTERNAL_SERVER_ERROR);
     }
 
-    IndexLoadingConfig indexLoadingConfig = 
tableDataManager.fetchIndexLoadingConfig();
-
-    // Check if this segment is already being re-ingested
-    AtomicBoolean isIngesting = 
_reingestingSegments.computeIfAbsent(segmentName, k -> new 
AtomicBoolean(false));
-    if (!isIngesting.compareAndSet(false, true)) {
+    String jobId = UUID.randomUUID().toString();
+    ReingestionJob job = new ReingestionJob(jobId, segmentName);
+    // Track the job before submitting it, so queued jobs are listed and 
duplicate requests are rejected
+    if (_runningJobs.putIfAbsent(segmentName, job) != null) {
       return Response.status(Response.Status.CONFLICT)
           .entity("Re-ingestion for segment: " + segmentName + " is already in 
progress.")
           .build();
     }
 
-    // Generate a jobId for tracking
-    String jobId = UUID.randomUUID().toString();
-    ReingestionJob job = new ReingestionJob(jobId, segmentName);
-
-    // Track the job before submitting it, so that it is listed while waiting 
for a re-ingestion thread
-    _runningJobs.put(jobId, job);
-
     // Kick off the actual work asynchronously
     try {
       _reingestionExecutor.submit(() -> {
         try {
-          doReingestSegment(realtimeTableName, segmentZKMetadata, 
indexLoadingConfig,
-              tableDataManager.getSegmentBuildSemaphore());
+          doReingestSegment(realtimeTableName, segmentName);
         } catch (Exception e) {
           LOGGER.error("Error during async re-ingestion for job {} 
(segment={})", jobId, segmentName, e);
           _serverInstance.getServerMetrics()
               .addMeteredTableValue(realtimeTableName, 
ServerMeter.SEGMENT_REINGESTION_FAILURE, 1);
         } finally {
-          _runningJobs.remove(jobId);
-          _reingestingSegments.remove(segmentName);
+          _runningJobs.remove(segmentName, job);
         }
       });
     } catch (RejectedExecutionException e) {
       // The executor is shut down when the admin API stops
-      _runningJobs.remove(jobId);
-      _reingestingSegments.remove(segmentName);
+      _runningJobs.remove(segmentName, job);
       throw new WebApplicationException("Server is shutting down, cannot 
re-ingest segment: " + segmentName,
           Response.Status.SERVICE_UNAVAILABLE);
     }
@@ -249,14 +232,38 @@ public class ReingestionResource {
     LOGGER.info("Stopped re-ingestion executor, dropped {} jobs waiting for a 
re-ingestion thread", numDroppedJobs);
   }
 
-  /// The actual re-ingestion logic, moved into a separate method for clarity.
-  /// This is essentially the old synchronous logic you had in reingestSegment.
-  private void doReingestSegment(String realtimeTableName, SegmentZKMetadata 
segmentZKMetadata,
-      IndexLoadingConfig indexLoadingConfig, @Nullable Semaphore 
segmentBuildSemaphore)
+  private void doReingestSegment(String realtimeTableName, String segmentName)
       throws Exception {
-    String segmentName = segmentZKMetadata.getSegmentName();
+    // The table or segment may have been removed or repaired on another 
replica while this job was queued.
+    RealtimeTableDataManager tableDataManager =
+        (RealtimeTableDataManager) 
_serverInstance.getInstanceDataManager().getTableDataManager(realtimeTableName);
+    if (tableDataManager == null) {
+      LOGGER.warn("Skipping re-ingestion of segment: {} because table: {} is 
no longer available", segmentName,
+          realtimeTableName);
+      return;
+    }
+    if (tableDataManager.isShutDown()) {
+      LOGGER.warn("Skipping re-ingestion of segment: {} because table: {} is 
shut down", segmentName,
+          realtimeTableName);
+      return;
+    }
+    SegmentZKMetadata segmentZKMetadata = 
tableDataManager.fetchZKMetadataNullable(segmentName);
+    if (segmentZKMetadata == null) {
+      LOGGER.warn("Skipping re-ingestion of segment: {} because its metadata 
no longer exists", segmentName);
+      return;
+    }
+    if (segmentZKMetadata.getStatus() != Status.COMMITTING) {
+      LOGGER.warn("Skipping re-ingestion of segment: {} because its status is 
{} (expected COMMITTING)", segmentName,
+          segmentZKMetadata.getStatus());
+      return;
+    }
+    if (segmentZKMetadata.getDownloadUrl() != null) {
+      LOGGER.warn("Skipping re-ingestion of segment: {} because its download 
URL is already present", segmentName);
+      return;
+    }
+    IndexLoadingConfig indexLoadingConfig = 
tableDataManager.fetchIndexLoadingConfig();
     try (StatelessRealtimeSegmentWriter writer = new 
StatelessRealtimeSegmentWriter(segmentZKMetadata,
-        indexLoadingConfig, segmentBuildSemaphore)) {
+        indexLoadingConfig, tableDataManager.getSegmentBuildSemaphore())) {
       // Read when the consumption starts, so that a job waiting for a 
re-ingestion thread uses the latest timeout
       long consumptionTimeoutMs = _consumptionTimeout.getTimeoutMs();
       LOGGER.info("Starting consumption to re-ingest segment: {} with timeout: 
{}ms", segmentName,
diff --git 
a/pinot-server/src/test/java/org/apache/pinot/server/api/resources/ReingestionConsumptionTimeoutTest.java
 
b/pinot-server/src/test/java/org/apache/pinot/server/api/resources/ReingestionConsumptionTimeoutTest.java
index 2c04697c02f..83e67f1e76f 100644
--- 
a/pinot-server/src/test/java/org/apache/pinot/server/api/resources/ReingestionConsumptionTimeoutTest.java
+++ 
b/pinot-server/src/test/java/org/apache/pinot/server/api/resources/ReingestionConsumptionTimeoutTest.java
@@ -44,18 +44,6 @@ public class ReingestionConsumptionTimeoutTest {
     assertThat(createTimeout(EMPTY_SERVER_CONF, 
Map.of()).getTimeoutMs()).isEqualTo(DEFAULT_TIMEOUT_MS);
   }
 
-  @Test
-  public void testServerConfig() {
-    assertThat(createTimeout(SERVER_CONF, 
Map.of()).getTimeoutMs()).isEqualTo(3_600_000L);
-  }
-
-  @Test
-  public void testClusterConfigOverridesServerConfig() {
-    Map<String, String> clusterConfigs = 
Map.of(CONFIG_OF_REINGESTION_CONSUMPTION_TIMEOUT_MS, "7200000");
-    assertThat(createTimeout(SERVER_CONF, 
clusterConfigs).getTimeoutMs()).isEqualTo(7_200_000L);
-    assertThat(createTimeout(EMPTY_SERVER_CONF, 
clusterConfigs).getTimeoutMs()).isEqualTo(7_200_000L);
-  }
-
   @DataProvider
   public Object[][] validTimeouts() {
     return new Object[][]{{"1", 1L}, {" 7200000 ", 7_200_000L}, 
{String.valueOf(Long.MAX_VALUE), Long.MAX_VALUE}};
@@ -66,6 +54,7 @@ public class ReingestionConsumptionTimeoutTest {
     Map<String, String> configs = 
Map.of(CONFIG_OF_REINGESTION_CONSUMPTION_TIMEOUT_MS, timeout);
     assertThat(createTimeout(new PinotConfiguration(configs), 
Map.of()).getTimeoutMs()).isEqualTo(expectedTimeoutMs);
     assertThat(createTimeout(SERVER_CONF, 
configs).getTimeoutMs()).isEqualTo(expectedTimeoutMs);
+    assertThat(createTimeout(EMPTY_SERVER_CONF, 
configs).getTimeoutMs()).isEqualTo(expectedTimeoutMs);
   }
 
   @DataProvider
diff --git 
a/pinot-server/src/test/java/org/apache/pinot/server/api/resources/ReingestionResourceApiTest.java
 
b/pinot-server/src/test/java/org/apache/pinot/server/api/resources/ReingestionResourceApiTest.java
index f4083caf56a..baa6004e733 100644
--- 
a/pinot-server/src/test/java/org/apache/pinot/server/api/resources/ReingestionResourceApiTest.java
+++ 
b/pinot-server/src/test/java/org/apache/pinot/server/api/resources/ReingestionResourceApiTest.java
@@ -23,19 +23,27 @@ import java.util.concurrent.Semaphore;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
+import javax.annotation.Nullable;
 import javax.ws.rs.client.Entity;
 import javax.ws.rs.core.Response;
 import org.apache.pinot.common.metadata.segment.SegmentZKMetadata;
+import org.apache.pinot.common.metrics.ServerMeter;
 import org.apache.pinot.common.utils.LLCSegmentName;
 import org.apache.pinot.core.data.manager.realtime.RealtimeTableDataManager;
 import org.apache.pinot.server.api.BaseResourceTest;
+import org.apache.pinot.spi.utils.CommonConstants.Segment.Realtime.Status;
 import org.mockito.stubbing.Answer;
+import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
 
 import static 
org.apache.pinot.server.api.resources.ReingestionResource.MAX_PARALLEL_REINGESTIONS;
+import static 
org.apache.pinot.server.api.resources.ReingestionResource.waitForCondition;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 
@@ -47,11 +55,9 @@ public class ReingestionResourceApiTest extends 
BaseResourceTest {
   // Makes the segment names unique across tests, since a segment is held 
until its previous job finishes
   private static final AtomicInteger SEQUENCE_NUMBER = new AtomicInteger();
 
-  @Test
-  public void testResourceDependenciesAreInjected() {
-    // Any request to the resource fails if one of its injected dependencies 
is not bound
-    Response response = 
_webTarget.path("/reingestSegment/jobs").request().get(Response.class);
-    
assertThat(response.getStatus()).isEqualTo(Response.Status.OK.getStatusCode());
+  /// Table availability when a queued re-ingestion starts.
+  private enum TableState {
+    AVAILABLE, REMOVED, SHUT_DOWN
   }
 
   @Test
@@ -102,6 +108,78 @@ public class ReingestionResourceApiTest extends 
BaseResourceTest {
     }
   }
 
+  @DataProvider
+  public Object[][] staleQueuedReingestions() {
+    return new Object[][]{
+        {TableState.AVAILABLE, Status.DONE, null},
+        {TableState.AVAILABLE, null, null},
+        {TableState.AVAILABLE, Status.IN_PROGRESS, null},
+        {TableState.AVAILABLE, Status.COMMITTING, "http://deep-store/segment"},
+        {TableState.REMOVED, Status.COMMITTING, null},
+        {TableState.SHUT_DOWN, Status.COMMITTING, null}
+    };
+  }
+
+  @Test(dataProvider = "staleQueuedReingestions")
+  public void testQueuedReingestionSkipsObsoleteJob(TableState tableState, 
@Nullable Status status,
+      @Nullable String downloadUrl)
+      throws Exception {
+    waitForCondition(() -> getJobs().equals("[]"), "previous re-ingestions to 
finish", 10, 10_000);
+    CountDownLatch jobsStarted = new CountDownLatch(MAX_PARALLEL_REINGESTIONS);
+    CountDownLatch releaseJobs = new CountDownLatch(1);
+    addTableDataManager(invocation -> {
+      jobsStarted.countDown();
+      releaseJobs.await();
+      throw new IllegalStateException("Finish blocker without building a 
segment");
+    });
+
+    String targetRawTableName = "queuedReingestionTable";
+    String targetTableName = targetRawTableName + "_REALTIME";
+    String targetSegmentName = segmentName(targetRawTableName, 0);
+    AtomicReference<SegmentZKMetadata> metadata = new 
AtomicReference<>(segmentMetadata(targetSegmentName));
+    RealtimeTableDataManager targetTableDataManager = 
mock(RealtimeTableDataManager.class);
+    
when(targetTableDataManager.fetchZKMetadata(targetSegmentName)).thenAnswer(invocation
 -> metadata.get());
+    
when(targetTableDataManager.fetchZKMetadataNullable(targetSegmentName)).thenAnswer(invocation
 -> metadata.get());
+    _tableDataManagerMap.put(targetTableName, targetTableDataManager);
+
+    try {
+      for (int partitionId = 0; partitionId < MAX_PARALLEL_REINGESTIONS; 
partitionId++) {
+        
assertThat(postReingestion(segmentName(partitionId))).isEqualTo(Response.Status.OK.getStatusCode());
+      }
+      assertThat(jobsStarted.await(10, TimeUnit.SECONDS)).isTrue();
+      
assertThat(postReingestion(targetSegmentName)).isEqualTo(Response.Status.OK.getStatusCode());
+      assertThat(getJobs()).contains(targetSegmentName);
+
+      // Change the metadata or table availability while the job is queued.
+      SegmentZKMetadata updatedMetadata = null;
+      if (status != null) {
+        updatedMetadata = segmentMetadata(targetSegmentName);
+        updatedMetadata.setStatus(status);
+        updatedMetadata.setDownloadUrl(downloadUrl);
+      }
+      metadata.set(updatedMetadata);
+      if (tableState == TableState.REMOVED) {
+        _tableDataManagerMap.remove(targetTableName);
+      } else if (tableState == TableState.SHUT_DOWN) {
+        when(targetTableDataManager.isShutDown()).thenReturn(true);
+      }
+      releaseJobs.countDown();
+      waitForCondition(() -> !getJobs().contains(targetSegmentName), "queued 
re-ingestion to be removed", 10, 10_000);
+
+      verify(targetTableDataManager).fetchZKMetadata(targetSegmentName);
+      verify(targetTableDataManager, tableState == TableState.AVAILABLE ? 
times(1) : never())
+          .fetchZKMetadataNullable(targetSegmentName);
+      verify(targetTableDataManager, never()).fetchIndexLoadingConfig();
+      verify(targetTableDataManager, never()).getSegmentBuildSemaphore();
+      verify(_serverInstance.getServerMetrics(), never())
+          .addMeteredTableValue(targetTableName, 
ServerMeter.SEGMENT_REINGESTION_FAILURE, 1);
+    } finally {
+      releaseJobs.countDown();
+      _tableDataManagerMap.remove(REINGESTION_REALTIME_TABLE_NAME);
+      _tableDataManagerMap.remove(targetTableName);
+    }
+  }
+
   // Runs after the other tests since it stops the admin API
   @Test(priority = 1)
   public void testRunningReingestionIsStoppedOnShutdown()
@@ -128,21 +206,31 @@ public class ReingestionResourceApiTest extends 
BaseResourceTest {
   /// fetched on the re-ingestion thread with the given answer.
   private void addTableDataManager(Answer<Semaphore> 
segmentBuildSemaphoreAnswer) {
     RealtimeTableDataManager tableDataManager = 
mock(RealtimeTableDataManager.class);
-    when(tableDataManager.fetchZKMetadata(anyString())).thenAnswer(invocation 
-> {
-      SegmentZKMetadata segmentZKMetadata = new SegmentZKMetadata((String) 
invocation.getArgument(0));
-      segmentZKMetadata.setStartOffset("0");
-      segmentZKMetadata.setEndOffset("100");
-      return segmentZKMetadata;
-    });
+    when(tableDataManager.fetchZKMetadata(anyString()))
+        .thenAnswer(invocation -> segmentMetadata(invocation.getArgument(0)));
+    when(tableDataManager.fetchZKMetadataNullable(anyString()))
+        .thenAnswer(invocation -> segmentMetadata(invocation.getArgument(0)));
     
when(tableDataManager.getSegmentBuildSemaphore()).thenAnswer(segmentBuildSemaphoreAnswer);
     _tableDataManagerMap.put(REINGESTION_REALTIME_TABLE_NAME, 
tableDataManager);
   }
 
   private static String segmentName(int partitionId) {
-    return new LLCSegmentName(REINGESTION_RAW_TABLE_NAME, partitionId, 
SEQUENCE_NUMBER.getAndIncrement(),
+    return segmentName(REINGESTION_RAW_TABLE_NAME, partitionId);
+  }
+
+  private static String segmentName(String rawTableName, int partitionId) {
+    return new LLCSegmentName(rawTableName, partitionId, 
SEQUENCE_NUMBER.getAndIncrement(),
         System.currentTimeMillis()).getSegmentName();
   }
 
+  private static SegmentZKMetadata segmentMetadata(String segmentName) {
+    SegmentZKMetadata metadata = new SegmentZKMetadata(segmentName);
+    metadata.setStatus(Status.COMMITTING);
+    metadata.setStartOffset("0");
+    metadata.setEndOffset("100");
+    return metadata;
+  }
+
   private int postReingestion(String segmentName) {
     try (Response response = _webTarget.path("/reingestSegment/" + 
segmentName).request().post(Entity.json(""))) {
       return response.getStatus();


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to