Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions lib/MySrvConnList.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,17 @@
// we may consider creating a new connection
{
if (decision.create_new_connection) {
// Only swap when we'd otherwise exceed the cap.
// When alive < max_connections there is room for the
// new conn, so keep the misfit free conn around — a
// later client may match it perfectly and reuse it.
// When alive >= max_connections we MUST delete the
// misfit first to keep alive bounded by max.
unsigned int alive = conns_used + conns_free;
if (alive >= (unsigned int)mysrvc->max_connections) {
MySQL_Connection* stale = (MySQL_Connection *)conns->remove_index_fast(conn_found_idx);
delete stale;

Check failure on line 259 in lib/MySrvConnList.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rewrite the code so that you no longer need this "delete".

See more on https://sonarcloud.io/project/issues?id=sysown_proxysql&issues=AZ4HxvUWAn4pVuhofrT1&open=AZ4HxvUWAn4pVuhofrT1&pullRequest=5769
}
Comment on lines +250 to +260

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

SonarCloud CI failure: replace delete stale with a scoped unique_ptr

The cap-enforcement logic is correct and symmetric with the PgSQL fix. SonarCloud flags line 259 (delete stale) with the same rule as the PgSQL file, blocking CI.

🔧 Proposed fix
-						MySQL_Connection* stale = (MySQL_Connection *)conns->remove_index_fast(conn_found_idx);
-						delete stale;
+						std::unique_ptr<MySQL_Connection> stale(
+							static_cast<MySQL_Connection*>(conns->remove_index_fast(conn_found_idx))
+						);
🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[failure] 259-259: Rewrite the code so that you no longer need this "delete".

See more on https://sonarcloud.io/project/issues?id=sysown_proxysql&issues=AZ4HxvUWAn4pVuhofrT1&open=AZ4HxvUWAn4pVuhofrT1&pullRequest=5769

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/MySrvConnList.cpp` around lines 250 - 260, The code currently calls
delete on a raw pointer returned by conns->remove_index_fast(conn_found_idx)
(stored in MySQL_Connection* stale) which SonarCloud flags; change this to
transfer the returned pointer into a scoped std::unique_ptr<MySQL_Connection>
(e.g., construct a unique_ptr from conns->remove_index_fast(...)) so the
connection is automatically deleted when the unique_ptr goes out of scope;
ensure <memory> is included and no other behavior changes in the cap-enforcement
block that references conns, conns_used, conns_free, or mysrvc->max_connections.

conn = new MySQL_Connection();
conn->parent=mysrvc;
// if attributes.multiplex == true , STATUS_MYSQL_CONNECTION_NO_MULTIPLEX_HG is set to false. And vice-versa
Expand Down
11 changes: 11 additions & 0 deletions lib/PgSQL_HostGroups_Manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2413,6 +2413,17 @@
// we may consider creating a new connection
{
if (decision.create_new_connection) {
// Only swap when we'd otherwise exceed the cap.
// When alive < max_connections there is room for the
// new conn, so keep the misfit free conn around — a
// later client may match it perfectly and reuse it.
// When alive >= max_connections we MUST delete the
// misfit first to keep alive bounded by max.
unsigned int alive = conns_used + conns_free;
if (alive >= (unsigned int)mysrvc->max_connections) {
PgSQL_Connection* stale = (PgSQL_Connection *)conns->remove_index_fast(conn_found_idx);
delete stale;

Check failure on line 2425 in lib/PgSQL_HostGroups_Manager.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rewrite the code so that you no longer need this "delete".

See more on https://sonarcloud.io/project/issues?id=sysown_proxysql&issues=AZ4HxvHUAn4pVuhofrTz&open=AZ4HxvHUAn4pVuhofrTz&pullRequest=5769
}
Comment on lines +2416 to +2426

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

SonarCloud CI failure: replace delete stale with a scoped unique_ptr

The swap-before-allocate logic is correct — conns_free/conns_used are captured before the read-only inner search and all of this runs under wrlock(), so the cached values used for alive are accurate. However, SonarCloud flags line 2425 (delete stale), blocking the CI check.

A local unique_ptr satisfies the rule with no semantic change:

🔧 Proposed fix
-						PgSQL_Connection* stale = (PgSQL_Connection *)conns->remove_index_fast(conn_found_idx);
-						delete stale;
+						std::unique_ptr<PgSQL_Connection> stale(
+							static_cast<PgSQL_Connection*>(conns->remove_index_fast(conn_found_idx))
+						);
🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[failure] 2425-2425: Rewrite the code so that you no longer need this "delete".

See more on https://sonarcloud.io/project/issues?id=sysown_proxysql&issues=AZ4HxvHUAn4pVuhofrTz&open=AZ4HxvHUAn4pVuhofrTz&pullRequest=5769

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/PgSQL_HostGroups_Manager.cpp` around lines 2416 - 2426, Replace the bare
delete on the removed connection with a scoped std::unique_ptr to satisfy the
SonarCloud rule: after calling conns->remove_index_fast(conn_found_idx) wrap the
returned PgSQL_Connection* in a std::unique_ptr<PgSQL_Connection> (e.g.
unique_ptr<PgSQL_Connection> stale(ptr)) so the object is deleted automatically
when it goes out of scope; ensure <memory> is included and leave the surrounding
logic using alive, conns_used, conns_free, mysrvc->max_connections, and
conn_found_idx unchanged.

conn = new PgSQL_Connection(false);
conn->parent=mysrvc;
// if attributes.multiplex == true , STATUS_PGSQL_CONNECTION_NO_MULTIPLEX_HG is set to false. And vice-versa
Expand Down
2 changes: 2 additions & 0 deletions test/tap/groups/groups.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
"mysql_error_classifier_unit-t" : [ "unit-tests-g1" ],
"mysql_hostgroup_attributes-servers_defaults-t" : [ "legacy-g1","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1","mysql84-g1","mysql90-g1","mysql95-g1" ],
"mysql_hostgroup_attributes_config_file-t" : [ "legacy-g1","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1","mysql84-g1","mysql90-g1","mysql95-g1" ],
"mysql_max_connections_cap_violation-t" : [ "legacy-g1","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1","mysql84-g1","mysql90-g1","mysql95-g1" ],
"mysql_query_logging_memory-t" : [ "legacy-g1","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1","mysql84-g1","mysql90-g1","mysql95-g1" ],
"mysql_reconnect_libmariadb-t" : [ "legacy-g1","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1","mysql84-g1","mysql90-g1","mysql95-g1" ],
"mysql_reconnect_libmysql-t" : [ "legacy-g1","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1","mysql84-g1","mysql90-g1","mysql95-g1" ],
Expand Down Expand Up @@ -150,6 +151,7 @@
"pgsql-extended_query_protocol_query_rules_test-t" : [ "legacy-g4","mysql-auto_increment_delay_multiplex=0-g4","mysql-multiplexing=false-g4","mysql-query_digests=0-g4","mysql-query_digests_keep_comment=1-g4" ],
"pgsql-extended_query_protocol_test-t" : [ "legacy-g4","mysql-auto_increment_delay_multiplex=0-g4","mysql-multiplexing=false-g4","mysql-query_digests=0-g4","mysql-query_digests_keep_comment=1-g4" ],
"pgsql-issue5384-t" : [ "legacy-g4","mysql-auto_increment_delay_multiplex=0-g4","mysql-multiplexing=false-g4","mysql-query_digests=0-g4","mysql-query_digests_keep_comment=1-g4" ],
"pgsql-max_connections_cap_violation-t" : [ "legacy-g6","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1","pgsql17-repl-g4" ],
"pgsql-monitor_ssl_connections_test-t" : [ "legacy-g4","mysql-auto_increment_delay_multiplex=0-g4","mysql-multiplexing=false-g4","mysql-query_digests=0-g4","mysql-query_digests_keep_comment=1-g4" ],
"pgsql-multiplex_status_test-t" : [ "legacy-g4","mysql-auto_increment_delay_multiplex=0-g4","mysql-multiplexing=false-g4","mysql-query_digests=0-g4","mysql-query_digests_keep_comment=1-g4" ],
"pgsql-notice_test-t" : [ "legacy-g6","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1" ],
Expand Down
326 changes: 326 additions & 0 deletions test/tap/tests/mysql_max_connections_cap_violation-t.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,326 @@
/**
* @file mysql_max_connections_cap_violation-t.cpp
* @brief Regression for the per-server `max_connections` cap on the MySQL path.
*
* - conns 1-3: cl.username ('testuser', frontend=1 in mysql_users)
* - conn 4 : cl.root_username ('root', frontend=1 in mysql_users) -- the
* user mismatch with conns 1-3 is what fires the matcher's
* requires_CHANGE_USER -> quality 1 -> case-1 + create_new path
* that the cap-violation fix patches.
*
* Setup avoids baseline contamination via "drain-by-reinsert":
* 1. Disable monitor; sleep `mysql-monitor_ping_interval + 1s` so any
* in-flight monitor activity has fully drained.
* 2. Find the test user's default hostgroup `hg` and pick the first
* server in it as the target.
* 3. DELETE mysql_query_rules so SELECT 1 isn't redirected to a read
* hostgroup.
* 4. DELETE FROM mysql_servers (all rows); LOAD MYSQL SERVERS TO RUNTIME.
* ProxySQL's commit() (lib/MySQL_HostGroups_Manager.cpp -- mirror
* of the pgsql commit path at PgSQL_HostGroups_Manager.cpp:1362)
* destroys every Free conn to every removed server; backend mysqld
* ends those sessions when proxysql closes the sockets.
* 5. INSERT only the target back, with status='ONLINE',
* max_connections=3; LOAD. Fresh MySQL_SrvC, empty pool, only one
* server in the hostgroup.
* 6. Open the direct-to-backend observer (cl.mysql_*) and assert that
* count(*) FROM information_schema.processlist
* WHERE user IN ('<testuser>','root') AND id != CONNECTION_ID()
* equals 0. The `id != CONNECTION_ID()` filter excludes the
* observer's own session (the observer connects as 'root' direct
* to the backend; `CONNECTION_ID()` direct-to-backend is NOT
* intercepted by proxysql -- the interceptor at
* lib/MySQL_Session.cpp:7266 only fires for the proxy frontend).
*
* Scenario:
* 1. Conn 1 (cl.username) BEGIN -- pinned, Used=1.
* 2. Conn 2 (cl.username) BEGIN -- pinned, Used=2.
* 3. Conn 3 (cl.username) SELECT 1, close -- backend conn returns to
* Free, tagged user=cl.username. Used=2, Free=1, alive=3.
* 4. Conn 4 (cl.root_username) BEGIN; SELECT 1; ROLLBACK; close --
* different user vs the lone Free conn -> requires_CHANGE_USER true
* -> quality 1 -> case-1 + create_new. With Used+Free=3=max:
* - Pre-fix: opens new without removing the misfit -> alive=4.
* - Post-fix: deletes the misfit free conn, then opens new ->
* alive=3.
* BEGIN ... ROLLBACK pins conn 4 to the write hostgroup so a stray
* query rule (if any survived the DELETE) can't redirect SELECT 1.
*
* Verification: observer counts the same filter while conn 4 is still
* open; assert `alive <= MAX_CONN`.
*
* Cleanup: close client conns and exit. We never SAVE TO DISK any
* runtime state we modified (monitor, query rules, mysql_servers).
* proxysql-tester.py runs LOAD MYSQL {SERVERS,USERS,VARIABLES,QUERY RULES}
* FROM DISK + LOAD ... TO RUNTIME before each test, which restores the
* original on-disk state. The test never touches `mysql_users`.
*
* `stats_mysql_connection_pool` is deliberately not used at all:
* reading it runs a side-effect cleanup loop in `SQL3_Connection_Pool`
* that trims overshoots and would mask the bug.
*/

#include <chrono>
#include <string>
#include <thread>
#include <vector>

#include "mysql.h"

#include "command_line.h"
#include "tap.h"
#include "utils.h"

using std::string;

CommandLine cl;

static constexpr int MAX_CONN = 3;

struct SrvRow {
int hg;
string host;
int port;
string status;
int max_connections;
};

// ----- libmysql helpers -----

static MYSQL* open_conn(const char* host, int port,
const char* user, const char* pass,
const char* label)
{
MYSQL* m = mysql_init(NULL);
if (!m) return nullptr;
if (!mysql_real_connect(m, host, user, pass, NULL, port, NULL, 0)) {
diag("Connection to %s (%s:%d user=%s) failed: %s",
label, host, port, user, mysql_error(m));
mysql_close(m);
return nullptr;
}
return m;
}

static bool exec_ok(MYSQL* m, const string& q) {
if (mysql_query(m, q.c_str()) != 0) {
diag("query failed: %s -- %s", q.c_str(), mysql_error(m));
return false;
}
while (MYSQL_RES* r = mysql_store_result(m)) mysql_free_result(r);
return true;
}

static int query_one_int(MYSQL* m, const string& q) {
if (mysql_query(m, q.c_str()) != 0) return -1;
MYSQL_RES* r = mysql_store_result(m);
if (!r) return -1;
int v = -1;
if (auto row = mysql_fetch_row(r)) v = atoi(row[0]);
mysql_free_result(r);
return v;
}

static string query_one_str(MYSQL* m, const string& q) {
if (mysql_query(m, q.c_str()) != 0) return "";
MYSQL_RES* r = mysql_store_result(m);
if (!r) return "";
string v;
if (auto row = mysql_fetch_row(r)) v = row[0] ? row[0] : "";
mysql_free_result(r);
return v;
}

// ----- targeting helpers -----

static std::vector<SrvRow> load_servers(MYSQL* admin, const string& where) {
std::vector<SrvRow> rows;
string q = "SELECT hostgroup_id, hostname, port, status, max_connections "
"FROM mysql_servers " + where + " ORDER BY hostgroup_id, hostname, port";
if (mysql_query(admin, q.c_str()) != 0) return rows;
MYSQL_RES* r = mysql_store_result(admin);
if (!r) return rows;
while (MYSQL_ROW row = mysql_fetch_row(r)) {
SrvRow s;
s.hg = atoi(row[0]);
s.host = row[1];
s.port = atoi(row[2]);
s.status = row[3];
s.max_connections = atoi(row[4]);
rows.push_back(s);
}
mysql_free_result(r);
return rows;
}

/**
* Count proxy-owned backend sessions for our two test users on the
* target backend, EXCLUDING the observer's own session (which is also
* a 'root' session opened direct-to-backend by this function).
*
* direct-to-backend CONNECTION_ID() is NOT intercepted by proxysql --
* the interceptor (lib/MySQL_Session.cpp:7266) only fires when the
* client connects to proxysql's frontend.
*/
static int count_test_backends(const SrvRow& target) {
MYSQL* m = open_conn(target.host.c_str(), target.port,
cl.mysql_username, cl.mysql_password,
"MySQL-direct (root observer)");
if (!m) return -1;
int n = query_one_int(m,
string("SELECT COUNT(*) FROM information_schema.processlist "
"WHERE user IN ('") + cl.username + "', '" + cl.root_username +
"') AND id != CONNECTION_ID()");
mysql_close(m);
return n;
}

// ----- main -----

int main() {
if (cl.getEnv()) {
diag("Failed to get the required environmental variables.");
return exit_status();
}

plan(6);

MYSQL* admin = open_conn(cl.host, cl.admin_port,
cl.admin_username, cl.admin_password, "ProxySQL admin");
ok(admin != nullptr, "connected to ProxySQL admin (port %d)", cl.admin_port);
if (!admin) return exit_status();

// Disable monitor for the duration of the test. We never SAVE TO DISK
// here; proxysql-tester.py reloads mysql-monitor_enabled from disk
// before the next test runs, which restores the original value.
exec_ok(admin, "SET mysql-monitor_enabled='false'");
exec_ok(admin, "LOAD MYSQL VARIABLES TO RUNTIME");
Comment on lines +193 to +197

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Abort the test when setup mutations fail.

At Line 196 onward, critical setup operations call exec_ok(...) but ignore the boolean result. If one step fails, the test may proceed with invalid preconditions.

Suggested minimal pattern
+static bool must_exec(MYSQL* m, const string& q) {
+	if (!exec_ok(m, q)) {
+		diag("setup step failed, aborting test: %s", q.c_str());
+		return false;
+	}
+	return true;
+}
...
-	exec_ok(admin, "SET mysql-monitor_enabled='false'");
-	exec_ok(admin, "LOAD MYSQL VARIABLES TO RUNTIME");
+	if (!must_exec(admin, "SET mysql-monitor_enabled='false'")) { mysql_close(admin); return exit_status(); }
+	if (!must_exec(admin, "LOAD MYSQL VARIABLES TO RUNTIME")) { mysql_close(admin); return exit_status(); }

Also applies to: 239-247, 249-255

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/tap/tests/mysql_max_connections_cap_violation-t.cpp` around lines 193 -
197, The setup uses exec_ok(admin, "...") but ignores its boolean result; if
these calls fail the test should immediately abort to avoid running with bad
preconditions. Update each exec_ok call in
mysql_max_connections_cap_violation-t.cpp (notably the block around the "SET
mysql-monitor_enabled" / "LOAD MYSQL VARIABLES TO RUNTIME" calls and the other
spots around lines 239-247 and 249-255) to check the returned bool and abort the
test on failure (e.g., fail the test/return early or call the existing test
abort helper) so that failures in exec_ok(admin, ...) stop further execution.


// Wait for any in-flight monitor activity to drain. After LOAD MYSQL
// VARIABLES the monitor thread observes the disabled flag on its
// next loop iteration; the longest sleep between iterations is
// mysql-monitor_ping_interval. Sleeping that interval + 1s grace
// guarantees no thread that was already mid-iteration can still
// fire a backend ping after this point. Pings use the monitor user
// (filtered out by count_test_backends) so this is defensive against
// any misconfig where monitor_username overlaps the test user.
{
const string ping_str = query_one_str(admin,
"SELECT variable_value FROM global_variables "
"WHERE variable_name='mysql-monitor_ping_interval'");
const int ping_ms = ping_str.empty() ? 10000 : std::stoi(ping_str);
diag("quiescing monitor: sleep %d ms (mysql-monitor_ping_interval + 1s grace)",
ping_ms + 1000);
std::this_thread::sleep_for(std::chrono::milliseconds(ping_ms + 1000));
}

// Discover the test user's default hostgroup.
int hg = query_one_int(admin,
string("SELECT default_hostgroup FROM mysql_users WHERE username='") +
cl.username + "' LIMIT 1");
ok(hg >= 0, "discovered hostgroup for '%s' (hg=%d)", cl.username, hg);
if (hg < 0) { mysql_close(admin); return exit_status(); }

// Pick the target = first server in the user's default hostgroup.
std::vector<SrvRow> hg_servers = load_servers(admin,
"WHERE hostgroup_id=" + std::to_string(hg));
ok(!hg_servers.empty(),
"hostgroup %d has at least one server (got %zu)",
hg, hg_servers.size());
if (hg_servers.empty()) { mysql_close(admin); return exit_status(); }

const SrvRow target = hg_servers.front();
diag("targeting backend %s:%d in hg=%d", target.host.c_str(), target.port, hg);

// DELETE all query rules so our SELECT 1 isn't redirected to a read
// hostgroup. We never SAVE TO DISK here; proxysql-tester.py reloads
// mysql_query_rules from disk before the next test, which puts the
// original ruleset back.
exec_ok(admin, "DELETE FROM mysql_query_rules");
exec_ok(admin, "LOAD MYSQL QUERY RULES TO RUNTIME");

// Drain-by-reinsert: DELETE every mysql_servers row, then LOAD.
// ProxySQL's commit() treats every row as removed and destroys every
// Free conn to those servers, ending the backend sessions cleanly.
exec_ok(admin, "DELETE FROM mysql_servers");
exec_ok(admin, "LOAD MYSQL SERVERS TO RUNTIME");

// Re-INSERT only the target with the test cap. Fresh empty pool.
exec_ok(admin,
"INSERT INTO mysql_servers (hostgroup_id, hostname, port, status, "
"max_connections) VALUES (" + std::to_string(hg) + ", '" +
target.host + "', " + std::to_string(target.port) +
", 'ONLINE', " + std::to_string(MAX_CONN) + ")");
exec_ok(admin, "LOAD MYSQL SERVERS TO RUNTIME");

std::this_thread::sleep_for(std::chrono::milliseconds(300));

// Baseline-sanity: the drain ended every proxysql-managed session for
// these users on the target. Anything still present is foreign and
// would invalidate the absolute invariant below.
int baseline = count_test_backends(target);
ok(baseline == 0,
"baseline after drain on %s:%d for users {'%s','%s'} = %d (expect 0)",
target.host.c_str(), target.port, cl.username, cl.root_username,
baseline);

// Conn 1: BEGIN -- pins the conn (Used=1).
MYSQL* c1 = open_conn(cl.host, cl.port, cl.username, cl.password,
"proxy as test user (c1)");
bool c1_ok = c1 && exec_ok(c1, "BEGIN");
// Conn 2: BEGIN -- pins the conn (Used=2).
MYSQL* c2 = open_conn(cl.host, cl.port, cl.username, cl.password,
"proxy as test user (c2)");
bool c2_ok = c2 && exec_ok(c2, "BEGIN");
ok(c1_ok && c2_ok, "two BEGIN-locked conns established as '%s'", cl.username);

// Conn 3: short-lived as cl.username -- closes -> backend conn returns
// to ConnectionsFree tagged with cl.username. Used=2, Free=1, alive=3.
{
MYSQL* c3 = open_conn(cl.host, cl.port, cl.username, cl.password,
"proxy as test user (c3)");
if (c3) { exec_ok(c3, "SELECT 1"); mysql_close(c3); }
}
std::this_thread::sleep_for(std::chrono::milliseconds(300));

// Conn 4: as cl.root_username -- user mismatch with the lone Free conn
// drives the matcher to quality=1 -> case 1 + create_new path. The
// BEGIN ... ROLLBACK wrapper pins to the write hostgroup so SELECT 1
// can't get redirected even if a stray rule sneaks in.
int after_victim = -1;
{
MYSQL* c4 = open_conn(cl.host, cl.port,
cl.root_username, cl.root_password,
"proxy as root (c4)");
if (c4) {
exec_ok(c4, "BEGIN");
exec_ok(c4, "SELECT 1");
std::this_thread::sleep_for(std::chrono::milliseconds(300));
after_victim = count_test_backends(target);
exec_ok(c4, "ROLLBACK");
mysql_close(c4);
}
}

diag("after victim: alive=%d baseline=%d (max=%d)",
after_victim, baseline, MAX_CONN);

// With baseline=0 by construction, alive_after_victim is exactly the
// count of OUR conns still alive on the backend. Patched: 3 (case-1
// swap evicted conn 3's backend session). Pre-fix: 4 (conn 3 leaked
// into Free, backend session still alive).
ok(after_victim > 0 && after_victim <= MAX_CONN,
"INVARIANT: backend session count <= max_connections "
"(alive=%d, max=%d). Pre-fix this FAILS at alive=max+1: case 1 + "
"create_new leaks the misfit Free conn while adding a new one to Used.",
after_victim, MAX_CONN);

// Release the two BEGIN-locked conns (close implies ROLLBACK). We do
// NOT SAVE TO DISK any of the runtime state we touched: proxysql-tester.py
// runs LOAD MYSQL {SERVERS,USERS,VARIABLES,QUERY RULES} FROM DISK +
// LOAD ... TO RUNTIME before the next test, which restores everything.
if (c1) mysql_close(c1);
if (c2) mysql_close(c2);
mysql_close(admin);
return exit_status();
}
Loading
Loading