-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat: report timeout terminations with SQLSTATE #6016
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v3.0
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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); | ||
|
|
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 Save the prior values, run cleanup before every return, and fail the test if cleanup or 🤖 Prompt for AI Agents |
||
|
|
||
| PQfinish(admin); | ||
| return exit_status(); | ||
| } | ||
There was a problem hiding this comment.
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 👍 / 👎