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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions include/Base_Session.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<typename S, typename DS, typename B, typename T>
class Base_Session {
public:
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions lib/MySQL_Session.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,7 @@
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;
Expand Down Expand Up @@ -5696,7 +5697,7 @@
if (mysql_thread___multiplexing && (myds->myconn->reusable==true) && myds->myconn->IsActiveTransaction()==false && myds->myconn->MultiplexDisabled()==false) {
myds->DSS=STATE_NOT_INITIALIZED;
if (mysql_thread___autocommit_false_not_reusable && myds->myconn->IsAutoCommit()==false) {
if (mysql_thread___reset_connection_algorithm == 2 && myds->myconn->healthy) {

Check failure on line 5700 in lib/MySQL_Session.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this code to not nest more than 3 if|for|do|while|switch statements.

See more on https://sonarcloud.io/project/issues?id=sysown_proxysql&issues=AZ_nJJeNHB_X69K24eQn&open=AZ_nJJeNHB_X69K24eQn&pullRequest=6016
create_new_session_and_reset_connection(myds);
} else {
myds->destroy_MySQL_Connection_From_Pool(true);
Expand Down Expand Up @@ -9224,7 +9225,7 @@
myds->wait_until=0;
myds->DSS=STATE_NOT_INITIALIZED;
if (mysql_thread___autocommit_false_not_reusable && myds->myconn->IsAutoCommit()==false) {
if (mysql_thread___reset_connection_algorithm == 2 && myds->myconn->healthy) {

Check failure on line 9228 in lib/MySQL_Session.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this code to not nest more than 3 if|for|do|while|switch statements.

See more on https://sonarcloud.io/project/issues?id=sysown_proxysql&issues=AZ_nJJeNHB_X69K24eQo&open=AZ_nJJeNHB_X69K24eQo&pullRequest=6016
create_new_session_and_reset_connection(myds);
} else {
myds->destroy_MySQL_Connection_From_Pool(true);
Expand Down
35 changes: 35 additions & 0 deletions lib/PgSQL_Session.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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.
Expand Down
6 changes: 6 additions & 0 deletions lib/PgSQL_Thread.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand All @@ -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);
}
Expand Down Expand Up @@ -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);
}
Expand Down
1 change: 1 addition & 0 deletions test/tap/groups/groups.json
Original file line number Diff line number Diff line change
Expand Up @@ -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" ],
Expand Down
161 changes: 161 additions & 0 deletions test/tap/tests/pgsql-wait_timeout-t.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/**
* @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 <unistd.h>
#include <cstring>
#include <sstream>

#include "libpq-fe.h"

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

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;
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 : "<none>");

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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Quality: Timeout test relies on fixed sleeps and wall-clock timing

pgsql-wait_timeout-t.cpp drives both cases with real timeouts plus a hard-coded sleep(9) (~18s total), and depends on the maintenance loop firing within that window. On loaded CI this wall-clock dependence makes the test slow and potentially flaky. Consider polling for termination in a loop with a bounded deadline instead of a single fixed sleep, so the test succeeds as soon as the connection is killed and fails fast otherwise.

Was this helpful? React with 👍 / 👎


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");
Comment on lines +154 to +157

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Restore every modified runtime variable on every exit path.

Lines 67 and 95 set pgsql-poll_timeout=500, but this cleanup does not restore it. Lines 131 and 136 also return before this block runs. This can change timeout behavior for later TAP tests.

Save the prior values, run cleanup before every return, and fail the test if cleanup or LOAD PGSQL VARIABLES TO RUNTIME fails.

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

In `@test/tap/tests/pgsql-wait_timeout-t.cpp` around lines 140 - 143, Update the
test cleanup around the existing timeout setup and early returns to save the
original values of every modified runtime variable, including
pgsql-poll_timeout, and restore them on every exit path. Ensure cleanup and LOAD
PGSQL VARIABLES TO RUNTIME failures cause the test to fail, including when
execution reaches the returns near the existing test flow and the final cleanup
block.


PQfinish(admin);
return exit_status();
}