blob: 953bd1f7cbe57194e4ed4291fe90e16ee229da1f [file]
// 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.
// Unit tests of the nshead_mcpack protocol, focused on the client-side
// response handler: `ProcessNsheadMcpackResponse' must always release the
// bthread_id lock (via `accessor.OnResponse') on every path, otherwise the
// RPC caller hangs on Join() forever and even the timeout mechanism cannot
// rescue it.
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <gtest/gtest.h>
#include <gflags/gflags.h>
#include <google/protobuf/descriptor.h>
#include "butil/time.h"
#include "butil/macros.h"
#include "bthread/id.h"
#include "brpc/socket.h"
#include "brpc/acceptor.h"
#include "brpc/server.h"
#include "brpc/nshead.h"
#include "brpc/controller.h"
#include "brpc/policy/nshead_mcpack_protocol.h"
#include "brpc/policy/nshead_protocol.h"
#include "brpc/policy/most_common_message.h"
#include "mcpack2pb/mcpack2pb.h"
#include "echo.pb.h"
namespace {
static const std::string EXP_RESPONSE = "world";
// Simulates the mcpack parser facing a garbage body: drains the input and
// returns 0 so that `parse_from_iobuf' reports failure.
static size_t FailingParse(google::protobuf::Message*,
google::protobuf::io::ZeroCopyInputStream* input) {
const void* data;
int size;
while (input->Next(&data, &size)) {}
return 0;
}
// Simulates a successful mcpack parsing which fills the response.
static size_t SuccessfulParse(google::protobuf::Message* msg,
google::protobuf::io::ZeroCopyInputStream* input) {
const void* data;
int size;
size_t total = 0;
while (input->Next(&data, &size)) {
total += size;
}
static_cast<test::EchoRequest*>(msg)->set_message(EXP_RESPONSE);
return total;
}
static void RegisterMcpackHandlers() {
static bool registered = []() {
mcpack2pb::MessageHandler failing_handler = {
FailingParse, nullptr, nullptr, nullptr};
// Note: `full_name()' returns absl::string_view in newer protobuf,
// wrap it into std::string explicitly.
mcpack2pb::register_message_handler_or_die(
std::string(test::EchoResponse::descriptor()->full_name()), failing_handler);
mcpack2pb::MessageHandler working_handler = {
SuccessfulParse, nullptr, nullptr, nullptr};
mcpack2pb::register_message_handler_or_die(
std::string(test::EchoRequest::descriptor()->full_name()), working_handler);
return true;
}();
(void)registered;
}
class NsheadMcpackTest : public ::testing::Test {
protected:
NsheadMcpackTest() {
RegisterMcpackHandlers();
EXPECT_EQ(0, pipe(_pipe_fds));
brpc::SocketId id;
brpc::SocketOptions options;
options.fd = _pipe_fds[1];
EXPECT_EQ(0, brpc::Socket::Create(options, &id));
EXPECT_EQ(0, brpc::Socket::Address(id, &_socket));
}
virtual ~NsheadMcpackTest() {
// The write end (_pipe_fds[1]) is owned by `_socket' and closed
// when it is released; close the unused read end here to avoid
// leaking fds across the per-case fixtures.
close(_pipe_fds[0]);
};
virtual void SetUp() {};
virtual void TearDown() {};
void ProcessMessage(brpc::InputMessageBase* msg) {
if (msg->_socket == nullptr) {
_socket->ReAddress(&msg->_socket);
}
_socket->PostponeEOF();
brpc::policy::ProcessNsheadMcpackResponse(msg);
}
// Make a response with a valid nshead header followed by a garbage
// body which cannot be parsed as an mcpack message.
brpc::policy::MostCommonMessage* MakeMalformedResponseMessage() {
brpc::policy::MostCommonMessage* msg =
brpc::policy::MostCommonMessage::Get();
brpc::nshead_t head;
memset(&head, 0, sizeof(head));
head.magic_num = brpc::NSHEAD_MAGICNUM;
const char garbage[] = "\xde\xad\xbe\xef";
head.body_len = sizeof(garbage) - 1;
msg->meta.append(&head, sizeof(head));
msg->payload.append(garbage, sizeof(garbage) - 1);
return msg;
}
// Assert that the correlation_id bound to the RPC is no longer locked.
// If the lock was leaked, `brpc::Join' in user code would hang forever
// and even the RPC timeout could not rescue it, because the timeout
// error is only consumed during unlocking.
void AssertLockReleased(const brpc::CallId& cid) {
// A locked id returns EBUSY while a destroyed (released) id
// returns EINVAL.
ASSERT_EQ(EINVAL, bthread_id_trylock(cid, nullptr))
<< "correlation_id lock was leaked";
}
int _pipe_fds[2];
brpc::SocketUniquePtr _socket;
};
// A malformed mcpack body must fail the RPC instead of leaking the lock.
TEST_F(NsheadMcpackTest, process_response_with_malformed_body) {
test::EchoResponse res;
brpc::Controller cntl;
cntl._response = &res;
brpc::policy::MostCommonMessage* msg = MakeMalformedResponseMessage();
_socket->set_correlation_id(cntl.call_id().value);
ProcessMessage(msg);
ASSERT_TRUE(cntl.Failed());
AssertLockReleased(cntl.call_id());
}
// A response message whose mcpack handler was never registered must not
// leak the lock either.
TEST_F(NsheadMcpackTest, process_response_with_unregistered_handler) {
// test::ComboRequest has no registered mcpack handler at all.
test::ComboRequest res;
brpc::Controller cntl;
cntl._response = &res;
brpc::policy::MostCommonMessage* msg = MakeMalformedResponseMessage();
_socket->set_correlation_id(cntl.call_id().value);
ProcessMessage(msg);
ASSERT_TRUE(cntl.Failed());
AssertLockReleased(cntl.call_id());
}
// The controller without a response object must not leak the lock.
TEST_F(NsheadMcpackTest, process_response_without_response_object) {
brpc::Controller cntl;
brpc::policy::MostCommonMessage* msg = MakeMalformedResponseMessage();
_socket->set_correlation_id(cntl.call_id().value);
ProcessMessage(msg);
AssertLockReleased(cntl.call_id());
}
// A well-formed response must be delivered to the user and unlock normally.
TEST_F(NsheadMcpackTest, process_response_success) {
// Reuse test::EchoRequest whose registered handler always succeeds.
test::EchoRequest res;
brpc::Controller cntl;
cntl._response = &res;
brpc::policy::MostCommonMessage* msg = MakeMalformedResponseMessage();
_socket->set_correlation_id(cntl.call_id().value);
ProcessMessage(msg);
ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText();
ASSERT_EQ(EXP_RESPONSE, res.message());
AssertLockReleased(cntl.call_id());
}
} //namespace