Copilot commented on code in PR #61782:
URL: https://github.com/apache/doris/pull/61782#discussion_r3055431164


##########
be/src/exec/runtime_filter/runtime_filter.cpp:
##########
@@ -26,6 +28,34 @@
 
 namespace doris {
 #include "common/compile_check_begin.h"
+
+namespace {
+template <typename Response, typename Lock>
+class ManagedTrivialBrpcCallback final : public TrivialBrpcCallback<Response> {
+    ENABLE_FACTORY_CREATOR(ManagedTrivialBrpcCallback);
+
+public:
+    ManagedTrivialBrpcCallback(Lock* lock, std::vector<std::shared_ptr<void>>* 
callbacks,
+                               std::weak_ptr<QueryContext> context = {})
+            : TrivialBrpcCallback<Response>(std::move(context)),
+              _lock(lock),
+              _callbacks(callbacks) {}
+
+    void call() override {
+        TrivialBrpcCallback<Response>::call();
+        std::lock_guard<Lock> l(*_lock);
+        _callbacks->erase(
+                std::remove_if(_callbacks->begin(), _callbacks->end(),
+                               [this](const auto& callback) { return 
callback.get() == this; }),
+                _callbacks->end());
+    }

Review Comment:
   `ManagedTrivialBrpcCallback` keeps raw pointers to `_rmtx` and 
`_async_rpc_callbacks` and erases itself from `_async_rpc_callbacks` inside 
`call()`. If the `RuntimeFilter` instance is destroyed concurrently with RPC 
completion, the callback may run while the vector/mutex are being destructed, 
which is undefined behavior. To make this safe, keep the removal bookkeeping in 
a ref-counted shared state (owned by both the filter and callbacks) or 
otherwise guarantee the `RuntimeFilter` outlives all in-flight callbacks.



##########
be/src/exec/runtime_filter/runtime_filter_producer.h:
##########
@@ -17,15 +17,71 @@
 
 #pragma once
 
+#include <glog/logging.h>
+
 #include <mutex>
 
 #include "exec/pipeline/dependency.h"
 #include "exec/runtime_filter/runtime_filter.h"
 #include "runtime/query_context.h"
-#include "runtime/runtime_profile.h"
 
 namespace doris {
 #include "common/compile_check_begin.h"
+
+// Callback for sync-size RPCs. Handles errors (disable wrapper + sub 
dependency) in call().
+class SyncSizeCallback : public TrivialBrpcCallback<PSendFilterSizeResponse> {
+    ENABLE_FACTORY_CREATOR(SyncSizeCallback);
+
+public:
+    SyncSizeCallback(std::shared_ptr<Dependency> dependency,
+                     std::shared_ptr<RuntimeFilterWrapper> wrapper,
+                     std::weak_ptr<QueryContext> context)
+            : _dependency(std::move(dependency)), _wrapper(wrapper), 
_context(std::move(context)) {}
+
+    void call() override {
+        // On error: disable the wrapper and sub the dependency here, because 
set_synced_size()
+        // will never be called (the merge node won't respond with a sync).
+        // On success: do NOT sub here. The merge node will respond with 
sync_filter_size,
+        // which calls set_synced_size() -> _dependency->sub().
+        if (cntl_->Failed()) {
+            LOG(WARNING) << fmt::format("RPC meet failed: {}", 
cntl_->ErrorText());
+            if (auto w = _wrapper.lock()) {
+                w->set_state(RuntimeFilterWrapper::State::DISABLED, 
cntl_->ErrorText());
+            }
+            if (auto ctx = _context.lock()) {
+                if (!ctx->ignore_runtime_filter_error()) {
+                    ctx->cancel(Status::NetworkError("RPC meet failed: {}", 
cntl_->ErrorText()));
+                }
+            }
+            ((CountedFinishDependency*)_dependency.get())->sub();
+            return;

Review Comment:
   `SyncSizeCallback::call()` uses a C-style downcast to 
`CountedFinishDependency` (`(CountedFinishDependency*)_dependency.get()`). 
Since `_dependency` is typed as `std::shared_ptr<Dependency>` in this callback, 
the cast is unchecked and becomes UB if a different `Dependency` implementation 
is passed in the future. Prefer storing a 
`std::shared_ptr<CountedFinishDependency>` in the callback (or use 
`static_pointer_cast` with a DCHECK/dynamic_cast in debug builds) to make the 
type guarantee explicit.



##########
be/src/exec/runtime_filter/runtime_filter_producer.h:
##########
@@ -17,15 +17,71 @@
 
 #pragma once
 
+#include <glog/logging.h>
+
 #include <mutex>
 
 #include "exec/pipeline/dependency.h"
 #include "exec/runtime_filter/runtime_filter.h"
 #include "runtime/query_context.h"
-#include "runtime/runtime_profile.h"
 
 namespace doris {
 #include "common/compile_check_begin.h"
+
+// Callback for sync-size RPCs. Handles errors (disable wrapper + sub 
dependency) in call().
+class SyncSizeCallback : public TrivialBrpcCallback<PSendFilterSizeResponse> {
+    ENABLE_FACTORY_CREATOR(SyncSizeCallback);
+
+public:

Review Comment:
   `SyncSizeCallback` derives from `TrivialBrpcCallback`, but this header does 
not include `util/brpc_closure.h` (it only forward-declares 
`TrivialBrpcCallback` in `runtime_filter.h`). This makes the header 
non-self-contained and can fail to compile depending on include order (e.g. 
`runtime_filter_mgr.cpp` includes this header before `util/brpc_closure.h`). 
Add the missing include (or move `SyncSizeCallback` definition to a .cpp that 
includes `util/brpc_closure.h`).



##########
be/src/exec/runtime_filter/runtime_filter_mgr.cpp:
##########
@@ -40,13 +41,42 @@
 #include "runtime/exec_env.h"
 #include "runtime/memory/mem_tracker.h"
 #include "runtime/query_context.h"
+#include "runtime/runtime_profile.h"
 #include "runtime/runtime_state.h"
 #include "runtime/thread_context.h"
 #include "util/brpc_client_cache.h"
 #include "util/brpc_closure.h"
 
 namespace doris {
 #include "common/compile_check_begin.h"
+
+namespace {
+template <typename Response, typename Lock>
+class ManagedTrivialBrpcCallback final : public TrivialBrpcCallback<Response> {
+    ENABLE_FACTORY_CREATOR(ManagedTrivialBrpcCallback);
+
+public:
+    ManagedTrivialBrpcCallback(Lock* lock, std::vector<std::shared_ptr<void>>* 
callbacks,
+                               std::weak_ptr<QueryContext> context = {})
+            : TrivialBrpcCallback<Response>(std::move(context)),
+              _lock(lock),
+              _callbacks(callbacks) {}
+
+    void call() override {
+        TrivialBrpcCallback<Response>::call();
+        std::lock_guard<Lock> l(*_lock);
+        _callbacks->erase(
+                std::remove_if(_callbacks->begin(), _callbacks->end(),
+                               [this](const auto& callback) { return 
callback.get() == this; }),
+                _callbacks->end());
+    }

Review Comment:
   `ManagedTrivialBrpcCallback` stores raw pointers to the owner’s mutex/vector 
(`_lock`, `_callbacks`) and uses them from the brpc completion thread. If the 
owning object is being destroyed concurrently with RPC completion, these 
pointers can dangle (e.g. vector/mutex destruct while `call()` is erasing), 
leading to undefined behavior. Consider moving the lock+vector into a separate 
`shared_ptr` state owned by both the entity and callbacks (or make the callback 
removal conditional on successfully locking a weak_ptr to the owner/state).



##########
be/src/exec/sink/writer/vtablet_writer.cpp:
##########
@@ -94,6 +94,30 @@ class TExpr;
 
 #include "common/compile_check_begin.h"
 
+namespace {
+template <typename Response>
+class ManagedTrivialBrpcCallback final : public TrivialBrpcCallback<Response> {
+    ENABLE_FACTORY_CREATOR(ManagedTrivialBrpcCallback);
+
+public:
+    ManagedTrivialBrpcCallback(std::mutex* lock, 
std::vector<std::shared_ptr<void>>* callbacks)
+            : _lock(lock), _callbacks(callbacks) {}
+
+    void call() override {
+        TrivialBrpcCallback<Response>::call();
+        std::lock_guard<std::mutex> l(*_lock);
+        _callbacks->erase(
+                std::remove_if(_callbacks->begin(), _callbacks->end(),
+                               [this](const auto& callback) { return 
callback.get() == this; }),
+                _callbacks->end());
+    }
+
+private:
+    std::mutex* _lock;
+    std::vector<std::shared_ptr<void>>* _callbacks;

Review Comment:
   This `ManagedTrivialBrpcCallback` stores raw pointers to `VNodeChannel`’s 
`_cancel_callbacks_lock` and `_cancel_callbacks` and uses them asynchronously 
in `call()`. If the `VNodeChannel` is destroyed while the cancel RPC is still 
in flight, `call()` can run with dangling pointers (UB) or race with 
destruction of the vector/mutex. Prefer a `shared_ptr`-owned state object for 
callback bookkeeping (mutex + vector), or ensure the state outlives all 
in-flight cancel callbacks.
   ```suggestion
   struct CancelCallbackState {
       std::mutex lock;
       std::vector<std::shared_ptr<void>> callbacks;
   };
   
   template <typename Response>
   class ManagedTrivialBrpcCallback final : public 
TrivialBrpcCallback<Response> {
       ENABLE_FACTORY_CREATOR(ManagedTrivialBrpcCallback);
   
   public:
       explicit ManagedTrivialBrpcCallback(std::shared_ptr<CancelCallbackState> 
state)
               : _state(std::move(state)) {}
   
       void call() override {
           TrivialBrpcCallback<Response>::call();
           std::lock_guard<std::mutex> l(_state->lock);
           _state->callbacks.erase(
                   std::remove_if(_state->callbacks.begin(), 
_state->callbacks.end(),
                                  [this](const auto& callback) { return 
callback.get() == this; }),
                   _state->callbacks.end());
       }
   
   private:
       std::shared_ptr<CancelCallbackState> _state;
   ```



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