github-actions[bot] commented on code in PR #66841:
URL: https://github.com/apache/doris/pull/66841#discussion_r3796633506


##########
gensrc/thrift/PlanNodes.thrift:
##########
@@ -631,10 +631,11 @@ struct TFileScanRangeParams {
     35: optional string serialized_table_cache_key
     // Serialized Substrait ExtendedExpression executed by the native Lance 
scanner. Set at
     // ScanNode level so it is not serialized once per fragment split.
-    36: optional binary lance_substrait_filter
+    37: optional binary lance_substrait_filter

Review Comment:
   [P1] Preserve the deployed field IDs for rolling upgrades. Thrift identifies 
fields by number, so moving `lance_substrait_filter` from 36 to 37 (and the 
existing search request from 37 to 38) makes mixed-version peers skip them. 
This is result-changing for ordinary Lance scans: FE removes a successfully 
pushed conjunct from the residual list and sends its only remaining copy in 
field 36, while a new BE no longer recognizes field 36 and can return 
unfiltered rows. The search request is also dropped across old/new peers 
because field 37 changes wire type. Please keep these existing fields at 36 and 
37; the member/type names can change without renumbering them.



##########
be/src/format_v2/table/lance_reader.cpp:
##########
@@ -369,33 +368,13 @@ Status LanceTableReader::prepare_split(const 
SplitReadOptions& options) {
     if (current_split_pruned()) {
         return Status::OK();
     }
-
-    if (_vector_search) {
-        const auto& lance_params = 
options.current_range.table_format_params.lance_params;
-        if (lance_params.version <= 0) {
-            return Status::InvalidArgument(
-                    "Lance vector search requires a fixed positive dataset 
version");
-        }
-        if (lance_params.__isset.fragment_ids) {
-            return Status::InvalidArgument("Lance vector search split must not 
set fragment ids");
-        }
-        if (_search_split_prepared) {
-            return Status::InvalidArgument(
-                    "Lance vector search supports exactly one whole-dataset 
split");
-        }
-    }
-
-    const auto key = _dataset_key(options.current_range);
-    if (_dataset == nullptr) {
-        RETURN_IF_ERROR(_open_dataset(key));
-        _opened_dataset_key = key;
-    } else if (!_opened_dataset_key.has_value() || *_opened_dataset_key != 
key) {
+    if (_global_rowid_output_idx.has_value() && 
!_global_rowid_context.has_value()) {
         return Status::InvalidArgument(
-                "Lance reader cannot mix dataset snapshots or storage options 
in one scan");
+                "Lance global row id requested without global row id context");
     }
 
+    RETURN_IF_ERROR(_ensure_dataset_open(options.current_range));

Review Comment:
   [P1] Retain the negative-fragment check before opening this scanner. An old 
FE widens Lance Java's signed `int Fragment.getId()` without this PR's new 
unsigned conversion, so a high-bit u32 ID arrives negative during a rolling 
upgrade. The unchanged scanner path casts it to a huge u64 rather than the 
original fragment ID, and [pinned 
lance-c](https://github.com/lance-format/lance-c/blob/v0.1.6/src/scanner.rs#L118-L131)
 accepts that nonexistent ID by selecting an empty fragment set; the scan then 
succeeds while silently dropping the fragment. Keeping the 
deserialized-boundary validation preserves an explicit failure for 
old-FE/new-BE execution instead of wrong results.



##########
be/src/storage/utils.h:
##########
@@ -249,4 +252,24 @@ struct GlobalRowLoacationV2 {
     auto operator<=>(const GlobalRowLoacationV2&) const = default;
 };
 
+// Global row location for data sources whose native row ID is wider than the
+// uint32_t row ordinal carried by GlobalRowLoacationV2. Keep V2 unchanged for
+// existing internal, Parquet, and ORC readers.
+struct GlobalRowLocationV3 {
+    static constexpr uint8_t VERSION = 1;
+
+    GlobalRowLocationV3(int64_t bid, uint32_t fid, uint64_t rid)
+            : version(VERSION), backend_id(bid), file_id(fid), row_id(rid) {}
+    uint8_t version;
+    std::array<uint8_t, 7> reserved_before_backend_id {};
+    int64_t backend_id;
+    uint32_t file_id;
+    std::array<uint8_t, 4> reserved_before_row_id {};

Review Comment:
   [P1] Gate this V3 row-location encoding on the materializing BE's 
capability. During a rolling upgrade, a new scan BE can send these bytes 
through the distributed TopN to an old materialization BE. The old release code 
raw-casts every value as the 24-byte V2 layout without checking its version or 
length. This 32-byte layout preserves the backend/file offsets but puts zero 
padding at V2's row-id offset 20, so the old BE silently decodes every V3 value 
as row ID 0 and may splice the first row's deferred columns into a different 
selected row. Please negotiate V3 support (or use an old-reader-safe 
representation) before emitting it.



##########
be/src/exec/rowid_fetcher.cpp:
##########
@@ -927,13 +1001,26 @@ Status RowIdStorageReader::read_batch_external_row(
                     RETURN_IF_ERROR(remote_scan_sched->submit_scan_task(
                             SimplifiedScanTask(
                                     [&, idx, scan_info]() -> Status {
+                                        Defer complete_task {[&]() {
+                                            semaphore.release();
+                                            if (++producer_count == 
scan_rows.size()) {
+                                                std::lock_guard<std::mutex> 
lock(mtx);
+                                                cv.notify_one();
+                                            }
+                                        }};
                                         const auto& [row_ids, file_mapping] = 
scan_info;
-                                        return 
read_external_row_from_file_mapping(
+                                        auto status = 
read_external_row_from_file_mapping(
                                                 idx, row_ids, file_mapping, 
slots, query_id,
                                                 runtime_state, scan_blocks, 
row_id_block_idx,
                                                 fetch_statistics, 
rpc_scan_params,
-                                                colname_to_slot_id, 
producer_count,
-                                                scan_rows.size(), semaphore, 
cv, mtx, tuple_desc);
+                                                colname_to_slot_id, 
tuple_desc);
+                                        if (!status.ok()) {
+                                            std::lock_guard<std::mutex> 
lock(mtx);
+                                            if (scan_status.ok()) {
+                                                scan_status = status;
+                                            }
+                                        }
+                                        return status;

Review Comment:
   [P1] Mark this one-shot scheduler task complete even when the fetch fails. 
`SimplifiedScanTask` stores a `std::function<bool()>`, so returning an error 
`Status` here converts to `false`. `ScannerSplitRunner` then never sets its 
completion future, it is not auto-rescheduled, and the one-shot wrapper removes 
its task handle only for a `true` result. The new defer wakes the waiter, so 
the caller returns `scan_status` while the executor still retains an unfinished 
split/closure capturing this stack frame by reference; repeated read errors 
leak those scheduler tasks for the process lifetime. Record the error in 
`scan_status`, but return `true` from the scheduler callback after the single 
attempt.



##########
fe/fe-core/src/main/java/org/apache/doris/datasource/lance/source/LanceScanNode.java:
##########
@@ -152,42 +153,34 @@ public void createScanRangeLocations() throws 
UserException {
 
     @Override
     public List<Split> getSplits(int numBackends) throws UserException {
-        if (isExternalSearch()) {
-            plannedVersion = plannedMetadata.getVersion();
-            plannedFragments = plannedMetadata.getFragments().size();
-
-            // Do not attach fragment IDs. A vector index is a dataset-wide 
structure and one
-            // scanner must see every fragment visible in this pinned snapshot 
to produce global
-            // TopK. Fragment-level parallel search and result merging are 
intentionally deferred.
-            return Collections.singletonList(LanceSplit.wholeDatasetAtVersion(
-                    plannedMetadata.getDatasetUri(), 
plannedMetadata.getVersion(),
-                    plannedMetadata.getRowCount()));
-        } else {
-            LanceTableMetadata metadata = plannedMetadata;
-            plannedVersion = metadata.getVersion();
-            plannedFragments = metadata.getFragments().size();
-            Set<Long> fragmentIds = new HashSet<>();
-            long targetRows = 1;
-            for (LanceTableMetadata.LanceFragmentInfo fragment : 
metadata.getFragments()) {
-                if (!fragmentIds.add(fragment.getId())) {
-                    throw new UserException("Duplicate Lance fragment id " + 
fragment.getId()
-                            + " at dataset version " + metadata.getVersion());
-                }
-                targetRows = Math.max(targetRows, 
Math.max(fragment.getPhysicalRows(), 1));
+        LanceTableMetadata metadata = plannedMetadata;
+        plannedVersion = metadata.getVersion();
+        plannedFragments = metadata.getFragments().size();
+        if (isExternalSearch() && plannedVersion <= 0) {
+            throw new UserException(
+                    "Lance vector search requires a fixed positive dataset 
version");
+        }
+        Set<Long> fragmentIds = new HashSet<>();
+        long targetRows = 1;
+        for (LanceFragmentInfo fragment : metadata.getFragments()) {
+            if (!fragmentIds.add(fragment.getId())) {
+                throw new UserException("Duplicate Lance fragment id " + 
fragment.getId()
+                        + " at dataset version " + metadata.getVersion());
             }
+            targetRows = Math.max(targetRows, 
Math.max(fragment.getPhysicalRows(), 1));
+        }
 
-            // Use the largest fragment as one standard split so smaller 
fragments keep
-            // their relative row-count weight during backend assignment. 
Physical rows drive
-            // the weight because the BE legacy reader scans physical batches 
before deletions.
-            List<Split> splits = new ArrayList<>(plannedFragments);
-            for (LanceTableMetadata.LanceFragmentInfo fragment : 
metadata.getFragments()) {
-                LanceSplit split = new LanceSplit(metadata.getDatasetUri(), 
metadata.getVersion(),
-                        fragment.getId(), fragment.getPhysicalRows());
-                split.setTargetSplitSize(targetRows);
-                splits.add(split);
-            }
-            return splits;
+        // Keep one fragment per split. Use the largest fragment's physical 
row count as the
+        // normalization baseline for split weights, so backend scheduling 
reflects the relative
+        // amount of physical data each fragment scans, including rows covered 
by deletion metadata.
+        List<Split> splits = new ArrayList<>(plannedFragments);
+        for (LanceFragmentInfo fragment : metadata.getFragments()) {
+            LanceSplit split = new LanceSplit(metadata.getDatasetUri(), 
metadata.getVersion(),

Review Comment:
   [P1] Gate fragment-scoped vector search on BE capability during rolling 
upgrades. Once the existing search request stays at field 37, these new 
one-fragment ranges decode on an old BE, but its [pinned Lance 
path](https://github.com/lance-format/lance-c/blob/v0.1.6/src/scanner.rs#L145-L178)
 installs `nearest()` before prefilter (and enables prefilter only for an 
explicit filter). The lance-c patch in this PR documents that 
fragment-restricted nearest is accepted only when prefilter is active first, so 
a new-FE/old-BE query now fails instead of using the previously supported 
whole-snapshot search. Please retain the old whole-snapshot plan or exclude old 
scan BEs until every target supports distributed Lance search.



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