Skip to content

aurora: Implement autopurge & fix monitor race condition - #5660

Closed
wazir-ahmed wants to merge 17 commits into
sysown:v3.0from
wazir-ahmed:test/cluster-simulator
Closed

aurora: Implement autopurge & fix monitor race condition#5660
wazir-ahmed wants to merge 17 commits into
sysown:v3.0from
wazir-ahmed:test/cluster-simulator

Conversation

@wazir-ahmed

@wazir-ahmed wazir-ahmed commented Apr 23, 2026

Copy link
Copy Markdown
Member

Summary

  • Implement autopurge for Aurora so that replicas missing from REPLICA_HOST_STATUS are removed from mysql_servers after the configured autopurge_missing_checks threshold.
  • Move cluster_simulator from the Jenkins scripts repo into test/deps/cluster_simulator/ and wire it into the top-level Makefile so make test<cluster_type> builds both src/proxysql (with -DTEST_<TYPE>)
    and the simulator binary.
  • Add 5 simulator-backed TAP groups — cluster_sim_{aurora,galera,group_repl,read_only,repl_lag}-g1 — each driving the simulator via TAP tests test_cluster_sim_<type>-t
  • Small infra plumbing: docker --add-host support in start-proxysql-isolated.bash; .sql pre-proxysql hook dispatcher and missing-infras.lst tolerance in ensure-infras / destroy-infras.

How to run

make test<cluster_type>    # make testaurora | make testgalera

WORKSPACE=$(pwd) INFRA_ID=dev-$USER TAP_GROUP=cluster_sim_<type>-g1 \
    test/infra/control/ensure-infras.bash

WORKSPACE=$(pwd) INFRA_ID=dev-$USER TAP_GROUP=cluster_sim_<type>-g1 \
    test/infra/control/run-tests-isolated.bash

WORKSPACE=$(pwd) INFRA_ID=dev-$USER TAP_GROUP=cluster_sim_<type>-g1 \
    test/infra/control/destroy-infras.bash

Validation

Group Payloads Result
cluster_sim_aurora-g1 11 ok
cluster_sim_galera-g1 5 ok
cluster_sim_group_repl-g1 9 ok
cluster_sim_read_only-g1 9 ok
cluster_sim_repl_lag-g1 3 ok

Summary by CodeRabbit

  • New Features

    • Added a comprehensive cluster simulator test framework (build targets, executable, example payloads, processing tools, and documentation); simulator binary is now ignored by VCS.
  • Build / Test

    • Test targets now ensure the simulator is built before running TAP tests; new phony build targets added for simulator builds.
  • New Configuration / Monitoring

    • Added Aurora autopurge_missing_checks and runtime autopurge behavior to automatically remove missing replicas after a configurable threshold; schema and upgrade paths updated.

- Move cluster_simulator from the Jenkins scripts repo into
  test/deps/cluster_simulator/ (sources, configs, docs, payloads).
- Rework the simulator Makefile to use PROXYSQL_PATH + makefiles_*.mk
  and the repo's vendored deps/.
- Switch vendored json.hpp includes to $(JSON_IDIR).
- Read TEST_<FAMILY> SQLite endpoint from env vars with legacy fallbacks.

Signed-off-by: Wazir Ahmed <wazir@proxysql.com>
- Add build_cluster_simulator{,_debug} that recurse into
  test/deps/cluster_simulator.
- Wire it as a prerequisite of testaurora/testgalera/testgrouprep/
  testreadonly/testreplicationlag.
- .gitignore the cluster_simulator binary.

Signed-off-by: Wazir Ahmed <wazir@proxysql.com>
- Add 5 simulator-backed TAP groups (aurora, galera, group_repl, read_only,
  repl_lag).
- Add a TAP test per group — thin wrappers around cluster_simulator sharing
  cluster_sim_runner.h.
- start-proxysql-isolated.bash: inject CLUSTER_SIM_HOST_FILE entries into
  the proxysql container via docker --add-host.
- ensure-infras.bash / destroy-infras.bash: tolerate groups that omit
  infras.lst; ensure-infras also dispatches both pre-proxysql.bash and
  pre-proxysql.sql.
- test/infra/README.md documents the per-family build requirement.

Signed-off-by: Wazir Ahmed <wazir@proxysql.com>
@wazir-ahmed
wazir-ahmed requested a review from renecannao April 23, 2026 08:26
@coderabbitai

coderabbitai Bot commented Apr 23, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new cluster_simulator test framework (binary, build rules, docs, jq/node helpers, and many JSON fixtures) and implements Aurora autopurge support: schema/runtime column, admin persistence and disk-upgrade, monitor-side autopurge logic, and integration into monitor/hostgroup workflows.

Changes

Cluster Simulator (tests/tooling/runtime simulation DAG)

