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 90563dc3b61 feat: partial load rule announcement (#19682)
90563dc3b61 is described below
commit 90563dc3b61ffbcf3ce1ea2ce79dafa63b050450
Author: Clint Wylie <[email protected]>
AuthorDate: Wed Jul 15 01:09:55 2026 -0700
feat: partial load rule announcement (#19682)
---
.../query/PartialProjectionLoadRuleQueryTest.java | 55 ++++++++++-
.../indexing/common/task/CompactionTaskTest.java | 4 +-
.../indexing/input/DruidSegmentReaderTest.java | 4 +-
.../org/apache/druid/timeline/DataSegment.java | 40 --------
.../druid/client/DataSegmentAndLoadProfile.java | 1 +
.../druid/client/HttpServerInventoryView.java | 25 ++---
.../loading/PartialSegmentMetadataCacheEntry.java | 37 +++++++
.../druid/segment/loading/SegmentCacheManager.java | 10 +-
.../segment/loading/SegmentLocalCacheManager.java | 55 ++++++++---
.../org/apache/druid/server/SegmentManager.java | 24 ++++-
.../coordination/SegmentCacheBootstrapper.java | 8 +-
.../coordination/SegmentChangeRequestLoad.java | 30 ++++--
.../coordination/SegmentLoadDropHandler.java | 10 +-
.../coordinator/loading/PartialLoadProfile.java | 43 +++------
.../server/coordinator/loading/SegmentHolder.java | 13 +--
.../druid/client/DruidServerPartialLoadTest.java | 9 +-
.../segment/loading/NoopSegmentCacheManager.java | 4 +-
...egmentLocalCacheManagerPartialRuleLoadTest.java | 106 +++++++++++++++++++++
.../coordination/SegmentChangeRequestLoadTest.java | 41 +++++++-
.../coordination/SegmentLoadDropHandlerTest.java | 13 ++-
.../loading/PartialLoadProfileTest.java | 20 +---
.../StrategicSegmentAssignerPartialTest.java | 15 ++-
.../druid/test/utils/TestSegmentCacheManager.java | 6 +-
23 files changed, 397 insertions(+), 176 deletions(-)
diff --git
a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/query/PartialProjectionLoadRuleQueryTest.java
b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/query/PartialProjectionLoadRuleQueryTest.java
index a3efef184dc..0215e63fa63 100644
---
a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/query/PartialProjectionLoadRuleQueryTest.java
+++
b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/query/PartialProjectionLoadRuleQueryTest.java
@@ -38,6 +38,7 @@ import org.apache.druid.java.util.common.Intervals;
import org.apache.druid.java.util.common.StringUtils;
import org.apache.druid.java.util.common.granularity.Granularities;
import org.apache.druid.query.QueryContexts;
+import org.apache.druid.query.aggregation.LongMinAggregatorFactory;
import org.apache.druid.query.aggregation.LongSumAggregatorFactory;
import org.apache.druid.server.coordinator.rules.CannotMatchBehavior;
import org.apache.druid.server.coordinator.rules.ForeverPartialLoadRule;
@@ -84,6 +85,12 @@ import java.util.UUID;
class PartialProjectionLoadRuleQueryTest extends EmbeddedClusterTestBase
{
private static final String PROJECTION_NAME = "country_delta";
+ // A second projection ingested alongside country_delta but NOT selected by
the rule. Under the partial-load rule
+ // its container bytes stay off the historical's disk, so the historical's
realized footprint is measurably less
+ // than the full segment size — that inequality is what {@link
#testCoordinatorSeesPartialLoadFootprint} asserts
+ // against sys.servers. Uses a min-aggregator on delta so neither test
query's SUM(delta) projection-select can
+ // route through it (planner needs matching aggregator).
+ private static final String UNMATCHED_PROJECTION_NAME = "country_min_delta";
private static final long CACHE_SIZE = HumanReadableBytes.parse("1MiB");
private static final long MAX_SIZE = HumanReadableBytes.parse("100MiB");
private static final long ESTIMATE_SIZE = HumanReadableBytes.parse("2KiB");
@@ -248,6 +255,43 @@ class PartialProjectionLoadRuleQueryTest extends
EmbeddedClusterTestBase
);
}
+ @Test
+ void testCoordinatorSeesPartialLoadFootprint()
+ {
+ // End-to-end check that PartialLoadedDataSegment reaches the
coordinator's inventory accounting: the historical's
+ // announcement stamps realizedBytes (metadata + selected projection +
__base dep) as loadedBytes, so
+ // sys.servers.curr_size for the historical reflects the partial footprint
— strictly less than the segment's full
+ // size (which includes the unmatched projection's bytes). Without the
fix, forAnnouncement would stamp
+ // segment.getSize() and curr_size would equal the full size.
+ final long fullSize = Long.parseLong(
+ cluster.callApi().runSql(
+ "SELECT \"size\" FROM sys.segments WHERE datasource = '" +
dataSource + "'"
+ ).trim()
+ );
+ Assertions.assertTrue(fullSize > 0, "sys.segments.size must be populated
for the ingested segment");
+
+ final long currSize = Long.parseLong(
+ cluster.callApi().runSql(
+ "SELECT curr_size FROM sys.servers WHERE server_type =
'historical'"
+ ).trim()
+ );
+
+ Assertions.assertTrue(
+ currSize > 0,
+ "coordinator's historical curr_size must reflect the partial-load
footprint, got 0"
+ );
+ Assertions.assertTrue(
+ currSize < fullSize,
+ StringUtils.format(
+ "coordinator should see the partial footprint; got curr_size=%d,
full segment size=%d "
+ + "(without PartialLoadedDataSegment, forAnnouncement would stamp
segment.getSize() as loadedBytes and "
+ + "these would be equal)",
+ currSize,
+ fullSize
+ )
+ );
+ }
+
@Test
void testProjectionMissLoadsBaseBundleOnDemand()
{
@@ -344,6 +388,15 @@ class PartialProjectionLoadRuleQueryTest extends
EmbeddedClusterTestBase
.aggregators(new LongSumAggregatorFactory("sumDelta", "delta"))
.build();
+ // Second projection ingested but NOT selected by the rule. Its containers
stay off disk under the partial-load
+ // rule, giving the historical a realized footprint measurably smaller
than the full segment size. The
+ // min-aggregator prevents the planner from routing SUM(delta) queries
through this projection.
+ final AggregateProjectionSpec unmatchedProjection =
+ AggregateProjectionSpec.builder(UNMATCHED_PROJECTION_NAME)
+ .groupingColumns(new StringDimensionSchema("countryName"))
+ .aggregators(new LongMinAggregatorFactory("minDelta", "delta"))
+ .build();
+
final SegmentGranularitySpec segmentGranularitySpec = new
SegmentGranularitySpec(
Granularities.HOUR,
List.of(Intervals.of("2024-01-01/2024-01-02"))
@@ -360,7 +413,7 @@ class PartialProjectionLoadRuleQueryTest extends
EmbeddedClusterTestBase
.withTimestamp(new TimestampSpec("time", "iso", null))
.withSegmentGranularity(segmentGranularitySpec)
.withBaseTable(clusterSpec)
- .withProjections(List.of(projection))
+ .withProjections(List.of(projection, unmatchedProjection))
)
.tuningConfig(t -> t.withMaxNumConcurrentSubTasks(1))
.withId(taskId);
diff --git
a/indexing-service/src/test/java/org/apache/druid/indexing/common/task/CompactionTaskTest.java
b/indexing-service/src/test/java/org/apache/druid/indexing/common/task/CompactionTaskTest.java
index ff219260dbf..5c8a49ed482 100644
---
a/indexing-service/src/test/java/org/apache/druid/indexing/common/task/CompactionTaskTest.java
+++
b/indexing-service/src/test/java/org/apache/druid/indexing/common/task/CompactionTaskTest.java
@@ -1990,9 +1990,9 @@ public class CompactionTaskTest
final SegmentCacheManager segmentCacheManager = new
NoopSegmentCacheManager()
{
@Override
- public void load(DataSegment segment)
+ public DataSegment load(DataSegment segment)
{
- // do nothing
+ return segment;
}
@Override
diff --git
a/indexing-service/src/test/java/org/apache/druid/indexing/input/DruidSegmentReaderTest.java
b/indexing-service/src/test/java/org/apache/druid/indexing/input/DruidSegmentReaderTest.java
index 54a6c2d8434..4f16fc52796 100644
---
a/indexing-service/src/test/java/org/apache/druid/indexing/input/DruidSegmentReaderTest.java
+++
b/indexing-service/src/test/java/org/apache/druid/indexing/input/DruidSegmentReaderTest.java
@@ -932,9 +932,9 @@ public class DruidSegmentReaderTest extends
InitializedNullHandlingTest
new NoopSegmentCacheManager()
{
@Override
- public void load(DataSegment segment)
+ public DataSegment load(DataSegment segment)
{
- // do nothing
+ return segment;
}
@Override
diff --git
a/processing/src/main/java/org/apache/druid/timeline/DataSegment.java
b/processing/src/main/java/org/apache/druid/timeline/DataSegment.java
index 987f2982061..0a82f8575a8 100644
--- a/processing/src/main/java/org/apache/druid/timeline/DataSegment.java
+++ b/processing/src/main/java/org/apache/druid/timeline/DataSegment.java
@@ -223,46 +223,6 @@ public class DataSegment implements
Comparable<DataSegment>, Overshadowable<Data
);
}
- /**
- * @deprecated use {@link #builder(SegmentId)} or {@link
#builder(DataSegment)} instead.
- */
- @Deprecated
- public DataSegment(
- String dataSource,
- Interval interval,
- String version,
- @Nullable Map<String, Object> loadSpec,
- @Nullable List<String> dimensions,
- @Nullable List<String> metrics,
- @Nullable List<String> projections,
- @Nullable ShardSpec shardSpec,
- @Nullable CompactionState lastCompactionState,
- Integer binaryVersion,
- long size,
- Integer totalRows,
- String indexingStateFingerprint,
- PruneSpecsHolder pruneSpecsHolder
- )
- {
- this(
- dataSource,
- interval,
- version,
- loadSpec,
- dimensions,
- metrics,
- projections,
- null,
- shardSpec,
- lastCompactionState,
- binaryVersion,
- size,
- totalRows,
- indexingStateFingerprint,
- pruneSpecsHolder
- );
- }
-
@JsonCreator
private DataSegment(
@JsonProperty("dataSource") String dataSource,
diff --git
a/server/src/main/java/org/apache/druid/client/DataSegmentAndLoadProfile.java
b/server/src/main/java/org/apache/druid/client/DataSegmentAndLoadProfile.java
index 26d35d51393..e551c6af1d0 100644
---
a/server/src/main/java/org/apache/druid/client/DataSegmentAndLoadProfile.java
+++
b/server/src/main/java/org/apache/druid/client/DataSegmentAndLoadProfile.java
@@ -53,6 +53,7 @@ public class DataSegmentAndLoadProfile extends DataSegment
delegate.getDimensions(),
delegate.getMetrics(),
delegate.getProjections(),
+ delegate.getClusterGroups(),
delegate.getShardSpec(),
delegate.getLastCompactionState(),
delegate.getBinaryVersion(),
diff --git
a/server/src/main/java/org/apache/druid/client/HttpServerInventoryView.java
b/server/src/main/java/org/apache/druid/client/HttpServerInventoryView.java
index d3063da74fc..ea81900ad4f 100644
--- a/server/src/main/java/org/apache/druid/client/HttpServerInventoryView.java
+++ b/server/src/main/java/org/apache/druid/client/HttpServerInventoryView.java
@@ -674,19 +674,8 @@ public class HttpServerInventoryView implements
ServerInventoryView, FilteredSer
/**
* Builds a {@link PartialLoadProfile} from a load announcement when the
historical populated the partial-load
* wire fields ({@code fingerprint} + {@code loadedBytes}). Returns {@code
null} for plain full-load
- * announcements.
- * <p>
- * Picks between {@link PartialLoadProfile#forLoaded forLoaded} and
- * {@link PartialLoadProfile#forFullFallback forFullFallback} based on
what the historical actually realized:
- * <ul>
- * <li>If {@code loadedBytes} is strictly less than the segment's full
size <em>and</em> the announced segment
- * carries a {@link PartialLoadSpec} wrapper on its load spec, the
historical honored the partial request —
- * preserve the wrapped load spec on the profile so per-replica
observability (e.g.
- * {@link PartialLoadProfile#isFullFallback}) reports correctly.</li>
- * <li>Otherwise the historical either fell back to full or doesn't yet
implement scheme-specific partial
- * loading; build a full-fallback profile (the wire fingerprint
still satisfies the rule, so the
- * coordinator's reconciler counts it as matching without reload
thrash).</li>
- * </ul>
+ * announcements, or defensively when a fingerprint/loadedBytes pair
arrives without a partial-load wrapper on
+ * the segment's load spec (a wire-form contract violation from the
historical).
*/
@Nullable
private static PartialLoadProfile
partialLoadProfileFor(SegmentChangeRequestLoad loadRequest)
@@ -698,10 +687,14 @@ public class HttpServerInventoryView implements
ServerInventoryView, FilteredSer
}
final DataSegment segment = loadRequest.getSegment();
final Map<String, Object> loadSpec = segment.getLoadSpec();
- if (loadedBytes < segment.getSize() &&
PartialLoadSpec.detectPartialLoadSpec(loadSpec)) {
- return PartialLoadProfile.forLoaded(loadSpec, fingerprint,
loadedBytes);
+ if (!PartialLoadSpec.detectPartialLoadSpec(loadSpec)) {
+ // fingerprint/loadedBytes on the wire without a partial-load wrapper
on the segment; shouldn't happen
+ // (SegmentChangeRequestLoad.forAnnouncement only stamps them when the
loadSpec is partial or a
+ // DataSegmentAndLoadProfile carries the profile). Fall back to null
so the inventory treats this as a
+ // plain load rather than materializing a nonsense profile.
+ return null;
}
- return PartialLoadProfile.forFullFallback(fingerprint, loadedBytes);
+ return PartialLoadProfile.forLoaded(loadSpec, fingerprint, loadedBytes);
}
private void removeSegment(final DataSegment segment, boolean fullSync)
diff --git
a/server/src/main/java/org/apache/druid/segment/loading/PartialSegmentMetadataCacheEntry.java
b/server/src/main/java/org/apache/druid/segment/loading/PartialSegmentMetadataCacheEntry.java
index 9f8b53272c3..be1d871dc10 100644
---
a/server/src/main/java/org/apache/druid/segment/loading/PartialSegmentMetadataCacheEntry.java
+++
b/server/src/main/java/org/apache/druid/segment/loading/PartialSegmentMetadataCacheEntry.java
@@ -626,6 +626,43 @@ public class PartialSegmentMetadataCacheEntry implements
SegmentCacheEntry, Resi
}
}
+ /**
+ * The rule-declared on-disk footprint for this segment: this metadata
entry's own reservation (V10 header plus
+ * post-mount adjustment) plus the sum of every rule-held bundle's
reservation, plus every transitive dependency
+ * indirectly pinned by those rule-holds. A projection rule pins the
projection bundle in {@link #ruleBundleHolds}
+ * and indirectly pins {@code __base} too. Both contribute to what the rule
caused to be on disk, so both are
+ * included here. Deliberately excludes bundles linked for reasons unrelated
to the rule (e.g. on-demand-loaded by
+ * queries), which are evictable and would inflate the report with transient
state. Used by the historical to stamp
+ * accurate {@code loadedBytes} on partial-load announcements.
+ */
+ public long getRealizedBytes()
+ {
+ entryLock.lock();
+ try {
+ long total = currentSize;
+ // Rule-selected bundles PLUS their transitive dependency closure. Each
bundle's getSize() covers only that
+ // bundle's own containers, deps like __base are separate entries whose
sizes need to be added explicitly.
+ for (String name : bundlesInMountOrder(ruleBundleHolds.keySet())) {
+ final StorageLocation.ReservationHold<PartialSegmentBundleCacheEntry>
hold = ruleBundleHolds.get(name);
+ if (hold != null) {
+ // Rule-selected: size from the hold's entry (guaranteed non-null;
final long field, no lock nesting).
+ total += hold.getEntry().getSize();
+ } else {
+ // Transitive dependency of a rule-selected bundle: pinned via
parent-hold on the dependent, not present
+ // in ruleBundleHolds. Look up in linkedBundles.
+ final PartialSegmentBundleCacheEntry dep = linkedBundles.get(name);
+ if (dep != null) {
+ total += dep.getSize();
+ }
+ }
+ }
+ return total;
+ }
+ finally {
+ entryLock.unlock();
+ }
+ }
+
@Override
public boolean isMounted()
{
diff --git
a/server/src/main/java/org/apache/druid/segment/loading/SegmentCacheManager.java
b/server/src/main/java/org/apache/druid/segment/loading/SegmentCacheManager.java
index c81c1d256a8..cb5570a1731 100644
---
a/server/src/main/java/org/apache/druid/segment/loading/SegmentCacheManager.java
+++
b/server/src/main/java/org/apache/druid/segment/loading/SegmentCacheManager.java
@@ -75,10 +75,14 @@ public interface SegmentCacheManager
* {@link SegmentLoadingException}.
*
* @param segment Segment to get on each download (after service bootstrap)
+ * @return the input {@code segment}, or a
+ * {@link org.apache.druid.client.DataSegmentAndLoadProfile}
wrapping it when the historical
+ * actually materialized a partial-load footprint; the announcement
path uses the wrapper's
+ * {@code realizedBytes} to stamp accurate {@code loadedBytes} on
the wire form.
* @throws SegmentLoadingException If there is an error in loading the
segment or insufficient storage space
* @see SegmentCacheManager#bootstrap(DataSegment,
SegmentLazyLoadFailCallback)
*/
- void load(DataSegment segment) throws SegmentLoadingException;
+ DataSegment load(DataSegment segment) throws SegmentLoadingException;
/**
* Similar to {@link #load(DataSegment)}, this method loads segments during
startup on data nodes. Implementations of
@@ -88,11 +92,13 @@ public interface SegmentCacheManager
* @param segment Segment to retrieve during service bootstrap
* @param loadFailed Callback to execute when segment lazy load failed. This
applies only when
* {@code lazyLoadOnStart} is enabled
+ * @return same as {@link #load(DataSegment)}: the input {@code segment} or a
+ * {@link org.apache.druid.client.DataSegmentAndLoadProfile}
wrapping it.
* @throws SegmentLoadingException - If there is an error in loading the
segment or insufficient storage space
* @see SegmentCacheManager#load(DataSegment)
* @see SegmentCacheManager#shutdownBootstrap()
*/
- void bootstrap(DataSegment segment, SegmentLazyLoadFailCallback loadFailed)
throws SegmentLoadingException;
+ DataSegment bootstrap(DataSegment segment, SegmentLazyLoadFailCallback
loadFailed) throws SegmentLoadingException;
/**
* Cleanup the segment files cache space used by the segment, releasing the
{@link StorageLocation} reservation
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 1e3e353f482..5059c932a03 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
@@ -29,6 +29,7 @@ import com.google.errorprone.annotations.concurrent.GuardedBy;
import com.google.inject.Inject;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.NullOutputStream;
+import org.apache.druid.client.DataSegmentAndLoadProfile;
import org.apache.druid.error.DruidException;
import org.apache.druid.guice.annotations.Json;
import org.apache.druid.java.util.common.FileUtils;
@@ -42,6 +43,7 @@ import
org.apache.druid.segment.ReferenceCountedSegmentProvider;
import org.apache.druid.segment.Segment;
import org.apache.druid.segment.SegmentLazyLoadFailCallback;
import org.apache.druid.segment.file.PartialSegmentFileMapperV10;
+import org.apache.druid.server.coordinator.loading.PartialLoadProfile;
import org.apache.druid.timeline.DataSegment;
import org.apache.druid.timeline.SegmentId;
import org.apache.druid.utils.CloseableUtils;
@@ -967,7 +969,7 @@ public class SegmentLocalCacheManager implements
SegmentCacheManager
* <li>Release the transient hold; the metadata's self-hold keeps the
entry resident under the applied rule.</li>
* </ol>
*/
- private void loadPartial(DataSegment dataSegment) throws
SegmentLoadingException
+ private DataSegment loadPartial(DataSegment dataSegment) throws
SegmentLoadingException
{
final PartialLoadSpec wrapper = materializePartialLoadSpec(dataSegment);
final SegmentRangeReader rangeReader = openPartialRangeReader(dataSegment,
wrapper);
@@ -989,12 +991,18 @@ public class SegmentLocalCacheManager implements
SegmentCacheManager
final ReservedPartial existing =
findExistingPartialWithHold(dataSegment.getId());
if (existing != null) {
try {
+ // Snapshot the prior realized footprint before clearRule zeroes
out ruleBundleHolds so the log can
+ // surface the coordinator-visible curr_size transition
+ final long priorRealizedBytes =
existing.metadata().getRealizedBytes();
existing.metadata().clearRule();
log.warn(
- "Backend for segment[%s] does not support range reads;
released rule[fingerprint=%s], segment "
- + "will fall back to weak full-load at query time",
+ "Backend for segment[%s] does not support range reads;
released rule[fingerprint=%s]. Segment "
+ + "will fall back to weak full-load at query time, and the
next announcement will report "
+ + "loadedBytes=segment.getSize()=%d (up from realized=%d).",
dataSegment.getId(),
- wrapper.getFingerprint()
+ wrapper.getFingerprint(),
+ dataSegment.getSize(),
+ priorRealizedBytes
);
}
finally {
@@ -1014,7 +1022,9 @@ public class SegmentLocalCacheManager implements
SegmentCacheManager
wrapper.getFingerprint()
);
}
- return;
+ // Rule couldn't be applied, nothing partial materialized, so hand
back the plain segment; the
+ // announcement layer will fall back to segment.getSize() for
loadedBytes.
+ return dataSegment;
}
final ReservedPartial reserved = findOrReservePartial(dataSegment,
rangeReader);
@@ -1072,6 +1082,13 @@ public class SegmentLocalCacheManager implements
SegmentCacheManager
// the coordinator's load queue can retry on its next sync. The
announced fingerprint == "rule fully
// realized" contract stays intact.
awaitEagerDownloadsOrClearRule(dataSegment, metadata, selected);
+ // Wrap the announcement with a DataSegmentAndLoadProfile carrying
the historical's realized footprint AFTER
+ // eager downloads finished. forAnnouncement reads the profile back
via profileOf() and stamps its
+ // loadedBytes + fingerprint.
+ return new DataSegmentAndLoadProfile(
+ dataSegment,
+ PartialLoadProfile.forLoaded(dataSegment.getLoadSpec(),
wrapper.getFingerprint(), metadata.getRealizedBytes())
+ );
}
finally {
CloseableUtils.closeAndSuppressExceptions(
@@ -1292,7 +1309,7 @@ public class SegmentLocalCacheManager implements
SegmentCacheManager
}
@Override
- public void load(final DataSegment dataSegment) throws
SegmentLoadingException
+ public DataSegment load(final DataSegment dataSegment) throws
SegmentLoadingException
{
if (config.isVirtualStorage()) {
if (config.isVirtualStorageEphemeral()) {
@@ -1306,8 +1323,7 @@ public class SegmentLocalCacheManager implements
SegmentCacheManager
// take the existing weak/no-op path below.
if (config.isVirtualStoragePartialDownloadsEnabled()
&& PartialLoadSpec.detectPartialLoadSpec(dataSegment.getLoadSpec()))
{
- loadPartial(dataSegment);
- return;
+ return loadPartial(dataSegment);
}
// virtual storage doesn't do anything with loading immediately, but
check to see if the segment is already cached
// and if so, clear out the onUnmount action
@@ -1326,7 +1342,7 @@ public class SegmentLocalCacheManager implements
SegmentCacheManager
unlock(dataSegment, lock);
}
}
- return;
+ return dataSegment;
}
final CompleteSegmentCacheEntry cacheEntry = new
CompleteSegmentCacheEntry(dataSegment);
final ReferenceCountingLock lock = lock(dataSegment);
@@ -1341,10 +1357,11 @@ public class SegmentLocalCacheManager implements
SegmentCacheManager
unlock(dataSegment, lock);
}
}
+ return dataSegment;
}
@Override
- public void bootstrap(
+ public DataSegment bootstrap(
final DataSegment dataSegment,
final SegmentLazyLoadFailCallback loadFailed
) throws SegmentLoadingException
@@ -1358,6 +1375,8 @@ public class SegmentLocalCacheManager implements
SegmentCacheManager
// during bootstrap, check if the segment exists in a location and mount
it; getCachedSegments already
// did the reserving for us
final SegmentCacheEntryIdentifier id = new
SegmentCacheEntryIdentifier(dataSegment.getId());
+ // Assemble the loaded-profile inside the segment lock (null = no
partial materialized)
+ PartialLoadProfile loadedProfile = null;
final ReferenceCountingLock lock = lock(dataSegment);
synchronized (lock) {
try {
@@ -1382,7 +1401,9 @@ public class SegmentLocalCacheManager implements
SegmentCacheManager
);
}
// If the persisted DataSegment's loadSpec is a
partial-load-rule wrapper, reapply the rule so the
- // segment continues to be protected from eviction across the
restart.
+ // segment continues to be protected from eviction across the
restart AND build the announcement profile.
+ // Otherwise (partial downloads disabled at restart, or
persisted loadSpec no longer partial), don't
+ // advertise a rule footprint.
//
// Strict failure handling: reapplyRuleFromInfoFile throws on
eager-download failure (and clears the
// rule state before throwing). The exception propagates up
through SegmentManager.loadSegmentOnBootstrap,
@@ -1391,6 +1412,11 @@ public class SegmentLocalCacheManager implements
SegmentCacheManager
if (config.isVirtualStoragePartialDownloadsEnabled()
&&
PartialLoadSpec.detectPartialLoadSpec(dataSegment.getLoadSpec())) {
reapplyRuleFromInfoFile(dataSegment, partial);
+ loadedProfile = PartialLoadProfile.forLoaded(
+ dataSegment.getLoadSpec(),
+ (String) dataSegment.getLoadSpec().get("fingerprint"),
+ partial.getRealizedBytes()
+ );
}
} else {
throw DruidException.defensive(
@@ -1399,13 +1425,17 @@ public class SegmentLocalCacheManager implements
SegmentCacheManager
dataSegment.getId()
);
}
+ // A given segment id lives in exactly one location:
getCachedSegments assigns each cached segment to a
+ // single StorageLocation and getCacheEntry is location-local, so
hitting one location has fully handled
+ // the segment
+ break;
}
}
finally {
unlock(dataSegment, lock);
}
}
- return;
+ return loadedProfile == null ? dataSegment : new
DataSegmentAndLoadProfile(dataSegment, loadedProfile);
}
final ReferenceCountingLock lock = lock(dataSegment);
synchronized (lock) {
@@ -1419,6 +1449,7 @@ public class SegmentLocalCacheManager implements
SegmentCacheManager
unlock(dataSegment, lock);
}
}
+ return dataSegment;
}
@Nullable
diff --git a/server/src/main/java/org/apache/druid/server/SegmentManager.java
b/server/src/main/java/org/apache/druid/server/SegmentManager.java
index a3d58e10251..4bea68953ac 100644
--- a/server/src/main/java/org/apache/druid/server/SegmentManager.java
+++ b/server/src/main/java/org/apache/druid/server/SegmentManager.java
@@ -271,22 +271,30 @@ public class SegmentManager
* @param loadFailed callback to execute when segment lazy load fails. This
applies only
* when lazy loading is enabled.
*
+ * @return the input {@code dataSegment}, or a
+ * {@link org.apache.druid.client.DataSegmentAndLoadProfile}
wrapping it when the historical actually
+ * materialized a partial-load footprint. Callers pass the returned
value to the announcement layer so
+ * partial-load announcements carry accurate {@code loadedBytes}.
* @throws SegmentLoadingException if the segment cannot be loaded
* @throws IOException if the segment info cannot be cached on disk
*/
- public void loadSegmentOnBootstrap(
+ public DataSegment loadSegmentOnBootstrap(
final DataSegment dataSegment,
final SegmentLazyLoadFailCallback loadFailed
) throws SegmentLoadingException, IOException
{
+ final DataSegment loaded;
try {
- cacheManager.bootstrap(dataSegment, loadFailed);
+ loaded = cacheManager.bootstrap(dataSegment, loadFailed);
}
catch (SegmentLoadingException e) {
cacheManager.drop(dataSegment);
throw e;
}
+ // Pass the plain dataSegment (not the potentially-wrapped `loaded`) to
loadSegmentInternal: the wrapper is a
+ // load-time announcement-path artifact only
loadSegmentInternal(dataSegment);
+ return loaded;
}
@@ -298,19 +306,27 @@ public class SegmentManager
*
* @param dataSegment segment to load
*
+ * @return the input {@code dataSegment}, or a
+ * {@link org.apache.druid.client.DataSegmentAndLoadProfile}
wrapping it when the historical actually
+ * materialized a partial-load footprint. Callers pass the returned
value to the announcement layer so
+ * partial-load announcements carry accurate {@code loadedBytes}.
* @throws SegmentLoadingException if the segment cannot be loaded
* @throws IOException if the segment info cannot be cached on disk
*/
- public void loadSegment(final DataSegment dataSegment) throws
SegmentLoadingException, IOException
+ public DataSegment loadSegment(final DataSegment dataSegment) throws
SegmentLoadingException, IOException
{
+ final DataSegment loaded;
try {
- cacheManager.load(dataSegment);
+ loaded = cacheManager.load(dataSegment);
}
catch (SegmentLoadingException e) {
cacheManager.drop(dataSegment);
throw e;
}
+ // Pass the plain dataSegment (not the potentially-wrapped `loaded`) to
loadSegmentInternal: the wrapper is a
+ // load-time announcement-path artifact only
loadSegmentInternal(dataSegment);
+ return loaded;
}
private void loadSegmentInternal(
diff --git
a/server/src/main/java/org/apache/druid/server/coordination/SegmentCacheBootstrapper.java
b/server/src/main/java/org/apache/druid/server/coordination/SegmentCacheBootstrapper.java
index e9e16c9b051..c4388b9e25d 100644
---
a/server/src/main/java/org/apache/druid/server/coordination/SegmentCacheBootstrapper.java
+++
b/server/src/main/java/org/apache/druid/server/coordination/SegmentCacheBootstrapper.java
@@ -190,8 +190,9 @@ public class SegmentCacheBootstrapper
"Loading segment[%d/%d][%s]",
counter.incrementAndGet(), numSegments, segment.getId()
);
+ final DataSegment loaded;
try {
- segmentManager.loadSegmentOnBootstrap(
+ loaded = segmentManager.loadSegmentOnBootstrap(
segment,
() -> loadDropHandler.removeSegment(segment,
DataSegmentChangeCallback.NOOP, false)
);
@@ -201,7 +202,10 @@ public class SegmentCacheBootstrapper
throw new SegmentLoadingException(e, "Exception loading
segment[%s]", segment.getId());
}
try {
- backgroundSegmentAnnouncer.announceSegment(segment);
+ // loadSegmentOnBootstrap returns a PartialLoadedDataSegment
wrapper when the historical
+ // materialized a partial-load footprint on restart; enqueue
that so the announcement carries
+ // accurate loadedBytes (see PartialLoadedDataSegment +
SegmentChangeRequestLoad.forAnnouncement).
+ backgroundSegmentAnnouncer.announceSegment(loaded);
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
diff --git
a/server/src/main/java/org/apache/druid/server/coordination/SegmentChangeRequestLoad.java
b/server/src/main/java/org/apache/druid/server/coordination/SegmentChangeRequestLoad.java
index be9810b7f03..c6b5f4ac290 100644
---
a/server/src/main/java/org/apache/druid/server/coordination/SegmentChangeRequestLoad.java
+++
b/server/src/main/java/org/apache/druid/server/coordination/SegmentChangeRequestLoad.java
@@ -23,9 +23,11 @@ import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
+import org.apache.druid.client.DataSegmentAndLoadProfile;
import org.apache.druid.java.util.common.StringUtils;
import org.apache.druid.java.util.common.logger.Logger;
import org.apache.druid.segment.loading.PartialLoadSpec;
+import org.apache.druid.server.coordinator.loading.PartialLoadProfile;
import org.apache.druid.timeline.DataSegment;
import javax.annotation.Nullable;
@@ -44,15 +46,20 @@ public class SegmentChangeRequestLoad implements
DataSegmentChangeRequest
private static final Logger log = new Logger(SegmentChangeRequestLoad.class);
/**
- * Builds a load announcement for a segment loaded on a historical. Any
{@link PartialLoadSpec} subtype (identified
- * by the wire-form conventions documented on that class: a {@code type}
starting with
- * {@link PartialLoadSpec#TYPE_PREFIX}, plus top-level {@code fingerprint}
and {@code delegate} fields) produces a
- * "full-fallback" announcement: the wrapper's fingerprint and {@link
DataSegment#getSize()} ride along as
- * {@code loadedBytes}, satisfying the coordinator's partial-load rule even
when the historical did a regular full
- * load via the inner delegate. Without this, the coordinator's reconciler
would treat the replica as stale and
- * re-queue the load indefinitely.
- * <p>
- * Detection is convention-based (no subtype allowlist) so future {@link
PartialLoadSpec} subtypes work
+ * Builds a load announcement for a segment loaded on a historical. Two ways
the announcement carries partial-load
+ * metadata:
+ * <ul>
+ * <li>If {@code segment} is a {@link DataSegmentAndLoadProfile}, the
historical materialized a real partial
+ * footprint and attached a {@link PartialLoadProfile}. The profile's
{@code fingerprint} and
+ * {@code loadedBytes} ride directly onto the wire form. This is the
accurate on-disk-footprint path.
+ * <li>Otherwise, if the segment's {@code loadSpec} is a {@link
PartialLoadSpec} wrapper (identified by wire-form
+ * conventions: a {@code type} starting with {@link
PartialLoadSpec#TYPE_PREFIX}, plus top-level
+ * {@code fingerprint} and {@code delegate} fields), the historical
was asked to partial-load but fell back
+ * to a full download via the inner delegate (zipped storage,
capability mismatch, etc.). {@code loadedBytes}
+ * is {@link DataSegment#getSize()} so the fingerprint still satisfies
the coordinator's partial-load rule and
+ * no reload-thrash occurs.
+ * </ul>
+ * Fallback detection is convention-based (no subtype allowlist) so future
{@link PartialLoadSpec} subtypes work
* automatically without touching this code.
* <p>
* For segments loaded without a partial-load wrapper (the common case),
this returns a bare load request with no
@@ -60,8 +67,13 @@ public class SegmentChangeRequestLoad implements
DataSegmentChangeRequest
*/
public static SegmentChangeRequestLoad forAnnouncement(DataSegment segment)
{
+ final PartialLoadProfile profile =
DataSegmentAndLoadProfile.profileOf(segment);
+ if (profile != null) {
+ return new SegmentChangeRequestLoad(segment, profile.fingerprint(),
profile.loadedBytes());
+ }
final Map<String, Object> loadSpec = segment.getLoadSpec();
if (PartialLoadSpec.detectPartialLoadSpec(loadSpec)) {
+ // Historical didn't wrap, treat as full-fallback: fingerprint from the
loadSpec, loadedBytes = full size.
return new SegmentChangeRequestLoad(segment, (String)
loadSpec.get("fingerprint"), segment.getSize());
}
if (PartialLoadSpec.hasPartialTypePrefix(loadSpec)) {
diff --git
a/server/src/main/java/org/apache/druid/server/coordination/SegmentLoadDropHandler.java
b/server/src/main/java/org/apache/druid/server/coordination/SegmentLoadDropHandler.java
index 6d8faf593cb..1e024fcc5bb 100644
---
a/server/src/main/java/org/apache/druid/server/coordination/SegmentLoadDropHandler.java
+++
b/server/src/main/java/org/apache/druid/server/coordination/SegmentLoadDropHandler.java
@@ -161,16 +161,20 @@ public class SegmentLoadDropHandler
currentDropLatch.cancelOrAwait();
}
+ final DataSegment loaded;
try {
- segmentManager.loadSegment(segment);
+ loaded = segmentManager.loadSegment(segment);
}
catch (Exception e) {
removeSegment(segment, DataSegmentChangeCallback.NOOP, false);
throw new SegmentLoadingException(e, "Exception loading segment[%s]",
segment.getId());
}
try {
- // Announce segment even if the segment file already exists.
- announcer.announceSegment(segment);
+ // Announce segment even if the segment file already exists.
loadSegment returns a
+ // PartialLoadedDataSegment wrapper when the historical actually
materialized a partial-load footprint;
+ // announcing that wrapper lets
SegmentChangeRequestLoad.forAnnouncement stamp accurate loadedBytes on
+ // the wire form, so the coordinator's inventory sees the partial
footprint rather than segment.getSize().
+ announcer.announceSegment(loaded);
}
catch (IOException e) {
throw new SegmentLoadingException(e, "Failed to announce segment[%s]",
segment.getId());
diff --git
a/server/src/main/java/org/apache/druid/server/coordinator/loading/PartialLoadProfile.java
b/server/src/main/java/org/apache/druid/server/coordinator/loading/PartialLoadProfile.java
index 4cd1cc2e224..04e1f86f658 100644
---
a/server/src/main/java/org/apache/druid/server/coordinator/loading/PartialLoadProfile.java
+++
b/server/src/main/java/org/apache/druid/server/coordinator/loading/PartialLoadProfile.java
@@ -33,26 +33,22 @@ import java.util.Objects;
* matches the shape of the wire-form load-spec wrapper that gets stamped onto
outbound load requests via
* {@code DataSegment.withLoadSpec(...)}.
* <p>
- * Used in three distinct states:
+ * Used in two distinct states:
* <ul>
* <li>{@link #forRequest(Map, String) forRequest}: outbound from
coordinator to historical. Carries the wrapped
* load-spec map identifying what to load and the fingerprint
identifying that request. {@code loadedBytes} is
* null because the on-disk footprint is only known after the historical
has parsed segment metadata.</li>
- * <li>{@link #forLoaded(Map, String, long) forLoaded}: inbound on a
historical announcement after a successful
- * partial load. {@code wrappedLoadSpec} echoes the request and {@code
loadedBytes} reports the realized
- * footprint.</li>
- * <li>{@link #forFullFallback(String, long) forFullFallback}: inbound on a
historical announcement when partial
- * loading was requested but the historical fell back to a full download
(zipped-V10 segment, capability
- * mismatch on a partially-upgraded server, etc.). {@code
wrappedLoadSpec} is null to signal the fallback;
- * {@code loadedBytes} equals the full segment size. The matching
fingerprint still satisfies the rule that
- * requested the load, so no reload-thrash occurs.</li>
+ * <li>{@link #forLoaded(Map, String, long) forLoaded}: inbound on a
historical announcement after a load completed.
+ * {@code wrappedLoadSpec} echoes the request and {@code loadedBytes}
reports the realized footprint. Covers
+ * both real partial loads and historicals that fell back to a full
download. The fingerprint match is what
+ * satisfies the coordinator's rule either way; the footprint number
rides through for capacity accounting.</li>
* </ul>
* The fingerprint is derived from the resolved scheme-specific data (e.g.,
for projections, the sorted/deduped name
* list, see {@code ProjectionPartialLoadMatcher}) so that two rule
configurations that resolve to the same set on a
* segment produce the same fingerprint and don't churn replicas across
coordinator runs.
*/
public record PartialLoadProfile(
- @Nullable Map<String, Object> wrappedLoadSpec,
+ Map<String, Object> wrappedLoadSpec,
String fingerprint,
@Nullable Long loadedBytes
)
@@ -66,10 +62,9 @@ public record PartialLoadProfile(
public PartialLoadProfile
{
+ Objects.requireNonNull(wrappedLoadSpec, "wrappedLoadSpec");
Objects.requireNonNull(fingerprint, "fingerprint");
- if (wrappedLoadSpec != null) {
- wrappedLoadSpec = Map.copyOf(wrappedLoadSpec);
- }
+ wrappedLoadSpec = Map.copyOf(wrappedLoadSpec);
}
/**
@@ -86,7 +81,9 @@ public record PartialLoadProfile(
}
/**
- * Build the inbound profile for a successful partial load announcement.
+ * Build the inbound profile for a completed load announcement. Applies to
both real partial loads (whose
+ * {@code loadedBytes} reflects the rule-declared on-disk footprint) and
full-download fallbacks (whose
+ * {@code loadedBytes} equals the full segment size).
*/
public static PartialLoadProfile forLoaded(Map<String, Object>
wrappedLoadSpec, String fingerprint, long loadedBytes)
{
@@ -96,26 +93,8 @@ public record PartialLoadProfile(
return intern(new PartialLoadProfile(wrappedLoadSpec, fingerprint,
loadedBytes));
}
- /**
- * Build the inbound profile for a historical that was asked to partial-load
but fell back to a full download.
- * {@code wrappedLoadSpec} is null as a sentinel; {@code loadedBytes} should
be the full segment size so that
- * inventory accounting reflects actual on-disk footprint.
- */
- public static PartialLoadProfile forFullFallback(String fingerprint, long
fullBytes)
- {
- return intern(new PartialLoadProfile(null, fingerprint, fullBytes));
- }
-
private static PartialLoadProfile intern(PartialLoadProfile profile)
{
return INTERNER.intern(profile);
}
-
- /**
- * Whether this profile represents a historical's full-fallback (i.e., it
was asked to partial-load but couldn't).
- */
- public boolean isFullFallback()
- {
- return wrappedLoadSpec == null;
- }
}
diff --git
a/server/src/main/java/org/apache/druid/server/coordinator/loading/SegmentHolder.java
b/server/src/main/java/org/apache/druid/server/coordinator/loading/SegmentHolder.java
index 968478cd875..39f1f8a2767 100644
---
a/server/src/main/java/org/apache/druid/server/coordinator/loading/SegmentHolder.java
+++
b/server/src/main/java/org/apache/druid/server/coordinator/loading/SegmentHolder.java
@@ -119,22 +119,11 @@ public class SegmentHolder implements
Comparable<SegmentHolder>
this.profile = profile;
if (action == SegmentAction.DROP) {
this.changeRequest = new SegmentChangeRequestDrop(segment);
- } else if (profile != null && profile.wrappedLoadSpec() != null) {
+ } else if (profile != null) {
// Stamp the wrapped load-spec map onto the outbound segment so the
historical receives the partial-load
// wrapper; identity (segment field) stays original so dedup in the load
queue is unaffected.
this.changeRequest = new
SegmentChangeRequestLoad(segment.withLoadSpec(profile.wrappedLoadSpec()));
} else {
- if (profile != null) {
- // Outbound profiles (forRequest / forLoaded) must always carry a
non-null wrappedLoadSpec; only the inbound
- // forFullFallback announcement uses the null sentinel, and those
never reach this constructor. Log if it
- // happens so the bug surfaces — we still proceed with a plain load
request to avoid stalling the queue.
- log.warn(
- "Profile with null wrappedLoadSpec on outbound load for
segment[%s], action[%s]; "
- + "queuing as a regular load.",
- segment.getId(),
- action
- );
- }
this.changeRequest = new SegmentChangeRequestLoad(segment);
}
if (callback != null) {
diff --git
a/server/src/test/java/org/apache/druid/client/DruidServerPartialLoadTest.java
b/server/src/test/java/org/apache/druid/client/DruidServerPartialLoadTest.java
index e41af11672e..e4e30f1b15a 100644
---
a/server/src/test/java/org/apache/druid/client/DruidServerPartialLoadTest.java
+++
b/server/src/test/java/org/apache/druid/client/DruidServerPartialLoadTest.java
@@ -105,17 +105,20 @@ public class DruidServerPartialLoadTest
}
@Test
- void testFullFallbackProfileUsesFullSegmentSize()
+ void testFullFallbackLoadedBytesUsesFullSegmentSize()
{
// Historical was asked to partial-load but fell back to full download;
profile.loadedBytes equals
// segment.getSize, so currSize accounting reflects the full footprint.
DruidServer server = newServer();
DataSegment segment = buildSegment("ds", "v1", 1000L);
- PartialLoadProfile profile =
PartialLoadProfile.forFullFallback(FINGERPRINT, segment.getSize());
+ PartialLoadProfile profile = PartialLoadProfile.forLoaded(
+ Map.of("type", "partialProjection", "fingerprint", FINGERPRINT),
+ FINGERPRINT,
+ segment.getSize()
+ );
server.addDataSegment(segment, profile);
Assertions.assertEquals(1000L, server.getCurrSize());
Assertions.assertEquals(profile,
server.getPartialLoadProfile(segment.getId()));
- Assertions.assertTrue(profile.isFullFallback());
}
@Test
diff --git
a/server/src/test/java/org/apache/druid/segment/loading/NoopSegmentCacheManager.java
b/server/src/test/java/org/apache/druid/segment/loading/NoopSegmentCacheManager.java
index 1db7a1618eb..2d9cfc39de3 100644
---
a/server/src/test/java/org/apache/druid/segment/loading/NoopSegmentCacheManager.java
+++
b/server/src/test/java/org/apache/druid/segment/loading/NoopSegmentCacheManager.java
@@ -71,13 +71,13 @@ public class NoopSegmentCacheManager implements
SegmentCacheManager
}
@Override
- public void load(DataSegment segment)
+ public DataSegment load(DataSegment segment)
{
throw new UnsupportedOperationException();
}
@Override
- public void bootstrap(DataSegment segment, SegmentLazyLoadFailCallback
loadFailed)
+ public DataSegment bootstrap(DataSegment segment,
SegmentLazyLoadFailCallback loadFailed)
{
throw new UnsupportedOperationException();
}
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 7edb4e7f40f..f63563474e4 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
@@ -22,6 +22,7 @@ package org.apache.druid.segment.loading;
import com.fasterxml.jackson.databind.InjectableValues;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.NamedType;
+import org.apache.druid.client.DataSegmentAndLoadProfile;
import org.apache.druid.data.input.InputRow;
import org.apache.druid.data.input.ListBasedInputRow;
import org.apache.druid.data.input.impl.AggregateProjectionSpec;
@@ -33,6 +34,7 @@ import org.apache.druid.jackson.SegmentizerModule;
import org.apache.druid.java.util.common.DateTimes;
import org.apache.druid.java.util.common.FileUtils;
import org.apache.druid.java.util.common.Intervals;
+import org.apache.druid.java.util.common.StringUtils;
import org.apache.druid.java.util.emitter.EmittingLogger;
import org.apache.druid.math.expr.ExprMacroTable;
import org.apache.druid.query.aggregation.CountAggregatorFactory;
@@ -51,6 +53,7 @@ import
org.apache.druid.segment.file.PartialSegmentFileMapperV10;
import org.apache.druid.segment.incremental.IncrementalIndexSchema;
import org.apache.druid.segment.projections.Projections;
import
org.apache.druid.segment.writeout.OffHeapMemorySegmentWriteOutMediumFactory;
+import org.apache.druid.server.coordinator.loading.PartialLoadProfile;
import org.apache.druid.server.metrics.NoopServiceEmitter;
import org.apache.druid.timeline.DataSegment;
import org.apache.druid.timeline.SegmentId;
@@ -289,6 +292,101 @@ class SegmentLocalCacheManagerPartialRuleLoadTest
);
}
+ @Test
+ void testLoadPartialReturnsDataSegmentAndLoadProfileWithRealizedBytes()
throws Exception
+ {
+ // load() on the partial-load path returns a DataSegmentAndLoadProfile
carrying a PartialLoadProfile.forLoaded with
+ // the actual on-disk footprint. Under a projection rule, that footprint
is metadata header + rule-selected
+ // projection bundle + __base (transitive dependency pinned via the
projection's parent-hold). Anything less
+ // would under-report to the coordinator: the historical downloads __base
as part of the rule (see
+ // awaitEagerDownloadsOrClearRule's dep expansion), so its bytes belong in
the announcement's loadedBytes.
+ manager = makeManager(true, true);
+ final StorageLocation location = manager.getLocations().get(0);
+
+ final DataSegment loaded =
manager.load(partialWrapperSegment(List.of(AGG_BUNDLE)));
+
+ Assertions.assertInstanceOf(
+ DataSegmentAndLoadProfile.class,
+ loaded,
+ "load on the partial-rule path must return a DataSegmentAndLoadProfile
so the announcer can report actual footprint"
+ );
+ final PartialLoadProfile profile = ((DataSegmentAndLoadProfile)
loaded).profile();
+ Assertions.assertEquals(FINGERPRINT, profile.fingerprint(), "profile's
fingerprint must match the applied rule");
+
+ // Sum the parts we expect: metadata + AGG_BUNDLE (rule-selected) + __base
(transitive dep).
+ final PartialSegmentMetadataCacheEntry metadata =
weakReservedMetadata(location, SEGMENT_ID);
+ final PartialSegmentBundleCacheEntry aggEntry = location.getCacheEntry(
+ new PartialSegmentBundleCacheEntryIdentifier(SEGMENT_ID, AGG_BUNDLE)
+ );
+ final PartialSegmentBundleCacheEntry baseEntry = location.getCacheEntry(
+ new PartialSegmentBundleCacheEntryIdentifier(SEGMENT_ID,
Projections.BASE_TABLE_PROJECTION_NAME)
+ );
+ Assertions.assertNotNull(aggEntry, "rule-selected projection bundle should
be present");
+ Assertions.assertNotNull(baseEntry, "transitive __base dependency should
be present");
+ final long expectedRealizedBytes = metadata.getSize() + aggEntry.getSize()
+ baseEntry.getSize();
+
+ Assertions.assertEquals(
+ Long.valueOf(expectedRealizedBytes),
+ profile.loadedBytes(),
+ "profile's loadedBytes must include metadata + rule-selected
projection + __base dependency; missing any of "
+ + "these would under-report the on-disk footprint to the coordinator"
+ );
+ Assertions.assertTrue(
+ baseEntry.getSize() > 0,
+ "sanity check: __base's size should be non-zero (so the dep-inclusion
assertion is meaningful)"
+ );
+ }
+ @Test
+ void testRealizedBytesIncludesPinnedBaseDependency() throws Exception
+ {
+ manager = makeManager(true, true);
+ final StorageLocation location = manager.getLocations().get(0);
+
+ manager.load(partialWrapperSegment(List.of(AGG_BUNDLE)));
+
+ final PartialSegmentMetadataCacheEntry metadata =
weakReservedMetadata(location, SEGMENT_ID);
+ final long metadataBytes = metadata.getSize();
+ final long aggBytes = bundleEntry(location, AGG_BUNDLE).getSize();
+ final long baseBytes = bundleEntry(location,
Projections.BASE_TABLE_PROJECTION_NAME).getSize();
+
+ Assertions.assertTrue(baseBytes > 0, "base dependency must occupy real
on-disk bytes");
+
+ final long realized = metadata.getRealizedBytes();
+
+ Assertions.assertEquals(
+ metadataBytes + aggBytes + baseBytes,
+ realized,
+ StringUtils.format(
+ "realizedBytes must include the pinned __base dependency:
metadata=%d + selected=%d + base=%d = %d, "
+ + "but got %d (short by %d, exactly the base footprint)",
+ metadataBytes, aggBytes, baseBytes, metadataBytes + aggBytes +
baseBytes,
+ realized, (metadataBytes + aggBytes + baseBytes) - realized
+ )
+ );
+ }
+
+ @Test
+ void testRealizedBytesCountsSharedBaseDependencyOnce() throws Exception
+ {
+ // Two projections in a single rule both pin the same __base. The base
bytes must be counted exactly once.
+ manager = makeManager(true, true);
+ final StorageLocation location = manager.getLocations().get(0);
+
+ manager.load(partialWrapperSegment(List.of(AGG_BUNDLE, OTHER_AGG_BUNDLE)));
+
+ final PartialSegmentMetadataCacheEntry metadata =
weakReservedMetadata(location, SEGMENT_ID);
+ final long metadataBytes = metadata.getSize();
+ final long aggBytes = bundleEntry(location, AGG_BUNDLE).getSize();
+ final long otherBytes = bundleEntry(location, OTHER_AGG_BUNDLE).getSize();
+ final long baseBytes = bundleEntry(location,
Projections.BASE_TABLE_PROJECTION_NAME).getSize();
+
+ Assertions.assertEquals(
+ metadataBytes + aggBytes + otherBytes + baseBytes,
+ metadata.getRealizedBytes(),
+ "realizedBytes must include the shared __base exactly once across both
dependent projections"
+ );
+ }
+
@Test
void testLoadWithoutVirtualStorageDoesNotInstallRuleHolds() throws Exception
{
@@ -590,6 +688,14 @@ class SegmentLocalCacheManagerPartialRuleLoadTest
}
}
+ private static CacheEntry bundleEntry(StorageLocation location, String
bundleName)
+ {
+ final CacheEntry entry =
+ location.getCacheEntry(new
PartialSegmentBundleCacheEntryIdentifier(SEGMENT_ID, bundleName));
+ Assertions.assertNotNull(entry, "bundle[" + bundleName + "] must be
resident after load");
+ return entry;
+ }
+
private SegmentLocalCacheManager makeManager(boolean virtualStorage, boolean
partialDownloadsEnabled)
{
return makeManagerAtLocations(virtualStorage, partialDownloadsEnabled,
List.of(cacheRoot));
diff --git
a/server/src/test/java/org/apache/druid/server/coordination/SegmentChangeRequestLoadTest.java
b/server/src/test/java/org/apache/druid/server/coordination/SegmentChangeRequestLoadTest.java
index 35d5bd7f35a..74ab1f34f58 100644
---
a/server/src/test/java/org/apache/druid/server/coordination/SegmentChangeRequestLoadTest.java
+++
b/server/src/test/java/org/apache/druid/server/coordination/SegmentChangeRequestLoadTest.java
@@ -21,10 +21,12 @@ package org.apache.druid.server.coordination;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.ImmutableMap;
+import org.apache.druid.client.DataSegmentAndLoadProfile;
import org.apache.druid.jackson.DefaultObjectMapper;
import org.apache.druid.java.util.common.Intervals;
import org.apache.druid.java.util.common.jackson.JacksonUtils;
import org.apache.druid.segment.IndexIO;
+import org.apache.druid.server.coordinator.loading.PartialLoadProfile;
import org.apache.druid.timeline.DataSegment;
import org.apache.druid.timeline.SegmentId;
import org.apache.druid.timeline.partition.NoneShardSpec;
@@ -142,9 +144,10 @@ public class SegmentChangeRequestLoadTest
@Test
public void testForAnnouncementPartialProjectionWrapperProducesFullFallback()
{
- // When the segment's loadSpec is a partialProjection wrapper, the
announcement stamps the wrapper's fingerprint
- // and the segment's full size as loadedBytes — coordinator reads this as
a full-fallback profile and counts the
- // replica as matching, avoiding reload thrash on historicals that don't
(yet) do real partial loading.
+ // Plain DataSegment carrying a partial-load wrapper (i.e. the historical
didn't attach a
+ // DataSegmentAndLoadProfile) is the full-fallback case: the wrapper's
fingerprint rides along, but loadedBytes
+ // is the segment's full size. Coordinator reads this as a fallback and
counts the replica as matching,
+ // avoiding reload thrash on historicals that don't (yet) do real partial
loading.
Map<String, Object> wrapped = Map.of(
"type", "partialProjection",
"delegate", Map.of("type", "local", "path", "/var/druid/segments/foo"),
@@ -162,6 +165,38 @@ public class SegmentChangeRequestLoadTest
Assert.assertEquals(Long.valueOf(12345L), announcement.getLoadedBytes());
}
+ @Test
+ public void
testForAnnouncementDataSegmentAndLoadProfileReportsProfileLoadedBytes()
+ {
+ // When the historical actually materialized a partial-load footprint,
load() returns a DataSegmentAndLoadProfile
+ // carrying a PartialLoadProfile.forLoaded(...) with the realized bytes.
forAnnouncement must stamp the profile's
+ // loadedBytes so HttpServerInventoryView detects it and capacity
accounting sees the actual on-disk footprint
+ // instead of the full segment size.
+ Map<String, Object> wrapped = Map.of(
+ "type", "partialProjection",
+ "delegate", Map.of("type", "local", "path", "/var/druid/segments/foo"),
+ "projections", List.of("revenue"),
+ "fingerprint", "v1:abcdef0123456789"
+ );
+ DataSegment segment = segmentBuilder("ds",
Intervals.of("2024-01-01/2024-02-01"), "v1")
+ .loadSpec(wrapped)
+ .dimensions(List.of("d"))
+ .metrics(List.of("m"))
+ .size(12345)
+ .build();
+ DataSegmentAndLoadProfile wrappedSegment = new DataSegmentAndLoadProfile(
+ segment,
+ PartialLoadProfile.forLoaded(wrapped, "v1:abcdef0123456789", 4321L)
+ );
+ SegmentChangeRequestLoad announcement =
SegmentChangeRequestLoad.forAnnouncement(wrappedSegment);
+ Assert.assertEquals("v1:abcdef0123456789", announcement.getFingerprint());
+ Assert.assertEquals(
+ "loadedBytes must come from the DataSegmentAndLoadProfile's
PartialLoadProfile, not segment.getSize()",
+ Long.valueOf(4321L),
+ announcement.getLoadedBytes()
+ );
+ }
+
@Test
public void
testForAnnouncementUnknownPartialTypeStillRecognizedByConvention()
{
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 607e5a3fa89..4c8fc0c4079 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
@@ -257,10 +257,11 @@ public class SegmentLoadDropHandlerTest
public void
testProcessBatchDuplicateLoadRequestsWhenFirstRequestFailsSecondRequestShouldSucceed()
throws Exception
{
final SegmentManager segmentManager = Mockito.mock(SegmentManager.class);
- Mockito.doThrow(new RuntimeException("segment loading failure test"))
- .doNothing()
- .when(segmentManager)
- .loadSegment(ArgumentMatchers.any());
+ // loadSegment returns DataSegment, so doNothing() would be rejected by
Mockito. Throw on the first call,
+ // then return the input segment on subsequent calls (the announcement
path uses the returned segment).
+ Mockito.when(segmentManager.loadSegment(ArgumentMatchers.any()))
+ .thenThrow(new RuntimeException("segment loading failure test"))
+ .thenAnswer(invocation -> invocation.getArgument(0));
final SegmentLoadDropHandler handler =
initSegmentLoadDropHandler(segmentManager);
@@ -291,7 +292,9 @@ public class SegmentLoadDropHandlerTest
public void testProcessBatchLoadDropLoadSequenceForSameSegment() throws
Exception
{
final SegmentManager segmentManager = Mockito.mock(SegmentManager.class);
-
Mockito.doNothing().when(segmentManager).loadSegment(ArgumentMatchers.any());
+ // loadSegment returns DataSegment; return the input so the announcement
path sees a plain DataSegment.
+ Mockito.when(segmentManager.loadSegment(ArgumentMatchers.any()))
+ .thenAnswer(invocation -> invocation.getArgument(0));
Mockito.doNothing().when(segmentManager).dropSegment(ArgumentMatchers.any());
final File storageDir = temporaryFolder.newFolder();
diff --git
a/server/src/test/java/org/apache/druid/server/coordinator/loading/PartialLoadProfileTest.java
b/server/src/test/java/org/apache/druid/server/coordinator/loading/PartialLoadProfileTest.java
index f6a919f5ac4..ce0f42b7c07 100644
---
a/server/src/test/java/org/apache/druid/server/coordinator/loading/PartialLoadProfileTest.java
+++
b/server/src/test/java/org/apache/druid/server/coordinator/loading/PartialLoadProfileTest.java
@@ -49,7 +49,6 @@ public class PartialLoadProfileTest
Assertions.assertEquals(WRAPPED, profile.wrappedLoadSpec());
Assertions.assertEquals(FINGERPRINT, profile.fingerprint());
Assertions.assertNull(profile.loadedBytes());
- Assertions.assertFalse(profile.isFullFallback());
}
@Test
@@ -83,7 +82,6 @@ public class PartialLoadProfileTest
Assertions.assertEquals(WRAPPED, profile.wrappedLoadSpec());
Assertions.assertEquals(FINGERPRINT, profile.fingerprint());
Assertions.assertEquals(12345L, profile.loadedBytes());
- Assertions.assertFalse(profile.isFullFallback());
}
@Test
@@ -98,16 +96,6 @@ public class PartialLoadProfileTest
);
}
- @Test
- public void testForFullFallback()
- {
- PartialLoadProfile profile =
PartialLoadProfile.forFullFallback(FINGERPRINT, 99999L);
- Assertions.assertNull(profile.wrappedLoadSpec());
- Assertions.assertEquals(FINGERPRINT, profile.fingerprint());
- Assertions.assertEquals(99999L, profile.loadedBytes());
- Assertions.assertTrue(profile.isFullFallback());
- }
-
@Test
public void testFingerprintRequired()
{
@@ -131,7 +119,7 @@ public class PartialLoadProfileTest
public void testEquals()
{
EqualsVerifier.forClass(PartialLoadProfile.class)
- .withNonnullFields("fingerprint")
+ .withNonnullFields("wrappedLoadSpec", "fingerprint")
.usingGetClass()
.verify();
}
@@ -155,11 +143,5 @@ public class PartialLoadProfileTest
// Different fingerprint ⇒ different profile, no sharing.
PartialLoadProfile pd = PartialLoadProfile.forLoaded(WRAPPED,
"v1:differentfingerprint", 12345L);
Assertions.assertNotSame(pa, pd);
-
- // Full-fallback variants intern independently of forLoaded variants.
- PartialLoadProfile fb1 = PartialLoadProfile.forFullFallback(FINGERPRINT,
12345L);
- PartialLoadProfile fb2 = PartialLoadProfile.forFullFallback(FINGERPRINT,
12345L);
- Assertions.assertSame(fb1, fb2);
- Assertions.assertNotSame(pa, fb1);
}
}
diff --git
a/server/src/test/java/org/apache/druid/server/coordinator/loading/StrategicSegmentAssignerPartialTest.java
b/server/src/test/java/org/apache/druid/server/coordinator/loading/StrategicSegmentAssignerPartialTest.java
index 83fd61d047a..e1cc1fb0d39 100644
---
a/server/src/test/java/org/apache/druid/server/coordinator/loading/StrategicSegmentAssignerPartialTest.java
+++
b/server/src/test/java/org/apache/druid/server/coordinator/loading/StrategicSegmentAssignerPartialTest.java
@@ -152,13 +152,18 @@ public class StrategicSegmentAssignerPartialTest
}
@Test
- public void testFullFallbackProfileCountsAsMatching()
+ public void testFullFallbackLoadedBytesCountsAsMatching()
{
- // s1 announced with a full-fallback profile (historical was asked to
partial-load but fell back to full). The
- // fingerprint matches the rule, so the rule is satisfied; no further
action.
+ // s1 announced with a loaded profile whose loadedBytes equals the full
segment size (historical was asked to
+ // partial-load but fell back to full). The fingerprint matches the rule,
so the rule is satisfied; no further
+ // action.
final DataSegment segment = createSegment();
- final PartialLoadProfile fullFallback =
PartialLoadProfile.forFullFallback(FP_REVENUE, 1024L);
- final ServerHolder s1 = createServerWithLoaded(TIER1, segment,
fullFallback);
+ final PartialLoadProfile fullSizeLoaded = PartialLoadProfile.forLoaded(
+ profileForRevenue().wrappedLoadSpec(),
+ FP_REVENUE,
+ segment.getSize()
+ );
+ final ServerHolder s1 = createServerWithLoaded(TIER1, segment,
fullSizeLoaded);
final ServerHolder s2 = createServer(TIER1);
final DruidCluster cluster = DruidCluster.builder().addTier(TIER1, s1,
s2).build();
diff --git
a/server/src/test/java/org/apache/druid/test/utils/TestSegmentCacheManager.java
b/server/src/test/java/org/apache/druid/test/utils/TestSegmentCacheManager.java
index 06059e6cd8d..f4cbb9dcd6d 100644
---
a/server/src/test/java/org/apache/druid/test/utils/TestSegmentCacheManager.java
+++
b/server/src/test/java/org/apache/druid/test/utils/TestSegmentCacheManager.java
@@ -103,17 +103,19 @@ public class TestSegmentCacheManager extends
NoopSegmentCacheManager
}
@Override
- public void bootstrap(DataSegment segment, SegmentLazyLoadFailCallback
loadFailed)
+ public DataSegment bootstrap(DataSegment segment,
SegmentLazyLoadFailCallback loadFailed)
{
observedBootstrapSegments.add(segment);
getSegmentInternal(segment);
+ return segment;
}
@Override
- public void load(final DataSegment segment)
+ public DataSegment load(final DataSegment segment)
{
observedSegments.add(segment);
getSegmentInternal(segment);
+ return segment;
}
private ReferenceCountedSegmentProvider getSegmentInternal(final DataSegment
segment)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]