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


##########
cloud/src/meta-service/meta_service_partition.cpp:
##########
@@ -976,6 +1445,7 @@ void 
MetaServiceImpl::drop_partition(::google::protobuf::RpcController* controll
     drop_partition_log.set_db_id(request->db_id());
     drop_partition_log.set_table_id(request->table_id());
     drop_partition_log.mutable_index_ids()->CopyFrom(request->index_ids());
+    
drop_partition_log.mutable_table_streams()->CopyFrom(request->table_streams());

Review Comment:
   [P1] Include Stream offset history in this drop's retention boundary
   
   This list makes the later partition recycler call `versioned_remove_all` for 
every referenced Stream offset, but this drop's `min_timestamp` is still 
derived only from partition/table metadata. For example, an offset at V20 can 
still be needed by a snapshot at V25 after an index operation rewrites 
partition metadata at V30; a drop at V40 then records V30, so `can_recycle` 
searches only `[V30,V40)`, misses V25, and deletes the V20 offset. Read the 
referenced offset histories when computing the boundary (or otherwise make 
physical deletion wait for their older operation logs) so active 
snapshots/clones cannot lose their offsets.



##########
cloud/src/meta-service/meta_service_txn.cpp:
##########
@@ -48,6 +54,280 @@ namespace doris::cloud {
 
 static constexpr std::string_view kMetaSyncPointDummyKey = 
"__meta_service_sync_point_dummy_key__";
 
+static bool validate_table_stream_updates(const CommitTxnRequest* request, 
MetaServiceCode& code,
+                                          std::string& msg) {
+    if (request->has_is_2pc() && request->is_2pc()) {
+        code = MetaServiceCode::INVALID_ARGUMENT;
+        msg = "table stream consumption does not support 2PC";
+        return false;
+    }
+    if ((request->has_is_txn_load() && request->is_txn_load()) ||
+        !request->sub_txn_infos().empty()) {
+        code = MetaServiceCode::INVALID_ARGUMENT;
+        msg = "table stream consumption does not support transaction load or 
sub transactions";
+        return false;
+    }
+
+    std::set<std::pair<int64_t, int64_t>> stream_partitions;
+    for (const auto& stream_update : request->table_stream_updates()) {
+        if (!stream_update.has_identity() ||
+            !is_valid_table_stream_identity(stream_update.identity()) ||
+            stream_update.partition_updates().empty()) {
+            code = MetaServiceCode::INVALID_ARGUMENT;
+            msg = "invalid table stream update";
+            return false;
+        }
+        for (const auto& partition_update : stream_update.partition_updates()) 
{
+            if (!partition_update.has_partition_id() || 
partition_update.partition_id() <= 0 ||

Review Comment:
   [P2] Reject negative offset TSOs before they are persisted
   
   This validates only that `next_offset_tso` is present. For an absent offset, 
`expected_state=UNKNOWN` bypasses the backward check, and any negative value 
also passes the later `next <= visible_tso` check, so the commit stores a 
`CONSUMED` offset such as `-1`. FE then treats it as a real consumed start 
boundary and sends it to the row-binlog scan, which can produce an invalid 
range or replay data. Enforce the valid TSO lower bound here (and on 
create/expected offsets) before writing either projection.



##########
cloud/src/meta-service/meta_service_partition.cpp:
##########
@@ -83,6 +88,121 @@ static TxnErrorCode check_recycle_key_exist(Transaction* 
txn, const std::string&
     return txn->get(key, &val);
 }
 
+static bool is_table_stream_versioned_write(MultiVersionStatus 
multi_version_status) {
+    return multi_version_status == 
MultiVersionStatus::MULTI_VERSION_WRITE_ONLY ||
+           multi_version_status == 
MultiVersionStatus::MULTI_VERSION_READ_WRITE;
+}
+
+static bool validate_table_stream_index_request(const IndexRequest* request, 
MetaServiceCode& code,
+                                                std::string& msg) {
+    if (request->index_ids_size() != 1 || !request->has_db_id() || 
!request->has_stream_db_id()) {

Review Comment:
   [P2] Reject Stream identities that cannot be read back
   
   These checks require the identity fields to be present but never require 
positive IDs. A request with `stream_id=0` (or another nonpositive binding ID) 
can therefore prepare and commit offset keys, while 
`is_valid_table_stream_identity` rejects the same identity in the read, 
consume, and drop paths. That leaves committed metadata with no usable 
lifecycle. Apply the same positive-ID validator before any Stream 
prepare/partition write.



##########
cloud/src/recycler/checker.cpp:
##########
@@ -2275,6 +2333,179 @@ int InstanceChecker::scan_and_handle_kv(
     return ret;
 }
 
+// The check validates Offset values and the Latest/Versioned projection 
invariant. FE Catalog is
+// the authority for Stream existence and binding, so no MS-side Stream 
Mapping is checked here.
+int InstanceChecker::do_table_stream_check() {
+    struct VersionedOffset {
+        Versionstamp versionstamp;
+        TableStreamOffsetPB offset;
+    };
+
+    auto decode_components =
+            [](std::string_view key, size_t expected_size,
+               std::vector<std::tuple<std::variant<int64_t, std::string>, int, 
int>>* components) {
+                if (key.empty()) {
+                    return false;
+                }
+                key.remove_prefix(1);
+                return decode_key(&key, components) == 0 && components->size() 
== expected_size;
+            };
+
+    std::unordered_map<int64_t, int> recycling_streams;
+    auto is_recycling = [&](int64_t stream_id) {
+        auto cached = recycling_streams.find(stream_id);
+        if (cached != recycling_streams.end()) {
+            return cached->second;
+        }
+        const int existence =
+                key_exist(txn_kv_.get(), recycle_index_key({instance_id_, 
stream_id}));
+        const int result = existence < 0 ? -1 : existence == 0;
+        recycling_streams.emplace(stream_id, result);
+        return result;
+    };
+
+    int check_ret = 0;
+    std::unordered_map<std::string, TableStreamOffsetPB> latest_offsets;
+    auto validate_offset = [&](int64_t base_db_id, int64_t base_table_id, 
int64_t stream_db_id,
+                               int64_t stream_id, int64_t partition_id,
+                               const TableStreamOffsetPB& offset) {
+        if (base_db_id <= 0 || base_table_id <= 0 || stream_db_id <= 0 || 
stream_id <= 0 ||
+            partition_id <= 0 || !offset.has_partition_id() || 
!offset.has_state() ||
+            !offset.has_offset_tso() || offset.partition_id() != partition_id 
||
+            (offset.state() != TABLE_STREAM_OFFSET_INITIAL_SNAPSHOT_PENDING &&
+             offset.state() != TABLE_STREAM_OFFSET_CONSUMED)) {
+            LOG_WARNING("Table Stream Offset value does not match its key")
+                    .tag("instance_id", instance_id_)
+                    .tag("stream_id", stream_id)
+                    .tag("key_partition_id", partition_id)
+                    .tag("value_partition_id", offset.partition_id())
+                    .tag("state", offset.state());
+            return 1;
+        }
+
+        const int recycling = is_recycling(stream_id);
+        if (recycling < 0) {
+            return -1;
+        }
+        if (recycling > 0) {
+            return 2;
+        }
+        return 0;
+    };
+
+    std::string begin = table_stream_offset_key({instance_id_, 0, 0, 0, 0, 0});
+    const std::string latest_end = table_stream_offset_key({instance_id_, 
INT64_MAX, 0, 0, 0, 0});
+    int ret = scan_and_handle_kv(
+            begin, latest_end, [&](std::string_view key, std::string_view 
value) {
+                std::vector<std::tuple<std::variant<int64_t, std::string>, 
int, int>> components;
+                if (!decode_components(key, 8, &components)) {
+                    LOG_WARNING("failed to decode Latest Stream Offset 
key").tag("key", hex(key));
+                    return -1;
+                }
+                TableStreamOffsetPB offset;
+                if (!offset.ParseFromArray(value.data(), value.size())) {
+                    LOG_WARNING("failed to parse Latest Stream 
Offset").tag("key", hex(key));
+                    return -1;
+                }
+                int64_t base_db_id = 
std::get<int64_t>(std::get<0>(components[3]));
+                int64_t base_table_id = 
std::get<int64_t>(std::get<0>(components[4]));
+                int64_t stream_db_id = 
std::get<int64_t>(std::get<0>(components[5]));
+                int64_t stream_id = 
std::get<int64_t>(std::get<0>(components[6]));
+                int64_t partition_id = 
std::get<int64_t>(std::get<0>(components[7]));
+                int validation = validate_offset(base_db_id, base_table_id, 
stream_db_id, stream_id,
+                                                 partition_id, offset);
+                if (validation == 2) {
+                    return 0;
+                }
+                if (validation != 0) {
+                    return validation;
+                }
+                latest_offsets.emplace(std::string(key), std::move(offset));
+                return 0;
+            });
+    if (ret < 0) {
+        return ret;
+    }
+    check_ret = std::max(check_ret, ret);
+
+    std::unordered_map<std::string, VersionedOffset> versioned_offsets;
+    begin = versioned::table_stream_offset_key({instance_id_, 0, 0, 0, 0, 0});
+    const std::string versioned_end =
+            versioned::table_stream_offset_key({instance_id_, INT64_MAX, 0, 0, 
0, 0});
+    ret = scan_and_handle_kv(

Review Comment:
   [P2] Compare the two projections at one snapshot
   
   `scan_and_handle_kv` creates a fresh transaction for each call, so the 
Latest and Versioned maps are not a coherent snapshot. A normal consumer can 
advance both projections from 100 to 110 after the first scan: this code then 
compares Latest=100 from RV1 with Versioned=110 from RV2 and marks the checker 
job failed even though the atomic update is healthy. Use one stable read 
version for both scans, or revalidate every apparent mismatch from one fresh 
transaction before reporting corruption.



##########
fe/fe-core/src/main/java/org/apache/doris/datasource/InternalCatalog.java:
##########
@@ -4003,6 +4008,7 @@ public void createTableStream(CreateStreamCommand 
command) throws DdlException {
                     throw new DdlException("Unknown properties: " + 
properties);
                 }
                 newStream.setId((Env.getCurrentEnv().getNextId()));
+                beforeCreateTableStream(db, newStream, baseTable);

Review Comment:
   [P1] Make Stream publication recoverable before committing MetaService state
   
   This hook fully commits the new Stream and removes its PREPARED recycle 
marker before `createTableWithLock` arbitrates the name or journals the FE 
entry. Two concurrent same-name creates can therefore commit distinct IDs; only 
one is registered (and `IF NOT EXISTS` even returns success for the loser). A 
master crash in the same gap leaves the same permanent MetaService-only Stream, 
with no catalog owner or recycle key to clean it up. Add a durable pending/name 
reservation whose replay can finish or drop the ID before final activation; an 
in-process rollback alone will not close the crash window.



##########
fe/fe-core/src/main/java/org/apache/doris/catalog/stream/TableStreamManager.java:
##########
@@ -377,6 +416,114 @@ public void 
fillStreamConsumptionValuesMetadataResult(List<TRow> dataBatch) {
         }
     }
 
+    private void fillCloudStreamConsumptionValuesMetadataResult(List<TRow> 
dataBatch) throws UserException {
+        Map<Cloud.TableStreamIdentityPB, CloudStreamConsumptionSnapshot> 
snapshots = new LinkedHashMap<>();
+        for (Map.Entry<Long, Set<Long>> entry : copyDbStreamMap().entrySet()) {
+            Optional<Database> db = 
Env.getCurrentInternalCatalog().getDb(entry.getKey());
+            if (!db.isPresent()) {
+                continue;
+            }
+            for (Long tableId : entry.getValue()) {
+                Optional<Table> table = db.get().getTable(tableId);
+                if (!table.isPresent()) {
+                    continue;
+                }
+                Preconditions.checkArgument(table.get() instanceof 
OlapTableStream);
+                OlapTableStream stream = (OlapTableStream) table.get();
+                if (!stream.readLockIfExist()) {
+                    continue;
+                }
+                try {
+                    OlapTable baseTable = stream.getBaseTableNullable();
+                    if (baseTable == null || !baseTable.readLockIfExist()) {
+                        continue;
+                    }
+                    try {
+                        Map<Long, String> partitionNames = new 
LinkedHashMap<>();
+                        baseTable.getPartitions().forEach(partition ->
+                                partitionNames.put(partition.getId(), 
partition.getName()));
+                        Cloud.TableStreamIdentityPB identity = 
Cloud.TableStreamIdentityPB.newBuilder()
+                                
.setBaseDbId(stream.getBaseTableInfo().getDbId())
+                                
.setBaseTableId(stream.getBaseTableInfo().getTableId())
+                                .setStreamDbId(entry.getKey())
+                                .setStreamId(stream.getId())
+                                .build();
+                        CloudStreamConsumptionSnapshot previous = 
snapshots.put(identity,
+                                new 
CloudStreamConsumptionSnapshot(db.get().getFullName(), stream.getName(),
+                                        stream.getId(), partitionNames));
+                        Preconditions.checkState(previous == null,
+                                "Duplicate Cloud Table Stream identity %s", 
identity);
+                    } finally {
+                        baseTable.readUnlock();
+                    }
+                } finally {
+                    stream.readUnlock();
+                }
+            }
+        }
+        if (snapshots.isEmpty()) {
+            return;
+        }
+
+        Map<Cloud.TableStreamIdentityPB, Set<Long>> requestedPartitions = new 
LinkedHashMap<>();
+        snapshots.forEach((identity, snapshot) ->
+                requestedPartitions.put(identity, 
snapshot.partitionNames.keySet()));

Review Comment:
   [P2] Omit Streams with no current partitions from this batch
   
   A valid Stream can have an empty `partitionNames` map after all formal 
partitions are dropped, but this code still sends its identity with an empty 
partition set. MetaService deliberately rejects such a binding as 
`INVALID_ARGUMENT`; because every Stream is fetched in one RPC, that one empty 
Stream makes the whole consumption system-table query fail and hides rows for 
unrelated Streams. Skip empty sets here and return normally when all sets are 
empty; add a mixed empty/nonempty Stream test.



##########
cloud/test/meta_service_table_stream_test.cpp:
##########
@@ -0,0 +1,1006 @@
+// 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 <brpc/controller.h>
+#include <gen_cpp/cloud.pb.h>
+#include <gtest/gtest.h>
+
+#include <cstdint>
+#include <initializer_list>
+#include <limits>
+#include <memory>
+#include <optional>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include "common/config.h"
+#include "common/defer.h"
+#include "common/lexical_util.h"
+#include "meta-service/meta_service.h"
+#include "meta-store/keys.h"
+#include "meta-store/txn_kv.h"
+#include "meta-store/txn_kv_error.h"
+#include "meta-store/versioned_value.h"
+#include "recycler/recycler.h"
+#include "resource-manager/resource_manager.h"
+#include "snapshot/snapshot_manager.h"
+
+namespace doris::cloud {
+
+extern std::unique_ptr<MetaServiceProxy> get_meta_service(bool 
mock_resource_mgr);
+extern TxnErrorCode read_operation_log(Transaction* txn, std::string_view 
log_key,
+                                       Versionstamp* log_version, 
OperationLogPB* operation_log);
+
+class MetaServiceTableStreamTest : public ::testing::Test {
+protected:
+    void SetUp() override {
+        service_ = get_meta_service(false);
+        instance_id_ = "table_stream_read_state_instance";
+        cloud_unique_id_ = "1:" + instance_id_ + ":test";
+        identity_.set_base_db_id(1001);
+        identity_.set_base_table_id(1002);
+        identity_.set_stream_db_id(1003);
+        identity_.set_stream_id(1004);
+    }
+
+    void set_multi_version_status(MultiVersionStatus mode) {
+        multi_version_status_ = mode;
+        std::unique_ptr<Transaction> txn;
+        ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK);
+        InstanceInfoPB instance;
+        instance.set_instance_id(instance_id_);
+        instance.set_multi_version_status(mode);
+        txn->put(instance_key({instance_id_}), instance.SerializeAsString());
+        ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK);
+        auto [code, message] = 
service_->resource_mgr()->refresh_instance(instance_id_);
+        ASSERT_EQ(code, MetaServiceCode::OK) << message;
+    }
+
+    void put_partition_mapping(Transaction* txn, int64_t partition_id) {
+        PartitionIndexPB partition;
+        partition.set_db_id(identity_.base_db_id());
+        partition.set_table_id(identity_.base_table_id());
+        txn->put(versioned::partition_index_key({instance_id_, partition_id}),
+                 partition.SerializeAsString());
+    }
+
+    void put_latest_partition_state(Transaction* txn, int64_t partition_id, 
int64_t visible_version,
+                                    int64_t visible_tso, 
std::optional<int64_t> offset_tso) {
+        VersionPB version;
+        version.set_version(visible_version);
+        version.set_visible_tso(visible_tso);
+        txn->put(partition_version_key({instance_id_, identity_.base_db_id(),
+                                        identity_.base_table_id(), 
partition_id}),
+                 version.SerializeAsString());
+        if (offset_tso.has_value()) {
+            TableStreamOffsetPB offset;
+            offset.set_partition_id(partition_id);
+            offset.set_state(TABLE_STREAM_OFFSET_CONSUMED);
+            offset.set_offset_tso(*offset_tso);
+            offset.set_last_consumption_time_ms(1234);
+            txn->put(table_stream_offset_key({instance_id_, 
identity_.base_db_id(),
+                                              identity_.base_table_id(), 
identity_.stream_db_id(),
+                                              identity_.stream_id(), 
partition_id}),
+                     offset.SerializeAsString());
+        }
+    }
+
+    void put_versioned_partition_state(Transaction* txn, int64_t partition_id,
+                                       int64_t visible_version, int64_t 
visible_tso,
+                                       std::optional<int64_t> offset_tso) {
+        put_partition_mapping(txn, partition_id);
+        versioned_put(txn, versioned::meta_partition_key({instance_id_, 
partition_id}),
+                      Versionstamp(41, 0), "");
+
+        VersionPB version;
+        version.set_version(visible_version);
+        version.set_visible_tso(visible_tso);
+        versioned_put(txn, versioned::partition_version_key({instance_id_, 
partition_id}),
+                      Versionstamp(42, 0), version.SerializeAsString());
+        if (offset_tso.has_value()) {
+            TableStreamOffsetPB offset;
+            offset.set_partition_id(partition_id);
+            offset.set_state(TABLE_STREAM_OFFSET_INITIAL_SNAPSHOT_PENDING);
+            offset.set_offset_tso(*offset_tso);
+            versioned_put(txn,
+                          versioned::table_stream_offset_key(
+                                  {instance_id_, identity_.base_db_id(), 
identity_.base_table_id(),
+                                   identity_.stream_db_id(), 
identity_.stream_id(), partition_id}),
+                          Versionstamp(43, 0), offset.SerializeAsString());
+        }
+    }
+
+    GetTableStreamOffsetResponse get_read_state(const std::vector<int64_t>& 
partitions) {
+        GetTableStreamOffsetRequest request;
+        request.set_cloud_unique_id(cloud_unique_id_);
+        auto* binding = request.add_bindings();
+        binding->mutable_identity()->CopyFrom(identity_);
+        for (int64_t partition_id : partitions) {
+            binding->add_partition_ids(partition_id);
+        }
+        GetTableStreamOffsetResponse response;
+        brpc::Controller controller;
+        service_->get_table_stream_offset(&controller, &request, &response, 
nullptr);
+        return response;
+    }
+
+    GetTableStreamOffsetResponse get_read_state(std::initializer_list<int64_t> 
partitions) {
+        return get_read_state(std::vector<int64_t>(partitions));
+    }
+
+    int64_t begin_target_transaction(const std::string& label) {
+        BeginTxnRequest request;
+        request.set_cloud_unique_id(cloud_unique_id_);
+        TxnInfoPB* txn_info = request.mutable_txn_info();
+        txn_info->set_db_id(3001);
+        txn_info->set_label(label);
+        txn_info->add_table_ids(3002);
+        txn_info->set_timeout_ms(36000);
+        BeginTxnResponse response;
+        brpc::Controller controller;
+        service_->begin_txn(&controller, &request, &response, nullptr);
+        EXPECT_EQ(response.status().code(), MetaServiceCode::OK) << 
response.status().msg();
+        return response.txn_id();
+    }
+
+    CommitTxnRequest make_consume_request(int64_t txn_id, int64_t partition_id,
+                                          TableStreamOffsetStatePB 
expected_state,
+                                          int64_t expected_tso, int64_t 
next_tso) {
+        CommitTxnRequest request;
+        request.set_cloud_unique_id(cloud_unique_id_);
+        request.set_db_id(3001);
+        request.set_txn_id(txn_id);
+        TableStreamUpdatePB* stream_update = 
request.add_table_stream_updates();
+        stream_update->mutable_identity()->CopyFrom(identity_);
+        TableStreamPartitionUpdatePB* partition_update = 
stream_update->add_partition_updates();
+        partition_update->set_partition_id(partition_id);
+        partition_update->set_expected_state(expected_state);
+        if (expected_state != TABLE_STREAM_OFFSET_UNKNOWN) {
+            partition_update->set_expected_offset_tso(expected_tso);
+        }
+        partition_update->set_next_offset_tso(next_tso);
+        return request;
+    }
+
+    CommitTxnResponse commit_transaction(const CommitTxnRequest& request) {
+        CommitTxnResponse response;
+        brpc::Controller controller;
+        service_->commit_txn(&controller, &request, &response, nullptr);
+        return response;
+    }
+
+    CommitTxnResponse consume_partition(int64_t txn_id, int64_t partition_id,
+                                        TableStreamOffsetStatePB 
expected_state,
+                                        int64_t expected_tso, int64_t 
next_tso) {
+        return commit_transaction(
+                make_consume_request(txn_id, partition_id, expected_state, 
expected_tso, next_tso));
+    }
+
+    TableStreamOffsetPB get_latest_offset(const TableStreamIdentityPB& 
identity,
+                                          int64_t partition_id) {
+        std::unique_ptr<Transaction> txn;
+        EXPECT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK);
+        std::string value;
+        EXPECT_EQ(txn->get(table_stream_offset_key(
+                                   {instance_id_, identity.base_db_id(), 
identity.base_table_id(),
+                                    identity.stream_db_id(), 
identity.stream_id(), partition_id}),
+                           &value),
+                  TxnErrorCode::TXN_OK);
+        TableStreamOffsetPB offset;
+        EXPECT_TRUE(offset.ParseFromString(value));
+        return offset;
+    }
+
+    TableStreamOffsetPB get_latest_offset(int64_t partition_id) {
+        return get_latest_offset(identity_, partition_id);
+    }
+
+    void set_clone_source(const std::string& source_instance_id, Versionstamp 
snapshot_version) {
+        multi_version_status_ = MULTI_VERSION_READ_WRITE;
+        std::unique_ptr<Transaction> txn;
+        ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK);
+        InstanceInfoPB instance;
+        instance.set_instance_id(instance_id_);
+        instance.set_multi_version_status(MULTI_VERSION_READ_WRITE);
+        instance.set_source_instance_id(source_instance_id);
+        
instance.set_source_snapshot_id(SnapshotManager::serialize_snapshot_id(snapshot_version));
+        txn->put(instance_key({instance_id_}), instance.SerializeAsString());
+        ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK);
+        auto [code, message] = 
service_->resource_mgr()->refresh_instance(instance_id_);
+        ASSERT_EQ(code, MetaServiceCode::OK) << message;
+    }
+
+    Versionstamp put_auto_versioned_offset(const std::string& 
target_instance_id,
+                                           const TableStreamIdentityPB& 
identity,
+                                           int64_t partition_id, int64_t 
offset_tso,
+                                           bool write_latest = false) {
+        std::unique_ptr<Transaction> txn;
+        EXPECT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK);
+        TableStreamOffsetPB offset;
+        offset.set_partition_id(partition_id);
+        offset.set_state(TABLE_STREAM_OFFSET_CONSUMED);
+        offset.set_offset_tso(offset_tso);
+        const TableStreamOffsetKeyInfo key_info {target_instance_id,       
identity.base_db_id(),
+                                                 identity.base_table_id(), 
identity.stream_db_id(),
+                                                 identity.stream_id(),     
partition_id};
+        const std::string value = offset.SerializeAsString();
+        if (write_latest) {
+            txn->put(table_stream_offset_key(key_info), value);
+        }
+        txn->enable_get_versionstamp();
+        versioned_put(txn.get(), versioned::table_stream_offset_key(key_info), 
value);
+        EXPECT_EQ(txn->commit(), TxnErrorCode::TXN_OK);
+        Versionstamp version;
+        EXPECT_EQ(txn->get_versionstamp(&version), TxnErrorCode::TXN_OK);
+        return version;
+    }
+
+    std::unique_ptr<MetaServiceProxy> service_;
+    std::string instance_id_;
+    std::string cloud_unique_id_;
+    TableStreamIdentityPB identity_;
+    MultiVersionStatus multi_version_status_ = MULTI_VERSION_DISABLED;
+};
+
+TEST_F(MetaServiceTableStreamTest, ReadLatestAndUnknownOffsets) {
+    set_multi_version_status(MULTI_VERSION_DISABLED);
+    std::unique_ptr<Transaction> txn;
+    ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK);
+    put_latest_partition_state(txn.get(), 2001, 8, 130, 100);
+    put_latest_partition_state(txn.get(), 2002, 9, 140, std::nullopt);
+    ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK);
+
+    GetTableStreamOffsetResponse response = get_read_state({2001, 2002});
+    ASSERT_EQ(response.status().code(), MetaServiceCode::OK) << 
response.status().msg();
+    ASSERT_EQ(response.bindings_size(), 1);
+    ASSERT_EQ(response.bindings(0).partition_states_size(), 2);
+
+    const auto& consumed = response.bindings(0).partition_states(0);
+    EXPECT_EQ(consumed.partition_id(), 2001);
+    EXPECT_EQ(consumed.offset_state(), TABLE_STREAM_OFFSET_CONSUMED);
+    EXPECT_EQ(consumed.offset_tso(), 100);
+    EXPECT_EQ(consumed.end_tso(), 130);
+    EXPECT_EQ(consumed.visible_version(), 8);
+    EXPECT_EQ(consumed.last_consumption_time_ms(), 1234);
+
+    const auto& unknown = response.bindings(0).partition_states(1);
+    EXPECT_EQ(unknown.partition_id(), 2002);
+    EXPECT_EQ(unknown.offset_state(), TABLE_STREAM_OFFSET_UNKNOWN);
+    EXPECT_FALSE(unknown.has_offset_tso());
+    EXPECT_EQ(unknown.end_tso(), 140);
+    EXPECT_EQ(unknown.visible_version(), 9);
+}
+
+TEST_F(MetaServiceTableStreamTest, ReadVersionedState) {
+    set_multi_version_status(MULTI_VERSION_READ_WRITE);
+    std::unique_ptr<Transaction> txn;
+    ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK);
+    put_versioned_partition_state(txn.get(), 2001, 18, 230, 200);
+    ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK);
+
+    GetTableStreamOffsetResponse response = get_read_state({2001});
+    ASSERT_EQ(response.status().code(), MetaServiceCode::OK) << 
response.status().msg();
+    ASSERT_EQ(response.bindings_size(), 1);
+    ASSERT_EQ(response.bindings(0).partition_states_size(), 1);
+    const auto& state = response.bindings(0).partition_states(0);
+    EXPECT_EQ(state.offset_state(), 
TABLE_STREAM_OFFSET_INITIAL_SNAPSHOT_PENDING);
+    EXPECT_EQ(state.offset_tso(), 200);
+    EXPECT_EQ(state.end_tso(), 230);
+    EXPECT_EQ(state.visible_version(), 18);
+}
+
+TEST_F(MetaServiceTableStreamTest, ReadVersionedStateFromCloneChainInBatch) {
+    const std::string source_instance_id = "table_stream_batch_read_source";
+    set_clone_source(source_instance_id, Versionstamp(100, 0));
+    const std::vector<int64_t> partition_ids = {2001, 2002, 2003};
+
+    std::unique_ptr<Transaction> txn;
+    ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK);
+    for (size_t i = 0; i < partition_ids.size(); ++i) {
+        int64_t partition_id = partition_ids[i];
+        PartitionIndexPB partition;
+        partition.set_db_id(identity_.base_db_id());
+        partition.set_table_id(identity_.base_table_id());
+        txn->put(versioned::partition_index_key({source_instance_id, 
partition_id}),
+                 partition.SerializeAsString());
+        versioned_put(txn.get(), 
versioned::meta_partition_key({source_instance_id, partition_id}),
+                      Versionstamp(51 + i, 0), "");
+
+        VersionPB version;
+        version.set_version(10 + i);
+        version.set_visible_tso(1000 + i);
+        versioned_put(txn.get(),
+                      versioned::partition_version_key({source_instance_id, 
partition_id}),
+                      Versionstamp(60 + i, 0), version.SerializeAsString());
+    }
+
+    auto put_offset = [&](const std::string& target_instance_id, int64_t 
partition_id,
+                          Versionstamp versionstamp, int64_t offset_tso) {
+        TableStreamOffsetPB offset;
+        offset.set_partition_id(partition_id);
+        offset.set_state(TABLE_STREAM_OFFSET_CONSUMED);
+        offset.set_offset_tso(offset_tso);
+        versioned_put(
+                txn.get(),
+                versioned::table_stream_offset_key(
+                        {target_instance_id, identity_.base_db_id(), 
identity_.base_table_id(),
+                         identity_.stream_db_id(), identity_.stream_id(), 
partition_id}),
+                versionstamp, offset.SerializeAsString());
+    };
+    put_offset(source_instance_id, 2001, Versionstamp(70, 0), 701);
+    put_offset(source_instance_id, 2002, Versionstamp(71, 0), 702);
+    put_offset(instance_id_, 2002, Versionstamp(200, 0), 802);
+    ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK);
+
+    GetTableStreamOffsetResponse response = get_read_state(partition_ids);
+    ASSERT_EQ(response.status().code(), MetaServiceCode::OK) << 
response.status().msg();
+    ASSERT_EQ(response.bindings(0).partition_states_size(), 
partition_ids.size());
+    EXPECT_EQ(response.bindings(0).partition_states(0).offset_tso(), 701);
+    EXPECT_EQ(response.bindings(0).partition_states(1).offset_tso(), 802);
+    EXPECT_EQ(response.bindings(0).partition_states(2).offset_state(), 
TABLE_STREAM_OFFSET_UNKNOWN);
+}
+
+TEST_F(MetaServiceTableStreamTest, ReadLargePartitionBatch) {
+    set_multi_version_status(MULTI_VERSION_DISABLED);
+    constexpr int kPartitionCount = 1201;
+    std::vector<int64_t> partition_ids;
+    partition_ids.reserve(kPartitionCount);
+
+    std::unique_ptr<Transaction> txn;
+    ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK);
+    for (int i = 0; i < kPartitionCount; ++i) {
+        int64_t partition_id = 10000 + i;
+        partition_ids.push_back(partition_id);
+        put_latest_partition_state(txn.get(), partition_id, i + 1, 100000 + i, 
90000 + i);
+    }
+    ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK);
+
+    GetTableStreamOffsetResponse response = get_read_state(partition_ids);
+    ASSERT_EQ(response.status().code(), MetaServiceCode::OK) << 
response.status().msg();
+    ASSERT_EQ(response.bindings(0).partition_states_size(), kPartitionCount);
+    EXPECT_EQ(response.bindings(0).partition_states(0).partition_id(), 
partition_ids.front());
+    EXPECT_EQ(response.bindings(0).partition_states(0).offset_tso(), 90000);
+    EXPECT_EQ(response.bindings(0).partition_states(kPartitionCount - 
1).partition_id(),
+              partition_ids.back());
+    EXPECT_EQ(response.bindings(0).partition_states(kPartitionCount - 
1).offset_tso(),
+              90000 + kPartitionCount - 1);
+}
+
+TEST_F(MetaServiceTableStreamTest, ReadMultipleBindingsInBatch) {
+    set_multi_version_status(MULTI_VERSION_READ_WRITE);
+    constexpr int kBindingCount = 101;
+    constexpr int64_t kPartitionId = 2001;
+
+    std::unique_ptr<Transaction> txn;
+    ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK);
+    PartitionIndexPB partition;
+    partition.set_db_id(identity_.base_db_id());
+    partition.set_table_id(identity_.base_table_id());
+    txn->put(versioned::partition_index_key({instance_id_, kPartitionId}),
+             partition.SerializeAsString());
+    versioned_put(txn.get(), versioned::meta_partition_key({instance_id_, 
kPartitionId}),
+                  Versionstamp(20, 0), "");
+    VersionPB version;
+    version.set_version(8);
+    version.set_visible_tso(130);
+    versioned_put(txn.get(), versioned::partition_version_key({instance_id_, 
kPartitionId}),
+                  Versionstamp(21, 0), version.SerializeAsString());
+
+    GetTableStreamOffsetRequest request;
+    request.set_cloud_unique_id(cloud_unique_id_);
+    for (int i = 0; i < kBindingCount; ++i) {
+        int64_t stream_id = identity_.stream_id() + i;
+        TableStreamPartitionSetPB* binding = request.add_bindings();
+        binding->mutable_identity()->CopyFrom(identity_);
+        binding->mutable_identity()->set_stream_id(stream_id);
+        binding->add_partition_ids(kPartitionId);
+    }
+    ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK);
+
+    GetTableStreamOffsetResponse response;
+    brpc::Controller controller;
+    service_->get_table_stream_offset(&controller, &request, &response, 
nullptr);
+    ASSERT_EQ(response.status().code(), MetaServiceCode::OK) << 
response.status().msg();
+    ASSERT_EQ(response.bindings_size(), kBindingCount);
+    for (const TableStreamReadBindingResultPB& binding : response.bindings()) {
+        ASSERT_EQ(binding.partition_states_size(), 1);
+        EXPECT_EQ(binding.partition_states(0).offset_state(), 
TABLE_STREAM_OFFSET_UNKNOWN);
+    }
+}
+
+TEST_F(MetaServiceTableStreamTest, ReadWriteOnlyState) {
+    set_multi_version_status(MULTI_VERSION_WRITE_ONLY);
+    std::unique_ptr<Transaction> txn;
+    ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK);
+    put_latest_partition_state(txn.get(), 2001, 28, 330, 300);
+    ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK);
+
+    GetTableStreamOffsetResponse response = get_read_state({2001});
+    ASSERT_EQ(response.status().code(), MetaServiceCode::OK) << 
response.status().msg();
+    ASSERT_EQ(response.bindings_size(), 1);
+    ASSERT_EQ(response.bindings(0).partition_states_size(), 1);
+    const auto& state = response.bindings(0).partition_states(0);
+    EXPECT_EQ(state.offset_state(), TABLE_STREAM_OFFSET_CONSUMED);
+    EXPECT_EQ(state.offset_tso(), 300);
+    EXPECT_EQ(state.end_tso(), 330);
+    EXPECT_EQ(state.visible_version(), 28);
+}
+
+TEST_F(MetaServiceTableStreamTest, RejectDuplicateBindingsAndPartitions) {
+    GetTableStreamOffsetRequest request;
+    request.set_cloud_unique_id(cloud_unique_id_);
+    auto* binding = request.add_bindings();
+    binding->mutable_identity()->CopyFrom(identity_);
+    binding->add_partition_ids(2001);
+    binding->add_partition_ids(2001);
+    GetTableStreamOffsetResponse response;
+    brpc::Controller controller;
+    service_->get_table_stream_offset(&controller, &request, &response, 
nullptr);
+    EXPECT_EQ(response.status().code(), MetaServiceCode::INVALID_ARGUMENT);
+
+    request.mutable_bindings()->Clear();
+    for (int i = 0; i < 2; ++i) {
+        binding = request.add_bindings();
+        binding->mutable_identity()->CopyFrom(identity_);
+        binding->add_partition_ids(2001 + i);
+    }
+    response.Clear();
+    brpc::Controller second_controller;
+    service_->get_table_stream_offset(&second_controller, &request, &response, 
nullptr);
+    EXPECT_EQ(response.status().code(), MetaServiceCode::INVALID_ARGUMENT);
+}
+
+TEST_F(MetaServiceTableStreamTest, RejectEnabledModeAndRecyclingStream) {
+    set_multi_version_status(MULTI_VERSION_DISABLED);
+    std::unique_ptr<Transaction> txn;
+    ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK);
+    InstanceInfoPB instance;
+    instance.set_instance_id(instance_id_);
+    instance.set_multi_version_status(MULTI_VERSION_ENABLED);
+    txn->put(instance_key({instance_id_}), instance.SerializeAsString());
+    ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK);
+
+    GetTableStreamOffsetResponse response = get_read_state({2001});
+    EXPECT_EQ(response.status().code(), MetaServiceCode::INVALID_ARGUMENT);
+
+    set_multi_version_status(MULTI_VERSION_DISABLED);
+    ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK);
+    put_latest_partition_state(txn.get(), 2001, 8, 130, 100);
+    RecycleIndexPB recycle_index;
+    recycle_index.set_table_id(identity_.base_table_id());
+    recycle_index.set_state(RecycleIndexPB::RECYCLING);
+    recycle_index.set_object_type(TABLE_STREAM);
+    txn->put(recycle_index_key({instance_id_, identity_.stream_id()}),
+             recycle_index.SerializeAsString());
+    ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK);
+
+    response = get_read_state({2001});
+    EXPECT_EQ(response.status().code(), MetaServiceCode::INVALID_ARGUMENT);
+}
+
+TEST_F(MetaServiceTableStreamTest, CommitUpdatesLatestOffsetAndIsIdempotent) {

Review Comment:
   [P2] Exercise target-write/offset atomicity in this test
   
   All consumption tests begin an otherwise empty target transaction, so they 
only prove atomicity among offset keys. The feature's critical contract is that 
a stale CAS publishes neither the target rowset nor the offset, while a valid 
CAS publishes both in the same commit. Please prepare a real target tmp rowset 
here, assert both sides remain invisible after a stale expected offset, then 
commit a fresh transaction and assert the rowset and offset become visible 
together (including retry/idempotency).



##########
fe/fe-core/src/main/java/org/apache/doris/cloud/datasource/CloudInternalCatalog.java:
##########
@@ -949,6 +1053,9 @@ public void dropCloudPartition(long dbId, long tableId, 
List<Long> partitionIds,
         partitionRequestBuilder.setTableId(tableId);
         partitionRequestBuilder.addAllPartitionIds(partitionIds);
         partitionRequestBuilder.addAllIndexIds(indexIds);
+        partitionRequestBuilder.addAllTableStreams(

Review Comment:
   [P2] Coordinate partition drops with pending Stream creation
   
   The Stream's MetaService offsets are committed while CREATE holds only the 
base-table read lock, and the FE entry is published later under the Stream 
database lock. A concurrent `DROP PARTITION ... FORCE` can already hold the 
base database lock, run as soon as that read lock is released, and enumerate 
Stream identities here before the new entry is visible. Its operation log then 
omits the committed Stream offset; CREATE publishes afterward with a stale 
Latest/Versioned offset for the deleted partition, and no later Cloud cleanup 
removes it. Make pending Stream identities visible to base-table DDL, or 
revalidate and delete offsets for partitions removed before activation.



##########
fe/fe-core/src/main/java/org/apache/doris/catalog/CatalogRecycleBin.java:
##########
@@ -463,15 +474,22 @@ private void eraseTable(long currentTimeMs, int keepNum) {
             for (Long tableId : expiredIds) {
                 writeLock();
                 try {
-                    RecycleTableInfo tableInfo = idToTable.remove(tableId);
+                    RecycleTableInfo tableInfo = idToTable.get(tableId);
                     if (tableInfo == null) {
                         continue;
                     }
                     Table table = tableInfo.getTable();
+                    try {
+                        
Env.getCurrentInternalCatalog().beforeEraseTable(tableInfo.dbId, table, false);

Review Comment:
   [P1] Move the remote Stream drop outside the global recycle-bin lock
   
   For Cloud Streams this hook synchronously calls a retrying `dropIndex`, but 
this call and the other three new call sites all run under the recycle bin's 
single write lock. A slow/unavailable MetaService therefore blocks unrelated 
drops, recoveries, recycle inserts, and even read-side inspection for the 
nested retry/deadline window. Reserve or durably stage the exact entry while 
locked, perform the idempotent RPC after unlocking, then reacquire and verify 
the reservation before logging/removing it so recovery cannot race the 
completed remote drop.



##########
fe/fe-core/src/main/java/org/apache/doris/cloud/datasource/CloudInternalCatalog.java:
##########
@@ -98,6 +103,140 @@ public CloudInternalCatalog() {
         super();
     }
 
+    @Override
+    protected void setTableStreamProperties(BaseTableStream stream, 
Map<String, String> properties)
+            throws AnalysisException {
+        if (stream instanceof OlapTableStream) {
+            ((OlapTableStream) 
stream).setPropertiesWithoutOffsetInitialization(properties);
+            return;
+        }
+        super.setTableStreamProperties(stream, properties);
+    }
+
+    @Override
+    protected void beforeCreateTableStream(Database streamDb, BaseTableStream 
stream, TableIf baseTable)
+            throws DdlException {
+        if (!(stream instanceof OlapTableStream) || !(baseTable instanceof 
OlapTable)) {
+            throw new DdlException("Cloud Table Stream requires an OLAP base 
table");
+        }
+        OlapTableStream olapStream = (OlapTableStream) stream;
+        OlapTable olapBaseTable = (OlapTable) baseTable;
+        List<Cloud.TableStreamOffsetPB> initialOffsets = 
captureTableStreamInitialOffsets(
+                olapStream, olapBaseTable);
+        Set<Long> basePartitionIds = new 
HashSet<>(olapBaseTable.getPartitionIds());
+        Set<Long> offsetPartitionIds = initialOffsets.stream()
+                .map(Cloud.TableStreamOffsetPB::getPartitionId)
+                .collect(Collectors.toSet());
+        if (initialOffsets.size() != offsetPartitionIds.size()
+                || !basePartitionIds.equals(offsetPartitionIds)) {
+            throw new DdlException("Cloud Table Stream initial offsets do not 
match base table partitions");
+        }
+
+        long baseDbId = olapStream.getBaseTableInfo().getDbId();
+        prepareTableStream(baseDbId, olapBaseTable.getId(), streamDb.getId(), 
olapStream.getId());

Review Comment:
   [P2] Release the base-table lock before retrying MetaService RPCs
   
   `createTableStream` invokes this hook while holding `baseTable.readLock()`, 
and this call starts a sequence of prepare, partition-batch, and final-commit 
RPCs. Both this helper and `MetaServiceProxy` can retry and sleep, so an 
unhealthy MetaService holds the table lock across multiple deadline windows and 
blocks unrelated ALTER/DROP operations. Capture and validate an immutable 
base-table snapshot under the lock, then perform the remote work after 
unlocking, with the pending-create/revalidation state needed to detect 
intervening metadata changes.



##########
cloud/src/meta-service/meta_service_partition.cpp:
##########
@@ -83,6 +88,121 @@ static TxnErrorCode check_recycle_key_exist(Transaction* 
txn, const std::string&
     return txn->get(key, &val);
 }
 
+static bool is_table_stream_versioned_write(MultiVersionStatus 
multi_version_status) {
+    return multi_version_status == 
MultiVersionStatus::MULTI_VERSION_WRITE_ONLY ||
+           multi_version_status == 
MultiVersionStatus::MULTI_VERSION_READ_WRITE;
+}
+
+static bool validate_table_stream_index_request(const IndexRequest* request, 
MetaServiceCode& code,
+                                                std::string& msg) {
+    if (request->index_ids_size() != 1 || !request->has_db_id() || 
!request->has_stream_db_id()) {
+        code = MetaServiceCode::INVALID_ARGUMENT;
+        msg = "table stream requires exactly one index_id, db_id and 
stream_db_id";
+        return false;
+    }
+    if ((request->has_is_new_table() && request->is_new_table()) ||
+        !request->partition_ids().empty()) {
+        code = MetaServiceCode::INVALID_ARGUMENT;
+        msg = "table stream index must not create a table or physical 
partitions";
+        return false;
+    }
+    return true;
+}
+
+template <typename Request>
+static bool table_stream_recycle_index_matches(const RecycleIndexPB& 
recycle_index,
+                                               const Request& request) {
+    return recycle_index.state() == RecycleIndexPB::PREPARED &&
+           recycle_index.object_type() == IndexObjectTypePB::TABLE_STREAM &&
+           recycle_index.has_db_id() && recycle_index.db_id() == 
request.db_id() &&
+           recycle_index.table_id() == request.table_id() && 
recycle_index.has_stream_db_id() &&
+           recycle_index.stream_db_id() == request.stream_db_id();
+}
+
+static TxnErrorCode table_stream_offset_exists(Transaction* txn, const 
std::string& instance_id,
+                                               int64_t base_db_id, int64_t 
base_table_id,
+                                               int64_t stream_db_id, int64_t 
stream_id) {
+    const std::string begin = table_stream_offset_key_prefix(instance_id, 
base_db_id, base_table_id,
+                                                             stream_db_id, 
stream_id);
+    std::unique_ptr<RangeGetIterator> it;
+    TxnErrorCode err = txn->get(begin, lexical_end(begin), &it, false, 1);
+    if (err != TxnErrorCode::TXN_OK) {
+        return err;
+    }
+    // One returned key is sufficient for existence. `more()` only means that 
additional
+    // partition offsets remain in the range.
+    return it->has_next() ? TxnErrorCode::TXN_OK : 
TxnErrorCode::TXN_KEY_NOT_FOUND;
+}
+
+static TxnErrorCode get_table_stream_min_local_offset_version(
+        Transaction* txn, const std::string& instance_id, int64_t base_db_id, 
int64_t base_table_id,
+        int64_t stream_db_id, int64_t stream_id, Versionstamp* min_version) {
+    // Stream recycling removes only the current instance's offset prefix. 
Keep this range read in
+    // the DROP transaction so an offset write committed before this 
transaction cannot escape the
+    // min_timestamp calculation. A later recycle index rejects writes after 
it is materialized.
+    const std::string begin = versioned::table_stream_offset_key_prefix(
+            instance_id, base_db_id, base_table_id, stream_db_id, stream_id);
+    FullRangeGetOptions options;
+    options.snapshot = false;
+    options.prefetch = true;
+    auto iter = txn->full_range_get(begin, lexical_end(begin), options);

Review Comment:
   [P2] Avoid scanning the full offset history in the DROP transaction
   
   This binds `full_range_get` to the same FoundationDB transaction that must 
later install the drop log, then walks every retained Versioned offset. Every 
consumption adds more keys and active snapshots intentionally retain old 
versions, so a large Stream history can exceed the transaction timeout before 
this loop completes. Retrying repeats the same unbounded scan, leaving the 
Stream undroppable. Maintain a conservative per-Stream earliest-local-version 
summary with offset writes (a stale-low value is safe), conflict-read that 
bounded key during DROP, and clear it during final recycling.



##########
cloud/src/meta-service/meta_service_txn.cpp:
##########
@@ -48,6 +54,280 @@ namespace doris::cloud {
 
 static constexpr std::string_view kMetaSyncPointDummyKey = 
"__meta_service_sync_point_dummy_key__";
 
+static bool validate_table_stream_updates(const CommitTxnRequest* request, 
MetaServiceCode& code,
+                                          std::string& msg) {
+    if (request->has_is_2pc() && request->is_2pc()) {
+        code = MetaServiceCode::INVALID_ARGUMENT;
+        msg = "table stream consumption does not support 2PC";
+        return false;
+    }
+    if ((request->has_is_txn_load() && request->is_txn_load()) ||
+        !request->sub_txn_infos().empty()) {
+        code = MetaServiceCode::INVALID_ARGUMENT;
+        msg = "table stream consumption does not support transaction load or 
sub transactions";
+        return false;
+    }
+
+    std::set<std::pair<int64_t, int64_t>> stream_partitions;
+    for (const auto& stream_update : request->table_stream_updates()) {
+        if (!stream_update.has_identity() ||
+            !is_valid_table_stream_identity(stream_update.identity()) ||
+            stream_update.partition_updates().empty()) {
+            code = MetaServiceCode::INVALID_ARGUMENT;
+            msg = "invalid table stream update";
+            return false;
+        }
+        for (const auto& partition_update : stream_update.partition_updates()) 
{
+            if (!partition_update.has_partition_id() || 
partition_update.partition_id() <= 0 ||
+                !partition_update.has_expected_state() || 
!partition_update.has_next_offset_tso()) {
+                code = MetaServiceCode::INVALID_ARGUMENT;
+                msg = "invalid table stream partition update";
+                return false;
+            }
+            if (partition_update.expected_state() !=
+                        TableStreamOffsetStatePB::TABLE_STREAM_OFFSET_UNKNOWN 
&&
+                partition_update.expected_state() !=
+                        
TableStreamOffsetStatePB::TABLE_STREAM_OFFSET_INITIAL_SNAPSHOT_PENDING &&
+                partition_update.expected_state() !=
+                        
TableStreamOffsetStatePB::TABLE_STREAM_OFFSET_CONSUMED) {
+                code = MetaServiceCode::INVALID_ARGUMENT;
+                msg = "invalid expected table stream offset state";
+                return false;
+            }
+            bool expects_existing_offset = partition_update.expected_state() !=
+                                           
TableStreamOffsetStatePB::TABLE_STREAM_OFFSET_UNKNOWN;
+            if (partition_update.has_expected_offset_tso() != 
expects_existing_offset) {
+                code = MetaServiceCode::INVALID_ARGUMENT;
+                msg = "expected offset TSO does not match table stream offset 
state";
+                return false;
+            }
+            auto [_, inserted] = 
stream_partitions.emplace(stream_update.identity().stream_id(),
+                                                           
partition_update.partition_id());
+            if (!inserted) {
+                code = MetaServiceCode::INVALID_ARGUMENT;
+                msg = "duplicate table stream partition update";
+                return false;
+            }
+        }
+    }
+    return true;
+}
+
+static void append_table_stream_commit_size_error(TxnErrorCode err, 
std::string& msg) {
+    if (err == TxnErrorCode::TXN_BYTES_TOO_LARGE) {
+        msg += ", table stream offset updates cannot use lazy commit. "
+               "Please consume fewer partitions in one statement.";
+    }
+}
+
+class TableStreamUpdateTxnContext {
+public:
+    TableStreamUpdateTxnContext(Transaction* txn, const std::string& 
instance_id,
+                                MultiVersionStatus multi_version_status,
+                                CloneChainReader* clone_reader, 
MetaServiceCode& code,
+                                std::string& msg)
+            : txn_(txn),
+              instance_id_(instance_id),
+              metadata_reader_(txn, instance_id, multi_version_status, 
clone_reader),
+              code_(code),
+              msg_(msg),
+              consumption_time_ms_(
+                      
duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count()) {
+    }
+
+    bool versioned_write() const { return 
metadata_reader_.writes_versioned_metadata(); }
+
+    bool check_stream(const TableStreamIdentityPB& identity) {
+        const std::vector<int64_t> stream_ids {identity.stream_id()};
+        std::unordered_set<int64_t> recycling_stream_ids;
+        if (!apply_read_result(metadata_reader_.read_recycling_streams(
+                    stream_ids, TableStreamReadIntent::CONFLICT, 
&recycling_stream_ids))) {
+            return false;
+        }
+        if (recycling_stream_ids.contains(identity.stream_id())) {

Review Comment:
   [P1] Keep DROP fenced until older consume transactions expire
   
   This is the only lifecycle guard read by a consume commit, but final Stream 
recycling deletes the key after removing both offset prefixes. A transaction 
planned before DROP for a newly added partition carries 
`expected_state=UNKNOWN`; once this marker is gone, the live base partition 
still validates and absent-offset CAS succeeds, so the old target commit 
recreates Latest/Versioned offsets for a Stream that no longer exists in FE. 
The default dropped-index retention is 3 hours while valid load transactions 
may run for up to 3 days. Retain a tombstone that commits conflict-read for at 
least the maximum transaction lifetime (or durably for non-reused Stream IDs), 
and test DROP plus full recycle before a delayed UNKNOWN-state commit.



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