Jackie-Jiang commented on code in PR #19400:
URL: https://github.com/apache/pinot/pull/19400#discussion_r4430044048


##########
pinot-core/src/main/java/org/apache/pinot/core/data/manager/realtime/PinotFSSegmentUploader.java:
##########
@@ -69,47 +80,116 @@ public URI uploadSegment(File segmentFile, LLCSegmentName 
segmentName, int timeo
       return null;
     }
     final String rawTableName = 
TableNameBuilder.extractRawTableName(segmentName.getTableName());
-    Callable<URI> uploadTask = () -> {
-      URI destUri = new URI(StringUtil.join(File.separator, 
_segmentStoreUriStr, segmentName.getTableName(),
+    final URI destUri;
+    try {
+      destUri = new URI(StringUtil.join(File.separator, _segmentStoreUriStr, 
segmentName.getTableName(),
           
SegmentCompletionUtils.generateTmpSegmentFileName(segmentName.getSegmentName())));
-      long startTime = System.currentTimeMillis();
-      try {
-        PinotFS pinotFS = PinotFSFactory.create(new 
URI(_segmentStoreUriStr).getScheme());
-        // Check and delete any existing segment file.
-        if (pinotFS.exists(destUri)) {
-          pinotFS.delete(destUri, true);
-        }
-        pinotFS.copyFromLocalFile(segmentFile, destUri);
-        return destUri;
-      } catch (Exception e) {
-        LOGGER.warn("Failed copy segment tar file {} to segment store {}: {}", 
segmentFile.getName(), destUri, e);
-      } finally {
-        long duration = System.currentTimeMillis() - startTime;
-        _serverMetrics.addTimedTableValue(rawTableName, 
ServerTimer.SEGMENT_UPLOAD_TIME_MS, duration,
-            TimeUnit.MILLISECONDS);
-      }
+    } catch (URISyntaxException e) {
+      LOGGER.warn("Failed copy segment tar file {} to segment store {}", 
segmentFile.getName(), _segmentStoreUriStr,
+          e);
+      _serverMetrics.addMeteredTableValue(rawTableName, 
ServerMeter.SEGMENT_UPLOAD_FAILURE, 1);
       return null;
-    };
-    Future<URI> future = _executorService.submit(uploadTask);
+    }
+    AtomicReference<AttemptState> state = new 
AtomicReference<>(AttemptState.IN_FLIGHT);
+    Future<URI> future = _executorService.submit(() -> 
copySegment(segmentFile, destUri, rawTableName, state));
     try {
       URI segmentLocation = future.get(timeoutInMillis, TimeUnit.MILLISECONDS);
-      LOGGER.info("Successfully upload segment {} to {}.", segmentName, 
segmentLocation);
-      _serverMetrics.addMeteredTableValue(rawTableName,
-          segmentLocation == null ? ServerMeter.SEGMENT_UPLOAD_FAILURE : 
ServerMeter.SEGMENT_UPLOAD_SUCCESS, 1);
-      return segmentLocation;
+      if (segmentLocation != null) {
+        LOGGER.info("Successfully upload segment {} to {}.", segmentName, 
segmentLocation);
+        _serverMetrics.addMeteredTableValue(rawTableName, 
ServerMeter.SEGMENT_UPLOAD_SUCCESS, 1);
+        return segmentLocation;
+      }
     } catch (InterruptedException e) {
       LOGGER.info("Interrupted while waiting for segment upload of {} to {}.", 
segmentName, _segmentStoreUriStr);
+      // future.get already cleared the interrupt flag. Leave it clear until a 
published object is deleted.
+      // S3 and HDFS abort a delete when the calling thread is interrupted, 
and the worker will not delete a URI
+      // it already published. Restore the flag after that delete.
+      if (abandonUnlessPublished(state)) {
+        deleteBestEffort(destUri);
+      }
       Thread.currentThread().interrupt();
     } catch (TimeoutException e) {
-      // Emit a separate metric for timeout since this is relatively more 
common than other errors.
+      if (abandonUnlessPublished(state)) {
+        LOGGER.info("Successfully upload segment {} to {}.", segmentName, 
destUri);
+        _serverMetrics.addMeteredTableValue(rawTableName, 
ServerMeter.SEGMENT_UPLOAD_SUCCESS, 1);
+        return destUri;
+      }
       _serverMetrics.addMeteredTableValue(rawTableName, 
ServerMeter.SEGMENT_UPLOAD_TIMEOUT, 1);
       LOGGER.warn("Timed out waiting to upload segment: {} for table: {}", 
segmentName.getSegmentName(), rawTableName);
     } catch (Exception e) {
       LOGGER.warn("Failed to upload file {} of segment {} for table {}",
-              segmentFile.getAbsolutePath(), segmentName, rawTableName, e);
+          segmentFile.getAbsolutePath(), segmentName, rawTableName, e);
     }
     _serverMetrics.addMeteredTableValue(rawTableName, 
ServerMeter.SEGMENT_UPLOAD_FAILURE, 1);
-
     return null;
   }