Layer / File(s) Summary
Data Shape / Configs
test/deps/cluster_simulator/configs/*, test/deps/cluster_simulator/docs/*, test/deps/cluster_simulator/constants
Add CNF templates, README, payload docs, jq filters, Node helper, TAP constants, and many JSON test fixtures describing cluster payload shapes.
Build / Wiring
.gitignore, Makefile, test/deps/cluster_simulator/Makefile
Ignore simulator binary; add build_cluster_simulator/build_cluster_simulator_debug phony targets; require build target for TAP tests; new Makefile builds cluster_simulator (all/debug/clean).
Core Implementation
test/deps/cluster_simulator/cluster_simulator.cpp, test/deps/cluster_simulator/lib/*.cpp, test/deps/cluster_simulator/lib/*.h
New executable with simulate/verify modes and per-cluster handlers; new utility modules (common_utils, aurora_utils, galera_utils, grouprep_utils, readonly_utils, replicationlag_utils) implement JSON parsing, DB prep, diffs/updates, monitor interaction helpers, deterministic serialization/checksums, and runtime/table preparation.
Tests / Fixtures / Tools
test/deps/cluster_simulator/tests/*, test/deps/cluster_simulator/docs/payloads_processing/jq_filters/*, .../node/payload.js
Add extensive JSON fixtures for Aurora/Galera/GroupRep/ReadOnly/Replication-Lag, jq filters, example payloads, and node/jq processing examples; provide README and usage docs.

Aurora autopurge & schema integration (runtime + admin DAG)

Layer / File(s) Summary
Schema / Header
include/ProxySQL_Admin_Tables_Definitions.h, include/Base_HostGroups_Manager.h, include/MySQL_HostGroups_Manager.h, include/ProxySQL_Cluster.hpp
Introduce autopurge_missing_checks column in Aurora hostgroups DDL (V2_0_10), add member to AWS_Aurora_Info, and include field in SELECT/projection macros.
Config / Parsing
lib/ProxySQL_Config.cpp, lib/MySQL_HostGroups_Manager.cpp, lib/Base_HostGroups_Manager.cpp
Read/write autopurge_missing_checks from config and DB rows; bind it in INSERTs; propagate into AWS_Aurora_Info; adjust dump/select projections.
Disk Upgrade / Persistence
lib/ProxySQL_Admin_Disk_Upgrade.cpp, lib/ProxySQL_Admin.cpp, include/ProxySQL_Admin_Tables_Definitions.h
Add upgrade path V2_0_9→V2_0_10 to migrate Aurora hostgroups schema and expand INSERT prepared statements to persist autopurge_missing_checks.
Monitor Logic
include/MySQL_Monitor.hpp, lib/MySQL_Monitor.cpp
Add MySQL_Monitor::aws_aurora_autopurge_servers(...); monitor thread reads autopurge_missing_checks and optional domain_name, tracks per-server absence counters, and removes missing replicas after threshold; small timing safety tweak in unshun recovery computation.
Tests / Fixtures
test/deps/cluster_simulator/tests/*-autopurge*
Add JSON test fixtures exercising autopurge_missing_checks behavior and expected ProxySQL state changes.

Sequence Diagram(s)

sequenceDiagram
participant Runner as "cluster_simulator\n(Runner)"
participant Admin as "ProxySQL Admin\n(runtime/sqlite)"
participant Monitor as "MySQL_Monitor\n(thread)"
participant MySQL as "MySQL Instances"

Runner->>Admin: prepare/INSERT mysql_servers & hostgroups (init)
Runner->>Monitor: trigger monitor cycle / set monitor vars
Monitor->>MySQL: query cluster-specific status (wsrep/replication/aurora)
MySQL-->>Monitor: return host status payloads
Monitor->>Admin: update runtime tables (REPLICA_HOST_STATUS / READONLY_STATUS / etc.)
Monitor->>Monitor: if aurora.autopurge_missing_checks>0 → aws_aurora_autopurge_servers()
Monitor->>Admin: remove missing replica rows (after threshold) and update runtime
Admin-->>Runner: SELECT runtime/host_status for verification
Runner->>Runner: compare actual vs expected, emit JSON result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

  • sysown/proxysql#5373 — Modifies MySQL_Monitor implementation and touches Aurora monitor pathways; related to monitor integrations here.

Suggested reviewers

  • renecannao

Poem

"I tunneled through JSON, C++ and make,

I hop where monitors wake.
I count absent replicas with a twitchy nose,
and nudge the hostgroups where the missing one goes.
A carrot for tests — the simulator grows!"

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

@wazir-ahmed wazir-ahmed changed the title Add cluster simulator to the new CI CI: Add cluster simulator TAP groups Apr 23, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces the ProxySQL Cluster Simulator, a tool for simulating and verifying cluster behaviors across various modes (Galera, ReadOnly, Group Replication, Replication Lag, and Aurora) using an internal SQLite database. The implementation includes the core simulator logic, utility libraries, and an extensive collection of test payloads. The review feedback highlights several critical safety and robustness concerns, including missing null checks for mysql_init() return values, potential resource leaks from unclosed connections in cleanup paths, and a Makefile path detection logic that could trigger an infinite loop. Suggestions were also provided to improve memory safety by nullifying pointers after closing connections and using sizeof for buffer limits in strftime.

Comment thread test/deps/cluster_simulator/cluster_simulator.cpp
Comment thread test/deps/cluster_simulator/cluster_simulator.cpp
Comment thread test/deps/cluster_simulator/Makefile
Comment thread test/deps/cluster_simulator/cluster_simulator.cpp
Comment thread test/deps/cluster_simulator/cluster_simulator.cpp
Comment thread test/deps/cluster_simulator/lib/common_utils.cpp

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 13

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (18)
test/deps/cluster_simulator/cluster_simulator.cpp-2218-2415 (1)

2218-2415: ⚠️ Potential issue | 🟠 Major

proxysql_sqlite handle leaked on connection error paths.

In every cluster-type branch the code does:

proxysql_sqlite = mysql_init(NULL);
if (!mysql_real_connect(proxysql_sqlite, ...)) {
    result = invalid_input_error(...);
    goto cleanup;   // <-- leaks proxysql_sqlite
}

cleanup: only calls mysql_close(proxysql_admin) — the initialized proxysql_sqlite MYSQL* is never freed. The per-iteration mysql_close(proxysql_sqlite) at line 2414 is also skipped by the goto. Additionally, even on the success path, if the previous iteration already closed proxysql_sqlite and a later iteration hits a connection error before the next mysql_init, the same leak occurs.

Suggest closing the handle before jumping to cleanup (or in cleanup: after ensuring it's safe):

🔧 Proposed fix
 cleanup:
 	std::string t_str_res { serialize_result(result.second) };
 	std::cout << t_str_res << std::endl;
 
 	mysql_close(proxysql_admin);
+	if (proxysql_sqlite) {
+		mysql_close(proxysql_sqlite);
+	}
 
 	return result.first;

And reset proxysql_sqlite = nullptr; after each mysql_close(proxysql_sqlite) at line 2414 so the cleanup guard remains accurate.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/deps/cluster_simulator/cluster_simulator.cpp` around lines 2218 - 2415,
The proxysql_sqlite MYSQL* is leaked on error paths because mysql_init(...) is
called then mysql_real_connect(...) can fail and code jumps to the cleanup label
without closing proxysql_sqlite; fix by closing and nulling proxysql_sqlite
before any goto cleanup on connection failure (referencing proxysql_sqlite,
mysql_init, mysql_real_connect, result and the cleanup label), and also make
cleanup robust by checking if proxysql_sqlite != nullptr and calling
mysql_close(proxysql_sqlite) then setting proxysql_sqlite = nullptr; finally
ensure after the normal per-iteration mysql_close(proxysql_sqlite) you reset
proxysql_sqlite = nullptr to keep the guard accurate.
test/deps/cluster_simulator/cluster_simulator.cpp-254-258 (1)

254-258: ⚠️ Potential issue | 🟠 Major

Unused computed state suggests incomplete or incorrect logic across all simulators.

galera_update_cluster_state is designed to return a merged state by comparing servers_state_p (initial) against servers_state_n (new) and updating only fields that differ (via galera_update_state). However, the result is assigned to galera_new_state_to_set and never used; instead, the raw galera_new_servers_state is passed to prepare_galera_cluster_state.

The same pattern occurs in all other simulators:

  • readonly_update_cluster_state (lines 624–627): replication_new_state_to_set unused.
  • grouprep_update_cluster_state (lines 1033–1036): group_replication_new_state_to_set unused.
  • aurora_update_cluster_state (lines 1797–1801): aurora_new_state_to_set unused.

prepare_galera_cluster_state iterates through the entire input vector and inserts each server into the database (lines 301+). This implies it expects the complete, merged state, not just the raw new servers. If the test uses partial state patches, passing only raw galera_new_servers_state instead of the merged result would silently discard fields from the initial state that should be preserved.

Either prepare_*_cluster_state should receive the merged *_new_state_to_set, or the *_update_cluster_state calls and unused locals should be removed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/deps/cluster_simulator/cluster_simulator.cpp` around lines 254 - 258,
The code computes a merged state via galera_update_cluster_state but then passes
the raw galera_new_servers_state into prepare_galera_cluster_state (discarding
preserved fields); change the call to pass galera_new_state_to_set into
prepare_galera_cluster_state (and do the analogous fixes for
readonly_update_cluster_state -> use replication_new_state_to_set,
grouprep_update_cluster_state -> use group_replication_new_state_to_set, and
aurora_update_cluster_state -> use aurora_new_state_to_set), or if you prefer
the alternate approach remove the unused *_update_cluster_state calls and
locals; update each simulator so prepare_*_cluster_state receives the merged
state variable (or delete the dead merge logic) to ensure initial fields are
preserved.
test/deps/cluster_simulator/docs/payloads_processing/node/payload.js-3-16 (1)

3-16: ⚠️ Potential issue | 🟠 Major

Make the helper parse-clean and avoid implicit globals.

Line 7 fails Biome parsing with "Illegal return statement outside of a function," and lines 4 and 16 assign undeclared variables (file and payloads), creating implicit globals. Replace the top-level return with process.exit(1), declare all variable bindings, and set a non-zero exit code in the error handler for proper failure signaling.

Proposed fix
-if (process.argv[2]) {
-    file = process.argv[2];
-} else {
-    console.log('No file for processing supplied.');
-    return -1;
-}
+const file = process.argv[2];
+
+if (!file) {
+    console.error('No file for processing supplied.');
+    process.exit(1);
+}
 
 fs.readFile(file, 'utf8', (err, data) => {
     if (err) {
         console.error(err);
+        process.exitCode = 1;
         return;
     }
 
-    payloads = JSON.parse(data);
+    const payloads = JSON.parse(data);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/deps/cluster_simulator/docs/payloads_processing/node/payload.js` around
lines 3 - 16, The helper uses implicit globals and a top-level return causing
parse errors: declare variables instead of assigning to undeclared identifiers
(e.g., create a const/let for file and let payloads), replace the top-level
"return -1" with process.exit(1) after logging "No file for processing
supplied.", and in the fs.readFile callback set a non-zero exit code on error
(e.g., call process.exit(1) after console.error(err)) before returning; keep
JSON.parse(data) to assign to the declared payloads variable so there are no
implicit globals and the script exits with failure codes on error.
test/deps/cluster_simulator/docs/payloads_processing/jq_filters/servers_in_reader_hostgroups.jq-4-4 (1)

4-4: ⚠️ Potential issue | 🟠 Major

Include port in the dedup key to avoid dropping valid servers.

On Line 4, deduplicating by only hostgroup_id + hostname can collapse distinct entries that differ by port.

Proposed fix
-| unique_by([.hostgroup_id, .hostname])
+| unique_by([.hostgroup_id, .hostname, .port])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@test/deps/cluster_simulator/docs/payloads_processing/jq_filters/servers_in_reader_hostgroups.jq`
at line 4, The dedup key in the jq filter uses unique_by([.hostgroup_id,
.hostname]) which drops distinct server entries that only differ by port; update
the deduplication key to include port by changing the expression to
unique_by([.hostgroup_id, .hostname, .port]) so servers with the same hostgroup
and hostname but different ports are preserved.
test/deps/cluster_simulator/docs/payloads_processing/jq_filters/all_init_state_writers.jq-2-3 (1)

2-3: ⚠️ Potential issue | 🟠 Major

Fix field name in filter comparison.

Line 3 references .writers, but mysql_replication_hostgroups payloads use writer_hostgroup. The filter currently produces empty output.

Fix
 .[0]
     | [.proxysql_init_state[] as $server
-        | (.mysql_replication_hostgroups[] | if $server.hostgroup_id == .writers then $server else empty end)]
+        | (.mysql_replication_hostgroups[] | if $server.hostgroup_id == .writer_hostgroup then $server else empty end)]
     | unique_by([.hostgroup_id, .hostname])
     | .[]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@test/deps/cluster_simulator/docs/payloads_processing/jq_filters/all_init_state_writers.jq`
around lines 2 - 3, The jq filter is comparing $server.hostgroup_id to a
non-existent field `.writers`, so it yields empty results; update the comparison
inside the pipeline that iterates `.mysql_replication_hostgroups[]` to use
`.writer_hostgroup` instead of `.writers` (i.e., change the conditional `if
$server.hostgroup_id == .writers` to `if $server.hostgroup_id ==
.writer_hostgroup`) so the `$server` selection works when using
`.proxysql_init_state[]` and `.mysql_replication_hostgroups[]`.
test/deps/cluster_simulator/docs/payloads_processing/jq_filters/init_state_writers_only.jq-7-20 (1)

7-20: ⚠️ Potential issue | 🟠 Major

Fix server identity correlation in writer selection logic.

The filter selects a writer merely because some init-state server exists in a writer hostgroup, without correlating by hostname and port. Additionally, the reader exclusion loop emits $writer for each non-matching server/hostgroup row rather than excluding servers that appear in reader hostgroups.

The reproduction confirms the issue: writer.example is incorrectly emitted even though it appears in both writer and reader hostgroups with no matching init-state entry.

Proposed rewrite
 .[0]
-    | {
-          init_state: .proxysql_init_state,
-          replication_hostgroups: .mysql_replication_hostgroups,
-          writers_only:
-             [
-                 [.mysql_servers[] as $server
-                         | .proxysql_init_state[] as $init_server
-                         | .mysql_replication_hostgroups[]
-                         | if $server.hostgroup_id == .writer_hostgroup and
-                              $init_server.hostgroup_id == .writer_hostgroup
-                           then $server
-                           else empty end
-                 ][] as $writer
-                 | .mysql_servers[] as $server
-                     | .mysql_replication_hostgroups[]
-                     | if $server.hostname == $writer.hostname and
-                          $server.hostgroup_id == .reader_hostgroup
-                       then empty
-                       else $writer end
-             ]
-      }
-    | .writers_only
-    | unique_by([.hostname, .hostgroup_id]) | .[]
+    as $root
+    | ($root.mysql_replication_hostgroups | map(.writer_hostgroup)) as $writer_hostgroups
+    | ($root.mysql_replication_hostgroups | map(.reader_hostgroup)) as $reader_hostgroups
+    | [
+          $root.mysql_servers[]
+          | select(.hostgroup_id as $hostgroup_id | any($writer_hostgroups[]; . == $hostgroup_id))
+          | select(. as $server
+              | any($root.proxysql_init_state[];
+                    .hostname == $server.hostname
+                    and .port == $server.port
+                    and (.hostgroup_id as $hostgroup_id | any($writer_hostgroups[]; . == $hostgroup_id))))
+          | select(. as $writer
+              | all($root.mysql_servers[];
+                    ((.hostname == $writer.hostname
+                      and .port == $writer.port
+                      and (.hostgroup_id as $hostgroup_id | any($reader_hostgroups[]; . == $hostgroup_id)))
+                     | not)))
+      ]
+    | unique_by([.hostname, .hostgroup_id])
+    | .[]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@test/deps/cluster_simulator/docs/payloads_processing/jq_filters/init_state_writers_only.jq`
around lines 7 - 20, The writer selection currently matches any server in the
writer hostgroup if any proxysql_init_state row exists in that hostgroup and
later the reader exclusion emits $writer repeatedly; update the logic in the jq
filter so (1) when iterating .mysql_servers as $server and
.proxysql_init_state[] as $init_server you require both hostgroup_id AND
identity (hostname and port) to match (use $server.hostname ==
$init_server.hostname and $server.port == $init_server.port) before emitting
$server as $writer, and (2) change the reader exclusion loop over .mysql_servers
and .mysql_replication_hostgroups so you filter out writers by checking identity
(hostname and port) against reader hostgroup entries instead of emitting $writer
for every non-matching row (i.e., only emit $writer when no reader hostgroup
entry matches the writer's hostname+port).
test/deps/cluster_simulator/lib/aurora_utils.cpp-543-569 (1)

543-569: ⚠️ Potential issue | 🟠 Major

aurora_update_state returns identity-less tuples that cannot be correlated back to servers, and the result of aurora_update_cluster_state is currently unused.

result is default-constructed, so std::get<0> (SERVER_ID) and std::get<1> (DOMAIN_NAME) are empty strings. Only indices 2 and 3 are conditionally populated. In aurora_update_cluster_state (lines 571–602), these identity-less tuples are pushed into the returned vector, but the caller's result (aurora_new_state_to_set at line 1798 in cluster_simulator.cpp) is never used—it only appears at assignment. If this code is intended for future use (e.g., logging state changes, auditing), the identity fields must be present; as-is, the function design will fail. Additionally, locals st1_read_only / st2_read_only (lines 556–557) are named for read_only but actually hold REPLICA_LAG_IN_MILLISECONDS — rename for clarity.

🛠️ Suggested fix
 aurora_server_state_t aurora_update_state(
 	const aurora_server_state_t& st1,
 	const aurora_server_state_t& st2
 ) {
-	aurora_server_state_t result {};
+	// Preserve the server identity so callers can correlate the update back to a server.
+	aurora_server_state_t result {
+		std::get<AURORA_SERVER_STATE::SERVER_ID>(st2),
+		std::get<AURORA_SERVER_STATE::DOMAIN_NAME>(st2),
+		"",
+		-1
+	};

 	// TODO: Make this proper doc.
 	// SERVER_ID and DOMAIN_NAME **can't** be changed, because the are part of the server 'id'. Only the other
 	// fields are allowed to change, otherwise, the verification step should have failed.

 	const string st1_session_id { std::get<AURORA_SERVER_STATE::SESSION_ID>(st1) };
 	const string st2_session_id { std::get<AURORA_SERVER_STATE::SESSION_ID>(st2) };

-	int32_t st1_read_only { std::get<AURORA_SERVER_STATE::REPLICA_LAG_IN_MILLISECONDS>(st1) };
-	int32_t st2_read_only { std::get<AURORA_SERVER_STATE::REPLICA_LAG_IN_MILLISECONDS>(st2) };
+	int32_t st1_replica_lag { std::get<AURORA_SERVER_STATE::REPLICA_LAG_IN_MILLISECONDS>(st1) };
+	int32_t st2_replica_lag { std::get<AURORA_SERVER_STATE::REPLICA_LAG_IN_MILLISECONDS>(st2) };

 	// Since empty 'SESSION_IDs' have no meaning, we ignore them for updated states
 	if (st2_session_id != "" && st1_session_id != st2_session_id) {
-		std::get<2>(result) = st2_session_id;
+		std::get<AURORA_SERVER_STATE::SESSION_ID>(result) = st2_session_id;
 	}

-	if (st2_read_only != -1 && st1_read_only != st2_read_only) {
-		std::get<3>(result) = st2_read_only;
+	if (st2_replica_lag != -1 && st1_replica_lag != st2_replica_lag) {
+		std::get<AURORA_SERVER_STATE::REPLICA_LAG_IN_MILLISECONDS>(result) = st2_replica_lag;
 	}

 	return result;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/deps/cluster_simulator/lib/aurora_utils.cpp` around lines 543 - 569,
aurora_update_state currently returns tuples with empty identity fields
(SERVER_ID, DOMAIN_NAME) and misnamed variables for replica lag; update
aurora_update_state to copy the immutable identity fields from st1 (or st2 if
preferred but keep consistent) into result[SERVER_ID] and result[DOMAIN_NAME],
populate indices 2/3 only as before, and rename st1_read_only/st2_read_only to
st1_replica_lag/st2_replica_lag to reflect REPLICA_LAG_IN_MILLISECONDS; also
ensure callers (aurora_update_cluster_state) push the fully-populated tuples
into the return vector (or otherwise consume/return aurora_update_state results)
so the produced state changes can be correlated back to servers.
test/deps/cluster_simulator/tests/grouprep_test_payloads_shunned_preservation/five_node_multimaster-offline_soft.json-56-57 (1)

56-57: ⚠️ Potential issue | 🟠 Major

Fill in the expected final ProxySQL state for the SHUNNED/OFFLINE_SOFT transition.

This fixture configures five servers but leaves proxysql_final_state empty. The cluster_simulator framework validates fixtures against actual computed state during verification (check_cluster_status() in cluster_simulator.cpp), so an empty array will fail verification. Similar fixtures in the same directory—such as five_node_multimaster-unshunning.json and three_node_grouprep-offline_soft.json—all have populated final states reflecting expected OFFLINE_SOFT or status transitions. This fixture needs a corresponding final state matching the SHUNNED/OFFLINE_SOFT behavior it describes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@test/deps/cluster_simulator/tests/grouprep_test_payloads_shunned_preservation/five_node_multimaster-offline_soft.json`
around lines 56 - 57, Add the expected ProxySQL final state entries to the
proxysql_final_state array in the five_node_multimaster-offline_soft.json
fixture: determine per-server final statuses and hostgroup/weight entries that
represent the SHUNNED -> OFFLINE_SOFT transition (matching how
proxysql_final_state is structured in five_node_multimaster-unshunning.json and
three_node_grouprep-offline_soft.json), then populate proxysql_final_state with
one object per server reflecting those OFFLINE_SOFT/shunned states so
check_cluster_status() can validate the fixture.
test/deps/cluster_simulator/tests/grouprep_test_payloads_shunned_preservation/five_node_multimaster-unshunning.json-3-13 (1)

3-13: ⚠️ Potential issue | 🟠 Major

Clarify which server is undergoing the SHUNNED→unshunning transition.

The initial mysql_servers config seeds 127.2.1.1 as SHUNNED (line 13), but the comment on line 8 says "Server '127.2.1.3' should be moved as 'SHUNNED'...". The final state shows 127.2.1.1 moved to backup_writer (unshunned) and 127.2.1.3 promoted to writer, indicating two concurrent transitions are being tested. Rewrite the comment to clearly describe which server exhibits the SHUNNED→unshunning transition versus which is being promoted to writer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@test/deps/cluster_simulator/tests/grouprep_test_payloads_shunned_preservation/five_node_multimaster-unshunning.json`
around lines 3 - 13, The test comment is ambiguous about which host is unshunned
vs promoted; update the "__comment__" block so it explicitly states that
mysql_servers entry "127.2.1.1" is the server that starts as SHUNNED and will be
unshunned/moved to BACKUP WRITER, while "127.2.1.3" is the server that will be
promoted to WRITER (and ensure the comment mentions the expected log entry
showing "UNSHUNNED" for 127.2.1.1 and promotion for 127.2.1.3); keep the rest of
the scenario details
(monitor_groupreplication_max_transaction_behind_for_read_only = 0, max_writers
= 2) intact.
test/deps/cluster_simulator/Makefile-10-11 (1)

10-11: ⚠️ Potential issue | 🟠 Major

Avoid reusing release-compiled objects when running make debug.

The debug target only changes OPT via target-specific variable assignment, but both release and debug modes write object files to ./obj. After a release build, object files are timestamped and up-to-date; GNU Make does not track variable changes as dependencies. Running make debug next will find the .o and .a files already present and skip recompilation, resulting in a "debug" executable containing release-optimized objects (-O2) instead of debug flags (-O0 -DDEBUG).

Separate object directories by build mode (or add another rebuild trigger) so that flag changes force recompilation.

🔧 Proposed fix: separate object/archive paths by build mode
+BUILD_MODE ?= release
 UTILSIDIR := ./lib
-UTILSLDIR := ./obj
+UTILSLDIR = ./obj/$(BUILD_MODE)
@@
 .PHONY: debug
+debug: BUILD_MODE := debug
 debug: OPT := $(STDCPP) -O0 -DDEBUG -ggdb -DDISABLE_WARNING_COUNT_LOGGING -Wl,--no-as-needed -Wl,-rpath,$(TAP_LDIR) -Wl,-rpath,$(POSTGRESQL_PATH)/interfaces/libpq -Wl,-rpath,$(RE2_LDIR) $(WGCOV) $(WASAN) -DGITVERSION=\"$(GIT_VERSION)\"
 debug: cluster_simulator
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/deps/cluster_simulator/Makefile` around lines 10 - 11, The Makefile
currently reuses the same object/archive directories (UTILSIDIR and UTILSLDIR)
for both release and debug builds, causing make to skip recompilation when only
OPT changes; update the Makefile so debug and release builds use distinct
directories (e.g., UTILSIDIR_DEBUG / UTILSLDIR_DEBUG or append the build mode to
UTILSIDIR/UTILSLDIR) or otherwise make the debug target depend on a rebuild
trigger; modify references to UTILSIDIR and UTILSLDIR in the debug-specific
rules and in targets that create .o/.a (and any rules that use OPT or the debug
target-specific variable assignment) so debug objects are written to and read
from the separate debug directories, ensuring debug builds actually recompile
with OPT=-O0 -DDEBUG.
Makefile-202-219 (1)

202-219: ⚠️ Potential issue | 🟠 Major

Sequence the simulator build after the TEST-flavored lib build to avoid race conditions under parallel make.

The test targets list build_cluster_simulator as a sibling prerequisite alongside build_src_test<family>, allowing GNU Make to run both in parallel under make -j. Since build_cluster_simulator has no prerequisites, it can start and complete before build_lib_test<family> finishes, causing the simulator to link against the previous libproxysql.a instead of the newly built TEST-flavored library—contradicting the design intent stated at lines 354-358.

Move the simulator build into the recipe to enforce sequential execution:

Proposed fix
 .PHONY: testaurora
-testaurora: build_src_testaurora build_cluster_simulator
+testaurora: build_src_testaurora
+	${MAKE} build_cluster_simulator
 	# cd test/tap && OPTZ="${O0} -ggdb -DDEBUG -DTEST_AURORA" CC=${CC} CXX=${CXX} ${MAKE}
 	# cd test/tap/tests && OPTZ="${O0} -ggdb -DDEBUG -DTEST_AURORA" CC=${CC} CXX=${CXX} ${MAKE} $(MAKECMDGOALS)
 
 .PHONY: testgalera
-testgalera: build_src_testgalera build_cluster_simulator
+testgalera: build_src_testgalera
+	${MAKE} build_cluster_simulator
 	cd test/tap && OPTZ="${O0} -ggdb -DDEBUG -DTEST_GALERA" CC=${CC} CXX=${CXX} ${MAKE}
 	cd test/tap/tests && OPTZ="${O0} -ggdb -DDEBUG -DTEST_GALERA" CC=${CC} CXX=${CXX} ${MAKE} $(MAKECMDGOALS)
 
 .PHONY: testgrouprep
-testgrouprep: build_src_testgrouprep build_cluster_simulator
+testgrouprep: build_src_testgrouprep
+	${MAKE} build_cluster_simulator
 
 .PHONY: testreadonly
-testreadonly: build_src_testreadonly build_cluster_simulator
+testreadonly: build_src_testreadonly
+	${MAKE} build_cluster_simulator
 
 .PHONY: testreplicationlag
-testreplicationlag: build_src_testreplicationlag build_cluster_simulator
+testreplicationlag: build_src_testreplicationlag
+	${MAKE} build_cluster_simulator
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Makefile` around lines 202 - 219, The test targets (testaurora, testgalera,
testgrouprep, testreadonly, testreplicationlag) currently list
build_cluster_simulator as a prerequisite, allowing parallel make to run it
before the TEST-flavored lib (e.g., build_src_testaurora) finishes; remove
build_cluster_simulator from the prerequisite lists and instead invoke the
simulator build as a command inside each target's recipe so it runs sequentially
after the corresponding build_src_test<family> step (e.g., after
build_src_testaurora complete run build_cluster_simulator via ${MAKE} or
equivalent); update testgalera, testaurora, testgrouprep, testreadonly, and
testreplicationlag targets accordingly.
test/deps/cluster_simulator/lib/readonly_utils.cpp-260-312 (1)

260-312: ⚠️ Potential issue | 🟠 Major

Uninitialized int port / int read_only risk on exception path.

Lines 261–262 declare int port; and int read_only; uninitialized. In the init-state branch they are always set before use inside try, but a caller-visible return path through the catch block leaves them unused which is fine — the actual risk is in the else branch: if all three m_* values are non-null and readable, they are set; otherwise the sentinel values (-1) are assigned. That's OK, but read_only being int here is inconsistent with the header's std::tuple<hostname, port, bool> — assigning -1 to a bool via std::make_tuple will narrow to true, silently losing the "unset" sentinel. Either widen the tuple's third field to int (to match this code's -1 sentinel semantics) or stop emitting the -1 sentinel.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/deps/cluster_simulator/lib/readonly_utils.cpp` around lines 260 - 312,
The code uses an int sentinel (-1) for read_only but the header's return type is
std::tuple<..., bool>, causing silent narrowing; to fix, change the header's
third tuple element from bool to int (preserving the -1 sentinel) and update all
call sites and any std::make_tuple usages to expect an int for the third
element, converting to bool explicitly where the boolean semantics are needed;
this keeps readonly_utils.cpp (variables port and read_only) consistent with the
function's declared return type.
test/deps/cluster_simulator/lib/common_utils.cpp-1042-1080 (1)

1042-1080: ⚠️ Potential issue | 🟠 Major

std::stoi can throw outside the existing try/catch — uncaught exception on bad numeric data.

Lines 1056–1058 call std::stoi on weight / max_connections / use_ssl fetched from runtime_mysql_servers. These aren't wrapped in a try/catch (the extract_cluster_status call below is, but these conversions are not). A non-numeric or empty value would terminate the simulator via an uncaught std::invalid_argument / std::out_of_range. atoi is used for the same columns elsewhere — consider using atoi here too, or wrap in try/catch and return an error pair.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/deps/cluster_simulator/lib/common_utils.cpp` around lines 1042 - 1080,
The loop converting proxysql_init_state fields can throw from std::stoi (calls
in the loop that set "weight", "max_connections", "use_ssl"); replace those
std::stoi calls with safe atoi-style conversions or wrap them in try/catch and
on error populate err_res and return (same failure pattern used when
extract_cluster_status fails) so an invalid/non-numeric value doesn't raise an
uncaught exception; update the conversion block that iterates
j_servers["proxysql_init_state"] and ensure you propagate errors into err_res
before returning out_cluster_status or exiting, referencing the same error
formatting pattern used with string_format and extract_cluster_status.
test/deps/cluster_simulator/lib/common_utils.h-235-272 (1)

235-272: 🛠️ Refactor suggestion | 🟠 Major

Duplicate declarations — remove one of each pair.

check_present_and_type is declared at Line 235 and again at Line 263; matching_server_status is declared at Line 244 and again at Line 272. Identical declarations are legal but obscure which doc block applies and create maintenance drag (any signature change must be done in two places).

-/**
- * `@brief` TODO
- *
- * `@param` j
- * `@param` path
- * `@param` type
- *
- * `@return`
- */
-bool check_present_and_type(const json& j, const std::vector<std::string>& path, const json::value_t& type);
-/**
- * `@brief` TODO
- *
- * `@param` srv_st1
- * `@param` srv_st2
- *
- * `@return`
- */
-bool matching_server_status(const server_status& srv_st1, const server_status& srv_st2);
 /**
  * `@brief`

(Apply to Lines 254–272 — the second duplicated block.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/deps/cluster_simulator/lib/common_utils.h` around lines 235 - 272,
Remove the duplicated declarations and doc blocks for check_present_and_type and
matching_server_status: keep the original declarations (the first occurrences)
and delete the second identical declarations/doc-comments that repeat the same
signatures; ensure only one declaration exists for each symbol
(check_present_and_type and matching_server_status) so future signature/doc
updates are made in a single place.
test/deps/cluster_simulator/lib/common_utils.cpp-863-904 (1)

863-904: ⚠️ Potential issue | 🟠 Major

Resource leak: mysql_store_result result is never freed in get_current_mysql_servers.

my_servers_res is allocated on Line 878 and passed to parse_result_to_json but mysql_free_result is never called (contrast with get_current_cluster_status on Line 1048 which does). Every call leaks a result set.

🛡️ Proposed fix
 		MYSQL_RES* my_servers_res = mysql_store_result(proxysql_admin);
 		parse_result_to_json(my_servers_res, j_servers["mysql_servers"]);
+		mysql_free_result(my_servers_res);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/deps/cluster_simulator/lib/common_utils.cpp` around lines 863 - 904,
get_current_mysql_servers currently calls mysql_store_result and assigns it to
my_servers_res but never calls mysql_free_result, leaking the MYSQL_RES; after
calling parse_result_to_json(my_servers_res, ...) and after
extract_mysql_servers(...) returns (both success and failure paths), ensure
mysql_free_result(my_servers_res) is always called (e.g., free it before setting
err_res in the extract failure branch and in the else/mysql_error branch, and
after successful assignment to out_cur_mysql_servers) so the result set is
released regardless of outcome.
test/deps/cluster_simulator/lib/grouprep_utils.h-21-29 (1)

21-29: ⚠️ Potential issue | 🟠 Major

grouprep_server_state tuple uses bool slots that the implementation treats as int with -1 sentinel.

viable_candidate and read_only are declared bool here, but grouprep_utils.cpp::grouprep_state_members_diff (L659-678) and grouprep_update_state (L733-755) extract them as int and gate updates on != -1. Because bool→int is always 0/1, the sentinel never matches and the “unspecified” semantics silently don’t work. Either change those two slots to int (matching the Galera side) or drop the sentinel checks.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/deps/cluster_simulator/lib/grouprep_utils.h` around lines 21 - 29, The
tuple type grouprep_server_state currently declares the VIABLE_CANDIDATE and
READ_ONLY slots as bool but the implementation (grouprep_state_members_diff and
grouprep_update_state) extracts them as int and checks for -1; change those two
slots in grouprep_server_state from bool to int so the sentinel -1 semantics
work (i.e., update the typedef/using declaration to use int for the third and
fourth elements corresponding to VIABLE_CANDIDATE and READ_ONLY) and verify
callers referencing those tuple positions (and the GROUPREP_SERVER_STATE enum
constants) continue to treat them as int.
test/deps/cluster_simulator/lib/grouprep_utils.cpp-659-681 (1)

659-681: ⚠️ Potential issue | 🟠 Major

-1 sentinel is dead code for viable_candidate / read_only (stored as bool).

Per the header (L21), those slots are bool, so std::get<2>(st) / std::get<3>(st) yield 0 or 1; the != -1 guard is always true and the intended "field unspecified" semantics do not work here. Same pattern is duplicated in grouprep_update_state at L733-755. Consider either widening those tuple elements to int (matching how Galera does it with -1 sentinels) or dropping the sentinel check for boolean fields and using a dedicated "present" mask.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/deps/cluster_simulator/lib/grouprep_utils.cpp` around lines 659 - 681,
The boolean tuple fields st1_viable_candidate/std::get<2>(st) and
st1_read_only/std::get<3>(st) currently use a -1 sentinel check that never
triggers; fix by either (A) changing those tuple element types from bool to int
everywhere they are defined/constructed so the -1 sentinel semantics work
(update the tuple typedef/constructor sites and all uses including
grouprep_update_state), or (B) remove the redundant "!= -1" guards and implement
a proper presence indicator (e.g., add a dedicated bitmask or optional flags
field to the tuple/struct and check that instead before pushing "read_only" /
"viable_candidate"); apply the same fix in the duplicate logic at
grouprep_update_state so the presence semantics are correct and consistent.
test/deps/cluster_simulator/lib/grouprep_utils.cpp-721-755 (1)

721-755: ⚠️ Potential issue | 🟠 Major

grouprep_update_state returns a tuple with identity fields left default.

result is default-constructed and only changed fields are written, so the returned tuple loses hostname, port, and members — every updated server in grouprep_update_cluster_state ends up with empty hostname and port = 0. Callers relying on this vector to identify which server changed will see all entries collapsed onto the same “empty” identity. Start from st2 (or st1) and then overwrite only the diffed fields.

♻️ Suggested shape
-	grouprep_server_state result {};
+	grouprep_server_state result { st2 };   // carry identity fields (hostname/port/members)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/deps/cluster_simulator/lib/grouprep_utils.cpp` around lines 721 - 755,
grouprep_update_state currently default-constructs result so
hostname/port/members are lost; change it to initialize result from st2 (or st1)
and then apply the conditional overwrites for viable_candidate, read_only, and
transactions_behind so identity fields are preserved; update the function
grouprep_update_state to return a copy of st2 with only the differing fields
replaced (this will also fix grouprep_update_cluster_state consumers that expect
host/port/members to remain intact).
🧹 Nitpick comments (11)
test/deps/cluster_simulator/cluster_simulator.cpp (2)

35-43: Empty @brief TODO doc comments.

The Doxygen blocks for operation_mode (line 35) and simulator_options (line 42) are placeholders (@brief TODO). Please fill them in or drop the comment — they currently add noise without information.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/deps/cluster_simulator/cluster_simulator.cpp` around lines 35 - 43,
Replace the placeholder Doxygen brief comments for the enum operation_mode and
the struct/class simulator_options by either removing the empty `@brief` blocks
or, preferably, replacing them with concise descriptions: for operation_mode
document the two modes (simulate — run simulation; verify — run verification
checks) and for simulator_options summarize what configuration it holds (e.g.,
simulation parameters, flags, timeouts). Update the Doxygen comments directly
above the operation_mode declaration and the simulator_options definition to
reflect these meaningful summaries.

589-592: Replace usleep with portable sleep alternatives.

sleep_delay is computed as ..._interval_s + size*timeout_s*0.1 + 1, so it's always ≥ 1 second, meaning the usleep argument is always ≥ 10⁶ microseconds. While POSIX specifies EINVAL as a valid return value for useconds ≥ 1,000,000, glibc and musl handle it by internally converting to nanosleep, making this pattern work on Linux but non-portable. Use std::this_thread::sleep_for(std::chrono::duration<double>(sleep_delay)) or nanosleep instead. Same pattern at lines 635, 997, 1044, 1373, 1416, 1760, 1809.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/deps/cluster_simulator/cluster_simulator.cpp` around lines 589 - 592,
The code uses usleep(sleep_delay * pow(10, 6)) where sleep_delay (double) is
computed (e.g., readonly_interval_s +
mysql_servers.size()*readonly_timeout_s*0.1 + 1), which is non-portable; replace
each such usleep call (the one shown and the similar occurrences at the other
listed sites) with
std::this_thread::sleep_for(std::chrono::duration<double>(sleep_delay)) and add
the required headers (<thread> and <chrono>) to the file; search for the usleep
usages in cluster_simulator.cpp (the sleep_delay variable and the usleep(...)
call) and change them all to the std::this_thread::sleep_for variant for
portability.
test/deps/cluster_simulator/lib/aurora_utils.cpp (1)

395-405: Unconditional 1-second usleep inside a helper called on every state transition.

usleep(1000 * 1000) between the cleanup and the transactional insert is a hard-coded sleep that runs unconditionally on every call to prepare_aurora_cluster_state. Across 10 aurora scenarios this adds ~10s of wall time per run, and the rationale isn't commented. If this is waiting for ProxySQL's monitor loop to observe the cleanup before the new inserts, it should be a named constant tied to the monitor interval (or, better, poll until the state is observable) rather than a magic sleep.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/deps/cluster_simulator/lib/aurora_utils.cpp` around lines 395 - 405, The
unconditional usleep(1000 * 1000) in prepare_aurora_cluster_state causes a fixed
1s delay on every state transition; replace it with a bounded wait that either
polls until ProxySQL observes the cleanup or uses a named constant tied to the
monitor interval. Specifically, remove the hard sleep and implement a short loop
(with timeout) that checks the observable condition (or a helper that queries
proxysql_sqlite / monitor state) before proceeding to the transactional
mysql_query(proxysql_sqlite, "BEGIN IMMEDIATE"); introduce a descriptive
constant like PROXYSQL_MONITOR_POLL_MS or MAX_WAIT_FOR_MONITOR_MS and use it for
polling/backoff and timeout to avoid indefinite hangs.
test/deps/cluster_simulator/tests/aurora_tests_payloads/multiple_clusters-failover_replica_lag.json (1)

92-100: Known-issue note is helpful; consider linking an issue tracker.

The __proxysql_final_state_details__ documents an accepted race between aws_aurora_replication_lag_action and update_aws_aurora_set_writer across clusters. If a GitHub issue or ticket tracks the potential fix ("regenerate myhgm.mysql_servers after a replication_lag action"), consider referencing its ID here so future readers can follow up without grepping the codebase.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@test/deps/cluster_simulator/tests/aurora_tests_payloads/multiple_clusters-failover_replica_lag.json`
around lines 92 - 100, Update the documented known-issue entry under
"__proxysql_final_state_details__" to include a direct reference to the tracking
ticket/issue so readers can follow progress; specifically append one more array
element with the issue ID or URL (for example "Tracked in GH issue `#1234`: <url>"
or a JIRA ticket) and mention the related symbols
"aws_aurora_replication_lag_action", "update_aws_aurora_set_writer", and the
proposed fix "regenerate myhgm.mysql_servers after a replication_lag action" so
the reference is discoverable.
test/deps/cluster_simulator/docs/payloads_creation/README.md (1)

34-35: Preposition nit.

"Moved in reader_hostgroup" reads awkwardly; consider "Moved to reader_hostgroup" (applies to lines 34-35 and the mirrored text at line 122).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/deps/cluster_simulator/docs/payloads_creation/README.md` around lines 34
- 35, Replace the awkward phrase "Moved in `reader_hostgroup`" with "Moved to
`reader_hostgroup`" wherever it appears in the README payload examples
(specifically the lines describing `writer_hostgroup` detections showing `RO=1`
with `writer_is_also_reader=0` and `writer_is_also_reader=1` and the mirrored
occurrence later in the file); update the two instances of the exact string
"Moved in `reader_hostgroup`" to "Moved to `reader_hostgroup`" to fix the
preposition and keep wording consistent.
test/deps/cluster_simulator/lib/common_utils.cpp (1)

56-88: Redundant mysql_fetch_fields call.

Line 62 calls mysql_fetch_fields(result) and discards the return value; Line 64 calls it again and keeps it. Drop the first call.

-	mysql_fetch_fields(result);
 	int num_fields = mysql_num_fields(result);
 	MYSQL_FIELD* fields = mysql_fetch_fields(result);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/deps/cluster_simulator/lib/common_utils.cpp` around lines 56 - 88, In
parse_result_to_json(MYSQL_RES *result, ordered_json& j) remove the redundant
call to mysql_fetch_fields(result) at the earlier location so only the single
call that assigns MYSQL_FIELD* fields = mysql_fetch_fields(result); remains;
ensure num_fields = mysql_num_fields(result) is still computed before iterating
and that field_names is populated from fields as currently done.
test/deps/cluster_simulator/lib/replicationlag_utils.cpp (1)

392-405: replicationlag_state_members_diff signature mismatch vs. .cpp storage type.

The header declares replicationlag_server_state as std::tuple<hostname, port, std::shared_ptr<int>>, and std::get<2> here is accessed as std::shared_ptr<int>. However, extract_replicationlag_servers_state (Lines 105, 117, 151) stores values into a std::unique_ptr<int> local then moves it into std::make_tuple, relying on unique_ptr→shared_ptr implicit conversion at tuple construction — this works but is confusing and will silently stop working if someone changes the header to use unique_ptr (or vice versa). Consider using std::make_shared<int>(...) directly to match the stored type exactly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/deps/cluster_simulator/lib/replicationlag_utils.cpp` around lines 392 -
405, The comparison function accesses std::get<2> as std::shared_ptr<int> but
elsewhere (extract_replicationlag_servers_state) you create a
std::unique_ptr<int> and rely on implicit conversion when building the tuple;
change those factories to create std::shared_ptr<int> directly (use
std::make_shared<int>(value)) when constructing the replicationlag_server_state
tuple so the stored type exactly matches the header and
replicationlag_state_members_diff's use of std::shared_ptr<int>, avoiding the
fragile unique_ptr→shared_ptr conversion.
test/deps/cluster_simulator/lib/galera_utils.h (1)

40-53: Consider a named struct or enum for the 11-field galera_server_state tuple.

An 11-element heterogeneous tuple with repeated int fields (positions 0, 2, 3, 4, 5, 6, 8) is easy to index wrong — e.g., std::get<4> could mean any of several similarly typed members. Either use an enum-based accessor like MYSQL_SERVER_STATUS_T in common_utils.h, or switch to a named struct as done for aurora_hostgroup_config_t. This prevents silent field-position bugs in the simulator code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/deps/cluster_simulator/lib/galera_utils.h` around lines 40 - 53, The
11-element heterogeneous tuple alias galera_server_state is error-prone due to
repeated int positions; replace it with a named struct (e.g., struct
galera_server_state_t) containing clearly named fields for each tuple element
(or define an enum accessor similar to MYSQL_SERVER_STATUS_T) and update uses of
galera_server_state to access members by name (or via enum indices) instead of
std::get<N>; reference the existing galera_server_state alias,
MYSQL_SERVER_STATUS_T in common_utils.h, and aurora_hostgroup_config_t as
examples for naming and usage.
test/deps/cluster_simulator/lib/aurora_utils.h (1)

39-44: Prefer scoped enum or struct-wrapped enum to avoid name collisions.

AURORA_SERVER_STATE is an unscoped enum with very generic identifiers (SERVER_ID, DOMAIN_NAME, SESSION_ID, REPLICA_LAG_IN_MILLISECONDS) that leak into the enclosing namespace. SERVER_ID in particular is a common name and is likely to collide with existing macros/identifiers elsewhere in the ProxySQL tree as more code is linked. Use the struct-wrapped pattern already adopted in common_utils.h (MYSQL_SERVER_STATUS_T, HOSTGROUP_ATTRIBUTES_T) or an enum class for consistency.

-enum AURORA_SERVER_STATE {
-	SERVER_ID,
-	DOMAIN_NAME,
-	SESSION_ID,
-	REPLICA_LAG_IN_MILLISECONDS
-};
+struct AURORA_SERVER_STATE_T {
+	enum {
+		SERVER_ID,
+		DOMAIN_NAME,
+		SESSION_ID,
+		REPLICA_LAG_IN_MILLISECONDS
+	};
+};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/deps/cluster_simulator/lib/aurora_utils.h` around lines 39 - 44,
AURORA_SERVER_STATE is an unscoped enum exposing generic names (SERVER_ID,
DOMAIN_NAME, SESSION_ID, REPLICA_LAG_IN_MILLISECONDS) into the global namespace;
change it to a scoped enum or the project’s struct-wrapped pattern (e.g., wrap
in a struct like AURORA_SERVER_STATE_T or use enum class AURORA_SERVER_STATE)
and update all uses to qualify the enumerators (e.g.,
AURORA_SERVER_STATE_T::SERVER_ID or AURORA_SERVER_STATE::SERVER_ID) to avoid
symbol collisions with existing identifiers/macros.
test/deps/cluster_simulator/lib/readonly_utils.cpp (1)

420-457: prepare_readonly_cluster_state: unconditional 1-second sleep on every call.

usleep(1000 * 1000); (Line 437) runs for every invocation even when cleanup == false and there were no prior DDL/runtime loads to settle. Across the ~9 readonly test payloads this adds serial wall-clock time with no corresponding barrier. Consider gating it behind if (cleanup) or removing it if the caller already synchronizes via ProxySQL runtime reloads. Same observation applies to prepare_replicationlag_cluster_state (Line 261).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/deps/cluster_simulator/lib/readonly_utils.cpp` around lines 420 - 457,
The unconditional usleep call inside prepare_readonly_cluster_state causes an
unnecessary 1s delay on every invocation; change the behavior so the usleep(1000
* 1000) is only executed when cleanup is true (i.e. gate the sleep behind if
(cleanup)) or remove it entirely if the caller already ensures synchronization,
and apply the same fix to prepare_replicationlag_cluster_state to avoid the
serial delay on every call; locate the usleep invocation and the cleanup boolean
in those functions to make the modification.
test/deps/cluster_simulator/lib/grouprep_utils.h (1)

42-51: Uses MYSQL* without explicitly including <mysql.h>.

The declarations at L42, L47, L63, L65 take MYSQL* but the header only includes <string>, <vector>, <tuple>, <utility>, json.hpp, and common_utils.h. This compiles today only because common_utils.h happens to pull <mysql.h> in transitively — any reorg there would break this header. Add a direct include for robustness.

♻️ Suggested include
 `#include` <utility>

+#include <mysql.h>
+
 `#include` "json.hpp"

 `#include` "common_utils.h"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/deps/cluster_simulator/lib/grouprep_utils.h` around lines 42 - 51, The
header uses the MYSQL type in declarations (e.g.,
prepare_mysql_group_replication_hostgroups and prepare_grouprep_cluster_state)
but does not directly include <mysql.h>, relying on a transitive include from
common_utils.h; add a direct `#include` <mysql.h> at the top of
test/deps/cluster_simulator/lib/grouprep_utils.h alongside the existing includes
so MYSQL is defined regardless of include reorganization.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: acd37cb6-e8e4-4302-b9f9-d86e3fceb558

📥 Commits

Reviewing files that changed from the base of the PR and between 7ca0d15 and 1a42cd9.

📒 Files selected for processing (115)
  • .gitignore
  • Makefile
  • test/deps/cluster_simulator/Makefile
  • test/deps/cluster_simulator/README.md
  • test/deps/cluster_simulator/cluster_simulator.cpp
  • test/deps/cluster_simulator/configs/aurora_sim_conf.cnf
  • test/deps/cluster_simulator/configs/galera_sim_conf.cnf
  • test/deps/cluster_simulator/configs/grouprep_sim_conf.cnf
  • test/deps/cluster_simulator/configs/readonly_sim_conf.cnf
  • test/deps/cluster_simulator/configs/repl_sim_conf.cnf
  • test/deps/cluster_simulator/configs/repl_sim_conf.sql
  • test/deps/cluster_simulator/constants
  • test/deps/cluster_simulator/docs/example_payloads/simple_test_err_payload.json
  • test/deps/cluster_simulator/docs/example_payloads/simple_test_payload.json
  • test/deps/cluster_simulator/docs/payloads_creation/README.md
  • test/deps/cluster_simulator/docs/payloads_processing/README.md
  • test/deps/cluster_simulator/docs/payloads_processing/examples/read_only_test_multiple_servers_multiple_hostgroups_incomplete.json
  • test/deps/cluster_simulator/docs/payloads_processing/examples/read_only_test_multiple_servers_multiple_hostgroups_invalid.json
  • test/deps/cluster_simulator/docs/payloads_processing/jq_filters/add_servers_comment.jq
  • test/deps/cluster_simulator/docs/payloads_processing/jq_filters/add_servers_comment.md
  • test/deps/cluster_simulator/docs/payloads_processing/jq_filters/all_init_state_readers.jq
  • test/deps/cluster_simulator/docs/payloads_processing/jq_filters/all_init_state_readers.md
  • test/deps/cluster_simulator/docs/payloads_processing/jq_filters/all_init_state_writers.jq
  • test/deps/cluster_simulator/docs/payloads_processing/jq_filters/hostgroups_by_type_by_hostname.jq
  • test/deps/cluster_simulator/docs/payloads_processing/jq_filters/hostgroups_by_type_by_hostname.md
  • test/deps/cluster_simulator/docs/payloads_processing/jq_filters/init_state_rm_invalid_readers.jq
  • test/deps/cluster_simulator/docs/payloads_processing/jq_filters/init_state_rm_invalid_readers.md
  • test/deps/cluster_simulator/docs/payloads_processing/jq_filters/init_state_writers_only.jq
  • test/deps/cluster_simulator/docs/payloads_processing/jq_filters/init_state_writers_only.md
  • test/deps/cluster_simulator/docs/payloads_processing/jq_filters/mv_readers_to_writers.jq
  • test/deps/cluster_simulator/docs/payloads_processing/jq_filters/mv_readers_to_writers.md
  • test/deps/cluster_simulator/docs/payloads_processing/jq_filters/servers_in_reader_hostgroups.jq
  • test/deps/cluster_simulator/docs/payloads_processing/jq_filters/servers_in_reader_hostgroups.md
  • test/deps/cluster_simulator/docs/payloads_processing/jq_filters/servers_in_writer_hostgroups.jq
  • test/deps/cluster_simulator/docs/payloads_processing/jq_filters/servers_in_writer_hostgroups.md
  • test/deps/cluster_simulator/docs/payloads_processing/node/payload.js
  • test/deps/cluster_simulator/lib/aurora_utils.cpp
  • test/deps/cluster_simulator/lib/aurora_utils.h
  • test/deps/cluster_simulator/lib/common_utils.cpp
  • test/deps/cluster_simulator/lib/common_utils.h
  • test/deps/cluster_simulator/lib/galera_utils.cpp
  • test/deps/cluster_simulator/lib/galera_utils.h
  • test/deps/cluster_simulator/lib/grouprep_utils.cpp
  • test/deps/cluster_simulator/lib/grouprep_utils.h
  • test/deps/cluster_simulator/lib/readonly_utils.cpp
  • test/deps/cluster_simulator/lib/readonly_utils.h
  • test/deps/cluster_simulator/lib/replicationlag_utils.cpp
  • test/deps/cluster_simulator/lib/replicationlag_utils.h
  • test/deps/cluster_simulator/tests/aurora_tests_payloads/multiple_clusters-autodiscovery.json
  • test/deps/cluster_simulator/tests/aurora_tests_payloads/multiple_clusters-duplicated_servers.json
  • test/deps/cluster_simulator/tests/aurora_tests_payloads/multiple_clusters-failover.json
  • test/deps/cluster_simulator/tests/aurora_tests_payloads/multiple_clusters-failover_replica_lag.json
  • test/deps/cluster_simulator/tests/aurora_tests_payloads/multiple_clusters-replica_lag.json
  • test/deps/cluster_simulator/tests/aurora_tests_payloads/three_nodes_cluster-autodiscovery.json
  • test/deps/cluster_simulator/tests/aurora_tests_payloads/three_nodes_cluster-failover_only.json
  • test/deps/cluster_simulator/tests/aurora_tests_payloads/three_nodes_cluster-failover_replica_lag.json
  • test/deps/cluster_simulator/tests/aurora_tests_payloads/three_nodes_cluster-new_reader_weight.json
  • test/deps/cluster_simulator/tests/aurora_tests_payloads/three_nodes_cluster-replica_lag.json
  • test/deps/cluster_simulator/tests/galera_tests_payloads/test_template.json
  • test/deps/cluster_simulator/tests/galera_tests_payloads/three_node_galera-SST.json
  • test/deps/cluster_simulator/tests/galera_tests_payloads/three_node_galera-no_valid_nodes.json
  • test/deps/cluster_simulator/tests/galera_tests_payloads/three_node_galera-wsrep_desync.json
  • test/deps/cluster_simulator/tests/galera_tests_payloads/three_node_galera-wsrep_reject_queries.json
  • test/deps/cluster_simulator/tests/grouprep_test_payloads_shunned_preservation/README.md
  • test/deps/cluster_simulator/tests/grouprep_test_payloads_shunned_preservation/five_node_multimaster-offline_soft.json
  • test/deps/cluster_simulator/tests/grouprep_test_payloads_shunned_preservation/five_node_multimaster-unshunning.json
  • test/deps/cluster_simulator/tests/grouprep_test_payloads_shunned_preservation/five_node_multimaster.json
  • test/deps/cluster_simulator/tests/grouprep_tests_payloads/five_node_multimaster-extra_hostgroup.json
  • test/deps/cluster_simulator/tests/grouprep_tests_payloads/five_node_multimaster-one_primary_shunning.json
  • test/deps/cluster_simulator/tests/grouprep_tests_payloads/five_node_multimaster-shunned_promotion.json
  • test/deps/cluster_simulator/tests/grouprep_tests_payloads/five_node_multimaster-two_primary_shunning.json
  • test/deps/cluster_simulator/tests/grouprep_tests_payloads/grouprep_test_template.json
  • test/deps/cluster_simulator/tests/grouprep_tests_payloads/single_primary_autodiscovery.json
  • test/deps/cluster_simulator/tests/grouprep_tests_payloads/three_node_grouprep-extra_hostgroup.json
  • test/deps/cluster_simulator/tests/grouprep_tests_payloads/three_node_grouprep-offline_soft.json
  • test/deps/cluster_simulator/tests/grouprep_tests_payloads/three_node_grouprep-replication_lag.json
  • test/deps/cluster_simulator/tests/readonly_tests_payloads/read_only_test_multiple_hostgroups.json
  • test/deps/cluster_simulator/tests/readonly_tests_payloads/read_only_test_multiple_hostgroups_writer_only.json
  • test/deps/cluster_simulator/tests/readonly_tests_payloads/read_only_test_multiple_servers_multiple_hostgroups.json
  • test/deps/cluster_simulator/tests/readonly_tests_payloads/read_only_test_multiple_servers_single_hostgroup.json
  • test/deps/cluster_simulator/tests/readonly_tests_payloads/read_only_test_single_server_multiple_hostgroups.json
  • test/deps/cluster_simulator/tests/readonly_tests_payloads/read_only_test_single_server_single_hostgroups.json
  • test/deps/cluster_simulator/tests/readonly_tests_payloads/read_only_test_single_server_two_hostgroups.json
  • test/deps/cluster_simulator/tests/readonly_tests_payloads/read_only_test_two_server_single_hostgroup.json
  • test/deps/cluster_simulator/tests/readonly_tests_payloads/read_only_test_two_server_two_hostgroups.json
  • test/deps/cluster_simulator/tests/repl_tests_payloads/replication_lag_test.json
  • test/deps/cluster_simulator/tests/repl_tests_payloads/replication_lag_test_by_host.json
  • test/deps/cluster_simulator/tests/repl_tests_payloads/replication_lag_test_template.json
  • test/infra/README.md
  • test/infra/control/destroy-infras.bash
  • test/infra/control/ensure-infras.bash
  • test/infra/control/start-proxysql-isolated.bash
  • test/tap/groups/cluster_sim_aurora/add-hosts
  • test/tap/groups/cluster_sim_aurora/env.sh
  • test/tap/groups/cluster_sim_aurora/pre-proxysql.bash
  • test/tap/groups/cluster_sim_aurora/pre-proxysql.sql
  • test/tap/groups/cluster_sim_galera/env.sh
  • test/tap/groups/cluster_sim_galera/pre-proxysql.bash
  • test/tap/groups/cluster_sim_galera/pre-proxysql.sql
  • test/tap/groups/cluster_sim_group_repl/env.sh
  • test/tap/groups/cluster_sim_group_repl/pre-proxysql.bash
  • test/tap/groups/cluster_sim_group_repl/pre-proxysql.sql
  • test/tap/groups/cluster_sim_read_only/env.sh
  • test/tap/groups/cluster_sim_read_only/pre-proxysql.bash
  • test/tap/groups/cluster_sim_read_only/pre-proxysql.sql
  • test/tap/groups/cluster_sim_repl_lag/env.sh
  • test/tap/groups/cluster_sim_repl_lag/pre-proxysql.bash
  • test/tap/groups/cluster_sim_repl_lag/pre-proxysql.sql
  • test/tap/groups/groups.json
  • test/tap/tests/cluster_sim_runner.h
  • test/tap/tests/test_cluster_sim_aurora-t.cpp
  • test/tap/tests/test_cluster_sim_galera-t.cpp
  • test/tap/tests/test_cluster_sim_group_repl-t.cpp
  • test/tap/tests/test_cluster_sim_read_only-t.cpp
  • test/tap/tests/test_cluster_sim_repl_lag-t.cpp

Comment on lines +104 to +141
string comment {};
json j_comment {};

// Default values for optional fields
int32_t min_lag_ms = 30;
int32_t add_lag_ms = 30;

try {
writer_hostgroup = j_aurora_hg.at("writer_hostgroup");
reader_hostgroup = j_aurora_hg.at("reader_hostgroup");
active = j_aurora_hg.at("active");
domain_name = j_aurora_hg.at("domain_name");
max_lag_ms = j_aurora_hg.at("max_lag_ms");
writer_is_also_reader = j_aurora_hg.at("writer_is_also_reader");
new_reader_weight = j_aurora_hg.at("new_reader_weight");

// Optional fields
if (j_aurora_hg.find("add_lag_ms") != j_aurora_hg.end()) {
add_lag_ms = j_aurora_hg.at("add_lag_ms");
}
if (j_aurora_hg.find("min_lag_ms") != j_aurora_hg.end()) {
min_lag_ms = j_aurora_hg.at("min_lag_ms");
}
} catch (const std::exception& e) {
// TODO: Improve error message here
return { EXIT_FAILURE, e.what() };
}

if (j_comment == nullptr) {
comment = "NULL";
} else {
try {
comment = j_aurora_hg.at("comment");
} catch (const std::exception& e) {
// TODO: Improve error message here
return { EXIT_FAILURE, e.what() };
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

j_comment is never assigned — the comment field is always set to the literal string "NULL".

j_comment is default-constructed to a null JSON value on line 105 and nothing ever assigns to it before the check on line 132, so j_comment == nullptr is unconditionally true and the else branch (which would actually read j_aurora_hg.at("comment")) is dead code. As a result every inserted row gets comment = 'NULL' (a string literal), regardless of what the payload provides.

Note this is further compounded by t_aurora_hostgroup_insert formatting comment with single quotes (line 53: '%s'), so even an intentional SQL NULL cannot be produced via this path.

🛠️ Suggested fix
 		int new_reader_weight = 0;
 		string comment {};
-		json j_comment {};
+		json j_comment {};

 		// Default values for optional fields
 		int32_t min_lag_ms = 30;
 		int32_t add_lag_ms = 30;

 		try {
 			writer_hostgroup = j_aurora_hg.at("writer_hostgroup");
 			reader_hostgroup = j_aurora_hg.at("reader_hostgroup");
 			active = j_aurora_hg.at("active");
 			domain_name = j_aurora_hg.at("domain_name");
 			max_lag_ms = j_aurora_hg.at("max_lag_ms");
 			writer_is_also_reader = j_aurora_hg.at("writer_is_also_reader");
 			new_reader_weight = j_aurora_hg.at("new_reader_weight");
+
+			if (j_aurora_hg.find("comment") != j_aurora_hg.end()) {
+				j_comment = j_aurora_hg.at("comment");
+			}

 			// Optional fields
 			if (j_aurora_hg.find("add_lag_ms") != j_aurora_hg.end()) {
 				add_lag_ms = j_aurora_hg.at("add_lag_ms");
 			}
 			if (j_aurora_hg.find("min_lag_ms") != j_aurora_hg.end()) {
 				min_lag_ms = j_aurora_hg.at("min_lag_ms");
 			}
 		} catch (const std::exception& e) {
 			// TODO: Improve error message here
 			return { EXIT_FAILURE, e.what() };
 		}

-		if (j_comment == nullptr) {
+		if (j_comment.is_null()) {
 			comment = "NULL";
 		} else {
-			try {
-				comment = j_aurora_hg.at("comment");
-			} catch (const std::exception& e) {
-				// TODO: Improve error message here
-				return { EXIT_FAILURE, e.what() };
-			}
+			comment = j_comment.get<std::string>();
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/deps/cluster_simulator/lib/aurora_utils.cpp` around lines 104 - 141, The
code never assigns j_comment from j_aurora_hg so the check "if (j_comment ==
nullptr)" is always true and comment is always the literal "NULL"; fix by
reading the "comment" field from j_aurora_hg (use j_aurora_hg.find("comment") or
j_aurora_hg.contains("comment") to avoid exceptions) into j_comment before the
null check, then set comment to "NULL" if j_comment.is_null() or set comment =
j_comment.get<string>() otherwise; also be aware t_aurora_hostgroup_insert
formats comment with '%s' (single quotes) so if you want an actual SQL NULL
handle that separately (e.g., leave an empty indicator or adjust the insert
formatting) to avoid inserting the string 'NULL'.

Comment on lines +250 to +287
} else {
try {
const auto& m_server_id = server_state.at("SERVER_ID");
const auto& m_session_id = server_state.at("SESSION_ID");
const auto& m_replica_lag_in_ms = server_state.at("REPLICA_LAG_IN_MILLISECONDS");

if (m_server_id == nullptr) {
server_id = "nullptr";
} else {
try {
server_id = server_state["SERVER_ID"];
} catch (const std::exception& e) {
return { EXIT_FAILURE, e.what() };
}
}
if (m_session_id == nullptr) {
session_id = true;
} else {
try {
session_id = server_state["SESSION_ID"];
} catch (const std::exception& e) {
return { EXIT_FAILURE, e.what() };
}
}
if (m_replica_lag_in_ms == nullptr) {
replica_lag_in_ms = -1;
} else {
try {
replica_lag_in_ms = server_state["REPLICA_LAG_IN_MILLISECONDS"];
} catch (const std::exception& e) {
return { EXIT_FAILURE, e.what() };
}
}
} catch (const std::exception& e) {
// TODO: Improve
return { EXIT_FAILURE, e.what() };
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

session_id = true; assigns a bool to a std::string — almost certainly a typo.

On line 266, when SESSION_ID is JSON null in the new_state branch, the code assigns the bool literal true to string session_id. Through bool → int → char this compiles silently and sets session_id to a one-byte string containing \x01, which is then used as the session identity in diffs/updates and the downstream REPLICA_HOST_STATUS INSERT. This is inconsistent with the adjacent handling of m_server_id == nullptr (which sets "nullptr") and of m_replica_lag_in_ms == nullptr (which sets -1 as sentinel).

Given the downstream aurora_update_state/aurora_state_members_diff treat "" as the "ignore" sentinel for SESSION_ID (line 496, 560), the intended value here is very likely "" (or a named sentinel).

🛠️ Suggested fix
 					if (m_session_id == nullptr) {
-						session_id = true;
+						session_id = "";
 					} else {
📝 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.

Suggested change
} else {
try {
const auto& m_server_id = server_state.at("SERVER_ID");
const auto& m_session_id = server_state.at("SESSION_ID");
const auto& m_replica_lag_in_ms = server_state.at("REPLICA_LAG_IN_MILLISECONDS");
if (m_server_id == nullptr) {
server_id = "nullptr";
} else {
try {
server_id = server_state["SERVER_ID"];
} catch (const std::exception& e) {
return { EXIT_FAILURE, e.what() };
}
}
if (m_session_id == nullptr) {
session_id = true;
} else {
try {
session_id = server_state["SESSION_ID"];
} catch (const std::exception& e) {
return { EXIT_FAILURE, e.what() };
}
}
if (m_replica_lag_in_ms == nullptr) {
replica_lag_in_ms = -1;
} else {
try {
replica_lag_in_ms = server_state["REPLICA_LAG_IN_MILLISECONDS"];
} catch (const std::exception& e) {
return { EXIT_FAILURE, e.what() };
}
}
} catch (const std::exception& e) {
// TODO: Improve
return { EXIT_FAILURE, e.what() };
}
}
} else {
try {
const auto& m_server_id = server_state.at("SERVER_ID");
const auto& m_session_id = server_state.at("SESSION_ID");
const auto& m_replica_lag_in_ms = server_state.at("REPLICA_LAG_IN_MILLISECONDS");
if (m_server_id == nullptr) {
server_id = "nullptr";
} else {
try {
server_id = server_state["SERVER_ID"];
} catch (const std::exception& e) {
return { EXIT_FAILURE, e.what() };
}
}
if (m_session_id == nullptr) {
session_id = "";
} else {
try {
session_id = server_state["SESSION_ID"];
} catch (const std::exception& e) {
return { EXIT_FAILURE, e.what() };
}
}
if (m_replica_lag_in_ms == nullptr) {
replica_lag_in_ms = -1;
} else {
try {
replica_lag_in_ms = server_state["REPLICA_LAG_IN_MILLISECONDS"];
} catch (const std::exception& e) {
return { EXIT_FAILURE, e.what() };
}
}
} catch (const std::exception& e) {
// TODO: Improve
return { EXIT_FAILURE, e.what() };
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/deps/cluster_simulator/lib/aurora_utils.cpp` around lines 250 - 287, The
code incorrectly assigns the boolean literal true to the std::string variable
session_id when m_session_id == nullptr; change that sentinel assignment to an
empty string (""), matching the intended "ignore" sentinel used by
aurora_update_state / aurora_state_members_diff; update the branch in the try
block that checks m_session_id to set session_id = "" (and ensure the
surrounding logic that reads server_state["SESSION_ID"] remains unchanged) so
SESSION_ID nulls are handled consistently with m_server_id and
m_replica_lag_in_ms handling.

Comment thread test/deps/cluster_simulator/lib/common_utils.cpp
Comment on lines +935 to +971
if (state == cluster_state::init_state) {
json_key = "proxysql_init_state";
} else {
json_key = "proxysql_final_state";
}

bool has_proxysql_init_state =
check_present_and_type(
galera_test_def, { json_key }, json::value_t::object
);

if (has_proxysql_init_state) {
std::string t_err_msg {
"Invalid input. Unable to find required field '%s'"
};
std::string err_msg {};
string_format(t_err_msg, err_msg, json_key.c_str());

return {
EXIT_FAILURE,
err_msg
};
}

ordered_json m_proxysql_init_state = galera_test_def[json_key];
if (!m_proxysql_init_state.is_array()) {
std::string t_err_msg {
"Invalid input. '%s' isn't of expected type 'array'"
};
std::string err_msg {};
string_format(t_err_msg, err_msg, json_key.c_str());

return {
EXIT_FAILURE,
err_msg
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: extract_cluster_status has inverted presence check and wrong expected type.

Two bugs in the payload validation of proxysql_init_state / proxysql_final_state:

  1. Line 943 asks check_present_and_type(..., json::value_t::object), but the payload is an array (confirmed by is_array() on Line 960 and the downstream for-loop treating entries as objects).
  2. Line 946 branches on if (has_proxysql_init_state) and returns "Unable to find required field" — i.e., the function errors out precisely when the field IS present and of the checked type, and falls through when it is missing.

Net effect: when a valid array is provided, the first check is false (array ≠ object) so execution continues "by luck"; if the field is missing, execution also continues and then hits galera_test_def[json_key] returning null, producing a misleading is_array failure. A test payload with an object there would be wrongly rejected with a "field not found" message.

🐛 Proposed fix
-	bool has_proxysql_init_state =
-		check_present_and_type(
-			galera_test_def, { json_key }, json::value_t::object
-		);
-
-	if (has_proxysql_init_state) {
+	bool has_proxysql_init_state =
+		check_present_and_type(
+			galera_test_def, { json_key }, json::value_t::array
+		);
+
+	if (!has_proxysql_init_state) {
 		std::string t_err_msg {
 			"Invalid input. Unable to find required field '%s'"
 		};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/deps/cluster_simulator/lib/common_utils.cpp` around lines 935 - 971, The
presence/type check for proxysql_init_state/proxysql_final_state is inverted and
uses the wrong expected type: change the call to
check_present_and_type(galera_test_def, { json_key }, json::value_t::array) and
invert the conditional to if (!has_proxysql_init_state) to return the "Unable to
find required field" error; keep the subsequent retrieval into ordered_json
m_proxysql_init_state and the is_array() guard (or remove redundant is_array()
after the corrected check) so valid array payloads pass and missing/non-array
payloads produce the correct error messages; references: json_key,
check_present_and_type, has_proxysql_init_state, m_proxysql_init_state,
extract_cluster_status.

Comment on lines +131 to +176
const std::string t_galera_hosgroup_insert {
"INSERT INTO mysql_galera_hostgroups ("
" writer_hostgroup,"
" backup_writer_hostgroup,"
" reader_hostgroup,"
" offline_hostgroup,"
" active,"
" max_writers,"
" writer_is_also_reader,"
" max_transactions_behind,"
" comment"
") VALUES ("
" %d, %d, %d, %d, %d, %d, %d, %d, %s"
")"
};

std::pair<int, std::string> prepare_mysql_galera_hostgroups(
MYSQL* proxysql_admin,
const std::vector<galera_hostgroup_config>& hostgroups_configs
) {
int query_error = 0;

const std::string hostgroups_cleanup { "DELETE FROM mysql_galera_hostgroups" };
query_error = mysql_query(proxysql_admin, hostgroups_cleanup.c_str());
if (query_error) { return create_query_error(proxysql_admin, hostgroups_cleanup, __FILE__, __LINE__); }

for (const auto& hostgroup_config : hostgroups_configs) {
std::string galera_hostgroup_insert {};

// NOTE: Comment can't be null, no need of special handling
string_format(
t_galera_hosgroup_insert,
galera_hostgroup_insert,
std::get<0>(hostgroup_config),
std::get<1>(hostgroup_config),
std::get<2>(hostgroup_config),
std::get<3>(hostgroup_config),
std::get<4>(hostgroup_config),
std::get<5>(hostgroup_config),
std::get<6>(hostgroup_config),
std::get<7>(hostgroup_config),
std::get<8>(hostgroup_config).c_str()
);

query_error = mysql_query(proxysql_admin, galera_hostgroup_insert.c_str());
if (query_error) { return create_query_error(proxysql_admin, galera_hostgroup_insert, __FILE__, __LINE__); }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Comment placeholder %s is unquoted — INSERT breaks for any non-NULL comment.

t_galera_hosgroup_insert ends with ..., %d, %d, %d, %s) while extractor sets comment = "NULL" when the JSON value is null but otherwise preserves the raw string ("my comment"). Since the template does not wrap %s in single quotes, substituting a real comment produces ..., 0, 0, my comment) which is a syntax error. The grouprep analogue at L48 uses '%s' for the same reason.

Either quote the placeholder and keep storing literal "NULL" (loses the real NULL semantics), or branch on the null case and emit bare NULL vs '<comment>'.

🐛 Minimal fix that matches grouprep behavior
-		" %d, %d, %d, %d, %d, %d, %d, %d, %s"
+		" %d, %d, %d, %d, %d, %d, %d, %d, '%s'"

Comment on lines +366 to +375
if (m_read_only == nullptr) {
read_only = "nullptr";
} else {
try {
read_only =
server_state["read_only"] == "YES" ? true : false;
} catch (const std::exception& e) {
return { EXIT_FAILURE, e.what() };
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

read_only = "nullptr" assigns a string literal to a bool (always true).

read_only is declared bool at L312. The string literal decays to a non-null const char*, which converts to true. The null-branch effectively forces read_only=true regardless of intent. Either drop the null-handling (the tuple can’t represent a tri-state) or use a dedicated sentinel (e.g., change the tuple type).

🐛 Minimal fix
-				if (m_read_only == nullptr) {
-					read_only = "nullptr";
-				} else {
+				if (m_read_only == nullptr) {
+					// TODO: tuple is bool; cannot represent NULL. Default to true (read_only) for now.
+					read_only = true;
+				} else {
 					try {
 						read_only =
 							server_state["read_only"] == "YES" ? true : false;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/deps/cluster_simulator/lib/grouprep_utils.cpp` around lines 366 - 375,
The code assigns the string "nullptr" to the bool variable read_only (declared
at L312), which always converts to true; fix by removing the invalid string
assignment and handle the m_read_only == nullptr case consistently: either set
read_only to a sensible default (e.g., false) or bail out/return an error.
Update the block around m_read_only, replacing the branch that does read_only =
"nullptr" with a deterministic boolean assignment or an early return; ensure you
reference m_read_only, read_only and server_state in the change and remember the
surrounding tuple cannot represent a tri-state so do not attempt to assign a
non-bool sentinel.

Comment on lines +337 to +374
int q_res = mysql_query(
proxysql_admin,
"SELECT hostname, port, read_only FROM READONLY_STATUS"
);

if (q_res == 0) {
ordered_json j_servers {};
j_servers["cluster_type"] = "readonly";
j_servers["readonly_servers_init_state"] = {};

MYSQL_RES* my_servers_res = mysql_store_result(proxysql_admin);
parse_result_to_json(my_servers_res, j_servers["readonly_servers_init_state"]);

// convert the fields into the proper types
for (ordered_json& j_server : j_servers["readonly_servers_init_state"]) {
j_server["hostname"] = atoi(std::string {j_server["hostname"]}.c_str());
j_server["port"] = atoi(std::string {j_server["port"]}.c_str());
j_server["read_only"] = atoi(std::string {j_server["read_only"]}.c_str());
j_server["comment"] = atoi(std::string {j_server["commetn"]}.c_str());
}

std::vector<readonly_server_state> cur_readonly_servers_state {};
std::pair<int, std::string> ext_res =
extract_readonly_servers_state(readonly_state_id::init_state, j_servers, cur_readonly_servers_state);

if (ext_res.first == EXIT_SUCCESS) {
out_cur_readonly_servers_state = cur_readonly_servers_state;
} else {
string_format(t_err_msg, err_msg, ext_res.second.c_str());
err_res = { EXIT_FAILURE, err_msg };
}
} else {
string_format(t_err_msg, err_msg, mysql_error(proxysql_admin));
err_res = { EXIT_FAILURE, err_msg };
}

return err_res;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: get_current_readonly_servers_state will always throw.

Same class of bug as in replicationlag_utils.cpp:

  1. Line 352 applies atoi to hostname (overwrites the string with an int).
  2. Line 355: j_server["commetn"] — typo for comment, and the SELECT at Line 339 only returns hostname, port, read_only, so this field doesn't exist. Accessing it with operator[] inserts null and then std::string{null} throws. Even if the typo were fixed, the column isn't in the query.
  3. extract_readonly_servers_state would also reject a comment key since valid_readonly_entries doesn't include it.
🐛 Proposed fix
 		for (ordered_json& j_server : j_servers["readonly_servers_init_state"]) {
-			j_server["hostname"] = atoi(std::string {j_server["hostname"]}.c_str());
+			j_server["hostname"] = std::string { j_server["hostname"] };
 			j_server["port"] = atoi(std::string {j_server["port"]}.c_str());
 			j_server["read_only"] = atoi(std::string {j_server["read_only"]}.c_str());
-			j_server["comment"] = atoi(std::string {j_server["commetn"]}.c_str());
 		}

Also consider adding mysql_free_result(my_servers_res); for symmetry with other query helpers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/deps/cluster_simulator/lib/readonly_utils.cpp` around lines 337 - 374,
In get_current_readonly_servers_state: stop overwriting hostname with atoi —
leave j_server["hostname"] as a string (do not call atoi on it); convert only
numeric fields (j_server["port"] and j_server["read_only"]) to ints using a safe
string->int conversion after ensuring the keys exist; remove the attempted
access to j_server["commetn"] (typo) since the SELECT only returns hostname,
port, read_only — either add comment to the SELECT if you need it and to
valid_readonly_entries, or drop any comment handling; call
mysql_free_result(my_servers_res) after parse_result_to_json to free resources;
ensure extract_readonly_servers_state is called with the expected keys from
j_servers.

Comment thread test/deps/cluster_simulator/lib/readonly_utils.cpp
Comment on lines +187 to +232
int q_res = mysql_query(
proxysql_admin,
"SELECT hostname, port, seconds_behind_master FROM REPLICATIONLAG_HOST_STATUS"
);

if (q_res == 0) {
ordered_json j_servers {};
j_servers["cluster_type"] = "replicationlag";
j_servers["replicationlag_servers_init_state"] = {};

MYSQL_RES* my_servers_res = mysql_store_result(proxysql_admin);
parse_result_to_json(my_servers_res, j_servers["replicationlag_servers_init_state"]);

// convert the fields into the proper types
for (ordered_json& j_server : j_servers["replicationlag_servers_init_state"]) {
j_server["hostname"] = atoi(std::string {j_server["hostname"]}.c_str());
j_server["port"] = atoi(std::string {j_server["port"]}.c_str());

const auto& seconds_behind_master = j_server.at("seconds_behind_master");
if (seconds_behind_master == nullptr || seconds_behind_master.is_null()) {
j_server["seconds_behind_master"] = nullptr;
} else {
j_server["seconds_behind_master"] = atoi(std::string {j_server["seconds_behind_master"]}.c_str());
}
j_server["comment"] = atoi(std::string {j_server["comment"]}.c_str());
}

std::vector<replicationlag_server_state> cur_replicationlag_servers_state {};
std::pair<int, std::string> ext_res =
extract_replicationlag_servers_state(
replicationlag_state_id::init_state, j_servers, cur_replicationlag_servers_state
);

if (ext_res.first == EXIT_SUCCESS) {
out_cur_replicationlag_servers_state = std::move(cur_replicationlag_servers_state);
} else {
string_format(t_err_msg, err_msg, ext_res.second.c_str());
err_res = { EXIT_FAILURE, err_msg };
}
} else {
string_format(t_err_msg, err_msg, mysql_error(proxysql_admin));
err_res = { EXIT_FAILURE, err_msg };
}

return err_res;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: get_current_replicationlag_servers_state will always fail at runtime.

Multiple problems in the post-query conversion block:

  1. Line 202 applies atoi to hostname and overwrites the JSON field with an integer — this clobbers a string value and then extract_replicationlag_servers_state later expects a string.
  2. Line 211 references j_server["comment"], but the SELECT at Line 189 only returns hostname, port, seconds_behind_master. Access-by-bracket will insert a null entry, then std::string{ null } throws — the whole path dies before reaching extract_replicationlag_servers_state.
  3. The downstream extract_replicationlag_servers_state will then also reject the payload due to invalid_keys because comment isn't in valid_replicationlag_entries.
🐛 Proposed fix
 		for (ordered_json& j_server : j_servers["replicationlag_servers_init_state"]) {
-			j_server["hostname"] = atoi(std::string {j_server["hostname"]}.c_str());
+			j_server["hostname"] = std::string { j_server["hostname"] };
 			j_server["port"] = atoi(std::string {j_server["port"]}.c_str());
 
 			const auto& seconds_behind_master = j_server.at("seconds_behind_master");
 			if (seconds_behind_master == nullptr || seconds_behind_master.is_null()) {
 				j_server["seconds_behind_master"] = nullptr;	
 			} else {
 				j_server["seconds_behind_master"] = atoi(std::string {j_server["seconds_behind_master"]}.c_str());
 			}
-			j_server["comment"] = atoi(std::string {j_server["comment"]}.c_str());
 		}

Also add mysql_free_result(my_servers_res); after parse_result_to_json for consistency with other modules.

📝 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.

Suggested change
int q_res = mysql_query(
proxysql_admin,
"SELECT hostname, port, seconds_behind_master FROM REPLICATIONLAG_HOST_STATUS"
);
if (q_res == 0) {
ordered_json j_servers {};
j_servers["cluster_type"] = "replicationlag";
j_servers["replicationlag_servers_init_state"] = {};
MYSQL_RES* my_servers_res = mysql_store_result(proxysql_admin);
parse_result_to_json(my_servers_res, j_servers["replicationlag_servers_init_state"]);
// convert the fields into the proper types
for (ordered_json& j_server : j_servers["replicationlag_servers_init_state"]) {
j_server["hostname"] = atoi(std::string {j_server["hostname"]}.c_str());
j_server["port"] = atoi(std::string {j_server["port"]}.c_str());
const auto& seconds_behind_master = j_server.at("seconds_behind_master");
if (seconds_behind_master == nullptr || seconds_behind_master.is_null()) {
j_server["seconds_behind_master"] = nullptr;
} else {
j_server["seconds_behind_master"] = atoi(std::string {j_server["seconds_behind_master"]}.c_str());
}
j_server["comment"] = atoi(std::string {j_server["comment"]}.c_str());
}
std::vector<replicationlag_server_state> cur_replicationlag_servers_state {};
std::pair<int, std::string> ext_res =
extract_replicationlag_servers_state(
replicationlag_state_id::init_state, j_servers, cur_replicationlag_servers_state
);
if (ext_res.first == EXIT_SUCCESS) {
out_cur_replicationlag_servers_state = std::move(cur_replicationlag_servers_state);
} else {
string_format(t_err_msg, err_msg, ext_res.second.c_str());
err_res = { EXIT_FAILURE, err_msg };
}
} else {
string_format(t_err_msg, err_msg, mysql_error(proxysql_admin));
err_res = { EXIT_FAILURE, err_msg };
}
return err_res;
}
MYSQL_RES* my_servers_res = mysql_store_result(proxysql_admin);
parse_result_to_json(my_servers_res, j_servers["replicationlag_servers_init_state"]);
// convert the fields into the proper types
for (ordered_json& j_server : j_servers["replicationlag_servers_init_state"]) {
j_server["hostname"] = std::string { j_server["hostname"] };
j_server["port"] = atoi(std::string {j_server["port"]}.c_str());
const auto& seconds_behind_master = j_server.at("seconds_behind_master");
if (seconds_behind_master == nullptr || seconds_behind_master.is_null()) {
j_server["seconds_behind_master"] = nullptr;
} else {
j_server["seconds_behind_master"] = atoi(std::string {j_server["seconds_behind_master"]}.c_str());
}
}
std::vector<replicationlag_server_state> cur_replicationlag_servers_state {};
std::pair<int, std::string> ext_res =
extract_replicationlag_servers_state(
replicationlag_state_id::init_state, j_servers, cur_replicationlag_servers_state
);
if (ext_res.first == EXIT_SUCCESS) {
out_cur_replicationlag_servers_state = std::move(cur_replicationlag_servers_state);
} else {
string_format(t_err_msg, err_msg, ext_res.second.c_str());
err_res = { EXIT_FAILURE, err_msg };
}
} else {
string_format(t_err_msg, err_msg, mysql_error(proxysql_admin));
err_res = { EXIT_FAILURE, err_msg };
}
return err_res;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/deps/cluster_simulator/lib/replicationlag_utils.cpp` around lines 187 -
232, The post-query conversion in get_current_replicationlag_servers_state is
corrupting and accessing fields that aren't present: do not atoi the hostname
(leave j_server["hostname"] as string), only convert j_server["port"] and
j_server["seconds_behind_master"] to integers when non-null, and remove the
erroneous conversion/access of j_server["comment"] (since SELECT returns only
hostname, port, seconds_behind_master) so extract_replicationlag_servers_state
receives the expected keys; also call mysql_free_result(my_servers_res) after
parse_result_to_json to free the result.

Comment on lines +445 to +469
replicationlag_server_state replicationlag_update_state(
const replicationlag_server_state& st1,
const replicationlag_server_state& st2
) {
replicationlag_server_state result {};

// TODO: Make this proper doc.
// hostname and port **can't** be changed,
// because the are part of the server 'id'. Only the
// other fields are allowed to change, otherwise, the
// verification step should have failed.

const std::shared_ptr<int>& st1_replicationlag = std::get<2>(st1);
const std::shared_ptr<int>& st2_replicationlag = std::get<2>(st2);

if (st1_replicationlag != nullptr && st2_replicationlag != nullptr) {
if (st1_replicationlag != st2_replicationlag) {
std::get<2>(result) = st2_replicationlag;
}
} else {
std::get<2>(result) = st2_replicationlag;
}

return result;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: replicationlag_update_state drops hostname/port, producing broken next-state tuples.

result is default-constructed and only std::get<2>(result) is ever assigned. The returned tuple therefore has an empty hostname, port = 0, and the lag maybe set. replicationlag_update_cluster_state then push_backs this into result, meaning the "updated cluster state" loses every server's identity.

Additionally, Line 461 compares the raw shared_ptr pointers rather than their pointees:

if (st1_replicationlag != st2_replicationlag) // different pointers, not values

This should be *st1_replicationlag != *st2_replicationlag.

🐛 Proposed fix
 	replicationlag_server_state result {};
 
-	// TODO: Make this proper doc.
-	// hostname and port **can't** be changed,
-	// because the are part of the server 'id'. Only the
-	// other fields are allowed to change, otherwise, the
-	// verification step should have failed.
+	// hostname and port are part of the identity — preserve them from st1.
+	std::get<0>(result) = std::get<0>(st1);
+	std::get<1>(result) = std::get<1>(st1);
+	std::get<2>(result) = std::get<2>(st1);
 
 	const std::shared_ptr<int>& st1_replicationlag = std::get<2>(st1);
 	const std::shared_ptr<int>& st2_replicationlag = std::get<2>(st2);
 
 	if (st1_replicationlag != nullptr && st2_replicationlag != nullptr) {
-		if (st1_replicationlag != st2_replicationlag) {
+		if (*st1_replicationlag != *st2_replicationlag) {
 			std::get<2>(result) = st2_replicationlag;
 		}
 	} else {
 		std::get<2>(result) = st2_replicationlag;
 	}

The analogous readonly_update_state in readonly_utils.cpp has the same identity-loss bug — see the separate comment on that file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/deps/cluster_simulator/lib/replicationlag_utils.cpp` around lines 445 -
469, replicationlag_update_state currently returns a default-constructed
replicationlag_server_state so hostname and port are lost; change the function
to initialize result from the incoming tuple (e.g., copy st1 or st2 since
identity must be identical) and only update the replication-lag field
(std::get<2>(result)); also fix the pointer comparison: when both
std::shared_ptr<int> st1_replicationlag and st2_replicationlag are non-null
compare their pointees (*st1_replicationlag != *st2_replicationlag) rather than
the shared_ptr addresses; apply the same identity-preserving pattern to the
analogous readonly_update_state if present.

- Closes sysown#5546 and sysown#5547

Signed-off-by: Wazir Ahmed <wazir@proxysql.com>
Signed-off-by: Wazir Ahmed <wazir@proxysql.com>
…hecks`

- Extend the simulator's Aurora hostgroup payload schema with two optional
  fields, `check_interval_ms` and `autopurge_missing_checks`, plumb them
  through `aurora_hostgroup_config_t`, the `valid_aurora_hostgroup_entries`
  allowlist, the INSERT template and the parser/binder so payload-driven
  tests can exercise the autopurge path added for issue sysown#5547.

- Add `three_nodes_cluster-autopurge_missing_replica.json`
  with two scenarios:
    - autopurge_missing_checks=2: a reader vanishes from REPLICA_HOST_STATUS
      and ProxySQL must drop it from mysql_servers within the threshold.
    - autopurge_missing_checks=0: the same vanishing reader stays in
      mysql_servers, asserting the default backwards-compatible behavior.

Signed-off-by: Wazir Ahmed <wazir@proxysql.com>
…er the purge

- Pass cleanup=1 to `prepare_aurora_cluster_state` for the new_state transition.
  Without it, INSERT-OR-REPLACE only adds/updates rows; a reader removed from
  `aurora_servers_new_state` was silently kept in `REPLICA_HOST_STATUS`, so the
  monitor never saw the absence and autopurge never fired.
- With cleanup=1 the simulator now treats `aurora_servers_new_state` as the full
  desired state and deletes anything missing. This is backwards compatible since
  every existing aurora payload already lists the complete server set.

Signed-off-by: Wazir Ahmed <wazir@proxysql.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/ProxySQL_Config.cpp (1)

1400-1443: ⚠️ Potential issue | 🟠 Major

Keep Aurora hostgroup config round-tripping.

autopurge_missing_checks is now loaded into mysql_aws_aurora_hostgroups, but the serializer still uses the older fixed Aurora column order, so saving config will drop this value. Also validate the 0–100 range before insert; otherwise bad configs will trip the table CHECK and this loader won't surface the failure.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ProxySQL_Config.cpp` around lines 1400 - 1443, The INSERT format string q
and the corresponding sprintf call in the mysql_aws_aurora_hostgroups
serialization must be updated to include autopurge_missing_checks in the correct
column order so that saved configs round-trip (adjust q to list
autopurge_missing_checks in VALUES and include autopurge_missing_checks in the
sprintf argument list in the same position), and validate
autopurge_missing_checks before building the query (in the loader code that sets
the autopurge_missing_checks variable) to ensure it is within 0–100—if out of
range, log a proxy_error and clamp it to the nearest valid value (0 or 100)
prior to formatting the query so the DB CHECK cannot fail silently.
🧹 Nitpick comments (1)
test/deps/cluster_simulator/lib/aurora_utils.h (1)

20-51: New Aurora types don't follow the repository naming convention.

If these types are going to stay public in the simulator API, I'd align them now instead of spreading aurora_hostgroup_config_t / aurora_server_state_t / aurora_state_id to more call sites.

As per coding guidelines, **/*.{cpp,h,hpp}: Class names should use PascalCase with protocol prefixes (MySQL_, PgSQL_, ProxySQL_).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/deps/cluster_simulator/lib/aurora_utils.h` around lines 20 - 51, Rename
the public Aurora types to follow PascalCase + prefix convention and update all
references: change struct aurora_hostgroup_config_t to AuroraHostgroupConfig and
update the vector parameter type in extract_aurora_hostgroup_config, change
using aurora_server_state_t to using AuroraServerState, rename enum
AURORA_SERVER_STATE to a PascalCase identifier (e.g., AuroraServerStateIndex or
keep namespaced members and adjust usages), and rename enum class
aurora_state_id to enum class AuroraStateId; update the function signature for
extract_aurora_hostgroup_config and any call sites to use the new type names
(refer to symbols aurora_hostgroup_config_t, aurora_server_state_t,
AURORA_SERVER_STATE, aurora_state_id, and extract_aurora_hostgroup_config to
locate and rename).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/MySQL_HostGroups_Manager.cpp`:
- Around line 3743-3750: The calculation for the shun grace period currently
truncates sub-second values by doing integer division on b (where b is 2 *
mysql_thread___monitor_ping_interval), so change the division to round up
instead of floor: compute the seconds by dividing the millisecond value with
rounding up (e.g., use std::ceil on b/1000.0 or add 999 before integer divide)
so mysql_thread___shun_recovery_time_sec gets the intended safety margin and
avoids losing sub-second intervals; update the code around variables b,
mysql_thread___monitor_ping_interval and mysql_thread___shun_recovery_time_sec
accordingly.

