This is an automated email from the ASF dual-hosted git repository.
shauryachats 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 1f1521b8389 Add peer download fallback to PredownloadScheduler (#18641)
1f1521b8389 is described below
commit 1f1521b83890932bd05b49feb2fdbdae631c2ded
Author: Pradeep Singh Negi <[email protected]>
AuthorDate: Tue Jul 7 12:34:37 2026 +0530
Add peer download fallback to PredownloadScheduler (#18641)
* Add peer download fallback to PredownloadScheduler
When a segment download from deep storage fails in the predownload
container, fall back to downloading from peer servers if peer download
is enabled. This improves predownload reliability when deep storage is
temporarily unavailable or experiencing issues.
Changes:
- Accept peerDownloadEnabled flag in PredownloadScheduler constructor
- Extract deep store download logic into downloadFromDeepStore()
- Add downloadFromPeers() method that discovers ONLINE peer servers
via ExternalView and downloads the segment from a shuffled peer list
- Add getPeerServerURIs() to PredownloadZKClient to discover peers
without requiring HelixManager
- Add tests for peer download fallback and ZK peer discovery
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* Read peerDownloadEnabled from properties instead of constructor parameter
The upstream StartServer now sets pinot.server.peer.download.enabled
in properties, so the constructor no longer needs the boolean parameter.
This simplifies the API and keeps configuration in one place.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* Address review comments: sanitize peer download scheme and fix Javadoc
1. Sanitize _peerDownloadScheme similar to BaseTableDataManager:
lowercase the value and validate it's http or https.
2. Remove "excluding the current instance" from getPeerServerURIs
Javadoc — predownload runs before server starts, so the current
instance is never in ExternalView.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* Add peer download and deep store metrics for predownload
- PREDOWNLOAD_DEEPSTORE_DOWNLOAD_COUNT: deep store success counter
- PREDOWNLOAD_PEER_SEGMENT_DOWNLOAD_COUNT: peer download success counter
- PREDOWNLOAD_PEER_SEGMENT_DOWNLOAD_FAILURE_COUNT: peer download failure
counter with segment name as key for per-segment tracking
- PEER_DOWNLOAD_SPEED: gauge for peer download speed (MB/s)
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* Add tests for peer download and deep store metrics
- testDeepStoreSuccessEmitsDeepStoreMetric: verifies
deepStoreSegmentDownloaded()
is called on deep store success, peer metrics not called
- testPeerDownloadSuccessEmitsPeerMetrics: verifies
peerSegmentDownloaded(true)
and segmentDownloaded(true) both called on peer fallback success
- testPeerDownloadFailureEmitsSegmentLevelMetric: verifies
peerSegmentDownloaded(false, segmentName) called with segment name on
failure
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* Add streaming-untar support for peer download in predownload
When streaming download-untar is enabled and the segment is not encrypted,
peer download now uses the streaming path (fetchAndStreamUntarToLocal)
instead of download-then-untar, matching the existing deep store behavior.
Framework changes:
- Add fetchUntarSegmentToLocalStreamed(segmentName, uriSupplier, dest,
rateLimit)
to SegmentFetcher interface, BaseSegmentFetcher (with retry), and
HttpSegmentFetcher
- Add corresponding SegmentFetcherFactory.fetchAndStreamUntarToLocal
overload
Tests added for HttpSegmentFetcher, SegmentFetcherFactory, and
PredownloadScheduler streaming peer fallback path.
* Address review comments: metric naming, config constant, guard refactor
- Rename PEER_DOWNLOAD_SPEED to PEER_DOWNLOAD_SPEED_MBPS with unit "mbps"
- Remove segmentName from failure metric to avoid high cardinality
- Move hardcoded "pinot.server.peer.download.enabled" to
CommonConstants.Server.CONFIG_OF_PEER_DOWNLOAD_ENABLED
- Move peer download enabled/scheme guard into downloadFromPeers
- On peer download failure, throw the original deep store exception
---------
Co-authored-by: psinghnegi <[email protected]>
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
---
.../apache/pinot/common/metrics/ServerGauge.java | 1 +
.../apache/pinot/common/metrics/ServerMeter.java | 3 +
.../common/utils/fetcher/BaseSegmentFetcher.java | 39 ++
.../common/utils/fetcher/HttpSegmentFetcher.java | 16 +
.../pinot/common/utils/fetcher/SegmentFetcher.java | 6 +
.../utils/fetcher/SegmentFetcherFactory.java | 10 +
.../utils/fetcher/HttpSegmentFetcherTest.java | 52 +++
.../utils/fetcher/SegmentFetcherFactoryTest.java | 16 +
.../server/predownload/PredownloadMetrics.java | 14 +
.../server/predownload/PredownloadScheduler.java | 110 +++++-
.../server/predownload/PredownloadZKClient.java | 47 +++
.../server/predownload/PredownloadMetricsTest.java | 64 +++
.../predownload/PredownloadSchedulerTest.java | 430 ++++++++++++++++++++-
.../predownload/PredownloadZKClientTest.java | 72 ++++
.../apache/pinot/spi/utils/CommonConstants.java | 2 +
15 files changed, 865 insertions(+), 17 deletions(-)
diff --git
a/pinot-common/src/main/java/org/apache/pinot/common/metrics/ServerGauge.java
b/pinot-common/src/main/java/org/apache/pinot/common/metrics/ServerGauge.java
index ac29c9541bd..0a909ce4341 100644
---
a/pinot-common/src/main/java/org/apache/pinot/common/metrics/ServerGauge.java
+++
b/pinot-common/src/main/java/org/apache/pinot/common/metrics/ServerGauge.java
@@ -106,6 +106,7 @@ public enum ServerGauge implements AbstractMetrics.Gauge {
REALTIME_CONSUMER_DIR_USAGE("bytes", true),
SEGMENT_DOWNLOAD_SPEED("bytes", true),
PREDOWNLOAD_SPEED("bytes", true),
+ PEER_DOWNLOAD_SPEED_MBPS("mbps", true),
ZK_JUTE_MAX_BUFFER("zkJuteMaxBuffer", true),
// gRPC Netty buffer metrics
diff --git
a/pinot-common/src/main/java/org/apache/pinot/common/metrics/ServerMeter.java
b/pinot-common/src/main/java/org/apache/pinot/common/metrics/ServerMeter.java
index c5dc9855006..81ff8c3df82 100644
---
a/pinot-common/src/main/java/org/apache/pinot/common/metrics/ServerMeter.java
+++
b/pinot-common/src/main/java/org/apache/pinot/common/metrics/ServerMeter.java
@@ -222,6 +222,9 @@ public enum ServerMeter implements AbstractMetrics.Meter {
PREDOWNLOAD_SEGMENT_DOWNLOAD_FAILURE_COUNT("predownloadSegmentFailureCount",
true),
PREDOWNLOAD_SUCCEED("predownloadSucceed", true),
PREDOWNLOAD_FAILED("predownloadFailed", true),
+ PREDOWNLOAD_PEER_SEGMENT_DOWNLOAD_COUNT("predownloadPeerSegmentCount", true),
+
PREDOWNLOAD_PEER_SEGMENT_DOWNLOAD_FAILURE_COUNT("predownloadPeerSegmentFailureCount",
false),
+ PREDOWNLOAD_DEEPSTORE_DOWNLOAD_COUNT("predownloadDeepstoreDownloadCount",
true),
// reingestion metrics
SEGMENT_REINGESTION_FAILURE("segments", false),
diff --git
a/pinot-common/src/main/java/org/apache/pinot/common/utils/fetcher/BaseSegmentFetcher.java
b/pinot-common/src/main/java/org/apache/pinot/common/utils/fetcher/BaseSegmentFetcher.java
index 5fb82388f2b..0cdcc670e00 100644
---
a/pinot-common/src/main/java/org/apache/pinot/common/utils/fetcher/BaseSegmentFetcher.java
+++
b/pinot-common/src/main/java/org/apache/pinot/common/utils/fetcher/BaseSegmentFetcher.java
@@ -22,6 +22,7 @@ import java.io.File;
import java.net.URI;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import org.apache.pinot.common.auth.AuthProviderUtils;
import org.apache.pinot.common.utils.RoundRobinURIProvider;
@@ -151,4 +152,42 @@ public abstract class BaseSegmentFetcher implements
SegmentFetcher {
throws Exception {
throw new UnsupportedOperationException();
}
+
+ @Override
+ public File fetchUntarSegmentToLocalStreamed(String segmentName,
Supplier<List<URI>> uriSupplier, File dest,
+ long maxStreamRateInByte) throws Exception {
+ AtomicReference<File> ret = new AtomicReference<>();
+ try {
+ int attempt =
+ RetryPolicies.exponentialBackoffRetryPolicy(_retryCount,
_retryWaitMs, _retryDelayScaleFactor).attempt(() -> {
+ List<URI> suppliedURIs = uriSupplier.get();
+ // Go through the list of URIs to fetch and untar the segment
until success.
+ for (URI uri : suppliedURIs) {
+ try {
+ ret.set(fetchUntarSegmentToLocalWithoutRetry(uri, dest,
maxStreamRateInByte));
+ return true;
+ } catch (Exception e) {
+ _logger.warn("Stream download-untar segment {} from peer {}
failed.", segmentName, uri, e);
+ }
+ }
+ // None of the URI works. Return false for retry.
+ return false;
+ });
+ _logger.info("Stream downloaded and untarred segment {} successfully
with {} attempts.", segmentName,
+ attempt + 1);
+ return ret.get();
+ } catch (Exception e) {
+ _logger.error("Failed to stream download-untar segment {} after
retries.", segmentName, e);
+ throw e;
+ }
+ }
+
+ /**
+ * Fetches a segment from URI location and untars it to local in a streamed
manner, without retry. Sub-class
+ * should override this to support {@link
#fetchUntarSegmentToLocalStreamed(String, Supplier, File, long)}.
+ */
+ protected File fetchUntarSegmentToLocalWithoutRetry(URI uri, File dest, long
maxStreamRateInByte)
+ throws Exception {
+ throw new UnsupportedOperationException();
+ }
}
diff --git
a/pinot-common/src/main/java/org/apache/pinot/common/utils/fetcher/HttpSegmentFetcher.java
b/pinot-common/src/main/java/org/apache/pinot/common/utils/fetcher/HttpSegmentFetcher.java
index 010b43eb777..dbb37455bc7 100644
---
a/pinot-common/src/main/java/org/apache/pinot/common/utils/fetcher/HttpSegmentFetcher.java
+++
b/pinot-common/src/main/java/org/apache/pinot/common/utils/fetcher/HttpSegmentFetcher.java
@@ -200,6 +200,22 @@ public class HttpSegmentFetcher extends BaseSegmentFetcher
{
return ret.get();
}
+ @Override
+ protected File fetchUntarSegmentToLocalWithoutRetry(URI uri, File dest, long
maxStreamRateInByte)
+ throws Exception {
+ String hostName = uri.getHost();
+ int port = uri.getPort();
+ // If the original download address is specified as host name, need add a
"HOST" HTTP header to the HTTP
+ // request. Otherwise, if the download address is a LB address, when the
LB be configured as "disallow direct
+ // access by IP address", downloading will fail.
+ List<Header> httpHeaders = new LinkedList<>();
+ if (!InetAddresses.isInetAddress(hostName)) {
+ httpHeaders.add(new BasicHeader(HttpHeaders.HOST, hostName + ":" +
port));
+ }
+ return _httpClient.downloadUntarFileStreamed(uri, dest, _authProvider,
httpHeaders, maxStreamRateInByte,
+ _connectionRequestTimeoutMs, _socketTimeoutMs);
+ }
+
@Override
public void fetchSegmentToLocalWithoutRetry(URI uri, File dest)
throws Exception {
diff --git
a/pinot-common/src/main/java/org/apache/pinot/common/utils/fetcher/SegmentFetcher.java
b/pinot-common/src/main/java/org/apache/pinot/common/utils/fetcher/SegmentFetcher.java
index 96961e4a089..d17086b6f84 100644
---
a/pinot-common/src/main/java/org/apache/pinot/common/utils/fetcher/SegmentFetcher.java
+++
b/pinot-common/src/main/java/org/apache/pinot/common/utils/fetcher/SegmentFetcher.java
@@ -58,4 +58,10 @@ public interface SegmentFetcher {
* @throws Exception when the segment fetch fails after all attempts are
exhausted or other runtime exceptions occur.
*/
void fetchSegmentToLocal(String segmentName, Supplier<List<URI>>
uriSupplier, File dest) throws Exception;
+
+ /**
+ * Fetches a segment from any uri in the given supplier's list, and untars
it to local in a streamed manner.
+ */
+ File fetchUntarSegmentToLocalStreamed(String segmentName,
Supplier<List<URI>> uriSupplier, File dest,
+ long maxStreamRateInByte) throws Exception;
}
diff --git
a/pinot-common/src/main/java/org/apache/pinot/common/utils/fetcher/SegmentFetcherFactory.java
b/pinot-common/src/main/java/org/apache/pinot/common/utils/fetcher/SegmentFetcherFactory.java
index de93adfe38c..ee6f89cac93 100644
---
a/pinot-common/src/main/java/org/apache/pinot/common/utils/fetcher/SegmentFetcherFactory.java
+++
b/pinot-common/src/main/java/org/apache/pinot/common/utils/fetcher/SegmentFetcherFactory.java
@@ -196,4 +196,14 @@ public class SegmentFetcherFactory {
crypter.decrypt(tempDownloadedFile, dest);
}
}
+
+ /**
+ * Fetches a segment from any uri in the given supplier's list, and untars
it to local in a streamed manner.
+ */
+ public static File fetchAndStreamUntarToLocal(String segmentName, String
scheme, Supplier<List<URI>> uriSupplier,
+ File dest, long maxStreamRateInByte)
+ throws Exception {
+ return
getSegmentFetcher(scheme).fetchUntarSegmentToLocalStreamed(segmentName,
uriSupplier, dest,
+ maxStreamRateInByte);
+ }
}
diff --git
a/pinot-common/src/test/java/org/apache/pinot/common/utils/fetcher/HttpSegmentFetcherTest.java
b/pinot-common/src/test/java/org/apache/pinot/common/utils/fetcher/HttpSegmentFetcherTest.java
index c83036a6332..2819ad542e2 100644
---
a/pinot-common/src/test/java/org/apache/pinot/common/utils/fetcher/HttpSegmentFetcherTest.java
+++
b/pinot-common/src/test/java/org/apache/pinot/common/utils/fetcher/HttpSegmentFetcherTest.java
@@ -19,6 +19,7 @@
package org.apache.pinot.common.utils.fetcher;
import java.io.File;
+import java.io.IOException;
import java.net.URI;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
@@ -33,6 +34,8 @@ import org.testng.annotations.Test;
import static org.apache.pinot.common.utils.fetcher.HttpSegmentFetcher.*;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -126,4 +129,53 @@ public class HttpSegmentFetcherTest {
List<URI> uris = List.of();
segmentFetcher.fetchSegmentToLocal(SEGMENT_NAME, () -> uris, SEGMENT_FILE);
}
+
+ @Test
+ public void testFetchUntarSegmentToLocalStreamedSucceedAtFirstAttempt()
+ throws Exception {
+ FileUploadDownloadClient client = mock(FileUploadDownloadClient.class);
+ when(client.downloadUntarFileStreamed(any(), any(), any(), any(),
anyLong(), anyInt(), anyInt())).thenReturn(
+ SEGMENT_FILE);
+ HttpSegmentFetcher segmentFetcher = getSegmentFetcher(client);
+ List<URI> uris = List.of(new URI("http://h1:8080"), new
URI("http://h2:8080"));
+ File result =
segmentFetcher.fetchUntarSegmentToLocalStreamed(SEGMENT_NAME, () -> uris,
SEGMENT_FILE, -1);
+ Assert.assertEquals(result, SEGMENT_FILE);
+ }
+
+ @Test(expectedExceptions = AttemptsExceededException.class)
+ public void testFetchUntarSegmentToLocalStreamedAllDownloadAttemptsFailed()
+ throws Exception {
+ FileUploadDownloadClient client = mock(FileUploadDownloadClient.class);
+ // All attempts failed
+ when(client.downloadUntarFileStreamed(any(), any(), any(), any(),
anyLong(), anyInt(), anyInt())).thenThrow(
+ new IOException("Failed to download"));
+ HttpSegmentFetcher segmentFetcher = getSegmentFetcher(client);
+ List<URI> uris = List.of(new URI("http://h1:8080"), new
URI("http://h2:8080"));
+ segmentFetcher.fetchUntarSegmentToLocalStreamed(SEGMENT_NAME, () -> uris,
SEGMENT_FILE, -1);
+ }
+
+ @Test
+ public void testFetchUntarSegmentToLocalStreamedSuccessAfterRetry()
+ throws Exception {
+ FileUploadDownloadClient client = mock(FileUploadDownloadClient.class);
+ // The first two attempts failed and the last attempt succeeded
+ when(client.downloadUntarFileStreamed(any(), any(), any(), any(),
anyLong(), anyInt(), anyInt())).thenThrow(
+ new IOException("Failed to download")).thenThrow(new
IOException("Failed to download"))
+ .thenReturn(SEGMENT_FILE);
+ HttpSegmentFetcher segmentFetcher = getSegmentFetcher(client);
+ List<URI> uris = List.of(new URI("http://h1:8080"), new
URI("http://h2:8080"));
+ File result =
segmentFetcher.fetchUntarSegmentToLocalStreamed(SEGMENT_NAME, () -> uris,
SEGMENT_FILE, -1);
+ Assert.assertEquals(result, SEGMENT_FILE);
+ }
+
+ @Test(expectedExceptions = AttemptsExceededException.class)
+ public void testFetchUntarSegmentToLocalStreamedFailureWithNoPeerServers()
+ throws Exception {
+ FileUploadDownloadClient client = mock(FileUploadDownloadClient.class);
+ when(client.downloadUntarFileStreamed(any(), any(), any(), any(),
anyLong(), anyInt(), anyInt())).thenReturn(
+ SEGMENT_FILE);
+ HttpSegmentFetcher segmentFetcher = getSegmentFetcher(client);
+ List<URI> uris = List.of();
+ segmentFetcher.fetchUntarSegmentToLocalStreamed(SEGMENT_NAME, () -> uris,
SEGMENT_FILE, -1);
+ }
}
diff --git
a/pinot-common/src/test/java/org/apache/pinot/common/utils/fetcher/SegmentFetcherFactoryTest.java
b/pinot-common/src/test/java/org/apache/pinot/common/utils/fetcher/SegmentFetcherFactoryTest.java
index 8e6576f0ab7..96bed9e751c 100644
---
a/pinot-common/src/test/java/org/apache/pinot/common/utils/fetcher/SegmentFetcherFactoryTest.java
+++
b/pinot-common/src/test/java/org/apache/pinot/common/utils/fetcher/SegmentFetcherFactoryTest.java
@@ -40,6 +40,7 @@ public class SegmentFetcherFactoryTest {
private static final String FILE_PROTOCOL = "file";
private static final String TEST_PROTOCOL = "test";
private static final String TEST_URI = "test://foo/bar";
+ private static final String SEGMENT_NAME = "testSegment";
@Test
public void testDefaultSegmentFetcherFactory() {
@@ -98,6 +99,12 @@ public class SegmentFetcherFactoryTest {
assertEquals(fakeCrypter._encryptCalled, 0);
assertEquals(fakeCrypter._originalPath, "foo/bar.enc");
assertEquals(fakeCrypter._decryptedPath, "foo/bar");
+
+ List<URI> streamedUris = List.of(new URI(TEST_URI));
+ File untaredSegDir =
SegmentFetcherFactory.fetchAndStreamUntarToLocal(SEGMENT_NAME, TEST_PROTOCOL,
+ () -> streamedUris, new File("foo/bar"), -1);
+ assertEquals(untaredSegDir, new File("fakeSegmentIndexFile"));
+ assertEquals(testFileFetcher._fetchFileToLocalCalled, 5);
}
public static class FakeSegmentFetcher implements SegmentFetcher {
@@ -135,6 +142,15 @@ public class SegmentFetcherFactoryTest {
throws Exception {
throw new UnsupportedOperationException();
}
+
+ @Override
+ public File fetchUntarSegmentToLocalStreamed(String segmentName,
Supplier<List<URI>> uriSupplier, File dest,
+ long maxStreamRateInByte)
+ throws Exception {
+ assertEquals(uriSupplier.get(), List.of(new URI(TEST_URI)));
+ _fetchFileToLocalCalled++;
+ return new File("fakeSegmentIndexFile");
+ }
}
public static class FakePinotCrypter implements PinotCrypter {
diff --git
a/pinot-server/src/main/java/org/apache/pinot/server/predownload/PredownloadMetrics.java
b/pinot-server/src/main/java/org/apache/pinot/server/predownload/PredownloadMetrics.java
index 7fb26f79038..353f22e24ce 100644
---
a/pinot-server/src/main/java/org/apache/pinot/server/predownload/PredownloadMetrics.java
+++
b/pinot-server/src/main/java/org/apache/pinot/server/predownload/PredownloadMetrics.java
@@ -47,6 +47,20 @@ public class PredownloadMetrics {
}
}
+ public void peerSegmentDownloaded(boolean succeed, String segmentName, long
segmentSizeBytes, long downloadTimeMs) {
+ if (succeed) {
+
_serverMetrics.addMeteredGlobalValue(ServerMeter.PREDOWNLOAD_PEER_SEGMENT_DOWNLOAD_COUNT,
1);
+
_serverMetrics.setValueOfGlobalGauge(ServerGauge.PEER_DOWNLOAD_SPEED_MBPS,
+ (segmentSizeBytes / BYTES_TO_MB) / (downloadTimeMs / 1000 + 1));
+ } else {
+
_serverMetrics.addMeteredGlobalValue(ServerMeter.PREDOWNLOAD_PEER_SEGMENT_DOWNLOAD_FAILURE_COUNT,
1);
+ }
+ }
+
+ public void deepStoreSegmentDownloaded() {
+
_serverMetrics.addMeteredGlobalValue(ServerMeter.PREDOWNLOAD_DEEPSTORE_DOWNLOAD_COUNT,
1);
+ }
+
public void preDownloadSucceed(long totalSegmentSizeBytes, long
totalDownloadTimeMs) {
_serverMetrics.setValueOfGlobalGauge(ServerGauge.PREDOWNLOAD_SPEED,
(totalSegmentSizeBytes / BYTES_TO_MB) / (totalDownloadTimeMs / 1000 +
1));
diff --git
a/pinot-server/src/main/java/org/apache/pinot/server/predownload/PredownloadScheduler.java
b/pinot-server/src/main/java/org/apache/pinot/server/predownload/PredownloadScheduler.java
index 558f055f152..89fa99effd7 100644
---
a/pinot-server/src/main/java/org/apache/pinot/server/predownload/PredownloadScheduler.java
+++
b/pinot-server/src/main/java/org/apache/pinot/server/predownload/PredownloadScheduler.java
@@ -19,9 +19,12 @@
package org.apache.pinot.server.predownload;
import com.google.common.annotations.VisibleForTesting;
+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;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -36,6 +39,7 @@ import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Supplier;
import org.apache.commons.configuration2.PropertiesConfiguration;
import org.apache.commons.io.FileUtils;
import org.apache.pinot.common.utils.TarCompressionUtils;
@@ -72,6 +76,8 @@ public class PredownloadScheduler {
@VisibleForTesting
Set<String> _failedSegments;
private PredownloadMetrics _predownloadMetrics;
+ private boolean _peerDownloadEnabled;
+ private String _peerDownloadScheme;
private int _numOfSkippedSegments;
private int _numOfUnableToDownloadSegments;
private int _numOfDownloadSegments;
@@ -107,6 +113,19 @@ public class PredownloadScheduler {
_failedSegments = ConcurrentHashMap.newKeySet();
_executor = Executors.newFixedThreadPool(predownloadParallelism);
LOGGER.info("Created thread pool with num of threads: {}",
predownloadParallelism);
+
+ _peerDownloadEnabled =
properties.getBoolean(CommonConstants.Server.CONFIG_OF_PEER_DOWNLOAD_ENABLED,
+ CommonConstants.Server.DEFAULT_PEER_DOWNLOAD_ENABLED);
+ _peerDownloadScheme =
_instanceDataManagerConfig.getSegmentPeerDownloadScheme();
+ if (_peerDownloadScheme != null) {
+ _peerDownloadScheme = _peerDownloadScheme.toLowerCase();
+ Preconditions.checkState(
+ CommonConstants.HTTP_PROTOCOL.equals(_peerDownloadScheme)
+ || CommonConstants.HTTPS_PROTOCOL.equals(_peerDownloadScheme),
+ "Unsupported peer download scheme: %s", _peerDownloadScheme);
+ }
+ LOGGER.info("Peer download enabled: {}, scheme: {}", _peerDownloadEnabled,
_peerDownloadScheme);
+
_numOfSkippedSegments = 0;
_numOfDownloadSegments = 0;
}
@@ -301,23 +320,24 @@ public class PredownloadScheduler {
throws Exception {
try {
long startTime = System.currentTimeMillis();
- File tempRootDir = getTmpSegmentDataDir(predownloadSegmentInfo);
- if (_instanceDataManagerConfig.isStreamSegmentDownloadUntar()
- && predownloadSegmentInfo.getCrypterName() == null) {
- try {
- // TODO: increase rate limit here
- File untaredSegDir =
downloadAndStreamUntarWithRateLimit(predownloadSegmentInfo, tempRootDir,
-
_instanceDataManagerConfig.getStreamSegmentDownloadUntarRateLimit());
- moveSegment(predownloadSegmentInfo, untaredSegDir);
- } finally {
- FileUtils.deleteQuietly(tempRootDir);
+ try {
+ downloadFromDeepStore(predownloadSegmentInfo);
+ _predownloadMetrics.deepStoreSegmentDownloaded();
+ } catch (Exception deepStoreException) {
+ if (!_peerDownloadEnabled || _peerDownloadScheme == null) {
+ throw deepStoreException;
}
- } else {
+ LOGGER.warn("Deep store download failed for segment: {} of table: {},
falling back to peer download",
+ predownloadSegmentInfo.getSegmentName(),
predownloadSegmentInfo.getTableNameWithType(), deepStoreException);
+ long peerStartTime = System.currentTimeMillis();
try {
- File tarFile = downloadAndDecrypt(predownloadSegmentInfo,
tempRootDir);
- untarAndMoveSegment(predownloadSegmentInfo, tarFile, tempRootDir);
- } finally {
- FileUtils.deleteQuietly(tempRootDir);
+ downloadFromPeers(predownloadSegmentInfo);
+ _predownloadMetrics.peerSegmentDownloaded(true,
predownloadSegmentInfo.getSegmentName(),
+ predownloadSegmentInfo.getLocalSizeBytes(),
+ System.currentTimeMillis() - peerStartTime);
+ } catch (Exception peerEx) {
+ _predownloadMetrics.peerSegmentDownloaded(false,
predownloadSegmentInfo.getSegmentName(), 0, 0);
+ throw deepStoreException;
}
}
_failedSegments.remove(predownloadSegmentInfo.getSegmentName());
@@ -336,6 +356,65 @@ public class PredownloadScheduler {
}
}
+ private void downloadFromDeepStore(PredownloadSegmentInfo
predownloadSegmentInfo)
+ throws Exception {
+ File tempRootDir = getTmpSegmentDataDir(predownloadSegmentInfo);
+ if (_instanceDataManagerConfig.isStreamSegmentDownloadUntar()
+ && predownloadSegmentInfo.getCrypterName() == null) {
+ try {
+ File untaredSegDir =
downloadAndStreamUntarWithRateLimit(predownloadSegmentInfo, tempRootDir,
+
_instanceDataManagerConfig.getStreamSegmentDownloadUntarRateLimit());
+ moveSegment(predownloadSegmentInfo, untaredSegDir);
+ } finally {
+ FileUtils.deleteQuietly(tempRootDir);
+ }
+ } else {
+ try {
+ File tarFile = downloadAndDecrypt(predownloadSegmentInfo, tempRootDir);
+ untarAndMoveSegment(predownloadSegmentInfo, tarFile, tempRootDir);
+ } finally {
+ FileUtils.deleteQuietly(tempRootDir);
+ }
+ }
+ }
+
+ private void downloadFromPeers(PredownloadSegmentInfo predownloadSegmentInfo)
+ throws Exception {
+ if (!_peerDownloadEnabled || _peerDownloadScheme == null) {
+ throw new PredownloadException("Peer download is not enabled or scheme
is not configured");
+ }
+ String segmentName = predownloadSegmentInfo.getSegmentName();
+ String tableNameWithType = predownloadSegmentInfo.getTableNameWithType();
+ LOGGER.info("Downloading segment: {} of table: {} from peers using scheme:
{}", segmentName, tableNameWithType,
+ _peerDownloadScheme);
+ File tempRootDir = getTmpSegmentDataDir(predownloadSegmentInfo);
+ Supplier<List<URI>> uriSupplier = () -> {
+ List<URI> peerURIs =
+
_predownloadZkClient.getPeerServerURIs(_predownloadZkClient.getDataAccessor(),
tableNameWithType,
+ segmentName, _peerDownloadScheme);
+ Collections.shuffle(peerURIs);
+ return peerURIs;
+ };
+ try {
+ if (_instanceDataManagerConfig.isStreamSegmentDownloadUntar()
+ && predownloadSegmentInfo.getCrypterName() == null) {
+ File untaredSegDir =
SegmentFetcherFactory.fetchAndStreamUntarToLocal(segmentName,
_peerDownloadScheme,
+ uriSupplier, tempRootDir,
_instanceDataManagerConfig.getStreamSegmentDownloadUntarRateLimit());
+ LOGGER.info("Downloaded and untarred segment: {} from peers to: {}",
segmentName, untaredSegDir);
+ moveSegment(predownloadSegmentInfo, untaredSegDir);
+ } else {
+ File segmentTarFile = new File(tempRootDir, segmentName +
TarCompressionUtils.TAR_GZ_FILE_EXTENSION);
+ SegmentFetcherFactory.fetchAndDecryptSegmentToLocal(segmentName,
_peerDownloadScheme, uriSupplier,
+ segmentTarFile, predownloadSegmentInfo.getCrypterName());
+ LOGGER.info("Downloaded segment: {} from peers to: {}, file length:
{}", segmentName, segmentTarFile,
+ segmentTarFile.length());
+ untarAndMoveSegment(predownloadSegmentInfo, segmentTarFile,
tempRootDir);
+ }
+ } finally {
+ FileUtils.deleteQuietly(tempRootDir);
+ }
+ }
+
private File getTmpSegmentDataDir(PredownloadSegmentInfo
predownloadSegmentInfo)
throws Exception {
PredownloadTableInfo predownloadTableInfo =
_tableInfoMap.get(predownloadSegmentInfo.getTableNameWithType());
@@ -395,7 +474,6 @@ public class PredownloadScheduler {
} catch (AttemptsExceededException e) {
LOGGER.error("Attempts exceeded when downloading segment: {} for table:
{} from: {} to: {}", segmentName,
tableNameWithType, uri, tarFile);
- // TODO: add download from peer logic
throw e;
}
}
diff --git
a/pinot-server/src/main/java/org/apache/pinot/server/predownload/PredownloadZKClient.java
b/pinot-server/src/main/java/org/apache/pinot/server/predownload/PredownloadZKClient.java
index c504af4a65f..ac1610c2c7d 100644
---
a/pinot-server/src/main/java/org/apache/pinot/server/predownload/PredownloadZKClient.java
+++
b/pinot-server/src/main/java/org/apache/pinot/server/predownload/PredownloadZKClient.java
@@ -18,6 +18,7 @@
*/
package org.apache.pinot.server.predownload;
+import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -28,6 +29,7 @@ import org.apache.helix.PropertyPathBuilder;
import org.apache.helix.manager.zk.ZKHelixDataAccessor;
import org.apache.helix.manager.zk.ZkBaseDataAccessor;
import org.apache.helix.model.CurrentState;
+import org.apache.helix.model.ExternalView;
import org.apache.helix.model.InstanceConfig;
import org.apache.helix.model.LiveInstance;
import org.apache.helix.store.zk.AutoFallbackPropertyStore;
@@ -147,6 +149,51 @@ public class PredownloadZKClient implements AutoCloseable {
return predownloadSegmentInfos;
}
+ /**
+ * Returns URIs of ONLINE peer servers hosting the given segment.
+ * Uses ExternalView to discover peers — mirrors PeerServerSegmentFinder
without requiring HelixManager.
+ */
+ public List<URI> getPeerServerURIs(HelixDataAccessor accessor, String
tableNameWithType, String segmentName,
+ String downloadScheme) {
+ org.apache.helix.PropertyKey.Builder keyBuilder = accessor.keyBuilder();
+ ExternalView externalView =
accessor.getProperty(keyBuilder.externalView(tableNameWithType));
+ if (externalView == null) {
+ LOGGER.warn("No external view for table: {} when finding peers for
segment: {}", tableNameWithType, segmentName);
+ return new ArrayList<>();
+ }
+ Map<String, String> instanceStateMap =
externalView.getStateMap(segmentName);
+ if (instanceStateMap == null) {
+ LOGGER.warn("Segment: {} not found in external view of table: {}",
segmentName, tableNameWithType);
+ return new ArrayList<>();
+ }
+ String adminPortKey = CommonConstants.HTTP_PROTOCOL.equals(downloadScheme)
+ ? CommonConstants.Helix.Instance.ADMIN_PORT_KEY
+ : CommonConstants.Helix.Instance.ADMIN_HTTPS_PORT_KEY;
+ List<URI> peerURIs = new ArrayList<>();
+ for (Map.Entry<String, String> entry : instanceStateMap.entrySet()) {
+ String instanceId = entry.getKey();
+ if
(!CommonConstants.Helix.StateModel.SegmentStateModel.ONLINE.equals(entry.getValue()))
{
+ continue;
+ }
+ InstanceConfig instanceConfig =
accessor.getProperty(keyBuilder.instanceConfig(instanceId));
+ if (instanceConfig == null) {
+ LOGGER.warn("Failed to get instance config for peer: {}", instanceId);
+ continue;
+ }
+ String hostName = instanceConfig.getHostName();
+ int port = instanceConfig.getRecord()
+ .getIntField(adminPortKey,
CommonConstants.Server.DEFAULT_ADMIN_API_PORT);
+ try {
+ peerURIs.add(new URI(
+ String.format("%s://%s:%d/segments/%s/%s",
+ downloadScheme, hostName, port, tableNameWithType,
segmentName)));
+ } catch (Exception e) {
+ LOGGER.warn("Failed to construct peer URI for instance: {}",
instanceId, e);
+ }
+ }
+ return peerURIs;
+ }
+
/**
* Update the segment deepstore metadata from ZK.
*
diff --git
a/pinot-server/src/test/java/org/apache/pinot/server/predownload/PredownloadMetricsTest.java
b/pinot-server/src/test/java/org/apache/pinot/server/predownload/PredownloadMetricsTest.java
new file mode 100644
index 00000000000..36a5ec31936
--- /dev/null
+++
b/pinot-server/src/test/java/org/apache/pinot/server/predownload/PredownloadMetricsTest.java
@@ -0,0 +1,64 @@
+/**
+ * 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.server.predownload;
+
+import org.apache.pinot.common.metrics.ServerGauge;
+import org.apache.pinot.common.metrics.ServerMeter;
+import org.apache.pinot.common.metrics.ServerMetrics;
+import org.mockito.MockedStatic;
+import org.testng.annotations.Test;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+
+public class PredownloadMetricsTest {
+
+ @Test
+ public void testPeerSegmentDownloadedFailureEmitsGlobalMeter() {
+ ServerMetrics mockServerMetrics = mock(ServerMetrics.class);
+ try (MockedStatic<ServerMetrics> serverMetricsMock =
mockStatic(ServerMetrics.class)) {
+ serverMetricsMock.when(ServerMetrics::get).thenReturn(mockServerMetrics);
+ PredownloadMetrics predownloadMetrics = new PredownloadMetrics();
+
+ predownloadMetrics.peerSegmentDownloaded(false, "testSegment", 0, 0);
+
+
verify(mockServerMetrics).addMeteredGlobalValue(ServerMeter.PREDOWNLOAD_PEER_SEGMENT_DOWNLOAD_FAILURE_COUNT,
1);
+ verifyNoMoreInteractions(mockServerMetrics);
+ }
+ }
+
+ @Test
+ public void testPeerSegmentDownloadedSuccessEmitsGlobalMeterAndGauge() {
+ ServerMetrics mockServerMetrics = mock(ServerMetrics.class);
+ try (MockedStatic<ServerMetrics> serverMetricsMock =
mockStatic(ServerMetrics.class)) {
+ serverMetricsMock.when(ServerMetrics::get).thenReturn(mockServerMetrics);
+ PredownloadMetrics predownloadMetrics = new PredownloadMetrics();
+
+ predownloadMetrics.peerSegmentDownloaded(true, "testSegment", 1024 *
1024, 1000);
+
+
verify(mockServerMetrics).addMeteredGlobalValue(ServerMeter.PREDOWNLOAD_PEER_SEGMENT_DOWNLOAD_COUNT,
1);
+
verify(mockServerMetrics).setValueOfGlobalGauge(eq(ServerGauge.PEER_DOWNLOAD_SPEED_MBPS),
eq(0L));
+ verifyNoMoreInteractions(mockServerMetrics);
+ }
+ }
+}
diff --git
a/pinot-server/src/test/java/org/apache/pinot/server/predownload/PredownloadSchedulerTest.java
b/pinot-server/src/test/java/org/apache/pinot/server/predownload/PredownloadSchedulerTest.java
index a5dca3bd8b5..7432681d289 100644
---
a/pinot-server/src/test/java/org/apache/pinot/server/predownload/PredownloadSchedulerTest.java
+++
b/pinot-server/src/test/java/org/apache/pinot/server/predownload/PredownloadSchedulerTest.java
@@ -22,6 +22,7 @@ import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
+import java.lang.reflect.Field;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
@@ -29,6 +30,7 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.atomic.AtomicInteger;
import org.apache.commons.configuration2.PropertiesConfiguration;
import org.apache.commons.io.FileUtils;
import org.apache.helix.model.InstanceConfig;
@@ -39,6 +41,8 @@ import
org.apache.pinot.spi.config.instance.InstanceDataManagerConfig;
import org.apache.pinot.spi.config.table.TableConfig;
import org.apache.pinot.spi.env.CommonsConfigurationUtils;
import org.apache.pinot.spi.filesystem.PinotFSFactory;
+import org.apache.pinot.spi.utils.CommonConstants;
+import org.apache.pinot.spi.utils.retry.AttemptsExceededException;
import org.mockito.MockedConstruction;
import org.mockito.MockedStatic;
import org.testng.annotations.AfterClass;
@@ -46,11 +50,15 @@ import org.testng.annotations.Test;
import static org.apache.pinot.server.predownload.PredownloadTestUtil.*;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertThrows;
+import static org.testng.Assert.assertTrue;
public class PredownloadSchedulerTest {
@@ -299,7 +307,8 @@ public class PredownloadSchedulerTest {
}
@Test
- public void testPredownloadParallelismConfiguration() throws Exception {
+ public void testPredownloadParallelismConfiguration()
+ throws Exception {
// Test default parallelism (should use numProcessors * 3)
Map<String, Object> defaultProps = Map.of(
"pinot.server.instance.id", INSTANCE_ID,
@@ -355,4 +364,423 @@ public class PredownloadSchedulerTest {
"Zero parallelism should fall back to default");
zeroScheduler.stop();
}
+
+ // ── Peer download tests
────────────────────────────────────────────────────
+
+ private PredownloadScheduler
buildPeerEnabledScheduler(PropertiesConfiguration properties)
+ throws Exception {
+
properties.setProperty(CommonConstants.Server.CONFIG_OF_PEER_DOWNLOAD_ENABLED,
true);
+ PredownloadScheduler scheduler = spy(new PredownloadScheduler(properties));
+ scheduler._executor = Runnable::run;
+
+ Field schemeField =
PredownloadScheduler.class.getDeclaredField("_peerDownloadScheme");
+ schemeField.setAccessible(true);
+ schemeField.set(scheduler, "http");
+
+ Field metricsField =
PredownloadScheduler.class.getDeclaredField("_predownloadMetrics");
+ metricsField.setAccessible(true);
+ metricsField.set(scheduler, mock(PredownloadMetrics.class));
+
+ return scheduler;
+ }
+
+ private void injectMockZkClient(PredownloadScheduler scheduler,
PredownloadZKClient mockZkClient)
+ throws Exception {
+ Field zkField =
PredownloadScheduler.class.getDeclaredField("_predownloadZkClient");
+ zkField.setAccessible(true);
+ zkField.set(scheduler, mockZkClient);
+ }
+
+ private void injectSegmentState(PredownloadScheduler scheduler,
+ List<PredownloadSegmentInfo> segments, Map<String, PredownloadTableInfo>
tableInfoMap)
+ throws Exception {
+ Field segField =
PredownloadScheduler.class.getDeclaredField("_predownloadSegmentInfoList");
+ segField.setAccessible(true);
+ segField.set(scheduler, segments);
+
+ Field mapField =
PredownloadScheduler.class.getDeclaredField("_tableInfoMap");
+ mapField.setAccessible(true);
+ mapField.set(scheduler, tableInfoMap);
+ }
+
+ @Test
+ public void testPeerDownloadEnabledViaConstructor()
+ throws Exception {
+ String propertiesFilePath =
+
this.getClass().getClassLoader().getResource(SAMPLE_PROPERTIES_FILE_NAME).getPath();
+ PropertiesConfiguration properties =
CommonsConfigurationUtils.fromPath(propertiesFilePath);
+
+ Field enabledField =
PredownloadScheduler.class.getDeclaredField("_peerDownloadEnabled");
+ enabledField.setAccessible(true);
+
+ PredownloadScheduler disabled = new PredownloadScheduler(properties);
+ assertEquals(enabledField.get(disabled), false);
+ disabled.stop();
+
+
properties.setProperty(CommonConstants.Server.CONFIG_OF_PEER_DOWNLOAD_ENABLED,
true);
+ PredownloadScheduler enabled = new PredownloadScheduler(properties);
+ assertEquals(enabledField.get(enabled), true);
+ enabled.stop();
+ }
+
+ @Test
+ public void testDownloadSegmentFallbackToPeerOnDeepStoreFailure()
+ throws Exception {
+ String propertiesFilePath =
+
this.getClass().getClassLoader().getResource(SAMPLE_PROPERTIES_FILE_NAME).getPath();
+ PropertiesConfiguration properties =
CommonsConfigurationUtils.fromPath(propertiesFilePath);
+ setUp(properties);
+
+ PredownloadScheduler scheduler = buildPeerEnabledScheduler(properties);
+ PredownloadZKClient mockZkClient = mock(PredownloadZKClient.class);
+ when(mockZkClient.getPeerServerURIs(any(), anyString(), anyString(),
anyString())).thenReturn(new ArrayList<>());
+ injectMockZkClient(scheduler, mockZkClient);
+
+ PredownloadSegmentInfo segment = new PredownloadSegmentInfo(TABLE_NAME,
SEGMENT_NAME);
+ segment.updateSegmentInfo(createSegmentZKMetadata());
+ scheduler._failedSegments.add(SEGMENT_NAME);
+
+ File testFolder = new File(_temporaryFolder, "peerFallbackTest");
+ testFolder.mkdirs();
+ String dataDir = testFolder.getAbsolutePath();
+ int lastIndex = dataDir.lastIndexOf(File.separator);
+
when(_predownloadTableInfo.getInstanceDataManagerConfig()).thenReturn(_instanceDataManagerConfig);
+ when(_predownloadTableInfo.getTableConfig()).thenReturn(_tableConfig);
+
when(_instanceDataManagerConfig.getInstanceDataDir()).thenReturn(dataDir.substring(0,
lastIndex));
+ when(_tableConfig.getTableName()).thenReturn(dataDir.substring(lastIndex +
1));
+ when(_predownloadTableInfo.loadSegmentFromLocal(any())).thenReturn(false);
+ injectSegmentState(scheduler, List.of(segment), Map.of(TABLE_NAME,
_predownloadTableInfo));
+
+ try (MockedStatic<SegmentFetcherFactory> sfMock =
mockStatic(SegmentFetcherFactory.class)) {
+ // 3-arg (deep store) throws — simulating exhausted deep store retries
+ sfMock.when(
+ () ->
SegmentFetcherFactory.fetchAndDecryptSegmentToLocal(anyString(),
any(File.class), anyString()))
+ .thenThrow(new AttemptsExceededException("deep store failed", 3));
+ // 5-arg (peer) succeeds — supplier lambda is not invoked by mock, so no
ZK call needed
+ sfMock.when(
+ () -> SegmentFetcherFactory.fetchAndDecryptSegmentToLocal(
+ anyString(), anyString(), any(), any(File.class),
anyString()))
+ .thenAnswer(inv -> null);
+
+ try (MockedStatic<TarCompressionUtils> tarMock =
mockStatic(TarCompressionUtils.class)) {
+ tarMock.when(() -> TarCompressionUtils.untar(any(File.class),
any(File.class)))
+ .thenAnswer(inv -> {
+ File untarDir = new File(testFolder, "untared_peer");
+ untarDir.mkdirs();
+ return List.of(untarDir);
+ });
+ scheduler.downloadSegment(segment);
+ }
+ }
+
+ assertFalse(scheduler._failedSegments.contains(SEGMENT_NAME),
+ "Segment should be removed from failed set after successful peer
download");
+ scheduler._executor = null; // lambda can't be cast to ThreadPoolExecutor
+ scheduler.stop();
+ }
+
+ @Test
+ public void testDownloadSegmentPeerDownloadStreamedOnDeepStoreFailure()
+ throws Exception {
+ String propertiesFilePath =
+
this.getClass().getClassLoader().getResource(SAMPLE_PROPERTIES_FILE_NAME).getPath();
+ PropertiesConfiguration properties =
CommonsConfigurationUtils.fromPath(propertiesFilePath);
+
properties.setProperty("pinot.server.instance.segment.stream.download.untar",
true);
+ setUp(properties);
+
+ PredownloadScheduler scheduler = buildPeerEnabledScheduler(properties);
+ PredownloadZKClient mockZkClient = mock(PredownloadZKClient.class);
+ when(mockZkClient.getPeerServerURIs(any(), anyString(), anyString(),
anyString())).thenReturn(new ArrayList<>());
+ injectMockZkClient(scheduler, mockZkClient);
+
+ PredownloadSegmentInfo segment = new PredownloadSegmentInfo(TABLE_NAME,
SEGMENT_NAME);
+ SegmentZKMetadata zkMetadataWithoutCrypterName = createSegmentZKMetadata();
+ zkMetadataWithoutCrypterName.setCrypterName(null);
+ segment.updateSegmentInfo(zkMetadataWithoutCrypterName);
+ scheduler._failedSegments.add(SEGMENT_NAME);
+
+ File testFolder = new File(_temporaryFolder, "peerStreamedFallbackTest");
+ testFolder.mkdirs();
+ String dataDir = testFolder.getAbsolutePath();
+ int lastIndex = dataDir.lastIndexOf(File.separator);
+
when(_predownloadTableInfo.getInstanceDataManagerConfig()).thenReturn(_instanceDataManagerConfig);
+ when(_predownloadTableInfo.getTableConfig()).thenReturn(_tableConfig);
+
when(_instanceDataManagerConfig.getInstanceDataDir()).thenReturn(dataDir.substring(0,
lastIndex));
+ when(_tableConfig.getTableName()).thenReturn(dataDir.substring(lastIndex +
1));
+ when(_predownloadTableInfo.loadSegmentFromLocal(any())).thenReturn(false);
+ injectSegmentState(scheduler, List.of(segment), Map.of(TABLE_NAME,
_predownloadTableInfo));
+
+ File untarDir = new File(testFolder, "untared_peer_streamed");
+ untarDir.mkdirs();
+
+ try (MockedStatic<SegmentFetcherFactory> sfMock =
mockStatic(SegmentFetcherFactory.class)) {
+ // deep store streamed (single-URI) download throws — simulating
exhausted deep store retries
+ sfMock.when(
+ () ->
SegmentFetcherFactory.fetchAndStreamUntarToLocal(anyString(), any(File.class),
anyLong(),
+ any(AtomicInteger.class)))
+ .thenThrow(new AttemptsExceededException("deep store streamed
failed", 3));
+ // peer streamed (multi-peer supplier) download succeeds
+ sfMock.when(
+ () ->
SegmentFetcherFactory.fetchAndStreamUntarToLocal(anyString(), anyString(),
any(), any(File.class),
+ anyLong()))
+ .thenReturn(untarDir);
+
+ scheduler.downloadSegment(segment);
+ }
+
+ assertFalse(scheduler._failedSegments.contains(SEGMENT_NAME),
+ "Segment should be removed from failed set after successful streamed
peer download");
+ scheduler._executor = null; // lambda can't be cast to ThreadPoolExecutor
+ scheduler.stop();
+ }
+
+ @Test
+ public void testDownloadSegmentNoPeerFallbackWhenPeerDisabled()
+ throws Exception {
+ String propertiesFilePath =
+
this.getClass().getClassLoader().getResource(SAMPLE_PROPERTIES_FILE_NAME).getPath();
+ PropertiesConfiguration properties =
CommonsConfigurationUtils.fromPath(propertiesFilePath);
+ setUp(properties);
+
+ // peerDownloadEnabled=false (default from properties) — no fallback to
peer
+ PredownloadScheduler scheduler = spy(new PredownloadScheduler(properties));
+ scheduler._executor = Runnable::run;
+ Field metricsField =
PredownloadScheduler.class.getDeclaredField("_predownloadMetrics");
+ metricsField.setAccessible(true);
+ metricsField.set(scheduler, mock(PredownloadMetrics.class));
+
+ PredownloadSegmentInfo segment = new PredownloadSegmentInfo(TABLE_NAME,
SEGMENT_NAME);
+ segment.updateSegmentInfo(createSegmentZKMetadata());
+
+ File testFolder = new File(_temporaryFolder, "noPeerTest");
+ testFolder.mkdirs();
+ String dataDir = testFolder.getAbsolutePath();
+ int lastIndex = dataDir.lastIndexOf(File.separator);
+
when(_predownloadTableInfo.getInstanceDataManagerConfig()).thenReturn(_instanceDataManagerConfig);
+ when(_predownloadTableInfo.getTableConfig()).thenReturn(_tableConfig);
+
when(_instanceDataManagerConfig.getInstanceDataDir()).thenReturn(dataDir.substring(0,
lastIndex));
+ when(_tableConfig.getTableName()).thenReturn(dataDir.substring(lastIndex +
1));
+ injectSegmentState(scheduler, List.of(segment), Map.of(TABLE_NAME,
_predownloadTableInfo));
+
+ try (MockedStatic<SegmentFetcherFactory> sfMock =
mockStatic(SegmentFetcherFactory.class)) {
+ sfMock.when(
+ () ->
SegmentFetcherFactory.fetchAndDecryptSegmentToLocal(anyString(),
any(File.class), anyString()))
+ .thenThrow(new AttemptsExceededException("deep store failed", 3));
+
+ try (MockedStatic<TarCompressionUtils> tarMock =
mockStatic(TarCompressionUtils.class)) {
+ assertThrows(AttemptsExceededException.class, () ->
scheduler.downloadSegment(segment));
+ }
+ }
+
+ assertTrue(scheduler._failedSegments.contains(SEGMENT_NAME),
+ "Segment should remain in failed set when peer download is disabled");
+ scheduler._executor = null;
+ scheduler.stop();
+ }
+
+ @Test
+ public void testDownloadSegmentBothDeepStoreAndPeerFail()
+ throws Exception {
+ String propertiesFilePath =
+
this.getClass().getClassLoader().getResource(SAMPLE_PROPERTIES_FILE_NAME).getPath();
+ PropertiesConfiguration properties =
CommonsConfigurationUtils.fromPath(propertiesFilePath);
+ setUp(properties);
+
+ PredownloadScheduler scheduler = buildPeerEnabledScheduler(properties);
+ PredownloadZKClient mockZkClient = mock(PredownloadZKClient.class);
+ when(mockZkClient.getPeerServerURIs(any(), anyString(), anyString(),
anyString())).thenReturn(new ArrayList<>());
+ injectMockZkClient(scheduler, mockZkClient);
+
+ PredownloadSegmentInfo segment = new PredownloadSegmentInfo(TABLE_NAME,
SEGMENT_NAME);
+ segment.updateSegmentInfo(createSegmentZKMetadata());
+
+ File testFolder = new File(_temporaryFolder, "bothFailTest");
+ testFolder.mkdirs();
+ String dataDir = testFolder.getAbsolutePath();
+ int lastIndex = dataDir.lastIndexOf(File.separator);
+
when(_predownloadTableInfo.getInstanceDataManagerConfig()).thenReturn(_instanceDataManagerConfig);
+ when(_predownloadTableInfo.getTableConfig()).thenReturn(_tableConfig);
+
when(_instanceDataManagerConfig.getInstanceDataDir()).thenReturn(dataDir.substring(0,
lastIndex));
+ when(_tableConfig.getTableName()).thenReturn(dataDir.substring(lastIndex +
1));
+ injectSegmentState(scheduler, List.of(segment), Map.of(TABLE_NAME,
_predownloadTableInfo));
+
+ try (MockedStatic<SegmentFetcherFactory> sfMock =
mockStatic(SegmentFetcherFactory.class)) {
+ sfMock.when(
+ () ->
SegmentFetcherFactory.fetchAndDecryptSegmentToLocal(anyString(),
any(File.class), anyString()))
+ .thenThrow(new AttemptsExceededException("deep store failed", 3));
+ sfMock.when(
+ () -> SegmentFetcherFactory.fetchAndDecryptSegmentToLocal(
+ anyString(), anyString(), any(), any(File.class),
anyString()))
+ .thenThrow(new AttemptsExceededException("peer download failed", 3));
+
+ try (MockedStatic<TarCompressionUtils> tarMock =
mockStatic(TarCompressionUtils.class)) {
+ assertThrows(AttemptsExceededException.class, () ->
scheduler.downloadSegment(segment));
+ }
+ }
+
+ assertTrue(scheduler._failedSegments.contains(SEGMENT_NAME),
+ "Segment should be in failed set when both deep store and peer
download fail");
+ scheduler._executor = null;
+ scheduler.stop();
+ }
+
+ @Test
+ public void testDeepStoreSuccessEmitsDeepStoreMetric()
+ throws Exception {
+ String propertiesFilePath =
+
this.getClass().getClassLoader().getResource(SAMPLE_PROPERTIES_FILE_NAME).getPath();
+ PropertiesConfiguration properties =
CommonsConfigurationUtils.fromPath(propertiesFilePath);
+ setUp(properties);
+
+ PredownloadScheduler scheduler = buildPeerEnabledScheduler(properties);
+
+ PredownloadSegmentInfo segment = new PredownloadSegmentInfo(TABLE_NAME,
SEGMENT_NAME);
+ segment.updateSegmentInfo(createSegmentZKMetadata());
+
+ File testFolder = new File(_temporaryFolder, "deepStoreMetricTest");
+ testFolder.mkdirs();
+ String dataDir = testFolder.getAbsolutePath();
+ int lastIndex = dataDir.lastIndexOf(File.separator);
+
when(_predownloadTableInfo.getInstanceDataManagerConfig()).thenReturn(_instanceDataManagerConfig);
+ when(_predownloadTableInfo.getTableConfig()).thenReturn(_tableConfig);
+
when(_instanceDataManagerConfig.getInstanceDataDir()).thenReturn(dataDir.substring(0,
lastIndex));
+ when(_tableConfig.getTableName()).thenReturn(dataDir.substring(lastIndex +
1));
+ when(_predownloadTableInfo.loadSegmentFromLocal(any())).thenReturn(false);
+ injectSegmentState(scheduler, List.of(segment), Map.of(TABLE_NAME,
_predownloadTableInfo));
+
+ PredownloadMetrics mockMetrics = getMockMetrics(scheduler);
+
+ try (MockedStatic<SegmentFetcherFactory> sfMock =
mockStatic(SegmentFetcherFactory.class)) {
+ sfMock.when(
+ () ->
SegmentFetcherFactory.fetchAndDecryptSegmentToLocal(anyString(),
any(File.class), anyString()))
+ .thenAnswer(inv -> null);
+ try (MockedStatic<TarCompressionUtils> tarMock =
mockStatic(TarCompressionUtils.class)) {
+ tarMock.when(() -> TarCompressionUtils.untar(any(File.class),
any(File.class)))
+ .thenAnswer(inv -> {
+ File untarDir = new File(testFolder, "untared");
+ untarDir.mkdirs();
+ return List.of(untarDir);
+ });
+ scheduler.downloadSegment(segment);
+ }
+ }
+
+ verify(mockMetrics).deepStoreSegmentDownloaded();
+ verify(mockMetrics).segmentDownloaded(eq(true), eq(SEGMENT_NAME),
anyLong(), anyLong());
+ verify(mockMetrics, never()).peerSegmentDownloaded(anyBoolean(),
anyString(), anyLong(), anyLong());
+ scheduler._executor = null;
+ scheduler.stop();
+ }
+
+ @Test
+ public void testPeerDownloadSuccessEmitsPeerMetrics()
+ throws Exception {
+ String propertiesFilePath =
+
this.getClass().getClassLoader().getResource(SAMPLE_PROPERTIES_FILE_NAME).getPath();
+ PropertiesConfiguration properties =
CommonsConfigurationUtils.fromPath(propertiesFilePath);
+ setUp(properties);
+
+ PredownloadScheduler scheduler = buildPeerEnabledScheduler(properties);
+ PredownloadZKClient mockZkClient = mock(PredownloadZKClient.class);
+ when(mockZkClient.getPeerServerURIs(any(), anyString(), anyString(),
anyString())).thenReturn(new ArrayList<>());
+ injectMockZkClient(scheduler, mockZkClient);
+
+ PredownloadSegmentInfo segment = new PredownloadSegmentInfo(TABLE_NAME,
SEGMENT_NAME);
+ segment.updateSegmentInfo(createSegmentZKMetadata());
+
+ File testFolder = new File(_temporaryFolder, "peerMetricTest");
+ testFolder.mkdirs();
+ String dataDir = testFolder.getAbsolutePath();
+ int lastIndex = dataDir.lastIndexOf(File.separator);
+
when(_predownloadTableInfo.getInstanceDataManagerConfig()).thenReturn(_instanceDataManagerConfig);
+ when(_predownloadTableInfo.getTableConfig()).thenReturn(_tableConfig);
+
when(_instanceDataManagerConfig.getInstanceDataDir()).thenReturn(dataDir.substring(0,
lastIndex));
+ when(_tableConfig.getTableName()).thenReturn(dataDir.substring(lastIndex +
1));
+ when(_predownloadTableInfo.loadSegmentFromLocal(any())).thenReturn(false);
+ injectSegmentState(scheduler, List.of(segment), Map.of(TABLE_NAME,
_predownloadTableInfo));
+
+ PredownloadMetrics mockMetrics = getMockMetrics(scheduler);
+
+ try (MockedStatic<SegmentFetcherFactory> sfMock =
mockStatic(SegmentFetcherFactory.class)) {
+ sfMock.when(
+ () ->
SegmentFetcherFactory.fetchAndDecryptSegmentToLocal(anyString(),
any(File.class), anyString()))
+ .thenThrow(new AttemptsExceededException("deep store failed", 3));
+ sfMock.when(
+ () -> SegmentFetcherFactory.fetchAndDecryptSegmentToLocal(
+ anyString(), anyString(), any(), any(File.class),
anyString()))
+ .thenAnswer(inv -> null);
+
+ try (MockedStatic<TarCompressionUtils> tarMock =
mockStatic(TarCompressionUtils.class)) {
+ tarMock.when(() -> TarCompressionUtils.untar(any(File.class),
any(File.class)))
+ .thenAnswer(inv -> {
+ File untarDir = new File(testFolder, "untared_peer");
+ untarDir.mkdirs();
+ return List.of(untarDir);
+ });
+ scheduler.downloadSegment(segment);
+ }
+ }
+
+ verify(mockMetrics, never()).deepStoreSegmentDownloaded();
+ verify(mockMetrics).peerSegmentDownloaded(eq(true), eq(SEGMENT_NAME),
anyLong(), anyLong());
+ verify(mockMetrics).segmentDownloaded(eq(true), eq(SEGMENT_NAME),
anyLong(), anyLong());
+ scheduler._executor = null;
+ scheduler.stop();
+ }
+
+ @Test
+ public void testPeerDownloadFailureEmitsSegmentLevelMetric()
+ throws Exception {
+ String propertiesFilePath =
+
this.getClass().getClassLoader().getResource(SAMPLE_PROPERTIES_FILE_NAME).getPath();
+ PropertiesConfiguration properties =
CommonsConfigurationUtils.fromPath(propertiesFilePath);
+ setUp(properties);
+
+ PredownloadScheduler scheduler = buildPeerEnabledScheduler(properties);
+ PredownloadZKClient mockZkClient = mock(PredownloadZKClient.class);
+ when(mockZkClient.getPeerServerURIs(any(), anyString(), anyString(),
anyString())).thenReturn(new ArrayList<>());
+ injectMockZkClient(scheduler, mockZkClient);
+
+ PredownloadSegmentInfo segment = new PredownloadSegmentInfo(TABLE_NAME,
SEGMENT_NAME);
+ segment.updateSegmentInfo(createSegmentZKMetadata());
+
+ File testFolder = new File(_temporaryFolder, "peerFailMetricTest");
+ testFolder.mkdirs();
+ String dataDir = testFolder.getAbsolutePath();
+ int lastIndex = dataDir.lastIndexOf(File.separator);
+
when(_predownloadTableInfo.getInstanceDataManagerConfig()).thenReturn(_instanceDataManagerConfig);
+ when(_predownloadTableInfo.getTableConfig()).thenReturn(_tableConfig);
+
when(_instanceDataManagerConfig.getInstanceDataDir()).thenReturn(dataDir.substring(0,
lastIndex));
+ when(_tableConfig.getTableName()).thenReturn(dataDir.substring(lastIndex +
1));
+ injectSegmentState(scheduler, List.of(segment), Map.of(TABLE_NAME,
_predownloadTableInfo));
+
+ PredownloadMetrics mockMetrics = getMockMetrics(scheduler);
+
+ try (MockedStatic<SegmentFetcherFactory> sfMock =
mockStatic(SegmentFetcherFactory.class)) {
+ sfMock.when(
+ () ->
SegmentFetcherFactory.fetchAndDecryptSegmentToLocal(anyString(),
any(File.class), anyString()))
+ .thenThrow(new AttemptsExceededException("deep store failed", 3));
+ sfMock.when(
+ () -> SegmentFetcherFactory.fetchAndDecryptSegmentToLocal(
+ anyString(), anyString(), any(), any(File.class),
anyString()))
+ .thenThrow(new AttemptsExceededException("peer download failed", 3));
+
+ try (MockedStatic<TarCompressionUtils> tarMock =
mockStatic(TarCompressionUtils.class)) {
+ assertThrows(AttemptsExceededException.class, () ->
scheduler.downloadSegment(segment));
+ }
+ }
+
+ verify(mockMetrics, never()).deepStoreSegmentDownloaded();
+ verify(mockMetrics).peerSegmentDownloaded(eq(false), eq(SEGMENT_NAME),
eq(0L), eq(0L));
+ verify(mockMetrics).segmentDownloaded(eq(false), eq(SEGMENT_NAME), eq(0L),
eq(0L));
+ scheduler._executor = null;
+ scheduler.stop();
+ }
+
+ private PredownloadMetrics getMockMetrics(PredownloadScheduler scheduler)
+ throws Exception {
+ Field metricsField =
PredownloadScheduler.class.getDeclaredField("_predownloadMetrics");
+ metricsField.setAccessible(true);
+ return (PredownloadMetrics) metricsField.get(scheduler);
+ }
}
diff --git
a/pinot-server/src/test/java/org/apache/pinot/server/predownload/PredownloadZKClientTest.java
b/pinot-server/src/test/java/org/apache/pinot/server/predownload/PredownloadZKClientTest.java
index fda970a4272..3867c1927d7 100644
---
a/pinot-server/src/test/java/org/apache/pinot/server/predownload/PredownloadZKClientTest.java
+++
b/pinot-server/src/test/java/org/apache/pinot/server/predownload/PredownloadZKClientTest.java
@@ -18,6 +18,7 @@
*/
package org.apache.pinot.server.predownload;
+import java.net.URI;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
@@ -25,15 +26,20 @@ import java.util.Map;
import org.apache.helix.HelixDataAccessor;
import org.apache.helix.HelixException;
import org.apache.helix.PropertyKey;
+import org.apache.helix.PropertyType;
import org.apache.helix.model.CurrentState;
+import org.apache.helix.model.ExternalView;
+import org.apache.helix.model.InstanceConfig;
import org.apache.helix.model.LiveInstance;
import org.apache.helix.zookeeper.api.client.HelixZkClient;
+import org.apache.helix.zookeeper.datamodel.ZNRecord;
import org.apache.helix.zookeeper.impl.factory.SharedZkClientFactory;
import org.apache.pinot.common.metadata.ZKMetadataProvider;
import org.apache.pinot.server.starter.helix.HelixInstanceDataManagerConfig;
import org.apache.pinot.spi.config.instance.InstanceDataManagerConfig;
import org.apache.pinot.spi.config.table.TableConfig;
import org.apache.pinot.spi.env.PinotConfiguration;
+import org.apache.pinot.spi.utils.CommonConstants;
import org.mockito.MockedStatic;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
@@ -41,6 +47,7 @@ import org.testng.annotations.Test;
import static org.apache.pinot.server.predownload.PredownloadTestUtil.*;
import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.spy;
@@ -182,4 +189,69 @@ public class PredownloadZKClientTest {
assertEquals(predownloadSegmentInfoList.get(1).getCrc(), 0);
}
}
+
+ @Test
+ public void testGetPeerServerURIs()
+ throws Exception {
+ // Pass accessor directly — no spy needed since method signature accepts
accessor as param
+ HelixDataAccessor dataAccessor = mock(HelixDataAccessor.class);
+ when(dataAccessor.keyBuilder()).thenReturn(new
PropertyKey.Builder(CLUSTER_NAME));
+
+ ExternalView externalView = mock(ExternalView.class);
+ String peerInstanceId = "Server_peer_8098";
+ InstanceConfig peerConfig = mock(InstanceConfig.class);
+ when(peerConfig.getHostName()).thenReturn("peerHost");
+ ZNRecord peerRecord = new ZNRecord(peerInstanceId);
+ peerRecord.setIntField(CommonConstants.Helix.Instance.ADMIN_PORT_KEY,
9000);
+ when(peerConfig.getRecord()).thenReturn(peerRecord);
+
+ // Case 1: ExternalView is null → empty list
+ doAnswer(inv ->
null).when(dataAccessor).getProperty(any(PropertyKey.class));
+ assertTrue(_predownloadZkClient.getPeerServerURIs(dataAccessor,
TABLE_NAME, SEGMENT_NAME, "http").isEmpty(),
+ "Should return empty list when ExternalView is null");
+
+ // Case 2: ExternalView found but segment has no state map → empty list
+ doAnswer(inv -> {
+ PropertyKey key = inv.getArgument(0);
+ return (key.getType() == PropertyType.EXTERNALVIEW) ? externalView :
null;
+ }).when(dataAccessor).getProperty(any(PropertyKey.class));
+ when(externalView.getStateMap(SEGMENT_NAME)).thenReturn(null);
+ assertTrue(_predownloadZkClient.getPeerServerURIs(dataAccessor,
TABLE_NAME, SEGMENT_NAME, "http").isEmpty(),
+ "Should return empty list when segment not in ExternalView");
+
+ // Case 3: Only self ONLINE → empty list
+ // In @BeforeClass: new PredownloadZKClient(ZK_ADDRESS, INSTANCE_ID,
CLUSTER_NAME)
+ // → _instanceName = CLUSTER_NAME
+
when(externalView.getStateMap(SEGMENT_NAME)).thenReturn(Map.of(CLUSTER_NAME,
"ONLINE"));
+ assertTrue(_predownloadZkClient.getPeerServerURIs(dataAccessor,
TABLE_NAME, SEGMENT_NAME, "http").isEmpty(),
+ "Should exclude self instance from peer list");
+
+ // Case 4: peer ONLINE + one OFFLINE → only peer returned (self-exclusion
tested in Case 3)
+ Map<String, String> stateMap = new HashMap<>();
+ stateMap.put(peerInstanceId, "ONLINE"); // peer → included
+ stateMap.put("Server_offline", "OFFLINE"); // offline → excluded
+ when(externalView.getStateMap(SEGMENT_NAME)).thenReturn(stateMap);
+ doAnswer(inv -> {
+ PropertyKey key = inv.getArgument(0);
+ return (key.getType() == PropertyType.EXTERNALVIEW) ? externalView :
peerConfig;
+ }).when(dataAccessor).getProperty(any(PropertyKey.class));
+
+ List<URI> uris = _predownloadZkClient.getPeerServerURIs(dataAccessor,
TABLE_NAME, SEGMENT_NAME, "http");
+ assertEquals(uris.size(), 1, "Should return exactly one peer URI");
+ String uriStr = uris.get(0).toString();
+ assertTrue(uriStr.contains("peerHost"), "URI should contain peer
hostname");
+ assertTrue(uriStr.contains("9000"), "URI should contain peer admin port");
+ assertTrue(uriStr.contains(TABLE_NAME), "URI should contain table name");
+ assertTrue(uriStr.contains(SEGMENT_NAME), "URI should contain segment
name");
+ assertTrue(uriStr.startsWith("http://"), "URI should use http scheme");
+
+ // Case 5: Peer InstanceConfig is null → skipped, empty list
+
when(externalView.getStateMap(SEGMENT_NAME)).thenReturn(Map.of(peerInstanceId,
"ONLINE"));
+ doAnswer(inv -> {
+ PropertyKey key = inv.getArgument(0);
+ return (key.getType() == PropertyType.EXTERNALVIEW) ? externalView :
null;
+ }).when(dataAccessor).getProperty(any(PropertyKey.class));
+ assertTrue(_predownloadZkClient.getPeerServerURIs(dataAccessor,
TABLE_NAME, SEGMENT_NAME, "http").isEmpty(),
+ "Should skip peer when its InstanceConfig is null");
+ }
}
diff --git
a/pinot-spi/src/main/java/org/apache/pinot/spi/utils/CommonConstants.java
b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/CommonConstants.java
index b81f4fb2828..ee25435988a 100644
--- a/pinot-spi/src/main/java/org/apache/pinot/spi/utils/CommonConstants.java
+++ b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/CommonConstants.java
@@ -1727,6 +1727,8 @@ public class CommonConstants {
// Predownload related configs
public static final String CONFIG_OF_PREDOWNLOAD_PARALLELISM =
"pinot.server.predownload.parallelism";
public static final int DEFAULT_PREDOWNLOAD_PARALLELISM = -1; // Use
numProcessors * 3 as default
+ public static final String CONFIG_OF_PEER_DOWNLOAD_ENABLED =
"pinot.server.peer.download.enabled";
+ public static final boolean DEFAULT_PEER_DOWNLOAD_ENABLED = false;
public static final String CONFIG_OF_CURRENT_DATA_TABLE_VERSION =
"pinot.server.instance.currentDataTableVersion";
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]