github-advanced-security[bot] commented on code in PR #19683:
URL: https://github.com/apache/druid/pull/19683#discussion_r3578921307


##########
processing/src/main/java/org/apache/druid/segment/file/PartialSegmentFileMapperV10.java:
##########
@@ -487,58 +537,290 @@
   }
 
   /**
-   * Download an entire container in a single range read and mark every 
internal file it holds as downloaded. The whole
-   * region streams straight to the container's local sparse file at offset 0.
+   * Ensure the given internal files are resident using as few deep-storage 
range reads as possible. Files are grouped
+   * by container; within a container, requested files that are adjacent (they 
tile back-to-back with no padding) are
+   * coalesced into a single range read, and two runs separated by unrequested 
files are merged into one read when the
+   * separating byte gap is at most {@code coalesceGapBytes}. Bridged gap 
files are whole, valid internal files, so
+   * they are marked downloaded too: the "extra" bytes become useful cached 
data rather than waste, trading at most
+   * {@code coalesceGapBytes} of extra bandwidth per bridged gap (a run can 
bridge several gaps) for one fewer
+   * deep-storage round trip each. Already-resident files are never 
re-fetched; a resident file inside a gap always
+   * splits the run. Unknown names are ignored. Runs never span containers.
    * <p>
-   * No-op when every file in the container is already downloaded. A 
partially-downloaded container is re-fetched in
-   * full (already-present files are overwritten with byte-identical data). 
Holds the container lock for the whole
-   * fetch so a concurrent {@link #evictContainer} or container-init can't 
race the write; concurrent per-file
-   * {@link #ensureFileDownloaded} calls write byte-identical data so they 
remain safe, and the download-bookkeeping is
-   * gated on the atomic {@link #downloadedFiles} add so neither path 
double-counts.
+   * Callers must hold the same eviction-exclusion the {@link #mapFile} 
contract requires (see {@link #evictContainer});
+   * only per-file locks are held across the wire fetch, deliberately not the 
container lock, so unrelated container
+   * init/eviction and downloads of non-member files proceed in parallel. A 
concurrent {@link #mapFile} of a run member
+   * blocks on that member's lock and finds the file resident on wake.
    */
