This is an automated email from the ASF dual-hosted git repository.
Jackie-Jiang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git
The following commit(s) were added to refs/heads/master by this push:
new b8e2ef27671 Build METADATA push metadata from the local segment
instead of re-downloading the staged tar (#19490)
b8e2ef27671 is described below
commit b8e2ef27671ab9a62b3e2b955af664015ddb42e0
Author: Kartik Khare <[email protected]>
AuthorDate: Tue Oct 6 00:12:22 2026 +0530
Build METADATA push metadata from the local segment instead of
re-downloading the staged tar (#19490)
---
.../BaseMultipleSegmentsConversionExecutor.java | 67 ++---
.../tasks/BaseSingleSegmentConversionExecutor.java | 20 +-
.../plugin/minion/tasks/BaseTaskExecutor.java | 19 --
...BaseMultipleSegmentsConversionExecutorTest.java | 177 +++++++++++-
.../BaseSingleSegmentConversionExecutorTest.java | 42 ++-
.../segment/local/utils/SegmentPushUtils.java | 307 ++++++++++++---------
.../segment/local/utils/SegmentPushUtilsTest.java | 112 ++++++++
7 files changed, 542 insertions(+), 202 deletions(-)
diff --git
a/pinot-plugins/pinot-minion-tasks/pinot-minion-builtin-tasks/src/main/java/org/apache/pinot/plugin/minion/tasks/BaseMultipleSegmentsConversionExecutor.java
b/pinot-plugins/pinot-minion-tasks/pinot-minion-builtin-tasks/src/main/java/org/apache/pinot/plugin/minion/tasks/BaseMultipleSegmentsConversionExecutor.java
index 052dd2df721..b5df8a4306e 100644
---
a/pinot-plugins/pinot-minion-tasks/pinot-minion-builtin-tasks/src/main/java/org/apache/pinot/plugin/minion/tasks/BaseMultipleSegmentsConversionExecutor.java
+++
b/pinot-plugins/pinot-minion-tasks/pinot-minion-builtin-tasks/src/main/java/org/apache/pinot/plugin/minion/tasks/BaseMultipleSegmentsConversionExecutor.java
@@ -36,6 +36,7 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.stream.Collectors;
+import javax.annotation.Nullable;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
@@ -57,7 +58,6 @@ import org.apache.pinot.segment.spi.SegmentMetadata;
import org.apache.pinot.segment.spi.index.metadata.SegmentMetadataImpl;
import org.apache.pinot.spi.auth.AuthProvider;
import org.apache.pinot.spi.config.table.TableType;
-import org.apache.pinot.spi.filesystem.PinotFS;
import org.apache.pinot.spi.ingestion.batch.BatchConfigProperties;
import org.apache.pinot.spi.ingestion.batch.spec.PushJobSpec;
import org.apache.pinot.spi.ingestion.batch.spec.SegmentGenerationJobSpec;
@@ -214,6 +214,10 @@ public abstract class
BaseMultipleSegmentsConversionExecutor extends BaseTaskExe
int numOutputSegments = segmentConversionResults.size();
List<File> tarredSegmentFiles = new ArrayList<>(numOutputSegments);
+ // METADATA pushes send only metadata.properties and creation.meta,
built here from the local converted segment
+ // so the staged tar is never downloaded back. Null entries for TAR push.
+ List<File> segmentMetadataTarFiles = new ArrayList<>(numOutputSegments);
+ BatchConfigProperties.SegmentPushType pushType =
getSegmentPushType(taskConfigs);
int count = 1;
for (SegmentConversionResult segmentConversionResult :
segmentConversionResults) {
File convertedSegmentDir = segmentConversionResult.getFile();
@@ -226,6 +230,9 @@ public abstract class
BaseMultipleSegmentsConversionExecutor extends BaseTaskExe
segmentConversionResult.getSegmentName() +
TarCompressionUtils.TAR_GZ_FILE_EXTENSION);
TarCompressionUtils.createCompressedTarFile(convertedSegmentDir,
convertedSegmentTarFile);
tarredSegmentFiles.add(convertedSegmentTarFile);
+ segmentMetadataTarFiles.add(pushType ==
BatchConfigProperties.SegmentPushType.TAR ? null
+ :
SegmentPushUtils.generateSegmentMetadataFile(convertedSegmentDir,
convertedTarredSegmentDir,
+ segmentConversionResult.getSegmentName()));
if (!FileUtils.deleteQuietly(convertedSegmentDir)) {
LOGGER.warn("Failed to delete converted segment: {}",
convertedSegmentDir.getAbsolutePath());
}
@@ -249,13 +256,14 @@ public abstract class
BaseMultipleSegmentsConversionExecutor extends BaseTaskExe
SegmentUploadContext segmentUploadContext = new
SegmentUploadContext(pinotTaskConfig, segmentConversionResults);
preUploadSegments(segmentUploadContext);
- Map<String, String> segmentUriToTarPathMap = new HashMap<>();
+ Map<String, File> segmentUriToMetadataFileMap = new HashMap<>();
PushJobSpec pushJobSpec = getPushJobSpec(taskConfigs);
boolean batchSegmentUpload = pushJobSpec.isBatchSegmentUpload();
// Upload the tarred segments
for (int i = 0; i < numOutputSegments; i++) {
File convertedTarredSegmentFile = tarredSegmentFiles.get(i);
+ File segmentMetadataTarFile = segmentMetadataTarFiles.get(i);
SegmentConversionResult segmentConversionResult =
segmentConversionResults.get(i);
String resultSegmentName = segmentConversionResult.getSegmentName();
_eventObserver.notifyProgress(_pinotTaskConfig, "Uploading segment: "
+ resultSegmentName + " (" + (i + 1)
@@ -294,20 +302,24 @@ public abstract class
BaseMultipleSegmentsConversionExecutor extends BaseTaskExe
}
if (batchSegmentUpload) {
- updateSegmentUriToTarPathMap(taskConfigs, outputSegmentTarURI,
segmentConversionResult,
- segmentUriToTarPathMap, pushJobSpec);
+ for (String segmentUri : getSegmentUris(taskConfigs,
outputSegmentTarURI, segmentConversionResult,
+ pushJobSpec)) {
+ segmentUriToMetadataFileMap.put(segmentUri,
segmentMetadataTarFile);
+ }
} else {
String rawTableName =
TableNameBuilder.extractRawTableName(tableNameWithType);
- pushSegment(rawTableName, taskConfigs, outputSegmentTarURI,
httpHeaders, parameters, segmentConversionResult);
+ pushSegment(rawTableName, taskConfigs, outputSegmentTarURI,
httpHeaders, parameters, segmentConversionResult,
+ segmentMetadataTarFile);
if (!FileUtils.deleteQuietly(convertedTarredSegmentFile)) {
LOGGER.warn("Failed to delete tarred converted segment: {}",
convertedTarredSegmentFile.getAbsolutePath());
}
+ FileUtils.deleteQuietly(segmentMetadataTarFile);
}
}
if (batchSegmentUpload) {
try {
- pushSegments(tableNameWithType, taskConfigs, pinotTaskConfig,
segmentUriToTarPathMap, pushJobSpec,
+ pushSegments(tableNameWithType, taskConfigs, pinotTaskConfig,
segmentUriToMetadataFileMap, pushJobSpec,
authProvider, segmentConversionResults);
} finally {
for (File convertedTarredSegmentFile : tarredSegmentFiles) {
@@ -316,6 +328,7 @@ public abstract class
BaseMultipleSegmentsConversionExecutor extends BaseTaskExe
convertedTarredSegmentFile.getAbsolutePath());
}
}
+ segmentMetadataTarFiles.forEach(FileUtils::deleteQuietly);
}
}
@@ -410,21 +423,17 @@ public abstract class
BaseMultipleSegmentsConversionExecutor extends BaseTaskExe
}
}
+ /// Download URIs the controller should register for a staged segment tar
(prefix/suffix rules from the push spec).
@VisibleForTesting
- void updateSegmentUriToTarPathMap(Map<String, String> taskConfigs, URI
outputSegmentTarURI,
- SegmentConversionResult segmentConversionResult, Map<String, String>
segmentUriToTarPathMap,
- PushJobSpec pushJobSpec) {
+ Set<String> getSegmentUris(Map<String, String> taskConfigs, URI
outputSegmentTarURI,
+ SegmentConversionResult segmentConversionResult, PushJobSpec
pushJobSpec) {
String segmentName = segmentConversionResult.getSegmentName();
if
(!taskConfigs.containsKey(BatchConfigProperties.OUTPUT_SEGMENT_DIR_URI)) {
throw new RuntimeException("Output dir URI missing for metadata push
while processing segment: " + segmentName);
}
URI outputSegmentDirURI =
URI.create(taskConfigs.get(BatchConfigProperties.OUTPUT_SEGMENT_DIR_URI));
- Map<String, String> localSegmentUriToTarPathMap =
- SegmentPushUtils.getSegmentUriToTarPathMap(outputSegmentDirURI,
pushJobSpec,
- new String[]{outputSegmentTarURI.toString()});
- if (!localSegmentUriToTarPathMap.isEmpty()) {
- segmentUriToTarPathMap.putAll(localSegmentUriToTarPathMap);
- }
+ return SegmentPushUtils.getSegmentUriToTarPathMap(outputSegmentDirURI,
pushJobSpec,
+ new String[]{outputSegmentTarURI.toString()}).keySet();
}
@VisibleForTesting
@@ -436,23 +445,20 @@ public abstract class
BaseMultipleSegmentsConversionExecutor extends BaseTaskExe
}
private void pushSegments(String tableNameWithType, Map<String, String>
taskConfigs, PinotTaskConfig pinotTaskConfig,
- Map<String, String> segmentUriToTarPathMap, PushJobSpec pushJobSpec,
- AuthProvider authProvider, List<SegmentConversionResult>
segmentConversionResults)
+ Map<String, File> segmentUriToMetadataFileMap, PushJobSpec pushJobSpec,
AuthProvider authProvider,
+ List<SegmentConversionResult> segmentConversionResults)
throws Exception {
String tableName = TableNameBuilder.extractRawTableName(tableNameWithType);
SegmentGenerationJobSpec spec =
generateSegmentGenerationJobSpec(tableName, taskConfigs, pushJobSpec);
List<Header> headers = getSegmentPushCommonHeaders(pinotTaskConfig,
authProvider, segmentConversionResults);
List<NameValuePair> parameters =
getSegmentPushCommonParams(tableNameWithType);
-
- URI outputSegmentDirURI =
URI.create(taskConfigs.get(BatchConfigProperties.OUTPUT_SEGMENT_DIR_URI));
- try (PinotFS outputFileFS = MinionTaskUtils.getOutputPinotFS(taskConfigs,
outputSegmentDirURI)) {
- SegmentPushUtils.sendSegmentsUriAndMetadata(spec, outputFileFS,
segmentUriToTarPathMap, headers, parameters);
- }
+ SegmentPushUtils.sendSegmentsUriAndMetadata(spec,
segmentUriToMetadataFileMap, headers, parameters);
}
private void pushSegment(String tableName, Map<String, String> taskConfigs,
URI outputSegmentTarURI,
- List<Header> headers, List<NameValuePair> parameters,
SegmentConversionResult segmentConversionResult)
+ List<Header> headers, List<NameValuePair> parameters,
SegmentConversionResult segmentConversionResult,
+ @Nullable File segmentMetadataTarFile)
throws Exception {
BatchConfigProperties.SegmentPushType pushType =
getSegmentPushType(taskConfigs);
LOGGER.info("Trying to push Pinot segment with push mode {} from {}",
pushType, outputSegmentTarURI);
@@ -470,17 +476,12 @@ public abstract class
BaseMultipleSegmentsConversionExecutor extends BaseTaskExe
uploadURL, tarFile);
break;
case METADATA:
- if
(taskConfigs.containsKey(BatchConfigProperties.OUTPUT_SEGMENT_DIR_URI)) {
- URI outputSegmentDirURI =
URI.create(taskConfigs.get(BatchConfigProperties.OUTPUT_SEGMENT_DIR_URI));
- try (PinotFS outputFileFS =
MinionTaskUtils.getOutputPinotFS(taskConfigs, outputSegmentDirURI)) {
- Map<String, String> segmentUriToTarPathMap =
-
SegmentPushUtils.getSegmentUriToTarPathMap(outputSegmentDirURI, pushJobSpec,
- new String[]{outputSegmentTarURI.toString()});
- SegmentPushUtils.sendSegmentUriAndMetadata(spec, outputFileFS,
segmentUriToTarPathMap, headers, parameters);
- }
- } else {
- throw new RuntimeException("Output dir URI missing for metadata
push");
+ Map<String, File> segmentUriToMetadataFileMap = new HashMap<>();
+ for (String segmentUri : getSegmentUris(taskConfigs,
outputSegmentTarURI, segmentConversionResult,
+ pushJobSpec)) {
+ segmentUriToMetadataFileMap.put(segmentUri, segmentMetadataTarFile);
}
+ SegmentPushUtils.sendSegmentUriAndMetadata(spec,
segmentUriToMetadataFileMap, headers, parameters);
break;
default:
throw new UnsupportedOperationException("Unrecognized push mode - " +
pushType);
diff --git
a/pinot-plugins/pinot-minion-tasks/pinot-minion-builtin-tasks/src/main/java/org/apache/pinot/plugin/minion/tasks/BaseSingleSegmentConversionExecutor.java
b/pinot-plugins/pinot-minion-tasks/pinot-minion-builtin-tasks/src/main/java/org/apache/pinot/plugin/minion/tasks/BaseSingleSegmentConversionExecutor.java
index 72311626f07..eb348cef805 100644
---
a/pinot-plugins/pinot-minion-tasks/pinot-minion-builtin-tasks/src/main/java/org/apache/pinot/plugin/minion/tasks/BaseSingleSegmentConversionExecutor.java
+++
b/pinot-plugins/pinot-minion-tasks/pinot-minion-builtin-tasks/src/main/java/org/apache/pinot/plugin/minion/tasks/BaseSingleSegmentConversionExecutor.java
@@ -23,6 +23,7 @@ import com.google.common.base.Preconditions;
import java.io.File;
import java.net.URI;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@@ -147,12 +148,16 @@ public abstract class BaseSingleSegmentConversionExecutor
extends BaseTaskExecut
BatchConfigProperties.SegmentPushType pushType =
getSegmentPushType(configs);
boolean copyToDeepStore =
pushType == BatchConfigProperties.SegmentPushType.METADATA &&
isCopyToDeepStoreForMetadataPush();
- // Controller-copy pushes send only metadata.properties and
creation.meta, built from the local converted
- // segment. An unchanged segment (same CRC) skips the tar and the
staging and only re-registers its metadata.
+ // METADATA pushes send only metadata.properties and creation.meta,
built here from the local converted segment
+ // so the staged tar is never downloaded back. A controller-copy push of
an unchanged segment (same CRC) skips
+ // the tar and the staging and only re-registers its metadata.
File segmentMetadataTarFile = null;
boolean reuseExistingSegment = false;
+ if (pushType == BatchConfigProperties.SegmentPushType.METADATA) {
+ segmentMetadataTarFile =
+ SegmentPushUtils.generateSegmentMetadataFile(convertedSegmentDir,
tempDataDir, segmentName);
+ }
if (copyToDeepStore) {
- segmentMetadataTarFile =
createSegmentMetadataTarFile(convertedSegmentDir, tempDataDir, segmentName);
long convertedSegmentCrc = new
SegmentMetadataImpl(convertedSegmentDir).getCrc();
reuseExistingSegment =
convertedSegmentCrc == Long.parseLong(originalSegmentCrc) &&
StringUtils.isNotEmpty(downloadURL);
@@ -224,7 +229,7 @@ public abstract class BaseSingleSegmentConversionExecutor
extends BaseTaskExecut
uploadURL, downloadURL, convertedTarredSegmentFile,
segmentMetadataTarFile);
} else {
uploadSegmentWithMetadata(configs, pinotTaskConfig,
segmentConversionResult, authProvider, parameters,
- tableNameWithType, convertedTarredSegmentFile);
+ tableNameWithType, convertedTarredSegmentFile,
segmentMetadataTarFile);
}
break;
default:
@@ -262,7 +267,7 @@ public abstract class BaseSingleSegmentConversionExecutor
extends BaseTaskExecut
/// [BatchConfigProperties#PUSH_CONTROLLER_URI] in configs.
private void uploadSegmentWithMetadata(Map<String, String> configs,
PinotTaskConfig pinotTaskConfig,
SegmentConversionResult segmentConversionResult, AuthProvider
authProvider, List<NameValuePair> parameters,
- String tableNameWithType, File convertedTarredSegmentFile)
+ String tableNameWithType, File convertedTarredSegmentFile, File
segmentMetadataTarFile)
throws Exception {
if (!configs.containsKey(BatchConfigProperties.OUTPUT_SEGMENT_DIR_URI)) {
throw new RuntimeException("Output dir URI missing for metadata push.
Set "
@@ -282,9 +287,10 @@ public abstract class BaseSingleSegmentConversionExecutor
extends BaseTaskExecut
try (PinotFS outputFileFS = MinionTaskUtils.getOutputPinotFS(configs,
outputSegmentDirURI)) {
Map<String, String> segmentUriToTarPathMap =
SegmentPushUtils.getSegmentUriToTarPathMap(outputSegmentDirURI,
pushJobSpec, new String[]{outputSegmentTarURI.toString()});
+ Map<String, File> segmentUriToMetadataFileMap = new HashMap<>();
+ segmentUriToTarPathMap.keySet().forEach(uri ->
segmentUriToMetadataFileMap.put(uri, segmentMetadataTarFile));
try {
- SegmentPushUtils.sendSegmentUriAndMetadata(spec, outputFileFS,
segmentUriToTarPathMap, metadataHeaders,
- parameters);
+ SegmentPushUtils.sendSegmentUriAndMetadata(spec,
segmentUriToMetadataFileMap, metadataHeaders, parameters);
} catch (Exception e) {
// The tar was already staged to the output PinotFS before this
failure. If the task is retried, the next
// moveSegmentToOutputPinotFS() would fail with "Output file already
exists" (overwriteOutput defaults to
diff --git
a/pinot-plugins/pinot-minion-tasks/pinot-minion-builtin-tasks/src/main/java/org/apache/pinot/plugin/minion/tasks/BaseTaskExecutor.java
b/pinot-plugins/pinot-minion-tasks/pinot-minion-builtin-tasks/src/main/java/org/apache/pinot/plugin/minion/tasks/BaseTaskExecutor.java
index 61fe7b74601..dce1317d5f1 100644
---
a/pinot-plugins/pinot-minion-tasks/pinot-minion-builtin-tasks/src/main/java/org/apache/pinot/plugin/minion/tasks/BaseTaskExecutor.java
+++
b/pinot-plugins/pinot-minion-tasks/pinot-minion-builtin-tasks/src/main/java/org/apache/pinot/plugin/minion/tasks/BaseTaskExecutor.java
@@ -20,7 +20,6 @@ package org.apache.pinot.plugin.minion.tasks;
import com.google.common.base.Preconditions;
import java.io.File;
-import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
@@ -45,14 +44,12 @@ import org.apache.pinot.core.minion.PinotTaskConfig;
import org.apache.pinot.core.util.PeerServerSegmentFinder;
import org.apache.pinot.minion.MinionContext;
import org.apache.pinot.minion.executor.PinotTaskExecutor;
-import org.apache.pinot.segment.spi.store.SegmentDirectoryPaths;
import org.apache.pinot.spi.auth.AuthProvider;
import org.apache.pinot.spi.config.table.TableConfig;
import org.apache.pinot.spi.config.table.TableType;
import org.apache.pinot.spi.data.Schema;
import org.apache.pinot.spi.filesystem.PinotFS;
import org.apache.pinot.spi.ingestion.batch.BatchConfigProperties;
-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;
import org.apache.pinot.spi.ingestion.batch.spec.SegmentGenerationJobSpec;
@@ -249,22 +246,6 @@ public abstract class BaseTaskExecutor implements
PinotTaskExecutor {
}
}
- /// Tars only metadata.properties and creation.meta from the local segment,
which is all a METADATA push sends.
- protected File createSegmentMetadataTarFile(File segmentDir, File outputDir,
String segmentName)
- throws IOException {
- File metadataDir = new File(outputDir, segmentName + "-metadata");
- File metadataTarFile = new File(outputDir, segmentName +
Constants.METADATA_TAR_GZ_FILE_EXT);
- try {
- FileUtils.forceMkdir(metadataDir);
-
FileUtils.copyFileToDirectory(SegmentDirectoryPaths.findMetadataFile(segmentDir),
metadataDir);
-
FileUtils.copyFileToDirectory(SegmentDirectoryPaths.findCreationMetaFile(segmentDir),
metadataDir);
- TarCompressionUtils.createCompressedTarFile(metadataDir,
metadataTarFile);
- return metadataTarFile;
- } finally {
- FileUtils.deleteQuietly(metadataDir);
- }
- }
-
/// Returns HTTP parameters common to segment upload and metadata push
(parallel push protection, table name, type).
protected List<NameValuePair> getSegmentPushCommonParams(String
tableNameWithType) {
List<NameValuePair> params = new ArrayList<>();
diff --git
a/pinot-plugins/pinot-minion-tasks/pinot-minion-builtin-tasks/src/test/java/org/apache/pinot/plugin/minion/tasks/BaseMultipleSegmentsConversionExecutorTest.java
b/pinot-plugins/pinot-minion-tasks/pinot-minion-builtin-tasks/src/test/java/org/apache/pinot/plugin/minion/tasks/BaseMultipleSegmentsConversionExecutorTest.java
index 6793e1ad175..7f1a2c15529 100644
---
a/pinot-plugins/pinot-minion-tasks/pinot-minion-builtin-tasks/src/test/java/org/apache/pinot/plugin/minion/tasks/BaseMultipleSegmentsConversionExecutorTest.java
+++
b/pinot-plugins/pinot-minion-tasks/pinot-minion-builtin-tasks/src/test/java/org/apache/pinot/plugin/minion/tasks/BaseMultipleSegmentsConversionExecutorTest.java
@@ -22,9 +22,12 @@ import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.UUID;
import org.apache.commons.io.FileUtils;
import org.apache.hc.core5.http.Header;
@@ -32,17 +35,39 @@ import org.apache.hc.core5.http.NameValuePair;
import org.apache.hc.core5.http.message.BasicNameValuePair;
import org.apache.pinot.common.auth.NullAuthProvider;
import
org.apache.pinot.common.metadata.segment.SegmentZKMetadataCustomMapModifier;
+import org.apache.pinot.common.metrics.MinionMetrics;
import org.apache.pinot.common.utils.FileUploadDownloadClient;
+import org.apache.pinot.common.utils.TarCompressionUtils;
import org.apache.pinot.core.common.MinionConstants;
import org.apache.pinot.core.minion.PinotTaskConfig;
+import org.apache.pinot.minion.MinionConf;
import org.apache.pinot.minion.MinionContext;
+import org.apache.pinot.minion.event.MinionEventObservers;
+import
org.apache.pinot.segment.local.segment.creator.impl.SegmentIndexCreationDriverImpl;
+import org.apache.pinot.segment.local.segment.readers.GenericRowRecordReader;
+import org.apache.pinot.segment.local.utils.SegmentPushUtils;
+import org.apache.pinot.segment.spi.V1Constants;
+import org.apache.pinot.segment.spi.creator.SegmentGeneratorConfig;
+import org.apache.pinot.segment.spi.index.metadata.SegmentMetadataImpl;
import org.apache.pinot.spi.auth.AuthProvider;
+import org.apache.pinot.spi.config.instance.InstanceType;
+import org.apache.pinot.spi.config.table.TableConfig;
+import org.apache.pinot.spi.config.table.TableType;
+import org.apache.pinot.spi.data.FieldSpec;
+import org.apache.pinot.spi.data.Schema;
+import org.apache.pinot.spi.data.readers.GenericRow;
+import org.apache.pinot.spi.filesystem.PinotFS;
import org.apache.pinot.spi.ingestion.batch.BatchConfigProperties;
+import org.apache.pinot.spi.ingestion.batch.spec.Constants;
import org.apache.pinot.spi.ingestion.batch.spec.PushJobSpec;
+import org.apache.pinot.spi.utils.builder.TableConfigBuilder;
+import org.mockito.MockedStatic;
import org.mockito.Mockito;
+import org.mockito.stubbing.Answer;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
@@ -237,7 +262,7 @@ public class BaseMultipleSegmentsConversionExecutorTest {
}
@Test
- public void testUpdateSegmentURIToTarPathMap()
+ public void testGetSegmentUris()
throws IOException {
// setup
File segmentDir = new File(_tempDir, "segments");
@@ -254,14 +279,154 @@ public class BaseMultipleSegmentsConversionExecutorTest {
PushJobSpec pushJobSpec = new PushJobSpec();
SegmentConversionResult conversionResult =
new
SegmentConversionResult.Builder().setSegmentName("mySegment").build();
- Map<String, String> segmentUriToTarPathMap = new HashMap<>();
// test
- _executor.updateSegmentUriToTarPathMap(taskConfigs, outputSegmentTarURI,
conversionResult, segmentUriToTarPathMap,
- pushJobSpec);
+ Set<String> segmentUris = _executor.getSegmentUris(taskConfigs,
outputSegmentTarURI, conversionResult, pushJobSpec);
// validate
- Assert.assertEquals(segmentUriToTarPathMap.size(), 1);
- Assert.assertTrue(segmentUriToTarPathMap.containsKey(_tempDir.toURI() +
"segments/segment.tar.gz"));
+ Assert.assertEquals(segmentUris, Set.of(_tempDir.toURI() +
"segments/segment.tar.gz"));
+ }
+
+ @DataProvider(name = "batchSegmentUpload")
+ public Object[][] batchSegmentUpload() {
+ return new Object[][]{{false}, {true}};
+ }
+
+ /// On METADATA push the metadata tar of every output segment is built from
the local converted segment and pushed
+ /// with the local-metadata overload, so the staged tar is never downloaded
back. Covers per-segment and batch pushes.
+ @Test(dataProvider = "batchSegmentUpload")
+ public void testMetadataPushUsesLocalMetadataTars(boolean batchSegmentUpload)
+ throws Exception {
+ File outputDir = new File(_tempDir, "output");
+ FileUtils.forceMkdir(outputDir);
+ File dataDir = new File(_tempDir, "minionData");
+ FileUtils.forceMkdir(dataDir);
+ File capturedDir = new File(_tempDir, "captured");
+ FileUtils.forceMkdir(capturedDir);
+ File inputSegmentDir = buildSegment("input_0");
+ List<String> outputSegmentNames = List.of("merged_0", "merged_1");
+ Map<String, File> outputSegmentDirs = new HashMap<>();
+ for (String name : outputSegmentNames) {
+ outputSegmentDirs.put(name, buildSegment(name));
+ }
+ String taskId = "Task_TestMultiSegmentTask_" + UUID.randomUUID();
+ MinionContext.getInstance().setDataDir(dataDir);
+ MinionMetrics.register(Mockito.mock(MinionMetrics.class));
+ MinionEventObservers.getInstance().addMinionEventObserver(taskId,
MinionTaskTestUtils.getMinionProgressObserver());
+
+ Map<String, String> configs = new HashMap<>();
+ configs.put(MinionConstants.TABLE_NAME_KEY, "myTable_OFFLINE");
+ configs.put(MinionConstants.SEGMENT_NAME_KEY, "input_0");
+ configs.put(MinionConstants.DOWNLOAD_URL_KEY, "http://unused/download");
+ configs.put(MinionConstants.UPLOAD_URL_KEY, "http://unused/upload");
+ configs.put(BatchConfigProperties.PUSH_MODE,
BatchConfigProperties.SegmentPushType.METADATA.name());
+ configs.put(BatchConfigProperties.OUTPUT_SEGMENT_DIR_URI,
outputDir.toURI().toString());
+ configs.put(BatchConfigProperties.BATCH_SEGMENT_UPLOAD,
String.valueOf(batchSegmentUpload));
+ configs.put("TASK_ID", taskId);
+
+ Map<String, File> capturedUriToMetadataFile = new HashMap<>();
+ try (MockedStatic<SegmentPushUtils> mocked =
Mockito.mockStatic(SegmentPushUtils.class,
+ Mockito.CALLS_REAL_METHODS)) {
+ Answer<Void> capture = invocation -> {
+ Map<String, File> uriToMetadataFile = invocation.getArgument(1);
+ for (Map.Entry<String, File> entry : uriToMetadataFile.entrySet()) {
+ File copy = new File(capturedDir, entry.getValue().getName());
+ FileUtils.copyFile(entry.getValue(), copy);
+ capturedUriToMetadataFile.put(entry.getKey(), copy);
+ }
+ return null;
+ };
+ mocked.when(() ->
SegmentPushUtils.sendSegmentUriAndMetadata(Mockito.any(), Mockito.anyMap(),
Mockito.anyList(),
+ Mockito.anyList())).thenAnswer(capture);
+ mocked.when(() ->
SegmentPushUtils.sendSegmentsUriAndMetadata(Mockito.any(), Mockito.anyMap(),
Mockito.anyList(),
+ Mockito.anyList())).thenAnswer(capture);
+
+ new TestMultipleSegmentsConversionExecutor(inputSegmentDir,
outputSegmentDirs).executeTask(
+ new PinotTaskConfig("TestMultiSegmentTask", configs));
+
+ // Never the PinotFS variants, which download the staged tar back to
extract the metadata
+ mocked.verify(() ->
SegmentPushUtils.sendSegmentUriAndMetadata(Mockito.any(),
Mockito.any(PinotFS.class),
+ Mockito.anyMap(), Mockito.anyList(), Mockito.anyList()),
Mockito.never());
+ mocked.verify(() ->
SegmentPushUtils.sendSegmentsUriAndMetadata(Mockito.any(),
Mockito.any(PinotFS.class),
+ Mockito.anyMap(), Mockito.anyList(), Mockito.anyList()),
Mockito.never());
+ } finally {
+ MinionEventObservers.getInstance().removeMinionEventObserver(taskId);
+ MinionContext.getInstance().setDataDir(null);
+ }
+
+ Assert.assertEquals(capturedUriToMetadataFile.size(), 2);
+ for (String name : outputSegmentNames) {
+ File stagedTar = new File(outputDir, name +
TarCompressionUtils.TAR_GZ_FILE_EXTENSION);
+ Assert.assertTrue(stagedTar.isFile(), "staged tar is the download URL
and must stay");
+ File metadataTar =
capturedUriToMetadataFile.get(stagedTar.toURI().toString());
+ Assert.assertNotNull(metadataTar, "metadata pushed for " +
stagedTar.toURI());
+ Assert.assertEquals(metadataTar.getName(), name +
Constants.METADATA_TAR_GZ_FILE_EXT);
+ File untarred = TarCompressionUtils.untar(metadataTar, new
File(_tempDir, "untar-" + name)).get(0);
+ Assert.assertEquals(new HashSet<>(Arrays.asList(untarred.list())),
+ Set.of(V1Constants.MetadataKeys.METADATA_FILE_NAME,
V1Constants.SEGMENT_CREATION_META));
+ Assert.assertEquals(new SegmentMetadataImpl(untarred).getName(), name);
+ }
+ }
+
+ private File buildSegment(String segmentName)
+ throws Exception {
+ TableConfig tableConfig = new
TableConfigBuilder(TableType.OFFLINE).setTableName("myTable").build();
+ Schema schema = new Schema.SchemaBuilder().addSingleValueDimension("d1",
FieldSpec.DataType.INT).build();
+ GenericRow row = new GenericRow();
+ row.putValue("d1", 1);
+ SegmentGeneratorConfig config = new SegmentGeneratorConfig(tableConfig,
schema);
+ config.setInstanceType(InstanceType.MINION);
+ config.setOutDir(new File(_tempDir, "built").getPath());
+ config.setSegmentName(segmentName);
+ SegmentIndexCreationDriverImpl driver = new
SegmentIndexCreationDriverImpl();
+ driver.init(config, new GenericRowRecordReader(List.of(row)));
+ driver.build();
+ return new File(new File(_tempDir, "built"), segmentName);
+ }
+
+ /// Stubs download, the segment-existence pre-check and conversion so
executeTask runs to the push step offline.
+ private static class TestMultipleSegmentsConversionExecutor extends
BaseMultipleSegmentsConversionExecutor {
+ private final File _inputSegmentDir;
+ private final Map<String, File> _outputSegmentDirs;
+
+ TestMultipleSegmentsConversionExecutor(File inputSegmentDir, Map<String,
File> outputSegmentDirs) {
+ super(new MinionConf());
+ _inputSegmentDir = inputSegmentDir;
+ _outputSegmentDirs = outputSegmentDirs;
+ }
+
+ @Override
+ protected void preProcess(PinotTaskConfig pinotTaskConfig) {
+ }
+
+ @Override
+ protected File downloadSegmentToLocalAndUntar(String tableNameWithType,
String segmentName, String deepstoreURL,
+ String taskType, File tempDataDir, String suffix)
+ throws Exception {
+ File indexDir = new File(tempDataDir, "input" + suffix);
+ FileUtils.copyDirectory(_inputSegmentDir, indexDir);
+ return indexDir;
+ }
+
+ @Override
+ protected List<SegmentConversionResult> convert(PinotTaskConfig
pinotTaskConfig, List<File> segmentDirs,
+ File workingDir)
+ throws Exception {
+ List<SegmentConversionResult> results = new ArrayList<>();
+ for (Map.Entry<String, File> entry : _outputSegmentDirs.entrySet()) {
+ File convertedDir = new File(workingDir, entry.getKey());
+ FileUtils.copyDirectory(entry.getValue(), convertedDir);
+ results.add(new SegmentConversionResult.Builder().setFile(convertedDir)
+
.setTableNameWithType(pinotTaskConfig.getConfigs().get(MinionConstants.TABLE_NAME_KEY))
+ .setSegmentName(entry.getKey()).build());
+ }
+ return results;
+ }
+
+ @Override
+ protected SegmentZKMetadataCustomMapModifier
getSegmentZKMetadataCustomMapModifier(PinotTaskConfig pinotTaskConfig,
+ SegmentConversionResult segmentConversionResult) {
+ return new
SegmentZKMetadataCustomMapModifier(SegmentZKMetadataCustomMapModifier.ModifyMode.UPDATE,
Map.of());
+ }
}
}
diff --git
a/pinot-plugins/pinot-minion-tasks/pinot-minion-builtin-tasks/src/test/java/org/apache/pinot/plugin/minion/tasks/BaseSingleSegmentConversionExecutorTest.java
b/pinot-plugins/pinot-minion-tasks/pinot-minion-builtin-tasks/src/test/java/org/apache/pinot/plugin/minion/tasks/BaseSingleSegmentConversionExecutorTest.java
index e677ebfcb10..7bb7a542cf9 100644
---
a/pinot-plugins/pinot-minion-tasks/pinot-minion-builtin-tasks/src/test/java/org/apache/pinot/plugin/minion/tasks/BaseSingleSegmentConversionExecutorTest.java
+++
b/pinot-plugins/pinot-minion-tasks/pinot-minion-builtin-tasks/src/test/java/org/apache/pinot/plugin/minion/tasks/BaseSingleSegmentConversionExecutorTest.java
@@ -165,8 +165,8 @@ public class BaseSingleSegmentConversionExecutorTest {
Mockito.mockStatic(SegmentPushUtils.class,
Mockito.CALLS_REAL_METHODS)) {
minionTaskUtils.when(() ->
MinionTaskUtils.getOutputPinotFS(Mockito.any(), Mockito.any()))
.thenReturn(mockOutputFS);
- segmentPushUtils.when(() ->
SegmentPushUtils.sendSegmentUriAndMetadata(Mockito.any(), Mockito.any(),
- Mockito.any(), Mockito.anyList(), Mockito.anyList()))
+ segmentPushUtils.when(() ->
SegmentPushUtils.sendSegmentUriAndMetadata(Mockito.any(), Mockito.anyMap(),
+ Mockito.anyList(), Mockito.anyList()))
.thenThrow(new RuntimeException("simulated metadata push failure"));
TestSingleSegmentConversionExecutor executor = new
TestSingleSegmentConversionExecutor(STALE_SEGMENT_CRC);
@@ -182,30 +182,40 @@ public class BaseSingleSegmentConversionExecutorTest {
}
- /// The default METADATA push still registers the staged tar's URI and never
takes the controller-copy path.
+ /// The default METADATA push still registers the staged tar's URI, with a
metadata tar built from the local
+ /// segment instead of the staged tar being downloaded back, and never takes
the controller-copy path.
@Test
public void testDefaultMetadataPushRegistersStagedUri()
throws Exception {
File outputDir = new File(TEMP_DIR, "output-default");
FileUtils.forceMkdir(outputDir);
+ File capturedMetadataTar = new File(TEMP_DIR,
"captured-default-metadata.tar.gz");
+ List<String> capturedUris = new ArrayList<>();
try (MockedStatic<SegmentPushUtils> segmentPushUtils =
Mockito.mockStatic(SegmentPushUtils.class,
Mockito.CALLS_REAL_METHODS);
MockedStatic<SegmentConversionUtils> conversionUtils =
Mockito.mockStatic(SegmentConversionUtils.class)) {
- segmentPushUtils.when(() ->
SegmentPushUtils.sendSegmentUriAndMetadata(Mockito.any(), Mockito.any(),
- Mockito.any(), Mockito.anyList(),
Mockito.anyList())).thenAnswer(invocation -> null);
+ segmentPushUtils.when(() ->
SegmentPushUtils.sendSegmentUriAndMetadata(Mockito.any(), Mockito.anyMap(),
+ Mockito.anyList(), Mockito.anyList())).thenAnswer(invocation -> {
+ Map<String, File> uriToMetadataFile = invocation.getArgument(1);
+ capturedUris.addAll(uriToMetadataFile.keySet());
+ FileUtils.copyFile(uriToMetadataFile.values().iterator().next(),
capturedMetadataTar);
+ return null;
+ });
new TestSingleSegmentConversionExecutor(STALE_SEGMENT_CRC).executeTask(
createMetadataPushTaskConfig(STALE_SEGMENT_CRC, outputDir));
- String stagedTarName = SEGMENT_NAME +
TarCompressionUtils.TAR_GZ_FILE_EXTENSION;
- segmentPushUtils.verify(() ->
SegmentPushUtils.sendSegmentUriAndMetadata(Mockito.any(), Mockito.any(),
- Mockito.argThat((Map<String, String> uriToTar) -> uriToTar.size() ==
1
- && uriToTar.values().iterator().next().endsWith(stagedTarName)),
- Mockito.anyList(), Mockito.anyList()));
+ // No PinotFS-based push, which is the variant that downloads the staged
tar back.
+ segmentPushUtils.verify(() ->
SegmentPushUtils.sendSegmentUriAndMetadata(Mockito.any(),
+ Mockito.any(PinotFS.class), Mockito.anyMap(), Mockito.anyList(),
Mockito.anyList()), Mockito.never());
conversionUtils.verifyNoInteractions();
}
+ File stagedTar = new File(outputDir, SEGMENT_NAME +
TarCompressionUtils.TAR_GZ_FILE_EXTENSION);
+ Assert.assertEquals(capturedUris.size(), 1);
+ Assert.assertEquals(new File(URI.create(capturedUris.get(0))), stagedTar);
+ assertMetadataTarDescribesSegment(capturedMetadataTar, new File(TEMP_DIR,
"untar-default-metadata"));
// The staged tar is the segment's download URL in this mode, so it stays.
- Assert.assertTrue(new File(outputDir, SEGMENT_NAME +
TarCompressionUtils.TAR_GZ_FILE_EXTENSION).isFile());
+ Assert.assertTrue(stagedTar.isFile());
}
/// Controller-copy push of a changed segment: task-unique staging name, TAR
guards plus copy flag, a metadata-only
@@ -250,9 +260,13 @@ public class BaseSingleSegmentConversionExecutorTest {
String downloadUri = headerValue(capturedHeaders,
FileUploadDownloadClient.CustomHeaders.DOWNLOAD_URI);
Assert.assertEquals(new File(URI.create(downloadUri)), stagedTar);
- // The metadata tar carries only the two files the controller reads, and
they describe the converted segment.
- File untarredMetadataDir = TarCompressionUtils.untar(capturedMetadataTar,
new File(TEMP_DIR, "untar-metadata"))
- .get(0);
+ assertMetadataTarDescribesSegment(capturedMetadataTar, new File(TEMP_DIR,
"untar-metadata"));
+ }
+
+ /// The metadata tar carries only the two files the controller reads, and
they describe the converted segment.
+ private void assertMetadataTarDescribesSegment(File metadataTar, File
untarDir)
+ throws Exception {
+ File untarredMetadataDir = TarCompressionUtils.untar(metadataTar,
untarDir).get(0);
Set<String> fileNames =
java.util.Arrays.stream(untarredMetadataDir.listFiles()).map(File::getName).collect(Collectors.toSet());
Assert.assertEquals(fileNames,
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 cd8bcc80a06..e1785d0919f 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
@@ -59,6 +59,7 @@ import org.apache.pinot.common.utils.tls.TlsUtils;
import org.apache.pinot.segment.local.constants.SegmentUploadConstants;
import org.apache.pinot.segment.spi.V1Constants;
import org.apache.pinot.segment.spi.creator.name.SegmentNameUtils;
+import org.apache.pinot.segment.spi.store.SegmentDirectoryPaths;
import org.apache.pinot.spi.auth.AuthProvider;
import org.apache.pinot.spi.config.table.TableType;
import org.apache.pinot.spi.env.PinotConfiguration;
@@ -342,87 +343,17 @@ public class SegmentPushUtils implements Serializable {
public static void sendSegmentUriAndMetadata(SegmentGenerationJobSpec spec,
PinotFS fileSystem,
Map<String, String> segmentUriToTarPathMap, List<Header> headers,
List<NameValuePair> parameters)
throws Exception {
- String tableName = spec.getTableSpec().getTableName();
LOGGER.info("Start pushing segment metadata: {} to locations: {} for table
{}", segmentUriToTarPathMap,
- Arrays.toString(spec.getPinotClusterSpecs()), tableName);
+ Arrays.toString(spec.getPinotClusterSpecs()),
spec.getTableSpec().getTableName());
FileUploadDownloadClient fileUploadDownloadClient =
getOrCreateFileUploadDownloadClient(spec);
- int socketTimeoutMs = getSocketTimeoutMs(spec);
try {
- for (String segmentUriPath : segmentUriToTarPathMap.keySet()) {
- String tarFilePath = segmentUriToTarPathMap.get(segmentUriPath);
- String fileName = new File(tarFilePath).getName();
- // segments stored in Pinot deep store do not have .tar.gz extension
- String segmentName = fileName.endsWith(Constants.TAR_GZ_FILE_EXT)
- ? fileName.substring(0, fileName.length() -
Constants.TAR_GZ_FILE_EXT.length()) : fileName;
- SegmentNameUtils.validatePartialOrFullSegmentName(segmentName);
- File segmentMetadataFile;
- // Check if there is a segment metadata tar gz file named
`segmentName.metadata.tar.gz`, already in the remote
- // directory. This is to avoid generating a new segment metadata tar
gz file every time we push a segment,
- // which requires downloading the entire segment tar gz file.
-
- URI metadataTarGzFilePath = generateSegmentMetadataURI(tarFilePath,
segmentName);
- LOGGER.info("Checking if metadata tar gz file {} exists",
metadataTarGzFilePath);
- if (spec.getPushJobSpec().isPreferMetadataTarGz() &&
fileSystem.exists(metadataTarGzFilePath)) {
- segmentMetadataFile = new File(FileUtils.getTempDirectory(),
- "segmentMetadata-" + UUID.randomUUID() +
TarCompressionUtils.TAR_GZ_FILE_EXTENSION);
- if (segmentMetadataFile.exists()) {
- FileUtils.forceDelete(segmentMetadataFile);
- }
- fileSystem.copyToLocalFile(metadataTarGzFilePath,
segmentMetadataFile);
- } else {
- segmentMetadataFile = generateSegmentMetadataFile(fileSystem,
URI.create(tarFilePath));
- }
+ for (Map.Entry<String, String> entry :
segmentUriToTarPathMap.entrySet()) {
+ String tarFilePath = entry.getValue();
+ String segmentName = getSegmentName(tarFilePath);
+ File segmentMetadataFile = getSegmentMetadataFile(spec, fileSystem,
tarFilePath, segmentName);
try {
- for (PinotClusterSpec pinotClusterSpec :
spec.getPinotClusterSpecs()) {
- URI controllerURI;
- try {
- controllerURI = new URI(pinotClusterSpec.getControllerURI());
- } catch (URISyntaxException e) {
- throw new RuntimeException("Got invalid controller uri - '" +
pinotClusterSpec.getControllerURI() + "'");
- }
- LOGGER.info("Pushing segment: {} to location: {} for table {}",
segmentName, controllerURI, tableName);
- int attempts = 1;
- if (spec.getPushJobSpec() != null &&
spec.getPushJobSpec().getPushAttempts() > 0) {
- attempts = spec.getPushJobSpec().getPushAttempts();
- }
- long retryWaitMs = 1000L;
- if (spec.getPushJobSpec() != null &&
spec.getPushJobSpec().getPushRetryIntervalMillis() > 0) {
- retryWaitMs = spec.getPushJobSpec().getPushRetryIntervalMillis();
- }
- RetryPolicies.exponentialBackoffRetryPolicy(attempts, retryWaitMs,
5).attempt(() -> {
- List<Header> reqHttpHeaders = new ArrayList<>(headers);
- try {
- reqHttpHeaders.add(
- new
BasicHeader(FileUploadDownloadClient.CustomHeaders.DOWNLOAD_URI,
segmentUriPath));
- reqHttpHeaders.add(new
BasicHeader(FileUploadDownloadClient.CustomHeaders.UPLOAD_TYPE,
-
FileUploadDownloadClient.FileUploadType.METADATA.toString()));
- if (spec.getPushJobSpec() != null) {
- reqHttpHeaders.add(new
BasicHeader(FileUploadDownloadClient.CustomHeaders.COPY_SEGMENT_TO_DEEP_STORE,
-
String.valueOf(spec.getPushJobSpec().getCopyToDeepStoreForMetadataPush())));
- }
-
- SimpleHttpResponse response =
fileUploadDownloadClient.uploadSegmentMetadata(
-
FileUploadDownloadClient.getUploadSegmentURI(controllerURI), segmentName,
- segmentMetadataFile, reqHttpHeaders, parameters,
socketTimeoutMs);
- LOGGER.info("Response for pushing table {} segment {} to
location {} - {}: {}", tableName, segmentName,
- controllerURI, response.getStatusCode(),
response.getResponse());
- return true;
- } catch (HttpErrorStatusException e) {
- int statusCode = e.getStatusCode();
- if (statusCode >= 500) {
- // Temporary exception
- LOGGER.warn("Caught temporary exception while pushing table:
{} segment: {} to {}, will retry",
- tableName, segmentName, controllerURI, e);
- return false;
- } else {
- // Permanent exception
- LOGGER.error("Caught permanent exception while pushing
table: {} segment: {} to {}, won't retry",
- tableName, segmentName, controllerURI, e);
- throw e;
- }
- }
- });
- }
+ pushSegmentMetadata(spec, fileUploadDownloadClient, entry.getKey(),
segmentName, segmentMetadataFile,
+ headers, parameters);
} finally {
FileUtils.deleteQuietly(segmentMetadataFile);
}
@@ -432,30 +363,171 @@ public class SegmentPushUtils implements Serializable {
}
}
- public static void sendSegmentsUriAndMetadata(SegmentGenerationJobSpec spec,
PinotFS fileSystem,
- Map<String, String> segmentUriToTarPathMap, List<Header> headers,
List<NameValuePair> parameters)
+ /// Same as above for segments whose metadata tars the caller already holds
locally (see
+ /// [#generateSegmentMetadataFile(File, File, String)]), so nothing is
downloaded back from the output filesystem.
+ /// Each file must be named `<segmentName>.metadata.tar.gz`, and the caller
owns the files.
+ public static void sendSegmentUriAndMetadata(SegmentGenerationJobSpec spec,
+ Map<String, File> segmentUriToMetadataFileMap, List<Header> headers,
List<NameValuePair> parameters)
throws Exception {
- String tableName = spec.getTableSpec().getTableName();
- ConcurrentHashMap<String, File> segmentMetadataFileMap = new
ConcurrentHashMap<>();
- ConcurrentLinkedQueue<String> segmentURIs = new ConcurrentLinkedQueue<>();
- Map<String, File> allSegmentsMetadataMap = new HashMap<>();
- File allSegmentsMetadataTarFile = null;
- int nThreads =
spec.getPushJobSpec().getSegmentMetadataGenerationParallelism();
+ LOGGER.info("Start pushing local segment metadata for: {} to locations: {}
for table {}",
+ segmentUriToMetadataFileMap.keySet(),
Arrays.toString(spec.getPinotClusterSpecs()),
+ spec.getTableSpec().getTableName());
FileUploadDownloadClient fileUploadDownloadClient =
getOrCreateFileUploadDownloadClient(spec);
+ try {
+ for (Map.Entry<String, File> entry :
segmentUriToMetadataFileMap.entrySet()) {
+ File segmentMetadataFile = entry.getValue();
+ pushSegmentMetadata(spec, fileUploadDownloadClient, entry.getKey(),
+ getSegmentNameFromMetadataFile(segmentMetadataFile),
segmentMetadataFile, headers, parameters);
+ }
+ } finally {
+ closeFileUploadDownloadClient(spec, fileUploadDownloadClient);
+ }
+ }
+
+ private static void pushSegmentMetadata(SegmentGenerationJobSpec spec,
+ FileUploadDownloadClient fileUploadDownloadClient, String
segmentUriPath, String segmentName,
+ File segmentMetadataFile, List<Header> headers, List<NameValuePair>
parameters)
+ throws Exception {
+ String tableName = spec.getTableSpec().getTableName();
int socketTimeoutMs = getSocketTimeoutMs(spec);
- ExecutorService executor = Executors.newFixedThreadPool(nThreads);
+ for (PinotClusterSpec pinotClusterSpec : spec.getPinotClusterSpecs()) {
+ URI controllerURI;
+ try {
+ controllerURI = new URI(pinotClusterSpec.getControllerURI());
+ } catch (URISyntaxException e) {
+ throw new RuntimeException("Got invalid controller uri - '" +
pinotClusterSpec.getControllerURI() + "'");
+ }
+ LOGGER.info("Pushing segment: {} to location: {} for table {}",
segmentName, controllerURI, tableName);
+ int attempts = 1;
+ if (spec.getPushJobSpec() != null &&
spec.getPushJobSpec().getPushAttempts() > 0) {
+ attempts = spec.getPushJobSpec().getPushAttempts();
+ }
+ long retryWaitMs = 1000L;
+ if (spec.getPushJobSpec() != null &&
spec.getPushJobSpec().getPushRetryIntervalMillis() > 0) {
+ retryWaitMs = spec.getPushJobSpec().getPushRetryIntervalMillis();
+ }
+ RetryPolicies.exponentialBackoffRetryPolicy(attempts, retryWaitMs,
5).attempt(() -> {
+ List<Header> reqHttpHeaders = new ArrayList<>(headers);
+ try {
+ reqHttpHeaders.add(
+ new
BasicHeader(FileUploadDownloadClient.CustomHeaders.DOWNLOAD_URI,
segmentUriPath));
+ reqHttpHeaders.add(new
BasicHeader(FileUploadDownloadClient.CustomHeaders.UPLOAD_TYPE,
+ FileUploadDownloadClient.FileUploadType.METADATA.toString()));
+ if (spec.getPushJobSpec() != null) {
+ reqHttpHeaders.add(new
BasicHeader(FileUploadDownloadClient.CustomHeaders.COPY_SEGMENT_TO_DEEP_STORE,
+
String.valueOf(spec.getPushJobSpec().getCopyToDeepStoreForMetadataPush())));
+ }
+
+ SimpleHttpResponse response =
fileUploadDownloadClient.uploadSegmentMetadata(
+ FileUploadDownloadClient.getUploadSegmentURI(controllerURI),
segmentName,
+ segmentMetadataFile, reqHttpHeaders, parameters,
socketTimeoutMs);
+ LOGGER.info("Response for pushing table {} segment {} to location {}
- {}: {}", tableName, segmentName,
+ controllerURI, response.getStatusCode(), response.getResponse());
+ return true;
+ } catch (HttpErrorStatusException e) {
+ int statusCode = e.getStatusCode();
+ if (statusCode >= 500) {
+ // Temporary exception
+ LOGGER.warn("Caught temporary exception while pushing table: {}
segment: {} to {}, will retry",
+ tableName, segmentName, controllerURI, e);
+ return false;
+ } else {
+ // Permanent exception
+ LOGGER.error("Caught permanent exception while pushing table: {}
segment: {} to {}, won't retry",
+ tableName, segmentName, controllerURI, e);
+ throw e;
+ }
+ }
+ });
+ }
+ }
+
+ /// Metadata tar for a staged segment tar: the
`<segmentName>.metadata.tar.gz` sidecar when the spec prefers it and
+ /// it exists, otherwise extracted from the segment tar (downloaded first
unless the filesystem is local).
+ private static File getSegmentMetadataFile(SegmentGenerationJobSpec spec,
PinotFS fileSystem, String tarFilePath,
+ String segmentName)
+ throws Exception {
+ URI metadataTarGzFilePath = generateSegmentMetadataURI(tarFilePath,
segmentName);
+ LOGGER.info("Checking if metadata tar gz file {} exists",
metadataTarGzFilePath);
+ if (spec.getPushJobSpec().isPreferMetadataTarGz() &&
fileSystem.exists(metadataTarGzFilePath)) {
+ File segmentMetadataFile = new File(FileUtils.getTempDirectory(),
+ SegmentUploadConstants.SEGMENT_METADATA_DIR_PREFIX +
UUID.randomUUID()
+ + TarCompressionUtils.TAR_GZ_FILE_EXTENSION);
+ if (segmentMetadataFile.exists()) {
+ FileUtils.forceDelete(segmentMetadataFile);
+ }
+ fileSystem.copyToLocalFile(metadataTarGzFilePath, segmentMetadataFile);
+ return segmentMetadataFile;
+ }
+ 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) {
+ 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;
+ SegmentNameUtils.validatePartialOrFullSegmentName(segmentName);
+ return segmentName;
+ }
+
+ private static String getSegmentNameFromMetadataFile(File
segmentMetadataFile) {
+ String fileName = segmentMetadataFile.getName();
+
Preconditions.checkArgument(fileName.endsWith(Constants.METADATA_TAR_GZ_FILE_EXT),
+ "Segment metadata file: %s must be named <segmentName>%s", fileName,
Constants.METADATA_TAR_GZ_FILE_EXT);
+ String segmentName = fileName.substring(0, fileName.length() -
Constants.METADATA_TAR_GZ_FILE_EXT.length());
+ SegmentNameUtils.validatePartialOrFullSegmentName(segmentName);
+ return segmentName;
+ }
+
+ public static void sendSegmentsUriAndMetadata(SegmentGenerationJobSpec spec,
PinotFS fileSystem,
+ Map<String, String> segmentUriToTarPathMap, List<Header> headers,
List<NameValuePair> parameters)
+ throws Exception {
LOGGER.info("Start pushing segment metadata: {} to locations: {} for
table: {} with parallelism: {}",
- segmentUriToTarPathMap, Arrays.toString(spec.getPinotClusterSpecs()),
tableName,
+ segmentUriToTarPathMap, Arrays.toString(spec.getPinotClusterSpecs()),
spec.getTableSpec().getTableName(),
spec.getPushJobSpec().getPushParallelism());
-
+ ConcurrentHashMap<String, File> segmentMetadataFileMap = new
ConcurrentHashMap<>();
+ ConcurrentLinkedQueue<String> segmentURIs = new ConcurrentLinkedQueue<>();
+ ExecutorService executor =
+
Executors.newFixedThreadPool(spec.getPushJobSpec().getSegmentMetadataGenerationParallelism());
try {
generateSegmentMetadataFiles(spec, fileSystem, segmentUriToTarPathMap,
segmentMetadataFileMap, segmentURIs,
executor);
- allSegmentsMetadataTarFile = createSegmentsMetadataTarFile(segmentURIs,
segmentMetadataFileMap);
- // the key is unused in batch upload mode and hence 'noopKey'
- allSegmentsMetadataMap.put("noopKey", allSegmentsMetadataTarFile);
+ pushSegmentsMetadata(spec, segmentURIs, segmentMetadataFileMap, headers,
parameters);
+ } finally {
+ for (File segmentMetadataFile : segmentMetadataFileMap.values()) {
+ FileUtils.deleteQuietly(segmentMetadataFile);
+ }
+ executor.shutdown();
+ }
+ }
- // perform metadata push in batch mode for every cluster
+ /// Batch variant of [#sendSegmentUriAndMetadata(SegmentGenerationJobSpec,
Map, List, List)] for locally held
+ /// metadata tars named `<segmentName>.metadata.tar.gz`. The caller owns the
files.
+ public static void sendSegmentsUriAndMetadata(SegmentGenerationJobSpec spec,
+ Map<String, File> segmentUriToMetadataFileMap, List<Header> headers,
List<NameValuePair> parameters)
+ throws Exception {
+ Map<String, File> segmentMetadataFileMap = new HashMap<>();
+ List<String> segmentURIs = new ArrayList<>();
+ for (Map.Entry<String, File> entry :
segmentUriToMetadataFileMap.entrySet()) {
+ String segmentName = getSegmentNameFromMetadataFile(entry.getValue());
+ segmentMetadataFileMap.put(segmentName, entry.getValue());
+ segmentURIs.add(segmentName);
+ segmentURIs.add(entry.getKey());
+ }
+ pushSegmentsMetadata(spec, segmentURIs, segmentMetadataFileMap, headers,
parameters);
+ }
+
+ private static void pushSegmentsMetadata(SegmentGenerationJobSpec spec,
Collection<String> segmentURIs,
+ Map<String, File> segmentMetadataFileMap, List<Header> headers,
List<NameValuePair> parameters)
+ throws Exception {
+ String tableName = spec.getTableSpec().getTableName();
+ FileUploadDownloadClient fileUploadDownloadClient =
getOrCreateFileUploadDownloadClient(spec);
+ int socketTimeoutMs = getSocketTimeoutMs(spec);
+ File allSegmentsMetadataTarFile =
createSegmentsMetadataTarFile(segmentURIs, segmentMetadataFileMap);
+ // the key is unused in batch upload mode and hence 'noopKey'
+ Map<String, File> allSegmentsMetadataMap = Map.of("noopKey",
allSegmentsMetadataTarFile);
+ try {
for (PinotClusterSpec pinotClusterSpec : spec.getPinotClusterSpecs()) {
URI controllerURI;
try {
@@ -500,14 +572,8 @@ public class SegmentPushUtils implements Serializable {
});
}
} finally {
- for (Map.Entry<String, File> metadataFileEntry :
segmentMetadataFileMap.entrySet()) {
- FileUtils.deleteQuietly(metadataFileEntry.getValue());
- }
- if (allSegmentsMetadataTarFile != null) {
- FileUtils.deleteQuietly(allSegmentsMetadataTarFile);
- }
+ FileUtils.deleteQuietly(allSegmentsMetadataTarFile);
closeFileUploadDownloadClient(spec, fileUploadDownloadClient);
- executor.shutdown();
}
}
@@ -522,30 +588,8 @@ public class SegmentPushUtils implements Serializable {
futures.add(
executor.submit(() -> {
String tarFilePath = segmentUriToTarPathMap.get(segmentUriPath);
- String fileName = new File(tarFilePath).getName();
- // segments stored in Pinot deep store do not have .tar.gz
extension
- String segmentName = fileName.endsWith(Constants.TAR_GZ_FILE_EXT)
- ? fileName.substring(0, fileName.length() -
Constants.TAR_GZ_FILE_EXT.length()) : fileName;
- SegmentNameUtils.validatePartialOrFullSegmentName(segmentName);
- File segmentMetadataFile;
- // Check if there is a segment metadata tar gz file named
`segmentName.metadata.tar.gz`, already in the
- // remote directory. This is to avoid generating a new segment
metadata tar gz file every time we push a
- // segment, which requires downloading the entire segment tar gz
file.
-
- URI metadataTarGzFilePath =
generateSegmentMetadataURI(tarFilePath, segmentName);
- LOGGER.info("Checking if metadata tar gz file {} exists",
metadataTarGzFilePath);
- if (spec.getPushJobSpec().isPreferMetadataTarGz() &&
fileSystem.exists(metadataTarGzFilePath)) {
- segmentMetadataFile = new File(FileUtils.getTempDirectory(),
- SegmentUploadConstants.SEGMENT_METADATA_DIR_PREFIX +
UUID.randomUUID()
- + TarCompressionUtils.TAR_GZ_FILE_EXTENSION);
- if (segmentMetadataFile.exists()) {
- FileUtils.forceDelete(segmentMetadataFile);
- }
- fileSystem.copyToLocalFile(metadataTarGzFilePath,
segmentMetadataFile);
- } else {
- segmentMetadataFile = generateSegmentMetadataFile(fileSystem,
URI.create(tarFilePath));
- }
- segmentMetadataFileMap.put(segmentName, segmentMetadataFile);
+ String segmentName = getSegmentName(tarFilePath);
+ segmentMetadataFileMap.put(segmentName,
getSegmentMetadataFile(spec, fileSystem, tarFilePath, segmentName));
segmentURIs.add(segmentName);
segmentURIs.add(segmentUriPath);
return null;
@@ -707,6 +751,23 @@ public class SegmentPushUtils implements Serializable {
}
}
+ /// Builds the metadata-only tar a METADATA push sends
(`metadata.properties` and `creation.meta`) from a local
+ /// segment directory, named `<segmentName>.metadata.tar.gz`, so the segment
tar never has to be downloaded back.
+ public static File generateSegmentMetadataFile(File segmentDir, File
outputDir, String segmentName)
+ throws IOException {
+ File metadataDir = new File(outputDir, segmentName + "-metadata");
+ File metadataTarFile = new File(outputDir, segmentName +
Constants.METADATA_TAR_GZ_FILE_EXT);
+ try {
+ FileUtils.forceMkdir(metadataDir);
+
FileUtils.copyFileToDirectory(SegmentDirectoryPaths.findMetadataFile(segmentDir),
metadataDir);
+
FileUtils.copyFileToDirectory(SegmentDirectoryPaths.findCreationMetaFile(segmentDir),
metadataDir);
+ TarCompressionUtils.createCompressedTarFile(metadataDir,
metadataTarFile);
+ return metadataTarFile;
+ } finally {
+ FileUtils.deleteQuietly(metadataDir);
+ }
+ }
+
public static URI generateSegmentMetadataURI(String segmentTarPath, String
segmentName)
throws URISyntaxException {
URI segmentTarURI = URI.create(segmentTarPath);
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 0bf6005b8c3..a1763c47252 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
@@ -34,13 +34,17 @@ import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.SecureRandom;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -58,6 +62,7 @@ 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.PinotFS;
+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;
import org.apache.pinot.spi.ingestion.batch.spec.SegmentGenerationJobSpec;
@@ -375,6 +380,113 @@ public class SegmentPushUtilsTest {
}
}
+ @Test
+ public void testGenerateSegmentMetadataFileFromLocalSegment()
+ throws Exception {
+ // A v3 segment layout with a data file that must not be included
+ File segmentDir = new File(_tempDir, TEST_SEGMENT_NAME);
+ File v3Dir = new File(segmentDir, "v3");
+ FileUtils.forceMkdir(v3Dir);
+ FileUtils.writeStringToFile(new File(v3Dir, "metadata.properties"),
"segment.name = " + TEST_SEGMENT_NAME,
+ StandardCharsets.UTF_8);
+ FileUtils.writeStringToFile(new File(v3Dir, "creation.meta"), "crc",
StandardCharsets.UTF_8);
+ FileUtils.writeStringToFile(new File(v3Dir, "columns.psf"), "data",
StandardCharsets.UTF_8);
+
+ File metadataTar =
SegmentPushUtils.generateSegmentMetadataFile(segmentDir, _tempDir,
TEST_SEGMENT_NAME);
+
+ assertEquals(metadataTar.getName(), TEST_SEGMENT_NAME +
Constants.METADATA_TAR_GZ_FILE_EXT);
+ 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);
+ }
+
+ @Test
+ public void testSendSegmentUriAndMetadataWithLocalMetadataFiles()
+ throws Exception {
+ List<Headers> requests = new CopyOnWriteArrayList<>();
+ HttpsServer httpsServer = createHttpsServer("/v2/segments", new
RecordingHandler(requests));
+ try {
+ URI controllerUri = new URI("https://localhost:" +
httpsServer.getAddress().getPort());
+ SegmentGenerationJobSpec spec =
createSegmentGenerationJobSpec(controllerUri, createTlsSpec());
+ spec.getPushJobSpec().setCopyToDeepStoreForMetadataPush(true);
+ File metadataTar = createMetadataTar(TEST_SEGMENT_NAME);
+
+ SegmentPushUtils.sendSegmentUriAndMetadata(spec,
Map.of(TEST_SEGMENT_URI, metadataTar), new ArrayList<>(),
+ new ArrayList<>());
+
+ assertEquals(requests.size(), 1);
+ Headers headers = requests.get(0);
+
assertEquals(headers.getFirst(FileUploadDownloadClient.CustomHeaders.UPLOAD_TYPE),
+ FileUploadDownloadClient.FileUploadType.METADATA.toString());
+
assertEquals(headers.getFirst(FileUploadDownloadClient.CustomHeaders.DOWNLOAD_URI),
TEST_SEGMENT_URI);
+
assertEquals(headers.getFirst(FileUploadDownloadClient.CustomHeaders.COPY_SEGMENT_TO_DEEP_STORE),
"true");
+ // The caller owns the metadata file
+ assertTrue(metadataTar.isFile());
+ } finally {
+ httpsServer.stop(0);
+ }
+ }
+
+ @Test
+ public void testSendSegmentsUriAndMetadataWithLocalMetadataFiles()
+ throws Exception {
+ List<Headers> requests = new CopyOnWriteArrayList<>();
+ HttpsServer httpsServer = createHttpsServer("/segments/batchUpload", new
RecordingHandler(requests));
+ try {
+ URI controllerUri = new URI("https://localhost:" +
httpsServer.getAddress().getPort());
+ SegmentGenerationJobSpec spec =
createSegmentGenerationJobSpec(controllerUri, createTlsSpec());
+ Map<String, File> segmentUriToMetadataFileMap = new HashMap<>();
+ segmentUriToMetadataFileMap.put("file:///tmp/segment1.tar.gz",
createMetadataTar("segment1"));
+ segmentUriToMetadataFileMap.put("file:///tmp/segment2.tar.gz",
createMetadataTar("segment2"));
+
+ SegmentPushUtils.sendSegmentsUriAndMetadata(spec,
segmentUriToMetadataFileMap, new ArrayList<>(),
+ new ArrayList<>());
+
+ // One batch request for both segments, and the caller still owns the
metadata files
+ assertEquals(requests.size(), 1);
+
assertEquals(requests.get(0).getFirst(FileUploadDownloadClient.CustomHeaders.UPLOAD_TYPE),
+ FileUploadDownloadClient.FileUploadType.METADATA.toString());
+ segmentUriToMetadataFileMap.values().forEach(file ->
assertTrue(file.isFile()));
+ } finally {
+ httpsServer.stop(0);
+ }
+ }
+
+ @Test
+ public void testSendSegmentUriAndMetadataRejectsUnnamedMetadataFile() {
+ SegmentGenerationJobSpec spec =
createSegmentGenerationJobSpec(URI.create("https://localhost:1"), null);
+ File notAMetadataTar = new File(_tempDir, "segment.tar.gz");
+ assertThrows(IllegalArgumentException.class, () ->
SegmentPushUtils.sendSegmentUriAndMetadata(spec,
+ Map.of(TEST_SEGMENT_URI, notAMetadataTar), new ArrayList<>(), new
ArrayList<>()));
+ }
+
+ private File createMetadataTar(String segmentName)
+ throws IOException {
+ File segmentDir = new File(_tempDir, segmentName);
+ FileUtils.forceMkdir(segmentDir);
+ FileUtils.touch(new File(segmentDir, "metadata.properties"));
+ FileUtils.touch(new File(segmentDir, "creation.meta"));
+ return SegmentPushUtils.generateSegmentMetadataFile(segmentDir, _tempDir,
segmentName);
+ }
+
+ /// Records request headers and answers OK, for both single and batch
metadata pushes.
+ private static class RecordingHandler implements HttpHandler {
+ private final List<Headers> _requests;
+
+ private RecordingHandler(List<Headers> requests) {
+ _requests = requests;
+ }
+
+ @Override
+ public void handle(HttpExchange httpExchange)
+ throws IOException {
+ _requests.add(httpExchange.getRequestHeaders());
+ httpExchange.getRequestBody().readAllBytes();
+ writeResponse(httpExchange, OK_RESPONSE);
+ }
+ }
+
@Test
public void testGetOrCreateFileUploadDownloadClientUsesSharedDefaultClient()
throws Exception {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]