Gabriel39 commented on code in PR #70216:
URL: https://github.com/apache/doris/pull/70216#discussion_r4488702155


##########
be/src/format_v2/table/fluss_union_lake_reader.cpp:
##########
@@ -65,6 +71,185 @@ constexpr const char* PROP_LOG_STOP_OFFSET = 
"fluss.log_stop_offset";
 
 constexpr size_t TAIL_BATCH_ROWS = 4096;
 
+// Marks the rows of one block of lake keys that the tail's sorted keys name, 
by walking both in
+// ascending order: a comparison or two per row instead of a hash lookup. A 
primary-key table's lake files are
+// sorted by key, so a block read from one ascends; one that does not is left 
unmarked and false is returned, so
+// the caller falls back to its hash set. A lake that jumps ahead (a filter, a 
sparse file) is caught up by
+// galloping rather than by stepping through every key in between.
+template <typename T>
+bool union_walk_sorted_keys(const std::vector<int64_t>& sorted, const T* data, 
size_t rows,
+                            UInt8* keep, int64_t* dropped) {
+    if (rows == 0) {
+        return true;
+    }
+    if (!std::is_sorted(data, data + rows)) {
+        return false;
+    }
+    const auto begin = sorted.begin();
+    const size_t size = sorted.size();
+    size_t at = std::lower_bound(begin, sorted.end(), 
static_cast<int64_t>(data[0])) - begin;
+    for (size_t row = 0; row < rows; ++row) {
+        const auto value = static_cast<int64_t>(data[row]);
+        if (at < size && sorted[at] < value) {
+            size_t step = 1;
+            while (at + step < size && sorted[at + step] < value) {
+                at += step;
+                step <<= 1;
+            }
+            at = std::lower_bound(begin + at, begin + std::min(size, at + step 
+ 1), value) - begin;
+        }
+        const bool replaced = at < size && sorted[at] == value;
+        keep[row] = !replaced;
+        *dropped += replaced;
+    }
+    return true;
+}
+
+// The class a key column packs as, or TYPE_NULL when it cannot be packed. A 
packed value is
+// the bytes the value is stored as, so a type qualifies only when two of its 
values are equal exactly
+// when those bytes are: not floating point (0.0 and -0.0 compare equal), not 
the legacy date and
+// decimal types. The string types pack alike, as the generic predicate 
compares them as bytes.
+PrimitiveType union_packing_type(PrimitiveType type) {
+    switch (type) {
+    case TYPE_BOOLEAN:
+    case TYPE_TINYINT:
+    case TYPE_SMALLINT:
+    case TYPE_INT:
+    case TYPE_BIGINT:
+    case TYPE_LARGEINT:
+    case TYPE_DATEV2:
+    case TYPE_DATETIMEV2:
+    case TYPE_TIMESTAMPTZ:
+    case TYPE_DECIMAL32:
+    case TYPE_DECIMAL64:
+    case TYPE_DECIMAL128I:
+    case TYPE_DECIMAL256:
+        return type;
+    case TYPE_CHAR:
+    case TYPE_VARCHAR:
+    case TYPE_STRING:
+    case TYPE_VARBINARY:
+        return TYPE_STRING;
+    default:
+        return TYPE_NULL;
+    }
+}
+
+// One key column as packing reads it.
+struct UnionPackingColumn {
+    // Keeps a constant column, expanded to one value per row, alive while the 
block is probed.
+    ColumnPtr holder;
+    const IColumn* data = nullptr;
+    const UInt8* nulls = nullptr;
+};
+
+// The key columns at `positions`, with the class each packs as; false when 
one cannot be packed.
+bool union_packing_columns(const Block& block, const std::vector<size_t>& 
positions,
+                           std::vector<PrimitiveType>* types,
+                           std::vector<UnionPackingColumn>* columns) {
+    types->clear();
+    columns->clear();
+    for (const auto position : positions) {
+        const auto& entry = block.get_by_position(position);
+        const auto type = 
union_packing_type(remove_nullable(entry.type)->get_primitive_type());
+        if (type == TYPE_NULL) {
+            return false;
+        }
+        UnionPackingColumn column;
+        column.holder = entry.column->convert_to_full_column_if_const();
+        column.data = column.holder.get();
+        if (const auto* nullable = 
check_and_get_column<ColumnNullable>(*column.data)) {
+            column.nulls = nullable->get_null_map_data().data();
+            column.data = &nullable->get_nested_column();
+        }
+        types->push_back(type);
+        columns->push_back(std::move(column));
+    }
+    return true;
+}
+
+// One key appended as bytes. Per column a null byte, then the value's length 
and bytes; the
+// length keeps ("ab", "c") and ("a", "bc") apart.
+void union_pack_key(const std::vector<UnionPackingColumn>& columns, size_t 
row, std::string* out) {
+    for (const auto& column : columns) {
+        if (column.nulls != nullptr && column.nulls[row] != 0) {
+            out->push_back('\1');
+            continue;
+        }
+        out->push_back('\0');
+        const auto value = column.data->get_data_at(row);
+        const auto size = cast_set<uint32_t>(value.size);
+        out->append(reinterpret_cast<const char*>(&size), sizeof(size));
+        out->append(value.data, value.size);
+    }
+}
+
+// Immutable, versioned tails may outlive a query. Use the normal byte-bounded 
BE cache so
+// retained columns are charged to the cache tracker and memory GC can reclaim 
idle entries.
+class ProcessTailCache final : public LRUCachePolicy {
+public:
+    using Keys = FlussUnionLakeReader::SuppressionKeys;
+
+    static ProcessTailCache& instance() {
+        static ProcessTailCache cache;

Review Comment:
   Fixed in 0f4434dcf83. ExecEnv now initializes the tail cache after 
CacheManager and releases it after scanner/query teardown, before the manager 
and memory infrastructure. The new reader test verifies that cached keys are 
released during that teardown and fails with the former static-cache behavior. 
An additional local ASAN probe compiles CachePolicy without BE_TEST and 
verifies actual unregistration before clearing the manager pointer. All 39 
reader tests and the probe pass; the teardown/collision checks also passed 20 
repeated runs.



##########
be/src/format_v2/table/fluss_union_lake_reader.cpp:
##########
@@ -65,6 +71,185 @@ constexpr const char* PROP_LOG_STOP_OFFSET = 
"fluss.log_stop_offset";
 
 constexpr size_t TAIL_BATCH_ROWS = 4096;
 
+// Marks the rows of one block of lake keys that the tail's sorted keys name, 
by walking both in
+// ascending order: a comparison or two per row instead of a hash lookup. A 
primary-key table's lake files are
+// sorted by key, so a block read from one ascends; one that does not is left 
unmarked and false is returned, so
+// the caller falls back to its hash set. A lake that jumps ahead (a filter, a 
sparse file) is caught up by
+// galloping rather than by stepping through every key in between.
+template <typename T>
+bool union_walk_sorted_keys(const std::vector<int64_t>& sorted, const T* data, 
size_t rows,
+                            UInt8* keep, int64_t* dropped) {
+    if (rows == 0) {
+        return true;
+    }
+    if (!std::is_sorted(data, data + rows)) {
+        return false;
+    }
+    const auto begin = sorted.begin();
+    const size_t size = sorted.size();
+    size_t at = std::lower_bound(begin, sorted.end(), 
static_cast<int64_t>(data[0])) - begin;
+    for (size_t row = 0; row < rows; ++row) {
+        const auto value = static_cast<int64_t>(data[row]);
+        if (at < size && sorted[at] < value) {
+            size_t step = 1;
+            while (at + step < size && sorted[at + step] < value) {
+                at += step;
+                step <<= 1;
+            }
+            at = std::lower_bound(begin + at, begin + std::min(size, at + step 
+ 1), value) - begin;
+        }
+        const bool replaced = at < size && sorted[at] == value;
+        keep[row] = !replaced;
+        *dropped += replaced;
+    }
+    return true;
+}
+
+// The class a key column packs as, or TYPE_NULL when it cannot be packed. A 
packed value is
+// the bytes the value is stored as, so a type qualifies only when two of its 
values are equal exactly
+// when those bytes are: not floating point (0.0 and -0.0 compare equal), not 
the legacy date and
+// decimal types. The string types pack alike, as the generic predicate 
compares them as bytes.
+PrimitiveType union_packing_type(PrimitiveType type) {
+    switch (type) {
+    case TYPE_BOOLEAN:
+    case TYPE_TINYINT:
+    case TYPE_SMALLINT:
+    case TYPE_INT:
+    case TYPE_BIGINT:
+    case TYPE_LARGEINT:
+    case TYPE_DATEV2:
+    case TYPE_DATETIMEV2:
+    case TYPE_TIMESTAMPTZ:
+    case TYPE_DECIMAL32:
+    case TYPE_DECIMAL64:
+    case TYPE_DECIMAL128I:
+    case TYPE_DECIMAL256:
+        return type;
+    case TYPE_CHAR:
+    case TYPE_VARCHAR:
+    case TYPE_STRING:
+    case TYPE_VARBINARY:
+        return TYPE_STRING;
+    default:
+        return TYPE_NULL;
+    }
+}
+
+// One key column as packing reads it.
+struct UnionPackingColumn {
+    // Keeps a constant column, expanded to one value per row, alive while the 
block is probed.
+    ColumnPtr holder;
+    const IColumn* data = nullptr;
+    const UInt8* nulls = nullptr;
+};
+
+// The key columns at `positions`, with the class each packs as; false when 
one cannot be packed.
+bool union_packing_columns(const Block& block, const std::vector<size_t>& 
positions,
+                           std::vector<PrimitiveType>* types,
+                           std::vector<UnionPackingColumn>* columns) {
+    types->clear();
+    columns->clear();
+    for (const auto position : positions) {
+        const auto& entry = block.get_by_position(position);
+        const auto type = 
union_packing_type(remove_nullable(entry.type)->get_primitive_type());
+        if (type == TYPE_NULL) {
+            return false;
+        }
+        UnionPackingColumn column;
+        column.holder = entry.column->convert_to_full_column_if_const();
+        column.data = column.holder.get();
+        if (const auto* nullable = 
check_and_get_column<ColumnNullable>(*column.data)) {
+            column.nulls = nullable->get_null_map_data().data();
+            column.data = &nullable->get_nested_column();
+        }
+        types->push_back(type);
+        columns->push_back(std::move(column));
+    }
+    return true;
+}
+
+// One key appended as bytes. Per column a null byte, then the value's length 
and bytes; the
+// length keeps ("ab", "c") and ("a", "bc") apart.
+void union_pack_key(const std::vector<UnionPackingColumn>& columns, size_t 
row, std::string* out) {
+    for (const auto& column : columns) {
+        if (column.nulls != nullptr && column.nulls[row] != 0) {
+            out->push_back('\1');
+            continue;
+        }
+        out->push_back('\0');
+        const auto value = column.data->get_data_at(row);
+        const auto size = cast_set<uint32_t>(value.size);
+        out->append(reinterpret_cast<const char*>(&size), sizeof(size));
+        out->append(value.data, value.size);
+    }
+}
+
+// Immutable, versioned tails may outlive a query. Use the normal byte-bounded 
BE cache so
+// retained columns are charged to the cache tracker and memory GC can reclaim 
idle entries.
+class ProcessTailCache final : public LRUCachePolicy {
+public:
+    using Keys = FlussUnionLakeReader::SuppressionKeys;
+
+    static ProcessTailCache& instance() {
+        static ProcessTailCache cache;
+        return cache;
+    }
+
+    Status get(const std::string& key, const std::function<Status(Keys*)>& 
load,
+               std::shared_ptr<const Keys>* result, bool* hit) {
+        // A stripe serializes concurrent misses without holding the LRU lock 
across network I/O.
+        std::lock_guard<std::mutex> guard(
+                _loading[std::hash<std::string>()(key) % _loading.size()]);

Review Comment:
   Fixed in 0f4434dcf83. The cache now looks up and pins hits before acquiring 
a load stripe. Misses recheck under the stripe so concurrent loads still 
coalesce. The regression test chooses two colliding tail keys, pauses the cold 
JNI load, and verifies that the cached tail completes and returns the correct 
suppressed rows before that load is released. It fails with the previous lock 
ordering and passes with the fix, including 20 repeated runs.



##########
fe/fe-connector/fe-connector-fluss/src/main/java/org/apache/doris/connector/fluss/FlussConnector.java:
##########
@@ -94,10 +94,26 @@ public class FlussConnector implements Connector {
     // database and table, and targeted invalidation must follow the same path 
as lookup.
     private final Map<TablePath, TablePath> lakeTablePaths = new 
LinkedHashMap<>();
 
+    // Outlives the per-statement admin ops; see FlussTableInfoCache.
+    private final FlussTableInfoCache tableInfos;
+
+    // Outlives the per-node scan providers; see FlussLakePropertiesCache.
+    private final FlussLakePropertiesCache lakeProperties;
+
+    // Each metadata scope owns its pruning list so a new statement cannot 
miss newly created partitions.
+    private final long partitionCacheTtlSec;
+
     public FlussConnector(FlussCatalogProperties properties, ConnectorContext 
context) {
         this.properties = properties;
         this.catalogName = context.getCatalogName();
         this.context = context;
+        this.tableInfos = new 
FlussTableInfoCache(Long.parseLong(properties.getRawCatalogProperties()

Review Comment:
   Fixed in 0f4434dcf83. Both TTLs are now bound by FlussCatalogProperties, so 
malformed and overflowing values are rejected by CREATE/merged ALTER validation 
before persistence; connector construction uses the validated fields. Tests 
cover both keys, ALTER validation without mutating current properties, 
whitespace, defaults, disable values and long boundaries. The validated 
metadata TTL is also explicitly forwarded to JNI under its Doris key, while 
both TTLs stay out of the Fluss client configuration. All 302 connector tests 
and 97 scanner tests pass.



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