Merge pull request #5277 from sysown/v3.0_5272

Fix #5272: Add mysql-select_version_forwarding variable for SELECT VERSION()
pull/5286/head
René Cannaò 4 months ago committed by GitHub
commit 591e1bca6b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -49,6 +49,22 @@ enum ps_type : uint8_t {
ps_type_execute_stmt = 0x2
};
/**
* @enum SelectVersionForwardingMode
* @brief Defines modes for handling SELECT VERSION() queries in ProxySQL.
*
* These modes control how ProxySQL responds to SELECT VERSION() queries:
* - NEVER: Always return ProxySQL's own version
* - ALWAYS: Always proxy the query to a backend server
* - SMART_FALLBACK_INTERNAL: Try to get version from backend connection, fallback to ProxySQL version
* - SMART_FALLBACK_PROXY: Try to get version from backend connection, fallback to proxying the query
*/
enum SelectVersionForwardingMode : uint8_t {
SELECT_VERSION_NEVER = 0,
SELECT_VERSION_ALWAYS = 1,
SELECT_VERSION_SMART_FALLBACK_INTERNAL = 2,
SELECT_VERSION_SMART_FALLBACK_PROXY = 3
};
//std::string proxysql_session_type_str(enum proxysql_session_type session_type);
@ -473,6 +489,18 @@ class MySQL_Session: public Base_Session<MySQL_Session, MySQL_Data_Stream, MySQL
void reset_warning_hostgroup_flag_and_release_connection();
void set_previous_status_mode3(bool allow_execute=true);
char* get_current_query(int max_length = -1);
/**
* @brief Attempts to get the server version string from a backend connection in the specified hostgroup.
* @details This function iterates through servers in the hostgroup and checks for any available
* free connections to extract the server version string. It does NOT remove the connection
* from the pool - it only peeks at the version information.
*
* @param hostgroup_id The hostgroup ID to search for backend connections.
* @return Pointer to the server version string if found, NULL otherwise.
* Note: The returned pointer points to the connection's internal data and should
* not be freed or modified. The pointer is only valid while the connection exists.
*/
char * get_backend_version_for_hostgroup(int hostgroup_id);
friend void SQLite3_Server_session_handler(MySQL_Session*, void *_pa, PtrSize_t *pkt);

@ -461,6 +461,7 @@ class MySQL_Threads_Handler
char *default_schema;
char *interfaces;
char *server_version;
int select_version_forwarding;
char *keep_multiplexing_variables;
char *default_authentication_plugin;
char *proxy_protocol_networks;

@ -1208,6 +1208,7 @@ __thread int pgsql_thread___max_stmts_cache;
__thread char *mysql_thread___default_schema;
__thread char *mysql_thread___server_version;
__thread int mysql_thread___select_version_forwarding;
__thread char *mysql_thread___keep_multiplexing_variables;
__thread char *mysql_thread___default_authentication_plugin;
__thread char *mysql_thread___proxy_protocol_networks;
@ -1511,6 +1512,7 @@ extern __thread int pgsql_thread___max_stmts_cache;
extern __thread char *mysql_thread___default_schema;
extern __thread char *mysql_thread___server_version;
extern __thread int mysql_thread___select_version_forwarding;
extern __thread char *mysql_thread___keep_multiplexing_variables;
extern __thread char *mysql_thread___default_authentication_plugin;
extern __thread char *mysql_thread___proxy_protocol_networks;

