From 397b03fa55e724dfdd457b105031ebf182ee573a Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Sun, 9 Aug 2026 20:43:14 +0530 Subject: [PATCH 1/2] feat: report timeout terminations with SQLSTATE Signed-off-by: Snehil Shah --- include/Base_Session.h | 7 ++ lib/MySQL_Session.cpp | 1 + lib/PgSQL_Session.cpp | 35 ++++++ lib/PgSQL_Thread.cpp | 6 + test/tap/groups/groups.json | 1 + test/tap/tests/pgsql-wait_timeout-t.cpp | 147 ++++++++++++++++++++++++ 6 files changed, 197 insertions(+) create mode 100644 test/tap/tests/pgsql-wait_timeout-t.cpp diff --git a/include/Base_Session.h b/include/Base_Session.h index f57a7b0788..d59a1b1fdf 100644 --- a/include/Base_Session.h +++ b/include/Base_Session.h @@ -26,6 +26,12 @@ enum SESSION_FORWARD_TYPE : uint8_t { SESSION_FORWARD_TYPE_START_REPLICATION = 0x08 | SESSION_FORWARD_TYPE_TEMPORARY, }; +enum SESSION_KILL_REASON : uint8_t { + SESSION_KILL_REASON_NONE = 0x00, + SESSION_KILL_REASON_IDLE_IN_TRANSACTION_TIMEOUT = 0x01, // max_transaction_idle_time + SESSION_KILL_REASON_IDLE_SESSION_TIMEOUT = 0x02, // wait_timeout +}; + template class Base_Session { public: @@ -84,6 +90,7 @@ class Base_Session { bool autocommit_handled; bool sending_set_autocommit; bool killed; + SESSION_KILL_REASON kill_reason; bool locked_on_hostgroup_and_all_variables_set; //bool admin; bool max_connections_reached; diff --git a/lib/MySQL_Session.cpp b/lib/MySQL_Session.cpp index bf143b39dd..398e7e162a 100644 --- a/lib/MySQL_Session.cpp +++ b/lib/MySQL_Session.cpp @@ -649,6 +649,7 @@ MySQL_Session::MySQL_Session() { sending_set_autocommit=false; autocommit_on_hostgroup=-1; killed=false; + kill_reason=SESSION_KILL_REASON_NONE; session_type=PROXYSQL_SESSION_MYSQL; //admin=false; connections_handler=false; diff --git a/lib/PgSQL_Session.cpp b/lib/PgSQL_Session.cpp index 35cdede51f..41ddc5d1e9 100644 --- a/lib/PgSQL_Session.cpp +++ b/lib/PgSQL_Session.cpp @@ -302,6 +302,7 @@ PgSQL_Session::PgSQL_Session() { autocommit_handled = false; sending_set_autocommit = false; killed = false; + kill_reason = SESSION_KILL_REASON_NONE; session_type = PROXYSQL_SESSION_PGSQL; //admin=false; connections_handler = false; @@ -3125,6 +3126,29 @@ void PgSQL_Session::handler___status_WAITING_CLIENT_DATA() { // is left below as an example of how to perform a more passive maintenance over session connections. } +static PGSQL_ERROR_CODES kill_reason_to_sqlstate(SESSION_KILL_REASON reason) { + switch (reason) { + case SESSION_KILL_REASON_IDLE_IN_TRANSACTION_TIMEOUT: + return PGSQL_ERROR_CODES::ERRCODE_IDLE_IN_TRANSACTION_SESSION_TIMEOUT; + case SESSION_KILL_REASON_IDLE_SESSION_TIMEOUT: + return PGSQL_ERROR_CODES::ERRCODE_IDLE_SESSION_TIMEOUT; + default: + return PGSQL_ERROR_CODES::ERRCODE_ADMIN_SHUTDOWN; + } +} + +// Message text matches PG's own error texts. +static const char* kill_reason_to_message(SESSION_KILL_REASON reason) { + switch (reason) { + case SESSION_KILL_REASON_IDLE_IN_TRANSACTION_TIMEOUT: + return "terminating connection due to idle-in-transaction timeout"; + case SESSION_KILL_REASON_IDLE_SESSION_TIMEOUT: + return "terminating connection due to idle-session timeout"; + default: + return "terminating connection"; + } +} + int PgSQL_Session::handler() { #if ENABLE_TIMER Timer timer(thread->Timers.Sessions_Handlers); @@ -3807,6 +3831,17 @@ int PgSQL_Session::handler() { return handler_ret; } + // end things by sending an ErrorResponse packet, if a reason exists to report. + if (killed == true && kill_reason != SESSION_KILL_REASON_NONE && client_myds != NULL) { + client_myds->setDSS_STATE_QUERY_SENT_NET(); + client_myds->myprot.generate_error_packet(true, false, kill_reason_to_message(kill_reason), + kill_reason_to_sqlstate(kill_reason), true); + client_myds->array2buffer_full(); + client_myds->write_to_net(); + handler_ret = -1; + return handler_ret; + } + // previously active query has been fully processed and its response has been sent to the client. // At this point, check whether deferred packets from the client exist in PSarrayIN. // If so, control is redirected to get_pkts_from_client() to process them. diff --git a/lib/PgSQL_Thread.cpp b/lib/PgSQL_Thread.cpp index 69ec9d4932..3609275f95 100644 --- a/lib/PgSQL_Thread.cpp +++ b/lib/PgSQL_Thread.cpp @@ -3411,6 +3411,9 @@ void PgSQL_Thread::idle_thread_to_kill_idle_sessions() { uint32_t sess_pos = mysess_idx; PgSQL_Session* mysess = (PgSQL_Session*)mysql_sessions->index(sess_pos); if (mysess->idle_since < min_idle || mysess->killed == true) { + if (mysess->killed == false) { + mysess->kill_reason = SESSION_KILL_REASON_IDLE_SESSION_TIMEOUT; + } mysess->killed = true; PgSQL_Data_Stream* tmp_myds = mysess->client_myds; int dsidx = tmp_myds->poll_fds_idx; @@ -3746,6 +3749,7 @@ void PgSQL_Thread::ProcessAllSessions_MaintenanceLoop(PgSQL_Session * sess, unsi // the session has idle transactions, kill it if (sess_time / 1000 > (unsigned long long)pgsql_thread___max_transaction_idle_time) { sess->killed = true; + sess->kill_reason = SESSION_KILL_REASON_IDLE_IN_TRANSACTION_TIMEOUT; if (sess->client_myds) { proxy_warning("Killing client connection %s:%d because of (possible) transaction idle for %llums\n", sess->client_myds->addr.addr, sess->client_myds->addr.port, sess_time / 1000); } @@ -3755,6 +3759,7 @@ void PgSQL_Thread::ProcessAllSessions_MaintenanceLoop(PgSQL_Session * sess, unsi // the session is idle, kill it if (sess_time / 1000 > (unsigned long long)pgsql_thread___wait_timeout) { sess->killed = true; + sess->kill_reason = SESSION_KILL_REASON_IDLE_SESSION_TIMEOUT; if (sess->client_myds) { proxy_warning("Killing client connection %s:%d because inactive for %llums\n", sess->client_myds->addr.addr, sess->client_myds->addr.port, sess_time / 1000); } @@ -3872,6 +3877,7 @@ void PgSQL_Thread::process_all_sessions() { { if ((sess_time / 1000 > (unsigned long long)pgsql_thread___wait_timeout)) { sess->killed = true; + sess->kill_reason = SESSION_KILL_REASON_IDLE_SESSION_TIMEOUT; sess->to_process = 1; proxy_warning("Killing client connection %s:%d because inactive for %llums\n", sess->client_myds->addr.addr, sess->client_myds->addr.port, sess_time / 1000); } diff --git a/test/tap/groups/groups.json b/test/tap/groups/groups.json index 91d9e0d0fe..e4fdbef34b 100644 --- a/test/tap/groups/groups.json +++ b/test/tap/groups/groups.json @@ -161,6 +161,7 @@ "pgsql-extended_query_protocol_query_rules_test-t" : [ "legacy-g4","mysql-auto_increment_delay_multiplex=0-g4","mysql-multiplexing=false-g4","mysql-query_digests=0-g4","mysql-query_digests_keep_comment=1-g4" ], "pgsql-extended_query_protocol_test-t" : [ "legacy-g4","mysql-auto_increment_delay_multiplex=0-g4","mysql-multiplexing=false-g4","mysql-query_digests=0-g4","mysql-query_digests_keep_comment=1-g4" ], "pgsql-hostgroup_default_query_timeout-t" : [ "legacy-g6","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1" ], + "pgsql-wait_timeout-t" : [ "legacy-g6","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1" ], "pgsql-issue5384-t" : [ "legacy-g4","mysql-auto_increment_delay_multiplex=0-g4","mysql-multiplexing=false-g4","mysql-query_digests=0-g4","mysql-query_digests_keep_comment=1-g4" ], "pgsql-monitor_ssl_connections_test-t" : [ "legacy-g4","mysql-auto_increment_delay_multiplex=0-g4","mysql-multiplexing=false-g4","mysql-query_digests=0-g4","mysql-query_digests_keep_comment=1-g4" ], "pgsql-multiplex_status_test-t" : [ "legacy-g4","mysql-auto_increment_delay_multiplex=0-g4","mysql-multiplexing=false-g4","mysql-query_digests=0-g4","mysql-query_digests_keep_comment=1-g4" ], diff --git a/test/tap/tests/pgsql-wait_timeout-t.cpp b/test/tap/tests/pgsql-wait_timeout-t.cpp new file mode 100644 index 0000000000..2790667cf9 --- /dev/null +++ b/test/tap/tests/pgsql-wait_timeout-t.cpp @@ -0,0 +1,147 @@ +/** + * @file pgsql-wait_timeout-t.cpp + * @brief This TAP test validates if session idle timeouts are working correctly, and that + * the termination is reported with the SQLSTATE PostgreSQL uses for it. + */ + +#include +#include +#include + +#include "libpq-fe.h" + +#include "tap.h" +#include "command_line.h" +#include "utils.h" + +PGconn* init_pgsql_conn(char* host, char* user, char* pass, int port) { + diag("Creating PgSQL conn host=\"%s\" port=\"%d\" user=\"%s\"", host, port, user); + + std::stringstream ss; + ss << "host=" << host << " port=" << port << " user=" << user + << " password=" << pass << " dbname=postgres sslmode=disable"; + + PGconn* conn = PQconnectdb(ss.str().c_str()); + if (PQstatus(conn) != CONNECTION_OK) { + PQfinish(conn); + return nullptr; + } + + return conn; +} + +int run_q(PGconn* conn, const char* q) { + PGresult* res = PQexec(conn, q); + const ExecStatusType st = PQresultStatus(res); + PQclear(res); + return (st == PGRES_COMMAND_OK || st == PGRES_TUPLES_OK) ? 0 : 1; +} + +int admin_q(PGconn* admin, const char* q) { + if (run_q(admin, q)) { + fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, PQerrorMessage(admin)); + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} + +void check_terminated_with(PGconn* proxy, const char* expected_sqlstate) { + PGresult* res = PQexec(proxy, "SELECT 1"); + const ExecStatusType st = PQresultStatus(res); + + ok(st != PGRES_TUPLES_OK, (st == PGRES_TUPLES_OK ? "Connection alive" : "Connection killed")); + + const char* sqlstate = PQresultErrorField(res, PG_DIAG_SQLSTATE); + ok(sqlstate != nullptr && strcmp(sqlstate, expected_sqlstate) == 0, + "Termination reported as SQLSTATE %s (got '%s')", + expected_sqlstate, sqlstate ? sqlstate : ""); + + PQclear(res); +} + +int test_session_timeout(CommandLine* cl, PGconn* admin) { + diag("Test: %s", __func__); + + diag("Setting pgsql-wait_timeout=4000"); + if (admin_q(admin, "SET pgsql-wait_timeout=4000")) return EXIT_FAILURE; + diag("Setting pgsql-poll_timeout=500 , required for more precise timeout"); + if (admin_q(admin, "SET pgsql-poll_timeout=500")) return EXIT_FAILURE; + if (admin_q(admin, "LOAD PGSQL VARIABLES TO RUNTIME")) return EXIT_FAILURE; + + PGconn* proxy = init_pgsql_conn(cl->pgsql_host, cl->pgsql_username, cl->pgsql_password, cl->pgsql_port); + if (!proxy) { + fprintf(stderr, "File %s, line %d, Error: connection failed\n", __FILE__, __LINE__); + return EXIT_FAILURE; + } + + int rc = run_q(proxy, "SELECT 1"); + ok(rc == 0, (rc == 0 ? "Connection alive" : "Connection killed")); + + sleep(9); + + check_terminated_with(proxy, "57P05"); + + PQfinish(proxy); + return EXIT_SUCCESS; +} + +int test_transaction_idle_timeout(CommandLine* cl, PGconn* admin) { + diag("Test: %s", __func__); + + // wait_timeout is left high so only max_transaction_idle_time can fire here. + diag("Setting pgsql-max_transaction_idle_time=3000"); + if (admin_q(admin, "SET pgsql-max_transaction_idle_time=3000")) return EXIT_FAILURE; + if (admin_q(admin, "SET pgsql-wait_timeout=60000")) return EXIT_FAILURE; + if (admin_q(admin, "SET pgsql-poll_timeout=500")) return EXIT_FAILURE; + if (admin_q(admin, "LOAD PGSQL VARIABLES TO RUNTIME")) return EXIT_FAILURE; + + PGconn* proxy = init_pgsql_conn(cl->pgsql_host, cl->pgsql_username, cl->pgsql_password, cl->pgsql_port); + if (!proxy) { + fprintf(stderr, "File %s, line %d, Error: connection failed\n", __FILE__, __LINE__); + return EXIT_FAILURE; + } + + int rc = run_q(proxy, "BEGIN"); + ok(rc == 0, (rc == 0 ? "Transaction started" : "Failed to start transaction")); + + sleep(9); + + check_terminated_with(proxy, "25P03"); + + PQfinish(proxy); + return EXIT_SUCCESS; +} + +int main(int argc, char** argv) { + CommandLine cl; + if (cl.getEnv()) { + diag("Failed to get the required environmental variables."); + return exit_status(); + } + + plan(6); + + PGconn* admin = init_pgsql_conn(cl.pgsql_admin_host, cl.admin_username, cl.admin_password, cl.pgsql_admin_port); + if (!admin) { + fprintf(stderr, "File %s, line %d, Error: admin connection failed\n", __FILE__, __LINE__); + return exit_status(); + } + + int rc = test_session_timeout(&cl, admin); + if (rc != EXIT_SUCCESS) { + return exit_status(); + } + + rc = test_transaction_idle_timeout(&cl, admin); + if (rc != EXIT_SUCCESS) { + return exit_status(); + } + + // restore defaults so the short timeouts don't leak into later tests in the group + admin_q(admin, "SET pgsql-wait_timeout=28800000"); + admin_q(admin, "SET pgsql-max_transaction_idle_time=14400000"); + admin_q(admin, "LOAD PGSQL VARIABLES TO RUNTIME"); + + PQfinish(admin); + return exit_status(); +} From a086c58caf28525e91ad5293d523451793e50401 Mon Sep 17 00:00:00 2001 From: Snehil Shah Date: Sun, 9 Aug 2026 21:02:30 +0530 Subject: [PATCH 2/2] lint Signed-off-by: Snehil Shah --- test/tap/tests/pgsql-wait_timeout-t.cpp | 30 ++++++++++++++++++------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/test/tap/tests/pgsql-wait_timeout-t.cpp b/test/tap/tests/pgsql-wait_timeout-t.cpp index 2790667cf9..61060e2381 100644 --- a/test/tap/tests/pgsql-wait_timeout-t.cpp +++ b/test/tap/tests/pgsql-wait_timeout-t.cpp @@ -14,7 +14,7 @@ #include "command_line.h" #include "utils.h" -PGconn* init_pgsql_conn(char* host, char* user, char* pass, int port) { +PGconn* init_pgsql_conn(const char* host, const char* user, const char* pass, int port) { diag("Creating PgSQL conn host=\"%s\" port=\"%d\" user=\"%s\"", host, port, user); std::stringstream ss; @@ -63,10 +63,16 @@ int test_session_timeout(CommandLine* cl, PGconn* admin) { diag("Test: %s", __func__); diag("Setting pgsql-wait_timeout=4000"); - if (admin_q(admin, "SET pgsql-wait_timeout=4000")) return EXIT_FAILURE; + if (admin_q(admin, "SET pgsql-wait_timeout=4000")) { + return EXIT_FAILURE; + } diag("Setting pgsql-poll_timeout=500 , required for more precise timeout"); - if (admin_q(admin, "SET pgsql-poll_timeout=500")) return EXIT_FAILURE; - if (admin_q(admin, "LOAD PGSQL VARIABLES TO RUNTIME")) return EXIT_FAILURE; + if (admin_q(admin, "SET pgsql-poll_timeout=500")) { + return EXIT_FAILURE; + } + if (admin_q(admin, "LOAD PGSQL VARIABLES TO RUNTIME")) { + return EXIT_FAILURE; + } PGconn* proxy = init_pgsql_conn(cl->pgsql_host, cl->pgsql_username, cl->pgsql_password, cl->pgsql_port); if (!proxy) { @@ -90,10 +96,18 @@ int test_transaction_idle_timeout(CommandLine* cl, PGconn* admin) { // wait_timeout is left high so only max_transaction_idle_time can fire here. diag("Setting pgsql-max_transaction_idle_time=3000"); - if (admin_q(admin, "SET pgsql-max_transaction_idle_time=3000")) return EXIT_FAILURE; - if (admin_q(admin, "SET pgsql-wait_timeout=60000")) return EXIT_FAILURE; - if (admin_q(admin, "SET pgsql-poll_timeout=500")) return EXIT_FAILURE; - if (admin_q(admin, "LOAD PGSQL VARIABLES TO RUNTIME")) return EXIT_FAILURE; + if (admin_q(admin, "SET pgsql-max_transaction_idle_time=3000")) { + return EXIT_FAILURE; + } + if (admin_q(admin, "SET pgsql-wait_timeout=60000")) { + return EXIT_FAILURE; + } + if (admin_q(admin, "SET pgsql-poll_timeout=500")) { + return EXIT_FAILURE; + } + if (admin_q(admin, "LOAD PGSQL VARIABLES TO RUNTIME")) { + return EXIT_FAILURE; + } PGconn* proxy = init_pgsql_conn(cl->pgsql_host, cl->pgsql_username, cl->pgsql_password, cl->pgsql_port); if (!proxy) {