In `@lib/MySQL_Monitor.cpp`:
- Around line 6597-6600: The current use of server_id.rfind(domain_name) can
match domain_name anywhere in the hostname; change the logic to only strip
domain_name when it is a suffix: after finding pos, verify pos != npos AND pos +
domain_name.size() == server_id.size() (or use an ends-with check) before
calling server_id.erase(pos); apply the same suffix-only check for the other
occurrence handling the same variables at the later block (the lines that
currently use rfind(domain_name) around the second occurrence).

In `@test/deps/cluster_simulator/cluster_simulator.cpp`:
- Around line 936-950: The call to set_monitor_variables (its return stored in
set_monitor_variables_err) is currently ignored—add an error check after each
set_monitor_variables invocation (including the REPLICATION_LAG and AURORA code
paths and the other occurrences around lines referenced) that mirrors the
existing mysql_query error handling: on non-zero set_monitor_variables_err
create a query_error (or appropriate error using
create_query_error/create_monitor_error pattern used elsewhere), convert to
internal_error/result, and goto exit to stop the simulator; update the branches
that call set_monitor_variables so they abort on failure rather than continuing
with stale runtime variables.
- Around line 531-564: The current flow pushes monitor_variables into runtime
before applying defaults, causing READ_ONLY to inherit prior state; move the
defaulting step so monitor_variables =
set_monitor_variables_defaults(monitor_variables, monitor_defaults_variables) is
called before invoking set_monitor_variables(proxysql_admin,
rephostgrp_monitor_variables, monitor_variables) and before the subsequent
mysql_query(proxysql_admin, "LOAD MYSQL VARIABLES TO RUNTIME") so the runtime
load receives the defaulted vector; adjust the code around
set_monitor_variables, set_monitor_variables_defaults, and the "LOAD MYSQL
VARIABLES TO RUNTIME" call accordingly.

