From 87c83ce49609720e370acd6ea6df7c4b25d1c874 Mon Sep 17 00:00:00 2001 From: Andrew Regner Date: Mon, 8 Jun 2026 14:56:17 -0700 Subject: [PATCH 1/3] feat(query-counters): add per-hostgroup query error/success counters --- include/MySQL_HostGroups_Manager.h | 3 +++ include/ProxySQL_Admin_Tables_Definitions.h | 4 ++-- include/ProxySQL_Statistics.hpp | 4 +++- lib/Base_HostGroups_Manager.cpp | 20 +++++++++++++++++++- lib/MySQL_HostGroups_Manager.cpp | 20 +++++++++++++++++++- lib/MySrvC.cpp | 3 +++ lib/ProxySQL_Admin_Stats.cpp | 6 +++--- lib/mysql_connection.cpp | 15 +++++++++++++-- 8 files changed, 65 insertions(+), 10 deletions(-) diff --git a/include/MySQL_HostGroups_Manager.h b/include/MySQL_HostGroups_Manager.h index ef77d67429..232aaa7a65 100644 --- a/include/MySQL_HostGroups_Manager.h +++ b/include/MySQL_HostGroups_Manager.h @@ -200,6 +200,9 @@ class MySrvC { // MySQL Server Container unsigned int connect_ERR_at_time_last_detected_error; unsigned long long queries_sent; unsigned long long queries_gtid_sync; + unsigned long long queries_ok; + unsigned long long queries_err; + unsigned long long queries_warnings; unsigned long long bytes_sent; unsigned long long bytes_recv; bool shunned_automatic; diff --git a/include/ProxySQL_Admin_Tables_Definitions.h b/include/ProxySQL_Admin_Tables_Definitions.h index 46f21dfbed..0ca28f0dba 100644 --- a/include/ProxySQL_Admin_Tables_Definitions.h +++ b/include/ProxySQL_Admin_Tables_Definitions.h @@ -164,9 +164,9 @@ #define STATS_SQLITE_TABLE_MYSQL_USERS "CREATE TABLE stats_mysql_users (username VARCHAR PRIMARY KEY , frontend_connections INT NOT NULL , frontend_max_connections INT NOT NULL)" #define STATS_SQLITE_TABLE_MYSQL_COMMANDS_COUNTERS "CREATE TABLE stats_mysql_commands_counters (Command VARCHAR NOT NULL PRIMARY KEY , Total_Time_us INT NOT NULL , Total_cnt INT NOT NULL , cnt_100us INT NOT NULL , cnt_500us INT NOT NULL , cnt_1ms INT NOT NULL , cnt_5ms INT NOT NULL , cnt_10ms INT NOT NULL , cnt_50ms INT NOT NULL , cnt_100ms INT NOT NULL , cnt_500ms INT NOT NULL , cnt_1s INT NOT NULL , cnt_5s INT NOT NULL , cnt_10s INT NOT NULL , cnt_INFs)" #define STATS_SQLITE_TABLE_MYSQL_PROCESSLIST "CREATE TABLE stats_mysql_processlist (ThreadID INT NOT NULL , SessionID INTEGER PRIMARY KEY , user VARCHAR , db VARCHAR , cli_host VARCHAR , cli_port INT , hostgroup INT , l_srv_host VARCHAR , l_srv_port INT , srv_host VARCHAR , srv_port INT , command VARCHAR , time_ms INT NOT NULL , info VARCHAR , status_flags INT , extended_info VARCHAR)" -#define STATS_SQLITE_TABLE_MYSQL_CONNECTION_POOL "CREATE TABLE stats_mysql_connection_pool (hostgroup INT , srv_host VARCHAR , srv_port INT , status VARCHAR , ConnUsed INT , ConnFree INT , ConnOK INT , ConnERR INT , MaxConnUsed INT , Queries INT , Queries_GTID_sync INT , Bytes_data_sent INT , Bytes_data_recv INT , Latency_us INT)" +#define STATS_SQLITE_TABLE_MYSQL_CONNECTION_POOL "CREATE TABLE stats_mysql_connection_pool (hostgroup INT , srv_host VARCHAR , srv_port INT , status VARCHAR , ConnUsed INT , ConnFree INT , ConnOK INT , ConnERR INT , MaxConnUsed INT , Queries INT , Queries_GTID_sync INT , Bytes_data_sent INT , Bytes_data_recv INT , Latency_us INT , Queries_OK INT , Queries_ERR INT , Queries_warnings INT)" -#define STATS_SQLITE_TABLE_MYSQL_CONNECTION_POOL_RESET "CREATE TABLE stats_mysql_connection_pool_reset (hostgroup INT , srv_host VARCHAR , srv_port INT , status VARCHAR , ConnUsed INT , ConnFree INT , ConnOK INT , ConnERR INT , MaxConnUsed INT , Queries INT , Queries_GTID_sync INT , Bytes_data_sent INT , Bytes_data_recv INT , Latency_us INT)" +#define STATS_SQLITE_TABLE_MYSQL_CONNECTION_POOL_RESET "CREATE TABLE stats_mysql_connection_pool_reset (hostgroup INT , srv_host VARCHAR , srv_port INT , status VARCHAR , ConnUsed INT , ConnFree INT , ConnOK INT , ConnERR INT , MaxConnUsed INT , Queries INT , Queries_GTID_sync INT , Bytes_data_sent INT , Bytes_data_recv INT , Latency_us INT , Queries_OK INT , Queries_ERR INT , Queries_warnings INT)" #define STATS_SQLITE_TABLE_MYSQL_FREE_CONNECTIONS "CREATE TABLE stats_mysql_free_connections (fd INT NOT NULL , hostgroup INT NOT NULL , srv_host VARCHAR NOT NULL , srv_port INT NOT NULL , user VARCHAR NOT NULL , schema VARCHAR , init_connect VARCHAR , time_zone VARCHAR , sql_mode VARCHAR , autocommit VARCHAR , idle_ms INT , statistics VARCHAR , mysql_info VARCHAR)" diff --git a/include/ProxySQL_Statistics.hpp b/include/ProxySQL_Statistics.hpp index ddd9e61821..bd7eaea0ef 100644 --- a/include/ProxySQL_Statistics.hpp +++ b/include/ProxySQL_Statistics.hpp @@ -25,7 +25,9 @@ #define STATSDB_SQLITE_TABLE_HISTORY_STATS_MYSQL_CONNECTION_POOL_V2_0_10 "CREATE TABLE history_stats_mysql_connection_pool (timestamp INT NOT NULL , hostgroup INT , srv_host VARCHAR , srv_port INT , status VARCHAR , ConnUsed INT , ConnFree INT , ConnOK INT , ConnERR INT , MaxConnUsed INT , Queries INT , Queries_GTID_sync INT , Bytes_data_sent INT , Bytes_data_recv INT , Latency_us INT, PRIMARY KEY (timestamp, hostgroup , srv_host , srv_port))" -#define STATSDB_SQLITE_TABLE_HISTORY_STATS_MYSQL_CONNECTION_POOL STATSDB_SQLITE_TABLE_HISTORY_STATS_MYSQL_CONNECTION_POOL_V2_0_10 +#define STATSDB_SQLITE_TABLE_HISTORY_STATS_MYSQL_CONNECTION_POOL_V3_0_10 "CREATE TABLE history_stats_mysql_connection_pool (timestamp INT NOT NULL , hostgroup INT , srv_host VARCHAR , srv_port INT , status VARCHAR , ConnUsed INT , ConnFree INT , ConnOK INT , ConnERR INT , MaxConnUsed INT , Queries INT , Queries_GTID_sync INT , Bytes_data_sent INT , Bytes_data_recv INT , Latency_us INT , Queries_OK INT , Queries_ERR INT , Queries_warnings INT, PRIMARY KEY (timestamp, hostgroup , srv_host , srv_port))" + +#define STATSDB_SQLITE_TABLE_HISTORY_STATS_MYSQL_CONNECTION_POOL STATSDB_SQLITE_TABLE_HISTORY_STATS_MYSQL_CONNECTION_POOL_V3_0_10 #define STATSDB_SQLITE_TABLE_MYHGM_CONNECTIONS_V2_0 "CREATE TABLE myhgm_connections (timestamp INT NOT NULL, MyHGM_myconnpoll_destroy INT NOT NULL, MyHGM_myconnpoll_get INT NOT NULL, MyHGM_myconnpoll_get_ok INT NOT NULL, MyHGM_myconnpoll_push INT NOT NULL, MyHGM_myconnpoll_reset INT NOT NULL, PRIMARY KEY (timestamp))" diff --git a/lib/Base_HostGroups_Manager.cpp b/lib/Base_HostGroups_Manager.cpp index 99a85d207f..4a112800e4 100644 --- a/lib/Base_HostGroups_Manager.cpp +++ b/lib/Base_HostGroups_Manager.cpp @@ -3076,7 +3076,7 @@ void MySQL_HostGroups_Manager::p_update_connection_pool() { } SQLite3_result * MySQL_HostGroups_Manager::SQL3_Connection_Pool(bool _reset, int *hid) { - const int colnum=14; + const int colnum=17; proxy_debug(PROXY_DEBUG_MYSQL_CONNECTION, 4, "Dumping Connection Pool\n"); SQLite3_result *result=new SQLite3_result(colnum); result->add_column_definition(SQLITE_TEXT,"hostgroup"); @@ -3093,6 +3093,9 @@ SQLite3_result * MySQL_HostGroups_Manager::SQL3_Connection_Pool(bool _reset, int result->add_column_definition(SQLITE_TEXT,"Bytes_sent"); result->add_column_definition(SQLITE_TEXT,"Bytes_recv"); result->add_column_definition(SQLITE_TEXT,"Latency_us"); + result->add_column_definition(SQLITE_TEXT,"Queries_OK"); + result->add_column_definition(SQLITE_TEXT,"Queries_ERR"); + result->add_column_definition(SQLITE_TEXT,"Queries_warnings"); wrlock(); int i,j, k; for (i=0; i<(int)MyHostGroups->len; i++) { @@ -3187,6 +3190,21 @@ SQLite3_result * MySQL_HostGroups_Manager::SQL3_Connection_Pool(bool _reset, int } sprintf(buf,"%u", mysrvc->current_latency_us); pta[13]=strdup(buf); + sprintf(buf,"%llu", mysrvc->queries_ok); + pta[14]=strdup(buf); + if (_reset) { + mysrvc->queries_ok=0; + } + sprintf(buf,"%llu", mysrvc->queries_err); + pta[15]=strdup(buf); + if (_reset) { + mysrvc->queries_err=0; + } + sprintf(buf,"%llu", mysrvc->queries_warnings); + pta[16]=strdup(buf); + if (_reset) { + mysrvc->queries_warnings=0; + } result->add_row(pta); for (k=0; kadd_column_definition(SQLITE_TEXT,"hostgroup"); @@ -3441,6 +3441,9 @@ SQLite3_result * MySQL_HostGroups_Manager::SQL3_Connection_Pool(bool _reset, int result->add_column_definition(SQLITE_TEXT,"Bytes_sent"); result->add_column_definition(SQLITE_TEXT,"Bytes_recv"); result->add_column_definition(SQLITE_TEXT,"Latency_us"); + result->add_column_definition(SQLITE_TEXT,"Queries_OK"); + result->add_column_definition(SQLITE_TEXT,"Queries_ERR"); + result->add_column_definition(SQLITE_TEXT,"Queries_warnings"); wrlock(); int i,j, k; for (i=0; i<(int)MyHostGroups->len; i++) { @@ -3535,6 +3538,21 @@ SQLite3_result * MySQL_HostGroups_Manager::SQL3_Connection_Pool(bool _reset, int } sprintf(buf,"%u", mysrvc->current_latency_us); pta[13]=strdup(buf); + sprintf(buf,"%llu", mysrvc->queries_ok); + pta[14]=strdup(buf); + if (_reset) { + mysrvc->queries_ok=0; + } + sprintf(buf,"%llu", mysrvc->queries_err); + pta[15]=strdup(buf); + if (_reset) { + mysrvc->queries_err=0; + } + sprintf(buf,"%llu", mysrvc->queries_warnings); + pta[16]=strdup(buf); + if (_reset) { + mysrvc->queries_warnings=0; + } result->add_row(pta); for (k=0; kexecute("BEGIN"); statsdb->execute("DELETE FROM stats_mysql_connection_pool"); - char *a=(char *)"INSERT INTO stats_mysql_connection_pool VALUES (\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\")"; + char *a=(char *)"INSERT INTO stats_mysql_connection_pool VALUES (\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\")"; for (std::vector::iterator it = resultset->rows.begin() ; it != resultset->rows.end(); ++it) { SQLite3_row *r=*it; int arg_len=0; - for (int i=0; i<14; i++) { + for (int i=0; i<17; i++) { arg_len+=strlen(r->fields[i]); } char *query=(char *)malloc(strlen(a)+arg_len+32); - snprintf(query, strlen(a)+arg_len+32, a,r->fields[0],r->fields[1],r->fields[2],r->fields[3],r->fields[4],r->fields[5],r->fields[6],r->fields[7],r->fields[8],r->fields[9],r->fields[10],r->fields[11],r->fields[12],r->fields[13]); + snprintf(query, strlen(a)+arg_len+32, a,r->fields[0],r->fields[1],r->fields[2],r->fields[3],r->fields[4],r->fields[5],r->fields[6],r->fields[7],r->fields[8],r->fields[9],r->fields[10],r->fields[11],r->fields[12],r->fields[13],r->fields[14],r->fields[15],r->fields[16]); statsdb->execute(query); free(query); } diff --git a/lib/mysql_connection.cpp b/lib/mysql_connection.cpp index a48c36f40e..b61103b948 100644 --- a/lib/mysql_connection.cpp +++ b/lib/mysql_connection.cpp @@ -2258,9 +2258,13 @@ int MySQL_Connection::async_query(short event, char *stmt, unsigned long length, if (async_state_machine==ASYNC_QUERY_END) { PROXY_TRACE2(); compute_unknown_transaction_status(); - if (mysql_errno(mysql)) { + int _myerrno = mysql_errno(mysql); + if (_myerrno) { + if (_myerrno < 2000) __sync_fetch_and_add(&parent->queries_err, 1); return -1; } else { + __sync_fetch_and_add(&parent->queries_ok, 1); + if (warning_count > 0) __sync_fetch_and_add(&parent->queries_warnings, warning_count); return 0; } } @@ -2269,9 +2273,13 @@ int MySQL_Connection::async_query(short event, char *stmt, unsigned long length, query.stmt_meta=NULL; async_state_machine=ASYNC_QUERY_END; compute_unknown_transaction_status(); - if (mysql_stmt_errno(query.stmt)) { + unsigned int _stmt_errno = mysql_stmt_errno(query.stmt); + if (_stmt_errno) { + if (_stmt_errno < 2000) __sync_fetch_and_add(&parent->queries_err, 1); return -1; } else { + __sync_fetch_and_add(&parent->queries_ok, 1); + if (warning_count > 0) __sync_fetch_and_add(&parent->queries_warnings, warning_count); return 0; } } @@ -2279,9 +2287,12 @@ int MySQL_Connection::async_query(short event, char *stmt, unsigned long length, query.stmt_meta=NULL; compute_unknown_transaction_status(); if (async_state_machine==ASYNC_STMT_PREPARE_FAILED) { + unsigned int _prep_errno = mysql_stmt_errno(query.stmt); + if (_prep_errno < 2000) __sync_fetch_and_add(&parent->queries_err, 1); return -1; } else { *_stmt=query.stmt; + __sync_fetch_and_add(&parent->queries_ok, 1); return 0; } } From 4992753f76d74b8d255ede832b00b6bb96a9378f Mon Sep 17 00:00:00 2001 From: Andrew Regner Date: Mon, 8 Jun 2026 20:52:48 -0700 Subject: [PATCH 2/3] add test cases --- test/tap/groups/groups.json | 1 + .../tap/tests/test_query_error_counters-t.cpp | 282 ++++++++++++++++++ 2 files changed, 283 insertions(+) create mode 100644 test/tap/tests/test_query_error_counters-t.cpp diff --git a/test/tap/groups/groups.json b/test/tap/groups/groups.json index 99d41e6520..80704c04cc 100644 --- a/test/tap/groups/groups.json +++ b/test/tap/groups/groups.json @@ -406,6 +406,7 @@ "test_ps_min_gtid_fc-t" : [ "legacy-binlog-g1" ], "test_ps_no_store-t" : [ "legacy-g8","mysql-auto_increment_delay_multiplex=0-g3","mysql-multiplexing=false-g3","mysql-query_digests=0-g3","mysql-query_digests_keep_comment=1-g3","mysql84-g8","mysql90-g3","mysql95-g3" ], "test_query_cache_soft_ttl_pct-t" : [ "legacy-g9","mysql-auto_increment_delay_multiplex=0-g4","mysql-multiplexing=false-g4","mysql-query_digests=0-g4","mysql-query_digests_keep_comment=1-g4","mysql84-g4","mysql90-g4","mysql95-g4" ], + "test_query_error_counters-t" : [ "legacy-g5","mysql84-g5","mysql90-g5","mysql95-g5" ], "test_query_rules_fast_routing_algorithm-t" : [ "legacy-g9","mysql-auto_increment_delay_multiplex=0-g4","mysql-multiplexing=false-g4","mysql-query_digests=0-g4","mysql-query_digests_keep_comment=1-g4","mysql84-g4","mysql90-g4","mysql95-g4" ], "test_query_rules_routing-t" : [ "legacy-g9","mysql-auto_increment_delay_multiplex=0-g4","mysql-multiplexing=false-g4","mysql-query_digests=0-g4","mysql-query_digests_keep_comment=1-g4","mysql84-g4","mysql90-g4","mysql95-g4" ], "test_query_timeout-t" : [ "legacy-g9","mariadb10-galera-g9","mysql-auto_increment_delay_multiplex=0-g4","mysql-multiplexing=false-g4","mysql-query_digests=0-g4","mysql-query_digests_keep_comment=1-g4","mysql84-g9","mysql84-gr-g9","mysql90-g4","mysql95-g4" ], diff --git a/test/tap/tests/test_query_error_counters-t.cpp b/test/tap/tests/test_query_error_counters-t.cpp new file mode 100644 index 0000000000..8f6b660150 --- /dev/null +++ b/test/tap/tests/test_query_error_counters-t.cpp @@ -0,0 +1,282 @@ +/** + * @file test_query_error_counters-t.cpp + * @brief Verifies the per-hostgroup Queries_OK, Queries_ERR, and Queries_warnings + * columns added to stats_mysql_connection_pool. + * + * @details Test methodology: + * 1. Record baseline values for Queries_OK, Queries_ERR, Queries_warnings from + * stats_mysql_connection_pool for the default hostgroup. + * 2. Send a batch of successful queries through ProxySQL. + * Verify Queries_OK increases by the expected count. + * Verify Queries_ERR does not change. + * 3. Send a query that produces server warnings (INSERT IGNORE on a duplicate row). + * Verify Queries_warnings increases. + * 4. Point to a non-existent server, send a query, verify Queries_ERR increases. + * Error numbers >= 2000 are client-side errors (CR_*) and must NOT increment + * Queries_ERR — only server-side errors (errno < 2000) count. + */ + +#include +#include +#include +#include + +#include "mysql.h" +#include "tap.h" +#include "utils.h" +#include "command_line.h" + +using std::string; +using std::to_string; + +struct pool_counters_t { + long long queries_ok; + long long queries_err; + long long queries_warnings; +}; + +/** + * @brief Read Queries_OK, Queries_ERR, Queries_warnings from stats_mysql_connection_pool + * for a single hostgroup, summing across all servers in that hostgroup. + * @return 0 on success, -1 on failure. + */ +int fetch_query_counters(MYSQL* admin, int hostgroup, pool_counters_t& out) { + char query[256]; + snprintf(query, sizeof(query), + "SELECT SUM(Queries_OK), SUM(Queries_ERR), SUM(Queries_warnings)" + " FROM stats_mysql_connection_pool WHERE hostgroup=%d", + hostgroup + ); + + if (mysql_query(admin, query)) { + diag("fetch_query_counters: query failed: %s", mysql_error(admin)); + return -1; + } + + MYSQL_RES* res = mysql_store_result(admin); + if (!res) { + diag("fetch_query_counters: mysql_store_result failed: %s", mysql_error(admin)); + return -1; + } + + MYSQL_ROW row = mysql_fetch_row(res); + if (!row || !row[0]) { + diag("fetch_query_counters: no row for hostgroup %d", hostgroup); + mysql_free_result(res); + return -1; + } + + out.queries_ok = row[0] ? atoll(row[0]) : 0; + out.queries_err = row[1] ? atoll(row[1]) : 0; + out.queries_warnings = row[2] ? atoll(row[2]) : 0; + mysql_free_result(res); + return 0; +} + +int main(int, char**) { + CommandLine cl; + + plan(8); + + if (cl.getEnv()) { + diag("Failed to get required environment variables."); + return EXIT_FAILURE; + } + + MYSQL* admin = mysql_init(NULL); + if (!mysql_real_connect(admin, cl.host, cl.admin_username, cl.admin_password, NULL, cl.admin_port, NULL, 0)) { + fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(admin)); + return EXIT_FAILURE; + } + + MYSQL* proxy = mysql_init(NULL); + if (!mysql_real_connect(proxy, cl.host, cl.username, cl.password, NULL, cl.port, NULL, 0)) { + fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(proxy)); + mysql_close(admin); + return EXIT_FAILURE; + } + + // Determine the default hostgroup for the test user so we know which row to read. + int target_hg = 0; + { + char q[256]; + snprintf(q, sizeof(q), + "SELECT default_hostgroup FROM runtime_mysql_users WHERE username='%s' LIMIT 1", + cl.username + ); + if (mysql_query(admin, q) == 0) { + MYSQL_RES* res = mysql_store_result(admin); + MYSQL_ROW row = mysql_fetch_row(res); + if (row && row[0]) target_hg = atoi(row[0]); + mysql_free_result(res); + } + diag("Using default hostgroup %d for user '%s'", target_hg, cl.username); + } + + // ------------------------------------------------------------------------- + // Warm up: issue one query so at least one server entry exists in the pool. + // ------------------------------------------------------------------------- + mysql_query(proxy, "DO 1"); + + // ------------------------------------------------------------------------- + // Step 1: record baseline counters. + // ------------------------------------------------------------------------- + pool_counters_t baseline {}; + if (fetch_query_counters(admin, target_hg, baseline) != 0) { + diag("Failed to read baseline counters — aborting."); + mysql_close(proxy); + mysql_close(admin); + return EXIT_FAILURE; + } + diag("Baseline: Queries_OK=%lld Queries_ERR=%lld Queries_warnings=%lld", + baseline.queries_ok, baseline.queries_err, baseline.queries_warnings); + + // ------------------------------------------------------------------------- + // Step 2: send N successful queries, verify Queries_OK increments correctly + // and Queries_ERR stays the same. + // ------------------------------------------------------------------------- + const int N_OK = 5; + for (int i = 0; i < N_OK; i++) { + MYSQL_QUERY_T(proxy, "DO 1"); + } + + pool_counters_t after_ok {}; + if (fetch_query_counters(admin, target_hg, after_ok) != 0) { + diag("Failed to read counters after OK queries — aborting."); + mysql_close(proxy); + mysql_close(admin); + return EXIT_FAILURE; + } + diag("After %d OK queries: Queries_OK=%lld Queries_ERR=%lld Queries_warnings=%lld", + N_OK, after_ok.queries_ok, after_ok.queries_err, after_ok.queries_warnings); + + ok( + after_ok.queries_ok >= baseline.queries_ok + N_OK, + "Queries_OK increases by at least %d after %d successful queries" + " (baseline=%lld, after=%lld)", + N_OK, N_OK, baseline.queries_ok, after_ok.queries_ok + ); + ok( + after_ok.queries_err == baseline.queries_err, + "Queries_ERR unchanged after successful queries" + " (baseline=%lld, after=%lld)", + baseline.queries_err, after_ok.queries_err + ); + + // ------------------------------------------------------------------------- + // Step 3: generate server warnings. + // We use CREATE TABLE IF NOT EXISTS on an already-existing table: the server + // executes the statement, raises Note 1050 ("Table already exists"), and + // returns warning_count=1 with errno=0 (success). ProxySQL should add 1 to + // Queries_warnings and 1 to Queries_OK. + // ------------------------------------------------------------------------- + MYSQL_QUERY_T(proxy, "CREATE DATABASE IF NOT EXISTS test"); + MYSQL_QUERY_T(proxy, "CREATE TABLE IF NOT EXISTS test.qec_warn (id INT PRIMARY KEY)"); + + pool_counters_t pre_warn {}; + if (fetch_query_counters(admin, target_hg, pre_warn) != 0) { + diag("Failed to read pre-warning counters — aborting."); + mysql_close(proxy); + mysql_close(admin); + return EXIT_FAILURE; + } + diag("Pre-warning baseline: Queries_OK=%lld Queries_ERR=%lld Queries_warnings=%lld", + pre_warn.queries_ok, pre_warn.queries_err, pre_warn.queries_warnings); + + // Issue the same CREATE TABLE IF NOT EXISTS again — table now exists, + // server responds with errno=0 + warning_count>=1. + mysql_query(proxy, "CREATE TABLE IF NOT EXISTS test.qec_warn (id INT PRIMARY KEY)"); + + pool_counters_t after_warn {}; + if (fetch_query_counters(admin, target_hg, after_warn) != 0) { + diag("Failed to read post-warning counters — aborting."); + mysql_close(proxy); + mysql_close(admin); + return EXIT_FAILURE; + } + diag("After warning query: Queries_OK=%lld Queries_ERR=%lld Queries_warnings=%lld", + after_warn.queries_ok, after_warn.queries_err, after_warn.queries_warnings); + + ok( + after_warn.queries_ok == pre_warn.queries_ok + 1, + "Queries_OK increments by 1 for a successful-but-warning query" + " (pre=%lld, after=%lld)", + pre_warn.queries_ok, after_warn.queries_ok + ); + ok( + after_warn.queries_warnings > pre_warn.queries_warnings, + "Queries_warnings increases after a query that produces server warnings" + " (pre=%lld, after=%lld)", + pre_warn.queries_warnings, after_warn.queries_warnings + ); + ok( + after_warn.queries_err == pre_warn.queries_err, + "Queries_ERR unchanged after warning-only query" + " (pre=%lld, after=%lld)", + pre_warn.queries_err, after_warn.queries_err + ); + + // Cleanup warning test table. + mysql_query(proxy, "DROP TABLE IF EXISTS test.qec_warn"); + + // ------------------------------------------------------------------------- + // Step 4: verify Queries_ERR increments on server-side errors (errno < 2000). + // We provoke a server error against a non-existent table. This produces + // ER_NO_SUCH_TABLE (errno 1146), which is < 2000, so it must be counted. + // + // IMPORTANT: the error query must route to the same hostgroup we measure + // (target_hg, the user's default/writer hostgroup). A plain SELECT would be + // caught by read/write-split query rules (^SELECT -> reader hostgroup) and + // land on a different hostgroup than target_hg, so its Queries_ERR would not + // show up here. We therefore use an INSERT (a write), which routes to the + // default hostgroup just like the DO/CREATE statements above. + // ------------------------------------------------------------------------- + pool_counters_t pre_err {}; + if (fetch_query_counters(admin, target_hg, pre_err) != 0) { + diag("Failed to read pre-error counters — aborting."); + mysql_close(proxy); + mysql_close(admin); + return EXIT_FAILURE; + } + diag("Pre-error baseline: Queries_OK=%lld Queries_ERR=%lld Queries_warnings=%lld", + pre_err.queries_ok, pre_err.queries_err, pre_err.queries_warnings); + + // This query fails on the backend (table does not exist) — errno 1146 < 2000. + mysql_query(proxy, "INSERT INTO test.qec_nonexistent_table_for_err_counter (id) VALUES (1)"); + int err = mysql_errno(proxy); + diag("Error query produced errno=%d: %s", err, mysql_error(proxy)); + + ok( + err > 0 && err < 2000, + "Error query produced a server-side error (errno %d, must be < 2000 to count toward Queries_ERR)", + err + ); + + pool_counters_t after_err {}; + if (fetch_query_counters(admin, target_hg, after_err) != 0) { + diag("Failed to read post-error counters — aborting."); + mysql_close(proxy); + mysql_close(admin); + return EXIT_FAILURE; + } + diag("After error query: Queries_OK=%lld Queries_ERR=%lld Queries_warnings=%lld", + after_err.queries_ok, after_err.queries_err, after_err.queries_warnings); + + ok( + after_err.queries_err == pre_err.queries_err + 1, + "Queries_ERR increments by 1 after a server-side error (errno %d < 2000)" + " (pre=%lld, after=%lld)", + err, pre_err.queries_err, after_err.queries_err + ); + ok( + after_err.queries_ok == pre_err.queries_ok, + "Queries_OK unchanged after a server-side error" + " (pre=%lld, after=%lld)", + pre_err.queries_ok, after_err.queries_ok + ); + + mysql_close(proxy); + mysql_close(admin); + + return exit_status(); +} From 688b354b832926cd51f68cec86643909d33966bc Mon Sep 17 00:00:00 2001 From: Andrew Regner Date: Tue, 9 Jun 2026 09:35:35 -0700 Subject: [PATCH 3/3] defensive null check fix --- lib/mysql_connection.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mysql_connection.cpp b/lib/mysql_connection.cpp index b61103b948..7bd0f01be1 100644 --- a/lib/mysql_connection.cpp +++ b/lib/mysql_connection.cpp @@ -2287,7 +2287,7 @@ int MySQL_Connection::async_query(short event, char *stmt, unsigned long length, query.stmt_meta=NULL; compute_unknown_transaction_status(); if (async_state_machine==ASYNC_STMT_PREPARE_FAILED) { - unsigned int _prep_errno = mysql_stmt_errno(query.stmt); + unsigned int _prep_errno = query.stmt ? mysql_stmt_errno(query.stmt) : mysql_errno(mysql); if (_prep_errno < 2000) __sync_fetch_and_add(&parent->queries_err, 1); return -1; } else {