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