In
`@test/deps/cluster_simulator/tests/aurora_tests_payloads/three_nodes_cluster-autopurge_missing_replica.json`:
- Around line 52-53: Update the scenario prose to match the fixture's expected
final state: change the "__details__" (or "__comment__") text that currently
says the missing reader stays "just OFFLINE" to state that the missing reader
remains "ONLINE" (or otherwise indicate it remains present but marked ONLINE) so
the description aligns with the assertions around the expected entry at lines
95-98; edit the "__details__" string in the JSON fixture accordingly.

---

Outside diff comments:
In `@lib/ProxySQL_Config.cpp`:
- Around line 1400-1443: The INSERT format string q and the corresponding
sprintf call in the mysql_aws_aurora_hostgroups serialization must be updated to
include autopurge_missing_checks in the correct column order so that saved
configs round-trip (adjust q to list autopurge_missing_checks in VALUES and
include autopurge_missing_checks in the sprintf argument list in the same
position), and validate autopurge_missing_checks before building the query (in
the loader code that sets the autopurge_missing_checks variable) to ensure it is
within 0–100—if out of range, log a proxy_error and clamp it to the nearest
valid value (0 or 100) prior to formatting the query so the DB CHECK cannot fail
silently.

---

Nitpick comments:
In `@test/deps/cluster_simulator/lib/aurora_utils.h`:
- Around line 20-51: Rename the public Aurora types to follow PascalCase +
prefix convention and update all references: change struct
aurora_hostgroup_config_t to AuroraHostgroupConfig and update the vector
parameter type in extract_aurora_hostgroup_config, change using
aurora_server_state_t to using AuroraServerState, rename enum
AURORA_SERVER_STATE to a PascalCase identifier (e.g., AuroraServerStateIndex or
keep namespaced members and adjust usages), and rename enum class
aurora_state_id to enum class AuroraStateId; update the function signature for
extract_aurora_hostgroup_config and any call sites to use the new type names
(refer to symbols aurora_hostgroup_config_t, aurora_server_state_t,
AURORA_SERVER_STATE, aurora_state_id, and extract_aurora_hostgroup_config to
locate and rename).
🪄 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: c1954577-9b3e-4860-89fa-fcb2c409abd9

