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


##########
gensrc/proto/internal_service.proto:
##########
@@ -471,17 +471,21 @@ message PKinesisLoadInfo {
 
 message PKinesisMetaProxyRequest {
     optional PKinesisLoadInfo kinesis_info = 1;
+    // Resolve initial LATEST positions before data tasks are created.
+    repeated string shard_ids_for_latest_sequences = 2;
 };
 
 message PShardInfo {
     required string shard_id = 1;
     optional string parent_shard_id = 2;
     optional string adjacent_parent_shard_id = 3;
+    optional bool closed = 4;
 };
 
 message PKinesisMetaProxyResult {
-    repeated string shard_ids = 1;  // Deprecated, use shard_infos instead
-    repeated PShardInfo shard_infos = 2;
+    repeated PShardInfo shard_infos = 1;

Review Comment:
   [P1] Keep the existing field numbers for the supported old-FE/new-BE upgrade 
phase. Before this change tag 1 was `repeated string shard_ids` and tag 2 was 
`repeated PShardInfo shard_infos`; because the replacement payloads are also 
length-delimited, the old FE decodes serialized `PShardInfo` bytes as shard IDs 
instead of skipping them. Please preserve `shard_ids = 1`, keep the full 
retained open/closed `shard_infos = 2`, and put `shard_latest_sequences` on a 
new tag. Populate legacy field 1 with OPEN shard IDs only, matching the 
pre-change BE behavior; populating it from all retained descriptors would make 
an old FE revive completed parents. Add a mixed-version serialization fixture 
for this direction.



##########
fe/fe-core/src/main/java/org/apache/doris/load/routineload/kinesis/KinesisRoutineLoadJob.java:
##########
@@ -128,8 +133,19 @@ public class KinesisRoutineLoadJob extends RoutineLoadJob {
     // Will be updated periodically by calling hasMoreDataToConsume()
     private Map<String, Long> cachedShardWithMillsBehindLatest = 
Maps.newConcurrentMap();
 
-    // newly discovered shards from Kinesis.
-    private List<String> newCurrentKinesisShards = Lists.newArrayList();
+    // Compatibility views for SHOW/old unit fixtures. Topology remains the 
only source of truth.

Review Comment:
   [P1] Please migrate the old persisted shard boundary before making these 
lists transient. Existing images contain `opks`/`clks` but no `topo`; Gson now 
ignores those keys, constructs an empty topology, and `gsonPostProcess()` only 
reconciles positions into nodes that already exist. The first post-upgrade 
ListShards result is consequently treated as a fresh initial snapshot. In 
particular, an already completed parent was removed from old progress and 
`clks` but can still be retained by Kinesis, so this code recreates and 
schedules it from the default position, duplicating data. Keep legacy holders 
for migration and build the initial topology/boundary from them before 
accepting the full retained-shard snapshot; add a pre-change image fixture 
covering a completed retained parent.



##########
be/src/load/routine_load/routine_load_task_executor.cpp:
##########
@@ -176,13 +195,117 @@ Status 
RoutineLoadTaskExecutor::get_kinesis_shard_meta(const PKinesisMetaProxyRe
     std::shared_ptr<DataConsumer> consumer;
     RETURN_IF_ERROR(_data_consumer_pool.get_consumer(ctx, &consumer));
 
-    Status st = 
std::static_pointer_cast<KinesisDataConsumer>(consumer)->get_shard_list(shard_ids);
+    Status st =
+            
std::static_pointer_cast<KinesisDataConsumer>(consumer)->get_shard_list(shard_infos);
     if (st.ok()) {
         _data_consumer_pool.return_consumer(consumer);
     }
     return st;
 }
 
+// All workers of one RPC share a deadline and publish results only after all 
workers exit.
+struct KinesisLatestSequenceBatch {
+    PKinesisMetaProxyRequest request;
+    int64_t deadline_ms;
+    std::function<bool()> is_cancelled;
+    RoutineLoadTaskExecutor::KinesisScanCallback on_finish;
+    std::atomic<int> next_shard {0};
+    std::atomic<int> remaining_workers;
+    std::mutex mutex;
+    Status status;
+    std::map<std::string, std::string> sequences;
+
+    KinesisLatestSequenceBatch(PKinesisMetaProxyRequest req, int64_t 
timeout_ms,
+                               std::function<bool()> cancelled,
+                               RoutineLoadTaskExecutor::KinesisScanCallback 
finish, int workers)
+            : request(std::move(req)),
+              deadline_ms(timeout_ms == -1 ? -1 : MonotonicMillis() + 
timeout_ms),
+              is_cancelled(std::move(cancelled)),
+              on_finish(std::move(finish)),
+              remaining_workers(workers) {}
+
+    Status check_status() {
+        std::lock_guard<std::mutex> lock(mutex);
+        RETURN_IF_ERROR(status);
+        if (is_cancelled()) {
+            return Status::Cancelled("Kinesis latest sequence scan cancelled");
+        }
+        if (deadline_ms != -1 && MonotonicMillis() >= deadline_ms) {
+            return Status::TimedOut("Kinesis latest sequence scan exceeded its 
total timeout");
+        }
+        return Status::OK();
+    }
+
+    void finish_worker(const Status& worker_status) {
+        {
+            std::lock_guard<std::mutex> lock(mutex);
+            if (status.ok() && !worker_status.ok()) {
+                status = worker_status;
+            }
+        }
+        if (remaining_workers.fetch_sub(1) == 1) {
+            // No worker or SDK callback may access the RPC controller after 
on_finish runs.
+            Status final_status = check_status();
+            on_finish(final_status, sequences);
+        }
+    }
+};
+
+Status RoutineLoadTaskExecutor::_run_kinesis_scan_worker(
+        const std::shared_ptr<KinesisLatestSequenceBatch>& batch) {
+    RETURN_IF_ERROR(batch->check_status());
+    auto ctx = std::make_shared<StreamLoadContext>(_exec_env);
+    RETURN_IF_ERROR(_prepare_ctx(batch->request, ctx));
+    // Each worker owns a client. Never share or return an in-flight scan 
consumer.
+    KinesisDataConsumer consumer(ctx, 
config::kinesis_latest_sequence_request_timeout_ms);
+    RETURN_IF_ERROR(consumer.init(ctx));
+    while (true) {
+        RETURN_IF_ERROR(batch->check_status());
+        int index = batch->next_shard.fetch_add(1);
+        if (index >= batch->request.shard_ids_for_latest_sequences_size()) {
+            return Status::OK();
+        }
+        const auto& shard = 
batch->request.shard_ids_for_latest_sequences(index);
+        std::string sequence;
+        RETURN_IF_ERROR(consumer.get_latest_sequence_number(
+                shard, [batch] { return batch->check_status(); }, &sequence));
+        std::lock_guard<std::mutex> lock(batch->mutex);
+        batch->sequences.emplace(shard, std::move(sequence));
+    }
+}
+
+void RoutineLoadTaskExecutor::get_kinesis_latest_sequence_numbers(
+        const PKinesisMetaProxyRequest& request, int64_t timeout_ms,
+        std::function<bool()> is_cancelled, KinesisScanCallback on_finish) {
+    CHECK(request.has_kinesis_info());
+    int workers = std::min(request.shard_ids_for_latest_sequences_size(),
+                           config::kinesis_latest_sequence_scan_threads);
+    DCHECK_GT(workers, 0);
+    auto batch = std::make_shared<KinesisLatestSequenceBatch>(
+            request, timeout_ms,
+            [this, is_cancelled = std::move(is_cancelled)] {
+                return _kinesis_scan_stopping.load() || is_cancelled();
+            },
+            std::move(on_finish), workers);
+    for (int i = 0; i < workers; ++i) {
+        auto st = _kinesis_scan_pool->submit_func([this, batch] {

Review Comment:
   [P2] Attach a non-Orphan memory limiter at this new pool-callback entry. 
These workers construct a stream-load context and AWS client and can 
materialize repeated 10,000-record pages, but a bare `ThreadPool` worker 
initializes its TLS limiter to `Orphan`; nothing in this path switches it. That 
bypasses task/subsystem accounting and trips orphan-memory checking for a 
potentially large scan. Please store a per-RPC or subsystem limiter with the 
batch and enter the callback with `SCOPED_ATTACH_TASK` before allocating the 
context/client or fetching records.



##########
be/src/load/routine_load/routine_load_task_executor.cpp:
##########
@@ -176,13 +195,117 @@ Status 
RoutineLoadTaskExecutor::get_kinesis_shard_meta(const PKinesisMetaProxyRe
     std::shared_ptr<DataConsumer> consumer;
     RETURN_IF_ERROR(_data_consumer_pool.get_consumer(ctx, &consumer));
 
-    Status st = 
std::static_pointer_cast<KinesisDataConsumer>(consumer)->get_shard_list(shard_ids);
+    Status st =
+            
std::static_pointer_cast<KinesisDataConsumer>(consumer)->get_shard_list(shard_infos);
     if (st.ok()) {
         _data_consumer_pool.return_consumer(consumer);
     }
     return st;
 }
 
+// All workers of one RPC share a deadline and publish results only after all 
workers exit.
+struct KinesisLatestSequenceBatch {
+    PKinesisMetaProxyRequest request;
+    int64_t deadline_ms;
+    std::function<bool()> is_cancelled;
+    RoutineLoadTaskExecutor::KinesisScanCallback on_finish;
+    std::atomic<int> next_shard {0};
+    std::atomic<int> remaining_workers;
+    std::mutex mutex;
+    Status status;
+    std::map<std::string, std::string> sequences;
+
+    KinesisLatestSequenceBatch(PKinesisMetaProxyRequest req, int64_t 
timeout_ms,
+                               std::function<bool()> cancelled,
+                               RoutineLoadTaskExecutor::KinesisScanCallback 
finish, int workers)
+            : request(std::move(req)),
+              deadline_ms(timeout_ms == -1 ? -1 : MonotonicMillis() + 
timeout_ms),
+              is_cancelled(std::move(cancelled)),
+              on_finish(std::move(finish)),
+              remaining_workers(workers) {}
+
+    Status check_status() {
+        std::lock_guard<std::mutex> lock(mutex);
+        RETURN_IF_ERROR(status);
+        if (is_cancelled()) {
+            return Status::Cancelled("Kinesis latest sequence scan cancelled");
+        }
+        if (deadline_ms != -1 && MonotonicMillis() >= deadline_ms) {
+            return Status::TimedOut("Kinesis latest sequence scan exceeded its 
total timeout");
+        }
+        return Status::OK();
+    }
+
+    void finish_worker(const Status& worker_status) {
+        {
+            std::lock_guard<std::mutex> lock(mutex);
+            if (status.ok() && !worker_status.ok()) {
+                status = worker_status;
+            }
+        }
+        if (remaining_workers.fetch_sub(1) == 1) {
+            // No worker or SDK callback may access the RPC controller after 
on_finish runs.
+            Status final_status = check_status();
+            on_finish(final_status, sequences);
+        }
+    }
+};
+
+Status RoutineLoadTaskExecutor::_run_kinesis_scan_worker(
+        const std::shared_ptr<KinesisLatestSequenceBatch>& batch) {
+    RETURN_IF_ERROR(batch->check_status());
+    auto ctx = std::make_shared<StreamLoadContext>(_exec_env);
+    RETURN_IF_ERROR(_prepare_ctx(batch->request, ctx));
+    // Each worker owns a client. Never share or return an in-flight scan 
consumer.
+    KinesisDataConsumer consumer(ctx, 
config::kinesis_latest_sequence_request_timeout_ms);
+    RETURN_IF_ERROR(consumer.init(ctx));
+    while (true) {
+        RETURN_IF_ERROR(batch->check_status());
+        int index = batch->next_shard.fetch_add(1);
+        if (index >= batch->request.shard_ids_for_latest_sequences_size()) {
+            return Status::OK();
+        }
+        const auto& shard = 
batch->request.shard_ids_for_latest_sequences(index);
+        std::string sequence;
+        RETURN_IF_ERROR(consumer.get_latest_sequence_number(
+                shard, [batch] { return batch->check_status(); }, &sequence));
+        std::lock_guard<std::mutex> lock(batch->mutex);
+        batch->sequences.emplace(shard, std::move(sequence));
+    }
+}
+
+void RoutineLoadTaskExecutor::get_kinesis_latest_sequence_numbers(
+        const PKinesisMetaProxyRequest& request, int64_t timeout_ms,
+        std::function<bool()> is_cancelled, KinesisScanCallback on_finish) {
+    CHECK(request.has_kinesis_info());
+    int workers = std::min(request.shard_ids_for_latest_sequences_size(),
+                           config::kinesis_latest_sequence_scan_threads);
+    DCHECK_GT(workers, 0);
+    auto batch = std::make_shared<KinesisLatestSequenceBatch>(
+            request, timeout_ms,
+            [this, is_cancelled = std::move(is_cancelled)] {
+                return _kinesis_scan_stopping.load() || is_cancelled();
+            },
+            std::move(on_finish), workers);
+    for (int i = 0; i < workers; ++i) {
+        auto st = _kinesis_scan_pool->submit_func([this, batch] {
+            Status worker_status;
+            try {
+                worker_status = _run_kinesis_scan_worker(batch);
+            } catch (const Exception& e) {
+                worker_status = Status::Error<false>(e.code(), e.to_string());
+            } catch (const std::exception& e) {
+                worker_status =
+                        Status::InternalError("Kinesis latest sequence scan 
failed: {}", e.what());
+            }
+            batch->finish_worker(worker_status);
+        });
+        if (!st.ok()) {
+            batch->finish_worker(st);

Review Comment:
   [P1] Do not account this worker as rejected solely from `submit_func()`'s 
return value. `ThreadPool::do_submit` queues the runnable before attempting 
thread creation, and if creation fails with no workers it returns that error 
without removing the queued runnable; another caller explicitly documents this 
retained-runnable contract. Here the error path decrements `remaining_workers`, 
so a one-worker RPC can run `done` immediately and later execute the retained 
lambda, which accesses the captured controller and decrements the batch a 
second time. Please add an exactly-once admission/completion guard (or use an 
API with unambiguous rejection semantics) so the submitter and runnable cannot 
finish the same logical worker.



##########
fe/fe-core/src/main/java/org/apache/doris/load/routineload/kinesis/KinesisRoutineLoadJob.java:
##########
@@ -699,82 +904,86 @@ public void modifyProperties(AlterRoutineLoadCommand 
command) throws UserExcepti
     private void modifyPropertiesInternal(Map<String, String> jobProperties,
                                           KinesisDataSourceProperties 
dataSourceProperties)
             throws UserException {
-        if (dataSourceProperties != null) {
-            List<Pair<String, String>> shardPositions = Lists.newArrayList();
-            Map<String, String> customKinesisProperties = Maps.newHashMap();
-            boolean resetProgress = false;
-            boolean hasExplicitShardPositions = false;
+        List<Pair<String, String>> shardPositions = Lists.newArrayList();
+        Map<String, String> customKinesisProperties = Maps.newHashMap();
+        boolean resetProgress = false;
+        boolean sourceChanged = false;
+        boolean sourceGenerationBumped = false;
+        boolean hasExplicitShardPositions = false;
 
+        if (dataSourceProperties != null) {
             if 
(MapUtils.isNotEmpty(dataSourceProperties.getOriginalDataSourceProperties())) {
                 shardPositions = 
dataSourceProperties.getKinesisShardPositions();
                 customKinesisProperties = 
dataSourceProperties.getCustomKinesisProperties();
                 hasExplicitShardPositions = !shardPositions.isEmpty();
+                sourceChanged = true;
             }
+            resetProgress = 
!Strings.isNullOrEmpty(dataSourceProperties.getStream());
+            sourceChanged |= resetProgress
+                    || !Strings.isNullOrEmpty(dataSourceProperties.getRegion())
+                    || 
!Strings.isNullOrEmpty(dataSourceProperties.getEndpoint());
+        }
 
-            // Update custom properties
+        // Validate every failure-prone input before mutating Kinesis or 
common job state.
+        if (hasExplicitShardPositions && !resetProgress) {
+            ((KinesisProgress) progress).checkShards(shardPositions);
+        }
+        if (!jobProperties.isEmpty()) {
+            Map<String, String> copiedJobProperties = 
Maps.newHashMap(jobProperties);
+            modifyCommonJobProperties(copiedJobProperties);
+            this.jobProperties.putAll(copiedJobProperties);
+            if 
(jobProperties.containsKey(CreateRoutineLoadInfo.PARTIAL_COLUMNS)) {
+                this.isPartialUpdate = 
BooleanUtils.toBoolean(jobProperties.get(CreateRoutineLoadInfo.PARTIAL_COLUMNS));
+            }
+            if 
(jobProperties.containsKey(CreateRoutineLoadInfo.PARTIAL_UPDATE_NEW_KEY_POLICY))
 {
+                String policy = 
jobProperties.get(CreateRoutineLoadInfo.PARTIAL_UPDATE_NEW_KEY_POLICY);
+                this.partialUpdateNewKeyPolicy = 
"ERROR".equalsIgnoreCase(policy)
+                        ? TPartialUpdateNewRowPolicy.ERROR : 
TPartialUpdateNewRowPolicy.APPEND;
+            }
+        }
+
+        if (dataSourceProperties != null) {
             if (!customKinesisProperties.isEmpty()) {
                 this.customProperties.putAll(customKinesisProperties);
                 convertCustomProperties(true);
             }
-
-            // Modify stream if provided
             if (!Strings.isNullOrEmpty(dataSourceProperties.getStream())) {
                 this.stream = dataSourceProperties.getStream();
-                resetProgress = true;
             }
-
-            // Modify region if provided
             if (!Strings.isNullOrEmpty(dataSourceProperties.getRegion())) {
                 this.region = dataSourceProperties.getRegion();
             }
-
-            // Modify endpoint if provided
             if (!Strings.isNullOrEmpty(dataSourceProperties.getEndpoint())) {
                 this.endpoint = dataSourceProperties.getEndpoint();
             }
 
+            if (sourceChanged && !sourceGenerationBumped) {
+                sourceGeneration++;
+                resetLatestSequenceFetch();
+                newCurrentKinesisShardInfos = null;
+                sourceGenerationBumped = true;
+            }
             if (resetProgress) {
                 this.progress = new KinesisProgress();
+                this.shardTopology.reset();

Review Comment:
   [P1] Use the full source-identity change here, not only `resetProgress` 
(which is true only when `stream` is supplied). The new code correctly treats 
region and endpoint changes as `sourceChanged`, but a region-only or 
endpoint-only ALTER retains the previous topology, completion states, 
explicit-shard filter, lag, and sequence numbers. Kinesis shard IDs are reused 
across sources, so the next task can send an old source's sequence as 
`AFTER_SEQUENCE_NUMBER` to the new source or leave a same-named shard 
permanently completed. Please reset all source-bound state for 
stream/region/endpoint changes and then apply any explicit new positions; cover 
region-only and endpoint-only ALTER plus journal replay.



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