-  private void downloadContainer(int containerIndex) throws IOException
+  public void fetchFiles(Collection<String> fileNames) throws IOException
   {
-    final List<String> fileNames = containerFileNames.get(containerIndex);
-    if (fileNames.isEmpty() || downloadedFiles.containsAll(fileNames)) {
-      return;
+    for (FetchRun run : planFetch(fileNames)) {
+      fetchRun(run);
     }
-    containerLocks[containerIndex].lock();
-    try {
-      checkClosed();
-      if (downloadedFiles.containsAll(fileNames)) {
-        return;
+  }
+
+  /**
+   * Plan the coalesced range reads that {@link #fetchFiles} would execute for 
the given internal files, without
+   * fetching anything. Callers that want the reads to proceed concurrently 
(each is an independent deep-storage
+   * request) submit each returned run to an executor as its own {@link 
#fetchRun} call instead of using the
+   * sequential {@code fetchFiles}; the runs of one plan cover disjoint file 
sets, so they can execute in any order
+   * and in parallel. Plans are advisory snapshots: files that become resident 
between planning and fetching are
+   * skipped or trimmed by {@code fetchRun}'s under-lock re-check, so a stale 
plan costs at most a redundant no-op.
+   */
+  public List<FetchRun> planFetch(Collection<String> fileNames)
+  {
+    checkClosed();
+
+    // group the not-yet-resident requested files by container, ascending 
container index so a plan's run order is
+    // deterministic (locking happens per-file inside fetchRun; runs are 
order-independent)
+    final TreeMap<Integer, Set<String>> requestedByContainer = new TreeMap<>();
+    for (String name : fileNames) {
+      final SegmentInternalFileMetadata fileMetadata = 
metadata.getFiles().get(name);
+      if (fileMetadata != null && !downloadedFiles.contains(name)) {
+        requestedByContainer.computeIfAbsent(fileMetadata.getContainer(), k -> 
new HashSet<>()).add(name);
       }
-      ensureContainerInitialized(containerIndex);
+    }
 
-      final SegmentFileContainerMetadata containerMeta = 
metadata.getContainers().get(containerIndex);
-      streamRangeIntoContainer(
-          containerIndex,
-          headerSize + containerMeta.getStartOffset(),
-          0,
-          containerMeta.getSize(),
-          StringUtils.format("container[%d]", containerIndex)
+    final List<FetchRun> runs = new ArrayList<>();
+    for (Map.Entry<Integer, Set<String>> entry : 
requestedByContainer.entrySet()) {
+      runs.addAll(
+          planFetchRuns(
+              entry.getKey(),
+              containerFileNames.get(entry.getKey()),
+              metadata.getFiles(),
+              entry.getValue(),
+              downloadedFiles,
+              coalesceGapBytes
+          )
       );
+    }
+    return runs;
+  }
 
-      for (String name : fileNames) {
-        markDownloaded(name, metadata.getFiles().get(name).getSize());
+  /**
+   * {@link #planFetch} for callers that execute the runs concurrently: 
additionally splits any run larger than the
+   * configured maximum at file boundaries, so a large fetch spreads across 
multiple parallel deep-storage
+   * connections. Do not use for sequential execution — splitting there only 
adds request overhead; that's why the cap
+   * doesn't apply to {@link #planFetch}/{@link #fetchFiles} and the eager 
download paths built on them.
+   */
+  public List<FetchRun> planParallelFetch(Collection<String> fileNames)
+  {
+    return splitRuns(planFetch(fileNames), metadata.getFiles(), 
maxFetchRunBytes);
+  }
+
+  /**
+   * Plan the parallel-capped range reads for every not-yet-resident file in 
{@code bundleName}'s containers, across
+   * this mapper AND every attached external mapper so runs come back paired 
with their owning mapper, unlike the
+   * self-scoped {@link #planParallelFetch}. Empty for an unknown bundle.
+   */
+  public List<PlannedFetch> planParallelFetchBundle(String bundleName)
+  {
+    checkClosed();
+    final List<PlannedFetch> fetches = new ArrayList<>();
+    for (int containerIndex : getContainerIndicesForBundle(bundleName)) {
+      // the container's file list is already grouped and offset-sorted, so 
plan it directly rather than going
+      // through planFetch's group-by-container pass
+      final List<String> fileNames = containerFileNames.get(containerIndex);
+      final List<FetchRun> runs = splitRuns(
+          planFetchRuns(
+              containerIndex,
+              fileNames,
+              metadata.getFiles(),
+              Set.copyOf(fileNames),
+              downloadedFiles,
+              coalesceGapBytes
+          ),
+          metadata.getFiles(),
+          maxFetchRunBytes
+      );
+      for (FetchRun run : runs) {
+        fetches.add(new PlannedFetch(this, run));
       }
     }
-    finally {
-      containerLocks[containerIndex].unlock();
+    for (PartialSegmentFileMapperV10 external : externalMappers.values()) {
+      fetches.addAll(external.planParallelFetchBundle(bundleName));
     }
+    return fetches;
   }
 
   /**
-   * Pre-download a set of internal files so that subsequent {@link 
#mapFile(String)} calls for these files will not
-   * trigger individual downloads. Files that are already downloaded are 
skipped. Useful for batch-downloading all
-   * files in a bundle at once (see {@link 
SegmentFileBuilder#startFileBundle}).
+   * Split any run larger than {@code maxRunBytes} into consecutive sub-runs 
of at most that size, cutting only at
+   * file boundaries so each sub-run still marks exactly its own whole files 
downloaded (the bitmap tracks residency
+   * per file, so a read must never end mid-file). A single file larger than 
the cap forms its own sub-run since
+   * splitting within a file would need sub-file bookkeeping; internal files 
are bounded (large columns split into
+   * multiple internal files at write time) so this caps the worst case 
acceptably. {@code maxRunBytes <= 0} disables
+   * splitting.
    */
-  public void ensureFilesAvailable(Set<String> fileNames) throws IOException
+  @VisibleForTesting
+  static List<FetchRun> splitRuns(
+      List<FetchRun> runs,
+      Map<String, SegmentInternalFileMetadata> fileMetadata,
+      long maxRunBytes
+  )
   {
-    for (String name : fileNames) {
-      final SegmentInternalFileMetadata fileMetadata = 
metadata.getFiles().get(name);
-      if (fileMetadata != null) {
-        ensureFileDownloaded(name, fileMetadata);
+    if (maxRunBytes <= 0) {
+      return runs;
+    }
+    final List<FetchRun> split = new ArrayList<>(runs.size());
+    for (FetchRun run : runs) {
+      if (run.length() <= maxRunBytes) {
+        split.add(run);
+        continue;
+      }
+      List<String> groupFiles = new ArrayList<>();
+      long groupStart = run.startOffset();
+      long groupEnd = groupStart;
+      for (String file : run.files()) {
+        final SegmentInternalFileMetadata fileMeta = fileMetadata.get(file);
+        final long fileEnd = fileMeta.getStartOffset() + fileMeta.getSize();
+        if (!groupFiles.isEmpty() && fileEnd - groupStart > maxRunBytes) {
+          split.add(new FetchRun(run.containerIndex(), groupStart, groupEnd - 
groupStart, List.copyOf(groupFiles)));
+          groupFiles = new ArrayList<>();
+          groupStart = fileMeta.getStartOffset();
+          groupEnd = groupStart;
+        }
+        groupFiles.add(file);
+        // max, not assignment: a zero-length file sharing a neighbor's start 
offset must never shrink the sub-run
+        groupEnd = Math.max(groupEnd, fileEnd);
+      }
+      split.add(new FetchRun(run.containerIndex(), groupStart, groupEnd - 
groupStart, List.copyOf(groupFiles)));
+    }
+    return split;
+  }
+
+  /**
+   * Plan the coalesced range reads for one container. {@code filesByOffset} 
is the container's complete file list in
+   * ascending start-offset order; a run opens at a requested non-resident 
file and extends across subsequent files,
+   * bridging unrequested non-resident files whenever the byte gap back to the 
run's current end is within
+   * {@code gapTolerance} once the next requested file is reached. Resident 
files always split (their bytes are never
+   * re-fetched), and a run never ends with unfetched trailing gap bytes. Pure 
function over its arguments; the caller
+   * re-checks residency under per-file locks before fetching since {@code 
resident} may grow concurrently.
+   */
+  @VisibleForTesting
+  static List<FetchRun> planFetchRuns(
+      int containerIndex,
+      List<String> filesByOffset,
+      Map<String, SegmentInternalFileMetadata> fileMetadata,
+      Set<String> requested,
+      Set<String> resident,
+      long gapTolerance
+  )
+  {
+    final List<FetchRun> runs = new ArrayList<>();
+    List<String> runFiles = null;
+    long runStart = 0;
+    // once a run is open, runEnd only ever grows (Math.max, never plain 
assignment): zero-length internal files
+    // (all-null columns) share a start offset with a neighboring file, and a 
plain overwrite could shrink the run
+    // and mark a covered file downloaded without having fetched all of its 
bytes
+    long runEnd = 0;
+    final List<String> pendingGapFiles = new ArrayList<>();
+
+    for (String name : filesByOffset) {
+      if (resident.contains(name)) {
+        if (runFiles != null) {
+          runs.add(new FetchRun(containerIndex, runStart, runEnd - runStart, 
List.copyOf(runFiles)));
+          runFiles = null;
+        }
+        pendingGapFiles.clear();
+      } else if (requested.contains(name)) {
+        final SegmentInternalFileMetadata fileMeta = fileMetadata.get(name);
+        final long fileEnd = fileMeta.getStartOffset() + fileMeta.getSize();
+        if (runFiles != null && fileMeta.getStartOffset() - runEnd <= 
gapTolerance) {
+          for (String gapName : pendingGapFiles) {
+            final SegmentInternalFileMetadata gapMeta = 
fileMetadata.get(gapName);
+            runEnd = Math.max(runEnd, gapMeta.getStartOffset() + 
gapMeta.getSize());
+          }
+          runFiles.addAll(pendingGapFiles);
+          runFiles.add(name);
+          runEnd = Math.max(runEnd, fileEnd);
+        } else {
+          if (runFiles != null) {
+            runs.add(new FetchRun(containerIndex, runStart, runEnd - runStart, 
List.copyOf(runFiles)));
+          }
+          runFiles = new ArrayList<>();
+          runFiles.add(name);
+          runStart = fileMeta.getStartOffset();
+          runEnd = fileEnd;
+        }
+        pendingGapFiles.clear();
+      } else if (runFiles != null) {
+        // unrequested and not resident: bridge candidate while a run is open
+        pendingGapFiles.add(name);
+      }
+    }
+    if (runFiles != null) {
+      runs.add(new FetchRun(containerIndex, runStart, runEnd - runStart, 
List.copyOf(runFiles)));
+    }
+    return runs;
+  }
+
+  /**
+   * Execute one planned run: take every member's per-file lock in offset 
order (the canonical acquisition order
+   * shared by all {@link #fetchFiles} callers, so overlapping concurrent runs 
can't deadlock, and
+   * {@link #ensureFileDownloaded} holds at most one of these locks so it 
can't participate in a cycle either), trim
+   * files that became resident since planning off the run's edges, then 
stream the remaining span in a single range
+   * read and mark each covered file downloaded after the bytes are on disk 
(bytes-before-bits, preserving the bitmap
+   * corruption invariant). Interior files that became resident mid-plan are 
re-fetched with byte-identical data;
+   * {@link #markDownloaded}'s add-gate keeps the accounting straight.
+   * <p>
+   * Runs from one {@link #planFetch} cover disjoint files, so callers may 
execute them concurrently (each is one
+   * deep-storage request); the caller must hold the same eviction-exclusion 
{@link #mapFile} requires for the
+   * duration of every run.
+   */
+  public void fetchRun(FetchRun run) throws IOException
+  {
+    final int containerIndex = run.containerIndex();
+    final List<String> runFiles = run.files();
+    final List<ReentrantLock> locks = new ArrayList<>(runFiles.size());
+    int locked = 0;
+    try {
+      for (String name : runFiles) {
+        final ReentrantLock lock = fileLocks.computeIfAbsent(name, k -> new 
ReentrantLock());
+        locks.add(lock);
+        lock.lock();

Review Comment:
   ## CodeQL / Unreleased lock
   
   This lock might not be unlocked or might be locked more times than it is 
unlocked.
   
   [Show more 
details](https://github.com/apache/druid/security/code-scanning/11335)



-- 
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