morningman opened a new pull request, #66403:
URL: https://github.com/apache/doris/pull/66403

   ### What problem does this PR solve?
   
   Issue Number: close #xxx
   
   Related PR: #66399
   
   Problem Summary:
   
   Five independent fixes, none of them in one connector's own code. They were 
found while building the fluss catalog (#66399), which is where each one's 
symptom first showed up — but every one of them is a Doris bug or a Doris gap 
that exists without fluss, so they are proposed on their own, ahead of and 
separately from that connector. **#66399 will be rebased on top of this and 
shrink by exactly these five commits.**
   
   They are unrelated to each other; there is one commit per fix and each can 
be reviewed alone.
   
   ---
   
   #### 1. `[fix](be) Stop exporting the statically linked RocksDB symbols`
   
   `be/src/service/CMakeLists.txt` — one line, plus why.
   
   `doris_be` sets `ENABLE_EXPORTS`, so the 4840 rocksdb symbols it links 
statically are exported into the global dynamic symbol table. The executable is 
the highest-priority definition for everything loaded after it, so **any** JNI 
library that carries its own RocksDB has its internal calls resolved into 
doris_be's copy instead — 2576 symbols with byte-identical mangled names.
   
   That would be survivable if the two agreed on layout. They do not: such 
libraries are commonly built against the pre-C++11 libstdc++ string ABI 
(`...C1ERKSs`) while doris_be is built against the new one 
(`...RKNSt7__cxx1112basic_stringE`). An object constructed with one layout and 
used by functions compiled for the other yields a garbage length, an 
`std::bad_alloc` that escapes through the JNI frame, and an aborted BE process.
   
   The fix hides that one archive from the dynamic symbol table, so such a 
library binds to its own copy. It is scoped to the archive rather than dropping 
`ENABLE_EXPORTS`, because what actually needs the exports is native UDFs 
(`runtime/user_function_cache.cpp` dlopens them) and those use the Doris UDF 
ABI, which has nothing to do with RocksDB. Crash stacks do not need it either — 
they are symbolized from debug info, which is why they can name even 
anonymous-namespace functions.
   
   Verified by symbol table rather than by argument: after the change 61 
rocksdb symbols remain exported (compiler-instantiated inline/template members 
that landed in Doris's own objects, which an archive-level exclusion cannot 
reach). 29 of those share a name with a JNI library's, but `readelf -r` shows 
**none** of them in that library's relocation table, so it never looks them up. 
The zstd/lz4/snappy/bzip2/zlib duplicates are left alone deliberately: those 
are C ABIs, stable and layout-free, unlike RocksDB's C++ objects.
   
   ⚠️ This changes BE's link behaviour, so it wants a full relink and a BE 
regression run — tablet metadata itself lives in RocksDB.
   
   #### 2. `[fix](be) Pick the table reader per scan range, not per scan node`
   
   `be/src/exec/scan/file_scanner_v2.{h,cpp}` + unit test.
   
   `_open_impl` builds one `_table_reader` from the **first** scan range; 
`_prepare_next_split` then reuses it for every range that follows, and never 
revisits the choice. The reader is format-specific, so a scan node holding 
ranges of two different `table_format_type`s hands the second kind to the first 
kind's reader.
   
   That does not fail cleanly. It fails as whatever the wrong reader makes of a 
foreign range — e.g. paimon's reader reporting an unsupported file format for a 
range that carries no paimon parameters at all. And which ranges end up in the 
same scanner is the engine's assignment, so **the same query succeeds or fails 
depending on how the ranges happened to be dealt out**, and changing the 
projection can change the outcome.
   
   The fix records the format the reader was built for and rebuilds when a 
range disagrees. The expression contexts are deliberately *not* rebuilt: they 
are per-scanner and format-independent, and `_init_expr_ctxes` is not 
idempotent.
   
   A scan node mixing formats is what a connector reading a table as "a lake 
plus the log written after it" produces — its lake half planned by a sibling 
connector, its own half by itself — but nothing in the scanner assumes that, 
and the fix is a general one.
   
   New unit test `TheTableReaderIsRebuiltWhenARangeChangesTableFormat`: same 
format reuses the reader, a different format replaces it, and the formats 
really do map to different reader types (otherwise the first two assertions 
would hold for a scanner that never rebuilt anything). Reverting the comparison 
to the pre-fix behaviour turns it red.
   
   #### 3. `[fix](paimon) Claim the table handles this connector produces`
   
   `fe/fe-connector/fe-connector-paimon` + unit tests.
   
   `Connector.ownsHandle` defaults to `false`. The iceberg and hudi connectors 
override it — they are already used as siblings behind the hms gateway — but 
paimon never did. Any gateway connector that embeds paimon therefore asks "is 
this handle yours?" about a handle paimon itself produced and is told no, so 
every one of the gateway's type guards fails open and the first cast throws 
`ClassCastException`.
   
   One method, same implementation as the two siblings that already have it.
   
   #### 4. `[feat](paimon) Say which bucket a scan range came from`
   
   `fe/fe-connector/fe-connector-paimon` + unit tests.
   
   Adds `paimon.bucket` = `DataSplit.bucket()` to the scan range properties, so 
a connector that plans paimon splits on behalf of its own table can line them 
up with its own per-bucket state.
   
   FE-only: `populateRangeParams` does not forward it, so **BE is unaffected**. 
Set on every `DataSplit`-backed range, native and JNI alike, so which reader BE 
ends up using cannot change what a caller can learn about the split. 
Deliberately **not** set on the collapsed `COUNT(*)` range (it stands for 
splits from several buckets, so any single number would be a lie) nor on a 
non-`DataSplit` system split (there is no bucket). Consumers are expected to 
fail loud when it is absent on a range they meant to bind, since treating that 
as "no state for this bucket" is a wrong-results bug rather than a degradation.
   
   #### 5. `[feat](connector) Let a connector name the columns its reader must 
read`
   
   `fe/fe-connector/fe-connector-api` + `fe/fe-core` + unit tests. **The only 
engine-side change here.**
   
   A connector whose BE-side reader merges, suppresses or otherwise identifies 
rows by key needs those key columns to be READ, whether or not the query 
selected them. Today the plugin scan's tuple is pruned to the projection, so 
the reader is handed a scan without the column it needs.
   
   **This is not a new mechanism.** Doris does exactly this for its own 
aggregate and merge-on-read unique-key tables: 
`PhysicalPlanTranslator.preserveExtraStorageKeySlots` keeps the key slots and 
ships them as `extra_key_column_slot_ids`, because BE merges by key regardless 
of what was selected. The new branch sits beside that one, before the same 
`removeIf`, and only widens the scan's tuple — the project above it was already 
given its own output tuple, so a preserved column is read and then dropped and 
never reaches the query's output.
   
   Three names:
   
   - SPI `ConnectorScanPlanProvider.getMustReadColumns(session, handle)` — 
**defaults to an empty set**, so every existing connector prunes exactly as 
before
   - `PluginDrivenScanNode.mustReadColumnsFromConnector()` — same memoized 
provider the rest of planning uses, with the plugin classloader pinned
   - `PhysicalPlanTranslator.preserveConnectorMustReadSlots()`
   
   A returned name that matches no slot fails the query loud rather than being 
skipped: it means the connector and the engine disagree about the table, and 
reading on would hand the connector's reader a scan missing a column it said it 
needs — silently wrong rows, not an error.
   
   ### Release note
   
   None
   
   ### Check List (For Author)
   
   - Test
       - [x] Unit Test
       - [x] Manual test (add detailed scripts or steps below)
   
     Unit tests, all 0 skipped: `fe-connector-api` 113, `fe-connector-paimon` 
511 (1 pre-existing skip), `fe-core` neighbourhood 153 
(`PluginDrivenScanNode*`, `PhysicalPlanTranslator*`, 
`CountStarSmallestSlotTest`, which starts a real FE and exercises the OLAP 
pruning path this change sits next to). BE: `FileScannerV2*:FileScannerTest*` 
26 and `Paimon*:*Iceberg*:*EqualityDelete*` 227.
   
     Every new behaviour was mutation-tested — the change inverted, rebuilt, 
and the test required to go red. For #2 that is the reader-rebuild comparison; 
for #3, #4 and #5 the mutations are listed in the individual commit messages.
   
     #1 cannot be covered by a unit test — it is a link-time property. It was 
verified against a real BE: the failure reproduced twice before the change (BE 
abort with `Java_org_rocksdb_RocksDB_openROnly` → 
`ColumnFamilyDescriptor::ColumnFamilyDescriptor` on the stack), the same Java 
code passed in a plain JVM, and after a full relink the same workload runs and 
BE's own tablet metadata survives a restart. Residual exported symbols were 
checked with `nm -D` and `readelf -r` as described above.
   
   - Behavior changed:
       - [x] No. <!-- #1 removes symbols that were never meant to be part of 
BE's interface; #5's SPI method defaults to empty; #4 is FE-only and not 
forwarded to BE. #2 changes behaviour only for a scan node that mixes table 
formats, which today has no correct outcome. -->
   
   - Does this need documentation?
       - [x] No.
   


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