diff --git a/include/MySQL_HostGroup_Routing.h b/include/MySQL_HostGroup_Routing.h new file mode 100644 index 0000000000..35ade8df59 --- /dev/null +++ b/include/MySQL_HostGroup_Routing.h @@ -0,0 +1,62 @@ +#ifndef MYSQL_HOSTGROUP_ROUTING_H +#define MYSQL_HOSTGROUP_ROUTING_H + +#include + +/** + * @struct MySQL_Routing_Session_State + * @brief Represents the session state relevant for hostgroup routing decisions. + */ +struct MySQL_Routing_Session_State { + int current_hostgroup{-1}; + int default_hostgroup{-1}; + int locked_on_hostgroup{-1}; + int transaction_persistent_hostgroup{-1}; + int last_hg_affected_rows{-1}; + int warning_in_hg{-1}; + bool autocommit{true}; + int autocommit_on_hostgroup{-1}; + bool mirror{false}; +}; + +/** + * @struct MySQL_Routing_QPO_State + * @brief Represents the Query Processor Output relevant for hostgroup routing decisions. + */ +struct MySQL_Routing_QPO_State { + int destination_hostgroup{-1}; + bool lock_hostgroup{false}; // Derived from query parsing or QPO + bool is_show_warnings{false}; // Derived from query parsing + bool is_last_insert_id{false}; // Derived from query parsing + bool is_version_query{false}; // Derived from query parsing +}; + +/** + * @struct MySQL_Routing_Result + * @brief Represents the output of the hostgroup routing decision. + */ +struct MySQL_Routing_Result { + int new_current_hostgroup{-1}; + int new_locked_on_hostgroup{-1}; + bool lock_hostgroup{false}; + bool error{false}; + std::string error_msg; +}; + +/** + * @brief Resolves the target hostgroup and locking decisions based on session and QPO state. + * + * This is a pure function designed to be easily testable. + * + * @param sess_state Current session state. + * @param qpo_state Query Processor Output state. + * @param set_query_lock_on_hostgroup Global configuration (mysql-set_query_lock_on_hostgroup). + * @return MySQL_Routing_Result The routing decision. + */ +MySQL_Routing_Result resolve_hostgroup_routing( + const MySQL_Routing_Session_State& sess_state, + const MySQL_Routing_QPO_State& qpo_state, + int set_query_lock_on_hostgroup +); + +#endif // MYSQL_HOSTGROUP_ROUTING_H diff --git a/include/PgSQL_HostGroup_Routing.h b/include/PgSQL_HostGroup_Routing.h new file mode 100644 index 0000000000..353f0890d6 --- /dev/null +++ b/include/PgSQL_HostGroup_Routing.h @@ -0,0 +1,54 @@ +#ifndef PGSQL_HOSTGROUP_ROUTING_H +#define PGSQL_HOSTGROUP_ROUTING_H + +#include + +/** + * @struct PgSQL_Routing_Session_State + * @brief Represents the session state relevant for hostgroup routing decisions in PostgreSQL. + */ +struct PgSQL_Routing_Session_State { + int current_hostgroup{-1}; + int default_hostgroup{-1}; + int locked_on_hostgroup{-1}; + int transaction_persistent_hostgroup{-1}; +}; + +/** + * @struct PgSQL_Routing_QPO_State + * @brief Represents the Query Processor Output relevant for hostgroup routing decisions in PostgreSQL. + */ +struct PgSQL_Routing_QPO_State { + int destination_hostgroup{-1}; + bool lock_hostgroup{false}; // Derived from query parsing +}; + +/** + * @struct PgSQL_Routing_Result + * @brief Represents the output of the hostgroup routing decision for PostgreSQL. + */ +struct PgSQL_Routing_Result { + int new_current_hostgroup{-1}; + int new_locked_on_hostgroup{-1}; + bool lock_hostgroup{false}; + bool error{false}; + std::string error_msg; +}; + +/** + * @brief Resolves the target hostgroup and locking decisions based on session and QPO state. + * + * This is a pure function designed to be easily testable. + * + * @param sess_state Current session state. + * @param qpo_state Query Processor Output state. + * @param set_query_lock_on_hostgroup Global configuration (pgsql-set_query_lock_on_hostgroup). + * @return PgSQL_Routing_Result The routing decision. + */ +PgSQL_Routing_Result resolve_pgsql_hostgroup_routing( + const PgSQL_Routing_Session_State& sess_state, + const PgSQL_Routing_QPO_State& qpo_state, + int set_query_lock_on_hostgroup +); + +#endif // PGSQL_HOSTGROUP_ROUTING_H diff --git a/lib/Makefile b/lib/Makefile index f7f24075c0..4c4f7b7564 100644 --- a/lib/Makefile +++ b/lib/Makefile @@ -88,7 +88,7 @@ MYCXXFLAGS := $(STDCPP) $(MYCFLAGS) $(PSQLCH) $(PSQLGA) $(PSQL31) $(PSQLFFTO) $( default: libproxysql.a .PHONY: default -_OBJ_CXX := ProxySQL_GloVars.oo network.oo debug.oo configfile.oo Query_Cache.oo SpookyV2.oo MySQL_Authentication.oo gen_utils.oo sqlite3db.oo mysql_connection.oo MySQL_HostGroups_Manager.oo mysql_data_stream.oo MySQL_Thread.oo MySQL_Session.oo MySQL_Protocol.oo mysql_backend.oo Query_Processor.oo MySQL_Query_Processor.oo PgSQL_Query_Processor.oo ProxySQL_Admin.oo ProxySQL_Config.oo ProxySQL_Restapi.oo MySQL_Monitor.oo MySQL_Logger.oo log_utils.oo thread.oo MySQL_PreparedStatement.oo ProxySQL_Cluster.oo ClickHouse_Authentication.oo ClickHouse_Server.oo ProxySQL_Statistics.oo Chart_bundle_js.oo ProxySQL_HTTP_Server.oo ProxySQL_RESTAPI_Server.oo font-awesome.min.css.oo main-bundle.min.css.oo MySQL_Variables.oo c_tokenizer.oo proxysql_utils.oo proxysql_coredump.oo proxysql_sslkeylog.oo \ +_OBJ_CXX := ProxySQL_GloVars.oo network.oo debug.oo configfile.oo Query_Cache.oo SpookyV2.oo MySQL_Authentication.oo gen_utils.oo sqlite3db.oo mysql_connection.oo MySQL_HostGroups_Manager.oo mysql_data_stream.oo MySQL_Thread.oo MySQL_Session.oo MySQL_HostGroup_Routing.oo PgSQL_Session.oo PgSQL_HostGroup_Routing.oo MySQL_Protocol.oo mysql_backend.oo Query_Processor.oo MySQL_Query_Processor.oo PgSQL_Query_Processor.oo ProxySQL_Admin.oo ProxySQL_Config.oo ProxySQL_Restapi.oo MySQL_Monitor.oo MySQL_Logger.oo log_utils.oo thread.oo MySQL_PreparedStatement.oo ProxySQL_Cluster.oo ClickHouse_Authentication.oo ClickHouse_Server.oo ProxySQL_Statistics.oo Chart_bundle_js.oo ProxySQL_HTTP_Server.oo ProxySQL_RESTAPI_Server.oo font-awesome.min.css.oo main-bundle.min.css.oo MySQL_Variables.oo c_tokenizer.oo proxysql_utils.oo proxysql_coredump.oo proxysql_sslkeylog.oo \ sha256crypt.oo \ BaseSrvList.oo BaseHGC.oo Base_HostGroups_Manager.oo \ QP_rule_text.oo QP_query_digest_stats.oo \ diff --git a/lib/MonitorHealthDecision.cpp b/lib/MonitorHealthDecision.cpp index 5b37cb03be..83c1dbd5c0 100644 --- a/lib/MonitorHealthDecision.cpp +++ b/lib/MonitorHealthDecision.cpp @@ -11,6 +11,7 @@ */ #include "MonitorHealthDecision.h" +#include bool should_shun_on_connect_errors( unsigned int errors_this_second, diff --git a/lib/MySQL_HostGroup_Routing.cpp b/lib/MySQL_HostGroup_Routing.cpp new file mode 100644 index 0000000000..be464f3f01 --- /dev/null +++ b/lib/MySQL_HostGroup_Routing.cpp @@ -0,0 +1,72 @@ +#include "MySQL_HostGroup_Routing.h" + +MySQL_Routing_Result resolve_hostgroup_routing( + const MySQL_Routing_Session_State& sess_state, + const MySQL_Routing_QPO_State& qpo_state, + int set_query_lock_on_hostgroup +) { + MySQL_Routing_Result result; + result.new_current_hostgroup = sess_state.current_hostgroup; + result.new_locked_on_hostgroup = sess_state.locked_on_hostgroup; + result.lock_hostgroup = false; + result.error = false; + result.error_msg = ""; + + // 1. Mirroring (highest priority) + if (sess_state.mirror) { + result.new_current_hostgroup = qpo_state.destination_hostgroup; + return result; + } + + // 2. SHOW WARNINGS / SHOW COUNT(*) WARNINGS + if (qpo_state.is_show_warnings) { + if (sess_state.warning_in_hg > -1) { + result.new_current_hostgroup = sess_state.warning_in_hg; + } + return result; + } + + // 3. LAST_INSERT_ID / @@IDENTITY + if (qpo_state.is_last_insert_id) { + if (sess_state.last_hg_affected_rows >= 0) { + result.new_current_hostgroup = sess_state.last_hg_affected_rows; + return result; + } + } + + // 4. Default routing from QPO or Transaction Persistence + if (sess_state.transaction_persistent_hostgroup != -1) { + result.new_current_hostgroup = sess_state.transaction_persistent_hostgroup; + } else { + if (qpo_state.destination_hostgroup >= 0) { + result.new_current_hostgroup = qpo_state.destination_hostgroup; + } else { + // qpo_state.destination_hostgroup < 0 means no override from QPO + result.new_current_hostgroup = sess_state.default_hostgroup; + } + } + + // 5. Hostgroup Locking Decisions (mysql-set_query_lock_on_hostgroup) + if (set_query_lock_on_hostgroup == 1) { + // Algorithm introduced in ProxySQL 2.0.6 + if (result.new_locked_on_hostgroup < 0) { + if (qpo_state.lock_hostgroup) { + result.lock_hostgroup = true; + result.new_locked_on_hostgroup = result.new_current_hostgroup; + } + } + + if (result.new_locked_on_hostgroup >= 0) { + if (result.new_current_hostgroup != result.new_locked_on_hostgroup) { + result.error = true; + result.error_msg = "ProxySQL Error: connection is locked to hostgroup " + + std::to_string(result.new_locked_on_hostgroup) + + " but trying to reach hostgroup " + + std::to_string(result.new_current_hostgroup); + return result; + } + } + } + + return result; +} diff --git a/lib/MySQL_Session.cpp b/lib/MySQL_Session.cpp index 5946e95a5c..edf63b764c 100644 --- a/lib/MySQL_Session.cpp +++ b/lib/MySQL_Session.cpp @@ -23,6 +23,7 @@ using json = nlohmann::json; #include "MySQL_Authentication.hpp" #include "MySQL_LDAP_Authentication.hpp" #include "MySQL_Protocol.h" +#include "MySQL_HostGroup_Routing.h" #include "SQLite3_Server.h" #include "MySQL_Variables.h" #include "ProxySQL_Cluster.hpp" @@ -3328,34 +3329,61 @@ void MySQL_Session::handler___status_WAITING_CLIENT_DATA___STATE_SLEEP___MYSQL_C if (rc_break==true) { return; } - if (mysql_thread___set_query_lock_on_hostgroup == 1) { // algorithm introduced in 2.0.6 - if (locked_on_hostgroup < 0) { - if (lock_hostgroup) { - // we are locking on hostgroup now - locked_on_hostgroup = current_hostgroup; + + { + MySQL_Routing_Session_State sess_state = {0}; + sess_state.current_hostgroup = current_hostgroup; + sess_state.default_hostgroup = default_hostgroup; + sess_state.locked_on_hostgroup = locked_on_hostgroup; + sess_state.transaction_persistent_hostgroup = transaction_persistent_hostgroup; + sess_state.last_hg_affected_rows = last_HG_affected_rows; + sess_state.warning_in_hg = warning_in_hg; + sess_state.autocommit = autocommit; + sess_state.autocommit_on_hostgroup = autocommit_on_hostgroup; + sess_state.mirror = mirror; + + MySQL_Routing_QPO_State qpo_state = {0}; + qpo_state.destination_hostgroup = qpo->destination_hostgroup; + qpo_state.lock_hostgroup = lock_hostgroup; + if (CurrentQuery.QueryParserArgs.digest_text) { + const char* dig_text = CurrentQuery.QueryParserArgs.digest_text; + if (strcasestr(dig_text, "SHOW WARNINGS") || strcasestr(dig_text, "SHOW COUNT(*) WARNINGS")) { + qpo_state.is_show_warnings = true; } - } - if (locked_on_hostgroup >= 0) { - if (current_hostgroup != locked_on_hostgroup) { - client_myds->DSS=STATE_QUERY_SENT_NET; - int l = CurrentQuery.QueryLength; - char *end = (char *)""; - if (l>256) { - l=253; - end = (char *)"..."; - } - string nqn = string((char *)CurrentQuery.QueryPointer,l); - char *err_msg = (char *)"Session trying to reach HG %d while locked on HG %d . Rejecting query: %s"; - char *buf = (char *)malloc(strlen(err_msg)+strlen(nqn.c_str())+strlen(end)+64); - sprintf(buf, err_msg, current_hostgroup, locked_on_hostgroup, nqn.c_str(), end); - client_myds->myprot.generate_pkt_ERR(true,NULL,NULL,client_myds->pkt_sid+1,9005,(char *)"HY000",buf, true); - thread->status_variables.stvar[st_var_hostgroup_locked_queries]++; - RequestEnd(NULL, 9005, buf); - free(buf); - l_free(pkt.size,pkt.ptr); - return; + if (strcasestr(dig_text,"LAST_INSERT_ID") || strcasestr(dig_text,"@@IDENTITY")) { + qpo_state.is_last_insert_id = true; } } + + MySQL_Routing_Result res = resolve_hostgroup_routing(sess_state, qpo_state, mysql_thread___set_query_lock_on_hostgroup); + + if (res.error) { + client_myds->DSS=STATE_QUERY_SENT_NET; + int l = CurrentQuery.QueryLength; + char *end = (char *)""; + if (l>256) { + l=253; + end = (char *)"..."; + } + string nqn = string((char *)CurrentQuery.QueryPointer,l); + const char *err_msg = "Session trying to reach HG %d while locked on HG %d . Rejecting query: %s%s"; + size_t buf_size = strlen(err_msg)+strlen(nqn.c_str())+strlen(end)+64; + char *buf = (char *)malloc(buf_size); + snprintf(buf, buf_size, err_msg, res.new_current_hostgroup, locked_on_hostgroup, nqn.c_str(), end); + client_myds->myprot.generate_pkt_ERR(true,NULL,NULL,client_myds->pkt_sid+1,9005,(char *)"HY000",buf, true); + thread->status_variables.stvar[st_var_hostgroup_locked_queries]++; + RequestEnd(NULL, 9005, buf); + free(buf); + l_free(pkt.size,pkt.ptr); + return; + } + + current_hostgroup = res.new_current_hostgroup; + locked_on_hostgroup = res.new_locked_on_hostgroup; + if (res.lock_hostgroup) { + thread->status_variables.stvar[st_var_hostgroup_locked]++; + thread->status_variables.stvar[st_var_hostgroup_locked_set_cmds]++; + } } mybe=find_or_create_backend(current_hostgroup); if (client_myds->myconn->local_stmts==NULL) { @@ -3500,34 +3528,65 @@ void MySQL_Session::handler___status_WAITING_CLIENT_DATA___STATE_SLEEP___MYSQL_C if (rc_break==true) { return; } - if (mysql_thread___set_query_lock_on_hostgroup == 1) { // algorithm introduced in 2.0.6 - if (locked_on_hostgroup < 0) { - if (lock_hostgroup) { - // we are locking on hostgroup now - locked_on_hostgroup = current_hostgroup; - } + + { + MySQL_Routing_Session_State sess_state = {0}; + sess_state.current_hostgroup = current_hostgroup; + sess_state.default_hostgroup = default_hostgroup; + sess_state.locked_on_hostgroup = locked_on_hostgroup; + sess_state.transaction_persistent_hostgroup = transaction_persistent_hostgroup; + sess_state.last_hg_affected_rows = last_HG_affected_rows; + sess_state.warning_in_hg = warning_in_hg; + sess_state.autocommit = autocommit; + sess_state.autocommit_on_hostgroup = autocommit_on_hostgroup; + sess_state.mirror = mirror; + + MySQL_Routing_QPO_State qpo_state = {0}; + qpo_state.destination_hostgroup = qpo->destination_hostgroup; + qpo_state.lock_hostgroup = lock_hostgroup; + if (CurrentQuery.QueryParserArgs.digest_text) { + const char* dig_text = CurrentQuery.QueryParserArgs.digest_text; + if (strcasestr(dig_text, "SHOW WARNINGS") || strcasestr(dig_text, "SHOW COUNT(*) WARNINGS")) { + qpo_state.is_show_warnings = true; + } + if (strcasestr(dig_text,"LAST_INSERT_ID") || strcasestr(dig_text,"@@IDENTITY")) { + qpo_state.is_last_insert_id = true; + } + } else if (CurrentQuery.stmt_info && CurrentQuery.stmt_info->query) { + const char* query_text = CurrentQuery.stmt_info->query; + if (strcasestr(query_text,"LAST_INSERT_ID") || strcasestr(query_text,"@@IDENTITY")) { + qpo_state.is_last_insert_id = true; + } + } + + MySQL_Routing_Result res = resolve_hostgroup_routing(sess_state, qpo_state, mysql_thread___set_query_lock_on_hostgroup); + + if (res.error) { + client_myds->DSS=STATE_QUERY_SENT_NET; + int l = CurrentQuery.stmt_info->query_length; + char *end = (char *)""; + if (l>256) { + l=253; + end = (char *)"..."; + } + string nqn = string((char *)CurrentQuery.stmt_info->query,l); + const char *err_msg = "Session trying to reach HG %d while locked on HG %d . Rejecting query: %s%s"; + size_t buf_size = strlen(err_msg)+strlen(nqn.c_str())+strlen(end)+64; + char *buf = (char *)malloc(buf_size); + snprintf(buf, buf_size, err_msg, res.new_current_hostgroup, locked_on_hostgroup, nqn.c_str(), end); + client_myds->myprot.generate_pkt_ERR(true,NULL,NULL,client_myds->pkt_sid+1,9005,(char *)"HY000",buf, true); + thread->status_variables.stvar[st_var_hostgroup_locked_queries]++; + RequestEnd(NULL, 9005, buf); + free(buf); + l_free(pkt.size,pkt.ptr); + return; } - if (locked_on_hostgroup >= 0) { - if (current_hostgroup != locked_on_hostgroup) { - client_myds->DSS=STATE_QUERY_SENT_NET; - //int l = CurrentQuery.QueryLength; - int l = CurrentQuery.stmt_info->query_length; - char *end = (char *)""; - if (l>256) { - l=253; - end = (char *)"..."; - } - string nqn = string((char *)CurrentQuery.stmt_info->query,l); - char *err_msg = (char *)"Session trying to reach HG %d while locked on HG %d . Rejecting query: %s"; - char *buf = (char *)malloc(strlen(err_msg)+strlen(nqn.c_str())+strlen(end)+64); - sprintf(buf, err_msg, current_hostgroup, locked_on_hostgroup, nqn.c_str(), end); - client_myds->myprot.generate_pkt_ERR(true,NULL,NULL,client_myds->pkt_sid+1,9005,(char *)"HY000",buf, true); - thread->status_variables.stvar[st_var_hostgroup_locked_queries]++; - RequestEnd(NULL, 9005, buf); - free(buf); - l_free(pkt.size,pkt.ptr); - return; - } + + current_hostgroup = res.new_current_hostgroup; + locked_on_hostgroup = res.new_locked_on_hostgroup; + if (res.lock_hostgroup) { + thread->status_variables.stvar[st_var_hostgroup_locked]++; + thread->status_variables.stvar[st_var_hostgroup_locked_set_cmds]++; } } mybe=find_or_create_backend(current_hostgroup); @@ -5181,15 +5240,18 @@ int MySQL_Session::get_pkts_from_client(bool& wrong_pass, PtrSize_t& pkt) { case STATE_SLEEP: // only this section can be executed ALSO by mirror command_counters->incr(thread->curtime/1000000); if (transaction_persistent_hostgroup==-1) { - if (mysql_thread___set_query_lock_on_hostgroup == 0) { // behavior before 2.0.6 - current_hostgroup=default_hostgroup; - } else { - if (locked_on_hostgroup==-1) { - current_hostgroup = default_hostgroup; - } else { - current_hostgroup = locked_on_hostgroup; - } - } + MySQL_Routing_Session_State sess_state = {0}; + sess_state.current_hostgroup = current_hostgroup; + sess_state.default_hostgroup = default_hostgroup; + sess_state.locked_on_hostgroup = locked_on_hostgroup; + sess_state.transaction_persistent_hostgroup = transaction_persistent_hostgroup; + sess_state.mirror = mirror; + + MySQL_Routing_QPO_State qpo_state = {0}; + qpo_state.destination_hostgroup = -1; + + MySQL_Routing_Result res = resolve_hostgroup_routing(sess_state, qpo_state, mysql_thread___set_query_lock_on_hostgroup); + current_hostgroup = res.new_current_hostgroup; } proxy_debug(PROXY_DEBUG_MYSQL_CONNECTION, 5, "Session=%p , client_myds=%p . Statuses: WAITING_CLIENT_DATA - STATE_SLEEP\n", this, client_myds); @@ -5339,40 +5401,60 @@ int MySQL_Session::get_pkts_from_client(bool& wrong_pass, PtrSize_t& pkt) { if (autocommit_on_hostgroup>=0) { } - if (mysql_thread___set_query_lock_on_hostgroup == 1) { // algorithm introduced in 2.0.6 - if (locked_on_hostgroup < 0) { - if (lock_hostgroup) { - // we are locking on hostgroup now - if ( qpo->destination_hostgroup >= 0 ) { - if (transaction_persistent_hostgroup == -1) { - current_hostgroup=qpo->destination_hostgroup; - } - } - locked_on_hostgroup = current_hostgroup; - thread->status_variables.stvar[st_var_hostgroup_locked]++; - thread->status_variables.stvar[st_var_hostgroup_locked_set_cmds]++; + + { + MySQL_Routing_Session_State sess_state = {0}; + sess_state.current_hostgroup = current_hostgroup; + sess_state.default_hostgroup = default_hostgroup; + sess_state.locked_on_hostgroup = locked_on_hostgroup; + sess_state.transaction_persistent_hostgroup = transaction_persistent_hostgroup; + sess_state.last_hg_affected_rows = last_HG_affected_rows; + sess_state.warning_in_hg = warning_in_hg; + sess_state.autocommit = autocommit; + sess_state.autocommit_on_hostgroup = autocommit_on_hostgroup; + sess_state.mirror = mirror; + + MySQL_Routing_QPO_State qpo_state = {0}; + qpo_state.destination_hostgroup = qpo->destination_hostgroup; + qpo_state.lock_hostgroup = lock_hostgroup; + if (CurrentQuery.QueryParserArgs.digest_text) { + const char* dig_text = CurrentQuery.QueryParserArgs.digest_text; + if (strcasestr(dig_text, "SHOW WARNINGS") || strcasestr(dig_text, "SHOW COUNT(*) WARNINGS")) { + qpo_state.is_show_warnings = true; + } + if (strcasestr(dig_text,"LAST_INSERT_ID") || strcasestr(dig_text,"@@IDENTITY")) { + qpo_state.is_last_insert_id = true; } } - if (locked_on_hostgroup >= 0) { - if (current_hostgroup != locked_on_hostgroup) { - client_myds->DSS=STATE_QUERY_SENT_NET; - int l = CurrentQuery.QueryLength; - char *end = (char *)""; - if (l>256) { - l=253; - end = (char *)"..."; - } - string nqn = string((char *)CurrentQuery.QueryPointer,l); - char *err_msg = (char *)"Session trying to reach HG %d while locked on HG %d . Rejecting query: %s"; - char *buf = (char *)malloc(strlen(err_msg)+strlen(nqn.c_str())+strlen(end)+64); - sprintf(buf, err_msg, current_hostgroup, locked_on_hostgroup, nqn.c_str(), end); - client_myds->myprot.generate_pkt_ERR(true,NULL,NULL,client_myds->pkt_sid+1,9005,(char *)"HY000",buf, true); - thread->status_variables.stvar[st_var_hostgroup_locked_queries]++; - RequestEnd(NULL, 9005, buf); - free(buf); - l_free(pkt.size,pkt.ptr); - break; + + MySQL_Routing_Result res = resolve_hostgroup_routing(sess_state, qpo_state, mysql_thread___set_query_lock_on_hostgroup); + + if (res.error) { + client_myds->DSS=STATE_QUERY_SENT_NET; + int l = CurrentQuery.QueryLength; + char *end = (char *)""; + if (l>256) { + l=253; + end = (char *)"..."; } + string nqn = string((char *)CurrentQuery.QueryPointer,l); + const char *err_msg = "Session trying to reach HG %d while locked on HG %d . Rejecting query: %s%s"; + size_t buf_size = strlen(err_msg)+strlen(nqn.c_str())+strlen(end)+64; + char *buf = (char *)malloc(buf_size); + snprintf(buf, buf_size, err_msg, res.new_current_hostgroup, locked_on_hostgroup, nqn.c_str(), end); + client_myds->myprot.generate_pkt_ERR(true,NULL,NULL,client_myds->pkt_sid+1,9005,(char *)"HY000",buf, true); + thread->status_variables.stvar[st_var_hostgroup_locked_queries]++; + RequestEnd(NULL, 9005, buf); + free(buf); + l_free(pkt.size,pkt.ptr); + break; + } + + current_hostgroup = res.new_current_hostgroup; + locked_on_hostgroup = res.new_locked_on_hostgroup; + if (res.lock_hostgroup) { + thread->status_variables.stvar[st_var_hostgroup_locked]++; + thread->status_variables.stvar[st_var_hostgroup_locked_set_cmds]++; } } mybe=find_or_create_backend(current_hostgroup); @@ -8262,26 +8344,51 @@ bool MySQL_Session::handler___status_WAITING_CLIENT_DATA___STATE_SLEEP___MYSQL_C if ( qpo->next_query_flagIN >= 0 ) { next_query_flagIN=qpo->next_query_flagIN; } - if ( qpo->destination_hostgroup >= 0 ) { - if (transaction_persistent_hostgroup == -1) { - current_hostgroup=qpo->destination_hostgroup; + + { + MySQL_Routing_Session_State sess_state = {0}; + sess_state.current_hostgroup = current_hostgroup; + sess_state.default_hostgroup = default_hostgroup; + sess_state.locked_on_hostgroup = locked_on_hostgroup; + sess_state.transaction_persistent_hostgroup = transaction_persistent_hostgroup; + sess_state.last_hg_affected_rows = last_HG_affected_rows; + sess_state.warning_in_hg = warning_in_hg; + sess_state.autocommit = autocommit; + sess_state.autocommit_on_hostgroup = autocommit_on_hostgroup; + sess_state.mirror = mirror; + + MySQL_Routing_QPO_State qpo_state = {0}; + qpo_state.destination_hostgroup = qpo->destination_hostgroup; + qpo_state.lock_hostgroup = *lock_hostgroup; + if (CurrentQuery.QueryParserArgs.digest_text) { + const char* dig_text = CurrentQuery.QueryParserArgs.digest_text; + if (strcasestr(dig_text, "SHOW WARNINGS") || strcasestr(dig_text, "SHOW COUNT(*) WARNINGS")) { + qpo_state.is_show_warnings = true; + } + if (strcasestr(dig_text,"LAST_INSERT_ID") || strcasestr(dig_text,"@@IDENTITY")) { + qpo_state.is_last_insert_id = true; + } + } + + MySQL_Routing_Result res = resolve_hostgroup_routing(sess_state, qpo_state, mysql_thread___set_query_lock_on_hostgroup); + + if (res.error) { + client_myds->DSS=STATE_QUERY_SENT_NET; + client_myds->myprot.generate_pkt_ERR(true,NULL,NULL,client_myds->pkt_sid+1,9006,(char *)"HY000", (char*)res.error_msg.c_str()); + thread->status_variables.stvar[st_var_hostgroup_locked_queries]++; + RequestEnd(NULL, 9006, (char*)res.error_msg.c_str()); + l_free(pkt->size,pkt->ptr); + return true; } - } - if (mysql_thread___set_query_lock_on_hostgroup == 1) { // algorithm introduced in 2.0.6 - if (locked_on_hostgroup >= 0) { - if (current_hostgroup != locked_on_hostgroup) { - client_myds->DSS=STATE_QUERY_SENT_NET; - char buf[140]; - sprintf(buf,"ProxySQL Error: connection is locked to hostgroup %d but trying to reach hostgroup %d", locked_on_hostgroup, current_hostgroup); - client_myds->myprot.generate_pkt_ERR(true,NULL,NULL,client_myds->pkt_sid+1,9006,(char *)"Y0000",buf); - thread->status_variables.stvar[st_var_hostgroup_locked_queries]++; - RequestEnd(NULL, 9006, buf); - l_free(pkt->size,pkt->ptr); - return true; - } + current_hostgroup = res.new_current_hostgroup; + locked_on_hostgroup = res.new_locked_on_hostgroup; + if (res.lock_hostgroup) { + thread->status_variables.stvar[st_var_hostgroup_locked]++; + thread->status_variables.stvar[st_var_hostgroup_locked_set_cmds]++; } } + return false; } diff --git a/lib/PgSQL_HostGroup_Routing.cpp b/lib/PgSQL_HostGroup_Routing.cpp new file mode 100644 index 0000000000..b5f61399bc --- /dev/null +++ b/lib/PgSQL_HostGroup_Routing.cpp @@ -0,0 +1,50 @@ +#include "PgSQL_HostGroup_Routing.h" + +PgSQL_Routing_Result resolve_pgsql_hostgroup_routing( + const PgSQL_Routing_Session_State& sess_state, + const PgSQL_Routing_QPO_State& qpo_state, + int set_query_lock_on_hostgroup +) { + PgSQL_Routing_Result result; + result.new_current_hostgroup = sess_state.current_hostgroup; + result.new_locked_on_hostgroup = sess_state.locked_on_hostgroup; + result.lock_hostgroup = false; + result.error = false; + result.error_msg = ""; + + // 1. Default routing from QPO or Transaction Persistence + if (sess_state.transaction_persistent_hostgroup != -1) { + result.new_current_hostgroup = sess_state.transaction_persistent_hostgroup; + } else { + if (qpo_state.destination_hostgroup >= 0) { + result.new_current_hostgroup = qpo_state.destination_hostgroup; + } else { + // qpo_state.destination_hostgroup < 0 means no override from QPO + result.new_current_hostgroup = sess_state.default_hostgroup; + } + } + + // 2. Hostgroup Locking Decisions (pgsql-set_query_lock_on_hostgroup) + if (set_query_lock_on_hostgroup == 1) { + // Algorithm introduced in ProxySQL 2.0.6 + if (result.new_locked_on_hostgroup < 0) { + if (qpo_state.lock_hostgroup) { + result.lock_hostgroup = true; + result.new_locked_on_hostgroup = result.new_current_hostgroup; + } + } + + if (result.new_locked_on_hostgroup >= 0) { + if (result.new_current_hostgroup != result.new_locked_on_hostgroup) { + result.error = true; + result.error_msg = "ProxySQL Error: connection is locked to hostgroup " + + std::to_string(result.new_locked_on_hostgroup) + + " but trying to reach hostgroup " + + std::to_string(result.new_current_hostgroup); + return result; + } + } + } + + return result; +} diff --git a/lib/PgSQL_Session.cpp b/lib/PgSQL_Session.cpp index 6b7f05fd16..bc6584445c 100644 --- a/lib/PgSQL_Session.cpp +++ b/lib/PgSQL_Session.cpp @@ -15,6 +15,7 @@ using json = nlohmann::json; #include "MySQL_Data_Stream.h" #include "PgSQL_Query_Processor.h" #include "PgSQL_PreparedStatement.h" +#include "PgSQL_HostGroup_Routing.h" #include "PgSQL_Logger.hpp" #include "StatCounters.h" #include "PgSQL_Authentication.h" @@ -1974,15 +1975,17 @@ int PgSQL_Session::get_pkts_from_client(bool& wrong_pass, PtrSize_t& pkt) { case STATE_SLEEP: // only this section can be executed ALSO by mirror command_counters->incr(thread->curtime / 1000000); if (transaction_persistent_hostgroup == -1) { - if (pgsql_thread___set_query_lock_on_hostgroup == 0) { // behavior before 2.0.6 - current_hostgroup = default_hostgroup; - } else { - if (locked_on_hostgroup == -1) { - current_hostgroup = default_hostgroup; - } else { - current_hostgroup = locked_on_hostgroup; - } - } + PgSQL_Routing_Session_State sess_state = {0}; + sess_state.current_hostgroup = current_hostgroup; + sess_state.default_hostgroup = default_hostgroup; + sess_state.locked_on_hostgroup = locked_on_hostgroup; + sess_state.transaction_persistent_hostgroup = transaction_persistent_hostgroup; + + PgSQL_Routing_QPO_State qpo_state = {0}; + qpo_state.destination_hostgroup = -1; + + PgSQL_Routing_Result res = resolve_pgsql_hostgroup_routing(sess_state, qpo_state, pgsql_thread___set_query_lock_on_hostgroup); + current_hostgroup = res.new_current_hostgroup; } proxy_debug(PROXY_DEBUG_MYSQL_CONNECTION, 5, "Session=%p , client_myds=%p . Statuses: WAITING_CLIENT_DATA - STATE_SLEEP\n", this, client_myds); if (session_fast_forward) { // if it is fast forward @@ -2151,41 +2154,45 @@ int PgSQL_Session::get_pkts_from_client(bool& wrong_pass, PtrSize_t& pkt) { //handler___status_WAITING_CLIENT_DATA___STATE_SLEEP___MYSQL_COM_QUERY___create_mirror_session(); } - if (pgsql_thread___set_query_lock_on_hostgroup == 1) { // algorithm introduced in 2.0.6 - if (locked_on_hostgroup < 0) { - if (lock_hostgroup) { - // we are locking on hostgroup now - if (qpo->destination_hostgroup >= 0) { - if (transaction_persistent_hostgroup == -1) { - current_hostgroup = qpo->destination_hostgroup; - } - } - locked_on_hostgroup = current_hostgroup; - thread->status_variables.stvar[st_var_hostgroup_locked]++; - thread->status_variables.stvar[st_var_hostgroup_locked_set_cmds]++; + { + PgSQL_Routing_Session_State sess_state = {0}; + sess_state.current_hostgroup = current_hostgroup; + sess_state.default_hostgroup = default_hostgroup; + sess_state.locked_on_hostgroup = locked_on_hostgroup; + sess_state.transaction_persistent_hostgroup = transaction_persistent_hostgroup; + + PgSQL_Routing_QPO_State qpo_state = {0}; + qpo_state.destination_hostgroup = qpo->destination_hostgroup; + qpo_state.lock_hostgroup = lock_hostgroup; + + PgSQL_Routing_Result res = resolve_pgsql_hostgroup_routing(sess_state, qpo_state, pgsql_thread___set_query_lock_on_hostgroup); + + if (res.error) { + client_myds->DSS = STATE_QUERY_SENT_NET; + int l = CurrentQuery.QueryLength; + char* end = (char*)""; + if (l > 256) { + l = 253; + end = (char*)"..."; } + string nqn = string((char*)CurrentQuery.QueryPointer, l); + const char* err_msg = "Session trying to reach HG %d while locked on HG %d . Rejecting query: %s%s"; + char* buf = (char*)malloc(strlen(err_msg) + strlen(nqn.c_str()) + strlen(end) + 64); + sprintf(buf, err_msg, res.new_current_hostgroup, locked_on_hostgroup, nqn.c_str(), end); + client_myds->myprot.generate_error_packet(true, true, buf, PGSQL_ERROR_CODES::ERRCODE_RAISE_EXCEPTION, + false, true); + thread->status_variables.stvar[st_var_hostgroup_locked_queries]++; + RequestEnd(NULL, true); + free(buf); + l_free(pkt.size, pkt.ptr); + break; } - if (locked_on_hostgroup >= 0) { - if (current_hostgroup != locked_on_hostgroup) { - client_myds->DSS = STATE_QUERY_SENT_NET; - int l = CurrentQuery.QueryLength; - char* end = (char*)""; - if (l > 256) { - l = 253; - end = (char*)"..."; - } - string nqn = string((char*)CurrentQuery.QueryPointer, l); - const char* err_msg = "Session trying to reach HG %d while locked on HG %d . Rejecting query: %s%s"; - char* buf = (char*)malloc(strlen(err_msg) + strlen(nqn.c_str()) + strlen(end) + 64); - sprintf(buf, err_msg, current_hostgroup, locked_on_hostgroup, nqn.c_str(), end); - client_myds->myprot.generate_error_packet(true, true, buf, PGSQL_ERROR_CODES::ERRCODE_RAISE_EXCEPTION, - false, true); - thread->status_variables.stvar[st_var_hostgroup_locked_queries]++; - RequestEnd(NULL, true); - free(buf); - l_free(pkt.size, pkt.ptr); - break; - } + + current_hostgroup = res.new_current_hostgroup; + locked_on_hostgroup = res.new_locked_on_hostgroup; + if (res.lock_hostgroup) { + thread->status_variables.stvar[st_var_hostgroup_locked]++; + thread->status_variables.stvar[st_var_hostgroup_locked_set_cmds]++; } } mybe = find_or_create_backend(current_hostgroup); @@ -4717,24 +4724,35 @@ bool PgSQL_Session::handler___status_WAITING_CLIENT_DATA___STATE_SLEEP___PGSQL_Q next_query_flagIN = qpo->next_query_flagIN; } - if (qpo->destination_hostgroup >= 0 && transaction_persistent_hostgroup == -1) { - current_hostgroup = qpo->destination_hostgroup; - } + { + PgSQL_Routing_Session_State sess_state = {0}; + sess_state.current_hostgroup = current_hostgroup; + sess_state.default_hostgroup = default_hostgroup; + sess_state.locked_on_hostgroup = locked_on_hostgroup; + sess_state.transaction_persistent_hostgroup = transaction_persistent_hostgroup; - // Hostgroup locking check - if (pgsql_thread___set_query_lock_on_hostgroup == 1 && locked_on_hostgroup >= 0) { - if (current_hostgroup != locked_on_hostgroup) { + PgSQL_Routing_QPO_State qpo_state = {0}; + qpo_state.destination_hostgroup = qpo->destination_hostgroup; + qpo_state.lock_hostgroup = false; // Not set in this state + + PgSQL_Routing_Result res = resolve_pgsql_hostgroup_routing(sess_state, qpo_state, pgsql_thread___set_query_lock_on_hostgroup); + + if (res.error) { client_myds->DSS = STATE_QUERY_SENT_NET; - char buf[140]; - sprintf(buf, "ProxySQL Error: connection is locked to hostgroup %d but trying to reach hostgroup %d", - locked_on_hostgroup, current_hostgroup); - client_myds->myprot.generate_error_packet(true, true, buf, + client_myds->myprot.generate_error_packet(true, true, (char*)res.error_msg.c_str(), PGSQL_ERROR_CODES::ERRCODE_RAISE_EXCEPTION, false); thread->status_variables.stvar[st_var_hostgroup_locked_queries]++; RequestEnd(NULL, true); l_free(pkt->size, pkt->ptr); return true; } + + current_hostgroup = res.new_current_hostgroup; + locked_on_hostgroup = res.new_locked_on_hostgroup; + if (res.lock_hostgroup) { + thread->status_variables.stvar[st_var_hostgroup_locked]++; + thread->status_variables.stvar[st_var_hostgroup_locked_set_cmds]++; + } } return false; @@ -6109,19 +6127,30 @@ int PgSQL_Session::handle_post_sync_parse_message(PgSQL_Parse_Message* parse_msg this, client_myds, previous_hostgroup); } - if (pgsql_thread___set_query_lock_on_hostgroup == 1) { - if (locked_on_hostgroup < 0) { - if (lock_hostgroup) { - // we are locking on hostgroup now - locked_on_hostgroup = current_hostgroup; - } + { + PgSQL_Routing_Session_State sess_state = {0}; + sess_state.current_hostgroup = current_hostgroup; + sess_state.default_hostgroup = default_hostgroup; + sess_state.locked_on_hostgroup = locked_on_hostgroup; + sess_state.transaction_persistent_hostgroup = transaction_persistent_hostgroup; + + PgSQL_Routing_QPO_State qpo_state = {0}; + qpo_state.destination_hostgroup = -1; // Not relevant here as current_hostgroup was already reset to previous + qpo_state.lock_hostgroup = lock_hostgroup; + + PgSQL_Routing_Result res = resolve_pgsql_hostgroup_routing(sess_state, qpo_state, pgsql_thread___set_query_lock_on_hostgroup); + + if (res.error) { + handle_post_sync_locked_on_hostgroup_error((const char*)CurrentQuery.QueryPointer, CurrentQuery.QueryLength); + l_free(parse_pkt.size, parse_pkt.ptr); + return 2; } - if (locked_on_hostgroup >= 0) { - if (current_hostgroup != locked_on_hostgroup) { - handle_post_sync_locked_on_hostgroup_error((const char*)CurrentQuery.QueryPointer, CurrentQuery.QueryLength); - l_free(parse_pkt.size, parse_pkt.ptr); - return 2; - } + + current_hostgroup = res.new_current_hostgroup; + locked_on_hostgroup = res.new_locked_on_hostgroup; + if (res.lock_hostgroup) { + thread->status_variables.stvar[st_var_hostgroup_locked]++; + thread->status_variables.stvar[st_var_hostgroup_locked_set_cmds]++; } } @@ -6355,19 +6384,30 @@ int PgSQL_Session::handle_post_sync_describe_message(PgSQL_Describe_Message* des proxy_debug(PROXY_DEBUG_MYSQL_COM, 5, "Session=%p client_myds=%p. Using previous hostgroup '%d'\n", this, client_myds, previous_hostgroup); } - if (pgsql_thread___set_query_lock_on_hostgroup == 1) { - if (locked_on_hostgroup < 0) { - if (lock_hostgroup) { - // we are locking on hostgroup now - locked_on_hostgroup = current_hostgroup; - } + { + PgSQL_Routing_Session_State sess_state = {0}; + sess_state.current_hostgroup = current_hostgroup; + sess_state.default_hostgroup = default_hostgroup; + sess_state.locked_on_hostgroup = locked_on_hostgroup; + sess_state.transaction_persistent_hostgroup = transaction_persistent_hostgroup; + + PgSQL_Routing_QPO_State qpo_state = {0}; + qpo_state.destination_hostgroup = -1; // Not relevant here as current_hostgroup was already reset to previous + qpo_state.lock_hostgroup = lock_hostgroup; + + PgSQL_Routing_Result res = resolve_pgsql_hostgroup_routing(sess_state, qpo_state, pgsql_thread___set_query_lock_on_hostgroup); + + if (res.error) { + handle_post_sync_locked_on_hostgroup_error(CurrentQuery.extended_query_info.stmt_info->query, + CurrentQuery.extended_query_info.stmt_info->query_length); + return 2; } - if (locked_on_hostgroup >= 0) { - if (current_hostgroup != locked_on_hostgroup) { - handle_post_sync_locked_on_hostgroup_error(CurrentQuery.extended_query_info.stmt_info->query, - CurrentQuery.extended_query_info.stmt_info->query_length); - return 2; - } + + current_hostgroup = res.new_current_hostgroup; + locked_on_hostgroup = res.new_locked_on_hostgroup; + if (res.lock_hostgroup) { + thread->status_variables.stvar[st_var_hostgroup_locked]++; + thread->status_variables.stvar[st_var_hostgroup_locked_set_cmds]++; } } @@ -6501,19 +6541,30 @@ int PgSQL_Session::handle_post_sync_bind_message(PgSQL_Bind_Message* bind_msg) { this, client_myds, previous_hostgroup); } - if (pgsql_thread___set_query_lock_on_hostgroup == 1) { - if (locked_on_hostgroup < 0) { - if (lock_hostgroup) { - // we are locking on hostgroup now - locked_on_hostgroup = current_hostgroup; - } + { + PgSQL_Routing_Session_State sess_state = {0}; + sess_state.current_hostgroup = current_hostgroup; + sess_state.default_hostgroup = default_hostgroup; + sess_state.locked_on_hostgroup = locked_on_hostgroup; + sess_state.transaction_persistent_hostgroup = transaction_persistent_hostgroup; + + PgSQL_Routing_QPO_State qpo_state = {0}; + qpo_state.destination_hostgroup = -1; // Not relevant here as current_hostgroup was already reset to previous + qpo_state.lock_hostgroup = lock_hostgroup; + + PgSQL_Routing_Result res = resolve_pgsql_hostgroup_routing(sess_state, qpo_state, pgsql_thread___set_query_lock_on_hostgroup); + + if (res.error) { + handle_post_sync_locked_on_hostgroup_error(CurrentQuery.extended_query_info.stmt_info->query, + CurrentQuery.extended_query_info.stmt_info->query_length); + return 2; } - if (locked_on_hostgroup >= 0) { - if (current_hostgroup != locked_on_hostgroup) { - handle_post_sync_locked_on_hostgroup_error(CurrentQuery.extended_query_info.stmt_info->query, - CurrentQuery.extended_query_info.stmt_info->query_length); - return 2; - } + + current_hostgroup = res.new_current_hostgroup; + locked_on_hostgroup = res.new_locked_on_hostgroup; + if (res.lock_hostgroup) { + thread->status_variables.stvar[st_var_hostgroup_locked]++; + thread->status_variables.stvar[st_var_hostgroup_locked_set_cmds]++; } } @@ -6642,19 +6693,30 @@ int PgSQL_Session::handle_post_sync_execute_message(PgSQL_Execute_Message* execu this, client_myds, previous_hostgroup); } - if (pgsql_thread___set_query_lock_on_hostgroup == 1) { - if (locked_on_hostgroup < 0) { - if (lock_hostgroup) { - // we are locking on hostgroup now - locked_on_hostgroup = current_hostgroup; - } + { + PgSQL_Routing_Session_State sess_state = {0}; + sess_state.current_hostgroup = current_hostgroup; + sess_state.default_hostgroup = default_hostgroup; + sess_state.locked_on_hostgroup = locked_on_hostgroup; + sess_state.transaction_persistent_hostgroup = transaction_persistent_hostgroup; + + PgSQL_Routing_QPO_State qpo_state = {0}; + qpo_state.destination_hostgroup = -1; // Not relevant here as current_hostgroup was already reset to previous + qpo_state.lock_hostgroup = lock_hostgroup; + + PgSQL_Routing_Result res = resolve_pgsql_hostgroup_routing(sess_state, qpo_state, pgsql_thread___set_query_lock_on_hostgroup); + + if (res.error) { + handle_post_sync_locked_on_hostgroup_error(CurrentQuery.extended_query_info.stmt_info->query, + CurrentQuery.extended_query_info.stmt_info->query_length); + return 2; } - if (locked_on_hostgroup >= 0) { - if (current_hostgroup != locked_on_hostgroup) { - handle_post_sync_locked_on_hostgroup_error(CurrentQuery.extended_query_info.stmt_info->query, - CurrentQuery.extended_query_info.stmt_info->query_length); - return 2; - } + + current_hostgroup = res.new_current_hostgroup; + locked_on_hostgroup = res.new_locked_on_hostgroup; + if (res.lock_hostgroup) { + thread->status_variables.stvar[st_var_hostgroup_locked]++; + thread->status_variables.stvar[st_var_hostgroup_locked_set_cmds]++; } } diff --git a/test/tap/tests/unit/Makefile b/test/tap/tests/unit/Makefile index 0d56f1758e..45801c4245 100644 --- a/test/tap/tests/unit/Makefile +++ b/test/tap/tests/unit/Makefile @@ -231,7 +231,20 @@ $(ODIR)/test_init.o: $(TEST_HELPERS_DIR)/test_init.cpp | $(ODIR) # Unit test targets # =========================================================================== -UNIT_TESTS := smoke_test-t query_cache_unit-t query_processor_unit-t protocol_unit-t auth_unit-t connection_pool_unit-t rule_matching_unit-t hostgroups_unit-t monitor_health_unit-t +UNIT_TESTS := smoke_test-t query_cache_unit-t query_processor_unit-t \ + protocol_unit-t auth_unit-t connection_pool_unit-t \ + rule_matching_unit-t hostgroups_unit-t monitor_health_unit-t \ + pgsql_command_complete_unit-t \ + ffto_protocol_unit-t \ + server_selection_unit-t \ + hostgroup_routing_unit-t \ + transaction_state_unit-t \ + pgsql_error_classifier_unit-t \ + pgsql_monitor_unit-t \ + mysql_error_classifier_unit-t \ + backend_sync_unit-t \ + MySQL_HostGroup_Routing-t \ + PgSQL_HostGroup_Routing-t .PHONY: all all: $(UNIT_TESTS) @@ -245,47 +258,15 @@ ifneq ($(UNAME_S),Darwin) ALLOW_MULTI_DEF := -Wl,--allow-multiple-definition endif -smoke_test-t: smoke_test-t.cpp $(TEST_HELPERS_OBJ) $(LIBPROXYSQLAR) +# Pattern rule: all unit tests use the same compile + link flags. +# Each test binary is built from its .cpp source, linked against +# the test harness objects and libproxysql.a with all dependencies. +MySQL_HostGroup_Routing-t: MySQL_HostGroup_Routing-t.cpp $(TEST_HELPERS_OBJ) $(LIBPROXYSQLAR) $(CXX) $< $(TEST_HELPERS_OBJ) $(IDIRS) $(LDIRS) $(OPT) \ $(LIBPROXYSQLAR_FULL) $(STATIC_LIBS) $(MYLIBS) \ $(ALLOW_MULTI_DEF) -o $@ -query_cache_unit-t: query_cache_unit-t.cpp $(TEST_HELPERS_OBJ) $(LIBPROXYSQLAR) - $(CXX) $< $(TEST_HELPERS_OBJ) $(IDIRS) $(LDIRS) $(OPT) \ - $(LIBPROXYSQLAR_FULL) $(STATIC_LIBS) $(MYLIBS) \ - $(ALLOW_MULTI_DEF) -o $@ - -query_processor_unit-t: query_processor_unit-t.cpp $(TEST_HELPERS_OBJ) $(LIBPROXYSQLAR) - $(CXX) $< $(TEST_HELPERS_OBJ) $(IDIRS) $(LDIRS) $(OPT) \ - $(LIBPROXYSQLAR_FULL) $(STATIC_LIBS) $(MYLIBS) \ - $(ALLOW_MULTI_DEF) -o $@ - -protocol_unit-t: protocol_unit-t.cpp $(TEST_HELPERS_OBJ) $(LIBPROXYSQLAR) - $(CXX) $< $(TEST_HELPERS_OBJ) $(IDIRS) $(LDIRS) $(OPT) \ - $(LIBPROXYSQLAR_FULL) $(STATIC_LIBS) $(MYLIBS) \ - $(ALLOW_MULTI_DEF) -o $@ - -auth_unit-t: auth_unit-t.cpp $(TEST_HELPERS_OBJ) $(LIBPROXYSQLAR) - $(CXX) $< $(TEST_HELPERS_OBJ) $(IDIRS) $(LDIRS) $(OPT) \ - $(LIBPROXYSQLAR_FULL) $(STATIC_LIBS) $(MYLIBS) \ - $(ALLOW_MULTI_DEF) -o $@ - -connection_pool_unit-t: connection_pool_unit-t.cpp $(TEST_HELPERS_OBJ) $(LIBPROXYSQLAR) - $(CXX) $< $(TEST_HELPERS_OBJ) $(IDIRS) $(LDIRS) $(OPT) \ - $(LIBPROXYSQLAR_FULL) $(STATIC_LIBS) $(MYLIBS) \ - $(ALLOW_MULTI_DEF) -o $@ - -rule_matching_unit-t: rule_matching_unit-t.cpp $(TEST_HELPERS_OBJ) $(LIBPROXYSQLAR) - $(CXX) $< $(TEST_HELPERS_OBJ) $(IDIRS) $(LDIRS) $(OPT) \ - $(LIBPROXYSQLAR_FULL) $(STATIC_LIBS) $(MYLIBS) \ - $(ALLOW_MULTI_DEF) -o $@ - -hostgroups_unit-t: hostgroups_unit-t.cpp $(TEST_HELPERS_OBJ) $(LIBPROXYSQLAR) - $(CXX) $< $(TEST_HELPERS_OBJ) $(IDIRS) $(LDIRS) $(OPT) \ - $(LIBPROXYSQLAR_FULL) $(STATIC_LIBS) $(MYLIBS) \ - $(ALLOW_MULTI_DEF) -o $@ - -monitor_health_unit-t: monitor_health_unit-t.cpp $(TEST_HELPERS_OBJ) $(LIBPROXYSQLAR) +PgSQL_HostGroup_Routing-t: PgSQL_HostGroup_Routing-t.cpp $(TEST_HELPERS_OBJ) $(LIBPROXYSQLAR) $(CXX) $< $(TEST_HELPERS_OBJ) $(IDIRS) $(LDIRS) $(OPT) \ $(LIBPROXYSQLAR_FULL) $(STATIC_LIBS) $(MYLIBS) \ $(ALLOW_MULTI_DEF) -o $@ diff --git a/test/tap/tests/unit/MySQL_HostGroup_Routing-t.cpp b/test/tap/tests/unit/MySQL_HostGroup_Routing-t.cpp new file mode 100644 index 0000000000..59fa50be51 --- /dev/null +++ b/test/tap/tests/unit/MySQL_HostGroup_Routing-t.cpp @@ -0,0 +1,122 @@ +#include "tap.h" +#include "test_globals.h" +#include "test_init.h" +#include "MySQL_HostGroup_Routing.h" + +void test_mirroring() { + MySQL_Routing_Session_State sess; + sess.mirror = true; + sess.current_hostgroup = 10; + + MySQL_Routing_QPO_State qpo; + qpo.destination_hostgroup = 20; + + MySQL_Routing_Result res = resolve_hostgroup_routing(sess, qpo, 1); + + ok(res.new_current_hostgroup == 20, "Mirroring: current_hostgroup should be destination_hostgroup from QPO"); + ok(res.error == false, "Mirroring: no error expected"); +} + +void test_show_warnings() { + MySQL_Routing_Session_State sess; + sess.warning_in_hg = 15; + sess.current_hostgroup = 10; + + MySQL_Routing_QPO_State qpo; + qpo.is_show_warnings = true; + + MySQL_Routing_Result res = resolve_hostgroup_routing(sess, qpo, 1); + + ok(res.new_current_hostgroup == 15, "SHOW WARNINGS: current_hostgroup should be warning_in_hg"); + + sess.warning_in_hg = -1; + res = resolve_hostgroup_routing(sess, qpo, 1); + ok(res.new_current_hostgroup == 10, "SHOW WARNINGS: current_hostgroup should remain unchanged if warning_in_hg is -1"); +} + +void test_last_insert_id() { + MySQL_Routing_Session_State sess; + sess.last_hg_affected_rows = 25; + sess.current_hostgroup = 10; + + MySQL_Routing_QPO_State qpo; + qpo.is_last_insert_id = true; + + MySQL_Routing_Result res = resolve_hostgroup_routing(sess, qpo, 1); + + ok(res.new_current_hostgroup == 25, "LAST_INSERT_ID: current_hostgroup should be last_hg_affected_rows"); +} + +void test_locking_success() { + MySQL_Routing_Session_State sess; + sess.current_hostgroup = 10; + sess.default_hostgroup = 10; + sess.locked_on_hostgroup = -1; + sess.transaction_persistent_hostgroup = -1; + + MySQL_Routing_QPO_State qpo; + qpo.destination_hostgroup = 20; + qpo.lock_hostgroup = true; + + // Test initial locking + MySQL_Routing_Result res = resolve_hostgroup_routing(sess, qpo, 1); + ok(res.new_current_hostgroup == 20, "Locking: current_hostgroup updated to destination (expected 20, got %d)", res.new_current_hostgroup); + ok(res.lock_hostgroup == true, "Locking: lock_hostgroup flag set"); + ok(res.new_locked_on_hostgroup == 20, "Locking: new_locked_on_hostgroup set to 20 (got %d)", res.new_locked_on_hostgroup); + ok(res.error == false, "Locking: no error expected"); + + // Test subsequent query on same hostgroup + sess.current_hostgroup = 20; + sess.locked_on_hostgroup = 20; + qpo.lock_hostgroup = false; + qpo.destination_hostgroup = 20; + res = resolve_hostgroup_routing(sess, qpo, 1); + ok(res.new_current_hostgroup == 20, "Locked: current_hostgroup remains 20"); + ok(res.error == false, "Locked: no error when hostgroup matches"); +} + +void test_locking_error() { + MySQL_Routing_Session_State sess; + sess.current_hostgroup = 20; + sess.locked_on_hostgroup = 20; + sess.transaction_persistent_hostgroup = -1; + + MySQL_Routing_QPO_State qpo; + qpo.destination_hostgroup = 30; // Trying to reach a different hostgroup + + MySQL_Routing_Result res = resolve_hostgroup_routing(sess, qpo, 1); + ok(res.error == true, "Locked Error: error set when trying to reach different hostgroup"); + ok(res.error_msg.find("locked to hostgroup 20") != std::string::npos, "Locked Error: error message contains correct hostgroup"); +} + +void test_legacy_behavior() { + MySQL_Routing_Session_State sess; + sess.current_hostgroup = 99; + sess.default_hostgroup = 5; + sess.transaction_persistent_hostgroup = -1; + + MySQL_Routing_QPO_State qpo; + qpo.destination_hostgroup = -1; // No rule match + + MySQL_Routing_Result res = resolve_hostgroup_routing(sess, qpo, 0); // Legacy mode + ok(res.new_current_hostgroup == 5, "Legacy: falls back to default_hostgroup when no QPO destination"); + + sess.transaction_persistent_hostgroup = 10; + res = resolve_hostgroup_routing(sess, qpo, 0); + ok(res.new_current_hostgroup == 10, "Legacy: remains on transaction_persistent_hostgroup"); +} + +int main() { + plan(15); + test_init_minimal(); + + test_mirroring(); + test_show_warnings(); + test_last_insert_id(); + test_locking_success(); + test_locking_error(); + test_legacy_behavior(); + + test_cleanup_minimal(); + return exit_status(); +} diff --git a/test/tap/tests/unit/PgSQL_HostGroup_Routing-t.cpp b/test/tap/tests/unit/PgSQL_HostGroup_Routing-t.cpp new file mode 100644 index 0000000000..e329e9aeda --- /dev/null +++ b/test/tap/tests/unit/PgSQL_HostGroup_Routing-t.cpp @@ -0,0 +1,86 @@ +#include "tap.h" +#include "test_globals.h" +#include "test_init.h" +#include "PgSQL_HostGroup_Routing.h" + +void test_basic_routing() { + PgSQL_Routing_Session_State sess; + sess.current_hostgroup = 10; + sess.default_hostgroup = 10; + sess.transaction_persistent_hostgroup = -1; + + PgSQL_Routing_QPO_State qpo; + qpo.destination_hostgroup = 20; + qpo.lock_hostgroup = false; + + PgSQL_Routing_Result res = resolve_pgsql_hostgroup_routing(sess, qpo, 1); + + ok(res.new_current_hostgroup == 20, "Basic: current_hostgroup should be destination_hostgroup from QPO"); + ok(res.error == false, "Basic: no error expected"); +} + +void test_locking_success() { + PgSQL_Routing_Session_State sess; + sess.current_hostgroup = 10; + sess.default_hostgroup = 10; + sess.locked_on_hostgroup = -1; + sess.transaction_persistent_hostgroup = -1; + + PgSQL_Routing_QPO_State qpo; + qpo.destination_hostgroup = 20; + qpo.lock_hostgroup = true; + + // Test initial locking + PgSQL_Routing_Result res = resolve_pgsql_hostgroup_routing(sess, qpo, 1); + ok(res.new_current_hostgroup == 20, "Locking: current_hostgroup updated to destination"); + ok(res.lock_hostgroup == true, "Locking: lock_hostgroup flag set"); + ok(res.new_locked_on_hostgroup == 20, "Locking: new_locked_on_hostgroup set to 20"); + ok(res.error == false, "Locking: no error expected"); +} + +void test_locking_error() { + PgSQL_Routing_Session_State sess; + sess.current_hostgroup = 20; + sess.default_hostgroup = 10; + sess.locked_on_hostgroup = 20; + sess.transaction_persistent_hostgroup = -1; + + PgSQL_Routing_QPO_State qpo; + qpo.destination_hostgroup = 30; // Trying to reach a different hostgroup + qpo.lock_hostgroup = false; + + PgSQL_Routing_Result res = resolve_pgsql_hostgroup_routing(sess, qpo, 1); + ok(res.error == true, "Locked Error: error set when trying to reach different hostgroup"); + ok(res.error_msg.find("locked to hostgroup 20") != std::string::npos, "Locked Error: error message contains correct hostgroup"); +} + +void test_legacy_behavior() { + PgSQL_Routing_Session_State sess; + sess.current_hostgroup = 99; + sess.default_hostgroup = 5; + sess.transaction_persistent_hostgroup = -1; + + PgSQL_Routing_QPO_State qpo; + qpo.destination_hostgroup = -1; // No rule match + qpo.lock_hostgroup = false; + + PgSQL_Routing_Result res = resolve_pgsql_hostgroup_routing(sess, qpo, 0); // Legacy mode + ok(res.new_current_hostgroup == 5, "Legacy: falls back to default_hostgroup when no QPO destination"); + + sess.transaction_persistent_hostgroup = 10; + res = resolve_pgsql_hostgroup_routing(sess, qpo, 0); + ok(res.new_current_hostgroup == 10, "Legacy: remains on transaction_persistent_hostgroup"); +} + +int main() { + plan(10); + test_init_minimal(); + + test_basic_routing(); + test_locking_success(); + test_locking_error(); + test_legacy_behavior(); + + test_cleanup_minimal(); + return exit_status(); +}