@ -7060,6 +7060,43 @@ bool MySQL_Session::handler___status_WAITING_CLIENT_DATA___STATE_SLEEP___MYSQL_C
// handle case, about SELECT_MYSQL_VERSION or SELECT VERSION()
if ((pkt->size==SELECT_MYSQL_VERSION_LEN+5 && *((char *)(pkt->ptr)+4)==(char)0x03 && strncasecmp((char *)SELECT_MYSQL_VERSION,(char *)pkt->ptr+5,pkt->size-5)==0) ||
(pkt->size==SELECT_MYSQL_VERSION_FUNC_LEN+5 && *((char *)(pkt->ptr)+4)==(char)0x03 && strncasecmp((char *)SELECT_MYSQL_VERSION_FUNC,(char *)pkt->ptr+5,pkt->size-5)==0)) {
char *version_to_return = NULL;
int mode = mysql_thread___select_version_forwarding; // SelectVersionForwardingMode enum values
if (mode == SELECT_VERSION_ALWAYS) {
// always: proxy to backend, don't handle here
return false;
}
else if (mode == SELECT_VERSION_SMART_FALLBACK_INTERNAL || mode == SELECT_VERSION_SMART_FALLBACK_PROXY) {
// smart modes: try to get version from backend connection, then fallback based on mode
int target_hg = (current_hostgroup >= 0) ? current_hostgroup : default_hostgroup;
if (target_hg >= 0) {
version_to_return = get_backend_version_for_hostgroup(target_hg);
// Check if backend is ProxySQL (to avoid recursion)
if (version_to_return && strstr(version_to_return, "ProxySQL")) {
version_to_return = NULL;
}
}
// Fallback behavior depends on mode
if (!version_to_return) {
if (mode == SELECT_VERSION_SMART_FALLBACK_INTERNAL) {
// fallback to internal (ProxySQL) version
version_to_return = mysql_thread___server_version;
} else {
// SELECT_VERSION_SMART_FALLBACK_PROXY: fallback to proxying the query
return false;
}
}
}
else {
// SELECT_VERSION_NEVER (mode 0): use ProxySQL's version
version_to_return = mysql_thread___server_version;
}
// Generate response packet with version_to_return
char buf2[32];
int l0=0;
if (pkt->size == SELECT_MYSQL_VERSION_LEN+5)
@ -7085,8 +7122,8 @@ bool MySQL_Session::handler___status_WAITING_CLIENT_DATA___STATE_SLEEP___MYSQL_C
}
char **p=(char **)malloc(sizeof(char*)*1);
unsigned long *l=(unsigned long *)malloc(sizeof(unsigned long *)*1);
l[0]= strlen(mysql_thread___server_version);
p[0]=mysql_thread___server_version;
l[0]= strlen(version_to_return);
p[0]=version_to_return;
myprot->generate_pkt_row(true,NULL,NULL,sid,1,l,p); sid++;
myds->DSS=STATE_ROW;
if (deprecate_eof_active) {
@ -7163,6 +7200,62 @@ __exit_set_destination_hostgroup:
return false;
}
char * MySQL_Session::get_backend_version_for_hostgroup(int hostgroup_id) {
// Step 1: Validate input
if (hostgroup_id < 0) {
return NULL;
}
// Step 2: Access the global MySQL_HostGroups_Manager
// MyHGM is the global instance
MySQL_HostGroups_Manager *myhgm = MyHGM;
if (!myhgm) {
return NULL;
}
// Step 3: Lock for reading
// We only have wrlock(), no rdlock() - use wrlock() for safety
myhgm->wrlock();
// Step 4: Find the hostgroup
MyHGC *myhgc = myhgm->MyHGC_lookup(hostgroup_id);
if (!myhgc) {
// Hostgroup doesn't exist
myhgm->wrunlock();
return NULL;
}
// Step 5: Check if the hostgroup has any servers
if (!myhgc->mysrvs || !myhgc->mysrvs->servers || myhgc->mysrvs->servers->len == 0) {
myhgm->wrunlock();
return NULL;
}
// Step 6: Iterate through servers in the hostgroup
for (unsigned int i = 0; i < myhgc->mysrvs->servers->len; i++) {
MySrvC *mysrvc = myhgc->mysrvs->idx(i);
if (!mysrvc) {
continue;
}
// Step 7: Check if this server has any free connections
if (mysrvc->ConnectionsFree && mysrvc->ConnectionsFree->conns_length() > 0) {
// Step 8: Peek at the first free connection WITHOUT removing it
// ConnectionsFree::index() only returns a pointer, doesn't remove from array
MySQL_Connection *myconn = mysrvc->ConnectionsFree->index(0);
if (myconn && myconn->options.server_version) {
// Found a connection with a version string
myhgm->wrunlock();
return myconn->options.server_version;
}
}
}
// Step 9: No free connections found in any server of this hostgroup
myhgm->wrunlock();
return NULL;
}
void MySQL_Session::handler___status_WAITING_CLIENT_DATA___STATE_SLEEP___MYSQL_COM_STATISTICS(PtrSize_t *pkt) {
proxy_debug(PROXY_DEBUG_MYSQL_COM, 5, "Got COM_STATISTICS packet\n");
l_free(pkt->size,pkt->ptr);

@ -466,6 +466,7 @@ static char * mysql_thread_variables_names[]= {
(char *)"poll_timeout_on_failure",
(char *)"server_capabilities",
(char *)"server_version",
(char *)"select_version_forwarding",
(char *)"keep_multiplexing_variables",
(char *)"default_authentication_plugin",
(char *)"kill_backend_connection_when_disconnect",
@ -1106,6 +1107,7 @@ MySQL_Threads_Handler::MySQL_Threads_Handler() {
variables.handle_unknown_charset=1;
variables.interfaces=strdup((char *)"");
variables.server_version=strdup((char *)"8.0.11"); // changed in 2.6.0 , was 5.5.30
variables.select_version_forwarding=3; // 0=never, 1=always, 2=smart(fallback to 0), 3=smart(fallback to 1, default)
variables.eventslog_filename=strdup((char *)""); // proxysql-mysql-eventslog is recommended
variables.eventslog_filesize=100*1024*1024;
variables.eventslog_buffer_history_size=0;
@ -2357,6 +2359,7 @@ char ** MySQL_Threads_Handler::get_variables_list() {
VariablesPointers_int["binlog_reader_connect_retry_msec"] = make_tuple(&variables.binlog_reader_connect_retry_msec, 0, 0, true);
VariablesPointers_int["eventslog_format"] = make_tuple(&variables.eventslog_format, 0, 0, true);
VariablesPointers_int["wait_timeout"] = make_tuple(&variables.wait_timeout, 0, 0, true);
VariablesPointers_int["select_version_forwarding"] = make_tuple(&variables.select_version_forwarding, 0, 3, false);
VariablesPointers_int["data_packets_history_size"] = make_tuple(&variables.data_packets_history_size, 0, 0, true);
}
@ -4182,6 +4185,7 @@ void MySQL_Thread::refresh_variables() {
REFRESH_VARIABLE_INT(connect_timeout_server_max);
REFRESH_VARIABLE_INT(free_connections_pct);
REFRESH_VARIABLE_INT(fast_forward_grace_close_ms);
REFRESH_VARIABLE_INT(select_version_forwarding);
#ifdef IDLE_THREADS
REFRESH_VARIABLE_INT(session_idle_ms);
#endif // IDLE_THREADS
@ -4349,6 +4353,7 @@ MySQL_Thread::MySQL_Thread() {
last_processing_idles=0;
__thread_MySQL_Thread_Variables_version=0;
mysql_thread___server_version=NULL;
mysql_thread___select_version_forwarding=3; // default: smart (fallback to 1)
mysql_thread___init_connect=NULL;
mysql_thread___ldap_user_variable=NULL;
mysql_thread___add_ldap_user_comment=NULL;

@ -1,6 +1,76 @@
/**
* @file mysql-select_version_without_backend-t.cpp
* @brief This TAP test validates if SELECT VERSION() works without making calls to the backend.
* @brief TAP test for validating SELECT VERSION() behavior with mysql-select_version_forwarding
*
* ## Overview
*
* This test validates the behavior of the `mysql-select_version_forwarding` variable
* when ProxySQL has NO backend servers configured. This scenario tests how ProxySQL
* handles SELECT VERSION() and SELECT @@VERSION queries when there are no available
* backend connections to peek at.
*
* ## Background
*
* Since ProxySQL 3.0.4, SELECT VERSION() queries are intercepted by ProxySQL.
* The `mysql-select_version_forwarding` variable controls this behavior with 4 modes:
*
* - Mode 0 (NEVER): Always return ProxySQL's own mysql-server_version
* - Mode 1 (ALWAYS): Always proxy the query to a backend server
* - Mode 2 (SMART_FALLBACK_INTERNAL): Try to get version from backend connection,
* fallback to ProxySQL's mysql-server_version if no connection available
* - Mode 3 (SMART_FALLBACK_PROXY, default): Try to get version from backend connection,
* fallback to proxying the query if no connection available
*
* ## Test Scenarios
*
* This test runs TWO scenarios with NO backend servers configured:
*
* ### Scenario 1: Mode 3 (smart with fallback to proxying) - EXPECTED TO FAIL
*
* When `mysql-select_version_forwarding=3` and there are NO backend servers:
* - ProxySQL tries to peek at backend connections to get the version
* - No connections exist (no backends configured)
* - Fallback behavior: ProxySQL attempts to proxy the query to a backend
* - Result: Query FAILS because there is no backend to proxy to
*
* This test explicitly verifies that mode 3 behaves correctly when there are no
* backends - it should attempt to proxy and fail, rather than returning an internal
* version incorrectly.
*
* ### Scenario 2: Mode 2 (smart with fallback to internal) - EXPECTED TO SUCCEED
*
* When `mysql-select_version_forwarding=2` and there are NO backend servers:
* - ProxySQL tries to peek at backend connections to get the version
* - No connections exist (no backends configured)
* - Fallback behavior: Return ProxySQL's mysql-server_version (internal version)
* - Result: Query SUCCEEDS and returns the configured mysql-server_version
*
* This test verifies that mode 2 provides a safe fallback that allows queries
* to succeed even when no backends are available.
*
* ## Test Execution Flow
*
* For each scenario:
* 1. Set `mysql-select_version_forwarding` to the desired mode (3 or 2)
* 2. Set `mysql-server_version` to a known test value (e.g., "8.4.6")
* 3. Delete all backend servers (ensure no backends exist)
* 4. Execute two queries:
* - "SELECT @@VERSION" - Returns ProxySQL's mysql-server_version
* - "SELECT VERSION()" - Behavior depends on mode
* 5. Verify the results match the expected behavior for the mode
*
* ## Why Both Scenarios Matter
*
* - Mode 3 is the DEFAULT and provides the most accurate version information
* by ensuring clients get the real backend version or nothing. This is
* important for clients like SQLAlchemy that need to detect MariaDB vs MySQL.
*
* - Mode 2 provides a safer fallback that maintains availability at the cost
* of potentially returning less accurate version information when no backends
* are available.
*
* Testing both ensures the feature works correctly in all configurations and
* prevents regressions in future versions.
*/
#include <unistd.h>
@ -37,32 +107,30 @@ int run_q(MYSQL *mysql, const char *q) {
return 0;
}
int main(int argc, char** argv) {
plan(2);
CommandLine cl;
if (cl.getEnv()) {
diag("Failed to get the required environmental variables.");
return exit_status();
}
MYSQL* admin = init_mysql_conn(cl.host, cl.admin_username, cl.admin_password, cl.admin_port);
if (!admin) {
fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(admin));
return exit_status();
}
MYSQL_QUERY_T(admin, MYSQL_SET_SERVER_VERSION_QUERY);
/**
* @brief Test SELECT VERSION() behavior with a specific mode
*
* @param admin Admin connection for configuration
* @param proxy Proxy connection for queries
* @param mode The mysql-select_version_forwarding mode to test
* @param expect_success Whether queries are expected to succeed
* @return int 0 on success, EXIT_FAILURE on error
*/
int test_mode(MYSQL* admin, MYSQL* proxy, int mode, bool expect_success) {
// Set the mode explicitly
char set_mode_query[128];
snprintf(set_mode_query, sizeof(set_mode_query),
"SET mysql-select_version_forwarding=%d", mode);
MYSQL_QUERY_T(admin, set_mode_query);
MYSQL_QUERY_T(admin, "LOAD MYSQL VARIABLES TO RUNTIME");
// Ensure no backends exist
MYSQL_QUERY_T(admin, "DELETE FROM mysql_servers");
MYSQL_QUERY_T(admin, "LOAD MYSQL SERVERS TO RUNTIME");
MYSQL* proxy = init_mysql_conn(cl.host, cl.username, cl.password, cl.port);
if (!proxy) {
fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(proxy));
return exit_status();
}
diag("=== Testing mode %d (expect %s) ===",
mode,
expect_success ? "SUCCESS" : "FAILURE");
const char *version_get_queries[2] = {
"SELECT @@VERSION",
@ -73,8 +141,11 @@ int main(int argc, char** argv) {
MYSQL_ROW row = nullptr;
string res_server_version;
// Attempt to run the query
int rc = run_q(proxy, version_get_queries[i]);
if (rc == 0) {
// Query succeeded - fetch result
MYSQL_RES* proxy_res = mysql_store_result(proxy);
row = mysql_fetch_row(proxy_res);
@ -85,9 +156,65 @@ int main(int argc, char** argv) {
mysql_free_result(proxy_res);
}
ok(row && (res_server_version == MYSQL_TEST_SERVER_VERSION), "Server version: %s", res_server_version.c_str());
// Verify result matches expected behavior
if (expect_success) {
// Mode 2: Query should succeed and return ProxySQL's version
bool test_passed = row && (res_server_version == MYSQL_TEST_SERVER_VERSION);
ok(test_passed,
"Mode %d: %s should return '%s' - got '%s'",
mode,
version_get_queries[i],
MYSQL_TEST_SERVER_VERSION,
res_server_version.c_str());
} else {
// Mode 3: Query should FAIL (no backend to proxy to)
bool test_passed = (row == nullptr);
ok(test_passed,
"Mode %d: %s should FAIL (no backend) - %s",
mode,
version_get_queries[i],
row ? "UNEXPECTED SUCCESS" : "correctly failed");
}
}
return 0;
}
int main(int argc, char** argv) {
// We have 2 queries × 2 modes = 4 tests total
plan(4);
CommandLine cl;
if (cl.getEnv()) {
diag("Failed to get the required environmental variables.");
return exit_status();
}
MYSQL* admin = init_mysql_conn(cl.host, cl.admin_username, cl.admin_password, cl.admin_port);
if (!admin) {
fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(admin));
return exit_status();
}
// Set the ProxySQL version that will be returned for internal fallback
MYSQL_QUERY_T(admin, MYSQL_SET_SERVER_VERSION_QUERY);
MYSQL_QUERY_T(admin, "LOAD MYSQL VARIABLES TO RUNTIME");
MYSQL* proxy = init_mysql_conn(cl.host, cl.username, cl.password, cl.port);
if (!proxy) {
fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(proxy));
mysql_close(admin);
return exit_status();
}
// Scenario 1: Mode 3 (smart with fallback to proxying) - EXPECTED TO FAIL
// With no backends, mode 3 will try to proxy the query and fail
test_mode(admin, proxy, 3, false);
// Scenario 2: Mode 2 (smart with fallback to internal) - EXPECTED TO SUCCEED
// With no backends, mode 2 will fall back to returning mysql-server_version
test_mode(admin, proxy, 2, true);
mysql_close(admin);
mysql_close(proxy);

Loading…
Cancel
Save