+
+  /// Settles a wait that ended without the worker's result (timeout or 
interrupt) against the worker's publish.
+  /// Returns true when the worker already published, so the object belongs to 
the caller. Otherwise the attempt is
+  /// abandoned (or had already failed) and the worker deletes whatever its 
copy leaves behind, including a late
+  /// success. The caller must not delete in that case: an early delete can 
race a copy that recreates the object.
+  /// Does not cancel the future. An interrupt mid-upload can leave multipart 
parts this code cannot abort.
+  static boolean abandonUnlessPublished(AtomicReference<AttemptState> state) {
+    return !state.compareAndSet(AttemptState.IN_FLIGHT, AttemptState.ABANDONED)

Review Comment:
   Nonblocking simplification: this can express the same ownership transition 
with one atomic operation:
   
   ```java
   return state.compareAndExchange(AttemptState.IN_FLIGHT, 
AttemptState.ABANDONED)
       == AttemptState.PUBLISHED;
   ```
   
   The returned previous state tells us whether the worker already published, 
removing the negated compareAndSet and subsequent get.



##########
pinot-core/src/main/java/org/apache/pinot/core/data/manager/realtime/PinotFSSegmentUploader.java:
##########
@@ -69,47 +80,116 @@ public URI uploadSegment(File segmentFile, LLCSegmentName 
segmentName, int timeo
       return null;
     }
     final String rawTableName = 
TableNameBuilder.extractRawTableName(segmentName.getTableName());
-    Callable<URI> uploadTask = () -> {
-      URI destUri = new URI(StringUtil.join(File.separator, 
_segmentStoreUriStr, segmentName.getTableName(),
+    final URI destUri;
+    try {
+      destUri = new URI(StringUtil.join(File.separator, _segmentStoreUriStr, 
segmentName.getTableName(),
           
SegmentCompletionUtils.generateTmpSegmentFileName(segmentName.getSegmentName())));
-      long startTime = System.currentTimeMillis();
-      try {
-        PinotFS pinotFS = PinotFSFactory.create(new 
URI(_segmentStoreUriStr).getScheme());
-        // Check and delete any existing segment file.
-        if (pinotFS.exists(destUri)) {
-          pinotFS.delete(destUri, true);
-        }
-        pinotFS.copyFromLocalFile(segmentFile, destUri);
-        return destUri;
-      } catch (Exception e) {
-        LOGGER.warn("Failed copy segment tar file {} to segment store {}: {}", 
segmentFile.getName(), destUri, e);
-      } finally {
-        long duration = System.currentTimeMillis() - startTime;
-        _serverMetrics.addTimedTableValue(rawTableName, 
ServerTimer.SEGMENT_UPLOAD_TIME_MS, duration,
-            TimeUnit.MILLISECONDS);
-      }
+    } catch (URISyntaxException e) {
+      LOGGER.warn("Failed copy segment tar file {} to segment store {}", 
segmentFile.getName(), _segmentStoreUriStr,
+          e);
+      _serverMetrics.addMeteredTableValue(rawTableName, 
ServerMeter.SEGMENT_UPLOAD_FAILURE, 1);
       return null;
-    };
-    Future<URI> future = _executorService.submit(uploadTask);
+    }
+    AtomicReference<AttemptState> state = new 
AtomicReference<>(AttemptState.IN_FLIGHT);
+    Future<URI> future = _executorService.submit(() -> 
copySegment(segmentFile, destUri, rawTableName, state));

Review Comment:
   Nonblocking simplification: resolve the PinotFS once alongside destUri, 
before submitting the task, using PinotFSFactory.create(destUri.getScheme()), 
then pass that instance to the worker and cleanup. PinotFSFactory.create() only 
looks up the registered instance, so this does not introduce remote I/O before 
the timed wait. This would remove openSegmentStore(), the URI-only 
deleteBestEffort() overload, and repeated URI parsing. Preserve the null return 
and failure metric when URI construction or filesystem lookup fails.



##########
pinot-core/src/test/java/org/apache/pinot/core/data/manager/realtime/PinotFSSegmentUploaderTest.java:
##########
@@ -24,72 +24,231 @@
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.UUID;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
 import org.apache.commons.io.FileUtils;
 import org.apache.pinot.common.exception.HttpErrorStatusException;
+import org.apache.pinot.common.metrics.ServerMeter;
 import org.apache.pinot.common.metrics.ServerMetrics;
 import org.apache.pinot.common.utils.LLCSegmentName;
+import 
org.apache.pinot.core.data.manager.realtime.PinotFSSegmentUploader.AttemptState;
 import org.apache.pinot.spi.env.PinotConfiguration;
 import org.apache.pinot.spi.filesystem.BasePinotFS;
 import org.apache.pinot.spi.filesystem.PinotFSFactory;
 import org.apache.pinot.spi.utils.StringUtil;
-import org.mockito.Mockito;
-import org.testng.Assert;
 import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
 
 public class PinotFSSegmentUploaderTest {
   private static final int TIMEOUT_IN_MS = 1000;
   private File _file;
   private LLCSegmentName _llcSegmentName;
-  private ServerMetrics _serverMetrics = Mockito.mock(ServerMetrics.class);
+  private ServerMetrics _serverMetrics = mock(ServerMetrics.class);
 
   @BeforeClass
   public void setUp()
       throws URISyntaxException, IOException, HttpErrorStatusException {
     Map<String, Object> properties = new HashMap<>();
     properties.put("class.hdfs",
         
"org.apache.pinot.core.data.manager.realtime.PinotFSSegmentUploaderTest$AlwaysSucceedPinotFS");
-    properties.put("class.timeout",
-        
"org.apache.pinot.core.data.manager.realtime.PinotFSSegmentUploaderTest$AlwaysTimeoutPinotFS");
-    properties.put("class.existing",
-        
"org.apache.pinot.core.data.manager.realtime.PinotFSSegmentUploaderTest$AlwaysExistPinotFS");
+    properties.put("class.record",
+        
"org.apache.pinot.core.data.manager.realtime.PinotFSSegmentUploaderTest$RecordingPinotFS");
     PinotFSFactory.init(new PinotConfiguration(properties));
     _file = FileUtils.getFile(FileUtils.getTempDirectory(), 
UUID.randomUUID().toString());
     _file.deleteOnExit();
     _llcSegmentName = new LLCSegmentName("test_REALTIME", 1, 0, 
System.currentTimeMillis());
   }
 
+  @BeforeMethod
+  public void resetRecordedUploads() {
+    RecordingPinotFS.reset();
+    clearInvocations(_serverMetrics);
+  }
+
   @Test
   public void testSuccessfulUpload() {
-    SegmentUploader segmentUploader = new 
PinotFSSegmentUploader("hdfs://root", TIMEOUT_IN_MS, _serverMetrics);
-    URI segmentURI = segmentUploader.uploadSegment(_file, _llcSegmentName);
-    Assert.assertTrue(segmentURI.toString().startsWith(StringUtil
-        .join(File.separator, "hdfs://root", _llcSegmentName.getTableName(), 
_llcSegmentName.getSegmentName())));
+    SegmentUploader segmentUploader = new 
PinotFSSegmentUploader("record://root", TIMEOUT_IN_MS, _serverMetrics);
+    URI first = segmentUploader.uploadSegment(_file, _llcSegmentName);
+    URI second = segmentUploader.uploadSegment(_file, _llcSegmentName);
+    assertNotNull(first);
+    assertNotNull(second);
+    assertNotEquals(first, second);
+    assertUuidTempUri(first);
+    assertUuidTempUri(second);
+    assertEquals(RecordingPinotFS.COPIED_DEST_URIS.size(), 2);
+    assertTrue(RecordingPinotFS.DELETED_DEST_URIS.isEmpty());
   }
 
   @Test
-  public void testSegmentAlreadyExist() {
-    SegmentUploader segmentUploader = new 
PinotFSSegmentUploader("existing://root", TIMEOUT_IN_MS, _serverMetrics);
+  public void testCopyFailureDeletesThatUri() {
+    RecordingPinotFS._throwOnCopy = true;
+    SegmentUploader segmentUploader = new 
PinotFSSegmentUploader("record://root", TIMEOUT_IN_MS, _serverMetrics);
     URI segmentURI = segmentUploader.uploadSegment(_file, _llcSegmentName);
-    Assert.assertTrue(segmentURI.toString().startsWith(StringUtil
-        .join(File.separator, "existing://root", 
_llcSegmentName.getTableName(), _llcSegmentName.getSegmentName())));
+    assertNull(segmentURI);
+    assertEquals(RecordingPinotFS.COPIED_DEST_URIS.size(), 1);
+    assertEquals(RecordingPinotFS.DELETED_DEST_URIS, 
RecordingPinotFS.COPIED_DEST_URIS);
   }
 
   @Test
-  public void testUploadTimeOut() {
-    SegmentUploader segmentUploader = new 
PinotFSSegmentUploader("timeout://root", TIMEOUT_IN_MS, _serverMetrics);
-    URI segmentURI = segmentUploader.uploadSegment(_file, _llcSegmentName);
-    Assert.assertNull(segmentURI);
+  public void testCopyFailureSurvivesThrowingDelete() {
+    RecordingPinotFS._throwOnCopy = true;
+    RecordingPinotFS._throwOnDelete = true;
+    SegmentUploader segmentUploader = new 
PinotFSSegmentUploader("record://root", TIMEOUT_IN_MS, _serverMetrics);
+    assertNull(segmentUploader.uploadSegment(_file, _llcSegmentName));
+    assertEquals(RecordingPinotFS.COPIED_DEST_URIS.size(), 1);
+    assertTrue(RecordingPinotFS.DELETED_DEST_URIS.isEmpty());
+  }
+
+  @Test
+  public void testTimeoutDeletesOnlyTheAbandonedAttempt()
+      throws Exception {
+    RecordingPinotFS._enteredCopy = new CountDownLatch(1);
+    RecordingPinotFS._releaseCopy = new CountDownLatch(1);
+    RecordingPinotFS._deleted = new CountDownLatch(1);
+    SegmentUploader segmentUploader = new 
PinotFSSegmentUploader("record://root", TIMEOUT_IN_MS, _serverMetrics);
+    ExecutorService pool = Executors.newSingleThreadExecutor();
+    try {
+      Future<URI> first = pool.submit(() -> 
segmentUploader.uploadSegment(_file, _llcSegmentName, 200));
+      assertTrue(RecordingPinotFS._enteredCopy.await(5, TimeUnit.SECONDS));
+      assertNull(first.get(5, TimeUnit.SECONDS));
+      assertTrue(RecordingPinotFS.DELETED_DEST_URIS.isEmpty());
+      URI abandoned = RecordingPinotFS.COPIED_DEST_URIS.get(0);
+      RecordingPinotFS._releaseCopy.countDown();
+      assertTrue(RecordingPinotFS._deleted.await(5, TimeUnit.SECONDS));
+      assertTrue(RecordingPinotFS.DELETED_DEST_URIS.contains(abandoned));
+
+      URI retry = segmentUploader.uploadSegment(_file, _llcSegmentName, 
TIMEOUT_IN_MS);
+      assertNotNull(retry);
+      assertNotEquals(retry, abandoned);
+      assertFalse(RecordingPinotFS.DELETED_DEST_URIS.contains(retry));
+    } finally {
+      RecordingPinotFS.releaseCopy();
+      pool.shutdownNow();
+    }
+  }
+
+  @Test
+  public void testOverlappingUploadsKeepDistinctUris()
+      throws Exception {
+    RecordingPinotFS._enteredCopy = new CountDownLatch(2);
+    RecordingPinotFS._releaseCopy = new CountDownLatch(1);
+    SegmentUploader segmentUploader = new 
PinotFSSegmentUploader("record://root", 30_000, _serverMetrics);
+    ExecutorService pool = Executors.newFixedThreadPool(2);
+    try {
+      Future<URI> first = pool.submit(() -> 
segmentUploader.uploadSegment(_file, _llcSegmentName));
+      Future<URI> second = pool.submit(() -> 
segmentUploader.uploadSegment(_file, _llcSegmentName));
+      assertTrue(RecordingPinotFS._enteredCopy.await(5, TimeUnit.SECONDS));
+      RecordingPinotFS._releaseCopy.countDown();
+      URI uri1 = first.get(5, TimeUnit.SECONDS);
+      URI uri2 = second.get(5, TimeUnit.SECONDS);
+      assertNotNull(uri1);
+      assertNotNull(uri2);
+      assertNotEquals(uri1, uri2);
+      assertTrue(RecordingPinotFS.DELETED_DEST_URIS.isEmpty());
+    } finally {
+      RecordingPinotFS.releaseCopy();
+      pool.shutdownNow();
+    }
+  }
+
+  @Test
+  public void testAbandonUnlessPublished() {

Review Comment:
   Nonblocking testing suggestion: the helper test verifies the PUBLISHED 
state, but the removed post-publication tests also covered the caller's 
behavior. Please consider restoring deterministic cases for (1) timeout after 
publication returning the URI without deleting it, and (2) interruption after 
publication deleting the URI and restoring the interrupt flag. The mocked 
addTimedTableValue() call in the worker's finally block can be paused with 
latches after publication and before the Future completes, so these tests do 
not require production TestHooks.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to