This is an automated email from the ASF dual-hosted git repository.
Mryange pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 11a8d6217cd [improvement](be) Share encoded protobuf blocks across
broadcast RPCs (#68961)
11a8d6217cd is described below
commit 11a8d6217cd912fe484586911df3e55458b1e75d
Author: HappenLee <[email protected]>
AuthorDate: Wed Oct 14 16:24:41 2026 +0800
[improvement](be) Share encoded protobuf blocks across broadcast RPCs
(#68961)
### What problem does this PR solve?
Related PR: #63258
Remote broadcast channels share the same `PBlock`, but brpc normally
serializes its large `column_values` into a separate request buffer for
every destination. Broadcasting an S-byte block to N destinations
therefore copies roughly N*S bytes while preparing the protobuf
requests.
Encode the `block` or repeated `blocks` field once in
`BroadcastPBlockHolder`, before publishing it to the destination queues.
Each RPC serializes its routing fields and appends references to the
cached encoding through `brpc::SerializedRequest`. The receiver
continues to parse the existing `PTransmitDataParams` wire format. Queue
order, packet sequences, EOS, and per-RPC callback ownership are
preserved.
The original PBlock remains available to the existing send paths. Queue
admission accounts for both it and the cache once per holder.
Single-destination broadcasts and oversized fragments retain the
original path; assembled requests retain required-field validation and
the protobuf size limit. The HTTP borrowed-data fix remains in #63258.
The earlier standalone prototype measured packing CPU at 1 MiB x 8
destinations dropping from 3370.81 us to 402.43 us. This is motivation
for the change, not an end-to-end measurement of this production patch.
Network bytes and receiver processing are unchanged.
### Release note
Reduce repeated protobuf serialization and payload copies when
broadcasting exchange blocks to multiple remote destinations.
### Check List (For Author)
- Test
- [x] Unit Test: `broadcast_protobuf_test.cpp` covers wire equivalence,
shared backing buffers, batching/EOS, the actual send branch, queue
accounting, size limits, and asynchronous RPC completion/cancellation.
- ASAN: `ENABLE_PCH=OFF ENABLE_UNITY_BUILD=ON ./run-be-ut.sh -j 48 --run
--filter='*BroadcastProtobufTest*:*ExchangeSinkOperatorTest*:*ExchangeSinkTest*'`
— 24 tests passed (14 new and 10 existing).
- `build-support/clang-format.sh` / `build-support/check-format.sh` with
clang-format 16.0.6; `build-support/check-build-hygiene.sh` and `git
diff --check` passed.
- `build-support/run-clang-tidy.sh`: all changed production lines and
the entire new test file passed. The local LLVM 20 package needed an
explicit resource-directory argument.
- Behavior changed:
- [x] No. SQL results and the exchange wire protocol are unchanged.
- Does this need documentation?
- [x] No.
### Check List (For Reviewer who merge this PR)
- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label
---
be/src/exec/operator/exchange_sink_buffer.cpp | 143 +++++--
be/src/exec/operator/exchange_sink_buffer.h | 19 +
be/src/exec/operator/exchange_sink_operator.cpp | 3 +
be/src/util/proto_util.h | 12 +
be/test/exec/operator/broadcast_protobuf_test.cpp | 464 ++++++++++++++++++++++
5 files changed, 618 insertions(+), 23 deletions(-)
diff --git a/be/src/exec/operator/exchange_sink_buffer.cpp
b/be/src/exec/operator/exchange_sink_buffer.cpp
index f6938a5eccd..4db029bfad5 100644
--- a/be/src/exec/operator/exchange_sink_buffer.cpp
+++ b/be/src/exec/operator/exchange_sink_buffer.cpp
@@ -18,6 +18,7 @@
#include "exec/operator/exchange_sink_buffer.h"
#include <brpc/controller.h>
+#include <brpc/serialized_request.h>
#include <butil/errno.h>
#include <butil/iobuf_inl.h>
#include <fmt/format.h>
@@ -28,6 +29,7 @@
#include <pdqsort.h>
#include <stddef.h>
+#include <algorithm>
#include <atomic>
#include <cstdint>
#include <exception>
@@ -49,6 +51,54 @@
namespace doris {
+namespace {
+
+void add_borrowed_broadcast_block(PTransmitDataParams& request, PBlock&
source) {
+ auto* block = request.add_blocks();
+ block->mutable_column_metas()->CopyFrom(source.column_metas());
+ block->set_be_exec_version(source.be_exec_version());
+ block->set_compressed(source.compressed());
+ block->set_compression_type(source.compression_type());
+ block->set_uncompressed_size(source.uncompressed_size());
+ block->set_allocated_column_values(source.mutable_column_values());
+}
+
+} // namespace
+
+Status BroadcastPBlockHolder::prepare_serialized_block(bool send_multi_blocks,
+ size_t num_receivers) {
+ DCHECK(_serialized_block.empty());
+ if (num_receivers <= 1 || _pblock->column_metas().empty()) {
+ return Status::OK();
+ }
+ PTransmitDataParams fragment;
+ if (send_multi_blocks) {
+ // Match the existing multi-block request's metadata and proto2 field
presence.
+ add_borrowed_broadcast_block(fragment, *_pblock);
+ } else {
+ fragment.set_allocated_block(_pblock.get());
+ }
+ Defer detach([&] {
+ if (send_multi_blocks) {
+
static_cast<void>(fragment.mutable_blocks(0)->release_column_values());
+ } else {
+ static_cast<void>(fragment.release_block());
+ }
+ });
+ // Protobuf cannot encode >= 2 GiB messages. Leave these on the existing
path,
+ // where the complete request is checked for HTTP attachment transport.
+ if (fragment.ByteSizeLong() >= MIN_HTTP_BRPC_SIZE) {
+ return Status::OK();
+ }
+ butil::IOBufAsZeroCopyOutputStream stream(&_serialized_block);
+ // Routing fields are intentionally absent from this reusable message
fragment.
+ // PBlock and its nested metadata contain no required fields.
+ if (!fragment.SerializePartialToZeroCopyStream(&stream)) {
+ return Status::InternalError("Failed to serialize broadcast block");
+ }
+ return Status::OK();
+}
+
BroadcastPBlockHolder::~BroadcastPBlockHolder() {
// lock the parent queue, if the queue could lock success, then return the
block
// to the queue, to reuse the block
@@ -64,8 +114,9 @@ void
BroadcastPBlockHolderMemLimiter::acquire(BroadcastPBlockHolder& holder) {
std::unique_lock l(_holders_lock);
DCHECK(_broadcast_dependency != nullptr);
holder.set_parent_creator(shared_from_this());
- auto size = holder._pblock->column_values().size();
- _total_queue_buffer_size += size;
+ holder._queue_charge_bytes =
+ holder._pblock->column_values().size() +
holder._serialized_block.size();
+ _total_queue_buffer_size += holder._queue_charge_bytes;
_total_queue_blocks_count++;
if (_total_queue_buffer_size >= _total_queue_buffer_size_limit ||
_total_queue_blocks_count >= _total_queue_blocks_count_limit) {
@@ -76,8 +127,7 @@ void
BroadcastPBlockHolderMemLimiter::acquire(BroadcastPBlockHolder& holder) {
void BroadcastPBlockHolderMemLimiter::release(const BroadcastPBlockHolder&
holder) {
std::unique_lock l(_holders_lock);
DCHECK(_broadcast_dependency != nullptr);
- auto size = holder._pblock->column_values().size();
- _total_queue_buffer_size -= size;
+ _total_queue_buffer_size -= holder._queue_charge_bytes;
_total_queue_blocks_count--;
if (_total_queue_buffer_size <= 0) {
_broadcast_dependency->set_ready();
@@ -418,34 +468,34 @@ Status ExchangeSinkBuffer::_send_rpc(RpcInstance&
instance_data) {
}
}
+ const bool use_shared_protobuf =
+ std::ranges::any_of(requests,
+ [](const auto& req) {
+ return
!req.block_holder->serialized_block().empty();
+ }) &&
+ std::ranges::all_of(requests, [](const auto& req) {
+ 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) {
- for (int i = 0; i < requests.size(); i++) {
- auto& req = requests[i];
+ 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()) {
- auto add_block = brpc_request->add_blocks();
- for (int j = 0; j < block->column_metas_size(); ++j) {
-
add_block->add_column_metas()->CopyFrom(block->column_metas(j));
- }
- add_block->set_be_exec_version(block->be_exec_version());
- add_block->set_compressed(block->compressed());
- add_block->set_compression_type(block->compression_type());
-
add_block->set_uncompressed_size(block->uncompressed_size());
-
add_block->set_allocated_column_values(block->mutable_column_values());
+ add_borrowed_broadcast_block(*brpc_request, *block);
}
}
- } else {
+ } 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());
}
}
- Defer release_block([&]() {
+ auto detach_blocks = [&]() {
if (!_send_multi_blocks && request.block_holder->get_block()) {
static_cast<void>(brpc_request->release_block());
} else {
@@ -454,7 +504,8 @@ Status ExchangeSinkBuffer::_send_rpc(RpcInstance&
instance_data) {
}
brpc_request->clear_blocks();
}
- });
+ };
+ Defer release_block(detach_blocks);
instance_data.seq += requests.size();
brpc_request->set_packet_seq(instance_data.seq);
brpc_request->set_eos(requests.back().eos);
@@ -510,15 +561,30 @@ Status ExchangeSinkBuffer::_send_rpc(RpcInstance&
instance_data) {
}
});
{
- auto send_remote_block_closure = AutoReleaseClosure<
- PTransmitDataParams,
-
ExchangeSendCallback<PTransmitDataResult>>::create_unique(brpc_request,
-
send_callback);
if (enable_http_send_block(*brpc_request)) {
+ auto send_remote_block_closure = AutoReleaseClosure<
+ PTransmitDataParams,
+
ExchangeSendCallback<PTransmitDataResult>>::create_unique(brpc_request,
+
send_callback);
RETURN_IF_ERROR(transmit_block_httpv2(_context->exec_env(),
std::move(send_remote_block_closure),
channel->_brpc_dest_addr));
+ } else if (use_shared_protobuf) {
+ // The HTTP decision above needs the complete singular
request. Detach its
+ // borrowed block before encoding only the per-RPC routing
fields.
+ detach_blocks();
+ auto serialized_request =
std::make_shared<brpc::SerializedRequest>();
+ RETURN_IF_ERROR(_serialize_broadcast_request(*brpc_request,
requests,
+
serialized_request.get()));
+ auto closure = AutoReleaseClosure<brpc::SerializedRequest,
+
ExchangeSendCallback<PTransmitDataResult>>::
+ create_unique(serialized_request, send_callback);
+ transmit_serialized_block(channel->_brpc_stub.get(),
std::move(closure));
} else {
+ auto send_remote_block_closure = AutoReleaseClosure<
+ PTransmitDataParams,
+
ExchangeSendCallback<PTransmitDataResult>>::create_unique(brpc_request,
+
send_callback);
transmit_blockv2(channel->_brpc_stub.get(),
std::move(send_remote_block_closure));
}
}
@@ -529,6 +595,37 @@ Status ExchangeSinkBuffer::_send_rpc(RpcInstance&
instance_data) {
return Status::OK();
}
+Status ExchangeSinkBuffer::_serialize_broadcast_request(
+ const PTransmitDataParams& header, const
std::vector<BroadcastTransmitInfo>& requests,
+ brpc::SerializedRequest* serialized_request) {
+ DCHECK(!header.has_block());
+ DCHECK_EQ(header.blocks_size(), 0);
+ if (!header.IsInitialized()) {
+ return Status::InternalError("Uninitialized broadcast request header:
{}",
+ header.InitializationErrorString());
+ }
+ size_t size = header.ByteSizeLong();
+ for (const auto& request : requests) {
+ size += request.block_holder->serialized_block().size();
+ }
+ // SerializedRequest bypasses protobuf's normal size check. Preserve that
limit
+ // even when a multi-block batch consists of individually encodable
fragments.
+ if (size >= MIN_HTTP_BRPC_SIZE) {
+ return Status::InternalError("Broadcast protobuf request exceeds 2
GiB: {} bytes", size);
+ }
+ auto& data = serialized_request->serialized_data();
+ {
+ butil::IOBufAsZeroCopyOutputStream stream(&data);
+ if (!header.SerializeToZeroCopyStream(&stream)) {
+ return Status::InternalError("Failed to serialize broadcast
request header");
+ }
+ }
+ for (const auto& request : requests) {
+ data.append(request.block_holder->serialized_block());
+ }
+ return Status::OK();
+}
+
void ExchangeSinkBuffer::_ended(RpcInstance& ins) {
std::unique_lock<std::mutex> lock(*ins.mutex);
ins.running_sink_count--;
diff --git a/be/src/exec/operator/exchange_sink_buffer.h
b/be/src/exec/operator/exchange_sink_buffer.h
index 7478c63f4e8..2942fb04f57 100644
--- a/be/src/exec/operator/exchange_sink_buffer.h
+++ b/be/src/exec/operator/exchange_sink_buffer.h
@@ -18,6 +18,7 @@
#pragma once
#include <brpc/controller.h>
+#include <butil/iobuf.h>
#include <gen_cpp/data.pb.h>
#include <gen_cpp/internal_service.pb.h>
#include <gen_cpp/types.pb.h>
@@ -38,6 +39,10 @@
#include "service/backend_options.h"
#include "util/brpc_closure.h"
+namespace brpc {
+class SerializedRequest;
+}
+
namespace doris {
class PTransmitDataParams;
class TUniqueId;
@@ -64,9 +69,18 @@ public:
void reset_block() { _pblock->Clear(); }
+ // Called once by the producer, before publishing the holder to any
channel.
+ Status prepare_serialized_block(bool send_multi_blocks, size_t
num_receivers);
+
+ const butil::IOBuf& serialized_block() const { return _serialized_block; }
+
private:
friend class BroadcastPBlockHolderMemLimiter;
std::unique_ptr<PBlock> _pblock;
+ // Includes the outer PTransmitDataParams.block/blocks field tag and
length.
+ // Keep _pblock for the existing protobuf and HTTP paths.
+ butil::IOBuf _serialized_block;
+ int64_t _queue_charge_bytes = 0;
std::weak_ptr<BroadcastPBlockHolderMemLimiter> _parent_creator;
void set_parent_creator(std::shared_ptr<BroadcastPBlockHolderMemLimiter>
parent_creator) {
_parent_creator = parent_creator;
@@ -271,6 +285,7 @@ public:
Status add_block(Channel* channel, TransmitInfo&& request);
Status add_block(Channel* channel, BroadcastTransmitInfo&& request);
void close();
+ bool send_multi_blocks() const { return _send_multi_blocks; }
void update_rpc_time(RpcInstance& ins, int64_t start_rpc_time, int64_t
receive_rpc_time);
void update_profile(RuntimeProfile* profile);
@@ -308,6 +323,10 @@ private:
Status _send_rpc(RpcInstance& ins);
+ static Status _serialize_broadcast_request(const PTransmitDataParams&
header,
+ const
std::vector<BroadcastTransmitInfo>& requests,
+ brpc::SerializedRequest*
serialized_request);
+
#ifndef BE_TEST
inline void _ended(RpcInstance& ins);
inline void _failed(InstanceLoId id, const std::string& err);
diff --git a/be/src/exec/operator/exchange_sink_operator.cpp
b/be/src/exec/operator/exchange_sink_operator.cpp
index a4b3e682ebd..c4c6b6be830 100644
--- a/be/src/exec/operator/exchange_sink_operator.cpp
+++ b/be/src/exec/operator/exchange_sink_operator.cpp
@@ -516,6 +516,9 @@ Status ExchangeSinkOperatorX::sink_impl(RuntimeState*
state, Block* block, bool
block_holder->reset_block();
}
+ RETURN_IF_ERROR(block_holder->prepare_serialized_block(
+ local_state._sink_buffer->send_multi_blocks(),
+ local_state._rpc_channels_num));
local_state._broadcast_pb_mem_limiter->acquire(*block_holder);
size_t idx = 0;
diff --git a/be/src/util/proto_util.h b/be/src/util/proto_util.h
index aec345688a9..a03024f4684 100644
--- a/be/src/util/proto_util.h
+++ b/be/src/util/proto_util.h
@@ -66,6 +66,18 @@ void transmit_blockv2(PBackendService_Stub* stub,
std::unique_ptr<Closure> closu
closure.release();
}
+template <typename Closure>
+void transmit_serialized_block(PBackendService_Stub* stub,
std::unique_ptr<Closure> closure) {
+ // SerializedRequest bypasses brpc's protobuf serializer, including
request compression.
+ // Exchange compresses column_values before preparing the shared protobuf
fragment.
+ DCHECK_EQ(closure->cntl_->request_compress_type(),
brpc::COMPRESS_TYPE_NONE);
+ static const auto* method =
PBackendService::descriptor()->FindMethodByName("transmit_block");
+ closure->cntl_->http_request().Clear();
+ stub->channel()->CallMethod(method, closure->cntl_.get(),
closure->request_.get(),
+ closure->response_.get(), closure.get());
+ closure.release();
+}
+
template <typename Closure>
Status transmit_block_httpv2(ExecEnv* exec_env, std::unique_ptr<Closure>
closure,
TNetworkAddress brpc_dest_addr) {
diff --git a/be/test/exec/operator/broadcast_protobuf_test.cpp
b/be/test/exec/operator/broadcast_protobuf_test.cpp
new file mode 100644
index 00000000000..da593d5ea38
--- /dev/null
+++ b/be/test/exec/operator/broadcast_protobuf_test.cpp
@@ -0,0 +1,464 @@
+// 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/channel.h>
+#include <brpc/closure_guard.h>
+#include <brpc/protocol.h>
+#include <brpc/serialized_request.h>
+#include <brpc/server.h>
+#include <bthread/countdown_event.h>
+#include <gtest/gtest.h>
+
+#include <atomic>
+#include <memory>
+#include <string>
+#include <thread>
+#include <vector>
+
+#include "exec/exchange/vdata_stream_sender.h"
+#include "exec/operator/exchange_sink_buffer.h"
+#include "exec/operator/exchange_sink_operator.h"
+#include "exec/pipeline/dependency.h"
+#include "runtime/thread_context.h"
+#include "testutil/mock/mock_runtime_state.h"
+#include "util/debug/leakcheck_disabler.h"
+#include "util/defer_op.h"
+#include "util/proto_util.h"
+
+namespace doris {
+namespace {
+
+PTransmitDataParams make_header(int64_t destination = 1, int64_t sequence = 1,
bool eos = false) {
+ PTransmitDataParams request;
+ request.mutable_finst_id()->set_hi(11);
+ request.mutable_finst_id()->set_lo(destination);
+ request.mutable_query_id()->set_hi(21);
+ request.mutable_query_id()->set_lo(22);
+ request.set_node_id(3);
+ request.set_sender_id(4);
+ request.set_be_number(5);
+ request.set_packet_seq(sequence);
+ request.set_eos(eos);
+ request.mutable_query_statistics()->set_scan_rows(42);
+ request.mutable_exec_status()->set_status_code(0);
+ request.set_transfer_by_attachment(false);
+ return request;
+}
+
+std::shared_ptr<BroadcastPBlockHolder> make_holder(char value = 'a') {
+ auto holder = BroadcastPBlockHolder::create_shared();
+ auto* block = holder->get_block();
+ auto* meta = block->add_column_metas();
+ meta->set_name("values");
+ meta->set_type(PGenericType::STRING);
+ meta->set_is_nullable(true);
+ block->set_column_values(std::string(64 * 1024, value));
+ block->set_compressed(true);
+ block->set_compression_type(segment_v2::LZ4);
+ block->set_uncompressed_size(128 * 1024);
+ block->set_be_exec_version(7);
+ return holder;
+}
+
+PTransmitDataParams parse(const butil::IOBuf& data) {
+ PTransmitDataParams parsed;
+ butil::IOBufAsZeroCopyInputStream stream(data);
+ EXPECT_TRUE(parsed.ParseFromZeroCopyStream(&stream));
+ return parsed;
+}
+
+class BroadcastProtobufTest : public testing::TestWithParam<bool> {};
+
+class CapturingRpcChannel : public google::protobuf::RpcChannel {
+public:
+ ~CapturingRpcChannel() override {
+ if (completion != nullptr) {
+ completion->Run();
+ }
+ }
+
+ butil::IOBuf wire;
+ google::protobuf::Closure* completion = nullptr;
+
+ void CallMethod(const google::protobuf::MethodDescriptor* method,
+ google::protobuf::RpcController* controller,
+ const google::protobuf::Message* request,
google::protobuf::Message* response,
+ google::protobuf::Closure* done) override {
+ EXPECT_EQ(method,
PBackendService::descriptor()->FindMethodByName("transmit_block"));
+ EXPECT_EQ(request->GetDescriptor(),
brpc::SerializedRequest::descriptor());
+ brpc::SerializeRequestDefault(&wire,
static_cast<brpc::Controller*>(controller), request);
+ completion = done;
+ }
+};
+
+void expect_broadcast_packet(const PTransmitDataParams& packet, const
RpcInstance& instance,
+ bool multi, int sent, bool eos) {
+ EXPECT_EQ(packet.packet_seq(), multi ? 3 : sent);
+ EXPECT_EQ(packet.eos(), eos);
+ EXPECT_EQ(packet.finst_id().lo(), instance.id);
+ ASSERT_EQ(packet.blocks_size(), multi ? 2 : 0);
+ const auto& block = multi ? packet.blocks(0) : packet.block();
+ EXPECT_EQ(block.column_values(), std::string(64 * 1024, multi ? 'a' : 'a'
+ sent - 1));
+ EXPECT_FALSE(instance.request->has_block());
+ EXPECT_EQ(instance.request->blocks_size(), 0);
+}
+
+TEST_P(BroadcastProtobufTest,
SendRpcPreservesBatchSequenceEosAndRequestTemplate) {
+ auto state = std::make_shared<MockRuntimeState>();
+ ExchangeSinkBuffer buffer(state.get(), 1);
+ buffer._query_id = make_header().query_id();
+ buffer._dest_node_id = 3;
+ buffer._send_multi_blocks = GetParam();
+ buffer._send_multi_blocks_byte_size = 1024 * 1024;
+ TUniqueId destination;
+ destination.__set_hi(11);
+ destination.__set_lo(1);
+ buffer.construct_request(destination);
+ auto sink = ExchangeSinkLocalState::create_shared(state.get());
+ CapturingRpcChannel transport;
+ Channel channel(sink.get(), TNetworkAddress {}, destination, 0);
+ channel._brpc_stub = std::make_shared<PBackendService_Stub>(&transport);
+ auto& instance = *buffer._rpc_instances.at(destination.lo);
+ auto& queue = instance.broadcast_package_queue[&channel];
+ for (int i = 0; i < 2; ++i) {
+ auto holder = make_holder('a' + i);
+ ASSERT_TRUE(holder->prepare_serialized_block(GetParam(), 2).ok());
+ queue.push({.block_holder = holder, .eos = !GetParam() && i == 1});
+ }
+ if (GetParam()) {
+ queue.push({.block_holder = BroadcastPBlockHolder::create_shared(),
.eos = true});
+ }
+ int sent = 0;
+ while (!queue.empty()) {
+ ASSERT_TRUE(buffer._send_rpc(instance).ok());
+ ASSERT_NE(transport.completion, nullptr);
+ ++sent;
+ expect_broadcast_packet(parse(transport.wire), instance, GetParam(),
sent, queue.empty());
+ // We drive the next send explicitly; expire the callback before
completing this RPC.
+ channel._send_callback.reset();
+ transport.completion->Run();
+ transport.completion = nullptr;
+ transport.wire.clear();
+ }
+}
+
+void expect_shared_backing_blocks(const PTransmitDataParams& header,
+ const brpc::SerializedRequest& request,
+ const BroadcastPBlockHolder& holder) {
+ brpc::Controller controller;
+ butil::IOBuf wire;
+ brpc::SerializeRequestDefault(&wire, &controller, &request);
+ ASSERT_FALSE(controller.Failed());
+ wire.pop_front(header.ByteSizeLong());
+ ASSERT_EQ(wire.backing_block_num(),
holder.serialized_block().backing_block_num());
+ for (size_t i = 0; i < wire.backing_block_num(); ++i) {
+ EXPECT_EQ(wire.backing_block(i).data(),
holder.serialized_block().backing_block(i).data());
+ EXPECT_EQ(wire.backing_block(i).size(),
holder.serialized_block().backing_block(i).size());
+ }
+}
+
+TEST_P(BroadcastProtobufTest,
MatchesExistingWireMessageAndSharesBackingBlocks) {
+ auto holder = make_holder();
+ const auto original = holder->get_block()->SerializeAsString();
+ ASSERT_TRUE(holder->prepare_serialized_block(GetParam(), 8).ok());
+ ASSERT_FALSE(holder->serialized_block().empty());
+
+ for (int destination = 1; destination <= 8; ++destination) {
+ auto header = make_header(destination, destination + 100, destination
== 8);
+ brpc::SerializedRequest request;
+ ASSERT_TRUE(ExchangeSinkBuffer::_serialize_broadcast_request(
+ header, {{.block_holder = holder, .eos = false}},
&request)
+ .ok());
+ auto expected = header;
+ auto* block = GetParam() ? expected.add_blocks() :
expected.mutable_block();
+ block->CopyFrom(*holder->get_block());
+ EXPECT_EQ(parse(request.serialized_data()).SerializeAsString(),
+ expected.SerializeAsString());
+
+ // Exercise the actual brpc serializer: it must retain the same
backing data,
+ // rather than encoding the payload again for this destination.
+ expect_shared_backing_blocks(header, request, *holder);
+ }
+ EXPECT_EQ(holder->get_block()->SerializeAsString(), original);
+}
+
+TEST_P(BroadcastProtobufTest, ConcurrentDestinationsAndHolderRelease) {
+ auto holder = make_holder();
+ ASSERT_TRUE(holder->prepare_serialized_block(GetParam(), 8).ok());
+ std::weak_ptr<BroadcastPBlockHolder> weak_holder = holder;
+ std::vector<brpc::SerializedRequest> requests(8);
+ std::vector<std::thread> senders;
+ for (int i = 0; i < 8; ++i) {
+ senders.emplace_back([&, i, shared_holder = holder] {
+ SCOPED_INIT_THREAD_CONTEXT();
+ EXPECT_TRUE(ExchangeSinkBuffer::_serialize_broadcast_request(
+ make_header(i + 1, i + 10),
+ {{.block_holder = shared_holder, .eos =
false}}, &requests[i])
+ .ok());
+ });
+ }
+ for (auto& sender : senders) {
+ sender.join();
+ }
+ holder.reset();
+ ASSERT_TRUE(weak_holder.expired());
+ for (int i = 0; i < 8; ++i) {
+ auto parsed = parse(requests[i].serialized_data());
+ EXPECT_EQ(parsed.finst_id().lo(), i + 1);
+ EXPECT_EQ(parsed.packet_seq(), i + 10);
+ const auto& block = GetParam() ? parsed.blocks(0) : parsed.block();
+ EXPECT_EQ(block.column_values(), std::string(64 * 1024, 'a'));
+ }
+}
+
+TEST(BroadcastProtobufTest, RepeatedBlockPreservesLegacyDefaultFieldPresence) {
+ auto holder = make_holder();
+ holder->get_block()->clear_compressed();
+ holder->get_block()->clear_uncompressed_size();
+ holder->get_block()->clear_compression_type();
+ ASSERT_TRUE(holder->prepare_serialized_block(true, 2).ok());
+ brpc::SerializedRequest request;
+ ASSERT_TRUE(ExchangeSinkBuffer::_serialize_broadcast_request(
+ make_header(), {{.block_holder = holder, .eos =
false}}, &request)
+ .ok());
+ const auto parsed = parse(request.serialized_data());
+ ASSERT_EQ(parsed.blocks_size(), 1);
+ EXPECT_TRUE(parsed.blocks(0).has_compressed());
+ EXPECT_FALSE(parsed.blocks(0).compressed());
+ EXPECT_TRUE(parsed.blocks(0).has_uncompressed_size());
+ EXPECT_EQ(parsed.blocks(0).uncompressed_size(),
holder->get_block()->uncompressed_size());
+ EXPECT_TRUE(parsed.blocks(0).has_compression_type());
+ EXPECT_EQ(parsed.blocks(0).compression_type(),
holder->get_block()->compression_type());
+ EXPECT_FALSE(holder->get_block()->has_compressed());
+ EXPECT_FALSE(holder->get_block()->has_uncompressed_size());
+ EXPECT_FALSE(holder->get_block()->has_compression_type());
+}
+
+TEST(BroadcastProtobufTest, DifferentBatchBoundariesAndEmptyEos) {
+ auto first = make_holder('a');
+ auto second = make_holder('b');
+ auto eos = BroadcastPBlockHolder::create_shared();
+ for (const auto& holder : {first, second, eos}) {
+ ASSERT_TRUE(holder->prepare_serialized_block(true, 2).ok());
+ }
+ ASSERT_TRUE(eos->serialized_block().empty());
+
+ const std::vector<std::vector<BroadcastTransmitInfo>> batches = {
+ {{.block_holder = first, .eos = false},
+ {.block_holder = second, .eos = false},
+ {.block_holder = eos, .eos = true}},
+ {{.block_holder = first, .eos = false}},
+ {{.block_holder = second, .eos = false}, {.block_holder = eos,
.eos = true}},
+ {{.block_holder = eos, .eos = true}}};
+ const int64_t sequences[] = {3, 1, 3, 4};
+ const int expected_blocks[] = {2, 1, 1, 0};
+ for (size_t i = 0; i < batches.size(); ++i) {
+ brpc::SerializedRequest request;
+ auto header = make_header(i + 1, sequences[i], batches[i].back().eos);
+ ASSERT_TRUE(ExchangeSinkBuffer::_serialize_broadcast_request(header,
batches[i], &request)
+ .ok());
+ auto parsed = parse(request.serialized_data());
+ EXPECT_EQ(parsed.packet_seq(), sequences[i]);
+ EXPECT_EQ(parsed.eos(), batches[i].back().eos);
+ ASSERT_EQ(parsed.blocks_size(), expected_blocks[i]);
+ for (int j = 0; j < parsed.blocks_size(); ++j) {
+ EXPECT_EQ(parsed.blocks(j).SerializeAsString(),
+
batches[i][j].block_holder->get_block()->SerializeAsString());
+ }
+ }
+}
+
+TEST(BroadcastProtobufTest, SingleReceiverKeepsOriginalPath) {
+ for (bool multi : {false, true}) {
+ auto holder = make_holder();
+ const auto original = holder->get_block()->SerializeAsString();
+ ASSERT_TRUE(holder->prepare_serialized_block(multi, 1).ok());
+ EXPECT_TRUE(holder->serialized_block().empty());
+ EXPECT_EQ(holder->get_block()->SerializeAsString(), original);
+ }
+}
+
+TEST(BroadcastProtobufTest,
RejectsMissingRequiredFieldsBeforeSerializedRequestBypassesValidation) {
+ auto valid_holder = make_holder();
+ ASSERT_TRUE(valid_holder->prepare_serialized_block(false, 2).ok());
+ auto header = make_header();
+ header.clear_packet_seq();
+ brpc::SerializedRequest request;
+ EXPECT_FALSE(ExchangeSinkBuffer::_serialize_broadcast_request(
+ header, {{.block_holder = valid_holder, .eos =
false}}, &request)
+ .ok());
+ EXPECT_TRUE(request.serialized_data().empty());
+}
+
+TEST(BroadcastProtobufTest,
QueueAccountsSharedCacheOnceAndReleasesAdmissionCharge) {
+ auto dependency = Dependency::create_shared(1, 1, "broadcast", true);
+ auto limiter = BroadcastPBlockHolderMemLimiter::create_shared(dependency);
+ auto holder = make_holder();
+ ASSERT_TRUE(holder->prepare_serialized_block(false, 8).ok());
+ const auto charge =
+ holder->get_block()->column_values().size() +
holder->serialized_block().size();
+ limiter->_total_queue_buffer_size_limit = charge;
+ limiter->acquire(*holder);
+ EXPECT_EQ(limiter->_total_queue_buffer_size, charge);
+ EXPECT_EQ(limiter->_total_queue_blocks_count, 1);
+ EXPECT_EQ(dependency->is_blocked_by(nullptr), dependency.get());
+
+ std::vector<std::shared_ptr<BroadcastPBlockHolder>> channels(8, holder);
+ holder.reset();
+ EXPECT_EQ(limiter->_total_queue_buffer_size, charge);
+ // Release must subtract the admission charge even if the original HTTP
path
+ // moves column_values out of the PBlock before the holder is destroyed.
+ channels.front()->get_block()->clear_column_values();
+ channels.clear();
+ EXPECT_EQ(limiter->_total_queue_buffer_size, 0);
+ EXPECT_EQ(limiter->_total_queue_blocks_count, 0);
+ EXPECT_EQ(dependency->is_blocked_by(nullptr), nullptr);
+}
+
+TEST(BroadcastProtobufTest,
RejectsOversizedBatchBeforeBrpcBypassesProtobufSizeCheck) {
+ auto holder = BroadcastPBlockHolder::create_shared();
+ ASSERT_EQ(holder->_serialized_block.append(std::string(1024 * 1024, 'x')),
0);
+ // Grow references, not a 2 GiB allocation. Only the size check reads this
buffer.
+ while (holder->_serialized_block.size() < MIN_HTTP_BRPC_SIZE) {
+ auto copy = holder->_serialized_block;
+ holder->_serialized_block.append(copy);
+ }
+ brpc::SerializedRequest request;
+ auto status = ExchangeSinkBuffer::_serialize_broadcast_request(
+ make_header(), {{.block_holder = holder, .eos = false}}, &request);
+ EXPECT_FALSE(status.ok());
+ EXPECT_TRUE(request.serialized_data().empty());
+}
+
+class BroadcastService : public PBackendService {
+public:
+ bthread::CountdownEvent release {1};
+ std::atomic<int> received {0};
+
+ void release_requests() {
+ if (!_released) {
+ release.signal();
+ _released = true;
+ }
+ }
+
+ void transmit_block(google::protobuf::RpcController* controller,
+ const PTransmitDataParams* request,
PTransmitDataResult* response,
+ google::protobuf::Closure* done) override {
+ SCOPED_INIT_THREAD_CONTEXT();
+ brpc::ClosureGuard guard(done);
+ release.wait();
+ const auto& block = request->has_block() ? request->block() :
request->blocks(0);
+ EXPECT_EQ(block.column_values(), std::string(64 * 1024, 'a'));
+ EXPECT_TRUE(block.compressed());
+ EXPECT_EQ(request->packet_seq(), request->finst_id().lo() + 10);
+ received.fetch_add(1);
+ if (request->finst_id().lo() == 1) {
+ controller->SetFailed("injected transport failure");
+ } else if (request->finst_id().lo() == 2) {
+ Status::EndOfFile("receiver
finished").to_protobuf(response->mutable_status());
+ } else {
+ Status::OK().to_protobuf(response->mutable_status());
+ }
+ }
+
+private:
+ bool _released = false;
+};
+
+auto make_counting_callback(std::atomic<int>& succeeded, std::atomic<int>&
failed,
+ std::atomic<int>& eof) {
+ auto callback = ExchangeSendCallback<PTransmitDataResult>::create_shared();
+ callback->init(nullptr, false);
+ callback->start_rpc_time = 0;
+ callback->addFailedHandler([&](auto*, const auto&) { failed.fetch_add(1);
});
+ callback->addSuccessHandler([&](auto*, const auto&, const auto& result,
const auto&) {
+ if (Status::create(result.status()).template
is<ErrorCode::END_OF_FILE>()) {
+ eof.fetch_add(1);
+ } else {
+ EXPECT_TRUE(Status::create(result.status()).ok());
+ succeeded.fetch_add(1);
+ }
+ });
+ return callback;
+}
+
+TEST_P(BroadcastProtobufTest,
AsyncRpcPreservesPayloadAndReleasesCancelledCallbackRequest) {
+ BroadcastService service;
+ brpc::Server server;
+ ASSERT_EQ(server.AddService(&service, brpc::SERVER_DOESNT_OWN_SERVICE), 0);
+ {
+ // brpc registers process-lifetime built-in services on first startup.
+ debug::ScopedLeakCheckDisabler disable_lsan;
+ ASSERT_EQ(server.Start("127.0.0.1:0", nullptr), 0);
+ }
+ Defer stop([&] {
+ server.Stop(0);
+ server.Join();
+ });
+ Defer unblock([&] { service.release_requests(); });
+ brpc::Channel channel;
+ brpc::ChannelOptions options;
+ options.max_retry = 0;
+ options.timeout_ms = 10000;
+ ASSERT_EQ(channel.Init(server.listen_address(), &options), 0);
+ PBackendService_Stub stub(&channel);
+ std::atomic<int> succeeded {0};
+ std::atomic<int> failed {0};
+ std::atomic<int> eof {0};
+ std::vector<std::shared_ptr<ExchangeSendCallback<PTransmitDataResult>>>
callbacks;
+ std::vector<brpc::CallId> ids;
+ std::vector<std::weak_ptr<brpc::SerializedRequest>> weak_requests;
+ auto holder = make_holder();
+ ASSERT_TRUE(holder->prepare_serialized_block(GetParam(), 4).ok());
+ for (int i = 1; i <= 4; ++i) {
+ auto request = std::make_shared<brpc::SerializedRequest>();
+ ASSERT_TRUE(ExchangeSinkBuffer::_serialize_broadcast_request(
+ make_header(i, i + 10), {{.block_holder = holder,
.eos = false}},
+ request.get())
+ .ok());
+ auto callback = make_counting_callback(succeeded, failed, eof);
+ ids.push_back(callback->cntl_->call_id());
+ weak_requests.push_back(request);
+ auto closure = AutoReleaseClosure<
+ brpc::SerializedRequest,
+
ExchangeSendCallback<PTransmitDataResult>>::create_unique(request, callback);
+ transmit_serialized_block(&stub, std::move(closure));
+ // Simulate query cancellation before the fourth destination replies.
+ if (i != 4) {
+ callbacks.push_back(std::move(callback));
+ }
+ }
+ // No queue/producer owns the source block while these RPCs are
outstanding.
+ holder.reset();
+ service.release_requests();
+ for (auto id : ids) {
+ brpc::Join(id);
+ }
+ EXPECT_EQ(service.received, 4);
+ EXPECT_EQ(succeeded, 1);
+ EXPECT_EQ(failed, 1);
+ EXPECT_EQ(eof, 1);
+ for (const auto& request : weak_requests) {
+ EXPECT_TRUE(request.expired());
+ }
+}
+
+INSTANTIATE_TEST_SUITE_P(BlockField, BroadcastProtobufTest, testing::Bool());
+
+} // namespace
+} // namespace doris
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]