fix(pgsql): surface mid-transaction backend errors instead of silently replaying#5654
Conversation
… broken When a backend connection breaks with an error (e.g. postmaster SIGKILL mid-query), libpq's PQtransactionStatus() returns PQTRANS_UNKNOWN rather than PQTRANS_INTRANS, so IsActiveTransaction() reports false for a connection that was in a transaction moments earlier. The retry path in PgSQL_Session::handler_minus1_ClientLibraryError() then replays the failed statement on a fresh backend connection as autocommit, breaking transactional semantics: the client's next COMMIT lands on a connection with no open transaction and PostgreSQL emits "WARNING: there is no transaction in progress". MySQL does not have this issue because mysql->server_status caches SERVER_STATUS_IN_TRANS from the last OK packet and survives the connection breaking. Fix compute_unknown_transaction_status() to force unknown_transaction_status=true when the backend is disconnected, so the retry guard in handler_minus1_ClientLibraryError() refuses to replay statements that were running inside an explicit transaction.
Reproduces the bug fixed in the preceding commit: while a client is in an explicit transaction, terminating the backend mid-query (simulated via pg_terminate_backend from a separate superuser connection) must surface a fatal error to the client. Pre-fix ProxySQL silently retried the in-flight statement on a fresh backend as autocommit, so the expected PGRES_FATAL_ERROR came back as PGRES_TUPLES_OK and the next COMMIT landed on a connection with no open transaction. The test also asserts the client<->ProxySQL connection stays open and that ROLLBACK + a follow-up SELECT 1 recover the session cleanly.
📝 Walkthrough🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request addresses a critical data consistency issue in the PostgreSQL module where statements were silently retried on a new connection if the backend connection failed during an active transaction. By forcing the transaction status to unknown when a connection is lost, the retry logic correctly identifies the active transaction state and prevents unsafe re-execution. A new regression test has been implemented to verify this behavior and ensure that such failures are correctly propagated to the client as fatal errors. I have no feedback to provide.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@test/tap/tests/pgsql-retry_guard_in_txn_on_broken_backend-t.cpp`:
- Line 75: The test currently only emits a diagnostic for the expected
"ROLLBACK" recovery and does not assert it actually succeeded; replace the
diagnostic that prints "ROLLBACK" with a TAP assertion that the ROLLBACK command
executed successfully (e.g. use ok(...) or is(...) on the function that runs
SQL/commands in the test so it verifies
exec("ROLLBACK")/run_sql("ROLLBACK")/txn->rollback() returned success), and do
the same replacement for the second occurrence around lines 168-174; keep the
assertion message like "ROLLBACK recovery after broken backend" so it clearly
documents the check.
- Around line 111-141: Pre-open the direct superuser connection with
openConnection and store the PGconn* (e.g. PGconn* direct) before spawning the
killer thread so the 500ms sleep only controls when pg_terminate_backend is
sent, not connection setup; then start the killer thread which uses that
pre-opened direct connection to run snprintf + PQexec for "SELECT
pg_terminate_backend(%d)" (using backend_pid) and sets kill_delivered, and
ensure PQclear/PQfinish still run (either in the killer thread or after joining)
to avoid leaks; keep the PQexec(cli, "SELECT pg_sleep(3)") call as the in-flight
query under test.
- Line 41: Remove the unused POSIX include directive "<unistd.h>" from the test;
verify no uses of POSIX symbols (search for usleep, sleep, close, getpid, read,
write, pipe, fork, exec) remain in the test code before deleting the include,
and then delete the `#include` <unistd.h> line (no other code changes required).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 60ec2520-cb18-45a8-a199-f836e2309bb4
📒 Files selected for processing (2)
lib/PgSQL_Connection.cpptest/tap/tests/pgsql-retry_guard_in_txn_on_broken_backend-t.cpp
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
- GitHub Check: CI-builds / builds (ubuntu24,-tap-genai-gcov)
- GitHub Check: CI-builds / builds (ubuntu22,-tap)
- GitHub Check: CI-builds / builds (debian12,-dbg)
- GitHub Check: run / trigger
- GitHub Check: claude-review
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{cpp,h,hpp}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{cpp,h,hpp}: Class names should use PascalCase with protocol prefixes (MySQL_,PgSQL_,ProxySQL_)
Member variables should use snake_case naming
Constants and macros should use UPPER_SNAKE_CASE naming
C++17 is required; use conditional compilation via#ifdef PROXYSQLGENAI,#ifdef PROXYSQL31, etc. for feature-gated code
Use jemalloc for memory allocation
Use pthread mutexes for synchronization andstd::atomic<>for counters
Files:
lib/PgSQL_Connection.cpptest/tap/tests/pgsql-retry_guard_in_txn_on_broken_backend-t.cpp
lib/**/*.cpp
📄 CodeRabbit inference engine (CLAUDE.md)
One class per file typically in
lib/directory
Files:
lib/PgSQL_Connection.cpp
test/tap/tests/*-t.cpp
📄 CodeRabbit inference engine (CLAUDE.md)
Test files should follow naming pattern
test_*.cppor*-t.cppintest/tap/tests/
Files:
test/tap/tests/pgsql-retry_guard_in_txn_on_broken_backend-t.cpp
🧠 Learnings (6)
📚 Learning: 2026-04-01T21:27:03.216Z
Learnt from: wazir-ahmed
Repo: sysown/proxysql PR: 5557
File: test/tap/tests/unit/gtid_set_unit-t.cpp:14-17
Timestamp: 2026-04-01T21:27:03.216Z
Learning: In ProxySQL's unit test directory (test/tap/tests/unit/), test_globals.h and test_init.h are only required for tests that depend on the ProxySQL runtime globals/initialization (i.e., tests that exercise components linked against libproxysql.a). Pure data-structure or utility tests (e.g., ezoption_parser_unit-t.cpp, gtid_set_unit-t.cpp, gtid_trxid_interval_unit-t.cpp) only need tap.h and the relevant project header — omitting test_globals.h and test_init.h is correct and intentional in these cases.
Applied to files:
test/tap/tests/pgsql-retry_guard_in_txn_on_broken_backend-t.cpp
📚 Learning: 2026-04-17T16:31:58.178Z
Learnt from: CR
Repo: sysown/proxysql PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-17T16:31:58.178Z
Learning: Applies to test/tap/tests/unit/**/*.cpp : Unit tests must use `test_globals.h` and `test_init.h` and link against `libproxysql.a` via the custom test harness
Applied to files:
test/tap/tests/pgsql-retry_guard_in_txn_on_broken_backend-t.cpp
📚 Learning: 2026-01-20T09:34:19.124Z
Learnt from: yuji-hatakeyama
Repo: sysown/proxysql PR: 5307
File: test/tap/tests/reg_test_5306-show_warnings_with_comment-t.cpp:39-48
Timestamp: 2026-01-20T09:34:19.124Z
Learning: In ProxySQL's TAP test suite, resource leaks (e.g., not calling mysql_close() on early return paths) are commonly tolerated because test processes are short-lived and OS frees resources on exit. This pattern applies to all C++ test files under test/tap/tests. When reviewing, recognize this as a project-wide test convention and focus on test correctness and isolation rather than insisting on fixing such leaks in these test files.
Applied to files:
test/tap/tests/pgsql-retry_guard_in_txn_on_broken_backend-t.cpp
📚 Learning: 2026-04-17T16:31:58.178Z
Learnt from: CR
Repo: sysown/proxysql PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-17T16:31:58.178Z
Learning: Applies to **/*.{cpp,h,hpp} : C++17 is required; use conditional compilation via `#ifdef PROXYSQLGENAI`, `#ifdef PROXYSQL31`, etc. for feature-gated code
Applied to files:
test/tap/tests/pgsql-retry_guard_in_txn_on_broken_backend-t.cpp
📚 Learning: 2026-04-17T16:31:58.178Z
Learnt from: CR
Repo: sysown/proxysql PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-17T16:31:58.178Z
Learning: Applies to test/tap/tests/*-t.cpp : Test files should follow naming pattern `test_*.cpp` or `*-t.cpp` in `test/tap/tests/`
Applied to files:
test/tap/tests/pgsql-retry_guard_in_txn_on_broken_backend-t.cpp
📚 Learning: 2026-01-20T07:40:34.938Z
Learnt from: yuji-hatakeyama
Repo: sysown/proxysql PR: 5307
File: test/tap/tests/reg_test_5306-show_warnings_with_comment-t.cpp:24-28
Timestamp: 2026-01-20T07:40:34.938Z
Learning: In ProxySQL test files, calling `mysql_error(NULL)` after `mysql_init()` failure is safe because the MariaDB client library implementation returns an empty string for NULL handles (not undefined behavior).
Applied to files:
test/tap/tests/pgsql-retry_guard_in_txn_on_broken_backend-t.cpp
🪛 Clang (14.0.6)
test/tap/tests/pgsql-retry_guard_in_txn_on_broken_backend-t.cpp
[error] 41-41: 'unistd.h' file not found
(clang-diagnostic-error)
🔇 Additional comments (1)
lib/PgSQL_Connection.cpp (1)
1311-1320: LGTM — disconnected backends now block unsafe retry.Setting
unknown_transaction_status = truebefore consultingPQtransactionStatus()preserves the retry guard when libpq can only report an unknown state on a broken connection.
| * regression guard to raise. | ||
| */ | ||
|
|
||
| #include <unistd.h> |
There was a problem hiding this comment.
Remove the unused POSIX header flagged by static analysis.
<unistd.h> is not used in this test, and the analyzer reports it as unavailable. Dropping it avoids a portability/build failure without changing behavior.
🧹 Proposed fix
-#include <unistd.h>
`#include` <cstdlib>Verify the include has no symbol usage before removing it:
#!/bin/bash
# Expect: only the include line, with no sleep/usleep/close/getpid/etc. usage.
rg -n '#include <unistd\.h>|\b(usleep|sleep|close|getpid|read|write|pipe|fork|exec)\b' test/tap/tests/pgsql-retry_guard_in_txn_on_broken_backend-t.cpp🧰 Tools
🪛 Clang (14.0.6)
[error] 41-41: 'unistd.h' file not found
(clang-diagnostic-error)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@test/tap/tests/pgsql-retry_guard_in_txn_on_broken_backend-t.cpp` at line 41,
Remove the unused POSIX include directive "<unistd.h>" from the test; verify no
uses of POSIX symbols (search for usleep, sleep, close, getpid, read, write,
pipe, fork, exec) remain in the test code before deleting the include, and then
delete the `#include` <unistd.h> line (no other code changes required).
| } | ||
|
|
||
| int main(int /*argc*/, char** /*argv*/) { | ||
| plan(6); |
There was a problem hiding this comment.
Assert ROLLBACK recovery instead of only logging it.
The test summary says recovery is verified with ROLLBACK, but the current code only emits a diagnostic and would still pass that step if ROLLBACK failed.
✅ Proposed fix
- plan(6);
+ plan(7);
@@
// Recover: ROLLBACK clears the failed transaction on a pool-reusable
// connection. A follow-up SELECT 1 should succeed.
r = PQexec(cli, "ROLLBACK");
- diag("post-kill ROLLBACK status: %s", PQresStatus(PQresultStatus(r)));
+ ExecStatusType rollback_st = PQresultStatus(r);
+ ok(rollback_st == PGRES_COMMAND_OK,
+ "ROLLBACK after backend kill succeeded (%s)", PQresStatus(rollback_st));
PQclear(r);
r = PQexec(cli, "SELECT 1");Also applies to: 168-174
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@test/tap/tests/pgsql-retry_guard_in_txn_on_broken_backend-t.cpp` at line 75,
The test currently only emits a diagnostic for the expected "ROLLBACK" recovery
and does not assert it actually succeeded; replace the diagnostic that prints
"ROLLBACK" with a TAP assertion that the ROLLBACK command executed successfully
(e.g. use ok(...) or is(...) on the function that runs SQL/commands in the test
so it verifies exec("ROLLBACK")/run_sql("ROLLBACK")/txn->rollback() returned
success), and do the same replacement for the second occurrence around lines
168-174; keep the assertion message like "ROLLBACK recovery after broken
backend" so it clearly documents the check.
| // Fire the kill from a separate thread so it lands while the main thread | ||
| // is blocked inside PQexec(SELECT pg_sleep). We use a DIRECT superuser | ||
| // connection to the primary (not ProxySQL) so the kill path is unrelated | ||
| // to the code path under test. | ||
| std::atomic<bool> kill_delivered(false); | ||
| std::thread killer([&]() { | ||
| // Let the main thread enter pg_sleep before we fire. | ||
| std::this_thread::sleep_for(std::chrono::milliseconds(500)); | ||
|
|
||
| PGconn* direct = openConnection(cl.pgsql_server_host, cl.pgsql_server_port, | ||
| cl.pgsql_server_username, cl.pgsql_server_password, | ||
| "PG-direct (superuser)"); | ||
| if (!direct) return; | ||
|
|
||
| char q[96]; | ||
| snprintf(q, sizeof(q), "SELECT pg_terminate_backend(%d)", backend_pid); | ||
| PGresult* kr = PQexec(direct, q); | ||
| if (PQresultStatus(kr) == PGRES_TUPLES_OK && PQntuples(kr) == 1) { | ||
| // pg_terminate_backend returns boolean: 't' iff signal delivered. | ||
| kill_delivered.store(PQgetvalue(kr, 0, 0)[0] == 't'); | ||
| } | ||
| PQclear(kr); | ||
| PQfinish(direct); | ||
| }); | ||
|
|
||
| // This query will be in-flight when the terminate_backend signal arrives. | ||
| // With the fix, ProxySQL refuses to retry it (because the session is in | ||
| // an explicit transaction) and the error propagates to the client. Without | ||
| // the fix, ProxySQL retries on a fresh backend as autocommit and we'd see | ||
| // the sleep complete successfully — the silent-corruption path. | ||
| r = PQexec(cli, "SELECT pg_sleep(3)"); |
There was a problem hiding this comment.
Avoid timing flake in the kill window.
Opening the direct PostgreSQL connection inside the killer thread after the 500ms delay can push pg_terminate_backend() past the 3s pg_sleep() on slow CI, causing a false regression failure. Pre-open the direct connection before starting the in-flight query so the delay controls only the kill timing.
🧪 Proposed hardening
// Fire the kill from a separate thread so it lands while the main thread
// is blocked inside PQexec(SELECT pg_sleep). We use a DIRECT superuser
// connection to the primary (not ProxySQL) so the kill path is unrelated
// to the code path under test.
+ PGconn* direct = openConnection(cl.pgsql_server_host, cl.pgsql_server_port,
+ cl.pgsql_server_username, cl.pgsql_server_password,
+ "PG-direct (superuser)");
std::atomic<bool> kill_delivered(false);
std::thread killer([&]() {
// Let the main thread enter pg_sleep before we fire.
std::this_thread::sleep_for(std::chrono::milliseconds(500));
- PGconn* direct = openConnection(cl.pgsql_server_host, cl.pgsql_server_port,
- cl.pgsql_server_username, cl.pgsql_server_password,
- "PG-direct (superuser)");
if (!direct) return;
char q[96];
snprintf(q, sizeof(q), "SELECT pg_terminate_backend(%d)", backend_pid);
@@
}
PQclear(kr);
- PQfinish(direct);
});
@@
r = PQexec(cli, "SELECT pg_sleep(3)");
killer.join();
+ if (direct) PQfinish(direct);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Fire the kill from a separate thread so it lands while the main thread | |
| // is blocked inside PQexec(SELECT pg_sleep). We use a DIRECT superuser | |
| // connection to the primary (not ProxySQL) so the kill path is unrelated | |
| // to the code path under test. | |
| std::atomic<bool> kill_delivered(false); | |
| std::thread killer([&]() { | |
| // Let the main thread enter pg_sleep before we fire. | |
| std::this_thread::sleep_for(std::chrono::milliseconds(500)); | |
| PGconn* direct = openConnection(cl.pgsql_server_host, cl.pgsql_server_port, | |
| cl.pgsql_server_username, cl.pgsql_server_password, | |
| "PG-direct (superuser)"); | |
| if (!direct) return; | |
| char q[96]; | |
| snprintf(q, sizeof(q), "SELECT pg_terminate_backend(%d)", backend_pid); | |
| PGresult* kr = PQexec(direct, q); | |
| if (PQresultStatus(kr) == PGRES_TUPLES_OK && PQntuples(kr) == 1) { | |
| // pg_terminate_backend returns boolean: 't' iff signal delivered. | |
| kill_delivered.store(PQgetvalue(kr, 0, 0)[0] == 't'); | |
| } | |
| PQclear(kr); | |
| PQfinish(direct); | |
| }); | |
| // This query will be in-flight when the terminate_backend signal arrives. | |
| // With the fix, ProxySQL refuses to retry it (because the session is in | |
| // an explicit transaction) and the error propagates to the client. Without | |
| // the fix, ProxySQL retries on a fresh backend as autocommit and we'd see | |
| // the sleep complete successfully — the silent-corruption path. | |
| r = PQexec(cli, "SELECT pg_sleep(3)"); | |
| // Fire the kill from a separate thread so it lands while the main thread | |
| // is blocked inside PQexec(SELECT pg_sleep). We use a DIRECT superuser | |
| // connection to the primary (not ProxySQL) so the kill path is unrelated | |
| // to the code path under test. | |
| PGconn* direct = openConnection(cl.pgsql_server_host, cl.pgsql_server_port, | |
| cl.pgsql_server_username, cl.pgsql_server_password, | |
| "PG-direct (superuser)"); | |
| std::atomic<bool> kill_delivered(false); | |
| std::thread killer([&]() { | |
| // Let the main thread enter pg_sleep before we fire. | |
| std::this_thread::sleep_for(std::chrono::milliseconds(500)); | |
| if (!direct) return; | |
| char q[96]; | |
| snprintf(q, sizeof(q), "SELECT pg_terminate_backend(%d)", backend_pid); | |
| PGresult* kr = PQexec(direct, q); | |
| if (PQresultStatus(kr) == PGRES_TUPLES_OK && PQntuples(kr) == 1) { | |
| // pg_terminate_backend returns boolean: 't' iff signal delivered. | |
| kill_delivered.store(PQgetvalue(kr, 0, 0)[0] == 't'); | |
| } | |
| PQclear(kr); | |
| }); | |
| // This query will be in-flight when the terminate_backend signal arrives. | |
| // With the fix, ProxySQL refuses to retry it (because the session is in | |
| // an explicit transaction) and the error propagates to the client. Without | |
| // the fix, ProxySQL retries on a fresh backend as autocommit and we'd see | |
| // the sleep complete successfully — the silent-corruption path. | |
| r = PQexec(cli, "SELECT pg_sleep(3)"); | |
| killer.join(); | |
| if (direct) PQfinish(direct); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@test/tap/tests/pgsql-retry_guard_in_txn_on_broken_backend-t.cpp` around lines
111 - 141, Pre-open the direct superuser connection with openConnection and
store the PGconn* (e.g. PGconn* direct) before spawning the killer thread so the
500ms sleep only controls when pg_terminate_backend is sent, not connection
setup; then start the killer thread which uses that pre-opened direct connection
to run snprintf + PQexec for "SELECT pg_terminate_backend(%d)" (using
backend_pid) and sets kill_delivered, and ensure PQclear/PQfinish still run
(either in the killer thread or after joining) to avoid leaks; keep the
PQexec(cli, "SELECT pg_sleep(3)") call as the in-flight query under test.
…json The test added in 8e509d3 was not registered in groups.json, so no TAP group was running it. Register it under legacy-g2.
Previously CI-lint-groups-json only triggered when groups.json itself was
edited and only checked formatting. That can't catch the "author added a
new test source but forgot to register it" case (which is exactly what
happened with pgsql-retry_guard_in_txn_on_broken_backend-t before this
series).
- Drop the paths filter so the workflow runs on every pull_request/push.
- Add a --source mode to check_groups.py that scans test/tap/tests* for
*-t.{cpp,sh,py,php} (and extension-less -t scripts) and flags any
source file not registered in groups.json as an ERROR. Requires no
build — runs in seconds.
- The reverse direction (entry in groups.json with no matching source)
is a NOTE in source mode, because a handful of binaries are built
from differently-named sources via explicit Makefile targets (e.g.
mysql_reconnect_libmariadb-t from mysql_reconnect.cpp).
- Executable mode behavior is unchanged; run-multi-group.bash keeps
calling check_groups.py with no flag.
…th WASAN obj/tap.o is compiled with $(OPT), which includes $(WASAN) = -fsanitize=address when WITHASAN=1. Every other unit test binary goes through the pattern rule on line 333 which re-applies $(OPT) on the link line, pulling in libasan. The two explicit rules for ezoption_parser_unit-t and mysql_resolution_unit-t were written without $(OPT) on the link step (they avoid linking libproxysql.a since they are standalone header-only tests), but they also dropped $(WASAN) — so the linker sees ASAN-instrumented tap.o and nothing to resolve __asan_report_*, __asan_init, __asan_handle_no_return, etc. against: /usr/bin/ld: obj/tap.o: undefined reference to `__asan_report_load8' ... (hundreds more) ... collect2: error: ld returned 1 exit status make[1]: *** [Makefile:321: ezoption_parser_unit-t] Error 1 Put $(WASAN) back on both link commands. CI-unit-tests-asan-coverage has been red on every branch (including v3.0 itself) since at least 2026-04-12 because of this.
…8s iterations
fast_forward_grace_close-t writes a binlog, then for each target_time in
{0..7, 20, 30, 60} reads it via a throttled client and asserts that
target_time <= 8s reaches EOF and target_time > 8s does not (because the
8s grace_close_ms should cut the session before the binlog is drained).
The >8s iterations have been flaky on GitHub runners. Analysis of the
actual CI logs (proxysql.log timestamps vs test TAP output) shows what
goes wrong: during the 8s grace window, ProxySQL keeps pushing bytes
into the client's kernel recv buffer as fast as the buffer accepts them.
If the whole binlog — including the empty-event EOF marker — fits into
"what the client has already read" + "what is sitting in the client's
recv buffer" by t=8s, grace_close does fire and cut the session, but
the client's next mysql_binlog_fetch just returns the already-buffered
EOF event and reports reached_EOF=TRUE. The test then fails the
"Expected FALSE" assertion.
With total_bytes = N and client recv-buffer capacity R, the test is
only robust when N > ~1.7*R. Prior bumps (3 events -> 50 events,
2026-04-13) lifted N from 150 KB to 2.5 MB, which is enough on
dbdeployer but too close to the margin on GH runners whose TCP recv
autotune can grow past a couple of MB.
1000 x 50 KB = ~50 MB puts N well above any realistic R (kernels cap
autotune around a few MB), so the grace close always fires while there
is still many MB of undelivered binlog sitting in ProxySQL's output
queue — the client cannot reach EOF.
Side effects on other tests: none. The only other binlog-reading test
in the same group, fast_forward_switch_replication_deprecate_eof-t, is
already incidentally reading grace_close's binlog (it picks the first
file from SHOW BINARY LOGS, which ends up being grace_close's rotated
file). It reads at full speed and only asserts reached_EOF==true,
which stays correct — it just reports ~50 MB instead of ~2.5 MB in its
informational TAP message. Full-speed read of 50 MB over the Docker
bridge is sub-second per config (4 configs -> ~1-2 s added). Data
generation cost at test start is ~1-2 s for the extra INSERTs.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@test/tap/groups/check_groups.py`:
- Around line 66-94: In find_source_tests, avoid unconditionally adding files
that merely end with "-t": change the loop variable name dirpath to _dirpath,
and for the extensionless branch (the entry.endswith("-t") case) check that the
file is executable before adding it (use os.access(os.path.join(_dirpath,
entry), os.X_OK)) so editor backups or stray artifacts aren’t reported; keep the
existing behavior for recognized script extensions (SCRIPT_EXTS) and continue
using entry[:-len(ext)] when adding those entries.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 8e9773ae-cff3-4001-af0c-bf3ecde40dfd
📒 Files selected for processing (5)
.github/workflows/CI-lint-groups-json.ymltest/tap/groups/check_groups.pytest/tap/groups/groups.jsontest/tap/tests/fast_forward_grace_close.cpptest/tap/tests/unit/Makefile
✅ Files skipped from review due to trivial changes (1)
- test/tap/groups/groups.json
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
- GitHub Check: CI-builds / builds (ubuntu24,-tap-genai-gcov)
- GitHub Check: CI-builds / builds (debian12,-dbg)
- GitHub Check: CI-builds / builds (ubuntu22,-tap)
- GitHub Check: run / trigger
- GitHub Check: claude-review
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{cpp,h,hpp}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{cpp,h,hpp}: Class names should use PascalCase with protocol prefixes (MySQL_,PgSQL_,ProxySQL_)
Member variables should use snake_case naming
Constants and macros should use UPPER_SNAKE_CASE naming
C++17 is required; use conditional compilation via#ifdef PROXYSQLGENAI,#ifdef PROXYSQL31, etc. for feature-gated code
Use jemalloc for memory allocation
Use pthread mutexes for synchronization andstd::atomic<>for counters
Files:
test/tap/tests/fast_forward_grace_close.cpp
🧠 Learnings (7)
📚 Learning: 2026-04-17T16:31:58.178Z
Learnt from: CR
Repo: sysown/proxysql PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-17T16:31:58.178Z
Learning: Applies to test/tap/tests/unit/**/*.cpp : Unit tests must use `test_globals.h` and `test_init.h` and link against `libproxysql.a` via the custom test harness
Applied to files:
test/tap/tests/unit/Makefiletest/tap/tests/fast_forward_grace_close.cpp
📚 Learning: 2026-04-01T21:27:03.216Z
Learnt from: wazir-ahmed
Repo: sysown/proxysql PR: 5557
File: test/tap/tests/unit/gtid_set_unit-t.cpp:14-17
Timestamp: 2026-04-01T21:27:03.216Z
Learning: In ProxySQL's unit test directory (test/tap/tests/unit/), test_globals.h and test_init.h are only required for tests that depend on the ProxySQL runtime globals/initialization (i.e., tests that exercise components linked against libproxysql.a). Pure data-structure or utility tests (e.g., ezoption_parser_unit-t.cpp, gtid_set_unit-t.cpp, gtid_trxid_interval_unit-t.cpp) only need tap.h and the relevant project header — omitting test_globals.h and test_init.h is correct and intentional in these cases.
Applied to files:
test/tap/tests/unit/Makefiletest/tap/tests/fast_forward_grace_close.cpp
📚 Learning: 2026-04-17T16:31:58.178Z
Learnt from: CR
Repo: sysown/proxysql PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-17T16:31:58.178Z
Learning: Applies to test/tap/groups/groups.json : Register new test files in `test/tap/groups/groups.json` for test discovery via pattern rules in `test/tap/tests/Makefile`
Applied to files:
test/tap/tests/unit/Makefile.github/workflows/CI-lint-groups-json.ymltest/tap/groups/check_groups.py
📚 Learning: 2026-04-17T16:31:58.178Z
Learnt from: CR
Repo: sysown/proxysql PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-17T16:31:58.178Z
Learning: Applies to test/tap/tests/*-t.cpp : Test files should follow naming pattern `test_*.cpp` or `*-t.cpp` in `test/tap/tests/`
Applied to files:
test/tap/tests/unit/Makefile
📚 Learning: 2026-04-17T16:31:58.178Z
Learnt from: CR
Repo: sysown/proxysql PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-17T16:31:58.178Z
Learning: Applies to **/*.{cpp,h,hpp} : C++17 is required; use conditional compilation via `#ifdef PROXYSQLGENAI`, `#ifdef PROXYSQL31`, etc. for feature-gated code
Applied to files:
test/tap/tests/unit/Makefile
📚 Learning: 2026-01-20T09:34:19.124Z
Learnt from: yuji-hatakeyama
Repo: sysown/proxysql PR: 5307
File: test/tap/tests/reg_test_5306-show_warnings_with_comment-t.cpp:39-48
Timestamp: 2026-01-20T09:34:19.124Z
Learning: In ProxySQL's TAP test suite, resource leaks (e.g., not calling mysql_close() on early return paths) are commonly tolerated because test processes are short-lived and OS frees resources on exit. This pattern applies to all C++ test files under test/tap/tests. When reviewing, recognize this as a project-wide test convention and focus on test correctness and isolation rather than insisting on fixing such leaks in these test files.
Applied to files:
test/tap/tests/fast_forward_grace_close.cpp
📚 Learning: 2026-01-20T07:40:34.938Z
Learnt from: yuji-hatakeyama
Repo: sysown/proxysql PR: 5307
File: test/tap/tests/reg_test_5306-show_warnings_with_comment-t.cpp:24-28
Timestamp: 2026-01-20T07:40:34.938Z
Learning: In ProxySQL test files, calling `mysql_error(NULL)` after `mysql_init()` failure is safe because the MariaDB client library implementation returns an empty string for NULL handles (not undefined behavior).
Applied to files:
test/tap/tests/fast_forward_grace_close.cpp
🪛 Ruff (0.15.10)
test/tap/groups/check_groups.py
[warning] 85-85: Loop control variable dirpath not used within loop body
Rename unused dirpath to _dirpath
(B007)
🔇 Additional comments (4)
test/tap/tests/fast_forward_grace_close.cpp (1)
62-73: LGTM — binlog volume increase is well justified.The rationale is clearly documented in the comment, and bumping to ~50 MB gives sufficient margin over autotuned recv buffers so the EOF marker cannot slip through during the 8s grace window. Be aware this adds ~50 MB of INSERTs through ProxySQL at startup; on slow CI runners this may noticeably extend test runtime, but it's a reasonable tradeoff for reducing flakiness.
test/tap/tests/unit/Makefile (1)
320-328: LGTM —$(WASAN)added to standalone link lines.Adding
$(WASAN)here makes the two standalone rules consistent with the pattern rule (which propagates it via$(OPT)), resolving the ASAN link errors mentioned in the PR description. No other concerns..github/workflows/CI-lint-groups-json.yml (1)
3-16: LGTM — always lint + source-registration check.Running
lint_groups_json.pyandcheck_groups.py --sourceon every PR/push (nopaths:filter) ensures that adding a new TAP source without agroups.jsonentry fails CI, which matches the PR intent.workflow_dispatchfor manual runs is a nice touch.test/tap/groups/check_groups.py (1)
132-155: LGTM — mode-dependent severity is correctly centralized.
missing_on_disk_is_errorcleanly encodes the asymmetry: in source mode,groups.jsonentries without matching sources are only a NOTE (since some binaries come from differently-named sources via explicit Makefile targets), while on-disk-but-missing-from-groups.json remains an error in both modes. Matches the documented behavior.
| def find_source_tests(tap_root): | ||
| """Find all TAP test source files under the TAP test directories. | ||
|
|
||
| Recognized forms: | ||
| - foo-t.cpp / foo-t.sh / foo-t.py / foo-t.php -> expected binary 'foo-t' | ||
| - executable file named 'foo-t' with no extension -> 'foo-t' | ||
|
|
||
| Unlike find_executable_tests(), this does not require a build: it scans | ||
| the source tree, so it works on a fresh checkout in a lint CI job. | ||
| """ | ||
| scan_roots = [ | ||
| os.path.join(tap_root, "tests"), | ||
| os.path.join(tap_root, "tests_with_deps"), | ||
| ] | ||
|
|
||
| sources = set() | ||
| for scan_root in scan_roots: | ||
| if not os.path.isdir(scan_root): | ||
| continue | ||
| for dirpath, _, filenames in os.walk(scan_root): | ||
| for entry in filenames: | ||
| if entry.endswith("-t"): | ||
| sources.add(entry) | ||
| continue | ||
| for ext in SCRIPT_EXTS: | ||
| if entry.endswith("-t" + ext): | ||
| sources.add(entry[: -len(ext)]) | ||
| break | ||
| return sources |
There was a problem hiding this comment.
find_source_tests will pick up any file ending in -t, including non-source artifacts.
Unlike find_executable_tests (which filters os.access(..., X_OK) and excludes .cpp), find_source_tests adds every filename ending in -t unconditionally at line 87-88. A stray foo-t file (e.g., editor backup, generated artifact left over in a dev checkout) would be reported as an unregistered test. CI runs on fresh checkouts so this is low risk in practice, but you may want to at least require the extensionless branch to be executable, to mirror find_executable_tests.
Also, per Ruff B007, dirpath at line 85 is unused — rename to _dirpath.
♻️ Suggested tightening
- for dirpath, _, filenames in os.walk(scan_root):
+ for _dirpath, _, filenames in os.walk(scan_root):
for entry in filenames:
if entry.endswith("-t"):
- sources.add(entry)
- continue
+ path = os.path.join(_dirpath, entry)
+ if os.path.isfile(path) and os.access(path, os.X_OK):
+ sources.add(entry)
+ continue
for ext in SCRIPT_EXTS:
if entry.endswith("-t" + ext):
sources.add(entry[: -len(ext)])
break📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def find_source_tests(tap_root): | |
| """Find all TAP test source files under the TAP test directories. | |
| Recognized forms: | |
| - foo-t.cpp / foo-t.sh / foo-t.py / foo-t.php -> expected binary 'foo-t' | |
| - executable file named 'foo-t' with no extension -> 'foo-t' | |
| Unlike find_executable_tests(), this does not require a build: it scans | |
| the source tree, so it works on a fresh checkout in a lint CI job. | |
| """ | |
| scan_roots = [ | |
| os.path.join(tap_root, "tests"), | |
| os.path.join(tap_root, "tests_with_deps"), | |
| ] | |
| sources = set() | |
| for scan_root in scan_roots: | |
| if not os.path.isdir(scan_root): | |
| continue | |
| for dirpath, _, filenames in os.walk(scan_root): | |
| for entry in filenames: | |
| if entry.endswith("-t"): | |
| sources.add(entry) | |
| continue | |
| for ext in SCRIPT_EXTS: | |
| if entry.endswith("-t" + ext): | |
| sources.add(entry[: -len(ext)]) | |
| break | |
| return sources | |
| def find_source_tests(tap_root): | |
| """Find all TAP test source files under the TAP test directories. | |
| Recognized forms: | |
| - foo-t.cpp / foo-t.sh / foo-t.py / foo-t.php -> expected binary 'foo-t' | |
| - executable file named 'foo-t' with no extension -> 'foo-t' | |
| Unlike find_executable_tests(), this does not require a build: it scans | |
| the source tree, so it works on a fresh checkout in a lint CI job. | |
| """ | |
| scan_roots = [ | |
| os.path.join(tap_root, "tests"), | |
| os.path.join(tap_root, "tests_with_deps"), | |
| ] | |
| sources = set() | |
| for scan_root in scan_roots: | |
| if not os.path.isdir(scan_root): | |
| continue | |
| for _dirpath, _, filenames in os.walk(scan_root): | |
| for entry in filenames: | |
| if entry.endswith("-t"): | |
| path = os.path.join(_dirpath, entry) | |
| if os.path.isfile(path) and os.access(path, os.X_OK): | |
| sources.add(entry) | |
| continue | |
| for ext in SCRIPT_EXTS: | |
| if entry.endswith("-t" + ext): | |
| sources.add(entry[: -len(ext)]) | |
| break | |
| return sources |
🧰 Tools
🪛 Ruff (0.15.10)
[warning] 85-85: Loop control variable dirpath not used within loop body
Rename unused dirpath to _dirpath
(B007)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@test/tap/groups/check_groups.py` around lines 66 - 94, In find_source_tests,
avoid unconditionally adding files that merely end with "-t": change the loop
variable name dirpath to _dirpath, and for the extensionless branch (the
entry.endswith("-t") case) check that the file is executable before adding it
(use os.access(os.path.join(_dirpath, entry), os.X_OK)) so editor backups or
stray artifacts aren’t reported; keep the existing behavior for recognized
script extensions (SCRIPT_EXTS) and continue using entry[:-len(ext)] when adding
those entries.
…end_pid()
The first CI run of pgsql-retry_guard_in_txn_on_broken_backend-t (now
registered in legacy-g2) failed at "pg_terminate_backend signalled
successfully" because ProxySQL intercepts pg_backend_pid() and returns
its own thread_session_id (see PgSQL_Protocol.cpp:1398), not the real
backend PID.
Postgres server log from the failing run made this unambiguous:
[239] LOG: statement: BEGIN
[239] LOG: statement: SELECT pg_sleep(3)
[240] LOG: statement: SELECT pg_terminate_backend(22)
[240] WARNING: PID 22 is not a PostgreSQL backend process
[239] LOG: statement: ROLLBACK
Real backend is PID 239. "22" came from ProxySQL's fake-PID response,
so the kill went nowhere, pg_sleep(3) ran to completion, and the post-
fix FATAL_ERROR assertion never had a chance to be exercised — the
regression guard was trivially passing the bug through.
Replace the pg_backend_pid() probe with a pg_stat_activity lookup
issued from the direct superuser connection:
- embed a unique literal marker ("retry_guard_marker_<time>_<pid>")
in the sleep query so the backend running it is trivially
identifiable;
- do the find + terminate in one PQexecParams round-trip against
pg_stat_activity, so the pid can't change between lookup and kill.
plan() drops from 6 to 5 because we no longer have a separate "got a
pid" assertion. On a build that includes 68c76eb all 5 pass; on a
build without the fix the FATAL_ERROR assertion still fails as
before.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
test/tap/tests/pgsql-retry_guard_in_txn_on_broken_backend-t.cpp (1)
122-152: Optional: tolerate a slow query registration inpg_stat_activity.The killer waits 500ms then does a single
pg_stat_activitylookup. On a loaded CI host the in-flightpg_sleepquery may not yet be visible asstate='active'at that instant, which would yield 0 rows, leavekill_delivered=false, and fail the assertion on line 162 even though the fix is present. Since the in-flight query sleeps for 3s, a short bounded retry (e.g., up to ~2s, 100–200ms intervals) on an empty result set would make the test more robust without weakening it. Not a correctness bug — just a flake-hardening suggestion.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test/tap/tests/pgsql-retry_guard_in_txn_on_broken_backend-t.cpp` around lines 122 - 152, The killer thread's single-shot pg_stat_activity lookup (in the lambda that creates PGconn* direct and runs the find_and_kill query via PQexecParams) can return 0 rows on slow CI; change that block to retry the parameterized lookup when PQresultStatus is PGRES_TUPLES_OK but PQntuples(kr) == 0 (and for transient errors) in a short loop (e.g., total timeout ~2s with 100–200ms sleeps) until either a row is found or the timeout elapses, then set kill_delivered as before when a tuple is returned; ensure PQclear is called each iteration and break on success or non-recoverable error so we don't leak results or connections.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@test/tap/tests/pgsql-retry_guard_in_txn_on_broken_backend-t.cpp`:
- Around line 122-152: The killer thread's single-shot pg_stat_activity lookup
(in the lambda that creates PGconn* direct and runs the find_and_kill query via
PQexecParams) can return 0 rows on slow CI; change that block to retry the
parameterized lookup when PQresultStatus is PGRES_TUPLES_OK but PQntuples(kr) ==
0 (and for transient errors) in a short loop (e.g., total timeout ~2s with
100–200ms sleeps) until either a row is found or the timeout elapses, then set
kill_delivered as before when a tuple is returned; ensure PQclear is called each
iteration and break on success or non-recoverable error so we don't leak results
or connections.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 3dc0ca59-569b-45ca-b39a-16e59ac420dd
📒 Files selected for processing (1)
test/tap/tests/pgsql-retry_guard_in_txn_on_broken_backend-t.cpp
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
- GitHub Check: CI-builds / builds (ubuntu22,-tap)
- GitHub Check: CI-builds / builds (ubuntu24,-tap-genai-gcov)
- GitHub Check: CI-builds / builds (debian12,-dbg)
- GitHub Check: run / trigger
- GitHub Check: claude-review
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{cpp,h,hpp}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{cpp,h,hpp}: Class names should use PascalCase with protocol prefixes (MySQL_,PgSQL_,ProxySQL_)
Member variables should use snake_case naming
Constants and macros should use UPPER_SNAKE_CASE naming
C++17 is required; use conditional compilation via#ifdef PROXYSQLGENAI,#ifdef PROXYSQL31, etc. for feature-gated code
Use jemalloc for memory allocation
Use pthread mutexes for synchronization andstd::atomic<>for counters
Files:
test/tap/tests/pgsql-retry_guard_in_txn_on_broken_backend-t.cpp
test/tap/tests/*-t.cpp
📄 CodeRabbit inference engine (CLAUDE.md)
Test files should follow naming pattern
test_*.cppor*-t.cppintest/tap/tests/
Files:
test/tap/tests/pgsql-retry_guard_in_txn_on_broken_backend-t.cpp
🧠 Learnings (8)
📓 Common learnings
Learnt from: yuji-hatakeyama
Repo: sysown/proxysql PR: 5548
File: lib/mysql_connection.cpp:1837-1843
Timestamp: 2026-03-26T16:39:02.446Z
Learning: In ProxySQL's lib/mysql_connection.cpp, `SHOW WARNINGS` detection for both `update_warning_count_from_connection()` and the `add_eof()` call in `ASYNC_USE_RESULT_CONT` intentionally uses `myds->sess->CurrentQuery.QueryParserArgs.digest_text` (comment-stripped digest text). This means the fix/feature does not work when `mysql-query_digests_keep_comment=1` (digest_text contains comments) or `mysql-query_digests=0` (digest_text is unavailable) — these configurations are explicitly excluded from the regression test for `reg_test_5306-show_warnings_with_comment-t`. This design is consistent across the codebase and is an accepted, documented limitation.
📚 Learning: 2026-04-01T21:27:03.216Z
Learnt from: wazir-ahmed
Repo: sysown/proxysql PR: 5557
File: test/tap/tests/unit/gtid_set_unit-t.cpp:14-17
Timestamp: 2026-04-01T21:27:03.216Z
Learning: In ProxySQL's unit test directory (test/tap/tests/unit/), test_globals.h and test_init.h are only required for tests that depend on the ProxySQL runtime globals/initialization (i.e., tests that exercise components linked against libproxysql.a). Pure data-structure or utility tests (e.g., ezoption_parser_unit-t.cpp, gtid_set_unit-t.cpp, gtid_trxid_interval_unit-t.cpp) only need tap.h and the relevant project header — omitting test_globals.h and test_init.h is correct and intentional in these cases.
Applied to files:
test/tap/tests/pgsql-retry_guard_in_txn_on_broken_backend-t.cpp
📚 Learning: 2026-04-17T16:31:58.178Z
Learnt from: CR
Repo: sysown/proxysql PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-17T16:31:58.178Z
Learning: Applies to test/tap/tests/unit/**/*.cpp : Unit tests must use `test_globals.h` and `test_init.h` and link against `libproxysql.a` via the custom test harness
Applied to files:
test/tap/tests/pgsql-retry_guard_in_txn_on_broken_backend-t.cpp
📚 Learning: 2026-01-20T09:34:19.124Z
Learnt from: yuji-hatakeyama
Repo: sysown/proxysql PR: 5307
File: test/tap/tests/reg_test_5306-show_warnings_with_comment-t.cpp:39-48
Timestamp: 2026-01-20T09:34:19.124Z
Learning: In ProxySQL's TAP test suite, resource leaks (e.g., not calling mysql_close() on early return paths) are commonly tolerated because test processes are short-lived and OS frees resources on exit. This pattern applies to all C++ test files under test/tap/tests. When reviewing, recognize this as a project-wide test convention and focus on test correctness and isolation rather than insisting on fixing such leaks in these test files.
Applied to files:
test/tap/tests/pgsql-retry_guard_in_txn_on_broken_backend-t.cpp
📚 Learning: 2026-04-17T16:31:58.178Z
Learnt from: CR
Repo: sysown/proxysql PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-17T16:31:58.178Z
Learning: Applies to **/*.{cpp,h,hpp} : C++17 is required; use conditional compilation via `#ifdef PROXYSQLGENAI`, `#ifdef PROXYSQL31`, etc. for feature-gated code
Applied to files:
test/tap/tests/pgsql-retry_guard_in_txn_on_broken_backend-t.cpp
📚 Learning: 2026-04-17T16:31:58.178Z
Learnt from: CR
Repo: sysown/proxysql PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-17T16:31:58.178Z
Learning: Applies to test/tap/tests/*-t.cpp : Test files should follow naming pattern `test_*.cpp` or `*-t.cpp` in `test/tap/tests/`
Applied to files:
test/tap/tests/pgsql-retry_guard_in_txn_on_broken_backend-t.cpp
📚 Learning: 2026-03-26T16:39:02.446Z
Learnt from: yuji-hatakeyama
Repo: sysown/proxysql PR: 5548
File: lib/mysql_connection.cpp:1837-1843
Timestamp: 2026-03-26T16:39:02.446Z
Learning: In ProxySQL's lib/mysql_connection.cpp, `SHOW WARNINGS` detection for both `update_warning_count_from_connection()` and the `add_eof()` call in `ASYNC_USE_RESULT_CONT` intentionally uses `myds->sess->CurrentQuery.QueryParserArgs.digest_text` (comment-stripped digest text). This means the fix/feature does not work when `mysql-query_digests_keep_comment=1` (digest_text contains comments) or `mysql-query_digests=0` (digest_text is unavailable) — these configurations are explicitly excluded from the regression test for `reg_test_5306-show_warnings_with_comment-t`. This design is consistent across the codebase and is an accepted, documented limitation.
Applied to files:
test/tap/tests/pgsql-retry_guard_in_txn_on_broken_backend-t.cpp
📚 Learning: 2026-01-20T07:40:34.938Z
Learnt from: yuji-hatakeyama
Repo: sysown/proxysql PR: 5307
File: test/tap/tests/reg_test_5306-show_warnings_with_comment-t.cpp:24-28
Timestamp: 2026-01-20T07:40:34.938Z
Learning: In ProxySQL test files, calling `mysql_error(NULL)` after `mysql_init()` failure is safe because the MariaDB client library implementation returns an empty string for NULL handles (not undefined behavior).
Applied to files:
test/tap/tests/pgsql-retry_guard_in_txn_on_broken_backend-t.cpp
🪛 Clang (14.0.6)
test/tap/tests/pgsql-retry_guard_in_txn_on_broken_backend-t.cpp
[error] 47-47: 'unistd.h' file not found
(clang-diagnostic-error)
🔇 Additional comments (1)
test/tap/tests/pgsql-retry_guard_in_txn_on_broken_backend-t.cpp (1)
47-47: Keep<unistd.h>; it's required forgetpid().A prior review suggested removing this include, but line 111 calls
getpid(), which on POSIX is declared in<unistd.h>. Removing it would rely on transitive inclusion and is not portable. The Clang static-analysis "file not found" is a sandbox header-path issue, not a real defect. Leave as-is.
Previously, when a PgSQL backend died mid-transaction, ProxySQL's handler_minus1_ClientLibraryError refused to silently retry the failed statement on a fresh backend (correct, per 68c76eb) but then also tore down the client session by returning handler_ret=-1. The application had to reconnect before it could make forward progress, even though Postgres's wire protocol has enough granularity to let us signal the client "your transaction is aborted, send ROLLBACK to recover" while keeping the socket open. This change introduces that alternative path, gated behind a new admin variable pgsql-preserve_client_on_broken_backend_in_tx (default true). When the backend dies mid-transaction with the admin var on and the result-set transfer to the client has not yet started, ProxySQL now synthesizes on the client OUT queue: * ErrorResponse severity=ERROR sqlstate=25P02 "current transaction is aborted, commands ignored until end of transaction block" * NoticeResponse carrying the backend's original error-message text (no leak of the original 57P01 sqlstate — per design, the only client-visible sqlstate is the synthesized 25P02) * ReadyForQuery('E') — libpq now reports PQTRANS_INERROR. The session is marked tx_poisoned. handler_special_queries gains an earliest gate that dispatches to handler_poisoned_simple_query before query rules / digests / routing: * ROLLBACK / ROLLBACK TO SAVEPOINT / ABORT -> synthesize CommandComplete('ROLLBACK') + ReadyForQuery('I'), clear the flag. * COMMIT / END -> match Postgres native: emit NoticeResponse with "there is no transaction in progress" then the same synthesized ROLLBACK, clear the flag. * RELEASE SAVEPOINT -> ERROR 25P02 (matches Postgres native). * Anything else -> ERROR 25P02, stay poisoned. Extended Query (Parse/Bind/Describe/Close/Execute/Sync) while poisoned is rejected wholesale in V1: P/B/D/C/E are swallowed, Sync emits ErrorResponse + ReadyForQuery('E'). The client can always recover via a Simple-Query ROLLBACK. Extended-query recovery is deliberate future work (documented in the accompanying issue). The fallback paths stay intact. With the admin var off, or when the result-set transfer has already started to the client, the session is terminated as before — preserving pre-feature behavior for operators who rely on session termination as an app-layer signal. Three counters expose the feature's activity on stats_pgsql_global: pgsql_tx_poisoned_total pgsql_tx_poisoned_recovered_total pgsql_tx_poisoned_rejected_statements_total Each PgSQL thread maintains its own lock-free copy; PgSQL_Threads_Handler aggregates across threads when the admin table is rendered. Prometheus wiring for these counters is deliberately not added here — the existing PgSQL Prometheus pipeline (p_gauge_array) is incomplete even for active_transactions. Follow-up work. tx_poisoned resets on PgSQL_Session::reset(). No retry-loop interacts with the flag; session handoff to a fresh backend only happens after the client itself sends ROLLBACK/COMMIT/ABORT to clear it. Related: issue #5658.
pgsql-tx_poisoned_recovery-t covers the whole UX flow added by the
preceding commit and the admin-var-off fallback:
Case A — ROLLBACK recovery path
* Mid-tx backend kill produces ErrorResponse severity=ERROR sqlstate=25P02
(asserts no leak of the original FATAL / 57P01).
* PQstatus stays CONNECTION_OK, PQtransactionStatus is PQTRANS_INERROR.
* Any non-end-of-tx statement while poisoned returns 25P02.
* RELEASE SAVEPOINT while poisoned returns 25P02 (matches PG native).
* ROLLBACK returns PGRES_COMMAND_OK and the session goes to PQTRANS_IDLE.
* A subsequent SELECT 1 succeeds on a different backend pid than the
one that died (discovered via pg_stat_activity from a direct
superuser connection, since pg_backend_pid() via ProxySQL returns
thread_session_id, not the real backend pid — see the sibling
pgsql-retry_guard_in_txn_on_broken_backend-t).
* The three new stats counters (pgsql_tx_poisoned_total,
pgsql_tx_poisoned_recovered_total,
pgsql_tx_poisoned_rejected_statements_total) are read from
stats_pgsql_global via the admin port and asserted to have
incremented.
Case B — COMMIT recovery path
* COMMIT inside a poisoned tx returns PGRES_COMMAND_OK with command
tag 'ROLLBACK' (matches PG native — PG itself rolls back on COMMIT
inside an aborted tx).
Case C — admin variable off
* Toggle pgsql-preserve_client_on_broken_backend_in_tx=false, repeat
the mid-tx kill, assert the client session is terminated (pre-
feature fallback behavior preserved). The admin variable is
restored to true at test exit so subsequent tests in the group
see the default.
Registered in legacy-g2 alongside its sibling regression guard so
both run together on the same infra.
Related: issue #5658.
Two small fixes caught by running the test against the local legacy-g2 infra: 1. Connect to ProxySQL admin via the PgSQL-protocol admin port (cl.pgsql_admin_host / cl.pgsql_admin_port = 6132), not the MySQL-protocol admin port (6032). libpq on 6032 gets back the MySQL server greeting byte and fails with "expected authentication request from server, but received J". All three counter reads and the admin-variable toggles go through the same helper, so this single flip fixes both flows. 2. plan(15), not plan(16). The test has 15 distinct ok() assertions; the prior plan(16) left one slot unfilled and the TAP runner reported the test as failed even when every assertion passed. With both fixes, the test now passes 15/15 against the live legacy-g2 infra (PgSQL 16 single-node). counters before/after the ROLLBACK path: total 6->7 , recovered 4->5 , rejected 6->8 (two rejects = one for SELECT 42 while poisoned, one for RELEASE SAVEPOINT while poisoned).
New sibling to pgsql-tx_poisoned_recovery-t, plus a one-line semantics
fix in the extended-query poison gate.
Semantics fix (lib/PgSQL_Session.cpp):
Before, the pgsql_tx_poisoned_rejected_statements_total counter was
incremented on EVERY swallowed ext-query wire packet — one per Parse /
Bind / Describe / Close / Execute / Sync. That made a single logical
PQexecParams call count as 4+, inconsistent with the simple-query
path where a 'Q' packet bumps it exactly once.
Now P/B/D/C/E are silently swallowed (no counter bump), and Sync is
the single accounting point — the counter value matches the number
of client-visible "rejected statements" regardless of protocol.
New test pgsql-tx_poisoned_extended_query-t (plan(11)):
* Poison the session via a simple-query pg_sleep + mid-tx kill.
* While poisoned:
- PQexecParams("SELECT 42", 0 params) returns 25P02.
- PQprepare("stmt_poisoned", "SELECT 43") returns 25P02.
- PQexecPrepared on that name returns 25P02.
* The rejected counter increments by exactly 3 (one per logical
ext-query call), not by the 8-11 wire packets that libpq actually
sends. This assertion IS the mechanical proof that the semantics
fix above took effect.
* Simple-query ROLLBACK recovers the session.
* Post-recovery PQexecParams and PQprepare+PQexecPrepared both
execute cleanly and return rows.
Registered alongside the simple-query sibling in legacy-g2. Verified
against a live legacy-g2 infra: both tests pass (11/11 and 15/15),
counter deltas exactly match expected per-statement semantics.
Consolidated feedback from four independent reviews (correctness, security, test-coverage, code-quality). Changes, in four logical groups: 1. Extend coverage to clean-ErrorResponse backend-death path (correctness review #2). The most common real-world mid-tx backend-death scenarios — graceful postgres shutdown (57P01), crash shutdown (57P02), connect-now-refused (57P03) — deliver a clean ErrorResponse on an otherwise-reusable connection, so they reach handler_minus1_HandleErrorCodes, not handler_minus1_ClientLibraryError. Pre-fix, poison only fired on the connection-broken path. Now it fires on both. The caller at PgSQL_Session.cpp:~3222 grew a tx_poisoned check that mirrors the one already added at :3214 for the reusable-state=false path. 2. Tighten the poison preflight to true in-tx only (security F1). Previously gated on myconn->IsActiveTransaction(), which returns true via the disconnect-heuristic set by 68c76eb's compute_unknown_transaction_status — even for a plain autocommit statement that happened to be killed mid-query. Now gates on is_in_transaction() via transaction_state_manager, which tracks explicit BEGIN/COMMIT/ROLLBACK. A non-tx statement killed mid-flight no longer gets a misleading "current transaction is aborted" synthesized error. 3. Harden the simple-query classifier (correctness #3/#4/#5, test review 1.11). - Trim the trailing null terminator from the Simple Query payload before applying the "no trailing content" check, otherwise valid statements like "ROLLBACK\0" / "ROLLBACK WORK\0" fall through to KIND_REJECT. - Skip SQL line comments (-- to EOL) and block comments (/* */ non-nesting) in leading whitespace, so ORM-prepended comments don't block recovery. - After matching ROLLBACK/ABORT/COMMIT/END, require the rest of the statement to be a plain whole-tx end (optional WORK/ TRANSACTION noise word + optional trailing ';'). Reject: * ROLLBACK TO SAVEPOINT (partial rollback — can't honor on a dead backend) * ROLLBACK AND CHAIN (chained tx — same) * ROLLBACK; SELECT 1; (multi-statement — we'd silently drop the second stmt) Rejection is ERROR 25P02 + stay poisoned. 4. Code-quality refactor (simplify review #1, correctness #7/#8, security F2/F3). - Centralize the ErrorResponse(25P02) + ReadyForQuery('E') packet construction via write_tx_poisoned_error(). Four sites collapse to one-liners; message string lives in one place. - Fix NoticeResponse SQLSTATEs: WARNING-class notices now use ERRCODE_WARNING (01000, generic) for the backend- context notice and ERRCODE_NO_ACTIVE_SQL_TRANSACTION (25P01) for the "there is no transaction in progress" notice on the COMMIT-on-poisoned path. Previously both used ERRCODE_SUCCESSFUL_COMPLETION (00000) which is semantically wrong for a WARNING. - Cap backend error text to 4 KB and strip embedded NUL bytes before re-embedding in a NoticeResponse. Prevents memory-amplification if a compromised/rogue backend sends multi-MB error text, and prevents silent truncation at the first NUL (PG_pkt::put_string -> strlen). - Enrich the poison-event proxy_warning with hostgroup, backend addr:port, backend PID, client addr, user, dbname, query digest, and the full backend error code+message. Mirrors handler_minus1_LogErrorDuringQuery so operators can correlate poison events with the triggering query. Related to PR #5659 and issue #5658.
Driven by the test-coverage review findings. Seven substantive changes across both test files: 1. NoticeResponse payload is now inspected (review 1.1/1.2). Install PQsetNoticeReceiver with a NoticeBag collector. Assert the poison NoticeResponse has severity=WARNING, sqlstate=01000, and non-empty message; and that NO notice carries sqlstate 57P01 (backend-leak) or 00000 (wrong pre-fix code). Without this, the no-57P01-leak invariant called out in issue #5658 was unverified. 2. COMMIT-on-poisoned notice payload is inspected (review 1.4 / 3.3). Case B now asserts the 25P01 "there is no transaction in progress" notice is emitted alongside the synthesized ROLLBACK command tag — the other half of the PG-native parity contract. 3. ROLLBACK TO SAVEPOINT, AND CHAIN, multi-statement, and ROLLBACKET word-boundary are exercised (review 1.3/1.14). Added as a batch alongside SELECT 42 and RELEASE SAVEPOINT, each asserted to return 25P02 and stay poisoned. 4. Autocommit-no-poison (case C) + admin-off-no-counter-move (case D) are now explicit (review 1.8 / 2.4). Pre-fix behavior: a non-tx statement killed mid-flight bumped the total counter because the feature used IsActiveTransaction() (polluted by the disconnect heuristic). Case C confirms the is_in_transaction() tightening. Case D confirms that toggling the admin var off truly disables the feature (no counter movement), not just preserves client termination. 5. Counter deltas are EXACT (==), not >= (review 2.1). Recovery test's +1/+1/+6 and ext-query test's +3 are all equality-checked. A bug that over-counts (e.g. regressed to per-wire-packet accounting) would be caught. 6. Flakiness-resistance (review 4.1/4.2/4.3/4.4/5.3). The killer no longer sleeps 500 ms blindly; it polls pg_stat_activity up to 6 s for the marker backend and reports a clean diag-plus-skip if the kill didn't fire. This is the same pattern used by the sibling retry_guard test. kill_delivered is now surfaced on the return struct so the caller can distinguish "feature regressed" from "CI infra flaked". 7. Admin-variable hygiene (review 5.1/6.4). - RAII-scoped RestorePreserveClientAdminVar: restore to default (=true) on every exit path including failure. - UPDATE now uses the full 'pgsql-<name>' prefix on variable_name; the previous WHERE clause matched zero rows and silently left the runtime value unchanged (which is how the pre-fix case D appeared to pass PQstatus checks while counters still moved). - After LOAD PGSQL VARIABLES TO RUNTIME, open a burst of short- lived PGconns to trigger activity on every PgSQL worker, guaranteeing each thread's next poll() wake-up picks up the new __thread-local admin-var value. Eliminates the up-to-2s refresh lag (pgsql-poll_timeout) that otherwise leaves stale worker state when case D runs. Severity / severity (ext-query test only): the rejected_25P02_as_error() predicate now also checks severity=="ERROR", catching a regression that might emit FATAL 25P02 (libpq would close the socket on FATAL despite the 25P02 code). Verified: both tests pass 17/17 and 11/11 against a live legacy-g2 infra. Case D cleanly shows counters unchanged when admin is off; case C cleanly shows counters unchanged for an autocommit kill.
…tx_poisoned counters SonarCloud flagged 3 unbounded sprintf calls in SQL3_GlobalStatus() that write tx_poisoned counters into a fixed 256-byte buffer. Replace with snprintf(buf, sizeof(buf), ...) for bounds safety. Follow-up to PR #5659.
CI-maketest / builds (testgalera) has been red on every branch since commit a3883a8 (2026-04-12) which replaced the galera rules' use of the stub test/tap/tap/SQLite3_Server.cpp with the real src/SQLite3_Server.cpp. The stub was a type-inconsistency hazard, but the real source pulls in three new link-time problems that the galera Makefile rules did not accommodate: 1. Symbol collisions. src/SQLite3_Server.cpp has test-variant definitions of init_galera_ifaces_string() and populate_galera_table() gated by #ifdef TEST_GALERA (lines 1323 and 1424), while galera_1_timeout_count.cpp and galera_2_timeout_no_count.cpp provide their own versions of the same symbols plus SQLite3_Server_session_handler. Both land in the link and ld rejects with "multiple definition". Fix: -Wl,--allow-multiple-definition. The sibling anomaly_detection-t rule already uses this. 2. ClickHouse typeinfo. libproxysql.a pulls in ClickHouse_Server.oo, which references typeinfo for clickhouse::Column*. The galera rules do not link libclickhouse-cpp-lib.a, libzstdstatic.a, liblz4.a, and they do not add -I$(CLICKHOUSE_CPP_IDIR) / the corresponding -L paths. Copy the incantation from anomaly_detection-t. 3. Missing ProxySQL_Admin::enable_galera_testing(). This method is also #ifdef TEST_GALERA-gated (lib/ProxySQL_Admin.cpp:9305). libproxysql.a is built without TEST_GALERA, so the method is not in the archive. src/SQLite3_Server.cpp:1848 calls it under its own TEST_GALERA guard — linker error. Compile lib/ProxySQL_Admin.cpp fresh with -DTEST_GALERA alongside the galera test and SQLite3_Server.cpp. --allow-multiple-definition above handles the symbol overlap with libproxysql.a. Add -I$(COREDUMPER_IDIR) for ProxySQL_Admin.cpp's transitive include of coredumper/coredumper.h. Verified locally: make testgalera from scratch, rc=0, both binaries produced (124 MB each).
feat(pgsql): preserve client session on mid-tx backend death
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (2)
test/tap/tests/pgsql-tx_poisoned_recovery-t.cpp (1)
663-674: Minor: clean up the plan-accounting comment.The comment reads like working scratch notes ("wait counted = 11 -- let me recount manually below") rather than final documentation. The count is correct, just consider tightening the prose so the plan rationale reads cleanly for future maintainers.
✏️ Proposed cleanup
- // Plan breakdown: - // case A: 8 assertions (BEGIN, ERR shape, notice shape, no-57P01-in-notice, - // PQstatus, PQtransactionStatus, reject-batch, ROLLBACK WORK recovers, - // post-recovery SELECT 1, victim gone, counters) -- wait counted = 11 - // let me recount manually below. - // case A ok() count = 11 - // case B ok() count = 3 - // case C ok() count = 1 - // case D ok() count = 2 - // Total = 17 + // Plan breakdown (ok() calls per case on the success path; abort paths + // use emit_fail_fill() to match these counts): + // case A: 11 (BEGIN, err shape, notice shape, no-57P01, PQstatus, + // PQtransactionStatus, reject-batch, ROLLBACK WORK, + // post-recovery SELECT, victim-gone, counters) + // case B: 3 (COMMIT recovers, 25P01 notice, counters) + // case C: 1 (counters unchanged) + // case D: 2 (session terminated, counters unchanged) plan(17);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test/tap/tests/pgsql-tx_poisoned_recovery-t.cpp` around lines 663 - 674, The plan-accounting comment inside main near the plan(17) call is written as scratch notes; clean it up to concise documentation by removing informal phrases like "let me recount manually below" and "wait counted = 11", keep the breakdown of cases A–D and their assertion counts (case A = 11, B = 3, C = 1, D = 2) and ensure the total matches plan(17); update the comment text around main and plan(17) to a single clear sentence or brief bullet-style explanation so future maintainers understand the rationale without informal notes.lib/PgSQL_Session.cpp (1)
104-119: Rename the new constant to UPPER_SNAKE_CASE.
kTxPoisonedNoticeMaxLenviolates the repository constant naming rule.Suggested rename
-static constexpr unsigned int kTxPoisonedNoticeMaxLen = 4096; +static constexpr unsigned int TX_POISONED_NOTICE_MAX_LEN = 4096; static std::string sanitize_backend_err_msg(const std::string& src) { std::string out; - const size_t cap = std::min<size_t>(src.size(), kTxPoisonedNoticeMaxLen); + const size_t cap = std::min<size_t>(src.size(), TX_POISONED_NOTICE_MAX_LEN); out.reserve(cap); for (size_t i = 0; i < cap; ++i) { char c = src[i]; out.push_back(c == '\0' ? '?' : c); } - if (src.size() > kTxPoisonedNoticeMaxLen) { + if (src.size() > TX_POISONED_NOTICE_MAX_LEN) { out.append("...[truncated]"); }As per coding guidelines, Constants and macros should use UPPER_SNAKE_CASE naming.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/PgSQL_Session.cpp` around lines 104 - 119, Rename the constant kTxPoisonedNoticeMaxLen to an UPPER_SNAKE_CASE identifier (for example TX_POISONED_NOTICE_MAX_LEN) and update every reference to it (including the declaration and its use inside sanitize_backend_err_msg) so the code follows the repository constant naming convention; ensure the value and constexpr/unsigned int type remain unchanged and run a build to verify no remaining references to the old name.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@include/PgSQL_Session.h`:
- Around line 396-401: Update the misleading comment blocks that describe which
simple-query packets clear transaction poison: in the comment above the function
handler_poisoned_simple_query and the corresponding comment around the later
block (previously listing ROLLBACK TO SAVEPOINT as a recovery action), remove
ROLLBACK TO SAVEPOINT from the list of commands that "clear the poison" and
instead include it among the rejected statements that leave the session poisoned
(i.e., keep it in the ERROR 25P02/reject category); reference the
handler_poisoned_simple_query symbol to locate the first comment and the
matching poisoned-handling block for the second comment and make the wording
explicitly state that only ROLLBACK, COMMIT and ABORT clear poison while
ROLLBACK TO SAVEPOINT is rejected and retains the poison.
In `@lib/PgSQL_Session.cpp`:
- Around line 2747-2790: The log embeds the uncapped backend string via
myconn->get_error_code_with_message(), so replace that with the
already-sanitized/capped backend_err_msg (produced by sanitize_backend_err_msg)
when calling proxy_warning in this block (i.e., use backend_err_msg.c_str() or
build a small capped string combining code+backend_err_msg) so the warning log
cannot be amplified by an oversized backend message; update the proxy_warning
call site accordingly (references: backend_err_msg, proxy_warning,
get_error_code_with_message(), sanitize_backend_err_msg).
- Around line 2300-2328: The code treats Flush ('H') as unsupported in a
poisoned extended-query transaction; update the conditional in the tx_poisoned
check (the block that inspects command in PgSQL_Session.cpp where tx_poisoned
and command != 'Q' && command != 'X' are tested) to include 'H' alongside
'P','B','D','C','E' so that when tx_poisoned and command == 'H' the packet is
silently swallowed (l_free(pkt.size, pkt.ptr); continue;) just like
Parse/Bind/Describe/Close/Execute, leaving the single Sync-path response
unchanged.
- Around line 495-575: skip_ws_and_comments currently swallows unterminated /*
... */ and should return a bool to signal malformed input; change its signature
to return false on malformed/unterminated block comments and true on success,
then update all call sites (the initial call skip_ws_and_comments(sql, sql_len)
and the three subsequent calls on rest at the matches_kw checks and semicolon
handling) to check the return value and set kind = KIND_REJECT if it returns
false; ensure you propagate the same behavior for the optional WORK/TRANSACTION
consumption paths so any failed comment-skip forces rejection and leaves
tx_poisoned intact.
In `@test/tap/tests/Makefile`:
- Around line 191-195: The Makefile targets galera_1_timeout_count and
galera_2_timeout_no_count compile ../../../src/SQLite3_Server.cpp and
../../../lib/ProxySQL_Admin.cpp but do not list them as prerequisites; update
the rules for targets galera_1_timeout_count and galera_2_timeout_no_count to
include ../../../src/SQLite3_Server.cpp and ../../../lib/ProxySQL_Admin.cpp in
their prerequisite lists so changes to those sources trigger a rebuild of the
corresponding binaries.
In `@test/tap/tests/pgsql-tx_poisoned_extended_query-t.cpp`:
- Around line 68-76: set_admin_bool currently treats a 0-row UPDATE as success
and RestorePreserveClientAdminVar's destructor always restores true instead of
the original value, risking leaked global config; change set_admin_bool (the
function that runs the snprintf/PQexec sequence) to check the PGresult for
affected rows (use PQcmdTuples or equivalent) and return false when no rows were
updated, and modify RestorePreserveClientAdminVar to read and store the current
value of "pgsql-preserve_client_on_broken_backend_in_tx" before performing the
update and then restore that captured value in its destructor rather than
hardcoding true.
- Around line 252-257: Replace the failing ok(0, "SKIPPED: ...") loop with a
proper TAP skip: when kr.kill_delivered is false, emit a diagnostic with
diag("poison: killer failed to deliver pg_terminate_backend within 6s") and call
skip(9, "poison did not fire") to mark the remaining 9 tests as skipped; still
ensure you PQclear(kr.pgresult), PQfinish(cli) and return exit_status() in that
branch (references: kr.kill_delivered, kr.pgresult, PQclear, PQfinish,
exit_status).
- Around line 259-263: The diagnostic arguments dereference kr.pgresult
unconditionally and use the GNU ?: extension; change the three inline
expressions to be null-safe and C++17-compliant by guarding
PQresultStatus/PQresultErrorField calls with a kr.pgresult check and replacing
?: with the standard ternary operator. For example, when building the
status/severity/sqlstate strings for the ok(...) call in the test where
rejected_25P02_as_error(kr.pgresult) is used, compute temporary const char* (or
std::string) values using checks like kr.pgresult ?
PQresStatus(PQresultStatus(kr.pgresult)) : "(none)" and kr.pgresult &&
PQresultErrorField(kr.pgresult, PG_DIAG_SEVERITY) ?
PQresultErrorField(kr.pgresult, PG_DIAG_SEVERITY) : "(none)" (and similarly for
PG_DIAG_SQLSTATE), then pass those temporaries into the diagnostic format
instead of dereferencing kr.pgresult inline.
In `@test/tap/tests/pgsql-tx_poisoned_recovery-t.cpp`:
- Around line 176-198: Remove the unused NoticeCollector struct and its callback
stub (symbols: NoticeCollector, NoticeCollector::callback) from the test file to
avoid confusion; ensure there are no references to it elsewhere (tests use
NoticeBag and notice_receiver_cb), delete the whole struct and its trailing
explanatory comment block so the file only contains the active notice-handling
implementation (NoticeBag/notice_receiver_cb).
---
Nitpick comments:
In `@lib/PgSQL_Session.cpp`:
- Around line 104-119: Rename the constant kTxPoisonedNoticeMaxLen to an
UPPER_SNAKE_CASE identifier (for example TX_POISONED_NOTICE_MAX_LEN) and update
every reference to it (including the declaration and its use inside
sanitize_backend_err_msg) so the code follows the repository constant naming
convention; ensure the value and constexpr/unsigned int type remain unchanged
and run a build to verify no remaining references to the old name.
In `@test/tap/tests/pgsql-tx_poisoned_recovery-t.cpp`:
- Around line 663-674: The plan-accounting comment inside main near the plan(17)
call is written as scratch notes; clean it up to concise documentation by
removing informal phrases like "let me recount manually below" and "wait counted
= 11", keep the breakdown of cases A–D and their assertion counts (case A = 11,
B = 3, C = 1, D = 2) and ensure the total matches plan(17); update the comment
text around main and plan(17) to a single clear sentence or brief bullet-style
explanation so future maintainers understand the rationale without informal
notes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 0af022c8-e51e-4ebf-8b79-afb8fad68cf2
📒 Files selected for processing (9)
include/PgSQL_Session.hinclude/PgSQL_Thread.hinclude/proxysql_structs.hlib/PgSQL_Session.cpplib/PgSQL_Thread.cpptest/tap/groups/groups.jsontest/tap/tests/Makefiletest/tap/tests/pgsql-tx_poisoned_extended_query-t.cpptest/tap/tests/pgsql-tx_poisoned_recovery-t.cpp
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
- GitHub Check: CI-builds / builds (debian12,-dbg)
- GitHub Check: CI-builds / builds (ubuntu24,-tap-genai-gcov)
- GitHub Check: CI-builds / builds (ubuntu22,-tap)
- GitHub Check: run / trigger
- GitHub Check: claude-review
🧰 Additional context used
📓 Path-based instructions (5)
include/**/*.{h,hpp}
📄 CodeRabbit inference engine (CLAUDE.md)
Use
#ifndef __CLASS_*_Hinclude guards in header files
Files:
include/proxysql_structs.hinclude/PgSQL_Session.hinclude/PgSQL_Thread.h
**/*.{cpp,h,hpp}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{cpp,h,hpp}: Class names should use PascalCase with protocol prefixes (MySQL_,PgSQL_,ProxySQL_)
Member variables should use snake_case naming
Constants and macros should use UPPER_SNAKE_CASE naming
C++17 is required; use conditional compilation via#ifdef PROXYSQLGENAI,#ifdef PROXYSQL31, etc. for feature-gated code
Use jemalloc for memory allocation
Use pthread mutexes for synchronization andstd::atomic<>for counters
Files:
include/proxysql_structs.hinclude/PgSQL_Session.hlib/PgSQL_Thread.cpptest/tap/tests/pgsql-tx_poisoned_recovery-t.cppinclude/PgSQL_Thread.htest/tap/tests/pgsql-tx_poisoned_extended_query-t.cpplib/PgSQL_Session.cpp
test/tap/groups/groups.json
📄 CodeRabbit inference engine (CLAUDE.md)
Register new test files in
test/tap/groups/groups.jsonfor test discovery via pattern rules intest/tap/tests/Makefile
Files:
test/tap/groups/groups.json
lib/**/*.cpp
📄 CodeRabbit inference engine (CLAUDE.md)
One class per file typically in
lib/directory
Files:
lib/PgSQL_Thread.cpplib/PgSQL_Session.cpp
test/tap/tests/*-t.cpp
📄 CodeRabbit inference engine (CLAUDE.md)
Test files should follow naming pattern
test_*.cppor*-t.cppintest/tap/tests/
Files:
test/tap/tests/pgsql-tx_poisoned_recovery-t.cpptest/tap/tests/pgsql-tx_poisoned_extended_query-t.cpp
🧠 Learnings (13)
📓 Common learnings
Learnt from: yuji-hatakeyama
Repo: sysown/proxysql PR: 5548
File: lib/mysql_connection.cpp:1837-1843
Timestamp: 2026-03-26T16:39:02.446Z
Learning: In ProxySQL's lib/mysql_connection.cpp, `SHOW WARNINGS` detection for both `update_warning_count_from_connection()` and the `add_eof()` call in `ASYNC_USE_RESULT_CONT` intentionally uses `myds->sess->CurrentQuery.QueryParserArgs.digest_text` (comment-stripped digest text). This means the fix/feature does not work when `mysql-query_digests_keep_comment=1` (digest_text contains comments) or `mysql-query_digests=0` (digest_text is unavailable) — these configurations are explicitly excluded from the regression test for `reg_test_5306-show_warnings_with_comment-t`. This design is consistent across the codebase and is an accepted, documented limitation.
📚 Learning: 2026-04-17T16:31:58.196Z
Learnt from: CR
Repo: sysown/proxysql PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-17T16:31:58.196Z
Learning: Applies to test/tap/groups/groups.json : Register new test files in `test/tap/groups/groups.json` for test discovery via pattern rules in `test/tap/tests/Makefile`
Applied to files:
test/tap/groups/groups.json
📚 Learning: 2026-04-01T21:27:03.216Z
Learnt from: wazir-ahmed
Repo: sysown/proxysql PR: 5557
File: test/tap/tests/unit/gtid_set_unit-t.cpp:14-17
Timestamp: 2026-04-01T21:27:03.216Z
Learning: In ProxySQL's unit test directory (test/tap/tests/unit/), test_globals.h and test_init.h are only required for tests that depend on the ProxySQL runtime globals/initialization (i.e., tests that exercise components linked against libproxysql.a). Pure data-structure or utility tests (e.g., ezoption_parser_unit-t.cpp, gtid_set_unit-t.cpp, gtid_trxid_interval_unit-t.cpp) only need tap.h and the relevant project header — omitting test_globals.h and test_init.h is correct and intentional in these cases.
Applied to files:
test/tap/groups/groups.jsontest/tap/tests/Makefiletest/tap/tests/pgsql-tx_poisoned_recovery-t.cpptest/tap/tests/pgsql-tx_poisoned_extended_query-t.cpplib/PgSQL_Session.cpp
📚 Learning: 2026-01-14T11:36:39.934Z
Learnt from: rahim-kanji
Repo: sysown/proxysql PR: 5299
File: lib/PgSQL_Session.cpp:5173-5233
Timestamp: 2026-01-14T11:36:39.934Z
Learning: The function `PgSQL_Session::send_parameter_error_response()` in `lib/PgSQL_Session.cpp` has a default parameter for its second argument (PGSQL_ERROR_CODES code) with the default value of PGSQL_ERROR_CODES::ERRCODE_INVALID_TEXT_REPRESENTATION, as declared in include/PgSQL_Session.h. Single-argument calls to this function are valid.
Applied to files:
include/PgSQL_Session.hlib/PgSQL_Session.cpp
📚 Learning: 2026-04-17T16:31:58.196Z
Learnt from: CR
Repo: sysown/proxysql PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-17T16:31:58.196Z
Learning: Applies to test/tap/tests/unit/**/*.cpp : Unit tests must use `test_globals.h` and `test_init.h` and link against `libproxysql.a` via the custom test harness
Applied to files:
test/tap/tests/Makefiletest/tap/tests/pgsql-tx_poisoned_recovery-t.cpptest/tap/tests/pgsql-tx_poisoned_extended_query-t.cpp
📚 Learning: 2026-04-17T16:31:58.196Z
Learnt from: CR
Repo: sysown/proxysql PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-17T16:31:58.196Z
Learning: Applies to **/*.{cpp,h,hpp} : C++17 is required; use conditional compilation via `#ifdef PROXYSQLGENAI`, `#ifdef PROXYSQL31`, etc. for feature-gated code
Applied to files:
test/tap/tests/Makefilelib/PgSQL_Session.cpp
📚 Learning: 2026-01-20T09:34:27.165Z
Learnt from: yuji-hatakeyama
Repo: sysown/proxysql PR: 5307
File: test/tap/tests/reg_test_5306-show_warnings_with_comment-t.cpp:39-48
Timestamp: 2026-01-20T09:34:27.165Z
Learning: In ProxySQL test files (test/tap/tests/), resource leaks (such as not calling `mysql_close()` on early return paths) are not typically fixed because test processes are short-lived and the OS frees resources on process exit. This is a common pattern across the test suite.
Applied to files:
test/tap/tests/Makefile
📚 Learning: 2026-04-17T16:31:58.196Z
Learnt from: CR
Repo: sysown/proxysql PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-17T16:31:58.196Z
Learning: Use feature flags `PROXYSQL31=1` for v3.1.x Innovative tier and `PROXYSQLGENAI=1` for v4.0.x AI/MCP tier; PROXYSQLGENAI implies both PROXYSQL31 and PROXYSQLFFTO/PROXYSQLTSDB
Applied to files:
lib/PgSQL_Thread.cpp
📚 Learning: 2026-04-17T16:31:58.196Z
Learnt from: CR
Repo: sysown/proxysql PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-17T16:31:58.196Z
Learning: Applies to test/tap/tests/*-t.cpp : Test files should follow naming pattern `test_*.cpp` or `*-t.cpp` in `test/tap/tests/`
Applied to files:
test/tap/tests/pgsql-tx_poisoned_recovery-t.cpp
📚 Learning: 2026-01-20T09:34:19.124Z
Learnt from: yuji-hatakeyama
Repo: sysown/proxysql PR: 5307
File: test/tap/tests/reg_test_5306-show_warnings_with_comment-t.cpp:39-48
Timestamp: 2026-01-20T09:34:19.124Z
Learning: In ProxySQL's TAP test suite, resource leaks (e.g., not calling mysql_close() on early return paths) are commonly tolerated because test processes are short-lived and OS frees resources on exit. This pattern applies to all C++ test files under test/tap/tests. When reviewing, recognize this as a project-wide test convention and focus on test correctness and isolation rather than insisting on fixing such leaks in these test files.
Applied to files:
test/tap/tests/pgsql-tx_poisoned_recovery-t.cpptest/tap/tests/pgsql-tx_poisoned_extended_query-t.cpp
📚 Learning: 2026-04-17T16:31:58.196Z
Learnt from: CR
Repo: sysown/proxysql PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-17T16:31:58.196Z
Learning: Applies to **/*.{cpp,h,hpp} : Use pthread mutexes for synchronization and `std::atomic<>` for counters
Applied to files:
include/PgSQL_Thread.h
📚 Learning: 2026-03-26T16:39:02.446Z
Learnt from: yuji-hatakeyama
Repo: sysown/proxysql PR: 5548
File: lib/mysql_connection.cpp:1837-1843
Timestamp: 2026-03-26T16:39:02.446Z
Learning: In ProxySQL's lib/mysql_connection.cpp, `SHOW WARNINGS` detection for both `update_warning_count_from_connection()` and the `add_eof()` call in `ASYNC_USE_RESULT_CONT` intentionally uses `myds->sess->CurrentQuery.QueryParserArgs.digest_text` (comment-stripped digest text). This means the fix/feature does not work when `mysql-query_digests_keep_comment=1` (digest_text contains comments) or `mysql-query_digests=0` (digest_text is unavailable) — these configurations are explicitly excluded from the regression test for `reg_test_5306-show_warnings_with_comment-t`. This design is consistent across the codebase and is an accepted, documented limitation.
Applied to files:
lib/PgSQL_Session.cpp
📚 Learning: 2026-01-20T07:40:34.938Z
Learnt from: yuji-hatakeyama
Repo: sysown/proxysql PR: 5307
File: test/tap/tests/reg_test_5306-show_warnings_with_comment-t.cpp:24-28
Timestamp: 2026-01-20T07:40:34.938Z
Learning: In ProxySQL test files, calling `mysql_error(NULL)` after `mysql_init()` failure is safe because the MariaDB client library implementation returns an empty string for NULL handles (not undefined behavior).
Applied to files:
lib/PgSQL_Session.cpp
🪛 Clang (14.0.6)
test/tap/tests/pgsql-tx_poisoned_recovery-t.cpp
[error] 48-48: 'unistd.h' file not found
(clang-diagnostic-error)
test/tap/tests/pgsql-tx_poisoned_extended_query-t.cpp
[error] 26-26: 'unistd.h' file not found
(clang-diagnostic-error)
🪛 Cppcheck (2.20.0)
lib/PgSQL_Thread.cpp
[warning] 4429-4429: If memory allocation fails, then there is a possible null pointer dereference
(nullPointerOutOfMemory)
[warning] 4431-4431: If memory allocation fails, then there is a possible null pointer dereference
(nullPointerOutOfMemory)
[warning] 4435-4435: If memory allocation fails, then there is a possible null pointer dereference
(nullPointerOutOfMemory)
[warning] 4437-4437: If memory allocation fails, then there is a possible null pointer dereference
(nullPointerOutOfMemory)
[warning] 4443-4443: If memory allocation fails, then there is a possible null pointer dereference
(nullPointerOutOfMemory)
[warning] 4445-4445: If memory allocation fails, then there is a possible null pointer dereference
(nullPointerOutOfMemory)
🔇 Additional comments (5)
test/tap/groups/groups.json (1)
151-160: LGTM: the new PostgreSQL TAP tests are registered.The three new test entries are discoverable via
groups.jsonand consistently assigned tolegacy-g2. As per coding guidelines,test/tap/groups/groups.json: Register new test files intest/tap/groups/groups.jsonfor test discovery via pattern rules intest/tap/tests/Makefile.include/proxysql_structs.h (1)
1110-1447: LGTM: the thread-local flag is declared consistently.The new PostgreSQL thread variable is added in both the defining and
externsections, matching the surrounding thread-local configuration pattern.lib/PgSQL_Thread.cpp (2)
379-379: Config flag wiring looks consistent.The new
preserve_client_on_broken_backend_in_txvariable is listed, defaulted, registered as a bool, and refreshed into the per-thread runtime flag.Also applies to: 1177-1177, 2203-2203, 3971-3971
4248-4250: Tx-poison counters are wired coherently.The counters are initialized per thread, rendered in
stats_pgsql_global, and aggregated with the same guard pattern as the existing status getters.Also applies to: 4427-4447, 5581-5601
include/PgSQL_Thread.h (1)
246-251: LGTM — consistent with existing per-thread counter pattern.The new per-thread counters match the established design in this struct (see the file-level comment on line 240-242: "status variables are per thread only... no need for atomic operation"), and the aggregation responsibility is delegated to
PgSQL_Threads_Handler. Naming and types follow convention.
| // While tx_poisoned, classify a 'Q' packet and either clear the poison | ||
| // and synthesize a ROLLBACK response (for ROLLBACK / COMMIT / ABORT / | ||
| // ROLLBACK TO SAVEPOINT) or reject with ERROR 25P02 (anything else). | ||
| // Returns true if the packet was handled here. Increments the | ||
| // pgsql_tx_poisoned_{recovered,rejected_statements}_total counters. | ||
| bool handler_poisoned_simple_query(PtrSize_t* pkt); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Inspect the poisoned simple-query classifier and tests for savepoint recovery behavior.
# Expected: ROLLBACK TO SAVEPOINT should not be classified as a full recovery command that clears tx_poisoned.
rg -n -C4 'ROLLBACK TO|SAVEPOINT|tx_poisoned|handler_poisoned_simple_query|PoisonTransaction' --iglob '*PgSQL_Session*' --iglob '*pgsql-tx_poisoned*'Repository: sysown/proxysql
Length of output: 33011
🌐 Web query:
PostgreSQL ROLLBACK TO SAVEPOINT transaction remains active ReadyForQuery transaction status
💡 Result:
In PostgreSQL, executing ROLLBACK TO SAVEPOINT rolls back changes made after the named savepoint within the current transaction but does not end the overall transaction. The transaction remains active, allowing further statements to be executed until an explicit COMMIT or (full) ROLLBACK is issued. After processing the ROLLBACK TO SAVEPOINT command successfully, the PostgreSQL backend sends a ReadyForQuery (RFQ) message to the frontend with transaction status indicator 'T' (in transaction block), confirming the connection is ready for the next command while still inside the active transaction. This is the expected behavior, not an error—the transaction status remains active (not 'I' for idle), as the top-level transaction continues. Only a full ROLLBACK, COMMIT, or session termination ends the transaction and returns the status to 'I' (idle). Savepoints create subtransactions, but rolling back to one keeps the parent transaction open.
Citations:
- 1: https://www.postgresql.org/docs/current/sql-rollback-to.html
- 2: https://www.postgresql.org/docs/15/sql-savepoint.html
- 3: https://www.postgresql.org/docs/current/sql-savepoint.html
- 4: https://github.com/postgres/postgres/blob/master/src/backend/access/transam/README
- 5: http://www.postgresql.org/docs/15/protocol-flow.html
- 6: https://www.postgresql.org/docs/current/protocol-message-formats.html
Fix misleading comments about transaction poison recovery.
The implementation correctly rejects ROLLBACK TO SAVEPOINT while poisoned (keeping the session poisoned), but multiple comments incorrectly list it as a recovery/clear case alongside ROLLBACK, COMMIT, and ABORT. PostgreSQL semantics confirm ROLLBACK TO SAVEPOINT leaves the outer transaction active and cannot be the sole recovery path on a dead backend. Update the comments at lines 396–401 and 484–489 to exclude ROLLBACK TO SAVEPOINT from recovery actions and clarify it belongs in the rejection category.
Comment fixes required
- // While tx_poisoned, classify a 'Q' packet and either clear the poison
- // and synthesize a ROLLBACK response (for ROLLBACK / COMMIT / ABORT /
- // ROLLBACK TO SAVEPOINT) or reject with ERROR 25P02 (anything else).
+ // While tx_poisoned, classify a 'Q' packet and either clear the poison
+ // and synthesize a ROLLBACK response (for full-transaction ROLLBACK /
+ // COMMIT / ABORT) or reject with ERROR 25P02 (anything else, including
+ // ROLLBACK TO SAVEPOINT because it does not end the transaction).- // ROLLBACK / ROLLBACK TO SAVEPOINT / ABORT -> synthesize
+ // ROLLBACK / ABORT -> synthesize
// CommandComplete('ROLLBACK') + ReadyForQuery('I'), clear flag.
// COMMIT -> same ROLLBACK response + NoticeResponse carrying the
// "there is no transaction in progress" warning, clear flag.
- // anything else (including RELEASE SAVEPOINT) -> reply ERROR 25P02
- // + ReadyForQuery('E'), stay poisoned.
+ // anything else (including ROLLBACK TO SAVEPOINT and RELEASE SAVEPOINT)
+ // -> reply ERROR 25P02 + ReadyForQuery('E'), stay poisoned.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@include/PgSQL_Session.h` around lines 396 - 401, Update the misleading
comment blocks that describe which simple-query packets clear transaction
poison: in the comment above the function handler_poisoned_simple_query and the
corresponding comment around the later block (previously listing ROLLBACK TO
SAVEPOINT as a recovery action), remove ROLLBACK TO SAVEPOINT from the list of
commands that "clear the poison" and instead include it among the rejected
statements that leave the session poisoned (i.e., keep it in the ERROR
25P02/reject category); reference the handler_poisoned_simple_query symbol to
locate the first comment and the matching poisoned-handling block for the second
comment and make the wording explicitly state that only ROLLBACK, COMMIT and
ABORT clear poison while ROLLBACK TO SAVEPOINT is rejected and retains the
poison.
| // skip whitespace and SQL comments (-- to EOL, /* ... */ non-nesting) in-place. | ||
| auto skip_ws_and_comments = [](const char*& p, unsigned int& n) { | ||
| for (;;) { | ||
| while (n > 0 && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) { p++; n--; } | ||
| if (n >= 2 && p[0] == '-' && p[1] == '-') { | ||
| p += 2; n -= 2; | ||
| while (n > 0 && *p != '\n') { p++; n--; } | ||
| continue; | ||
| } | ||
| if (n >= 2 && p[0] == '/' && p[1] == '*') { | ||
| p += 2; n -= 2; | ||
| while (n >= 2 && !(p[0] == '*' && p[1] == '/')) { p++; n--; } | ||
| if (n >= 2) { p += 2; n -= 2; } else { n = 0; } | ||
| continue; | ||
| } | ||
| break; | ||
| } | ||
| }; | ||
|
|
||
| // Skip leading whitespace and comments. An ORM-prepended `/* query tag */ ROLLBACK` | ||
| // must recover, not be rejected. | ||
| skip_ws_and_comments(sql, sql_len); | ||
|
|
||
| enum RecoveryKind { KIND_REJECT, KIND_ROLLBACK, KIND_COMMIT }; | ||
| RecoveryKind kind = KIND_REJECT; | ||
|
|
||
| // Case-insensitive prefix match, followed by a word boundary so "ROLLBACKET" | ||
| // doesn't get classified as ROLLBACK. | ||
| auto matches_kw = [](const char* p, unsigned int n, const char* kw, unsigned int klen) -> bool { | ||
| if (n < klen) return false; | ||
| if (strncasecmp(p, kw, klen) != 0) return false; | ||
| if (n == klen) return true; | ||
| char ch = p[klen]; | ||
| return !(ch == '_' || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9')); | ||
| }; | ||
|
|
||
| // ABORT is a Postgres synonym for ROLLBACK. END is a Postgres synonym for COMMIT. | ||
| // Both ROLLBACK and COMMIT recover the session to IDLE. | ||
| unsigned int verb_len = 0; | ||
| if (matches_kw(sql, sql_len, "ROLLBACK", 8)) { kind = KIND_ROLLBACK; verb_len = 8; } | ||
| else if (matches_kw(sql, sql_len, "ABORT", 5)) { kind = KIND_ROLLBACK; verb_len = 5; } | ||
| else if (matches_kw(sql, sql_len, "COMMIT", 6)) { kind = KIND_COMMIT; verb_len = 6; } | ||
| else if (matches_kw(sql, sql_len, "END", 3)) { kind = KIND_COMMIT; verb_len = 3; } | ||
|
|
||
| // Having identified a recovery verb, verify the rest of the statement is a | ||
| // *plain* whole-tx end — no modifiers that change the semantics into | ||
| // something we can't honor on a dead backend, and no second statement | ||
| // pipelined after it. The only modifiers we accept are: | ||
| // WORK | TRANSACTION (Postgres noise-word aliases; no semantic effect) | ||
| // Anything else — specifically ROLLBACK TO SAVEPOINT (partial rollback, | ||
| // keeps tx open) and AND CHAIN (rolls back and opens a new tx) — would | ||
| // leave the client's transaction-status view inconsistent with what we can | ||
| // actually deliver once the backend is gone. Reject those with 25P02 and | ||
| // keep the session poisoned; the client can then issue a plain ROLLBACK. | ||
| // | ||
| // Multi-statement Simple Query (e.g. "ROLLBACK; SELECT 1;") is rejected for | ||
| // the same reason: we would recover, synthesize a ROLLBACK response, and | ||
| // silently drop the second statement. Safer to make the client split. | ||
| if (kind != KIND_REJECT) { | ||
| const char* rest = sql + verb_len; | ||
| unsigned int rest_len = sql_len - verb_len; | ||
| skip_ws_and_comments(rest, rest_len); | ||
| // Optionally consume a single WORK or TRANSACTION noise word. | ||
| if (matches_kw(rest, rest_len, "WORK", 4)) { | ||
| rest += 4; rest_len -= 4; | ||
| skip_ws_and_comments(rest, rest_len); | ||
| } else if (matches_kw(rest, rest_len, "TRANSACTION", 11)) { | ||
| rest += 11; rest_len -= 11; | ||
| skip_ws_and_comments(rest, rest_len); | ||
| } | ||
| // Optional trailing semicolon. | ||
| if (rest_len > 0 && *rest == ';') { | ||
| rest++; rest_len--; | ||
| skip_ws_and_comments(rest, rest_len); | ||
| } | ||
| // Anything remaining is either a savepoint/chain modifier or a second | ||
| // pipelined statement. Either way, reject. | ||
| if (rest_len > 0) { | ||
| kind = KIND_REJECT; | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, check if the file exists and view the context around lines 495-575
fd -t f "PgSQL_Session.cpp" | head -5Repository: sysown/proxysql
Length of output: 80
🏁 Script executed:
# Get the file size to understand what we're working with
wc -l lib/PgSQL_Session.cppRepository: sysown/proxysql
Length of output: 85
🏁 Script executed:
# View the specific lines in question to verify the code matches
sed -n '495,575p' lib/PgSQL_Session.cpp | head -100Repository: sysown/proxysql
Length of output: 3645
🏁 Script executed:
# Search for where tx_poisoned is set/cleared and how poisoned sessions are handled
rg "tx_poisoned" lib/PgSQL_Session.cpp -B2 -A2Repository: sysown/proxysql
Length of output: 6119
🏁 Script executed:
# Look for existing tests related to poisoned sessions and ROLLBACK
fd -t f "\.cpp$|\.h$" lib/ | xargs grep -l "poisoned" | head -10Repository: sysown/proxysql
Length of output: 101
🏁 Script executed:
# Search for test files related to poisoned sessions or PgSQL
fd -t f "test" . -o "reg_test" . -o "*poisoned*" . 2>/dev/null | grep -E "\.(py|sql|cpp)$" | head -20Repository: sysown/proxysql
Length of output: 41
🏁 Script executed:
# Search for regression tests in the repo
find . -type f -name "*reg_test*" -o -name "*test*poisoned*" 2>/dev/null | head -20Repository: sysown/proxysql
Length of output: 1301
🏁 Script executed:
# Look for test directories
fd -t d "test" -o "tests" -o "t" . | grep -v node_modules | head -10Repository: sysown/proxysql
Length of output: 188
🏁 Script executed:
# Check the handler_poisoned_simple_query function signature and recovery logic
sed -n '470,600p' lib/PgSQL_Session.cpp | grep -A50 "handler_poisoned_simple_query"Repository: sysown/proxysql
Length of output: 41
🏁 Script executed:
# Find the handler_poisoned_simple_query function definition
rg "handler_poisoned_simple_query" lib/PgSQL_Session.cpp -B5 -A100 | head -150Repository: sysown/proxysql
Length of output: 6421
🏁 Script executed:
# Check if there are any existing tests for poisoned sessions
rg "tx_poisoned\|poisoned" test/ -l | head -20Repository: sysown/proxysql
Length of output: 41
🏁 Script executed:
# Look for test files that might test ROLLBACK/COMMIT behavior
fd -type f -name "*pgsql*" test/tap/tests/*.cpp | grep -E "commit|rollback|poison" -i | head -10Repository: sysown/proxysql
Length of output: 229
🏁 Script executed:
# Search more broadly for pgsql tests with ROLLBACK in the test code
rg "ROLLBACK" test/tap/tests/ --iglob "*.cpp" -l | head -10Repository: sysown/proxysql
Length of output: 524
🏁 Script executed:
# Check what the full handler_poisoned_simple_query function returns and how it handles kind
sed -n '480,625p' lib/PgSQL_Session.cppRepository: sysown/proxysql
Length of output: 6230
🏁 Script executed:
# Search for any existing tests that test ROLLBACK with comments
rg "ROLLBACK.*\/\*\|\/\*.*ROLLBACK" test/ -iRepository: sysown/proxysql
Length of output: 165
🏁 Script executed:
# Look for pgsql tests related to comments or poisoned state
rg "poisoned" test/tap/tests/*.cpp -A5 -B5Repository: sysown/proxysql
Length of output: 27458
🏁 Script executed:
# Get the full reject_cases list from the poisoned recovery test
sed -n '170,210p' test/tap/tests/pgsql-tx_poisoned_recovery-t.cppRepository: sysown/proxysql
Length of output: 1867
🏁 Script executed:
# Check if there are any comment-related test cases
rg "\/\*|comment" test/tap/tests/pgsql-tx_poisoned_recovery-t.cpp -B2 -A2Repository: sysown/proxysql
Length of output: 137
🏁 Script executed:
# Look at the actual reject_cases array to see all test cases
sed -n '200,240p' test/tap/tests/pgsql-tx_poisoned_recovery-t.cppRepository: sysown/proxysql
Length of output: 1727
🏁 Script executed:
# Find the reject_cases array definition
rg "reject_cases\[\]" test/tap/tests/pgsql-tx_poisoned_recovery-t.cpp -A20Repository: sysown/proxysql
Length of output: 1149
🏁 Script executed:
# Look for any tests with malformed input or trailing comments
rg "ROLLBACK\|COMMIT" test/tap/tests/pgsql-tx_poisoned_recovery-t.cpp | head -30Repository: sysown/proxysql
Length of output: 41
🏁 Script executed:
# Check the entire case_A_rejection function to see all test scenarios
sed -n '290,380p' test/tap/tests/pgsql-tx_poisoned_recovery-t.cppRepository: sysown/proxysql
Length of output: 3268
🏁 Script executed:
# Check if there are any tests that explicitly test unterminated comments
rg "unterminated\|malformed" test/tap/tests/pgsql-tx_poisoned*.cpp -iRepository: sysown/proxysql
Length of output: 41
🏁 Script executed:
# Verify the current behavior by tracing through with "ROLLBACK /*"
# Let's also check the exact line numbers in the handler where recovery happens
sed -n '600,615p' lib/PgSQL_Session.cppRepository: sysown/proxysql
Length of output: 519
🏁 Script executed:
# Check what error code is used for rejection and understand the severity
sed -n '600,615p' lib/PgSQL_Session.cppRepository: sysown/proxysql
Length of output: 519
🏁 Script executed:
# Look at the write_tx_poisoned_error function to understand the rejection response
rg "write_tx_poisoned_error" lib/PgSQL_Session.cpp -A10 | head -30Repository: sysown/proxysql
Length of output: 1114
🏁 Script executed:
# Trace through the exact behavior with a simpler test
# Check if rest_len calculation is correct for "ROLLBACK /*"
python3 << 'EOF'
# Input: "ROLLBACK /*" (11 chars)
sql_len = 11
verb_len = 8 # "ROLLBACK"
rest_len = sql_len - verb_len
print(f"After ROLLBACK match: rest_len = {rest_len}")
# rest points to " /*" (3 chars)
# Skip whitespace: rest_len goes from 3 to 2, rest points to "/*"
rest_len_after_ws = 2
print(f"After skipping whitespace: rest_len = {rest_len_after_ws}")
# Process /* comment:
# p += 2, n -= 2 (consumes "/*")
# Inner while: n >= 2 && !(p[0]=='*' && p[1]=='/')
# n becomes 0, else sets n=0
rest_len_final = 0
print(f"After processing unterminated comment: rest_len = {rest_len_final}")
# Recovery condition check at line 573
if rest_len_final > 0:
print("Result: KIND_REJECT (would reject)")
else:
print("Result: KIND_ROLLBACK (would recover and clear poison) - BUG!")
EOFRepository: sysown/proxysql
Length of output: 247
Reject unterminated block comments in poisoned-session recovery classifier.
Statements like ROLLBACK /* (unterminated block comment) currently allow recovery to proceed: skip_ws_and_comments() silently sets n=0 when no closing */ is found, making the classifier believe nothing remains after the verb. This causes the check at line 573 (if (rest_len > 0)) to fail, leaving kind as KIND_ROLLBACK and triggering the recovery path that clears tx_poisoned. Keep the session poisoned instead by having skip_ws_and_comments() return bool to signal malformed input, and check all four call sites (lines 517, 554, 558, 563) to force KIND_REJECT on failure.
Suggested direction
- auto skip_ws_and_comments = [](const char*& p, unsigned int& n) {
+ auto skip_ws_and_comments = [](const char*& p, unsigned int& n) -> bool {
for (;;) {
while (n > 0 && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) { p++; n--; }
if (n >= 2 && p[0] == '-' && p[1] == '-') {
p += 2; n -= 2;
while (n > 0 && *p != '\n') { p++; n--; }
continue;
}
if (n >= 2 && p[0] == '/' && p[1] == '*') {
p += 2; n -= 2;
while (n >= 2 && !(p[0] == '*' && p[1] == '/')) { p++; n--; }
- if (n >= 2) { p += 2; n -= 2; } else { n = 0; }
+ if (n >= 2) { p += 2; n -= 2; } else { return false; }
continue;
}
break;
}
+ return true;
};
- skip_ws_and_comments(sql, sql_len);
+ if (!skip_ws_and_comments(sql, sql_len)) {
+ sql_len = 0;
+ }Also check return values at lines 554, 558, and 563 (the rest calls) to force KIND_REJECT when trailing comments are malformed.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/PgSQL_Session.cpp` around lines 495 - 575, skip_ws_and_comments currently
swallows unterminated /* ... */ and should return a bool to signal malformed
input; change its signature to return false on malformed/unterminated block
comments and true on success, then update all call sites (the initial call
skip_ws_and_comments(sql, sql_len) and the three subsequent calls on rest at the
matches_kw checks and semicolon handling) to check the return value and set kind
= KIND_REJECT if it returns false; ensure you propagate the same behavior for
the optional WORK/TRANSACTION consumption paths so any failed comment-skip
forces rejection and leaves tx_poisoned intact.
| if (tx_poisoned && command != 'Q' && command != 'X') { | ||
| if (command == 'P' || command == 'B' || command == 'D' || | ||
| command == 'C' || command == 'E') { | ||
| // Silent swallow — Parse/Bind/Describe/Close/Execute | ||
| // packets carry no client-visible response while | ||
| // poisoned. The whole logical ext-query gets | ||
| // accounted for once, at Sync. | ||
| l_free(pkt.size, pkt.ptr); | ||
| continue; | ||
| } | ||
| if (command == 'S') { | ||
| // One "rejected statement" per logical extended | ||
| // query (== per Sync), matching the simple-query | ||
| // path where a single 'Q' packet bumps the counter | ||
| // exactly once. | ||
| thread->status_variables.tx_poisoned_rejected_statements_total++; | ||
| PG_pkt pgpkt{}; | ||
| pgpkt.set_multi_pkt_mode(true); | ||
| write_tx_poisoned_error(pgpkt, | ||
| "current transaction is aborted, commands ignored until end of transaction block (extended-query path; issue ROLLBACK via simple query to recover)"); | ||
| pgpkt.write_ReadyForQuery('E'); | ||
| pgpkt.set_multi_pkt_mode(false); | ||
| auto buff = pgpkt.detach(); | ||
| client_myds->PSarrayOUT->add((void*)buff.first, buff.second); | ||
| client_myds->DSS = STATE_SLEEP; | ||
| l_free(pkt.size, pkt.ptr); | ||
| if (mirror == false) RequestEnd(NULL, false); | ||
| continue; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's check the file exists and examine the mentioned sections
wc -l lib/PgSQL_Session.cppRepository: sysown/proxysql
Length of output: 85
🏁 Script executed:
# Read the section around lines 2300-2328 to see the context
sed -n '2280,2340p' lib/PgSQL_Session.cpp | cat -nRepository: sysown/proxysql
Length of output: 3225
🏁 Script executed:
# Now check the default branch mentioned at lines 2582-2594
sed -n '2570,2610p' lib/PgSQL_Session.cpp | cat -nRepository: sysown/proxysql
Length of output: 1901
🏁 Script executed:
# Search for where command 'H' is explicitly handled, if anywhere
rg -n "command == 'H'" lib/PgSQL_Session.cppRepository: sysown/proxysql
Length of output: 41
🏁 Script executed:
# Search for the broader switch/if structure that handles commands
rg -n -B2 -A2 "command == 'P'" lib/PgSQL_Session.cpp | head -50Repository: sysown/proxysql
Length of output: 369
🏁 Script executed:
# Search for comments about Flush or 'H' handling
rg -n -B3 -A3 "c != 'H'" lib/PgSQL_Session.cppRepository: sysown/proxysql
Length of output: 552
🏁 Script executed:
# Search for Flush message documentation or comments
rg -n -i "flush" lib/PgSQL_Session.cpp | head -20Repository: sysown/proxysql
Length of output: 160
🏁 Script executed:
# Check if there's any explicit H handling in the switch statement
sed -n '2330,2580p' lib/PgSQL_Session.cpp | grep -n "case 'H'" | head -5Repository: sysown/proxysql
Length of output: 41
🏁 Script executed:
# Let's look at the broader context around line 2587 to understand the H handling
sed -n '2560,2610p' lib/PgSQL_Session.cpp | cat -nRepository: sysown/proxysql
Length of output: 2374
🏁 Script executed:
# Search for any other H/Flush related code or comments
rg -n "'H'" lib/PgSQL_Session.cpp | head -20Repository: sysown/proxysql
Length of output: 144
🏁 Script executed:
# Check what happens in a normal (non-poisoned) extended query with H
sed -n '2300,2580p' lib/PgSQL_Session.cpp | grep -A10 -B10 "case 'H'" | head -40Repository: sysown/proxysql
Length of output: 41
🏁 Script executed:
# Let's check the PostgreSQL extended-query protocol docs context - search for any protocol documentation in comments
rg -n "Extended Query\|extended.query" lib/PgSQL_Session.cpp | head -20Repository: sysown/proxysql
Length of output: 41
🏁 Script executed:
# Let's see what happens around the poisoned gate more broadly - get more context before and after
sed -n '2290,2360p' lib/PgSQL_Session.cpp | cat -nRepository: sysown/proxysql
Length of output: 3610
🏁 Script executed:
# Search for PostgreSQL protocol documentation or constants for message types
rg -n "Flush\|'H'" lib/PgSQL_Session.cpp -B2 -A2 | head -40Repository: sysown/proxysql
Length of output: 41
🏁 Script executed:
# Let's check if there are tests or examples showing the expected behavior
fd -t f "test.*pgsql" -o ".*pgsql.*test" | head -10Repository: sysown/proxysql
Length of output: 204
🏁 Script executed:
# Check the poisoned gate comments more carefully and see what Extended Query messages should be
sed -n '2293,2305p' lib/PgSQL_Session.cppRepository: sysown/proxysql
Length of output: 857
🏁 Script executed:
# Let's look at the PostgreSQL protocol specification comment or any protocol-related doc
rg -n "Parse\|Bind\|Execute" lib/PgSQL_Session.cpp | head -5Repository: sysown/proxysql
Length of output: 41
🏁 Script executed:
# Check if there's any mention of protocol message types or their names
rg -n "'P'\|'B'\|'E'\|'S'" lib/PgSQL_Session.cpp | grep -i "parse\|bind\|execute\|sync\|flush" | head -10Repository: sysown/proxysql
Length of output: 41
🏁 Script executed:
# Search for HTTP/web references about PostgreSQL Flush message type
# Let's verify from the codebase if H is documented anywhere
rg -n "Message type" lib/PgSQL_Session.cpp | head -10Repository: sysown/proxysql
Length of output: 210
🏁 Script executed:
# Let's check if there are any constants defined for message types
rg -n "0x48\|0x[Hh]" lib/PgSQL_Session.cpp | head -10Repository: sysown/proxysql
Length of output: 41
🏁 Script executed:
# Look for the full switch case statement to see all handled message types
sed -n '2330,2600p' lib/PgSQL_Session.cpp | grep "case '" | head -20Repository: sysown/proxysql
Length of output: 234
🏁 Script executed:
# Let's verify the PostgreSQL protocol message types to confirm H is Flush
# Check if there's a header or protocol definition file
fd -t f "protocol\|pgsql" lib/ | grep -E "\.(h|hpp)$" | head -10Repository: sysown/proxysql
Length of output: 41
🏁 Script executed:
# Let's search for any documentation about message types in header files
rg -n "Flush" lib/ -t h -t hpp | head -10Repository: sysown/proxysql
Length of output: 86
🏁 Script executed:
# Let's check what the PostgreSQL protocol actually expects for Flush
# by looking at the myprot.generate_error_packet implementation
rg -n "generate_error_packet" lib/ -t h -t hpp | head -5Repository: sysown/proxysql
Length of output: 86
🏁 Script executed:
# Let's check if there's any include or reference to PostgreSQL protocol documentation
rg -n "H\|Flush" include/ 2>/dev/null | head -20Repository: sysown/proxysql
Length of output: 41
🏁 Script executed:
# Let's look at what message types are being handled in the switch statement
sed -n '2330,2600p' lib/PgSQL_Session.cpp | cat -n | grep "case '"Repository: sysown/proxysql
Length of output: 311
🏁 Script executed:
# Let's examine the context where H is specially handled (line 2587)
# to understand the intent
sed -n '2575,2600p' lib/PgSQL_Session.cpp | cat -nRepository: sysown/proxysql
Length of output: 1231
🏁 Script executed:
# Let's verify the ASCII code for 'H' and understand the protocol better
python3 << 'EOF'
# Check ASCII value
print(f"H is ASCII: {ord('H')} or 0x{ord('H'):02X}")
# In PostgreSQL extended-query protocol:
# P = Parse
# B = Bind
# D = Describe
# C = Close
# E = Execute
# S = Sync
# H = Flush (0x48)
# Q = Simple Query
# X = Terminate
protocol_messages = {
'P': 'Parse',
'B': 'Bind',
'D': 'Describe',
'C': 'Close',
'E': 'Execute',
'S': 'Sync',
'H': 'Flush',
'Q': 'Simple Query',
'X': 'Terminate'
}
print("\nPostgreSQL Frontend message types:")
for code, name in sorted(protocol_messages.items()):
print(f" {code} = {name}")
print("\nCurrently handled in poisoned gate:")
print(" P, B, D, C, E (silent swallow)")
print(" S (error response)")
print(" Q, X (fall through)")
print("\nNOT in poisoned gate (falls through to default):")
print(" H (Flush)")
EOFRepository: sysown/proxysql
Length of output: 414
Add Flush ('H') to the poisoned extended-query silent-swallow list.
Flush currently falls through to the default "Feature not supported" error path. In poisoned transactions, Flush (like Parse/Bind/Describe/Close/Execute) should be silently swallowed as part of the extended-query frame; the only client-visible response should come at Sync.
Suggested fix
- if (command == 'P' || command == 'B' || command == 'D' ||
- command == 'C' || command == 'E') {
+ if (command == 'P' || command == 'B' || command == 'D' ||
+ command == 'C' || command == 'E' || command == 'H') {
// Silent swallow — Parse/Bind/Describe/Close/Execute
// packets carry no client-visible response while
// poisoned. The whole logical ext-query gets
// accounted for once, at Sync.
l_free(pkt.size, pkt.ptr);
continue;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (tx_poisoned && command != 'Q' && command != 'X') { | |
| if (command == 'P' || command == 'B' || command == 'D' || | |
| command == 'C' || command == 'E') { | |
| // Silent swallow — Parse/Bind/Describe/Close/Execute | |
| // packets carry no client-visible response while | |
| // poisoned. The whole logical ext-query gets | |
| // accounted for once, at Sync. | |
| l_free(pkt.size, pkt.ptr); | |
| continue; | |
| } | |
| if (command == 'S') { | |
| // One "rejected statement" per logical extended | |
| // query (== per Sync), matching the simple-query | |
| // path where a single 'Q' packet bumps the counter | |
| // exactly once. | |
| thread->status_variables.tx_poisoned_rejected_statements_total++; | |
| PG_pkt pgpkt{}; | |
| pgpkt.set_multi_pkt_mode(true); | |
| write_tx_poisoned_error(pgpkt, | |
| "current transaction is aborted, commands ignored until end of transaction block (extended-query path; issue ROLLBACK via simple query to recover)"); | |
| pgpkt.write_ReadyForQuery('E'); | |
| pgpkt.set_multi_pkt_mode(false); | |
| auto buff = pgpkt.detach(); | |
| client_myds->PSarrayOUT->add((void*)buff.first, buff.second); | |
| client_myds->DSS = STATE_SLEEP; | |
| l_free(pkt.size, pkt.ptr); | |
| if (mirror == false) RequestEnd(NULL, false); | |
| continue; | |
| } | |
| if (tx_poisoned && command != 'Q' && command != 'X') { | |
| if (command == 'P' || command == 'B' || command == 'D' || | |
| command == 'C' || command == 'E' || command == 'H') { | |
| // Silent swallow — Parse/Bind/Describe/Close/Execute | |
| // packets carry no client-visible response while | |
| // poisoned. The whole logical ext-query gets | |
| // accounted for once, at Sync. | |
| l_free(pkt.size, pkt.ptr); | |
| continue; | |
| } | |
| if (command == 'S') { | |
| // One "rejected statement" per logical extended | |
| // query (== per Sync), matching the simple-query | |
| // path where a single 'Q' packet bumps the counter | |
| // exactly once. | |
| thread->status_variables.tx_poisoned_rejected_statements_total++; | |
| PG_pkt pgpkt{}; | |
| pgpkt.set_multi_pkt_mode(true); | |
| write_tx_poisoned_error(pgpkt, | |
| "current transaction is aborted, commands ignored until end of transaction block (extended-query path; issue ROLLBACK via simple query to recover)"); | |
| pgpkt.write_ReadyForQuery('E'); | |
| pgpkt.set_multi_pkt_mode(false); | |
| auto buff = pgpkt.detach(); | |
| client_myds->PSarrayOUT->add((void*)buff.first, buff.second); | |
| client_myds->DSS = STATE_SLEEP; | |
| l_free(pkt.size, pkt.ptr); | |
| if (mirror == false) RequestEnd(NULL, false); | |
| continue; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/PgSQL_Session.cpp` around lines 2300 - 2328, The code treats Flush ('H')
as unsupported in a poisoned extended-query transaction; update the conditional
in the tx_poisoned check (the block that inspects command in PgSQL_Session.cpp
where tx_poisoned and command != 'Q' && command != 'X' are tested) to include
'H' alongside 'P','B','D','C','E' so that when tx_poisoned and command == 'H'
the packet is silently swallowed (l_free(pkt.size, pkt.ptr); continue;) just
like Parse/Bind/Describe/Close/Execute, leaving the single Sync-path response
unchanged.
| // Sanitize + cap the backend message before embedding into a NoticeResponse. | ||
| // See sanitize_backend_err_msg() comment for NUL / length rationale. | ||
| const std::string backend_err_msg = sanitize_backend_err_msg(myconn->get_error_message()); | ||
|
|
||
| PG_pkt pgpkt{}; | ||
| pgpkt.set_multi_pkt_mode(true); | ||
| // ErrorResponse severity=ERROR SQLSTATE=25P02. Not FATAL — libpq treats | ||
| // FATAL as connection-loss and drops the socket. | ||
| write_tx_poisoned_error(pgpkt); | ||
| // NoticeResponse carries the backend's original message text as context at | ||
| // SQLSTATE 01000 (ERRCODE_WARNING). Per design, the backend's original | ||
| // SQLSTATE (e.g. 57P01 terminating connection due to administrator command) | ||
| // is NOT propagated — the only SQLSTATE surfaced to the client is the | ||
| // synthesized 25P02 on the ErrorResponse above. | ||
| if (!backend_err_msg.empty()) { | ||
| write_tx_poisoned_warning_notice(pgpkt, PGSQL_ERROR_CODES::ERRCODE_WARNING, backend_err_msg.c_str()); | ||
| } | ||
| // ReadyForQuery with txn_state='E' signals the client it is in an aborted | ||
| // transaction — libpq will report PQTRANS_INERROR and accept only | ||
| // ROLLBACK/COMMIT/ABORT. | ||
| pgpkt.write_ReadyForQuery('E'); | ||
| pgpkt.set_multi_pkt_mode(false); | ||
| auto buff = pgpkt.detach(); | ||
| client_myds->PSarrayOUT->add((void*)buff.first, buff.second); | ||
| client_myds->DSS = STATE_SLEEP; | ||
|
|
||
| tx_poisoned = true; | ||
| thread->status_variables.tx_poisoned_total++; | ||
| // Rich log context for production debugging: hostgroup, backend addr:port, | ||
| // backend PID, client addr, frontend user, dbname, digest text, original | ||
| // backend error code+message. Mirrors handler_minus1_LogErrorDuringQuery so | ||
| // operators can correlate poison events with the query that triggered them. | ||
| const char* client_addr = (client_myds && client_myds->addr.addr) ? client_myds->addr.addr : "unknown"; | ||
| const char* fe_user = (client_myds && client_myds->myconn && client_myds->myconn->userinfo) | ||
| ? client_myds->myconn->userinfo->username : "?"; | ||
| const char* fe_db = (client_myds && client_myds->myconn && client_myds->myconn->userinfo) | ||
| ? client_myds->myconn->userinfo->dbname : "?"; | ||
| const char* digest = CurrentQuery.QueryParserArgs.digest_text ? CurrentQuery.QueryParserArgs.digest_text : ""; | ||
| proxy_warning("Backend broken mid-transaction; poisoning client session on " | ||
| "(hg=%d,%s:%d,backend_pid=%d) for user=\"%s@%s\" db=\"%s\" digest=\"%s\" backend_err=\"%s\". " | ||
| "Client must issue ROLLBACK to recover.\n", | ||
| myconn->parent->myhgc->hid, myconn->parent->address, myconn->parent->port, | ||
| myconn->get_backend_pid(), fe_user, client_addr, fe_db, digest, | ||
| myconn->get_error_code_with_message().c_str()); |
There was a problem hiding this comment.
Apply the backend-error cap to the warning log too.
The NoticeResponse is capped, but the log still embeds get_error_code_with_message() uncapped. A rogue backend can still amplify logs with a very large error string.
Suggested fix
const std::string backend_err_msg = sanitize_backend_err_msg(myconn->get_error_message());
+ const std::string backend_err_for_log = sanitize_backend_err_msg(myconn->get_error_code_with_message());
PG_pkt pgpkt{};
pgpkt.set_multi_pkt_mode(true);
@@
proxy_warning("Backend broken mid-transaction; poisoning client session on "
"(hg=%d,%s:%d,backend_pid=%d) for user=\"%s@%s\" db=\"%s\" digest=\"%s\" backend_err=\"%s\". "
"Client must issue ROLLBACK to recover.\n",
myconn->parent->myhgc->hid, myconn->parent->address, myconn->parent->port,
myconn->get_backend_pid(), fe_user, client_addr, fe_db, digest,
- myconn->get_error_code_with_message().c_str());
+ backend_err_for_log.c_str());🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/PgSQL_Session.cpp` around lines 2747 - 2790, The log embeds the uncapped
backend string via myconn->get_error_code_with_message(), so replace that with
the already-sanitized/capped backend_err_msg (produced by
sanitize_backend_err_msg) when calling proxy_warning in this block (i.e., use
backend_err_msg.c_str() or build a small capped string combining
code+backend_err_msg) so the warning log cannot be amplified by an oversized
backend message; update the proxy_warning call site accordingly (references:
backend_err_msg, proxy_warning, get_error_code_with_message(),
sanitize_backend_err_msg).
| galera_1_timeout_count: galera_1_timeout_count.cpp $(TAP_LDIR)/libtap.so | ||
| $(CXX) -DTEST_GALERA $< ../../../src/SQLite3_Server.cpp $(IDIRS) $(LDIRS) $(OPT) $(OBJ) $(MYLIBSJEMALLOC) $(MYLIBS) $(STATIC_LIBS) -lscram -lusual -o $@ | ||
| $(CXX) -DTEST_GALERA $< ../../../src/SQLite3_Server.cpp ../../../lib/ProxySQL_Admin.cpp -I$(CLICKHOUSE_CPP_IDIR) -I$(COREDUMPER_IDIR) $(IDIRS) $(LDIRS) -L$(CLICKHOUSE_CPP_LDIR) -L$(LZ4_LDIR) $(OPT) $(OBJ) $(MYLIBSJEMALLOC) $(MYLIBS) $(STATIC_LIBS) $(CLICKHOUSE_CPP_LDIR)/libclickhouse-cpp-lib.a $(CLICKHOUSE_CPP_PATH)/contrib/zstd/zstd/libzstdstatic.a $(LZ4_LDIR)/liblz4.a -lscram -lusual -Wl,--allow-multiple-definition -o $@ | ||
|
|
||
| galera_2_timeout_no_count: galera_2_timeout_no_count.cpp $(TAP_LDIR)/libtap.so | ||
| $(CXX) -DTEST_GALERA $< ../../../src/SQLite3_Server.cpp $(IDIRS) $(LDIRS) $(OPT) $(OBJ) $(MYLIBSJEMALLOC) $(MYLIBS) $(STATIC_LIBS) -lscram -lusual -o $@ | ||
| $(CXX) -DTEST_GALERA $< ../../../src/SQLite3_Server.cpp ../../../lib/ProxySQL_Admin.cpp -I$(CLICKHOUSE_CPP_IDIR) -I$(COREDUMPER_IDIR) $(IDIRS) $(LDIRS) -L$(CLICKHOUSE_CPP_LDIR) -L$(LZ4_LDIR) $(OPT) $(OBJ) $(MYLIBSJEMALLOC) $(MYLIBS) $(STATIC_LIBS) $(CLICKHOUSE_CPP_LDIR)/libclickhouse-cpp-lib.a $(CLICKHOUSE_CPP_PATH)/contrib/zstd/zstd/libzstdstatic.a $(LZ4_LDIR)/liblz4.a -lscram -lusual -Wl,--allow-multiple-definition -o $@ |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Show Galera target prerequisites and recipes.
# Expected: both target prerequisite lines list ../../../src/SQLite3_Server.cpp and ../../../lib/ProxySQL_Admin.cpp.
awk '
/^galera_1_timeout_count:/ { print; getline; print; next }
/^galera_2_timeout_no_count:/ { print; getline; print; next }
' test/tap/tests/MakefileRepository: sysown/proxysql
Length of output: 1066
Add the linked source files to the target prerequisites.
Lines 192 and 195 compile ../../../src/SQLite3_Server.cpp and ../../../lib/ProxySQL_Admin.cpp but these files are not listed as prerequisites. Incremental builds can keep stale binaries after changes to these sources.
Proposed Makefile dependency fix
-galera_1_timeout_count: galera_1_timeout_count.cpp $(TAP_LDIR)/libtap.so
+galera_1_timeout_count: galera_1_timeout_count.cpp ../../../src/SQLite3_Server.cpp ../../../lib/ProxySQL_Admin.cpp $(TAP_LDIR)/libtap.so
$(CXX) -DTEST_GALERA $< ../../../src/SQLite3_Server.cpp ../../../lib/ProxySQL_Admin.cpp -I$(CLICKHOUSE_CPP_IDIR) -I$(COREDUMPER_IDIR) $(IDIRS) $(LDIRS) -L$(CLICKHOUSE_CPP_LDIR) -L$(LZ4_LDIR) $(OPT) $(OBJ) $(MYLIBSJEMALLOC) $(MYLIBS) $(STATIC_LIBS) $(CLICKHOUSE_CPP_LDIR)/libclickhouse-cpp-lib.a $(CLICKHOUSE_CPP_PATH)/contrib/zstd/zstd/libzstdstatic.a $(LZ4_LDIR)/liblz4.a -lscram -lusual -Wl,--allow-multiple-definition -o $@
-galera_2_timeout_no_count: galera_2_timeout_no_count.cpp $(TAP_LDIR)/libtap.so
+galera_2_timeout_no_count: galera_2_timeout_no_count.cpp ../../../src/SQLite3_Server.cpp ../../../lib/ProxySQL_Admin.cpp $(TAP_LDIR)/libtap.so
$(CXX) -DTEST_GALERA $< ../../../src/SQLite3_Server.cpp ../../../lib/ProxySQL_Admin.cpp -I$(CLICKHOUSE_CPP_IDIR) -I$(COREDUMPER_IDIR) $(IDIRS) $(LDIRS) -L$(CLICKHOUSE_CPP_LDIR) -L$(LZ4_LDIR) $(OPT) $(OBJ) $(MYLIBSJEMALLOC) $(MYLIBS) $(STATIC_LIBS) $(CLICKHOUSE_CPP_LDIR)/libclickhouse-cpp-lib.a $(CLICKHOUSE_CPP_PATH)/contrib/zstd/zstd/libzstdstatic.a $(LZ4_LDIR)/liblz4.a -lscram -lusual -Wl,--allow-multiple-definition -o $@📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| galera_1_timeout_count: galera_1_timeout_count.cpp $(TAP_LDIR)/libtap.so | |
| $(CXX) -DTEST_GALERA $< ../../../src/SQLite3_Server.cpp $(IDIRS) $(LDIRS) $(OPT) $(OBJ) $(MYLIBSJEMALLOC) $(MYLIBS) $(STATIC_LIBS) -lscram -lusual -o $@ | |
| $(CXX) -DTEST_GALERA $< ../../../src/SQLite3_Server.cpp ../../../lib/ProxySQL_Admin.cpp -I$(CLICKHOUSE_CPP_IDIR) -I$(COREDUMPER_IDIR) $(IDIRS) $(LDIRS) -L$(CLICKHOUSE_CPP_LDIR) -L$(LZ4_LDIR) $(OPT) $(OBJ) $(MYLIBSJEMALLOC) $(MYLIBS) $(STATIC_LIBS) $(CLICKHOUSE_CPP_LDIR)/libclickhouse-cpp-lib.a $(CLICKHOUSE_CPP_PATH)/contrib/zstd/zstd/libzstdstatic.a $(LZ4_LDIR)/liblz4.a -lscram -lusual -Wl,--allow-multiple-definition -o $@ | |
| galera_2_timeout_no_count: galera_2_timeout_no_count.cpp $(TAP_LDIR)/libtap.so | |
| $(CXX) -DTEST_GALERA $< ../../../src/SQLite3_Server.cpp $(IDIRS) $(LDIRS) $(OPT) $(OBJ) $(MYLIBSJEMALLOC) $(MYLIBS) $(STATIC_LIBS) -lscram -lusual -o $@ | |
| $(CXX) -DTEST_GALERA $< ../../../src/SQLite3_Server.cpp ../../../lib/ProxySQL_Admin.cpp -I$(CLICKHOUSE_CPP_IDIR) -I$(COREDUMPER_IDIR) $(IDIRS) $(LDIRS) -L$(CLICKHOUSE_CPP_LDIR) -L$(LZ4_LDIR) $(OPT) $(OBJ) $(MYLIBSJEMALLOC) $(MYLIBS) $(STATIC_LIBS) $(CLICKHOUSE_CPP_LDIR)/libclickhouse-cpp-lib.a $(CLICKHOUSE_CPP_PATH)/contrib/zstd/zstd/libzstdstatic.a $(LZ4_LDIR)/liblz4.a -lscram -lusual -Wl,--allow-multiple-definition -o $@ | |
| galera_1_timeout_count: galera_1_timeout_count.cpp ../../../src/SQLite3_Server.cpp ../../../lib/ProxySQL_Admin.cpp $(TAP_LDIR)/libtap.so | |
| $(CXX) -DTEST_GALERA $< ../../../src/SQLite3_Server.cpp ../../../lib/ProxySQL_Admin.cpp -I$(CLICKHOUSE_CPP_IDIR) -I$(COREDUMPER_IDIR) $(IDIRS) $(LDIRS) -L$(CLICKHOUSE_CPP_LDIR) -L$(LZ4_LDIR) $(OPT) $(OBJ) $(MYLIBSJEMALLOC) $(MYLIBS) $(STATIC_LIBS) $(CLICKHOUSE_CPP_LDIR)/libclickhouse-cpp-lib.a $(CLICKHOUSE_CPP_PATH)/contrib/zstd/zstd/libzstdstatic.a $(LZ4_LDIR)/liblz4.a -lscram -lusual -Wl,--allow-multiple-definition -o $@ | |
| galera_2_timeout_no_count: galera_2_timeout_no_count.cpp ../../../src/SQLite3_Server.cpp ../../../lib/ProxySQL_Admin.cpp $(TAP_LDIR)/libtap.so | |
| $(CXX) -DTEST_GALERA $< ../../../src/SQLite3_Server.cpp ../../../lib/ProxySQL_Admin.cpp -I$(CLICKHOUSE_CPP_IDIR) -I$(COREDUMPER_IDIR) $(IDIRS) $(LDIRS) -L$(CLICKHOUSE_CPP_LDIR) -L$(LZ4_LDIR) $(OPT) $(OBJ) $(MYLIBSJEMALLOC) $(MYLIBS) $(STATIC_LIBS) $(CLICKHOUSE_CPP_LDIR)/libclickhouse-cpp-lib.a $(CLICKHOUSE_CPP_PATH)/contrib/zstd/zstd/libzstdstatic.a $(LZ4_LDIR)/liblz4.a -lscram -lusual -Wl,--allow-multiple-definition -o $@ |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@test/tap/tests/Makefile` around lines 191 - 195, The Makefile targets
galera_1_timeout_count and galera_2_timeout_no_count compile
../../../src/SQLite3_Server.cpp and ../../../lib/ProxySQL_Admin.cpp but do not
list them as prerequisites; update the rules for targets galera_1_timeout_count
and galera_2_timeout_no_count to include ../../../src/SQLite3_Server.cpp and
../../../lib/ProxySQL_Admin.cpp in their prerequisite lists so changes to those
sources trigger a rebuild of the corresponding binaries.
| char q[256]; | ||
| snprintf(q, sizeof(q), | ||
| "UPDATE global_variables SET variable_value='%s' WHERE variable_name='pgsql-%s'", | ||
| value ? "true" : "false", var); | ||
| PGresult* r = PQexec(admin, q); | ||
| bool ok1 = (PQresultStatus(r) == PGRES_COMMAND_OK); | ||
| PQclear(r); | ||
| r = PQexec(admin, "LOAD PGSQL VARIABLES TO RUNTIME"); | ||
| bool ok2 = (PQresultStatus(r) == PGRES_COMMAND_OK); |
There was a problem hiding this comment.
Restore the previous admin value and reject 0-row updates.
set_admin_bool() currently treats a 0-row UPDATE as success, and the guard always restores true instead of the value that was present before the test. That can leak global config into later tests and hide admin-variable drift.
Suggested direction
PGresult* r = PQexec(admin, q);
- bool ok1 = (PQresultStatus(r) == PGRES_COMMAND_OK);
+ bool ok1 = (PQresultStatus(r) == PGRES_COMMAND_OK
+ && strcmp(PQcmdTuples(r), "1") == 0);Then have RestorePreserveClientAdminVar capture the current pgsql-preserve_client_on_broken_backend_in_tx value before Line 234 and restore that captured value in the destructor.
Also applies to: 98-101, 232-235
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@test/tap/tests/pgsql-tx_poisoned_extended_query-t.cpp` around lines 68 - 76,
set_admin_bool currently treats a 0-row UPDATE as success and
RestorePreserveClientAdminVar's destructor always restores true instead of the
original value, risking leaked global config; change set_admin_bool (the
function that runs the snprintf/PQexec sequence) to check the PGresult for
affected rows (use PQcmdTuples or equivalent) and return false when no rows were
updated, and modify RestorePreserveClientAdminVar to read and store the current
value of "pgsql-preserve_client_on_broken_backend_in_tx" before performing the
update and then restore that captured value in its destructor rather than
hardcoding true.
| if (!kr.kill_delivered) { | ||
| ok(0, "poison: killer failed to deliver pg_terminate_backend within 6s"); | ||
| for (int i = 0; i < 8; ++i) ok(0, "SKIPPED: poison did not fire"); | ||
| PQclear(kr.pgresult); | ||
| PQfinish(cli); | ||
| return exit_status(); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Find existing TAP skip usage in this test suite.
rg -n -C3 '\bskip\s*\(|# SKIP|SKIPPED:' test/tap/testsRepository: sysown/proxysql
Length of output: 50372
Replace ok(0, "SKIPPED: ...") loop with proper TAP skip directive.
The pattern ok(0, "SKIPPED: ...") still records failures even with the "SKIPPED" string. When kill-delivery fails due to infrastructure timing issues, use the standard skip() function instead:
diag("poison: killer failed to deliver pg_terminate_backend within 6s");
skip(9, "poison did not fire");This keeps infrastructure flakiness from appearing as test failures.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@test/tap/tests/pgsql-tx_poisoned_extended_query-t.cpp` around lines 252 -
257, Replace the failing ok(0, "SKIPPED: ...") loop with a proper TAP skip: when
kr.kill_delivered is false, emit a diagnostic with diag("poison: killer failed
to deliver pg_terminate_backend within 6s") and call skip(9, "poison did not
fire") to mark the remaining 9 tests as skipped; still ensure you
PQclear(kr.pgresult), PQfinish(cli) and return exit_status() in that branch
(references: kr.kill_delivered, kr.pgresult, PQclear, PQfinish, exit_status).
| ok(rejected_25P02_as_error(kr.pgresult), | ||
| "pg_sleep after mid-tx backend kill returns 25P02 severity=ERROR (status=%s severity=%s sqlstate=%s)", | ||
| PQresStatus(PQresultStatus(kr.pgresult)), | ||
| PQresultErrorField(kr.pgresult, PG_DIAG_SEVERITY) ?: "(none)", | ||
| PQresultErrorField(kr.pgresult, PG_DIAG_SQLSTATE) ?: "(none)"); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Detect GNU omitted-middle conditional operators in this file.
python3 - <<'PY'
from pathlib import Path
p = Path("test/tap/tests/pgsql-tx_poisoned_extended_query-t.cpp")
for i, line in enumerate(p.read_text().splitlines(), 1):
if "?:" in line or " ?: " in line:
print(f"{p}:{i}: {line}")
PYRepository: sysown/proxysql
Length of output: 311
🏁 Script executed:
# Read the exact lines from the file to verify the snippet
sed -n '259,265p' test/tap/tests/pgsql-tx_poisoned_extended_query-t.cppRepository: sysown/proxysql
Length of output: 412
🏁 Script executed:
# Search for the rejected_25P02_as_error function definition to check if it's null-safe
rg -n "rejected_25P02_as_error" test/tap/tests/pgsql-tx_poisoned_extended_query-t.cpp -A 3Repository: sysown/proxysql
Length of output: 942
🏁 Script executed:
# Check if there are any other GNU ?:0 operators in the file beyond the known ones
grep -n "?:" test/tap/tests/pgsql-tx_poisoned_extended_query-t.cppRepository: sysown/proxysql
Length of output: 201
Make the poison failure diagnostic null-safe and C++17-standard.
The predicate rejected_25P02_as_error() handles nullptr correctly, but the diagnostic arguments still dereference kr.pgresult unconditionally. Additionally, the ?: operator on lines 262–263 is a GNU extension, not standard C++17.
Suggested fix
+ const char* poison_status = kr.pgresult ? PQresStatus(PQresultStatus(kr.pgresult)) : "(null)";
+ const char* poison_severity = kr.pgresult ? PQresultErrorField(kr.pgresult, PG_DIAG_SEVERITY) : nullptr;
+ const char* poison_sqlstate = kr.pgresult ? PQresultErrorField(kr.pgresult, PG_DIAG_SQLSTATE) : nullptr;
ok(rejected_25P02_as_error(kr.pgresult),
"pg_sleep after mid-tx backend kill returns 25P02 severity=ERROR (status=%s severity=%s sqlstate=%s)",
- PQresStatus(PQresultStatus(kr.pgresult)),
- PQresultErrorField(kr.pgresult, PG_DIAG_SEVERITY) ?: "(none)",
- PQresultErrorField(kr.pgresult, PG_DIAG_SQLSTATE) ?: "(none)");
+ poison_status,
+ poison_severity ? poison_severity : "(none)",
+ poison_sqlstate ? poison_sqlstate : "(none)");🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@test/tap/tests/pgsql-tx_poisoned_extended_query-t.cpp` around lines 259 -
263, The diagnostic arguments dereference kr.pgresult unconditionally and use
the GNU ?: extension; change the three inline expressions to be null-safe and
C++17-compliant by guarding PQresultStatus/PQresultErrorField calls with a
kr.pgresult check and replacing ?: with the standard ternary operator. For
example, when building the status/severity/sqlstate strings for the ok(...) call
in the test where rejected_25P02_as_error(kr.pgresult) is used, compute
temporary const char* (or std::string) values using checks like kr.pgresult ?
PQresStatus(PQresultStatus(kr.pgresult)) : "(none)" and kr.pgresult &&
PQresultErrorField(kr.pgresult, PG_DIAG_SEVERITY) ?
PQresultErrorField(kr.pgresult, PG_DIAG_SEVERITY) : "(none)" (and similarly for
PG_DIAG_SQLSTATE), then pass those temporaries into the diagnostic format
instead of dereferencing kr.pgresult inline.
| // Collect NoticeResponse payloads on a PGconn so we can later inspect them. | ||
| struct NoticeCollector { | ||
| std::vector<PGresult*> notices; | ||
| static void callback(void* arg, const PGresult* res) { | ||
| NoticeCollector* self = static_cast<NoticeCollector*>(arg); | ||
| // Dup the PGresult so we can outlive the libpq-owned original. | ||
| PGresult* dup = PQmakeEmptyPGresult(nullptr, PGRES_NONFATAL_ERROR); | ||
| (void)res; | ||
| if (!dup) return; | ||
| // We can't deep-copy arbitrary PGresult state via public libpq, so | ||
| // instead stash the severity + sqlstate + message as synthesized fields. | ||
| // Callers of this helper that need finer-grained inspection would read | ||
| // the original PGresult; for our assertions the three fields suffice. | ||
| // Store the three fields by overloading the PGresult's error message. | ||
| // libpq provides no public setter for PG_DIAG_SEVERITY etc., so we | ||
| // keep a side-car copy instead. | ||
| (void)dup; | ||
| } | ||
| // Simpler approach: use PQnoticeProcessor (the text-message processor) | ||
| // plus a parallel SQLSTATE/severity capture via PQsetNoticeReceiver on | ||
| // a fresh PGresult stream — but PGresult fields aren't settable. We work | ||
| // around that by capturing the raw text the backend was going to print. | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Remove abandoned NoticeCollector struct.
This struct is never instantiated or referenced. Its callback is an empty stub (it allocates a PGresult then (void)dup;s it without doing anything), and its trailing comment describes an approach that was ultimately replaced by NoticeBag/notice_receiver_cb below. Leaving it in place is confusing and will likely trip up future readers or static analysis.
♻️ Proposed cleanup
-// Collect NoticeResponse payloads on a PGconn so we can later inspect them.
-struct NoticeCollector {
- std::vector<PGresult*> notices;
- static void callback(void* arg, const PGresult* res) {
- NoticeCollector* self = static_cast<NoticeCollector*>(arg);
- // Dup the PGresult so we can outlive the libpq-owned original.
- PGresult* dup = PQmakeEmptyPGresult(nullptr, PGRES_NONFATAL_ERROR);
- (void)res;
- if (!dup) return;
- // We can't deep-copy arbitrary PGresult state via public libpq, so
- // instead stash the severity + sqlstate + message as synthesized fields.
- // Callers of this helper that need finer-grained inspection would read
- // the original PGresult; for our assertions the three fields suffice.
- // Store the three fields by overloading the PGresult's error message.
- // libpq provides no public setter for PG_DIAG_SEVERITY etc., so we
- // keep a side-car copy instead.
- (void)dup;
- }
- // Simpler approach: use PQnoticeProcessor (the text-message processor)
- // plus a parallel SQLSTATE/severity capture via PQsetNoticeReceiver on
- // a fresh PGresult stream — but PGresult fields aren't settable. We work
- // around that by capturing the raw text the backend was going to print.
-};
-
-// Simplified, working notice collector: we capture the *text* libpq would
-// normally print (via PQsetNoticeProcessor) plus a parallel stash of sqlstate
-// strings pulled from each notice (via PQsetNoticeReceiver). Both callbacks
-// fire for the same notice, so we record once per callback invocation.
+// Notice collector: captures severity / sqlstate / primary message for every
+// NoticeResponse fired on the connection. Installed via PQsetNoticeReceiver.
struct CapturedNotice {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@test/tap/tests/pgsql-tx_poisoned_recovery-t.cpp` around lines 176 - 198,
Remove the unused NoticeCollector struct and its callback stub (symbols:
NoticeCollector, NoticeCollector::callback) from the test file to avoid
confusion; ensure there are no references to it elsewhere (tests use NoticeBag
and notice_receiver_cb), delete the whole struct and its trailing explanatory
comment block so the file only contains the active notice-handling
implementation (NoticeBag/notice_receiver_cb).
|


Summary
Fixes a silent data-consistency hazard in the PostgreSQL code path: when a backend connection broke mid-transaction (e.g. postmaster SIGKILL, backend
pg_terminate_backend), ProxySQL would silently replay the failed statement on a fresh backend connection as autocommit. The client's nextCOMMITthen landed on a connection with no open transaction and PostgreSQL emittedWARNING: there is no transaction in progress— which the application never saw.Two commits:
68c76eb42fix(pgsql): mark transaction state unknown when backend connection is brokenIn
compute_unknown_transaction_status(), forceunknown_transaction_status = truewheneveris_connected() == false. libpq'sPQtransactionStatus()returnsPQTRANS_UNKNOWNon a broken connection regardless of whether a transaction was active (no cachedSERVER_STATUS_IN_TRANSequivalent to MySQL's), so without this branchIsActiveTransaction()wrongly reportsfalseand the retry guard inhandler_minus1_ClientLibraryError()doesn't trip.8e509d371test(pgsql): regression guard for silent in-transaction retry fixNew TAP test
pgsql-retry_guard_in_txn_on_broken_backend-tthat reproduces the bug: BEGIN +SELECT pg_sleep(3)inside the tx, terminate the backend mid-sleep via a direct superuser connection, assert the client seesPGRES_FATAL_ERROR. Pre-fix this returnsPGRES_TUPLES_OK(silent replay).Why the branch was previously commented out
The
is_connected() == falsebranch was introduced in65fc633cf(Jun 2024), then commented out in24fecc1f6(Jun 2025, "Add PostgreSQL extended query (prepared statement) support") alongside changes to thePQTRANS_UNKNOWNswitch case. The surrounding work points to a workaround for transient states libpq can report during pipeline-mode error recovery. This fix is strictly narrower than that 2025 disable: it restores only the disconnect branch (PQstatus() != CONNECTION_OK, which is not a pipeline transient) and leaves thePQTRANS_UNKNOWN default:preservation intact, so pipeline-mode behavior is unchanged.MySQL is not affected: the MySQL protocol caches
SERVER_STATUS_IN_TRANSinmysql->server_statusfrom the last OK packet and that bit survives the connection breaking.Test plan
make debug && make build_tap_test_debugcd test/infra/docker-pgsql16-single && ./docker-compose-init.bashcd test/tap/tests && ./pgsql-retry_guard_in_txn_on_broken_backend-t— passes on this branch, fails on v3.0 tip (regression guard works both directions)../pgsql-extended_query_protocol_test-t,./pgsql-transaction_state_comprehensive-t,./pgsql-reg_test_5352_prepared_statement_refcount_race-t— confirms the narrower restore does not re-open whatever24fecc1f6was working around.v3.0_pgsql_blogsbranch via the unplanned-primary-failure lab — pre-fix reported zero errors on a 90 s workload across a SIGKILL of the primary postmaster; post-fix reports exactly eight errors (one per in-flight worker) and a sub-second failover window once the synthetic detection delay is backed out.Summary by CodeRabbit
New Features
Bug Fixes
Tests
Chores