📥 Commits

Reviewing files that changed from the base of the PR and between 1a42cd9 and 4742050.

📒 Files selected for processing (21)
  • include/Base_HostGroups_Manager.h
  • include/MySQL_HostGroups_Manager.h
  • include/MySQL_Monitor.hpp
  • include/ProxySQL_Admin_Tables_Definitions.h
  • include/ProxySQL_Cluster.hpp
  • lib/Base_HostGroups_Manager.cpp
  • lib/MySQL_HostGroups_Manager.cpp
  • lib/MySQL_Monitor.cpp
  • lib/ProxySQL_Admin.cpp
  • lib/ProxySQL_Admin_Disk_Upgrade.cpp
  • lib/ProxySQL_Cluster.cpp
  • lib/ProxySQL_Config.cpp
  • test/deps/cluster_simulator/cluster_simulator.cpp
  • test/deps/cluster_simulator/lib/aurora_utils.cpp
  • test/deps/cluster_simulator/lib/aurora_utils.h
  • test/deps/cluster_simulator/tests/aurora_tests_payloads/three_nodes_cluster-autopurge_missing_replica.json
  • test/infra/control/docker-fs-helper.bash
  • test/infra/control/start-proxysql-isolated.bash
  • test/infra/control/stop-proxysql-isolated.bash
  • test/infra/infra-mysql57-binlog/docker-compose-init.bash
  • test/tap/groups/groups.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/deps/cluster_simulator/lib/aurora_utils.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). (4)
  • 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
🧰 Additional context used
📓 Path-based instructions (3)
include/**/*.{h,hpp}

📄 CodeRabbit inference engine (CLAUDE.md)

Use #ifndef __CLASS_*_H include guards in header files

Files:

  • include/MySQL_Monitor.hpp
  • include/ProxySQL_Cluster.hpp
  • include/MySQL_HostGroups_Manager.h
  • include/ProxySQL_Admin_Tables_Definitions.h
  • include/Base_HostGroups_Manager.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 and std::atomic<> for counters

Files:

  • include/MySQL_Monitor.hpp
  • lib/Base_HostGroups_Manager.cpp
  • include/ProxySQL_Cluster.hpp
  • lib/ProxySQL_Admin_Disk_Upgrade.cpp
  • lib/ProxySQL_Config.cpp
  • include/MySQL_HostGroups_Manager.h
  • lib/ProxySQL_Cluster.cpp
  • include/ProxySQL_Admin_Tables_Definitions.h
  • include/Base_HostGroups_Manager.h
  • lib/ProxySQL_Admin.cpp
  • lib/MySQL_Monitor.cpp
  • test/deps/cluster_simulator/lib/aurora_utils.h
  • test/deps/cluster_simulator/cluster_simulator.cpp
  • lib/MySQL_HostGroups_Manager.cpp
lib/**/*.cpp

📄 CodeRabbit inference engine (CLAUDE.md)

One class per file typically in lib/ directory

Files:

  • lib/Base_HostGroups_Manager.cpp
  • lib/ProxySQL_Admin_Disk_Upgrade.cpp
  • lib/ProxySQL_Config.cpp
  • lib/ProxySQL_Cluster.cpp
  • lib/ProxySQL_Admin.cpp
  • lib/MySQL_Monitor.cpp
  • lib/MySQL_HostGroups_Manager.cpp
🧠 Learnings (9)
📓 Common learnings
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.
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
📚 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:

  • include/ProxySQL_Cluster.hpp
  • lib/ProxySQL_Admin_Disk_Upgrade.cpp
  • lib/ProxySQL_Config.cpp
  • lib/ProxySQL_Cluster.cpp
  • lib/ProxySQL_Admin.cpp
  • test/deps/cluster_simulator/cluster_simulator.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: 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:

  • include/ProxySQL_Cluster.hpp
  • lib/ProxySQL_Admin_Disk_Upgrade.cpp
  • lib/ProxySQL_Config.cpp
  • lib/ProxySQL_Cluster.cpp
  • lib/ProxySQL_Admin.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:

  • lib/ProxySQL_Admin_Disk_Upgrade.cpp
  • lib/ProxySQL_Config.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/deps/cluster_simulator/lib/aurora_utils.h
  • test/deps/cluster_simulator/cluster_simulator.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/deps/cluster_simulator/cluster_simulator.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/deps/cluster_simulator/cluster_simulator.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/deps/cluster_simulator/cluster_simulator.cpp
📚 Learning: 2026-02-13T05:55:42.693Z
Learnt from: mevishalr
Repo: sysown/proxysql PR: 5364
File: lib/MySQL_Logger.cpp:1211-1232
Timestamp: 2026-02-13T05:55:42.693Z
Learning: In ProxySQL, the MySQL_Logger and PgSQL_Logger destructors run after all worker threads have been joined during shutdown. The sequence in src/main.cpp is: (1) join all worker threads, (2) call ProxySQL_Main_shutdown_all_modules() which deletes the loggers. Therefore, there is no concurrent thread access during logger destruction, and lock ordering in the destructors cannot cause deadlocks.

Applied to files:

  • test/deps/cluster_simulator/cluster_simulator.cpp
🪛 Cppcheck (2.20.0)
test/deps/cluster_simulator/cluster_simulator.cpp

[warning] 329-329: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 384-384: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[error] 326-326: If memory allocation fails

(nullPointerArithmeticOutOfMemory)

🔇 Additional comments (16)
lib/Base_HostGroups_Manager.cpp (1)

1955-1957: Looks correct.

Adding autopurge_missing_checks keeps dump_table_mysql() aligned with the 15-column INSERT/bind sequence used downstream, so the Aurora hostgroup projection stays consistent. As shown in lib/ProxySQL_Admin.cpp:7676-7702, this column order matters.

lib/ProxySQL_Cluster.cpp (1)

2373-2397: Autopurge column integration is consistent and correct.

autopurge_missing_checks is wired in at the correct Aurora column position, and the row indexing/format arguments were updated coherently (row[13] for autopurge, row[14] for comment).

lib/ProxySQL_Admin.cpp (1)

7676-7678: Column/bind alignment for Aurora hostgroups looks correct.

The new autopurge_missing_checks column is inserted and bound in the correct position, and comment is correctly shifted to ?15. This matches the expected 15-column layout.

Also applies to: 7701-7702

include/ProxySQL_Cluster.hpp (1)

73-73: Looks aligned with the schema update.

Including autopurge_missing_checks in the Aurora projection keeps the cluster checksum/query shape in sync with the new table definition.

lib/ProxySQL_Admin_Disk_Upgrade.cpp (1)

486-505: Upgrade path looks correct.

The new V2_0_9 → V2_0_10 migration preserves the existing Aurora fields and lets autopurge_missing_checks take its default for legacy rows.

include/MySQL_Monitor.hpp (1)

553-553: Good public hook for the new monitor action.

The declaration is consistent with the Aurora autopurge flow added elsewhere in this PR.

include/Base_HostGroups_Manager.h (2)

85-87: Schema extension looks good.

autopurge_missing_checks is added with the expected constraint and default.


413-413: Ctor/update wiring looks consistent.

Propagating the new field through AWS_Aurora_Info keeps the in-memory model aligned with the persisted schema.

Also applies to: 420-421

include/ProxySQL_Admin_Tables_Definitions.h (1)

231-237: Schema bump looks consistent.

Adding autopurge_missing_checks to both the admin and runtime Aurora table definitions, then remapping the default macro to V2_0_10, is internally consistent with the versioned schema pattern here.

include/MySQL_HostGroups_Manager.h (1)

348-356: All AWS_Aurora_Info call sites have been properly updated.

The new autopurge_missing_checks parameter in the constructor and update() method has been wired through all invocation points in lib/MySQL_HostGroups_Manager.cpp (lines 6234, 6239), database queries, schema definitions, and Monitor code. No stale call sites remain.

lib/MySQL_HostGroups_Manager.cpp (4)

2247-2249: Aurora dump stays in sync with the schema.

dump_table_mysql() now exposes autopurge_missing_checks alongside the other Aurora columns, so the exported shape matches the updated table layout.


5795-5915: AuroraInfo now carries the new purge threshold end-to-end.

The constructor/update pair persists autopurge_missing_checks and marks the object dirty when it changes, which keeps the in-memory Aurora state aligned with the stored row.


6179-6241: The Aurora table write path matches the new column order.

The prepared INSERT, row parsing, and AWS_Aurora_Info construction all use the same autopurge_missing_checks position, so I don’t see a column-shift bug here.


6809-6810: Good: the monitor checksum now includes the purge threshold.

Adding autopurge_missing_checks to the monitor-facing Aurora query ensures that threshold changes participate in the resultset checksum and refresh flow.

lib/MySQL_Monitor.cpp (2)

6329-6331: Good safeguard on autopurge trigger conditions.

Line 6329 correctly gates autopurge on enabled threshold + successful query + non-empty host status, which helps avoid purging on transient fetch failures.


6587-6608: Lock/reentrancy analysis is not required here—this code is safe as written.

remove_server_in_hg() does not re-acquire the write lock; it only marks the server OFFLINE_HARD, drops free connections, and deletes the sqlite record. The in-memory mysrvs container is not modified. Index-based iteration (whgc->mysrvs->idx(j)) is inherently safe during element status changes, and the continue check for MYSQL_SERVER_STATUS_OFFLINE_HARD ensures proper handling. No deadlock or element-skipping risk exists.

Comment on lines 3743 to 3750
int a = mysql_thread___shun_recovery_time_sec;
int b = mysql_thread___monitor_ping_interval;

// Issue #5546: Add safety margin to prevent race condition
// where server can be unshunned before next ping arrives.
// Using 2 * ping_interval provides buffer for scheduling delays.
int b = 2 * mysql_thread___monitor_ping_interval;

b = b/1000;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Round the shun grace period instead of truncating it.

b = b / 1000 floors the doubled ping interval to whole seconds, so sub-second or near-second ping intervals can lose the intended safety margin and reintroduce the race you’re trying to avoid.

🔧 Suggested fix
-						int a = mysql_thread___shun_recovery_time_sec;
+						int a = mysql_thread___shun_recovery_time_sec;
 						// Issue `#5546`: Add safety margin to prevent race condition
 						// where server can be unshunned before next ping arrives.
