diff --git a/include/proxysql_structs.h b/include/proxysql_structs.h index 893a2bfda0..b5a620101b 100644 --- a/include/proxysql_structs.h +++ b/include/proxysql_structs.h @@ -758,6 +758,14 @@ enum proxysql_session_type { PROXYSQL_SESSION_NONE }; +// Stop state enumeration for PROXYSQL STOP command (issue 5186) +// Used to manage admin query access during module stop/start cycle +enum proxy_stop_state { + STOP_STATE_RUNNING = 0, // Normal operation, all modules running + STOP_STATE_DRAINING = 1, // Admin queries being drained, modules stopping + STOP_STATE_STOPPED = 2 // Modules stopped, only safe queries allowed +}; + #endif /* PROXYSQL_ENUMS */ @@ -959,6 +967,11 @@ struct _global_variables_t { bool nostart; int reload; + // Stop state management for PROXYSQL STOP command + // See issue 5186: Fix query handling after PROXYSQL STOP + volatile int stop_state; + uint64_t active_admin_queries; + unsigned char protocol_version; char *mysql_server_version; uint32_t server_capabilities; diff --git a/lib/Admin_Handler.cpp b/lib/Admin_Handler.cpp index 288ca2a85c..07b54ae584 100644 --- a/lib/Admin_Handler.cpp +++ b/lib/Admin_Handler.cpp @@ -89,6 +89,9 @@ extern "C" void __gcov_dump(); extern "C" void __gcov_reset(); #endif +// Function declarations for issue 5186 +extern void ProxySQL_Main_init_main_modules(); + #ifdef DEBUG //#define BENCHMARK_FASTROUTING_LOAD @@ -482,6 +485,54 @@ bool admin_handler_command_proxysql(char *query_no_space, unsigned int query_no_ ProxySQL_Admin* SPA = (ProxySQL_Admin*)pa; bool rc = false; + // Handle PROXYSQL START after PROXYSQL STOP (issue 5186) + if (glovars.stop_state == STOP_STATE_STOPPED) { + proxy_info("PROXYSQL START: Restarting modules after STOP\n"); + + /* + * CRITICAL: Why proper restart after PROXYSQL STOP is essential + * ================================================================= + * + * PROXYSQL STOP performs a complete shutdown of all core modules: + * 1. MySQL and PgSQL thread pools (GloMTH, GloPTH) are shutdown + * 2. Query Processors (GloMyQPro, GloMyAuth, etc.) are destroyed + * 3. All global module pointers are set to NULL + * 4. Thread synchronization objects are cleaned up + * + * Simply calling ProxySQL_Main_init_main_modules() is INSUFFICIENT because: + * - It doesn't properly reinitialize thread synchronization + * - It doesn't restart the MySQL/PgSQL thread pools + * - It doesn't ensure proper thread initialization sequencing + * - It leaves modules in inconsistent state + * + * Without complete reinitialization, admin queries will crash with: + * - Segmentation faults accessing destroyed Query Processor modules + * - Race conditions with partially initialized thread pools + * - NULL pointer dereferences in GloMyQPro, GloMyAuth, etc. + * - Lock contention on destroyed synchronization objects + * + * SOLUTION: Simulate initial startup conditions: + * 1. Set GloVars.global.nostart = 1 (simulate "not started" state) + * 2. Set admin_nostart_ = true (trigger startup logic) + * 3. Let the normal START sequence reinitialize everything properly + * 4. Ensure thread pools, query processors, and sync objects are rebuilt + * 5. Maintain same initialization order as initial startup + * + * This prevents crashes and ensures full STOP/START functionality. + */ + + // Reset state to running and set nostart_ to trigger normal startup sequence + glovars.stop_state = STOP_STATE_RUNNING; + glovars.reload = 0; + glovars.shutdown = 0; + // Set nostart_ to true so the normal startup logic will trigger + GloVars.global.nostart = 1; + + // Continue to normal startup logic below + admin_nostart_ = true; + } + + // Handle normal START (initial startup or restart after STOP) if (admin_nostart_) { rc = __sync_bool_compare_and_swap(&GloVars.global.nostart, 1, 0); } @@ -536,6 +587,16 @@ bool admin_handler_command_proxysql(char *query_no_space, unsigned int query_no_ } } + // Check if already stopped (issue 5186) + if (glovars.stop_state == STOP_STATE_STOPPED) { + SPA->send_error_msg_to_client(sess, (char*)"ProxySQL modules are already stopped"); + return false; + } + + // Set state to DRAINING - stop accepting new admin queries (issue 5186) + glovars.stop_state = STOP_STATE_DRAINING; + proxy_info("PROXYSQL STOP: Setting state to DRAINING, waiting for %lu admin queries to complete\n", (unsigned long)glovars.active_admin_queries); + char buf[32]; // ----- MySQL module stop ----- @@ -558,22 +619,72 @@ bool admin_handler_command_proxysql(char *query_no_space, unsigned int query_no_ GloPTH->set_variable((char*)"wait_timeout", buf); GloPTH->commit(); - // ----- Common shutdown actions ----- + // ----- Wait for admin queries to complete (issue 5186) ----- + int wait_time_ms = 0; + int max_wait_time_ms = 30000; // 30 seconds timeout + uint64_t last_active_queries = glovars.active_admin_queries; + int stable_count = 0; + + proxy_info("PROXYSQL STOP: Initial admin query count: %lu\n", (unsigned long)glovars.active_admin_queries); + + // Wait for all other admin queries to complete (subtract 1 for current PROXYSQL STOP query) + while (glovars.active_admin_queries > 1 && wait_time_ms < max_wait_time_ms) { + usleep(100000); // 100ms intervals + wait_time_ms += 100; + + if (last_active_queries == glovars.active_admin_queries) { + stable_count++; + } else { + stable_count = 0; + last_active_queries = glovars.active_admin_queries; + proxy_info("PROXYSQL STOP: Admin query count changed to: %lu\n", (unsigned long)glovars.active_admin_queries); + } + + if (wait_time_ms % 1000 == 0) { + proxy_info("PROXYSQL STOP: Waiting for %lu admin queries to complete (%d/%ds), stable for %d cycles\n", + (unsigned long)(glovars.active_admin_queries - 1), wait_time_ms/1000, max_wait_time_ms/1000, stable_count); + } + } + + if (glovars.active_admin_queries > 1) { + proxy_warning("PROXYSQL STOP: %lu admin queries still active after timeout (stable count: %d), proceeding with module stop\n", + (unsigned long)(glovars.active_admin_queries - 1), stable_count); + } else { + proxy_info("PROXYSQL STOP: All admin queries completed, proceeding with module stop\n"); + } + + // ----- Common module stop actions ----- glovars.reload = 2; + glovars.stop_state = STOP_STATE_STOPPED; // Reset Prometheus counters if (GloVars.prometheus_registry) GloVars.prometheus_registry->ResetCounters(); - // Signal shutdown and wait for completion + // Signal module stop and wait for completion + proxy_info("PROXYSQL STOP: Starting thread shutdown sequence\n"); __sync_bool_compare_and_swap(&glovars.shutdown, 0, 1); + + proxy_info("PROXYSQL STOP: Signaling MySQL threads to shutdown\n"); GloMTH->signal_all_threads(0); + proxy_info("PROXYSQL STOP: MySQL threads signaled\n"); + + proxy_info("PROXYSQL STOP: Signaling PgSQL threads to shutdown\n"); GloPTH->signal_all_threads(0); + proxy_info("PROXYSQL STOP: PgSQL threads signaled\n"); + proxy_info("PROXYSQL STOP: Entering shutdown wait loop\n"); + int wait_count = 0; while (__sync_fetch_and_add(&glovars.shutdown, 0) == 1) { usleep(1000); + wait_count++; + if (wait_count % 1000 == 0) { // Log every 1 second + proxy_info("PROXYSQL STOP: Still waiting for thread shutdown, count=%d\n", wait_count); + } } + proxy_info("PROXYSQL STOP: Exited shutdown wait loop after %d iterations\n", wait_count); + proxy_info("PROXYSQL STOP: Module stop completed, all modules stopped\n"); SPA->send_ok_msg_to_client(sess, NULL, 0, query_no_space); return false; @@ -2332,6 +2443,10 @@ template void admin_session_handler(S* sess, void *_pa, PtrSize_t *pkt) { ProxySQL_Admin *pa=(ProxySQL_Admin *)_pa; + + // Increment admin query counter for issue 5186 + // This tracks active admin queries during PROXYSQL STOP + __sync_fetch_and_add(&glovars.active_admin_queries, 1); bool needs_vacuum = false; char *error=NULL; int cols; @@ -2597,9 +2712,16 @@ void admin_session_handler(S* sess, void *_pa, PtrSize_t *pkt) { if (!strncasecmp(CLUSTER_QUERY_MYSQL_USERS, query_no_space, strlen(CLUSTER_QUERY_MYSQL_USERS))) { if (sess->session_type == PROXYSQL_SESSION_ADMIN) { - pthread_mutex_lock(&users_mutex); - resultset = GloMyAuth->get_current_mysql_users(); - pthread_mutex_unlock(&users_mutex); + if (glovars.stop_state == STOP_STATE_RUNNING) { + pthread_mutex_lock(&users_mutex); + resultset = GloMyAuth->get_current_mysql_users(); + pthread_mutex_unlock(&users_mutex); + } else { + // Return empty resultset when modules are stopped (issue 5186) + resultset = new SQLite3_result(2); + resultset->add_column_definition(SQLITE_TEXT, "username"); + resultset->add_column_definition(SQLITE_TEXT, "password"); + } if (resultset != nullptr) { sess->SQLite3_to_MySQL(resultset, error, affected_rows, &sess->client_myds->myprot); run_query=false; @@ -2610,28 +2732,37 @@ void admin_session_handler(S* sess, void *_pa, PtrSize_t *pkt) { if (sess->session_type == PROXYSQL_SESSION_ADMIN) { // no stats if (!strncasecmp(CLUSTER_QUERY_MYSQL_QUERY_RULES, query_no_space, strlen(CLUSTER_QUERY_MYSQL_QUERY_RULES))) { - GloMyQPro->wrlock(); - resultset = GloMyQPro->get_current_query_rules_inner(); - if (resultset == NULL) { - GloMyQPro->wrunlock(); // unlock first - resultset = GloMyQPro->get_current_query_rules(); - if (resultset) { + if (glovars.stop_state == STOP_STATE_RUNNING) { + GloMyQPro->wrlock(); + resultset = GloMyQPro->get_current_query_rules_inner(); + if (resultset == NULL) { + GloMyQPro->wrunlock(); // unlock first + resultset = GloMyQPro->get_current_query_rules(); + if (resultset) { + sess->SQLite3_to_MySQL(resultset, error, affected_rows, &sess->client_myds->myprot); + delete resultset; + run_query=false; + } + } else { sess->SQLite3_to_MySQL(resultset, error, affected_rows, &sess->client_myds->myprot); - delete resultset; + GloMyQPro->wrunlock(); run_query=false; - goto __run_query; } } else { + // Return empty resultset when modules are stopped (issue 5186) + resultset = new SQLite3_result(2); + resultset->add_column_definition(SQLITE_TEXT, "rule_id"); + resultset->add_column_definition(SQLITE_TEXT, "active"); sess->SQLite3_to_MySQL(resultset, error, affected_rows, &sess->client_myds->myprot); - //delete resultset; // DO NOT DELETE . This is the inner resultset of Query_Processor - GloMyQPro->wrunlock(); + delete resultset; run_query=false; goto __run_query; } } if (!strncasecmp(CLUSTER_QUERY_MYSQL_QUERY_RULES_FAST_ROUTING, query_no_space, strlen(CLUSTER_QUERY_MYSQL_QUERY_RULES_FAST_ROUTING))) { - GloMyQPro->wrlock(); - resultset = GloMyQPro->get_current_query_rules_fast_routing_inner(); + if (glovars.stop_state == STOP_STATE_RUNNING) { + GloMyQPro->wrlock(); + resultset = GloMyQPro->get_current_query_rules_fast_routing_inner(); if (resultset == NULL) { GloMyQPro->wrunlock(); // unlock first resultset = GloMyQPro->get_current_query_rules_fast_routing(); @@ -2648,6 +2779,16 @@ void admin_session_handler(S* sess, void *_pa, PtrSize_t *pkt) { run_query=false; goto __run_query; } + } else { + // Return empty resultset when modules are stopped (issue 5186) + resultset = new SQLite3_result(2); + resultset->add_column_definition(SQLITE_TEXT, "rule_id"); + resultset->add_column_definition(SQLITE_TEXT, "hostname"); + sess->SQLite3_to_MySQL(resultset, error, affected_rows, &sess->client_myds->myprot); + delete resultset; + run_query=false; + goto __run_query; + } } } @@ -2655,7 +2796,10 @@ void admin_session_handler(S* sess, void *_pa, PtrSize_t *pkt) { // SELECT COUNT(*) FROM runtime_mysql_query_rules_fast_routing // we just return the count if (strcmp("SELECT COUNT(*) FROM runtime_mysql_query_rules_fast_routing", query_no_space)==0) { - int cnt = GloMyQPro->get_current_query_rules_fast_routing_count(); + int cnt = 0; + if (glovars.stop_state == STOP_STATE_RUNNING) { + cnt = GloMyQPro->get_current_query_rules_fast_routing_count(); + } l_free(query_length,query); char buf[256]; sprintf(buf,"SELECT %d AS 'COUNT(*)'", cnt); @@ -2861,11 +3005,46 @@ void admin_session_handler(S* sess, void *_pa, PtrSize_t *pkt) { goto __run_query; } } +#if 0 { + // this block seems unnecessary, as we have enough fencing ProxySQL_Admin *SPA=(ProxySQL_Admin *)pa; + // Check if this is a dangerous query that should be blocked during STOP states (issue 5186) + if (glovars.stop_state != STOP_STATE_RUNNING && sess->session_type == PROXYSQL_SESSION_ADMIN) { + // Block dangerous runtime_* queries that access destroyed modules + if (!strncasecmp(query_no_space, "SELECT COUNT(*) FROM runtime_mysql_query_rules", strlen("SELECT COUNT(*) FROM runtime_mysql_query_rules")) || + !strncasecmp(query_no_space, "SELECT COUNT(*) FROM runtime_mysql_query_rules_fast_routing", strlen("SELECT COUNT(*) FROM runtime_mysql_query_rules_fast_routing")) || + !strncasecmp(query_no_space, "SELECT COUNT(*) FROM runtime_mysql_users", strlen("SELECT COUNT(*) FROM runtime_mysql_users")) || + !strncasecmp(query_no_space, "SELECT COUNT(*) FROM stats_mysql_query_digest", strlen("SELECT COUNT(*) FROM stats_mysql_query_digest")) || + !strncasecmp(query_no_space, "SELECT * FROM runtime_mysql_query_rules", strlen("SELECT * FROM runtime_mysql_query_rules")) || + !strncasecmp(query_no_space, "SELECT * FROM runtime_mysql_query_rules_fast_routing", strlen("SELECT * FROM runtime_mysql_query_rules_fast_routing")) || + !strncasecmp(query_no_space, "SELECT * FROM runtime_mysql_users", strlen("SELECT * FROM runtime_mysql_users")) || + !strncasecmp(query_no_space, "SELECT * FROM stats_mysql_query_digest", strlen("SELECT * FROM stats_mysql_query_digest"))) { + + //l_free(query_length, query); // ASAN correctly reports a double free + + // Return empty resultset instead of crashing + SQLite3_result *resultset = new SQLite3_result(1); + resultset->add_column_definition(SQLITE_TEXT, "COUNT(*)"); + + // Add a single row with 0 for COUNT(*) queries + SQLite3_row *row = new SQLite3_row(1); + char *field_val = strdup("0"); + row->fields[0] = field_val; + resultset->add_row(row); + + sess->SQLite3_to_MySQL(resultset, error, affected_rows, &sess->client_myds->myprot); + delete resultset; + delete row; + free(field_val); + + run_query = false; + goto __run_query; + } + } needs_vacuum = SPA->GenericRefreshStatistics(query_no_space,query_no_space_length, ( sess->session_type == PROXYSQL_SESSION_ADMIN ? true : false ) ); } - +#endif // 0 if (!strncasecmp("SHOW GLOBAL VARIABLES LIKE 'read_only'", query_no_space, strlen("SHOW GLOBAL VARIABLES LIKE 'read_only'"))) { l_free(query_length,query); @@ -4137,6 +4316,12 @@ void admin_session_handler(S* sess, void *_pa, PtrSize_t *pkt) { pthread_mutex_unlock(&pa->sql_query_global_mutex); } } + +__exit_cleanup: + // Decrement admin query counter for issue 5186 + // This tracks active admin queries during PROXYSQL STOP + __sync_fetch_and_sub(&glovars.active_admin_queries, 1); + l_free(pkt->size-sizeof(mysql_hdr),query_no_space); // it is always freed here l_free(query_length,query); } diff --git a/lib/ProxySQL_Admin.cpp b/lib/ProxySQL_Admin.cpp index daebe79ced..f59235d59c 100644 --- a/lib/ProxySQL_Admin.cpp +++ b/lib/ProxySQL_Admin.cpp @@ -4282,6 +4282,13 @@ bool ProxySQL_Admin::set_variable(char *name, char *value, bool lock) { // this } void ProxySQL_Admin::save_mysql_query_rules_fast_routing_from_runtime(bool _runtime) { + // Check if Query Processor is initialized (issue 5186) + // Prevent crashes during PROXYSQL START race conditions + if (GloMyQPro == nullptr) { + proxy_warning("MySQL Query Processor not initialized, skipping save_mysql_query_rules_fast_routing_from_runtime\n"); + return; + } + if (_runtime) { admindb->execute("DELETE FROM runtime_mysql_query_rules_fast_routing"); } else { @@ -4347,6 +4354,13 @@ void ProxySQL_Admin::save_mysql_query_rules_fast_routing_from_runtime(bool _runt } void ProxySQL_Admin::save_pgsql_query_rules_fast_routing_from_runtime(bool _runtime) { + // Check if PgSQL Query Processor is initialized (issue 5186) + // Prevent crashes during PROXYSQL START race conditions + if (GloPgQPro == nullptr) { + proxy_warning("PgSQL Query Processor not initialized, skipping save_pgsql_query_rules_fast_routing_from_runtime\n"); + return; + } + if (_runtime) { admindb->execute("DELETE FROM runtime_pgsql_query_rules_fast_routing"); } @@ -4416,6 +4430,13 @@ void ProxySQL_Admin::save_pgsql_query_rules_fast_routing_from_runtime(bool _runt } void ProxySQL_Admin::save_mysql_query_rules_from_runtime(bool _runtime) { + // Check if Query Processor is initialized (issue 5186) + // Prevent crashes during PROXYSQL START race conditions + if (GloMyQPro == nullptr) { + proxy_warning("MySQL Query Processor not initialized, skipping save_mysql_query_rules_from_runtime\n"); + return; + } + if (_runtime) { admindb->execute("DELETE FROM runtime_mysql_query_rules"); } else { @@ -4501,6 +4522,13 @@ void ProxySQL_Admin::save_mysql_query_rules_from_runtime(bool _runtime) { } void ProxySQL_Admin::save_pgsql_query_rules_from_runtime(bool _runtime) { + // Check if PgSQL Query Processor is initialized (issue 5186) + // Prevent crashes during PROXYSQL START race conditions + if (GloPgQPro == nullptr) { + proxy_warning("PgSQL Query Processor not initialized, skipping save_pgsql_query_rules_from_runtime\n"); + return; + } + if (_runtime) { admindb->execute("DELETE FROM runtime_pgsql_query_rules"); } diff --git a/src/main.cpp b/src/main.cpp index 52ee9ef4a6..46efa56f8b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -360,6 +360,15 @@ static bool check_openssl_version() { void ProxySQL_Main_init_SSL_module() { + // Check if SSL context is already initialized (issue 5186) + // This prevents SSL context corruption during PROXYSQL STOP/START restart cycles + if (GloVars.global.ssl_ctx != NULL) { + proxy_info("SSL context already initialized at %p, skipping reinitialization\n", GloVars.global.ssl_ctx); + return; + } + + proxy_info("Initializing new SSL context\n"); + int rc = SSL_library_init(); if (rc==0) { proxy_error("%s\n", SSL_alert_desc_string_long(rc)); @@ -377,6 +386,7 @@ void ProxySQL_Main_init_SSL_module() { proxy_error("Unable to initialize SSL. Shutting down...\n"); exit(EXIT_SUCCESS); // we exit gracefully to not be restarted } + proxy_info("SSL context created successfully at %p\n", GloVars.global.ssl_ctx); if (!SSL_CTX_set_min_proto_version(GloVars.global.ssl_ctx,TLS1_VERSION)) { proxy_error("Unable to initialize SSL. SSL_set_min_proto_version failed. Shutting down...\n"); exit(EXIT_SUCCESS); // we exit gracefully to not be restarted @@ -1322,6 +1332,11 @@ void ProxySQL_Main_init() { #else glovars.has_debug=false; #endif /* DEBUG */ + + // Initialize stop state management for issue 5186 + glovars.stop_state = STOP_STATE_RUNNING; + glovars.active_admin_queries = 0; + // __thr_sfp=l_mem_init(); proxysql_init_debug_prometheus_metrics(); } diff --git a/test/tap/groups/groups.json b/test/tap/groups/groups.json index f972143b29..9e0d1c8c50 100644 --- a/test/tap/groups/groups.json +++ b/test/tap/groups/groups.json @@ -7,6 +7,7 @@ "admin_various_commands3-t" : [ "default-g1","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1","mysql84-g1","mysql84-gr-g1","mysql90-g1","mysql90-gr-g1","mysql91-g1","mysql91-gr-g1","mysql92-g1","mysql92-gr-g1","mysql93-g1","mysql93-gr-g1" ], "admin_various_commands-t" : [ "default-g1","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1","mysql84-g1","mysql84-gr-g1","mysql90-g1","mysql90-gr-g1","mysql91-g1","mysql91-gr-g1","mysql92-g1","mysql92-gr-g1","mysql93-g1","mysql93-gr-g1" ], "basic-t" : [ "default-g1","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1","mysql84-g1","mysql84-gr-g1","mysql90-g1","mysql90-gr-g1","mysql91-g1","mysql91-gr-g1","mysql92-g1","mysql92-gr-g1","mysql93-g1","mysql93-gr-g1" ], + "test_proxysql_stop_query_handling-t" : [ "default-g1" ], "charset_unsigned_int-t" : [ "default-g1","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1" ], "clickhouse_php_conn-t" : [ "default-g1","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1" ], "envvars-t" : [ "default-g1","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1","mysql84-g1","mysql84-gr-g1","mysql90-g1","mysql90-gr-g1","mysql91-g1","mysql91-gr-g1","mysql92-g1","mysql92-gr-g1","mysql93-g1","mysql93-gr-g1" ], diff --git a/test/tap/tests/test_proxysql_stop_query_handling-t.cpp b/test/tap/tests/test_proxysql_stop_query_handling-t.cpp new file mode 100644 index 0000000000..7b7163fc4d --- /dev/null +++ b/test/tap/tests/test_proxysql_stop_query_handling-t.cpp @@ -0,0 +1,169 @@ +/** + * @file test_proxysql_stop_query_handling-t.cpp + * @brief This test verifies PROXYSQL STOP query handling fix for issue 5186. + * Tests that admin queries are properly handled during STOP state. + * @date 2025-01-18 + */ + +#include +#include +#include +#include +#include + +#include "mysql.h" +#include "mysqld_error.h" + +#include "tap.h" +#include "command_line.h" +#include "utils.h" + +using std::string; + +// Helper function to execute query and check if it succeeds +bool execute_query_succeeds(MYSQL* mysql, const string& query) { + if (mysql_query(mysql, query.c_str()) == 0) { + mysql_free_result(mysql_store_result(mysql)); + return true; + } + return false; +} + +// Helper function to execute query and check if it fails as expected +bool execute_query_fails(MYSQL* mysql, const string& query, const string& expected_error_substring = "") { + int rc = mysql_query(mysql, query.c_str()); + if (rc != 0) { + string error = mysql_error(mysql); + if (expected_error_substring.empty() || error.find(expected_error_substring) != string::npos) { + return true; // Failed as expected + } + } + return false; // Should have failed but didn't +} + +// Helper function to get row count from a query +int get_row_count(MYSQL* mysql, const string& query) { + if (mysql_query(mysql, query.c_str()) == 0) { + MYSQL_RES* result = mysql_store_result(mysql); + if (result) { + int count = mysql_num_rows(result); + mysql_free_result(result); + return count; + } + } + return -1; +} + +int main(int argc, char** argv) { + CommandLine cl; + + if (cl.getEnv()) { + diag("Failed to get the required environmental variables."); + return -1; + } + + // We expect 13 test cases: + // 1. Test STOP command succeeds + // 2-5. Test queries that work with null pointer protection during STOP state (4 queries) + // 6. Test LOAD MYSQL USERS TO RUNTIME succeeds (MySQL Auth module is loaded) + // 7. Test LOAD MYSQL QUERY RULES TO RUNTIME fails (Query Processor not started) + // 8-11. Test queries that should succeed during STOP state (4 queries) + // 12. Test START command succeeds + // 13. Test that queries continue to work after START + plan(13); + + MYSQL* proxysql_admin = mysql_init(NULL); + if (!proxysql_admin) { + fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(proxysql_admin)); + return -1; + } + + // Connect to local ProxySQL admin interface + if (!mysql_real_connect(proxysql_admin, cl.host, cl.admin_username, cl.admin_password, NULL, cl.admin_port, NULL, 0)) { + fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(proxysql_admin)); + return -1; + } + + // === TEST 1: Execute PROXYSQL STOP === + bool stop_success = execute_query_succeeds(proxysql_admin, "PROXYSQL STOP"); + ok(stop_success, "PROXYSQL STOP command should succeed"); + + if (!stop_success) { + diag("PROXYSQL STOP failed, cannot continue with remaining tests"); + mysql_close(proxysql_admin); + return exit_status(); + } + + // Give some time for STOP to complete + sleep(2); + + // === TESTS 2-5: Test queries that work with null pointer protection during STOP state === + + // TEST 2: runtime_mysql_query_rules should work normally with null pointer protection + int row_count = get_row_count(proxysql_admin, "SELECT COUNT(*) FROM runtime_mysql_query_rules"); + ok(row_count >= 0, "runtime_mysql_query_rules should return valid count during STOP state, actual: %d", row_count); + + // TEST 3: runtime_mysql_query_rules_fast_routing should work normally with null pointer protection + row_count = get_row_count(proxysql_admin, "SELECT COUNT(*) FROM runtime_mysql_query_rules_fast_routing"); + ok(row_count >= 0, "runtime_mysql_query_rules_fast_routing should return valid count during STOP state, actual: %d", row_count); + + // TEST 4: runtime_mysql_users should work normally with null pointer protection + row_count = get_row_count(proxysql_admin, "SELECT COUNT(*) FROM runtime_mysql_users"); + ok(row_count >= 0, "runtime_mysql_users should return valid count during STOP state, actual: %d", row_count); + + // TEST 5: stats_mysql_query_digest should work normally with null pointer protection + row_count = get_row_count(proxysql_admin, "SELECT COUNT(*) FROM stats_mysql_query_digest"); + ok(row_count >= 0, "stats_mysql_query_digest should return valid count during STOP state, actual: %d", row_count); + + // === TEST 6: Test modification queries during STOP state === + + // TEST 6: LOAD MYSQL USERS TO RUNTIME should succeed (MySQL Auth module is loaded) + bool load_users_success = execute_query_succeeds(proxysql_admin, "LOAD MYSQL USERS TO RUNTIME"); + ok(load_users_success, "LOAD MYSQL USERS TO RUNTIME should succeed during STOP state"); + + // TEST 7: LOAD MYSQL QUERY RULES TO RUNTIME should fail (Query Processor not started) + bool load_rules_fails = execute_query_fails(proxysql_admin, "LOAD MYSQL QUERY RULES TO RUNTIME", "Global Query Processor not started"); + ok(load_rules_fails, "LOAD MYSQL QUERY RULES TO RUNTIME should fail during STOP state"); + + // === TESTS 8-11: Test queries that should SUCCEED during STOP state === + + // TEST 8: Basic arithmetic query should work + bool basic_query_success = execute_query_succeeds(proxysql_admin, "SELECT 1+1"); + ok(basic_query_success, "Basic arithmetic query (SELECT 1+1) should work during STOP state"); + + // TEST 9: Version query should work + bool version_success = execute_query_succeeds(proxysql_admin, "SELECT @@version"); + ok(version_success, "Version query should work during STOP state"); + + // TEST 10: SHOW PROMETHEUS METRICS should work (existing functionality) + bool prometheus_success = execute_query_succeeds(proxysql_admin, "SHOW PROMETHEUS METRICS"); + ok(prometheus_success, "SHOW PROMETHEUS METRICS should work during STOP state"); + + // TEST 11: Basic SELECT should work + bool db_select_success = execute_query_succeeds(proxysql_admin, "SELECT DATABASE()"); + bool user_select_success = execute_query_succeeds(proxysql_admin, "SELECT USER()"); + ok(db_select_success && user_select_success, "Basic SELECT (DATABASE() and USER()) should work during STOP state"); + + // === TEST 12: Execute PROXYSQL START === + bool start_success = execute_query_succeeds(proxysql_admin, "PROXYSQL START"); + ok(start_success, "PROXYSQL START command should succeed"); + + if (!start_success) { + diag("PROXYSQL START failed, cannot continue with final test"); + mysql_close(proxysql_admin); + return exit_status(); + } + + // Give some time for START to complete + sleep(3); + + // === TEST 13: Test that queries continue to work after START === + + // After START, runtime queries should continue to work normally + row_count = get_row_count(proxysql_admin, "SELECT COUNT(*) FROM runtime_mysql_query_rules"); + ok(row_count >= 0, "runtime_mysql_query_rules should continue to work after START, rows: %d", row_count); + + mysql_close(proxysql_admin); + + return exit_status(); +} \ No newline at end of file