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


##########
processing/src/main/java/org/apache/druid/segment/PartialQueryableIndexCursorFactory.java:
##########
@@ -104,77 +104,60 @@ public CursorHolder makeCursorHolder(CursorBuildSpec spec)
   @Override
   public AsyncCursorHolder makeCursorHolderAsync(CursorBuildSpec spec)
   {
-    final QueryableProjection<QueryableIndex> matched = 
index.getProjection(spec);
-    if (matched == null && index.getClusteredBaseSummary() != null) {
-      return makeClusteredCursorHolderAsync(spec);
-    }
-
-    // Aggregate-projection match, or the plain base table: a single bundle 
(the projection's, or __base) with the
-    // query's required columns.
-    final QueryableIndex rowSelector = matched != null ? 
matched.getRowSelector() : index;
-    final String bundleName = matched != null ? matched.getName() : 
Projections.BASE_TABLE_PROJECTION_NAME;
-    final DownloadBundle bundle = new DownloadBundle(bundleName, rowSelector, 
requiredColumns(rowSelector, matched, spec));
-    return buildAsyncCursorHolder(List.of(bundle), () -> 
delegate.makeCursorHolderForProjection(spec, matched));
-  }
-
-  /**
-   * Async cursor holder for a clustered base table. Cluster-group resolution 
is metadata-only
-   * ({@link Projections#planClusterGroupQuery}): only the groups whose 
clustering tuples survive the query's filters
-   * are downloaded, one bundle (and its required columns) per surviving 
group. Once every group is resident, the
-   * holder is built via the delegate's clustered path.
-   */
-  private AsyncCursorHolder makeClusteredCursorHolderAsync(CursorBuildSpec 
spec)
-  {
-    final ClusterGroupQueryPlan plan = Projections.planClusterGroupQuery(
-        new ArrayList<>(index.getClusterGroupSchemas()),
-        spec
-    );
-    final List<TableClusterGroupSpec> surviving = plan.survivingGroups();
-    if (surviving.isEmpty()) {
-      // Filter rules out every group: no bundle to acquire, nothing to 
download.
-      return AsyncCursorHolder.completed(EmptyCursorHolder.INSTANCE);
-    }
-
-    final List<DownloadBundle> bundles = new ArrayList<>(surviving.size());
-    for (TableClusterGroupSpec group : surviving) {
-      final QueryableIndex groupIndex = 
index.getClusterGroupQueryableIndex(group, true);
-      final CursorBuildSpec groupSpec = plan.rebuildCursorBuildSpec(spec, 
group);
-      bundles.add(
-          new DownloadBundle(
-              
Projections.getClusterGroupBundleName(group.getClusteringValueIds()),
-              groupIndex,
-              requiredColumns(groupIndex, null, groupSpec)
-          )
+    // The index resolves what the spec needs (projection match / surviving 
cluster groups / base table, required
+    // columns, planned range reads); this factory purely orchestrates the 
downloads and cursor wiring.
+    final PartialQueryableIndex.CursorPrefetchPlan plan = 
index.planCursorPrefetch(spec);
+    if (plan.clusterGroupPlan() != null) {
+      if (plan.bundles().isEmpty()) {
+        // Filter rules out every cluster group: no bundle to acquire, nothing 
to download.
+        return AsyncCursorHolder.completed(EmptyCursorHolder.INSTANCE);
+      }
+      // reuse the plan's cluster resolution
+      return buildAsyncCursorHolder(
+          plan.bundles(),
+          () -> delegate.makeClusteredCursorHolder(spec, 
plan.clusterGroupPlan())
       );
     }
-    // Reuse the plan we already computed: delegate.makeClusteredCursorHolder 
skips a second planClusterGroupQuery and
-    // builds over the now-resident surviving groups (their columns memoized 
via getClusterGroupQueryableIndex).
-    return buildAsyncCursorHolder(bundles, () -> 
delegate.makeClusteredCursorHolder(spec, plan));
+    return buildAsyncCursorHolder(
+        plan.bundles(),
+        () -> delegate.makeCursorHolderForProjection(spec, 
plan.matchedProjection())
+    );
   }
 
   /**
-   * Generalized async-holder build over one or more bundles. Each {@link 
DownloadBundle} mounts its cache-layer bundle
-   * (its own {@link BundleHoldRelease}, since eviction is per-bundle) and 
submits a download per required column; the
-   * holder becomes ready once every column of every bundle is downloaded, at 
which point {@code innerBuilder} builds the
-   * (now fully-resident) cursor holder and the produced holder takes 
ownership of {@code inner} plus every bundle hold.
+   * Generalized async-holder build over one or more bundles. Each {@link 
PartialQueryableIndex.PrefetchBundle}
+   * mounts its cache-layer bundle (its own {@link BundleHoldRelease}, since 
eviction is per-bundle) and submits one
+   * download task per planned range read. Once every run of every bundle is 
resident, the ready callback materializes
+   * each bundle's required columns and builds the cursor holder, all inside 
every bundle's hold handshake
+   * (deserialization and holder construction both touch the container mmaps, 
see {@link #buildHolderUnderHolds}); the
+   * produced holder takes ownership of {@code inner} plus every bundle hold.
    */
-  private AsyncCursorHolder buildAsyncCursorHolder(List<DownloadBundle> 
bundles, Supplier<CursorHolder> innerBuilder)
+  private AsyncCursorHolder buildAsyncCursorHolder(
+      List<PartialQueryableIndex.PrefetchBundle> bundles,
+      Supplier<CursorHolder> innerBuilder
+  )
   {
     final List<BundleHoldRelease> holdReleases = new 
ArrayList<>(bundles.size());
     try {
-      final List<AsyncResource<String>> columnDownloads = new ArrayList<>();
-      for (DownloadBundle bundle : bundles) {
+      final List<AsyncResource<String>> runDownloads = new ArrayList<>();
+      for (PartialQueryableIndex.PrefetchBundle bundle : bundles) {
         final BundleHoldRelease holdRelease = new 
BundleHoldRelease(bundleAcquirer.acquire(bundle.bundleName()));
         holdReleases.add(holdRelease);
-        // submit one materialization task per column so a multi-threaded 
download executor can fan them out
-        for (String column : bundle.requiredColumns()) {
-          columnDownloads.add(submitColumnDownload(bundle.rowSelector(), 
column, holdRelease));
+        for (PartialSegmentFileMapperV10.PlannedFetch fetch : 
bundle.fetches()) {
+          runDownloads.add(submitRunFetch(bundle.bundleName(), fetch, 
holdRelease));
         }
       }
-      final AsyncResource<List<String>> downloaded = 
AsyncResources.collect(columnDownloads);
+      if (runDownloads.isEmpty()) {
+        // Everything is already resident, so an empty collect would complete 
in its constructor and fire the ready
+        // callback inline on THIS thread — but the callback deserializes 
columns and can even hit deep storage
+        // (projection parent columns load lazily), which must not run on a 
processing thread. Submit one token task
+        // so the callback hops to the download executor like every other 
build.
+        runDownloads.add(bundleAcquirer.submitDownload(() -> "resident"));

Review Comment:
   [P1] Keep holder construction off the caller thread
   
   The no-op token can complete before `AsyncResources.collect` and 
`addReadyCallback` are wired, and callbacks registered on an already-ready 
resource execute synchronously. In that ordering, the callback materializes 
columns and may lazily download projection-parent files on the processing 
thread, violating the async I/O contract. Schedule the continuation itself on 
the download executor instead of relying on the token's completion thread.



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