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


##########
be/src/exec/operator/exchange_sink_buffer.cpp:
##########
@@ -49,6 +49,52 @@
 
 namespace doris {
 
+void borrow_block(PBlock* src, PBlock* dst, BlockSource source) {
+    if (source == BlockSource::OWNED) {
+        // O(1) pointer swap; the owner is destroyed right after the send and 
takes the request's
+        // cleared metas with it.
+        dst->mutable_column_metas()->Swap(src->mutable_column_metas());
+    } else {
+        dst->mutable_column_metas()->CopyFrom(src->column_metas());
+    }
+    // set_allocated_column_values() stores the owner's std::string pointer as 
is; the thirdparty
+    // protobuf is built without NDEBUG checks, otherwise 
ArenaStringPtr::SetAllocated would move the
+    // string into a fresh copy and delete the owner's one.
+    dst->set_be_exec_version(src->be_exec_version());
+    dst->set_compressed(src->compressed());
+    dst->set_compression_type(src->compression_type());
+    dst->set_uncompressed_size(src->uncompressed_size());
+    dst->set_allocated_column_values(src->mutable_column_values());

Review Comment:
   [P2] Keep shared-source lookup free of mutable protobuf access
   
   With `exchange_multi_blocks_byte_size = -1`, an unpartitioned exchange can 
queue one `BroadcastPBlockHolder` for two busy destinations. Their previous RPC 
completions then call `_send_rpc()` concurrently under different `RpcInstance` 
mutexes and both reach this call. In pinned protobuf 21.11, the [mutable string 
accessor](https://github.com/protocolbuffers/protobuf/blob/v21.11/src/google/protobuf/compiler/cpp/string_field.cc#L243-L275)
 always [writes the non-atomic presence 
bits](https://github.com/protocolbuffers/protobuf/blob/v21.11/src/google/protobuf/compiler/cpp/field.cc#L269-L279),
 even when the payload is already present, producing a data race on the shared 
owner. This is distinct from the earlier HTTP release/restore thread: that 
payload mutation is fixed, but the old single-block branch only borrowed the 
whole `PBlock`, and this helper adds source mutation even for small ordinary 
RPCs. Obtain the already-initialized shared payload through the const 
`column_values()` acces
 sor and keep mutable protobuf operations on the destination request.



##########
be/src/util/proto_util.h:
##########
@@ -36,26 +42,128 @@ 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());
+    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("");
+    }
+    Defer attach_back([&]() {

Review Comment:
   [P3] Install the restoration guard before creating placeholders
   
   `release_column_values()` resets each request slot to its default, so the 
following `set_column_values("")` [allocates a new empty string 
object](https://github.com/protocolbuffers/protobuf/blob/v21.11/src/google/protobuf/arenastring.cc#L99-L128).
 In a multi-block HTTP batch, if the first placeholder succeeds and a later 
allocation throws, this `attach_back` guard has not been constructed. The 
enclosing `_send_rpc()` guard then calls `release_borrowed_blocks()`, which 
releases and discards the earlier request-owned placeholder as though it were a 
borrowed owner string. The original payloads remain safe, but those empty 
string objects have no remaining owner and leak during unwind. Register 
restoration before the loop and restore the payloads already detached if a 
later placeholder allocation fails.



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