FrankChen021 commented on code in PR #19671:
URL: https://github.com/apache/druid/pull/19671#discussion_r3558725842


##########
server/src/main/java/org/apache/druid/segment/loading/SegmentLocalCacheManager.java:
##########
@@ -872,6 +937,290 @@ private record ReservedPartial(
   {
   }
 
+  /**
+   * Apply (or reconcile) a partial-load rule for a segment coming through 
{@link #load}. The rule state lives on the
+   * {@link PartialSegmentMetadataCacheEntry} itself; this method just drives 
it:
+   * <ol>
+   *   <li>Materialize the wrapper's {@link PartialLoadSpec} and open a range 
reader over the backend. If the backend
+   *       can't do range reads, clear any prior rule on the segment and 
return; the segment falls back to the
+   *       weak-full-load path at query time.</li>
+   *   <li>Acquire (or reserve) the metadata entry via the shared on-demand 
path ({@link #findOrReservePartial}),
+   *       take a transient eviction-protective hold across the rest of the 
flow.</li>
+   *   <li>Mount the metadata entry (idempotent).</li>
+   *   <li>Compute the rule's selected bundle names from the wrapper + parsed 
segment metadata, and call
+   *       {@link PartialSegmentMetadataCacheEntry#applyRule} which installs 
the metadata self-hold and takes
+   *       holds on any already-registered selected bundles.</li>
+   *   <li>Kick off async eager downloads on the loading pool for any selected 
bundle not yet linked. Each async task
+   *       drives {@link 
PartialSegmentMetadataCacheEntry#ensureBundleResidentForRule}: its bundle mount 
will call
+   *       {@link PartialSegmentMetadataCacheEntry#registerBundle}, which 
acquires the rule-hold on that bundle.</li>
+   *   <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
+  {
+    final PartialLoadSpec wrapper = materializePartialLoadSpec(dataSegment);
+    final SegmentRangeReader rangeReader = openPartialRangeReader(dataSegment, 
wrapper);
+    final ReferenceCountingLock lock = lock(dataSegment);
+    synchronized (lock) {
+      try {
+        // If a stale non-partial cache entry sits at this segment id (a 
CompleteSegmentCacheEntry created by a prior
+        // acquireSegment while virtualStoragePartialDownloadsEnabled=false, 
for example), evict it before any
+        // partial-entry lookup or reservation; otherwise 
findExistingPartialWithHold's defensive type-check would
+        // throw, and reservePartial's addWeakReservationHold would land on 
the incompatible entry. If the stale
+        // entry is currently held (in-flight query), this throws a retryable 
SegmentLoadingException; the
+        // coordinator's load queue retries on next sync, and by then the 
query should have released.
+        evictStaleNonPartialWeakEntry(dataSegment.getId());
+
+        if (rangeReader == null) {
+          // Backend doesn't support range reads (e.g. zipped deep storage). 
The rule can't be honored as a partial
+          // load; clear any prior rule so the segment falls through to the 
ordinary weak-full-load path at query
+          // time.
+          final ReservedPartial existing = 
findExistingPartialWithHold(dataSegment.getId());
+          if (existing != null) {
+            try {
+              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",
+                  dataSegment.getId(),
+                  wrapper.getFingerprint()
+              );
+            }
+            finally {
+              CloseableUtils.closeAndSuppressExceptions(
+                  existing.hold(),
+                  t -> log.warn(t, "Failed to release transient hold on 
partial metadata for segment[%s]",
+                                dataSegment.getId())
+              );
+            }
+          } else {
+            // No prior entry AND range-reader is null on this fresh load: the 
coordinator asked for a partial rule
+            // this historical can't honor. Log so operators can debug "why 
isn't my rule applied on historical X".
+            log.warn(
+                "Backend for segment[%s] does not support range reads and no 
prior partial entry exists; rule"
+                + "[fingerprint=%s] cannot be applied on this historical",
+                dataSegment.getId(),
+                wrapper.getFingerprint()
+            );
+          }
+          return;
+        }
+
+        final ReservedPartial reserved = findOrReservePartial(dataSegment, 
rangeReader);
+        try {
+          final PartialSegmentMetadataCacheEntry metadata = 
reserved.metadata();
+          try {
+            metadata.mount(reserved.location());
+          }
+          catch (IOException e) {
+            throw new SegmentLoadingException(
+                e,
+                "Failed to mount partial metadata for segment[%s]",
+                dataSegment.getId()
+            );
+          }
+          final PartialSegmentFileMapperV10 mapper = metadata.getFileMapper();
+          if (mapper == null) {
+            throw DruidException.defensive(
+                "Partial metadata for segment[%s] mounted without a file 
mapper",
+                dataSegment.getId()
+            );
+          }
+          // Register the info-file cleanup hook BEFORE anything that could 
throw. Any throw between mount and
+          // awaitEagerDownloadsOrClearRule leaves the metadata entry 
weak-reserved with the hook attached; when
+          // cache eventually reclaims, the info file gets deleted along with 
the entry.
+          metadata.setOnUnmount(() -> deleteSegmentInfoFile(dataSegment));
+          final Set<String> selected = Set.copyOf(
+              wrapper.getSelectedBundleNames(dataSegment, 
mapper.getSegmentFileMetadata())
+          );
+          final String priorFingerprint = metadata.getRuleFingerprint();
+          metadata.applyRule(wrapper.getFingerprint(), selected);
+          if (priorFingerprint != null && 
!priorFingerprint.equals(wrapper.getFingerprint())) {
+            log.info(
+                "Reconciled partial-load rule for segment[%s]: fingerprint 
transitioned [%s] → [%s]",
+                dataSegment.getId(),
+                priorFingerprint,
+                wrapper.getFingerprint()
+            );
+          }
+          // Block until every eager download completes so the announcement 
fingerprint reflects reality: any failure
+          // clears the rule state (releasing self-hold + all bundle 
rule-holds) and propagates as a load failure so
+          // the coordinator's load queue can retry on its next sync. The 
announced fingerprint == "rule fully
+          // realized" contract stays intact.
+          awaitEagerDownloadsOrClearRule(dataSegment, metadata, selected);
+        }
+        finally {
+          CloseableUtils.closeAndSuppressExceptions(
+              reserved.hold(),
+              t -> log.warn(t, "Failed to release transient hold on partial 
metadata for segment[%s]",
+                            dataSegment.getId())
+          );
+        }
+      }
+      finally {
+        unlock(dataSegment, lock);
+      }
+    }
+  }
+
+  /**
+   * Submit eager-download tasks to the loading pool for every rule-selected 
bundle not yet registered with
+   * {@code metadata}, then block until every task completes. On any failure 
the rule state is cleared
+   * and a {@link SegmentLoadingException} is thrown so the caller treats the 
load as failed and retries.
+   */
+  private void awaitEagerDownloadsOrClearRule(
+      DataSegment dataSegment,
+      PartialSegmentMetadataCacheEntry metadata,
+      Set<String> selected
+  ) throws SegmentLoadingException
+  {
+    final List<Future<?>> pending = new ArrayList<>();
+    Throwable firstFailure = null;
+    try {
+      for (String bundleName : selected) {
+        // Skip only when the bundle is BOTH rule-held AND fully downloaded 
(every container's files present on disk).
+        if (metadata.isBundleRuleHeld(bundleName) && 
metadata.isBundleFullyDownloaded(bundleName)) {
+          continue;
+        }
+        
pending.add(virtualStorageLoadingThreadPool.getExecutorService().submit(() -> {
+          metadata.ensureBundleResidentForRule(bundleName);

Review Comment:
   [P1] Download transitive bundle dependencies
   
   `ensureBundleResidentForRule` downloads only the selected bundle, although 
mounting a projection may infer `__base` as a required dependency. The 
dependency is sparse-mounted and held, but its files are never eagerly 
downloaded; the otherwise-unused `bundlesInMountOrder` confirms this missing 
expansion. Queries can therefore perform on-demand deep-storage reads after the 
rule was reported fully realized, and fail if deep storage is unavailable.



##########
server/src/main/java/org/apache/druid/segment/loading/SegmentLocalCacheManager.java:
##########
@@ -927,6 +1276,15 @@ public void load(final DataSegment dataSegment) throws 
SegmentLoadingException
             "load() should not be called when virtualStorageIsEphemeral is 
true"
         );
       }
+      // Partial-load-rule routing: a wrapped load spec carrying a fingerprint 
+ per-spec selection (cluster groups,
+      // projections, ...) means the coordinator wants this segment loaded 
with rule holds pinning the selected
+      // bundles + metadata. Install the holds via loadPartial; other segments 
(no wrapper, or partials disabled)
+      // take the existing weak/no-op path below.
+      if (config.isVirtualStoragePartialDownloadsEnabled()
+          && PartialLoadSpec.detectPartialLoadSpec(dataSegment.getLoadSpec())) 
{
+        loadPartial(dataSegment);

Review Comment:
   [P1] Announce the realized partial footprint
   
   After this new partial path succeeds, `SegmentLoadDropHandler` still 
announces the original `DataSegment`, and unchanged 
`SegmentChangeRequestLoad.forAnnouncement` stamps `loadedBytes = 
segment.getSize()`. `HttpServerInventoryView` consequently classifies every 
successful partial load as a full fallback, overstates server disk usage, and 
can make coordinator capacity accounting reject otherwise-valid placements. The 
successful path needs to expose its realized loaded bytes to announcement 
generation.



##########
server/src/main/java/org/apache/druid/segment/loading/PartialSegmentMetadataCacheEntry.java:
##########
@@ -199,6 +230,322 @@ File getLocalCacheDir()
     return localCacheDir;
   }
 
+  /**
+   * The fingerprint of the partial-load rule currently applied to this entry, 
or {@code null} if no rule has been
+   * applied. Set by {@link #applyRule(String, Set)} and cleared by {@link 
#clearRule()}. {@link #doActualUnmount()}
+   * asserts that this field is {@code null} at unmount time, the {@code 
metadataSelfHold} taken by applyRule pins
+   * the entry's phaser and prevents eviction reclaim / doActualUnmount from 
firing while a rule is applied. A fresh
+   * mount therefore always starts with no rule applied.
+   */
+  @Nullable
+  public String getRuleFingerprint()
+  {
+    entryLock.lock();
+    try {
+      return ruleFingerprint;
+    }
+    finally {
+      entryLock.unlock();
+    }
+  }
+
+  /**
+   * The set of bundle names currently pinned by an applied partial-load rule, 
or an empty set if no rule is applied.
+   * Return value is immutable; safe to inspect without further 
synchronization.
+   */
+  public Set<String> getRuleSelectedBundleNames()
+  {
+    entryLock.lock();
+    try {
+      return ruleSelectedBundleNames;
+    }
+    finally {
+      entryLock.unlock();
+    }
+  }
+
+  /**
+   * Whether a partial-load rule is currently applied to this entry. 
Equivalent to {@code getRuleFingerprint() != null}.
+   * While {@code true}, this entry holds a self-referential {@link 
StorageLocation.ReservationHold} that prevents cache
+   * from evicting it, so the metadata's V10 header stays resident until 
{@link #clearRule()} runs.
+   */
+  public boolean isRuleHeld()
+  {
+    entryLock.lock();
+    try {
+      return ruleFingerprint != null;
+    }
+    finally {
+      entryLock.unlock();
+    }
+  }
+
+  /**
+   * Apply (or replace) a partial-load rule on this entry. Must be called 
after {@link #mount(StorageLocation)}, the
+   * entry must be registered with its {@link StorageLocation} so it can 
acquire holds on itself and on its bundles.
+   * <p>
+   * <b>Concurrency contract.</b> The caller MUST serialize {@code applyRule} 
and {@link #clearRule} for a given
+   * segment id through an external per-segment lock ({@code 
SegmentLocalCacheManager} uses its
+   * {@code ReferenceCountingLock segmentLocks}). This method releases {@link 
#entryLock} between Phase 2 (acquire
+   * holds outside the lock) and Phase 4 (release holds outside the lock); a 
concurrent invocation of
+   * {@code applyRule}/{@code clearRule} on the same entry can observe 
intermediate state during those windows and
+   * silently miss rule-hold installations or leak them. {@link #entryLock} 
alone is insufficient because it is
+   * released across the acquire/release phases by design (holding it across 
{@link StorageLocation} lock acquisition
+   * would invert the writeLock &rarr; entryLock ordering the storage layer 
already uses for eviction).
+   * <p>
+   * On first application ({@code ruleFingerprint} transitions from {@code 
null}), a self-referential
+   * {@link StorageLocation.ReservationHold} is taken to pin the metadata 
entry in-cache. On repeat calls with the same
+   * {@code fingerprint} and matching {@code selectedBundleNames}, this is a 
no-op. On a genuine rule swap, only the
+   * delta between the previous and new selection is applied: bundle holds for 
names dropped from the selection are
+   * closed, and holds for names newly added are acquired for any bundle 
already registered with this entry (later
+   * arrivals get their hold via {@link #registerBundle}).
+   * <p>
+   * Bundles whose {@link StorageLocation.ReservationHold} is currently 
zero-refcount (never mounted, or evicted but
+   * unregistered before this call) will not appear in {@link #linkedBundles} 
yet, so no hold is taken for them here;
+   * the caller is expected to drive an on-demand acquire per selected bundle 
name to trigger a fresh mount, which will
+   * call {@link #registerBundle} and pick up the rule-hold at that point.
+   *
+   * @throws DruidException if the entry is not currently mounted
+   */
+  public void applyRule(String fingerprint, Set<String> selectedBundleNames)
+  {
+    Objects.requireNonNull(fingerprint, "fingerprint");
+    Objects.requireNonNull(selectedBundleNames, "selectedBundleNames");
+    final Set<String> newSelection = Set.copyOf(selectedBundleNames);
+
+    // Phase 1: snapshot under entryLock and compute the diff. Do NOT call 
StorageLocation methods here — that would
+    // acquire the location's readLock while holding entryLock and could 
deadlock with a concurrent writeLock holder
+    // that needs entryLock
+    final StorageLocation loc;
+    final boolean needsSelfHold;
+    final List<String> namesToRelease;
+    final List<String> namesToAcquire;
+    entryLock.lock();
+    try {
+      if (location == null) {
+        throw DruidException.defensive(
+            "applyRule on partial metadata entry[%s] requires the entry to be 
mounted",
+            id
+        );
+      }
+      if (fingerprint.equals(ruleFingerprint) && 
newSelection.equals(ruleSelectedBundleNames)) {
+        return;
+      }
+      loc = location;
+      needsSelfHold = (metadataSelfHold == null);
+      namesToRelease = new ArrayList<>();
+      for (String name : ruleBundleHolds.keySet()) {
+        if (!newSelection.contains(name)) {
+          namesToRelease.add(name);
+        }
+      }
+      namesToAcquire = new ArrayList<>();
+      for (String name : newSelection) {
+        if (!ruleBundleHolds.containsKey(name) && findLinkedBundleByName(name) 
!= null) {

Review Comment:
   [P2] Close the concurrent registration gap
   
   A query can insert a newly mounted bundle into `linkedBundles` after this 
snapshot but before `ruleSelectedBundleNames` is committed. `registerBundle` 
observes the old selection and skips its hold, while the eager reconciliation 
later acquires the already-mounted bundle without invoking `registerBundle` 
again. The selected bundle remains permanently absent from `ruleBundleHolds` 
and can be SIEVE-evicted despite the applied rule.



##########
server/src/main/java/org/apache/druid/segment/loading/SegmentLocalCacheManager.java:
##########
@@ -872,6 +937,290 @@ private record ReservedPartial(
   {
   }
 
+  /**
+   * Apply (or reconcile) a partial-load rule for a segment coming through 
{@link #load}. The rule state lives on the
+   * {@link PartialSegmentMetadataCacheEntry} itself; this method just drives 
it:
+   * <ol>
+   *   <li>Materialize the wrapper's {@link PartialLoadSpec} and open a range 
reader over the backend. If the backend
+   *       can't do range reads, clear any prior rule on the segment and 
return; the segment falls back to the
+   *       weak-full-load path at query time.</li>
+   *   <li>Acquire (or reserve) the metadata entry via the shared on-demand 
path ({@link #findOrReservePartial}),
+   *       take a transient eviction-protective hold across the rest of the 
flow.</li>
+   *   <li>Mount the metadata entry (idempotent).</li>
+   *   <li>Compute the rule's selected bundle names from the wrapper + parsed 
segment metadata, and call
+   *       {@link PartialSegmentMetadataCacheEntry#applyRule} which installs 
the metadata self-hold and takes
+   *       holds on any already-registered selected bundles.</li>
+   *   <li>Kick off async eager downloads on the loading pool for any selected 
bundle not yet linked. Each async task
+   *       drives {@link 
PartialSegmentMetadataCacheEntry#ensureBundleResidentForRule}: its bundle mount 
will call
+   *       {@link PartialSegmentMetadataCacheEntry#registerBundle}, which 
acquires the rule-hold on that bundle.</li>
+   *   <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
+  {
+    final PartialLoadSpec wrapper = materializePartialLoadSpec(dataSegment);
+    final SegmentRangeReader rangeReader = openPartialRangeReader(dataSegment, 
wrapper);
+    final ReferenceCountingLock lock = lock(dataSegment);
+    synchronized (lock) {
+      try {
+        // If a stale non-partial cache entry sits at this segment id (a 
CompleteSegmentCacheEntry created by a prior
+        // acquireSegment while virtualStoragePartialDownloadsEnabled=false, 
for example), evict it before any
+        // partial-entry lookup or reservation; otherwise 
findExistingPartialWithHold's defensive type-check would
+        // throw, and reservePartial's addWeakReservationHold would land on 
the incompatible entry. If the stale
+        // entry is currently held (in-flight query), this throws a retryable 
SegmentLoadingException; the
+        // coordinator's load queue retries on next sync, and by then the 
query should have released.
+        evictStaleNonPartialWeakEntry(dataSegment.getId());
+
+        if (rangeReader == null) {
+          // Backend doesn't support range reads (e.g. zipped deep storage). 
The rule can't be honored as a partial
+          // load; clear any prior rule so the segment falls through to the 
ordinary weak-full-load path at query
+          // time.
+          final ReservedPartial existing = 
findExistingPartialWithHold(dataSegment.getId());
+          if (existing != null) {
+            try {
+              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",
+                  dataSegment.getId(),
+                  wrapper.getFingerprint()
+              );
+            }
+            finally {
+              CloseableUtils.closeAndSuppressExceptions(
+                  existing.hold(),
+                  t -> log.warn(t, "Failed to release transient hold on 
partial metadata for segment[%s]",
+                                dataSegment.getId())
+              );
+            }
+          } else {
+            // No prior entry AND range-reader is null on this fresh load: the 
coordinator asked for a partial rule
+            // this historical can't honor. Log so operators can debug "why 
isn't my rule applied on historical X".
+            log.warn(
+                "Backend for segment[%s] does not support range reads and no 
prior partial entry exists; rule"
+                + "[fingerprint=%s] cannot be applied on this historical",
+                dataSegment.getId(),
+                wrapper.getFingerprint()
+            );
+          }
+          return;
+        }
+
+        final ReservedPartial reserved = findOrReservePartial(dataSegment, 
rangeReader);

Review Comment:
   [P2] Persist rule changes when reusing metadata
   
   `findOrReservePartial` returns an existing entry without passing through 
`reservePartial`, the only partial path that rewrites the info file. A 
successful fingerprint/selection change is therefore applied in memory while 
disk retains the prior wrapped load spec; after restart, bootstrap re-applies 
and announces the old rule until the coordinator corrects it.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to