Files
apache--brpc/test/brpc_mysql_txn_integration_unittest.cpp
T
2026-07-13 13:29:29 +08:00

606 lines
24 KiB
C++

// 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.
// MySQL client TRANSACTION integration tests, run through brpc's PUBLIC API
// against a REAL mysqld.
//
// Each TEST_F drives a transaction scenario end to end:
// * brpc::Channel(protocol="mysql", connection_type="pooled",
// auth=MysqlAuthenticator) -> a live connection to the server;
// * brpc::NewMysqlTransaction(channel, opts) -> a connection-pinned
// transaction handle (START TRANSACTION on a dedicated socket);
// * MysqlRequest(tx).Query(...) + channel.CallMethod(...) -> statements
// INSIDE the transaction (same pinned socket);
// * MysqlRequest().Query(...) on the SAME channel -> a SECOND connection
// from the pool, used as an independent observer to prove isolation
// (uncommitted rows are invisible until commit());
// * tx->commit() / tx->rollback() -> terminate the transaction.
//
// Because transactions, simple SELECTs and DML all flow through the same
// COM_QUERY text protocol, these tests also cover simple-query execution
// and text-result parsing (column metadata + row field decoding).
//
// Harness (server spawn / skip convention, -mysql_use_running_server and
// -mysql_host/-port/-user/-password gflags) follows
// test/mysql/brpc_mysql_auth_handshake_unittest.cpp and, transitively,
// test/brpc_redis_unittest.cpp's which-then-spawn pattern. When mysqld is
// absent every test GTEST_SKIP()s, so the file is CI-safe with no server.
#include <gflags/gflags.h>
#include <gtest/gtest.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <pthread.h>
#include <string>
#include "brpc/channel.h"
#include "brpc/policy/mysql/mysql.h"
#include "brpc/policy/mysql/mysql_transaction.h"
#include "brpc/policy/mysql/mysql_authenticator.h"
#include "butil/logging.h"
// These gflags are intentionally re-declared here (not shared with the auth
// unittest): the CMake glob at test/CMakeLists.txt builds each
// brpc_mysql_*_unittest.cpp into its OWN executable, so there is no symbol
// collision across test binaries.
DEFINE_bool(mysql_use_running_server, false,
"Use an already-running MySQL server instead of spawning a "
"throwaway one; the running server is neither started nor stopped "
"by the test.");
DEFINE_string(mysql_host, "127.0.0.1",
"Host of the running MySQL server "
"(only with -mysql_use_running_server).");
DEFINE_int32(mysql_port, 13306,
"TCP port of the MySQL server (used for both the running server "
"and the spawned throwaway server).");
DEFINE_string(mysql_user, "root", "Login user for the transaction tests.");
DEFINE_string(mysql_password, "",
"Password for -mysql_user (empty for the spawned server).");
DEFINE_string(mysql_schema, "brpc_txn_test",
"Schema (database) the transaction tests create and use.");
namespace {
// Throwaway-server harness (mirrors brpc_mysql_auth_handshake_unittest.cpp,
// which mirrors brpc_redis_unittest.cpp). >0: forked pid; -2: external
// running server reachable; -1: no server -> tests skip.
#define MYSQLD_BIN "mysqld"
static pthread_once_t s_start_once = PTHREAD_ONCE_INIT;
static pid_t s_mysqld_pid = -1;
static std::string s_host = "127.0.0.1";
static int s_port = 13306;
static std::string s_user = "root";
static std::string s_password;
static std::string TestDataDir() {
char cwd[1024];
if (getcwd(cwd, sizeof(cwd)) == NULL) {
return std::string("/tmp/mysql_txn_data_for_test");
}
return std::string(cwd) + "/mysql_txn_data_for_test";
}
static void RemoveMysqlServer() {
if (s_mysqld_pid > 0) {
puts("[Stopping mysqld]");
char cmd[1280];
snprintf(cmd, sizeof(cmd), "kill %d", s_mysqld_pid);
CHECK(0 == system(cmd));
usleep(500000);
snprintf(cmd, sizeof(cmd), "rm -rf '%s'", TestDataDir().c_str());
CHECK(0 == system(cmd));
}
}
// Raw TCP probe to detect server readiness; returns fd (caller closes) or -1.
static int ProbeMysql() {
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
return -1;
}
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(static_cast<uint16_t>(s_port));
addr.sin_addr.s_addr = inet_addr(s_host.c_str());
if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) != 0) {
close(fd);
return -1;
}
return fd;
}
static void RunMysqlServer() {
if (FLAGS_mysql_use_running_server) {
s_host = FLAGS_mysql_host;
s_port = FLAGS_mysql_port;
s_user = FLAGS_mysql_user;
s_password = FLAGS_mysql_password;
printf("[Using running mysqld at %s:%d as user '%s']\n",
s_host.c_str(), s_port, s_user.c_str());
int fd = ProbeMysql();
if (fd >= 0) {
close(fd);
s_mysqld_pid = -2;
} else {
printf("Cannot reach running mysqld at %s:%d, tests will skip\n",
s_host.c_str(), s_port);
}
return;
}
if (system("which " MYSQLD_BIN) != 0) {
puts("Fail to find " MYSQLD_BIN ", transaction tests will be skipped");
return;
}
s_host = "127.0.0.1";
s_port = FLAGS_mysql_port;
s_user = "root";
s_password.clear();
const std::string datadir = TestDataDir();
char cmd[2048];
snprintf(cmd, sizeof(cmd), "rm -rf '%s' && mkdir -p '%s'",
datadir.c_str(), datadir.c_str());
if (system(cmd) != 0) {
puts("Fail to create datadir, transaction tests will be skipped");
return;
}
snprintf(cmd, sizeof(cmd),
MYSQLD_BIN " --initialize-insecure --datadir='%s'"
" --log-error='%s/init.err'",
datadir.c_str(), datadir.c_str());
if (system(cmd) != 0) {
puts("Fail to initialize mysqld datadir, tests will be skipped");
snprintf(cmd, sizeof(cmd), "rm -rf '%s'", datadir.c_str());
CHECK(0 == system(cmd));
return;
}
atexit(RemoveMysqlServer);
s_mysqld_pid = fork();
if (s_mysqld_pid < 0) {
puts("Fail to fork");
exit(1);
} else if (s_mysqld_pid == 0) {
puts("[Starting mysqld]");
char port_arg[32];
snprintf(port_arg, sizeof(port_arg), "--port=%d", FLAGS_mysql_port);
const std::string datadir_arg = "--datadir=" + datadir;
const std::string socket_arg = "--socket=" + datadir + "/mysqld.sock";
const std::string pidfile_arg = "--pid-file=" + datadir + "/mysqld.pid";
const std::string logerr_arg = "--log-error=" + datadir + "/mysqld.err";
char* const argv[] = {
(char*)MYSQLD_BIN,
(char*)datadir_arg.c_str(),
(char*)port_arg,
(char*)socket_arg.c_str(),
(char*)pidfile_arg.c_str(),
(char*)logerr_arg.c_str(),
(char*)"--mysqlx=OFF",
(char*)"--bind-address=127.0.0.1",
NULL};
if (execvp(MYSQLD_BIN, argv) < 0) {
puts("Fail to run " MYSQLD_BIN);
exit(1);
}
}
// Wait for TCP readiness (fresh tablespace recovery), then create a
// password account so the caching_sha2 client can authenticate over TCP
// exactly like the running-server mode. root keeps its empty password on
// the unix socket; we exercise the spawned server as empty-password root.
for (int i = 0; i < 300; ++i) {
int fd = ProbeMysql();
if (fd >= 0) {
close(fd);
return;
}
usleep(100000);
}
puts("mysqld did not become ready, transaction tests will be skipped");
s_mysqld_pid = -1;
}
// Small helpers over the brpc MySQL public API.
// Runs |sql| outside any transaction on |channel| (a fresh pooled
// connection). Returns false on transport failure.
static bool RunPlain(brpc::Channel& channel, const std::string& sql,
brpc::MysqlResponse* resp) {
brpc::MysqlRequest req;
if (!req.Query(sql)) {
return false;
}
brpc::Controller cntl;
channel.CallMethod(NULL, &cntl, &req, resp, NULL);
return !cntl.Failed();
}
// Runs |sql| INSIDE transaction |tx| (its pinned connection). Returns false
// on transport failure.
static bool RunInTx(brpc::Channel& channel, const brpc::MysqlTransaction* tx,
const std::string& sql, brpc::MysqlResponse* resp) {
brpc::MysqlRequest req(tx);
if (!req.Query(sql)) {
return false;
}
brpc::Controller cntl;
channel.CallMethod(NULL, &cntl, &req, resp, NULL);
return !cntl.Failed();
}
// Convenience: expects a single OK reply for a DML/DDL statement.
static ::testing::AssertionResult ExpectOk(const brpc::MysqlResponse& resp) {
if (resp.reply_size() < 1) {
return ::testing::AssertionFailure() << "no reply";
}
const brpc::MysqlReply& r = resp.reply(0);
if (r.is_error()) {
return ::testing::AssertionFailure()
<< "ERR " << r.error().errcode() << ": "
<< r.error().msg().as_string();
}
if (!r.is_ok()) {
return ::testing::AssertionFailure() << "reply is not OK, type=" << r.type();
}
return ::testing::AssertionSuccess();
}
// Returns the row count of the FIRST reply, asserting it is a result set.
// On any non-resultset reply returns -1 (so callers can fail clearly).
static int64_t ResultRowCount(const brpc::MysqlResponse& resp) {
if (resp.reply_size() < 1) {
return -1;
}
const brpc::MysqlReply& r = resp.reply(0);
if (!r.is_resultset()) {
return -1;
}
return static_cast<int64_t>(r.row_count());
}
// Fixture: one channel + a scratch table per test (built in SetUp, dropped in
// TearDown). InnoDB so DML is transactional.
class MysqlTxnIntegrationTest : public testing::Test {
protected:
static bool NoServer() { return s_mysqld_pid == -1; }
void SetUp() override {
pthread_once(&s_start_once, RunMysqlServer);
if (NoServer()) {
GTEST_SKIP() << "no mysqld available; skipping transaction tests";
}
// Authenticator carries user/password and the working schema. An
// empty schema is created first over a schema-less channel.
ASSERT_TRUE(InitChannel(&_setup_channel, /*schema=*/""));
brpc::MysqlResponse resp;
ASSERT_TRUE(RunPlain(_setup_channel, "CREATE DATABASE IF NOT EXISTS " +
FLAGS_mysql_schema, &resp));
ASSERT_TRUE(InitChannel(&_channel, FLAGS_mysql_schema));
ASSERT_TRUE(RunPlain(_channel, "DROP TABLE IF EXISTS " + Table(), &resp));
ASSERT_TRUE(ExpectOk(resp)) << "drop pre-existing scratch table";
ASSERT_TRUE(RunPlain(_channel,
"CREATE TABLE " + Table() +
" (id INT PRIMARY KEY, name VARCHAR(32)) "
"ENGINE=InnoDB",
&resp));
ASSERT_TRUE(ExpectOk(resp)) << "create scratch table";
}
void TearDown() override {
if (NoServer()) {
return;
}
brpc::MysqlResponse resp;
RunPlain(_channel, "DROP TABLE IF EXISTS " + Table(), &resp);
}
// Pooled channel is required so a transaction can pin its own dedicated
// connection while the test issues independent observer queries on others.
bool InitChannel(brpc::Channel* channel, const std::string& schema) {
_auth.reset(new brpc::policy::MysqlAuthenticator(s_user, s_password,
schema));
brpc::ChannelOptions options;
options.protocol = "mysql";
options.connection_type = "pooled";
options.auth = _auth.get();
options.timeout_ms = 5000;
options.max_retry = 0;
char addr[128];
snprintf(addr, sizeof(addr), "%s:%d", s_host.c_str(), s_port);
return channel->Init(addr, &options) == 0;
}
std::string Table() const { return "txn_scratch"; }
brpc::Channel _setup_channel;
brpc::Channel _channel;
// Authenticator must outlive the channels that point at it.
std::unique_ptr<brpc::policy::MysqlAuthenticator> _auth;
};
// Test cases. Each fat test chains several transactional behaviors so a single
// TEST_F validates a whole group of related transaction guarantees together.
// Transaction lifecycle: commit publishes a row to other connections, and a
// rolled-back insert as well as a rolled-back delete leave the table exactly as
// it was before the transaction started.
TEST_F(MysqlTxnIntegrationTest, CommitPublishesRollbackRestores) {
brpc::MysqlResponse resp;
// 1) committed INSERT must be visible on a fresh connection afterwards.
{
brpc::MysqlTransactionUniquePtr tx =
brpc::NewMysqlTransaction(_channel, brpc::MysqlTransactionOptions());
ASSERT_TRUE(tx != NULL) << "failed to start transaction";
ASSERT_TRUE(RunInTx(_channel, tx.get(),
"INSERT INTO " + Table() + " VALUES (3107, 'quill')",
&resp));
EXPECT_TRUE(ExpectOk(resp));
EXPECT_EQ(resp.reply(0).ok().affect_row(), 1u);
ASSERT_TRUE(tx->commit());
}
ASSERT_TRUE(RunPlain(_channel, "SELECT id, name FROM " + Table(), &resp));
EXPECT_EQ(ResultRowCount(resp), 1);
{
const brpc::MysqlReply& r = resp.reply(0);
ASSERT_TRUE(r.is_resultset());
ASSERT_EQ(r.row_count(), 1u);
const brpc::MysqlReply::Row& row = r.next();
ASSERT_EQ(row.field_count(), 2u);
EXPECT_EQ(row.field(0).sinteger(), 3107);
EXPECT_EQ(row.field(1).string().as_string(), "quill");
}
// 2) a rolled-back INSERT must leave no trace (still exactly one row).
{
brpc::MysqlTransactionUniquePtr tx =
brpc::NewMysqlTransaction(_channel, brpc::MysqlTransactionOptions());
ASSERT_TRUE(tx != NULL);
ASSERT_TRUE(RunInTx(_channel, tx.get(),
"INSERT INTO " + Table() + " VALUES (5288, 'brindle')",
&resp));
EXPECT_TRUE(ExpectOk(resp));
ASSERT_TRUE(tx->rollback());
}
ASSERT_TRUE(RunPlain(_channel, "SELECT id FROM " + Table(), &resp));
EXPECT_EQ(ResultRowCount(resp), 1) << "rolled-back insert must vanish";
// 3) a rolled-back DELETE of the committed row must restore it.
{
brpc::MysqlTransactionUniquePtr tx =
brpc::NewMysqlTransaction(_channel, brpc::MysqlTransactionOptions());
ASSERT_TRUE(tx != NULL);
ASSERT_TRUE(RunInTx(_channel, tx.get(),
"DELETE FROM " + Table() + " WHERE id = 3107", &resp));
EXPECT_TRUE(ExpectOk(resp));
EXPECT_EQ(resp.reply(0).ok().affect_row(), 1u);
ASSERT_TRUE(tx->rollback());
}
ASSERT_TRUE(RunPlain(_channel, "SELECT id FROM " + Table(), &resp));
EXPECT_EQ(ResultRowCount(resp), 1) << "rolled-back delete must restore row";
{
const brpc::MysqlReply& r = resp.reply(0);
ASSERT_TRUE(r.is_resultset());
ASSERT_EQ(r.row_count(), 1u);
EXPECT_EQ(r.next().field(0).sinteger(), 3107);
}
}
// Isolation in both directions on the same open transaction: the transaction
// reads its own not-yet-committed write on its pinned connection, while an
// independent pooled connection sees nothing until the rollback.
TEST_F(MysqlTxnIntegrationTest, OwnWriteVisibleOthersIsolated) {
brpc::MysqlTransactionUniquePtr tx =
brpc::NewMysqlTransaction(_channel, brpc::MysqlTransactionOptions());
ASSERT_TRUE(tx != NULL);
brpc::MysqlResponse resp;
ASSERT_TRUE(RunInTx(_channel, tx.get(),
"INSERT INTO " + Table() + " VALUES (6741, 'tangle')",
&resp));
EXPECT_TRUE(ExpectOk(resp));
// Same pinned connection: must read its own uncommitted row.
ASSERT_TRUE(RunInTx(_channel, tx.get(),
"SELECT name FROM " + Table() + " WHERE id = 6741", &resp));
EXPECT_EQ(ResultRowCount(resp), 1);
{
const brpc::MysqlReply& r = resp.reply(0);
ASSERT_TRUE(r.is_resultset());
ASSERT_EQ(r.row_count(), 1u);
EXPECT_EQ(r.next().field(0).string().as_string(), "tangle");
}
// A different pooled connection must NOT see the uncommitted write.
ASSERT_TRUE(RunPlain(_channel, "SELECT id FROM " + Table(), &resp));
EXPECT_EQ(ResultRowCount(resp), 0)
<< "uncommitted write leaked to another connection";
ASSERT_TRUE(tx->rollback());
// After rollback nothing remains anywhere.
ASSERT_TRUE(RunPlain(_channel, "SELECT id FROM " + Table(), &resp));
EXPECT_EQ(ResultRowCount(resp), 0);
}
// Autocommit behavior, both states in one test: with autocommit on (the
// default) a bare INSERT is immediately durable on a new connection; toggling
// autocommit off on a pinned connection turns a later INSERT into pending work
// that ROLLBACK discards.
TEST_F(MysqlTxnIntegrationTest, AutocommitOnDurableOffRollbackable) {
brpc::MysqlResponse resp;
// autocommit ON: immediate durability.
ASSERT_TRUE(RunPlain(_channel,
"INSERT INTO " + Table() + " VALUES (4419, 'amber')",
&resp));
EXPECT_TRUE(ExpectOk(resp));
ASSERT_TRUE(RunPlain(_channel, "SELECT id FROM " + Table(), &resp));
EXPECT_EQ(ResultRowCount(resp), 1);
// autocommit OFF on a pinned connection: a new INSERT is pending and a
// ROLLBACK drops only it, leaving the earlier durable row in place.
{
brpc::MysqlTransactionUniquePtr tx =
brpc::NewMysqlTransaction(_channel, brpc::MysqlTransactionOptions());
ASSERT_TRUE(tx != NULL);
ASSERT_TRUE(RunInTx(_channel, tx.get(), "SET autocommit = 0", &resp));
EXPECT_TRUE(ExpectOk(resp));
ASSERT_TRUE(RunInTx(_channel, tx.get(),
"INSERT INTO " + Table() + " VALUES (8053, 'frost')",
&resp));
EXPECT_TRUE(ExpectOk(resp));
ASSERT_TRUE(tx->rollback());
}
ASSERT_TRUE(RunPlain(_channel, "SELECT id FROM " + Table(), &resp));
EXPECT_EQ(ResultRowCount(resp), 1)
<< "autocommit=0 + rollback should drop only the pending insert";
EXPECT_EQ(resp.reply(0).next().field(0).sinteger(), 4419);
}
// Multi-statement transactional grouping plus partial undo: two inserts grouped
// under one transaction become visible together only after commit, and within a
// second transaction a SAVEPOINT lets a later insert be peeled back while the
// pre-savepoint work survives the final commit.
TEST_F(MysqlTxnIntegrationTest, GroupedInsertsThenSavepointPartialUndo) {
brpc::MysqlResponse resp;
// Two inserts under one transaction: invisible until commit, then both show.
{
brpc::MysqlTransactionUniquePtr tx =
brpc::NewMysqlTransaction(_channel, brpc::MysqlTransactionOptions());
ASSERT_TRUE(tx != NULL);
ASSERT_TRUE(RunInTx(_channel, tx.get(),
"INSERT INTO " + Table() + " VALUES (211, 'one')",
&resp));
EXPECT_TRUE(ExpectOk(resp));
ASSERT_TRUE(RunInTx(_channel, tx.get(),
"INSERT INTO " + Table() + " VALUES (733, 'two')",
&resp));
EXPECT_TRUE(ExpectOk(resp));
// Not yet visible to a separate connection.
ASSERT_TRUE(RunPlain(_channel, "SELECT id FROM " + Table(), &resp));
EXPECT_EQ(ResultRowCount(resp), 0);
ASSERT_TRUE(tx->commit());
}
ASSERT_TRUE(RunPlain(_channel, "SELECT id FROM " + Table(), &resp));
EXPECT_EQ(ResultRowCount(resp), 2) << "both grouped inserts visible";
// Start fresh for the savepoint half.
ASSERT_TRUE(RunPlain(_channel, "DELETE FROM " + Table(), &resp));
ASSERT_TRUE(ExpectOk(resp));
// SAVEPOINT then ROLLBACK TO it: pre-savepoint row kept, post dropped.
{
brpc::MysqlTransactionUniquePtr tx =
brpc::NewMysqlTransaction(_channel, brpc::MysqlTransactionOptions());
ASSERT_TRUE(tx != NULL);
ASSERT_TRUE(RunInTx(_channel, tx.get(),
"INSERT INTO " + Table() + " VALUES (901, 'kept')",
&resp));
EXPECT_TRUE(ExpectOk(resp));
ASSERT_TRUE(RunInTx(_channel, tx.get(), "SAVEPOINT mark1", &resp));
EXPECT_TRUE(ExpectOk(resp));
ASSERT_TRUE(RunInTx(_channel, tx.get(),
"INSERT INTO " + Table() + " VALUES (902, 'gone')",
&resp));
EXPECT_TRUE(ExpectOk(resp));
ASSERT_TRUE(RunInTx(_channel, tx.get(), "ROLLBACK TO SAVEPOINT mark1",
&resp));
EXPECT_TRUE(ExpectOk(resp));
// Inside the txn only the pre-savepoint row remains.
ASSERT_TRUE(RunInTx(_channel, tx.get(), "SELECT id FROM " + Table(),
&resp));
EXPECT_EQ(ResultRowCount(resp), 1);
ASSERT_TRUE(tx->commit());
}
ASSERT_TRUE(RunPlain(_channel, "SELECT id FROM " + Table(), &resp));
EXPECT_EQ(ResultRowCount(resp), 1) << "only the kept row should persist";
{
const brpc::MysqlReply& r = resp.reply(0);
ASSERT_TRUE(r.is_resultset());
ASSERT_EQ(r.row_count(), 1u);
EXPECT_EQ(r.next().field(0).sinteger(), 901);
}
}
// Error surfaces from within a transaction: a duplicate-primary-key insert
// returns an ERR reply (and the transaction still rolls back cleanly), and a
// write attempted in a read-only transaction is likewise rejected with ERR.
TEST_F(MysqlTxnIntegrationTest, DuplicateKeyAndReadOnlyWriteReportErr) {
brpc::MysqlResponse resp;
// Seed a committed row so the in-txn insert collides on the primary key.
ASSERT_TRUE(RunPlain(_channel,
"INSERT INTO " + Table() + " VALUES (1505, 'seed')",
&resp));
ASSERT_TRUE(ExpectOk(resp));
{
brpc::MysqlTransactionUniquePtr tx =
brpc::NewMysqlTransaction(_channel, brpc::MysqlTransactionOptions());
ASSERT_TRUE(tx != NULL);
// Duplicate-key insert -> ERR packet (errno 1062, ER_DUP_ENTRY).
ASSERT_TRUE(RunInTx(_channel, tx.get(),
"INSERT INTO " + Table() + " VALUES (1505, 'clash')",
&resp));
ASSERT_GE(resp.reply_size(), 1u);
const brpc::MysqlReply& r = resp.reply(0);
EXPECT_TRUE(r.is_error()) << "duplicate key should yield an ERR reply";
if (r.is_error()) {
EXPECT_EQ(r.error().errcode(), 1062) << "expected ER_DUP_ENTRY (1062)";
}
ASSERT_TRUE(tx->rollback());
}
// A read-only transaction must reject a write.
{
brpc::MysqlTransactionOptions opts;
opts.readonly = true;
brpc::MysqlTransactionUniquePtr tx =
brpc::NewMysqlTransaction(_channel, opts);
ASSERT_TRUE(tx != NULL) << "failed to start read-only transaction";
ASSERT_TRUE(RunInTx(_channel, tx.get(),
"INSERT INTO " + Table() + " VALUES (1777, 'nope')",
&resp));
ASSERT_GE(resp.reply_size(), 1u);
const brpc::MysqlReply& r = resp.reply(0);
EXPECT_TRUE(r.is_error())
<< "write in a read-only transaction should be rejected with ERR";
if (r.is_error()) {
// ER_CANT_EXECUTE_IN_READ_ONLY_TRANSACTION == 1792.
EXPECT_EQ(r.error().errcode(), 1792)
<< "expected read-only-transaction error (1792)";
}
ASSERT_TRUE(tx->rollback());
}
}
} // namespace