Gabriel39 commented on code in PR #66399:
URL: https://github.com/apache/doris/pull/66399#discussion_r3708894135
##########
be/src/service/CMakeLists.txt:
##########
@@ -49,6 +49,24 @@ if (${MAKE_TEST} STREQUAL "OFF" AND ${BUILD_BENCHMARK}
STREQUAL "OFF")
# This permits libraries loaded by dlopen to link to the symbols in the
program.
set_target_properties(doris_be PROPERTIES ENABLE_EXPORTS 1)
+ # ...but not the symbols of the RocksDB we link statically. Exporting
those makes this
+ # executable the definition every later-loaded library binds to, and a JNI
library that
+ # carries its own RocksDB then runs half on ours: the fluss scanner
bundles frocksdbjni,
+ # whose librocksdbjni.so defines 2576 rocksdb symbols under names
identical to ours but
+ # was built against the pre-C++11 libstdc++ string ABI. Objects laid out
by one and used
+ # by the other yield a garbage length, an std::bad_alloc that escapes the
JNI frame, and
+ # an aborted BE. Hiding this archive lets that library bind to its own
copy.
+ #
+ # Scoped to the archive rather than dropping ENABLE_EXPORTS: what 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 name even
anonymous-namespace functions.
+ #
+ # The same library also duplicates zstd, lz4, snappy, bzip2 and zlib
symbols. Those are C
+ # ABIs, stable across versions and layout-free, so they are left alone
until something
+ # shows otherwise -- unlike RocksDB, whose C++ objects are what actually
corrupt.
+ target_link_options(doris_be PRIVATE "-Wl,--exclude-libs,librocksdb.a")
Review Comment:
[P1] Please make this linker option platform-specific. `--exclude-libs` is a
GNU/ELF linker option; Apple ld rejects it with `ld: unknown options:
--exclude-libs`, which is exactly why the current **BE UT (macOS)** check fails
while linking `doris_be`. Gate this option to the supported ELF platforms (and
use a Darwin-specific solution only if the symbol-hiding behavior is needed
there) so macOS can still build.
##########
fe/fe-connector/fe-connector-fluss/src/main/resources/META-INF/services/org.apache.doris.connector.spi.ConnectorProvider:
##########
@@ -0,0 +1 @@
+org.apache.doris.connector.fluss.FlussConnectorProvider
Review Comment:
[P1] Please add the ASF license header using `#` comments, as the existing
connector service descriptors do. This new one-line descriptor currently makes
License Check fail with an invalid/missing license header, so the PR cannot
merge as-is.
##########
be/src/format_v2/table/fluss_union_lake_reader.cpp:
##########
@@ -0,0 +1,524 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include "format_v2/table/fluss_union_lake_reader.h"
+
+#include <algorithm>
+#include <charconv>
+#include <memory>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+#include "common/cast_set.h"
+#include "core/assert_cast.h"
+#include "core/column/column_vector.h"
+#include "exprs/vexpr_context.h"
+#include "exprs/vslot_ref.h"
+#include "format_v2/column_mapper.h"
+#include "format_v2/expr/equality_delete_predicate.h"
+#include "format_v2/jni/fluss_jni_reader.h"
+#include "format_v2/table/paimon_reader.h"
+#include "runtime/descriptors.h"
+#include "runtime/file_scan_profile.h"
+#include "runtime/runtime_state.h"
+
+namespace doris::format::fluss {
+namespace {
+
+// The scan-node properties this reader reads. The fluss connector states that
`fluss.union.*` is the
+// whole of what BE's C++ side knows about fluss; anything added here has to
be added there too.
+constexpr const char* PROP_PK_NAMES = "fluss.union.pk_names";
+constexpr const char* PROP_MAX_TAIL_ROWS = "fluss.union.max_tail_rows";
+
+// The per-range payload of a wrapped lake split.
+constexpr const char* PROP_RANGE_TYPE = "fluss.range_type";
+constexpr const char* PROP_TAIL = "fluss.union.tail";
+constexpr const char* RANGE_TYPE_LAKE_SUPPRESS = "LAKE_SUPPRESS";
+
+// The range this reader synthesizes to read the tail. A plain bounded log
read, which is what the
+// suppression set is: every key the tail touched, whatever it ended up saying
about it.
+constexpr const char* RANGE_TYPE_LOG = "LOG";
+constexpr const char* PROP_PARTITION_ID = "fluss.partition_id";
+constexpr const char* PROP_BUCKET_ID = "fluss.bucket_id";
+constexpr const char* PROP_LOG_START_OFFSET = "fluss.log_start_offset";
+constexpr const char* PROP_LOG_STOP_OFFSET = "fluss.log_stop_offset";
+
+constexpr size_t TAIL_BATCH_ROWS = 4096;
+
+std::vector<std::string_view> split_on(std::string_view value, char separator)
{
+ std::vector<std::string_view> parts;
+ size_t start = 0;
+ while (true) {
+ const auto end = value.find(separator, start);
+ if (end == std::string_view::npos) {
+ parts.push_back(value.substr(start));
+ return parts;
+ }
+ parts.push_back(value.substr(start, end - start));
+ start = end + 1;
+ }
+}
+
+void update_counter(RuntimeProfile::Counter* counter, int64_t value) {
+ if (counter != nullptr) {
+ COUNTER_UPDATE(counter, value);
+ }
+}
+
+template <typename T>
+bool parse_integer(std::string_view text, T* value) {
+ if (text.empty()) {
+ return false;
+ }
+ const auto result = std::from_chars(text.data(), text.data() +
text.size(), *value);
+ return result.ec == std::errc() && result.ptr == text.data() + text.size();
+}
+
+} // namespace
+
+Status FlussUnionLakeReader::parse_tail(const std::string& spec, Tail* tail) {
+ DORIS_CHECK(tail != nullptr);
+ const auto parts = split_on(spec, ':');
+ if (parts.size() != 4) {
+ return Status::InternalError(
+ "fluss union read: '{}' is not a log tail of the form "
+ "partitionId:bucket:start:stop",
+ spec);
+ }
+ // An unpartitioned table leaves the partition segment empty rather than
writing a sentinel, so
+ // that its bucket 0 and a partitioned table's bucket 0 cannot become the
same cache entry.
+ if (!parts[0].empty()) {
+ int64_t partition_id = 0;
+ if (!parse_integer(parts[0], &partition_id)) {
+ return Status::InternalError(
+ "fluss union read: '{}' has a partition id that is not a "
+ "number in log tail '{}'",
+ parts[0], spec);
+ }
+ }
+ Tail parsed;
+ parsed.partition_id = std::string(parts[0]);
+ if (!parse_integer(parts[1], &parsed.bucket_id) ||
+ !parse_integer(parts[2], &parsed.start_offset) ||
+ !parse_integer(parts[3], &parsed.stop_offset)) {
+ return Status::InternalError(
+ "fluss union read: log tail '{}' has a bucket or offset that
is not a number",
+ spec);
+ }
+ if (parsed.start_offset >= parsed.stop_offset) {
+ // Planning never wraps a lake split whose bucket has nothing left in
its log. One arriving
+ // here means the two halves of this read were bounded by different
offsets, and that is a
+ // duplicated or a missing row either way.
+ return Status::InternalError(
+ "fluss union read: a suppressing log tail must contain
something, but bucket {} "
+ "was "
+ "given [{}, {})",
+ parsed.bucket_id, parsed.start_offset, parsed.stop_offset);
+ }
+ parsed.spec = spec;
+ *tail = std::move(parsed);
+ return Status::OK();
+}
+
+TFileRangeDesc FlussUnionLakeReader::tail_scan_range(const Tail& tail) {
+ std::map<std::string, std::string> params {
+ {PROP_RANGE_TYPE, RANGE_TYPE_LOG},
+ {PROP_BUCKET_ID, std::to_string(tail.bucket_id)},
+ {PROP_LOG_START_OFFSET, std::to_string(tail.start_offset)},
+ {PROP_LOG_STOP_OFFSET, std::to_string(tail.stop_offset)},
+ };
+ if (!tail.partition_id.empty()) {
+ // Absent rather than -1 on an unpartitioned table: that is how the
scanner tells the two
+ // apart, and fluss subscribes to a bucket of each by a different call.
+ params.emplace(PROP_PARTITION_ID, tail.partition_id);
+ }
+ TTableFormatFileDesc table_format_params;
+ table_format_params.__set_table_format_type("fluss");
+ table_format_params.__set_fluss_params(std::move(params));
+ TFileRangeDesc range;
+ range.__set_table_format_params(std::move(table_format_params));
+ range.__set_format_type(TFileFormatType::FORMAT_JNI);
+ return range;
+}
+
+Status FlussUnionLakeReader::init(format::TableReadOptions&& options) {
+ RETURN_IF_ERROR(format::TableReader::init(std::move(options)));
+ RETURN_IF_ERROR(_resolve_union_properties());
+ _init_union_profile();
+
+ VExprContextSPtrs conjuncts;
+ conjuncts.reserve(_conjuncts.size());
+ for (const auto& conjunct : _conjuncts) {
+ VExprSPtr root;
+ RETURN_IF_ERROR(format::clone_table_expr_tree(conjunct->root(),
&root));
+ conjuncts.push_back(VExprContext::create_shared(std::move(root)));
+ }
+ _lake_reader = std::make_unique<format::paimon::PaimonHybridReader>();
+ RETURN_IF_ERROR(_lake_reader->init({
+ .projected_columns = _projected_columns,
+ .conjuncts = std::move(conjuncts),
+ .format = _format,
+ .scan_params = _scan_params,
+ .io_ctx = _io_ctx,
+ .runtime_state = _runtime_state,
+ .scanner_profile = _scanner_profile,
+ .file_slot_descs = _file_slot_descs,
+ // Aggregate pushdown is withheld from the lake half on purpose. A
COUNT answered from
+ // paimon's own file metadata would count the very rows this
reader is about to suppress,
+ // and it would do so without ever producing a block to suppress
them from.
+ .push_down_agg_type = TPushAggOp::type::NONE,
+ .condition_cache_digest = _condition_cache_digest,
+ }));
+ if (_batch_size > 0) {
+ _lake_reader->set_batch_size(_batch_size);
+ }
+ return Status::OK();
+}
+
+Status FlussUnionLakeReader::_resolve_union_properties() {
+ if (_scan_params == nullptr || !_scan_params->__isset.fluss_properties) {
+ return Status::InternalError(
+ "missing fluss_properties for a fluss union read, possibly
caused by FE/BE "
+ "protocol "
+ "mismatch");
+ }
+ const auto& properties = _scan_params->fluss_properties;
+ const auto names_it = properties.find(PROP_PK_NAMES);
+ if (names_it == properties.end() || names_it->second.empty()) {
+ return Status::InternalError(
+ "missing '{}' for a fluss union read: without the key columns
the lake rows the "
+ "log "
+ "tail supersedes cannot be identified",
+ PROP_PK_NAMES);
+ }
+ for (const auto name : split_on(names_it->second, ',')) {
+ const auto column = std::ranges::find_if(
+ _projected_columns,
+ [&](const format::ColumnDefinition& candidate) { return
candidate.name == name; });
+ if (column == _projected_columns.end()) {
+ // FE keeps the key columns in the scan's tuple whenever it plans
a union read, so this
+ // means its planning-time decision and its split-planning
decision disagreed. Suppressing
+ // nothing would return every superseded lake row a second time,
silently.
+ return Status::InternalError(
+ "fluss union read: key column '{}' is not among the
columns this scan "
+ "projects. "
+ "The lake rows its log tail supersedes cannot be
identified without it",
+ name);
+ }
+ _key_column_indexes.push_back(
+ cast_set<size_t>(std::distance(_projected_columns.begin(),
column)));
+ _key_columns.push_back(*column);
+ }
+
+ const auto rows_it = properties.find(PROP_MAX_TAIL_ROWS);
+ if (rows_it == properties.end() ||
+ !parse_integer(std::string_view(rows_it->second), &_max_tail_rows) ||
_max_tail_rows <= 0) {
+ return Status::InternalError(
+ "fluss union read: '{}' must be a positive number of rows, but
was '{}'",
+ PROP_MAX_TAIL_ROWS,
+ rows_it == properties.end() ? std::string("missing") :
rows_it->second);
+ }
+ return Status::OK();
+}
+
+void FlussUnionLakeReader::_init_union_profile() {
+ if (_scanner_profile == nullptr) {
+ return;
+ }
+ static const char* table_profile = file_scan_profile::TABLE_READER;
+ _suppressed_rows_counter = ADD_CHILD_COUNTER_WITH_LEVEL(
+ _scanner_profile, "FlussUnionSuppressedRows", TUnit::UNIT,
table_profile, 1);
+ _tail_keys_read_counter = ADD_CHILD_COUNTER_WITH_LEVEL(
+ _scanner_profile, "FlussUnionTailKeysRead", TUnit::UNIT,
table_profile, 1);
+ _tail_cache_hit_counter = ADD_CHILD_COUNTER_WITH_LEVEL(
+ _scanner_profile, "FlussUnionTailCacheHitCount", TUnit::UNIT,
table_profile, 1);
+}
+
+Status FlussUnionLakeReader::prepare_split(const format::SplitReadOptions&
options) {
+ DORIS_CHECK(_lake_reader != nullptr);
+ RETURN_IF_ERROR(_lake_reader->prepare_split(options));
+ if (_lake_reader->current_split_pruned()) {
+ // A pruned split returns no rows, so there is nothing to suppress and
no reason to spend a
+ // read of the tail on it.
+ return Status::OK();
+ }
+ return _prepare_suppression(options);
+}
+
+Status FlussUnionLakeReader::_prepare_suppression(const
format::SplitReadOptions& options) {
+ const auto& range = options.current_range;
+ if (!range.__isset.table_format_params ||
!range.table_format_params.__isset.fluss_params) {
+ return Status::InternalError(
+ "missing fluss_params on a fluss union lake split, possibly
caused by FE/BE "
+ "protocol "
+ "mismatch");
+ }
+ const auto& params = range.table_format_params.fluss_params;
+ const auto type_it = params.find(PROP_RANGE_TYPE);
+ if (type_it == params.end() || type_it->second !=
RANGE_TYPE_LAKE_SUPPRESS) {
+ return Status::InternalError(
+ "a fluss union lake split must carry '{}={}', but carries
'{}'", PROP_RANGE_TYPE,
+ RANGE_TYPE_LAKE_SUPPRESS,
+ type_it == params.end() ? std::string("nothing") :
type_it->second);
+ }
+ const auto tail_it = params.find(PROP_TAIL);
+ if (tail_it == params.end()) {
+ return Status::InternalError("missing '{}' on a fluss union lake
split", PROP_TAIL);
+ }
+ if (_suppression != nullptr && _suppression_tail_spec == tail_it->second) {
+ // Consecutive splits of one bucket are common; their suppression is
the same one.
+ return Status::OK();
+ }
+ Tail tail;
+ RETURN_IF_ERROR(parse_tail(tail_it->second, &tail));
+ RETURN_IF_ERROR(_load_suppression_keys(options, tail));
+ _suppression_tail_spec = tail_it->second;
+ return Status::OK();
+}
+
+Status FlussUnionLakeReader::_load_suppression_keys(const
format::SplitReadOptions& options,
+ const Tail& tail) {
+ if (options.cache == nullptr) {
+ return Status::InternalError(
+ "fluss union read: no split cache to hold the keys of log tail
'{}'", tail.spec);
+ }
+ // Length-prefixed so that no boundary between the fixed prefix and the
tail can be reinterpreted
+ // as part of the tail itself. One scan node reads one table, so the tail
alone identifies it.
+ const auto cache_key = fmt::format("fluss_union_tail:{}:{}",
tail.spec.size(), tail.spec);
+ Status read_status = Status::OK();
+ bool cache_hit = false;
+ auto* cached = options.cache->get<SuppressionKeys>(
Review Comment:
[P1] Please bound or release these cached tail-key blocks at the scan level.
`fluss.union_read.max_tail_rows` limits one partition/bucket tail, but
`ShardedKVCache` has no eviction and lives for the entire `FileScanLocalState`,
so every tail touched by every partition and bucket remains resident until the
scan finishes. A partitioned table can therefore retain `partition_count *
bucket_count * max_tail_rows` key records (the default is two million per
tail), even after all lake splits for earlier tails have completed, and hit the
query/BE memory limit although the input can otherwise be streamed. Consider a
scan-wide byte/row budget plus eviction/ref-counting after the last split using
a tail, and deduplicate touched keys before retaining them.
--
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]