@ -1,21 +1,15 @@
/**
* @ file mcp_stats_refresh - t . cpp
* @ brief TAP integration test for MCP stats refresh- on - read behavior .
* @ brief TAP integration test for MCP stats endpoint functionality .
*
* This test validates the temporary MCP stats correctness strategy implemented in
* ` Stats_Tool_Handler : : execute_admin_query ( ) ` :
*
* 1. MCP stats queries are serialized with ` GloAdmin - > sql_query_global_mutex ` .
* 2. ` ProxySQL_Admin : : GenericRefreshStatistics ( ) ` is executed before reading
* runtime - populated ` stats . * ` tables .
* This test validates that the MCP stats endpoint can query runtime metrics
* and that the metrics reflect actual ProxySQL state .
*
* Test strategy :
* - Inject a synthetic marker row directly into ` stats . stats_mysql_global ` .
* - Query ` show_status ` over ` / mcp / stats ` for that marker .
* - Expect the marker to disappear because refresh repopulates the table from
* runtime state , dropping synthetic stale rows .
*
* A second ` show_status ` call validates normal data retrieval ( ` ProxySQL_Uptime ` ) .
* 1. Query Client_Connections_connected via MCP stats endpoint
* 2. Create several new MySQL connections to generate traffic
* 3. Query the same metric again
* 4. Verify the connection count has increased
*/
# include <string>
@ -31,9 +25,6 @@ using json = nlohmann::json;
namespace {
static const char * k_marker_name = " MCP_REFRESH_MARKER " ;
static const char * k_marker_value = " mcp_stale_value " ;
/**
* @ brief Execute an admin SQL statement and report success / failure .
*
@ -57,9 +48,6 @@ bool run_admin_stmt(MYSQL* admin, const std::string& query, const char* context)
/**
* @ brief Configure MCP runtime variables required by this test .
*
* The test enables MCP endpoint handling and clears stats endpoint auth so the
* TAP client can call ` / mcp / stats ` without a token .
*
* @ param admin Open admin connection .
* @ param cl TAP command - line environment with target MCP port .
* @ return true if all configuration statements succeeded .
@ -67,6 +55,7 @@ bool run_admin_stmt(MYSQL* admin, const std::string& query, const char* context)
bool configure_mcp_stats_endpoint ( MYSQL * admin , const CommandLine & cl ) {
const std : : vector < std : : string > statements = {
" SET mcp-port= " + std : : to_string ( cl . mcp_port ) ,
" SET mcp-use_ssl=false " ,
" SET mcp-enabled=true " ,
" SET mcp-stats_endpoint_auth='' " ,
" LOAD MCP VARIABLES TO RUNTIME "
@ -83,13 +72,13 @@ bool configure_mcp_stats_endpoint(MYSQL* admin, const CommandLine& cl) {
/**
* @ brief Parse and validate the payload returned by MCP ` show_status ` .
*
* Expected payload shape :
* ` { " success" : true , " result " : { " variables " : [ . . . ] } } `
* Expected payload shape ( after MCPClient extracts from content [ 0 ] . text ) :
* ` { " db_type" : " mysql " , " variables " : [ . . . ] } `
*
* @ param response MCP response object .
* @ param variables Output JSON array of variables .
* @ param error Output error text on failure .
* @ return true when payload structure is valid and tool - level success is true .
* @ return true when payload structure is valid .
*/
bool extract_show_status_variables ( const MCPResponse & response , json & variables , std : : string & error ) {
if ( ! response . is_success ( ) ) {
@ -103,6 +92,13 @@ bool extract_show_status_variables(const MCPResponse& response, json& variables,
return false ;
}
// Check for variables array directly (new format)
if ( payload . contains ( " variables " ) & & payload [ " variables " ] . is_array ( ) ) {
variables = payload [ " variables " ] ;
return true ;
}
// Check for legacy format with success/result wrapper
if ( ! payload . value ( " success " , false ) ) {
error = payload . value ( " error " , std : : string ( " show_status returned tool error " ) ) ;
return false ;
@ -123,6 +119,42 @@ bool extract_show_status_variables(const MCPResponse& response, json& variables,
return true ;
}
/**
* @ brief Extract a numeric metric value from show_status variables array .
*
* @ param variables JSON array of variable objects ( each with variable_name / value or Variable_Name / Variable_Value ) .
* @ param var_name Name of the variable to find .
* @ param value Output value if found .
* @ return true if variable was found and parsed as integer .
*/
bool get_metric_value ( const json & variables , const std : : string & var_name , long & value ) {
for ( const auto & var : variables ) {
// Try lowercase field names (new format)
if ( var . contains ( " variable_name " ) & & var [ " variable_name " ] = = var_name ) {
if ( var . contains ( " value " ) ) {
try {
value = std : : stol ( var [ " value " ] . get < std : : string > ( ) ) ;
return true ;
} catch ( . . . ) {
return false ;
}
}
}
// Try uppercase field names (legacy format)
if ( var . contains ( " Variable_Name " ) & & var [ " Variable_Name " ] = = var_name ) {
if ( var . contains ( " Variable_Value " ) ) {
try {
value = std : : stol ( var [ " Variable_Value " ] . get < std : : string > ( ) ) ;
return true ;
} catch ( . . . ) {
return false ;
}
}
}
}
return false ;
}
} // namespace
int main ( int argc , char * * argv ) {
@ -166,88 +198,157 @@ int main(int argc, char** argv) {
}
bool mcp_reachable = false ;
bool using_ssl = false ;
if ( can_continue ) {
// Retry loop: MCP server may need a moment to start after LOAD MCP VARIABLES TO RUNTIME
const int k_max_retries = 30 ; // 30 retries * 100ms = 3 seconds max wait
const int k_retry_delay_ms = 100 ;
int retry_count = 0 ;
while ( ! mcp_reachable & & retry_count < k_max_retries ) {
usleep ( k_retry_delay_ms * 1000 ) ;
mcp_reachable = mcp - > check_server ( ) ;
retry_count + + ;
// First try HTTP, then HTTPS if HTTP fails
for ( int ssl_attempt = 0 ; ssl_attempt < = 1 & & ! mcp_reachable ; ssl_attempt + + ) {
bool try_ssl = ( ssl_attempt = = 1 ) ;
mcp - > set_use_ssl ( try_ssl ) ;
if ( try_ssl ) {
diag ( " HTTP failed, trying HTTPS... " ) ;
}
retry_count = 0 ;
while ( ! mcp_reachable & & retry_count < k_max_retries ) {
usleep ( k_retry_delay_ms * 1000 ) ;
mcp_reachable = mcp - > check_server ( ) ;
retry_count + + ;
}
if ( mcp_reachable ) {
using_ssl = try_ssl ;
diag ( " MCP server reachable via %s after %d retries (%dms) " ,
try_ssl ? " HTTPS " : " HTTP " , retry_count , retry_count * k_retry_delay_ms ) ;
}
}
if ( mcp_reachable ) {
diag ( " MCP server became reachable after %d retries (%dms) " , retry_count , retry_count * k_retry_delay_ms ) ;
}
ok ( mcp_reachable , " MCP server reachable at %s " , mcp - > get_connection_info ( ) . c_str ( ) ) ;
ok ( mcp_reachable , " MCP server reachable at %s (%s) " , mcp - > get_connection_info ( ) . c_str ( ) ,
using_ssl ? " HTTPS " : " HTTP " ) ;
if ( ! mcp_reachable ) {
skip ( 7 , " Cannot continue without MCP connectivity " ) ;
can_continue = false ;
}
}
bool marker_deleted = false ;
bool marker_inserted = false ;
if ( can_continue ) {
// Inject synthetic stale row into stats global table.
marker_deleted = run_admin_stmt (
admin ,
" DELETE FROM stats.stats_mysql_global WHERE Variable_Name='MCP_REFRESH_MARKER' " ,
" Delete stale marker row "
) ;
marker_inserted = run_admin_stmt (
admin ,
" INSERT OR REPLACE INTO stats.stats_mysql_global (Variable_Name, Variable_Value) VALUES (' "
+ std : : string ( k_marker_name ) + " ', ' " + std : : string ( k_marker_value ) + " ') " ,
" Insert stale marker row "
) ;
ok ( marker_deleted & & marker_inserted , " Injected synthetic stale marker into stats.stats_mysql_global " ) ;
if ( ! ( marker_deleted & & marker_inserted ) ) {
skip ( 6 , " Cannot continue without marker row setup " ) ;
can_continue = false ;
}
}
// Variables needed across multiple blocks
json initial_vars = json : : array ( ) ;
long initial_count = 0 ;
// Test: Query Client_Connections_connected, create connections, verify count increases
if ( can_continue ) {
const MCPResponse marker_resp = mcp - > call_tool (
diag ( " Step 1: Querying initial Client_Connections_connected via MCP stats endpoint " ) ;
// Step 1: Get initial connection count via MCP stats
const MCPResponse initial_resp = mcp - > call_tool (
" stats " ,
" show_status " ,
json { { " db_type " , " mysql " } , { " variable_name " , k_marker_name } }
json { { " db_type " , " mysql " } , { " variable_name " , " Client_Connections_connected " } }
) ;
ok ( marker _resp. is_success ( ) , " MCP call stats.show_status( marker) transport/protocol success" ) ;
ok ( initial_resp . is_success ( ) , " MCP call stats.show_status(Client_Connections_connected) transport success " ) ;
json marker_vars = json : : array ( ) ;
std: : string marker_err ;
const bool marker_payload_ok = extract_show_status_variables ( marker_resp , marker_vars , marker_err ) ;
ok ( marker_payload_ok , " stats.show_status(marker) payload valid%s%s " ,
marker_payload_ok ? " " : " : " , marker_payload_ok ? " " : marker_err . c_str ( ) ) ;
// Debug: print raw response
diag ( " Raw HTTP response code: %ld " , initial_resp . get_http_code ( ) ) ;
if ( ! initial_resp . is_success ( ) ) {
diag ( " Transport/protocol error: %s " , initial_resp . get_error_message ( ) . c_str ( ) ) ;
}
const size_t marker_row_count = marker_payload_ok ? marker_vars . size ( ) : 0 ;
ok ( marker_payload_ok & & marker_row_count = = 0 ,
" Marker row removed after refresh-before-read (variables=%zu) " , marker_row_count ) ;
std : : string initial_err ;
bool initial_payload_ok = extract_show_status_variables ( initial_resp , initial_vars , initial_err ) ;
if ( ! initial_payload_ok ) {
diag ( " Payload extraction failed: %s " , initial_err . c_str ( ) ) ;
diag ( " Raw response body: %s " , initial_resp . get_http_response ( ) . substr ( 0 , 500 ) . c_str ( ) ) ;
}
ok ( initial_payload_ok , " Initial show_status payload valid%s%s " ,
initial_payload_ok ? " " : " : " , initial_payload_ok ? " " : initial_err . c_str ( ) ) ;
if ( initial_payload_ok ) {
diag ( " Received %zu variables in response " , initial_vars . size ( ) ) ;
bool found = get_metric_value ( initial_vars , " Client_Connections_connected " , initial_count ) ;
if ( found ) {
diag ( " Client_Connections_connected initial value: %ld " , initial_count ) ;
}
ok ( found , " Found Client_Connections_connected in initial response (value=%ld) " , initial_count ) ;
if ( ! found ) {
skip ( 4 , " Cannot continue without initial connection count " ) ;
can_continue = false ;
}
} else {
skip ( 5 , " Cannot continue without valid initial payload " ) ;
can_continue = false ;
}
}
// Create additional connections and verify count increases
if ( can_continue ) {
// Step 2: Create several new MySQL connections to the frontend port
const int k_new_connections = 5 ;
std : : vector < MYSQL * > new_conns ;
diag ( " Step 2: Creating %d new MySQL connections to %s:%d to generate traffic " ,
k_new_connections , cl . host , cl . port ) ;
for ( int i = 0 ; i < k_new_connections ; i + + ) {
MYSQL * conn = init_mysql_conn ( cl . host , cl . port , cl . username , cl . password ) ;
if ( conn ) {
new_conns . push_back ( conn ) ;
diag ( " Created connection %d " , i + 1 ) ;
} else {
diag ( " Failed to create connection %d " , i + 1 ) ;
}
}
diag ( " Successfully created %zu new connections " , new_conns . size ( ) ) ;
ok ( new_conns . size ( ) > = 1 , " Created at least 1 new connection (created %zu) " , new_conns . size ( ) ) ;
const MCPResponse uptime_resp = mcp - > call_tool (
// Step 3: Query connection count again
diag ( " Step 3: Querying Client_Connections_connected again via MCP stats endpoint " ) ;
const MCPResponse updated_resp = mcp - > call_tool (
" stats " ,
" show_status " ,
json { { " db_type " , " mysql " } , { " variable_name " , " ProxySQL_Uptime " } }
json { { " db_type " , " mysql " } , { " variable_name " , " Client_Connections_connected " } }
) ;
ok ( uptime_resp . is_success ( ) , " MCP call stats.show_status(ProxySQL_Uptime) transport/protocol success " ) ;
json uptime_vars = json : : array ( ) ;
std : : string uptime_err ;
const bool uptime_payload_ok = extract_show_status_variables ( uptime_resp , uptime_vars , uptime_err ) ;
ok ( uptime_payload_ok , " stats.show_status(ProxySQL_Uptime) payload valid%s%s " ,
uptime_payload_ok ? " " : " : " , uptime_payload_ok ? " " : uptime_err . c_str ( ) ) ;
ok ( updated_resp . is_success ( ) , " MCP call stats.show_status after connections transport success " ) ;
json updated_vars = json : : array ( ) ;
std : : string updated_err ;
bool updated_payload_ok = extract_show_status_variables ( updated_resp , updated_vars , updated_err ) ;
if ( ! updated_payload_ok ) {
diag ( " Payload extraction failed: %s " , updated_err . c_str ( ) ) ;
diag ( " Raw response body: %s " , updated_resp . get_http_response ( ) . substr ( 0 , 500 ) . c_str ( ) ) ;
}
ok ( updated_payload_ok , " Updated show_status payload valid%s%s " ,
updated_payload_ok ? " " : " : " , updated_payload_ok ? " " : updated_err . c_str ( ) ) ;
// Step 4: Verify count increased
if ( updated_payload_ok ) {
long updated_count = 0 ;
bool found = get_metric_value ( updated_vars , " Client_Connections_connected " , updated_count ) ;
if ( found ) {
diag ( " Client_Connections_connected updated value: %ld (initial was: %ld) " , updated_count , initial_count ) ;
ok ( updated_count > initial_count ,
" Connection count increased after creating connections (before=%ld, after=%ld, diff=%ld) " ,
initial_count , updated_count , updated_count - initial_count ) ;
} else {
diag ( " Client_Connections_connected not found in response " ) ;
diag ( " Available variables: %s " , updated_vars . dump ( ) . substr ( 0 , 500 ) . c_str ( ) ) ;
ok ( false , " Client_Connections_connected not found in updated response " ) ;
}
} else {
skip ( 1 , " Cannot verify count without valid updated payload " ) ;
}
ok ( uptime_payload_ok & & ! uptime_vars . empty ( ) ,
" stats.show_status(ProxySQL_Uptime) returned at least one variable row " ) ;
// Cleanup: close the connections we created
diag ( " Cleanup: Closing %zu test connections " , new_conns . size ( ) ) ;
for ( MYSQL * conn : new_conns ) {
mysql_close ( conn ) ;
}
}
if ( admin ) {
run_q ( admin , " DELETE FROM stats.stats_mysql_global WHERE Variable_Name='MCP_REFRESH_MARKER' " ) ;
run_q ( admin , " SET mcp-stats_endpoint_auth='' " ) ;
run_q ( admin , " SET mcp-enabled=false " ) ;
run_q ( admin , " LOAD MCP VARIABLES TO RUNTIME " ) ;