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

clintropolis pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git


The following commit(s) were added to refs/heads/master by this push:
     new f0cbad8f0f0 feat: support partial loads for tasks (#19739)
f0cbad8f0f0 is described below

commit f0cbad8f0f05e2b9e728782761c8f236b303d1f6
Author: Clint Wylie <[email protected]>
AuthorDate: Tue Jul 28 15:48:54 2026 -0700

    feat: support partial loads for tasks (#19739)
---
 .../msq/EmbeddedMSQPartialDownloadsTest.java       | 204 +++++++++
 .../MultipleKubernetesTaskRunnerFactoryTest.java   |   2 +-
 .../common/SegmentCacheManagerFactory.java         |  49 ++-
 .../druid/indexing/common/TaskToolboxFactory.java  |  12 +-
 .../druid/indexing/common/config/TaskConfig.java   |  27 +-
 .../apache/druid/indexing/common/task/Tasks.java   |   1 +
 .../druid/indexing/input/DruidInputSource.java     |   2 +-
 .../common/SegmentCacheManagerFactoryTest.java     | 116 ++++-
 .../druid/indexing/common/TaskToolboxTest.java     |   4 +
 .../indexing/common/config/TaskConfigBuilder.java  |  10 +-
 .../indexing/common/config/TaskConfigTest.java     |  45 ++
 .../common/task/CompactionTaskRunBase.java         |  53 ++-
 .../druid/indexing/common/task/IndexTaskTest.java  |  13 +-
 .../indexing/common/task/IngestionTestBase.java    |   2 +-
 .../AbstractMultiPhaseParallelIndexingTest.java    |   2 +-
 .../druid/msq/indexing/IndexerWorkerContext.java   |   8 +-
 .../druid/msq/util/MultiStageQueryContext.java     |  15 +
 .../msq/input/RegularLoadableSegmentTest.java      |  17 +-
 .../druid/msq/test/CalciteMSQTestsHelper.java      |   2 +-
 .../org/apache/druid/msq/test/MSQTestBase.java     |   3 +-
 .../druid/msq/util/MultiStageQueryContextTest.java |  24 +
 .../org/apache/druid/guice/StorageNodeModule.java  |   2 +-
 .../druid/segment/loading/SegmentLoaderConfig.java | 487 ++++++++++++++++-----
 .../segment/loading/SegmentLocalCacheManager.java  |   6 +
 .../apache/druid/client/DruidServerConfigTest.java |  13 +-
 .../apache/druid/guice/StorageNodeModuleTest.java  |   6 +-
 .../PermitLimitedSegmentRangeReaderTest.java       |   9 +-
 .../segment/loading/SegmentLoaderConfigTest.java   |  68 ++-
 .../SegmentLocalCacheManagerConcurrencyTest.java   |  50 +--
 ...SegmentLocalCacheManagerPartialAcquireTest.java |  38 +-
 .../SegmentLocalCacheManagerPartialDropTest.java   |   2 +-
 ...egmentLocalCacheManagerPartialRuleLoadTest.java |   9 +-
 .../loading/SegmentLocalCacheManagerTest.java      | 246 +++--------
 .../loading/StorageLoadingThreadPoolTest.java      |  46 +-
 .../appenderator/StreamAppenderatorTester.java     |  11 +-
 .../apache/druid/server/SegmentManagerTest.java    |  48 +-
 .../apache/druid/server/StatusResourceTest.java    |   6 +-
 .../SegmentCacheBootstrapperCacheTest.java         |  23 +-
 .../coordination/SegmentCacheBootstrapperTest.java |  35 +-
 .../coordination/SegmentLoadDropHandlerTest.java   |  83 +---
 ...egmentManagerBroadcastJoinIndexedTableTest.java |  20 +-
 .../SegmentManagerThreadSafetyTest.java            |  20 +-
 .../server/metrics/SegmentStatsMonitorTest.java    |   2 +-
 43 files changed, 1135 insertions(+), 706 deletions(-)

diff --git 
a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/msq/EmbeddedMSQPartialDownloadsTest.java
 
b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/msq/EmbeddedMSQPartialDownloadsTest.java
new file mode 100644
index 00000000000..af3581b6563
--- /dev/null
+++ 
b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/msq/EmbeddedMSQPartialDownloadsTest.java
@@ -0,0 +1,204 @@
+/*
+ * 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.druid.testing.embedded.msq;
+
+import org.apache.druid.common.utils.IdUtils;
+import org.apache.druid.data.input.impl.LocalInputSource;
+import org.apache.druid.indexer.TaskState;
+import org.apache.druid.java.util.common.StringUtils;
+import org.apache.druid.msq.indexing.report.MSQTaskReportPayload;
+import org.apache.druid.msq.util.MultiStageQueryContext;
+import org.apache.druid.query.DruidProcessingConfigTest;
+import org.apache.druid.query.http.SqlTaskStatus;
+import org.apache.druid.server.metrics.LatchableEmitter;
+import org.apache.druid.server.metrics.StorageMonitor;
+import org.apache.druid.sql.calcite.planner.Calcites;
+import org.apache.druid.testing.embedded.EmbeddedBroker;
+import org.apache.druid.testing.embedded.EmbeddedCoordinator;
+import org.apache.druid.testing.embedded.EmbeddedDruidCluster;
+import org.apache.druid.testing.embedded.EmbeddedHistorical;
+import org.apache.druid.testing.embedded.EmbeddedIndexer;
+import org.apache.druid.testing.embedded.EmbeddedOverlord;
+import org.apache.druid.testing.embedded.EmbeddedRouter;
+import org.apache.druid.testing.embedded.junit5.EmbeddedClusterTestBase;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * End-to-end wiring test for on-demand partial segment downloads on MSQ 
worker tasks. Partial downloads default to
+ * enabled for tasks (the {@code 
druid.indexer.task.virtualStoragePartialDownloadsEnabled} {@code TaskConfig} 
default is
+ * true), so run 1 sets no property or context and verifies the default 
engages the partial path. Run 2 verifies the
+ * per-query {@link 
MultiStageQueryContext#CTX_VIRTUAL_STORAGE_PARTIAL_DOWNLOADS} override can turn 
it back off.
+ * <p>
+ * The observable signal is the indexer's {@link 
StorageMonitor#VSF_READ_COUNT} metric: the partial path performs
+ * on-demand deep-storage range reads (so the count is positive), while the 
full-download path does not (the count
+ * stays zero because {@code virtualStoragePartialDownloadsEnabled=false} 
makes the worker mount segments as complete
+ * cache entries and download them in full instead of via range reads).
+ */
+class EmbeddedMSQPartialDownloadsTest extends EmbeddedClusterTestBase
+{
+  // A few times the storage monitor's PT1s emission period, so a single 
missed tick doesn't falsely read as "idle".
+  private static final long MONITOR_QUIESCE_TIMEOUT_MILLIS = 3_000L;
+
+  private static final String SELECT_SQL = "SELECT channel, COUNT(*) AS c FROM 
\"%s\" GROUP BY channel";
+
+  private final EmbeddedBroker broker = new EmbeddedBroker();
+  private final EmbeddedIndexer indexer = new EmbeddedIndexer();
+  private final EmbeddedOverlord overlord = new EmbeddedOverlord();
+  private final EmbeddedHistorical historical = new EmbeddedHistorical();
+  private final EmbeddedCoordinator coordinator = new EmbeddedCoordinator();
+  private final EmbeddedRouter router = new EmbeddedRouter();
+
+  private EmbeddedMSQApis msqApis;
+
+  @Override
+  public EmbeddedDruidCluster createCluster()
+  {
+    indexer.setServerMemory(400_000_000)
+           .addProperty("druid.worker.capacity", "4")
+           .addProperty("druid.processing.numThreads", "3");
+
+    broker.setServerMemory(200_000_000);
+
+    coordinator.addProperty("druid.manager.segments.useIncrementalCache", 
"always");
+    overlord.addProperty("druid.manager.segments.useIncrementalCache", 
"always")
+            .addProperty("druid.manager.segments.pollDuration", "PT0.1s");
+
+    return EmbeddedDruidCluster
+        .withEmbeddedDerbyAndZookeeper()
+        .useLatchableEmitter()
+        .useDefaultTimeoutForLatchableEmitter(20)
+        // Partial downloads require unzipped, V10 segments (range-readable 
via LocalLoadSpec's directory reader).
+        .addCommonProperty("druid.storage.zip", "false")
+        .addCommonProperty("druid.indexer.task.buildV10", "true")
+        .addCommonProperty("druid.monitoring.emissionPeriod", "PT1s")
+        .addServer(coordinator)
+        .addServer(overlord)
+        .addServer(indexer)
+        .addServer(historical)
+        .addServer(broker)
+        .addServer(router);
+  }
+
+  @BeforeAll
+  void loadData() throws IOException
+  {
+    msqApis = new EmbeddedMSQApis(cluster, overlord);
+    dataSource = "wiki-" + IdUtils.getRandomId();
+    loadWikiData();
+  }
+
+  @Override
+  protected void refreshDatasourceName()
+  {
+    // Keep the datasource stable: it is ingested once in loadData() before 
all tests.
+  }
+
+  @Test
+  void testPartialDownloadsWiredViaRuntimePropertyAndQueryContext()
+  {
+    final LatchableEmitter emitter = indexer.latchableEmitter();
+
+    // Run 1: no context override, so the query uses the indexer's TaskConfig 
default (partial enabled). The worker
+    // reads the input segment's columns on demand, which shows up as 
deep-storage range reads.
+    emitter.awaitMetricQuiescent(StorageMonitor.VSF_READ_COUNT, 
MONITOR_QUIESCE_TIMEOUT_MILLIS);
+    emitter.flush();
+    runMsqSelect(Collections.emptyMap());
+    // The worker's StorageMonitor only emits while the (ephemeral) task is 
alive, so the VSF_READ_COUNT events land
+    // during runMsqSelect. waitForEventAggregate (unlike waitForNextEvent) 
also considers events already emitted since
+    // the last flush, so it sees them; it fails the test (timeout) if no 
on-demand range reads ever happened.
+    emitter.waitForEventAggregate(
+        matcher -> matcher.hasMetricName(StorageMonitor.VSF_READ_COUNT),
+        aggregate -> aggregate.hasSumAtLeast(1L)
+    );
+
+    // Run 2: the query context disables partial downloads, so the worker 
downloads segments in full and performs no
+    // range reads. The flag is passed as the string "false" (as it can arrive 
from a client): it is copied into the
+    // worker task context and must be parsed, not unboxed, when building the 
toolbox and worker context.
+    emitter.awaitMetricQuiescent(StorageMonitor.VSF_READ_COUNT, 
MONITOR_QUIESCE_TIMEOUT_MILLIS);
+    emitter.flush();
+    
runMsqSelect(Map.of(MultiStageQueryContext.CTX_VIRTUAL_STORAGE_PARTIAL_DOWNLOADS,
 "false"));
+    emitter.awaitMetricQuiescent(StorageMonitor.VSF_READ_COUNT, 
MONITOR_QUIESCE_TIMEOUT_MILLIS);
+    Assertions.assertEquals(
+        0L,
+        emitter.getMetricEventLongSum(StorageMonitor.VSF_READ_COUNT),
+        "with partial downloads disabled via query context, the worker should 
download in full (no range reads)"
+    );
+  }
+
+  private void runMsqSelect(Map<String, Object> queryContext)
+  {
+    final SqlTaskStatus status = msqApis.submitTaskSql(queryContext, 
SELECT_SQL, dataSource);
+    cluster.callApi().waitForTaskToSucceed(status.getTaskId(), overlord);
+  }
+
+  private void loadWikiData() throws IOException
+  {
+    final File tmpDir = cluster.getTestFolder().newFolder();
+    final File wikiFile = new File(tmpDir, "wiki.gz");
+    try (var in = 
DruidProcessingConfigTest.class.getResourceAsStream("/wikipedia/wikiticker-2015-09-12-sampled.json.gz"))
 {
+      Files.copy(
+          Objects.requireNonNull(in, "wikiticker sample resource not found on 
the test classpath"),
+          wikiFile.toPath()
+      );
+    }
+
+    final String sql = StringUtils.format(
+        "SET waitUntilSegmentsLoad = TRUE;\n"
+        + "REPLACE INTO \"%s\" OVERWRITE ALL\n"
+        + "SELECT\n"
+        + "  TIME_PARSE(\"time\") AS __time,\n"
+        + "  channel,\n"
+        + "  countryName,\n"
+        + "  page,\n"
+        + "  \"user\",\n"
+        + "  added,\n"
+        + "  deleted,\n"
+        + "  delta\n"
+        + "FROM TABLE(\n"
+        + "    EXTERN(\n"
+        + "      %s,\n"
+        + "      '{\"type\":\"json\"}',\n"
+        + "      
'[{\"name\":\"channel\",\"type\":\"string\"},{\"name\":\"time\",\"type\":\"string\"},{\"name\":\"page\",\"type\":\"string\"},{\"name\":\"added\",\"type\":\"long\"},{\"name\":\"user\",\"type\":\"string\"},{\"name\":\"delta\",\"type\":\"long\"},{\"name\":\"deleted\",\"type\":\"long\"},{\"name\":\"countryName\",\"type\":\"string\"}]'\n"
+        + "    )\n"
+        + "  )\n"
+        + "PARTITIONED BY DAY\n"
+        + "CLUSTERED BY channel",
+        dataSource,
+        Calcites.escapeStringLiteral(
+            broker.bindings()
+                  .jsonMapper()
+                  .writeValueAsString(new LocalInputSource(null, null, 
Collections.singletonList(wikiFile), null))
+        )
+    );
+
+    final MSQTaskReportPayload payload = msqApis.runTaskSqlAndGetReport(sql);
+    Assertions.assertEquals(TaskState.SUCCESS, 
payload.getStatus().getStatus());
+    Assertions.assertNull(payload.getStatus().getErrorReport());
+  }
+}
diff --git 
a/extensions-core/kubernetes-overlord-extensions/src/test/java/org/apache/druid/k8s/overlord/MultipleKubernetesTaskRunnerFactoryTest.java
 
b/extensions-core/kubernetes-overlord-extensions/src/test/java/org/apache/druid/k8s/overlord/MultipleKubernetesTaskRunnerFactoryTest.java
index 7d0c3f20a6b..160c3eabd9e 100644
--- 
a/extensions-core/kubernetes-overlord-extensions/src/test/java/org/apache/druid/k8s/overlord/MultipleKubernetesTaskRunnerFactoryTest.java
+++ 
b/extensions-core/kubernetes-overlord-extensions/src/test/java/org/apache/druid/k8s/overlord/MultipleKubernetesTaskRunnerFactoryTest.java
@@ -209,7 +209,7 @@ public class MultipleKubernetesTaskRunnerFactoryTest
         new NoopServiceEmitter(),
         () -> null,
         configManager,
-        new TaskConfig(null, null, false, null, null, null, false, null, 
false, null, false),
+        new TaskConfig(null, null, false, null, null, null, false, null, 
false, null, false, null),
         new StartupLoggingConfig(),
         new DruidNode("test-overlord", "localhost", false, 8080, null, true, 
false),
         new DruidKubernetesVertxHttpClientFactory(new 
DruidKubernetesVertxHttpClientConfig(), objectMapper)
diff --git 
a/indexing-service/src/main/java/org/apache/druid/indexing/common/SegmentCacheManagerFactory.java
 
b/indexing-service/src/main/java/org/apache/druid/indexing/common/SegmentCacheManagerFactory.java
index 402e4be10c5..9897338f9fa 100644
--- 
a/indexing-service/src/main/java/org/apache/druid/indexing/common/SegmentCacheManagerFactory.java
+++ 
b/indexing-service/src/main/java/org/apache/druid/indexing/common/SegmentCacheManagerFactory.java
@@ -37,7 +37,6 @@ import org.apache.druid.segment.loading.StorageLocationConfig;
 import org.apache.druid.timeline.DataSegment;
 
 import java.io.File;
-import java.util.Collections;
 import java.util.List;
 
 /**
@@ -47,6 +46,11 @@ public class SegmentCacheManagerFactory
 {
   private final IndexIO indexIO;
   private final ObjectMapper jsonMapper;
+  /**
+   * The injected {@code druid.segmentCache} config. Per-task caches are 
derived from it (see {@link #manufacturate})
+   * so operator-tuned virtual-storage settings.
+   */
+  private final SegmentLoaderConfig segmentLoaderConfig;
   /**
    * Resolved lazily (only on the {@code virtualStorage=true} path of {@link 
#manufacturate}) so that injecting this
    * factory into a component that only builds non-virtual caches (e.g. {@code 
DruidInputSource}) does not force the
@@ -59,11 +63,13 @@ public class SegmentCacheManagerFactory
   public SegmentCacheManagerFactory(
       IndexIO indexIO,
       @Json ObjectMapper mapper,
+      SegmentLoaderConfig segmentLoaderConfig,
       @EphemeralStorageLoading Provider<StorageLoadingThreadPool> 
loadingThreadPoolProvider
   )
   {
     this.indexIO = indexIO;
     this.jsonMapper = mapper;
+    this.segmentLoaderConfig = segmentLoaderConfig;
     this.loadingThreadPoolProvider = loadingThreadPoolProvider;
   }
 
@@ -75,10 +81,12 @@ public class SegmentCacheManagerFactory
    */
   public static SegmentCacheManagerFactory createWithOwnedPool(IndexIO 
indexIO, ObjectMapper mapper)
   {
+    final SegmentLoaderConfig defaults = SegmentLoaderConfig.builder().build();
     return new SegmentCacheManagerFactory(
         indexIO,
         mapper,
-        Providers.of(StorageLoadingThreadPool.createFromConfig(new 
SegmentLoaderConfig().setVirtualStorage(true)))
+        defaults,
+        
Providers.of(StorageLoadingThreadPool.createFromConfig(defaults.toEphemeralVirtualStorage()))
     );
   }
 
@@ -88,24 +96,41 @@ public class SegmentCacheManagerFactory
    * {@link EphemeralStorageLoading} loading pool (shared across all per-task 
caches and stopped by the lifecycle)
    * rather than creating its own.
    *
-   * @param storageDir     storage location
-   * @param maxSize        size limit, or null for no limit
-   * @param virtualStorage whether to configure the cache manager in ephemeral 
virtual storage mode. In this mode,
-   *                       loading is triggered by {@link 
SegmentCacheManager#acquireSegment(DataSegment, AcquireMode)},
-   *                       and segment files are deleted as soon as all holds 
are closed.
+   * @param storageDir              storage location
+   * @param maxSize                 size limit, or null for no limit
+   * @param virtualStorage          whether to configure the cache manager in 
ephemeral virtual storage mode. In this
+   *                                mode, loading is triggered by
+   *                                {@link 
SegmentCacheManager#acquireSegment(DataSegment, AcquireMode)}, and segment
+   *                                files are deleted as soon as all holds are 
closed.
+   * @param partialDownloadsEnabled when true (and {@code virtualStorage} is 
true), partial-eligible segments are read
+   *                                via on-demand column downloads rather than 
downloaded in full up front. Has no
+   *                                effect when {@code virtualStorage} is 
false.
    */
-  public SegmentCacheManager manufacturate(File storageDir, Long maxSize, 
boolean virtualStorage)
+  public SegmentCacheManager manufacturate(
+      File storageDir,
+      Long maxSize,
+      boolean virtualStorage,
+      boolean partialDownloadsEnabled
+  )
   {
     final StorageLocationConfig locationConfig = new StorageLocationConfig(
         storageDir,
         maxSize != null ? maxSize : Long.MAX_VALUE,
         null
     );
+    // For virtual storage, derive an ephemeral virtual-storage cache config 
from the injected node config: this keeps
+    // its virtual-storage tuning while dropping the classic on-disk-cache 
settings that don't apply. A non-virtual
+    // (eager) per-task cache needs none of that tuning and must NOT inherit 
the node's transient settings, so it
+    // starts from fresh defaults instead. Both then get the per-task location 
and mode flags.
+    final SegmentLoaderConfig.Builder loaderConfigBuilder =
+        virtualStorage ? 
segmentLoaderConfig.toEphemeralVirtualStorage().toBuilder() : 
SegmentLoaderConfig.builder();
     final SegmentLoaderConfig loaderConfig =
-        new SegmentLoaderConfig()
-            .setLocations(Collections.singletonList(locationConfig))
-            .setVirtualStorage(virtualStorage)
-            .setVirtualStorageIsEphemeral(virtualStorage);
+        loaderConfigBuilder
+            .locations(locationConfig)
+            .virtualStorage(virtualStorage)
+            .virtualStorageIsEphemeral(virtualStorage)
+            .virtualStoragePartialDownloadsEnabled(partialDownloadsEnabled)
+            .build();
     final List<StorageLocation> storageLocations = 
loaderConfig.toStorageLocations();
     return new SegmentLocalCacheManager(
         storageLocations,
diff --git 
a/indexing-service/src/main/java/org/apache/druid/indexing/common/TaskToolboxFactory.java
 
b/indexing-service/src/main/java/org/apache/druid/indexing/common/TaskToolboxFactory.java
index 6ee32438d49..655e3399e2a 100644
--- 
a/indexing-service/src/main/java/org/apache/druid/indexing/common/TaskToolboxFactory.java
+++ 
b/indexing-service/src/main/java/org/apache/druid/indexing/common/TaskToolboxFactory.java
@@ -45,6 +45,7 @@ import 
org.apache.druid.indexing.worker.shuffle.IntermediaryDataManager;
 import org.apache.druid.java.util.emitter.service.ServiceEmitter;
 import org.apache.druid.java.util.metrics.MonitorScheduler;
 import org.apache.druid.query.DruidProcessingConfig;
+import org.apache.druid.query.QueryContext;
 import org.apache.druid.query.QueryProcessingPool;
 import org.apache.druid.query.QueryRunnerFactoryConglomerate;
 import org.apache.druid.query.policy.PolicyEnforcer;
@@ -247,7 +248,16 @@ public class TaskToolboxFactory
         .queryProcessingPool(queryProcessingPool)
         .joinableFactory(joinableFactory)
         .monitorSchedulerProvider(monitorSchedulerProvider)
-        
.segmentCacheManager(segmentCacheManagerFactory.manufacturate(taskWorkDir, 
null, true))
+        .segmentCacheManager(segmentCacheManagerFactory.manufacturate(
+            taskWorkDir,
+            null,
+            true,
+            // getBoolean (not the raw getContextValue) so a string 
"true"/"false" is parsed
+            QueryContext.of(task.getContext()).getBoolean(
+                Tasks.VIRTUAL_STORAGE_PARTIAL_DOWNLOADS_KEY,
+                config.isVirtualStoragePartialDownloadsEnabled()
+            )
+        ))
         .jsonMapper(jsonMapper)
         .taskWorkDir(taskWorkDir)
         .indexIO(indexIO)
diff --git 
a/indexing-service/src/main/java/org/apache/druid/indexing/common/config/TaskConfig.java
 
b/indexing-service/src/main/java/org/apache/druid/indexing/common/config/TaskConfig.java
index d043fa7c825..1b65b9f6843 100644
--- 
a/indexing-service/src/main/java/org/apache/druid/indexing/common/config/TaskConfig.java
+++ 
b/indexing-service/src/main/java/org/apache/druid/indexing/common/config/TaskConfig.java
@@ -45,6 +45,7 @@ public class TaskConfig implements TaskDirectory
   private static final Period DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT = new 
Period("PT5M");
   private static final boolean DEFAULT_STORE_EMPTY_COLUMNS = true;
   private static final long DEFAULT_TMP_STORAGE_BYTES_PER_TASK = -1;
+  private static final boolean 
DEFAULT_VIRTUAL_STORAGE_PARTIAL_DOWNLOADS_ENABLED = true;
 
   @JsonProperty
   private final String baseDir;
@@ -79,6 +80,9 @@ public class TaskConfig implements TaskDirectory
   @JsonProperty
   private final boolean buildV10;
 
+  @JsonProperty
+  private final boolean virtualStoragePartialDownloadsEnabled;
+
   @JsonCreator
   public TaskConfig(
       @JsonProperty("baseDir") String baseDir,
@@ -91,7 +95,8 @@ public class TaskConfig implements TaskDirectory
       @JsonProperty("storeEmptyColumns") @Nullable Boolean storeEmptyColumns,
       @JsonProperty("encapsulatedTask") boolean enableTaskLevelLogPush,
       @JsonProperty("tmpStorageBytesPerTask") @Nullable Long 
tmpStorageBytesPerTask,
-      @JsonProperty("buildV10") boolean buildV10
+      @JsonProperty("buildV10") boolean buildV10,
+      @JsonProperty("virtualStoragePartialDownloadsEnabled") @Nullable Boolean 
virtualStoragePartialDownloadsEnabled
   )
   {
     this.baseDir = Configs.valueOrDefault(baseDir, 
System.getProperty("java.io.tmpdir"));
@@ -118,6 +123,10 @@ public class TaskConfig implements TaskDirectory
     this.storeEmptyColumns = Configs.valueOrDefault(storeEmptyColumns, 
DEFAULT_STORE_EMPTY_COLUMNS);
     this.tmpStorageBytesPerTask = 
Configs.valueOrDefault(tmpStorageBytesPerTask, 
DEFAULT_TMP_STORAGE_BYTES_PER_TASK);
     this.buildV10 = buildV10;
+    this.virtualStoragePartialDownloadsEnabled = Configs.valueOrDefault(
+        virtualStoragePartialDownloadsEnabled,
+        DEFAULT_VIRTUAL_STORAGE_PARTIAL_DOWNLOADS_ENABLED
+    );
   }
 
   private TaskConfig(
@@ -131,7 +140,8 @@ public class TaskConfig implements TaskDirectory
       boolean storeEmptyColumns,
       boolean encapsulatedTask,
       long tmpStorageBytesPerTask,
-      boolean buildV10
+      boolean buildV10,
+      boolean virtualStoragePartialDownloadsEnabled
   )
   {
     this.baseDir = baseDir;
@@ -145,6 +155,7 @@ public class TaskConfig implements TaskDirectory
     this.encapsulatedTask = encapsulatedTask;
     this.tmpStorageBytesPerTask = tmpStorageBytesPerTask;
     this.buildV10 = buildV10;
+    this.virtualStoragePartialDownloadsEnabled = 
virtualStoragePartialDownloadsEnabled;
   }
 
   @JsonProperty
@@ -243,6 +254,12 @@ public class TaskConfig implements TaskDirectory
     return buildV10;
   }
 
+  @JsonProperty
+  public boolean isVirtualStoragePartialDownloadsEnabled()
+  {
+    return virtualStoragePartialDownloadsEnabled;
+  }
+
   private String defaultDir(@Nullable String configParameter, final String 
defaultVal)
   {
     if (configParameter == null) {
@@ -265,7 +282,8 @@ public class TaskConfig implements TaskDirectory
         storeEmptyColumns,
         encapsulatedTask,
         tmpStorageBytesPerTask,
-        buildV10
+        buildV10,
+        virtualStoragePartialDownloadsEnabled
     );
   }
 
@@ -282,7 +300,8 @@ public class TaskConfig implements TaskDirectory
         storeEmptyColumns,
         encapsulatedTask,
         tmpStorageBytesPerTask,
-        buildV10
+        buildV10,
+        virtualStoragePartialDownloadsEnabled
     );
   }
 }
diff --git 
a/indexing-service/src/main/java/org/apache/druid/indexing/common/task/Tasks.java
 
b/indexing-service/src/main/java/org/apache/druid/indexing/common/task/Tasks.java
index 7f64a6cbad0..5e09198828e 100644
--- 
a/indexing-service/src/main/java/org/apache/druid/indexing/common/task/Tasks.java
+++ 
b/indexing-service/src/main/java/org/apache/druid/indexing/common/task/Tasks.java
@@ -55,6 +55,7 @@ public class Tasks
   public static final String SUB_TASK_TIMEOUT_KEY = "subTaskTimeoutMillis";
   public static final String FORCE_TIME_CHUNK_LOCK_KEY = "forceTimeChunkLock";
   public static final String STORE_EMPTY_COLUMNS_KEY = "storeEmptyColumns";
+  public static final String VIRTUAL_STORAGE_PARTIAL_DOWNLOADS_KEY = 
"virtualStoragePartialDownloadsEnabled";
   public static final String USE_SHARED_LOCK = "useSharedLock";
   public static final String TASK_LOCK_TYPE = "taskLockType";
   public static final String USE_CONCURRENT_LOCKS = "useConcurrentLocks";
diff --git 
a/indexing-service/src/main/java/org/apache/druid/indexing/input/DruidInputSource.java
 
b/indexing-service/src/main/java/org/apache/druid/indexing/input/DruidInputSource.java
index 84fead9916b..917936c03c5 100644
--- 
a/indexing-service/src/main/java/org/apache/druid/indexing/input/DruidInputSource.java
+++ 
b/indexing-service/src/main/java/org/apache/druid/indexing/input/DruidInputSource.java
@@ -314,7 +314,7 @@ public class DruidInputSource extends AbstractInputSource 
implements SplittableI
   protected InputSourceReader fixedFormatReader(InputRowSchema inputRowSchema, 
@Nullable File temporaryDirectory)
   {
     final SegmentCacheManager segmentCacheManager =
-        segmentCacheManagerFactory.manufacturate(temporaryDirectory, null, 
false);
+        segmentCacheManagerFactory.manufacturate(temporaryDirectory, null, 
false, false);
 
     final List<TimelineObjectHolder<String, DataSegment>> timeline = 
createTimeline();
     final Iterator<DruidSegmentInputEntity> entityIterator = FluentIterable
diff --git 
a/indexing-service/src/test/java/org/apache/druid/indexing/common/SegmentCacheManagerFactoryTest.java
 
b/indexing-service/src/test/java/org/apache/druid/indexing/common/SegmentCacheManagerFactoryTest.java
index 87622425794..b24f9e6f084 100644
--- 
a/indexing-service/src/test/java/org/apache/druid/indexing/common/SegmentCacheManagerFactoryTest.java
+++ 
b/indexing-service/src/test/java/org/apache/druid/indexing/common/SegmentCacheManagerFactoryTest.java
@@ -19,6 +19,7 @@
 
 package org.apache.druid.indexing.common;
 
+import com.fasterxml.jackson.databind.InjectableValues;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.inject.Provider;
 import com.google.inject.util.Providers;
@@ -27,8 +28,10 @@ import org.apache.druid.segment.TestHelper;
 import org.apache.druid.segment.TestIndex;
 import org.apache.druid.segment.loading.SegmentCacheManager;
 import org.apache.druid.segment.loading.SegmentLoaderConfig;
+import org.apache.druid.segment.loading.SegmentLocalCacheManager;
 import org.apache.druid.segment.loading.StorageLoadingThreadPool;
 import org.apache.druid.server.metrics.NoopServiceEmitter;
+import org.apache.druid.utils.RuntimeInfo;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.BeforeEach;
@@ -60,12 +63,17 @@ class SegmentCacheManagerFactoryTest
   void testVirtualStorageManagersShareTheInjectedPool()
   {
     final StorageLoadingThreadPool shared =
-        StorageLoadingThreadPool.createFromConfig(new 
SegmentLoaderConfig().withVirtualStorage(true));
+        
StorageLoadingThreadPool.createFromConfig(SegmentLoaderConfig.builder().build().toEphemeralVirtualStorage());
     try {
       final SegmentCacheManagerFactory factory =
-          new SegmentCacheManagerFactory(TestIndex.INDEX_IO, jsonMapper, 
Providers.of(shared));
-      final SegmentCacheManager m1 = factory.manufacturate(new File(tempDir, 
"a"), null, true);
-      final SegmentCacheManager m2 = factory.manufacturate(new File(tempDir, 
"b"), null, true);
+          new SegmentCacheManagerFactory(
+              TestIndex.INDEX_IO,
+              jsonMapper,
+              SegmentLoaderConfig.builder().build(),
+              Providers.of(shared)
+          );
+      final SegmentCacheManager m1 = factory.manufacturate(new File(tempDir, 
"a"), null, true, false);
+      final SegmentCacheManager m2 = factory.manufacturate(new File(tempDir, 
"b"), null, true, false);
 
       Assertions.assertSame(shared, m1.getLoadingThreadPool());
       Assertions.assertSame(shared, m2.getLoadingThreadPool());
@@ -84,19 +92,111 @@ class SegmentCacheManagerFactoryTest
       throw new AssertionError("ephemeral loading pool must not be resolved 
for virtualStorage=false");
     };
     final SegmentCacheManagerFactory factory =
-        new SegmentCacheManagerFactory(TestIndex.INDEX_IO, jsonMapper, 
throwingProvider);
+        new SegmentCacheManagerFactory(
+            TestIndex.INDEX_IO,
+            jsonMapper,
+            SegmentLoaderConfig.builder().build(),
+            throwingProvider
+        );
 
-    final SegmentCacheManager m = factory.manufacturate(new File(tempDir, 
"c"), null, false);
+    final SegmentCacheManager m = factory.manufacturate(new File(tempDir, 
"c"), null, false, false);
     Assertions.assertFalse(m.getLoadingThreadPool().isAvailable());
   }
 
+  @Test
+  void testManufacturateDerivesPerTaskConfigFromInjectedConfig() throws 
Exception
+  {
+    // The injected druid.segmentCache config carries operator-tuned 
virtual-storage settings and (typical of an
+    // indexing node) is not itself in virtual-storage mode. Deriving per-task 
caches from it must preserve that tuning.
+    final ObjectMapper mapper = TestHelper.makeJsonMapper();
+    mapper.setInjectableValues(new 
InjectableValues.Std().addValue(RuntimeInfo.class, new RuntimeInfo()));
+    final SegmentLoaderConfig injected = mapper.readValue(
+        "{\"virtualStorageMetadataReservationEstimate\": 99999999,"
+        + " \"virtualStorageCoalesceGapBytes\": 65536,"
+        + " \"virtualStorageMaxFetchRunBytes\": 8388608,"
+        + " \"infoDir\": \"/var/druid/segment-cache/info_dir\","
+        + " \"numThreadsToLoadSegmentsIntoPageCacheOnDownload\": 4}",
+        SegmentLoaderConfig.class
+    );
+
+    final StorageLoadingThreadPool shared =
+        
StorageLoadingThreadPool.createFromConfig(SegmentLoaderConfig.builder().build().toEphemeralVirtualStorage());
+    try {
+      final SegmentCacheManagerFactory factory =
+          new SegmentCacheManagerFactory(TestIndex.INDEX_IO, jsonMapper, 
injected, Providers.of(shared));
+      final SegmentLocalCacheManager m =
+          (SegmentLocalCacheManager) factory.manufacturate(new File(tempDir, 
"task"), null, true, true);
+      final SegmentLoaderConfig derived = m.getConfig();
+
+      // Operator-tuned settings are carried over from the injected config...
+      Assertions.assertEquals(99999999L, 
derived.getVirtualStorageMetadataReservationEstimate());
+      Assertions.assertEquals(65536L, 
derived.getVirtualStorageCoalesceGapBytes());
+      Assertions.assertEquals(8388608L, 
derived.getVirtualStorageMaxFetchRunBytes());
+      // ...while the per-task location and mode flags are overridden.
+      Assertions.assertTrue(derived.isVirtualStorage());
+      Assertions.assertTrue(derived.isVirtualStorageEphemeral());
+      Assertions.assertTrue(derived.isVirtualStoragePartialDownloadsEnabled());
+      Assertions.assertEquals(1, derived.getLocations().size());
+      Assertions.assertEquals(new File(tempDir, "task"), 
derived.getLocations().get(0).getPath());
+      // ...and settings incompatible with virtual storage are cleared (no 
shared info dir, no page-cache warming).
+      Assertions.assertNull(derived.getInfoDir());
+      Assertions.assertEquals(0, 
derived.getNumThreadsToLoadSegmentsIntoPageCacheOnDownload());
+
+      // The shared injected config is not mutated by the derive.
+      Assertions.assertFalse(injected.isVirtualStorage());
+      Assertions.assertTrue(injected.getLocations().isEmpty());
+    }
+    finally {
+      shared.stop();
+    }
+  }
+
+  @Test
+  void testNonVirtualManufacturateUsesFreshDefaultsNotInjectedConfig() throws 
Exception
+  {
+    // A non-virtual (eager) per-task cache must NOT inherit the node's 
transient settings: a positive
+    // numThreadsToLoadSegmentsIntoPageCacheOnDownload would leak a per-reader 
executor (input entities drop segments
+    // but never shut the manager down), and deleteOnRemove=false would leave 
downloaded segments behind. It starts
+    // from fresh defaults instead.
+    final ObjectMapper mapper = TestHelper.makeJsonMapper();
+    mapper.setInjectableValues(new 
InjectableValues.Std().addValue(RuntimeInfo.class, new RuntimeInfo()));
+    final SegmentLoaderConfig injected = mapper.readValue(
+        "{\"numThreadsToLoadSegmentsIntoPageCacheOnDownload\": 4,"
+        + " \"deleteOnRemove\": false,"
+        + " \"lazyLoadOnStart\": true,"
+        + " \"infoDir\": \"/var/druid/segment-cache/info_dir\"}",
+        SegmentLoaderConfig.class
+    );
+
+    // The ephemeral loading pool must not even be resolved for a non-virtual 
cache.
+    final Provider<StorageLoadingThreadPool> throwingProvider = () -> {
+      throw new AssertionError("ephemeral loading pool must not be resolved 
for virtualStorage=false");
+    };
+    final SegmentCacheManagerFactory factory =
+        new SegmentCacheManagerFactory(TestIndex.INDEX_IO, jsonMapper, 
injected, throwingProvider);
+
+    final SegmentLocalCacheManager m =
+        (SegmentLocalCacheManager) factory.manufacturate(new File(tempDir, 
"eager"), null, false, false);
+    final SegmentLoaderConfig derived = m.getConfig();
+
+    // Transient settings are the safe defaults, not the injected node's 
values.
+    Assertions.assertFalse(derived.isVirtualStorage());
+    Assertions.assertEquals(0, 
derived.getNumThreadsToLoadSegmentsIntoPageCacheOnDownload());
+    Assertions.assertTrue(derived.isDeleteOnRemove());
+    Assertions.assertNull(derived.getInfoDir());
+    Assertions.assertFalse(derived.isLazyLoadOnStart());
+    // The per-task location is still applied.
+    Assertions.assertEquals(1, derived.getLocations().size());
+    Assertions.assertEquals(new File(tempDir, "eager"), 
derived.getLocations().get(0).getPath());
+  }
+
   @Test
   void testCreateWithOwnedPoolBuildsOneAvailablePoolPerFactory()
   {
     final SegmentCacheManagerFactory factory =
         SegmentCacheManagerFactory.createWithOwnedPool(TestIndex.INDEX_IO, 
jsonMapper);
-    final SegmentCacheManager m1 = factory.manufacturate(new File(tempDir, 
"d"), null, true);
-    final SegmentCacheManager m2 = factory.manufacturate(new File(tempDir, 
"e"), null, true);
+    final SegmentCacheManager m1 = factory.manufacturate(new File(tempDir, 
"d"), null, true, false);
+    final SegmentCacheManager m2 = factory.manufacturate(new File(tempDir, 
"e"), null, true, false);
     try {
       Assertions.assertTrue(m1.getLoadingThreadPool().isAvailable());
       // createWithOwnedPool builds one pool and shares it across the 
factory's manufacturate calls.
diff --git 
a/indexing-service/src/test/java/org/apache/druid/indexing/common/TaskToolboxTest.java
 
b/indexing-service/src/test/java/org/apache/druid/indexing/common/TaskToolboxTest.java
index d83ea288586..5933f57bdaf 100644
--- 
a/indexing-service/src/test/java/org/apache/druid/indexing/common/TaskToolboxTest.java
+++ 
b/indexing-service/src/test/java/org/apache/druid/indexing/common/TaskToolboxTest.java
@@ -20,6 +20,7 @@
 package org.apache.druid.indexing.common;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.ImmutableMap;
 import org.apache.druid.client.cache.Cache;
 import org.apache.druid.client.cache.CacheConfig;
 import org.apache.druid.client.cache.CachePopulatorStats;
@@ -109,6 +110,9 @@ public class TaskToolboxTest
     EasyMock.expect(task.getId()).andReturn("task_id").anyTimes();
     EasyMock.expect(task.getDataSource()).andReturn("task_ds").anyTimes();
     EasyMock.expect(task.getContextValue(Tasks.STORE_EMPTY_COLUMNS_KEY, 
true)).andReturn(true).anyTimes();
+    EasyMock.expect(task.getContext())
+            
.andReturn(ImmutableMap.of(Tasks.VIRTUAL_STORAGE_PARTIAL_DOWNLOADS_KEY, 
"false"))
+            .anyTimes();
     IndexMergerV9 indexMergerV9 = EasyMock.createMock(IndexMergerV9.class);
     
EasyMock.expect(mockIndexMergerV9.create(true)).andReturn(indexMergerV9).anyTimes();
     EasyMock.replay(task, mockHandoffNotifierFactory, mockIndexMergerV9);
diff --git 
a/indexing-service/src/test/java/org/apache/druid/indexing/common/config/TaskConfigBuilder.java
 
b/indexing-service/src/test/java/org/apache/druid/indexing/common/config/TaskConfigBuilder.java
index 69f05e763a0..ecfbdbbd393 100644
--- 
a/indexing-service/src/test/java/org/apache/druid/indexing/common/config/TaskConfigBuilder.java
+++ 
b/indexing-service/src/test/java/org/apache/druid/indexing/common/config/TaskConfigBuilder.java
@@ -37,6 +37,7 @@ public class TaskConfigBuilder
   private boolean enableTaskLevelLogPush;
   private Long tmpStorageBytesPerTask;
   private boolean buildV10;
+  private Boolean virtualStoragePartialDownloadsEnabled;
 
   public TaskConfigBuilder setBaseDir(String baseDir)
   {
@@ -104,6 +105,12 @@ public class TaskConfigBuilder
     return this;
   }
 
+  public TaskConfigBuilder setVirtualStoragePartialDownloadsEnabled(Boolean 
virtualStoragePartialDownloadsEnabled)
+  {
+    this.virtualStoragePartialDownloadsEnabled = 
virtualStoragePartialDownloadsEnabled;
+    return this;
+  }
+
   public TaskConfig build()
   {
     return new TaskConfig(
@@ -117,7 +124,8 @@ public class TaskConfigBuilder
         storeEmptyColumns,
         enableTaskLevelLogPush,
         tmpStorageBytesPerTask,
-        buildV10
+        buildV10,
+        virtualStoragePartialDownloadsEnabled
     );
   }
 }
diff --git 
a/indexing-service/src/test/java/org/apache/druid/indexing/common/config/TaskConfigTest.java
 
b/indexing-service/src/test/java/org/apache/druid/indexing/common/config/TaskConfigTest.java
new file mode 100644
index 00000000000..b1a4d0e3b79
--- /dev/null
+++ 
b/indexing-service/src/test/java/org/apache/druid/indexing/common/config/TaskConfigTest.java
@@ -0,0 +1,45 @@
+/*
+ * 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.druid.indexing.common.config;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.druid.jackson.DefaultObjectMapper;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class TaskConfigTest
+{
+  private final ObjectMapper jsonMapper = new DefaultObjectMapper();
+
+  @Test
+  void testVirtualStoragePartialDownloadsEnabledDefaultsToTrue() throws 
Exception
+  {
+    final TaskConfig config = jsonMapper.readValue("{}", TaskConfig.class);
+    Assertions.assertTrue(config.isVirtualStoragePartialDownloadsEnabled());
+  }
+
+  @Test
+  void testVirtualStoragePartialDownloadsEnabledFromJson() throws Exception
+  {
+    final TaskConfig config =
+        jsonMapper.readValue("{\"virtualStoragePartialDownloadsEnabled\": 
false}", TaskConfig.class);
+    Assertions.assertFalse(config.isVirtualStoragePartialDownloadsEnabled());
+  }
+}
diff --git 
a/indexing-service/src/test/java/org/apache/druid/indexing/common/task/CompactionTaskRunBase.java
 
b/indexing-service/src/test/java/org/apache/druid/indexing/common/task/CompactionTaskRunBase.java
index 267cc906881..65948d2f405 100644
--- 
a/indexing-service/src/test/java/org/apache/druid/indexing/common/task/CompactionTaskRunBase.java
+++ 
b/indexing-service/src/test/java/org/apache/druid/indexing/common/task/CompactionTaskRunBase.java
@@ -1268,7 +1268,12 @@ public abstract class CompactionTaskRunBase
     Assert.assertEquals(new NumberedShardSpec(0, 1), 
segments.get(0).getShardSpec());
 
     final File cacheDir = temporaryFolder.newFolder();
-    final SegmentCacheManager segmentCacheManager = 
segmentCacheManagerFactory.manufacturate(cacheDir, null, false);
+    final SegmentCacheManager segmentCacheManager = 
segmentCacheManagerFactory.manufacturate(
+        cacheDir,
+        null,
+        false,
+        false
+    );
 
     List<String> rowsFromSegment = new ArrayList<>();
     for (DataSegment segment : segments) {
@@ -1380,7 +1385,12 @@ public abstract class CompactionTaskRunBase
     Assert.assertEquals(new NumberedShardSpec(0, 1), 
segments.get(0).getShardSpec());
 
     final File cacheDir = temporaryFolder.newFolder();
-    final SegmentCacheManager segmentCacheManager = 
segmentCacheManagerFactory.manufacturate(cacheDir, null, false);
+    final SegmentCacheManager segmentCacheManager = 
segmentCacheManagerFactory.manufacturate(
+        cacheDir,
+        null,
+        false,
+        false
+    );
 
     List<String> rowsFromSegment = new ArrayList<>();
     for (DataSegment segment : segments) {
@@ -1497,7 +1507,12 @@ public abstract class CompactionTaskRunBase
     Assert.assertEquals(new NumberedShardSpec(0, 1), 
compactSegment.getShardSpec());
 
     final File cacheDir = temporaryFolder.newFolder();
-    final SegmentCacheManager segmentCacheManager = 
segmentCacheManagerFactory.manufacturate(cacheDir, null, false);
+    final SegmentCacheManager segmentCacheManager = 
segmentCacheManagerFactory.manufacturate(
+        cacheDir,
+        null,
+        false,
+        false
+    );
 
     List<String> rowsFromSegment = new ArrayList<>();
     segmentCacheManager.load(compactSegment);
@@ -1695,26 +1710,11 @@ public abstract class CompactionTaskRunBase
 
   private TaskToolbox createTaskToolbox(ObjectMapper objectMapper, 
TaskActionClient taskActionClient) throws IOException
   {
-    final SegmentLoaderConfig loaderConfig = new SegmentLoaderConfig()
-    {
-      @Override
-      public List<StorageLocationConfig> getLocations()
-      {
-        return ImmutableList.of(new StorageLocationConfig(localDeepStorage, 
null, null));
-      }
-
-      @Override
-      public boolean isVirtualStorage()
-      {
-        return true;
-      }
-
-      @Override
-      public boolean isVirtualStorageEphemeral()
-      {
-        return true;
-      }
-    };
+    final SegmentLoaderConfig loaderConfig = SegmentLoaderConfig.builder()
+        .locations(new StorageLocationConfig(localDeepStorage, null, null))
+        .virtualStorage(true)
+        .virtualStorageIsEphemeral(true)
+        .build();
     final List<StorageLocation> storageLocations = 
loaderConfig.toStorageLocations();
     final SegmentCacheManager cacheManager = new SegmentLocalCacheManager(
         storageLocations,
@@ -1754,7 +1754,12 @@ public abstract class CompactionTaskRunBase
   protected List<String> getCSVFormatRowsFromSegments(List<DataSegment> 
segments) throws Exception
   {
     final File cacheDir = temporaryFolder.newFolder();
-    final SegmentCacheManager segmentCacheManager = 
segmentCacheManagerFactory.manufacturate(cacheDir, null, false);
+    final SegmentCacheManager segmentCacheManager = 
segmentCacheManagerFactory.manufacturate(
+        cacheDir,
+        null,
+        false,
+        false
+    );
 
     List<String> rowsFromSegment = new ArrayList<>();
     for (DataSegment segment : segments) {
diff --git 
a/indexing-service/src/test/java/org/apache/druid/indexing/common/task/IndexTaskTest.java
 
b/indexing-service/src/test/java/org/apache/druid/indexing/common/task/IndexTaskTest.java
index 399cb9a513e..1aedb848dc5 100644
--- 
a/indexing-service/src/test/java/org/apache/druid/indexing/common/task/IndexTaskTest.java
+++ 
b/indexing-service/src/test/java/org/apache/druid/indexing/common/task/IndexTaskTest.java
@@ -206,16 +206,9 @@ public class IndexTaskTest extends IngestionTestBase
   {
     final File cacheDir = temporaryFolder.newFolder();
     tmpDir = temporaryFolder.newFolder();
-    final SegmentLoaderConfig loaderConfig = new SegmentLoaderConfig()
-    {
-      @Override
-      public List<StorageLocationConfig> getLocations()
-      {
-        return Collections.singletonList(
-            new StorageLocationConfig(cacheDir, null, null)
-        );
-      }
-    };
+    final SegmentLoaderConfig loaderConfig = SegmentLoaderConfig.builder()
+        .locations(new StorageLocationConfig(cacheDir, null, null))
+        .build();
     final List<StorageLocation> storageLocations = 
loaderConfig.toStorageLocations();
     segmentCacheManager = new SegmentLocalCacheManager(
         storageLocations,
diff --git 
a/indexing-service/src/test/java/org/apache/druid/indexing/common/task/IngestionTestBase.java
 
b/indexing-service/src/test/java/org/apache/druid/indexing/common/task/IngestionTestBase.java
index eb0f1c0b653..aa38dc731db 100644
--- 
a/indexing-service/src/test/java/org/apache/druid/indexing/common/task/IngestionTestBase.java
+++ 
b/indexing-service/src/test/java/org/apache/druid/indexing/common/task/IngestionTestBase.java
@@ -227,7 +227,7 @@ public abstract class IngestionTestBase extends 
InitializedNullHandlingTest
 
   public SegmentCacheManager newSegmentLoader(File storageDir)
   {
-    return segmentCacheManagerFactory.manufacturate(storageDir, null, true);
+    return segmentCacheManagerFactory.manufacturate(storageDir, null, true, 
false);
   }
 
   public ObjectMapper getObjectMapper()
diff --git 
a/indexing-service/src/test/java/org/apache/druid/indexing/common/task/batch/parallel/AbstractMultiPhaseParallelIndexingTest.java
 
b/indexing-service/src/test/java/org/apache/druid/indexing/common/task/batch/parallel/AbstractMultiPhaseParallelIndexingTest.java
index ef0a730e932..8154da3536c 100644
--- 
a/indexing-service/src/test/java/org/apache/druid/indexing/common/task/batch/parallel/AbstractMultiPhaseParallelIndexingTest.java
+++ 
b/indexing-service/src/test/java/org/apache/druid/indexing/common/task/batch/parallel/AbstractMultiPhaseParallelIndexingTest.java
@@ -258,7 +258,7 @@ abstract class AbstractMultiPhaseParallelIndexingTest 
extends AbstractParallelIn
   private Segment loadSegment(DataSegment dataSegment, File tempSegmentDir)
   {
     final SegmentCacheManager cacheManager = 
SegmentCacheManagerFactory.createWithOwnedPool(TestIndex.INDEX_IO, 
getObjectMapper())
-        .manufacturate(tempSegmentDir, null, false);
+        .manufacturate(tempSegmentDir, null, false, false);
     try {
       cacheManager.load(dataSegment);
       return cacheManager.acquireCachedSegment(dataSegment.getId(), 
AcquireMode.FULL).orElseThrow();
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/IndexerWorkerContext.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/IndexerWorkerContext.java
index 3e1c151e5f2..241241bcae0 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/IndexerWorkerContext.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/IndexerWorkerContext.java
@@ -174,6 +174,11 @@ public class IndexerWorkerContext implements WorkerContext
   {
     final IndexIO indexIO = injector.getInstance(IndexIO.class);
     final TaskConfig taskConfig = injector.getInstance(TaskConfig.class);
+    // Opt into on-demand partial downloads via the MSQ query context (falls 
back to the node's TaskConfig default).
+    final boolean partialDownloadsEnabled = 
MultiStageQueryContext.getVirtualStoragePartialDownloadsEnabled(
+        QueryContext.of(task.getContext()),
+        taskConfig.isVirtualStoragePartialDownloadsEnabled()
+    );
     final SegmentCacheManager cacheManager =
         injector.getInstance(SegmentCacheManagerFactory.class)
                 .manufacturate(
@@ -181,7 +186,8 @@ public class IndexerWorkerContext implements WorkerContext
                     // Divide tmpStorageBytesPerTask by 3 so the local cache 
never takes up the majority of space.
                     // In a typical leaf stage run, we may need some disk 
space for inputs and some for outputs.
                     taskConfig.getTmpStorageBytesPerTask() > 0 ? 
taskConfig.getTmpStorageBytesPerTask() / 3 : null,
-                    true
+                    true,
+                    partialDownloadsEnabled
                 );
     final SegmentManager segmentManager = new SegmentManager(cacheManager);
     final VirtualStorageManager virtualStorageManager =
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/util/MultiStageQueryContext.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/util/MultiStageQueryContext.java
index 114e70a7132..cf840e7bc03 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/util/MultiStageQueryContext.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/util/MultiStageQueryContext.java
@@ -235,6 +235,13 @@ public class MultiStageQueryContext
   public static final String CTX_INCLUDE_ALL_COUNTERS = "includeAllCounters";
   public static final boolean DEFAULT_INCLUDE_ALL_COUNTERS = true;
 
+  /**
+   * Whether worker tasks read input segments via on-demand partial 
(per-column) downloads instead of downloading each
+   * segment in full. Aliases the task-context key {@link 
Tasks#VIRTUAL_STORAGE_PARTIAL_DOWNLOADS_KEY} so it can be set
+   * on the MSQ query context; when unset it falls back to the node's {@code 
TaskConfig} default.
+   */
+  public static final String CTX_VIRTUAL_STORAGE_PARTIAL_DOWNLOADS = 
Tasks.VIRTUAL_STORAGE_PARTIAL_DOWNLOADS_KEY;
+
   /**
    * Whether workers should send live counter updates to the controller via 
the message relay. When enabled, workers
    * periodically send counter snapshots to the controller, allowing the 
controller to have more up-to-date progress
@@ -322,6 +329,14 @@ public class MultiStageQueryContext
     );
   }
 
+  public static boolean getVirtualStoragePartialDownloadsEnabled(
+      final QueryContext queryContext,
+      final boolean defaultValue
+  )
+  {
+    return queryContext.getBoolean(CTX_VIRTUAL_STORAGE_PARTIAL_DOWNLOADS, 
defaultValue);
+  }
+
   public static boolean isDurableStorageEnabled(final QueryContext 
queryContext)
   {
     return queryContext.getBoolean(
diff --git 
a/multi-stage-query/src/test/java/org/apache/druid/msq/input/RegularLoadableSegmentTest.java
 
b/multi-stage-query/src/test/java/org/apache/druid/msq/input/RegularLoadableSegmentTest.java
index 003d8a6275c..9fa2630f424 100644
--- 
a/multi-stage-query/src/test/java/org/apache/druid/msq/input/RegularLoadableSegmentTest.java
+++ 
b/multi-stage-query/src/test/java/org/apache/druid/msq/input/RegularLoadableSegmentTest.java
@@ -24,7 +24,6 @@ import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonTypeName;
 import com.fasterxml.jackson.databind.InjectableValues;
 import com.fasterxml.jackson.databind.ObjectMapper;
-import com.google.common.collect.ImmutableList;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
@@ -164,11 +163,12 @@ class RegularLoadableSegmentTest extends 
InitializedNullHandlingTest
 
     // SegmentManager with virtualStorage for dynamically-loaded data tests
     cacheDir = tempDir.resolve("cache").toFile();
-    final SegmentLoaderConfig virtualLoaderConfig = new SegmentLoaderConfig()
-        .setLocations(ImmutableList.of(new StorageLocationConfig(cacheDir, 
10_000_000_000L, null)))
-        .setVirtualStorage(true)
-        .setVirtualStorageIsEphemeral(true)
-        .setVirtualStoragePartialDownloadsEnabled(true);
+    final SegmentLoaderConfig virtualLoaderConfig = 
SegmentLoaderConfig.builder()
+        .locations(new StorageLocationConfig(cacheDir, 10_000_000_000L, null))
+        .virtualStorage(true)
+        .virtualStorageIsEphemeral(true)
+        .virtualStoragePartialDownloadsEnabled(true)
+        .build();
     final List<StorageLocation> virtualLocations = 
virtualLoaderConfig.toStorageLocations();
     segmentManagerDynamic = new SegmentManager(
         new SegmentLocalCacheManager(
@@ -183,8 +183,9 @@ class RegularLoadableSegmentTest extends 
InitializedNullHandlingTest
 
     // SegmentManager without virtualStorage for pre-loaded data tests
     preLoadCacheDir = tempDir.resolve("localCache").toFile();
-    final SegmentLoaderConfig localLoaderConfig = new SegmentLoaderConfig()
-        .setLocations(ImmutableList.of(new 
StorageLocationConfig(preLoadCacheDir, 10_000_000_000L, null)));
+    final SegmentLoaderConfig localLoaderConfig = SegmentLoaderConfig.builder()
+        .locations(new StorageLocationConfig(preLoadCacheDir, 10_000_000_000L, 
null))
+        .build();
     final List<StorageLocation> localLocations = 
localLoaderConfig.toStorageLocations();
     segmentManagerPreLoad = new SegmentManager(
         new SegmentLocalCacheManager(
diff --git 
a/multi-stage-query/src/test/java/org/apache/druid/msq/test/CalciteMSQTestsHelper.java
 
b/multi-stage-query/src/test/java/org/apache/druid/msq/test/CalciteMSQTestsHelper.java
index 15d74d025c4..5827a71c50f 100644
--- 
a/multi-stage-query/src/test/java/org/apache/druid/msq/test/CalciteMSQTestsHelper.java
+++ 
b/multi-stage-query/src/test/java/org/apache/druid/msq/test/CalciteMSQTestsHelper.java
@@ -95,7 +95,7 @@ public class CalciteMSQTestsHelper
     public SegmentCacheManager provideSegmentCacheManager(ObjectMapper 
testMapper, TempDirProducer tempDirProducer)
     {
       return 
SegmentCacheManagerFactory.createWithOwnedPool(TestIndex.INDEX_IO, testMapper)
-          .manufacturate(tempDirProducer.newTempFolder("test"), null, true);
+          .manufacturate(tempDirProducer.newTempFolder("test"), null, true, 
false);
     }
 
     @Provides
diff --git 
a/multi-stage-query/src/test/java/org/apache/druid/msq/test/MSQTestBase.java 
b/multi-stage-query/src/test/java/org/apache/druid/msq/test/MSQTestBase.java
index 19ae4829f59..f0381b25c23 100644
--- a/multi-stage-query/src/test/java/org/apache/druid/msq/test/MSQTestBase.java
+++ b/multi-stage-query/src/test/java/org/apache/druid/msq/test/MSQTestBase.java
@@ -474,7 +474,8 @@ public class MSQTestBase extends BaseCalciteQueryTest
     indexIO = new IndexIO(objectMapper, ColumnConfig.DEFAULT);
 
     segmentCacheManager =
-        SegmentCacheManagerFactory.createWithOwnedPool(indexIO, 
objectMapper).manufacturate(newTempFolder("cacheManager"), null, true);
+        SegmentCacheManagerFactory.createWithOwnedPool(indexIO, objectMapper)
+                                  
.manufacturate(newTempFolder("cacheManager"), null, true, false);
 
     testSegmentManager = new TestSegmentManager();
 
diff --git 
a/multi-stage-query/src/test/java/org/apache/druid/msq/util/MultiStageQueryContextTest.java
 
b/multi-stage-query/src/test/java/org/apache/druid/msq/util/MultiStageQueryContextTest.java
index 6db3597d632..44e05c0a81a 100644
--- 
a/multi-stage-query/src/test/java/org/apache/druid/msq/util/MultiStageQueryContextTest.java
+++ 
b/multi-stage-query/src/test/java/org/apache/druid/msq/util/MultiStageQueryContextTest.java
@@ -470,6 +470,30 @@ public class MultiStageQueryContextTest
     Assert.assertEquals(Integer.valueOf(4), 
MultiStageQueryContext.getMaxThreads(QueryContext.of(propertyMap)));
   }
 
+  @Test
+  public void getVirtualStoragePartialDownloadsEnabled_unset_returnsDefault()
+  {
+    
Assert.assertFalse(MultiStageQueryContext.getVirtualStoragePartialDownloadsEnabled(QueryContext.empty(),
 false));
+    
Assert.assertTrue(MultiStageQueryContext.getVirtualStoragePartialDownloadsEnabled(QueryContext.empty(),
 true));
+  }
+
+  @Test
+  public void getVirtualStoragePartialDownloadsEnabled_set_overridesDefault()
+  {
+    Assert.assertTrue(
+        MultiStageQueryContext.getVirtualStoragePartialDownloadsEnabled(
+            
QueryContext.of(ImmutableMap.of(MultiStageQueryContext.CTX_VIRTUAL_STORAGE_PARTIAL_DOWNLOADS,
 true)),
+            false
+        )
+    );
+    Assert.assertFalse(
+        MultiStageQueryContext.getVirtualStoragePartialDownloadsEnabled(
+            
QueryContext.of(ImmutableMap.of(MultiStageQueryContext.CTX_VIRTUAL_STORAGE_PARTIAL_DOWNLOADS,
 false)),
+            true
+        )
+    );
+  }
+
   @Test
   public void withCommonContext_noTimeout_setsStartTimeOnly()
   {
diff --git a/server/src/main/java/org/apache/druid/guice/StorageNodeModule.java 
b/server/src/main/java/org/apache/druid/guice/StorageNodeModule.java
index 24d4e360c1e..8feeb2cc25b 100644
--- a/server/src/main/java/org/apache/druid/guice/StorageNodeModule.java
+++ b/server/src/main/java/org/apache/druid/guice/StorageNodeModule.java
@@ -146,7 +146,7 @@ public class StorageNodeModule implements Module
   {
     // Force virtual-storage mode: this pool serves per-task caches even when 
the node itself is not in virtual-storage
     // mode. Threads are created lazily, so an unused pool on a non-task 
process is cheap.
-    return 
StorageLoadingThreadPool.createFromConfig(config.withVirtualStorage(true));
+    return 
StorageLoadingThreadPool.createFromConfig(config.toEphemeralVirtualStorage());
   }
 
   @Provides
diff --git 
a/server/src/main/java/org/apache/druid/segment/loading/SegmentLoaderConfig.java
 
b/server/src/main/java/org/apache/druid/segment/loading/SegmentLoaderConfig.java
index b9c9f0804fe..75d1264ea05 100644
--- 
a/server/src/main/java/org/apache/druid/segment/loading/SegmentLoaderConfig.java
+++ 
b/server/src/main/java/org/apache/druid/segment/loading/SegmentLoaderConfig.java
@@ -20,63 +20,74 @@
 package org.apache.druid.segment.loading;
 
 import com.fasterxml.jackson.annotation.JacksonInject;
+import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
-import com.google.common.collect.Lists;
+import org.apache.druid.common.config.Configs;
 import org.apache.druid.segment.file.PartialSegmentFileMapperV10;
 import org.apache.druid.utils.RuntimeInfo;
 
+import javax.annotation.Nullable;
 import java.io.File;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
 /**
- *
+ * Configuration for a local segment cache, bound from {@code 
druid.segmentCache}.
  */
 public class SegmentLoaderConfig
 {
-  @JacksonInject
-  private final RuntimeInfo runtimeInfo = new RuntimeInfo();
-
-  @JsonProperty
-  private List<StorageLocationConfig> locations = Collections.emptyList();
-
+  private static final boolean DEFAULT_LAZY_LOAD_ON_START = false;
+  private static final boolean DEFAULT_DELETE_ON_REMOVE = true;
+  private static final int DEFAULT_DROP_SEGMENT_DELAY_MILLIS = (int) 
TimeUnit.SECONDS.toMillis(30);
+  private static final int DEFAULT_ANNOUNCE_INTERVAL_MILLIS = 0; // do not 
background announce
+  private static final int 
DEFAULT_NUM_THREADS_TO_LOAD_INTO_PAGE_CACHE_ON_DOWNLOAD = 0;
+  private static final int DEFAULT_STATUS_QUEUE_MAX_SIZE = 100;
+  private static final boolean DEFAULT_VIRTUAL_STORAGE = false;
+  private static final boolean DEFAULT_VIRTUAL_STORAGE_USE_VIRTUAL_THREADS = 
true;
+  private static final boolean DEFAULT_VIRTUAL_STORAGE_IS_EPHEMERAL = false;
+  private static final long 
DEFAULT_VIRTUAL_STORAGE_METADATA_RESERVATION_ESTIMATE = 16L * 1024L * 1024L;
+  private static final boolean 
DEFAULT_VIRTUAL_STORAGE_PARTIAL_DOWNLOADS_ENABLED = false;
+
+  private final RuntimeInfo runtimeInfo;
+
+  @JsonProperty("locations")
+  private final List<StorageLocationConfig> locations;
   @JsonProperty("lazyLoadOnStart")
-  private boolean lazyLoadOnStart = false;
-
+  private final boolean lazyLoadOnStart;
   @JsonProperty("deleteOnRemove")
-  private boolean deleteOnRemove = true;
-
+  private final boolean deleteOnRemove;
   @JsonProperty("dropSegmentDelayMillis")
-  private int dropSegmentDelayMillis = (int) TimeUnit.SECONDS.toMillis(30);
-
+  private final int dropSegmentDelayMillis;
   @JsonProperty("announceIntervalMillis")
-  private int announceIntervalMillis = 0; // do not background announce
-
+  private final int announceIntervalMillis;
   @JsonProperty("numLoadingThreads")
-  private int numLoadingThreads = Math.max(1, 
runtimeInfo.getAvailableProcessors() / 6);
-
+  private final int numLoadingThreads;
+  /**
+   * Nullable so {@link #getNumBootstrapThreads()} can fall back to {@link 
#numLoadingThreads} when unset.
+   */
   @JsonProperty("numBootstrapThreads")
-  private Integer numBootstrapThreads = null;
-
+  @Nullable
+  private final Integer numBootstrapThreads;
   @JsonProperty("numThreadsToLoadSegmentsIntoPageCacheOnDownload")
-  private int numThreadsToLoadSegmentsIntoPageCacheOnDownload = 0;
-
+  private final int numThreadsToLoadSegmentsIntoPageCacheOnDownload;
+  /**
+   * Nullable so {@link 
#getNumThreadsToLoadSegmentsIntoPageCacheOnBootstrap()} can fall back to the 
on-download value.
+   */
   @JsonProperty("numThreadsToLoadSegmentsIntoPageCacheOnBootstrap")
-  private Integer numThreadsToLoadSegmentsIntoPageCacheOnBootstrap = null;
-
-  @JsonProperty
-  private File infoDir = null;
-
-  @JsonProperty
-  private int statusQueueMaxSize = 100;
-
+  @Nullable
+  private final Integer numThreadsToLoadSegmentsIntoPageCacheOnBootstrap;
+  @JsonProperty("infoDir")
+  @Nullable
+  private final File infoDir;
+  @JsonProperty("statusQueueMaxSize")
+  private final int statusQueueMaxSize;
   @JsonProperty("virtualStorage")
-  private boolean virtualStorage = false;
-
+  private final boolean virtualStorage;
   @JsonProperty("virtualStorageLoadThreads")
-  private int virtualStorageLoadThreads = Math.max(32, 4 * 
runtimeInfo.getAvailableProcessors());
+  private final int virtualStorageLoadThreads;
 
   /**
    * When true (the default), the on-demand load executor uses one virtual 
thread per task with a {@link
@@ -85,15 +96,15 @@ public class SegmentLoaderConfig
    * particular deep storage SDK or workload.
    */
   @JsonProperty("virtualStorageUseVirtualThreads")
-  private boolean virtualStorageUseVirtualThreads = true;
+  private final boolean virtualStorageUseVirtualThreads;
 
   /**
    * When enabled, weakly-held cache entries are evicted immediately upon 
release of all holds, rather than
    * waiting for space pressure to trigger eviction. This setting is not 
intended to be configured directly by
-   * administrators. Instead, it is expected to be set when appropriate via 
{@link #setVirtualStorage}.
+   * administrators. Instead, it is expected to be set when appropriate via 
{@link Builder#virtualStorageIsEphemeral}.
    */
   @JsonProperty("virtualStorageIsEphemeral")
-  private boolean virtualStorageIsEphemeral = false;
+  private final boolean virtualStorageIsEphemeral;
 
   /**
    * Up-front size reservation (in bytes) used when mounting a partial-segment 
metadata cache entry. The entry
@@ -104,7 +115,7 @@ public class SegmentLoaderConfig
    * need a higher value.
    */
   @JsonProperty("virtualStorageMetadataReservationEstimate")
-  private long virtualStorageMetadataReservationEstimate = 16L * 1024L * 1024L;
+  private final long virtualStorageMetadataReservationEstimate;
 
   /**
    * When true, partial-eligible V10 segments are mounted via the partial 
machinery and
@@ -113,7 +124,7 @@ public class SegmentLoaderConfig
    * {@link AcquireMode#FULL} so the entire segment is downloaded up front 
(matching pre-partial-download behavior).
    */
   @JsonProperty("virtualStoragePartialDownloadsEnabled")
-  private boolean virtualStoragePartialDownloadsEnabled = false;
+  private final boolean virtualStoragePartialDownloadsEnabled;
 
   /**
    * Maximum number of unrequested bytes a partial-download range read will 
fetch in order to bridge two requested
@@ -124,7 +135,7 @@ public class SegmentLoaderConfig
    * request latency vs streaming throughput.
    */
   @JsonProperty("virtualStorageCoalesceGapBytes")
-  private long virtualStorageCoalesceGapBytes = 
PartialSegmentFileMapperV10.DEFAULT_COALESCE_GAP_BYTES;
+  private final long virtualStorageCoalesceGapBytes;
 
   /**
    * Maximum size of a single range read in a query-driven partial download. 
Larger fetches split at internal-file
@@ -133,36 +144,82 @@ public class SegmentLoaderConfig
    * containers sequentially and are unaffected. {@code <= 0} disables 
splitting. Defaults to 64 MiB.
    */
   @JsonProperty("virtualStorageMaxFetchRunBytes")
-  private long virtualStorageMaxFetchRunBytes = 
PartialSegmentFileMapperV10.DEFAULT_MAX_FETCH_RUN_BYTES;
-
-  private long combinedMaxSize = 0;
-
-  public SegmentLoaderConfig()
-  {
-  }
-
-  private SegmentLoaderConfig(SegmentLoaderConfig other)
+  private final long virtualStorageMaxFetchRunBytes;
+
+  @JsonCreator
+  public SegmentLoaderConfig(
+      @JacksonInject @Nullable RuntimeInfo runtimeInfo,
+      @JsonProperty("locations") @Nullable List<StorageLocationConfig> 
locations,
+      @JsonProperty("lazyLoadOnStart") @Nullable Boolean lazyLoadOnStart,
+      @JsonProperty("deleteOnRemove") @Nullable Boolean deleteOnRemove,
+      @JsonProperty("dropSegmentDelayMillis") @Nullable Integer 
dropSegmentDelayMillis,
+      @JsonProperty("announceIntervalMillis") @Nullable Integer 
announceIntervalMillis,
+      @JsonProperty("numLoadingThreads") @Nullable Integer numLoadingThreads,
+      @JsonProperty("numBootstrapThreads") @Nullable Integer 
numBootstrapThreads,
+      @JsonProperty("numThreadsToLoadSegmentsIntoPageCacheOnDownload") 
@Nullable Integer numThreadsToLoadSegmentsIntoPageCacheOnDownload,
+      @JsonProperty("numThreadsToLoadSegmentsIntoPageCacheOnBootstrap") 
@Nullable Integer numThreadsToLoadSegmentsIntoPageCacheOnBootstrap,
+      @JsonProperty("infoDir") @Nullable File infoDir,
+      @JsonProperty("statusQueueMaxSize") @Nullable Integer statusQueueMaxSize,
+      @JsonProperty("virtualStorage") @Nullable Boolean virtualStorage,
+      @JsonProperty("virtualStorageLoadThreads") @Nullable Integer 
virtualStorageLoadThreads,
+      @JsonProperty("virtualStorageUseVirtualThreads") @Nullable Boolean 
virtualStorageUseVirtualThreads,
+      @JsonProperty("virtualStorageIsEphemeral") @Nullable Boolean 
virtualStorageIsEphemeral,
+      @JsonProperty("virtualStorageMetadataReservationEstimate") @Nullable 
Long virtualStorageMetadataReservationEstimate,
+      @JsonProperty("virtualStoragePartialDownloadsEnabled") @Nullable Boolean 
virtualStoragePartialDownloadsEnabled,
+      @JsonProperty("virtualStorageCoalesceGapBytes") @Nullable Long 
virtualStorageCoalesceGapBytes,
+      @JsonProperty("virtualStorageMaxFetchRunBytes") @Nullable Long 
virtualStorageMaxFetchRunBytes
+  )
   {
-    this.locations = other.locations;
-    this.lazyLoadOnStart = other.lazyLoadOnStart;
-    this.deleteOnRemove = other.deleteOnRemove;
-    this.dropSegmentDelayMillis = other.dropSegmentDelayMillis;
-    this.announceIntervalMillis = other.announceIntervalMillis;
-    this.numLoadingThreads = other.numLoadingThreads;
-    this.numBootstrapThreads = other.numBootstrapThreads;
-    this.numThreadsToLoadSegmentsIntoPageCacheOnDownload = 
other.numThreadsToLoadSegmentsIntoPageCacheOnDownload;
-    this.numThreadsToLoadSegmentsIntoPageCacheOnBootstrap = 
other.numThreadsToLoadSegmentsIntoPageCacheOnBootstrap;
-    this.infoDir = other.infoDir;
-    this.statusQueueMaxSize = other.statusQueueMaxSize;
-    this.virtualStorage = other.virtualStorage;
-    this.virtualStorageLoadThreads = other.virtualStorageLoadThreads;
-    this.virtualStorageUseVirtualThreads = 
other.virtualStorageUseVirtualThreads;
-    this.virtualStorageIsEphemeral = other.virtualStorageIsEphemeral;
-    this.virtualStorageMetadataReservationEstimate = 
other.virtualStorageMetadataReservationEstimate;
-    this.virtualStoragePartialDownloadsEnabled = 
other.virtualStoragePartialDownloadsEnabled;
-    this.virtualStorageCoalesceGapBytes = other.virtualStorageCoalesceGapBytes;
-    this.virtualStorageMaxFetchRunBytes = other.virtualStorageMaxFetchRunBytes;
-    this.combinedMaxSize = other.combinedMaxSize;
+    // RuntimeInfo is an injected @LazySingleton (test-overridable); fall back 
to a fresh instance when it is not
+    // available, e.g. when deserializing outside a Guice context. Only used 
to size the thread-count defaults.
+    final RuntimeInfo resolvedRuntimeInfo = runtimeInfo == null ? new 
RuntimeInfo() : runtimeInfo;
+    this.runtimeInfo = resolvedRuntimeInfo;
+    this.locations = locations == null ? Collections.emptyList() : 
List.copyOf(locations);
+    this.lazyLoadOnStart = Configs.valueOrDefault(lazyLoadOnStart, 
DEFAULT_LAZY_LOAD_ON_START);
+    this.deleteOnRemove = Configs.valueOrDefault(deleteOnRemove, 
DEFAULT_DELETE_ON_REMOVE);
+    this.dropSegmentDelayMillis = 
Configs.valueOrDefault(dropSegmentDelayMillis, 
DEFAULT_DROP_SEGMENT_DELAY_MILLIS);
+    this.announceIntervalMillis = 
Configs.valueOrDefault(announceIntervalMillis, 
DEFAULT_ANNOUNCE_INTERVAL_MILLIS);
+    this.numLoadingThreads = Configs.valueOrDefault(
+        numLoadingThreads,
+        Math.max(1, resolvedRuntimeInfo.getAvailableProcessors() / 6)
+    );
+    this.numBootstrapThreads = numBootstrapThreads;
+    this.numThreadsToLoadSegmentsIntoPageCacheOnDownload = 
Configs.valueOrDefault(
+        numThreadsToLoadSegmentsIntoPageCacheOnDownload,
+        DEFAULT_NUM_THREADS_TO_LOAD_INTO_PAGE_CACHE_ON_DOWNLOAD
+    );
+    this.numThreadsToLoadSegmentsIntoPageCacheOnBootstrap = 
numThreadsToLoadSegmentsIntoPageCacheOnBootstrap;
+    this.infoDir = infoDir;
+    this.statusQueueMaxSize = Configs.valueOrDefault(statusQueueMaxSize, 
DEFAULT_STATUS_QUEUE_MAX_SIZE);
+    this.virtualStorage = Configs.valueOrDefault(virtualStorage, 
DEFAULT_VIRTUAL_STORAGE);
+    this.virtualStorageLoadThreads = Configs.valueOrDefault(
+        virtualStorageLoadThreads,
+        Math.max(32, 4 * resolvedRuntimeInfo.getAvailableProcessors())
+    );
+    this.virtualStorageUseVirtualThreads = Configs.valueOrDefault(
+        virtualStorageUseVirtualThreads,
+        DEFAULT_VIRTUAL_STORAGE_USE_VIRTUAL_THREADS
+    );
+    this.virtualStorageIsEphemeral = Configs.valueOrDefault(
+        virtualStorageIsEphemeral,
+        DEFAULT_VIRTUAL_STORAGE_IS_EPHEMERAL
+    );
+    this.virtualStorageMetadataReservationEstimate = Configs.valueOrDefault(
+        virtualStorageMetadataReservationEstimate,
+        DEFAULT_VIRTUAL_STORAGE_METADATA_RESERVATION_ESTIMATE
+    );
+    this.virtualStoragePartialDownloadsEnabled = Configs.valueOrDefault(
+        virtualStoragePartialDownloadsEnabled,
+        DEFAULT_VIRTUAL_STORAGE_PARTIAL_DOWNLOADS_ENABLED
+    );
+    this.virtualStorageCoalesceGapBytes = Configs.valueOrDefault(
+        virtualStorageCoalesceGapBytes,
+        PartialSegmentFileMapperV10.DEFAULT_COALESCE_GAP_BYTES
+    );
+    this.virtualStorageMaxFetchRunBytes = Configs.valueOrDefault(
+        virtualStorageMaxFetchRunBytes,
+        PartialSegmentFileMapperV10.DEFAULT_MAX_FETCH_RUN_BYTES
+    );
   }
 
   public List<StorageLocationConfig> getLocations()
@@ -212,6 +269,7 @@ public class SegmentLoaderConfig
            numThreadsToLoadSegmentsIntoPageCacheOnBootstrap;
   }
 
+  @Nullable
   public File getInfoDir()
   {
     return infoDir;
@@ -224,10 +282,7 @@ public class SegmentLoaderConfig
 
   public long getCombinedMaxSize()
   {
-    if (combinedMaxSize == 0) {
-      combinedMaxSize = 
getLocations().stream().mapToLong(StorageLocationConfig::getMaxSize).sum();
-    }
-    return combinedMaxSize;
+    return 
getLocations().stream().mapToLong(StorageLocationConfig::getMaxSize).sum();
   }
 
   public boolean isVirtualStorage()
@@ -270,53 +325,29 @@ public class SegmentLoaderConfig
     return virtualStorageMaxFetchRunBytes;
   }
 
-  public SegmentLoaderConfig setLocations(List<StorageLocationConfig> 
locations)
-  {
-    this.locations = Lists.newArrayList(locations);
-    return this;
-  }
-
-  public SegmentLoaderConfig setVirtualStoragePartialDownloadsEnabled(boolean 
enabled)
-  {
-    this.virtualStoragePartialDownloadsEnabled = enabled;
-    return this;
-  }
-
   /**
-   * Sets {@link #virtualStorage}.
+   * Returns a copy of this config configured as an ephemeral, on-demand 
virtual-storage cache:
+   * {@link #isVirtualStorage()} and {@link #isVirtualStorageEphemeral()} are 
set, and the settings that only apply to
+   * a classic on-disk historical cache are dropped. Virtual-storage tuning 
({@link #getVirtualStorageLoadThreads()},
+   * {@link #isVirtualStorageUseVirtualThreads()}, {@link 
#getVirtualStorageMetadataReservationEstimate()}, the
+   * coalescing/fetch-run limits) is preserved, so a per-task cache derived 
from a node's {@code druid.segmentCache}
+   * config keeps operator tuning.
    */
-  public SegmentLoaderConfig setVirtualStorage(boolean virtualStorage)
+  public SegmentLoaderConfig toEphemeralVirtualStorage()
   {
-    this.virtualStorage = virtualStorage;
-    return this;
-  }
-
-  /**
-   * Returns a copy of this config with {@link #virtualStorage} set to {@code 
virtualStorage}. All other settings
-   * (notably {@link #getVirtualStorageLoadThreads()} and {@link 
#isVirtualStorageUseVirtualThreads()}) are preserved.
-   * Used to derive an always-virtual config for the shared ephemeral 
on-demand loading pool from a node config that
-   * may not itself run in virtual-storage mode.
-   */
-  public SegmentLoaderConfig withVirtualStorage(boolean virtualStorage)
-  {
-    final SegmentLoaderConfig copy = new SegmentLoaderConfig(this);
-    copy.virtualStorage = virtualStorage;
-    return copy;
-  }
-
-  /**
-   * Sets {@link #virtualStorageIsEphemeral}.
-   */
-  public SegmentLoaderConfig setVirtualStorageIsEphemeral(boolean 
virtualStorageFabricEphemeral)
-  {
-    this.virtualStorageIsEphemeral = virtualStorageFabricEphemeral;
-    return this;
+    return toBuilder()
+        .virtualStorage(true)
+        .virtualStorageIsEphemeral(true)
+        .lazyLoadOnStart(false)
+        .infoDir(null)
+        .numThreadsToLoadSegmentsIntoPageCacheOnDownload(0)
+        .numThreadsToLoadSegmentsIntoPageCacheOnBootstrap(null)
+        .deleteOnRemove(true)
+        .build();
   }
 
   /**
    * Convert a list of {@link StorageLocationConfig} objects to {@link 
StorageLocation} objects.
-   * <p>
-   * Note: {@link #getLocations} is called instead of variable access because 
some testcases overrides this method
    */
   public List<StorageLocation> toStorageLocations()
   {
@@ -338,6 +369,31 @@ public class SegmentLoaderConfig
                .collect(Collectors.toList());
   }
 
+  public Builder toBuilder()
+  {
+    return new Builder()
+        .runtimeInfo(runtimeInfo)
+        .locations(locations)
+        .lazyLoadOnStart(lazyLoadOnStart)
+        .deleteOnRemove(deleteOnRemove)
+        .dropSegmentDelayMillis(dropSegmentDelayMillis)
+        .announceIntervalMillis(announceIntervalMillis)
+        .numLoadingThreads(numLoadingThreads)
+        .numBootstrapThreads(numBootstrapThreads)
+        
.numThreadsToLoadSegmentsIntoPageCacheOnDownload(numThreadsToLoadSegmentsIntoPageCacheOnDownload)
+        
.numThreadsToLoadSegmentsIntoPageCacheOnBootstrap(numThreadsToLoadSegmentsIntoPageCacheOnBootstrap)
+        .infoDir(infoDir)
+        .statusQueueMaxSize(statusQueueMaxSize)
+        .virtualStorage(virtualStorage)
+        .virtualStorageLoadThreads(virtualStorageLoadThreads)
+        .virtualStorageUseVirtualThreads(virtualStorageUseVirtualThreads)
+        .virtualStorageIsEphemeral(virtualStorageIsEphemeral)
+        
.virtualStorageMetadataReservationEstimate(virtualStorageMetadataReservationEstimate)
+        
.virtualStoragePartialDownloadsEnabled(virtualStoragePartialDownloadsEnabled)
+        .virtualStorageCoalesceGapBytes(virtualStorageCoalesceGapBytes)
+        .virtualStorageMaxFetchRunBytes(virtualStorageMaxFetchRunBytes);
+  }
+
   @Override
   public String toString()
   {
@@ -361,7 +417,212 @@ public class SegmentLoaderConfig
            ", virtualStoragePartialDownloadsEnabled=" + 
virtualStoragePartialDownloadsEnabled +
            ", virtualStorageCoalesceGapBytes=" + 
virtualStorageCoalesceGapBytes +
            ", virtualStorageMaxFetchRunBytes=" + 
virtualStorageMaxFetchRunBytes +
-           ", combinedMaxSize=" + combinedMaxSize +
            '}';
   }
+
+  public static Builder builder()
+  {
+    return new Builder();
+  }
+
+  /**
+   * Builds a {@link SegmentLoaderConfig}. Any field left unset takes the same 
default the {@link JsonCreator}
+   * constructor applies. Obtain one via {@link SegmentLoaderConfig#builder()} 
(all defaults) or
+   * {@link SegmentLoaderConfig#toBuilder()} (a copy of an existing config).
+   */
+  public static class Builder
+  {
+    @Nullable
+    private RuntimeInfo runtimeInfo;
+    @Nullable
+    private List<StorageLocationConfig> locations;
+    @Nullable
+    private Boolean lazyLoadOnStart;
+    @Nullable
+    private Boolean deleteOnRemove;
+    @Nullable
+    private Integer dropSegmentDelayMillis;
+    @Nullable
+    private Integer announceIntervalMillis;
+    @Nullable
+    private Integer numLoadingThreads;
+    @Nullable
+    private Integer numBootstrapThreads;
+    @Nullable
+    private Integer numThreadsToLoadSegmentsIntoPageCacheOnDownload;
+    @Nullable
+    private Integer numThreadsToLoadSegmentsIntoPageCacheOnBootstrap;
+    @Nullable
+    private File infoDir;
+    @Nullable
+    private Integer statusQueueMaxSize;
+    @Nullable
+    private Boolean virtualStorage;
+    @Nullable
+    private Integer virtualStorageLoadThreads;
+    @Nullable
+    private Boolean virtualStorageUseVirtualThreads;
+    @Nullable
+    private Boolean virtualStorageIsEphemeral;
+    @Nullable
+    private Long virtualStorageMetadataReservationEstimate;
+    @Nullable
+    private Boolean virtualStoragePartialDownloadsEnabled;
+    @Nullable
+    private Long virtualStorageCoalesceGapBytes;
+    @Nullable
+    private Long virtualStorageMaxFetchRunBytes;
+
+    public Builder runtimeInfo(@Nullable RuntimeInfo runtimeInfo)
+    {
+      this.runtimeInfo = runtimeInfo;
+      return this;
+    }
+
+    public Builder locations(@Nullable List<StorageLocationConfig> locations)
+    {
+      this.locations = locations;
+      return this;
+    }
+
+    public Builder locations(StorageLocationConfig... locations)
+    {
+      this.locations = Arrays.asList(locations);
+      return this;
+    }
+
+    public Builder lazyLoadOnStart(boolean lazyLoadOnStart)
+    {
+      this.lazyLoadOnStart = lazyLoadOnStart;
+      return this;
+    }
+
+    public Builder deleteOnRemove(boolean deleteOnRemove)
+    {
+      this.deleteOnRemove = deleteOnRemove;
+      return this;
+    }
+
+    public Builder dropSegmentDelayMillis(int dropSegmentDelayMillis)
+    {
+      this.dropSegmentDelayMillis = dropSegmentDelayMillis;
+      return this;
+    }
+
+    public Builder announceIntervalMillis(int announceIntervalMillis)
+    {
+      this.announceIntervalMillis = announceIntervalMillis;
+      return this;
+    }
+
+    public Builder numLoadingThreads(int numLoadingThreads)
+    {
+      this.numLoadingThreads = numLoadingThreads;
+      return this;
+    }
+
+    public Builder numBootstrapThreads(@Nullable Integer numBootstrapThreads)
+    {
+      this.numBootstrapThreads = numBootstrapThreads;
+      return this;
+    }
+
+    public Builder numThreadsToLoadSegmentsIntoPageCacheOnDownload(int 
numThreads)
+    {
+      this.numThreadsToLoadSegmentsIntoPageCacheOnDownload = numThreads;
+      return this;
+    }
+
+    public Builder numThreadsToLoadSegmentsIntoPageCacheOnBootstrap(@Nullable 
Integer numThreads)
+    {
+      this.numThreadsToLoadSegmentsIntoPageCacheOnBootstrap = numThreads;
+      return this;
+    }
+
+    public Builder infoDir(@Nullable File infoDir)
+    {
+      this.infoDir = infoDir;
+      return this;
+    }
+
+    public Builder statusQueueMaxSize(int statusQueueMaxSize)
+    {
+      this.statusQueueMaxSize = statusQueueMaxSize;
+      return this;
+    }
+
+    public Builder virtualStorage(boolean virtualStorage)
+    {
+      this.virtualStorage = virtualStorage;
+      return this;
+    }
+
+    public Builder virtualStorageLoadThreads(int virtualStorageLoadThreads)
+    {
+      this.virtualStorageLoadThreads = virtualStorageLoadThreads;
+      return this;
+    }
+
+    public Builder virtualStorageUseVirtualThreads(boolean 
virtualStorageUseVirtualThreads)
+    {
+      this.virtualStorageUseVirtualThreads = virtualStorageUseVirtualThreads;
+      return this;
+    }
+
+    public Builder virtualStorageIsEphemeral(boolean virtualStorageIsEphemeral)
+    {
+      this.virtualStorageIsEphemeral = virtualStorageIsEphemeral;
+      return this;
+    }
+
+    public Builder virtualStorageMetadataReservationEstimate(long 
virtualStorageMetadataReservationEstimate)
+    {
+      this.virtualStorageMetadataReservationEstimate = 
virtualStorageMetadataReservationEstimate;
+      return this;
+    }
+
+    public Builder virtualStoragePartialDownloadsEnabled(boolean 
virtualStoragePartialDownloadsEnabled)
+    {
+      this.virtualStoragePartialDownloadsEnabled = 
virtualStoragePartialDownloadsEnabled;
+      return this;
+    }
+
+    public Builder virtualStorageCoalesceGapBytes(long 
virtualStorageCoalesceGapBytes)
+    {
+      this.virtualStorageCoalesceGapBytes = virtualStorageCoalesceGapBytes;
+      return this;
+    }
+
+    public Builder virtualStorageMaxFetchRunBytes(long 
virtualStorageMaxFetchRunBytes)
+    {
+      this.virtualStorageMaxFetchRunBytes = virtualStorageMaxFetchRunBytes;
+      return this;
+    }
+
+    public SegmentLoaderConfig build()
+    {
+      return new SegmentLoaderConfig(
+          runtimeInfo,
+          locations,
+          lazyLoadOnStart,
+          deleteOnRemove,
+          dropSegmentDelayMillis,
+          announceIntervalMillis,
+          numLoadingThreads,
+          numBootstrapThreads,
+          numThreadsToLoadSegmentsIntoPageCacheOnDownload,
+          numThreadsToLoadSegmentsIntoPageCacheOnBootstrap,
+          infoDir,
+          statusQueueMaxSize,
+          virtualStorage,
+          virtualStorageLoadThreads,
+          virtualStorageUseVirtualThreads,
+          virtualStorageIsEphemeral,
+          virtualStorageMetadataReservationEstimate,
+          virtualStoragePartialDownloadsEnabled,
+          virtualStorageCoalesceGapBytes,
+          virtualStorageMaxFetchRunBytes
+      );
+    }
+  }
 }
diff --git 
a/server/src/main/java/org/apache/druid/segment/loading/SegmentLocalCacheManager.java
 
b/server/src/main/java/org/apache/druid/segment/loading/SegmentLocalCacheManager.java
index b48e4202f9b..8ee13001f23 100644
--- 
a/server/src/main/java/org/apache/druid/segment/loading/SegmentLocalCacheManager.java
+++ 
b/server/src/main/java/org/apache/druid/segment/loading/SegmentLocalCacheManager.java
@@ -1595,6 +1595,12 @@ public class SegmentLocalCacheManager implements 
SegmentCacheManager
     return virtualStorageLoadingThreadPool;
   }
 
+  @VisibleForTesting
+  public SegmentLoaderConfig getConfig()
+  {
+    return config;
+  }
+
   /**
    * Checks whether a segment is already cached. This method does not confirm 
if the segment is actually mounted in
    * the location, or even that the segment files in some location are valid, 
just that some files exist in the
diff --git 
a/server/src/test/java/org/apache/druid/client/DruidServerConfigTest.java 
b/server/src/test/java/org/apache/druid/client/DruidServerConfigTest.java
index 185685daab8..258fec51b9c 100644
--- a/server/src/test/java/org/apache/druid/client/DruidServerConfigTest.java
+++ b/server/src/test/java/org/apache/druid/client/DruidServerConfigTest.java
@@ -87,7 +87,7 @@ public class DruidServerConfigTest
     locations.add(locationConfig2);
     DruidServerConfig druidServerConfig = new DruidServerConfig(
         new RuntimeInfo(),
-        new SegmentLoaderConfig().setLocations(locations)
+        SegmentLoaderConfig.builder().locations(locations).build()
     );
     Assert.assertEquals(30000000000L, druidServerConfig.getMaxSize());
   }
@@ -103,14 +103,7 @@ public class DruidServerConfigTest
         return HumanReadableBytes.parse("3GiB");
       }
     };
-    SegmentLoaderConfig segmentLoaderConfig = new SegmentLoaderConfig()
-    {
-      @Override
-      public boolean isVirtualStorage()
-      {
-        return true;
-      }
-    };
+    SegmentLoaderConfig segmentLoaderConfig = 
SegmentLoaderConfig.builder().virtualStorage(true).build();
     DruidServerConfig druidServerConfig = new DruidServerConfig(runtimeInfo, 
segmentLoaderConfig);
     Assert.assertEquals(HumanReadableBytes.parse("45TiB"), 
druidServerConfig.getMaxSize());
   }
@@ -132,7 +125,7 @@ public class DruidServerConfigTest
     mapper.setInjectableValues(new 
InjectableValues.Std().addValue(ObjectMapper.class, new DefaultObjectMapper())
                                                          .addValue(
                                                              
SegmentLoaderConfig.class,
-                                                             new 
SegmentLoaderConfig().setLocations(locations)
+                                                             
SegmentLoaderConfig.builder().locations(locations).build()
                                                          )
                                                          
.addValue(RuntimeInfo.class, new RuntimeInfo()));
 
diff --git 
a/server/src/test/java/org/apache/druid/guice/StorageNodeModuleTest.java 
b/server/src/test/java/org/apache/druid/guice/StorageNodeModuleTest.java
index 06c01e1ad5a..f45d49a7a2c 100644
--- a/server/src/test/java/org/apache/druid/guice/StorageNodeModuleTest.java
+++ b/server/src/test/java/org/apache/druid/guice/StorageNodeModuleTest.java
@@ -174,9 +174,9 @@ public class StorageNodeModuleTest
   {
     // The qualified ephemeral loading pool must resolve from the core 
injector (StorageNodeModule is universal via
     // CoreInjectorBuilder), so SegmentCacheManagerFactory's @Inject 
constructor can be satisfied on every process.
-    // The provider forces virtual storage via config.withVirtualStorage(true) 
regardless of the node's flag.
-    Mockito.when(segmentLoaderConfig.withVirtualStorage(true))
-           .thenReturn(new SegmentLoaderConfig().setVirtualStorage(true));
+    // The provider forces virtual storage via 
config.toEphemeralVirtualStorage() regardless of the node's flag.
+    Mockito.when(segmentLoaderConfig.toEphemeralVirtualStorage())
+           
.thenReturn(SegmentLoaderConfig.builder().virtualStorage(true).build());
 
     final StorageLoadingThreadPool pool = injector().getInstance(
         Key.get(StorageLoadingThreadPool.class, EphemeralStorageLoading.class)
diff --git 
a/server/src/test/java/org/apache/druid/segment/loading/PermitLimitedSegmentRangeReaderTest.java
 
b/server/src/test/java/org/apache/druid/segment/loading/PermitLimitedSegmentRangeReaderTest.java
index 193d8474b1f..c725ec8db4d 100644
--- 
a/server/src/test/java/org/apache/druid/segment/loading/PermitLimitedSegmentRangeReaderTest.java
+++ 
b/server/src/test/java/org/apache/druid/segment/loading/PermitLimitedSegmentRangeReaderTest.java
@@ -31,14 +31,7 @@ class PermitLimitedSegmentRangeReaderTest
   private static StorageLoadingThreadPool oneThreadPool()
   {
     return StorageLoadingThreadPool.createFromConfig(
-        new SegmentLoaderConfig()
-        {
-          @Override
-          public int getVirtualStorageLoadThreads()
-          {
-            return 1;
-          }
-        }.setVirtualStorage(true)
+        
SegmentLoaderConfig.builder().virtualStorageLoadThreads(1).virtualStorage(true).build()
     );
   }
 
diff --git 
a/server/src/test/java/org/apache/druid/segment/loading/SegmentLoaderConfigTest.java
 
b/server/src/test/java/org/apache/druid/segment/loading/SegmentLoaderConfigTest.java
index a3db293b6ad..eb520c2169a 100644
--- 
a/server/src/test/java/org/apache/druid/segment/loading/SegmentLoaderConfigTest.java
+++ 
b/server/src/test/java/org/apache/druid/segment/loading/SegmentLoaderConfigTest.java
@@ -29,18 +29,16 @@ import org.junit.jupiter.api.Test;
 class SegmentLoaderConfigTest
 {
   @Test
-  void testSetVirtualStorage()
+  void testBuilderVirtualStorageFlags()
   {
-    final SegmentLoaderConfig config = new SegmentLoaderConfig();
-
-    // Verify default values
-    Assertions.assertFalse(config.isVirtualStorage());
-    Assertions.assertFalse(config.isVirtualStorageEphemeral());
-
-    // Set both to true
-    config.setVirtualStorage(true).setVirtualStorageIsEphemeral(true);
-
-    // Verify both fields are set
+    // Defaults are false.
+    final SegmentLoaderConfig defaults = SegmentLoaderConfig.builder().build();
+    Assertions.assertFalse(defaults.isVirtualStorage());
+    Assertions.assertFalse(defaults.isVirtualStorageEphemeral());
+
+    // The builder sets both.
+    final SegmentLoaderConfig config =
+        
SegmentLoaderConfig.builder().virtualStorage(true).virtualStorageIsEphemeral(true).build();
     Assertions.assertTrue(config.isVirtualStorage());
     Assertions.assertTrue(config.isVirtualStorageEphemeral());
   }
@@ -82,15 +80,16 @@ class SegmentLoaderConfigTest
   }
 
   @Test
-  public void testWithVirtualStorageReturnsCopyAndDoesNotMutateOriginal()
+  public void 
testToEphemeralVirtualStorageReturnsCopyAndDoesNotMutateOriginal()
   {
-    final SegmentLoaderConfig original = new SegmentLoaderConfig();
+    final SegmentLoaderConfig original = SegmentLoaderConfig.builder().build();
     Assertions.assertFalse(original.isVirtualStorage());
 
-    final SegmentLoaderConfig copy = original.withVirtualStorage(true);
+    final SegmentLoaderConfig copy = original.toEphemeralVirtualStorage();
 
-    // The copy has the flag flipped, while other settings are preserved.
+    // The copy is an ephemeral virtual-storage config, while tuning settings 
are preserved.
     Assertions.assertTrue(copy.isVirtualStorage());
+    Assertions.assertTrue(copy.isVirtualStorageEphemeral());
     Assertions.assertEquals(original.getVirtualStorageLoadThreads(), 
copy.getVirtualStorageLoadThreads());
     Assertions.assertEquals(original.isVirtualStorageUseVirtualThreads(), 
copy.isVirtualStorageUseVirtualThreads());
 
@@ -98,4 +97,43 @@ class SegmentLoaderConfigTest
     Assertions.assertNotSame(original, copy);
     Assertions.assertFalse(original.isVirtualStorage());
   }
+
+  @Test
+  void testEnablingVirtualStorageClearsIncompatibleSettingsButKeepsTuning() 
throws Exception
+  {
+    final ObjectMapper jsonMapper = new DefaultObjectMapper();
+    jsonMapper.setInjectableValues(new 
InjectableValues.Std().addValue(RuntimeInfo.class, new RuntimeInfo()));
+    final SegmentLoaderConfig node = jsonMapper.readValue(
+        "{\"lazyLoadOnStart\": true,"
+        + " \"deleteOnRemove\": false,"
+        + " \"infoDir\": \"/var/druid/segment-cache/info_dir\","
+        + " \"numThreadsToLoadSegmentsIntoPageCacheOnDownload\": 4,"
+        + " \"numThreadsToLoadSegmentsIntoPageCacheOnBootstrap\": 2,"
+        + " \"virtualStorageMetadataReservationEstimate\": 99999999,"
+        + " \"virtualStorageCoalesceGapBytes\": 65536}",
+        SegmentLoaderConfig.class
+    );
+
+    final SegmentLoaderConfig virtual = node.toEphemeralVirtualStorage();
+
+    // Classic on-disk-cache settings are reset to virtual-storage-safe 
values...
+    Assertions.assertFalse(virtual.isLazyLoadOnStart());
+    Assertions.assertTrue(virtual.isDeleteOnRemove());
+    Assertions.assertNull(virtual.getInfoDir());
+    Assertions.assertEquals(0, 
virtual.getNumThreadsToLoadSegmentsIntoPageCacheOnDownload());
+    Assertions.assertEquals(0, 
virtual.getNumThreadsToLoadSegmentsIntoPageCacheOnBootstrap());
+    // ...while virtual-storage tuning is preserved.
+    Assertions.assertTrue(virtual.isVirtualStorage());
+    Assertions.assertEquals(99999999L, 
virtual.getVirtualStorageMetadataReservationEstimate());
+    Assertions.assertEquals(65536L, 
virtual.getVirtualStorageCoalesceGapBytes());
+
+    // The original node config is untouched, and a plain toBuilder copy 
preserves everything (only the ephemeral
+    // virtual-storage derivation sanitizes).
+    Assertions.assertTrue(node.isLazyLoadOnStart());
+    Assertions.assertEquals(4, 
node.getNumThreadsToLoadSegmentsIntoPageCacheOnDownload());
+    final SegmentLoaderConfig plainCopy = node.toBuilder().build();
+    Assertions.assertTrue(plainCopy.isLazyLoadOnStart());
+    Assertions.assertFalse(plainCopy.isDeleteOnRemove());
+    Assertions.assertEquals(4, 
plainCopy.getNumThreadsToLoadSegmentsIntoPageCacheOnDownload());
+  }
 }
diff --git 
a/server/src/test/java/org/apache/druid/segment/loading/SegmentLocalCacheManagerConcurrencyTest.java
 
b/server/src/test/java/org/apache/druid/segment/loading/SegmentLocalCacheManagerConcurrencyTest.java
index 5c2b482779c..921d1597cc2 100644
--- 
a/server/src/test/java/org/apache/druid/segment/loading/SegmentLocalCacheManagerConcurrencyTest.java
+++ 
b/server/src/test/java/org/apache/druid/segment/loading/SegmentLocalCacheManagerConcurrencyTest.java
@@ -136,46 +136,16 @@ class SegmentLocalCacheManagerConcurrencyTest
     locations.add(locationConfig);
     locations.add(locationConfig2);
 
-    loaderConfig = new SegmentLoaderConfig()
-    {
-      @Override
-      public List<StorageLocationConfig> getLocations()
-      {
-        return locations;
-      }
-
-      @Override
-      public File getInfoDir()
-      {
-        return new File(tempDir, "info");
-      }
-    };
-    vsfLoaderConfig = new SegmentLoaderConfig()
-    {
-      @Override
-      public List<StorageLocationConfig> getLocations()
-      {
-        return locations;
-      }
-
-      @Override
-      public boolean isVirtualStorage()
-      {
-        return true;
-      }
-
-      @Override
-      public int getVirtualStorageLoadThreads()
-      {
-        return Runtime.getRuntime().availableProcessors();
-      }
-
-      @Override
-      public File getInfoDir()
-      {
-        return new File(tempDir, "info");
-      }
-    };
+    loaderConfig = SegmentLoaderConfig.builder()
+                                      .locations(locations)
+                                      .infoDir(new File(tempDir, "info"))
+                                      .build();
+    vsfLoaderConfig = SegmentLoaderConfig.builder()
+                                         .locations(locations)
+                                         .virtualStorage(true)
+                                         
.virtualStorageLoadThreads(Runtime.getRuntime().availableProcessors())
+                                         .infoDir(new File(tempDir, "info"))
+                                         .build();
     final List<StorageLocation> storageLocations = 
loaderConfig.toStorageLocations();
     location = storageLocations.get(0);
     location2 = storageLocations.get(1);
diff --git 
a/server/src/test/java/org/apache/druid/segment/loading/SegmentLocalCacheManagerPartialAcquireTest.java
 
b/server/src/test/java/org/apache/druid/segment/loading/SegmentLocalCacheManagerPartialAcquireTest.java
index 7a6ed49c37e..7a823904db6 100644
--- 
a/server/src/test/java/org/apache/druid/segment/loading/SegmentLocalCacheManagerPartialAcquireTest.java
+++ 
b/server/src/test/java/org/apache/druid/segment/loading/SegmentLocalCacheManagerPartialAcquireTest.java
@@ -253,10 +253,11 @@ class SegmentLocalCacheManagerPartialAcquireTest
     FileUtils.mkdirp(cacheRoot);
 
     final StorageLocationConfig locConfig = new 
StorageLocationConfig(cacheRoot, 1024L * 1024L * 1024L, null);
-    final SegmentLoaderConfig loaderConfig = new SegmentLoaderConfig()
-        .setLocations(List.of(locConfig))
-        .setVirtualStorage(true)
-        .setVirtualStoragePartialDownloadsEnabled(true);
+    final SegmentLoaderConfig loaderConfig = SegmentLoaderConfig.builder()
+        .locations(locConfig)
+        .virtualStorage(true)
+        .virtualStoragePartialDownloadsEnabled(true)
+        .build();
     final List<StorageLocation> storageLocations = 
loaderConfig.toStorageLocations();
     manager = new SegmentLocalCacheManager(
         storageLocations,
@@ -350,7 +351,7 @@ class SegmentLocalCacheManagerPartialAcquireTest
       throws ExecutionException, InterruptedException, IOException
   {
     final StorageLocation loc = manager.getLocations().get(0);
-    final long estimate = new 
SegmentLoaderConfig().getVirtualStorageMetadataReservationEstimate();
+    final long estimate = 
SegmentLoaderConfig.builder().build().getVirtualStorageMetadataReservationEstimate();
 
     // A lazy (PARTIAL) acquire mounts only the metadata entry (downloads the 
header); bundles stay lazy until a cursor
     // is built, so exactly one weak reservation + one weak load is expected.
@@ -630,10 +631,11 @@ class SegmentLocalCacheManagerPartialAcquireTest
     final File plainCacheRoot = new File(perTestTempDir, "plain_cache_" + 
ThreadLocalRandom.current().nextInt(Integer.MAX_VALUE));
     FileUtils.mkdirp(plainCacheRoot);
     final StorageLocationConfig locConfig = new 
StorageLocationConfig(plainCacheRoot, 1024L * 1024L * 1024L, null);
-    final SegmentLoaderConfig loaderConfig = new SegmentLoaderConfig()
-        .setLocations(List.of(locConfig))
-        .setVirtualStorage(true)
-        .setVirtualStoragePartialDownloadsEnabled(true);
+    final SegmentLoaderConfig loaderConfig = SegmentLoaderConfig.builder()
+        .locations(locConfig)
+        .virtualStorage(true)
+        .virtualStoragePartialDownloadsEnabled(true)
+        .build();
     final List<StorageLocation> storageLocations = 
loaderConfig.toStorageLocations();
     final SegmentLocalCacheManager plain = new SegmentLocalCacheManager(
         storageLocations,
@@ -697,10 +699,11 @@ class SegmentLocalCacheManagerPartialAcquireTest
     // eager counterparts: the returned segment is an eager 
QueryableIndexSegment (NOT PartialQueryableIndexSegment),
     // and no PartialSegmentMetadataCacheEntry is registered on the location.
     final StorageLocationConfig locConfig = new 
StorageLocationConfig(cacheRoot, 1024L * 1024L * 1024L, null);
-    final SegmentLoaderConfig disabledConfig = new SegmentLoaderConfig()
-        .setLocations(List.of(locConfig))
-        .setVirtualStorage(true)
-        .setVirtualStoragePartialDownloadsEnabled(false);
+    final SegmentLoaderConfig disabledConfig = SegmentLoaderConfig.builder()
+        .locations(locConfig)
+        .virtualStorage(true)
+        .virtualStoragePartialDownloadsEnabled(false)
+        .build();
     final List<StorageLocation> storageLocations = 
disabledConfig.toStorageLocations();
     final SegmentLocalCacheManager disabledManager = new 
SegmentLocalCacheManager(
         storageLocations,
@@ -807,10 +810,11 @@ class SegmentLocalCacheManagerPartialAcquireTest
     // A manager with partial downloads disabled (operator toggled the flag 
off) over the same cache dir must reclaim
     // the now-unusable partial layout at bootstrap rather than reserving it 
and failing at query time.
     final StorageLocationConfig locConfig = new 
StorageLocationConfig(cacheRoot, 1024L * 1024L * 1024L, null);
-    final SegmentLoaderConfig disabledConfig = new SegmentLoaderConfig()
-        .setLocations(List.of(locConfig))
-        .setVirtualStorage(true)
-        .setVirtualStoragePartialDownloadsEnabled(false);
+    final SegmentLoaderConfig disabledConfig = SegmentLoaderConfig.builder()
+        .locations(locConfig)
+        .virtualStorage(true)
+        .virtualStoragePartialDownloadsEnabled(false)
+        .build();
     final List<StorageLocation> storageLocations = 
disabledConfig.toStorageLocations();
     final SegmentLocalCacheManager disabledManager = new 
SegmentLocalCacheManager(
         storageLocations,
diff --git 
a/server/src/test/java/org/apache/druid/segment/loading/SegmentLocalCacheManagerPartialDropTest.java
 
b/server/src/test/java/org/apache/druid/segment/loading/SegmentLocalCacheManagerPartialDropTest.java
index bc4901bcb33..f0f0d227846 100644
--- 
a/server/src/test/java/org/apache/druid/segment/loading/SegmentLocalCacheManagerPartialDropTest.java
+++ 
b/server/src/test/java/org/apache/druid/segment/loading/SegmentLocalCacheManagerPartialDropTest.java
@@ -156,7 +156,7 @@ class SegmentLocalCacheManagerPartialDropTest extends 
InitializedNullHandlingTes
     FileUtils.mkdirp(infoDir);
 
     final StorageLocationConfig locConfig = new 
StorageLocationConfig(cacheDir, ESTIMATE * 16, null);
-    final SegmentLoaderConfig loaderConfig = new 
SegmentLoaderConfig().setLocations(List.of(locConfig));
+    final SegmentLoaderConfig loaderConfig = 
SegmentLoaderConfig.builder().locations(locConfig).build();
     final List<StorageLocation> locations = loaderConfig.toStorageLocations();
     manager = new SegmentLocalCacheManager(
         locations,
diff --git 
a/server/src/test/java/org/apache/druid/segment/loading/SegmentLocalCacheManagerPartialRuleLoadTest.java
 
b/server/src/test/java/org/apache/druid/segment/loading/SegmentLocalCacheManagerPartialRuleLoadTest.java
index f63563474e4..1d72d2a2ed4 100644
--- 
a/server/src/test/java/org/apache/druid/segment/loading/SegmentLocalCacheManagerPartialRuleLoadTest.java
+++ 
b/server/src/test/java/org/apache/druid/segment/loading/SegmentLocalCacheManagerPartialRuleLoadTest.java
@@ -710,10 +710,11 @@ class SegmentLocalCacheManagerPartialRuleLoadTest
     final List<StorageLocationConfig> locConfigs = locationRoots.stream()
         .map(root -> new StorageLocationConfig(root, 1024L * 1024L * 1024L, 
null))
         .toList();
-    final SegmentLoaderConfig loaderConfig = new SegmentLoaderConfig()
-        .setLocations(locConfigs)
-        .setVirtualStorage(virtualStorage)
-        .setVirtualStoragePartialDownloadsEnabled(partialDownloadsEnabled);
+    final SegmentLoaderConfig loaderConfig = SegmentLoaderConfig.builder()
+        .locations(locConfigs)
+        .virtualStorage(virtualStorage)
+        .virtualStoragePartialDownloadsEnabled(partialDownloadsEnabled)
+        .build();
     final List<StorageLocation> storageLocations = 
loaderConfig.toStorageLocations();
     return new SegmentLocalCacheManager(
         storageLocations,
diff --git 
a/server/src/test/java/org/apache/druid/segment/loading/SegmentLocalCacheManagerTest.java
 
b/server/src/test/java/org/apache/druid/segment/loading/SegmentLocalCacheManagerTest.java
index d6f96739fc2..458da17f219 100644
--- 
a/server/src/test/java/org/apache/druid/segment/loading/SegmentLocalCacheManagerTest.java
+++ 
b/server/src/test/java/org/apache/druid/segment/loading/SegmentLocalCacheManagerTest.java
@@ -118,16 +118,10 @@ public class SegmentLocalCacheManagerTest extends 
InitializedNullHandlingTest
   {
     // Only injecting config locations without locations shouldn't really be 
the case.
     // It possibly suggests an issue with injection.
-    final SegmentLoaderConfig loaderConfig = new SegmentLoaderConfig()
-    {
-      @Override
-      public List<StorageLocationConfig> getLocations()
-      {
-        return Collections.singletonList(
-            new StorageLocationConfig(localSegmentCacheDir, null, null)
-        );
-      }
-    };
+    final SegmentLoaderConfig loaderConfig =
+        SegmentLoaderConfig.builder()
+                           .locations(new 
StorageLocationConfig(localSegmentCacheDir, null, null))
+                           .build();
 
     manager = new SegmentLocalCacheManager(
         ImmutableList.of(),
@@ -146,7 +140,7 @@ public class SegmentLocalCacheManagerTest extends 
InitializedNullHandlingTest
     final ImmutableList<StorageLocation> locations = ImmutableList.of(
         new StorageLocation(localSegmentCacheDir, 10000000000L, null)
     );
-    final SegmentLoaderConfig loaderConfig = new SegmentLoaderConfig();
+    final SegmentLoaderConfig loaderConfig = 
SegmentLoaderConfig.builder().build();
     SegmentLocalCacheManager manager = new SegmentLocalCacheManager(
         locations,
         loaderConfig,
@@ -161,7 +155,7 @@ public class SegmentLocalCacheManagerTest extends 
InitializedNullHandlingTest
   @Test
   public void testCanHandleSegmentsWithEmptyLocationsAndConfigLocations()
   {
-    final SegmentLoaderConfig loaderConfig = new SegmentLoaderConfig();
+    final SegmentLoaderConfig loaderConfig = 
SegmentLoaderConfig.builder().build();
     SegmentLocalCacheManager manager = new SegmentLocalCacheManager(
         ImmutableList.of(),
         loaderConfig,
@@ -176,7 +170,7 @@ public class SegmentLocalCacheManagerTest extends 
InitializedNullHandlingTest
   @Test
   public void testGetCachedSegmentsWhenCanHandleSegmentsIsFalse()
   {
-    final SegmentLoaderConfig loaderConfig = new SegmentLoaderConfig();
+    final SegmentLoaderConfig loaderConfig = 
SegmentLoaderConfig.builder().build();
     SegmentLocalCacheManager manager = new SegmentLocalCacheManager(
         null,
         loaderConfig,
@@ -346,24 +340,13 @@ public class SegmentLocalCacheManagerTest extends 
InitializedNullHandlingTest
   }
 
   @Test
-  public void testLoadSegmentInPageCache() throws IOException, 
SegmentLoadingException
+  public void testLoadSegmentInPageCache() throws IOException
   {
-    final SegmentLoaderConfig loaderConfig = new SegmentLoaderConfig()
-    {
-      @Override
-      public int getNumThreadsToLoadSegmentsIntoPageCacheOnDownload()
-      {
-        return 1;
-      }
-
-      @Override
-      public List<StorageLocationConfig> getLocations()
-      {
-        return Collections.singletonList(
-            new StorageLocationConfig(localSegmentCacheDir, null, null)
-        );
-      }
-    };
+    final SegmentLoaderConfig loaderConfig =
+        SegmentLoaderConfig.builder()
+                           .numThreadsToLoadSegmentsIntoPageCacheOnDownload(1)
+                           .locations(new 
StorageLocationConfig(localSegmentCacheDir, null, null))
+                           .build();
     final List<StorageLocation> locations = loaderConfig.toStorageLocations();
     SegmentLocalCacheManager manager = new SegmentLocalCacheManager(
         locations,
@@ -415,7 +398,7 @@ public class SegmentLocalCacheManagerTest extends 
InitializedNullHandlingTest
     final StorageLocationConfig locationConfig2 = new 
StorageLocationConfig(localStorageFolder2, 1000000000L, null);
     locations.add(locationConfig2);
 
-    final SegmentLoaderConfig loaderConfig = new 
SegmentLoaderConfig().setLocations(locations);
+    final SegmentLoaderConfig loaderConfig = 
SegmentLoaderConfig.builder().locations(locations).build();
     final List<StorageLocation> storageLocations = 
loaderConfig.toStorageLocations();
     SegmentLocalCacheManager manager = new SegmentLocalCacheManager(
         storageLocations,
@@ -454,7 +437,7 @@ public class SegmentLocalCacheManagerTest extends 
InitializedNullHandlingTest
     final StorageLocationConfig locationConfig2 = new 
StorageLocationConfig(localStorageFolder2, 10000000L, null);
     locations.add(locationConfig2);
 
-    final SegmentLoaderConfig loaderConfig = new 
SegmentLoaderConfig().setLocations(locations);
+    final SegmentLoaderConfig loaderConfig = 
SegmentLoaderConfig.builder().locations(locations).build();
     final List<StorageLocation> storageLocations = 
loaderConfig.toStorageLocations();
     SegmentLocalCacheManager manager = new SegmentLocalCacheManager(
         storageLocations,
@@ -495,7 +478,7 @@ public class SegmentLocalCacheManagerTest extends 
InitializedNullHandlingTest
     final StorageLocationConfig locationConfig2 = new 
StorageLocationConfig(localStorageFolder2, 10000000L, null);
     locations.add(locationConfig2);
 
-    final SegmentLoaderConfig loaderConfig = new 
SegmentLoaderConfig().setLocations(locations);
+    final SegmentLoaderConfig loaderConfig = 
SegmentLoaderConfig.builder().locations(locations).build();
     final List<StorageLocation> storageLocations = 
loaderConfig.toStorageLocations();
     SegmentLocalCacheManager manager = new SegmentLocalCacheManager(
         storageLocations,
@@ -535,7 +518,7 @@ public class SegmentLocalCacheManagerTest extends 
InitializedNullHandlingTest
     final StorageLocationConfig locationConfig2 = new 
StorageLocationConfig(localStorageFolder2, 10L, null);
     locations.add(locationConfig2);
 
-    final SegmentLoaderConfig loaderConfig = new 
SegmentLoaderConfig().setLocations(locations);
+    final SegmentLoaderConfig loaderConfig = 
SegmentLoaderConfig.builder().locations(locations).build();
     final List<StorageLocation> storageLocations = 
loaderConfig.toStorageLocations();
     SegmentLocalCacheManager manager = new SegmentLocalCacheManager(
         storageLocations,
@@ -592,7 +575,7 @@ public class SegmentLocalCacheManagerTest extends 
InitializedNullHandlingTest
       );
     }
 
-    final SegmentLoaderConfig loaderConfig = new 
SegmentLoaderConfig().setLocations(locationConfigs);
+    final SegmentLoaderConfig loaderConfig = 
SegmentLoaderConfig.builder().locations(locationConfigs).build();
     SegmentLocalCacheManager manager = new SegmentLocalCacheManager(
         locations,
         loaderConfig,
@@ -670,11 +653,11 @@ public class SegmentLocalCacheManagerTest extends 
InitializedNullHandlingTest
     locations.add(locationConfig2);
     locations.add(locationConfig3);
 
-    final SegmentLoaderConfig loaderConfig = new 
SegmentLoaderConfig().setLocations(locations);
+    final SegmentLoaderConfig loaderConfig = 
SegmentLoaderConfig.builder().locations(locations).build();
     final List<StorageLocation> storageLocations = 
loaderConfig.toStorageLocations();
     SegmentLocalCacheManager manager = new SegmentLocalCacheManager(
         storageLocations,
-        new SegmentLoaderConfig().setLocations(locations),
+        SegmentLoaderConfig.builder().locations(locations).build(),
         StorageLoadingThreadPool.createFromConfig(loaderConfig),
         new LeastBytesUsedStorageLocationSelectorStrategy(storageLocations),
         TestHelper.getTestIndexIO(jsonMapper, ColumnConfig.DEFAULT),
@@ -752,7 +735,7 @@ public class SegmentLocalCacheManagerTest extends 
InitializedNullHandlingTest
     locationConfigs.add(locationConfig2);
     locationConfigs.add(locationConfig3);
 
-    SegmentLoaderConfig segmentLoaderConfig = new 
SegmentLoaderConfig().setLocations(locationConfigs);
+    SegmentLoaderConfig segmentLoaderConfig = 
SegmentLoaderConfig.builder().locations(locationConfigs).build();
 
     final List<StorageLocation> locations = 
segmentLoaderConfig.toStorageLocations();
     SegmentLocalCacheManager manager = new SegmentLocalCacheManager(
@@ -835,7 +818,7 @@ public class SegmentLocalCacheManagerTest extends 
InitializedNullHandlingTest
   public void testGetBootstrapSegment() throws SegmentLoadingException
   {
     final StorageLocationConfig locationConfig = new 
StorageLocationConfig(localSegmentCacheDir, 10000L, null);
-    final SegmentLoaderConfig loaderConfig = new 
SegmentLoaderConfig().setLocations(ImmutableList.of(locationConfig));
+    final SegmentLoaderConfig loaderConfig = 
SegmentLoaderConfig.builder().locations(locationConfig).build();
     final List<StorageLocation> storageLocations = 
loaderConfig.toStorageLocations();
     SegmentLocalCacheManager manager = new SegmentLocalCacheManager(
         storageLocations,
@@ -860,20 +843,10 @@ public class SegmentLocalCacheManagerTest extends 
InitializedNullHandlingTest
   public void testGetBootstrapSegmentLazy() throws SegmentLoadingException
   {
     final StorageLocationConfig locationConfig = new 
StorageLocationConfig(localSegmentCacheDir, 10000L, null);
-    final SegmentLoaderConfig loaderConfig = new SegmentLoaderConfig()
-    {
-      @Override
-      public boolean isLazyLoadOnStart()
-      {
-        return true;
-      }
-
-      @Override
-      public List<StorageLocationConfig> getLocations()
-      {
-        return List.of(locationConfig);
-      }
-    };
+    final SegmentLoaderConfig loaderConfig = SegmentLoaderConfig.builder()
+                                                                
.lazyLoadOnStart(true)
+                                                                
.locations(locationConfig)
+                                                                .build();
     final List<StorageLocation> storageLocations = 
loaderConfig.toStorageLocations();
     SegmentLocalCacheManager manager = new SegmentLocalCacheManager(
         storageLocations,
@@ -897,31 +870,11 @@ public class SegmentLocalCacheManagerTest extends 
InitializedNullHandlingTest
   public void testGetSegmentVirtualStorage() throws Exception
   {
     final StorageLocationConfig locationConfig = new 
StorageLocationConfig(localSegmentCacheDir, 10000L, null);
-    final SegmentLoaderConfig loaderConfig = new SegmentLoaderConfig()
-    {
-      @Override
-      public List<StorageLocationConfig> getLocations()
-      {
-        return ImmutableList.of(locationConfig);
-      }
-
-      @Override
-      public boolean isVirtualStorage()
-      {
-        return true;
-      }
-
-      @Override
-      public File getInfoDir()
-      {
-        try {
-          return tmpFolder.newFolder();
-        }
-        catch (IOException e) {
-          throw new RuntimeException(e);
-        }
-      }
-    };
+    final SegmentLoaderConfig loaderConfig = SegmentLoaderConfig.builder()
+                                                                
.locations(locationConfig)
+                                                                
.virtualStorage(true)
+                                                                
.infoDir(tmpFolder.newFolder())
+                                                                .build();
     final List<StorageLocation> storageLocations = 
loaderConfig.toStorageLocations();
     SegmentLocalCacheManager manager = new SegmentLocalCacheManager(
         storageLocations,
@@ -969,26 +922,11 @@ public class SegmentLocalCacheManagerTest extends 
InitializedNullHandlingTest
   public void testVirtualStorageRejectsNonPositiveLoadThreads()
   {
     final StorageLocationConfig locationConfig = new 
StorageLocationConfig(localSegmentCacheDir, 10000L, null);
-    final SegmentLoaderConfig loaderConfig = new SegmentLoaderConfig()
-    {
-      @Override
-      public List<StorageLocationConfig> getLocations()
-      {
-        return ImmutableList.of(locationConfig);
-      }
-
-      @Override
-      public boolean isVirtualStorage()
-      {
-        return true;
-      }
-
-      @Override
-      public int getVirtualStorageLoadThreads()
-      {
-        return 0;
-      }
-    };
+    final SegmentLoaderConfig loaderConfig = SegmentLoaderConfig.builder()
+                                                                
.locations(locationConfig)
+                                                                
.virtualStorage(true)
+                                                                
.virtualStorageLoadThreads(0)
+                                                                .build();
     final List<StorageLocation> storageLocations = 
loaderConfig.toStorageLocations();
     MatcherAssert.assertThat(
         Assert.assertThrows(
@@ -1014,31 +952,11 @@ public class SegmentLocalCacheManagerTest extends 
InitializedNullHandlingTest
   public void testGetBootstrapSegmentVirtualStorage() throws Exception
   {
     final StorageLocationConfig locationConfig = new 
StorageLocationConfig(localSegmentCacheDir, 10000L, null);
-    final SegmentLoaderConfig loaderConfig = new SegmentLoaderConfig()
-    {
-      @Override
-      public List<StorageLocationConfig> getLocations()
-      {
-        return ImmutableList.of(locationConfig);
-      }
-
-      @Override
-      public boolean isVirtualStorage()
-      {
-        return true;
-      }
-
-      @Override
-      public File getInfoDir()
-      {
-        try {
-          return tmpFolder.newFolder();
-        }
-        catch (IOException e) {
-          throw new RuntimeException(e);
-        }
-      }
-    };
+    final SegmentLoaderConfig loaderConfig = SegmentLoaderConfig.builder()
+                                                                
.locations(locationConfig)
+                                                                
.virtualStorage(true)
+                                                                
.infoDir(tmpFolder.newFolder())
+                                                                .build();
     final List<StorageLocation> storageLocations = 
loaderConfig.toStorageLocations();
     SegmentLocalCacheManager manager = new SegmentLocalCacheManager(
         storageLocations,
@@ -1087,20 +1005,10 @@ public class SegmentLocalCacheManagerTest extends 
InitializedNullHandlingTest
   public void testGetBootstrapSegmentVirtualStorageSegmentAlreadyCached() 
throws Exception
   {
     final StorageLocationConfig locationConfig = new 
StorageLocationConfig(localSegmentCacheDir, 10000L, null);
-    final SegmentLoaderConfig loaderConfig = new SegmentLoaderConfig()
-    {
-      @Override
-      public List<StorageLocationConfig> getLocations()
-      {
-        return ImmutableList.of(locationConfig);
-      }
-
-      @Override
-      public boolean isVirtualStorage()
-      {
-        return true;
-      }
-    };
+    final SegmentLoaderConfig loaderConfig = SegmentLoaderConfig.builder()
+                                                                
.locations(locationConfig)
+                                                                
.virtualStorage(true)
+                                                                .build();
     final List<StorageLocation> storageLocations = 
loaderConfig.toStorageLocations();
     SegmentLocalCacheManager manager = new SegmentLocalCacheManager(
         storageLocations,
@@ -1165,31 +1073,11 @@ public class SegmentLocalCacheManagerTest extends 
InitializedNullHandlingTest
   public void testGetSegmentVirtualStorageMountAfterDrop() throws Exception
   {
     final StorageLocationConfig locationConfig = new 
StorageLocationConfig(localSegmentCacheDir, 10L, null);
-    final SegmentLoaderConfig loaderConfig = new SegmentLoaderConfig()
-    {
-      @Override
-      public List<StorageLocationConfig> getLocations()
-      {
-        return ImmutableList.of(locationConfig);
-      }
-
-      @Override
-      public boolean isVirtualStorage()
-      {
-        return true;
-      }
-
-      @Override
-      public File getInfoDir()
-      {
-        try {
-          return tmpFolder.newFolder();
-        }
-        catch (IOException e) {
-          throw new RuntimeException(e);
-        }
-      }
-    };
+    final SegmentLoaderConfig loaderConfig = SegmentLoaderConfig.builder()
+                                                                
.locations(locationConfig)
+                                                                
.virtualStorage(true)
+                                                                
.infoDir(tmpFolder.newFolder())
+                                                                .build();
     final List<StorageLocation> storageLocations = 
loaderConfig.toStorageLocations();
     SegmentLocalCacheManager manager = new SegmentLocalCacheManager(
         storageLocations,
@@ -1249,32 +1137,12 @@ public class SegmentLocalCacheManagerTest extends 
InitializedNullHandlingTest
   {
     final StorageLocationConfig locationConfig = new 
StorageLocationConfig(localSegmentCacheDir, 10000L, null);
     final File infoDir = tmpFolder.newFolder();
-    final SegmentLoaderConfig loaderConfig = new SegmentLoaderConfig()
-    {
-      @Override
-      public List<StorageLocationConfig> getLocations()
-      {
-        return ImmutableList.of(locationConfig);
-      }
-
-      @Override
-      public boolean isVirtualStorage()
-      {
-        return true;
-      }
-
-      @Override
-      public boolean isVirtualStorageEphemeral()
-      {
-        return true;
-      }
-
-      @Override
-      public File getInfoDir()
-      {
-        return infoDir;
-      }
-    };
+    final SegmentLoaderConfig loaderConfig = SegmentLoaderConfig.builder()
+                                                                
.locations(locationConfig)
+                                                                
.virtualStorage(true)
+                                                                
.virtualStorageIsEphemeral(true)
+                                                                
.infoDir(infoDir)
+                                                                .build();
     final List<StorageLocation> storageLocations = 
loaderConfig.toStorageLocations();
     final SegmentLocalCacheManager manager = new SegmentLocalCacheManager(
         storageLocations,
@@ -1399,7 +1267,7 @@ public class SegmentLocalCacheManagerTest extends 
InitializedNullHandlingTest
     final List<StorageLocationConfig> locationConfigs = new ArrayList<>();
     final StorageLocationConfig locationConfig = new 
StorageLocationConfig(localSegmentCacheDir, 10000000000L, null);
     locationConfigs.add(locationConfig);
-    final SegmentLoaderConfig loaderConfig = new 
SegmentLoaderConfig().setLocations(locationConfigs);
+    final SegmentLoaderConfig loaderConfig = 
SegmentLoaderConfig.builder().locations(locationConfigs).build();
     final List<StorageLocation> locations = loaderConfig.toStorageLocations();
     return new SegmentLocalCacheManager(
         locations,
diff --git 
a/server/src/test/java/org/apache/druid/segment/loading/StorageLoadingThreadPoolTest.java
 
b/server/src/test/java/org/apache/druid/segment/loading/StorageLoadingThreadPoolTest.java
index 196b1d96437..3f92a401f34 100644
--- 
a/server/src/test/java/org/apache/druid/segment/loading/StorageLoadingThreadPoolTest.java
+++ 
b/server/src/test/java/org/apache/druid/segment/loading/StorageLoadingThreadPoolTest.java
@@ -28,46 +28,32 @@ class StorageLoadingThreadPoolTest
 {
   private static SegmentLoaderConfig oneVirtualThreadConfig()
   {
-    return new SegmentLoaderConfig()
-    {
-      @Override
-      public int getVirtualStorageLoadThreads()
-      {
-        return 1;
-      }
-    }.setVirtualStorage(true);
+    return 
SegmentLoaderConfig.builder().virtualStorageLoadThreads(1).virtualStorage(true).build();
   }
 
   private static SegmentLoaderConfig fixedThreadConfig()
   {
-    return new SegmentLoaderConfig()
-    {
-      @Override
-      public int getVirtualStorageLoadThreads()
-      {
-        return 2;
-      }
-
-      @Override
-      public boolean isVirtualStorageUseVirtualThreads()
-      {
-        return false;
-      }
-    }.setVirtualStorage(true);
+    return SegmentLoaderConfig.builder()
+                              .virtualStorageLoadThreads(2)
+                              .virtualStorageUseVirtualThreads(false)
+                              .virtualStorage(true)
+                              .build();
   }
 
   @Test
   void testCreateFromConfigIsUnavailableWhenNotVirtualStorage()
   {
-    Assertions.assertFalse(StorageLoadingThreadPool.createFromConfig(new 
SegmentLoaderConfig()).isAvailable());
+    Assertions.assertFalse(
+        
StorageLoadingThreadPool.createFromConfig(SegmentLoaderConfig.builder().build()).isAvailable()
+    );
   }
 
   @Test
   void testCreateFromConfigIsAvailableWhenVirtualStorage()
   {
-    // The shared ephemeral pool is built this way: 
createFromConfig(config.withVirtualStorage(true)).
+    // The shared ephemeral pool is built this way: 
createFromConfig(config.toEphemeralVirtualStorage()).
     final StorageLoadingThreadPool pool =
-        StorageLoadingThreadPool.createFromConfig(new 
SegmentLoaderConfig().withVirtualStorage(true));
+        
StorageLoadingThreadPool.createFromConfig(SegmentLoaderConfig.builder().build().toEphemeralVirtualStorage());
     try {
       Assertions.assertTrue(pool.isAvailable());
       Assertions.assertNotNull(pool.getExecutorService());
@@ -80,14 +66,8 @@ class StorageLoadingThreadPoolTest
   @Test
   void testCreateFromConfigRejectsNonPositiveThreadCountInVirtualStorage()
   {
-    final SegmentLoaderConfig config = new SegmentLoaderConfig()
-    {
-      @Override
-      public int getVirtualStorageLoadThreads()
-      {
-        return 0;
-      }
-    }.setVirtualStorage(true);
+    final SegmentLoaderConfig config =
+        
SegmentLoaderConfig.builder().virtualStorageLoadThreads(0).virtualStorage(true).build();
     Assertions.assertThrows(DruidException.class, () -> 
StorageLoadingThreadPool.createFromConfig(config));
   }
 
diff --git 
a/server/src/test/java/org/apache/druid/segment/realtime/appenderator/StreamAppenderatorTester.java
 
b/server/src/test/java/org/apache/druid/segment/realtime/appenderator/StreamAppenderatorTester.java
index 93571eba5ba..76344d9565d 100644
--- 
a/server/src/test/java/org/apache/druid/segment/realtime/appenderator/StreamAppenderatorTester.java
+++ 
b/server/src/test/java/org/apache/druid/segment/realtime/appenderator/StreamAppenderatorTester.java
@@ -233,14 +233,9 @@ public class StreamAppenderatorTester implements 
AutoCloseable
           taskIntervalUnlocker
       );
     } else {
-      SegmentLoaderConfig segmentLoaderConfig = new SegmentLoaderConfig()
-      {
-        @Override
-        public int getDropSegmentDelayMillis()
-        {
-          return delayInMilli;
-        }
-      };
+      SegmentLoaderConfig segmentLoaderConfig = SegmentLoaderConfig.builder()
+                                                                   
.dropSegmentDelayMillis(delayInMilli)
+                                                                   .build();
       appenderator = Appenderators.createRealtime(
           segmentLoaderConfig,
           schema.getDataSource(),
diff --git 
a/server/src/test/java/org/apache/druid/server/SegmentManagerTest.java 
b/server/src/test/java/org/apache/druid/server/SegmentManagerTest.java
index ea0412709a9..22c20731c11 100644
--- a/server/src/test/java/org/apache/druid/server/SegmentManagerTest.java
+++ b/server/src/test/java/org/apache/druid/server/SegmentManagerTest.java
@@ -70,7 +70,6 @@ import org.junit.rules.TemporaryFolder;
 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -105,50 +104,21 @@ public class SegmentManagerTest extends 
InitializedNullHandlingTest
   {
     EmittingLogger.registerEmitter(new NoopServiceEmitter());
     final File segmentCacheDir = temporaryFolder.newFolder();
-    final SegmentLoaderConfig loaderConfig = new SegmentLoaderConfig()
-    {
-      @Override
-      public File getInfoDir()
-      {
-        return segmentCacheDir;
-      }
-
-      @Override
-      public List<StorageLocationConfig> getLocations()
-      {
-        return Collections.singletonList(
-            new StorageLocationConfig(segmentCacheDir, null, null)
-        );
-      }
-    };
+    final SegmentLoaderConfig loaderConfig = SegmentLoaderConfig.builder()
+        .infoDir(segmentCacheDir)
+        .locations(new StorageLocationConfig(segmentCacheDir, null, null))
+        .build();
 
     final File vsfRoot = temporaryFolder.newFolder();
     final File virtualSegmentCacheDir = new File(vsfRoot, "segmentCache");
     FileUtils.mkdirp(virtualSegmentCacheDir);
     final File vsfInfoDir = new File(vsfRoot, "info");
     FileUtils.mkdirp(vsfInfoDir);
-    final SegmentLoaderConfig virtualLoaderConfig = new SegmentLoaderConfig()
-    {
-      @Override
-      public File getInfoDir()
-      {
-        return vsfInfoDir;
-      }
-
-      @Override
-      public List<StorageLocationConfig> getLocations()
-      {
-        return Collections.singletonList(
-            new StorageLocationConfig(virtualSegmentCacheDir, null, null)
-        );
-      }
-
-      @Override
-      public boolean isVirtualStorage()
-      {
-        return true;
-      }
-    };
+    final SegmentLoaderConfig virtualLoaderConfig = 
SegmentLoaderConfig.builder()
+        .infoDir(vsfInfoDir)
+        .locations(new StorageLocationConfig(virtualSegmentCacheDir, null, 
null))
+        .virtualStorage(true)
+        .build();
 
     final ObjectMapper objectMapper = TestHelper.makeJsonMapper();
     objectMapper.registerSubtypes(TestSegmentUtils.TestLoadSpec.class);
diff --git 
a/server/src/test/java/org/apache/druid/server/StatusResourceTest.java 
b/server/src/test/java/org/apache/druid/server/StatusResourceTest.java
index 54dab17a40a..c677d48ef28 100644
--- a/server/src/test/java/org/apache/druid/server/StatusResourceTest.java
+++ b/server/src/test/java/org/apache/druid/server/StatusResourceTest.java
@@ -28,6 +28,7 @@ import org.apache.druid.guice.StartupInjectorBuilder;
 import org.apache.druid.guice.TestDruidModule;
 import org.apache.druid.initialization.DruidModule;
 import org.apache.druid.java.util.common.StringUtils;
+import org.apache.druid.segment.loading.SegmentLoaderConfig;
 import org.apache.druid.utils.JvmUtils;
 import org.junit.Assert;
 import org.junit.Test;
@@ -103,7 +104,10 @@ public class StatusResourceTest
   private void testHiddenPropertiesWithPropertyFileName(String fileName) 
throws Exception
   {
     Injector injector = new StartupInjectorBuilder()
-        .add(new PropertiesModule(Collections.singletonList(fileName)))
+        .add(
+            new PropertiesModule(Collections.singletonList(fileName)),
+            binder -> 
binder.bind(SegmentLoaderConfig.class).toInstance(SegmentLoaderConfig.builder().build())
+        )
         .build();
     Map<String, String> returnedProperties = 
injector.getInstance(StatusResource.class).getProperties();
     Set<String> lowerCasePropertyNames = returnedProperties.keySet()
diff --git 
a/server/src/test/java/org/apache/druid/server/coordination/SegmentCacheBootstrapperCacheTest.java
 
b/server/src/test/java/org/apache/druid/server/coordination/SegmentCacheBootstrapperCacheTest.java
index 7878630d2af..e5c4063eac6 100644
--- 
a/server/src/test/java/org/apache/druid/server/coordination/SegmentCacheBootstrapperCacheTest.java
+++ 
b/server/src/test/java/org/apache/druid/server/coordination/SegmentCacheBootstrapperCacheTest.java
@@ -46,7 +46,6 @@ import org.junit.rules.TemporaryFolder;
 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 
 /**
@@ -75,22 +74,10 @@ public class SegmentCacheBootstrapperCacheTest
   {
     infoDir = temporaryFolder.newFolder();
     cacheDir = temporaryFolder.newFolder();
-    loaderConfig = new SegmentLoaderConfig()
-    {
-      @Override
-      public File getInfoDir()
-      {
-        return infoDir;
-      }
-
-      @Override
-      public List<StorageLocationConfig> getLocations()
-      {
-        return Collections.singletonList(
-            new StorageLocationConfig(cacheDir, MAX_SIZE, null)
-        );
-      }
-    };
+    loaderConfig = SegmentLoaderConfig.builder()
+        .infoDir(infoDir)
+        .locations(new StorageLocationConfig(cacheDir, MAX_SIZE, null))
+        .build();
 
     objectMapper = TestHelper.makeJsonMapper();
     objectMapper.registerSubtypes(TestSegmentUtils.TestLoadSpec.class);
@@ -116,7 +103,7 @@ public class SegmentCacheBootstrapperCacheTest
   public void testLoadStartStopWithEmptyLocations() throws IOException
   {
     final List<StorageLocation> emptyLocations = ImmutableList.of();
-    final SegmentLoaderConfig loaderConfig = new SegmentLoaderConfig();
+    final SegmentLoaderConfig loaderConfig = 
SegmentLoaderConfig.builder().build();
     segmentManager = new SegmentManager(
         new SegmentLocalCacheManager(
             emptyLocations,
diff --git 
a/server/src/test/java/org/apache/druid/server/coordination/SegmentCacheBootstrapperTest.java
 
b/server/src/test/java/org/apache/druid/server/coordination/SegmentCacheBootstrapperTest.java
index 594554022c7..75f09988314 100644
--- 
a/server/src/test/java/org/apache/druid/server/coordination/SegmentCacheBootstrapperTest.java
+++ 
b/server/src/test/java/org/apache/druid/server/coordination/SegmentCacheBootstrapperTest.java
@@ -40,7 +40,6 @@ import org.junit.rules.TemporaryFolder;
 
 import java.io.File;
 import java.io.IOException;
-import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -65,34 +64,12 @@ public class SegmentCacheBootstrapperTest
     final File segmentCacheDir = temporaryFolder.newFolder();
 
     segmentAnnouncer = new TestDataSegmentAnnouncer();
-    segmentLoaderConfig = new SegmentLoaderConfig()
-    {
-      @Override
-      public File getInfoDir()
-      {
-        return segmentCacheDir;
-      }
-
-      @Override
-      public int getNumLoadingThreads()
-      {
-        return 5;
-      }
-
-      @Override
-      public int getAnnounceIntervalMillis()
-      {
-        return 50;
-      }
-
-      @Override
-      public List<StorageLocationConfig> getLocations()
-      {
-        return Collections.singletonList(
-            new StorageLocationConfig(segmentCacheDir, null, null)
-        );
-      }
-    };
+    segmentLoaderConfig = SegmentLoaderConfig.builder()
+        .infoDir(segmentCacheDir)
+        .numLoadingThreads(5)
+        .announceIntervalMillis(50)
+        .locations(new StorageLocationConfig(segmentCacheDir, null, null))
+        .build();
 
     coordinatorClient = new TestCoordinatorClient();
     serviceEmitter = new StubServiceEmitter();
diff --git 
a/server/src/test/java/org/apache/druid/server/coordination/SegmentLoadDropHandlerTest.java
 
b/server/src/test/java/org/apache/druid/server/coordination/SegmentLoadDropHandlerTest.java
index 4c8fc0c4079..b46150423aa 100644
--- 
a/server/src/test/java/org/apache/druid/server/coordination/SegmentLoadDropHandlerTest.java
+++ 
b/server/src/test/java/org/apache/druid/server/coordination/SegmentLoadDropHandlerTest.java
@@ -44,7 +44,6 @@ import org.mockito.Mockito;
 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -72,40 +71,13 @@ public class SegmentLoadDropHandlerTest
 
     scheduledRunnable = new ArrayList<>();
     segmentAnnouncer = new TestDataSegmentAnnouncer();
-    segmentLoaderConfig = new SegmentLoaderConfig()
-    {
-      @Override
-      public File getInfoDir()
-      {
-        return segmentCacheDir;
-      }
-
-      @Override
-      public int getNumLoadingThreads()
-      {
-        return 5;
-      }
-
-      @Override
-      public int getAnnounceIntervalMillis()
-      {
-        return 50;
-      }
-
-      @Override
-      public List<StorageLocationConfig> getLocations()
-      {
-        return Collections.singletonList(
-            new StorageLocationConfig(segmentCacheDir, null, null)
-        );
-      }
-
-      @Override
-      public int getDropSegmentDelayMillis()
-      {
-        return 0;
-      }
-    };
+    segmentLoaderConfig = SegmentLoaderConfig.builder()
+        .infoDir(segmentCacheDir)
+        .numLoadingThreads(5)
+        .announceIntervalMillis(50)
+        .locations(new StorageLocationConfig(segmentCacheDir, null, null))
+        .dropSegmentDelayMillis(0)
+        .build();
 
     scheduledExecutorFactory = (corePoolSize, nameFormat) -> {
       // Override normal behavior by adding the runnable to a list so that you 
can make sure
@@ -298,40 +270,13 @@ public class SegmentLoadDropHandlerTest
     
Mockito.doNothing().when(segmentManager).dropSegment(ArgumentMatchers.any());
 
     final File storageDir = temporaryFolder.newFolder();
-    final SegmentLoaderConfig noAnnouncerSegmentLoaderConfig = new 
SegmentLoaderConfig()
-    {
-      @Override
-      public File getInfoDir()
-      {
-        return storageDir;
-      }
-
-      @Override
-      public int getNumLoadingThreads()
-      {
-        return 5;
-      }
-
-      @Override
-      public int getAnnounceIntervalMillis()
-      {
-        return 0;
-      }
-
-      @Override
-      public List<StorageLocationConfig> getLocations()
-      {
-        return Collections.singletonList(
-            new StorageLocationConfig(storageDir, null, null)
-        );
-      }
-
-      @Override
-      public int getDropSegmentDelayMillis()
-      {
-        return 0;
-      }
-    };
+    final SegmentLoaderConfig noAnnouncerSegmentLoaderConfig = 
SegmentLoaderConfig.builder()
+        .infoDir(storageDir)
+        .numLoadingThreads(5)
+        .announceIntervalMillis(0)
+        .locations(new StorageLocationConfig(storageDir, null, null))
+        .dropSegmentDelayMillis(0)
+        .build();
 
     final SegmentLoadDropHandler handler = initSegmentLoadDropHandler(
         noAnnouncerSegmentLoaderConfig,
diff --git 
a/server/src/test/java/org/apache/druid/server/coordination/SegmentManagerBroadcastJoinIndexedTableTest.java
 
b/server/src/test/java/org/apache/druid/server/coordination/SegmentManagerBroadcastJoinIndexedTableTest.java
index 5ee311c389b..a16e24bfc0e 100644
--- 
a/server/src/test/java/org/apache/druid/server/coordination/SegmentManagerBroadcastJoinIndexedTableTest.java
+++ 
b/server/src/test/java/org/apache/druid/server/coordination/SegmentManagerBroadcastJoinIndexedTableTest.java
@@ -129,22 +129,10 @@ public class SegmentManagerBroadcastJoinIndexedTableTest 
extends InitializedNull
     segmentCacheDir = temporaryFolder.newFolder();
     segmentDeepStorageDir = temporaryFolder.newFolder();
 
-    final SegmentLoaderConfig loaderConfig = new SegmentLoaderConfig()
-    {
-      @Override
-      public File getInfoDir()
-      {
-        return segmentCacheDir;
-      }
-
-      @Override
-      public List<StorageLocationConfig> getLocations()
-      {
-        return Collections.singletonList(
-            new StorageLocationConfig(segmentCacheDir, null, null)
-        );
-      }
-    };
+    final SegmentLoaderConfig loaderConfig = SegmentLoaderConfig.builder()
+        .infoDir(segmentCacheDir)
+        .locations(new StorageLocationConfig(segmentCacheDir, null, null))
+        .build();
     final List<StorageLocation> storageLocations = 
loaderConfig.toStorageLocations();
 
     segmentCacheManager = new SegmentLocalCacheManager(
diff --git 
a/server/src/test/java/org/apache/druid/server/coordination/SegmentManagerThreadSafetyTest.java
 
b/server/src/test/java/org/apache/druid/server/coordination/SegmentManagerThreadSafetyTest.java
index 75f9c347a9c..fec53f4c20f 100644
--- 
a/server/src/test/java/org/apache/druid/server/coordination/SegmentManagerThreadSafetyTest.java
+++ 
b/server/src/test/java/org/apache/druid/server/coordination/SegmentManagerThreadSafetyTest.java
@@ -100,22 +100,10 @@ public class SegmentManagerThreadSafetyTest
     segmentCacheDir = temporaryFolder.newFolder();
     segmentDeepStorageDir = temporaryFolder.newFolder();
 
-    final SegmentLoaderConfig loaderConfig = new SegmentLoaderConfig()
-    {
-      @Override
-      public File getInfoDir()
-      {
-        return segmentCacheDir;
-      }
-
-      @Override
-      public List<StorageLocationConfig> getLocations()
-      {
-        return Collections.singletonList(
-            new StorageLocationConfig(segmentCacheDir, null, null)
-        );
-      }
-    };
+    final SegmentLoaderConfig loaderConfig = SegmentLoaderConfig.builder()
+        .infoDir(segmentCacheDir)
+        .locations(new StorageLocationConfig(segmentCacheDir, null, null))
+        .build();
     final List<StorageLocation> storageLocations = 
loaderConfig.toStorageLocations();
     segmentCacheManager = new SegmentLocalCacheManager(
         storageLocations,
diff --git 
a/server/src/test/java/org/apache/druid/server/metrics/SegmentStatsMonitorTest.java
 
b/server/src/test/java/org/apache/druid/server/metrics/SegmentStatsMonitorTest.java
index 373b1c1dd2d..5aa4f926da2 100644
--- 
a/server/src/test/java/org/apache/druid/server/metrics/SegmentStatsMonitorTest.java
+++ 
b/server/src/test/java/org/apache/druid/server/metrics/SegmentStatsMonitorTest.java
@@ -52,7 +52,7 @@ public class SegmentStatsMonitorTest
   private SegmentLoadDropHandler segmentLoadDropMgr;
   private ServiceEmitter serviceEmitter;
   private SegmentStatsMonitor monitor;
-  private final SegmentLoaderConfig segmentLoaderConfig = new 
SegmentLoaderConfig();
+  private final SegmentLoaderConfig segmentLoaderConfig = 
SegmentLoaderConfig.builder().build();
 
   @Before
   public void setUp()


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

Reply via email to