-						// Using 2 * ping_interval provides buffer for scheduling delays.
-						int b = 2 * mysql_thread___monitor_ping_interval;
-
-						b = b/1000;
-						if (b > a) {
-							t = t + (b - a);
+						// Using 2 * ping_interval provides buffer for scheduling delays.
+						const auto recovery_margin_sec =
+							(2LL * mysql_thread___monitor_ping_interval + 999) / 1000;
+						if (recovery_margin_sec > a) {
+							t = t + (recovery_margin_sec - a);
 						}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/MySQL_HostGroups_Manager.cpp` around lines 3743 - 3750, The calculation
for the shun grace period currently truncates sub-second values by doing integer
division on b (where b is 2 * mysql_thread___monitor_ping_interval), so change
the division to round up instead of floor: compute the seconds by dividing the
millisecond value with rounding up (e.g., use std::ceil on b/1000.0 or add 999
before integer divide) so mysql_thread___shun_recovery_time_sec gets the
intended safety margin and avoids losing sub-second intervals; update the code
around variables b, mysql_thread___monitor_ping_interval and
mysql_thread___shun_recovery_time_sec accordingly.

Comment thread lib/MySQL_Monitor.cpp
Comment on lines +6597 to +6600
size_t pos = server_id.rfind(domain_name);
if (pos != std::string::npos) {
server_id.erase(pos);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use suffix-only domain stripping before server-id comparison.

Line 6597 and Line 6625 use rfind(domain_name) without enforcing suffix match. If domain_name appears earlier in the hostname, it can truncate the wrong portion and falsely purge healthy servers.

Suggested fix
-			size_t pos = server_id.rfind(domain_name);
-			if (pos != std::string::npos) {
-				server_id.erase(pos);
-			}
+			if (!domain_name.empty() &&
+				server_id.size() > domain_name.size() &&
+				server_id.compare(server_id.size() - domain_name.size(), domain_name.size(), domain_name) == 0) {
+				server_id.erase(server_id.size() - domain_name.size());
+			}
...
-				size_t pos = server_id.rfind(domain_name);
-				if (pos != std::string::npos) {
-					server_id.erase(pos);
-				}
+				if (!domain_name.empty() &&
+					server_id.size() > domain_name.size() &&
+					server_id.compare(server_id.size() - domain_name.size(), domain_name.size(), domain_name) == 0) {
+					server_id.erase(server_id.size() - domain_name.size());
+				}

Also applies to: 6625-6628

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/MySQL_Monitor.cpp` around lines 6597 - 6600, The current use of
server_id.rfind(domain_name) can match domain_name anywhere in the hostname;
change the logic to only strip domain_name when it is a suffix: after finding
pos, verify pos != npos AND pos + domain_name.size() == server_id.size() (or use
an ends-with check) before calling server_id.erase(pos); apply the same
suffix-only check for the other occurrence handling the same variables at the
later block (the lines that currently use rfind(domain_name) around the second
occurrence).

Comment on lines +531 to +564
/**
* @brief Change the monitor variable values before server loading for READ_ONLY.
* @details Due to the non-convergent nature of READ_ONLY actions due to 'writer_is_also_reader', servers could
* not match the right 'exp_init_cluster_status' if the configuration value for 'writer_is_also_reader' changes
* after previous monitoring actions has been already taken.
*/
{
// prepare monitor variables
set_monitor_variables_err =
set_monitor_variables(proxysql_admin, rephostgrp_monitor_variables, monitor_variables);

// final LOAD for 'mysql' variables after configuration has been setup
load_to_run_err = mysql_query(proxysql_admin, "LOAD MYSQL VARIABLES TO RUNTIME");
if (load_to_run_err) {
const auto& query_err =
create_query_error(proxysql_admin, "LOAD MYSQL VARIABLES TO RUNTIME", __FILE__, __LINE__);
result = internal_error(query_err.second, __FILE__, __LINE__);
goto exit;
}
}

// final LOAD after all the configuration has been setup
load_to_run_err = mysql_query(proxysql_admin, "LOAD MYSQL SERVERS TO RUNTIME");
if (load_to_run_err) {
const auto& query_err =
create_query_error(proxysql_admin, "LOAD MYSQL SERVERS TO RUNTIME", __FILE__, __LINE__);
result = internal_error(query_err.second, __FILE__, __LINE__);
goto exit;
}

// give the variables some defaults if they are not present
monitor_variables = set_monitor_variables_defaults(
monitor_variables, monitor_defaults_variables
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Set READ_ONLY defaults before loading monitor variables.

When mysql_monitor_config omits monitor_writer_is_also_reader, this path pushes the pre-default vector into runtime and only applies the default locally afterward. That makes READ_ONLY simulations depend on whatever value a previous scenario left behind.

Suggested fix
 	{
+		// give the variables some defaults if they are not present
+		monitor_variables = set_monitor_variables_defaults(
+			monitor_variables, monitor_defaults_variables
+		);
+
 		// prepare monitor variables
 		set_monitor_variables_err =
 			set_monitor_variables(proxysql_admin, rephostgrp_monitor_variables, monitor_variables);
+		if (set_monitor_variables_err.first) {
+			result = internal_error(set_monitor_variables_err.second, __FILE__, __LINE__);
+			goto exit;
+		}
 
 		// final LOAD for 'mysql' variables after configuration has been setup
 		load_to_run_err = mysql_query(proxysql_admin, "LOAD MYSQL VARIABLES TO RUNTIME");
 		if (load_to_run_err) {
 			const auto& query_err =
@@
-	// give the variables some defaults if they are not present
-	monitor_variables = set_monitor_variables_defaults(
-		monitor_variables, monitor_defaults_variables
-	);
-
 	set_monitor_tos_err = set_readonly_monitor_check_times(proxysql_admin);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/deps/cluster_simulator/cluster_simulator.cpp` around lines 531 - 564,
The current flow pushes monitor_variables into runtime before applying defaults,
causing READ_ONLY to inherit prior state; move the defaulting step so
monitor_variables = set_monitor_variables_defaults(monitor_variables,
monitor_defaults_variables) is called before invoking
set_monitor_variables(proxysql_admin, rephostgrp_monitor_variables,
monitor_variables) and before the subsequent mysql_query(proxysql_admin, "LOAD
MYSQL VARIABLES TO RUNTIME") so the runtime load receives the defaulted vector;
adjust the code around set_monitor_variables, set_monitor_variables_defaults,
and the "LOAD MYSQL VARIABLES TO RUNTIME" call accordingly.

Comment on lines +657 to +675
result = {
EXIT_SUCCESS,
nlohmann::ordered_json {
{ "err_type", "none" },
{ "result",
nlohmann::ordered_json {
{ "cluster_type", "replication" },
{ "mysql_servers", j_test_definition.at("mysql_servers") },
{ "mysql_replication_hostgroups", j_test_definition.at("mysql_replication_hostgroups") },
{ "readonly_servers_init_state", j_test_definition.at("readonly_servers_init_state") },
{ "readonly_servers_new_state", j_test_definition.at("readonly_servers_new_state") },
{ "proxysql_init_state_checksum", init_state_checksum },
{ "proxysql_init_state", j_test_definition.at("proxysql_init_state") },
{ "proxysql_final_state_checksum", final_state_checksum },
{ "proxysql_final_state", j_final_cluster_status },
}
}
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Emit canonical cluster_type values in simulate mode.

get_test_type() only accepts READ_ONLY and REPLICATION_LAG, but these simulate-mode payloads currently emit replication and replicationlag. The generated payloads for those families cannot be fed back into this same simulator.

Suggested fix
-							{ "cluster_type", "replication" },
+							{ "cluster_type", "READ_ONLY" },
-							{ "cluster_type", "replicationlag" },
+							{ "cluster_type", "REPLICATION_LAG" },

Also applies to: 1438-1455

Comment on lines +936 to +950
// prepare monitor variables
set_monitor_variables_err =
set_monitor_variables(
proxysql_admin, grouprep_monitor_variables, monitor_variables
);

// final LOAD for 'mysql' variables after configuration has been setup
load_to_run_err = mysql_query(proxysql_admin, "LOAD MYSQL VARIABLES TO RUNTIME");
if (load_to_run_err) {
const auto& query_err =
create_query_error(
proxysql_admin, "LOAD MYSQL VARIABLES TO RUNTIME", __FILE__, __LINE__
);
result = internal_error(query_err.second, __FILE__, __LINE__);
goto exit;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Stop if setting monitor variables fails.

These branches store set_monitor_variables_err and then ignore it. If the update fails, the simulator keeps running with stale runtime variables, which can turn an invalid setup into a false-positive test result.

Suggested fix
 	set_monitor_variables_err =
 		set_monitor_variables(
 			proxysql_admin, grouprep_monitor_variables, monitor_variables
 		);
+	if (set_monitor_variables_err.first) {
+		result = internal_error(set_monitor_variables_err.second, __FILE__, __LINE__);
+		goto exit;
+	}
 
 	// final LOAD for 'mysql' variables after configuration has been setup
 	load_to_run_err = mysql_query(proxysql_admin, "LOAD MYSQL VARIABLES TO RUNTIME");

Apply the same check in the REPLICATION_LAG and AURORA paths.

Also applies to: 1321-1335, 1700-1714

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/deps/cluster_simulator/cluster_simulator.cpp` around lines 936 - 950,
The call to set_monitor_variables (its return stored in
set_monitor_variables_err) is currently ignored—add an error check after each
set_monitor_variables invocation (including the REPLICATION_LAG and AURORA code
paths and the other occurrences around lines referenced) that mirrors the
existing mysql_query error handling: on non-zero set_monitor_variables_err
create a query_error (or appropriate error using
create_query_error/create_monitor_error pattern used elsewhere), convert to
internal_error/result, and goto exit to stop the simulator; update the branches
that call set_monitor_variables so they abort on failure rather than continuing
with stale runtime variables.

Comment on lines +52 to +53
"__comment__": "Three Nodes Aurora Cluster - Autopurge disabled (default)",
"__details__": "Without autopurge_missing_checks, missing reader stays in mysql_servers (just OFFLINE). Verifies autopurge=0 keeps backwards-compatible behavior.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Align the scenario description with the asserted final state.

Line 53 says the missing reader stays "just OFFLINE", but Lines 95-98 expect that same entry to remain ONLINE. Please make the prose match the fixture, otherwise this case is hard to trust when debugging autopurge_missing_checks=0.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@test/deps/cluster_simulator/tests/aurora_tests_payloads/three_nodes_cluster-autopurge_missing_replica.json`
around lines 52 - 53, Update the scenario prose to match the fixture's expected
final state: change the "__details__" (or "__comment__") text that currently
says the missing reader stays "just OFFLINE" to state that the missing reader
remains "ONLINE" (or otherwise indicate it remains present but marked ONLINE) so
the description aligns with the assertions around the expected entry at lines
95-98; edit the "__details__" string in the JSON fixture accordingly.

@wazir-ahmed wazir-ahmed changed the title CI: Add cluster simulator TAP groups aurora: Fix monitor race condition and implement autopurge Apr 29, 2026
@wazir-ahmed wazir-ahmed changed the title aurora: Fix monitor race condition and implement autopurge aurora: Fix monitor race condition & implement autopurge Apr 29, 2026
@wazir-ahmed wazir-ahmed changed the title aurora: Fix monitor race condition & implement autopurge aurora: Implement autopurge & fix monitor race condition Apr 29, 2026
Signed-off-by: Wazir Ahmed <wazir@proxysql.com>
@renecannao

Copy link
Copy Markdown
Contributor

I reviewed the PR locally on review-pr-5660. I have two concerns that I think should be addressed before merging.

1. Aurora autopurge updates mysql_servers without refreshing the Aurora monitor resultset

The new autopurge path removes servers here:

  • lib/MySQL_Monitor.cpp, aws_aurora_autopurge_servers()
  • calls to MyHGM->remove_server_in_hg(...) in both the writer and reader hostgroup loops

The concern is that this mutates the hostgroup manager / internal mysql_servers table, but it does not refresh AWS_Aurora_Hosts_resultset or AWS_Aurora_Hosts_resultset_checksum.

The Aurora monitor thread builds its local host array once at startup from GloMyMon->AWS_Aurora_Hosts_resultset:

host_def_t *hpa = (host_def_t *)malloc(sizeof(host_def_t)*num_hosts);
...
hpa[cur_host_idx].host = strdup(r->fields[2]);
hpa[cur_host_idx].port = atoi(r->fields[3]);

After that, the thread only exits/rebuilds when the cached checksum changes:

current_raw_checksum = GloMyMon->AWS_Aurora_Hosts_resultset_checksum;
...
if (current_raw_checksum != initial_raw_checksum) {
    exit_now = true;
    break;
}

Existing Aurora topology-changing paths do refresh this monitor resultset after they modify servers. For example, the writer/reader update paths call update_aws_aurora_hosts_monitor_resultset(...) after changing mysql_servers / runtime state. The new autopurge path does not do an equivalent refresh.

That means after autopurge removes a backend, the current Aurora monitor thread can keep using the stale hpa array and continue probing a server that was just purged. It also does not force the normal checksum-based restart/reload path, so the monitor may not rebuild its host list after topology shrink.

Suggested fix: after autopurge removes at least one server, refresh the Aurora monitor resultset/checksum under the same locking discipline used by the existing Aurora update paths, or otherwise force the monitor thread to reload. The important part is that the mutation performed by remove_server_in_hg() must be reflected in AWS_Aurora_Hosts_resultset_checksum, otherwise the thread-local host cache can remain stale.

2. Simulator verification ignores the backend port

In test/deps/cluster_simulator/lib/common_utils.cpp, matching_server_status() has this comparison:

bool same_hid_hostname =
    std::get<0>(srv_st1) == std::get<0>(srv_st2) &&
    std::get<1>(srv_st1) == std::get<1>(srv_st2) &&
    std::get<2>(srv_st2) == std::get<2>(srv_st2);

The last condition compares srv_st2 to itself, so it is always true. This makes the simulator consider two server states matching when hostgroup and hostname match, even if the port is different.

That weakens the new simulator TAP coverage substantially. A regression where ProxySQL adds, keeps, moves, or verifies a backend on the wrong port can pass unnoticed as long as hostgroup and hostname match.

This should be:

std::get<2>(srv_st1) == std::get<2>(srv_st2)

or, preferably, rewritten with named accessors / local variables so the expected-vs-actual comparison is easier to audit.

Separately, git diff --check origin/v3.0...HEAD currently reports trailing whitespace / blank EOF issues in added simulator files and JSON payloads. That is less important than the two behavioral concerns above, but it may matter if CI enforces whitespace checks.

Signed-off-by: Wazir Ahmed <wazir@proxysql.com>
Signed-off-by: Wazir Ahmed <wazir@proxysql.com>
Signed-off-by: Wazir Ahmed <wazir@proxysql.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
test/deps/cluster_simulator/lib/common_utils.cpp (3)

62-76: 💤 Low value

Remove dead code: duplicate mysql_fetch_fields call and unused lengths variable.

Line 62 calls mysql_fetch_fields() but discards the result (called again on line 64). The lengths variable (lines 75-76) is fetched but never used.

♻️ Proposed cleanup
 void parse_result_to_json(MYSQL_RES *result, ordered_json& j) {
 	if(!result) { return; }
 
 	// Get the fields
 	std::vector<std::string> field_names {};
 
-	mysql_fetch_fields(result);
 	int num_fields = mysql_num_fields(result);
 	MYSQL_FIELD* fields = mysql_fetch_fields(result);
 
 	for(int i = 0; i < num_fields; i++) {
 		field_names.push_back(fields[i].name);
 	}
 
 	// Get each of the row values
 	MYSQL_ROW row = nullptr;
 
 	while ((row = mysql_fetch_row(result))) {
 		ordered_json j_row {};
-		unsigned long *lengths;
-		lengths = mysql_fetch_lengths(result);
 
 		for(int i = 0; i < num_fields; i++) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/deps/cluster_simulator/lib/common_utils.cpp` around lines 62 - 76,
Remove the dead code by deleting the redundant mysql_fetch_fields() call (the
first call that discards its return) and stop fetching the unused lengths
variable: remove the declaration "unsigned long *lengths;" and the call to
mysql_fetch_lengths(result). Keep the single useful call to MYSQL_FIELD* fields
= mysql_fetch_fields(result) and ensure the loop that populates field_names and
the row-processing while loop (which uses MYSQL_ROW row and
mysql_fetch_row(result)) remain unchanged.

412-421: 💤 Low value

Sorting key omits port, causing undefined order for multi-port setups.

The comparator builds srv_st_id from hostgroup_id + hostname but excludes port. If multiple servers share the same hostgroup and hostname with different ports, sort order is undefined, potentially causing std::equal to compare mismatched pairs.

♻️ Proposed fix (also apply to other comparators at lines 423-432, 451-460, 462-471)
 	std::sort(
 		c_exp_status.begin(),
 		c_exp_status.end(),
 		[] (const server_status& srv_st1, const server_status& srv_st2) -> bool {
-			const std::string srv_st1_id { std::to_string(std::get<0>(srv_st1)) + std::get<1>(srv_st1) };
-			const std::string srv_st2_id { std::to_string(std::get<0>(srv_st2)) + std::get<1>(srv_st2) };
+			const std::string srv_st1_id { std::to_string(std::get<0>(srv_st1)) + std::get<1>(srv_st1) + std::to_string(std::get<2>(srv_st1)) };
+			const std::string srv_st2_id { std::to_string(std::get<0>(srv_st2)) + std::get<1>(srv_st2) + std::to_string(std::get<2>(srv_st2)) };
 
 			return srv_st1_id > srv_st2_id;
 		}
 	);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/deps/cluster_simulator/lib/common_utils.cpp` around lines 412 - 421, The
comparator used in the std::sort of c_exp_status builds srv_st_id from
std::to_string(std::get<0>(srv_st)) + std::get<1>(srv_st) and omits the port,
which makes ordering undefined for servers that share hostgroup_id and hostname
but differ by port; update the comparator lambda (and the other similar
comparator lambdas in this file that build srv_st_id) to include the port (e.g.,
append ":" + std::to_string(std::get<2>(srv_st)) or otherwise incorporate
std::get<2>()) into the key before comparing so sorting is deterministic and
std::equal compares matching server tuples.

1365-1375: 💤 Low value

Avoid reserved identifier prefix __ in variable names.

Identifiers starting with double underscore are reserved by the C++ standard for any use. While unlikely to cause issues in practice, this is technically undefined behavior.

♻️ Proposed fix
 std::string get_fmt_time() {
-	time_t __timer;
-	char __buffer[30];
+	time_t timer;
+	char buffer[30];
 
-	struct tm __tm_info {};
-	time(&__timer);
-	localtime_r(&__timer, &__tm_info);
-	strftime(__buffer, 25, "%Y-%m-%d %H:%M:%S", &__tm_info);
+	struct tm tm_info {};
+	time(&timer);
+	localtime_r(&timer, &tm_info);
+	strftime(buffer, 25, "%Y-%m-%d %H:%M:%S", &tm_info);
 
-	return std::string(__buffer);
+	return std::string(buffer);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/deps/cluster_simulator/lib/common_utils.cpp` around lines 1365 - 1375,
The function get_fmt_time uses reserved identifiers (__timer, __buffer,
__tm_info); rename these to non-reserved names (e.g., timer, buffer, tm_info)
throughout the function and keep existing logic (time(), localtime_r, strftime)
intact; update usages in get_fmt_time to reference the new names and ensure
buffer size/strftime call remains consistent with the char array length.
🤖 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/deps/cluster_simulator/lib/common_utils.cpp`:
- Around line 874-879: The code calls mysql_store_result(proxysql_admin) to
populate MYSQL_RES* my_servers_res and passes it to parse_result_to_json but
never frees it; add a null-check for my_servers_res and call
mysql_free_result(my_servers_res) after parse_result_to_json to avoid the memory
leak (similar to the pattern used in get_current_cluster_status); reference the
symbols mysql_store_result, my_servers_res, parse_result_to_json and
mysql_free_result when making the change.

---

Nitpick comments:
In `@test/deps/cluster_simulator/lib/common_utils.cpp`:
- Around line 62-76: Remove the dead code by deleting the redundant
mysql_fetch_fields() call (the first call that discards its return) and stop
fetching the unused lengths variable: remove the declaration "unsigned long
*lengths;" and the call to mysql_fetch_lengths(result). Keep the single useful
call to MYSQL_FIELD* fields = mysql_fetch_fields(result) and ensure the loop
that populates field_names and the row-processing while loop (which uses
MYSQL_ROW row and mysql_fetch_row(result)) remain unchanged.
- Around line 412-421: The comparator used in the std::sort of c_exp_status
builds srv_st_id from std::to_string(std::get<0>(srv_st)) + std::get<1>(srv_st)
and omits the port, which makes ordering undefined for servers that share
hostgroup_id and hostname but differ by port; update the comparator lambda (and
the other similar comparator lambdas in this file that build srv_st_id) to
include the port (e.g., append ":" + std::to_string(std::get<2>(srv_st)) or
otherwise incorporate std::get<2>()) into the key before comparing so sorting is
deterministic and std::equal compares matching server tuples.
- Around line 1365-1375: The function get_fmt_time uses reserved identifiers
(__timer, __buffer, __tm_info); rename these to non-reserved names (e.g., timer,
buffer, tm_info) throughout the function and keep existing logic (time(),
localtime_r, strftime) intact; update usages in get_fmt_time to reference the
new names and ensure buffer size/strftime call remains consistent with the char
array length.
🪄 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: 78b12988-6431-415a-adaf-ae65f8742981

📥 Commits

Reviewing files that changed from the base of the PR and between 4742050 and c847667.

📒 Files selected for processing (4)
  • lib/MySQL_Monitor.cpp
  • test/deps/cluster_simulator/lib/common_utils.cpp
  • test/tap/groups/groups.json
  • test/tap/tests/reg_test_5546-shun_recovery-t.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/MySQL_Monitor.cpp
📜 Review details
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{cpp,h,hpp}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{cpp,h,hpp}: Use PascalCase with protocol prefixes (MySQL_, PgSQL_, ProxySQL_) for class names
Use snake_case for member variables
Use UPPER_SNAKE_CASE for constants and macros
Require C++17 standard for all code
Use conditional compilation via #ifdef PROXYSQL31, #ifdef PROXYSQL40, #ifdef PROXYSQLFFTO, #ifdef PROXYSQLTSDB, #ifdef PROXYSQLCLICKHOUSE for feature-gated code
Use RAII for resource management throughout the codebase
Use jemalloc for memory allocation
Use pthread mutexes for synchronization and std::atomic<> for counters

Files:

  • test/deps/cluster_simulator/lib/common_utils.cpp
🔇 Additional comments (2)
test/deps/cluster_simulator/lib/common_utils.cpp (2)

941-957: Critical: extract_cluster_status still has inverted presence check and wrong expected type.

The bugs flagged in the previous review are still present:

  1. Line 943 checks for json::value_t::object, but the payload is an array (confirmed by is_array() check on line 960).
  2. Line 946 returns an error when the field IS present (if (has_proxysql_init_state)), which is inverted logic.

355-391: LGTM! Port comparison bug has been fixed.

The comparison at line 360 now correctly compares std::get<2>(srv_st1) == std::get<2>(srv_st2), ensuring the port field is properly validated between the two server statuses.

Comment on lines +874 to +879
if (q_res == 0) {
ordered_json j_servers {};
j_servers["mysql_servers"] = {};

MYSQL_RES* my_servers_res = mysql_store_result(proxysql_admin);
parse_result_to_json(my_servers_res, j_servers["mysql_servers"]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Memory leak: my_servers_res is never freed.

mysql_store_result() allocates a result set that must be freed with mysql_free_result(). Compare with get_current_cluster_status (line 1048) which correctly frees its result.

🐛 Proposed fix
 	if (q_res == 0) {
 		ordered_json j_servers {};
 		j_servers["mysql_servers"] = {};
 
 		MYSQL_RES* my_servers_res = mysql_store_result(proxysql_admin);
 		parse_result_to_json(my_servers_res, j_servers["mysql_servers"]);
+		mysql_free_result(my_servers_res);
 
 		// convert the fields into the proper types
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/deps/cluster_simulator/lib/common_utils.cpp` around lines 874 - 879, The
code calls mysql_store_result(proxysql_admin) to populate MYSQL_RES*
my_servers_res and passes it to parse_result_to_json but never frees it; add a
null-check for my_servers_res and call mysql_free_result(my_servers_res) after
parse_result_to_json to avoid the memory leak (similar to the pattern used in
get_current_cluster_status); reference the symbols mysql_store_result,
my_servers_res, parse_result_to_json and mysql_free_result when making the
change.

- `check_table_structure`, `build_table` and `check_and_build_table`
  never mutate their string arguments, so widen the parameter types
  from `char *` to `const char *`.
- Drop the matching C-style casts at every call site that stripped
  const from string literals.

Signed-off-by: Wazir Ahmed <wazir@proxysql.com>
Signed-off-by: Wazir Ahmed <wazir@proxysql.com>
- Drop placeholder `// TODO: Improve` comments in catch blocks that
  carry no actionable information.
- Change some of the `TODO` markers to `Note`.
- Replace empty `@brief TODO` Doxygen stubs with proper doxygen comments.

Signed-off-by: Wazir Ahmed <wazir@proxysql.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
lib/ProxySQL_Statistics.cpp (1)

1919-1939: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

DNS resolution blocks the probe timeout.

getaddrinfo() is a blocking call that runs before the non-blocking connect/poll path. The 1s poll timeout only covers the TCP handshake attempt, not DNS resolution time. A slow resolver will stall the entire tsdb_monitor_loop batch beyond the configured monitor interval.

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

In `@lib/ProxySQL_Statistics.cpp` around lines 1919 - 1939, The blocking
getaddrinfo call before the non-blocking connect can make the whole probe exceed
the monitor interval; wrap the blocking DNS resolution in an asynchronous call
with a bounded timeout (e.g., use std::async or a separate thread/future and
wait_for) instead of calling getaddrinfo directly in the probe path, so if the
resolution does not complete within the configured probe timeout you skip/fail
the probe early; locate the getaddrinfo(...) invocation in
ProxySQL_Statistics.cpp (the res variable and subsequent loop over ai) and
replace it with an async-resolve that returns success/failure within the
timeout, ensure you still call freeaddrinfo(res) on success and handle the case
where resolution timed out by not attempting connect and recording a timed-out
probe for tsdb_monitor_loop.
lib/sqlite3db.cpp (1)

549-555: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Initialize statement to nullptr in SQLite3DB::check_table_structure.

At lines 549–555, statement is declared without initialization, then sqlite3_finalize() is called on it if sqlite3_prepare_v2() fails. Since sqlite3_prepare_v2() does not write to the output parameter on failure, the pointer remains uninitialized. Calling sqlite3_finalize() with an uninitialized pointer causes undefined behavior (segfault or heap corruption). SQLite only guarantees safe behavior when finalize() receives either NULL (harmless no-op) or a valid prepared statement handle.

Initialize statement to nullptr at declaration so the finalize call is safe on the error path.

Suggested fix
-	sqlite3_stmt *statement;
+	sqlite3_stmt *statement = nullptr;
 	char *buff=(char *)calloc(1,l);
 	sprintf(buff, q1, table_name , table_def);
 	if((*proxy_sqlite3_prepare_v2)(db, buff, -1, &statement, 0) != SQLITE_OK) {
 	  proxy_debug(PROXY_DEBUG_SQLITE, 1, "SQLITE: Error on (*proxy_sqlite3_prepare_v2)() running query \"%s\" : %s\n", buff, (*proxy_sqlite3_errmsg)(db));
 	  (*proxy_sqlite3_finalize)(statement);
 	  free(buff);
 	  assert(0);
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/sqlite3db.cpp` around lines 549 - 555, The local variable `statement` in
SQLite3DB::check_table_structure is used in the error path without being
initialized; set `sqlite3_stmt *statement = nullptr;` at declaration so that
when (*proxy_sqlite3_prepare_v2)(...) fails the subsequent call to
(*proxy_sqlite3_finalize)(statement) is safe (finalize accepts NULL) and avoids
undefined behavior; update the declaration near the prepare call that uses
proxy_sqlite3_prepare_v2 and proxy_sqlite3_finalize accordingly.
♻️ Duplicate comments (9)
test/deps/cluster_simulator/lib/common_utils.cpp (2)

873-879: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Memory leak: my_servers_res is still not freed in get_current_mysql_servers.

mysql_store_result() allocates a result that must be released via mysql_free_result(). The analogous get_current_cluster_status (Line 1046) already does this; this path does not.

🐛 Proposed fix
 		MYSQL_RES* my_servers_res = mysql_store_result(proxysql_admin);
 		parse_result_to_json(my_servers_res, j_servers["mysql_servers"]);
+		mysql_free_result(my_servers_res);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/deps/cluster_simulator/lib/common_utils.cpp` around lines 873 - 879, In
get_current_mysql_servers, after calling MYSQL_RES* my_servers_res =
mysql_store_result(proxysql_admin) and passing it to
parse_result_to_json(j_servers["mysql_servers"]), ensure you call
mysql_free_result(my_servers_res) to release the allocated result (and guard
against nullptr before freeing if needed); this mirrors the cleanup done in
get_current_cluster_status and prevents the memory leak.

940-970: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: extract_cluster_status still has an inverted presence check and wrong expected type.

The previously flagged bugs remain in the current code:

  1. Line 942 asks check_present_and_type(..., json::value_t::object), but the payload is an array (confirmed by is_array() at Line 959 and the for-loop treating entries as objects).
  2. Line 945 branches on if (has_proxysql_init_state) and returns "Unable to find required field" — the function errors out exactly when the field IS present and falls through when it is missing.
🐛 Proposed fix
 	bool has_proxysql_init_state =
 		check_present_and_type(
-			galera_test_def, { json_key }, json::value_t::object
+			galera_test_def, { json_key }, json::value_t::array
 		);
 
-	if (has_proxysql_init_state) {
+	if (!has_proxysql_init_state) {
 		std::string t_err_msg {
 			"Invalid input. Unable to find required field '%s'"
 		};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/deps/cluster_simulator/lib/common_utils.cpp` around lines 940 - 970, In
extract_cluster_status change the presence/type check to expect an array and
invert the branch: call check_present_and_type(galera_test_def, { json_key },
json::value_t::array) to set has_proxysql_init_state, then if
(!has_proxysql_init_state) return the "Unable to find required field '%s'"
error; keep the subsequent ordered_json m_proxysql_init_state =
galera_test_def[json_key] and the is_array() guard (or remove redundant
is_array() after the corrected check) so the code treats m_proxysql_init_state
as an array as intended.
test/deps/cluster_simulator/lib/galera_utils.cpp (3)

140-140: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: comment %s placeholder is still unquoted in the INSERT template.

t_galera_hosgroup_insert ends with ..., %d, %d, %d, %s) while extract_galera_hostgroup_config only forces comment = "NULL" for null payload values and otherwise preserves the raw string. Any non-null comment substitutes as bare text, producing SQL like ..., 0, 0, my comment) — a syntax error. The grouprep template uses '%s'; this path should match.

🐛 Proposed fix
-		" %d, %d, %d, %d, %d, %d, %d, %d, %s"
+		" %d, %d, %d, %d, %d, %d, %d, %d, '%s'"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/deps/cluster_simulator/lib/galera_utils.cpp` at line 140, The INSERT
printf format in t_galera_hosgroup_insert ends with an unquoted %s which causes
raw comments to produce invalid SQL; update the format string to quote the
comment placeholder (use '%s' instead of %s) so it matches the grouprep path,
and ensure extract_galera_hostgroup_config still returns the literal "NULL"
(without quotes) for absent payloads while non-null comments are safely passed
into the quoted '%s' slot in t_galera_hosgroup_insert to avoid syntax errors;
adjust any surrounding formatting logic in t_galera_hosgroup_insert and keep
extract_galera_hostgroup_config behavior unchanged.

808-885: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: galera_update_state copy-paste bugs + identity loss persist.

Four slots are still being written from the wrong source variable, and result is still default-constructed (so hostgroup_id, hostname, port are returned as zeros/empty whenever this function is consulted):

  • Line 830: read_only slot ← st2_wsrep_local_state (should be st2_read_only).
  • Line 847: wsrep_desync slot ← st2_wsrep_local_recv_queue (should be st2_wsrep_desync).
  • Line 874: wsrep_cluster_status slot ← st2_wsrep_sst_donor_rejects_queries (should be st2_cluster_status).
  • Line 881: pxc_maint_mode slot ← st2_wsrep_sst_donor_rejects_queries (should be st2_pxc_maint_mode).
🐛 Proposed fix
-	galera_server_state result {};
+	galera_server_state result { st1 };
 ...
 	if (st2_read_only != -1 && st1_read_only != st2_read_only) {
-		std::get<4>(result) = st2_wsrep_local_state;
+		std::get<4>(result) = st2_read_only;
 	}
 ...
 	if (st2_wsrep_desync != -1 && st1_wsrep_desync != st2_wsrep_desync) {
-		std::get<6>(result) = st2_wsrep_local_recv_queue;
+		std::get<6>(result) = st2_wsrep_desync;
 	}
 ...
 	if (st2_cluster_status != "NULL" && st1_cluster_status != st2_cluster_status) {
-		std::get<9>(result) = st2_wsrep_sst_donor_rejects_queries;
+		std::get<9>(result) = st2_cluster_status;
 	}
 ...
 	if (st2_pxc_maint_mode != "NULL" && st1_pxc_maint_mode != st2_pxc_maint_mode) {
-		std::get<10>(result) = st2_wsrep_sst_donor_rejects_queries;
+		std::get<10>(result) = st2_pxc_maint_mode;
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/deps/cluster_simulator/lib/galera_utils.cpp` around lines 808 - 885,
galera_update_state currently returns a default-constructed result (losing
identity fields) and has multiple copy-paste errors writing the wrong source
variables into tuple slots; fix by initializing result from st1 for the
immutable identity fields (copy std::get<0/1/2>(st1) into
std::get<0/1/2>(result)), and correct the four wrong assignments inside
galera_update_state so that std::get<4>(result) is set from st2_read_only,
std::get<6>(result) is set from st2_wsrep_desync, std::get<9>(result) is set
from st2_cluster_status, and std::get<10>(result) is set from st2_pxc_maint_mode
(leave the existing conditional checks intact).

778-789: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: galera_state_members_diff still pushes st1_wsrep_sst_donor_rejects_queries (the old value).

Every other field in this function reports st2_...; only wsrep_sst_donor_rejects_queries reports the prior value, so its diff entry describes the wrong side of the change.

🐛 Proposed fix
 		result.push_back({
 			"wsrep_sst_donor_rejects_queries",
-			std::to_string(st1_wsrep_sst_donor_rejects_queries)
+			std::to_string(st2_wsrep_sst_donor_rejects_queries)
 		});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/deps/cluster_simulator/lib/galera_utils.cpp` around lines 778 - 789, In
galera_state_members_diff, the diff entry for wsrep_sst_donor_rejects_queries
incorrectly pushes st1_wsrep_sst_donor_rejects_queries (the old value) instead
of the new value; update the result.push_back call to use
st2_wsrep_sst_donor_rejects_queries so the diff reflects the new state. Locate
the block comparing st2_wsrep_sst_donor_rejects_queries and
st1_wsrep_sst_donor_rejects_queries and replace the value passed to
std::to_string in the result.push_back for "wsrep_sst_donor_rejects_queries"
from st1_wsrep_sst_donor_rejects_queries to st2_wsrep_sst_donor_rejects_queries.
test/deps/cluster_simulator/lib/replicationlag_utils.cpp (2)

441-464: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: replicationlag_update_state still loses identity and compares shared_ptr addresses.

Both previously flagged issues remain unresolved:

  1. result is default-constructed, so the returned tuple has empty hostname and port = 0. replicationlag_update_cluster_state then pushes these identity-less tuples into its result.
  2. Line 456 compares st1_replicationlag != st2_replicationlag — i.e., the raw shared_ptr addresses, not their pointees. Same defect in pointer-vs-value comparison.
🐛 Proposed fix
-	replicationlag_server_state result {};
+	replicationlag_server_state result { st1 };
 
 	const std::shared_ptr<int>& st1_replicationlag = std::get<2>(st1);
 	const std::shared_ptr<int>& st2_replicationlag = std::get<2>(st2);
 
 	if (st1_replicationlag != nullptr && st2_replicationlag != nullptr) {
-		if (st1_replicationlag != st2_replicationlag) {
+		if (*st1_replicationlag != *st2_replicationlag) {
 			std::get<2>(result) = st2_replicationlag;
 		}
 	} else {
 		std::get<2>(result) = st2_replicationlag;
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/deps/cluster_simulator/lib/replicationlag_utils.cpp` around lines 441 -
464, The function replicationlag_update_state currently returns a
default-constructed replicationlag_server_state (losing hostname/port identity)
and compares shared_ptr addresses instead of their pointees; fix it by
initializing result from st1 (copy hostname/port and existing fields) then
update only the replicationlag field: obtain st1_replicationlag and
st2_replicationlag (std::get<2> on st1/st2), and if both are non-null compare
*st1_replicationlag and *st2_replicationlag (value comparison) — if values
differ assign st2_replicationlag into std::get<2>(result); if one is null assign
st2_replicationlag into std::get<2>(result) as well; leave hostname/port from
st1 untouched (they are part of the identity).

197-208: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: post-query type conversion in get_current_replicationlag_servers_state is still broken.

The previously flagged issues are still present in the current code:

  • Line 198 calls atoi on hostname, replacing the string with an integer — downstream extract_replicationlag_servers_state expects a string.
  • Line 207 reads j_server["comment"], but the SELECT at Line 185 returns only hostname, port, seconds_behind_master. Bracket-access on a missing key inserts JSON null, then std::string{ null } throws.
  • my_servers_res from mysql_store_result is also never freed (compare with get_current_cluster_status in common_utils.cpp Line 1046).
🐛 Proposed fix
 		MYSQL_RES* my_servers_res = mysql_store_result(proxysql_admin);
 		parse_result_to_json(my_servers_res, j_servers["replicationlag_servers_init_state"]);
+		mysql_free_result(my_servers_res);
 
 		// convert the fields into the proper types
 		for (ordered_json& j_server : j_servers["replicationlag_servers_init_state"]) {
-			j_server["hostname"] = atoi(std::string {j_server["hostname"]}.c_str());
+			j_server["hostname"] = std::string { j_server["hostname"] };
 			j_server["port"] = atoi(std::string {j_server["port"]}.c_str());
 
 			const auto& seconds_behind_master = j_server.at("seconds_behind_master");
 			if (seconds_behind_master == nullptr || seconds_behind_master.is_null()) {
 				j_server["seconds_behind_master"] = nullptr;	
 			} else {
 				j_server["seconds_behind_master"] = atoi(std::string {j_server["seconds_behind_master"]}.c_str());
 			}
-			j_server["comment"] = atoi(std::string {j_server["comment"]}.c_str());
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/deps/cluster_simulator/lib/replicationlag_utils.cpp` around lines 197 -
208, In get_current_replicationlag_servers_state: stop converting hostname to an
integer (remove the atoi on j_server["hostname"]) so
extract_replicationlag_servers_state continues to receive a string; guard access
to "comment" (do not blindly read j_server["comment"] with std::string if the
key is missing—only convert if key exists and is not null, otherwise leave
absent/null); keep the existing handling for seconds_behind_master but avoid
constructing std::string from a null JSON value (check is_null() before
converting); and ensure the MYSQL_RES* my_servers_res returned by
mysql_store_result is freed with mysql_free_result(my_servers_res) before
returning—these changes touch get_current_replicationlag_servers_state, the
local j_server handling loop, and the my_servers_res cleanup to prevent crashes
and leaks.
test/deps/cluster_simulator/lib/aurora_utils.cpp (2)

273-281: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: session_id = true; still assigns a bool to std::string.

Line 274 still assigns the bool literal true to string session_id. Through bool → int → char this compiles silently and stores the one-byte string "\x01", which then participates in identity diffs and the REPLICA_HOST_STATUS INSERT. The downstream aurora_update_state / aurora_state_members_diff use "" as the "ignore" sentinel for SESSION_ID (Lines 505, 568), so the intent here is almost certainly "".

🐛 Proposed fix
 					if (m_session_id == nullptr) {
-						session_id = true;
+						session_id = "";
 					} else {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/deps/cluster_simulator/lib/aurora_utils.cpp` around lines 273 - 281, The
code incorrectly assigns the bool literal true to the std::string variable
session_id when m_session_id == nullptr, producing a one-byte "\x01" instead of
the intended empty-string sentinel; change the assignment in that branch to
session_id = "" (empty string) so session_id is a proper std::string sentinel
consistent with how SESSION_ID is checked in aurora_update_state and
aurora_state_members_diff, and ensure no other branches assign non-string types
to session_id (e.g., the try branch that reads server_state["SESSION_ID"]
remains unchanged).

107-150: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: j_comment is still never assigned — every row gets comment = 'NULL'.

j_comment is default-constructed as a JSON null on Line 108 and nothing ever writes to it before the j_comment == nullptr test at Line 142 — so the else branch (which would actually read j_aurora_hg.at("comment")) is dead. Every inserted row gets the string literal "NULL". Combined with the single-quoted '%s' formatting in t_aurora_hostgroup_insert (Line 57), an actual SQL NULL cannot be produced through this path.

🐛 Proposed fix
 		try {
 			...
 			new_reader_weight = j_aurora_hg.at("new_reader_weight");
+
+			if (j_aurora_hg.find("comment") != j_aurora_hg.end()) {
+				j_comment = j_aurora_hg.at("comment");
+			}
 			...
 		} catch (const std::exception& e) {
 			return { EXIT_FAILURE, e.what() };
 		}
 
-		if (j_comment == nullptr) {
+		if (j_comment.is_null()) {
 			comment = "NULL";
 		} else {
-			try {
-				comment = j_aurora_hg.at("comment");
-			} catch (const std::exception& e) {
-				return { EXIT_FAILURE, e.what() };
-			}
+			comment = j_comment.get<std::string>();
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/deps/cluster_simulator/lib/aurora_utils.cpp` around lines 107 - 150,
j_comment is never initialized from j_aurora_hg so the NULL-check always takes
the "NULL" branch; fix by reading the "comment" field from j_aurora_hg into
j_comment before the j_comment == nullptr test (use j_aurora_hg.find("comment")
to detect presence and assign either the value or json(nullptr)), and then set
the string variable comment only after that check; additionally ensure
t_aurora_hostgroup_insert (the SQL formatting that currently uses a
single-quoted '%s') can emit a true SQL NULL (remove the surrounding quotes or
switch to an explicit NULL token when comment is null) so real NULL values are
inserted.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/deps/cluster_simulator/lib/common_utils.cpp`:
- Around line 336-354: check_present_and_type currently always queries the root
json `j` for each `step` and never descends; change the traversal to start with
`json cur_j = j;` and for each `step` check `cur_j.contains(step)` then set
`cur_j = cur_j.at(step)` (not `j.at(step)`), and after consuming the final
element compare `type == cur_j.type()` and return that result; remove the bogus
address comparison `&step == &path.back()` and instead detect the final step by
index or by checking after advancing whether you've reached the last element;
return false on any missing step.

In `@test/deps/cluster_simulator/lib/galera_utils.cpp`:
- Around line 319-326: The cleanup DELETE uses "WHERE hostname NOT IN (...) AND
port NOT IN (...)" which only removes rows missing both values; change it so
rows whose (hostname,port) pair is not in the new set are removed. Update the
cleanup_query construction in galera_utils.cpp (the cleanup_query variable used
with mysql_query) to either: 1) build a tuple-based predicate "DELETE FROM
HOST_STATUS_GALERA WHERE (hostname,port) NOT IN ((h1,p1),(h2,p2),...)" matching
the srv_hostnames/srv_ports pairs, or 2) simplify to "DELETE FROM
HOST_STATUS_GALERA" and let the subsequent INSERTs repopulate; ensure the
replacement string correctly formats quotes/commas and is passed to mysql_query
exactly where query_error is checked.

In `@test/deps/cluster_simulator/lib/grouprep_utils.cpp`:
- Around line 478-484: The DELETE uses "hostname NOT IN (...) AND port NOT IN
(...)" which keeps rows that match any new port but incorrect hostname; instead
delete rows whose (hostname,port) pair is not among the supplied servers. In the
cleanup==1 branch (the cleanup_query construction in grouprep_utils.cpp), build
a composite-condition or tuple IN list and use "WHERE (hostname, port) NOT IN
((h1,p1),(h2,p2),...)" (or, if your SQL dialect doesn't support tuple IN,
generate explicit ORed mismatch checks per pair) so rows are removed unless
their exact (hostname,port) matches one of the provided server pairs.
- Around line 714-747: The function grouprep_update_state constructs result with
empty hostname/port so callers get "ghost" servers; fix by initializing result
from st1 (e.g., copy the full tuple from st1) then apply the conditional updates
for viable_candidate/read_only/transactions_behind from st2 so identity
(hostname/port) is preserved when grouprep_update_cluster_state pushes it. Also
fix the sentinel checks: viable_candidate and read_only are bools so tests
against -1 are invalid—either make those fields tri-state (std::optional or
enum) or change the update logic to compare st2 values against st1 (or use an
explicit "no-change" marker) so updates only occur when st2 intends a change.

---

Outside diff comments:
In `@lib/ProxySQL_Statistics.cpp`:
- Around line 1919-1939: The blocking getaddrinfo call before the non-blocking
connect can make the whole probe exceed the monitor interval; wrap the blocking
DNS resolution in an asynchronous call with a bounded timeout (e.g., use
std::async or a separate thread/future and wait_for) instead of calling
getaddrinfo directly in the probe path, so if the resolution does not complete
within the configured probe timeout you skip/fail the probe early; locate the
getaddrinfo(...) invocation in ProxySQL_Statistics.cpp (the res variable and
subsequent loop over ai) and replace it with an async-resolve that returns
success/failure within the timeout, ensure you still call freeaddrinfo(res) on
success and handle the case where resolution timed out by not attempting connect
and recording a timed-out probe for tsdb_monitor_loop.

In `@lib/sqlite3db.cpp`:
- Around line 549-555: The local variable `statement` in
SQLite3DB::check_table_structure is used in the error path without being
initialized; set `sqlite3_stmt *statement = nullptr;` at declaration so that
when (*proxy_sqlite3_prepare_v2)(...) fails the subsequent call to
(*proxy_sqlite3_finalize)(statement) is safe (finalize accepts NULL) and avoids
undefined behavior; update the declaration near the prepare call that uses
proxy_sqlite3_prepare_v2 and proxy_sqlite3_finalize accordingly.

---

Duplicate comments:
In `@test/deps/cluster_simulator/lib/aurora_utils.cpp`:
- Around line 273-281: The code incorrectly assigns the bool literal true to the
std::string variable session_id when m_session_id == nullptr, producing a
one-byte "\x01" instead of the intended empty-string sentinel; change the
assignment in that branch to session_id = "" (empty string) so session_id is a
proper std::string sentinel consistent with how SESSION_ID is checked in
aurora_update_state and aurora_state_members_diff, and ensure no other branches
assign non-string types to session_id (e.g., the try branch that reads
server_state["SESSION_ID"] remains unchanged).
- Around line 107-150: j_comment is never initialized from j_aurora_hg so the
NULL-check always takes the "NULL" branch; fix by reading the "comment" field
from j_aurora_hg into j_comment before the j_comment == nullptr test (use
j_aurora_hg.find("comment") to detect presence and assign either the value or
json(nullptr)), and then set the string variable comment only after that check;
additionally ensure t_aurora_hostgroup_insert (the SQL formatting that currently
uses a single-quoted '%s') can emit a true SQL NULL (remove the surrounding
quotes or switch to an explicit NULL token when comment is null) so real NULL
values are inserted.

In `@test/deps/cluster_simulator/lib/common_utils.cpp`:
- Around line 873-879: In get_current_mysql_servers, after calling MYSQL_RES*
my_servers_res = mysql_store_result(proxysql_admin) and passing it to
parse_result_to_json(j_servers["mysql_servers"]), ensure you call
mysql_free_result(my_servers_res) to release the allocated result (and guard
against nullptr before freeing if needed); this mirrors the cleanup done in
get_current_cluster_status and prevents the memory leak.
- Around line 940-970: In extract_cluster_status change the presence/type check
to expect an array and invert the branch: call
check_present_and_type(galera_test_def, { json_key }, json::value_t::array) to
set has_proxysql_init_state, then if (!has_proxysql_init_state) return the
"Unable to find required field '%s'" error; keep the subsequent ordered_json
m_proxysql_init_state = galera_test_def[json_key] and the is_array() guard (or
remove redundant is_array() after the corrected check) so the code treats
m_proxysql_init_state as an array as intended.

In `@test/deps/cluster_simulator/lib/galera_utils.cpp`:
- Line 140: The INSERT printf format in t_galera_hosgroup_insert ends with an
unquoted %s which causes raw comments to produce invalid SQL; update the format
string to quote the comment placeholder (use '%s' instead of %s) so it matches
the grouprep path, and ensure extract_galera_hostgroup_config still returns the
literal "NULL" (without quotes) for absent payloads while non-null comments are
safely passed into the quoted '%s' slot in t_galera_hosgroup_insert to avoid
syntax errors; adjust any surrounding formatting logic in
t_galera_hosgroup_insert and keep extract_galera_hostgroup_config behavior
unchanged.
- Around line 808-885: galera_update_state currently returns a
default-constructed result (losing identity fields) and has multiple copy-paste
errors writing the wrong source variables into tuple slots; fix by initializing
result from st1 for the immutable identity fields (copy std::get<0/1/2>(st1)
into std::get<0/1/2>(result)), and correct the four wrong assignments inside
galera_update_state so that std::get<4>(result) is set from st2_read_only,
std::get<6>(result) is set from st2_wsrep_desync, std::get<9>(result) is set
from st2_cluster_status, and std::get<10>(result) is set from st2_pxc_maint_mode
(leave the existing conditional checks intact).
- Around line 778-789: In galera_state_members_diff, the diff entry for
wsrep_sst_donor_rejects_queries incorrectly pushes
st1_wsrep_sst_donor_rejects_queries (the old value) instead of the new value;
update the result.push_back call to use st2_wsrep_sst_donor_rejects_queries so
the diff reflects the new state. Locate the block comparing
st2_wsrep_sst_donor_rejects_queries and st1_wsrep_sst_donor_rejects_queries and
replace the value passed to std::to_string in the result.push_back for
"wsrep_sst_donor_rejects_queries" from st1_wsrep_sst_donor_rejects_queries to
st2_wsrep_sst_donor_rejects_queries.

In `@test/deps/cluster_simulator/lib/replicationlag_utils.cpp`:
- Around line 441-464: The function replicationlag_update_state currently
returns a default-constructed replicationlag_server_state (losing hostname/port
identity) and compares shared_ptr addresses instead of their pointees; fix it by
initializing result from st1 (copy hostname/port and existing fields) then
update only the replicationlag field: obtain st1_replicationlag and
st2_replicationlag (std::get<2> on st1/st2), and if both are non-null compare
*st1_replicationlag and *st2_replicationlag (value comparison) — if values
differ assign st2_replicationlag into std::get<2>(result); if one is null assign
st2_replicationlag into std::get<2>(result) as well; leave hostname/port from
st1 untouched (they are part of the identity).
- Around line 197-208: In get_current_replicationlag_servers_state: stop
converting hostname to an integer (remove the atoi on j_server["hostname"]) so
extract_replicationlag_servers_state continues to receive a string; guard access
to "comment" (do not blindly read j_server["comment"] with std::string if the
key is missing—only convert if key exists and is not null, otherwise leave
absent/null); keep the existing handling for seconds_behind_master but avoid
constructing std::string from a null JSON value (check is_null() before
converting); and ensure the MYSQL_RES* my_servers_res returned by
mysql_store_result is freed with mysql_free_result(my_servers_res) before
returning—these changes touch get_current_replicationlag_servers_state, the
local j_server handling loop, and the my_servers_res cleanup to prevent crashes
and leaks.
🪄 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: 710ab782-2fb0-4c4b-b124-591fefc3bb7f

📥 Commits

Reviewing files that changed from the base of the PR and between eaa57d0 and 536fda1.

📒 Files selected for processing (21)
  • .gitignore
  • include/sqlite3db.h
  • lib/ProxySQL_Admin.cpp
  • lib/ProxySQL_Admin_Disk_Upgrade.cpp
  • lib/ProxySQL_Statistics.cpp
  • lib/sqlite3db.cpp
  • test/deps/cluster_simulator/cluster_simulator.cpp
  • test/deps/cluster_simulator/lib/aurora_utils.cpp
  • test/deps/cluster_simulator/lib/aurora_utils.h
  • test/deps/cluster_simulator/lib/common_utils.cpp
  • test/deps/cluster_simulator/lib/common_utils.h
  • test/deps/cluster_simulator/lib/galera_utils.cpp
  • test/deps/cluster_simulator/lib/galera_utils.h
  • test/deps/cluster_simulator/lib/grouprep_utils.cpp
  • test/deps/cluster_simulator/lib/grouprep_utils.h
  • test/deps/cluster_simulator/lib/readonly_utils.cpp
  • test/deps/cluster_simulator/lib/readonly_utils.h
  • test/deps/cluster_simulator/lib/replicationlag_utils.cpp
  • test/tap/groups/groups.json
  • test/tap/tests/sqlite3-t.cpp
  • test/tap/tests/unit/admin_disk_upgrade_unit-t.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/deps/cluster_simulator/lib/grouprep_utils.h
📜 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). (4)
  • GitHub Check: CI-builds / builds (debian12,-dbg)
  • GitHub Check: CI-builds / builds (ubuntu22,-tap)
  • GitHub Check: CI-builds / builds (ubuntu24,-tap-genai-gcov)
  • GitHub Check: run / trigger
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{cpp,h,hpp}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{cpp,h,hpp}: Class names must use PascalCase with protocol prefixes (MySQL_, PgSQL_, ProxySQL_)
Member variables must use snake_case naming convention
Constants and macros must use UPPER_SNAKE_CASE naming convention
Use conditional compilation via #ifdef PROXYSQL31, #ifdef PROXYSQL40, #ifdef PROXYSQLFFTO, #ifdef PROXYSQLTSDB, #ifdef PROXYSQLCLICKHOUSE for feature tier gating in core code

Files:

  • test/deps/cluster_simulator/lib/replicationlag_utils.cpp
  • lib/sqlite3db.cpp
  • include/sqlite3db.h
  • test/deps/cluster_simulator/lib/aurora_utils.cpp
  • test/deps/cluster_simulator/lib/common_utils.cpp
  • test/deps/cluster_simulator/cluster_simulator.cpp
  • test/deps/cluster_simulator/lib/galera_utils.cpp
  • test/deps/cluster_simulator/lib/common_utils.h
  • test/deps/cluster_simulator/lib/readonly_utils.h
  • test/deps/cluster_simulator/lib/readonly_utils.cpp
  • test/deps/cluster_simulator/lib/grouprep_utils.cpp
  • test/deps/cluster_simulator/lib/galera_utils.h
  • lib/ProxySQL_Admin.cpp
  • lib/ProxySQL_Admin_Disk_Upgrade.cpp
  • lib/ProxySQL_Statistics.cpp
  • test/deps/cluster_simulator/lib/aurora_utils.h
**/*.{h,hpp}

📄 CodeRabbit inference engine (CLAUDE.md)

Include guards in header files must use #ifndef __CLASS_*_H pattern

Files:

  • include/sqlite3db.h
  • test/deps/cluster_simulator/lib/common_utils.h
  • test/deps/cluster_simulator/lib/readonly_utils.h
  • test/deps/cluster_simulator/lib/galera_utils.h
  • test/deps/cluster_simulator/lib/aurora_utils.h
🧠 Learnings (1)
📚 Learning: 2026-03-26T16:38:58.553Z
Learnt from: yuji-hatakeyama
Repo: sysown/proxysql PR: 5548
File: lib/mysql_connection.cpp:1837-1843
Timestamp: 2026-03-26T16:38:58.553Z
Learning: In `lib/mysql_connection.cpp`, when reviewing `SHOW WARNINGS` handling, treat the digest source as an intentional design choice: both `update_warning_count_from_connection()` and the `add_eof()` call under `ASYNC_USE_RESULT_CONT` detect warnings using `myds->sess->CurrentQuery.QueryParserArgs.digest_text` (comment-stripped digest text). This is expected to fail/behave differently when `mysql-query_digests_keep_comment=1` (digest_text includes comments) or when `mysql-query_digests=0` (digest_text unavailable). Do not require a change unless the regression test coverage is expanded (noting `reg_test_5306-show_warnings_with_comment-t` explicitly excludes these configurations as an accepted limitation).

Applied to files:

  • .gitignore
🪛 Cppcheck (2.20.0)
test/deps/cluster_simulator/cluster_simulator.cpp

[warning] 329-329: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 384-384: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[error] 326-326: If memory allocation fails

(nullPointerArithmeticOutOfMemory)

lib/ProxySQL_Admin_Disk_Upgrade.cpp

[warning] 138-138: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)

🔇 Additional comments (9)
.gitignore (1)

85-85: LGTM! Proper ignore rule for the cluster simulator binary.

The addition correctly ignores the cluster_simulator executable built under test/deps/cluster_simulator/, preventing the test binary artifact from being committed to version control. The placement in the "#binary" section alongside other executable ignore rules is appropriate.

test/deps/cluster_simulator/cluster_simulator.cpp (3)

538-565: ⚡ Quick win

READ_ONLY: defaults still applied after LOAD MYSQL VARIABLES TO RUNTIME.

set_monitor_variables_defaults(...) at lines 562-565 still runs after the runtime load on lines 540-550, so missing keys (e.g. monitor_writer_is_also_reader) keep whatever the previous scenario left in runtime. Same fix as previously suggested: default first, then set_monitor_variables + LOAD MYSQL VARIABLES TO RUNTIME.


538-552: ⚡ Quick win

set_monitor_variables_err still silently dropped on all four paths.

In READ_ONLY (540-541), GROUP_REPLICATION (938-941), REPLICATION_LAG (1323-1326) and AWS_AURORA (1702-1705), the result of set_monitor_variables(...) is stored in set_monitor_variables_err and then the very next thing the code does is run LOAD MYSQL VARIABLES TO RUNTIME. If the SET commands themselves failed, the simulator proceeds with stale runtime values and can report a false-positive verdict.

Suggested fix (apply at each of the four sites)
 	set_monitor_variables_err =
 		set_monitor_variables(proxysql_admin, ..., monitor_variables);
+	if (set_monitor_variables_err.first) {
+		result = internal_error(set_monitor_variables_err.second, __FILE__, __LINE__);
+		goto exit;
+	}
 
 	load_to_run_err = mysql_query(proxysql_admin, "LOAD MYSQL VARIABLES TO RUNTIME");

Also applies to: 937-952, 1321-1337, 1700-1715


658-676: ⚡ Quick win

simulate mode still emits non-canonical cluster_type strings.

Line 664 emits "replication" and line 1445 emits "replicationlag", but get_test_type() (lines 1952-1968) only accepts "READ_ONLY" and "REPLICATION_LAG". Payloads regenerated by simulate mode for these two families cannot be replayed by the same simulator. The verbose branches (712, 1499) already emit the canonical values; the simulate branches should match.

Also applies to: 1438-1456

test/deps/cluster_simulator/lib/grouprep_utils.cpp (2)

169-173: ⚡ Quick win

SELECT still fetches 4 columns and references non-existent check_type.

mysql_group_replication_hostgroups doesn't have a check_type column (so the query errors against a real ProxySQL admin), and the loop at lines 184-200 reads 9 columns from each parsed row. This function is unusable as-is.

Fix
-		"SELECT writer_hostgroup, reader_hostgroup, check_type, comment"
-		" FROM mysql_group_replication_hostgroups"
+		"SELECT writer_hostgroup, backup_writer_hostgroup, reader_hostgroup,"
+		" offline_hostgroup, active, max_writers, writer_is_also_reader,"
+		" max_transactions_behind, comment FROM mysql_group_replication_hostgroups"

360-369: ⚡ Quick win

read_only = "nullptr" still assigns a string literal to a bool.

read_only is declared bool at line 307; the literal "nullptr" decays to a non-null const char* and converts to true, so the null branch silently forces read_only=true regardless of intent. Either drop the null-handling or change the tuple field to a tri-state. Same issue applies symmetrically to viable_candidate (also bool); it’s harmless today only because the null branch defaults to true anyway.

test/deps/cluster_simulator/lib/readonly_utils.cpp (2)

330-367: ⚡ Quick win

get_current_readonly_servers_state is still guaranteed to fail.

The three issues from the prior review are all still present:

  • Line 345 calls atoi(...) on the hostname field, replacing the string with an int.
  • Line 348 reads j_server["commetn"] (typo) and converts to int via atoi; even after fixing the typo, comment is not selected and is not part of valid_readonly_entries (lines 179-183), so extract_readonly_servers_state would reject it.
  • The SELECT at line 332 only fetches hostname, port, read_only — no comment column.
Suggested fix
-		for (ordered_json& j_server : j_servers["readonly_servers_init_state"]) {
-			j_server["hostname"] = atoi(std::string {j_server["hostname"]}.c_str());
-			j_server["port"] = atoi(std::string {j_server["port"]}.c_str());
-			j_server["read_only"] = atoi(std::string {j_server["read_only"]}.c_str());
-			j_server["comment"] = atoi(std::string {j_server["commetn"]}.c_str());
-		}
+		for (ordered_json& j_server : j_servers["readonly_servers_init_state"]) {
+			j_server["hostname"] = std::string { j_server["hostname"] };
+			j_server["port"] = atoi(std::string { j_server["port"] }.c_str());
+			j_server["read_only"] = atoi(std::string { j_server["read_only"] }.c_str());
+		}
+		mysql_free_result(my_servers_res);

609-628: ⚡ Quick win

readonly_update_state still loses hostname/port.

result is default-constructed at line 613 (empty hostname, port = 0, read_only = 0); only std::get<2>(result) is written conditionally. readonly_update_cluster_state (lines 657-660) then pushes this into the returned vector, so any caller that uses identity from the result will see ghost servers.

Suggested fix
 	readonly_server_state result {};
+	// hostname and port are part of the server identity — preserve them.
+	std::get<0>(result) = std::get<0>(st1);
+	std::get<1>(result) = std::get<1>(st1);
+	std::get<2>(result) = std::get<2>(st1);
 
 	int st1_read_only = std::get<2>(st1);
 	int st2_read_only = std::get<2>(st2);
lib/ProxySQL_Admin.cpp (1)

7514-7540: Aurora hostgroup INSERT/bind alignment looks correct.

The 15-column statement and bind mapping (autopurge_missing_checks at position 14, comment at 15) are consistent with the updated schema and sibling paths.

Comment on lines +336 to +354
bool check_present_and_type(const json& j, const std::vector<std::string>& path, const json::value_t& type) {
bool res = false;

json cur_j {};

for (const auto& step : path) {
if (j.contains(step)) {
cur_j = j.at(step);

if (&step == &path.back()) {
return type == cur_j.type();
}
} else {
break;
}
}

return res;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

check_present_and_type doesn't actually traverse nested paths.

Despite the contract documented in the header (and the multi-step path parameter), the loop checks j.contains(step) and reads j.at(step) for every step, never recursing into cur_j. For a path like {"a", "b"} it asks whether the root contains "b", not whether j["a"] does.

All current callers happen to use single-element paths, so the bug is latent — but anyone relying on the documented behavior in the future will silently get wrong results.

🐛 Proposed fix
 bool check_present_and_type(const json& j, const std::vector<std::string>& path, const json::value_t& type) {
-	bool res = false;
-
-	json cur_j {};
-
-	for (const auto& step : path) {
-		if (j.contains(step)) {
-			cur_j = j.at(step);
-
-			if (&step == &path.back()) {
-				return type == cur_j.type();
-			}
-		} else {
-			break;
-		}
-	}
-
-	return res;
+	const json* cur = &j;
+	for (const auto& step : path) {
+		if (!cur->is_object() || !cur->contains(step)) {
+			return false;
+		}
+		cur = &cur->at(step);
+	}
+	return cur->type() == type;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/deps/cluster_simulator/lib/common_utils.cpp` around lines 336 - 354,
check_present_and_type currently always queries the root json `j` for each
`step` and never descends; change the traversal to start with `json cur_j = j;`
and for each `step` check `cur_j.contains(step)` then set `cur_j =
cur_j.at(step)` (not `j.at(step)`), and after consuming the final element
compare `type == cur_j.type()` and return that result; remove the bogus address
comparison `&step == &path.back()` and instead detect the final step by index or
by checking after advancing whether you've reached the last element; return
false on any missing step.

Comment on lines +319 to +326
std::string cleanup_query {
"DELETE FROM HOST_STATUS_GALERA WHERE hostname NOT IN (" +
srv_hostnames + ") AND port NOT IN (" + srv_ports + ")"
};

// Cleanup the current servers from 'HOST_STATUS_GALERA' and insert the ones for testing the cluster
query_error = mysql_query(proxysql_sqlite, cleanup_query.c_str());
if (query_error) { return create_query_error(proxysql_sqlite, cleanup_query, __FILE__, __LINE__); }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Cleanup query: AND should not span hostname/port — stale rows survive when only one component matches.

DELETE FROM HOST_STATUS_GALERA WHERE hostname NOT IN (...) AND port NOT IN (...) only deletes rows where both the hostname and port are absent from the new set. Any row sharing a hostname with one new server and a port with a different new server is silently retained, even though its (hostname, port) pair is not in the new list.

Example: new = [(host_a, 100)], existing extra row (host_a, 200)host_a NOT IN ('host_a') is false, so the WHERE clause is false and the stale row remains.

The intent is "delete rows whose (hostname, port) is not in the new set". Either branch on the tuple, or just unconditionally DELETE FROM HOST_STATUS_GALERA and rely on the subsequent INSERTs.

🐛 Suggested fix
-		std::string cleanup_query {
-			"DELETE FROM HOST_STATUS_GALERA WHERE hostname NOT IN (" +
-				srv_hostnames + ") AND port NOT IN (" + srv_ports + ")"
-		};
+		// Delete every row whose (hostname, port) tuple is not part of the new set.
+		std::string cleanup_query { "DELETE FROM HOST_STATUS_GALERA" };
+		for (size_t i = 0; i < servers.size(); ++i) {
+			cleanup_query += i == 0 ? " WHERE NOT (" : " OR NOT (";
+			cleanup_query +=
+				"hostname = '" + std::get<1>(servers[i]) + "'"
+				" AND port = " + std::to_string(std::get<2>(servers[i])) + ")";
+		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/deps/cluster_simulator/lib/galera_utils.cpp` around lines 319 - 326, The
cleanup DELETE uses "WHERE hostname NOT IN (...) AND port NOT IN (...)" which
only removes rows missing both values; change it so rows whose (hostname,port)
pair is not in the new set are removed. Update the cleanup_query construction in
galera_utils.cpp (the cleanup_query variable used with mysql_query) to either:
1) build a tuple-based predicate "DELETE FROM HOST_STATUS_GALERA WHERE
(hostname,port) NOT IN ((h1,p1),(h2,p2),...)" matching the
srv_hostnames/srv_ports pairs, or 2) simplify to "DELETE FROM
HOST_STATUS_GALERA" and let the subsequent INSERTs repopulate; ensure the
replacement string correctly formats quotes/commas and is passed to mysql_query
exactly where query_error is checked.

Comment on lines +478 to +484

if (cleanup == 1) {
cleanup_query = "DELETE FROM GR_MEMBER_ROUTING_CANDIDATE_STATUS WHERE hostname NOT IN (" +
srv_hostnames + ") AND port NOT IN (" + srv_ports + ")";
} else {
cleanup_query = "DELETE FROM GR_MEMBER_ROUTING_CANDIDATE_STATUS";
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

cleanup == 1 DELETE keeps unrelated rows that share a port.

DELETE FROM GR_MEMBER_ROUTING_CANDIDATE_STATUS
 WHERE hostname NOT IN (...) AND port NOT IN (...)

The AND is too narrow: a stale row with a hostname that isn't in the new list will still be retained as long as its port matches any of the new ports (3306 in practice for every entry). The intent is "delete rows whose (hostname, port) pair isn't among the supplied servers".

Suggested fix
-			cleanup_query = "DELETE FROM GR_MEMBER_ROUTING_CANDIDATE_STATUS WHERE hostname NOT IN (" +
-				srv_hostnames + ") AND port NOT IN (" + srv_ports + ")";
+			// Build "(hostname,port) NOT IN (('h1',p1),('h2',p2), ...)"
+			std::string pairs;
+			for (const auto& server : servers) {
+				if (!pairs.empty()) pairs += ",";
+				pairs += "('" + std::get<0>(server) + "'," +
+					std::to_string(std::get<1>(server)) + ")";
+			}
+			cleanup_query = "DELETE FROM GR_MEMBER_ROUTING_CANDIDATE_STATUS"
+				" WHERE (hostname, port) NOT IN (" + pairs + ")";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/deps/cluster_simulator/lib/grouprep_utils.cpp` around lines 478 - 484,
The DELETE uses "hostname NOT IN (...) AND port NOT IN (...)" which keeps rows
that match any new port but incorrect hostname; instead delete rows whose
(hostname,port) pair is not among the supplied servers. In the cleanup==1 branch
(the cleanup_query construction in grouprep_utils.cpp), build a
composite-condition or tuple IN list and use "WHERE (hostname, port) NOT IN
((h1,p1),(h2,p2),...)" (or, if your SQL dialect doesn't support tuple IN,
generate explicit ORed mismatch checks per pair) so rows are removed unless
their exact (hostname,port) matches one of the provided server pairs.

Comment on lines +714 to +747
grouprep_server_state grouprep_update_state(
const grouprep_server_state& st1,
const grouprep_server_state& st2
) {
grouprep_server_state result {};

// hostname and port **can't** be changed,
// because the are part of the server 'id'. Only the
// other fields are allowed to change, otherwise, the
// verification step should have failed.

int st1_viable_candidate = std::get<2>(st1);
int st2_viable_candidate = std::get<2>(st2);

int st1_read_only = std::get<3>(st1);
int st2_read_only = std::get<3>(st2);

int st1_transactions_behind = std::get<4>(st1);
int st2_transactions_behind = std::get<4>(st2);

if (st2_viable_candidate != -1 && st1_viable_candidate != st2_viable_candidate) {
std::get<2>(result) = st2_viable_candidate;
}

if (st2_read_only != -1 && st1_read_only != st2_read_only) {
std::get<3>(result) = st2_read_only;
}

if (st2_transactions_behind != -1 && st1_transactions_behind != st2_transactions_behind) {
std::get<4>(result) = st2_transactions_behind;
}

return result;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

grouprep_update_state returns ghost servers (hostname/port discarded).

result is default-constructed at line 718, so hostname (idx 0) and port (idx 1) stay empty/0. Only conditional fields are written, and grouprep_update_cluster_state then pushes this back at line 779. The returned vector is unusable for any caller that relies on identity. Same shape of bug already flagged for readonly_update_state, but the grouprep variant is independent and equally broken.

Suggested fix
 	grouprep_server_state result {};
+	// hostname and port are part of the server identity — preserve them.
+	std::get<0>(result) = std::get<0>(st1);
+	std::get<1>(result) = std::get<1>(st1);
+	// carry forward existing values; only override fields that legitimately changed.
+	std::get<2>(result) = std::get<2>(st1);
+	std::get<3>(result) = std::get<3>(st1);
+	std::get<4>(result) = std::get<4>(st1);
+	std::get<5>(result) = std::get<5>(st1);

Note: viable_candidate/read_only are bool, so the != -1 sentinel guards (lines 734, 738) can never short-circuit; you may want to make those fields tri-state if you actually need a "no change" signal.

📝 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.

Suggested change
grouprep_server_state grouprep_update_state(
const grouprep_server_state& st1,
const grouprep_server_state& st2
) {
grouprep_server_state result {};
// hostname and port **can't** be changed,
// because the are part of the server 'id'. Only the
// other fields are allowed to change, otherwise, the
// verification step should have failed.
int st1_viable_candidate = std::get<2>(st1);
int st2_viable_candidate = std::get<2>(st2);
int st1_read_only = std::get<3>(st1);
int st2_read_only = std::get<3>(st2);
int st1_transactions_behind = std::get<4>(st1);
int st2_transactions_behind = std::get<4>(st2);
if (st2_viable_candidate != -1 && st1_viable_candidate != st2_viable_candidate) {
std::get<2>(result) = st2_viable_candidate;
}
if (st2_read_only != -1 && st1_read_only != st2_read_only) {
std::get<3>(result) = st2_read_only;
}
if (st2_transactions_behind != -1 && st1_transactions_behind != st2_transactions_behind) {
std::get<4>(result) = st2_transactions_behind;
}
return result;
}
grouprep_server_state grouprep_update_state(
const grouprep_server_state& st1,
const grouprep_server_state& st2
) {
grouprep_server_state result {};
// hostname and port are part of the server identity — preserve them.
std::get<0>(result) = std::get<0>(st1);
std::get<1>(result) = std::get<1>(st1);
// carry forward existing values; only override fields that legitimately changed.
std::get<2>(result) = std::get<2>(st1);
std::get<3>(result) = std::get<3>(st1);
std::get<4>(result) = std::get<4>(st1);
std::get<5>(result) = std::get<5>(st1);
// hostname and port **can't** be changed,
// because the are part of the server 'id'. Only the
// other fields are allowed to change, otherwise, the
// verification step should have failed.
int st1_viable_candidate = std::get<2>(st1);
int st2_viable_candidate = std::get<2>(st2);
int st1_read_only = std::get<3>(st1);
int st2_read_only = std::get<3>(st2);
int st1_transactions_behind = std::get<4>(st1);
int st2_transactions_behind = std::get<4>(st2);
if (st2_viable_candidate != -1 && st1_viable_candidate != st2_viable_candidate) {
std::get<2>(result) = st2_viable_candidate;
}
if (st2_read_only != -1 && st1_read_only != st2_read_only) {
std::get<3>(result) = st2_read_only;
}
if (st2_transactions_behind != -1 && st1_transactions_behind != st2_transactions_behind) {
std::get<4>(result) = st2_transactions_behind;
}
return result;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/deps/cluster_simulator/lib/grouprep_utils.cpp` around lines 714 - 747,
The function grouprep_update_state constructs result with empty hostname/port so
callers get "ghost" servers; fix by initializing result from st1 (e.g., copy the
full tuple from st1) then apply the conditional updates for
viable_candidate/read_only/transactions_behind from st2 so identity
(hostname/port) is preserved when grouprep_update_cluster_state pushes it. Also
fix the sentinel checks: viable_candidate and read_only are bools so tests
against -1 are invalid—either make those fields tri-state (std::optional or
enum) or change the update logic to compare st2 values against st1 (or use an
explicit "no-change" marker) so updates only occur when st2 intends a change.

@sonarqubecloud

sonarqubecloud Bot commented May 6, 2026

Copy link
Copy Markdown

Quality Gate Passed Quality Gate passed

Issues
31 New issues
0 Accepted issues

Measures
0 Security Hotspots
No data about Coverage
0.0% Duplication on New Code

See analysis details on SonarQube Cloud

@wazir-ahmed

Copy link
Copy Markdown
Member Author

Closing this in favor of #5760

@wazir-ahmed wazir-ahmed closed this May 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants