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


##########
be/src/util/proto_util.h:
##########
@@ -36,26 +42,131 @@ namespace doris {
 // 2G: In the default "baidu_std" brpcd, upper limit of the request and 
attachment length is 2G.
 constexpr size_t MIN_HTTP_BRPC_SIZE = (1ULL << 31);
 
-// Embed column_values and brpc request serialization string in controller 
attachment.
+// Attachment layout shared by every http send path:
+//
+//   [int64 request_size][serialized request][int64 payload_size][payload]...
+//
+// The serialized request carries everything but the block payloads 
(PBlock::column_values).
+// One payload section follows per payload block of the request, in block 
order, see
+// payload_blocks(). A request with a single block therefore keeps the layout 
older
+// receivers expect; receivers older than this layout read exactly one payload 
into the
+// singular `block`, so a multi-payload attachment (repeated `blocks`) is only 
understood by
+// receivers that carry it. Such requests could not be sent at all before 
(plain brpc rejected
+// them for exceeding its size limit), so no working mixed-version case is 
lost.
+
+// The blocks whose column_values travel as attachment payloads. A generic 
request carries one
+// block; PTransmitDataParams carries either the singular `block` or the 
repeated `blocks`.
+template <typename Params>
+std::vector<PBlock*> payload_blocks(Params* brpc_request) {
+    return {brpc_request->mutable_block()};
+}
+
+inline std::vector<PBlock*> payload_blocks(PTransmitDataParams* brpc_request) {
+    if (brpc_request->has_block()) {
+        DCHECK(brpc_request->blocks().empty());
+        return {brpc_request->mutable_block()};
+    }
+    std::vector<PBlock*> blocks;
+    blocks.reserve(brpc_request->blocks_size());
+    for (auto& block : *brpc_request->mutable_blocks()) {
+        blocks.push_back(&block);
+    }
+    return blocks;
+}
+
+// Append one `[int64 size][bytes]` section to the attachment.
+inline Status attachment_append_section(butil::IOBuf* attachment, const 
std::string& data) {
+    int64_t data_size = data.size();
+    attachment->append(&data_size, sizeof(data_size));
+    try {
+        attachment->append(data);
+    } catch (...) {
+        LOG(WARNING) << "Try to alloc " << data_size
+                     << " bytes for append data to attachment failed. ";
+        return Status::MemoryAllocFailed("request embed attachment failed to 
memcpy {} bytes",
+                                         data_size);
+    }
+    return Status::OK();
+}
+
+// Embed `brpc_request` followed by `payloads`, one section each, in the 
controller attachment.
+template <typename Params, typename Closure>
+Status request_embed_attachmentv2(Params* brpc_request,
+                                  const std::vector<const std::string*>& 
payloads,
+                                  std::unique_ptr<Closure>& closure) {
+    std::string req_str;
+    if (!brpc_request->SerializeToString(&req_str)) {
+        return Status::InternalError("failed to serialize the request");
+    }
+    butil::IOBuf attachment;
+    RETURN_IF_ERROR(attachment_append_section(&attachment, req_str));
+    for (const auto* payload : payloads) {
+        RETURN_IF_ERROR(attachment_append_section(&attachment, *payload));
+    }
+    closure->cntl_->request_attachment().swap(attachment);
+    return Status::OK();
+}
+
+template <typename Params, typename Closure>
+Status request_embed_attachmentv2(Params* brpc_request, const std::string& 
data,
+                                  std::unique_ptr<Closure>& closure) {
+    return request_embed_attachmentv2(brpc_request, std::vector<const 
std::string*> {&data},
+                                      closure);
+}
+
+// Embed a request that owns its block: column_values is moved into the 
attachment and the
+// request keeps the field present but empty.
 template <typename Params, typename Closure>
 Status request_embed_attachment_contain_blockv2(Params* brpc_request,
                                                 std::unique_ptr<Closure>& 
closure) {
-    std::string column_values = 
std::move(*brpc_request->mutable_block()->mutable_column_values());
-    brpc_request->mutable_block()->mutable_column_values()->clear();
+    auto* block = brpc_request->mutable_block();
+    std::string column_values = std::move(*block->mutable_column_values());
+    block->mutable_column_values()->clear();
     return request_embed_attachmentv2(brpc_request, column_values, closure);
 }
 
+// Embed a request whose blocks only borrow their column_values (see
+// ExchangeSinkBuffer / borrow_block()). Each payload is detached from the 
request while the
+// request is serialized, appended by reference and attached back, so the 
owner of the payload
+// is never read through the request's presence bits nor mutated: the same 
owner may be
+// serialized by other channels at the same time.
+template <typename Closure>
+Status request_embed_attachment_borrowed_blocksv2(PTransmitDataParams* 
brpc_request,
+                                                  std::unique_ptr<Closure>& 
closure) {
+    std::vector<PBlock*> blocks = payload_blocks(brpc_request);
+    std::vector<std::string*> payloads;
+    payloads.reserve(blocks.size());
+    // Registered before any payload is detached: if a placeholder allocation 
below throws, the
+    // payloads detached so far are attached back and the placeholders already 
installed are freed
+    // by set_allocated_column_values().
+    Defer attach_back([&]() {
+        for (size_t i = 0; i < payloads.size(); ++i) {
+            blocks[i]->set_allocated_column_values(payloads[i]);
+        }
+    });
+    for (auto* block : blocks) {
+        payloads.push_back(block->release_column_values());
+        DCHECK(payloads.back() != nullptr);
+        // Keep the field present on the wire: receivers that take the payload 
slot before
+        // parsing the request rely on the serialized field to restore its 
presence bit.
+        block->set_column_values("");
+    }
+    return request_embed_attachmentv2(
+            brpc_request, std::vector<const std::string*>(payloads.begin(), 
payloads.end()),
+            closure);
+}
+
 inline bool enable_http_send_block(const PTransmitDataParams& request) {
     if (!config::transfer_large_data_by_brpc) {
         return false;
     }
-    if (!request.has_block() || !request.block().has_column_values()) {
-        return false;
-    }
-    if (request.ByteSizeLong() < MIN_HTTP_BRPC_SIZE) {
+    if (!request.has_block() && request.blocks().empty()) {

Review Comment:
   [P1] Keep repeated-block HTTP compatible with older receivers
   
   During a rolling upgrade, a new sender with multi-block exchange enabled can 
send a >=2 GiB request through this branch to a BE still running the base 
version. That receiver's `attachment_extract_request_contain_block()` only 
fills the singular `block.column_values`; it parses the repeated `blocks` but 
leaves their payloads empty. `VDataStreamMgr::transmit_block()` then selects 
those repeated blocks and enqueues them. With the default 
`fragment_transmission_compression_codec = 'none'`, `Block::deserialize()` 
passes these empty buffers to unchecked column header decoding 
(`deserialize_const_flag_and_row_num`), causing invalid reads/decoding instead 
of the former transport rejection. Preserve the legacy singular framing, 
negotiate receiver support, or fail before emitting the new shape. This differs 
from the fixed singular presence-bit thread: setting the empty repeated fields 
present does not teach the old receiver to attach their data.



##########
be/src/exec/operator/exchange_sink_buffer.cpp:
##########
@@ -477,35 +490,14 @@ Status ExchangeSinkBuffer::_send_rpc(RpcInstance& 
instance_data) {
                     return !req.block_holder->serialized_block().empty() ||
                            
req.block_holder->get_block()->column_metas().empty();
                 });
-        auto& request = requests[0];
         auto& brpc_request = instance_data.request;
         brpc_request->set_sender_id(channel->_parent->sender_id());
         brpc_request->set_be_number(channel->_parent->be_number());
 
-        if (_send_multi_blocks && !use_shared_protobuf) {
-            for (const auto& req : requests) {
-                if (auto block = req.block_holder->get_block();
-                    block && !block->column_metas().empty()) {
-                    add_borrowed_broadcast_block(*brpc_request, *block);
-                }
-            }
-        } else if (!_send_multi_blocks) {
-            if (request.block_holder->get_block() &&
-                !request.block_holder->get_block()->column_metas().empty()) {
-                
brpc_request->set_allocated_block(request.block_holder->get_block());
-            }
-        }
-        auto detach_blocks = [&]() {
-            if (!_send_multi_blocks && request.block_holder->get_block()) {
-                static_cast<void>(brpc_request->release_block());
-            } else {
-                for (int i = 0; i < brpc_request->mutable_blocks()->size(); 
++i) {
-                    
static_cast<void>(brpc_request->mutable_blocks(i)->release_column_values());
-                }
-                brpc_request->clear_blocks();
-            }
-        };
-        Defer release_block(detach_blocks);
+        Defer release_blocks([&]() { 
release_borrowed_blocks(brpc_request.get()); });
+        borrow_blocks(

Review Comment:
   [P2] Skip rebuilding blocks for cached broadcast sends
   
   When `_send_multi_blocks` and `use_shared_protobuf` are true, this new 
unconditional call deep-copies every block's recursive `column_metas` into a 
request for each destination. For normal requests below 2 GiB, the cached 
branch immediately clears these blocks and sends the pre-serialized holder 
fragments instead. The previous branch skipped construction in this case. A 
wide/nested-schema broadcast with many remote destinations and small batches 
therefore adds a full schema-copy pass per destination under the RPC mutex for 
metadata that is never sent. Compute the HTTP decision from the routing header 
and cached fragment sizes, and construct borrowed blocks only for HTTP or the 
uncached fallback. This is separate from the earlier metadata-ownership 
simplification: no request uses these copies.



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