From f93432ab0663a648f2e2559ab5819b9949f6d3fa Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Thu, 19 Feb 2026 01:59:26 +0000 Subject: [PATCH] TAP MCP stats: add mixed-load profile/churn runners and enhance mixed stress configurability Enhance mcp_mixed_mysql_pgsql_concurrency_stress-t with environment-driven load parameters so it can be reused as a quick demo, sustained stress run, or heavier load scenario without source edits. Add optional live MCP cap churn support that updates mcp-stats_show_processlist_max_rows and mcp-stats_show_queries_max_rows during active mixed MySQL+PgSQL traffic and concurrent MCP polling. Generalize cap-metadata assertions in processlist/show_queries pollers to support both fixed-cap and churned-cap modes through accepted cap profiles. Add mcp_mixed_stats_profile_matrix-t as an orchestrator TAP that executes multiple mixed-load profiles (quick, churn, heavy) and validates successful completion of each run. Add mcp_mixed_stats_cap_churn-t as a focused orchestrator TAP for aggressive cap-churn scenarios under mixed protocol traffic. Both orchestrator TAPs isolate child output to per-run log files, preserve parent TAP stream integrity, and emit diagnostic log tails on failures for easier triage. Compilation and runtime validation performed locally before commit: enhanced mixed stress TAP plus both new orchestrator TAPs passed. --- ...mixed_mysql_pgsql_concurrency_stress-t.cpp | 327 ++++++++++++++++-- .../tap/tests/mcp_mixed_stats_cap_churn-t.cpp | 151 ++++++++ .../mcp_mixed_stats_profile_matrix-t.cpp | 156 +++++++++ 3 files changed, 610 insertions(+), 24 deletions(-) create mode 100644 test/tap/tests/mcp_mixed_stats_cap_churn-t.cpp create mode 100644 test/tap/tests/mcp_mixed_stats_profile_matrix-t.cpp diff --git a/test/tap/tests/mcp_mixed_mysql_pgsql_concurrency_stress-t.cpp b/test/tap/tests/mcp_mixed_mysql_pgsql_concurrency_stress-t.cpp index 64eeff8fc..c04327c40 100644 --- a/test/tap/tests/mcp_mixed_mysql_pgsql_concurrency_stress-t.cpp +++ b/test/tap/tests/mcp_mixed_mysql_pgsql_concurrency_stress-t.cpp @@ -46,16 +46,16 @@ using json = nlohmann::json; namespace { -/** Number of MySQL worker threads. */ -static constexpr int k_mysql_worker_threads = 12; -/** Number of PgSQL worker threads. */ -static constexpr int k_pgsql_worker_threads = 12; -/** Total stress runtime in seconds. */ -static constexpr int k_runtime_seconds = 14; -/** Configured MCP cap for `stats.show_processlist`. */ -static constexpr int k_processlist_cap = 120; -/** Configured MCP cap for `stats.show_queries`. */ -static constexpr int k_show_queries_cap = 180; +/** Default number of MySQL worker threads. */ +static constexpr int k_default_mysql_worker_threads = 12; +/** Default number of PgSQL worker threads. */ +static constexpr int k_default_pgsql_worker_threads = 12; +/** Default stress runtime in seconds. */ +static constexpr int k_default_runtime_seconds = 14; +/** Default MCP cap for `stats.show_processlist`. */ +static constexpr int k_default_processlist_cap = 120; +/** Default MCP cap for `stats.show_queries`. */ +static constexpr int k_default_show_queries_cap = 180; /** Requested processlist limit used to verify cap metadata. */ static constexpr int k_processlist_requested_limit = 500; /** Requested show_queries limit used to verify cap metadata. */ @@ -69,6 +69,21 @@ static constexpr uint64_t k_min_total_pgsql_queries = 300; /** Fixed schema used for MySQL workload table. */ static constexpr const char* k_mysql_workload_schema = "test"; +/** Lower bound for generated processlist cap profile values. */ +static constexpr int k_min_processlist_cap = 10; +/** Lower bound for generated show_queries cap profile values. */ +static constexpr int k_min_show_queries_cap = 10; +/** Maximum allowed worker threads per protocol from environment. */ +static constexpr int k_max_worker_threads = 256; +/** Maximum allowed runtime in seconds from environment. */ +static constexpr int k_max_runtime_seconds = 1800; +/** Maximum allowed cap-churn interval in milliseconds from environment. */ +static constexpr int k_max_cap_churn_interval_ms = 10000; +/** Minimum allowed cap-churn interval in milliseconds from environment. */ +static constexpr int k_min_cap_churn_interval_ms = 50; +/** Default cap-churn interval in milliseconds. */ +static constexpr int k_default_cap_churn_interval_ms = 700; + using MYSQLConnPtr = std::unique_ptr; using PGConnPtr = std::unique_ptr; @@ -121,6 +136,90 @@ struct show_queries_poll_stats_t { std::atomic filtered_invalid_rows {0}; }; +/** + * @brief Parse an integer environment variable with range clamping. + * + * If the variable is absent or invalid, the default value is returned. + * + * @param name Environment variable name. + * @param default_value Value used when the variable is absent or invalid. + * @param min_value Lower accepted bound. + * @param max_value Upper accepted bound. + * @return Parsed and clamped value. + */ +int env_int_clamped(const char* name, int default_value, int min_value, int max_value) { + const char* value = std::getenv(name); + if (!value || std::strlen(value) == 0) { + return default_value; + } + + char* end = nullptr; + long parsed = std::strtol(value, &end, 10); + if (!end || *end != '\0') { + return default_value; + } + + if (parsed < static_cast(min_value)) { + return min_value; + } + if (parsed > static_cast(max_value)) { + return max_value; + } + return static_cast(parsed); +} + +/** + * @brief Parse a boolean environment variable. + * + * Accepted true values: `1`, `true`, `yes`, `on` (case-insensitive). + * Accepted false values: `0`, `false`, `no`, `off` (case-insensitive). + * Any other value falls back to the provided default. + * + * @param name Environment variable name. + * @param default_value Value used when the variable is absent or invalid. + * @return Parsed boolean value. + */ +bool env_bool(const char* name, bool default_value) { + const char* raw = std::getenv(name); + if (!raw || std::strlen(raw) == 0) { + return default_value; + } + + std::string value(raw); + std::transform(value.begin(), value.end(), value.begin(), [] (unsigned char c) { + return static_cast(std::tolower(c)); + }); + + if (value == "1" || value == "true" || value == "yes" || value == "on") { + return true; + } + if (value == "0" || value == "false" || value == "no" || value == "off") { + return false; + } + return default_value; +} + +/** + * @brief Build a three-level cap profile used for optional live cap churn. + * + * The returned profile always includes @p max_cap and one or two lower levels, + * then removes duplicates while preserving ascending order. + * + * @param max_cap Highest cap value in the profile. + * @param min_cap Minimum cap value to enforce. + * @return Ordered unique cap profile. + */ +std::vector build_cap_profile(int max_cap, int min_cap) { + std::vector caps = { + std::max(min_cap, max_cap / 3), + std::max(min_cap, max_cap / 2), + std::max(min_cap, max_cap) + }; + std::sort(caps.begin(), caps.end()); + caps.erase(std::unique(caps.begin(), caps.end()), caps.end()); + return caps; +} + /** * @brief Execute an admin SQL statement and consume any result set. * @@ -152,14 +251,19 @@ bool run_admin_stmt(MYSQL* admin, const std::string& query, const char* context) * @param cl TAP command-line configuration. * @return true if all setup statements succeeded. */ -bool configure_mcp_runtime(MYSQL* admin, const CommandLine& cl) { +bool configure_mcp_runtime( + MYSQL* admin, + const CommandLine& cl, + int processlist_cap, + int show_queries_cap +) { const std::vector statements = { "SET mcp-port=" + std::to_string(cl.mcp_port), "SET mcp-use_ssl=false", "SET mcp-enabled=true", "SET mcp-stats_endpoint_auth=''", - "SET mcp-stats_show_processlist_max_rows=" + std::to_string(k_processlist_cap), - "SET mcp-stats_show_queries_max_rows=" + std::to_string(k_show_queries_cap), + "SET mcp-stats_show_processlist_max_rows=" + std::to_string(processlist_cap), + "SET mcp-stats_show_queries_max_rows=" + std::to_string(show_queries_cap), "LOAD MCP VARIABLES TO RUNTIME" }; @@ -687,12 +791,82 @@ void run_pgsql_worker( } } +/** + * @brief Continuously churn MCP cap variables while traffic and polling run. + * + * This thread cycles configured processlist/query caps and reloads MCP runtime + * variables. It is optional and enabled only when `MCP_MIXED_STRESS_ENABLE_CAP_CHURN` + * is true. + * + * @param cl TAP command-line configuration. + * @param stop Stop flag set by main thread. + * @param processlist_caps Ordered processlist cap profile. + * @param show_queries_caps Ordered show_queries cap profile. + * @param interval_ms Delay between cap updates. + */ +void run_mcp_cap_churner( + const CommandLine& cl, + const std::atomic& stop, + const std::vector& processlist_caps, + const std::vector& show_queries_caps, + int interval_ms +) { + if (processlist_caps.empty() || show_queries_caps.empty()) { + return; + } + + std::string connect_error; + MYSQLConnPtr admin_conn = create_mysql_connection( + cl.admin_host, + cl.admin_port, + cl.admin_username, + cl.admin_password, + connect_error + ); + if (!admin_conn) { + diag("MCP cap churner: cannot open admin connection: %s", connect_error.c_str()); + return; + } + + size_t idx = 0; + while (!stop.load(std::memory_order_relaxed)) { + const int processlist_cap = processlist_caps[idx % processlist_caps.size()]; + const int show_queries_cap = show_queries_caps[idx % show_queries_caps.size()]; + + const bool ok = + run_admin_stmt( + admin_conn.get(), + "SET mcp-stats_show_processlist_max_rows=" + std::to_string(processlist_cap), + "MCP cap churner" + ) && + run_admin_stmt( + admin_conn.get(), + "SET mcp-stats_show_queries_max_rows=" + std::to_string(show_queries_cap), + "MCP cap churner" + ) && + run_admin_stmt(admin_conn.get(), "LOAD MCP VARIABLES TO RUNTIME", "MCP cap churner"); + + if (!ok) { + diag( + "MCP cap churner: failed updating caps to processlist=%d show_queries=%d", + processlist_cap, + show_queries_cap + ); + } + + ++idx; + std::this_thread::sleep_for(std::chrono::milliseconds(interval_ms)); + } +} + /** * @brief MCP poller validating `stats.show_processlist` for a specific protocol. * * @param cl TAP command-line configuration. * @param db_type MCP db_type (`mysql` or `pgsql`). * @param filter_token Token expected in filtered `info` values. + * @param accepted_caps Cap values accepted in metadata assertions. + * @param dynamic_cap_check Whether `limit_cap` can vary across calls. * @param stop Stop flag set by main thread. * @param stats Shared processlist poll counters. */ @@ -700,6 +874,8 @@ void run_processlist_poller( const CommandLine& cl, const std::string& db_type, const std::string& filter_token, + const std::vector& accepted_caps, + bool dynamic_cap_check, const std::atomic& stop, processlist_poll_stats_t& stats ) { @@ -733,9 +909,17 @@ void run_processlist_poller( const int requested_limit = result_obj.value("requested_limit", -1); const int effective_limit = result_obj.value("effective_limit", -1); const int limit_cap = result_obj.value("limit_cap", -1); + + bool cap_match = false; + if (dynamic_cap_check) { + cap_match = std::find(accepted_caps.begin(), accepted_caps.end(), limit_cap) != accepted_caps.end(); + } else { + cap_match = !accepted_caps.empty() && (limit_cap == accepted_caps.back()); + } + if (!(requested_limit == k_processlist_requested_limit && effective_limit >= 0 && - limit_cap == k_processlist_cap && + cap_match && effective_limit <= limit_cap)) { stats.metadata_failures.fetch_add(1, std::memory_order_relaxed); } @@ -807,6 +991,8 @@ void run_processlist_poller( * @param cl TAP command-line configuration. * @param db_type MCP db_type (`mysql` or `pgsql`). * @param filter_token Token expected in filtered `digest_text` values. + * @param accepted_caps Cap values accepted in metadata assertions. + * @param dynamic_cap_check Whether `limit_cap` can vary across calls. * @param stop Stop flag set by main thread. * @param stats Shared show_queries poll counters. */ @@ -814,6 +1000,8 @@ void run_show_queries_poller( const CommandLine& cl, const std::string& db_type, const std::string& filter_token, + const std::vector& accepted_caps, + bool dynamic_cap_check, const std::atomic& stop, show_queries_poll_stats_t& stats ) { @@ -846,9 +1034,17 @@ void run_show_queries_poller( const int requested_limit = result_obj.value("requested_limit", -1); const int effective_limit = result_obj.value("effective_limit", -1); const int limit_cap = result_obj.value("limit_cap", -1); + + bool cap_match = false; + if (dynamic_cap_check) { + cap_match = std::find(accepted_caps.begin(), accepted_caps.end(), limit_cap) != accepted_caps.end(); + } else { + cap_match = !accepted_caps.empty() && (limit_cap == accepted_caps.back()); + } + if (!(requested_limit == k_show_queries_requested_limit && effective_limit >= 0 && - limit_cap == k_show_queries_cap && + cap_match && effective_limit <= limit_cap)) { stats.metadata_failures.fetch_add(1, std::memory_order_relaxed); } @@ -924,6 +1120,61 @@ int main(int argc, char** argv) { return exit_status(); } + const int mysql_worker_threads = env_int_clamped( + "MCP_MIXED_STRESS_MYSQL_WORKERS", + k_default_mysql_worker_threads, + 1, + k_max_worker_threads + ); + const int pgsql_worker_threads = env_int_clamped( + "MCP_MIXED_STRESS_PGSQL_WORKERS", + k_default_pgsql_worker_threads, + 1, + k_max_worker_threads + ); + const int runtime_seconds = env_int_clamped( + "MCP_MIXED_STRESS_RUNTIME_SEC", + k_default_runtime_seconds, + 1, + k_max_runtime_seconds + ); + const int processlist_cap_max = env_int_clamped( + "MCP_MIXED_STRESS_PROCESSLIST_CAP", + k_default_processlist_cap, + k_min_processlist_cap, + 1000 + ); + const int show_queries_cap_max = env_int_clamped( + "MCP_MIXED_STRESS_SHOW_QUERIES_CAP", + k_default_show_queries_cap, + k_min_show_queries_cap, + 1000 + ); + const bool cap_churn_enabled = env_bool("MCP_MIXED_STRESS_ENABLE_CAP_CHURN", false); + const int cap_churn_interval_ms = env_int_clamped( + "MCP_MIXED_STRESS_CAP_CHURN_INTERVAL_MS", + k_default_cap_churn_interval_ms, + k_min_cap_churn_interval_ms, + k_max_cap_churn_interval_ms + ); + + const std::vector processlist_cap_profile = cap_churn_enabled + ? build_cap_profile(processlist_cap_max, k_min_processlist_cap) + : std::vector{processlist_cap_max}; + const std::vector show_queries_cap_profile = cap_churn_enabled + ? build_cap_profile(show_queries_cap_max, k_min_show_queries_cap) + : std::vector{show_queries_cap_max}; + + diag( + "Mixed MCP stress config: runtime=%ds mysql_workers=%d pgsql_workers=%d cap_churn=%d processlist_cap_max=%d show_queries_cap_max=%d", + runtime_seconds, + mysql_worker_threads, + pgsql_worker_threads, + cap_churn_enabled ? 1 : 0, + processlist_cap_max, + show_queries_cap_max + ); + MYSQL* admin = nullptr; bool can_continue = true; @@ -935,7 +1186,12 @@ int main(int argc, char** argv) { } if (can_continue) { - const bool configured = configure_mcp_runtime(admin, cl); + const bool configured = configure_mcp_runtime( + admin, + cl, + processlist_cap_profile.back(), + show_queries_cap_profile.back() + ); ok(configured, "Configured MCP runtime for mixed MySQL+PgSQL stress"); if (!configured) { skip(45, "Cannot continue without MCP runtime configuration"); @@ -1023,9 +1279,9 @@ int main(int argc, char** argv) { if (can_continue) { std::atomic stop {false}; std::vector workers; - workers.reserve(static_cast(k_mysql_worker_threads + k_pgsql_worker_threads)); + workers.reserve(static_cast(mysql_worker_threads + pgsql_worker_threads)); - for (int i = 0; i < k_mysql_worker_threads; ++i) { + for (int i = 0; i < mysql_worker_threads; ++i) { workers.emplace_back( run_mysql_worker, i, @@ -1035,7 +1291,7 @@ int main(int argc, char** argv) { std::ref(mysql_workload_stats) ); } - for (int i = 0; i < k_pgsql_worker_threads; ++i) { + for (int i = 0; i < pgsql_worker_threads; ++i) { workers.emplace_back( run_pgsql_worker, i, @@ -1051,6 +1307,8 @@ int main(int argc, char** argv) { std::cref(cl), std::string("mysql"), std::string("SLEEP("), + std::cref(processlist_cap_profile), + cap_churn_enabled, std::cref(stop), std::ref(mysql_processlist_stats) ); @@ -1059,6 +1317,8 @@ int main(int argc, char** argv) { std::cref(cl), std::string("pgsql"), std::string("pg_sleep"), + std::cref(processlist_cap_profile), + cap_churn_enabled, std::cref(stop), std::ref(pgsql_processlist_stats) ); @@ -1067,6 +1327,8 @@ int main(int argc, char** argv) { std::cref(cl), std::string("mysql"), std::string("SLEEP"), + std::cref(show_queries_cap_profile), + cap_churn_enabled, std::cref(stop), std::ref(mysql_show_queries_stats) ); @@ -1075,11 +1337,25 @@ int main(int argc, char** argv) { std::cref(cl), std::string("pgsql"), std::string("PG_SLEEP"), + std::cref(show_queries_cap_profile), + cap_churn_enabled, std::cref(stop), std::ref(pgsql_show_queries_stats) ); - std::this_thread::sleep_for(std::chrono::seconds(k_runtime_seconds)); + std::thread cap_churner; + if (cap_churn_enabled) { + cap_churner = std::thread( + run_mcp_cap_churner, + std::cref(cl), + std::cref(stop), + std::cref(processlist_cap_profile), + std::cref(show_queries_cap_profile), + cap_churn_interval_ms + ); + } + + std::this_thread::sleep_for(std::chrono::seconds(runtime_seconds)); stop.store(true, std::memory_order_relaxed); for (auto& t : workers) { @@ -1099,6 +1375,9 @@ int main(int argc, char** argv) { if (pgsql_show_queries_poller.joinable()) { pgsql_show_queries_poller.join(); } + if (cap_churner.joinable()) { + cap_churner.join(); + } } if (can_continue) { @@ -1113,10 +1392,10 @@ int main(int argc, char** argv) { const uint64_t mysql_max_failed_queries = std::max(5, mysql_total_queries / 200); ok( - mysql_connected_workers >= static_cast(k_mysql_worker_threads / 2), + mysql_connected_workers >= static_cast(mysql_worker_threads / 2), "MySQL: at least half of worker connections succeeded (%llu/%d)", static_cast(mysql_connected_workers), - k_mysql_worker_threads + mysql_worker_threads ); ok( mysql_total_queries >= k_min_total_mysql_queries, @@ -1154,10 +1433,10 @@ int main(int argc, char** argv) { const uint64_t pg_max_failed_queries = std::max(5, pg_total_queries / 200); ok( - pg_connected_workers >= static_cast(k_pgsql_worker_threads / 2), + pg_connected_workers >= static_cast(pgsql_worker_threads / 2), "PgSQL: at least half of worker connections succeeded (%llu/%d)", static_cast(pg_connected_workers), - k_pgsql_worker_threads + pgsql_worker_threads ); ok( pg_total_queries >= k_min_total_pgsql_queries, diff --git a/test/tap/tests/mcp_mixed_stats_cap_churn-t.cpp b/test/tap/tests/mcp_mixed_stats_cap_churn-t.cpp new file mode 100644 index 000000000..1684b81fe --- /dev/null +++ b/test/tap/tests/mcp_mixed_stats_cap_churn-t.cpp @@ -0,0 +1,151 @@ +/** + * @file mcp_mixed_stats_cap_churn-t.cpp + * @brief TAP focused on live MCP cap churn during mixed MySQL+PgSQL load. + * + * This test runs `mcp_mixed_mysql_pgsql_concurrency_stress-t` in churn-enabled + * modes where `mcp-stats_show_processlist_max_rows` and + * `mcp-stats_show_queries_max_rows` are updated repeatedly while mixed protocol + * traffic and MCP polling are in progress. + */ + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "tap.h" + +namespace { + +/** + * @brief Cap-churn execution profile for child mixed-stress runs. + */ +struct churn_profile_t { + std::string name; + int runtime_sec; + int mysql_workers; + int pgsql_workers; + int processlist_cap; + int show_queries_cap; + int churn_interval_ms; +}; + +/** + * @brief Convert `system()` status to a normalized exit code. + * + * @param rc Raw `system()` return code. + * @return Process exit code, signal-mapped code, or -1 on launcher failure. + */ +int normalize_system_rc(int rc) { + if (rc == -1) { + return -1; + } + if (WIFEXITED(rc)) { + return WEXITSTATUS(rc); + } + if (WIFSIGNALED(rc)) { + return 128 + WTERMSIG(rc); + } + return rc; +} + +/** + * @brief Print the tail of a log file via TAP diagnostics. + * + * @param path Log file path. + * @param max_lines Maximum number of lines to print. + */ +void diag_log_tail(const std::string& path, size_t max_lines) { + std::ifstream in(path); + if (!in.good()) { + diag("Cannot open log file: %s", path.c_str()); + return; + } + + std::deque tail; + std::string line; + while (std::getline(in, line)) { + tail.push_back(line); + if (tail.size() > max_lines) { + tail.pop_front(); + } + } + + diag("---- child log tail (%s) ----", path.c_str()); + for (const auto& l : tail) { + diag("%s", l.c_str()); + } + diag("---- end child log tail ----"); +} + +/** + * @brief Execute one cap-churn profile as a child process. + * + * @param profile Churn configuration. + * @param log_path Output log path used by the child command. + * @return Normalized child exit code. + */ +int run_churn_profile(const churn_profile_t& profile, std::string& log_path) { + std::ostringstream log_name; + log_name << "/tmp/mcp_mixed_cap_churn_" << profile.name << "_" << getpid() << ".log"; + log_path = log_name.str(); + + std::ostringstream cmd; + cmd << "TAP_QUIET_ENVLOAD=1 " + << "MCP_MIXED_STRESS_RUNTIME_SEC=" << profile.runtime_sec << " " + << "MCP_MIXED_STRESS_MYSQL_WORKERS=" << profile.mysql_workers << " " + << "MCP_MIXED_STRESS_PGSQL_WORKERS=" << profile.pgsql_workers << " " + << "MCP_MIXED_STRESS_PROCESSLIST_CAP=" << profile.processlist_cap << " " + << "MCP_MIXED_STRESS_SHOW_QUERIES_CAP=" << profile.show_queries_cap << " " + << "MCP_MIXED_STRESS_ENABLE_CAP_CHURN=1 " + << "MCP_MIXED_STRESS_CAP_CHURN_INTERVAL_MS=" << profile.churn_interval_ms << " " + << "./mcp_mixed_mysql_pgsql_concurrency_stress-t" + << " > " << log_path << " 2>&1"; + + const int rc = std::system(cmd.str().c_str()); + return normalize_system_rc(rc); +} + +} // namespace + +int main() { + plan(4); + + const bool child_available = (access("./mcp_mixed_mysql_pgsql_concurrency_stress-t", X_OK) == 0); + ok(child_available, "Child mixed-stress binary is available"); + if (!child_available) { + skip(3, "Cannot run cap-churn scenarios without child mixed-stress binary"); + return exit_status(); + } + + const std::vector profiles = { + {"moderate", 7, 10, 10, 120, 180, 200}, + {"aggressive", 9, 12, 12, 120, 180, 90} + }; + + bool all_ok = true; + for (const auto& profile : profiles) { + std::string log_path; + const int exit_code = run_churn_profile(profile, log_path); + const bool passed = (exit_code == 0); + ok( + passed, + "Cap-churn profile `%s` completed successfully (exit_code=%d)", + profile.name.c_str(), + exit_code + ); + if (!passed) { + diag_log_tail(log_path, 140); + all_ok = false; + } + } + + ok(all_ok, "All MCP cap-churn mixed-load scenarios passed"); + + return exit_status(); +} diff --git a/test/tap/tests/mcp_mixed_stats_profile_matrix-t.cpp b/test/tap/tests/mcp_mixed_stats_profile_matrix-t.cpp new file mode 100644 index 000000000..eb2a588b9 --- /dev/null +++ b/test/tap/tests/mcp_mixed_stats_profile_matrix-t.cpp @@ -0,0 +1,156 @@ +/** + * @file mcp_mixed_stats_profile_matrix-t.cpp + * @brief TAP matrix runner for MCP mixed MySQL+PgSQL stress profiles. + * + * This test executes `mcp_mixed_mysql_pgsql_concurrency_stress-t` multiple times + * using different runtime/load/churn profiles. The objective is validating that + * MCP stats remains stable across quick, churn-enabled, and heavier mixed-load + * scenarios without duplicating the full workload implementation. + */ + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "tap.h" + +namespace { + +/** + * @brief Stress profile used to parameterize a child mixed-stress run. + */ +struct stress_profile_t { + std::string name; + int runtime_sec; + int mysql_workers; + int pgsql_workers; + int processlist_cap; + int show_queries_cap; + int cap_churn_enabled; + int cap_churn_interval_ms; +}; + +/** + * @brief Convert `system()` status to a normalized exit code. + * + * @param rc Raw `system()` return code. + * @return Process exit code, signal-mapped code, or -1 on launcher failure. + */ +int normalize_system_rc(int rc) { + if (rc == -1) { + return -1; + } + if (WIFEXITED(rc)) { + return WEXITSTATUS(rc); + } + if (WIFSIGNALED(rc)) { + return 128 + WTERMSIG(rc); + } + return rc; +} + +/** + * @brief Print the tail of a log file via TAP diagnostics. + * + * @param path Log file path. + * @param max_lines Maximum number of lines to print. + */ +void diag_log_tail(const std::string& path, size_t max_lines) { + std::ifstream in(path); + if (!in.good()) { + diag("Cannot open log file: %s", path.c_str()); + return; + } + + std::deque tail; + std::string line; + while (std::getline(in, line)) { + tail.push_back(line); + if (tail.size() > max_lines) { + tail.pop_front(); + } + } + + diag("---- child log tail (%s) ----", path.c_str()); + for (const auto& l : tail) { + diag("%s", l.c_str()); + } + diag("---- end child log tail ----"); +} + +/** + * @brief Execute one mixed-stress profile as a child process. + * + * The child output is redirected to a profile-specific log file to keep TAP + * output from the parent test protocol-clean. + * + * @param profile Profile configuration. + * @param log_path Output log path used by the child command. + * @return Normalized child exit code. + */ +int run_profile(const stress_profile_t& profile, std::string& log_path) { + std::ostringstream log_name; + log_name << "/tmp/mcp_mixed_profile_" << profile.name << "_" << getpid() << ".log"; + log_path = log_name.str(); + + std::ostringstream cmd; + cmd << "TAP_QUIET_ENVLOAD=1 " + << "MCP_MIXED_STRESS_RUNTIME_SEC=" << profile.runtime_sec << " " + << "MCP_MIXED_STRESS_MYSQL_WORKERS=" << profile.mysql_workers << " " + << "MCP_MIXED_STRESS_PGSQL_WORKERS=" << profile.pgsql_workers << " " + << "MCP_MIXED_STRESS_PROCESSLIST_CAP=" << profile.processlist_cap << " " + << "MCP_MIXED_STRESS_SHOW_QUERIES_CAP=" << profile.show_queries_cap << " " + << "MCP_MIXED_STRESS_ENABLE_CAP_CHURN=" << profile.cap_churn_enabled << " " + << "MCP_MIXED_STRESS_CAP_CHURN_INTERVAL_MS=" << profile.cap_churn_interval_ms << " " + << "./mcp_mixed_mysql_pgsql_concurrency_stress-t" + << " > " << log_path << " 2>&1"; + + const int rc = std::system(cmd.str().c_str()); + return normalize_system_rc(rc); +} + +} // namespace + +int main() { + plan(5); + + const bool child_available = (access("./mcp_mixed_mysql_pgsql_concurrency_stress-t", X_OK) == 0); + ok(child_available, "Child mixed-stress binary is available"); + if (!child_available) { + skip(4, "Cannot run profile matrix without child mixed-stress binary"); + return exit_status(); + } + + const std::vector profiles = { + {"quick", 6, 8, 8, 120, 180, 0, 700}, + {"churn", 6, 8, 8, 120, 180, 1, 250}, + {"heavy", 8, 14, 14, 120, 180, 1, 120} + }; + + bool all_ok = true; + for (const auto& profile : profiles) { + std::string log_path; + const int exit_code = run_profile(profile, log_path); + const bool passed = (exit_code == 0); + ok( + passed, + "Profile `%s` completed successfully (exit_code=%d)", + profile.name.c_str(), + exit_code + ); + if (!passed) { + diag_log_tail(log_path, 120); + all_ok = false; + } + } + + ok(all_ok, "All MCP mixed-stress profiles passed"); + + return exit_status(); +}