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

KKcorps 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 7f024b6b623 Release the consumer semaphore only after the segment's 
upsert metadata is removed on offload (#19444)
7f024b6b623 is described below

commit 7f024b6b6230f88f47526e7fb0f1479757bce6ca
Author: Kartik Khare <[email protected]>
AuthorDate: Tue Sep 8 14:11:04 2026 +0530

    Release the consumer semaphore only after the segment's upsert metadata is 
removed on offload (#19444)
    
    RealtimeSegmentDataManager.doOffload() released the consumer semaphore
    before _realtimeSegment.offload() removed the segment from the upsert and
    dedup metadata managers. For partial-upsert tables in PROTECTED consistency
    mode that removal reverts primary keys to their previous record locations,
    so the next consuming segment of the partition could start replaying while
    keys still pointed at the mutable segment being offloaded, and merge the
    same update twice.
    
    closeStreamConsumer() used to both close the stream consumer and release
    the semaphore. Split the two: closeStreamConsumer() now only closes, and
    still runs before the segment is offloaded so nothing can consume into an
    offloaded segment. The build and download paths that let the next segment
    start early call closeStreamConsumerAndReleaseSemaphore(), which keeps
    their behavior under ALLOW_DURING_BUILD_ONLY and ALLOW_ALWAYS unchanged.
    doOffload() releases the semaphore in a finally block after the metadata
    removal, so a failure there cannot leave the partition unable to consume.
    
    Co-authored-by: Kartik Khare <[email protected]>
---
 .../realtime/RealtimeSegmentDataManager.java       |  32 +++++-
 .../realtime/RealtimeSegmentDataManagerTest.java   | 111 +++++++++++++++++++++
 2 files changed, 138 insertions(+), 5 deletions(-)

diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/data/manager/realtime/RealtimeSegmentDataManager.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/data/manager/realtime/RealtimeSegmentDataManager.java
index 71a61b92d5b..4c511c00b2d 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/data/manager/realtime/RealtimeSegmentDataManager.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/data/manager/realtime/RealtimeSegmentDataManager.java
@@ -1198,7 +1198,7 @@ public class RealtimeSegmentDataManager extends 
SegmentDataManager {
   protected SegmentBuildDescriptor buildSegmentInternal(boolean forCommit)
       throws SegmentBuildFailureException {
     if (_parallelSegmentConsumptionPolicy.isAllowedDuringBuild()) {
-      closeStreamConsumer();
+      closeStreamConsumerAndReleaseSemaphore();
     }
     // Do not allow building segment when table data manager is already shut 
down
     if (_realtimeTableDataManager.isShutDown()) {
@@ -1445,15 +1445,25 @@ public class RealtimeSegmentDataManager extends 
SegmentDataManager {
     }
   }
 
+  /// Closes the stream consumer so that no more data can be consumed into 
this segment. Does NOT release the consumer
+  /// semaphore, see [#closeStreamConsumerAndReleaseSemaphore()] and 
[#doOffload()].
   private void closeStreamConsumer() {
     if (_streamConsumerClosed.compareAndSet(false, true)) {
       closePartitionGroupConsumer();
       closePartitionMetadataProvider();
-      releaseConsumerSemaphore();
       _transformPipeline.reportStats();
     }
   }
 
+  /// Closes the stream consumer and releases the consumer semaphore so that 
the next consuming segment of the
+  /// partition can start consuming in parallel with the build or download of 
this segment. Only called when the
+  /// [ParallelSegmentConsumptionPolicy] allows it.
+  @VisibleForTesting
+  void closeStreamConsumerAndReleaseSemaphore() {
+    closeStreamConsumer();
+    releaseConsumerSemaphore();
+  }
+
   private void closePartitionGroupConsumer() {
     try {
       _partitionGroupConsumer.close();
@@ -1722,7 +1732,7 @@ public class RealtimeSegmentDataManager extends 
SegmentDataManager {
   protected void downloadSegmentAndReplace(SegmentZKMetadata segmentZKMetadata)
       throws Exception {
     if (_parallelSegmentConsumptionPolicy.isAllowedDuringDownload()) {
-      closeStreamConsumer();
+      closeStreamConsumerAndReleaseSemaphore();
     }
     
_realtimeTableDataManager.downloadAndReplaceConsumingSegment(segmentZKMetadata);
   }
@@ -1764,9 +1774,21 @@ public class RealtimeSegmentDataManager extends 
SegmentDataManager {
     } catch (Exception e) {
       _segmentLogger.error("Caught exception while stopping the consumer 
thread", e);
     }
+    // Close the stream consumer first so that nothing can consume into the 
segment once it is offloaded.
     closeStreamConsumer();
-    cleanupMetrics();
-    _realtimeSegment.offload();
+    // Remove this segment's upsert/dedup metadata BEFORE releasing the 
consumer semaphore. For partial upsert in
+    // PROTECTED consistency mode, offload() reverts the primary keys owned by 
this consuming segment to their previous
+    // record locations. If the semaphore were released first, the next 
consuming segment of the partition could start
+    // replaying while primary keys still point to this mutable segment, and 
merge against the un-reverted state.
+    // When the parallel consumption policy allowed the next segment to start 
during build or download, the semaphore
+    // was already released there and the release below is a no-op.
+    // The semaphore is released in a finally block so that a failure in 
metadata removal cannot stall the partition.
+    try {
+      _realtimeSegment.offload();
+    } finally {
+      releaseConsumerSemaphore();
+      cleanupMetrics();
+    }
   }
 
   @Override
diff --git 
a/pinot-core/src/test/java/org/apache/pinot/core/data/manager/realtime/RealtimeSegmentDataManagerTest.java
 
b/pinot-core/src/test/java/org/apache/pinot/core/data/manager/realtime/RealtimeSegmentDataManagerTest.java
index 9ef007b2e49..8843f289cee 100644
--- 
a/pinot-core/src/test/java/org/apache/pinot/core/data/manager/realtime/RealtimeSegmentDataManagerTest.java
+++ 
b/pinot-core/src/test/java/org/apache/pinot/core/data/manager/realtime/RealtimeSegmentDataManagerTest.java
@@ -29,7 +29,9 @@ import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Semaphore;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.locks.Lock;
 import java.util.function.BooleanSupplier;
@@ -48,6 +50,7 @@ import 
org.apache.pinot.core.realtime.impl.fakestream.FakeStreamConsumerFactory;
 import org.apache.pinot.core.realtime.impl.fakestream.FakeStreamMessageDecoder;
 import org.apache.pinot.segment.local.data.manager.SegmentDataManager;
 import org.apache.pinot.segment.local.data.manager.TableDataManager;
+import org.apache.pinot.segment.local.indexsegment.mutable.MutableSegmentImpl;
 import 
org.apache.pinot.segment.local.realtime.impl.RealtimeSegmentStatsHistory;
 import org.apache.pinot.segment.local.segment.creator.Fixtures;
 import 
org.apache.pinot.segment.local.segment.creator.impl.SegmentIndexCreationDriverImpl;
@@ -83,8 +86,11 @@ import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.same;
 import static org.mockito.Mockito.anyString;
 import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -652,6 +658,96 @@ public class RealtimeSegmentDataManagerTest {
     }
   }
 
+  @Test
+  public void 
testOffloadRemovesSegmentMetadataBeforeReleasingConsumerSemaphore()
+      throws Exception {
+    // Use a fresh coordinator. Other tests release the shared semaphore 
without acquiring it, which inflates permits.
+    _partitionGroupIdToConsumerCoordinatorMap.remove(PARTITION_GROUP_ID);
+    try (FakeRealtimeSegmentDataManager segmentDataManager = 
createFakeSegmentManager()) {
+      Semaphore semaphore = 
_partitionGroupIdToConsumerCoordinatorMap.get(PARTITION_GROUP_ID).getSemaphore();
+      Assert.assertTrue(semaphore.tryAcquire());
+      segmentDataManager.getConsumerSemaphoreAcquired().set(true);
+
+      // MutableSegmentImpl.offload() removes the segment from the 
upsert/dedup metadata managers. For partial upsert in
+      // PROTECTED mode that is where the primary keys are reverted to their 
previous locations, so it has to run
+      // (1) after the stream consumer is closed, so nothing can consume into 
the offloaded segment, and
+      // (2) while the consumer semaphore is still held, so the next consuming 
segment cannot replay against
+      //     un-reverted state.
+      AtomicInteger permitsWhenMetadataRemoved = new AtomicInteger(-1);
+      AtomicBoolean streamConsumerClosedWhenMetadataRemoved = new 
AtomicBoolean(false);
+      MutableSegmentImpl realtimeSegment = spy((MutableSegmentImpl) 
segmentDataManager.getSegment());
+      doAnswer(invocation -> {
+        permitsWhenMetadataRemoved.set(semaphore.availablePermits());
+        
streamConsumerClosedWhenMetadataRemoved.set(segmentDataManager.isStreamConsumerClosed());
+        return null;
+      }).when(realtimeSegment).offload();
+      segmentDataManager.setRealtimeSegment(realtimeSegment);
+
+      segmentDataManager.offload();
+
+      verify(realtimeSegment).offload();
+      Assert.assertTrue(streamConsumerClosedWhenMetadataRemoved.get(),
+          "Stream consumer must be closed before the segment metadata is 
removed");
+      Assert.assertEquals(permitsWhenMetadataRemoved.get(), 0,
+          "Segment metadata must be removed while the consumer semaphore is 
still held");
+      Assert.assertEquals(semaphore.availablePermits(), 1, "Consumer semaphore 
must be released after offload");
+      
Assert.assertFalse(segmentDataManager.getConsumerSemaphoreAcquired().get());
+    }
+  }
+
+  @Test
+  public void 
testOffloadAfterParallelConsumptionReleaseDoesNotReleaseSemaphoreTwice()
+      throws Exception {
+    _partitionGroupIdToConsumerCoordinatorMap.remove(PARTITION_GROUP_ID);
+    try (FakeRealtimeSegmentDataManager segmentDataManager = 
createFakeSegmentManager()) {
+      Semaphore semaphore = 
_partitionGroupIdToConsumerCoordinatorMap.get(PARTITION_GROUP_ID).getSemaphore();
+      Assert.assertTrue(semaphore.tryAcquire());
+      segmentDataManager.getConsumerSemaphoreAcquired().set(true);
+
+      // With ALLOW_DURING_BUILD_ONLY / ALLOW_ALWAYS, buildSegmentInternal() 
and downloadSegmentAndReplace() let the
+      // next consuming segment start early by closing the consumer and 
releasing the semaphore together.
+      segmentDataManager.closeStreamConsumerAndReleaseSemaphore();
+      Assert.assertTrue(segmentDataManager.isStreamConsumerClosed());
+      Assert.assertEquals(semaphore.availablePermits(), 1,
+          "Consumer semaphore must be released for parallel consumption");
+      
Assert.assertFalse(segmentDataManager.getConsumerSemaphoreAcquired().get());
+
+      MutableSegmentImpl realtimeSegment = spy((MutableSegmentImpl) 
segmentDataManager.getSegment());
+      doAnswer(invocation -> null).when(realtimeSegment).offload();
+      segmentDataManager.setRealtimeSegment(realtimeSegment);
+
+      segmentDataManager.offload();
+
+      verify(realtimeSegment).offload();
+      Assert.assertEquals(semaphore.availablePermits(), 1, "Offload must not 
release the consumer semaphore twice");
+    }
+  }
+
+  @Test
+  public void testOffloadReleasesConsumerSemaphoreWhenMetadataRemovalFails()
+      throws Exception {
+    _partitionGroupIdToConsumerCoordinatorMap.remove(PARTITION_GROUP_ID);
+    try (FakeRealtimeSegmentDataManager segmentDataManager = 
createFakeSegmentManager()) {
+      Semaphore semaphore = 
_partitionGroupIdToConsumerCoordinatorMap.get(PARTITION_GROUP_ID).getSemaphore();
+      Assert.assertTrue(semaphore.tryAcquire());
+      segmentDataManager.getConsumerSemaphoreAcquired().set(true);
+
+      MutableSegmentImpl realtimeSegment = spy((MutableSegmentImpl) 
segmentDataManager.getSegment());
+      doThrow(new RuntimeException("metadata removal 
failed")).when(realtimeSegment).offload();
+      segmentDataManager.setRealtimeSegment(realtimeSegment);
+
+      try {
+        segmentDataManager.offload();
+        Assert.fail("Expected the metadata removal failure to propagate");
+      } catch (RuntimeException e) {
+        Assert.assertEquals(e.getMessage(), "metadata removal failed");
+      }
+      // A failed metadata removal must not leave the semaphore held, or the 
partition can never consume again.
+      Assert.assertEquals(semaphore.availablePermits(), 1,
+          "Consumer semaphore must be released even when metadata removal 
fails");
+    }
+  }
+
   @Test
   public void testOnlineTransitionSkipsLocalBuildOnCrcMismatch()
       throws Exception {
@@ -1419,6 +1515,21 @@ public class RealtimeSegmentDataManagerTest {
       return _tableDataManager;
     }
 
+    /// Replaces the mutable segment so tests can observe or fail its 
offload().
+    public void setRealtimeSegment(MutableSegmentImpl realtimeSegment)
+        throws Exception {
+      Field realtimeSegmentField = 
RealtimeSegmentDataManager.class.getDeclaredField("_realtimeSegment");
+      realtimeSegmentField.setAccessible(true);
+      realtimeSegmentField.set(this, realtimeSegment);
+    }
+
+    public boolean isStreamConsumerClosed()
+        throws Exception {
+      Field streamConsumerClosedField = 
RealtimeSegmentDataManager.class.getDeclaredField("_streamConsumerClosed");
+      streamConsumerClosedField.setAccessible(true);
+      return ((AtomicBoolean) streamConsumerClosedField.get(this)).get();
+    }
+
     public String getStopReason() {
       try {
         return (String) _stopReason.get(this);


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

Reply via email to