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

xiangfu0 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 adb87d861c8 Avoid redundant local input copies and support Zstandard 
batch archives (#19898)
adb87d861c8 is described below

commit adb87d861c87a09bdd902cbf4e56bf7d6832483c
Author: Xiang Fu <[email protected]>
AuthorDate: Fri Oct 23 15:42:45 2026 -0700

    Avoid redundant local input copies and support Zstandard batch archives 
(#19898)
    
    * Avoid redundant local input copies and support Zstandard batch archives
    
    **Usage:** set `segmentTarCompressionCodec: zstd` in batch YAML for 
standalone, Spark, or Hadoop; the [README](README.md) includes an example. Gzip 
remains the default, and metadata sidecars remain gzip.
    **Validation:** 44 focused tests, Hadoop/Spark compilation, and 
style/license checks passed, including source ownership and archive discovery.
    
    * Document batch archive compatibility and upgrade behavior
    
    Keep gzip as default and enable zstd only when archive consumers support 
it; distinguish generation from LZ4 discovery.
    Document duplicate-name rejection and validate the SPI and README 
style/license checks.
    
    * Filter batch metadata archives before duplicate validation
    
    Reproduce with same-named gzip and zstd archives and a pushFileNamePattern 
that selects only one.
    Filter on the driver before checking duplicates, and skip empty selections.
    
    * Centralize batch archive codecs and trim redundant tests
    
    Use named codec and archive extension constants, including the gzip default.
    Replace repeated mock-based runners and filesystem adapters with focused 
checks.
    
    * Address archive ingestion review feedback
    
    Split out LZ4 and duplicate-name changes, move the usage guide to 
pinot-docs, and fix codec-aware backfill discovery. Keep archive selection 
consistent and isolate the input ownership test filesystem.
    
    * Preserve metadata push URIs and stabilize LZ4 assertions
    
    Handle metadata pushes with a URI prefix and no output directory. Verify 
default LZ4 output by content because reading the input can change its tar 
access-time header.
    
    * Address empty archive selection and local URI review feedback
    
    Skip consistent replacement when the push pattern selects no archives, and 
warn with selection context. Cover literal percent-encoded filenames in the 
existing URI tests.
---
 .../common/utils/TarCompressionUtilsTest.java      |   4 +-
 .../batch/common/BaseSegmentPushJobRunner.java     |  11 +-
 .../batch/hadoop/HadoopSegmentCreationMapper.java  |   3 +-
 .../hadoop/HadoopSegmentMetadataPushJobRunner.java |  18 +--
 .../hadoop/HadoopSegmentTarPushJobRunner.java      |  14 +-
 .../hadoop/HadoopSegmentUriPushJobRunner.java      |  15 +-
 .../spark3/SparkSegmentGenerationJobRunner.java    |   3 +-
 .../spark3/SparkSegmentMetadataPushJobRunner.java  |  16 +--
 .../BaseSparkSegmentMetadataPushJobRunner.java     |  10 +-
 .../common/BaseSparkSegmentTarPushJobRunner.java   |  10 +-
 .../common/BaseSparkSegmentUriPushJobRunner.java   |  14 +-
 .../BaseSparkSegmentMetadataPushJobRunnerTest.java | 125 +++++++++++++++++
 .../standalone/SegmentGenerationJobRunner.java     |  23 +++-
 .../standalone/SegmentGenerationJobRunnerTest.java | 151 +++++++++++++++++++++
 .../segmentuploader/SegmentUploaderDefault.java    |   2 +-
 .../segment/local/utils/SegmentPushUtils.java      |  39 ++++--
 .../segment/local/utils/SegmentPushUtilsTest.java  | 140 +++++++++++++++++++
 .../pinot/spi/ingestion/batch/spec/Constants.java  |  20 +++
 .../batch/spec/SegmentGenerationJobSpec.java       |  20 +++
 .../command/LaunchBackfillIngestionJobCommand.java |  35 +++--
 .../LaunchBackfillIngestionJobCommandTest.java     |  59 ++++++++
 21 files changed, 626 insertions(+), 106 deletions(-)

diff --git 
a/pinot-common/src/test/java/org/apache/pinot/common/utils/TarCompressionUtilsTest.java
 
b/pinot-common/src/test/java/org/apache/pinot/common/utils/TarCompressionUtilsTest.java
index 8941168644f..afe4d84fe70 100644
--- 
a/pinot-common/src/test/java/org/apache/pinot/common/utils/TarCompressionUtilsTest.java
+++ 
b/pinot-common/src/test/java/org/apache/pinot/common/utils/TarCompressionUtilsTest.java
@@ -126,9 +126,11 @@ public class TarCompressionUtilsTest {
     File archive = new File(TAR_DIR, "data" + 
TarCompressionUtils.TAR_LZ4_FILE_EXTENSION);
     TarCompressionUtils.createCompressedTarFile(inputFile, archive, 
compressorName);
     if (compressorName.equals(LZ4_FAST)) {
+      assertEquals(TarCompressionUtils.COMPRESSOR_NAME_BY_FILE_EXTENSIONS.get(
+          TarCompressionUtils.TAR_LZ4_FILE_EXTENSION), LZ4_FAST);
       File defaultArchive = new File(TAR_DIR, "default" + 
TarCompressionUtils.TAR_LZ4_FILE_EXTENSION);
       TarCompressionUtils.createCompressedTarFile(inputFile, defaultArchive);
-      assertEquals(Files.readAllBytes(defaultArchive.toPath()), 
Files.readAllBytes(archive.toPath()));
+      
assertEquals(Files.readAllBytes(TarCompressionUtils.untar(defaultArchive, 
UNTAR_DIR).get(0).toPath()), content);
     }
 
     List<File> extractedFiles = TarCompressionUtils.untar(archive, UNTAR_DIR);
diff --git 
a/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-common/src/main/java/org/apache/pinot/plugin/ingestion/batch/common/BaseSegmentPushJobRunner.java
 
b/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-common/src/main/java/org/apache/pinot/plugin/ingestion/batch/common/BaseSegmentPushJobRunner.java
index 305d05520b8..efe28e59162 100644
--- 
a/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-common/src/main/java/org/apache/pinot/plugin/ingestion/batch/common/BaseSegmentPushJobRunner.java
+++ 
b/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-common/src/main/java/org/apache/pinot/plugin/ingestion/batch/common/BaseSegmentPushJobRunner.java
@@ -18,7 +18,6 @@
  */
 package org.apache.pinot.plugin.ingestion.batch.common;
 
-import com.google.common.base.Preconditions;
 import java.io.File;
 import java.io.IOException;
 import java.net.URI;
@@ -35,7 +34,6 @@ import org.apache.pinot.spi.env.PinotConfiguration;
 import org.apache.pinot.spi.filesystem.PinotFS;
 import org.apache.pinot.spi.filesystem.PinotFSFactory;
 import org.apache.pinot.spi.ingestion.batch.runner.IngestionJobRunner;
-import org.apache.pinot.spi.ingestion.batch.spec.Constants;
 import org.apache.pinot.spi.ingestion.batch.spec.PinotFSSpec;
 import org.apache.pinot.spi.ingestion.batch.spec.SegmentGenerationJobSpec;
 
@@ -105,11 +103,7 @@ public abstract class BaseSegmentPushJobRunner implements 
IngestionJobRunner {
     Collection<String> tarFilePaths = segmentsUriToTarPathMap.values();
     List<String> segmentNames = new ArrayList<>(tarFilePaths.size());
     for (String tarFilePath : tarFilePaths) {
-      File tarFile = new File(tarFilePath);
-      String fileName = tarFile.getName();
-      
Preconditions.checkArgument(fileName.endsWith(Constants.TAR_GZ_FILE_EXT));
-      String segmentName = fileName.substring(0, fileName.length() - 
Constants.TAR_GZ_FILE_EXT.length());
-      segmentNames.add(segmentName);
+      
segmentNames.add(SegmentPushUtils.getSegmentNameFromTarFile(tarFilePath));
     }
     return segmentNames;
   }
@@ -131,6 +125,9 @@ public abstract class BaseSegmentPushJobRunner implements 
IngestionJobRunner {
     try {
       Map<String, String> segmentsUriToTarPathMap =
           SegmentPushUtils.getSegmentUriToTarPathMap(_outputDirURI, 
_spec.getPushJobSpec(), _files);
+      if (segmentsUriToTarPathMap.isEmpty()) {
+        return;
+      }
       if (_consistentPushEnabled) {
         List<String> segmentsToReplace = 
getSegmentsToReplace(segmentsUriToTarPathMap);
         uriToLineageEntryIdMap = ConsistentDataPushUtils.preUpload(_spec, 
segmentsToReplace);
diff --git 
a/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-hadoop/src/main/java/org/apache/pinot/plugin/ingestion/batch/hadoop/HadoopSegmentCreationMapper.java
 
b/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-hadoop/src/main/java/org/apache/pinot/plugin/ingestion/batch/hadoop/HadoopSegmentCreationMapper.java
index 8400bc69072..d8a0c2897ea 100644
--- 
a/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-hadoop/src/main/java/org/apache/pinot/plugin/ingestion/batch/hadoop/HadoopSegmentCreationMapper.java
+++ 
b/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-hadoop/src/main/java/org/apache/pinot/plugin/ingestion/batch/hadoop/HadoopSegmentCreationMapper.java
@@ -176,7 +176,8 @@ public class HadoopSegmentCreationMapper extends 
Mapper<LongWritable, Text, Long
 
       // Tar segment directory to compress file
       File localSegmentDir = new File(localOutputTempDir, segmentName);
-      String segmentTarFileName = URIUtils.encode(segmentName + 
Constants.TAR_GZ_FILE_EXT);
+      String segmentTarFileName = URIUtils.encode(
+          segmentName + 
Constants.SEGMENT_TAR_FILE_EXTENSIONS.get(_spec.getSegmentTarCompressionCodec()));
       File localSegmentTarFile = new File(localOutputTempDir, 
segmentTarFileName);
       LOGGER.info("Tarring segment from: {} to: {}", localSegmentDir, 
localSegmentTarFile);
       TarCompressionUtils.createCompressedTarFile(localSegmentDir, 
localSegmentTarFile);
diff --git 
a/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-hadoop/src/main/java/org/apache/pinot/plugin/ingestion/batch/hadoop/HadoopSegmentMetadataPushJobRunner.java
 
b/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-hadoop/src/main/java/org/apache/pinot/plugin/ingestion/batch/hadoop/HadoopSegmentMetadataPushJobRunner.java
index fed62025a31..216bcf6fae3 100644
--- 
a/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-hadoop/src/main/java/org/apache/pinot/plugin/ingestion/batch/hadoop/HadoopSegmentMetadataPushJobRunner.java
+++ 
b/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-hadoop/src/main/java/org/apache/pinot/plugin/ingestion/batch/hadoop/HadoopSegmentMetadataPushJobRunner.java
@@ -23,7 +23,6 @@ import java.io.IOException;
 import java.io.Serializable;
 import java.net.URI;
 import java.net.URISyntaxException;
-import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import org.apache.pinot.segment.local.utils.SegmentPushUtils;
@@ -31,7 +30,6 @@ import org.apache.pinot.spi.env.PinotConfiguration;
 import org.apache.pinot.spi.filesystem.PinotFS;
 import org.apache.pinot.spi.filesystem.PinotFSFactory;
 import org.apache.pinot.spi.ingestion.batch.runner.IngestionJobRunner;
-import org.apache.pinot.spi.ingestion.batch.spec.Constants;
 import org.apache.pinot.spi.ingestion.batch.spec.PinotFSSpec;
 import org.apache.pinot.spi.ingestion.batch.spec.SegmentGenerationJobSpec;
 
@@ -79,22 +77,14 @@ public class HadoopSegmentMetadataPushJobRunner implements 
IngestionJobRunner, S
       throw new RuntimeException("Unable to list all files under outputDirURI 
- '" + outputDirURI + "'");
     }
 
-    List<String> segmentsToPush = new ArrayList<>();
-    for (String file : files) {
-      if (file.endsWith(Constants.TAR_GZ_FILE_EXT)) {
-        segmentsToPush.add(file);
-      }
-    }
-
-    int pushParallelism = _spec.getPushJobSpec().getPushParallelism();
-    if (pushParallelism < 1) {
-      pushParallelism = segmentsToPush.size();
+    Map<String, String> segmentUriToTarPathMap =
+        SegmentPushUtils.getSegmentUriToTarPathMap(outputDirURI, 
_spec.getPushJobSpec(), files);
+    if (segmentUriToTarPathMap.isEmpty()) {
+      return;
     }
     // Push from driver
 
     try {
-      Map<String, String> segmentUriToTarPathMap = SegmentPushUtils
-          .getSegmentUriToTarPathMap(outputDirURI, _spec.getPushJobSpec(), 
segmentsToPush.toArray(new String[0]));
       SegmentPushUtils
           .sendSegmentUriAndMetadata(_spec, 
PinotFSFactory.create(outputDirURI.getScheme()), segmentUriToTarPathMap);
     } catch (Exception e) {
diff --git 
a/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-hadoop/src/main/java/org/apache/pinot/plugin/ingestion/batch/hadoop/HadoopSegmentTarPushJobRunner.java
 
b/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-hadoop/src/main/java/org/apache/pinot/plugin/ingestion/batch/hadoop/HadoopSegmentTarPushJobRunner.java
index 2dd6d39dd23..6771db1a3e8 100644
--- 
a/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-hadoop/src/main/java/org/apache/pinot/plugin/ingestion/batch/hadoop/HadoopSegmentTarPushJobRunner.java
+++ 
b/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-hadoop/src/main/java/org/apache/pinot/plugin/ingestion/batch/hadoop/HadoopSegmentTarPushJobRunner.java
@@ -30,7 +30,6 @@ import org.apache.pinot.spi.env.PinotConfiguration;
 import org.apache.pinot.spi.filesystem.PinotFS;
 import org.apache.pinot.spi.filesystem.PinotFSFactory;
 import org.apache.pinot.spi.ingestion.batch.runner.IngestionJobRunner;
-import org.apache.pinot.spi.ingestion.batch.spec.Constants;
 import org.apache.pinot.spi.ingestion.batch.spec.PinotFSSpec;
 import org.apache.pinot.spi.ingestion.batch.spec.SegmentGenerationJobSpec;
 import org.apache.pinot.spi.utils.retry.AttemptsExceededException;
@@ -80,17 +79,12 @@ public class HadoopSegmentTarPushJobRunner implements 
IngestionJobRunner, Serial
       throw new RuntimeException("Unable to list all files under outputDirURI 
- '" + outputDirURI + "'");
     }
 
-    List<String> segmentsToPush = new ArrayList<>();
-    for (String file : files) {
-      if (file.endsWith(Constants.TAR_GZ_FILE_EXT)) {
-        segmentsToPush.add(file);
-      }
+    List<String> segmentsToPush = new ArrayList<>(
+        SegmentPushUtils.getSegmentUriToTarPathMap(outputDirURI, 
_spec.getPushJobSpec(), files).values());
+    if (segmentsToPush.isEmpty()) {
+      return;
     }
 
-    int pushParallelism = _spec.getPushJobSpec().getPushParallelism();
-    if (pushParallelism < 1) {
-      pushParallelism = segmentsToPush.size();
-    }
     // Push from driver
     try {
       SegmentPushUtils.pushSegments(_spec, outputDirFS, segmentsToPush);
diff --git 
a/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-hadoop/src/main/java/org/apache/pinot/plugin/ingestion/batch/hadoop/HadoopSegmentUriPushJobRunner.java
 
b/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-hadoop/src/main/java/org/apache/pinot/plugin/ingestion/batch/hadoop/HadoopSegmentUriPushJobRunner.java
index 0f253386f94..0352859145e 100644
--- 
a/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-hadoop/src/main/java/org/apache/pinot/plugin/ingestion/batch/hadoop/HadoopSegmentUriPushJobRunner.java
+++ 
b/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-hadoop/src/main/java/org/apache/pinot/plugin/ingestion/batch/hadoop/HadoopSegmentUriPushJobRunner.java
@@ -30,7 +30,6 @@ import org.apache.pinot.spi.env.PinotConfiguration;
 import org.apache.pinot.spi.filesystem.PinotFS;
 import org.apache.pinot.spi.filesystem.PinotFSFactory;
 import org.apache.pinot.spi.ingestion.batch.runner.IngestionJobRunner;
-import org.apache.pinot.spi.ingestion.batch.spec.Constants;
 import org.apache.pinot.spi.ingestion.batch.spec.PinotFSSpec;
 import org.apache.pinot.spi.ingestion.batch.spec.SegmentGenerationJobSpec;
 import org.apache.pinot.spi.utils.retry.AttemptsExceededException;
@@ -82,16 +81,12 @@ public class HadoopSegmentUriPushJobRunner implements 
IngestionJobRunner, Serial
     } catch (IOException e) {
       throw new RuntimeException("Unable to list all files under outputDirURI 
- '" + outputDirURI + "'");
     }
-    List<String> segmentUris = new ArrayList<>();
-    for (String file : files) {
-      URI uri = URI.create(file);
-      if (uri.getPath().endsWith(Constants.TAR_GZ_FILE_EXT)) {
-        URI updatedURI = SegmentPushUtils
-            .generateSegmentTarURI(outputDirURI, uri, 
_spec.getPushJobSpec().getSegmentUriPrefix(),
-                _spec.getPushJobSpec().getSegmentUriSuffix());
-        segmentUris.add(updatedURI.toString());
-      }
+    List<String> segmentUris = new ArrayList<>(
+        SegmentPushUtils.getSegmentUriToTarPathMap(outputDirURI, 
_spec.getPushJobSpec(), files).keySet());
+    if (segmentUris.isEmpty()) {
+      return;
     }
+
     // Push from driver
     try {
       SegmentPushUtils.sendSegmentUris(_spec, segmentUris);
diff --git 
a/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-spark-3/src/main/java/org/apache/pinot/plugin/ingestion/batch/spark3/SparkSegmentGenerationJobRunner.java
 
b/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-spark-3/src/main/java/org/apache/pinot/plugin/ingestion/batch/spark3/SparkSegmentGenerationJobRunner.java
index 2d16805798d..8415302fd2d 100644
--- 
a/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-spark-3/src/main/java/org/apache/pinot/plugin/ingestion/batch/spark3/SparkSegmentGenerationJobRunner.java
+++ 
b/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-spark-3/src/main/java/org/apache/pinot/plugin/ingestion/batch/spark3/SparkSegmentGenerationJobRunner.java
@@ -293,7 +293,8 @@ public class SparkSegmentGenerationJobRunner implements 
IngestionJobRunner, Seri
 
           // Tar segment directory to compress file
           File localSegmentDir = new File(localOutputTempDir, segmentName);
-          String segmentTarFileName = URIUtils.encode(segmentName + 
Constants.TAR_GZ_FILE_EXT);
+          String segmentTarFileName = URIUtils.encode(
+              segmentName + 
Constants.SEGMENT_TAR_FILE_EXTENSIONS.get(_spec.getSegmentTarCompressionCodec()));
           File localSegmentTarFile = new File(localOutputTempDir, 
segmentTarFileName);
           LOGGER.info("Tarring segment from: {} to: {}", localSegmentDir, 
localSegmentTarFile);
           TarCompressionUtils.createCompressedTarFile(localSegmentDir, 
localSegmentTarFile);
diff --git 
a/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-spark-3/src/main/java/org/apache/pinot/plugin/ingestion/batch/spark3/SparkSegmentMetadataPushJobRunner.java
 
b/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-spark-3/src/main/java/org/apache/pinot/plugin/ingestion/batch/spark3/SparkSegmentMetadataPushJobRunner.java
index 644b8f810e0..a34a1e66cd0 100644
--- 
a/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-spark-3/src/main/java/org/apache/pinot/plugin/ingestion/batch/spark3/SparkSegmentMetadataPushJobRunner.java
+++ 
b/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-spark-3/src/main/java/org/apache/pinot/plugin/ingestion/batch/spark3/SparkSegmentMetadataPushJobRunner.java
@@ -18,19 +18,16 @@
  */
 package org.apache.pinot.plugin.ingestion.batch.spark3;
 
-import com.google.common.base.Preconditions;
 import java.io.File;
 import java.io.IOException;
 import java.io.Serializable;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
-import java.util.stream.Collectors;
 import org.apache.pinot.common.segment.generation.SegmentGenerationUtils;
 import org.apache.pinot.segment.local.utils.ConsistentDataPushUtils;
 import org.apache.pinot.segment.local.utils.SegmentPushUtils;
@@ -39,7 +36,6 @@ import org.apache.pinot.spi.env.PinotConfiguration;
 import org.apache.pinot.spi.filesystem.PinotFS;
 import org.apache.pinot.spi.filesystem.PinotFSFactory;
 import org.apache.pinot.spi.ingestion.batch.runner.IngestionJobRunner;
-import org.apache.pinot.spi.ingestion.batch.spec.Constants;
 import org.apache.pinot.spi.ingestion.batch.spec.PinotClusterSpec;
 import org.apache.pinot.spi.ingestion.batch.spec.PinotFSSpec;
 import org.apache.pinot.spi.ingestion.batch.spec.SegmentGenerationJobSpec;
@@ -107,6 +103,9 @@ public class SparkSegmentMetadataPushJobRunner implements 
IngestionJobRunner, Se
 
     // Collect files to process
     List<String> segmentsToPush = getSegmentsToPush(outputDirFS, outputDirURI);
+    if (segmentsToPush.isEmpty()) {
+      return;
+    }
 
     // Ensure tableConfigURI is set if missing
     setupTableConfigURI();
@@ -239,7 +238,8 @@ public class SparkSegmentMetadataPushJobRunner implements 
IngestionJobRunner, Se
 
   private List<String> getSegmentsToPush(PinotFS outputDirFS, URI 
outputDirURI) {
     String[] files = listFiles(outputDirFS, outputDirURI);
-    return Arrays.stream(files).filter(file -> 
file.endsWith(Constants.TAR_GZ_FILE_EXT)).collect(Collectors.toList());
+    return new ArrayList<>(
+        SegmentPushUtils.getSegmentUriToTarPathMap(outputDirURI, 
_spec.getPushJobSpec(), files).values());
   }
 
   private void setupFileSystems() {
@@ -308,11 +308,7 @@ public class SparkSegmentMetadataPushJobRunner implements 
IngestionJobRunner, Se
     Collection<String> tarFilePaths = segmentsUriToTarPathMap.values();
     List<String> segmentNames = new ArrayList<>(tarFilePaths.size());
     for (String tarFilePath : tarFilePaths) {
-      File tarFile = new File(tarFilePath);
-      String fileName = tarFile.getName();
-      
Preconditions.checkArgument(fileName.endsWith(Constants.TAR_GZ_FILE_EXT));
-      String segmentName = fileName.substring(0, fileName.length() - 
Constants.TAR_GZ_FILE_EXT.length());
-      segmentNames.add(segmentName);
+      
segmentNames.add(SegmentPushUtils.getSegmentNameFromTarFile(tarFilePath));
     }
     return segmentNames;
   }
diff --git 
a/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-spark-base/src/main/java/org/apache/pinot/plugin/ingestion/batch/spark/common/BaseSparkSegmentMetadataPushJobRunner.java
 
b/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-spark-base/src/main/java/org/apache/pinot/plugin/ingestion/batch/spark/common/BaseSparkSegmentMetadataPushJobRunner.java
index 049a0eb3b3b..cf98c4447f8 100644
--- 
a/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-spark-base/src/main/java/org/apache/pinot/plugin/ingestion/batch/spark/common/BaseSparkSegmentMetadataPushJobRunner.java
+++ 
b/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-spark-base/src/main/java/org/apache/pinot/plugin/ingestion/batch/spark/common/BaseSparkSegmentMetadataPushJobRunner.java
@@ -30,7 +30,6 @@ import org.apache.pinot.spi.env.PinotConfiguration;
 import org.apache.pinot.spi.filesystem.PinotFS;
 import org.apache.pinot.spi.filesystem.PinotFSFactory;
 import org.apache.pinot.spi.ingestion.batch.runner.IngestionJobRunner;
-import org.apache.pinot.spi.ingestion.batch.spec.Constants;
 import org.apache.pinot.spi.ingestion.batch.spec.PinotFSSpec;
 import org.apache.pinot.spi.ingestion.batch.spec.SegmentGenerationJobSpec;
 import org.apache.pinot.spi.utils.retry.AttemptsExceededException;
@@ -79,11 +78,10 @@ public abstract class BaseSparkSegmentMetadataPushJobRunner 
implements Ingestion
       throw new RuntimeException("Unable to list all files under outputDirURI 
- '" + outputDirURI + "'");
     }
 
-    List<String> segmentsToPush = new ArrayList<>();
-    for (String file : files) {
-      if (file.endsWith(Constants.TAR_GZ_FILE_EXT)) {
-        segmentsToPush.add(file);
-      }
+    List<String> segmentsToPush = new ArrayList<>(
+        SegmentPushUtils.getSegmentUriToTarPathMap(outputDirURI, 
_spec.getPushJobSpec(), files).values());
+    if (segmentsToPush.isEmpty()) {
+      return;
     }
 
     int pushParallelism = _spec.getPushJobSpec().getPushParallelism();
diff --git 
a/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-spark-base/src/main/java/org/apache/pinot/plugin/ingestion/batch/spark/common/BaseSparkSegmentTarPushJobRunner.java
 
b/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-spark-base/src/main/java/org/apache/pinot/plugin/ingestion/batch/spark/common/BaseSparkSegmentTarPushJobRunner.java
index f04c4d6fd17..b9f1e4480ef 100644
--- 
a/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-spark-base/src/main/java/org/apache/pinot/plugin/ingestion/batch/spark/common/BaseSparkSegmentTarPushJobRunner.java
+++ 
b/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-spark-base/src/main/java/org/apache/pinot/plugin/ingestion/batch/spark/common/BaseSparkSegmentTarPushJobRunner.java
@@ -30,7 +30,6 @@ import org.apache.pinot.spi.env.PinotConfiguration;
 import org.apache.pinot.spi.filesystem.PinotFS;
 import org.apache.pinot.spi.filesystem.PinotFSFactory;
 import org.apache.pinot.spi.ingestion.batch.runner.IngestionJobRunner;
-import org.apache.pinot.spi.ingestion.batch.spec.Constants;
 import org.apache.pinot.spi.ingestion.batch.spec.PinotFSSpec;
 import org.apache.pinot.spi.ingestion.batch.spec.SegmentGenerationJobSpec;
 import org.apache.pinot.spi.utils.retry.AttemptsExceededException;
@@ -79,11 +78,10 @@ public abstract class BaseSparkSegmentTarPushJobRunner 
implements IngestionJobRu
       throw new RuntimeException("Unable to list all files under outputDirURI 
- '" + outputDirURI + "'");
     }
 
-    List<String> segmentsToPush = new ArrayList<>();
-    for (String file : files) {
-      if (file.endsWith(Constants.TAR_GZ_FILE_EXT)) {
-        segmentsToPush.add(file);
-      }
+    List<String> segmentsToPush = new ArrayList<>(
+        SegmentPushUtils.getSegmentUriToTarPathMap(outputDirURI, 
_spec.getPushJobSpec(), files).values());
+    if (segmentsToPush.isEmpty()) {
+      return;
     }
 
     int pushParallelism = _spec.getPushJobSpec().getPushParallelism();
diff --git 
a/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-spark-base/src/main/java/org/apache/pinot/plugin/ingestion/batch/spark/common/BaseSparkSegmentUriPushJobRunner.java
 
b/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-spark-base/src/main/java/org/apache/pinot/plugin/ingestion/batch/spark/common/BaseSparkSegmentUriPushJobRunner.java
index 883eb23f56f..11c88975bbc 100644
--- 
a/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-spark-base/src/main/java/org/apache/pinot/plugin/ingestion/batch/spark/common/BaseSparkSegmentUriPushJobRunner.java
+++ 
b/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-spark-base/src/main/java/org/apache/pinot/plugin/ingestion/batch/spark/common/BaseSparkSegmentUriPushJobRunner.java
@@ -30,7 +30,6 @@ import org.apache.pinot.spi.env.PinotConfiguration;
 import org.apache.pinot.spi.filesystem.PinotFS;
 import org.apache.pinot.spi.filesystem.PinotFSFactory;
 import org.apache.pinot.spi.ingestion.batch.runner.IngestionJobRunner;
-import org.apache.pinot.spi.ingestion.batch.spec.Constants;
 import org.apache.pinot.spi.ingestion.batch.spec.PinotFSSpec;
 import org.apache.pinot.spi.ingestion.batch.spec.SegmentGenerationJobSpec;
 import org.apache.pinot.spi.utils.retry.AttemptsExceededException;
@@ -83,15 +82,10 @@ public abstract class BaseSparkSegmentUriPushJobRunner 
implements IngestionJobRu
     } catch (IOException e) {
       throw new RuntimeException("Unable to list all files under outputDirURI 
- '" + outputDirURI + "'");
     }
-    List<String> segmentUris = new ArrayList<>();
-    for (String file : files) {
-      URI uri = URI.create(file);
-      if (uri.getPath().endsWith(Constants.TAR_GZ_FILE_EXT)) {
-        URI updatedURI =
-            SegmentPushUtils.generateSegmentTarURI(outputDirURI, uri, 
_spec.getPushJobSpec().getSegmentUriPrefix(),
-                _spec.getPushJobSpec().getSegmentUriSuffix());
-        segmentUris.add(updatedURI.toString());
-      }
+    List<String> segmentUris = new ArrayList<>(
+        SegmentPushUtils.getSegmentUriToTarPathMap(outputDirURI, 
_spec.getPushJobSpec(), files).keySet());
+    if (segmentUris.isEmpty()) {
+      return;
     }
 
     int pushParallelism = _spec.getPushJobSpec().getPushParallelism();
diff --git 
a/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-spark-base/src/test/java/org/apache/pinot/plugin/ingestion/batch/spark/common/BaseSparkSegmentMetadataPushJobRunnerTest.java
 
b/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-spark-base/src/test/java/org/apache/pinot/plugin/ingestion/batch/spark/common/BaseSparkSegmentMetadataPushJobRunnerTest.java
new file mode 100644
index 00000000000..39b6ec26962
--- /dev/null
+++ 
b/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-spark-base/src/test/java/org/apache/pinot/plugin/ingestion/batch/spark/common/BaseSparkSegmentMetadataPushJobRunnerTest.java
@@ -0,0 +1,125 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.plugin.ingestion.batch.spark.common;
+
+import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.io.FileUtils;
+import org.apache.pinot.plugin.ingestion.batch.common.BaseSegmentPushJobRunner;
+import org.apache.pinot.spi.ingestion.batch.runner.IngestionJobRunner;
+import org.apache.pinot.spi.ingestion.batch.spec.PinotFSSpec;
+import org.apache.pinot.spi.ingestion.batch.spec.PushJobSpec;
+import org.apache.pinot.spi.ingestion.batch.spec.SegmentGenerationJobSpec;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.fail;
+
+
+/// Exercises shared archive selection before dispatching Spark push jobs.
+public class BaseSparkSegmentMetadataPushJobRunnerTest {
+  @Test
+  public void testEmptyStandaloneSelectionSkipsConsistentPush()
+      throws Exception {
+    BaseSegmentPushJobRunner runner = new BaseSegmentPushJobRunner() {
+      @Override
+      public void initFileSys() {
+        _spec = new SegmentGenerationJobSpec();
+        _spec.setPushJobSpec(new PushJobSpec());
+        _spec.getPushJobSpec().setPushFileNamePattern("glob:**/*.tar.zst");
+        _outputDirURI = URI.create("file:/segments/");
+        _files = new String[]{"/segments/segment.tar.gz"};
+        _consistentPushEnabled = true;
+      }
+
+      @Override
+      public List<String> getSegmentsToReplace(Map<String, String> segments) {
+        throw new AssertionError("Empty selection must not start consistent 
push");
+      }
+
+      @Override
+      public void uploadSegments(Map<String, String> segments) {
+        fail("Empty selection must not upload segments");
+      }
+    };
+    runner.run();
+  }
+
+  @Test
+  public void testPushPatternSelectsSegmentArchives()
+      throws Exception {
+    Path outputDir = Files.createTempDirectory("archive push-");
+    try {
+      Path current = Files.createDirectory(outputDir.resolve("current"));
+      Path selected = Files.createFile(current.resolve("segment.tar.zst"));
+      Files.createFile(current.resolve("segment.tar.gz"));
+      
Files.createFile(Files.createDirectory(outputDir.resolve("stale")).resolve("segment.tar.zst"));
+      SegmentGenerationJobSpec spec = new SegmentGenerationJobSpec();
+      spec.setOutputDirURI(outputDir.toUri().toString());
+      spec.setPinotFSSpecs(List.of());
+      PushJobSpec pushSpec = new PushJobSpec();
+      pushSpec.setPushParallelism(2);
+      pushSpec.setPushFileNamePattern("glob:**/current/*.tar.zst");
+      spec.setPushJobSpec(pushSpec);
+      List<String> pushed = new ArrayList<>();
+      BaseSparkSegmentMetadataPushJobRunner runner = new 
BaseSparkSegmentMetadataPushJobRunner(spec) {
+        @Override
+        public void parallelizeMetadataPushJob(List<String> segments, 
List<PinotFSSpec> fileSystems,
+            int parallelism, URI output) {
+          assertFalse(segments.isEmpty());
+          pushed.addAll(segments);
+        }
+      };
+      BaseSparkSegmentTarPushJobRunner tarRunner = new 
BaseSparkSegmentTarPushJobRunner(spec) {
+        @Override
+        public void parallelizeTarPushJob(List<PinotFSSpec> fileSystems, 
List<String> segments,
+            int parallelism, URI output) {
+          assertFalse(segments.isEmpty());
+          pushed.addAll(segments);
+        }
+      };
+      BaseSparkSegmentUriPushJobRunner uriRunner = new 
BaseSparkSegmentUriPushJobRunner(spec) {
+        @Override
+        public void parallelizeUriPushJob(List<PinotFSSpec> fileSystems, 
List<String> segments, int parallelism) {
+          assertFalse(segments.isEmpty());
+          
segments.stream().map(URI::create).map(URI::getPath).forEach(pushed::add);
+        }
+      };
+      for (IngestionJobRunner pushRunner : List.of(runner, tarRunner, 
uriRunner)) {
+        pushed.clear();
+        pushSpec.setPushParallelism(2);
+        pushSpec.setPushFileNamePattern("glob:**/current/*.tar.zst");
+        pushRunner.run();
+        assertEquals(pushed, List.of(selected.toString()));
+        pushed.clear();
+        pushSpec.setPushParallelism(0);
+        pushSpec.setPushFileNamePattern("glob:**/missing/*");
+        pushRunner.run();
+        assertEquals(pushed, List.of());
+      }
+    } finally {
+      FileUtils.deleteDirectory(outputDir.toFile());
+    }
+  }
+}
diff --git 
a/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-standalone/src/main/java/org/apache/pinot/plugin/ingestion/batch/standalone/SegmentGenerationJobRunner.java
 
b/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-standalone/src/main/java/org/apache/pinot/plugin/ingestion/batch/standalone/SegmentGenerationJobRunner.java
index dc6cc6cb730..0163a7da90a 100644
--- 
a/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-standalone/src/main/java/org/apache/pinot/plugin/ingestion/batch/standalone/SegmentGenerationJobRunner.java
+++ 
b/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-standalone/src/main/java/org/apache/pinot/plugin/ingestion/batch/standalone/SegmentGenerationJobRunner.java
@@ -42,7 +42,9 @@ import 
org.apache.pinot.plugin.ingestion.batch.common.SegmentGenerationTaskRunne
 import org.apache.pinot.segment.local.utils.ConsistentDataPushUtils;
 import org.apache.pinot.spi.config.table.TableConfig;
 import org.apache.pinot.spi.data.Schema;
+import org.apache.pinot.spi.data.readers.RecordReaderUtils;
 import org.apache.pinot.spi.env.PinotConfiguration;
+import org.apache.pinot.spi.filesystem.LocalPinotFS;
 import org.apache.pinot.spi.filesystem.PinotFS;
 import org.apache.pinot.spi.filesystem.PinotFSFactory;
 import org.apache.pinot.spi.ingestion.batch.BatchConfigProperties;
@@ -230,9 +232,17 @@ public class SegmentGenerationJobRunner implements 
IngestionJobRunner {
     File localOutputTempDir = new File(localTempDir, "output");
     FileUtils.forceMkdir(localOutputTempDir);
 
-    //copy input path to local
-    File localInputDataFile = createLocalInputDateFile(inputFileURI, 
localInputTempDir);
-    _inputDirFS.copyToLocalFile(inputFileURI, localInputDataFile);
+    // Gzip readers can unpack beside the input, so keep those writes inside 
an owned staging directory.
+    File inputFile = new File(URIUtils.decode(inputFileURI.getRawPath()));
+    boolean borrowedInputFile = 
PinotFSFactory.isFileSystemInstanceOf(_inputDirFS, LocalPinotFS.class)
+        && !RecordReaderUtils.isGZippedFile(inputFile);
+    File localInputDataFile;
+    if (borrowedInputFile) {
+      localInputDataFile = inputFile;
+    } else {
+      localInputDataFile = createLocalInputDateFile(inputFileURI, 
localInputTempDir);
+      _inputDirFS.copyToLocalFile(inputFileURI, localInputDataFile);
+    }
 
     //create task spec
     SegmentGenerationTaskSpec taskSpec = new SegmentGenerationTaskSpec();
@@ -265,7 +275,8 @@ public class SegmentGenerationJobRunner implements 
IngestionJobRunner {
         String segmentName = taskRunner.run();
         // Tar segment directory to compress file
         localSegmentDir = new File(localOutputTempDir, segmentName);
-        String segmentTarFileName = URIUtils.encode(segmentName + 
Constants.TAR_GZ_FILE_EXT);
+        String segmentTarFileName = URIUtils.encode(
+            segmentName + 
Constants.SEGMENT_TAR_FILE_EXTENSIONS.get(_spec.getSegmentTarCompressionCodec()));
         localSegmentTarFile = new File(localOutputTempDir, segmentTarFileName);
         LOGGER.info("Tarring segment from: {} to: {}", localSegmentDir, 
localSegmentTarFile);
         TarCompressionUtils.createCompressedTarFile(localSegmentDir, 
localSegmentTarFile);
@@ -295,7 +306,9 @@ public class SegmentGenerationJobRunner implements 
IngestionJobRunner {
         _segmentCreationTaskCountDownLatch.countDown();
         FileUtils.deleteQuietly(localSegmentDir);
         FileUtils.deleteQuietly(localSegmentTarFile);
-        FileUtils.deleteQuietly(localInputDataFile);
+        if (!borrowedInputFile) {
+          FileUtils.deleteQuietly(localInputDataFile);
+        }
       }
     });
   }
diff --git 
a/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-standalone/src/test/java/org/apache/pinot/plugin/ingestion/batch/standalone/SegmentGenerationJobRunnerTest.java
 
b/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-standalone/src/test/java/org/apache/pinot/plugin/ingestion/batch/standalone/SegmentGenerationJobRunnerTest.java
index 6bc9218e30f..873caa76fa8 100644
--- 
a/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-standalone/src/test/java/org/apache/pinot/plugin/ingestion/batch/standalone/SegmentGenerationJobRunnerTest.java
+++ 
b/pinot-plugins/pinot-batch-ingestion/pinot-batch-ingestion-standalone/src/test/java/org/apache/pinot/plugin/ingestion/batch/standalone/SegmentGenerationJobRunnerTest.java
@@ -22,13 +22,21 @@ import com.google.common.collect.Lists;
 import java.io.File;
 import java.io.FilenameFilter;
 import java.io.IOException;
+import java.lang.reflect.Field;
+import java.net.URI;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
+import java.util.ArrayList;
 import java.util.List;
+import java.util.Set;
+import java.util.zip.GZIPOutputStream;
+import javax.annotation.Nullable;
 import org.apache.commons.io.FileUtils;
+import org.apache.pinot.common.utils.TarCompressionUtils;
 import 
org.apache.pinot.plugin.ingestion.batch.common.SegmentGenerationTaskRunner;
 import org.apache.pinot.plugin.inputformat.csv.CSVRecordReader;
 import org.apache.pinot.plugin.inputformat.csv.CSVRecordReaderConfig;
+import org.apache.pinot.segment.spi.index.metadata.SegmentMetadataImpl;
 import org.apache.pinot.spi.config.table.TableConfig;
 import org.apache.pinot.spi.config.table.TableType;
 import org.apache.pinot.spi.config.table.ingestion.BatchIngestionConfig;
@@ -36,6 +44,8 @@ import 
org.apache.pinot.spi.config.table.ingestion.IngestionConfig;
 import org.apache.pinot.spi.data.FieldSpec.DataType;
 import org.apache.pinot.spi.data.Schema;
 import org.apache.pinot.spi.data.Schema.SchemaBuilder;
+import org.apache.pinot.spi.data.readers.RecordReaderConfig;
+import org.apache.pinot.spi.data.readers.RecordReaderUtils;
 import org.apache.pinot.spi.filesystem.LocalPinotFS;
 import org.apache.pinot.spi.ingestion.batch.BatchConfigProperties;
 import org.apache.pinot.spi.ingestion.batch.spec.ExecutionFrameworkSpec;
@@ -46,14 +56,134 @@ import 
org.apache.pinot.spi.ingestion.batch.spec.SegmentNameGeneratorSpec;
 import org.apache.pinot.spi.ingestion.batch.spec.TableSpec;
 import org.apache.pinot.spi.utils.builder.TableConfigBuilder;
 import org.testng.Assert;
+import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
 
 import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertThrows;
 import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.expectThrows;
 import static org.testng.Assert.fail;
 
 public class SegmentGenerationJobRunnerTest {
 
+  @DataProvider
+  public Object[][] localInputOwnershipCases() {
+    return new Object[][]{{false}, {true}};
+  }
+
+  @Test(dataProvider = "localInputOwnershipCases")
+  public void testLocalInputIsBorrowedAndPreserved(boolean failGeneration)
+      throws Exception {
+    File testDir = makeTestDir();
+    try {
+      File inputDir = new File(testDir, "input");
+      FileUtils.forceMkdir(inputDir);
+      File inputFile = new File(inputDir, "input with spaces.csv");
+      FileUtils.writeLines(inputFile, List.of("col1,col2", "value1,1", 
failGeneration ? "value2,invalid" : "value2,2"));
+      byte[] originalBytes = Files.readAllBytes(inputFile.toPath());
+      File outputDir = new File(testDir, "output");
+      SegmentGenerationJobSpec spec = makeJobSpec(inputDir, outputDir, 
makeSchemaFile(testDir, "mySchema"),
+          makeTableConfigFile(testDir, "mySchema"));
+      spec.setPinotFSSpecs(List.of());
+      SegmentGenerationJobRunner runner = new SegmentGenerationJobRunner(spec);
+      TrackingLocalPinotFS fileSystem = new TrackingLocalPinotFS();
+      Field inputDirFS = 
SegmentGenerationJobRunner.class.getDeclaredField("_inputDirFS");
+      inputDirFS.setAccessible(true);
+      inputDirFS.set(runner, fileSystem);
+      if (failGeneration) {
+        RuntimeException failure = expectThrows(RuntimeException.class, 
runner::run);
+        assertTrue(failure.getMessage().contains("Failed to generate Pinot 
segment"));
+      } else {
+        runner.run();
+        File unpacked = new File(testDir, "unpacked");
+        TarCompressionUtils.untar(new File(outputDir, 
"myTable_OFFLINE_0.tar.gz"), unpacked);
+        assertEquals(new SegmentMetadataImpl(new File(unpacked, 
"myTable_OFFLINE_0")).getTotalDocs(), 2);
+      }
+      assertTrue(fileSystem._copiedInputs.isEmpty(), "Local inputs must be 
borrowed without copying");
+      assertEquals(Files.readAllBytes(inputFile.toPath()), originalBytes);
+    } finally {
+      FileUtils.deleteDirectory(testDir);
+    }
+  }
+
+  @DataProvider
+  public Object[][] compressedInputOwnershipCases() {
+    return new Object[][]{{false, false}, {true, true}};
+  }
+
+  @Test(dataProvider = "compressedInputOwnershipCases")
+  public void testCompressedLocalInputPreservesSourceAndSibling(boolean 
gzipSuffix, boolean failGeneration)
+      throws Exception {
+    File testDir = makeTestDir();
+    try {
+      File inputDir = new File(testDir, "input");
+      FileUtils.forceMkdir(inputDir);
+      String inputName = gzipSuffix ? "input.csv.gz" : "compressed-input";
+      File inputFile = new File(inputDir, inputName);
+      File sibling = new File(inputDir, gzipSuffix ? "input.csv" : 
"compressed-input.csv");
+      byte[] sentinel = "preserve the existing source 
sibling".getBytes(StandardCharsets.UTF_8);
+      Files.write(sibling.toPath(), sentinel);
+      String csv = "col1,col2\nvalue1,1\nvalue2," + (failGeneration ? 
"invalid" : "2") + "\n";
+      try (GZIPOutputStream compressed = new 
GZIPOutputStream(Files.newOutputStream(inputFile.toPath()))) {
+        compressed.write(csv.getBytes(StandardCharsets.UTF_8));
+      }
+      byte[] originalBytes = Files.readAllBytes(inputFile.toPath());
+      File outputDir = new File(testDir, "output");
+      SegmentGenerationJobSpec spec = makeJobSpec(inputDir, outputDir, 
makeSchemaFile(testDir, "mySchema"),
+          makeTableConfigFile(testDir, "mySchema"));
+      spec.setIncludeFileNamePattern("glob:**/" + inputName);
+      
spec.getRecordReaderSpec().setClassName(UnpackingCSVRecordReader.class.getName());
+      SegmentGenerationJobRunner runner = new SegmentGenerationJobRunner(spec);
+      if (failGeneration) {
+        expectThrows(RuntimeException.class, runner::run);
+      } else {
+        runner.run();
+        assertTrue(new File(outputDir, "myTable_OFFLINE_0.tar.gz").isFile());
+      }
+      assertEquals(Files.readAllBytes(inputFile.toPath()), originalBytes);
+      assertEquals(Files.readAllBytes(sibling.toPath()), sentinel, "Existing 
source siblings must not be overwritten");
+    } finally {
+      FileUtils.deleteDirectory(testDir);
+    }
+  }
+
+  @DataProvider(name = "segmentArchiveCodecs")
+  public Object[][] segmentArchiveCodecs() {
+    return new Object[][]{{"gzip", ".tar.gz"}, {"zstd", ".tar.zst"}};
+  }
+
+  @Test(dataProvider = "segmentArchiveCodecs")
+  public void testSegmentGenerationArchiveCodecs(String codec, String 
extension)
+      throws Exception {
+    File testDir = makeTestDir();
+    try {
+      File inputDir = new File(testDir, "input");
+      FileUtils.forceMkdir(inputDir);
+      FileUtils.writeLines(new File(inputDir, "input.csv"), 
List.of("col1,col2", "value1,1", "value2,2"));
+      File outputDir = new File(testDir, "output");
+      SegmentGenerationJobSpec spec = makeJobSpec(inputDir, outputDir, 
makeSchemaFile(testDir, "mySchema"),
+          makeTableConfigFile(testDir, "mySchema"));
+      spec.setSegmentTarCompressionCodec(codec);
+      new SegmentGenerationJobRunner(spec).run();
+
+      File archive = new File(outputDir, "myTable_OFFLINE_0" + extension);
+      assertTrue(archive.isFile());
+      File unpacked = new File(testDir, "unpacked");
+      TarCompressionUtils.untar(archive, unpacked);
+      assertEquals(new SegmentMetadataImpl(new File(unpacked, 
"myTable_OFFLINE_0")).getTotalDocs(), 2);
+    } finally {
+      FileUtils.deleteDirectory(testDir);
+    }
+  }
+
+  @Test
+  public void testSegmentGenerationRejectsUnsupportedArchiveCodec() {
+    SegmentGenerationJobSpec spec = new SegmentGenerationJobSpec();
+    assertEquals(spec.getSegmentTarCompressionCodec(), "gzip");
+    assertThrows(IllegalArgumentException.class, () -> 
spec.setSegmentTarCompressionCodec("bzip2"));
+  }
+
   @Test
   public void testSegmentGeneration() throws Exception {
     // TODO use common resource definitions & code shared with Hadoop unit 
test.
@@ -227,6 +357,27 @@ public class SegmentGenerationJobRunnerTest {
     }
   }
 
+  /// Exercises the same sibling-writing unpacker as the Parquet and ORC 
readers without another plugin dependency.
+  public static class UnpackingCSVRecordReader extends CSVRecordReader {
+    @Override
+    public void init(File dataFile, @Nullable Set<String> fieldsToRead, 
@Nullable RecordReaderConfig config)
+        throws IOException {
+      super.init(RecordReaderUtils.unpackIfRequired(dataFile, "csv"), 
fieldsToRead, config);
+    }
+  }
+
+  /// Records copies to verify that plain local inputs are borrowed.
+  private static class TrackingLocalPinotFS extends LocalPinotFS {
+    private final List<File> _copiedInputs = new ArrayList<>();
+
+    @Override
+    public void copyToLocalFile(URI source, File destination)
+        throws Exception {
+      _copiedInputs.add(destination);
+      super.copyToLocalFile(source, destination);
+    }
+  }
+
   private File makeTestDir() throws IOException {
     File testDir = 
Files.createTempDirectory("testSegmentGeneration-").toFile();
     testDir.delete();
diff --git 
a/pinot-plugins/pinot-segment-uploader/pinot-segment-uploader-default/src/main/java/org/apache/pinot/plugin/segmentuploader/SegmentUploaderDefault.java
 
b/pinot-plugins/pinot-segment-uploader/pinot-segment-uploader-default/src/main/java/org/apache/pinot/plugin/segmentuploader/SegmentUploaderDefault.java
index 4ec4c19c2a3..7d8a79f3601 100644
--- 
a/pinot-plugins/pinot-segment-uploader/pinot-segment-uploader-default/src/main/java/org/apache/pinot/plugin/segmentuploader/SegmentUploaderDefault.java
+++ 
b/pinot-plugins/pinot-segment-uploader/pinot-segment-uploader-default/src/main/java/org/apache/pinot/plugin/segmentuploader/SegmentUploaderDefault.java
@@ -99,7 +99,7 @@ public class SegmentUploaderDefault implements 
SegmentUploader {
     String[] filePaths = outputPinotFS.listFiles(segmentDir, true);
     for (String filePath : filePaths) {
       URI uri = URI.create(filePath);
-      if (!outputPinotFS.isDirectory(uri) && 
filePath.endsWith(Constants.TAR_GZ_FILE_EXT)) {
+      if (!outputPinotFS.isDirectory(uri) && 
Constants.isSegmentTarFile(filePath)) {
         segmentTarURIs.add(uri);
       }
     }
diff --git 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/SegmentPushUtils.java
 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/SegmentPushUtils.java
index e1785d0919f..5ac39f52693 100644
--- 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/SegmentPushUtils.java
+++ 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/SegmentPushUtils.java
@@ -210,10 +210,7 @@ public class SegmentPushUtils implements Serializable {
     try {
       for (String tarFilePath : tarFilePaths) {
         URI tarFileURI = URI.create(tarFilePath);
-        File tarFile = new File(tarFilePath);
-        String fileName = tarFile.getName();
-        
Preconditions.checkArgument(fileName.endsWith(Constants.TAR_GZ_FILE_EXT));
-        String segmentName = fileName.substring(0, fileName.length() - 
Constants.TAR_GZ_FILE_EXT.length());
+        String segmentName = getSegmentNameFromTarFile(tarFilePath);
         for (PinotClusterSpec pinotClusterSpec : spec.getPinotClusterSpecs()) {
           URI controllerURI;
           try {
@@ -462,15 +459,27 @@ public class SegmentPushUtils implements Serializable {
     return generateSegmentMetadataFile(fileSystem, URI.create(tarFilePath));
   }
 
-  /// Segments stored in the deep store do not have the .tar.gz extension.
-  private static String getSegmentName(String tarFilePath) {
+  /// Resolves and validates the segment name from a supported gzip, LZ4, or 
Zstandard archive path.
+  public static String getSegmentNameFromTarFile(String tarFilePath) {
     String fileName = new File(tarFilePath).getName();
-    String segmentName = fileName.endsWith(Constants.TAR_GZ_FILE_EXT)
-        ? fileName.substring(0, fileName.length() - 
Constants.TAR_GZ_FILE_EXT.length()) : fileName;
+    Preconditions.checkArgument(Constants.isSegmentTarFile(fileName), 
"Unsupported segment archive: %s", fileName);
+    String extension = 
Constants.SEGMENT_TAR_FILE_EXTENSIONS.values().stream().filter(fileName::endsWith)
+        .findFirst().orElseThrow();
+    String segmentName = fileName.substring(0, fileName.length() - 
extension.length());
     SegmentNameUtils.validatePartialOrFullSegmentName(segmentName);
     return segmentName;
   }
 
+  /// Segments stored in the deep store can omit the archive extension.
+  private static String getSegmentName(String tarFilePath) {
+    String fileName = new File(tarFilePath).getName();
+    if (Constants.isSegmentTarFile(fileName)) {
+      return getSegmentNameFromTarFile(tarFilePath);
+    }
+    SegmentNameUtils.validatePartialOrFullSegmentName(fileName);
+    return fileName;
+  }
+
   private static String getSegmentNameFromMetadataFile(File 
segmentMetadataFile) {
     String fileName = segmentMetadataFile.getName();
     
Preconditions.checkArgument(fileName.endsWith(Constants.METADATA_TAR_GZ_FILE_EXT),
@@ -684,17 +693,22 @@ public class SegmentPushUtils implements Serializable {
         }
       }
 
-      URI uri = URI.create(file);
+      URI uri = outputDirURI != null && 
"file".equals(outputDirURI.getScheme()) && new File(file).isAbsolute()
+          ? new File(file).toURI() : URI.create(file);
       if (uri.getPath().endsWith(Constants.METADATA_TAR_GZ_FILE_EXT)) {
         // Skip segment metadata tar gz files
         continue;
       }
-      if (uri.getPath().endsWith(Constants.TAR_GZ_FILE_EXT)) {
+      if (Constants.isSegmentTarFile(uri.getPath())) {
         URI updatedURI = SegmentPushUtils.generateSegmentTarURI(outputDirURI, 
uri, pushSpec.getSegmentUriPrefix(),
             pushSpec.getSegmentUriSuffix());
         segmentUriToTarPathMap.put(updatedURI.toString(), file);
       }
     }
+    if (segmentUriToTarPathMap.isEmpty()) {
+      LOGGER.warn("No segment archives selected under outputDirURI: {} with 
pushFileNamePattern: {}", outputDirURI,
+          pushSpec.getPushFileNamePattern());
+    }
     return segmentUriToTarPathMap;
   }
 
@@ -711,8 +725,9 @@ public class SegmentPushUtils implements Serializable {
     File tarFile =
         new File(FileUtils.getTempDirectory(), "segmentTar-" + uuid + 
TarCompressionUtils.TAR_GZ_FILE_EXTENSION);
     File segmentMetadataDir = new File(FileUtils.getTempDirectory(), 
"segmentMetadataDir-" + uuid);
+    boolean localFileSystem = 
PinotFSFactory.isFileSystemInstanceOf(fileSystem, LocalPinotFS.class);
     try {
-      if (fileSystem instanceof LocalPinotFS) {
+      if (localFileSystem) {
         // For local file system, we don't need to copy the tar file.
         tarFile = new File(URIUtils.decode(tarFileURI.getRawPath()));
       } else {
@@ -743,7 +758,7 @@ public class SegmentPushUtils implements Serializable {
       TarCompressionUtils.createCompressedTarFile(segmentMetadataDir, 
segmentMetadataTarFile);
       return segmentMetadataTarFile;
     } finally {
-      if (!(fileSystem instanceof LocalPinotFS)) {
+      if (!localFileSystem) {
         // For local file system, we don't need to delete the tar file.
         FileUtils.deleteQuietly(tarFile);
       }
diff --git 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/SegmentPushUtilsTest.java
 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/SegmentPushUtilsTest.java
index a1763c47252..80d2cf929d8 100644
--- 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/SegmentPushUtilsTest.java
+++ 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/SegmentPushUtilsTest.java
@@ -50,6 +50,7 @@ import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
 import javax.net.ssl.KeyManagerFactory;
 import javax.net.ssl.SSLContext;
 import javax.net.ssl.SSLParameters;
@@ -61,7 +62,9 @@ import org.apache.pinot.common.utils.FileUploadDownloadClient;
 import org.apache.pinot.common.utils.TarCompressionUtils;
 import org.apache.pinot.common.utils.http.HttpClientConfig;
 import org.apache.pinot.common.utils.tls.TlsUtils;
+import org.apache.pinot.spi.filesystem.LocalPinotFS;
 import org.apache.pinot.spi.filesystem.PinotFS;
+import org.apache.pinot.spi.filesystem.PinotFSFactory;
 import org.apache.pinot.spi.ingestion.batch.spec.Constants;
 import org.apache.pinot.spi.ingestion.batch.spec.PinotClusterSpec;
 import org.apache.pinot.spi.ingestion.batch.spec.PushJobSpec;
@@ -72,14 +75,23 @@ import org.mockito.Mockito;
 import org.testng.Assert;
 import org.testng.annotations.AfterMethod;
 import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
+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;
 import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
 import static org.testng.Assert.assertThrows;
 import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.expectThrows;
 
 
 public class SegmentPushUtilsTest {
@@ -401,6 +413,95 @@ public class SegmentPushUtilsTest {
         "segment.name = " + TEST_SEGMENT_NAME);
   }
 
+  @Test(dataProvider = "segmentTarExtensions")
+  public void testGenerateSegmentMetadataFileFromWrappedLocalArchive(String 
extension)
+      throws Exception {
+    File archive = createSegmentMetadataSourceArchive(extension, true);
+    byte[] originalBytes = Files.readAllBytes(archive.toPath());
+    PinotFS fileSystem = spy(PinotFSFactory.create("file"));
+    assertFalse(fileSystem instanceof LocalPinotFS);
+    assertTrue(PinotFSFactory.isFileSystemInstanceOf(fileSystem, 
LocalPinotFS.class));
+    File metadataTar = null;
+    try {
+      metadataTar = SegmentPushUtils.generateSegmentMetadataFile(fileSystem, 
archive.toURI());
+      assertMetadataArchiveContents(metadataTar);
+      verify(fileSystem, never()).copyToLocalFile(any(URI.class), 
any(File.class));
+      verify(fileSystem, never()).close();
+      assertEquals(Files.readAllBytes(archive.toPath()), originalBytes);
+    } finally {
+      FileUtils.deleteQuietly(metadataTar);
+    }
+  }
+
+  @Test
+  public void 
testGenerateSegmentMetadataFileFailurePreservesWrappedLocalArchive()
+      throws Exception {
+    File archive = createSegmentMetadataSourceArchive(".tar.zst", false);
+    byte[] originalBytes = Files.readAllBytes(archive.toPath());
+    PinotFS fileSystem = spy(PinotFSFactory.create("file"));
+    IOException failure = expectThrows(IOException.class,
+        () -> SegmentPushUtils.generateSegmentMetadataFile(fileSystem, 
archive.toURI()));
+    assertTrue(failure.getMessage().contains("creation.meta"));
+    verify(fileSystem, never()).copyToLocalFile(any(URI.class), 
any(File.class));
+    verify(fileSystem, never()).close();
+    assertEquals(Files.readAllBytes(archive.toPath()), originalBytes);
+  }
+
+  @Test
+  public void 
testGenerateSegmentMetadataFileCopiesCustomFileUriAndDeletesOnlyStaging()
+      throws Exception {
+    File archive = createSegmentMetadataSourceArchive(".tar.gz", true);
+    byte[] originalBytes = Files.readAllBytes(archive.toPath());
+    URI customSourceUri = URI.create("file:///virtual/remote-segment.tar.gz");
+    PinotFS fileSystem = mock(PinotFS.class);
+    AtomicReference<File> stagedArchive = new AtomicReference<>();
+    doAnswer(invocation -> {
+      URI source = invocation.getArgument(0);
+      assertEquals(source, customSourceUri);
+      File destination = invocation.getArgument(1);
+      assertFalse(destination.equals(archive));
+      FileUtils.copyFile(archive, destination);
+      stagedArchive.set(destination);
+      return null;
+    }).when(fileSystem).copyToLocalFile(any(URI.class), any(File.class));
+    File metadataTar = null;
+    try {
+      metadataTar = SegmentPushUtils.generateSegmentMetadataFile(fileSystem, 
customSourceUri);
+      assertMetadataArchiveContents(metadataTar);
+      assertNotNull(stagedArchive.get());
+      assertFalse(stagedArchive.get().exists());
+      verify(fileSystem).copyToLocalFile(customSourceUri, stagedArchive.get());
+      verify(fileSystem, never()).close();
+      assertEquals(Files.readAllBytes(archive.toPath()), originalBytes);
+    } finally {
+      FileUtils.deleteQuietly(metadataTar);
+    }
+  }
+
+  private File createSegmentMetadataSourceArchive(String extension, boolean 
includeCreationMeta)
+      throws IOException {
+    File segmentDir = new File(_tempDir, TEST_SEGMENT_NAME);
+    FileUtils.forceMkdir(segmentDir);
+    FileUtils.writeStringToFile(new File(segmentDir, "metadata.properties"), 
"segment.name = " + TEST_SEGMENT_NAME,
+        StandardCharsets.UTF_8);
+    if (includeCreationMeta) {
+      FileUtils.writeStringToFile(new File(segmentDir, "creation.meta"), 
"crc", StandardCharsets.UTF_8);
+    }
+    FileUtils.writeStringToFile(new File(segmentDir, "columns.psf"), "data", 
StandardCharsets.UTF_8);
+    File archive = new File(_tempDir, "segment with spaces and percent%" + 
extension);
+    TarCompressionUtils.createCompressedTarFile(segmentDir, archive);
+    return archive;
+  }
+
+  private void assertMetadataArchiveContents(File metadataTar)
+      throws IOException {
+    File untarred = TarCompressionUtils.untar(metadataTar, new File(_tempDir, 
"untar")).get(0);
+    assertEquals(new HashSet<>(Arrays.asList(untarred.list())), 
Set.of("metadata.properties", "creation.meta"));
+    assertEquals(FileUtils.readFileToString(new File(untarred, 
"metadata.properties"), StandardCharsets.UTF_8),
+        "segment.name = " + TEST_SEGMENT_NAME);
+    assertEquals(FileUtils.readFileToString(new File(untarred, 
"creation.meta"), StandardCharsets.UTF_8), "crc");
+  }
+
   @Test
   public void testSendSegmentUriAndMetadataWithLocalMetadataFiles()
       throws Exception {
@@ -528,6 +629,45 @@ public class SegmentPushUtilsTest {
     assertEquals(result.get(segmentFiles[3]), segmentFiles[3]);
   }
 
+  @DataProvider(name = "segmentTarExtensions")
+  public Object[][] segmentTarExtensions() {
+    return new Object[][]{{".tar.gz"}, {".tar.lz4"}, {".tar.zst"}};
+  }
+
+  @Test(dataProvider = "segmentTarExtensions")
+  public void testSegmentArchiveScanning(String extension)
+      throws IOException {
+    String archiveUri = _tempDir.toURI().resolve(TEST_SEGMENT_NAME + 
extension).toString();
+    assertEquals(SegmentPushUtils.getSegmentNameFromTarFile(archiveUri), 
TEST_SEGMENT_NAME);
+    String[] files = {archiveUri, new File(_tempDir, 
"testSegment.metadata.tar.gz").toURI().toString(),
+        new File(_tempDir, "testSegment.tar.bz2").toURI().toString()};
+    assertEquals(SegmentPushUtils.getSegmentUriToTarPathMap(_tempDir.toURI(), 
new PushJobSpec(), files),
+        Map.of(archiveUri, archiveUri));
+
+    PushJobSpec pushSpec = new PushJobSpec();
+    pushSpec.setSegmentUriPrefix("s3://segments-bucket");
+    assertEquals(SegmentPushUtils.getSegmentUriToTarPathMap(null, pushSpec, 
files),
+        Map.of("s3://segments-bucket" + URI.create(archiveUri).getRawPath(), 
archiveUri));
+
+    File localArchive = new File(_tempDir, "a%3Ab" + extension);
+    assertTrue(localArchive.createNewFile());
+    String localArchivePath = localArchive.getAbsolutePath();
+    String localArchiveUri = _tempDir.toURI().resolve("a%253Ab" + 
extension).toString();
+    String[] localFiles = {localArchivePath};
+    assertEquals(SegmentPushUtils.getSegmentUriToTarPathMap(_tempDir.toURI(), 
new PushJobSpec(), localFiles),
+        Map.of(localArchiveUri, localArchivePath));
+    assertEquals(SegmentPushUtils.getSegmentUriToTarPathMap(_tempDir.toURI(), 
pushSpec, localFiles),
+        Map.of("s3://segments-bucket" + 
URI.create(localArchiveUri).getRawPath(), localArchivePath));
+  }
+
+  @Test
+  public void testSegmentArchiveNameRejectsSidecarsAndUnsupportedCodecs() {
+    assertThrows(IllegalArgumentException.class,
+        () -> 
SegmentPushUtils.getSegmentNameFromTarFile("testSegment.metadata.tar.gz"));
+    assertThrows(IllegalArgumentException.class,
+        () -> 
SegmentPushUtils.getSegmentNameFromTarFile("testSegment.tar.bz2"));
+  }
+
   @Test
   public void testGenerateSegmentMetadataURI()
       throws URISyntaxException {
diff --git 
a/pinot-spi/src/main/java/org/apache/pinot/spi/ingestion/batch/spec/Constants.java
 
b/pinot-spi/src/main/java/org/apache/pinot/spi/ingestion/batch/spec/Constants.java
index 7523fa6e48a..71294dcc7d0 100644
--- 
a/pinot-spi/src/main/java/org/apache/pinot/spi/ingestion/batch/spec/Constants.java
+++ 
b/pinot-spi/src/main/java/org/apache/pinot/spi/ingestion/batch/spec/Constants.java
@@ -18,11 +18,31 @@
  */
 package org.apache.pinot.spi.ingestion.batch.spec;
 
+import java.util.Map;
+
 public class Constants {
   private Constants() {
   }
 
   /// By default Pinot segments are compressed in 'tar.gz' format then pushed 
to controller.
   public static final String TAR_GZ_FILE_EXT = ".tar.gz";
+  public static final String TAR_LZ4_FILE_EXT = ".tar.lz4";
+  public static final String TAR_ZST_FILE_EXT = ".tar.zst";
   public static final String METADATA_TAR_GZ_FILE_EXT = ".metadata.tar.gz";
+
+  public static final String SEGMENT_COMPRESSION_CODEC_GZIP = "gzip";
+  public static final String SEGMENT_COMPRESSION_CODEC_LZ4 = "lz4";
+  public static final String SEGMENT_COMPRESSION_CODEC_ZSTD = "zstd";
+  public static final String DEFAULT_SEGMENT_COMPRESSION_CODEC = 
SEGMENT_COMPRESSION_CODEC_GZIP;
+
+  /// Archive extensions understood by segment upload and metadata extraction, 
including existing LZ4 archives.
+  public static final Map<String, String> SEGMENT_TAR_FILE_EXTENSIONS =
+      Map.of(SEGMENT_COMPRESSION_CODEC_GZIP, TAR_GZ_FILE_EXT, 
SEGMENT_COMPRESSION_CODEC_LZ4, TAR_LZ4_FILE_EXT,
+          SEGMENT_COMPRESSION_CODEC_ZSTD, TAR_ZST_FILE_EXT);
+
+  /// Returns whether a path names a supported segment archive, excluding the 
gzip metadata sidecars.
+  public static boolean isSegmentTarFile(String fileName) {
+    return !fileName.endsWith(METADATA_TAR_GZ_FILE_EXT)
+        && 
SEGMENT_TAR_FILE_EXTENSIONS.values().stream().anyMatch(fileName::endsWith);
+  }
 }
diff --git 
a/pinot-spi/src/main/java/org/apache/pinot/spi/ingestion/batch/spec/SegmentGenerationJobSpec.java
 
b/pinot-spi/src/main/java/org/apache/pinot/spi/ingestion/batch/spec/SegmentGenerationJobSpec.java
index 3992c2e4202..da6b22a2cb2 100644
--- 
a/pinot-spi/src/main/java/org/apache/pinot/spi/ingestion/batch/spec/SegmentGenerationJobSpec.java
+++ 
b/pinot-spi/src/main/java/org/apache/pinot/spi/ingestion/batch/spec/SegmentGenerationJobSpec.java
@@ -19,6 +19,7 @@
 package org.apache.pinot.spi.ingestion.batch.spec;
 
 import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.base.Preconditions;
 import java.io.Serializable;
 import java.util.List;
 import org.apache.pinot.spi.utils.JsonUtils;
@@ -58,6 +59,9 @@ public class SegmentGenerationJobSpec implements Serializable 
{
   /// Segment creation job parallelism.
   private int _segmentCreationJobParallelism;
 
+  /// Codec for segment archives: `gzip` (default) or `zstd`. Metadata 
sidecars always use gzip.
+  private String _segmentTarCompressionCodec = 
Constants.DEFAULT_SEGMENT_COMPRESSION_CODEC;
+
   /// Should overwrite output segments if existed.
   private boolean _overwriteOutput;
 
@@ -228,6 +232,22 @@ public class SegmentGenerationJobSpec implements 
Serializable {
     _segmentCreationJobParallelism = segmentCreationJobParallelism;
   }
 
+  public String getSegmentTarCompressionCodec() {
+    return _segmentTarCompressionCodec == null
+        ? Constants.DEFAULT_SEGMENT_COMPRESSION_CODEC
+        : _segmentTarCompressionCodec;
+  }
+
+  /// Selects the archive codec for standalone, Spark, and Hadoop segment 
generation jobs.
+  /// Generation accepts `gzip` and `zstd`; archive discovery also consumes 
existing LZ4 archives.
+  public void setSegmentTarCompressionCodec(String segmentTarCompressionCodec) 
{
+    
Preconditions.checkArgument(Constants.SEGMENT_COMPRESSION_CODEC_GZIP.equals(segmentTarCompressionCodec)
+            || 
Constants.SEGMENT_COMPRESSION_CODEC_ZSTD.equals(segmentTarCompressionCodec),
+        "Unsupported segmentTarCompressionCodec: %s; supported codecs: %s, 
%s", segmentTarCompressionCodec,
+        Constants.SEGMENT_COMPRESSION_CODEC_GZIP, 
Constants.SEGMENT_COMPRESSION_CODEC_ZSTD);
+    _segmentTarCompressionCodec = segmentTarCompressionCodec;
+  }
+
   public void setCleanUpOutputDir(boolean cleanUpOutputDir) {
     _cleanUpOutputDir = cleanUpOutputDir;
   }
diff --git 
a/pinot-tools/src/main/java/org/apache/pinot/tools/admin/command/LaunchBackfillIngestionJobCommand.java
 
b/pinot-tools/src/main/java/org/apache/pinot/tools/admin/command/LaunchBackfillIngestionJobCommand.java
index bf327153a89..6122e7ebdaf 100644
--- 
a/pinot-tools/src/main/java/org/apache/pinot/tools/admin/command/LaunchBackfillIngestionJobCommand.java
+++ 
b/pinot-tools/src/main/java/org/apache/pinot/tools/admin/command/LaunchBackfillIngestionJobCommand.java
@@ -23,6 +23,9 @@ import java.io.File;
 import java.io.IOException;
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.nio.file.FileSystems;
+import java.nio.file.PathMatcher;
+import java.nio.file.Paths;
 import java.time.LocalDate;
 import java.time.ZoneId;
 import java.util.ArrayList;
@@ -41,6 +44,7 @@ import org.apache.pinot.common.utils.http.HttpClient;
 import 
org.apache.pinot.plugin.ingestion.batch.common.SegmentGenerationTaskRunner;
 import org.apache.pinot.plugin.minion.tasks.SegmentConversionUtils;
 import org.apache.pinot.segment.local.utils.ConsistentDataPushUtils;
+import org.apache.pinot.segment.local.utils.SegmentPushUtils;
 import org.apache.pinot.segment.spi.partition.PartitionFunctionFactory;
 import org.apache.pinot.segment.spi.partition.metadata.ColumnPartitionMetadata;
 import org.apache.pinot.spi.auth.AuthProvider;
@@ -247,7 +251,7 @@ public class LaunchBackfillIngestionJobCommand extends 
LaunchDataIngestionJobCom
     return generatedSegments;
   }
 
-  private List<String> collectSegmentNames(SegmentGenerationJobSpec spec) {
+  List<String> collectSegmentNames(SegmentGenerationJobSpec spec) {
     URI outputDirURI;
     try {
       outputDirURI = new URI(spec.getOutputDirURI());
@@ -259,21 +263,28 @@ public class LaunchBackfillIngestionJobCommand extends 
LaunchDataIngestionJobCom
     }
     PinotFS outputDirFS = PinotFSFactory.create(outputDirURI.getScheme());
 
-    // Get list of files to process
-    String[] files;
+    PathMatcher pushFilePathMatcher =
+        spec.getPushJobSpec() != null && 
spec.getPushJobSpec().getPushFileNamePattern() != null
+            ? 
FileSystems.getDefault().getPathMatcher(spec.getPushJobSpec().getPushFileNamePattern())
+            : null;
+    List<String> generatedSegments = new ArrayList<>();
     try {
-      files = outputDirFS.listFiles(outputDirURI, true);
+      for (String file : outputDirFS.listFiles(outputDirURI, true)) {
+        if (!Constants.isSegmentTarFile(file)
+            || pushFilePathMatcher != null && 
!pushFilePathMatcher.matches(Paths.get(file))) {
+          continue;
+        }
+        URI fileURI = 
PinotFSFactory.LOCAL_PINOT_FS_SCHEME.equals(outputDirURI.getScheme())
+            && !file.startsWith(PinotFSFactory.LOCAL_PINOT_FS_SCHEME + ":")
+            ? new File(file).toURI()
+            : URI.create(file);
+        if (!outputDirFS.isDirectory(fileURI)) {
+          
generatedSegments.add(SegmentPushUtils.getSegmentNameFromTarFile(file));
+        }
+      }
     } catch (IOException e) {
       throw new RuntimeException("Unable to list all files under outputDirURI 
- '" + outputDirURI + "'");
     }
-
-    List<String> generatedSegments = new ArrayList<>();
-    for (String file : files) {
-      if (file.endsWith(Constants.TAR_GZ_FILE_EXT)) {
-        generatedSegments.add(
-            file.substring(file.lastIndexOf(File.separator) + 1, 
file.indexOf(Constants.TAR_GZ_FILE_EXT)));
-      }
-    }
     return generatedSegments;
   }
 
diff --git 
a/pinot-tools/src/test/java/org/apache/pinot/tools/admin/command/LaunchBackfillIngestionJobCommandTest.java
 
b/pinot-tools/src/test/java/org/apache/pinot/tools/admin/command/LaunchBackfillIngestionJobCommandTest.java
new file mode 100644
index 00000000000..db9be4152dd
--- /dev/null
+++ 
b/pinot-tools/src/test/java/org/apache/pinot/tools/admin/command/LaunchBackfillIngestionJobCommandTest.java
@@ -0,0 +1,59 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.tools.admin.command;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.util.List;
+import java.util.Set;
+import org.apache.commons.io.FileUtils;
+import org.apache.pinot.spi.ingestion.batch.spec.PushJobSpec;
+import org.apache.pinot.spi.ingestion.batch.spec.SegmentGenerationJobSpec;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+
+
+/// Tests archive discovery for backfill replacement lineage.
+public class LaunchBackfillIngestionJobCommandTest {
+  @Test
+  public void testCollectSegmentNames()
+      throws Exception {
+    File outputDir = Files.createTempDirectory("backfill segments ").toFile();
+    try {
+      File nestedDir = new File(outputDir, "nested.tar.gz");
+      FileUtils.forceMkdir(nestedDir);
+      for (String file : List.of("gzip.tar.gz", "zstd.tar.zst", "lz4.tar.lz4", 
"gzip.metadata.tar.gz", "input.csv")) {
+        FileUtils.touch(new File(nestedDir, file));
+      }
+      FileUtils.forceMkdir(new File(nestedDir, "directory.tar.zst"));
+      SegmentGenerationJobSpec spec = new SegmentGenerationJobSpec();
+      spec.setOutputDirURI(outputDir.toURI().toString());
+      LaunchBackfillIngestionJobCommand command = new 
LaunchBackfillIngestionJobCommand();
+      assertEquals(Set.copyOf(command.collectSegmentNames(spec)), 
Set.of("gzip", "zstd", "lz4"));
+
+      PushJobSpec pushSpec = new PushJobSpec();
+      pushSpec.setPushFileNamePattern("glob:**/*.tar.zst");
+      spec.setPushJobSpec(pushSpec);
+      assertEquals(command.collectSegmentNames(spec), List.of("zstd"));
+    } finally {
+      FileUtils.deleteDirectory(outputDir);
+    }
+  }
+}


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

Reply via email to