1251 lines
51 KiB
C++
1251 lines
51 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.
|
|
|
|
// Concurrency stress test on a POOLED mysql channel: many bthreads hammering
|
|
// ONE pooled Channel, re-running concurrently the self-checking work units
|
|
// that the two sibling integration files cover serially:
|
|
//
|
|
// * brpc_mysql_txn_integration_unittest.cpp (transaction scenarios)
|
|
// * brpc_mysql_prepared_integration_unittest.cpp (prepared-statement
|
|
// scenarios)
|
|
//
|
|
// Only the literal data values differ from the siblings so that cross-talk
|
|
// between concurrent workers is detectable by value. A POOLED Channel must
|
|
// check out / return pooled sockets without races, must PIN one socket per
|
|
// MysqlTransaction, and must keep concurrent transactions / prepared
|
|
// statements isolated.
|
|
//
|
|
// WHAT IT CHECKS
|
|
// ConnectionType = POOLED, pool capped at FIVE connections via the gflag
|
|
// `max_connection_pool_size` (DEFINE_int32 max_connection_pool_size in
|
|
// src/brpc/socket.cpp:99). FIVE is deliberate: with more workers than
|
|
// pooled sockets we exercise BOTH pooled-socket reuse AND the create-a-NEW-
|
|
// connection-under-load path concurrently, surfacing checkout/return races,
|
|
// transaction connection-affinity/pinning under contention, and fd_version ABA.
|
|
//
|
|
// * ManyWorkersMixedScenarios:
|
|
// 16-32 bthreads, each looping ~50x, each iteration picks ONE of five
|
|
// representative self-checking work units (3 txn + 2 prepared) and
|
|
// asserts its OWN correct, independent result. Per-worker scratch tables
|
|
// / per-iteration ids keep row-count assertions exact under concurrency.
|
|
//
|
|
// * TwoTransactionsHoldDifferentPinnedSockets (focused check a):
|
|
// two transactions in parallel must hold DIFFERENT pinned SocketIds
|
|
// (GetSocketId()) and must not see each other's rows.
|
|
//
|
|
// * TransactionPlusPreparedInParallel (focused check b):
|
|
// one transaction + one prepared statement in parallel, each returns its
|
|
// own correct independent result; the prepared path must not disturb the
|
|
// transaction's pinned connection.
|
|
//
|
|
// The bar: NO concurrency bug across all iterations -- no shared-socket
|
|
// corruption, no interleaved/wrong replies, no crash.
|
|
//
|
|
// HARNESS
|
|
// Reuses the gflag-driven, self-spawning-mysqld harness from the two sibling
|
|
// integration files (flags -mysql_use_running_server / -mysql_host / -port /
|
|
// -user / -password / -schema; MysqlAuthenticator-based pooled Channel). When
|
|
// no mysqld is reachable every test GTEST_SKIP()s, so the file is CI-safe.
|
|
|
|
#include <gflags/gflags.h>
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <arpa/inet.h>
|
|
#include <netinet/in.h>
|
|
#include <pthread.h>
|
|
#include <sys/socket.h>
|
|
#include <unistd.h>
|
|
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "brpc/channel.h"
|
|
#include "brpc/controller.h"
|
|
#include "brpc/policy/mysql/mysql.h"
|
|
#include "brpc/policy/mysql/mysql_transaction.h"
|
|
#include "brpc/policy/mysql/mysql_authenticator.h"
|
|
#include "bthread/bthread.h"
|
|
#include "butil/logging.h"
|
|
#include "butil/string_printf.h"
|
|
#include "butil/strings/string_piece.h"
|
|
|
|
// Flags mirror the sibling integration files so one command line drives them
|
|
// all against the same server. Each *_unittest.cpp links into its own binary
|
|
// (the test/ CMake glob), so re-declaring these flags here is not a clash.
|
|
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 concurrency tests.");
|
|
DEFINE_string(mysql_password, "",
|
|
"Password for -mysql_user (empty for the spawned server).");
|
|
DEFINE_string(mysql_schema, "brpc_pool_conc_test",
|
|
"Schema (database) the concurrency tests create and use.");
|
|
|
|
namespace {
|
|
|
|
#ifndef GFLAGS_NS
|
|
#define GFLAGS_NS GFLAGS_NAMESPACE
|
|
#endif
|
|
|
|
#define MYSQLD_BIN "mysqld"
|
|
|
|
static const char* kCollation = "utf8mb4_general_ci";
|
|
|
|
// Concurrency knobs. kWorkers is deliberately > the pool cap (5) so workers
|
|
// contend for pooled sockets AND force new-connection creation under load.
|
|
const int kWorkers = 24;
|
|
const int kIterationsPerWorker = 50;
|
|
const int kPoolCap = 5;
|
|
|
|
// Throwaway-server harness (mirrors the two sibling integration files, which
|
|
// mirror brpc_redis_unittest.cpp). >0: forked pid; -2: external running
|
|
// server reachable; -1: no server -> tests skip.
|
|
static pthread_once_t g_start_once = PTHREAD_ONCE_INIT;
|
|
static pid_t g_mysqld_pid = -1;
|
|
static std::string g_host = "127.0.0.1";
|
|
static int g_port = 13306;
|
|
static std::string g_user = "root";
|
|
static std::string g_password;
|
|
static std::string g_schema;
|
|
|
|
static std::string TestDataDir() {
|
|
char cwd[1024];
|
|
if (getcwd(cwd, sizeof(cwd)) == NULL) {
|
|
return std::string("/tmp/mysql_pool_conc_data_for_test");
|
|
}
|
|
return std::string(cwd) + "/mysql_pool_conc_data_for_test";
|
|
}
|
|
|
|
static void RemoveMysqlServer() {
|
|
if (g_mysqld_pid > 0) {
|
|
puts("[Stopping mysqld]");
|
|
char cmd[1280];
|
|
snprintf(cmd, sizeof(cmd), "kill %d", g_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 for server readiness; returns fd (caller closes) or -1.
|
|
static int ProbeConnect() {
|
|
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>(g_port));
|
|
addr.sin_addr.s_addr = inet_addr(g_host.c_str());
|
|
if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) != 0) {
|
|
close(fd);
|
|
return -1;
|
|
}
|
|
return fd;
|
|
}
|
|
|
|
static void StartServerOnce() {
|
|
if (FLAGS_mysql_use_running_server) {
|
|
g_host = FLAGS_mysql_host;
|
|
g_port = FLAGS_mysql_port;
|
|
g_user = FLAGS_mysql_user;
|
|
g_password = FLAGS_mysql_password;
|
|
g_schema = FLAGS_mysql_schema;
|
|
printf("[Using running mysqld at %s:%d as user '%s', schema '%s']\n",
|
|
g_host.c_str(), g_port, g_user.c_str(), g_schema.c_str());
|
|
int fd = ProbeConnect();
|
|
if (fd >= 0) {
|
|
close(fd);
|
|
g_mysqld_pid = -2;
|
|
} else {
|
|
printf("Cannot reach running mysqld at %s:%d, tests will skip\n",
|
|
g_host.c_str(), g_port);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (system("which " MYSQLD_BIN) != 0) {
|
|
puts("Fail to find " MYSQLD_BIN ", concurrency tests will be skipped");
|
|
return;
|
|
}
|
|
g_host = "127.0.0.1";
|
|
g_port = FLAGS_mysql_port;
|
|
g_user = "root";
|
|
g_password.clear();
|
|
g_schema = FLAGS_mysql_schema;
|
|
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, concurrency 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);
|
|
|
|
g_mysqld_pid = fork();
|
|
if (g_mysqld_pid < 0) {
|
|
puts("Fail to fork");
|
|
exit(1);
|
|
} else if (g_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);
|
|
}
|
|
}
|
|
for (int i = 0; i < 300; ++i) {
|
|
int fd = ProbeConnect();
|
|
if (fd >= 0) {
|
|
close(fd);
|
|
return;
|
|
}
|
|
usleep(100000);
|
|
}
|
|
puts("mysqld did not become ready, concurrency tests will be skipped");
|
|
g_mysqld_pid = -1;
|
|
}
|
|
|
|
// Small helpers over the brpc MySQL public API.
|
|
|
|
// Plain (no transaction / no statement) query on a fresh pooled connection.
|
|
static bool RunPlain(brpc::Channel& channel, const std::string& sql,
|
|
brpc::MysqlResponse* resp, std::string* err) {
|
|
brpc::MysqlRequest req;
|
|
if (!req.Query(sql)) {
|
|
if (err) *err = "build query failed: " + sql;
|
|
return false;
|
|
}
|
|
brpc::Controller cntl;
|
|
channel.CallMethod(NULL, &cntl, &req, resp, NULL);
|
|
if (cntl.Failed()) {
|
|
if (err) *err = "rpc failed: " + cntl.ErrorText();
|
|
return false;
|
|
}
|
|
if (resp->reply_size() < 1) {
|
|
if (err) *err = "no reply for: " + sql;
|
|
return false;
|
|
}
|
|
if (resp->reply(0).is_error()) {
|
|
const brpc::MysqlReply& r = resp->reply(0);
|
|
if (err) {
|
|
*err = butil::string_printf("mysql error %u: %.*s (sql=%s)",
|
|
r.error().errcode(), (int)r.error().msg().size(),
|
|
r.error().msg().data(), sql.c_str());
|
|
}
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// |sql| INSIDE transaction |tx| (its pinned connection).
|
|
static bool RunInTx(brpc::Channel& channel, const brpc::MysqlTransaction* tx,
|
|
const std::string& sql, brpc::MysqlResponse* resp,
|
|
std::string* err) {
|
|
brpc::MysqlRequest req(tx);
|
|
if (!req.Query(sql)) {
|
|
if (err) *err = "build query failed: " + sql;
|
|
return false;
|
|
}
|
|
brpc::Controller cntl;
|
|
channel.CallMethod(NULL, &cntl, &req, resp, NULL);
|
|
if (cntl.Failed()) {
|
|
if (err) *err = "rpc failed: " + cntl.ErrorText();
|
|
return false;
|
|
}
|
|
if (resp->reply_size() < 1) {
|
|
if (err) *err = "no reply for: " + sql;
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Row count of the first reply when it is a result set, else -1.
|
|
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());
|
|
}
|
|
|
|
// Coerce an integer-ish field to long long (handles the various widths the
|
|
// server may choose for a column / expression).
|
|
static bool FieldToLongLong(const brpc::MysqlReply::Field& f, long long* out) {
|
|
if (f.is_sbigint()) *out = f.sbigint();
|
|
else if (f.is_bigint()) *out = (long long)f.bigint();
|
|
else if (f.is_sinteger()) *out = f.sinteger();
|
|
else if (f.is_integer()) *out = (long long)f.integer();
|
|
else if (f.is_string()) *out = atoll(f.string().as_string().c_str());
|
|
else return false;
|
|
return true;
|
|
}
|
|
|
|
static int InitPooledChannel(brpc::Channel* channel,
|
|
brpc::policy::MysqlAuthenticator** out_auth,
|
|
const std::string& schema) {
|
|
brpc::policy::MysqlAuthenticator* auth =
|
|
new brpc::policy::MysqlAuthenticator(g_user, g_password, schema, "",
|
|
kCollation);
|
|
*out_auth = auth;
|
|
brpc::ChannelOptions options;
|
|
options.protocol = brpc::PROTOCOL_MYSQL;
|
|
options.connection_type = "pooled"; // CONNECTION_TYPE_POOLED
|
|
options.auth = auth;
|
|
options.timeout_ms = 10000;
|
|
options.connect_timeout_ms = 5000;
|
|
options.max_retry = 0;
|
|
return channel->Init(g_host.c_str(), g_port, &options);
|
|
}
|
|
|
|
// Fixture: one shared pooled channel, pool capped at FIVE. Per-test scratch
|
|
// tables are created by the tests themselves (per-worker, to keep row counts
|
|
// exact under concurrency).
|
|
class MysqlPoolConcurrencyTest : public testing::Test {
|
|
protected:
|
|
static bool NoServer() { return g_mysqld_pid == -1; }
|
|
|
|
void SetUp() override {
|
|
pthread_once(&g_start_once, StartServerOnce);
|
|
if (NoServer()) {
|
|
GTEST_SKIP() << "no mysqld available; skipping pool-concurrency "
|
|
"integration test (set -mysql_use_running_server "
|
|
"or install mysqld)";
|
|
}
|
|
// Cap the pool at FIVE for the whole test. Verified flag name:
|
|
// src/brpc/socket.cpp:99 DEFINE_int32(max_connection_pool_size, ...).
|
|
ASSERT_FALSE(GFLAGS_NS::SetCommandLineOption(
|
|
"max_connection_pool_size",
|
|
std::to_string(kPoolCap).c_str()).empty())
|
|
<< "failed to set gflag max_connection_pool_size";
|
|
|
|
// Create the schema over a schema-less channel, then bind to it.
|
|
brpc::policy::MysqlAuthenticator* setup_auth = NULL;
|
|
ASSERT_EQ(0, InitPooledChannel(&_setup_channel, &setup_auth, ""));
|
|
_setup_auth.reset(setup_auth);
|
|
brpc::MysqlResponse resp;
|
|
std::string err;
|
|
ASSERT_TRUE(RunPlain(_setup_channel,
|
|
"CREATE DATABASE IF NOT EXISTS " + g_schema,
|
|
&resp, &err)) << err;
|
|
|
|
brpc::policy::MysqlAuthenticator* auth = NULL;
|
|
ASSERT_EQ(0, InitPooledChannel(&_channel, &auth, g_schema));
|
|
_auth.reset(auth);
|
|
}
|
|
|
|
brpc::Channel _setup_channel;
|
|
brpc::Channel _channel;
|
|
// Authenticators must outlive the channels that point at them.
|
|
std::unique_ptr<brpc::policy::MysqlAuthenticator> _setup_auth;
|
|
std::unique_ptr<brpc::policy::MysqlAuthenticator> _auth;
|
|
};
|
|
|
|
// WORK UNITS
|
|
// Each work unit is a self-checking re-run of a sibling scenario, with its OWN
|
|
// independent expected result so cross-talk/corruption is detectable. Each
|
|
// returns true on a correct result; on any failure it fills |err|.
|
|
//
|
|
// All work units use a PER-WORKER scratch table (passed in) so concurrent
|
|
// workers never share rows, keeping row-count assertions exact.
|
|
|
|
// WU1 -- txn commit makes rows visible.
|
|
// (Transaction-commit-visibility check; uses its own per-worker id 71xxx
|
|
// 'aria' so concurrent workers never collide on data.)
|
|
static bool WU_TxnCommitVisible(brpc::Channel& ch, const std::string& table,
|
|
int iter, std::string* err) {
|
|
const int id = 71000 + iter;
|
|
const char* name = "aria";
|
|
brpc::MysqlResponse resp;
|
|
brpc::MysqlTransactionUniquePtr tx =
|
|
brpc::NewMysqlTransaction(ch, brpc::MysqlTransactionOptions());
|
|
if (tx == NULL) { *err = "WU1: NewMysqlTransaction NULL"; return false; }
|
|
if (!RunInTx(ch, tx.get(),
|
|
butil::string_printf("INSERT INTO %s VALUES (%d, '%s')",
|
|
table.c_str(), id, name),
|
|
&resp, err)) return false;
|
|
if (resp.reply(0).is_error()) {
|
|
*err = "WU1 INSERT err: " + resp.reply(0).error().msg().as_string();
|
|
return false;
|
|
}
|
|
if (!tx->commit()) { *err = "WU1: commit failed"; return false; }
|
|
|
|
// A fresh pooled connection must now see exactly our committed row.
|
|
if (!RunPlain(ch, butil::string_printf(
|
|
"SELECT id, name FROM %s WHERE id=%d",
|
|
table.c_str(), id), &resp, err)) return false;
|
|
if (ResultRowCount(resp) != 1) {
|
|
*err = butil::string_printf("WU1: expected 1 visible row, got %lld",
|
|
(long long)ResultRowCount(resp));
|
|
return false;
|
|
}
|
|
const brpc::MysqlReply::Row& row = resp.reply(0).next();
|
|
long long got_id = 0;
|
|
if (!FieldToLongLong(row.field(0), &got_id) || got_id != id) {
|
|
*err = "WU1: wrong id read back"; return false;
|
|
}
|
|
if (row.field(1).string().as_string() != name) {
|
|
*err = "WU1: wrong name read back"; return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// WU2 -- txn rollback discards the insert.
|
|
// (Rollback-discards-insert check; uses per-worker id 82xxx 'cory'.)
|
|
static bool WU_TxnRollbackDiscards(brpc::Channel& ch, const std::string& table,
|
|
int iter, std::string* err) {
|
|
const int id = 82000 + iter;
|
|
brpc::MysqlResponse resp;
|
|
brpc::MysqlTransactionUniquePtr tx =
|
|
brpc::NewMysqlTransaction(ch, brpc::MysqlTransactionOptions());
|
|
if (tx == NULL) { *err = "WU2: NewMysqlTransaction NULL"; return false; }
|
|
if (!RunInTx(ch, tx.get(),
|
|
butil::string_printf("INSERT INTO %s VALUES (%d, 'cory')",
|
|
table.c_str(), id),
|
|
&resp, err)) return false;
|
|
if (resp.reply(0).is_error()) {
|
|
*err = "WU2 INSERT err: " + resp.reply(0).error().msg().as_string();
|
|
return false;
|
|
}
|
|
if (!tx->rollback()) { *err = "WU2: rollback failed"; return false; }
|
|
|
|
// The rolled-back row must be gone on a fresh connection.
|
|
if (!RunPlain(ch, butil::string_printf("SELECT id FROM %s WHERE id=%d",
|
|
table.c_str(), id),
|
|
&resp, err)) return false;
|
|
if (ResultRowCount(resp) != 0) {
|
|
*err = butil::string_printf(
|
|
"WU2: rolled-back insert still visible (%lld rows)",
|
|
(long long)ResultRowCount(resp));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// WU3 -- a SELECT inside an open txn sees the txn's own uncommitted write.
|
|
// (Read-your-own-write check; uses per-worker id 93xxx 'echo'.)
|
|
static bool WU_TxnReadsOwnWrite(brpc::Channel& ch, const std::string& table,
|
|
int iter, std::string* err) {
|
|
const int id = 93000 + iter;
|
|
const char* name = "echo";
|
|
brpc::MysqlResponse resp;
|
|
brpc::MysqlTransactionUniquePtr tx =
|
|
brpc::NewMysqlTransaction(ch, brpc::MysqlTransactionOptions());
|
|
if (tx == NULL) { *err = "WU3: NewMysqlTransaction NULL"; return false; }
|
|
if (!RunInTx(ch, tx.get(),
|
|
butil::string_printf("INSERT INTO %s VALUES (%d, '%s')",
|
|
table.c_str(), id, name),
|
|
&resp, err)) return false;
|
|
if (resp.reply(0).is_error()) {
|
|
*err = "WU3 INSERT err: " + resp.reply(0).error().msg().as_string();
|
|
return false;
|
|
}
|
|
// Same pinned connection: must see its own uncommitted row.
|
|
if (!RunInTx(ch, tx.get(),
|
|
butil::string_printf("SELECT name FROM %s WHERE id=%d",
|
|
table.c_str(), id),
|
|
&resp, err)) return false;
|
|
bool ok = (ResultRowCount(resp) == 1) &&
|
|
resp.reply(0).next().field(0).string().as_string() == name;
|
|
// Discard the row so the per-worker table stays empty for the next pick.
|
|
tx->rollback();
|
|
if (!ok) { *err = "WU3: txn did not read its own uncommitted write"; }
|
|
return ok;
|
|
}
|
|
|
|
// WU4 -- prepared: bind one INT param, fetch the matching row.
|
|
// (Prepared-bind-int check; each worker seeds its OWN (id 64xxx, 'lyra') row
|
|
// and binds it back so the result is unique per worker.)
|
|
static bool WU_PreparedBindInt(brpc::Channel& ch, const std::string& table,
|
|
int iter, std::string* err) {
|
|
const int id = 64000 + iter;
|
|
const char* name = "lyra";
|
|
brpc::MysqlResponse resp;
|
|
// Seed our own row (autocommit) then read it back via a prepared SELECT.
|
|
if (!RunPlain(ch, butil::string_printf("INSERT INTO %s VALUES (%d, '%s')",
|
|
table.c_str(), id, name),
|
|
&resp, err)) return false;
|
|
|
|
brpc::MysqlStatementUniquePtr stmt = brpc::NewMysqlStatement(
|
|
ch, butil::string_printf("SELECT name FROM %s WHERE id=?",
|
|
table.c_str()));
|
|
if (stmt == NULL) { *err = "WU4: NewMysqlStatement NULL"; return false; }
|
|
if (stmt->param_count() != 1u) { *err = "WU4: param_count != 1"; return false; }
|
|
|
|
brpc::MysqlRequest req(stmt.get());
|
|
if (!req.AddParam((int32_t)id)) { *err = "WU4: AddParam failed"; return false; }
|
|
brpc::Controller cntl;
|
|
ch.CallMethod(NULL, &cntl, &req, &resp, NULL);
|
|
if (cntl.Failed()) { *err = "WU4 rpc: " + cntl.ErrorText(); return false; }
|
|
if (resp.reply_size() < 1 || !resp.reply(0).is_resultset()) {
|
|
*err = "WU4: not a resultset"; return false;
|
|
}
|
|
if (resp.reply(0).row_count() != 1u) {
|
|
*err = "WU4: expected exactly 1 row"; return false;
|
|
}
|
|
bool ok = resp.reply(0).next().field(0).string().as_string() == name;
|
|
// Clean our seeded row so subsequent picks on this table stay exact.
|
|
RunPlain(ch, butil::string_printf("DELETE FROM %s WHERE id=%d",
|
|
table.c_str(), id), &resp, err);
|
|
if (!ok) { *err = "WU4: prepared bind returned wrong/cross-talked name"; }
|
|
return ok;
|
|
}
|
|
|
|
// WU5 -- prepared multi-param arithmetic: SELECT ? + ? with two INT params.
|
|
// (Two-param arithmetic check; uses per-iteration operands so each worker
|
|
// verifies its OWN sum.)
|
|
static bool WU_PreparedArithmetic(brpc::Channel& ch, int worker, int iter,
|
|
std::string* err) {
|
|
const int32_t a = 1000 + worker;
|
|
const int32_t b = 7 + iter;
|
|
const long long expect = (long long)a + b;
|
|
brpc::MysqlStatementUniquePtr stmt =
|
|
brpc::NewMysqlStatement(ch, "SELECT CAST(? AS SIGNED) + CAST(? AS SIGNED)");
|
|
if (stmt == NULL) { *err = "WU5: NewMysqlStatement NULL"; return false; }
|
|
if (stmt->param_count() != 2u) { *err = "WU5: param_count != 2"; return false; }
|
|
|
|
brpc::MysqlRequest req(stmt.get());
|
|
if (!req.AddParam(a) || !req.AddParam(b)) {
|
|
*err = "WU5: AddParam failed"; return false;
|
|
}
|
|
brpc::MysqlResponse resp;
|
|
brpc::Controller cntl;
|
|
ch.CallMethod(NULL, &cntl, &req, &resp, NULL);
|
|
if (cntl.Failed()) { *err = "WU5 rpc: " + cntl.ErrorText(); return false; }
|
|
if (resp.reply_size() < 1 || !resp.reply(0).is_resultset() ||
|
|
resp.reply(0).row_count() != 1u) {
|
|
*err = "WU5: bad resultset"; return false;
|
|
}
|
|
long long got = 0;
|
|
if (!FieldToLongLong(resp.reply(0).next().field(0), &got) || got != expect) {
|
|
*err = butil::string_printf("WU5: ?+? wrong (got %lld want %lld)",
|
|
got, expect);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Worker driver for ManyWorkersMixedScenarios.
|
|
struct MixWorkerArgs {
|
|
brpc::Channel* channel;
|
|
int worker_id;
|
|
std::string table; // per-worker scratch table
|
|
std::string error; // first failure (empty == all good)
|
|
int completed; // iterations completed without error
|
|
};
|
|
|
|
void* MixWorker(void* p) {
|
|
MixWorkerArgs* a = static_cast<MixWorkerArgs*>(p);
|
|
a->error.clear();
|
|
a->completed = 0;
|
|
for (int iter = 0; iter < kIterationsPerWorker; ++iter) {
|
|
// Rotate through the five work units; offset by worker so different
|
|
// workers run different units at the same instant.
|
|
const int pick = (a->worker_id + iter) % 5;
|
|
std::string err;
|
|
bool ok = false;
|
|
switch (pick) {
|
|
case 0: ok = WU_TxnCommitVisible(*a->channel, a->table, iter, &err); break;
|
|
case 1: ok = WU_TxnRollbackDiscards(*a->channel, a->table, iter, &err); break;
|
|
case 2: ok = WU_TxnReadsOwnWrite(*a->channel, a->table, iter, &err); break;
|
|
case 3: ok = WU_PreparedBindInt(*a->channel, a->table, iter, &err); break;
|
|
default: ok = WU_PreparedArithmetic(*a->channel, a->worker_id, iter, &err); break;
|
|
}
|
|
if (!ok) {
|
|
a->error = butil::string_printf("worker %d iter %d pick %d: %s",
|
|
a->worker_id, iter, pick, err.c_str());
|
|
return NULL;
|
|
}
|
|
++a->completed;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// TEST 1: many bthreads, each looping ~50x over a mix of the reused work
|
|
// units, on ONE pooled channel capped at 5 sockets. Surfaces checkout/return
|
|
// races, affinity-under-contention bugs, fd_version ABA, and the new-connection
|
|
// creation path.
|
|
TEST_F(MysqlPoolConcurrencyTest, ManyWorkersMixedScenarios) {
|
|
std::string err;
|
|
std::vector<MixWorkerArgs> args(kWorkers);
|
|
// One scratch table per worker so concurrent workers never share rows.
|
|
for (int w = 0; w < kWorkers; ++w) {
|
|
args[w].channel = &_channel;
|
|
args[w].worker_id = w;
|
|
args[w].table = butil::string_printf("pool_conc_w%d", w);
|
|
brpc::MysqlResponse resp;
|
|
ASSERT_TRUE(RunPlain(_channel, "DROP TABLE IF EXISTS " + args[w].table,
|
|
&resp, &err)) << err;
|
|
ASSERT_TRUE(RunPlain(_channel,
|
|
"CREATE TABLE " + args[w].table +
|
|
" (id INT PRIMARY KEY, name VARCHAR(32)) "
|
|
"ENGINE=InnoDB",
|
|
&resp, &err)) << err;
|
|
}
|
|
|
|
std::vector<bthread_t> threads(kWorkers);
|
|
for (int w = 0; w < kWorkers; ++w) {
|
|
ASSERT_EQ(0, bthread_start_background(&threads[w], NULL, MixWorker,
|
|
&args[w]));
|
|
}
|
|
for (int w = 0; w < kWorkers; ++w) {
|
|
bthread_join(threads[w], NULL);
|
|
}
|
|
|
|
for (int w = 0; w < kWorkers; ++w) {
|
|
EXPECT_TRUE(args[w].error.empty()) << args[w].error;
|
|
EXPECT_EQ(kIterationsPerWorker, args[w].completed)
|
|
<< "worker " << w << " did not finish all iterations";
|
|
}
|
|
|
|
// Cleanup.
|
|
for (int w = 0; w < kWorkers; ++w) {
|
|
brpc::MysqlResponse resp;
|
|
RunPlain(_channel, "DROP TABLE IF EXISTS " + args[w].table, &resp, &err);
|
|
}
|
|
}
|
|
|
|
// Focused-check worker A: one transaction, per-worker table, INSERT + SELECT,
|
|
// records its pinned SocketId and the value it read.
|
|
struct AffinityWorkerArgs {
|
|
brpc::Channel* channel;
|
|
std::string table;
|
|
int id; // value inserted (and expected back)
|
|
brpc::SocketId socket_id; // pinned socket for this txn
|
|
int64_t row_count;
|
|
int read_value;
|
|
bool committed;
|
|
std::string error;
|
|
};
|
|
|
|
void* AffinityWorker(void* p) {
|
|
AffinityWorkerArgs* a = static_cast<AffinityWorkerArgs*>(p);
|
|
a->error.clear();
|
|
a->socket_id = 0;
|
|
a->row_count = -1;
|
|
a->read_value = -1;
|
|
a->committed = false;
|
|
|
|
brpc::MysqlTransactionUniquePtr tx =
|
|
brpc::NewMysqlTransaction(*a->channel, brpc::MysqlTransactionOptions());
|
|
if (tx == NULL) { a->error = "NewMysqlTransaction NULL"; return NULL; }
|
|
a->socket_id = tx->GetSocketId();
|
|
|
|
brpc::MysqlResponse resp;
|
|
if (!RunInTx(*a->channel, tx.get(),
|
|
butil::string_printf("INSERT INTO %s VALUES (%d)",
|
|
a->table.c_str(), a->id),
|
|
&resp, &a->error)) return NULL;
|
|
if (resp.reply(0).is_error()) {
|
|
a->error = "INSERT err: " + resp.reply(0).error().msg().as_string();
|
|
return NULL;
|
|
}
|
|
// Read inside the txn: must see exactly our own row (per-worker table).
|
|
if (!RunInTx(*a->channel, tx.get(),
|
|
butil::string_printf("SELECT v FROM %s", a->table.c_str()),
|
|
&resp, &a->error)) return NULL;
|
|
a->row_count = ResultRowCount(resp);
|
|
if (a->row_count == 1) {
|
|
long long v = 0;
|
|
if (FieldToLongLong(resp.reply(0).next().field(0), &v)) {
|
|
a->read_value = (int)v;
|
|
}
|
|
}
|
|
a->committed = tx->commit();
|
|
if (!a->committed) a->error = "commit failed";
|
|
return NULL;
|
|
}
|
|
|
|
// TEST 2 (focused check a): two transactions in parallel must hold DIFFERENT
|
|
// pinned SocketIds and must not see each other's rows.
|
|
TEST_F(MysqlPoolConcurrencyTest, TwoTransactionsHoldDifferentPinnedSockets) {
|
|
const std::string t0 = "pool_conc_affinity_a";
|
|
const std::string t1 = "pool_conc_affinity_b";
|
|
std::string err;
|
|
brpc::MysqlResponse resp;
|
|
for (const std::string& t : {t0, t1}) {
|
|
ASSERT_TRUE(RunPlain(_channel, "DROP TABLE IF EXISTS " + t, &resp, &err)) << err;
|
|
ASSERT_TRUE(RunPlain(_channel,
|
|
"CREATE TABLE " + t + " (v INT) ENGINE=InnoDB",
|
|
&resp, &err)) << err;
|
|
}
|
|
|
|
for (int iter = 0; iter < kIterationsPerWorker; ++iter) {
|
|
ASSERT_TRUE(RunPlain(_channel, "TRUNCATE TABLE " + t0, &resp, &err)) << err;
|
|
ASSERT_TRUE(RunPlain(_channel, "TRUNCATE TABLE " + t1, &resp, &err)) << err;
|
|
|
|
AffinityWorkerArgs a0{&_channel, t0, 30100 + iter, 0, -1, -1, false, ""};
|
|
AffinityWorkerArgs a1{&_channel, t1, 30900 + iter, 0, -1, -1, false, ""};
|
|
|
|
bthread_t b0, b1;
|
|
ASSERT_EQ(0, bthread_start_background(&b0, NULL, AffinityWorker, &a0));
|
|
ASSERT_EQ(0, bthread_start_background(&b1, NULL, AffinityWorker, &a1));
|
|
bthread_join(b0, NULL);
|
|
bthread_join(b1, NULL);
|
|
|
|
ASSERT_TRUE(a0.error.empty()) << "iter " << iter << " txn0: " << a0.error;
|
|
ASSERT_TRUE(a1.error.empty()) << "iter " << iter << " txn1: " << a1.error;
|
|
|
|
// Each saw exactly its own single row -- no cross-talk.
|
|
EXPECT_EQ(1, a0.row_count) << "iter " << iter;
|
|
EXPECT_EQ(1, a1.row_count) << "iter " << iter;
|
|
EXPECT_EQ(a0.id, a0.read_value) << "iter " << iter;
|
|
EXPECT_EQ(a1.id, a1.read_value) << "iter " << iter;
|
|
EXPECT_TRUE(a0.committed) << "iter " << iter;
|
|
EXPECT_TRUE(a1.committed) << "iter " << iter;
|
|
|
|
// Connection affinity: two concurrent txns hold DIFFERENT pinned sockets.
|
|
EXPECT_NE(0u, a0.socket_id) << "iter " << iter;
|
|
EXPECT_NE(0u, a1.socket_id) << "iter " << iter;
|
|
EXPECT_NE(a0.socket_id, a1.socket_id)
|
|
<< "iter " << iter
|
|
<< ": two concurrent transactions shared a pooled socket! sid0="
|
|
<< a0.socket_id << " sid1=" << a1.socket_id;
|
|
}
|
|
|
|
for (const std::string& t : {t0, t1}) {
|
|
RunPlain(_channel, "DROP TABLE IF EXISTS " + t, &resp, &err);
|
|
}
|
|
}
|
|
|
|
// Focused-check worker B: a prepared statement (SELECT ? + ?) run repeatedly
|
|
// in parallel with a transaction; must return its own correct sum each time.
|
|
struct PreparedWorkerArgs {
|
|
brpc::Channel* channel;
|
|
int base; // operand seed
|
|
bool ok;
|
|
std::string error;
|
|
};
|
|
|
|
void* PreparedWorker(void* p) {
|
|
PreparedWorkerArgs* a = static_cast<PreparedWorkerArgs*>(p);
|
|
a->ok = true;
|
|
a->error.clear();
|
|
for (int k = 0; k < 4; ++k) {
|
|
std::string err;
|
|
if (!WU_PreparedArithmetic(*a->channel, a->base, k, &err)) {
|
|
a->ok = false;
|
|
a->error = err;
|
|
return NULL;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// TEST 3 (focused check b): one transaction + one prepared statement in
|
|
// parallel each return correct independent results; the prepared path must not
|
|
// disturb the transaction's pinned connection.
|
|
TEST_F(MysqlPoolConcurrencyTest, TransactionPlusPreparedInParallel) {
|
|
const std::string t = "pool_conc_txn_stmt";
|
|
std::string err;
|
|
brpc::MysqlResponse resp;
|
|
ASSERT_TRUE(RunPlain(_channel, "DROP TABLE IF EXISTS " + t, &resp, &err)) << err;
|
|
ASSERT_TRUE(RunPlain(_channel, "CREATE TABLE " + t + " (v INT) ENGINE=InnoDB",
|
|
&resp, &err)) << err;
|
|
|
|
for (int iter = 0; iter < kIterationsPerWorker; ++iter) {
|
|
ASSERT_TRUE(RunPlain(_channel, "TRUNCATE TABLE " + t, &resp, &err)) << err;
|
|
|
|
AffinityWorkerArgs ta{&_channel, t, 50500 + iter, 0, -1, -1, false, ""};
|
|
PreparedWorkerArgs pa{&_channel, 200 + iter, true, ""};
|
|
|
|
bthread_t bt, bp;
|
|
ASSERT_EQ(0, bthread_start_background(&bt, NULL, AffinityWorker, &ta));
|
|
ASSERT_EQ(0, bthread_start_background(&bp, NULL, PreparedWorker, &pa));
|
|
bthread_join(bt, NULL);
|
|
bthread_join(bp, NULL);
|
|
|
|
ASSERT_TRUE(ta.error.empty()) << "iter " << iter << " txn: " << ta.error;
|
|
ASSERT_TRUE(pa.ok) << "iter " << iter << " prepared: " << pa.error;
|
|
|
|
// Transaction result intact and isolated.
|
|
EXPECT_EQ(1, ta.row_count) << "iter " << iter;
|
|
EXPECT_EQ(ta.id, ta.read_value) << "iter " << iter;
|
|
EXPECT_TRUE(ta.committed) << "iter " << iter;
|
|
EXPECT_NE(0u, ta.socket_id) << "iter " << iter;
|
|
}
|
|
|
|
RunPlain(_channel, "DROP TABLE IF EXISTS " + t, &resp, &err);
|
|
}
|
|
|
|
// OWNER-PRIORITY CONCURRENCY CHECKS (TEST A / B / C)
|
|
//
|
|
// These three tests intentionally cap the pool SMALL relative to the number of
|
|
// concurrent workers (TEST A/B: 4 sockets vs 8 workers; TEST C: 2 sockets) so
|
|
// that workers both CONTEND for pooled sockets and FORCE new-connection
|
|
// creation, while transactions RESERVE (pull out) pooled sockets. They use
|
|
// their OWN data namespace (ids in the 120xxx/130xxx/140xxx ranges, names
|
|
// "nova"/"zephyr"/"quill") that is distinct from the reused work units above
|
|
// (71xxx/82xxx/93xxx/64xxx; aria/cory/echo/lyra) and from the sibling files.
|
|
//
|
|
// Pool size is set PER-TEST via SetCommandLineOption at the top of each test
|
|
// body (tests may share a process, so the previous test's value must not leak
|
|
// in). No ASSERT_* runs inside a bthread; every worker records into its args
|
|
// struct and the main thread asserts after join.
|
|
|
|
static void SetPoolCap(int cap) {
|
|
GFLAGS_NS::SetCommandLineOption("max_connection_pool_size",
|
|
std::to_string(cap).c_str());
|
|
}
|
|
|
|
// TEST A worker: one transaction running SEVERAL statements into its OWN
|
|
// per-worker scratch table, recording the pinned SocketId seen BEFORE every
|
|
// statement so the main thread can check intra-txn pinning and inter-txn
|
|
// isolation.
|
|
struct PinnedTxnWorkerArgs {
|
|
brpc::Channel* channel;
|
|
std::string table; // per-worker scratch table
|
|
int id; // per-worker row id (unique)
|
|
std::vector<brpc::SocketId> socket_ids; // GetSocketId() before each stmt
|
|
std::string error; // empty == ok
|
|
};
|
|
|
|
void* PinnedTxnWorker(void* p) {
|
|
PinnedTxnWorkerArgs* a = static_cast<PinnedTxnWorkerArgs*>(p);
|
|
a->error.clear();
|
|
a->socket_ids.clear();
|
|
|
|
brpc::MysqlTransactionUniquePtr tx =
|
|
brpc::NewMysqlTransaction(*a->channel, brpc::MysqlTransactionOptions());
|
|
if (tx == NULL) { a->error = "NewMysqlTransaction NULL"; return NULL; }
|
|
|
|
brpc::MysqlResponse resp;
|
|
|
|
// Statement 1: INSERT our own row.
|
|
a->socket_ids.push_back(tx->GetSocketId());
|
|
if (!RunInTx(*a->channel, tx.get(),
|
|
butil::string_printf("INSERT INTO %s VALUES (%d, 'nova')",
|
|
a->table.c_str(), a->id),
|
|
&resp, &a->error)) return NULL;
|
|
if (resp.reply(0).is_error()) {
|
|
a->error = "INSERT err: " + resp.reply(0).error().msg().as_string();
|
|
return NULL;
|
|
}
|
|
|
|
// Statement 2: SELECT our own (uncommitted) row back.
|
|
a->socket_ids.push_back(tx->GetSocketId());
|
|
if (!RunInTx(*a->channel, tx.get(),
|
|
butil::string_printf("SELECT name FROM %s WHERE id=%d",
|
|
a->table.c_str(), a->id),
|
|
&resp, &a->error)) return NULL;
|
|
if (ResultRowCount(resp) != 1 ||
|
|
resp.reply(0).next().field(0).string().as_string() != "nova") {
|
|
a->error = "SELECT-own-row did not read back its own write";
|
|
return NULL;
|
|
}
|
|
|
|
// Statement 3: UPDATE our own row.
|
|
a->socket_ids.push_back(tx->GetSocketId());
|
|
if (!RunInTx(*a->channel, tx.get(),
|
|
butil::string_printf("UPDATE %s SET name='zephyr' WHERE id=%d",
|
|
a->table.c_str(), a->id),
|
|
&resp, &a->error)) return NULL;
|
|
if (resp.reply(0).is_error()) {
|
|
a->error = "UPDATE err: " + resp.reply(0).error().msg().as_string();
|
|
return NULL;
|
|
}
|
|
|
|
// Statement 4: SELECT the updated value back.
|
|
a->socket_ids.push_back(tx->GetSocketId());
|
|
if (!RunInTx(*a->channel, tx.get(),
|
|
butil::string_printf("SELECT name FROM %s WHERE id=%d",
|
|
a->table.c_str(), a->id),
|
|
&resp, &a->error)) return NULL;
|
|
if (ResultRowCount(resp) != 1 ||
|
|
resp.reply(0).next().field(0).string().as_string() != "zephyr") {
|
|
a->error = "SELECT after UPDATE saw wrong value";
|
|
return NULL;
|
|
}
|
|
|
|
// Discard so the per-worker table is empty for the next outer-loop pass.
|
|
if (!tx->rollback()) { a->error = "rollback failed"; return NULL; }
|
|
return NULL;
|
|
}
|
|
|
|
// TEST A: ConcurrentTxnsStayPinned (the most important check)
|
|
//
|
|
// kTxns overlapping transactions, each on its own bthread, on the POOLED
|
|
// channel with the pool capped SMALL (4) relative to kTxns (8) so they contend
|
|
// AND force new-connection creation. Each txn runs 4 statements into its OWN
|
|
// scratch table and snapshots tx->GetSocketId() before every statement.
|
|
// Asserts (on the main thread, after join):
|
|
// * INTRA-txn: the SocketId is CONSTANT across a transaction's statements
|
|
// (the txn is pinned to one connection);
|
|
// * INTER-txn: the live SocketIds are DISTINCT across the concurrent txns
|
|
// (no two simultaneously-open transactions share a pooled connection).
|
|
// The whole thing loops a few times to shake scheduling.
|
|
TEST_F(MysqlPoolConcurrencyTest, ConcurrentTxnsStayPinned) {
|
|
const int kTxns = 8;
|
|
SetPoolCap(4); // SMALL vs kTxns=8: contend + force new connections.
|
|
|
|
std::string err;
|
|
brpc::MysqlResponse resp;
|
|
std::vector<std::string> tables(kTxns);
|
|
for (int w = 0; w < kTxns; ++w) {
|
|
tables[w] = butil::string_printf("pool_conc_pin_w%d", w);
|
|
ASSERT_TRUE(RunPlain(_channel, "DROP TABLE IF EXISTS " + tables[w],
|
|
&resp, &err)) << err;
|
|
ASSERT_TRUE(RunPlain(_channel,
|
|
"CREATE TABLE " + tables[w] +
|
|
" (id INT PRIMARY KEY, name VARCHAR(32)) "
|
|
"ENGINE=InnoDB",
|
|
&resp, &err)) << err;
|
|
}
|
|
|
|
for (int loop = 0; loop < 10; ++loop) {
|
|
std::vector<PinnedTxnWorkerArgs> args(kTxns);
|
|
for (int w = 0; w < kTxns; ++w) {
|
|
args[w].channel = &_channel;
|
|
args[w].table = tables[w];
|
|
// Unique per worker AND per loop so rows never collide.
|
|
args[w].id = 120000 + loop * 100 + w;
|
|
}
|
|
|
|
std::vector<bthread_t> threads(kTxns);
|
|
for (int w = 0; w < kTxns; ++w) {
|
|
ASSERT_EQ(0, bthread_start_background(&threads[w], NULL,
|
|
PinnedTxnWorker, &args[w]));
|
|
}
|
|
for (int w = 0; w < kTxns; ++w) {
|
|
bthread_join(threads[w], NULL);
|
|
}
|
|
|
|
// No worker errored.
|
|
for (int w = 0; w < kTxns; ++w) {
|
|
ASSERT_TRUE(args[w].error.empty())
|
|
<< "loop " << loop << " txn " << w << ": " << args[w].error;
|
|
ASSERT_EQ(4u, args[w].socket_ids.size()) << "loop " << loop;
|
|
}
|
|
|
|
// INTRA-txn pinning: one constant non-zero SocketId per transaction.
|
|
std::vector<brpc::SocketId> live(kTxns);
|
|
for (int w = 0; w < kTxns; ++w) {
|
|
const brpc::SocketId sid = args[w].socket_ids[0];
|
|
EXPECT_NE(0u, sid) << "loop " << loop << " txn " << w;
|
|
for (size_t s = 1; s < args[w].socket_ids.size(); ++s) {
|
|
EXPECT_EQ(sid, args[w].socket_ids[s])
|
|
<< "loop " << loop << " txn " << w << " stmt " << s
|
|
<< ": transaction was NOT pinned to one connection";
|
|
}
|
|
live[w] = sid;
|
|
}
|
|
|
|
// INTER-txn isolation: all live (simultaneously-open) SocketIds distinct.
|
|
for (int i = 0; i < kTxns; ++i) {
|
|
for (int j = i + 1; j < kTxns; ++j) {
|
|
EXPECT_NE(live[i], live[j])
|
|
<< "loop " << loop << ": txns " << i << " and " << j
|
|
<< " shared a pooled connection (sid=" << live[i] << ")";
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int w = 0; w < kTxns; ++w) {
|
|
RunPlain(_channel, "DROP TABLE IF EXISTS " + tables[w], &resp, &err);
|
|
}
|
|
}
|
|
|
|
// TEST B workers: an aborting-transaction mix. Mode 0 explicitly rollback()s
|
|
// after an INSERT; mode 1 simply drops the MysqlTransactionUniquePtr WITHOUT
|
|
// commit, so its destructor auto-rollbacks (see MysqlTransaction::~ in
|
|
// mysql_transaction.h). Either way the insert must NOT survive.
|
|
struct AbortWorkerArgs {
|
|
brpc::Channel* channel;
|
|
std::string table; // shared abort table
|
|
int id; // unique id this worker inserts then aborts
|
|
int mode; // 0 == explicit rollback, 1 == drop -> dtor rollback
|
|
std::string error;
|
|
};
|
|
|
|
void* AbortWorker(void* p) {
|
|
AbortWorkerArgs* a = static_cast<AbortWorkerArgs*>(p);
|
|
a->error.clear();
|
|
{
|
|
brpc::MysqlTransactionUniquePtr tx = brpc::NewMysqlTransaction(
|
|
*a->channel, brpc::MysqlTransactionOptions());
|
|
if (tx == NULL) { a->error = "NewMysqlTransaction NULL"; return NULL; }
|
|
|
|
brpc::MysqlResponse resp;
|
|
if (!RunInTx(*a->channel, tx.get(),
|
|
butil::string_printf("INSERT INTO %s VALUES (%d, 'quill')",
|
|
a->table.c_str(), a->id),
|
|
&resp, &a->error)) return NULL;
|
|
if (resp.reply(0).is_error()) {
|
|
a->error = "INSERT err: " + resp.reply(0).error().msg().as_string();
|
|
return NULL;
|
|
}
|
|
if (a->mode == 0) {
|
|
if (!tx->rollback()) { a->error = "explicit rollback failed"; return NULL; }
|
|
}
|
|
// mode 1: fall off the end of this scope -> tx dtor auto-rollbacks.
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// TEST B: ConcurrentTxnAbortAndAutoRollback
|
|
//
|
|
// Under the same small-pool contended setup, run a concurrent MIX of
|
|
// explicitly-rolled-back transactions and dropped-without-commit transactions
|
|
// (whose dtor auto-rollbacks). Exercises the reserve -> return / auto-rollback
|
|
// path under contention (the UAF/leak-prone path). After join, from a fresh
|
|
// non-tx connection, assert NONE of the aborted inserts are visible, workers
|
|
// saw no errors, and the channel is still healthy (a final pooled SELECT
|
|
// succeeds -> reserved connections were returned to the pool cleanly).
|
|
TEST_F(MysqlPoolConcurrencyTest, ConcurrentTxnAbortAndAutoRollback) {
|
|
const int kWorkersB = 8;
|
|
SetPoolCap(4); // SMALL vs 8 workers: contend + force new connections.
|
|
|
|
const std::string t = "pool_conc_abort";
|
|
std::string err;
|
|
brpc::MysqlResponse resp;
|
|
ASSERT_TRUE(RunPlain(_channel, "DROP TABLE IF EXISTS " + t, &resp, &err)) << err;
|
|
ASSERT_TRUE(RunPlain(_channel,
|
|
"CREATE TABLE " + t +
|
|
" (id INT PRIMARY KEY, name VARCHAR(32)) "
|
|
"ENGINE=InnoDB",
|
|
&resp, &err)) << err;
|
|
|
|
for (int loop = 0; loop < 6; ++loop) {
|
|
std::vector<AbortWorkerArgs> args(kWorkersB);
|
|
for (int w = 0; w < kWorkersB; ++w) {
|
|
args[w].channel = &_channel;
|
|
args[w].table = t;
|
|
args[w].id = 130000 + loop * 100 + w; // unique per loop+worker
|
|
args[w].mode = w % 2; // half explicit rollback, half dtor rollback
|
|
}
|
|
|
|
std::vector<bthread_t> threads(kWorkersB);
|
|
for (int w = 0; w < kWorkersB; ++w) {
|
|
ASSERT_EQ(0, bthread_start_background(&threads[w], NULL,
|
|
AbortWorker, &args[w]));
|
|
}
|
|
for (int w = 0; w < kWorkersB; ++w) {
|
|
bthread_join(threads[w], NULL);
|
|
}
|
|
|
|
for (int w = 0; w < kWorkersB; ++w) {
|
|
EXPECT_TRUE(args[w].error.empty())
|
|
<< "loop " << loop << " worker " << w << " (mode "
|
|
<< args[w].mode << "): " << args[w].error;
|
|
}
|
|
|
|
// Effects discarded: not a single aborted insert is visible from a
|
|
// fresh (non-tx) pooled connection.
|
|
ASSERT_TRUE(RunPlain(_channel,
|
|
"SELECT COUNT(*) FROM " + t, &resp, &err)) << err;
|
|
long long visible = -1;
|
|
ASSERT_EQ(1, ResultRowCount(resp)) << "loop " << loop;
|
|
ASSERT_TRUE(FieldToLongLong(resp.reply(0).next().field(0), &visible));
|
|
EXPECT_EQ(0, visible)
|
|
<< "loop " << loop << ": " << visible
|
|
<< " aborted/dropped insert(s) leaked into the table";
|
|
}
|
|
|
|
// Channel still healthy: a final simple SELECT on a fresh pooled request
|
|
// succeeds -> the reserved connections were returned to the pool cleanly.
|
|
ASSERT_TRUE(RunPlain(_channel, "SELECT 1", &resp, &err)) << err;
|
|
EXPECT_EQ(1, ResultRowCount(resp));
|
|
|
|
RunPlain(_channel, "DROP TABLE IF EXISTS " + t, &resp, &err);
|
|
}
|
|
|
|
// TEST C support: a transaction that RESERVES a pooled connection (pulls it out
|
|
// of the pool) and holds it for the lifetime of the worker, so that the
|
|
// prepared statement S is forced onto connections that may not have its
|
|
// server-side stmt_id cached.
|
|
struct ReserveWorkerArgs {
|
|
brpc::Channel* channel;
|
|
std::string error;
|
|
};
|
|
|
|
void* ReserveWorker(void* p) {
|
|
ReserveWorkerArgs* a = static_cast<ReserveWorkerArgs*>(p);
|
|
a->error.clear();
|
|
// Open a few short transactions in series; each pulls a pooled connection
|
|
// out (reserve) and returns it (rollback), churning which sockets are in
|
|
// the pool while S is being executed concurrently.
|
|
for (int k = 0; k < 8; ++k) {
|
|
brpc::MysqlTransactionUniquePtr tx = brpc::NewMysqlTransaction(
|
|
*a->channel, brpc::MysqlTransactionOptions());
|
|
if (tx == NULL) { a->error = "reserve: NewMysqlTransaction NULL"; return NULL; }
|
|
brpc::MysqlResponse resp;
|
|
if (!RunInTx(*a->channel, tx.get(), "SELECT 1", &resp, &a->error)) return NULL;
|
|
if (!tx->rollback()) { a->error = "reserve: rollback failed"; return NULL; }
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// Execute a shared prepared statement S with a fresh INT param and verify the
|
|
// bound value comes back correctly. S is shared across bthreads, so it lands
|
|
// on whatever pooled connection is currently free.
|
|
struct StmtExecWorkerArgs {
|
|
brpc::Channel* channel;
|
|
brpc::MysqlStatement* stmt; // shared prepared statement S
|
|
int base; // param seed (unique per worker)
|
|
std::string error;
|
|
};
|
|
|
|
void* StmtExecWorker(void* p) {
|
|
StmtExecWorkerArgs* a = static_cast<StmtExecWorkerArgs*>(p);
|
|
a->error.clear();
|
|
for (int k = 0; k < 12; ++k) {
|
|
const int32_t v = a->base + k;
|
|
brpc::MysqlRequest req(a->stmt);
|
|
if (!req.AddParam(v)) { a->error = "AddParam failed"; return NULL; }
|
|
brpc::MysqlResponse resp;
|
|
brpc::Controller cntl;
|
|
a->channel->CallMethod(NULL, &cntl, &req, &resp, NULL);
|
|
if (cntl.Failed()) { a->error = "rpc: " + cntl.ErrorText(); return NULL; }
|
|
if (resp.reply_size() < 1 || !resp.reply(0).is_resultset() ||
|
|
resp.reply(0).row_count() != 1u) {
|
|
a->error = "bad resultset for S"; return NULL;
|
|
}
|
|
long long got = 0;
|
|
if (!FieldToLongLong(resp.reply(0).next().field(0), &got) || got != v) {
|
|
a->error = butil::string_printf(
|
|
"S returned wrong value (got %lld want %d)", got, v);
|
|
return NULL;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// TEST C: PreparedRePreparesWhenConnectionStolen
|
|
//
|
|
// MECHANISM: a server-side prepared statement id is per-(SocketId, fd_version)
|
|
// -- it is meaningful only on the exact connection that ran COM_STMT_PREPARE
|
|
// (see MysqlStatement::StatementId(SocketId)/SetStatementId(SocketId,...) in
|
|
// mysql_statement.h, and the fd_version/SocketId ABA discussion in this file's
|
|
// header). When a shared MysqlStatement S lands on a pooled connection that
|
|
// does NOT have S's stmt_id cached, brpc must transparently issue a fresh
|
|
// COM_STMT_PREPARE on that connection before COM_STMT_EXECUTE, and the bound
|
|
// result must still be correct.
|
|
//
|
|
// SETUP: pool capped at 2. S = "SELECT CAST(? AS SIGNED) AS v" is created and
|
|
// executed once (caching its stmt_id on whatever connection it first landed
|
|
// on). Then background bthreads open transactions that RESERVE the two pooled
|
|
// connections (pulling S's original connection out), while other bthreads keep
|
|
// executing S with fresh params. Looped with churn so S repeatedly hits
|
|
// connections it was not prepared on. Every execute must still return the
|
|
// correct bound value; per-worker errors are recorded and asserted empty after
|
|
// join.
|
|
TEST_F(MysqlPoolConcurrencyTest, PreparedRePreparesWhenConnectionStolen) {
|
|
SetPoolCap(2); // tiny pool: 2 sockets, easy to "steal" via reserving txns.
|
|
|
|
std::string err;
|
|
brpc::MysqlResponse resp;
|
|
|
|
brpc::MysqlStatementUniquePtr S =
|
|
brpc::NewMysqlStatement(_channel, "SELECT CAST(? AS SIGNED) AS v");
|
|
ASSERT_TRUE(S != NULL);
|
|
ASSERT_EQ(1u, S->param_count());
|
|
|
|
// Execute S once to cache its stmt_id on whatever connection it lands on.
|
|
{
|
|
brpc::MysqlRequest req(S.get());
|
|
ASSERT_TRUE(req.AddParam((int32_t)140000));
|
|
brpc::Controller cntl;
|
|
_channel.CallMethod(NULL, &cntl, &req, &resp, NULL);
|
|
ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText();
|
|
ASSERT_TRUE(resp.reply_size() >= 1 && resp.reply(0).is_resultset());
|
|
long long got = 0;
|
|
ASSERT_TRUE(FieldToLongLong(resp.reply(0).next().field(0), &got));
|
|
ASSERT_EQ(140000, got);
|
|
}
|
|
|
|
const int kExecutors = 4;
|
|
const int kReservers = 3;
|
|
for (int loop = 0; loop < 30; ++loop) {
|
|
std::vector<StmtExecWorkerArgs> exec_args(kExecutors);
|
|
std::vector<ReserveWorkerArgs> res_args(kReservers);
|
|
std::vector<bthread_t> exec_threads(kExecutors);
|
|
std::vector<bthread_t> res_threads(kReservers);
|
|
|
|
for (int w = 0; w < kReservers; ++w) {
|
|
res_args[w].channel = &_channel;
|
|
ASSERT_EQ(0, bthread_start_background(&res_threads[w], NULL,
|
|
ReserveWorker, &res_args[w]));
|
|
}
|
|
for (int w = 0; w < kExecutors; ++w) {
|
|
exec_args[w].channel = &_channel;
|
|
exec_args[w].stmt = S.get();
|
|
// Unique param ranges per worker+loop so a wrong value is unambiguous.
|
|
exec_args[w].base = 141000 + loop * 1000 + w * 100;
|
|
ASSERT_EQ(0, bthread_start_background(&exec_threads[w], NULL,
|
|
StmtExecWorker, &exec_args[w]));
|
|
}
|
|
for (int w = 0; w < kReservers; ++w) {
|
|
bthread_join(res_threads[w], NULL);
|
|
}
|
|
for (int w = 0; w < kExecutors; ++w) {
|
|
bthread_join(exec_threads[w], NULL);
|
|
}
|
|
|
|
for (int w = 0; w < kReservers; ++w) {
|
|
EXPECT_TRUE(res_args[w].error.empty())
|
|
<< "loop " << loop << " reserver " << w << ": " << res_args[w].error;
|
|
}
|
|
for (int w = 0; w < kExecutors; ++w) {
|
|
EXPECT_TRUE(exec_args[w].error.empty())
|
|
<< "loop " << loop << " executor " << w << ": " << exec_args[w].error;
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace
|